diff --git a/Localizable.xcstrings b/Localizable.xcstrings index a1d2ddab..65b72c6b 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -27,16 +27,6 @@ }, "%@" : { - }, - "%@ - %@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "%1$@ - %2$@" - } - } - } }, "%@ - %@ - %@" : { "localizations" : { @@ -47,6 +37,28 @@ } } } + }, + "%@ - %d Hops Towards %d Hops Back" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$@ - %2$d Hops Towards %3$d Hops Back" + } + } + } + }, + "%@ - 1 Hop" : { + + }, + "%@ - Direct" : { + + }, + "%@ - No Response" : { + + }, + "%@ - Not Sent" : { + }, "%@ (%@)" : { "localizations" : { @@ -469,9 +481,6 @@ }, "Admin & Direct Message Keys" : { - }, - "Admin Key" : { - }, "admin.log" : { "extractionState" : "manual", @@ -1534,13 +1543,13 @@ "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "尝试连接%@失败,你可能需要在系统设置的蓝牙选项中忽略该电台。" + "value" : "尝试连接%d失败,你可能需要在系统设置的蓝牙选项中忽略该电台。" } }, "zh-Hant-TW" : { "stringUnit" : { "state" : "translated", - "value" : "嘗試連接%@失敗,你可能需要在系统設定的藍芽選項中忽略該電台。" + "value" : "嘗試連接%d失敗,你可能需要在系统設定的藍芽選項中忽略該電台。" } } } @@ -5198,9 +5207,6 @@ }, "Detection sensor messages are received as text messages. If you enable notifications you will recieve a notification for each detection message received and a corresponding unread message badge." : { - }, - "Detection trigger High" : { - }, "detection.sensor" : { "localizations" : { @@ -5877,55 +5883,55 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Repeater - Mesh packets will prefer to be routed over this node. This role eliminates unnecessary overhead such as NodeInfo, DeviceTelemetry, and any other mesh packet, resulting in the device not appearing as part of the network. Please see Rebroadcast Mode for additional settings specific to this role." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list." + "value" : "Infrastructure node on a tower or mountain top only. Not to be used for roofs or mobile nodes. Relays messages with minimal overhead. Not visible in Nodes list." } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Noeud d'infrastructure qui étend la couverture du réseau en relayant les messages avec un minimum de surcharge. Invisible dans la liste des noeuds." } }, "he" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "מכשיר תשתית להרחבת המש על ידי העברת הודעות עם דאטה נוסף מינימלי." } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Przekaźnik - Pakiety siatki będą preferować trasowanie przez ten węzeł. Ta rola eliminuje niepotrzebny nadmiar, taki jak NodeInfo, DeviceTelemetry i inne pakiety siatki, skutkując tym, że urządzenie nie będzie widoczne jako część sieci. Proszę zobaczyć tryb Rebroadcast dla dodatkowych ustawień specyficznych dla tej roli." } }, "pt-PT" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Nó de infraestrutura para ampliar a cobertura da rede transmitindo mensagens com sobrecarga mínima. Não visível na lista de Nós." } }, "se" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Infrastrukturnod för att utöka nätverkstäckningen genom att vidarebefordra meddelanden med minimal overhead. Syns inte i Noder-listan." } }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "中继模式 - Mesh 网络数据包将优先通过此节点路由。此模式可消除不必要的开销,如节点信息、设备遥测和任何其他 Mesh 数据包,从而使设备不显示为 Mesh 网络的一部分。有关此角色的其他特定设置,请参阅转播模式。" } }, "zh-Hant-TW" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "中繼模式 - Mesh 網路數據包將優先通過此中繼點路由。此模式可消除不必要的開銷,如 NodeInfo、DeviceTelemetry 和任何其他 Mesh 數據包,從而使設備不顯示為 Mesh 網路的一部分。有關此角色的其他特定設置,請參閱轉播模式。" } } @@ -5936,55 +5942,55 @@ "localizations" : { "de" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Router - Mesh Pakete werden bevorzugt über diesen Node gerouted. Dieser Node wird nicht von einer Client App benutzt. WLAN, Bluetooth und Display sind aus." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "Infrastructure node for extending network coverage by relaying messages. Visible in Nodes list." + "value" : "Infrastructure node on a tower or mountain top only. Not to be used for roofs or mobile nodes. Needs exceptional coverage. Visible in Nodes list." } }, "fr" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Noeud d'infrastructure qui étend la couverture du réseau en relayant les messages. Visible dans la liste des noeuds." } }, "he" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "מכשיר תשתית להרחבת המש על ידי העברת הודעות. מופיע ברשימת מכשירים." } }, "pl" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Router - Pakiety siatki będą preferować trasowanie przez ten węzeł. Zakłada, że urządzenie będzie działać samodzielnie, umieszczone w miejscu z przewagą zasięgu. UWAGA: Radia BLE/Wi-Fi i ekran OLED zostaną uśpione." } }, "pt-PT" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Nó de infraestrutura para ampliar a cobertura da rede transmitindo mensagens. Visível na lista de Nós." } }, "se" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "Infrastrukturnod för att utöka nätverkstäckningen genom att vidarebefordra meddelanden. Synlig i Noder-listan." } }, "zh-Hans" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "纯路由模式 - 自动转发 Mesh 网络中其他节点的消息,中继模式下屏幕会熄灭,Wi-Fi 和蓝牙将会进入睡眠模式,App 将无法连接到电台进行收发操作。" } }, "zh-Hant-TW" : { "stringUnit" : { - "state" : "translated", + "state" : "needs_review", "value" : "纯路由模式 - 自動轉發 Mesh 網路中其他中繼點的消息,中繼模式下螢幕會熄滅,Wi-Fi 和藍芽將會進入睡眠模式,App 將無法連接到電台進行收發操作。" } } @@ -6002,7 +6008,7 @@ "en" : { "stringUnit" : { "state" : "translated", - "value" : "Combination of both ROUTER and CLIENT. Not for mobile devices. Deprecated" + "value" : "Deprecated role use client." } }, "fr" : { @@ -10884,9 +10890,6 @@ }, "Location:" : { - }, - "Location: %@" : { - }, "Locked" : { @@ -16683,6 +16686,9 @@ }, "Primary" : { + }, + "Primary Admin Key" : { + }, "Primary GPIO" : { @@ -18913,6 +18919,9 @@ }, "Secondary" : { + }, + "Secondary Admin Key" : { + }, "Security" : { @@ -21031,6 +21040,9 @@ }, "Ten Minutes" : { + }, + "Tertiary Admin Key" : { + }, "The amount of time to wait before we consider your packet as done." : { @@ -21062,7 +21074,7 @@ "The most recent public key for this node does not match the previously recorded key. You can delete the node and let it exchange keys again, but this also may indicate a more serious security problem. Contact the user through another trusted channel to determine if the key change was due to a factory reset or other intentional action." : { }, - "The public key authorized to send admin messages to this node." : { + "The primary public key authorized to send admin messages to this node." : { }, "The public key does not match the recorded key. You may delete the node and let it exchange keys again, but this may indicate a more serious security problem. Contact the user through another trusted channel, to determine if the key change was due to a factory reset or other intentional action." : { @@ -21073,9 +21085,15 @@ }, "The root topic to use for MQTT." : { + }, + "The secondary public key authorized to send admin messages to this node." : { + }, "The state of the LED (on/off)" : { + }, + "The tertiarypublic key authorized to send admin messages to this node." : { + }, "There has been no response to a request for device metadata over the admin channel for this node." : { @@ -21866,6 +21884,9 @@ }, "Treat double tap on supported accelerometers as a user button press." : { + }, + "TriggerType" : { + }, "Triple Click Ad Hoc Ping" : { @@ -22607,9 +22628,6 @@ }, "When using in GPIO mode, keep the output on for this long. " : { - }, - "Whether or not the GPIO pin state detection is triggered on HIGH (1) or LOW (0)" : { - }, "WiFi Options" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 20a5586d..c82b37b5 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -54,6 +54,7 @@ D9C983A22B79D1A600BDBE6A /* RequestPositionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C983A12B79D1A600BDBE6A /* RequestPositionButton.swift */; }; DD007BAE2AA4E91200F5FA12 /* MyInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */; }; DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */; }; + DD0BE3102CB9FDC4000BA445 /* DetectionSensorEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0BE30F2CB9FDC4000BA445 /* DetectionSensorEnums.swift */; }; DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */ = {isa = PBXBuildFile; productRef = DD0D3D212A55CEB10066DB71 /* CocoaMQTT */; }; DD0E21012B8A6F1300F2D100 /* DeviceHardware.json in Resources */ = {isa = PBXBuildFile; fileRef = DD0E21002B8A6BC500F2D100 /* DeviceHardware.json */; }; DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */; }; @@ -299,6 +300,8 @@ DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoEntityExtension.swift; sourceTree = ""; }; DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityExtension.swift; sourceTree = ""; }; DD05296F2B77F454008E44CD /* MeshtasticDataModelV 26.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 26.xcdatamodel"; sourceTree = ""; }; + DD0BE30C2CB785D8000BA445 /* MeshtasticDataModelV 46.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 46.xcdatamodel"; sourceTree = ""; }; + DD0BE30F2CB9FDC4000BA445 /* DetectionSensorEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorEnums.swift; sourceTree = ""; }; DD0E20FF2B892E1300F2D100 /* MeshtasticDataModelV 28.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 28.xcdatamodel"; sourceTree = ""; }; DD0E21002B8A6BC500F2D100 /* DeviceHardware.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = DeviceHardware.json; sourceTree = ""; }; DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV14.xcdatamodel; sourceTree = ""; }; @@ -796,6 +799,7 @@ DDB6ABD828B0A4BA00384BA1 /* BluetoothModes.swift */, DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */, DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */, + DD0BE30F2CB9FDC4000BA445 /* DetectionSensorEnums.swift */, DDB6ABDF28B13AC700384BA1 /* DeviceEnums.swift */, DDB6ABE328B13FFF00384BA1 /* DisplayEnums.swift */, DD5D0A9B2931B9F200F7EA61 /* EthernetModes.swift */, @@ -1394,6 +1398,7 @@ DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */, DDB6ABD928B0A4BA00384BA1 /* BluetoothModes.swift in Sources */, DD1BD0EE2C603C91008C0C70 /* CustomFormatters.swift in Sources */, + DD0BE3102CB9FDC4000BA445 /* DetectionSensorEnums.swift in Sources */, DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */, DD2553592855B52700E55709 /* PositionConfig.swift in Sources */, DD97E96828EFE9A00056DDA4 /* About.swift in Sources */, @@ -1699,7 +1704,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.8; + MARKETING_VERSION = 2.5.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1733,7 +1738,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.5.8; + MARKETING_VERSION = 2.5.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1765,7 +1770,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.8; + MARKETING_VERSION = 2.5.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1798,7 +1803,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.5.8; + MARKETING_VERSION = 2.5.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1910,6 +1915,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD0BE30C2CB785D8000BA445 /* MeshtasticDataModelV 46.xcdatamodel */, DD6D5A342CA13BA600ED3032 /* MeshtasticDataModelV 45.xcdatamodel */, DD7CF8DA2C93663C008BD10E /* MeshtasticDataModelV 44.xcdatamodel */, DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */, @@ -1956,7 +1962,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD6D5A342CA13BA600ED3032 /* MeshtasticDataModelV 45.xcdatamodel */; + currentVersion = DD0BE30C2CB785D8000BA445 /* MeshtasticDataModelV 46.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Enums/DetectionSensorEnums.swift b/Meshtastic/Enums/DetectionSensorEnums.swift new file mode 100644 index 00000000..34401a82 --- /dev/null +++ b/Meshtastic/Enums/DetectionSensorEnums.swift @@ -0,0 +1,53 @@ +// +// DetectionSensorEnums.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 10/11/24. +// +import MeshtasticProtobufs + +enum TriggerTypes: Int, CaseIterable, Identifiable { + + case logicLow = 0 + case logicHigh = 1 + case fallingEdge = 2 + case risingEdge = 3 + case eitherEdgeActiveLow = 4 + case eitherEdgeActiveHigh = 5 + + var id: Int { self.rawValue } + + var name: String { + switch self { + case .logicLow: + return "Low" + case .logicHigh: + return "High" + case .fallingEdge: + return "Falling Edge" + case .risingEdge: + return "Rising Edge" + case .eitherEdgeActiveLow: + return "Either Edge Low" + case .eitherEdgeActiveHigh: + return "Either Edge Hight" + } + } + func protoEnumValue() -> ModuleConfig.DetectionSensorConfig.TriggerType { + + switch self { + case .logicLow: + return ModuleConfig.DetectionSensorConfig.TriggerType.logicLow + case .logicHigh: + return ModuleConfig.DetectionSensorConfig.TriggerType.logicHigh + case .fallingEdge: + return ModuleConfig.DetectionSensorConfig.TriggerType.fallingEdge + case .risingEdge: + return ModuleConfig.DetectionSensorConfig.TriggerType.risingEdge + case .eitherEdgeActiveLow: + return ModuleConfig.DetectionSensorConfig.TriggerType.eitherEdgeActiveLow + case .eitherEdgeActiveHigh: + return ModuleConfig.DetectionSensorConfig.TriggerType.eitherEdgeActiveHigh + } + } +} diff --git a/Meshtastic/Enums/LoraConfigEnums.swift b/Meshtastic/Enums/LoraConfigEnums.swift index b0dd9966..2a2d7090 100644 --- a/Meshtastic/Enums/LoraConfigEnums.swift +++ b/Meshtastic/Enums/LoraConfigEnums.swift @@ -25,9 +25,12 @@ enum RegionCodes: Int, CaseIterable, Identifiable { case th = 12 case ua433 = 14 case ua868 = 15 - case my_433 = 16 - case my_919 = 17 - case sg_923 = 18 + case my433 = 16 + case my919 = 17 + case sg923 = 18 + case ph433 = 19 + case ph868 = 20 + case ph915 = 21 case lora24 = 13 var topic: String { switch self { @@ -61,12 +64,18 @@ enum RegionCodes: Int, CaseIterable, Identifiable { "UA_433" case .ua868: "UA_868" - case .my_433: + case .my433: "MY_433" - case .my_919: + case .my919: "MY_919" - case .sg_923: + case .sg923: "SG_923" + case .ph433: + "ph_433" + case .ph868: + "ph_868" + case .ph915: + "ph_915" case .lora24: "LORA_24" } } @@ -105,12 +114,18 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return "Ukraine 868mhz" case .lora24: return "2.4 GHZ" - case .my_433: + case .my433: return "Malaysia 433mhz" - case .my_919: + case .my919: return "Malaysia 919mhz" - case .sg_923: + case .sg923: return "Singapore 923mhz" + case .ph433: + return "Philippines 433mhz" + case .ph868: + return "Philippines 868mhz" + case .ph915: + return "Philippines 915mhz" } } var dutyCycle: Int { @@ -147,11 +162,17 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return 10 case .lora24: return 100 - case .my_433: + case .my433: return 100 - case .my_919: + case .my919: return 100 - case .sg_923: + case .sg923: + return 100 + case .ph433: + return 100 + case .ph868: + return 100 + case .ph915: return 100 } } @@ -190,12 +211,18 @@ enum RegionCodes: Int, CaseIterable, Identifiable { return Config.LoRaConfig.RegionCode.ua868 case .lora24: return Config.LoRaConfig.RegionCode.lora24 - case .my_433: + case .my433: return Config.LoRaConfig.RegionCode.my433 - case .my_919: + case .my919: return Config.LoRaConfig.RegionCode.my919 - case .sg_923: + case .sg923: return Config.LoRaConfig.RegionCode.sg923 + case .ph433: + return Config.LoRaConfig.RegionCode.ph433 + case .ph868: + return Config.LoRaConfig.RegionCode.ph868 + case .ph915: + return Config.LoRaConfig.RegionCode.ph915 } } } diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index 7585fb1e..7d313191 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -40,7 +40,8 @@ extension NodeInfoEntity { } var hasTraceRoutes: Bool { - return traceRoutes?.count ?? 0 > 0 + let routes = traceRoutes?.filter { ($0 as AnyObject).response } + return routes?.count ?? 0 > 0 } var hasPax: Bool { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 805f32a2..4d6e2c75 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -840,24 +840,31 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.direct %@".localized, String(snr)) MeshLogger.log("🪧 \(logString)") } else { + guard let connectedNode = getNodeInfo(id: Int64(connectedPeripheral.num), context: context) else { + return + } var hopNodes: [TraceRouteHopEntity] = [] let connectedHop = TraceRouteHopEntity(context: context) - connectedHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized connectedHop.time = Date() + connectedHop.num = connectedPeripheral.num + connectedHop.name = connectedNode.user?.longName ?? "???" + connectedHop.snr = Float(routingMessage.snrBack.last ?? 0 / 4) if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { connectedHop.altitude = mostRecent.altitude connectedHop.latitudeI = mostRecent.latitudeI connectedHop.longitudeI = mostRecent.longitudeI traceRoute?.hasPositions = true } + var routeString = "\(connectedNode.user?.longName ?? "???") --> " hopNodes.append(connectedHop) - var routeString = "You --> " + traceRoute?.hopsTowards = Int32(routingMessage.route.count) for (index, node) in routingMessage.route.enumerated() { var hopNode = getNodeInfo(id: Int64(node), context: context) if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { hopNode = createNodeInfo(num: Int64(node), context: context) } let traceRouteHop = TraceRouteHopEntity(context: context) + traceRouteHop.time = Date() if routingMessage.snrTowards.count >= index + 1 { traceRouteHop.snr = Float(routingMessage.snrTowards[index] / 4) } @@ -874,11 +881,26 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if decodedInfo.packet.rxTime > 0 { hopNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.packet.rxTime))) } - hopNodes.append(traceRouteHop) } + hopNodes.append(traceRouteHop) routeString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB) --> " } - var routeBackString = traceRoute?.node?.user?.longName ?? "unknown".localized + let destinationHop = TraceRouteHopEntity(context: context) + destinationHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized + destinationHop.time = Date() + destinationHop.snr = Float(routingMessage.snrTowards.last ?? 0 / 4) + destinationHop.num = traceRoute?.node?.num ?? 0 + if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { + destinationHop.altitude = mostRecent.altitude + destinationHop.latitudeI = mostRecent.latitudeI + destinationHop.longitudeI = mostRecent.longitudeI + traceRoute?.hasPositions = true + } + hopNodes.append(destinationHop) + /// Add the destination node to the end of the route towards string and the beginning of teh route back string + routeString += "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) \(traceRoute?.node?.snr ?? 0 > 0 ? traceRoute?.node?.snr ?? 0 : 0.0)dB)" + var routeBackString = "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) \(traceRoute?.node?.snr ?? 0 > 0 ? traceRoute?.node?.snr ?? 0 : 0.0)dB) --> " + traceRoute?.hopsBack = Int32(routingMessage.routeBack.count) for (index, node) in routingMessage.routeBack.enumerated() { var hopNode = getNodeInfo(id: Int64(node), context: context) if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { @@ -903,20 +925,22 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if decodedInfo.packet.rxTime > 0 { hopNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.packet.rxTime))) } - hopNodes.append(traceRouteHop) } + hopNodes.append(traceRouteHop) routeBackString += "\(hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))) \(hopNode?.viaMqtt ?? false ? "MQTT" : "") (\(traceRouteHop.snr > 0 ? hopNode?.snr ?? 0.0 : 0.0)dB) --> " } + routeBackString += "\(connectedNode.user?.longName ?? String(connectedNode.num.toHex())) \(connectedNode.snr > 0 ? connectedNode.snr : 0.0)dB)" traceRoute?.routeText = routeString traceRoute?.routeBackText = routeBackString traceRoute?.hops = NSOrderedSet(array: hopNodes) + traceRoute?.time = Date() do { try context.save() Logger.data.info("💾 Saved Trace Route") } catch { context.rollback() let nsError = error as NSError - Logger.data.error("Error Updating Core Data TraceRouteHOp: \(nsError, privacy: .public)") + Logger.data.error("Error Updating Core Data TraceRouteHop: \(nsError, privacy: .public)") } let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.route %@".localized, routeString) MeshLogger.log("🪧 \(logString)") diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 9c4ed69c..a5cfa7ba 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -726,6 +726,9 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage telemetry.numPacketsTx = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsTx) telemetry.numPacketsRx = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRx) telemetry.numPacketsRxBad = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRxBad) + telemetry.numRxDupe = Int32(truncatingIfNeeded: telemetryMessage.localStats.numRxDupe) + telemetry.numTxRelay = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTxRelay) + telemetry.numTxRelayCanceled = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTxRelayCanceled) telemetry.numOnlineNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numOnlineNodes) telemetry.numTotalNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTotalNodes) telemetry.metricsType = 4 @@ -780,6 +783,9 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage sentPackets: UInt32(telemetry.numPacketsTx), receivedPackets: UInt32(telemetry.numPacketsRx), badReceivedPackets: UInt32(telemetry.numPacketsRxBad), + dupeReceivedPackets: UInt32(telemetry.numRxDupe), + packetsSentRelay: UInt32(telemetry.numTxRelay), + packetsCanceledRelay: UInt32(telemetry.numTxRelayCanceled), nodesOnline: UInt32(telemetry.numOnlineNodes), totalNodes: UInt32(telemetry.numTotalNodes), timerRange: date) diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 4269da21..6d376a5f 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 45.xcdatamodel + MeshtasticDataModelV 46.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 46.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 46.xcdatamodel/contents new file mode 100644 index 00000000..bc188564 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 46.xcdatamodel/contents @@ -0,0 +1,484 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 69757588..6ddd664c 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -989,7 +989,7 @@ func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSenso newConfig.sendBell = config.sendBell newConfig.name = config.name newConfig.monitorPin = Int32(config.monitorPin) - newConfig.detectionTriggeredHigh = config.detectionTriggeredHigh + newConfig.triggerType = Int32(config.detectionTriggerType.rawValue) newConfig.usePullup = config.usePullup newConfig.minimumBroadcastSecs = Int32(truncatingIfNeeded: config.minimumBroadcastSecs) newConfig.stateBroadcastSecs = Int32(truncatingIfNeeded: config.stateBroadcastSecs) @@ -1000,7 +1000,7 @@ func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSenso fetchedNode[0].detectionSensorConfig?.name = config.name fetchedNode[0].detectionSensorConfig?.monitorPin = Int32(config.monitorPin) fetchedNode[0].detectionSensorConfig?.usePullup = config.usePullup - fetchedNode[0].detectionSensorConfig?.detectionTriggeredHigh = config.detectionTriggeredHigh + fetchedNode[0].detectionSensorConfig?.triggerType = Int32(config.detectionTriggerType.rawValue) fetchedNode[0].detectionSensorConfig?.minimumBroadcastSecs = Int32(truncatingIfNeeded: config.minimumBroadcastSecs) fetchedNode[0].detectionSensorConfig?.stateBroadcastSecs = Int32(truncatingIfNeeded: config.stateBroadcastSecs) } diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index eaf5db0b..0b88202d 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -127,6 +127,14 @@ "activelySupported": true, "displayName": "LILYGO T-LoRa T3-S3" }, + { + "hwModel": 16, + "hwModelSlug": "TLORA_T3_S3", + "platformioTarget": "tlora-t3s3-epaper", + "architecture": "esp32-s3", + "activelySupported": true, + "displayName": "LILYGO T-LoRa T3-S3 E-Paper" + }, { "hwModel": 17, "hwModelSlug": "NANO_G1_EXPLORER", @@ -414,5 +422,13 @@ "architecture": "nrf52840", "activelySupported": true, "displayName": "Seeed Card Tracker T1000-E" + }, + { + "hwModel": 72, + "hwModelSlug": "Seeed_XIAO_S3", + "platformioTarget": "seeed-xiao-s3", + "architecture": "esp32-s3", + "activelySupported": true, + "displayName": "Seeed XIAO S3" } ] diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index c26b7676..be466530 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -341,6 +341,9 @@ struct Connect: View { sentPackets: UInt32(mostRecent?.numPacketsTx ?? 0), receivedPackets: UInt32(mostRecent?.numPacketsRx ?? 0), badReceivedPackets: UInt32(mostRecent?.numPacketsRxBad ?? 0), + dupeReceivedPackets: UInt32(mostRecent?.numRxDupe ?? 0), + packetsSentRelay: UInt32(mostRecent?.numTxRelay ?? 0), + packetsCanceledRelay: UInt32(mostRecent?.numTxRelayCanceled ?? 0), nodesOnline: UInt32(mostRecent?.numOnlineNodes ?? 0), totalNodes: UInt32(mostRecent?.numTotalNodes ?? 0), timerRange: Date.now...future) diff --git a/Meshtastic/Views/Helpers/SecureInput.swift b/Meshtastic/Views/Helpers/SecureInput.swift index a2994747..6bcab1d0 100644 --- a/Meshtastic/Views/Helpers/SecureInput.swift +++ b/Meshtastic/Views/Helpers/SecureInput.swift @@ -12,7 +12,7 @@ struct SecureInput: View { private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @Binding private var text: String @Binding private var isValid: Bool - @State private var isSecure: Bool = true + @State var isSecure: Bool = true private var title: String init(_ title: String, text: Binding, isValid: Binding) { diff --git a/Meshtastic/Views/Layouts/TraceRoute.swift b/Meshtastic/Views/Layouts/TraceRoute.swift index bb79f747..fd4d7d92 100644 --- a/Meshtastic/Views/Layouts/TraceRoute.swift +++ b/Meshtastic/Views/Layouts/TraceRoute.swift @@ -56,13 +56,13 @@ struct TraceRoute: Layout { subview.place(at: point, anchor: .center, proposal: .unspecified) - DispatchQueue.main.async { + // DispatchQueue.main.async { if index % 2 == 0 { subview[Rotation.self]?.wrappedValue = .zero } else { subview[Rotation.self]?.wrappedValue = .radians(angle) } - } + // } } } } diff --git a/Meshtastic/Views/MapKitMap/Custom/MapButtons.swift b/Meshtastic/Views/MapKitMap/Custom/MapButtons.swift index 2d45b4f4..89959f74 100644 --- a/Meshtastic/Views/MapKitMap/Custom/MapButtons.swift +++ b/Meshtastic/Views/MapKitMap/Custom/MapButtons.swift @@ -1,64 +1,64 @@ // -// MapButtons.swift -// Meshtastic +//// MapButtons.swift +//// Meshtastic +//// +//// Copyright © Garth Vander Houwen 4/23/23. +//// // -// Copyright © Garth Vander Houwen 4/23/23. +//import SwiftUI // - -import SwiftUI - -struct MapButtons: View { - let buttonWidth: CGFloat = 22 - let width: CGFloat = 45 - @Binding var tracking: UserTrackingModes - @Binding var isPresentingInfoSheet: Bool - var body: some View { - VStack { - let impactLight = UIImpactFeedbackGenerator(style: .light) - Button(action: { - self.isPresentingInfoSheet.toggle() - }) { - Image(systemName: isPresentingInfoSheet ? "info.circle.fill" : "info.circle") - .resizable() - .frame(width: buttonWidth, height: buttonWidth, alignment: .center) - .offset(y: -2) - } - Divider() - Button(action: { - switch self.tracking { - case .none: - self.tracking = .follow - case .follow: - self.tracking = .followWithHeading - case .followWithHeading: - self.tracking = .none - } - impactLight.impactOccurred() - }) { - Image(systemName: tracking.icon) - .frame(width: buttonWidth, height: buttonWidth, alignment: .center) - .offset(y: 3) - } - } - .frame(width: width, height: width*2, alignment: .center) - .background(Color(UIColor.systemBackground)) - .cornerRadius(8) - .shadow(radius: 1) - .offset(x: 3, y: 25) - } -} - -// MARK: Previews -// struct MapControl_Previews: PreviewProvider { -// @State static var tracking: UserTrackingModes = .none -// @State static var isPresentingInfoSheet = false -// static var previews: some View { -// Group { -// MapButtons(tracking: $tracking, isPresentingInfoSheet: $isPresentingInfoSheet) -// .environment(\.colorScheme, .light) -// MapButtons(tracking: $tracking, isPresentingInfoSheet: $isPresentingInfoSheet) -// .environment(\.colorScheme, .dark) +//struct MapButtons: View { +// let buttonWidth: CGFloat = 22 +// let width: CGFloat = 45 +// @Binding var tracking: UserTrackingModes +// @Binding var isPresentingInfoSheet: Bool +// var body: some View { +// VStack { +// let impactLight = UIImpactFeedbackGenerator(style: .light) +// Button(action: { +// self.isPresentingInfoSheet.toggle() +// }) { +// Image(systemName: isPresentingInfoSheet ? "info.circle.fill" : "info.circle") +// .resizable() +// .frame(width: buttonWidth, height: buttonWidth, alignment: .center) +// .offset(y: -2) +// } +// Divider() +// Button(action: { +// switch self.tracking { +// case .none: +// self.tracking = .follow +// case .follow: +// self.tracking = .followWithHeading +// case .followWithHeading: +// self.tracking = .none +// } +// impactLight.impactOccurred() +// }) { +// Image(systemName: tracking.icon) +// .frame(width: buttonWidth, height: buttonWidth, alignment: .center) +// .offset(y: 3) +// } // } -// .previewLayout(.fixed(width: 60, height: 100)) +// .frame(width: width, height: width*2, alignment: .center) +// .background(Color(UIColor.systemBackground)) +// .cornerRadius(8) +// .shadow(radius: 1) +// .offset(x: 3, y: 25) // } -// } +//} +// +//// MARK: Previews +//// struct MapControl_Previews: PreviewProvider { +//// @State static var tracking: UserTrackingModes = .none +//// @State static var isPresentingInfoSheet = false +//// static var previews: some View { +//// Group { +//// MapButtons(tracking: $tracking, isPresentingInfoSheet: $isPresentingInfoSheet) +//// .environment(\.colorScheme, .light) +//// MapButtons(tracking: $tracking, isPresentingInfoSheet: $isPresentingInfoSheet) +//// .environment(\.colorScheme, .dark) +//// } +//// .previewLayout(.fixed(width: 60, height: 100)) +//// } +//// } diff --git a/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift index b7629326..8cdf6d84 100644 --- a/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift @@ -1,434 +1,434 @@ +//// +//// MapViewSwitUI.swift +//// Meshtastic +//// +//// Copyright(c) Josh Pirihi & Garth Vander Houwen 1/16/22. // -// MapViewSwitUI.swift -// Meshtastic +//import Foundation +//import SwiftUI +//import MapKit +//import OSLog // -// Copyright(c) Josh Pirihi & Garth Vander Houwen 1/16/22. - -import Foundation -import SwiftUI -import MapKit -import OSLog - -struct PolygonInfo: Codable { - let stroke: String? - let strokeWidth, strokeOpacity: Int? - let fill: String? - let fillOpacity: Double? - let title, subtitle: String? -} - -func degreesToRadians(_ number: Double) -> Double { - return number * .pi / 180 -} -var currentMapLayer: MapLayer? - -struct MapViewSwiftUI: UIViewRepresentable { - var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void - var onWaypointEdit: (_ waypointId: Int ) -> Void - let mapView = MKMapView() - // Parameters - let selectedMapLayer: MapLayer - let selectedWeatherLayer: MapOverlayServer = UserDefaults.mapOverlayServer - let positions: [PositionEntity] - let waypoints: [WaypointEntity] - let userTrackingMode: MKUserTrackingMode - let showNodeHistory: Bool - let showRouteLines: Bool - let mapViewType: MKMapType = MKMapType.standard - // Offline Map Tiles - @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0 - @State private var loadedLastUpdatedLocalMapFile = 0 - var customMapOverlay: CustomMapOverlay? - @State private var presentCustomMapOverlayHash: CustomMapOverlay? - // MARK: Private methods - private func configureMap(mapView: MKMapView) { - // Map View Parameters - mapView.mapType = mapViewType - mapView.addAnnotations(waypoints) - // Do the initial map centering - let latest = positions - .filter { $0.latest == true } - .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } - let span = MKCoordinateSpan(latitudeDelta: 0.003, longitudeDelta: 0.003) - let center = (latest.count > 0 && userTrackingMode == MKUserTrackingMode.none) ? latest[0].coordinate : LocationHelper.currentLocation - let region = MKCoordinateRegion(center: center, span: span) - mapView.addAnnotations(showNodeHistory ? positions : latest) - mapView.setRegion(region, animated: true) - // Set user (phone gps) tracking options - mapView.setUserTrackingMode(userTrackingMode, animated: true) - if userTrackingMode == MKUserTrackingMode.none { - if latest.count == 1 { - mapView.fit(annotations: showNodeHistory ? positions: latest, andShow: false) - } else { - mapView.fitAllAnnotations() - } - mapView.showsUserLocation = false - } else { - mapView.showsUserLocation = true - } - // Other MKMapView Settings - mapView.preferredConfiguration.elevationStyle = .realistic// .flat - mapView.pointOfInterestFilter = MKPointOfInterestFilter.excludingAll - mapView.isPitchEnabled = true - mapView.isRotateEnabled = true - mapView.isScrollEnabled = true - mapView.isZoomEnabled = true - mapView.showsBuildings = true - mapView.showsScale = true - mapView.showsTraffic = true - - mapView.showsCompass = false - let compass = MKCompassButton(mapView: mapView) - compass.translatesAutoresizingMaskIntoConstraints = false - #if targetEnvironment(macCatalyst) - // Show the default always visible compass and the mac only controls - compass.compassVisibility = .visible - mapView.addSubview(compass) - mapView.showsZoomControls = true - mapView.showsPitchControl = true - compass.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -115).isActive = true - compass.bottomAnchor.constraint(equalTo: mapView.bottomAnchor, constant: -5).isActive = true - #else - compass.compassVisibility = .adaptive - mapView.addSubview(compass) - compass.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -5).isActive = true - compass.topAnchor.constraint(equalTo: mapView.topAnchor, constant: 145).isActive = true - #endif - } - private func setMapBaseLayer(mapView: MKMapView) { - // Avoid refreshing UI if selectedLayer has not changed - guard currentMapLayer != selectedMapLayer else { return } - currentMapLayer = selectedMapLayer - for overlay in mapView.overlays where overlay is MKTileOverlay { - mapView.removeOverlay(overlay) - } - switch selectedMapLayer { - case .offline: - mapView.mapType = .standard - let overlay = TileOverlay() - overlay.canReplaceMapContent = false - overlay.minimumZ = UserDefaults.mapTileServer.zoomRange.startIndex - overlay.maximumZ = UserDefaults.mapTileServer.zoomRange.endIndex - mapView.addOverlay(overlay, level: UserDefaults.mapTilesAboveLabels ? .aboveLabels : .aboveRoads) - case .satellite: - mapView.mapType = .satellite - case .hybrid: - mapView.mapType = .hybrid - default: - mapView.mapType = .standard - } - } - private func setMapOverlays(mapView: MKMapView) { - // Weather radar - if UserDefaults.enableOverlayServer { - let locale = Locale.current - if locale.region?.identifier ?? "no locale" == "US" { - let overlay = MKTileOverlay(urlTemplate: selectedWeatherLayer.tileUrl) - overlay.canReplaceMapContent = false - overlay.minimumZ = selectedWeatherLayer.zoomRange.startIndex - overlay.maximumZ = selectedWeatherLayer.zoomRange.endIndex - mapView.addOverlay(overlay, level: .aboveLabels) - } - } - } - - func makeUIView(context: Context) -> MKMapView { - currentMapLayer = nil - mapView.delegate = context.coordinator - self.configureMap(mapView: mapView) - return mapView - } - func updateUIView(_ mapView: MKMapView, context: Context) { - // Set selected map base layer - setMapBaseLayer(mapView: mapView) - // Set map tile server and weather overlay layers - setMapOverlays(mapView: mapView) - let latest = positions - .filter { $0.latest == true } - .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } - // Node Route Lines - if showRouteLines { - // Remove all existing PolyLine Overlays - for overlay in mapView.overlays where overlay is MKPolyline { - mapView.removeOverlay(overlay) - } - var lineIndex = 0 - for position in latest { - let nodePositions = positions.filter { $0.nodeCoordinate != nil && $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 } - let lineCoords = nodePositions.compactMap({(position) -> CLLocationCoordinate2D in - return position.nodeCoordinate ?? LocationHelper.DefaultLocation - }) - let polyline = MKPolyline(coordinates: lineCoords, count: nodePositions.count) - polyline.title = "\(String(position.nodePosition?.num ?? 0))" - mapView.addOverlay(polyline, level: .aboveLabels) - lineIndex += 1 - // There are 18 colors for lines, start over if we are at index 17 - if lineIndex > 17 { - lineIndex = 0 - } - } - } else { - // Remove all existing PolyLine Overlays - for overlay in mapView.overlays where overlay is MKPolyline { - mapView.removeOverlay(overlay) - } - } - let annotationCount = waypoints.count + (showNodeHistory ? positions.count : latest.count) - if annotationCount != mapView.annotations.count { - Logger.services.info("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)") - mapView.removeAnnotations(mapView.annotations) - mapView.addAnnotations(waypoints) - } - mapView.addAnnotations(showNodeHistory ? positions : latest) - if userTrackingMode == MKUserTrackingMode.none { - mapView.showsUserLocation = false - if UserDefaults.enableMapRecentering { - if latest.count == 1 { - mapView.fit(annotations: showNodeHistory ? positions : latest, andShow: true) - } else { - mapView.fitAllAnnotations() - } - } - } else { - mapView.showsUserLocation = true - } - mapView.setUserTrackingMode(userTrackingMode, animated: true) - } - func makeCoordinator() -> MapCoordinator { - return Coordinator(self) - } - final class MapCoordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate { - var parent: MapViewSwiftUI - var longPressRecognizer = UILongPressGestureRecognizer() - init(_ parent: MapViewSwiftUI) { - self.parent = parent - super.init() - self.longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressHandler)) - self.longPressRecognizer.minimumPressDuration = 0.5 - self.longPressRecognizer.cancelsTouchesInView = true - self.longPressRecognizer.delegate = self - self.parent.mapView.addGestureRecognizer(longPressRecognizer) - } - func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { - switch annotation { - case let positionAnnotation as PositionEntity: - let reuseID = String(positionAnnotation.nodePosition?.num ?? 0) + "-" + String(positionAnnotation.time?.timeIntervalSince1970 ?? 0) - let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: reuseID ) - annotationView.tag = -1 - annotationView.canShowCallout = true - if positionAnnotation.latest { - annotationView.markerTintColor = UIColor(hex: UInt32(positionAnnotation.nodePosition?.num ?? 0)).darker() - annotationView.displayPriority = .required - annotationView.titleVisibility = .visible - } else { - annotationView.markerTintColor = UIColor(hex: UInt32(positionAnnotation.nodePosition?.num ?? 0)).lighter() - annotationView.displayPriority = .defaultHigh - annotationView.titleVisibility = .adaptive - } - annotationView.tag = -1 - annotationView.canShowCallout = true - annotationView.titleVisibility = .adaptive - let leftIcon = UIImageView(image: annotationView.glyphText?.image()) - leftIcon.backgroundColor = UIColor(.indigo) - annotationView.leftCalloutAccessoryView = leftIcon - let subtitle = UILabel() - subtitle.text = "Long Name: \(positionAnnotation.nodePosition?.user?.longName ?? "Unknown") \n" - subtitle.text? += "Latitude: \(String(format: "%.5f", positionAnnotation.coordinate.latitude)) \n" - subtitle.text! += "Longitude: \(String(format: "%.5f", positionAnnotation.coordinate.longitude)) \n" - let distanceFormatter = MKDistanceFormatter() - subtitle.text! += "Altitude: \(distanceFormatter.string(fromDistance: Double(positionAnnotation.altitude))) \n" - if positionAnnotation.nodePosition?.metadata != nil { - if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.client || - DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.clientMute || - DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.routerClient { - annotationView.glyphImage = UIImage(systemName: "flipphone") - } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.repeater { - annotationView.glyphImage = UIImage(systemName: "repeat") - } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.router { - annotationView.glyphImage = UIImage(systemName: "wifi.router.fill") - } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.tracker { - annotationView.glyphImage = UIImage(systemName: "location.viewfinder") - } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.sensor { - annotationView.glyphImage = UIImage(systemName: "sensor") - } - let pf = PositionFlags(rawValue: Int(positionAnnotation.nodePosition?.metadata?.positionFlags ?? 3)) - if pf.contains(.Satsinview) { - subtitle.text! += "Sats in view: \(String(positionAnnotation.satsInView)) \n" - } - if pf.contains(.SeqNo) { - subtitle.text! += "Sequence: \(String(positionAnnotation.seqNo)) \n" - } - if pf.contains(.Heading) { - if parent.userTrackingMode != MKUserTrackingMode.followWithHeading { - annotationView.glyphImage = UIImage(systemName: "location.north.fill")?.rotate(radians: Float(degreesToRadians(Double(positionAnnotation.heading)))) - subtitle.text! += "Heading: \(String(positionAnnotation.heading)) \n" - } else { - annotationView.glyphImage = UIImage(systemName: "flipphone") - } - } - if pf.contains(.Speed) { - let formatter = MeasurementFormatter() - formatter.locale = Locale.current - if positionAnnotation.speed <= 1 { - annotationView.glyphImage = UIImage(systemName: "hexagon") - } - subtitle.text! += "Speed: \(formatter.string(from: Measurement(value: Double(positionAnnotation.speed), unit: UnitSpeed.kilometersPerHour))) \n" - } - } else { - // node metadata is nil - annotationView.glyphImage = UIImage(systemName: "flipphone") - } - if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 { - let metersAway = positionAnnotation.coordinate.distance(from: LocationHelper.currentLocation) - subtitle.text! += "distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway))) \n" - } - subtitle.text! += positionAnnotation.time?.formatted() ?? "Unknown \n" - subtitle.numberOfLines = 0 - annotationView.detailCalloutAccessoryView = subtitle - let detailsIcon = UIButton(type: .detailDisclosure) - detailsIcon.setImage(UIImage(systemName: "trash"), for: .normal) - annotationView.rightCalloutAccessoryView = detailsIcon - return annotationView - case let waypointAnnotation as WaypointEntity: - let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "waypoint") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: String(waypointAnnotation.id)) - annotationView.tag = Int(waypointAnnotation.id) - annotationView.isEnabled = true - annotationView.canShowCallout = true - if waypointAnnotation.icon == 0 { - annotationView.glyphText = "📍" - } else { - annotationView.glyphText = String(UnicodeScalar(Int(waypointAnnotation.icon)) ?? "📍") - } - annotationView.markerTintColor = UIColor(.accentColor) - annotationView.displayPriority = .required - annotationView.titleVisibility = .adaptive - let leftIcon = UIImageView(image: annotationView.glyphText?.image()) - leftIcon.backgroundColor = UIColor(.accentColor) - annotationView.leftCalloutAccessoryView = leftIcon - let subtitle = UILabel() - if waypointAnnotation.longDescription?.count ?? 0 > 0 { - subtitle.text = (waypointAnnotation.longDescription ?? "") + "\n" - } else { - subtitle.text = "" - } - if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 { - let metersAway = waypointAnnotation.coordinate.distance(from: LocationHelper.currentLocation) - let distanceFormatter = MKDistanceFormatter() - subtitle.text! += "distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway))) \n" - } - if waypointAnnotation.created != nil { - subtitle.text! += "Created: \(waypointAnnotation.created?.formatted() ?? "Unknown") \n" - } - if waypointAnnotation.lastUpdated != nil { - subtitle.text! += "Updated: \(waypointAnnotation.lastUpdated?.formatted() ?? "Unknown") \n" - } - if waypointAnnotation.expire != nil { - subtitle.text! += "Expires: \(waypointAnnotation.expire?.formatted() ?? "Unknown") \n" - } - subtitle.numberOfLines = 0 - annotationView.detailCalloutAccessoryView = subtitle - let editIcon = UIButton(type: .detailDisclosure) - editIcon.setImage(UIImage(systemName: "square.and.pencil"), for: .normal) - annotationView.rightCalloutAccessoryView = editIcon - return annotationView - default: return nil - } - } - func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) { - switch view.annotation { - case _ as WaypointEntity: - // Only Allow Edit for waypoint annotations with a id - if view.tag > 0 { - parent.onWaypointEdit(view.tag) - } - default: break - } - } - @objc func longPressHandler(_ gesture: UILongPressGestureRecognizer) { - if gesture.state != UIGestureRecognizer.State.ended { - return - } else if gesture.state != UIGestureRecognizer.State.began { - // Screen Position - CGPoint - let location = longPressRecognizer.location(in: self.parent.mapView) - // Map Coordinate - CLLocationCoordinate2D - let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) - let annotation = MKPointAnnotation() - annotation.title = "Dropped Pin" - annotation.coordinate = coordinate - parent.mapView.addAnnotation(annotation) - UINotificationFeedbackGenerator().notificationOccurred(.success) - parent.onLongPress(coordinate) - } - } - public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { - if let tileOverlay = overlay as? MKTileOverlay { - return MKTileOverlayRenderer(tileOverlay: tileOverlay) - } else { - if let routePolyline = overlay as? MKPolyline { - let titleString = routePolyline.title ?? "0" - let renderer = MKPolylineRenderer(polyline: routePolyline) - renderer.strokeColor = UIColor(hex: UInt32(titleString) ?? 0).lighter() - renderer.lineWidth = 8 - return renderer - } - if let polygon = overlay as? MKPolygon { - let renderer = MKPolygonRenderer(polygon: polygon) - renderer.fillColor = UIColor.purple.withAlphaComponent(0.2) - renderer.strokeColor = .purple.withAlphaComponent(0.7) - return renderer - } - return MKOverlayRenderer(overlay: overlay) - } - } - } - /// is supposed to be located in the folder with the map name - public struct DefaultTile: Hashable { - let tileName: String - let tileType: String - public init(tileName: String, tileType: String) { - self.tileName = tileName - self.tileType = tileType - } - } - public struct CustomMapOverlay: Equatable, Hashable { - let mapName: String - let tileType: String - var canReplaceMapContent: Bool - var minimumZoomLevel: Int? - var maximumZoomLevel: Int? - let defaultTile: DefaultTile? - public init( - mapName: String, - tileType: String, - canReplaceMapContent: Bool = true, // false for transparent tiles - minimumZoomLevel: Int? = nil, - maximumZoomLevel: Int? = nil, - defaultTile: DefaultTile? = nil - ) { - self.mapName = mapName - self.tileType = tileType - self.canReplaceMapContent = canReplaceMapContent - self.minimumZoomLevel = minimumZoomLevel - self.maximumZoomLevel = maximumZoomLevel - self.defaultTile = defaultTile - } - public init?( - mapName: String?, - tileType: String, - canReplaceMapContent: Bool = true, // false for transparent tiles - minimumZoomLevel: Int? = nil, - maximumZoomLevel: Int? = nil, - defaultTile: DefaultTile? = nil - ) { - if mapName == nil || mapName! == "" { - return nil - } - self.mapName = mapName! - self.tileType = tileType - self.canReplaceMapContent = canReplaceMapContent - self.minimumZoomLevel = minimumZoomLevel - self.maximumZoomLevel = maximumZoomLevel - self.defaultTile = defaultTile - } - } -} +//struct PolygonInfo: Codable { +// let stroke: String? +// let strokeWidth, strokeOpacity: Int? +// let fill: String? +// let fillOpacity: Double? +// let title, subtitle: String? +//} +// +//func degreesToRadians(_ number: Double) -> Double { +// return number * .pi / 180 +//} +//var currentMapLayer: MapLayer? +// +//struct MapViewSwiftUI: UIViewRepresentable { +// var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void +// var onWaypointEdit: (_ waypointId: Int ) -> Void +// let mapView = MKMapView() +// // Parameters +// let selectedMapLayer: MapLayer +// let selectedWeatherLayer: MapOverlayServer = UserDefaults.mapOverlayServer +// let positions: [PositionEntity] +// let waypoints: [WaypointEntity] +// let userTrackingMode: MKUserTrackingMode +// let showNodeHistory: Bool +// let showRouteLines: Bool +// let mapViewType: MKMapType = MKMapType.standard +// // Offline Map Tiles +// @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0 +// @State private var loadedLastUpdatedLocalMapFile = 0 +// var customMapOverlay: CustomMapOverlay? +// @State private var presentCustomMapOverlayHash: CustomMapOverlay? +// // MARK: Private methods +// private func configureMap(mapView: MKMapView) { +// // Map View Parameters +// mapView.mapType = mapViewType +// mapView.addAnnotations(waypoints) +// // Do the initial map centering +// let latest = positions +// .filter { $0.latest == true } +// .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } +// let span = MKCoordinateSpan(latitudeDelta: 0.003, longitudeDelta: 0.003) +// let center = (latest.count > 0 && userTrackingMode == MKUserTrackingMode.none) ? latest[0].coordinate : LocationHelper.currentLocation +// let region = MKCoordinateRegion(center: center, span: span) +// mapView.addAnnotations(showNodeHistory ? positions : latest) +// mapView.setRegion(region, animated: true) +// // Set user (phone gps) tracking options +// mapView.setUserTrackingMode(userTrackingMode, animated: true) +// if userTrackingMode == MKUserTrackingMode.none { +// if latest.count == 1 { +// mapView.fit(annotations: showNodeHistory ? positions: latest, andShow: false) +// } else { +// mapView.fitAllAnnotations() +// } +// mapView.showsUserLocation = false +// } else { +// mapView.showsUserLocation = true +// } +// // Other MKMapView Settings +// mapView.preferredConfiguration.elevationStyle = .realistic// .flat +// mapView.pointOfInterestFilter = MKPointOfInterestFilter.excludingAll +// mapView.isPitchEnabled = true +// mapView.isRotateEnabled = true +// mapView.isScrollEnabled = true +// mapView.isZoomEnabled = true +// mapView.showsBuildings = true +// mapView.showsScale = true +// mapView.showsTraffic = true +// +// mapView.showsCompass = false +// let compass = MKCompassButton(mapView: mapView) +// compass.translatesAutoresizingMaskIntoConstraints = false +// #if targetEnvironment(macCatalyst) +// // Show the default always visible compass and the mac only controls +// compass.compassVisibility = .visible +// mapView.addSubview(compass) +// mapView.showsZoomControls = true +// mapView.showsPitchControl = true +// compass.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -115).isActive = true +// compass.bottomAnchor.constraint(equalTo: mapView.bottomAnchor, constant: -5).isActive = true +// #else +// compass.compassVisibility = .adaptive +// mapView.addSubview(compass) +// compass.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -5).isActive = true +// compass.topAnchor.constraint(equalTo: mapView.topAnchor, constant: 145).isActive = true +// #endif +// } +// private func setMapBaseLayer(mapView: MKMapView) { +// // Avoid refreshing UI if selectedLayer has not changed +// guard currentMapLayer != selectedMapLayer else { return } +// currentMapLayer = selectedMapLayer +// for overlay in mapView.overlays where overlay is MKTileOverlay { +// mapView.removeOverlay(overlay) +// } +// switch selectedMapLayer { +// case .offline: +// mapView.mapType = .standard +// let overlay = TileOverlay() +// overlay.canReplaceMapContent = false +// overlay.minimumZ = UserDefaults.mapTileServer.zoomRange.startIndex +// overlay.maximumZ = UserDefaults.mapTileServer.zoomRange.endIndex +// mapView.addOverlay(overlay, level: UserDefaults.mapTilesAboveLabels ? .aboveLabels : .aboveRoads) +// case .satellite: +// mapView.mapType = .satellite +// case .hybrid: +// mapView.mapType = .hybrid +// default: +// mapView.mapType = .standard +// } +// } +// private func setMapOverlays(mapView: MKMapView) { +// // Weather radar +// if UserDefaults.enableOverlayServer { +// let locale = Locale.current +// if locale.region?.identifier ?? "no locale" == "US" { +// let overlay = MKTileOverlay(urlTemplate: selectedWeatherLayer.tileUrl) +// overlay.canReplaceMapContent = false +// overlay.minimumZ = selectedWeatherLayer.zoomRange.startIndex +// overlay.maximumZ = selectedWeatherLayer.zoomRange.endIndex +// mapView.addOverlay(overlay, level: .aboveLabels) +// } +// } +// } +// +// func makeUIView(context: Context) -> MKMapView { +// currentMapLayer = nil +// mapView.delegate = context.coordinator +// self.configureMap(mapView: mapView) +// return mapView +// } +// func updateUIView(_ mapView: MKMapView, context: Context) { +// // Set selected map base layer +// setMapBaseLayer(mapView: mapView) +// // Set map tile server and weather overlay layers +// setMapOverlays(mapView: mapView) +// let latest = positions +// .filter { $0.latest == true } +// .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } +// // Node Route Lines +// if showRouteLines { +// // Remove all existing PolyLine Overlays +// for overlay in mapView.overlays where overlay is MKPolyline { +// mapView.removeOverlay(overlay) +// } +// var lineIndex = 0 +// for position in latest { +// let nodePositions = positions.filter { $0.nodeCoordinate != nil && $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 } +// let lineCoords = nodePositions.compactMap({(position) -> CLLocationCoordinate2D in +// return position.nodeCoordinate ?? LocationHelper.DefaultLocation +// }) +// let polyline = MKPolyline(coordinates: lineCoords, count: nodePositions.count) +// polyline.title = "\(String(position.nodePosition?.num ?? 0))" +// mapView.addOverlay(polyline, level: .aboveLabels) +// lineIndex += 1 +// // There are 18 colors for lines, start over if we are at index 17 +// if lineIndex > 17 { +// lineIndex = 0 +// } +// } +// } else { +// // Remove all existing PolyLine Overlays +// for overlay in mapView.overlays where overlay is MKPolyline { +// mapView.removeOverlay(overlay) +// } +// } +// let annotationCount = waypoints.count + (showNodeHistory ? positions.count : latest.count) +// if annotationCount != mapView.annotations.count { +// Logger.services.info("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)") +// mapView.removeAnnotations(mapView.annotations) +// mapView.addAnnotations(waypoints) +// } +// mapView.addAnnotations(showNodeHistory ? positions : latest) +// if userTrackingMode == MKUserTrackingMode.none { +// mapView.showsUserLocation = false +// if UserDefaults.enableMapRecentering { +// if latest.count == 1 { +// mapView.fit(annotations: showNodeHistory ? positions : latest, andShow: true) +// } else { +// mapView.fitAllAnnotations() +// } +// } +// } else { +// mapView.showsUserLocation = true +// } +// mapView.setUserTrackingMode(userTrackingMode, animated: true) +// } +// func makeCoordinator() -> MapCoordinator { +// return Coordinator(self) +// } +// final class MapCoordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate { +// var parent: MapViewSwiftUI +// var longPressRecognizer = UILongPressGestureRecognizer() +// init(_ parent: MapViewSwiftUI) { +// self.parent = parent +// super.init() +// self.longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(longPressHandler)) +// self.longPressRecognizer.minimumPressDuration = 0.5 +// self.longPressRecognizer.cancelsTouchesInView = true +// self.longPressRecognizer.delegate = self +// self.parent.mapView.addGestureRecognizer(longPressRecognizer) +// } +// func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { +// switch annotation { +// case let positionAnnotation as PositionEntity: +// let reuseID = String(positionAnnotation.nodePosition?.num ?? 0) + "-" + String(positionAnnotation.time?.timeIntervalSince1970 ?? 0) +// let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: reuseID ) +// annotationView.tag = -1 +// annotationView.canShowCallout = true +// if positionAnnotation.latest { +// annotationView.markerTintColor = UIColor(hex: UInt32(positionAnnotation.nodePosition?.num ?? 0)).darker() +// annotationView.displayPriority = .required +// annotationView.titleVisibility = .visible +// } else { +// annotationView.markerTintColor = UIColor(hex: UInt32(positionAnnotation.nodePosition?.num ?? 0)).lighter() +// annotationView.displayPriority = .defaultHigh +// annotationView.titleVisibility = .adaptive +// } +// annotationView.tag = -1 +// annotationView.canShowCallout = true +// annotationView.titleVisibility = .adaptive +// let leftIcon = UIImageView(image: annotationView.glyphText?.image()) +// leftIcon.backgroundColor = UIColor(.indigo) +// annotationView.leftCalloutAccessoryView = leftIcon +// let subtitle = UILabel() +// subtitle.text = "Long Name: \(positionAnnotation.nodePosition?.user?.longName ?? "Unknown") \n" +// subtitle.text? += "Latitude: \(String(format: "%.5f", positionAnnotation.coordinate.latitude)) \n" +// subtitle.text! += "Longitude: \(String(format: "%.5f", positionAnnotation.coordinate.longitude)) \n" +// let distanceFormatter = MKDistanceFormatter() +// subtitle.text! += "Altitude: \(distanceFormatter.string(fromDistance: Double(positionAnnotation.altitude))) \n" +// if positionAnnotation.nodePosition?.metadata != nil { +// if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.client || +// DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.clientMute || +// DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.routerClient { +// annotationView.glyphImage = UIImage(systemName: "flipphone") +// } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.repeater { +// annotationView.glyphImage = UIImage(systemName: "repeat") +// } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.router { +// annotationView.glyphImage = UIImage(systemName: "wifi.router.fill") +// } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.tracker { +// annotationView.glyphImage = UIImage(systemName: "location.viewfinder") +// } else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.sensor { +// annotationView.glyphImage = UIImage(systemName: "sensor") +// } +// let pf = PositionFlags(rawValue: Int(positionAnnotation.nodePosition?.metadata?.positionFlags ?? 3)) +// if pf.contains(.Satsinview) { +// subtitle.text! += "Sats in view: \(String(positionAnnotation.satsInView)) \n" +// } +// if pf.contains(.SeqNo) { +// subtitle.text! += "Sequence: \(String(positionAnnotation.seqNo)) \n" +// } +// if pf.contains(.Heading) { +// if parent.userTrackingMode != MKUserTrackingMode.followWithHeading { +// annotationView.glyphImage = UIImage(systemName: "location.north.fill")?.rotate(radians: Float(degreesToRadians(Double(positionAnnotation.heading)))) +// subtitle.text! += "Heading: \(String(positionAnnotation.heading)) \n" +// } else { +// annotationView.glyphImage = UIImage(systemName: "flipphone") +// } +// } +// if pf.contains(.Speed) { +// let formatter = MeasurementFormatter() +// formatter.locale = Locale.current +// if positionAnnotation.speed <= 1 { +// annotationView.glyphImage = UIImage(systemName: "hexagon") +// } +// subtitle.text! += "Speed: \(formatter.string(from: Measurement(value: Double(positionAnnotation.speed), unit: UnitSpeed.kilometersPerHour))) \n" +// } +// } else { +// // node metadata is nil +// annotationView.glyphImage = UIImage(systemName: "flipphone") +// } +// if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 { +// let metersAway = positionAnnotation.coordinate.distance(from: LocationHelper.currentLocation) +// subtitle.text! += "distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway))) \n" +// } +// subtitle.text! += positionAnnotation.time?.formatted() ?? "Unknown \n" +// subtitle.numberOfLines = 0 +// annotationView.detailCalloutAccessoryView = subtitle +// let detailsIcon = UIButton(type: .detailDisclosure) +// detailsIcon.setImage(UIImage(systemName: "trash"), for: .normal) +// annotationView.rightCalloutAccessoryView = detailsIcon +// return annotationView +// case let waypointAnnotation as WaypointEntity: +// let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "waypoint") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: String(waypointAnnotation.id)) +// annotationView.tag = Int(waypointAnnotation.id) +// annotationView.isEnabled = true +// annotationView.canShowCallout = true +// if waypointAnnotation.icon == 0 { +// annotationView.glyphText = "📍" +// } else { +// annotationView.glyphText = String(UnicodeScalar(Int(waypointAnnotation.icon)) ?? "📍") +// } +// annotationView.markerTintColor = UIColor(.accentColor) +// annotationView.displayPriority = .required +// annotationView.titleVisibility = .adaptive +// let leftIcon = UIImageView(image: annotationView.glyphText?.image()) +// leftIcon.backgroundColor = UIColor(.accentColor) +// annotationView.leftCalloutAccessoryView = leftIcon +// let subtitle = UILabel() +// if waypointAnnotation.longDescription?.count ?? 0 > 0 { +// subtitle.text = (waypointAnnotation.longDescription ?? "") + "\n" +// } else { +// subtitle.text = "" +// } +// if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 { +// let metersAway = waypointAnnotation.coordinate.distance(from: LocationHelper.currentLocation) +// let distanceFormatter = MKDistanceFormatter() +// subtitle.text! += "distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway))) \n" +// } +// if waypointAnnotation.created != nil { +// subtitle.text! += "Created: \(waypointAnnotation.created?.formatted() ?? "Unknown") \n" +// } +// if waypointAnnotation.lastUpdated != nil { +// subtitle.text! += "Updated: \(waypointAnnotation.lastUpdated?.formatted() ?? "Unknown") \n" +// } +// if waypointAnnotation.expire != nil { +// subtitle.text! += "Expires: \(waypointAnnotation.expire?.formatted() ?? "Unknown") \n" +// } +// subtitle.numberOfLines = 0 +// annotationView.detailCalloutAccessoryView = subtitle +// let editIcon = UIButton(type: .detailDisclosure) +// editIcon.setImage(UIImage(systemName: "square.and.pencil"), for: .normal) +// annotationView.rightCalloutAccessoryView = editIcon +// return annotationView +// default: return nil +// } +// } +// func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) { +// switch view.annotation { +// case _ as WaypointEntity: +// // Only Allow Edit for waypoint annotations with a id +// if view.tag > 0 { +// parent.onWaypointEdit(view.tag) +// } +// default: break +// } +// } +// @objc func longPressHandler(_ gesture: UILongPressGestureRecognizer) { +// if gesture.state != UIGestureRecognizer.State.ended { +// return +// } else if gesture.state != UIGestureRecognizer.State.began { +// // Screen Position - CGPoint +// let location = longPressRecognizer.location(in: self.parent.mapView) +// // Map Coordinate - CLLocationCoordinate2D +// let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView) +// let annotation = MKPointAnnotation() +// annotation.title = "Dropped Pin" +// annotation.coordinate = coordinate +// parent.mapView.addAnnotation(annotation) +// UINotificationFeedbackGenerator().notificationOccurred(.success) +// parent.onLongPress(coordinate) +// } +// } +// public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { +// if let tileOverlay = overlay as? MKTileOverlay { +// return MKTileOverlayRenderer(tileOverlay: tileOverlay) +// } else { +// if let routePolyline = overlay as? MKPolyline { +// let titleString = routePolyline.title ?? "0" +// let renderer = MKPolylineRenderer(polyline: routePolyline) +// renderer.strokeColor = UIColor(hex: UInt32(titleString) ?? 0).lighter() +// renderer.lineWidth = 8 +// return renderer +// } +// if let polygon = overlay as? MKPolygon { +// let renderer = MKPolygonRenderer(polygon: polygon) +// renderer.fillColor = UIColor.purple.withAlphaComponent(0.2) +// renderer.strokeColor = .purple.withAlphaComponent(0.7) +// return renderer +// } +// return MKOverlayRenderer(overlay: overlay) +// } +// } +// } +// /// is supposed to be located in the folder with the map name +// public struct DefaultTile: Hashable { +// let tileName: String +// let tileType: String +// public init(tileName: String, tileType: String) { +// self.tileName = tileName +// self.tileType = tileType +// } +// } +// public struct CustomMapOverlay: Equatable, Hashable { +// let mapName: String +// let tileType: String +// var canReplaceMapContent: Bool +// var minimumZoomLevel: Int? +// var maximumZoomLevel: Int? +// let defaultTile: DefaultTile? +// public init( +// mapName: String, +// tileType: String, +// canReplaceMapContent: Bool = true, // false for transparent tiles +// minimumZoomLevel: Int? = nil, +// maximumZoomLevel: Int? = nil, +// defaultTile: DefaultTile? = nil +// ) { +// self.mapName = mapName +// self.tileType = tileType +// self.canReplaceMapContent = canReplaceMapContent +// self.minimumZoomLevel = minimumZoomLevel +// self.maximumZoomLevel = maximumZoomLevel +// self.defaultTile = defaultTile +// } +// public init?( +// mapName: String?, +// tileType: String, +// canReplaceMapContent: Bool = true, // false for transparent tiles +// minimumZoomLevel: Int? = nil, +// maximumZoomLevel: Int? = nil, +// defaultTile: DefaultTile? = nil +// ) { +// if mapName == nil || mapName! == "" { +// return nil +// } +// self.mapName = mapName! +// self.tileType = tileType +// self.canReplaceMapContent = canReplaceMapContent +// self.minimumZoomLevel = minimumZoomLevel +// self.maximumZoomLevel = maximumZoomLevel +// self.defaultTile = defaultTile +// } +// } +//} diff --git a/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift b/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift index ed48e1a4..456472e0 100644 --- a/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift +++ b/Meshtastic/Views/MapKitMap/WaypointFormMapKit.swift @@ -1,266 +1,266 @@ +//// +//// WaypointFormView.swift +//// Meshtastic +//// +//// Copyright Garth Vander Houwen 1/10/23. +//// // -// WaypointFormView.swift -// Meshtastic +//import CoreLocation +//import MeshtasticProtobufs +//import OSLog +//import SwiftUI // -// Copyright Garth Vander Houwen 1/10/23. +//struct WaypointFormMapKit: View { // - -import CoreLocation -import MeshtasticProtobufs -import OSLog -import SwiftUI - -struct WaypointFormMapKit: View { - - @EnvironmentObject var bleManager: BLEManager - @Environment(\.dismiss) private var dismiss - @State var coordinate: WaypointCoordinate - @FocusState private var iconIsFocused: Bool - @State private var name: String = "" - @State private var description: String = "" - @State private var icon: String = "📍" - @State private var latitude: Double = 0 - @State private var longitude: Double = 0 - @State private var expires: Bool = false - @State private var expire: Date = Date.now.addingTimeInterval(60 * 480) // 1 minute * 480 = 8 Hours - @State private var locked: Bool = false - @State private var lockedTo: Int64 = 0 - - var body: some View { - - Form { - let distance = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude).distance(from: CLLocation(latitude: coordinate.coordinate?.latitude ?? 0, longitude: coordinate.coordinate?.longitude ?? 0)) - Section(header: Text((coordinate.waypointId > 0) ? "Editing Waypoint" : "Create Waypoint")) { - HStack { - Text("Location: \(String(format: "%.5f", latitude) + "," + String(format: "%.5f", longitude))") - .textSelection(.enabled) - .foregroundColor(Color.gray) - .font(.caption2) - if coordinate.coordinate?.latitude ?? 0 != 0 && coordinate.coordinate?.longitude ?? 0 != 0 { - DistanceText(meters: distance) - .foregroundColor(Color.gray) - .font(.caption2) - } - } - HStack { - Text("Name") - Spacer() - TextField( - "Name", - text: $name, - axis: .vertical - ) - .foregroundColor(Color.gray) - .onChange(of: name) { - var totalBytes = name.utf8.count - // Only mess with the value if it is too big - while totalBytes > 30 { - name = String(name.dropLast()) - totalBytes = name.utf8.count - } - if totalBytes > 30 { - name = String(name.dropLast()) - } - } - } - HStack { - Text("Description") - Spacer() - TextField( - "Description", - text: $description, - axis: .vertical - ) - .foregroundColor(Color.gray) - .onChange(of: description) { - var totalBytes = description.utf8.count - // Only mess with the value if it is too big - while totalBytes > 100 { - description = String(description.dropLast()) - totalBytes = description.utf8.count - } - } - } - HStack { - Text("Icon") - Spacer() - EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji") - .font(.title) - .focused($iconIsFocused) - .onChange(of: icon) { _, value in - - // If you have anything other than emojis in your string make it empty - if !value.onlyEmojis() { - icon = "" - } - // If a second emoji is entered delete the first one - if value.count >= 1 { - - if value.count > 1 { - let index = value.index(value.startIndex, offsetBy: 1) - icon = String(value[index]) - } - iconIsFocused = false - } - } - - } - Toggle(isOn: $expires) { - Label("Expires", systemImage: "clock.badge.xmark") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - if expires { - DatePicker("Expire", selection: $expire, in: Date.now...) - .datePickerStyle(.compact) - .font(.callout) - } - Toggle(isOn: $locked) { - Label("Locked", systemImage: "lock") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - } - } - HStack { - Button { - - var newWaypoint = Waypoint() - // Loading a waypoint from edit - if coordinate.waypointId > 0 { - newWaypoint.id = UInt32(coordinate.waypointId) - let waypoint = getWaypoint(id: Int64(coordinate.waypointId), context: bleManager.context) - newWaypoint.latitudeI = waypoint.latitudeI - newWaypoint.longitudeI = waypoint.longitudeI - } else { - // New waypoint - newWaypoint.id = UInt32.random(in: UInt32(UInt8.max).. 0 ? name : "Dropped Pin" - newWaypoint.description_p = description - // Unicode scalar value for the icon emoji string - let unicodeScalers = icon.unicodeScalars - // First element as an UInt32 - let unicode = unicodeScalers[unicodeScalers.startIndex].value - newWaypoint.icon = unicode - if locked { - if lockedTo == 0 { - newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num) - } else { - newWaypoint.lockedTo = UInt32(lockedTo) - } - } - if expires { - newWaypoint.expire = UInt32(expire.timeIntervalSince1970) - } else { - newWaypoint.expire = 0 - } - if bleManager.sendWaypoint(waypoint: newWaypoint) { - dismiss() - } else { - dismiss() - Logger.mesh.error("Send waypoint failed") - } - } label: { - Label("Send", systemImage: "arrow.up") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.regular) - .disabled(bleManager.connectedPeripheral == nil) - .padding(.bottom) - - Button(role: .cancel) { - dismiss() - } label: { - Label("cancel", systemImage: "x.circle") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.regular) - .padding(.bottom) - - if coordinate.waypointId > 0 { - - Menu { - Button("For me", action: { - let waypoint = getWaypoint(id: Int64(coordinate.waypointId), context: bleManager.context) - bleManager.context.delete(waypoint) - do { - try bleManager.context.save() - } catch { - bleManager.context.rollback() - } - dismiss() }) - Button("For everyone", action: { - var newWaypoint = Waypoint() - - if coordinate.waypointId > 0 { - newWaypoint.id = UInt32(coordinate.waypointId) - } - newWaypoint.name = name.count > 0 ? name : "Dropped Pin" - newWaypoint.description_p = description - newWaypoint.latitudeI = Int32(coordinate.coordinate?.latitude ?? 0 * 1e7) - newWaypoint.longitudeI = Int32(coordinate.coordinate?.longitude ?? 0 * 1e7) - // Unicode scalar value for the icon emoji string - let unicodeScalers = icon.unicodeScalars - // First element as an UInt32 - let unicode = unicodeScalers[unicodeScalers.startIndex].value - newWaypoint.icon = unicode - if locked { - if lockedTo == 0 { - newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num) - } else { - newWaypoint.lockedTo = UInt32(lockedTo) - } - } - newWaypoint.expire = 1 - if bleManager.sendWaypoint(waypoint: newWaypoint) { - dismiss() - } else { - dismiss() - Logger.mesh.error("Send waypoint failed") - } - }) - } - label: { - Label("delete", systemImage: "trash") - .foregroundColor(.red) - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.regular) - .padding(.bottom) - } - } - .onAppear { - if coordinate.waypointId > 0 { - let waypoint = getWaypoint(id: Int64(coordinate.waypointId), context: bleManager.context) - name = waypoint.name ?? "Dropped Pin" - description = waypoint.longDescription ?? "" - icon = String(UnicodeScalar(Int(waypoint.icon)) ?? "📍") - latitude = Double(waypoint.latitudeI) / 1e7 - longitude = Double(waypoint.longitudeI) / 1e7 - if waypoint.expire != nil { - expires = true - expire = waypoint.expire ?? Date() - } else { - expires = false - } - if waypoint.locked > 0 { - locked = true - lockedTo = waypoint.locked - } - } else { - name = "" - description = "" - locked = false - expires = false - expire = Date.now.addingTimeInterval(60 * 480) - icon = "📍" - latitude = coordinate.coordinate?.latitude ?? 0 - longitude = coordinate.coordinate?.longitude ?? 0 - } - } - } -} +// @EnvironmentObject var bleManager: BLEManager +// @Environment(\.dismiss) private var dismiss +// @State var coordinate: WaypointCoordinate +// @FocusState private var iconIsFocused: Bool +// @State private var name: String = "" +// @State private var description: String = "" +// @State private var icon: String = "📍" +// @State private var latitude: Double = 0 +// @State private var longitude: Double = 0 +// @State private var expires: Bool = false +// @State private var expire: Date = Date.now.addingTimeInterval(60 * 480) // 1 minute * 480 = 8 Hours +// @State private var locked: Bool = false +// @State private var lockedTo: Int64 = 0 +// +// var body: some View { +// +// Form { +// let distance = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude).distance(from: CLLocation(latitude: coordinate.coordinate?.latitude ?? 0, longitude: coordinate.coordinate?.longitude ?? 0)) +// Section(header: Text((coordinate.waypointId > 0) ? "Editing Waypoint" : "Create Waypoint")) { +// HStack { +// Text("Location: \(String(format: "%.5f", latitude) + "," + String(format: "%.5f", longitude))") +// .textSelection(.enabled) +// .foregroundColor(Color.gray) +// .font(.caption2) +// if coordinate.coordinate?.latitude ?? 0 != 0 && coordinate.coordinate?.longitude ?? 0 != 0 { +// DistanceText(meters: distance) +// .foregroundColor(Color.gray) +// .font(.caption2) +// } +// } +// HStack { +// Text("Name") +// Spacer() +// TextField( +// "Name", +// text: $name, +// axis: .vertical +// ) +// .foregroundColor(Color.gray) +// .onChange(of: name) { +// var totalBytes = name.utf8.count +// // Only mess with the value if it is too big +// while totalBytes > 30 { +// name = String(name.dropLast()) +// totalBytes = name.utf8.count +// } +// if totalBytes > 30 { +// name = String(name.dropLast()) +// } +// } +// } +// HStack { +// Text("Description") +// Spacer() +// TextField( +// "Description", +// text: $description, +// axis: .vertical +// ) +// .foregroundColor(Color.gray) +// .onChange(of: description) { +// var totalBytes = description.utf8.count +// // Only mess with the value if it is too big +// while totalBytes > 100 { +// description = String(description.dropLast()) +// totalBytes = description.utf8.count +// } +// } +// } +// HStack { +// Text("Icon") +// Spacer() +// EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji") +// .font(.title) +// .focused($iconIsFocused) +// .onChange(of: icon) { _, value in +// +// // If you have anything other than emojis in your string make it empty +// if !value.onlyEmojis() { +// icon = "" +// } +// // If a second emoji is entered delete the first one +// if value.count >= 1 { +// +// if value.count > 1 { +// let index = value.index(value.startIndex, offsetBy: 1) +// icon = String(value[index]) +// } +// iconIsFocused = false +// } +// } +// +// } +// Toggle(isOn: $expires) { +// Label("Expires", systemImage: "clock.badge.xmark") +// } +// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) +// if expires { +// DatePicker("Expire", selection: $expire, in: Date.now...) +// .datePickerStyle(.compact) +// .font(.callout) +// } +// Toggle(isOn: $locked) { +// Label("Locked", systemImage: "lock") +// } +// .toggleStyle(SwitchToggleStyle(tint: .accentColor)) +// } +// } +// HStack { +// Button { +// +// var newWaypoint = Waypoint() +// // Loading a waypoint from edit +// if coordinate.waypointId > 0 { +// newWaypoint.id = UInt32(coordinate.waypointId) +// let waypoint = getWaypoint(id: Int64(coordinate.waypointId), context: bleManager.context) +// newWaypoint.latitudeI = waypoint.latitudeI +// newWaypoint.longitudeI = waypoint.longitudeI +// } else { +// // New waypoint +// newWaypoint.id = UInt32.random(in: UInt32(UInt8.max).. 0 ? name : "Dropped Pin" +// newWaypoint.description_p = description +// // Unicode scalar value for the icon emoji string +// let unicodeScalers = icon.unicodeScalars +// // First element as an UInt32 +// let unicode = unicodeScalers[unicodeScalers.startIndex].value +// newWaypoint.icon = unicode +// if locked { +// if lockedTo == 0 { +// newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num) +// } else { +// newWaypoint.lockedTo = UInt32(lockedTo) +// } +// } +// if expires { +// newWaypoint.expire = UInt32(expire.timeIntervalSince1970) +// } else { +// newWaypoint.expire = 0 +// } +// if bleManager.sendWaypoint(waypoint: newWaypoint) { +// dismiss() +// } else { +// dismiss() +// Logger.mesh.error("Send waypoint failed") +// } +// } label: { +// Label("Send", systemImage: "arrow.up") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.regular) +// .disabled(bleManager.connectedPeripheral == nil) +// .padding(.bottom) +// +// Button(role: .cancel) { +// dismiss() +// } label: { +// Label("cancel", systemImage: "x.circle") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.regular) +// .padding(.bottom) +// +// if coordinate.waypointId > 0 { +// +// Menu { +// Button("For me", action: { +// let waypoint = getWaypoint(id: Int64(coordinate.waypointId), context: bleManager.context) +// bleManager.context.delete(waypoint) +// do { +// try bleManager.context.save() +// } catch { +// bleManager.context.rollback() +// } +// dismiss() }) +// Button("For everyone", action: { +// var newWaypoint = Waypoint() +// +// if coordinate.waypointId > 0 { +// newWaypoint.id = UInt32(coordinate.waypointId) +// } +// newWaypoint.name = name.count > 0 ? name : "Dropped Pin" +// newWaypoint.description_p = description +// newWaypoint.latitudeI = Int32(coordinate.coordinate?.latitude ?? 0 * 1e7) +// newWaypoint.longitudeI = Int32(coordinate.coordinate?.longitude ?? 0 * 1e7) +// // Unicode scalar value for the icon emoji string +// let unicodeScalers = icon.unicodeScalars +// // First element as an UInt32 +// let unicode = unicodeScalers[unicodeScalers.startIndex].value +// newWaypoint.icon = unicode +// if locked { +// if lockedTo == 0 { +// newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num) +// } else { +// newWaypoint.lockedTo = UInt32(lockedTo) +// } +// } +// newWaypoint.expire = 1 +// if bleManager.sendWaypoint(waypoint: newWaypoint) { +// dismiss() +// } else { +// dismiss() +// Logger.mesh.error("Send waypoint failed") +// } +// }) +// } +// label: { +// Label("delete", systemImage: "trash") +// .foregroundColor(.red) +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.regular) +// .padding(.bottom) +// } +// } +// .onAppear { +// if coordinate.waypointId > 0 { +// let waypoint = getWaypoint(id: Int64(coordinate.waypointId), context: bleManager.context) +// name = waypoint.name ?? "Dropped Pin" +// description = waypoint.longDescription ?? "" +// icon = String(UnicodeScalar(Int(waypoint.icon)) ?? "📍") +// latitude = Double(waypoint.latitudeI) / 1e7 +// longitude = Double(waypoint.longitudeI) / 1e7 +// if waypoint.expire != nil { +// expires = true +// expire = waypoint.expire ?? Date() +// } else { +// expires = false +// } +// if waypoint.locked > 0 { +// locked = true +// lockedTo = waypoint.locked +// } +// } else { +// name = "" +// description = "" +// locked = false +// expires = false +// expire = Date.now.addingTimeInterval(60 * 480) +// icon = "📍" +// latitude = coordinate.coordinate?.latitude ?? 0 +// longitude = coordinate.coordinate?.longitude ?? 0 +// } +// } +// } +//} diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 4a4028e2..14c826ed 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -302,9 +302,13 @@ struct UserList: View { let loraPredicate = NSPredicate(format: "userNode.viaMqtt == NO") predicates.append(loraPredicate) } else { - let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES") + let mqttPredicate = NSPredicate(format: "userNode.viaMqtt == YES AND userNode.hopsAway == 0") predicates.append(mqttPredicate) } + } else { + /// Only show mqtt nodes that can be contacted (zero hops) on the default key + // let bothPredicate = NSPredicate(format: "userNode.viaMqtt == YES AND userNode.hopsAway == 0 OR userNode.viaMqtt == NO") + // predicates.append(bothPredicate) } /// Roles if roleFilter && deviceRoles.count > 0 { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index bec035b7..36d9a915 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -24,7 +24,7 @@ struct NodeMapSwiftUI: View { @Namespace var mapScope @State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: .all, showsTraffic: true) @State var position = MapCameraPosition.automatic - @State var distance = 0.0 + @State var distance = 10000.0 @State var scene: MKLookAroundScene? @State var isLookingAround = false @State var isShowingAltitude = false diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 66f58a07..b75d5822 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -156,39 +156,36 @@ struct NodeListItem: View { Image(systemName: "scroll") .symbolRenderingMode(.hierarchical) .font(.callout) - .frame(width: 30) Text("Logs:") .foregroundColor(.gray) - .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) + .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption2) + .allowsTightening(true) if node.hasDeviceMetrics { Image(systemName: "flipphone") .symbolRenderingMode(.hierarchical) .font(.callout) - .frame(width: 30) } if node.hasPositions { Image(systemName: "mappin.and.ellipse") .symbolRenderingMode(.hierarchical) .font(.callout) - .frame(width: 30) + } if node.hasEnvironmentMetrics { Image(systemName: "cloud.sun.rain") .symbolRenderingMode(.hierarchical) .font(.callout) - .frame(width: 30) + } if node.hasDetectionSensorMetrics { Image(systemName: "sensor") .symbolRenderingMode(.hierarchical) .font(.callout) - .frame(width: 30) } if node.hasTraceRoutes { Image(systemName: "signpost.right.and.left") .symbolRenderingMode(.hierarchical) .font(.callout) - .frame(width: 30) } } } diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 398a12b0..45206cb0 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -37,8 +37,22 @@ struct TraceRouteLog: View { VStack { List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in Label { - Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hops?.count ?? 0) \(route.hops?.count ?? 0 == 1 ? "Hop": "Hops")") : (route.sent ? "No Response" : "Not Sent"))") - .font(.callout) + if route.response && route.hopsTowards == 0 { + Text("\(route.time?.formatted() ?? "unknown".localized) - Direct") + .font(.caption) + } else if route.response && route.hopsTowards == 1 { + Text("\(route.time?.formatted() ?? "unknown".localized) - 1 Hop") + .font(.caption) + } else if route.response { + Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.hopsTowards) Hops Towards \(route.hopsBack) Hops Back") + .font(.caption) + } else if route.sent { + Text("\(route.time?.formatted() ?? "unknown".localized) - No Response") + .font(.caption) + } else { + Text("\(route.time?.formatted() ?? "unknown".localized) - Not Sent") + .font(.caption) + } } icon: { Image(systemName: route.response ? (route.hops?.count == 0 && route.response ? "person.line.dotted.person" : "point.3.connected.trianglepath.dotted") : "person.slash") .symbolRenderingMode(.hierarchical) @@ -62,7 +76,16 @@ struct TraceRouteLog: View { Divider() ScrollView { if selectedRoute != nil { - if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 > 0 { + + if selectedRoute?.response ?? false && selectedRoute?.hopsTowards ?? 0 == 0 { + Label { + Text("Trace route received directly by \(selectedRoute?.node?.user?.longName ?? "unknown".localized) with a SNR of \(String(format: "%.2f", selectedRoute?.node?.snr ?? 0.0)) dB") + } icon: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) + } + .font(.title3) + } else if selectedRoute?.response ?? false && selectedRoute?.hopsTowards ?? 0 > 0 { Label { Text("Route: \(selectedRoute?.routeText ?? "unknown".localized)") } icon: { @@ -77,14 +100,6 @@ struct TraceRouteLog: View { .symbolRenderingMode(.hierarchical) } .font(.title3) - } else if selectedRoute?.response ?? false { - Label { - Text("Trace route received directly by \(selectedRoute?.node?.user?.longName ?? "unknown".localized) with a SNR of \(String(format: "%.2f", selectedRoute?.node?.snr ?? 0.0)) dB") - } icon: { - Image(systemName: "signpost.right.and.left") - .symbolRenderingMode(.hierarchical) - } - .font(.title3) } else if !(selectedRoute?.sent ?? true) { Label { VStack { @@ -116,7 +131,7 @@ struct TraceRouteLog: View { .symbolRenderingMode(.hierarchical) } } - if selectedRoute?.hops?.count ?? 0 >= 3 { + if false {//selectedRoute?.hops?.count ?? 0 >= 3 { HStack(alignment: .center) { GeometryReader { geometry in let size = ((geometry.size.width >= geometry.size.height ? geometry.size.height : geometry.size.width) / 2) - (idiom == .phone ? 45 : 85) diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index eb50a80f..aff3dfea 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -36,7 +36,7 @@ struct DetectionSensorConfig: View { @State var enabled = false @State var sendBell: Bool = false @State var name: String = "" - @State var detectionTriggeredHigh: Bool = true + @State var triggerType = 0 @State var usePullup: Bool = false @State var minimumBroadcastSecs = 0 @State var stateBroadcastSecs = 0 @@ -116,11 +116,13 @@ struct DetectionSensorConfig: View { } .pickerStyle(DefaultPickerStyle()) - Toggle(isOn: $detectionTriggeredHigh) { - Label("Detection trigger High", systemImage: "dial.high") - Text("Whether or not the GPIO pin state detection is triggered on HIGH (1) or LOW (0)") + Picker("TriggerType", selection: $triggerType) { + ForEach(TriggerTypes.allCases) { tt in + Text(tt.name).tag(tt.rawValue) + } } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .pickerStyle(DefaultPickerStyle()) + .listRowSeparator(.hidden) Toggle(isOn: $usePullup) { Label("Uses pullup resistor", systemImage: "arrow.up.to.line") @@ -166,7 +168,7 @@ struct DetectionSensorConfig: View { dsc.sendBell = self.sendBell dsc.name = self.name dsc.monitorPin = UInt32(self.monitorPin) - dsc.detectionTriggeredHigh = self.detectionTriggeredHigh + dsc.detectionTriggerType = TriggerTypes(rawValue: triggerType)!.protoEnumValue() dsc.usePullup = self.usePullup dsc.minimumBroadcastSecs = UInt32(self.minimumBroadcastSecs) dsc.stateBroadcastSecs = UInt32(self.stateBroadcastSecs) @@ -216,8 +218,8 @@ struct DetectionSensorConfig: View { .onChange(of: sendBell) { _, newSendBell in if newSendBell != node?.detectionSensorConfig?.sendBell { hasChanges = true } } - .onChange(of: detectionTriggeredHigh) { _, newDetectionTriggeredHigh in - if newDetectionTriggeredHigh != node?.detectionSensorConfig?.detectionTriggeredHigh { hasChanges = true } + .onChange(of: triggerType) { _, newTriggerType in + if newTriggerType != node?.detectionSensorConfig?.triggerType ?? 0 { hasChanges = true } } .onChange(of: usePullup) { _, newUsePullup in if newUsePullup != node?.detectionSensorConfig?.usePullup { hasChanges = true } @@ -244,7 +246,7 @@ struct DetectionSensorConfig: View { self.name = (node?.detectionSensorConfig?.name ?? "") self.monitorPin = Int(node?.detectionSensorConfig?.monitorPin ?? 0) self.usePullup = (node?.detectionSensorConfig?.usePullup ?? false) - self.detectionTriggeredHigh = (node?.detectionSensorConfig?.detectionTriggeredHigh ?? true) + self.triggerType = Int(node?.detectionSensorConfig?.triggerType ?? 0) self.minimumBroadcastSecs = Int(node?.detectionSensorConfig?.minimumBroadcastSecs ?? 45) self.stateBroadcastSecs = Int(node?.detectionSensorConfig?.stateBroadcastSecs ?? 0) diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift index 75b69463..8ddd5816 100644 --- a/Meshtastic/Views/Settings/Config/SecurityConfig.swift +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -26,7 +26,11 @@ struct SecurityConfig: View { @State var privateKey = "" @State var hasValidPrivateKey: Bool = false @State var adminKey = "" + @State var adminKey2 = "" + @State var adminKey3 = "" @State var hasValidAdminKey: Bool = true + @State var hasValidAdminKey2: Bool = true + @State var hasValidAdminKey3: Bool = true @State var isManaged = false @State var serialEnabled = false @State var debugLogApiEnabled = false @@ -49,8 +53,7 @@ struct SecurityConfig: View { Text("Sent out to other nodes on the mesh to allow them to compute a shared secret key.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) - } - VStack(alignment: .leading) { + Divider() Label("Private Key", systemImage: "key.fill") SecureInput("Private Key", text: $privateKey, isValid: $hasValidPrivateKey) .background( @@ -60,11 +63,34 @@ struct SecurityConfig: View { Text("Used to create a shared key with a remote device.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) - } - VStack(alignment: .leading) { - Label("Admin Key", systemImage: "key.viewfinder") - SecureInput("Admin Key", text: $adminKey, isValid: $hasValidAdminKey) - Text("The public key authorized to send admin messages to this node.") + Divider() + Label("Primary Admin Key", systemImage: "key.viewfinder") + SecureInput("Primary Admin Key", text: $adminKey, isValid: $hasValidAdminKey) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(hasValidAdminKey ? Color.clear : Color.red, lineWidth: 2.0) + ) + Text("The primary public key authorized to send admin messages to this node.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) + Divider() + Label("Secondary Admin Key", systemImage: "key.viewfinder") + SecureInput("Secondary Admin Key", text: $adminKey2, isValid: $hasValidAdminKey2) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(hasValidAdminKey2 ? Color.clear : Color.red, lineWidth: 2.0) + ) + Text("The secondary public key authorized to send admin messages to this node.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) + Divider() + Label("Tertiary Admin Key", systemImage: "key.viewfinder") + SecureInput("Tertiary Admin Key", text: $adminKey2, isValid: $hasValidAdminKey2) + .background( + RoundedRectangle(cornerRadius: 10.0) + .stroke(hasValidAdminKey3 ? Color.clear : Color.red, lineWidth: 2.0) + ) + Text("The tertiarypublic key authorized to send admin messages to this node.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) } @@ -147,6 +173,28 @@ struct SecurityConfig: View { } hasChanges = true } + .onChange(of: adminKey2) { _, key in + let tempKey = Data(base64Encoded: key) ?? Data() + if key.isEmpty { + hasValidAdminKey2 = true + } else if tempKey.count == 32 { + hasValidAdminKey2 = true + } else { + hasValidAdminKey2 = false + } + hasChanges = true + } + .onChange(of: adminKey3) { _, key in + let tempKey = Data(base64Encoded: key) ?? Data() + if key.isEmpty { + hasValidAdminKey3 = true + } else if tempKey.count == 32 { + hasValidAdminKey3 = true + } else { + hasValidAdminKey3 = false + } + hasChanges = true + } .onFirstAppear { // Need to request a DeviceConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node { @@ -186,7 +234,7 @@ struct SecurityConfig: View { var config = Config.SecurityConfig() config.publicKey = Data(base64Encoded: publicKey) ?? Data() config.privateKey = Data(base64Encoded: privateKey) ?? Data() - config.adminKey = [Data(base64Encoded: adminKey) ?? Data()] + config.adminKey = [Data(base64Encoded: adminKey) ?? Data(), Data(base64Encoded: adminKey2) ?? Data(), Data(base64Encoded: adminKey3) ?? Data()] config.isManaged = isManaged config.serialEnabled = serialEnabled config.debugLogApiEnabled = debugLogApiEnabled @@ -211,6 +259,8 @@ struct SecurityConfig: View { self.publicKey = node?.securityConfig?.publicKey?.base64EncodedString() ?? "" self.privateKey = node?.securityConfig?.privateKey?.base64EncodedString() ?? "" self.adminKey = node?.securityConfig?.adminKey?.base64EncodedString() ?? "" + self.adminKey2 = node?.securityConfig?.adminKey?.base64EncodedString() ?? "" + self.adminKey3 = node?.securityConfig?.adminKey?.base64EncodedString() ?? "" self.isManaged = node?.securityConfig?.isManaged ?? false self.serialEnabled = node?.securityConfig?.serialEnabled ?? false self.debugLogApiEnabled = node?.securityConfig?.debugLogApiEnabled ?? false diff --git a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift index c756d94d..867648a9 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/atak.pb.swift @@ -324,7 +324,7 @@ public struct TAKPacket { /// /// Generic CoT detail XML - /// May be compressed / truncated by the sender + /// May be compressed / truncated by the sender (EUD) public var detail: Data { get { if case .detail(let v)? = payloadVariant {return v} @@ -346,7 +346,7 @@ public struct TAKPacket { case chat(GeoChat) /// /// Generic CoT detail XML - /// May be compressed / truncated by the sender + /// May be compressed / truncated by the sender (EUD) case detail(Data) #if !swift(>=4.1) diff --git a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift index 37832baa..38df8ea0 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/config.pb.swift @@ -1344,6 +1344,18 @@ public struct Config { /// /// Singapore 923mhz case sg923 // = 18 + + /// + /// Philippines 433mhz + case ph433 // = 19 + + /// + /// Philippines 868mhz + case ph868 // = 20 + + /// + /// Philippines 915mhz + case ph915 // = 21 case UNRECOGNIZED(Int) public init() { @@ -1371,6 +1383,9 @@ public struct Config { case 16: self = .my433 case 17: self = .my919 case 18: self = .sg923 + case 19: self = .ph433 + case 20: self = .ph868 + case 21: self = .ph915 default: self = .UNRECOGNIZED(rawValue) } } @@ -1396,6 +1411,9 @@ public struct Config { case .my433: return 16 case .my919: return 17 case .sg923: return 18 + case .ph433: return 19 + case .ph868: return 20 + case .ph915: return 21 case .UNRECOGNIZED(let i): return i } } @@ -1747,6 +1765,9 @@ extension Config.LoRaConfig.RegionCode: CaseIterable { .my433, .my919, .sg923, + .ph433, + .ph868, + .ph915, ] } @@ -2834,6 +2855,9 @@ extension Config.LoRaConfig.RegionCode: SwiftProtobuf._ProtoNameProviding { 16: .same(proto: "MY_433"), 17: .same(proto: "MY_919"), 18: .same(proto: "SG_923"), + 19: .same(proto: "PH_433"), + 20: .same(proto: "PH_868"), + 21: .same(proto: "PH_915"), ] } diff --git a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift index 6bddec60..f604d4a7 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/mesh.pb.swift @@ -126,6 +126,10 @@ public enum HardwareModel: SwiftProtobuf.Enum { /// Heltec HRU-3601: https://heltec.org/project/hru-3601/ case heltecHru3601 // = 23 + /// + /// Heltec Wireless Bridge + case heltecWirelessBridge // = 24 + /// /// B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station case stationG1 // = 25 @@ -197,7 +201,7 @@ public enum HardwareModel: SwiftProtobuf.Enum { case drDev // = 41 /// - /// M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, Paper) https://m5stack.com/ + /// M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ case m5Stack // = 42 /// @@ -355,10 +359,27 @@ public enum HardwareModel: SwiftProtobuf.Enum { /// ^^^ short A0 to switch to I2C address 0x3C case rp2040FeatherRfm95 // = 76 - /// M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, Paper) https://m5stack.com/ + /// M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ case m5StackCorebasic // = 77 case m5StackCore2 // = 78 + /// Pico2 with Waveshare Hat, same as Pico + case rpiPico2 // = 79 + + /// M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ + case m5StackCores3 // = 80 + + /// Seeed XIAO S3 DK + case seeedXiaoS3 // = 81 + + /// + /// Nordic nRF52840+Semtech SX1262 LoRa BLE Combo Module. nRF52840+SX1262 MS24SF1 + case ms24Sf1 // = 82 + + /// + /// Lilygo TLora-C6 with the new ESP32-C6 MCU + case tloraC6 // = 83 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. @@ -396,6 +417,7 @@ public enum HardwareModel: SwiftProtobuf.Enum { case 21: self = .wioWm1110 case 22: self = .rak2560 case 23: self = .heltecHru3601 + case 24: self = .heltecWirelessBridge case 25: self = .stationG1 case 26: self = .rak11310 case 27: self = .senseloraRp2040 @@ -450,6 +472,11 @@ public enum HardwareModel: SwiftProtobuf.Enum { case 76: self = .rp2040FeatherRfm95 case 77: self = .m5StackCorebasic case 78: self = .m5StackCore2 + case 79: self = .rpiPico2 + case 80: self = .m5StackCores3 + case 81: self = .seeedXiaoS3 + case 82: self = .ms24Sf1 + case 83: self = .tloraC6 case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -481,6 +508,7 @@ public enum HardwareModel: SwiftProtobuf.Enum { case .wioWm1110: return 21 case .rak2560: return 22 case .heltecHru3601: return 23 + case .heltecWirelessBridge: return 24 case .stationG1: return 25 case .rak11310: return 26 case .senseloraRp2040: return 27 @@ -535,6 +563,11 @@ public enum HardwareModel: SwiftProtobuf.Enum { case .rp2040FeatherRfm95: return 76 case .m5StackCorebasic: return 77 case .m5StackCore2: return 78 + case .rpiPico2: return 79 + case .m5StackCores3: return 80 + case .seeedXiaoS3: return 81 + case .ms24Sf1: return 82 + case .tloraC6: return 83 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -571,6 +604,7 @@ extension HardwareModel: CaseIterable { .wioWm1110, .rak2560, .heltecHru3601, + .heltecWirelessBridge, .stationG1, .rak11310, .senseloraRp2040, @@ -625,6 +659,11 @@ extension HardwareModel: CaseIterable { .rp2040FeatherRfm95, .m5StackCorebasic, .m5StackCore2, + .rpiPico2, + .m5StackCores3, + .seeedXiaoS3, + .ms24Sf1, + .tloraC6, .privateHw, ] } @@ -3262,6 +3301,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 21: .same(proto: "WIO_WM1110"), 22: .same(proto: "RAK2560"), 23: .same(proto: "HELTEC_HRU_3601"), + 24: .same(proto: "HELTEC_WIRELESS_BRIDGE"), 25: .same(proto: "STATION_G1"), 26: .same(proto: "RAK11310"), 27: .same(proto: "SENSELORA_RP2040"), @@ -3316,6 +3356,11 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 76: .same(proto: "RP2040_FEATHER_RFM95"), 77: .same(proto: "M5STACK_COREBASIC"), 78: .same(proto: "M5STACK_CORE2"), + 79: .same(proto: "RPI_PICO2"), + 80: .same(proto: "M5STACK_CORES3"), + 81: .same(proto: "SEEED_XIAO_S3"), + 82: .same(proto: "MS24SF1"), + 83: .same(proto: "TLORA_C6"), 255: .same(proto: "PRIVATE_HW"), ] } diff --git a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift index 3186c349..30a8e0a4 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/module_config.pb.swift @@ -475,13 +475,15 @@ public struct ModuleConfig { public var enabled: Bool = false /// - /// Interval in seconds of how often we can send a message to the mesh when a state change is detected + /// Interval in seconds of how often we can send a message to the mesh when a + /// trigger event is detected public var minimumBroadcastSecs: UInt32 = 0 /// - /// Interval in seconds of how often we should send a message to the mesh with the current state regardless of changes - /// When set to 0, only state changes will be broadcasted - /// Works as a sort of status heartbeat for peace of mind + /// Interval in seconds of how often we should send a message to the mesh + /// with the current state regardless of trigger events When set to 0, only + /// trigger events will be broadcasted Works as a sort of status heartbeat + /// for peace of mind public var stateBroadcastSecs: UInt32 = 0 /// @@ -500,9 +502,8 @@ public struct ModuleConfig { public var monitorPin: UInt32 = 0 /// - /// Whether or not the GPIO pin state detection is triggered on HIGH (1) - /// Otherwise LOW (0) - public var detectionTriggeredHigh: Bool = false + /// The type of trigger event to be used + public var detectionTriggerType: ModuleConfig.DetectionSensorConfig.TriggerType = .logicLow /// /// Whether or not use INPUT_PULLUP mode for GPIO pin @@ -511,6 +512,60 @@ public struct ModuleConfig { public var unknownFields = SwiftProtobuf.UnknownStorage() + public enum TriggerType: SwiftProtobuf.Enum { + public typealias RawValue = Int + + /// Event is triggered if pin is low + case logicLow // = 0 + + /// Event is triggered if pin is high + case logicHigh // = 1 + + /// Event is triggered when pin goes high to low + case fallingEdge // = 2 + + /// Event is triggered when pin goes low to high + case risingEdge // = 3 + + /// Event is triggered on every pin state change, low is considered to be + /// "active" + case eitherEdgeActiveLow // = 4 + + /// Event is triggered on every pin state change, high is considered to be + /// "active" + case eitherEdgeActiveHigh // = 5 + case UNRECOGNIZED(Int) + + public init() { + self = .logicLow + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .logicLow + case 1: self = .logicHigh + case 2: self = .fallingEdge + case 3: self = .risingEdge + case 4: self = .eitherEdgeActiveLow + case 5: self = .eitherEdgeActiveHigh + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .logicLow: return 0 + case .logicHigh: return 1 + case .fallingEdge: return 2 + case .risingEdge: return 3 + case .eitherEdgeActiveLow: return 4 + case .eitherEdgeActiveHigh: return 5 + case .UNRECOGNIZED(let i): return i + } + } + + } + public init() {} } @@ -980,20 +1035,32 @@ public struct ModuleConfig { public var airQualityInterval: UInt32 = 0 /// - /// Interval in seconds of how often we should try to send our - /// air quality metrics to the mesh + /// Enable/disable Power metrics public var powerMeasurementEnabled: Bool = false /// /// Interval in seconds of how often we should try to send our - /// air quality metrics to the mesh + /// power metrics to the mesh public var powerUpdateInterval: UInt32 = 0 /// - /// Interval in seconds of how often we should try to send our - /// air quality metrics to the mesh + /// Enable/Disable the power measurement module on-device display public var powerScreenEnabled: Bool = false + /// + /// Preferences for the (Health) Telemetry Module + /// Enable/Disable the telemetry measurement module measurement collection + public var healthMeasurementEnabled: Bool = false + + /// + /// Interval in seconds of how often we should try to send our + /// health metrics to the mesh + public var healthUpdateInterval: UInt32 = 0 + + /// + /// Enable/Disable the health telemetry module on-device display + public var healthScreenEnabled: Bool = false + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} @@ -1167,6 +1234,18 @@ public struct ModuleConfig { #if swift(>=4.2) +extension ModuleConfig.DetectionSensorConfig.TriggerType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [ModuleConfig.DetectionSensorConfig.TriggerType] = [ + .logicLow, + .logicHigh, + .fallingEdge, + .risingEdge, + .eitherEdgeActiveLow, + .eitherEdgeActiveHigh, + ] +} + extension ModuleConfig.AudioConfig.Audio_Baud: CaseIterable { // The compiler won't synthesize support with the UNRECOGNIZED case. public static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [ @@ -1266,6 +1345,7 @@ extension ModuleConfig.MapReportSettings: @unchecked Sendable {} extension ModuleConfig.RemoteHardwareConfig: @unchecked Sendable {} extension ModuleConfig.NeighborInfoConfig: @unchecked Sendable {} extension ModuleConfig.DetectionSensorConfig: @unchecked Sendable {} +extension ModuleConfig.DetectionSensorConfig.TriggerType: @unchecked Sendable {} extension ModuleConfig.AudioConfig: @unchecked Sendable {} extension ModuleConfig.AudioConfig.Audio_Baud: @unchecked Sendable {} extension ModuleConfig.PaxcounterConfig: @unchecked Sendable {} @@ -1787,7 +1867,7 @@ extension ModuleConfig.DetectionSensorConfig: SwiftProtobuf.Message, SwiftProtob 4: .standard(proto: "send_bell"), 5: .same(proto: "name"), 6: .standard(proto: "monitor_pin"), - 7: .standard(proto: "detection_triggered_high"), + 7: .standard(proto: "detection_trigger_type"), 8: .standard(proto: "use_pullup"), ] @@ -1803,7 +1883,7 @@ extension ModuleConfig.DetectionSensorConfig: SwiftProtobuf.Message, SwiftProtob case 4: try { try decoder.decodeSingularBoolField(value: &self.sendBell) }() case 5: try { try decoder.decodeSingularStringField(value: &self.name) }() case 6: try { try decoder.decodeSingularUInt32Field(value: &self.monitorPin) }() - case 7: try { try decoder.decodeSingularBoolField(value: &self.detectionTriggeredHigh) }() + case 7: try { try decoder.decodeSingularEnumField(value: &self.detectionTriggerType) }() case 8: try { try decoder.decodeSingularBoolField(value: &self.usePullup) }() default: break } @@ -1829,8 +1909,8 @@ extension ModuleConfig.DetectionSensorConfig: SwiftProtobuf.Message, SwiftProtob if self.monitorPin != 0 { try visitor.visitSingularUInt32Field(value: self.monitorPin, fieldNumber: 6) } - if self.detectionTriggeredHigh != false { - try visitor.visitSingularBoolField(value: self.detectionTriggeredHigh, fieldNumber: 7) + if self.detectionTriggerType != .logicLow { + try visitor.visitSingularEnumField(value: self.detectionTriggerType, fieldNumber: 7) } if self.usePullup != false { try visitor.visitSingularBoolField(value: self.usePullup, fieldNumber: 8) @@ -1845,13 +1925,24 @@ extension ModuleConfig.DetectionSensorConfig: SwiftProtobuf.Message, SwiftProtob if lhs.sendBell != rhs.sendBell {return false} if lhs.name != rhs.name {return false} if lhs.monitorPin != rhs.monitorPin {return false} - if lhs.detectionTriggeredHigh != rhs.detectionTriggeredHigh {return false} + if lhs.detectionTriggerType != rhs.detectionTriggerType {return false} if lhs.usePullup != rhs.usePullup {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } } +extension ModuleConfig.DetectionSensorConfig.TriggerType: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "LOGIC_LOW"), + 1: .same(proto: "LOGIC_HIGH"), + 2: .same(proto: "FALLING_EDGE"), + 3: .same(proto: "RISING_EDGE"), + 4: .same(proto: "EITHER_EDGE_ACTIVE_LOW"), + 5: .same(proto: "EITHER_EDGE_ACTIVE_HIGH"), + ] +} + extension ModuleConfig.AudioConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = ModuleConfig.protoMessageName + ".AudioConfig" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -2326,6 +2417,9 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me 8: .standard(proto: "power_measurement_enabled"), 9: .standard(proto: "power_update_interval"), 10: .standard(proto: "power_screen_enabled"), + 11: .standard(proto: "health_measurement_enabled"), + 12: .standard(proto: "health_update_interval"), + 13: .standard(proto: "health_screen_enabled"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -2344,6 +2438,9 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me case 8: try { try decoder.decodeSingularBoolField(value: &self.powerMeasurementEnabled) }() case 9: try { try decoder.decodeSingularUInt32Field(value: &self.powerUpdateInterval) }() case 10: try { try decoder.decodeSingularBoolField(value: &self.powerScreenEnabled) }() + case 11: try { try decoder.decodeSingularBoolField(value: &self.healthMeasurementEnabled) }() + case 12: try { try decoder.decodeSingularUInt32Field(value: &self.healthUpdateInterval) }() + case 13: try { try decoder.decodeSingularBoolField(value: &self.healthScreenEnabled) }() default: break } } @@ -2380,6 +2477,15 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me if self.powerScreenEnabled != false { try visitor.visitSingularBoolField(value: self.powerScreenEnabled, fieldNumber: 10) } + if self.healthMeasurementEnabled != false { + try visitor.visitSingularBoolField(value: self.healthMeasurementEnabled, fieldNumber: 11) + } + if self.healthUpdateInterval != 0 { + try visitor.visitSingularUInt32Field(value: self.healthUpdateInterval, fieldNumber: 12) + } + if self.healthScreenEnabled != false { + try visitor.visitSingularBoolField(value: self.healthScreenEnabled, fieldNumber: 13) + } try unknownFields.traverse(visitor: &visitor) } @@ -2394,6 +2500,9 @@ extension ModuleConfig.TelemetryConfig: SwiftProtobuf.Message, SwiftProtobuf._Me if lhs.powerMeasurementEnabled != rhs.powerMeasurementEnabled {return false} if lhs.powerUpdateInterval != rhs.powerUpdateInterval {return false} if lhs.powerScreenEnabled != rhs.powerScreenEnabled {return false} + if lhs.healthMeasurementEnabled != rhs.healthMeasurementEnabled {return false} + if lhs.healthUpdateInterval != rhs.healthUpdateInterval {return false} + if lhs.healthScreenEnabled != rhs.healthScreenEnabled {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift index dc1d6cce..ec5faaa4 100644 --- a/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift +++ b/MeshtasticProtobufs/Sources/meshtastic/telemetry.pb.swift @@ -144,6 +144,14 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { /// /// Custom I2C sensor implementation based on https://github.com/meshtastic/i2c-sensor case customSensor // = 29 + + /// + /// MAX30102 Pulse Oximeter and Heart-Rate Sensor + case max30102 // = 30 + + /// + /// MLX90614 non-contact IR temperature sensor. + case mlx90614 // = 31 case UNRECOGNIZED(Int) public init() { @@ -182,6 +190,8 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case 27: self = .icm20948 case 28: self = .max17048 case 29: self = .customSensor + case 30: self = .max30102 + case 31: self = .mlx90614 default: self = .UNRECOGNIZED(rawValue) } } @@ -218,6 +228,8 @@ public enum TelemetrySensorType: SwiftProtobuf.Enum { case .icm20948: return 27 case .max17048: return 28 case .customSensor: return 29 + case .max30102: return 30 + case .mlx90614: return 31 case .UNRECOGNIZED(let i): return i } } @@ -259,6 +271,8 @@ extension TelemetrySensorType: CaseIterable { .icm20948, .max17048, .customSensor, + .max30102, + .mlx90614, ] } @@ -806,7 +820,7 @@ public struct LocalStats { public var numPacketsTx: UInt32 = 0 /// - /// Number of packets received good + /// Number of packets received (both good and bad) public var numPacketsRx: UInt32 = 0 /// @@ -821,11 +835,74 @@ public struct LocalStats { /// Number of nodes total public var numTotalNodes: UInt32 = 0 + /// + /// Number of received packets that were duplicates (due to multiple nodes relaying). + /// If this number is high, there are nodes in the mesh relaying packets when it's unnecessary, for example due to the ROUTER/REPEATER role. + public var numRxDupe: UInt32 = 0 + + /// + /// Number of packets we transmitted that were a relay for others (not originating from ourselves). + public var numTxRelay: UInt32 = 0 + + /// + /// Number of times we canceled a packet to be relayed, because someone else did it before us. + /// This will always be zero for ROUTERs/REPEATERs. If this number is high, some other node(s) is/are relaying faster than you. + public var numTxRelayCanceled: UInt32 = 0 + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} } +/// +/// Health telemetry metrics +public struct HealthMetrics { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + /// + /// Heart rate (beats per minute) + public var heartBpm: UInt32 { + get {return _heartBpm ?? 0} + set {_heartBpm = newValue} + } + /// Returns true if `heartBpm` has been explicitly set. + public var hasHeartBpm: Bool {return self._heartBpm != nil} + /// Clears the value of `heartBpm`. Subsequent reads from it will return its default value. + public mutating func clearHeartBpm() {self._heartBpm = nil} + + /// + /// SpO2 (blood oxygen saturation) level + public var spO2: UInt32 { + get {return _spO2 ?? 0} + set {_spO2 = newValue} + } + /// Returns true if `spO2` has been explicitly set. + public var hasSpO2: Bool {return self._spO2 != nil} + /// Clears the value of `spO2`. Subsequent reads from it will return its default value. + public mutating func clearSpO2() {self._spO2 = nil} + + /// + /// Body temperature in degrees Celsius + public var temperature: Float { + get {return _temperature ?? 0} + set {_temperature = newValue} + } + /// Returns true if `temperature` has been explicitly set. + public var hasTemperature: Bool {return self._temperature != nil} + /// Clears the value of `temperature`. Subsequent reads from it will return its default value. + public mutating func clearTemperature() {self._temperature = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} + + fileprivate var _heartBpm: UInt32? = nil + fileprivate var _spO2: UInt32? = nil + fileprivate var _temperature: Float? = nil +} + /// /// Types of Measurements the telemetry module is equipped to handle public struct Telemetry { @@ -889,6 +966,16 @@ public struct Telemetry { set {variant = .localStats(newValue)} } + /// + /// Health telemetry metrics + public var healthMetrics: HealthMetrics { + get { + if case .healthMetrics(let v)? = variant {return v} + return HealthMetrics() + } + set {variant = .healthMetrics(newValue)} + } + public var unknownFields = SwiftProtobuf.UnknownStorage() public enum OneOf_Variant: Equatable { @@ -907,6 +994,9 @@ public struct Telemetry { /// /// Local device mesh statistics case localStats(LocalStats) + /// + /// Health telemetry metrics + case healthMetrics(HealthMetrics) #if !swift(>=4.1) public static func ==(lhs: Telemetry.OneOf_Variant, rhs: Telemetry.OneOf_Variant) -> Bool { @@ -934,6 +1024,10 @@ public struct Telemetry { guard case .localStats(let l) = lhs, case .localStats(let r) = rhs else { preconditionFailure() } return l == r }() + case (.healthMetrics, .healthMetrics): return { + guard case .healthMetrics(let l) = lhs, case .healthMetrics(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -970,6 +1064,7 @@ extension EnvironmentMetrics: @unchecked Sendable {} extension PowerMetrics: @unchecked Sendable {} extension AirQualityMetrics: @unchecked Sendable {} extension LocalStats: @unchecked Sendable {} +extension HealthMetrics: @unchecked Sendable {} extension Telemetry: @unchecked Sendable {} extension Telemetry.OneOf_Variant: @unchecked Sendable {} extension Nau7802Config: @unchecked Sendable {} @@ -1011,6 +1106,8 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 27: .same(proto: "ICM20948"), 28: .same(proto: "MAX17048"), 29: .same(proto: "CUSTOM_SENSOR"), + 30: .same(proto: "MAX30102"), + 31: .same(proto: "MLX90614"), ] } @@ -1457,6 +1554,9 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio 6: .standard(proto: "num_packets_rx_bad"), 7: .standard(proto: "num_online_nodes"), 8: .standard(proto: "num_total_nodes"), + 9: .standard(proto: "num_rx_dupe"), + 10: .standard(proto: "num_tx_relay"), + 11: .standard(proto: "num_tx_relay_canceled"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1473,6 +1573,9 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio case 6: try { try decoder.decodeSingularUInt32Field(value: &self.numPacketsRxBad) }() case 7: try { try decoder.decodeSingularUInt32Field(value: &self.numOnlineNodes) }() case 8: try { try decoder.decodeSingularUInt32Field(value: &self.numTotalNodes) }() + case 9: try { try decoder.decodeSingularUInt32Field(value: &self.numRxDupe) }() + case 10: try { try decoder.decodeSingularUInt32Field(value: &self.numTxRelay) }() + case 11: try { try decoder.decodeSingularUInt32Field(value: &self.numTxRelayCanceled) }() default: break } } @@ -1503,6 +1606,15 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if self.numTotalNodes != 0 { try visitor.visitSingularUInt32Field(value: self.numTotalNodes, fieldNumber: 8) } + if self.numRxDupe != 0 { + try visitor.visitSingularUInt32Field(value: self.numRxDupe, fieldNumber: 9) + } + if self.numTxRelay != 0 { + try visitor.visitSingularUInt32Field(value: self.numTxRelay, fieldNumber: 10) + } + if self.numTxRelayCanceled != 0 { + try visitor.visitSingularUInt32Field(value: self.numTxRelayCanceled, fieldNumber: 11) + } try unknownFields.traverse(visitor: &visitor) } @@ -1515,6 +1627,57 @@ extension LocalStats: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if lhs.numPacketsRxBad != rhs.numPacketsRxBad {return false} if lhs.numOnlineNodes != rhs.numOnlineNodes {return false} if lhs.numTotalNodes != rhs.numTotalNodes {return false} + if lhs.numRxDupe != rhs.numRxDupe {return false} + if lhs.numTxRelay != rhs.numTxRelay {return false} + if lhs.numTxRelayCanceled != rhs.numTxRelayCanceled {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension HealthMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".HealthMetrics" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "heart_bpm"), + 2: .same(proto: "spO2"), + 3: .same(proto: "temperature"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularUInt32Field(value: &self._heartBpm) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self._spO2) }() + case 3: try { try decoder.decodeSingularFloatField(value: &self._temperature) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._heartBpm { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 1) + } }() + try { if let v = self._spO2 { + try visitor.visitSingularUInt32Field(value: v, fieldNumber: 2) + } }() + try { if let v = self._temperature { + try visitor.visitSingularFloatField(value: v, fieldNumber: 3) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: HealthMetrics, rhs: HealthMetrics) -> Bool { + if lhs._heartBpm != rhs._heartBpm {return false} + if lhs._spO2 != rhs._spO2 {return false} + if lhs._temperature != rhs._temperature {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1529,6 +1692,7 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation 4: .standard(proto: "air_quality_metrics"), 5: .standard(proto: "power_metrics"), 6: .standard(proto: "local_stats"), + 7: .standard(proto: "health_metrics"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1603,6 +1767,19 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation self.variant = .localStats(v) } }() + case 7: try { + var v: HealthMetrics? + var hadOneofValue = false + if let current = self.variant { + hadOneofValue = true + if case .healthMetrics(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.variant = .healthMetrics(v) + } + }() default: break } } @@ -1637,6 +1814,10 @@ extension Telemetry: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation guard case .localStats(let v)? = self.variant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 6) }() + case .healthMetrics?: try { + guard case .healthMetrics(let v)? = self.variant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) diff --git a/Settings.bundle/Root.plist b/Settings.bundle/Root.plist index 7fb7c0cc..e67ebad7 100644 --- a/Settings.bundle/Root.plist +++ b/Settings.bundle/Root.plist @@ -68,16 +68,6 @@ DefaultValue - - Type - PSToggleSwitchSpecifier - Title - Use Legacy Mesh Map - Key - mapUseLegacy - DefaultValue - - Type PSGroupSpecifier diff --git a/Widgets/MeshActivityAttributes.swift b/Widgets/MeshActivityAttributes.swift index a2abdbba..876b75de 100644 --- a/Widgets/MeshActivityAttributes.swift +++ b/Widgets/MeshActivityAttributes.swift @@ -21,8 +21,13 @@ struct MeshActivityAttributes: ActivityAttributes { var sentPackets: UInt32 var receivedPackets: UInt32 var badReceivedPackets: UInt32 + var dupeReceivedPackets: UInt32 + var packetsSentRelay: UInt32 + var packetsCanceledRelay: UInt32 var nodesOnline: UInt32 var totalNodes: UInt32 + + public var numTxRelayCanceled: UInt32 = 0 var timerRange: ClosedRange } diff --git a/Widgets/WidgetsLiveActivity.swift b/Widgets/WidgetsLiveActivity.swift index e16e3913..7c6396a3 100644 --- a/Widgets/WidgetsLiveActivity.swift +++ b/Widgets/WidgetsLiveActivity.swift @@ -20,6 +20,9 @@ struct WidgetsLiveActivity: Widget { sentPackets: context.state.sentPackets, receivedPackets: context.state.receivedPackets, badReceivedPackets: context.state.badReceivedPackets, + dupeReceivedPackets: context.state.dupeReceivedPackets, + packetsSentRelay: context.state.packetsSentRelay, + packetsCanceledRelay: context.state.packetsCanceledRelay, nodesOnline: context.state.nodesOnline, totalNodes: context.state.totalNodes, timerRange: context.state.timerRange) @@ -28,33 +31,31 @@ struct WidgetsLiveActivity: Widget { } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { - HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) { - Spacer() - Text("Mesh") - .font(.callout) - .fontWeight(.medium) - .foregroundStyle(.primary) - .padding(.bottom, 10) - .fixedSize() - Spacer() - } if context.state.totalNodes >= 100 { Text("100+ online") - .font(.caption) + .font(.callout) .foregroundStyle(.secondary) .fixedSize() } else { Text("\(context.state.nodesOnline) of \(context.state.totalNodes) online") - .font(.caption) + .font(.callout) .foregroundStyle(.secondary) .fixedSize() } Text("\(String(format: "Ch. Util: %.2f", context.state.channelUtilization))%") - .font(.caption) + .font(.caption2) .foregroundStyle(.secondary) .fixedSize() Text("\(String(format: "Airtime: %.2f", context.state.airtime))%") - .font(.caption) + .font(.caption2) + .foregroundStyle(.secondary) + .fixedSize() + Text("Sent: \(context.state.sentPackets)") + .font(.caption2) + .foregroundStyle(.secondary) + .fixedSize() + Text("Received: \(context.state.receivedPackets)") + .font(.caption2) .foregroundStyle(.secondary) .fixedSize() } @@ -63,32 +64,27 @@ struct WidgetsLiveActivity: Widget { .tint(Color("LightIndigo")) } DynamicIslandExpandedRegion(.trailing, priority: 1) { - HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) { - Spacer() - Text("Packets") - .font(.callout) - .fontWeight(.medium) - .foregroundStyle(.primary) - .padding(.bottom, 10) - .fixedSize() - Spacer() - } - Text("Sent: \(context.state.sentPackets)") - .font(.caption) - .foregroundStyle(.secondary) - .fixedSize() - Text("Received: \(context.state.receivedPackets)") - .font(.caption) - .foregroundStyle(.secondary) - .fixedSize() + Spacer() Text("Bad: \(context.state.badReceivedPackets)") .font(.caption) .foregroundStyle(.secondary) .fixedSize() + Text("Dupe: \(context.state.dupeReceivedPackets)") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize() + Text("Relayed: \(context.state.packetsSentRelay)") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize() + Text("Relay Cancel: \(context.state.packetsCanceledRelay)") + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize() } DynamicIslandExpandedRegion(.bottom) { Text("Last Heard: \(Date().formatted())") - .font(.caption) + .font(.caption2) .fontWeight(.medium) .foregroundStyle(.tint) .fixedSize() @@ -122,7 +118,7 @@ struct WidgetsLiveActivity: Widget { struct WidgetsLiveActivity_Previews: PreviewProvider { static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G") - static let state = MeshActivityAttributes.ContentState(uptimeSeconds: 600, channelUtilization: 1.2, airtime: 3.5, sentPackets: 12587, receivedPackets: 12555, badReceivedPackets: 800, nodesOnline: 99, totalNodes: 100, timerRange: Date.now...Date(timeIntervalSinceNow: 300)) + static let state = MeshActivityAttributes.ContentState(uptimeSeconds: 600, channelUtilization: 1.2, airtime: 3.5, sentPackets: 12587, receivedPackets: 12555, badReceivedPackets: 800, dupeReceivedPackets: 100 , packetsSentRelay: 250, packetsCanceledRelay: 372, nodesOnline: 99, totalNodes: 100, timerRange: Date.now...Date(timeIntervalSinceNow: 300)) static var previews: some View { attributes @@ -150,6 +146,9 @@ struct LiveActivityView: View { var sentPackets: UInt32 var receivedPackets: UInt32 var badReceivedPackets: UInt32 + var dupeReceivedPackets: UInt32 + var packetsSentRelay: UInt32 + var packetsCanceledRelay: UInt32 var nodesOnline: UInt32 var totalNodes: UInt32 var timerRange: ClosedRange @@ -164,7 +163,8 @@ struct LiveActivityView: View { .aspectRatio(contentMode: .fit) .frame(minWidth: 25, idealWidth: 45, maxWidth: 55) Spacer() - NodeInfoView(isLuminanceReduced: _isLuminanceReduced, nodeName: nodeName, uptimeSeconds: uptimeSeconds, channelUtilization: channelUtilization, airtime: airtime, sentPackets: sentPackets, receivedPackets: receivedPackets, badReceivedPackets: badReceivedPackets, nodesOnline: nodesOnline, totalNodes: totalNodes, timerRange: timerRange) + NodeInfoView(isLuminanceReduced: _isLuminanceReduced, nodeName: nodeName, uptimeSeconds: uptimeSeconds, channelUtilization: channelUtilization, airtime: airtime, sentPackets: sentPackets, receivedPackets: receivedPackets, badReceivedPackets: badReceivedPackets, + dupeReceivedPackets: dupeReceivedPackets, packetsSentRelay: packetsSentRelay, packetsCanceledRelay: packetsCanceledRelay, nodesOnline: nodesOnline, totalNodes: totalNodes, timerRange: timerRange) Spacer() } .tint(.primary) @@ -185,6 +185,9 @@ struct NodeInfoView: View { var sentPackets: UInt32 var receivedPackets: UInt32 var badReceivedPackets: UInt32 + var dupeReceivedPackets: UInt32 + var packetsSentRelay: UInt32 + var packetsCanceledRelay: UInt32 var nodesOnline: UInt32 var totalNodes: UInt32 var timerRange: ClosedRange diff --git a/protobufs b/protobufs index 5709c0a0..c9ae7fd4 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 5709c0a05eaefccbc9cb8ed3917adbf5fd134197 +Subproject commit c9ae7fd478bffe5f954b30de6cb140821fe9ff52