Merge branch '2.0.11_Working_Changes' into main

This commit is contained in:
Garth Vander Houwen 2023-01-10 08:46:59 -08:00 committed by GitHub
commit 3c4ad682f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 2626 additions and 2039 deletions

View file

@ -7,7 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
C9483F6D2773017500998F6B /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9483F6C2773017500998F6B /* MapView.swift */; };
C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */; };
C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; };
C9A7BC1027759A9600760B50 /* PositionAnnotationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */; };
@ -22,6 +21,7 @@
DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; };
DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553562855B02500E55709 /* LoRaConfig.swift */; };
DD2553592855B52700E55709 /* PositionConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553582855B52700E55709 /* PositionConfig.swift */; };
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */; };
DD2E65262767A01F00E45FC5 /* NodeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2E65252767A01F00E45FC5 /* NodeDetail.swift */; };
DD3501892852FC3B000FC853 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3501882852FC3B000FC853 /* Settings.swift */; };
DD35018B2852FC79000FC853 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD35018A2852FC79000FC853 /* UserSettings.swift */; };
@ -36,7 +36,6 @@
DD457188293C7E63000C49FB /* SignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD457187293C7E63000C49FB /* SignalStrengthIndicator.swift */; };
DD47E3CE26F103C600029299 /* NodeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3CD26F103C600029299 /* NodeList.swift */; };
DD47E3D626F17ED900029299 /* CircleText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3D526F17ED900029299 /* CircleText.swift */; };
DD47E3D926F3093800029299 /* MessageBubble.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3D826F3093800029299 /* MessageBubble.swift */; };
DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A911D2708C65400501B7E /* AppSettings.swift */; };
DD4C158C2824A91E0032668E /* module_config.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C158B2824A91E0032668E /* module_config.pb.swift */; };
DD4C158E2824AA7E0032668E /* config.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C158D2824AA7E0032668E /* config.pb.swift */; };
@ -69,6 +68,7 @@
DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */; };
DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */; };
DD97E96828EFE9A00056DDA4 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96728EFE9A00056DDA4 /* About.swift */; };
DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD994B68295F88B60013760A /* IntervalEnums.swift */; };
DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA0B6B1294CDC55001356EC /* Channels.swift */; };
DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */; };
DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */; };
@ -105,6 +105,7 @@
DDD3BBD5292D763200D609B3 /* MeshtasticTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */; };
DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */; };
DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */; };
DDE0F7C5295F77B700B8AAB3 /* AppSettingsEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */; };
DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; };
/* End PBXBuildFile section */
@ -127,7 +128,6 @@
/* Begin PBXFileReference section */
A65FA974296876BF00A97686 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
C9483F6C2773017500998F6B /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = "<group>"; };
C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMBTileOverlay.swift; sourceTree = "<group>"; };
C9A7BC0F27759A9600760B50 /* PositionAnnotationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionAnnotationView.swift; sourceTree = "<group>"; };
C9A88B54278B503C00BD810A /* MapViewModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapViewModule.swift; sourceTree = "<group>"; };
@ -141,6 +141,7 @@
DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = "<group>"; };
DD2553562855B02500E55709 /* LoRaConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaConfig.swift; sourceTree = "<group>"; };
DD2553582855B52700E55709 /* PositionConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionConfig.swift; sourceTree = "<group>"; };
DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewSwiftUI.swift; sourceTree = "<group>"; };
DD2E65252767A01F00E45FC5 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = "<group>"; };
DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
DD35018A2852FC79000FC853 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = "<group>"; };
@ -153,9 +154,9 @@
DD415827285859C4009B0E59 /* TelemetryConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryConfig.swift; sourceTree = "<group>"; };
DD41582928585C32009B0E59 /* RangeTestConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RangeTestConfig.swift; sourceTree = "<group>"; };
DD457187293C7E63000C49FB /* SignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalStrengthIndicator.swift; sourceTree = "<group>"; };
DD457BC4295D5E35004BCE4D /* MeshtasticDataModelV5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV5.xcdatamodel; sourceTree = "<group>"; };
DD47E3CD26F103C600029299 /* NodeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeList.swift; sourceTree = "<group>"; };
DD47E3D526F17ED900029299 /* CircleText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleText.swift; sourceTree = "<group>"; };
DD47E3D826F3093800029299 /* MessageBubble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBubble.swift; sourceTree = "<group>"; };
DD4A911D2708C65400501B7E /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
DD4C158B2824A91E0032668E /* module_config.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = module_config.pb.swift; sourceTree = "<group>"; };
DD4C158D2824AA7E0032668E /* config.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = config.pb.swift; sourceTree = "<group>"; };
@ -189,6 +190,7 @@
DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationManager.swift; sourceTree = "<group>"; };
DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticLogo.swift; sourceTree = "<group>"; };
DD97E96728EFE9A00056DDA4 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = "<group>"; };
DD994B68295F88B60013760A /* IntervalEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalEnums.swift; sourceTree = "<group>"; };
DDA0B6B1294CDC55001356EC /* Channels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channels.swift; sourceTree = "<group>"; };
DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelRoles.swift; sourceTree = "<group>"; };
DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshPackets.swift; sourceTree = "<group>"; };
@ -233,6 +235,7 @@
DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeshtasticTests.swift; sourceTree = "<group>"; };
DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeText.swift; sourceTree = "<group>"; };
DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntityExtension.swift; 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>"; };
DDF924C926FBB953009FE055 /* ConnectedDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -268,9 +271,9 @@
isa = PBXGroup;
children = (
C9A7BC0E27759A6800760B50 /* Custom */,
C9483F6C2773017500998F6B /* MapView.swift */,
C9A88B54278B503C00BD810A /* MapViewModule.swift */,
C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */,
DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */,
);
path = Map;
sourceTree = "<group>";
@ -341,10 +344,10 @@
children = (
DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */,
DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */,
DD2160AE28C5552500C17253 /* MQTTConfig.swift */,
DD41582928585C32009B0E59 /* RangeTestConfig.swift */,
DD6193782863875F00E59241 /* SerialConfig.swift */,
DD415827285859C4009B0E59 /* TelemetryConfig.swift */,
DD2160AE28C5552500C17253 /* MQTTConfig.swift */,
);
path = Module;
sourceTree = "<group>";
@ -362,6 +365,7 @@
DD8ED9C6289CE4A100B3B0AB /* Enums */ = {
isa = PBXGroup;
children = (
DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */,
DDB6ABD828B0A4BA00384BA1 /* BluetoothModes.swift */,
DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */,
DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */,
@ -373,6 +377,7 @@
DDB6ABE128B13FB500384BA1 /* PositionConfigEnums.swift */,
DD8ED9C7289CE4B900B3B0AB /* RoutingError.swift */,
DD1925B828CDA93900720036 /* SerialConfigEnums.swift */,
DD994B68295F88B60013760A /* IntervalEnums.swift */,
);
path = Enums;
sourceTree = "<group>";
@ -521,7 +526,6 @@
isa = PBXGroup;
children = (
DD47E3D526F17ED900029299 /* CircleText.swift */,
DD47E3D826F3093800029299 /* MessageBubble.swift */,
DDF924C926FBB953009FE055 /* ConnectedDevice.swift */,
DDC3B273283F411B00AC321C /* LastHeardText.swift */,
DDA6B2EA28420A7B003E8C16 /* NodeAnnotation.swift */,
@ -742,6 +746,7 @@
DDC4D568275499A500A4208E /* Persistence.swift in Sources */,
DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */,
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */,
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */,
DD6193792863875F00E59241 /* SerialConfig.swift in Sources */,
DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */,
DDAF8C6926ED0D070058C060 /* deviceonly.pb.swift in Sources */,
@ -757,6 +762,7 @@
DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */,
DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */,
DD3CC6C228EB9D4900FA9159 /* UpdateCoreData.swift in Sources */,
DDE0F7C5295F77B700B8AAB3 /* AppSettingsEnums.swift in Sources */,
DDB6ABE628B1406100384BA1 /* LoraConfigEnums.swift in Sources */,
DDB2CC6E27F3EB47009C5FCC /* telemetry.pb.swift in Sources */,
DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */,
@ -780,7 +786,6 @@
DD86D40C287F401000BAEB7A /* SaveChannelQRCode.swift in Sources */,
DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */,
DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */,
C9483F6D2773017500998F6B /* MapView.swift in Sources */,
DDAF8C5826ED07FD0058C060 /* mesh.pb.swift in Sources */,
DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */,
DD8ED9C52898D51F00B3B0AB /* NetworkConfig.swift in Sources */,
@ -797,8 +802,8 @@
DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */,
C9A88B57278B559900BD810A /* apponly.pb.swift in Sources */,
DD4C158E2824AA7E0032668E /* config.pb.swift in Sources */,
DD47E3D926F3093800029299 /* MessageBubble.swift in Sources */,
DDB6ABE228B13FB500384BA1 /* PositionConfigEnums.swift in Sources */,
DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */,
DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */,
DD73FD1128750779000852D6 /* PositionLog.swift in Sources */,
DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */,
@ -1004,7 +1009,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.0.9;
MARKETING_VERSION = 2.0.11;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1037,7 +1042,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.0.9;
MARKETING_VERSION = 2.0.11;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1213,12 +1218,13 @@
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
DD457BC4295D5E35004BCE4D /* MeshtasticDataModelV5.xcdatamodel */,
DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */,
DDCDC69A29467643004C1DDA /* MeshtasticDataModelV3.xcdatamodel */,
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
);
currentVersion = DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */;
currentVersion = DD457BC4295D5E35004BCE4D /* MeshtasticDataModelV5.xcdatamodel */;
name = Meshtastic.xcdatamodeld;
path = Meshtastic/Meshtastic.xcdatamodeld;
sourceTree = "<group>";

View file

@ -0,0 +1,93 @@
//
// AppSettingsEnums.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 12/30/22.
//
import Foundation
enum KeyboardType: Int, CaseIterable, Identifiable {
case defaultKeyboard = 0
case asciiCapable = 1
case twitter = 9
case emailAddress = 7
case numbersAndPunctuation = 2
var id: Int { self.rawValue }
var description: String {
get {
switch self {
case .defaultKeyboard:
return NSLocalizedString("default", comment: "Default Keyboard")
case .asciiCapable:
return NSLocalizedString("ascii.capable", comment: "ASCII Capable Keyboard")
case .twitter:
return NSLocalizedString("twitter", comment: "Twitter Keyboard")
case .emailAddress:
return NSLocalizedString("email.address", comment: "Email Address Keyboard")
case .numbersAndPunctuation:
return NSLocalizedString("numbers.punctuation", comment: "Numbers and Punctuation Keyboard")
}
}
}
}
enum MeshMapType: String, CaseIterable, Identifiable {
case satellite = "satellite"
case hybrid = "hybrid"
case standard = "standard"
var id: String { self.rawValue }
var description: String {
get {
switch self {
case .satellite:
return NSLocalizedString("satellite", comment: "Satellite Map Type")
case .standard:
return NSLocalizedString("standard", comment: "Standard Map Type")
case .hybrid:
return NSLocalizedString("hybrid", comment: "Hybrid Map Type")
}
}
}
}
enum LocationUpdateInterval: Int, CaseIterable, Identifiable {
case fiveSeconds = 5
case tenSeconds = 10
case fifteenSeconds = 15
case thirtySeconds = 30
case oneMinute = 60
case fiveMinutes = 300
case tenMinutes = 600
case fifteenMinutes = 900
var id: Int { self.rawValue }
var description: String {
get {
switch self {
case .fiveSeconds:
return NSLocalizedString("interval.five.seconds", comment: "Five Seconds")
case .tenSeconds:
return NSLocalizedString("interval.ten.seconds", comment: "Ten Seconds")
case .fifteenSeconds:
return NSLocalizedString("interval.fifteen.seconds", comment: "Fifteen Seconds")
case .thirtySeconds:
return NSLocalizedString("interval.thirty.seconds", comment: "Thirty Seconds")
case .oneMinute:
return NSLocalizedString("interval.one.minute", comment: "One Minute")
case .fiveMinutes:
return NSLocalizedString("interval.five.minutes", comment: "Five Minutes")
case .tenMinutes:
return NSLocalizedString("interval.ten.minutes", comment: "Ten Minutes")
case .fifteenMinutes:
return NSLocalizedString("interval.fifteen.minutes", comment: "Fifteen Minutes")
}
}
}
}

View file

@ -24,14 +24,14 @@ enum EthernetMode: Int, CaseIterable, Identifiable {
}
}
}
func protoEnumValue() -> Config.NetworkConfig.EthMode {
func protoEnumValue() -> Config.NetworkConfig.AddressMode {
switch self {
case .dhcp:
return Config.NetworkConfig.EthMode.dhcp
return Config.NetworkConfig.AddressMode.dhcp
case .staticip:
return Config.NetworkConfig.EthMode.static
return Config.NetworkConfig.AddressMode.static
}
}
}

View file

@ -0,0 +1,162 @@
//
// UpdateIntervals.swift
// Meshtastic
//
// Created by Garth Vander Houwen on 12/30/22.
//
import Foundation
enum OutputIntervals: Int, CaseIterable, Identifiable {
case unset = 0
case oneSecond = 1000
case twoSeconds = 2000
case threeSeconds = 3000
case fourSeconds = 4000
case fiveSeconds = 5000
case tenSeconds = 10000
case fifteenSeconds = 15000
case thirtySeconds = 30000
case oneMinute = 60000
var id: Int { self.rawValue }
var description: String {
get {
switch self {
case .unset:
return NSLocalizedString("unset", comment: "Unset")
case .oneSecond:
return NSLocalizedString("interval.one.second", comment: "One Second")
case .twoSeconds:
return NSLocalizedString("interval.two.seconds", comment: "Two Seconds")
case .threeSeconds:
return NSLocalizedString("interval.three.seconds", comment: "Three Seconds")
case .fourSeconds:
return NSLocalizedString("interval.four.seconds", comment: "Four Seconds")
case .fiveSeconds:
return NSLocalizedString("interval.five.seconds", comment: "Five Seconds")
case .tenSeconds:
return NSLocalizedString("interval.ten.seconds", comment: "Ten Seconds")
case .fifteenSeconds:
return NSLocalizedString("interval.fifteen.seconds", comment: "Fifteen Seconds")
case .thirtySeconds:
return NSLocalizedString("interval.thirty.seconds", comment: "Thirty Seconds")
case .oneMinute:
return NSLocalizedString("interval.one.minute", comment: "One Minute")
}
}
}
}
// Default of 0 is off
enum SenderIntervals: Int, CaseIterable, Identifiable {
case off = 0
case fifteenSeconds = 15
case thirtySeconds = 30
case oneMinute = 60
case fiveMinutes = 300
case tenMinutes = 600
case fifteenMinutes = 900
case thirtyMinutes = 1800
case oneHour = 3600
var id: Int { self.rawValue }
var description: String {
get {
switch self {
case .off:
return NSLocalizedString("off", comment: "Off")
case .fifteenSeconds:
return NSLocalizedString("interval.fifteen.seconds", comment: "Fifteen Seconds")
case .thirtySeconds:
return NSLocalizedString("interval.thirty.seconds", comment: "Thirty Seconds")
case .oneMinute:
return NSLocalizedString("interval.one.minute", comment: "One Minute")
case .fiveMinutes:
return NSLocalizedString("interval.five.minutes", comment: "Five Minutes")
case .tenMinutes:
return NSLocalizedString("interval.ten.minutes", comment: "Ten Minutes")
case .fifteenMinutes:
return NSLocalizedString("interval.fifteen.minutes", comment: "Fifteen Minutes")
case .thirtyMinutes:
return NSLocalizedString("interval.thirty.minutes", comment: "Thirty Minutes")
case .oneHour:
return NSLocalizedString("interval.one.hour", comment: "One Hour")
}
}
}
}
enum UpdateIntervals: Int, CaseIterable, Identifiable {
case fifteenSeconds = 15
case thirtySeconds = 30
case oneMinute = 60
case fiveMinutes = 300
case tenMinutes = 600
case fifteenMinutes = 900
case thirtyMinutes = 1800
case oneHour = 3600
case twoHours = 7200
case threeHours = 10800
case fourHours = 14400
case fiveHours = 18000
case sixHours = 21600
case twelveHours = 43200
case eighteenHours = 64800
case twentyFourHours = 86400
case thirtySixHours = 129600
case fortyeightHours = 172800
case seventyTwoHours = 259200
var id: Int { self.rawValue }
var description: String {
get {
switch self {
case .fifteenSeconds:
return NSLocalizedString("interval.fifteen.seconds", comment: "Fifteen Seconds")
case .thirtySeconds:
return NSLocalizedString("interval.thirty.seconds", comment: "Thirty Seconds")
case .oneMinute:
return NSLocalizedString("interval.one.minute", comment: "One Minute")
case .fiveMinutes:
return NSLocalizedString("interval.five.minutes", comment: "Five Minutes")
case .tenMinutes:
return NSLocalizedString("interval.ten.minutes", comment: "Ten Minutes")
case .fifteenMinutes:
return NSLocalizedString("interval.fifteen.minutes", comment: "Fifteen Minutes")
case .thirtyMinutes:
return NSLocalizedString("interval.thirty.minutes", comment: "Thirty Minutes")
case .oneHour:
return NSLocalizedString("interval.one.hour", comment: "One Hour")
case .twoHours:
return NSLocalizedString("interval.two.hours", comment: "Two Hours")
case .threeHours:
return NSLocalizedString("interval.three.hours", comment: "Three Hours")
case .fourHours:
return NSLocalizedString("interval.four.hours", comment: "Four Hours")
case .fiveHours:
return NSLocalizedString("interval.five.hours", comment: "Five Hours")
case .sixHours:
return NSLocalizedString("interval.six.hours", comment: "Six Hours")
case .twelveHours:
return NSLocalizedString("interval.twelve.hours", comment: "Twelve Hours")
case .eighteenHours:
return NSLocalizedString("interval.eighteen.hours", comment: "Eighteen Hours")
case .twentyFourHours:
return NSLocalizedString("interval.twentyfour.hours", comment: "Twenty Four Hours")
case .thirtySixHours:
return NSLocalizedString("interval.thirtysix.hours", comment: "Thirty Six Hours")
case .fortyeightHours:
return NSLocalizedString("interval.fortyeight.hours", comment: "Forty Eight Hours")
case .seventyTwoHours:
return NSLocalizedString("interval.seventytwo.hours", comment: "Seventy Two Hours")
}
}
}
}

View file

