From b582d0d4bf824f04f3265ea7178a2b1e3dd4281a Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Thu, 13 Mar 2025 21:45:47 -0700 Subject: [PATCH 1/4] Read all messages when one in the channel is read --- Meshtastic/Views/Messages/ChannelMessageList.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 573a7dc8..c1350ce4 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -115,6 +115,9 @@ struct ChannelMessageList: View { if !message.read { message.read = true do { + for unreadMessage in channel.allPrivateMessages.filter({ !$0.read }) { + unreadMessage.read = true + } try context.save() Logger.data.info("πŸ“– [App] Read message \(message.messageId) ") appState.unreadChannelMessages = myInfo.unreadMessages From ff81f77aa6491819795515e6b3de8f5873598e1d Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Thu, 13 Mar 2025 21:43:13 -0700 Subject: [PATCH 2/4] Read all messages in channel at once --- Meshtastic/Views/Messages/ChannelMessageList.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index c1350ce4..7753ddbb 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -113,7 +113,6 @@ struct ChannelMessageList: View { .id(message.messageId) .onAppear { if !message.read { - message.read = true do { for unreadMessage in channel.allPrivateMessages.filter({ !$0.read }) { unreadMessage.read = true From d33e7007835a86b45f50f57aceb9c66133a9ab21 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sat, 15 Mar 2025 00:02:43 -0700 Subject: [PATCH 3/4] Overhaul Store and Forward --- Localizable.xcstrings | 23 +++++++ Meshtastic/Helpers/BLEManager.swift | 6 +- Meshtastic/Helpers/MeshPackets.swift | 14 ++-- .../contents | 5 +- Meshtastic/Persistence/UpdateCoreData.swift | 1 + Meshtastic/Views/Messages/MessageText.swift | 23 +++++-- .../Config/Module/StoreForwardConfig.swift | 68 ++++++++----------- 7 files changed, 85 insertions(+), 55 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 6c2731d1..1357bbd4 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -9313,6 +9313,9 @@ } } } + }, + "Enable this device as a Store and Forward server. Requires an ESP32 device with PSRAM." : { + }, "enabled" : { "localizations" : { @@ -9409,8 +9412,12 @@ } } } + }, + "Enables the store and forward module." : { + }, "Enables the store and forward module. Store and forward must be enabled on both client and router devices." : { + "extractionState" : "stale", "localizations" : { "sr" : { "stringUnit" : { @@ -25056,6 +25063,7 @@ } }, "Router" : { + "extractionState" : "stale", "localizations" : { "sr" : { "stringUnit" : { @@ -25066,6 +25074,7 @@ } }, "Router Options" : { + "extractionState" : "stale", "localizations" : { "sr" : { "stringUnit" : { @@ -27139,6 +27148,9 @@ } } } + }, + "Send a heartbeat to advertise the server's presence." : { + }, "Send a message to a certain meshtastic channel" : { "localizations" : { @@ -27888,6 +27900,9 @@ } } } + }, + "Server Option" : { + }, "Set" : { "localizations" : { @@ -28046,6 +28061,9 @@ } } } + }, + "Settings" : { + }, "Share QR Code & Link" : { "localizations" : { @@ -28883,6 +28901,7 @@ } }, "Store and forward clients can request history from routers on the network." : { + "extractionState" : "stale", "localizations" : { "sr" : { "stringUnit" : { @@ -28893,6 +28912,7 @@ } }, "Store and forward router devices require a ESP32 device with PSRAM." : { + "extractionState" : "stale", "localizations" : { "sr" : { "stringUnit" : { @@ -28901,6 +28921,9 @@ } } } + }, + "Store and forward servers require an ESP32 device with PSRAM or Linux Native." : { + }, "storeforward.heartbeat" : { "localizations" : { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 47db7ebd..64cb2b9f 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -811,11 +811,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate case .serialApp: Logger.mesh.info("πŸ•ΈοΈ MESH PACKET received for Serial App UNHANDLED UNHANDLED") case .storeForwardApp: - if wantStoreAndForwardPackets { - storeAndForwardPacket(packet: decodedInfo.packet, connectedNodeNum: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context) - } else { - Logger.mesh.info("πŸ•ΈοΈ MESH PACKET received for Store and Forward App - Store and Forward is disabled.") - } + storeAndForwardPacket(packet: decodedInfo.packet, connectedNodeNum: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context) case .rangeTestApp: if wantRangeTestPackets { textMessageAppPacket( diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 093066ef..e47f462f 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -898,11 +898,14 @@ func textMessageAppPacket( if packet.decoded.replyID > 0 { newMessage.replyID = Int64(packet.decoded.replyID) } + // Updated logic for handling toUser if fetchedUsers.first(where: { $0.num == packet.to }) != nil && packet.to != Constants.maximumNodeNum { if !storeForwardBroadcast { newMessage.toUser = fetchedUsers.first(where: { $0.num == packet.to }) + } else if storeForwardBroadcast { + // For S&F broadcast messages, treat as a channel message (not a DM) + newMessage.toUser = nil } else { - /// Make a new to user if they are unknown newMessage.toUser = createUser(num: Int64(truncatingIfNeeded: packet.to), context: context) } } @@ -961,7 +964,7 @@ func textMessageAppPacket( appState.unreadDirectMessages = newMessage.toUser?.unreadMessages ?? 0 } if !(newMessage.fromUser?.mute ?? false) { - // Create an iOS Notification for the received DM message and schedule it immediately + // Create an iOS Notification for the received DM message let manager = LocalNotificationManager() manager.notifications = [ Notification( @@ -983,18 +986,16 @@ func textMessageAppPacket( } else if newMessage.fromUser != nil && newMessage.toUser == nil { let fetchMyInfoRequest = MyInfoEntity.fetchRequest() fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode)) - do { let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) if !fetchedMyInfo.isEmpty { appState.unreadChannelMessages = fetchedMyInfo[0].unreadMessages - for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { if channel.index == newMessage.channel { context.refresh(channel, mergeChanges: true) } if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications { - // Create an iOS Notification for the received private channel message and schedule it immediately + // Create an iOS Notification for the received channel message let manager = LocalNotificationManager() manager.notifications = [ Notification( @@ -1007,7 +1008,8 @@ func textMessageAppPacket( messageId: newMessage.messageId, channel: newMessage.channel, userNum: Int64(newMessage.fromUser?.userId ?? "0"), - critical: critical) + critical: critical + ) ] manager.schedule() Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)") diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 49.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 49.xcdatamodel/contents index 9e23de8a..03e88490 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 49.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 49.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -373,7 +373,8 @@ - + + diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 0c367968..3a169b17 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -1363,6 +1363,7 @@ func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfi newConfig.records = Int32(config.records) newConfig.historyReturnMax = Int32(config.historyReturnMax) newConfig.historyReturnWindow = Int32(config.historyReturnWindow) + newConfig.isServer = config.isServer fetchedNode[0].storeForwardConfig = newConfig } else { fetchedNode[0].storeForwardConfig?.enabled = config.enabled diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index df5b1f3d..f444b8d8 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -37,14 +37,28 @@ struct MessageText: View { HStack { Spacer() Image(systemName: "lock.circle.fill") - .symbolRenderingMode(.palette) - .foregroundStyle(.white, .green) - .font(.system(size: 20)) - .offset(x: 8, y: 8) + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .green) + .font(.system(size: 20)) + .offset(x: 8, y: 8) } } } + let isStoreAndForward = message.portNum == Int32(PortNum.storeForwardApp.rawValue) let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue) + if isStoreAndForward { + VStack(alignment: .trailing) { + Spacer() + HStack { + Spacer() + Image(systemName: "envelope.circle.fill") + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .gray) + .font(.system(size: 20)) + .offset(x: 8, y: 8) + } + } + } if tapBackDestination.overlaySensorMessage { VStack { isDetectionSensorMessage ? Image(systemName: "sensor.fill") @@ -59,6 +73,7 @@ struct MessageText: View { } else { EmptyView() } + } .contextMenu { MessageContextMenuItems( diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index f0718ac2..ee1359c1 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -18,8 +18,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 + /// Is a S&F Server + @State var isServer = false /// Send a Heartbeat @State var heartbeat: Bool = false /// Number of Records @@ -35,43 +35,19 @@ struct StoreForwardConfig: View { ConfigHeader(title: "Store & Forward", config: \.storeForwardConfig, node: node, onAppear: setStoreAndForwardValues) Section(header: Text("options")) { - Toggle(isOn: $enabled) { Label("enabled", systemImage: "envelope.arrow.triangle.branch") - Text("Enables the store and forward module. Store and forward must be enabled on both client and router devices.") + Text("Enables the store and forward module.") } .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) - } - VStack { - if isRouter { - Text("Store and forward router devices require a ESP32 device with PSRAM.") - .foregroundColor(.gray) - .font(.callout) - } else { - Text("Store and forward clients can request history from routers on the network.") - .foregroundColor(.gray) - .font(.callout) - } - } - } } - if isRouter { - Section(header: Text("Router Options")) { + if enabled { + Section(header: Text("Settings")) { Toggle(isOn: $heartbeat) { Label("storeforward.heartbeat", systemImage: "waveform.path.ecg") + Text("Send a heartbeat to advertise the server's presence.") } Picker("Number of records", selection: $records) { Text("unset").tag(0) @@ -81,7 +57,7 @@ struct StoreForwardConfig: View { Text("100").tag(100) } .pickerStyle(DefaultPickerStyle()) - Picker("History Return Max", selection: $historyReturnMax ) { + Picker("History Return Max", selection: $historyReturnMax) { Text("unset").tag(0) Text("25").tag(25) Text("50").tag(50) @@ -89,7 +65,7 @@ struct StoreForwardConfig: View { Text("100").tag(100) } .pickerStyle(DefaultPickerStyle()) - Picker("History Return Window", selection: $historyReturnWindow ) { + Picker("History Return Window", selection: $historyReturnWindow) { Text("unset").tag(0) Text("One Minute").tag(60) Text("Five Minutes").tag(300) @@ -101,6 +77,20 @@ struct StoreForwardConfig: View { } .pickerStyle(DefaultPickerStyle()) } + + Section(header: Text("Server Option")) { + Toggle(isOn: $isServer) { + Label("Server", systemImage: "server.rack") + Text("Enable this device as a Store and Forward server. Requires an ESP32 device with PSRAM.") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + if isServer { + Text("Store and forward servers require an ESP32 device with PSRAM or Linux Native.") + .foregroundColor(.gray) + .font(.callout) + } + } } } .scrollDismissesKeyboard(.interactively) @@ -110,18 +100,19 @@ struct StoreForwardConfig: View { SaveConfigButton(node: node, hasChanges: $hasChanges) { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context) if connectedNode != nil { - /// Let the user set isRouter for the connected node, for nodes on the mesh set isRouter based + /// Let the user set isServer for the connected node, for nodes on the mesh set isServer based /// on receipt of a primary heartbeat if connectedNode?.num ?? 0 == node?.num ?? -1 { - connectedNode?.storeForwardConfig?.isRouter = isRouter + connectedNode?.storeForwardConfig?.isServer = isServer do { try context.save() } catch { - Logger.mesh.error("Failed to save isRouter: \(error.localizedDescription)") + Logger.mesh.error("Failed to save isServer: \(error.localizedDescription)") } } var sfc = ModuleConfig.StoreForwardConfig() + sfc.isServer = isServer sfc.enabled = self.enabled sfc.heartbeat = self.heartbeat sfc.records = UInt32(self.records) @@ -171,8 +162,8 @@ struct StoreForwardConfig: View { .onChange(of: enabled) { oldEnabled, newEnabled in if oldEnabled != newEnabled && newEnabled != node!.storeForwardConfig!.enabled { hasChanges = true } } - .onChange(of: isRouter) { oldIsRouter, newIsRouter in - if oldIsRouter != newIsRouter && newIsRouter != node!.storeForwardConfig!.isRouter { hasChanges = true } + .onChange(of: isServer) { oldIsServer, newIsServer in + if oldIsServer != newIsServer && newIsServer != node!.storeForwardConfig!.isServer { hasChanges = true } } .onChange(of: heartbeat) { oldHeartbeat, newHeartbeat in if oldHeartbeat != newHeartbeat && newHeartbeat != node?.storeForwardConfig?.heartbeat ?? true { hasChanges = true } @@ -187,9 +178,10 @@ struct StoreForwardConfig: View { if oldHistoryReturnWindow != newHistoryReturnWindow && newHistoryReturnWindow != node!.storeForwardConfig?.historyReturnWindow ?? -1 { hasChanges = true } } } + func setStoreAndForwardValues() { self.enabled = (node?.storeForwardConfig?.enabled ?? false) - self.isRouter = (node?.storeForwardConfig?.isRouter ?? false) + self.isServer = (node?.storeForwardConfig?.isServer ?? false) self.heartbeat = (node?.storeForwardConfig?.heartbeat ?? true) self.records = Int(node?.storeForwardConfig?.records ?? 50) self.historyReturnMax = Int(node?.storeForwardConfig?.historyReturnMax ?? 100) From 360906f31d35c627af057e7c8bba94042f8dbf20 Mon Sep 17 00:00:00 2001 From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Date: Sat, 15 Mar 2025 00:05:09 -0700 Subject: [PATCH 4/4] Revert read all --- Meshtastic/Views/Messages/ChannelMessageList.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 7753ddbb..573a7dc8 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -113,10 +113,8 @@ struct ChannelMessageList: View { .id(message.messageId) .onAppear { if !message.read { + message.read = true do { - for unreadMessage in channel.allPrivateMessages.filter({ !$0.read }) { - unreadMessage.read = true - } try context.save() Logger.data.info("πŸ“– [App] Read message \(message.messageId) ") appState.unreadChannelMessages = myInfo.unreadMessages