Merge pull request #1262 from meshtastic/2.6.6

2.6.6
This commit is contained in:
Garth Vander Houwen 2025-06-15 20:44:33 -07:00 committed by GitHub
commit ab2e70d2dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 1831 additions and 514 deletions

View file

@ -1512,6 +1512,9 @@
}
}
}
},
"12 Hour Clock" : {
},
"25" : {
"localizations" : {
@ -2809,34 +2812,6 @@
}
}
},
"Allow incoming device control over the insecure legacy admin channel." : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Erlaubt die eingehende Gerätesteuerung über den unsicheren Legacy-Admin-Kanal."
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Consentire il controllo del dispositivo in entrata attraverso il canale di amministrazione legacy non sicuro."
}
},
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Дозволите контролу долазног уређаја над небезбедним старим администраторским каналом."
}
},
"zh-Hant-TW" : {
"stringUnit" : {
"state" : "translated",
"value" : "允許經由不安全的傳統管理通道接收裝置控制指令。"
}
}
}
},
"Allow Position Requests" : {
"localizations" : {
"it" : {
@ -11316,6 +11291,9 @@
}
}
}
},
"Expiration" : {
},
"Expire" : {
"localizations" : {
@ -13243,6 +13221,9 @@
}
}
}
},
"GitHub Repository" : {
},
"Good" : {
"localizations" : {
@ -13816,34 +13797,6 @@
}
}
},
"Help with App Development" : {
"localizations" : {
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Aiuto per lo sviluppo di app"
}
},
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Помози при развоју апликације"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "帮助开发应用程序"
}
},
"zh-Hant-TW" : {
"stringUnit" : {
"state" : "translated",
"value" : "幫助App開發"
}
}
}
},
"Hide alerts" : {
"localizations" : {
"it" : {
@ -15650,6 +15603,12 @@
}
}
}
},
"Latitude in degrees (e.g., 37.7749)" : {
},
"Latitude must be between -90 and 90 degrees" : {
},
"LED Heartbeat" : {
"localizations" : {
@ -15765,34 +15724,6 @@
}
}
},
"Legacy Administration" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Alte Administrationsart"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Amministrazione del patrimonio"
}
},
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Стари начин администрације"
}
},
"zh-Hant-TW" : {
"stringUnit" : {
"state" : "translated",
"value" : "舊版遠端管理"
}
}
}
},
"Level" : {
"localizations" : {
"de" : {
@ -15957,40 +15888,6 @@
}
}
},
"Location" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Standort"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Posizione"
}
},
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Локација:"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "位置"
}
},
"zh-Hant-TW" : {
"stringUnit" : {
"state" : "translated",
"value" : "位置"
}
}
}
},
"Location:" : {
"localizations" : {
"de" : {
@ -16396,6 +16293,12 @@
}
}
}
},
"Longitude in degrees (e.g., -122.4194)" : {
},
"Longitude must be between -180 and 180 degrees" : {
},
"LoRa" : {
"localizations" : {
@ -23974,6 +23877,9 @@
}
}
}
},
"Regenerate Private Key" : {
},
"Region" : {
"localizations" : {
@ -24353,17 +24259,6 @@
}
}
},
"Replying to a message" : {
"extractionState" : "stale",
"localizations" : {
"zh-Hant-TW" : {
"stringUnit" : {
"state" : "translated",
"value" : "正在回覆訊息"
}
}
}
},
"Request Legacy Admin: %@" : {
"localizations" : {
"it" : {
@ -27776,6 +27671,9 @@
}
}
}
},
"Set to current location" : {
},
"Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. O hop broadcast messages will not get ACKs." : {
"localizations" : {
@ -27798,6 +27696,9 @@
}
}
}
},
"Sets the screen clock format to 12-hour." : {
},
"Settings" : {
"localizations" : {
@ -28970,6 +28871,9 @@
}
}
}
},
"Sponsor App Development" : {
},
"Spread Factor" : {
"localizations" : {
@ -33330,6 +33234,9 @@
}
}
}
},
"Use my Location" : {
},
"Use Preset" : {
"localizations" : {
@ -34157,6 +34064,9 @@
}
}
}
},
"Waypoint Failed to Send" : {
},
"Waypoint Options" : {
"localizations" : {

View file

@ -371,6 +371,7 @@
DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFormatters.swift; sourceTree = "<group>"; };
DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 42.xcdatamodel"; sourceTree = "<group>"; };
DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityConfig.swift; sourceTree = "<group>"; };
DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 53.xcdatamodel"; sourceTree = "<group>"; };
DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = "<group>"; };
DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = "<group>"; };
DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = "<group>"; };
@ -1802,12 +1803,12 @@
INFOPLIST_FILE = Meshtastic/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Meshtastic;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.3;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.6.5;
MARKETING_VERSION = 2.6.6;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1835,12 +1836,12 @@
INFOPLIST_FILE = Meshtastic/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Meshtastic;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.3;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.6.5;
MARKETING_VERSION = 2.6.6;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1865,13 +1866,13 @@
INFOPLIST_FILE = Widgets/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Widgets;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.3;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.5;
MARKETING_VERSION = 2.6.6;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1897,13 +1898,13 @@
INFOPLIST_FILE = Widgets/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Widgets;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 17.3;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.5;
MARKETING_VERSION = 2.6.6;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -2002,6 +2003,7 @@
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */,
DD0836AB2DE7C7CB00A3A973 /* MeshtasticDataModelV 52.xcdatamodel */,
DD63CB4E2DD4FBEA00AFCAE2 /* MeshtasticDataModelV 51.xcdatamodel */,
233E99B32D84969500CC3A77 /* MeshtasticDataModelV 50.xcdatamodel */,
@ -2055,7 +2057,7 @@
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
);
currentVersion = DD0836AB2DE7C7CB00A3A973 /* MeshtasticDataModelV 52.xcdatamodel */;
currentVersion = DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */;
name = Meshtastic.xcdatamodeld;
path = Meshtastic/Meshtastic.xcdatamodeld;
sourceTree = "<group>";

View file

