Merge pull request #960 from meshtastic/2.5.9

Changes for 2.5.9
This commit is contained in:
Garth Vander Houwen 2024-10-19 08:03:13 -07:00 committed by GitHub
commit 6ee3a4ec5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 2000 additions and 937 deletions

View file

@ -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" : {

View file

@ -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 = "<group>"; };
DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityExtension.swift; sourceTree = "<group>"; };
DD05296F2B77F454008E44CD /* MeshtasticDataModelV 26.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 26.xcdatamodel"; sourceTree = "<group>"; };
DD0BE30C2CB785D8000BA445 /* MeshtasticDataModelV 46.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 46.xcdatamodel"; sourceTree = "<group>"; };
DD0BE30F2CB9FDC4000BA445 /* DetectionSensorEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorEnums.swift; sourceTree = "<group>"; };
DD0E20FF2B892E1300F2D100 /* MeshtasticDataModelV 28.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 28.xcdatamodel"; sourceTree = "<group>"; };
DD0E21002B8A6BC500F2D100 /* DeviceHardware.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = DeviceHardware.json; sourceTree = "<group>"; };
DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV14.xcdatamodel; sourceTree = "<group>"; };
@ -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 = "<group>";

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -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 {

View file

@ -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)")

View file

@ -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)

View file

@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>MeshtasticDataModelV 45.xcdatamodel</string>
<string>MeshtasticDataModelV 46.xcdatamodel</string>
</dict>
</plist>

View file

@ -0,0 +1,484 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23231" systemVersion="24B5055e" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="AmbientLightingConfigEntity" representedClassName="AmbientLightingConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="blue" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="current" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="green" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ledState" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="red" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="ambientLightingConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="ambientLightingConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="BluetoothConfigEntity" representedClassName="BluetoothConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="deviceLoggingEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="fixedPin" optional="YES" attributeType="Integer 32" defaultValueString="123456" usesScalarValueType="YES"/>
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="bluetoothConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="bluetoothConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="CannedMessageConfigEntity" representedClassName="CannedMessageConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="inputbrokerEventCcw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="inputbrokerEventCw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="inputbrokerEventPress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="inputbrokerPinA" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="inputbrokerPinB" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
<attribute name="inputbrokerPinPress" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
<attribute name="messages" optional="YES" attributeType="String" minValueString="0" maxValueString="198"/>
<attribute name="rotary1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="updown1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<relationship name="cannedMessagesConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="cannedMessageConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="ChannelEntity" representedClassName="ChannelEntity" syncable="YES" codeGenerationType="class">
<attribute name="downlinkEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="index" attributeType="Integer 32" minValueString="0" maxValueString="13" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="positionPrecision" optional="YES" attributeType="Integer 32" defaultValueString="32" usesScalarValueType="YES"/>
<attribute name="psk" optional="YES" attributeType="Binary"/>
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="uplinkEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<relationship name="myInfoChannel" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="channels" inverseEntity="MyInfoEntity"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="index"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="DetectionSensorConfigEntity" representedClassName="DetectionSensorConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="minimumBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="monitorPin" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="stateBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="triggerType" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
<attribute name="usePullup" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<relationship name="detectionSensorConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="detectionSensorConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="DeviceConfigEntity" representedClassName="DeviceConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="buttonGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="buzzerGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="debugLogEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="disableTripleClick" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="doubleTapAsButtonPress" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="isManaged" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="ledHeartbeatEnabled" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="nodeInfoBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rebroadcastMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="serialEnabled" optional="YES" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="tripleClickAsAdHocPing" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="tzdef" optional="YES" attributeType="String"/>
<relationship name="deviceConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="deviceConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="DeviceMetadataEntity" representedClassName="DeviceMetadataEntity" syncable="YES" codeGenerationType="class">
<attribute name="canShutdown" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="deviceStateVersion" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="firmwareVersion" optional="YES" attributeType="String"/>
<attribute name="hasBluetooth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hasEthernet" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hasWifi" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hwModel" optional="YES" attributeType="String"/>
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="metadataNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="metadata" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="DisplayConfigEntity" representedClassName="DisplayConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="compassNorthTop" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="displayMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="flipScreen" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="gpsFormat" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="headingBold" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="oledType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="screenCarouselInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="screenOnSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="units" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wakeOnTapOrMotion" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<relationship name="displayConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="displayConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="ExternalNotificationConfigEntity" representedClassName="ExternalNotificationConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="active" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertBellBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertBellVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertMessage" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertMessageBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertMessageVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="nagTimeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="output" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="outputBuzzer" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="outputMilliseconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="outputVibra" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="useI2SAsBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="usePWM" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="LocationEntity" representedClassName="LocationEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="routeLocation" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RouteEntity" inverseName="locations" inverseEntity="RouteEntity"/>
</entity>
<entity name="LoRaConfigEntity" representedClassName="LoRaConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="bandwidth" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="channelNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="codingRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="frequencyOffset" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="hopLimit" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ignoreMqtt" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="modemPreset" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="okToMqtt" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="overrideDutyCycle" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="overrideFrequency" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="regionCode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="spreadFactor" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sx126xRxBoostedGain" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="txEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="txPower" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="usePreset" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<relationship name="loRaConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="loRaConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="MessageEntity" representedClassName="MessageEntity" syncable="YES" codeGenerationType="class">
<attribute name="ackError" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ackSNR" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="ackTimestamp" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="admin" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="adminDescription" optional="YES" attributeType="String"/>
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isEmoji" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="messageId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="messagePayload" optional="YES" attributeType="String" defaultValueString=""/>
<attribute name="messagePayloadMarkdown" optional="YES" attributeType="String"/>
<attribute name="messageTimestamp" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="pkiEncrypted" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="portNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="publicKey" optional="YES" attributeType="Binary"/>
<attribute name="read" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="realACK" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="receivedACK" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="replyID" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="fromUser" optional="YES" maxCount="1" deletionRule="Nullify" ordered="YES" destinationEntity="UserEntity" inverseName="sentMessages" inverseEntity="UserEntity"/>
<relationship name="toUser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="receivedMessages" inverseEntity="UserEntity"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="messageId"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="MQTTConfigEntity" representedClassName="MQTTConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String"/>
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="encryptionEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="jsonEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="mapPositionPrecision" optional="YES" attributeType="Integer 32" defaultValueString="13" usesScalarValueType="YES"/>
<attribute name="mapPublishIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="mapReportingEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="password" optional="YES" attributeType="String" maxValueString="30"/>
<attribute name="proxyToClientEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="root" optional="YES" attributeType="String" defaultValueString="msh"/>
<attribute name="tlsEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="username" optional="YES" attributeType="String" maxValueString="30"/>
<relationship name="mqttConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="mqttConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="MyInfoEntity" representedClassName="MyInfoEntity" syncable="YES" codeGenerationType="class">
<attribute name="adminIndex" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="bleName" optional="YES" attributeType="String"/>
<attribute name="minAppVersion" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="myNodeNum" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="peripheralId" optional="YES" attributeType="String"/>
<attribute name="rebootCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="channels" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="ChannelEntity" inverseName="myInfoChannel" inverseEntity="ChannelEntity"/>
<relationship name="myInfoNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="myInfo" inverseEntity="NodeInfoEntity"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="myNodeNum"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="NetworkConfigEntity" representedClassName="NetworkConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="dns" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ethEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="gateway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ip" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ntpServer" optional="YES" attributeType="String"/>
<attribute name="subnet" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wifiEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="wifiMode" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="wifiPsk" optional="YES" attributeType="String" minValueString="0" maxValueString="60"/>
<attribute name="wifiSsid" optional="YES" attributeType="String" minValueString="0" maxValueString="30"/>
<relationship name="networkConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="networkConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="NodeInfoEntity" representedClassName="NodeInfoEntity" syncable="YES" codeGenerationType="class">
<attribute name="bleName" optional="YES" attributeType="String"/>
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="favorite" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="firstHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="hopsAway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="lastHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="peripheralId" optional="YES" attributeType="String"/>
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sessionExpiration" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="sessionPasskey" optional="YES" attributeType="Binary"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="viaMqtt" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<relationship name="ambientLightingConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="AmbientLightingConfigEntity" inverseName="ambientLightingConfigNode" inverseEntity="AmbientLightingConfigEntity"/>
<relationship name="bluetoothConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="BluetoothConfigEntity" inverseName="bluetoothConfigNode" inverseEntity="BluetoothConfigEntity"/>
<relationship name="cannedMessageConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="CannedMessageConfigEntity" inverseName="cannedMessagesConfigNode" inverseEntity="CannedMessageConfigEntity"/>
<relationship name="detectionSensorConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DetectionSensorConfigEntity" inverseName="detectionSensorConfigNode" inverseEntity="DetectionSensorConfigEntity"/>
<relationship name="deviceConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DeviceConfigEntity" inverseName="deviceConfigNode" inverseEntity="DeviceConfigEntity"/>
<relationship name="displayConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DisplayConfigEntity" inverseName="displayConfigNode" inverseEntity="DisplayConfigEntity"/>
<relationship name="externalNotificationConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ExternalNotificationConfigEntity" inverseName="externalNotificationConfigNode" inverseEntity="ExternalNotificationConfigEntity"/>
<relationship name="loRaConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LoRaConfigEntity" inverseName="loRaConfigNode" inverseEntity="LoRaConfigEntity"/>
<relationship name="metadata" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceMetadataEntity" inverseName="metadataNode" inverseEntity="DeviceMetadataEntity"/>
<relationship name="mqttConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MQTTConfigEntity" inverseName="mqttConfigNode" inverseEntity="MQTTConfigEntity"/>
<relationship name="myInfo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="myInfoNode" inverseEntity="MyInfoEntity"/>
<relationship name="networkConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NetworkConfigEntity" inverseName="networkConfigNode" inverseEntity="NetworkConfigEntity"/>
<relationship name="pax" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PaxCounterEntity" inverseName="paxNode" inverseEntity="PaxCounterEntity"/>
<relationship name="paxCounterConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PaxCounterConfigEntity" inverseName="paxCounterConfigNode" inverseEntity="PaxCounterConfigEntity"/>
<relationship name="positionConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PositionConfigEntity" inverseName="positionConfigNode" inverseEntity="PositionConfigEntity"/>
<relationship name="positions" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PositionEntity" inverseName="nodePosition" inverseEntity="PositionEntity"/>
<relationship name="powerConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PowerConfigEntity" inverseName="powerConfigNode" inverseEntity="PowerConfigEntity"/>
<relationship name="rangeTestConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RangeTestConfigEntity" inverseName="rangeTestConfigNode" inverseEntity="RangeTestConfigEntity"/>
<relationship name="rtttlConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RTTTLConfigEntity" inverseName="rtttlConfigNode" inverseEntity="RTTTLConfigEntity"/>
<relationship name="securityConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SecurityConfigEntity" inverseName="securityConfigNode" inverseEntity="SecurityConfigEntity"/>
<relationship name="serialConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SerialConfigEntity" inverseName="serialConfigNode" inverseEntity="SerialConfigEntity"/>
<relationship name="storeForwardConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreForwardConfigEntity" inverseName="storeForwardConfigNode" inverseEntity="StoreForwardConfigEntity"/>
<relationship name="telemetries" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TelemetryEntity" inverseName="nodeTelemetry" inverseEntity="TelemetryEntity"/>
<relationship name="telemetryConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TelemetryConfigEntity" inverseName="telemetryConfigNode" inverseEntity="TelemetryConfigEntity"/>
<relationship name="traceRoutes" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteEntity" inverseName="node" inverseEntity="TraceRouteEntity"/>
<relationship name="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="userNode" inverseEntity="UserEntity"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="num"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PaxCounterConfigEntity" representedClassName="PaxCounterConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="bleThreshold" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="updateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wifiThreshold" optional="YES" attributeType="Integer 32" defaultValueString="-80" usesScalarValueType="YES"/>
<relationship name="paxCounterConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="paxCounterConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="PaxCounterEntity" representedClassName="PaxCounterEntity" syncable="YES" codeGenerationType="class">
<attribute name="ble" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="uptime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wifi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="paxNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="pax" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="PositionConfigEntity" representedClassName="PositionConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="broadcastSmartMinimumDistance" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="broadcastSmartMinimumIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="deviceGpsEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="fixedPosition" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="gpsAttemptTime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="gpsEnGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="gpsMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="gpsUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="positionBroadcastSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rxGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="smartPositionEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="txGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="positionConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positionConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="PositionEntity" representedClassName="PositionEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="latest" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="precisionBits" optional="YES" attributeType="Integer 32" defaultValueString="32" usesScalarValueType="YES"/>
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="satsInView" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="seqNo" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="nodePosition" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positions" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="PowerConfigEntity" representedClassName="PowerConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="adcMultiplierOverride" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
<attribute name="deviceBatteryInaAddress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isPowerSaving" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="lsSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="minWakeSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="onBatteryShutdownAfterSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="waitBluetoothSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="powerConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="powerConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="RangeTestConfigEntity" representedClassName="RangeTestConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="save" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="sender" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
<relationship name="rangeTestConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rangeTestConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="RouteEntity" representedClassName="RouteEntity" syncable="YES" codeGenerationType="class">
<attribute name="color" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="distance" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="elevationGain" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="endDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="notes" optional="YES" attributeType="String"/>
<relationship name="locations" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="LocationEntity" inverseName="routeLocation" inverseEntity="LocationEntity"/>
</entity>
<entity name="RTTTLConfigEntity" representedClassName="RTTTLConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="ringtone" optional="YES" attributeType="String" maxValueString="228" defaultValueString=""/>
<relationship name="rtttlConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rtttlConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="SecurityConfigEntity" representedClassName="SecurityConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="adminChannelEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="adminKey" optional="YES" attributeType="Binary"/>
<attribute name="adminKey2" optional="YES" attributeType="Binary"/>
<attribute name="adminKey3" optional="YES" attributeType="Binary"/>
<attribute name="bluetoothLoggingEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="debugLogApiEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="isManaged" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="privateKey" optional="YES" attributeType="Binary"/>
<attribute name="publicKey" optional="YES" attributeType="Binary"/>
<attribute name="serialEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<relationship name="securityConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="securityConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="SerialConfigEntity" representedClassName="SerialConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="baudRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="echo" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="overrideConsoleSerialPort" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="rxd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="timeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="txd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="serialConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="serialConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="StoreForwardConfigEntity" representedClassName="StoreForwardConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="heartbeat" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="historyReturnMax" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="historyReturnWindow" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isRouter" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastHeartbeat" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="lastRequest" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="records" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="storeForwardConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="storeForwardConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TelemetryConfigEntity" representedClassName="TelemetryConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="deviceUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="environmentDisplayFahrenheit" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="environmentMeasurementEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="environmentScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="environmentUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="powerMeasurementEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="powerScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="powerUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="telemetryConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetryConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TelemetryEntity" representedClassName="TelemetryEntity" syncable="YES" codeGenerationType="class">
<attribute name="airUtilTx" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="barometricPressure" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="batteryLevel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="channelUtilization" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="current" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="gasResistance" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="iaq" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="metricsType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numOnlineNodes" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numPacketsRx" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numPacketsRxBad" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numPacketsTx" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numRxDupe" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numTotalNodes" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numTxRelay" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numTxRelayCanceled" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="relativeHumidity" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="temperature" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="uptimeSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="voltage" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="weight" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="windDirection" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="windGust" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="windLull" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="windSpeed" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="nodeTelemetry" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetries" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TraceRouteEntity" representedClassName="TraceRouteEntity" syncable="YES" codeGenerationType="class">
<attribute name="hasPositions" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="hopsBack" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="hopsTowards" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="response" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="routeBackText" optional="YES" attributeType="String"/>
<attribute name="routeText" optional="YES" attributeType="String"/>
<attribute name="sent" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="hops" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="TraceRouteHopEntity" inverseName="traceRoute" inverseEntity="TraceRouteHopEntity"/>
<relationship name="node" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="traceRoutes" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TraceRouteHopEntity" representedClassName="TraceRouteHopEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="back" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="time" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="traceRoute" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TraceRouteEntity" inverseName="hops" inverseEntity="TraceRouteEntity"/>
</entity>
<entity name="UserEntity" representedClassName="UserEntity" syncable="YES" codeGenerationType="class">
<attribute name="hwDisplayName" optional="YES" attributeType="String"/>
<attribute name="hwModel" attributeType="String"/>
<attribute name="hwModelId" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isLicensed" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="keyMatch" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="lastMessage" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="longName" attributeType="String"/>
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="newPublicKey" optional="YES" attributeType="Binary"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numString" optional="YES" attributeType="String"/>
<attribute name="pkiEncrypted" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="publicKey" optional="YES" attributeType="Binary"/>
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="shortName" attributeType="String"/>
<attribute name="userId" attributeType="String"/>
<relationship name="receivedMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="toUser" inverseEntity="MessageEntity"/>
<relationship name="sentMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="fromUser" inverseEntity="MessageEntity"/>
<relationship name="userNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="user" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="WaypointEntity" representedClassName="WaypointEntity" syncable="YES" codeGenerationType="class">
<attribute name="created" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="expire" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="icon" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="locked" attributeType="Integer 64" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="longDescription" optional="YES" attributeType="String" maxValueString="100"/>
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" attributeType="String" minValueString="1" maxValueString="30"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
</model>

View file

@ -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)
}

View file

@ -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"
}
]

View file

@ -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)

View file

@ -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<String>, isValid: Binding<Bool>) {

View file

@ -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)
}
}
// }
}
}
}

View file

@ -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))
//// }
//// }

View file

@ -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
// }
// }
//}

View file

@ -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)..<UInt32.max)
newWaypoint.latitudeI = Int32(Double(coordinate.coordinate?.latitude ?? 0) * 1e7)
newWaypoint.longitudeI = Int32(Double(coordinate.coordinate?.longitude ?? 0) * 1e7)
}
newWaypoint.name = name.count > 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)..<UInt32.max)
// newWaypoint.latitudeI = Int32(Double(coordinate.coordinate?.latitude ?? 0) * 1e7)
// newWaypoint.longitudeI = Int32(Double(coordinate.coordinate?.longitude ?? 0) * 1e7)
// }
// newWaypoint.name = name.count > 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
// }
// }
// }
//}

View file

@ -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 {

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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"),
]
}

View file

@ -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"),
]
}

View file

@ -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<D: SwiftProtobuf.Decoder>(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
}

View file

@ -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<D: SwiftProtobuf.Decoder>(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<D: SwiftProtobuf.Decoder>(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<V: SwiftProtobuf.Visitor>(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<D: SwiftProtobuf.Decoder>(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)

View file

@ -68,16 +68,6 @@
<key>DefaultValue</key>
<false/>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Use Legacy Mesh Map</string>
<key>Key</key>
<string>mapUseLegacy</string>
<key>DefaultValue</key>
<false/>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>

View file

@ -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<Date>
}

View file

@ -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<Date>
@ -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<Date>

@ -1 +1 @@
Subproject commit 5709c0a05eaefccbc9cb8ed3917adbf5fd134197
Subproject commit c9ae7fd478bffe5f954b30de6cb140821fe9ff52