From 3aa924c9544ca11e05a8cc1b92e6392b63206776 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 26 Apr 2024 08:18:49 -0700 Subject: [PATCH 01/11] Bump firmware version --- Meshtastic.xcodeproj/project.pbxproj | 14 +- Meshtastic/Views/Helpers/CircleText.swift | 30 +++- .../Views/Helpers/IndoorAirQuality.swift | 125 --------------- .../Views/Helpers/Weather/IAQScale.swift | 18 +++ .../Helpers/Weather/IndoorAirQuality.swift | 148 ++++++++++++++++++ Meshtastic/Views/Settings/Firmware.swift | 2 +- 6 files changed, 198 insertions(+), 139 deletions(-) delete mode 100644 Meshtastic/Views/Helpers/IndoorAirQuality.swift create mode 100644 Meshtastic/Views/Helpers/Weather/IAQScale.swift create mode 100644 Meshtastic/Views/Helpers/Weather/IndoorAirQuality.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 86eb6681..50002bf1 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -50,6 +50,7 @@ DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */; }; DD33DB622B3D27C7003E1EA0 /* FirmwareApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */; }; DD3501892852FC3B000FC853 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3501882852FC3B000FC853 /* Settings.swift */; }; + DD354FD92BD96A0B0061A25F /* IAQScale.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD354FD82BD96A0B0061A25F /* IAQScale.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 */; }; @@ -296,6 +297,7 @@ DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 23.xcdatamodel"; sourceTree = ""; }; DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareApi.swift; sourceTree = ""; }; DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; + DD354FD82BD96A0B0061A25F /* IAQScale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAQScale.swift; sourceTree = ""; }; DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV21.xcdatamodel; sourceTree = ""; }; DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsHandler.swift; sourceTree = ""; }; DD398EBD2B93F640002B4C51 /* MeshtasticDataModelV 29.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 29.xcdatamodel"; sourceTree = ""; }; @@ -651,7 +653,9 @@ children = ( DD5E523E298F5A9E00D21B61 /* AirQualityIndexCompact.swift */, DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */, + DDA9515D2BC6F56F00CEA535 /* IndoorAirQuality.swift */, DD41A61429AB0035003C5A37 /* NodeWeatherForecast.swift */, + DD354FD82BD96A0B0061A25F /* IAQScale.swift */, ); path = Weather; sourceTree = ""; @@ -921,7 +925,6 @@ DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */, DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */, DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */, - DDA9515D2BC6F56F00CEA535 /* IndoorAirQuality.swift */, ); path = Helpers; sourceTree = ""; @@ -1239,6 +1242,7 @@ DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */, + DD354FD92BD96A0B0061A25F /* IAQScale.swift in Sources */, DDDB445429F8AD1600EE2349 /* Data.swift in Sources */, DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */, DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */, @@ -1578,7 +1582,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3.6; + MARKETING_VERSION = 2.3.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1612,7 +1616,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3.6; + MARKETING_VERSION = 2.3.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1685,7 +1689,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.6; + MARKETING_VERSION = 2.3.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1718,7 +1722,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.6; + MARKETING_VERSION = 2.3.7; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Views/Helpers/CircleText.swift b/Meshtastic/Views/Helpers/CircleText.swift index bb3fa07b..0a348349 100644 --- a/Meshtastic/Views/Helpers/CircleText.swift +++ b/Meshtastic/Views/Helpers/CircleText.swift @@ -29,10 +29,8 @@ struct CircleText: View { struct CircleText_Previews: PreviewProvider { static var previews: some View { - - HStack { - VStack { - + VStack { + HStack { CircleText(text: "N1", color: Color.yellow, circleSize: 80) .previewLayout(.fixed(width: 300, height: 100)) CircleText(text: "8", color: Color.purple, circleSize: 80) @@ -41,17 +39,20 @@ struct CircleText_Previews: PreviewProvider { .previewLayout(.fixed(width: 300, height: 100)) CircleText(text: "🍔", color: Color.brown, circleSize: 80) .previewLayout(.fixed(width: 300, height: 100)) + } + HStack { CircleText(text: "👻", color: Color.orange, circleSize: 80) .previewLayout(.fixed(width: 300, height: 100)) CircleText(text: "🤙", color: Color.orange, circleSize: 80) .previewLayout(.fixed(width: 300, height: 100)) - - } - VStack { CircleText(text: "69", color: Color.green, circleSize: 80) .previewLayout(.fixed(width: 300, height: 100)) CircleText(text: "WWWW", color: Color.cyan, circleSize: 80) .previewLayout(.fixed(width: 300, height: 100)) + } + HStack { + + CircleText(text: "CW-A", color: Color.secondary) .previewLayout(.fixed(width: 300, height: 100)) CircleText(text: "CW-A", color: Color.secondary, circleSize: 80) @@ -60,7 +61,20 @@ struct CircleText_Previews: PreviewProvider { .previewLayout(.fixed(width: 300, height: 100)) CircleText(text: "IIII", color: Color.accentColor, circleSize: 80) .previewLayout(.fixed(width: 300, height: 100)) - CircleText(text: "LCP", color: Color.primary, circleSize: 80) + } + HStack { + + CircleText(text: "🚗", color: Color.orange) + .previewLayout(.fixed(width: 300, height: 100)) + CircleText(text: "🔋", color: Color.indigo, circleSize: 80) + .previewLayout(.fixed(width: 300, height: 100)) + CircleText(text: "🛢️", color: Color.orange, circleSize: 80) + .previewLayout(.fixed(width: 300, height: 100)) + CircleText(text: "LCP", color: Color.indigo, circleSize: 80) + .previewLayout(.fixed(width: 300, height: 100)) + } + HStack { + CircleText(text: "🤡", color: Color.red, circleSize: 80) .previewLayout(.fixed(width: 300, height: 100)) } } diff --git a/Meshtastic/Views/Helpers/IndoorAirQuality.swift b/Meshtastic/Views/Helpers/IndoorAirQuality.swift deleted file mode 100644 index fa90d6f5..00000000 --- a/Meshtastic/Views/Helpers/IndoorAirQuality.swift +++ /dev/null @@ -1,125 +0,0 @@ -// -// IndoorAirQuality.swift -// Meshtastic -// -// Copyright(c) by Garth Vander Houwen on 4/10/24. -// - -import Foundation -import SwiftUI - -enum IaqDisplayMode: Int, CaseIterable, Identifiable { - - case pill = 0 - case dot = 1 - case text = 2 - case gauge = 3 - - var id: Int { self.rawValue } -} - -struct IndoorAirQuality: View { - var iaq: Int = 0 - var displayMode: IaqDisplayMode = .pill - let gradient = Gradient(colors: [.green, .mint, .yellow, .orange, .red, .purple, .purple, .brown, .brown, .brown, .brown]) - - var body: some View { - let iaqEnum = Iaq.getIaq(for: iaq) - switch displayMode { - case .pill: - ZStack (alignment: .leading) { - RoundedRectangle(cornerRadius: 10) - .fill(iaqEnum.color) - .frame(width: 125, height: 30) - Label("IAQ \(iaq)", systemImage: iaq < 100 ? "aqi.low" : ((iaq > 100 && iaq < 201) ? "aqi.medium" : "aqi.high")) - .padding(.leading, 4) - } - case .dot: - VStack { - HStack { - Text("\(iaq)") - Circle() - .fill(iaqEnum.color) - .frame(width: 10, height: 10) - } - } - case .text: - Text(iaqEnum.description) - .font(.caption) - case .gauge: - Gauge(value: Double(iaq), in: 0...500) { - - Text("IAQ") - .foregroundColor(iaqEnum.color) - } currentValueLabel: { - Text("\(Int(iaq))") - } - .tint(gradient) - .gaugeStyle(.accessoryCircular) - } - } -} - -struct IndoorAirQuality_Previews: PreviewProvider { - static var previews: some View { - VStack { - Text(".pill") - .font(.title) - HStack { - VStack{ - IndoorAirQuality(iaq: 6) - IndoorAirQuality(iaq: 51) - IndoorAirQuality(iaq: 101) - } - VStack { - IndoorAirQuality(iaq: 201) - IndoorAirQuality(iaq: 350) - IndoorAirQuality(iaq: 351) - } - } - Text(".dot") - .font(.title) - HStack { - VStack (alignment: .leading) { - IndoorAirQuality(iaq: 6, displayMode: .dot) - IndoorAirQuality(iaq: 51, displayMode: .dot) - IndoorAirQuality(iaq: 101, displayMode: .dot) - } - VStack (alignment: .leading) { - IndoorAirQuality(iaq: 201, displayMode: .dot) - IndoorAirQuality(iaq: 350, displayMode: .dot) - IndoorAirQuality(iaq: 351, displayMode: .dot) - } - } - Text(".text") - .font(.title) - IndoorAirQuality(iaq: 6, displayMode: .text) - IndoorAirQuality(iaq: 51, displayMode: .text) - IndoorAirQuality(iaq: 101, displayMode: .text) - IndoorAirQuality(iaq: 201, displayMode: .text) - IndoorAirQuality(iaq: 350, displayMode: .text) - IndoorAirQuality(iaq: 351, displayMode: .text) - Text(".gauge") - .font(.title) - HStack (alignment: .top) { - VStack{ - IndoorAirQuality(iaq: 6, displayMode: .gauge) - IndoorAirQuality(iaq: 51, displayMode: .gauge) - IndoorAirQuality(iaq: 101, displayMode: .gauge) - IndoorAirQuality(iaq: 151, displayMode: .gauge) - } - VStack{ - IndoorAirQuality(iaq: 201, displayMode: .gauge) - IndoorAirQuality(iaq: 251, displayMode: .gauge) - IndoorAirQuality(iaq: 301, displayMode: .gauge) - IndoorAirQuality(iaq: 350, displayMode: .gauge) - } - VStack{ - IndoorAirQuality(iaq: 351, displayMode: .gauge) - IndoorAirQuality(iaq: 401, displayMode: .gauge) - IndoorAirQuality(iaq: 500, displayMode: .gauge) - } - } - }.previewLayout(.fixed(width: 300, height: 800)) - } -} diff --git a/Meshtastic/Views/Helpers/Weather/IAQScale.swift b/Meshtastic/Views/Helpers/Weather/IAQScale.swift new file mode 100644 index 00000000..7fa50902 --- /dev/null +++ b/Meshtastic/Views/Helpers/Weather/IAQScale.swift @@ -0,0 +1,18 @@ +// +// IAQScale.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 4/24/24. +// + +import SwiftUI + +struct IAQScale: View { + + let gradient = Gradient(colors: [.green, .mint, .yellow, .orange, .red, .purple, .purple, .brown, .brown, .brown, .brown]) + var body: some View { + ZStack (alignment: .leading) { + + } + } +} diff --git a/Meshtastic/Views/Helpers/Weather/IndoorAirQuality.swift b/Meshtastic/Views/Helpers/Weather/IndoorAirQuality.swift new file mode 100644 index 00000000..5e12aa9a --- /dev/null +++ b/Meshtastic/Views/Helpers/Weather/IndoorAirQuality.swift @@ -0,0 +1,148 @@ +// +// IndoorAirQuality.swift +// Meshtastic +// +// Copyright(c) by Garth Vander Houwen on 4/10/24. +// + +import Foundation +import SwiftUI + +enum IaqDisplayMode: Int, CaseIterable, Identifiable { + + case pill = 0 + case dot = 1 + case text = 2 + case gauge = 3 + case gradient = 4 + + var id: Int { self.rawValue } +} + +struct IndoorAirQuality: View { + var iaq: Int = 0 + var displayMode: IaqDisplayMode = .pill + let gradient = Gradient(colors: [.green, .mint, .yellow, .orange, .red, .purple, .purple, .brown, .brown, .brown, .brown]) + + var body: some View { + let iaqEnum = Iaq.getIaq(for: iaq) + switch displayMode { + case .pill: + ZStack (alignment: .leading) { + RoundedRectangle(cornerRadius: 10) + .fill(iaqEnum.color) + .frame(width: 125, height: 30) + Label("IAQ \(iaq)", systemImage: iaq < 100 ? "aqi.low" : ((iaq > 100 && iaq < 201) ? "aqi.medium" : "aqi.high")) + .padding(.leading, 4) + } + case .dot: + VStack { + HStack { + Text("\(iaq)") + Circle() + .fill(iaqEnum.color) + .frame(width: 10, height: 10) + } + } + case .text: + Text(iaqEnum.description) + .font(.caption) + case .gauge: + Gauge(value: Double(iaq), in: 0...500) { + + Text("IAQ") + .foregroundColor(iaqEnum.color) + } currentValueLabel: { + Text("\(Int(iaq))") + } + .tint(gradient) + .gaugeStyle(.accessoryCircular) + case .gradient: + HStack { + Gauge(value: Double(iaq), in: 0...500) { + Text("IAQ") + .foregroundColor(iaqEnum.color) + } currentValueLabel: { + Text("IAQ ")+Text("\(Int(iaq))") + .foregroundColor(.gray) + } + .tint(gradient) + .gaugeStyle(.accessoryLinear) + Text(iaqEnum.description) + .font(.caption) + } + .padding([.leading, .trailing]) + } + } +} + +struct IndoorAirQuality_Previews: PreviewProvider { + static var previews: some View { + VStack { + Text(".pill") + .font(.title2) + HStack { + IndoorAirQuality(iaq: 6) + IndoorAirQuality(iaq: 51) + } + HStack { + IndoorAirQuality(iaq: 101) + IndoorAirQuality(iaq: 201) + } + HStack { + IndoorAirQuality(iaq: 350) + IndoorAirQuality(iaq: 351) + } + Text(".dot") + .font(.title2) + HStack { + IndoorAirQuality(iaq: 6, displayMode: .dot) + IndoorAirQuality(iaq: 51, displayMode: .dot) + IndoorAirQuality(iaq: 101, displayMode: .dot) + IndoorAirQuality(iaq: 201, displayMode: .dot) + IndoorAirQuality(iaq: 350, displayMode: .dot) + IndoorAirQuality(iaq: 351, displayMode: .dot) + } + Text(".text") + .font(.title2) + HStack { + IndoorAirQuality(iaq: 6, displayMode: .text) + IndoorAirQuality(iaq: 51, displayMode: .text) + IndoorAirQuality(iaq: 101, displayMode: .text) + } + HStack { + IndoorAirQuality(iaq: 201, displayMode: .text) + IndoorAirQuality(iaq: 350, displayMode: .text) + IndoorAirQuality(iaq: 351, displayMode: .text) + } + Text(".gauge") + .font(.title2) + HStack (alignment: .top) { + IndoorAirQuality(iaq: 6, displayMode: .gauge) + IndoorAirQuality(iaq: 51, displayMode: .gauge) + IndoorAirQuality(iaq: 101, displayMode: .gauge) + IndoorAirQuality(iaq: 151, displayMode: .gauge) + } + HStack (alignment: .top) { + IndoorAirQuality(iaq: 201, displayMode: .gauge) + IndoorAirQuality(iaq: 251, displayMode: .gauge) + IndoorAirQuality(iaq: 301, displayMode: .gauge) + IndoorAirQuality(iaq: 351, displayMode: .gauge) + } + HStack (alignment: .top) { + IndoorAirQuality(iaq: 401, displayMode: .gauge) + IndoorAirQuality(iaq: 500, displayMode: .gauge) + } + Text(".gradient") + .font(.title2) + IndoorAirQuality(iaq: 6, displayMode: .gradient) + IndoorAirQuality(iaq: 51, displayMode: .gradient) + IndoorAirQuality(iaq: 101, displayMode: .gradient) + IndoorAirQuality(iaq: 201, displayMode: .gradient) + IndoorAirQuality(iaq: 351, displayMode: .gradient) + IndoorAirQuality(iaq: 401, displayMode: .gradient) + IndoorAirQuality(iaq: 500, displayMode: .gradient) + + }.previewLayout(.fixed(width: 300, height: 800)) + } +} diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index f12ba7db..60f02bd6 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -12,7 +12,7 @@ struct Firmware: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager var node: NodeInfoEntity? - @State var minimumVersion = "2.3.5" + @State var minimumVersion = "2.3.7" @State var version = "" @State private var currentDevice: DeviceHardware? @State private var latestStable: FirmwareRelease? From 52dd086e09b7b7e6d1e5e693e90e6db38fe98501 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 26 Apr 2024 15:02:49 -0700 Subject: [PATCH 02/11] IAQ legend, basic AQI options. --- Meshtastic.xcodeproj/project.pbxproj | 8 +- Meshtastic/Enums/TelemetryEnums.swift | 67 +++++++- Meshtastic/Extensions/Color.swift | 3 + .../Helpers/Weather/AirQualityIndex.swift | 146 ++++++++++++++++++ .../Weather/AirQualityIndexCompact.swift | 67 -------- .../Views/Helpers/Weather/IAQScale.swift | 26 +++- 6 files changed, 242 insertions(+), 75 deletions(-) create mode 100644 Meshtastic/Views/Helpers/Weather/AirQualityIndex.swift delete mode 100644 Meshtastic/Views/Helpers/Weather/AirQualityIndexCompact.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 50002bf1..e54c834f 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -88,7 +88,7 @@ DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51FF298EE33B00D21B61 /* remote_hardware.pb.swift */; }; DD5E5212298EE33B00D21B61 /* apponly.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E5200298EE33B00D21B61 /* apponly.pb.swift */; }; DD5E5213298EE33B00D21B61 /* deviceonly.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E5201298EE33B00D21B61 /* deviceonly.pb.swift */; }; - DD5E523F298F5A9E00D21B61 /* AirQualityIndexCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E523E298F5A9E00D21B61 /* AirQualityIndexCompact.swift */; }; + DD5E523F298F5A9E00D21B61 /* AirQualityIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E523E298F5A9E00D21B61 /* AirQualityIndex.swift */; }; DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */; }; DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */; }; DD6193792863875F00E59241 /* SerialConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193782863875F00E59241 /* SerialConfig.swift */; }; @@ -341,7 +341,7 @@ DD5E51FF298EE33B00D21B61 /* remote_hardware.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = remote_hardware.pb.swift; sourceTree = ""; }; DD5E5200298EE33B00D21B61 /* apponly.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = apponly.pb.swift; sourceTree = ""; }; DD5E5201298EE33B00D21B61 /* deviceonly.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = deviceonly.pb.swift; sourceTree = ""; }; - DD5E523E298F5A9E00D21B61 /* AirQualityIndexCompact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirQualityIndexCompact.swift; sourceTree = ""; }; + DD5E523E298F5A9E00D21B61 /* AirQualityIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirQualityIndex.swift; sourceTree = ""; }; DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalNotificationConfig.swift; sourceTree = ""; }; DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = ""; }; DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = ""; }; @@ -651,7 +651,7 @@ DD5E523D298F5A7D00D21B61 /* Weather */ = { isa = PBXGroup; children = ( - DD5E523E298F5A9E00D21B61 /* AirQualityIndexCompact.swift */, + DD5E523E298F5A9E00D21B61 /* AirQualityIndex.swift */, DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */, DDA9515D2BC6F56F00CEA535 /* IndoorAirQuality.swift */, DD41A61429AB0035003C5A37 /* NodeWeatherForecast.swift */, @@ -1207,7 +1207,7 @@ DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */, DD0E20FD2B87090400F2D100 /* clientonly.pb.swift in Sources */, D93069082B81DF040066FBC8 /* SaveConfigButton.swift in Sources */, - DD5E523F298F5A9E00D21B61 /* AirQualityIndexCompact.swift in Sources */, + DD5E523F298F5A9E00D21B61 /* AirQualityIndex.swift in Sources */, DD964FBF296E76EF007C176F /* WaypointFormMapKit.swift in Sources */, DD3501892852FC3B000FC853 /* Settings.swift in Sources */, DDDC22382BA92344002C44F1 /* MeshMapContent.swift in Sources */, diff --git a/Meshtastic/Enums/TelemetryEnums.swift b/Meshtastic/Enums/TelemetryEnums.swift index 1189660c..7aa548ff 100644 --- a/Meshtastic/Enums/TelemetryEnums.swift +++ b/Meshtastic/Enums/TelemetryEnums.swift @@ -8,6 +8,69 @@ import Foundation import SwiftUI +enum Aqi: Int, CaseIterable, Identifiable { + case good = 0 + case moderate = 1 + case sensitive = 2 + case unhealthy = 3 + case veryUnhealthy = 4 + case hazardous = 5 + + var id: Int { self.rawValue } + var description: String { + switch self { + case .good: + return "Good" + case .moderate: + return "Moderate" + case .sensitive: + return "Unhealthy for Sensitive Groups" + case .unhealthy: + return "Unhealthy" + case .veryUnhealthy: + return "Very Unhealthy" + case .hazardous: + return "Hazardous" + } + } + var color: Color { + switch self { + case .good: + return .green + case .moderate: + return .yellow + case .sensitive: + return .orange + case .unhealthy: + return .red + case .veryUnhealthy: + return .purple + case .hazardous: + return .magenta + } + } + static func getAqi(for value: Int) -> Aqi { + let aqi: Aqi + switch value { + case 0...50: + aqi = .good + case 51...100: + aqi = .moderate + case 101...150: + aqi = .sensitive + case 151...200: + aqi = .unhealthy + case 201...300: + aqi = .veryUnhealthy + case 301...500: + aqi = .hazardous + default: + fatalError("Invalid int value") + } + return aqi + } +} + enum Iaq: Int, CaseIterable, Identifiable { case excellent = 0 case good = 1 @@ -27,7 +90,7 @@ enum Iaq: Int, CaseIterable, Identifiable { case .lightlyPolluted: return "Lightly Polluted" case .moderatelyPolluted: - return "Lightly Polluted" + return "Moderately Polluted" case .heavilyPolluted: return "Heavily Polluted" case .severelyPolluted: @@ -49,7 +112,7 @@ enum Iaq: Int, CaseIterable, Identifiable { case .heavilyPolluted: return .red case .severelyPolluted: - return .purple + return .magenta case .extremelyPolluted: return .brown } diff --git a/Meshtastic/Extensions/Color.swift b/Meshtastic/Extensions/Color.swift index da47cadc..428e5707 100644 --- a/Meshtastic/Extensions/Color.swift +++ b/Meshtastic/Extensions/Color.swift @@ -17,6 +17,9 @@ extension Color { let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000 return (brightness > 0.5) } + public static var magenta: Color { + return Color(UIColor(red: 0.50, green: 0.00, blue: 0.00, alpha: 1.00)) //return Color(UIColor.magenta) + } } extension UIColor { diff --git a/Meshtastic/Views/Helpers/Weather/AirQualityIndex.swift b/Meshtastic/Views/Helpers/Weather/AirQualityIndex.swift new file mode 100644 index 00000000..160caae5 --- /dev/null +++ b/Meshtastic/Views/Helpers/Weather/AirQualityIndex.swift @@ -0,0 +1,146 @@ +// +// AQICircleDisplay.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 2/4/23. +// +import SwiftUI + +enum AqiDisplayMode: Int, CaseIterable, Identifiable { + + case pill = 0 + case dot = 1 + case text = 2 + case gauge = 3 + case gradient = 4 + + var id: Int { self.rawValue } +} + +struct AirQualityIndex: View { + var aqi: Int + var displayMode: IaqDisplayMode = .pill + let gradient = Gradient(colors: [.green, .yellow, .orange, .red, .purple, .magenta]) + + var body: some View { + + let aqiEnum = Aqi.getAqi(for: aqi) + switch displayMode { + case .pill: + ZStack (alignment: .leading) { + RoundedRectangle(cornerRadius: 10) + .fill(aqiEnum.color) + .frame(width: 125, height: 30) + Label("IAQ \(aqi)", systemImage: aqi < 100 ? "aqi.low" : ((aqi > 100 && aqi < 201) ? "aqi.medium" : "aqi.high")) + .padding(.leading, 4) + } + case .dot: + VStack { + HStack { + Text("\(aqi)") + Circle() + .fill(aqiEnum.color) + .frame(width: 10, height: 10) + } + } + case .text: + Text(aqiEnum.description) + .font(.caption) + case .gauge: + Gauge(value: Double(aqi), in: 0...500) { + + Text("IAQ") + .foregroundColor(aqiEnum.color) + } currentValueLabel: { + Text("\(Int(aqi))") + } + .tint(gradient) + .gaugeStyle(.accessoryCircular) + case .gradient: + HStack { + Gauge(value: Double(aqi), in: 0...500) { + Text("IAQ") + .foregroundColor(aqiEnum.color) + } currentValueLabel: { + Text("IAQ ")+Text("\(Int(aqi))") + .foregroundColor(.gray) + } + .tint(gradient) + .gaugeStyle(.accessoryLinear) + Text(aqiEnum.description) + .font(.caption) + } + .padding([.leading, .trailing]) + } + } +} + +struct AirQualityIndex_Previews: PreviewProvider { + static var previews: some View { + VStack { + Text(".pill") + .font(.title2) + HStack { + AirQualityIndex(aqi: 6) + AirQualityIndex(aqi: 51) + } + HStack { + AirQualityIndex(aqi: 101) + AirQualityIndex(aqi: 151) + } + HStack { + AirQualityIndex(aqi: 201) + AirQualityIndex(aqi: 351) + } + Text(".dot") + .font(.title2) + HStack { + AirQualityIndex(aqi: 6, displayMode: .dot) + AirQualityIndex(aqi: 51, displayMode: .dot) + AirQualityIndex(aqi: 101, displayMode: .dot) + AirQualityIndex(aqi: 201, displayMode: .dot) + AirQualityIndex(aqi: 350, displayMode: .dot) + AirQualityIndex(aqi: 351, displayMode: .dot) + } + Text(".text") + .font(.title2) + HStack { + AirQualityIndex(aqi: 6, displayMode: .text) + AirQualityIndex(aqi: 51, displayMode: .text) + AirQualityIndex(aqi: 101, displayMode: .text) + } + HStack { + AirQualityIndex(aqi: 201, displayMode: .text) + AirQualityIndex(aqi: 350, displayMode: .text) + } + Text(".gauge") + .font(.title2) + HStack (alignment: .top) { + AirQualityIndex(aqi: 6, displayMode: .gauge) + AirQualityIndex(aqi: 51, displayMode: .gauge) + AirQualityIndex(aqi: 101, displayMode: .gauge) + AirQualityIndex(aqi: 151, displayMode: .gauge) + } + HStack (alignment: .top) { + AirQualityIndex(aqi: 201, displayMode: .gauge) + AirQualityIndex(aqi: 251, displayMode: .gauge) + AirQualityIndex(aqi: 301, displayMode: .gauge) + AirQualityIndex(aqi: 351, displayMode: .gauge) + } + HStack (alignment: .top) { + AirQualityIndex(aqi: 401, displayMode: .gauge) + AirQualityIndex(aqi: 500, displayMode: .gauge) + } + Text(".gradient") + .font(.title2) + AirQualityIndex(aqi: 6, displayMode: .gradient) + AirQualityIndex(aqi: 51, displayMode: .gradient) + AirQualityIndex(aqi: 101, displayMode: .gradient) + AirQualityIndex(aqi: 201, displayMode: .gradient) + AirQualityIndex(aqi: 351, displayMode: .gradient) + AirQualityIndex(aqi: 401, displayMode: .gradient) + AirQualityIndex(aqi: 500, displayMode: .gradient) + + }.previewLayout(.fixed(width: 300, height: 800)) + } +} diff --git a/Meshtastic/Views/Helpers/Weather/AirQualityIndexCompact.swift b/Meshtastic/Views/Helpers/Weather/AirQualityIndexCompact.swift deleted file mode 100644 index 642a0596..00000000 --- a/Meshtastic/Views/Helpers/Weather/AirQualityIndexCompact.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// AQICircleDisplay.swift -// Meshtastic -// -// Copyright(c) Garth Vander Houwen 2/4/23. -// -import SwiftUI - -struct AirQualityIndexCompact: View { - var aqi: Int - - var body: some View { - - HStack(spacing: 0.5) { - Text("AQI \(aqi)") - .foregroundColor(.gray) - .padding(.trailing, 0) - .font(.caption) - - if aqi > 0 && aqi < 51 { - // Good - Circle() - .fill(.green) - .frame(width: 10, height: 10) - } else if aqi > 50 && aqi < 101 { - // Satisfactory - Circle() - .fill(Color(red: 0, green: 0.9882, blue: 0.1804)) - .frame(width: 10, height: 10) - } else if aqi > 100 && aqi < 201 { - // Moderate - Circle() - .fill(.yellow) - .frame(width: 10, height: 10) - } else if aqi > 200 && aqi < 301 { - // Poor - Circle() - .fill(.orange) - .frame(width: 10, height: 10) - - } else if aqi > 300 && aqi < 401 { - // Very Poor - Circle() - .fill(.red) - .frame(width: 10, height: 10) - } else if aqi >= 401 { - // Very Poor - Circle() - .fill(Color(red: 0.8392, green: 0.0667, blue: 0)) - .frame(width: 10, height: 10) - } - } - } -} -struct AQICircleDisplay_Previews: PreviewProvider { - static var previews: some View { - - VStack { - AirQualityIndexCompact(aqi: 5) - AirQualityIndexCompact(aqi: 51) - AirQualityIndexCompact(aqi: 101) - AirQualityIndexCompact(aqi: 201) - AirQualityIndexCompact(aqi: 301) - AirQualityIndexCompact(aqi: 401) - } - } -} diff --git a/Meshtastic/Views/Helpers/Weather/IAQScale.swift b/Meshtastic/Views/Helpers/Weather/IAQScale.swift index 7fa50902..97ec91e6 100644 --- a/Meshtastic/Views/Helpers/Weather/IAQScale.swift +++ b/Meshtastic/Views/Helpers/Weather/IAQScale.swift @@ -9,10 +9,32 @@ import SwiftUI struct IAQScale: View { - let gradient = Gradient(colors: [.green, .mint, .yellow, .orange, .red, .purple, .purple, .brown, .brown, .brown, .brown]) var body: some View { - ZStack (alignment: .leading) { + VStack(alignment:.leading) { + ForEach(Iaq.allCases) { iaq in + HStack { + RoundedRectangle(cornerRadius: 5) + .fill(iaq.color) + .frame(width: 30, height: 20) + Text(iaq.description) + .font(.callout) + } + } + } + .padding() + .background(.white) + .cornerRadius(20) /// make the background rounded + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(.secondary, lineWidth: 4) + ) + } +} +struct IAQSCalePreviews: PreviewProvider { + static var previews: some View { + VStack { + IAQScale() } } } From 940026b1eb2f423a4c3f5c24444f5710dae4e85c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 26 Apr 2024 15:35:08 -0700 Subject: [PATCH 03/11] Show long name and userid in group messages, above the message like imessage. --- Meshtastic/Views/Messages/ChannelMessageList.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 8688eed4..add3dc84 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -43,17 +43,24 @@ struct ChannelMessageList: View { .padding(.trailing) } } - HStack(alignment: .top) { + HStack(alignment: .bottom) { if currentUser { Spacer(minLength: 50) } if !currentUser { CircleText(text: message.fromUser?.shortName ?? "?", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 44) .padding(.all, 5) - .offset(y: -5) + .offset(y: -10) } + VStack(alignment: currentUser ? .trailing : .leading) { let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue) + if !currentUser && message.fromUser != nil { + Text("\(message.fromUser?.longName ?? "unknown".localized ) (!\(message.fromUser?.userId ?? "?"))") + .font(.caption) + .foregroundColor(.gray) + } HStack { + MessageText( message: message, tapBackDestination: .channel(channel), From a17c90a65763f0d18c7c7ac5f7d0f0a7c7a2418c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 26 Apr 2024 15:49:38 -0700 Subject: [PATCH 04/11] Remove the bang --- Meshtastic/Views/Messages/ChannelMessageList.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index add3dc84..8705e7d1 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -55,7 +55,7 @@ struct ChannelMessageList: View { let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue) if !currentUser && message.fromUser != nil { - Text("\(message.fromUser?.longName ?? "unknown".localized ) (!\(message.fromUser?.userId ?? "?"))") + Text("\(message.fromUser?.longName ?? "unknown".localized ) (\(message.fromUser?.userId ?? "?"))") .font(.caption) .foregroundColor(.gray) } From 5869a2736927d499b5d1f0f949f140487e1ee9d1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 26 Apr 2024 18:06:23 -0700 Subject: [PATCH 05/11] Mute all channel message notifications --- Meshtastic.xcodeproj/project.pbxproj | 4 +- Meshtastic/Enums/TelemetryEnums.swift | 36 ++ Meshtastic/Extensions/Color.swift | 8 +- Meshtastic/Extensions/UserDefaults.swift | 4 + Meshtastic/Helpers/MeshPackets.swift | 2 +- .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 461 ++++++++++++++++++ Meshtastic/Persistence/UpdateCoreData.swift | 2 + .../Protobufs/meshtastic/admin.pb.swift | 4 +- .../Protobufs/meshtastic/apponly.pb.swift | 10 +- Meshtastic/Protobufs/meshtastic/atak.pb.swift | 4 +- .../Protobufs/meshtastic/channel.pb.swift | 2 +- .../Protobufs/meshtastic/config.pb.swift | 63 +-- .../Protobufs/meshtastic/deviceonly.pb.swift | 22 +- .../Protobufs/meshtastic/localonly.pb.swift | 20 +- Meshtastic/Protobufs/meshtastic/mesh.pb.swift | 75 ++- .../meshtastic/module_config.pb.swift | 10 +- .../Protobufs/meshtastic/portnums.pb.swift | 2 +- .../meshtastic/remote_hardware.pb.swift | 2 +- .../meshtastic/storeforward.pb.swift | 2 +- .../Protobufs/meshtastic/telemetry.pb.swift | 20 +- .../Protobufs/meshtastic/xmodem.pb.swift | 2 +- .../Views/Helpers/Weather/IAQScale.swift | 11 +- .../Views/Messages/ChannelMessageList.swift | 3 +- .../Views/Settings/Config/DeviceConfig.swift | 18 +- .../Settings/Config/Module/MQTTConfig.swift | 7 +- Settings.bundle/Root.plist | 14 +- protobufs | 2 +- 28 files changed, 654 insertions(+), 158 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 36.xcdatamodel/contents diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index e54c834f..c1c78c19 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -293,6 +293,7 @@ DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV22.xcdatamodel; sourceTree = ""; }; DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewSwiftUI.swift; sourceTree = ""; }; DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV19.xcdatamodel; sourceTree = ""; }; + DD31B04D2BDC6FD30024FA63 /* MeshtasticDataModelV 36.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 36.xcdatamodel"; sourceTree = ""; }; DD31EC492B7F18B7006A3995 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = ""; }; DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 23.xcdatamodel"; sourceTree = ""; }; DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareApi.swift; sourceTree = ""; }; @@ -1824,6 +1825,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD31B04D2BDC6FD30024FA63 /* MeshtasticDataModelV 36.xcdatamodel */, DD268D8C2BCC7D11008073AE /* MeshtasticDataModelV 35.xcdatamodel */, DDDBC87C2BC65682001E8DF7 /* MeshtasticDataModelV 34.xcdatamodel */, DDF45C382BC46B16005ED5F2 /* MeshtasticDataModelV33.xcdatamodel */, @@ -1860,7 +1862,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD268D8C2BCC7D11008073AE /* MeshtasticDataModelV 35.xcdatamodel */; + currentVersion = DD31B04D2BDC6FD30024FA63 /* MeshtasticDataModelV 36.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Enums/TelemetryEnums.swift b/Meshtastic/Enums/TelemetryEnums.swift index 7aa548ff..1e9f54fd 100644 --- a/Meshtastic/Enums/TelemetryEnums.swift +++ b/Meshtastic/Enums/TelemetryEnums.swift @@ -49,6 +49,23 @@ enum Aqi: Int, CaseIterable, Identifiable { return .magenta } } + var range: Range { + switch self { + case .good: + return Range(0...50) + case .moderate: + return Range(51...100) + case .sensitive: + return Range(101...150) + case .unhealthy: + return Range(151...200) + case .veryUnhealthy: + return Range(201...300) + case .hazardous: + return Range(301...500) + } + } + static func getAqi(for value: Int) -> Aqi { let aqi: Aqi switch value { @@ -117,6 +134,25 @@ enum Iaq: Int, CaseIterable, Identifiable { return .brown } } + + var range: Range { + switch self { + case .excellent: + return Range(0...50) + case .good: + return Range(51...100) + case .lightlyPolluted: + return Range(101...150) + case .moderatelyPolluted: + return Range(151...200) + case .heavilyPolluted: + return Range(201...250) + case .severelyPolluted: + return Range(251...350) + case .extremelyPolluted: + return Range(351...500) + } + } static func getIaq(for value: Int) -> Iaq { let iaq: Iaq switch value { diff --git a/Meshtastic/Extensions/Color.swift b/Meshtastic/Extensions/Color.swift index 428e5707..b51fd839 100644 --- a/Meshtastic/Extensions/Color.swift +++ b/Meshtastic/Extensions/Color.swift @@ -17,9 +17,11 @@ extension Color { let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000 return (brightness > 0.5) } - public static var magenta: Color { - return Color(UIColor(red: 0.50, green: 0.00, blue: 0.00, alpha: 1.00)) //return Color(UIColor.magenta) - } + public static let magenta = Color(red: 0.50, green: 0.00, blue: 0.00) +// public static var magenta: Color { +// return Color(red: 0.50, green: 0.00, blue: 0.00) +// //return Color(UIColor(red: 0.50, green: 0.00, blue: 0.00, alpha: 1.00)) //return Color(UIColor.magenta) +// } } extension UIColor { diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 49757d69..d4b7c098 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -68,6 +68,7 @@ extension UserDefaults { case enableSmartPosition case newNodeNotifications case lowBatteryNotifications + case channelMessageNotifications case modemPreset case firmwareVersion case testIntEnum @@ -146,6 +147,9 @@ extension UserDefaults { @UserDefault(.enableSmartPosition, defaultValue: false) static var enableSmartPosition: Bool + @UserDefault(.channelMessageNotifications, defaultValue: true) + static var channelMessageNotifications: Bool + @UserDefault(.newNodeNotifications, defaultValue: true) static var newNodeNotifications: Bool diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 3f6aa1ee..892c81f5 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -878,7 +878,7 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec if channel.index == newMessage.channel { context.refresh(channel, mergeChanges: true) } - if channel.index == newMessage.channel && !channel.mute { + if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications { // Create an iOS Notification for the received private channel message and schedule it immediately let manager = LocalNotificationManager() manager.notifications = [ diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index f837dfda..9ac5587b 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 35.xcdatamodel + MeshtasticDataModelV 36.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 36.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 36.xcdatamodel/contents new file mode 100644 index 00000000..f7144b48 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 36.xcdatamodel/contents @@ -0,0 +1,461 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 90a35cce..ee358176 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -452,6 +452,7 @@ func upsertDeviceConfigPacket(config: Meshtastic.Config.DeviceConfig, nodeNum: I newDeviceConfig.rebroadcastMode = Int32(config.rebroadcastMode.rawValue) newDeviceConfig.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber) newDeviceConfig.doubleTapAsButtonPress = config.doubleTapAsButtonPress + newDeviceConfig.ledHeartbeatEnabled = !config.ledHeartbeatDisabled newDeviceConfig.isManaged = config.isManaged newDeviceConfig.tzdef = config.tzdef fetchedNode[0].deviceConfig = newDeviceConfig @@ -464,6 +465,7 @@ func upsertDeviceConfigPacket(config: Meshtastic.Config.DeviceConfig, nodeNum: I fetchedNode[0].deviceConfig?.rebroadcastMode = Int32(config.rebroadcastMode.rawValue) fetchedNode[0].deviceConfig?.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber) fetchedNode[0].deviceConfig?.doubleTapAsButtonPress = config.doubleTapAsButtonPress + fetchedNode[0].deviceConfig?.ledHeartbeatEnabled = !config.ledHeartbeatDisabled fetchedNode[0].deviceConfig?.isManaged = config.isManaged fetchedNode[0].deviceConfig?.tzdef = config.tzdef } diff --git a/Meshtastic/Protobufs/meshtastic/admin.pb.swift b/Meshtastic/Protobufs/meshtastic/admin.pb.swift index 230f5304..1f2b193e 100644 --- a/Meshtastic/Protobufs/meshtastic/admin.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/admin.pb.swift @@ -924,7 +924,7 @@ struct AdminMessage { extension AdminMessage.ConfigType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [AdminMessage.ConfigType] = [ + static var allCases: [AdminMessage.ConfigType] = [ .deviceConfig, .positionConfig, .powerConfig, @@ -937,7 +937,7 @@ extension AdminMessage.ConfigType: CaseIterable { extension AdminMessage.ModuleConfigType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [AdminMessage.ModuleConfigType] = [ + static var allCases: [AdminMessage.ModuleConfigType] = [ .mqttConfig, .serialConfig, .extnotifConfig, diff --git a/Meshtastic/Protobufs/meshtastic/apponly.pb.swift b/Meshtastic/Protobufs/meshtastic/apponly.pb.swift index 11abf7af..ffce4849 100644 --- a/Meshtastic/Protobufs/meshtastic/apponly.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/apponly.pb.swift @@ -75,15 +75,7 @@ extension ChannelSet: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio var _settings: [ChannelSettings] = [] var _loraConfig: Config.LoRaConfig? = nil - #if swift(>=5.10) - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif + static let defaultInstance = _StorageClass() private init() {} diff --git a/Meshtastic/Protobufs/meshtastic/atak.pb.swift b/Meshtastic/Protobufs/meshtastic/atak.pb.swift index 77c35e7c..f1bc14ad 100644 --- a/Meshtastic/Protobufs/meshtastic/atak.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/atak.pb.swift @@ -136,7 +136,7 @@ enum Team: SwiftProtobuf.Enum { extension Team: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Team] = [ + static var allCases: [Team] = [ .unspecifedColor, .white, .yellow, @@ -239,7 +239,7 @@ enum MemberRole: SwiftProtobuf.Enum { extension MemberRole: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [MemberRole] = [ + static var allCases: [MemberRole] = [ .unspecifed, .teamMember, .teamLead, diff --git a/Meshtastic/Protobufs/meshtastic/channel.pb.swift b/Meshtastic/Protobufs/meshtastic/channel.pb.swift index b2c55540..493230ba 100644 --- a/Meshtastic/Protobufs/meshtastic/channel.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/channel.pb.swift @@ -215,7 +215,7 @@ struct Channel { extension Channel.Role: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Channel.Role] = [ + static var allCases: [Channel.Role] = [ .disabled, .primary, .secondary, diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index db2b7f04..cf1077e5 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -194,6 +194,10 @@ struct Config { /// POSIX Timezone definition string from https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv. var tzdef: String = String() + /// + /// If true, disable the default blinking LED (LED_PIN) behavior on the device + var ledHeartbeatDisabled: Bool = false + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -584,27 +588,25 @@ struct Config { // methods supported on all messages. /// - /// If set, we are powered from a low-current source (i.e. solar), so even if it looks like we have power flowing in - /// we should try to minimize power consumption as much as possible. - /// YOU DO NOT NEED TO SET THIS IF YOU'VE set is_router (it is implied in that case). - /// Advanced Option + /// Description: Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. + /// Don't use this setting if you want to use your device with the phone apps or are using a device without a user button. + /// Technical Details: Works for ESP32 devices and NRF52 devices in the Sensor or Tracker roles var isPowerSaving: Bool = false /// - /// If non-zero, the device will fully power off this many seconds after external power is removed. + /// Description: If non-zero, the device will fully power off this many seconds after external power is removed. var onBatteryShutdownAfterSecs: UInt32 = 0 /// /// Ratio of voltage divider for battery pin eg. 3.20 (R1=100k, R2=220k) /// Overrides the ADC_MULTIPLIER defined in variant for battery voltage calculation. - /// Should be set to floating point value between 2 and 4 - /// Fixes issues on Heltec v2 + /// https://meshtastic.org/docs/configuration/radio/power/#adc-multiplier-override + /// Should be set to floating point value between 2 and 6 var adcMultiplierOverride: Float = 0 /// - /// Wait Bluetooth Seconds - /// The number of seconds for to wait before turning off BLE in No Bluetooth states - /// 0 for default of 1 minute + /// Description: The number of seconds for to wait before turning off BLE in No Bluetooth states + /// Technical Details: ESP32 Only 0 for default of 1 minute var waitBluetoothSecs: UInt32 = 0 /// @@ -615,16 +617,13 @@ struct Config { var sdsSecs: UInt32 = 0 /// - /// Light Sleep Seconds - /// In light sleep the CPU is suspended, LoRa radio is on, BLE is off an GPS is on - /// ESP32 Only - /// 0 for default of 300 + /// Description: In light sleep the CPU is suspended, LoRa radio is on, BLE is off an GPS is on + /// Technical Details: ESP32 Only 0 for default of 300 var lsSecs: UInt32 = 0 /// - /// Minimum Wake Seconds - /// While in light sleep when we receive packets on the LoRa radio we will wake and handle them and stay awake in no BLE mode for this value - /// 0 for default of 10 seconds + /// Description: While in light sleep when we receive packets on the LoRa radio we will wake and handle them and stay awake in no BLE mode for this value + /// Technical Details: ESP32 Only 0 for default of 10 seconds var minWakeSecs: UInt32 = 0 /// @@ -1387,7 +1386,7 @@ struct Config { extension Config.DeviceConfig.Role: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Config.DeviceConfig.Role] = [ + static var allCases: [Config.DeviceConfig.Role] = [ .client, .clientMute, .router, @@ -1404,7 +1403,7 @@ extension Config.DeviceConfig.Role: CaseIterable { extension Config.DeviceConfig.RebroadcastMode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Config.DeviceConfig.RebroadcastMode] = [ + static var allCases: [Config.DeviceConfig.RebroadcastMode] = [ .all, .allSkipDecoding, .localOnly, @@ -1414,7 +1413,7 @@ extension Config.DeviceConfig.RebroadcastMode: CaseIterable { extension Config.PositionConfig.PositionFlags: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Config.PositionConfig.PositionFlags] = [ + static var allCases: [Config.PositionConfig.PositionFlags] = [ .unset, .altitude, .altitudeMsl, @@ -1431,7 +1430,7 @@ extension Config.PositionConfig.PositionFlags: CaseIterable { extension Config.PositionConfig.GpsMode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Config.PositionConfig.GpsMode] = [ + static var allCases: [Config.PositionConfig.GpsMode] = [ .disabled, .enabled, .notPresent, @@ -1440,7 +1439,7 @@ extension Config.PositionConfig.GpsMode: CaseIterable { extension Config.NetworkConfig.AddressMode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Config.NetworkConfig.AddressMode] = [ + static var allCases: [Config.NetworkConfig.AddressMode] = [ .dhcp, .static, ] @@ -1448,7 +1447,7 @@ extension Config.NetworkConfig.AddressMode: CaseIterable { extension Config.DisplayConfig.GpsCoordinateFormat: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [ + static var allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [ .dec, .dms, .utm, @@ -1460,7 +1459,7 @@ extension Config.DisplayConfig.GpsCoordinateFormat: CaseIterable { extension Config.DisplayConfig.DisplayUnits: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Config.DisplayConfig.DisplayUnits] = [ + static var allCases: [Config.DisplayConfig.DisplayUnits] = [ .metric, .imperial, ] @@ -1468,7 +1467,7 @@ extension Config.DisplayConfig.DisplayUnits: CaseIterable { extension Config.DisplayConfig.OledType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Config.DisplayConfig.OledType] = [ + static var allCases: [Config.DisplayConfig.OledType] = [ .oledAuto, .oledSsd1306, .oledSh1106, @@ -1478,7 +1477,7 @@ extension Config.DisplayConfig.OledType: CaseIterable { extension Config.DisplayConfig.DisplayMode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Config.DisplayConfig.DisplayMode] = [ + static var allCases: [Config.DisplayConfig.DisplayMode] = [ .default, .twocolor, .inverted, @@ -1488,7 +1487,7 @@ extension Config.DisplayConfig.DisplayMode: CaseIterable { extension Config.LoRaConfig.RegionCode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Config.LoRaConfig.RegionCode] = [ + static var allCases: [Config.LoRaConfig.RegionCode] = [ .unset, .us, .eu433, @@ -1513,7 +1512,7 @@ extension Config.LoRaConfig.RegionCode: CaseIterable { extension Config.LoRaConfig.ModemPreset: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Config.LoRaConfig.ModemPreset] = [ + static var allCases: [Config.LoRaConfig.ModemPreset] = [ .longFast, .longSlow, .veryLongSlow, @@ -1527,7 +1526,7 @@ extension Config.LoRaConfig.ModemPreset: CaseIterable { extension Config.BluetoothConfig.PairingMode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Config.BluetoothConfig.PairingMode] = [ + static var allCases: [Config.BluetoothConfig.PairingMode] = [ .randomPin, .fixedPin, .noPin, @@ -1739,6 +1738,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl 9: .standard(proto: "is_managed"), 10: .standard(proto: "disable_triple_click"), 11: .same(proto: "tzdef"), + 12: .standard(proto: "led_heartbeat_disabled"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1758,6 +1758,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl case 9: try { try decoder.decodeSingularBoolField(value: &self.isManaged) }() case 10: try { try decoder.decodeSingularBoolField(value: &self.disableTripleClick) }() case 11: try { try decoder.decodeSingularStringField(value: &self.tzdef) }() + case 12: try { try decoder.decodeSingularBoolField(value: &self.ledHeartbeatDisabled) }() default: break } } @@ -1797,6 +1798,9 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if !self.tzdef.isEmpty { try visitor.visitSingularStringField(value: self.tzdef, fieldNumber: 11) } + if self.ledHeartbeatDisabled != false { + try visitor.visitSingularBoolField(value: self.ledHeartbeatDisabled, fieldNumber: 12) + } try unknownFields.traverse(visitor: &visitor) } @@ -1812,6 +1816,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if lhs.isManaged != rhs.isManaged {return false} if lhs.disableTripleClick != rhs.disableTripleClick {return false} if lhs.tzdef != rhs.tzdef {return false} + if lhs.ledHeartbeatDisabled != rhs.ledHeartbeatDisabled {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift b/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift index ebd5e3f7..433bffd3 100644 --- a/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift @@ -66,7 +66,7 @@ enum ScreenFonts: SwiftProtobuf.Enum { extension ScreenFonts: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [ScreenFonts] = [ + static var allCases: [ScreenFonts] = [ .fontSmall, .fontMedium, .fontLarge, @@ -509,15 +509,7 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat var _hopsAway: UInt32 = 0 var _isFavorite: Bool = false - #if swift(>=5.10) - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif + static let defaultInstance = _StorageClass() private init() {} @@ -657,15 +649,7 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati var _nodeRemoteHardwarePins: [NodeRemoteHardwarePin] = [] var _nodeDbLite: [NodeInfoLite] = [] - #if swift(>=5.10) - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif + static let defaultInstance = _StorageClass() private init() {} diff --git a/Meshtastic/Protobufs/meshtastic/localonly.pb.swift b/Meshtastic/Protobufs/meshtastic/localonly.pb.swift index 0778e962..6e215220 100644 --- a/Meshtastic/Protobufs/meshtastic/localonly.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/localonly.pb.swift @@ -314,15 +314,7 @@ extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati var _bluetooth: Config.BluetoothConfig? = nil var _version: UInt32 = 0 - #if swift(>=5.10) - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif + static let defaultInstance = _StorageClass() private init() {} @@ -458,15 +450,7 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem var _paxcounter: ModuleConfig.PaxcounterConfig? = nil var _version: UInt32 = 0 - #if swift(>=5.10) - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif + static let defaultInstance = _StorageClass() private init() {} diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index c9c26377..83c930df 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -265,6 +265,15 @@ enum HardwareModel: SwiftProtobuf.Enum { /// Compatible with the TD-WRLS development board case tdLorac // = 60 + /// + /// CDEBYTE EoRa-S3 board using their own MM modules, clone of LILYGO T3S3 + case cdebyteEoraS3 // = 61 + + /// + /// TWC_MESH_V4 + /// Adafruit NRF52840 feather express with SX1262, SSD1306 OLED and NEO6M GPS + case twcMeshV4 // = 62 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. @@ -334,6 +343,8 @@ enum HardwareModel: SwiftProtobuf.Enum { case 58: self = .heltecWirelessTrackerV10 case 59: self = .unphone case 60: self = .tdLorac + case 61: self = .cdebyteEoraS3 + case 62: self = .twcMeshV4 case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -397,6 +408,8 @@ enum HardwareModel: SwiftProtobuf.Enum { case .heltecWirelessTrackerV10: return 58 case .unphone: return 59 case .tdLorac: return 60 + case .cdebyteEoraS3: return 61 + case .twcMeshV4: return 62 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -408,7 +421,7 @@ enum HardwareModel: SwiftProtobuf.Enum { extension HardwareModel: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [HardwareModel] = [ + static var allCases: [HardwareModel] = [ .unset, .tloraV2, .tloraV1, @@ -465,6 +478,8 @@ extension HardwareModel: CaseIterable { .heltecWirelessTrackerV10, .unphone, .tdLorac, + .cdebyteEoraS3, + .twcMeshV4, .privateHw, ] } @@ -514,7 +529,7 @@ enum Constants: SwiftProtobuf.Enum { extension Constants: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Constants] = [ + static var allCases: [Constants] = [ .zero, .dataPayloadLen, ] @@ -627,7 +642,7 @@ enum CriticalErrorCode: SwiftProtobuf.Enum { extension CriticalErrorCode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [CriticalErrorCode] = [ + static var allCases: [CriticalErrorCode] = [ .none, .txWatchdog, .sleepEnterWait, @@ -946,7 +961,7 @@ struct Position { extension Position.LocSource: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Position.LocSource] = [ + static var allCases: [Position.LocSource] = [ .locUnset, .locManual, .locInternal, @@ -956,7 +971,7 @@ extension Position.LocSource: CaseIterable { extension Position.AltSource: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Position.AltSource] = [ + static var allCases: [Position.AltSource] = [ .altUnset, .altManual, .altInternal, @@ -1237,7 +1252,7 @@ struct Routing { extension Routing.Error: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [Routing.Error] = [ + static var allCases: [Routing.Error] = [ .none, .noRoute, .gotNak, @@ -1755,7 +1770,7 @@ struct MeshPacket { extension MeshPacket.Priority: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [MeshPacket.Priority] = [ + static var allCases: [MeshPacket.Priority] = [ .unset, .min, .background, @@ -1768,7 +1783,7 @@ extension MeshPacket.Priority: CaseIterable { extension MeshPacket.Delayed: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [MeshPacket.Delayed] = [ + static var allCases: [MeshPacket.Delayed] = [ .noDelay, .broadcast, .direct, @@ -2022,7 +2037,7 @@ struct LogRecord { extension LogRecord.Level: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [LogRecord.Level] = [ + static var allCases: [LogRecord.Level] = [ .unset, .critical, .error, @@ -2762,6 +2777,8 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 58: .same(proto: "HELTEC_WIRELESS_TRACKER_V1_0"), 59: .same(proto: "UNPHONE"), 60: .same(proto: "TD_LORAC"), + 61: .same(proto: "CDEBYTE_EORA_S3"), + 62: .same(proto: "TWC_MESH_V4"), 255: .same(proto: "PRIVATE_HW"), ] } @@ -2843,15 +2860,7 @@ extension Position: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB var _seqNumber: UInt32 = 0 var _precisionBits: UInt32 = 0 - #if swift(>=5.10) - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif + static let defaultInstance = _StorageClass() private init() {} @@ -3513,15 +3522,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio var _viaMqtt: Bool = false var _hopStart: UInt32 = 0 - #if swift(>=5.10) - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif + static let defaultInstance = _StorageClass() private init() {} @@ -3733,15 +3734,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB var _hopsAway: UInt32 = 0 var _isFavorite: Bool = false - #if swift(>=5.10) - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif + static let defaultInstance = _StorageClass() private init() {} @@ -4033,15 +4026,7 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation var _id: UInt32 = 0 var _payloadVariant: FromRadio.OneOf_PayloadVariant? - #if swift(>=5.10) - // This property is used as the initial default value for new instances of the type. - // The type itself is protecting the reference to its storage via CoW semantics. - // This will force a copy to be made of this reference when the first mutation occurs; - // hence, it is safe to mark this as `nonisolated(unsafe)`. - static nonisolated(unsafe) let defaultInstance = _StorageClass() - #else - static let defaultInstance = _StorageClass() - #endif + static let defaultInstance = _StorageClass() private init() {} diff --git a/Meshtastic/Protobufs/meshtastic/module_config.pb.swift b/Meshtastic/Protobufs/meshtastic/module_config.pb.swift index f6c28745..b688479b 100644 --- a/Meshtastic/Protobufs/meshtastic/module_config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/module_config.pb.swift @@ -64,7 +64,7 @@ enum RemoteHardwarePinType: SwiftProtobuf.Enum { extension RemoteHardwarePinType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [RemoteHardwarePinType] = [ + static var allCases: [RemoteHardwarePinType] = [ .unknown, .digitalRead, .digitalWrite, @@ -1152,7 +1152,7 @@ struct ModuleConfig { extension ModuleConfig.AudioConfig.Audio_Baud: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ + static var allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ .codec2Default, .codec23200, .codec22400, @@ -1167,7 +1167,7 @@ extension ModuleConfig.AudioConfig.Audio_Baud: CaseIterable { extension ModuleConfig.SerialConfig.Serial_Baud: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [ + static var allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [ .baudDefault, .baud110, .baud300, @@ -1189,7 +1189,7 @@ extension ModuleConfig.SerialConfig.Serial_Baud: CaseIterable { extension ModuleConfig.SerialConfig.Serial_Mode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [ + static var allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [ .default, .simple, .proto, @@ -1201,7 +1201,7 @@ extension ModuleConfig.SerialConfig.Serial_Mode: CaseIterable { extension ModuleConfig.CannedMessageConfig.InputEventChar: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [ + static var allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [ .none, .up, .down, diff --git a/Meshtastic/Protobufs/meshtastic/portnums.pb.swift b/Meshtastic/Protobufs/meshtastic/portnums.pb.swift index a67e5ae6..c8948d7d 100644 --- a/Meshtastic/Protobufs/meshtastic/portnums.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/portnums.pb.swift @@ -277,7 +277,7 @@ enum PortNum: SwiftProtobuf.Enum { extension PortNum: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [PortNum] = [ + static var allCases: [PortNum] = [ .unknownApp, .textMessageApp, .remoteHardwareApp, diff --git a/Meshtastic/Protobufs/meshtastic/remote_hardware.pb.swift b/Meshtastic/Protobufs/meshtastic/remote_hardware.pb.swift index b26a80a2..1f24d0db 100644 --- a/Meshtastic/Protobufs/meshtastic/remote_hardware.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/remote_hardware.pb.swift @@ -119,7 +119,7 @@ struct HardwareMessage { extension HardwareMessage.TypeEnum: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [HardwareMessage.TypeEnum] = [ + static var allCases: [HardwareMessage.TypeEnum] = [ .unset, .writeGpios, .watchGpios, diff --git a/Meshtastic/Protobufs/meshtastic/storeforward.pb.swift b/Meshtastic/Protobufs/meshtastic/storeforward.pb.swift index 697b4e87..a8fa5f90 100644 --- a/Meshtastic/Protobufs/meshtastic/storeforward.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/storeforward.pb.swift @@ -344,7 +344,7 @@ struct StoreAndForward { extension StoreAndForward.RequestResponse: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [StoreAndForward.RequestResponse] = [ + static var allCases: [StoreAndForward.RequestResponse] = [ .unset, .routerError, .routerHeartbeat, diff --git a/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift b/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift index d878629a..5f96c02b 100644 --- a/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift @@ -88,6 +88,10 @@ enum TelemetrySensorType: SwiftProtobuf.Enum { /// /// BMP085/BMP180 High accuracy temperature and pressure (older Version of BMP280) case bmp085 // = 15 + + /// + /// RCWL-9620 Doppler Radar Distance Sensor, used for water level detection + case rcwl9620 // = 16 case UNRECOGNIZED(Int) init() { @@ -112,6 +116,7 @@ enum TelemetrySensorType: SwiftProtobuf.Enum { case 13: self = .pmsa003I case 14: self = .ina3221 case 15: self = .bmp085 + case 16: self = .rcwl9620 default: self = .UNRECOGNIZED(rawValue) } } @@ -134,6 +139,7 @@ enum TelemetrySensorType: SwiftProtobuf.Enum { case .pmsa003I: return 13 case .ina3221: return 14 case .bmp085: return 15 + case .rcwl9620: return 16 case .UNRECOGNIZED(let i): return i } } @@ -144,7 +150,7 @@ enum TelemetrySensorType: SwiftProtobuf.Enum { extension TelemetrySensorType: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [TelemetrySensorType] = [ + static var allCases: [TelemetrySensorType] = [ .sensorUnset, .bme280, .bme680, @@ -161,6 +167,7 @@ extension TelemetrySensorType: CaseIterable { .pmsa003I, .ina3221, .bmp085, + .rcwl9620, ] } @@ -234,6 +241,10 @@ struct EnvironmentMetrics { /// Belongs to Air Quality but is not particle but VOC measurement. Other VOC values can also be put in here. var iaq: UInt32 = 0 + /// + /// RCWL9620 Doppler Radar Distance Sensor, used for water level detection. Float value in mm. + var distance: Float = 0 + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -467,6 +478,7 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 13: .same(proto: "PMSA003I"), 14: .same(proto: "INA3221"), 15: .same(proto: "BMP085"), + 16: .same(proto: "RCWL9620"), ] } @@ -536,6 +548,7 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple 5: .same(proto: "voltage"), 6: .same(proto: "current"), 7: .same(proto: "iaq"), + 8: .same(proto: "distance"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -551,6 +564,7 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple case 5: try { try decoder.decodeSingularFloatField(value: &self.voltage) }() case 6: try { try decoder.decodeSingularFloatField(value: &self.current) }() case 7: try { try decoder.decodeSingularUInt32Field(value: &self.iaq) }() + case 8: try { try decoder.decodeSingularFloatField(value: &self.distance) }() default: break } } @@ -578,6 +592,9 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.iaq != 0 { try visitor.visitSingularUInt32Field(value: self.iaq, fieldNumber: 7) } + if self.distance != 0 { + try visitor.visitSingularFloatField(value: self.distance, fieldNumber: 8) + } try unknownFields.traverse(visitor: &visitor) } @@ -589,6 +606,7 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if lhs.voltage != rhs.voltage {return false} if lhs.current != rhs.current {return false} if lhs.iaq != rhs.iaq {return false} + if lhs.distance != rhs.distance {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Protobufs/meshtastic/xmodem.pb.swift b/Meshtastic/Protobufs/meshtastic/xmodem.pb.swift index 4df972b8..a70841f2 100644 --- a/Meshtastic/Protobufs/meshtastic/xmodem.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/xmodem.pb.swift @@ -88,7 +88,7 @@ struct XModem { extension XModem.Control: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. - static let allCases: [XModem.Control] = [ + static var allCases: [XModem.Control] = [ .nul, .soh, .stx, diff --git a/Meshtastic/Views/Helpers/Weather/IAQScale.swift b/Meshtastic/Views/Helpers/Weather/IAQScale.swift index 97ec91e6..58de0bb8 100644 --- a/Meshtastic/Views/Helpers/Weather/IAQScale.swift +++ b/Meshtastic/Views/Helpers/Weather/IAQScale.swift @@ -22,12 +22,11 @@ struct IAQScale: View { } } .padding() - .background(.white) - .cornerRadius(20) /// make the background rounded - .overlay( - RoundedRectangle(cornerRadius: 20) - .stroke(.secondary, lineWidth: 4) - ) + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) +// .overlay( +// RoundedRectangle(cornerRadius: 20) +// .stroke(.secondary, lineWidth: 5) +// ) } } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 8705e7d1..da0d7c48 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -48,7 +48,7 @@ struct ChannelMessageList: View { if !currentUser { CircleText(text: message.fromUser?.shortName ?? "?", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 44) .padding(.all, 5) - .offset(y: -10) + .offset(y: -7) } VStack(alignment: currentUser ? .trailing : .leading) { @@ -58,6 +58,7 @@ struct ChannelMessageList: View { Text("\(message.fromUser?.longName ?? "unknown".localized ) (\(message.fromUser?.userId ?? "?"))") .font(.caption) .foregroundColor(.gray) + .offset(y: 5) } HStack { diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 68596dab..d6e84492 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -25,6 +25,7 @@ struct DeviceConfig: View { @State var rebroadcastMode = 0 @State var nodeInfoBroadcastSecs = 10800 @State var doubleTapAsButtonPress = false + @State var ledHeartbeatEnabled = true @State var isManaged = false @State var tzdef = "" @@ -58,6 +59,12 @@ struct DeviceConfig: View { } .pickerStyle(DefaultPickerStyle()) + Toggle(isOn: $isManaged) { + Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath") + Text("Enabling Managed mode will restrict access to all radio configurations, such as short/long names, regions, channels, modules, etc. and will only be accessible through the Admin channel. To avoid being locked out, make sure the Admin channel is working properly before enabling it.") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Picker("Node Info Broadcast Interval", selection: $nodeInfoBroadcastSecs ) { ForEach(UpdateIntervals.allCases) { ui in if ui.rawValue >= 3600 { @@ -66,15 +73,18 @@ struct DeviceConfig: View { } } .pickerStyle(DefaultPickerStyle()) + } + Section(header: Text("Hardware")) { + Toggle(isOn: $doubleTapAsButtonPress) { Label("Double Tap as Button", systemImage: "hand.tap") Text("Treat double tap on supported accelerometers as a user button press.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $isManaged) { - Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath") - Text("Enabling Managed mode will restrict access to all radio configurations, such as short/long names, regions, channels, modules, etc. and will only be accessible through the Admin channel. To avoid being locked out, make sure the Admin channel is working properly before enabling it.") + Toggle(isOn: $ledHeartbeatEnabled) { + Label("LED Heartbeat", systemImage: "waveform.path.ecg") + Text("Controls the blinking LED on the device. For most devices this will control one of the up to 4 LEDS, the charger and GPS LEDs are not controllable.") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } @@ -202,6 +212,7 @@ struct DeviceConfig: View { dc.doubleTapAsButtonPress = doubleTapAsButtonPress dc.isManaged = isManaged dc.tzdef = tzdef + dc.ledHeartbeatDisabled = !ledHeartbeatEnabled if isManaged { serialEnabled = false debugLogEnabled = false @@ -300,6 +311,7 @@ struct DeviceConfig: View { nodeInfoBroadcastSecs = 3600 } self.doubleTapAsButtonPress = node?.deviceConfig?.doubleTapAsButtonPress ?? false + self.ledHeartbeatEnabled = node?.deviceConfig?.ledHeartbeatEnabled ?? true self.isManaged = node?.deviceConfig?.isManaged ?? false if self.tzdef.isEmpty { self.tzdef = TimeZone.current.posixDescription diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index a6931aff..58beac1c 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -398,21 +398,20 @@ struct MQTTConfig: View { if !stateTopic.isEmpty { nearbyTopics.append(stateTopic) } - let countyTopic = defaultTopic + "/" + (placemark.subAdministrativeArea?.lowercased().replacingOccurrences(of: " ", with: "") ?? "") + let countyTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subAdministrativeArea?.lowercased().replacingOccurrences(of: " ", with: "") ?? "") if !countyTopic.isEmpty { nearbyTopics.append(countyTopic) } - let cityTopic = defaultTopic + "/" + (placemark.locality?.lowercased().replacingOccurrences(of: " ", with: "") ?? "") + let cityTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.locality?.lowercased().replacingOccurrences(of: " ", with: "") ?? "") if !cityTopic.isEmpty { nearbyTopics.append(cityTopic) } - let neightborhoodTopic = defaultTopic + "/" + (placemark.subLocality?.lowercased() + let neightborhoodTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subLocality?.lowercased() .replacingOccurrences(of: " ", with: "") .replacingOccurrences(of: "'", with: "") ?? "") if !neightborhoodTopic.isEmpty { nearbyTopics.append(neightborhoodTopic) } - } else { diff --git a/Settings.bundle/Root.plist b/Settings.bundle/Root.plist index 6d3b6588..7625b424 100644 --- a/Settings.bundle/Root.plist +++ b/Settings.bundle/Root.plist @@ -78,9 +78,9 @@ Type PSToggleSwitchSpecifier Title - Low Battery + Channel Messages Key - lowBatteryNotifications + channelMessageNotifications DefaultValue @@ -94,6 +94,16 @@ DefaultValue + + Type + PSToggleSwitchSpecifier + Title + Low Battery + Key + lowBatteryNotifications + DefaultValue + + diff --git a/protobufs b/protobufs index 22cbd0d4..86640f20 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 22cbd0d4cfafa4b8c1e64517e06edc2d7a22cca9 +Subproject commit 86640f20db7b9b5be42949d18e8d96ad10d47a68 From c6965b28ce8fcd960a15990cb78c4b9dc104df73 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 27 Apr 2024 18:55:51 -0700 Subject: [PATCH 06/11] Remove power values from environment metrics add a little space to the user info in channel messages --- Meshtastic/Export/WriteCsvFile.swift | 6 +----- Meshtastic/Views/Messages/ChannelMessageList.swift | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Meshtastic/Export/WriteCsvFile.swift b/Meshtastic/Export/WriteCsvFile.swift index 396c00fe..1048846c 100644 --- a/Meshtastic/Export/WriteCsvFile.swift +++ b/Meshtastic/Export/WriteCsvFile.swift @@ -32,7 +32,7 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin } } else if metricsType == 1 { // Create Environment Telemetry Header - csvString = "Temperature, Relative Humidity, Barometric Pressure, Indoor Air Quality, Gas Resistance, \("voltage".localized), \("current".localized), \("timestamp".localized)" + csvString = "Temperature, Relative Humidity, Barometric Pressure, Indoor Air Quality, Gas Resistance, \("timestamp".localized)" for dm in telemetry { if dm.metricsType == 1 { csvString += "\n" @@ -46,10 +46,6 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin csvString += ", " csvString += String(dm.gasResistance) csvString += ", " - csvString += String(dm.voltage) - csvString += ", " - csvString += String(dm.current) - csvString += ", " csvString += dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized } } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index da0d7c48..7d7a7962 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -58,10 +58,10 @@ struct ChannelMessageList: View { Text("\(message.fromUser?.longName ?? "unknown".localized ) (\(message.fromUser?.userId ?? "?"))") .font(.caption) .foregroundColor(.gray) - .offset(y: 5) + .offset(y: 8) } + HStack { - MessageText( message: message, tapBackDestination: .channel(channel), @@ -75,7 +75,7 @@ struct ChannelMessageList: View { RetryButton(message: message, destination: .channel(channel)) } } - + TapbackResponses(message: message) { appState.unreadChannelMessages = myInfo.unreadMessages UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages From 60831f6e75aa584643cda7ee5623bb87fbf23caa Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 30 Apr 2024 11:09:04 -0700 Subject: [PATCH 07/11] Mute channels --- .../CoreData/ChannelEntityExtension.swift | 10 +++++ Meshtastic/Helpers/MeshPackets.swift | 38 ++++++++++--------- .../Protobufs/meshtastic/channel.pb.swift | 11 ++++++ Meshtastic/Views/Messages/ChannelList.swift | 18 +++++++++ protobufs | 2 +- 5 files changed, 61 insertions(+), 18 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift index dfa373be..e5a4d95a 100644 --- a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift @@ -18,4 +18,14 @@ extension ChannelEntity { let unreadMessages = allPrivateMessages.filter{ ($0 as AnyObject).read == false } return unreadMessages.count } + + var protoBuf: Channel { + var channel = Channel() + channel.index = self.index + channel.settings.name = self.name ?? "" + channel.settings.psk = self.psk ?? Data() + channel.role = Channel.Role(rawValue: Int(self.role)) ?? Channel.Role.secondary + channel.settings.moduleSettings.positionPrecision = UInt32(self.positionPrecision) + return channel + } } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 892c81f5..c4003f26 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -160,12 +160,14 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo newChannel.name = channel.settings.name newChannel.role = Int32(channel.role.rawValue) newChannel.psk = channel.settings.psk - newChannel.positionPrecision = Int32(truncatingIfNeeded: channel.settings.moduleSettings.positionPrecision) + if channel.settings.hasModuleSettings { + newChannel.positionPrecision = Int32(truncatingIfNeeded: channel.settings.moduleSettings.positionPrecision) + newChannel.mute = channel.settings.moduleSettings.isClientMuted + } guard let mutableChannels = fetchedMyInfo[0].channels!.mutableCopy() as? NSMutableOrderedSet else { return } if let oldChannel = mutableChannels.first(where: {($0 as AnyObject).index == newChannel.index }) as? ChannelEntity { - newChannel.mute = oldChannel.mute let index = mutableChannels.index(of: oldChannel as Any) mutableChannels.replaceObject(at: index, with: newChannel) } else { @@ -841,26 +843,28 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec return } let appState = AppState.shared - if newMessage.fromUser != nil && newMessage.toUser != nil && !(newMessage.fromUser?.mute ?? false) { + if newMessage.fromUser != nil && newMessage.toUser != nil { // Set Unread Message Indicators if packet.to == connectedNode { appState.unreadDirectMessages = newMessage.toUser?.unreadMessages ?? 0 UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages } - // Create an iOS Notification for the received DM message and schedule it immediately - let manager = LocalNotificationManager() - manager.notifications = [ - Notification( - id: ("notification.id.\(newMessage.messageId)"), - title: "\(newMessage.fromUser?.longName ?? "unknown".localized)", - subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", - content: messageText!, - target: "message", - path: "meshtastic://open-dm?userid=\(newMessage.fromUser?.num ?? 0)&id=\(newMessage.messageId)" - ) - ] - manager.schedule() - print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)") + if !(newMessage.fromUser?.mute ?? false) { + // Create an iOS Notification for the received DM message and schedule it immediately + let manager = LocalNotificationManager() + manager.notifications = [ + Notification( + id: ("notification.id.\(newMessage.messageId)"), + title: "\(newMessage.fromUser?.longName ?? "unknown".localized)", + subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", + content: messageText!, + target: "message", + path: "meshtastic://open-dm?userid=\(newMessage.fromUser?.num ?? 0)&id=\(newMessage.messageId)" + ) + ] + manager.schedule() + print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)") + } } else if newMessage.fromUser != nil && newMessage.toUser == nil { let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") diff --git a/Meshtastic/Protobufs/meshtastic/channel.pb.swift b/Meshtastic/Protobufs/meshtastic/channel.pb.swift index 493230ba..992d8f4c 100644 --- a/Meshtastic/Protobufs/meshtastic/channel.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/channel.pb.swift @@ -120,6 +120,11 @@ struct ModuleSettings { /// Bits of precision for the location sent in position packets. var positionPrecision: UInt32 = 0 + /// + /// Controls whether or not the phone / clients should mute the current channel + /// Useful for noisy public channels you don't necessarily want to disable + var isClientMuted: Bool = false + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -311,6 +316,7 @@ extension ModuleSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement static let protoMessageName: String = _protobuf_package + ".ModuleSettings" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "position_precision"), + 2: .standard(proto: "is_client_muted"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -320,6 +326,7 @@ extension ModuleSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement // enabled. https://github.com/apple/swift-protobuf/issues/1034 switch fieldNumber { case 1: try { try decoder.decodeSingularUInt32Field(value: &self.positionPrecision) }() + case 2: try { try decoder.decodeSingularBoolField(value: &self.isClientMuted) }() default: break } } @@ -329,11 +336,15 @@ extension ModuleSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement if self.positionPrecision != 0 { try visitor.visitSingularUInt32Field(value: self.positionPrecision, fieldNumber: 1) } + if self.isClientMuted != false { + try visitor.visitSingularBoolField(value: self.isClientMuted, fieldNumber: 2) + } try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: ModuleSettings, rhs: ModuleSettings) -> Bool { if lhs.positionPrecision != rhs.positionPrecision {return false} + if lhs.isClientMuted != rhs.isClientMuted {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index 2c3a83f4..d8a4e96d 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -109,6 +109,24 @@ struct ChannelList: View { Label("Delete Messages", systemImage: "trash") } } + Button { + channel.mute = !channel.mute + + do { + let adminMessageId = bleManager.saveChannel(channel: channel.protoBuf, fromUser: node!.user!, toUser: node!.user!) + if adminMessageId > 0 { + context.refresh(channel, mergeChanges: true) + } + + try context.save() + + } catch { + context.rollback() + print("💥 Save Channel Mute Error") + } + } label: { + Label(channel.mute ? "Show Alerts" : "Hide Alerts", systemImage: channel.mute ? "bell" : "bell.slash") + } } .confirmationDialog( "This conversation will be deleted.", diff --git a/protobufs b/protobufs index 86640f20..e21899aa 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 86640f20db7b9b5be42949d18e8d96ad10d47a68 +Subproject commit e21899aa6b2b49863cfa2758e5e3b6faacf04bba From 04fef0cba97f720712049b281af3ce36f836e216 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 30 Apr 2024 12:26:57 -0700 Subject: [PATCH 08/11] Get out of disconnect loop --- Meshtastic/Helpers/BLEManager.swift | 3 +++ Meshtastic/Views/Bluetooth/Connect.swift | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 084fe083..394d6d61 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -151,12 +151,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate isConnecting = false isConnected = false isSubscribed = false + self.connectedPeripheral = nil invalidVersion = false connectedVersion = "0.0.0" connectedPeripheral = nil if timeoutTimer != nil { timeoutTimer!.invalidate() } + automaticallyReconnect = false + stopScanning() startScanning() } diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index da0b2a9e..8fa4d162 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -258,6 +258,18 @@ struct Connect: View { .controlSize(.large) .padding() } + if bleManager.isConnecting { + Button(role: .destructive, action: { + bleManager.cancelPeripheralConnection() + + }) { + Label("disconnect", systemImage: "antenna.radiowaves.left.and.right.slash") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + } #endif Spacer() } From 9663cd69ff10c29d1f7eaee99d131b2a709f40ae Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 30 Apr 2024 12:44:47 -0700 Subject: [PATCH 09/11] Let the firmware handle position precision value from channel --- Meshtastic/Helpers/BLEManager.swift | 61 +++++++++++------------------ 1 file changed, 23 insertions(+), 38 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 394d6d61..8e8a4b53 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -559,7 +559,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate tryClearExistingChannels() } // NodeInfo - if decodedInfo.nodeInfo.num > 0 {// && !invalidVersion { + if context != nil && decodedInfo.nodeInfo.num > 0 {// && !invalidVersion { nowKnown = true let nodeInfo = nodeInfoPacket(nodeInfo: decodedInfo.nodeInfo, channel: decodedInfo.packet.channel, context: context!) @@ -573,17 +573,17 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } // Channels - if decodedInfo.channel.isInitialized && connectedPeripheral != nil { + if context != nil && decodedInfo.channel.isInitialized && connectedPeripheral != nil { nowKnown = true channelPacket(channel: decodedInfo.channel, fromNum: Int64(truncatingIfNeeded: connectedPeripheral.num), context: context!) } // Config - if decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil { + if context != nil && decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil { nowKnown = true localConfig(config: decodedInfo.config, context: context!, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral.longName) } // Module Config - if decodedInfo.moduleConfig.isInitialized && !invalidVersion && self.connectedPeripheral?.num != 0{ + if context != nil && decodedInfo.moduleConfig.isInitialized && !invalidVersion && self.connectedPeripheral?.num != 0{ nowKnown = true moduleConfig(config: decodedInfo.moduleConfig, context: context!, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral?.num ?? 0), nodeLongName: self.connectedPeripheral.longName) if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) { @@ -593,7 +593,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } // Device Metadata - if decodedInfo.metadata.firmwareVersion.count > 0 && !invalidVersion { + if context != nil && decodedInfo.metadata.firmwareVersion.count > 0 && !invalidVersion { nowKnown = true deviceMetadataPacket(metadata: decodedInfo.metadata, fromNum: connectedPeripheral.num, context: context!) connectedPeripheral.firmwareVersion = decodedInfo.metadata.firmwareVersion @@ -997,37 +997,28 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return success } - public func getPositionFromPhoneGPS(channel: Int32, destNum: Int64) -> Position? { + public func getPositionFromPhoneGPS(destNum: Int64) -> Position? { var positionPacket = Position() - - let fetchChannelRequest: NSFetchRequest = NSFetchRequest.init(entityName: "ChannelEntity") - fetchChannelRequest.predicate = NSPredicate(format: "index == %lld", channel) - do { - guard let fetchedChannel = try context!.fetch(fetchChannelRequest) as? [ChannelEntity] else { - return nil - } if #available(iOS 17.0, macOS 14.0, *) { if let lastLocation = LocationsHandler.shared.locationsArray.last { - if fetchedChannel.count > 0 { - positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7) - positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7) - let timestamp = lastLocation.timestamp - positionPacket.time = UInt32(timestamp.timeIntervalSince1970) - positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970) - positionPacket.altitude = Int32(lastLocation.altitude) - positionPacket.satsInView = UInt32(LocationsHandler.satsInView) - positionPacket.precisionBits = UInt32(fetchedChannel[0].positionPrecision) - let currentSpeed = lastLocation.speed - if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { - positionPacket.groundSpeed = UInt32(currentSpeed * 3.6) - } - let currentHeading = lastLocation.course - if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) { - positionPacket.groundTrack = UInt32(currentHeading) - } + positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7) + positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7) + let timestamp = lastLocation.timestamp + positionPacket.time = UInt32(timestamp.timeIntervalSince1970) + positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970) + positionPacket.altitude = Int32(lastLocation.altitude) + positionPacket.satsInView = UInt32(LocationsHandler.satsInView) + + let currentSpeed = lastLocation.speed + if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { + positionPacket.groundSpeed = UInt32(currentSpeed * 3.6) + } + let currentHeading = lastLocation.course + if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) { + positionPacket.groundTrack = UInt32(currentHeading) } } @@ -1035,9 +1026,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if destNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 { return nil } - if fetchedChannel.count <= 0 { - return nil - } positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7) positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7) @@ -1046,7 +1034,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970) positionPacket.altitude = Int32(LocationHelper.shared.locationManager.location?.altitude ?? 0) positionPacket.satsInView = UInt32(LocationHelper.satsInView) - positionPacket.precisionBits = UInt32(fetchedChannel[0].positionPrecision) let currentSpeed = LocationHelper.shared.locationManager.location?.speed ?? 0 if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { positionPacket.groundSpeed = UInt32(currentSpeed * 3.6) @@ -1056,17 +1043,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate positionPacket.groundTrack = UInt32(currentHeading) } } - } catch { return nil } - return positionPacket } public func setFixedPosition(fromUser: UserEntity, channel: Int32) -> Bool { var adminPacket = AdminMessage() - guard let positionPacket = getPositionFromPhoneGPS(channel: channel, destNum: fromUser.num) else { + guard let positionPacket = getPositionFromPhoneGPS(destNum: fromUser.num) else { return false } adminPacket.setFixedPosition = positionPacket @@ -1112,7 +1097,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func sendPosition(channel: Int32, destNum: Int64, wantResponse: Bool) -> Bool { var success = false let fromNodeNum = connectedPeripheral.num - guard let positionPacket = getPositionFromPhoneGPS(channel: channel, destNum: destNum) else { + guard let positionPacket = getPositionFromPhoneGPS(destNum: destNum) else { return false } From c871caa21d60dd2345fea7087c5a0059c5fd3bb7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 1 May 2024 10:13:15 -0700 Subject: [PATCH 10/11] Simplify settings --- Meshtastic/Extensions/Color.swift | 4 ---- Settings.bundle/Root.plist | 12 +++++++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Meshtastic/Extensions/Color.swift b/Meshtastic/Extensions/Color.swift index b51fd839..252eb361 100644 --- a/Meshtastic/Extensions/Color.swift +++ b/Meshtastic/Extensions/Color.swift @@ -18,10 +18,6 @@ extension Color { return (brightness > 0.5) } public static let magenta = Color(red: 0.50, green: 0.00, blue: 0.00) -// public static var magenta: Color { -// return Color(red: 0.50, green: 0.00, blue: 0.00) -// //return Color(UIColor(red: 0.50, green: 0.00, blue: 0.00, alpha: 1.00)) //return Color(UIColor.magenta) -// } } extension UIColor { diff --git a/Settings.bundle/Root.plist b/Settings.bundle/Root.plist index 7625b424..7fb7c0cc 100644 --- a/Settings.bundle/Root.plist +++ b/Settings.bundle/Root.plist @@ -6,6 +6,16 @@ Root PreferenceSpecifiers + + Type + PSTitleValueSpecifier + DefaultValue + + Title + Will share your phone GPS location with your node. + Key + shareLocationTitle + Type PSToggleSwitchSpecifier @@ -20,7 +30,7 @@ Type PSMultiValueSpecifier Title - Share Location Interval + Interval Key provideLocationInterval Values From 7f14f69dc3bbcd1054f943da4cc3b3e2ee7ccb8c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 1 May 2024 11:38:34 -0700 Subject: [PATCH 11/11] Improve block range test packet logic --- Meshtastic/Helpers/MeshPackets.swift | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index c4003f26..f61d1994 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -8,6 +8,7 @@ import Foundation import CoreData import SwiftUI +import RegexBuilder #if canImport(ActivityKit) import ActivityKit #endif @@ -774,7 +775,19 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connectedNode: Int64, storeForward: Bool = false, context: NSManagedObjectContext) { var messageText = String(bytes: packet.decoded.payload, encoding: .utf8) - if !wantRangeTestPackets && (String(messageText ?? "seq ").starts(with: "seq ")) { + let rangeRef = Reference(Int.self) + let rangeTestRegex = Regex { + "seq " + + TryCapture(as: rangeRef) { + OneOrMore(.digit) + } transform: { match in + Int(match) + } + } + let rangeTest = messageText?.contains(rangeTestRegex) ?? false && messageText?.starts(with: "seq ") ?? false + + if !wantRangeTestPackets && rangeTest { return } var storeForwardBroadcast = false