From 029a820f769417a3110dcb648c63069716202727 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 7 Feb 2024 15:42:20 -0800 Subject: [PATCH 01/22] Bump version, fix client hidden crash --- Meshtastic.xcodeproj/project.pbxproj | 8 ++++---- Meshtastic/Persistence/UpdateCoreData.swift | 8 +++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 49932aa0..37094294 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1493,7 +1493,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.21; + MARKETING_VERSION = 2.2.22; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1527,7 +1527,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.21; + MARKETING_VERSION = 2.2.22; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1649,7 +1649,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.21; + MARKETING_VERSION = 2.2.22; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1682,7 +1682,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.21; + MARKETING_VERSION = 2.2.22; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index f922d28c..b6aae30b 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -119,6 +119,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newNode.snr = packet.rxSnr newNode.rssi = packet.rxRssi newNode.viaMqtt = packet.viaMqtt + newNode.channel = Int32(packet.channel) if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { newNode.channel = Int32(nodeInfoMessage.channel) print(packet.channel) @@ -163,6 +164,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].snr = packet.rxSnr fetchedNode[0].rssi = packet.rxRssi fetchedNode[0].viaMqtt = packet.viaMqtt + fetchedNode[0].channel = Int32(packet.channel) if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { fetchedNode[0].channel = Int32(nodeInfoMessage.channel) @@ -369,7 +371,7 @@ func upsertDeviceConfigPacket(config: Meshtastic.Config.DeviceConfig, nodeNum: I newDeviceConfig.buttonGpio = Int32(config.buttonGpio) newDeviceConfig.buzzerGpio = Int32(config.buzzerGpio) newDeviceConfig.rebroadcastMode = Int32(config.rebroadcastMode.rawValue) - newDeviceConfig.nodeInfoBroadcastSecs = Int32(config.nodeInfoBroadcastSecs) + newDeviceConfig.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber) newDeviceConfig.doubleTapAsButtonPress = config.doubleTapAsButtonPress newDeviceConfig.isManaged = config.isManaged fetchedNode[0].deviceConfig = newDeviceConfig @@ -380,7 +382,7 @@ func upsertDeviceConfigPacket(config: Meshtastic.Config.DeviceConfig, nodeNum: I fetchedNode[0].deviceConfig?.buttonGpio = Int32(config.buttonGpio) fetchedNode[0].deviceConfig?.buzzerGpio = Int32(config.buzzerGpio) fetchedNode[0].deviceConfig?.rebroadcastMode = Int32(config.rebroadcastMode.rawValue) - fetchedNode[0].deviceConfig?.nodeInfoBroadcastSecs = Int32(config.nodeInfoBroadcastSecs) + fetchedNode[0].deviceConfig?.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber) fetchedNode[0].deviceConfig?.doubleTapAsButtonPress = config.doubleTapAsButtonPress fetchedNode[0].deviceConfig?.isManaged = config.isManaged } @@ -618,7 +620,7 @@ func upsertPositionConfigPacket(config: Meshtastic.Config.PositionConfig, nodeNu fetchedNode[0].positionConfig?.txGpio = Int32(config.txGpio) fetchedNode[0].positionConfig?.gpsEnGpio = Int32(config.gpsEnGpio) fetchedNode[0].positionConfig?.fixedPosition = config.fixedPosition - fetchedNode[0].positionConfig?.positionBroadcastSeconds = Int32(config.positionBroadcastSecs) + fetchedNode[0].positionConfig?.positionBroadcastSeconds = Int32(truncatingIfNeeded: config.positionBroadcastSecs) fetchedNode[0].positionConfig?.broadcastSmartMinimumIntervalSecs = Int32(config.broadcastSmartMinimumIntervalSecs) fetchedNode[0].positionConfig?.broadcastSmartMinimumDistance = Int32(config.broadcastSmartMinimumDistance) fetchedNode[0].positionConfig?.gpsAttemptTime = 900 From a89490097f1885c917953d518dd3818cf2a797bf Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 10 Feb 2024 17:21:31 -0800 Subject: [PATCH 02/22] Add Singapore Update store and forward logic --- Meshtastic.xcodeproj/project.pbxproj | 4 +- Meshtastic/Enums/LoraConfigEnums.swift | 7 + Meshtastic/Helpers/BLEManager.swift | 75 +++- .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 415 ++++++++++++++++++ Meshtastic/Persistence/UpdateCoreData.swift | 1 + .../Protobufs/meshtastic/config.pb.swift | 8 + .../Protobufs/meshtastic/portnums.pb.swift | 9 + .../Views/Nodes/Helpers/NodeListItem.swift | 6 +- Meshtastic/Views/Nodes/NodeList.swift | 20 + Meshtastic/Views/Settings/Firmware.swift | 2 +- 11 files changed, 521 insertions(+), 28 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 26.xcdatamodel/contents diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 37094294..6b056a19 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -229,6 +229,7 @@ C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMBTileOverlay.swift; sourceTree = ""; }; DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoEntityExtension.swift; sourceTree = ""; }; DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityExtension.swift; sourceTree = ""; }; + DD05296F2B77F454008E44CD /* MeshtasticDataModelV 26.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 26.xcdatamodel"; sourceTree = ""; }; DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV14.xcdatamodel; sourceTree = ""; }; DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminMessageList.swift; sourceTree = ""; }; DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionPopover.swift; sourceTree = ""; }; @@ -1793,6 +1794,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD05296F2B77F454008E44CD /* MeshtasticDataModelV 26.xcdatamodel */, DD23D9AB2B7133F6003F5CBE /* MeshtasticDataModelV 25.xcdatamodel */, DDB234392B5CA9B000DA6FB1 /* MeshtasticDataModelV 24.xcdatamodel */, DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */, @@ -1819,7 +1821,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD23D9AB2B7133F6003F5CBE /* MeshtasticDataModelV 25.xcdatamodel */; + currentVersion = DD05296F2B77F454008E44CD /* MeshtasticDataModelV 26.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Enums/LoraConfigEnums.swift b/Meshtastic/Enums/LoraConfigEnums.swift index 3e638773..53084e54 100644 --- a/Meshtastic/Enums/LoraConfigEnums.swift +++ b/Meshtastic/Enums/LoraConfigEnums.swift @@ -26,6 +26,7 @@ enum RegionCodes: Int, CaseIterable, Identifiable { case ua868 = 15 case my_433 = 16 case my_919 = 17 + case sg_923 = 18 case lora24 = 13 var id: Int { self.rawValue } @@ -67,6 +68,8 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return "Malaysia 433mhz" case .my_919: return "Malaysia 919mhz" + case .sg_923: + return "Singapore 923mhz" } } var dutyCycle: Int { @@ -107,6 +110,8 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return 100 case .my_919: return 100 + case .sg_923: + return 100 } } func protoEnumValue() -> Config.LoRaConfig.RegionCode { @@ -148,6 +153,8 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return Config.LoRaConfig.RegionCode.my433 case .my_919: return Config.LoRaConfig.RegionCode.my919 + case .sg_923: + return Config.LoRaConfig.RegionCode.sg923 } } } diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 5e9e19c1..33c64012 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -703,6 +703,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate MeshLogger.log("🕸️ MESH PACKET received for Other App UNHANDLED \(try! decodedInfo.packet.jsonString())") case .max: print("MAX PORT NUM OF 511") + case .atakPlugin: + MeshLogger.log("🕸️ MESH PACKET received for ATAK Plugin App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") } if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == configNonce { @@ -2367,6 +2369,36 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return false } + public func requestStoreAndForwardClientHistory(fromUser: UserEntity, toUser: UserEntity) -> Bool { + + /// send a request for ClientHistory with a time period matching the heartbeat + var sfPacket = StoreAndForward() + sfPacket.rr = StoreAndForward.RequestResponse.clientHistory + sfPacket.history.window = 120 // storeAndForwardMessage.heartbeat.period + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. _XCCurrentVersionName - MeshtasticDataModelV 25.xcdatamodel + MeshtasticDataModelV 26.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 26.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 26.xcdatamodel/contents new file mode 100644 index 00000000..9dec53eb --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 26.xcdatamodel/contents @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index b6aae30b..b2822b05 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -282,6 +282,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].snr = packet.rxSnr fetchedNode[0].rssi = packet.rxRssi fetchedNode[0].viaMqtt = packet.viaMqtt + fetchedNode[0].channel = Int32(packet.channel) fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet do { diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index ef43b94a..38991510 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -1168,6 +1168,10 @@ struct Config { /// /// Malaysia 919mhz case my919 // = 17 + + /// + /// Singapore 923mhz + case sg923 // = 18 case UNRECOGNIZED(Int) init() { @@ -1194,6 +1198,7 @@ struct Config { case 15: self = .ua868 case 16: self = .my433 case 17: self = .my919 + case 18: self = .sg923 default: self = .UNRECOGNIZED(rawValue) } } @@ -1218,6 +1223,7 @@ struct Config { case .ua868: return 15 case .my433: return 16 case .my919: return 17 + case .sg923: return 18 case .UNRECOGNIZED(let i): return i } } @@ -1488,6 +1494,7 @@ extension Config.LoRaConfig.RegionCode: CaseIterable { .ua868, .my433, .my919, + .sg923, ] } @@ -2416,6 +2423,7 @@ extension Config.LoRaConfig.RegionCode: SwiftProtobuf._ProtoNameProviding { 15: .same(proto: "UA_868"), 16: .same(proto: "MY_433"), 17: .same(proto: "MY_919"), + 18: .same(proto: "SG_923"), ] } diff --git a/Meshtastic/Protobufs/meshtastic/portnums.pb.swift b/Meshtastic/Protobufs/meshtastic/portnums.pb.swift index 319d48f9..ea5ce5bd 100644 --- a/Meshtastic/Protobufs/meshtastic/portnums.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/portnums.pb.swift @@ -176,6 +176,11 @@ enum PortNum: SwiftProtobuf.Enum { /// ENCODING: Protobuf case neighborinfoApp // = 71 + /// + /// ATAK Plugin + /// Portnum for payloads from the official Meshtastic ATAK plugin + case atakPlugin // = 72 + /// /// Private applications should use portnums >= 256. /// To simplify initial development and testing you can use "PRIVATE_APP" @@ -220,6 +225,7 @@ enum PortNum: SwiftProtobuf.Enum { case 69: self = .simulatorApp case 70: self = .tracerouteApp case 71: self = .neighborinfoApp + case 72: self = .atakPlugin case 256: self = .privateApp case 257: self = .atakForwarder case 511: self = .max @@ -251,6 +257,7 @@ enum PortNum: SwiftProtobuf.Enum { case .simulatorApp: return 69 case .tracerouteApp: return 70 case .neighborinfoApp: return 71 + case .atakPlugin: return 72 case .privateApp: return 256 case .atakForwarder: return 257 case .max: return 511 @@ -287,6 +294,7 @@ extension PortNum: CaseIterable { .simulatorApp, .tracerouteApp, .neighborinfoApp, + .atakPlugin, .privateApp, .atakForwarder, .max, @@ -325,6 +333,7 @@ extension PortNum: SwiftProtobuf._ProtoNameProviding { 69: .same(proto: "SIMULATOR_APP"), 70: .same(proto: "TRACEROUTE_APP"), 71: .same(proto: "NEIGHBORINFO_APP"), + 72: .same(proto: "ATAK_PLUGIN"), 256: .same(proto: "PRIVATE_APP"), 257: .same(proto: "ATAK_FORWARDER"), 511: .same(proto: "MAX"), diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 4dfb1af2..f4d87e08 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -95,14 +95,16 @@ struct NodeListItem: View { .font(.callout) .symbolRenderingMode(.hierarchical) Text("Channel: \(node.channel)") - .font(.callout) + .foregroundColor(.gray) + .font(.caption) } if node.viaMqtt && connectedNode != node.num { Image(systemName: "network") .symbolRenderingMode(.hierarchical) .font(.callout) Text("Via MQTT") - .font(.callout) + .foregroundColor(.gray) + .font(.caption) } } if !connected { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index fec6c003..18b923ad 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -12,6 +12,7 @@ struct NodeList: View { @State private var columnVisibility = NavigationSplitViewVisibility.all @State private var selectedNode: NodeInfoEntity? @State private var isPresentingTraceRouteSentAlert = false + @State private var isPresentingClientHistorySentAlert = false @State private var isPresentingDeleteNodeAlert = false @State private var deleteNodeId: Int64 = 0 @@ -84,6 +85,17 @@ struct NodeList: View { } label: { Label("Trace Route", systemImage: "signpost.right.and.left") } + if true {//node?.storeForwardConfig != nil && node.storeForwardConfig?.isRouter ?? false { + + Button { + let success = bleManager.requestStoreAndForwardClientHistory(fromUser: connectedNode!.user!, toUser: node.user!) + if success { + isPresentingClientHistorySentAlert = true + } + } label: { + Label("Client History", systemImage: "envelope.arrow.triangle.branch") + } + } } if bleManager.connectedPeripheral != nil { Button (role: .destructive) { @@ -103,6 +115,14 @@ struct NodeList: View { } message: { Text("This could take a while, response will appear in the trace route log for the node it was sent to.") } + .alert( + "Store and Forward Client Hitory Request Sent", + isPresented: $isPresentingClientHistorySentAlert + ) { + Button("OK", role: .cancel) { } + } message: { + Text("Any messages you have missed will be delivered again.") + } } .searchable(text: nodesQuery, prompt: "Find a node") .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 20efcff7..21aa3b82 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -12,7 +12,7 @@ struct Firmware: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager var node: NodeInfoEntity? - @State var minimumVersion = "2.2.17" + @State var minimumVersion = "2.2.21" @State var version = "" @State private var currentDevice: DeviceHardware? @State private var latestStable: FirmwareRelease? From aba2c9bece5a3ffd3887941c29a4e16a497583e3 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 10 Feb 2024 17:43:01 -0800 Subject: [PATCH 03/22] Add store and forward router bool --- .../Extensions/CoreData/NodeInfoEntityExtension.swift | 4 ++++ Meshtastic/Views/Nodes/Helpers/NodeListItem.swift | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index 00175801..6c9254fa 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -31,6 +31,10 @@ extension NodeInfoEntity { return traceRoutes?.count ?? 0 > 0 } + var isStoreForwardRouter: Bool { + return storeForwardConfig?.isRouter ?? false + } + var isOnline: Bool { let fifteenMinutesAgo = Calendar.current.date(byAdding: .minute, value: -15, to: Date()) if lastHeard?.compare(fifteenMinutesAgo!) == .orderedDescending { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index f4d87e08..b6d1a673 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -60,6 +60,16 @@ struct NodeListItem: View { Text("Role: \(role?.name ?? "unknown".localized)") .font(.callout) } + if node.isStoreForwardRouter { + HStack { + Image(systemName: "envelope.arrow.triangle.branch") + .font(.callout) + .symbolRenderingMode(.hierarchical) + Text("storeforward".localized) + .font(.callout) + } + } + if node.positions?.count ?? 0 > 0 && connectedNode != node.num { HStack { let lastPostion = node.positions!.reversed()[0] as! PositionEntity From 880509a9a72217bee0101324023f88b656433829 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 10 Feb 2024 17:43:54 -0800 Subject: [PATCH 04/22] Add route recorder back --- 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 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 65a94f63..d9d6f598 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 }) From a5ae02978e72066f71647d9befc47231f64b30c7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 10 Feb 2024 18:03:13 -0800 Subject: [PATCH 05/22] Tidy up the node list --- .../Views/Helpers/BatteryLevelCompact.swift | 3 +++ Meshtastic/Views/Helpers/LoRaSignalStrength.swift | 6 ++++-- Meshtastic/Views/Nodes/Helpers/NodeListItem.swift | 15 ++++++++++----- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Meshtastic/Views/Helpers/BatteryLevelCompact.swift b/Meshtastic/Views/Helpers/BatteryLevelCompact.swift index f45c1c33..e3fc1100 100644 --- a/Meshtastic/Views/Helpers/BatteryLevelCompact.swift +++ b/Meshtastic/Views/Helpers/BatteryLevelCompact.swift @@ -62,12 +62,15 @@ struct BatteryLevelCompact: View { } if batteryLevel > 100 { Text("PWD") + .foregroundStyle(.gray) .font(font) } else if batteryLevel == 100 { Text("CHG") + .foregroundStyle(.gray) .font(font) } else { Text("\(batteryLevel)%") + .foregroundStyle(.gray) .font(font) } } diff --git a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift index 7e6cca8f..d388cb93 100644 --- a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift +++ b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift @@ -33,13 +33,15 @@ struct LoRaSignalStrengthMeter: View { Gauge(value: Double(signalStrength.rawValue), in: 0...3) { } currentValueLabel: { Image(systemName: "dot.radiowaves.left.and.right") - .font(.callout) + .font(.caption) Text("Signal \(signalStrength.description)") - .font(.callout) + .font(.caption) + .foregroundColor(.gray) } .gaugeStyle(.accessoryLinear) .tint(gradient) .font(.caption) + } } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index b6d1a673..9365cffa 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -50,7 +50,8 @@ struct NodeListItem: View { .symbolRenderingMode(.hierarchical) .foregroundColor(node.isOnline ? .green : .orange) LastHeardText(lastHeard: node.lastHeard) - .font(.callout) + .font(.caption) + .foregroundColor(.gray) } HStack { let role = DeviceRoles(rawValue: Int(node.user?.role ?? 0)) @@ -58,7 +59,8 @@ struct NodeListItem: View { .font(.callout) .symbolRenderingMode(.hierarchical) Text("Role: \(role?.name ?? "unknown".localized)") - .font(.callout) + .font(.caption) + .foregroundColor(.gray) } if node.isStoreForwardRouter { HStack { @@ -66,7 +68,8 @@ struct NodeListItem: View { .font(.callout) .symbolRenderingMode(.hierarchical) Text("storeforward".localized) - .font(.callout) + .font(.caption) + .foregroundColor(.gray) } } @@ -82,7 +85,8 @@ struct NodeListItem: View { Image(systemName: "lines.measurement.horizontal") .font(.callout) .symbolRenderingMode(.hierarchical) - DistanceText(meters: metersAway).font(.callout) + DistanceText(meters: metersAway).font(.caption) + .foregroundColor(.gray) } } } else { @@ -94,7 +98,8 @@ struct NodeListItem: View { Image(systemName: "lines.measurement.horizontal") .font(.callout) .symbolRenderingMode(.hierarchical) - DistanceText(meters: metersAway).font(.callout) + DistanceText(meters: metersAway).font(.caption) + .foregroundColor(.gray) } } } From e9aec54508a8dbceb9997714fd1c14bb5098f183 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 11 Feb 2024 17:45:03 -0800 Subject: [PATCH 06/22] Hook up store and forward as reccomended by @GUVWAF --- Meshtastic/Helpers/BLEManager.swift | 31 +++++++++++++- Meshtastic/Protobufs/meshtastic/mesh.pb.swift | 8 ++++ .../meshtastic/storeforward.pb.swift | 41 ++++++++++--------- 3 files changed, 58 insertions(+), 22 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 33c64012..c0a71255 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -2374,7 +2374,8 @@ 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 = 120 // storeAndForwardMessage.heartbeat.period + sfPacket.history.window = UInt32(toUser.userNode?.storeForwardConfig?.historyReturnWindow ?? 120) + sfPacket.history.lastRequest = UInt32(toUser.userNode?.storeForwardConfig?.lastRequest?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2401,7 +2402,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate func storeAndForwardPacket(packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) { if let storeAndForwardMessage = try? StoreAndForward(serializedData: packet.decoded.payload) { - // Request Response + + // Handle the text variant as a text message packet + if storeAndForwardMessage.variant == StoreAndForward.OneOf_Variant.text(packet.decoded.payload) { + MeshLogger.log("📮 Store and Forward history text message received \(storeAndForwardMessage)") + textMessageAppPacket(packet: packet, connectedNode: connectedNodeNum, context: context) + return + } + // Handle each of the store and forward request / response messages switch storeAndForwardMessage.rr { case .unset: MeshLogger.log("📮 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") @@ -2424,6 +2432,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate newConfig.enabled = true newConfig.isRouter = storeAndForwardMessage.heartbeat.secondary == 0 newConfig.lastHeartbeat = Date() + routerNode.storeForwardConfig = newConfig } do { @@ -2441,6 +2450,24 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate case .routerBusy: MeshLogger.log("🐝 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") case .routerHistory: + /// Set the Router History Last Request Value + guard let routerNode = getNodeInfo(id: Int64(packet.from), context: context) else { + return + } + if routerNode.storeForwardConfig != nil { + routerNode.storeForwardConfig?.lastRequest = Date(timeIntervalSince1970: TimeInterval(storeAndForwardMessage.history.lastRequest)) + } else { + let newConfig = StoreForwardConfigEntity(context: context) + newConfig.lastRequest = Date(timeIntervalSince1970: TimeInterval(storeAndForwardMessage.history.lastRequest)) + routerNode.storeForwardConfig = newConfig + } + + do { + try context.save() + } catch { + context.rollback() + print("💥 Save Store and Forward Router Error") + } MeshLogger.log("📜 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") case .routerStats: MeshLogger.log("📊 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index 7496d559..1380fba9 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -130,6 +130,10 @@ enum HardwareModel: SwiftProtobuf.Enum { /// Canary Radio Company - CanaryOne: https://canaryradio.io/products/canaryone case canaryone // = 29 + /// + /// Waveshare RP2040 LoRa - https://www.waveshare.com/rp2040-lora.htm + case rp2040Lora // = 30 + /// /// --------------------------------------------------------------------------- /// Less common/prototype boards listed here (needs one more byte over the air) @@ -280,6 +284,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case 27: self = .senseloraRp2040 case 28: self = .senseloraS3 case 29: self = .canaryone + case 30: self = .rp2040Lora case 32: self = .loraRelayV1 case 33: self = .nrf52840Dk case 34: self = .ppr @@ -338,6 +343,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case .senseloraRp2040: return 27 case .senseloraS3: return 28 case .canaryone: return 29 + case .rp2040Lora: return 30 case .loraRelayV1: return 32 case .nrf52840Dk: return 33 case .ppr: return 34 @@ -401,6 +407,7 @@ extension HardwareModel: CaseIterable { .senseloraRp2040, .senseloraS3, .canaryone, + .rp2040Lora, .loraRelayV1, .nrf52840Dk, .ppr, @@ -2589,6 +2596,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 27: .same(proto: "SENSELORA_RP2040"), 28: .same(proto: "SENSELORA_S3"), 29: .same(proto: "CANARYONE"), + 30: .same(proto: "RP2040_LORA"), 32: .same(proto: "LORA_RELAY_V1"), 33: .same(proto: "NRF52840DK"), 34: .same(proto: "PPR"), diff --git a/Meshtastic/Protobufs/meshtastic/storeforward.pb.swift b/Meshtastic/Protobufs/meshtastic/storeforward.pb.swift index 925eb558..891e8ef0 100644 --- a/Meshtastic/Protobufs/meshtastic/storeforward.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/storeforward.pb.swift @@ -66,13 +66,13 @@ struct StoreAndForward { } /// - /// Empty Payload - var empty: Bool { + /// Text from history message. + var text: Data { get { - if case .empty(let v)? = variant {return v} - return false + if case .text(let v)? = variant {return v} + return Data() } - set {variant = .empty(newValue)} + set {variant = .text(newValue)} } var unknownFields = SwiftProtobuf.UnknownStorage() @@ -90,8 +90,8 @@ struct StoreAndForward { /// TODO: REPLACE case heartbeat(StoreAndForward.Heartbeat) /// - /// Empty Payload - case empty(Bool) + /// Text from history message. + case text(Data) #if !swift(>=4.1) static func ==(lhs: StoreAndForward.OneOf_Variant, rhs: StoreAndForward.OneOf_Variant) -> Bool { @@ -111,8 +111,8 @@ struct StoreAndForward { guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() } return l == r }() - case (.empty, .empty): return { - guard case .empty(let l) = lhs, case .empty(let r) = rhs else { preconditionFailure() } + case (.text, .text): return { + guard case .text(let l) = lhs, case .text(let r) = rhs else { preconditionFailure() } return l == r }() default: return false @@ -268,11 +268,11 @@ struct StoreAndForward { var heartbeat: Bool = false /// - /// Is the heartbeat enabled on the server? + /// Maximum number of messages the server will return. var returnMax: UInt32 = 0 /// - /// Is the heartbeat enabled on the server? + /// Maximum history window in minutes the server will return messages from. var returnWindow: UInt32 = 0 var unknownFields = SwiftProtobuf.UnknownStorage() @@ -296,7 +296,8 @@ struct StoreAndForward { var window: UInt32 = 0 /// - /// The window of messages that was used to filter the history client requested + /// Index in the packet history of the last message sent in a previous request to the server. + /// Will be sent to the client before sending the history and can be set in a subsequent request to avoid getting packets the server already sent to the client. var lastRequest: UInt32 = 0 var unknownFields = SwiftProtobuf.UnknownStorage() @@ -312,7 +313,7 @@ struct StoreAndForward { // methods supported on all messages. /// - /// Number of that will be sent to the client + /// Period in seconds that the heartbeat is sent out that will be sent to the client var period: UInt32 = 0 /// @@ -371,7 +372,7 @@ extension StoreAndForward: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen 2: .same(proto: "stats"), 3: .same(proto: "history"), 4: .same(proto: "heartbeat"), - 5: .same(proto: "empty"), + 5: .same(proto: "text"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -421,11 +422,11 @@ extension StoreAndForward: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen } }() case 5: try { - var v: Bool? - try decoder.decodeSingularBoolField(value: &v) + var v: Data? + try decoder.decodeSingularBytesField(value: &v) if let v = v { if self.variant != nil {try decoder.handleConflictingOneOf()} - self.variant = .empty(v) + self.variant = .text(v) } }() default: break @@ -454,9 +455,9 @@ extension StoreAndForward: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen guard case .heartbeat(let v)? = self.variant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 4) }() - case .empty?: try { - guard case .empty(let v)? = self.variant else { preconditionFailure() } - try visitor.visitSingularBoolField(value: v, fieldNumber: 5) + case .text?: try { + guard case .text(let v)? = self.variant else { preconditionFailure() } + try visitor.visitSingularBytesField(value: v, fieldNumber: 5) }() case nil: break } From 1e2cb76b0f04f573ad2a2ac7ea9f4bbc5aa67982 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 11 Feb 2024 17:50:18 -0800 Subject: [PATCH 07/22] Add text message handling to unset --- Meshtastic/Helpers/BLEManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index c0a71255..dad6249a 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -2413,6 +2413,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate switch storeAndForwardMessage.rr { case .unset: MeshLogger.log("📮 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") + textMessageAppPacket(packet: packet, connectedNode: connectedNodeNum, context: context) case .routerError: MeshLogger.log("☠️ Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") case .routerHeartbeat: From 416d5e5f413da750e6a924080d3f12e19f4eb202 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 11 Feb 2024 19:38:51 -0800 Subject: [PATCH 08/22] Frequency Slot --- Meshtastic/Views/Settings/Config/LoRaConfig.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 9b7b33a2..d19d41f2 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -161,9 +161,9 @@ struct LoRaConfig: View { .font(.caption) HStack { - Text("LoRa Channel Number") + Text("LoRa Frequency Slot") .fixedSize() - TextField("Channel Number", value: $channelNum, formatter: formatter) + TextField("Frequency Slot", value: $channelNum, formatter: formatter) .toolbar { ToolbarItemGroup(placement: .keyboard) { Button("dismiss.keyboard") { From 3882add56af18dc08bc5b340a26eab0788b1d782 Mon Sep 17 00:00:00 2001 From: Austin Payne Date: Mon, 5 Feb 2024 23:31:54 -0700 Subject: [PATCH 09/22] fix: slow typing speed when lots of messages Refactors both the channel and user message views to isolate typing state which prevents excessive re-rendering of large message lists on every new character typed. Also consolidates typing view code of both lists into the new TextMessageField and related sub views. --- Meshtastic.xcodeproj/project.pbxproj | 24 +++ Meshtastic/Helpers/BLEManager.swift | 6 +- .../Views/Messages/ChannelMessageList.swift | 147 +-------------- .../TextMessageField/AlertButton.swift | 21 +++ .../RequestPositionButton.swift | 20 +++ .../TextMessageField/TextMessageField.swift | 170 ++++++++++++++++++ .../TextMessageField/TextMessageSize.swift | 20 +++ .../Views/Messages/UserMessageList.swift | 117 +----------- 8 files changed, 275 insertions(+), 250 deletions(-) create mode 100644 Meshtastic/Views/Messages/TextMessageField/AlertButton.swift create mode 100644 Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift create mode 100644 Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift create mode 100644 Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 6b056a19..b6989aed 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -10,8 +10,12 @@ 6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */; }; 6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */; }; 6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */; }; + B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; }; C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */; }; C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; }; + D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */; }; + D9C983A02B79D0E800BDBE6A /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */; }; + D9C983A22B79D1A600BDBE6A /* RequestPositionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C983A12B79D1A600BDBE6A /* RequestPositionButton.swift */; }; DD007BAE2AA4E91200F5FA12 /* MyInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */; }; DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */; }; DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */ = {isa = PBXBuildFile; productRef = DD0D3D212A55CEB10066DB71 /* CocoaMQTT */; }; @@ -226,7 +230,11 @@ 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = ""; }; 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = ""; }; A65FA974296876BF00A97686 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = ""; }; C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMBTileOverlay.swift; sourceTree = ""; }; + D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageSize.swift; sourceTree = ""; }; + D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = ""; }; + D9C983A12B79D1A600BDBE6A /* RequestPositionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestPositionButton.swift; sourceTree = ""; }; DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoEntityExtension.swift; sourceTree = ""; }; DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityExtension.swift; sourceTree = ""; }; DD05296F2B77F454008E44CD /* MeshtasticDataModelV 26.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 26.xcdatamodel"; sourceTree = ""; }; @@ -487,6 +495,17 @@ path = Custom; sourceTree = ""; }; + D9C9839E2B79D0C600BDBE6A /* TextMessageField */ = { + isa = PBXGroup; + children = ( + B3E905B02B71F7F300654D07 /* TextMessageField.swift */, + D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */, + D9C983A12B79D1A600BDBE6A /* RequestPositionButton.swift */, + D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */, + ); + path = TextMessageField; + sourceTree = ""; + }; DD007BB12AA59B9A00F5FA12 /* CoreData */ = { isa = PBXGroup; children = ( @@ -818,6 +837,7 @@ DDC2E18B26CE25A70042C5E4 /* Messages */ = { isa = PBXGroup; children = ( + D9C9839E2B79D0C600BDBE6A /* TextMessageField */, DDB8F4132A9EE5F000230ECE /* ChannelList.swift */, DD798B062915928D005217CD /* ChannelMessageList.swift */, DDB8F40F2A9EE5B400230ECE /* Messages.swift */, @@ -1162,6 +1182,7 @@ DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */, DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */, DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */, + D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */, DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */, DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, @@ -1238,6 +1259,7 @@ DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */, DDDE5A1029AFE69700490C6C /* MeshActivityAttributes.swift in Sources */, DD1925B928CDA93900720036 /* SerialConfigEnums.swift in Sources */, + D9C983A02B79D0E800BDBE6A /* AlertButton.swift in Sources */, DD86D4112881D16900BAEB7A /* WriteCsvFile.swift in Sources */, DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */, DD86D40F2881BE4C00BAEB7A /* CsvDocument.swift in Sources */, @@ -1271,6 +1293,7 @@ DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */, DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */, DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */, + D9C983A22B79D1A600BDBE6A /* RequestPositionButton.swift in Sources */, DDDB26442AAC0206003AFCB7 /* NodeDetail.swift in Sources */, DD5E5210298EE33B00D21B61 /* telemetry.pb.swift in Sources */, DD77093F2AA1B146007A8BF0 /* UIColor.swift in Sources */, @@ -1281,6 +1304,7 @@ DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */, DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */, DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */, + B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */, DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */, DD5E5204298EE33B00D21B61 /* xmodem.pb.swift in Sources */, DDE5B4062B227E3200FCDD05 /* TraceRouteEntityExtension.swift in Sources */, diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index dad6249a..11dbc360 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -38,7 +38,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var timeoutTimerCount = 0 var positionTimer: Timer? var lastPosition: CLLocationCoordinate2D? - let emptyNodeNum: UInt32 = 4294967295 + static let emptyNodeNum: UInt32 = 4294967295 let mqttManager = MqttClientProxyManager.shared var wantRangeTestPackets = false var wantStoreAndForwardPackets = false @@ -865,7 +865,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if toUserNum > 0 { meshPacket.to = UInt32(toUserNum) } else { - meshPacket.to = emptyNodeNum + meshPacket.to = Self.emptyNodeNum } meshPacket.channel = UInt32(channel) meshPacket.from = UInt32(fromUserNum) @@ -912,7 +912,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var success = false let fromNodeNum = UInt32(connectedPeripheral.num) var meshPacket = MeshPacket() - meshPacket.to = emptyNodeNum + meshPacket.to = Self.emptyNodeNum meshPacket.from = fromNodeNum meshPacket.wantAck = true var dataMessage = DataMessage() diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index e1a8811d..e9f469d6 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -9,27 +9,18 @@ import SwiftUI import CoreData struct ChannelMessageList: View { - @StateObject var appState = AppState.shared @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - enum Field: Hashable { - case messageText - } - // Keyboard State - @State var typingMessage: String = "" - @State private var totalBytes = 0 - var maxbytes = 228 - @FocusState var focusedField: Field? + @FocusState var messageFieldFocused: Bool @ObservedObject var myInfo: MyInfoEntity @ObservedObject var channel: ChannelEntity @State var showDeleteMessageAlert = false @State private var deleteMessageId: Int64 = 0 @State private var replyMessageId: Int64 = 0 - @State private var sendPositionWithMessage: Bool = false @AppStorage("preferredPeripheralNum") private var preferredPeripheralNum = -1 var body: some View { @@ -115,7 +106,7 @@ struct ChannelMessageList: View { } Button(action: { self.replyMessageId = message.messageId - self.focusedField = .messageText + self.messageFieldFocused = true print("I want to reply to \(message.messageId)") }) { Text("reply") @@ -288,134 +279,14 @@ struct ChannelMessageList: View { } }) } - #if targetEnvironment(macCatalyst) - HStack { - Spacer() - Button { - let bell = "🔔 Alert Bell Character! \u{7}" - print(bell) - typingMessage += bell - - } label: { - Text("Alert Bell") - Image(systemName: "bell.fill") - .symbolRenderingMode(.hierarchical) - .imageScale(.large).foregroundColor(.accentColor) - } - Spacer() - Button { - let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" - sendPositionWithMessage = true - typingMessage += "📍 " + userLongName + " has shared their position with you." - } label: { - Text("share.position") - Image(systemName: "mappin.and.ellipse") - .symbolRenderingMode(.hierarchical) - .imageScale(.large).foregroundColor(.accentColor) - } - ProgressView("\("bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) - .frame(width: 130) - .padding(5) - .font(.subheadline) - .accentColor(.accentColor) - .padding(.trailing) + + TextMessageField( + destination: .channel(channel.index), + replyMessageId: $replyMessageId, + isFocused: $messageFieldFocused + ) { + context.refresh(channel, mergeChanges: true) } - #endif - HStack(alignment: .top) { - - ZStack { - TextField("message", text: $typingMessage, axis: .vertical) - .onChange(of: typingMessage, perform: { value in - totalBytes = value.utf8.count - // Only mess with the value if it is too big - if totalBytes > maxbytes { - let firstNBytes = Data(typingMessage.utf8.prefix(maxbytes)) - if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { - // Set the message back to the last place where it was the right size - typingMessage = maxBytesString - } else { - print("not a valid UTF-8 sequence") - } - } - }) - .keyboardType(.default) - .toolbar { - ToolbarItemGroup(placement: .keyboard) { - Button("dismiss.keyboard") { - focusedField = nil - } - .font(.subheadline) - Spacer() - Button { - let bell = "🔔 Alert Bell Character! \u{7}" - print(bell) - typingMessage += bell - - } label: { - Text("Alert") - Image(systemName: "bell.fill") - .symbolRenderingMode(.hierarchical) - .imageScale(.large).foregroundColor(.accentColor) - } - Spacer() - Button { - let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" - sendPositionWithMessage = true - typingMessage = "📍 " + userLongName + " has shared their position with you." - - } label: { - Image(systemName: "mappin.and.ellipse") - .symbolRenderingMode(.hierarchical) - .imageScale(.large).foregroundColor(.accentColor) - } - - ProgressView("\("bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) - .frame(width: 130) - .padding(5) - .font(.subheadline) - .accentColor(.accentColor) - } - } - .padding(.horizontal, 8) - .focused($focusedField, equals: .messageText) - .multilineTextAlignment(.leading) - .frame(minHeight: 50) - .keyboardShortcut(.defaultAction) - .onSubmit { - #if targetEnvironment(macCatalyst) - if bleManager.sendMessage(message: typingMessage, toUserNum: 0, channel: channel.index, isEmoji: false, replyID: replyMessageId) { - typingMessage = "" - focusedField = nil - replyMessageId = 0 - if sendPositionWithMessage { - if bleManager.sendPosition(channel: Int32(channel.index), destNum: Int64(bleManager.emptyNodeNum), wantResponse: false) { - print("Location Sent") - } - } - } - #endif - } - Text(typingMessage).opacity(0).padding(.all, 0) - } - .overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1)) - .padding(.bottom, 15) - Button(action: { - if bleManager.sendMessage(message: typingMessage, toUserNum: 0, channel: channel.index, isEmoji: false, replyID: replyMessageId) { - typingMessage = "" - focusedField = nil - replyMessageId = 0 - if sendPositionWithMessage { - if bleManager.sendPosition(channel: Int32(channel.index), destNum: Int64(bleManager.emptyNodeNum), wantResponse: false) { - print("Location Sent") - } - } - } - }) { - Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.accentColor) - } - - } - .padding(.all, 15) } .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/Meshtastic/Views/Messages/TextMessageField/AlertButton.swift b/Meshtastic/Views/Messages/TextMessageField/AlertButton.swift new file mode 100644 index 00000000..84820bef --- /dev/null +++ b/Meshtastic/Views/Messages/TextMessageField/AlertButton.swift @@ -0,0 +1,21 @@ +import SwiftUI + +struct AlertButton: View { + let action: () -> Void + + var body: some View { + Button(action: action) { + Text("Alert") + Image(systemName: "bell.fill") + .symbolRenderingMode(.hierarchical) + .imageScale(.large) + .foregroundColor(.accentColor) + } + } +} + +struct AlertButtonPreview: PreviewProvider { + static var previews: some View { + AlertButton {} + } +} diff --git a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift new file mode 100644 index 00000000..4a69ea50 --- /dev/null +++ b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift @@ -0,0 +1,20 @@ +import SwiftUI + +struct RequestPositionButton: View { + let action: () -> Void + + var body: some View { + Button(action: action) { + Image(systemName: "mappin.and.ellipse") + .symbolRenderingMode(.hierarchical) + .imageScale(.large) + .foregroundColor(.accentColor) + } + } +} + +struct RequestPositionButtonPreview: PreviewProvider { + static var previews: some View { + RequestPositionButton {} + } +} diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift new file mode 100644 index 00000000..67175642 --- /dev/null +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageField.swift @@ -0,0 +1,170 @@ +import SwiftUI + +struct TextMessageField: View { + static let maxbytes = 228 + @EnvironmentObject var bleManager: BLEManager + + let destination: Destination + @Binding var replyMessageId: Int64 + @FocusState.Binding var isFocused: Bool + let onSubmit: () -> Void + + enum Destination { + case user(Int64) + case channel(Int32) + } + + @State private var typingMessage: String = "" + @State private var totalBytes = 0 + @State private var sendPositionWithMessage = false + + var body: some View { + #if targetEnvironment(macCatalyst) + HStack { + if destination.showAlertButton { + Spacer() + AlertButton { typingMessage += "🔔 Alert Bell Character! \u{7}" } + } + Spacer() + RequestPositionButton(action: requestPosition) + TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes).padding(.trailing) + } + #endif + + HStack(alignment: .top) { + ZStack { + TextField("message", text: $typingMessage, axis: .vertical) + .onChange(of: typingMessage, perform: { value in + totalBytes = value.utf8.count + // Only mess with the value if it is too big + if totalBytes > Self.maxbytes { + let firstNBytes = Data(typingMessage.utf8.prefix(Self.maxbytes)) + if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { + // Set the message back to the last place where it was the right size + typingMessage = maxBytesString + } else { + print("not a valid UTF-8 sequence") + } + } + }) + .keyboardType(.default) + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Button("dismiss.keyboard") { + isFocused = false + } + .font(.subheadline) + + if destination.showAlertButton { + Spacer() + AlertButton { typingMessage += "🔔 Alert Bell Character! \u{7}" } + } + + Spacer() + RequestPositionButton(action: requestPosition) + TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes) + } + } + .padding(.horizontal, 8) + .focused($isFocused) + .multilineTextAlignment(.leading) + .frame(minHeight: 50) + .keyboardShortcut(.defaultAction) + .onSubmit { + #if targetEnvironment(macCatalyst) + sendMessage() + #endif + } + + Text(typingMessage) + .opacity(0) + .padding(.all, 0) + } + .overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1)) + .padding(.bottom, 15) + + Button(action: sendMessage) { + Image(systemName: "arrow.up.circle.fill") + .font(.largeTitle) + .foregroundColor(.accentColor) + } + } + .padding(.all, 15) + } + + private func requestPosition() { + let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" + sendPositionWithMessage = true + typingMessage = "📍 " + userLongName + " \(destination.positionShareMessage)." + } + + private func sendMessage() { + let messageSent = bleManager.sendMessage( + message: typingMessage, + toUserNum: destination.userNum, + channel: destination.channelNum, + isEmoji: false, + replyID: replyMessageId + ) + if messageSent { + typingMessage = "" + isFocused = false + replyMessageId = 0 + onSubmit() + if sendPositionWithMessage { + let positionSent = bleManager.sendPosition( + channel: destination.channelNum, + destNum: destination.positionDestNum, + wantResponse: destination.wantPositionResponse + ) + if positionSent { + print("Location Sent") + } + } + } + } +} + +private extension TextMessageField.Destination { + var positionShareMessage: String { + switch self { + case .user: return "has shared their position and requested a response with your position" + case .channel: return "has shared their position with you" + } + } + + var userNum: Int64 { + switch self { + case let .user(num): return num + case .channel: return 0 + } + } + + var channelNum: Int32 { + switch self { + case .user: return 0 + case let .channel(num): return num + } + } + + var positionDestNum: Int64 { + switch self { + case let .user(num): return num + case .channel: return Int64(BLEManager.emptyNodeNum) + } + } + + var showAlertButton: Bool { + switch self { + case .user: return false + case .channel: return true + } + } + + var wantPositionResponse: Bool { + switch self { + case .user: return true + case .channel: return false + } + } +} diff --git a/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift new file mode 100644 index 00000000..d7428110 --- /dev/null +++ b/Meshtastic/Views/Messages/TextMessageField/TextMessageSize.swift @@ -0,0 +1,20 @@ +import SwiftUI + +struct TextMessageSize: View { + let maxbytes: Int + let totalBytes: Int + + var body: some View { + ProgressView("\("bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) + .frame(width: 130) + .padding(5) + .font(.subheadline) + .accentColor(.accentColor) + } +} + +struct TextMessageSizePreview: PreviewProvider { + static var previews: some View { + TextMessageSize(maxbytes: 228, totalBytes: 100) + } +} diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index d0c9c0e1..0e63090d 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -9,25 +9,17 @@ import SwiftUI import CoreData struct UserMessageList: View { - @StateObject var appState = AppState.shared @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - enum Field: Hashable { - case messageText - } // Keyboard State - @State var typingMessage: String = "" - @State private var totalBytes = 0 - var maxbytes = 228 - @FocusState var focusedField: Field? + @FocusState var messageFieldFocused: Bool // View State Items @ObservedObject var user: UserEntity @State var showDeleteMessageAlert = false @State private var deleteMessageId: Int64 = 0 @State private var replyMessageId: Int64 = 0 - @State private var sendPositionWithMessage: Bool = false var body: some View { VStack { @@ -88,7 +80,7 @@ struct UserMessageList: View { } Button(action: { self.replyMessageId = message.messageId - self.focusedField = .messageText + self.messageFieldFocused = true print("I want to reply to \(message.messageId)") }) { Text("reply") @@ -265,107 +257,14 @@ struct UserMessageList: View { } }) } - #if targetEnvironment(macCatalyst) - HStack { - Spacer() - Button { - let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" - sendPositionWithMessage = true - typingMessage = "📍 " + userLongName + " has shared their position and requested a response with your position." - } label: { - Text("share.position") - Image(systemName: "mappin.and.ellipse") - .symbolRenderingMode(.hierarchical) - .imageScale(.large).foregroundColor(.accentColor) - } - ProgressView("\("bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) - .frame(width: 130) - .padding(5) - .font(.subheadline) - .accentColor(.accentColor) - .padding(.trailing) - } - #endif - HStack(alignment: .top) { - ZStack { - TextField("message", text: $typingMessage, axis: .vertical) - .onChange(of: typingMessage, perform: { value in - totalBytes = value.utf8.count - // Only mess with the value if it is too big - if totalBytes > maxbytes { - let firstNBytes = Data(typingMessage.utf8.prefix(maxbytes)) - if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { - // Set the message back to the last place where it was the right size - typingMessage = maxBytesString - } else { - print("not a valid UTF-8 sequence") - } - } - }) - .keyboardType(.default) - .toolbar { - ToolbarItemGroup(placement: .keyboard) { - Button("dismiss.keyboard") { - focusedField = nil - } - .font(.subheadline) - Spacer() - Button { - let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" - sendPositionWithMessage = true - typingMessage = "📍 " + userLongName + " has shared their position and requested a response with your position." - } label: { - Image(systemName: "mappin.and.ellipse") - .symbolRenderingMode(.hierarchical) - .imageScale(.large).foregroundColor(.accentColor) - } - ProgressView("\("bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) - .frame(width: 130) - .padding(5) - .font(.subheadline) - .accentColor(.accentColor) - } - } - .padding(.horizontal, 8) - .focused($focusedField, equals: .messageText) - .multilineTextAlignment(.leading) - .frame(minHeight: 50) - .keyboardShortcut(.defaultAction) - .onSubmit { - #if targetEnvironment(macCatalyst) - if bleManager.sendMessage(message: typingMessage, toUserNum: user.num, channel: 0, isEmoji: false, replyID: replyMessageId) { - typingMessage = "" - focusedField = nil - replyMessageId = 0 - if sendPositionWithMessage { - if bleManager.sendPosition(channel: 0, destNum: user.num, wantResponse: true) { - print("Location Sent") - } - } - } - #endif - } - Text(typingMessage).opacity(0).padding(.all, 0) - } - .overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1)) - .padding(.bottom, 15) - Button(action: { - if bleManager.sendMessage(message: typingMessage, toUserNum: user.num, channel: 0, isEmoji: false, replyID: replyMessageId) { - typingMessage = "" - focusedField = nil - replyMessageId = 0 - if sendPositionWithMessage { - if bleManager.sendPosition(channel: 0, destNum: user.num, wantResponse: true) { - print("Location Sent") - } - } - } - }) { - Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.accentColor) - } + TextMessageField( + destination: .user(user.num), + replyMessageId: $replyMessageId, + isFocused: $messageFieldFocused + ) { + context.refresh(user, mergeChanges: true) } - .padding(.all, 15) } .navigationBarTitleDisplayMode(.inline) .toolbar { From ae66e5af596f9b94d86c24fd3257f0eda87dacee Mon Sep 17 00:00:00 2001 From: Austin Payne Date: Sat, 3 Feb 2024 20:58:23 -0700 Subject: [PATCH 10/22] fix: map tiles being downloaded remotely twice The url returned by MKTileOverlay.url(forTilePath:) is subsequently used by MKTileOverlay.loadTile(at:result:) for download. In the case of a tile that was just cached by OfflineTileManager.persistLocally(path:) we now return the local file URL to avoid downloading the remote image twice. --- Meshtastic/Helpers/Map/OfflineTileManager.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/Map/OfflineTileManager.swift b/Meshtastic/Helpers/Map/OfflineTileManager.swift index 235ef0b7..ebf235ca 100644 --- a/Meshtastic/Helpers/Map/OfflineTileManager.swift +++ b/Meshtastic/Helpers/Map/OfflineTileManager.swift @@ -148,8 +148,9 @@ class OfflineTileManager: ObservableObject { try data.write(to: filename) } catch { print("💀 Save Tile Error = \(error)") + return url } - return url + return filename } private func filterTilesAlreadyExisting(paths: [MKTileOverlayPath]) -> [MKTileOverlayPath] { paths.filter { From 58fa4e26f18f13fd5e294340571158ced50b9030 Mon Sep 17 00:00:00 2001 From: Austin Payne Date: Sat, 3 Feb 2024 20:50:11 -0700 Subject: [PATCH 11/22] fix: don't fetch tiles greater than specified upper bound --- Meshtastic/Helpers/Map/OfflineTileManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/Map/OfflineTileManager.swift b/Meshtastic/Helpers/Map/OfflineTileManager.swift index 235ef0b7..b8ce4b70 100644 --- a/Meshtastic/Helpers/Map/OfflineTileManager.swift +++ b/Meshtastic/Helpers/Map/OfflineTileManager.swift @@ -110,7 +110,7 @@ class OfflineTileManager: ObservableObject { if fileManager.fileExists(atPath: tilesUrl.path) { return tilesUrl } else { - if UserDefaults.enableOfflineMaps { // Get and persist newTile + if UserDefaults.enableOfflineMaps, UserDefaults.mapTileServer.zoomRange.contains(path.z) { // Get and persist newTile return persistLocally(path: path) } else { // Else display empty tile (transparent over Maps tiles) return Bundle.main.url(forResource: "alpha", withExtension: "png")! From 8cd79053798e8f36ef17230950d7e542128a96fd Mon Sep 17 00:00:00 2001 From: Austin Payne Date: Wed, 31 Jan 2024 23:33:50 -0700 Subject: [PATCH 12/22] fix: inverted map options sheet resizing --- 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 96697ddf..d5ce3676 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -223,7 +223,7 @@ struct NodeMap: View { .padding(.bottom) #endif } - .presentationDetents([UserDefaults.enableOfflineMaps || UserDefaults.enableOverlayServer ? .large : .medium]) + .presentationDetents([enableOfflineMaps || enableOverlayServer ? .large : .medium]) .presentationDragIndicator(.visible) } } From c23e18316d0774c5f880b47aa50f5d3789e49338 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 12 Feb 2024 16:35:29 -0800 Subject: [PATCH 13/22] Store and Forward updates --- Meshtastic.xcodeproj/project.pbxproj | 8 +- Meshtastic/Helpers/BLEManager.swift | 10 +- Meshtastic/Helpers/MeshPackets.swift | 4 + .../contents | 4 +- .../Protobufs/meshtastic/config.pb.swift | 49 ++++++---- Meshtastic/Protobufs/meshtastic/mesh.pb.swift | 20 +++- .../meshtastic/storeforward.pb.swift | 16 ++++ Meshtastic/Views/Nodes/NodeList.swift | 2 +- .../Views/Settings/Config/LoRaConfig.swift | 3 +- ...Forward.swift => StoreForwardConfig.swift} | 95 +++++++++++++------ en.lproj/Localizable.strings | 18 ++-- 11 files changed, 156 insertions(+), 73 deletions(-) rename Meshtastic/Views/Settings/Config/Module/{StoreForward.swift => StoreForwardConfig.swift} (75%) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 6b056a19..dd2a0557 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -177,7 +177,7 @@ DDE5B4042B2279A700FCDD05 /* TraceRouteLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE5B4032B2279A700FCDD05 /* TraceRouteLog.swift */; }; DDE5B4062B227E3200FCDD05 /* TraceRouteEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */; }; DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */; }; - DDF6B2482A9AEBF500BA6931 /* StoreForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */; }; + DDF6B2482A9AEBF500BA6931 /* StoreForwardConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForwardConfig.swift */; }; DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; }; DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */; }; DDFFA7472B3A7F3C004730DB /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFFA7462B3A7F3C004730DB /* Bundle.swift */; }; @@ -422,7 +422,7 @@ DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteRecorder.swift; sourceTree = ""; }; DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = ""; }; DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV17.xcdatamodel; sourceTree = ""; }; - DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreForward.swift; sourceTree = ""; }; + DDF6B2472A9AEBF500BA6931 /* StoreForwardConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreForwardConfig.swift; sourceTree = ""; }; DDF6B24B2A9C2FC800BA6931 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; DDF924C926FBB953009FE055 /* ConnectedDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = ""; }; DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentConditionsCompact.swift; sourceTree = ""; }; @@ -625,7 +625,7 @@ DD41582928585C32009B0E59 /* RangeTestConfig.swift */, DDC94FCD29CF55310082EA6E /* RtttlConfig.swift */, DD6193782863875F00E59241 /* SerialConfig.swift */, - DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */, + DDF6B2472A9AEBF500BA6931 /* StoreForwardConfig.swift */, DD415827285859C4009B0E59 /* TelemetryConfig.swift */, ); path = Module; @@ -1275,7 +1275,7 @@ DD5E5210298EE33B00D21B61 /* telemetry.pb.swift in Sources */, DD77093F2AA1B146007A8BF0 /* UIColor.swift in Sources */, DD5E5205298EE33B00D21B61 /* mesh.pb.swift in Sources */, - DDF6B2482A9AEBF500BA6931 /* StoreForward.swift in Sources */, + DDF6B2482A9AEBF500BA6931 /* StoreForwardConfig.swift in Sources */, DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */, DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */, DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */, diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index dad6249a..bad07c7e 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -2375,7 +2375,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var sfPacket = StoreAndForward() sfPacket.rr = StoreAndForward.RequestResponse.clientHistory sfPacket.history.window = UInt32(toUser.userNode?.storeForwardConfig?.historyReturnWindow ?? 120) - sfPacket.history.lastRequest = UInt32(toUser.userNode?.storeForwardConfig?.lastRequest?.timeIntervalSince1970 ?? Date().timeIntervalSince1970) + sfPacket.history.lastRequest = UInt32(toUser.userNode?.storeForwardConfig?.lastRequest ?? 0) var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2456,10 +2456,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return } if routerNode.storeForwardConfig != nil { - routerNode.storeForwardConfig?.lastRequest = Date(timeIntervalSince1970: TimeInterval(storeAndForwardMessage.history.lastRequest)) + routerNode.storeForwardConfig?.lastRequest = Int32(storeAndForwardMessage.history.lastRequest) } else { let newConfig = StoreForwardConfigEntity(context: context) - newConfig.lastRequest = Date(timeIntervalSince1970: TimeInterval(storeAndForwardMessage.history.lastRequest)) + newConfig.lastRequest = Int32(storeAndForwardMessage.history.lastRequest) routerNode.storeForwardConfig = newConfig } @@ -2487,6 +2487,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate case .UNRECOGNIZED: textMessageAppPacket(packet: packet, connectedNode: connectedNodeNum, context: context) MeshLogger.log("📮 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") + case .routerTextDirect: + MeshLogger.log("💬 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") + case .routerTextBroadcast: + MeshLogger.log("✉️ Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") } } } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index d2e6a8f1..a2e93d38 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -723,6 +723,10 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) { + //let storeForwardMessage = packet.decoded.payload.text/// String(bytes: packet.decoded.payload.text, encoding: .utf8) + + + if let messageText = String(bytes: packet.decoded.payload, encoding: .utf8) { MeshLogger.log("💬 \("mesh.log.textmessage.received".localized)") diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 26.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 26.xcdatamodel/contents index 9dec53eb..30ed9b22 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 26.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 26.xcdatamodel/contents @@ -325,7 +325,7 @@ - + @@ -412,4 +412,4 @@ - + \ No newline at end of file diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index 38991510..a31b3f57 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -198,64 +198,63 @@ struct Config { typealias RawValue = Int /// - /// Client device role + /// Description: App connected or stand alone messaging device. + /// Technical Details: Default Role case client // = 0 /// - /// Client Mute device role - /// Same as a client except packets will not hop over this node, does not contribute to routing packets for mesh. + /// Description: Device that does not forward packets from other devices. case clientMute // = 1 /// - /// Router device role. - /// Mesh packets will prefer to be routed over this node. This node will not be used by client apps. - /// The wifi/ble radios and the oled screen will be put to sleep. + /// Description: Infrastructure node for extending network coverage by relaying messages. Visible in Nodes list. + /// Technical Details: Mesh packets will prefer to be routed over this node. This node will not be used by client apps. + /// The wifi radio and the oled screen will be put to sleep. /// This mode may still potentially have higher power usage due to it's preference in message rebroadcasting on the mesh. case router // = 2 /// - /// Router Client device role - /// Mesh packets will prefer to be routed over this node. The Router Client can be used as both a Router and an app connected Client. + /// Description: Combination of both ROUTER and CLIENT. Not for mobile devices. case routerClient // = 3 /// - /// Repeater device role - /// Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry + /// Description: Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list. + /// Technical Details: Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry /// or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate. case repeater // = 4 /// - /// Tracker device role - /// Position Mesh packets will be prioritized higher and sent more frequently by default. + /// Description: Broadcasts GPS position packets as priority. + /// Technical Details: Position Mesh packets will be prioritized higher and sent more frequently by default. /// When used in conjunction with power.is_power_saving = true, nodes will wake up, /// send position, and then sleep for position.position_broadcast_secs seconds. case tracker // = 5 /// - /// Sensor device role - /// Telemetry Mesh packets will be prioritized higher and sent more frequently by default. + /// Description: Broadcasts telemetry packets as priority. + /// Technical Details: Telemetry Mesh packets will be prioritized higher and sent more frequently by default. /// When used in conjunction with power.is_power_saving = true, nodes will wake up, /// send environment telemetry, and then sleep for telemetry.environment_update_interval seconds. case sensor // = 6 /// - /// TAK device role - /// Used for nodes dedicated for connection to an ATAK EUD. + /// Description: Optimized for ATAK system communication, reduces routine broadcasts. + /// Technical Details: 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 case tak // = 7 /// - /// Client Hidden device role - /// Used for nodes that "only speak when spoken to" + /// Description: Device that only broadcasts as needed for stealth or power savings. + /// Technical Details: Used for nodes that "only speak when spoken to" /// Turns all of the routine broadcasts but allows for ad-hoc communication /// Still rebroadcasts, but with local only rebroadcast mode (known meshes only) /// Can be used for clandestine operation or to dramatically reduce airtime / power consumption case clientHidden // = 8 /// - /// Lost and Found device role - /// Used to automatically send a text message to the mesh + /// Description: Broadcasts location as message to default channel regularly for to assist with device recovery. + /// Technical Details: Used to automatically send a text message to the mesh /// with the current position of the device on a frequent interval: /// "I'm lost! Position: lat / long" case lostAndFound // = 9 @@ -419,6 +418,10 @@ struct Config { /// Set where GPS is enabled, disabled, or not present var gpsMode: Config.PositionConfig.GpsMode = .disabled + /// + /// Set GPS precision in bits per channel, or 0 for disabled + var channelPrecision: [UInt32] = [] + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -1838,6 +1841,7 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm 11: .standard(proto: "broadcast_smart_minimum_interval_secs"), 12: .standard(proto: "gps_en_gpio"), 13: .standard(proto: "gps_mode"), + 14: .standard(proto: "channel_precision"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1859,6 +1863,7 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm 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) }() + case 14: try { try decoder.decodeRepeatedUInt32Field(value: &self.channelPrecision) }() default: break } } @@ -1904,6 +1909,9 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if self.gpsMode != .disabled { try visitor.visitSingularEnumField(value: self.gpsMode, fieldNumber: 13) } + if !self.channelPrecision.isEmpty { + try visitor.visitPackedUInt32Field(value: self.channelPrecision, fieldNumber: 14) + } try unknownFields.traverse(visitor: &visitor) } @@ -1921,6 +1929,7 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm if lhs.broadcastSmartMinimumIntervalSecs != rhs.broadcastSmartMinimumIntervalSecs {return false} if lhs.gpsEnGpio != rhs.gpsEnGpio {return false} if lhs.gpsMode != rhs.gpsMode {return false} + if lhs.channelPrecision != rhs.channelPrecision {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index 1380fba9..080d9bfb 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -202,7 +202,8 @@ enum HardwareModel: SwiftProtobuf.Enum { /// /// Heltec Wireless Tracker with ESP32-S3 CPU, built-in GPS, and TFT - case heltecWirelessTracker // = 48 + /// Newer V1.1, version is written on the PCB near the display. + case heltecWirelessTrackerV11 // = 48 /// /// Heltec Wireless Paper with ESP32-S3 CPU and E-Ink display @@ -246,6 +247,11 @@ enum HardwareModel: SwiftProtobuf.Enum { /// Flex connector marking is FPC-7528B case heltecWirelessPaperV10 // = 57 + /// + /// Heltec Wireless Tracker with ESP32-S3 CPU, built-in GPS, and TFT + /// Older "V1.0" Variant + case heltecWirelessTrackerV10 // = 58 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// 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. @@ -301,7 +307,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case 45: self = .betafpv2400Tx case 46: self = .betafpv900NanoTx case 47: self = .rpiPico - case 48: self = .heltecWirelessTracker + case 48: self = .heltecWirelessTrackerV11 case 49: self = .heltecWirelessPaper case 50: self = .tDeck case 51: self = .tWatchS3 @@ -311,6 +317,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case 55: self = .esp32S3Pico case 56: self = .chatter2 case 57: self = .heltecWirelessPaperV10 + case 58: self = .heltecWirelessTrackerV10 case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -360,7 +367,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case .betafpv2400Tx: return 45 case .betafpv900NanoTx: return 46 case .rpiPico: return 47 - case .heltecWirelessTracker: return 48 + case .heltecWirelessTrackerV11: return 48 case .heltecWirelessPaper: return 49 case .tDeck: return 50 case .tWatchS3: return 51 @@ -370,6 +377,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case .esp32S3Pico: return 55 case .chatter2: return 56 case .heltecWirelessPaperV10: return 57 + case .heltecWirelessTrackerV10: return 58 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -424,7 +432,7 @@ extension HardwareModel: CaseIterable { .betafpv2400Tx, .betafpv900NanoTx, .rpiPico, - .heltecWirelessTracker, + .heltecWirelessTrackerV11, .heltecWirelessPaper, .tDeck, .tWatchS3, @@ -434,6 +442,7 @@ extension HardwareModel: CaseIterable { .esp32S3Pico, .chatter2, .heltecWirelessPaperV10, + .heltecWirelessTrackerV10, .privateHw, ] } @@ -2613,7 +2622,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 45: .same(proto: "BETAFPV_2400_TX"), 46: .same(proto: "BETAFPV_900_NANO_TX"), 47: .same(proto: "RPI_PICO"), - 48: .same(proto: "HELTEC_WIRELESS_TRACKER"), + 48: .same(proto: "HELTEC_WIRELESS_TRACKER_V1_1"), 49: .same(proto: "HELTEC_WIRELESS_PAPER"), 50: .same(proto: "T_DECK"), 51: .same(proto: "T_WATCH_S3"), @@ -2623,6 +2632,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 55: .same(proto: "ESP32_S3_PICO"), 56: .same(proto: "CHATTER_2"), 57: .same(proto: "HELTEC_WIRELESS_PAPER_V1_0"), + 58: .same(proto: "HELTEC_WIRELESS_TRACKER_V1_0"), 255: .same(proto: "PRIVATE_HW"), ] } diff --git a/Meshtastic/Protobufs/meshtastic/storeforward.pb.swift b/Meshtastic/Protobufs/meshtastic/storeforward.pb.swift index 891e8ef0..a8fa5f90 100644 --- a/Meshtastic/Protobufs/meshtastic/storeforward.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/storeforward.pb.swift @@ -160,6 +160,14 @@ struct StoreAndForward { /// Router is responding to a request for stats. case routerStats // = 7 + /// + /// Router sends a text message from its history that was a direct message. + case routerTextDirect // = 8 + + /// + /// Router sends a text message from its history that was a broadcast. + case routerTextBroadcast // = 9 + /// /// Client is an in error state. case clientError // = 64 @@ -200,6 +208,8 @@ struct StoreAndForward { case 5: self = .routerBusy case 6: self = .routerHistory case 7: self = .routerStats + case 8: self = .routerTextDirect + case 9: self = .routerTextBroadcast case 64: self = .clientError case 65: self = .clientHistory case 66: self = .clientStats @@ -220,6 +230,8 @@ struct StoreAndForward { case .routerBusy: return 5 case .routerHistory: return 6 case .routerStats: return 7 + case .routerTextDirect: return 8 + case .routerTextBroadcast: return 9 case .clientError: return 64 case .clientHistory: return 65 case .clientStats: return 66 @@ -341,6 +353,8 @@ extension StoreAndForward.RequestResponse: CaseIterable { .routerBusy, .routerHistory, .routerStats, + .routerTextDirect, + .routerTextBroadcast, .clientError, .clientHistory, .clientStats, @@ -482,6 +496,8 @@ extension StoreAndForward.RequestResponse: SwiftProtobuf._ProtoNameProviding { 5: .same(proto: "ROUTER_BUSY"), 6: .same(proto: "ROUTER_HISTORY"), 7: .same(proto: "ROUTER_STATS"), + 8: .same(proto: "ROUTER_TEXT_DIRECT"), + 9: .same(proto: "ROUTER_TEXT_BROADCAST"), 64: .same(proto: "CLIENT_ERROR"), 65: .same(proto: "CLIENT_HISTORY"), 66: .same(proto: "CLIENT_STATS"), diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 18b923ad..6552eb9a 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -85,7 +85,7 @@ struct NodeList: View { } label: { Label("Trace Route", systemImage: "signpost.right.and.left") } - if true {//node?.storeForwardConfig != nil && node.storeForwardConfig?.isRouter ?? false { + if node.isStoreForwardRouter { Button { let success = bleManager.requestStoreAndForwardClientHistory(fromUser: connectedNode!.user!, toUser: node.user!) diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index d19d41f2..767aa51d 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -161,7 +161,7 @@ struct LoRaConfig: View { .font(.caption) HStack { - Text("LoRa Frequency Slot") + Text("Frequency Slot") .fixedSize() TextField("Frequency Slot", value: $channelNum, formatter: formatter) .toolbar { @@ -175,6 +175,7 @@ struct LoRaConfig: View { .keyboardType(.decimalPad) .scrollDismissesKeyboard(.immediately) .focused($focusedField, equals: .channelNum) + .disabled(overrideFrequency > 0.0) } 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) diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForward.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift similarity index 75% rename from Meshtastic/Views/Settings/Config/Module/StoreForward.swift rename to Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index 483ff4cc..9fd6302a 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForward.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -17,6 +17,8 @@ struct StoreForwardConfig: View { @State var hasChanges: Bool = false /// Enable the Store and Forward Module @State var enabled = false + /// Is a S&F Router + @State var isRouter = false /// Send a Heartbeat @State var heartbeat: Bool = false /// Number of Records @@ -56,38 +58,61 @@ struct StoreForwardConfig: View { .foregroundColor(.orange) } Section(header: Text("options")) { + Toggle(isOn: $enabled) { Label("enabled", systemImage: "envelope.arrow.triangle.branch") + Text("Enables the store and forward module.") + .font(.caption) } - Toggle(isOn: $heartbeat) { - Label("storeforward.heartbeat", systemImage: "waveform.path.ecg") + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + if enabled { + HStack { + Picker(selection: $isRouter, label: Text("Role")) { + Text("Client") + .tag(false) + Text("Router") + .tag(true) + } + .pickerStyle(SegmentedPickerStyle()) + .padding(.top, 5) + .padding(.bottom, 5) + } } - Picker("Number of records", selection: $records) { - Text("unset").tag(0) - Text("25").tag(25) - Text("50").tag(50) - Text("75").tag(75) - Text("100").tag(100) + } + + if isRouter { + Section(header: Text("options")) { + Toggle(isOn: $heartbeat) { + Label("storeforward.heartbeat", systemImage: "waveform.path.ecg") + } + Picker("Number of records", selection: $records) { + Text("unset").tag(0) + Text("25").tag(25) + Text("50").tag(50) + Text("75").tag(75) + Text("100").tag(100) + } + .pickerStyle(DefaultPickerStyle()) + Picker("History Return Max", selection: $historyReturnMax ) { + Text("unset").tag(0) + Text("25").tag(25) + Text("50").tag(50) + Text("75").tag(75) + Text("100").tag(100) + } + .pickerStyle(DefaultPickerStyle()) + Picker("History Return Window", selection: $historyReturnWindow ) { + Text("unset").tag(0) + Text("One Minute").tag(60) + Text("Five Minutes").tag(300) + Text("Ten Minutes").tag(600) + Text("Fifteen Minutes").tag(900) + Text("Thirty Minutes").tag(1800) + Text("One Hour").tag(3600) + } + .pickerStyle(DefaultPickerStyle()) } - .pickerStyle(DefaultPickerStyle()) - Picker("History Return Max", selection: $historyReturnMax ) { - Text("unset").tag(0) - Text("25").tag(25) - Text("50").tag(50) - Text("75").tag(75) - Text("100").tag(100) - } - .pickerStyle(DefaultPickerStyle()) - Picker("History Return Window", selection: $historyReturnWindow ) { - Text("unset").tag(0) - Text("One Minute").tag(60) - Text("Five Minutes").tag(300) - Text("Ten Minutes").tag(600) - Text("Fifteen Minutes").tag(900) - Text("Thirty Minutes").tag(1800) - Text("One Hour").tag(3600) - } - .pickerStyle(DefaultPickerStyle()) } } .scrollDismissesKeyboard(.interactively) @@ -113,6 +138,18 @@ struct StoreForwardConfig: View { let nodeName = node?.user?.longName ?? "unknown".localized let buttonText = String.localizedStringWithFormat("save.config %@".localized, nodeName) Button(buttonText) { + + /// Let the user set isRouter for the connected node, for nodes on the mesh set isRouter based + /// on receipt of a primary heartbeat + if connectedNode?.num ?? 0 == node?.num ?? -1 { + connectedNode?.storeForwardConfig?.isRouter = isRouter + do { + try context.save() + } catch { + print("Failed to save isRouter") + } + } + var sfc = ModuleConfig.StoreForwardConfig() sfc.enabled = self.enabled sfc.heartbeat = self.heartbeat @@ -125,7 +162,8 @@ struct StoreForwardConfig: View { // for now just disable the button after a successful save hasChanges = false goBack() - } } + } + } } } message: { @@ -178,6 +216,7 @@ struct StoreForwardConfig: View { } func setStoreAndForwardValues() { self.enabled = (node?.storeForwardConfig?.enabled ?? false) + self.isRouter = (node?.storeForwardConfig?.isRouter ?? false) self.heartbeat = (node?.storeForwardConfig?.heartbeat ?? true) self.records = Int(node?.storeForwardConfig?.records ?? 50) self.historyReturnMax = Int(node?.storeForwardConfig?.historyReturnMax ?? 100) diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 028c3069..707334a0 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -69,16 +69,16 @@ "device.config"="Device Config"; "device.metrics.delete"="Delete all device metrics?"; "device.metrics.log"="Device Metrics Log"; -"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.client"="App connected or stand alone messaging device."; +"device.role.clientmute"="Device that does not forward packets from other devices."; +"device.role.clienthidden"="Device that only broadcasts as needed for stealth or power savings."; +"device.role.tracker"="Broadcasts GPS position packets as priority."; +"device.role.lostandfound"="Broadcasts location as message to default channel regularly for to assist with device recovery."; +"device.role.sensor"="Broadcasts telemetry packets as priority."; "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."; +"device.role.repeater"="Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list."; +"device.role.router"="Infrastructure node for extending network coverage by relaying messages. Visible in Nodes list."; +"device.role.routerclient"="Combination of both ROUTER and CLIENT. Not for mobile devices."; "direct.messages"="Direct Messages"; "dismiss.keyboard"="Dismiss"; "display"="Display (Device Screen)"; From 1108f8d3622a485674423d3b7a27347e3424acdc Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 12 Feb 2024 18:00:04 -0800 Subject: [PATCH 14/22] Additional store and forward cleanup --- Meshtastic/Helpers/BLEManager.swift | 2 ++ Meshtastic/Views/Nodes/NodeList.swift | 4 +-- .../Config/Module/StoreForwardConfig.swift | 26 +++++++++++++++---- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index bad07c7e..3b3584a2 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -2489,8 +2489,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate MeshLogger.log("📮 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") case .routerTextDirect: MeshLogger.log("💬 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") + textMessageAppPacket(packet: packet, connectedNode: connectedNodeNum, context: context) case .routerTextBroadcast: MeshLogger.log("✉️ Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") + textMessageAppPacket(packet: packet, connectedNode: connectedNodeNum, context: context) } } } diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 6552eb9a..48bfeb80 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -116,12 +116,12 @@ struct NodeList: View { Text("This could take a while, response will appear in the trace route log for the node it was sent to.") } .alert( - "Store and Forward Client Hitory Request Sent", + "Client History Request Sent", isPresented: $isPresentingClientHistorySentAlert ) { Button("OK", role: .cancel) { } } message: { - Text("Any messages you have missed will be delivered again.") + Text("Any missed messages will be delivered again.") } } .searchable(text: nodesQuery, prompt: "Find a node") diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index 9fd6302a..980b02a2 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -61,7 +61,7 @@ struct StoreForwardConfig: View { Toggle(isOn: $enabled) { Label("enabled", systemImage: "envelope.arrow.triangle.branch") - Text("Enables the store and forward module.") + Text("Enables the store and forward module. Store and forward must be enabled on both client and router devices.") .font(.caption) } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) @@ -79,6 +79,15 @@ struct StoreForwardConfig: View { .padding(.bottom, 5) } } + VStack { + if isRouter { + Text("Store and forward router devices must also be in the router or router client device role and requires a ESP32 device with PSRAM.") + .font(.caption) + } else { + Text("Store and forward clients can request history from routers on the network.") + .font(.caption) + } + } } if isRouter { @@ -110,6 +119,7 @@ struct StoreForwardConfig: View { Text("Fifteen Minutes").tag(900) Text("Thirty Minutes").tag(1800) Text("One Hour").tag(3600) + Text("Two Hours").tag(7200) } .pickerStyle(DefaultPickerStyle()) } @@ -178,7 +188,7 @@ struct StoreForwardConfig: View { if self.bleManager.context == nil { self.bleManager.context = context } - setStoreAndForwardValues() + // Need to request a Detection Sensor Module Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.storeForwardConfig == nil { print("empty store and forward module config") @@ -187,10 +197,16 @@ struct StoreForwardConfig: View { _ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) } } + setStoreAndForwardValues() } .onChange(of: enabled) { newEnabled in - if node != nil && node?.detectionSensorConfig != nil { - if newEnabled != node!.detectionSensorConfig!.enabled { hasChanges = true } + if node != nil && node?.storeForwardConfig != nil { + if newEnabled != node!.storeForwardConfig!.enabled { hasChanges = true } + } + } + .onChange(of: isRouter) { newIsRouter in + if node != nil && node?.storeForwardConfig != nil { + if newIsRouter != node!.storeForwardConfig!.isRouter { hasChanges = true } } } .onChange(of: heartbeat) { newHeartbeat in @@ -220,7 +236,7 @@ struct StoreForwardConfig: View { self.heartbeat = (node?.storeForwardConfig?.heartbeat ?? true) self.records = Int(node?.storeForwardConfig?.records ?? 50) self.historyReturnMax = Int(node?.storeForwardConfig?.historyReturnMax ?? 100) - self.historyReturnWindow = Int(node?.storeForwardConfig?.historyReturnWindow ?? 300) + self.historyReturnWindow = Int(node?.storeForwardConfig?.historyReturnWindow ?? 7200) self.hasChanges = false } } From 48ef2e656cf3a9606c0fdf6ba85364a2cb9aa37a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 12 Feb 2024 22:09:22 -0800 Subject: [PATCH 15/22] Add node info broadcast interval --- Meshtastic/Persistence/Persistence.swift | 5 +---- Meshtastic/Views/Bluetooth/Connect.swift | 7 ++++++- .../Views/Settings/Config/DeviceConfig.swift | 18 +++++++++++++++++- .../Views/Settings/Config/PositionConfig.swift | 4 +++- 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/Meshtastic/Persistence/Persistence.swift b/Meshtastic/Persistence/Persistence.swift index 9165c433..997fdbea 100644 --- a/Meshtastic/Persistence/Persistence.swift +++ b/Meshtastic/Persistence/Persistence.swift @@ -57,17 +57,14 @@ class PersistenceController { guard let url = self.container.persistentStoreDescriptions.first?.url else { return } let persistentStoreCoordinator = self.container.persistentStoreCoordinator - do { - try persistentStoreCoordinator.destroyPersistentStore(at: url, ofType: NSSQLiteStoreType, options: nil) print("💥 CoreData database truncated. All app data has been erased.") - + do { try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil) } catch let error { print("💣 Failed to re-create CoreData database: " + error.localizedDescription) - try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil) } } catch let error { diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index 56fc496a..f0235049 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -216,7 +216,12 @@ struct Connect: View { if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == CBPeripheralState.connected { bleManager.disconnectPeripheral() } - PersistenceController.shared.clearDatabase() + do { + PersistenceController.shared.clearDatabase() + } catch let error { + print("💣 Failed to re-create CoreData database: " + error.localizedDescription) + } + let radio = bleManager.peripherals.first(where: { $0.peripheral.identifier.uuidString == selectedPeripherialId }) if radio != nil { bleManager.connectTo(peripheral: radio!.peripheral) diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 5572c330..4d4c227c 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -25,6 +25,7 @@ struct DeviceConfig: View { @State var serialEnabled = true @State var debugLogEnabled = false @State var rebroadcastMode = 0 + @State var nodeInfoBroadcastSecs = 900 @State var doubleTapAsButtonPress = false @State var isManaged = false @@ -77,6 +78,15 @@ struct DeviceConfig: View { Text(RebroadcastModes(rawValue: rebroadcastMode)?.description ?? "") .foregroundColor(.gray) .font(.caption) + Picker("Node Info Broadcast Interval", selection: $nodeInfoBroadcastSecs ) { + ForEach(UpdateIntervals.allCases) { ui in + if ui.rawValue >= 3600 { + Text(ui.description) + } + } + } + .pickerStyle(DefaultPickerStyle()) + .padding(.top, 10) Toggle(isOn: $doubleTapAsButtonPress) { Label("Double Tap as Button", systemImage: "hand.tap") } @@ -206,8 +216,8 @@ struct DeviceConfig: View { dc.debugLogEnabled = debugLogEnabled dc.buttonGpio = UInt32(buttonGPIO) dc.buzzerGpio = UInt32(buzzerGPIO) - //dc.gpsEnGpio = UInt32(gpsEnGPIO) dc.rebroadcastMode = RebroadcastModes(rawValue: rebroadcastMode)?.protoEnumValue() ?? RebroadcastModes.all.protoEnumValue() + dc.nodeInfoBroadcastSecs = UInt32(nodeInfoBroadcastSecs) dc.doubleTapAsButtonPress = doubleTapAsButtonPress dc.isManaged = isManaged let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) @@ -275,6 +285,11 @@ struct DeviceConfig: View { if newRebroadcastMode != node!.deviceConfig!.rebroadcastMode { hasChanges = true } } } + .onChange(of: nodeInfoBroadcastSecs) { newNodeInfoBroadcastSecs in + if node != nil && node?.deviceConfig != nil { + if newNodeInfoBroadcastSecs != node!.deviceConfig!.nodeInfoBroadcastSecs { hasChanges = true } + } + } .onChange(of: doubleTapAsButtonPress) { newDoubleTapAsButtonPress in if node != nil && node?.deviceConfig != nil { if newDoubleTapAsButtonPress != node!.deviceConfig!.doubleTapAsButtonPress { hasChanges = true } @@ -293,6 +308,7 @@ struct DeviceConfig: View { self.buttonGPIO = Int(node?.deviceConfig?.buttonGpio ?? 0) self.buzzerGPIO = Int(node?.deviceConfig?.buzzerGpio ?? 0) self.rebroadcastMode = Int(node?.deviceConfig?.rebroadcastMode ?? 0) + self.nodeInfoBroadcastSecs = Int(node?.deviceConfig?.nodeInfoBroadcastSecs ?? 900) self.doubleTapAsButtonPress = node?.deviceConfig?.doubleTapAsButtonPress ?? false self.isManaged = node?.deviceConfig?.isManaged ?? false self.hasChanges = false diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index dfbfb9c2..674f994d 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -108,7 +108,9 @@ struct PositionConfig: View { Picker("Position Broadcast Interval", selection: $positionBroadcastSeconds) { ForEach(UpdateIntervals.allCases) { at in - Text(at.description) + if at.rawValue >= 900 { + Text(at.description) + } } } .pickerStyle(DefaultPickerStyle()) From 3f4f493d699a4ffe3838644601c9d0dbb681b897 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 12 Feb 2024 22:31:03 -0800 Subject: [PATCH 16/22] Try and reduce device switching crashing a bit --- Meshtastic/Views/Bluetooth/Connect.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index f0235049..73f36095 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -216,8 +216,10 @@ struct Connect: View { if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == CBPeripheralState.connected { bleManager.disconnectPeripheral() } + context.reset() do { PersistenceController.shared.clearDatabase() + } catch let error { print("💣 Failed to re-create CoreData database: " + error.localizedDescription) } From 684997723949adc5e1f8ce56c21fa68e65d094c2 Mon Sep 17 00:00:00 2001 From: Austin Payne Date: Mon, 5 Feb 2024 09:28:47 -0700 Subject: [PATCH 17/22] feature: add simple retry mechanism --- Meshtastic.xcodeproj/project.pbxproj | 4 ++ .../Views/Messages/ChannelMessageList.swift | 5 ++ Meshtastic/Views/Messages/RetryButton.swift | 54 +++++++++++++++++++ .../Views/Messages/UserMessageList.swift | 5 ++ 4 files changed, 68 insertions(+) create mode 100644 Meshtastic/Views/Messages/RetryButton.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index bf68cc37..9c0fc8fc 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */; }; 6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */; }; 6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */; }; + B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B399E8A32B6F486400E4488E /* RetryButton.swift */; }; B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; }; C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */; }; C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; }; @@ -230,6 +231,7 @@ 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = ""; }; 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = ""; }; A65FA974296876BF00A97686 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = ""; }; B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = ""; }; C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMBTileOverlay.swift; sourceTree = ""; }; D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageSize.swift; sourceTree = ""; }; @@ -843,6 +845,7 @@ DDB8F40F2A9EE5B400230ECE /* Messages.swift */, DDB8F4112A9EE5DD00230ECE /* UserList.swift */, DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */, + B399E8A32B6F486400E4488E /* RetryButton.swift */, ); path = Messages; sourceTree = ""; @@ -1197,6 +1200,7 @@ DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */, DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */, DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */, + B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */, DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */, DDDB26482AACD6D1003AFCB7 /* NodeMapMapkit.swift in Sources */, DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */, diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index e9f469d6..70369d0e 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -224,6 +224,11 @@ struct ChannelMessageList: View { } .padding(.bottom) .id(channel.allPrivateMessages.firstIndex(of: message)) + + if currentUser && message.ackError > 0 { + RetryButton(message: message) + } + if !currentUser { Spacer(minLength: 50) } diff --git a/Meshtastic/Views/Messages/RetryButton.swift b/Meshtastic/Views/Messages/RetryButton.swift new file mode 100644 index 00000000..116a31c1 --- /dev/null +++ b/Meshtastic/Views/Messages/RetryButton.swift @@ -0,0 +1,54 @@ +import SwiftUI + +struct RetryButton: View { + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + + let message: MessageEntity + @State var isShowingConfirmation = false + + var body: some View { + Button { + isShowingConfirmation = true + } label: { + Image(systemName: "exclamationmark.circle") + .foregroundColor(.gray) + .frame(height: 30) + .padding(.top, 5) + } + .confirmationDialog( + "This message was likely not delivered.", + isPresented: $isShowingConfirmation, + titleVisibility: .visible + ) { + Button("Try Again") { + guard bleManager.connectedPeripheral?.peripheral.state == .connected else { + return + } + let messageID = message.messageId + let payload = message.messagePayload ?? "" + let userNum = message.toUser?.num ?? 0 + let channel = message.channel + let isEmoji = message.isEmoji + let replyID = message.replyID + context.delete(message) + do { + try context.save() + } catch { + print("Failed to delete message \(messageID)") + } + if !bleManager.sendMessage( + message: payload, + toUserNum: userNum, + channel: channel, + isEmoji: isEmoji, + replyID: replyID + ) { + // Best effort, unlikely since we already checked BLE state + print("Failed to resend message \(messageID)") + } + } + Button("Cancel", role: .cancel) {} + } + } +} diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 0e63090d..387abaf8 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -202,6 +202,11 @@ struct UserMessageList: View { } .padding(.bottom) .id(user.messageList.firstIndex(of: message)) + + if currentUser && message.ackError > 0 { + RetryButton(message: message) + } + if !currentUser { Spacer(minLength: 50) } From d4ff24cb16e3c5093b341a47163a8b82ed342cd2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 13 Feb 2024 10:58:07 -0800 Subject: [PATCH 18/22] Add some padding to the share location button --- Meshtastic/Helpers/BLEManager.swift | 5 +++-- Meshtastic/Helpers/MeshPackets.swift | 4 ---- .../Messages/TextMessageField/RequestPositionButton.swift | 1 + 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 3f31ec97..9dc704d3 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -695,7 +695,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } case .neighborinfoApp: if let neighborInfo = try? NeighborInfo(serializedData: decodedInfo.packet.decoded.payload) { - MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App App UNHANDLED \(neighborInfo)") + MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App UNHANDLED") + // MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App UNHANDLED \(neighborInfo)") } case .paxcounterApp: MeshLogger.log("🕸️ MESH PACKET received for PAX Counter App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") @@ -715,7 +716,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected }) // Config conplete returns so we don't read the characteristic again - /// MQTT Client Proxy and RangeTest interest + /// MQTT Client Proxy and RangeTest and Store and Forward interest if connectedPeripheral.num > 0 { let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index a2e93d38..d2e6a8f1 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -723,10 +723,6 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) { - //let storeForwardMessage = packet.decoded.payload.text/// String(bytes: packet.decoded.payload.text, encoding: .utf8) - - - if let messageText = String(bytes: packet.decoded.payload, encoding: .utf8) { MeshLogger.log("💬 \("mesh.log.textmessage.received".localized)") diff --git a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift index 4a69ea50..2f1634bc 100644 --- a/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift +++ b/Meshtastic/Views/Messages/TextMessageField/RequestPositionButton.swift @@ -10,6 +10,7 @@ struct RequestPositionButton: View { .imageScale(.large) .foregroundColor(.accentColor) } + .padding(.trailing) } } From caef40addcf490d6c5ee0d7f71eeeefe22a93839 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 13 Feb 2024 14:21:49 -0800 Subject: [PATCH 19/22] Handle incoming store and forward messages --- Meshtastic/Helpers/BLEManager.swift | 13 ++----------- Meshtastic/Helpers/MeshPackets.swift | 25 +++++++++++++++++++------ Meshtastic/Views/Nodes/NodeList.swift | 2 +- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 9dc704d3..d4528061 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -2403,18 +2403,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate func storeAndForwardPacket(packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) { if let storeAndForwardMessage = try? StoreAndForward(serializedData: packet.decoded.payload) { - - // Handle the text variant as a text message packet - if storeAndForwardMessage.variant == StoreAndForward.OneOf_Variant.text(packet.decoded.payload) { - MeshLogger.log("📮 Store and Forward history text message received \(storeAndForwardMessage)") - textMessageAppPacket(packet: packet, connectedNode: connectedNodeNum, context: context) - return - } // Handle each of the store and forward request / response messages switch storeAndForwardMessage.rr { case .unset: MeshLogger.log("📮 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") - textMessageAppPacket(packet: packet, connectedNode: connectedNodeNum, context: context) case .routerError: MeshLogger.log("☠️ Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") case .routerHeartbeat: @@ -2486,14 +2478,13 @@ 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)") case .routerTextDirect: MeshLogger.log("💬 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") - textMessageAppPacket(packet: packet, connectedNode: connectedNodeNum, context: context) + textMessageAppPacket(packet: packet, connectedNode: connectedNodeNum, storeForward: true, context: context) case .routerTextBroadcast: MeshLogger.log("✉️ Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)") - textMessageAppPacket(packet: packet, connectedNode: connectedNodeNum, context: context) + textMessageAppPacket(packet: packet, connectedNode: connectedNodeNum, storeForward: true, context: context) } } } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index d2e6a8f1..c25edc2f 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -721,9 +721,20 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage } } -func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) { +func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, storeForward: Bool = false, context: NSManagedObjectContext) { - if let messageText = String(bytes: packet.decoded.payload, encoding: .utf8) { + var messageText = String(bytes: packet.decoded.payload, encoding: .utf8) + var storeForwardBroadcast = false + if storeForward { + if let storeAndForwardMessage = try? StoreAndForward(serializedData: packet.decoded.payload) { + messageText = String(bytes: storeAndForwardMessage.text, encoding: .utf8) + if storeAndForwardMessage.rr == .routerTextBroadcast { + storeForwardBroadcast = true + } + } + } + + if messageText?.count ?? 0 > 0 { MeshLogger.log("💬 \("mesh.log.textmessage.received".localized)") @@ -753,13 +764,15 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM } if fetchedUsers.first(where: { $0.num == packet.to }) != nil && packet.to != 4294967295 { - newMessage.toUser = fetchedUsers.first(where: { $0.num == packet.to }) + if !storeForwardBroadcast { + newMessage.toUser = fetchedUsers.first(where: { $0.num == packet.to }) + } } if fetchedUsers.first(where: { $0.num == packet.from }) != nil { newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) } newMessage.messagePayload = messageText - newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: messageText) + newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: messageText!) if packet.to != 4294967295 && newMessage.fromUser != nil { newMessage.fromUser?.lastMessage = Date() } @@ -790,7 +803,7 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM id: ("notification.id.\(newMessage.messageId)"), title: "\(newMessage.fromUser?.longName ?? "unknown".localized)", subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", - content: messageText, + content: messageText!, target: "message", path: "meshtastic://open-dm?userid=\(newMessage.fromUser?.num ?? 0)&id=\(newMessage.messageId)" ) @@ -822,7 +835,7 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM id: ("notification.id.\(newMessage.messageId)"), title: "\(newMessage.fromUser?.longName ?? "unknown".localized)", subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", - content: messageText, + content: messageText!, target: "message", path: "meshtastic://messages/channel/\(newMessage.messageId)") ] diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 48bfeb80..500ca491 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -85,7 +85,7 @@ struct NodeList: View { } label: { Label("Trace Route", systemImage: "signpost.right.and.left") } - if node.isStoreForwardRouter { + if true {//node.isStoreForwardRouter { Button { let success = bleManager.requestStoreAndForwardClientHistory(fromUser: connectedNode!.user!, toUser: node.user!) From a2476a212f0ce0eb73763dfdc38f0e311bd39e2c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 13 Feb 2024 14:23:07 -0800 Subject: [PATCH 20/22] Only show client history to S&F routers --- Meshtastic/Views/Nodes/NodeList.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 500ca491..48bfeb80 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -85,7 +85,7 @@ struct NodeList: View { } label: { Label("Trace Route", systemImage: "signpost.right.and.left") } - if true {//node.isStoreForwardRouter { + if node.isStoreForwardRouter { Button { let success = bleManager.requestStoreAndForwardClientHistory(fromUser: connectedNode!.user!, toUser: node.user!) From bdce50cc20c3875e0a81ce21e74124466f70cf0d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 13 Feb 2024 18:47:07 -0800 Subject: [PATCH 21/22] Only show retry for ack error 5 and 3 --- Meshtastic/Views/Messages/ChannelMessageList.swift | 2 +- Meshtastic/Views/Messages/UserMessageList.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 70369d0e..8b0aa8ab 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -225,7 +225,7 @@ struct ChannelMessageList: View { .padding(.bottom) .id(channel.allPrivateMessages.firstIndex(of: message)) - if currentUser && message.ackError > 0 { + if currentUser && (message.ackError == 5 || message.ackError == 3) { RetryButton(message: message) } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 387abaf8..636a1081 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -203,7 +203,7 @@ struct UserMessageList: View { .padding(.bottom) .id(user.messageList.firstIndex(of: message)) - if currentUser && message.ackError > 0 { + if currentUser && (message.ackError == 5 || message.ackError == 3) { RetryButton(message: message) } From b16b0291283fbe30e247a6421aaee1fc17a9de3a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 13 Feb 2024 18:58:29 -0800 Subject: [PATCH 22/22] Show retry button for DM's --- Meshtastic/Views/Messages/UserMessageList.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 636a1081..a8372f38 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -203,7 +203,7 @@ struct UserMessageList: View { .padding(.bottom) .id(user.messageList.firstIndex(of: message)) - if currentUser && (message.ackError == 5 || message.ackError == 3) { + if currentUser && (message.receivedACK && !message.realACK) { RetryButton(message: message) }