@ -24,11 +24,10 @@ struct RestartNodeIntent: AppIntent {
if let connectedPeripheralNum = BLEManager.shared.connectedPeripheral?.num,
let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext),
let fromUser = connectedNode.user,
let toUser = connectedNode.user,
let adminIndex = connectedNode.myInfo?.adminIndex {
let toUser = connectedNode.user {
// Attempt to send shutdown, throw an error if it fails
if !BLEManager.shared.sendReboot(fromUser: fromUser, toUser: toUser, adminIndex: adminIndex) {
if !BLEManager.shared.sendReboot(fromUser: fromUser, toUser: toUser) {
throw AppIntentErrors.AppIntentError.message("Failed to restart")
}
} else {

View file

@ -11,6 +11,8 @@ import AppIntents
import MeshtasticProtobufs
struct SendWaypointIntent: AppIntent {
var defaultDate = Date.now.addingTimeInterval(60 * 480)
static var title = LocalizedStringResource("Send a Waypoint")
@ -23,13 +25,24 @@ struct SendWaypointIntent: AppIntent {
@Parameter(title: "Emoji", default: "📍")
var emojiParameter: String?
@Parameter(title: "Location")
var locationParameter: CLPlacemark
// Replace CLPlacemark with latitude and longitude parameters
@Parameter(title: "Latitude", description: "Latitude in degrees (e.g., 37.7749)")
var latitudeParameter: Double
@Parameter(title: "Longitude", description: "Longitude in degrees (e.g., -122.4194)")
var longitudeParameter: Double
@Parameter(title: "Locked", default: false)
var isLocked: Bool
@Parameter(title: "Expiration")
var expiration: Date?
func perform() async throws -> some IntentResult {
if !BLEManager.shared.isConnected {
throw AppIntentErrors.AppIntentError.notConnected
}
// Provide default values if parameters are nil
let name = nameParameter ?? "Dropped Pin"
let description = descriptionParameter ?? ""
@ -50,24 +63,39 @@ struct SendWaypointIntent: AppIntent {
throw $emojiParameter.needsValueError("Must be a single emoji")
}
// Validate latitude and longitude
guard abs(latitudeParameter) <= 90 else {
throw $latitudeParameter.needsValueError("Latitude must be between -90 and 90 degrees")
}
guard abs(longitudeParameter) <= 180 else {
throw $longitudeParameter.needsValueError("Longitude must be between -180 and 180 degrees")
}
var newWaypoint = Waypoint()
if let latitude = locationParameter.location?.coordinate.latitude {
newWaypoint.latitudeI = Int32(latitude * 10_000_000)
}
if let longitude = locationParameter.location?.coordinate.longitude {
newWaypoint.longitudeI = Int32(longitude * 10_000_000)
}
// Set latitude and longitude directly
newWaypoint.latitudeI = Int32(latitudeParameter * 10_000_000)
newWaypoint.longitudeI = Int32(longitudeParameter * 10_000_000)
newWaypoint.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
// Unicode scalar value for the icon emoji string
let unicodeScalers = emoji.unicodeScalars
// First element as an UInt32
let unicode = unicodeScalers[unicodeScalers.startIndex].value
newWaypoint.icon = unicode
newWaypoint.name = name
newWaypoint.description_p = description
if let expirationDate = expiration {
newWaypoint.expire = UInt32(expirationDate.timeIntervalSince1970)
}
if isLocked {
if let connectedPeripheral = BLEManager.shared.connectedPeripheral {
newWaypoint.lockedTo = UInt32(connectedPeripheral.num)
} else {
throw AppIntentErrors.AppIntentError.notConnected
}
}
if !BLEManager.shared.sendWaypoint(waypoint: newWaypoint) {
throw AppIntentErrors.AppIntentError.message("Failed to Send Waypoint")
}
@ -75,11 +103,9 @@ struct SendWaypointIntent: AppIntent {
}
private func isValidSingleEmoji(_ emoji: String) -> Bool {
// This regex pattern is for matching a single emoji
let emojiPattern = "^([\\p{So}\\p{Cn}])$"
let regex = try? NSRegularExpression(pattern: emojiPattern, options: [])
let matches = regex?.matches(in: emoji, options: [], range: NSRange(location: 0, length: emoji.utf16.count))
return matches?.count == 1
}
}

View file

@ -24,11 +24,10 @@ struct ShutDownNodeIntent: AppIntent {
if let connectedPeripheralNum = BLEManager.shared.connectedPeripheral?.num,
let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext),
let fromUser = connectedNode.user,
let toUser = connectedNode.user,
let adminIndex = connectedNode.myInfo?.adminIndex {
let toUser = connectedNode.user {
// Attempt to send shutdown, throw an error if it fails
if !BLEManager.shared.sendShutdown(fromUser: fromUser, toUser: toUser, adminIndex: adminIndex) {
if !BLEManager.shared.sendShutdown(fromUser: fromUser, toUser: toUser) {
throw AppIntentErrors.AppIntentError.message("Failed to shut down")
}
} else {

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "heltec_mesh_pocket.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,196 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="379.51 218.53 478.45 660.45"
version="1.1"
id="svg72"
sodipodi:docname="heltec_mesh_pocket.svg"
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview72"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.78391112"
inkscape:cx="4.4647919"
inkscape:cy="146.06248"
inkscape:window-width="1472"
inkscape:window-height="890"
inkscape:window-x="0"
inkscape:window-y="38"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_2" />
<defs
id="defs1">
<style
id="style1">.cls-1{fill:#d5bd0a;}.cls-2{fill:#eceded;}.cls-3{fill:#e3e1e0;}.cls-4{fill:#1d1d1b;}.cls-12,.cls-5,.cls-7{fill:none;}.cls-5,.cls-7{stroke:#1d1d1b;stroke-miterlimit:10;}.cls-5{stroke-width:0.87px;}.cls-6{fill:#fff;}.cls-7{stroke-width:3.49px;}.cls-8{fill:#2b293d;}.cls-9{fill:#2ea358;}.cls-10{fill:#efefef;}.cls-11{fill:#acd087;}.cls-12{stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.31px;}.cls-13{fill:#2590d0;}.cls-14{fill:#4e4d4e;}</style>
</defs>
<g
id="Layer_2"
data-name="Layer 2">
<path
class="cls-1"
d="M784.42,219.19H419.9a39.73,39.73,0,0,0-39.74,39.73V838.6a39.74,39.74,0,0,0,39.74,39.73H784.42a39.73,39.73,0,0,0,39.73-39.73V258.92A39.73,39.73,0,0,0,784.42,219.19Zm31,619.41a31,31,0,0,1-31,31H419.9a31,31,0,0,1-31-31V258.92a31,31,0,0,1,31-31H784.42a31,31,0,0,1,31,31Z"
id="path1" />
<rect
class="cls-2"
x="388.9"
y="227.92"
width="426.51"
height="641.67"
rx="31"
id="rect1"
style="fill:#fcfcfc;fill-opacity:1" />
<path
class="cls-1"
d="M857.3,302.32V633.56a7.53,7.53,0,0,1-1.75,4.84l-31.4,37.77V260a67.74,67.74,0,0,1,32.06,36.41A16.78,16.78,0,0,1,857.3,302.32Z"
id="path2" />
<rect
class="cls-3"
x="439.49"
y="398.62"
width="324.87"
height="163.57"
id="rect2"
style="fill:#bfbfbf;fill-opacity:1" />
<rect
class="cls-8"
x="513.04"
y="724.36"
width="166.81"
height="33.44"
rx="3.87"
id="rect48" />
<path
class="cls-9"
d="M558.92,724.36V757.8h-42a3.87,3.87,0,0,1-3.87-3.87V728.24a3.88,3.88,0,0,1,3.87-3.88Z"
id="path48" />
<rect
class="cls-10"
x="514.95"
y="740.28"
width="20.46"
height="2.8"
transform="translate(-383.39 761.4) rotate(-55.94)"
id="rect49" />
<path
class="cls-10"
d="M551.65,750.93,541.54,736l-10.12,14.93-2.31-1.56L540,733.29a1.8,1.8,0,0,1,1.54-.82h0a1.85,1.85,0,0,1,1.57.87l10.85,16Zm-10.88-16.07s0,0,0,0Zm1.57,0,0,0Z"
id="path49" />
<path
class="cls-10"
d="M566.72,731.61a31.88,31.88,0,0,1,5.15-.39c2.66,0,4.6.6,5.84,1.68a5.43,5.43,0,0,1,1.82,4.25,5.93,5.93,0,0,1-1.61,4.35,8.72,8.72,0,0,1-6.36,2.23,9.52,9.52,0,0,1-2.16-.18v8.14h-2.68Zm2.68,9.8a9.32,9.32,0,0,0,2.23.21c3.24,0,5.21-1.54,5.21-4.34s-1.94-4-4.9-4a11.68,11.68,0,0,0-2.54.21Z"
id="path50" />
<path
class="cls-10"
d="M595.8,744.27c0,5.4-3.83,7.75-7.44,7.75-4,0-7.16-2.89-7.16-7.51,0-4.88,3.27-7.75,7.41-7.75S595.8,739.81,595.8,744.27Zm-11.85.15c0,3.2,1.88,5.61,4.54,5.61s4.53-2.38,4.53-5.67c0-2.47-1.26-5.61-4.47-5.61S584,741.65,584,744.42Z"
id="path51" />
<path
class="cls-10"
d="M600.1,737.1l2,7.41c.43,1.63.84,3.14,1.11,4.65h.1c.34-1.48.83-3,1.32-4.62L607,737.1h2.29l2.31,7.29c.56,1.75,1,3.29,1.33,4.77h.09a44.65,44.65,0,0,1,1.14-4.74l2.13-7.32H619l-4.82,14.59h-2.47l-2.28-7a48.12,48.12,0,0,1-1.33-4.79h-.06a40.69,40.69,0,0,1-1.36,4.82l-2.41,6.94h-2.47l-4.5-14.59Z"
id="path52" />
<path
class="cls-10"
d="M623,744.87c.06,3.59,2.41,5.07,5.12,5.07a10.08,10.08,0,0,0,4.14-.75l.46,1.9a12.41,12.41,0,0,1-5,.9c-4.6,0-7.35-3-7.35-7.36s2.66-7.87,7-7.87c4.88,0,6.18,4.19,6.18,6.88a9.56,9.56,0,0,1-.1,1.23Zm8-1.89c0-1.69-.71-4.32-3.76-4.32-2.75,0-4,2.48-4.17,4.32Z"
id="path53" />
<path
class="cls-10"
d="M636.9,741.65c0-1.72,0-3.2-.12-4.55h2.38l.09,2.86h.12a4.48,4.48,0,0,1,4.14-3.2,3,3,0,0,1,.77.09v2.51a4.19,4.19,0,0,0-.93-.09,3.77,3.77,0,0,0-3.64,3.4,7.56,7.56,0,0,0-.12,1.24v7.78H636.9Z"
id="path54" />
<path
class="cls-10"
d="M648.2,744.87c.07,3.59,2.41,5.07,5.13,5.07a10.11,10.11,0,0,0,4.14-.75l.46,1.9a12.44,12.44,0,0,1-5,.9c-4.6,0-7.35-3-7.35-7.36s2.66-7.87,7-7.87c4.88,0,6.17,4.19,6.17,6.88a10,10,0,0,1-.09,1.23Zm8-1.89c0-1.69-.71-4.32-3.77-4.32-2.75,0-3.95,2.48-4.17,4.32Z"
id="path55" />
<path
class="cls-10"
d="M675.07,730.28v17.64c0,1.3,0,2.77.12,3.77h-2.44l-.12-2.53h-.07a5.55,5.55,0,0,1-5.09,2.86c-3.61,0-6.39-3-6.39-7.42,0-4.85,3.06-7.84,6.7-7.84a5.05,5.05,0,0,1,4.51,2.23h.06v-8.71ZM672.35,743a4.21,4.21,0,0,0-.13-1.11,4,4,0,0,0-3.91-3.08c-2.81,0-4.48,2.42-4.48,5.64,0,3,1.48,5.4,4.41,5.4a4.06,4.06,0,0,0,4-3.17,4.15,4.15,0,0,0,.13-1.14Z"
id="path56" />
<rect
class="cls-11"
x="561.8"
y="768.06"
width="65.15"
height="6.66"
rx="3.33"
id="rect56" />
<path
class="cls-12"
d="M784.42,219.19H419.9a39.73,39.73,0,0,0-39.74,39.73V838.6a39.74,39.74,0,0,0,39.74,39.73H784.42a39.73,39.73,0,0,0,39.73-39.73V258.92A39.73,39.73,0,0,0,784.42,219.19Zm31,619.41a31,31,0,0,1-31,31H419.9a31,31,0,0,1-31-31V258.92a31,31,0,0,1,31-31H784.42a31,31,0,0,1,31,31Z"
id="path57" />
<path
class="cls-12"
d="M824.15,260a67.74,67.74,0,0,1,32.06,36.41,16.78,16.78,0,0,1,1.09,5.87V633.56a7.53,7.53,0,0,1-1.75,4.84l-31.4,37.77"
id="path58" />
<path
class="cls-13"
d="M714.73,253.2s0,0,0,0l-.24,1-7.2,29a4.47,4.47,0,0,0,.7,3.69l.06.07a15.25,15.25,0,0,1-1.59-.42,9.75,9.75,0,0,1-3.94-2.24,6.35,6.35,0,0,1-1.63-6.06l.67-2.7L705,261.64h-4.77l-1.67,6.73c-.52,2.1-.6,3,.25,5a1.18,1.18,0,0,1,0,.25l-2.48-1.32a5.56,5.56,0,0,1-2.95-6.41l4.11-16.56a.09.09,0,0,0,0,0,4.73,4.73,0,0,0-.86-4.11l1.69.75.76.34a5.79,5.79,0,0,1,3.27,6.67l-1,4h4.76l2.07-8.35a4.52,4.52,0,0,0,0-2,6.32,6.32,0,0,0-1.17-2.68,13,13,0,0,1,2.1.71C711.81,245.69,715.73,248.21,714.73,253.2Z"
id="path59" />
<path
class="cls-14"
d="M724.83,258.46l-.59,2.36h5l-1.12,4.54h-5l-.76,3.06h5L726.25,273h-9.88l1.13-4.54.76-3.06,1.12-4.54.59-2.36.71-2.86a2.23,2.23,0,0,1,1.27-1.5h0a2.22,2.22,0,0,1,.87-.18H731l-1.13,4.54Z"
id="path60" />
<path
class="cls-14"
d="M765.52,258.46l-.59,2.36h5l-1.13,4.54h-5l-.76,3.06h5L766.94,273h-9.88l1.13-4.54.76-3.06,1.13-4.54.58-2.36.71-2.86a2.22,2.22,0,0,1,1.28-1.5h0a2.15,2.15,0,0,1,.87-.18h8.14l-1.13,4.54Z"
id="path61" />
<polygon
class="cls-14"
points="758.02 253.92 757.07 257.81 753.56 257.81 753.39 258.46 752.81 260.82 749.8 272.96 744.93 272.96 746.07 268.42 746.82 265.36 747.95 260.82 748.53 258.46 748.7 257.81 745.1 257.81 745.91 254.53 746.06 253.92 758.02 253.92"
id="polygon61" />
<polygon
class="cls-14"
points="741.13 268.42 740 272.96 730.13 272.96 731.26 268.42 732.01 265.36 733.14 260.82 733.73 258.46 734.7 254.53 734.85 253.92 739.71 253.92 738.59 258.46 738 260.82 736.12 268.42 741.13 268.42"
id="polygon62" />
<path
class="cls-14"
d="M788.73,255l-1.5,6.26a3.48,3.48,0,0,0-1.3-2.36c-1.86-1.4-4.88-.52-6.78,2s-1.92,5.66-.07,7.05,4.88.52,6.77-2c.15-.2.28-.4.41-.61l-1.73,7.24a7.65,7.65,0,0,1-6.77.91c-4.31-1.62-6.17-7.27-4.16-12.64C775.55,255.69,781.94,250.72,788.73,255Z"
id="path62" />
<path
class="cls-14"
d="M718.84,282.11h-2.13l-.55,1h-1.92l3.81-6.16h2.05l.76,6.16h-2Zm-.06-1.33-.12-2.22-1.21,2.22Z"
id="path63" />
<path
class="cls-14"
d="M726.65,277h1.88l-.91,3.67a3.57,3.57,0,0,1-.43,1,3.2,3.2,0,0,1-1.61,1.36,4.44,4.44,0,0,1-1.37.2,8.21,8.21,0,0,1-1-.06,2,2,0,0,1-.82-.25,1.53,1.53,0,0,1-.51-.53,1.47,1.47,0,0,1-.22-.71,3.41,3.41,0,0,1,.08-1l.91-3.67h1.87l-.93,3.75a1,1,0,0,0,.08.79.81.81,0,0,0,.7.29,1.32,1.32,0,0,0,.83-.28,1.4,1.4,0,0,0,.47-.8Z"
id="path64" />
<path
class="cls-14"
d="M729.34,277h5.72l-.38,1.52h-1.92l-1.15,4.64h-1.88l1.15-4.64H729Z"
id="path65" />
<path
class="cls-14"
d="M734.62,280.05A4.25,4.25,0,0,1,736,277.7a3.87,3.87,0,0,1,2.52-.84,2.49,2.49,0,0,1,2.13.83,2.69,2.69,0,0,1,.25,2.31,4.83,4.83,0,0,1-.8,1.77,3.77,3.77,0,0,1-1.3,1.08,3.93,3.93,0,0,1-1.79.38,3.32,3.32,0,0,1-1.62-.33,1.81,1.81,0,0,1-.83-1A3.25,3.25,0,0,1,734.62,280.05Zm1.87,0a1.91,1.91,0,0,0,0,1.34.89.89,0,0,0,.83.41,1.46,1.46,0,0,0,1-.4,3,3,0,0,0,.69-1.43,1.78,1.78,0,0,0,0-1.28.94.94,0,0,0-.85-.4,1.48,1.48,0,0,0-1,.41A2.67,2.67,0,0,0,736.49,280.06Z"
id="path66" />
<path
class="cls-14"
d="M742.67,277h2.47l0,3.75L747,277h2.47L748,283.13h-1.54l1.17-4.7-2.35,4.7h-1.39l0-4.7-1.17,4.7h-1.54Z"
id="path67" />
<path
class="cls-14"
d="M753.17,282.11H751l-.55,1h-1.92l3.82-6.16h2.05l.75,6.16h-2Zm-.06-1.33-.12-2.22-1.21,2.22Z"
id="path68" />
<path
class="cls-14"
d="M756.31,277H762l-.38,1.52h-1.92l-1.15,4.64h-1.88l1.16-4.64h-1.92Z"
id="path69" />
<path
class="cls-14"
d="M762.94,277h1.89l-1.53,6.16h-1.89Z"
id="path70" />
<path
class="cls-14"
d="M765.17,280.05a4.21,4.21,0,0,1,1.42-2.35,3.86,3.86,0,0,1,2.51-.84,2.5,2.5,0,0,1,2.14.83,2.69,2.69,0,0,1,.25,2.31,4.83,4.83,0,0,1-.8,1.77,3.8,3.8,0,0,1-1.31,1.08,3.89,3.89,0,0,1-1.78.38,3.32,3.32,0,0,1-1.62-.33,1.77,1.77,0,0,1-.83-1A3.23,3.23,0,0,1,765.17,280.05Zm1.88,0a1.91,1.91,0,0,0,0,1.34.89.89,0,0,0,.83.41,1.47,1.47,0,0,0,1-.4,3,3,0,0,0,.68-1.43,1.68,1.68,0,0,0,0-1.28.91.91,0,0,0-.84-.4,1.47,1.47,0,0,0-1,.41A2.67,2.67,0,0,0,767.05,280.06Z"
id="path71" />
<path
class="cls-14"
d="M773.26,277H775l1.44,3.41.85-3.41h1.77l-1.53,6.16h-1.77l-1.44-3.39-.84,3.39h-1.77Z"
id="path72" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "seeed_solar.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 98 KiB

View file

@ -106,14 +106,44 @@ extension UserEntity {
}
}
}
public func createUser(num: Int64, context: NSManagedObjectContext) -> UserEntity {
let newUser = UserEntity(context: context)
newUser.num = Int64(num)
let userId = String(format: "%2X", num)
newUser.userId = "!\(userId)"
let last4 = String(userId.suffix(4))
newUser.longName = "Meshtastic \(last4)"
newUser.shortName = last4
newUser.hwModel = "UNSET"
public func createUser(num: Int64, context: NSManagedObjectContext) throws -> UserEntity {
// Validate Input
guard num >= 0 else {
throw CoreDataError.invalidInput(message: "User number cannot be negative.")
}
var newUser: UserEntity! // Use an implicitly unwrapped optional, but ensure it's assigned
context.performAndWait {
newUser = UserEntity(context: context)
newUser.num = num
let userId = String(format: "%016llX", num)
newUser.userId = "!\(userId)"
let last4 = String(userId.suffix(4))
newUser.longName = "Meshtastic \(last4)"
newUser.shortName = last4
newUser.hwModel = "UNSET"
}
return newUser
}
enum CoreDataError: Error, LocalizedError {
case invalidInput(message: String)
case saveFailed(message: String)
case entityCreationFailed(message: String) // In case UserEntity(context:) fails for some reason
var errorDescription: String? {
switch self {
case .invalidInput(let message):
return "Core Data Input Error: \(message)"
case .saveFailed(let message):
return "Core Data Save Error: \(message)"
case .entityCreationFailed(let message):
return "Core Data Entity Creation Error: \(message)"
}
}
}

View file

@ -14,9 +14,6 @@ extension WaypointEntity {
static func allWaypointssFetchRequest() -> NSFetchRequest<WaypointEntity> {
let request: NSFetchRequest<WaypointEntity> = WaypointEntity.fetchRequest()
request.fetchLimit = 50
// request.fetchBatchSize = 1
// request.returnsObjectsAsFaults = false
// request.includesSubentities = true
request.returnsDistinctResults = true
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: false)]
request.predicate = NSPredicate(format: "expire == nil || expire >= %@", Date() as NSDate)
@ -24,7 +21,6 @@ extension WaypointEntity {
}
var latitude: Double? {
let d = Double(latitudeI)
if d == 0 {
return 0
@ -33,7 +29,6 @@ extension WaypointEntity {
}
var longitude: Double? {
let d = Double(longitudeI)
if d == 0 {
return 0
@ -46,7 +41,7 @@ extension WaypointEntity {
let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!)
return coord
} else {
return nil
return nil
}
}
@ -60,16 +55,29 @@ extension WaypointEntity {
}
extension WaypointEntity: MKAnnotation {
public var coordinate: CLLocationCoordinate2D { waypointCoordinate ?? LocationsHandler.DefaultLocation }
public var title: String? { name ?? "Dropped Pin" }
@MainActor
public var coordinate: CLLocationCoordinate2D {
get {
waypointCoordinate ?? LocationsHandler.DefaultLocation
}
set {
latitudeI = Int32(newValue.latitude * 1e7)
longitudeI = Int32(newValue.longitude * 1e7)
}
}
public var title: String? {
name ?? "Dropped Pin"
}
public var subtitle: String? {
(longDescription ?? "") +
String(expire != nil ? "\n⌛ Expires \(String(describing: expire?.formatted()))" : "") +
String(locked > 0 ? "\n🔒 Locked" : "") }
String(locked > 0 ? "\n🔒 Locked" : "")
}
}
struct WaypointCoordinate: Identifiable {
let id: UUID
let coordinate: CLLocationCoordinate2D?
let waypointId: Int64

File diff suppressed because it is too large Load diff

View file

@ -185,9 +185,6 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo
mutableChannels.add(newChannel)
}
fetchedMyInfo[0].channels = mutableChannels.copy() as? NSOrderedSet
if newChannel.name?.lowercased() == "admin" {
fetchedMyInfo[0].adminIndex = newChannel.index
}
context.refresh(newChannel, mergeChanges: true)
do {
try context.save()
@ -330,8 +327,14 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
}}
newNode.user = newUser
} else if nodeInfo.num > Constants.minimumNodeNum {
let newUser = createUser(num: Int64(nodeInfo.num), context: context)
newNode.user = newUser
do {
let newUser = try createUser(num: Int64(nodeInfo.num), context: context)
newNode.user = newUser
} catch CoreDataError.invalidInput(let message) {
Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(nodeInfo.num, privacy: .public) Error: \(message, privacy: .public)")
} catch {
Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(nodeInfo.num, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
}
}
if (nodeInfo.position.longitudeI != 0 && nodeInfo.position.latitudeI != 0) && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) {
@ -420,9 +423,14 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
}
} else {
if fetchedNode[0].user == nil && nodeInfo.num > Constants.minimumNodeNum {
let newUser = createUser(num: Int64(nodeInfo.num), context: context)
fetchedNode[0].user = newUser
do {
let newUser = try createUser(num: Int64(nodeInfo.num), context: context)
fetchedNode[0].user = newUser
} catch CoreDataError.invalidInput(let message) {
Logger.data.error("Error Creating a new Core Data UserEntity on an existing node (Invalid Input) from node number: \(nodeInfo.num, privacy: .public) Error: \(message, privacy: .public)")
} catch {
Logger.data.error("Error Creating a new Core Data UserEntity on an existing node from node number: \(nodeInfo.num, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
}
}
}
@ -711,7 +719,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
if let telemetryMessage = try? Telemetry(serializedBytes: packet.decoded.payload) {
let logString = String.localizedStringWithFormat("Telemetry received for: %@".localized, String(packet.from))
Logger.mesh.info("📈 \(logString, privacy: .public)")
if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) && telemetryMessage.variant != Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) {
if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) && telemetryMessage.variant != Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) {
/// Other unhandled telemetry packets
return
}
@ -932,7 +940,14 @@ func textMessageAppPacket(
// For S&F broadcast messages, treat as a channel message (not a DM)
newMessage.toUser = nil
} else {
newMessage.toUser = createUser(num: Int64(truncatingIfNeeded: packet.to), context: context)
do {
let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.to), context: context)
newMessage.toUser = newUser
} catch CoreDataError.invalidInput(let message) {
Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.to, privacy: .public) Error: \(message, privacy: .public)")
} catch {
Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.to, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
}
}
}
if fetchedUsers.first(where: { $0.num == packet.from }) != nil {
@ -962,7 +977,14 @@ func textMessageAppPacket(
}
} else {
/// Make a new from user if they are unknown
newMessage.fromUser = createUser(num: Int64(truncatingIfNeeded: packet.from), context: context)
do {
let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context)
newMessage.fromUser = newUser
} catch CoreDataError.invalidInput(let message) {
Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)")
} catch {
Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
}
}
if packet.rxTime > 0 {
newMessage.fromUser?.userNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime)))
@ -979,79 +1001,79 @@ func textMessageAppPacket(
try context.save()
Logger.data.info("💾 Saved a new message for \(newMessage.messageId, privacy: .public)")
messageSaved = true
if messageSaved {
if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications {
return
}
if newMessage.fromUser != nil && newMessage.toUser != nil {
// Set Unread Message Indicators
if packet.to == connectedNode {
appState.unreadDirectMessages = newMessage.toUser?.unreadMessages ?? 0
}
if !(newMessage.fromUser?.mute ?? false) {
// Create an iOS Notification for the received DM message
let manager = LocalNotificationManager()
manager.notifications = [
Notification(
id: ("notification.id.\(newMessage.messageId)"),
title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)",
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
content: messageText!,
target: "messages",
path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)",
messageId: newMessage.messageId,
channel: newMessage.channel,
userNum: Int64(packet.from),
critical: critical
)
]
manager.schedule()
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)")
}
} else if newMessage.fromUser != nil && newMessage.toUser == nil {
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode))
do {
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest)
if !fetchedMyInfo.isEmpty {
appState.unreadChannelMessages = fetchedMyInfo[0].unreadMessages
for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] {
if channel.index == newMessage.channel {
context.refresh(channel, mergeChanges: true)
}
if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications {
// Create an iOS Notification for the received channel message
let manager = LocalNotificationManager()
manager.notifications = [
Notification(
id: ("notification.id.\(newMessage.messageId)"),
title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)",
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
content: messageText!,
target: "messages",
path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)",
messageId: newMessage.messageId,
channel: newMessage.channel,
userNum: Int64(newMessage.fromUser?.userId ?? "0"),
critical: critical
)
]
manager.schedule()
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)")
}
}
}
} catch {
// Handle error
}
}
}
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Failed to save new MessageEntity \(nsError, privacy: .public)")
}
// Send notifications if the message saved properly to core data
if messageSaved {
if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications {
return
}
if newMessage.fromUser != nil && newMessage.toUser != nil {
// Set Unread Message Indicators
if packet.to == connectedNode {
appState.unreadDirectMessages = newMessage.toUser?.unreadMessages ?? 0
}
if !(newMessage.fromUser?.mute ?? false) {
// Create an iOS Notification for the received DM message
let manager = LocalNotificationManager()
manager.notifications = [
Notification(
id: ("notification.id.\(newMessage.messageId)"),
title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)",
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
content: messageText!,
target: "messages",
path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)",
messageId: newMessage.messageId,
channel: newMessage.channel,
userNum: Int64(packet.from),
critical: critical
)
]
manager.schedule()
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)")
}
} else if newMessage.fromUser != nil && newMessage.toUser == nil {
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode))
do {
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest)
if !fetchedMyInfo.isEmpty {
appState.unreadChannelMessages = fetchedMyInfo[0].unreadMessages
for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] {
if channel.index == newMessage.channel {
context.refresh(channel, mergeChanges: true)
}
if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications {
// Create an iOS Notification for the received channel message
let manager = LocalNotificationManager()
manager.notifications = [
Notification(
id: ("notification.id.\(newMessage.messageId)"),
title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)",
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
content: messageText!,
target: "messages",
path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)",
messageId: newMessage.messageId,
channel: newMessage.channel,
userNum: Int64(newMessage.fromUser?.userId ?? "0"),
critical: critical
)
]
manager.schedule()
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)")
}
}
}
} catch {
// Handle error
}
}
}
} catch {
Logger.data.error("Fetch Message To and From Users Error")
}

