diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index d7a7997a..6cf4d0e2 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -294,6 +294,7 @@ DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV21.xcdatamodel; sourceTree = ""; }; DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsHandler.swift; sourceTree = ""; }; + DD398EBD2B93F640002B4C51 /* MeshtasticDataModelV 29.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 29.xcdatamodel"; sourceTree = ""; }; DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareChannels.swift; sourceTree = ""; }; DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModel.xcdatamodel; sourceTree = ""; }; DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryGauge.swift; sourceTree = ""; }; @@ -1571,7 +1572,7 @@ CODE_SIGN_ENTITLEMENTS = Meshtastic/Meshtastic.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 842; DEVELOPMENT_ASSET_PATHS = "\"Meshtastic/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -1583,7 +1584,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.25; + MARKETING_VERSION = 2.3.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1605,7 +1606,7 @@ CODE_SIGN_ENTITLEMENTS = Meshtastic/Meshtastic.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 842; DEVELOPMENT_ASSET_PATHS = "\"Meshtastic/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -1617,7 +1618,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.25; + MARKETING_VERSION = 2.3.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1727,7 +1728,7 @@ CODE_SIGN_ENTITLEMENTS = Widgets/WidgetsExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 841; DEVELOPMENT_TEAM = GCH7VS5Y9R; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Widgets/Info.plist; @@ -1739,7 +1740,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.25; + MARKETING_VERSION = 2.3.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1760,7 +1761,7 @@ CODE_SIGN_ENTITLEMENTS = Widgets/WidgetsExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 841; DEVELOPMENT_TEAM = GCH7VS5Y9R; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Widgets/Info.plist; @@ -1772,7 +1773,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.25; + MARKETING_VERSION = 2.3.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1883,6 +1884,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD398EBD2B93F640002B4C51 /* MeshtasticDataModelV 29.xcdatamodel */, DD0E20FF2B892E1300F2D100 /* MeshtasticDataModelV 28.xcdatamodel */, D93069062B81D8900066FBC8 /* MeshtasticDataModelV 27.xcdatamodel */, DD05296F2B77F454008E44CD /* MeshtasticDataModelV 26.xcdatamodel */, @@ -1912,7 +1914,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD0E20FF2B892E1300F2D100 /* MeshtasticDataModelV 28.xcdatamodel */; + currentVersion = DD398EBD2B93F640002B4C51 /* MeshtasticDataModelV 29.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index b1b1785b..39cc9b89 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -27,6 +27,8 @@ extension UserDefaults { case enableDetectionNotifications case detectionSensorRole case enableSmartPosition + case modemPreset + case firmwareVersion } func reset() { @@ -202,4 +204,20 @@ extension UserDefaults { UserDefaults.standard.set(newValue, forKey: "enableSmartPosition") } } + static var modemPreset: Int { + get { + UserDefaults.standard.integer(forKey: "modemPreset") + } + set { + UserDefaults.standard.set(newValue, forKey: "modemPreset") + } + } + static var firmwareVersion: String { + get { + UserDefaults.standard.string(forKey: "firmwareVersion") ?? "0.0.0" + } + set { + UserDefaults.standard.set(newValue, forKey: "firmwareVersion") + } + } } diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index a0e6af59..5d85d325 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -582,6 +582,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate nowKnown = true connectedVersion = String(version.dropLast()) appState.firmwareVersion = connectedVersion + UserDefaults.firmwareVersion = connectedVersion } let supportedVersion = connectedVersion == "0.0.0" || self.minimumVersion.compare(connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(connectedVersion, options: .numeric) == .orderedSame if !supportedVersion { @@ -607,11 +608,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate case .adminApp: adminAppPacket(packet: decodedInfo.packet, context: context!) case .replyApp: - MeshLogger.log("🕸️ MESH PACKET received for Reply App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for Reply App handling as a text message") + textMessageAppPacket(packet: decodedInfo.packet, wantRangeTestPackets: wantRangeTestPackets, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) case .ipTunnelApp: - MeshLogger.log("🕸️ MESH PACKET received for IP Tunnel App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + //MeshLogger.log("🕸️ MESH PACKET received for IP Tunnel App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for IP Tunnel App UNHANDLED UNHANDLED") case .serialApp: - MeshLogger.log("🕸️ MESH PACKET received for Serial App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + //MeshLogger.log("🕸️ MESH PACKET received for Serial App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for Serial App UNHANDLED UNHANDLED") case .storeForwardApp: if wantStoreAndForwardPackets { storeAndForwardPacket(packet: decodedInfo.packet, connectedNodeNum: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) @@ -628,17 +632,23 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate case .telemetryApp: if !invalidVersion { telemetryPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) } case .textMessageCompressedApp: - MeshLogger.log("🕸️ MESH PACKET received for Text Message Compressed App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + //MeshLogger.log("🕸️ MESH PACKET received for Text Message Compressed App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for Text Message Compressed App UNHANDLED") case .zpsApp: - MeshLogger.log("🕸️ MESH PACKET received for ZPS App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + // MeshLogger.log("🕸️ MESH PACKET received for Zero Positioning System App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for Zero Positioning System App UNHANDLED") case .privateApp: - MeshLogger.log("🕸️ MESH PACKET received for Private App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + //MeshLogger.log("🕸️ MESH PACKET received for Private App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for Private App UNHANDLED UNHANDLED") case .atakForwarder: - MeshLogger.log("🕸️ MESH PACKET received for ATAK Forwarder App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + //MeshLogger.log("🕸️ MESH PACKET received for ATAK Forwarder App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for ATAK Forwarder App UNHANDLED UNHANDLED") case .simulatorApp: - MeshLogger.log("🕸️ MESH PACKET received for Simulator App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + //MeshLogger.log("🕸️ MESH PACKET received for Simulator App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for Simulator App UNHANDLED UNHANDLED") case .audioApp: - MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + //MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED UNHANDLED") case .tracerouteApp: if let routingMessage = try? RouteDiscovery(serializedData: decodedInfo.packet.decoded.payload) { let traceRoute = getTraceRoute(id: Int64(decodedInfo.packet.decoded.requestID), context: context!) @@ -1030,9 +1040,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } catch { return false } - return false - - var meshPacket = MeshPacket() meshPacket.to = UInt32(destNum) diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 82714d62..6ec44b90 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -60,14 +60,10 @@ import CoreLocation self.isStationary = update.isStationary var locationAdded: Bool - if enableSmartPosition { - locationAdded = addLocation(loc) - //print("Added Location \(self.count): \(loc)") - } else { - locationsArray.append(loc) - locationAdded = true - } - if locationAdded { + locationAdded = addLocation(loc, smartPostion: enableSmartPosition) + if !isRecording && locationAdded { + self.count = 1 + } else if locationAdded && isRecording { self.count += 1 } } @@ -84,19 +80,21 @@ import CoreLocation self.updatesStarted = false } - func addLocation(_ location: CLLocation) -> Bool { - let age = -location.timestamp.timeIntervalSinceNow - if age > 10 { - print("Bad Location \(self.count): Too Old \(age) seconds ago \(location)") - return false - } - if location.horizontalAccuracy < 0 { - print("Bad Location \(self.count): Horizontal Accuracy: \(location.horizontalAccuracy) \(location)") - return false - } - if location.horizontalAccuracy > 25 { - print("Bad Location \(self.count): Horizontal Accuracy: \(location.horizontalAccuracy) \(location)") - return false + func addLocation(_ location: CLLocation, smartPostion: Bool) -> Bool { + if smartPostion { + let age = -location.timestamp.timeIntervalSinceNow + if age > 10 { + print("Bad Location \(self.count): Too Old \(age) seconds ago \(location)") + return false + } + if location.horizontalAccuracy < 0 { + print("Bad Location \(self.count): Horizontal Accuracy: \(location.horizontalAccuracy) \(location)") + return false + } + if location.horizontalAccuracy > 25 { + print("Bad Location \(self.count): Horizontal Accuracy: \(location.horizontalAccuracy) \(location)") + return false + } } if isRecording { if let lastLocation = locationsArray.last { @@ -107,8 +105,10 @@ import CoreLocation elevationGain += gain } } + locationsArray.append(location) + } else { + locationsArray = [location] } - locationsArray.append(location) return true } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index ad763b23..c02835e7 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -775,7 +775,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connectedNode: Int64, storeForward: Bool = false, context: NSManagedObjectContext) { var messageText = String(bytes: packet.decoded.payload, encoding: .utf8) - if !wantRangeTestPackets && ((messageText?.starts(with: "seq ")) != nil) { + if !wantRangeTestPackets && (String(messageText ?? "seq ").starts(with: "seq ")) { return } var storeForwardBroadcast = false diff --git a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift index 1196e1b5..5a1a33d4 100644 --- a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift +++ b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift @@ -21,7 +21,7 @@ class MqttClientProxyManager { private static let defaultKeepAliveInterval: Int32 = 60 weak var delegate: MqttClientProxyManagerDelegate? var mqttClientProxy: CocoaMQTT? - var topic = "msh/2/c" + var topic = "msh" var debugLog = false func connectFromConfigSettings(node: NodeInfoEntity) { let defaultServerAddress = "mqtt.meshtastic.org" @@ -36,13 +36,16 @@ class MqttClientProxyManager { defaultServerPort = Int(fullHost.components(separatedBy: ":")[1]) ?? (useSsl ? 8883 : 1883) } } + let minimumVersion = "2.3.0" + let latestVersion = minimumVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedSame + if let host = host { let port = defaultServerPort let username = node.mqttConfig?.username let password = node.mqttConfig?.password let root = node.mqttConfig?.root?.count ?? 0 > 0 ? node.mqttConfig?.root : "msh" - let prefix = root! + "/2/c" - topic = prefix + "/#" + let prefix = root! + topic = prefix + (latestVersion ? "/2/e" : "/2/c") + "/#" let qos = CocoaMQTTQoS(rawValue: UInt8(1))! connect(host: host, port: port, useSsl: useSsl, username: username, password: password, topic: topic, qos: qos, cleanSession: true) } diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 37d79244..a647b881 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 28.xcdatamodel + MeshtasticDataModelV 29.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 28.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 28.xcdatamodel/contents index d3da3c8d..348b7fea 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 28.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 28.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -230,8 +230,8 @@ - - + + @@ -239,31 +239,31 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -422,8 +422,8 @@ - - + + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 29.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 29.xcdatamodel/contents new file mode 100644 index 00000000..aed0e3e0 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 29.xcdatamodel/contents @@ -0,0 +1,449 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index c6b290a5..cdd0d91e 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -147,11 +147,24 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newNode.channel = Int32(packet.channel) if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { newNode.channel = Int32(nodeInfoMessage.channel) - print(packet.channel) - print("Channel From Message\(nodeInfoMessage.channel)") + newNode.hopsAway = Int32(truncatingIfNeeded: nodeInfoMessage.hopsAway) } if let newUserMessage = try? User(serializedData: packet.decoded.payload) { - let newUser = UserEntity(context: context) + + if newUserMessage.id.isEmpty { + let newUser = UserEntity(context: context) + newUser.num = Int64(packet.from) + let userId = String(format:"%2X", packet.from) + newUser.userId = "!\(userId)" + let last4 = String(userId.suffix(4)) + newUser.longName = "Meshtastic \(last4)" + newUser.shortName = last4 + newUser.hwModel = "UNSET" + newNode.user = newUser + + } else { + + let newUser = UserEntity(context: context) newUser.userId = newUserMessage.id newUser.num = Int64(packet.from) newUser.longName = newUserMessage.longName @@ -159,6 +172,17 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newUser.role = Int32(newUserMessage.role.rawValue) newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased() newNode.user = newUser + } + } else { + let newUser = UserEntity(context: context) + newUser.num = Int64(packet.from) + let userId = String(format:"%2X", packet.from) + newUser.userId = "!\(userId)" + let last4 = String(userId.suffix(4)) + newUser.longName = "Meshtastic \(last4)" + newUser.shortName = last4 + newUser.hwModel = "UNSET" + newNode.user = newUser } if newNode.user == nil { @@ -177,7 +201,6 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) print("💥 Error Inserting New Core Data MyInfoEntity: \(nsError)") } newNode.myInfo = myInfoEntity - //newNode.objectWillChange.send() } else { // Update an existing node @@ -193,6 +216,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { fetchedNode[0].channel = Int32(nodeInfoMessage.channel) + fetchedNode[0].hopsAway = Int32(truncatingIfNeeded: nodeInfoMessage.hopsAway) if nodeInfoMessage.hasDeviceMetrics { let telemetry = TelemetryEntity(context: context) telemetry.batteryLevel = Int32(nodeInfoMessage.deviceMetrics.batteryLevel) @@ -211,20 +235,19 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].user!.shortName = nodeInfoMessage.user.shortName fetchedNode[0].user!.role = Int32(nodeInfoMessage.user.role.rawValue) fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() - } else { - if (fetchedNode[0].user == nil) { - let newUser = UserEntity(context: context) - newUser.num = Int64(nodeInfoMessage.num) - let userId = String(format:"%2X", nodeInfoMessage.num) - newUser.userId = "!\(userId)" - let last4 = String(userId.suffix(4)) - newUser.longName = "Meshtastic \(last4)" - newUser.shortName = last4 - newUser.hwModel = "UNSET" - fetchedNode[0].user! = newUser - } } } + if (fetchedNode[0].user == nil) { + let newUser = UserEntity(context: context) + newUser.num = Int64(packet.from) + let userId = String(format:"%2X", packet.from) + newUser.userId = "!\(userId)" + let last4 = String(userId.suffix(4)) + newUser.longName = "Meshtastic \(last4)" + newUser.shortName = last4 + newUser.hwModel = "UNSET" + fetchedNode[0].user! = newUser + } do { try context.save() print("💾 Updated NodeInfo from Node Info App Packet For: \(fetchedNode[0].num)") diff --git a/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift b/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift index 821b9370..048c99aa 100644 --- a/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift @@ -254,6 +254,20 @@ struct NodeInfoLite { set {_uniqueStorage()._channel = newValue} } + /// + /// True if we witnessed the node over MQTT instead of LoRA transport + var viaMqtt: Bool { + get {return _storage._viaMqtt} + set {_uniqueStorage()._viaMqtt = newValue} + } + + /// + /// Number of hops away from us this node is (0 if adjacent) + var hopsAway: UInt32 { + get {return _storage._hopsAway} + set {_uniqueStorage()._hopsAway = newValue} + } + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -583,6 +597,8 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 5: .standard(proto: "last_heard"), 6: .standard(proto: "device_metrics"), 7: .same(proto: "channel"), + 8: .standard(proto: "via_mqtt"), + 9: .standard(proto: "hops_away"), ] fileprivate class _StorageClass { @@ -593,6 +609,8 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat var _lastHeard: UInt32 = 0 var _deviceMetrics: DeviceMetrics? = nil var _channel: UInt32 = 0 + var _viaMqtt: Bool = false + var _hopsAway: UInt32 = 0 static let defaultInstance = _StorageClass() @@ -606,6 +624,8 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat _lastHeard = source._lastHeard _deviceMetrics = source._deviceMetrics _channel = source._channel + _viaMqtt = source._viaMqtt + _hopsAway = source._hopsAway } } @@ -631,6 +651,8 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat case 5: try { try decoder.decodeSingularFixed32Field(value: &_storage._lastHeard) }() case 6: try { try decoder.decodeSingularMessageField(value: &_storage._deviceMetrics) }() case 7: try { try decoder.decodeSingularUInt32Field(value: &_storage._channel) }() + case 8: try { try decoder.decodeSingularBoolField(value: &_storage._viaMqtt) }() + case 9: try { try decoder.decodeSingularUInt32Field(value: &_storage._hopsAway) }() default: break } } @@ -664,6 +686,12 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat if _storage._channel != 0 { try visitor.visitSingularUInt32Field(value: _storage._channel, fieldNumber: 7) } + if _storage._viaMqtt != false { + try visitor.visitSingularBoolField(value: _storage._viaMqtt, fieldNumber: 8) + } + if _storage._hopsAway != 0 { + try visitor.visitSingularUInt32Field(value: _storage._hopsAway, fieldNumber: 9) + } } try unknownFields.traverse(visitor: &visitor) } @@ -680,6 +708,8 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat if _storage._lastHeard != rhs_storage._lastHeard {return false} if _storage._deviceMetrics != rhs_storage._deviceMetrics {return false} if _storage._channel != rhs_storage._channel {return false} + if _storage._viaMqtt != rhs_storage._viaMqtt {return false} + if _storage._hopsAway != rhs_storage._hopsAway {return false} return true } if !storagesAreEqual {return false} diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index caa1f1bc..bdc8da97 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -1434,8 +1434,7 @@ struct MeshPacket { } /// - /// The (immediatSee Priority description for more details.y should be fixed32 instead, this encoding only - /// hurts the ble link though. + /// The (immediate) destination for this packet var to: UInt32 { get {return _storage._to} set {_uniqueStorage()._to = newValue} @@ -1566,6 +1565,14 @@ struct MeshPacket { set {_uniqueStorage()._viaMqtt = newValue} } + /// + /// Hop limit with which the original packet started. Sent via LoRa using three bits in the unencrypted header. + /// When receiving a packet, the difference between hop_start and hop_limit gives how many hops it traveled. + var hopStart: UInt32 { + get {return _storage._hopStart} + set {_uniqueStorage()._hopStart = newValue} + } + var unknownFields = SwiftProtobuf.UnknownStorage() enum OneOf_PayloadVariant: Equatable { @@ -1779,62 +1786,86 @@ struct NodeInfo { /// /// The node number - var num: UInt32 = 0 + var num: UInt32 { + get {return _storage._num} + set {_uniqueStorage()._num = newValue} + } /// /// The user info for this node var user: User { - get {return _user ?? User()} - set {_user = newValue} + get {return _storage._user ?? User()} + set {_uniqueStorage()._user = newValue} } /// Returns true if `user` has been explicitly set. - var hasUser: Bool {return self._user != nil} + var hasUser: Bool {return _storage._user != nil} /// Clears the value of `user`. Subsequent reads from it will return its default value. - mutating func clearUser() {self._user = nil} + mutating func clearUser() {_uniqueStorage()._user = nil} /// /// This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true. /// Position.time now indicates the last time we received a POSITION from that node. var position: Position { - get {return _position ?? Position()} - set {_position = newValue} + get {return _storage._position ?? Position()} + set {_uniqueStorage()._position = newValue} } /// Returns true if `position` has been explicitly set. - var hasPosition: Bool {return self._position != nil} + var hasPosition: Bool {return _storage._position != nil} /// Clears the value of `position`. Subsequent reads from it will return its default value. - mutating func clearPosition() {self._position = nil} + mutating func clearPosition() {_uniqueStorage()._position = nil} /// /// Returns the Signal-to-noise ratio (SNR) of the last received message, /// as measured by the receiver. Return SNR of the last received message in dB - var snr: Float = 0 + var snr: Float { + get {return _storage._snr} + set {_uniqueStorage()._snr = newValue} + } /// /// Set to indicate the last time we received a packet from this node - var lastHeard: UInt32 = 0 + var lastHeard: UInt32 { + get {return _storage._lastHeard} + set {_uniqueStorage()._lastHeard = newValue} + } /// /// The latest device metrics for the node. var deviceMetrics: DeviceMetrics { - get {return _deviceMetrics ?? DeviceMetrics()} - set {_deviceMetrics = newValue} + get {return _storage._deviceMetrics ?? DeviceMetrics()} + set {_uniqueStorage()._deviceMetrics = newValue} } /// Returns true if `deviceMetrics` has been explicitly set. - var hasDeviceMetrics: Bool {return self._deviceMetrics != nil} + var hasDeviceMetrics: Bool {return _storage._deviceMetrics != nil} /// Clears the value of `deviceMetrics`. Subsequent reads from it will return its default value. - mutating func clearDeviceMetrics() {self._deviceMetrics = nil} + mutating func clearDeviceMetrics() {_uniqueStorage()._deviceMetrics = nil} /// /// local channel index we heard that node on. Only populated if its not the default channel. - var channel: UInt32 = 0 + var channel: UInt32 { + get {return _storage._channel} + set {_uniqueStorage()._channel = newValue} + } + + /// + /// True if we witnessed the node over MQTT instead of LoRA transport + var viaMqtt: Bool { + get {return _storage._viaMqtt} + set {_uniqueStorage()._viaMqtt = newValue} + } + + /// + /// Number of hops away from us this node is (0 if adjacent) + var hopsAway: UInt32 { + get {return _storage._hopsAway} + set {_uniqueStorage()._hopsAway = newValue} + } var unknownFields = SwiftProtobuf.UnknownStorage() init() {} - fileprivate var _user: User? = nil - fileprivate var _position: Position? = nil - fileprivate var _deviceMetrics: DeviceMetrics? = nil + fileprivate var _storage = _StorageClass.defaultInstance } /// @@ -3369,6 +3400,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio 12: .standard(proto: "rx_rssi"), 13: .same(proto: "delayed"), 14: .standard(proto: "via_mqtt"), + 15: .standard(proto: "hop_start"), ] fileprivate class _StorageClass { @@ -3385,6 +3417,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio var _rxRssi: Int32 = 0 var _delayed: MeshPacket.Delayed = .noDelay var _viaMqtt: Bool = false + var _hopStart: UInt32 = 0 static let defaultInstance = _StorageClass() @@ -3404,6 +3437,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio _rxRssi = source._rxRssi _delayed = source._delayed _viaMqtt = source._viaMqtt + _hopStart = source._hopStart } } @@ -3455,6 +3489,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio case 12: try { try decoder.decodeSingularInt32Field(value: &_storage._rxRssi) }() case 13: try { try decoder.decodeSingularEnumField(value: &_storage._delayed) }() case 14: try { try decoder.decodeSingularBoolField(value: &_storage._viaMqtt) }() + case 15: try { try decoder.decodeSingularUInt32Field(value: &_storage._hopStart) }() default: break } } @@ -3514,6 +3549,9 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if _storage._viaMqtt != false { try visitor.visitSingularBoolField(value: _storage._viaMqtt, fieldNumber: 14) } + if _storage._hopStart != 0 { + try visitor.visitSingularUInt32Field(value: _storage._hopStart, fieldNumber: 15) + } } try unknownFields.traverse(visitor: &visitor) } @@ -3536,6 +3574,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if _storage._rxRssi != rhs_storage._rxRssi {return false} if _storage._delayed != rhs_storage._delayed {return false} if _storage._viaMqtt != rhs_storage._viaMqtt {return false} + if _storage._hopStart != rhs_storage._hopStart {return false} return true } if !storagesAreEqual {return false} @@ -3575,63 +3614,123 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB 5: .standard(proto: "last_heard"), 6: .standard(proto: "device_metrics"), 7: .same(proto: "channel"), + 8: .standard(proto: "via_mqtt"), + 9: .standard(proto: "hops_away"), ] + fileprivate class _StorageClass { + var _num: UInt32 = 0 + var _user: User? = nil + var _position: Position? = nil + var _snr: Float = 0 + var _lastHeard: UInt32 = 0 + var _deviceMetrics: DeviceMetrics? = nil + var _channel: UInt32 = 0 + var _viaMqtt: Bool = false + var _hopsAway: UInt32 = 0 + + static let defaultInstance = _StorageClass() + + private init() {} + + init(copying source: _StorageClass) { + _num = source._num + _user = source._user + _position = source._position + _snr = source._snr + _lastHeard = source._lastHeard + _deviceMetrics = source._deviceMetrics + _channel = source._channel + _viaMqtt = source._viaMqtt + _hopsAway = source._hopsAway + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeSingularUInt32Field(value: &self.num) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._user) }() - case 3: try { try decoder.decodeSingularMessageField(value: &self._position) }() - case 4: try { try decoder.decodeSingularFloatField(value: &self.snr) }() - case 5: try { try decoder.decodeSingularFixed32Field(value: &self.lastHeard) }() - case 6: try { try decoder.decodeSingularMessageField(value: &self._deviceMetrics) }() - case 7: try { try decoder.decodeSingularUInt32Field(value: &self.channel) }() - default: break + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt32Field(value: &_storage._num) }() + case 2: try { try decoder.decodeSingularMessageField(value: &_storage._user) }() + case 3: try { try decoder.decodeSingularMessageField(value: &_storage._position) }() + case 4: try { try decoder.decodeSingularFloatField(value: &_storage._snr) }() + case 5: try { try decoder.decodeSingularFixed32Field(value: &_storage._lastHeard) }() + case 6: try { try decoder.decodeSingularMessageField(value: &_storage._deviceMetrics) }() + case 7: try { try decoder.decodeSingularUInt32Field(value: &_storage._channel) }() + case 8: try { try decoder.decodeSingularBoolField(value: &_storage._viaMqtt) }() + case 9: try { try decoder.decodeSingularUInt32Field(value: &_storage._hopsAway) }() + default: break + } } } } 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.num != 0 { - try visitor.visitSingularUInt32Field(value: self.num, fieldNumber: 1) - } - try { if let v = self._user { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try { if let v = self._position { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - if self.snr != 0 { - try visitor.visitSingularFloatField(value: self.snr, fieldNumber: 4) - } - if self.lastHeard != 0 { - try visitor.visitSingularFixed32Field(value: self.lastHeard, fieldNumber: 5) - } - try { if let v = self._deviceMetrics { - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - } }() - if self.channel != 0 { - try visitor.visitSingularUInt32Field(value: self.channel, fieldNumber: 7) + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + // 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 _storage._num != 0 { + try visitor.visitSingularUInt32Field(value: _storage._num, fieldNumber: 1) + } + try { if let v = _storage._user { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try { if let v = _storage._position { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + if _storage._snr != 0 { + try visitor.visitSingularFloatField(value: _storage._snr, fieldNumber: 4) + } + if _storage._lastHeard != 0 { + try visitor.visitSingularFixed32Field(value: _storage._lastHeard, fieldNumber: 5) + } + try { if let v = _storage._deviceMetrics { + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + } }() + if _storage._channel != 0 { + try visitor.visitSingularUInt32Field(value: _storage._channel, fieldNumber: 7) + } + if _storage._viaMqtt != false { + try visitor.visitSingularBoolField(value: _storage._viaMqtt, fieldNumber: 8) + } + if _storage._hopsAway != 0 { + try visitor.visitSingularUInt32Field(value: _storage._hopsAway, fieldNumber: 9) + } } try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: NodeInfo, rhs: NodeInfo) -> Bool { - if lhs.num != rhs.num {return false} - if lhs._user != rhs._user {return false} - if lhs._position != rhs._position {return false} - if lhs.snr != rhs.snr {return false} - if lhs.lastHeard != rhs.lastHeard {return false} - if lhs._deviceMetrics != rhs._deviceMetrics {return false} - if lhs.channel != rhs.channel {return false} + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._num != rhs_storage._num {return false} + if _storage._user != rhs_storage._user {return false} + if _storage._position != rhs_storage._position {return false} + if _storage._snr != rhs_storage._snr {return false} + if _storage._lastHeard != rhs_storage._lastHeard {return false} + if _storage._deviceMetrics != rhs_storage._deviceMetrics {return false} + if _storage._channel != rhs_storage._channel {return false} + if _storage._viaMqtt != rhs_storage._viaMqtt {return false} + if _storage._hopsAway != rhs_storage._hopsAway {return false} + return true + } + if !storagesAreEqual {return false} + } if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Protobufs/meshtastic/portnums.pb.swift b/Meshtastic/Protobufs/meshtastic/portnums.pb.swift index ea5ce5bd..937ff635 100644 --- a/Meshtastic/Protobufs/meshtastic/portnums.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/portnums.pb.swift @@ -58,25 +58,25 @@ enum PortNum: SwiftProtobuf.Enum { /// /// The built-in position messaging app. - /// Payload is a [Position](/docs/developers/protobufs/api#position) message + /// Payload is a Position message. /// ENCODING: Protobuf case positionApp // = 3 /// /// The built-in user info app. - /// Payload is a [User](/docs/developers/protobufs/api#user) message + /// Payload is a User message. /// ENCODING: Protobuf case nodeinfoApp // = 4 /// /// Protocol control packets for mesh protocol use. - /// Payload is a [Routing](/docs/developers/protobufs/api#routing) message + /// Payload is a Routing message. /// ENCODING: Protobuf case routingApp // = 5 /// /// Admin control packets. - /// Payload is a [AdminMessage](/docs/developers/protobufs/api#adminmessage) message + /// Payload is a AdminMessage message. /// ENCODING: Protobuf case adminApp // = 6 @@ -90,7 +90,7 @@ enum PortNum: SwiftProtobuf.Enum { /// /// Waypoint payloads. - /// Payload is a [Waypoint](/docs/developers/protobufs/api#waypoint) message + /// Payload is a Waypoint message. /// ENCODING: Protobuf case waypointApp // = 8 diff --git a/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift b/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift index debcff60..72d378bc 100644 --- a/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift @@ -84,6 +84,10 @@ enum TelemetrySensorType: SwiftProtobuf.Enum { /// /// INA3221 3 Channel Voltage / Current Sensor case ina3221 // = 14 + + /// + /// BMP085/BMP180 High accuracy temperature and pressure (older Version of BMP280) + case bmp085 // = 15 case UNRECOGNIZED(Int) init() { @@ -107,6 +111,7 @@ enum TelemetrySensorType: SwiftProtobuf.Enum { case 12: self = .sht31 case 13: self = .pmsa003I case 14: self = .ina3221 + case 15: self = .bmp085 default: self = .UNRECOGNIZED(rawValue) } } @@ -128,6 +133,7 @@ enum TelemetrySensorType: SwiftProtobuf.Enum { case .sht31: return 12 case .pmsa003I: return 13 case .ina3221: return 14 + case .bmp085: return 15 case .UNRECOGNIZED(let i): return i } } @@ -154,6 +160,7 @@ extension TelemetrySensorType: CaseIterable { .sht31, .pmsa003I, .ina3221, + .bmp085, ] } @@ -450,6 +457,7 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 12: .same(proto: "SHT31"), 13: .same(proto: "PMSA003I"), 14: .same(proto: "INA3221"), + 15: .same(proto: "BMP085"), ] } diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index e4fa4b55..978e8c08 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -17,12 +17,19 @@ struct UserList: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @State private var searchText = "" + var usersQuery: Binding { Binding { searchText } set: { newValue in searchText = newValue - users.nsPredicate = newValue.isEmpty ? nil : NSPredicate(format: "longName CONTAINS[c] %@ OR shortName CONTAINS[c] %@", newValue, newValue) + /// Case Insensitive Search Text Predicates + let searchPredicates = ["userId", "hwModel", "longName", "shortName"].map { property in + return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText) + } + /// Create a compound predicate using each text search predicate as an OR + let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates) + users.nsPredicate = newValue.isEmpty ? nil : textSearchPredicate } } @FetchRequest( @@ -48,94 +55,94 @@ struct UserList: View { let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0 let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0 if user.num != bleManager.connectedPeripheral?.num ?? 0 { - NavigationLink(destination: UserMessageList(user: user)) { - ZStack { - Image(systemName: "circle.fill") - .opacity(user.unreadMessages > 0 ? 1 : 0) - .font(.system(size: 10)) - .foregroundColor(.accentColor) - .brightness(0.2) - } - - CircleText(text: user.shortName ?? "?", color: Color(UIColor(hex: UInt32(user.num)))) - - VStack(alignment: .leading){ - HStack{ - Text(user.longName ?? "unknown".localized) - .font(.headline) - Spacer() - if user.vip { - Image(systemName: "star.fill") - .foregroundColor(.yellow) + NavigationLink(destination: UserMessageList(user: user)) { + ZStack { + Image(systemName: "circle.fill") + .opacity(user.unreadMessages > 0 ? 1 : 0) + .font(.system(size: 10)) + .foregroundColor(.accentColor) + .brightness(0.2) + } + + CircleText(text: user.shortName ?? "?", color: Color(UIColor(hex: UInt32(user.num)))) + + VStack(alignment: .leading){ + HStack{ + Text(user.longName ?? "unknown".localized) + .font(.headline) + Spacer() + if user.vip { + Image(systemName: "star.fill") + .foregroundColor(.yellow) + } + if user.messageList.count > 0 { + if lastMessageDay == currentDay { + Text(lastMessageTime, style: .time ) + .font(.footnote) + .foregroundColor(.secondary) + } else if lastMessageDay == (currentDay - 1) { + Text("Yesterday") + .font(.footnote) + .foregroundColor(.secondary) + } else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) { + Text(lastMessageTime.formattedDate(format: dateFormatString)) + .font(.footnote) + .foregroundColor(.secondary) + } else if lastMessageDay < (currentDay - 1800) { + Text(lastMessageTime.formattedDate(format: dateFormatString)) + .font(.footnote) + .foregroundColor(.secondary) + } + } } + if user.messageList.count > 0 { - if lastMessageDay == currentDay { - Text(lastMessageTime, style: .time ) - .font(.footnote) - .foregroundColor(.secondary) - } else if lastMessageDay == (currentDay - 1) { - Text("Yesterday") - .font(.footnote) - .foregroundColor(.secondary) - } else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) { - Text(lastMessageTime.formattedDate(format: dateFormatString)) - .font(.footnote) - .foregroundColor(.secondary) - } else if lastMessageDay < (currentDay - 1800) { - Text(lastMessageTime.formattedDate(format: dateFormatString)) + HStack(alignment: .top) { + Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")") .font(.footnote) .foregroundColor(.secondary) } } } - - if user.messageList.count > 0 { - HStack(alignment: .top) { - Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")") - .font(.footnote) - .foregroundColor(.secondary) + } + .frame(height: 62) + .contextMenu { + Button { + user.vip = !user.vip + do { + try context.save() + } catch { + context.rollback() + print("💥 Save User VIP Error") + } + } label: { + Label(user.vip ? "Un-Favorite" : "Favorite", systemImage: user.vip ? "star.slash.fill" : "star.fill") + } + Button { + user.mute = !user.mute + do { + try context.save() + } catch { + context.rollback() + print("💥 Save User Mute Error") + } + } label: { + Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash") + } + if user.messageList.count > 0 { + Button(role: .destructive) { + isPresentingDeleteUserMessagesConfirm = true + userSelection = user + } label: { + Label("Delete Messages", systemImage: "trash") } } } - } - .frame(height: 62) - .contextMenu { - Button { - user.vip = !user.vip - do { - try context.save() - } catch { - context.rollback() - print("💥 Save User VIP Error") - } - } label: { - Label(user.vip ? "Un-Favorite" : "Favorite", systemImage: user.vip ? "star.slash.fill" : "star.fill") - } - Button { - user.mute = !user.mute - do { - try context.save() - } catch { - context.rollback() - print("💥 Save User Mute Error") - } - } label: { - Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash") - } - if user.messageList.count > 0 { - Button(role: .destructive) { - isPresentingDeleteUserMessagesConfirm = true - userSelection = user - } label: { - Label("Delete Messages", systemImage: "trash") - } - } - } - .confirmationDialog( - "This conversation will be deleted.", - isPresented: $isPresentingDeleteUserMessagesConfirm, - titleVisibility: .visible - ) { + .confirmationDialog( + "This conversation will be deleted.", + isPresented: $isPresentingDeleteUserMessagesConfirm, + titleVisibility: .visible + ) { Button(role: .destructive) { deleteUserMessages(user: userSelection!, context: context) context.refresh(node!.user!, mergeChanges: true) @@ -149,7 +156,9 @@ struct UserList: View { } .listStyle(.plain) .navigationTitle(String.localizedStringWithFormat("contacts %@".localized, String(users.count == 0 ? 0 : users.count - 1))) - .searchable(text: usersQuery, prompt: "Find a contact") + .searchable(text: usersQuery, placement: users.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a contact") + .disableAutocorrection(true) + .scrollDismissesKeyboard(.immediately) } } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 1ce1c603..3511cfdb 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -107,6 +107,7 @@ struct NodeMapSwiftUI: View { if radius > 0.0 { MapCircle(center: position.coordinate, radius: radius) .foregroundStyle(Color(nodeColor).opacity(0.25)) + .stroke(.white, lineWidth: 2) } } Annotation(position.latest ? node.user?.shortName ?? "?": "", coordinate: position.coordinate) { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index 9b062da4..b5fc9fd8 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -130,6 +130,19 @@ struct PositionPopover: View { .frame(width: 35) } .padding(.bottom, 5) + if position.nodePosition?.viaMqtt ?? false { + + Label { + let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees) + Text("MQTT") + } icon: { + Image(systemName: "network") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + .rotationEffect(degrees) + } + .padding(.bottom, 5) + } if let lastLocation = locationsHandler.locationsArray.last { /// Distance if lastLocation.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { @@ -181,8 +194,7 @@ struct PositionPopover: View { } BatteryGauge(node: position.nodePosition!) } - let mpInt = Int(position.nodePosition?.loRaConfig?.modemPreset ?? 0) - LoRaSignalStrengthMeter(snr: position.nodePosition?.snr ?? 0.0, rssi: position.nodePosition?.rssi ?? 0, preset: ModemPresets(rawValue: mpInt) ?? ModemPresets.longFast, compact: false) + LoRaSignalStrengthMeter(snr: position.nodePosition?.snr ?? 0.0, rssi: position.nodePosition?.rssi ?? 0, preset: ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast, compact: false) Spacer() } } @@ -202,7 +214,7 @@ struct PositionPopover: View { #endif } } - .presentationDetents([.fraction(0.45), .fraction(0.55), .fraction(0.65)]) + .presentationDetents([.fraction(0.55), .fraction(0.65), .fraction(0.75)]) .presentationDragIndicator(.visible) } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift index fb3ce8e4..0b381b9c 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeInfoItem.swift @@ -12,6 +12,7 @@ import MapKit struct NodeInfoItem: View { @ObservedObject var node: NodeInfoEntity + var modemPreset: ModemPresets = ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast var body: some View { @@ -37,11 +38,11 @@ struct NodeInfoItem: View { if node.snr != 0 && !node.viaMqtt { Divider() VStack(alignment: .center) { - let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: ModemPresets.longModerate) + let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: modemPreset) LoRaSignalStrengthIndicator(signalStrength: signalStrength) Text("Signal \(signalStrength.description)").font(.footnote) Text("SNR \(String(format: "%.2f", node.snr))dB") - .foregroundColor(getSnrColor(snr: node.snr, preset: ModemPresets.longModerate)) + .foregroundColor(getSnrColor(snr: node.snr, preset: modemPreset)) .font(.caption2) Text("RSSI \(node.rssi)dB") .foregroundColor(getRssiColor(rssi: node.rssi)) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 4dd1bf7c..4ffdffca 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -13,7 +13,6 @@ struct NodeListItem: View { @ObservedObject var node: NodeInfoEntity var connected: Bool var connectedNode: Int64 - var modemPreset: Int var body: some View { @@ -21,6 +20,7 @@ struct NodeListItem: View { LazyVStack(alignment: .leading) { HStack { VStack(alignment: .leading) { + CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 70) .padding(.trailing, 5) BatteryLevelCompact(node: node, font: .caption, iconFont: .callout, color: .accentColor) @@ -119,24 +119,41 @@ struct NodeListItem: View { } HStack { if node.channel > 0 { - Image(systemName: "fibrechannel") - .font(.callout) - .symbolRenderingMode(.hierarchical) - .frame(width: 30) - Text("Channel: \(node.channel)") - .foregroundColor(.gray) - .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) + HStack { + Image(systemName: "\(node.channel).circle.fill") + .font(.title2) + .symbolRenderingMode(.hierarchical) + .frame(width: 30) + .foregroundColor(.accentColor) + Text("Channel") + .foregroundColor(.gray) + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) + } } + if node.viaMqtt && connectedNode != node.num { Image(systemName: "network") .symbolRenderingMode(.hierarchical) .font(.callout) .frame(width: 30) - Text("Via MQTT") + Text("MQTT") .foregroundColor(.gray) .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) } } + if node.hopsAway > 0 { + HStack { + Image(systemName: "hare") + .font(.callout) + .symbolRenderingMode(.hierarchical) + Text("Hops Away:") + .foregroundColor(.gray) + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) + Image(systemName: "\(node.hopsAway).square") + .font(.title2) + .symbolRenderingMode(.hierarchical) + } + } if node.hasPositions || node.hasEnvironmentMetrics || node.hasDetectionSensorMetrics || node.hasTraceRoutes { HStack { Image(systemName: "scroll") @@ -170,21 +187,16 @@ struct NodeListItem: View { .font(.callout) .frame(width: 30) } - if node.hasTraceRoutes { - Image(systemName: "signpost.right.and.left") - .symbolRenderingMode(.hierarchical) - .font(.callout) - .frame(width: 30) + if #available(iOS 17.0, macOS 14.0, *) { + if node.hasTraceRoutes { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) + .font(.callout) + .frame(width: 30) + } } } } - if !connected { - HStack { - let preset = ModemPresets(rawValue: Int(modemPreset)) - LoRaSignalStrengthMeter(snr: node.snr, rssi: node.rssi, preset: preset ?? ModemPresets.longFast, compact: true) - - } - } } .frame(maxWidth: .infinity, alignment: .leading) } diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 1859993a..4394efce 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -123,6 +123,7 @@ struct MeshMap: View { if radius > 0.0 { MapCircle(center: position.coordinate, radius: radius) .foregroundStyle(Color(nodeColor).opacity(0.25)) + .stroke(.white, lineWidth: 2) } } /// Routes diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 48bfeb80..c2532ad7 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -7,6 +7,28 @@ import SwiftUI import CoreLocation +struct NodeSearchState { + var searchText = "" + var searchScope = SearchScopes.all + var predicate: NSPredicate = .init() + + enum SearchScopes: CaseIterable, Identifiable { + case all + case lora + case mqtt + + var id: Self { self } + + var title: LocalizedStringKey { + switch self { + case .all: return "All" + case .lora: return "LoRa" + case .mqtt: return "MQTT" + } + } + } +} + struct NodeList: View { @State private var columnVisibility = NavigationSplitViewVisibility.all @@ -14,19 +36,13 @@ struct NodeList: View { @State private var isPresentingTraceRouteSentAlert = false @State private var isPresentingClientHistorySentAlert = false @State private var isPresentingDeleteNodeAlert = false + @State private var isPresentingPositionSentAlert = false @State private var deleteNodeId: Int64 = 0 + @State private var searchState = NodeSearchState() @SceneStorage("selectedDetailView") var selectedDetailView: String? @State private var searchText = "" - var nodesQuery: Binding { - Binding { - searchText - } set: { newValue in - searchText = newValue - nodes.nsPredicate = newValue.isEmpty ? nil : NSPredicate(format: "user.longName CONTAINS[c] %@ OR user.shortName CONTAINS[c] %@", newValue, newValue) - } - } @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @@ -37,8 +53,6 @@ struct NodeList: View { var nodes: FetchedResults - - var body: some View { NavigationSplitView(columnVisibility: $columnVisibility) { @@ -48,8 +62,7 @@ struct NodeList: View { NodeListItem(node: node, connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num, - connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1), - modemPreset: Int(connectedNode?.loRaConfig?.modemPreset ?? 0)) + connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1)) .contextMenu { if node.user != nil { Button { @@ -76,7 +89,21 @@ struct NodeList: View { } label: { Label(node.user!.mute ? "Show Alerts" : "Hide Alerts", systemImage: node.user!.mute ? "bell" : "bell.slash") } - if connectedNodeNum != node.num { + if bleManager.connectedPeripheral != nil && node.num != connectedNodeNum { + Button { + let positionSent = bleManager.sendPosition( + channel: node.channel, + destNum: node.num, + wantResponse: true + ) + if positionSent { + isPresentingPositionSentAlert = true + } + } label: { + Label("Exchange Positions", systemImage: "arrow.triangle.2.circlepath") + } + } + if bleManager.connectedPeripheral != nil && connectedNodeNum != node.num { Button { let success = bleManager.sendTraceRouteRequest(destNum: node.user?.num ?? 0, wantResponse: true) if success { @@ -107,6 +134,14 @@ struct NodeList: View { } } } + .alert( + "Position Sent", + isPresented: $isPresentingPositionSentAlert + ) { + Button("OK", role: .cancel) { } + } message: { + Text("Your position has been sent with a request for a response with their position.") + } .alert( "Trace Route Sent", isPresented: $isPresentingTraceRouteSentAlert @@ -124,7 +159,14 @@ struct NodeList: View { Text("Any missed messages will be delivered again.") } } - .searchable(text: nodesQuery, prompt: "Find a node") + .searchable(text: $searchState.searchText, placement: nodes.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a node") + .disableAutocorrection(true) + .scrollDismissesKeyboard(.immediately) + .searchScopes($searchState.searchScope) { + ForEach(NodeSearchState.SearchScopes.allCases) { scope in + Text(scope.title).tag(scope) + } + } .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) .listStyle(.plain) .confirmationDialog( @@ -195,33 +237,51 @@ struct NodeList: View { } .navigationSplitViewStyle(.balanced) -// .onChange(of: selectedNode) { _ in -// if selectedNode == nil { -// columnVisibility = .all -// } else { -// columnVisibility = .doubleColumn -// } -// } + .onChange(of: searchState.searchText) { _ in + searchNodeList() + } + .onChange(of: searchState.searchScope) { _ in + searchNodeList() + } .onAppear { if self.bleManager.context == nil { self.bleManager.context = context } } - -// } detail: { -// VStack { -// Button("Detail Only") { -// columnVisibility = .detailOnly -// } -// -// Button("Content and Detail") { -// columnVisibility = .doubleColumn -// } -// -// Button("Show All") { -// columnVisibility = .all -// } -// } -// } + } + + private func searchNodeList() { + /// Case Insensitive Search Text Predicates + let searchPredicates = ["user.userId", "user.hwModel", "user.longName", "user.shortName"].map { property in + return NSPredicate(format: "%K CONTAINS[c] %@", property, searchState.searchText) + } + /// Create a compound predicate using each text search preicate as an OR + let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates) + + /// Set the predicate to nil if the search string is empty + if searchState.searchText.isEmpty { + nodes.nsPredicate = nil + return + } + + /// Add a predicate for the search scope if selected + if searchState.searchScope != .all { + + if searchState.searchScope == .lora { + let loraPredicate = NSPredicate(format: "viaMqtt == NO") + let scopePredicate = NSCompoundPredicate(type: .and, subpredicates: [loraPredicate]) + nodes.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, scopePredicate]) + return + + } else if searchState.searchScope == .mqtt { + let mqttPredicate = NSPredicate(format: "viaMqtt == YES") + let scopePredicate = NSCompoundPredicate(type: .and, subpredicates: [mqttPredicate]) + nodes.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, scopePredicate]) + return + } + } else { + /// Use the text search predicate + nodes.nsPredicate = textSearchPredicate + } } } diff --git a/Meshtastic/Views/Nodes/PaxCounterLog.swift b/Meshtastic/Views/Nodes/PaxCounterLog.swift index 3b04d662..df85fc01 100644 --- a/Meshtastic/Views/Nodes/PaxCounterLog.swift +++ b/Meshtastic/Views/Nodes/PaxCounterLog.swift @@ -75,8 +75,8 @@ struct PaxCounterLog: View { .chartXAxis(.automatic) .chartYScale(domain: 0...maxValue) .chartForegroundStyleScale([ - "paxcounter.ble": .blue, - "paxcounter.wifi": .orange, + "paxcounter.ble".localized: .blue, + "paxcounter.wifi".localized: .orange, "paxcounter.total".localized: .green ]) .chartLegend(position: .automatic, alignment: .bottom) @@ -111,11 +111,11 @@ struct PaxCounterLog: View { } else { ScrollView { let columns = [ - GridItem(.flexible(minimum: 20, maximum: 55), spacing: 0.1), - GridItem(.flexible(minimum: 20, maximum: 55), spacing: 0.1), - GridItem(.flexible(minimum: 20, maximum: 55), spacing: 0.1), - GridItem(.flexible(minimum: 60, maximum: 100), spacing: 0.1), - GridItem(.flexible(minimum: 130, maximum: 200), spacing: 0.1) + GridItem(.flexible(minimum: 20, maximum: 50), spacing: 0.1), + GridItem(.flexible(minimum: 20, maximum: 50), spacing: 0.1), + GridItem(.flexible(minimum: 20, maximum: 50), spacing: 0.1), + GridItem(.flexible(minimum: 60, maximum: 140), spacing: 0.1), + GridItem(.flexible(minimum: 100, maximum: 160), spacing: 0.1) ] LazyVGrid(columns: columns, alignment: .leading, spacing: 1) { GridRow { diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 9e559b62..cd9ef8f4 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -24,9 +24,15 @@ struct AppSettings: View { Label("appsettings.provide.location", systemImage: "location.circle.fill") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("Use your phone's gps to provide a location to your node. Must have location access and precise location enabled for Meshtastic in Settings.") + .font(.caption2) + .foregroundColor(.gray) if provideLocation { Toggle(isOn: $enableSmartPosition) { Label("appsettings.smartposition", systemImage: "brain") + Text("Will only send a position to the phone if it is recent and of high horizontal accuracy.") + .font(.caption2) + .foregroundColor(.gray) } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) VStack { diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index ea91d13b..db9c9a03 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -68,6 +68,8 @@ struct Channels: View { channelKeySize = 0 } else if channelKey == "AQ==" { channelKeySize = -1 + } else if channelKey.count == 4 { + channelKeySize = 1 } else if channelKey.count == 24 { channelKeySize = 16 } else if channelKey.count == 32 { @@ -268,7 +270,7 @@ struct Channels: View { if !preciseLocation { VStack(alignment: .leading) { - Label("Reduce Precision", systemImage: "location.viewfinder") + Label("Approximate Location", systemImage: "location.slash.circle.fill") Slider( value: $positionPrecision, in: 11...16, @@ -303,7 +305,7 @@ struct Channels: View { } .onAppear { let tempKey = Data(base64Encoded: channelKey) ?? Data() - if tempKey.count == channelKeySize || channelKeySize == -1{ + if tempKey.count == channelKeySize || channelKeySize == -1 { hasValidKey = true } else { diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 63ebf59b..6e190609 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -179,6 +179,10 @@ struct DeviceConfig: View { dc.nodeInfoBroadcastSecs = UInt32(nodeInfoBroadcastSecs) dc.doubleTapAsButtonPress = doubleTapAsButtonPress dc.isManaged = isManaged + if isManaged { + serialEnabled = false + debugLogEnabled = false + } let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index eace3897..3ca281c2 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -205,6 +205,9 @@ struct LoRaConfig: View { lc.sx126XRxBoostedGain = rxBoostedGain lc.overrideFrequency = overrideFrequency lc.ignoreMqtt = ignoreMqtt + if connectedNode?.num ?? -1 == node?.user?.num ?? 0 { + UserDefaults.modemPreset = modemPreset + } let adminMessageId = bleManager.saveLoRaConfig(config: lc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 21aa3b82..98b29957 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -12,7 +12,7 @@ struct Firmware: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager var node: NodeInfoEntity? - @State var minimumVersion = "2.2.21" + @State var minimumVersion = "2.3.0" @State var version = "" @State private var currentDevice: DeviceHardware? @State private var latestStable: FirmwareRelease? diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 88ffbf40..b98893e9 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -144,7 +144,7 @@ struct Settings: View { Text("Your region has a \(rc?.dutyCycle ?? 0)% hourly duty cycle, your radio will stop sending packets when it reaches the hourly limit.") .foregroundColor(.orange) .font(.caption) - Text("Limit all periodic broadcasts intervals especially telemetry and position. If you need to increase hops, do it on nodes at the edges, not the ones in the middle. MQTT is not advised when you are duty cycle restricted because the gateway node is then doing all the work.") + Text("Limit all periodic broadcast intervals especially telemetry and position. If you need to increase hops, do it on nodes at the edges, not the ones in the middle. MQTT is not advised when you are duty cycle restricted because the gateway node is then doing all the work.") .font(.caption2) .foregroundColor(.gray) } diff --git a/protobufs b/protobufs index 52415835..5a97acb1 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 5241583565ccbbb4986180bf4c6eb7f8a0dec285 +Subproject commit 5a97acb17543a10e114675a205e3274a83e721af