diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 50e4621a..d28ae0a6 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -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 */ @@ -152,9 +153,9 @@ DD415827285859C4009B0E59 /* TelemetryConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryConfig.swift; sourceTree = ""; }; DD41582928585C32009B0E59 /* RangeTestConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RangeTestConfig.swift; sourceTree = ""; }; DD457187293C7E63000C49FB /* SignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalStrengthIndicator.swift; sourceTree = ""; }; + DD457BC4295D5E35004BCE4D /* MeshtasticDataModelV5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV5.xcdatamodel; sourceTree = ""; }; DD47E3CD26F103C600029299 /* NodeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeList.swift; sourceTree = ""; }; DD47E3D526F17ED900029299 /* CircleText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleText.swift; sourceTree = ""; }; - DD47E3D826F3093800029299 /* MessageBubble.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBubble.swift; sourceTree = ""; }; DD4A911D2708C65400501B7E /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; DD4C158B2824A91E0032668E /* module_config.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = module_config.pb.swift; sourceTree = ""; }; DD4C158D2824AA7E0032668E /* config.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = config.pb.swift; sourceTree = ""; }; @@ -188,6 +189,7 @@ DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationManager.swift; sourceTree = ""; }; DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticLogo.swift; sourceTree = ""; }; DD97E96728EFE9A00056DDA4 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = ""; }; + DD994B68295F88B60013760A /* IntervalEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalEnums.swift; sourceTree = ""; }; DDA0B6B1294CDC55001356EC /* Channels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channels.swift; sourceTree = ""; }; DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelRoles.swift; sourceTree = ""; }; DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshPackets.swift; sourceTree = ""; }; @@ -232,6 +234,7 @@ DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeshtasticTests.swift; sourceTree = ""; }; DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeText.swift; sourceTree = ""; }; DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntityExtension.swift; sourceTree = ""; }; + DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsEnums.swift; sourceTree = ""; }; DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = ""; }; DDF924C926FBB953009FE055 /* ConnectedDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -340,10 +343,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 = ""; @@ -361,6 +364,7 @@ DD8ED9C6289CE4A100B3B0AB /* Enums */ = { isa = PBXGroup; children = ( + DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */, DDB6ABD828B0A4BA00384BA1 /* BluetoothModes.swift */, DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */, DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */, @@ -372,6 +376,7 @@ DDB6ABE128B13FB500384BA1 /* PositionConfigEnums.swift */, DD8ED9C7289CE4B900B3B0AB /* RoutingError.swift */, DD1925B828CDA93900720036 /* SerialConfigEnums.swift */, + DD994B68295F88B60013760A /* IntervalEnums.swift */, ); path = Enums; sourceTree = ""; @@ -520,7 +525,6 @@ isa = PBXGroup; children = ( DD47E3D526F17ED900029299 /* CircleText.swift */, - DD47E3D826F3093800029299 /* MessageBubble.swift */, DDF924C926FBB953009FE055 /* ConnectedDevice.swift */, DDC3B273283F411B00AC321C /* LastHeardText.swift */, DDA6B2EA28420A7B003E8C16 /* NodeAnnotation.swift */, @@ -755,6 +759,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 */, @@ -795,8 +800,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 */, @@ -1001,7 +1006,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.9; + MARKETING_VERSION = 2.0.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1034,7 +1039,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.9; + MARKETING_VERSION = 2.0.10; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1210,12 +1215,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 = ""; diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift new file mode 100644 index 00000000..f94a9b6c --- /dev/null +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -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") + } + } + } +} diff --git a/Meshtastic/Enums/EthernetModes.swift b/Meshtastic/Enums/EthernetModes.swift index 2297cd04..352586bd 100644 --- a/Meshtastic/Enums/EthernetModes.swift +++ b/Meshtastic/Enums/EthernetModes.swift @@ -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 } } } diff --git a/Meshtastic/Enums/IntervalEnums.swift b/Meshtastic/Enums/IntervalEnums.swift new file mode 100644 index 00000000..5fc0dc20 --- /dev/null +++ b/Meshtastic/Enums/IntervalEnums.swift @@ -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") + } + } + } +} diff --git a/Meshtastic/Enums/PositionConfigEnums.swift b/Meshtastic/Enums/PositionConfigEnums.swift index 081420f0..f4d7f4fa 100644 --- a/Meshtastic/Enums/PositionConfigEnums.swift +++ b/Meshtastic/Enums/PositionConfigEnums.swift @@ -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") } } } diff --git a/Meshtastic/Enums/SerialConfigEnums.swift b/Meshtastic/Enums/SerialConfigEnums.swift index f6c4dded..38a54de9 100644 --- a/Meshtastic/Enums/SerialConfigEnums.swift +++ b/Meshtastic/Enums/SerialConfigEnums.swift @@ -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: diff --git a/Meshtastic/Export/WriteCsvFile.swift b/Meshtastic/Export/WriteCsvFile.swift index b0de58cc..31d08e9e 100644 --- a/Meshtastic/Export/WriteCsvFile.swift +++ b/Meshtastic/Export/WriteCsvFile.swift @@ -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 } diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 24cfae62..d51723b0 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -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).. 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).. 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).. 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).. 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).. 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).. 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).. 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).. = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) @@ -49,47 +50,40 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 do { let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] - // Found a node, save Device Config if !fetchedNode.isEmpty { - if fetchedNode[0].bluetoothConfig == nil { - let newBluetoothConfig = BluetoothConfigEntity(context: context) - newBluetoothConfig.enabled = config.bluetooth.enabled newBluetoothConfig.mode = Int32(config.bluetooth.mode.rawValue) newBluetoothConfig.fixedPin = Int32(config.bluetooth.fixedPin) - fetchedNode[0].bluetoothConfig = newBluetoothConfig - } else { - fetchedNode[0].bluetoothConfig?.enabled = config.bluetooth.enabled fetchedNode[0].bluetoothConfig?.mode = Int32(config.bluetooth.mode.rawValue) fetchedNode[0].bluetoothConfig?.fixedPin = Int32(config.bluetooth.fixedPin) } - do { try context.save() - MeshLogger.log("💾 Updated Bluetooth Config for node number: \(String(nodeNum))") + print("💾 Updated Bluetooth Config for node number: \(String(nodeNum))") } catch { context.rollback() let nsError = error as NSError - MeshLogger.log("💥 Error Updating Core Data BluetoothConfigEntity: \(nsError)") + print("💥 Error Updating Core Data BluetoothConfigEntity: \(nsError)") } } else { - MeshLogger.log("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Bluetooth Config") + print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Bluetooth Config") } } catch { let nsError = error as NSError - MeshLogger.log("💥 Fetching node for core data BluetoothConfigEntity failed: \(nsError)") + print("💥 Fetching node for core data BluetoothConfigEntity failed: \(nsError)") } } if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) { - MeshLogger.log("📟 Device config received: \(String(nodeNum))") + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.device.config %@", comment: "Device config received: %@"), String(nodeNum)) + MeshLogger.log("📟 \(logString)") let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) @@ -98,9 +92,7 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] // Found a node, save Device Config if !fetchedNode.isEmpty { - if fetchedNode[0].deviceConfig == nil { - let newDeviceConfig = DeviceConfigEntity(context: context) newDeviceConfig.role = Int32(config.device.role.rawValue) newDeviceConfig.serialEnabled = config.device.serialEnabled @@ -115,26 +107,25 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 fetchedNode[0].deviceConfig?.buttonGpio = Int32(config.device.buttonGpio) fetchedNode[0].deviceConfig?.buzzerGpio = Int32(config.device.buzzerGpio) } - do { try context.save() - MeshLogger.log("💾 Updated Device Config for node number: \(String(nodeNum))") + print("💾 Updated Device Config for node number: \(String(nodeNum))") } catch { context.rollback() let nsError = error as NSError - MeshLogger.log("💥 Error Updating Core Data DeviceConfigEntity: \(nsError)") + print("💥 Error Updating Core Data DeviceConfigEntity: \(nsError)") } } - } catch { let nsError = error as NSError - MeshLogger.log("💥 Fetching node for core data DeviceConfigEntity failed: \(nsError)") + print("💥 Fetching node for core data DeviceConfigEntity failed: \(nsError)") } } if config.payloadVariant == Config.OneOf_PayloadVariant.display(config.display) { - MeshLogger.log("🖥️ Display config received: \(String(nodeNum))") + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.display.config %@", comment: "Display config received: %@"), String(nodeNum)) + MeshLogger.log("🖥️ \(logString)") let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) @@ -170,7 +161,7 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 do { try context.save() - MeshLogger.log("💾 Updated Display Config for node number: \(String(nodeNum))") + print("💾 Updated Display Config for node number: \(String(nodeNum))") } catch { @@ -193,7 +184,8 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) { - MeshLogger.log("📻 LoRa config received: \(String(nodeNum))") + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.lora.config %@", comment: "LoRa config received: %@"), String(nodeNum)) + MeshLogger.log("📻 \(logString)") let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) @@ -201,14 +193,10 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 do { let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] - // Found a node, save LoRa Config if !fetchedNode.isEmpty { - if fetchedNode[0].loRaConfig == nil { - let newLoRaConfig = LoRaConfigEntity(context: context) - newLoRaConfig.regionCode = Int32(config.lora.region.rawValue) newLoRaConfig.usePreset = config.lora.usePreset newLoRaConfig.modemPreset = Int32(config.lora.modemPreset.rawValue) @@ -221,9 +209,7 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 newLoRaConfig.txEnabled = config.lora.txEnabled newLoRaConfig.channelNum = Int32(config.lora.channelNum) fetchedNode[0].loRaConfig = newLoRaConfig - } else { - fetchedNode[0].loRaConfig?.regionCode = Int32(config.lora.region.rawValue) fetchedNode[0].loRaConfig?.usePreset = config.lora.usePreset fetchedNode[0].loRaConfig?.modemPreset = Int32(config.lora.modemPreset.rawValue) @@ -236,10 +222,9 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 fetchedNode[0].loRaConfig?.txEnabled = config.lora.txEnabled fetchedNode[0].loRaConfig?.channelNum = Int32(config.lora.channelNum) } - do { try context.save() - MeshLogger.log("💾 Updated LoRa Config for node number: \(String(nodeNum))") + print("💾 Updated LoRa Config for node number: \(String(nodeNum))") } catch { context.rollback() let nsError = error as NSError @@ -256,7 +241,9 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) { - MeshLogger.log("📶 Network config received \(String(nodeNum))") + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.network.config %@", comment: "Network config received: %@"), String(nodeNum)) + MeshLogger.log("🌐 \(logString)") + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) @@ -277,7 +264,7 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 do { try context.save() - MeshLogger.log("💾 Updated Network Config for node number: \(String(nodeNum))") + print("💾 Updated Network Config for node number: \(String(nodeNum))") } catch { context.rollback() @@ -289,13 +276,14 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 } } catch { let nsError = error as NSError - print("💥 Fetching node for core data WiFiConfigEntity failed: \(nsError)") + print("💥 Fetching node for core data NetworkConfigEntity failed: \(nsError)") } } if config.payloadVariant == Config.OneOf_PayloadVariant.position(config.position) { - MeshLogger.log("🗺️ Position config received: \(String(nodeNum))") + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.position.config %@", comment: "Positon config received: %@"), String(nodeNum)) + MeshLogger.log("🗺️ \(logString)") let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) @@ -303,14 +291,10 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 do { let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] - // Found a node, save LoRa Config if !fetchedNode.isEmpty { - if fetchedNode[0].positionConfig == nil { - let newPositionConfig = PositionConfigEntity(context: context) - newPositionConfig.smartPositionEnabled = config.position.positionBroadcastSmartEnabled newPositionConfig.deviceGpsEnabled = config.position.gpsEnabled newPositionConfig.fixedPosition = config.position.fixedPosition @@ -318,11 +302,8 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 newPositionConfig.gpsAttemptTime = Int32(config.position.gpsAttemptTime) newPositionConfig.positionBroadcastSeconds = Int32(config.position.positionBroadcastSecs) newPositionConfig.positionFlags = Int32(config.position.positionFlags) - fetchedNode[0].positionConfig = newPositionConfig - } else { - fetchedNode[0].positionConfig?.smartPositionEnabled = config.position.positionBroadcastSmartEnabled fetchedNode[0].positionConfig?.deviceGpsEnabled = config.position.gpsEnabled fetchedNode[0].positionConfig?.fixedPosition = config.position.fixedPosition @@ -331,25 +312,20 @@ func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64 fetchedNode[0].positionConfig?.positionBroadcastSeconds = Int32(config.position.positionBroadcastSecs) fetchedNode[0].positionConfig?.positionFlags = Int32(config.position.positionFlags) } - do { try context.save() - MeshLogger.log("💾 Updated Position Config for node number: \(String(nodeNum))") - + print("💾 Updated Position Config for node number: \(String(nodeNum))") } catch { context.rollback() let nsError = error as NSError - MeshLogger.log("💥 Error Updating Core Data PositionConfigEntity: \(nsError)") + print("💥 Error Updating Core Data PositionConfigEntity: \(nsError)") } - } else { - MeshLogger.log("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Position Config") + print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Position Config") } - } catch { - let nsError = error as NSError - MeshLogger.log("💥 Fetching node for core data PositionConfigEntity failed: \(nsError)") + print("💥 Fetching node for core data PositionConfigEntity failed: \(nsError)") } } } @@ -358,7 +334,8 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(config.cannedMessage) { - MeshLogger.log("🥫 Canned Message module config received: \(String(nodeNum))") + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.cannedmessage.config %@", comment: "Canned Message module config received: %@"), String(nodeNum)) + MeshLogger.log("🥫 \(logString)") let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) @@ -403,26 +380,26 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum do { try context.save() - MeshLogger.log("💾 Updated Canned Message Module Config for node number: \(String(nodeNum))") - + print("💾 Updated Canned Message Module Config for node number: \(String(nodeNum))") } catch { context.rollback() let nsError = error as NSError - MeshLogger.log("💥 Error Updating Core Data CannedMessageConfigEntity: \(nsError)") + print("💥 Error Updating Core Data CannedMessageConfigEntity: \(nsError)") } } else { - MeshLogger.log("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Canned Message Module Config") + print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Canned Message Module Config") } - } catch { let nsError = error as NSError - MeshLogger.log("💥 Fetching node for core data CannedMessageConfigEntity failed: \(nsError)") + print("💥 Fetching node for core data CannedMessageConfigEntity failed: \(nsError)") } } if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.externalNotification(config.externalNotification) { - MeshLogger.log("🚨 External Notifiation module config received: \(String(nodeNum))") + + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.externalnotification.config %@", comment: "External Notifiation module config received: %@"), String(nodeNum)) + MeshLogger.log("📣 \(logString)") let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) @@ -470,20 +447,16 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum do { try context.save() - MeshLogger.log("💾 Updated External Notification Module Config for node number: \(String(nodeNum))") - + print("💾 Updated External Notification Module Config for node number: \(String(nodeNum))") } catch { context.rollback() let nsError = error as NSError - MeshLogger.log("💥 Error Updating Core Data ExternalNotificationConfigEntity: \(nsError)") + print("💥 Error Updating Core Data ExternalNotificationConfigEntity: \(nsError)") } - } else { - MeshLogger.log("💥 No Nodes found in local database matching node number \(nodeNum) unable to save External Notifiation Module Config") + print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save External Notifiation Module Config") } - } catch { - let nsError = error as NSError print("💥 Fetching node for core data ExternalNotificationConfigEntity failed: \(nsError)") } @@ -491,7 +464,8 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.mqtt(config.mqtt) { - MeshLogger.log("🌐 MQTT module config received: \(String(nodeNum))") + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.mqtt.config %@", comment: "MQTT module config received: %@"), String(nodeNum)) + MeshLogger.log("🌉 \(logString)") let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) @@ -499,25 +473,19 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum do { let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] - // Found a node, save MQTT Config if !fetchedNode.isEmpty { if fetchedNode[0].mqttConfig == nil { - let newMQTTConfig = MQTTConfigEntity(context: context) - newMQTTConfig.enabled = config.mqtt.enabled newMQTTConfig.address = config.mqtt.address newMQTTConfig.address = config.mqtt.username newMQTTConfig.password = config.mqtt.password newMQTTConfig.encryptionEnabled = config.mqtt.encryptionEnabled newMQTTConfig.jsonEnabled = config.mqtt.jsonEnabled - fetchedNode[0].mqttConfig = newMQTTConfig - } else { - fetchedNode[0].mqttConfig?.enabled = config.mqtt.enabled fetchedNode[0].mqttConfig?.address = config.mqtt.address fetchedNode[0].mqttConfig?.address = config.mqtt.username @@ -525,34 +493,27 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum fetchedNode[0].mqttConfig?.encryptionEnabled = config.mqtt.encryptionEnabled fetchedNode[0].mqttConfig?.jsonEnabled = config.mqtt.jsonEnabled } - do { - try context.save() - MeshLogger.log("💾 Updated MQTT Config for node number: \(String(nodeNum))") - + print("💾 Updated MQTT Config for node number: \(String(nodeNum))") } catch { - context.rollback() - let nsError = error as NSError - MeshLogger.log("💥 Error Updating Core Data MQTTConfigEntity: \(nsError)") + print("💥 Error Updating Core Data MQTTConfigEntity: \(nsError)") } - } else { - MeshLogger.log("💥 No Nodes found in local database matching node number \(nodeNum) unable to save MQTT Module Config") + print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save MQTT Module Config") } - } catch { - let nsError = error as NSError - MeshLogger.log("💥 Fetching node for core data MQTTConfigEntity failed: \(nsError)") + print("💥 Fetching node for core data MQTTConfigEntity failed: \(nsError)") } } if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.rangeTest(config.rangeTest) { - MeshLogger.log("⛰️ Range Test module config received: \(String(nodeNum))") + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.rangetest.config %@", comment: "Range Test module config received: %@"), String(nodeNum)) + MeshLogger.log("⛰️ \(logString)") let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) @@ -573,29 +534,27 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum fetchedNode[0].rangeTestConfig?.enabled = config.rangeTest.enabled fetchedNode[0].rangeTestConfig?.save = config.rangeTest.save } - do { try context.save() - MeshLogger.log("💾 Updated Range Test Config for node number: \(String(nodeNum))") + print("💾 Updated Range Test Config for node number: \(String(nodeNum))") } catch { context.rollback() let nsError = error as NSError - MeshLogger.log("💥 Error Updating Core Data RangeTestConfigEntity: \(nsError)") + print("💥 Error Updating Core Data RangeTestConfigEntity: \(nsError)") } - } else { - MeshLogger.log("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Range Test Module Config") + print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Range Test Module Config") } - } catch { let nsError = error as NSError - MeshLogger.log("💥 Fetching node for core data RangeTestConfigEntity failed: \(nsError)") + print("💥 Fetching node for core data RangeTestConfigEntity failed: \(nsError)") } } if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.serial(config.serial) { - MeshLogger.log("🤖 Serial module config received: \(String(nodeNum))") + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.serial.config %@", comment: "Serial module config received: %@"), String(nodeNum)) + MeshLogger.log("🤖 \(logString)") let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) @@ -631,7 +590,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum do { try context.save() - MeshLogger.log("💾 Updated Serial Module Config for node number: \(String(nodeNum))") + print("💾 Updated Serial Module Config for node number: \(String(nodeNum))") } catch { @@ -655,7 +614,8 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.telemetry(config.telemetry) { - MeshLogger.log("📈 Telemetry module config received: \(String(nodeNum))") + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.telemetry.config %@", comment: "Telemetry module config received: %@"), String(nodeNum)) + MeshLogger.log("📈 \(logString)") let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) @@ -689,7 +649,7 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum do { try context.save() - MeshLogger.log("💾 Updated Telemetry Module Config for node number: \(String(nodeNum))") + print("💾 Updated Telemetry Module Config for node number: \(String(nodeNum))") } catch { context.rollback() @@ -710,6 +670,9 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedObjectContext) -> MyInfoEntity? { + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.myinfo %@", comment: "MyInfo received: %@"), String(myInfo.myNodeNum)) + MeshLogger.log("ℹ️ \(logString)") + let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(myInfo.myNodeNum)) @@ -732,18 +695,15 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO myInfoEntity.messageTimeoutMsec = Int32(bitPattern: myInfo.messageTimeoutMsec) myInfoEntity.minAppVersion = Int32(bitPattern: myInfo.minAppVersion) myInfoEntity.maxChannels = Int32(bitPattern: myInfo.maxChannels) - do { try context.save() - MeshLogger.log("💾 Saved a new myInfo for node number: \(String(myInfo.myNodeNum))") + print("💾 Saved a new myInfo for node number: \(String(myInfo.myNodeNum))") return myInfoEntity - } catch { context.rollback() let nsError = error as NSError print("💥 Error Inserting New Core Data MyInfoEntity: \(nsError)") } - } else { fetchedMyInfo[0].peripheralId = peripheralId @@ -760,9 +720,8 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO do { try context.save() - MeshLogger.log("💾 Updated myInfo for node number: \(String(myInfo.myNodeNum))") + print("💾 Updated myInfo for node number: \(String(myInfo.myNodeNum))") return fetchedMyInfo[0] - } catch { context.rollback() let nsError = error as NSError @@ -770,7 +729,6 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO } } } catch { - print("💥 Fetch MyInfo Error") } return nil @@ -779,6 +737,9 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectContext) { if channel.isInitialized && channel.hasSettings && channel.role != Channel.Role.disabled { + + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.channel.received %d %@", comment: "Channel %d received from: %@"), channel.index, String(fromNum)) + MeshLogger.log("🎛️ \(logString)") let fetchedMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") fetchedMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", fromNum) @@ -786,9 +747,7 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo do { let fetchedMyInfo = try context.fetch(fetchedMyInfoRequest) as! [MyInfoEntity] - if fetchedMyInfo.count == 1 { - let newChannel = ChannelEntity(context: context) newChannel.id = Int32(channel.index) newChannel.index = Int32(channel.index) @@ -809,15 +768,12 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo } catch { print("Failed to save channel") } - MeshLogger.log("💾 Updated MyInfo channel \(channel.index) from Channel App Packet For: \(fetchedMyInfo[0].myNodeNum)") + print("💾 Updated MyInfo channel \(channel.index) from Channel App Packet For: \(fetchedMyInfo[0].myNodeNum)") } else if channel.role.rawValue > 0 { print("💥 Trying to save a channel to a MyInfo that does not exist: \(fromNum)") } - } catch { - context.rollback() - let nsError = error as NSError print("💥 Error Saving MyInfo Channel from ADMIN_APP \(nsError)") } @@ -826,6 +782,9 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObjectContext) -> NodeInfoEntity? { + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.nodeinfo.received %@", comment: "Node info received for: %@"), String(nodeInfo.num)) + MeshLogger.log("📟 \(logString)") + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeInfo.num)) @@ -841,14 +800,11 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newNode.channel = Int32(channel) if nodeInfo.hasDeviceMetrics { - let telemetry = TelemetryEntity(context: context) - telemetry.batteryLevel = Int32(nodeInfo.deviceMetrics.batteryLevel) telemetry.voltage = nodeInfo.deviceMetrics.voltage telemetry.channelUtilization = nodeInfo.deviceMetrics.channelUtilization telemetry.airUtilTx = nodeInfo.deviceMetrics.airUtilTx - var newTelemetries = [TelemetryEntity]() newTelemetries.append(telemetry) newNode.telemetries? = NSOrderedSet(array: newTelemetries) @@ -856,9 +812,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) newNode.snr = nodeInfo.snr - if nodeInfo.hasUser { - let newUser = UserEntity(context: context) newUser.userId = nodeInfo.user.id newUser.num = Int64(nodeInfo.num) @@ -870,7 +824,6 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje } if nodeInfo.position.latitudeI > 0 || nodeInfo.position.longitudeI > 0 { - let position = PositionEntity(context: context) position.seqNo = Int32(nodeInfo.position.seqNumber) position.latitudeI = nodeInfo.position.latitudeI @@ -880,7 +833,6 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje position.speed = Int32(nodeInfo.position.groundSpeed) position.heading = Int32(nodeInfo.position.groundTrack) position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time))) - var newPostions = [PositionEntity]() newPostions.append(position) newNode.positions? = NSOrderedSet(array: newPostions) @@ -896,30 +848,17 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje if fetchedMyInfo.count > 0 { newNode.myInfo = fetchedMyInfo[0] } - do { - try context.save() - - if nodeInfo.hasUser { - MeshLogger.log("💾 BLE FROMRADIO received and nodeInfo inserted for \(nodeInfo.user.longName)") - } else { - MeshLogger.log("💾 BLE FROMRADIO received and nodeInfo inserted for \(nodeInfo.num)") - } return newNode - } catch { - context.rollback() - let nsError = error as NSError print("💥 Error Saving Core Data NodeInfoEntity: \(nsError)") } - } catch { print("💥 Fetch MyInfo Error") } - } else if nodeInfo.hasUser && nodeInfo.num > 0 { fetchedNode[0].id = Int64(nodeInfo.num) @@ -942,12 +881,10 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje if nodeInfo.hasDeviceMetrics { let newTelemetry = TelemetryEntity(context: context) - newTelemetry.batteryLevel = Int32(nodeInfo.deviceMetrics.batteryLevel) newTelemetry.voltage = nodeInfo.deviceMetrics.voltage newTelemetry.channelUtilization = nodeInfo.deviceMetrics.channelUtilization newTelemetry.airUtilTx = nodeInfo.deviceMetrics.airUtilTx - let mutableTelemetries = fetchedNode[0].telemetries!.mutableCopy() as! NSMutableOrderedSet fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet } @@ -960,11 +897,8 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje position.altitude = nodeInfo.position.altitude position.satsInView = Int32(nodeInfo.position.satsInView) position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time))) - let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet - fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet - } // Look for a MyInfo @@ -972,50 +906,34 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num)) do { - let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as! [MyInfoEntity] if fetchedMyInfo.count > 0 { - fetchedNode[0].myInfo = fetchedMyInfo[0] } - do { - try context.save() - - if nodeInfo.hasUser { - MeshLogger.log("💾 BLE FROMRADIO received and nodeInfo inserted for \(nodeInfo.user.longName)") - - } else { - MeshLogger.log("💾 BLE FROMRADIO received and nodeInfo inserted for \(nodeInfo.num)") - } - + print("💾 NodeInfo saved for \(nodeInfo.num)") return fetchedNode[0] - } catch { - context.rollback() - let nsError = error as NSError print("💥 Error Saving Core Data NodeInfoEntity: \(nsError)") } - } catch { print("💥 Fetch MyInfo Error") } } - } catch { - print("💥 Fetch NodeInfoEntity Error") } - return nil } - func nodeInfoAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.nodeinfo.received %@", comment: "Node info received for: %@"), String(packet.from)) + MeshLogger.log("📟 \(logString)") + let fetchNodeInfoAppRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) @@ -1031,22 +949,17 @@ func nodeInfoAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { fetchedNode[0].channel = Int32(packet.channel) if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { - if nodeInfoMessage.hasDeviceMetrics { - let telemetry = TelemetryEntity(context: context) - telemetry.batteryLevel = Int32(nodeInfoMessage.deviceMetrics.batteryLevel) telemetry.voltage = nodeInfoMessage.deviceMetrics.voltage telemetry.channelUtilization = nodeInfoMessage.deviceMetrics.channelUtilization telemetry.airUtilTx = nodeInfoMessage.deviceMetrics.airUtilTx - var newTelemetries = [TelemetryEntity]() newTelemetries.append(telemetry) fetchedNode[0].telemetries? = NSOrderedSet(array: newTelemetries) } if nodeInfoMessage.hasUser { - fetchedNode[0].user!.userId = nodeInfoMessage.user.id fetchedNode[0].user!.num = Int64(nodeInfoMessage.num) fetchedNode[0].user!.longName = nodeInfoMessage.user.longName @@ -1055,36 +968,75 @@ func nodeInfoAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() } } - do { try context.save() - MeshLogger.log("💾 Updated NodeInfo from Node Info App Packet For: \(fetchedNode[0].num)") + print("💾 Updated NodeInfo from Node Info App Packet For: \(fetchedNode[0].num)") } catch { context.rollback() let nsError = error as NSError - MeshLogger.log("💥 Error Saving NodeInfoEntity from NODEINFO_APP \(nsError)") + print("💥 Error Saving NodeInfoEntity from NODEINFO_APP \(nsError)") } - } else { - // New node info not from device but potentially from another network } - } catch { - MeshLogger.log("💥 Error Fetching NodeInfoEntity for NODEINFO_APP") + print("💥 Error Fetching NodeInfoEntity for NODEINFO_APP") } } func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { - if let messages = try? CannedMessageModuleConfig(serializedData: packet.decoded.payload) { - print(messages) + if let adminMessage = try? AdminMessage(serializedData: packet.decoded.payload) { + + if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getCannedMessageModuleMessagesResponse(adminMessage.getCannedMessageModuleMessagesResponse) { + + if let cmmc = try? CannedMessageModuleConfig(serializedData: packet.decoded.payload) { + + if !cmmc.messages.isEmpty { + + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.cannedmessages.messages.received %@", comment: "Canned Messages Messages Received For: %@"), String(packet.from)) + MeshLogger.log("🥫 \(logString)") + + let fetchNodeRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) + + do { + let fetchedNode = try context.fetch(fetchNodeRequest) as! [NodeInfoEntity] + if fetchedNode.count == 1 { + let messages = String(cmmc.textFormatString()) + .replacingOccurrences(of: "11: ", with: "") + .replacingOccurrences(of: "\"", with: "") + .trimmingCharacters(in: .whitespacesAndNewlines) + fetchedNode[0].cannedMessageConfig?.messages = messages + do { + try context.save() + print("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num)") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Saving NodeInfoEntity from POSITION_APP \(nsError)") + } + } + } catch { + print("💥 Error Deserializing ADMIN_APP packet.") + } + } + } + } + + else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getChannelResponse(adminMessage.getChannelResponse) { + + channelPacket(channel: adminMessage.getChannelResponse, fromNum: Int64(packet.from), context: context) + } } + } - func positionPacket (packet: MeshPacket, context: NSManagedObjectContext) { + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.position.received %@", comment: "Position Packet received from node: %@"), String(packet.from)) + MeshLogger.log("📍 \(logString)") + let fetchNodePositionRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodePositionRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) @@ -1097,7 +1049,6 @@ func positionPacket (packet: MeshPacket, context: NSManagedObjectContext) { if fetchedNode.count == 1 { let position = PositionEntity(context: context) - position.snr = packet.rxSnr position.seqNo = Int32(positionMessage.seqNumber) position.latitudeI = positionMessage.latitudeI @@ -1111,23 +1062,18 @@ func positionPacket (packet: MeshPacket, context: NSManagedObjectContext) { } else { position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) } - let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet mutablePositions.add(position) - fetchedNode[0].id = Int64(packet.from) fetchedNode[0].num = Int64(packet.from) fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) fetchedNode[0].snr = packet.rxSnr fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet - do { try context.save() - MeshLogger.log("💾 Updated Node Position Coordinates, SNR and Time from Position App Packet For: \(fetchedNode[0].num)") + print("💾 Updated Node Position Coordinates, SNR and Time from Position App Packet For: \(fetchedNode[0].num)") } catch { - context.rollback() - let nsError = error as NSError print("💥 Error Saving NodeInfoEntity from POSITION_APP \(nsError)") } @@ -1146,44 +1092,25 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana if let routingMessage = try? Routing(serializedData: packet.decoded.payload) { - let error = routingMessage.errorReason + let routingError = RoutingError(rawValue: routingMessage.errorReason.rawValue) - var errorExplanation = "Unknown Routing Error" - - switch error { - case Routing.Error.none: - errorExplanation = "This message is not a failure" - case Routing.Error.noRoute: - errorExplanation = "Our node doesn't have a route to the requested destination anymore." - case Routing.Error.gotNak: - errorExplanation = "We received a nak while trying to forward on your behalf" - case Routing.Error.timeout: - errorExplanation = "Timeout" - case Routing.Error.noInterface: - errorExplanation = "No suitable interface could be found for delivering this packet" - case Routing.Error.maxRetransmit: - errorExplanation = "We reached the max retransmission count (typically for naive flood routing)" - case Routing.Error.noChannel: - errorExplanation = "No suitable channel was found for sending this packet (i.e. was requested channel index disabled?)" - case Routing.Error.tooLarge: - errorExplanation = "The packet was too big for sending (exceeds interface MTU after encoding)" - case Routing.Error.noResponse: - errorExplanation = "The request had want_response set, the request reached the destination node, but no service on that node wants to send a response (possibly due to bad channel permissions)" - case Routing.Error.badRequest: - errorExplanation = "The application layer service on the remote node received your request, but considered your request somehow invalid" - case Routing.Error.notAuthorized: - errorExplanation = "The application layer service on the remote node received your request, but considered your request not authorized (i.e you did not send the request on the required bound channel)" - fallthrough - default: () - } - - MeshLogger.log("🕸️ ROUTING PACKET received for RequestID: \(packet.decoded.requestID) Error: \(errorExplanation)") + let routingErrorString = routingError?.display ?? NSLocalizedString("unknown", comment: "") + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.routing.message %@ %@", comment: "Routing received for RequestID: %@ Ack Status: %@"), String(packet.decoded.requestID), routingErrorString) + MeshLogger.log("🕸️ \(logString)") + let fetchMessageRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MessageEntity") fetchMessageRequest.predicate = NSPredicate(format: "messageId == %lld", Int64(packet.decoded.requestID)) do { let fetchedMessage = try context.fetch(fetchMessageRequest) as? [MessageEntity] if fetchedMessage?.count ?? 0 > 0 { + + if fetchedMessage![0].toUser != nil { + // Real ACK from DM Recipient + if packet.to != packet.from { + fetchedMessage![0].realACK = true + } + } fetchedMessage![0].ackError = Int32(routingMessage.errorReason.rawValue) if routingMessage.errorReason == Routing.Error.none { @@ -1218,11 +1145,11 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana return } try context.save() - MeshLogger.log("💾 ACK Received and saved for MessageID \(packet.decoded.requestID)") + print("💾 ACK Saved for Message: \(packet.decoded.requestID)") } catch { context.rollback() let nsError = error as NSError - MeshLogger.log("💥 Error Saving ACK for message MessageID \(packet.id) Error: \(nsError)") + print("💥 Error Saving ACK for message: \(packet.id) Error: \(nsError)") } } } @@ -1231,7 +1158,13 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage if let telemetryMessage = try? Telemetry(serializedData: packet.decoded.payload) { - let telemetry = TelemetryEntity(context: context) + // Only log telemetry from the mesh not the connected device + if connectedNode != Int64(packet.from) { + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.telemetry.received %@", comment: "Telemetry received for: %@"), String(packet.from)) + MeshLogger.log("📈 \(logString)") + } + + let telemetry = TelemetryEntity(context: context) let fetchNodeTelemetryRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeTelemetryRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) @@ -1239,20 +1172,15 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage do { let fetchedNode = try context.fetch(fetchNodeTelemetryRequest) as! [NodeInfoEntity] - if fetchedNode.count == 1 { - if telemetryMessage.variant == Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) { - // Device Metrics telemetry.airUtilTx = telemetryMessage.deviceMetrics.airUtilTx telemetry.channelUtilization = telemetryMessage.deviceMetrics.channelUtilization telemetry.batteryLevel = Int32(telemetryMessage.deviceMetrics.batteryLevel) telemetry.voltage = telemetryMessage.deviceMetrics.voltage telemetry.metricsType = 0 - } else if telemetryMessage.variant == Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) { - // Environment Metrics telemetry.barometricPressure = telemetryMessage.environmentMetrics.barometricPressure telemetry.current = telemetryMessage.environmentMetrics.current @@ -1262,30 +1190,25 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage telemetry.current = telemetryMessage.environmentMetrics.current telemetry.voltage = telemetryMessage.environmentMetrics.voltage telemetry.metricsType = 1 - } telemetry.time = Date(timeIntervalSince1970: TimeInterval(Int64(telemetryMessage.time))) let mutableTelemetries = fetchedNode[0].telemetries!.mutableCopy() as! NSMutableOrderedSet mutableTelemetries.add(telemetry) - fetchedNode[0].lastHeard = telemetry.time fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet } - try context.save() - // Only log telemetery from the mesh not the connected device + // Only log telemetry from the mesh not the connected device if connectedNode != Int64(packet.from) { - MeshLogger.log("💾 Telemetry Saved for Node: \(packet.from)") + print("💾 Telemetry Saved for Node: \(packet.from)") } - } catch { context.rollback() let nsError = error as NSError - MeshLogger.log("💥 Error Saving Telemetry for Node \(packet.from) Error: \(nsError)") + print("💥 Error Saving Telemetry for Node \(packet.from) Error: \(nsError)") } - } else { - MeshLogger.log("💥 Error Fetching NodeInfoEntity for Node \(packet.from)") + print("💥 Error Fetching NodeInfoEntity for Node \(packet.from)") } } @@ -1293,7 +1216,8 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM if let messageText = String(bytes: packet.decoded.payload, encoding: .utf8) { - MeshLogger.log("💬 Message received for text message app") + MeshLogger.log("💬 \(NSLocalizedString("mesh.log.textmessage.received", comment: "Message received from the text message app"))") + let messageUsers: NSFetchRequest = NSFetchRequest.init(entityName: "UserEntity") messageUsers.predicate = NSPredicate(format: "num IN %@", [packet.to, packet.from]) @@ -1329,7 +1253,7 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM do { try context.save() - MeshLogger.log("💾 Saved a new message for \(newMessage.messageId)") + print("💾 Saved a new message for \(newMessage.messageId)") messageSaved = true if messageSaved { @@ -1340,12 +1264,12 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM manager.notifications = [ Notification( id: ("notification.id.\(newMessage.messageId)"), - title: "\(newMessage.fromUser?.longName ?? "Unknown")", + title: "\(newMessage.fromUser?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))", subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")", content: messageText) ] manager.schedule() - MeshLogger.log("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown")") + print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))") } else if newMessage.fromUser != nil && newMessage.toUser == nil { let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") @@ -1364,27 +1288,26 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM manager.notifications = [ Notification( id: ("notification.id.\(newMessage.messageId)"), - title: "\(newMessage.fromUser?.longName ?? "Unknown")", + title: "\(newMessage.fromUser?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))", subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")", content: messageText) ] manager.schedule() - MeshLogger.log("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown")") + print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))") } } } catch { - } } } } catch { context.rollback() let nsError = error as NSError - MeshLogger.log("💥 Failed to save new MessageEntity \(nsError)") + print("💥 Failed to save new MessageEntity \(nsError)") } } catch { - MeshLogger.log("💥 Fetch Message To and From Users Error") + print("💥 Fetch Message To and From Users Error") } } } diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index a8bccd83..1b3c637e 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV4.xcdatamodel + MeshtasticDataModelV5.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV5.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV5.xcdatamodel/contents new file mode 100644 index 00000000..1f463d49 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV5.xcdatamodel/contents @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 84ae1e6a..7d9b80cf 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -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! diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index bd752922..40bc81b9 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -61,11 +61,8 @@ public func clearTelemetry(destNum: Int64, metricsType: Int32, context: NSManage } public func deleteChannelMessages(channel: ChannelEntity, context: NSManagedObjectContext) { - let fetchChannelMessagesRequest = NSFetchRequest(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(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) } diff --git a/Meshtastic/Protobufs/admin.pb.swift b/Meshtastic/Protobufs/admin.pb.swift index 21b5f2a0..ec6d69a8 100644 --- a/Meshtastic/Protobufs/admin.pb.swift +++ b/Meshtastic/Protobufs/admin.pb.swift @@ -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"), ] } diff --git a/Meshtastic/Protobufs/config.pb.swift b/Meshtastic/Protobufs/config.pb.swift index 3c300b12..bc6b37eb 100644 --- a/Meshtastic/Protobufs/config.pb.swift +++ b/Meshtastic/Protobufs/config.pb.swift @@ -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(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 = [ diff --git a/Meshtastic/Protobufs/localonly.pb.swift b/Meshtastic/Protobufs/localonly.pb.swift index 2c89a577..741b8e2d 100644 --- a/Meshtastic/Protobufs/localonly.pb.swift +++ b/Meshtastic/Protobufs/localonly.pb.swift @@ -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 } diff --git a/Meshtastic/Protobufs/mesh.pb.swift b/Meshtastic/Protobufs/mesh.pb.swift index a2dd9a94..05e93acc 100644 --- a/Meshtastic/Protobufs/mesh.pb.swift +++ b/Meshtastic/Protobufs/mesh.pb.swift @@ -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(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(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 } } diff --git a/Meshtastic/Protobufs/module_config.pb.swift b/Meshtastic/Protobufs/module_config.pb.swift index 19fa66f7..74a8e7a1 100644 --- a/Meshtastic/Protobufs/module_config.pb.swift +++ b/Meshtastic/Protobufs/module_config.pb.swift @@ -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(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(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(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 = [ diff --git a/Meshtastic/Protobufs/storeforward.pb.swift b/Meshtastic/Protobufs/storeforward.pb.swift index b6df6719..2311daa4 100644 --- a/Meshtastic/Protobufs/storeforward.pb.swift +++ b/Meshtastic/Protobufs/storeforward.pb.swift @@ -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(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"), ] } diff --git a/Meshtastic/Protobufs/telemetry.pb.swift b/Meshtastic/Protobufs/telemetry.pb.swift index f0404197..de098cdc 100644 --- a/Meshtastic/Protobufs/telemetry.pb.swift +++ b/Meshtastic/Protobufs/telemetry.pb.swift @@ -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"), ] } diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index cfa38d5e..d9e29e1b 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -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) } } diff --git a/Meshtastic/Views/Bluetooth/InvalidVersion.swift b/Meshtastic/Views/Bluetooth/InvalidVersion.swift index d4539b54..a92785cf 100644 --- a/Meshtastic/Views/Bluetooth/InvalidVersion.swift +++ b/Meshtastic/Views/Bluetooth/InvalidVersion.swift @@ -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) diff --git a/Meshtastic/Views/Helpers/ConnectedDevice.swift b/Meshtastic/Views/Helpers/ConnectedDevice.swift index e3552229..ce0f5e66 100644 --- a/Meshtastic/Views/Helpers/ConnectedDevice.swift +++ b/Meshtastic/Views/Helpers/ConnectedDevice.swift @@ -30,7 +30,7 @@ struct ConnectedDevice: View { } } else { - Text("Bluetooth Off").font(.subheadline).foregroundColor(.red) + Text("bluetooth.off").font(.subheadline).foregroundColor(.red) } } } diff --git a/Meshtastic/Views/Helpers/MessageBubble.swift b/Meshtastic/Views/Helpers/MessageBubble.swift deleted file mode 100644 index 494ca7d2..00000000 --- a/Meshtastic/Views/Helpers/MessageBubble.swift +++ /dev/null @@ -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)) - } -} diff --git a/Meshtastic/Views/Helpers/NodeAnnotation.swift b/Meshtastic/Views/Helpers/NodeAnnotation.swift index 817448f1..5de5656c 100644 --- a/Meshtastic/Views/Helpers/NodeAnnotation.swift +++ b/Meshtastic/Views/Helpers/NodeAnnotation.swift @@ -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)) diff --git a/Meshtastic/Views/Map/Custom/PositionAnnotationView.swift b/Meshtastic/Views/Map/Custom/PositionAnnotationView.swift index 47c20197..3c3e0f79 100644 --- a/Meshtastic/Views/Map/Custom/PositionAnnotationView.swift +++ b/Meshtastic/Views/Map/Custom/PositionAnnotationView.swift @@ -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) - } - } diff --git a/Meshtastic/Views/Map/MapView.swift b/Meshtastic/Views/Map/MapView.swift index 57deab83..02c752be 100644 --- a/Meshtastic/Views/Map/MapView.swift +++ b/Meshtastic/Views/Map/MapView.swift @@ -124,7 +124,7 @@ private extension MapView { 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.title = node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown") annotation.shortName = node.user?.shortName?.uppercased() ?? "???" view.addAnnotation(annotation) diff --git a/Meshtastic/Views/Map/MapViewModule.swift b/Meshtastic/Views/Map/MapViewModule.swift index fd0b6fe7..13d5bb77 100644 --- a/Meshtastic/Views/Map/MapViewModule.swift +++ b/Meshtastic/Views/Map/MapViewModule.swift @@ -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 { @@ -250,7 +224,7 @@ public struct MapView: UIViewRepresentable { let annotation = PositionAnnotation() annotation.coordinate = position.coordinate! - annotation.title = position.nodePosition!.user?.longName ?? "Unknown" + annotation.title = position.nodePosition!.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown") annotation.shortName = position.nodePosition!.user?.shortName?.uppercased() ?? "???" mapView.addAnnotation(annotation) diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index ba3c638a..31f71bbe 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -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) { diff --git a/Meshtastic/Views/Messages/Contacts.swift b/Meshtastic/Views/Messages/Contacts.swift index d009d5f6..241a694e 100644 --- a/Meshtastic/Views/Messages/Contacts.swift +++ b/Meshtastic/Views/Messages/Contacts.swift @@ -20,7 +20,8 @@ struct Contacts: View { private var users: FetchedResults @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 { diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 83fd25f3..d696714e 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -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) { diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 903d94b3..0925335d 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -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).") } } ) diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index 18f72249..e044a08f 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -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).") } } diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index ee0da7d0..932ae13a 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -192,28 +192,21 @@ struct NodeDetail: View { 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) { @@ -362,9 +355,7 @@ 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") } } @@ -377,26 +368,23 @@ struct NodeDetail: View { }) { - 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 - ) { - - Button("Reboot Node?", role: .destructive) { - - if !bleManager.sendReboot(destNum: node.num) { - - print("Reboot Failed") + .confirmationDialog("are.you.sure", + + isPresented: $showingRebootConfirm + ) { + Button("reboot.node", role: .destructive) { + + if !bleManager.sendReboot(fromUser: node.user!, toUser: node.user!) { + print("Reboot Failed") + } + } } - } - } } .padding(5) } @@ -404,12 +392,10 @@ struct NodeDetail: View { .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, @@ -423,15 +409,3 @@ struct NodeDetail: View { } } } - -struct NodeInfoEntityDetail_Previews: PreviewProvider { - - static let bleManager = BLEManager() - - static var previews: some View { - Group { - - // NodeDetail(node: node) - } - } -} diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index aaefe178..1925c767 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -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,7 +53,7 @@ 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 { + if lastPostion.coordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude { let nodeCoord = CLLocation(latitude: lastPostion.coordinate!.latitude, longitude: lastPostion.coordinate!.longitude) let metersAway = nodeCoord.distance(from: myCoord) Image(systemName: "lines.measurement.horizontal") diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 9442e16c..3f968bab 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -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: diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index 4554c7b9..2be8210c 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -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 { diff --git a/Meshtastic/Views/Settings/About.swift b/Meshtastic/Views/Settings/About.swift index 32745f11..43e67809 100644 --- a/Meshtastic/Views/Settings/About.swift +++ b/Meshtastic/Views/Settings/About.swift @@ -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) } } diff --git a/Meshtastic/Views/Settings/AdminMessageList.swift b/Meshtastic/Views/Settings/AdminMessageList.swift index d52e3fd7..b2c40f8a 100644 --- a/Meshtastic/Views/Settings/AdminMessageList.swift +++ b/Meshtastic/Views/Settings/AdminMessageList.swift @@ -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 } } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 7a1cd178..a9d9ada1 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -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 diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 486c3693..52a4d8ec 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -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 + } + } +} diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index 1516a5a9..dcc10981 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -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 { diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 1abc2cdf..fcb55ad1 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -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 { diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index 13b12138..9103c5ce 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -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 { diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 6f5164a3..dc4ea18e 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -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") diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index a7b16b39..46071f37 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -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 diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index c3a9b9fe..90b8e759 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -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 { diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 89f472dc..54f63e6d 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -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 } } } diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index 38ecae08..9fbd5ffd 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -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 { diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index 349373bd..e515244c 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -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: diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index 0eeb1902..c803add3 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -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 { diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index c71bfaa0..65e73b2c 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -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") diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index e800a64f..8600dd53 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -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: diff --git a/Meshtastic/Views/Settings/MeshLog.swift b/Meshtastic/Views/Settings/MeshLog.swift index a2db3e19..63f86766 100644 --- a/Meshtastic/Views/Settings/MeshLog.swift +++ b/Meshtastic/Views/Settings/MeshLog.swift @@ -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") } } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 9bb805f7..d4728fc1 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -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") diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index 5810b5cf..663926a3 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -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 { diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index 6380430b..f372499e 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -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() diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index bde184d0..54aeb93d 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -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..."; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index dbe62ed9..6b772ea7 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -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. . .";