View file

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

View file

@ -0,0 +1,506 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788.4" systemVersion="24D81" 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="excludedModules" 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="use12HClock" attributeType="Boolean" defaultValueString="NO" 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="mapReportingShouldReportLocation" optional="YES" attributeType="Boolean" 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="bleName" optional="YES" attributeType="String"/>
<attribute name="deviceId" optional="YES" attributeType="Binary"/>
<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"/>
<attribute name="registered" attributeType="Boolean" defaultValueString="NO" 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="enabledProtocols" 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="ignored" attributeType="Boolean" defaultValueString="NO" 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">
<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="irLux" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="lux" optional="YES" attributeType="Float" defaultValueString="0.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="powerCh1Current" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
<attribute name="powerCh1Voltage" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
<attribute name="powerCh2Current" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
<attribute name="powerCh2Voltage" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
<attribute name="powerCh3Current" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
<attribute name="powerCh3Voltage" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
<attribute name="radiation" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="rainfall1H" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="rainfall24H" optional="YES" attributeType="Float" defaultValueString="0.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="soilMoisture" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="soilTemperature" 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="uvLux" optional="YES" attributeType="Float" defaultValueString="0.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="whiteLux" 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="unmessagable" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<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

@ -170,8 +170,14 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
if newUserMessage.id.isEmpty {
if packet.from > Constants.minimumNodeNum {
let newUser = createUser(num: Int64(packet.from), context: context)
newNode.user = newUser
do {
let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context)
newNode.user = newUser
} catch CoreDataError.invalidInput(let message) {
Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)")
} catch {
Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
}
}
} else {
@ -225,17 +231,34 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
}
} else {
if packet.from > Constants.minimumNodeNum {
let newUser = createUser(num: Int64(packet.from), context: context)
if !packet.publicKey.isEmpty {
newNode.user?.pkiEncrypted = true
newNode.user?.publicKey = packet.publicKey
do {
let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context)
if !packet.publicKey.isEmpty {
newNode.user?.pkiEncrypted = true
newNode.user?.publicKey = packet.publicKey
}
newNode.user = newUser
} catch CoreDataError.invalidInput(let message) {
Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)")
} catch {
Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
}
newNode.user = newUser
}
}
// User is messed up and has failed to create at least once, if this fails bail out
if newNode.user == nil && packet.from > Constants.minimumNodeNum {
newNode.user = createUser(num: Int64(packet.from), context: context)
do {
let newUser = try createUser(num: Int64(packet.from), context: context)
newNode.user = newUser
} catch CoreDataError.invalidInput(let message) {
Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)")
context.rollback()
return
} catch {
Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
context.rollback()
return
}
}
let myInfoEntity = MyInfoEntity(context: context)
@ -317,9 +340,14 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit)
}
if fetchedNode[0].user == nil {
let newUser = createUser(num: Int64(truncatingIfNeeded: packet.from), context: context)
fetchedNode[0].user? = newUser
do {
let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context)
fetchedNode[0].user = newUser
} catch CoreDataError.invalidInput(let message) {
Logger.data.error("Error Creating a new Core Data UserEntity on an existing node (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)")
} catch {
Logger.data.error("Error Creating a new Core Data UserEntity on an existing node from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
}
}
do {
try context.save()
@ -553,6 +581,7 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, ses
newDisplayConfig.displayMode = Int32(config.displaymode.rawValue)
newDisplayConfig.units = Int32(config.units.rawValue)
newDisplayConfig.headingBold = config.headingBold
newDisplayConfig.use12HClock = config.use12HClock
fetchedNode[0].displayConfig = newDisplayConfig
} else {
@ -564,6 +593,7 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, ses
fetchedNode[0].displayConfig?.oledType = Int32(config.oled.rawValue)
fetchedNode[0].displayConfig?.displayMode = Int32(config.displaymode.rawValue)
fetchedNode[0].displayConfig?.units = Int32(config.units.rawValue)
fetchedNode[0].displayConfig?.use12HClock = config.use12HClock
fetchedNode[0].displayConfig?.headingBold = config.headingBold
}
if sessionPasskey != nil {

View file

@ -229,7 +229,8 @@
"images": [
"tlora-t3s3-epaper.svg"
],
"requiresDfu": true
"requiresDfu": true,
"hasInkHud": true
},
{
"hwModel": 17,
@ -604,7 +605,7 @@
"hwModelSlug": "HELTEC_WIRELESS_TRACKER",
"platformioTarget": "tracksenger",
"architecture": "esp32-s3",
"activelySupported": false,
"activelySupported": true,
"supportLevel": 3,
"displayName": "TrackSenger (small TFT)",
"requiresDfu": true,
@ -626,7 +627,7 @@
"hwModelSlug": "HELTEC_WIRELESS_TRACKER",
"platformioTarget": "tracksenger-oled",
"architecture": "esp32-s3",
"activelySupported": false,
"activelySupported": true,
"supportLevel": 3,
"displayName": "TrackSenger (big OLED)",
"partitionScheme": "8MB"
@ -872,5 +873,110 @@
"images": [
"thinknode_m2.svg"
]
},
{
"hwModel": 94,
"hwModelSlug": "HELTEC_MESH_POCKET",
"platformioTarget": "heltec-mesh-pocket-10000",
"architecture": "nrf52840",
"activelySupported": true,
"supportLevel": 1,
"displayName": "Heltec MeshPocket",
"tags": [
"Heltec"
],
"images": [
"heltec_mesh_pocket.svg"
],
"requiresDfu": true,
"hasInkHud": true
},
{
"hwModel": 95,
"hwModelSlug": "SEEED_SOLAR_NODE",
"platformioTarget": "seeed_solar_node",
"architecture": "nrf52840",
"activelySupported": true,
"supportLevel": 1,
"displayName": "Seeed SenseCAP Solar Node",
"tags": [
"Seeed"
],
"images": [
"seeed_solar.svg"
],
"requiresDfu": true
},
{
"hwModel": 99,
"hwModelSlug": "SEEED_WIO_TRACKER_L1",
"platformioTarget": "seeed_wio_tracker_L1",
"architecture": "nrf52840",
"activelySupported": true,
"supportLevel": 1,
"displayName": "Seeed Wio Tracker L1",
"tags": [
"Seeed"
],
"images": [
"wio_tracker_l1_case.svg"
],
"requiresDfu": true
},
{
"hwModel": 97,
"hwModelSlug": "CROWPANEL",
"platformioTarget": "elecrow-adv1-43-50-70-tft",
"architecture": "esp32-s3",
"activelySupported": true,
"supportLevel": 1,
"displayName": "Crowpanel Adv 4.3/5.0/7.0 TFT",
"tags": [
"Elecrow"
],
"requiresDfu": true,
"images": [
"crowpanel_5_0.svg",
"crowpanel_7_0.svg"
],
"partitionScheme": "16MB",
"hasMui": true
},
{
"hwModel": 97,
"hwModelSlug": "CROWPANEL",
"platformioTarget": "elecrow-adv-24-28-tft",
"architecture": "esp32-s3",
"activelySupported": true,
"supportLevel": 1,
"displayName": "Crowpanel Adv 2.4/2.8 TFT",
"tags": [
"Elecrow"
],
"requiresDfu": true,
"images": [
"crowpanel_2_4.svg",
"crowpanel_2_8.svg"
],
"partitionScheme": "16MB",
"hasMui": true
},
{
"hwModel": 97,
"hwModelSlug": "CROWPANEL",
"platformioTarget": "elecrow-adv-35-tft",
"architecture": "esp32-s3",
"activelySupported": true,
"supportLevel": 1,
"displayName": "Crowpanel Adv 3.5 TFT",
"tags": [
"Elecrow"
],
"requiresDfu": true,
"images": [
"crowpanel_3_5.svg"
],
"partitionScheme": "16MB",
"hasMui": true
}
]