@ -7,52 +7,6 @@
import Foundation
enum PositionBroadcastIntervals: Int, CaseIterable, Identifiable {
case fifteenSeconds = 15
case thirtySeconds = 30
case oneMinute = 60
case fiveMinutes = 300
case tenMinutes = 600
case fifteenMinutes = 900
case thirtyMinutes = 1800
case oneHour = 3600
case sixHours = 21600
case twelveHours = 43200
case twentyFourHours = 86400
var id: Int { self.rawValue }
var description: String {
get {
switch self {
case .fifteenSeconds:
return NSLocalizedString("interval.fifteen.seconds", comment: "Fifteen Seconds")
case .thirtySeconds:
return NSLocalizedString("interval.thirty.seconds", comment: "Thirty Seconds")
case .oneMinute:
return NSLocalizedString("interval.one.minute", comment: "One Minute")
case .fiveMinutes:
return NSLocalizedString("interval.five.minutes", comment: "Five Minutes")
case .tenMinutes:
return NSLocalizedString("interval.ten.minutes", comment: "Ten Minutes")
case .fifteenMinutes:
return NSLocalizedString("interval.fifteen.minutes", comment: "Fifteen Minutes")
case .thirtyMinutes:
return NSLocalizedString("interval.thirty.minutes", comment: "Thirty Minutes")
case .oneHour:
return NSLocalizedString("interval.one.hour", comment: "One Hour")
case .sixHours:
return NSLocalizedString("interval.six.hours", comment: "Six Hours")
case .twelveHours:
return NSLocalizedString("interval.twelve.hours", comment: "Twelve Hours")
case .twentyFourHours:
return NSLocalizedString("interval.twentyfour.hours", comment: "Twenty Four Hours")
}
}
}
}
enum GpsFormats: Int, CaseIterable, Identifiable {
case gpsFormatDec = 0
@ -178,6 +132,8 @@ enum GpsAttemptTimes: Int, CaseIterable, Identifiable {
case oneMinute = 60
case twoMinutes = 120
case fiveMinutes = 300
case tenMinutes = 600
case fifteenMinutes = 900
var id: Int { self.rawValue }
var description: String {
@ -204,6 +160,10 @@ enum GpsAttemptTimes: Int, CaseIterable, Identifiable {
return NSLocalizedString("interval.two.minutes", comment: "Two Minutes")
case .fiveMinutes:
return NSLocalizedString("interval.five.minutes", comment: "Five Minutes")
case .tenMinutes:
return NSLocalizedString("interval.ten.minutes", comment: "Ten Minutes")
case .fifteenMinutes:
return NSLocalizedString("interval.fifteen.minutes", comment: "Fifteen Minutes")
}
}
}

View file

@ -165,7 +165,7 @@ enum SerialTimeoutIntervals: Int, CaseIterable, Identifiable {
get {
switch self {
case .unset:
return "Unset"
return NSLocalizedString("unset", comment: "Unset")
case .oneSecond:
return NSLocalizedString("interval.one.second", comment: "One Second")
case .fiveSeconds:

View file

@ -9,9 +9,11 @@ import SwiftUI
func TelemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> String {
var csvString: String = ""
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma")
if metricsType == 0 {
// Create Device Metrics Header
csvString = "Battery Level, Voltage, Channel Utilization, Airtime, Timestamp"
csvString = "\(NSLocalizedString("battery.level", comment: "")), \(NSLocalizedString("voltage", comment: "")), \(NSLocalizedString("channel.utilization", comment: "")), \(NSLocalizedString("airtime", comment: "")), \(NSLocalizedString("timestamp", comment: ""))"
for dm in telemetry{
if dm.metricsType == 0 {
csvString += "\n"
@ -23,12 +25,12 @@ func TelemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
csvString += ", "
csvString += String(dm.airUtilTx)
csvString += ", "
csvString += dm.time?.formattedDate(format: "yyyy-MM-dd HH:mm:ss") ?? NSLocalizedString("unknown.age", comment: "")
csvString += dm.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")
}
}
} else if metricsType == 1 {
// Create Environment Telemetry Header
csvString = "Temperature, Relative Humidity, Barometric Pressure, Gas Resistance, Voltage, Current"
csvString = "Temperature, Relative Humidity, Barometric Pressure, Gas Resistance, \(NSLocalizedString("voltage", comment: "")), \(NSLocalizedString("current", comment: "")), \(NSLocalizedString("timestamp", comment: ""))"
for dm in telemetry{
if dm.metricsType == 1 {
csvString += "\n"
@ -44,7 +46,7 @@ func TelemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
csvString += ", "
csvString += String(dm.current)
csvString += ", "
csvString += dm.time?.formattedDate(format: "yyyy-MM-dd HH:mm:ss") ?? NSLocalizedString("unknown.age", comment: "")
csvString += dm.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")
}
}
}
@ -53,8 +55,10 @@ func TelemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
func PositionToCsvFile(positions: [PositionEntity]) -> String {
var csvString: String = ""
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma")
// Create Position Header
csvString = "SeqNo, Latitude, Longitude, Alt, Sats, Speed, Heading, SNR, Timestamp"
csvString = "SeqNo, Latitude, Longitude, Altitude, Sats, Speed, Heading, SNR, \(NSLocalizedString("timestamp", comment: ""))"
for pos in positions {
csvString += "\n"
csvString += String(pos.seqNo)
@ -73,7 +77,7 @@ func PositionToCsvFile(positions: [PositionEntity]) -> String {
csvString += ", "
csvString += String(pos.snr)
csvString += ", "
csvString += pos.time?.formattedDate(format: "yyyy-MM-dd HH:mm:ss") ?? NSLocalizedString("unknown.age", comment: "")
csvString += pos.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")
}
return csvString
}

View file

@ -27,6 +27,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
@Published var invalidVersion = false
@Published var preferredPeripheral = false
@Published var isSwitchedOn: Bool = false
@Published var automaticallyReconnect: Bool = true
public var minimumVersion = "1.3.48"
public var connectedVersion: String
@ -104,13 +105,16 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
}
self.isConnected = false
self.isConnecting = false
self.lastConnectionError = "🚨 Connection failed after \(timeoutTimerCount) attempts to connect to \(name). You may need to forget your device under Settings > Bluetooth."
self.lastConnectionError = "🚨 " + String.localizedStringWithFormat(NSLocalizedString("ble.connection.timeout %d %@",
comment: "Connection failed after %d attempts to connect to %@. You may need to forget your device under Settings > Bluetooth."),
timeoutTimerCount, name)
MeshLogger.log(lastConnectionError)
self.timeoutTimerCount = 0
self.timeoutTimerRuns += 1
self.startScanning()
} else {
MeshLogger.log("🚨 BLE Connecting 2 Second Timeout Timer Fired \(timeoutTimerCount) Time(s): \(name)")
print("🚨 BLE Connecting 2 Second Timeout Timer Fired \(timeoutTimerCount) Time(s): \(name)")
}
}
@ -120,9 +124,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
DispatchQueue.main.async {
self.isConnecting = true
self.lastConnectionError = ""
self.automaticallyReconnect = true
}
if connectedPeripheral != nil {
MeshLogger.log(" BLE Disconnecting from: \(connectedPeripheral.name) to connect to \(peripheral.name ?? "Unknown")")
print(" BLE Disconnecting from: \(connectedPeripheral.name) to connect to \(peripheral.name ?? "Unknown")")
disconnectPeripheral()
}
centralManager?.connect(peripheral)
@ -135,13 +140,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
let context = ["name": "\(peripheral.name ?? "Unknown")"]
timeoutTimer = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(timeoutTimerFired), userInfo: context, repeats: true)
RunLoop.current.add(timeoutTimer!, forMode: .common)
MeshLogger.log(" BLE Connecting: \(peripheral.name ?? "Unknown")")
print(" BLE Connecting: \(peripheral.name ?? "Unknown")")
}
// Disconnect Connected Peripheral
func disconnectPeripheral() {
func disconnectPeripheral(reconnect: Bool = true) {
guard let connectedPeripheral = connectedPeripheral else { return }
automaticallyReconnect = reconnect
centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral)
FROMRADIO_characteristic = nil
isConnected = false
@ -186,13 +192,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
}
// Discover Services
peripheral.discoverServices([meshtasticServiceCBUUID])
MeshLogger.log("✅ BLE Connected: \(peripheral.name ?? "Unknown")")
print("✅ BLE Connected: \(peripheral.name ?? "Unknown")")
}
// Called when a Peripheral fails to connect
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
disconnectPeripheral()
MeshLogger.log("🚫 BLE Failed to Connect: \(peripheral.name ?? "Unknown")")
print("🚫 BLE Failed to Connect: \(peripheral.name ?? "Unknown")")
}
// Disconnect Peripheral Event
@ -206,26 +212,28 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
let errorCode = (e as NSError).code
if errorCode == 6 { // CBError.Code.connectionTimeout The connection has timed out unexpectedly.
// Happens when device is manually reset / powered off
lastConnectionError = "🚨 \(e.localizedDescription) The app will automatically reconnect to the preferred radio if it come back in range."
MeshLogger.log("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)")
lastConnectionError = "🚨" + String.localizedStringWithFormat(NSLocalizedString("ble.errorcode.6 %@",
comment: "The app will automatically reconnect to the preferred radio if it come back in range."),
e.localizedDescription)
print("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)")
} else if errorCode == 7 { // CBError.Code.peripheralDisconnected The specified device has disconnected from us.
// Seems to be what is received when a tbeam sleeps, immediately recconnecting does not work.
lastConnectionError = e.localizedDescription
MeshLogger.log("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)")
lastConnectionError = "🚨 \(e.localizedDescription)"
print("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)")
} else if errorCode == 14 { // Peer removed pairing information
// Forgetting and reconnecting seems to be necessary so we need to show the user an error telling them to do that
lastConnectionError = "🚨 \(e.localizedDescription) This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re-connecting to the radio."
MeshLogger.log("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(lastConnectionError)")
lastConnectionError = "🚨 " + String.localizedStringWithFormat(NSLocalizedString("ble.errorcode.14 %@",
comment: "This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re-connecting to the radio."),
e.localizedDescription)
print("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(lastConnectionError)")
} else {
lastConnectionError = e.localizedDescription
MeshLogger.log("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)")
lastConnectionError = "🚨 \(e.localizedDescription)"
print("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)")
}
} else {
// Disconnected without error which indicates user intent to disconnect
// Happens when swiping to disconnect
MeshLogger.log(" BLE Disconnected: \(peripheral.name ?? "Unknown"): User Initiated Disconnect")
print(" BLE Disconnected: \(peripheral.name ?? "Unknown"): User Initiated Disconnect")
}
// Start a scan so the disconnected peripheral is moved to the peripherals[] if it is awake
self.startScanning()
@ -240,7 +248,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
for service in services {
if service.uuid == meshtasticServiceCBUUID {
peripheral.discoverCharacteristics([TORADIO_UUID, FROMRADIO_UUID, FROMNUM_UUID], for: service)
MeshLogger.log("✅ BLE Service for Meshtastic discovered by \(peripheral.name ?? "Unknown")")
print("✅ BLE Service for Meshtastic discovered by \(peripheral.name ?? "Unknown")")
}
}
}
@ -249,7 +257,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
if let e = error {
MeshLogger.log("🚫 BLE Discover Characteristics error for \(peripheral.name ?? "Unknown") \(e) disconnecting device")
print("🚫 BLE Discover Characteristics error for \(peripheral.name ?? "Unknown") \(e) disconnecting device")
// Try and stop crashes when this error occurs
disconnectPeripheral()
return
@ -261,16 +269,16 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
switch characteristic.uuid {
case TORADIO_UUID:
MeshLogger.log("✅ BLE did discover TORADIO characteristic for Meshtastic by \(peripheral.name ?? "Unknown")")
print("✅ BLE did discover TORADIO characteristic for Meshtastic by \(peripheral.name ?? "Unknown")")
TORADIO_characteristic = characteristic
case FROMRADIO_UUID:
MeshLogger.log("✅ BLE did discover FROMRADIO characteristic for Meshtastic by \(peripheral.name ?? "Unknown")")
print("✅ BLE did discover FROMRADIO characteristic for Meshtastic by \(peripheral.name ?? "Unknown")")
FROMRADIO_characteristic = characteristic
peripheral.readValue(for: FROMRADIO_characteristic)
case FROMNUM_UUID:
MeshLogger.log("✅ BLE did discover FROMNUM (Notify) characteristic for Meshtastic by \(peripheral.name ?? "Unknown")")
print("✅ BLE did discover FROMNUM (Notify) characteristic for Meshtastic by \(peripheral.name ?? "Unknown")")
FROMNUM_characteristic = characteristic
peripheral.setNotifyValue(true, for: characteristic)
@ -284,31 +292,28 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
}
func requestDeviceMetadata() {
guard (connectedPeripheral!.peripheral.state == CBPeripheralState.connected) else { return }
MeshLogger.log(" Requesting Device Metadata for \(connectedPeripheral!.peripheral.name ?? "Unknown")")
let nodeName = connectedPeripheral!.peripheral.name ?? NSLocalizedString("unknown", comment: NSLocalizedString("unknown", comment: "Unknown"))
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.devicemetadata %@",
comment: "Requesting Device Metadata for %@"), nodeName)
MeshLogger.log("🛎️ \(logString)")
var adminPacket = AdminMessage()
adminPacket.getDeviceMetadataRequest = true
var meshPacket: MeshPacket = MeshPacket()
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.wantAck = true
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
dataMessage.wantResponse = true
meshPacket.decoded = dataMessage
var toRadio: ToRadio = ToRadio()
toRadio.packet = meshPacket
let binaryData: Data = try! toRadio.serializedData()
connectedPeripheral!.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
// Either Read the config complete value or from num notify value
connectedPeripheral!.peripheral.readValue(for: FROMRADIO_characteristic)
}
@ -337,7 +342,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
if connectedPeripheral!.peripheral.state == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
success = true
MeshLogger.log("🪧 Sent a Trace Route Packet to node: \(destNum).")
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.traceroute.sent %@",
comment: "Sent a Trace Route Request to node: %@"), String(destNum))
MeshLogger.log("🪧 \(logString)")
}
return success
}
@ -346,11 +354,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
guard (connectedPeripheral!.peripheral.state == CBPeripheralState.connected) else { return }
if FROMRADIO_characteristic == nil {
MeshLogger.log("🚨 Unsupported Firmware Version Detected, unable to connect to device.")
MeshLogger.log("🚨 \(NSLocalizedString("firmware.version.unsupported", comment: "Unsupported Firmware Version Detected, unable to connect to device."))")
invalidVersion = true
return
} else {
MeshLogger.log(" Issuing wantConfig to \(connectedPeripheral!.peripheral.name ?? "Unknown")")
let nodeName = connectedPeripheral!.peripheral.name ?? NSLocalizedString("unknown", comment: NSLocalizedString("unknown", comment: "Unknown"))
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.wantconfig %@", comment: "Issuing Want Config to %@"), nodeName)
MeshLogger.log("🛎️ \(logString)")
//BLE Characteristics discovered, issue wantConfig
var toRadio: ToRadio = ToRadio()
configNonce += 1
@ -363,9 +374,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
}
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
if let errorText = error?.localizedDescription {
MeshLogger.log("🚫 didUpdateNotificationStateFor error: \(errorText)")
print("🚫 didUpdateNotificationStateFor error: \(errorText)")
}
}
@ -378,17 +388,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
let errorCode = (e as NSError).code
if errorCode == 5 { // CBATTErrorDomain Code=5 "Authentication is insufficient."
// BLE Pin connection error
lastConnectionError = "🚫 BLE \(e.localizedDescription) Please try connecting again and check the PIN carefully."
MeshLogger.log("🚫 BLE \(e.localizedDescription) Please try connecting again and check the PIN carefully.")
self.centralManager?.cancelPeripheralConnection(peripheral)
}
if errorCode == 15 { // CBATTErrorDomain Code=15 "Encryption is insufficient."
// BLE Pin connection error
lastConnectionError = "🚫 BLE \(e.localizedDescription) Please try connecting again and check the PIN carefully."
MeshLogger.log("🚫 BLE \(e.localizedDescription) Please try connecting again. You may need to forget the device under Settings > General > Bluetooth.")
self.centralManager?.cancelPeripheralConnection(peripheral)
if errorCode == 5 || errorCode == 15 {
// BLE PIN connection errors
// 5 CBATTErrorDomain Code=5 "Authentication is insufficient."
// 15 CBATTErrorDomain Code=15 "Encryption is insufficient."
lastConnectionError = "🚨" + String.localizedStringWithFormat(NSLocalizedString("ble.errorcode.pin %@",
comment: "Please try connecting again and check the PIN carefully."),
e.localizedDescription)
print("🚨 \(e.localizedDescription) Please try connecting again and check the PIN carefully.")
self.disconnectPeripheral(reconnect: false)
}
}
@ -399,11 +407,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
if characteristic.value == nil || characteristic.value!.isEmpty {
return
}
var decodedInfo = FromRadio()
do {
decodedInfo = try FromRadio(serializedData: characteristic.value!)
} catch {
@ -414,10 +420,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
// Handle Any local only packets we get over BLE
case .unknownApp:
var nowKnown = false
// MyInfo
// MyInfo from initial connection
if decodedInfo.myInfo.isInitialized && decodedInfo.myInfo.myNodeNum > 0 {
let lastDotIndex = decodedInfo.myInfo.firmwareVersion.lastIndex(of: ".")
@ -434,7 +439,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
let supportedVersion = connectedVersion == "0.0.0" || self.minimumVersion.compare(connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(connectedVersion, options: .numeric) == .orderedSame
if !supportedVersion {
invalidVersion = true
lastConnectionError = "🚨 Update your firmware"
lastConnectionError = "🚨" + NSLocalizedString("update.firmware", comment: "Update Your Firmware")
return
} else {
@ -445,9 +450,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
if myInfo != nil {
connectedPeripheral.num = myInfo!.myNodeNum
connectedPeripheral.firmwareVersion = myInfo?.firmwareVersion ?? "Unknown"
connectedPeripheral.name = myInfo?.bleName ?? "Unknown"
connectedPeripheral.longName = myInfo?.bleName ?? "Unknown"
connectedPeripheral.firmwareVersion = myInfo?.firmwareVersion ?? NSLocalizedString("unknown", comment: "Unknown")
connectedPeripheral.name = myInfo?.bleName ?? NSLocalizedString("unknown", comment: "Unknown")
connectedPeripheral.longName = myInfo?.bleName ?? NSLocalizedString("unknown", comment: "Unknown")
}
}
}
@ -461,7 +466,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
if self.connectedPeripheral != nil && self.connectedPeripheral.num == nodeInfo!.num {
if nodeInfo!.user != nil {
connectedPeripheral.shortName = nodeInfo?.user?.shortName ?? "????"
connectedPeripheral.longName = nodeInfo?.user?.longName ?? "Unknown"
connectedPeripheral.longName = nodeInfo?.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown")
}
}
}
@ -492,16 +497,16 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
}
}
// Log any other unknownApp calls
if !nowKnown { MeshLogger.log(" MESH PACKET received for Unknown App UNHANDLED \(try! decodedInfo.packet.jsonString())") }
if !nowKnown { MeshLogger.log("🕸 MESH PACKET received for Unknown App UNHANDLED \(try! decodedInfo.packet.jsonString())") }
case .textMessageApp:
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())")
MeshLogger.log("🕸 MESH PACKET received for Remote Hardware App UNHANDLED \(try! decodedInfo.packet.jsonString())")
case .positionApp:
positionPacket(packet: decodedInfo.packet, context: context!)
case .waypointApp:
MeshLogger.log(" MESH PACKET received for Waypoint App UNHANDLED \(try! decodedInfo.packet.jsonString())")
MeshLogger.log("🕸 MESH PACKET received for Waypoint App UNHANDLED \(try! decodedInfo.packet.jsonString())")
case .nodeinfoApp:
if !invalidVersion { nodeInfoAppPacket(packet: decodedInfo.packet, context: context!) }
case .routingApp:
@ -509,46 +514,50 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
case .adminApp:
adminAppPacket(packet: decodedInfo.packet, context: context!)
case .replyApp:
MeshLogger.log(" MESH PACKET received for Reply App UNHANDLED \(try! decodedInfo.packet.jsonString())")
MeshLogger.log("🕸 MESH PACKET received for Reply App UNHANDLED \(try! decodedInfo.packet.jsonString())")
case .ipTunnelApp:
MeshLogger.log(" MESH PACKET received for IP Tunnel App UNHANDLED \(try! decodedInfo.packet.jsonString())")
MeshLogger.log("🕸 MESH PACKET received for IP Tunnel App UNHANDLED \(try! decodedInfo.packet.jsonString())")
case .serialApp:
MeshLogger.log(" MESH PACKET received for Serial App UNHANDLED \(try! decodedInfo.packet.jsonString())")
MeshLogger.log("🕸 MESH PACKET received for Serial App UNHANDLED \(try! decodedInfo.packet.jsonString())")
case .storeForwardApp:
MeshLogger.log(" MESH PACKET received for Store Forward App UNHANDLED \(try! decodedInfo.packet.jsonString())")
MeshLogger.log("🕸 MESH PACKET received for Store Forward App UNHANDLED \(try! decodedInfo.packet.jsonString())")
case .rangeTestApp:
MeshLogger.log(" MESH PACKET received for Range Test App UNHANDLED \(try! decodedInfo.packet.jsonString())")
MeshLogger.log("🕸 MESH PACKET received for Range Test App UNHANDLED \(try! decodedInfo.packet.jsonString())")
case .telemetryApp:
if !invalidVersion { telemetryPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) }
case .textMessageCompressedApp:
MeshLogger.log(" MESH PACKET received for Text Message Compressed App UNHANDLED \(try! decodedInfo.packet.jsonString())")
MeshLogger.log("🕸 MESH PACKET received for Text Message Compressed App UNHANDLED \(try! decodedInfo.packet.jsonString())")
case .zpsApp:
MeshLogger.log(" MESH PACKET received for ZPS App UNHANDLED \(try! decodedInfo.packet.jsonString())")
MeshLogger.log("🕸 MESH PACKET received for ZPS App UNHANDLED \(try! decodedInfo.packet.jsonString())")
case .privateApp:
MeshLogger.log(" MESH PACKET received for Private App UNHANDLED \(try! decodedInfo.packet.jsonString())")
MeshLogger.log("🕸 MESH PACKET received for Private App UNHANDLED \(try! decodedInfo.packet.jsonString())")
case .atakForwarder:
MeshLogger.log(" MESH PACKET received for ATAK Forwarder App UNHANDLED \(try! decodedInfo.packet.jsonString())")
MeshLogger.log("🕸 MESH PACKET received for ATAK Forwarder App UNHANDLED \(try! decodedInfo.packet.jsonString())")
case .simulatorApp:
MeshLogger.log(" MESH PACKET received for Simulator App UNHANDLED \(try! decodedInfo.packet.jsonString())")
MeshLogger.log("🕸 MESH PACKET received for Simulator App UNHANDLED \(try! decodedInfo.packet.jsonString())")
case .audioApp:
MeshLogger.log(" MESH PACKET received for Audio App UNHANDLED \(try! decodedInfo.packet.jsonString())")
MeshLogger.log("🕸 MESH PACKET received for Audio App UNHANDLED \(try! decodedInfo.packet.jsonString())")
case .tracerouteApp:
if let routingMessage = try? RouteDiscovery(serializedData: decodedInfo.packet.decoded.payload) {
if routingMessage.route.count == 0 {
MeshLogger.log("🪧 Trace Route request sent to \(decodedInfo.packet.from) was recieived directly.")
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.traceroute.received.direct %@",
comment: "Trace Route request sent to node: %@ was recieived directly."), String(decodedInfo.packet.from))
MeshLogger.log("🪧 \(logString)")
} else {
var routeString = "🪧 Trace Route request returned: \(decodedInfo.packet.to) --> "
var routeString = "\(decodedInfo.packet.to) --> "
for node in routingMessage.route {
routeString += "\(node) --> "
}
routeString += "\(decodedInfo.packet.from)"
MeshLogger.log(routeString)
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.traceroute.received.route %@",
comment: "Trace Route request returned: %@"), routeString)
MeshLogger.log("🪧 \(logString)")
}
}
case .UNRECOGNIZED(_):
MeshLogger.log(" MESH PACKET received for Other App UNHANDLED \(try! decodedInfo.packet.jsonString())")
MeshLogger.log("🕸 MESH PACKET received for Other App UNHANDLED \(try! decodedInfo.packet.jsonString())")
case .max:
print("MAX PORT NUM OF 511")
}
@ -560,13 +569,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
do {
let fetchedUser = try context?.fetch(fetchBCUserRequest) as! [UserEntity]
if fetchedUser.count > 0 {
context?.delete(fetchedUser[0])
print("🗑️ Deleted the All - Broadcast User")
}
} catch {
MeshLogger.log("💥 Error Deleting the All - Broadcast User")
print("💥 Error Deleting the All - Broadcast User")
}
if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == configNonce {
@ -574,7 +581,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
lastConnectionError = ""
timeoutTimerRuns = 0
isSubscribed = true
MeshLogger.log("🤜 BLE Config Complete Packet Id: \(decodedInfo.configCompleteID)")
print("🤜 Want Config Complete. ID:\(decodedInfo.configCompleteID)")
peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected })
// Config conplete returns so we don't read the characteristic again
// MARK: Share Location Position Update Timer
@ -620,7 +627,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
if preferredPeripheral != nil && preferredPeripheral?.peripheral != nil {
connectTo(peripheral: preferredPeripheral!.peripheral)
}
MeshLogger.log("🚫 Message Send Failed, not properly connected to \(preferredPeripheral?.name ?? "Unknown")")
let nodeName = connectedPeripheral!.peripheral.name ?? NSLocalizedString("unknown", comment: NSLocalizedString("unknown", comment: "Unknown"))
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.textmessage.send.failed %@",
comment: "Message Send Failed, not properly connected to %@"), nodeName)
MeshLogger.log("🚫 \(logString)")
success = false
} else if message.count < 1 {
@ -690,24 +701,21 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
var toRadio: ToRadio!
toRadio = ToRadio()
toRadio.packet = meshPacket
let binaryData: Data = try! toRadio.serializedData()
MeshLogger.log("📲 New messageId \(newMessage.messageId) sent to \(newMessage.toUser?.longName! ?? "Unknown")")
if connectedPeripheral!.peripheral.state == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.textmessage.sent %@ %@ %@", comment: "Sent message %@ from %@ to %@"), String(newMessage.messageId), String(fromUserNum), String(toUserNum))
MeshLogger.log("💬 \(logString)")
do {
try context!.save()
MeshLogger.log("💾 Saved a new sent message from \(connectedPeripheral.num) to \(toUserNum)")
print("💾 Saved a new sent message from \(connectedPeripheral.num) to \(toUserNum)")
success = true
} catch {
context!.rollback()
let nsError = error as NSError
MeshLogger.log("💥 Unresolved Core Data error in Send Message Function your database is corrupted running a node db reset should clean up the data. Error: \(nsError)")
print("💥 Unresolved Core Data error in Send Message Function your database is corrupted running a node db reset should clean up the data. Error: \(nsError)")
}
}
}
@ -744,8 +752,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
toRadio = ToRadio()
toRadio.packet = meshPacket
let binaryData: Data = try! toRadio.serializedData()
MeshLogger.log("📍 Sent a Waypoint Packet from the Apple device GPS to node: \(fromNodeNum)")
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.waypoint.sent %@", comment: "Sent a Waypoint Packet from: %@"), String(fromNodeNum))
MeshLogger.log("📍 \(logString)")
if connectedPeripheral!.peripheral.state == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
@ -794,7 +802,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
if connectedPeripheral!.peripheral.state == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
success = true
MeshLogger.log("📍 Sent a Position Packet from the Apple device GPS to node: \(fromNodeNum)")
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.sharelocation %@", comment: "Sent a Position Packet from the Apple device GPS to node: %@"), String(fromNodeNum))
MeshLogger.log("📍 \(logString)")
}
return success
}
@ -817,54 +826,37 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
}
}
public func sendShutdown(destNum: Int64) -> Bool {
public func sendShutdown(fromUser: UserEntity, toUser: UserEntity) -> Bool {
var adminPacket = AdminMessage()
adminPacket.shutdownSeconds = 10
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(connectedPeripheral.num)
meshPacket.from = 0 //UInt32(connectedPeripheral.num)
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.wantAck = true
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
var toRadio: ToRadio!
toRadio = ToRadio()
toRadio.packet = meshPacket
let binaryData: Data = try! toRadio.serializedData()
if connectedPeripheral!.peripheral.state == CBPeripheralState.connected {
do {
try context!.save()
MeshLogger.log("💾 Saved a Shutdown Admin Message for node: \(String(destNum))")
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
return true
} catch {
context!.rollback()
let nsError = error as NSError
MeshLogger.log("💥 Error Inserting New Core Data MessageEntity: \(nsError)")
}
let messageDescription = "Sent Shutdown Admin Message to: \(toUser.longName ?? NSLocalizedString("unknown", comment: ""))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
return true
}
return false
}
public func sendReboot(destNum: Int64) -> Bool {
public func sendReboot(fromUser: UserEntity, toUser: UserEntity) -> Bool {
var adminPacket = AdminMessage()
adminPacket.rebootSeconds = 10
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(connectedPeripheral.num)
meshPacket.from = 0 //UInt32(connectedPeripheral.num)
meshPacket.from = UInt32(fromUser.num)
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.wantAck = true
@ -874,69 +866,46 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
var toRadio: ToRadio!
toRadio = ToRadio()
toRadio.packet = meshPacket
let binaryData: Data = try! toRadio.serializedData()
if connectedPeripheral!.peripheral.state == CBPeripheralState.connected {
do {
try context!.save()
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
MeshLogger.log("💾 Saved a Reboot Admin Message for node: \(String(destNum))")
return true
} catch {
context!.rollback()
let nsError = error as NSError
MeshLogger.log("💥 Error Inserting New Core Data MessageEntity: \(nsError)")
}
}
return false
}
public func sendFactoryReset(destNum: Int64) -> Bool {
var adminPacket = AdminMessage()
adminPacket.factoryReset = 1
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(destNum)
meshPacket.from = 0 //UInt32(connectedPeripheral.num)
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.wantAck = true
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
var toRadio: ToRadio!
toRadio = ToRadio()
toRadio.packet = meshPacket
let binaryData: Data = try! toRadio.serializedData()
if connectedPeripheral!.peripheral.state == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
MeshLogger.log("💾 Sent a Factory Reset for node: \(String(destNum))")
let messageDescription = "Sent Reboot Admin Message to: \(toUser.longName ?? NSLocalizedString("unknown", comment: ""))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
return true
}
return false
}
public func sendNodeDBReset(destNum: Int64) -> Bool {
public func sendFactoryReset(fromUser: UserEntity, toUser: UserEntity) -> Bool {
var adminPacket = AdminMessage()
adminPacket.factoryReset = 1
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.wantAck = true
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "Sent Factory Reset Admin Message to: \(toUser.longName ?? NSLocalizedString("unknown", comment: ""))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
return true
}
return false
}
public func sendNodeDBReset(fromUser: UserEntity, toUser: UserEntity) -> Bool {
var adminPacket = AdminMessage()
adminPacket.nodedbReset = 1
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(destNum)
meshPacket.from = 0 //UInt32(connectedPeripheral.num)
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.wantAck = true
@ -946,16 +915,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
var toRadio: ToRadio!
toRadio = ToRadio()
toRadio.packet = meshPacket
let binaryData: Data = try! toRadio.serializedData()
if connectedPeripheral!.peripheral.state == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
MeshLogger.log("💾 Sent a NodeDB Reset for node: \(String(destNum))")
let messageDescription = "Sent NodeDB Reset Admin Message to: \(toUser.longName ?? NSLocalizedString("unknown", comment: ""))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
return true
}
return false
@ -982,6 +943,32 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
return success
}
public func getChannel(channel: Channel, fromUser: UserEntity, toUser: UserEntity) -> Int64 {
var adminPacket = AdminMessage()
adminPacket.getChannelRequest = UInt32(channel.index + 1)
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(toUser.num)
meshPacket.from = 0 //UInt32(fromUser.num)
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
dataMessage.wantResponse = true
meshPacket.decoded = dataMessage
let messageDescription = "Requested Channel \(channel.index) for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
return Int64(meshPacket.id)
}
return 0
}
public func saveChannel(channel: Channel, fromUser: UserEntity, toUser: UserEntity) -> Int64 {
var adminPacket = AdminMessage()
@ -991,18 +978,16 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
meshPacket.from = 0 //UInt32(fromUser.num)
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.wantAck = true
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
dataMessage.wantResponse = true
meshPacket.decoded = dataMessage
let messageDescription = "Saved Channel \(channel.index) for \(toUser.longName ?? "Unknown")"
let messageDescription = "Saved Channel \(channel.index) for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
return Int64(meshPacket.id)
}
@ -1053,7 +1038,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
adminPacket.setChannel = chan
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(connectedPeripheral.num)
meshPacket.from = 0 //UInt32(connectedPeripheral.num)
meshPacket.from = UInt32(connectedPeripheral.num)
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.wantAck = true
@ -1068,7 +1053,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
let binaryData: Data = try! toRadio.serializedData()
if connectedPeripheral!.peripheral.state == CBPeripheralState.connected {
self.connectedPeripheral.peripheral.writeValue(binaryData, for: self.TORADIO_characteristic, type: .withResponse)
MeshLogger.log("✈️ Sent a Channel for: \(String(self.connectedPeripheral.num)) Channel Index \(chan.index)")
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.channel.sent %@ %d", comment: "Sent a Channel for: %@ Channel Index %d"), String(connectedPeripheral.num), chan.index)
MeshLogger.log("🎛️ \(logString)")
}
}
// Save the LoRa Config and the device will reboot
@ -1091,7 +1077,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
let binaryData: Data = try! toRadio.serializedData()
if connectedPeripheral!.peripheral.state == CBPeripheralState.connected {
self.connectedPeripheral.peripheral.writeValue(binaryData, for: self.TORADIO_characteristic, type: .withResponse)
MeshLogger.log("✈️ Sent a LoRaConfig for: \(String(self.connectedPeripheral.num))")
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.lora.config.sent %@", comment: "Sent a LoRaConfig for: %@"), String(connectedPeripheral.num))
MeshLogger.log("📻 \(logString)")
}
return true
@ -1118,7 +1105,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "Saved User Config for \(toUser.longName ?? "Unknown")"
let messageDescription = "Saved User Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
return Int64(meshPacket.id)
@ -1141,7 +1128,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "Saved Bluetooth Config for \(toUser.longName ?? "Unknown")"
let messageDescription = "Saved Bluetooth Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
return Int64(meshPacket.id)
@ -1166,7 +1153,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "Saved Device Config for \(toUser.longName ?? "Unknown")"
let messageDescription = "Saved Device Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
@ -1191,7 +1178,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "Saved Display Config for \(toUser.longName ?? "Unknown")"
let messageDescription = "Saved Display Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
return Int64(meshPacket.id)
@ -1215,7 +1202,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "Saved LoRa Config for \(toUser.longName ?? "Unknown")"
let messageDescription = "Saved LoRa Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
return Int64(meshPacket.id)
@ -1243,7 +1230,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
meshPacket.decoded = dataMessage
let messageDescription = "Saved Position Config for \(toUser.longName ?? "Unknown")"
let messageDescription = "Saved Position Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
@ -1272,7 +1259,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
meshPacket.decoded = dataMessage
let messageDescription = "Saved WiFi Config for \(toUser.longName ?? "Unknown")"
let messageDescription = "Saved WiFi Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
@ -1300,7 +1287,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
meshPacket.decoded = dataMessage
let messageDescription = "Saved Canned Message Module Config for \(toUser.longName ?? "Unknown")"
let messageDescription = "Saved Canned Message Module Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
@ -1329,7 +1316,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
meshPacket.decoded = dataMessage
let messageDescription = "💾 Saved Canned Message Module Messages for \(toUser.longName ?? "Unknown")"
let messageDescription = "💾 Saved Canned Message Module Messages for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
@ -1395,8 +1382,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
let binaryData: Data = try! toRadio.serializedData()
if connectedPeripheral!.peripheral.state == CBPeripheralState.connected {
MeshLogger.log("✈️ Sent a Canned Messages Module Get Messages Request Admin Message for node: \(String(destNum))")
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.cannedmessages.messages.get %@", comment: "Requested Canned Messages Module Messages for node: %@"), String(connectedPeripheral.num))
MeshLogger.log("🥫 \(logString)")
return true
}
@ -1418,16 +1406,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "Saved External Notification Module Config for \(toUser.longName ?? "Unknown")"
let messageDescription = "Saved External Notification Module Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
return Int64(meshPacket.id)
}
return 0
}
@ -1450,7 +1434,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
meshPacket.decoded = dataMessage
let messageDescription = "Saved WiFi Config for \(toUser.longName ?? "Unknown")"
let messageDescription = "Saved WiFi Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
@ -1478,7 +1462,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
meshPacket.decoded = dataMessage
let messageDescription = "Saved Range Test Module Config for \(toUser.longName ?? "Unknown")"
let messageDescription = "Saved Range Test Module Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
@ -1504,16 +1488,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "Saved Serial Module Config for \(toUser.longName ?? "Unknown")"
let messageDescription = "Saved Serial Module Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
return Int64(meshPacket.id)
}
return 0
}
@ -1532,16 +1512,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "Saved Telemetry Module Config for \(toUser.longName ?? "Unknown")"
let messageDescription = "Saved Telemetry Module Config for \(toUser.longName ?? NSLocalizedString("unknown", comment: "Unknown"))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
return Int64(meshPacket.id)
}
return 0
}
@ -1551,11 +1527,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
var toRadio: ToRadio!
toRadio = ToRadio()
toRadio.packet = meshPacket
let binaryData: Data = try! toRadio.serializedData()
if connectedPeripheral!.peripheral.state == CBPeripheralState.connected {
let newMessage = MessageEntity(context: context!)
newMessage.messageId = Int64(meshPacket.id)
newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970)
@ -1566,19 +1540,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
newMessage.toUser = toUser
do {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
try context!.save()
MeshLogger.log("💾 \(adminDescription)")
print("⚙️ \(adminDescription)")
return true
} catch {
context!.rollback()
let nsError = error as NSError
MeshLogger.log("💥 Error inserting new core data MessageEntity: \(nsError)")
print("💥 Error inserting new core data MessageEntity: \(nsError)")
}
}
return false
@ -1623,9 +1592,9 @@ extension BLEManager: CBCentralManagerDelegate {
// Called each time a peripheral is discovered
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
if timeoutTimerRuns < 2 && peripheral.identifier.uuidString == UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" {
if self.automaticallyReconnect && timeoutTimerRuns < 2 && peripheral.identifier.uuidString == UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" {
self.connectTo(peripheral: peripheral)
MeshLogger.log(" BLE Reconnecting to prefered peripheral: \(peripheral.name ?? "Unknown")")
print(" BLE Reconnecting to prefered peripheral: \(peripheral.name ?? "Unknown")")
}
let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String
let device = Peripheral(id: peripheral.identifier.uuidString, num: 0, name: name ?? "Unknown", shortName: "????", longName: name ?? "Unknown", firmwareVersion: "Unknown", rssi: RSSI.intValue, lastUpdate: Date(), peripheral: peripheral)

