From f4a114c958051d6eb14a47e0d875e7f72fa4bf29 Mon Sep 17 00:00:00 2001 From: xerion79585 <82042289+xerion79585@users.noreply.github.com> Date: Mon, 18 Mar 2024 09:27:23 +0800 Subject: [PATCH 01/21] Update Localizable.strings Corrected some text to make the sentences more in line with Taiwanese usage habits --- zh-TW.lproj/Localizable.strings | 90 ++++++++++++++++----------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/zh-TW.lproj/Localizable.strings b/zh-TW.lproj/Localizable.strings index 6f36c512..f25fc223 100644 --- a/zh-TW.lproj/Localizable.strings +++ b/zh-TW.lproj/Localizable.strings @@ -23,9 +23,9 @@ "automatic.detection"="自動識別"; "battery.level"="電池電量"; "ble.name"="藍芽名稱"; -"ble.connection.timeout %d %@"="嘗試連接%@失敗,你可能需要在系统設定的藍芽選項中忽略該電台。"; -"ble.errorcode.6 %@"="%@ 如果在首選電台的旁邊,App 將會自動重連。"; -"ble.errorcode.14 %@"="%@ 這個錯誤通常無法自動修復,你需要在系統設定的藍芽選項中忽略該電台並重新配對。"; +"ble.connection.timeout %d %@"="嘗試連接%@失敗,你可能需要在系统設定的藍芽選項中忽略該設備。"; +"ble.errorcode.6 %@"="%@ 如果在首選裝置的旁邊,App 將會自動重連。"; +"ble.errorcode.14 %@"="%@ 這個錯誤通常無法自動修復,你需要在系統設定的藍芽選項中忽略該裝置並重新配對。"; "ble.errorcode.pin %@"="%@ 請再次嘗試連接並仔細檢查 PIN 碼。"; "bluetooth"="藍芽"; "bluetooth.off"="藍芽已關閉"; @@ -60,22 +60,22 @@ "config.power.ls.secs"="Light Sleep Interval"; "config.power.min.wake.secs"="最小的喚醒間隔時間"; "config.power.saving"="省電模式"; -"config.power.saving.description"="Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. Don't use this setting if you want to use your device with the phone apps or are using a device without a user button."; +"config.power.saving.description"="將會盡可能的進入休眠,追蹤器模式和感測器模式將會包含在內"; "config.power.shutdown.on.power.loss"="失去電源後關機"; "config.power.shutdown.after.secs"="之後"; "config.power.wait.bluetooth.secs"="等待藍芽"; "config.ringtone"="RTTTL Ringtone"; "config.ringtone.title"="鈴聲"; "config.ringtone.label"="Ringtone Transfer Language"; -"config.ringtone.description"="Ringtone Transfer Language(RTTTL) Ringtone String used by supported buzzers in external notifications."; +"config.ringtone.description"="支援外部通知的蜂鳴器所使用的 RTTTL(Ringtone Transfer Language)鈴聲字串"; "config.module.paxcounter.settings"="PAX Counter"; "config.module.paxcounter.title"="PAX Counter Config"; -"config.module.paxcounter.enabled.description"="When enabled the PAX Counter module counts the number of people passing by using WiFi and Bluetooth. Both WiFI and Bluetooth must be enabled for PAX counter to work."; -"config.module.paxcounter.updateinterval"="Update Interval"; +"config.module.paxcounter.enabled.description"="啟用 PAX 計數器模組後,將使用 WiFi 和藍牙計算經過的人數。PAX 計數器需要同時啟用 WiFi 和藍牙才能正常運作"; +"config.module.paxcounter.updateinterval"="更新間隔"; "config.module.paxcounter.updateinterval.description"="How often we can send a message to the mesh when people are detected."; -"config.save.confirm"="電台將會在設定儲存後重啟。"; -"connected.radio"="已連接的電台"; -"communicating"="與電台進行通訊中..."; +"config.save.confirm"="裝置將會在設定儲存後重啟。"; +"connected.radio"="已連接的裝置"; +"communicating"="與裝置進行通訊中..."; "connected"="已連接"; "connecting"="連接中..."; "contacts"="聯絡人"; @@ -86,21 +86,21 @@ "delete"="刪除"; "detection.sensor"="檢測感測器"; "device"="設備"; -"device.config"="電台設定"; +"device.config"="裝置設定"; "device.configuration"="設備設定"; -"device.metrics.delete"="刪除所有電台指標??"; -"device.metrics.log"="電台指標紀錄檔"; -"device.role.client"="標準模式 - App 可以連接到電台進行收發操作,並且會自動轉發 Mesh 網路中其他中繼點的消息。"; -"device.role.clientmute"="靜音模式 - 與標準模式類似,App 可以連接到電台進行收發操作,但不會轉發 Mesh 網路中其他中繼點的消息。"; +"device.metrics.delete"="刪除所有設備指標??"; +"device.metrics.log"="設備指標紀錄檔"; +"device.role.client"="標準模式 - App 可以連接到裝置進行收發操作,並且會自動轉發 Mesh 網路中其他中繼節點的消息。"; +"device.role.clientmute"="靜音模式 - 與標準模式類似,App 可以連接到裝置進行收發操作,但不會轉發 Mesh 網路中其他中繼節點的消息。"; "device.role.clienthidden"=" Used for nodes that \"only speak when spoken to\" Turns all of the routine broadcasts but allows for ad-hoc communication. Still rebroadcasts, but with local only rebroadcast mode (known meshes only). Can be used for private operation or to dramatically reduce airtime / power consumption."; "device.role.lostandfound"="Used to automatically send a text message to the mesh with the current position of the device on a frequent interval: \"I'm lost! Position: lat / long\""; -"device.role.router"="纯路由模式 - 自動轉發 Mesh 網路中其他中繼點的消息,中繼模式下螢幕會熄滅,Wi-Fi 和藍芽將會進入睡眠模式,App 將無法連接到電台進行收發操作。"; -"device.role.routerclient"="路由客户端模式 - 優先轉發 Mesh 網路中其他中繼點的消息,App 也可以連接到電台進行收發操作。"; -"device.role.repeater"="中繼模式 - Mesh 網路數據包將優先通過此中繼點路由。此模式可消除不必要的開銷,如 NodeInfo、DeviceTelemetry 和任何其他 Mesh 數據包,從而使設備不顯示為 Mesh 網路的一部分。有關此角色的其他特定設置,請參閱轉播模式。"; +"device.role.router"="纯路由模式 - 自動轉發 Mesh 網路中其他中繼節點的消息,中繼模式下螢幕會熄滅,Wi-Fi 和藍芽將會進入睡眠模式,App 將無法連接到裝置進行收發操作。"; +"device.role.routerclient"="路由客户端模式 - 優先轉發 Mesh 網路中其他中繼節點的消息,App 也可以連接到裝置進行收發操作。"; +"device.role.repeater"="中繼模式 - Mesh 網路數據包將優先通過此中繼節點路由。此模式可消除不必要的開銷,如 NodeInfo、DeviceTelemetry 和任何其他 Mesh 數據包,從而使設備不顯示為 Mesh 網路的一部分。有關此角色的其他特定設置,請參閱轉播模式。"; "device.role.tracker"="追蹤模式 - 用於作為 GPS 追蹤器。從該設備發送的定位數據包優先級較高,每兩分鐘廣播一次。智能位置廣播預設為關閉。"; "direct.messages"="聊天"; "dismiss.keyboard"="隱藏鍵盤"; -"display"="螢幕(電台螢幕)"; +"display"="螢幕(設備螢幕)"; "display.config"="螢幕設定"; "distance"="距離"; "disconnect"="斷開連接"; @@ -112,7 +112,7 @@ "external.notification.config"="外部通知設定"; "finish"="完成"; "firmware.version"="韌體版本"; -"firmware.version.unsupported"="檢測到不支援的韌體版本,無法連接到電台。"; +"firmware.version.unsupported"="檢測到不支援的韌體版本,無法連接到裝置。"; "gas"="Gas"; "gas.resistance"="Gas Resistance"; "generate.qr.code"="生成QRcode"; @@ -165,13 +165,13 @@ "interval.tyeight.hours"="四十八小时小時"; "interval.eventytwo.hours"="七十二小時"; "keyboard.type"="鍵盤類型"; -"logging"="加載中"; +"logging"="載入中"; "lora"="LoRa"; "lora.config"="LoRa 設定"; "map"="Mesh 地圖"; -"map.centering"="居中"; +"map.centering"="置中"; "map.tiles.delete"="刪除已緩存的地圖區塊"; -"map.recentering"="自動重新居中"; +"map.recentering"="自動重新置中"; "map.use.legacy"="Use Legacy Mesh Map"; "map.type"="地圖類型"; "map.usertrackingmode"="使用者跟隨模式"; @@ -198,18 +198,18 @@ "mesh.log.mqtt.config %@"="MQTT module config received: %@"; "mesh.log.myinfo %@"="MyInfo received: %@"; "mesh.log.network.config %@"="收到網路設定: %@"; -"mesh.log.nodeinfo.received %@"="收到中繼點訊息: %@"; +"mesh.log.nodeinfo.received %@"="收到中繼節點訊息: %@"; "mesh.log.paxcounter %@"="PAX Counter message received for: %@"; "mesh.log.position.config %@"="Positon config received: %@"; -"mesh.log.position.received %@"="從中繼點接收到定位封包: %@"; +"mesh.log.position.received %@"="從中繼節點接收到定位封包: %@"; "mesh.log.rangetest.config %@"="收到拉距測試模組設定: %@"; "mesh.log.ringtone.config %@"="RTTTL Ringtone config received: %@"; "mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@"; "mesh.log.serial.config %@"="Serial module config received: %@"; -"mesh.log.sharelocation %@"="傳送iOS裝置的GPS定位封包到中繼點上: %@"; +"mesh.log.sharelocation %@"="傳送iOS裝置的GPS定位封包到中繼節點上: %@"; "mesh.log.storeforward.config %@"="Store & Forward module config received: %@"; -"mesh.log.telemetry.config %@"="收到遠測模組設定: %@"; -"mesh.log.telemetry.received %@"="收到遠測資料: %@"; +"mesh.log.telemetry.config %@"="收到遙測模組設定: %@"; +"mesh.log.telemetry.received %@"="收到遙測資料: %@"; "mesh.log.textmessage.received"="Message received from the text message app."; "mesh.log.textmessage.send.failed %@"="訊息傳送失敗, 沒有正確連接到 %@"; "mesh.log.textmessage.sent %@ %@ %@"="傳送訊息 %@ 從 %@ 到 %@"; @@ -233,10 +233,10 @@ "name"="名稱"; "network"="網路"; "network.config"="網路設定"; -"nodes"="中繼點"; -"nodes %@"="中繼點 (%@)"; -"no.nodes"="未找到 Meshtastic 中繼點"; -"not.connected"="未連接到電台"; +"nodes"="中繼節點"; +"nodes %@"="中繼節點 (%@)"; +"no.nodes"="未找到 Meshtastic 中繼節點"; +"not.connected"="未連接到設備"; "numbers.punctuation"="數字和標點符號"; "off"="關閉"; "offline"="離線"; @@ -245,17 +245,17 @@ "password"="密碼"; "pause"="暫停"; "phone.gps"="手機 GPS"; -"phone.gps.interval.description"="電台通過手機獲得定位的時間間隔,但是向 Mesh 網路中更新定位的時間間隔由電台控制。"; +"phone.gps.interval.description"="設備通過手機獲得定位的時間間隔,但是向 Mesh 網路中更新定位的時間間隔由裝置控制。"; "position"="定位"; "position.config"="定位設定"; -"preferred.radio"="首選電台"; -"radio.configuration"="電台設定"; +"preferred.radio"="首選設備"; +"radio.configuration"="設備設定"; "range.test"="拉距測試"; "range.test.blocked"="區塊範圍測試"; "range.test.config"="拉距測試設定"; "reply"="回復"; "reboot"="重新啟動"; -"reboot.node"="重啟中繼點"; +"reboot.node"="重啟中繼節點"; "received.ack"="收到確認"; "received.ack.real"="收件人確認"; "resume"="恢復"; @@ -272,7 +272,7 @@ "routing.nochannel"="没有頻道"; "routing.toolarge"="數據包過大"; "routing.noresponse"="無回應"; -"routing.dutycyclelimit"="已達到物錢區域循環週期發射上限"; +"routing.dutycyclelimit"="已達到目前區域循環週期發射上限"; "routing.badRequest"="錯誤請求"; "routing.notauthorized"="未授權"; "satellite"="衛星"; @@ -291,7 +291,7 @@ "share.position"="分享位置"; "subscribed"="連接到 Mesh 網路"; "select.contact"="選擇聯絡人"; -"select.node"="選擇中繼點"; +"select.node"="選擇中繼節點"; "select.menu.item"="從菜單選擇項目"; "set.region"="設定 LoRa 區域"; "standard"="標準"; @@ -303,22 +303,22 @@ "storeforward.heartbeat"="發送心跳包"; "tapback"="響應"; "tapback.heart"="心"; -"tapback.thumbsup"="豎大拇指"; -"tapback.thumbsdown"="倒大拇指"; +"tapback.thumbsup"="讚"; +"tapback.thumbsdown"="倒讚"; "tapback.haha"="哈哈"; "tapback.exclamation"="驚嘆號"; "tapback.question"="問號"; "tapback.poop"="便便"; -"telemetry"="遠測(傳感器)"; -"telemetry.config"="遠側設定"; +"telemetry"="遙測(傳感器)"; +"telemetry.config"="遙測設定"; "timeout"="超時"; "timestamp"="時間戳記"; -"tip.bluetooth.connect.title"="連接到 LoRa 電台"; -"tip.bluetooth.connect.message"="顯示目前通過藍芽連接的 Lora 電台的信息。您可以向左滑動斷開電台,長按查看統計訊息或開始即時活動。"; +"tip.bluetooth.connect.title"="連接到 LoRa 設備"; +"tip.bluetooth.connect.message"="顯示目前通過藍芽連接的 Lora 裝置的信息。您可以向左滑動斷開裝置,長按查看統計訊息或開始即時活動。"; "tip.channels.create.title"="管理頻道"; "tip.channels.create.message"="現在 Mesh 上的資料會通過主通道發送。您可以設定輔助通道來建立由自己的金鑰保護的其他訊息組 [頻道設定提示](https://meshtastic.org/docs/configuration/radio/channels/)"; "tip.channels.share.title"="共享 Meshtastic 頻道"; -"tip.channels.share.message"="在 Meshtastic 網路中最多有 8 個頻道。第一個頻道是主頻道,大多數活動都發生在這裡,也是必需的。如果您不共享主頻道,您的第一個共享頻道就會成為其他網路的主頻道。它會在其主頻道和您的輔助頻道上對話。名稱為 admin 的頻道可遠端控制中繼點。其他頻道用於私人群组,每個群組都有自己的密鑰。"; +"tip.channels.share.message"="在 Meshtastic 網路中最多有 8 個頻道。第一個頻道是主頻道,大多數活動都發生在這裡,也是必需的。如果您不共享主頻道,您的第一個共享頻道就會成為其他網路的主頻道。它會在其主頻道和您的輔助頻道上對話。名稱為 admin 的頻道可遠端控制中繼節點。其他頻道用於私人群组,每個群組都有自己的密鑰。"; "tip.messages.title"="消息"; "tip.messages.message"="您可以發送和接收1對1聊天和群聊。在任何訊息中,您都可以長按查看可用的操作,如複製、回復、拍一拍、刪除以及詳情。"; "twitter"="Twitter"; From 36ba2e4508aaed402b270530e0b2c6a3dca566db Mon Sep 17 00:00:00 2001 From: Matthew Davies Date: Thu, 28 Mar 2024 21:31:37 -0700 Subject: [PATCH 02/21] Update readme to include better info about protos submodule init Update gen_protos.sh to check the proper directory and echo the command --- README.md | 3 +++ gen_protos.sh | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ec8a8b5c..8ef16e48 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,9 @@ SwiftUI client applications for iOS, iPadOS and macOS. brew install swift-protobuf ``` - check out the latest protobuf commit from the master branch + ```bash + git submodule update --init + ``` - run: ```bash ./gen_proto.sh diff --git a/gen_protos.sh b/gen_protos.sh index d36fafed..b829095f 100755 --- a/gen_protos.sh +++ b/gen_protos.sh @@ -1,14 +1,14 @@ #!/bin/bash # simple sanity checking for repo -if [ ! -d "../protobufs" ]; then - echo "Please check out the protobuf submodule by running: `git submodule update --init`" +if [ ! -d "./protobufs" ]; then + echo 'Please check out the protobuf submodule by running: `git submodule update --init`' exit fi # simple sanity checking for executable if [ ! -x "$(which protoc)" ]; then - echo "Please install swift-protobuf by running: brew install swift-protobuf" + echo 'Please install swift-protobuf by running: `brew install swift-protobuf`' exit fi From f38e61b0b2393aafcfd963a1f68b7b430fbe957c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 29 Mar 2024 14:37:09 -0700 Subject: [PATCH 03/21] Hook up favorites, clean up admin dropdown list --- Meshtastic.xcodeproj/project.pbxproj | 8 +-- Meshtastic/Helpers/BLEManager.swift | 49 +++++++++++++++++++ .../Map/MapContent/MeshMapContent.swift | 4 +- .../Views/Nodes/Helpers/NodeListItem.swift | 2 +- Meshtastic/Views/Nodes/NodeList.swift | 44 ++++++++++++----- Meshtastic/Views/Settings/Settings.swift | 24 +++++---- 6 files changed, 102 insertions(+), 29 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 9339e55d..094f1625 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1610,7 +1610,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3.2; + MARKETING_VERSION = 2.3.3; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1644,7 +1644,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3.2; + MARKETING_VERSION = 2.3.3; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1766,7 +1766,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.2; + MARKETING_VERSION = 2.3.3; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1799,7 +1799,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.2; + MARKETING_VERSION = 2.3.3; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 9327ebc8..abf65753 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -761,6 +761,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } if fetchedNodeInfo.count == 1 { if !(fetchedNodeInfo[0].user?.vip ?? false) { + fetchedNodeInfo[0].favorite = true fetchedNodeInfo[0].user?.vip = true do { try context!.save() @@ -1448,6 +1449,54 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return false } + public func setFavoriteNode(node: NodeInfoEntity, connectedNodeNum: Int64) -> Bool { + var adminPacket = AdminMessage() + adminPacket.setFavoriteNode = UInt32(node.num) + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(connectedNodeNum) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + var adminPacket = AdminMessage() + adminPacket.removeFavoriteNode = UInt32(node.num) + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(connectedNodeNum) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setHamMode = ham diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 2655fbd4..6a03aba8 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -93,7 +93,7 @@ struct MeshMapContent: MapContent { } /// Node History and Route Lines for favorites - if position.nodePosition?.user?.vip ?? false { + if position.nodePosition?.favorite ?? false { if showRouteLines { let nodePositions = Array(position.nodePosition!.positions!) as! [PositionEntity] let routeCoords = nodePositions.compactMap({(pos) -> CLLocationCoordinate2D in @@ -112,7 +112,7 @@ struct MeshMapContent: MapContent { } if showNodeHistory { ForEach(Array(position.nodePosition!.positions!) as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in - if mappin.latest == false && mappin.nodePosition?.user?.vip ?? false { + if mappin.latest == false && mappin.nodePosition?.favorite ?? false { let pf = PositionFlags(rawValue: Int(mappin.nodePosition?.metadata?.positionFlags ?? 771)) let headingDegrees = Angle.degrees(Double(mappin.heading)) Annotation("", coordinate: mappin.coordinate) { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index f7ea4ffc..21bf839d 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -31,7 +31,7 @@ struct NodeListItem: View { Text(node.user?.longName ?? "unknown".localized) .fontWeight(.medium) .font(.headline) - if node.user?.vip ?? false { + if node.favorite { Spacer() Image(systemName: "star.fill") .foregroundColor(.yellow) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 8a63d2a9..1cb3508e 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -33,7 +33,7 @@ struct NodeList: View { @EnvironmentObject var bleManager: BLEManager @FetchRequest( - sortDescriptors: [NSSortDescriptor(key: "user.vip", ascending: false), NSSortDescriptor(key: "lastHeard", ascending: false)], + sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false), NSSortDescriptor(key: "lastHeard", ascending: false)], animation: .default) var nodes: FetchedResults @@ -49,19 +49,39 @@ struct NodeList: View { connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num, connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1)) .contextMenu { - if node.user != nil { - Button { - node.user!.vip = !node.user!.vip - context.refresh(node, mergeChanges: true) - do { - try context.save() - } catch { - context.rollback() - print("💥 Save User VIP Error") + + Button { + node.user!.vip = !node.user!.vip + if !node.favorite { + let success = bleManager.setFavoriteNode(node: node, connectedNodeNum: Int64(connectedNodeNum)) + if success { + node.favorite = !node.favorite + do { + try context.save() + } catch { + context.rollback() + print("💥 Save Node Favorite Error") + } + print("Favorited a node") + } + } else { + let success = bleManager.removeFavoriteNode(node: node, connectedNodeNum: Int64(connectedNodeNum)) + if success { + node.favorite = !node.favorite + do { + try context.save() + } catch { + context.rollback() + print("💥 Save Node Favorite Error") + } + print("Favorited a node") } - } label: { - Label(node.user?.vip ?? false ? "Un-Favorite" : "Favorite", systemImage: node.user?.vip ?? false ? "star.slash.fill" : "star.fill") } + + } label: { + Label(node.favorite ? "Un-Favorite" : "Favorite", systemImage: node.favorite ? "star.slash.fill" : "star.fill") + } + if node.user != nil { Button { node.user!.mute = !node.user!.mute context.refresh(node, mergeChanges: true) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index b990b0f4..3db5cb86 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -13,7 +13,8 @@ import TipKit struct Settings: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "user.longName", ascending: true)], animation: .default) + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false), + NSSortDescriptor(key: "user.longName", ascending: true)], animation: .default) private var nodes: FetchedResults @State private var selectedNode: Int = 0 @State private var preferredNodeNum: Int = 0 @@ -106,16 +107,19 @@ struct Settings: View { if selectedNode == 0 { Text("Connect to a Node").tag(0) } + ForEach(nodes) { node in - if node.num == bleManager.connectedPeripheral?.num ?? 0 { - Text("BLE Config: \(node.user?.longName ?? "unknown".localized)") - .tag(Int(node.num)) - } else if node.metadata != nil { - Text("Remote Config: \(node.user?.longName ?? "unknown".localized)") - .tag(Int(node.num)) - } else if hasAdmin { - Text("Request Admin: \(node.user?.longName ?? "unknown".localized)") - .tag(Int(node.num)) + if !node.viaMqtt { + if node.num == bleManager.connectedPeripheral?.num ?? 0 { + Text("BLE Config: \(node.user?.longName ?? "unknown".localized)") + .tag(Int(node.num)) + } else if node.metadata != nil { + Text("Remote Config: \(node.user?.longName ?? "unknown".localized)") + .tag(Int(node.num)) + } else if hasAdmin { + Text("Request Admin: \(node.user?.longName ?? "unknown".localized)") + .tag(Int(node.num)) + } } } } From 769a35c485ff605cd528a9bfa6eebc07f12da9e2 Mon Sep 17 00:00:00 2001 From: Matthew Davies Date: Fri, 29 Mar 2024 15:04:00 -0700 Subject: [PATCH 04/21] Add mqtt status icon in channels with uplink/downlink enabled --- Meshtastic/Views/Helpers/ConnectedDevice.swift | 7 ++++--- Meshtastic/Views/Messages/ChannelMessageList.swift | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Views/Helpers/ConnectedDevice.swift b/Meshtastic/Views/Helpers/ConnectedDevice.swift index 3a3b6d2a..7e9d1852 100644 --- a/Meshtastic/Views/Helpers/ConnectedDevice.swift +++ b/Meshtastic/Views/Helpers/ConnectedDevice.swift @@ -9,6 +9,7 @@ struct ConnectedDevice: View { var bluetoothOn: Bool var deviceConnected: Bool var name: String + var mqttProxyEnabled: Bool = false var mqttProxyConnected: Bool = false var phoneOnly: Bool = false @@ -18,11 +19,11 @@ struct ConnectedDevice: View { if (phoneOnly && UIDevice.current.userInterfaceIdiom == .phone) || !phoneOnly { if bluetoothOn { - if deviceConnected && mqttProxyConnected { - if mqttProxyConnected { + if deviceConnected && (mqttProxyEnabled || mqttProxyConnected) { + if (mqttProxyConnected || mqttProxyEnabled) { Image(systemName: "iphone.gen3.radiowaves.left.and.right.circle.fill") .imageScale(.large) - .foregroundColor(.green) + .foregroundColor(mqttProxyConnected ? .green : .gray) .symbolRenderingMode(.hierarchical) } } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 22438b6c..234d705a 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -156,7 +156,10 @@ struct ChannelMessageList: View { ConnectedDevice( bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, - name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") + name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", + mqttProxyEnabled: channel.uplinkEnabled || channel.downlinkEnabled, + mqttProxyConnected: channel.uplinkEnabled || channel.downlinkEnabled ? bleManager.mqttProxyConnected : false + ) } } } From 9aa0a2c7a50ffc6ccd38ade1e004a2eb3c2d8e29 Mon Sep 17 00:00:00 2001 From: Matthew Davies Date: Fri, 29 Mar 2024 15:19:06 -0700 Subject: [PATCH 05/21] Add mqtt status in mqtt config settings screen --- Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index be067293..b0de0cbb 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -289,7 +289,7 @@ struct MQTTConfig: View { .navigationTitle("mqtt.config") .navigationBarItems(trailing: ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", mqttProxyConnected: bleManager.mqttProxyConnected) + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", mqttProxyEnabled: self.enabled, mqttProxyConnected: bleManager.mqttProxyConnected) }) .onChange(of: address) { newAddress in if node != nil && node?.mqttConfig != nil { From badc658cbae1c7e0b73cfa5dcf31be5ce3ec1ce7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 29 Mar 2024 17:56:54 -0700 Subject: [PATCH 06/21] Clean up admin drop down list --- Meshtastic/Views/Settings/Settings.swift | 30 ++++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 3db5cb86..ec2c747f 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -109,17 +109,27 @@ struct Settings: View { } ForEach(nodes) { node in - if !node.viaMqtt { - if node.num == bleManager.connectedPeripheral?.num ?? 0 { - Text("BLE Config: \(node.user?.longName ?? "unknown".localized)") - .tag(Int(node.num)) - } else if node.metadata != nil { - Text("Remote Config: \(node.user?.longName ?? "unknown".localized)") - .tag(Int(node.num)) - } else if hasAdmin { - Text("Request Admin: \(node.user?.longName ?? "unknown".localized)") - .tag(Int(node.num)) + if node.num == bleManager.connectedPeripheral?.num ?? 0 { + Label { + Text("BLE: \(node.user?.longName ?? "unknown".localized)") + } icon: { + Image(systemName: "antenna.radiowaves.left.and.right") } + .tag(Int(node.num)) + } else if node.metadata != nil { + Label { + Text("Remote: \(node.user?.longName ?? "unknown".localized)") + } icon: { + Image(systemName: "av.remote") + } + .tag(Int(node.num)) + } else if hasAdmin { + Label { + Text("Request Admin: \(node.user?.longName ?? "unknown".localized)") + } icon: { + Image(systemName: "rectangle.and.hand.point.up.left") + } + .tag(Int(node.num)) } } } From 52c52ae1e6091a2dcce6c99a7913b6b2d6ff2894 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 31 Mar 2024 12:38:17 -0700 Subject: [PATCH 07/21] 100 node limit on the node map, remove 2500 mile distance filter --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 3 ++- Meshtastic/Enums/AppSettingsEnums.swift | 1 - Meshtastic/Extensions/CoreData/PositionEntityExtension.swift | 4 ++-- Meshtastic/Views/Nodes/MeshMap.swift | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 85e8785d..a62998be 100644 --- a/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "e9855e3a299c14a10f11ee0b8f29e4170b09548533939361223a0f50e7caac8c", "pins" : [ { "identity" : "cocoamqtt", @@ -46,5 +47,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index c53c3826..4b0d619f 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -56,7 +56,6 @@ enum MeshMapDistances: Double, CaseIterable, Identifiable { case twoHundredMiles = 321869 case fiveHundredMiles = 804672 case oneThousandMiles = 1609000 - case twentyFiveHundredMiles = 4023360 var id: Double { self.rawValue } var description: String { let distanceFormatter = MKDistanceFormatter() diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index 0221a1da..2643b242 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -14,8 +14,8 @@ extension PositionEntity { static func allPositionsFetchRequest() -> NSFetchRequest { let request: NSFetchRequest = PositionEntity.fetchRequest() - request.fetchLimit = 200 - //request.fetchBatchSize = 1 + request.fetchLimit = 100 + request.fetchBatchSize = 1 request.returnsObjectsAsFaults = false request.includesSubentities = true request.returnsDistinctResults = true diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 7f1e803e..7b144fe1 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -166,7 +166,6 @@ struct MeshMap: View { .padding(5) } } - .navigationTitle("Mesh Map") .navigationBarItems(leading: MeshtasticLogo(), trailing: ZStack { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) From 515bc7fcd6faa927140ad3ae40616f0ea3503a49 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 31 Mar 2024 14:45:30 -0700 Subject: [PATCH 08/21] Finish hooking up favorites --- Meshtastic/Helpers/BLEManager.swift | 14 ------------- Meshtastic/Views/Messages/UserList.swift | 26 +++++++++++++++++++----- Meshtastic/Views/Nodes/NodeList.swift | 3 +-- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index abf65753..d5ca39ef 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -759,20 +759,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if fetchedNodeInfo.count == 1 && fetchedNodeInfo[0].storeForwardConfig?.enabled == true { wantStoreAndForwardPackets = true; } - if fetchedNodeInfo.count == 1 { - if !(fetchedNodeInfo[0].user?.vip ?? false) { - fetchedNodeInfo[0].favorite = true - fetchedNodeInfo[0].user?.vip = true - do { - try context!.save() - - } catch { - context!.rollback() - let nsError = error as NSError - print("💥 Core Data error. Error: \(nsError)") - } - } - } } catch { print("Failed to find a node info for the connected node") diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 978e8c08..3f10b3c0 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -33,7 +33,9 @@ struct UserList: View { } } @FetchRequest( - sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false), NSSortDescriptor(key: "vip", ascending: false), NSSortDescriptor(key: "longName", ascending: true)], + sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false), + NSSortDescriptor(key: "userNode.favorite", ascending: false), + NSSortDescriptor(key: "longName", ascending: true)], animation: .default) private var users: FetchedResults @@ -71,7 +73,7 @@ struct UserList: View { Text(user.longName ?? "unknown".localized) .font(.headline) Spacer() - if user.vip { + if (user.userNode?.favorite ?? false) { Image(systemName: "star.fill") .foregroundColor(.yellow) } @@ -108,15 +110,29 @@ struct UserList: View { .frame(height: 62) .contextMenu { Button { - user.vip = !user.vip + + if node != nil && !(user.userNode?.favorite ?? false) { + let success = bleManager.setFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num)) + if success { + user.userNode?.favorite = !(user.userNode?.favorite ?? true) + print("Favorited a node") + } + } else { + let success = bleManager.removeFavoriteNode(node: user.userNode!, connectedNodeNum: Int64(node!.num)) + if success { + user.userNode?.favorite = !(user.userNode?.favorite ?? true) + print("Favorited a node") + } + } + context.refresh(user, mergeChanges: true) do { try context.save() } catch { context.rollback() - print("💥 Save User VIP Error") + print("💥 Save Node Favorite Error") } } label: { - Label(user.vip ? "Un-Favorite" : "Favorite", systemImage: user.vip ? "star.slash.fill" : "star.fill") + Label((user.userNode?.favorite ?? false) ? "Un-Favorite" : "Favorite", systemImage: (user.userNode?.favorite ?? false) ? "star.slash.fill" : "star.fill") } Button { user.mute = !user.mute diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 1cb3508e..1dfec72d 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -51,8 +51,7 @@ struct NodeList: View { .contextMenu { Button { - node.user!.vip = !node.user!.vip - if !node.favorite { + if node.favorite { let success = bleManager.setFavoriteNode(node: node, connectedNodeNum: Int64(connectedNodeNum)) if success { node.favorite = !node.favorite From d64acccc3e119d18611ee06c29cd0010edc0c8d3 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 1 Apr 2024 10:51:06 -0700 Subject: [PATCH 09/21] Remove VIP, clean up add channel method --- Meshtastic.xcodeproj/project.pbxproj | 4 +- .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 460 ++++++++++++++++++ Meshtastic/Persistence/UpdateCoreData.swift | 1 - .../Map/MapContent/NodeMapContent.swift | 4 +- .../Nodes/Helpers/Map/PositionPopover.swift | 2 +- Meshtastic/Views/Settings/Channels.swift | 51 +- 7 files changed, 493 insertions(+), 31 deletions(-) create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV32.xcdatamodel/contents diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 094f1625..000b7ec6 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -374,6 +374,7 @@ DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointEntityExtension.swift; sourceTree = ""; }; DD964FC32974767D007C176F /* MapViewFitExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewFitExtension.swift; sourceTree = ""; }; DD964FC52975DBFD007C176F /* QueryCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryCoreData.swift; sourceTree = ""; }; + DD9681A22BBB22BE00FD2C47 /* MeshtasticDataModelV32.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV32.xcdatamodel; sourceTree = ""; }; DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticLogo.swift; sourceTree = ""; }; DD97E96728EFE9A00056DDA4 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = ""; }; DD994B68295F88B60013760A /* IntervalEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalEnums.swift; sourceTree = ""; }; @@ -1910,6 +1911,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD9681A22BBB22BE00FD2C47 /* MeshtasticDataModelV32.xcdatamodel */, DDDCD5712BB3246500BE6B60 /* MeshtasticDataModelV 31.xcdatamodel */, DD9A1A912BA2D2D3001E602E /* MeshtasticDataModelV 30.xcdatamodel */, DD398EBD2B93F640002B4C51 /* MeshtasticDataModelV 29.xcdatamodel */, @@ -1942,7 +1944,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DDDCD5712BB3246500BE6B60 /* MeshtasticDataModelV 31.xcdatamodel */; + currentVersion = DD9681A22BBB22BE00FD2C47 /* MeshtasticDataModelV32.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 825d8915..75e1f5e3 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 31.xcdatamodel + MeshtasticDataModelV32.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV32.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV32.xcdatamodel/contents new file mode 100644 index 00000000..21860be1 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV32.xcdatamodel/contents @@ -0,0 +1,460 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index f2933922..8841e7c2 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -236,7 +236,6 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].telemetries? = NSOrderedSet(array: newTelemetries) } if nodeInfoMessage.hasUser { - fetchedNode[0].user!.vip = nodeInfoMessage.isFavorite /// Seeing Some crashes here ? fetchedNode[0].user!.userId = nodeInfoMessage.user.id fetchedNode[0].user!.num = Int64(nodeInfoMessage.num) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift index f196bf07..83a5ccbc 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift @@ -128,13 +128,13 @@ struct NodeMapContent: MapContent { } } } - // .tag(position.time) + .tag(position.time) .annotationTitles(.automatic) .annotationSubtitles(.automatic) } /// Node History if showNodeHistory { - if position.latest == false && position.nodePosition?.user?.vip ?? false { + if position.latest == false && position.nodePosition?.favorite ?? false { let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 771)) let headingDegrees = Angle.degrees(Double(position.heading)) Annotation("", coordinate: position.coordinate) { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index b5fc9fd8..daa43ac9 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -162,7 +162,7 @@ struct PositionPopover: View { Spacer() VStack (alignment: .center) { if position.nodePosition != nil { - if position.nodePosition?.user?.vip ?? false { + if position.nodePosition?.favorite ?? false { Image(systemName: "star.fill") .foregroundColor(.yellow) .symbolRenderingMode(.hierarchical) diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 6c5fe5ed..81a5c39f 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -32,17 +32,17 @@ struct Channels: View { @State var hasChanges = false @State var hasValidKey = true @State private var isPresentingSaveConfirm: Bool = false - @State private var channelIndex: Int32 = 0 - @State private var channelName = "" - @State private var channelKeySize = 16 - @State private var channelKey = "AQ==" - @State private var channelRole = 0 - @State private var uplink = false - @State private var downlink = false - @State private var positionPrecision = 32.0 - @State private var preciseLocation = true - @State private var positionsEnabled = true - @State private var supportedVersion = true + @State var channelIndex: Int32 = 0 + @State var channelName = "" + @State var channelKeySize = 16 + @State var channelKey = "AQ==" + @State var channelRole = 0 + @State var uplink = false + @State var downlink = false + @State var positionPrecision = 32.0 + @State var preciseLocation = true + @State var positionsEnabled = true + @State var supportedVersion = true @State var selectedChannel: ChannelEntity? /// Minimum Version for granular position configuration @@ -150,26 +150,17 @@ struct Channels: View { channel.settings.downlinkEnabled = downlink channel.settings.moduleSettings.positionPrecision = UInt32(positionPrecision) - let newChannel = ChannelEntity(context: context) - newChannel.id = Int32(channel.index) - newChannel.index = Int32(channel.index) - newChannel.uplinkEnabled = channel.settings.uplinkEnabled - newChannel.downlinkEnabled = channel.settings.downlinkEnabled - newChannel.name = channel.settings.name - newChannel.role = Int32(channel.role.rawValue) - newChannel.psk = channel.settings.psk - newChannel.positionPrecision = Int32(positionPrecision) + guard let mutableChannels = node?.myInfo?.channels?.mutableCopy() as? NSMutableOrderedSet else { return } - if mutableChannels.contains(newChannel) { - mutableChannels.replaceObject(at: Int(newChannel.index), with: newChannel) + if mutableChannels.contains(selectedChannel as Any) { + mutableChannels.replaceObject(at: Int(channel.index), with: selectedChannel as Any) } else { - mutableChannels.add(newChannel) + mutableChannels.add(selectedChannel as Any) } node!.myInfo!.channels = mutableChannels.copy() as? NSOrderedSet - context.refresh(newChannel, mergeChanges: true) do { try context.save() print("💾 Saved Channel: \(channel.settings.name)") @@ -249,7 +240,17 @@ struct Channels: View { uplink = false downlink = false hasChanges = true - selectedChannel = ChannelEntity(context: context) + + let newChannel = ChannelEntity(context: context) + newChannel.id = channelIndex + newChannel.index = channelIndex + newChannel.uplinkEnabled = uplink + newChannel.downlinkEnabled = downlink + newChannel.name = channelName + newChannel.role = Int32(channelRole) + //newChannel.psk = channelKey + newChannel.positionPrecision = Int32(positionPrecision) + selectedChannel = newChannel } label: { Label("Add Channel", systemImage: "plus.square") From ee891f62c70e45b276fa6db13c5e120b6c753d80 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 1 Apr 2024 12:16:09 -0700 Subject: [PATCH 10/21] Add channel fixes --- Meshtastic/Views/Settings/Channels.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 81a5c39f..8dfddfd2 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -161,6 +161,7 @@ struct Channels: View { mutableChannels.add(selectedChannel as Any) } node!.myInfo!.channels = mutableChannels.copy() as? NSOrderedSet + context.refresh(selectedChannel!, mergeChanges: true) do { try context.save() print("💾 Saved Channel: \(channel.settings.name)") @@ -197,6 +198,8 @@ struct Channels: View { channelName = "" channelRole = 2 hasChanges = false + + _ = bleManager.getChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!) } } label: { Label("save", systemImage: "square.and.arrow.down") @@ -248,7 +251,7 @@ struct Channels: View { newChannel.downlinkEnabled = downlink newChannel.name = channelName newChannel.role = Int32(channelRole) - //newChannel.psk = channelKey + newChannel.psk = Data(base64Encoded: channelKey) ?? Data() newChannel.positionPrecision = Int32(positionPrecision) selectedChannel = newChannel From 78c0bca781b2c4c02ef63e6a679288ac0b211027 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 2 Apr 2024 06:17:03 -0700 Subject: [PATCH 11/21] Add favorite and distance filter to the node list --- .../CoreData/MyInfoEntityExtension.swift | 2 +- .../Views/Nodes/Helpers/NodeListFilter.swift | 65 ++++++++++++------- Meshtastic/Views/Nodes/NodeList.swift | 25 +++++-- Meshtastic/Views/Settings/Channels.swift | 6 +- .../Views/Settings/Channels/ChannelForm.swift | 3 +- 5 files changed, 62 insertions(+), 39 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift index 1e04282a..a9e22a39 100644 --- a/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift @@ -14,7 +14,7 @@ extension MyInfoEntity { } var unreadMessages: Int { - let unreadMessages = messageList.filter{ ($0 as AnyObject).read == false } + let unreadMessages = messageList.filter{ ($0 as AnyObject).read == false && ($0 as AnyObject).isEmoji == false } return unreadMessages.count } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift index 79574927..6726c90b 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -14,6 +14,7 @@ struct NodeListFilter: View { @Binding var viaLora: Bool @Binding var viaMqtt: Bool @Binding var isOnline: Bool + @Binding var isFavorite: Bool @Binding var distanceFilter: Bool @Binding var maximumDistance: Double @Binding var hopsAway: Int @@ -48,7 +49,7 @@ struct NodeListFilter: View { Toggle(isOn: $isOnline) { Label { - Text("Online Only") + Text("Online") } icon: { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) @@ -58,29 +59,43 @@ struct NodeListFilter: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .listRowSeparator(.visible) -// Toggle(isOn: $distanceFilter) { -// -// Label { -// Text("Distance") -// } icon: { -// Image(systemName: "map") -// } -// } -// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) -// -// .listRowSeparator(distanceFilter ? .hidden : .visible) -// if distanceFilter { -// HStack { -// Label("Show nodes", systemImage: "lines.measurement.horizontal") -// Picker("", selection: $maximumDistance) { -// ForEach(MeshMapDistances.allCases) { di in -// Text(di.description) -// .tag(di.id) -// } -// } -// .pickerStyle(DefaultPickerStyle()) -// } -// } + Toggle(isOn: $isFavorite) { + + Label { + Text("Favorites") + } icon: { + + Image(systemName: "star.fill") + .foregroundColor(.yellow) + .symbolRenderingMode(.hierarchical) + } + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + + Toggle(isOn: $distanceFilter) { + + Label { + Text("Distance") + } icon: { + Image(systemName: "map") + } + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + + .listRowSeparator(distanceFilter ? .hidden : .visible) + if distanceFilter { + HStack { + Label("Show nodes", systemImage: "lines.measurement.horizontal") + Picker("", selection: $maximumDistance) { + ForEach(MeshMapDistances.allCases) { di in + Text(di.description) + .tag(di.id) + } + } + .pickerStyle(DefaultPickerStyle()) + } + } HStack { Label("Hops Away", systemImage: "hare") Picker("", selection: $hopsAway) { @@ -125,7 +140,7 @@ struct NodeListFilter: View { .padding(.bottom) #endif } - .presentationDetents([.fraction(0.40), .fraction(0.50)]) + .presentationDetents([.fraction(0.6), .fraction(0.75)]) .presentationDragIndicator(.visible) } } diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 1dfec72d..8ecb1df8 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -20,6 +20,7 @@ struct NodeList: View { @State private var viaLora = true @State private var viaMqtt = true @State private var isOnline = false + @State private var isFavorite = false @State private var distanceFilter = false @State private var maxDistance: Double = 800000 @State private var hopsAway: Int = -1 @@ -33,7 +34,9 @@ struct NodeList: View { @EnvironmentObject var bleManager: BLEManager @FetchRequest( - sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false), NSSortDescriptor(key: "lastHeard", ascending: false)], + sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false), + NSSortDescriptor(key: "lastHeard", ascending: false), + NSSortDescriptor(key: "user.longName", ascending: true)], animation: .default) var nodes: FetchedResults @@ -164,7 +167,7 @@ struct NodeList: View { } } .sheet(isPresented: $isEditingFilters) { - NodeListFilter(viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, deviceRole: $deviceRole) + NodeListFilter(viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isFavorite: $isFavorite, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, deviceRole: $deviceRole) } .safeAreaInset(edge: .bottom, alignment: .trailing) { HStack { @@ -282,6 +285,12 @@ struct NodeList: View { .onChange(of: isOnline) { _ in searchNodeList() } + .onChange(of: isFavorite) { _ in + searchNodeList() + } + .onChange(of: maxDistance) { _ in + searchNodeList() + } .onAppear { if self.bleManager.context == nil { self.bleManager.context = context @@ -325,6 +334,11 @@ struct NodeList: View { let isOnlinePredicate = NSPredicate(format: "lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate) predicates.append(isOnlinePredicate) } + /// Favorites + if isFavorite { + let isFavoritePredicate = NSPredicate(format: "favorite == YES") + predicates.append(isFavoritePredicate) + } /// Distance if distanceFilter { let pointOfInterest = LocationHelper.currentLocation @@ -339,15 +353,12 @@ struct NodeList: View { let maxLatitude: Double = pointOfInterest.latitude + deltaLatitude let minLongitude: Double = pointOfInterest.longitude - deltaLongitude let maxLongitude: Double = pointOfInterest.longitude + deltaLongitude - let distancePredicate = NSPredicate(format: "(%lf <= (positions[first].longitudeI / 1e7))", minLongitude, maxLongitude,minLatitude, maxLatitude) - //let distancePredicate = NSPredicate(format: "(%lf <= (positions[LAST].longitudeI / 1e7)) AND ((positions[LAST].longitudeI / 1e7) <= %lf) AND (%lf <= (positions[LAST].latitudeI / 1e7)) AND ((positions[LAST].latitudeI / 1e7) <= %lf)", minLongitude, maxLongitude,minLatitude, maxLatitude) - - //predicates.append(distancePredicate) + let distancePredicate = NSPredicate(format: "(SUBQUERY(positions, $position, $position.latest == TRUE && (%lf <= ($position.longitudeI / 1e7)) AND (($position.longitudeI / 1e7) <= %lf) AND (%lf <= ($position.latitudeI / 1e7)) AND (($position.latitudeI / 1e7) <= %lf))).@count > 0", minLongitude, maxLongitude,minLatitude, maxLatitude) + predicates.append(distancePredicate) } } if predicates.count > 0 || !searchText.isEmpty { - if !searchText.isEmpty { let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates) nodes.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, filterPredicates]) diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 8dfddfd2..d2bd8cf5 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -134,6 +134,8 @@ struct Channels: View { .padding() #endif ChannelForm(channelIndex: $channelIndex, channelName: $channelName, channelKeySize: $channelKeySize, channelKey: $channelKey, channelRole: $channelRole, uplink: $uplink, downlink: $downlink, positionPrecision: $positionPrecision, preciseLocation: $preciseLocation, positionsEnabled: $positionsEnabled, hasChanges: $hasChanges, hasValidKey: $hasValidKey, supportedVersion: $supportedVersion) + .presentationDetents([.large]) + .presentationDragIndicator(.visible) .onAppear { supportedVersion = bleManager.connectedVersion == "0.0.0" || self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame } @@ -149,8 +151,6 @@ struct Channels: View { channel.settings.uplinkEnabled = uplink channel.settings.downlinkEnabled = downlink channel.settings.moduleSettings.positionPrecision = UInt32(positionPrecision) - - guard let mutableChannels = node?.myInfo?.channels?.mutableCopy() as? NSMutableOrderedSet else { return @@ -221,8 +221,6 @@ struct Channels: View { .padding(.bottom) #endif } - .presentationDetents([.fraction(0.85), .large]) - .presentationDragIndicator(.visible) } if node?.myInfo?.channels?.array.count ?? 0 < 8 && node != nil { diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index da6127df..09238d49 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -105,6 +105,7 @@ struct ChannelForm: View { ) .onChange(of: channelKey, perform: { _ in + let tempKey = Data(base64Encoded: channelKey) ?? Data() if tempKey.count == channelKeySize || channelKeySize == -1{ hasValidKey = true @@ -245,7 +246,5 @@ struct ChannelForm: View { } } } - .presentationDetents([.large]) - .presentationDragIndicator(.visible) } } From ab82848d9993d8d21a72c5768cd9060ca2623629 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 2 Apr 2024 06:20:48 -0700 Subject: [PATCH 12/21] filter update --- Meshtastic/Views/Nodes/NodeList.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 8ecb1df8..5e51314f 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -291,6 +291,9 @@ struct NodeList: View { .onChange(of: maxDistance) { _ in searchNodeList() } + .onChange(of: distanceFilter) { _ in + searchNodeList() + } .onAppear { if self.bleManager.context == nil { self.bleManager.context = context From e68734135bf97fc288aae4b99493e2cae60772bb Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 2 Apr 2024 07:00:07 -0700 Subject: [PATCH 13/21] Add node num to text search --- Meshtastic/Helpers/MeshPackets.swift | 2 ++ .../MeshtasticDataModelV32.xcdatamodel/contents | 1 + Meshtastic/Views/Nodes/NodeList.swift | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index a408af14..bbc57937 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -293,6 +293,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje } else { let newUser = UserEntity(context: context) newUser.num = Int64(nodeInfo.num) + newUser.numString = String(nodeInfo.num) let userId = String(format:"%2X", nodeInfo.num) newUser.userId = "!\(userId)" let last4 = String(userId.suffix(4)) @@ -357,6 +358,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje } fetchedNode[0].user!.userId = nodeInfo.user.id fetchedNode[0].user!.num = Int64(nodeInfo.num) + fetchedNode[0].user!.numString = String(nodeInfo.num) fetchedNode[0].user!.longName = nodeInfo.user.longName fetchedNode[0].user!.shortName = nodeInfo.user.shortName fetchedNode[0].user!.isLicensed = nodeInfo.user.isLicensed diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV32.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV32.xcdatamodel/contents index 21860be1..5d065d55 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV32.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV32.xcdatamodel/contents @@ -424,6 +424,7 @@ + diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 5e51314f..866d4f78 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -304,7 +304,7 @@ struct NodeList: View { private func searchNodeList() { /// Case Insensitive Search Text Predicates - let searchPredicates = ["user.userId", "user.hwModel", "user.longName", "user.shortName"].map { property in + let searchPredicates = ["user.userId", "user.numString", "user.hwModel", "user.longName", "user.shortName"].map { property in return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText) } /// Create a compound predicate using each text search preicate as an OR From 8a214d93eb69c0939abf5dbf1519edb633049524 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 2 Apr 2024 11:16:32 -0700 Subject: [PATCH 14/21] Contact list filters --- Meshtastic/Enums/AppSettingsEnums.swift | 4 +- .../CoreData/PositionEntityExtension.swift | 3 +- .../Protobufs/meshtastic/admin.pb.swift | 4 +- Meshtastic/Protobufs/meshtastic/atak.pb.swift | 2 +- .../Protobufs/meshtastic/config.pb.swift | 8 +- Meshtastic/Protobufs/meshtastic/mesh.pb.swift | 6 +- .../Protobufs/meshtastic/telemetry.pb.swift | 4 +- Meshtastic/Views/Messages/UserList.swift | 159 ++++++++++++++++-- .../Nodes/Helpers/Map/NodeMapSwiftUI.swift | 2 +- .../Views/Nodes/Helpers/NodeListFilter.swift | 3 +- Meshtastic/Views/Nodes/MeshMap.swift | 2 +- de.lproj/Localizable.strings | 1 + en.lproj/Localizable.strings | 2 + fr.lproj/Localizable.strings | 1 + he.lproj/Localizable.strings | 1 + pl.lproj/Localizable.strings | 1 + protobufs | 2 +- zh-Hans.lproj/Localizable.strings | 1 + zh-Hant-TW.lproj/Localizable.strings | 1 + 19 files changed, 173 insertions(+), 34 deletions(-) diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index 4b0d619f..3dc3d49c 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -56,10 +56,12 @@ enum MeshMapDistances: Double, CaseIterable, Identifiable { case twoHundredMiles = 321869 case fiveHundredMiles = 804672 case oneThousandMiles = 1609000 + case fifteenHundredMiles = 2414016 + case twentyFiveHundredMiles = 4023360 var id: Double { self.rawValue } var description: String { let distanceFormatter = MKDistanceFormatter() - return "up to \(distanceFormatter.string(fromDistance: Double(self.rawValue))) away" + return String.localizedStringWithFormat("nodelist.filter.distance %@".localized, distanceFormatter.string(fromDistance: Double(self.rawValue))) } } diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index 2643b242..680b88bc 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -15,12 +15,11 @@ extension PositionEntity { static func allPositionsFetchRequest() -> NSFetchRequest { let request: NSFetchRequest = PositionEntity.fetchRequest() request.fetchLimit = 100 - request.fetchBatchSize = 1 request.returnsObjectsAsFaults = false request.includesSubentities = true request.returnsDistinctResults = true request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)] - let positionPredicate = NSPredicate(format: "nodePosition != nil && (nodePosition.user.shortName != nil || nodePosition.user.shortName != '') && latest == true && time >= %@", Calendar.current.date(byAdding: .day, value: -2, to: Date())! as NSDate) + let positionPredicate = NSPredicate(format: "nodePosition != nil && (nodePosition.user.shortName != nil || nodePosition.user.shortName != '') && latest == true") let pointOfInterest = LocationHelper.currentLocation diff --git a/Meshtastic/Protobufs/meshtastic/admin.pb.swift b/Meshtastic/Protobufs/meshtastic/admin.pb.swift index fa1ac990..1f2b193e 100644 --- a/Meshtastic/Protobufs/meshtastic/admin.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/admin.pb.swift @@ -350,7 +350,7 @@ struct AdminMessage { } /// - /// Clear fixed position coordinates and then set position.fixed_position = false + /// Clear fixed position coordinates and then set position.fixed_position = false var removeFixedPosition: Bool { get { if case .removeFixedPosition(let v)? = payloadVariant {return v} @@ -547,7 +547,7 @@ struct AdminMessage { /// Set fixed position data on the node and then set the position.fixed_position = true case setFixedPosition(Position) /// - /// Clear fixed position coordinates and then set position.fixed_position = false + /// Clear fixed position coordinates and then set position.fixed_position = false case removeFixedPosition(Bool) /// /// Begins an edit transaction for config, module config, owner, and channel settings changes diff --git a/Meshtastic/Protobufs/meshtastic/atak.pb.swift b/Meshtastic/Protobufs/meshtastic/atak.pb.swift index 31cf5313..f1bc14ad 100644 --- a/Meshtastic/Protobufs/meshtastic/atak.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/atak.pb.swift @@ -255,7 +255,7 @@ extension MemberRole: CaseIterable { #endif // swift(>=4.2) /// -/// Packets for the official ATAK Plugin +/// Packets for the official ATAK Plugin struct TAKPacket { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index 7506bc88..cb99fded 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -226,14 +226,14 @@ struct Config { /// /// 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, + /// 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 /// /// 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, + /// 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 @@ -249,12 +249,12 @@ struct Config { /// 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 + /// Can be used for clandestine operation or to dramatically reduce airtime / power consumption case clientHidden // = 8 /// /// 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 + /// 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 diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index 1ae038f4..a881d288 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -1565,8 +1565,8 @@ struct MeshPacket { set {_uniqueStorage()._viaMqtt = newValue} } - /// - /// Hop limit with which the original packet started. Sent via LoRa using three bits in the unencrypted header. + /// + /// Hop limit with which the original packet started. Sent via LoRa using three bits in the unencrypted header. /// When receiving a packet, the difference between hop_start and hop_limit gives how many hops it traveled. var hopStart: UInt32 { get {return _storage._hopStart} @@ -2606,7 +2606,7 @@ struct DeviceMetadata { init() {} } -/// +/// /// A heartbeat message is sent to the node from the client to keep the connection alive. /// This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI. struct Heartbeat { diff --git a/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift b/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift index 72d378bc..aa2ebae4 100644 --- a/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift @@ -370,7 +370,7 @@ struct Telemetry { } /// - /// Power Metrics + /// Power Metrics var powerMetrics: PowerMetrics { get { if case .powerMetrics(let v)? = variant {return v} @@ -392,7 +392,7 @@ struct Telemetry { /// Air quality metrics case airQualityMetrics(AirQualityMetrics) /// - /// Power Metrics + /// Power Metrics case powerMetrics(PowerMetrics) #if !swift(>=4.1) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 3f10b3c0..35382a13 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -17,21 +17,16 @@ struct UserList: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @State private var searchText = "" + @State private var viaLora = true + @State private var viaMqtt = true + @State private var isOnline = false + @State private var isFavorite = false + @State private var distanceFilter = false + @State private var maxDistance: Double = 800000 + @State private var hopsAway: Int = -1 + @State private var deviceRole: Int = -1 + @State var isEditingFilters = false - var usersQuery: Binding { - Binding { - searchText - } set: { newValue in - searchText = newValue - /// Case Insensitive Search Text Predicates - let searchPredicates = ["userId", "hwModel", "longName", "shortName"].map { property in - return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText) - } - /// Create a compound predicate using each text search predicate as an OR - let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates) - users.nsPredicate = newValue.isEmpty ? nil : textSearchPredicate - } - } @FetchRequest( sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false), NSSortDescriptor(key: "userNode.favorite", ascending: false), @@ -172,9 +167,143 @@ struct UserList: View { } .listStyle(.plain) .navigationTitle(String.localizedStringWithFormat("contacts %@".localized, String(users.count == 0 ? 0 : users.count - 1))) - .searchable(text: usersQuery, placement: users.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a contact") + .sheet(isPresented: $isEditingFilters) { + NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isFavorite: $isFavorite, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, deviceRole: $deviceRole) + } + .onChange(of: searchText) { _ in + searchUserList() + } + .onChange(of: viaLora) { _ in + if !viaLora && !viaMqtt { + viaMqtt = true + } + searchUserList() + } + .onChange(of: viaMqtt) { _ in + if !viaLora && !viaMqtt { + viaLora = true + } + searchUserList() + } + .onChange(of: deviceRole) { _ in + searchUserList() + } + .onChange(of: hopsAway) { _ in + searchUserList() + } + .onChange(of: isOnline) { _ in + searchUserList() + } + .onChange(of: isFavorite) { _ in + searchUserList() + } + .onChange(of: maxDistance) { _ in + searchUserList() + } + .onChange(of: distanceFilter) { _ in + searchUserList() + } + .onAppear { + if self.bleManager.context == nil { + self.bleManager.context = context + } + searchUserList() + } + .safeAreaInset(edge: .bottom, alignment: .trailing) { + HStack { + Button(action: { + withAnimation { + isEditingFilters = !isEditingFilters + } + }) { + Image(systemName: !isEditingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + + } + .controlSize(.regular) + .padding(5) + } + .padding(.bottom, 5) + .searchable(text: $searchText, placement: users.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a contact") .disableAutocorrection(true) .scrollDismissesKeyboard(.immediately) } } + + private func searchUserList() { + + /// Case Insensitive Search Text Predicates + let searchPredicates = ["userId", "numString", "hwModel", "longName", "shortName"].map { property in + return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText) + } + /// Create a compound predicate using each text search preicate as an OR + let textSearchPredicate = NSCompoundPredicate(type: .or, subpredicates: searchPredicates) + /// Create an array of predicates to hold our AND predicates + var predicates: [NSPredicate] = [] + /// Mqtt + if !(viaLora && viaMqtt) { + if viaLora { + let loraPredicate = NSPredicate(format: "userNode.viaMqtt == NO") + predicates.append(loraPredicate) + } else { + let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES") + predicates.append(mqttPredicate) + } + } + /// Role + if deviceRole > -1 { + let rolePredicate = NSPredicate(format: "role == %i", Int32(deviceRole)) + predicates.append(rolePredicate) + } + /// Hops Away + if hopsAway > 0 { + let hopsAwayPredicate = NSPredicate(format: "userNode.hopsAway == %i", Int32(hopsAway)) + predicates.append(hopsAwayPredicate) + } + + /// Online + if isOnline { + let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate) + predicates.append(isOnlinePredicate) + } + /// Favorites + if isFavorite { + let isFavoritePredicate = NSPredicate(format: "userNode.favorite == YES") + predicates.append(isFavoritePredicate) + } + /// Distance + if distanceFilter { + let pointOfInterest = LocationHelper.currentLocation + + if pointOfInterest.latitude != LocationHelper.DefaultLocation.latitude && pointOfInterest.longitude != LocationHelper.DefaultLocation.longitude { + let D: Double = maxDistance * 1.1 + let R: Double = 6371009 + let meanLatitidue = pointOfInterest.latitude * .pi / 180 + let deltaLatitude = D / R * 180 / .pi + let deltaLongitude = D / (R * cos(meanLatitidue)) * 180 / .pi + let minLatitude: Double = pointOfInterest.latitude - deltaLatitude + let maxLatitude: Double = pointOfInterest.latitude + deltaLatitude + let minLongitude: Double = pointOfInterest.longitude - deltaLongitude + let maxLongitude: Double = pointOfInterest.longitude + deltaLongitude + let distancePredicate = NSPredicate(format: "(SUBQUERY(userNode.positions, $position, $position.latest == TRUE && (%lf <= ($position.longitudeI / 1e7)) AND (($position.longitudeI / 1e7) <= %lf) AND (%lf <= ($position.latitudeI / 1e7)) AND (($position.latitudeI / 1e7) <= %lf))).@count > 0", minLongitude, maxLongitude,minLatitude, maxLatitude) + predicates.append(distancePredicate) + } + } + + if predicates.count > 0 || !searchText.isEmpty { + if !searchText.isEmpty { + let filterPredicates = NSCompoundPredicate(type: .and, subpredicates: predicates) + users.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: [textSearchPredicate, filterPredicates]) + } else { + users.nsPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates) + } + } else { + users.nsPredicate = nil + } + + } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 9cbb352b..cf983edb 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -138,7 +138,7 @@ struct NodeMapSwiftUI: View { } } } - .safeAreaInset(edge: .bottom, alignment: UIDevice.current.userInterfaceIdiom == .phone ? .leading : .trailing) { + .safeAreaInset(edge: .bottom, alignment: .trailing) { HStack { Button(action: { withAnimation { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift index 6726c90b..321054c1 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -11,6 +11,7 @@ import SwiftUI struct NodeListFilter: View { @Environment(\.dismiss) private var dismiss /// Filters + var filterTitle = "Node Filters" @Binding var viaLora: Bool @Binding var viaMqtt: Bool @Binding var isOnline: Bool @@ -24,7 +25,7 @@ struct NodeListFilter: View { NavigationStack { Form { - Section(header: Text("Node Filters")) { + Section(header: Text(filterTitle)) { Toggle(isOn: $viaLora) { Label { diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 7b144fe1..5a848908 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -148,7 +148,7 @@ struct MeshMap: View { return } } - .safeAreaInset(edge: .bottom, alignment: UIDevice.current.userInterfaceIdiom == .phone ? .leading : .trailing) { + .safeAreaInset(edge: .bottom, alignment: .trailing) { HStack { Button(action: { withAnimation { diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index bf9e8d39..54b004f6 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -239,6 +239,7 @@ "network.config"="Netzwerkeinstellungen"; "nodes"="Nodes"; "nodes %@"="Nodes (%@)"; +"nodelist.filter.distance %@"="up to %@ away"; "no.nodes"="Keine Meshtastic Nodes gefunden"; "not.connected"="Kein Gerät verbunden"; "numbers.punctuation"="Ziffern und Interpunktion"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index e031fd63..b395eb99 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -245,6 +245,8 @@ "network.config"="Network Config"; "nodes"="Nodes"; "nodes %@"="Nodes (%@)"; +"nodelist.filter.distance %@"="up to %@ away"; +"save.config %@"="Save Config for %@"; "no.nodes"="No Meshtastic Nodes Found"; "not.connected"="No device connected"; "numbers.punctuation"="Numbers and Punctuation"; diff --git a/fr.lproj/Localizable.strings b/fr.lproj/Localizable.strings index 34a507da..717d5997 100644 --- a/fr.lproj/Localizable.strings +++ b/fr.lproj/Localizable.strings @@ -219,6 +219,7 @@ "network.config"="Configuration du réseau"; "nodes"="Noeuds"; "nodes %@"="Noeuds (%@)"; +"nodelist.filter.distance %@"="up to %@ away"; "no.nodes"="Aucun noeud Meshtastic trouvé"; "not.connected"="Aucun appareil connecté"; "numbers.punctuation"="Nombres and Ponctuation"; diff --git a/he.lproj/Localizable.strings b/he.lproj/Localizable.strings index d7e0a05d..ce0d9316 100644 --- a/he.lproj/Localizable.strings +++ b/he.lproj/Localizable.strings @@ -243,6 +243,7 @@ "network.config"="הגדרות רשת"; "nodes"="מכשירים"; "nodes %@"="מכשירים (%@)"; +"nodelist.filter.distance %@"="up to %@ away"; "no.nodes"="לא נמצאו מכשירי משטסטיק"; "not.connected"="אין מכשיר מחובר"; "numbers.punctuation"="מספרים וסימני פיסוק "; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index 6a3b18b8..82b7aa4c 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -240,6 +240,7 @@ "network"="Sieć"; "network.config"="Konfiguracja sieci"; "nodes %@"="Węzły (%@)"; +"nodelist.filter.distance %@"="up to %@ away"; "no.nodes"="Nie znaleziono węzłów Meshtastic"; "not.connected"="Brak podłączonych urządzeń"; "numbers.punctuation"="Cyfry i interpunkcja"; diff --git a/protobufs b/protobufs index dea3a82e..e6b4c590 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit dea3a82ef2accd25112b4ef1c6f8991b579740f4 +Subproject commit e6b4c590e7c489306c9c44e3ad1fcf62a3efd288 diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 886cab0a..9a424c0e 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -239,6 +239,7 @@ "network.config"="网络配置"; "nodes"="节点"; "nodes %@"="节点 (%@)"; +"nodelist.filter.distance %@"="up to %@ away"; "no.nodes"="未找到 Meshtastic 节点"; "not.connected"="未连接到电台"; "numbers.punctuation"="数字和标点符号"; diff --git a/zh-Hant-TW.lproj/Localizable.strings b/zh-Hant-TW.lproj/Localizable.strings index 68562e56..4a557826 100644 --- a/zh-Hant-TW.lproj/Localizable.strings +++ b/zh-Hant-TW.lproj/Localizable.strings @@ -238,6 +238,7 @@ "network.config"="網路設定"; "nodes"="中繼點"; "nodes %@"="中繼點 (%@)"; +"nodelist.filter.distance %@"="up to %@ away"; "no.nodes"="未找到 Meshtastic 中繼點"; "not.connected"="未連接到電台"; "numbers.punctuation"="數字和標點符號"; From 6b769ddb5ba387c98f1b02e2919dd2348c058fb4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 2 Apr 2024 17:10:42 -0700 Subject: [PATCH 15/21] Touch up favorites --- Meshtastic/Views/Messages/UserList.swift | 1 - Meshtastic/Views/Nodes/NodeList.swift | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 35382a13..03dcf882 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -304,6 +304,5 @@ struct UserList: View { } else { users.nsPredicate = nil } - } } diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 866d4f78..4eb8b001 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -54,7 +54,8 @@ struct NodeList: View { .contextMenu { Button { - if node.favorite { + if !node.favorite { + let success = bleManager.setFavoriteNode(node: node, connectedNodeNum: Int64(connectedNodeNum)) if success { node.favorite = !node.favorite From 6a41cf63befa37c9612db5728c7e5a83a7f5a982 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 2 Apr 2024 21:49:36 -0700 Subject: [PATCH 16/21] Put qr code experiment in place, fix up channel editor --- Meshtastic/Extensions/Url.swift | 29 ++++++++---- Meshtastic/Helpers/BLEManager.swift | 17 ++++--- Meshtastic/MeshtasticApp.swift | 11 +++-- Meshtastic/Views/Settings/Channels.swift | 44 ++++++++++--------- .../Views/Settings/SaveChannelQRCode.swift | 3 +- Meshtastic/Views/Settings/ShareChannels.swift | 2 +- 6 files changed, 67 insertions(+), 39 deletions(-) diff --git a/Meshtastic/Extensions/Url.swift b/Meshtastic/Extensions/Url.swift index a8589e55..20d3ca6e 100644 --- a/Meshtastic/Extensions/Url.swift +++ b/Meshtastic/Extensions/Url.swift @@ -8,13 +8,26 @@ import Foundation extension URL { - - func regularFileAllocatedSize() throws -> UInt64 { - let resourceValues = try self.resourceValues(forKeys: allocatedSizeResourceKeys) - - guard resourceValues.isRegularFile ?? false else { - return 0 + + func regularFileAllocatedSize() throws -> UInt64 { + let resourceValues = try self.resourceValues(forKeys: allocatedSizeResourceKeys) + + guard resourceValues.isRegularFile ?? false else { + return 0 + } + return UInt64(resourceValues.totalFileAllocatedSize ?? resourceValues.fileAllocatedSize ?? 0) + } + subscript(queryParam: String) -> String? { + guard let url = URLComponents(string: self.absoluteString) else { return nil } + if let parameters = url.queryItems { + return parameters.first(where: { $0.name == queryParam })?.value + } else if let paramPairs = url.fragment?.components(separatedBy: "?").last?.components(separatedBy: "&") { + for pair in paramPairs where pair.contains(queryParam) { + return pair.components(separatedBy: "=").last + } + return nil + } else { + return nil + } } - return UInt64(resourceValues.totalFileAllocatedSize ?? resourceValues.fileAllocatedSize ?? 0) - } } diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index d5ca39ef..c28363e1 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1304,13 +1304,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return 0 } - public func saveChannelSet(base64UrlString: String) -> Bool { + public func saveChannelSet(base64UrlString: String, addChannel: Bool = false) -> Bool { if isConnected { // Before we get started delete the existing channels from the myNodeInfo let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedPeripheral.num)) - - tryClearExistingChannels() + if !addChannel { + tryClearExistingChannels() + } let decodedString = base64UrlString.base64urlToBase64() if let decodedData = Data(base64Encoded: decodedString) { do { @@ -1318,14 +1319,18 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var i: Int32 = 0 for cs in channelSet.settings { var chan = Channel() - if i == 0 { + + if i == 0 && !addChannel { chan.role = Channel.Role.primary + } else { chan.role = Channel.Role.secondary } chan.settings = cs - chan.index = i - i += 1 + if !addChannel { + chan.index = i + i += 1 + } var adminPacket = AdminMessage() adminPacket.setChannel = chan var meshPacket: MeshPacket = MeshPacket() diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index e21b96e9..765969d4 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -18,6 +18,7 @@ struct MeshtasticAppleApp: App { @State var saveChannels = false @State var incomingUrl: URL? @State var channelSettings: String? + @State var addChannel = false @StateObject var appState = AppState.shared var body: some Scene { @@ -26,7 +27,7 @@ struct MeshtasticAppleApp: App { .environment(\.managedObjectContext, persistenceController.container.viewContext) .environmentObject(bleManager) .sheet(isPresented: $saveChannels) { - SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", bleManager: bleManager) + SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", addChannel: addChannel, bleManager: bleManager) .presentationDetents([.medium, .large]) .presentationDragIndicator(.visible) } @@ -36,9 +37,13 @@ struct MeshtasticAppleApp: App { self.incomingUrl = userActivity.webpageURL if (self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/e/#")) != nil { - if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") { - self.channelSettings = components.last! + guard let cs = components.last!.components(separatedBy: "?").first else { + return + } + self.channelSettings = cs + self.addChannel = Bool(self.incomingUrl?["add"] ?? "false") ?? false + print("Add Channel \(self.addChannel)") } self.saveChannels = true print("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")") diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index d2bd8cf5..2581ee15 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -151,6 +151,14 @@ struct Channels: View { channel.settings.uplinkEnabled = uplink channel.settings.downlinkEnabled = downlink channel.settings.moduleSettings.positionPrecision = UInt32(positionPrecision) + + selectedChannel!.role = Int32(channelRole) + selectedChannel!.index = channelIndex + selectedChannel!.name = channelName + selectedChannel!.psk = Data(base64Encoded: channelKey) ?? Data() + selectedChannel!.uplinkEnabled = uplink + selectedChannel!.downlinkEnabled = downlink + selectedChannel!.positionPrecision = Int32(positionPrecision) guard let mutableChannels = node?.myInfo?.channels?.mutableCopy() as? NSMutableOrderedSet else { return @@ -160,7 +168,7 @@ struct Channels: View { } else { mutableChannels.add(selectedChannel as Any) } - node!.myInfo!.channels = mutableChannels.copy() as? NSOrderedSet + node?.myInfo?.channels = mutableChannels.copy() as? NSOrderedSet context.refresh(selectedChannel!, mergeChanges: true) do { try context.save() @@ -171,23 +179,21 @@ struct Channels: View { print("💥 Unresolved Core Data error in the channel editor. Error: \(nsError)") } } else { - if channelIndex <= node!.myInfo!.channels?.count ?? 0 { - guard let channelEntity = node!.myInfo!.channels?[Int(channelIndex)] as? ChannelEntity else { - return - } - let objects = channelEntity.allPrivateMessages - for object in objects { - context.delete(object) - } - context.delete(channelEntity) - do { - try context.save() - print("💾 Deleted Channel: \(channel.settings.name)") - } catch { - context.rollback() - let nsError = error as NSError - print("💥 Unresolved Core Data error in the channel editor. Error: \(nsError)") - } + guard let channelEntity = node?.myInfo?.channels?.first(where: { ($0 as! ChannelEntity).index == channelIndex }) else { + return + } + let objects = (channelEntity as! ChannelEntity).allPrivateMessages + for object in objects { + context.delete(object) + } + context.delete(channelEntity as! ChannelEntity) + do { + try context.save() + print("💾 Deleted Channel: \(channel.settings.name)") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Unresolved Core Data error in the channel editor. Error: \(nsError)") } } @@ -198,8 +204,6 @@ struct Channels: View { channelName = "" channelRole = 2 hasChanges = false - - _ = bleManager.getChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!) } } label: { Label("save", systemImage: "square.and.arrow.down") diff --git a/Meshtastic/Views/Settings/SaveChannelQRCode.swift b/Meshtastic/Views/Settings/SaveChannelQRCode.swift index 7a825bb5..a44e9e01 100644 --- a/Meshtastic/Views/Settings/SaveChannelQRCode.swift +++ b/Meshtastic/Views/Settings/SaveChannelQRCode.swift @@ -11,6 +11,7 @@ struct SaveChannelQRCode: View { @Environment(\.dismiss) private var dismiss var channelSetLink: String + var addChannel: Bool = false var bleManager: BLEManager @State var connectedToDevice = false @@ -26,7 +27,7 @@ struct SaveChannelQRCode: View { HStack { Button { - let success = bleManager.saveChannelSet(base64UrlString: channelSetLink) + let success = bleManager.saveChannelSet(base64UrlString: channelSetLink, addChannel: addChannel) if success { dismiss() } diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index 8fa6bf32..0c17e07b 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -272,7 +272,7 @@ struct ShareChannels: View { } } let settingsString = try! channelSet.serializedData().base64EncodedString() - channelsUrl = ("https://meshtastic.org/e/#" + settingsString.base64ToBase64url()) + channelsUrl = ("https://meshtastic.org/e/#" + settingsString.base64ToBase64url())// + "?add=true") } } } From 2a38dbbb1cfab3b25e9bd1ae9b6b62c10ed5d0bd Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 2 Apr 2024 23:43:00 -0700 Subject: [PATCH 17/21] Wire up adding instead of replacing channels via qr code. --- Meshtastic/Helpers/BLEManager.swift | 33 +++++++++++++------ Meshtastic/Views/Settings/ShareChannels.swift | 21 +++++++++--- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index c28363e1..b4b508f0 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1306,31 +1306,44 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func saveChannelSet(base64UrlString: String, addChannel: Bool = false) -> Bool { if isConnected { + + var i: Int32 = 0 // Before we get started delete the existing channels from the myNodeInfo - let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedPeripheral.num)) if !addChannel { tryClearExistingChannels() + } else { + // We are trying to add a channel so lets get the last index + let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedPeripheral.num)) + do { + let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as? [MyInfoEntity] ?? [] + if fetchedMyInfo.count == 1 { + if addChannel { + i = Int32(fetchedMyInfo[0].channels?.count ?? -1) + // Bail out if the index is negative or bigger than our max of 8 + if i < 0 || i > 8 { + return false + } + } + } + } catch { + print("Failed to find a node MyInfo to save these channels to") + } } let decodedString = base64UrlString.base64urlToBase64() if let decodedData = Data(base64Encoded: decodedString) { do { let channelSet: ChannelSet = try ChannelSet(serializedData: decodedData) - var i: Int32 = 0 for cs in channelSet.settings { var chan = Channel() - - if i == 0 && !addChannel { + if i == 0 { chan.role = Channel.Role.primary - } else { chan.role = Channel.Role.secondary } chan.settings = cs - if !addChannel { - chan.index = i - i += 1 - } + chan.index = i + i += 1 var adminPacket = AdminMessage() adminPacket.setChannel = chan var meshPacket: MeshPacket = MeshPacket() diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index 0c17e07b..627b7874 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -46,6 +46,7 @@ struct ShareChannels: View { @State var includeChannel5 = true @State var includeChannel6 = true @State var includeChannel7 = true + @State var replaceChannels = true var node: NodeInfoEntity? @State private var channelsUrl = "https://www.meshtastic.org/e/#" var qrCodeImage = QrCodeImage() @@ -53,9 +54,9 @@ struct ShareChannels: View { var body: some View { if #available(iOS 17.0, macOS 14.0, *) { - VStack { - TipView(ShareChannelsTip(), arrowEdge: .bottom) - } +// VStack { +// TipView(ShareChannelsTip(), arrowEdge: .bottom) +// } } GeometryReader { bounds in let smallest = min(bounds.size.width, bounds.size.height) @@ -191,6 +192,17 @@ struct ShareChannels: View { let qrImage = qrCodeImage.generateQRCode(from: channelsUrl) VStack { if node != nil { + Toggle(isOn: $replaceChannels) { + Label(replaceChannels ? "Replace Channels" : "Add Channels", systemImage: replaceChannels ? "arrow.triangle.2.circlepath.circle" : "plus.app") + } + .tint(.accentColor) + .toggleStyle(.button) + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.top) + .padding(.bottom) + ShareLink("Share QR Code & Link", item: Image(uiImage: qrImage), subject: Text("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you"), @@ -235,6 +247,7 @@ struct ShareChannels: View { .onChange(of: includeChannel5) { _ in generateChannelSet() } .onChange(of: includeChannel6) { _ in generateChannelSet() } .onChange(of: includeChannel7) { _ in generateChannelSet() } + .onChange(of: replaceChannels) { _ in generateChannelSet() } } } func generateChannelSet() { @@ -272,7 +285,7 @@ struct ShareChannels: View { } } let settingsString = try! channelSet.serializedData().base64EncodedString() - channelsUrl = ("https://meshtastic.org/e/#" + settingsString.base64ToBase64url())// + "?add=true") + channelsUrl = ("https://meshtastic.org/e/#" + settingsString.base64ToBase64url() + (replaceChannels ? "" : "?add=true")) } } } From 85b8c0b0910c7ac574b6121ae3b441e5d4bf66f6 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 3 Apr 2024 00:04:46 -0700 Subject: [PATCH 18/21] Yolo --- Meshtastic/Helpers/BLEManager.swift | 6 +++--- Meshtastic/MeshtasticApp.swift | 8 ++++---- Meshtastic/Views/Settings/SaveChannelQRCode.swift | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index b4b508f0..aaa664ff 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1304,12 +1304,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return 0 } - public func saveChannelSet(base64UrlString: String, addChannel: Bool = false) -> Bool { + public func saveChannelSet(base64UrlString: String, addChannels: Bool = false) -> Bool { if isConnected { var i: Int32 = 0 // Before we get started delete the existing channels from the myNodeInfo - if !addChannel { + if !addChannels { tryClearExistingChannels() } else { // We are trying to add a channel so lets get the last index @@ -1318,7 +1318,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate do { let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as? [MyInfoEntity] ?? [] if fetchedMyInfo.count == 1 { - if addChannel { + if addChannels { i = Int32(fetchedMyInfo[0].channels?.count ?? -1) // Bail out if the index is negative or bigger than our max of 8 if i < 0 || i > 8 { diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 765969d4..aeb6488c 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -18,7 +18,7 @@ struct MeshtasticAppleApp: App { @State var saveChannels = false @State var incomingUrl: URL? @State var channelSettings: String? - @State var addChannel = false + @State var addChannels = false @StateObject var appState = AppState.shared var body: some Scene { @@ -27,7 +27,7 @@ struct MeshtasticAppleApp: App { .environment(\.managedObjectContext, persistenceController.container.viewContext) .environmentObject(bleManager) .sheet(isPresented: $saveChannels) { - SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", addChannel: addChannel, bleManager: bleManager) + SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", addChannels: addChannels, bleManager: bleManager) .presentationDetents([.medium, .large]) .presentationDragIndicator(.visible) } @@ -42,8 +42,8 @@ struct MeshtasticAppleApp: App { return } self.channelSettings = cs - self.addChannel = Bool(self.incomingUrl?["add"] ?? "false") ?? false - print("Add Channel \(self.addChannel)") + self.addChannels = Bool(self.incomingUrl?["add"] ?? "false") ?? false + print("Add Channel \(self.addChannels)") } self.saveChannels = true print("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")") diff --git a/Meshtastic/Views/Settings/SaveChannelQRCode.swift b/Meshtastic/Views/Settings/SaveChannelQRCode.swift index a44e9e01..f9ca2557 100644 --- a/Meshtastic/Views/Settings/SaveChannelQRCode.swift +++ b/Meshtastic/Views/Settings/SaveChannelQRCode.swift @@ -11,23 +11,23 @@ struct SaveChannelQRCode: View { @Environment(\.dismiss) private var dismiss var channelSetLink: String - var addChannel: Bool = false + var addChannels: Bool = false var bleManager: BLEManager @State var connectedToDevice = false var body: some View { VStack { - Text("Save Channel Settings?") + Text("\(addChannels ? "Add" : "Replace all") Channels?") .font(.title) - Text("These settings will replace the current LoRa Config and Channel Settings on your radio. After everything saves your device will reboot.") + Text("These settings will \(addChannels ? "add" : "replace all") channels. The current LoRa Config will be replaced. After everything saves your device will reboot.") .foregroundColor(.gray) - .font(.callout) + .font(.title3) .padding() HStack { Button { - let success = bleManager.saveChannelSet(base64UrlString: channelSetLink, addChannel: addChannel) + let success = bleManager.saveChannelSet(base64UrlString: channelSetLink, addChannels: addChannels) if success { dismiss() } From 95109a3d9dd44d4900be838061316d25eb0f1fa5 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 3 Apr 2024 08:13:16 -0700 Subject: [PATCH 19/21] Send want config after channels changes to get new data now that lora config does not reboot --- Meshtastic/Helpers/BLEManager.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index aaa664ff..e234cd35 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1390,7 +1390,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let logString = String.localizedStringWithFormat("mesh.log.lora.config.sent %@".localized, String(connectedPeripheral.num)) MeshLogger.log("📻 \(logString)") } - return true + + if self.connectedPeripheral != nil { + self.sendWantConfig() + return true + } + } catch { return false } From 186d4c542d9d8f3fd46e6347f23512d5279a5769 Mon Sep 17 00:00:00 2001 From: Ian McEwen Date: Wed, 3 Apr 2024 10:49:36 -0700 Subject: [PATCH 20/21] Make yellow/Fair RSSI possible I'm guessing a bit, but I suspect this was the intent, but the inequalities are backwards (as clearly no number is both greater than -115 and less than -120). --- Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift b/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift index 85db8ec1..4405e819 100644 --- a/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift +++ b/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift @@ -85,7 +85,7 @@ func getRssiColor(rssi: Int32) -> Color { if rssi > -115 { /// Good return .green - } else if rssi > -115 && rssi < -120 { + } else if rssi > -120 { /// Fair return .yellow } else if rssi > -126 { From 7cdf77ba2e8e74b497c120c77712aaa5a7dfd292 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 3 Apr 2024 13:33:55 -0700 Subject: [PATCH 21/21] Delete nodes on that channel when deleting a channel --- Meshtastic/Views/Settings/Channels.swift | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 2581ee15..5f0f44fc 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -47,6 +47,14 @@ struct Channels: View { /// Minimum Version for granular position configuration @State var minimumVersion = "2.2.24" + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false), + NSSortDescriptor(key: "lastHeard", ascending: false), + NSSortDescriptor(key: "user.longName", ascending: true)], + animation: .default) + + var nodes: FetchedResults var body: some View { @@ -182,10 +190,16 @@ struct Channels: View { guard let channelEntity = node?.myInfo?.channels?.first(where: { ($0 as! ChannelEntity).index == channelIndex }) else { return } + let objects = (channelEntity as! ChannelEntity).allPrivateMessages for object in objects { context.delete(object) } + for node in nodes { + if node.channel == (channelEntity as AnyObject).index { + context.delete(node) + } + } context.delete(channelEntity as! ChannelEntity) do { try context.save() @@ -288,7 +302,6 @@ func firstMissingChannelIndex(_ indexes: [Int]) -> Int { return indexes.count + 1 } - enum PositionPrecision: Int, CaseIterable, Identifiable { case eleven = 11