View file

@ -133,7 +133,7 @@ struct Connect: View {
Label("Disconnect", systemImage: "antenna.radiowaves.left.and.right.slash")
}
Button {
if !bleManager.sendShutdown(fromUser: node!.user!, toUser: node!.user!, adminIndex: node!.myInfo!.adminIndex) {
if !bleManager.sendShutdown(fromUser: node!.user!, toUser: node!.user!) {
Logger.mesh.error("Shutdown Failed")
}

View file

@ -58,6 +58,13 @@ struct ChannelList: View {
VStack(alignment: .leading) {
HStack {
if channel.psk?.hexDescription.count ?? 0 < 3 {
Image(systemName: "lock.slash.fill")
.foregroundColor(.red)
} else {
Image(systemName: "lock.fill")
.foregroundColor(.green)
}
if channel.name?.isEmpty ?? false {
if channel.role == 1 {
Text(String("PrimaryChannel").camelCaseToWords())

View file

@ -180,24 +180,24 @@ struct ChannelMessageList: View {
}
.scrollDismissesKeyboard(.interactively)
.onFirstAppear {
// Find first unread message
if let firstUnreadMessageId = channel.allPrivateMessages.first(where: { !$0.read })?.messageId {
if channel.unreadMessages == 0 {
withAnimation {
scrollView.scrollTo(firstUnreadMessageId, anchor: .top)
showScrollToBottomButton = true
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
hasReachedBottom = true
}
} else {
// If no unread messages, scroll to bottom
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
hasReachedBottom = true
if let firstUnreadMessageId = channel.allPrivateMessages.first(where: { !$0.read })?.messageId {
withAnimation {
scrollView.scrollTo(firstUnreadMessageId, anchor: .top)
showScrollToBottomButton = true
}
}
}
gotFirstUnreadMessage = true
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
hasReachedBottom = true
showScrollToBottomButton = false
}
@ -205,7 +205,7 @@ struct ChannelMessageList: View {
.onChange(of: channel.allPrivateMessages) {
if hasReachedBottom {
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
}
} else {
showScrollToBottomButton = true

View file

@ -167,24 +167,24 @@ struct UserMessageList: View {
}
.scrollDismissesKeyboard(.interactively)
.onFirstAppear {
// Find first unread message
if let firstUnreadMessageId = user.messageList.first(where: { !$0.read })?.messageId {
if user.unreadMessages == 0 {
withAnimation {
scrollView.scrollTo(firstUnreadMessageId, anchor: .top)
showScrollToBottomButton = true
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
hasReachedBottom = true
}
} else {
// If no unread messages, scroll to bottom
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
hasReachedBottom = true
if let firstUnreadMessageId = user.messageList.first(where: { !$0.read })?.messageId {
withAnimation {
scrollView.scrollTo(firstUnreadMessageId, anchor: .top)
showScrollToBottomButton = true
}
}
}
gotFirstUnreadMessage = true
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
hasReachedBottom = true
showScrollToBottomButton = false
}
@ -192,7 +192,7 @@ struct UserMessageList: View {
.onChange(of: user.messageList) {
if hasReachedBottom {
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
}
} else {
showScrollToBottomButton = true

View file

@ -30,6 +30,7 @@ struct WaypointForm: View {
@State private var lockedTo: Int64 = 0
@State private var detents: Set<PresentationDetent> = [.medium, .fraction(0.85)]
@State private var selectedDetent: PresentationDetent = .medium
@State private var waypointFailedAlert: Bool = false
var body: some View {
NavigationStack {
@ -47,7 +48,19 @@ struct WaypointForm: View {
.textSelection(.enabled)
.foregroundColor(.secondary)
.font(.caption)
}
Button {
let currentLoc = LocationsHandler.currentLocation
waypoint.coordinate.longitude = currentLoc.longitude
waypoint.coordinate.latitude = currentLoc.latitude
} label: {
HStack {
Text("Use my Location")
Image(systemName: "location")
}
}
.accessibilityLabel("Set to current location")
HStack {
if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 {
DistanceText(meters: distance)
@ -72,6 +85,7 @@ struct WaypointForm: View {
name = String(name.dropLast())
totalBytes = name.utf8.count
}
waypoint.name = name.count > 0 ? name : "Dropped Pin"
}
}
HStack {
@ -167,8 +181,8 @@ struct WaypointForm: View {
if bleManager.sendWaypoint(waypoint: newWaypoint) {
dismiss()
} else {
dismiss()
Logger.mesh.warning("Send waypoint failed")
waypointFailedAlert = true
}
} else {
Logger.mesh.warning("Send waypoint failed, node not connected")
@ -233,8 +247,8 @@ struct WaypointForm: View {
}
dismiss()
} else {
dismiss()
Logger.mesh.warning("Send waypoint failed")
waypointFailedAlert = true
}
})
}
@ -256,8 +270,8 @@ struct WaypointForm: View {
Text(waypoint.name ?? "?")
.font(.largeTitle)
Spacer()
if waypoint.locked > 0 {
Image(systemName: "lock.fill" )
if waypoint.locked > 0 && waypoint.locked != UInt32(BLEManager.shared.connectedPeripheral?.num ?? 0) {
Image(systemName: "lock.fill")
.font(.largeTitle)
} else {
Button {
@ -368,6 +382,17 @@ struct WaypointForm: View {
}
}
}
.alert("Waypoint Failed to Send", isPresented: $waypointFailedAlert) {
Button("OK", role: .cancel) {
bleManager.context.delete(waypoint)
do {
try bleManager.context.save()
} catch {
bleManager.context.rollback()
}
dismiss()
}
}
.onDisappear {
if waypoint.id == 0 {
// New, unsent waypoint created by the user: delete it

View file

@ -516,7 +516,6 @@ struct NodeDetail: View {
let adminMessageId = bleManager.requestDeviceMetadata(
fromUser: connectedNode.user!,
toUser: node.user!,
adminIndex: connectedNode.myInfo!.adminIndex,
context: context
)
if adminMessageId > 0 {
@ -543,8 +542,7 @@ struct NodeDetail: View {
Button("Shutdown Node?", role: .destructive) {
if !bleManager.sendShutdown(
fromUser: connectedNode.user!,
toUser: node.user!,
adminIndex: connectedNode.myInfo!.adminIndex
toUser: node.user!
) {
Logger.mesh.warning("Shutdown Failed")
}
@ -566,8 +564,7 @@ struct NodeDetail: View {
Button("Reboot node?", role: .destructive) {
if !bleManager.sendReboot(
fromUser: connectedNode.user!,
toUser: node.user!,
adminIndex: connectedNode.myInfo!.adminIndex
toUser: node.user!
) {
Logger.mesh.warning("Reboot Failed")
}

View file

@ -38,7 +38,9 @@ struct AboutMeshtastic: View {
}
}
}
Link("Help with App Development", destination: URL(string: "https://github.com/meshtastic/Meshtastic-Apple")!)
Link("Sponsor App Development", destination: URL(string: "https://github.com/sponsors/garthvh")!)
.font(.title2)
Link("GitHub Repository", destination: URL(string: "https://github.com/meshtastic/Meshtastic-Apple")!)
.font(.title2)
Button("Review the app") {
if let scene = UIApplication.shared.connectedScenes

View file

@ -80,7 +80,7 @@ struct BluetoothConfig: View {
bc.enabled = enabled
bc.mode = BluetoothModes(rawValue: mode)?.protoEnumValue() ?? Config.BluetoothConfig.PairingMode.randomPin
bc.fixedPin = UInt32(fixedPin) ?? 123456
let adminMessageId = bleManager.saveBluetoothConfig(config: bc, fromUser: connectedNode.user!, toUser: node!.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.saveBluetoothConfig(config: bc, fromUser: connectedNode.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
@ -111,7 +111,7 @@ struct BluetoothConfig: View {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.bluetoothConfig == nil {
Logger.mesh.info("⚙️ Empty or expired bluetooth config requesting via PKI admin")
_ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
} else {
/// Legacy Administration

View file

@ -245,7 +245,7 @@ struct DeviceConfig: View {
dc.disableTripleClick = !tripleClickAsAdHocPing
dc.tzdef = tzdef
dc.ledHeartbeatDisabled = !ledHeartbeatEnabled
let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
@ -278,7 +278,7 @@ struct DeviceConfig: View {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.deviceConfig == nil {
Logger.mesh.info("⚙️ Empty or expired device config requesting via PKI admin")
_ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
} else {
if node.deviceConfig == nil {

View file

@ -27,6 +27,7 @@ struct DisplayConfig: View {
@State var oledType = 0
@State var displayMode = 0
@State var units = 0
@State var use12HourClock = false
var body: some View {
Form {
@ -74,6 +75,11 @@ struct DisplayConfig: View {
.font(.callout)
}
.pickerStyle(DefaultPickerStyle())
Toggle(isOn: $use12HourClock) {
Label("12 Hour Clock", systemImage: "clock")
Text("Sets the screen clock format to 12-hour.")
}
.tint(Color.accentColor)
}
Section(header: Text("Timing & Format")) {
VStack(alignment: .leading) {
@ -141,8 +147,9 @@ struct DisplayConfig: View {
dc.oled = OledTypes(rawValue: oledType)!.protoEnumValue()
dc.displaymode = DisplayModes(rawValue: displayMode)!.protoEnumValue()
dc.units = Units(rawValue: units)!.protoEnumValue()
dc.use12HClock = use12HourClock
let adminMessageId = bleManager.saveDisplayConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.saveDisplayConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
@ -174,7 +181,7 @@ struct DisplayConfig: View {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.displayConfig == nil {
Logger.mesh.info("⚙️ Empty or expired display config requesting via PKI admin")
_ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
} else {
/// Legacy Administration
@ -211,6 +218,9 @@ struct DisplayConfig: View {
.onChange(of: units) { oldUnits, newUnits in
if oldUnits != newUnits && newUnits != node?.displayConfig?.units ?? -1 { hasChanges = true }
}
.onChange(of: use12HourClock) { oldUse12HourClock, newUse12HourClock in
if oldUse12HourClock != newUse12HourClock && newUse12HourClock != node?.displayConfig?.use12HClock { hasChanges = true }
}
}
func setDisplayValues() {
self.gpsFormat = Int(node?.displayConfig?.gpsFormat ?? 0)
@ -222,6 +232,7 @@ struct DisplayConfig: View {
self.oledType = Int(node?.displayConfig?.oledType ?? 0)
self.displayMode = Int(node?.displayConfig?.displayMode ?? 0)
self.units = Int(node?.displayConfig?.units ?? 0)
self.hasChanges = false
self.use12HourClock = node?.displayConfig?.use12HClock ?? false
self.hasChanges = node?.displayConfig?.use12HClock ?? false
}
}

View file

@ -218,7 +218,7 @@ struct LoRaConfig: View {
if connectedNode?.num ?? -1 == node?.user?.num ?? 0 {
UserDefaults.modemPreset = modemPreset
}
let adminMessageId = bleManager.saveLoRaConfig(config: lc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.saveLoRaConfig(config: lc, fromUser: connectedNode!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
@ -250,7 +250,7 @@ struct LoRaConfig: View {
if expiration < Date() || node.loRaConfig == nil {
Logger.mesh.info("⚙️ Empty or expired lora config requesting via PKI admin")
if connectedNode.user != nil && node.user != nil {
_ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
}
} else {

View file

@ -66,7 +66,7 @@ struct AmbientLightingConfig: View {
al.blue = UInt32(components.blue * 255)
}
let adminMessageId = bleManager.saveAmbientLightingModuleConfig(config: al, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.saveAmbientLightingModuleConfig(config: al, fromUser: connectedNode!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
@ -96,7 +96,7 @@ struct AmbientLightingConfig: View {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.ambientLightingConfig == nil {
Logger.mesh.info("⚙️ Empty or expired ambient lighting module config requesting via PKI admin")
_ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
} else {
/// Legacy Administration

View file

@ -201,7 +201,7 @@ struct CannedMessagesConfig: View {
cmc.inputbrokerEventCw = InputEventChars(rawValue: inputbrokerEventCw)!.protoEnumValue()
cmc.inputbrokerEventCcw = InputEventChars(rawValue: inputbrokerEventCcw)!.protoEnumValue()
cmc.inputbrokerEventPress = InputEventChars(rawValue: inputbrokerEventPress)!.protoEnumValue()
let adminMessageId = bleManager.saveCannedMessageModuleConfig(config: cmc, fromUser: node!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.saveCannedMessageModuleConfig(config: cmc, fromUser: node!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
@ -211,7 +211,7 @@ struct CannedMessagesConfig: View {
}
}
if hasMessagesChanges {
let adminMessageId = bleManager.saveCannedMessageModuleMessages(messages: messages, fromUser: node!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.saveCannedMessageModuleMessages(messages: messages, fromUser: node!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
@ -244,7 +244,7 @@ struct CannedMessagesConfig: View {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.cannedMessageConfig == nil {
Logger.mesh.info("⚙️ Empty or expired canned messages module config requesting via PKI admin")
_ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
} else {
/// Legacy Administration

View file

@ -172,7 +172,7 @@ struct DetectionSensorConfig: View {
dsc.usePullup = self.usePullup
dsc.minimumBroadcastSecs = UInt32(self.minimumBroadcastSecs)
dsc.stateBroadcastSecs = UInt32(self.stateBroadcastSecs)
let adminMessageId = bleManager.saveDetectionSensorModuleConfig(config: dsc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.saveDetectionSensorModuleConfig(config: dsc, fromUser: connectedNode!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
@ -202,7 +202,7 @@ struct DetectionSensorConfig: View {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.detectionSensorConfig == nil {
Logger.mesh.info("⚙️ Empty or expired detection sensor module config requesting via PKI admin")
_ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
} else {
/// Legacy Administration

View file

@ -180,7 +180,7 @@ struct ExternalNotificationConfig: View {
enc.outputMs = UInt32(outputMilliseconds)
enc.usePwm = usePWM
enc.useI2SAsBuzzer = useI2SAsBuzzer
let adminMessageId = bleManager.saveExternalNotificationModuleConfig(config: enc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.saveExternalNotificationModuleConfig(config: enc, fromUser: connectedNode!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
@ -210,7 +210,7 @@ struct ExternalNotificationConfig: View {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.externalNotificationConfig == nil {
Logger.mesh.info("⚙️ Empty or expired external notificaiton module config requesting via PKI admin")
_ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
} else {
/// Legacy Administration

View file

@ -268,7 +268,7 @@ struct MQTTConfig: View {
mqtt.mapReportingEnabled = self.mapReportingEnabled
mqtt.mapReportSettings.positionPrecision = UInt32(self.mapPositionPrecision)
mqtt.mapReportSettings.publishIntervalSecs = UInt32(self.mapPublishIntervalSecs)
let adminMessageId = bleManager.saveMQTTConfig(config: mqtt, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.saveMQTTConfig(config: mqtt, fromUser: connectedNode!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
@ -360,7 +360,7 @@ struct MQTTConfig: View {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.mqttConfig == nil {
Logger.mesh.info("⚙️ Empty or expired mqtt module config requesting via PKI admin")
_ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
} else {
/// Legacy Administration

View file

@ -69,7 +69,7 @@ struct PaxCounterConfig: View {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.paxCounterConfig == nil {
Logger.mesh.info("⚙️ Empty or expired pax counter module config requesting via PKI admin")
_ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
} else {
/// Legacy Administration
@ -100,8 +100,7 @@ struct PaxCounterConfig: View {
let adminMessageId = bleManager.savePaxcounterModuleConfig(
config: config,
fromUser: fromUser,
toUser: toUser,
adminIndex: connectedNode.myInfo?.adminIndex ?? 0
toUser: toUser
)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true

View file

@ -62,7 +62,7 @@ struct RangeTestConfig: View {
rtc.enabled = enabled
rtc.save = save
rtc.sender = UInt32(sender)
let adminMessageId = bleManager.saveRangeTestModuleConfig(config: rtc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.saveRangeTestModuleConfig(config: rtc, fromUser: connectedNode!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
@ -92,7 +92,7 @@ struct RangeTestConfig: View {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.rangeTestConfig == nil {
Logger.mesh.info("⚙️ Empty or expired range test module config requesting via PKI admin")
_ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
} else {
/// Legacy Administration

View file

@ -53,7 +53,7 @@ struct RtttlConfig: View {
SaveConfigButton(node: node, hasChanges: $hasChanges) {
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
if connectedNode != nil {
let adminMessageId = bleManager.saveRtttlConfig(ringtone: ringtone.trimmingCharacters(in: .whitespacesAndNewlines), fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.saveRtttlConfig(ringtone: ringtone.trimmingCharacters(in: .whitespacesAndNewlines), fromUser: connectedNode!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
@ -83,7 +83,7 @@ struct RtttlConfig: View {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.rtttlConfig == nil {
Logger.mesh.info("⚙️ Empty or expired ringtone module config requesting via PKI admin")
_ = bleManager.requestRtttlConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestRtttlConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
} else {
/// Legacy Administration

View file

@ -116,7 +116,7 @@ struct SerialConfig: View {
sc.overrideConsoleSerialPort = overrideConsoleSerialPort
sc.mode = SerialModeTypes(rawValue: mode)!.protoEnumValue()
let adminMessageId = bleManager.saveSerialModuleConfig(config: sc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.saveSerialModuleConfig(config: sc, fromUser: connectedNode!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
@ -147,7 +147,7 @@ struct SerialConfig: View {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.serialConfig == nil {
Logger.mesh.info("⚙️ Empty or expired serial module config requesting via PKI admin")
_ = bleManager.requestSerialModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestSerialModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
} else {
/// Legacy Administration

View file

@ -118,7 +118,7 @@ struct StoreForwardConfig: View {
sfc.records = UInt32(self.records)
sfc.historyReturnMax = UInt32(self.historyReturnMax)
sfc.historyReturnWindow = UInt32(self.historyReturnWindow)
let adminMessageId = bleManager.saveStoreForwardModuleConfig(config: sfc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.saveStoreForwardModuleConfig(config: sfc, fromUser: connectedNode!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
@ -148,7 +148,7 @@ struct StoreForwardConfig: View {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.storeForwardConfig == nil {
Logger.mesh.info("⚙️ Empty or expired store & forward module config requesting via PKI admin")
_ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
} else {
/// Legacy Administration

View file

@ -115,7 +115,7 @@ struct TelemetryConfig: View {
tc.powerMeasurementEnabled = powerMeasurementEnabled
tc.powerUpdateInterval = UInt32(powerUpdateInterval)
tc.powerScreenEnabled = powerScreenEnabled
let adminMessageId = bleManager.saveTelemetryModuleConfig(config: tc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.saveTelemetryModuleConfig(config: tc, fromUser: connectedNode!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
@ -145,7 +145,7 @@ struct TelemetryConfig: View {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.telemetryConfig == nil {
Logger.mesh.info("⚙️ Empty or expired telemetry module config requesting via PKI admin")
_ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
} else {
/// Legacy Administration

View file

@ -114,7 +114,7 @@ struct NetworkConfig: View {
network.enabledProtocols = self.udpEnabled ? UInt32(Config.NetworkConfig.ProtocolFlags.udpBroadcast.rawValue) : UInt32(Config.NetworkConfig.ProtocolFlags.noBroadcast.rawValue)
// network.addressMode = Config.NetworkConfig.AddressMode.dhcp
let adminMessageId = bleManager.saveNetworkConfig(config: network, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.saveNetworkConfig(config: network, fromUser: connectedNode!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
@ -140,7 +140,7 @@ struct NetworkConfig: View {
Logger.mesh.info("empty network config")
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
if node != nil && connectedNode != nil {
_ = bleManager.requestNetworkConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
_ = bleManager.requestNetworkConfig(fromUser: connectedNode!.user!, toUser: node!.user!)
}
}
}
@ -155,7 +155,7 @@ struct NetworkConfig: View {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.networkConfig == nil {
Logger.mesh.info("⚙️ Empty or expired network config requesting via PKI admin")
_ = bleManager.requestNetworkConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestNetworkConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
} else {
/// Legacy Administration

View file

@ -345,7 +345,7 @@ struct PositionConfig: View {
if includeSpeed { pf.insert(.Speed) }
if includeHeading { pf.insert(.Heading) }
pc.positionFlags = UInt32(pf.rawValue)
let adminMessageId = bleManager.savePositionConfig(config: pc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.savePositionConfig(config: pc, fromUser: connectedNode!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Disable the button after a successful save
hasChanges = false
@ -412,7 +412,7 @@ struct PositionConfig: View {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.positionConfig == nil {
Logger.mesh.info("⚙️ Empty or expired position config requesting via PKI admin")
_ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
} else {
/// Legacy Administration

View file

@ -139,7 +139,7 @@ struct PowerConfig: View {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.powerConfig == nil {
Logger.mesh.info("⚙️ Empty or expired power config requesting via PKI admin")
_ = bleManager.requestPowerConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestPowerConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
} else {
/// Legacy Administration
@ -194,8 +194,7 @@ struct PowerConfig: View {
let adminMessageId = bleManager.savePowerConfig(
config: config,
fromUser: fromUser,
toUser: toUser,
adminIndex: connectedNode.myInfo?.adminIndex ?? 0
toUser: toUser
)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true

View file

@ -33,7 +33,6 @@ struct SecurityConfig: View {
@State var isManaged = false
@State var serialEnabled = false
@State var debugLogApiEnabled = false
@State var adminChannelEnabled = false
var body: some View {
VStack {
@ -65,6 +64,21 @@ struct SecurityConfig: View {
Text("Used to create a shared key with a remote device.")
.foregroundStyle(.secondary)
.font(idiom == .phone ? .caption : .callout)
HStack(alignment: .firstTextBaseline) {
Label("Regenerate Private Key", systemImage: "arrow.clockwise.circle")
Spacer()
Button {
if let keyBytes = generatePrivateKey(count: 32) {
privateKey = keyBytes.base64EncodedString()
}
} label: {
Image(systemName: "lock.rotation")
.font(.title)
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.small)
}
Divider()
Label("Primary Admin Key", systemImage: "key.viewfinder")
SecureInput("Primary Admin Key", text: $adminKey, isValid: $hasValidAdminKey)
@ -109,19 +123,14 @@ struct SecurityConfig: View {
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
Section(header: Text("Administration")) {
if adminKey.length > 0 || adminChannelEnabled {
if adminKey.length > 0 || UserDefaults.enableAdministration {
Section(header: Text("Administration")) {
Toggle(isOn: $isManaged) {
Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath")
Text("Device is managed by a mesh administrator, the user is unable to access any of the device settings.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
Toggle(isOn: $adminChannelEnabled) {
Label("Legacy Administration", systemImage: "lock.slash")
Text("Allow incoming device control over the insecure legacy admin channel.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
}
}
@ -143,17 +152,14 @@ struct SecurityConfig: View {
.onChange(of: debugLogApiEnabled) { _, newDebugLogApiEnabled in
if newDebugLogApiEnabled != node?.securityConfig?.debugLogApiEnabled { hasChanges = true }
}
.onChange(of: adminChannelEnabled) { _, newAdminChannelEnabled in
if newAdminChannelEnabled != node?.securityConfig?.adminChannelEnabled { hasChanges = true }
}
.onChange(of: privateKey) {
.onChange(of: privateKey) { _, key in
let tempKey = Data(base64Encoded: privateKey) ?? Data()
if tempKey.count == 32 {
hasValidPrivateKey = true
} else {
hasValidPrivateKey = false
}
hasChanges = true
if key != node?.securityConfig?.privateKey?.base64EncodedString() ?? "" && hasValidPrivateKey { hasChanges = true }
}
.onChange(of: adminKey) { _, key in
let tempKey = Data(base64Encoded: key) ?? Data()
@ -164,7 +170,7 @@ struct SecurityConfig: View {
} else {
hasValidAdminKey = false
}
hasChanges = true
if key != node?.securityConfig?.adminKey?.base64EncodedString() ?? "" && hasValidAdminKey { hasChanges = true }
}
.onChange(of: adminKey2) { _, key in
let tempKey = Data(base64Encoded: key) ?? Data()
@ -175,7 +181,7 @@ struct SecurityConfig: View {
} else {
hasValidAdminKey2 = false
}
hasChanges = true
if key != node?.securityConfig?.adminKey2?.base64EncodedString() ?? "" && hasValidAdminKey2 { hasChanges = true }
}
.onChange(of: adminKey3) { _, key in
let tempKey = Data(base64Encoded: key) ?? Data()
@ -186,10 +192,10 @@ struct SecurityConfig: View {
} else {
hasValidAdminKey3 = false
}
hasChanges = true
if key != node?.securityConfig?.adminKey3?.base64EncodedString() ?? "" && hasValidAdminKey3 { hasChanges = true }
}
.onFirstAppear {
// Need to request a DeviceConfig from the remote node before allowing changes
// Need to request a SecurityConfig from the remote node before allowing changes
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
if let connectedNode {
@ -199,7 +205,7 @@ struct SecurityConfig: View {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.securityConfig == nil {
Logger.mesh.info("⚙️ Empty or expired security config requesting via PKI admin")
_ = bleManager.requestSecurityConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
_ = bleManager.requestSecurityConfig(fromUser: connectedNode.user!, toUser: node.user!)
}
} else {
if node.deviceConfig == nil {
@ -231,18 +237,26 @@ struct SecurityConfig: View {
config.isManaged = isManaged
config.serialEnabled = serialEnabled
config.debugLogApiEnabled = debugLogApiEnabled
config.adminChannelEnabled = adminChannelEnabled
let reboot = node?.securityConfig?.privateKey?.base64EncodedString() ?? "" != privateKey
let adminMessageId = bleManager.saveSecurityConfig(
config: config,
fromUser: fromUser,
toUser: toUser,
adminIndex: connectedNode.myInfo?.adminIndex ?? 0
toUser: toUser
)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
hasChanges = false
if reboot {
if !bleManager.sendReboot(
fromUser: fromUser,
toUser: toUser
) {
Logger.mesh.warning("Reboot Failed")
}
}
goBack()
}
}
@ -257,7 +271,24 @@ struct SecurityConfig: View {
self.isManaged = node?.securityConfig?.isManaged ?? false
self.serialEnabled = node?.securityConfig?.serialEnabled ?? false
self.debugLogApiEnabled = node?.securityConfig?.debugLogApiEnabled ?? false
self.adminChannelEnabled = node?.securityConfig?.adminChannelEnabled ?? false
self.hasChanges = false
}
func generatePrivateKey(count: Int) -> Data? {
var randomBytes = Data(count: count)
let status = randomBytes.withUnsafeMutableBytes { (mutableBytes: UnsafeMutableRawBufferPointer) -> Int32 in
guard let pointer = mutableBytes.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Indicate an error
}
return SecRandomCopyBytes(kSecRandomDefault, count, pointer)
}
if status == errSecSuccess {
return randomBytes
} else {
// Handle error, perhaps by logging or throwing an exception
print("Error generating random bytes: \(status)")
return nil
}
}
}

View file

@ -160,7 +160,7 @@ struct Firmware: View {
Button {
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context)
if connectedNode != nil {
if !bleManager.sendRebootOta(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex) {
if !bleManager.sendRebootOta(fromUser: connectedNode!.user!, toUser: node!.user!) {
Logger.mesh.error("Reboot Failed")
}
}

View file

@ -432,7 +432,7 @@ struct Settings: View {
let connectedNode = nodes.first(where: { $0.num == preferredNodeNum })
preferredNodeNum = Int(connectedNode?.num ?? 0)// Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
if connectedNode != nil && connectedNode?.user != nil && connectedNode?.myInfo != nil && node?.user != nil {// && node?.metadata == nil {
let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context)
let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, context: context)
if adminMessageId > 0 {
Logger.mesh.info("Sent node metadata request from node details")
}

View file

@ -83,7 +83,7 @@ struct ShareChannels: View {
.labelsHidden()
Text(((channel.name!.isEmpty ? "Primary" : channel.name) ?? "Primary").camelCaseToWords())
if channel.psk?.hexDescription.count ?? 0 < 3 {
Image(systemName: "lock.slash")
Image(systemName: "lock.slash.fill")
.foregroundColor(.red)
} else {
Image(systemName: "lock.fill")
@ -96,7 +96,7 @@ struct ShareChannels: View {
.disabled(channel.role == 1)
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
if channel.psk?.hexDescription.count ?? 0 < 3 {
Image(systemName: "lock.slash")
Image(systemName: "lock.slash.fill")
.foregroundColor(.red)
} else {
Image(systemName: "lock.fill")
@ -109,7 +109,7 @@ struct ShareChannels: View {
.disabled(channel.role == 1)
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
if channel.psk?.hexDescription.count ?? 0 < 3 {
Image(systemName: "lock.slash")
Image(systemName: "lock.slash.fill")
.foregroundColor(.red)
} else {
Image(systemName: "lock.fill")
@ -122,7 +122,7 @@ struct ShareChannels: View {
.disabled(channel.role == 1)
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
if channel.psk?.hexDescription.count ?? 0 < 3 {
Image(systemName: "lock.slash")
Image(systemName: "lock.slash.fill")
.foregroundColor(.red)
} else {
Image(systemName: "lock.fill")
@ -135,7 +135,7 @@ struct ShareChannels: View {
.disabled(channel.role == 1)
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
if channel.psk?.hexDescription.count ?? 0 < 3 {
Image(systemName: "lock.slash")
Image(systemName: "lock.slash.fill")
.foregroundColor(.red)
} else {
Image(systemName: "lock.fill")
@ -148,7 +148,7 @@ struct ShareChannels: View {
.disabled(channel.role == 1)
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
if channel.psk?.hexDescription.count ?? 0 < 3 {
Image(systemName: "lock.slash")
Image(systemName: "lock.slash.fill")
.foregroundColor(.red)
} else {
Image(systemName: "lock.fill")
@ -161,7 +161,7 @@ struct ShareChannels: View {
.disabled(channel.role == 1)
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
if channel.psk?.hexDescription.count ?? 0 < 3 {
Image(systemName: "lock.slash")
Image(systemName: "lock.slash.fill")
.foregroundColor(.red)
} else {
Image(systemName: "lock.fill")
@ -174,7 +174,7 @@ struct ShareChannels: View {
.disabled(channel.role == 1)
Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize()
if channel.psk?.hexDescription.count ?? 0 < 3 {
Image(systemName: "lock.slash")
Image(systemName: "lock.slash.fill")
.foregroundColor(.red)
} else {
Image(systemName: "lock.fill")

View file

@ -176,7 +176,7 @@ struct UserConfig: View {
u.shortName = shortName
u.longName = longName
u.isUnmessagable = isUnmessagable
let adminMessageId = bleManager.saveUser(config: u, fromUser: connectedUser, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.saveUser(config: u, fromUser: connectedUser, toUser: node!.user!)
if adminMessageId > 0 {
hasChanges = false
goBack()
@ -188,7 +188,7 @@ struct UserConfig: View {
ham.callSign = longName
ham.txPower = Int32(txPower)
ham.frequency = overrideFrequency
let adminMessageId = bleManager.saveLicensedUser(ham: ham, fromUser: connectedUser, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
let adminMessageId = bleManager.saveLicensedUser(ham: ham, fromUser: connectedUser, toUser: node!.user!)
if adminMessageId > 0 {
hasChanges = false
goBack()

View file

@ -292,6 +292,17 @@ public struct AdminMessage: @unchecked Sendable {
set {payloadVariant = .removeBackupPreferences(newValue)}
}
///
/// Send an input event to the node.
/// This is used to trigger physical input events like button presses, touch events, etc.
public var sendInputEvent: AdminMessage.InputEvent {
get {
if case .sendInputEvent(let v)? = payloadVariant {return v}
return AdminMessage.InputEvent()
}
set {payloadVariant = .sendInputEvent(newValue)}
}
///
/// Set the owner for this node
public var setOwner: User {
@ -663,6 +674,10 @@ public struct AdminMessage: @unchecked Sendable {
/// Remove backups of the node's preferences
case removeBackupPreferences(AdminMessage.BackupLocation)
///
/// Send an input event to the node.
/// This is used to trigger physical input events like button presses, touch events, etc.
case sendInputEvent(AdminMessage.InputEvent)
///
/// Set the owner for this node
case setOwner(User)
///
@ -1014,6 +1029,34 @@ public struct AdminMessage: @unchecked Sendable {
}
///
/// Input event message to be sent to the node.
public struct InputEvent: Sendable {
// 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.
///
/// The input event code
public var eventCode: UInt32 = 0
///
/// Keyboard character code
public var kbChar: UInt32 = 0
///
/// The touch X coordinate
public var touchX: UInt32 = 0
///
/// The touch Y coordinate
public var touchY: UInt32 = 0
public var unknownFields = SwiftProtobuf.UnknownStorage()
public init() {}
}
public init() {}
}
@ -1083,6 +1126,10 @@ public struct SharedContact: Sendable {
/// Clears the value of `user`. Subsequent reads from it will return its default value.
public mutating func clearUser() {self._user = nil}
///
/// Add this contact to the blocked / ignored list
public var shouldIgnore: Bool = false
public var unknownFields = SwiftProtobuf.UnknownStorage()
public init() {}
@ -1215,6 +1262,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
24: .standard(proto: "backup_preferences"),
25: .standard(proto: "restore_preferences"),
26: .standard(proto: "remove_backup_preferences"),
27: .standard(proto: "send_input_event"),
32: .standard(proto: "set_owner"),
33: .standard(proto: "set_channel"),
34: .standard(proto: "set_config"),
@ -1491,6 +1539,19 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
self.payloadVariant = .removeBackupPreferences(v)
}
}()
case 27: try {
var v: AdminMessage.InputEvent?
var hadOneofValue = false
if let current = self.payloadVariant {
hadOneofValue = true
if case .sendInputEvent(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
self.payloadVariant = .sendInputEvent(v)
}
}()
case 32: try {
var v: User?
var hadOneofValue = false
@ -1872,6 +1933,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
guard case .removeBackupPreferences(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularEnumField(value: v, fieldNumber: 26)
}()
case .sendInputEvent?: try {
guard case .sendInputEvent(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 27)
}()
case .setOwner?: try {
guard case .setOwner(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 32)
@ -2040,6 +2105,56 @@ extension AdminMessage.BackupLocation: SwiftProtobuf._ProtoNameProviding {
]
}
extension AdminMessage.InputEvent: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = AdminMessage.protoMessageName + ".InputEvent"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .standard(proto: "event_code"),
2: .standard(proto: "kb_char"),
3: .standard(proto: "touch_x"),
4: .standard(proto: "touch_y"),
]
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.eventCode) }()
case 2: try { try decoder.decodeSingularUInt32Field(value: &self.kbChar) }()
case 3: try { try decoder.decodeSingularUInt32Field(value: &self.touchX) }()
case 4: try { try decoder.decodeSingularUInt32Field(value: &self.touchY) }()
default: break
}
}
}
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if self.eventCode != 0 {
try visitor.visitSingularUInt32Field(value: self.eventCode, fieldNumber: 1)
}
if self.kbChar != 0 {
try visitor.visitSingularUInt32Field(value: self.kbChar, fieldNumber: 2)
}
if self.touchX != 0 {
try visitor.visitSingularUInt32Field(value: self.touchX, fieldNumber: 3)
}
if self.touchY != 0 {
try visitor.visitSingularUInt32Field(value: self.touchY, fieldNumber: 4)
}
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: AdminMessage.InputEvent, rhs: AdminMessage.InputEvent) -> Bool {
if lhs.eventCode != rhs.eventCode {return false}
if lhs.kbChar != rhs.kbChar {return false}
if lhs.touchX != rhs.touchX {return false}
if lhs.touchY != rhs.touchY {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".HamParameters"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
@ -2127,6 +2242,7 @@ extension SharedContact: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .standard(proto: "node_num"),
2: .same(proto: "user"),
3: .standard(proto: "should_ignore"),
]
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -2137,6 +2253,7 @@ extension SharedContact: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa
switch fieldNumber {
case 1: try { try decoder.decodeSingularUInt32Field(value: &self.nodeNum) }()
case 2: try { try decoder.decodeSingularMessageField(value: &self._user) }()
case 3: try { try decoder.decodeSingularBoolField(value: &self.shouldIgnore) }()
default: break
}
}
@ -2153,12 +2270,16 @@ extension SharedContact: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa
try { if let v = self._user {
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
} }()
if self.shouldIgnore != false {
try visitor.visitSingularBoolField(value: self.shouldIgnore, fieldNumber: 3)
}
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: SharedContact, rhs: SharedContact) -> Bool {
if lhs.nodeNum != rhs.nodeNum {return false}
if lhs._user != rhs._user {return false}
if lhs.shouldIgnore != rhs.shouldIgnore {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}

View file

@ -189,6 +189,11 @@ public struct Config: Sendable {
/// If true, disable the default blinking LED (LED_PIN) behavior on the device
public var ledHeartbeatDisabled: Bool = false
///
/// Controls buzzer behavior for audio feedback
/// Defaults to ENABLED
public var buzzerMode: Config.DeviceConfig.BuzzerMode = .allEnabled
public var unknownFields = SwiftProtobuf.UnknownStorage()
///
@ -406,6 +411,67 @@ public struct Config: Sendable {
}
///
/// Defines buzzer behavior for audio feedback
public enum BuzzerMode: SwiftProtobuf.Enum, Swift.CaseIterable {
public typealias RawValue = Int
///
/// Default behavior.
/// Buzzer is enabled for all audio feedback including button presses and alerts.
case allEnabled // = 0
///
/// Disabled.
/// All buzzer audio feedback is disabled.
case disabled // = 1
///
/// Notifications Only.
/// Buzzer is enabled only for notifications and alerts, but not for button presses.
/// External notification config determines the specifics of the notification behavior.
case notificationsOnly // = 2
///
/// Non-notification system buzzer tones only.
/// Buzzer is enabled only for non-notification tones such as button presses, startup, shutdown, but not for alerts.
case systemOnly // = 3
case UNRECOGNIZED(Int)
public init() {
self = .allEnabled
}
public init?(rawValue: Int) {
switch rawValue {
case 0: self = .allEnabled
case 1: self = .disabled
case 2: self = .notificationsOnly
case 3: self = .systemOnly
default: self = .UNRECOGNIZED(rawValue)
}
}
public var rawValue: Int {
switch self {
case .allEnabled: return 0
case .disabled: return 1
case .notificationsOnly: return 2
case .systemOnly: return 3
case .UNRECOGNIZED(let i): return i
}
}
// The compiler won't synthesize support with the UNRECOGNIZED case.
public static let allCases: [Config.DeviceConfig.BuzzerMode] = [
.allEnabled,
.disabled,
.notificationsOnly,
.systemOnly,
]
}
public init() {}
}
@ -2063,6 +2129,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
10: .standard(proto: "disable_triple_click"),
11: .same(proto: "tzdef"),
12: .standard(proto: "led_heartbeat_disabled"),
13: .standard(proto: "buzzer_mode"),
]
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -2082,6 +2149,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
case 10: try { try decoder.decodeSingularBoolField(value: &self.disableTripleClick) }()
case 11: try { try decoder.decodeSingularStringField(value: &self.tzdef) }()
case 12: try { try decoder.decodeSingularBoolField(value: &self.ledHeartbeatDisabled) }()
case 13: try { try decoder.decodeSingularEnumField(value: &self.buzzerMode) }()
default: break
}
}
@ -2121,6 +2189,9 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
if self.ledHeartbeatDisabled != false {
try visitor.visitSingularBoolField(value: self.ledHeartbeatDisabled, fieldNumber: 12)
}
if self.buzzerMode != .allEnabled {
try visitor.visitSingularEnumField(value: self.buzzerMode, fieldNumber: 13)
}
try unknownFields.traverse(visitor: &visitor)
}
@ -2136,6 +2207,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
if lhs.disableTripleClick != rhs.disableTripleClick {return false}
if lhs.tzdef != rhs.tzdef {return false}
if lhs.ledHeartbeatDisabled != rhs.ledHeartbeatDisabled {return false}
if lhs.buzzerMode != rhs.buzzerMode {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
@ -2169,6 +2241,15 @@ extension Config.DeviceConfig.RebroadcastMode: SwiftProtobuf._ProtoNameProviding
]
}
extension Config.DeviceConfig.BuzzerMode: SwiftProtobuf._ProtoNameProviding {
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
0: .same(proto: "ALL_ENABLED"),
1: .same(proto: "DISABLED"),
2: .same(proto: "NOTIFICATIONS_ONLY"),
3: .same(proto: "SYSTEM_ONLY"),
]
}
extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = Config.protoMessageName + ".PositionConfig"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [

View file

@ -141,6 +141,10 @@ public enum Language: SwiftProtobuf.Enum, Swift.CaseIterable {
/// Ukrainian
case ukrainian // = 16
///
/// Bulgarian
case bulgarian // = 17
///
/// Simplified Chinese (experimental)
case simplifiedChinese // = 30
@ -173,6 +177,7 @@ public enum Language: SwiftProtobuf.Enum, Swift.CaseIterable {
case 14: self = .norwegian
case 15: self = .slovenian
case 16: self = .ukrainian
case 17: self = .bulgarian
case 30: self = .simplifiedChinese
case 31: self = .traditionalChinese
default: self = .UNRECOGNIZED(rawValue)
@ -198,6 +203,7 @@ public enum Language: SwiftProtobuf.Enum, Swift.CaseIterable {
case .norwegian: return 14
case .slovenian: return 15
case .ukrainian: return 16
case .bulgarian: return 17
case .simplifiedChinese: return 30
case .traditionalChinese: return 31
case .UNRECOGNIZED(let i): return i
@ -223,6 +229,7 @@ public enum Language: SwiftProtobuf.Enum, Swift.CaseIterable {
.norwegian,
.slovenian,
.ukrainian,
.bulgarian,
.simplifiedChinese,
.traditionalChinese,
]
@ -502,6 +509,7 @@ extension Language: SwiftProtobuf._ProtoNameProviding {
14: .same(proto: "NORWEGIAN"),
15: .same(proto: "SLOVENIAN"),
16: .same(proto: "UKRAINIAN"),
17: .same(proto: "BULGARIAN"),
30: .same(proto: "SIMPLIFIED_CHINESE"),
31: .same(proto: "TRADITIONAL_CHINESE"),
]

View file

@ -458,6 +458,18 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable {
/// Reserved ID for future and past use
case qwantzTinyArms // = 101
///*
/// Lilygo T-Deck Pro
case tDeckPro // = 102
///*
/// Lilygo TLora Pager
case tLoraPager // = 103
///*
/// GAT562 Mesh Trial Tracker
case gat562MeshTrialTracker // = 104
///
/// ------------------------------------------------------------------------------------------------------------------------------------------
/// 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.
@ -573,6 +585,9 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable {
case 99: self = .seeedWioTrackerL1
case 100: self = .seeedWioTrackerL1Eink
case 101: self = .qwantzTinyArms
case 102: self = .tDeckPro
case 103: self = .tLoraPager
case 104: self = .gat562MeshTrialTracker
case 255: self = .privateHw
default: self = .UNRECOGNIZED(rawValue)
}
@ -682,6 +697,9 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable {
case .seeedWioTrackerL1: return 99
case .seeedWioTrackerL1Eink: return 100
case .qwantzTinyArms: return 101
case .tDeckPro: return 102
case .tLoraPager: return 103
case .gat562MeshTrialTracker: return 104
case .privateHw: return 255
case .UNRECOGNIZED(let i): return i
}
@ -791,6 +809,9 @@ public enum HardwareModel: SwiftProtobuf.Enum, Swift.CaseIterable {
.seeedWioTrackerL1,
.seeedWioTrackerL1Eink,
.qwantzTinyArms,
.tDeckPro,
.tLoraPager,
.gat562MeshTrialTracker,
.privateHw,
]
@ -2991,12 +3012,30 @@ public struct ClientNotification: Sendable {
set {payloadVariant = .keyVerificationFinal(newValue)}
}
public var duplicatedPublicKey: DuplicatedPublicKey {
get {
if case .duplicatedPublicKey(let v)? = payloadVariant {return v}
return DuplicatedPublicKey()
}
set {payloadVariant = .duplicatedPublicKey(newValue)}
}
public var lowEntropyKey: LowEntropyKey {
get {
if case .lowEntropyKey(let v)? = payloadVariant {return v}
return LowEntropyKey()
}
set {payloadVariant = .lowEntropyKey(newValue)}
}
public var unknownFields = SwiftProtobuf.UnknownStorage()
public enum OneOf_PayloadVariant: Equatable, Sendable {
case keyVerificationNumberInform(KeyVerificationNumberInform)
case keyVerificationNumberRequest(KeyVerificationNumberRequest)
case keyVerificationFinal(KeyVerificationFinal)
case duplicatedPublicKey(DuplicatedPublicKey)
case lowEntropyKey(LowEntropyKey)
}
@ -3053,6 +3092,26 @@ public struct KeyVerificationFinal: Sendable {
public init() {}
}
public struct DuplicatedPublicKey: Sendable {
// 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.
public var unknownFields = SwiftProtobuf.UnknownStorage()
public init() {}
}
public struct LowEntropyKey: Sendable {
// 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.
public var unknownFields = SwiftProtobuf.UnknownStorage()
public init() {}
}
///
/// Individual File info for the device
public struct FileInfo: Sendable {
@ -3578,6 +3637,9 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding {
99: .same(proto: "SEEED_WIO_TRACKER_L1"),
100: .same(proto: "SEEED_WIO_TRACKER_L1_EINK"),
101: .same(proto: "QWANTZ_TINY_ARMS"),
102: .same(proto: "T_DECK_PRO"),
103: .same(proto: "T_LORA_PAGER"),
104: .same(proto: "GAT562_MESH_TRIAL_TRACKER"),
255: .same(proto: "PRIVATE_HW"),
]
}
@ -5348,6 +5410,8 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple
11: .standard(proto: "key_verification_number_inform"),
12: .standard(proto: "key_verification_number_request"),
13: .standard(proto: "key_verification_final"),
14: .standard(proto: "duplicated_public_key"),
15: .standard(proto: "low_entropy_key"),
]
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -5399,6 +5463,32 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple
self.payloadVariant = .keyVerificationFinal(v)
}
}()
case 14: try {
var v: DuplicatedPublicKey?
var hadOneofValue = false
if let current = self.payloadVariant {
hadOneofValue = true
if case .duplicatedPublicKey(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
self.payloadVariant = .duplicatedPublicKey(v)
}
}()
case 15: try {
var v: LowEntropyKey?
var hadOneofValue = false
if let current = self.payloadVariant {
hadOneofValue = true
if case .lowEntropyKey(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
self.payloadVariant = .lowEntropyKey(v)
}
}()
default: break
}
}
@ -5434,6 +5524,14 @@ extension ClientNotification: SwiftProtobuf.Message, SwiftProtobuf._MessageImple
guard case .keyVerificationFinal(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 13)
}()
case .duplicatedPublicKey?: try {
guard case .duplicatedPublicKey(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 14)
}()
case .lowEntropyKey?: try {
guard case .lowEntropyKey(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 15)
}()
case nil: break
}
try unknownFields.traverse(visitor: &visitor)
@ -5582,6 +5680,44 @@ extension KeyVerificationFinal: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
}
}
extension DuplicatedPublicKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".DuplicatedPublicKey"
public static let _protobuf_nameMap = SwiftProtobuf._NameMap()
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
// Load everything into unknown fields
while try decoder.nextFieldNumber() != nil {}
}
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: DuplicatedPublicKey, rhs: DuplicatedPublicKey) -> Bool {
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension LowEntropyKey: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".LowEntropyKey"
public static let _protobuf_nameMap = SwiftProtobuf._NameMap()
public mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
// Load everything into unknown fields
while try decoder.nextFieldNumber() != nil {}
}
public func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
try unknownFields.traverse(visitor: &visitor)
}
public static func ==(lhs: LowEntropyKey, rhs: LowEntropyKey) -> Bool {
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension FileInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
public static let protoMessageName: String = _protobuf_package + ".FileInfo"
public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [