From 1c4b0c0cb391c2f98787aede4dfa60fddd175dcf Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 26 Nov 2023 23:17:19 -0800 Subject: [PATCH 01/20] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index b01e0ee0..949ca08b 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1460,7 +1460,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.13; + MARKETING_VERSION = 2.2.14; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1494,7 +1494,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.13; + MARKETING_VERSION = 2.2.14; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1616,7 +1616,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.13; + MARKETING_VERSION = 2.2.14; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1649,7 +1649,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.13; + MARKETING_VERSION = 2.2.14; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From 67900bfa512b4420bc8baa0ed2d86e274efdc2fd Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 28 Nov 2023 20:03:08 -0800 Subject: [PATCH 02/20] Remove GPS Update interval Remove GPS Attempt Time options Don't show positions with empty times in the elevation graph Hook up ambient lighitng config --- Meshtastic.xcodeproj/project.pbxproj | 4 + Meshtastic/Enums/PositionConfigEnums.swift | 87 +--------- Meshtastic/Helpers/BLEManager.swift | 54 ++++++ Meshtastic/Helpers/MeshPackets.swift | 8 +- Meshtastic/Persistence/UpdateCoreData.swift | 56 +++++++ .../Helpers/Map/PositionAltitudeChart.swift | 4 +- .../Config/Module/AmbientLightingConfig.swift | 157 ++++++++++++++++++ .../Settings/Config/PositionConfig.swift | 14 +- Meshtastic/Views/Settings/Settings.swift | 11 ++ 9 files changed, 293 insertions(+), 102 deletions(-) create mode 100644 Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 949ca08b..1ac11fbe 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -41,6 +41,7 @@ DD457188293C7E63000C49FB /* BLESignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */; }; DD4640202AFF10F4002A5ECB /* WaypointForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD46401F2AFF10F4002A5ECB /* WaypointForm.swift */; }; DD47E3D626F17ED900029299 /* CircleText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3D526F17ED900029299 /* CircleText.swift */; }; + DD4975A52B147BA90026544E /* AmbientLightingConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4975A42B147BA90026544E /* AmbientLightingConfig.swift */; }; DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A911D2708C65400501B7E /* AppSettings.swift */; }; DD4F23CD28779A3C001D37CB /* EnvironmentMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */; }; DD5394FC276993AD00AD86B1 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = DD5394FB276993AD00AD86B1 /* SwiftProtobuf */; }; @@ -254,6 +255,7 @@ DD457BC4295D5E35004BCE4D /* MeshtasticDataModelV5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV5.xcdatamodel; sourceTree = ""; }; DD46401F2AFF10F4002A5ECB /* WaypointForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointForm.swift; sourceTree = ""; }; DD47E3D526F17ED900029299 /* CircleText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleText.swift; sourceTree = ""; }; + DD4975A42B147BA90026544E /* AmbientLightingConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmbientLightingConfig.swift; sourceTree = ""; }; DD4A911D2708C65400501B7E /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentMetricsLog.swift; sourceTree = ""; }; DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionEntityExtension.swift; sourceTree = ""; }; @@ -592,6 +594,7 @@ DD61937B2863877A00E59241 /* Module */ = { isa = PBXGroup; children = ( + DD4975A42B147BA90026544E /* AmbientLightingConfig.swift */, DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */, DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */, DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */, @@ -1216,6 +1219,7 @@ DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */, DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */, DDB6ABE428B13FFF00384BA1 /* DisplayEnums.swift in Sources */, + DD4975A52B147BA90026544E /* AmbientLightingConfig.swift in Sources */, DD86D40A287F04F100BAEB7A /* InvalidVersion.swift in Sources */, DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */, DD5E5212298EE33B00D21B61 /* apponly.pb.swift in Sources */, diff --git a/Meshtastic/Enums/PositionConfigEnums.swift b/Meshtastic/Enums/PositionConfigEnums.swift index 69e155ba..171df3a5 100644 --- a/Meshtastic/Enums/PositionConfigEnums.swift +++ b/Meshtastic/Enums/PositionConfigEnums.swift @@ -53,52 +53,18 @@ enum GpsFormats: Int, CaseIterable, Identifiable { } } -enum GpsUpdateIntervals: Int, CaseIterable, Identifiable { +enum GpsAttemptTimes: Int, CaseIterable, Identifiable { - case fiveSeconds = 5 - case tenSeconds = 10 - case fifteenSeconds = 15 - case twentySeconds = 20 - case twentyFiveSeconds = 25 - case thirtySeconds = 30 - case fortyFiveSeconds = 45 - case oneMinute = 60 - case twoMinutes = 120 - 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 .fiveSeconds: - return "interval.five.seconds".localized - case .tenSeconds: - return "interval.ten.seconds".localized - case .fifteenSeconds: - return "interval.fifteen.seconds".localized - case .twentySeconds: - return "interval.twenty.seconds".localized - case .twentyFiveSeconds: - return "interval.twentyfive.seconds".localized - case .thirtySeconds: - return "interval.thirty.seconds".localized - case .fortyFiveSeconds: - return "interval.fortyfive.seconds".localized - case .oneMinute: - return "interval.one.minute".localized - case .twoMinutes: - return "interval.two.minutes".localized - case .fiveMinutes: - return "interval.five.minutes".localized - case .tenMinutes: - return "interval.ten.minutes".localized case .fifteenMinutes: return "interval.fifteen.minutes".localized case .thirtyMinutes: @@ -111,57 +77,6 @@ enum GpsUpdateIntervals: Int, CaseIterable, Identifiable { return "interval.twelve.hours".localized case .twentyFourHours: return "interval.twentyfour.hours".localized - case .maxInt32: - return "on.boot" - } - } -} - -enum GpsAttemptTimes: Int, CaseIterable, Identifiable { - - case twoSeconds = 2 - case fiveSeconds = 5 - case tenSeconds = 10 - case fifteenSeconds = 15 - case twentySeconds = 20 - case twentyFiveSeconds = 25 - case thirtySeconds = 30 - case fortyFiveSeconds = 45 - case oneMinute = 60 - case twoMinutes = 120 - case fiveMinutes = 300 - case tenMinutes = 600 - case fifteenMinutes = 900 - - var id: Int { self.rawValue } - var description: String { - switch self { - case .twoSeconds: - return "interval.two.seconds".localized - case .fiveSeconds: - return "interval.five.seconds".localized - case .tenSeconds: - return "interval.ten.seconds".localized - case .fifteenSeconds: - return "interval.fifteen.seconds".localized - case .twentySeconds: - return "interval.twenty.seconds".localized - case .twentyFiveSeconds: - return "interval.twentyfive.seconds".localized - case .thirtySeconds: - return "interval.thirty.seconds".localized - case .fortyFiveSeconds: - return "interval.fortyfive.seconds".localized - case .oneMinute: - return "interval.one.minute".localized - case .twoMinutes: - return "interval.two.minutes".localized - case .fiveMinutes: - return "interval.five.minutes".localized - case .tenMinutes: - return "interval.ten.minutes".localized - case .fifteenMinutes: - return "interval.fifteen.minutes".localized } } } diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index f49dcc78..bf5b0949 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1359,6 +1359,33 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return 0 } + public func saveAmbientLightingModuleConfig(config: ModuleConfig.AmbientLightingConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { + + var adminPacket = AdminMessage() + adminPacket.setModuleConfig.ambientLighting = config + + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.channel = UInt32(adminIndex) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() @@ -1858,6 +1885,33 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return false } + public func requestAmbientLightingConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { + + var adminPacket = AdminMessage() + adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.ambientlightingConfig + + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index f11e57b0..eb8b1abd 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -56,7 +56,9 @@ func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int6 func moduleConfig (config: ModuleConfig, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) { - if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(config.cannedMessage) { + if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.ambientLighting(config.ambientLighting) { + upsertAmbientLightingModuleConfigPacket(config: config.ambientLighting, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(config.cannedMessage) { upsertCannedMessagesModuleConfigPacket(config: config.cannedMessage, nodeNum: nodeNum, context: context) } else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.detectionSensor(config.detectionSensor) { upsertDetectionSensorModuleConfigPacket(config: config.detectionSensor, nodeNum: nodeNum, context: context) @@ -472,7 +474,9 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { } } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getModuleConfigResponse(adminMessage.getModuleConfigResponse) { let moduleConfig = adminMessage.getModuleConfigResponse - if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(moduleConfig.cannedMessage) { + if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.ambientLighting(moduleConfig.ambientLighting) { + upsertAmbientLightingModuleConfigPacket(config: moduleConfig.ambientLighting, nodeNum: Int64(packet.from), context: context) + } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(moduleConfig.cannedMessage) { upsertCannedMessagesModuleConfigPacket(config: moduleConfig.cannedMessage, nodeNum: Int64(packet.from), context: context) } else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.detectionSensor(moduleConfig.detectionSensor) { upsertDetectionSensorModuleConfigPacket(config: moduleConfig.detectionSensor, nodeNum: Int64(packet.from), context: context) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index a02837c3..198226bc 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -624,6 +624,62 @@ func upsertPositionConfigPacket(config: Meshtastic.Config.PositionConfig, nodeNu } } +func upsertAmbientLightingModuleConfigPacket(config: Meshtastic.ModuleConfig.AmbientLightingConfig, nodeNum: Int64, context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("mesh.log.ambientlighting.config %@".localized, String(nodeNum)) + MeshLogger.log("🏮 \(logString)") + + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + + guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else { + return + } + // Found a node, save Ambient Lighting Config + if !fetchedNode.isEmpty { + + if fetchedNode[0].cannedMessageConfig == nil { + + let newAmbientLightingConfig = AmbientLightingConfigEntity(context: context) + + newAmbientLightingConfig.ledState = config.ledState + newAmbientLightingConfig.current = Int32(config.current) + newAmbientLightingConfig.red = Int32(config.red) + newAmbientLightingConfig.green = Int32(config.green) + newAmbientLightingConfig.blue = Int32(config.blue) + fetchedNode[0].ambientLightingConfig = newAmbientLightingConfig + + } else { + + if fetchedNode[0].ambientLightingConfig == nil { + fetchedNode[0].ambientLightingConfig = AmbientLightingConfigEntity(context: context) + } + fetchedNode[0].ambientLightingConfig?.ledState = config.ledState + fetchedNode[0].ambientLightingConfig?.current = Int32(config.current) + fetchedNode[0].ambientLightingConfig?.red = Int32(config.red) + fetchedNode[0].ambientLightingConfig?.green = Int32(config.green) + fetchedNode[0].ambientLightingConfig?.blue = Int32(config.blue) + } + + do { + try context.save() + print("💾 Updated Ambient Lighting Module Config for node number: \(String(nodeNum))") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Updating Core Data AmbientLightingConfigEntity: \(nsError)") + } + } else { + print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Ambient Lighting Module Config") + } + } catch { + let nsError = error as NSError + print("💥 Fetching node for core data AmbientLightingConfigEntity failed: \(nsError)") + } +} + func upsertCannedMessagesModuleConfigPacket(config: Meshtastic.ModuleConfig.CannedMessageConfig, nodeNum: Int64, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.cannedmessage.config %@".localized, String(nodeNum)) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift index eba287e7..4f69bd2a 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionAltitudeChart.swift @@ -23,8 +23,10 @@ struct PositionAltitudeChart: View { @State private var lineWidth = 2.0 var body: some View { + let fiveYearsAgo = Calendar.current.date(byAdding: .year, value: -5, to: Date()) let nodePositions = Array(node.positions!) as! [PositionEntity] - let data = nodePositions.map { PositionAltitude(time: $0.time ?? Date(), altitude: Measurement(value: Double($0.altitude), unit: .meters) ) } + let filteredPositions = nodePositions.filter({$0.time != nil && ($0.time ?? fiveYearsAgo!) > fiveYearsAgo!}) + let data = filteredPositions.map { PositionAltitude(time: $0.time ?? Date(), altitude: Measurement(value: Double($0.altitude), unit: .meters) ) } GroupBox(label: Label("Altitude", systemImage: "mountain.2")) { Chart(data, id: \.time) { diff --git a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift new file mode 100644 index 00000000..17a44545 --- /dev/null +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -0,0 +1,157 @@ +// +// AmbientLightingConfig.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 11/26/23 +// + +import SwiftUI +@available(iOS 17.0, macOS 14.0, *) +struct AmbientLightingConfig: View { + @Environment(\.self) var environment + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + @Environment(\.dismiss) private var goBack + + var node: NodeInfoEntity? + + @State private var isPresentingSaveConfirm: Bool = false + @State var hasChanges = false + @State var ledState: Bool = false + @State var current = 10 + @State var red = 0 + @State var green = 0 + @State var blue = 0 + @State private var color = Color(red: 51, green: 199, blue: 88) // Color(.sRGB, red: 0.98, green: 0.9, blue: 0.2) + @State private var components: Color.Resolved? + var body: some View { + VStack { + Form { + 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) + .foregroundColor(.orange) + + } else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { + // Let users know what is going on if they are using remote admin and don't have the config yet + if node?.rtttlConfig == nil { + Text("Ambient Lighting config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.") + .font(.callout) + .foregroundColor(.orange) + } else { + Text("Remote administration for: \(node?.user?.longName ?? "Unknown")") + .font(.title3) + .onAppear { + setAmbientLightingConfigValue() + } + } + } else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 { + Text("Configuration for: \(node?.user?.longName ?? "Unknown")") + .font(.title3) + } else { + Text("Please connect to a radio to configure settings.") + .font(.callout) + .foregroundColor(.orange) + } + Section(header: Text("options")) { + VStack { + Toggle(isOn: $ledState) { + Label("LED State", systemImage: ledState ? "lightbulb.led.fill" : "lightbulb.led") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + HStack { + Image(systemName: "eyedropper") + .foregroundColor(.accentColor) + ColorPicker("Color", selection: $color, supportsOpacity: false) + .padding(5) + } + HStack { + Image(systemName: "directcurrent") + .foregroundColor(.accentColor) + Stepper("Current: \(current)", value: $current, in: 0...31, step: 1) + .padding(5) + } + + } + .onChange(of: color, initial: true) { + components = color.resolve(in: environment) + hasChanges = true + } + } + } + //.disabled(self.bleManager.connectedPeripheral == nil || node?.ambientLightingConfig == nil) + Button { + isPresentingSaveConfirm = true + } label: { + Label("save", systemImage: "square.and.arrow.down") + } + .disabled(self.bleManager.connectedPeripheral == nil || !hasChanges) + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .confirmationDialog( + "are.you.sure", + isPresented: $isPresentingSaveConfirm, + titleVisibility: .visible + ) { + let nodeName = node?.user?.longName ?? "unknown".localized + let buttonText = String.localizedStringWithFormat("save.config %@".localized, nodeName) + Button(buttonText) { + + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) + if connectedNode != nil { + var al = ModuleConfig.AmbientLightingConfig() + al.ledState = ledState + al.current = UInt32(current) + if let components { + al.red = UInt32(components.red * 255) + al.green = UInt32(components.green * 255) + al.blue = UInt32(components.blue * 255) + } + + let adminMessageId = bleManager.saveAmbientLightingModuleConfig(config: al, 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 + // for now just disable the button after a successful save + hasChanges = false + goBack() + } + } + } + } + message: { + Text("config.save.confirm") + } + .navigationTitle("ambient.lighting.config") + .navigationBarItems(trailing: + ZStack { + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") + }) + .onAppear { + self.bleManager.context = context + setAmbientLightingConfigValue() + // Need to request a Ambient Lighting Config from the remote node before allowing changes + if bleManager.connectedPeripheral != nil && node?.ambientLightingConfig == nil { + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) + if node != nil && connectedNode != nil { + _ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + } + } + } + .onChange(of: ledState) { newLedState in + if node != nil && node!.ambientLightingConfig != nil { + if newLedState != node!.ambientLightingConfig!.ledState { hasChanges = true } + } + } + } + } + func setAmbientLightingConfigValue() { + self.ledState = node?.ambientLightingConfig?.ledState ?? false + self.current = Int(node?.ambientLightingConfig?.current ?? 10) + color = Color(red: Double((node?.ambientLightingConfig?.red ?? 255) / 255), + green: Double((node?.ambientLightingConfig?.green ?? 255) / 255), + blue: Double((node?.ambientLightingConfig?.blue ?? 255) / 255)) + self.hasChanges = false + } +} diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 8ad8397c..a0205ae2 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -212,13 +212,7 @@ struct PositionConfig: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) if deviceGpsEnabled { - 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) + Picker("Attempt Time", selection: $gpsAttemptTime) { ForEach(GpsAttemptTimes.allCases) { at in Text(at.description) @@ -286,7 +280,6 @@ struct PositionConfig: View { pc.positionBroadcastSmartEnabled = smartPositionEnabled pc.gpsEnabled = deviceGpsEnabled pc.fixedPosition = fixedPosition - pc.gpsUpdateInterval = UInt32(gpsUpdateInterval) pc.gpsAttemptTime = UInt32(gpsAttemptTime) pc.positionBroadcastSecs = UInt32(positionBroadcastSeconds) pc.broadcastSmartMinimumIntervalSecs = UInt32(broadcastSmartMinimumIntervalSecs) @@ -359,11 +352,6 @@ struct PositionConfig: View { if newGpsAttemptTime != node!.positionConfig!.gpsAttemptTime { hasChanges = true } } } - .onChange(of: gpsUpdateInterval) { newGpsUpdateInterval in - if node != nil && node!.positionConfig != nil { - if newGpsUpdateInterval != node!.positionConfig!.gpsUpdateInterval { hasChanges = true } - } - } .onChange(of: smartPositionEnabled) { newSmartPositionEnabled in if node != nil && node!.positionConfig != nil { if newSmartPositionEnabled != node!.positionConfig!.smartPositionEnabled { hasChanges = true } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 7fb37da8..16423c60 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -27,6 +27,7 @@ struct Settings: View { case displayConfig case networkConfig case positionConfig + case ambientLightingConfig case cannedMessagesConfig case detectionSensorConfig case externalNotificationConfig @@ -187,6 +188,16 @@ struct Settings: View { .tag(SettingsSidebar.positionConfig) } Section("module.configuration") { + if #available(iOS 17.0, macOS 14.0, *) { + NavigationLink { + AmbientLightingConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + Image(systemName: "light.max") + .symbolRenderingMode(.hierarchical) + Text("ambient.lighting") + } + .tag(SettingsSidebar.ambientLightingConfig) + } NavigationLink { CannedMessagesConfig(node: nodes.first(where: { $0.num == selectedNode })) } label: { From 6c2e3cd316698ae05ba1fa073a8ca3071e6947fc Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 28 Nov 2023 21:18:59 -0800 Subject: [PATCH 03/20] Hook up the color picker --- .../Settings/Config/Module/AmbientLightingConfig.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift index 17a44545..f5e9dc26 100644 --- a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -149,9 +149,10 @@ struct AmbientLightingConfig: View { func setAmbientLightingConfigValue() { self.ledState = node?.ambientLightingConfig?.ledState ?? false self.current = Int(node?.ambientLightingConfig?.current ?? 10) - color = Color(red: Double((node?.ambientLightingConfig?.red ?? 255) / 255), - green: Double((node?.ambientLightingConfig?.green ?? 255) / 255), - blue: Double((node?.ambientLightingConfig?.blue ?? 255) / 255)) + let red = Double(node?.ambientLightingConfig?.red ?? 255) + let green = Double(node?.ambientLightingConfig?.green ?? 255) + let blue = Double(node?.ambientLightingConfig?.blue ?? 255) + color = Color(red: red / 255.0, green: green / 255.0, blue: blue / 255.0) self.hasChanges = false } } From 9c57bdd4c427c12bc530b9fc23e6b7d16f02f91f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 29 Nov 2023 23:01:51 -0800 Subject: [PATCH 04/20] Update detection sensor log query to update in real time --- Meshtastic/Persistence/QueryCoreData.swift | 18 ------------------ .../Views/Nodes/DetectionSensorLog.swift | 5 ++++- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/Meshtastic/Persistence/QueryCoreData.swift b/Meshtastic/Persistence/QueryCoreData.swift index fa200d8a..32cbffc0 100644 --- a/Meshtastic/Persistence/QueryCoreData.swift +++ b/Meshtastic/Persistence/QueryCoreData.swift @@ -82,21 +82,3 @@ public func getWaypoint(id: Int64, context: NSManagedObjectContext) -> WaypointE } return WaypointEntity(context: context) } - -public func getDetectionSensorMessages(nodeNum: Int64?, context: NSManagedObjectContext) -> [MessageEntity] { - - let fetchDetectionMessagesPredicate: NSFetchRequest = NSFetchRequest.init(entityName: "MessageEntity") - fetchDetectionMessagesPredicate.predicate = NSPredicate(format: "portNum == %d", Int32(PortNum.detectionSensorApp.rawValue)) - - do { - let fetched = try context.fetch(fetchDetectionMessagesPredicate) as? [MessageEntity] ?? [] - if nodeNum == nil { - return fetched.reversed() - } - return fetched.filter { message in - return message.fromUser?.num == nodeNum! - }.reversed() - } catch { - return [] - } -} diff --git a/Meshtastic/Views/Nodes/DetectionSensorLog.swift b/Meshtastic/Views/Nodes/DetectionSensorLog.swift index b6d13c99..27fe7198 100644 --- a/Meshtastic/Views/Nodes/DetectionSensorLog.swift +++ b/Meshtastic/Views/Nodes/DetectionSensorLog.swift @@ -15,10 +15,13 @@ struct DetectionSensorLog: View { @State var isExporting = false @State var exportString = "" @ObservedObject var node: NodeInfoEntity + + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "messageTimestamp", ascending: false)], + predicate: NSPredicate(format: "portNum == %d", Int32(PortNum.detectionSensorApp.rawValue)), animation: .none) + private var detections: FetchedResults var body: some View { let oneDayAgo = Calendar.current.date(byAdding: .day, value: -1, to: Date()) - let detections = getDetectionSensorMessages(nodeNum: node.num, context: context) let chartData = detections .filter { $0.timestamp >= oneDayAgo! } .sorted { $0.timestamp < $1.timestamp } From d70c7e60acc74ff0cec1f6cd1910348c02c476f2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 30 Nov 2023 23:41:14 -0800 Subject: [PATCH 05/20] Fix detection sensor graph --- Meshtastic/Views/Nodes/DetectionSensorLog.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Meshtastic/Views/Nodes/DetectionSensorLog.swift b/Meshtastic/Views/Nodes/DetectionSensorLog.swift index 27fe7198..bf4e7e56 100644 --- a/Meshtastic/Views/Nodes/DetectionSensorLog.swift +++ b/Meshtastic/Views/Nodes/DetectionSensorLog.swift @@ -33,7 +33,7 @@ struct DetectionSensorLog: View { ForEach(chartData, id: \.self) { point in Plot { BarMark( - x: .value("x", point.timestamp), + x: .value("x", point.timestamp, unit: .hour), y: .value("y", 1) ) } @@ -52,12 +52,8 @@ struct DetectionSensorLog: View { } .chartXAxis(content: { AxisMarks(position: .top) -// AxisMarks(position: .top, values: .stride(by: .hour)) { date in -// AxisValueLabel(format: .dateTime.hour()) -// } }) .chartXAxis(.automatic) - .chartYScale(domain: 0...20) .chartForegroundStyleScale([ "Detection events": .green ]) From d63daf5cf35bad05259241b78ee578e1d1b82487 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 1 Dec 2023 13:56:29 -0800 Subject: [PATCH 06/20] Detection sensor updates --- Meshtastic/Extensions/UserDefaults.swift | 20 ++ Meshtastic/Helpers/MeshPackets.swift | 10 +- .../Views/Nodes/DetectionSensorLog.swift | 1 + .../Config/Module/DetectionSensorConfig.swift | 177 ++++++++++++------ 4 files changed, 153 insertions(+), 55 deletions(-) diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 1c9e21fd..4ea8d2be 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -26,6 +26,8 @@ extension UserDefaults { case mapTileServer case mapTilesAboveLabels case mapUseLegacy + case enableDetectionNotifications + case detectionSensorRole } func reset() { @@ -190,4 +192,22 @@ extension UserDefaults { UserDefaults.standard.set(newValue, forKey: "mapUseLegacy") } } + + static var enableDetectionNotifications: Bool { + get { + UserDefaults.standard.bool(forKey: "enableDetectionNotifications") + } + set { + UserDefaults.standard.set(newValue, forKey: "enableDetectionNotifications") + } + } + + static var detectionSensorRole: DetectionSensorRole { + get { + DetectionSensorRole(rawValue: UserDefaults.standard.string(forKey: "detectionSensorRole") ?? DetectionSensorRole.sensor.rawValue) ?? DetectionSensorRole.sensor + } + set { + UserDefaults.standard.set(newValue.rawValue, forKey: "detectionSensorRole") + } + } } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index eb8b1abd..b8f48d7e 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -730,7 +730,11 @@ func textMessageAppPacket(packet: MeshPacket, blockRangeTest: Bool, connectedNod newMessage.isEmoji = packet.decoded.emoji == 1 newMessage.channel = Int32(packet.channel) newMessage.portNum = Int32(packet.decoded.portnum.rawValue) - + if packet.decoded.portnum == PortNum.detectionSensorApp { + if !UserDefaults.enableDetectionNotifications { + newMessage.read = true + } + } if packet.decoded.replyID > 0 { newMessage.replyID = Int64(packet.decoded.replyID) } @@ -755,6 +759,10 @@ func textMessageAppPacket(packet: MeshPacket, blockRangeTest: Bool, connectedNod messageSaved = true if messageSaved { + + if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications { + return + } let appState = AppState.shared if newMessage.fromUser != nil && newMessage.toUser != nil && !(newMessage.fromUser?.mute ?? false) { // Set Unread Message Indicators diff --git a/Meshtastic/Views/Nodes/DetectionSensorLog.swift b/Meshtastic/Views/Nodes/DetectionSensorLog.swift index bf4e7e56..410dfb11 100644 --- a/Meshtastic/Views/Nodes/DetectionSensorLog.swift +++ b/Meshtastic/Views/Nodes/DetectionSensorLog.swift @@ -93,6 +93,7 @@ struct DetectionSensorLog: View { ForEach(detections) { d in GridRow { Text(d.messagePayload ?? "Detected") + .font(.caption) Text(d.timestamp.formattedDate(format: dateFormatString)) .font(.caption) } diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index 35ab9b0e..778412eb 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -6,6 +6,20 @@ // import SwiftUI +enum DetectionSensorRole: String, CaseIterable, Equatable { + case sensor + case client + var description: String { + switch self { + case .sensor: + return "Sensor" + case .client: + return "Client" + } + } + var localized: String { self.rawValue.localized } +} + struct DetectionSensorConfig: View { @Environment(\.managedObjectContext) var context @@ -14,8 +28,10 @@ struct DetectionSensorConfig: View { var node: NodeInfoEntity? @State private var isPresentingSaveConfirm: Bool = false @State var hasChanges: Bool = false + @AppStorage("detectionSensorRole") private var role: DetectionSensorRole = .sensor + @AppStorage("enableDetectionNotifications") private var detectionNotificationsEnabled = false + /// Module Config Settings @State var enabled = false - /// DetectionSensorModule will sends a bell character with the messages. @State var sendBell: Bool = false @State var name: String = "" @State var detectionTriggeredHigh: Bool = true @@ -54,70 +70,120 @@ struct DetectionSensorConfig: View { .foregroundColor(.orange) } Section(header: Text("options")) { + Toggle(isOn: $enabled) { Label("enabled", systemImage: "dot.radiowaves.right") + Text("Enables the detection sensor module, it needs to be enabled on both the node with the sensor, and any nodes that you want to receive detection sensor text messages or view the detection sensor log and chart.") + .font(.caption) } - Toggle(isOn: $sendBell) { - Label("Send Bell", systemImage: "bell") - } - TextField("Friendly name (sent for detection alerts text messages)", text: $name, axis: .vertical) - .foregroundColor(.gray) - .autocapitalization(.none) - .disableAutocorrection(true) - .onChange(of: name, perform: { _ in - - let totalBytes = name.utf8.count - // Only mess with the value if it is too big - if totalBytes > 20 { - - let firstNBytes = Data(name.utf8.prefix(20)) - if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { - // Set the shortName back to the last place where it was the right size - name = maxBytesString + .listRowSeparator(.visible) + if enabled { + HStack { + Picker(selection: $role, label: Text("Role")) { + ForEach(DetectionSensorRole.allCases, id: \.self) { r in + Text(r.description) + .tag(r) } } - }) - .foregroundColor(.gray) + .pickerStyle(SegmentedPickerStyle()) + .padding(.top, 5) + .padding(.bottom, 5) + } + } } - Section(header: Text("Sensor option")) { - Picker("GPIO Pin to monitor", selection: $monitorPin) { - ForEach(0..<46) { - if $0 == 0 { - Text("unset") - } else { - Text("Pin \($0)") + if enabled && role == .client { + Section(header: Text("Client options")) { + Toggle(isOn: $detectionNotificationsEnabled) { + Label("Enable Notifications", systemImage: "bell.badge") + Text("Detection sensor messages are received as text messages. If you enable notifications you will recieve a notification for each detection message received and a corresponding unread message badge.") + .font(.caption) + } + .listRowSeparator(.visible) + } + } + if enabled && role == .sensor { + Section(header: Text("Sensor options")) { + Toggle(isOn: $sendBell) { + Label("Send Bell", systemImage: "bell") + Text("Send ASCII bell with alert message. Useful for triggering external notification on bell.") + .font(.caption) + } + .listRowSeparator(.visible) + HStack { + Label("Name", systemImage: "signature") + TextField("Friendly name", text: $name, axis: .vertical) + .foregroundColor(.gray) + .autocapitalization(.none) + .disableAutocorrection(true) + .onChange(of: name, perform: { _ in + + let totalBytes = name.utf8.count + // Only mess with the value if it is too big + if totalBytes > 20 { + + let firstNBytes = Data(name.utf8.prefix(20)) + if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { + // Set the shortName back to the last place where it was the right size + name = maxBytesString + } + } + }) + .foregroundColor(.gray) + } + .listRowSeparator(.hidden) + Text("Friendly name used to format message sent to mesh. Example: A name \"Motion\" would result in a message \"Motion detected\"") + .font(.caption) + .foregroundStyle(.gray) + .listRowSeparator(.visible) + .offset(y: -10) + Picker("GPIO Pin to monitor", selection: $monitorPin) { + ForEach(0..<46) { + if $0 == 0 { + Text("unset") + } else { + Text("Pin \($0)") + } } } - } - .pickerStyle(DefaultPickerStyle()) - Toggle(isOn: $detectionTriggeredHigh) { - Label("Detection trigger High", systemImage: "dial.high") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - - Toggle(isOn: $usePullup) { - Label("Uses pullup resistor", systemImage: "arrow.up.to.line") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - } - Section(header: Text("update.interval")) { - Picker("Minimum time between detection broadcasts", selection: $minimumBroadcastSecs) { - ForEach(UpdateIntervals.allCases) { ui in - Text(ui.description).tag(ui.rawValue) + .pickerStyle(DefaultPickerStyle()) + Toggle(isOn: $detectionTriggeredHigh) { + Label("Detection trigger High", systemImage: "dial.high") + Text("Whether or not the GPIO pin state detection is triggered on HIGH (1) or LOW (0)") + .font(.caption) } - } - .pickerStyle(DefaultPickerStyle()) - Text("Mininum time between detection broadcasts. Default is 45 seconds.") - .font(.caption) - Picker("State Broadcast Interval", selection: $stateBroadcastSecs) { - Text("Never").tag(0) - ForEach(UpdateIntervals.allCases) { ui in - Text(ui.description).tag(ui.rawValue) + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + + Toggle(isOn: $usePullup) { + Label("Uses pullup resistor", systemImage: "arrow.up.to.line") + Text(" Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin") + .font(.caption) } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + } + Section(header: Text("update.interval")) { + Picker("Minimum time between detection broadcasts", selection: $minimumBroadcastSecs) { + ForEach(UpdateIntervals.allCases) { ui in + Text(ui.description).tag(ui.rawValue) + } + } + .pickerStyle(DefaultPickerStyle()) + .listRowSeparator(.hidden) + Text("Mininum time between detection broadcasts. Default is 45 seconds.") + .font(.caption) + .foregroundStyle(.gray) + .listRowSeparator(.visible) + Picker("State Broadcast Interval", selection: $stateBroadcastSecs) { + Text("Never").tag(0) + ForEach(UpdateIntervals.allCases) { ui in + Text(ui.description).tag(ui.rawValue) + } + } + .pickerStyle(DefaultPickerStyle()) + .listRowSeparator(.hidden) + Text("How often to send detection sensor state to mesh regardless of detection. Default is Never.") + .font(.caption) + .foregroundStyle(.gray) } - .pickerStyle(DefaultPickerStyle()) - Text("How often to send detection sensor state to mesh regardless of detection. Default is Never.") - .font(.caption) } } .scrollDismissesKeyboard(.interactively) @@ -223,6 +289,9 @@ struct DetectionSensorConfig: View { if newStateBroadcastSecs != node!.detectionSensorConfig!.stateBroadcastSecs { hasChanges = true } } } + .onChange(of: detectionNotificationsEnabled) { newDetectionNotificationsEnabled in + UserDefaults.enableDetectionNotifications = newDetectionNotificationsEnabled + } } func setDetectionSensorValues() { self.enabled = (node?.detectionSensorConfig?.enabled ?? false) From 7b77cfc3de4d584cd833b832b0447812138d7105 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 1 Dec 2023 16:33:17 -0800 Subject: [PATCH 07/20] Ambient lighting updates --- .../Config/Module/AmbientLightingConfig.swift | 39 ++++++++++--------- de.lproj/Localizable.strings | 2 + en.lproj/Localizable.strings | 2 + pl.lproj/Localizable.strings | 2 + zh-Hans.lproj/Localizable.strings | 2 + 5 files changed, 28 insertions(+), 19 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift index f5e9dc26..bd59c988 100644 --- a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -54,24 +54,25 @@ struct AmbientLightingConfig: View { .foregroundColor(.orange) } Section(header: Text("options")) { - VStack { - Toggle(isOn: $ledState) { - Label("LED State", systemImage: ledState ? "lightbulb.led.fill" : "lightbulb.led") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - HStack { - Image(systemName: "eyedropper") - .foregroundColor(.accentColor) - ColorPicker("Color", selection: $color, supportsOpacity: false) - .padding(5) - } - HStack { - Image(systemName: "directcurrent") - .foregroundColor(.accentColor) - Stepper("Current: \(current)", value: $current, in: 0...31, step: 1) - .padding(5) - } - + Toggle(isOn: $ledState) { + Label("LED State", systemImage: ledState ? "lightbulb.led.fill" : "lightbulb.led") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.hidden) + Text("The state of the LED (on/off)") + .font(.caption) + .foregroundStyle(.gray) + HStack { + Image(systemName: "eyedropper") + .foregroundColor(.accentColor) + ColorPicker("Color", selection: $color, supportsOpacity: false) + .padding(5) + } + HStack { + Image(systemName: "directcurrent") + .foregroundColor(.accentColor) + Stepper("Current: \(current)", value: $current, in: 0...31, step: 1) + .padding(5) } .onChange(of: color, initial: true) { components = color.resolve(in: environment) @@ -79,7 +80,7 @@ struct AmbientLightingConfig: View { } } } - //.disabled(self.bleManager.connectedPeripheral == nil || node?.ambientLightingConfig == nil) + .disabled(self.bleManager.connectedPeripheral == nil || node?.ambientLightingConfig == nil) Button { isPresentingSaveConfirm = true } label: { diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index 60d9ff6b..daed2a2f 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -12,6 +12,8 @@ "ago"="her"; "airtime"="Airtime"; "always.on"="Immer an"; +"ambient.lighting"="Ambient Lighting"; +"ambient.lighting.config"="Ambient Lighting Config"; "app.settings"="App Einstellungen"; "are.you.sure"="Bist Du sicher?"; "ascii.capable"="ASCII fähig"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 23c19d6a..6d47569c 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -12,6 +12,8 @@ "ago"="ago"; "airtime"="Airtime"; "always.on"="Always On"; +"ambient.lighting"="Ambient Lighting"; +"ambient.lighting.config"="Ambient Lighting Config"; "app.settings"="App Settings"; "are.you.sure"="Are you sure?"; "ascii.capable"="ASCII Capable"; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index 489db06a..50c1c882 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -14,6 +14,8 @@ "ago"="temu"; "airtime"="Czas nadawania"; "always.on"="Zawsze włączone"; +"ambient.lighting"="Ambient Lighting"; +"ambient.lighting.config"="Ambient Lighting Config"; "app.settings"="Ustawienia aplikacji"; "are.you.sure"="Jesteś pewny?"; "ascii.capable"="Zgodny z ASCII"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index cd4a522b..b2d86a68 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -12,6 +12,8 @@ "ago"="ago"; "airtime"="广播时间"; "always.on"="常亮"; +"ambient.lighting"="Ambient Lighting"; +"ambient.lighting.config"="Ambient Lighting Config"; "app.settings"="通用设置"; "are.you.sure"="是否确认?"; "ascii.capable"="ASCII Capable"; From fb66e3b2500a4aa78e28edfc1e892759920ea121 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 2 Dec 2023 20:14:49 -0800 Subject: [PATCH 08/20] Detection sensor cleanup location manager cleanup --- Meshtastic/Helpers/BLEManager.swift | 20 +++--- Meshtastic/Helpers/LocationHelper.swift | 62 ++++++++----------- Meshtastic/Info.plist | 2 + .../Views/Nodes/DetectionSensorLog.swift | 7 +-- .../Config/Module/DetectionSensorConfig.swift | 2 + 5 files changed, 43 insertions(+), 50 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index bf5b0949..531d4206 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -880,17 +880,19 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var positionPacket = Position() positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7) positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7) - positionPacket.time = UInt32(LocationHelper.currentTimestamp.timeIntervalSince1970) - positionPacket.timestamp = UInt32(LocationHelper.currentTimestamp.timeIntervalSince1970) - positionPacket.altitude = Int32(LocationHelper.currentAltitude) + let timestamp = LocationHelper.shared.locationManager.location?.timestamp ?? Date() + positionPacket.time = UInt32(timestamp.timeIntervalSince1970) + positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970) + positionPacket.altitude = Int32(LocationHelper.shared.locationManager.location?.altitude ?? 0) positionPacket.satsInView = UInt32(LocationHelper.satsInView) - if LocationHelper.currentSpeed > 0 && (!LocationHelper.currentSpeed.isNaN || !LocationHelper.currentSpeed.isInfinite) { - positionPacket.groundSpeed = UInt32(LocationHelper.currentSpeed * 3.6) + let currentSpeed = LocationHelper.shared.locationManager.location?.speed ?? 0 + if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { + positionPacket.groundSpeed = UInt32(currentSpeed * 3.6) } - if LocationHelper.currentHeading > 0 && (!LocationHelper.currentHeading.isNaN || !LocationHelper.currentHeading.isInfinite) { - positionPacket.groundTrack = UInt32(LocationHelper.currentHeading) + let currentHeading = LocationHelper.shared.locationManager.location?.course ?? 0 + if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) { + positionPacket.groundTrack = UInt32(currentHeading) } - var meshPacket = MeshPacket() meshPacket.to = UInt32(destNum) meshPacket.from = UInt32(fromNodeNum) @@ -2175,7 +2177,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 = storeAndForwardMessage.heartbeat.period + sfPacket.history.window = 18000000 // storeAndForwardMessage.heartbeat.period var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(packet.from) meshPacket.from = UInt32(connectedNodeNum) diff --git a/Meshtastic/Helpers/LocationHelper.swift b/Meshtastic/Helpers/LocationHelper.swift index 118623b8..857fdc63 100644 --- a/Meshtastic/Helpers/LocationHelper.swift +++ b/Meshtastic/Helpers/LocationHelper.swift @@ -14,43 +14,16 @@ class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate { locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters locationManager.pausesLocationUpdatesAutomatically = true locationManager.allowsBackgroundLocationUpdates = true - locationManager.activityType = .otherNavigation + locationManager.activityType = .other } // Apple Park static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090) - static let DefaultAltitude = CLLocationDistance(integerLiteral: 0) - static let DefaultSpeed = CLLocationSpeed(integerLiteral: 0) - static let DefaultHeading = CLLocationDirection(integerLiteral: 0) static var currentLocation: CLLocationCoordinate2D { guard let location = shared.locationManager.location else { return DefaultLocation } return location.coordinate } - static var currentAltitude: CLLocationDistance { - guard let altitude = shared.locationManager.location?.altitude else { - return DefaultAltitude - } - return altitude - } - static var currentSpeed: CLLocationSpeed { - guard let speed = shared.locationManager.location?.speed else { - return DefaultSpeed - } - return speed - } - static var currentHeading: CLLocationDirection { - guard let heading = shared.locationManager.location?.course else { - return DefaultHeading - } - return heading - } - static var currentTimestamp: Date { - guard let timestamp = shared.locationManager.location?.timestamp else { - return Date.now - } - return timestamp - } static var satsInView: Int { // If we have a position we have a sat var sats = 1 @@ -74,9 +47,11 @@ class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate { } return sats } - + func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { switch manager.authorizationStatus { + case .authorizedAlways: + authorizationStatus = .authorizedAlways case .authorizedWhenInUse: authorizationStatus = .authorizedWhenInUse locationManager.requestLocation() @@ -86,19 +61,32 @@ class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate { authorizationStatus = .denied case .notDetermined: authorizationStatus = .notDetermined - locationManager.requestWhenInUseAuthorization() + locationManager.requestAlwaysAuthorization() default: break } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { -// locationManager.stopUpdatingLocation() -// locations.last.map { -// region = MKCoordinateRegion( -// center: $0.coordinate, -// span: .init(latitudeDelta: 0.01, longitudeDelta: 0.01) -// ) -// } + let chimeOnLocationUpdate = true + // locationManager.stopUpdatingLocation() + // locations.last.map { + // region = MKCoordinateRegion( + // center: $0.coordinate, + // span: .init(latitudeDelta: 0.01, longitudeDelta: 0.01) + // ) + // } + // Play a sound so it's easy to tell when a location update occurs while the app is in the background. + if chimeOnLocationUpdate && !locations.isEmpty { + // setSessionActiveWithMixing(true) // Ducks the audio of other apps when playing the chime. + // playSound() + } + + // Always process all of the provided locations. Don't assume the array only contains a single location. + for location in locations { + + print("process a location") + // displayNewBreadcrumbOnMap(location) + } } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { print("Location manager error: \(error.localizedDescription)") diff --git a/Meshtastic/Info.plist b/Meshtastic/Info.plist index c41afa05..b24ff3d9 100644 --- a/Meshtastic/Info.plist +++ b/Meshtastic/Info.plist @@ -55,6 +55,8 @@ We use your location to display it on the mesh map as well as to have GPS coordinatess to send to the connected device. NSLocationWhenInUseUsageDescription We use your location to display it on the mesh map as well as to have GPS coordinatess to send to the connected device. + NSLocationAlwaysUsageDescription + We use your location to display it on the mesh map as well as to have GPS coordinatess to send to the connected device. NSSupportsLiveActivities Privacy – Bluetooth Always Usage Description diff --git a/Meshtastic/Views/Nodes/DetectionSensorLog.swift b/Meshtastic/Views/Nodes/DetectionSensorLog.swift index 410dfb11..c59fd318 100644 --- a/Meshtastic/Views/Nodes/DetectionSensorLog.swift +++ b/Meshtastic/Views/Nodes/DetectionSensorLog.swift @@ -15,7 +15,6 @@ struct DetectionSensorLog: View { @State var isExporting = false @State var exportString = "" @ObservedObject var node: NodeInfoEntity - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "messageTimestamp", ascending: false)], predicate: NSPredicate(format: "portNum == %d", Int32(PortNum.detectionSensorApp.rawValue)), animation: .none) private var detections: FetchedResults @@ -23,12 +22,12 @@ struct DetectionSensorLog: View { var body: some View { let oneDayAgo = Calendar.current.date(byAdding: .day, value: -1, to: Date()) let chartData = detections - .filter { $0.timestamp >= oneDayAgo! } + .filter { $0.timestamp >= oneDayAgo! && $0.fromUser?.num ?? -1 == node.user?.num ?? 0 } .sorted { $0.timestamp < $1.timestamp } VStack { if chartData.count > 0 { - GroupBox(label: Label("\(detections.count) Total Detection Events", systemImage: "sensor")) { + GroupBox(label: Label("\(chartData.count) Total Detection Events", systemImage: "sensor")) { Chart { ForEach(chartData, id: \.self) { point in Plot { @@ -90,7 +89,7 @@ struct DetectionSensorLog: View { .font(.caption) .fontWeight(.bold) } - ForEach(detections) { d in + ForEach(detections.filter( {$0.fromUser?.num ?? -1 == node.user?.num ?? 0})) { d in GridRow { Text(d.messagePayload ?? "Detected") .font(.caption) diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index 778412eb..868aaf85 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -76,6 +76,7 @@ struct DetectionSensorConfig: View { Text("Enables the detection sensor module, it needs to be enabled on both the node with the sensor, and any nodes that you want to receive detection sensor text messages or view the detection sensor log and chart.") .font(.caption) } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .listRowSeparator(.visible) if enabled { HStack { @@ -108,6 +109,7 @@ struct DetectionSensorConfig: View { Text("Send ASCII bell with alert message. Useful for triggering external notification on bell.") .font(.caption) } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .listRowSeparator(.visible) HStack { Label("Name", systemImage: "signature") From 126cdfbdb3557ecbda975c88cc153293a8bc827a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 6 Dec 2023 12:32:17 -0800 Subject: [PATCH 09/20] Settings rework - new async location handler --- Meshtastic.xcodeproj/project.pbxproj | 12 +- Meshtastic/Helpers/BLEManager.swift | 83 ++-- Meshtastic/Helpers/LocationsHandler.swift | 96 +++++ .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 4 +- .../contents | 378 ++++++++++++++++++ Meshtastic/MeshtasticAppDelegate.swift | 14 +- .../Nodes/Helpers/Map/NodeMapSwiftUI.swift | 2 +- .../Nodes/Helpers/Map/PositionPopover.swift | 6 +- .../Config/Module/CannedMessagesConfig.swift | 1 - .../Config/Module/DetectionSensorConfig.swift | 269 ++++++------- .../Module/ExternalNotificationConfig.swift | 229 +++++------ .../Settings/Config/Module/MQTTConfig.swift | 327 +++++++-------- .../Settings/Config/Module/SerialConfig.swift | 2 - .../Settings/Config/Module/StoreForward.swift | 120 +++--- .../Config/Module/TelemetryConfig.swift | 1 - .../Settings/Config/PositionConfig.swift | 16 - Meshtastic/Views/Settings/RouteRecorder.swift | 160 ++++++++ Meshtastic/Views/Settings/Routes.swift | 5 +- Meshtastic/Views/Settings/Settings.swift | 11 + 20 files changed, 1193 insertions(+), 545 deletions(-) create mode 100644 Meshtastic/Helpers/LocationsHandler.swift create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV21.xcdatamodel/contents create mode 100644 Meshtastic/Views/Settings/RouteRecorder.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 1ac11fbe..e7b9c1b7 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */; }; DD2DC2C029BCD8AB003B383C /* HardwareModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */; }; DD3501892852FC3B000FC853 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3501882852FC3B000FC853 /* Settings.swift */; }; + DD3619152B1EF9F900C41C8C /* LocationsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */; }; DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */; }; DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */; }; DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */; }; @@ -172,6 +173,7 @@ DDDE5A1329AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; DDDE5A1429AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; DDE0F7C5295F77B700B8AAB3 /* AppSettingsEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */; }; + DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */; }; DDF6B2482A9AEBF500BA6931 /* StoreForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */; }; DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; }; DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */; }; @@ -240,6 +242,8 @@ DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV19.xcdatamodel; sourceTree = ""; }; DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HardwareModels.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -403,6 +407,7 @@ DDDE5A1229AFEAB900490C6C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV11.xcdatamodel; sourceTree = ""; }; DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsEnums.swift; sourceTree = ""; }; + DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteRecorder.swift; sourceTree = ""; }; DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = ""; }; DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV17.xcdatamodel; sourceTree = ""; }; DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreForward.swift; sourceTree = ""; }; @@ -531,6 +536,7 @@ DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */, DD4A911D2708C65400501B7E /* AppSettings.swift */, DDAB580C2B0DAA9E00147258 /* Routes.swift */, + DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */, DDA0B6B1294CDC55001356EC /* Channels.swift */, DDD6EEAE29BC024700383354 /* Firmware.swift */, DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */, @@ -836,6 +842,7 @@ DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */, DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */, DDDB443C29F6592F00EE2349 /* NetworkManager.swift */, + DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */, ); path = Helpers; sourceTree = ""; @@ -1149,6 +1156,7 @@ DD6193792863875F00E59241 /* SerialConfig.swift in Sources */, DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */, DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */, + DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */, DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */, DDDB26482AACD6D1003AFCB7 /* NodeMapMapkit.swift in Sources */, DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */, @@ -1163,6 +1171,7 @@ DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */, DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */, DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */, + DD3619152B1EF9F900C41C8C /* LocationsHandler.swift in Sources */, DDDB444A29F8AA3A00EE2349 /* CLLocationCoordinate2D.swift in Sources */, DD41582628582E9B009B0E59 /* DeviceConfig.swift in Sources */, DD007BAE2AA4E91200F5FA12 /* MyInfoEntityExtension.swift in Sources */, @@ -1764,6 +1773,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */, DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */, DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */, DDDB26492AAD743E003AFCB7 /* MeshtasticDataModelV18.xcdatamodel */, @@ -1785,7 +1795,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */; + currentVersion = DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 531d4206..524aa226 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -20,7 +20,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var context: NSManagedObjectContext? //var userSettings: UserSettings? private var centralManager: CBCentralManager! - private let restoreKey = "Meshtastic.BLE.Manager" @Published var peripherals: [Peripheral] = [] @Published var connectedPeripheral: Peripheral! @Published var lastConnectionError: String @@ -874,25 +873,48 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func sendPosition(destNum: Int64, wantResponse: Bool) -> Bool { var success = false let fromNodeNum = connectedPeripheral.num - if fromNodeNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 { - return false - } var positionPacket = Position() - positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7) - positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7) - let timestamp = LocationHelper.shared.locationManager.location?.timestamp ?? Date() - positionPacket.time = UInt32(timestamp.timeIntervalSince1970) - positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970) - positionPacket.altitude = Int32(LocationHelper.shared.locationManager.location?.altitude ?? 0) - positionPacket.satsInView = UInt32(LocationHelper.satsInView) - let currentSpeed = LocationHelper.shared.locationManager.location?.speed ?? 0 - if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { - positionPacket.groundSpeed = UInt32(currentSpeed * 3.6) - } - let currentHeading = LocationHelper.shared.locationManager.location?.course ?? 0 - if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) { - positionPacket.groundTrack = UInt32(currentHeading) + + if #available(iOS 17.0, macOS 14.0, *) { + if fromNodeNum <= 0 { + return false + } + positionPacket.latitudeI = Int32(LocationsHandler.shared.lastLocation.coordinate.latitude * 1e7) + positionPacket.longitudeI = Int32(LocationsHandler.shared.lastLocation.coordinate.longitude * 1e7) + let timestamp = LocationsHandler.shared.lastLocation.timestamp + positionPacket.time = UInt32(timestamp.timeIntervalSince1970) + positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970) + positionPacket.altitude = Int32(LocationsHandler.shared.lastLocation.altitude) + positionPacket.satsInView = UInt32(LocationsHandler.satsInView) + let currentSpeed = LocationsHandler.shared.lastLocation.speed + if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { + positionPacket.groundSpeed = UInt32(currentSpeed * 3.6) + } + let currentHeading = LocationsHandler.shared.lastLocation.course + if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) { + positionPacket.groundTrack = UInt32(currentHeading) + } + } else { + if fromNodeNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 { + return false + } + positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7) + positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7) + let timestamp = LocationHelper.shared.locationManager.location?.timestamp ?? Date() + positionPacket.time = UInt32(timestamp.timeIntervalSince1970) + positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970) + positionPacket.altitude = Int32(LocationHelper.shared.locationManager.location?.altitude ?? 0) + positionPacket.satsInView = UInt32(LocationHelper.satsInView) + let currentSpeed = LocationHelper.shared.locationManager.location?.speed ?? 0 + if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { + positionPacket.groundSpeed = UInt32(currentSpeed * 3.6) + } + let currentHeading = LocationHelper.shared.locationManager.location?.course ?? 0 + if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) { + positionPacket.groundTrack = UInt32(currentHeading) + } } + var meshPacket = MeshPacket() meshPacket.to = UInt32(destNum) meshPacket.from = UInt32(fromNodeNum) @@ -2305,29 +2327,4 @@ extension BLEManager: CBCentralManagerDelegate { let visibleDuration = Calendar.current.date(byAdding: .second, value: -5, to: today)! self.peripherals.removeAll(where: { $0.lastUpdate < visibleDuration}) } - - // func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { - // - // guard let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] else { - // return - // } - // - // if peripherals.count > 0 { - // - // for peripheral in peripherals { - // print(peripheral) - // switch peripheral.state { - // case .connecting: // I've only seen this happen when - // // re-launching attached to Xcode. - // print("Xcode Restore") - // - // case .connected: - // connectTo(peripheral: peripheral) - // print("Restore BLE State") - // default: break - // } - // } - // } - // print("willRestoreState Hit!") - // } } diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift new file mode 100644 index 00000000..9c5626c6 --- /dev/null +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -0,0 +1,96 @@ +// +// LocationsHandler.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 12/4/23. +// + +import SwiftUI +import CoreLocation + + +// Shared state that manages the `CLLocationManager` and `CLBackgroundActivitySession`. +@available(iOS 17.0, macOS 14.0, *) +@MainActor class LocationsHandler: ObservableObject { + + static let shared = LocationsHandler() // Create a single, shared instance of the object. + private let manager: CLLocationManager + private var background: CLBackgroundActivitySession? + + @Published var lastLocation = CLLocation() + @Published var isStationary = false + @Published var count = 0 + + @Published + var updatesStarted: Bool = UserDefaults.standard.bool(forKey: "liveUpdatesStarted") { + didSet { UserDefaults.standard.set(updatesStarted, forKey: "liveUpdatesStarted") } + } + + @Published + var backgroundActivity: Bool = UserDefaults.standard.bool(forKey: "BGActivitySessionStarted") { + didSet { + backgroundActivity ? self.background = CLBackgroundActivitySession() : self.background?.invalidate() + UserDefaults.standard.set(backgroundActivity, forKey: "BGActivitySessionStarted") + } + } + + private init() { + self.manager = CLLocationManager() // Creating a location manager instance is safe to call here in `MainActor`. + } + + func startLocationUpdates() { + if self.manager.authorizationStatus == .notDetermined { + self.manager.requestWhenInUseAuthorization() + } + print("Starting location updates") + Task() { + do { + 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 let loc = update.location { + self.lastLocation = loc + self.isStationary = update.isStationary + self.count += 1 + //print("Location \(self.count): \(self.lastLocation)") + } + } + } catch { + print("Could not start location updates") + } + return + } + } + + func stopLocationUpdates() { + print("Stopping location updates") + self.updatesStarted = false + } + + static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090) + + static var satsInView: Int { + // If we have a position we have a sat + var sats = 1 + if shared.lastLocation.verticalAccuracy > 0 { + sats = 4 + if 0...5 ~= shared.lastLocation.horizontalAccuracy { + sats = 12 + } else if 6...15 ~= shared.lastLocation.horizontalAccuracy { + sats = 10 + } else if 16...30 ~= shared.lastLocation.horizontalAccuracy { + sats = 9 + } else if 31...45 ~= shared.lastLocation.horizontalAccuracy { + sats = 7 + } else if 46...60 ~= shared.lastLocation.horizontalAccuracy { + sats = 5 + } + } else if shared.lastLocation.verticalAccuracy < 0 && 60...300 ~= shared.lastLocation.horizontalAccuracy { + sats = 3 + } else if shared.lastLocation.verticalAccuracy < 0 && shared.lastLocation.horizontalAccuracy > 300 { + sats = 2 + } + return sats + } +} diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index fbcba258..12116f95 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV20.xcdatamodel + MeshtasticDataModelV21.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV20.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV20.xcdatamodel/contents index fc942300..88f217f5 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV20.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV20.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -116,10 +116,12 @@ + + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV21.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV21.xcdatamodel/contents new file mode 100644 index 00000000..fc942300 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV21.xcdatamodel/contents @@ -0,0 +1,378 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/MeshtasticAppDelegate.swift b/Meshtastic/MeshtasticAppDelegate.swift index 0918f9ea..df9fe7ac 100644 --- a/Meshtastic/MeshtasticAppDelegate.swift +++ b/Meshtastic/MeshtasticAppDelegate.swift @@ -7,7 +7,7 @@ import SwiftUI -class MeshtasticAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { +class MeshtasticAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate, ObservableObject { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { print("🚀 Meshtstic Apple App launched!") // Default User Default Values @@ -16,6 +16,18 @@ class MeshtasticAppDelegate: NSObject, UIApplicationDelegate, UNUserNotification UserDefaults.standard.register(defaults: ["meshMapShowNodeHistory" : true]) UserDefaults.standard.register(defaults: ["meshMapShowRouteLines" : true]) UNUserNotificationCenter.current().delegate = self + if #available(iOS 17.0, macOS 14.0, *) { + let locationsHandler = LocationsHandler.shared + + // If location updates were previously active, restart them after the background launch. + if locationsHandler.updatesStarted { + locationsHandler.startLocationUpdates() + } + // If a background activity session was previously active, reinstantiate it after the background launch. + if locationsHandler.backgroundActivity { + locationsHandler.backgroundActivity = true + } + } return true } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 48ff4ba1..850c4e7e 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -50,7 +50,7 @@ struct NodeMapSwiftUI: View { let positionArray = node.positions?.array as? [PositionEntity] ?? [] var mostRecent = node.positions?.lastObject as? PositionEntity let lineCoords = positionArray.compactMap({(position) -> CLLocationCoordinate2D in - return position.nodeCoordinate ?? LocationHelper.DefaultLocation + return position.nodeCoordinate ?? LocationsHandler.DefaultLocation }) if node.hasPositions { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index d92d4b35..ea7a550c 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -8,7 +8,9 @@ import SwiftUI import MapKit +@available(iOS 17.0, macOS 14.0, *) struct PositionPopover: View { + @ObservedObject var locationsHandler = LocationsHandler.shared @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @Environment(\.dismiss) private var dismiss @@ -132,8 +134,8 @@ struct PositionPopover: View { .padding(.bottom, 5) /// Distance - if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 { - let metersAway = position.coordinate.distance(from: LocationHelper.currentLocation) + if locationsHandler.lastLocation.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { + let metersAway = position.coordinate.distance(from:CLLocationCoordinate2D(latitude: locationsHandler.lastLocation.coordinate.latitude, longitude: locationsHandler.lastLocation.coordinate.longitude)) Label { Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") .foregroundColor(.primary) diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index f79c90b6..42a83ed3 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -37,7 +37,6 @@ struct CannedMessagesConfig: View { @State var messages = "" var body: some View { VStack { - Form { 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.") diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index 868aaf85..64e98f2a 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -41,150 +41,151 @@ struct DetectionSensorConfig: View { @State var monitorPin = 0 var body: some View { - - Form { - 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) - .foregroundColor(.orange) - - } else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { - // Let users know what is going on if they are using remote admin and don't have the config yet - if node?.detectionSensorConfig == nil { - Text("Detection Sensor config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.") + VStack { + Form { + 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) .foregroundColor(.orange) - } else { - Text("Remote administration for: \(node?.user?.longName ?? "Unknown")") - .font(.title3) - .onAppear { - setDetectionSensorValues() - } - } - } else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 { - Text("Configuration for: \(node?.user?.longName ?? "Unknown")") - .font(.title3) - } else { - Text("Please connect to a radio to configure settings.") - .font(.callout) - .foregroundColor(.orange) - } - Section(header: Text("options")) { - - Toggle(isOn: $enabled) { - Label("enabled", systemImage: "dot.radiowaves.right") - Text("Enables the detection sensor module, it needs to be enabled on both the node with the sensor, and any nodes that you want to receive detection sensor text messages or view the detection sensor log and chart.") - .font(.caption) - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .listRowSeparator(.visible) - if enabled { - HStack { - Picker(selection: $role, label: Text("Role")) { - ForEach(DetectionSensorRole.allCases, id: \.self) { r in - Text(r.description) - .tag(r) - } - } - .pickerStyle(SegmentedPickerStyle()) - .padding(.top, 5) - .padding(.bottom, 5) - } - } - } - if enabled && role == .client { - Section(header: Text("Client options")) { - Toggle(isOn: $detectionNotificationsEnabled) { - Label("Enable Notifications", systemImage: "bell.badge") - Text("Detection sensor messages are received as text messages. If you enable notifications you will recieve a notification for each detection message received and a corresponding unread message badge.") - .font(.caption) - } - .listRowSeparator(.visible) - } - } - if enabled && role == .sensor { - Section(header: Text("Sensor options")) { - Toggle(isOn: $sendBell) { - Label("Send Bell", systemImage: "bell") - Text("Send ASCII bell with alert message. Useful for triggering external notification on bell.") - .font(.caption) - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .listRowSeparator(.visible) - HStack { - Label("Name", systemImage: "signature") - TextField("Friendly name", text: $name, axis: .vertical) - .foregroundColor(.gray) - .autocapitalization(.none) - .disableAutocorrection(true) - .onChange(of: name, perform: { _ in - - let totalBytes = name.utf8.count - // Only mess with the value if it is too big - if totalBytes > 20 { - - let firstNBytes = Data(name.utf8.prefix(20)) - if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { - // Set the shortName back to the last place where it was the right size - name = maxBytesString - } - } - }) - .foregroundColor(.gray) - } - .listRowSeparator(.hidden) - Text("Friendly name used to format message sent to mesh. Example: A name \"Motion\" would result in a message \"Motion detected\"") - .font(.caption) - .foregroundStyle(.gray) - .listRowSeparator(.visible) - .offset(y: -10) - Picker("GPIO Pin to monitor", selection: $monitorPin) { - ForEach(0..<46) { - if $0 == 0 { - Text("unset") - } else { - Text("Pin \($0)") - } - } - } - .pickerStyle(DefaultPickerStyle()) - Toggle(isOn: $detectionTriggeredHigh) { - Label("Detection trigger High", systemImage: "dial.high") - Text("Whether or not the GPIO pin state detection is triggered on HIGH (1) or LOW (0)") - .font(.caption) - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $usePullup) { - Label("Uses pullup resistor", systemImage: "arrow.up.to.line") - Text(" Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin") + } else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { + // Let users know what is going on if they are using remote admin and don't have the config yet + if node?.detectionSensorConfig == nil { + Text("Detection Sensor config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.") + .font(.callout) + .foregroundColor(.orange) + } else { + Text("Remote administration for: \(node?.user?.longName ?? "Unknown")") + .font(.title3) + .onAppear { + setDetectionSensorValues() + } + } + } else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 { + Text("Configuration for: \(node?.user?.longName ?? "Unknown")") + .font(.title3) + } else { + Text("Please connect to a radio to configure settings.") + .font(.callout) + .foregroundColor(.orange) + } + Section(header: Text("options")) { + + Toggle(isOn: $enabled) { + Label("enabled", systemImage: "dot.radiowaves.right") + Text("Enables the detection sensor module, it needs to be enabled on both the node with the sensor, and any nodes that you want to receive detection sensor text messages or view the detection sensor log and chart.") .font(.caption) } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + if enabled { + HStack { + Picker(selection: $role, label: Text("Role")) { + ForEach(DetectionSensorRole.allCases, id: \.self) { r in + Text(r.description) + .tag(r) + } + } + .pickerStyle(SegmentedPickerStyle()) + .padding(.top, 5) + .padding(.bottom, 5) + } + } } - Section(header: Text("update.interval")) { - Picker("Minimum time between detection broadcasts", selection: $minimumBroadcastSecs) { - ForEach(UpdateIntervals.allCases) { ui in - Text(ui.description).tag(ui.rawValue) + if enabled && role == .client { + Section(header: Text("Client options")) { + Toggle(isOn: $detectionNotificationsEnabled) { + Label("Enable Notifications", systemImage: "bell.badge") + Text("Detection sensor messages are received as text messages. If you enable notifications you will recieve a notification for each detection message received and a corresponding unread message badge.") + .font(.caption) } - } - .pickerStyle(DefaultPickerStyle()) - .listRowSeparator(.hidden) - Text("Mininum time between detection broadcasts. Default is 45 seconds.") - .font(.caption) - .foregroundStyle(.gray) .listRowSeparator(.visible) - Picker("State Broadcast Interval", selection: $stateBroadcastSecs) { - Text("Never").tag(0) - ForEach(UpdateIntervals.allCases) { ui in - Text(ui.description).tag(ui.rawValue) - } } - .pickerStyle(DefaultPickerStyle()) - .listRowSeparator(.hidden) - Text("How often to send detection sensor state to mesh regardless of detection. Default is Never.") - .font(.caption) - .foregroundStyle(.gray) + } + if enabled && role == .sensor { + Section(header: Text("Sensor options")) { + Toggle(isOn: $sendBell) { + Label("Send Bell", systemImage: "bell") + Text("Send ASCII bell with alert message. Useful for triggering external notification on bell.") + .font(.caption) + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + HStack { + Label("Name", systemImage: "signature") + TextField("Friendly name", text: $name, axis: .vertical) + .foregroundColor(.gray) + .autocapitalization(.none) + .disableAutocorrection(true) + .onChange(of: name, perform: { _ in + + let totalBytes = name.utf8.count + // Only mess with the value if it is too big + if totalBytes > 20 { + + let firstNBytes = Data(name.utf8.prefix(20)) + if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { + // Set the shortName back to the last place where it was the right size + name = maxBytesString + } + } + }) + .foregroundColor(.gray) + } + .listRowSeparator(.hidden) + Text("Friendly name used to format message sent to mesh. Example: A name \"Motion\" would result in a message \"Motion detected\"") + .font(.caption) + .foregroundStyle(.gray) + .listRowSeparator(.visible) + .offset(y: -10) + Picker("GPIO Pin to monitor", selection: $monitorPin) { + ForEach(0..<46) { + if $0 == 0 { + Text("unset") + } else { + Text("Pin \($0)") + } + } + } + .pickerStyle(DefaultPickerStyle()) + Toggle(isOn: $detectionTriggeredHigh) { + Label("Detection trigger High", systemImage: "dial.high") + Text("Whether or not the GPIO pin state detection is triggered on HIGH (1) or LOW (0)") + .font(.caption) + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + + Toggle(isOn: $usePullup) { + Label("Uses pullup resistor", systemImage: "arrow.up.to.line") + Text(" Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin") + .font(.caption) + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + } + Section(header: Text("update.interval")) { + Picker("Minimum time between detection broadcasts", selection: $minimumBroadcastSecs) { + ForEach(UpdateIntervals.allCases) { ui in + Text(ui.description).tag(ui.rawValue) + } + } + .pickerStyle(DefaultPickerStyle()) + .listRowSeparator(.hidden) + Text("Mininum time between detection broadcasts. Default is 45 seconds.") + .font(.caption) + .foregroundStyle(.gray) + .listRowSeparator(.visible) + Picker("State Broadcast Interval", selection: $stateBroadcastSecs) { + Text("Never").tag(0) + ForEach(UpdateIntervals.allCases) { ui in + Text(ui.description).tag(ui.rawValue) + } + } + .pickerStyle(DefaultPickerStyle()) + .listRowSeparator(.hidden) + Text("How often to send detection sensor state to mesh regardless of detection. Default is Never.") + .font(.caption) + .foregroundStyle(.gray) + } } } } diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index 3b99f7f0..0c27ee33 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -32,137 +32,138 @@ struct ExternalNotificationConfig: View { @State var nagTimeout = 0 var body: some View { - - Form { - 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) - .foregroundColor(.orange) - - } else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { - // Let users know what is going on if they are using remote admin and don't have the config yet - if node?.externalNotificationConfig == nil { - Text("External notification config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.") + VStack { + Form { + 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) .foregroundColor(.orange) - } else { - Text("Remote administration for: \(node?.user?.longName ?? "Unknown")") + + } else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { + // Let users know what is going on if they are using remote admin and don't have the config yet + if node?.externalNotificationConfig == nil { + Text("External notification config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.") + .font(.callout) + .foregroundColor(.orange) + } else { + Text("Remote administration for: \(node?.user?.longName ?? "Unknown")") + .font(.title3) + .onAppear { + setExternalNotificationValues() + } + } + } else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 { + Text("Configuration for: \(node?.user?.longName ?? "Unknown")") .font(.title3) - .onAppear { - setExternalNotificationValues() - } + } else { + Text("Please connect to a radio to configure settings.") + .font(.callout) + .foregroundColor(.orange) } - } else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 { - Text("Configuration for: \(node?.user?.longName ?? "Unknown")") - .font(.title3) - } else { - Text("Please connect to a radio to configure settings.") - .font(.callout) - .foregroundColor(.orange) - } - Section(header: Text("options")) { - Toggle(isOn: $enabled) { - Label("enabled", systemImage: "megaphone") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $alertBell) { - Label("Alert when receiving a bell", systemImage: "bell") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $alertMessage) { - Label("Alert when receiving a message", systemImage: "message") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $usePWM) { - Label("Use PWM Buzzer", systemImage: "light.beacon.max.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead.") - .font(.caption) - } - Section(header: Text("Advanced GPIO Options")) { - Section(header: Text("Primary GPIO") - .font(.caption) - .foregroundColor(.gray) - .textCase(.uppercase)) { - Toggle(isOn: $active) { - Label("Active", systemImage: "togglepower") + Section(header: Text("options")) { + Toggle(isOn: $enabled) { + Label("enabled", systemImage: "megaphone") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("If enabled, the 'output' Pin will be pulled active high, disabled means active low.") - .font(.caption) - Picker("Output pin GPIO", selection: $output) { - ForEach(0..<46) { - if $0 == 0 { - Text("unset") - } else { - Text("Pin \($0)") - } - } + Toggle(isOn: $alertBell) { + Label("Alert when receiving a bell", systemImage: "bell") } - .pickerStyle(DefaultPickerStyle()) - Picker("GPIO Output Duration", selection: $outputMilliseconds ) { - ForEach(OutputIntervals.allCases) { oi in - Text(oi.description) - } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $alertMessage) { + Label("Alert when receiving a message", systemImage: "message") } - .pickerStyle(DefaultPickerStyle()) - Text("When using in GPIO mode, keep the output on for this long. ") - .font(.caption) - Picker("Nag timeout", selection: $nagTimeout ) { - ForEach(OutputIntervals.allCases) { oi in - Text(oi.description) - } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $usePWM) { + Label("Use PWM Buzzer", systemImage: "light.beacon.max.fill") } - .pickerStyle(DefaultPickerStyle()) - Text("Specifies how long the monitored GPIO should output.") + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead.") .font(.caption) } - - Section(header: Text("Optional GPIO") - .font(.caption) - .foregroundColor(.gray) - .textCase(.uppercase)) { - Toggle(isOn: $alertBellBuzzer) { - Label("Alert GPIO buzzer when receiving a bell", systemImage: "bell") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $alertBellVibra) { - Label("Alert GPIO vibra motor when receiving a bell", systemImage: "bell") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $alertMessageBuzzer) { - Label("Alert GPIO buzzer when receiving a message", systemImage: "message") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $alertMessageBuzzer) { - Label("Alert GPIO vibra motor when receiving a message", systemImage: "message") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Picker("Output pin buzzer GPIO ", selection: $outputBuzzer) { - ForEach(0..<46) { - if $0 == 0 { - Text("unset") - } else { - Text("Pin \($0)") + Section(header: Text("Advanced GPIO Options")) { + Section(header: Text("Primary GPIO") + .font(.caption) + .foregroundColor(.gray) + .textCase(.uppercase)) { + Toggle(isOn: $active) { + Label("Active", systemImage: "togglepower") } - } - } - .pickerStyle(DefaultPickerStyle()) - Picker("Output pin vibra GPIO", selection: $outputVibra) { - ForEach(0..<46) { - if $0 == 0 { - Text("unset") - } else { - Text("Pin \($0)") + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("If enabled, the 'output' Pin will be pulled active high, disabled means active low.") + .font(.caption) + Picker("Output pin GPIO", selection: $output) { + ForEach(0..<46) { + if $0 == 0 { + Text("unset") + } else { + Text("Pin \($0)") + } + } } + .pickerStyle(DefaultPickerStyle()) + Picker("GPIO Output Duration", selection: $outputMilliseconds ) { + ForEach(OutputIntervals.allCases) { oi in + Text(oi.description) + } + } + .pickerStyle(DefaultPickerStyle()) + Text("When using in GPIO mode, keep the output on for this long. ") + .font(.caption) + Picker("Nag timeout", selection: $nagTimeout ) { + ForEach(OutputIntervals.allCases) { oi in + Text(oi.description) + } + } + .pickerStyle(DefaultPickerStyle()) + Text("Specifies how long the monitored GPIO should output.") + .font(.caption) + } + + Section(header: Text("Optional GPIO") + .font(.caption) + .foregroundColor(.gray) + .textCase(.uppercase)) { + Toggle(isOn: $alertBellBuzzer) { + Label("Alert GPIO buzzer when receiving a bell", systemImage: "bell") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $alertBellVibra) { + Label("Alert GPIO vibra motor when receiving a bell", systemImage: "bell") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $alertMessageBuzzer) { + Label("Alert GPIO buzzer when receiving a message", systemImage: "message") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $alertMessageBuzzer) { + Label("Alert GPIO vibra motor when receiving a message", systemImage: "message") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Picker("Output pin buzzer GPIO ", selection: $outputBuzzer) { + ForEach(0..<46) { + if $0 == 0 { + Text("unset") + } else { + Text("Pin \($0)") + } + } + } + .pickerStyle(DefaultPickerStyle()) + Picker("Output pin vibra GPIO", selection: $outputVibra) { + ForEach(0..<46) { + if $0 == 0 { + Text("unset") + } else { + Text("Pin \($0)") + } + } + } + .pickerStyle(DefaultPickerStyle()) } - } - .pickerStyle(DefaultPickerStyle()) } } + .disabled(self.bleManager.connectedPeripheral == nil || node?.externalNotificationConfig == nil) } - .disabled(self.bleManager.connectedPeripheral == nil || node?.externalNotificationConfig == nil) Button { isPresentingSaveConfirm = true } label: { diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 1dd1f6cb..001ef1f7 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -25,176 +25,177 @@ struct MQTTConfig: View { @State var root = "msh" var body: some View { - Form { - 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) - .foregroundColor(.orange) - - } else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { - // Let users know what is going on if they are using remote admin and don't have the config yet - if node?.mqttConfig == nil { - Text("MQTT config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.") + VStack { + Form { + 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) .foregroundColor(.orange) - } else { - Text("Remote administration for: \(node?.user?.longName ?? "Unknown")") + + } else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { + // Let users know what is going on if they are using remote admin and don't have the config yet + if node?.mqttConfig == nil { + Text("MQTT config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.") + .font(.callout) + .foregroundColor(.orange) + } else { + Text("Remote administration for: \(node?.user?.longName ?? "Unknown")") + .font(.title3) + .onAppear { + setMqttValues() + } + } + } else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 { + Text("Configuration for: \(node?.user?.longName ?? "Unknown")") .font(.title3) - .onAppear { - setMqttValues() - } + } else { + Text("Please connect to a radio to configure settings.") + .font(.callout) + .foregroundColor(.orange) } - } else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 { - Text("Configuration for: \(node?.user?.longName ?? "Unknown")") - .font(.title3) - } else { - Text("Please connect to a radio to configure settings.") + Section(header: Text("options")) { + Toggle(isOn: $enabled) { + + Label("enabled", systemImage: "dot.radiowaves.right") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $proxyToClientEnabled) { + + Label("mqtt.clientproxy", systemImage: "iphone.radiowaves.left.and.right") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("If both MQTT and the client proxy are enabled your mobile device will utalize an available network connection to connect to the specified MQTT server.") + .font(.caption2) + + Toggle(isOn: $encryptionEnabled) { + + Label("Encryption Enabled", systemImage: "lock.icloud") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + + Toggle(isOn: $jsonEnabled) { + + Label("JSON Enabled", systemImage: "ellipsis.curlybraces") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("JSON mode is a limited, unencrypted MQTT output.") + .font(.caption2) + + Toggle(isOn: $tlsEnabled) { + + Label("TLS Enabled", systemImage: "checkmark.shield.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("Your MQTT Server must support TLS.") + .font(.caption2) + } + Section(header: Text("Custom Server")) { + HStack { + Label("Address", systemImage: "server.rack") + TextField("Server Address", text: $address) + .foregroundColor(.gray) + .autocapitalization(.none) + .disableAutocorrection(true) + .onChange(of: address, perform: { _ in + let totalBytes = address.utf8.count + // Only mess with the value if it is too big + if totalBytes > 62 { + let firstNBytes = Data(username.utf8.prefix(62)) + if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { + // Set the shortName back to the last place where it was the right size + address = maxBytesString + } + } + hasChanges = true + }) + .foregroundColor(.gray) + .keyboardType(.default) + } + .autocorrectionDisabled() + + HStack { + Label("mqtt.username", systemImage: "person.text.rectangle") + TextField("mqtt.username", text: $username) + .foregroundColor(.gray) + .autocapitalization(.none) + .disableAutocorrection(true) + .onChange(of: username, perform: { _ in + + let totalBytes = username.utf8.count + + // Only mess with the value if it is too big + if totalBytes > 62 { + + let firstNBytes = Data(username.utf8.prefix(62)) + + if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { + + // Set the shortName back to the last place where it was the right size + username = maxBytesString + } + } + hasChanges = true + }) + .foregroundColor(.gray) + } + .keyboardType(.default) + .scrollDismissesKeyboard(.interactively) + HStack { + Label("password", systemImage: "wallet.pass") + TextField("password", text: $password) + .foregroundColor(.gray) + .autocapitalization(.none) + .disableAutocorrection(true) + .onChange(of: password, perform: { _ in + + let totalBytes = password.utf8.count + + // Only mess with the value if it is too big + if totalBytes > 62 { + + let firstNBytes = Data(password.utf8.prefix(62)) + + if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { + + // Set the shortName back to the last place where it was the right size + password = maxBytesString + } + } + hasChanges = true + }) + .foregroundColor(.gray) + } + .keyboardType(.default) + .scrollDismissesKeyboard(.interactively) + HStack { + Label("Root Topic", systemImage: "tree") + TextField("Root Topic", text: $root) + .foregroundColor(.gray) + .onChange(of: root, perform: { _ in + let totalBytes = root.utf8.count + // Only mess with the value if it is too big + if totalBytes > 14 { + let firstNBytes = Data(root.utf8.prefix(14)) + if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { + // Set the shortName back to the last place where it was the right size + root = maxBytesString + } + } + }) + .foregroundColor(.gray) + } + .keyboardType(.asciiCapable) + .scrollDismissesKeyboard(.interactively) + .disableAutocorrection(true) + Text("The root topic to use for MQTT messages. Default is \"msh\". This is useful if you want to use a single MQTT server for multiple meshtastic networks and separate them via ACLs") + .font(.caption2) + } + Text("You can set uplink and downlink for each channel.") .font(.callout) - .foregroundColor(.orange) } - Section(header: Text("options")) { - Toggle(isOn: $enabled) { - - Label("enabled", systemImage: "dot.radiowaves.right") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $proxyToClientEnabled) { - - Label("mqtt.clientproxy", systemImage: "iphone.radiowaves.left.and.right") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("If both MQTT and the client proxy are enabled your mobile device will utalize an available network connection to connect to the specified MQTT server.") - .font(.caption2) - - Toggle(isOn: $encryptionEnabled) { - - Label("Encryption Enabled", systemImage: "lock.icloud") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - - Toggle(isOn: $jsonEnabled) { - - Label("JSON Enabled", systemImage: "ellipsis.curlybraces") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("JSON mode is a limited, unencrypted MQTT output.") - .font(.caption2) - - Toggle(isOn: $tlsEnabled) { - - Label("TLS Enabled", systemImage: "checkmark.shield.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("Your MQTT Server must support TLS.") - .font(.caption2) - } - Section(header: Text("Custom Server")) { - HStack { - Label("Address", systemImage: "server.rack") - TextField("Server Address", text: $address) - .foregroundColor(.gray) - .autocapitalization(.none) - .disableAutocorrection(true) - .onChange(of: address, perform: { _ in - let totalBytes = address.utf8.count - // Only mess with the value if it is too big - if totalBytes > 62 { - let firstNBytes = Data(username.utf8.prefix(62)) - if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { - // Set the shortName back to the last place where it was the right size - address = maxBytesString - } - } - hasChanges = true - }) - .foregroundColor(.gray) - .keyboardType(.default) - } - .autocorrectionDisabled() - - HStack { - Label("mqtt.username", systemImage: "person.text.rectangle") - TextField("mqtt.username", text: $username) - .foregroundColor(.gray) - .autocapitalization(.none) - .disableAutocorrection(true) - .onChange(of: username, perform: { _ in - - let totalBytes = username.utf8.count - - // Only mess with the value if it is too big - if totalBytes > 62 { - - let firstNBytes = Data(username.utf8.prefix(62)) - - if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { - - // Set the shortName back to the last place where it was the right size - username = maxBytesString - } - } - hasChanges = true - }) - .foregroundColor(.gray) - } - .keyboardType(.default) - .scrollDismissesKeyboard(.interactively) - HStack { - Label("password", systemImage: "wallet.pass") - TextField("password", text: $password) - .foregroundColor(.gray) - .autocapitalization(.none) - .disableAutocorrection(true) - .onChange(of: password, perform: { _ in - - let totalBytes = password.utf8.count - - // Only mess with the value if it is too big - if totalBytes > 62 { - - let firstNBytes = Data(password.utf8.prefix(62)) - - if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { - - // Set the shortName back to the last place where it was the right size - password = maxBytesString - } - } - hasChanges = true - }) - .foregroundColor(.gray) - } - .keyboardType(.default) - .scrollDismissesKeyboard(.interactively) - HStack { - Label("Root Topic", systemImage: "tree") - TextField("Root Topic", text: $root) - .foregroundColor(.gray) - .onChange(of: root, perform: { _ in - let totalBytes = root.utf8.count - // Only mess with the value if it is too big - if totalBytes > 14 { - let firstNBytes = Data(root.utf8.prefix(14)) - if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { - // Set the shortName back to the last place where it was the right size - root = maxBytesString - } - } - }) - .foregroundColor(.gray) - } - .keyboardType(.asciiCapable) - .scrollDismissesKeyboard(.interactively) - .disableAutocorrection(true) - Text("The root topic to use for MQTT messages. Default is \"msh\". This is useful if you want to use a single MQTT server for multiple meshtastic networks and separate them via ACLs") - .font(.caption2) - } - Text("You can set uplink and downlink for each channel.") - .font(.callout) + .scrollDismissesKeyboard(.interactively) + .disabled(self.bleManager.connectedPeripheral == nil || node?.mqttConfig == nil) } - .scrollDismissesKeyboard(.interactively) - .disabled(self.bleManager.connectedPeripheral == nil || node?.mqttConfig == nil) - Button { isPresentingSaveConfirm = true } label: { diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index 7e716166..4386b3ca 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -26,9 +26,7 @@ struct SerialConfig: View { @State var mode = 0 var body: some View { - VStack { - Form { 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.") diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForward.swift b/Meshtastic/Views/Settings/Config/Module/StoreForward.swift index d1b22798..cec01a74 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForward.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForward.swift @@ -27,72 +27,72 @@ struct StoreForwardConfig: View { @State var historyReturnWindow = 0 var body: some View { - - Form { - 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) - .foregroundColor(.orange) - - } else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { - // Let users know what is going on if they are using remote admin and don't have the config yet - if node?.storeForwardConfig == nil { - Text("Store and forward config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.") + VStack { + Form { + 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) .foregroundColor(.orange) - } else { - Text("Remote administration for: \(node?.user?.longName ?? "Unknown")") + + } else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { + // Let users know what is going on if they are using remote admin and don't have the config yet + if node?.storeForwardConfig == nil { + Text("Store and forward config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.") + .font(.callout) + .foregroundColor(.orange) + } else { + Text("Remote administration for: \(node?.user?.longName ?? "Unknown")") + .font(.title3) + .onAppear { + setDetectionSensorValues() + } + } + } else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 { + Text("Configuration for: \(node?.user?.longName ?? "Unknown")") .font(.title3) - .onAppear { - setDetectionSensorValues() - } + } else { + Text("Please connect to a radio to configure settings.") + .font(.callout) + .foregroundColor(.orange) + } + Section(header: Text("options")) { + Toggle(isOn: $enabled) { + Label("enabled", systemImage: "envelope.arrow.triangle.branch") + } + Toggle(isOn: $heartbeat) { + Label("storeforward.heartbeat", systemImage: "waveform.path.ecg") + } + Picker("Number of records", selection: $records) { + Text("unset").tag(0) + Text("25").tag(25) + Text("50").tag(50) + Text("75").tag(75) + Text("100").tag(100) + } + .pickerStyle(DefaultPickerStyle()) + Picker("History Return Max", selection: $historyReturnMax ) { + Text("unset").tag(0) + Text("25").tag(25) + Text("50").tag(50) + Text("75").tag(75) + Text("100").tag(100) + } + .pickerStyle(DefaultPickerStyle()) + Picker("History Return Window", selection: $historyReturnWindow ) { + Text("unset").tag(0) + Text("One Minute").tag(60) + Text("Five Minutes").tag(300) + Text("Ten Minutes").tag(600) + Text("Fifteen Minutes").tag(900) + Text("Thirty Minutes").tag(1800) + Text("One Hour").tag(3600) + } + .pickerStyle(DefaultPickerStyle()) } - } else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 { - Text("Configuration for: \(node?.user?.longName ?? "Unknown")") - .font(.title3) - } else { - Text("Please connect to a radio to configure settings.") - .font(.callout) - .foregroundColor(.orange) - } - Section(header: Text("options")) { - Toggle(isOn: $enabled) { - Label("enabled", systemImage: "envelope.arrow.triangle.branch") - } - Toggle(isOn: $heartbeat) { - Label("storeforward.heartbeat", systemImage: "waveform.path.ecg") - } - Picker("Number of records", selection: $records) { - Text("unset").tag(0) - Text("25").tag(25) - Text("50").tag(50) - Text("75").tag(75) - Text("100").tag(100) - } - .pickerStyle(DefaultPickerStyle()) - Picker("History Return Max", selection: $historyReturnMax ) { - Text("unset").tag(0) - Text("25").tag(25) - Text("50").tag(50) - Text("75").tag(75) - Text("100").tag(100) - } - .pickerStyle(DefaultPickerStyle()) - Picker("History Return Window", selection: $historyReturnWindow ) { - Text("unset").tag(0) - Text("One Minute").tag(60) - Text("Five Minutes").tag(300) - Text("Ten Minutes").tag(600) - Text("Fifteen Minutes").tag(900) - Text("Thirty Minutes").tag(1800) - Text("One Hour").tag(3600) - } - .pickerStyle(DefaultPickerStyle()) } + .scrollDismissesKeyboard(.interactively) + .disabled(self.bleManager.connectedPeripheral == nil || node?.storeForwardConfig == nil) } - .scrollDismissesKeyboard(.interactively) - .disabled(self.bleManager.connectedPeripheral == nil || node?.storeForwardConfig == nil) - Button { isPresentingSaveConfirm = true } label: { diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index 560331e2..f3cdb5ef 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -23,7 +23,6 @@ struct TelemetryConfig: View { @State var environmentDisplayFahrenheit = false var body: some View { - VStack { Form { if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index a0205ae2..746926dd 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -39,8 +39,6 @@ struct PositionConfig: View { @State var rxGpio = 0 @State var txGpio = 0 @State var fixedPosition = false - @State var gpsUpdateInterval = 0 - @State var gpsAttemptTime = 0 @State var positionBroadcastSeconds = 0 @State var broadcastSmartMinimumDistance = 0 @State var broadcastSmartMinimumIntervalSecs = 0 @@ -213,12 +211,6 @@ struct PositionConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) if deviceGpsEnabled { - Picker("Attempt Time", selection: $gpsAttemptTime) { - ForEach(GpsAttemptTimes.allCases) { at in - Text(at.description) - } - } - .pickerStyle(DefaultPickerStyle()) Picker("GPS Receive GPIO", selection: $rxGpio) { ForEach(0..<46) { if $0 == 0 { @@ -280,7 +272,6 @@ struct PositionConfig: View { pc.positionBroadcastSmartEnabled = smartPositionEnabled pc.gpsEnabled = deviceGpsEnabled pc.fixedPosition = fixedPosition - pc.gpsAttemptTime = UInt32(gpsAttemptTime) pc.positionBroadcastSecs = UInt32(positionBroadcastSeconds) pc.broadcastSmartMinimumIntervalSecs = UInt32(broadcastSmartMinimumIntervalSecs) pc.broadcastSmartMinimumDistance = UInt32(broadcastSmartMinimumDistance) @@ -347,11 +338,6 @@ struct PositionConfig: View { if newTxGpio != node!.positionConfig!.txGpio { hasChanges = true } } } - .onChange(of: gpsAttemptTime) { newGpsAttemptTime in - if node != nil && node!.positionConfig != nil { - if newGpsAttemptTime != node!.positionConfig!.gpsAttemptTime { hasChanges = true } - } - } .onChange(of: smartPositionEnabled) { newSmartPositionEnabled in if node != nil && node!.positionConfig != nil { if newSmartPositionEnabled != node!.positionConfig!.smartPositionEnabled { hasChanges = true } @@ -439,8 +425,6 @@ struct PositionConfig: View { self.rxGpio = Int(node?.positionConfig?.rxGpio ?? 0) self.txGpio = Int(node?.positionConfig?.txGpio ?? 0) self.fixedPosition = node?.positionConfig?.fixedPosition ?? false - self.gpsUpdateInterval = Int(node?.positionConfig?.gpsUpdateInterval ?? 30) - self.gpsAttemptTime = Int(node?.positionConfig?.gpsAttemptTime ?? 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/RouteRecorder.swift b/Meshtastic/Views/Settings/RouteRecorder.swift new file mode 100644 index 00000000..df5ca511 --- /dev/null +++ b/Meshtastic/Views/Settings/RouteRecorder.swift @@ -0,0 +1,160 @@ +// +// Routes.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 11/21/23. +// + +import SwiftUI +import CoreData +import MapKit +import CoreLocation +import CoreMotion + +struct TimerDisplayObject { + var seconds: Int = 0 + var minutes: Int = 0 + var hours: Int = 0 + + var display: String { + if self.seconds == 0 { + "\(String(format: "%02d", self.hours)):\(String(format: "%02d", self.minutes)):00" + } else { + "\(String(format: "%02d", self.hours)):\(String(format: "%02d", self.minutes)):\(String(format: "%02d", self.seconds))" + } + } + + var timeMinuteCalculator: Float { Float(hours*60+seconds/60+minutes) } +} + +@available(iOS 17.0, macOS 14.0, *) +struct RouteRecorder: View { + + @ObservedObject var locationsHandler = LocationsHandler.shared + @Environment(\.managedObjectContext) var context + @State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic) + @State var isTimerRunning = false + @State var isShowingDetails = false + @State var timer: Timer? + @Namespace var namespace + @Namespace var mapscope + @State var timeElapsed: TimerDisplayObject = TimerDisplayObject() + @State var timerDisplay = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + + var body: some View { + VStack { + VStack { + VStack { + Map(position: $position, scope: mapscope) { + UserAnnotation() +// ForEach(locations, id: \.id) { location in +// Marker(location.name, systemImage: location.icon, coordinate: location.location) +// .tint(location.colour) +// } + } + } + .mapControls { + MapUserLocationButton() + MapCompass() + MapScaleView() + MapPitchToggle() + } + .mapStyle(.hybrid(elevation: .realistic, showsTraffic: true)) + .transition(.slide) + .mapControlVisibility(.visible) + .task { + print("this is running") + locationsHandler.startLocationUpdates() + } + .safeAreaInset(edge: .bottom) { + ZStack { + VStack { + HStack(spacing: 10) { + Spacer() + if isTimerRunning { + Button { + isShowingDetails = true + isTimerRunning = false + } label: { + Image(systemName: "pause.fill") + .frame(width: 60, height: 60) + } + .buttonStyle(.bordered) + .buttonBorderShape(.circle) + .matchedGeometryEffect(id: "Pause Button", in: namespace) + } else { + Button { + isShowingDetails = true + isTimerRunning = true + timeElapsed.seconds -= 1 + } label: { + Image(systemName: "play.fill") + .frame(width: 60, height: 60) + } + .buttonStyle(.bordered) + .buttonBorderShape(.circle) + .matchedGeometryEffect(id: "Play Button", in: namespace) + } + Spacer() + } + } + .onReceive(timerDisplay) { _ in + if isTimerRunning { + timeElapsed.seconds += 1 + if timeElapsed.seconds == 60 { + timeElapsed.seconds = 0 + timeElapsed.minutes += 1 + if timeElapsed.minutes == 60 { + timeElapsed.minutes = 0 + timeElapsed.hours += 1 + } + } + } + } + } + .padding() + } + .sheet(isPresented: $isShowingDetails) { + NavigationStack { + VStack { + HStack { + Text(timeElapsed.display) + .font(.largeTitle) + Text("Time Elapseed") + .font(.callout) + } + .padding() + Divider() + VStack(alignment: .leading) { + let horizontalAccuracy = Measurement(value: locationsHandler.lastLocation.horizontalAccuracy, unit: UnitLength.meters) + let verticalAccuracy = Measurement(value: locationsHandler.lastLocation.verticalAccuracy, unit: UnitLength.meters) + let altitiude = Measurement(value: locationsHandler.lastLocation.altitude, unit: UnitLength.meters) + let speed = Measurement(value: locationsHandler.lastLocation.speed, unit: UnitSpeed.kilometersPerHour) + List { + Label("Coordinate \(String(format: "%.5f", locationsHandler.lastLocation.coordinate.latitude)), \(String(format: "%.5f", locationsHandler.lastLocation.coordinate.longitude))", systemImage: "mappin") + .textSelection(.enabled) + Label("Horizontal Accuracy \(horizontalAccuracy.formatted())", systemImage: "scope") + if locationsHandler.lastLocation.verticalAccuracy > 0 { + Label("Altitude \(altitiude.formatted())", systemImage: "mountain.2") + } + Label("Vertical Accuracy \(verticalAccuracy.formatted())", systemImage: "lines.measurement.vertical") + Label("Satellites Estimate \(LocationHelper.satsInView)", systemImage: "sparkles") + Label("\(locationsHandler.isStationary ? "Moving" : "Stationary")", systemImage: locationsHandler.isStationary ? "figure.walk.motion" : "figure.stand") + if locationsHandler.lastLocation.speedAccuracy > 0 { + Label("Speed \(speed.formatted())", systemImage: "speedometer") + } + if locationsHandler.lastLocation.courseAccuracy > 0 { + Label("Heading \(String(format: "%.2f", locationsHandler.lastLocation.course))°", systemImage: "location.circle") + } + } + .listStyle(.plain) + } + } + } + .presentationDetents([.fraction(0.5)]) + .presentationDragIndicator(.visible) + } + } + } + } +} diff --git a/Meshtastic/Views/Settings/Routes.swift b/Meshtastic/Views/Settings/Routes.swift index 1c3d52c7..5039f724 100644 --- a/Meshtastic/Views/Settings/Routes.swift +++ b/Meshtastic/Views/Settings/Routes.swift @@ -23,8 +23,7 @@ struct Routes: View { var routes: FetchedResults var body: some View { - //NavigationSplitView(columnVisibility: $columnVisibility) { - NavigationStack { + VStack { Button("Import Route") { importing = true } @@ -152,8 +151,6 @@ struct Routes: View { .listStyle(.plain) } .navigationTitle("Route List") -// } detail: { - VStack { if selectedRoute != nil { let locationArray = selectedRoute?.locations?.array as? [LocationEntity] ?? [] diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 16423c60..f8daa9e8 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -18,6 +18,7 @@ struct Settings: View { enum SettingsSidebar { case appSettings case routes + case routeRecorder case shareChannels case userConfig case loraConfig @@ -68,6 +69,15 @@ struct Settings: View { Text("routes") } .tag(SettingsSidebar.routes) + + NavigationLink { + RouteRecorder() + } label: { + Image(systemName: "record.circle") + .symbolRenderingMode(.hierarchical) + Text("route.recorder") + } + .tag(SettingsSidebar.routeRecorder) } let node = nodes.first(where: { $0.num == preferredNodeNum }) @@ -303,6 +313,7 @@ struct Settings: View { } } .onAppear { + selection = SettingsSidebar.about if self.bleManager.context == nil { self.bleManager.context = context } From bd5191ccd2bacb44379d17bc9bf35e887aecdeb6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 8 Dec 2023 11:41:29 -0800 Subject: [PATCH 10/20] Initial trace route log --- Meshtastic.xcodeproj/project.pbxproj | 8 ++ .../CoreData/TraceRouteEntityExtension.swift | 71 ++++++++++++ Meshtastic/Helpers/BLEManager.swift | 78 +++++++++++-- .../contents | 28 ++++- Meshtastic/Persistence/QueryCoreData.swift | 17 +++ .../Views/Nodes/Helpers/NodeDetail.swift | 16 ++- Meshtastic/Views/Nodes/TraceRouteLog.swift | 108 ++++++++++++++++++ 7 files changed, 316 insertions(+), 10 deletions(-) create mode 100644 Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift create mode 100644 Meshtastic/Views/Nodes/TraceRouteLog.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index e7b9c1b7..2ad60579 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -173,6 +173,8 @@ DDDE5A1329AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; DDDE5A1429AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; DDE0F7C5295F77B700B8AAB3 /* AppSettingsEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */; }; + DDE5B4042B2279A700FCDD05 /* TraceRouteLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE5B4032B2279A700FCDD05 /* TraceRouteLog.swift */; }; + DDE5B4062B227E3200FCDD05 /* TraceRouteEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */; }; DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */; }; DDF6B2482A9AEBF500BA6931 /* StoreForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */; }; DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; }; @@ -407,6 +409,8 @@ DDDE5A1229AFEAB900490C6C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV11.xcdatamodel; sourceTree = ""; }; DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsEnums.swift; sourceTree = ""; }; + DDE5B4032B2279A700FCDD05 /* TraceRouteLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRouteLog.swift; sourceTree = ""; }; + DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRouteEntityExtension.swift; sourceTree = ""; }; DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteRecorder.swift; sourceTree = ""; }; DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = ""; }; DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV17.xcdatamodel; sourceTree = ""; }; @@ -485,6 +489,7 @@ DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */, DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */, DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */, + DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */, ); path = CoreData; sourceTree = ""; @@ -515,6 +520,7 @@ DD73FD1028750779000852D6 /* PositionLog.swift */, DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */, 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */, + DDE5B4032B2279A700FCDD05 /* TraceRouteLog.swift */, ); path = Nodes; sourceTree = ""; @@ -1152,6 +1158,7 @@ DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */, DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */, DD5E5213298EE33B00D21B61 /* deviceonly.pb.swift in Sources */, + DDE5B4042B2279A700FCDD05 /* TraceRouteLog.swift in Sources */, DD5E5208298EE33B00D21B61 /* rtttl.pb.swift in Sources */, DD6193792863875F00E59241 /* SerialConfig.swift in Sources */, DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */, @@ -1263,6 +1270,7 @@ DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */, DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */, DD5E5204298EE33B00D21B61 /* xmodem.pb.swift in Sources */, + DDE5B4062B227E3200FCDD05 /* TraceRouteEntityExtension.swift in Sources */, DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift b/Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift new file mode 100644 index 00000000..4e7cdb60 --- /dev/null +++ b/Meshtastic/Extensions/CoreData/TraceRouteEntityExtension.swift @@ -0,0 +1,71 @@ +// +// TraceRouteEntityExtension.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 12/7/23. +// + +import CoreData +import CoreLocation +import MapKit +import SwiftUI + +extension TraceRouteEntity { + + var latitude: Double? { + + let d = Double(latitudeI) + if d == 0 { + return 0 + } + return d / 1e7 + } + + var longitude: Double? { + + let d = Double(longitudeI) + if d == 0 { + return 0 + } + return d / 1e7 + } + + var coordinate: CLLocationCoordinate2D? { + if latitudeI != 0 && longitudeI != 0 { + let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!) + return coord + } else { + return nil + } + } +} + +extension TraceRouteHopEntity { + + var latitude: Double? { + + let d = Double(latitudeI) + if d == 0 { + return 0 + } + return d / 1e7 + } + + var longitude: Double? { + + let d = Double(longitudeI) + if d == 0 { + return 0 + } + return d / 1e7 + } + + var coordinate: CLLocationCoordinate2D? { + if latitudeI != 0 && longitudeI != 0 { + let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!) + return coord + } else { + return nil + } + } +} diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 524aa226..c82b4796 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -376,6 +376,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let fromNodeNum = connectedPeripheral.num let routePacket = RouteDiscovery() var meshPacket = MeshPacket() + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. = NSFetchRequest.init(entityName: "NodeInfoEntity") + nodes.predicate = NSPredicate(format: "num IN %@", [destNum, self.connectedPeripheral.num]) + do { + guard let fetchedNodes = try context!.fetch(nodes) as? [NodeInfoEntity] else { + return false + } + let receivingNode = fetchedNodes.first(where: { $0.num == destNum }) + let connectedNode = fetchedNodes.first(where: { $0.num == self.connectedPeripheral.num }) + + traceRoute.id = Int64(meshPacket.id) + traceRoute.time = Date() + traceRoute.node = receivingNode + // Grab the most recent postion, within the last hour + if connectedNode?.positions?.count ?? 0 > 0 { + let mostRecent = connectedNode?.positions?.lastObject as! PositionEntity + if mostRecent.time! >= Calendar.current.date(byAdding: .minute, value: -60, to: Date())! { + traceRoute.altitude = mostRecent.altitude + traceRoute.latitudeI = mostRecent.latitudeI + traceRoute.longitudeI = mostRecent.longitudeI + } + } + do { + try context!.save() + print("💾 Saved TraceRoute sent to node: \(String(receivingNode?.user?.longName ?? "unknown".localized))") + } catch { + context!.rollback() + let nsError = error as NSError + print("💥 Error Updating Core Data BluetoothConfigEntity: \(nsError)") + } + + let logString = String.localizedStringWithFormat("mesh.log.traceroute.sent %@".localized, String(destNum)) + MeshLogger.log("🪧 \(logString)") + + } catch { + + } } return success } @@ -595,16 +631,44 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") case .tracerouteApp: if let routingMessage = try? RouteDiscovery(serializedData: decodedInfo.packet.decoded.payload) { - + let traceRoute = getTraceRoute(id: Int64(decodedInfo.packet.decoded.requestID), context: context!) + traceRoute?.response = true + traceRoute?.route = routingMessage.route if routingMessage.route.count == 0 { let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.direct %@".localized, String(decodedInfo.packet.from)) MeshLogger.log("🪧 \(logString)") - } else { - var routeString = "\(decodedInfo.packet.to) --> " - for node in routingMessage.route { - routeString += "\(node) --> " + } else { + var routeString = "You --> " + var hopNodes: [TraceRouteHopEntity] = [] +// for node in routingMessage.route { +// let hopNode = getNodeInfo(id: Int64(node), context: context!) +// let traceRouteHop = TraceRouteHopEntity(context: context!) +// traceRouteHop.time = Date() +// let mostRecent = hopNode?.positions?.lastObject as! PositionEntity +// if mostRecent.time! >= Calendar.current.date(byAdding: .minute, value: -60, to: Date())! { +// traceRouteHop.altitude = mostRecent.altitude +// traceRouteHop.latitudeI = mostRecent.latitudeI +// traceRouteHop.longitudeI = mostRecent.longitudeI +// traceRouteHop.name = hopNode?.user?.longName ?? "unknown".localized +// } +// traceRouteHop.num = hopNode?.num ?? 0 +// if hopNode != nil { +// hopNodes.append(traceRouteHop) +// } +// routeString += "\(hopNode?.user?.longName ?? "unknown".localized) --> " +// } + traceRoute?.routeText = routeString + traceRoute?.hops = NSOrderedSet(array: hopNodes) + do { + try context!.save() + print("💾 Saved Trace Route") + } catch { + context!.rollback() + let nsError = error as NSError + print("💥 Error Updating Core Data TraceRouteHOp: \(nsError)") } + routeString += "\(decodedInfo.packet.from)" let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.route %@".localized, routeString) MeshLogger.log("🪧 \(logString)") diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV21.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV21.xcdatamodel/contents index fc942300..d30a2970 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV21.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV21.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -112,6 +112,9 @@ + + + @@ -244,6 +247,7 @@ + @@ -340,6 +344,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Persistence/QueryCoreData.swift b/Meshtastic/Persistence/QueryCoreData.swift index 32cbffc0..502aa61a 100644 --- a/Meshtastic/Persistence/QueryCoreData.swift +++ b/Meshtastic/Persistence/QueryCoreData.swift @@ -46,6 +46,23 @@ public func getStoreAndForwardMessageIds(seconds: Int, context: NSManagedObjectC return [] } +public func getTraceRoute(id: Int64, context: NSManagedObjectContext) -> TraceRouteEntity? { + + let fetchTraceRouteRequest: NSFetchRequest = NSFetchRequest.init(entityName: "TraceRouteEntity") + fetchTraceRouteRequest.predicate = NSPredicate(format: "id == %lld", Int64(id)) + + do { + guard let fetchedTraceRoute = try context.fetch(fetchTraceRouteRequest) as? [TraceRouteEntity] else { + return nil + } + if fetchedTraceRoute.count == 1 { + return fetchedTraceRoute[0] + } + } catch { + return nil + } + return nil +} public func getUser(id: Int64, context: NSManagedObjectContext) -> UserEntity { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index b644f2b6..ab5765dc 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -93,9 +93,21 @@ struct NodeDetail: View { } .disabled(!node.hasDetectionSensorMetrics) Divider() + if #available(iOS 17.0, macOS 14.0, *) { + NavigationLink { + TraceRouteLog(node: node) + } label: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Trace Route Log") + .font(.title3) + } + .disabled(node.traceRoutes?.count ?? 0 == 0) + Divider() + } } - - if self.bleManager.connectedPeripheral != nil && node.metadata != nil { HStack { if node.metadata?.canShutdown ?? false { diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift new file mode 100644 index 00000000..8f139f77 --- /dev/null +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -0,0 +1,108 @@ +// +// TraceRouteLog.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 12/7/23. +// + +import SwiftUI +#if canImport(MapKit) +import MapKit +#endif + +@available(iOS 17.0, macOS 14.0, *) +struct TraceRouteLog: View { + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + + @State private var isPresentingClearLogConfirm: Bool = false + @State var isExporting = false + @State var exportString = "" + @ObservedObject var node: NodeInfoEntity + @State private var selectedRoute: TraceRouteEntity? + + var body: some View { + VStack { + VStack { + List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in + Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "Other") : "No Response")") + } + .listStyle(.plain) + } + .navigationTitle("Trace Route List") + VStack { + if selectedRoute != nil { + Divider() + if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 > 0 { + Text("Trace Route received by \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + Text("Route: \(selectedRoute?.routeText ?? "unknown".localized)") + .font(.title) + } else if selectedRoute?.response ?? false { + Text("Trace Route received directly by \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + .font(.title) + } + let hopsArray = selectedRoute?.hops?.array as? [TraceRouteHopEntity] ?? [] + let lineCoords = hopsArray.compactMap({(hop) -> CLLocationCoordinate2D in + return hop.coordinate ?? LocationHelper.DefaultLocation + }) + if selectedRoute?.response ?? false { + Map() { + Annotation("You", coordinate: selectedRoute?.coordinate ?? LocationHelper.DefaultLocation) { + ZStack { + Circle() + .fill(Color(.green)) + .strokeBorder(.white, lineWidth: 3) + .frame(width: 15, height: 15) + } + } + .annotationTitles(.automatic) + // Direct Trace Route + if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 == 0 { + if selectedRoute?.node?.positions?.count ?? 0 > 0 { + let mostRecent = selectedRoute?.node?.positions?.lastObject as! PositionEntity + var traceRouteCoords: [CLLocationCoordinate2D] = [selectedRoute?.coordinate ?? LocationsHandler.DefaultLocation, mostRecent.coordinate] + Annotation(selectedRoute?.node?.user?.shortName ?? "???", coordinate: mostRecent.nodeCoordinate ?? LocationHelper.DefaultLocation) { + ZStack { + Circle() + .fill(Color(.black)) + .strokeBorder(.white, lineWidth: 3) + .frame(width: 15, height: 15) + } + } + let dashed = StrokeStyle( + lineWidth: 3, + lineCap: .round, lineJoin: .round, dash: [7, 10] + ) + MapPolyline(coordinates: traceRouteCoords) + .stroke(.blue, style: dashed) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + Text("Trace Route sent to \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + .font(.title) + .padding(.top) + Spacer() + Text("\(selectedRoute?.time?.formatted() ?? "")") + .font(.title3) + Spacer() + Text("No response") + .font(.title2) + Spacer() + } + } + } + .navigationTitle("Route Details") + } + .navigationBarItems(trailing: + ZStack { + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") + }) + .onAppear { + if self.bleManager.context == nil { + self.bleManager.context = context + } + } + } +} From a8787ebc21452b75bc6dc86b9f9e23991413916f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 8 Dec 2023 12:36:42 -0800 Subject: [PATCH 11/20] Fix error when loading jpeg mbtile file? --- Meshtastic/Views/MapKitMap/Custom/LocalMBTileOverlay.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/MapKitMap/Custom/LocalMBTileOverlay.swift b/Meshtastic/Views/MapKitMap/Custom/LocalMBTileOverlay.swift index 5409dc2b..4c9dc828 100644 --- a/Meshtastic/Views/MapKitMap/Custom/LocalMBTileOverlay.swift +++ b/Meshtastic/Views/MapKitMap/Custom/LocalMBTileOverlay.swift @@ -60,7 +60,7 @@ class LocalMBTileOverlay: MKTileOverlay { // make sure it's raster let formatQuery = try mb.pluck(metadata.select(value).filter(name == "format")) - if formatQuery?[value] == nil || (formatQuery![value] != "jpg" && formatQuery![value] != "png") { + if formatQuery?[value] == nil || (formatQuery![value] != "jpeg" && formatQuery![value] != "jpg" && formatQuery![value] != "png") { throw MapTileError.invalidFormat } From a30fc18d73180b20850166a308abe691138b67f5 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 9 Dec 2023 15:01:01 -0800 Subject: [PATCH 12/20] Trace route cleanup --- Meshtastic/Views/Messages/UserList.swift | 17 -- Meshtastic/Views/Nodes/NodeList.swift | 21 ++- Meshtastic/Views/Nodes/TraceRouteLog.swift | 171 ++++++++++++------ Meshtastic/Views/Settings/RouteRecorder.swift | 16 +- 4 files changed, 146 insertions(+), 79 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index d35f6592..f102b6bd 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -33,7 +33,6 @@ struct UserList: View { @State var node: NodeInfoEntity? @State private var userSelection: UserEntity? // Nothing selected by default. @State private var isPresentingDeleteUserMessagesConfirm: Bool = false - @State private var isPresentingTraceRouteSentAlert = false var body: some View { let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current) @@ -126,14 +125,6 @@ struct UserList: View { } label: { Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash") } - Button { - let success = bleManager.sendTraceRouteRequest(destNum: user.num, wantResponse: true) - if success { - isPresentingTraceRouteSentAlert = true - } - } label: { - Label("Trace Route", systemImage: "signpost.right.and.left") - } if user.messageList.count > 0 { Button(role: .destructive) { isPresentingDeleteUserMessagesConfirm = true @@ -143,14 +134,6 @@ struct UserList: View { } } } - .alert( - "Trace Route Sent", - isPresented: $isPresentingTraceRouteSentAlert - ) { - Button("OK", role: .cancel) { } - } message: { - Text("This could take a while, response will appear in the mesh log.") - } .confirmationDialog( "This conversation will be deleted.", isPresented: $isPresentingDeleteUserMessagesConfirm, diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 7e6f30ec..c0df0167 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -11,6 +11,7 @@ struct NodeList: View { @State private var columnVisibility = NavigationSplitViewVisibility.all @State private var selectedNode: NodeInfoEntity? + @State private var isPresentingTraceRouteSentAlert = false @SceneStorage("selectedDetailView") var selectedDetailView: String? @@ -72,13 +73,31 @@ struct NodeList: View { } label: { Label(node.user!.mute ? "Show Alerts" : "Hide Alerts", systemImage: node.user!.mute ? "bell" : "bell.slash") } + if connectedNodeNum != node.num { + Button { + let success = bleManager.sendTraceRouteRequest(destNum: node.user?.num ?? 0, wantResponse: true) + if success { + isPresentingTraceRouteSentAlert = true + } + } label: { + Label("Trace Route", systemImage: "signpost.right.and.left") + } + } } - + } + .alert( + "Trace Route Sent", + isPresented: $isPresentingTraceRouteSentAlert + ) { + Button("OK", role: .cancel) { } + } message: { + Text("This could take a while, response will appear in the trace route log for the node it was sent to.") } } .searchable(text: nodesQuery, prompt: "Find a node") .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) .listStyle(.plain) + .navigationSplitViewColumnWidth(min: 100, ideal: 250, max: 500) .navigationBarItems(leading: MeshtasticLogo(), diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 8f139f77..7b525932 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -12,6 +12,7 @@ import MapKit @available(iOS 17.0, macOS 14.0, *) struct TraceRouteLog: View { + @ObservedObject var locationsHandler = LocationsHandler.shared @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @@ -20,84 +21,136 @@ struct TraceRouteLog: View { @State var exportString = "" @ObservedObject var node: NodeInfoEntity @State private var selectedRoute: TraceRouteEntity? - + // Map Configuration + @Namespace var mapScope + @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic, emphasis: MapStyle.StandardEmphasis.muted ,pointsOfInterest: .all, showsTraffic: true) + @State var position = MapCameraPosition.automatic + let distanceFormatter = MKDistanceFormatter() + var body: some View { - VStack { + HStack (alignment: .top) { VStack { - List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in - Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "Other") : "No Response")") - } - .listStyle(.plain) - } - .navigationTitle("Trace Route List") - VStack { - if selectedRoute != nil { - Divider() - if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 > 0 { - Text("Trace Route received by \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") - Text("Route: \(selectedRoute?.routeText ?? "unknown".localized)") - .font(.title) - } else if selectedRoute?.response ?? false { - Text("Trace Route received directly by \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") - .font(.title) + VStack { + List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in + + Label { + Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hops?.count == 0) Hops") : "No Response")") + } icon: { + Image(systemName: route.response ? (route.hops?.count == 0 && route.response ? "person.line.dotted.person" : "point.3.connected.trianglepath.dotted") : "person.slash") + .symbolRenderingMode(.hierarchical) + } } - let hopsArray = selectedRoute?.hops?.array as? [TraceRouteHopEntity] ?? [] - let lineCoords = hopsArray.compactMap({(hop) -> CLLocationCoordinate2D in - return hop.coordinate ?? LocationHelper.DefaultLocation - }) - if selectedRoute?.response ?? false { - Map() { - Annotation("You", coordinate: selectedRoute?.coordinate ?? LocationHelper.DefaultLocation) { - ZStack { - Circle() - .fill(Color(.green)) - .strokeBorder(.white, lineWidth: 3) - .frame(width: 15, height: 15) - } + .listStyle(.plain) + } + .frame(minHeight: 200, maxHeight: 230) + VStack { + if selectedRoute != nil { + if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 > 0 { + Text("Received by \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + Text("Route: \(selectedRoute?.routeText ?? "unknown".localized)") + .font(.title3) + } else if selectedRoute?.response ?? false { + Label { + Text("Trace route received directly by \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + } icon: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) } - .annotationTitles(.automatic) - // Direct Trace Route - if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 == 0 { - if selectedRoute?.node?.positions?.count ?? 0 > 0 { - let mostRecent = selectedRoute?.node?.positions?.lastObject as! PositionEntity - var traceRouteCoords: [CLLocationCoordinate2D] = [selectedRoute?.coordinate ?? LocationsHandler.DefaultLocation, mostRecent.coordinate] - Annotation(selectedRoute?.node?.user?.shortName ?? "???", coordinate: mostRecent.nodeCoordinate ?? LocationHelper.DefaultLocation) { + .font(.title3) + } + + let hopsArray = selectedRoute?.hops?.array as? [TraceRouteHopEntity] ?? [] + let lineCoords = hopsArray.compactMap({(hop) -> CLLocationCoordinate2D in + return hop.coordinate ?? LocationHelper.DefaultLocation + }) + if selectedRoute?.response ?? false { + if selectedRoute?.coordinate != nil && (selectedRoute?.node?.positions?.count ?? 0 > 0 || false ) { + Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { + Annotation("You", coordinate: selectedRoute?.coordinate ?? LocationHelper.DefaultLocation) { ZStack { Circle() - .fill(Color(.black)) + .fill(Color(.green)) .strokeBorder(.white, lineWidth: 3) .frame(width: 15, height: 15) } } - let dashed = StrokeStyle( - lineWidth: 3, - lineCap: .round, lineJoin: .round, dash: [7, 10] - ) - MapPolyline(coordinates: traceRouteCoords) - .stroke(.blue, style: dashed) + .annotationTitles(.automatic) + // Direct Trace Route + if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 == 0 { + if selectedRoute?.node?.positions?.count ?? 0 > 0 { + let mostRecent = selectedRoute?.node?.positions?.lastObject as! PositionEntity + var traceRouteCoords: [CLLocationCoordinate2D] = [selectedRoute?.coordinate ?? LocationsHandler.DefaultLocation, mostRecent.coordinate] + Annotation(selectedRoute?.node?.user?.shortName ?? "???", coordinate: mostRecent.nodeCoordinate ?? LocationHelper.DefaultLocation) { + ZStack { + Circle() + .fill(Color(.black)) + .strokeBorder(.white, lineWidth: 3) + .frame(width: 15, height: 15) + } + } + let dashed = StrokeStyle( + lineWidth: 2, + lineCap: .round, lineJoin: .round, dash: [7, 10] + ) + MapPolyline(coordinates: traceRouteCoords) + .stroke(.blue, style: dashed) + } + } else if selectedRoute?.hops?.count ?? 0 == 0 { + + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + VStack { + /// Distance + if selectedRoute?.node?.positions?.count ?? 0 > 0 && selectedRoute?.coordinate != nil { + let mostRecent = selectedRoute?.node?.positions?.lastObject as! PositionEntity + let startPoint = CLLocation(latitude: selectedRoute?.coordinate?.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: selectedRoute?.coordinate?.longitude ?? LocationsHandler.DefaultLocation.longitude) + + if startPoint.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { + let metersAway = selectedRoute?.coordinate?.distance(from:CLLocationCoordinate2D(latitude: mostRecent.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: mostRecent.longitude ?? LocationsHandler.DefaultLocation.longitude)) + Label { + Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway ?? 0)))") + .foregroundColor(.primary) + } icon: { + Image(systemName: "lines.measurement.horizontal") + .symbolRenderingMode(.hierarchical) + } + } } } + } else { + VStack { + Label { + Text("Trace route sent to \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") + } icon: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) + } + .font(.title3) + Divider() + Label { + Text("\(selectedRoute?.time?.formatted() ?? "") - No response") + + } icon: { + Image(systemName: "person.slash") + .symbolRenderingMode(.hierarchical) + } + .font(.callout) + Spacer() + } } - .frame(maxWidth: .infinity, maxHeight: .infinity) } else { - Text("Trace Route sent to \(selectedRoute?.node?.user?.longName ?? "unknown".localized)") - .font(.title) - .padding(.top) - Spacer() - Text("\(selectedRoute?.time?.formatted() ?? "")") - .font(.title3) - Spacer() - Text("No response") - .font(.title2) - Spacer() + ContentUnavailableView("Select a Trace Route", systemImage: "signpost.right.and.left") } } + Spacer() } - .navigationTitle("Route Details") + .navigationTitle("Trace Route Log") } .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") + ZStack { + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { if self.bleManager.context == nil { diff --git a/Meshtastic/Views/Settings/RouteRecorder.swift b/Meshtastic/Views/Settings/RouteRecorder.swift index df5ca511..eb9ba679 100644 --- a/Meshtastic/Views/Settings/RouteRecorder.swift +++ b/Meshtastic/Views/Settings/RouteRecorder.swift @@ -144,14 +144,26 @@ struct RouteRecorder: View { Label("Speed \(speed.formatted())", systemImage: "speedometer") } if locationsHandler.lastLocation.courseAccuracy > 0 { - Label("Heading \(String(format: "%.2f", locationsHandler.lastLocation.course))°", systemImage: "location.circle") + /// Heading + let degrees = Angle.degrees(Double(locationsHandler.lastLocation.course)) + Label { + let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees) + /// Text("Heading: \(heading.formatted())") + Text("Heading \(String(format: "%.2f", locationsHandler.lastLocation.course))°") + .foregroundColor(.primary) + } icon: { + Image(systemName: "location.circle") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + .rotationEffect(degrees) + } } } .listStyle(.plain) } } } - .presentationDetents([.fraction(0.5)]) + .presentationDetents([.fraction(0.6)]) .presentationDragIndicator(.visible) } } From f2aea53ab61ba04d0146d71debb157bd5698c59f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 10 Dec 2023 12:38:01 -0800 Subject: [PATCH 13/20] Location cleanup --- Meshtastic/Helpers/BLEManager.swift | 12 ++++++-- .../Views/Nodes/Helpers/NodeListItem.swift | 30 ++++++++++++++----- Meshtastic/Views/Settings/RouteRecorder.swift | 9 ++---- Meshtastic/Views/Settings/Settings.swift | 1 - 4 files changed, 35 insertions(+), 17 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index c82b4796..8f38858b 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -41,6 +41,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let emptyNodeNum: UInt32 = 4294967295 let mqttManager = MqttClientProxyManager.shared var wantRangeTestPackets = false + var wantStoreAndForwardPackets = false /* Meshtastic Service Details */ var TORADIO_characteristic: CBCharacteristic! var FROMRADIO_characteristic: CBCharacteristic! @@ -607,13 +608,17 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate case .serialApp: MeshLogger.log("🕸️ MESH PACKET received for Serial App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") case .storeForwardApp: - storeAndForwardPacket(packet: decodedInfo.packet, connectedNodeNum: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) + if wantStoreAndForwardPackets { + storeAndForwardPacket(packet: decodedInfo.packet, connectedNodeNum: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) + } else { + 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!) } else { - MeshLogger.log("🕸️ MESH PACKET received for Range Test App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") + MeshLogger.log("🕸️ MESH PACKET received for Range Test App Range testing is disabled.") } case .telemetryApp: if !invalidVersion { telemetryPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) } @@ -719,6 +724,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if fetchedNodeInfo.count == 1 && fetchedNodeInfo[0].rangeTestConfig?.enabled == true { wantRangeTestPackets = true; } + if fetchedNodeInfo.count == 1 && fetchedNodeInfo[0].storeForwardConfig?.enabled == true { + wantStoreAndForwardPackets = true; + } } catch { print("Failed to find a node info for the connected node") diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index a5b5a3db..2fb63b53 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -55,14 +55,28 @@ struct NodeListItem: View { if node.positions?.count ?? 0 > 0 && connectedNode != node.num { HStack { let lastPostion = node.positions!.reversed()[0] as! PositionEntity - let myCoord = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude) - if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude { - let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude) - let metersAway = nodeCoord.distance(from: myCoord) - Image(systemName: "lines.measurement.horizontal") - .font(.callout) - .symbolRenderingMode(.hierarchical) - DistanceText(meters: metersAway).font(.callout) + if #available(iOS 17.0, macOS 14.0, *) { + let myCoord = CLLocation(latitude: LocationsHandler.shared.lastLocation.coordinate.latitude, longitude: LocationsHandler.shared.lastLocation.coordinate.longitude) + if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationsHandler.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationsHandler.DefaultLocation.latitude { + let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude) + let metersAway = nodeCoord.distance(from: myCoord) + Image(systemName: "lines.measurement.horizontal") + .font(.callout) + .symbolRenderingMode(.hierarchical) + DistanceText(meters: metersAway).font(.callout) + } + + } else { + + let myCoord = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude) + if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude { + let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude) + let metersAway = nodeCoord.distance(from: myCoord) + Image(systemName: "lines.measurement.horizontal") + .font(.callout) + .symbolRenderingMode(.hierarchical) + DistanceText(meters: metersAway).font(.callout) + } } } } diff --git a/Meshtastic/Views/Settings/RouteRecorder.swift b/Meshtastic/Views/Settings/RouteRecorder.swift index eb9ba679..3a168866 100644 --- a/Meshtastic/Views/Settings/RouteRecorder.swift +++ b/Meshtastic/Views/Settings/RouteRecorder.swift @@ -37,7 +37,7 @@ struct RouteRecorder: View { @State var isShowingDetails = false @State var timer: Timer? @Namespace var namespace - @Namespace var mapscope + @Namespace var routerecorderscope @State var timeElapsed: TimerDisplayObject = TimerDisplayObject() @State var timerDisplay = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @@ -45,7 +45,7 @@ struct RouteRecorder: View { VStack { VStack { VStack { - Map(position: $position, scope: mapscope) { + Map(position: $position, scope: routerecorderscope) { UserAnnotation() // ForEach(locations, id: \.id) { location in // Marker(location.name, systemImage: location.icon, coordinate: location.location) @@ -53,6 +53,7 @@ struct RouteRecorder: View { // } } } + .mapScope(routerecorderscope) .mapControls { MapUserLocationButton() MapCompass() @@ -62,10 +63,6 @@ struct RouteRecorder: View { .mapStyle(.hybrid(elevation: .realistic, showsTraffic: true)) .transition(.slide) .mapControlVisibility(.visible) - .task { - print("this is running") - locationsHandler.startLocationUpdates() - } .safeAreaInset(edge: .bottom) { ZStack { VStack { diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index f8daa9e8..83e00e32 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -313,7 +313,6 @@ struct Settings: View { } } .onAppear { - selection = SettingsSidebar.about if self.bleManager.context == nil { self.bleManager.context = context } From 8c95713ea2cedd96996c23086bb410c5d0f02014 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 10 Dec 2023 12:43:43 -0800 Subject: [PATCH 14/20] Background location description --- Meshtastic/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Meshtastic/Info.plist b/Meshtastic/Info.plist index b24ff3d9..48ae0608 100644 --- a/Meshtastic/Info.plist +++ b/Meshtastic/Info.plist @@ -57,6 +57,8 @@ We use your location to display it on the mesh map as well as to have GPS coordinatess to send to the connected device. NSLocationAlwaysUsageDescription We use your location to display it on the mesh map as well as to have GPS coordinatess to send to the connected device. + NSLocationAlwaysAndWhenInUseUsageDescription + We use your location to display it on the mesh map as well as to have GPS coordinatess to send to the connected device. Route Recording uses location in the background. NSSupportsLiveActivities Privacy – Bluetooth Always Usage Description From bde94e877ebf60508480e5e23de09468ad671c92 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 10 Dec 2023 17:54:45 -0800 Subject: [PATCH 15/20] Update protos and related enums. --- Meshtastic/Enums/DeviceEnums.swift | 28 + Meshtastic/MeshtasticApp.swift | 68 +- .../Protobufs/meshtastic/config.pb.swift | 32 + Meshtastic/Protobufs/meshtastic/mesh.pb.swift | 580 ++++++++++-------- 4 files changed, 419 insertions(+), 289 deletions(-) diff --git a/Meshtastic/Enums/DeviceEnums.swift b/Meshtastic/Enums/DeviceEnums.swift index 052607ab..25938a50 100644 --- a/Meshtastic/Enums/DeviceEnums.swift +++ b/Meshtastic/Enums/DeviceEnums.swift @@ -17,6 +17,9 @@ enum DeviceRoles: Int, CaseIterable, Identifiable { case repeater = 4 case tracker = 5 case sensor = 6 + case tak = 7 + case clientHidden = 8 + case lostAndFound = 9 var id: Int { self.rawValue } var name: String { @@ -35,6 +38,12 @@ enum DeviceRoles: Int, CaseIterable, Identifiable { return "Tracker" case .sensor: return "Sensor" + case .tak: + return "TAK" + case .clientHidden: + return "Client Hidden" + case .lostAndFound: + return "Lost and Found" } } var description: String { @@ -53,6 +62,12 @@ enum DeviceRoles: Int, CaseIterable, Identifiable { return "device.role.tracker".localized case .sensor: return "device.role.sensor".localized + case .tak: + return "device.role.tak".localized + case .clientHidden: + return "device.role.clienthidden".localized + case .lostAndFound: + return "device.role.lostandfound".localized } } func protoEnumValue() -> Config.DeviceConfig.Role { @@ -72,6 +87,12 @@ enum DeviceRoles: Int, CaseIterable, Identifiable { return Config.DeviceConfig.Role.tracker case .sensor: return Config.DeviceConfig.Role.sensor + case .tak: + return Config.DeviceConfig.Role.tak + case .clientHidden: + return Config.DeviceConfig.Role.clientHidden + case .lostAndFound: + return Config.DeviceConfig.Role.lostAndFound } } } @@ -81,6 +102,7 @@ enum RebroadcastModes: Int, CaseIterable, Identifiable { case all = 0 case allSkipDecoding = 1 case localOnly = 2 + case knownOnly = 3 var id: Int { self.rawValue } @@ -92,6 +114,8 @@ enum RebroadcastModes: Int, CaseIterable, Identifiable { return "All Skip Decoding" case .localOnly: return "Local Only" + case .knownOnly: + return "Known Only" } } var description: String { @@ -102,6 +126,8 @@ enum RebroadcastModes: Int, CaseIterable, Identifiable { return "Same as behavior as ALL but skips packet decoding and simply rebroadcasts them. Only available in Repeater role. Setting this on any other roles will result in ALL behavior." case .localOnly: return "Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. Only rebroadcasts message on the nodes local primary / secondary channels." + case .knownOnly: + return "Ignores observed messages from foreign meshes like Local Only, but takes it step further by also ignoring messages from nodes not already in the node's known list." } } func protoEnumValue() -> Config.DeviceConfig.RebroadcastMode { @@ -113,6 +139,8 @@ enum RebroadcastModes: Int, CaseIterable, Identifiable { return Config.DeviceConfig.RebroadcastMode.allSkipDecoding case .localOnly: return Config.DeviceConfig.RebroadcastMode.localOnly + case .knownOnly: + return Config.DeviceConfig.RebroadcastMode.knownOnly } } } diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 9a43be75..852b2504 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -59,40 +59,42 @@ struct MeshtasticAppleApp: App { saveChannels = false print("User wants to import a MBTILES offline map file: \(self.incomingUrl?.absoluteString ?? "No Tiles link")") } + + if UserDefaults.mapUseLegacy { + /// we are expecting a .mbtiles map file that contains raster data + /// save it to the documents directory, and name it offline_map.mbtiles + let fileManager = FileManager.default + let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! + let destination = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false) - // we are expecting a .mbtiles map file that contains raster data - // save it to the documents directory, and name it offline_map.mbtiles - let fileManager = FileManager.default - let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! - let destination = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false) - - if !self.saveChannels { - - // tell the system we want the file please - guard url.startAccessingSecurityScopedResource() else { - return - } - - // do we need to delete an old one? - if fileManager.fileExists(atPath: destination.path) { - print("ℹ️ Found an old map file. Deleting it") - try? fileManager.removeItem(atPath: destination.path) - } - - do { - try fileManager.copyItem(at: url, to: destination) - } catch { - print("Copy MB Tile file failed. Error: \(error)") - } - - if fileManager.fileExists(atPath: destination.path) { - print("ℹ️ Saved the map file") - - // need to tell the map view that it needs to update and try loading the new overlay - UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "lastUpdatedLocalMapFile") - - } else { - print("💥 Didn't save the map file") + if !self.saveChannels { + + // tell the system we want the file please + guard url.startAccessingSecurityScopedResource() else { + return + } + + // do we need to delete an old one? + if fileManager.fileExists(atPath: destination.path) { + print("ℹ️ Found an old map file. Deleting it") + try? fileManager.removeItem(atPath: destination.path) + } + + do { + try fileManager.copyItem(at: url, to: destination) + } catch { + print("Copy MB Tile file failed. Error: \(error)") + } + + if fileManager.fileExists(atPath: destination.path) { + print("ℹ️ Saved the map file") + + // need to tell the map view that it needs to update and try loading the new overlay + UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "lastUpdatedLocalMapFile") + + } else { + print("💥 Didn't save the map file") + } } } }) diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index d04e9003..ef91027c 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -244,6 +244,21 @@ struct Config { /// Turns off many of the routine broadcasts to favor CoT packet stream /// from the Meshtastic ATAK plugin -> IMeshService -> Node case tak // = 7 + + /// + /// Client Hidden device role + /// 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 clandestine operation or to dramatically reduce airtime / power consumption + case clientHidden // = 8 + + /// + /// Lost and Found device role + /// 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" + case lostAndFound // = 9 case UNRECOGNIZED(Int) init() { @@ -260,6 +275,8 @@ struct Config { case 5: self = .tracker case 6: self = .sensor case 7: self = .tak + case 8: self = .clientHidden + case 9: self = .lostAndFound default: self = .UNRECOGNIZED(rawValue) } } @@ -274,6 +291,8 @@ struct Config { case .tracker: return 5 case .sensor: return 6 case .tak: return 7 + case .clientHidden: return 8 + case .lostAndFound: return 9 case .UNRECOGNIZED(let i): return i } } @@ -299,6 +318,11 @@ struct Config { /// Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. /// Only rebroadcasts message on the nodes local primary / secondary channels. case localOnly // = 2 + + /// + /// Ignores observed messages from foreign meshes like LOCAL_ONLY, + /// but takes it step further by also ignoring messages from nodenums not in the node's known list (NodeDB) + case knownOnly // = 3 case UNRECOGNIZED(Int) init() { @@ -310,6 +334,7 @@ struct Config { case 0: self = .all case 1: self = .allSkipDecoding case 2: self = .localOnly + case 3: self = .knownOnly default: self = .UNRECOGNIZED(rawValue) } } @@ -319,6 +344,7 @@ struct Config { case .all: return 0 case .allSkipDecoding: return 1 case .localOnly: return 2 + case .knownOnly: return 3 case .UNRECOGNIZED(let i): return i } } @@ -1295,6 +1321,8 @@ extension Config.DeviceConfig.Role: CaseIterable { .tracker, .sensor, .tak, + .clientHidden, + .lostAndFound, ] } @@ -1304,6 +1332,7 @@ extension Config.DeviceConfig.RebroadcastMode: CaseIterable { .all, .allSkipDecoding, .localOnly, + .knownOnly, ] } @@ -1703,6 +1732,8 @@ extension Config.DeviceConfig.Role: SwiftProtobuf._ProtoNameProviding { 5: .same(proto: "TRACKER"), 6: .same(proto: "SENSOR"), 7: .same(proto: "TAK"), + 8: .same(proto: "CLIENT_HIDDEN"), + 9: .same(proto: "LOST_AND_FOUND"), ] } @@ -1711,6 +1742,7 @@ extension Config.DeviceConfig.RebroadcastMode: SwiftProtobuf._ProtoNameProviding 0: .same(proto: "ALL"), 1: .same(proto: "ALL_SKIP_DECODING"), 2: .same(proto: "LOCAL_ONLY"), + 3: .same(proto: "KNOWN_ONLY"), ] } diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index cc2388a8..b8ebf212 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -118,6 +118,14 @@ enum HardwareModel: SwiftProtobuf.Enum { /// RAK11310 (RP2040 + SX1262) case rak11310 // = 26 + /// + /// Makerfabs SenseLoRA Receiver (RP2040 + RFM96) + case senseloraRp2040 // = 27 + + /// + /// Makerfabs SenseLoRA Industrial Monitor (ESP32-S3 + RFM96) + case senseloraS3 // = 28 + /// /// --------------------------------------------------------------------------- /// Less common/prototype boards listed here (needs one more byte over the air) @@ -247,6 +255,8 @@ enum HardwareModel: SwiftProtobuf.Enum { case 19: self = .loraType case 25: self = .stationG1 case 26: self = .rak11310 + case 27: self = .senseloraRp2040 + case 28: self = .senseloraS3 case 32: self = .loraRelayV1 case 33: self = .nrf52840Dk case 34: self = .ppr @@ -299,6 +309,8 @@ enum HardwareModel: SwiftProtobuf.Enum { case .loraType: return 19 case .stationG1: return 25 case .rak11310: return 26 + case .senseloraRp2040: return 27 + case .senseloraS3: return 28 case .loraRelayV1: return 32 case .nrf52840Dk: return 33 case .ppr: return 34 @@ -356,6 +368,8 @@ extension HardwareModel: CaseIterable { .loraType, .stationG1, .rak11310, + .senseloraRp2040, + .senseloraS3, .loraRelayV1, .nrf52840Dk, .ppr, @@ -935,6 +949,10 @@ struct User { /// Also, "long_name" should be their licence number. var isLicensed: Bool = false + /// + /// Indicates that the user's role in the mesh + var role: Config.DeviceConfig.Role = .client + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -1930,20 +1948,26 @@ struct FromRadio { /// /// The packet id, used to allow the phone to request missing read packets from the FIFO, /// see our bluetooth docs - var id: UInt32 = 0 + var id: UInt32 { + get {return _storage._id} + set {_uniqueStorage()._id = newValue} + } /// /// Log levels, chosen to match python logging conventions. - var payloadVariant: FromRadio.OneOf_PayloadVariant? = nil + var payloadVariant: OneOf_PayloadVariant? { + get {return _storage._payloadVariant} + set {_uniqueStorage()._payloadVariant = newValue} + } /// /// Log levels, chosen to match python logging conventions. var packet: MeshPacket { get { - if case .packet(let v)? = payloadVariant {return v} + if case .packet(let v)? = _storage._payloadVariant {return v} return MeshPacket() } - set {payloadVariant = .packet(newValue)} + set {_uniqueStorage()._payloadVariant = .packet(newValue)} } /// @@ -1951,10 +1975,10 @@ struct FromRadio { /// NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps. var myInfo: MyNodeInfo { get { - if case .myInfo(let v)? = payloadVariant {return v} + if case .myInfo(let v)? = _storage._payloadVariant {return v} return MyNodeInfo() } - set {payloadVariant = .myInfo(newValue)} + set {_uniqueStorage()._payloadVariant = .myInfo(newValue)} } /// @@ -1962,30 +1986,30 @@ struct FromRadio { /// starts over with the first node in our DB var nodeInfo: NodeInfo { get { - if case .nodeInfo(let v)? = payloadVariant {return v} + if case .nodeInfo(let v)? = _storage._payloadVariant {return v} return NodeInfo() } - set {payloadVariant = .nodeInfo(newValue)} + set {_uniqueStorage()._payloadVariant = .nodeInfo(newValue)} } /// /// Include a part of the config (was: RadioConfig radio) var config: Config { get { - if case .config(let v)? = payloadVariant {return v} + if case .config(let v)? = _storage._payloadVariant {return v} return Config() } - set {payloadVariant = .config(newValue)} + set {_uniqueStorage()._payloadVariant = .config(newValue)} } /// /// Set to send debug console output over our protobuf stream var logRecord: LogRecord { get { - if case .logRecord(let v)? = payloadVariant {return v} + if case .logRecord(let v)? = _storage._payloadVariant {return v} return LogRecord() } - set {payloadVariant = .logRecord(newValue)} + set {_uniqueStorage()._payloadVariant = .logRecord(newValue)} } /// @@ -1995,10 +2019,10 @@ struct FromRadio { /// NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps. var configCompleteID: UInt32 { get { - if case .configCompleteID(let v)? = payloadVariant {return v} + if case .configCompleteID(let v)? = _storage._payloadVariant {return v} return 0 } - set {payloadVariant = .configCompleteID(newValue)} + set {_uniqueStorage()._payloadVariant = .configCompleteID(newValue)} } /// @@ -2008,70 +2032,70 @@ struct FromRadio { /// NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps. var rebooted: Bool { get { - if case .rebooted(let v)? = payloadVariant {return v} + if case .rebooted(let v)? = _storage._payloadVariant {return v} return false } - set {payloadVariant = .rebooted(newValue)} + set {_uniqueStorage()._payloadVariant = .rebooted(newValue)} } /// /// Include module config var moduleConfig: ModuleConfig { get { - if case .moduleConfig(let v)? = payloadVariant {return v} + if case .moduleConfig(let v)? = _storage._payloadVariant {return v} return ModuleConfig() } - set {payloadVariant = .moduleConfig(newValue)} + set {_uniqueStorage()._payloadVariant = .moduleConfig(newValue)} } /// /// One packet is sent for each channel var channel: Channel { get { - if case .channel(let v)? = payloadVariant {return v} + if case .channel(let v)? = _storage._payloadVariant {return v} return Channel() } - set {payloadVariant = .channel(newValue)} + set {_uniqueStorage()._payloadVariant = .channel(newValue)} } /// /// Queue status info var queueStatus: QueueStatus { get { - if case .queueStatus(let v)? = payloadVariant {return v} + if case .queueStatus(let v)? = _storage._payloadVariant {return v} return QueueStatus() } - set {payloadVariant = .queueStatus(newValue)} + set {_uniqueStorage()._payloadVariant = .queueStatus(newValue)} } /// /// File Transfer Chunk var xmodemPacket: XModem { get { - if case .xmodemPacket(let v)? = payloadVariant {return v} + if case .xmodemPacket(let v)? = _storage._payloadVariant {return v} return XModem() } - set {payloadVariant = .xmodemPacket(newValue)} + set {_uniqueStorage()._payloadVariant = .xmodemPacket(newValue)} } /// /// Device metadata message var metadata: DeviceMetadata { get { - if case .metadata(let v)? = payloadVariant {return v} + if case .metadata(let v)? = _storage._payloadVariant {return v} return DeviceMetadata() } - set {payloadVariant = .metadata(newValue)} + set {_uniqueStorage()._payloadVariant = .metadata(newValue)} } /// /// MQTT Client Proxy Message (device sending to client / phone for publishing to MQTT) var mqttClientProxyMessage: MqttClientProxyMessage { get { - if case .mqttClientProxyMessage(let v)? = payloadVariant {return v} + if case .mqttClientProxyMessage(let v)? = _storage._payloadVariant {return v} return MqttClientProxyMessage() } - set {payloadVariant = .mqttClientProxyMessage(newValue)} + set {_uniqueStorage()._payloadVariant = .mqttClientProxyMessage(newValue)} } var unknownFields = SwiftProtobuf.UnknownStorage() @@ -2192,6 +2216,8 @@ struct FromRadio { } init() {} + + fileprivate var _storage = _StorageClass.defaultInstance } /// @@ -2519,6 +2545,8 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 19: .same(proto: "LORA_TYPE"), 25: .same(proto: "STATION_G1"), 26: .same(proto: "RAK11310"), + 27: .same(proto: "SENSELORA_RP2040"), + 28: .same(proto: "SENSELORA_S3"), 32: .same(proto: "LORA_RELAY_V1"), 33: .same(proto: "NRF52840DK"), 34: .same(proto: "PPR"), @@ -2830,6 +2858,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, 4: .same(proto: "macaddr"), 5: .standard(proto: "hw_model"), 6: .standard(proto: "is_licensed"), + 7: .same(proto: "role"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2844,6 +2873,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, case 4: try { try decoder.decodeSingularBytesField(value: &self.macaddr) }() case 5: try { try decoder.decodeSingularEnumField(value: &self.hwModel) }() case 6: try { try decoder.decodeSingularBoolField(value: &self.isLicensed) }() + case 7: try { try decoder.decodeSingularEnumField(value: &self.role) }() default: break } } @@ -2868,6 +2898,9 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, if self.isLicensed != false { try visitor.visitSingularBoolField(value: self.isLicensed, fieldNumber: 6) } + if self.role != .client { + try visitor.visitSingularEnumField(value: self.role, fieldNumber: 7) + } try unknownFields.traverse(visitor: &visitor) } @@ -2878,6 +2911,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, if lhs.macaddr != rhs.macaddr {return false} if lhs.hwModel != rhs.hwModel {return false} if lhs.isLicensed != rhs.isLicensed {return false} + if lhs.role != rhs.role {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -3687,246 +3721,280 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 14: .same(proto: "mqttClientProxyMessage"), ] + fileprivate class _StorageClass { + var _id: UInt32 = 0 + var _payloadVariant: FromRadio.OneOf_PayloadVariant? + + static let defaultInstance = _StorageClass() + + private init() {} + + init(copying source: _StorageClass) { + _id = source._id + _payloadVariant = source._payloadVariant + } + } + + 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.id) }() - case 2: try { - var v: MeshPacket? - var hadOneofValue = false - if let current = self.payloadVariant { - hadOneofValue = true - if case .packet(let m) = current {v = m} + _ = _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._id) }() + case 2: try { + var v: MeshPacket? + var hadOneofValue = false + if let current = _storage._payloadVariant { + hadOneofValue = true + if case .packet(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._payloadVariant = .packet(v) + } + }() + case 3: try { + var v: MyNodeInfo? + var hadOneofValue = false + if let current = _storage._payloadVariant { + hadOneofValue = true + if case .myInfo(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._payloadVariant = .myInfo(v) + } + }() + case 4: try { + var v: NodeInfo? + var hadOneofValue = false + if let current = _storage._payloadVariant { + hadOneofValue = true + if case .nodeInfo(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._payloadVariant = .nodeInfo(v) + } + }() + case 5: try { + var v: Config? + var hadOneofValue = false + if let current = _storage._payloadVariant { + hadOneofValue = true + if case .config(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._payloadVariant = .config(v) + } + }() + case 6: try { + var v: LogRecord? + var hadOneofValue = false + if let current = _storage._payloadVariant { + hadOneofValue = true + if case .logRecord(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._payloadVariant = .logRecord(v) + } + }() + case 7: try { + var v: UInt32? + try decoder.decodeSingularUInt32Field(value: &v) + if let v = v { + if _storage._payloadVariant != nil {try decoder.handleConflictingOneOf()} + _storage._payloadVariant = .configCompleteID(v) + } + }() + case 8: try { + var v: Bool? + try decoder.decodeSingularBoolField(value: &v) + if let v = v { + if _storage._payloadVariant != nil {try decoder.handleConflictingOneOf()} + _storage._payloadVariant = .rebooted(v) + } + }() + case 9: try { + var v: ModuleConfig? + var hadOneofValue = false + if let current = _storage._payloadVariant { + hadOneofValue = true + if case .moduleConfig(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._payloadVariant = .moduleConfig(v) + } + }() + case 10: try { + var v: Channel? + var hadOneofValue = false + if let current = _storage._payloadVariant { + hadOneofValue = true + if case .channel(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._payloadVariant = .channel(v) + } + }() + case 11: try { + var v: QueueStatus? + var hadOneofValue = false + if let current = _storage._payloadVariant { + hadOneofValue = true + if case .queueStatus(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._payloadVariant = .queueStatus(v) + } + }() + case 12: try { + var v: XModem? + var hadOneofValue = false + if let current = _storage._payloadVariant { + hadOneofValue = true + if case .xmodemPacket(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._payloadVariant = .xmodemPacket(v) + } + }() + case 13: try { + var v: DeviceMetadata? + var hadOneofValue = false + if let current = _storage._payloadVariant { + hadOneofValue = true + if case .metadata(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._payloadVariant = .metadata(v) + } + }() + case 14: try { + var v: MqttClientProxyMessage? + var hadOneofValue = false + if let current = _storage._payloadVariant { + hadOneofValue = true + if case .mqttClientProxyMessage(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + _storage._payloadVariant = .mqttClientProxyMessage(v) + } + }() + default: break } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.payloadVariant = .packet(v) - } - }() - case 3: try { - var v: MyNodeInfo? - var hadOneofValue = false - if let current = self.payloadVariant { - hadOneofValue = true - if case .myInfo(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.payloadVariant = .myInfo(v) - } - }() - case 4: try { - var v: NodeInfo? - var hadOneofValue = false - if let current = self.payloadVariant { - hadOneofValue = true - if case .nodeInfo(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.payloadVariant = .nodeInfo(v) - } - }() - case 5: try { - var v: Config? - var hadOneofValue = false - if let current = self.payloadVariant { - hadOneofValue = true - if case .config(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.payloadVariant = .config(v) - } - }() - case 6: try { - var v: LogRecord? - var hadOneofValue = false - if let current = self.payloadVariant { - hadOneofValue = true - if case .logRecord(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.payloadVariant = .logRecord(v) - } - }() - case 7: try { - var v: UInt32? - try decoder.decodeSingularUInt32Field(value: &v) - if let v = v { - if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} - self.payloadVariant = .configCompleteID(v) - } - }() - case 8: try { - var v: Bool? - try decoder.decodeSingularBoolField(value: &v) - if let v = v { - if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} - self.payloadVariant = .rebooted(v) - } - }() - case 9: try { - var v: ModuleConfig? - var hadOneofValue = false - if let current = self.payloadVariant { - hadOneofValue = true - if case .moduleConfig(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.payloadVariant = .moduleConfig(v) - } - }() - case 10: try { - var v: Channel? - var hadOneofValue = false - if let current = self.payloadVariant { - hadOneofValue = true - if case .channel(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.payloadVariant = .channel(v) - } - }() - case 11: try { - var v: QueueStatus? - var hadOneofValue = false - if let current = self.payloadVariant { - hadOneofValue = true - if case .queueStatus(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.payloadVariant = .queueStatus(v) - } - }() - case 12: try { - var v: XModem? - var hadOneofValue = false - if let current = self.payloadVariant { - hadOneofValue = true - if case .xmodemPacket(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.payloadVariant = .xmodemPacket(v) - } - }() - case 13: try { - var v: DeviceMetadata? - var hadOneofValue = false - if let current = self.payloadVariant { - hadOneofValue = true - if case .metadata(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.payloadVariant = .metadata(v) - } - }() - case 14: try { - var v: MqttClientProxyMessage? - var hadOneofValue = false - if let current = self.payloadVariant { - hadOneofValue = true - if case .mqttClientProxyMessage(let m) = current {v = m} - } - try decoder.decodeSingularMessageField(value: &v) - if let v = v { - if hadOneofValue {try decoder.handleConflictingOneOf()} - self.payloadVariant = .mqttClientProxyMessage(v) - } - }() - 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.id != 0 { - try visitor.visitSingularUInt32Field(value: self.id, fieldNumber: 1) - } - switch self.payloadVariant { - case .packet?: try { - guard case .packet(let v)? = self.payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - }() - case .myInfo?: try { - guard case .myInfo(let v)? = self.payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - }() - case .nodeInfo?: try { - guard case .nodeInfo(let v)? = self.payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 4) - }() - case .config?: try { - guard case .config(let v)? = self.payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 5) - }() - case .logRecord?: try { - guard case .logRecord(let v)? = self.payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 6) - }() - case .configCompleteID?: try { - guard case .configCompleteID(let v)? = self.payloadVariant else { preconditionFailure() } - try visitor.visitSingularUInt32Field(value: v, fieldNumber: 7) - }() - case .rebooted?: try { - guard case .rebooted(let v)? = self.payloadVariant else { preconditionFailure() } - try visitor.visitSingularBoolField(value: v, fieldNumber: 8) - }() - case .moduleConfig?: try { - guard case .moduleConfig(let v)? = self.payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 9) - }() - case .channel?: try { - guard case .channel(let v)? = self.payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 10) - }() - case .queueStatus?: try { - guard case .queueStatus(let v)? = self.payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 11) - }() - case .xmodemPacket?: try { - guard case .xmodemPacket(let v)? = self.payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 12) - }() - case .metadata?: try { - guard case .metadata(let v)? = self.payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 13) - }() - case .mqttClientProxyMessage?: try { - guard case .mqttClientProxyMessage(let v)? = self.payloadVariant else { preconditionFailure() } - try visitor.visitSingularMessageField(value: v, fieldNumber: 14) - }() - case nil: break + 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._id != 0 { + try visitor.visitSingularUInt32Field(value: _storage._id, fieldNumber: 1) + } + switch _storage._payloadVariant { + case .packet?: try { + guard case .packet(let v)? = _storage._payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case .myInfo?: try { + guard case .myInfo(let v)? = _storage._payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + }() + case .nodeInfo?: try { + guard case .nodeInfo(let v)? = _storage._payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case .config?: try { + guard case .config(let v)? = _storage._payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 5) + }() + case .logRecord?: try { + guard case .logRecord(let v)? = _storage._payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 6) + }() + case .configCompleteID?: try { + guard case .configCompleteID(let v)? = _storage._payloadVariant else { preconditionFailure() } + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 7) + }() + case .rebooted?: try { + guard case .rebooted(let v)? = _storage._payloadVariant else { preconditionFailure() } + try visitor.visitSingularBoolField(value: v, fieldNumber: 8) + }() + case .moduleConfig?: try { + guard case .moduleConfig(let v)? = _storage._payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 9) + }() + case .channel?: try { + guard case .channel(let v)? = _storage._payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 10) + }() + case .queueStatus?: try { + guard case .queueStatus(let v)? = _storage._payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + }() + case .xmodemPacket?: try { + guard case .xmodemPacket(let v)? = _storage._payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 12) + }() + case .metadata?: try { + guard case .metadata(let v)? = _storage._payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 13) + }() + case .mqttClientProxyMessage?: try { + guard case .mqttClientProxyMessage(let v)? = _storage._payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 14) + }() + case nil: break + } } try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: FromRadio, rhs: FromRadio) -> Bool { - if lhs.id != rhs.id {return false} - if lhs.payloadVariant != rhs.payloadVariant {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._id != rhs_storage._id {return false} + if _storage._payloadVariant != rhs_storage._payloadVariant {return false} + return true + } + if !storagesAreEqual {return false} + } if lhs.unknownFields != rhs.unknownFields {return false} return true } From 21b997d760006d841e3789bf86b36bbb2e63dfd7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 10 Dec 2023 18:38:48 -0800 Subject: [PATCH 16/20] Localize the new roles --- de.lproj/Localizable.strings | 2 ++ en.lproj/Localizable.strings | 3 +++ pl.lproj/Localizable.strings | 2 ++ zh-Hans.lproj/Localizable.strings | 2 ++ 4 files changed, 9 insertions(+) diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index daed2a2f..c58b80ae 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -66,6 +66,8 @@ "device.metrics.log"="Device Metrics Log"; "device.role.client"="Client (Standard) - Mit App verbundener Client."; "device.role.clientmute"="Client Leise - Das selbe wie Client, außer das die Pakete nicht über diesen Node weitergeleitet werden. Nimmt nicht am Mesh-Routing teil."; +"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.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.router"="Router - Mesh Pakete werden bevorzugt über diesen Node gerouted. Dieser Node wird nicht von einer Client App benutzt. WLAN, Bluetooth und Display sind aus."; "device.role.routerclient"="Router Client - Mesh Pakete werden bevorzugt über diesen Node gerouted. Der Router Client kann parallel auch von einer Client-App genutzt werden."; "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."; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 6d47569c..2af3db19 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -68,11 +68,14 @@ "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"; "direct.messages"="Direct Messages"; "dismiss.keyboard"="Dismiss"; "display"="Display (Device Screen)"; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index 50c1c882..8fd93d41 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -68,6 +68,8 @@ "device.metrics.log"="Dziennik metryk urządzenia"; "device.role.client"="Klient (domyślnie) - Klient połączony z aplikacją."; "device.role.clientmute"="Wyciszenie klienta - To samo, co klient, z wyjątkiem pakietów, które nie przeskakują przez ten węzeł, nie przyczynia się do routingu pakietów dla siatki."; +"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.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 - Pakiety siatki będą preferować trasowanie przez ten węzeł. Zakłada, że urządzenie będzie działać samodzielnie, umieszczone w miejscu z przewagą zasięgu. UWAGA: Radia BLE/Wi-Fi i ekran OLED zostaną uśpione."; "device.role.routerclient"="Router Client - Hybryda ról klienta i routera. Podobnie jak w przypadku routera, z tym że Router Client może być używany zarówno jako router, jak i klient połączony z aplikacją. Radia BLE/Wi-Fi i ekran OLED nie zostaną uśpione."; "device.role.repeater"="Przekaźnik - Pakiety siatki będą preferować trasowanie przez ten węzeł. Ta rola eliminuje niepotrzebny nadmiar, taki jak NodeInfo, DeviceTelemetry i inne pakiety siatki, skutkując tym, że urządzenie nie będzie widoczne jako część sieci. Proszę zobaczyć tryb Rebroadcast dla dodatkowych ustawień specyficznych dla tej roli."; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index b2d86a68..31269553 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -66,6 +66,8 @@ "device.metrics.log"="电台指标日志"; "device.role.client"="标准模式 - App 可以连接到电台进行收发操作,并且会自动转发 Mesh 网络中其他节点的消息。"; "device.role.clientmute"="静默模式 - 与标准模式类似,App 可以连接到电台进行收发操作,但不会转发 Mesh 网络中其他节点的消息。"; +"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.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"="纯路由模式 - 自动转发 Mesh 网络中其他节点的消息,中继模式下屏幕会熄灭,Wi-Fi 和蓝牙将会进入睡眠模式,App 将无法连接到电台进行收发操作。"; "device.role.routerclient"="路由客户端模式 - 优先转发 Mesh 网络中其他节点的消息,App 也可以连接到电台进行收发操作。"; "device.role.repeater"="中继模式 - Mesh 网络数据包将优先通过此节点路由。此模式可消除不必要的开销,如 NodeInfo、DeviceTelemetry 和任何其他 Mesh 数据包,从而使设备不显示为 Mesh 网络的一部分。有关此角色的其他特定设置,请参阅转播模式。"; From 0a501768a2fae98ae3fff0ed4a37e98b60a88570 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 10 Dec 2023 19:18:51 -0800 Subject: [PATCH 17/20] Ambient lighting localization strings --- de.lproj/Localizable.strings | 1 + en.lproj/Localizable.strings | 1 + pl.lproj/Localizable.strings | 1 + zh-Hans.lproj/Localizable.strings | 1 + 4 files changed, 4 insertions(+) diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index c58b80ae..54c5d6f3 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -153,6 +153,7 @@ "map.usertrackingmode.followwithheading"="Follow with heading"; "mesh.live.activity"="Mesh Live Activity"; "mesh.log"="Mesh Log"; +"mesh.log.ambientlighting.config %@"="Ambient Lighting module config received: %@"; "mesh.log.bluetooth.config %@"="Bluetooth Konfiguration empfangen: %@"; "mesh.log.cannedmessage.config %@"="Canned Message module config received: %@"; "mesh.log.cannedmessages.messages.get %@"="Requested Canned Messages Module Messages for node: %@"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 2af3db19..355a9c26 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -157,6 +157,7 @@ "map.usertrackingmode.none"="None"; "mesh.live.activity"="Mesh Live Activity"; "mesh.log"="Mesh Log"; +"mesh.log.ambientlighting.config %@"="Ambient Lighting module config received: %@"; "mesh.log.bluetooth.config %@"="Bluetooth config received: %@"; "mesh.log.cannedmessage.config %@"="Canned Message module config received: %@"; "mesh.log.cannedmessages.messages.get %@"="Requested Canned Messages Module Messages for node: %@"; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index 8fd93d41..95ccadb5 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -155,6 +155,7 @@ "map.usertrackingmode.none"="Brak"; "mesh.live.activity"="Aktywność na Żywo"; "mesh.log"="Dziennik Sieci"; +"mesh.log.ambientlighting.config %@"="Ambient Lighting module config received: %@"; "mesh.log.bluetooth.config %@"="Otrzymano konfigurację Bluetooth: %@"; "mesh.log.cannedmessage.config %@"="Otrzymano konfigurację modułu wiadomości gotowych: %@"; "mesh.log.cannedmessages.messages.get %@"="Zażądano Wiadomości z Modułu Wiadomości Gotowych dla węzła: %@"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 31269553..202a0871 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -153,6 +153,7 @@ "map.usertrackingmode.followwithheading"="Follow with heading"; "mesh.live.activity"="Mesh 实时活动"; "mesh.log"="Mesh 日志"; +"mesh.log.ambientlighting.config %@"="Ambient Lighting module config received: %@"; "mesh.log.bluetooth.config %@"="Bluetooth config received: %@"; "mesh.log.cannedmessage.config %@"="Canned Message module config received: %@"; "mesh.log.cannedmessages.messages.get %@"="Requested Canned Messages Module Messages for node: %@"; From 5a98929264bfd7b56dd9c4d80dfc845e605a8113 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 10 Dec 2023 19:25:50 -0800 Subject: [PATCH 18/20] Hide route recorder --- Meshtastic/Views/Settings/Settings.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 83e00e32..69a8cc96 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -70,14 +70,14 @@ struct Settings: View { } .tag(SettingsSidebar.routes) - NavigationLink { - RouteRecorder() - } label: { - Image(systemName: "record.circle") - .symbolRenderingMode(.hierarchical) - Text("route.recorder") - } - .tag(SettingsSidebar.routeRecorder) +// NavigationLink { +// RouteRecorder() +// } label: { +// Image(systemName: "record.circle") +// .symbolRenderingMode(.hierarchical) +// Text("route.recorder") +// } +// .tag(SettingsSidebar.routeRecorder) } let node = nodes.first(where: { $0.num == preferredNodeNum }) From 616f681eacfc254019ee1bbc9603b5ba5c5718fb Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 12 Dec 2023 22:59:09 -0800 Subject: [PATCH 19/20] Delete unused enum --- Meshtastic/Enums/PositionConfigEnums.swift | 28 ---------------------- 1 file changed, 28 deletions(-) diff --git a/Meshtastic/Enums/PositionConfigEnums.swift b/Meshtastic/Enums/PositionConfigEnums.swift index 171df3a5..49f74cd6 100644 --- a/Meshtastic/Enums/PositionConfigEnums.swift +++ b/Meshtastic/Enums/PositionConfigEnums.swift @@ -52,31 +52,3 @@ enum GpsFormats: Int, CaseIterable, Identifiable { } } } - -enum GpsAttemptTimes: Int, CaseIterable, Identifiable { - - case fifteenMinutes = 900 - case thirtyMinutes = 1800 - case oneHour = 3600 - case sixHours = 21600 - case twelveHours = 43200 - case twentyFourHours = 86400 - - var id: Int { self.rawValue } - var description: String { - switch self { - 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 - } - } -} From ae548a217650c55abf46263350553e30f352054f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 12 Dec 2023 23:01:26 -0800 Subject: [PATCH 20/20] Remove unused location code --- Meshtastic/Helpers/LocationHelper.swift | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/Meshtastic/Helpers/LocationHelper.swift b/Meshtastic/Helpers/LocationHelper.swift index 857fdc63..8d0100c0 100644 --- a/Meshtastic/Helpers/LocationHelper.swift +++ b/Meshtastic/Helpers/LocationHelper.swift @@ -67,26 +67,7 @@ class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate { } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - let chimeOnLocationUpdate = true - // locationManager.stopUpdatingLocation() - // locations.last.map { - // region = MKCoordinateRegion( - // center: $0.coordinate, - // span: .init(latitudeDelta: 0.01, longitudeDelta: 0.01) - // ) - // } - // Play a sound so it's easy to tell when a location update occurs while the app is in the background. - if chimeOnLocationUpdate && !locations.isEmpty { - // setSessionActiveWithMixing(true) // Ducks the audio of other apps when playing the chime. - // playSound() - } - // Always process all of the provided locations. Don't assume the array only contains a single location. - for location in locations { - - print("process a location") - // displayNewBreadcrumbOnMap(location) - } } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { print("Location manager error: \(error.localizedDescription)")