View file

@ -12,9 +12,11 @@ class MeshLogger {
guard let logFile = logFile else {
return
}
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmmssSSa", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss.SS a")
let formatter = DateFormatter()
formatter.dateFormat = "M/d/yy h:mm:ss.SSSS"
formatter.dateFormat = dateFormatString
let timestamp = formatter.string(from: Date())
guard let data = (message + " - " + timestamp + "\n").data(using: String.Encoding.utf8) else { return }
print(message)

File diff suppressed because it is too large Load diff

View file

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

View file

@ -0,0 +1,279 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22A400" 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="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="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="DisplayConfigEntity" representedClassName="DisplayConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="compassNorthTop" optional="YES" attributeType="Boolean" defaultValueString="NO" 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="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"/>
<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="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="regionCode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="spreadFactor" optional="YES" attributeType="Integer 32" defaultValueString="0" 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="realACK" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="receivedACK" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="replyID" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="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="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="bitrate" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="bleName" optional="YES" attributeType="String"/>
<attribute name="errorCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="firmwareVersion" attributeType="String"/>
<attribute name="hasGps" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hasWifi" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="maxChannels" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="messageTimeoutMsec" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<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" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="num" attributeType="Integer 64" 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="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="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="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="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="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitudeI" 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="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="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="macaddr" optional="YES" attributeType="Binary"/>
<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="(toUser.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="expire" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="locked" attributeType="Boolean" 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" optional="YES" attributeType="String" maxValueString="30"/>
</entity>
</model>

View file

