diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index cb6e47ba..49932aa0 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -241,6 +241,7 @@ DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; + DD23D9AB2B7133F6003F5CBE /* MeshtasticDataModelV 25.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 25.xcdatamodel"; sourceTree = ""; }; DD2553562855B02500E55709 /* LoRaConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaConfig.swift; sourceTree = ""; }; DD2553582855B52700E55709 /* PositionConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionConfig.swift; sourceTree = ""; }; DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV22.xcdatamodel; sourceTree = ""; }; @@ -1492,7 +1493,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.20; + MARKETING_VERSION = 2.2.21; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1526,7 +1527,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.20; + MARKETING_VERSION = 2.2.21; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1648,7 +1649,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.20; + MARKETING_VERSION = 2.2.21; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1681,7 +1682,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.20; + MARKETING_VERSION = 2.2.21; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1792,6 +1793,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD23D9AB2B7133F6003F5CBE /* MeshtasticDataModelV 25.xcdatamodel */, DDB234392B5CA9B000DA6FB1 /* MeshtasticDataModelV 24.xcdatamodel */, DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */, DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */, @@ -1817,7 +1819,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DDB234392B5CA9B000DA6FB1 /* MeshtasticDataModelV 24.xcdatamodel */; + currentVersion = DD23D9AB2B7133F6003F5CBE /* MeshtasticDataModelV 25.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Enums/DeviceEnums.swift b/Meshtastic/Enums/DeviceEnums.swift index 7e97aea7..b754e986 100644 --- a/Meshtastic/Enums/DeviceEnums.swift +++ b/Meshtastic/Enums/DeviceEnums.swift @@ -12,22 +12,22 @@ enum DeviceRoles: Int, CaseIterable, Identifiable { case client = 0 case clientMute = 1 - case router = 2 - case routerClient = 3 - case repeater = 4 + case clientHidden = 8 case tracker = 5 + case lostAndFound = 9 case sensor = 6 case tak = 7 - case clientHidden = 8 - case lostAndFound = 9 - + case repeater = 4 + case router = 2 + case routerClient = 3 + var id: Int { self.rawValue } var name: String { switch self { case .client: return "Client" case .clientMute: - return "Muted Client" + return "Client Mute" case .router: return "Router" case .routerClient: diff --git a/Meshtastic/Enums/LoraConfigEnums.swift b/Meshtastic/Enums/LoraConfigEnums.swift index 85ed35ce..3e638773 100644 --- a/Meshtastic/Enums/LoraConfigEnums.swift +++ b/Meshtastic/Enums/LoraConfigEnums.swift @@ -24,6 +24,8 @@ enum RegionCodes: Int, CaseIterable, Identifiable { case th = 12 case ua433 = 14 case ua868 = 15 + case my_433 = 16 + case my_919 = 17 case lora24 = 13 var id: Int { self.rawValue } @@ -61,9 +63,52 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return "Ukraine 868mhz" case .lora24: return "2.4 GHZ" + case .my_433: + return "Malaysia 433mhz" + case .my_919: + return "Malaysia 919mhz" + } + } + var dutyCycle: Int { + switch self { + case .unset: + return 0 + case .us: + return 100 + case .eu433: + return 10 + case .eu868: + return 10 + case .cn: + return 100 + case .jp: + return 100 + case .anz: + return 100 + case .kr: + return 100 + case .tw: + return 100 + case .ru: + return 100 + case .in: + return 100 + case .nz865: + return 100 + case .th: + return 100 + case .ua433: + return 10 + case .ua868: + return 10 + case .lora24: + return 100 + case .my_433: + return 100 + case .my_919: + return 100 } } - func protoEnumValue() -> Config.LoRaConfig.RegionCode { switch self { @@ -99,6 +144,10 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return Config.LoRaConfig.RegionCode.ua868 case .lora24: return Config.LoRaConfig.RegionCode.lora24 + case .my_433: + return Config.LoRaConfig.RegionCode.my433 + case .my_919: + return Config.LoRaConfig.RegionCode.my919 } } } diff --git a/Meshtastic/Enums/PositionConfigEnums.swift b/Meshtastic/Enums/PositionConfigEnums.swift index 49f74cd6..e9d4fec2 100644 --- a/Meshtastic/Enums/PositionConfigEnums.swift +++ b/Meshtastic/Enums/PositionConfigEnums.swift @@ -52,3 +52,77 @@ enum GpsFormats: Int, CaseIterable, Identifiable { } } } + +enum GpsUpdateIntervals: Int, CaseIterable, Identifiable { + + case thirtySeconds = 30 + case oneMinute = 60 + case fiveMinutes = 300 + case tenMinutes = 600 + case fifteenMinutes = 900 + case thirtyMinutes = 1800 + case oneHour = 3600 + case sixHours = 21600 + case twelveHours = 43200 + case twentyFourHours = 86400 + case maxInt32 = 2147483647 + + var id: Int { self.rawValue } + var description: String { + switch self { + case .thirtySeconds: + return "interval.thirty.seconds".localized + case .oneMinute: + return "interval.one.minute".localized + case .fiveMinutes: + return "interval.five.minutes".localized + case .tenMinutes: + return "interval.ten.minutes".localized + case .fifteenMinutes: + return "interval.fifteen.minutes".localized + case .thirtyMinutes: + return "interval.thirty.minutes".localized + case .oneHour: + return "interval.one.hour".localized + case .sixHours: + return "interval.six.hours".localized + case .twelveHours: + return "interval.twelve.hours".localized + case .twentyFourHours: + return "interval.twentyfour.hours".localized + case .maxInt32: + return "on.boot".localized + } + } +} + +enum GpsMode: Int, CaseIterable, Equatable { + case disabled = 0 + case enabled = 1 + case notPresent = 2 + + var id: Int { self.rawValue } + + var description: String { + switch self { + case .disabled: + return "gpsmode.disabled".localized + case .enabled: + return "gpsmode.enabled".localized + case .notPresent: + return "gpsmode.notPresent".localized + } + } + func protoEnumValue() -> Config.PositionConfig.GpsMode { + + switch self { + + case .enabled: + return Config.PositionConfig.GpsMode.enabled + case .disabled: + return Config.PositionConfig.GpsMode.disabled + case .notPresent: + return Config.PositionConfig.GpsMode.notPresent + } + } +} diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 00b5b6e3..b1b1785b 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -9,7 +9,6 @@ import Foundation extension UserDefaults { enum Keys: String, CaseIterable { - case enableRangeTest case preferredPeripheralId case preferredPeripheralNum case provideLocation @@ -27,18 +26,12 @@ extension UserDefaults { case mapUseLegacy case enableDetectionNotifications case detectionSensorRole + case enableSmartPosition } func reset() { Keys.allCases.forEach { removeObject(forKey: $0.rawValue) } } - static var blockRangeTest: Bool { - get { - UserDefaults.standard.bool(forKey: "blockRangeTest") - } set { - UserDefaults.standard.set(newValue, forKey: "blockRangeTest") - } - } static var preferredPeripheralId: String { get { UserDefaults.standard.string(forKey: "preferredPeripheralId") ?? "" @@ -201,4 +194,12 @@ extension UserDefaults { UserDefaults.standard.set(newValue.rawValue, forKey: "detectionSensorRole") } } + static var enableSmartPosition: Bool { + get { + UserDefaults.standard.bool(forKey: "enableSmartPosition") + } + set { + UserDefaults.standard.set(newValue, forKey: "enableSmartPosition") + } + } } diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 155f19bf..5e9e19c1 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -594,7 +594,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Log any other unknownApp calls if !nowKnown { MeshLogger.log("🕸️ MESH PACKET received for Unknown App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") } case .textMessageApp, .detectionSensorApp: - textMessageAppPacket(packet: decodedInfo.packet, blockRangeTest: UserDefaults.blockRangeTest, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) + textMessageAppPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) case .remoteHardwareApp: MeshLogger.log("🕸️ MESH PACKET received for Remote Hardware App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") case .positionApp: @@ -620,8 +620,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate MeshLogger.log("🕸️ MESH PACKET received for Store and Forward App - Store and Forward is disabled.") } case .rangeTestApp: - if wantRangeTestPackets && !UserDefaults.blockRangeTest { - textMessageAppPacket(packet: decodedInfo.packet, blockRangeTest: false, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) + if wantRangeTestPackets { + textMessageAppPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) } else { MeshLogger.log("🕸️ MESH PACKET received for Range Test App Range testing is disabled.") @@ -976,10 +976,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if let lastLocation = LocationsHandler.shared.locationsArray.last { - /// Throw out crappy locations and only send a position if we are connected to a device - if fromNodeNum <= 0 || lastLocation.horizontalAccuracy < 0 || lastLocation.horizontalAccuracy > 100 { - return false - } positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7) positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7) let timestamp = lastLocation.timestamp @@ -2386,7 +2382,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate /// send a request for ClientHistory with a time period matching the heartbeat var sfPacket = StoreAndForward() sfPacket.rr = StoreAndForward.RequestResponse.clientHistory - sfPacket.history.window = 18000000 // storeAndForwardMessage.heartbeat.period + sfPacket.history.window = 120 // storeAndForwardMessage.heartbeat.period var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(packet.from) meshPacket.from = UInt32(connectedNodeNum) @@ -2432,6 +2428,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate case .clientAbort: MeshLogger.log("🛑 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") case .UNRECOGNIZED: + textMessageAppPacket(packet: packet, connectedNode: connectedNodeNum, context: context) MeshLogger.log("📮 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") } } diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 4742914a..53712c24 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -15,7 +15,7 @@ import CoreLocation static let shared = LocationsHandler() // Create a single, shared instance of the object. private let manager: CLLocationManager private var background: CLBackgroundActivitySession? - var enableSmartPosition: Bool + var enableSmartPosition: Bool = UserDefaults.enableSmartPosition @Published var locationsArray: [CLLocation] @Published var isStationary = false @@ -41,8 +41,8 @@ import CoreLocation private init() { self.manager = CLLocationManager() // Creating a location manager instance is safe to call here in `MainActor`. + self.manager.allowsBackgroundLocationUpdates = true locationsArray = [CLLocation]() - enableSmartPosition = true } func startLocationUpdates() { @@ -55,7 +55,7 @@ import CoreLocation self.updatesStarted = true let updates = CLLocationUpdate.liveUpdates() for try await update in updates { - if !self.updatesStarted { break } // End location updates by breaking out of the loop. + if !self.updatesStarted { break } if let loc = update.location { self.isStationary = update.isStationary diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 9b720845..d2e6a8f1 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -721,13 +721,9 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage } } -func textMessageAppPacket(packet: MeshPacket, blockRangeTest: Bool, connectedNode: Int64, context: NSManagedObjectContext) { +func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) { if let messageText = String(bytes: packet.decoded.payload, encoding: .utf8) { - - if blockRangeTest && messageText.starts(with: "seq ") { - return - } MeshLogger.log("💬 \("mesh.log.textmessage.received".localized)") diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 66c74a0d..07493add 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 24.xcdatamodel + MeshtasticDataModelV 25.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 25.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 25.xcdatamodel/contents new file mode 100644 index 00000000..85cf1eaa --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 25.xcdatamodel/contents @@ -0,0 +1,412 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/Persistence.swift b/Meshtastic/Persistence/Persistence.swift index 1c768ccf..9165c433 100644 --- a/Meshtastic/Persistence/Persistence.swift +++ b/Meshtastic/Persistence/Persistence.swift @@ -61,8 +61,14 @@ class PersistenceController { do { try persistentStoreCoordinator.destroyPersistentStore(at: url, ofType: NSSQLiteStoreType, options: nil) - try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil) print("💥 CoreData database truncated. All app data has been erased.") + + do { + try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil) + } catch let error { + print("💣 Failed to re-create CoreData database: " + error.localizedDescription) + try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil) + } } catch let error { print("💣 Failed to destroy CoreData database, delete the app and re-install to clear data. Attempted to clear persistent store: " + error.localizedDescription) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index b7edbb4a..f922d28c 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -118,6 +118,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) newNode.snr = packet.rxSnr newNode.rssi = packet.rxRssi + newNode.viaMqtt = packet.viaMqtt if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { newNode.channel = Int32(nodeInfoMessage.channel) print(packet.channel) @@ -161,6 +162,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) } fetchedNode[0].snr = packet.rxSnr fetchedNode[0].rssi = packet.rxRssi + fetchedNode[0].viaMqtt = packet.viaMqtt if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { fetchedNode[0].channel = Int32(nodeInfoMessage.channel) @@ -277,6 +279,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) } fetchedNode[0].snr = packet.rxSnr fetchedNode[0].rssi = packet.rxRssi + fetchedNode[0].viaMqtt = packet.viaMqtt fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet do { @@ -492,6 +495,7 @@ func upsertLoRaConfigPacket(config: Meshtastic.Config.LoRaConfig, nodeNum: Int64 newLoRaConfig.txEnabled = config.txEnabled newLoRaConfig.channelNum = Int32(config.channelNum) newLoRaConfig.sx126xRxBoostedGain = config.sx126XRxBoostedGain + newLoRaConfig.ignoreMqtt = config.ignoreMqtt fetchedNode[0].loRaConfig = newLoRaConfig } else { fetchedNode[0].loRaConfig?.regionCode = Int32(config.region.rawValue) @@ -508,6 +512,8 @@ func upsertLoRaConfigPacket(config: Meshtastic.Config.LoRaConfig, nodeNum: Int64 fetchedNode[0].loRaConfig?.txEnabled = config.txEnabled fetchedNode[0].loRaConfig?.channelNum = Int32(config.channelNum) fetchedNode[0].loRaConfig?.sx126xRxBoostedGain = config.sx126XRxBoostedGain + fetchedNode[0].loRaConfig?.ignoreMqtt = config.ignoreMqtt + fetchedNode[0].loRaConfig?.sx126xRxBoostedGain = config.sx126XRxBoostedGain } do { try context.save() @@ -592,6 +598,7 @@ func upsertPositionConfigPacket(config: Meshtastic.Config.PositionConfig, nodeNu let newPositionConfig = PositionConfigEntity(context: context) newPositionConfig.smartPositionEnabled = config.positionBroadcastSmartEnabled newPositionConfig.deviceGpsEnabled = config.gpsEnabled + newPositionConfig.gpsMode = Int32(config.gpsMode.rawValue) newPositionConfig.rxGpio = Int32(config.rxGpio) newPositionConfig.txGpio = Int32(config.txGpio) newPositionConfig.gpsEnGpio = Int32(config.gpsEnGpio) @@ -601,11 +608,12 @@ func upsertPositionConfigPacket(config: Meshtastic.Config.PositionConfig, nodeNu newPositionConfig.broadcastSmartMinimumDistance = Int32(config.broadcastSmartMinimumDistance) newPositionConfig.positionFlags = Int32(config.positionFlags) newPositionConfig.gpsAttemptTime = 900 - newPositionConfig.gpsUpdateInterval = 120 + newPositionConfig.gpsUpdateInterval = Int32(config.gpsUpdateInterval) fetchedNode[0].positionConfig = newPositionConfig } else { fetchedNode[0].positionConfig?.smartPositionEnabled = config.positionBroadcastSmartEnabled fetchedNode[0].positionConfig?.deviceGpsEnabled = config.gpsEnabled + fetchedNode[0].positionConfig?.gpsMode = Int32(config.gpsMode.rawValue) fetchedNode[0].positionConfig?.rxGpio = Int32(config.rxGpio) fetchedNode[0].positionConfig?.txGpio = Int32(config.txGpio) fetchedNode[0].positionConfig?.gpsEnGpio = Int32(config.gpsEnGpio) @@ -614,7 +622,7 @@ func upsertPositionConfigPacket(config: Meshtastic.Config.PositionConfig, nodeNu fetchedNode[0].positionConfig?.broadcastSmartMinimumIntervalSecs = Int32(config.broadcastSmartMinimumIntervalSecs) fetchedNode[0].positionConfig?.broadcastSmartMinimumDistance = Int32(config.broadcastSmartMinimumDistance) fetchedNode[0].positionConfig?.gpsAttemptTime = 900 - fetchedNode[0].positionConfig?.gpsUpdateInterval = 120 + fetchedNode[0].positionConfig?.gpsUpdateInterval = Int32(config.gpsUpdateInterval) fetchedNode[0].positionConfig?.positionFlags = Int32(config.positionFlags) } do { diff --git a/Meshtastic/Protobufs/meshtastic/admin.pb.swift b/Meshtastic/Protobufs/meshtastic/admin.pb.swift index ab96c32c..baa3c742 100644 --- a/Meshtastic/Protobufs/meshtastic/admin.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/admin.pb.swift @@ -235,6 +235,16 @@ struct AdminMessage { set {payloadVariant = .enterDfuModeRequest(newValue)} } + /// + /// Delete the file by the specified path from the device + var deleteFileRequest: String { + get { + if case .deleteFileRequest(let v)? = payloadVariant {return v} + return String() + } + set {payloadVariant = .deleteFileRequest(newValue)} + } + /// /// Set the owner for this node var setOwner: User { @@ -460,6 +470,9 @@ struct AdminMessage { /// Only implemented on NRF52 currently case enterDfuModeRequest(Bool) /// + /// Delete the file by the specified path from the device + case deleteFileRequest(String) + /// /// Set the owner for this node case setOwner(User) /// @@ -598,6 +611,10 @@ struct AdminMessage { guard case .enterDfuModeRequest(let l) = lhs, case .enterDfuModeRequest(let r) = rhs else { preconditionFailure() } return l == r }() + case (.deleteFileRequest, .deleteFileRequest): return { + guard case .deleteFileRequest(let l) = lhs, case .deleteFileRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() case (.setOwner, .setOwner): return { guard case .setOwner(let l) = lhs, case .setOwner(let r) = rhs else { preconditionFailure() } return l == r @@ -953,6 +970,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 19: .standard(proto: "get_node_remote_hardware_pins_request"), 20: .standard(proto: "get_node_remote_hardware_pins_response"), 21: .standard(proto: "enter_dfu_mode_request"), + 22: .standard(proto: "delete_file_request"), 32: .standard(proto: "set_owner"), 33: .standard(proto: "set_channel"), 34: .standard(proto: "set_config"), @@ -1176,6 +1194,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .enterDfuModeRequest(v) } }() + case 22: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .deleteFileRequest(v) + } + }() case 32: try { var v: User? var hadOneofValue = false @@ -1407,6 +1433,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .enterDfuModeRequest(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 21) }() + case .deleteFileRequest?: try { + guard case .deleteFileRequest(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 22) + }() case .setOwner?: try { guard case .setOwner(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 32) diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index b984ef22..ef43b94a 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -415,6 +415,10 @@ struct Config { /// (Re)define PIN_GPS_EN for your board. var gpsEnGpio: UInt32 = 0 + /// + /// Set where GPS is enabled, disabled, or not present + var gpsMode: Config.PositionConfig.GpsMode = .disabled + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -516,6 +520,46 @@ struct Config { } + enum GpsMode: SwiftProtobuf.Enum { + typealias RawValue = Int + + /// + /// GPS is present but disabled + case disabled // = 0 + + /// + /// GPS is present and enabled + case enabled // = 1 + + /// + /// GPS is not present on the device + case notPresent // = 2 + case UNRECOGNIZED(Int) + + init() { + self = .disabled + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .disabled + case 1: self = .enabled + case 2: self = .notPresent + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .disabled: return 0 + case .enabled: return 1 + case .notPresent: return 2 + case .UNRECOGNIZED(let i): return i + } + } + + } + init() {} } @@ -1366,6 +1410,15 @@ extension Config.PositionConfig.PositionFlags: CaseIterable { ] } +extension Config.PositionConfig.GpsMode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static var allCases: [Config.PositionConfig.GpsMode] = [ + .disabled, + .enabled, + .notPresent, + ] +} + extension Config.NetworkConfig.AddressMode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. static var allCases: [Config.NetworkConfig.AddressMode] = [ @@ -1471,6 +1524,7 @@ extension Config.DeviceConfig.Role: @unchecked Sendable {} extension Config.DeviceConfig.RebroadcastMode: @unchecked Sendable {} extension Config.PositionConfig: @unchecked Sendable {} extension Config.PositionConfig.PositionFlags: @unchecked Sendable {} +extension Config.PositionConfig.GpsMode: @unchecked Sendable {} extension Config.PowerConfig: @unchecked Sendable {} extension Config.NetworkConfig: @unchecked Sendable {} extension Config.NetworkConfig.AddressMode: @unchecked Sendable {} @@ -1776,6 +1830,7 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm 10: .standard(proto: "broadcast_smart_minimum_distance"), 11: .standard(proto: "broadcast_smart_minimum_interval_secs"), 12: .standard(proto: "gps_en_gpio"), + 13: .standard(proto: "gps_mode"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1796,6 +1851,7 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm case 10: try { try decoder.decodeSingularUInt32Field(value: &self.broadcastSmartMinimumDistance) }() case 11: try { try decoder.decodeSingularUInt32Field(value: &self.broadcastSmartMinimumIntervalSecs) }() case 12: try { try decoder.decodeSingularUInt32Field(value: &self.gpsEnGpio) }() + case 13: try { try decoder.decodeSingularEnumField(value: &self.gpsMode) }() default: break } } @@ -1838,6 +1894,9 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if self.gpsEnGpio != 0 { try visitor.visitSingularUInt32Field(value: self.gpsEnGpio, fieldNumber: 12) } + if self.gpsMode != .disabled { + try visitor.visitSingularEnumField(value: self.gpsMode, fieldNumber: 13) + } try unknownFields.traverse(visitor: &visitor) } @@ -1854,6 +1913,7 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if lhs.broadcastSmartMinimumDistance != rhs.broadcastSmartMinimumDistance {return false} if lhs.broadcastSmartMinimumIntervalSecs != rhs.broadcastSmartMinimumIntervalSecs {return false} if lhs.gpsEnGpio != rhs.gpsEnGpio {return false} + if lhs.gpsMode != rhs.gpsMode {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1875,6 +1935,14 @@ extension Config.PositionConfig.PositionFlags: SwiftProtobuf._ProtoNameProviding ] } +extension Config.PositionConfig.GpsMode: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "DISABLED"), + 1: .same(proto: "ENABLED"), + 2: .same(proto: "NOT_PRESENT"), + ] +} + extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = Config.protoMessageName + ".PowerConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index 31ebb6a2..7496d559 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -126,6 +126,10 @@ enum HardwareModel: SwiftProtobuf.Enum { /// Makerfabs SenseLoRA Industrial Monitor (ESP32-S3 + RFM96) case senseloraS3 // = 28 + /// + /// Canary Radio Company - CanaryOne: https://canaryradio.io/products/canaryone + case canaryone // = 29 + /// /// --------------------------------------------------------------------------- /// Less common/prototype boards listed here (needs one more byte over the air) @@ -230,6 +234,14 @@ enum HardwareModel: SwiftProtobuf.Enum { /// with one cut and one jumper Meshtastic works case chatter2 // = 56 + /// + /// Heltec Wireless Paper, With ESP32-S3 CPU and E-Ink display + /// Older "V1.0" Variant, has no "version sticker" + /// E-Ink model is DEPG0213BNS800 + /// Tab on the screen protector is RED + /// Flex connector marking is FPC-7528B + case heltecWirelessPaperV10 // = 57 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// 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. @@ -267,6 +279,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case 26: self = .rak11310 case 27: self = .senseloraRp2040 case 28: self = .senseloraS3 + case 29: self = .canaryone case 32: self = .loraRelayV1 case 33: self = .nrf52840Dk case 34: self = .ppr @@ -292,6 +305,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case 54: self = .ebyteEsp32S3 case 55: self = .esp32S3Pico case 56: self = .chatter2 + case 57: self = .heltecWirelessPaperV10 case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -323,6 +337,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case .rak11310: return 26 case .senseloraRp2040: return 27 case .senseloraS3: return 28 + case .canaryone: return 29 case .loraRelayV1: return 32 case .nrf52840Dk: return 33 case .ppr: return 34 @@ -348,6 +363,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case .ebyteEsp32S3: return 54 case .esp32S3Pico: return 55 case .chatter2: return 56 + case .heltecWirelessPaperV10: return 57 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -384,6 +400,7 @@ extension HardwareModel: CaseIterable { .rak11310, .senseloraRp2040, .senseloraS3, + .canaryone, .loraRelayV1, .nrf52840Dk, .ppr, @@ -409,6 +426,7 @@ extension HardwareModel: CaseIterable { .ebyteEsp32S3, .esp32S3Pico, .chatter2, + .heltecWirelessPaperV10, .privateHw, ] } @@ -2570,6 +2588,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 26: .same(proto: "RAK11310"), 27: .same(proto: "SENSELORA_RP2040"), 28: .same(proto: "SENSELORA_S3"), + 29: .same(proto: "CANARYONE"), 32: .same(proto: "LORA_RELAY_V1"), 33: .same(proto: "NRF52840DK"), 34: .same(proto: "PPR"), @@ -2595,6 +2614,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 54: .same(proto: "EBYTE_ESP32_S3"), 55: .same(proto: "ESP32_S3_PICO"), 56: .same(proto: "CHATTER_2"), + 57: .same(proto: "HELTEC_WIRELESS_PAPER_V1_0"), 255: .same(proto: "PRIVATE_HW"), ] } diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 834ea5db..56fc496a 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -209,21 +209,18 @@ struct Connect: View { }.padding([.bottom, .top]) } } - .confirmationDialog("Connecting to a new radio will clear all local app data on the phone.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) { + .confirmationDialog("Connecting to a new radio will clear all local app data on the phone. The app may close.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) { Button("Connect to new radio?", role: .destructive) { - bleManager.stopScanning() - bleManager.connectedPeripheral = nil - UserDefaults.preferredPeripheralId = "" + UserDefaults.preferredPeripheralId = selectedPeripherialId if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == CBPeripheralState.connected { bleManager.disconnectPeripheral() } - - clearCoreDataDatabase(context: context) - let radio = bleManager.peripherals.first(where: { $0.peripheral.identifier.uuidString == selectedPeripherialId}) - bleManager.connectTo(peripheral: radio!.peripheral) - presentingSwitchPreferredPeripheral = false - selectedPeripherialId = "" + PersistenceController.shared.clearDatabase() + let radio = bleManager.peripherals.first(where: { $0.peripheral.identifier.uuidString == selectedPeripherialId }) + if radio != nil { + bleManager.connectTo(peripheral: radio!.peripheral) + } } } .textCase(nil) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index a4692ec9..4dfb1af2 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -89,14 +89,21 @@ struct NodeListItem: View { } } } - if node.channel > 0 { - HStack { + HStack { + if node.channel > 0 { Image(systemName: "fibrechannel") .font(.callout) .symbolRenderingMode(.hierarchical) Text("Channel: \(node.channel)") .font(.callout) } + if node.viaMqtt && connectedNode != node.num { + Image(systemName: "network") + .symbolRenderingMode(.hierarchical) + .font(.callout) + Text("Via MQTT") + .font(.callout) + } } if !connected { HStack { diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 109fc225..96697ddf 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -86,7 +86,7 @@ struct NodeMap: View { Section(header: Text("Map Options")) { Picker(selection: $selectedMapLayer, label: Text("")) { ForEach(MapLayer.allCases, id: \.self) { layer in - if layer == MapLayer.offline && UserDefaults.enableOfflineMaps { + if layer == MapLayer.offline && enableOfflineMaps { Text(layer.localized) } else if layer != MapLayer.offline { Text(layer.localized) diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index ba705afc..44b1790f 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -11,7 +11,7 @@ struct AppSettings: View { @State var totalDownloadedTileSize = "" @StateObject var locationHelper = LocationHelper() @State var provideLocation: Bool = UserDefaults.provideLocation - @State var blockRangeTest: Bool = UserDefaults.blockRangeTest + @State var enableSmartPosition: Bool = UserDefaults.enableSmartPosition @State var useLegacyMap: Bool = UserDefaults.mapUseLegacy @State var provideLocationInterval: Int = UserDefaults.provideLocationInterval @State private var isPresentingCoreDataResetConfirm = false @@ -20,12 +20,6 @@ struct AppSettings: View { VStack { Form { Section(header: Text("options")) { - - Toggle(isOn: $blockRangeTest) { - Label("range.test.blocked", systemImage: "x.circle") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $useLegacyMap) { Label("map.use.legacy", systemImage: "map") } @@ -71,10 +65,14 @@ struct AppSettings: View { } Section(header: Text("Location Settings")) { Toggle(isOn: $provideLocation) { - Label("provide.location", systemImage: "location.circle.fill") + Label("appsettings.provide.location", systemImage: "location.circle.fill") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - if UserDefaults.provideLocation { + if provideLocation { + Toggle(isOn: $enableSmartPosition) { + Label("appsettings.smartposition", systemImage: "brain.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) VStack { Picker("update.interval", selection: $provideLocationInterval) { ForEach(LocationUpdateInterval.allCases) { lu in @@ -106,8 +104,8 @@ struct AppSettings: View { Button("Erase all app data?", role: .destructive) { bleManager.disconnectPeripheral() clearCoreDataDatabase(context: context) + context.refreshAllObjects() UserDefaults.standard.reset() - UserDefaults.standard.synchronize() } } } @@ -137,7 +135,7 @@ struct AppSettings: View { totalDownloadedTileSize = tileManager.getAllDownloadedSize() }) } - .navigationTitle("app.settings") + .navigationTitle("appsettings") .navigationBarItems(trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") @@ -151,9 +149,6 @@ struct AppSettings: View { self.bleManager.context = context } } - .onChange(of: blockRangeTest) { newBlockRangeTest in - UserDefaults.blockRangeTest = newBlockRangeTest - } .onChange(of: provideLocation) { newProvideLocation in UserDefaults.provideLocation = newProvideLocation if bleManager.connectedPeripheral != nil { diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 88bcd2ab..491dcdf8 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -1,6 +1,6 @@ // -// ShareChannel.swift -// MeshtasticApple +// Channels.swift +// Meshtastic Apple // // Copyright(c) Garth Vander Houwen 4/8/22. // @@ -25,12 +25,11 @@ struct Channels: View { var node: NodeInfoEntity? @State var hasChanges = false - @State var hasValidKey = false @State private var isPresentingEditView = false @State private var isPresentingSaveConfirm: Bool = false @State private var channelIndex: Int32 = 0 @State private var channelName = "" - @State private var channelKeySize = 32 + @State private var channelKeySize = 16 @State private var channelKey = "AQ==" @State private var channelRole = 0 @State private var uplink = false @@ -90,7 +89,7 @@ struct Channels: View { if node?.myInfo?.channels?.array.count ?? 0 < 8 && node != nil { Button { - let key = generateChannelKey(size: 32) + let key = generateChannelKey(size: 16) channelName = "" channelIndex = Int32(node!.myInfo!.channels!.array.count) channelRole = 2 @@ -168,34 +167,16 @@ struct Channels: View { HStack(alignment: .top) { Text("Key") Spacer() - TextField( - "Key", - text: $channelKey - ) - .padding(4) - .disableAutocorrection(true) - .keyboardType(.alphabet) - .foregroundColor(Color.gray) - .textSelection(.enabled) - .background( - RoundedRectangle(cornerRadius: 25.0) - .stroke( - hasValidKey ? - Color.green : - Color.red - , lineWidth: 2.0) - ) - .onChange(of: channelKey, perform: { _ in - let tempKey = Data(base64Encoded: channelKey) ?? Data() - if tempKey.count == channelKeySize || channelKeySize == -1{ - hasValidKey = true - } - else { - hasValidKey = false - } - hasChanges = true - }) - .disabled(channelKeySize <= 0) + Text(channelKey) + .foregroundColor(Color.gray) + .textSelection(.enabled) +// TextField( +// "", +// text: $channelKey, +// axis: .vertical +// ) +// .foregroundColor(Color.gray) +// .disabled(true) } Picker("Channel Role", selection: $channelRole) { if channelRole == 1 { @@ -275,7 +256,7 @@ struct Channels: View { } label: { Label("save", systemImage: "square.and.arrow.down") } - .disabled(bleManager.connectedPeripheral == nil || !hasChanges || !hasValidKey) + .disabled(bleManager.connectedPeripheral == nil || !hasChanges) .buttonStyle(.bordered) .buttonBorderShape(.capsule) .controlSize(.large) diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index b746dfcb..9b7b33a2 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -176,7 +176,7 @@ struct LoRaConfig: View { .scrollDismissesKeyboard(.immediately) .focused($focusedField, equals: .channelNum) } - Text("This determines the actual frequency you are transmitting on in the band.") + Text("This determines the actual frequency you are transmitting on in the band. If set to 0 this value will be calculated automatically based on the primary channel name.") .font(.caption) Toggle(isOn: $rxBoostedGain) { Label("RX Boosted Gain", systemImage: "waveform.badge.plus") diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index be78fb96..f1ce1aa3 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -30,6 +30,15 @@ struct MQTTConfig: View { var body: some View { VStack { Form { + if node != nil && node?.loRaConfig != nil { + let rc = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0)) + if rc?.dutyCycle ?? 0 <= 10 { + Text("Your region has a \(rc?.dutyCycle ?? 0)% duty cycle. MQTT is not advised when you are duty cycle restricted, the extra traffice will quickly overwhelm your LoRa mesh.") + .font(.callout) + .foregroundColor(.red) + } + } + if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { Text("There has been no response to a request for device metadata over the admin channel for this node.") .font(.callout) @@ -88,7 +97,7 @@ struct MQTTConfig: View { Label("JSON Enabled", systemImage: "ellipsis.curlybraces") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("JSON mode is a limited, unencrypted MQTT output.") + Text("JSON mode is a limited, unencrypted MQTT output that can crash your node it should not be enabled unless you are locally integrating with home assistant") .font(.caption2) Toggle(isOn: $tlsEnabled) { @@ -304,19 +313,11 @@ struct MQTTConfig: View { .onChange(of: encryptionEnabled) { newEncryptionEnabled in if node != nil && node?.mqttConfig != nil { if newEncryptionEnabled != node!.mqttConfig!.encryptionEnabled { hasChanges = true } - if newEncryptionEnabled { - jsonEnabled = false - } } } .onChange(of: jsonEnabled) { newJsonEnabled in if node != nil && node?.mqttConfig != nil { if newJsonEnabled != node!.mqttConfig!.jsonEnabled { hasChanges = true } - - if newJsonEnabled { - encryptionEnabled = false - proxyToClientEnabled = false - } } } .onChange(of: tlsEnabled) { newTlsEnabled in diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index a92f06ef..dfbfb9c2 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -36,14 +36,16 @@ struct PositionConfig: View { @State var smartPositionEnabled = true @State var deviceGpsEnabled = true + @State var gpsMode = 0 @State var rxGpio = 0 @State var txGpio = 0 @State var gpsEnGpio = 0 @State var fixedPosition = false + @State var gpsUpdateInterval = 0 @State var positionBroadcastSeconds = 0 @State var broadcastSmartMinimumDistance = 0 @State var broadcastSmartMinimumIntervalSecs = 0 - @State var positionFlags = 3 + @State var positionFlags = 811 /// Position Flags /// Altitude value - 1 @@ -143,6 +145,34 @@ struct PositionConfig: View { .font(.caption) } } + Section(header: Text("Device GPS")) { + Picker("", selection: $gpsMode) { + ForEach(GpsMode.allCases, id: \.self) { at in + Text(at.description) + .tag(at.id) + } + } + .pickerStyle(SegmentedPickerStyle()) + .padding(.top, 5) + .padding(.bottom, 5) + + if gpsMode == 1 { + Picker("Update Interval", selection: $gpsUpdateInterval) { + ForEach(GpsUpdateIntervals.allCases) { ui in + Text(ui.description) + } + } + Text("How often should we try to get a GPS position.") + .font(.caption) + } else { + Toggle(isOn: $fixedPosition) { + Label("Fixed Position", systemImage: "location.square.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("If enabled your current phone location will be sent to the device and will broadcast over the mesh on the position interval. Fixed positon will always use the most recent position the device has.") + .font(.caption) + } + } Section(header: Text("Position Flags")) { Text("Optional fields to include when assembling position messages. the more fields are included, the larger the message will be - leading to longer airtime and a higher risk of packet loss") @@ -205,13 +235,9 @@ struct PositionConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } } - Section(header: Text("Device GPS")) { - Toggle(isOn: $deviceGpsEnabled) { - Label("Device GPS Enabled", systemImage: "location") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - if deviceGpsEnabled { - + + if gpsMode == 1 { + Section(header: Text("Advanced Device GPS")) { Picker("GPS Receive GPIO", selection: $rxGpio) { ForEach(0..<49) { if $0 == 0 { @@ -244,13 +270,6 @@ struct PositionConfig: View { .pickerStyle(DefaultPickerStyle()) Text("(Re)define PIN_GPS_EN for your board.") .font(.caption) - } else { - Toggle(isOn: $fixedPosition) { - Label("Fixed Position", systemImage: "location.square.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("If enabled your current location will be set as a fixed position.") - .font(.caption) } } } @@ -283,8 +302,10 @@ struct PositionConfig: View { if connectedNode != nil { var pc = Config.PositionConfig() pc.positionBroadcastSmartEnabled = smartPositionEnabled - pc.gpsEnabled = deviceGpsEnabled + pc.gpsEnabled = gpsMode == 1 + pc.gpsMode = Config.PositionConfig.GpsMode(rawValue: gpsMode) ?? Config.PositionConfig.GpsMode.notPresent pc.fixedPosition = fixedPosition + pc.gpsUpdateInterval = UInt32(gpsUpdateInterval) pc.positionBroadcastSecs = UInt32(positionBroadcastSeconds) pc.broadcastSmartMinimumIntervalSecs = UInt32(broadcastSmartMinimumIntervalSecs) pc.broadcastSmartMinimumDistance = UInt32(broadcastSmartMinimumDistance) @@ -342,6 +363,11 @@ struct PositionConfig: View { if newDeviceGps != node!.positionConfig!.deviceGpsEnabled { hasChanges = true } } } + .onChange(of: gpsMode) { newGpsMode in + if node != nil && node!.positionConfig != nil { + if newGpsMode != node!.positionConfig!.gpsMode { hasChanges = true } + } + } .onChange(of: rxGpio) { newRxGpio in if node != nil && node!.positionConfig != nil { if newRxGpio != node!.positionConfig!.rxGpio { hasChanges = true } @@ -382,6 +408,11 @@ struct PositionConfig: View { if newBroadcastSmartMinimumDistance != node!.positionConfig!.broadcastSmartMinimumDistance { hasChanges = true } } } + .onChange(of: gpsUpdateInterval) { newGpsUpdateInterval in + if node != nil && node!.positionConfig != nil { + if newGpsUpdateInterval != node!.positionConfig!.gpsUpdateInterval { hasChanges = true } + } + } .onChange(of: includeAltitude) { altFlag in let pf = PositionFlags(rawValue: self.positionFlags) let existingValue = pf.contains(.Altitude) @@ -440,11 +471,16 @@ struct PositionConfig: View { } func setPositionValues() { self.smartPositionEnabled = node?.positionConfig?.smartPositionEnabled ?? true - self.deviceGpsEnabled = node?.positionConfig?.deviceGpsEnabled ?? true + self.deviceGpsEnabled = node?.positionConfig?.deviceGpsEnabled ?? false + self.gpsMode = Int(node?.positionConfig?.gpsMode ?? 0) + if node?.positionConfig?.deviceGpsEnabled ?? false && gpsMode != 1 { + self.gpsMode = 1 + } self.rxGpio = Int(node?.positionConfig?.rxGpio ?? 0) self.txGpio = Int(node?.positionConfig?.txGpio ?? 0) self.gpsEnGpio = Int(node?.positionConfig?.gpsEnGpio ?? 0) self.fixedPosition = node?.positionConfig?.fixedPosition ?? false + self.gpsUpdateInterval = Int(node?.positionConfig?.gpsUpdateInterval ?? 30) self.positionBroadcastSeconds = Int(node?.positionConfig?.positionBroadcastSeconds ?? 900) self.broadcastSmartMinimumIntervalSecs = Int(node?.positionConfig?.broadcastSmartMinimumIntervalSecs ?? 30) self.broadcastSmartMinimumDistance = Int(node?.positionConfig?.broadcastSmartMinimumDistance ?? 50) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 66d1e02e..65a94f63 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -57,7 +57,7 @@ struct Settings: View { } label: { Image(systemName: "gearshape") .symbolRenderingMode(.hierarchical) - Text("app.settings") + Text("appsettings") } .tag(SettingsSidebar.appSettings) if #available(iOS 17.0, macOS 14.0, *) { @@ -312,13 +312,15 @@ struct Settings: View { } } .onAppear { - self.preferredNodeNum = UserDefaults.preferredPeripheralNum - if nodes.count > 1 { - if selectedNode == 0 { + if self.preferredNodeNum == 0 { + self.preferredNodeNum = UserDefaults.preferredPeripheralNum + if nodes.count > 1 { + if selectedNode == 0 { + self.selectedNode = Int(bleManager.connectedPeripheral != nil ? UserDefaults.preferredPeripheralNum : 0) + } + } else { self.selectedNode = Int(bleManager.connectedPeripheral != nil ? UserDefaults.preferredPeripheralNum : 0) } - } else { - self.selectedNode = Int(bleManager.connectedPeripheral != nil ? UserDefaults.preferredPeripheralNum : 0) } } .listStyle(GroupedListStyle()) diff --git a/README.md b/README.md index c2336593..ec8a8b5c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## Overview -SwiftUI client applicaitons for iOS, iPadOS and macOS. +SwiftUI client applications for iOS, iPadOS and macOS. ## OS Requirements diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index 7a3be89c..548a9b04 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -14,7 +14,9 @@ "always.on"="Immer an"; "ambient.lighting"="Ambient Lighting"; "ambient.lighting.config"="Ambient Lighting Config"; -"app.settings"="App Einstellungen"; +"appsettings"="App Einstellungen"; +"appsettings.provide.location"="Standort im Mesh veröffentlichen"; +"appsettings.smartposition"="Smart Position"; "are.you.sure"="Bist Du sicher?"; "ascii.capable"="ASCII fähig"; "available.radios"="Geräte in der Nähe"; @@ -60,6 +62,7 @@ "current"="Current"; "default"="Standard"; "delete"="Löschen"; +"detection.sensor"="Detection Sensor"; "device"="Gerät"; "device.config"="Gerätekonfiguration"; "device.metrics.delete"="Delete all device metrics?"; @@ -222,7 +225,6 @@ "position"="Position"; "position.config"="Positionseinstellungen"; "preferred.radio"="Bevorzugtes Gerät"; -"provide.location"="Standort im Mesh veröffentlichen"; "radio.configuration"="Geräteeinstellungen"; "range.test"="Entfernungstest"; "range.test.blocked"="Block Range Test"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 9d8671a1..028c3069 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -14,7 +14,9 @@ "always.on"="Always On"; "ambient.lighting"="Ambient Lighting"; "ambient.lighting.config"="Ambient Lighting Config"; -"app.settings"="App Settings"; +"appsettings"="App Settings"; +"appsettings.provide.location"="Share location"; +"appsettings.smartposition"="Smart Position"; "are.you.sure"="Are you sure?"; "ascii.capable"="ASCII Capable"; "available.radios"="Available Radios"; @@ -67,15 +69,16 @@ "device.config"="Device Config"; "device.metrics.delete"="Delete all device metrics?"; "device.metrics.log"="Device Metrics Log"; -"device.role.client"="Client (default) - App connected client."; -"device.role.clienthidden"=" Used for nodes that \"only speak when spoken to\" Turns all of the routine broadcasts but allows for ad-hoc communication. Still rebroadcasts, but with local only rebroadcast mode (known meshes only). Can be used for private operation or to dramatically reduce airtime / power consumption."; -"device.role.clientmute"="Client Mute - Same as a client except packets will not hop over this node, does not contribute to routing packets for mesh."; -"device.role.lostandfound"="Used to automatically send a text message to the mesh with the current position of the device on a frequent interval: \"I'm lost! Position: lat / long\""; -"device.role.router"="Router - Mesh packets will prefer to be routed over this node. Assumes device will operate in a standalone manner while placed in a location with a coverage advantage. WARNING: The BLE/Wi-Fi radios and the OLED screen will be put to sleep."; -"device.role.routerclient"="Router Client - Hybrid of the Client and Router roles. Similar to Router, except the Router Client can be used as both a Router and an app connected Client. BLE/Wi-Fi and OLED screen will not be put to sleep."; -"device.role.repeater"="Repeater - Mesh packets will prefer to be routed over this node. This role eliminates unnecessary overhead such as NodeInfo, DeviceTelemetry, and any other mesh packet, resulting in the device not appearing as part of the network. Please see Rebroadcast Mode for additional settings specific to this role."; -"device.role.tracker"="Tracker - For use with devices intended as a GPS tracker. Position packets sent from this device will be higher priority, with position broadcasting every two minutes. Smart Position Broadcast will default to off."; -"device.role.tak"="Used for nodes dedicated for connection to an ATAK EUD. Turns off many of the routine broadcasts to favor CoT packet stream from the Meshtastic ATAK plugin -> IMeshService -> Node"; +"device.role.client"="App connected or stand alone messaging client."; +"device.role.clientmute"="Client that does not forward packets from other devices."; +"device.role.clienthidden"="Client that only broadcasts as needed for stealth or power savings."; +"device.role.tracker"="Prioritizes broadcasting GPS position packets."; +"device.role.lostandfound"="Broadcasts location as message to default channel regularly for to assist with node recovery."; +"device.role.sensor"="Prioritizes broadcasting telemetry packets."; +"device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts."; +"device.role.repeater"="Infrastructure node for extending mesh network coverage by relaying messages with minimal overhead. Not visible in Nodes list. Best positioned in strategic locations to maximize the network's overall coverage. Device is not shown in topology."; +"device.role.router"="Infrastructure node for on extending mesh network coverage by relaying messages. Visible in Nodes list. Best positioned in strategic locations to maximize the network's overall coverage. Device is shown in topology."; +"device.role.routerclient"="Combination of both ROUTER and CLIENT. Not for mobile nodes."; "direct.messages"="Direct Messages"; "dismiss.keyboard"="Dismiss"; "display"="Display (Device Screen)"; @@ -100,6 +103,9 @@ "gpsformat.mgrs"="Military Grid Reference System"; "gpsformat.olc"="Open Location Code (aka Plus Codes)"; "gpsformat.osgr"="Ordnance Survey Grid Reference"; +"gpsmode.disabled"="Disabled"; +"gpsmode.enabled"="Enabled"; +"gpsmode.notPresent"="Not Present"; "heard"="Heard"; "heard.last"="Last Heard"; "hybrid"="Hybrid"; @@ -226,7 +232,6 @@ "position"="Position"; "position.config"="Position Config"; "preferred.radio"="Preferred Radio"; -"provide.location"="Share location"; "radio.configuration"="Radio Configuration"; "range.test"="Range Test"; "range.test.blocked"="Block Range Test"; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index 73b8cef2..bea348c0 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -16,7 +16,9 @@ "always.on"="Zawsze włączone"; "ambient.lighting"="Ambient Lighting"; "ambient.lighting.config"="Ambient Lighting Config"; -"app.settings"="Ustawienia aplikacji"; +"appsettings"="Ustawienia aplikacji"; +"appsettings.provide.location"="Udostępnij lokalizację"; +"appsettings.smartposition"="Smart Position"; "are.you.sure"="Jesteś pewny?"; "ascii.capable"="Zgodny z ASCII"; "available.radios"="Dostępne radia"; @@ -62,6 +64,7 @@ "current"="Bieżący"; "default"="Domyślny"; "delete"="Usuń"; +"detection.sensor"="Detection Sensor"; "device"="Urządzenie"; "device.config"="Konfiguracja urządzenia"; "device.metrics.delete"="Usunąć wszystkie metryki urządzenia?"; @@ -223,7 +226,6 @@ "position"="Pozycja"; "position.config"="Konfiguracja pozycji"; "preferred.radio"="Preferowane radio"; -"provide.location"="Udostępnij lokalizację"; "radio.configuration"="Konfiguracja radia"; "range.test"="Test zasięgu"; "range.test.blocked"="Block Range Test"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 811cb242..178833e2 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -14,7 +14,9 @@ "always.on"="常亮"; "ambient.lighting"="Ambient Lighting"; "ambient.lighting.config"="Ambient Lighting Config"; -"app.settings"="通用设置"; +"appsettings"="通用设置"; +"appsettings.provide.location"="提供定位到 Mesh 网络"; +"appsettings.smartposition"="Smart Position"; "are.you.sure"="是否确认?"; "ascii.capable"="ASCII Capable"; "available.radios"="可以连接的电台"; @@ -60,6 +62,7 @@ "current"="当前"; "default"="默认"; "delete"="删除"; +"detection.sensor"="Detection Sensor"; "device"="电台"; "device.config"="电台配置"; "device.metrics.delete"="删除所有电台指标?"; @@ -222,7 +225,6 @@ "position"="定位"; "position.config"="定位配置"; "preferred.radio"="首选电台"; -"provide.location"="提供定位到 Mesh 网络"; "radio.configuration"="电台配置"; "range.test"="拉距测试"; "range.test.blocked"="区块范围测试";