From a0693fe6c0751d997ebeaea43224cb31fe066ce3 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 25 Jan 2024 18:39:41 -0800 Subject: [PATCH 01/22] Bump version, update lora channel text --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- Meshtastic/Views/Settings/Config/LoRaConfig.swift | 2 +- en.lproj/Localizable.strings | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index cb6e47ba..7edfcbf1 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1492,7 +1492,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.20; + MARKETING_VERSION = 2.2.21; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1526,7 +1526,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.20; + MARKETING_VERSION = 2.2.21; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1648,7 +1648,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.20; + MARKETING_VERSION = 2.2.21; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1681,7 +1681,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.20; + MARKETING_VERSION = 2.2.21; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index b746dfcb..9b7b33a2 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -176,7 +176,7 @@ struct LoRaConfig: View { .scrollDismissesKeyboard(.immediately) .focused($focusedField, equals: .channelNum) } - Text("This determines the actual frequency you are transmitting on in the band.") + Text("This determines the actual frequency you are transmitting on in the band. If set to 0 this value will be calculated automatically based on the primary channel name.") .font(.caption) Toggle(isOn: $rxBoostedGain) { Label("RX Boosted Gain", systemImage: "waveform.badge.plus") diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 3a5ab36e..16ae26bc 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -73,7 +73,7 @@ "device.role.lostandfound"="Used to automatically send a text message to the mesh with the current position of the device on a frequent interval: \"I'm lost! Position: lat / long\""; "device.role.router"="Router - Mesh packets will prefer to be routed over this node. Assumes device will operate in a standalone manner while placed in a location with a coverage advantage. WARNING: The BLE/Wi-Fi radios and the OLED screen will be put to sleep."; "device.role.routerclient"="Router Client - Hybrid of the Client and Router roles. Similar to Router, except the Router Client can be used as both a Router and an app connected Client. BLE/Wi-Fi and OLED screen will not be put to sleep."; -"device.role.repeater"="Repeater - Mesh packets will prefer to be routed over this node. This role eliminates unnecessary overhead such as NodeInfo, DeviceTelemetry, and any other mesh packet, resulting in the device not appearing as part of the network. Please see Rebroadcast Mode for additional settings specific to this role."; +"device.role.repeater"="Repeater - Mesh packets will prefer to be routed over this node. This role eliminates all features other than mesh routing, this node will not even appear as part of the network. Please see Rebroadcast Mode for additional settings specific to this role."; "device.role.tracker"="Tracker - For use with devices intended as a GPS tracker. Position packets sent from this device will be higher priority, with position broadcasting every two minutes. Smart Position Broadcast will default to off."; "device.role.tak"="Used for nodes dedicated for connection to an ATAK EUD. Turns off many of the routine broadcasts to favor CoT packet stream from the Meshtastic ATAK plugin -> IMeshService -> Node"; "direct.messages"="Direct Messages"; From 1bec0dae66d3e9710b0dd962a25ceddd2d8a95fe Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 26 Jan 2024 17:42:13 -0800 Subject: [PATCH 02/22] Add ignoremqtt to mqttconfig upsert method --- Meshtastic/Persistence/UpdateCoreData.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 0794cbea..9de520e8 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -492,6 +492,7 @@ func upsertLoRaConfigPacket(config: Meshtastic.Config.LoRaConfig, nodeNum: Int64 newLoRaConfig.txEnabled = config.txEnabled newLoRaConfig.channelNum = Int32(config.channelNum) newLoRaConfig.sx126xRxBoostedGain = config.sx126XRxBoostedGain + newLoRaConfig.ignoreMqtt = config.ignoreMqtt fetchedNode[0].loRaConfig = newLoRaConfig } else { fetchedNode[0].loRaConfig?.regionCode = Int32(config.region.rawValue) @@ -508,6 +509,8 @@ func upsertLoRaConfigPacket(config: Meshtastic.Config.LoRaConfig, nodeNum: Int64 fetchedNode[0].loRaConfig?.txEnabled = config.txEnabled fetchedNode[0].loRaConfig?.channelNum = Int32(config.channelNum) fetchedNode[0].loRaConfig?.sx126xRxBoostedGain = config.sx126XRxBoostedGain + fetchedNode[0].loRaConfig?.ignoreMqtt = config.ignoreMqtt + fetchedNode[0].loRaConfig?.sx126xRxBoostedGain = config.sx126XRxBoostedGain } do { try context.save() From 350ec121d7ef4e08353e17282956064a8d953607 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 28 Jan 2024 09:36:19 -0800 Subject: [PATCH 03/22] Revert broken channel key validation Remove block range test Add notes that router role is not for mobile nodes --- Meshtastic/Extensions/UserDefaults.swift | 8 ---- Meshtastic/Helpers/BLEManager.swift | 6 +-- Meshtastic/Helpers/MeshPackets.swift | 6 +-- Meshtastic/Views/Settings/AppSettings.swift | 10 ----- Meshtastic/Views/Settings/Channels.swift | 45 ++++++--------------- en.lproj/Localizable.strings | 4 +- 6 files changed, 19 insertions(+), 60 deletions(-) diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 00b5b6e3..6a4c54a4 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -9,7 +9,6 @@ import Foundation extension UserDefaults { enum Keys: String, CaseIterable { - case enableRangeTest case preferredPeripheralId case preferredPeripheralNum case provideLocation @@ -32,13 +31,6 @@ extension UserDefaults { func reset() { Keys.allCases.forEach { removeObject(forKey: $0.rawValue) } } - static var blockRangeTest: Bool { - get { - UserDefaults.standard.bool(forKey: "blockRangeTest") - } set { - UserDefaults.standard.set(newValue, forKey: "blockRangeTest") - } - } static var preferredPeripheralId: String { get { UserDefaults.standard.string(forKey: "preferredPeripheralId") ?? "" diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 155f19bf..bcb027fd 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -594,7 +594,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Log any other unknownApp calls if !nowKnown { MeshLogger.log("🕸️ MESH PACKET received for Unknown App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") } case .textMessageApp, .detectionSensorApp: - textMessageAppPacket(packet: decodedInfo.packet, blockRangeTest: UserDefaults.blockRangeTest, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) + textMessageAppPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) case .remoteHardwareApp: MeshLogger.log("🕸️ MESH PACKET received for Remote Hardware App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") case .positionApp: @@ -620,8 +620,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate MeshLogger.log("🕸️ MESH PACKET received for Store and Forward App - Store and Forward is disabled.") } case .rangeTestApp: - if wantRangeTestPackets && !UserDefaults.blockRangeTest { - textMessageAppPacket(packet: decodedInfo.packet, blockRangeTest: false, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) + if wantRangeTestPackets { + textMessageAppPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) } else { MeshLogger.log("🕸️ MESH PACKET received for Range Test App Range testing is disabled.") diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 9b720845..d2e6a8f1 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -721,13 +721,9 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage } } -func textMessageAppPacket(packet: MeshPacket, blockRangeTest: Bool, connectedNode: Int64, context: NSManagedObjectContext) { +func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) { if let messageText = String(bytes: packet.decoded.payload, encoding: .utf8) { - - if blockRangeTest && messageText.starts(with: "seq ") { - return - } MeshLogger.log("💬 \("mesh.log.textmessage.received".localized)") diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index ba705afc..f9520b91 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -11,7 +11,6 @@ struct AppSettings: View { @State var totalDownloadedTileSize = "" @StateObject var locationHelper = LocationHelper() @State var provideLocation: Bool = UserDefaults.provideLocation - @State var blockRangeTest: Bool = UserDefaults.blockRangeTest @State var useLegacyMap: Bool = UserDefaults.mapUseLegacy @State var provideLocationInterval: Int = UserDefaults.provideLocationInterval @State private var isPresentingCoreDataResetConfirm = false @@ -20,12 +19,6 @@ struct AppSettings: View { VStack { Form { Section(header: Text("options")) { - - Toggle(isOn: $blockRangeTest) { - Label("range.test.blocked", systemImage: "x.circle") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $useLegacyMap) { Label("map.use.legacy", systemImage: "map") } @@ -151,9 +144,6 @@ struct AppSettings: View { self.bleManager.context = context } } - .onChange(of: blockRangeTest) { newBlockRangeTest in - UserDefaults.blockRangeTest = newBlockRangeTest - } .onChange(of: provideLocation) { newProvideLocation in UserDefaults.provideLocation = newProvideLocation if bleManager.connectedPeripheral != nil { diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 88bcd2ab..b8ad588c 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -1,6 +1,6 @@ // -// ShareChannel.swift -// MeshtasticApple +// Channels.swift +// Meshtastic Apple // // Copyright(c) Garth Vander Houwen 4/8/22. // @@ -25,7 +25,6 @@ struct Channels: View { var node: NodeInfoEntity? @State var hasChanges = false - @State var hasValidKey = false @State private var isPresentingEditView = false @State private var isPresentingSaveConfirm: Bool = false @State private var channelIndex: Int32 = 0 @@ -168,34 +167,16 @@ struct Channels: View { HStack(alignment: .top) { Text("Key") Spacer() - TextField( - "Key", - text: $channelKey - ) - .padding(4) - .disableAutocorrection(true) - .keyboardType(.alphabet) - .foregroundColor(Color.gray) - .textSelection(.enabled) - .background( - RoundedRectangle(cornerRadius: 25.0) - .stroke( - hasValidKey ? - Color.green : - Color.red - , lineWidth: 2.0) - ) - .onChange(of: channelKey, perform: { _ in - let tempKey = Data(base64Encoded: channelKey) ?? Data() - if tempKey.count == channelKeySize || channelKeySize == -1{ - hasValidKey = true - } - else { - hasValidKey = false - } - hasChanges = true - }) - .disabled(channelKeySize <= 0) + Text(channelKey) + .foregroundColor(Color.gray) + .textSelection(.enabled) +// TextField( +// "", +// text: $channelKey, +// axis: .vertical +// ) +// .foregroundColor(Color.gray) +// .disabled(true) } Picker("Channel Role", selection: $channelRole) { if channelRole == 1 { @@ -275,7 +256,7 @@ struct Channels: View { } label: { Label("save", systemImage: "square.and.arrow.down") } - .disabled(bleManager.connectedPeripheral == nil || !hasChanges || !hasValidKey) + .disabled(bleManager.connectedPeripheral == nil || !hasChanges) .buttonStyle(.bordered) .buttonBorderShape(.capsule) .controlSize(.large) diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 16ae26bc..425b8e01 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -71,8 +71,8 @@ "device.role.clienthidden"=" Used for nodes that \"only speak when spoken to\" Turns all of the routine broadcasts but allows for ad-hoc communication. Still rebroadcasts, but with local only rebroadcast mode (known meshes only). Can be used for private operation or to dramatically reduce airtime / power consumption."; "device.role.clientmute"="Client Mute - Same as a client except packets will not hop over this node, does not contribute to routing packets for mesh."; "device.role.lostandfound"="Used to automatically send a text message to the mesh with the current position of the device on a frequent interval: \"I'm lost! Position: lat / long\""; -"device.role.router"="Router - Mesh packets will prefer to be routed over this node. Assumes device will operate in a standalone manner while placed in a location with a coverage advantage. WARNING: The BLE/Wi-Fi radios and the OLED screen will be put to sleep."; -"device.role.routerclient"="Router Client - Hybrid of the Client and Router roles. Similar to Router, except the Router Client can be used as both a Router and an app connected Client. BLE/Wi-Fi and OLED screen will not be put to sleep."; +"device.role.router"="Router - Assumes device will operate in a standalone manner while placed in a location with a coverage advantage, not for mobile nodes. WARNING: The BLE/Wi-Fi radios and the OLED screen will be put to sleep."; +"device.role.routerclient"="Router Client - Hybrid of the Client and Router roles. Similar to Router, except the Router Client can be used as both a Router and an app connected Client. Also not for mobile nodes. BLE/Wi-Fi and OLED screen will not be put to sleep."; "device.role.repeater"="Repeater - Mesh packets will prefer to be routed over this node. This role eliminates all features other than mesh routing, this node will not even appear as part of the network. Please see Rebroadcast Mode for additional settings specific to this role."; "device.role.tracker"="Tracker - For use with devices intended as a GPS tracker. Position packets sent from this device will be higher priority, with position broadcasting every two minutes. Smart Position Broadcast will default to off."; "device.role.tak"="Used for nodes dedicated for connection to an ATAK EUD. Turns off many of the routine broadcasts to favor CoT packet stream from the Meshtastic ATAK plugin -> IMeshService -> Node"; From f61e91a282706b2da4a298d7b3843d17d5d13a9f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 28 Jan 2024 16:22:25 -0800 Subject: [PATCH 04/22] Add back route recorder --- Meshtastic/Enums/LoraConfigEnums.swift | 51 +- Meshtastic/Views/Settings/RouteRecorder.swift | 584 +++++++++--------- Meshtastic/Views/Settings/Settings.swift | 28 +- 3 files changed, 357 insertions(+), 306 deletions(-) diff --git a/Meshtastic/Enums/LoraConfigEnums.swift b/Meshtastic/Enums/LoraConfigEnums.swift index 85ed35ce..3e638773 100644 --- a/Meshtastic/Enums/LoraConfigEnums.swift +++ b/Meshtastic/Enums/LoraConfigEnums.swift @@ -24,6 +24,8 @@ enum RegionCodes: Int, CaseIterable, Identifiable { case th = 12 case ua433 = 14 case ua868 = 15 + case my_433 = 16 + case my_919 = 17 case lora24 = 13 var id: Int { self.rawValue } @@ -61,9 +63,52 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return "Ukraine 868mhz" case .lora24: return "2.4 GHZ" + case .my_433: + return "Malaysia 433mhz" + case .my_919: + return "Malaysia 919mhz" + } + } + var dutyCycle: Int { + switch self { + case .unset: + return 0 + case .us: + return 100 + case .eu433: + return 10 + case .eu868: + return 10 + case .cn: + return 100 + case .jp: + return 100 + case .anz: + return 100 + case .kr: + return 100 + case .tw: + return 100 + case .ru: + return 100 + case .in: + return 100 + case .nz865: + return 100 + case .th: + return 100 + case .ua433: + return 10 + case .ua868: + return 10 + case .lora24: + return 100 + case .my_433: + return 100 + case .my_919: + return 100 } } - func protoEnumValue() -> Config.LoRaConfig.RegionCode { switch self { @@ -99,6 +144,10 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return Config.LoRaConfig.RegionCode.ua868 case .lora24: return Config.LoRaConfig.RegionCode.lora24 + case .my_433: + return Config.LoRaConfig.RegionCode.my433 + case .my_919: + return Config.LoRaConfig.RegionCode.my919 } } } diff --git a/Meshtastic/Views/Settings/RouteRecorder.swift b/Meshtastic/Views/Settings/RouteRecorder.swift index 71794725..88c4a44e 100644 --- a/Meshtastic/Views/Settings/RouteRecorder.swift +++ b/Meshtastic/Views/Settings/RouteRecorder.swift @@ -1,295 +1,295 @@ -//// -//// Routes.swift -//// Meshtastic -//// -//// Created by Garth Vander Houwen on 11/21/23. -//// // -//import SwiftUI -//import CoreData -//import MapKit -//import CoreLocation -//import CoreMotion +// Routes.swift +// Meshtastic // -//@available(iOS 17.0, macOS 14.0, *) -//struct RouteRecorder: View { -// -// @ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared -// @Environment(\.managedObjectContext) var context -// @State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic) -// //@State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true) -// @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic) -// @State var isShowingDetails = false -// @Namespace var namespace -// @Namespace var routerecorderscope -// @State var recording: RouteEntity? -// @State var color: Color = .blue -// -// var body: some View { -// VStack { -// ZStack { -// Map(position: $position, scope: routerecorderscope) { -// UserAnnotation() -// /// Route Lines -// let lineCoords = locationsHandler.locationsArray.compactMap({(position) -> CLLocationCoordinate2D in -// return position.coordinate -// }) -// -// let gradient = LinearGradient( -// colors: [color], -// startPoint: .leading, endPoint: .trailing -// ) -// let dashed = StrokeStyle( -// lineWidth: 3, -// lineCap: .round, lineJoin: .round, dash: [10, 10] -// ) -// MapPolyline(coordinates: lineCoords) -// .stroke(gradient, style: dashed) +// Created by Garth Vander Houwen on 11/21/23. // -// } -// .mapStyle(mapStyle) -// } -// .mapScope(routerecorderscope) -// .safeAreaInset(edge: .bottom) { -// ZStack { -// VStack { -// HStack(spacing: 10) { -// Spacer() -// -// Button { -// isShowingDetails = true -// } label: { -// Image(systemName: locationsHandler.isRecording ? "record.circle.fill" : "record.circle") -// .font(.system(size: 72)) -// .symbolRenderingMode(.multicolor) -// .foregroundColor(.red) -// } -// .buttonStyle(.bordered) -// .foregroundColor(.red) -// .buttonBorderShape(.circle) -// .matchedGeometryEffect(id: "Details Button", in: namespace) -// -// Spacer() -// } -// } -// } -// .padding() -// } -// .sheet(isPresented: $isShowingDetails) { -// NavigationStack { -// VStack { -// if locationsHandler.isRecording { -// HStack (alignment: .center) { -// Image(systemName: "record.circle.fill") -// .symbolRenderingMode(.multicolor) -// .font(.title) -// .foregroundColor(.red) -// Text("Recording route") -// .font(.title) -// Spacer() -// Text("\(locationsHandler.count)") -// .foregroundColor(.red) -// .font(.title2) -// } -// .padding() -// } else if locationsHandler.isRecordingPaused { -// HStack (alignment: .center) { -// -// Image(systemName: "playpause") -// .symbolRenderingMode(.multicolor) -// .font(.title3) -// .foregroundColor(.red) -// Text("Route recording paused") -// .font(.title) -// } -// .padding(.top) -// } -// -// if locationsHandler.isRecording || locationsHandler.isRecordingPaused { -// Divider() -// HStack { -// VStack { -// Text(locationsHandler.recordingStarted ?? Date(), style: .timer) -// .font(.title) -// .fixedSize() -// Text("Time") -// .font(.callout) -// .fixedSize() -// } -// .padding(.horizontal) -// Divider() -// VStack { -// let distance = Measurement(value: locationsHandler.distanceTraveled, unit: UnitLength.meters) -// Text("\(distance.formatted())") -// .font(.title) -// .fixedSize() -// Text("Distance") -// .font(.callout) -// .fixedSize() -// } -// .padding(.horizontal) -// Divider() -// VStack { -// let gain = Measurement(value: locationsHandler.elevationGain, unit: UnitLength.meters) -// Text(gain.formatted()) -// .font(.title) -// Text("Elev. Gain") -// .font(.callout) -// } -// .padding(.horizontal) -// } -// .frame(maxHeight: 90) -// } -// Divider() -// VStack(alignment: .leading) { -// List { -// GPSStatus(largeFont: .body, smallFont: .callout) -// } -// .listStyle(.plain) -// HStack { -// Spacer() -// if !locationsHandler.isRecording && !locationsHandler.isRecordingPaused { -// /// We are not recording or paused, show start recording button -// Button { -// locationsHandler.isRecording = true -// locationsHandler.count = 0 -// locationsHandler.distanceTraveled = 0.0 -// locationsHandler.elevationGain = 0.0 -// locationsHandler.locationsArray.removeAll() -// locationsHandler.recordingStarted = Date() -// let newRoute = RouteEntity(context: context) -// newRoute.name = String("Route Recording") -// newRoute.id = Int32.random(in: Int32(Int8.max) ... Int32.max) -// newRoute.color = Int64(UIColor.random.hex) -// newRoute.date = Date() -// newRoute.enabled = false -// color = Color(UIColor(hex: UInt32(newRoute.color))) -// self.recording = newRoute -// do { -// try context.save() -// print("💾 Saved a new route") -// } catch { -// context.rollback() -// let nsError = error as NSError -// print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") -// } -// } label: { -// Label("start", systemImage: "play") -// } -// .buttonStyle(.bordered) -// .buttonBorderShape(.capsule) -// .controlSize(.large) -// .padding(.bottom) -// -// } else if locationsHandler.isRecording { -// /// We are recording show pause button -// Button { -// locationsHandler.isRecording = false -// locationsHandler.isRecordingPaused = true -// } label: { -// Label("pause", systemImage: "pause") -// } -// .buttonStyle(.bordered) -// .buttonBorderShape(.capsule) -// .controlSize(.large) -// .padding(.bottom) -// } else if locationsHandler.isRecordingPaused { -// /// We are paused show resume button -// Button { -// locationsHandler.isRecording = true -// locationsHandler.isRecordingPaused = false -// } label: { -// Label("resume", systemImage: "playpause") -// } -// .buttonStyle(.bordered) -// .buttonBorderShape(.capsule) -// .controlSize(.large) -// .padding(.bottom) -// } -// -// if locationsHandler.isRecording || locationsHandler.isRecordingPaused { -// /// We are recording or paused, show finish button -// Button { -// locationsHandler.isRecording = false -// locationsHandler.isRecordingPaused = false -// locationsHandler.distanceTraveled = 0.0 -// locationsHandler.elevationGain = 0.0 -// locationsHandler.locationsArray.removeAll() -// locationsHandler.recordingStarted = nil -// if let rec = recording { -// rec.enabled = true -// context.refresh(rec, mergeChanges:true) -// } -// -// do { -// try context.save() -// print("💾 Saved a route finish") -// } catch { -// context.rollback() -// let nsError = error as NSError -// print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") -// } -// } label: { -// Label("finish", systemImage: "flag.checkered") -// } -// .buttonStyle(.bordered) -// .buttonBorderShape(.capsule) -// .controlSize(.large) -// .padding(.bottom) -// } -//#if targetEnvironment(macCatalyst) -// Button(role: .cancel) { -// isShowingDetails = false -// } label: { -// Label("close", systemImage: "xmark") -// } -// .buttonStyle(.bordered) -// .buttonBorderShape(.capsule) -// .controlSize(.large) -// .padding(.bottom) -//#endif -// Spacer() -// } -// -// } -// } -// } -// .presentationDetents([.fraction(0.30), .fraction(0.65)]) -// .presentationDragIndicator(.hidden) -// .interactiveDismissDisabled(false) -// .onAppear { -// UIApplication.shared.isIdleTimerDisabled = true -// } -// .onDisappear(perform: { -// UIApplication.shared.isIdleTimerDisabled = false -// }) -// .onChange(of: locationsHandler.locationsArray.last) { newLoc in -// if locationsHandler.isRecording { -// if let loc = newLoc { -// if recording != nil { -// let locationEntity = LocationEntity(context: context) -// locationEntity.routeLocation = recording -// locationEntity.id = Int32(locationsHandler.count) -// locationEntity.altitude = Int32(loc.altitude) -// locationEntity.heading = Int32(loc.course) -// locationEntity.speed = Int32(loc.speed) -// locationEntity.latitudeI = Int32(loc.coordinate.latitude * 1e7) -// locationEntity.longitudeI = Int32(loc.coordinate.longitude * 1e7) -// do { -// try context.save() -// print("💾 Saved a new route location") -// //print("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num)") -// } catch { -// context.rollback() -// let nsError = error as NSError -// print("💥 Error Saving LocationEntity from the Route Recorder \(nsError)") -// } -// } -// } -// } -// } -// } -// } -// .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) -// } -//} + +import SwiftUI +import CoreData +import MapKit +import CoreLocation +import CoreMotion + +@available(iOS 17.0, macOS 14.0, *) +struct RouteRecorder: View { + + @ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared + @Environment(\.managedObjectContext) var context + @State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic) + //@State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true) + @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic) + @State var isShowingDetails = false + @Namespace var namespace + @Namespace var routerecorderscope + @State var recording: RouteEntity? + @State var color: Color = .blue + + var body: some View { + VStack { + ZStack { + Map(position: $position, scope: routerecorderscope) { + UserAnnotation() + /// Route Lines + let lineCoords = locationsHandler.locationsArray.compactMap({(position) -> CLLocationCoordinate2D in + return position.coordinate + }) + + let gradient = LinearGradient( + colors: [color], + startPoint: .leading, endPoint: .trailing + ) + let dashed = StrokeStyle( + lineWidth: 3, + lineCap: .round, lineJoin: .round, dash: [10, 10] + ) + MapPolyline(coordinates: lineCoords) + .stroke(gradient, style: dashed) + + } + .mapStyle(mapStyle) + } + .mapScope(routerecorderscope) + .safeAreaInset(edge: .bottom) { + ZStack { + VStack { + HStack(spacing: 10) { + Spacer() + + Button { + isShowingDetails = true + } label: { + Image(systemName: locationsHandler.isRecording ? "record.circle.fill" : "record.circle") + .font(.system(size: 72)) + .symbolRenderingMode(.multicolor) + .foregroundColor(.red) + } + .buttonStyle(.bordered) + .foregroundColor(.red) + .buttonBorderShape(.circle) + .matchedGeometryEffect(id: "Details Button", in: namespace) + + Spacer() + } + } + } + .padding() + } + .sheet(isPresented: $isShowingDetails) { + NavigationStack { + VStack { + if locationsHandler.isRecording { + HStack (alignment: .center) { + Image(systemName: "record.circle.fill") + .symbolRenderingMode(.multicolor) + .font(.title) + .foregroundColor(.red) + Text("Recording route") + .font(.title) + Spacer() + Text("\(locationsHandler.count)") + .foregroundColor(.red) + .font(.title2) + } + .padding() + } else if locationsHandler.isRecordingPaused { + HStack (alignment: .center) { + + Image(systemName: "playpause") + .symbolRenderingMode(.multicolor) + .font(.title3) + .foregroundColor(.red) + Text("Route recording paused") + .font(.title) + } + .padding(.top) + } + + if locationsHandler.isRecording || locationsHandler.isRecordingPaused { + Divider() + HStack { + VStack { + Text(locationsHandler.recordingStarted ?? Date(), style: .timer) + .font(.title) + .fixedSize() + Text("Time") + .font(.callout) + .fixedSize() + } + .padding(.horizontal) + Divider() + VStack { + let distance = Measurement(value: locationsHandler.distanceTraveled, unit: UnitLength.meters) + Text("\(distance.formatted())") + .font(.title) + .fixedSize() + Text("Distance") + .font(.callout) + .fixedSize() + } + .padding(.horizontal) + Divider() + VStack { + let gain = Measurement(value: locationsHandler.elevationGain, unit: UnitLength.meters) + Text(gain.formatted()) + .font(.title) + Text("Elev. Gain") + .font(.callout) + } + .padding(.horizontal) + } + .frame(maxHeight: 90) + } + Divider() + VStack(alignment: .leading) { + List { + GPSStatus(largeFont: .body, smallFont: .callout) + } + .listStyle(.plain) + HStack { + Spacer() + if !locationsHandler.isRecording && !locationsHandler.isRecordingPaused { + /// We are not recording or paused, show start recording button + Button { + locationsHandler.isRecording = true + locationsHandler.count = 0 + locationsHandler.distanceTraveled = 0.0 + locationsHandler.elevationGain = 0.0 + locationsHandler.locationsArray.removeAll() + locationsHandler.recordingStarted = Date() + let newRoute = RouteEntity(context: context) + newRoute.name = String("Route Recording") + newRoute.id = Int32.random(in: Int32(Int8.max) ... Int32.max) + newRoute.color = Int64(UIColor.random.hex) + newRoute.date = Date() + newRoute.enabled = false + color = Color(UIColor(hex: UInt32(newRoute.color))) + self.recording = newRoute + do { + try context.save() + print("💾 Saved a new route") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") + } + } label: { + Label("start", systemImage: "play") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + + } else if locationsHandler.isRecording { + /// We are recording show pause button + Button { + locationsHandler.isRecording = false + locationsHandler.isRecordingPaused = true + } label: { + Label("pause", systemImage: "pause") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + } else if locationsHandler.isRecordingPaused { + /// We are paused show resume button + Button { + locationsHandler.isRecording = true + locationsHandler.isRecordingPaused = false + } label: { + Label("resume", systemImage: "playpause") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + } + + if locationsHandler.isRecording || locationsHandler.isRecordingPaused { + /// We are recording or paused, show finish button + Button { + locationsHandler.isRecording = false + locationsHandler.isRecordingPaused = false + locationsHandler.distanceTraveled = 0.0 + locationsHandler.elevationGain = 0.0 + locationsHandler.locationsArray.removeAll() + locationsHandler.recordingStarted = nil + if let rec = recording { + rec.enabled = true + context.refresh(rec, mergeChanges:true) + } + + do { + try context.save() + print("💾 Saved a route finish") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") + } + } label: { + Label("finish", systemImage: "flag.checkered") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + } +#if targetEnvironment(macCatalyst) + Button(role: .cancel) { + isShowingDetails = false + } label: { + Label("close", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) +#endif + Spacer() + } + + } + } + } + .presentationDetents([.fraction(0.30), .fraction(0.65)]) + .presentationDragIndicator(.hidden) + .interactiveDismissDisabled(false) + .onAppear { + UIApplication.shared.isIdleTimerDisabled = true + } + .onDisappear(perform: { + UIApplication.shared.isIdleTimerDisabled = false + }) + .onChange(of: locationsHandler.locationsArray.last) { newLoc in + if locationsHandler.isRecording { + if let loc = newLoc { + if recording != nil { + let locationEntity = LocationEntity(context: context) + locationEntity.routeLocation = recording + locationEntity.id = Int32(locationsHandler.count) + locationEntity.altitude = Int32(loc.altitude) + locationEntity.heading = Int32(loc.course) + locationEntity.speed = Int32(loc.speed) + locationEntity.latitudeI = Int32(loc.coordinate.latitude * 1e7) + locationEntity.longitudeI = Int32(loc.coordinate.longitude * 1e7) + do { + try context.save() + print("💾 Saved a new route location") + //print("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num)") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Saving LocationEntity from the Route Recorder \(nsError)") + } + } + } + } + } + } + } + .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) + } +} diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 66d1e02e..297e7481 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -69,14 +69,14 @@ struct Settings: View { Text("routes") } .tag(SettingsSidebar.routes) -// NavigationLink { -// RouteRecorder() -// } label: { -// Image(systemName: "record.circle") -// .symbolRenderingMode(.hierarchical) -// Text("route.recorder") -// } -// .tag(SettingsSidebar.routeRecorder) + NavigationLink { + RouteRecorder() + } label: { + Image(systemName: "record.circle") + .symbolRenderingMode(.hierarchical) + Text("route.recorder") + } + .tag(SettingsSidebar.routeRecorder) } let node = nodes.first(where: { $0.num == preferredNodeNum }) @@ -312,13 +312,15 @@ struct Settings: View { } } .onAppear { - self.preferredNodeNum = UserDefaults.preferredPeripheralNum - if nodes.count > 1 { - if selectedNode == 0 { + if self.preferredNodeNum == 0 { + self.preferredNodeNum = UserDefaults.preferredPeripheralNum + if nodes.count > 1 { + if selectedNode == 0 { + self.selectedNode = Int(bleManager.connectedPeripheral != nil ? UserDefaults.preferredPeripheralNum : 0) + } + } else { self.selectedNode = Int(bleManager.connectedPeripheral != nil ? UserDefaults.preferredPeripheralNum : 0) } - } else { - self.selectedNode = Int(bleManager.connectedPeripheral != nil ? UserDefaults.preferredPeripheralNum : 0) } } .listStyle(GroupedListStyle()) From 1b51795cf82e9e1960a6062a292e3de3fdbdc176 Mon Sep 17 00:00:00 2001 From: Austin Payne Date: Wed, 31 Jan 2024 23:11:28 -0700 Subject: [PATCH 05/22] fix: offline map type option getting out of sync Fixes a bug where the offline map picker option was getting inverted until another action was taken, e.g. close options, change map type, etc. --- Meshtastic/Views/Nodes/NodeMap.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 109fc225..96697ddf 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -86,7 +86,7 @@ struct NodeMap: View { Section(header: Text("Map Options")) { Picker(selection: $selectedMapLayer, label: Text("")) { ForEach(MapLayer.allCases, id: \.self) { layer in - if layer == MapLayer.offline && UserDefaults.enableOfflineMaps { + if layer == MapLayer.offline && enableOfflineMaps { Text(layer.localized) } else if layer != MapLayer.offline { Text(layer.localized) From 88b0ed5ca19f24ab35d43b5d1f075f0574d3f68d Mon Sep 17 00:00:00 2001 From: Andrej Toth <48795449+tandrej98@users.noreply.github.com> Date: Sat, 3 Feb 2024 21:28:36 +0000 Subject: [PATCH 06/22] fix typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c2336593..ec8a8b5c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## Overview -SwiftUI client applicaitons for iOS, iPadOS and macOS. +SwiftUI client applications for iOS, iPadOS and macOS. ## OS Requirements From 64909fc603ad98d56357d2eeb32b18e9308d81e6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Feb 2024 21:12:17 -0800 Subject: [PATCH 07/22] Don't syncronize user defaults when clearing app data --- Meshtastic/Views/Settings/AppSettings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index f9520b91..575718e0 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -99,8 +99,8 @@ struct AppSettings: View { Button("Erase all app data?", role: .destructive) { bleManager.disconnectPeripheral() clearCoreDataDatabase(context: context) + context.refreshAllObjects() UserDefaults.standard.reset() - UserDefaults.standard.synchronize() } } } From 03e3bb2f46367db6dfc7efc45821ba6ef8df543f Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Feb 2024 21:46:11 -0800 Subject: [PATCH 08/22] Update logic when switching devices --- Meshtastic/Views/Bluetooth/Connect.swift | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 834ea5db..56fc496a 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -209,21 +209,18 @@ struct Connect: View { }.padding([.bottom, .top]) } } - .confirmationDialog("Connecting to a new radio will clear all local app data on the phone.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) { + .confirmationDialog("Connecting to a new radio will clear all local app data on the phone. The app may close.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) { Button("Connect to new radio?", role: .destructive) { - bleManager.stopScanning() - bleManager.connectedPeripheral = nil - UserDefaults.preferredPeripheralId = "" + UserDefaults.preferredPeripheralId = selectedPeripherialId if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == CBPeripheralState.connected { bleManager.disconnectPeripheral() } - - clearCoreDataDatabase(context: context) - let radio = bleManager.peripherals.first(where: { $0.peripheral.identifier.uuidString == selectedPeripherialId}) - bleManager.connectTo(peripheral: radio!.peripheral) - presentingSwitchPreferredPeripheral = false - selectedPeripherialId = "" + PersistenceController.shared.clearDatabase() + let radio = bleManager.peripherals.first(where: { $0.peripheral.identifier.uuidString == selectedPeripherialId }) + if radio != nil { + bleManager.connectTo(peripheral: radio!.peripheral) + } } } .textCase(nil) From ea4aa8a7ffbc18c9f391781a5a261bd2579370fa Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Feb 2024 21:59:54 -0800 Subject: [PATCH 09/22] Add a try catch when adding the persistant store back after switching between nodes. --- Meshtastic/Persistence/Persistence.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Persistence/Persistence.swift b/Meshtastic/Persistence/Persistence.swift index 1c768ccf..2bb447f7 100644 --- a/Meshtastic/Persistence/Persistence.swift +++ b/Meshtastic/Persistence/Persistence.swift @@ -61,8 +61,13 @@ class PersistenceController { do { try persistentStoreCoordinator.destroyPersistentStore(at: url, ofType: NSSQLiteStoreType, options: nil) - try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil) print("💥 CoreData database truncated. All app data has been erased.") + + do { + try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil) + } catch let error { + print("💣 Failed to re-create CoreData database: " + error.localizedDescription) + } } catch let error { print("💣 Failed to destroy CoreData database, delete the app and re-install to clear data. Attempted to clear persistent store: " + error.localizedDescription) From 34d4a36813c7c5222c58157b027f027296365118 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Feb 2024 22:04:40 -0800 Subject: [PATCH 10/22] Try again and crash if the persistent store is not re-created properly --- Meshtastic/Persistence/Persistence.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Meshtastic/Persistence/Persistence.swift b/Meshtastic/Persistence/Persistence.swift index 2bb447f7..9165c433 100644 --- a/Meshtastic/Persistence/Persistence.swift +++ b/Meshtastic/Persistence/Persistence.swift @@ -67,6 +67,7 @@ class PersistenceController { try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil) } catch let error { print("💣 Failed to re-create CoreData database: " + error.localizedDescription) + try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil) } } catch let error { From bd93f76ccb3affd771f084817fb11e12e23c0e32 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 4 Feb 2024 22:29:10 -0800 Subject: [PATCH 11/22] Add detection sensor localizable string to languages other than english --- de.lproj/Localizable.strings | 1 + pl.lproj/Localizable.strings | 1 + zh-Hans.lproj/Localizable.strings | 1 + 3 files changed, 3 insertions(+) diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index 8ef6b857..49ed6531 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -60,6 +60,7 @@ "current"="Current"; "default"="Standard"; "delete"="Löschen"; +"detection.sensor"="Detection Sensor"; "device"="Gerät"; "device.config"="Gerätekonfiguration"; "device.metrics.delete"="Delete all device metrics?"; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index 73b8cef2..a734697b 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -62,6 +62,7 @@ "current"="Bieżący"; "default"="Domyślny"; "delete"="Usuń"; +"detection.sensor"="Detection Sensor"; "device"="Urządzenie"; "device.config"="Konfiguracja urządzenia"; "device.metrics.delete"="Usunąć wszystkie metryki urządzenia?"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 1b9411d7..6ba34940 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -60,6 +60,7 @@ "current"="当前"; "default"="默认"; "delete"="删除"; +"detection.sensor"="Detection Sensor"; "device"="电台"; "device.config"="电台配置"; "device.metrics.delete"="删除所有电台指标?"; From 8f6e1a2d0d83ca35456533eddddfeae9f087f18d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 5 Feb 2024 21:46:16 -0800 Subject: [PATCH 12/22] Add app smart position and setting Update protobufs Use new GPS tri state on position config --- Meshtastic.xcodeproj/project.pbxproj | 4 +- Meshtastic/Enums/PositionConfigEnums.swift | 74 ++++ Meshtastic/Extensions/UserDefaults.swift | 9 + Meshtastic/Helpers/BLEManager.swift | 4 - Meshtastic/Helpers/LocationsHandler.swift | 6 +- .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 411 ++++++++++++++++++ .../Protobufs/meshtastic/admin.pb.swift | 30 ++ .../Protobufs/meshtastic/config.pb.swift | 68 +++ Meshtastic/Protobufs/meshtastic/mesh.pb.swift | 20 + Meshtastic/Views/Settings/AppSettings.swift | 5 + .../Settings/Config/PositionConfig.swift | 71 ++- en.lproj/Localizable.strings | 3 + 13 files changed, 681 insertions(+), 26 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 25.xcdatamodel/contents diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 7edfcbf1..49932aa0 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -241,6 +241,7 @@ DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; + DD23D9AB2B7133F6003F5CBE /* MeshtasticDataModelV 25.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 25.xcdatamodel"; sourceTree = ""; }; DD2553562855B02500E55709 /* LoRaConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaConfig.swift; sourceTree = ""; }; DD2553582855B52700E55709 /* PositionConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionConfig.swift; sourceTree = ""; }; DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV22.xcdatamodel; sourceTree = ""; }; @@ -1792,6 +1793,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD23D9AB2B7133F6003F5CBE /* MeshtasticDataModelV 25.xcdatamodel */, DDB234392B5CA9B000DA6FB1 /* MeshtasticDataModelV 24.xcdatamodel */, DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */, DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */, @@ -1817,7 +1819,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DDB234392B5CA9B000DA6FB1 /* MeshtasticDataModelV 24.xcdatamodel */; + currentVersion = DD23D9AB2B7133F6003F5CBE /* MeshtasticDataModelV 25.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Enums/PositionConfigEnums.swift b/Meshtastic/Enums/PositionConfigEnums.swift index 49f74cd6..5d6751d2 100644 --- a/Meshtastic/Enums/PositionConfigEnums.swift +++ b/Meshtastic/Enums/PositionConfigEnums.swift @@ -52,3 +52,77 @@ enum GpsFormats: Int, CaseIterable, Identifiable { } } } + +enum GpsUpdateIntervals: Int, CaseIterable, Identifiable { + + case thirtySeconds = 30 + case oneMinute = 60 + case fiveMinutes = 300 + case tenMinutes = 600 + case fifteenMinutes = 900 + case thirtyMinutes = 1800 + case oneHour = 3600 + case sixHours = 21600 + case twelveHours = 43200 + case twentyFourHours = 86400 + case maxInt32 = 2147483647 + + var id: Int { self.rawValue } + var description: String { + switch self { + case .thirtySeconds: + return "interval.thirty.seconds".localized + case .oneMinute: + return "interval.one.minute".localized + case .fiveMinutes: + return "interval.five.minutes".localized + case .tenMinutes: + return "interval.ten.minutes".localized + case .fifteenMinutes: + return "interval.fifteen.minutes".localized + case .thirtyMinutes: + return "interval.thirty.minutes".localized + case .oneHour: + return "interval.one.hour".localized + case .sixHours: + return "interval.six.hours".localized + case .twelveHours: + return "interval.twelve.hours".localized + case .twentyFourHours: + return "interval.twentyfour.hours".localized + case .maxInt32: + return "on.boot" + } + } +} + +enum GpsMode: Int, CaseIterable, Equatable { + case disabled = 0 + case enabled = 1 + case notPresent = 2 + + var id: Int { self.rawValue } + + var description: String { + switch self { + case .disabled: + return "gpsmode.disabled".localized + case .enabled: + return "gpsmode.enabled".localized + case .notPresent: + return "gpsmode.notPresent".localized + } + } + func protoEnumValue() -> Config.PositionConfig.GpsMode { + + switch self { + + case .enabled: + return Config.PositionConfig.GpsMode.enabled + case .disabled: + return Config.PositionConfig.GpsMode.disabled + case .notPresent: + return Config.PositionConfig.GpsMode.notPresent + } + } +} diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 6a4c54a4..b1b1785b 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -26,6 +26,7 @@ extension UserDefaults { case mapUseLegacy case enableDetectionNotifications case detectionSensorRole + case enableSmartPosition } func reset() { @@ -193,4 +194,12 @@ extension UserDefaults { UserDefaults.standard.set(newValue.rawValue, forKey: "detectionSensorRole") } } + static var enableSmartPosition: Bool { + get { + UserDefaults.standard.bool(forKey: "enableSmartPosition") + } + set { + UserDefaults.standard.set(newValue, forKey: "enableSmartPosition") + } + } } diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index bcb027fd..0969d262 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -976,10 +976,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if let lastLocation = LocationsHandler.shared.locationsArray.last { - /// Throw out crappy locations and only send a position if we are connected to a device - if fromNodeNum <= 0 || lastLocation.horizontalAccuracy < 0 || lastLocation.horizontalAccuracy > 100 { - return false - } positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7) positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7) let timestamp = lastLocation.timestamp diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 4742914a..53712c24 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -15,7 +15,7 @@ import CoreLocation static let shared = LocationsHandler() // Create a single, shared instance of the object. private let manager: CLLocationManager private var background: CLBackgroundActivitySession? - var enableSmartPosition: Bool + var enableSmartPosition: Bool = UserDefaults.enableSmartPosition @Published var locationsArray: [CLLocation] @Published var isStationary = false @@ -41,8 +41,8 @@ import CoreLocation private init() { self.manager = CLLocationManager() // Creating a location manager instance is safe to call here in `MainActor`. + self.manager.allowsBackgroundLocationUpdates = true locationsArray = [CLLocation]() - enableSmartPosition = true } func startLocationUpdates() { @@ -55,7 +55,7 @@ import CoreLocation self.updatesStarted = true let updates = CLLocationUpdate.liveUpdates() for try await update in updates { - if !self.updatesStarted { break } // End location updates by breaking out of the loop. + if !self.updatesStarted { break } if let loc = update.location { self.isStationary = update.isStationary diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 66c74a0d..07493add 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 24.xcdatamodel + MeshtasticDataModelV 25.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 25.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 25.xcdatamodel/contents new file mode 100644 index 00000000..381347db --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 25.xcdatamodel/contents @@ -0,0 +1,411 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Protobufs/meshtastic/admin.pb.swift b/Meshtastic/Protobufs/meshtastic/admin.pb.swift index ab96c32c..baa3c742 100644 --- a/Meshtastic/Protobufs/meshtastic/admin.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/admin.pb.swift @@ -235,6 +235,16 @@ struct AdminMessage { set {payloadVariant = .enterDfuModeRequest(newValue)} } + /// + /// Delete the file by the specified path from the device + var deleteFileRequest: String { + get { + if case .deleteFileRequest(let v)? = payloadVariant {return v} + return String() + } + set {payloadVariant = .deleteFileRequest(newValue)} + } + /// /// Set the owner for this node var setOwner: User { @@ -460,6 +470,9 @@ struct AdminMessage { /// Only implemented on NRF52 currently case enterDfuModeRequest(Bool) /// + /// Delete the file by the specified path from the device + case deleteFileRequest(String) + /// /// Set the owner for this node case setOwner(User) /// @@ -598,6 +611,10 @@ struct AdminMessage { guard case .enterDfuModeRequest(let l) = lhs, case .enterDfuModeRequest(let r) = rhs else { preconditionFailure() } return l == r }() + case (.deleteFileRequest, .deleteFileRequest): return { + guard case .deleteFileRequest(let l) = lhs, case .deleteFileRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() case (.setOwner, .setOwner): return { guard case .setOwner(let l) = lhs, case .setOwner(let r) = rhs else { preconditionFailure() } return l == r @@ -953,6 +970,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 19: .standard(proto: "get_node_remote_hardware_pins_request"), 20: .standard(proto: "get_node_remote_hardware_pins_response"), 21: .standard(proto: "enter_dfu_mode_request"), + 22: .standard(proto: "delete_file_request"), 32: .standard(proto: "set_owner"), 33: .standard(proto: "set_channel"), 34: .standard(proto: "set_config"), @@ -1176,6 +1194,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .enterDfuModeRequest(v) } }() + case 22: try { + var v: String? + try decoder.decodeSingularStringField(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .deleteFileRequest(v) + } + }() case 32: try { var v: User? var hadOneofValue = false @@ -1407,6 +1433,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .enterDfuModeRequest(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularBoolField(value: v, fieldNumber: 21) }() + case .deleteFileRequest?: try { + guard case .deleteFileRequest(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularStringField(value: v, fieldNumber: 22) + }() case .setOwner?: try { guard case .setOwner(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 32) diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index b984ef22..ef43b94a 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -415,6 +415,10 @@ struct Config { /// (Re)define PIN_GPS_EN for your board. var gpsEnGpio: UInt32 = 0 + /// + /// Set where GPS is enabled, disabled, or not present + var gpsMode: Config.PositionConfig.GpsMode = .disabled + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -516,6 +520,46 @@ struct Config { } + enum GpsMode: SwiftProtobuf.Enum { + typealias RawValue = Int + + /// + /// GPS is present but disabled + case disabled // = 0 + + /// + /// GPS is present and enabled + case enabled // = 1 + + /// + /// GPS is not present on the device + case notPresent // = 2 + case UNRECOGNIZED(Int) + + init() { + self = .disabled + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .disabled + case 1: self = .enabled + case 2: self = .notPresent + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .disabled: return 0 + case .enabled: return 1 + case .notPresent: return 2 + case .UNRECOGNIZED(let i): return i + } + } + + } + init() {} } @@ -1366,6 +1410,15 @@ extension Config.PositionConfig.PositionFlags: CaseIterable { ] } +extension Config.PositionConfig.GpsMode: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static var allCases: [Config.PositionConfig.GpsMode] = [ + .disabled, + .enabled, + .notPresent, + ] +} + extension Config.NetworkConfig.AddressMode: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. static var allCases: [Config.NetworkConfig.AddressMode] = [ @@ -1471,6 +1524,7 @@ extension Config.DeviceConfig.Role: @unchecked Sendable {} extension Config.DeviceConfig.RebroadcastMode: @unchecked Sendable {} extension Config.PositionConfig: @unchecked Sendable {} extension Config.PositionConfig.PositionFlags: @unchecked Sendable {} +extension Config.PositionConfig.GpsMode: @unchecked Sendable {} extension Config.PowerConfig: @unchecked Sendable {} extension Config.NetworkConfig: @unchecked Sendable {} extension Config.NetworkConfig.AddressMode: @unchecked Sendable {} @@ -1776,6 +1830,7 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm 10: .standard(proto: "broadcast_smart_minimum_distance"), 11: .standard(proto: "broadcast_smart_minimum_interval_secs"), 12: .standard(proto: "gps_en_gpio"), + 13: .standard(proto: "gps_mode"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1796,6 +1851,7 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm case 10: try { try decoder.decodeSingularUInt32Field(value: &self.broadcastSmartMinimumDistance) }() case 11: try { try decoder.decodeSingularUInt32Field(value: &self.broadcastSmartMinimumIntervalSecs) }() case 12: try { try decoder.decodeSingularUInt32Field(value: &self.gpsEnGpio) }() + case 13: try { try decoder.decodeSingularEnumField(value: &self.gpsMode) }() default: break } } @@ -1838,6 +1894,9 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if self.gpsEnGpio != 0 { try visitor.visitSingularUInt32Field(value: self.gpsEnGpio, fieldNumber: 12) } + if self.gpsMode != .disabled { + try visitor.visitSingularEnumField(value: self.gpsMode, fieldNumber: 13) + } try unknownFields.traverse(visitor: &visitor) } @@ -1854,6 +1913,7 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if lhs.broadcastSmartMinimumDistance != rhs.broadcastSmartMinimumDistance {return false} if lhs.broadcastSmartMinimumIntervalSecs != rhs.broadcastSmartMinimumIntervalSecs {return false} if lhs.gpsEnGpio != rhs.gpsEnGpio {return false} + if lhs.gpsMode != rhs.gpsMode {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1875,6 +1935,14 @@ extension Config.PositionConfig.PositionFlags: SwiftProtobuf._ProtoNameProviding ] } +extension Config.PositionConfig.GpsMode: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "DISABLED"), + 1: .same(proto: "ENABLED"), + 2: .same(proto: "NOT_PRESENT"), + ] +} + extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = Config.protoMessageName + ".PowerConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index 31ebb6a2..7496d559 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -126,6 +126,10 @@ enum HardwareModel: SwiftProtobuf.Enum { /// Makerfabs SenseLoRA Industrial Monitor (ESP32-S3 + RFM96) case senseloraS3 // = 28 + /// + /// Canary Radio Company - CanaryOne: https://canaryradio.io/products/canaryone + case canaryone // = 29 + /// /// --------------------------------------------------------------------------- /// Less common/prototype boards listed here (needs one more byte over the air) @@ -230,6 +234,14 @@ enum HardwareModel: SwiftProtobuf.Enum { /// with one cut and one jumper Meshtastic works case chatter2 // = 56 + /// + /// Heltec Wireless Paper, With ESP32-S3 CPU and E-Ink display + /// Older "V1.0" Variant, has no "version sticker" + /// E-Ink model is DEPG0213BNS800 + /// Tab on the screen protector is RED + /// Flex connector marking is FPC-7528B + case heltecWirelessPaperV10 // = 57 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. @@ -267,6 +279,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case 26: self = .rak11310 case 27: self = .senseloraRp2040 case 28: self = .senseloraS3 + case 29: self = .canaryone case 32: self = .loraRelayV1 case 33: self = .nrf52840Dk case 34: self = .ppr @@ -292,6 +305,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case 54: self = .ebyteEsp32S3 case 55: self = .esp32S3Pico case 56: self = .chatter2 + case 57: self = .heltecWirelessPaperV10 case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -323,6 +337,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case .rak11310: return 26 case .senseloraRp2040: return 27 case .senseloraS3: return 28 + case .canaryone: return 29 case .loraRelayV1: return 32 case .nrf52840Dk: return 33 case .ppr: return 34 @@ -348,6 +363,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case .ebyteEsp32S3: return 54 case .esp32S3Pico: return 55 case .chatter2: return 56 + case .heltecWirelessPaperV10: return 57 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -384,6 +400,7 @@ extension HardwareModel: CaseIterable { .rak11310, .senseloraRp2040, .senseloraS3, + .canaryone, .loraRelayV1, .nrf52840Dk, .ppr, @@ -409,6 +426,7 @@ extension HardwareModel: CaseIterable { .ebyteEsp32S3, .esp32S3Pico, .chatter2, + .heltecWirelessPaperV10, .privateHw, ] } @@ -2570,6 +2588,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 26: .same(proto: "RAK11310"), 27: .same(proto: "SENSELORA_RP2040"), 28: .same(proto: "SENSELORA_S3"), + 29: .same(proto: "CANARYONE"), 32: .same(proto: "LORA_RELAY_V1"), 33: .same(proto: "NRF52840DK"), 34: .same(proto: "PPR"), @@ -2595,6 +2614,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 54: .same(proto: "EBYTE_ESP32_S3"), 55: .same(proto: "ESP32_S3_PICO"), 56: .same(proto: "CHATTER_2"), + 57: .same(proto: "HELTEC_WIRELESS_PAPER_V1_0"), 255: .same(proto: "PRIVATE_HW"), ] } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 575718e0..9f4582e7 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -11,6 +11,7 @@ struct AppSettings: View { @State var totalDownloadedTileSize = "" @StateObject var locationHelper = LocationHelper() @State var provideLocation: Bool = UserDefaults.provideLocation + @State var enableSmartPosition: Bool = UserDefaults.enableSmartPosition @State var useLegacyMap: Bool = UserDefaults.mapUseLegacy @State var provideLocationInterval: Int = UserDefaults.provideLocationInterval @State private var isPresentingCoreDataResetConfirm = false @@ -67,6 +68,10 @@ struct AppSettings: View { Label("provide.location", systemImage: "location.circle.fill") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $enableSmartPosition) { + Label("appsettings.enablesmartposition", systemImage: "brain.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) if UserDefaults.provideLocation { VStack { Picker("update.interval", selection: $provideLocationInterval) { diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index a92f06ef..31dd2319 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -36,14 +36,16 @@ struct PositionConfig: View { @State var smartPositionEnabled = true @State var deviceGpsEnabled = true + @State var gpsMode = 0 @State var rxGpio = 0 @State var txGpio = 0 @State var gpsEnGpio = 0 @State var fixedPosition = false + @State var gpsUpdateInterval = 0 @State var positionBroadcastSeconds = 0 @State var broadcastSmartMinimumDistance = 0 @State var broadcastSmartMinimumIntervalSecs = 0 - @State var positionFlags = 3 + @State var positionFlags = 811 /// Position Flags /// Altitude value - 1 @@ -143,6 +145,35 @@ struct PositionConfig: View { .font(.caption) } } + Section(header: Text("Device GPS")) { + Picker("", selection: $gpsMode) { + ForEach(GpsMode.allCases, id: \.self) { at in + Text(at.description) + .tag(at.id) + } + } + .pickerStyle(SegmentedPickerStyle()) + .padding(.top, 5) + .padding(.bottom, 5) + + + if gpsMode == 1 { + Picker("Update Interval", selection: $gpsUpdateInterval) { + ForEach(GpsUpdateIntervals.allCases) { ui in + Text(ui.description) + } + } + Text("How often should we try to get a GPS position.") + .font(.caption) + } else { + Toggle(isOn: $fixedPosition) { + Label("Fixed Position", systemImage: "location.square.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("If enabled your current phone location will be sent to the device and will broadcast over the mesh on the position interval. Fixed positon will always use the most recent position the device has.") + .font(.caption) + } + } Section(header: Text("Position Flags")) { Text("Optional fields to include when assembling position messages. the more fields are included, the larger the message will be - leading to longer airtime and a higher risk of packet loss") @@ -205,13 +236,9 @@ struct PositionConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } } - Section(header: Text("Device GPS")) { - Toggle(isOn: $deviceGpsEnabled) { - Label("Device GPS Enabled", systemImage: "location") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - if deviceGpsEnabled { - + + if gpsMode == 1 { + Section(header: Text("Advanced Device GPS")) { Picker("GPS Receive GPIO", selection: $rxGpio) { ForEach(0..<49) { if $0 == 0 { @@ -244,13 +271,6 @@ struct PositionConfig: View { .pickerStyle(DefaultPickerStyle()) Text("(Re)define PIN_GPS_EN for your board.") .font(.caption) - } else { - Toggle(isOn: $fixedPosition) { - Label("Fixed Position", systemImage: "location.square.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("If enabled your current location will be set as a fixed position.") - .font(.caption) } } } @@ -283,8 +303,10 @@ struct PositionConfig: View { if connectedNode != nil { var pc = Config.PositionConfig() pc.positionBroadcastSmartEnabled = smartPositionEnabled - pc.gpsEnabled = deviceGpsEnabled + pc.gpsEnabled = gpsMode == 1 + pc.gpsMode = Config.PositionConfig.GpsMode(rawValue: gpsMode) ?? Config.PositionConfig.GpsMode.notPresent pc.fixedPosition = fixedPosition + pc.gpsUpdateInterval = UInt32(gpsUpdateInterval) pc.positionBroadcastSecs = UInt32(positionBroadcastSeconds) pc.broadcastSmartMinimumIntervalSecs = UInt32(broadcastSmartMinimumIntervalSecs) pc.broadcastSmartMinimumDistance = UInt32(broadcastSmartMinimumDistance) @@ -342,6 +364,11 @@ struct PositionConfig: View { if newDeviceGps != node!.positionConfig!.deviceGpsEnabled { hasChanges = true } } } + .onChange(of: gpsMode) { newGpsMode in + if node != nil && node!.positionConfig != nil { + if newGpsMode != node!.positionConfig!.gpsMode { hasChanges = true } + } + } .onChange(of: rxGpio) { newRxGpio in if node != nil && node!.positionConfig != nil { if newRxGpio != node!.positionConfig!.rxGpio { hasChanges = true } @@ -382,6 +409,11 @@ struct PositionConfig: View { if newBroadcastSmartMinimumDistance != node!.positionConfig!.broadcastSmartMinimumDistance { hasChanges = true } } } + .onChange(of: gpsUpdateInterval) { newGpsUpdateInterval in + if node != nil && node!.positionConfig != nil { + if newGpsUpdateInterval != node!.positionConfig!.gpsUpdateInterval { hasChanges = true } + } + } .onChange(of: includeAltitude) { altFlag in let pf = PositionFlags(rawValue: self.positionFlags) let existingValue = pf.contains(.Altitude) @@ -440,11 +472,16 @@ struct PositionConfig: View { } func setPositionValues() { self.smartPositionEnabled = node?.positionConfig?.smartPositionEnabled ?? true - self.deviceGpsEnabled = node?.positionConfig?.deviceGpsEnabled ?? true + self.deviceGpsEnabled = node?.positionConfig?.deviceGpsEnabled ?? false + self.gpsMode = Int(node?.positionConfig?.gpsMode ?? 0) + if node?.positionConfig?.deviceGpsEnabled ?? false && gpsMode != 1 { + self.gpsMode = 1 + } self.rxGpio = Int(node?.positionConfig?.rxGpio ?? 0) self.txGpio = Int(node?.positionConfig?.txGpio ?? 0) self.gpsEnGpio = Int(node?.positionConfig?.gpsEnGpio ?? 0) self.fixedPosition = node?.positionConfig?.fixedPosition ?? false + self.gpsUpdateInterval = Int(node?.positionConfig?.gpsUpdateInterval ?? 30) self.positionBroadcastSeconds = Int(node?.positionConfig?.positionBroadcastSeconds ?? 900) self.broadcastSmartMinimumIntervalSecs = Int(node?.positionConfig?.broadcastSmartMinimumIntervalSecs ?? 30) self.broadcastSmartMinimumDistance = Int(node?.positionConfig?.broadcastSmartMinimumDistance ?? 50) diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 425b8e01..a2cef31e 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -100,6 +100,9 @@ "gpsformat.mgrs"="Military Grid Reference System"; "gpsformat.olc"="Open Location Code (aka Plus Codes)"; "gpsformat.osgr"="Ordnance Survey Grid Reference"; +"gpsmode.disabled"="Disabled"; +"gpsmode.enabled"="Enabled"; +"gpsmode.notPresent"="Not Present"; "heard"="Heard"; "heard.last"="Last Heard"; "hybrid"="Hybrid"; From 6c03f4164dac0aae44701befeb79761b0ac36850 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 5 Feb 2024 22:45:16 -0800 Subject: [PATCH 13/22] Update Roles --- Meshtastic/Enums/DeviceEnums.swift | 14 +++++++------- en.lproj/Localizable.strings | 19 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/Meshtastic/Enums/DeviceEnums.swift b/Meshtastic/Enums/DeviceEnums.swift index 7e97aea7..b754e986 100644 --- a/Meshtastic/Enums/DeviceEnums.swift +++ b/Meshtastic/Enums/DeviceEnums.swift @@ -12,22 +12,22 @@ enum DeviceRoles: Int, CaseIterable, Identifiable { case client = 0 case clientMute = 1 - case router = 2 - case routerClient = 3 - case repeater = 4 + case clientHidden = 8 case tracker = 5 + case lostAndFound = 9 case sensor = 6 case tak = 7 - case clientHidden = 8 - case lostAndFound = 9 - + case repeater = 4 + case router = 2 + case routerClient = 3 + var id: Int { self.rawValue } var name: String { switch self { case .client: return "Client" case .clientMute: - return "Muted Client" + return "Client Mute" case .router: return "Router" case .routerClient: diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index c05df159..5cb9d1ed 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -67,15 +67,16 @@ "device.config"="Device Config"; "device.metrics.delete"="Delete all device metrics?"; "device.metrics.log"="Device Metrics Log"; -"device.role.client"="Client (default) - App connected client."; -"device.role.clienthidden"=" Used for nodes that \"only speak when spoken to\" Turns all of the routine broadcasts but allows for ad-hoc communication. Still rebroadcasts, but with local only rebroadcast mode (known meshes only). Can be used for private operation or to dramatically reduce airtime / power consumption."; -"device.role.clientmute"="Client Mute - Same as a client except packets will not hop over this node, does not contribute to routing packets for mesh."; -"device.role.lostandfound"="Used to automatically send a text message to the mesh with the current position of the device on a frequent interval: \"I'm lost! Position: lat / long\""; -"device.role.router"="Router - Assumes device will operate in a standalone manner while placed in a location with a coverage advantage, not for mobile nodes. WARNING: The BLE/Wi-Fi radios and the OLED screen will be put to sleep."; -"device.role.routerclient"="Router Client - Hybrid of the Client and Router roles. Similar to Router, except the Router Client can be used as both a Router and an app connected Client. Also not for mobile nodes. BLE/Wi-Fi and OLED screen will not be put to sleep."; -"device.role.repeater"="Repeater - Mesh packets will prefer to be routed over this node. This role eliminates all features other than mesh routing, this node will not even appear as part of the network. Please see Rebroadcast Mode for additional settings specific to this role."; -"device.role.tracker"="Tracker - For use with devices intended as a GPS tracker. Position packets sent from this device will be higher priority, with position broadcasting every two minutes. Smart Position Broadcast will default to off."; -"device.role.tak"="Used for nodes dedicated for connection to an ATAK EUD. Turns off many of the routine broadcasts to favor CoT packet stream from the Meshtastic ATAK plugin -> IMeshService -> Node"; +"device.role.client"="App connected or stand alone messaging client."; +"device.role.clientmute"="Client that does not forward packets from other devices."; +"device.role.clienthidden"="Client that only broadcasts as needed for stealth or power savings."; +"device.role.tracker"="Prioritizes broadcasting GPS position packets."; +"device.role.lostandfound"="Broadcasts location as message to default channel regularly for to assist with node recovery."; +"device.role.sensor"="Prioritizes broadcasting telemetry packets."; +"device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts."; +"device.role.repeater"="Infrastructure node for extending mesh network coverage by relaying messages with minimal overhead. Not visible in Nodes list. Best positioned in strategic locations to maximize the network's overall coverage. Device is not shown in topology."; +"device.role.router"="Infrastructure node for on extending mesh network coverage by relaying messages. Visible in Nodes list. Best positioned in strategic locations to maximize the network's overall coverage. Device is shown in topology."; +"device.role.routerclient"="Combination of both ROUTER and CLIENT. Not for mobile nodes."; "direct.messages"="Direct Messages"; "dismiss.keyboard"="Dismiss"; "display"="Display (Device Screen)"; From c3f49235317804b12f5934a974f44ce63a379813 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 5 Feb 2024 23:02:33 -0800 Subject: [PATCH 14/22] Add via mqtt, does not seem to be working with the client proxy --- .../MeshtasticDataModelV 25.xcdatamodel/contents | 1 + Meshtastic/Persistence/UpdateCoreData.swift | 2 ++ Meshtastic/Views/Nodes/Helpers/NodeListItem.swift | 7 +++++++ 3 files changed, 10 insertions(+) diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 25.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 25.xcdatamodel/contents index 381347db..85cf1eaa 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 25.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 25.xcdatamodel/contents @@ -229,6 +229,7 @@ + diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 5389ad4b..7f66989d 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -118,6 +118,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) newNode.snr = packet.rxSnr newNode.rssi = packet.rxRssi + newNode.viaMqtt = packet.viaMqtt if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { newNode.channel = Int32(nodeInfoMessage.channel) print(packet.channel) @@ -161,6 +162,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) } fetchedNode[0].snr = packet.rxSnr fetchedNode[0].rssi = packet.rxRssi + fetchedNode[0].viaMqtt = packet.viaMqtt if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { fetchedNode[0].channel = Int32(nodeInfoMessage.channel) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index a4692ec9..a0faee9c 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -129,6 +129,13 @@ struct NodeListItem: View { .symbolRenderingMode(.hierarchical) .font(.callout) } + if node.viaMqtt { + Image(systemName: "network") + .symbolRenderingMode(.hierarchical) + .font(.callout) + Text("mqtt") + .font(.caption) + } } .padding(.top, 3) } From b0272e057896b473bf564161a6750599bceafd22 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 5 Feb 2024 23:48:23 -0800 Subject: [PATCH 15/22] Via MQTT --- Meshtastic/Persistence/UpdateCoreData.swift | 1 + .../Views/Nodes/Helpers/NodeListItem.swift | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 7f66989d..c94f73b6 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -279,6 +279,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) } fetchedNode[0].snr = packet.rxSnr fetchedNode[0].rssi = packet.rxRssi + fetchedNode[0].viaMqtt = packet.viaMqtt fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet do { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index a0faee9c..4dfb1af2 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -89,14 +89,21 @@ struct NodeListItem: View { } } } - if node.channel > 0 { - HStack { + HStack { + if node.channel > 0 { Image(systemName: "fibrechannel") .font(.callout) .symbolRenderingMode(.hierarchical) Text("Channel: \(node.channel)") .font(.callout) } + if node.viaMqtt && connectedNode != node.num { + Image(systemName: "network") + .symbolRenderingMode(.hierarchical) + .font(.callout) + Text("Via MQTT") + .font(.callout) + } } if !connected { HStack { @@ -129,13 +136,6 @@ struct NodeListItem: View { .symbolRenderingMode(.hierarchical) .font(.callout) } - if node.viaMqtt { - Image(systemName: "network") - .symbolRenderingMode(.hierarchical) - .font(.callout) - Text("mqtt") - .font(.caption) - } } .padding(.top, 3) } From 69e61cac53f48415300e264ed20b275e61d1467e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 6 Feb 2024 09:13:57 -0800 Subject: [PATCH 16/22] Warn that using MQTT with a duty cycle is the end of your mesh --- .../Settings/Config/Module/MQTTConfig.swift | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index be78fb96..f1ce1aa3 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -30,6 +30,15 @@ struct MQTTConfig: View { var body: some View { VStack { Form { + if node != nil && node?.loRaConfig != nil { + let rc = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0)) + if rc?.dutyCycle ?? 0 <= 10 { + Text("Your region has a \(rc?.dutyCycle ?? 0)% duty cycle. MQTT is not advised when you are duty cycle restricted, the extra traffice will quickly overwhelm your LoRa mesh.") + .font(.callout) + .foregroundColor(.red) + } + } + if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 { Text("There has been no response to a request for device metadata over the admin channel for this node.") .font(.callout) @@ -88,7 +97,7 @@ struct MQTTConfig: View { Label("JSON Enabled", systemImage: "ellipsis.curlybraces") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("JSON mode is a limited, unencrypted MQTT output.") + Text("JSON mode is a limited, unencrypted MQTT output that can crash your node it should not be enabled unless you are locally integrating with home assistant") .font(.caption2) Toggle(isOn: $tlsEnabled) { @@ -304,19 +313,11 @@ struct MQTTConfig: View { .onChange(of: encryptionEnabled) { newEncryptionEnabled in if node != nil && node?.mqttConfig != nil { if newEncryptionEnabled != node!.mqttConfig!.encryptionEnabled { hasChanges = true } - if newEncryptionEnabled { - jsonEnabled = false - } } } .onChange(of: jsonEnabled) { newJsonEnabled in if node != nil && node?.mqttConfig != nil { if newJsonEnabled != node!.mqttConfig!.jsonEnabled { hasChanges = true } - - if newJsonEnabled { - encryptionEnabled = false - proxyToClientEnabled = false - } } } .onChange(of: tlsEnabled) { newTlsEnabled in From 2b15fd93a6f02c1dec99bd0bc81963deeea2a248 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 6 Feb 2024 10:04:04 -0800 Subject: [PATCH 17/22] Default to 128bit keys --- Meshtastic/Views/Settings/Channels.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index b8ad588c..491dcdf8 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -29,7 +29,7 @@ struct Channels: View { @State private var isPresentingSaveConfirm: Bool = false @State private var channelIndex: Int32 = 0 @State private var channelName = "" - @State private var channelKeySize = 32 + @State private var channelKeySize = 16 @State private var channelKey = "AQ==" @State private var channelRole = 0 @State private var uplink = false @@ -89,7 +89,7 @@ struct Channels: View { if node?.myInfo?.channels?.array.count ?? 0 < 8 && node != nil { Button { - let key = generateChannelKey(size: 32) + let key = generateChannelKey(size: 16) channelName = "" channelIndex = Int32(node!.myInfo!.channels!.array.count) channelRole = 2 From 4d420a8008103b2e08d46fb80fc3240238afee6d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 6 Feb 2024 11:11:03 -0800 Subject: [PATCH 18/22] Save GPS state --- Meshtastic/Persistence/UpdateCoreData.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index c94f73b6..f922d28c 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -598,6 +598,7 @@ func upsertPositionConfigPacket(config: Meshtastic.Config.PositionConfig, nodeNu let newPositionConfig = PositionConfigEntity(context: context) newPositionConfig.smartPositionEnabled = config.positionBroadcastSmartEnabled newPositionConfig.deviceGpsEnabled = config.gpsEnabled + newPositionConfig.gpsMode = Int32(config.gpsMode.rawValue) newPositionConfig.rxGpio = Int32(config.rxGpio) newPositionConfig.txGpio = Int32(config.txGpio) newPositionConfig.gpsEnGpio = Int32(config.gpsEnGpio) @@ -607,11 +608,12 @@ func upsertPositionConfigPacket(config: Meshtastic.Config.PositionConfig, nodeNu newPositionConfig.broadcastSmartMinimumDistance = Int32(config.broadcastSmartMinimumDistance) newPositionConfig.positionFlags = Int32(config.positionFlags) newPositionConfig.gpsAttemptTime = 900 - newPositionConfig.gpsUpdateInterval = 120 + newPositionConfig.gpsUpdateInterval = Int32(config.gpsUpdateInterval) fetchedNode[0].positionConfig = newPositionConfig } else { fetchedNode[0].positionConfig?.smartPositionEnabled = config.positionBroadcastSmartEnabled fetchedNode[0].positionConfig?.deviceGpsEnabled = config.gpsEnabled + fetchedNode[0].positionConfig?.gpsMode = Int32(config.gpsMode.rawValue) fetchedNode[0].positionConfig?.rxGpio = Int32(config.rxGpio) fetchedNode[0].positionConfig?.txGpio = Int32(config.txGpio) fetchedNode[0].positionConfig?.gpsEnGpio = Int32(config.gpsEnGpio) @@ -620,7 +622,7 @@ func upsertPositionConfigPacket(config: Meshtastic.Config.PositionConfig, nodeNu fetchedNode[0].positionConfig?.broadcastSmartMinimumIntervalSecs = Int32(config.broadcastSmartMinimumIntervalSecs) fetchedNode[0].positionConfig?.broadcastSmartMinimumDistance = Int32(config.broadcastSmartMinimumDistance) fetchedNode[0].positionConfig?.gpsAttemptTime = 900 - fetchedNode[0].positionConfig?.gpsUpdateInterval = 120 + fetchedNode[0].positionConfig?.gpsUpdateInterval = Int32(config.gpsUpdateInterval) fetchedNode[0].positionConfig?.positionFlags = Int32(config.positionFlags) } do { From ef59160b163347f7d66625b29a3a2146c4df8756 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 6 Feb 2024 12:29:42 -0800 Subject: [PATCH 19/22] Handle unknown store and forwared packets as text messages. --- Meshtastic/Helpers/BLEManager.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 0969d262..a6dbae0e 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -2372,6 +2372,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Request Response switch storeAndForwardMessage.rr { case .unset: + textMessageAppPacket(packet: packet, connectedNode: connectedNodeNum, context: context) MeshLogger.log("📮 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") case .routerError: MeshLogger.log("☠️ Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") @@ -2382,7 +2383,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate /// send a request for ClientHistory with a time period matching the heartbeat var sfPacket = StoreAndForward() sfPacket.rr = StoreAndForward.RequestResponse.clientHistory - sfPacket.history.window = 18000000 // storeAndForwardMessage.heartbeat.period + sfPacket.history.window = 120 // storeAndForwardMessage.heartbeat.period var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(packet.from) meshPacket.from = UInt32(connectedNodeNum) @@ -2428,6 +2429,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate case .clientAbort: MeshLogger.log("🛑 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") case .UNRECOGNIZED: + textMessageAppPacket(packet: packet, connectedNode: connectedNodeNum, context: context) MeshLogger.log("📮 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") } } From 718842bafb713af88c5cae9dc248da0ff6ad5bae Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 6 Feb 2024 16:47:12 -0800 Subject: [PATCH 20/22] Localize new settings --- Meshtastic/Enums/PositionConfigEnums.swift | 2 +- Meshtastic/Views/Settings/AppSettings.swift | 14 +++++++------- .../Views/Settings/Config/PositionConfig.swift | 1 - Meshtastic/Views/Settings/Settings.swift | 2 +- de.lproj/Localizable.strings | 5 +++-- en.lproj/Localizable.strings | 5 +++-- pl.lproj/Localizable.strings | 5 +++-- zh-Hans.lproj/Localizable.strings | 5 +++-- 8 files changed, 21 insertions(+), 18 deletions(-) diff --git a/Meshtastic/Enums/PositionConfigEnums.swift b/Meshtastic/Enums/PositionConfigEnums.swift index 5d6751d2..e9d4fec2 100644 --- a/Meshtastic/Enums/PositionConfigEnums.swift +++ b/Meshtastic/Enums/PositionConfigEnums.swift @@ -91,7 +91,7 @@ enum GpsUpdateIntervals: Int, CaseIterable, Identifiable { case .twentyFourHours: return "interval.twentyfour.hours".localized case .maxInt32: - return "on.boot" + return "on.boot".localized } } } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 9f4582e7..44b1790f 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -65,14 +65,14 @@ struct AppSettings: View { } Section(header: Text("Location Settings")) { Toggle(isOn: $provideLocation) { - Label("provide.location", systemImage: "location.circle.fill") + Label("appsettings.provide.location", systemImage: "location.circle.fill") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $enableSmartPosition) { - Label("appsettings.enablesmartposition", systemImage: "brain.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - if UserDefaults.provideLocation { + if provideLocation { + Toggle(isOn: $enableSmartPosition) { + Label("appsettings.smartposition", systemImage: "brain.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) VStack { Picker("update.interval", selection: $provideLocationInterval) { ForEach(LocationUpdateInterval.allCases) { lu in @@ -135,7 +135,7 @@ struct AppSettings: View { totalDownloadedTileSize = tileManager.getAllDownloadedSize() }) } - .navigationTitle("app.settings") + .navigationTitle("appsettings") .navigationBarItems(trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 31dd2319..dfbfb9c2 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -156,7 +156,6 @@ struct PositionConfig: View { .padding(.top, 5) .padding(.bottom, 5) - if gpsMode == 1 { Picker("Update Interval", selection: $gpsUpdateInterval) { ForEach(GpsUpdateIntervals.allCases) { ui in diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 297e7481..d9d6f598 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -57,7 +57,7 @@ struct Settings: View { } label: { Image(systemName: "gearshape") .symbolRenderingMode(.hierarchical) - Text("app.settings") + Text("appsettings") } .tag(SettingsSidebar.appSettings) if #available(iOS 17.0, macOS 14.0, *) { diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index 595e9c03..548a9b04 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -14,7 +14,9 @@ "always.on"="Immer an"; "ambient.lighting"="Ambient Lighting"; "ambient.lighting.config"="Ambient Lighting Config"; -"app.settings"="App Einstellungen"; +"appsettings"="App Einstellungen"; +"appsettings.provide.location"="Standort im Mesh veröffentlichen"; +"appsettings.smartposition"="Smart Position"; "are.you.sure"="Bist Du sicher?"; "ascii.capable"="ASCII fähig"; "available.radios"="Geräte in der Nähe"; @@ -223,7 +225,6 @@ "position"="Position"; "position.config"="Positionseinstellungen"; "preferred.radio"="Bevorzugtes Gerät"; -"provide.location"="Standort im Mesh veröffentlichen"; "radio.configuration"="Geräteeinstellungen"; "range.test"="Entfernungstest"; "range.test.blocked"="Block Range Test"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 5cb9d1ed..028c3069 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -14,7 +14,9 @@ "always.on"="Always On"; "ambient.lighting"="Ambient Lighting"; "ambient.lighting.config"="Ambient Lighting Config"; -"app.settings"="App Settings"; +"appsettings"="App Settings"; +"appsettings.provide.location"="Share location"; +"appsettings.smartposition"="Smart Position"; "are.you.sure"="Are you sure?"; "ascii.capable"="ASCII Capable"; "available.radios"="Available Radios"; @@ -230,7 +232,6 @@ "position"="Position"; "position.config"="Position Config"; "preferred.radio"="Preferred Radio"; -"provide.location"="Share location"; "radio.configuration"="Radio Configuration"; "range.test"="Range Test"; "range.test.blocked"="Block Range Test"; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index a734697b..bea348c0 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -16,7 +16,9 @@ "always.on"="Zawsze włączone"; "ambient.lighting"="Ambient Lighting"; "ambient.lighting.config"="Ambient Lighting Config"; -"app.settings"="Ustawienia aplikacji"; +"appsettings"="Ustawienia aplikacji"; +"appsettings.provide.location"="Udostępnij lokalizację"; +"appsettings.smartposition"="Smart Position"; "are.you.sure"="Jesteś pewny?"; "ascii.capable"="Zgodny z ASCII"; "available.radios"="Dostępne radia"; @@ -224,7 +226,6 @@ "position"="Pozycja"; "position.config"="Konfiguracja pozycji"; "preferred.radio"="Preferowane radio"; -"provide.location"="Udostępnij lokalizację"; "radio.configuration"="Konfiguracja radia"; "range.test"="Test zasięgu"; "range.test.blocked"="Block Range Test"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 60079d48..178833e2 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -14,7 +14,9 @@ "always.on"="常亮"; "ambient.lighting"="Ambient Lighting"; "ambient.lighting.config"="Ambient Lighting Config"; -"app.settings"="通用设置"; +"appsettings"="通用设置"; +"appsettings.provide.location"="提供定位到 Mesh 网络"; +"appsettings.smartposition"="Smart Position"; "are.you.sure"="是否确认?"; "ascii.capable"="ASCII Capable"; "available.radios"="可以连接的电台"; @@ -223,7 +225,6 @@ "position"="定位"; "position.config"="定位配置"; "preferred.radio"="首选电台"; -"provide.location"="提供定位到 Mesh 网络"; "radio.configuration"="电台配置"; "range.test"="拉距测试"; "range.test.blocked"="区块范围测试"; From 2d2a94a3d6a5cba68c32d99e0f2ee67e2ecd6d73 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 7 Feb 2024 08:43:35 -0800 Subject: [PATCH 21/22] store and forward fix --- Meshtastic/Helpers/BLEManager.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index a6dbae0e..5e9e19c1 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -2372,7 +2372,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Request Response switch storeAndForwardMessage.rr { case .unset: - textMessageAppPacket(packet: packet, connectedNode: connectedNodeNum, context: context) MeshLogger.log("📮 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") case .routerError: MeshLogger.log("☠️ Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") From f8eebfa0773b623aa7ad2cf25898198d10b541af Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 7 Feb 2024 08:44:29 -0800 Subject: [PATCH 22/22] Hide route recorder --- Meshtastic/Views/Settings/RouteRecorder.swift | 584 +++++++++--------- Meshtastic/Views/Settings/Settings.swift | 16 +- 2 files changed, 300 insertions(+), 300 deletions(-) diff --git a/Meshtastic/Views/Settings/RouteRecorder.swift b/Meshtastic/Views/Settings/RouteRecorder.swift index 88c4a44e..71794725 100644 --- a/Meshtastic/Views/Settings/RouteRecorder.swift +++ b/Meshtastic/Views/Settings/RouteRecorder.swift @@ -1,295 +1,295 @@ +//// +//// Routes.swift +//// Meshtastic +//// +//// Created by Garth Vander Houwen on 11/21/23. +//// // -// Routes.swift -// Meshtastic +//import SwiftUI +//import CoreData +//import MapKit +//import CoreLocation +//import CoreMotion // -// Created by Garth Vander Houwen on 11/21/23. +//@available(iOS 17.0, macOS 14.0, *) +//struct RouteRecorder: View { +// +// @ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared +// @Environment(\.managedObjectContext) var context +// @State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic) +// //@State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true) +// @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic) +// @State var isShowingDetails = false +// @Namespace var namespace +// @Namespace var routerecorderscope +// @State var recording: RouteEntity? +// @State var color: Color = .blue +// +// var body: some View { +// VStack { +// ZStack { +// Map(position: $position, scope: routerecorderscope) { +// UserAnnotation() +// /// Route Lines +// let lineCoords = locationsHandler.locationsArray.compactMap({(position) -> CLLocationCoordinate2D in +// return position.coordinate +// }) +// +// let gradient = LinearGradient( +// colors: [color], +// startPoint: .leading, endPoint: .trailing +// ) +// let dashed = StrokeStyle( +// lineWidth: 3, +// lineCap: .round, lineJoin: .round, dash: [10, 10] +// ) +// MapPolyline(coordinates: lineCoords) +// .stroke(gradient, style: dashed) // - -import SwiftUI -import CoreData -import MapKit -import CoreLocation -import CoreMotion - -@available(iOS 17.0, macOS 14.0, *) -struct RouteRecorder: View { - - @ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared - @Environment(\.managedObjectContext) var context - @State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic) - //@State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true) - @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic) - @State var isShowingDetails = false - @Namespace var namespace - @Namespace var routerecorderscope - @State var recording: RouteEntity? - @State var color: Color = .blue - - var body: some View { - VStack { - ZStack { - Map(position: $position, scope: routerecorderscope) { - UserAnnotation() - /// Route Lines - let lineCoords = locationsHandler.locationsArray.compactMap({(position) -> CLLocationCoordinate2D in - return position.coordinate - }) - - let gradient = LinearGradient( - colors: [color], - startPoint: .leading, endPoint: .trailing - ) - let dashed = StrokeStyle( - lineWidth: 3, - lineCap: .round, lineJoin: .round, dash: [10, 10] - ) - MapPolyline(coordinates: lineCoords) - .stroke(gradient, style: dashed) - - } - .mapStyle(mapStyle) - } - .mapScope(routerecorderscope) - .safeAreaInset(edge: .bottom) { - ZStack { - VStack { - HStack(spacing: 10) { - Spacer() - - Button { - isShowingDetails = true - } label: { - Image(systemName: locationsHandler.isRecording ? "record.circle.fill" : "record.circle") - .font(.system(size: 72)) - .symbolRenderingMode(.multicolor) - .foregroundColor(.red) - } - .buttonStyle(.bordered) - .foregroundColor(.red) - .buttonBorderShape(.circle) - .matchedGeometryEffect(id: "Details Button", in: namespace) - - Spacer() - } - } - } - .padding() - } - .sheet(isPresented: $isShowingDetails) { - NavigationStack { - VStack { - if locationsHandler.isRecording { - HStack (alignment: .center) { - Image(systemName: "record.circle.fill") - .symbolRenderingMode(.multicolor) - .font(.title) - .foregroundColor(.red) - Text("Recording route") - .font(.title) - Spacer() - Text("\(locationsHandler.count)") - .foregroundColor(.red) - .font(.title2) - } - .padding() - } else if locationsHandler.isRecordingPaused { - HStack (alignment: .center) { - - Image(systemName: "playpause") - .symbolRenderingMode(.multicolor) - .font(.title3) - .foregroundColor(.red) - Text("Route recording paused") - .font(.title) - } - .padding(.top) - } - - if locationsHandler.isRecording || locationsHandler.isRecordingPaused { - Divider() - HStack { - VStack { - Text(locationsHandler.recordingStarted ?? Date(), style: .timer) - .font(.title) - .fixedSize() - Text("Time") - .font(.callout) - .fixedSize() - } - .padding(.horizontal) - Divider() - VStack { - let distance = Measurement(value: locationsHandler.distanceTraveled, unit: UnitLength.meters) - Text("\(distance.formatted())") - .font(.title) - .fixedSize() - Text("Distance") - .font(.callout) - .fixedSize() - } - .padding(.horizontal) - Divider() - VStack { - let gain = Measurement(value: locationsHandler.elevationGain, unit: UnitLength.meters) - Text(gain.formatted()) - .font(.title) - Text("Elev. Gain") - .font(.callout) - } - .padding(.horizontal) - } - .frame(maxHeight: 90) - } - Divider() - VStack(alignment: .leading) { - List { - GPSStatus(largeFont: .body, smallFont: .callout) - } - .listStyle(.plain) - HStack { - Spacer() - if !locationsHandler.isRecording && !locationsHandler.isRecordingPaused { - /// We are not recording or paused, show start recording button - Button { - locationsHandler.isRecording = true - locationsHandler.count = 0 - locationsHandler.distanceTraveled = 0.0 - locationsHandler.elevationGain = 0.0 - locationsHandler.locationsArray.removeAll() - locationsHandler.recordingStarted = Date() - let newRoute = RouteEntity(context: context) - newRoute.name = String("Route Recording") - newRoute.id = Int32.random(in: Int32(Int8.max) ... Int32.max) - newRoute.color = Int64(UIColor.random.hex) - newRoute.date = Date() - newRoute.enabled = false - color = Color(UIColor(hex: UInt32(newRoute.color))) - self.recording = newRoute - do { - try context.save() - print("💾 Saved a new route") - } catch { - context.rollback() - let nsError = error as NSError - print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") - } - } label: { - Label("start", systemImage: "play") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) - - } else if locationsHandler.isRecording { - /// We are recording show pause button - Button { - locationsHandler.isRecording = false - locationsHandler.isRecordingPaused = true - } label: { - Label("pause", systemImage: "pause") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) - } else if locationsHandler.isRecordingPaused { - /// We are paused show resume button - Button { - locationsHandler.isRecording = true - locationsHandler.isRecordingPaused = false - } label: { - Label("resume", systemImage: "playpause") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) - } - - if locationsHandler.isRecording || locationsHandler.isRecordingPaused { - /// We are recording or paused, show finish button - Button { - locationsHandler.isRecording = false - locationsHandler.isRecordingPaused = false - locationsHandler.distanceTraveled = 0.0 - locationsHandler.elevationGain = 0.0 - locationsHandler.locationsArray.removeAll() - locationsHandler.recordingStarted = nil - if let rec = recording { - rec.enabled = true - context.refresh(rec, mergeChanges:true) - } - - do { - try context.save() - print("💾 Saved a route finish") - } catch { - context.rollback() - let nsError = error as NSError - print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") - } - } label: { - Label("finish", systemImage: "flag.checkered") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) - } -#if targetEnvironment(macCatalyst) - Button(role: .cancel) { - isShowingDetails = false - } label: { - Label("close", systemImage: "xmark") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) -#endif - Spacer() - } - - } - } - } - .presentationDetents([.fraction(0.30), .fraction(0.65)]) - .presentationDragIndicator(.hidden) - .interactiveDismissDisabled(false) - .onAppear { - UIApplication.shared.isIdleTimerDisabled = true - } - .onDisappear(perform: { - UIApplication.shared.isIdleTimerDisabled = false - }) - .onChange(of: locationsHandler.locationsArray.last) { newLoc in - if locationsHandler.isRecording { - if let loc = newLoc { - if recording != nil { - let locationEntity = LocationEntity(context: context) - locationEntity.routeLocation = recording - locationEntity.id = Int32(locationsHandler.count) - locationEntity.altitude = Int32(loc.altitude) - locationEntity.heading = Int32(loc.course) - locationEntity.speed = Int32(loc.speed) - locationEntity.latitudeI = Int32(loc.coordinate.latitude * 1e7) - locationEntity.longitudeI = Int32(loc.coordinate.longitude * 1e7) - do { - try context.save() - print("💾 Saved a new route location") - //print("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num)") - } catch { - context.rollback() - let nsError = error as NSError - print("💥 Error Saving LocationEntity from the Route Recorder \(nsError)") - } - } - } - } - } - } - } - .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) - } -} +// } +// .mapStyle(mapStyle) +// } +// .mapScope(routerecorderscope) +// .safeAreaInset(edge: .bottom) { +// ZStack { +// VStack { +// HStack(spacing: 10) { +// Spacer() +// +// Button { +// isShowingDetails = true +// } label: { +// Image(systemName: locationsHandler.isRecording ? "record.circle.fill" : "record.circle") +// .font(.system(size: 72)) +// .symbolRenderingMode(.multicolor) +// .foregroundColor(.red) +// } +// .buttonStyle(.bordered) +// .foregroundColor(.red) +// .buttonBorderShape(.circle) +// .matchedGeometryEffect(id: "Details Button", in: namespace) +// +// Spacer() +// } +// } +// } +// .padding() +// } +// .sheet(isPresented: $isShowingDetails) { +// NavigationStack { +// VStack { +// if locationsHandler.isRecording { +// HStack (alignment: .center) { +// Image(systemName: "record.circle.fill") +// .symbolRenderingMode(.multicolor) +// .font(.title) +// .foregroundColor(.red) +// Text("Recording route") +// .font(.title) +// Spacer() +// Text("\(locationsHandler.count)") +// .foregroundColor(.red) +// .font(.title2) +// } +// .padding() +// } else if locationsHandler.isRecordingPaused { +// HStack (alignment: .center) { +// +// Image(systemName: "playpause") +// .symbolRenderingMode(.multicolor) +// .font(.title3) +// .foregroundColor(.red) +// Text("Route recording paused") +// .font(.title) +// } +// .padding(.top) +// } +// +// if locationsHandler.isRecording || locationsHandler.isRecordingPaused { +// Divider() +// HStack { +// VStack { +// Text(locationsHandler.recordingStarted ?? Date(), style: .timer) +// .font(.title) +// .fixedSize() +// Text("Time") +// .font(.callout) +// .fixedSize() +// } +// .padding(.horizontal) +// Divider() +// VStack { +// let distance = Measurement(value: locationsHandler.distanceTraveled, unit: UnitLength.meters) +// Text("\(distance.formatted())") +// .font(.title) +// .fixedSize() +// Text("Distance") +// .font(.callout) +// .fixedSize() +// } +// .padding(.horizontal) +// Divider() +// VStack { +// let gain = Measurement(value: locationsHandler.elevationGain, unit: UnitLength.meters) +// Text(gain.formatted()) +// .font(.title) +// Text("Elev. Gain") +// .font(.callout) +// } +// .padding(.horizontal) +// } +// .frame(maxHeight: 90) +// } +// Divider() +// VStack(alignment: .leading) { +// List { +// GPSStatus(largeFont: .body, smallFont: .callout) +// } +// .listStyle(.plain) +// HStack { +// Spacer() +// if !locationsHandler.isRecording && !locationsHandler.isRecordingPaused { +// /// We are not recording or paused, show start recording button +// Button { +// locationsHandler.isRecording = true +// locationsHandler.count = 0 +// locationsHandler.distanceTraveled = 0.0 +// locationsHandler.elevationGain = 0.0 +// locationsHandler.locationsArray.removeAll() +// locationsHandler.recordingStarted = Date() +// let newRoute = RouteEntity(context: context) +// newRoute.name = String("Route Recording") +// newRoute.id = Int32.random(in: Int32(Int8.max) ... Int32.max) +// newRoute.color = Int64(UIColor.random.hex) +// newRoute.date = Date() +// newRoute.enabled = false +// color = Color(UIColor(hex: UInt32(newRoute.color))) +// self.recording = newRoute +// do { +// try context.save() +// print("💾 Saved a new route") +// } catch { +// context.rollback() +// let nsError = error as NSError +// print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") +// } +// } label: { +// Label("start", systemImage: "play") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding(.bottom) +// +// } else if locationsHandler.isRecording { +// /// We are recording show pause button +// Button { +// locationsHandler.isRecording = false +// locationsHandler.isRecordingPaused = true +// } label: { +// Label("pause", systemImage: "pause") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding(.bottom) +// } else if locationsHandler.isRecordingPaused { +// /// We are paused show resume button +// Button { +// locationsHandler.isRecording = true +// locationsHandler.isRecordingPaused = false +// } label: { +// Label("resume", systemImage: "playpause") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding(.bottom) +// } +// +// if locationsHandler.isRecording || locationsHandler.isRecordingPaused { +// /// We are recording or paused, show finish button +// Button { +// locationsHandler.isRecording = false +// locationsHandler.isRecordingPaused = false +// locationsHandler.distanceTraveled = 0.0 +// locationsHandler.elevationGain = 0.0 +// locationsHandler.locationsArray.removeAll() +// locationsHandler.recordingStarted = nil +// if let rec = recording { +// rec.enabled = true +// context.refresh(rec, mergeChanges:true) +// } +// +// do { +// try context.save() +// print("💾 Saved a route finish") +// } catch { +// context.rollback() +// let nsError = error as NSError +// print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") +// } +// } label: { +// Label("finish", systemImage: "flag.checkered") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding(.bottom) +// } +//#if targetEnvironment(macCatalyst) +// Button(role: .cancel) { +// isShowingDetails = false +// } label: { +// Label("close", systemImage: "xmark") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding(.bottom) +//#endif +// Spacer() +// } +// +// } +// } +// } +// .presentationDetents([.fraction(0.30), .fraction(0.65)]) +// .presentationDragIndicator(.hidden) +// .interactiveDismissDisabled(false) +// .onAppear { +// UIApplication.shared.isIdleTimerDisabled = true +// } +// .onDisappear(perform: { +// UIApplication.shared.isIdleTimerDisabled = false +// }) +// .onChange(of: locationsHandler.locationsArray.last) { newLoc in +// if locationsHandler.isRecording { +// if let loc = newLoc { +// if recording != nil { +// let locationEntity = LocationEntity(context: context) +// locationEntity.routeLocation = recording +// locationEntity.id = Int32(locationsHandler.count) +// locationEntity.altitude = Int32(loc.altitude) +// locationEntity.heading = Int32(loc.course) +// locationEntity.speed = Int32(loc.speed) +// locationEntity.latitudeI = Int32(loc.coordinate.latitude * 1e7) +// locationEntity.longitudeI = Int32(loc.coordinate.longitude * 1e7) +// do { +// try context.save() +// print("💾 Saved a new route location") +// //print("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num)") +// } catch { +// context.rollback() +// let nsError = error as NSError +// print("💥 Error Saving LocationEntity from the Route Recorder \(nsError)") +// } +// } +// } +// } +// } +// } +// } +// .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) +// } +//} diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index d9d6f598..65a94f63 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -69,14 +69,14 @@ struct Settings: View { Text("routes") } .tag(SettingsSidebar.routes) - NavigationLink { - RouteRecorder() - } label: { - Image(systemName: "record.circle") - .symbolRenderingMode(.hierarchical) - Text("route.recorder") - } - .tag(SettingsSidebar.routeRecorder) +// NavigationLink { +// RouteRecorder() +// } label: { +// Image(systemName: "record.circle") +// .symbolRenderingMode(.hierarchical) +// Text("route.recorder") +// } +// .tag(SettingsSidebar.routeRecorder) } let node = nodes.first(where: { $0.num == preferredNodeNum })