Merge branch 'meshtastic:main' into main

This commit is contained in:
Phil Rosa-Leeke 2024-05-07 17:33:05 +01:00 committed by GitHub
commit a663c9b996
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
96 changed files with 1614 additions and 437 deletions

2
.gitmodules vendored
View file

@ -1,3 +1,3 @@
[submodule "protobufs"]
path = protobufs
url = http://github.com/meshtastic/protobufs
url = https://github.com/meshtastic/protobufs.git

View file

@ -50,6 +50,7 @@
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */; };
DD33DB622B3D27C7003E1EA0 /* FirmwareApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */; };
DD3501892852FC3B000FC853 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3501882852FC3B000FC853 /* Settings.swift */; };
DD354FD92BD96A0B0061A25F /* IAQScale.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD354FD82BD96A0B0061A25F /* IAQScale.swift */; };
DD3619152B1EF9F900C41C8C /* LocationsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */; };
DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */; };
DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */; };
@ -87,7 +88,7 @@
DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51FF298EE33B00D21B61 /* remote_hardware.pb.swift */; };
DD5E5212298EE33B00D21B61 /* apponly.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E5200298EE33B00D21B61 /* apponly.pb.swift */; };
DD5E5213298EE33B00D21B61 /* deviceonly.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E5201298EE33B00D21B61 /* deviceonly.pb.swift */; };
DD5E523F298F5A9E00D21B61 /* AirQualityIndexCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E523E298F5A9E00D21B61 /* AirQualityIndexCompact.swift */; };
DD5E523F298F5A9E00D21B61 /* AirQualityIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E523E298F5A9E00D21B61 /* AirQualityIndex.swift */; };
DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */; };
DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */; };
DD6193792863875F00E59241 /* SerialConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193782863875F00E59241 /* SerialConfig.swift */; };
@ -292,10 +293,12 @@
DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV22.xcdatamodel; sourceTree = "<group>"; };
DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewSwiftUI.swift; sourceTree = "<group>"; };
DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV19.xcdatamodel; sourceTree = "<group>"; };
DD31B04D2BDC6FD30024FA63 /* MeshtasticDataModelV 36.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 36.xcdatamodel"; sourceTree = "<group>"; };
DD31EC492B7F18B7006A3995 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = "<group>"; };
DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 23.xcdatamodel"; sourceTree = "<group>"; };
DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareApi.swift; sourceTree = "<group>"; };
DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
DD354FD82BD96A0B0061A25F /* IAQScale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAQScale.swift; sourceTree = "<group>"; };
DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV21.xcdatamodel; sourceTree = "<group>"; };
DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsHandler.swift; sourceTree = "<group>"; };
DD398EBD2B93F640002B4C51 /* MeshtasticDataModelV 29.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 29.xcdatamodel"; sourceTree = "<group>"; };
@ -339,7 +342,7 @@
DD5E51FF298EE33B00D21B61 /* remote_hardware.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = remote_hardware.pb.swift; sourceTree = "<group>"; };
DD5E5200298EE33B00D21B61 /* apponly.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = apponly.pb.swift; sourceTree = "<group>"; };
DD5E5201298EE33B00D21B61 /* deviceonly.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = deviceonly.pb.swift; sourceTree = "<group>"; };
DD5E523E298F5A9E00D21B61 /* AirQualityIndexCompact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirQualityIndexCompact.swift; sourceTree = "<group>"; };
DD5E523E298F5A9E00D21B61 /* AirQualityIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirQualityIndex.swift; sourceTree = "<group>"; };
DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalNotificationConfig.swift; sourceTree = "<group>"; };
DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = "<group>"; };
DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = "<group>"; };
@ -649,9 +652,11 @@
DD5E523D298F5A7D00D21B61 /* Weather */ = {
isa = PBXGroup;
children = (
DD5E523E298F5A9E00D21B61 /* AirQualityIndexCompact.swift */,
DD5E523E298F5A9E00D21B61 /* AirQualityIndex.swift */,
DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */,
DDA9515D2BC6F56F00CEA535 /* IndoorAirQuality.swift */,
DD41A61429AB0035003C5A37 /* NodeWeatherForecast.swift */,
DD354FD82BD96A0B0061A25F /* IAQScale.swift */,
);
path = Weather;
sourceTree = "<group>";
@ -921,7 +926,6 @@
DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */,
DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */,
DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */,
DDA9515D2BC6F56F00CEA535 /* IndoorAirQuality.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -1204,7 +1208,7 @@
DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */,
DD0E20FD2B87090400F2D100 /* clientonly.pb.swift in Sources */,
D93069082B81DF040066FBC8 /* SaveConfigButton.swift in Sources */,
DD5E523F298F5A9E00D21B61 /* AirQualityIndexCompact.swift in Sources */,
DD5E523F298F5A9E00D21B61 /* AirQualityIndex.swift in Sources */,
DD964FBF296E76EF007C176F /* WaypointFormMapKit.swift in Sources */,
DD3501892852FC3B000FC853 /* Settings.swift in Sources */,
DDDC22382BA92344002C44F1 /* MeshMapContent.swift in Sources */,
@ -1239,6 +1243,7 @@
DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */,
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */,
DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */,
DD354FD92BD96A0B0061A25F /* IAQScale.swift in Sources */,
DDDB445429F8AD1600EE2349 /* Data.swift in Sources */,
DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */,
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */,
@ -1578,7 +1583,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.3.6;
MARKETING_VERSION = 2.3.8;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1612,7 +1617,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.3.6;
MARKETING_VERSION = 2.3.8;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1685,7 +1690,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.3.6;
MARKETING_VERSION = 2.3.8;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1718,7 +1723,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.3.6;
MARKETING_VERSION = 2.3.8;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1820,6 +1825,7 @@
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
DD31B04D2BDC6FD30024FA63 /* MeshtasticDataModelV 36.xcdatamodel */,
DD268D8C2BCC7D11008073AE /* MeshtasticDataModelV 35.xcdatamodel */,
DDDBC87C2BC65682001E8DF7 /* MeshtasticDataModelV 34.xcdatamodel */,
DDF45C382BC46B16005ED5F2 /* MeshtasticDataModelV33.xcdatamodel */,
@ -1856,7 +1862,7 @@
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
);
currentVersion = DD268D8C2BCC7D11008073AE /* MeshtasticDataModelV 35.xcdatamodel */;
currentVersion = DD31B04D2BDC6FD30024FA63 /* MeshtasticDataModelV 36.xcdatamodel */;
name = Meshtastic.xcdatamodeld;
path = Meshtastic/Meshtastic.xcdatamodeld;
sourceTree = "<group>";

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "play_store_icon_114px-2.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "play_store_icon_114px-3.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "play_store_icon_114px-4.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "play_store_icon_114px-2.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "play_store_icon_114px-3.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "play_store_icon_114px-4.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "play_store_icon_114px-2.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "play_store_icon_114px-3.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "play_store_icon_114px-4.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "play_store_icon_114px-2.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "play_store_icon_114px-3.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "play_store_icon_114px-4.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,17 +1,17 @@
{
"images" : [
{
"filename" : "play_store_icon_114px-2.png",
"filename" : "Paper-Meshtastic-2 copy.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "play_store_icon_114px-3.png",
"filename" : "Paper-Meshtastic-2 copy 1.jpg",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "play_store_icon_114px-4.png",
"filename" : "Paper-Meshtastic-2 copy 2.jpg",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "Paper-Meshtastic-2 copy.jpg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Paper-Meshtastic-2 copy 1.jpg",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Paper-Meshtastic-2 copy 2.jpg",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View file

@ -1,17 +1,17 @@
{
"images" : [
{
"filename" : "play_store_icon_114px-2.png",
"filename" : "images.jpeg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "play_store_icon_114px-3.png",
"filename" : "images 1.jpeg",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "play_store_icon_114px-4.png",
"filename" : "images 2.jpeg",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "images.jpeg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "images 1.jpeg",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "images 2.jpeg",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "play_store_icon_114px-2.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "play_store_icon_114px-2 1.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "play_store_icon_114px-2 2.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "play_store_icon_114px-2.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "play_store_icon_114px-3.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "play_store_icon_114px-4.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "UNPHONE.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "UNPHONE@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "UNPHONE@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 KiB

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "play_store_icon_114px-2.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "play_store_icon_114px-3.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "play_store_icon_114px-4.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -100,8 +100,8 @@ enum GpsUpdateIntervals: Int, CaseIterable, Identifiable {
}
enum GpsMode: Int, CaseIterable, Equatable {
case disabled = 0
case enabled = 1
case disabled = 0
case notPresent = 2
var id: Int { self.rawValue }

View file

@ -8,6 +8,86 @@
import Foundation
import SwiftUI
enum Aqi: Int, CaseIterable, Identifiable {
case good = 0
case moderate = 1
case sensitive = 2
case unhealthy = 3
case veryUnhealthy = 4
case hazardous = 5
var id: Int { self.rawValue }
var description: String {
switch self {
case .good:
return "Good"
case .moderate:
return "Moderate"
case .sensitive:
return "Unhealthy for Sensitive Groups"
case .unhealthy:
return "Unhealthy"
case .veryUnhealthy:
return "Very Unhealthy"
case .hazardous:
return "Hazardous"
}
}
var color: Color {
switch self {
case .good:
return .green
case .moderate:
return .yellow
case .sensitive:
return .orange
case .unhealthy:
return .red
case .veryUnhealthy:
return .purple
case .hazardous:
return .magenta
}
}
var range: Range<Int> {
switch self {
case .good:
return Range(0...50)
case .moderate:
return Range(51...100)
case .sensitive:
return Range(101...150)
case .unhealthy:
return Range(151...200)
case .veryUnhealthy:
return Range(201...300)
case .hazardous:
return Range(301...500)
}
}
static func getAqi(for value: Int) -> Aqi {
let aqi: Aqi
switch value {
case 0...50:
aqi = .good
case 51...100:
aqi = .moderate
case 101...150:
aqi = .sensitive
case 151...200:
aqi = .unhealthy
case 201...300:
aqi = .veryUnhealthy
case 301...500:
aqi = .hazardous
default:
fatalError("Invalid int value")
}
return aqi
}
}
enum Iaq: Int, CaseIterable, Identifiable {
case excellent = 0
case good = 1
@ -27,7 +107,7 @@ enum Iaq: Int, CaseIterable, Identifiable {
case .lightlyPolluted:
return "Lightly Polluted"
case .moderatelyPolluted:
return "Lightly Polluted"
return "Moderately Polluted"
case .heavilyPolluted:
return "Heavily Polluted"
case .severelyPolluted:
@ -49,11 +129,30 @@ enum Iaq: Int, CaseIterable, Identifiable {
case .heavilyPolluted:
return .red
case .severelyPolluted:
return .purple
return .magenta
case .extremelyPolluted:
return .brown
}
}
var range: Range<Int> {
switch self {
case .excellent:
return Range(0...50)
case .good:
return Range(51...100)
case .lightlyPolluted:
return Range(101...150)
case .moderatelyPolluted:
return Range(151...200)
case .heavilyPolluted:
return Range(201...250)
case .severelyPolluted:
return Range(251...350)
case .extremelyPolluted:
return Range(351...500)
}
}
static func getIaq(for value: Int) -> Iaq {
let iaq: Iaq
switch value {

View file

@ -32,7 +32,7 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
}
} else if metricsType == 1 {
// Create Environment Telemetry Header
csvString = "Temperature, Relative Humidity, Barometric Pressure, Indoor Air Quality, Gas Resistance, \("voltage".localized), \("current".localized), \("timestamp".localized)"
csvString = "Temperature, Relative Humidity, Barometric Pressure, Indoor Air Quality, Gas Resistance, \("timestamp".localized)"
for dm in telemetry {
if dm.metricsType == 1 {
csvString += "\n"
@ -46,10 +46,6 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
csvString += ", "
csvString += String(dm.gasResistance)
csvString += ", "
csvString += String(dm.voltage)
csvString += ", "
csvString += String(dm.current)
csvString += ", "
csvString += dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized
}
}

View file

@ -17,6 +17,7 @@ extension Color {
let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000
return (brightness > 0.5)
}
public static let magenta = Color(red: 0.50, green: 0.00, blue: 0.00)
}
extension UIColor {

View file

@ -18,4 +18,14 @@ extension ChannelEntity {
let unreadMessages = allPrivateMessages.filter{ ($0 as AnyObject).read == false }
return unreadMessages.count
}
var protoBuf: Channel {
var channel = Channel()
channel.index = self.index
channel.settings.name = self.name ?? ""
channel.settings.psk = self.psk ?? Data()
channel.role = Channel.Role(rawValue: Int(self.role)) ?? Channel.Role.secondary
channel.settings.moduleSettings.positionPrecision = UInt32(self.positionPrecision)
return channel
}
}

View file

@ -68,6 +68,7 @@ extension UserDefaults {
case enableSmartPosition
case newNodeNotifications
case lowBatteryNotifications
case channelMessageNotifications
case modemPreset
case firmwareVersion
case testIntEnum
@ -146,6 +147,9 @@ extension UserDefaults {
@UserDefault(.enableSmartPosition, defaultValue: false)
static var enableSmartPosition: Bool
@UserDefault(.channelMessageNotifications, defaultValue: true)
static var channelMessageNotifications: Bool
@UserDefault(.newNodeNotifications, defaultValue: true)
static var newNodeNotifications: Bool

View file

@ -18,6 +18,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
}
var context: NSManagedObjectContext?
static let shared = BLEManager()
//var userSettings: UserSettings?
private var centralManager: CBCentralManager!
@Published var peripherals: [Peripheral] = []
@ -151,12 +153,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
isConnecting = false
isConnected = false
isSubscribed = false
self.connectedPeripheral = nil
invalidVersion = false
connectedVersion = "0.0.0"
connectedPeripheral = nil
if timeoutTimer != nil {
timeoutTimer!.invalidate()
}
automaticallyReconnect = false
stopScanning()
startScanning()
}
@ -542,10 +547,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
// Handle Any local only packets we get over BLE
case .unknownApp:
var nowKnown = false
guard let ctx = context else {
return
}
// MyInfo from initial connection
if context != nil && decodedInfo.myInfo.isInitialized && decodedInfo.myInfo.myNodeNum > 0 {
let myInfo = myInfoPacket(myInfo: decodedInfo.myInfo, peripheralId: self.connectedPeripheral.id, context: context!)
if decodedInfo.myInfo.isInitialized && decodedInfo.myInfo.myNodeNum > 0 {
let myInfo = myInfoPacket(myInfo: decodedInfo.myInfo, peripheralId: self.connectedPeripheral.id, context: ctx)
if myInfo != nil {
UserDefaults.preferredPeripheralNum = Int(myInfo?.myNodeNum ?? 0)
@ -556,15 +564,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
tryClearExistingChannels()
}
// NodeInfo
if decodedInfo.nodeInfo.num > 0 {// && !invalidVersion {
if decodedInfo.nodeInfo.num > 0 {
nowKnown = true
let nodeInfo = nodeInfoPacket(nodeInfo: decodedInfo.nodeInfo, channel: decodedInfo.packet.channel, context: context!)
if nodeInfo != nil {
if self.connectedPeripheral != nil && self.connectedPeripheral.num == nodeInfo?.num ?? -1 {
if nodeInfo!.user != nil {
connectedPeripheral.shortName = nodeInfo?.user?.shortName ?? "?"
connectedPeripheral.longName = nodeInfo?.user?.longName ?? "unknown".localized
if let nodeInfo = nodeInfoPacket(nodeInfo: decodedInfo.nodeInfo, channel: decodedInfo.packet.channel, context: ctx) {
if self.connectedPeripheral != nil && self.connectedPeripheral.num == nodeInfo.num {
if nodeInfo.user != nil {
connectedPeripheral.shortName = nodeInfo.user?.shortName ?? "?"
connectedPeripheral.longName = nodeInfo.user?.longName ?? "unknown".localized
}
}
}
@ -572,17 +578,17 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
// Channels
if decodedInfo.channel.isInitialized && connectedPeripheral != nil {
nowKnown = true
channelPacket(channel: decodedInfo.channel, fromNum: Int64(truncatingIfNeeded: connectedPeripheral.num), context: context!)
channelPacket(channel: decodedInfo.channel, fromNum: Int64(truncatingIfNeeded: connectedPeripheral.num), context: ctx)
}
// Config
if decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil {
nowKnown = true
localConfig(config: decodedInfo.config, context: context!, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral.longName)
localConfig(config: decodedInfo.config, context: ctx, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral.longName)
}
// Module Config
if decodedInfo.moduleConfig.isInitialized && !invalidVersion && self.connectedPeripheral?.num != 0{
nowKnown = true
moduleConfig(config: decodedInfo.moduleConfig, context: context!, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral?.num ?? 0), nodeLongName: self.connectedPeripheral.longName)
moduleConfig(config: decodedInfo.moduleConfig, context: ctx, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral?.num ?? 0), nodeLongName: self.connectedPeripheral.longName)
if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) {
if decodedInfo.moduleConfig.cannedMessage.enabled {
_ = self.getCannedMessageModuleMessages(destNum: self.connectedPeripheral.num, wantResponse: true)
@ -592,7 +598,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
// Device Metadata
if decodedInfo.metadata.firmwareVersion.count > 0 && !invalidVersion {
nowKnown = true
deviceMetadataPacket(metadata: decodedInfo.metadata, fromNum: connectedPeripheral.num, context: context!)
deviceMetadataPacket(metadata: decodedInfo.metadata, fromNum: connectedPeripheral.num, context: ctx)
connectedPeripheral.firmwareVersion = decodedInfo.metadata.firmwareVersion
let lastDotIndex = decodedInfo.metadata.firmwareVersion.lastIndex(of: ".")
if lastDotIndex == nil {
@ -994,37 +1000,28 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
return success
}
public func getPositionFromPhoneGPS(channel: Int32, destNum: Int64) -> Position? {
public func getPositionFromPhoneGPS(destNum: Int64) -> Position? {
var positionPacket = Position()
let fetchChannelRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "ChannelEntity")
fetchChannelRequest.predicate = NSPredicate(format: "index == %lld", channel)
do {
guard let fetchedChannel = try context!.fetch(fetchChannelRequest) as? [ChannelEntity] else {
return nil
}
if #available(iOS 17.0, macOS 14.0, *) {
if let lastLocation = LocationsHandler.shared.locationsArray.last {
if fetchedChannel.count > 0 {
positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7)
positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7)
let timestamp = lastLocation.timestamp
positionPacket.time = UInt32(timestamp.timeIntervalSince1970)
positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970)
positionPacket.altitude = Int32(lastLocation.altitude)
positionPacket.satsInView = UInt32(LocationsHandler.satsInView)
positionPacket.precisionBits = UInt32(fetchedChannel[0].positionPrecision)
let currentSpeed = lastLocation.speed
if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) {
positionPacket.groundSpeed = UInt32(currentSpeed * 3.6)
}
let currentHeading = lastLocation.course
if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) {
positionPacket.groundTrack = UInt32(currentHeading)
}
positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7)
positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7)
let timestamp = lastLocation.timestamp
positionPacket.time = UInt32(timestamp.timeIntervalSince1970)
positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970)
positionPacket.altitude = Int32(lastLocation.altitude)
positionPacket.satsInView = UInt32(LocationsHandler.satsInView)
let currentSpeed = lastLocation.speed
if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) {
positionPacket.groundSpeed = UInt32(currentSpeed * 3.6)
}
let currentHeading = lastLocation.course
if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) {
positionPacket.groundTrack = UInt32(currentHeading)
}
}
@ -1032,9 +1029,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if destNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 {
return nil
}
if fetchedChannel.count <= 0 {
return nil
}
positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7)
positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7)
@ -1043,7 +1037,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970)
positionPacket.altitude = Int32(LocationHelper.shared.locationManager.location?.altitude ?? 0)
positionPacket.satsInView = UInt32(LocationHelper.satsInView)
positionPacket.precisionBits = UInt32(fetchedChannel[0].positionPrecision)
let currentSpeed = LocationHelper.shared.locationManager.location?.speed ?? 0
if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) {
positionPacket.groundSpeed = UInt32(currentSpeed * 3.6)
@ -1053,17 +1046,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
positionPacket.groundTrack = UInt32(currentHeading)
}
}
} catch {
return nil
}
return positionPacket
}
public func setFixedPosition(fromUser: UserEntity, channel: Int32) -> Bool {
var adminPacket = AdminMessage()
guard let positionPacket = getPositionFromPhoneGPS(channel: channel, destNum: fromUser.num) else {
guard let positionPacket = getPositionFromPhoneGPS(destNum: fromUser.num) else {
return false
}
adminPacket.setFixedPosition = positionPacket
@ -1109,7 +1100,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
public func sendPosition(channel: Int32, destNum: Int64, wantResponse: Bool) -> Bool {
var success = false
let fromNodeNum = connectedPeripheral.num
guard let positionPacket = getPositionFromPhoneGPS(channel: channel, destNum: destNum) else {
guard let positionPacket = getPositionFromPhoneGPS(destNum: destNum) else {
return false
}
@ -2635,6 +2626,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
do {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
try context!.save()
print(adminDescription)
return true
} catch {
context!.rollback()

View file

@ -8,6 +8,7 @@
import Foundation
import CoreData
import SwiftUI
import RegexBuilder
#if canImport(ActivityKit)
import ActivityKit
#endif
@ -160,12 +161,14 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo
newChannel.name = channel.settings.name
newChannel.role = Int32(channel.role.rawValue)
newChannel.psk = channel.settings.psk
newChannel.positionPrecision = Int32(truncatingIfNeeded: channel.settings.moduleSettings.positionPrecision)
if channel.settings.hasModuleSettings {
newChannel.positionPrecision = Int32(truncatingIfNeeded: channel.settings.moduleSettings.positionPrecision)
newChannel.mute = channel.settings.moduleSettings.isClientMuted
}
guard let mutableChannels = fetchedMyInfo[0].channels!.mutableCopy() as? NSMutableOrderedSet else {
return
}
if let oldChannel = mutableChannels.first(where: {($0 as AnyObject).index == newChannel.index }) as? ChannelEntity {
newChannel.mute = oldChannel.mute
let index = mutableChannels.index(of: oldChannel as Any)
mutableChannels.replaceObject(at: index, with: newChannel)
} else {
@ -772,7 +775,19 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connectedNode: Int64, storeForward: Bool = false, context: NSManagedObjectContext) {
var messageText = String(bytes: packet.decoded.payload, encoding: .utf8)
if !wantRangeTestPackets && (String(messageText ?? "seq ").starts(with: "seq ")) {
let rangeRef = Reference(Int.self)
let rangeTestRegex = Regex {
"seq "
TryCapture(as: rangeRef) {
OneOrMore(.digit)
} transform: { match in
Int(match)
}
}
let rangeTest = messageText?.contains(rangeTestRegex) ?? false && messageText?.starts(with: "seq ") ?? false
if !wantRangeTestPackets && rangeTest {
return
}
var storeForwardBroadcast = false
@ -841,26 +856,28 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
return
}
let appState = AppState.shared
if newMessage.fromUser != nil && newMessage.toUser != nil && !(newMessage.fromUser?.mute ?? false) {
if newMessage.fromUser != nil && newMessage.toUser != nil {
// Set Unread Message Indicators
if packet.to == connectedNode {
appState.unreadDirectMessages = newMessage.toUser?.unreadMessages ?? 0
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
}
// Create an iOS Notification for the received DM message and schedule it immediately
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: "message",
path: "meshtastic://open-dm?userid=\(newMessage.fromUser?.num ?? 0)&id=\(newMessage.messageId)"
)
]
manager.schedule()
print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)")
if !(newMessage.fromUser?.mute ?? false) {
// Create an iOS Notification for the received DM message and schedule it immediately
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: "message",
path: "meshtastic://open-dm?userid=\(newMessage.fromUser?.num ?? 0)&id=\(newMessage.messageId)"
)
]
manager.schedule()
print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)")
}
} else if newMessage.fromUser != nil && newMessage.toUser == nil {
let fetchMyInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MyInfoEntity")
@ -878,7 +895,7 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
if channel.index == newMessage.channel {
context.refresh(channel, mergeChanges: true)
}
if channel.index == newMessage.channel && !channel.mute {
if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications {
// Create an iOS Notification for the received private channel message and schedule it immediately
let manager = LocalNotificationManager()
manager.notifications = [

View file

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

View file

@ -0,0 +1,461 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23F5064f" 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="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"/>
<fetchedProperty name="allPrivateMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="channel == $FETCH_SOURCE.index &amp;&amp; toUser == nil AND isEmoji == false"/>
</fetchedProperty>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="index"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="DetectionSensorConfigEntity" representedClassName="DetectionSensorConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="detectionTriggeredHigh" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<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="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="tzdef" optional="YES" attributeType="String"/>
<relationship name="deviceConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="deviceConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="DeviceMetadataEntity" representedClassName="DeviceMetadataEntity" syncable="YES" codeGenerationType="class">
<attribute name="canShutdown" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="deviceStateVersion" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="firmwareVersion" optional="YES" attributeType="String"/>
<attribute name="hasBluetooth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hasEthernet" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hasWifi" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hwModel" optional="YES" attributeType="String"/>
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="metadataNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="metadata" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="DisplayConfigEntity" representedClassName="DisplayConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="compassNorthTop" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="displayMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="flipScreen" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="gpsFormat" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="headingBold" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="oledType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="screenCarouselInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="screenOnSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="units" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wakeOnTapOrMotion" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<relationship name="displayConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="displayConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="ExternalNotificationConfigEntity" representedClassName="ExternalNotificationConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="active" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertBellBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertBellVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertMessage" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertMessageBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertMessageVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="nagTimeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="output" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="outputBuzzer" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="outputMilliseconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="outputVibra" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="useI2SAsBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="usePWM" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
<fetchedProperty name="fetchedProperty" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="ExternalNotificationConfigEntity"/>
</fetchedProperty>
</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="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="portNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<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="receivedTimestamp" optional="YES" attributeType="Integer 32" defaultValueString="0" 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"/>
<fetchedProperty name="tapbacks" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="replyID == $FETCH_SOURCE.messageId AND isEmoji == true"/>
</fetchedProperty>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="messageId"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="MQTTConfigEntity" representedClassName="MQTTConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String" maxValueString="30"/>
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="encryptionEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="jsonEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="mapPositionPrecision" optional="YES" attributeType="Integer 32" defaultValueString="13" usesScalarValueType="YES"/>
<attribute name="mapPublishIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="mapReportingEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="password" optional="YES" attributeType="String" maxValueString="30"/>
<attribute name="proxyToClientEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="root" optional="YES" attributeType="String" defaultValueString="msh"/>
<attribute name="tlsEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="username" optional="YES" attributeType="String" maxValueString="30"/>
<relationship name="mqttConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="mqttConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="MyInfoEntity" representedClassName="MyInfoEntity" syncable="YES" codeGenerationType="class">
<attribute name="adminIndex" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="bleName" optional="YES" attributeType="String"/>
<attribute name="minAppVersion" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="myNodeNum" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="peripheralId" optional="YES" attributeType="String"/>
<attribute name="rebootCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="channels" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="ChannelEntity" inverseName="myInfoChannel" inverseEntity="ChannelEntity"/>
<relationship name="myInfoNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="myInfo" inverseEntity="NodeInfoEntity"/>
<fetchedProperty name="allMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="toUser == nil"/>
</fetchedProperty>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="myNodeNum"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="NetworkConfigEntity" representedClassName="NetworkConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="dns" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ethEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="gateway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ip" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ntpServer" optional="YES" attributeType="String"/>
<attribute name="subnet" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wifiEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="wifiMode" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="wifiPsk" optional="YES" attributeType="String" minValueString="0" maxValueString="60"/>
<attribute name="wifiSsid" optional="YES" attributeType="String" minValueString="0" maxValueString="30"/>
<relationship name="networkConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="networkConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="NodeInfoEntity" representedClassName="NodeInfoEntity" syncable="YES" codeGenerationType="class">
<attribute name="bleName" optional="YES" attributeType="String"/>
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="favorite" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="hopsAway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="lastHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="peripheralId" optional="YES" attributeType="String"/>
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="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="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="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="paxcounterUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" 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="SerialConfigEntity" representedClassName="SerialConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="baudRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="echo" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="overrideConsoleSerialPort" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="rxd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="timeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="txd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="serialConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="serialConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="StoreForwardConfigEntity" representedClassName="StoreForwardConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="heartbeat" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="historyReturnMax" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="historyReturnWindow" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isRouter" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastHeartbeat" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="lastRequest" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="records" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="storeForwardConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="storeForwardConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TelemetryConfigEntity" representedClassName="TelemetryConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="deviceUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="environmentDisplayFahrenheit" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="environmentMeasurementEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="environmentScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="environmentUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="powerMeasurementEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="powerScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="powerUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="telemetryConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetryConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TelemetryEntity" representedClassName="TelemetryEntity" syncable="YES" codeGenerationType="class">
<attribute name="airUtilTx" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="barometricPressure" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="batteryLevel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="channelUtilization" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="current" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="gasResistance" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="iaq" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="metricsType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="relativeHumidity" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="temperature" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="uptimeSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="voltage" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<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="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="hasPositions" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" 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="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="response" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="route" optional="YES" attributeType="Transformable" customClassName="[UInt32]"/>
<attribute name="routeText" optional="YES" attributeType="String"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="hops" optional="YES" toMany="YES" deletionRule="Nullify" 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="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="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="hwModel" attributeType="String"/>
<attribute name="isLicensed" optional="YES" attributeType="Boolean" defaultValueString="NO" 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="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numString" optional="YES" attributeType="String"/>
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="shortName" attributeType="String"/>
<attribute name="userId" attributeType="String"/>
<relationship name="receivedMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="toUser" inverseEntity="MessageEntity"/>
<relationship name="sentMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="fromUser" inverseEntity="MessageEntity"/>
<relationship name="userNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="user" inverseEntity="NodeInfoEntity"/>
<fetchedProperty name="adminMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(fromUser.num == $FETCH_SOURCE.num) AND isEmoji == false AND admin = true"/>
</fetchedProperty>
<fetchedProperty name="allMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="((toUser.num == $FETCH_SOURCE.num) OR (fromUser.num == $FETCH_SOURCE.num)) AND toUser != nil AND fromUser != nil AND isEmoji == false AND admin = false AND portNum != 10 "/>
</fetchedProperty>
<fetchedProperty name="detectionSensorMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(fromUser.num == $FETCH_SOURCE.num) AND portNum = 10"/>
</fetchedProperty>
</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

@ -11,7 +11,7 @@ struct MeshtasticAppleApp: App {
@UIApplicationDelegateAdaptor(MeshtasticAppDelegate.self) var appDelegate
let persistenceController = PersistenceController.shared
@ObservedObject private var bleManager: BLEManager = BLEManager()
@ObservedObject private var bleManager: BLEManager = BLEManager.shared
@Environment(\.scenePhase) var scenePhase

View file

@ -355,7 +355,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
if mostRecent.coordinate.distance(from: position.coordinate) < 15.0 {
mutablePositions.remove(mostRecent)
}
} else if mutablePositions.count > 0 && 11...16 ~= position.precisionBits {
} else if mutablePositions.count > 0 {
/// Don't store any history for reduced accuracy positions, we will just show a circle
mutablePositions.removeAllObjects()
}
@ -462,6 +462,7 @@ func upsertDeviceConfigPacket(config: Meshtastic.Config.DeviceConfig, nodeNum: I
newDeviceConfig.rebroadcastMode = Int32(config.rebroadcastMode.rawValue)
newDeviceConfig.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber)
newDeviceConfig.doubleTapAsButtonPress = config.doubleTapAsButtonPress
newDeviceConfig.ledHeartbeatEnabled = !config.ledHeartbeatDisabled
newDeviceConfig.isManaged = config.isManaged
newDeviceConfig.tzdef = config.tzdef
fetchedNode[0].deviceConfig = newDeviceConfig
@ -474,6 +475,7 @@ func upsertDeviceConfigPacket(config: Meshtastic.Config.DeviceConfig, nodeNum: I
fetchedNode[0].deviceConfig?.rebroadcastMode = Int32(config.rebroadcastMode.rawValue)
fetchedNode[0].deviceConfig?.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber)
fetchedNode[0].deviceConfig?.doubleTapAsButtonPress = config.doubleTapAsButtonPress
fetchedNode[0].deviceConfig?.ledHeartbeatEnabled = !config.ledHeartbeatDisabled
fetchedNode[0].deviceConfig?.isManaged = config.isManaged
fetchedNode[0].deviceConfig?.tzdef = config.tzdef
}

View file

@ -924,7 +924,7 @@ struct AdminMessage {
extension AdminMessage.ConfigType: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [AdminMessage.ConfigType] = [
static let allCases: [AdminMessage.ConfigType] = [
.deviceConfig,
.positionConfig,
.powerConfig,
@ -937,7 +937,7 @@ extension AdminMessage.ConfigType: CaseIterable {
extension AdminMessage.ModuleConfigType: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [AdminMessage.ModuleConfigType] = [
static let allCases: [AdminMessage.ModuleConfigType] = [
.mqttConfig,
.serialConfig,
.extnotifConfig,

View file

@ -75,7 +75,15 @@ extension ChannelSet: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio
var _settings: [ChannelSettings] = []
var _loraConfig: Config.LoRaConfig? = nil
static let defaultInstance = _StorageClass()
#if swift(>=5.10)
// This property is used as the initial default value for new instances of the type.
// The type itself is protecting the reference to its storage via CoW semantics.
// This will force a copy to be made of this reference when the first mutation occurs;
// hence, it is safe to mark this as `nonisolated(unsafe)`.
static nonisolated(unsafe) let defaultInstance = _StorageClass()
#else
static let defaultInstance = _StorageClass()
#endif
private init() {}

View file

@ -136,7 +136,7 @@ enum Team: SwiftProtobuf.Enum {
extension Team: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Team] = [
static let allCases: [Team] = [
.unspecifedColor,
.white,
.yellow,
@ -239,7 +239,7 @@ enum MemberRole: SwiftProtobuf.Enum {
extension MemberRole: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [MemberRole] = [
static let allCases: [MemberRole] = [
.unspecifed,
.teamMember,
.teamLead,

View file

@ -120,6 +120,11 @@ struct ModuleSettings {
/// Bits of precision for the location sent in position packets.
var positionPrecision: UInt32 = 0
///
/// Controls whether or not the phone / clients should mute the current channel
/// Useful for noisy public channels you don't necessarily want to disable
var isClientMuted: Bool = false
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
@ -215,7 +220,7 @@ struct Channel {
extension Channel.Role: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Channel.Role] = [
static let allCases: [Channel.Role] = [
.disabled,
.primary,
.secondary,
@ -311,6 +316,7 @@ extension ModuleSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement
static let protoMessageName: String = _protobuf_package + ".ModuleSettings"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .standard(proto: "position_precision"),
2: .standard(proto: "is_client_muted"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -320,6 +326,7 @@ extension ModuleSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularUInt32Field(value: &self.positionPrecision) }()
case 2: try { try decoder.decodeSingularBoolField(value: &self.isClientMuted) }()
default: break
}
}
@ -329,11 +336,15 @@ extension ModuleSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement
if self.positionPrecision != 0 {
try visitor.visitSingularUInt32Field(value: self.positionPrecision, fieldNumber: 1)
}
if self.isClientMuted != false {
try visitor.visitSingularBoolField(value: self.isClientMuted, fieldNumber: 2)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: ModuleSettings, rhs: ModuleSettings) -> Bool {
if lhs.positionPrecision != rhs.positionPrecision {return false}
if lhs.isClientMuted != rhs.isClientMuted {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}

View file

@ -1386,7 +1386,7 @@ struct Config {
extension Config.DeviceConfig.Role: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Config.DeviceConfig.Role] = [
static let allCases: [Config.DeviceConfig.Role] = [
.client,
.clientMute,
.router,
@ -1403,7 +1403,7 @@ extension Config.DeviceConfig.Role: CaseIterable {
extension Config.DeviceConfig.RebroadcastMode: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Config.DeviceConfig.RebroadcastMode] = [
static let allCases: [Config.DeviceConfig.RebroadcastMode] = [
.all,
.allSkipDecoding,
.localOnly,
@ -1413,7 +1413,7 @@ extension Config.DeviceConfig.RebroadcastMode: CaseIterable {
extension Config.PositionConfig.PositionFlags: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Config.PositionConfig.PositionFlags] = [
static let allCases: [Config.PositionConfig.PositionFlags] = [
.unset,
.altitude,
.altitudeMsl,
@ -1430,7 +1430,7 @@ extension Config.PositionConfig.PositionFlags: CaseIterable {
extension Config.PositionConfig.GpsMode: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Config.PositionConfig.GpsMode] = [
static let allCases: [Config.PositionConfig.GpsMode] = [
.disabled,
.enabled,
.notPresent,
@ -1439,7 +1439,7 @@ extension Config.PositionConfig.GpsMode: CaseIterable {
extension Config.NetworkConfig.AddressMode: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Config.NetworkConfig.AddressMode] = [
static let allCases: [Config.NetworkConfig.AddressMode] = [
.dhcp,
.static,
]
@ -1447,7 +1447,7 @@ extension Config.NetworkConfig.AddressMode: CaseIterable {
extension Config.DisplayConfig.GpsCoordinateFormat: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [
static let allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [
.dec,
.dms,
.utm,
@ -1459,7 +1459,7 @@ extension Config.DisplayConfig.GpsCoordinateFormat: CaseIterable {
extension Config.DisplayConfig.DisplayUnits: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Config.DisplayConfig.DisplayUnits] = [
static let allCases: [Config.DisplayConfig.DisplayUnits] = [
.metric,
.imperial,
]
@ -1467,7 +1467,7 @@ extension Config.DisplayConfig.DisplayUnits: CaseIterable {
extension Config.DisplayConfig.OledType: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Config.DisplayConfig.OledType] = [
static let allCases: [Config.DisplayConfig.OledType] = [
.oledAuto,
.oledSsd1306,
.oledSh1106,
@ -1477,7 +1477,7 @@ extension Config.DisplayConfig.OledType: CaseIterable {
extension Config.DisplayConfig.DisplayMode: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Config.DisplayConfig.DisplayMode] = [
static let allCases: [Config.DisplayConfig.DisplayMode] = [
.default,
.twocolor,
.inverted,
@ -1487,7 +1487,7 @@ extension Config.DisplayConfig.DisplayMode: CaseIterable {
extension Config.LoRaConfig.RegionCode: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Config.LoRaConfig.RegionCode] = [
static let allCases: [Config.LoRaConfig.RegionCode] = [
.unset,
.us,
.eu433,
@ -1512,7 +1512,7 @@ extension Config.LoRaConfig.RegionCode: CaseIterable {
extension Config.LoRaConfig.ModemPreset: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Config.LoRaConfig.ModemPreset] = [
static let allCases: [Config.LoRaConfig.ModemPreset] = [
.longFast,
.longSlow,
.veryLongSlow,
@ -1526,7 +1526,7 @@ extension Config.LoRaConfig.ModemPreset: CaseIterable {
extension Config.BluetoothConfig.PairingMode: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Config.BluetoothConfig.PairingMode] = [
static let allCases: [Config.BluetoothConfig.PairingMode] = [
.randomPin,
.fixedPin,
.noPin,

View file

@ -66,7 +66,7 @@ enum ScreenFonts: SwiftProtobuf.Enum {
extension ScreenFonts: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [ScreenFonts] = [
static let allCases: [ScreenFonts] = [
.fontSmall,
.fontMedium,
.fontLarge,
@ -509,7 +509,15 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
var _hopsAway: UInt32 = 0
var _isFavorite: Bool = false
static let defaultInstance = _StorageClass()
#if swift(>=5.10)
// This property is used as the initial default value for new instances of the type.
// The type itself is protecting the reference to its storage via CoW semantics.
// This will force a copy to be made of this reference when the first mutation occurs;
// hence, it is safe to mark this as `nonisolated(unsafe)`.
static nonisolated(unsafe) let defaultInstance = _StorageClass()
#else
static let defaultInstance = _StorageClass()
#endif
private init() {}
@ -649,7 +657,15 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
var _nodeRemoteHardwarePins: [NodeRemoteHardwarePin] = []
var _nodeDbLite: [NodeInfoLite] = []
static let defaultInstance = _StorageClass()
#if swift(>=5.10)
// This property is used as the initial default value for new instances of the type.
// The type itself is protecting the reference to its storage via CoW semantics.
// This will force a copy to be made of this reference when the first mutation occurs;
// hence, it is safe to mark this as `nonisolated(unsafe)`.
static nonisolated(unsafe) let defaultInstance = _StorageClass()
#else
static let defaultInstance = _StorageClass()
#endif
private init() {}

View file

@ -314,7 +314,15 @@ extension LocalConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
var _bluetooth: Config.BluetoothConfig? = nil
var _version: UInt32 = 0
static let defaultInstance = _StorageClass()
#if swift(>=5.10)
// This property is used as the initial default value for new instances of the type.
// The type itself is protecting the reference to its storage via CoW semantics.
// This will force a copy to be made of this reference when the first mutation occurs;
// hence, it is safe to mark this as `nonisolated(unsafe)`.
static nonisolated(unsafe) let defaultInstance = _StorageClass()
#else
static let defaultInstance = _StorageClass()
#endif
private init() {}
@ -450,7 +458,15 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
var _paxcounter: ModuleConfig.PaxcounterConfig? = nil
var _version: UInt32 = 0
static let defaultInstance = _StorageClass()
#if swift(>=5.10)
// This property is used as the initial default value for new instances of the type.
// The type itself is protecting the reference to its storage via CoW semantics.
// This will force a copy to be made of this reference when the first mutation occurs;
// hence, it is safe to mark this as `nonisolated(unsafe)`.
static nonisolated(unsafe) let defaultInstance = _StorageClass()
#else
static let defaultInstance = _StorageClass()
#endif
private init() {}

View file

@ -110,6 +110,10 @@ enum HardwareModel: SwiftProtobuf.Enum {
/// LoRAType device: https://loratype.org/
case loraType // = 19
///
/// wiphone https://www.wiphone.io/
case wiphone // = 20
///
/// B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station
case stationG1 // = 25
@ -307,6 +311,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
case 17: self = .nanoG1Explorer
case 18: self = .nanoG2Ultra
case 19: self = .loraType
case 20: self = .wiphone
case 25: self = .stationG1
case 26: self = .rak11310
case 27: self = .senseloraRp2040
@ -372,6 +377,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
case .nanoG1Explorer: return 17
case .nanoG2Ultra: return 18
case .loraType: return 19
case .wiphone: return 20
case .stationG1: return 25
case .rak11310: return 26
case .senseloraRp2040: return 27
@ -421,7 +427,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
extension HardwareModel: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [HardwareModel] = [
static let allCases: [HardwareModel] = [
.unset,
.tloraV2,
.tloraV1,
@ -442,6 +448,7 @@ extension HardwareModel: CaseIterable {
.nanoG1Explorer,
.nanoG2Ultra,
.loraType,
.wiphone,
.stationG1,
.rak11310,
.senseloraRp2040,
@ -529,7 +536,7 @@ enum Constants: SwiftProtobuf.Enum {
extension Constants: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Constants] = [
static let allCases: [Constants] = [
.zero,
.dataPayloadLen,
]
@ -642,7 +649,7 @@ enum CriticalErrorCode: SwiftProtobuf.Enum {
extension CriticalErrorCode: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [CriticalErrorCode] = [
static let allCases: [CriticalErrorCode] = [
.none,
.txWatchdog,
.sleepEnterWait,
@ -961,7 +968,7 @@ struct Position {
extension Position.LocSource: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Position.LocSource] = [
static let allCases: [Position.LocSource] = [
.locUnset,
.locManual,
.locInternal,
@ -971,7 +978,7 @@ extension Position.LocSource: CaseIterable {
extension Position.AltSource: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Position.AltSource] = [
static let allCases: [Position.AltSource] = [
.altUnset,
.altManual,
.altInternal,
@ -1252,7 +1259,7 @@ struct Routing {
extension Routing.Error: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Routing.Error] = [
static let allCases: [Routing.Error] = [
.none,
.noRoute,
.gotNak,
@ -1770,7 +1777,7 @@ struct MeshPacket {
extension MeshPacket.Priority: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [MeshPacket.Priority] = [
static let allCases: [MeshPacket.Priority] = [
.unset,
.min,
.background,
@ -1783,7 +1790,7 @@ extension MeshPacket.Priority: CaseIterable {
extension MeshPacket.Delayed: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [MeshPacket.Delayed] = [
static let allCases: [MeshPacket.Delayed] = [
.noDelay,
.broadcast,
.direct,
@ -2037,7 +2044,7 @@ struct LogRecord {
extension LogRecord.Level: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [LogRecord.Level] = [
static let allCases: [LogRecord.Level] = [
.unset,
.critical,
.error,
@ -2741,6 +2748,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding {
17: .same(proto: "NANO_G1_EXPLORER"),
18: .same(proto: "NANO_G2_ULTRA"),
19: .same(proto: "LORA_TYPE"),
20: .same(proto: "WIPHONE"),
25: .same(proto: "STATION_G1"),
26: .same(proto: "RAK11310"),
27: .same(proto: "SENSELORA_RP2040"),
@ -2860,7 +2868,15 @@ extension Position: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
var _seqNumber: UInt32 = 0
var _precisionBits: UInt32 = 0
static let defaultInstance = _StorageClass()
#if swift(>=5.10)
// This property is used as the initial default value for new instances of the type.
// The type itself is protecting the reference to its storage via CoW semantics.
// This will force a copy to be made of this reference when the first mutation occurs;
// hence, it is safe to mark this as `nonisolated(unsafe)`.
static nonisolated(unsafe) let defaultInstance = _StorageClass()
#else
static let defaultInstance = _StorageClass()
#endif
private init() {}
@ -3522,7 +3538,15 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio
var _viaMqtt: Bool = false
var _hopStart: UInt32 = 0
static let defaultInstance = _StorageClass()
#if swift(>=5.10)
// This property is used as the initial default value for new instances of the type.
// The type itself is protecting the reference to its storage via CoW semantics.
// This will force a copy to be made of this reference when the first mutation occurs;
// hence, it is safe to mark this as `nonisolated(unsafe)`.
static nonisolated(unsafe) let defaultInstance = _StorageClass()
#else
static let defaultInstance = _StorageClass()
#endif
private init() {}
@ -3734,7 +3758,15 @@ extension NodeInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB
var _hopsAway: UInt32 = 0
var _isFavorite: Bool = false
static let defaultInstance = _StorageClass()
#if swift(>=5.10)
// This property is used as the initial default value for new instances of the type.
// The type itself is protecting the reference to its storage via CoW semantics.
// This will force a copy to be made of this reference when the first mutation occurs;
// hence, it is safe to mark this as `nonisolated(unsafe)`.
static nonisolated(unsafe) let defaultInstance = _StorageClass()
#else
static let defaultInstance = _StorageClass()
#endif
private init() {}
@ -4026,7 +4058,15 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
var _id: UInt32 = 0
var _payloadVariant: FromRadio.OneOf_PayloadVariant?
static let defaultInstance = _StorageClass()
#if swift(>=5.10)
// This property is used as the initial default value for new instances of the type.
// The type itself is protecting the reference to its storage via CoW semantics.
// This will force a copy to be made of this reference when the first mutation occurs;
// hence, it is safe to mark this as `nonisolated(unsafe)`.
static nonisolated(unsafe) let defaultInstance = _StorageClass()
#else
static let defaultInstance = _StorageClass()
#endif
private init() {}

View file

@ -64,7 +64,7 @@ enum RemoteHardwarePinType: SwiftProtobuf.Enum {
extension RemoteHardwarePinType: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [RemoteHardwarePinType] = [
static let allCases: [RemoteHardwarePinType] = [
.unknown,
.digitalRead,
.digitalWrite,
@ -1152,7 +1152,7 @@ struct ModuleConfig {
extension ModuleConfig.AudioConfig.Audio_Baud: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [
static let allCases: [ModuleConfig.AudioConfig.Audio_Baud] = [
.codec2Default,
.codec23200,
.codec22400,
@ -1167,7 +1167,7 @@ extension ModuleConfig.AudioConfig.Audio_Baud: CaseIterable {
extension ModuleConfig.SerialConfig.Serial_Baud: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [
static let allCases: [ModuleConfig.SerialConfig.Serial_Baud] = [
.baudDefault,
.baud110,
.baud300,
@ -1189,7 +1189,7 @@ extension ModuleConfig.SerialConfig.Serial_Baud: CaseIterable {
extension ModuleConfig.SerialConfig.Serial_Mode: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [
static let allCases: [ModuleConfig.SerialConfig.Serial_Mode] = [
.default,
.simple,
.proto,
@ -1201,7 +1201,7 @@ extension ModuleConfig.SerialConfig.Serial_Mode: CaseIterable {
extension ModuleConfig.CannedMessageConfig.InputEventChar: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [
static let allCases: [ModuleConfig.CannedMessageConfig.InputEventChar] = [
.none,
.up,
.down,

View file

@ -277,7 +277,7 @@ enum PortNum: SwiftProtobuf.Enum {
extension PortNum: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [PortNum] = [
static let allCases: [PortNum] = [
.unknownApp,
.textMessageApp,
.remoteHardwareApp,

View file

@ -119,7 +119,7 @@ struct HardwareMessage {
extension HardwareMessage.TypeEnum: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [HardwareMessage.TypeEnum] = [
static let allCases: [HardwareMessage.TypeEnum] = [
.unset,
.writeGpios,
.watchGpios,

View file

@ -344,7 +344,7 @@ struct StoreAndForward {
extension StoreAndForward.RequestResponse: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [StoreAndForward.RequestResponse] = [
static let allCases: [StoreAndForward.RequestResponse] = [
.unset,
.routerError,
.routerHeartbeat,

View file

@ -150,7 +150,7 @@ enum TelemetrySensorType: SwiftProtobuf.Enum {
extension TelemetrySensorType: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [TelemetrySensorType] = [
static let allCases: [TelemetrySensorType] = [
.sensorUnset,
.bme280,
.bme680,

View file

@ -88,7 +88,7 @@ struct XModem {
extension XModem.Control: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [XModem.Control] = [
static let allCases: [XModem.Control] = [
.nul,
.soh,
.stx,

View file

@ -266,6 +266,18 @@ struct Connect: View {
.controlSize(.large)
.padding()
}
if bleManager.isConnecting {
Button(role: .destructive, action: {
bleManager.cancelPeripheralConnection()
}) {
Label("disconnect", systemImage: "antenna.radiowaves.left.and.right.slash")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
}
#endif
Spacer()
}

View file

@ -29,10 +29,8 @@ struct CircleText: View {
struct CircleText_Previews: PreviewProvider {
static var previews: some View {
HStack {
VStack {
VStack {
HStack {
CircleText(text: "N1", color: Color.yellow, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "8", color: Color.purple, circleSize: 80)
@ -41,17 +39,20 @@ struct CircleText_Previews: PreviewProvider {
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "🍔", color: Color.brown, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
}
HStack {
CircleText(text: "👻", color: Color.orange, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "🤙", color: Color.orange, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
}
VStack {
CircleText(text: "69", color: Color.green, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "WWWW", color: Color.cyan, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
}
HStack {
CircleText(text: "CW-A", color: Color.secondary)
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "CW-A", color: Color.secondary, circleSize: 80)
@ -60,7 +61,20 @@ struct CircleText_Previews: PreviewProvider {
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "IIII", color: Color.accentColor, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "LCP", color: Color.primary, circleSize: 80)
}
HStack {
CircleText(text: "🚗", color: Color.orange)
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "🔋", color: Color.indigo, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "🛢️", color: Color.orange, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "LCP", color: Color.indigo, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
}
HStack {
CircleText(text: "🤡", color: Color.red, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
}
}

View file

@ -1,125 +0,0 @@
//
// IndoorAirQuality.swift
// Meshtastic
//
// Copyright(c) by Garth Vander Houwen on 4/10/24.
//
import Foundation
import SwiftUI
enum IaqDisplayMode: Int, CaseIterable, Identifiable {
case pill = 0
case dot = 1
case text = 2
case gauge = 3
var id: Int { self.rawValue }
}
struct IndoorAirQuality: View {
var iaq: Int = 0
var displayMode: IaqDisplayMode = .pill
let gradient = Gradient(colors: [.green, .mint, .yellow, .orange, .red, .purple, .purple, .brown, .brown, .brown, .brown])
var body: some View {
let iaqEnum = Iaq.getIaq(for: iaq)
switch displayMode {
case .pill:
ZStack (alignment: .leading) {
RoundedRectangle(cornerRadius: 10)
.fill(iaqEnum.color)
.frame(width: 125, height: 30)
Label("IAQ \(iaq)", systemImage: iaq < 100 ? "aqi.low" : ((iaq > 100 && iaq < 201) ? "aqi.medium" : "aqi.high"))
.padding(.leading, 4)
}
case .dot:
VStack {
HStack {
Text("\(iaq)")
Circle()
.fill(iaqEnum.color)
.frame(width: 10, height: 10)
}
}
case .text:
Text(iaqEnum.description)
.font(.caption)
case .gauge:
Gauge(value: Double(iaq), in: 0...500) {
Text("IAQ")
.foregroundColor(iaqEnum.color)
} currentValueLabel: {
Text("\(Int(iaq))")
}
.tint(gradient)
.gaugeStyle(.accessoryCircular)
}
}
}
struct IndoorAirQuality_Previews: PreviewProvider {
static var previews: some View {
VStack {
Text(".pill")
.font(.title)
HStack {
VStack{
IndoorAirQuality(iaq: 6)
IndoorAirQuality(iaq: 51)
IndoorAirQuality(iaq: 101)
}
VStack {
IndoorAirQuality(iaq: 201)
IndoorAirQuality(iaq: 350)
IndoorAirQuality(iaq: 351)
}
}
Text(".dot")
.font(.title)
HStack {
VStack (alignment: .leading) {
IndoorAirQuality(iaq: 6, displayMode: .dot)
IndoorAirQuality(iaq: 51, displayMode: .dot)
IndoorAirQuality(iaq: 101, displayMode: .dot)
}
VStack (alignment: .leading) {
IndoorAirQuality(iaq: 201, displayMode: .dot)
IndoorAirQuality(iaq: 350, displayMode: .dot)
IndoorAirQuality(iaq: 351, displayMode: .dot)
}
}
Text(".text")
.font(.title)
IndoorAirQuality(iaq: 6, displayMode: .text)
IndoorAirQuality(iaq: 51, displayMode: .text)
IndoorAirQuality(iaq: 101, displayMode: .text)
IndoorAirQuality(iaq: 201, displayMode: .text)
IndoorAirQuality(iaq: 350, displayMode: .text)
IndoorAirQuality(iaq: 351, displayMode: .text)
Text(".gauge")
.font(.title)
HStack (alignment: .top) {
VStack{
IndoorAirQuality(iaq: 6, displayMode: .gauge)
IndoorAirQuality(iaq: 51, displayMode: .gauge)
IndoorAirQuality(iaq: 101, displayMode: .gauge)
IndoorAirQuality(iaq: 151, displayMode: .gauge)
}
VStack{
IndoorAirQuality(iaq: 201, displayMode: .gauge)
IndoorAirQuality(iaq: 251, displayMode: .gauge)
IndoorAirQuality(iaq: 301, displayMode: .gauge)
IndoorAirQuality(iaq: 350, displayMode: .gauge)
}
VStack{
IndoorAirQuality(iaq: 351, displayMode: .gauge)
IndoorAirQuality(iaq: 401, displayMode: .gauge)
IndoorAirQuality(iaq: 500, displayMode: .gauge)
}
}
}.previewLayout(.fixed(width: 300, height: 800))
}
}

View file

@ -0,0 +1,146 @@
//
// AQICircleDisplay.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 2/4/23.
//
import SwiftUI
enum AqiDisplayMode: Int, CaseIterable, Identifiable {
case pill = 0
case dot = 1
case text = 2
case gauge = 3
case gradient = 4
var id: Int { self.rawValue }
}
struct AirQualityIndex: View {
var aqi: Int
var displayMode: IaqDisplayMode = .pill
let gradient = Gradient(colors: [.green, .yellow, .orange, .red, .purple, .magenta])
var body: some View {
let aqiEnum = Aqi.getAqi(for: aqi)
switch displayMode {
case .pill:
ZStack (alignment: .leading) {
RoundedRectangle(cornerRadius: 10)
.fill(aqiEnum.color)
.frame(width: 125, height: 30)
Label("IAQ \(aqi)", systemImage: aqi < 100 ? "aqi.low" : ((aqi > 100 && aqi < 201) ? "aqi.medium" : "aqi.high"))
.padding(.leading, 4)
}
case .dot:
VStack {
HStack {
Text("\(aqi)")
Circle()
.fill(aqiEnum.color)
.frame(width: 10, height: 10)
}
}
case .text:
Text(aqiEnum.description)
.font(.caption)
case .gauge:
Gauge(value: Double(aqi), in: 0...500) {
Text("IAQ")
.foregroundColor(aqiEnum.color)
} currentValueLabel: {
Text("\(Int(aqi))")
}
.tint(gradient)
.gaugeStyle(.accessoryCircular)
case .gradient:
HStack {
Gauge(value: Double(aqi), in: 0...500) {
Text("IAQ")
.foregroundColor(aqiEnum.color)
} currentValueLabel: {
Text("IAQ ")+Text("\(Int(aqi))")
.foregroundColor(.gray)
}
.tint(gradient)
.gaugeStyle(.accessoryLinear)
Text(aqiEnum.description)
.font(.caption)
}
.padding([.leading, .trailing])
}
}
}
struct AirQualityIndex_Previews: PreviewProvider {
static var previews: some View {
VStack {
Text(".pill")
.font(.title2)
HStack {
AirQualityIndex(aqi: 6)
AirQualityIndex(aqi: 51)
}
HStack {
AirQualityIndex(aqi: 101)
AirQualityIndex(aqi: 151)
}
HStack {
AirQualityIndex(aqi: 201)
AirQualityIndex(aqi: 351)
}
Text(".dot")
.font(.title2)
HStack {
AirQualityIndex(aqi: 6, displayMode: .dot)
AirQualityIndex(aqi: 51, displayMode: .dot)
AirQualityIndex(aqi: 101, displayMode: .dot)
AirQualityIndex(aqi: 201, displayMode: .dot)
AirQualityIndex(aqi: 350, displayMode: .dot)
AirQualityIndex(aqi: 351, displayMode: .dot)
}
Text(".text")
.font(.title2)
HStack {
AirQualityIndex(aqi: 6, displayMode: .text)
AirQualityIndex(aqi: 51, displayMode: .text)
AirQualityIndex(aqi: 101, displayMode: .text)
}
HStack {
AirQualityIndex(aqi: 201, displayMode: .text)
AirQualityIndex(aqi: 350, displayMode: .text)
}
Text(".gauge")
.font(.title2)
HStack (alignment: .top) {
AirQualityIndex(aqi: 6, displayMode: .gauge)
AirQualityIndex(aqi: 51, displayMode: .gauge)
AirQualityIndex(aqi: 101, displayMode: .gauge)
AirQualityIndex(aqi: 151, displayMode: .gauge)
}
HStack (alignment: .top) {
AirQualityIndex(aqi: 201, displayMode: .gauge)
AirQualityIndex(aqi: 251, displayMode: .gauge)
AirQualityIndex(aqi: 301, displayMode: .gauge)
AirQualityIndex(aqi: 351, displayMode: .gauge)
}
HStack (alignment: .top) {
AirQualityIndex(aqi: 401, displayMode: .gauge)
AirQualityIndex(aqi: 500, displayMode: .gauge)
}
Text(".gradient")
.font(.title2)
AirQualityIndex(aqi: 6, displayMode: .gradient)
AirQualityIndex(aqi: 51, displayMode: .gradient)
AirQualityIndex(aqi: 101, displayMode: .gradient)
AirQualityIndex(aqi: 201, displayMode: .gradient)
AirQualityIndex(aqi: 351, displayMode: .gradient)
AirQualityIndex(aqi: 401, displayMode: .gradient)
AirQualityIndex(aqi: 500, displayMode: .gradient)
}.previewLayout(.fixed(width: 300, height: 800))
}
}

View file

@ -1,67 +0,0 @@
//
// AQICircleDisplay.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 2/4/23.
//
import SwiftUI
struct AirQualityIndexCompact: View {
var aqi: Int
var body: some View {
HStack(spacing: 0.5) {
Text("AQI \(aqi)")
.foregroundColor(.gray)
.padding(.trailing, 0)
.font(.caption)
if aqi > 0 && aqi < 51 {
// Good
Circle()
.fill(.green)
.frame(width: 10, height: 10)
} else if aqi > 50 && aqi < 101 {
// Satisfactory
Circle()
.fill(Color(red: 0, green: 0.9882, blue: 0.1804))
.frame(width: 10, height: 10)
} else if aqi > 100 && aqi < 201 {
// Moderate
Circle()
.fill(.yellow)
.frame(width: 10, height: 10)
} else if aqi > 200 && aqi < 301 {
// Poor
Circle()
.fill(.orange)
.frame(width: 10, height: 10)
} else if aqi > 300 && aqi < 401 {
// Very Poor
Circle()
.fill(.red)
.frame(width: 10, height: 10)
} else if aqi >= 401 {
// Very Poor
Circle()
.fill(Color(red: 0.8392, green: 0.0667, blue: 0))
.frame(width: 10, height: 10)
}
}
}
}
struct AQICircleDisplay_Previews: PreviewProvider {
static var previews: some View {
VStack {
AirQualityIndexCompact(aqi: 5)
AirQualityIndexCompact(aqi: 51)
AirQualityIndexCompact(aqi: 101)
AirQualityIndexCompact(aqi: 201)
AirQualityIndexCompact(aqi: 301)
AirQualityIndexCompact(aqi: 401)
}
}
}

View file

@ -0,0 +1,39 @@
//
// IAQScale.swift
// Meshtastic
//
// Created by Garth Vander Houwen on 4/24/24.
//
import SwiftUI
struct IAQScale: View {
var body: some View {
VStack(alignment:.leading) {
ForEach(Iaq.allCases) { iaq in
HStack {
RoundedRectangle(cornerRadius: 5)
.fill(iaq.color)
.frame(width: 30, height: 20)
Text(iaq.description)
.font(.callout)
}
}
}
.padding()
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
// .overlay(
// RoundedRectangle(cornerRadius: 20)
// .stroke(.secondary, lineWidth: 5)
// )
}
}
struct IAQSCalePreviews: PreviewProvider {
static var previews: some View {
VStack {
IAQScale()
}
}
}

View file

@ -0,0 +1,148 @@
//
// IndoorAirQuality.swift
// Meshtastic
//
// Copyright(c) by Garth Vander Houwen on 4/10/24.
//
import Foundation
import SwiftUI
enum IaqDisplayMode: Int, CaseIterable, Identifiable {
case pill = 0
case dot = 1
case text = 2
case gauge = 3
case gradient = 4
var id: Int { self.rawValue }
}
struct IndoorAirQuality: View {
var iaq: Int = 0
var displayMode: IaqDisplayMode = .pill
let gradient = Gradient(colors: [.green, .mint, .yellow, .orange, .red, .purple, .purple, .brown, .brown, .brown, .brown])
var body: some View {
let iaqEnum = Iaq.getIaq(for: iaq)
switch displayMode {
case .pill:
ZStack (alignment: .leading) {
RoundedRectangle(cornerRadius: 10)
.fill(iaqEnum.color)
.frame(width: 125, height: 30)
Label("IAQ \(iaq)", systemImage: iaq < 100 ? "aqi.low" : ((iaq > 100 && iaq < 201) ? "aqi.medium" : "aqi.high"))
.padding(.leading, 4)
}
case .dot:
VStack {
HStack {
Text("\(iaq)")
Circle()
.fill(iaqEnum.color)
.frame(width: 10, height: 10)
}
}
case .text:
Text(iaqEnum.description)
.font(.caption)
case .gauge:
Gauge(value: Double(iaq), in: 0...500) {
Text("IAQ")
.foregroundColor(iaqEnum.color)
} currentValueLabel: {
Text("\(Int(iaq))")
}
.tint(gradient)
.gaugeStyle(.accessoryCircular)
case .gradient:
HStack {
Gauge(value: Double(iaq), in: 0...500) {
Text("IAQ")
.foregroundColor(iaqEnum.color)
} currentValueLabel: {
Text("IAQ ")+Text("\(Int(iaq))")
.foregroundColor(.gray)
}
.tint(gradient)
.gaugeStyle(.accessoryLinear)
Text(iaqEnum.description)
.font(.caption)
}
.padding([.leading, .trailing])
}
}
}
struct IndoorAirQuality_Previews: PreviewProvider {
static var previews: some View {
VStack {
Text(".pill")
.font(.title2)
HStack {
IndoorAirQuality(iaq: 6)
IndoorAirQuality(iaq: 51)
}
HStack {
IndoorAirQuality(iaq: 101)
IndoorAirQuality(iaq: 201)
}
HStack {
IndoorAirQuality(iaq: 350)
IndoorAirQuality(iaq: 351)
}
Text(".dot")
.font(.title2)
HStack {
IndoorAirQuality(iaq: 6, displayMode: .dot)
IndoorAirQuality(iaq: 51, displayMode: .dot)
IndoorAirQuality(iaq: 101, displayMode: .dot)
IndoorAirQuality(iaq: 201, displayMode: .dot)
IndoorAirQuality(iaq: 350, displayMode: .dot)
IndoorAirQuality(iaq: 351, displayMode: .dot)
}
Text(".text")
.font(.title2)
HStack {
IndoorAirQuality(iaq: 6, displayMode: .text)
IndoorAirQuality(iaq: 51, displayMode: .text)
IndoorAirQuality(iaq: 101, displayMode: .text)
}
HStack {
IndoorAirQuality(iaq: 201, displayMode: .text)
IndoorAirQuality(iaq: 350, displayMode: .text)
IndoorAirQuality(iaq: 351, displayMode: .text)
}
Text(".gauge")
.font(.title2)
HStack (alignment: .top) {
IndoorAirQuality(iaq: 6, displayMode: .gauge)
IndoorAirQuality(iaq: 51, displayMode: .gauge)
IndoorAirQuality(iaq: 101, displayMode: .gauge)
IndoorAirQuality(iaq: 151, displayMode: .gauge)
}
HStack (alignment: .top) {
IndoorAirQuality(iaq: 201, displayMode: .gauge)
IndoorAirQuality(iaq: 251, displayMode: .gauge)
IndoorAirQuality(iaq: 301, displayMode: .gauge)
IndoorAirQuality(iaq: 351, displayMode: .gauge)
}
HStack (alignment: .top) {
IndoorAirQuality(iaq: 401, displayMode: .gauge)
IndoorAirQuality(iaq: 500, displayMode: .gauge)
}
Text(".gradient")
.font(.title2)
IndoorAirQuality(iaq: 6, displayMode: .gradient)
IndoorAirQuality(iaq: 51, displayMode: .gradient)
IndoorAirQuality(iaq: 101, displayMode: .gradient)
IndoorAirQuality(iaq: 201, displayMode: .gradient)
IndoorAirQuality(iaq: 351, displayMode: .gradient)
IndoorAirQuality(iaq: 401, displayMode: .gradient)
IndoorAirQuality(iaq: 500, displayMode: .gradient)
}.previewLayout(.fixed(width: 300, height: 800))
}
}

View file

@ -109,6 +109,24 @@ struct ChannelList: View {
Label("Delete Messages", systemImage: "trash")
}
}
Button {
channel.mute = !channel.mute
do {
let adminMessageId = bleManager.saveChannel(channel: channel.protoBuf, fromUser: node!.user!, toUser: node!.user!)
if adminMessageId > 0 {
context.refresh(channel, mergeChanges: true)
}
try context.save()
} catch {
context.rollback()
print("💥 Save Channel Mute Error")
}
} label: {
Label(channel.mute ? "Show Alerts" : "Hide Alerts", systemImage: channel.mute ? "bell" : "bell.slash")
}
}
.confirmationDialog(
"This conversation will be deleted.",

View file

@ -43,16 +43,24 @@ struct ChannelMessageList: View {
.padding(.trailing)
}
}
HStack(alignment: .top) {
HStack(alignment: .bottom) {
if currentUser { Spacer(minLength: 50) }
if !currentUser {
CircleText(text: message.fromUser?.shortName ?? "?", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 44)
.padding(.all, 5)
.offset(y: -5)
.offset(y: -7)
}
VStack(alignment: currentUser ? .trailing : .leading) {
let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue)
if !currentUser && message.fromUser != nil {
Text("\(message.fromUser?.longName ?? "unknown".localized ) (\(message.fromUser?.userId ?? "?"))")
.font(.caption)
.foregroundColor(.gray)
.offset(y: 8)
}
HStack {
MessageText(
message: message,
@ -67,7 +75,7 @@ struct ChannelMessageList: View {
RetryButton(message: message, destination: .channel(channel))
}
}
TapbackResponses(message: message) {
appState.unreadChannelMessages = myInfo.unreadMessages
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages

View file

@ -32,7 +32,8 @@ struct NodeInfoItem: View {
.frame(width: 75, height: 75)
.cornerRadius(5)
Text(String(node.user!.hwModel ?? "unset".localized))
.font(.caption2).fixedSize()
.font(.caption2)
.frame(maxWidth: 125)
}
}
if node.snr != 0 && !node.viaMqtt {

View file

@ -308,18 +308,52 @@ func firstMissingChannelIndex(_ indexes: [Int]) -> Int {
enum PositionPrecision: Int, CaseIterable, Identifiable {
case two = 2
case three = 3
case four = 4
case five = 5
case six = 6
case seven = 7
case eight = 8
case nine = 9
case ten = 10
case eleven = 11
case twelve = 12
case thirteen = 13
case fourteen = 14
case fifteen = 15
case sixteen = 16
case seventeen = 17
case eightteen = 18
case nineteen = 19
case twenty = 20
case twentyone = 21
case twentytwo = 22
case twentythree = 23
case twentyfour = 24
var id: Int { self.rawValue }
var precisionMeters: Double {
switch self {
case .two:
return 5976446.981252
case .three:
return 2988223.4850600003
case .four:
return 1494111.7369640006
case .five:
return 747055.8629159998
case .six:
return 373527.9258920002
case .seven:
return 186763.95738000044
case .eight:
return 93381.97312400135
case .nine:
return 46690.98099600022
case .ten:
return 23345.48493200123
case .eleven:
return 11672.736900000944
case .twelve:
@ -332,25 +366,27 @@ enum PositionPrecision: Int, CaseIterable, Identifiable {
return 729.5356200010741
case .sixteen:
return 364.7622440000765
case .seventeen:
return 182.37555600115968
case .eightteen:
return 91.1822120001193
case .nineteen:
return 45.58554000039009
case .twenty:
return 22.787204001316468
case .twentyone:
return 11.388036000988677
case .twentytwo:
return 5.688452000824781
case .twentythree:
return 2.8386600007428338
case .twentyfour:
return 1.413763999910884
}
}
var description: String {
let distanceFormatter = MKDistanceFormatter()
switch self {
case .eleven:
return String.localizedStringWithFormat("position.precision %@".localized, String(distanceFormatter.string(fromDistance: precisionMeters)))
case .twelve:
return String.localizedStringWithFormat("position.precision %@".localized, String(distanceFormatter.string(fromDistance: precisionMeters)))
case .thirteen:
return String.localizedStringWithFormat("position.precision %@".localized, String(distanceFormatter.string(fromDistance: precisionMeters)))
case .fourteen:
return String.localizedStringWithFormat("position.precision %@".localized, String(distanceFormatter.string(fromDistance: precisionMeters)))
case .fifteen:
return String.localizedStringWithFormat("position.precision %@".localized, String(distanceFormatter.string(fromDistance: precisionMeters)))
case .sixteen:
return String.localizedStringWithFormat("position.precision %@".localized, String(distanceFormatter.string(fromDistance: precisionMeters)))
}
return String.localizedStringWithFormat("position.precision %@".localized, String(distanceFormatter.string(fromDistance: precisionMeters)))
}
}

View file

@ -160,7 +160,7 @@ struct ChannelForm: View {
if !preciseLocation {
VStack(alignment: .leading) {
Label("Approximate Location", systemImage: "location.slash.circle.fill")
Slider(value: $positionPrecision, in: 11...16, step: 1) {
Slider(value: $positionPrecision, in: 11...18, step: 1) {
} minimumValueLabel: {
Image(systemName: "minus")
} maximumValueLabel: {

View file

@ -25,6 +25,7 @@ struct DeviceConfig: View {
@State var rebroadcastMode = 0
@State var nodeInfoBroadcastSecs = 10800
@State var doubleTapAsButtonPress = false
@State var ledHeartbeatEnabled = true
@State var isManaged = false
@State var tzdef = ""
@ -58,6 +59,12 @@ struct DeviceConfig: View {
}
.pickerStyle(DefaultPickerStyle())
Toggle(isOn: $isManaged) {
Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath")
Text("Enabling Managed mode will restrict access to all radio configurations, such as short/long names, regions, channels, modules, etc. and will only be accessible through the Admin channel. To avoid being locked out, make sure the Admin channel is working properly before enabling it.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Picker("Node Info Broadcast Interval", selection: $nodeInfoBroadcastSecs ) {
ForEach(UpdateIntervals.allCases) { ui in
if ui.rawValue >= 3600 {
@ -66,15 +73,18 @@ struct DeviceConfig: View {
}
}
.pickerStyle(DefaultPickerStyle())
}
Section(header: Text("Hardware")) {
Toggle(isOn: $doubleTapAsButtonPress) {
Label("Double Tap as Button", systemImage: "hand.tap")
Text("Treat double tap on supported accelerometers as a user button press.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $isManaged) {
Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath")
Text("Enabling Managed mode will restrict access to all radio configurations, such as short/long names, regions, channels, modules, etc. and will only be accessible through the Admin channel. To avoid being locked out, make sure the Admin channel is working properly before enabling it.")
Toggle(isOn: $ledHeartbeatEnabled) {
Label("LED Heartbeat", systemImage: "waveform.path.ecg")
Text("Controls the blinking LED on the device. For most devices this will control one of the up to 4 LEDS, the charger and GPS LEDs are not controllable.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
@ -202,6 +212,7 @@ struct DeviceConfig: View {
dc.doubleTapAsButtonPress = doubleTapAsButtonPress
dc.isManaged = isManaged
dc.tzdef = tzdef
dc.ledHeartbeatDisabled = !ledHeartbeatEnabled
if isManaged {
serialEnabled = false
debugLogEnabled = false
@ -300,7 +311,9 @@ struct DeviceConfig: View {
nodeInfoBroadcastSecs = 3600
}
self.doubleTapAsButtonPress = node?.deviceConfig?.doubleTapAsButtonPress ?? false
self.ledHeartbeatEnabled = node?.deviceConfig?.ledHeartbeatEnabled ?? true
self.isManaged = node?.deviceConfig?.isManaged ?? false
self.tzdef = node?.deviceConfig?.tzdef ?? ""
if self.tzdef.isEmpty {
self.tzdef = TimeZone.current.posixDescription
self.hasChanges = true

View file

@ -398,21 +398,20 @@ struct MQTTConfig: View {
if !stateTopic.isEmpty {
nearbyTopics.append(stateTopic)
}
let countyTopic = defaultTopic + "/" + (placemark.subAdministrativeArea?.lowercased().replacingOccurrences(of: " ", with: "") ?? "")
let countyTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subAdministrativeArea?.lowercased().replacingOccurrences(of: " ", with: "") ?? "")
if !countyTopic.isEmpty {
nearbyTopics.append(countyTopic)
}
let cityTopic = defaultTopic + "/" + (placemark.locality?.lowercased().replacingOccurrences(of: " ", with: "") ?? "")
let cityTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.locality?.lowercased().replacingOccurrences(of: " ", with: "") ?? "")
if !cityTopic.isEmpty {
nearbyTopics.append(cityTopic)
}
let neightborhoodTopic = defaultTopic + "/" + (placemark.subLocality?.lowercased()
let neightborhoodTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subLocality?.lowercased()
.replacingOccurrences(of: " ", with: "")
.replacingOccurrences(of: "'", with: "") ?? "")
if !neightborhoodTopic.isEmpty {
nearbyTopics.append(neightborhoodTopic)
}
}
else
{

View file

@ -146,8 +146,12 @@ struct PositionConfig: View {
.pickerStyle(SegmentedPickerStyle())
.padding(.top, 5)
.padding(.bottom, 5)
if gpsMode == 1 {
Text("Positions will be provided by your device GPS, if you select disabled or not present you can set a fixed position.")
.foregroundColor(.gray)
.font(.callout)
VStack(alignment: .leading) {
Picker("Update Interval", selection: $gpsUpdateInterval) {
ForEach(GpsUpdateIntervals.allCases) { ui in
@ -163,7 +167,11 @@ struct PositionConfig: View {
VStack(alignment: .leading) {
Toggle(isOn: $fixedPosition) {
Label("Fixed Position", systemImage: "location.square.fill")
Text("If enabled your current phone location and time will be sent to the device and will broadcast over the mesh on the position interval.")
if !(node?.positionConfig?.fixedPosition ?? false) {
Text("Your current location will be set as the fixed position and broadcast over the mesh on the position interval.")
} else {
}
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
@ -280,6 +288,9 @@ struct PositionConfig: View {
print("Set Position Failed")
}
print("Remove a fixed position here")
let mutablePositions = node?.positions?.mutableCopy() as? NSMutableOrderedSet
mutablePositions?.removeAllObjects()
node?.positions = mutablePositions
node?.positionConfig?.fixedPosition = false
do {
try context.save()
@ -295,7 +306,6 @@ struct PositionConfig: View {
if !bleManager.setFixedPosition(fromUser: node!.user!, channel: 0) {
print("Set Position Failed")
}
print("Set a fixed position")
node?.positionConfig?.fixedPosition = true
do {
try context.save()
@ -374,14 +384,10 @@ struct PositionConfig: View {
}
}
.onChange(of: fixedPosition) { newFixed in
print("Changing Fixed Position Value")
if supportedVersion {
if node != nil && node!.positionConfig != nil {
print("We have a node and position config")
print("We have turned on fixed position \(!node!.positionConfig!.fixedPosition && newFixed)")
/// Fixed Position is off to start
if !node!.positionConfig!.fixedPosition && newFixed {
print("fire alert")
showingSetFixedAlert = true
} else if node!.positionConfig!.fixedPosition && !newFixed {
/// Fixed Position is on to start

View file

@ -12,7 +12,7 @@ struct Firmware: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
var node: NodeInfoEntity?
@State var minimumVersion = "2.3.5"
@State var minimumVersion = "2.3.8"
@State var version = ""
@State private var currentDevice: DeviceHardware?
@State private var latestStable: FirmwareRelease?
@ -20,7 +20,7 @@ struct Firmware: View {
var body: some View {
let supportedVersion = bleManager.connectedVersion == "0.0.0" || self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame
let supportedVersion = self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame
ScrollView {
VStack(alignment: .leading) {
let deviceString = currentDevice?.hwModelSlug.replacingOccurrences(of: "_", with: "")
@ -57,12 +57,12 @@ struct Firmware: View {
.font(.title3)
.padding(.bottom)
} else {
Text("Your Firmware is out of date")
Text("Newer firmware is available")
.fixedSize(horizontal: false, vertical: true)
.foregroundStyle(.red)
.font(.title2)
.padding(.bottom)
Text("Current Firmware Version: \(bleManager.connectedVersion), Minimium Firmware Version: \(minimumVersion)")
Text("Current Firmware Version: \(bleManager.connectedVersion), Latest Firmware Version: \(minimumVersion)")
.fixedSize(horizontal: false, vertical: true)
.font(.title3)
.padding(.bottom)

View file

@ -51,7 +51,7 @@ struct UserConfig: View {
.onChange(of: longName, perform: { _ in
let totalBytes = longName.utf8.count
// Only mess with the value if it is too big
if totalBytes > (isLicensed ? 6 : 36) {
if totalBytes > (isLicensed ? 8 : 36) {
longName = String(longName.dropLast())
}
})

View file

@ -6,6 +6,16 @@
<string>Root</string>
<key>PreferenceSpecifiers</key>
<array>
<dict>
<key>Type</key>
<string>PSTitleValueSpecifier</string>
<key>DefaultValue</key>
<string></string>
<key>Title</key>
<string>Will share your phone GPS location with your node.</string>
<key>Key</key>
<string>shareLocationTitle</string>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
@ -20,7 +30,7 @@
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>Share Location Interval</string>
<string>Interval</string>
<key>Key</key>
<string>provideLocationInterval</string>
<key>Values</key>
@ -78,9 +88,9 @@
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Low Battery</string>
<string>Channel Messages</string>
<key>Key</key>
<string>lowBatteryNotifications</string>
<string>channelMessageNotifications</string>
<key>DefaultValue</key>
<true/>
</dict>
@ -94,6 +104,16 @@
<key>DefaultValue</key>
<true/>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Low Battery</string>
<key>Key</key>
<string>lowBatteryNotifications</string>
<key>DefaultValue</key>
<true/>
</dict>
</array>
</dict>
</plist>

@ -1 +1 @@
Subproject commit 86640f20db7b9b5be42949d18e8d96ad10d47a68
Subproject commit dd7d64cc038a6365c119ec7508762cc45f405948

View file

@ -7,19 +7,19 @@
*/
"about"="关于";
"about.meshtastic"="关于 Meshtastic";
"activity"="Activity";
"activity"="活动";
"admin"="管理员";
"admin.log"="管理员消息日志";
"ago"="ago";
"ago"="之前";
"airtime"="广播时间";
"always.on"="常亮";
"ambient.lighting"="Ambient Lighting";
"ambient.lighting.config"="Ambient Lighting Config";
"ambient.lighting"="氛围灯";
"ambient.lighting.config"="氛围灯配置";
"appsettings"="通用设置";
"appsettings.provide.location"="提供定位到 Mesh 网络";
"appsettings.smartposition"="Smart Position";
"appsettings.smartposition"="智能定位";
"are.you.sure"="是否确认?";
"ascii.capable"="ASCII Capable";
"ascii.capable"="ASCII 兼容";
"available.radios"="可以连接的电台";
"automatic.detection"="自动识别";
"battery.level"="电池电量";
@ -52,28 +52,28 @@
"clear.app.data"="清除 App 数据";
"clear.log"="清除日志";
"close"="关闭";
"config.power.settings"="Power";
"config.power.title"="Power Config";
"config.power.section.battery"="Battery";
"config.power.section.sleep"="Sleep";
"config.power.adc.override"="ADC Override";
"config.power.adc.multiplier"="Multiplier";
"config.power.ls.secs"="Light Sleep Interval";
"config.power.min.wake.secs"="Minimum Wake Interval";
"config.power.saving"="Power Saving";
"config.power.saving.description"="Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. Don't use this setting if you want to use your device with the phone apps or are using a device without a user button.";
"config.power.shutdown.on.power.loss"="Shutdown on Power Loss";
"config.power.settings"="电源";
"config.power.title"="电源配置";
"config.power.section.battery"="电池";
"config.power.section.sleep"="休眠";
"config.power.adc.override"="ADC 修改";
"config.power.adc.multiplier"="ADC 放大";
"config.power.ls.secs"="轻度睡眠间隔";
"config.power.min.wake.secs"="最小唤醒间隔";
"config.power.saving"="省电模式";
"config.power.saving.description"="尽可能将所有部件都置于休眠状态,对于跟踪器和传感器功能,这还包括 LoRa 无线电。如果您要使用手机应用程序或者使用没有用户按钮的设备,请不要使用这个设置。";
"config.power.shutdown.on.power.loss"="断电时关机";
"config.power.shutdown.after.secs"="After";
"config.power.wait.bluetooth.secs"="Bluetooth Off After";
"config.ringtone"="RTTTL Ringtone";
"config.ringtone.title"="Ringtone Config";
"config.ringtone.label"="Ringtone Transfer Language";
"config.ringtone.description"="Ringtone Transfer Language(RTTTL) Ringtone String used by supported buzzers in external notifications.";
"config.module.paxcounter.settings"="PAX Counter";
"config.module.paxcounter.title"="PAX Counter Config";
"config.module.paxcounter.enabled.description"="When enabled the PAX Counter module counts the number of people passing by using WiFi and Bluetooth. Both WiFI and Bluetooth must be disabled for PAX counter to work.";
"config.module.paxcounter.updateinterval"="Update Interval";
"config.module.paxcounter.updateinterval.description"="How often we can send a message to the mesh when people are detected.";
"config.power.wait.bluetooth.secs"="蓝牙关闭 After";
"config.ringtone"="RTTTL 铃声";
"config.ringtone.title"="铃声配置";
"config.ringtone.label"="铃声传输语言";
"config.ringtone.description"="支持外部通知中使用的铃声传输语言 (RTTTL) 铃声字符串。";
"config.module.paxcounter.settings"="PAX 计数器";
"config.module.paxcounter.title"="PAX 计数器配置";
"config.module.paxcounter.enabled.description"="启用 PAX 计数器模块时,通过使用 WiFi 和蓝牙来计算经过的人数。为了使 PAX 计数器正常工作,必须将 WiFi 和蓝牙都禁用。";
"config.module.paxcounter.updateinterval"="更新间隔";
"config.module.paxcounter.updateinterval.description"="检测到人员时,我们可以隔多久发送一条消息到 Mesh";
"config.save.confirm"="电台将会在配置保存后重启。";
"connected.radio"="已连接的电台";
"communicating"="与电台进行通讯中...";
@ -85,24 +85,24 @@
"current"="当前";
"default"="默认";
"delete"="删除";
"detection.sensor"="Detection Sensor";
"detection.sensor"="检测传感器";
"device"="电台";
"device.config"="电台配置";
"device.configuration"="Device Configuration";
"device.configuration"="设备配置";
"device.metrics.delete"="删除所有电台指标?";
"device.metrics.log"="电台指标日志";
"device.role.client"="标准模式 - App 可以连接到电台进行收发操作,并且会自动转发 Mesh 网络中其他节点的消息。";
"device.role.clientmute"="静默模式 - 与标准模式类似App 可以连接到电台进行收发操作,但不会转发 Mesh 网络中其他节点的消息。";
"device.role.clienthidden"=" Used for nodes that \"only speak when spoken to\" Turns all of the routine broadcasts but allows for ad-hoc communication. Still rebroadcasts, but with local only rebroadcast mode (known meshes only). Can be used for private operation or to dramatically reduce airtime / power consumption.";
"device.role.lostandfound"="Used to automatically send a text message to the mesh with the current position of the device on a frequent interval: \"I'm lost! Position: lat / long\"";
"device.role.clienthidden"="用于\"只有在被请求时才发言\"的节点。关闭所有常规广播,但允许临时通信。仍然会进行转发,但采用本地转发模式(仅限已知的网络)。可用于私密操作或大幅减少空中时间/功耗。";
"device.role.lostandfound"="用于定期自动向 Mesh 发送包含设备当前位置的文本消息的功能:\"I'm lost! Position: lat / long\"";
"device.role.router"="纯路由模式 - 自动转发 Mesh 网络中其他节点的消息中继模式下屏幕会熄灭Wi-Fi 和蓝牙将会进入睡眠模式App 将无法连接到电台进行收发操作。";
"device.role.routerclient"="路由客户端模式 - 优先转发 Mesh 网络中其他节点的消息App 也可以连接到电台进行收发操作。";
"device.role.repeater"="中继模式 - Mesh 网络数据包将优先通过此节点路由。此模式可消除不必要的开销,如 NodeInfo、DeviceTelemetry 和任何其他 Mesh 数据包,从而使设备不显示为 Mesh 网络的一部分。有关此角色的其他特定设置,请参阅转播模式。";
"device.role.repeater"="中继模式 - Mesh 网络数据包将优先通过此节点路由。此模式可消除不必要的开销,如节点信息、设备遥测和任何其他 Mesh 数据包,从而使设备不显示为 Mesh 网络的一部分。有关此角色的其他特定设置,请参阅转播模式。";
"device.role.tracker"="定位模式 - 用于作为 GPS 跟踪器。从该设备发送的定位数据包优先级较高,每两分钟广播一次。智能位置广播默认为关闭。";
"device.role.lostandfound"="Broadcasts location as message to default channel regularly for to assist with device recovery.";
"device.role.sensor"="Broadcasts telemetry packets as priority.";
"device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts.";
"device.role.taktracker"="Enables automatic TAK PLI broadcasts and reduces routine broadcasts.";
"device.role.lostandfound"="定期向默认通道广播位置信息,以帮助寻回设备。";
"device.role.sensor"="将遥测数据包优先广播。";
"device.role.tak"="针对 ATAK 系统通信进行优化,减少常规广播。";
"device.role.taktracker"="启用自动 TAK PLIPosition Location Information广播并减少常规广播。";
"direct.messages"="直频消息";
"dismiss.keyboard"="隐藏键盘";
"display"="屏幕(电台屏幕)";
@ -119,8 +119,8 @@
"finish"="Finish";
"firmware.version"="固件版本";
"firmware.version.unsupported"="检测到不支持的固件版本,无法连接到电台。";
"gas"="Gas";
"gas.resistance"="Gas Resistance";
"gas"="气体";
"gas.resistance"="气体阻抗";
"generate.qr.code"="生成二维码";
"gpsformat.dec"="十进制";
"gpsformat.dms"="度分秒";
@ -178,12 +178,12 @@
"map.centering"="居中";
"map.tiles.delete"="删除已缓存的地图区块";
"map.recentering"="自动重新居中";
"map.use.legacy"="Use Legacy Mesh Map";
"map.use.legacy"="使用传统网状地图";
"map.type"="地图类型";
"map.usertrackingmode"="用户跟随模式";
"map.usertrackingmode.none"="无";
"map.usertrackingmode.follow"="跟随";
"map.usertrackingmode.followwithheading"="Follow with heading";
"map.usertrackingmode.followwithheading"="跟随航向";
"mesh.live.activity"="Mesh 实时活动";
"mesh.log"="Mesh 日志";
"mesh.log.ambientlighting.config %@"="Ambient Lighting module config received: %@";
@ -231,17 +231,17 @@
"mode"="模式";
"module.configuration"="模块配置";
"mqtt"="MQTT";
"mqtt.connect"="Connect to MQTT";
"mqtt.connect"="连接至 MQTT";
"mqtt.config"="MQTT 配置";
"mqtt.clientproxy"="MQTT 客户端代理";
"mqtt.disconnect"="Disconnect from MQTT";
"mqtt.disconnect"="断开 MQTT 连接";
"mqtt.username"="用户名称";
"name"="名称";
"network"="网络";
"network.config"="网络配置";
"nodes"="节点";
"nodes %@"="节点 (%@)";
"nodelist.filter.distance %@"="up to %@ away";
"nodelist.filter.distance %@"="最远距离 %@";
"no.nodes"="未找到 Meshtastic 节点";
"not.connected"="未连接到电台";
"numbers.punctuation"="数字和标点符号";
@ -250,7 +250,7 @@
"on.boot"="仅在启动时";
"options"="选项";
"password"="密码";
"pause"="Pause";
"pause"="暂停";
"phone.gps"="手机 GPS";
"phone.gps.interval.description"="电台通过手机获取定位的时间间隔,但是向 Mesh 网络中刷新定位的时间间隔由电台控制。";
"position"="定位";
@ -265,11 +265,11 @@
"reboot.node"="重启节点?";
"received.ack"="收到确认";
"received.ack.real"="收件人确认";
"resume"="Resume";
"resume"="恢复";
"ringtone"="铃声";
"ringtone.config"="铃声设置";
"route.recorder"="Route Recorder";
"routes"="Routes";
"route.recorder"="路径记录器";
"routes"="路径";
"routing.acknowledged"="确认";
"routing.noroute"="找不到目标";
"routing.gotnak"="收到否认";
@ -303,7 +303,7 @@
"set.region"="设置 LoRa 区域";
"standard"="标准";
"standard.muted"="标准静音";
"start"="Start";
"start"="开始";
"ssid"="SSID";
"storeforward"="储存 & 转发";
"storeforward.config"="储存 & 转发设置";
@ -323,10 +323,10 @@
"timestamp"="时间戳";
"tip.bluetooth.connect.title"="连接到 LoRa 电台";
"tip.bluetooth.connect.message"="显示当前通过蓝牙连接的 Lora 电台的信息。您可以向左滑动断开电台,长按查看统计信息或开始实时活动。";
"tip.channel.admin.title"="Admin Channel";
"tip.channel.admin.message"="Admin channel detected: Select a node from the drop down to manage connected or remote devices.";
"tip.channels.create.title"="Manage Channels";
"tip.channels.create.message"="Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/radio/channels/)";
"tip.channel.admin.title"="admin 频道";
"tip.channel.admin.message"="检测到 admin 频道:请从下拉菜单中选择一个节点,来管理已连接或远程设备。";
"tip.channels.create.title"="管理频道";
"tip.channels.create.message"="大多数 Mesh 上的数据都通过主要频道发送。您可以设置次要频道,以创建额外的消息组,并通过其自己的密钥进行安全保护。 [频道配置提示](https://meshtastic.org/docs/configuration/radio/channels/)";
"tip.channels.share.title"="共享 Meshtastic 频道";
"tip.channels.share.message"="在 Meshtastic 网络中最多有 8 个频道。第一个频道是主频道,大多数活动都发生在这里,也是必需的。如果您不共享主频道,您的第一个共享频道就会成为其他网络的主频道。它会在其主频道和您的辅助频道上对话。名称为 admin 的频道可远程控制节点。其他频道用于私人群组,每个群组都有自己的密钥。";
"tip.messages.title"="消息";
@ -341,4 +341,4 @@
"user.details"="用户信息";
"voltage"="电压";
"waiting"="等待中...";
"appsettings.newNodeNotifications"="New Node Notifications";
"appsettings.newNodeNotifications"="新节点通知";