Merge branch '2.2.3_Working_Changes' into polish-translation

This commit is contained in:
Garth Vander Houwen 2023-08-27 18:31:47 -07:00 committed by GitHub
commit ab75e624d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
87 changed files with 2591 additions and 1218 deletions

View file

@ -7,6 +7,9 @@
objects = {
/* Begin PBXBuildFile section */
6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */; };
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */; };
6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */; };
C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */; };
C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; };
DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */ = {isa = PBXBuildFile; productRef = DD0D3D212A55CEB10066DB71 /* CocoaMQTT */; };
@ -115,6 +118,7 @@
DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */; };
DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */; };
DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC3B273283F411B00AC321C /* LastHeardText.swift */; };
DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */; };
DDC4D568275499A500A4208E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC4D567275499A500A4208E /* Persistence.swift */; };
DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */; };
DDC94FC229CE063B0082EA6E /* BatteryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */; };
@ -151,6 +155,7 @@
DDDE5A1429AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; };
DDDEE5E129DA3E1100A8E078 /* NodeInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDEE5E029DA3E1100A8E078 /* NodeInfoView.swift */; };
DDE0F7C5295F77B700B8AAB3 /* AppSettingsEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */; };
DDF6B2482A9AEBF500BA6931 /* StoreForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */; };
DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; };
DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */; };
/* End PBXBuildFile section */
@ -194,6 +199,9 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAppDelegate.swift; sourceTree = "<group>"; };
6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = "<group>"; };
6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = "<group>"; };
A65FA974296876BF00A97686 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMBTileOverlay.swift; sourceTree = "<group>"; };
DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV14.xcdatamodel; sourceTree = "<group>"; };
@ -318,6 +326,8 @@
DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationHelper.swift; sourceTree = "<group>"; };
DDC3B273283F411B00AC321C /* LastHeardText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastHeardText.swift; sourceTree = "<group>"; };
DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorConfig.swift; sourceTree = "<group>"; };
DDC4CA012A8DAA3800CE201C /* MeshtasticDataModelV16.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV16.xcdatamodel; sourceTree = "<group>"; };
DDC4D567275499A500A4208E /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryLevel.swift; sourceTree = "<group>"; };
DDC94FC329CED7280082EA6E /* MeshtasticDataModelV10.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV10.xcdatamodel; sourceTree = "<group>"; };
@ -357,6 +367,9 @@
DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV11.xcdatamodel; sourceTree = "<group>"; };
DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsEnums.swift; sourceTree = "<group>"; };
DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = "<group>"; };
DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV17.xcdatamodel; sourceTree = "<group>"; };
DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreForward.swift; sourceTree = "<group>"; };
DDF6B24B2A9C2FC800BA6931 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
DDF924C926FBB953009FE055 /* ConnectedDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = "<group>"; };
DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentConditionsCompact.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -428,6 +441,7 @@
DD90860D26F69BAE00DC5189 /* NodeMap.swift */,
DD73FD1028750779000852D6 /* PositionLog.swift */,
DD14E72D2A82A614006E39BC /* RemoteHardware.swift */,
6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */,
);
path = Nodes;
sourceTree = "<group>";
@ -511,11 +525,13 @@
isa = PBXGroup;
children = (
DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */,
DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */,
DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */,
DD2160AE28C5552500C17253 /* MQTTConfig.swift */,
DD41582928585C32009B0E59 /* RangeTestConfig.swift */,
DDC94FCD29CF55310082EA6E /* RtttlConfig.swift */,
DD6193782863875F00E59241 /* SerialConfig.swift */,
DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */,
DD415827285859C4009B0E59 /* TelemetryConfig.swift */,
);
path = Module;
@ -621,6 +637,7 @@
DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */,
DDC2E16526CE248F0042C5E4 /* Info.plist */,
DDC2E15D26CE248F0042C5E4 /* Preview Content */,
6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */,
);
path = Meshtastic;
sourceTree = "<group>";
@ -739,6 +756,7 @@
DD964FC52975DBFD007C176F /* QueryCoreData.swift */,
DD3CC6C128EB9D4900FA9159 /* UpdateCoreData.swift */,
DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */,
6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */,
);
path = Persistence;
sourceTree = "<group>";
@ -911,6 +929,7 @@
de,
Base,
"zh-Hans",
pl,
);
mainGroup = DDC2E14B26CE248E0042C5E4;
packageReferences = (
@ -1002,6 +1021,7 @@
DD3501892852FC3B000FC853 /* Settings.swift in Sources */,
DDDB443629F6287000EE2349 /* MapButtons.swift in Sources */,
DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */,
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */,
DD5E5203298EE33B00D21B61 /* config.pb.swift in Sources */,
DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */,
DDA6B2EB28420A7B003E8C16 /* NodeAnnotation.swift in Sources */,
@ -1032,6 +1052,7 @@
DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */,
DD5E5209298EE33B00D21B61 /* module_config.pb.swift in Sources */,
DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */,
6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */,
DDDB444229F8A88700EE2349 /* Double.swift in Sources */,
DD5E520F298EE33B00D21B61 /* cannedmessages.pb.swift in Sources */,
DDB75A232A13CDA9006ED576 /* BatteryLevelCompact.swift in Sources */,
@ -1054,6 +1075,7 @@
DD5E520D298EE33B00D21B61 /* storeforward.pb.swift in Sources */,
DD882F5D2772E4640005BF05 /* Contacts.swift in Sources */,
DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */,
6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */,
DD47E3CE26F103C600029299 /* NodeList.swift in Sources */,
DD5E520A298EE33B00D21B61 /* channel.pb.swift in Sources */,
DDDEE5E129DA3E1100A8E078 /* NodeInfoView.swift in Sources */,
@ -1109,8 +1131,10 @@
DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */,
DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */,
DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */,
DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */,
DD5E5210298EE33B00D21B61 /* telemetry.pb.swift in Sources */,
DD5E5205298EE33B00D21B61 /* mesh.pb.swift in Sources */,
DDF6B2482A9AEBF500BA6931 /* StoreForward.swift in Sources */,
DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */,
DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */,
DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */,
@ -1178,6 +1202,7 @@
DDCDC6CC29481FCC004C1DDA /* en */,
DDCDC6CE294821AD004C1DDA /* de */,
A65FA974296876BF00A97686 /* zh-Hans */,
DDF6B24B2A9C2FC800BA6931 /* pl */,
);
name = Localizable.strings;
sourceTree = "<group>";
@ -1237,7 +1262,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@ -1293,7 +1318,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@ -1323,12 +1348,12 @@
INFOPLIST_FILE = Meshtastic/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Meshtastic;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.2.0;
MARKETING_VERSION = 2.2.3;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1357,12 +1382,12 @@
INFOPLIST_FILE = Meshtastic/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Meshtastic;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.2.0;
MARKETING_VERSION = 2.2.3;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1382,7 +1407,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = GCH7VS5Y9R;
INFOPLIST_FILE = MeshtasticTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1407,7 +1432,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = GCH7VS5Y9R;
INFOPLIST_FILE = MeshtasticTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1428,6 +1453,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = GCH7VS5Y9R;
INFOPLIST_FILE = MeshtasticUITests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1448,6 +1474,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = GCH7VS5Y9R;
INFOPLIST_FILE = MeshtasticUITests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1468,6 +1495,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = Widgets/WidgetsExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GCH7VS5Y9R;
@ -1475,15 +1503,16 @@
INFOPLIST_FILE = Widgets/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Widgets;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.0;
MARKETING_VERSION = 2.2.3;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
@ -1499,6 +1528,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = Widgets/WidgetsExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GCH7VS5Y9R;
@ -1506,15 +1536,16 @@
INFOPLIST_FILE = Widgets/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Widgets;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.0;
MARKETING_VERSION = 2.2.3;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
@ -1622,6 +1653,8 @@
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */,
DDC4CA012A8DAA3800CE201C /* MeshtasticDataModelV16.xcdatamodel */,
DD14E72C2A80738F006E39BC /* MeshtasticDataModelV15.xcdatamodel */,
DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */,
DDB75A1F2A10766D006ED576 /* MeshtasticDataModelV13.xcdatamodel */,
@ -1638,7 +1671,7 @@
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
);
currentVersion = DD14E72C2A80738F006E39BC /* MeshtasticDataModelV15.xcdatamodel */;
currentVersion = DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */;
name = Meshtastic.xcdatamodeld;
path = Meshtastic/Meshtastic.xcdatamodeld;
sourceTree = "<group>";

View file

@ -23,8 +23,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/stephencelis/SQLite.swift.git",
"state" : {
"revision" : "60a65015f6402b7c34b9a924f755ca0a73afeeaa",
"version" : "0.13.1"
"revision" : "7a2e3cd27de56f6d396e84f63beefd0267b55ccb",
"version" : "0.14.1"
}
},
{
@ -41,8 +41,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "e1499bc69b9040b29184f7f2996f7bab467c1639",
"version" : "1.19.0"
"revision" : "ce20dc083ee485524b802669890291c0d8090170",
"version" : "1.22.1"
}
}
],

View file