@ -31,7 +31,7 @@ struct MeshtasticAppleApp: App {
print("URL received \(userActivity)")
self.incomingUrl = userActivity.webpageURL
if self.incomingUrl!.absoluteString.lowercased().contains("meshtastic.org/e/#") {
if ((self.incomingUrl?.absoluteString.lowercased().contains("meshtastic.org/e/#")) != nil) {
if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") {
self.channelSettings = components.last!

View file

@ -23,7 +23,7 @@ extension PositionEntity {
return d / 1e7
}
var coordinate: CLLocationCoordinate2D? {
var nodeCoordinate: CLLocationCoordinate2D? {
if latitudeI != 0 && longitudeI != 0 {
let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!)
return coord
@ -31,12 +31,17 @@ extension PositionEntity {
return nil
}
}
var annotaton: MKPointAnnotation {
let pointAnn = MKPointAnnotation()
if coordinate != nil {
pointAnn.coordinate = coordinate!
if nodeCoordinate != nil {
pointAnn.coordinate = nodeCoordinate!
}
return pointAnn
}
}
extension PositionEntity: MKAnnotation {
public var coordinate: CLLocationCoordinate2D { nodeCoordinate! }
public var subtitle: String? { time?.formatted() }
}

View file

@ -61,11 +61,8 @@ public func clearTelemetry(destNum: Int64, metricsType: Int32, context: NSManage
}
public func deleteChannelMessages(channel: ChannelEntity, context: NSManagedObjectContext) {
let fetchChannelMessagesRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "MessageEntity")
fetchChannelMessagesRequest.predicate = NSPredicate(format: "channel == %i AND toUser == nil AND admin == false", Int32(channel.id))
fetchChannelMessagesRequest.includesPropertyValues = false
do {
let objects = try context.fetch(fetchChannelMessagesRequest) as! [NSManagedObject]
let objects = channel.allPrivateMessages// try context.fetch(fetchChannelMessagesRequest) as! [NSManagedObject]
for object in objects {
context.delete(object)
}
@ -77,11 +74,8 @@ public func deleteChannelMessages(channel: ChannelEntity, context: NSManagedObje
public func deleteUserMessages(user: UserEntity, context: NSManagedObjectContext) {
let fetchUserMessagesRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "MessageEntity")
fetchUserMessagesRequest.predicate = NSPredicate(format: "((toUser.num == %lld) OR (fromUser.num == %lld)) AND toUser != nil AND fromUser != nil AND admin == false", Int64(user.num), Int64(user.num))
fetchUserMessagesRequest.includesPropertyValues = false
do {
let objects = try context.fetch(fetchUserMessagesRequest) as! [NSManagedObject]
let objects = user.messageList//try context.fetch(fetchUserMessagesRequest) as! [NSManagedObject]
for object in objects {
context.delete(object)
}

View file

@ -154,6 +154,26 @@ struct AdminMessage {
set {payloadVariant = .getDeviceMetadataResponse(newValue)}
}
///
/// Get the Ringtone in the response to this message.
var getRingtoneRequest: Bool {
get {
if case .getRingtoneRequest(let v)? = payloadVariant {return v}
return false
}
set {payloadVariant = .getRingtoneRequest(newValue)}
}
///
/// Get the Ringtone in the response to this message.
var getRingtoneResponse: String {
get {
if case .getRingtoneResponse(let v)? = payloadVariant {return v}
return String()
}
set {payloadVariant = .getRingtoneResponse(newValue)}
}
///
/// Set the owner for this node
var setOwner: User {
@ -208,6 +228,16 @@ struct AdminMessage {
set {payloadVariant = .setCannedMessageModuleMessages(newValue)}
}
///
/// Set the ringtone for ExternalNotification.
var setRingtoneMessage: String {
get {
if case .setRingtoneMessage(let v)? = payloadVariant {return v}
return String()
}
set {payloadVariant = .setRingtoneMessage(newValue)}
}
///
/// Begins an edit transaction for config, module config, owner, and channel settings changes
/// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings)
@ -357,6 +387,12 @@ struct AdminMessage {
/// Device metadata response
case getDeviceMetadataResponse(DeviceMetadata)
///
/// Get the Ringtone in the response to this message.
case getRingtoneRequest(Bool)
///
/// Get the Ringtone in the response to this message.
case getRingtoneResponse(String)
///
/// Set the owner for this node
case setOwner(User)
///
@ -376,6 +412,9 @@ struct AdminMessage {
/// Set the Canned Message Module messages text.
case setCannedMessageModuleMessages(String)
///
/// Set the ringtone for ExternalNotification.
case setRingtoneMessage(String)
///
/// Begins an edit transaction for config, module config, owner, and channel settings changes
/// This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings)
case beginEditSettings(Bool)
@ -466,6 +505,14 @@ struct AdminMessage {
guard case .getDeviceMetadataResponse(let l) = lhs, case .getDeviceMetadataResponse(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getRingtoneRequest, .getRingtoneRequest): return {
guard case .getRingtoneRequest(let l) = lhs, case .getRingtoneRequest(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.getRingtoneResponse, .getRingtoneResponse): return {
guard case .getRingtoneResponse(let l) = lhs, case .getRingtoneResponse(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.setOwner, .setOwner): return {
guard case .setOwner(let l) = lhs, case .setOwner(let r) = rhs else { preconditionFailure() }
return l == r
@ -486,6 +533,10 @@ struct AdminMessage {
guard case .setCannedMessageModuleMessages(let l) = lhs, case .setCannedMessageModuleMessages(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.setRingtoneMessage, .setRingtoneMessage): return {
guard case .setRingtoneMessage(let l) = lhs, case .setRingtoneMessage(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.beginEditSettings, .beginEditSettings): return {
guard case .beginEditSettings(let l) = lhs, case .beginEditSettings(let r) = rhs else { preconditionFailure() }
return l == r
@ -634,6 +685,10 @@ struct AdminMessage {
///
/// TODO: REPLACE
case audioConfig // = 7
///
/// TODO: REPLACE
case remotehardwareConfig // = 8
case UNRECOGNIZED(Int)
init() {
@ -650,6 +705,7 @@ struct AdminMessage {
case 5: self = .telemetryConfig
case 6: self = .cannedmsgConfig
case 7: self = .audioConfig
case 8: self = .remotehardwareConfig
default: self = .UNRECOGNIZED(rawValue)
}
}
@ -664,6 +720,7 @@ struct AdminMessage {
case .telemetryConfig: return 5
case .cannedmsgConfig: return 6
case .audioConfig: return 7
case .remotehardwareConfig: return 8
case .UNRECOGNIZED(let i): return i
}
}
@ -699,6 +756,7 @@ extension AdminMessage.ModuleConfigType: CaseIterable {
.telemetryConfig,
.cannedmsgConfig,
.audioConfig,
.remotehardwareConfig,
]
}
@ -728,11 +786,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
11: .standard(proto: "get_canned_message_module_messages_response"),
12: .standard(proto: "get_device_metadata_request"),
13: .standard(proto: "get_device_metadata_response"),
14: .standard(proto: "get_ringtone_request"),
15: .standard(proto: "get_ringtone_response"),
32: .standard(proto: "set_owner"),
33: .standard(proto: "set_channel"),
34: .standard(proto: "set_config"),
35: .standard(proto: "set_module_config"),
36: .standard(proto: "set_canned_message_module_messages"),
37: .standard(proto: "set_ringtone_message"),
64: .standard(proto: "begin_edit_settings"),
65: .standard(proto: "commit_edit_settings"),
66: .standard(proto: "confirm_set_channel"),
@ -872,6 +933,22 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
self.payloadVariant = .getDeviceMetadataResponse(v)
}
}()
case 14: try {
var v: Bool?
try decoder.decodeSingularBoolField(value: &v)
if let v = v {
if self.payloadVariant != nil {try decoder.handleConflictingOneOf()}
self.payloadVariant = .getRingtoneRequest(v)
}
}()
case 15: try {
var v: String?
try decoder.decodeSingularStringField(value: &v)
if let v = v {
if self.payloadVariant != nil {try decoder.handleConflictingOneOf()}
self.payloadVariant = .getRingtoneResponse(v)
}
}()
case 32: try {
var v: User?
var hadOneofValue = false
@ -932,6 +1009,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
self.payloadVariant = .setCannedMessageModuleMessages(v)
}
}()
case 37: try {
var v: String?
try decoder.decodeSingularStringField(value: &v)
if let v = v {
if self.payloadVariant != nil {try decoder.handleConflictingOneOf()}
self.payloadVariant = .setRingtoneMessage(v)
}
}()
case 64: try {
var v: Bool?
try decoder.decodeSingularBoolField(value: &v)
@ -1071,6 +1156,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
guard case .getDeviceMetadataResponse(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 13)
}()
case .getRingtoneRequest?: try {
guard case .getRingtoneRequest(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularBoolField(value: v, fieldNumber: 14)
}()
case .getRingtoneResponse?: try {
guard case .getRingtoneResponse(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularStringField(value: v, fieldNumber: 15)
}()
case .setOwner?: try {
guard case .setOwner(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 32)
@ -1091,6 +1184,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
guard case .setCannedMessageModuleMessages(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularStringField(value: v, fieldNumber: 36)
}()
case .setRingtoneMessage?: try {
guard case .setRingtoneMessage(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularStringField(value: v, fieldNumber: 37)
}()
case .beginEditSettings?: try {
guard case .beginEditSettings(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularBoolField(value: v, fieldNumber: 64)
@ -1165,5 +1262,6 @@ extension AdminMessage.ModuleConfigType: SwiftProtobuf._ProtoNameProviding {
5: .same(proto: "TELEMETRY_CONFIG"),
6: .same(proto: "CANNEDMSG_CONFIG"),
7: .same(proto: "AUDIO_CONFIG"),
8: .same(proto: "REMOTEHARDWARE_CONFIG"),
]
}

View file

@ -474,7 +474,7 @@ struct Config {
///
/// acquire an address via DHCP or assign static
var ethMode: Config.NetworkConfig.EthMode = .dhcp
var addressMode: Config.NetworkConfig.AddressMode = .dhcp
///
/// struct to keep static address
@ -489,7 +489,7 @@ struct Config {
var unknownFields = SwiftProtobuf.UnknownStorage()
enum EthMode: SwiftProtobuf.Enum {
enum AddressMode: SwiftProtobuf.Enum {
typealias RawValue = Int
///
@ -592,6 +592,14 @@ struct Config {
/// Override auto-detect in screen
var oled: Config.DisplayConfig.OledType = .oledAuto
///
/// Display Mode
var displaymode: Config.DisplayConfig.DisplayMode = .default
///
/// Print first line in pseudo-bold? FALSE is original style, TRUE is bold
var headingBold: Bool = false
var unknownFields = SwiftProtobuf.UnknownStorage()
///
@ -739,6 +747,52 @@ struct Config {
}
enum DisplayMode: SwiftProtobuf.Enum {
typealias RawValue = Int
///
/// Default. The old style for the 128x64 OLED screen
case `default` // = 0
///
/// Rearrange display elements to cater for bicolor OLED displays
case twocolor // = 1
///
/// Same as TwoColor, but with inverted top bar. Not so good for Epaper displays
case inverted // = 2
///
/// TFT Full Color Displays (not implemented yet)
case color // = 3
case UNRECOGNIZED(Int)
init() {
self = .default
}
init?(rawValue: Int) {
switch rawValue {
case 0: self = .default
case 1: self = .twocolor
case 2: self = .inverted
case 3: self = .color
default: self = .UNRECOGNIZED(rawValue)
}
}
var rawValue: Int {
switch self {
case .default: return 0
case .twocolor: return 1
case .inverted: return 2
case .color: return 3
case .UNRECOGNIZED(let i): return i
}
}
}
init() {}
}
@ -1099,9 +1153,9 @@ extension Config.PositionConfig.PositionFlags: CaseIterable {
]
}
extension Config.NetworkConfig.EthMode: CaseIterable {
extension Config.NetworkConfig.AddressMode: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Config.NetworkConfig.EthMode] = [
static var allCases: [Config.NetworkConfig.AddressMode] = [
.dhcp,
.static,
]
@ -1136,6 +1190,16 @@ extension Config.DisplayConfig.OledType: CaseIterable {
]
}
extension Config.DisplayConfig.DisplayMode: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Config.DisplayConfig.DisplayMode] = [
.default,
.twocolor,
.inverted,
.color,
]
}
extension Config.LoRaConfig.RegionCode: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Config.LoRaConfig.RegionCode] = [
@ -1189,12 +1253,13 @@ extension Config.PositionConfig: @unchecked Sendable {}
extension Config.PositionConfig.PositionFlags: @unchecked Sendable {}
extension Config.PowerConfig: @unchecked Sendable {}
extension Config.NetworkConfig: @unchecked Sendable {}
extension Config.NetworkConfig.EthMode: @unchecked Sendable {}
extension Config.NetworkConfig.AddressMode: @unchecked Sendable {}
extension Config.NetworkConfig.IpV4Config: @unchecked Sendable {}
extension Config.DisplayConfig: @unchecked Sendable {}
extension Config.DisplayConfig.GpsCoordinateFormat: @unchecked Sendable {}
extension Config.DisplayConfig.DisplayUnits: @unchecked Sendable {}
extension Config.DisplayConfig.OledType: @unchecked Sendable {}
extension Config.DisplayConfig.DisplayMode: @unchecked Sendable {}
extension Config.LoRaConfig: @unchecked Sendable {}
extension Config.LoRaConfig.RegionCode: @unchecked Sendable {}
extension Config.LoRaConfig.ModemPreset: @unchecked Sendable {}
@ -1607,7 +1672,7 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
4: .standard(proto: "wifi_psk"),
5: .standard(proto: "ntp_server"),
6: .standard(proto: "eth_enabled"),
7: .standard(proto: "eth_mode"),
7: .standard(proto: "address_mode"),
8: .standard(proto: "ipv4_config"),
]
@ -1622,7 +1687,7 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
case 4: try { try decoder.decodeSingularStringField(value: &self.wifiPsk) }()
case 5: try { try decoder.decodeSingularStringField(value: &self.ntpServer) }()
case 6: try { try decoder.decodeSingularBoolField(value: &self.ethEnabled) }()
case 7: try { try decoder.decodeSingularEnumField(value: &self.ethMode) }()
case 7: try { try decoder.decodeSingularEnumField(value: &self.addressMode) }()
case 8: try { try decoder.decodeSingularMessageField(value: &self._ipv4Config) }()
default: break
}
@ -1649,8 +1714,8 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
if self.ethEnabled != false {
try visitor.visitSingularBoolField(value: self.ethEnabled, fieldNumber: 6)
}
if self.ethMode != .dhcp {
try visitor.visitSingularEnumField(value: self.ethMode, fieldNumber: 7)
if self.addressMode != .dhcp {
try visitor.visitSingularEnumField(value: self.addressMode, fieldNumber: 7)
}
try { if let v = self._ipv4Config {
try visitor.visitSingularMessageField(value: v, fieldNumber: 8)
@ -1664,14 +1729,14 @@ extension Config.NetworkConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
if lhs.wifiPsk != rhs.wifiPsk {return false}
if lhs.ntpServer != rhs.ntpServer {return false}
if lhs.ethEnabled != rhs.ethEnabled {return false}
if lhs.ethMode != rhs.ethMode {return false}
if lhs.addressMode != rhs.addressMode {return false}
if lhs._ipv4Config != rhs._ipv4Config {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension Config.NetworkConfig.EthMode: SwiftProtobuf._ProtoNameProviding {
extension Config.NetworkConfig.AddressMode: SwiftProtobuf._ProtoNameProviding {
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
0: .same(proto: "DHCP"),
1: .same(proto: "STATIC"),
@ -1738,6 +1803,8 @@ extension Config.DisplayConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
5: .standard(proto: "flip_screen"),
6: .same(proto: "units"),
7: .same(proto: "oled"),
8: .same(proto: "displaymode"),
9: .standard(proto: "heading_bold"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -1753,6 +1820,8 @@ extension Config.DisplayConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
case 5: try { try decoder.decodeSingularBoolField(value: &self.flipScreen) }()
case 6: try { try decoder.decodeSingularEnumField(value: &self.units) }()
case 7: try { try decoder.decodeSingularEnumField(value: &self.oled) }()
case 8: try { try decoder.decodeSingularEnumField(value: &self.displaymode) }()
case 9: try { try decoder.decodeSingularBoolField(value: &self.headingBold) }()
default: break
}
}
@ -1780,6 +1849,12 @@ extension Config.DisplayConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
if self.oled != .oledAuto {
try visitor.visitSingularEnumField(value: self.oled, fieldNumber: 7)
}
if self.displaymode != .default {
try visitor.visitSingularEnumField(value: self.displaymode, fieldNumber: 8)
}
if self.headingBold != false {
try visitor.visitSingularBoolField(value: self.headingBold, fieldNumber: 9)
}
try unknownFields.traverse(visitor: &visitor)
}
@ -1791,6 +1866,8 @@ extension Config.DisplayConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
if lhs.flipScreen != rhs.flipScreen {return false}
if lhs.units != rhs.units {return false}
if lhs.oled != rhs.oled {return false}
if lhs.displaymode != rhs.displaymode {return false}
if lhs.headingBold != rhs.headingBold {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
@ -1822,6 +1899,15 @@ extension Config.DisplayConfig.OledType: SwiftProtobuf._ProtoNameProviding {
]
}
extension Config.DisplayConfig.DisplayMode: SwiftProtobuf._ProtoNameProviding {
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
0: .same(proto: "DEFAULT"),
1: .same(proto: "TWOCOLOR"),
2: .same(proto: "INVERTED"),
3: .same(proto: "COLOR"),
]
}
extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = Config.protoMessageName + ".LoRaConfig"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [

View file

@ -211,6 +211,17 @@ struct LocalModuleConfig {
/// Clears the value of `audio`. Subsequent reads from it will return its default value.
mutating func clearAudio() {_uniqueStorage()._audio = nil}
///
/// The part of the config that is specific to the Remote Hardware module
var remoteHardware: ModuleConfig.RemoteHardwareConfig {
get {return _storage._remoteHardware ?? ModuleConfig.RemoteHardwareConfig()}
set {_uniqueStorage()._remoteHardware = newValue}
}
/// Returns true if `remoteHardware` has been explicitly set.
var hasRemoteHardware: Bool {return _storage._remoteHardware != nil}
/// Clears the value of `remoteHardware`. Subsequent reads from it will return its default value.
mutating func clearRemoteHardware() {_uniqueStorage()._remoteHardware = 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
@ -369,6 +380,7 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
6: .same(proto: "telemetry"),
7: .standard(proto: "canned_message"),
9: .same(proto: "audio"),
10: .standard(proto: "remote_hardware"),
8: .same(proto: "version"),
]
@ -381,6 +393,7 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
var _telemetry: ModuleConfig.TelemetryConfig? = nil
var _cannedMessage: ModuleConfig.CannedMessageConfig? = nil
var _audio: ModuleConfig.AudioConfig? = nil
var _remoteHardware: ModuleConfig.RemoteHardwareConfig? = nil
var _version: UInt32 = 0
static let defaultInstance = _StorageClass()
@ -396,6 +409,7 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
_telemetry = source._telemetry
_cannedMessage = source._cannedMessage
_audio = source._audio
_remoteHardware = source._remoteHardware
_version = source._version
}
}
@ -424,6 +438,7 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
case 7: try { try decoder.decodeSingularMessageField(value: &_storage._cannedMessage) }()
case 8: try { try decoder.decodeSingularUInt32Field(value: &_storage._version) }()
case 9: try { try decoder.decodeSingularMessageField(value: &_storage._audio) }()
case 10: try { try decoder.decodeSingularMessageField(value: &_storage._remoteHardware) }()
default: break
}
}
@ -463,6 +478,9 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
try { if let v = _storage._audio {
try visitor.visitSingularMessageField(value: v, fieldNumber: 9)
} }()
try { if let v = _storage._remoteHardware {
try visitor.visitSingularMessageField(value: v, fieldNumber: 10)
} }()
}
try unknownFields.traverse(visitor: &visitor)
}
@ -480,6 +498,7 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
if _storage._telemetry != rhs_storage._telemetry {return false}
if _storage._cannedMessage != rhs_storage._cannedMessage {return false}
if _storage._audio != rhs_storage._audio {return false}
if _storage._remoteHardware != rhs_storage._remoteHardware {return false}
if _storage._version != rhs_storage._version {return false}
return true
}

View file

@ -94,6 +94,10 @@ enum HardwareModel: SwiftProtobuf.Enum {
/// TODO: REPLACE
case tloraV211P8 // = 15
///
/// TODO: REPLACE
case tloraT3S3 // = 16
///
/// B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station
case stationG1 // = 25
@ -150,6 +154,10 @@ enum HardwareModel: SwiftProtobuf.Enum {
/// New Heltec Wireless Stick Lite with ESP32-S3 CPU
case heltecWslV3 // = 44
///
/// New BETAFPV ELRS Micro TX Module 2.4G with ESP32 CPU
case betafpv2400Tx // = 45
///
/// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
case privateHw // = 255
@ -177,6 +185,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
case 13: self = .rak11200
case 14: self = .nanoG1
case 15: self = .tloraV211P8
case 16: self = .tloraT3S3
case 25: self = .stationG1
case 32: self = .loraRelayV1
case 33: self = .nrf52840Dk
@ -191,6 +200,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
case 42: self = .m5Stack
case 43: self = .heltecV3
case 44: self = .heltecWslV3
case 45: self = .betafpv2400Tx
case 255: self = .privateHw
default: self = .UNRECOGNIZED(rawValue)
}
@ -214,6 +224,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
case .rak11200: return 13
case .nanoG1: return 14
case .tloraV211P8: return 15
case .tloraT3S3: return 16
case .stationG1: return 25
case .loraRelayV1: return 32
case .nrf52840Dk: return 33
@ -228,6 +239,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
case .m5Stack: return 42
case .heltecV3: return 43
case .heltecWslV3: return 44
case .betafpv2400Tx: return 45
case .privateHw: return 255
case .UNRECOGNIZED(let i): return i
}
@ -256,6 +268,7 @@ extension HardwareModel: CaseIterable {
.rak11200,
.nanoG1,
.tloraV211P8,
.tloraT3S3,
.stationG1,
.loraRelayV1,
.nrf52840Dk,
@ -270,6 +283,7 @@ extension HardwareModel: CaseIterable {
.m5Stack,
.heltecV3,
.heltecWslV3,
.betafpv2400Tx,
.privateHw,
]
}
@ -1765,6 +1779,28 @@ extension LogRecord.Level: CaseIterable {
#endif // swift(>=4.2)
struct QueueStatus {
// 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.
/// Last attempt to queue status, ErrorCode
var res: Int32 = 0
/// Free entries in the outgoing queue
var free: UInt32 = 0
/// Maximum entries in the outgoing queue
var maxlen: UInt32 = 0
/// What was mesh packet id that generated this response?
var meshPacketID: UInt32 = 0
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
///
/// Packets from the radio to the phone will appear on the fromRadio characteristic.
/// It will support READ and NOTIFY. When a new packet arrives the device will BLE notify?
@ -1888,6 +1924,15 @@ struct FromRadio {
set {_uniqueStorage()._payloadVariant = .channel(newValue)}
}
/// Queue status info
var queueStatus: QueueStatus {
get {
if case .queueStatus(let v)? = _storage._payloadVariant {return v}
return QueueStatus()
}
set {_uniqueStorage()._payloadVariant = .queueStatus(newValue)}
}
var unknownFields = SwiftProtobuf.UnknownStorage()
///
@ -1928,6 +1973,8 @@ struct FromRadio {
///
/// One packet is sent for each channel
case channel(Channel)
/// Queue status info
case queueStatus(QueueStatus)
#if !swift(>=4.1)
static func ==(lhs: FromRadio.OneOf_PayloadVariant, rhs: FromRadio.OneOf_PayloadVariant) -> Bool {
@ -1971,6 +2018,10 @@ struct FromRadio {
guard case .channel(let l) = lhs, case .channel(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.queueStatus, .queueStatus): return {
guard case .queueStatus(let l) = lhs, case .queueStatus(let r) = rhs else { preconditionFailure() }
return l == r
}()
default: return false
}
}
@ -2126,6 +2177,7 @@ extension NodeInfo: @unchecked Sendable {}
extension MyNodeInfo: @unchecked Sendable {}
extension LogRecord: @unchecked Sendable {}
extension LogRecord.Level: @unchecked Sendable {}
extension QueueStatus: @unchecked Sendable {}
extension FromRadio: @unchecked Sendable {}
extension FromRadio.OneOf_PayloadVariant: @unchecked Sendable {}
extension ToRadio: @unchecked Sendable {}
@ -2153,6 +2205,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding {
13: .same(proto: "RAK11200"),
14: .same(proto: "NANO_G1"),
15: .same(proto: "TLORA_V2_1_1P8"),
16: .same(proto: "TLORA_T3_S3"),
25: .same(proto: "STATION_G1"),
32: .same(proto: "LORA_RELAY_V1"),
33: .same(proto: "NRF52840DK"),
@ -2167,6 +2220,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding {
42: .same(proto: "M5STACK"),
43: .same(proto: "HELTEC_V3"),
44: .same(proto: "HELTEC_WSL_V3"),
45: .same(proto: "BETAFPV_2400_TX"),
255: .same(proto: "PRIVATE_HW"),
]
}
@ -3237,6 +3291,56 @@ extension LogRecord.Level: SwiftProtobuf._ProtoNameProviding {
]
}
extension QueueStatus: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = "QueueStatus"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "res"),
2: .same(proto: "free"),
3: .same(proto: "maxlen"),
4: .standard(proto: "mesh_packet_id"),
]
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.decodeSingularInt32Field(value: &self.res) }()
case 2: try { try decoder.decodeSingularUInt32Field(value: &self.free) }()
case 3: try { try decoder.decodeSingularUInt32Field(value: &self.maxlen) }()
case 4: try { try decoder.decodeSingularUInt32Field(value: &self.meshPacketID) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if self.res != 0 {
try visitor.visitSingularInt32Field(value: self.res, fieldNumber: 1)
}
if self.free != 0 {
try visitor.visitSingularUInt32Field(value: self.free, fieldNumber: 2)
}
if self.maxlen != 0 {
try visitor.visitSingularUInt32Field(value: self.maxlen, fieldNumber: 3)
}
if self.meshPacketID != 0 {
try visitor.visitSingularUInt32Field(value: self.meshPacketID, fieldNumber: 4)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: QueueStatus, rhs: QueueStatus) -> Bool {
if lhs.res != rhs.res {return false}
if lhs.free != rhs.free {return false}
if lhs.maxlen != rhs.maxlen {return false}
if lhs.meshPacketID != rhs.meshPacketID {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = "FromRadio"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
@ -3250,6 +3354,7 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
8: .same(proto: "rebooted"),
9: .same(proto: "moduleConfig"),
10: .same(proto: "channel"),
11: .same(proto: "queueStatus"),
]
fileprivate class _StorageClass {
@ -3389,6 +3494,19 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
_storage._payloadVariant = .channel(v)
}
}()
case 11: try {
var v: QueueStatus?
var hadOneofValue = false
if let current = _storage._payloadVariant {
hadOneofValue = true
if case .queueStatus(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
_storage._payloadVariant = .queueStatus(v)
}
}()
default: break
}
}
@ -3441,6 +3559,10 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
guard case .channel(let v)? = _storage._payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 10)
}()
case .queueStatus?: try {
guard case .queueStatus(let v)? = _storage._payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 11)
}()
case nil: break
}
}

View file

@ -111,6 +111,16 @@ struct ModuleConfig {
set {payloadVariant = .audio(newValue)}
}
///
/// TODO: REPLACE
var remoteHardware: ModuleConfig.RemoteHardwareConfig {
get {
if case .remoteHardware(let v)? = payloadVariant {return v}
return ModuleConfig.RemoteHardwareConfig()
}
set {payloadVariant = .remoteHardware(newValue)}
}
var unknownFields = SwiftProtobuf.UnknownStorage()
///
@ -140,6 +150,9 @@ struct ModuleConfig {
///
/// TODO: REPLACE
case audio(ModuleConfig.AudioConfig)
///
/// TODO: REPLACE
case remoteHardware(ModuleConfig.RemoteHardwareConfig)
#if !swift(>=4.1)
static func ==(lhs: ModuleConfig.OneOf_PayloadVariant, rhs: ModuleConfig.OneOf_PayloadVariant) -> Bool {
@ -179,6 +192,10 @@ struct ModuleConfig {
guard case .audio(let l) = lhs, case .audio(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.remoteHardware, .remoteHardware): return {
guard case .remoteHardware(let l) = lhs, case .remoteHardware(let r) = rhs else { preconditionFailure() }
return l == r
}()
default: return false
}
}
@ -230,6 +247,22 @@ struct ModuleConfig {
init() {}
}
///
/// RemoteHardwareModule Config
struct RemoteHardwareConfig {
// 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
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
///
/// Audio Config for codec2 voice
struct AudioConfig {
@ -844,6 +877,7 @@ extension ModuleConfig.CannedMessageConfig.InputEventChar: CaseIterable {
extension ModuleConfig: @unchecked Sendable {}
extension ModuleConfig.OneOf_PayloadVariant: @unchecked Sendable {}
extension ModuleConfig.MQTTConfig: @unchecked Sendable {}
extension ModuleConfig.RemoteHardwareConfig: @unchecked Sendable {}
extension ModuleConfig.AudioConfig: @unchecked Sendable {}
extension ModuleConfig.AudioConfig.Audio_Baud: @unchecked Sendable {}
extension ModuleConfig.SerialConfig: @unchecked Sendable {}
@ -870,6 +904,7 @@ extension ModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
6: .same(proto: "telemetry"),
7: .standard(proto: "canned_message"),
8: .same(proto: "audio"),
9: .standard(proto: "remote_hardware"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -982,6 +1017,19 @@ extension ModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
self.payloadVariant = .audio(v)
}
}()
case 9: try {
var v: ModuleConfig.RemoteHardwareConfig?
var hadOneofValue = false
if let current = self.payloadVariant {
hadOneofValue = true
if case .remoteHardware(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
self.payloadVariant = .remoteHardware(v)
}
}()
default: break
}
}
@ -1025,6 +1073,10 @@ extension ModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
guard case .audio(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 8)
}()
case .remoteHardware?: try {
guard case .remoteHardware(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 9)
}()
case nil: break
}
try unknownFields.traverse(visitor: &visitor)
@ -1099,6 +1151,38 @@ extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._Message
}
}
extension ModuleConfig.RemoteHardwareConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = ModuleConfig.protoMessageName + ".RemoteHardwareConfig"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "enabled"),
]
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) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if self.enabled != false {
try visitor.visitSingularBoolField(value: self.enabled, fieldNumber: 1)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: ModuleConfig.RemoteHardwareConfig, rhs: ModuleConfig.RemoteHardwareConfig) -> Bool {
if lhs.enabled != rhs.enabled {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

@ -31,44 +31,99 @@ struct StoreAndForward {
/// TODO: REPLACE
var rr: StoreAndForward.RequestResponse = .unset
///
/// TODO: REPLACE
var variant: StoreAndForward.OneOf_Variant? = nil
///
/// TODO: REPLACE
var stats: StoreAndForward.Statistics {
get {return _stats ?? StoreAndForward.Statistics()}
set {_stats = newValue}
get {
if case .stats(let v)? = variant {return v}
return StoreAndForward.Statistics()
}
set {variant = .stats(newValue)}
}
/// Returns true if `stats` has been explicitly set.
var hasStats: Bool {return self._stats != nil}
/// Clears the value of `stats`. Subsequent reads from it will return its default value.
mutating func clearStats() {self._stats = nil}
///
/// TODO: REPLACE
var history: StoreAndForward.History {
get {return _history ?? StoreAndForward.History()}
set {_history = newValue}
get {
if case .history(let v)? = variant {return v}
return StoreAndForward.History()
}
set {variant = .history(newValue)}
}
/// Returns true if `history` has been explicitly set.
var hasHistory: Bool {return self._history != nil}
/// Clears the value of `history`. Subsequent reads from it will return its default value.
mutating func clearHistory() {self._history = nil}
///
/// TODO: REPLACE
var heartbeat: StoreAndForward.Heartbeat {
get {return _heartbeat ?? StoreAndForward.Heartbeat()}
set {_heartbeat = newValue}
get {
if case .heartbeat(let v)? = variant {return v}
return StoreAndForward.Heartbeat()
}
set {variant = .heartbeat(newValue)}
}
///
/// Empty Payload
var empty: Bool {
get {
if case .empty(let v)? = variant {return v}
return false
}
set {variant = .empty(newValue)}
}
/// Returns true if `heartbeat` has been explicitly set.
var hasHeartbeat: Bool {return self._heartbeat != nil}
/// Clears the value of `heartbeat`. Subsequent reads from it will return its default value.
mutating func clearHeartbeat() {self._heartbeat = nil}
var unknownFields = SwiftProtobuf.UnknownStorage()
///
/// 1 - 99 = From Router
/// 101 - 199 = From Client
/// TODO: REPLACE
enum OneOf_Variant: Equatable {
///
/// TODO: REPLACE
case stats(StoreAndForward.Statistics)
///
/// TODO: REPLACE
case history(StoreAndForward.History)
///
/// TODO: REPLACE
case heartbeat(StoreAndForward.Heartbeat)
///
/// Empty Payload
case empty(Bool)
#if !swift(>=4.1)
static func ==(lhs: StoreAndForward.OneOf_Variant, rhs: StoreAndForward.OneOf_Variant) -> Bool {
// 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 (lhs, rhs) {
case (.stats, .stats): return {
guard case .stats(let l) = lhs, case .stats(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.history, .history): return {
guard case .history(let l) = lhs, case .history(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.heartbeat, .heartbeat): return {
guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.empty, .empty): return {
guard case .empty(let l) = lhs, case .empty(let r) = rhs else { preconditionFailure() }
return l == r
}()
default: return false
}
}
#endif
}
///
/// 001 - 063 = From Router
/// 064 - 127 = From Client
enum RequestResponse: SwiftProtobuf.Enum {
typealias RawValue = Int
@ -101,26 +156,30 @@ struct StoreAndForward {
/// Router is responding to a request for history.
case routerHistory // = 6
///
/// Router is responding to a request for stats.
case routerStats // = 7
///
/// Client is an in error state.
case clientError // = 101
case clientError // = 64
///
/// Client has requested a replay from the router.
case clientHistory // = 102
case clientHistory // = 65
///
/// Client has requested stats from the router.
case clientStats // = 103
case clientStats // = 66
///
/// Client has requested the router respond. This can work as a
/// "are you there" message.
case clientPing // = 104
case clientPing // = 67
///
/// The response to a "Ping"
case clientPong // = 105
case clientPong // = 68
///
/// Client has requested that the router abort processing the client's request
@ -140,11 +199,12 @@ struct StoreAndForward {
case 4: self = .routerPong
case 5: self = .routerBusy
case 6: self = .routerHistory
case 101: self = .clientError
case 102: self = .clientHistory
case 103: self = .clientStats
case 104: self = .clientPing
case 105: self = .clientPong
case 7: self = .routerStats
case 64: self = .clientError
case 65: self = .clientHistory
case 66: self = .clientStats
case 67: self = .clientPing
case 68: self = .clientPong
case 106: self = .clientAbort
default: self = .UNRECOGNIZED(rawValue)
}
@ -159,11 +219,12 @@ struct StoreAndForward {
case .routerPong: return 4
case .routerBusy: return 5
case .routerHistory: return 6
case .clientError: return 101
case .clientHistory: return 102
case .clientStats: return 103
case .clientPing: return 104
case .clientPong: return 105
case .routerStats: return 7
case .clientError: return 64
case .clientHistory: return 65
case .clientStats: return 66
case .clientPing: return 67
case .clientPong: return 68
case .clientAbort: return 106
case .UNRECOGNIZED(let i): return i
}
@ -264,10 +325,6 @@ struct StoreAndForward {
}
init() {}
fileprivate var _stats: StoreAndForward.Statistics? = nil
fileprivate var _history: StoreAndForward.History? = nil
fileprivate var _heartbeat: StoreAndForward.Heartbeat? = nil
}
#if swift(>=4.2)
@ -282,6 +339,7 @@ extension StoreAndForward.RequestResponse: CaseIterable {
.routerPong,
.routerBusy,
.routerHistory,
.routerStats,
.clientError,
.clientHistory,
.clientStats,
@ -295,6 +353,7 @@ extension StoreAndForward.RequestResponse: CaseIterable {
#if swift(>=5.5) && canImport(_Concurrency)
extension StoreAndForward: @unchecked Sendable {}
extension StoreAndForward.OneOf_Variant: @unchecked Sendable {}
extension StoreAndForward.RequestResponse: @unchecked Sendable {}
extension StoreAndForward.Statistics: @unchecked Sendable {}
extension StoreAndForward.History: @unchecked Sendable {}
@ -310,6 +369,7 @@ extension StoreAndForward: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen
2: .same(proto: "stats"),
3: .same(proto: "history"),
4: .same(proto: "heartbeat"),
5: .same(proto: "empty"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -319,9 +379,53 @@ extension StoreAndForward: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularEnumField(value: &self.rr) }()
case 2: try { try decoder.decodeSingularMessageField(value: &self._stats) }()
case 3: try { try decoder.decodeSingularMessageField(value: &self._history) }()
case 4: try { try decoder.decodeSingularMessageField(value: &self._heartbeat) }()
case 2: try {
var v: StoreAndForward.Statistics?
var hadOneofValue = false
if let current = self.variant {
hadOneofValue = true
if case .stats(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
self.variant = .stats(v)
}
}()
case 3: try {
var v: StoreAndForward.History?
var hadOneofValue = false
if let current = self.variant {
hadOneofValue = true
if case .history(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
self.variant = .history(v)
}
}()
case 4: try {
var v: StoreAndForward.Heartbeat?
var hadOneofValue = false
if let current = self.variant {
hadOneofValue = true
if case .heartbeat(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
self.variant = .heartbeat(v)
}
}()
case 5: try {
var v: Bool?
try decoder.decodeSingularBoolField(value: &v)
if let v = v {
if self.variant != nil {try decoder.handleConflictingOneOf()}
self.variant = .empty(v)
}
}()
default: break
}
}
@ -335,23 +439,31 @@ extension StoreAndForward: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen
if self.rr != .unset {
try visitor.visitSingularEnumField(value: self.rr, fieldNumber: 1)
}
try { if let v = self._stats {
switch self.variant {
case .stats?: try {
guard case .stats(let v)? = self.variant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
} }()
try { if let v = self._history {
}()
case .history?: try {
guard case .history(let v)? = self.variant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
} }()
try { if let v = self._heartbeat {
}()
case .heartbeat?: try {
guard case .heartbeat(let v)? = self.variant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 4)
} }()
}()
case .empty?: try {
guard case .empty(let v)? = self.variant else { preconditionFailure() }
try visitor.visitSingularBoolField(value: v, fieldNumber: 5)
}()
case nil: break
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: StoreAndForward, rhs: StoreAndForward) -> Bool {
if lhs.rr != rhs.rr {return false}
if lhs._stats != rhs._stats {return false}
if lhs._history != rhs._history {return false}
if lhs._heartbeat != rhs._heartbeat {return false}
if lhs.variant != rhs.variant {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
@ -366,11 +478,12 @@ extension StoreAndForward.RequestResponse: SwiftProtobuf._ProtoNameProviding {
4: .same(proto: "ROUTER_PONG"),
5: .same(proto: "ROUTER_BUSY"),
6: .same(proto: "ROUTER_HISTORY"),
101: .same(proto: "CLIENT_ERROR"),
102: .same(proto: "CLIENT_HISTORY"),
103: .same(proto: "CLIENT_STATS"),
104: .same(proto: "CLIENT_PING"),
105: .same(proto: "CLIENT_PONG"),
7: .same(proto: "ROUTER_STATS"),
64: .same(proto: "CLIENT_ERROR"),
65: .same(proto: "CLIENT_HISTORY"),
66: .same(proto: "CLIENT_STATS"),
67: .same(proto: "CLIENT_PING"),
68: .same(proto: "CLIENT_PONG"),
106: .same(proto: "CLIENT_ABORT"),
]
}

View file

@ -72,6 +72,10 @@ enum TelemetrySensorType: SwiftProtobuf.Enum {
///
/// 3-Axis magnetic sensor
case qmc5883L // = 11
///
/// High accuracy temperature and humidity
case sht31 // = 12
case UNRECOGNIZED(Int)
init() {
@ -92,6 +96,7 @@ enum TelemetrySensorType: SwiftProtobuf.Enum {
case 9: self = .qmc6310
case 10: self = .qmi8658
case 11: self = .qmc5883L
case 12: self = .sht31
default: self = .UNRECOGNIZED(rawValue)
}
}
@ -110,6 +115,7 @@ enum TelemetrySensorType: SwiftProtobuf.Enum {
case .qmc6310: return 9
case .qmi8658: return 10
case .qmc5883L: return 11
case .sht31: return 12
case .UNRECOGNIZED(let i): return i
}
}
@ -133,6 +139,7 @@ extension TelemetrySensorType: CaseIterable {
.qmc6310,
.qmi8658,
.qmc5883L,
.sht31,
]
}
@ -296,6 +303,7 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding {
9: .same(proto: "QMC6310"),
10: .same(proto: "QMI8658"),
11: .same(proto: "QMC5883L"),
12: .same(proto: "SHT31"),
]
}

View file

@ -39,10 +39,10 @@ struct Connect: View {
if node != nil {
Text(bleManager.connectedPeripheral.longName).font(.title2)
}
Text("ble.name").font(.caption)+Text(": \(bleManager.connectedPeripheral.peripheral.name ?? "Unknown")")
Text("ble.name").font(.caption)+Text(": \(bleManager.connectedPeripheral.peripheral.name ?? NSLocalizedString("unknown", comment: "Unknown"))")
.font(.caption).foregroundColor(Color.gray)
if node != nil {
Text("firmware.version").font(.caption)+Text(": \(node?.myInfo?.firmwareVersion ?? "Unknown")")
Text("firmware.version").font(.caption)+Text(": \(node?.myInfo?.firmwareVersion ?? NSLocalizedString("unknown", comment: "Unknown"))")
.font(.caption).foregroundColor(Color.gray)
}
if bleManager.isSubscribed {
@ -88,7 +88,7 @@ struct Connect: View {
Button(role: .destructive) {
if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == CBPeripheralState.connected {
bleManager.disconnectPeripheral()
bleManager.disconnectPeripheral(reconnect: false)
isPreferredRadio = false
}
} label: {
@ -101,7 +101,7 @@ struct Connect: View {
Text("Num: \(String(node!.num))")
Text("Short Name: \(node?.user?.shortName ?? "????")")
Text("Long Name: \(node?.user?.longName ?? "Unknown")")
Text("Long Name: \(node?.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))")
Text("Max Channels: \(String(node?.myInfo?.maxChannels ?? 0))")
Text("Bitrate: \(String(format: "%.2f", node?.myInfo?.bitrate ?? 0.00))")
Text("BLE RSSI: \(bleManager.connectedPeripheral.rssi)")
@ -194,7 +194,7 @@ struct Connect: View {
}
} else {
Text("Bluetooth: OFF")
Text("bluetooth.off")
.foregroundColor(.red)
.font(.title)
}
@ -210,7 +210,7 @@ struct Connect: View {
Button(role: .destructive, action: {
if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == CBPeripheralState.connected {
bleManager.disconnectPeripheral()
bleManager.disconnectPeripheral(reconnect: false)
isPreferredRadio = false
}
@ -287,6 +287,6 @@ struct Connect: View {
})
}
func didDismissSheet() {
bleManager.disconnectPeripheral()
bleManager.disconnectPeripheral(reconnect: false)
}
}

View file

@ -17,7 +17,7 @@ struct InvalidVersion: View {
VStack {
Text("Update Firmware")
Text("update.firmware")
.font(.largeTitle)
.foregroundColor(.orange)
@ -51,7 +51,7 @@ struct InvalidVersion: View {
Button {
dismiss()
} label: {
Label("Close", systemImage: "xmark")
Label("close", systemImage: "xmark")
}
.buttonStyle(.bordered)

View file

@ -30,7 +30,7 @@ struct ConnectedDevice: View {
}
} else {
Text("Bluetooth Off").font(.subheadline).foregroundColor(.red)
Text("bluetooth.off").font(.subheadline).foregroundColor(.red)
}
}
}

View file

@ -1,64 +0,0 @@
import SwiftUI
struct MessageBubble: View {
@State var showAlert = false
var contentMessage: String
var isCurrentUser: Bool
var time: Int32
var shortName: String
var id: UInt32
var body: some View {
HStack(alignment: .top) {
CircleText(text: shortName, color: isCurrentUser ? .accentColor : Color(.gray)).padding(.all, 5)
.gesture(LongPressGesture(minimumDuration: 2)
.onEnded {_ in
print("I want to delete message: \(id)")
self.showAlert = true
})
VStack(alignment: .leading) {
Text(contentMessage)
.textSelection(.enabled)
.padding(10)
.foregroundColor(.white)
.background(isCurrentUser ? .accentColor : Color(.gray))
.cornerRadius(10)
HStack(spacing: 4) {
let messageDate = Date(timeIntervalSince1970: TimeInterval(time))
if time != 0 {
Text(messageDate, style: .date).font(.caption2).foregroundColor(.gray)
Text(messageDate, style: .time).font(.caption2).foregroundColor(.gray)
} else {
Text("Unknown").font(.caption2).foregroundColor(.gray)
}
}
.padding(.bottom, 10)
}
Spacer()
}
.alert(isPresented: $showAlert) {
Alert(title: Text("Are you sure you want to delete this message?"), message: Text("This action is permanent."),
primaryButton: .destructive(Text("OK")) {
print("OK button tapped")
},
secondaryButton: .cancel()
)
}
}
}
struct MessageBubble_Previews: PreviewProvider {
static var previews: some View {
Group {
MessageBubble(contentMessage: "this is the best text ever", isCurrentUser: true, time: 0, shortName: "EB", id: 12)
}
.previewLayout(.fixed(width: 300, height: 100))
}
}

View file

@ -21,7 +21,7 @@ struct NodeAnnotation: View {
} else {
VStack(spacing: 0) {
Text("Unknown Time")
Text("unknown.age")
.font(.caption2).foregroundColor(.accentColor)
.padding(5)
.background(Color(.white))

View file

@ -7,6 +7,7 @@
import UIKit
import MapKit
import SwiftUI
// a simple circle annotation, with a string in it
class PositionAnnotation: NSObject, MKAnnotation {
@ -53,11 +54,7 @@ class PositionAnnotationView: MKAnnotationView {
guard let context = UIGraphicsGetCurrentContext() else { return }
let circleRect = CGRect(x: 1, y: 1, width: 38, height: 38)
context.setFillColor(CGColor(red: 0, green: 0.5, blue: 1.0, alpha: 1.0))
context.setFillColor(Color.accentColor.cgColor ?? CGColor(red: 0, green: 0.5, blue: 1.0, alpha: 1.0))
context.fillEllipse(in: circleRect)
}
}

View file

@ -1,173 +0,0 @@
//
// MapView.swift
// MeshtasticApple
//
// Created by Joshua Pirihi on 22/12/21.
//
import Foundation
import UIKit
import MapKit
import SwiftUI
import CoreData
#if false
// wrap a MKMapView into something we can use in SwiftUI
struct MapView: UIViewRepresentable {
var nodes: FetchedResults<NodeInfoEntity>
var mapViewDelegate = MapViewDelegate()
// observe changes to the key in UserDefaults
@AppStorage("meshMapType") var type: String = "hybrid"
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView(frame: .zero)
map.userTrackingMode = .follow
let region = MKCoordinateRegion( center: map.centerCoordinate, latitudinalMeters: CLLocationDistance(exactly: 500)!, longitudinalMeters: CLLocationDistance(exactly: 500)!)
map.setRegion(map.regionThatFits(region), animated: false)
//self.updateMapType(map)
self.showNodePositions(to: map)
self.moveToMeshRegion(in: map)
map.register(PositionAnnotationView.self, forAnnotationViewWithReuseIdentifier: NSStringFromClass(PositionAnnotationView.self))
let overlay = MKTileOverlay(urlTemplate: //"http://tiles-a.data-cdn.linz.govt.nz/services;key=7fa19132d53240708c4ff436df5b9800/tiles/v4/layer=50767/EPSG:3857/{z}/{x}/{y}.png")
"http://10.147.253.250:5050/local/map/{z}/{x}/{y}.png")
overlay.canReplaceMapContent = true
self.mapViewDelegate.renderer = MKTileOverlayRenderer(tileOverlay: overlay)
map.addOverlay(overlay)
return map
}
func updateUIView(_ view: MKMapView, context: Context) {
view.delegate = mapViewDelegate // (1) This should be set in makeUIView, but it is getting reset to `nil`
view.translatesAutoresizingMaskIntoConstraints = false // (2) In the absence of this, we get constraints error on rotation; and again, it seems one should do this in makeUIView, but has to be here
self.updateMapType(view)
self.showNodePositions(to: view)
//if (self.needToMoveToMeshRegion) {
// self.moveToMeshRegion(in: view)
// self.needToMoveToMeshRegion = false
//}
}
func moveToMeshRegion(in mapView: MKMapView) {
//go through the annotations and create a bounding box that encloses them
var minLat: CLLocationDegrees = 90.0
var maxLat: CLLocationDegrees = -90.0
var minLon: CLLocationDegrees = 180.0
var maxLon: CLLocationDegrees = -180.0
for annotation in mapView.annotations {
if annotation.isKind(of: PositionAnnotation.self) {
minLat = min(minLat, annotation.coordinate.latitude)
maxLat = max(maxLat, annotation.coordinate.latitude)
minLon = min(minLon, annotation.coordinate.longitude)
maxLon = max(maxLon, annotation.coordinate.longitude)
}
}
//check if the mesh region looks sensible before we move to it. Otherwise we won't move the map (leave it at the current location)
if maxLat < minLat || (maxLat-minLat) > 5 || maxLon < minLon || (maxLon-minLon) > 5 {
return
}
let centerCoord = CLLocationCoordinate2D(latitude: (minLat+maxLat)/2, longitude: (minLon+maxLon)/2)
let span = MKCoordinateSpan(latitudeDelta: (maxLat-minLat)*1.5, longitudeDelta: (maxLon-minLon)*1.5)
let region = mapView.regionThatFits(MKCoordinateRegion(center: centerCoord, span: span))
mapView.setRegion(region, animated: true)
}
func updateMapType(_ map: MKMapView) {
switch self.type {
case "satellite":
map.mapType = .satellite
break
case "standard":
map.mapType = .standard
break
case "hybrid":
map.mapType = .hybrid
break
default:
map.mapType = .hybrid
}
}
}
private extension MapView {
func showNodePositions(to view: MKMapView) {
// clear any existing annotations
if !view.annotations.isEmpty {
view.removeAnnotations(view.annotations)
}
for node in self.nodes {
// try and get the last position
if (node.positions?.count ?? 0) > 0 && (node.positions!.lastObject as! PositionEntity).coordinate != nil {
let annotation = PositionAnnotation()
annotation.coordinate = (node.positions!.lastObject as! PositionEntity).coordinate!
annotation.title = node.user?.longName ?? "Unknown"
annotation.shortName = node.user?.shortName?.uppercased() ?? "???"
view.addAnnotation(annotation)
}
}
}
}
class MapViewDelegate: NSObject, MKMapViewDelegate {
var renderer: MKTileOverlayRenderer?
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard !annotation.isKind(of: MKUserLocation.self) else {
// Make a fast exit if the annotation is the `MKUserLocation`, as it's not an annotation view we wish to customize.
return nil
}
var annotationView: MKAnnotationView?
if let annotation = annotation as? PositionAnnotation {
annotationView = self.setupPositionAnnotationView(for: annotation, on: mapView)
}
return annotationView
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
return self.renderer!
}
private func setupPositionAnnotationView(for annotation: PositionAnnotation, on mapView: MKMapView) -> PositionAnnotationView {
let identifier = NSStringFromClass(PositionAnnotationView.self)
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? PositionAnnotationView ?? PositionAnnotationView()
annotationView.name = annotation.shortName ?? "???"
annotationView.canShowCallout = true
return annotationView
}
}
#endif

View file

@ -118,20 +118,6 @@ public struct MapView: UIViewRepresentable {
mapView.delegate = context.coordinator
mapView.register(PositionAnnotationView.self, forAnnotationViewWithReuseIdentifier: NSStringFromClass(PositionAnnotationView.self))
/*Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { timer in
for node in self.locationNodes {
// try and get the last position
if (node.positions?.count ?? 0) > 0 && (node.positions!.lastObject as! PositionEntity).coordinate != nil {
let annotation = PositionAnnotation()
annotation.coordinate = (node.positions!.lastObject as! PositionEntity).coordinate!
annotation.title = node.user?.longName ?? "Unknown"
annotation.shortName = node.user?.shortName?.uppercased() ?? "???"
mapView.addAnnotation(annotation)
}
}
}*/
return mapView
}
@ -229,18 +215,6 @@ public struct MapView: UIViewRepresentable {
} else {
shouldMoveRegion = true
}
/*for node in self.locationNodes {
// try and get the last position
if (node.positions?.count ?? 0) > 0 && (node.positions!.lastObject as! PositionEntity).coordinate != nil {
let annotation = PositionAnnotation()
annotation.coordinate = (node.positions!.lastObject as! PositionEntity).coordinate!
annotation.title = node.user?.longName ?? "Unknown"
annotation.shortName = node.user?.shortName?.uppercased() ?? "???"
mapView.addAnnotation(annotation)
}
}*/
var displayedNodes: [Int64] = []
for position in self.positions {
@ -249,8 +223,8 @@ public struct MapView: UIViewRepresentable {
}
let annotation = PositionAnnotation()
annotation.coordinate = position.coordinate!
annotation.title = position.nodePosition!.user?.longName ?? "Unknown"
annotation.coordinate = position.nodeCoordinate!
annotation.title = position.nodePosition!.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown")
annotation.shortName = position.nodePosition!.user?.shortName?.uppercased() ?? "???"
mapView.addAnnotation(annotation)

View file

@ -0,0 +1,57 @@
//
// MapViewSwitUI.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 1/9/23.
//
import SwiftUI
import MapKit
struct MapViewSwiftUI: UIViewRepresentable {
let positions: [PositionEntity]
let region: MKCoordinateRegion
let mapViewType: MKMapType
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.mapType = mapViewType
mapView.setRegion(region, animated: true)
mapView.isRotateEnabled = true
mapView.addAnnotations(positions)
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.mapType = mapViewType
}
func makeCoordinator() -> MapCoordinator {
.init()
}
final class MapCoordinator: NSObject, MKMapViewDelegate {
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
switch annotation {
case _ as MKClusterAnnotation:
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "nodeGroup") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "nodeGroup")
annotationView.markerTintColor = .darkGray
return annotationView
case _ as PositionEntity:
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Node")
annotationView.canShowCallout = true
annotationView.glyphText = "📟"
annotationView.clusteringIdentifier = "nodeGroup"
annotationView.markerTintColor = UIColor(.accentColor)
annotationView.titleVisibility = .visible
return annotationView
default: return nil
}
}
}
}

View file

@ -32,6 +32,8 @@ struct ChannelMessageList: View {
var body: some View {
NavigationStack {
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmmssa", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss a")
ScrollViewReader { scrollView in
ScrollView {
LazyVStack {
@ -104,7 +106,7 @@ struct ChannelMessageList: View {
Menu("message.details") {
VStack {
let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp))
Text("Date \(messageDate, style: .date) \(messageDate.formattedDate(format: "h:mm:ss a"))").font(.caption2).foregroundColor(.gray)
Text(" \(messageDate.formattedDate(format: dateFormatString))").foregroundColor(.gray)
}
if !currentUser {
VStack {
@ -120,23 +122,22 @@ struct ChannelMessageList: View {
Text("waiting")
} else if currentUser && message.ackError > 0 {
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true)
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
}
if currentUser {
VStack {
let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp))
let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date())
if ackDate >= sixMonthsAgo! {
Text((ackDate.formattedDate(format: "h:mm:ss a"))).font(.caption2).foregroundColor(.gray)
Text("Ack Time: \(ackDate.formattedDate(format: "h:mm:ss a"))").foregroundColor(.gray)
} else {
Text("unknown.age").font(.caption2).foregroundColor(.gray)
Text("unknown.age").foregroundColor(.gray)
}
}
}
if message.ackSNR != 0 {
VStack {
Text("Ack SNR\(String(format: "%.2f", message.ackSNR)) dB")
.font(.caption2)
Text("Ack SNR: \(String(format: "%.2f", message.ackSNR)) dB")
.foregroundColor(.gray)
}
}
@ -184,7 +185,7 @@ struct ChannelMessageList: View {
Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.orange)
} else if currentUser && message.ackError > 0 {
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true)
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.font(.caption2).foregroundColor(.red)
}
}
@ -355,7 +356,7 @@ struct ChannelMessageList: View {
ToolbarItem(placement: .principal) {
HStack {
CircleText(text: String(channel.index), color: .accentColor, circleSize: 44, fontSize: 30).fixedSize()
Text(String(channel.name ?? "Unknown").camelCaseToWords()).font(.headline)
Text(String(channel.name ?? NSLocalizedString("unknown", comment: "Unknown")).camelCaseToWords()).font(.headline)
}
}
ToolbarItem(placement: .navigationBarTrailing) {

View file

@ -20,7 +20,8 @@ struct Contacts: View {
private var users: FetchedResults<UserEntity>
@State var node: NodeInfoEntity? = nil
@State private var selection: UserEntity? = nil // Nothing selected by default.
@State private var userSelection: UserEntity? = nil // Nothing selected by default.
@State private var channelSelection: ChannelEntity? = nil // Nothing selected by default.
@State private var isPresentingDeleteChannelMessagesConfirm: Bool = false
@State private var isPresentingDeleteUserMessagesConfirm: Bool = false
@State private var isPresentingTraceRouteSentAlert = false
@ -28,6 +29,8 @@ struct Contacts: View {
var body: some View {
NavigationSplitView {
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY")
List {
Section(header: Text("channels")) {
// Display Contacts for the rest of the non admin channels
@ -65,10 +68,10 @@ struct Contacts: View {
Text("Yesterday")
.font(.subheadline)
} else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) {
Text(lastMessageTime.formattedDate(format: "MM/dd/yy"))
Text(lastMessageTime.formattedDate(format: dateFormatString))
.font(.subheadline)
} else if lastMessageDay < (currentDay - 1800) {
Text(lastMessageTime.formattedDate(format: "MM/dd/yy"))
Text(lastMessageTime.formattedDate(format: dateFormatString))
.font(.subheadline)
}
}
@ -111,6 +114,7 @@ struct Contacts: View {
if channel.allPrivateMessages.count > 0 {
Button(role: .destructive) {
isPresentingDeleteChannelMessagesConfirm = true
channelSelection = channel
} label: {
Label("Delete Messages", systemImage: "trash")
}
@ -119,13 +123,12 @@ struct Contacts: View {
.confirmationDialog(
"This conversation will be deleted.",
isPresented: $isPresentingDeleteChannelMessagesConfirm,
titleVisibility: .visible
) {
Button(role: .destructive) {
deleteChannelMessages(channel: channel, context: context)
deleteChannelMessages(channel: channelSelection!, context: context)
context.refresh(node!.myInfo!, mergeChanges: true)
channelSelection = nil
} label: {
Text("delete")
}
@ -151,7 +154,7 @@ struct Contacts: View {
.padding(.trailing, 5)
VStack {
HStack {
Text(user.longName ?? "Unknown").font(.headline)
Text(user.longName ?? NSLocalizedString("unknown", comment: "Unknown")).font(.headline)
Spacer()
if user.messageList.count > 0 {
VStack (alignment: .trailing) {
@ -162,10 +165,10 @@ struct Contacts: View {
Text("Yesterday")
.font(.subheadline)
} else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) {
Text(lastMessageTime.formattedDate(format: "MM/dd/yy"))
Text(lastMessageTime.formattedDate(format: dateFormatString))
.font(.subheadline)
} else if lastMessageDay < (currentDay - 1800) {
Text(lastMessageTime.formattedDate(format: "MM/dd/yy"))
Text(lastMessageTime.formattedDate(format: dateFormatString))
.font(.subheadline)
}
}
@ -206,6 +209,7 @@ struct Contacts: View {
if user.messageList.count > 0 {
Button(role: .destructive) {
isPresentingDeleteUserMessagesConfirm = true
userSelection = user
} label: {
Label("Delete Messages", systemImage: "trash")
}
@ -227,7 +231,7 @@ struct Contacts: View {
titleVisibility: .visible
) {
Button(role: .destructive) {
deleteUserMessages(user: user, context: context)
deleteUserMessages(user: userSelection!, context: context)
context.refresh(node!.user!, mergeChanges: true)
} label: {
Text("delete")
@ -272,7 +276,7 @@ struct Contacts: View {
}
}
detail: {
if let user = selection {
if let user = userSelection {
UserMessageList(user:user)
} else {

View file

@ -31,6 +31,8 @@ struct UserMessageList: View {
var body: some View {
NavigationStack {
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmmss", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss:a")
ScrollViewReader { scrollView in
ScrollView {
LazyVStack {
@ -105,8 +107,9 @@ struct UserMessageList: View {
}
Menu("message.details") {
VStack {
let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp))
Text("Date \(messageDate, style: .date) \(messageDate.formattedDate(format: "h:mm:ss a"))").font(.caption2).foregroundColor(.gray)
Text("\(messageDate.formattedDate(format: dateFormatString))").foregroundColor(.gray)
}
if !currentUser {
VStack {
@ -116,20 +119,21 @@ struct UserMessageList: View {
if currentUser && message.receivedACK {
VStack {
Text("received.ack")+Text(" \(message.receivedACK ? "✔️" : "")")
Text("received.ack.real")+Text(" \(message.realACK ? "✔️" : "")")
}
} else if currentUser && message.ackError == 0 {
// Empty Error
Text("waiting")
} else if currentUser && message.ackError > 0 {
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true)
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
}
if currentUser {
VStack {
let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp))
let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date())
if ackDate >= sixMonthsAgo! {
Text((ackDate.formattedDate(format: "h:mm:ss a"))).font(.caption2).foregroundColor(.gray)
Text("Ack Time: \(ackDate.formattedDate(format: "h:mm:ss a"))").foregroundColor(.gray)
} else {
Text("unknown.age").font(.caption2).foregroundColor(.gray)
}
@ -137,7 +141,7 @@ struct UserMessageList: View {
}
if message.ackSNR != 0 {
VStack {
Text("Ack SNR\(String(format: "%.2f", message.ackSNR)) dB")
Text("Ack SNR: \(String(format: "%.2f", message.ackSNR)) dB")
.font(.caption2)
.foregroundColor(.gray)
}
@ -181,12 +185,12 @@ struct UserMessageList: View {
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
if currentUser && message.receivedACK {
// Ack Received
Text("\(ackErrorVal?.display ?? "No Error" )").font(.caption2).foregroundColor(.gray)
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").font(.caption2).foregroundColor(message.realACK ? .gray : .orange)
} else if currentUser && message.ackError == 0 {
// Empty Error
Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.orange)
} else if currentUser && message.ackError > 0 {
Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true)
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.font(.caption2).foregroundColor(.red)
}
}
@ -353,7 +357,7 @@ struct UserMessageList: View {
ToolbarItem(placement: .principal) {
HStack {
CircleText(text: user.shortName ?? "???", color: .accentColor, circleSize: 44, fontSize: 14).fixedSize()
Text(user.longName ?? "Unknown").font(.headline)
Text(user.longName ?? NSLocalizedString("unknown", comment: "Unknown")).font(.headline)
}
}
ToolbarItem(placement: .navigationBarTrailing) {

View file

@ -5,9 +5,7 @@
// Copyright(c) Garth Vander Houwen 7/7/22.
//
import SwiftUI
#if canImport(Charts)
import Charts
#endif
struct DeviceMetricsLog: View {
@ -24,7 +22,7 @@ struct DeviceMetricsLog: View {
let oneDayAgo = Calendar.current.date(byAdding: .day, value: -3, to: Date())
let data = node.telemetries!.filtered(using: NSPredicate(format: "metricsType == 0 && time !=nil && time >= %@", oneDayAgo! as CVarArg))
if data.count > 0 {
GroupBox(label: Label("Battery Level Trend", systemImage: "battery.100")) {
GroupBox(label: Label("battery.level.trend", systemImage: "battery.100")) {
Chart(data.array as! [TelemetryEntity], id: \.self) {
LineMark(
x: .value("Hour", $0.time!.formattedDate(format: "ha")),
@ -38,10 +36,13 @@ struct DeviceMetricsLog: View {
.frame(height: 150)
}
}
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma")
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
//Add a table for mac and ipad
Table(node.telemetries!.reversed() as! [TelemetryEntity]) {
TableColumn("Battery Level") { dm in
TableColumn("battery.level") { dm in
if dm.metricsType == 0 {
if dm.batteryLevel == 0 {
Text("Powered")
@ -51,24 +52,24 @@ struct DeviceMetricsLog: View {
}
}
}
TableColumn("Voltage") { dm in
TableColumn("voltage") { dm in
if dm.metricsType == 0 {
Text("\(String(format: "%.2f", dm.voltage))")
}
}
TableColumn("Channel Utilization") { dm in
TableColumn("channel.utilization") { dm in
if dm.metricsType == 0 {
Text(String(format: "%.2f", dm.channelUtilization))
}
}
TableColumn("Airtime") { dm in
TableColumn("airtime") { dm in
if dm.metricsType == 0 {
Text("\(String(format: "%.2f", dm.airUtilTx))%")
}
}
TableColumn("Time Stamp") { dm in
TableColumn("timestamp") { dm in
if dm.metricsType == 0 {
Text(dm.time?.formattedDate(format: "MM/dd/yy hh:mm") ?? "Unknown time")
Text(dm.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: ""))
}
}
}
@ -81,14 +82,14 @@ struct DeviceMetricsLog: View {
GridItem(),
GridItem(),
GridItem(),
GridItem(.fixed(120))
GridItem(.fixed(140))
]
LazyVGrid(columns: columns, alignment: .leading, spacing: 1) {
GridRow {
Text("Batt")
.font(.caption)
.fontWeight(.bold)
Text("Voltage")
Text("Volt")
.font(.caption)
.fontWeight(.bold)
Text("ChUtil")
@ -97,7 +98,7 @@ struct DeviceMetricsLog: View {
Text("AirTm")
.font(.caption)
.fontWeight(.bold)
Text("Timestamp")
Text("timestamp")
.font(.caption)
.fontWeight(.bold)
}
@ -117,8 +118,9 @@ struct DeviceMetricsLog: View {
.font(.caption)
Text("\(String(format: "%.2f", dm.airUtilTx))%")
.font(.caption)
Text(dm.time?.formattedDate(format: "MM/dd/yy hh:mm") ?? "Unknown time")
.font(.caption)
Text(dm.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: ""))
.font(.caption2)
}
}
}
@ -132,7 +134,7 @@ struct DeviceMetricsLog: View {
Button(role: .destructive) {
isPresentingClearLogConfirm = true
} label: {
Label("Clear Log", systemImage: "trash.fill")
Label("clear.log", systemImage: "trash.fill")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
@ -143,7 +145,7 @@ struct DeviceMetricsLog: View {
isPresented: $isPresentingClearLogConfirm,
titleVisibility: .visible
) {
Button("Delete all device metrics?", role: .destructive) {
Button("device.metrics.delete", role: .destructive) {
if clearTelemetry(destNum: node.num, metricsType: 0, context: context) {
print("Cleared Device Metrics for \(node.num)")
} else {
@ -162,7 +164,7 @@ struct DeviceMetricsLog: View {
.controlSize(.large)
.padding()
}
.navigationTitle("Device Metrics Log")
.navigationTitle("device.metrics.log")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
ZStack {
@ -175,14 +177,14 @@ struct DeviceMetricsLog: View {
isPresented: $isExporting,
document: CsvDocument(emptyCsv: exportString),
contentType: .commaSeparatedText,
defaultFilename: String("\(node.user!.longName ?? "Node") Device Telemetry Log"),
defaultFilename: String("\(node.user!.longName ?? "Node") \(NSLocalizedString("device.metrics.log", comment: "Device Metrics Log"))"),
onCompletion: { result in
if case .success = result {
print("Device Telemetry log download succeeded.")
print("Device metrics log download succeeded.")
self.isExporting = false
} else {
print("Device Telemetry log download failed: \(result).")
print("Device metrics log download failed: \(result).")
}
}
)

View file

@ -21,7 +21,8 @@ struct EnvironmentMetricsLog: View {
var body: some View {
NavigationStack {
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma")
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
//Add a table for mac and ipad
Table(node.telemetries!.reversed() as! [TelemetryEntity]) {
@ -40,24 +41,24 @@ struct EnvironmentMetricsLog: View {
Text("\(String(format: "%.2f", em.barometricPressure))")
}
}
TableColumn("Gas Resistance") { em in
TableColumn("gas.resistance") { em in
if em.metricsType == 1 {
Text("\(String(format: "%.2f", em.gasResistance))")
}
}
TableColumn("Current") { em in
TableColumn("current") { em in
if em.metricsType == 1 {
Text("\(String(format: "%.2f", em.current))")
}
}
TableColumn("Voltage") { em in
TableColumn("voltage") { em in
if em.metricsType == 1 {
Text("\(String(format: "%.2f", em.voltage))")
}
}
TableColumn("Time Stamp") { em in
TableColumn("timestamp") { em in
if em.metricsType == 1 {
Text(em.time?.formattedDate(format: "MM/dd/yy hh:mm") ?? "Unknown time")
Text(em.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: ""))
}
}
}
@ -68,12 +69,11 @@ struct EnvironmentMetricsLog: View {
GridItem(),
GridItem(),
GridItem(),
GridItem(.fixed(115))
GridItem(.fixed(140))
]
LazyVGrid(columns: columns, alignment: .leading, spacing: 1) {
GridRow {
Text("Temp")
.font(.caption)
.fontWeight(.bold)
@ -83,10 +83,10 @@ struct EnvironmentMetricsLog: View {
Text("Bar")
.font(.caption)
.fontWeight(.bold)
Text("Gas")
Text("gas")
.font(.caption)
.fontWeight(.bold)
Text("Timestamp")
Text("timestamp")
.font(.caption)
.fontWeight(.bold)
}
@ -104,8 +104,8 @@ struct EnvironmentMetricsLog: View {
.font(.caption)
Text("\(String(format: "%.2f", em.gasResistance))")
.font(.caption)
Text(em.time?.formattedDate(format: "MM/dd/yy hh:mm") ?? "Unknown time")
.font(.caption)
Text(em.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: ""))
.font(.caption2)
}
}
}
@ -118,11 +118,8 @@ struct EnvironmentMetricsLog: View {
HStack {
Button(role: .destructive) {
isPresentingClearLogConfirm = true
} label: {
Label("Clear Log", systemImage: "trash.fill")
}
.buttonStyle(.bordered)
@ -135,22 +132,15 @@ struct EnvironmentMetricsLog: View {
titleVisibility: .visible
) {
Button("Delete all environment metrics?", role: .destructive) {
if clearTelemetry(destNum: node.num, metricsType: 1, context: context) {
print("Clear Environment Metrics Log Failed")
}
}
}
Button {
exportString = TelemetryToCsvFile(telemetry: node.telemetries!.array as! [TelemetryEntity], metricsType: 1)
isExporting = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
@ -161,13 +151,10 @@ struct EnvironmentMetricsLog: View {
.navigationTitle("Environment Metrics 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(
@ -176,15 +163,10 @@ struct EnvironmentMetricsLog: View {
contentType: .commaSeparatedText,
defaultFilename: String("\(node.user!.longName ?? "Node") Environment Metrics Log"),
onCompletion: { result in
if case .success = result {
print("Environment metrics log download succeeded.")
self.isExporting = false
} else {
print("Environment metrics log download failed: \(result).")
}
}

View file

@ -1,73 +1,62 @@
/*
Abstract:
A view showing the details for a node.
*/
Abstract:
A view showing the details for a node.
*/
import SwiftUI
import MapKit
import CoreLocation
struct NodeDetail: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@State private var mapType: MKMapType = .standard
@State private var showingDetailsPopover = false
@State var satsInView = 0
@State private var showingShutdownConfirm: Bool = false
@State private var showingRebootConfirm: Bool = false
var node: NodeInfoEntity
var body: some View {
let hwModelString = node.user?.hwModel ?? "UNSET"
NavigationStack {
GeometryReader { bounds in
VStack {
if node.positions?.count ?? 0 > 0 {
let mostRecent = node.positions?.lastObject as! PositionEntity
if mostRecent.coordinate != nil {
let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: mostRecent.latitude!, longitude: mostRecent.longitude!)
let regionBinding = Binding<MKCoordinateRegion>(
get: {
MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005))
},
set: { _ in }
)
let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: mostRecent.latitude!, longitude: mostRecent.longitude!)
ZStack {
let annotations = node.positions?.array as! [PositionEntity]
ZStack {
let annotations = node.positions?.array as! [PositionEntity]
Map(coordinateRegion: regionBinding,
interactionModes: [.all],
showsUserLocation: true,
userTrackingMode: .constant(.follow),
annotationItems: annotations) { location in
MapViewSwiftUI(positions: annotations, region: MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)), mapViewType: mapType)
VStack {
Spacer()
Text(mostRecent.satsInView > 0 ? "Sats: \(mostRecent.satsInView)" : " ")
.font(.caption2)
return MapAnnotation(
coordinate: location.coordinate ?? CLLocationCoordinate2D(latitude: 0, longitude: 0),
content: {
NodeAnnotation(time: location.time!)
}
)
Picker("", selection: $mapType) {
Text("Standard").tag(MKMapType.standard)
Text("Hybrid").tag(MKMapType.hybrid)
Text("Satellite").tag(MKMapType.satellite)
}
.pickerStyle(SegmentedPickerStyle())
}
.ignoresSafeArea(.all, edges: [.leading, .trailing])
.frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 2)
}
Text(mostRecent.satsInView > 0 ? "Sats: \(mostRecent.satsInView)" : " ")
.offset( y:-40)
.ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
.frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.65)
}
} else {
HStack {
}
.padding([.top], 60)
.padding([.top], 20)
}
ScrollView {
@ -80,13 +69,13 @@ struct NodeDetail: View {
Divider()
VStack {
if node.user != nil {
Image(hwModelString)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 100, height: 100)
.cornerRadius(5)
Text(String(hwModelString))
.foregroundColor(.gray)
.font(.largeTitle).fixedSize()
@ -96,7 +85,7 @@ struct NodeDetail: View {
if node.snr > 0 {
Divider()
VStack(alignment: .center) {
Image(systemName: "waveform.path")
.font(.title)
.foregroundColor(.accentColor)
@ -109,15 +98,15 @@ struct NodeDetail: View {
.fixedSize()
}
}
if node.telemetries?.count ?? 0 >= 1 {
let mostRecent = node.telemetries?.lastObject as! TelemetryEntity
Divider()
VStack(alignment: .center) {
BatteryGauge(batteryLevel: Double(mostRecent.batteryLevel))
if mostRecent.voltage > 0 {
Text(String(format: "%.2f", mostRecent.voltage) + " V")
.font(.title)
.foregroundColor(.gray)
@ -147,7 +136,7 @@ struct NodeDetail: View {
Divider()
VStack {
HStack {
Image(systemName: "number")
Image(systemName: "number")
.font(.title2)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
@ -160,8 +149,8 @@ struct NodeDetail: View {
HStack {
Image(systemName: "globe")
.font(.title)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("MAC Address: ").font(.title)
}
@ -174,8 +163,8 @@ struct NodeDetail: View {
HStack {
Image(systemName: "clock.badge.checkmark.fill")
.font(.title)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("heard.last").font(.title)+Text(":").font(.title)
}
@ -190,34 +179,27 @@ struct NodeDetail: View {
} else {
HStack {
VStack(alignment: .center) {
CircleText(text: node.user?.shortName ?? "???", color: .accentColor)
}
Divider()
VStack {
if node.user != nil {
Image(node.user!.hwModel ?? "UNSET")
Image(node.user!.hwModel ?? NSLocalizedString("unset", comment: "Unset"))
.resizable()
.frame(width: 75, height: 75)
.cornerRadius(5)
Text(String(node.user!.hwModel ?? "UNSET"))
Text(String(node.user!.hwModel ?? NSLocalizedString("unset", comment: "Unset")))
.font(.callout).fixedSize()
}
}
.padding(5)
if node.snr > 0 {
Divider()
VStack(alignment: .center) {
Image(systemName: "waveform.path")
.font(.title)
.foregroundColor(.accentColor)
@ -230,15 +212,15 @@ struct NodeDetail: View {
}
.padding(5)
}
if node.telemetries?.count ?? 0 >= 1 {
let mostRecent = node.telemetries?.lastObject as! TelemetryEntity
Divider()
VStack(alignment: .center) {
BatteryGauge(batteryLevel: Double(mostRecent.batteryLevel))
if mostRecent.voltage > 0 {
@ -269,7 +251,7 @@ struct NodeDetail: View {
Divider()
VStack {
HStack {
Image(systemName: "number")
Image(systemName: "number")
.font(.title2)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
@ -282,9 +264,9 @@ struct NodeDetail: View {
Divider()
HStack {
Image(systemName: "globe")
.font(.headline)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
.font(.headline)
.foregroundColor(.accentColor)
.symbolRenderingMode(.hierarchical)
Text("MAC Address: ")
Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address")).foregroundColor(.gray)
}
@ -341,16 +323,16 @@ struct NodeDetail: View {
}
if self.bleManager.connectedPeripheral != nil && self.bleManager.connectedPeripheral.num == node.num && self.bleManager.connectedPeripheral.num == node.num {
HStack {
if hwModelString == "TBEAM" || hwModelString == "TECHO" || hwModelString.contains("4631") {
Button(action: {
showingShutdownConfirm = true
}) {
Label("Power Off", systemImage: "power")
}
.buttonStyle(.bordered)
@ -362,37 +344,32 @@ struct NodeDetail: View {
isPresented: $showingShutdownConfirm
) {
Button("Shutdown Node?", role: .destructive) {
if !bleManager.sendShutdown(destNum: node.num) {
if !bleManager.sendShutdown(fromUser: node.user!, toUser: node.user!) {
print("Shutdown Failed")
}
}
}
}
Button(action: {
showingRebootConfirm = true
}) {
Label("Reboot", systemImage: "arrow.triangle.2.circlepath")
Label("reboot", systemImage: "arrow.triangle.2.circlepath")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.confirmationDialog(
"are.you.sure",
isPresented: $showingRebootConfirm
) {
.confirmationDialog("are.you.sure",
isPresented: $showingRebootConfirm
) {
Button("reboot.node", role: .destructive) {
Button("Reboot Node?", role: .destructive) {
if !bleManager.sendReboot(destNum: node.num) {
if !bleManager.sendReboot(fromUser: node.user!, toUser: node.user!) {
print("Reboot Failed")
}
}
@ -401,20 +378,18 @@ struct NodeDetail: View {
.padding(5)
}
}
.offset( y:-40)
//.offset( y:-40)
}
.edgesIgnoringSafeArea([.leading, .trailing])
.navigationBarTitle((node.user != nil) ? String(node.user!.longName ?? "Unknown") : "Unknown", displayMode: .inline)
.navigationBarTitle(String(node.user?.longName ?? NSLocalizedString("unknown", comment: "")), displayMode: .inline)
.padding(.bottom, 10)
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(
bluetoothOn: bleManager.isSwitchedOn,
deviceConnected: bleManager.connectedPeripheral != nil,
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
}
ZStack {
ConnectedDevice(
bluetoothOn: bleManager.isSwitchedOn,
deviceConnected: bleManager.connectedPeripheral != nil,
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
}
)
.onAppear {
self.bleManager.context = context
@ -423,15 +398,3 @@ struct NodeDetail: View {
}
}
}
struct NodeInfoEntityDetail_Previews: PreviewProvider {
static let bleManager = BLEManager()
static var previews: some View {
Group {
// NodeDetail(node: node)
}
}
}

View file

@ -39,7 +39,7 @@ struct NodeList: View {
CircleText(text: node.user?.shortName ?? "???", color: .accentColor, circleSize: 52, fontSize: 16, brightness: 0.1)
.padding(.trailing, 5)
VStack(alignment: .leading) {
Text(node.user?.longName ?? "Unknown").font(.headline)
Text(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown")).font(.headline)
if connected {
HStack(alignment: .bottom) {
Image(systemName: "repeat.circle.fill")
@ -53,8 +53,8 @@ struct NodeList: View {
HStack(alignment: .bottom) {
let lastPostion = node.positions!.reversed()[0] as! PositionEntity
let myCoord = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)
if lastPostion.coordinate != nil {
let nodeCoord = CLLocation(latitude: lastPostion.coordinate!.latitude, longitude: lastPostion.coordinate!.longitude)
if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude {
let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
let metersAway = nodeCoord.distance(from: myCoord)
Image(systemName: "lines.measurement.horizontal")
.font(.title3)

View file

@ -84,8 +84,6 @@ struct NodeMap: View {
var body: some View {
//self.$userLocation = LocationHelper.currentLocation
NavigationStack {
ZStack {
@ -112,11 +110,9 @@ struct NodeMap: View {
)
.frame(maxHeight: .infinity)
.ignoresSafeArea(.all, edges: [.leading, .trailing])
}
.ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
}
}
.navigationTitle("Mesh Map")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading:
MeshtasticLogo(), trailing:

View file

@ -21,6 +21,8 @@ struct PositionLog: View {
var body: some View {
NavigationStack {
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma")
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
//Add a table for mac and ipad
@ -50,7 +52,7 @@ struct PositionLog: View {
Text("\(String(format: "%.2f", position.snr)) dB")
}
TableColumn("Time Stamp") { position in
Text(position.time?.formattedDate(format: "MM/dd/yy hh:mm") ?? "Unknown time")
Text(position.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: ""))
}
}
@ -59,11 +61,11 @@ struct PositionLog: View {
ScrollView {
// Use a grid on iOS as a table only shows a single column
let columns = [
GridItem(.fixed(90)),
GridItem(.fixed(95)),
GridItem(.fixed(95)),
GridItem(),
GridItem(),
GridItem(.fixed(115))
GridItem(.fixed(45)),
GridItem(.fixed(40)),
GridItem(.fixed(140))
]
LazyVGrid(columns: columns, alignment: .leading, spacing: 1) {
@ -81,7 +83,7 @@ struct PositionLog: View {
Text("Alt")
.font(.caption2)
.fontWeight(.bold)
Text("Timestamp")
Text("timestamp")
.font(.caption2)
.fontWeight(.bold)
}
@ -95,7 +97,7 @@ struct PositionLog: View {
.font(.caption2)
Text(String(mappin.altitude))
.font(.caption2)
Text(mappin.time?.formattedDate(format: "MM/dd/yy hh:mm") ?? "Unknown time")
Text(mappin.time?.formattedDate(format: dateFormatString) ?? "Unknown time")
.font(.caption2)
}
}
@ -125,9 +127,7 @@ struct PositionLog: View {
titleVisibility: .visible
) {
Button("Delete all positions?", role: .destructive) {
if clearPositions(destNum: node.num, context: context) {
print("Successfully Cleared Position Log")
} else {

View file

@ -46,7 +46,7 @@ struct AboutMeshtastic: View {
Link("Documentation", destination: URL(string: "https://meshtastic.org/docs/getting-started")!)
.font(.title2)
}
Text("Meshtastic Copyright(c) Meshtastic LLC")
Text("Meshtastic® Copyright Meshtastic LLC")
.font(.caption)
}
}

View file

@ -21,7 +21,8 @@ struct AdminMessageList: View {
var user: UserEntity?
var body: some View {
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmmssa", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss a")
List {
if user != nil {
@ -29,7 +30,7 @@ struct AdminMessageList: View {
HStack {
Text("\(am.adminDescription ?? "Unknown") - \(Date(timeIntervalSince1970: TimeInterval(am.messageTimestamp)), style: .date) \(Date(timeIntervalSince1970: TimeInterval(am.messageTimestamp)).formattedDate(format: "h:mm:ss a"))")
Text("\(am.adminDescription ?? NSLocalizedString("unknown", comment: "Unknown")) - \(Date(timeIntervalSince1970: TimeInterval(am.messageTimestamp)).formattedDate(format: dateFormatString))")
.font(.caption)
if am.receivedACK {
@ -37,15 +38,16 @@ struct AdminMessageList: View {
Image(systemName: "checkmark.square")
.foregroundColor(.gray)
.font(.caption)
Text("Acknowledged: \(Date(timeIntervalSince1970: TimeInterval(am.ackTimestamp)).formattedDate(format: "h:mm:ss a"))")
Text("routing.acknowledged").foregroundColor(.gray).font(.caption) + Text(": \(Date(timeIntervalSince1970: TimeInterval(am.ackTimestamp)).formattedDate(format: "h:mm:ss a"))")
.foregroundColor(.gray)
.font(.caption)
} else {
let ackErrorVal = RoutingError(rawValue: Int(am.ackError))
Image(systemName: "square")
.foregroundColor(.gray)
.font(.caption)
Text("Not Acknowledged")
Text(ackErrorVal?.display ?? "Empty Ack Error")
.foregroundColor(.gray)
.font(.caption)
}
@ -53,15 +55,12 @@ struct AdminMessageList: View {
}
}
}
.navigationTitle("Admin Message Log")
.navigationTitle("admin.log")
.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

@ -4,91 +4,6 @@ import SwiftUI
import SwiftProtobuf
import MapKit
enum KeyboardType: Int, CaseIterable, Identifiable {
case defaultKeyboard = 0
case asciiCapable = 1
case twitter = 9
case emailAddress = 7
case numbersAndPunctuation = 2
var id: Int { self.rawValue }
var description: String {
get {
switch self {
case .defaultKeyboard:
return NSLocalizedString("default", comment: "Default Keyboard")
case .asciiCapable:
return NSLocalizedString("ascii.capable", comment: "ASCII Capable Keyboard")
case .twitter:
return NSLocalizedString("twitter", comment: "Twitter Keyboard")
case .emailAddress:
return NSLocalizedString("email.address", comment: "Email Address Keyboard")
case .numbersAndPunctuation:
return NSLocalizedString("numbers.punctuation", comment: "Numbers and Punctuation Keyboard")
}
}
}
}
enum MeshMapType: String, CaseIterable, Identifiable {
case satellite = "satellite"
case hybrid = "hybrid"
case standard = "standard"
var id: String { self.rawValue }
var description: String {
get {
switch self {
case .satellite:
return NSLocalizedString("satellite", comment: "Satellite Map Type")
case .standard:
return NSLocalizedString("standard", comment: "Standard Map Type")
case .hybrid:
return NSLocalizedString("hybrid", comment: "Hybrid Map Type")
}
}
}
}
enum LocationUpdateInterval: Int, CaseIterable, Identifiable {
case fiveSeconds = 5
case tenSeconds = 10
case fifteenSeconds = 15
case thirtySeconds = 30
case oneMinute = 60
case fiveMinutes = 300
case tenMinutes = 600
case fifteenMinutes = 900
var id: Int { self.rawValue }
var description: String {
get {
switch self {
case .fiveSeconds:
return NSLocalizedString("interval.five.seconds", comment: "Five Seconds")
case .tenSeconds:
return NSLocalizedString("interval.ten.seconds", comment: "Ten Seconds")
case .fifteenSeconds:
return NSLocalizedString("interval.fifteen.seconds", comment: "Fifteen Seconds")
case .thirtySeconds:
return NSLocalizedString("interval.thirty.seconds", comment: "Thirty Seconds")
case .oneMinute:
return NSLocalizedString("interval.one.minute", comment: "One Minute")
case .fiveMinutes:
return NSLocalizedString("interval.five.minutes", comment: "Five Minutes")
case .tenMinutes:
return NSLocalizedString("interval.ten.minutes", comment: "Ten Minutes")
case .fifteenMinutes:
return NSLocalizedString("interval.fifteen.minutes", comment: "Fifteen Minutes")
}
}
}
}
struct AppSettings: View {
@Environment(\.managedObjectContext) var context

View file

@ -1,288 +1,297 @@
////
//// ShareChannel.swift
//// MeshtasticApple
////
//// Copyright(c) Garth Vander Houwen 4/8/22.
////
//import SwiftUI
//import CoreData
//
//func generateChannelKey(size: Int) -> String {
// var keyData = Data(count: size)
// _ = keyData.withUnsafeMutableBytes {
// SecRandomCopyBytes(kSecRandomDefault, size, $0.baseAddress!)
// }
// return keyData.base64EncodedString()
//}
// ShareChannel.swift
// MeshtasticApple
//
//struct Channels: View {
//
// @Environment(\.managedObjectContext) var context
// @EnvironmentObject var bleManager: BLEManager
// @Environment(\.dismiss) private var goBack
// @Environment(\.sizeCategory) var sizeCategory
// Copyright(c) Garth Vander Houwen 4/8/22.
//
//
// var node: NodeInfoEntity?
//
// @State var hasChanges = false
// @State private var isPresentingEditView = false
// @State private var isPresentingSaveConfirm: Bool = false
// @State private var channelIndex: Int32 = 0
// @State private var channelName = ""
// @State private var channelKeySize = 32
// @State private var channelKey = "AQ=="
// @State private var channelRole = 0
// @State private var uplink = false
// @State private var downlink = false
//
// var body: some View {
//
// NavigationStack {
// List {
// if node != nil && node?.myInfo != nil {
// ForEach(node!.myInfo!.channels?.array as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in
// Button(action: {
// channelIndex = channel.index
// channelRole = Int(channel.role)
// channelKey = channel.psk?.base64EncodedString() ?? ""
// if channelKey.count == 0 {
// channelKeySize = 0
// } else if channelKey == "AQ==" {
// channelKeySize = -1
// } else if channelKey.count == 24 {
// channelKeySize = 16
// } else if channelKey.count == 32 {
// channelKeySize = 24
// } else if channelKey.count == 44 {
// channelKeySize = 32
// }
// channelName = channel.name ?? ""
// uplink = channel.uplinkEnabled
// downlink = channel.downlinkEnabled
// isPresentingEditView = true
// hasChanges = false
// }) {
// VStack(alignment: .leading) {
// HStack {
// CircleText(text: String(channel.index), color: .accentColor, circleSize: 45, fontSize: 36, brightness: 0.1)
// .padding(.trailing, 5)
// VStack {
// HStack {
// if channel.name?.isEmpty ?? false {
// if channel.role == 1 {
// Text(String("PrimaryChannel").camelCaseToWords()).font(.headline)
// } else {
// Text(String("Channel \(channel.index)").camelCaseToWords()).font(.headline)
// }
// } else {
// Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords()).font(.headline)
// }
// }
// }
// }
// }
// }
// }
// }
// }
// if node?.myInfo?.channels?.array.count ?? 0 < 8 {
//
// Button {
// let key = generateChannelKey(size: 32)
// channelName = ""
// channelIndex = Int32(node!.myInfo!.channels!.array.count)
// channelRole = 2
// channelKey = key
// uplink = false
// downlink = false
// hasChanges = false
// isPresentingEditView = true
//
// } label: {
// Label("Add Channel", systemImage: "plus.square")
// }
// .buttonStyle(.bordered)
// .buttonBorderShape(.capsule)
// .controlSize(.large)
// .padding()
// .sheet(isPresented: $isPresentingEditView) {
//
// #if targetEnvironment(macCatalyst)
// Text("channel")
// .font(.largeTitle)
// .padding()
// #endif
// Form {
// HStack {
// Text("name")
// Spacer()
// TextField(
// "Channel Name",
// text: $channelName
// )
// .disableAutocorrection(true)
// .keyboardType(.alphabet)
// .foregroundColor(Color.gray)
// .disabled(channelRole == 1 && channelName.count > 0)
// .onChange(of: channelName, perform: { value in
// channelName = channelName.replacing(" ", with: "")
// let totalBytes = channelName.utf8.count
// // Only mess with the value if it is too big
// if totalBytes > 11 {
// let firstNBytes = Data(channelName.utf8.prefix(11))
// if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// // Set the channelName back to the last place where it was the right size
// channelName = maxBytesString
// }
// }
// hasChanges = true
// })
// }
// HStack {
// Picker("Key Size", selection: $channelKeySize) {
// Text("Empty").tag(0)
// Text("Default").tag(-1)
// Text("1 bit").tag(1)
// Text("128 bit").tag(16)
// Text("192 bit").tag(24)
// Text("256 bit").tag(32)
// }
// .pickerStyle(DefaultPickerStyle())
// Spacer()
// Button {
// if channelKeySize == -1 {
// channelKey = "AQ=="
// } else {
// let key = generateChannelKey(size: channelKeySize)
// channelKey = key
// }
// } label: {
// Image(systemName: "lock.rotation")
// .font(.title)
// }
// .buttonStyle(.bordered)
// .buttonBorderShape(.capsule)
// .controlSize(.small)
// }
// HStack (alignment: .top) {
// Text("Key")
// Spacer()
// TextField (
// "",
// text: $channelKey,
// axis: .vertical
// )
// .foregroundColor(Color.gray)
// .disabled(true)
//
// }
// .textSelection(.enabled)
// Picker("Channel Role", selection: $channelRole) {
// if channelRole == 1 {
// Text("Primary").tag(1)
// } else{
// Text("Disabled").tag(0)
// Text("Secondary").tag(2)
// }
// }
// .pickerStyle(DefaultPickerStyle())
// .disabled(channelRole == 1)
// Toggle("Uplink Enabled", isOn: $uplink)
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
// Toggle("Downlink Enabled", isOn: $downlink)
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
// }
// .onSubmit {
// //validate(name: channelName)
// }
// .onChange(of: channelName) { newName in
// hasChanges = true
// }
// .onChange(of: channelKeySize) { newKeySize in
// if channelKeySize == -1 {
// channelKey = "AQ=="
// } else {
// let key = generateChannelKey(size: channelKeySize)
// channelKey = key
// }
// hasChanges = true
// }
// .onChange(of: channelKey) { newKey in
// hasChanges = true
// }
// .onChange(of: channelRole) { newRole in
// hasChanges = true
// }
// .onChange(of: uplink) { newUplink in
// hasChanges = true
// }
// .onChange(of: downlink) { newDownlink in
// hasChanges = true
// }
// HStack {
// Button {
// isPresentingSaveConfirm = true
// } label: {
// Label("save", systemImage: "square.and.arrow.down")
// }
// .disabled(bleManager.connectedPeripheral == nil || !hasChanges)
// .buttonStyle(.bordered)
// .buttonBorderShape(.capsule)
// .controlSize(.large)
// .padding(.bottom)
// .confirmationDialog(
// "are.you.sure",
// isPresented: $isPresentingSaveConfirm,
// titleVisibility: .visible
// ) {
// Button("Save Channel \(channelIndex) to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") {
//
// var channel = Channel()
// channel.index = channelIndex
// channel.settings.id = UInt32(channelIndex)
// channel.settings.name = channelName
// channel.settings.psk = Data(base64Encoded: channelKey) ?? Data()
// channel.role = ChannelRoles(rawValue: channelRole)?.protoEnumValue() ?? .secondary
// channel.settings.uplinkEnabled = uplink
// channel.settings.downlinkEnabled = downlink
//
// let adminMessageId = bleManager.saveChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!)
//
// if adminMessageId > 0 {
// // Should show a saved successfully alert once I know that to be true
// // for now just disable the button after a successful save
// channelName = ""
// hasChanges = false
// isPresentingEditView = false
// bleManager.disconnectPeripheral()
// }
// }
// }
// #if targetEnvironment(macCatalyst)
// Button {
// isPresentingEditView = false
// } label: {
// Label("Close", systemImage: "xmark")
// }
// .buttonStyle(.bordered)
// .buttonBorderShape(.capsule)
// .controlSize(.large)
// .padding(.bottom)
// #endif
// }
// .presentationDetents([.medium, .large])
// }
// }
// }
// .navigationTitle("channels")
// .navigationSplitViewStyle(.automatic)
// .navigationBarItems(trailing:
// ZStack {
// ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
// })
// .onAppear {
// bleManager.context = context
// }
// }
//}
import SwiftUI
import CoreData
func generateChannelKey(size: Int) -> String {
var keyData = Data(count: size)
_ = keyData.withUnsafeMutableBytes {
SecRandomCopyBytes(kSecRandomDefault, size, $0.baseAddress!)
}
return keyData.base64EncodedString()
}
struct Channels: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@Environment(\.dismiss) private var goBack
@Environment(\.sizeCategory) var sizeCategory
var node: NodeInfoEntity?
@State var hasChanges = false
@State private var isPresentingEditView = false
@State private var isPresentingSaveConfirm: Bool = false
@State private var channelIndex: Int32 = 0
@State private var channelName = ""
@State private var channelKeySize = 32
@State private var channelKey = "AQ=="
@State private var channelRole = 0
@State private var uplink = false
@State private var downlink = false
var body: some View {
NavigationStack {
List {
if node != nil && node?.myInfo != nil {
ForEach(node!.myInfo!.channels?.array as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in
Button(action: {
channelIndex = channel.index
channelRole = Int(channel.role)
channelKey = channel.psk?.base64EncodedString() ?? ""
if channelKey.count == 0 {
channelKeySize = 0
} else if channelKey == "AQ==" {
channelKeySize = -1
} else if channelKey.count == 24 {
channelKeySize = 16
} else if channelKey.count == 32 {
channelKeySize = 24
} else if channelKey.count == 44 {
channelKeySize = 32
}
channelName = channel.name ?? ""
uplink = channel.uplinkEnabled
downlink = channel.downlinkEnabled
isPresentingEditView = true
hasChanges = false
}) {
VStack(alignment: .leading) {
HStack {
CircleText(text: String(channel.index), color: .accentColor, circleSize: 45, fontSize: 36, brightness: 0.1)
.padding(.trailing, 5)
VStack {
HStack {
if channel.name?.isEmpty ?? false {
if channel.role == 1 {
Text(String("PrimaryChannel").camelCaseToWords()).font(.headline)
} else {
Text(String("Channel \(channel.index)").camelCaseToWords()).font(.headline)
}
} else {
Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords()).font(.headline)
}
}
}
}
}
}
}
}
}
if node?.myInfo?.channels?.array.count ?? 0 < 8 && node != nil {
Button {
let key = generateChannelKey(size: 32)
channelName = ""
channelIndex = Int32(node!.myInfo!.channels!.array.count)
channelRole = 2
channelKey = key
uplink = false
downlink = false
hasChanges = false
isPresentingEditView = true
} label: {
Label("Add Channel", systemImage: "plus.square")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.sheet(isPresented: $isPresentingEditView) {
#if targetEnvironment(macCatalyst)
Text("channel")
.font(.largeTitle)
.padding()
#endif
Form {
HStack {
Text("name")
Spacer()
TextField(
"Channel Name",
text: $channelName
)
.disableAutocorrection(true)
.keyboardType(.alphabet)
.foregroundColor(Color.gray)
.disabled(channelRole == 1 && channelName.count > 0)
.onChange(of: channelName, perform: { value in
channelName = channelName.replacing(" ", with: "")
let totalBytes = channelName.utf8.count
// Only mess with the value if it is too big
if totalBytes > 11 {
let firstNBytes = Data(channelName.utf8.prefix(11))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the channelName back to the last place where it was the right size
channelName = maxBytesString
}
}
hasChanges = true
})
}
HStack {
Picker("Key Size", selection: $channelKeySize) {
Text("Empty").tag(0)
Text("Default").tag(-1)
Text("1 bit").tag(1)
Text("128 bit").tag(16)
Text("192 bit").tag(24)
Text("256 bit").tag(32)
}
.pickerStyle(DefaultPickerStyle())
Spacer()
Button {
if channelKeySize == -1 {
channelKey = "AQ=="
} else {
let key = generateChannelKey(size: channelKeySize)
channelKey = key
}
} label: {
Image(systemName: "lock.rotation")
.font(.title)
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.small)
}
HStack (alignment: .top) {
Text("Key")
Spacer()
TextField (
"",
text: $channelKey,
axis: .vertical
)
.foregroundColor(Color.gray)
.disabled(true)
}
.textSelection(.enabled)
Picker("Channel Role", selection: $channelRole) {
if channelRole == 1 {
Text("Primary").tag(1)
} else{
Text("Disabled").tag(0)
Text("Secondary").tag(2)
}
}
.pickerStyle(DefaultPickerStyle())
.disabled(channelRole == 1)
Toggle("Uplink Enabled", isOn: $uplink)
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle("Downlink Enabled", isOn: $downlink)
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
//.onSubmit {
//validate(name: channelName)
//}
.onChange(of: channelName) { newName in
hasChanges = true
}
.onChange(of: channelKeySize) { newKeySize in
if channelKeySize == -1 {
channelKey = "AQ=="
} else {
let key = generateChannelKey(size: channelKeySize)
channelKey = key
}
hasChanges = true
}
.onChange(of: channelKey) { newKey in
hasChanges = true
}
.onChange(of: channelRole) { newRole in
hasChanges = true
}
.onChange(of: uplink) { newUplink in
hasChanges = true
}
.onChange(of: downlink) { newDownlink in
hasChanges = true
}
HStack {
Button {
var channel = Channel()
channel.index = channelIndex
channel.role = ChannelRoles(rawValue: channelRole)?.protoEnumValue() ?? .secondary
if channel.role != Channel.Role.disabled {
channel.settings.id = UInt32(channelIndex)
channel.settings.name = channelName
channel.settings.psk = Data(base64Encoded: channelKey) ?? Data()
channel.settings.uplinkEnabled = uplink
channel.settings.downlinkEnabled = downlink
} else {
if channelIndex <= node!.myInfo!.channels?.count ?? 0 {
let channelEntity = node!.myInfo!.channels?[Int(channelIndex)] as! ChannelEntity
context.delete(channelEntity)
do {
try context.save()
print("💾 Deleted Channel: \(channel.settings.name)")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Unresolved Core Data error in the channel editor. Error: \(nsError)")
}
}
}
let adminMessageId = bleManager.saveChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save.
self.isPresentingEditView = false
channelName = ""
hasChanges = false
// Would rather send a getChannel but I can't seem serialize it properly yet
bleManager.getChannel(channel: channel, fromUser: node!.user!, toUser: node!.user!)
//bleManager.sendWantConfig()
}
} label: {
Label("save", systemImage: "square.and.arrow.down")
}
.disabled(bleManager.connectedPeripheral == nil || !hasChanges)
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
#if targetEnvironment(macCatalyst)
Button {
isPresentingEditView = false
} label: {
Label("close", systemImage: "xmark")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
#endif
}
.presentationDetents([.medium, .large])
}
}
}
.navigationTitle("channels")
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
})
.onAppear {
bleManager.context = context
}
}
}

View file

@ -33,89 +33,87 @@ struct BluetoothConfig: View {
var body: some View {
VStack {
Form {
Section(header: Text("options")) {
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "antenna.radiowaves.left.and.right")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Picker("Pairing Mode", selection: $mode ) {
ForEach(BluetoothModes.allCases) { bm in
Text(bm.description)
}
}
.pickerStyle(DefaultPickerStyle())
if mode == 1 {
HStack {
Label("Fixed PIN", systemImage: "wallet.pass")
TextField("Fixed PIN", text: $fixedPin)
.foregroundColor(.gray)
.onChange(of: fixedPin, perform: { value in
//Require that pin is no more than 6 numbers and no less than 6 numbers
if fixedPin.utf8.count == pinLength {
shortPin = false
} else if fixedPin.utf8.count > pinLength {
shortPin = false
fixedPin = String(fixedPin.prefix(pinLength))
} else if fixedPin.utf8.count < pinLength {
shortPin = true
}
})
.foregroundColor(.gray)
}
.keyboardType(.decimalPad)
if shortPin {
Text("BLE Pin must be 6 digits long.")
.font(.callout)
.foregroundColor(.red)
}
}
}
}
.disabled(bleManager.connectedPeripheral == nil)
Form {
Section(header: Text("options")) {
Button {
isPresentingSaveConfirm = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
}
.disabled(bleManager.connectedPeripheral == nil || !hasChanges || shortPin)
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.confirmationDialog(
"Are you sure you want to save?",
isPresented: $isPresentingSaveConfirm,
titleVisibility: .visible
) {
Button("Save Config for \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")") {
var bc = Config.BluetoothConfig()
bc.enabled = enabled
bc.mode = BluetoothModes(rawValue: mode)?.protoEnumValue() ?? Config.BluetoothConfig.PairingMode.randomPin
bc.fixedPin = UInt32(fixedPin) ?? 123456
let adminMessageId = bleManager.saveBluetoothConfig(config: bc, fromUser: node!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
hasChanges = false
goBack()
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "antenna.radiowaves.left.and.right")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Picker("bluetooth.pairingmode", selection: $mode ) {
ForEach(BluetoothModes.allCases) { bm in
Text(bm.description)
}
}
.pickerStyle(DefaultPickerStyle())
if mode == 1 {
HStack {
Label("bluetooth.mode.fixedpin", systemImage: "wallet.pass")
TextField("bluetooth.mode.fixedpin", text: $fixedPin)
.foregroundColor(.gray)
.onChange(of: fixedPin, perform: { value in
// Don't let the first character be 0 because it will get stripped when saving a UInt32
if fixedPin.first == "0" {
fixedPin = fixedPin.replacing("0", with: "")
}
//Require that pin is no more than 6 numbers and no less than 6 numbers
if fixedPin.utf8.count == pinLength {
shortPin = false
} else if fixedPin.utf8.count > pinLength {
shortPin = false
fixedPin = String(fixedPin.prefix(pinLength))
} else if fixedPin.utf8.count < pinLength {
shortPin = true
}
})
.foregroundColor(.gray)
}
.keyboardType(.decimalPad)
if shortPin {
Text("bluetooth.pin.validation")
.font(.callout)
.foregroundColor(.red)
}
}
} message: {
Text("After bluetooth config saves the node will reboot.")
}
}
.disabled(bleManager.connectedPeripheral == nil)
Button {
isPresentingSaveConfirm = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
}
.disabled(bleManager.connectedPeripheral == nil || !hasChanges || shortPin)
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingSaveConfirm,
titleVisibility: .visible
) {
let nodeName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : NSLocalizedString("unknown", comment: "Unknown")
let buttonText = String.localizedStringWithFormat(NSLocalizedString("save.config %@", comment: "Save Config for %@"), nodeName)
Button(buttonText) {
var bc = Config.BluetoothConfig()
bc.enabled = enabled
bc.mode = BluetoothModes(rawValue: mode)?.protoEnumValue() ?? Config.BluetoothConfig.PairingMode.randomPin
bc.fixedPin = UInt32(fixedPin) ?? 123456
let adminMessageId = bleManager.saveBluetoothConfig(config: bc, fromUser: node!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
hasChanges = false
goBack()
}
}
} message: {
Text("config.save.confirm")
}
.navigationTitle("bluetooth.config")
.navigationBarItems(trailing:
ZStack {

View file

@ -63,7 +63,7 @@ struct DeviceConfig: View {
Picker("Button GPIO", selection: $buttonGPIO) {
ForEach(0..<40) {
if $0 == 0 {
Text("Unset")
Text("unset")
} else {
Text("Pin \($0)")
}
@ -73,7 +73,7 @@ struct DeviceConfig: View {
Picker("Buzzer GPIO", selection: $buzzerGPIO) {
ForEach(0..<40) {
if $0 == 0 {
Text("Unset")
Text("unset")
} else {
Text("Pin \($0)")
}
@ -90,7 +90,7 @@ struct DeviceConfig: View {
Button("Reset NodeDB", role: .destructive) {
isPresentingNodeDBResetConfirm = true
}
.disabled(bleManager.connectedPeripheral == nil)
.disabled(node?.user == nil)
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
@ -101,7 +101,7 @@ struct DeviceConfig: View {
titleVisibility: .visible
) {
Button("Erase all device and app data?", role: .destructive) {
if bleManager.sendNodeDBReset(destNum: bleManager.connectedPeripheral.num) {
if bleManager.sendNodeDBReset(fromUser: node!.user!, toUser: node!.user!) {
bleManager.disconnectPeripheral()
clearCoreDataDatabase(context: context)
} else {
@ -112,7 +112,7 @@ struct DeviceConfig: View {
Button("Factory Reset", role: .destructive) {
isPresentingFactoryResetConfirm = true
}
.disabled(bleManager.connectedPeripheral == nil)
.disabled(node?.user == nil)
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
@ -124,7 +124,7 @@ struct DeviceConfig: View {
) {
Button("Factory reset your device and app? ", role: .destructive) {
if bleManager.sendFactoryReset(destNum: bleManager.connectedPeripheral.num) {
if bleManager.sendFactoryReset(fromUser: node!.user!, toUser: node!.user!) {
bleManager.disconnectPeripheral()
clearCoreDataDatabase(context: context)
} else {
@ -152,11 +152,13 @@ struct DeviceConfig: View {
.padding()
.confirmationDialog(
"Are you sure you want to save?",
"are.you.sure",
isPresented: $isPresentingSaveConfirm,
titleVisibility: .visible
) {
Button("Save Device Config to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") {
let nodeName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : NSLocalizedString("unknown", comment: "Unknown")
let buttonText = String.localizedStringWithFormat(NSLocalizedString("save.config %@", comment: "Save Config for %@"), nodeName)
Button(buttonText) {
var dc = Config.DeviceConfig()
dc.role = DeviceRoles(rawValue: deviceRole)!.protoEnumValue()
@ -175,18 +177,14 @@ struct DeviceConfig: View {
}
}
message: {
Text("After device config saves the node will reboot.")
Text("config.save.confirm")
}
}
Spacer()
}
.navigationTitle("device.config")
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
})
.onAppear {

View file

@ -27,110 +27,109 @@ struct DisplayConfig: View {
var body: some View {
VStack {
Form {
Section(header: Text("Device Screen")) {
Picker("Screen on for", selection: $screenOnSeconds ) {
ForEach(ScreenOnIntervals.allCases) { soi in
Text(soi.description)
}
Form {
Section(header: Text("Device Screen")) {
Picker("Screen on for", selection: $screenOnSeconds ) {
ForEach(ScreenOnIntervals.allCases) { soi in
Text(soi.description)
}
.pickerStyle(DefaultPickerStyle())
Text("How long the screen remains on after the user button is pressed or messages are received.")
.font(.caption)
Picker("Carousel Interval", selection: $screenCarouselInterval ) {
ForEach(ScreenCarouselIntervals.allCases) { sci in
Text(sci.description)
}
}
.pickerStyle(DefaultPickerStyle())
Text("Automatically toggles to the next page on the screen like a carousel, based the specified interval.")
.font(.caption)
Toggle(isOn: $compassNorthTop) {
Label("Always point north", systemImage: "location.north.circle")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("The compass heading on the screen outside of the circle will always point north.")
.font(.caption)
Toggle(isOn: $flipScreen) {
Label("Flip Screen", systemImage: "pip.swap")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("Flip screen vertically")
.font(.caption)
Picker("OLED Type", selection: $oledType ) {
ForEach(OledTypes.allCases) { ot in
Text(ot.description)
}
}
.pickerStyle(DefaultPickerStyle())
Text("Override automatic OLED screen detection.")
.font(.caption)
}
Section(header: Text("Format")) {
Picker("GPS Format", selection: $gpsFormat ) {
ForEach(GpsFormats.allCases) { lu in
Text(lu.description)
}
.pickerStyle(DefaultPickerStyle())
Text("How long the screen remains on after the user button is pressed or messages are received.")
.font(.caption)
Picker("Carousel Interval", selection: $screenCarouselInterval ) {
ForEach(ScreenCarouselIntervals.allCases) { sci in
Text(sci.description)
}
.pickerStyle(DefaultPickerStyle())
Text("The format used to display GPS coordinates on the device screen.")
.font(.caption)
.listRowSeparator(.visible)
}
.pickerStyle(DefaultPickerStyle())
Text("Automatically toggles to the next page on the screen like a carousel, based the specified interval.")
.font(.caption)
Toggle(isOn: $compassNorthTop) {
Label("Always point north", systemImage: "location.north.circle")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("The compass heading on the screen outside of the circle will always point north.")
.font(.caption)
Toggle(isOn: $flipScreen) {
Label("Flip Screen", systemImage: "pip.swap")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("Flip screen vertically")
.font(.caption)
Picker("OLED Type", selection: $oledType ) {
ForEach(OledTypes.allCases) { ot in
Text(ot.description)
}
}
.pickerStyle(DefaultPickerStyle())
Text("Override automatic OLED screen detection.")
.font(.caption)
}
.disabled(bleManager.connectedPeripheral == nil)
Button {
isPresentingSaveConfirm = true
Section(header: Text("Format")) {
Picker("GPS Format", selection: $gpsFormat ) {
ForEach(GpsFormats.allCases) { lu in
Text(lu.description)
}
}
.pickerStyle(DefaultPickerStyle())
} label: {
Label("save", systemImage: "square.and.arrow.down")
Text("The format used to display GPS coordinates on the device screen.")
.font(.caption)
.listRowSeparator(.visible)
}
.disabled(bleManager.connectedPeripheral == nil || !hasChanges)
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingSaveConfirm
) {
Button("Save Display Config to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") {
var dc = Config.DisplayConfig()
dc.gpsFormat = GpsFormats(rawValue: gpsFormat)!.protoEnumValue()
dc.screenOnSecs = UInt32(screenOnSeconds)
dc.autoScreenCarouselSecs = UInt32(screenCarouselInterval)
dc.compassNorthTop = compassNorthTop
dc.flipScreen = flipScreen
dc.oled = OledTypes(rawValue: oledType)!.protoEnumValue()
let adminMessageId = bleManager.saveDisplayConfig(config: dc, fromUser: node!.user!, toUser: node!.user!)
if adminMessageId > 0 {
}
.disabled(bleManager.connectedPeripheral == nil)
Button {
// 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()
}
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
) {
let nodeName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : NSLocalizedString("unknown", comment: "Unknown")
let buttonText = String.localizedStringWithFormat(NSLocalizedString("save.config %@", comment: "Save Config for %@"), nodeName)
Button(buttonText) {
var dc = Config.DisplayConfig()
dc.gpsFormat = GpsFormats(rawValue: gpsFormat)!.protoEnumValue()
dc.screenOnSecs = UInt32(screenOnSeconds)
dc.autoScreenCarouselSecs = UInt32(screenCarouselInterval)
dc.compassNorthTop = compassNorthTop
dc.flipScreen = flipScreen
dc.oled = OledTypes(rawValue: oledType)!.protoEnumValue()
let adminMessageId = bleManager.saveDisplayConfig(config: dc, fromUser: node!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
hasChanges = false
goBack()
}
}
}
message: {
Text("config.save.confirm")
}
.navigationTitle("display.config")
.navigationBarItems(trailing:
ZStack {

View file

@ -71,11 +71,13 @@ struct LoRaConfig: View {
.controlSize(.large)
.padding()
.confirmationDialog(
"Are you sure you want to save?",
"are.you.sure",
isPresented: $isPresentingSaveConfirm,
titleVisibility: .visible
) {
Button("Save Config for \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")") {
let nodeName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : NSLocalizedString("unknown", comment: "Unknown")
let buttonText = String.localizedStringWithFormat(NSLocalizedString("save.config %@", comment: "Save Config for %@"), nodeName)
Button(buttonText) {
var lc = Config.LoRaConfig()
lc.hopLimit = UInt32(hopLimit)
lc.region = RegionCodes(rawValue: region)!.protoEnumValue()
@ -91,7 +93,7 @@ struct LoRaConfig: View {
}
}
} message: {
Text("After LoRa config saves the node will reboot.")
Text("config.save.confirm")
}
}
.navigationTitle("lora.config")

View file

@ -74,7 +74,7 @@ struct CannedMessagesConfig: View {
HStack {
Label("Messages", systemImage: "message.fill")
TextField("Messages seperate with |", text: $messages)
TextField("Messages seperate with |", text: $messages, axis: .vertical)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
@ -124,9 +124,7 @@ struct CannedMessagesConfig: View {
ForEach(0..<40) {
if $0 == 0 {
Text("Unset")
Text("unset")
} else {
Text("Pin \($0)")
@ -141,9 +139,7 @@ struct CannedMessagesConfig: View {
ForEach(0..<40) {
if $0 == 0 {
Text("Unset")
Text("unset")
} else {
Text("Pin \($0)")
@ -158,9 +154,7 @@ struct CannedMessagesConfig: View {
ForEach(0..<40) {
if $0 == 0 {
Text("Unset")
Text("unset")
} else {
Text("Pin \($0)")
@ -224,8 +218,9 @@ struct CannedMessagesConfig: View {
isPresented: $isPresentingSaveConfirm,
titleVisibility: .visible
) {
Button("Save Canned Messages Module Config to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") {
let nodeName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : NSLocalizedString("unknown", comment: "Unknown")
let buttonText = String.localizedStringWithFormat(NSLocalizedString("save.config %@", comment: "Save Config for %@"), nodeName)
Button(buttonText) {
if hasChanges {
var cmc = ModuleConfig.CannedMessageConfig()
cmc.enabled = enabled
@ -261,10 +256,17 @@ struct CannedMessagesConfig: View {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
hasMessagesChanges = false
if !hasChanges {
bleManager.sendWantConfig()
goBack()
}
}
}
}
}
message: {
Text("config.save.confirm")
}
.navigationTitle("canned.messages.config")
.navigationBarItems(trailing:
ZStack {
@ -282,7 +284,9 @@ struct CannedMessagesConfig: View {
self.inputbrokerEventCw = Int(node?.cannedMessageConfig?.inputbrokerEventCw ?? 0)
self.inputbrokerEventCcw = Int(node?.cannedMessageConfig?.inputbrokerEventCcw ?? 0)
self.inputbrokerEventPress = Int(node?.cannedMessageConfig?.inputbrokerEventPress ?? 0)
self.messages = node?.cannedMessageConfig?.messages ?? ""
self.hasChanges = false
self.hasMessagesChanges = false
}
.onChange(of: configPreset) { newPreset in

View file

@ -6,49 +6,6 @@
//
import SwiftUI
enum OutputIntervals: Int, CaseIterable, Identifiable {
case unset = 0
case oneSecond = 1000
case twoSeconds = 2000
case threeSeconds = 3000
case fourSeconds = 4000
case fiveSeconds = 5000
case tenSeconds = 10000
case fifteenSeconds = 15000
case thirtySeconds = 30000
case oneMinute = 60000
var id: Int { self.rawValue }
var description: String {
get {
switch self {
case .unset:
return "Unset"
case .oneSecond:
return "One Second"
case .twoSeconds:
return "Two Seconds"
case .threeSeconds:
return "Three Seconds"
case .fourSeconds:
return "Four Seconds"
case .fiveSeconds:
return "Five Seconds"
case .tenSeconds:
return "Ten Seconds"
case .fifteenSeconds:
return "Fifteen Seconds"
case .thirtySeconds:
return "Thirty Seconds"
case .oneMinute:
return "One Minute"
}
}
}
}
struct ExternalNotificationConfig: View {
@Environment(\.managedObjectContext) var context
@ -97,8 +54,12 @@ struct ExternalNotificationConfig: View {
Text("Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead.")
.font(.caption)
}
if !usePWM {
Section(header: Text("Primary GPIO")) {
Section(header: Text("Advanced GPIO Options")) {
Section(header: Text("Primary GPIO")
.font(.caption)
.foregroundColor(.gray)
.textCase(.uppercase))
{
Toggle(isOn: $active) {
Label("Active", systemImage: "togglepower")
}
@ -108,7 +69,7 @@ struct ExternalNotificationConfig: View {
Picker("Output pin GPIO", selection: $output) {
ForEach(0..<40) {
if $0 == 0 {
Text("Unset")
Text("unset")
} else {
Text("Pin \($0)")
}
@ -133,7 +94,11 @@ struct ExternalNotificationConfig: View {
.font(.caption)
}
Section(header: Text("Optional GPIO")) {
Section(header: Text("Optional GPIO")
.font(.caption)
.foregroundColor(.gray)
.textCase(.uppercase))
{
Toggle(isOn: $alertBellBuzzer) {
Label("Alert GPIO buzzer when receiving a bell", systemImage: "bell")
}
@ -153,7 +118,7 @@ struct ExternalNotificationConfig: View {
Picker("Output pin buzzer GPIO ", selection: $outputBuzzer) {
ForEach(0..<40) {
if $0 == 0 {
Text("Unset")
Text("unset")
} else {
Text("Pin \($0)")
}
@ -163,7 +128,7 @@ struct ExternalNotificationConfig: View {
Picker("Output pin vibra GPIO", selection: $outputVibra) {
ForEach(0..<40) {
if $0 == 0 {
Text("Unset")
Text("unset")
} else {
Text("Pin \($0)")
}
@ -189,7 +154,9 @@ struct ExternalNotificationConfig: View {
isPresented: $isPresentingSaveConfirm,
titleVisibility: .visible
) {
Button("Save External Notification Module Config to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") {
let nodeName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : NSLocalizedString("unknown", comment: "Unknown")
let buttonText = String.localizedStringWithFormat(NSLocalizedString("save.config %@", comment: "Save Config for %@"), nodeName)
Button(buttonText) {
var enc = ModuleConfig.ExternalNotificationConfig()
enc.enabled = enabled
enc.alertBell = alertBell
@ -213,6 +180,9 @@ struct ExternalNotificationConfig: View {
}
}
}
message: {
Text("config.save.confirm")
}
.navigationTitle("external.notification.config")
.navigationBarItems(trailing:
ZStack {

View file

@ -22,158 +22,149 @@ struct MQTTConfig: View {
@State var jsonEnabled = false
var body: some View {
VStack {
Form {
Section(header: Text("options")) {
Form {
Section(header: Text("options")) {
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "dot.radiowaves.right")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $encryptionEnabled) {
Label("Encryption Enabled", systemImage: "lock.icloud")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $jsonEnabled) {
Label("JSON Enabled", systemImage: "ellipsis.curlybraces")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
Section(header: Text("Custom Server")) {
HStack {
Label("Address", systemImage: "server.rack")
TextField("Server Address", text: $address)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: address, perform: { value in
let totalBytes = address.utf8.count
// Only mess with the value if it is too big
if totalBytes > 30 {
let firstNBytes = Data(username.utf8.prefix(30))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the shortName back to the last place where it was the right size
address = maxBytesString
}
}
hasChanges = true
})
.foregroundColor(.gray)
.keyboardType(.default)
}
.autocorrectionDisabled()
HStack {
Label("mqtt.username", systemImage: "person.text.rectangle")
TextField("mqtt.username", text: $username)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: username, perform: { value in
let totalBytes = username.utf8.count
// Only mess with the value if it is too big
if totalBytes > 62 {
let firstNBytes = Data(username.utf8.prefix(62))
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "dot.radiowaves.right")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $encryptionEnabled) {
Label("Encryption Enabled", systemImage: "lock.icloud")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $jsonEnabled) {
Label("JSON Enabled", systemImage: "ellipsis.curlybraces")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the shortName back to the last place where it was the right size
username = maxBytesString
}
}
hasChanges = true
})
.foregroundColor(.gray)
}
Section(header: Text("Custom Server")) {
HStack {
Label("Address", systemImage: "server.rack")
TextField("Server Address", text: $address)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: address, perform: { value in
.keyboardType(.default)
.scrollDismissesKeyboard(.interactively)
HStack {
Label("password", systemImage: "wallet.pass")
TextField("password", text: $password)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: password, perform: { value in
let totalBytes = address.utf8.count
// Only mess with the value if it is too big
if totalBytes > 30 {
let firstNBytes = Data(username.utf8.prefix(30))
let totalBytes = password.utf8.count
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the shortName back to the last place where it was the right size
address = maxBytesString
}
// Only mess with the value if it is too big
if totalBytes > 62 {
let firstNBytes = Data(password.utf8.prefix(62))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the shortName back to the last place where it was the right size
password = maxBytesString
}
hasChanges = true
})
.foregroundColor(.gray)
.keyboardType(.default)
}
.autocorrectionDisabled()
HStack {
Label("mqtt.username", systemImage: "person.text.rectangle")
TextField("mqtt.username", text: $username)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: username, perform: { value in
let totalBytes = username.utf8.count
// Only mess with the value if it is too big
if totalBytes > 62 {
let firstNBytes = Data(username.utf8.prefix(62))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the shortName back to the last place where it was the right size
username = maxBytesString
}
}
hasChanges = true
})
.foregroundColor(.gray)
}
.keyboardType(.default)
.scrollDismissesKeyboard(.interactively)
HStack {
Label("password", systemImage: "wallet.pass")
TextField("password", text: $password)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: password, perform: { value in
let totalBytes = password.utf8.count
// Only mess with the value if it is too big
if totalBytes > 62 {
let firstNBytes = Data(password.utf8.prefix(62))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the shortName back to the last place where it was the right size
password = maxBytesString
}
}
hasChanges = true
})
.foregroundColor(.gray)
}
.keyboardType(.default)
.scrollDismissesKeyboard(.interactively)
}
hasChanges = true
})
.foregroundColor(.gray)
}
Text("WiFi or Ethernet must also be enabled for MQTT to work. You can set uplink and downlink for each channel.")
.font(.callout)
.keyboardType(.default)
.scrollDismissesKeyboard(.interactively)
}
.scrollDismissesKeyboard(.interactively)
.disabled(!(node != 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
) {
Button("Save MQTT Config to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") {
var mqtt = ModuleConfig.MQTTConfig()
mqtt.enabled = self.enabled
mqtt.address = self.address
mqtt.username = self.username
mqtt.password = self.password
mqtt.encryptionEnabled = self.encryptionEnabled
mqtt.jsonEnabled = self.jsonEnabled
let adminMessageId = bleManager.saveMQTTConfig(config: mqtt, fromUser: node!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
hasChanges = false
goBack()
}
Text("WiFi or Ethernet must also be enabled for MQTT to work. You can set uplink and downlink for each channel.")
.font(.callout)
}
.scrollDismissesKeyboard(.interactively)
.disabled(!(node != 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 nodeName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : NSLocalizedString("unknown", comment: "Unknown")
let buttonText = String.localizedStringWithFormat(NSLocalizedString("save.config %@", comment: "Save Config for %@"), nodeName)
Button(buttonText) {
var mqtt = ModuleConfig.MQTTConfig()
mqtt.enabled = self.enabled
mqtt.address = self.address
mqtt.username = self.username
mqtt.password = self.password
mqtt.encryptionEnabled = self.encryptionEnabled
mqtt.jsonEnabled = self.jsonEnabled
let adminMessageId = bleManager.saveMQTTConfig(config: mqtt, fromUser: node!.user!, toUser: node!.user!)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
hasChanges = false
goBack()
}
}
}
message: {
Text("config.save.confirm")
}
.navigationTitle("mqtt.config")
.navigationBarItems(trailing:
ZStack {
@ -190,17 +181,17 @@ struct MQTTConfig: View {
self.hasChanges = false
}
.onChange(of: enabled) { newEnabled in
if node != nil && node!.mqttConfig != nil {
if node != nil && node?.mqttConfig != nil {
if newEnabled != node!.mqttConfig!.enabled { hasChanges = true }
}
}
.onChange(of: encryptionEnabled) { newEncryptionEnabled in
if node != nil && node!.mqttConfig != nil {
if node != nil && node?.mqttConfig != nil {
if newEncryptionEnabled != node!.mqttConfig!.encryptionEnabled { hasChanges = true }
}
}
.onChange(of: jsonEnabled) { newJsonEnabled in
if node != nil && node!.mqttConfig != nil {
if node != nil && node?.mqttConfig != nil {
if newJsonEnabled != node!.mqttConfig!.jsonEnabled { hasChanges = true }
}
}

View file

@ -6,44 +6,6 @@
//
import SwiftUI
// Default of 0 is off
enum SenderIntervals: Int, CaseIterable, Identifiable {
case off = 0
case fifteenSeconds = 15
case thirtySeconds = 30
case oneMinute = 60
case fiveMinutes = 300
case tenMinutes = 600
case fifteenMinutes = 900
case thirtyMinutes = 1800
var id: Int { self.rawValue }
var description: String {
get {
switch self {
case .off:
return "Off"
case .fifteenSeconds:
return "Fifteen Seconds"
case .thirtySeconds:
return "Thirty Seconds"
case .oneMinute:
return "One Minute"
case .fiveMinutes:
return "Five Minutes"
case .tenMinutes:
return "Ten Minutes"
case .fifteenMinutes:
return "Fifteen Minutes"
case .thirtyMinutes:
return "Thirty Minutes"
}
}
}
}
struct RangeTestConfig: View {
@Environment(\.managedObjectContext) var context
@ -100,7 +62,9 @@ struct RangeTestConfig: View {
isPresented: $isPresentingSaveConfirm,
titleVisibility: .visible
) {
Button("Save Range Test Module Config to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") {
let nodeName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : NSLocalizedString("unknown", comment: "Unknown")
let buttonText = String.localizedStringWithFormat(NSLocalizedString("save.config %@", comment: "Save Config for %@"), nodeName)
Button(buttonText) {
var rtc = ModuleConfig.RangeTestConfig()
rtc.enabled = enabled
rtc.save = save
@ -114,6 +78,9 @@ struct RangeTestConfig: View {
}
}
}
message: {
Text("config.save.confirm")
}
.navigationTitle("range.test.config")
.navigationBarItems(trailing:
ZStack {

View file

@ -74,13 +74,9 @@ struct SerialConfig: View {
Picker("Receive data (rxd) GPIO pin", selection: $rxd) {
ForEach(0..<40) {
if $0 == 0 {
Text("Unset")
Text("unset")
} else {
Text("Pin \($0)")
}
}
@ -89,13 +85,9 @@ struct SerialConfig: View {
Picker("Transmit data (txd) GPIO pin", selection: $txd) {
ForEach(0..<40) {
if $0 == 0 {
Text("Unset")
Text("unset")
} else {
Text("Pin \($0)")
}
}
@ -126,8 +118,9 @@ struct SerialConfig: View {
isPresented: $isPresentingSaveConfirm,
titleVisibility: .visible
) {
Button("Save Serial Module Config to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") {
let nodeName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : NSLocalizedString("unknown", comment: "Unknown")
let buttonText = String.localizedStringWithFormat(NSLocalizedString("save.config %@", comment: "Save Config for %@"), nodeName)
Button(buttonText) {
var sc = ModuleConfig.SerialConfig()
sc.enabled = enabled
sc.echo = echo
@ -147,7 +140,9 @@ struct SerialConfig: View {
}
}
}
message: {
Text("config.save.confirm")
}
.navigationTitle("serial.config")
.navigationBarItems(trailing:

View file

@ -6,66 +6,6 @@
//
import SwiftUI
enum UpdateIntervals: Int, CaseIterable, Identifiable {
case fifteenSeconds = 15
case thirtySeconds = 30
case oneMinute = 60
case fiveMinutes = 300
case tenMinutes = 600
case fifteenMinutes = 900
case thirtyMinutes = 1800
case oneHour = 3600
case twoHours = 7200
case threeHours = 10800
case fourHours = 14400
case fiveHours = 18000
case sixHours = 21600
case twelveHours = 43200
case eighteenHours = 64800
case twentyFourHours = 86400
var id: Int { self.rawValue }
var description: String {
get {
switch self {
case .fifteenSeconds:
return "Fifteen Seconds"
case .thirtySeconds:
return "Thirty Seconds"
case .oneMinute:
return "One Minute"
case .fiveMinutes:
return "Five Minutes"
case .tenMinutes:
return "Ten Minutes"
case .fifteenMinutes:
return "Fifteen Minutes"
case .thirtyMinutes:
return "Thirty Minutes"
case .oneHour:
return "One Hour"
case .twoHours:
return "Two Hours"
case .threeHours:
return "Three Hours"
case .fourHours:
return "Four Hours"
case .fiveHours:
return "Five Hours"
case .sixHours:
return "Six Hours"
case .twelveHours:
return "Twelve Hours"
case .eighteenHours:
return "Eighteen Hours"
case .twentyFourHours:
return "Twenty Four Hours"
}
}
}
}
struct TelemetryConfig: View {
@Environment(\.managedObjectContext) var context
@ -137,7 +77,9 @@ struct TelemetryConfig: View {
isPresented: $isPresentingSaveConfirm,
titleVisibility: .visible
) {
Button("Save Telemetry Module Config to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") {
let nodeName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : NSLocalizedString("unknown", comment: "Unknown")
let buttonText = String.localizedStringWithFormat(NSLocalizedString("save.config %@", comment: "Save Config for %@"), nodeName)
Button(buttonText) {
var tc = ModuleConfig.TelemetryConfig()
tc.deviceUpdateInterval = UInt32(deviceUpdateInterval)
tc.environmentUpdateInterval = UInt32(environmentUpdateInterval)
@ -153,7 +95,9 @@ struct TelemetryConfig: View {
}
}
}
message: {
Text("config.save.confirm")
}
.navigationTitle("telemetry.config")
.navigationBarItems(trailing:
ZStack {

View file

@ -104,17 +104,19 @@ struct NetworkConfig: View {
.controlSize(.large)
.padding()
.confirmationDialog(
"Are you sure you want to save?",
"are.you.sure",
isPresented: $isPresentingSaveConfirm,
titleVisibility: .visible
) {
Button("Save Config for \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")") {
let nodeName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : NSLocalizedString("unknown", comment: "Unknown")
let buttonText = String.localizedStringWithFormat(NSLocalizedString("save.config %@", comment: "Save Config for %@"), nodeName)
Button(buttonText) {
var network = Config.NetworkConfig()
network.wifiEnabled = self.wifiEnabled
network.wifiSsid = self.wifiSsid
network.wifiPsk = self.wifiPsk
network.ethEnabled = self.ethEnabled
network.ethMode = Config.NetworkConfig.EthMode.dhcp
//network.addressMode = Config.NetworkConfig.AddressMode.dhcp
let adminMessageId = bleManager.saveWiFiConfig(config: network, fromUser: node!.user!, toUser: node!.user!)
if adminMessageId > 0 {
@ -125,7 +127,7 @@ struct NetworkConfig: View {
}
}
} message: {
Text("After network config saves the node will reboot.")
Text("config.save.confirm")
}
}
.navigationTitle("network.config")

View file

@ -113,7 +113,7 @@ struct PositionConfig: View {
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Picker("Position Broadcast Interval", selection: $positionBroadcastSeconds) {
ForEach(PositionBroadcastIntervals.allCases) { at in
ForEach(UpdateIntervals.allCases) { at in
Text(at.description)
}
}
@ -155,7 +155,7 @@ struct PositionConfig: View {
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $includeTimestamp) { //128
Label("Timestamp", systemImage: "clock")
Label("timestamp", systemImage: "clock")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
@ -205,7 +205,9 @@ struct PositionConfig: View {
isPresented: $isPresentingSaveConfirm,
titleVisibility: .visible
) {
Button("Save Position Config to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") {
let nodeName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : NSLocalizedString("unknown", comment: "Unknown")
let buttonText = String.localizedStringWithFormat(NSLocalizedString("save.config %@", comment: "Save Config for %@"), nodeName)
Button(buttonText) {
if fixedPosition {
_ = bleManager.sendPosition(destNum: bleManager.connectedPeripheral.num, wantResponse: false)
@ -239,6 +241,9 @@ struct PositionConfig: View {
}
}
}
message: {
Text("config.save.confirm")
}
}
.navigationTitle("position.config")
.navigationBarItems(trailing:

View file

@ -10,40 +10,32 @@ struct MeshLog: View {
@State private var document: LogDocument = LogDocument(logFile: "MESHTASTIC MESH ACTIVITY LOG\n")
var body: some View {
List(logs, id: \.self, rowContent: Text.init)
.task {
do {
let url = logFile!
logs.removeAll()
var lineCount = 0
let lineLimit = 500
// Get the number of lines
for try await _ in url.lines {
lineCount += 1
}
// Set the record to start with if we have more lines than the limit
var startingLog = 0
if lineCount > lineLimit {
startingLog = lineCount - lineLimit
}
var lineNumber = 0
for try await log in url.lines {
if lineNumber >= startingLog {
logs.append(log)
document.logFile.append("\(log) \n")
}
lineNumber += 1
}
logs.reverse()
} catch {
// Stop adding logs when an error is thrown
}
@ -54,7 +46,6 @@ struct MeshLog: View {
contentType: UTType.plainText,
defaultFilename: "mesh-activity-log",
onCompletion: { result in
if case .success = result {
print("Mesh activity log download: success.")
} else {
@ -62,15 +53,12 @@ struct MeshLog: View {
}
}
)
.textSelection(.enabled)
.font(.caption)
HStack(alignment: .center) {
Spacer()
Button(role: .destructive) {
let text = ""
do {
try text.write(to: logFile!, atomically: false, encoding: .utf8)
@ -78,35 +66,27 @@ struct MeshLog: View {
} catch {
print(error)
}
} label: {
Label("Clear Log", systemImage: "trash.fill")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
Spacer()
Button {
isExporting = true
} label: {
Label("Save Log", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
Spacer()
}
.padding(.bottom, 10)
.navigationTitle("Mesh Activity Log")
.navigationTitle("mesh.log")
}
}

View file

@ -44,7 +44,6 @@ struct Settings: View {
Image(systemName: "person.crop.rectangle.fill")
.symbolRenderingMode(.hierarchical)
Text("user")
}
@ -55,20 +54,19 @@ struct Settings: View {
Image(systemName: "dot.radiowaves.left.and.right")
.symbolRenderingMode(.hierarchical)
Text("lora")
}
// NavigationLink() {
//
// Channels(node: nodes.first(where: { $0.num == connectedNodeNum }))
// } label: {
//
// Image(systemName: "fibrechannel")
// .symbolRenderingMode(.hierarchical)
//
// Text("channels")
// }
NavigationLink() {
Channels(node: nodes.first(where: { $0.num == connectedNodeNum }))
} label: {
Image(systemName: "fibrechannel")
.symbolRenderingMode(.hierarchical)
Text("channels")
}
NavigationLink() {
@ -179,15 +177,10 @@ struct Settings: View {
Text("admin.log")
}
}
Section(header: Text("about")) {
NavigationLink {
AboutMeshtastic()
} label: {
Image(systemName: "questionmark.app")
.symbolRenderingMode(.hierarchical)
@ -196,10 +189,8 @@ struct Settings: View {
}
}
.onAppear {
self.bleManager.context = context
self.bleManager.userSettings = userSettings
}
.listStyle(GroupedListStyle())
.navigationTitle("settings")

View file

@ -56,15 +56,15 @@ struct ShareChannels: View {
Grid() {
GridRow {
Spacer()
Text("Include")
Text("include")
.font(.caption)
.fontWeight(.bold)
.padding(.trailing)
Text("Channel")
Text("channel")
.font(.caption)
.fontWeight(.bold)
.padding(.trailing)
Text("Encrypted")
Text("encrypted")
.font(.caption)
.fontWeight(.bold)
}
@ -246,7 +246,7 @@ struct ShareChannels: View {
Button {
isPresentingHelp = false
} label: {
Label("Close", systemImage: "xmark")
Label("close", systemImage: "xmark")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
@ -254,7 +254,7 @@ struct ShareChannels: View {
.padding()
#endif
}
.navigationTitle("Generate QR Code")
.navigationTitle("generate.qr.code")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
ZStack {

View file

@ -80,7 +80,7 @@ struct UserConfig: View {
.controlSize(.large)
.padding()
.confirmationDialog(
"Are you sure you want to save?",
"are.you.sure",
isPresented: $isPresentingSaveConfirm,
titleVisibility: .visible
) {
@ -95,7 +95,7 @@ struct UserConfig: View {
}
}
} message: {
Text("After user config saves the node will reboot.")
Text("config.save.confirm")
}
}
Spacer()

View file

@ -8,43 +8,60 @@
"about"="Über";
"about.meshtastic"="Über Meshtastic";
"admin"="admin";
"admin.log"="Admin Message Log";
"admin.log"="Admin Log";
"ago"="her";
"airtime"="Airtime";
"always.on"="Immer an";
"app.settings"="App Einstellungen";
"are.you.sure"="Bist Du sicher?";
"ascii.capable"="ASCII fähig";
"available.radios"="Geräte in der Nähe";
"automatic.detection"="Automatische erkennung";
"ble.name"="BLE Name";
"battery.level"="Batterie Ladung";
"battery.level.trend"="Batterie Ladungstrend";
"ble.name"="BLE Name";"ble.connection.timeout %d %@"="Verbindung nach %d Versuchen zu %@ fehlgeschlagen. Evtl. hilft es, die Verbindung unter Einstellungen > Bluetooth manuell zu löschen.";
"ble.connection.timeout %d %@"="Verbindung nach %d Versuchen zu %@ fehlgeschlagen. Evtl. hilft es, die Verbindung unter Einstellungen > Bluetooth manuell zu löschen.";
"ble.errorcode.6 %@"="%@ Die App wird automatisch zum präferierten Gerät wiederverbinden, sobald es in Reichweite kommt.";
"ble.errorcode.14 %@"="%@ Dieser fehler kann üblicherweise behoben werden, in dem man unter Einstellungen > Bluetooth die Verbindung manuell löscht und sich erneut mit dem Gerät verbindet.";
"ble.errorcode.pin %@"="%@ Bitte versuche es erneut. achte sorgfältig auf die richtige PIN.";
"bluetooth"="Bluetooth";
"bluetooth.off"="Bluetooth ist aus";
"bluetooth.config"="Bluetooth Konfiguration";
"bluetooth.mode.randompin"="Zufällige PIN";
"bluetooth.mode.fixedpin"="Feste PIN";
"bluetooth.mode.nopin"="Keine PIN (geht einfach)";
"bluetooth.pairingmode"="Pairing Modus";
"bluetooth.pin.validation"="Die Bluetooth Pin muss 6 Stellen lang sein.";
"bytes"="Bytes";
"cancel"="Abbrechen";
"canned.messages"="Canned Messages";
"canned.messages.config"="Canned Messages Config";
"canned.messages.preset.manual"="Manualle Konfiguration";
"canned.messages.preset.manual"="Manuelle Konfiguration";
"canned.messages.preset.rakrotary"="RAK Drehimpulsgeber Modul";
"canned.messages.preset.cardkb"="M5 Stack Card KB / RAK Tastenfeld";
"channel"="Kanal";
"channel.role.disabled"="Deaktiviert";
"channel.role.primary"="Primär";
"channel.role.secondary"="Sekundär";
"channel.utilization"="Kanalbelegung";
"channels"="Kanäle";
"clear.app.data"="App Daten löschen";
"clear.log"="Log löschen";
"close"="Schließen";
"config.save.confirm"="Nach dem ändern der Einstellungen wird das Gerät neu starten.";
"connected.radio"="Verbundenes Gerät";
"communicating"="Verbinde mit Gerät...";
"connected"="Derzeit verbunden";
"connecting"="Verbinde...";
"contacts"="Kontakte";
"copy"="Kopieren";
"current"="Current";
"default"="Standard";
"delete"="Löschen";
"device"="Gerät";
"device.config"="Gerätekonfiguration";
"device.metrics.delete"="Delete all device metrics?";
"device.metrics.log"="Device Metrics Log";
"device.role.client"="Client (Standard) - Mit App verbundener Client.";
"device.role.clientmute"="Client Leise - Das selbe wie Client, außer das die Pakete nicht über diesen Node weitergeleitet werden. Nimmt nicht am Mesh-Routing teil.";
"device.role.router"="Router - Mesh Pakete werden bevorzugt über diesen Node gerouted. Dieser Node wird nicht von einer Client App benutzt. WLAN, Bluetooth und Display sind aus.";
@ -58,9 +75,14 @@
"echo"="Echo";
"email.address"="Email Adresse";
"enabled"="Aktiviert";
"encrypted"="Verschlüsselt";
"external.notification"="Externe Benachrichtigung";
"external.notification.config"="Einstellungen der externen Benachrichtigung";
"firmware.version"="Firmware Version";
"firmware.version.unsupported"="Nicht unterstützte Firmware Version erkannt. Kann nicht verbinden.";
"gas"="Gas";
"gas.resistance"="Gas Resistance";
"generate.qr.code"="QR Code Erzeugen";
"gpsformat.dec"="Dezimalgrad Format";
"gpsformat.dms"="Grad Minuten Sekunden";
"gpsformat.utm"="Universal Transversal Mercator";
@ -70,6 +92,7 @@
"heard"="Gehört";
"heard.last"="Zuletzt gehört";
"hybrid"="Hybrid";
"include"="Include";
"inputevent.none"="Keins";
"inputevent.up"="Hoch";
"inputevent.down"="Runter";
@ -80,6 +103,8 @@
"inputevent.cancel"="Abbrechen";
"interval.one.second"="Eine Sekunde";
"interval.two.seconds"="Zwei Sekunden";
"interval.three.seconds"="Three Seconds";
"interval.four.seconds"="Four Seconds";
"interval.five.seconds"="Fünf Sekunden";
"interval.ten.seconds"="Zehn Sekunden";
"interval.fifteen.seconds"="Fünfzehn Sekunden";
@ -93,9 +118,17 @@
"interval.fifteen.minutes"="Fünfzehn Minutes";
"interval.thirty.minutes"="Dreißig Minutes";
"interval.one.hour"="Eine Stunde";
"interval.two.hours"="Two Hours";
"interval.three.hours"="Three Hours";
"interval.four.hours"="Four Hours";
"interval.five.hours"="Five Hours";
"interval.six.hours"="Sechs Stunden";
"interval.twelve.hours"="Zwölf Stunden";
"interval.eighteen.hours"="Eighteen Hours";
"interval.twentyfour.hours"="Vierundzwanzig Stunden";
"interval.thirtysix.hours"="Thirty Six Hours";
"interval.fortyeight.hours"="Forty Eight Hours Hours";
"interval.seventytwo.hours"="Seventy Two Hours";
"keyboard.type"="Keyboard Typ";
"logging"="Logging";
"lora"="LoRa";
@ -103,6 +136,38 @@
"map"="Mesh Karte";
"map.type"="kartentyp";
"mesh.log"="Mesh Log";
"mesh.log.bluetooth.config %@"="Bluetooth Konfiguration empfangen: %@";
"mesh.log.cannedmessage.config %@"="Canned Message module config received: %@";
"mesh.log.cannedmessages.messages.get %@"="Requested Canned Messages Module Messages for node: %@";
"mesh.log.cannedmessages.messages.received %@"="Canned Messages Messages Received For: %@";
"mesh.log.channel.sent %@ %d"="Sent a Channel for: %@ Channel Index %d";
"mesh.log.channel.received %d %@"="Channel %d received from: %@";
"mesh.log.device.config %@"="Geräte Konfiguration empfangen: %@";
"mesh.log.display.config %@"="Display Konfiguration empfangen: %@";
"mesh.log.devicemetadata %@"="Anforderung der Geräte Metadaten für %@";
"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: %@";
"mesh.log.mqtt.config %@"="MQTT module config received: %@";
"mesh.log.myinfo %@"="MyInfo received: %@";
"mesh.log.network.config %@"="Netzwerk onfiguration empfangen: %@";
"mesh.log.nodeinfo.received %@"="Node info empfangen für: %@";
"mesh.log.position.config %@"="Positions Konfiguration empfangen: %@";
"mesh.log.position.received %@"="Positionspaket empfangen von Node: %@";
"mesh.log.rangetest.config %@"="Range Test Modul konfiguration empfangen: %@";
"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.telemetry.config %@"="Telemetrie Modul Konfiguration empfangen: %@";
"mesh.log.telemetry.received %@"="Telemetrie empfangen für: %@";
"mesh.log.textmessage.received"="Nachricht von der Textnachricht-App empfangen.";
"mesh.log.textmessage.send.failed %@"="Nachricht senden fehlgeschlagen. Nicht korrekt verbunden zu %@";
"mesh.log.textmessage.sent %@ %@ %@"="Sende Nachricht %@ von %@ an %@";
"mesh.log.traceroute.sent %@"="Sende Traceroute Anforderung zu Mode: %@";
"mesh.log.traceroute.received.direct %@"="Traceroute Anforderung an node gesendet: %@ wurde direkt empfangen.";
"mesh.log.traceroute.received.route %@"="Traceroute Ergebnis: %@";
"mesh.log.wantconfig %@"="Issuing Want Config to %@";
"mesh.log.waypoint.sent %@"="Sent a Waypoint Packet from: %@";
"message"="Nachricht";
"message.details"="Nachrichtendetails";
"messages"="Nachrichten";
@ -131,8 +196,11 @@
"radio.configuration"="Geräteeinstellungen";
"range.test"="Entfernungstest";
"range.test.config"="Entfernungstest Konfiguration";
"reboot"="Reboot";
"reboot.node"="Node neustarten?";
"reply"="Antworten";
"received.ack"="Empfangsbestätigung";
"received.ack.real"="Recipient Ack";
"routing.acknowledged"="Bestätigt";
"routing.noroute"="Keine Route";
"routing.gotnak"="Negative Empfangsbestätigung empfangen";
@ -147,6 +215,7 @@
"routing.notauthorized"="Nicht authorisiert";
"satellite"="Satellit";
"save"="Speichern";
"save.config %@"="Save Config for %@";
"serial"="Serial";
"serial.config"="Serial Konfiguration";
"serial.mode.default"="Standard";
@ -175,9 +244,14 @@
"telemetry"="Telemetrie (Sensoren)";
"telemetry.config"="Telemetrie Einstellungen";
"timeout"="Zeitlimit erreicht";
"timestamp"="Timestamp";
"twitter"="Twitter";
"unknown"="Unknown";
"unknown.age"="Unbekanntes alter";
"unset"="Unset";
"update.firmware"="Update Your Firmware";
"update.interval"="Update intervall";
"user"="Benutzer";
"user.details"="Benutzer Details";
"voltage"="Voltage";
"waiting"="Warte...";

View file

@ -10,18 +10,28 @@
"admin"="Admin";
"admin.log"="Admin Message Log";
"ago"="ago";
"airtime"="Airtime";
"always.on"="Always On";
"app.settings"="App Settings";
"are.you.sure"="Are you sure?";
"ascii.capable"="ASCII Capable";
"available.radios"="Available Radios";
"automatic.detection"="Automatic Detection";
"battery.level"="Battery Level";
"battery.level.trend"="Battery Level Trend";
"ble.name"="BLE Name";
"ble.connection.timeout %d %@"="Connection failed after %d attempts to connect to %@. You may need to forget your device under Settings > Bluetooth.";
"ble.errorcode.6 %@"="%@ The app will automatically reconnect to the preferred radio if it come back in range.";
"ble.errorcode.14 %@"="%@ This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re-connecting to the radio.";
"ble.errorcode.pin %@"="%@ Please try connecting again and check the PIN carefully.";
"bluetooth"="Bluetooth";
"bluetooth.off"="Bluetooth is off";
"bluetooth.config"="Bluetooth Config";
"bluetooth.mode.randompin"="Random PIN";
"bluetooth.mode.fixedpin"="Fixed PIN";
"bluetooth.mode.nopin"="No PIN (Just Works)";
"bluetooth.pairingmode"="Pairing Mode";
"bluetooth.pin.validation"="BLE Pin must be 6 digits long.";
"bytes"="Bytes";
"cancel"="Cancel";
"canned.messages"="Canned Messages";
@ -33,18 +43,25 @@
"channel.role.disabled"="Disabled";
"channel.role.primary"="Primary";
"channel.role.secondary"="Secondary";
"channel.utilization"="Channel Utilization";
"channels"="Channels";
"clear.app.data"="Clear App Data";
"clear.log"="Clear Log";
"close"="Close";
"config.save.confirm"="After config values save the node will reboot.";
"connected.radio"="Connected Radio";
"communicating"="Communicating with device. .";
"connected"="Currently Connected";
"connecting"="Connecting . .";
"contacts"="Contacts";
"copy"="Copy";
"current"="Current";
"default"="Default";
"delete"="Delete";
"device"="Device";
"device.config"="Device Config";
"device.metrics.delete"="Delete all device metrics?";
"device.metrics.log"="Device Metrics Log";
"device.role.client"="Client (default) - App connected client.";
"device.role.clientmute"="Client Mute - Same as a client except packets will not hop over this node, does not contribute to routing packets for mesh.";
"device.role.router"="Router - Mesh packets will prefer to be routed over this node. This node will not be used by client apps. The wifi/ble radios and the oled screen will be put to sleep.";
@ -58,9 +75,14 @@
"echo"="Echo";
"email.address"="Email Address";
"enabled"="Enabled";
"encrypted"="Encrypted";
"external.notification"="External Notification";
"external.notification.config"="External Notification Config";
"firmware.version"="Firmware Version";
"firmware.version.unsupported"="Unsupported Firmware Version Detected, unable to connect to device.";
"gas"="Gas";
"gas.resistance"="Gas Resistance";
"generate.qr.code"="Generate QR Code";
"gpsformat.dec"="Decimal Degrees Format";
"gpsformat.dms"="Degrees Minutes Seconds";
"gpsformat.utm"="Universal Transverse Mercator";
@ -70,6 +92,7 @@
"heard"="Heard";
"heard.last"="Last Heard";
"hybrid"="Hybrid";
"include"="Include";
"inputevent.none"="None";
"inputevent.up"="Up";
"inputevent.down"="Down";
@ -80,6 +103,8 @@
"inputevent.cancel"="Cancel";
"interval.one.second"="One Second";
"interval.two.seconds"="Two Seconds";
"interval.three.seconds"="Three Seconds";
"interval.four.seconds"="Four Seconds";
"interval.five.seconds"="Five Seconds";
"interval.ten.seconds"="Ten Seconds";
"interval.fifteen.seconds"="Fifteen Seconds";
@ -93,9 +118,17 @@
"interval.fifteen.minutes"="Fifteen Minutes";
"interval.thirty.minutes"="Thirty Minutes";
"interval.one.hour"="One Hour";
"interval.two.hours"="Two Hours";
"interval.three.hours"="Three Hours";
"interval.four.hours"="Four Hours";
"interval.five.hours"="Five Hours";
"interval.six.hours"="Six Hours";
"interval.twelve.hours"="Twelve Hours";
"interval.eighteen.hours"="Eighteen Hours";
"interval.twentyfour.hours"="Twenty Four Hours";
"interval.thirtysix.hours"="Thirty Six Hours";
"interval.fortyeight.hours"="Forty Eight Hours Hours";
"interval.seventytwo.hours"="Seventy Two Hours";
"keyboard.type"="Keyboard Type";
"logging"="Logging";
"lora"="LoRa";
@ -103,6 +136,38 @@
"map"="Mesh Map";
"map.type"="Map Type";
"mesh.log"="Mesh Log";
"mesh.log.bluetooth.config %@"="Bluetooth config received: %@";
"mesh.log.cannedmessage.config %@"="Canned Message module config received: %@";
"mesh.log.cannedmessages.messages.get %@"="Requested Canned Messages Module Messages for node: %@";
"mesh.log.cannedmessages.messages.received %@"="Canned Messages Messages Received For: %@";
"mesh.log.channel.sent %@ %d"="Sent a Channel for: %@ Channel Index %d";
"mesh.log.channel.received %d %@"="Channel %d received from: %@";
"mesh.log.device.config %@"="Device config received: %@";
"mesh.log.display.config %@"="Display config received: %@";
"mesh.log.devicemetadata %@"="Requesting Device Metadata for %@";
"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: %@";
"mesh.log.mqtt.config %@"="MQTT module config received: %@";
"mesh.log.myinfo %@"="MyInfo received: %@";
"mesh.log.network.config %@"="Network config received: %@";
"mesh.log.nodeinfo.received %@"="Node info received for: %@";
"mesh.log.position.config %@"="Positon config received: %@";
"mesh.log.position.received %@"="Position Packet received from node: %@";
"mesh.log.rangetest.config %@"="Range Test module config received: %@";
"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.telemetry.config %@"="Telemetry module config received: %@";
"mesh.log.telemetry.received %@"="Telemetry received for: %@";
"mesh.log.textmessage.received"="Message received from the text message app.";
"mesh.log.textmessage.send.failed %@"="Message Send Failed, not properly connected to %@";
"mesh.log.textmessage.sent %@ %@ %@"="Sent message %@ from %@ to %@";
"mesh.log.traceroute.received.direct %@"="Trace Route request sent to node: %@ was recieived directly.";
"mesh.log.traceroute.received.route %@"="Trace Route request returned: %@";
"mesh.log.traceroute.sent %@"="Sent a Trace Route Request to node: %@";
"mesh.log.wantconfig %@"="Issuing Want Config to %@";
"mesh.log.waypoint.sent %@"="Sent a Waypoint Packet from: %@";
"message"="Message";
"message.details"="Message Details";
"messages"="Messages";
@ -132,7 +197,10 @@
"range.test"="Range Test";
"range.test.config"="Range Test Config";
"reply"="Reply";
"reboot"="Reboot";
"reboot.node"="Reboot node?";
"received.ack"="Received Ack";
"received.ack.real"="Recipient Ack";
"routing.acknowledged"="Acknowledged";
"routing.noroute"="No Route";
"routing.gotnak"="Received a negative acknowledgment";
@ -147,6 +215,7 @@
"routing.notauthorized"="Not Authorized";
"satellite"="Satellite";
"save"="Save";
"save.config %@"="Save Config for %@";
"serial"="Serial";
"serial.config"="Serial Config";
"serial.mode.default"="Default";
@ -174,10 +243,15 @@
"tapback.poop"="Poop";
"telemetry"="Telemetry (Sensors)";
"telemetry.config"="Telemetry Config";
"timeout"="timeout";
"timeout"="Timeout";
"timestamp"="Timestamp";
"twitter"="Twitter";
"unknown"="Unknown";
"unknown.age"="Unknown Age";
"unset"="Unset";
"update.firmware"="Update Your Firmware";
"update.interval"="Update Interval";
"user"="User";
"user.details"="User Details";
"voltage"="Voltage";
"waiting"="Waiting. . .";