From 93ffd8c72d4336640bbcfc63597464b8eb04c49e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 24 Mar 2023 10:35:54 -0700 Subject: [PATCH 01/21] Fix username on mqtt config save Set mqtt.address character limit to 62 --- Meshtastic.xcodeproj/project.pbxproj | 6 ++ Meshtastic/Persistence/UpdateCoreData.swift | 4 +- .../Settings/Config/Module/MQTTConfig.swift | 4 +- Meshtastic/Views/Settings/ShareChannels.swift | 2 +- Widgets/BatteryLevel.swift | 76 +++++++++++++++++++ Widgets/WidgetsLiveActivity.swift | 73 +++++++++++------- 6 files changed, 133 insertions(+), 32 deletions(-) create mode 100644 Widgets/BatteryLevel.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 1353fce8..bdb16180 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -108,6 +108,8 @@ DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */; }; DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC3B273283F411B00AC321C /* LastHeardText.swift */; }; DDC4D568275499A500A4208E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC4D567275499A500A4208E /* Persistence.swift */; }; + DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */; }; + DDC94FC229CE063B0082EA6E /* BatteryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */; }; DDCDC6CB29481FCC004C1DDA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DDCDC6CD29481FCC004C1DDA /* Localizable.strings */; }; DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */; }; DDD3BBD5292D763200D609B3 /* MeshtasticTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */; }; @@ -282,6 +284,7 @@ DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationHelper.swift; sourceTree = ""; }; DDC3B273283F411B00AC321C /* LastHeardText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastHeardText.swift; sourceTree = ""; }; DDC4D567275499A500A4208E /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; + DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryLevel.swift; sourceTree = ""; }; DDCDC69A29467643004C1DDA /* MeshtasticDataModelV3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV3.xcdatamodel; sourceTree = ""; }; DDCDC6CC29481FCC004C1DDA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; DDCDC6CE294821AD004C1DDA /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; @@ -677,6 +680,7 @@ DDDE59FC29AF163D00490C6C /* Widgets.swift */, DDDE5A0029AF163E00490C6C /* Info.plist */, DDDE5A0F29AFE69700490C6C /* MeshActivityAttributes.swift */, + DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */, ); path = Widgets; sourceTree = ""; @@ -901,6 +905,7 @@ DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */, DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, + DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */, DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */, DD5E5213298EE33B00D21B61 /* deviceonly.pb.swift in Sources */, DD5E5208298EE33B00D21B61 /* rtttl.pb.swift in Sources */, @@ -1003,6 +1008,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DDC94FC229CE063B0082EA6E /* BatteryLevel.swift in Sources */, DDDE5A1129AFE69700490C6C /* MeshActivityAttributes.swift in Sources */, DDDE59FB29AF163D00490C6C /* WidgetsLiveActivity.swift in Sources */, DDDE59FD29AF163D00490C6C /* Widgets.swift in Sources */, diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 9763b0e7..639a2bb8 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -719,7 +719,7 @@ func upsertMqttModuleConfigPacket(config: Meshtastic.ModuleConfig.MQTTConfig, no let newMQTTConfig = MQTTConfigEntity(context: context) newMQTTConfig.enabled = config.enabled newMQTTConfig.address = config.address - newMQTTConfig.address = config.username + newMQTTConfig.username = config.username newMQTTConfig.password = config.password newMQTTConfig.encryptionEnabled = config.encryptionEnabled newMQTTConfig.jsonEnabled = config.jsonEnabled @@ -727,7 +727,7 @@ func upsertMqttModuleConfigPacket(config: Meshtastic.ModuleConfig.MQTTConfig, no } else { fetchedNode[0].mqttConfig?.enabled = config.enabled fetchedNode[0].mqttConfig?.address = config.address - fetchedNode[0].mqttConfig?.address = config.username + fetchedNode[0].mqttConfig?.username = config.username fetchedNode[0].mqttConfig?.password = config.password fetchedNode[0].mqttConfig?.encryptionEnabled = config.encryptionEnabled fetchedNode[0].mqttConfig?.jsonEnabled = config.jsonEnabled diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 33a57063..4cefda0c 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -79,8 +79,8 @@ struct MQTTConfig: View { .onChange(of: address, perform: { _ in let totalBytes = address.utf8.count // Only mess with the value if it is too big - if totalBytes > 30 { - let firstNBytes = Data(username.utf8.prefix(30)) + if totalBytes > 62 { + let firstNBytes = Data(username.utf8.prefix(62)) if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { // Set the shortName back to the last place where it was the right size address = maxBytesString diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index 5ca297dd..86d79877 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -224,7 +224,7 @@ struct ShareChannels: View { .font(.headline) .padding(.bottom) Text("Primary Channel").font(.title2) - Text("The first channel is the Primary channel and is where much of the mesh activity takes place. DM's are only available on the primary channel and it can not be disabled.") + Text("The first channel is the Primary channel and is where much of the mesh activity takes place. DM's are only available on the primary channel and it can not be disabled. If you don't share your primary channel, the first channel will become the primary channel on the other network and will allow communication with your mesh on the group channel.") .font(.callout) .padding([.leading, .trailing, .bottom]) Text("Admin Channel").font(.title2) diff --git a/Widgets/BatteryLevel.swift b/Widgets/BatteryLevel.swift new file mode 100644 index 00000000..f20d03aa --- /dev/null +++ b/Widgets/BatteryLevel.swift @@ -0,0 +1,76 @@ +// +// BatteryLevel.swift +// Meshtastic +// +// Copyright Garth Vander Houwen 3/24/23. +// +import SwiftUI + +struct BatteryIcon: View { + var batteryLevel: Int32? + var font: Font + var color: Color + + var body: some View { + + if batteryLevel == 100 { + + Image(systemName: "battery.100.bolt") + .font(font) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + } else if batteryLevel! < 100 && batteryLevel! > 74 { + + Image(systemName: "battery.75") + .font(font) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + } else if batteryLevel! < 75 && batteryLevel! > 49 { + + Image(systemName: "battery.50") + .font(font) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + } else if batteryLevel! < 50 && batteryLevel! > 14 { + + Image(systemName: "battery.25") + .font(font) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + } else if batteryLevel! < 15 && batteryLevel! >= 0 { + + Image(systemName: "battery.0") + .font(font) + .foregroundColor(.red) + .symbolRenderingMode(.hierarchical) + } else if batteryLevel! > 100 { + + Image(systemName: "powerplug") + .font(font) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + } + else { + + Image(systemName: "battery.0") + .font(font) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + } + } +} + +struct BatteryIcon_Previews: PreviewProvider { + static var previews: some View { + BatteryIcon(batteryLevel: 100, font: .title2, color: Color.accentColor) + .previewLayout(.fixed(width: 75, height: 75)) + BatteryIcon(batteryLevel: 99, font: .title2, color: Color.accentColor) + .previewLayout(.fixed(width: 75, height: 75)) + BatteryIcon(batteryLevel: 74, font: .title2, color: Color.accentColor) + .previewLayout(.fixed(width: 75, height: 75)) + BatteryIcon(batteryLevel: 49, font: .title2, color: Color.accentColor) + .previewLayout(.fixed(width: 75, height: 75)) + BatteryIcon(batteryLevel: 14, font: .title2, color: Color.accentColor) + .previewLayout(.fixed(width: 75, height: 75)) + } +} diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index 835ecbfa..92f246fa 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -20,22 +20,49 @@ struct WidgetsLiveActivity: Widget { } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { - NodeInfoView(nodeName: context.attributes.name, timerRange: context.state.timerRange, channelUtilization: context.state.channelUtilization, airtime: context.state.airtime, batteryLevel: context.state.batteryLevel) - .tint(Color("LightIndigo")) - .padding(.top) + Text("Network") + .font(.headline) + .fontWeight(.bold) + .foregroundStyle(.secondary) + .fixedSize() + .padding(.top, 10) + Text("\(String(format: "Ch. Util: %.2f", context.state.channelUtilization))%") + .font(.headline) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .fixedSize() + Text("\(String(format: "Airtime: %.2f", context.state.airtime))%") + .font(.headline) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .fixedSize() + Spacer() } - // Expanded UI goes here. Compose the expanded UI through - // various regions, like leading/trailing/center/bottom - DynamicIslandExpandedRegion(.trailing, priority: 1) { - HStack(alignment: .lastTextBaseline) { - - Spacer() - TimerView(timerRange: context.state.timerRange) - .tint(Color("LightIndigo")) + DynamicIslandExpandedRegion(.center) { + VStack(alignment: .center, spacing: 0) { + BatteryIcon(batteryLevel: Int32(context.state.batteryLevel), font: .title, color: .accentColor) + Text(String(context.state.batteryLevel) + "%") + .font(.title3) + .foregroundColor(.gray) + .fixedSize() } - .padding(.top) + } + DynamicIslandExpandedRegion(.trailing, priority: 1) { + TimerView(timerRange: context.state.timerRange) + .tint(Color("LightIndigo")) } + DynamicIslandExpandedRegion(.bottom){ + Text(context.attributes.name) + .font(context.attributes.name.count > 14 ? .callout : .title3) + .fontWeight(.semibold) + .foregroundStyle(.tint) + Text("Last Heard: \(Date().formatted())") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .fixedSize() + } } compactLeading: { Image("logo-black") @@ -65,7 +92,7 @@ struct WidgetsLiveActivity: Widget { @available(iOS 16.2, *) struct WidgetsLiveActivity_Previews: PreviewProvider { - static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "Meshtastic 8E6G") + static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G") static let state = MeshActivityAttributes.ContentState( timerRange: Date.now...Date(timeIntervalSinceNow: 3600), connected: true, channelUtilization: 25.84, airtime: 10.01, batteryLevel: 39) @@ -134,34 +161,34 @@ struct NodeInfoView: View { .fontWeight(.semibold) .foregroundStyle(.tint) Text("\(String(format: "Ch. Util: %.2f", channelUtilization))%") - .font(.subheadline) + .font(.headline) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() Text("\(String(format: "Airtime: %.2f", airtime))%") - .font(.subheadline) + .font(.headline) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() if batteryLevel < 101 { Text("Battery Level: \(batteryLevel > 0 ? String(batteryLevel) : "< 1")%") - .font(.subheadline) + .font(.headline) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() } else { Text("Plugged In") - .font(.subheadline) + .font(.headline) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() } Text(Date().formatted()) - .font(.subheadline) + .font(.headline) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) @@ -177,18 +204,11 @@ struct TimerView: View { var body: some View { VStack(alignment: .center) { - Text("NEXT") + Text("NEXT UPDATE") .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.5 : 1.0) - .fixedSize() - Text("UPDATE") - .font(.caption) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .opacity(isLuminanceReduced ? 0.5 : 1.0) - .fixedSize() Text(timerInterval: timerRange, countsDown: true) .monospacedDigit() .multilineTextAlignment(.center) @@ -215,7 +235,6 @@ struct ExpandedTrailingView: View { var body: some View { HStack(alignment: .lastTextBaseline) { - Spacer() TimerView(timerRange: timerInterval) } From 62d02aefc2d1a4f301f515f0201371e9f2882b20 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 24 Mar 2023 10:36:59 -0700 Subject: [PATCH 02/21] Bump version --- Meshtastic.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index bdb16180..a0ecd907 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1194,7 +1194,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.3; + MARKETING_VERSION = 2.1.4; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1228,7 +1228,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.3; + MARKETING_VERSION = 2.1.4; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; From 9fc6609cecee495b1a0c552c6903bf9fe6db9310 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 24 Mar 2023 12:32:03 -0700 Subject: [PATCH 03/21] Fix battery level and expanded view for live activity --- Widgets/BatteryLevel.swift | 18 ++++--- Widgets/WidgetsLiveActivity.swift | 88 +++++++++++++++++++++++-------- 2 files changed, 76 insertions(+), 30 deletions(-) diff --git a/Widgets/BatteryLevel.swift b/Widgets/BatteryLevel.swift index f20d03aa..c9bfdccd 100644 --- a/Widgets/BatteryLevel.swift +++ b/Widgets/BatteryLevel.swift @@ -37,8 +37,15 @@ struct BatteryIcon: View { .font(font) .foregroundColor(color) .symbolRenderingMode(.hierarchical) - } else if batteryLevel! < 15 && batteryLevel! >= 0 { + } else if batteryLevel! < 15 && batteryLevel! > 0 { + Image(systemName: "battery.0") + .font(font) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + + } else if batteryLevel! == 0 { + Image(systemName: "battery.0") .font(font) .foregroundColor(.red) @@ -50,18 +57,13 @@ struct BatteryIcon: View { .foregroundColor(color) .symbolRenderingMode(.hierarchical) } - else { - - Image(systemName: "battery.0") - .font(font) - .foregroundColor(color) - .symbolRenderingMode(.hierarchical) - } } } struct BatteryIcon_Previews: PreviewProvider { static var previews: some View { + BatteryIcon(batteryLevel: 111, font: .title2, color: Color.accentColor) + .previewLayout(.fixed(width: 75, height: 75)) BatteryIcon(batteryLevel: 100, font: .title2, color: Color.accentColor) .previewLayout(.fixed(width: 75, height: 75)) BatteryIcon(batteryLevel: 99, font: .title2, color: Color.accentColor) diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index 92f246fa..b6f1adfd 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -41,10 +41,21 @@ struct WidgetsLiveActivity: Widget { DynamicIslandExpandedRegion(.center) { VStack(alignment: .center, spacing: 0) { BatteryIcon(batteryLevel: Int32(context.state.batteryLevel), font: .title, color: .accentColor) - Text(String(context.state.batteryLevel) + "%") - .font(.title3) - .foregroundColor(.gray) - .fixedSize() + if context.state.batteryLevel == 0 { + Text("< 1%") + .font(.title3) + .foregroundColor(.gray) + .fixedSize() + } else if context.state.batteryLevel < 101 { + Text(String(context.state.batteryLevel) + "%") + .font(.title3) + .foregroundColor(.gray) + .fixedSize() + } else { + Text("Plugged In") + .font(.title3) + .foregroundColor(.gray) + } } } DynamicIslandExpandedRegion(.trailing, priority: 1) { @@ -135,7 +146,31 @@ struct LiveActivityView: View { Spacer() NodeInfoView(nodeName: nodeName, timerRange: timerRange, channelUtilization: channelUtilization, airtime: airtime, batteryLevel: batteryLevel) Spacer() - TimerView(timerRange: timerRange) + VStack { + BatteryIcon(batteryLevel: Int32(batteryLevel), font: .title, color: .secondary) + if batteryLevel == 0 { + Text("< 1%") + .font(.headline) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .opacity(isLuminanceReduced ? 0.8 : 1.0) + .fixedSize() + } else if batteryLevel < 101 { + Text(String(batteryLevel) + "%") + .font(.headline) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .opacity(isLuminanceReduced ? 0.8 : 1.0) + .fixedSize() + } else { + Text("Plugged In") + .font(.headline) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .opacity(isLuminanceReduced ? 0.8 : 1.0) + .fixedSize() + } + } } .tint(.primary) .padding([.leading, .top, .bottom]) @@ -172,27 +207,36 @@ struct NodeInfoView: View { .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() - if batteryLevel < 101 { - Text("Battery Level: \(batteryLevel > 0 ? String(batteryLevel) : "< 1")%") - .font(.headline) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .opacity(isLuminanceReduced ? 0.8 : 1.0) - .fixedSize() - } else { - Text("Plugged In") - .font(.headline) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .opacity(isLuminanceReduced ? 0.8 : 1.0) - .fixedSize() - } - Text(Date().formatted()) - .font(.headline) + let now = Date() + Text("Last Heard: \(now.formatted())") + .font(.caption) .fontWeight(.medium) .foregroundStyle(.secondary) .opacity(isLuminanceReduced ? 0.8 : 1.0) .fixedSize() + HStack { + + if timerRange.upperBound >= now { + Text("Next Update:") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .opacity(isLuminanceReduced ? 0.8 : 1.0) + .fixedSize() + Text(timerInterval: timerRange, countsDown: true) + .monospacedDigit() + .multilineTextAlignment(.leading) + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.tint) + } else { + Text("Not Connected") + .multilineTextAlignment(.leading) + .font(.callout) + .fontWeight(.semibold) + .foregroundStyle(.tint) + } + } } } } From 5a73b846f22fa9ea0f84c0fff4e72bf0733d7354 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 24 Mar 2023 22:57:36 -0700 Subject: [PATCH 04/21] * Centering Fixes * Restrict node details to last thousand positions instead of the last day * Fix battery level display on node metrics view --- .../Views/Map/Custom/MapViewSwiftUI.swift | 174 ++++++++---------- Meshtastic/Views/Nodes/DeviceMetricsLog.swift | 2 +- Meshtastic/Views/Nodes/NodeDetail.swift | 5 +- Widgets/WidgetsLiveActivity.swift | 2 +- 4 files changed, 82 insertions(+), 101 deletions(-) diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 2fb8b422..91f841b1 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -12,38 +12,38 @@ func degreesToRadians(_ number: Double) -> Double { } struct MapViewSwiftUI: UIViewRepresentable { - + var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void var onWaypointEdit: (_ waypointId: Int ) -> Void let mapView = MKMapView() + let colors: [UIColor] = [UIColor.systemIndigo, UIColor.orange, UIColor.green, UIColor.brown, UIColor.purple, UIColor.systemMint, UIColor.cyan, UIColor.magenta, UIColor.systemPink, UIColor.blue] + // Parameters let positions: [PositionEntity] let waypoints: [WaypointEntity] let mapViewType: MKMapType let userTrackingMode: MKUserTrackingMode let showNodeHistory: Bool let showRouteLines: Bool - let colors: [UIColor] = [UIColor.systemIndigo, UIColor.orange, UIColor.green, UIColor.brown, UIColor.purple, UIColor.systemMint, UIColor.cyan, UIColor.magenta, UIColor.systemPink, UIColor.blue] - @AppStorage("meshMapRecentering") private var recenter: Bool = false - // Offline Maps - // make this view dependent on the UserDefault that is updated when importing a new map file + // Offline Map Tiles @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0 @State private var loadedLastUpdatedLocalMapFile = 0 var customMapOverlay: CustomMapOverlay? @State private var presentCustomMapOverlayHash: CustomMapOverlay? - func makeUIView(context: Context) -> MKMapView { // Map View Parameters mapView.mapType = mapViewType mapView.addAnnotations(waypoints) // Do the initial map centering + let latest = positions + .filter { $0.latest == true } + .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } let span = MKCoordinateSpan(latitudeDelta: 0.003, longitudeDelta: 0.003) - let center = LocationHelper.currentLocation + let center = (latest.count > 0 && userTrackingMode == MKUserTrackingMode.none) ? latest[0].coordinate : LocationHelper.currentLocation let region = MKCoordinateRegion(center: center, span: span) mapView.setRegion(region, animated: true) // Set user (phone gps) tracking options - let latest = positions.filter { $0.latest == true } mapView.setUserTrackingMode(userTrackingMode, animated: true) if userTrackingMode == MKUserTrackingMode.none { mapView.showsUserLocation = false @@ -51,6 +51,15 @@ struct MapViewSwiftUI: UIViewRepresentable { mapView.showsUserLocation = true } mapView.addAnnotations(showNodeHistory ? positions : latest) + // Set user (phone gps) tracking options + mapView.setUserTrackingMode(userTrackingMode, animated: true) + if userTrackingMode == MKUserTrackingMode.none { + mapView.fitAllAnnotations() + mapView.showsUserLocation = false + } else { + mapView.showsUserLocation = true + } + mapView.addAnnotations(showNodeHistory ? positions : latest) // Other MKMapView Settings mapView.preferredConfiguration.elevationStyle = .realistic// .flat mapView.isPitchEnabled = true @@ -60,14 +69,14 @@ struct MapViewSwiftUI: UIViewRepresentable { mapView.showsBuildings = true mapView.showsScale = true mapView.showsTraffic = true - + #if targetEnvironment(macCatalyst) // Show the default always visible compass and the mac only controls mapView.showsCompass = true mapView.showsZoomControls = true mapView.showsPitchControl = true #else - + #if os(iOS) // Hide the default compass that only appears when you are not going north and instead always show the compass in the bottom right corner of the map mapView.showsCompass = false @@ -78,19 +87,19 @@ struct MapViewSwiftUI: UIViewRepresentable { compassButton.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -5).isActive = true compassButton.bottomAnchor.constraint(equalTo: mapView.bottomAnchor, constant: -25).isActive = true #endif - + #endif mapView.delegate = context.coordinator return mapView } - + func updateUIView(_ mapView: MKMapView, context: Context) { mapView.mapType = mapViewType - + if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile { mapView.removeOverlays(mapView.overlays) if self.customMapOverlay != nil { - + let fileManager = FileManager.default let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path @@ -109,40 +118,38 @@ struct MapViewSwiftUI: UIViewRepresentable { self.loadedLastUpdatedLocalMapFile = self.lastUpdatedLocalMapFile } } - + DispatchQueue.main.async { let latest = positions .filter { $0.latest == true } .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } - - if showRouteLines { - // Remove all existing PolyLine Overlays - for overlay in mapView.overlays { - if overlay is MKPolyline { - mapView.removeOverlay(overlay) + let annotationCount = waypoints.count + (showNodeHistory || showRouteLines ? positions.count : latest.count) + if annotationCount > mapView.annotations.count { + if showRouteLines { + // Remove all existing PolyLine Overlays + for overlay in mapView.overlays { + if overlay is MKPolyline { + mapView.removeOverlay(overlay) + } + } + var lineIndex = 0 + for position in latest { + + let nodePositions = positions.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) && $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 } + let lineCoords = nodePositions.map ({ + (position) -> CLLocationCoordinate2D in + return position.nodeCoordinate! + }) + let polyline = MKPolyline(coordinates: lineCoords, count: nodePositions.count) + polyline.title = "\(String(position.nodePosition?.num ?? 0))-\(String(lineIndex))" + mapView.addOverlay(polyline) + lineIndex += 1 + // There are 10 colors for lines, start over if we are at index 10 + if lineIndex > 9 { + lineIndex = 0 + } } } - var lineIndex = 0 - for position in latest { - - let nodePositions = positions.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) && $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 } - let lineCoords = nodePositions.map ({ - (position) -> CLLocationCoordinate2D in - return position.nodeCoordinate! - }) - let polyline = MKPolyline(coordinates: lineCoords, count: nodePositions.count) - polyline.title = "\(String(position.nodePosition?.num ?? 0))-\(String(lineIndex))" - mapView.addOverlay(polyline) - lineIndex += 1 - // There are 10 colors for lines, start over if we are at index 10 - if lineIndex > 9 { - lineIndex = 0 - } - } - } - - let annotationCount = waypoints.count + positions.count - if annotationCount != mapView.annotations.count { mapView.removeAnnotations(mapView.annotations) mapView.addAnnotations(waypoints) mapView.setUserTrackingMode(userTrackingMode, animated: true) @@ -152,7 +159,7 @@ struct MapViewSwiftUI: UIViewRepresentable { mapView.addAnnotations(showNodeHistory ? positions : latest) if recenter { if showRouteLines || showNodeHistory { - mapView.fit(annotations: showNodeHistory ? positions : positions, andShow: false) + mapView.fit(annotations: showNodeHistory ? positions : latest, andShow: false) } else { mapView.fitAllAnnotations() } @@ -165,16 +172,16 @@ struct MapViewSwiftUI: UIViewRepresentable { } } } - + func makeCoordinator() -> MapCoordinator { return Coordinator(self) } - + final class MapCoordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate { - + var parent: MapViewSwiftUI var longPressRecognizer = UILongPressGestureRecognizer() - + init(_ parent: MapViewSwiftUI) { self.parent = parent super.init() @@ -184,16 +191,16 @@ struct MapViewSwiftUI: UIViewRepresentable { self.longPressRecognizer.delegate = self self.parent.mapView.addGestureRecognizer(longPressRecognizer) } - + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { - + switch annotation { case let positionAnnotation as PositionEntity: let reuseID = String(positionAnnotation.nodePosition?.num ?? 0) + "-" + String(positionAnnotation.time?.timeIntervalSince1970 ?? 0) let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: reuseID ) annotationView.tag = -1 annotationView.canShowCallout = true - + if positionAnnotation.latest { annotationView.markerTintColor = .systemRed annotationView.displayPriority = .required @@ -216,7 +223,7 @@ struct MapViewSwiftUI: UIViewRepresentable { let distanceFormatter = MKDistanceFormatter() subtitle.text! += "Altitude: \(distanceFormatter.string(fromDistance: Double(positionAnnotation.altitude))) \n" if positionAnnotation.nodePosition?.metadata != nil { - + if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.client || DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.clientMute || DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.routerClient { @@ -230,7 +237,7 @@ struct MapViewSwiftUI: UIViewRepresentable { } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.sensor { annotationView.glyphImage = UIImage(systemName: "sensor") } - + let pf = PositionFlags(rawValue: Int(positionAnnotation.nodePosition?.metadata?.positionFlags ?? 3)) if pf.contains(.Satsinview) { subtitle.text! += "Sats in view: \(String(positionAnnotation.satsInView)) \n" @@ -239,7 +246,7 @@ struct MapViewSwiftUI: UIViewRepresentable { subtitle.text! += "Sequence: \(String(positionAnnotation.seqNo)) \n" } if pf.contains(.Heading) { - + if parent.userTrackingMode != MKUserTrackingMode.followWithHeading { annotationView.glyphImage = UIImage(systemName: "location.north.fill")?.rotate(radians: Float(degreesToRadians(Double(positionAnnotation.heading)))) subtitle.text! += "Heading: \(String(positionAnnotation.heading)) \n" @@ -255,7 +262,7 @@ struct MapViewSwiftUI: UIViewRepresentable { } subtitle.text! += "Speed: \(formatter.string(from: Measurement(value: Double(positionAnnotation.speed), unit: UnitSpeed.kilometersPerHour))) \n" } - + } else { // node metadata is nil annotationView.glyphImage = UIImage(systemName: "flipphone") @@ -316,23 +323,23 @@ struct MapViewSwiftUI: UIViewRepresentable { default: return nil } } - + func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) { // Only Allow Edit for waypoint annotations with a id if view.tag > 0 { parent.onWaypointEdit(view.tag) } } - + @objc func longPressHandler(_ gesture: UILongPressGestureRecognizer) { - + if gesture.state != UIGestureRecognizer.State.ended { return } else if gesture.state != UIGestureRecognizer.State.began { - + // Screen Position - CGPoint let location = longPressRecognizer.location(in: self.parent.mapView) - + // Map Coordinate - CLLocationCoordinate2D let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) let annotation = MKPointAnnotation() @@ -343,9 +350,9 @@ struct MapViewSwiftUI: UIViewRepresentable { parent.onLongPress(coordinate) } } - + public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { - + if let tileOverlay = overlay as? MKTileOverlay { return MKTileOverlayRenderer(tileOverlay: tileOverlay) } else { @@ -362,18 +369,18 @@ struct MapViewSwiftUI: UIViewRepresentable { } } } - + /// is supposed to be located in the folder with the map name public struct DefaultTile: Hashable { let tileName: String let tileType: String - + public init(tileName: String, tileType: String) { self.tileName = tileName self.tileType = tileType } } - + public struct CustomMapOverlay: Equatable, Hashable { let mapName: String let tileType: String @@ -381,7 +388,7 @@ struct MapViewSwiftUI: UIViewRepresentable { var minimumZoomLevel: Int? var maximumZoomLevel: Int? let defaultTile: DefaultTile? - + public init( mapName: String, tileType: String, @@ -397,7 +404,7 @@ struct MapViewSwiftUI: UIViewRepresentable { self.maximumZoomLevel = maximumZoomLevel self.defaultTile = defaultTile } - + public init?( mapName: String?, tileType: String, @@ -417,15 +424,15 @@ struct MapViewSwiftUI: UIViewRepresentable { self.defaultTile = defaultTile } } - + public class CustomMapOverlaySource: MKTileOverlay { - + // requires folder: tiles/{mapName}/z/y/y,{tileType} private var parent: MapViewSwiftUI private let mapName: String private let tileType: String private let defaultTile: DefaultTile? - + public init( parent: MapViewSwiftUI, mapName: String, @@ -438,7 +445,7 @@ struct MapViewSwiftUI: UIViewRepresentable { self.defaultTile = defaultTile super.init(urlTemplate: "") } - + public override func url(forTilePath path: MKTileOverlayPath) -> URL { if let tileUrl = Bundle.main.url( forResource: "\(path.y)", @@ -460,31 +467,4 @@ struct MapViewSwiftUI: UIViewRepresentable { } } } - -// public struct Overlay { -// -// public static func == (lhs: MapViewSwiftUI.Overlay, rhs: MapViewSwiftUI.Overlay) -> Bool { -// // maybe to use in the future for comparison of full array -// lhs.shape.coordinate.latitude == rhs.shape.coordinate.latitude && -// lhs.shape.coordinate.longitude == rhs.shape.coordinate.longitude && -// lhs.fillColor == rhs.fillColor -// } -// -// var shape: MKOverlay -// var fillColor: UIColor? -// var strokeColor: UIColor? -// var lineWidth: CGFloat -// -// public init( -// shape: MKOverlay, -// fillColor: UIColor? = nil, -// strokeColor: UIColor? = nil, -// lineWidth: CGFloat = 0 -// ) { -// self.shape = shape -// self.fillColor = fillColor -// self.strokeColor = strokeColor -// self.lineWidth = lineWidth -// } -// } } diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 98ba306b..3ed3ad81 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -105,7 +105,7 @@ struct DeviceMetricsLog: View { ForEach(node.telemetries?.reversed() as? [TelemetryEntity] ?? [], id: \.self) { (dm: TelemetryEntity) in if dm.metricsType == 0 { GridRow { - if dm.batteryLevel == 0 { + if dm.batteryLevel == 111 { Text("USB") .font(.caption) } else { diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 38544cde..f7c8ab6f 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -60,7 +60,8 @@ struct NodeDetail: View { if node.positions?.count ?? 0 > 0 { ZStack { let positionArray = node.positions?.array as? [PositionEntity] ?? [] - let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) } + let lastThousand = Array(positionArray.prefix(1000)) + // let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) } ZStack { MapViewSwiftUI(onLongPress: { coord in waypointCoordinate = coord @@ -71,7 +72,7 @@ struct NodeDetail: View { editingWaypoint = wpId presentingWaypointForm = true } - }, positions: todaysPositions, waypoints: Array(waypoints), + }, positions: lastThousand, waypoints: Array(waypoints), mapViewType: mapType, userTrackingMode: MKUserTrackingMode.none, showNodeHistory: meshMapShowNodeHistory, diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index b6f1adfd..a9ffa68e 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -232,7 +232,7 @@ struct NodeInfoView: View { } else { Text("Not Connected") .multilineTextAlignment(.leading) - .font(.callout) + .font(.caption) .fontWeight(.semibold) .foregroundStyle(.tint) } From 74e7ac5dd80fbe2b1d058f8c1079b32bd7efd06f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 24 Mar 2023 23:06:34 -0700 Subject: [PATCH 05/21] Update initial map centering --- Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 91f841b1..ed4a8695 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -43,13 +43,6 @@ struct MapViewSwiftUI: UIViewRepresentable { let center = (latest.count > 0 && userTrackingMode == MKUserTrackingMode.none) ? latest[0].coordinate : LocationHelper.currentLocation let region = MKCoordinateRegion(center: center, span: span) mapView.setRegion(region, animated: true) - // Set user (phone gps) tracking options - mapView.setUserTrackingMode(userTrackingMode, animated: true) - if userTrackingMode == MKUserTrackingMode.none { - mapView.showsUserLocation = false - } else { - mapView.showsUserLocation = true - } mapView.addAnnotations(showNodeHistory ? positions : latest) // Set user (phone gps) tracking options mapView.setUserTrackingMode(userTrackingMode, animated: true) @@ -59,7 +52,6 @@ struct MapViewSwiftUI: UIViewRepresentable { } else { mapView.showsUserLocation = true } - mapView.addAnnotations(showNodeHistory ? positions : latest) // Other MKMapView Settings mapView.preferredConfiguration.elevationStyle = .realistic// .flat mapView.isPitchEnabled = true @@ -125,6 +117,7 @@ struct MapViewSwiftUI: UIViewRepresentable { .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } let annotationCount = waypoints.count + (showNodeHistory || showRouteLines ? positions.count : latest.count) if annotationCount > mapView.annotations.count { + print("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)") if showRouteLines { // Remove all existing PolyLine Overlays for overlay in mapView.overlays { From 6945144cf68b5f47f4689c8a0956fde472f521a8 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 24 Mar 2023 23:16:34 -0700 Subject: [PATCH 06/21] set the initial node info position as the latest --- Meshtastic/Helpers/MeshPackets.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index d329dceb..10f468a3 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -279,6 +279,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje if nodeInfo.position.longitudeI > 0 || nodeInfo.position.latitudeI > 0 && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) { let position = PositionEntity(context: context) + position.latest = true position.seqNo = Int32(nodeInfo.position.seqNumber) position.latitudeI = nodeInfo.position.latitudeI position.longitudeI = nodeInfo.position.longitudeI From 738bb175510c60de9ec5ccbdccdb6867f678f9b4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 24 Mar 2023 23:56:31 -0700 Subject: [PATCH 07/21] Show all annotations when there is more than one node --- Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index ed4a8695..5833d35e 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -47,7 +47,11 @@ struct MapViewSwiftUI: UIViewRepresentable { // Set user (phone gps) tracking options mapView.setUserTrackingMode(userTrackingMode, animated: true) if userTrackingMode == MKUserTrackingMode.none { - mapView.fitAllAnnotations() + if latest.count == 1 { + mapView.fit(annotations:showNodeHistory ? positions : latest, andShow: false) + } else { + mapView.fitAllAnnotations() + } mapView.showsUserLocation = false } else { mapView.showsUserLocation = true @@ -151,8 +155,8 @@ struct MapViewSwiftUI: UIViewRepresentable { mapView.showsUserLocation = false mapView.addAnnotations(showNodeHistory ? positions : latest) if recenter { - if showRouteLines || showNodeHistory { - mapView.fit(annotations: showNodeHistory ? positions : latest, andShow: false) + if latest.count == 1 { + mapView.fit(annotations:showNodeHistory ? positions : latest, andShow: false) } else { mapView.fitAllAnnotations() } From e2796aac92528db4c543d598489ddc9aeebb0bf9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 25 Mar 2023 00:41:57 -0700 Subject: [PATCH 08/21] Add wake on tap or motion to display config --- Meshtastic.xcodeproj/project.pbxproj | 4 +- .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 306 ++++++++++++++++++ .../Views/Settings/Config/DisplayConfig.swift | 17 +- 4 files changed, 326 insertions(+), 3 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV10.xcdatamodel/contents diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index a0ecd907..b7ab23f0 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -285,6 +285,7 @@ DDC3B273283F411B00AC321C /* LastHeardText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastHeardText.swift; sourceTree = ""; }; DDC4D567275499A500A4208E /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryLevel.swift; sourceTree = ""; }; + DDC94FC329CED7280082EA6E /* MeshtasticDataModelV10.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV10.xcdatamodel; sourceTree = ""; }; DDCDC69A29467643004C1DDA /* MeshtasticDataModelV3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV3.xcdatamodel; sourceTree = ""; }; DDCDC6CC29481FCC004C1DDA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; DDCDC6CE294821AD004C1DDA /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; @@ -1475,6 +1476,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DDC94FC329CED7280082EA6E /* MeshtasticDataModelV10.xcdatamodel */, DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */, DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */, DD5E51CC2986643400D21B61 /* MeshtasticDataModelV7.xcdatamodel */, @@ -1485,7 +1487,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */; + currentVersion = DDC94FC329CED7280082EA6E /* MeshtasticDataModelV10.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 6e12995e..ff97f8c7 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV9.xcdatamodel + MeshtasticDataModelV10.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV10.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV10.xcdatamodel/contents new file mode 100644 index 00000000..196ac203 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV10.xcdatamodel/contents @@ -0,0 +1,306 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index 8a595a11..99d7e792 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -22,6 +22,7 @@ struct DisplayConfig: View { @State var screenCarouselInterval = 0 @State var gpsFormat = 0 @State var compassNorthTop = false + @State var wakeOnTapOrMotion = false @State var flipScreen = false @State var oledType = 0 @State var displayMode = 0 @@ -72,7 +73,14 @@ struct DisplayConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Text("The compass heading on the screen outside of the circle will always point north.") .font(.caption) - + + Toggle(isOn: $wakeOnTapOrMotion) { + Label("Wake Screen on tap or motion", systemImage: "gyroscope") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("Requires that there be an accelerometer on your device.") + .font(.caption) + Toggle(isOn: $flipScreen) { Label("Flip Screen", systemImage: "pip.swap") @@ -151,6 +159,7 @@ struct DisplayConfig: View { dc.screenOnSecs = UInt32(screenOnSeconds) dc.autoScreenCarouselSecs = UInt32(screenCarouselInterval) dc.compassNorthTop = compassNorthTop + dc.wakeOnTapOrMotion = wakeOnTapOrMotion dc.flipScreen = flipScreen dc.oled = OledTypes(rawValue: oledType)!.protoEnumValue() dc.displaymode = DisplayModes(rawValue: displayMode)!.protoEnumValue() @@ -202,6 +211,11 @@ struct DisplayConfig: View { if newCompassNorthTop != node!.displayConfig!.compassNorthTop { hasChanges = true } } } + .onChange(of: wakeOnTapOrMotion) { newWakeOnTapOrMotion in + if node != nil && node!.displayConfig != nil { + if newWakeOnTapOrMotion != node!.displayConfig!.wakeOnTapOrMotion { hasChanges = true } + } + } .onChange(of: gpsFormat) { newGpsFormat in if node != nil && node!.displayConfig != nil { if newGpsFormat != node!.displayConfig!.gpsFormat { hasChanges = true } @@ -229,6 +243,7 @@ struct DisplayConfig: View { self.screenOnSeconds = Int(node?.displayConfig?.screenOnSeconds ?? 0) self.screenCarouselInterval = Int(node?.displayConfig?.screenCarouselInterval ?? 0) self.compassNorthTop = node?.displayConfig?.compassNorthTop ?? false + self.wakeOnTapOrMotion = node?.displayConfig?.wakeOnTapOrMotion ?? false self.flipScreen = node?.displayConfig?.flipScreen ?? false self.oledType = Int(node?.displayConfig?.oledType ?? 0) self.displayMode = Int(node?.displayConfig?.displayMode ?? 0) From 9a42b4fb64562d0497e26ebea461cca5032ffe98 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 25 Mar 2023 14:30:18 -0700 Subject: [PATCH 09/21] ringtone config --- Meshtastic.xcodeproj/project.pbxproj | 4 + Meshtastic/Helpers/BLEManager.swift | 54 +++++++ .../contents | 5 + Meshtastic/Persistence/UpdateCoreData.swift | 39 +++++ Meshtastic/Protobufs/meshtastic/mesh.pb.swift | 10 ++ .../Settings/Config/Module/RtttlConfig.swift | 145 ++++++++++++++++++ Meshtastic/Views/Settings/Settings.swift | 10 +- 7 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index b7ab23f0..4e131874 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -110,6 +110,7 @@ DDC4D568275499A500A4208E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC4D567275499A500A4208E /* Persistence.swift */; }; DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */; }; DDC94FC229CE063B0082EA6E /* BatteryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */; }; + DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FCD29CF55310082EA6E /* RtttlConfig.swift */; }; DDCDC6CB29481FCC004C1DDA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DDCDC6CD29481FCC004C1DDA /* Localizable.strings */; }; DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */; }; DDD3BBD5292D763200D609B3 /* MeshtasticTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */; }; @@ -286,6 +287,7 @@ DDC4D567275499A500A4208E /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryLevel.swift; sourceTree = ""; }; DDC94FC329CED7280082EA6E /* MeshtasticDataModelV10.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV10.xcdatamodel; sourceTree = ""; }; + DDC94FCD29CF55310082EA6E /* RtttlConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RtttlConfig.swift; sourceTree = ""; }; DDCDC69A29467643004C1DDA /* MeshtasticDataModelV3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV3.xcdatamodel; sourceTree = ""; }; DDCDC6CC29481FCC004C1DDA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; DDCDC6CE294821AD004C1DDA /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; @@ -459,6 +461,7 @@ DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */, DD2160AE28C5552500C17253 /* MQTTConfig.swift */, DD41582928585C32009B0E59 /* RangeTestConfig.swift */, + DDC94FCD29CF55310082EA6E /* RtttlConfig.swift */, DD6193782863875F00E59241 /* SerialConfig.swift */, DD415827285859C4009B0E59 /* TelemetryConfig.swift */, ); @@ -904,6 +907,7 @@ DDC4D568275499A500A4208E /* Persistence.swift in Sources */, DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */, DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */, + DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */, DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */, diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index c5471674..5347d8b0 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1343,6 +1343,33 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return 0 } + + public func saveRtttlConfig(config: RTTTLConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { + + var adminPacket = AdminMessage() + adminPacket.setRingtoneMessage = config.ringtone + + var meshPacket: MeshPacket = MeshPacket() + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { @@ -1736,6 +1763,33 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return false } + + public func requestRtttlConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { + + var adminPacket = AdminMessage() + adminPacket.getRingtoneRequest = true + + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV10.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV10.xcdatamodel/contents index 196ac203..a6ce4ed4 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV10.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV10.xcdatamodel/contents @@ -199,6 +199,7 @@ + @@ -240,6 +241,10 @@ + + + + diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 639a2bb8..40b210f1 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -699,6 +699,45 @@ func upsertExternalNotificationModuleConfigPacket(config: Meshtastic.ModuleConfi } } +func upsertRtttlConfigPacket(config: RTTTLConfig, nodeNum: Int64, context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.rangetest.config %@", comment: "Range Test module config received: %@"), String(nodeNum)) + MeshLogger.log("⛰️ \(logString)") + + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + + guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else { + return + } + // Found a node, save RTTTL Config + if !fetchedNode.isEmpty { + if fetchedNode[0].rtttlConfig == nil { + let newRtttlConfig = RTTTLConfigEntity(context: context) + newRtttlConfig.ringtone = config.ringtone + fetchedNode[0].rtttlConfig = newRtttlConfig + } else { + fetchedNode[0].rtttlConfig?.ringtone = config.ringtone + } + do { + try context.save() + print("💾 Updated RTTTL Ringtone Config for node number: \(String(nodeNum))") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Updating Core Data RtttlConfigEntity: \(nsError)") + } + } else { + print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save RTTTL Ringtone Config") + } + } catch { + let nsError = error as NSError + print("💥 Fetching node for core data RtttlConfigEntity failed: \(nsError)") + } +} + func upsertMqttModuleConfigPacket(config: Meshtastic.ModuleConfig.MQTTConfig, nodeNum: Int64, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.mqtt.config %@", comment: "MQTT module config received: %@"), String(nodeNum)) diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index 7fa22878..03b6c4d1 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -1587,6 +1587,10 @@ struct NodeInfo { /// Clears the value of `deviceMetrics`. Subsequent reads from it will return its default value. mutating func clearDeviceMetrics() {self._deviceMetrics = nil} + /// + /// local channel index we heard that node on. Only populated if its not the default channel. + var channel: UInt32 = 0 + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -3181,6 +3185,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB 4: .same(proto: "snr"), 5: .standard(proto: "last_heard"), 6: .standard(proto: "device_metrics"), + 7: .same(proto: "channel"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -3195,6 +3200,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB case 4: try { try decoder.decodeSingularFloatField(value: &self.snr) }() case 5: try { try decoder.decodeSingularFixed32Field(value: &self.lastHeard) }() case 6: try { try decoder.decodeSingularMessageField(value: &self._deviceMetrics) }() + case 7: try { try decoder.decodeSingularUInt32Field(value: &self.channel) }() default: break } } @@ -3223,6 +3229,9 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB try { if let v = self._deviceMetrics { try visitor.visitSingularMessageField(value: v, fieldNumber: 6) } }() + if self.channel != 0 { + try visitor.visitSingularUInt32Field(value: self.channel, fieldNumber: 7) + } try unknownFields.traverse(visitor: &visitor) } @@ -3233,6 +3242,7 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB if lhs.snr != rhs.snr {return false} if lhs.lastHeard != rhs.lastHeard {return false} if lhs._deviceMetrics != rhs._deviceMetrics {return false} + if lhs.channel != rhs.channel {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift new file mode 100644 index 00000000..1528e307 --- /dev/null +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -0,0 +1,145 @@ +// +// RingtoneConfig.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 3/25/23. +// + +import SwiftUI + +struct RtttlConfig: View { + + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + @Environment(\.dismiss) private var goBack + + var node: NodeInfoEntity? + + @State private var isPresentingSaveConfirm: Bool = false + @State var hasChanges = false + @State var ringtone: String = "" + + var body: some View { + VStack { + Form { + if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { + Text("There has been no response to a request for device metadata over the admin channel for this node.") + .font(.callout) + .foregroundColor(.orange) + + } else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { + // Let users know what is going on if they are using remote admin and don't have the config yet + if node?.rangeTestConfig == nil { + Text("RTTTL Ringtone config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.") + .font(.callout) + .foregroundColor(.orange) + } else { + Text("Remote administration for: \(node?.user?.longName ?? "Unknown")") + .font(.title3) + .onAppear { + setRtttLConfigValue() + } + } + } else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 { + Text("Configuration for: \(node?.user?.longName ?? "Unknown")") + .font(.title3) + } else { + Text("Please connect to a radio to configure settings.") + .font(.callout) + .foregroundColor(.orange) + } + Section(header: Text("options")) { + + HStack { + Label("RTTTL Ringtone", systemImage: "music.quarternote.3") + TextField("Ringtone Transfer Language", text: $ringtone, axis: .vertical) + .foregroundColor(.gray) + .autocapitalization(.none) + .disableAutocorrection(true) + .onChange(of: ringtone, perform: { _ in + + let totalBytes = ringtone.utf8.count + // Only mess with the value if it is too big + if totalBytes > 228 { + + let firstNBytes = Data(ringtone.utf8.prefix(228)) + if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { + // Set the ringtone back to the last place where it was the right size + ringtone = maxBytesString + } + } + }) + .foregroundColor(.gray) + } + .keyboardType(.default) + Text("Ringtone Transfer Language(RTTTL) Ringtone String used by supported buzzers in external notifications.") + .font(.caption) + } + } + .disabled(self.bleManager.connectedPeripheral == nil || node?.rtttlConfig == nil) + Button { + isPresentingSaveConfirm = true + } label: { + Label("save", systemImage: "square.and.arrow.down") + } + .disabled(bleManager.connectedPeripheral == nil || !hasChanges || !(node?.myInfo?.hasWifi ?? false)) + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .confirmationDialog( + "are.you.sure", + isPresented: $isPresentingSaveConfirm, + titleVisibility: .visible + ) { + let nodeName = node?.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown") + let buttonText = String.localizedStringWithFormat(NSLocalizedString("save.config %@", comment: "Save Config for %@"), nodeName) + Button(buttonText) { + + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) + if connectedNode != nil { + var rtttl = RTTTLConfig() + rtttl.ringtone = ringtone + let adminMessageId = bleManager.saveRtttlConfig(config: rtttl, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + if adminMessageId > 0 { + // Should show a saved successfully alert once I know that to be true + // for now just disable the button after a successful save + hasChanges = false + goBack() + } + } + } + } + message: { + Text("config.save.confirm") + } + .navigationTitle("rtttl.config") + .navigationBarItems(trailing: + ZStack { + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") + }) + .onAppear { + self.bleManager.context = context + setRtttLConfigValue() + + // Need to request a Rtttl Config from the remote node before allowing changes + if bleManager.connectedPeripheral != nil && node?.rangeTestConfig == nil { + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) + if node != nil && connectedNode != nil { + _ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + } + } + } + .onChange(of: ringtone) { newRingtone in + if node != nil && node!.rtttlConfig != nil { + if newRingtone != node!.rtttlConfig!.ringtone { hasChanges = true } + } + } + } + } + + func setRtttLConfigValue() { + self.ringtone = node?.rtttlConfig?.ringtone ?? "" + self.hasChanges = false + } +} diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 6074fb0c..b5c8fbd6 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -35,6 +35,7 @@ struct Settings: View { case externalNotificationConfig case mqttConfig case rangeTestConfig + case ringtoneConfig case serialConfig case telemetryConfig case meshLog @@ -229,7 +230,14 @@ struct Settings: View { .symbolRenderingMode(.hierarchical) Text("range.test") } - .tag(SettingsSidebar.rangeTestConfig) + NavigationLink { + RtttlConfig(node: nodes.first(where: { $0.num == selectedNode })) + } label: { + Image(systemName: "music.note.list") + .symbolRenderingMode(.hierarchical) + Text("ringtone") + } + .tag(SettingsSidebar.ringtoneConfig) NavigationLink { SerialConfig(node: nodes.first(where: { $0.num == selectedNode })) From c496be722b0cd154ea276727f908f592f5180288 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 25 Mar 2023 15:18:44 -0700 Subject: [PATCH 10/21] Hook up admin message rtttl request --- Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index 1528e307..40fafcde 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -126,7 +126,7 @@ struct RtttlConfig: View { if bleManager.connectedPeripheral != nil && node?.rangeTestConfig == nil { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if node != nil && connectedNode != nil { - _ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + _ = bleManager.requestRtttlConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) } } } From e6d6efe495561b596d45814c711fc78b7ad0cd22 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 25 Mar 2023 22:14:39 -0700 Subject: [PATCH 11/21] Additional RTTTL config --- Meshtastic/Helpers/BLEManager.swift | 6 +++--- Meshtastic/Helpers/MeshPackets.swift | 3 +++ Meshtastic/Persistence/UpdateCoreData.swift | 6 +++--- Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift | 6 ++---- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 5347d8b0..40d69a25 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1344,10 +1344,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { return 0 } - public func saveRtttlConfig(config: RTTTLConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { + public func saveRtttlConfig(ringtone: String, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() - adminPacket.setRingtoneMessage = config.ringtone + adminPacket.setRingtoneMessage = ringtone var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save @@ -123,7 +121,7 @@ struct RtttlConfig: View { setRtttLConfigValue() // Need to request a Rtttl Config from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.rangeTestConfig == nil { + if bleManager.connectedPeripheral != nil && node?.rtttlConfig == nil { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if node != nil && connectedNode != nil { _ = bleManager.requestRtttlConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) From 002e6d6986bad9c3d889d382d84f353f9081085b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 26 Mar 2023 07:11:26 -0700 Subject: [PATCH 12/21] Add more line colors --- Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 5833d35e..776e98a1 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -16,7 +16,8 @@ struct MapViewSwiftUI: UIViewRepresentable { var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void var onWaypointEdit: (_ waypointId: Int ) -> Void let mapView = MKMapView() - let colors: [UIColor] = [UIColor.systemIndigo, UIColor.orange, UIColor.green, UIColor.brown, UIColor.purple, UIColor.systemMint, UIColor.cyan, UIColor.magenta, UIColor.systemPink, UIColor.blue] + let lineColors: [UIColor] = [UIColor.systemIndigo, UIColor.yellow, UIColor.white, UIColor.red, UIColor.purple, UIColor.orange, UIColor.magenta, UIColor.lightGray, UIColor.green, UIColor.gray, UIColor.systemMint, UIColor.darkGray, UIColor.cyan, UIColor.brown, UIColor.blue, UIColor.black, UIColor.systemPink, + UIColor.systemTeal] // Parameters let positions: [PositionEntity] let waypoints: [WaypointEntity] @@ -141,8 +142,8 @@ struct MapViewSwiftUI: UIViewRepresentable { polyline.title = "\(String(position.nodePosition?.num ?? 0))-\(String(lineIndex))" mapView.addOverlay(polyline) lineIndex += 1 - // There are 10 colors for lines, start over if we are at index 10 - if lineIndex > 9 { + // There are 18 colors for lines, start over if we are at index 17 + if lineIndex > 17 { lineIndex = 0 } } @@ -358,7 +359,7 @@ struct MapViewSwiftUI: UIViewRepresentable { let titleString = routePolyline.title ?? "None-0" let index = Int(titleString.components(separatedBy: "-").last ?? "0") let renderer = MKPolylineRenderer(polyline: routePolyline) - renderer.strokeColor = parent.colors[index ?? 0] + renderer.strokeColor = parent.lineColors[index ?? 0] renderer.lineWidth = 5 return renderer } From cfa4c03faa39f3c5aa00d648e3d68d993beddb88 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 26 Mar 2023 09:08:08 -0700 Subject: [PATCH 13/21] Move notification check to init Clean up initial map centering --- Meshtastic/Persistence/Persistence.swift | 4 +- .../Persistence/PositionEntityExtension.swift | 7 +++ .../Persistence/UserEntityExtension.swift | 2 +- Meshtastic/Views/Bluetooth/Connect.swift | 49 ++++++------------- .../Views/Map/Custom/MapViewSwiftUI.swift | 13 ++--- 5 files changed, 33 insertions(+), 42 deletions(-) diff --git a/Meshtastic/Persistence/Persistence.swift b/Meshtastic/Persistence/Persistence.swift index d4a86742..1c768ccf 100644 --- a/Meshtastic/Persistence/Persistence.swift +++ b/Meshtastic/Persistence/Persistence.swift @@ -1,8 +1,8 @@ // // Persistence.swift -// CoreDataSample +// Meshtastic // -// Created by Garth Vander Houwen on 11/28/21. +// Copyright(c) Garth Vander Houwen 11/28/21. // import CoreData diff --git a/Meshtastic/Persistence/PositionEntityExtension.swift b/Meshtastic/Persistence/PositionEntityExtension.swift index b671e6e8..75ba3f4b 100644 --- a/Meshtastic/Persistence/PositionEntityExtension.swift +++ b/Meshtastic/Persistence/PositionEntityExtension.swift @@ -1,3 +1,10 @@ +// +// PersistenceEntityExtenstion.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 11/28/21. +// + import CoreData import CoreLocation import MapKit diff --git a/Meshtastic/Persistence/UserEntityExtension.swift b/Meshtastic/Persistence/UserEntityExtension.swift index 1d2dd3da..6d9f5699 100644 --- a/Meshtastic/Persistence/UserEntityExtension.swift +++ b/Meshtastic/Persistence/UserEntityExtension.swift @@ -1,6 +1,6 @@ // // UserEntityExtension.swift -// MeshtasticApple +// Meshtastic // // Copyright(c) Garth Vander Houwen 6/3/22. // diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 58711e4e..6ee95a32 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -26,6 +26,20 @@ struct Connect: View { @State var presentingSwitchPreferredPeripheral = false @State var selectedPeripherialId = "" + init () { + let notificationCenter = UNUserNotificationCenter.current() + notificationCenter.getNotificationSettings(completionHandler: { (settings) in + if settings.authorizationStatus == .notDetermined { + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in + if success { + print("Notifications are all set!") + } else if let error = error { + print(error.localizedDescription) + } + } + } + }) + } var body: some View { NavigationStack { @@ -279,18 +293,9 @@ struct Connect: View { .onAppear(perform: { self.bleManager.context = context self.bleManager.userSettings = userSettings - - // Ask for notification permission - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in - if success { - print("Notifications are all set!") - } else if let error = error { - print(error.localizedDescription) - } - } }) } -#if canImport(ActivityKit) + #if canImport(ActivityKit) func startNodeActivity() { if #available(iOS 16.2, *) { liveActivityStarted = true @@ -330,29 +335,7 @@ struct Connect: View { } } } -#endif - -#if os(iOS) - func postNotification() { - let timerSeconds = 60 - let content = UNMutableNotificationContent() - content.title = "Mesh Live Activity Over" - content.body = "Your timed mesh live activity is over." - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(timerSeconds), repeats: false) - let uuidString = UUID().uuidString - let request = UNNotificationRequest(identifier: uuidString, - content: content, trigger: trigger) - let notificationCenter = UNUserNotificationCenter.current() - notificationCenter.add(request) { (error) in - if error != nil { - // Handle any errors. - print("Error posting local notification: \(error?.localizedDescription ?? "no description")") - } else { - print("Posted local notification.") - } - } - } -#endif + #endif func didDismissSheet() { bleManager.disconnectPeripheral(reconnect: false) diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 776e98a1..71b4bbf2 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -43,8 +43,8 @@ struct MapViewSwiftUI: UIViewRepresentable { let span = MKCoordinateSpan(latitudeDelta: 0.003, longitudeDelta: 0.003) let center = (latest.count > 0 && userTrackingMode == MKUserTrackingMode.none) ? latest[0].coordinate : LocationHelper.currentLocation let region = MKCoordinateRegion(center: center, span: span) - mapView.setRegion(region, animated: true) mapView.addAnnotations(showNodeHistory ? positions : latest) + mapView.setRegion(region, animated: true) // Set user (phone gps) tracking options mapView.setUserTrackingMode(userTrackingMode, animated: true) if userTrackingMode == MKUserTrackingMode.none { @@ -120,7 +120,7 @@ struct MapViewSwiftUI: UIViewRepresentable { let latest = positions .filter { $0.latest == true } .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } - let annotationCount = waypoints.count + (showNodeHistory || showRouteLines ? positions.count : latest.count) + let annotationCount = waypoints.count + (showNodeHistory ? positions.count : latest.count) if annotationCount > mapView.annotations.count { print("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)") if showRouteLines { @@ -150,23 +150,24 @@ struct MapViewSwiftUI: UIViewRepresentable { } mapView.removeAnnotations(mapView.annotations) mapView.addAnnotations(waypoints) - mapView.setUserTrackingMode(userTrackingMode, animated: true) - if userTrackingMode == MKUserTrackingMode.none { mapView.showsUserLocation = false - mapView.addAnnotations(showNodeHistory ? positions : latest) if recenter { if latest.count == 1 { - mapView.fit(annotations:showNodeHistory ? positions : latest, andShow: false) + mapView.fit(annotations:showNodeHistory ? positions : latest, andShow: true) } else { + mapView.addAnnotations(showNodeHistory ? positions : latest) mapView.fitAllAnnotations() } + } else { + mapView.addAnnotations(showNodeHistory ? positions : latest) } } else { // Centering Done by tracking mode mapView.addAnnotations(showNodeHistory ? positions : latest) mapView.showsUserLocation = true } + mapView.setUserTrackingMode(userTrackingMode, animated: true) } } } From 7e429b79eb49ac27f0619b65d837a9853e506ca1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 26 Mar 2023 09:46:51 -0700 Subject: [PATCH 14/21] Ringtone cleanup, translation strings --- Meshtastic/Persistence/UpdateCoreData.swift | 2 +- Meshtastic/Views/Bluetooth/Connect.swift | 2 +- .../Views/Settings/Config/Module/RtttlConfig.swift | 9 ++++----- de.lproj/Localizable.strings | 4 ++++ en.lproj/Localizable.strings | 6 +++++- zh-Hans.lproj/Localizable.strings | 4 ++++ 6 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 1b8789a9..e4d33347 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -701,7 +701,7 @@ func upsertExternalNotificationModuleConfigPacket(config: Meshtastic.ModuleConfi func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, context: NSManagedObjectContext) { - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.rangetest.config %@", comment: "Range Test module config received: %@"), String(nodeNum)) + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.ringtone.config %@", comment: "RTTTL Ringtone config received: %@"), String(nodeNum)) MeshLogger.log("⛰️ \(logString)") let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 6ee95a32..57594b2f 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -104,7 +104,7 @@ struct Connect: View { #endif } } label: { - Label("Mesh Live Activity", systemImage: liveActivityStarted ? "stop" : "play") + Label("mesh.live.activity", systemImage: liveActivityStarted ? "stop" : "play") } } #endif diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index 1f91c257..cd056ada 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -29,7 +29,7 @@ struct RtttlConfig: View { } else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { // Let users know what is going on if they are using remote admin and don't have the config yet - if node?.rangeTestConfig == nil { + if node?.rtttlConfig == nil { Text("RTTTL Ringtone config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.") .font(.callout) .foregroundColor(.orange) @@ -82,7 +82,7 @@ struct RtttlConfig: View { } label: { Label("save", systemImage: "square.and.arrow.down") } - .disabled(bleManager.connectedPeripheral == nil || !hasChanges || !(node?.myInfo?.hasWifi ?? false)) + .disabled(bleManager.connectedPeripheral == nil || !hasChanges) .buttonStyle(.bordered) .buttonBorderShape(.capsule) .controlSize(.large) @@ -111,7 +111,7 @@ struct RtttlConfig: View { message: { Text("config.save.confirm") } - .navigationTitle("rtttl.config") + .navigationTitle("ringtone.config") .navigationBarItems(trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") @@ -119,9 +119,8 @@ struct RtttlConfig: View { .onAppear { self.bleManager.context = context setRtttLConfigValue() - // Need to request a Rtttl Config from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.rtttlConfig == nil { + if bleManager.connectedPeripheral != nil && (node?.rtttlConfig == nil || node?.rtttlConfig?.ringtone?.count ?? 0 == 0) { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if node != nil && connectedNode != nil { _ = bleManager.requestRtttlConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index c535db76..d42c9caa 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -144,6 +144,7 @@ "map.usertrackingmode.none"="None"; "map.usertrackingmode.follow"="Follow"; "map.usertrackingmode.followwithheading"="Follow with heading"; +"mesh.live.activity"="Mesh Live Activity"; "mesh.log"="Mesh Log"; "mesh.log.bluetooth.config %@"="Bluetooth Konfiguration empfangen: %@"; "mesh.log.cannedmessage.config %@"="Canned Message module config received: %@"; @@ -165,6 +166,7 @@ "mesh.log.position.config %@"="Positions Konfiguration empfangen: %@"; "mesh.log.position.received %@"="Positionspaket empfangen von Node: %@"; "mesh.log.rangetest.config %@"="Range Test Modul konfiguration empfangen: %@"; +"mesh.log.ringtone.config %@"="RTTTL Ringtone config received: %@"; "mesh.log.routing.message %@ %@"="Routing empfangen für RequestID: %@ Ack Status: %@"; "mesh.log.serial.config %@"="Serial Modul Konfiguration empfangen: %@"; "mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@"; @@ -212,6 +214,8 @@ "reply"="Antworten"; "received.ack"="Empfangsbestätigung"; "received.ack.real"="Recipient Ack"; +"ringtone"="Ringtone"; +"ringtone.config"="Ringtone Config"; "routing.acknowledged"="Bestätigt"; "routing.noroute"="Keine Route"; "routing.gotnak"="Negative Empfangsbestätigung empfangen"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index df67d0e9..8183f1cf 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -49,8 +49,8 @@ "clear.log"="Clear Log"; "close"="Close"; "config.save.confirm"="After config values save the node will reboot."; -"connected.radio"="Connected Radio"; "communicating"="Communicating with device. ."; +"connected.radio"="Connected Radio"; "connected"="Currently Connected"; "connecting"="Connecting . ."; "contacts"="Contacts"; @@ -144,6 +144,7 @@ "map.usertrackingmode.follow"="Follow"; "map.usertrackingmode.followwithheading"="Follow with heading"; "map.usertrackingmode.none"="None"; +"mesh.live.activity"="Mesh Live Activity"; "mesh.log"="Mesh Log"; "mesh.log.bluetooth.config %@"="Bluetooth config received: %@"; "mesh.log.cannedmessage.config %@"="Canned Message module config received: %@"; @@ -165,6 +166,7 @@ "mesh.log.position.config %@"="Positon config received: %@"; "mesh.log.position.received %@"="Position Packet received from node: %@"; "mesh.log.rangetest.config %@"="Range Test module config received: %@"; +"mesh.log.ringtone.config %@"="RTTTL Ringtone config received: %@"; "mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@"; "mesh.log.serial.config %@"="Serial module config received: %@"; "mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@"; @@ -212,6 +214,8 @@ "reboot.node"="Reboot node?"; "received.ack"="Received Ack"; "received.ack.real"="Recipient Ack"; +"ringtone"="Ringtone"; +"ringtone.config"="Ringtone Config"; "routing.acknowledged"="Acknowledged"; "routing.noroute"="No Route"; "routing.gotnak"="Received a negative acknowledgment"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index e2f07a11..f0f54149 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -144,6 +144,7 @@ "map.usertrackingmode.none"="None"; "map.usertrackingmode.follow"="Follow"; "map.usertrackingmode.followwithheading"="Follow with heading"; +"mesh.live.activity"="Mesh Live Activity"; "mesh.log"="Mesh 日志"; "mesh.log.bluetooth.config %@"="Bluetooth config received: %@"; "mesh.log.cannedmessage.config %@"="Canned Message module config received: %@"; @@ -165,6 +166,7 @@ "mesh.log.position.config %@"="Positon config received: %@"; "mesh.log.position.received %@"="Position Packet received from node: %@"; "mesh.log.rangetest.config %@"="Range Test module config received: %@"; +"mesh.log.ringtone.config %@"="RTTTL Ringtone config received: %@"; "mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@"; "mesh.log.serial.config %@"="Serial module config received: %@"; "mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@"; @@ -212,6 +214,8 @@ "reboot.node"="重启节点?"; "received.ack"="收到确认"; "received.ack.real"="收件人确认"; +"ringtone"="Ringtone"; +"ringtone.config"="Ringtone Config"; "routing.acknowledged"="确认"; "routing.noroute"="找不到目标"; "routing.gotnak"="收到否认"; From 7f207bff27625448f4fc8c7f27d973ddb9c616c9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 27 Mar 2023 10:43:01 -0700 Subject: [PATCH 15/21] Smart broadcast updates --- .../contents | 2 + Meshtastic/Persistence/UpdateCoreData.swift | 8 ++ .../Protobufs/meshtastic/config.pb.swift | 20 ++++ .../Settings/Config/Module/RtttlConfig.swift | 2 +- .../Settings/Config/PositionConfig.swift | 98 +++++++++++++++++-- 5 files changed, 121 insertions(+), 9 deletions(-) diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV10.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV10.xcdatamodel/contents index a6ce4ed4..bd6cce58 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV10.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV10.xcdatamodel/contents @@ -211,6 +211,8 @@ + + diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index e4d33347..d881e082 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -535,19 +535,27 @@ func upsertPositionConfigPacket(config: Meshtastic.Config.PositionConfig, nodeNu let newPositionConfig = PositionConfigEntity(context: context) newPositionConfig.smartPositionEnabled = config.positionBroadcastSmartEnabled newPositionConfig.deviceGpsEnabled = config.gpsEnabled + newPositionConfig.rxGpio = Int32(config.rxGpio) + newPositionConfig.txGpio = Int32(config.txGpio) newPositionConfig.fixedPosition = config.fixedPosition newPositionConfig.gpsUpdateInterval = Int32(config.gpsUpdateInterval) newPositionConfig.gpsAttemptTime = Int32(config.gpsAttemptTime) newPositionConfig.positionBroadcastSeconds = Int32(config.positionBroadcastSecs) + newPositionConfig.broadcastSmartMinimumIntervalSecs = Int32(config.broadcastSmartMinimumIntervalSecs) + newPositionConfig.broadcastSmartMinimumDistance = Int32(config.broadcastSmartMinimumDistance) newPositionConfig.positionFlags = Int32(config.positionFlags) fetchedNode[0].positionConfig = newPositionConfig } else { fetchedNode[0].positionConfig?.smartPositionEnabled = config.positionBroadcastSmartEnabled fetchedNode[0].positionConfig?.deviceGpsEnabled = config.gpsEnabled + fetchedNode[0].positionConfig?.rxGpio = Int32(config.rxGpio) + fetchedNode[0].positionConfig?.txGpio = Int32(config.txGpio) fetchedNode[0].positionConfig?.fixedPosition = config.fixedPosition fetchedNode[0].positionConfig?.gpsUpdateInterval = Int32(config.gpsUpdateInterval) fetchedNode[0].positionConfig?.gpsAttemptTime = Int32(config.gpsAttemptTime) fetchedNode[0].positionConfig?.positionBroadcastSeconds = Int32(config.positionBroadcastSecs) + fetchedNode[0].positionConfig?.broadcastSmartMinimumIntervalSecs = Int32(config.broadcastSmartMinimumIntervalSecs) + fetchedNode[0].positionConfig?.broadcastSmartMinimumDistance = Int32(config.broadcastSmartMinimumDistance) fetchedNode[0].positionConfig?.positionFlags = Int32(config.positionFlags) } do { diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index 7be86a36..4f92bedd 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -354,6 +354,14 @@ struct Config { /// (Re)define GPS_TX_PIN for your board. var txGpio: UInt32 = 0 + /// + /// The minimum distance in meters traveled (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled + var broadcastSmartMinimumDistance: UInt32 = 0 + + /// + /// The minumum number of seconds (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled + var broadcastSmartMinimumIntervalSecs: UInt32 = 0 + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -1669,6 +1677,8 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm 7: .standard(proto: "position_flags"), 8: .standard(proto: "rx_gpio"), 9: .standard(proto: "tx_gpio"), + 10: .standard(proto: "broadcast_smart_minimum_distance"), + 11: .standard(proto: "broadcast_smart_minimum_interval_secs"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1686,6 +1696,8 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm case 7: try { try decoder.decodeSingularUInt32Field(value: &self.positionFlags) }() case 8: try { try decoder.decodeSingularUInt32Field(value: &self.rxGpio) }() case 9: try { try decoder.decodeSingularUInt32Field(value: &self.txGpio) }() + case 10: try { try decoder.decodeSingularUInt32Field(value: &self.broadcastSmartMinimumDistance) }() + case 11: try { try decoder.decodeSingularUInt32Field(value: &self.broadcastSmartMinimumIntervalSecs) }() default: break } } @@ -1719,6 +1731,12 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if self.txGpio != 0 { try visitor.visitSingularUInt32Field(value: self.txGpio, fieldNumber: 9) } + if self.broadcastSmartMinimumDistance != 0 { + try visitor.visitSingularUInt32Field(value: self.broadcastSmartMinimumDistance, fieldNumber: 10) + } + if self.broadcastSmartMinimumIntervalSecs != 0 { + try visitor.visitSingularUInt32Field(value: self.broadcastSmartMinimumIntervalSecs, fieldNumber: 11) + } try unknownFields.traverse(visitor: &visitor) } @@ -1732,6 +1750,8 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if lhs.positionFlags != rhs.positionFlags {return false} if lhs.rxGpio != rhs.rxGpio {return false} if lhs.txGpio != rhs.txGpio {return false} + if lhs.broadcastSmartMinimumDistance != rhs.broadcastSmartMinimumDistance {return false} + if lhs.broadcastSmartMinimumIntervalSecs != rhs.broadcastSmartMinimumIntervalSecs {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index cd056ada..88c102a9 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -51,7 +51,7 @@ struct RtttlConfig: View { Section(header: Text("options")) { HStack { - Label("RTTTL Ringtone", systemImage: "music.quarternote.3") + Label("ringtone", systemImage: "music.quarternote.3") TextField("Ringtone Transfer Language", text: $ringtone, axis: .vertical) .foregroundColor(.gray) .autocapitalization(.none) diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 25a91a69..5687a141 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -36,10 +36,14 @@ struct PositionConfig: View { @State var smartPositionEnabled = true @State var deviceGpsEnabled = true + @State var rxGpio = 0 + @State var txGpio = 0 @State var fixedPosition = false @State var gpsUpdateInterval = 0 @State var gpsAttemptTime = 0 @State var positionBroadcastSeconds = 0 + @State var broadcastSmartMinimumDistance = 0 + @State var broadcastSmartMinimumIntervalSecs = 0 @State var positionFlags = 3 /// Position Flags @@ -103,6 +107,7 @@ struct PositionConfig: View { Label("Device GPS Enabled", systemImage: "location") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + if deviceGpsEnabled { Picker("Update Interval", selection: $gpsUpdateInterval) { ForEach(GpsUpdateIntervals.allCases) { ui in @@ -119,6 +124,28 @@ struct PositionConfig: View { .pickerStyle(DefaultPickerStyle()) Text("How long should we try to get our position during each GPS Update Interval attempt?") .font(.caption) + + Picker("GPS Receive GPIO Override", selection: $rxGpio) { + ForEach(0..<40) { + if $0 == 0 { + Text("unset") + } else { + Text("Pin \($0)") + } + } + } + .pickerStyle(DefaultPickerStyle()) + + Picker("GPS Transmit GPIO Override", selection: $txGpio) { + ForEach(0..<40) { + if $0 == 0 { + Text("unset") + } else { + Text("Pin \($0)") + } + } + } + .pickerStyle(DefaultPickerStyle()) } else { Toggle(isOn: $fixedPosition) { Label("Fixed Position", systemImage: "location.square.fill") @@ -131,21 +158,50 @@ struct PositionConfig: View { Section(header: Text("Position Packet")) { - Toggle(isOn: $smartPositionEnabled) { - - Label("Smart Position Broadcast", systemImage: "location.fill.viewfinder") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Picker("Position Broadcast Interval", selection: $positionBroadcastSeconds) { ForEach(UpdateIntervals.allCases) { at in Text(at.description) } } .pickerStyle(DefaultPickerStyle()) - - Text("We should send our position this often (but only if it has changed significantly)") + Text("The maximum interval that can elapse without a node sending a position") .font(.caption) + + Toggle(isOn: $smartPositionEnabled) { + + Label("Smart Position Broadcast", systemImage: "location.fill.viewfinder") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + + if smartPositionEnabled { + Picker("Minimum Broadcast Interval", selection: $broadcastSmartMinimumIntervalSecs) { + ForEach(UpdateIntervals.allCases) { at in + Text(at.description) + } + } + .pickerStyle(DefaultPickerStyle()) + Text("The fastest that position updates will be sent if the minimum distance has been satisfied") + .font(.caption) + + Picker("Minimum Distance", selection: $broadcastSmartMinimumDistance) { + ForEach(25..<101) { + + if $0 == 0 { + Text("unset") + } else { + + if $0.isMultiple(of: 5) { + Text("\($0)") + .tag($0) + } + + } + } + } + .pickerStyle(DefaultPickerStyle()) + Text("The minimum distance change to be considered for a smart position broadcast.") + .font(.caption) + } } Section(header: Text("Position Flags")) { @@ -244,6 +300,8 @@ struct PositionConfig: View { pc.gpsUpdateInterval = UInt32(gpsUpdateInterval) pc.gpsAttemptTime = UInt32(gpsAttemptTime) pc.positionBroadcastSecs = UInt32(positionBroadcastSeconds) + pc.broadcastSmartMinimumIntervalSecs = UInt32(broadcastSmartMinimumIntervalSecs) + pc.broadcastSmartMinimumDistance = UInt32(broadcastSmartMinimumDistance) var pf: PositionFlags = [] if includeAltitude { pf.insert(.Altitude) } if includeAltitudeMsl { pf.insert(.AltitudeMsl) } @@ -296,6 +354,16 @@ struct PositionConfig: View { if newDeviceGps != node!.positionConfig!.deviceGpsEnabled { hasChanges = true } } } + .onChange(of: rxGpio) { newRxGpio in + if node != nil && node!.positionConfig != nil { + if newRxGpio != node!.positionConfig!.rxGpio { hasChanges = true } + } + } + .onChange(of: txGpio) { newTxGpio in + if node != nil && node!.positionConfig != nil { + if newTxGpio != node!.positionConfig!.txGpio { hasChanges = true } + } + } .onChange(of: gpsAttemptTime) { newGpsAttemptTime in if node != nil && node!.positionConfig != nil { if newGpsAttemptTime != node!.positionConfig!.gpsAttemptTime { hasChanges = true } @@ -321,6 +389,16 @@ struct PositionConfig: View { if newPositionBroadcastSeconds != node!.positionConfig!.positionBroadcastSeconds { hasChanges = true } } } + .onChange(of: broadcastSmartMinimumIntervalSecs) { newBroadcastSmartMinimumIntervalSecs in + if node != nil && node!.positionConfig != nil { + if newBroadcastSmartMinimumIntervalSecs != node!.positionConfig!.broadcastSmartMinimumIntervalSecs { hasChanges = true } + } + } + .onChange(of: broadcastSmartMinimumDistance) { newBroadcastSmartMinimumDistance in + if node != nil && node!.positionConfig != nil { + if newBroadcastSmartMinimumDistance != node!.positionConfig!.broadcastSmartMinimumDistance { hasChanges = true } + } + } .onChange(of: includeAltitude) { altFlag in let pf = PositionFlags(rawValue: self.positionFlags) let existingValue = pf.contains(.Altitude) @@ -382,10 +460,14 @@ struct PositionConfig: View { self.smartPositionEnabled = node?.positionConfig?.smartPositionEnabled ?? true self.deviceGpsEnabled = node?.positionConfig?.deviceGpsEnabled ?? true + self.rxGpio = Int(node?.positionConfig?.rxGpio ?? 0) + self.txGpio = Int(node?.positionConfig?.txGpio ?? 0) self.fixedPosition = node?.positionConfig?.fixedPosition ?? false self.gpsUpdateInterval = Int(node?.positionConfig?.gpsUpdateInterval ?? 30) self.gpsAttemptTime = Int(node?.positionConfig?.gpsAttemptTime ?? 30) self.positionBroadcastSeconds = Int(node?.positionConfig?.positionBroadcastSeconds ?? 900) + self.broadcastSmartMinimumIntervalSecs = Int(node?.positionConfig?.broadcastSmartMinimumIntervalSecs ?? 30) + self.broadcastSmartMinimumDistance = Int(node?.positionConfig?.broadcastSmartMinimumDistance ?? 40) self.positionFlags = Int(node?.positionConfig?.positionFlags ?? 3) let pf = PositionFlags(rawValue: self.positionFlags) From 0e677d17352513598dff26abb366509d57fc678c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 27 Mar 2023 17:04:24 -0700 Subject: [PATCH 16/21] Updates for new position settings --- .../Views/Map/Custom/MapViewSwiftUI.swift | 85 ++++++++++--------- Meshtastic/Views/Nodes/NodeDetail.swift | 1 + Meshtastic/Views/Nodes/NodeMap.swift | 3 +- .../Settings/Config/PositionConfig.swift | 2 +- 4 files changed, 47 insertions(+), 44 deletions(-) diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 71b4bbf2..687d3130 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -25,6 +25,7 @@ struct MapViewSwiftUI: UIViewRepresentable { let userTrackingMode: MKUserTrackingMode let showNodeHistory: Bool let showRouteLines: Bool + let showMultipleNodes: Bool @AppStorage("meshMapRecentering") private var recenter: Bool = false // Offline Map Tiles @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0 @@ -91,6 +92,8 @@ struct MapViewSwiftUI: UIViewRepresentable { } func updateUIView(_ mapView: MKMapView, context: Context) { + + mapView.mapType = mapViewType if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile { @@ -116,60 +119,58 @@ struct MapViewSwiftUI: UIViewRepresentable { } } - DispatchQueue.main.async { + // DispatchQueue.main.async { let latest = positions .filter { $0.latest == true } .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } let annotationCount = waypoints.count + (showNodeHistory ? positions.count : latest.count) - if annotationCount > mapView.annotations.count { - print("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)") - if showRouteLines { - // Remove all existing PolyLine Overlays - for overlay in mapView.overlays { - if overlay is MKPolyline { - mapView.removeOverlay(overlay) - } - } - var lineIndex = 0 - for position in latest { - - let nodePositions = positions.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) && $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 } - let lineCoords = nodePositions.map ({ - (position) -> CLLocationCoordinate2D in - return position.nodeCoordinate! - }) - let polyline = MKPolyline(coordinates: lineCoords, count: nodePositions.count) - polyline.title = "\(String(position.nodePosition?.num ?? 0))-\(String(lineIndex))" - mapView.addOverlay(polyline) - lineIndex += 1 - // There are 18 colors for lines, start over if we are at index 17 - if lineIndex > 17 { - lineIndex = 0 - } + print("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)") + mapView.removeAnnotations(mapView.annotations) + mapView.addAnnotations(waypoints) + if showRouteLines { + // Remove all existing PolyLine Overlays + for overlay in mapView.overlays { + if overlay is MKPolyline { + mapView.removeOverlay(overlay) } } - mapView.removeAnnotations(mapView.annotations) - mapView.addAnnotations(waypoints) - if userTrackingMode == MKUserTrackingMode.none { - mapView.showsUserLocation = false - if recenter { - if latest.count == 1 { - mapView.fit(annotations:showNodeHistory ? positions : latest, andShow: true) - } else { - mapView.addAnnotations(showNodeHistory ? positions : latest) - mapView.fitAllAnnotations() - } + var lineIndex = 0 + for position in latest { + + let nodePositions = positions.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) && $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 } + let lineCoords = nodePositions.map ({ + (position) -> CLLocationCoordinate2D in + return position.nodeCoordinate! + }) + let polyline = MKPolyline(coordinates: lineCoords, count: nodePositions.count) + polyline.title = "\(String(position.nodePosition?.num ?? 0))-\(String(lineIndex))" + mapView.addOverlay(polyline) + lineIndex += 1 + // There are 18 colors for lines, start over if we are at index 17 + if lineIndex > 17 { + lineIndex = 0 + } + } + } + if userTrackingMode == MKUserTrackingMode.none { + mapView.showsUserLocation = false + if recenter { + if !showMultipleNodes { + mapView.fit(annotations:showNodeHistory ? positions : latest, andShow: true) } else { mapView.addAnnotations(showNodeHistory ? positions : latest) + mapView.fitAllAnnotations() } } else { - // Centering Done by tracking mode mapView.addAnnotations(showNodeHistory ? positions : latest) - mapView.showsUserLocation = true } - mapView.setUserTrackingMode(userTrackingMode, animated: true) + } else { + // Centering Done by tracking mode + mapView.addAnnotations(showNodeHistory ? positions : latest) + mapView.showsUserLocation = true } - } + mapView.setUserTrackingMode(userTrackingMode, animated: true) + //} } func makeCoordinator() -> MapCoordinator { @@ -361,7 +362,7 @@ struct MapViewSwiftUI: UIViewRepresentable { let index = Int(titleString.components(separatedBy: "-").last ?? "0") let renderer = MKPolylineRenderer(polyline: routePolyline) renderer.strokeColor = parent.lineColors[index ?? 0] - renderer.lineWidth = 5 + renderer.lineWidth = 7 return renderer } return MKOverlayRenderer() diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index f7c8ab6f..a2ba57c0 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -77,6 +77,7 @@ struct NodeDetail: View { userTrackingMode: MKUserTrackingMode.none, showNodeHistory: meshMapShowNodeHistory, showRouteLines: meshMapShowRouteLines, + showMultipleNodes: false, customMapOverlay: self.customMapOverlay ) VStack(alignment: .leading) { diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 4cba5089..3bf9c241 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -77,8 +77,9 @@ struct NodeMap: View { waypoints: Array(waypoints), mapViewType: mapType, userTrackingMode: userTrackingMode, - showNodeHistory: meshMapShowNodeHistory, + showNodeHistory: meshMapShowNodeHistory, showRouteLines: meshMapShowRouteLines, + showMultipleNodes: true, customMapOverlay: self.customMapOverlay ) VStack { diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 5687a141..72e6830c 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -467,7 +467,7 @@ struct PositionConfig: View { self.gpsAttemptTime = Int(node?.positionConfig?.gpsAttemptTime ?? 30) self.positionBroadcastSeconds = Int(node?.positionConfig?.positionBroadcastSeconds ?? 900) self.broadcastSmartMinimumIntervalSecs = Int(node?.positionConfig?.broadcastSmartMinimumIntervalSecs ?? 30) - self.broadcastSmartMinimumDistance = Int(node?.positionConfig?.broadcastSmartMinimumDistance ?? 40) + self.broadcastSmartMinimumDistance = Int(node?.positionConfig?.broadcastSmartMinimumDistance ?? 50) self.positionFlags = Int(node?.positionConfig?.positionFlags ?? 3) let pf = PositionFlags(rawValue: self.positionFlags) From faf780a09ab22ee10cef80c5bacc2e843408ead1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 27 Mar 2023 17:41:34 -0700 Subject: [PATCH 17/21] Add milliseconds to ack time move device GPS config to the bottom --- .../Views/Messages/UserMessageList.swift | 2 +- .../Settings/Config/PositionConfig.swift | 108 +++++++++--------- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 0abcb0b9..dfdb4046 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -133,7 +133,7 @@ struct UserMessageList: View { let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp)) let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) if ackDate >= sixMonthsAgo! { - Text("Ack Time: \(ackDate.formattedDate(format: "h:mm:ss a"))").foregroundColor(.gray) + Text("Ack Time: \(ackDate.formattedDate(format: "h:mm:ss.SSSS a"))").foregroundColor(.gray) } else { Text("unknown.age").font(.caption2).foregroundColor(.gray) } diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 72e6830c..28829f44 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -102,59 +102,6 @@ struct PositionConfig: View { .font(.callout) .foregroundColor(.orange) } - Section(header: Text("Device GPS")) { - Toggle(isOn: $deviceGpsEnabled) { - Label("Device GPS Enabled", systemImage: "location") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - - if deviceGpsEnabled { - Picker("Update Interval", selection: $gpsUpdateInterval) { - ForEach(GpsUpdateIntervals.allCases) { ui in - Text(ui.description) - } - } - Text("How often should we try to get a GPS position.") - .font(.caption) - Picker("Attempt Time", selection: $gpsAttemptTime) { - ForEach(GpsAttemptTimes.allCases) { at in - Text(at.description) - } - } - .pickerStyle(DefaultPickerStyle()) - Text("How long should we try to get our position during each GPS Update Interval attempt?") - .font(.caption) - - Picker("GPS Receive GPIO Override", selection: $rxGpio) { - ForEach(0..<40) { - if $0 == 0 { - Text("unset") - } else { - Text("Pin \($0)") - } - } - } - .pickerStyle(DefaultPickerStyle()) - - Picker("GPS Transmit GPIO Override", selection: $txGpio) { - ForEach(0..<40) { - if $0 == 0 { - Text("unset") - } else { - Text("Pin \($0)") - } - } - } - .pickerStyle(DefaultPickerStyle()) - } else { - Toggle(isOn: $fixedPosition) { - Label("Fixed Position", systemImage: "location.square.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("If enabled your current location will be set as a fixed position.") - .font(.caption) - } - } Section(header: Text("Position Packet")) { @@ -184,7 +131,7 @@ struct PositionConfig: View { .font(.caption) Picker("Minimum Distance", selection: $broadcastSmartMinimumDistance) { - ForEach(25..<101) { + ForEach(10..<151) { if $0 == 0 { Text("unset") @@ -265,6 +212,59 @@ struct PositionConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } } + Section(header: Text("Device GPS")) { + Toggle(isOn: $deviceGpsEnabled) { + Label("Device GPS Enabled", systemImage: "location") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + + if deviceGpsEnabled { + Picker("Update Interval", selection: $gpsUpdateInterval) { + ForEach(GpsUpdateIntervals.allCases) { ui in + Text(ui.description) + } + } + Text("How often should we try to get a GPS position.") + .font(.caption) + Picker("Attempt Time", selection: $gpsAttemptTime) { + ForEach(GpsAttemptTimes.allCases) { at in + Text(at.description) + } + } + .pickerStyle(DefaultPickerStyle()) + Text("How long should we try to get our position during each GPS Update Interval attempt?") + .font(.caption) + + Picker("GPS Receive GPIO", selection: $rxGpio) { + ForEach(0..<40) { + if $0 == 0 { + Text("unset") + } else { + Text("Pin \($0)") + } + } + } + .pickerStyle(DefaultPickerStyle()) + + Picker("GPS Transmit GPIO", selection: $txGpio) { + ForEach(0..<40) { + if $0 == 0 { + Text("unset") + } else { + Text("Pin \($0)") + } + } + } + .pickerStyle(DefaultPickerStyle()) + } else { + Toggle(isOn: $fixedPosition) { + Label("Fixed Position", systemImage: "location.square.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("If enabled your current location will be set as a fixed position.") + .font(.caption) + } + } } .disabled(self.bleManager.connectedPeripheral == nil || node?.positionConfig == nil) From 23f2fc51366c9f7ec09359b2b97bc3815444ddc3 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 28 Mar 2023 06:50:23 -0700 Subject: [PATCH 18/21] Clean up form for position config --- Meshtastic/Enums/IntervalEnums.swift | 1 + Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift | 15 ++++----------- Meshtastic/Views/Nodes/NodeDetail.swift | 5 ++--- Meshtastic/Views/Nodes/NodeMap.swift | 1 - .../Views/Settings/Config/PositionConfig.swift | 2 +- 5 files changed, 8 insertions(+), 16 deletions(-) diff --git a/Meshtastic/Enums/IntervalEnums.swift b/Meshtastic/Enums/IntervalEnums.swift index 8a36933a..7e73f7f7 100644 --- a/Meshtastic/Enums/IntervalEnums.swift +++ b/Meshtastic/Enums/IntervalEnums.swift @@ -88,6 +88,7 @@ enum SenderIntervals: Int, CaseIterable, Identifiable { enum UpdateIntervals: Int, CaseIterable, Identifiable { + case fiveSeconds = 5 case tenSeconds = 10 case fifteenSeconds = 15 case thirtySeconds = 30 diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 687d3130..86f8c0a8 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -25,7 +25,6 @@ struct MapViewSwiftUI: UIViewRepresentable { let userTrackingMode: MKUserTrackingMode let showNodeHistory: Bool let showRouteLines: Bool - let showMultipleNodes: Bool @AppStorage("meshMapRecentering") private var recenter: Bool = false // Offline Map Tiles @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0 @@ -137,7 +136,7 @@ struct MapViewSwiftUI: UIViewRepresentable { var lineIndex = 0 for position in latest { - let nodePositions = positions.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) && $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 } + let nodePositions = positions.filter { $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 } let lineCoords = nodePositions.map ({ (position) -> CLLocationCoordinate2D in return position.nodeCoordinate! @@ -154,15 +153,9 @@ struct MapViewSwiftUI: UIViewRepresentable { } if userTrackingMode == MKUserTrackingMode.none { mapView.showsUserLocation = false + mapView.addAnnotations(showNodeHistory ? positions : latest) if recenter { - if !showMultipleNodes { - mapView.fit(annotations:showNodeHistory ? positions : latest, andShow: true) - } else { - mapView.addAnnotations(showNodeHistory ? positions : latest) - mapView.fitAllAnnotations() - } - } else { - mapView.addAnnotations(showNodeHistory ? positions : latest) + mapView.fit(annotations:showNodeHistory || showRouteLines ? positions : latest, andShow: false) } } else { // Centering Done by tracking mode @@ -362,7 +355,7 @@ struct MapViewSwiftUI: UIViewRepresentable { let index = Int(titleString.components(separatedBy: "-").last ?? "0") let renderer = MKPolylineRenderer(polyline: routePolyline) renderer.strokeColor = parent.lineColors[index ?? 0] - renderer.lineWidth = 7 + renderer.lineWidth = 8 return renderer } return MKOverlayRenderer() diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index a2ba57c0..24634d6c 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -60,7 +60,7 @@ struct NodeDetail: View { if node.positions?.count ?? 0 > 0 { ZStack { let positionArray = node.positions?.array as? [PositionEntity] ?? [] - let lastThousand = Array(positionArray.prefix(1000)) + let lastTenThousand = Array(positionArray.prefix(10000)) // let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) } ZStack { MapViewSwiftUI(onLongPress: { coord in @@ -72,12 +72,11 @@ struct NodeDetail: View { editingWaypoint = wpId presentingWaypointForm = true } - }, positions: lastThousand, waypoints: Array(waypoints), + }, positions: lastTenThousand, waypoints: Array(waypoints), mapViewType: mapType, userTrackingMode: MKUserTrackingMode.none, showNodeHistory: meshMapShowNodeHistory, showRouteLines: meshMapShowRouteLines, - showMultipleNodes: false, customMapOverlay: self.customMapOverlay ) VStack(alignment: .leading) { diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 3bf9c241..650a76a5 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -79,7 +79,6 @@ struct NodeMap: View { userTrackingMode: userTrackingMode, showNodeHistory: meshMapShowNodeHistory, showRouteLines: meshMapShowRouteLines, - showMultipleNodes: true, customMapOverlay: self.customMapOverlay ) VStack { diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 28829f44..6b36e95c 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -146,7 +146,7 @@ struct PositionConfig: View { } } .pickerStyle(DefaultPickerStyle()) - Text("The minimum distance change to be considered for a smart position broadcast.") + Text("The minimum distance change in meters to be considered for a smart position broadcast.") .font(.caption) } } From 3bbc2cf4a19ce480c51297137a77f81bb622f149 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 28 Mar 2023 06:51:41 -0700 Subject: [PATCH 19/21] Fix interval enum --- Meshtastic/Enums/IntervalEnums.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Meshtastic/Enums/IntervalEnums.swift b/Meshtastic/Enums/IntervalEnums.swift index 7e73f7f7..8a36933a 100644 --- a/Meshtastic/Enums/IntervalEnums.swift +++ b/Meshtastic/Enums/IntervalEnums.swift @@ -88,7 +88,6 @@ enum SenderIntervals: Int, CaseIterable, Identifiable { enum UpdateIntervals: Int, CaseIterable, Identifiable { - case fiveSeconds = 5 case tenSeconds = 10 case fifteenSeconds = 15 case thirtySeconds = 30 From 6361ff3385625ae7b5f023c0995e6b993443e95e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 28 Mar 2023 14:52:11 -0700 Subject: [PATCH 20/21] Smart position for packets sent from phone to device --- Meshtastic/Helpers/BLEManager.swift | 731 +++++++++--------- .../Views/Messages/ChannelMessageList.swift | 2 +- .../Views/Messages/UserMessageList.swift | 2 +- .../Settings/Config/PositionConfig.swift | 2 +- 4 files changed, 373 insertions(+), 364 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 40d69a25..c37d9757 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -8,7 +8,7 @@ import MapKit // Meshtastic BLE Device Manager // --------------------------------------------------------------------------------------- class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { - + private static var documentsFolder: URL { do { return try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) @@ -36,6 +36,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { var timeoutTimerCount = 0 var timeoutTimerRuns = 0 var positionTimer: Timer? + var lastPosition: CLLocationCoordinate2D? let emptyNodeNum: UInt32 = 4294967295 /* Meshtastic Service Details */ var TORADIO_characteristic: CBCharacteristic! @@ -46,9 +47,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { let FROMRADIO_UUID = CBUUID(string: "0x2C55E69E-4993-11ED-B878-0242AC120002") let EOL_FROMRADIO_UUID = CBUUID(string: "0x8BA2BCC2-EE02-4A55-A531-C525C5E454D5") let FROMNUM_UUID = CBUUID(string: "0xED9DA18C-A800-4F66-A670-AA7547E34453") - + let meshLog = documentsFolder.appendingPathComponent("meshlog.txt") - + // MARK: init BLEManager override init() { self.lastConnectionError = "" @@ -57,7 +58,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { centralManager = CBCentralManager(delegate: self, queue: nil) // centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey: restoreKey]) } - + // MARK: Scanning for BLE Devices // Scan for nearby BLE devices using the Meshtastic BLE service ID func startScanning() { @@ -66,7 +67,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { print("✅ Scanning Started") } } - + // Stop Scanning For BLE Devices func stopScanning() { if centralManager.isScanning { @@ -74,7 +75,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { print("🛑 Stopped Scanning") } } - + // MARK: BLE Connect functions /// The action after the timeout-timer has fired /// @@ -84,25 +85,25 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { @objc func timeoutTimerFired(timer: Timer) { guard let timerContext = timer.userInfo as? [String: String] else { return } let name: String = timerContext["name", default: "Unknown"] - + self.timeoutTimerCount += 1 self.lastConnectionError = "" - + if timeoutTimerCount == 10 { if connectedPeripheral != nil { self.centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) } connectedPeripheral = nil if self.timeoutTimer != nil { - + self.timeoutTimer!.invalidate() } self.isConnected = false self.isConnecting = false self.lastConnectionError = "🚨 " + String.localizedStringWithFormat(NSLocalizedString("ble.connection.timeout %d %@", - comment: "Connection failed after %d attempts to connect to %@. You may need to forget your device under Settings > Bluetooth."), - timeoutTimerCount, name) - + comment: "Connection failed after %d attempts to connect to %@. You may need to forget your device under Settings > Bluetooth."), + timeoutTimerCount, name) + MeshLogger.log(lastConnectionError) self.timeoutTimerCount = 0 self.timeoutTimerRuns += 1 @@ -111,7 +112,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { print("🚨 BLE Connecting 2 Second Timeout Timer Fired \(timeoutTimerCount) Time(s): \(name)") } } - + // Connect to a specific peripheral func connectTo(peripheral: CBPeripheral) { stopScanning() @@ -124,7 +125,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { print("ℹ️ BLE Disconnecting from: \(connectedPeripheral.name) to connect to \(peripheral.name ?? "Unknown")") disconnectPeripheral() } - + centralManager?.connect(peripheral) // Invalidate any existing timer if timeoutTimer != nil { @@ -137,10 +138,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { RunLoop.current.add(timeoutTimer!, forMode: .common) print("ℹ️ BLE Connecting: \(peripheral.name ?? "Unknown")") } - + // Disconnect Connected Peripheral func disconnectPeripheral(reconnect: Bool = true) { - + guard let connectedPeripheral = connectedPeripheral else { return } automaticallyReconnect = reconnect centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) @@ -151,7 +152,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { connectedVersion = "0.0.0" startScanning() } - + // Called each time a peripheral is discovered func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { isConnecting = false @@ -165,7 +166,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { if timeoutTimer != nil { timeoutTimer!.invalidate() } - + // remove any connection errors self.lastConnectionError = "" // Map the peripheral to the connectedPeripheral ObservedObjects @@ -182,13 +183,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { peripheral.discoverServices([meshtasticServiceCBUUID]) print("✅ BLE Connected: \(peripheral.name ?? "Unknown")") } - + // Called when a Peripheral fails to connect func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { disconnectPeripheral() print("🚫 BLE Failed to Connect: \(peripheral.name ?? "Unknown")") } - + // Disconnect Peripheral Event func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { self.connectedPeripheral = nil @@ -201,8 +202,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { if errorCode == 6 { // CBError.Code.connectionTimeout The connection has timed out unexpectedly. // Happens when device is manually reset / powered off lastConnectionError = "🚨" + String.localizedStringWithFormat(NSLocalizedString("ble.errorcode.6 %@", - comment: "The app will automatically reconnect to the preferred radio if it come back in range."), - e.localizedDescription) + comment: "The app will automatically reconnect to the preferred radio if it come back in range."), + e.localizedDescription) print("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)") } else if errorCode == 7 { // CBError.Code.peripheralDisconnected The specified device has disconnected from us. // Seems to be what is received when a tbeam sleeps, immediately recconnecting does not work. @@ -211,8 +212,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } else if errorCode == 14 { // Peer removed pairing information // Forgetting and reconnecting seems to be necessary so we need to show the user an error telling them to do that lastConnectionError = "🚨 " + String.localizedStringWithFormat(NSLocalizedString("ble.errorcode.14 %@", - comment: "This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re-connecting to the radio."), - e.localizedDescription) + comment: "This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re-connecting to the radio."), + e.localizedDescription) print("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(lastConnectionError)") } else { lastConnectionError = "🚨 \(e.localizedDescription)" @@ -226,7 +227,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { // Start a scan so the disconnected peripheral is moved to the peripherals[] if it is awake self.startScanning() } - + // MARK: Peripheral Services functions func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { if let e = error { @@ -240,36 +241,36 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } } } - + // MARK: Discover Characteristics Event func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { - + if let e = error { print("🚫 BLE Discover Characteristics error for \(peripheral.name ?? "Unknown") \(e) disconnecting device") // Try and stop crashes when this error occurs disconnectPeripheral() return } - + guard let characteristics = service.characteristics else { return } - + for characteristic in characteristics { switch characteristic.uuid { - + case TORADIO_UUID: print("✅ BLE did discover TORADIO characteristic for Meshtastic by \(peripheral.name ?? "Unknown")") TORADIO_characteristic = characteristic - + case FROMRADIO_UUID: print("✅ BLE did discover FROMRADIO characteristic for Meshtastic by \(peripheral.name ?? "Unknown")") FROMRADIO_characteristic = characteristic peripheral.readValue(for: FROMRADIO_characteristic) - + case FROMNUM_UUID: print("✅ BLE did discover FROMNUM (Notify) characteristic for Meshtastic by \(peripheral.name ?? "Unknown")") FROMNUM_characteristic = characteristic peripheral.setNotifyValue(true, for: characteristic) - + default: break } @@ -278,11 +279,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { sendWantConfig() } } - + func requestDeviceMetadata(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32, context: NSManagedObjectContext) -> Int64 { - + guard connectedPeripheral!.peripheral.state == CBPeripheralState.connected else { return 0 } - + var adminPacket = AdminMessage() adminPacket.getDeviceMetadataRequest = true var meshPacket: MeshPacket = MeshPacket() @@ -303,12 +304,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return 0 } - + func sendTraceRouteRequest(destNum: Int64, wantResponse: Bool) -> Bool { - + var success = false guard connectedPeripheral!.peripheral.state == CBPeripheralState.connected else { return success } - + let fromNodeNum = connectedPeripheral.num let routePacket = RouteDiscovery() var meshPacket = MeshPacket() @@ -320,100 +321,100 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.portnum = PortNum.tracerouteApp dataMessage.wantResponse = wantResponse meshPacket.decoded = dataMessage - + var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket let binaryData: Data = try! toRadio.serializedData() - + if connectedPeripheral!.peripheral.state == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) success = true - + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.traceroute.sent %@", - comment: "Sent a Trace Route Request to node: %@"), String(destNum)) + comment: "Sent a Trace Route Request to node: %@"), String(destNum)) MeshLogger.log("🪧 \(logString)") } return success } - + func sendWantConfig() { guard connectedPeripheral!.peripheral.state == CBPeripheralState.connected else { return } - + if FROMRADIO_characteristic == nil { MeshLogger.log("🚨 \(NSLocalizedString("firmware.version.unsupported", comment: "Unsupported Firmware Version Detected, unable to connect to device."))") invalidVersion = true return } else { - - let nodeName = connectedPeripheral!.peripheral.name ?? NSLocalizedString("unknown", comment: NSLocalizedString("unknown", comment: "Unknown")) - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.wantconfig %@", comment: "Issuing Want Config to %@"), nodeName) - MeshLogger.log("🛎️ \(logString)") - // BLE Characteristics discovered, issue wantConfig - var toRadio: ToRadio = ToRadio() - configNonce += 1 - toRadio.wantConfigID = configNonce - let binaryData: Data = try! toRadio.serializedData() - connectedPeripheral!.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) + + let nodeName = connectedPeripheral!.peripheral.name ?? NSLocalizedString("unknown", comment: NSLocalizedString("unknown", comment: "Unknown")) + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.wantconfig %@", comment: "Issuing Want Config to %@"), nodeName) + MeshLogger.log("🛎️ \(logString)") + // BLE Characteristics discovered, issue wantConfig + var toRadio: ToRadio = ToRadio() + configNonce += 1 + toRadio.wantConfigID = configNonce + let binaryData: Data = try! toRadio.serializedData() + connectedPeripheral!.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) // Either Read the config complete value or from num notify value connectedPeripheral!.peripheral.readValue(for: FROMRADIO_characteristic) } } - + func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { if let errorText = error?.localizedDescription { print("🚫 didUpdateNotificationStateFor error: \(errorText)") } } - + // MARK: Data Read / Update Characteristic Event func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { - + if let e = error { - + print("🚫 didUpdateValueFor Characteristic error \(e)") - + let errorCode = (e as NSError).code - + if errorCode == 5 || errorCode == 15 { // BLE PIN connection errors // 5 CBATTErrorDomain Code=5 "Authentication is insufficient." // 15 CBATTErrorDomain Code=15 "Encryption is insufficient." lastConnectionError = "🚨" + String.localizedStringWithFormat(NSLocalizedString("ble.errorcode.pin %@", - comment: "Please try connecting again and check the PIN carefully."), - e.localizedDescription) + comment: "Please try connecting again and check the PIN carefully."), + e.localizedDescription) print("🚨 \(e.localizedDescription) Please try connecting again and check the PIN carefully.") self.disconnectPeripheral(reconnect: false) } } - + switch characteristic.uuid { - + case FROMRADIO_UUID: - + if characteristic.value == nil || characteristic.value!.isEmpty { return } var decodedInfo = FromRadio() - + do { decodedInfo = try FromRadio(serializedData: characteristic.value!) - + } catch { print(characteristic.value!) } - + switch decodedInfo.packet.decoded.portnum { - - // Handle Any local only packets we get over BLE + + // Handle Any local only packets we get over BLE case .unknownApp: var nowKnown = false - + // MyInfo from initial connection if decodedInfo.myInfo.isInitialized && decodedInfo.myInfo.myNodeNum > 0 { - + let lastDotIndex = decodedInfo.myInfo.firmwareVersion.lastIndex(of: ".") - + if lastDotIndex == nil { invalidVersion = true connectedVersion = "0.0.0" @@ -422,17 +423,17 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { nowKnown = true connectedVersion = String(version.dropLast()) } - + let supportedVersion = connectedVersion == "0.0.0" || self.minimumVersion.compare(connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(connectedVersion, options: .numeric) == .orderedSame if !supportedVersion { invalidVersion = true lastConnectionError = "🚨" + NSLocalizedString("update.firmware", comment: "Update Your Firmware") return - + } else { - + let myInfo = myInfoPacket(myInfo: decodedInfo.myInfo, peripheralId: self.connectedPeripheral.id, context: context!) - + if myInfo != nil { connectedPeripheral.num = myInfo!.myNodeNum connectedPeripheral.firmwareVersion = myInfo?.firmwareVersion ?? NSLocalizedString("unknown", comment: "Unknown") @@ -446,7 +447,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { if decodedInfo.nodeInfo.num > 0 && !invalidVersion { nowKnown = true let nodeInfo = nodeInfoPacket(nodeInfo: decodedInfo.nodeInfo, channel: decodedInfo.packet.channel, context: context!) - + if nodeInfo != nil { if self.connectedPeripheral != nil && self.connectedPeripheral.num == nodeInfo!.num { if nodeInfo!.user != nil { @@ -463,18 +464,18 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } // Config if decodedInfo.config.isInitialized && !invalidVersion { - + nowKnown = true localConfig(config: decodedInfo.config, context: context!, nodeNum: self.connectedPeripheral.num, nodeLongName: self.connectedPeripheral.longName) } // Module Config if decodedInfo.moduleConfig.isInitialized && !invalidVersion { - + nowKnown = true moduleConfig(config: decodedInfo.moduleConfig, context: context!, nodeNum: self.connectedPeripheral.num, nodeLongName: self.connectedPeripheral.longName) - + if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) { - + if decodedInfo.moduleConfig.cannedMessage.enabled { _ = self.getCannedMessageModuleMessages(destNum: self.connectedPeripheral.num, wantResponse: true) } @@ -487,7 +488,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } // Log any other unknownApp calls if !nowKnown { MeshLogger.log("🕸️ MESH PACKET received for Unknown App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") } - + case .textMessageApp: textMessageAppPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) case .remoteHardwareApp: @@ -513,7 +514,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { case .rangeTestApp: MeshLogger.log("🕸️ MESH PACKET received for Range Test App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") case .telemetryApp: - if !invalidVersion { telemetryPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) } + if !invalidVersion { telemetryPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) } case .textMessageCompressedApp: MeshLogger.log("🕸️ MESH PACKET received for Text Message Compressed App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") case .zpsApp: @@ -527,34 +528,34 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { case .audioApp: MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") case .tracerouteApp: - if let routingMessage = try? RouteDiscovery(serializedData: decodedInfo.packet.decoded.payload) { - - if routingMessage.route.count == 0 { - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.traceroute.received.direct %@", - comment: "Trace Route request sent to node: %@ was recieived directly."), String(decodedInfo.packet.from)) - MeshLogger.log("🪧 \(logString)") - } else { - - var routeString = "\(decodedInfo.packet.to) --> " - for node in routingMessage.route { - routeString += "\(node) --> " - } - routeString += "\(decodedInfo.packet.from)" - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.traceroute.received.route %@", - comment: "Trace Route request returned: %@"), routeString) - MeshLogger.log("🪧 \(logString)") + if let routingMessage = try? RouteDiscovery(serializedData: decodedInfo.packet.decoded.payload) { + + if routingMessage.route.count == 0 { + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.traceroute.received.direct %@", + comment: "Trace Route request sent to node: %@ was recieived directly."), String(decodedInfo.packet.from)) + MeshLogger.log("🪧 \(logString)") + } else { + + var routeString = "\(decodedInfo.packet.to) --> " + for node in routingMessage.route { + routeString += "\(node) --> " } + routeString += "\(decodedInfo.packet.from)" + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.traceroute.received.route %@", + comment: "Trace Route request returned: %@"), routeString) + MeshLogger.log("🪧 \(logString)") } + } case .UNRECOGNIZED: MeshLogger.log("🕸️ MESH PACKET received for Other App UNHANDLED \(try! decodedInfo.packet.jsonString())") case .max: print("MAX PORT NUM OF 511") } - + // MARK: Check for an All / Broadcast User and delete it as a transition to multi channel let fetchBCUserRequest: NSFetchRequest = NSFetchRequest.init(entityName: "UserEntity") fetchBCUserRequest.predicate = NSPredicate(format: "num == %lld", Int64(emptyNodeNum)) - + do { guard let fetchedUser = try context?.fetch(fetchBCUserRequest) as? [UserEntity] else { return @@ -566,7 +567,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } catch { print("💥 Error Deleting the All - Broadcast User") } - + if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == configNonce { invalidVersion = false lastConnectionError = "" @@ -589,7 +590,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return } - + case FROMNUM_UUID: print("🗞️ BLE (Notify) characteristic, value will be read next") default: @@ -600,16 +601,16 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { peripheral.readValue(for: FROMRADIO_characteristic) } } - + public func sendMessage(message: String, toUserNum: Int64, channel: Int32, isEmoji: Bool, replyID: Int64) -> Bool { var success = false - + // Return false if we are not properly connected to a device, handle retry logic in the view for now if connectedPeripheral == nil || connectedPeripheral!.peripheral.state != CBPeripheralState.connected { - + self.disconnectPeripheral() self.startScanning() - + // Try and connect to the preferredPeripherial first let preferredPeripheral = peripherals.filter({ $0.peripheral.identifier.uuidString == UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" }).first if preferredPeripheral != nil && preferredPeripheral?.peripheral != nil { @@ -617,34 +618,34 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } let nodeName = connectedPeripheral?.peripheral.name ?? NSLocalizedString("unknown", comment: NSLocalizedString("unknown", comment: "Unknown")) let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.textmessage.send.failed %@", - comment: "Message Send Failed, not properly connected to %@"), nodeName) + comment: "Message Send Failed, not properly connected to %@"), nodeName) MeshLogger.log("🚫 \(logString)") - + success = false } else if message.count < 1 { - + // Don't send an empty message print("🚫 Don't Send an Empty Message") success = false - + } else { - + let fromUserNum: Int64 = self.connectedPeripheral.num - + let messageUsers: NSFetchRequest = NSFetchRequest.init(entityName: "UserEntity") messageUsers.predicate = NSPredicate(format: "num IN %@", [fromUserNum, Int64(toUserNum)]) - + do { - + guard let fetchedUsers = try context?.fetch(messageUsers) as? [UserEntity] else { return false } if fetchedUsers.isEmpty { - + print("🚫 Message Users Not Found, Fail") success = false } else if fetchedUsers.count >= 1 { - + let newMessage = MessageEntity(context: context!) newMessage.messageId = Int64(UInt32.random(in: UInt32(UInt8.max).. 0 { @@ -686,7 +687,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.decoded.replyID = UInt32(replyID) } meshPacket.wantAck = true - + var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket @@ -699,7 +700,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { try context!.save() print("💾 Saved a new sent message from \(connectedPeripheral.num) to \(toUserNum)") success = true - + } catch { context!.rollback() let nsError = error as NSError @@ -707,14 +708,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } } } - + } catch { - + } } return success } - + public func sendWaypoint(waypoint: Waypoint) -> Bool { if waypoint.latitudeI == 373346000 && waypoint.longitudeI == -1220090000 { return false @@ -771,14 +772,25 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return success } - - public func sendPosition(destNum: Int64, wantResponse: Bool) -> Bool { + + public func sendPosition(destNum: Int64, wantResponse: Bool, smartPosition: Bool) -> Bool { var success = false let fromNodeNum = connectedPeripheral.num if fromNodeNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 { return false } - + + if smartPosition { + if lastPosition != nil { + let connectedNode = getNodeInfo(id: connectedPeripheral?.num ?? 0, context: context!) + if connectedNode?.positionConfig?.smartPositionEnabled ?? false { + if lastPosition!.distance(from: LocationHelper.currentLocation) < Double(connectedNode?.positionConfig?.broadcastSmartMinimumDistance ?? 50) { + return false + } + } + } + } + lastPosition = LocationHelper.currentLocation var positionPacket = Position() positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7) positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7) @@ -800,7 +812,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.portnum = PortNum.positionApp dataMessage.wantResponse = wantResponse meshPacket.decoded = dataMessage - + var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket @@ -818,14 +830,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { if connectedPeripheral != nil { // Send a position out to the mesh if "share location with the mesh" is enabled in settings if userSettings!.provideLocation { - let success = sendPosition(destNum: connectedPeripheral.num, wantResponse: false) - if !success { - print("Failed to send position to device") - } + let _ = sendPosition(destNum: connectedPeripheral.num, wantResponse: false, smartPosition: true) } } } - + public func sendShutdown(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { var adminPacket = AdminMessage() adminPacket.shutdownSeconds = 5 @@ -846,7 +855,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return false } - + public func sendReboot(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { var adminPacket = AdminMessage() adminPacket.rebootSeconds = 5 @@ -867,7 +876,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return false } - + public func sendRebootOta(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { var adminPacket = AdminMessage() adminPacket.rebootOtaSeconds = 5 @@ -888,7 +897,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return false } - + public func sendFactoryReset(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.factoryReset = 5 @@ -902,14 +911,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage - + let messageDescription = "🚀 Sent Factory Reset Admin Message to: \(toUser.longName ?? NSLocalizedString("unknown", comment: "")) from: \(fromUser.longName ?? NSLocalizedString("unknown", comment: ""))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func sendNodeDBReset(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.nodedbReset = 5 @@ -922,7 +931,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp - + meshPacket.decoded = dataMessage let messageDescription = "🚀 Sent NodeDB Reset Admin Message to: \(toUser.longName ?? NSLocalizedString("unknown", comment: "")) from: \(fromUser.longName ?? NSLocalizedString("unknown", comment: ""))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { @@ -930,7 +939,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return false } - + public func connectToPreferredPeripheral() -> Bool { var success = false // Return false if we are not properly connected to a device, handle retry logic in the view for now @@ -948,9 +957,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return success } - + public func getChannel(channel: Channel, fromUser: UserEntity, toUser: UserEntity) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.getChannelRequest = UInt32(channel.index + 1) var meshPacket: MeshPacket = MeshPacket() @@ -963,7 +972,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true meshPacket.decoded = dataMessage - + let messageDescription = "🎛️ Requested Channel \(channel.index) for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return Int64(meshPacket.id) @@ -971,7 +980,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { return 0 } public func saveChannel(channel: Channel, fromUser: UserEntity, toUser: UserEntity) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setChannel = channel var meshPacket: MeshPacket = MeshPacket() @@ -984,20 +993,20 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true meshPacket.decoded = dataMessage - + let messageDescription = "🛟 Saved Channel \(channel.index) for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return Int64(meshPacket.id) } return 0 } - + public func saveChannelSet(base64UrlString: String) -> Bool { if isConnected { // Before we get started delete the existing channels from the myNodeInfo let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedPeripheral.num)) - + tryClearExistingChannels() let decodedString = base64UrlString.base64urlToBase64() if let decodedData = Data(base64Encoded: decodedString) { @@ -1057,8 +1066,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { let binaryData: Data = try! toRadio.serializedData() if connectedPeripheral!.peripheral.state == CBPeripheralState.connected { self.connectedPeripheral.peripheral.writeValue(binaryData, for: self.TORADIO_characteristic, type: .withResponse) - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.lora.config.sent %@", comment: "Sent a LoRaConfig for: %@"), String(connectedPeripheral.num)) - MeshLogger.log("📻 \(logString)") + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.lora.config.sent %@", comment: "Sent a LoRaConfig for: %@"), String(connectedPeripheral.num)) + MeshLogger.log("📻 \(logString)") } return true } catch { @@ -1068,7 +1077,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return false } - + public func saveUser(config: User, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setOwner = config @@ -1089,7 +1098,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return 0 } - + public func saveLicensedUser(ham: HamParameters, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setHamMode = ham @@ -1125,20 +1134,20 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage let messageDescription = "🛟 Saved Bluetooth Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { upsertBluetoothConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } - + return 0 } - + public func saveDeviceConfig(config: Config.DeviceConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setConfig.device = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1157,7 +1166,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return 0 } - + public func saveDisplayConfig(config: Config.DisplayConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.display = config @@ -1181,9 +1190,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return 0 } - + public func saveLoRaConfig(config: Config.LoRaConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setConfig.lora = config var meshPacket: MeshPacket = MeshPacket() @@ -1198,20 +1207,20 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage let messageDescription = "🛟 Saved LoRa Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } - + return 0 } - + public func savePositionConfig(config: Config.PositionConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setConfig.position = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1222,24 +1231,24 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp - + meshPacket.decoded = dataMessage - + let messageDescription = "🛟 Saved Position Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { upsertPositionConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } - + return 0 } - + public func saveNetworkConfig(config: Config.NetworkConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setConfig.network = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1250,24 +1259,24 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp - + meshPacket.decoded = dataMessage - + let messageDescription = "🛟 Saved Network Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { upsertNetworkConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } - + return 0 } - + public func saveCannedMessageModuleConfig(config: ModuleConfig.CannedMessageConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.cannedMessage = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1279,22 +1288,22 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp meshPacket.decoded = dataMessage - + let messageDescription = "🛟 Saved Canned Message Module Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { upsertCannedMessagesModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!) return Int64(meshPacket.id) } - + return 0 } - + public func saveCannedMessageModuleMessages(messages: String, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setCannedMessageModuleMessages = messages - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1307,22 +1316,22 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true meshPacket.decoded = dataMessage - + let messageDescription = "🛟 Saved Canned Message Module Messages for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { - + return Int64(meshPacket.id) } - + return 0 } - + public func saveExternalNotificationModuleConfig(config: ModuleConfig.ExternalNotificationConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.externalNotification = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1330,12 +1339,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { - + var adminPacket = AdminMessage() adminPacket.setRingtoneMessage = ringtone - + var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.mqtt = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.rangeTest = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.serial = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { - + var adminPacket = AdminMessage() adminPacket.setModuleConfig.telemetry = config - + var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { - + var adminPacket = AdminMessage() adminPacket.getChannelRequest = channelIndex - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { - + var adminPacket = AdminMessage() adminPacket.getCannedMessageModuleMessagesRequest = true - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(destNum) meshPacket.from = UInt32(connectedPeripheral.num) @@ -1518,35 +1527,35 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.wantAck = true meshPacket.decoded.wantResponse = wantResponse - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = wantResponse - + meshPacket.decoded = dataMessage - + var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket - + let binaryData: Data = try! toRadio.serializedData() - + if connectedPeripheral!.peripheral.state == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.cannedmessages.messages.get %@", comment: "Requested Canned Messages Module Messages for node: %@"), String(connectedPeripheral.num)) MeshLogger.log("🥫 \(logString)") return true } - + return false } - + public func requestBluetoothConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.bluetoothConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1554,27 +1563,27 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Bluetooth Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestDeviceConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.deviceConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1582,27 +1591,27 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Device Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestDisplayConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.displayConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1610,27 +1619,27 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Display Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestLoRaConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.loraConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1638,29 +1647,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested LoRa Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { - + return true } - + return false } - + public func requestNetworkConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.networkConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1668,26 +1677,26 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Network Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" - + if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestPositionConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.positionConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1695,26 +1704,26 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Position Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestCannedMessagesModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.cannedmsgConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1722,26 +1731,26 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Canned Messages Module Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestExternalNotificationModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.extnotifConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1749,14 +1758,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested External Notificaiton Module Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true @@ -1765,10 +1774,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } public func requestRtttlConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getRingtoneRequest = true - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1776,26 +1785,26 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested RTTTL Ringtone Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestRangeTestModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.rangetestConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1803,26 +1812,26 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Range Test Module Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestMqttModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.mqttConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1830,26 +1839,26 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested MQTT Module Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestSerialModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.serialConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1857,26 +1866,26 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Serial Module Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + public func requestTelemetryModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { - + var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.telemetryConfig - + var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1884,29 +1893,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { meshPacket.priority = MeshPacket.Priority.reliable meshPacket.channel = UInt32(adminIndex) meshPacket.wantAck = true - + var dataMessage = DataMessage() dataMessage.payload = try! adminPacket.serializedData() dataMessage.portnum = PortNum.adminApp dataMessage.wantResponse = true - + meshPacket.decoded = dataMessage - + let messageDescription = "🛎️ Requested Telemetry Module Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) { return true } return false } - + // Send an admin message to a radio, save a message to core data for logging private func sendAdminMessageToRadio(meshPacket: MeshPacket, adminDescription: String, fromUser: UserEntity, toUser: UserEntity) -> Bool { - + var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket let binaryData: Data = try! toRadio.serializedData() - + if connectedPeripheral!.peripheral.state == CBPeripheralState.connected { let newMessage = MessageEntity(context: context!) newMessage.messageId = Int64(meshPacket.id) @@ -1916,7 +1925,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { newMessage.adminDescription = adminDescription newMessage.fromUser = fromUser newMessage.toUser = toUser - + do { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) try context!.save() @@ -1930,12 +1939,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return false } - + public func tryClearExistingChannels() { // Before we get started delete the existing channels from the myNodeInfo let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedPeripheral.num)) - + do { let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as? [MyInfoEntity] ?? [] if fetchedMyInfo.count == 1 { @@ -1956,7 +1965,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { // MARK: - CB Central Manager implmentation extension BLEManager: CBCentralManagerDelegate { - + // MARK: Bluetooth enabled/disabled func centralManagerDidUpdateState(_ central: CBCentralManager) { if central.state == CBManagerState.poweredOn { @@ -1966,9 +1975,9 @@ extension BLEManager: CBCentralManagerDelegate { } else { isSwitchedOn = false } - + var status = "" - + switch central.state { case .poweredOff: status = "BLE is powered off" @@ -1987,10 +1996,10 @@ extension BLEManager: CBCentralManagerDelegate { } print("BLEManager status: \(status)") } - + // Called each time a peripheral is discovered func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { - + if self.automaticallyReconnect && timeoutTimerRuns < 2 && peripheral.identifier.uuidString == UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" { self.connectTo(peripheral: peripheral) print("ℹ️ BLE Reconnecting to prefered peripheral: \(peripheral.name ?? "Unknown")") @@ -1998,7 +2007,7 @@ extension BLEManager: CBCentralManagerDelegate { let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String let device = Peripheral(id: peripheral.identifier.uuidString, num: 0, name: name ?? "Unknown", shortName: "????", longName: name ?? "Unknown", firmwareVersion: "Unknown", rssi: RSSI.intValue, lastUpdate: Date(), peripheral: peripheral) let index = peripherals.map { $0.peripheral }.firstIndex(of: peripheral) - + if let peripheralIndex = index { peripherals[peripheralIndex] = device } else { @@ -2008,29 +2017,29 @@ extension BLEManager: CBCentralManagerDelegate { let visibleDuration = Calendar.current.date(byAdding: .second, value: -5, to: today)! self.peripherals.removeAll(where: { $0.lastUpdate < visibleDuration}) } - -// func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { -// -// guard let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] else { -// return -// } -// -// if peripherals.count > 0 { -// -// for peripheral in peripherals { -// print(peripheral) -// switch peripheral.state { -// case .connecting: // I've only seen this happen when -// // re-launching attached to Xcode. -// print("Xcode Restore") -// -// case .connected: -// connectTo(peripheral: peripheral) -// print("Restore BLE State") -// default: break -// } -// } -// } -// print("willRestoreState Hit!") -// } + + // func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { + // + // guard let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] else { + // return + // } + // + // if peripherals.count > 0 { + // + // for peripheral in peripherals { + // print(peripheral) + // switch peripheral.state { + // case .connecting: // I've only seen this happen when + // // re-launching attached to Xcode. + // print("Xcode Restore") + // + // case .connected: + // connectTo(peripheral: peripheral) + // print("Restore BLE State") + // default: break + // } + // } + // } + // print("willRestoreState Hit!") + // } } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 531c3672..5a435f24 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -339,7 +339,7 @@ struct ChannelMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false) { + if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false, smartPosition: false) { print("Location Sent") } } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index dfdb4046..1c3edb9d 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -345,7 +345,7 @@ struct UserMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: user.num, wantResponse: true) { + if bleManager.sendPosition(destNum: user.num, wantResponse: true, smartPosition: false) { print("Location Sent") } } diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 6b36e95c..55d61401 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -288,7 +288,7 @@ struct PositionConfig: View { Button(buttonText) { if fixedPosition { - _ = bleManager.sendPosition(destNum: node!.num, wantResponse: true) + _ = bleManager.sendPosition(destNum: node!.num, wantResponse: true, smartPosition: false) } let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) From 86cf2bb587a04034ebb15711e9bbea5991a8f97c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 28 Mar 2023 17:19:13 -0700 Subject: [PATCH 21/21] Fix send position function for catalyst --- Meshtastic/Views/Messages/ChannelMessageList.swift | 2 +- Meshtastic/Views/Messages/UserMessageList.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 5a435f24..3297f28f 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -322,7 +322,7 @@ struct ChannelMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false) { + if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false, smartPosition: false) { print("Location Sent") } } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 1c3edb9d..ee6ee822 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -328,7 +328,7 @@ struct UserMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: user.num, wantResponse: true) { + if bleManager.sendPosition(destNum: user.num, wantResponse: true, smartPosition: false) { print("Location Sent") } }