@ -9,16 +9,13 @@ import Foundation
import MapKit
enum MeshMapTypes: Int, CaseIterable, Identifiable {
case standard = 0
case mutedStandard = 5
case hybrid = 2
case hybridFlyover = 4
case satellite = 1
case satelliteFlyover = 3
var id: Int { self.rawValue }
var description: String {
switch self {
case .standard:
@ -36,7 +33,6 @@ enum MeshMapTypes: Int, CaseIterable, Identifiable {
}
}
func MKMapTypeValue() -> MKMapType {
switch self {
case .standard:
return MKMapType.standard
@ -55,13 +51,10 @@ enum MeshMapTypes: Int, CaseIterable, Identifiable {
}
enum UserTrackingModes: Int, CaseIterable, Identifiable {
case none = 0
case follow = 1
case followWithHeading = 2
var id: Int { self.rawValue }
var description: String {
switch self {
case .none:
@ -80,7 +73,6 @@ enum UserTrackingModes: Int, CaseIterable, Identifiable {
}
}
func MKUserTrackingModeValue() -> MKUserTrackingMode {
switch self {
case .none:
return MKUserTrackingMode.none
@ -93,7 +85,6 @@ enum UserTrackingModes: Int, CaseIterable, Identifiable {
}
enum LocationUpdateInterval: Int, CaseIterable, Identifiable {
case fiveSeconds = 5
case tenSeconds = 10
case fifteenSeconds = 15
@ -103,7 +94,6 @@ enum LocationUpdateInterval: Int, CaseIterable, Identifiable {
case fiveMinutes = 300
case tenMinutes = 600
case fifteenMinutes = 900
var id: Int { self.rawValue }
var description: String {
switch self {
@ -138,7 +128,6 @@ enum MapLayer: String, CaseIterable, Equatable {
}
enum MapTileServer: String, CaseIterable, Identifiable {
case openStreetMap
case openStreetMapDE
case openStreetMapFR
@ -154,8 +143,6 @@ enum MapTileServer: String, CaseIterable, Identifiable {
var id: String { self.rawValue }
var attribution: String {
switch self {
case .openStreetMap:
return "Map and data © [OpenStreetMap](http://www.openstreetmap.org) and contributors, [CC-BY-SA](http://creativecommons.org/licenses/by-sa/2.0/)"
case .openStreetMapDE:
@ -276,7 +263,6 @@ enum OverlayType: String, CaseIterable, Equatable {
}
enum MapOverlayServer: String, CaseIterable, Identifiable {
case baseReReflectivityCurrent
case baseReReflectivityOneHourAgo
case echoTopsEetCurrent
@ -290,7 +276,6 @@ enum MapOverlayServer: String, CaseIterable, Identifiable {
var id: String { self.rawValue }
var overlayType: OverlayType {
switch self {
case .baseReReflectivityCurrent:
return .tileServer
case .baseReReflectivityOneHourAgo:

View file

@ -307,9 +307,7 @@ enum HardwareModels: String, CaseIterable, Identifiable {
}
}
enum HardwarePlatforms: String, CaseIterable, Identifiable {
case none
case esp32
case nrf52
@ -319,7 +317,6 @@ enum HardwarePlatforms: String, CaseIterable, Identifiable {
var id: String { self.rawValue }
var description: String {
switch self {
case .none:
return "None"
case .esp32:

View file

@ -53,6 +53,21 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
return csvString
}
func detectionsToCsv(detections: [MessageEntity]) -> String {
var csvString: String = ""
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
// Create Header
csvString = "Detection event, \("timestamp".localized)"
for d in detections {
csvString += "\n"
csvString += d.messagePayload ?? "Detection"
csvString += ", "
csvString += d.timestamp.formattedDate(format: dateFormatString).localized
}
return csvString
}
func positionToCsvFile(positions: [PositionEntity]) -> String {
var csvString: String = ""
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)

View file

@ -20,7 +20,6 @@ extension Color {
}
extension UIColor {
/// Returns a boolean indicating if a color is light
/// - Returns: true if the color is light
func isLight() -> Bool {
@ -28,7 +27,6 @@ extension UIColor {
let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000
return (brightness > 0.5)
}
/// Returns a UInt32 from a UIColor
/// - Returns: UInt32
var hex: UInt32 {
@ -41,7 +39,6 @@ extension UIColor {
value += UInt32(blue * 255)
return value
}
/// Returns a UIColor from a UInt32 value
/// - Parameter hex: UInt32 value to convert to a color
/// - Returns: UIColor
@ -49,7 +46,7 @@ extension UIColor {
let red = CGFloat((hex & 0xFF0000) >> 16)
let green = CGFloat((hex & 0x00FF00) >> 8)
let blue = CGFloat((hex & 0x0000FF))
//print("\(red) - \(green) - \(blue)")
/// print("\(red) - \(green) - \(blue)")
self.init(red: red/255.0, green: green/255.0, blue: blue/255.0, alpha: 1.0)
}
}

View file

@ -7,7 +7,6 @@
import Foundation
extension Double {
var toBytes: String {
let formatter = MeasurementFormatter()
let measurement = Measurement(value: self, unit: UnitInformationStorage.bytes)

View file

@ -9,7 +9,7 @@ import Foundation
let allocatedSizeResourceKeys: Set<URLResourceKey> = [
.isRegularFileKey,
.fileAllocatedSizeKey,
.totalFileAllocatedSizeKey,
.totalFileAllocatedSizeKey
]
public extension FileManager {
@ -26,7 +26,7 @@ public extension FileManager {
func allocatedSizeOfDirectory(at directoryURL: URL) -> String {
// The error handler simply stores the error and stops traversal
var enumeratorError: Error? = nil
var enumeratorError: Error?
func errorHandler(_: URL, error: Error) -> Bool {
enumeratorError = error
return false

View file

@ -29,9 +29,6 @@ extension String {
}
var localized: String { NSLocalizedString(self, comment: self) }
func isEmoji() -> Bool {
// Emoji are no more than 4 bytes
if self.count > 4 {
@ -45,7 +42,6 @@ extension String {
}
}
}
func onlyEmojis() -> Bool {
return count > 0 && !contains { !$0.isEmoji }
}

View file

@ -15,7 +15,6 @@ extension URL {
guard resourceValues.isRegularFile ?? false else {
return 0
}
return UInt64(resourceValues.totalFileAllocatedSize ?? resourceValues.fileAllocatedSize ?? 0)
}
}

View file

@ -8,7 +8,6 @@
import Foundation
extension UserDefaults {
enum Keys: String, CaseIterable {
case meshtasticUsername
case preferredPeripheralId
@ -26,7 +25,6 @@ extension UserDefaults {
func reset() {
Keys.allCases.forEach { removeObject(forKey: $0.rawValue) }
}
static var meshtasticUsername: String {
get {
UserDefaults.standard.string(forKey: "meshtasticUsername") ?? ""
@ -44,7 +42,6 @@ extension UserDefaults {
UserDefaults.standard.set(newValue, forKey: "preferredPeripheralId")
}
}
static var provideLocation: Bool {
get {
UserDefaults.standard.bool(forKey: "provideLocation")
@ -52,7 +49,6 @@ extension UserDefaults {
UserDefaults.standard.set(newValue, forKey: "provideLocation")
}
}
static var provideLocationInterval: Int {
get {
UserDefaults.standard.integer(forKey: "provideLocationInterval")
@ -61,7 +57,6 @@ extension UserDefaults {
UserDefaults.standard.set(newValue, forKey: "provideLocationInterval")
}
}
static var mapLayer: MapLayer {
get {
MapLayer(rawValue: UserDefaults.standard.string(forKey: "mapLayer") ?? MapLayer.standard.rawValue) ?? MapLayer.standard
@ -70,7 +65,6 @@ extension UserDefaults {
UserDefaults.standard.set(newValue.rawValue, forKey: "mapLayer")
}
}
static var enableMapRecentering: Bool {
get {
UserDefaults.standard.bool(forKey: "meshMapRecentering")
@ -79,7 +73,6 @@ extension UserDefaults {
UserDefaults.standard.set(newValue, forKey: "meshMapRecentering")
}
}
static var enableMapNodeHistoryPins: Bool {
get {
UserDefaults.standard.bool(forKey: "meshMapShowNodeHistory")
@ -88,7 +81,6 @@ extension UserDefaults {
UserDefaults.standard.set(newValue, forKey: "meshMapShowNodeHistory")
}
}
static var enableMapRouteLines: Bool {
get {
UserDefaults.standard.bool(forKey: "meshMapShowRouteLines")
@ -97,7 +89,6 @@ extension UserDefaults {
UserDefaults.standard.set(newValue, forKey: "meshMapShowRouteLines")
}
}
static var enableOfflineMaps: Bool {
get {
UserDefaults.standard.bool(forKey: "enableOfflineMaps")
@ -114,17 +105,14 @@ extension UserDefaults {
UserDefaults.standard.set(newValue, forKey: "enableOfflineMapsMBTiles")
}
}
static var mapTileServer: MapTileServer {
get {
MapTileServer(rawValue: UserDefaults.standard.string(forKey: "mapTileServer") ?? MapTileServer.openStreetMap.rawValue) ?? MapTileServer.openStreetMap
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: "mapTileServer")
}
}
static var enableOverlayServer: Bool {
get {
UserDefaults.standard.bool(forKey: "enableOverlayServer")
@ -133,17 +121,14 @@ extension UserDefaults {
UserDefaults.standard.set(newValue, forKey: "enableOverlayServer")
}
}
static var mapOverlayServer: MapOverlayServer {
get {
MapOverlayServer(rawValue: UserDefaults.standard.string(forKey: "mapOverlayServer") ?? MapOverlayServer.baseReReflectivityCurrent.rawValue) ?? MapOverlayServer.baseReReflectivityCurrent
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: "mapOverlayServer")
}
}
static var mapTilesAboveLabels: Bool {
get {
UserDefaults.standard.bool(forKey: "mapTilesAboveLabels")

View file

@ -10,45 +10,6 @@ import CocoaMQTT
// ---------------------------------------------------------------------------------------
class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate, ObservableObject {
// MqttClientProxyManagerDelegate
func onMqttConnected() {
mqttManager.status = .connected
print("📲 Mqtt Client Proxy onMqttConnected now subscribing to \(mqttManager.topic).")
mqttManager.mqttClientProxy?.subscribe(mqttManager.topic)
}
func onMqttDisconnected() {
mqttManager.status = .disconnected
print("MQTT Disconnected")
}
func onMqttMessageReceived(message: CocoaMQTTMessage) {
print("📲 Mqtt Client Proxy onMqttMessageReceived for topic: \(message.topic)")
if message.topic.contains("/stat/") {
return
}
var proxyMessage = MqttClientProxyMessage()
proxyMessage.topic = message.topic
proxyMessage.data = Data(message.payload)
proxyMessage.retained = message.retained
var toRadio: ToRadio!
toRadio = ToRadio()
toRadio.mqttClientProxyMessage = proxyMessage
let binaryData: Data = try! toRadio.serializedData()
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
print("📲 Sent Mqtt client proxy message to the connected device.")
}
}
func onMqttError(message: String) {
print("MQTT Error")
}
private static var documentsFolder: URL {
do {
return try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
@ -66,6 +27,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
@Published var invalidVersion = false
@Published var isSwitchedOn: Bool = false
@Published var automaticallyReconnect: Bool = true
@Published var mqttProxyConnected: Bool = false
public var minimumVersion = "2.0.0"
public var connectedVersion: String
public var isConnecting: Bool = false
@ -78,6 +40,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var lastPosition: CLLocationCoordinate2D?
let emptyNodeNum: UInt32 = 4294967295
let mqttManager = MqttClientProxyManager.shared
var wantRangeTestPackets = false
/* Meshtastic Service Details */
var TORADIO_characteristic: CBCharacteristic!
var FROMRADIO_characteristic: CBCharacteristic!
@ -312,6 +275,45 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
}
// MARK: MqttClientProxyManagerDelegate Methods
func onMqttConnected() {
mqttProxyConnected = true
print("📲 Mqtt Client Proxy onMqttConnected now subscribing to \(mqttManager.topic).")
mqttManager.mqttClientProxy?.subscribe(mqttManager.topic)
}
func onMqttDisconnected() {
mqttProxyConnected = false
print("MQTT Disconnected")
}
func onMqttMessageReceived(message: CocoaMQTTMessage) {
print("📲 Mqtt Client Proxy onMqttMessageReceived for topic: \(message.topic)")
if message.topic.contains("/stat/") {
return
}
var proxyMessage = MqttClientProxyMessage()
proxyMessage.topic = message.topic
proxyMessage.data = Data(message.payload)
proxyMessage.retained = message.retained
var toRadio: ToRadio!
toRadio = ToRadio()
toRadio.mqttClientProxyMessage = proxyMessage
let binaryData: Data = try! toRadio.serializedData()
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
print("📲 Sent Mqtt client proxy message to the connected device.")
}
}
func onMqttError(message: String) {
mqttProxyConnected = false
print("📲 Mqtt Client Proxy onMqttError: \(message)")
}
// MARK: Protobuf Methods
func requestDeviceMetadata(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32, context: NSManagedObjectContext) -> Int64 {
guard connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected else { return 0 }
@ -503,7 +505,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if decodedInfo.metadata.firmwareVersion.count > 0 && !invalidVersion {
nowKnown = true
deviceMetadataPacket(metadata: decodedInfo.metadata, fromNum: connectedPeripheral.num, context: context!)
connectedPeripheral.firmwareVersion = decodedInfo.metadata.firmwareVersion ?? "unknown".localized
connectedPeripheral.firmwareVersion = decodedInfo.metadata.firmwareVersion
let lastDotIndex = decodedInfo.metadata.firmwareVersion.lastIndex(of: ".")
@ -528,7 +530,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
// Log any other unknownApp calls
if !nowKnown { MeshLogger.log("🕸️ MESH PACKET received for Unknown App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") }
case .textMessageApp:
case .textMessageApp, .detectionSensorApp:
textMessageAppPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!)
case .remoteHardwareApp:
MeshLogger.log("🕸️ MESH PACKET received for Remote Hardware App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
@ -549,9 +551,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
case .serialApp:
MeshLogger.log("🕸️ MESH PACKET received for Serial App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
case .storeForwardApp:
MeshLogger.log("🕸️ MESH PACKET received for Store Forward App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
storeAndForwardPacket(packet: decodedInfo.packet, connectedNodeNum: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!)
case .rangeTestApp:
MeshLogger.log("🕸️ MESH PACKET received for Range Test App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
if wantRangeTestPackets {
textMessageAppPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!)
}
else {
MeshLogger.log("🕸️ MESH PACKET received for Range Test App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
}
case .telemetryApp:
if !invalidVersion { telemetryPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) }
case .textMessageCompressedApp:
@ -599,7 +606,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected })
// Config conplete returns so we don't read the characteristic again
/// MQTT Client Proxy
/// MQTT Client Proxy and RangeTest interest
if connectedPeripheral.num > 0 {
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
@ -607,12 +614,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
do {
let fetchedNodeInfo = try context?.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] ?? []
if fetchedNodeInfo.count == 1 && fetchedNodeInfo[0].mqttConfig != nil {
//Subscribe to Mqtt Client Proxy if enabled
if fetchedNodeInfo[0].mqttConfig?.proxyToClientEnabled ?? false {
mqttManager.connectFromConfigSettings(node: fetchedNodeInfo[0])
}
}
if fetchedNodeInfo.count == 1 && fetchedNodeInfo[0].rangeTestConfig?.enabled == true {
wantRangeTestPackets = true;
}
} catch {
print("Failed to find a node info for the connected node")
}
@ -658,7 +668,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
self.startScanning()
// Try and connect to the preferredPeripherial first
let preferredPeripheral = peripherals.filter({ $0.peripheral.identifier.uuidString == UserDefaults.preferredPeripheralId as? String ?? "" }).first
let preferredPeripheral = peripherals.filter({ $0.peripheral.identifier.uuidString == UserDefaults.preferredPeripheralId as String }).first
if preferredPeripheral != nil && preferredPeripheral?.peripheral != nil {
connectTo(peripheral: preferredPeripheral!.peripheral)
}
@ -1374,6 +1384,32 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
return 0
}
public func saveDetectionSensorModuleConfig(config: ModuleConfig.DetectionSensorConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 {
var adminPacket = AdminMessage()
adminPacket.setModuleConfig.detectionSensor = config
var meshPacket: MeshPacket = MeshPacket()
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.to = UInt32(toUser.num)
meshPacket.from = UInt32(fromUser.num)
meshPacket.channel = UInt32(adminIndex)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.wantAck = true
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Detection Sensor Module Config for \(toUser.longName ?? "unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
upsertDetectionSensorModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!)
return Int64(meshPacket.id)
}
return 0
}
public func saveExternalNotificationModuleConfig(config: ModuleConfig.ExternalNotificationConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 {
var adminPacket = AdminMessage()
@ -1392,7 +1428,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "Saved External Notification Module Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved External Notification Module Config for \(toUser.longName ?? "unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
upsertExternalNotificationModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!)
return Int64(meshPacket.id)
@ -1417,7 +1453,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "Saved RTTTL Ringtone Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved RTTTL Ringtone Config for \(toUser.longName ?? "unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
upsertRtttlConfigPacket(ringtone: ringtone, nodeNum: toUser.num, context: context!)
@ -1446,7 +1482,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
meshPacket.decoded = dataMessage
let messageDescription = "Saved WiFi Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved MQTT Config for \(toUser.longName ?? "unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
upsertMqttModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!)
return Int64(meshPacket.id)
@ -1471,7 +1507,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "Saved Range Test Module Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved Range Test Module Config for \(toUser.longName ?? "unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
upsertRangeTestModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!)
@ -1499,7 +1535,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "Saved Serial Module Config for \(toUser.longName ?? "unknown".localized)"
let messageDescription = "🛟 Saved Serial Module Config for \(toUser.longName ?? "unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
upsertSerialModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!)
return Int64(meshPacket.id)
@ -1507,6 +1543,32 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
return 0
}
public func saveStoreForwardModuleConfig(config: ModuleConfig.StoreForwardConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 {
var adminPacket = AdminMessage()
adminPacket.setModuleConfig.storeForward = config
var meshPacket: MeshPacket = MeshPacket()
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.to = UInt32(toUser.num)
meshPacket.from = UInt32(fromUser.num)
meshPacket.channel = UInt32(adminIndex)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.wantAck = true
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Store & Forward Module Config for \(toUser.longName ?? "unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
upsertStoreForwardModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!)
return Int64(meshPacket.id)
}
return 0
}
public func saveTelemetryModuleConfig(config: ModuleConfig.TelemetryConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 {
var adminPacket = AdminMessage()
@ -1901,6 +1963,34 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
return false
}
public func requestDetectionSensorModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool {
var adminPacket = AdminMessage()
adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.detectionsensorConfig
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(toUser.num)
meshPacket.from = UInt32(fromUser.num)
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.channel = UInt32(adminIndex)
meshPacket.wantAck = true
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
dataMessage.wantResponse = true
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested Detection Sensor Module Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
return true
}
return false
}
public func requestSerialModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool {
var adminPacket = AdminMessage()

View file

@ -10,16 +10,16 @@ class LocalNotificationManager {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
if granted == true && error == nil {
self.scheduleNotifications()
self.scheduleNotifications()
}
}
}
func schedule() {
func schedule() {
UNUserNotificationCenter.current().getNotificationSettings { settings in
switch settings.authorizationStatus {
case .notDetermined:
self.requestAuthorization()
self.requestAuthorization()
case .authorized, .provisional:
self.scheduleNotifications()
default:
@ -37,6 +37,9 @@ class LocalNotificationManager {
content.body = notification.content
content.sound = .default
content.interruptionLevel = .timeSensitive
if notification.target != nil {
content.userInfo["target"] = notification.target
}
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger)
@ -65,4 +68,5 @@ struct Notification {
var title: String
var subtitle: String
var content: String
var target: String?
}

View file

@ -2,14 +2,10 @@ import Foundation
import CoreLocation
class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate {
static let shared = LocationHelper()
var locationManager = CLLocationManager()
@Published var authorizationStatus: CLAuthorizationStatus?
override init() {
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
@ -17,53 +13,41 @@ class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate {
locationManager.allowsBackgroundLocationUpdates = true
locationManager.activityType = .otherNavigation
}
// Apple Park
static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090)
static let DefaultAltitude = CLLocationDistance(integerLiteral: 0)
static let DefaultSpeed = CLLocationSpeed(integerLiteral: 0)
static let DefaultHeading = CLLocationDirection(integerLiteral: 0)
static var currentLocation: CLLocationCoordinate2D {
guard let location = shared.locationManager.location else {
return DefaultLocation
}
return location.coordinate
}
static var currentAltitude: CLLocationDistance {
guard let altitude = shared.locationManager.location?.altitude else {
return DefaultAltitude
}
return altitude
}
static var currentSpeed: CLLocationSpeed {
guard let speed = shared.locationManager.location?.speed else {
return DefaultSpeed
}
return speed
}
static var currentHeading: CLLocationDirection {
guard let heading = shared.locationManager.location?.course else {
return DefaultHeading
}
return heading
}
static var currentTimestamp: Date {
guard let timestamp = shared.locationManager.location?.timestamp else {
return Date.now
}
return timestamp
}
static var satsInView: Int {
// If we have a position we have a sat
var sats = 1
@ -93,26 +77,19 @@ class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate {
case .authorizedWhenInUse:
authorizationStatus = .authorizedWhenInUse
locationManager.requestLocation()
break
case .restricted:
authorizationStatus = .restricted
break
case .denied:
authorizationStatus = .denied
break
case .notDetermined:
authorizationStatus = .notDetermined
locationManager.requestWhenInUseAuthorization()
break
default:
break
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("Location manager error: \(error.localizedDescription)")
}

View file

@ -9,47 +9,35 @@ import Foundation
import MapKit
class OfflineTileManager: ObservableObject {
enum DownloadStatus {
case download, downloading, downloaded
}
static let shared = OfflineTileManager()
init() {
print("Documents Directory = \(documentsDirectory)")
createDirectoriesIfNecessary()
}
// MARK: - Private properties
// MARK: - Private properties
private var overlay: MKTileOverlay { MKTileOverlay(urlTemplate: UserDefaults.mapTileServer.tileUrl.count > 1 ? UserDefaults.mapTileServer.tileUrl : MapTileServer.openStreetMap.tileUrl) }
private var documentsDirectory: URL { fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! }
private let fileManager = FileManager.default
// MARK: - Public property
// MARK: - Public property
var progress: Float = 0
var status: DownloadStatus = .download
// MARK: - Public methods
// MARK: - Public methods
func getAllDownloadedSize() -> String {
fileManager.allocatedSizeOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"))
}
func hasBeenDownloaded(for boundingBox: MKMapRect) -> Bool {
getEstimatedDownloadSize(for: boundingBox) == 0
}
func getEstimatedDownloadSize(for boundingBox: MKMapRect) -> Double {
let paths = self.computeTileOverlayPaths(boundingBox: boundingBox)
let count = self.filterTilesAlreadyExisting(paths: paths).count
let size: Double = 30000 // Bytes (average size)
return Double(count) * size
}
func getDownloadedSize(for mapTileLink: MapTileServer) -> Double {
var accumulatedSize: UInt64 = 0
let mapTiles = try! fileManager.contentsOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"), includingPropertiesForKeys: [])
let matchingTiles = mapTiles.filter { fileName in
@ -61,10 +49,8 @@ class OfflineTileManager: ObservableObject {
let url = documentsDirectory.appendingPathComponent(tile.absoluteString)
accumulatedSize += (try? url.regularFileAllocatedSize()) ?? 0
}
return Double(accumulatedSize)
}
func getDownloadedSize(for boundingBox: MKMapRect) -> Double {
let paths = self.computeTileOverlayPaths(boundingBox: boundingBox)
var accumulatedSize: UInt64 = 0
@ -75,14 +61,11 @@ class OfflineTileManager: ObservableObject {
}
return Double(accumulatedSize)
}
func removeAll() {
try? fileManager.removeItem(at: documentsDirectory.appendingPathComponent("tiles"))
createDirectoriesIfNecessary()
}
func remove(for mapTileLink: MapTileServer) {
let mapTiles = try! fileManager.contentsOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"), includingPropertiesForKeys: [])
let matchingTiles = mapTiles.filter { fileName in
let fileNameLower = fileName.absoluteString
@ -93,7 +76,6 @@ class OfflineTileManager: ObservableObject {
try? fileManager.removeItem(at: tile.absoluteURL)
}
}
func remove(for boundingBox: MKMapRect) {
let paths = self.computeTileOverlayPaths(boundingBox: boundingBox)
for path in paths {
@ -103,7 +85,6 @@ class OfflineTileManager: ObservableObject {
}
self.status = .download
}
/// Download and persist all tiles within the boundingBox
func download(boundingBox: MKMapRect, name: String) {
NetworkManager.shared.runIfNetwork {
@ -116,18 +97,17 @@ class OfflineTileManager: ObservableObject {
self.progress = Float(i) / Float(filteredPaths.count)
}
DispatchQueue.main.async {
//NotificationManager.shared.sendNotification(title: "\("DownloadedTitle".localized) (\((self.getDownloadedSize(for: boundingBox)).toBytes))", message: "\("Downloaded".localized) (\(name))")
// NotificationManager.shared.sendNotification(title: "\("DownloadedTitle".localized) (\((self.getDownloadedSize(for: boundingBox)).toBytes))", message: "\("Downloaded".localized) (\(name))")
self.progress = 0
self.status = .downloaded
}
}
}
func getTileOverlay(for path: MKTileOverlayPath) -> URL {
let file = "\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png"
// Check is tile is already available
let tilesUrl = documentsDirectory.appendingPathComponent("tiles").appendingPathComponent(file)
if fileManager.fileExists(atPath: tilesUrl.path){
if fileManager.fileExists(atPath: tilesUrl.path) {
return tilesUrl
} else {
if UserDefaults.enableOfflineMaps { // Get and persist newTile
@ -137,8 +117,7 @@ class OfflineTileManager: ObservableObject {
}
}
}
// MARK: - Private methods
// MARK: Private methods
private func computeTileOverlayPaths(boundingBox box: MKMapRect, maxZ: Int = 17) -> [MKTileOverlayPath] {
var paths = [MKTileOverlayPath]()
for z in 1...maxZ {
@ -153,15 +132,13 @@ class OfflineTileManager: ObservableObject {
}
return paths
}
private func tranformCoordinate(coordinates: CLLocationCoordinate2D , zoom: Int) -> TileCoordinates {
private func tranformCoordinate(coordinates: CLLocationCoordinate2D, zoom: Int) -> TileCoordinates {
let lng = coordinates.longitude
let lat = coordinates.latitude
let tileX = Int(floor((lng + 180) / 360.0 * pow(2.0, Double(zoom))))
let tileY = Int(floor((1 - log( tan( lat * Double.pi / 180.0 ) + 1 / cos( lat * Double.pi / 180.0 )) / Double.pi ) / 2 * pow(2.0, Double(zoom))))
return (tileX, tileY, zoom)
}
@discardableResult private func persistLocally(path: MKTileOverlayPath) -> URL {
let url = overlay.url(forTilePath: path)
let file = "tiles/\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y).png"
@ -174,7 +151,6 @@ class OfflineTileManager: ObservableObject {
}
return url
}
private func filterTilesAlreadyExisting(paths: [MKTileOverlayPath]) -> [MKTileOverlayPath] {
paths.filter {
let file = "\(UserDefaults.mapTileServer.id)-z\($0.z)x\($0.x)y\($0.y).png"
@ -182,10 +158,8 @@ class OfflineTileManager: ObservableObject {
return !fileManager.fileExists(atPath: tilesPath)
}
}
private func createDirectoriesIfNecessary() {
let tiles = documentsDirectory.appendingPathComponent("tiles")
try? fileManager.createDirectory(at: tiles, withIntermediateDirectories: true, attributes: [:])
}
}

View file

@ -58,6 +58,8 @@ func moduleConfig (config: ModuleConfig, context: NSManagedObjectContext, nodeNu
if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(config.cannedMessage) {
upsertCannedMessagesModuleConfigPacket(config: config.cannedMessage, nodeNum: nodeNum, context: context)
} else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.detectionSensor(config.detectionSensor) {
upsertDetectionSensorModuleConfigPacket(config: config.detectionSensor, nodeNum: nodeNum, context: context)
} else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.externalNotification(config.externalNotification) {
upsertExternalNotificationModuleConfigPacket(config: config.externalNotification, nodeNum: nodeNum, context: context)
} else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.mqtt(config.mqtt) {
@ -68,6 +70,8 @@ func moduleConfig (config: ModuleConfig, context: NSManagedObjectContext, nodeNu
upsertSerialModuleConfigPacket(config: config.serial, nodeNum: nodeNum, context: context)
} else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.telemetry(config.telemetry) {
upsertTelemetryModuleConfigPacket(config: config.telemetry, nodeNum: nodeNum, context: context)
} else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.storeForward(config.storeForward) {
upsertStoreForwardModuleConfigPacket(config: config.storeForward, nodeNum: nodeNum, context: context)
}
}
@ -90,7 +94,6 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO
myInfoEntity.peripheralId = peripheralId
myInfoEntity.myNodeNum = Int64(myInfo.myNodeNum)
myInfoEntity.rebootCount = Int32(myInfo.rebootCount)
myInfoEntity.minAppVersion = Int32(bitPattern: myInfo.minAppVersion)
do {
try context.save()
print("💾 Saved a new myInfo for node number: \(String(myInfo.myNodeNum))")
@ -105,7 +108,6 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO
fetchedMyInfo[0].peripheralId = peripheralId
fetchedMyInfo[0].myNodeNum = Int64(myInfo.myNodeNum)
fetchedMyInfo[0].rebootCount = Int32(myInfo.rebootCount)
fetchedMyInfo[0].minAppVersion = Int32(bitPattern: myInfo.minAppVersion)
do {
try context.save()
@ -423,64 +425,48 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
}
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getChannelResponse(adminMessage.getChannelResponse) {
channelPacket(channel: adminMessage.getChannelResponse, fromNum: Int64(packet.from), context: context)
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getDeviceMetadataResponse(adminMessage.getDeviceMetadataResponse) {
deviceMetadataPacket(metadata: adminMessage.getDeviceMetadataResponse, fromNum: Int64(packet.from), context: context)
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getConfigResponse(adminMessage.getConfigResponse) {
let config = adminMessage.getConfigResponse
if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) {
upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: Int64(packet.from), context: context)
} else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) {
upsertDeviceConfigPacket(config: config.device, nodeNum: Int64(packet.from), context: context)
} else if config.payloadVariant == Config.OneOf_PayloadVariant.display(config.display) {
upsertDisplayConfigPacket(config: config.display, nodeNum: Int64(packet.from), context: context)
} else if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) {
upsertLoRaConfigPacket(config: config.lora, nodeNum: Int64(packet.from), context: context)
} else if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) {
upsertNetworkConfigPacket(config: config.network, nodeNum: Int64(packet.from), context: context)
} else if config.payloadVariant == Config.OneOf_PayloadVariant.position(config.position) {
upsertPositionConfigPacket(config: config.position, nodeNum: Int64(packet.from), context: context)
}
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getModuleConfigResponse(adminMessage.getModuleConfigResponse) {
let moduleConfig = adminMessage.getModuleConfigResponse
if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(moduleConfig.cannedMessage) {
upsertCannedMessagesModuleConfigPacket(config: moduleConfig.cannedMessage, nodeNum: Int64(packet.from), context: context)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.detectionSensor(moduleConfig.detectionSensor) {
upsertDetectionSensorModuleConfigPacket(config: moduleConfig.detectionSensor, nodeNum: Int64(packet.from), context: context)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.externalNotification(moduleConfig.externalNotification) {
upsertExternalNotificationModuleConfigPacket(config: moduleConfig.externalNotification, nodeNum: Int64(packet.from), context: context)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.mqtt(moduleConfig.mqtt) {
upsertMqttModuleConfigPacket(config: moduleConfig.mqtt, nodeNum: Int64(packet.from), context: context)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.rangeTest(moduleConfig.rangeTest) {
upsertRangeTestModuleConfigPacket(config: moduleConfig.rangeTest, nodeNum: Int64(packet.from), context: context)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.serial(moduleConfig.serial) {
upsertSerialModuleConfigPacket(config: moduleConfig.serial, nodeNum: Int64(packet.from), context: context)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.storeForward(moduleConfig.storeForward) {
upsertStoreForwardModuleConfigPacket(config: moduleConfig.storeForward, nodeNum: Int64(packet.from), context: context)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.telemetry(moduleConfig.telemetry) {
upsertTelemetryModuleConfigPacket(config: moduleConfig.telemetry, nodeNum: Int64(packet.from), context: context)
}
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getRingtoneResponse(adminMessage.getRingtoneResponse) {
let ringtone = adminMessage.getRingtoneResponse
upsertRtttlConfigPacket(ringtone: ringtone, nodeNum: Int64(packet.from), context: context)
} else {
MeshLogger.log("🕸️ MESH PACKET received for Admin App \(try! packet.decoded.jsonString())")
}
// Save an ack for the admin message log for each admin message response received as we stopped sending acks if there is also a response to reduce airtime.
adminResponseAck(packet: packet, context: context)
}
@ -580,6 +566,45 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana
}
}
func storeAndForwardPacket(packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) {
if let storeAndForwardMessage = try? StoreAndForward(serializedData: packet.decoded.payload) {
// Request Response
switch storeAndForwardMessage.rr {
case .unset:
MeshLogger.log("📮 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .routerError:
MeshLogger.log("☠️ Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .routerHeartbeat:
// Query any messages since the heartbeat.period. Send their ids to the store and forward node.
MeshLogger.log("💓 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .routerPing:
MeshLogger.log("🏓 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .routerPong:
MeshLogger.log("🏓 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .routerBusy:
MeshLogger.log("🐝 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .routerHistory:
MeshLogger.log("📜 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .routerStats:
MeshLogger.log("📊 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .clientError:
MeshLogger.log("☠️ Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .clientHistory:
MeshLogger.log("📜 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .clientStats:
MeshLogger.log("📊 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .clientPing:
MeshLogger.log("🏓 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .clientPong:
MeshLogger.log("🏓 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .clientAbort:
MeshLogger.log("🛑 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .UNRECOGNIZED:
MeshLogger.log("📮 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
}
}
}
func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) {
if let telemetryMessage = try? Telemetry(serializedData: packet.decoded.payload) {
@ -590,7 +615,6 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
MeshLogger.log("📈 \(logString)")
} else {
// If it is the connected node
}
let telemetry = TelemetryEntity(context: context)
@ -644,6 +668,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
let content = UNMutableNotificationContent()
content.title = "Critically Low Battery!"
content.body = "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining."
content.userInfo["target"] = "node"
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let uuidString = UUID().uuidString
let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger)
@ -659,7 +684,6 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
}
// Update our live activity if there is one running, not available on mac iOS >= 16.2
#if !targetEnvironment(macCatalyst)
if #available(iOS 16.2, *) {
let oneMinuteLater = Calendar.current.date(byAdding: .minute, value: (Int(1) ), to: Date())!
let date = Date.now...oneMinuteLater
@ -675,7 +699,6 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
print("Updated live activity.")
}
}
}
#endif
}
} catch {
@ -703,11 +726,13 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
let newMessage = MessageEntity(context: context)
newMessage.messageId = Int64(packet.id)
newMessage.messageTimestamp = Int32(bitPattern: packet.rxTime)
newMessage.receivedTimestamp = Int32(Date().timeIntervalSince1970)
newMessage.receivedACK = false
newMessage.snr = packet.rxSnr
newMessage.rssi = packet.rxRssi
newMessage.isEmoji = packet.decoded.emoji == 1
newMessage.channel = Int32(packet.channel)
newMessage.portNum = Int32(packet.decoded.portnum.rawValue)
if packet.decoded.replyID > 0 {
newMessage.replyID = Int64(packet.decoded.replyID)
@ -734,7 +759,6 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
messageSaved = true
if messageSaved {
if newMessage.fromUser != nil && newMessage.toUser != nil && !(newMessage.fromUser?.mute ?? false) {
// Create an iOS Notification for the received DM message and schedule it immediately
let manager = LocalNotificationManager()
@ -743,7 +767,9 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
id: ("notification.id.\(newMessage.messageId)"),
title: "\(newMessage.fromUser?.longName ?? "unknown".localized)",
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")",
content: messageText)
content: messageText,
target: "message"
)
]
manager.schedule()
print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)")
@ -761,7 +787,6 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
if channel.index == newMessage.channel {
context.refresh(channel, mergeChanges: true)
}
if channel.index == newMessage.channel && !channel.mute {
// Create an iOS Notification for the received private channel message and schedule it immediately
let manager = LocalNotificationManager()
@ -770,7 +795,8 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
id: ("notification.id.\(newMessage.messageId)"),
title: "\(newMessage.fromUser?.longName ?? "unknown".localized)",
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")",
content: messageText)
content: messageText,
target: "message")
]
manager.schedule()
print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)")
@ -825,7 +851,21 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
waypoint.created = Date()
do {
try context.save()
print("💾 Updated Node Waypoint App Packet For: \(waypoint.id)")
print("💾 Added Node Waypoint App Packet For: \(waypoint.id)")
let manager = LocalNotificationManager()
let icon = String(UnicodeScalar(Int(waypoint.icon)) ?? "📍")
let latitude = Double(waypoint.latitudeI) / 1e7
let longitude = Double(waypoint.longitudeI) / 1e7
manager.notifications = [
Notification(
id: ("notification.id.\(waypoint.id)"),
title: "New Waypoint Received",
subtitle: "\(icon) \(waypoint.name ?? "Dropped Pin")",
content: "\(waypoint.longDescription ?? "\(latitude), \(longitude)")",
target: "map"
)
]
manager.schedule()
} catch {
context.rollback()
let nsError = error as NSError

View file

@ -16,78 +16,42 @@ protocol MqttClientProxyManagerDelegate: AnyObject {
}
class MqttClientProxyManager {
enum ConnectionStatus {
case connecting
case connected
case disconnecting
case disconnected
case error
case none
}
enum MqttQos: Int {
case atMostOnce = 0
case atLeastOnce = 1
case exactlyOnce = 2
}
// Singleton Instance
static let shared = MqttClientProxyManager()
private static let defaultKeepAliveInterval: Int32 = 60
weak var delegate: MqttClientProxyManagerDelegate?
var status = ConnectionStatus.none
var mqttClientProxy: CocoaMQTT?
var topic = "msh/2/c"
private init() {
}
func connectFromConfigSettings(node: NodeInfoEntity) {
let defaultServerAddress = "mqtt.meshtastic.org"
let useSsl = node.mqttConfig?.tlsEnabled == true
var defaultServerPort = useSsl ? 8883 : 1883
var host = node.mqttConfig?.address
if host == nil || host!.isEmpty {
host = defaultServerAddress
}
else if host != nil && host!.contains(":") {
} else if host != nil && host!.contains(":") {
host = host!.components(separatedBy: ":")[0]
defaultServerPort = Int(host!.components(separatedBy: ":")[1])!
}
if let host = host {
let port = defaultServerPort
let username = node.mqttConfig?.username
let password = node.mqttConfig?.password
let root = node.mqttConfig?.root?.count ?? 0 > 0 ? node.mqttConfig?.root : "msh"
let prefix = root! + "/2/c"
topic = prefix + "/#"
let qos = CocoaMQTTQoS(rawValue :UInt8(1))!
let qos = CocoaMQTTQoS(rawValue: UInt8(1))!
connect(host: host, port: port, useSsl: useSsl, username: username, password: password, topic: topic, qos: qos, cleanSession: true)
}
}
func connect(host: String, port: Int, useSsl: Bool, username: String?, password: String?, topic: String?, qos: CocoaMQTTQoS, cleanSession: Bool) {
guard !host.isEmpty else {
delegate?.onMqttDisconnected()
return
}
status = .connecting
let clientId = "MeshtasticAppleMqttProxy-" + String(ProcessInfo().processIdentifier)
mqttClientProxy = CocoaMQTT(clientID: clientId, host: host, port: UInt16(port))
if let mqttClient = mqttClientProxy {
mqttClient.enableSSL = useSsl
mqttClient.allowUntrustCACertificate = true
mqttClient.username = username
@ -103,48 +67,33 @@ class MqttClientProxyManager {
let success = mqttClient.connect()
if !success {
delegate?.onMqttError(message: "Mqtt connect error")
status = .error
}
} else {
delegate?.onMqttError(message: "Mqtt initialization error")
status = .error
}
}
func subscribe(topic: String, qos: MqttQos) {
func subscribe(topic: String, qos: CocoaMQTTQoS) {
print("📲 MQTT Client Proxy subscribed to: " + topic)
let qos = CocoaMQTTQoS(rawValue :UInt8(qos.rawValue))!
mqttClientProxy?.subscribe(topic, qos: qos)
}
func unsubscribe(topic: String) {
mqttClientProxy?.unsubscribe(topic)
print("📲 MQTT Client Proxy unsubscribe for: " + topic)
}
func publish(message: String, topic: String, qos: MqttQos) {
let qos = CocoaMQTTQoS(rawValue :UInt8(qos.rawValue))!
func publish(message: String, topic: String, qos: CocoaMQTTQoS) {
mqttClientProxy?.publish(topic, withString: message, qos: qos)
print("📲 MQTT Client Proxy publish for: " + topic)
}
func disconnect() {
//MqttSettings.shared.isConnected = false
if let client = mqttClientProxy {
status = .disconnecting
client.disconnect()
print("📲 MQTT Client Proxy Disconnected")
} else {
status = .disconnected
}
}
}
extension MqttClientProxyManager: CocoaMQTTDelegate {
func mqtt(_ mqtt: CocoaMQTT, didConnectAck ack: CocoaMQTTConnAck) {
print("📲 MQTT Client Proxy didConnectAck: \(ack)")
if ack == .accept {
delegate?.onMqttConnected()
@ -169,29 +118,20 @@ extension MqttClientProxyManager: CocoaMQTTDelegate {
}
print(errorDescription)
delegate?.onMqttError(message: errorDescription)
//self.disconnect() // Stop reconnecting
//mqttSettings.isConnected = false // Disable automatic connect on start
self.disconnect()
}
self.status = ack == .accept ? ConnectionStatus.connected : ConnectionStatus.error // Set AFTER sending onMqttError (so the delegate can detect that was an error while establishing connection)
}
func mqttDidDisconnect(_ mqtt: CocoaMQTT, withError err: Error?) {
print("mqttDidDisconnect: \(err?.localizedDescription ?? "")")
if let error = err, status == .connecting {
if let error = err {
delegate?.onMqttError(message: error.localizedDescription)
}
status = err == nil ? .disconnected : .error
delegate?.onMqttDisconnected()
}
func mqtt(_ mqtt: CocoaMQTT, didPublishMessage message: CocoaMQTTMessage, id: UInt16) {
print("📲 MQTT Client Proxy didPublishMessage from MqttClientProxyManager: \(message)")
}
func mqtt(_ mqtt: CocoaMQTT, didPublishAck id: UInt16) {
print("📲 MQTT Client Proxy didPublishAck from MqttClientProxyManager: \(id)")
}
@ -200,19 +140,15 @@ extension MqttClientProxyManager: CocoaMQTTDelegate {
delegate?.onMqttMessageReceived(message: message)
print("📲 MQTT Client Proxy message received on topic: \(message.topic)")
}
func mqtt(_ mqtt: CocoaMQTT, didSubscribeTopics success: NSDictionary, failed: [String]) {
print("📲 MQTT Client Proxy didSubscribeTopics: \(success.allKeys.count) topics. failed: \(failed.count) topics")
}
func mqtt(_ mqtt: CocoaMQTT, didUnsubscribeTopics topics: [String]) {
print("didUnsubscribeTopics: \(topics.joined(separator: ", "))")
}
func mqttDidPing(_ mqtt: CocoaMQTT) {
print("📲 MQTT Client Proxy mqttDidPing")
}
func mqttDidReceivePong(_ mqtt: CocoaMQTT) {
print("📲 MQTT Client Proxy mqttDidReceivePong")
}

View file

@ -9,11 +9,9 @@ import Foundation
import Network
class NetworkManager {
static let shared = NetworkManager()
// MARK: - Public methods
func runIfNetwork(completion: @escaping ()->() ) {
// MARK: Public methods
func runIfNetwork(completion: @escaping () -> Void ) {
let pathMonitor = NWPathMonitor()
pathMonitor.pathUpdateHandler = {
guard $0.status == .satisfied else {
@ -26,5 +24,4 @@ class NetworkManager {
}
pathMonitor.start(queue: DispatchQueue.global(qos: .background))
}
}

View file

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

View file

@ -0,0 +1,346 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22F82" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<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" optional="YES" attributeType="Boolean" 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="psk" optional="YES" attributeType="Binary"/>
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="uplinkEnabled" attributeType="Boolean" 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="doubleTapAsButtonPress" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="isManaged" optional="YES" attributeType="Boolean" defaultValueString="NO" 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"/>
<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"/>
<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="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="usePWM" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="LocationEntity" representedClassName="LocationEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="Integer 32" usesScalarValueType="YES"/>
<attribute name="latitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitude" optional="YES" 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="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="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="password" optional="YES" attributeType="String" maxValueString="30"/>
<attribute name="proxyToClientEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="root" optional="YES" attributeType="String" defaultValueString="msh"/>
<attribute name="tlsEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="username" optional="YES" attributeType="String" maxValueString="30"/>
<relationship name="mqttConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="mqttConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="MyInfoEntity" representedClassName="MyInfoEntity" syncable="YES" codeGenerationType="class">
<attribute name="adminIndex" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="bleName" optional="YES" attributeType="String"/>
<attribute name="minAppVersion" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="myNodeNum" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="peripheralId" optional="YES" attributeType="String"/>
<attribute name="rebootCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="channels" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="ChannelEntity" inverseName="myInfoChannel" inverseEntity="ChannelEntity"/>
<relationship name="myInfoNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="myInfo" inverseEntity="NodeInfoEntity"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="myNodeNum"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="NetworkConfigEntity" representedClassName="NetworkConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="dns" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ethEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="gateway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ip" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ntpServer" optional="YES" attributeType="String"/>
<attribute name="subnet" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wifiEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="wifiMode" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="wifiPsk" optional="YES" attributeType="String" minValueString="0" maxValueString="60"/>
<attribute name="wifiSsid" optional="YES" attributeType="String" minValueString="0" maxValueString="30"/>
<relationship name="networkConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="networkConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="NodeInfoEntity" representedClassName="NodeInfoEntity" syncable="YES" codeGenerationType="class">
<attribute name="bleName" optional="YES" attributeType="String"/>
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="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"/>
<relationship name="bluetoothConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="BluetoothConfigEntity" inverseName="bluetoothConfigNode" inverseEntity="BluetoothConfigEntity"/>
<relationship name="cannedMessageConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CannedMessageConfigEntity" inverseName="cannedMessagesConfigNode" inverseEntity="CannedMessageConfigEntity"/>
<relationship name="detectionSensorConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DetectionSensorConfigEntity" inverseName="detectionSensorConfigNode" inverseEntity="DetectionSensorConfigEntity"/>
<relationship name="deviceConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceConfigEntity" inverseName="deviceConfigNode" inverseEntity="DeviceConfigEntity"/>
<relationship name="displayConfig" optional="YES" maxCount="1" deletionRule="Nullify" 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="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="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="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="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="userNode" inverseEntity="UserEntity"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="num"/>
</uniquenessConstraint>
</uniquenessConstraints>
</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="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="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="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 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="date" 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" maxCount="1" deletionRule="Nullify" 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="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="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"/>
<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="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="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="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="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="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"/>
</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"/>
</entity>
</model>

View file

@ -0,0 +1,355 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22F82" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<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" optional="YES" attributeType="Boolean" 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="psk" optional="YES" attributeType="Binary"/>
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="uplinkEnabled" attributeType="Boolean" 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="doubleTapAsButtonPress" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="isManaged" optional="YES" attributeType="Boolean" defaultValueString="NO" 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"/>
<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"/>
<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="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="usePWM" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="LocationEntity" representedClassName="LocationEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="Integer 32" usesScalarValueType="YES"/>
<attribute name="latitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitude" optional="YES" 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="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="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="password" optional="YES" attributeType="String" maxValueString="30"/>
<attribute name="proxyToClientEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="root" optional="YES" attributeType="String" defaultValueString="msh"/>
<attribute name="tlsEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="username" optional="YES" attributeType="String" maxValueString="30"/>
<relationship name="mqttConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="mqttConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="MyInfoEntity" representedClassName="MyInfoEntity" syncable="YES" codeGenerationType="class">
<attribute name="adminIndex" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="bleName" optional="YES" attributeType="String"/>
<attribute name="minAppVersion" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="myNodeNum" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="peripheralId" optional="YES" attributeType="String"/>
<attribute name="rebootCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="channels" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="ChannelEntity" inverseName="myInfoChannel" inverseEntity="ChannelEntity"/>
<relationship name="myInfoNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="myInfo" inverseEntity="NodeInfoEntity"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="myNodeNum"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="NetworkConfigEntity" representedClassName="NetworkConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="dns" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ethEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="gateway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ip" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ntpServer" optional="YES" attributeType="String"/>
<attribute name="subnet" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wifiEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="wifiMode" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="wifiPsk" optional="YES" attributeType="String" minValueString="0" maxValueString="60"/>
<attribute name="wifiSsid" optional="YES" attributeType="String" minValueString="0" maxValueString="30"/>
<relationship name="networkConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="networkConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="NodeInfoEntity" representedClassName="NodeInfoEntity" syncable="YES" codeGenerationType="class">
<attribute name="bleName" optional="YES" attributeType="String"/>
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="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"/>
<relationship name="bluetoothConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="BluetoothConfigEntity" inverseName="bluetoothConfigNode" inverseEntity="BluetoothConfigEntity"/>
<relationship name="cannedMessageConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CannedMessageConfigEntity" inverseName="cannedMessagesConfigNode" inverseEntity="CannedMessageConfigEntity"/>
<relationship name="detectionSensorConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DetectionSensorConfigEntity" inverseName="detectionSensorConfigNode" inverseEntity="DetectionSensorConfigEntity"/>
<relationship name="deviceConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceConfigEntity" inverseName="deviceConfigNode" inverseEntity="DeviceConfigEntity"/>
<relationship name="displayConfig" optional="YES" maxCount="1" deletionRule="Nullify" 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="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="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="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="userNode" inverseEntity="UserEntity"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="num"/>
</uniquenessConstraint>
</uniquenessConstraints>
</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="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="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="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 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="date" 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" maxCount="1" deletionRule="Nullify" 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="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="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"/>
<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="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="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="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="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="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"/>
</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"/>
</entity>
</model>

View file

@ -5,7 +5,7 @@ import CoreData
@main
struct MeshtasticAppleApp: App {
@UIApplicationDelegateAdaptor(MeshtasticAppDelegate.self) var appDelegate
let persistenceController = PersistenceController.shared
@ObservedObject private var bleManager: BLEManager = BLEManager()
@Environment(\.scenePhase) var scenePhase
@ -13,10 +13,10 @@ struct MeshtasticAppleApp: App {
@State var saveChannels = false
@State var incomingUrl: URL?
@State var channelSettings: String?
@StateObject var appState = AppState.shared
var body: some Scene {
WindowGroup {
ContentView()
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(bleManager)
.sheet(isPresented: $saveChannels) {
@ -45,7 +45,6 @@ struct MeshtasticAppleApp: App {
print("Some sort of URL was received \(url)")
self.incomingUrl = url
if url.absoluteString.lowercased().contains("meshtastic.org/e/#") {
if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") {
self.channelSettings = components.last!
@ -115,5 +114,11 @@ struct MeshtasticAppleApp: App {
print("💥 Apple must have changed something")
}
}
}
}
}
class AppState: ObservableObject {
static let shared = AppState()
@Published var tabSelection: Tab = .ble
}

View file

@ -0,0 +1,31 @@
//
// MeshtasticAppDelegate.swift
// Meshtastic
//
// Created by Ben on 8/20/23.
//
import SwiftUI
class MeshtasticAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("App launched!")
UNUserNotificationCenter.current().delegate = self
return true
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
}
// This method is called when user clicked on the notification
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
let targetValue = userInfo["target"] as? String
if targetValue == "map" {
AppState.shared.tabSelection = Tab.map
} else if targetValue == "message" {
AppState.shared.tabSelection = Tab.messages
} else if targetValue == "node" {
AppState.shared.tabSelection = Tab.nodes
}
completionHandler()
}
}

View file

@ -12,17 +12,17 @@ struct Peripheral: Identifiable {
var lastUpdate: Date
var peripheral: CBPeripheral
init(id: String, num: Int64, name: String, shortName: String, longName: String, firmwareVersion: String, rssi: Int, lastUpdate: Date, peripheral: CBPeripheral) {
self.id = id
self.num = num
self.name = name
self.shortName = shortName
self.longName = longName
self.firmwareVersion = firmwareVersion
self.rssi = rssi
self.lastUpdate = lastUpdate
self.peripheral = peripheral
}
// init(id: String, num: Int64, name: String, shortName: String, longName: String, firmwareVersion: String, rssi: Int, lastUpdate: Date, peripheral: CBPeripheral) {
// self.id = id
// self.num = num
// self.name = name
// self.shortName = shortName
// self.longName = longName
// self.firmwareVersion = firmwareVersion
// self.rssi = rssi
// self.lastUpdate = lastUpdate
// self.peripheral = peripheral
// }
func getSignalStrength() -> BLESignalStrength {
if NSNumber(value: rssi).compare(NSNumber(-65)) == ComparisonResult.orderedDescending {

View file

@ -0,0 +1,20 @@
//
// MessageEntityExtension.swift
// Meshtastic
//
// Created by Ben on 8/22/23.
//
import Foundation
import CoreData
import CoreLocation
import MapKit
import SwiftUI
extension MessageEntity {
var timestamp: Date {
let time = messageTimestamp <= 0 ? receivedTimestamp : messageTimestamp
return Date(timeIntervalSince1970: TimeInterval(time))
}
}

View file

@ -50,7 +50,6 @@ extension PositionEntity {
var annotaton: MKPointAnnotation {
let pointAnn = MKPointAnnotation()
if nodeCoordinate != nil {
pointAnn.coordinate = nodeCoordinate!
}

View file

@ -60,3 +60,21 @@ public func getWaypoint(id: Int64, context: NSManagedObjectContext) -> WaypointE
}
return WaypointEntity(context: context)
}
public func getDetectionSensorMessages(nodeNum: Int64?, context: NSManagedObjectContext) -> [MessageEntity] {
let fetchDetectionMessagesPredicate: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MessageEntity")
fetchDetectionMessagesPredicate.predicate = NSPredicate(format: "portNum == %d", Int32(PortNum.detectionSensorApp.rawValue))
do {
let fetched = try context.fetch(fetchDetectionMessagesPredicate) as? [MessageEntity] ?? []
if nodeNum == nil {
return fetched.reversed()
}
return fetched.filter { message in
return message.fromUser?.num == nodeNum!
}.reversed()
} catch {
return []
}
}

View file

@ -118,11 +118,9 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime)))
newNode.snr = packet.rxSnr
newNode.rssi = packet.rxRssi
if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) {
newNode.channel = Int32(nodeInfoMessage.channel)
}
if let newUserMessage = try? User(serializedData: packet.decoded.payload) {
let newUser = UserEntity(context: context)
newUser.userId = newUserMessage.id
@ -132,6 +130,21 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased()
newNode.user = newUser
}
let myInfoEntity = MyInfoEntity(context: context)
myInfoEntity.myNodeNum = Int64(packet.from)
myInfoEntity.rebootCount = 0
do {
try context.save()
print("💾 Saved a new myInfo for node number: \(String(packet.from))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Inserting New Core Data MyInfoEntity: \(nsError)")
}
newNode.myInfo = myInfoEntity
newNode.objectWillChange.send()
} else {
// Update an existing node
fetchedNode[0].id = Int64(packet.from)
@ -141,7 +154,6 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
fetchedNode[0].rssi = packet.rxRssi
if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) {
fetchedNode[0].channel = Int32(nodeInfoMessage.channel)
if nodeInfoMessage.hasDeviceMetrics {
let telemetry = TelemetryEntity(context: context)
@ -650,6 +662,67 @@ func upsertCannedMessagesModuleConfigPacket(config: Meshtastic.ModuleConfig.Cann
}
}
func upsertDetectionSensorModuleConfigPacket(config: Meshtastic.ModuleConfig.DetectionSensorConfig, nodeNum: Int64, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.detectionsensor.config %@".localized, String(nodeNum))
MeshLogger.log("🕵️ \(logString)")
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
do {
guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else {
return
}
// Found a node, save Detection Sensor Config
if !fetchedNode.isEmpty {
if fetchedNode[0].detectionSensorConfig == nil {
let newConfig = DetectionSensorConfigEntity(context: context)
newConfig.enabled = config.enabled
newConfig.sendBell = config.sendBell
newConfig.name = config.name
newConfig.monitorPin = Int32(config.monitorPin)
newConfig.detectionTriggeredHigh = config.detectionTriggeredHigh
newConfig.usePullup = config.usePullup
newConfig.minimumBroadcastSecs = Int32(config.minimumBroadcastSecs)
newConfig.stateBroadcastSecs = Int32(config.stateBroadcastSecs)
fetchedNode[0].detectionSensorConfig = newConfig
} else {
fetchedNode[0].detectionSensorConfig?.enabled = config.enabled
fetchedNode[0].detectionSensorConfig?.sendBell = config.sendBell
fetchedNode[0].detectionSensorConfig?.name = config.name
fetchedNode[0].detectionSensorConfig?.monitorPin = Int32(config.monitorPin)
fetchedNode[0].detectionSensorConfig?.usePullup = config.usePullup
fetchedNode[0].detectionSensorConfig?.detectionTriggeredHigh = config.detectionTriggeredHigh
fetchedNode[0].detectionSensorConfig?.minimumBroadcastSecs = Int32(config.minimumBroadcastSecs)
fetchedNode[0].detectionSensorConfig?.stateBroadcastSecs = Int32(config.stateBroadcastSecs)
}
do {
try context.save()
print("💾 Updated Detection Sensor Module Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data DetectionSensorConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Detection Sensor Module Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data DetectionSensorConfigEntity failed: \(nsError)")
}
}
func upsertExternalNotificationModuleConfigPacket(config: Meshtastic.ModuleConfig.ExternalNotificationConfig, nodeNum: Int64, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.externalnotification.config %@".localized, String(nodeNum))
@ -919,6 +992,56 @@ func upsertSerialModuleConfigPacket(config: Meshtastic.ModuleConfig.SerialConfig
}
}
func upsertStoreForwardModuleConfigPacket(config: Meshtastic.ModuleConfig.StoreForwardConfig, nodeNum: Int64, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.storeforward.config %@".localized, String(nodeNum))
MeshLogger.log("📬 \(logString)")
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
do {
guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else {
return
}
// Found a node, save Store & Forward Sensor Config
if !fetchedNode.isEmpty {
if fetchedNode[0].storeForwardConfig == nil {
let newConfig = StoreForwardConfigEntity(context: context)
newConfig.enabled = config.enabled
newConfig.heartbeat = config.heartbeat
newConfig.records = Int32(config.records)
newConfig.historyReturnMax = Int32(config.historyReturnMax)
newConfig.historyReturnWindow = Int32(config.historyReturnWindow)
fetchedNode[0].storeForwardConfig = newConfig
} else {
fetchedNode[0].storeForwardConfig?.enabled = config.enabled
fetchedNode[0].storeForwardConfig?.heartbeat = config.heartbeat
fetchedNode[0].storeForwardConfig?.records = Int32(config.records)
fetchedNode[0].storeForwardConfig?.historyReturnMax = Int32(config.historyReturnMax)
fetchedNode[0].storeForwardConfig?.historyReturnWindow = Int32(config.historyReturnWindow)
}
do {
try context.save()
print("💾 Updated Store & Forward Module Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data StoreForwardConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Store & Forward Module Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data DetectionSensorConfigEntity failed: \(nsError)")
}
}
func upsertTelemetryModuleConfigPacket(config: Meshtastic.ModuleConfig.TelemetryConfig, nodeNum: Int64, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.telemetry.config %@".localized, String(nodeNum))

View file

@ -734,6 +734,18 @@ struct AdminMessage {
///
/// TODO: REPLACE
case remotehardwareConfig // = 8
///
/// TODO: REPLACE
case neighborinfoConfig // = 9
///
/// TODO: REPLACE
case ambientlightingConfig // = 10
///
/// TODO: REPLACE
case detectionsensorConfig // = 11
case UNRECOGNIZED(Int)
init() {
@ -751,6 +763,9 @@ struct AdminMessage {
case 6: self = .cannedmsgConfig
case 7: self = .audioConfig
case 8: self = .remotehardwareConfig
case 9: self = .neighborinfoConfig
case 10: self = .ambientlightingConfig
case 11: self = .detectionsensorConfig
default: self = .UNRECOGNIZED(rawValue)
}
}
@ -766,6 +781,9 @@ struct AdminMessage {
case .cannedmsgConfig: return 6
case .audioConfig: return 7
case .remotehardwareConfig: return 8
case .neighborinfoConfig: return 9
case .ambientlightingConfig: return 10
case .detectionsensorConfig: return 11
case .UNRECOGNIZED(let i): return i
}
}
@ -802,6 +820,9 @@ extension AdminMessage.ModuleConfigType: CaseIterable {
.cannedmsgConfig,
.audioConfig,
.remotehardwareConfig,
.neighborinfoConfig,
.ambientlightingConfig,
.detectionsensorConfig,
]
}
@ -1412,6 +1433,9 @@ extension AdminMessage.ModuleConfigType: SwiftProtobuf._ProtoNameProviding {
6: .same(proto: "CANNEDMSG_CONFIG"),
7: .same(proto: "AUDIO_CONFIG"),
8: .same(proto: "REMOTEHARDWARE_CONFIG"),
9: .same(proto: "NEIGHBORINFO_CONFIG"),
10: .same(proto: "AMBIENTLIGHTING_CONFIG"),
11: .same(proto: "DETECTIONSENSOR_CONFIG"),
]
}

View file

@ -368,7 +368,7 @@ struct Config {
var broadcastSmartMinimumDistance: UInt32 = 0
///
/// The minumum number of seconds (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled
/// The minimum number of seconds (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled
var broadcastSmartMinimumIntervalSecs: UInt32 = 0
var unknownFields = SwiftProtobuf.UnknownStorage()
@ -506,13 +506,6 @@ struct Config {
/// 0 for default of 1 minute
var waitBluetoothSecs: UInt32 = 0
///
/// Mesh Super Deep Sleep Timeout Seconds
/// While in Light Sleep if this value is exceeded we will lower into super deep sleep
/// for sds_secs (default 1 year) or a button press
/// 0 for default of two hours, MAXUINT for disabled
var meshSdsTimeoutSecs: UInt32 = 0
///
/// Super Deep Sleep Seconds
/// While in Light Sleep if mesh_sds_timeout_secs is exceeded we will lower into super deep sleep
@ -1805,7 +1798,6 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple
2: .standard(proto: "on_battery_shutdown_after_secs"),
3: .standard(proto: "adc_multiplier_override"),
4: .standard(proto: "wait_bluetooth_secs"),
5: .standard(proto: "mesh_sds_timeout_secs"),
6: .standard(proto: "sds_secs"),
7: .standard(proto: "ls_secs"),
8: .standard(proto: "min_wake_secs"),
@ -1822,7 +1814,6 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple
case 2: try { try decoder.decodeSingularUInt32Field(value: &self.onBatteryShutdownAfterSecs) }()
case 3: try { try decoder.decodeSingularFloatField(value: &self.adcMultiplierOverride) }()
case 4: try { try decoder.decodeSingularUInt32Field(value: &self.waitBluetoothSecs) }()
case 5: try { try decoder.decodeSingularUInt32Field(value: &self.meshSdsTimeoutSecs) }()
case 6: try { try decoder.decodeSingularUInt32Field(value: &self.sdsSecs) }()
case 7: try { try decoder.decodeSingularUInt32Field(value: &self.lsSecs) }()
case 8: try { try decoder.decodeSingularUInt32Field(value: &self.minWakeSecs) }()
@ -1845,9 +1836,6 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple
if self.waitBluetoothSecs != 0 {
try visitor.visitSingularUInt32Field(value: self.waitBluetoothSecs, fieldNumber: 4)
}
if self.meshSdsTimeoutSecs != 0 {
try visitor.visitSingularUInt32Field(value: self.meshSdsTimeoutSecs, fieldNumber: 5)
}
if self.sdsSecs != 0 {
try visitor.visitSingularUInt32Field(value: self.sdsSecs, fieldNumber: 6)
}
@ -1868,7 +1856,6 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple
if lhs.onBatteryShutdownAfterSecs != rhs.onBatteryShutdownAfterSecs {return false}
if lhs.adcMultiplierOverride != rhs.adcMultiplierOverride {return false}
if lhs.waitBluetoothSecs != rhs.waitBluetoothSecs {return false}
if lhs.meshSdsTimeoutSecs != rhs.meshSdsTimeoutSecs {return false}
if lhs.sdsSecs != rhs.sdsSecs {return false}
if lhs.lsSecs != rhs.lsSecs {return false}
if lhs.minWakeSecs != rhs.minWakeSecs {return false}

View file

@ -108,14 +108,6 @@ struct DeviceState {
/// Clears the value of `owner`. Subsequent reads from it will return its default value.
mutating func clearOwner() {_uniqueStorage()._owner = nil}
///
/// Deprecated in 2.1.x
/// Old node_db. See NodeInfoLite node_db_lite
var nodeDb: [NodeInfo] {
get {return _storage._nodeDb}
set {_uniqueStorage()._nodeDb = newValue}
}
///
/// Received packets saved for delivery to the phone
var receiveQueue: [MeshPacket] {
@ -446,7 +438,6 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
2: .standard(proto: "my_node"),
3: .same(proto: "owner"),
4: .standard(proto: "node_db"),
5: .standard(proto: "receive_queue"),
8: .same(proto: "version"),
7: .standard(proto: "rx_text_message"),
@ -460,7 +451,6 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
fileprivate class _StorageClass {
var _myNode: MyNodeInfo? = nil
var _owner: User? = nil
var _nodeDb: [NodeInfo] = []
var _receiveQueue: [MeshPacket] = []
var _version: UInt32 = 0
var _rxTextMessage: MeshPacket? = nil
@ -477,7 +467,6 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
init(copying source: _StorageClass) {
_myNode = source._myNode
_owner = source._owner
_nodeDb = source._nodeDb
_receiveQueue = source._receiveQueue
_version = source._version
_rxTextMessage = source._rxTextMessage
@ -506,7 +495,6 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
switch fieldNumber {
case 2: try { try decoder.decodeSingularMessageField(value: &_storage._myNode) }()
case 3: try { try decoder.decodeSingularMessageField(value: &_storage._owner) }()
case 4: try { try decoder.decodeRepeatedMessageField(value: &_storage._nodeDb) }()
case 5: try { try decoder.decodeRepeatedMessageField(value: &_storage._receiveQueue) }()
case 7: try { try decoder.decodeSingularMessageField(value: &_storage._rxTextMessage) }()
case 8: try { try decoder.decodeSingularUInt32Field(value: &_storage._version) }()
@ -533,9 +521,6 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
try { if let v = _storage._owner {
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
} }()
if !_storage._nodeDb.isEmpty {
try visitor.visitRepeatedMessageField(value: _storage._nodeDb, fieldNumber: 4)
}
if !_storage._receiveQueue.isEmpty {
try visitor.visitRepeatedMessageField(value: _storage._receiveQueue, fieldNumber: 5)
}
@ -571,7 +556,6 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
let rhs_storage = _args.1
if _storage._myNode != rhs_storage._myNode {return false}
if _storage._owner != rhs_storage._owner {return false}
if _storage._nodeDb != rhs_storage._nodeDb {return false}
if _storage._receiveQueue != rhs_storage._receiveQueue {return false}
if _storage._version != rhs_storage._version {return false}
if _storage._rxTextMessage != rhs_storage._rxTextMessage {return false}

View file

@ -233,6 +233,28 @@ struct LocalModuleConfig {
/// Clears the value of `neighborInfo`. Subsequent reads from it will return its default value.
mutating func clearNeighborInfo() {_uniqueStorage()._neighborInfo = nil}
///
/// The part of the config that is specific to the Ambient Lighting module
var ambientLighting: ModuleConfig.AmbientLightingConfig {
get {return _storage._ambientLighting ?? ModuleConfig.AmbientLightingConfig()}
set {_uniqueStorage()._ambientLighting = newValue}
}
/// Returns true if `ambientLighting` has been explicitly set.
var hasAmbientLighting: Bool {return _storage._ambientLighting != nil}
/// Clears the value of `ambientLighting`. Subsequent reads from it will return its default value.
mutating func clearAmbientLighting() {_uniqueStorage()._ambientLighting = nil}
///
/// The part of the config that is specific to the Detection Sensor module
var detectionSensor: ModuleConfig.DetectionSensorConfig {
get {return _storage._detectionSensor ?? ModuleConfig.DetectionSensorConfig()}
set {_uniqueStorage()._detectionSensor = newValue}
}
/// Returns true if `detectionSensor` has been explicitly set.
var hasDetectionSensor: Bool {return _storage._detectionSensor != nil}
/// Clears the value of `detectionSensor`. Subsequent reads from it will return its default value.
mutating func clearDetectionSensor() {_uniqueStorage()._detectionSensor = nil}
///
/// A version integer used to invalidate old save files when we make
/// incompatible changes This integer is set at build time and is private to
@ -395,6 +417,8 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
9: .same(proto: "audio"),
10: .standard(proto: "remote_hardware"),
11: .standard(proto: "neighbor_info"),
12: .standard(proto: "ambient_lighting"),
13: .standard(proto: "detection_sensor"),
8: .same(proto: "version"),
]
@ -409,6 +433,8 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
var _audio: ModuleConfig.AudioConfig? = nil
var _remoteHardware: ModuleConfig.RemoteHardwareConfig? = nil
var _neighborInfo: ModuleConfig.NeighborInfoConfig? = nil
var _ambientLighting: ModuleConfig.AmbientLightingConfig? = nil
var _detectionSensor: ModuleConfig.DetectionSensorConfig? = nil
var _version: UInt32 = 0
static let defaultInstance = _StorageClass()
@ -426,6 +452,8 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
_audio = source._audio
_remoteHardware = source._remoteHardware
_neighborInfo = source._neighborInfo
_ambientLighting = source._ambientLighting
_detectionSensor = source._detectionSensor
_version = source._version
}
}
@ -456,6 +484,8 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
case 9: try { try decoder.decodeSingularMessageField(value: &_storage._audio) }()
case 10: try { try decoder.decodeSingularMessageField(value: &_storage._remoteHardware) }()
case 11: try { try decoder.decodeSingularMessageField(value: &_storage._neighborInfo) }()
case 12: try { try decoder.decodeSingularMessageField(value: &_storage._ambientLighting) }()
case 13: try { try decoder.decodeSingularMessageField(value: &_storage._detectionSensor) }()
default: break
}
}
@ -501,6 +531,12 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
try { if let v = _storage._neighborInfo {
try visitor.visitSingularMessageField(value: v, fieldNumber: 11)
} }()
try { if let v = _storage._ambientLighting {
try visitor.visitSingularMessageField(value: v, fieldNumber: 12)
} }()
try { if let v = _storage._detectionSensor {
try visitor.visitSingularMessageField(value: v, fieldNumber: 13)
} }()
}
try unknownFields.traverse(visitor: &visitor)
}
@ -520,6 +556,8 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
if _storage._audio != rhs_storage._audio {return false}
if _storage._remoteHardware != rhs_storage._remoteHardware {return false}
if _storage._neighborInfo != rhs_storage._neighborInfo {return false}
if _storage._ambientLighting != rhs_storage._ambientLighting {return false}
if _storage._detectionSensor != rhs_storage._detectionSensor {return false}
if _storage._version != rhs_storage._version {return false}
return true
}

File diff suppressed because it is too large Load diff

View file

@ -194,6 +194,16 @@ struct ModuleConfig {
set {payloadVariant = .ambientLighting(newValue)}
}
///
/// TODO: REPLACE
var detectionSensor: ModuleConfig.DetectionSensorConfig {
get {
if case .detectionSensor(let v)? = payloadVariant {return v}
return ModuleConfig.DetectionSensorConfig()
}
set {payloadVariant = .detectionSensor(newValue)}
}
var unknownFields = SwiftProtobuf.UnknownStorage()
///
@ -232,6 +242,9 @@ struct ModuleConfig {
///
/// TODO: REPLACE
case ambientLighting(ModuleConfig.AmbientLightingConfig)
///
/// TODO: REPLACE
case detectionSensor(ModuleConfig.DetectionSensorConfig)
#if !swift(>=4.1)
static func ==(lhs: ModuleConfig.OneOf_PayloadVariant, rhs: ModuleConfig.OneOf_PayloadVariant) -> Bool {
@ -283,6 +296,10 @@ struct ModuleConfig {
guard case .ambientLighting(let l) = lhs, case .ambientLighting(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.detectionSensor, .detectionSensor): return {
guard case .detectionSensor(let l) = lhs, case .detectionSensor(let r) = rhs else { preconditionFailure() }
return l == r
}()
default: return false
}
}
@ -392,6 +409,57 @@ struct ModuleConfig {
init() {}
}
///
/// Detection Sensor Module Config
struct DetectionSensorConfig {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
///
/// Whether the Module is enabled
var enabled: Bool = false
///
/// Interval in seconds of how often we can send a message to the mesh when a state change is detected
var minimumBroadcastSecs: UInt32 = 0
///
/// Interval in seconds of how often we should send a message to the mesh with the current state regardless of changes
/// When set to 0, only state changes will be broadcasted
/// Works as a sort of status heartbeat for peace of mind
var stateBroadcastSecs: UInt32 = 0
///
/// Send ASCII bell with alert message
/// Useful for triggering ext. notification on bell
var sendBell: Bool = false
///
/// Friendly name used to format message sent to mesh
/// Example: A name "Motion" would result in a message "Motion detected"
/// Maximum length of 20 characters
var name: String = String()
///
/// GPIO pin to monitor for state changes
var monitorPin: UInt32 = 0
///
/// Whether or not the GPIO pin state detection is triggered on HIGH (1)
/// Otherwise LOW (0)
var detectionTriggeredHigh: Bool = false
///
/// Whether or not use INPUT_PULLUP mode for GPIO pin
/// Only applicable if the board uses pull-up resistors on the pin
var usePullup: Bool = false
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
///
/// Audio Config for codec2 voice
struct AudioConfig {
@ -1080,6 +1148,7 @@ extension ModuleConfig.OneOf_PayloadVariant: @unchecked Sendable {}
extension ModuleConfig.MQTTConfig: @unchecked Sendable {}
extension ModuleConfig.RemoteHardwareConfig: @unchecked Sendable {}
extension ModuleConfig.NeighborInfoConfig: @unchecked Sendable {}
extension ModuleConfig.DetectionSensorConfig: @unchecked Sendable {}
extension ModuleConfig.AudioConfig: @unchecked Sendable {}
extension ModuleConfig.AudioConfig.Audio_Baud: @unchecked Sendable {}
extension ModuleConfig.SerialConfig: @unchecked Sendable {}
@ -1121,6 +1190,7 @@ extension ModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
9: .standard(proto: "remote_hardware"),
10: .standard(proto: "neighbor_info"),
11: .standard(proto: "ambient_lighting"),
12: .standard(proto: "detection_sensor"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -1272,6 +1342,19 @@ extension ModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
self.payloadVariant = .ambientLighting(v)
}
}()
case 12: try {
var v: ModuleConfig.DetectionSensorConfig?
var hadOneofValue = false
if let current = self.payloadVariant {
hadOneofValue = true
if case .detectionSensor(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
self.payloadVariant = .detectionSensor(v)
}
}()
default: break
}
}
@ -1327,6 +1410,10 @@ extension ModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
guard case .ambientLighting(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 11)
}()
case .detectionSensor?: try {
guard case .detectionSensor(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 12)
}()
case nil: break
}
try unknownFields.traverse(visitor: &visitor)
@ -1501,6 +1588,80 @@ extension ModuleConfig.NeighborInfoConfig: SwiftProtobuf.Message, SwiftProtobuf.
}
}
extension ModuleConfig.DetectionSensorConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = ModuleConfig.protoMessageName + ".DetectionSensorConfig"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "enabled"),
2: .standard(proto: "minimum_broadcast_secs"),
3: .standard(proto: "state_broadcast_secs"),
4: .standard(proto: "send_bell"),
5: .same(proto: "name"),
6: .standard(proto: "monitor_pin"),
7: .standard(proto: "detection_triggered_high"),
8: .standard(proto: "use_pullup"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularBoolField(value: &self.enabled) }()
case 2: try { try decoder.decodeSingularUInt32Field(value: &self.minimumBroadcastSecs) }()
case 3: try { try decoder.decodeSingularUInt32Field(value: &self.stateBroadcastSecs) }()
case 4: try { try decoder.decodeSingularBoolField(value: &self.sendBell) }()
case 5: try { try decoder.decodeSingularStringField(value: &self.name) }()
case 6: try { try decoder.decodeSingularUInt32Field(value: &self.monitorPin) }()
case 7: try { try decoder.decodeSingularBoolField(value: &self.detectionTriggeredHigh) }()
case 8: try { try decoder.decodeSingularBoolField(value: &self.usePullup) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if self.enabled != false {
try visitor.visitSingularBoolField(value: self.enabled, fieldNumber: 1)
}
if self.minimumBroadcastSecs != 0 {
try visitor.visitSingularUInt32Field(value: self.minimumBroadcastSecs, fieldNumber: 2)
}
if self.stateBroadcastSecs != 0 {
try visitor.visitSingularUInt32Field(value: self.stateBroadcastSecs, fieldNumber: 3)
}
if self.sendBell != false {
try visitor.visitSingularBoolField(value: self.sendBell, fieldNumber: 4)
}
if !self.name.isEmpty {
try visitor.visitSingularStringField(value: self.name, fieldNumber: 5)
}
if self.monitorPin != 0 {
try visitor.visitSingularUInt32Field(value: self.monitorPin, fieldNumber: 6)
}
if self.detectionTriggeredHigh != false {
try visitor.visitSingularBoolField(value: self.detectionTriggeredHigh, fieldNumber: 7)
}
if self.usePullup != false {
try visitor.visitSingularBoolField(value: self.usePullup, fieldNumber: 8)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: ModuleConfig.DetectionSensorConfig, rhs: ModuleConfig.DetectionSensorConfig) -> Bool {
if lhs.enabled != rhs.enabled {return false}
if lhs.minimumBroadcastSecs != rhs.minimumBroadcastSecs {return false}
if lhs.stateBroadcastSecs != rhs.stateBroadcastSecs {return false}
if lhs.sendBell != rhs.sendBell {return false}
if lhs.name != rhs.name {return false}
if lhs.monitorPin != rhs.monitorPin {return false}
if lhs.detectionTriggeredHigh != rhs.detectionTriggeredHigh {return false}
if lhs.usePullup != rhs.usePullup {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension ModuleConfig.AudioConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = ModuleConfig.protoMessageName + ".AudioConfig"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [

View file

@ -40,60 +40,81 @@ enum PortNum: SwiftProtobuf.Enum {
/// Deprecated: do not use in new code (formerly called OPAQUE)
/// A message sent from a device outside of the mesh, in a form the mesh does not understand
/// NOTE: This must be 0, because it is documented in IMeshService.aidl to be so
/// ENCODING: binary undefined
case unknownApp // = 0
///
/// A simple UTF-8 text message, which even the little micros in the mesh
/// can understand and show on their screen eventually in some circumstances
/// even signal might send messages in this form (see below)
/// ENCODING: UTF-8 Plaintext (?)
case textMessageApp // = 1
///
/// Reserved for built-in GPIO/example app.
/// See remote_hardware.proto/HardwareMessage for details on the message sent/received to this port number
/// ENCODING: Protobuf
case remoteHardwareApp // = 2
///
/// The built-in position messaging app.
/// Payload is a [Position](/docs/developers/protobufs/api#position) message
/// ENCODING: Protobuf
case positionApp // = 3
///
/// The built-in user info app.
/// Payload is a [User](/docs/developers/protobufs/api#user) message
/// ENCODING: Protobuf
case nodeinfoApp // = 4
///
/// Protocol control packets for mesh protocol use.
/// Payload is a [Routing](/docs/developers/protobufs/api#routing) message
/// ENCODING: Protobuf
case routingApp // = 5
///
/// Admin control packets.
/// Payload is a [AdminMessage](/docs/developers/protobufs/api#adminmessage) message
/// ENCODING: Protobuf
case adminApp // = 6
///
/// Compressed TEXT_MESSAGE payloads.
/// ENCODING: UTF-8 Plaintext (?) with Unishox2 Compression
/// NOTE: The Device Firmware converts a TEXT_MESSAGE_APP to TEXT_MESSAGE_COMPRESSED_APP if the compressed
/// payload is shorter. There's no need for app developers to do this themselves. Also the firmware will decompress
/// any incoming TEXT_MESSAGE_COMPRESSED_APP payload and convert to TEXT_MESSAGE_APP.
case textMessageCompressedApp // = 7
///
/// Waypoint payloads.
/// Payload is a [Waypoint](/docs/developers/protobufs/api#waypoint) message
/// ENCODING: Protobuf
case waypointApp // = 8
///
/// Audio Payloads.
/// Encapsulated codec2 packets. On 2.4 GHZ Bandwidths only for now
/// ENCODING: codec2 audio frames
/// NOTE: audio frames contain a 3 byte header (0xc0 0xde 0xc2) and a one byte marker for the decompressed bitrate.
/// This marker comes from the 'moduleConfig.audio.bitrate' enum minus one.
case audioApp // = 9
///
/// Same as Text Message but originating from Detection Sensor Module.
case detectionSensorApp // = 10
///
/// Provides a 'ping' service that replies to any packet it receives.
/// Also serves as a small example module.
/// ENCODING: ASCII Plaintext
case replyApp // = 32
///
/// Used for the python IP tunnel feature
/// ENCODING: IP Packet. Handled by the python API, firmware ignores this one and pases on.
case ipTunnelApp // = 33
///
@ -102,26 +123,31 @@ enum PortNum: SwiftProtobuf.Enum {
/// network is forwarded to the RX pin while sending a packet to TX will go out to the Mesh network.
/// Maximum packet size of 240 bytes.
/// Module is disabled by default can be turned on by setting SERIAL_MODULE_ENABLED = 1 in SerialPlugh.cpp.
/// ENCODING: binary undefined
case serialApp // = 64
///
/// STORE_FORWARD_APP (Work in Progress)
/// Maintained by Jm Casler (MC Hamster) : jm@casler.org
/// ENCODING: Protobuf
case storeForwardApp // = 65
///
/// Optional port for messages for the range test module.
/// ENCODING: ASCII Plaintext
case rangeTestApp // = 66
///
/// Provides a format to send and receive telemetry data from the Meshtastic network.
/// Maintained by Charles Crossan (crossan007) : crossan007@gmail.com
/// ENCODING: Protobuf
case telemetryApp // = 67
///
/// Experimental tools for estimating node position without a GPS
/// Maintained by Github user a-f-G-U-C (a Meshtastic contributor)
/// Project files at https://github.com/a-f-G-U-C/Meshtastic-ZPS
/// ENCODING: arrays of int64 fields
case zpsApp // = 68
///
@ -129,15 +155,18 @@ enum PortNum: SwiftProtobuf.Enum {
/// as if they did using their LoRa chip.
/// Maintained by GitHub user GUVWAF.
/// Project files at https://github.com/GUVWAF/Meshtasticator
/// ENCODING: Protobuf (?)
case simulatorApp // = 69
///
/// Provides a traceroute functionality to show the route a packet towards
/// a certain destination would take on the mesh.
/// ENCODING: Protobuf
case tracerouteApp // = 70
///
/// Aggregates edge info for the network by sending out a list of each node's neighbors
/// ENCODING: Protobuf
case neighborinfoApp // = 71
///
@ -148,6 +177,7 @@ enum PortNum: SwiftProtobuf.Enum {
///
/// ATAK Forwarder Module https://github.com/paulmandal/atak-forwarder
/// ENCODING: libcotshrink
case atakForwarder // = 257
///
@ -171,6 +201,7 @@ enum PortNum: SwiftProtobuf.Enum {
case 7: self = .textMessageCompressedApp
case 8: self = .waypointApp
case 9: self = .audioApp
case 10: self = .detectionSensorApp
case 32: self = .replyApp
case 33: self = .ipTunnelApp
case 64: self = .serialApp
@ -200,6 +231,7 @@ enum PortNum: SwiftProtobuf.Enum {
case .textMessageCompressedApp: return 7
case .waypointApp: return 8
case .audioApp: return 9
case .detectionSensorApp: return 10
case .replyApp: return 32
case .ipTunnelApp: return 33
case .serialApp: return 64
@ -234,6 +266,7 @@ extension PortNum: CaseIterable {
.textMessageCompressedApp,
.waypointApp,
.audioApp,
.detectionSensorApp,
.replyApp,
.ipTunnelApp,
.serialApp,
@ -270,6 +303,7 @@ extension PortNum: SwiftProtobuf._ProtoNameProviding {
7: .same(proto: "TEXT_MESSAGE_COMPRESSED_APP"),
8: .same(proto: "WAYPOINT_APP"),
9: .same(proto: "AUDIO_APP"),
10: .same(proto: "DETECTION_SENSOR_APP"),
32: .same(proto: "REPLY_APP"),
33: .same(proto: "IP_TUNNEL_APP"),
64: .same(proto: "SERIAL_APP"),

View file

@ -18,7 +18,6 @@ struct Connect: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
//@EnvironmentObject var userSettings: UserSettings
@State var node: NodeInfoEntity?
@State var isUnsetRegion = false
@State var invalidFirmwareVersion = false
@ -50,7 +49,7 @@ struct Connect: View {
if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == .connected {
HStack {
VStack(alignment: .center) {
CircleText(text: node?.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node?.num ?? 0))), circleSize: 80, fontSize: (node?.user?.shortName ?? "???").isEmoji() ? 52 : 30, textColor: UIColor(hex: UInt32(node?.num ?? 0)).isLight() ? .black : .white )
CircleText(text: node?.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node?.num ?? 0))), circleSize: 90, fontSize: (node?.user?.shortName ?? "???").isEmoji() ? 52 : (node?.user?.shortName?.count ?? 0 == 4 ? 26 : 36), textColor: UIColor(hex: UInt32(node?.num ?? 0)).isLight() ? .black : .white )
}
.padding(.trailing)
VStack(alignment: .leading) {
@ -89,22 +88,20 @@ struct Connect: View {
if node != nil {
#if !targetEnvironment(macCatalyst)
if #available(iOS 16.2, *) {
Button {
if !liveActivityStarted {
Button {
if !liveActivityStarted {
#if canImport(ActivityKit)
print("Start live activity.")
startNodeActivity()
#endif
} else {
#if canImport(ActivityKit)
print("Start live activity.")
startNodeActivity()
#endif
} else {
#if canImport(ActivityKit)
print("Stop live activity.")
endActivity()
#endif
}
} label: {
Label("mesh.live.activity", systemImage: liveActivityStarted ? "stop" : "play")
print("Stop live activity.")
endActivity()
#endif
}
} label: {
Label("mesh.live.activity", systemImage: liveActivityStarted ? "stop" : "play")
}
#endif
Text("Num: \(String(node!.num))")
@ -182,7 +179,6 @@ struct Connect: View {
.imageScale(.large).foregroundColor(.gray)
.padding(.trailing)
}
Button(action: {
if UserDefaults.preferredPeripheralId.count > 0 && peripheral.peripheral.identifier.uuidString != UserDefaults.preferredPeripheralId {
presentingSwitchPreferredPeripheral = true
@ -251,7 +247,7 @@ struct Connect: View {
.navigationTitle("bluetooth")
.navigationBarItems(leading: MeshtasticLogo(), trailing:
ZStack {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????", mqttProxyConnected: bleManager.mqttProxyConnected)
})
}
.sheet(isPresented: $invalidFirmwareVersion, onDismiss: didDismissSheet) {
@ -293,41 +289,34 @@ struct Connect: View {
}
#if canImport(ActivityKit)
func startNodeActivity() {
if #available(iOS 16.2, *) {
liveActivityStarted = true
let timerSeconds = 60
let deviceMetrics = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
liveActivityStarted = true
let timerSeconds = 60
let deviceMetrics = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
let activityAttributes = MeshActivityAttributes(nodeNum: Int(node?.num ?? 0), name: node?.user?.longName ?? "unknown")
let activityAttributes = MeshActivityAttributes(nodeNum: Int(node?.num ?? 0), name: node?.user?.longName ?? "unknown")
let future = Date(timeIntervalSinceNow: Double(timerSeconds))
let future = Date(timeIntervalSinceNow: Double(timerSeconds))
let initialContentState = MeshActivityAttributes.ContentState(timerRange: Date.now...future, connected: true, channelUtilization: mostRecent?.channelUtilization ?? 0.0, airtime: mostRecent?.airUtilTx ?? 0.0, batteryLevel: UInt32(mostRecent?.batteryLevel ?? 0))
let initialContentState = MeshActivityAttributes.ContentState(timerRange: Date.now...future, connected: true, channelUtilization: mostRecent?.channelUtilization ?? 0.0, airtime: mostRecent?.airUtilTx ?? 0.0, batteryLevel: UInt32(mostRecent?.batteryLevel ?? 0))
let activityContent = ActivityContent(state: initialContentState, staleDate: Calendar.current.date(byAdding: .minute, value: 2, to: Date())!)
let activityContent = ActivityContent(state: initialContentState, staleDate: Calendar.current.date(byAdding: .minute, value: 2, to: Date())!)
do {
let myActivity = try Activity<MeshActivityAttributes>.request(attributes: activityAttributes, content: activityContent,
pushType: nil)
print(" Requested MyActivity live activity. ID: \(myActivity.id)")
} catch let error {
print("Error requesting live activity: \(error.localizedDescription)")
}
do {
let myActivity = try Activity<MeshActivityAttributes>.request(attributes: activityAttributes, content: activityContent,
pushType: nil)
print(" Requested MyActivity live activity. ID: \(myActivity.id)")
} catch let error {
print("Error requesting live activity: \(error.localizedDescription)")
}
}
func endActivity() {
liveActivityStarted = false
Task {
if #available(iOS 16.2, *) {
for activity in Activity<MeshActivityAttributes>.activities {
// Check if this is the activity associated with this order.
if activity.attributes.nodeNum == node?.num ?? 0 {
await activity.end(nil, dismissalPolicy: .immediate)
}
}
for activity in Activity<MeshActivityAttributes>.activities {
// Check if this is the activity associated with this order.
if activity.attributes.nodeNum == node?.num ?? 0 { await activity.end(nil, dismissalPolicy: .immediate) }
}
}
}

View file

@ -5,22 +5,9 @@
import SwiftUI
struct ContentView: View {
@State private var selection: Tab = .ble
enum Tab {
case contacts
case messages
case map
case ble
case nodes
case settings
}
@StateObject var appState = AppState.shared
var body: some View {
TabView(selection: $selection) {
TabView(selection: $appState.tabSelection) {
Contacts()
.tabItem {
Label("messages", systemImage: "message")
@ -44,6 +31,7 @@ struct ContentView: View {
Settings()
.tabItem {
Label("settings", systemImage: "gear")
.font(.title)
}
.tag(Tab.settings)
}
@ -55,3 +43,12 @@ struct ContentView_Previews: PreviewProvider {
ContentView()
}
}
enum Tab {
case contacts
case messages
case map
case ble
case nodes
case settings
}

View file

@ -13,10 +13,8 @@ struct BatteryLevelCompact: View {
var color: Color
var body: some View {
HStack (alignment: .center, spacing: 0) {
HStack(alignment: .center, spacing: 0) {
if batteryLevel == 100 {
Image(systemName: "battery.100.bolt")
.font(iconFont)
.foregroundColor(color)
@ -45,21 +43,17 @@ struct BatteryLevelCompact: View {
.font(iconFont)
.foregroundColor(color)
.symbolRenderingMode(.hierarchical)
} else if batteryLevel! == 0 {
Image(systemName: "battery.0")
.font(iconFont)
.foregroundColor(.red)
.symbolRenderingMode(.hierarchical)
} else if batteryLevel! > 100 {
Image(systemName: "powerplug")
.font(iconFont)
.foregroundColor(color)
.symbolRenderingMode(.hierarchical)
}
if batteryLevel ?? 0 > 100 {
Text("PWD")
.font(font)

View file

@ -30,7 +30,7 @@ struct CircleText: View {
struct CircleText_Previews: PreviewProvider {
static var previews: some View {
CircleText(text: "RDDN", color: Color.accentColor)
CircleText(text: "MOMO", color: Color.accentColor)
.previewLayout(.fixed(width: 300, height: 100))
}
}

View file

@ -9,12 +9,20 @@ struct ConnectedDevice: View {
var bluetoothOn: Bool
var deviceConnected: Bool
var name: String
var mqttProxyConnected: Bool = false
var body: some View {
HStack {
if bluetoothOn {
if bluetoothOn {
if deviceConnected && mqttProxyConnected {
if mqttProxyConnected {
Image(systemName: "iphone.gen3.radiowaves.left.and.right.circle.fill")
.imageScale(.large)
.foregroundColor(.green)
.symbolRenderingMode(.hierarchical)
}
}
if deviceConnected {
Image(systemName: "antenna.radiowaves.left.and.right.circle.fill")
.imageScale(.large)
@ -27,7 +35,6 @@ struct ConnectedDevice: View {
.imageScale(.medium)
.foregroundColor(.red)
.symbolRenderingMode(.hierarchical)
}
} else {
Text("bluetooth.off").font(.subheadline).foregroundColor(.red)
@ -38,10 +45,10 @@ struct ConnectedDevice: View {
struct ConnectedDevice_Previews: PreviewProvider {
static var previews: some View {
ConnectedDevice(bluetoothOn: true, deviceConnected: false, name: "Yellow Beam")
ConnectedDevice(bluetoothOn: true, deviceConnected: true, name: "MEMO", mqttProxyConnected: true)
.previewLayout(.fixed(width: 80, height: 70))
ConnectedDevice(bluetoothOn: true, deviceConnected: false, name: "Yellow Beam")
ConnectedDevice(bluetoothOn: true, deviceConnected: false, name: "86D4", mqttProxyConnected: false)
.previewLayout(.fixed(width: 80, height: 70))
}

View file

@ -8,17 +8,13 @@ import Foundation
import SwiftUI
struct LoRaSignalStrengthMeter: View {
var snr: Float
var rssi: Int32
var preset: ModemPresets
var compact: Bool
var body: some View {
let signalStrength = getLoRaSignalStrength(snr: snr, rssi: rssi, preset: preset)
let gradient = Gradient(colors: [.red, .orange, .yellow, .green])
if !compact {
VStack {
LoRaSignalStrengthIndicator(signalStrength: signalStrength)
@ -92,5 +88,3 @@ struct LoRaSignalStrengthMeter_Previews: PreviewProvider {
}
}
}

View file

@ -72,16 +72,13 @@ private func getColor(signalStrength: LoRaSignalStrength) -> Color {
}
func getLoRaSignalStrength(snr: Float, rssi: Int32, preset: ModemPresets) -> LoRaSignalStrength {
if rssi > -115 && snr > (preset.snrLimit()) {
return .good
} else if rssi < -126 && snr < (preset.snrLimit() - 7.5) {
return .none
} else if rssi <= -120 || snr <= (preset.snrLimit() - 5.5) {
return .bad
} else {
return .fair
}
} else { return .fair }
}
func getRssiColor(rssi: Int32) -> Color {
@ -94,8 +91,7 @@ func getRssiColor(rssi: Int32) -> Color {
} else if rssi > -126 {
/// Bad
return .orange
} else {
// None
} else { // None
return .red
}
}
@ -110,7 +106,5 @@ func getSnrColor(snr: Float, preset: ModemPresets) -> Color {
} else if snr >= (preset.snrLimit() - 7.5) {
/// Bad
return .orange
} else {
return .red
}
} else { return .red }
}

View file

@ -21,7 +21,6 @@ struct NodeInfoView: View {
var node: NodeInfoEntity
var body: some View {
let hwModelString = node.user?.hwModel ?? "UNSET"
Divider()
@ -61,7 +60,6 @@ struct NodeInfoView: View {
}
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
if deviceMetrics?.count ?? 0 >= 1 {
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
VStack(alignment: .center) {
BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0))
@ -127,7 +125,6 @@ struct NodeInfoView: View {
VStack(alignment: .center) {
CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 42 : 20, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white )
}
if node.user != nil {
Divider()
VStack {
@ -243,14 +240,19 @@ struct NodeInfoView: View {
}
Divider()
}
}
}
}
struct NodeInfoView_Previews: PreviewProvider {
static var previews: some View {
NavigationLink {
DetectionSensorLog(node: node)
} label: {
VStack {
Image(systemName: "sensor")
.symbolRenderingMode(.hierarchical)
.font(.title)
Text("Detection Sensor Log")
.font(.title3)
}
.fixedSize(horizontal: false, vertical: true)
Divider()
}
}
}

View file

@ -109,7 +109,7 @@ class LocalMBTileOverlay: MKTileOverlay {
}
}
//public class CustomMapOverlaySource: MKTileOverlay {
// public class CustomMapOverlaySource: MKTileOverlay {
//
// // requires folder: tiles/{mapName}/z/y/y,{tileType}
// private var parent: MapViewSwiftUI
@ -150,4 +150,4 @@ class LocalMBTileOverlay: MKTileOverlay {
// return URL(string: urlstring)!
// }
// }
//}
// }

View file

@ -12,9 +12,8 @@ struct MapButtons: View {
let width: CGFloat = 45
@Binding var tracking: UserTrackingModes
@Binding var isPresentingInfoSheet: Bool
var body: some View {
VStack() {
VStack {
let impactLight = UIImpactFeedbackGenerator(style: .light)
Button(action: {
self.isPresentingInfoSheet.toggle()
@ -46,7 +45,6 @@ struct MapButtons: View {
.cornerRadius(8)
.shadow(radius: 1)
.offset(x: 3, y: 25)
}
}
@ -61,7 +59,6 @@ struct MapControl_Previews: PreviewProvider {
MapButtons(tracking: $tracking, isPresentingInfoSheet: $isPresentingInfoSheet)
.environment(\.colorScheme, .dark)
}
.previewLayout(.fixed(width: 60, height: 100))
}
}

View file

@ -22,31 +22,24 @@ func degreesToRadians(_ number: Double) -> Double {
var currentMapLayer: MapLayer?
struct MapViewSwiftUI: UIViewRepresentable {
var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void
var onWaypointEdit: (_ waypointId: Int ) -> Void
let mapView = MKMapView()
// Parameters
let selectedMapLayer: MapLayer
let selectedWeatherLayer: MapOverlayServer = UserDefaults.mapOverlayServer
let positions: [PositionEntity]
let waypoints: [WaypointEntity]
let userTrackingMode: MKUserTrackingMode
let showNodeHistory: Bool
let showRouteLines: Bool
let mapViewType: MKMapType = MKMapType.standard
// Offline Map Tiles
@AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0
@State private var loadedLastUpdatedLocalMapFile = 0
var customMapOverlay: CustomMapOverlay?
@State private var presentCustomMapOverlayHash: CustomMapOverlay?
// MARK: Private methods
private func configureMap(mapView: MKMapView) {
// Map View Parameters
mapView.mapType = mapViewType
@ -64,7 +57,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
mapView.setUserTrackingMode(userTrackingMode, animated: true)
if userTrackingMode == MKUserTrackingMode.none {
if latest.count == 1 {
mapView.fit(annotations:showNodeHistory ? positions : latest, andShow: false)
mapView.fit(annotations: showNodeHistory ? positions: latest, andShow: false)
} else {
mapView.fitAllAnnotations()
}
@ -100,7 +93,6 @@ struct MapViewSwiftUI: UIViewRepresentable {
#endif
#endif
}
private func setMapBaseLayer(mapView: MKMapView) {
// Avoid refreshing UI if selectedLayer has not changed
guard currentMapLayer != selectedMapLayer else { return }
@ -128,9 +120,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
mapView.mapType = .standard
}
}
private func setMapOverlays(mapView: MKMapView) {
// Weather radar
if UserDefaults.enableOverlayServer {
let locale = Locale.current
@ -143,16 +133,12 @@ struct MapViewSwiftUI: UIViewRepresentable {
}
}
}
private func setMbtilesOverlay(mapView: MKMapView) {
// MBTiles Offline
if UserDefaults.enableOfflineMaps && UserDefaults.enableOfflineMapsMBTiles {
if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile {
mapView.removeOverlays(mapView.overlays)
if self.customMapOverlay != nil {
let fileManager = FileManager.default
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path
@ -173,11 +159,10 @@ struct MapViewSwiftUI: UIViewRepresentable {
}
}
}
private func setGeoJsonOverlay(mapView: MKMapView) {
guard let geoJsonFileUrl = URL(string: "https://raw.githubusercontent.com/johan/world.geo.json/master/countries.geo.json"), // Bundle.main.url(forResource: "location", withExtension: "geojson"),
//guard let geoJsonFileUrl = URL(string: "https://hrbrmstr.github.io/noaa-alerts-sp-to-geojson/current-all.geojson"),
guard let geoJsonFileUrl = URL(string: "https://raw.githubusercontent.com/johan/world.geo.json/master/countries.geo.json"),
// Bundle.main.url(forResource: "location", withExtension: "geojson"),
// guard let geoJsonFileUrl = URL(string: "https://hrbrmstr.github.io/noaa-alerts-sp-to-geojson/current-all.geojson"),
let geoJsonData = try? Data.init(contentsOf: geoJsonFileUrl) else {
fatalError("Failure to fetch the file.")
}
@ -188,21 +173,20 @@ struct MapViewSwiftUI: UIViewRepresentable {
objs.forEach { (feature) in
guard let geometry = feature.geometry.first,
let propData = feature.properties else {
return;
return
}
// Check if it is MKPolygon
if let polygon = geometry as? MKPolygon {
let polygonInfo = try? JSONDecoder.init().decode(PolygonInfo.self, from: propData)
mapView.addOverlay(polygon)
//self.view?.render(overlay: polygon, info: polygonInfo)
// self.view?.render(overlay: polygon, info: polygonInfo)
}
// Check if it is MKPolyline
if let polyline = geometry as? MKPolyline {
mapView.addOverlay(polyline, level: .aboveLabels)
//let polylineInfo = try? JSONDecoder.init().decode(PolylineInfo.self, from: propData)
//self.view?.render(overlay: polyline, info: polylineInfo)
// let polylineInfo = try? JSONDecoder.init().decode(PolylineInfo.self, from: propData)
// self.view?.render(overlay: polyline, info: polylineInfo)
}
// Check if it is MKPointAnnotation
// if let annotation = geometry as? MKPointAnnotation {
// let info = try? JSONDecoder.init().decode(Info.self, from: propData)
@ -214,27 +198,22 @@ struct MapViewSwiftUI: UIViewRepresentable {
// }
}
}
func makeUIView(context: Context) -> MKMapView {
currentMapLayer = nil
mapView.delegate = context.coordinator
self.configureMap(mapView: mapView)
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
// Set MBTiles overlay layer
setMbtilesOverlay(mapView: mapView)
// Set selected map base layer
setMapBaseLayer(mapView: mapView)
// Set map tile server and weather overlay layers
setMapOverlays(mapView: mapView)
let latest = positions
.filter { $0.latest == true }
.sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 }
// Node Route Lines
if showRouteLines {
// Remove all existing PolyLine Overlays
@ -245,10 +224,8 @@ struct MapViewSwiftUI: UIViewRepresentable {
}
var lineIndex = 0
for position in latest {
let nodePositions = positions.filter { $0.nodeCoordinate != nil && $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 }
let lineCoords = nodePositions.compactMap ({
(position) -> CLLocationCoordinate2D in
let lineCoords = nodePositions.compactMap({(position) -> CLLocationCoordinate2D in
return position.nodeCoordinate ?? LocationHelper.DefaultLocation
})
let polyline = MKPolyline(coordinates: lineCoords, count: nodePositions.count)
@ -268,7 +245,6 @@ struct MapViewSwiftUI: UIViewRepresentable {
}
}
}
let annotationCount = waypoints.count + (showNodeHistory ? positions.count : latest.count)
if annotationCount != mapView.annotations.count {
print("Annotation Count: \(annotationCount) Map Annotations: \(mapView.annotations.count)")
@ -277,9 +253,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
}
if userTrackingMode == MKUserTrackingMode.none {
mapView.showsUserLocation = false
if UserDefaults.enableMapRecentering {
if latest.count == 1 {
mapView.fit(annotations: showNodeHistory ? positions : latest, andShow: true)
} else {
@ -287,23 +261,18 @@ struct MapViewSwiftUI: UIViewRepresentable {
mapView.fitAllAnnotations()
}
}
} else {
mapView.addAnnotations(showNodeHistory ? positions : latest)
mapView.showsUserLocation = true
}
mapView.setUserTrackingMode(userTrackingMode, animated: true)
}
func makeCoordinator() -> MapCoordinator {
return Coordinator(self)
}
final class MapCoordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate {
var parent: MapViewSwiftUI
var longPressRecognizer = UILongPressGestureRecognizer()
init(_ parent: MapViewSwiftUI) {
self.parent = parent
super.init()
@ -313,16 +282,13 @@ struct MapViewSwiftUI: UIViewRepresentable {
self.longPressRecognizer.delegate = self
self.parent.mapView.addGestureRecognizer(longPressRecognizer)
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
switch annotation {
case let positionAnnotation as PositionEntity:
let reuseID = String(positionAnnotation.nodePosition?.num ?? 0) + "-" + String(positionAnnotation.time?.timeIntervalSince1970 ?? 0)
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: reuseID )
annotationView.tag = -1
annotationView.canShowCallout = true
if positionAnnotation.latest {
annotationView.markerTintColor = .systemRed
annotationView.displayPriority = .required
@ -345,7 +311,6 @@ struct MapViewSwiftUI: UIViewRepresentable {
let distanceFormatter = MKDistanceFormatter()
subtitle.text! += "Altitude: \(distanceFormatter.string(fromDistance: Double(positionAnnotation.altitude))) \n"
if positionAnnotation.nodePosition?.metadata != nil {
if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.client ||
DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.clientMute ||
DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.routerClient {
@ -359,7 +324,6 @@ struct MapViewSwiftUI: UIViewRepresentable {
} else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.sensor {
annotationView.glyphImage = UIImage(systemName: "sensor")
}
let pf = PositionFlags(rawValue: Int(positionAnnotation.nodePosition?.metadata?.positionFlags ?? 3))
if pf.contains(.Satsinview) {
subtitle.text! += "Sats in view: \(String(positionAnnotation.satsInView)) \n"
@ -368,7 +332,6 @@ struct MapViewSwiftUI: UIViewRepresentable {
subtitle.text! += "Sequence: \(String(positionAnnotation.seqNo)) \n"
}
if pf.contains(.Heading) {
if parent.userTrackingMode != MKUserTrackingMode.followWithHeading {
annotationView.glyphImage = UIImage(systemName: "location.north.fill")?.rotate(radians: Float(degreesToRadians(Double(positionAnnotation.heading))))
subtitle.text! += "Heading: \(String(positionAnnotation.heading)) \n"
@ -384,7 +347,6 @@ struct MapViewSwiftUI: UIViewRepresentable {
}
subtitle.text! += "Speed: \(formatter.string(from: Measurement(value: Double(positionAnnotation.speed), unit: UnitSpeed.kilometersPerHour))) \n"
}
} else {
// node metadata is nil
annotationView.glyphImage = UIImage(systemName: "flipphone")
@ -445,9 +407,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
default: return nil
}
}
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
switch view.annotation {
case let positionAnnotation as PositionEntity:
print(positionAnnotation)
@ -456,20 +416,15 @@ struct MapViewSwiftUI: UIViewRepresentable {
if view.tag > 0 {
parent.onWaypointEdit(view.tag)
}
default: break
}
}
@objc func longPressHandler(_ gesture: UILongPressGestureRecognizer) {
if gesture.state != UIGestureRecognizer.State.ended {
return
} else if gesture.state != UIGestureRecognizer.State.began {
// Screen Position - CGPoint
let location = longPressRecognizer.location(in: self.parent.mapView)
// Map Coordinate - CLLocationCoordinate2D
let coordinate = self.parent.mapView.convert(location, toCoordinateFrom: self.parent.mapView)
let annotation = MKPointAnnotation()
@ -480,14 +435,11 @@ struct MapViewSwiftUI: UIViewRepresentable {
parent.onLongPress(coordinate)
}
}
public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let tileOverlay = overlay as? MKTileOverlay {
return MKTileOverlayRenderer(tileOverlay: tileOverlay)
} else {
if let routePolyline = overlay as? MKPolyline {
let titleString = routePolyline.title ?? "0"
let renderer = MKPolylineRenderer(polyline: routePolyline)
renderer.strokeColor = UIColor(hex: UInt32(titleString) ?? 0)
@ -498,25 +450,21 @@ struct MapViewSwiftUI: UIViewRepresentable {
let renderer = MKPolygonRenderer(polygon: polygon)
renderer.fillColor = UIColor.purple.withAlphaComponent(0.2)
renderer.strokeColor = .purple.withAlphaComponent(0.7)
return renderer
}
return MKOverlayRenderer(overlay: overlay)
}
}
}
/// is supposed to be located in the folder with the map name
public struct DefaultTile: Hashable {
let tileName: String
let tileType: String
public init(tileName: String, tileType: String) {
self.tileName = tileName
self.tileType = tileType
}
}
public struct CustomMapOverlay: Equatable, Hashable {
let mapName: String
let tileType: String
@ -524,7 +472,6 @@ struct MapViewSwiftUI: UIViewRepresentable {
var minimumZoomLevel: Int?
var maximumZoomLevel: Int?
let defaultTile: DefaultTile?
public init(
mapName: String,
tileType: String,
@ -540,7 +487,6 @@ struct MapViewSwiftUI: UIViewRepresentable {
self.maximumZoomLevel = maximumZoomLevel
self.defaultTile = defaultTile
}
public init?(
mapName: String?,
tileType: String,

View file

@ -63,12 +63,23 @@ struct ChannelMessageList: View {
VStack(alignment: currentUser ? .trailing : .leading) {
let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE"))
let linkBlue = Color(red: 0.4627, green: 0.8392, blue: 1) /* #76d6ff */
let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue)
Text(markdownText)
.tint(linkBlue)
.padding(10)
.foregroundColor(.white)
.background(currentUser ? .accentColor : Color(.gray))
.cornerRadius(15)
.overlay(
VStack {
isDetectionSensorMessage ? Image(systemName: "sensor.fill")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
.foregroundStyle(Color.orange)
.offset(x: 20, y: -20)
: nil
}
)
.contextMenu {
VStack {
Text("channel")+Text(": \(message.channel)")
@ -185,6 +196,9 @@ struct ChannelMessageList: View {
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.font(.caption2).foregroundColor(.red)
} else if isDetectionSensorMessage {
let messageDate = message.timestamp
Text(" \(messageDate.formattedDate(format: dateFormatString))").font(.caption2).foregroundColor(.gray)
}
}
}
@ -232,7 +246,6 @@ struct ChannelMessageList: View {
#if targetEnvironment(macCatalyst)
HStack {
Spacer()
Button {
let bell = "🔔 Alert Bell Character! \u{7}"
print(bell)

View file

@ -0,0 +1,144 @@
//
// DetectionSensorLog.swift
// Meshtastic
//
// Created by Ben on 8/22/23.
//
import SwiftUI
import Charts
struct DetectionSensorLog: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@State private var isPresentingClearLogConfirm: Bool = false
@State var isExporting = false
@State var exportString = ""
var node: NodeInfoEntity
var body: some View {
let oneDayAgo = Calendar.current.date(byAdding: .day, value: -1, to: Date())
let detections = getDetectionSensorMessages(nodeNum: node.num, context: context)
let chartData = detections
.filter { $0.timestamp >= oneDayAgo! }
.sorted { $0.timestamp < $1.timestamp }
NavigationStack {
if chartData.count > 0 {
GroupBox(label: Label("\(detections.count) Total Detection Events", systemImage: "sensor")) {
Chart {
ForEach(chartData, id: \.self) { point in
Plot {
BarMark(
x: .value("x", point.timestamp),
y: .value("y", 1)
)
}
.accessibilityLabel("Bar Series")
.accessibilityValue("X: \(point.timestamp), Y: \(1)")
.interpolationMethod(.cardinal)
.foregroundStyle(
.linearGradient(
colors: [.green, .yellow, .orange, .red],
startPoint: .bottom,
endPoint: .top
)
)
.alignsMarkStylesWithPlotArea()
}
}
.chartXAxis(content: {
AxisMarks(position: .top)
// AxisMarks(position: .top, values: .stride(by: .hour)) { date in
// AxisValueLabel(format: .dateTime.hour())
// }
})
.chartXAxis(.automatic)
.chartYScale(domain: 0...20)
.chartForegroundStyleScale([
"Detection events": .green
])
.chartLegend(position: .automatic, alignment: .bottom)
}
.frame(minHeight: 250)
}
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
// Add a table for mac and ipad
Table(detections) {
TableColumn("Detection event") { d in
Text(d.messagePayload ?? "Detected")
}
TableColumn("timestamp") { d in
Text(d.timestamp.formattedDate(format: dateFormatString))
}
.width(min: 180)
}
} else {
ScrollView {
let columns = [
GridItem(),
GridItem()
]
LazyVGrid(columns: columns, alignment: .leading, spacing: 1) {
GridRow {
Text("Detection")
.font(.caption)
.fontWeight(.bold)
Text("timestamp")
.font(.caption)
.fontWeight(.bold)
}
ForEach(detections) { d in
GridRow {
Text(d.messagePayload ?? "Detected")
Text(d.timestamp.formattedDate(format: dateFormatString))
.font(.caption)
}
}
}
.padding(.leading, 15)
.padding(.trailing, 5)
}
}
}
HStack {
Button {
exportString = detectionsToCsv(detections: chartData)
isExporting = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
.padding(.trailing)
}
.navigationTitle("detection.sensor.log")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
})
.onAppear {
self.bleManager.context = context
}
.fileExporter(
isPresented: $isExporting,
document: CsvDocument(emptyCsv: exportString),
contentType: .commaSeparatedText,
defaultFilename: String("\(node.user?.longName ?? "Node") \("detection.sensor.log".localized)"),
onCompletion: { result in
if case .success = result {
print("Detections metrics log download succeeded.")
self.isExporting = false
} else {
print("Detections log download failed: \(result).")
}
}
)
}
}

View file

@ -8,21 +8,21 @@ import SwiftUI
import Charts
struct DeviceMetricsLog: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@State private var isPresentingClearLogConfirm: Bool = false
@State var isExporting = false
@State var exportString = ""
@State private var batteryChartColor: Color = .blue
@State private var airtimeChartColor: Color = .orange
@State private var channelUtilizationChartColor: Color = .green
var node: NodeInfoEntity
var body: some View {
let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).reversed() as? [TelemetryEntity] ?? []
let chartData = deviceMetrics
@ -30,14 +30,14 @@ struct DeviceMetricsLog: View {
.sorted { $0.time! < $1.time! }
NavigationStack {
if chartData.count > 0 {
GroupBox(label: Label("\(deviceMetrics.count) Readings Total", systemImage: "chart.xyaxis.line")) {
Chart {
ForEach(chartData, id: \.self) { point in
Plot {
LineMark(
x: .value("x", point.time!),
@ -47,9 +47,8 @@ struct DeviceMetricsLog: View {
.accessibilityLabel("Line Series")
.accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)")
.foregroundStyle(batteryChartColor)
.interpolationMethod(.cardinal)
//.interpolationMethod(.catmullRom(alpha: 1.0))
.interpolationMethod(.catmullRom(alpha: 1.0))
Plot {
PointMark(
x: .value("x", point.time!),
@ -59,11 +58,11 @@ struct DeviceMetricsLog: View {
.accessibilityLabel("Line Series")
.accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)")
.foregroundStyle(channelUtilizationChartColor)
RuleMark(y: .value("Limit", 10))
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10]))
.foregroundStyle(airtimeChartColor)
Plot {
PointMark(
x: .value("x", point.time!),
@ -81,7 +80,7 @@ struct DeviceMetricsLog: View {
.chartXAxis(.automatic)
.chartYScale(domain: 0...100)
.chartForegroundStyleScale([
"Battery Level" : .blue,
"Battery Level": .blue,
"Channel Utilization": .green,
"Airtime": .orange
])
@ -92,9 +91,8 @@ struct DeviceMetricsLog: View {
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
// Add a table for mac and ipad
//Table(Array(deviceMetrics),id: \.self) {
// Table(Array(deviceMetrics),id: \.self) {
Table(deviceMetrics) {
TableColumn("battery.level") { dm in
if dm.batteryLevel > 100 {
@ -120,13 +118,11 @@ struct DeviceMetricsLog: View {
} else {
ScrollView {
let columns = [
//GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1),
GridItem(.flexible(minimum: 30, maximum: 45), spacing: 0.1),
GridItem(.flexible(minimum: 30, maximum: 50), spacing: 0.1),
GridItem(.flexible(minimum: 30, maximum: 70), spacing: 0.1),
GridItem(.flexible(minimum: 30, maximum: 65), spacing: 0.1),
GridItem(.flexible(minimum: 130, maximum: 200), spacing: 0.1)
]
LazyVGrid(columns: columns, alignment: .leading, spacing: 1) {
GridRow {
@ -181,7 +177,7 @@ struct DeviceMetricsLog: View {
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
.padding(.trailing)
.padding(.leading)
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingClearLogConfirm,
@ -195,6 +191,7 @@ struct DeviceMetricsLog: View {
}
}
}
Button {
exportString = telemetryToCsvFile(telemetry: deviceMetrics, metricsType: 0)
isExporting = true

View file

@ -20,8 +20,6 @@ struct EnvironmentMetricsLog: View {
var node: NodeInfoEntity
var body: some View {
let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())
let environmentMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 1")).reversed() as? [TelemetryEntity] ?? []
let chartData = environmentMetrics
@ -29,13 +27,10 @@ struct EnvironmentMetricsLog: View {
.sorted { $0.time! < $1.time! }
let locale = NSLocale.current as NSLocale
let localeUnit = locale.object(forKey: NSLocale.Key(rawValue: "kCFLocaleTemperatureUnitKey"))
var format: UnitTemperature = localeUnit as? String ?? "Celsius" == "Fahrenheit" ? .fahrenheit : .celsius
let format: UnitTemperature = localeUnit as? String ?? "Celsius" == "Fahrenheit" ? .fahrenheit : .celsius
NavigationStack {
if chartData.count > 0 {
GroupBox(label: Label("\(environmentMetrics.count) Readings Total", systemImage: "chart.xyaxis.line")) {
Chart {
ForEach(chartData, id: \.time) { dataPoint in
AreaMark(
@ -53,7 +48,6 @@ struct EnvironmentMetricsLog: View {
)
.alignsMarkStylesWithPlotArea()
.accessibilityHidden(true)
LineMark(
x: .value("Time", dataPoint.time!),
y: .value("Temperature", dataPoint.temperature.localeTemperature())
@ -74,12 +68,11 @@ struct EnvironmentMetricsLog: View {
})
.chartYScale(domain: format == .celsius ? -20...55 : 0...125)
.chartForegroundStyleScale([
"Temperature" : .clear
"Temperature": .clear
])
.chartLegend(position: .automatic, alignment: .bottom)
}
}
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
@ -169,7 +162,7 @@ struct EnvironmentMetricsLog: View {
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
.padding(.trailing)
.padding(.leading)
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingClearLogConfirm,
@ -191,7 +184,7 @@ struct EnvironmentMetricsLog: View {
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
.padding(.leading)
.padding(.trailing)
}
.navigationTitle("Environment Metrics Log")
.navigationBarTitleDisplayMode(.inline)

View file

@ -49,11 +49,8 @@ struct NodeDetail: View {
@State private var attributionLink: URL?
@State private var attributionLogo: URL?
var body: some View {
let hwModelString = node.user?.hwModel ?? "UNSET"
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
NavigationStack {
GeometryReader { bounds in
@ -71,11 +68,11 @@ struct NodeDetail: View {
waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId))
}
},
//visibleMapRect: $mapRect,
// visibleMapRect: $mapRect,
selectedMapLayer: selectedMapLayer,
positions: lastTenThousand,
waypoints: Array(waypoints),
//mapViewType: mapType,
// mapViewType: mapType,
userTrackingMode: MKUserTrackingMode.none,
showNodeHistory: meshMapShowNodeHistory,
showRouteLines: meshMapShowRouteLines,
@ -147,13 +144,10 @@ struct NodeDetail: View {
}
.padding([.top], 20)
}
ScrollView() {
ScrollView {
NodeInfoView(node: node)
if self.bleManager.connectedPeripheral != nil && node.metadata != nil {
HStack {
if node.metadata?.canShutdown ?? false {
Button(action: {
@ -233,22 +227,18 @@ struct NodeDetail: View {
})
.onAppear {
self.bleManager.context = context
//mapType = .standard// MeshMapTypes(rawValue: meshMapType)?.MKMapTypeValue() ?? .standard
// mapType = .standard// MeshMapTypes(rawValue: meshMapType)?.MKMapTypeValue() ?? .standard
}
.task(id: node.num) {
if !loadedWeather {
do {
if node.positions?.count ?? 0 > 0 {
let mostRecent = node.positions?.lastObject as? PositionEntity
let weather = try await WeatherService.shared.weather(for: mostRecent?.nodeLocation ?? CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude))
condition = weather.currentWeather.condition
temperature = weather.currentWeather.temperature
humidity = Int(weather.currentWeather.humidity * 100)
symbolName = weather.currentWeather.symbolName
let attribution = try await WeatherService.shared.attribution
attributionLink = attribution.legalPageURL
attributionLogo = colorScheme == .light ? attribution.combinedMarkLightURL : attribution.combinedMarkDarkURL

View file

@ -12,6 +12,15 @@ import SwiftUI
import CoreLocation
struct NodeList: View {
@State private var searchText = ""
var nodesQuery: Binding<String> {
Binding {
searchText
} set: { newValue in
searchText = newValue
nodes.nsPredicate = newValue.isEmpty ? nil : NSPredicate(format: "user.longName CONTAINS[c] %@ OR user.shortName CONTAINS[c] %@", newValue, newValue)
}
}
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@ -38,7 +47,7 @@ struct NodeList: View {
LazyVStack(alignment: .leading) {
HStack {
VStack(alignment: .leading) {
CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 44 : 22, brightness: 0.0, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white)
CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 44 : (node.user?.shortName?.count ?? 0 == 4 ? 19 : 26), brightness: 0.0, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white)
.padding(.trailing, 5)
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
if deviceMetrics?.count ?? 0 >= 1 {
@ -116,5 +125,6 @@ struct NodeList: View {
Text("select.node")
}
}
.searchable(text: nodesQuery, prompt: "Find a node")
}
}

View file

@ -11,12 +11,10 @@ import CoreLocation
import CoreData
struct NodeMap: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@ObservedObject var tileManager = OfflineTileManager.shared
@StateObject var appState = AppState.shared
@State var selectedMapLayer: MapLayer = UserDefaults.mapLayer
@State var enableMapRecentering: Bool = UserDefaults.enableMapRecentering
@State var enableMapRouteLines: Bool = UserDefaults.enableMapRouteLines
@ -27,35 +25,26 @@ struct NodeMap: View {
@State var enableOverlayServer: Bool = UserDefaults.enableOverlayServer
@State var selectedOverlayServer: MapOverlayServer = UserDefaults.mapOverlayServer
@State var mapTilesAboveLabels: Bool = UserDefaults.mapTilesAboveLabels
let fromDate: NSDate = Calendar.current.date(byAdding: .month, value: -1, to: Date())! as NSDate
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)],
predicate: NSPredicate(format: "time >= %@ && nodePosition != nil", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate), animation: .none)
private var positions: FetchedResults<PositionEntity>
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
predicate: NSPredicate(
format: "expire == nil || expire >= %@", Date() as NSDate
), animation: .none)
private var waypoints: FetchedResults<WaypointEntity>
@State var waypointCoordinate: WaypointCoordinate?
@State var selectedTracking: UserTrackingModes = .none
@State var isPresentingInfoSheet: Bool = false
@State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay(
mapName: "offlinemap",
tileType: "png",
canReplaceMapContent: true
)
var body: some View {
NavigationStack {
ZStack {
MapViewSwiftUI(
onLongPress: { coord in
waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0)
@ -73,16 +62,13 @@ struct NodeMap: View {
customMapOverlay: self.customMapOverlay
)
VStack(alignment: .trailing) {
HStack(alignment: .top) {
Spacer()
MapButtons(tracking: $selectedTracking, isPresentingInfoSheet: $isPresentingInfoSheet)
.padding(.trailing, 8)
.padding(.top, 16)
}
Spacer()
}
}
.ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
@ -111,9 +97,7 @@ struct NodeMap: View {
}
.padding(.top, 5)
.padding(.bottom, 5)
Toggle(isOn: $enableMapRecentering) {
Label("map.recentering", systemImage: "camera.metering.center.weighted")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
@ -121,9 +105,7 @@ struct NodeMap: View {
self.enableMapRecentering.toggle()
UserDefaults.enableMapRecentering = self.enableMapRecentering
}
Toggle(isOn: $enableMapNodeHistoryPins) {
Label("Show Node History", systemImage: "building.columns.fill")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
@ -131,9 +113,7 @@ struct NodeMap: View {
self.enableMapNodeHistoryPins.toggle()
UserDefaults.enableMapNodeHistoryPins = self.enableMapNodeHistoryPins
}
Toggle(isOn: $enableMapRouteLines) {
Label("Show Route Lines", systemImage: "road.lanes")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
@ -141,12 +121,9 @@ struct NodeMap: View {
self.enableMapRouteLines.toggle()
UserDefaults.enableMapRouteLines = self.enableMapRouteLines
}
let locale = Locale.current
if locale.region?.identifier ?? "no locale" == "US" {
Toggle(isOn: $enableOverlayServer) {
Label("Show Weather", systemImage: "cloud.fill")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
@ -154,7 +131,6 @@ struct NodeMap: View {
self.enableOverlayServer.toggle()
UserDefaults.enableOverlayServer = self.enableOverlayServer
}
if enableOverlayServer {
Picker(selection: $selectedOverlayServer,
label: Text("Radar")) {
@ -179,8 +155,8 @@ struct NodeMap: View {
Text("Enable Offline Maps")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.onChange(of: (enableOfflineMaps)) { newEnableOfflineMaps in
UserDefaults.enableOfflineMaps = enableOfflineMaps
.onChange(of: enableOfflineMaps) { newEnableOfflineMaps in
UserDefaults.enableOfflineMaps = newEnableOfflineMaps
if !enableOfflineMaps {
if self.selectedMapLayer == .offline {
self.selectedMapLayer = .standard
@ -188,10 +164,8 @@ struct NodeMap: View {
}
}
if enableOfflineMaps {
VStack (alignment: .leading) {
VStack(alignment: .leading) {
if !enableOfflineMapsMBTiles {
Picker(selection: $selectedTileServer,
label: Text("Tile Server")) {
ForEach(MapTileServer.allCases, id: \.self) { tsl in
@ -218,7 +192,6 @@ struct NodeMap: View {
self.mapTilesAboveLabels.toggle()
UserDefaults.mapTilesAboveLabels = self.mapTilesAboveLabels
}
}
Divider()
Toggle(isOn: $enableOfflineMapsMBTiles) {

View file

@ -7,7 +7,6 @@
import SwiftUI
struct PositionLog: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass?
@ -16,24 +15,18 @@ struct PositionLog: View {
let result = (verticalSizeClass == .regular || verticalSizeClass == .compact) && horizontalSizeClass == .compact
return result
}
@State var isExporting = false
@State var exportString = ""
var node: NodeInfoEntity
@State private var isPresentingClearLogConfirm = false
@State private var sortOrder = [KeyPathComparator(\PositionEntity.latitude)]
var body: some View {
NavigationStack {
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
if UIDevice.current.userInterfaceIdiom == .pad && !useGrid || UIDevice.current.userInterfaceIdiom == .mac {
// Add a table for mac and ipad
var positions = node.positions?.reversed() as? [PositionEntity] ?? []
let positions = node.positions?.reversed() as? [PositionEntity] ?? []
Table(positions) {
TableColumn("Latitude") { position in
Text(String(format: "%.5f", position.latitude ?? 0))
@ -65,9 +58,7 @@ struct PositionLog: View {
}
.width(min: 180)
}
} else {
ScrollView {
// Use a grid on iOS as a table only shows a single column
let columns = [
@ -78,7 +69,6 @@ struct PositionLog: View {
GridItem(spacing: 0)
]
LazyVGrid(columns: columns, alignment: .leading, spacing: 1) {
GridRow {
Text("Latitude")
.font(.caption2)
@ -115,9 +105,7 @@ struct PositionLog: View {
}
.padding(.leading)
}
HStack {
Button(role: .destructive) {
isPresentingClearLogConfirm = true
} label: {
@ -127,7 +115,7 @@ struct PositionLog: View {
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
.padding(.trailing)
.padding(.leading)
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingClearLogConfirm,
@ -136,25 +124,22 @@ struct PositionLog: View {
Button("Delete all positions?", role: .destructive) {
if clearPositions(destNum: node.num, context: context) {
print("Successfully Cleared Position Log")
} else {
print("Clear Position Log Failed")
}
}
}
Button {
exportString = positionToCsvFile(positions: node.positions!.array as? [PositionEntity] ?? [])
isExporting = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
.padding(.leading)
.padding(.trailing)
}
.fileExporter(
isPresented: $isExporting,
@ -162,14 +147,10 @@ struct PositionLog: View {
contentType: .commaSeparatedText,
defaultFilename: String("\(node.user?.longName ?? "Node") Position Log"),
onCompletion: { result in
if case .success = result {
print("Position log download succeeded.")
self.isExporting = false
} else {
print("Position log download failed: \(result).")
}
}
@ -177,13 +158,10 @@ struct PositionLog: View {
}
.navigationTitle("Position Log \(node.positions?.count ?? 0) Points")
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
})
.onAppear {
self.bleManager.context = context
}
}

View file

@ -36,7 +36,7 @@ struct AboutMeshtastic: View {
}
if locale.region?.identifier ?? "no locale" == "US" {
Section(header: Text("Get Devices")) {
Link("Buy Complete Radios", destination: URL(string: "https://www.etsy.com/shop/GarthVH")!)
Link("Buy Complete Radios", destination: URL(string: "http://garthvh.com")!)
.font(.title2)
}
}

View file

@ -5,7 +5,6 @@ import SwiftProtobuf
import MapKit
struct AppSettings: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@ObservedObject var tileManager = OfflineTileManager.shared
@ -16,12 +15,10 @@ struct AppSettings: View {
@State var provideLocationInterval: Int = UserDefaults.provideLocationInterval
@State private var isPresentingCoreDataResetConfirm = false
@State private var isPresentingDeleteMapTilesConfirm = false
var body: some View {
VStack {
Form {
Section(header: Text("user.details")) {
HStack {
Label("Name", systemImage: "person.crop.rectangle.fill")
TextField("Username", text: $meshtasticUsername)
@ -31,7 +28,6 @@ struct AppSettings: View {
.disableAutocorrection(true)
.listRowSeparator(.visible)
}
Section(header: Text("phone.gps")) {
let accuracy = Measurement(value: locationHelper.locationManager.location?.horizontalAccuracy ?? 300, unit: UnitLength.meters)
let altitiude = Measurement(value: locationHelper.locationManager.location?.altitude ?? 0, unit: UnitLength.meters)
@ -57,15 +53,12 @@ struct AppSettings: View {
Label("Speed \(speed.formatted())", systemImage: "speedometer")
.font(.footnote)
}
}
Section(header: Text("Location Settings")) {
Toggle(isOn: $provideLocation) {
Label("provide.location", systemImage: "location.circle.fill")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
if UserDefaults.provideLocation {
VStack {
Picker("update.interval", selection: $provideLocationInterval) {
@ -82,10 +75,8 @@ struct AppSettings: View {
.foregroundColor(.gray)
}
}
}
Section(header: Text("App Data")) {
Button {
isPresentingCoreDataResetConfirm = true
} label: {
@ -124,9 +115,7 @@ struct AppSettings: View {
print("delete all tiles")
}
}
ForEach(MapTileServer.allCases, id: \.self) { tsl in
Button {
tileManager.remove(for: tsl)
totalDownloadedTileSize = tileManager.getAllDownloadedSize()

View file

@ -7,18 +7,18 @@
import SwiftUI
struct DeviceConfig: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@Environment(\.dismiss) private var goBack
var node: NodeInfoEntity?
@State private var isPresentingNodeDBResetConfirm = false
@State private var isPresentingFactoryResetConfirm = false
@State private var isPresentingSaveConfirm = false
@State var hasChanges = false
@State var deviceRole = 0
@State var buzzerGPIO = 0
@State var buttonGPIO = 0
@ -27,17 +27,14 @@ struct DeviceConfig: View {
@State var rebroadcastMode = 0
@State var doubleTapAsButtonPress = false
@State var isManaged = false
var body: some View {
VStack {
Form {
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
Text("There has been no response to a request for device metadata over the admin channel for this node.")
.font(.callout)
.foregroundColor(.orange)
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
// Let users know what is going on if they are using remote admin and don't have the config yet
if node?.deviceConfig == nil {
@ -60,7 +57,6 @@ struct DeviceConfig: View {
.foregroundColor(.orange)
}
Section(header: Text("options")) {
Picker("Device Role", selection: $deviceRole ) {
ForEach(DeviceRoles.allCases) { dr in
Text(dr.name)
@ -71,7 +67,6 @@ struct DeviceConfig: View {
Text(DeviceRoles(rawValue: deviceRole)?.description ?? "")
.foregroundColor(.gray)
.font(.caption)
Picker("Rebroadcast Mode", selection: $rebroadcastMode ) {
ForEach(RebroadcastModes.allCases) { rm in
Text(rm.name)
@ -82,14 +77,13 @@ struct DeviceConfig: View {
Text(RebroadcastModes(rawValue: rebroadcastMode)?.description ?? "")
.foregroundColor(.gray)
.font(.caption)
Toggle(isOn: $doubleTapAsButtonPress) {
Label("Double Tap as Button", systemImage: "hand.tap")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("Treat double tap on supported accelerometers as a user button press.")
.font(.caption)
Toggle(isOn: $isManaged) {
Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath")
}
@ -97,24 +91,17 @@ struct DeviceConfig: View {
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.")
.font(.caption)
}
Section(header: Text("Debug")) {
Toggle(isOn: $serialEnabled) {
Label("Serial Console", systemImage: "terminal")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $debugLogEnabled) {
Label("Debug Log", systemImage: "ant.fill")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
Section(header: Text("GPIO")) {
Picker("Button GPIO", selection: $buttonGPIO) {
ForEach(0..<46) {
if $0 == 0 {
@ -136,14 +123,11 @@ struct DeviceConfig: View {
}
.pickerStyle(DefaultPickerStyle())
}
}
.disabled(self.bleManager.connectedPeripheral == nil || node?.deviceConfig == nil)
// Only show these buttons for the BLE connected node
if bleManager.connectedPeripheral != nil && node?.num ?? -1 == bleManager.connectedPeripheral.num {
HStack {
Button("Reset NodeDB", role: .destructive) {
isPresentingNodeDBResetConfirm = true
}
@ -151,14 +135,13 @@ struct DeviceConfig: View {
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.padding(.leading)
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingNodeDBResetConfirm,
titleVisibility: .visible
) {
Button("Erase all device and app data?", role: .destructive) {
if bleManager.sendNodeDBReset(fromUser: node!.user!, toUser: node!.user!) {
bleManager.disconnectPeripheral()
clearCoreDataDatabase(context: context)
@ -174,30 +157,26 @@ struct DeviceConfig: View {
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.padding(.trailing)
.confirmationDialog(
"All device and app data will be deleted. You will also need to forget your devices under Settings > Bluetooth.",
isPresented: $isPresentingFactoryResetConfirm,
titleVisibility: .visible
) {
Button("Factory reset your device and app? ", role: .destructive) {
if bleManager.sendFactoryReset(fromUser: node!.user!, toUser: node!.user!) {
bleManager.disconnectPeripheral()
clearCoreDataDatabase(context: context)
} else {
print("Factory Reset Failed")
}
}
}
}
}
HStack {
Button {
isPresentingSaveConfirm = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
}
@ -207,7 +186,6 @@ struct DeviceConfig: View {
.controlSize(.large)
.padding()
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingSaveConfirm,
titleVisibility: .visible
@ -226,7 +204,6 @@ struct DeviceConfig: View {
dc.rebroadcastMode = RebroadcastModes(rawValue: rebroadcastMode)?.protoEnumValue() ?? RebroadcastModes.all.protoEnumValue()
dc.doubleTapAsButtonPress = doubleTapAsButtonPress
dc.isManaged = isManaged
let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
@ -251,7 +228,6 @@ struct DeviceConfig: View {
.onAppear {
self.bleManager.context = context
setDeviceValues()
// Need to request a LoRaConfig from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.deviceConfig == nil {
print("empty device config")

View file

@ -73,14 +73,12 @@ struct DisplayConfig: View {
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("The compass heading on the screen outside of the circle will always point north.")
.font(.caption)
Toggle(isOn: $wakeOnTapOrMotion) {
Label("Wake Screen on tap or motion", systemImage: "gyroscope")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("Requires that there be an accelerometer on your device.")
.font(.caption)
Toggle(isOn: $flipScreen) {
Label("Flip Screen", systemImage: "pip.swap")
@ -97,7 +95,6 @@ struct DisplayConfig: View {
.pickerStyle(DefaultPickerStyle())
Text("Override automatic OLED screen detection.")
.font(.caption)
}
Section(header: Text("Timing & Format")) {
Picker("Screen on for", selection: $screenOnSeconds ) {
@ -238,7 +235,6 @@ struct DisplayConfig: View {
}
}
func setDisplayValues() {
self.gpsFormat = Int(node?.displayConfig?.gpsFormat ?? 0)
self.screenOnSeconds = Int(node?.displayConfig?.screenOnSeconds ?? 0)
self.screenCarouselInterval = Int(node?.displayConfig?.screenCarouselInterval ?? 0)

View file

@ -57,7 +57,6 @@ struct LoRaConfig: View {
Text("LoRa config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
.font(.callout)
.foregroundColor(.orange)
} else {
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
.font(.title3)
@ -144,7 +143,7 @@ struct LoRaConfig: View {
}
}
.pickerStyle(DefaultPickerStyle())
Text("Sets the maximum number of hops, default is 3. Increasing hops also increases air time utilization and should be used carefully.")
Text("Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully.")
.font(.caption)
HStack {

View file

@ -0,0 +1,239 @@
//
// DetectionSensorModule.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 8/16/23.
//
import SwiftUI
struct DetectionSensorConfig: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@Environment(\.dismiss) private var goBack
var node: NodeInfoEntity?
@State private var isPresentingSaveConfirm: Bool = false
@State var hasChanges: Bool = false
@State var enabled = false
/// DetectionSensorModule will sends a bell character with the messages.
@State var sendBell: Bool = false
@State var name: String = ""
@State var detectionTriggeredHigh: Bool = true
@State var usePullup: Bool = false
@State var minimumBroadcastSecs = 0
@State var stateBroadcastSecs = 0
@State var monitorPin = 0
var body: some View {
Form {
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
Text("There has been no response to a request for device metadata over the admin channel for this node.")
.font(.callout)
.foregroundColor(.orange)
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
// Let users know what is going on if they are using remote admin and don't have the config yet
if node?.detectionSensorConfig == nil {
Text("Detection Sensor config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
.font(.callout)
.foregroundColor(.orange)
} else {
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
.font(.title3)
.onAppear {
setDetectionSensorValues()
}
}
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
.font(.title3)
} else {
Text("Please connect to a radio to configure settings.")
.font(.callout)
.foregroundColor(.orange)
}
Section(header: Text("options")) {
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "dot.radiowaves.right")
}
Toggle(isOn: $sendBell) {
Label("Send Bell", systemImage: "bell")
}
TextField("Friendly name (sent for detection alerts text messages)", text: $name, axis: .vertical)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: name, perform: { _ in
let totalBytes = name.utf8.count
// Only mess with the value if it is too big
if totalBytes > 20 {
let firstNBytes = Data(name.utf8.prefix(20))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the shortName back to the last place where it was the right size
name = maxBytesString
}
}
})
.foregroundColor(.gray)
}
Section(header: Text("Sensor option")) {
Picker("GPIO Pin to monitor", selection: $monitorPin) {
ForEach(0..<46) {
if $0 == 0 {
Text("unset")
} else {
Text("Pin \($0)")
}
}
}
.pickerStyle(DefaultPickerStyle())
Toggle(isOn: $detectionTriggeredHigh) {
Label("Detection trigger High", systemImage: "dial.high")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $usePullup) {
Label("Uses pullup resistor", systemImage: "arrow.up.to.line")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
Section(header: Text("update.interval")) {
Picker("Minimum time between detection broadcasts", selection: $minimumBroadcastSecs) {
ForEach(UpdateIntervals.allCases) { ui in
Text(ui.description).tag(ui.rawValue)
}
}
.pickerStyle(DefaultPickerStyle())
Text("Mininum time between detection broadcasts. Default is 45 seconds.")
.font(.caption)
Picker("State Broadcast Interval", selection: $stateBroadcastSecs) {
Text("Never").tag(0)
ForEach(UpdateIntervals.allCases) { ui in
Text(ui.description).tag(ui.rawValue)
}
}
.pickerStyle(DefaultPickerStyle())
Text("How often to send detection sensor state to mesh regardless of detection. Default is Never.")
.font(.caption)
}
}
.scrollDismissesKeyboard(.interactively)
.disabled(self.bleManager.connectedPeripheral == nil || node?.detectionSensorConfig == nil)
Button {
isPresentingSaveConfirm = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
}
.disabled(bleManager.connectedPeripheral == nil || !hasChanges)
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingSaveConfirm,
titleVisibility: .visible
) {
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
if connectedNode != nil {
let nodeName = node?.user?.longName ?? "unknown".localized
let buttonText = String.localizedStringWithFormat("save.config %@".localized, nodeName)
Button(buttonText) {
var dsc = ModuleConfig.DetectionSensorConfig()
dsc.enabled = self.enabled
dsc.sendBell = self.sendBell
dsc.name = self.name
dsc.monitorPin = UInt32(self.monitorPin)
dsc.detectionTriggeredHigh = self.detectionTriggeredHigh
dsc.usePullup = self.usePullup
dsc.minimumBroadcastSecs = UInt32(self.minimumBroadcastSecs)
dsc.stateBroadcastSecs = UInt32(self.stateBroadcastSecs)
let adminMessageId = bleManager.saveDetectionSensorModuleConfig(config: dsc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
hasChanges = false
goBack()
}
}
}
}
message: {
Text("config.save.confirm")
}
.navigationTitle("detection.sensor.config")
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
})
.onAppear {
self.bleManager.context = context
setDetectionSensorValues()
// Need to request a Detection Sensor Module Config from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.detectionSensorConfig == nil {
print("empty detection sensor module config")
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
if node != nil && connectedNode != nil {
_ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
}
}
}
.onChange(of: enabled) { newEnabled in
if node != nil && node?.detectionSensorConfig != nil {
if newEnabled != node!.detectionSensorConfig!.enabled { hasChanges = true }
}
}
.onChange(of: sendBell) { newSendBell in
if node != nil && node?.detectionSensorConfig != nil {
if newSendBell != node!.detectionSensorConfig!.sendBell { hasChanges = true }
}
}
.onChange(of: detectionTriggeredHigh) { newDetectionTriggeredHigh in
if node != nil && node?.detectionSensorConfig != nil {
if newDetectionTriggeredHigh != node!.detectionSensorConfig!.detectionTriggeredHigh { hasChanges = true }
}
}
.onChange(of: usePullup) { newUsePullup in
if node != nil && node?.detectionSensorConfig != nil {
if newUsePullup != node!.detectionSensorConfig!.usePullup { hasChanges = true }
}
}
.onChange(of: name) { newName in
if node != nil && node?.detectionSensorConfig != nil {
if newName != node!.detectionSensorConfig!.name { hasChanges = true }
}
}
.onChange(of: monitorPin) { newMonitorPin in
if node != nil && node?.detectionSensorConfig != nil {
if newMonitorPin != node!.detectionSensorConfig!.monitorPin { hasChanges = true }
}
}
.onChange(of: minimumBroadcastSecs) { newMinimumBroadcastSecs in
if node != nil && node?.detectionSensorConfig != nil {
if newMinimumBroadcastSecs != node!.detectionSensorConfig!.minimumBroadcastSecs { hasChanges = true }
}
}
.onChange(of: stateBroadcastSecs) { newStateBroadcastSecs in
if node != nil && node?.detectionSensorConfig != nil {
if newStateBroadcastSecs != node!.detectionSensorConfig!.stateBroadcastSecs { hasChanges = true }
}
}
}
func setDetectionSensorValues() {
self.enabled = (node?.detectionSensorConfig?.enabled ?? false)
self.sendBell = (node?.detectionSensorConfig?.sendBell ?? false)
self.name = (node?.detectionSensorConfig?.name ?? "")
self.monitorPin = Int(node?.detectionSensorConfig?.monitorPin ?? 0)
self.usePullup = (node?.detectionSensorConfig?.usePullup ?? false)
self.detectionTriggeredHigh = (node?.detectionSensorConfig?.detectionTriggeredHigh ?? true)
self.minimumBroadcastSecs = Int(node?.detectionSensorConfig?.minimumBroadcastSecs ?? 45)
self.stateBroadcastSecs = Int(node?.detectionSensorConfig?.stateBroadcastSecs ?? 0)
self.hasChanges = false
}
}

View file

@ -23,10 +23,8 @@ struct MQTTConfig: View {
@State var jsonEnabled = false
@State var tlsEnabled = true
@State var root = "msh"
var body: some View {
Form {
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
Text("There has been no response to a request for device metadata over the admin channel for this node.")
@ -65,6 +63,8 @@ struct MQTTConfig: View {
Label("mqtt.clientproxy", systemImage: "iphone.radiowaves.left.and.right")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("If both MQTT and the client proxy are enabled your device will utalize an available network connection to connect to the specified MQTT server.")
.font(.caption2)
Toggle(isOn: $encryptionEnabled) {
@ -82,8 +82,6 @@ struct MQTTConfig: View {
Label("JSON Enabled", systemImage: "ellipsis.curlybraces")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("JSON mode is not reccomended it is incomplete and unstable.")
.font(.caption2)
}
Section(header: Text("Custom Server")) {
HStack {
@ -163,7 +161,6 @@ struct MQTTConfig: View {
}
.keyboardType(.default)
.scrollDismissesKeyboard(.interactively)
HStack {
Label("Root Topic", systemImage: "tree")
TextField("Root Topic", text: $root)
@ -187,7 +184,7 @@ struct MQTTConfig: View {
Text("The root topic to use for MQTT messages. Default is \"msh\". This is useful if you want to use a single MQTT server for multiple meshtastic networks and separate them via ACLs")
.font(.caption2)
}
Text("WiFi or Ethernet must also be enabled for MQTT to work. You can set uplink and downlink for each channel.")
Text("You can set uplink and downlink for each channel.")
.font(.callout)
}
.scrollDismissesKeyboard(.interactively)
@ -300,7 +297,6 @@ struct MQTTConfig: View {
}
}
}
func setMqttValues() {
self.enabled = (node?.mqttConfig?.enabled ?? false)
self.proxyToClientEnabled = (node?.mqttConfig?.proxyToClientEnabled ?? false)

View file

@ -72,13 +72,13 @@ struct RangeTestConfig: View {
.font(.caption)
}
}
.disabled(self.bleManager.connectedPeripheral == nil || node?.rangeTestConfig == nil || !(node != nil && node?.metadata?.hasWifi ?? false))
.disabled(self.bleManager.connectedPeripheral == nil || node?.rangeTestConfig == nil)
Button {
isPresentingSaveConfirm = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
}
.disabled(bleManager.connectedPeripheral == nil || !hasChanges || !(node?.metadata?.hasWifi ?? false))
.disabled(bleManager.connectedPeripheral == nil || !hasChanges)
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)

View file

@ -8,7 +8,6 @@
import SwiftUI
struct RtttlConfig: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@Environment(\.dismiss) private var goBack
@ -49,7 +48,6 @@ struct RtttlConfig: View {
.foregroundColor(.orange)
}
Section(header: Text("options")) {
HStack {
Label("ringtone", systemImage: "music.quarternote.3")
TextField("Ringtone Transfer Language", text: $ringtone, axis: .vertical)
@ -134,7 +132,6 @@ struct RtttlConfig: View {
}
}
}
func setRtttLConfigValue() {
self.ringtone = node?.rtttlConfig?.ringtone ?? ""
self.hasChanges = false

View file

@ -0,0 +1,186 @@
//
// StoreForward.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 8/26/23.
//
import SwiftUI
struct StoreForwardConfig: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@Environment(\.dismiss) private var goBack
var node: NodeInfoEntity?
@State private var isPresentingSaveConfirm: Bool = false
@State var hasChanges: Bool = false
/// Enable the Store and Forward Module
@State var enabled = false
/// Send a Heartbeat
@State var heartbeat: Bool = false
/// Number of Records
@State var records = 0
/// Max number of history items to return
@State var historyReturnMax = 0
/// Time window for history
@State var historyReturnWindow = 0
var body: some View {
Form {
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
Text("There has been no response to a request for device metadata over the admin channel for this node.")
.font(.callout)
.foregroundColor(.orange)
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
// Let users know what is going on if they are using remote admin and don't have the config yet
if node?.detectionSensorConfig == nil {
Text("Detection Sensor config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
.font(.callout)
.foregroundColor(.orange)
} else {
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
.font(.title3)
.onAppear {
setDetectionSensorValues()
}
}
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
.font(.title3)
} else {
Text("Please connect to a radio to configure settings.")
.font(.callout)
.foregroundColor(.orange)
}
Section(header: Text("options")) {
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "envelope.arrow.triangle.branch")
}
Toggle(isOn: $heartbeat) {
Label("storeforward.heartbeat", systemImage: "waveform.path.ecg")
}
Picker("Number of records", selection: $records) {
Text("unset").tag(0)
Text("25").tag(25)
Text("50").tag(50)
Text("75").tag(75)
Text("100").tag(100)
}
.pickerStyle(DefaultPickerStyle())
Picker("History Return Max", selection: $historyReturnMax ) {
Text("unset").tag(0)
Text("25").tag(25)
Text("50").tag(50)
Text("75").tag(75)
Text("100").tag(100)
}
.pickerStyle(DefaultPickerStyle())
Picker("History Return Window", selection: $historyReturnWindow ) {
Text("unset").tag(0)
Text("One Minute").tag(60)
Text("Five Minutes").tag(300)
Text("Ten Minutes").tag(600)
Text("Fifteen Minutes").tag(900)
Text("Thirty Minutes").tag(1800)
Text("One Hour").tag(3600)
}
.pickerStyle(DefaultPickerStyle())
}
}
.scrollDismissesKeyboard(.interactively)
.disabled(self.bleManager.connectedPeripheral == nil || node?.detectionSensorConfig == nil)
Button {
isPresentingSaveConfirm = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
}
.disabled(bleManager.connectedPeripheral == nil || !hasChanges)
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingSaveConfirm,
titleVisibility: .visible
) {
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
if connectedNode != nil {
let nodeName = node?.user?.longName ?? "unknown".localized
let buttonText = String.localizedStringWithFormat("save.config %@".localized, nodeName)
Button(buttonText) {
var sfc = ModuleConfig.StoreForwardConfig()
sfc.enabled = self.enabled
sfc.heartbeat = self.heartbeat
sfc.records = UInt32(self.records)
sfc.historyReturnMax = UInt32(self.historyReturnMax)
sfc.historyReturnWindow = UInt32(self.historyReturnWindow)
let adminMessageId = bleManager.saveStoreForwardModuleConfig(config: sfc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
hasChanges = false
goBack()
} }
}
}
message: {
Text("config.save.confirm")
}
.navigationTitle("storeforward.config")
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
})
.onAppear {
self.bleManager.context = context
setDetectionSensorValues()
// Need to request a Detection Sensor Module Config from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.detectionSensorConfig == nil {
print("empty store and forward module config")
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
if node != nil && connectedNode != nil {
_ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
}
}
}
.onChange(of: enabled) { newEnabled in
if node != nil && node?.detectionSensorConfig != nil {
if newEnabled != node!.detectionSensorConfig!.enabled { hasChanges = true }
}
}
.onChange(of: heartbeat) { newHeartbeat in
if node != nil && node?.storeForwardConfig != nil {
if newHeartbeat != node!.storeForwardConfig!.heartbeat { hasChanges = true }
}
}
.onChange(of: records) { newRecords in
if node != nil && node?.storeForwardConfig != nil {
if newRecords != node!.storeForwardConfig!.records { hasChanges = true }
}
}
.onChange(of: historyReturnMax) { newHistoryReturnMax in
if node != nil && node?.storeForwardConfig != nil {
if newHistoryReturnMax != node!.storeForwardConfig!.historyReturnMax { hasChanges = true }
}
}
.onChange(of: historyReturnWindow) { newHistoryReturnWindow in
if node != nil && node?.storeForwardConfig != nil {
if newHistoryReturnWindow != node!.storeForwardConfig!.historyReturnWindow { hasChanges = true }
}
}
}
func setDetectionSensorValues() {
self.enabled = (node?.storeForwardConfig?.enabled ?? false)
self.heartbeat = (node?.storeForwardConfig?.heartbeat ?? true)
self.records = Int(node?.storeForwardConfig?.records ?? 50)
self.historyReturnMax = Int(node?.storeForwardConfig?.historyReturnMax ?? 100)
self.historyReturnWindow = Int(node?.storeForwardConfig?.historyReturnWindow ?? 300)
self.hasChanges = false
}
}

View file

@ -106,7 +106,6 @@ struct NetworkConfig: View {
Text("Enabling WiFi will disable the bluetooth connection to the app.")
.font(.callout)
}
}
if (node != nil && node?.metadata?.hasEthernet ?? false) {
Section(header: Text("Ethernet Options")) {
@ -205,7 +204,6 @@ struct NetworkConfig: View {
}
}
}
func setNetworkValues() {
self.wifiEnabled = node?.networkConfig?.wifiEnabled ?? false
self.wifiSsid = node?.networkConfig?.wifiSsid ?? ""

View file

@ -113,13 +113,11 @@ struct PositionConfig: View {
.pickerStyle(DefaultPickerStyle())
Text("The maximum interval that can elapse without a node sending a position")
.font(.caption)
Toggle(isOn: $smartPositionEnabled) {
Label("Smart Position Broadcast", systemImage: "location.fill.viewfinder")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
if smartPositionEnabled {
Picker("Minimum Broadcast Interval", selection: $broadcastSmartMinimumIntervalSecs) {
ForEach(UpdateIntervals.allCases) { at in
@ -129,19 +127,15 @@ struct PositionConfig: View {
.pickerStyle(DefaultPickerStyle())
Text("The fastest that position updates will be sent if the minimum distance has been satisfied")
.font(.caption)
Picker("Minimum Distance", selection: $broadcastSmartMinimumDistance) {
ForEach(10..<151) {
if $0 == 0 {
Text("unset")
} else {
if $0.isMultiple(of: 5) {
Text("\($0)")
.tag($0)
}
}
}
}
@ -217,7 +211,6 @@ struct PositionConfig: View {
Label("Device GPS Enabled", systemImage: "location")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
if deviceGpsEnabled {
Picker("Update Interval", selection: $gpsUpdateInterval) {
ForEach(GpsUpdateIntervals.allCases) { ui in
@ -232,9 +225,6 @@ struct PositionConfig: View {
}
}
.pickerStyle(DefaultPickerStyle())
Text("How long should we try to get our position during each GPS Update Interval attempt?")
.font(.caption)
Picker("GPS Receive GPIO", selection: $rxGpio) {
ForEach(0..<46) {
if $0 == 0 {
@ -245,7 +235,6 @@ struct PositionConfig: View {
}
}
.pickerStyle(DefaultPickerStyle())
Picker("GPS Transmit GPIO", selection: $txGpio) {
ForEach(0..<46) {
if $0 == 0 {
@ -318,8 +307,7 @@ struct PositionConfig: View {
pc.positionFlags = UInt32(pf.rawValue)
let adminMessageId = bleManager.savePositionConfig(config: pc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
// Disable the button after a successful save
hasChanges = false
goBack()
}
@ -457,9 +445,7 @@ struct PositionConfig: View {
if existingValue != hvdopFlag { hasChanges = true }
}
}
func setPositionValues() {
self.smartPositionEnabled = node?.positionConfig?.smartPositionEnabled ?? true
self.deviceGpsEnabled = node?.positionConfig?.deviceGpsEnabled ?? true
self.rxGpio = Int(node?.positionConfig?.rxGpio ?? 0)

View file

@ -15,23 +15,16 @@ import SwiftUI
import StoreKit
struct Firmware: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
var node: NodeInfoEntity?
@State var minimumVersion = "2.1.0"
@State var version = ""
@State private var firmwareReleaseData: FirmwareRelease = FirmwareRelease()
var body: some View {
// NavigationSplitView {
NavigationStack {
let hwModel: HardwareModels = HardwareModels.allCases.first(where: { $0.rawValue == node?.user?.hwModel ?? "UNSET" }) ?? HardwareModels.UNSET
VStack(alignment: .leading) {
Text("Current Version: \(bleManager.connectedVersion)")
.font(.largeTitle)
@ -78,7 +71,7 @@ struct Firmware: View {
if connectedNode != nil {
if !bleManager.sendRebootOta(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex) {
print("Reboot Failed")
}
}
}
} label: {
Label("Send Reboot OTA", systemImage: "square.and.arrow.down")
@ -98,7 +91,6 @@ struct Firmware: View {
Text(hwModel.platform().description)
.font(.title3)
}
}.padding()
VStack(alignment: .leading) {
Text("Firmware Releases")
@ -152,85 +144,65 @@ struct Firmware: View {
.navigationBarTitleDisplayMode(.inline)
}
}
func loadData() {
guard let url = URL(string: "https://api.meshtastic.org/github/firmware/list") else {
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, _, _ in
if let data = data {
if let response_obj = try? JSONDecoder().decode(FirmwareRelease.self, from: data) {
DispatchQueue.main.async {
self.firmwareReleaseData = response_obj
}
}
}
}.resume()
}
}
struct FirmwareRelease: Codable {
var releases: Releases? = Releases()
var pullRequests: [PullRequests]? = []
enum CodingKeys: String, CodingKey {
case releases = "releases"
case pullRequests = "pullRequests"
case releases = "Releases"
case pullRequests = "Pull Requests"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
releases = try values.decodeIfPresent(Releases.self, forKey: .releases )
pullRequests = try values.decodeIfPresent([PullRequests].self, forKey: .pullRequests )
}
init() {
}
}
struct Releases: Codable {
var stable: [Stable]? = []
var alpha: [Alpha]? = []
enum CodingKeys: String, CodingKey {
case stable = "stable"
case alpha = "alpha"
case stable = "Stable"
case alpha = "Alpha"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
stable = try values.decodeIfPresent([Stable].self, forKey: .stable )
alpha = try values.decodeIfPresent([Alpha].self, forKey: .alpha )
}
init() {}
}
struct Alpha: Codable {
var id: String?
var title: String?
var pageUrl: String?
var zipUrl: String?
enum CodingKeys: String, CodingKey {
case id = "id"
case title = "title"
case pageUrl = "page_url"
case zipUrl = "zip_url"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(String.self, forKey: .id )
@ -238,24 +210,20 @@ struct Alpha: Codable {
pageUrl = try values.decodeIfPresent(String.self, forKey: .pageUrl )
zipUrl = try values.decodeIfPresent(String.self, forKey: .zipUrl )
}
init() {}
}
struct Stable: Codable {
var id: String?
var title: String?
var pageUrl: String?
var zipUrl: String?
enum CodingKeys: String, CodingKey {
case id = "id"
case title = "title"
case pageUrl = "page_url"
case zipUrl = "zip_url"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(String.self, forKey: .id )
@ -263,24 +231,20 @@ struct Stable: Codable {
pageUrl = try values.decodeIfPresent(String.self, forKey: .pageUrl )
zipUrl = try values.decodeIfPresent(String.self, forKey: .zipUrl )
}
init() {}
}
struct PullRequests: Codable {
var id: String?
var title: String?
var pageUrl: String?
var zipUrl: String?
enum CodingKeys: String, CodingKey {
case id = "id"
case title = "title"
case pageUrl = "page_url"
case zipUrl = "zip_url"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(String.self, forKey: .id )
@ -288,6 +252,5 @@ struct PullRequests: Codable {
pageUrl = try values.decodeIfPresent(String.self, forKey: .pageUrl )
zipUrl = try values.decodeIfPresent(String.self, forKey: .zipUrl )
}
init() {}
}

View file

@ -67,23 +67,24 @@ struct MeshLog: View {
print(error)
}
} label: {
Label("Clear Log", systemImage: "trash.fill")
Label("Clear", systemImage: "trash.fill")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
Spacer()
.padding(.bottom)
.padding(.leading)
Button {
isExporting = true
} label: {
Label("Save Log", systemImage: "square.and.arrow.down")
Label("Save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.padding(.bottom)
.padding(.trailing)
Spacer()
}
.padding(.bottom, 10)

View file

@ -8,7 +8,6 @@
import SwiftUI
struct Settings: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "user.longName", ascending: true)], animation: .default)
@ -16,9 +15,7 @@ struct Settings: View {
@State private var selectedNode: Int = 0
@State private var connectedNodeNum: Int = 0
@State private var initialLoad: Bool = true
@State private var selection: SettingsSidebar = .about
enum SettingsSidebar {
case appSettings
case shareChannels
@ -31,6 +28,7 @@ struct Settings: View {
case networkConfig
case positionConfig
case cannedMessagesConfig
case detectionSensorConfig
case externalNotificationConfig
case mqttConfig
case rangeTestConfig
@ -41,7 +39,6 @@ struct Settings: View {
case adminMessageLog
case about
}
var body: some View {
NavigationSplitView {
List {
@ -50,7 +47,6 @@ struct Settings: View {
} label: {
Image(systemName: "questionmark.app")
.symbolRenderingMode(.hierarchical)
Text("about.meshtastic")
}
.tag(SettingsSidebar.about)
@ -64,7 +60,6 @@ struct Settings: View {
.tag(SettingsSidebar.appSettings)
let node = nodes.first(where: { $0.num == connectedNodeNum })
if !(node?.deviceConfig?.isManaged ?? false) {
Section("Configure") {
Picker("Configuring Node", selection: $selectedNode) {
if selectedNode == 0 {
@ -90,10 +85,8 @@ struct Settings: View {
let node = nodes.first(where: { $0.num == newValue })
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
if connectedNode != nil && connectedNode?.user != nil && connectedNode?.myInfo != nil && node?.user != nil && node?.metadata == nil {
let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context)
if adminMessageId > 0 {
print("Sent node metadata request from node details")
}
@ -102,7 +95,6 @@ struct Settings: View {
}
}
Section("radio.configuration") {
NavigationLink {
ShareChannels(node: nodes.first(where: { $0.num == connectedNodeNum }))
} label: {
@ -112,17 +104,14 @@ struct Settings: View {
}
.tag(SettingsSidebar.shareChannels)
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
NavigationLink {
UserConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "person.crop.rectangle.fill")
.symbolRenderingMode(.hierarchical)
Text("user")
}
.tag(SettingsSidebar.userConfig)
NavigationLink {
LoRaConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
@ -131,7 +120,6 @@ struct Settings: View {
Text("lora")
}
.tag(SettingsSidebar.loraConfig)
NavigationLink {
Channels(node: nodes.first(where: { $0.num == connectedNodeNum }))
} label: {
@ -141,7 +129,6 @@ struct Settings: View {
}
.tag(SettingsSidebar.channelConfig)
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
NavigationLink {
BluetoothConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
@ -150,7 +137,6 @@ struct Settings: View {
Text("bluetooth")
}
.tag(SettingsSidebar.bluetoothConfig)
NavigationLink {
DeviceConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
@ -159,7 +145,6 @@ struct Settings: View {
Text("device")
}
.tag(SettingsSidebar.deviceConfig)
NavigationLink {
DisplayConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
@ -168,41 +153,40 @@ struct Settings: View {
Text("display")
}
.tag(SettingsSidebar.displayConfig)
NavigationLink {
NetworkConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "network")
.symbolRenderingMode(.hierarchical)
Text("network")
}
.tag(SettingsSidebar.networkConfig)
NavigationLink {
PositionConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "location")
.symbolRenderingMode(.hierarchical)
Text("position")
}
.tag(SettingsSidebar.positionConfig)
}
Section("module.configuration") {
NavigationLink {
CannedMessagesConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "list.bullet.rectangle.fill")
.symbolRenderingMode(.hierarchical)
Text("canned.messages")
}
.tag(SettingsSidebar.cannedMessagesConfig)
NavigationLink {
DetectionSensorConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "sensor")
.symbolRenderingMode(.hierarchical)
Text("detection.sensor")
}
.tag(SettingsSidebar.detectionSensorConfig)
NavigationLink {
ExternalNotificationConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
@ -211,7 +195,6 @@ struct Settings: View {
Text("external.notification")
}
.tag(SettingsSidebar.externalNotificationConfig)
NavigationLink {
MQTTConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
@ -220,7 +203,6 @@ struct Settings: View {
Text("mqtt")
}
.tag(SettingsSidebar.mqttConfig)
NavigationLink {
RangeTestConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
@ -236,7 +218,6 @@ struct Settings: View {
Text("ringtone")
}
.tag(SettingsSidebar.ringtoneConfig)
NavigationLink {
SerialConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
@ -245,7 +226,14 @@ struct Settings: View {
Text("serial")
}
.tag(SettingsSidebar.serialConfig)
NavigationLink {
StoreForwardConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "envelope.arrow.triangle.branch")
.symbolRenderingMode(.hierarchical)
Text("storeforward")
}
.tag(SettingsSidebar.serialConfig)
NavigationLink {
TelemetryConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
@ -255,7 +243,6 @@ struct Settings: View {
}
.tag(SettingsSidebar.telemetryConfig)
}
Section(header: Text("logging")) {
NavigationLink {
MeshLog()
@ -265,7 +252,6 @@ struct Settings: View {
Text("mesh.log")
}
.tag(SettingsSidebar.meshLog)
NavigationLink {
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
AdminMessageList(user: connectedNode?.user)
@ -281,8 +267,7 @@ struct Settings: View {
Firmware(node: nodes.first(where: { $0.num == connectedNodeNum }))
} label: {
Image(systemName: "arrow.up.arrow.down.square")
.symbolRenderingMode(.hierarchical)
.symbolRenderingMode(.hierarchical)
Text("Firmware Updates")
}
.tag(SettingsSidebar.about)

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

View file

@ -43,15 +43,12 @@ struct BatteryIcon: View {
.font(font)
.foregroundColor(color)
.symbolRenderingMode(.hierarchical)
} else if batteryLevel! == 0 {
Image(systemName: "battery.0")
.font(font)
.foregroundColor(.red)
.symbolRenderingMode(.hierarchical)
} else if batteryLevel! > 100 {
Image(systemName: "powerplug")
.font(font)
.foregroundColor(color)

View file

@ -8,7 +8,6 @@
import WidgetKit
import SwiftUI
@available(iOSApplicationExtension 16.2, *)
@main
struct WidgetsBundle: WidgetBundle {
var body: some Widget {

View file

@ -63,7 +63,7 @@ struct WidgetsLiveActivity: Widget {
.tint(Color("LightIndigo"))
}
DynamicIslandExpandedRegion(.bottom){
DynamicIslandExpandedRegion(.bottom) {
Text(context.attributes.name)
.font(context.attributes.name.count > 14 ? .callout : .title3)
.fontWeight(.semibold)
@ -76,7 +76,7 @@ struct WidgetsLiveActivity: Widget {
}
} compactLeading: {
Image("logo-black")
Image("m-logo-black")
.resizable()
.frame(width: 30.0)
.padding(4)
@ -87,7 +87,7 @@ struct WidgetsLiveActivity: Widget {
.foregroundColor(Color("LightIndigo"))
.frame(width: 40)
} minimal: {
Image("logo-black")
Image("m-logo-black")
.resizable()
.frame(width: 24.0)
.padding(4)
@ -137,7 +137,7 @@ struct LiveActivityView: View {
var body: some View {
HStack {
Image(colorScheme == .light ? "logo-black" : "logo-white")
Image(colorScheme == .light ? "m-logo-black" : "m-logo-white")
.resizable()
.clipShape(ContainerRelativeShape())
.opacity(isLuminanceReduced ? 0.5 : 1.0)

View file

@ -157,6 +157,7 @@
"mesh.log.display.config %@"="Display Konfiguration empfangen: %@";
"mesh.log.devicemetadata %@"="Anforderung der Geräte Metadaten für %@";
"mesh.log.device.metadata.received %@"="Device Metadata received from: %@";
"mesh.log.detectionsensor.config %@"="Detection Sensor module config received: %@";
"mesh.log.externalnotification.config %@"="External Notifiation module config received: %@";
"mesh.log.lora.config %@"="LoRa config received: %@";
"mesh.log.lora.config.sent %@"="Sent a LoRa.Config for: %@";
@ -171,6 +172,7 @@
"mesh.log.routing.message %@ %@"="Routing empfangen für RequestID: %@ Ack Status: %@";
"mesh.log.serial.config %@"="Serial Modul Konfiguration empfangen: %@";
"mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@";
"mesh.log.storeforward.config %@"="Store & Forward module config received: %@";
"mesh.log.telemetry.config %@"="Telemetrie Modul Konfiguration empfangen: %@";
"mesh.log.telemetry.received %@"="Telemetrie empfangen für: %@";
"mesh.log.textmessage.received"="Nachricht von der Textnachricht-App empfangen.";
@ -194,6 +196,7 @@
"name"="Name";
"network"="Netzwerk";
"network.config"="Netzwerkeinstellungen";
"nodes"="Nodes";
"nodes %@"="Nodes (%@)";
"no.nodes"="Keine Meshtastic Nodes gefunden";
"not.connected"="Kein Gerät verbunden";
@ -252,6 +255,9 @@
"set.region"="Setze LoRa Region";
"standard"="Standard";
"standard.muted"="Standard Muted";
"storeforward"="Store & Forward";
"storeforward.config"="Store & Forward Config";
"storeforward.heartbeat"="Send Heartbeat";
"ssid"="SSID";
"tapback"="Tapback Response";
"tapback.heart"="Gehört";

View file

@ -57,6 +57,9 @@
"current"="Current";
"default"="Default";
"delete"="Delete";
"detection.sensor"="Detection Sensor";
"detection.sensor.config"="Detection Sensor Config";
"detection.sensor.log"="Detection Sensor Log";
"device"="Device";
"device.config"="Device Config";
"device.metrics.delete"="Delete all device metrics?";
@ -157,6 +160,7 @@
"mesh.log.display.config %@"="Display config received: %@";
"mesh.log.devicemetadata %@"="Requesting Device Metadata for %@";
"mesh.log.device.metadata.received %@"="Device Metadata received from: %@";
"mesh.log.detectionsensor.config %@"="Detection Sensor module config received: %@";
"mesh.log.externalnotification.config %@"="External Notifiation module config received: %@";
"mesh.log.lora.config %@"="LoRa config received: %@";
"mesh.log.lora.config.sent %@"="Sent a LoRa.Config for: %@";
@ -171,6 +175,7 @@
"mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@";
"mesh.log.serial.config %@"="Serial module config received: %@";
"mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@";
"mesh.log.storeforward.config %@"="Store & Forward module config received: %@";
"mesh.log.telemetry.config %@"="Telemetry module config received: %@";
"mesh.log.telemetry.received %@"="Telemetry received for: %@";
"mesh.log.textmessage.received"="Message received from the text message app.";
@ -194,6 +199,7 @@
"name"="Name";
"network"="Network";
"network.config"="Network Config";
"nodes"="Nodes";
"nodes %@"="Nodes (%@)";
"no.nodes"="No Meshtastic Nodes Found";
"not.connected"="No device connected";
@ -252,6 +258,9 @@
"set.region"="Set LoRa Region";
"standard"="Standard";
"standard.muted"="Standard Muted";
"storeforward"="Store & Forward";
"storeforward.config"="Store & Forward Config";
"storeforward.heartbeat"="Send Heartbeat";
"ssid"="SSID";
"tapback"="Tapback Response";
"tapback.heart"="Heart";

View file

@ -274,4 +274,4 @@
"user"="Użytkownik";
"user.details"="Szczegóły użytkownika";
"voltage"="Napięcie";
"waiting"="Czekam. . .";
"waiting"="Czekam. . .";

6
swiftlint.geojson Normal file
View file

@ -0,0 +1,6 @@
{
"type": "MultiPolygon",
"coordinates": [
]
}

4
thebenternify.sh Executable file
View file

@ -0,0 +1,4 @@
#!/bin/bash
sed -i '' -e 's/GCH7VS5Y9R/6YF6QJH524/g' ./Meshtastic.xcodeproj/project.pbxproj
sed -i '' -e 's/gvh.Meshtastic/thebentern.Meshtastic/g' ./Meshtastic.xcodeproj/project.pbxproj

4
unthebenternify.sh Executable file
View file

@ -0,0 +1,4 @@
#!/bin/bash
sed -i '' -e 's/6YF6QJH524/GCH7VS5Y9R/g' ./Meshtastic.xcodeproj/project.pbxproj
sed -i '' -e 's/thebentern.Meshtastic/gvh.Meshtastic/g' ./Meshtastic.xcodeproj/project.pbxproj

View file

@ -157,6 +157,7 @@
"mesh.log.display.config %@"="Display config received: %@";
"mesh.log.devicemetadata %@"="Requesting Device Metadata for %@";
"mesh.log.device.metadata.received %@"="Device Metadata admin message received from: %@";
"mesh.log.detectionsensor.config %@"="Detection Sensor module config received: %@";
"mesh.log.externalnotification.config %@"="External Notifiation module config received: %@";
"mesh.log.lora.config %@"="LoRa config received: %@";
"mesh.log.lora.config.sent %@"="Sent a LoRa.Config for: %@";
@ -171,6 +172,7 @@
"mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@";
"mesh.log.serial.config %@"="Serial module config received: %@";
"mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@";
"mesh.log.storeforward.config %@"="Store & Forward module config received: %@";
"mesh.log.telemetry.config %@"="Telemetry module config received: %@";
"mesh.log.telemetry.received %@"="Telemetry received for: %@";
"mesh.log.textmessage.received"="Message received from the text message app.";
@ -194,6 +196,7 @@
"name"="名称";
"network"="网络";
"network.config"="网络配置";
"nodes"="节点";
"nodes %@"="节点 (%@)";
"no.nodes"="未找到 Meshtastic 节点";
"not.connected"="未连接到电台";
@ -253,6 +256,9 @@
"standard"="标准";
"standard.muted"="Standard Muted";
"ssid"="SSID";
"storeforward"="Store & Forward";
"storeforward.config"="Store & Forward Config";
"storeforward.heartbeat"="Send Heartbeat";
"tapback"="Tapback Response";
"tapback.heart"="Heart";
"tapback.thumbsup"="Thumbs Up";