diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index c2d1d4b9..5ad009b7 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -28,7 +28,7 @@ DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553562855B02500E55709 /* LoRaConfig.swift */; }; DD2553592855B52700E55709 /* PositionConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553582855B52700E55709 /* PositionConfig.swift */; }; DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */; }; - DD2DC2C029BCD8AB003B383C /* HardwareModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */; }; + DD33DB622B3D27C7003E1EA0 /* FirmwareApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */; }; DD3501892852FC3B000FC853 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3501882852FC3B000FC853 /* Settings.swift */; }; DD3619152B1EF9F900C41C8C /* LocationsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */; }; DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */; }; @@ -180,6 +180,7 @@ DDF6B2482A9AEBF500BA6931 /* StoreForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */; }; DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; }; DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */; }; + DDFFA7472B3A7F3C004730DB /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFFA7462B3A7F3C004730DB /* Bundle.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -245,7 +246,8 @@ DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV22.xcdatamodel; sourceTree = ""; }; DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewSwiftUI.swift; sourceTree = ""; }; DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV19.xcdatamodel; sourceTree = ""; }; - DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HardwareModels.swift; sourceTree = ""; }; + DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 23.xcdatamodel"; sourceTree = ""; }; + DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareApi.swift; sourceTree = ""; }; DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV21.xcdatamodel; sourceTree = ""; }; DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsHandler.swift; sourceTree = ""; }; @@ -421,6 +423,7 @@ DDF6B24B2A9C2FC800BA6931 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; DDF924C926FBB953009FE055 /* ConnectedDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = ""; }; DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentConditionsCompact.swift; sourceTree = ""; }; + DDFFA7462B3A7F3C004730DB /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -548,6 +551,7 @@ DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */, DDA0B6B1294CDC55001356EC /* Channels.swift */, DDD6EEAE29BC024700383354 /* Firmware.swift */, + DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */, DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */, DD86D40B287F401000BAEB7A /* SaveChannelQRCode.swift */, DD3501882852FC3B000FC853 /* Settings.swift */, @@ -661,7 +665,6 @@ DD1925B828CDA93900720036 /* SerialConfigEnums.swift */, DD994B68295F88B60013760A /* IntervalEnums.swift */, DD5E5239298EFA5300D21B61 /* TelemetryWeather.swift */, - DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */, ); path = Enums; sourceTree = ""; @@ -905,6 +908,7 @@ DDB75A0E2A05920E006ED576 /* FileManager.swift */, DDB75A102A059258006ED576 /* Url.swift */, DD1933772B084F4200771CD5 /* Measurement.swift */, + DDFFA7462B3A7F3C004730DB /* Bundle.swift */, ); path = Extensions; sourceTree = ""; @@ -1123,6 +1127,7 @@ buildActionMask = 2147483647; files = ( DDDB444829F8A9C900EE2349 /* String.swift in Sources */, + DDFFA7472B3A7F3C004730DB /* Bundle.swift in Sources */, DD5E520C298EE33B00D21B61 /* portnums.pb.swift in Sources */, DD457188293C7E63000C49FB /* BLESignalStrengthIndicator.swift in Sources */, DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */, @@ -1186,6 +1191,7 @@ DDDB444A29F8AA3A00EE2349 /* CLLocationCoordinate2D.swift in Sources */, DD41582628582E9B009B0E59 /* DeviceConfig.swift in Sources */, DD007BAE2AA4E91200F5FA12 /* MyInfoEntityExtension.swift in Sources */, + DD33DB622B3D27C7003E1EA0 /* FirmwareApi.swift in Sources */, DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */, DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */, DD5E5207298EE33B00D21B61 /* connection_status.pb.swift in Sources */, @@ -1209,7 +1215,6 @@ DD47E3D626F17ED900029299 /* CircleText.swift in Sources */, DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */, DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */, - DD2DC2C029BCD8AB003B383C /* HardwareModels.swift in Sources */, DDB6ABD928B0A4BA00384BA1 /* BluetoothModes.swift in Sources */, DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */, DD2553592855B52700E55709 /* PositionConfig.swift in Sources */, @@ -1486,7 +1491,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.17; + MARKETING_VERSION = 2.2.18; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1520,7 +1525,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.17; + MARKETING_VERSION = 2.2.18; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1642,7 +1647,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.17; + MARKETING_VERSION = 2.2.18; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1675,7 +1680,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.17; + MARKETING_VERSION = 2.2.18; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1786,6 +1791,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */, DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */, DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */, DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */, @@ -1809,7 +1815,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */; + currentVersion = DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/Contents.json b/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/Contents.json new file mode 100644 index 00000000..af90d4b0 --- /dev/null +++ b/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "solarnode.jpeg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "solarnode 1.jpeg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "solar_node.jpeg", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/solar_node.jpeg b/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/solar_node.jpeg new file mode 100644 index 00000000..bb84f38f Binary files /dev/null and b/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/solar_node.jpeg differ diff --git a/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/solarnode 1.jpeg b/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/solarnode 1.jpeg new file mode 100644 index 00000000..547de73b Binary files /dev/null and b/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/solarnode 1.jpeg differ diff --git a/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/solarnode.jpeg b/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/solarnode.jpeg new file mode 100644 index 00000000..547de73b Binary files /dev/null and b/Meshtastic/Assets.xcassets/SOLAR_NODE.imageset/solarnode.jpeg differ diff --git a/Meshtastic/Enums/HardwareModels.swift b/Meshtastic/Enums/HardwareModels.swift deleted file mode 100644 index e14cadfd..00000000 --- a/Meshtastic/Enums/HardwareModels.swift +++ /dev/null @@ -1,334 +0,0 @@ -// -// HardwareModels.swift -// Meshtastic -// -// Copyright(c) Garth Vander Houwen 3/11/23. -// - -import Foundation - -enum HardwareModels: String, CaseIterable, Identifiable { - - case UNSET - case TLORAV2 - case TLORAV1 - case TLORAV211P6 - case TBEAM - case HELTECV20 - case TBEAMV0P7 - case TECHO - case TLORAV11P3 - case RAK4631 - case HELTECV21 - case HELTECV1 - case DIYV1 - case LILYGOTBEAMS3CORE - case RAK11200 - case NANOG1 - case TLORAV211P8 - case TLORAT3S3 - case NANOG1EXPLORER - case STATIONG1 - case M5STACK - case HELTECV3 - case HELTECWSLV3 - case NANOG2ULTRA - case RAK11310 - case RPIPICO - case HELTECWIRELESSTRACKER - case HELTECWIRELESSPAPER - case TDECK - case TWATCHS3 - - var id: String { self.rawValue } - var description: String { - switch self { - - case .UNSET: - return "unset".localized - case .TLORAV2: - return "TLoRa V2" - case .TLORAV1: - return "TLoRa V1" - case .TLORAV211P6: - return "TLoRa V2.1.1.6" - case .TBEAM: - return "TBeam" - case .HELTECV20: - return "HELTEC V2.0" - case .TBEAMV0P7: - return "TBeam 0.7" - case .TECHO: - return "TEcho" - case .TLORAV11P3: - return "TLORA V1.1.3" - case .RAK4631: - return "RAK 4631 NRF" - case .HELTECV21: - return "HELTEC V2.1" - case .HELTECV1: - return "HELTEC V1" - case .DIYV1: - return "Hydra 1W DIY" - case .LILYGOTBEAMS3CORE: - return "TBEAM S3" - case .RAK11200: - return "RAK 11200 ESP32" - case .NANOG1: - return "Nano G1" - case .TLORAV211P8: - return "TLoRa V2.1.1.8" - case .TLORAT3S3: - return "TLoRa T3 S3" - case .NANOG1EXPLORER: - return "Nano G1 Explorer" - case .STATIONG1: - return "Station G1" - case .M5STACK: - return "M5 Stack" - case .HELTECV3: - return "Heltec V3" - case .HELTECWSLV3: - return "Heltec wireless stick lite V3" - case .NANOG2ULTRA: - return "Nano G2 Ultra" - case .RAK11310: - return "RAK 11310 Pi Pico" - case .RPIPICO: - return "Pi Pico" - case .HELTECWIRELESSTRACKER: - return "Heltec Wireless Tracker" - case .HELTECWIRELESSPAPER: - return "Heltec Wireless Paper" - case .TDECK: - return "T-Deck" - case .TWATCHS3: - return "T-Watch S3" - } - - } - var firmwareStrings: [String] { - switch self { - - case .UNSET: - return [] - case .TLORAV2: - return ["firmware-tlora-v2-"] - case .TLORAV1: - return ["firmware-tlora-v1-"] - case .TLORAV211P6: - return ["firmware-tlora-v2-1-1.6-"] - case .TBEAM: - return ["firmware-tbeam-"] - case .HELTECV20: - return ["firmware-heltec-v2.0-"] - case .TBEAMV0P7: - return ["firmware-tbeam0.7-"] - case .TECHO: - return ["firmware-t-echo-"] - case .TLORAV11P3: - return ["firmware-tlora_v1_3-"] - case .RAK4631: - return ["firmware-rak4631-", "firmware-rak4631_eink-"] - case .HELTECV21: - return ["firmware-heltec-v2.1-"] - case .HELTECV1: - return ["firmware-heltec-v1-"] - case .DIYV1: - return ["firmware-meshtastic-diy-v1"] - case .LILYGOTBEAMS3CORE: - return ["firmware-tbeam-s3-core-"] - case .RAK11200: - return ["firmware-rak11200-"] - case .NANOG1: - return ["firmware-nano-g1-"] - case .TLORAV211P8: - return ["firmware-tlora-v2-1-1.8-"] - case .TLORAT3S3: - return ["firmware-tlora-t3s3-v1-"] - case .NANOG1EXPLORER: - return ["firmware-nano-g1-explorer-"] - case .STATIONG1: - return ["firmware-station-g1-"] - case .M5STACK: - return ["firmware-m5stack-core-", "firmware-m5stack-coreink-"] - case .HELTECV3: - return ["firmware-heltec-v3-"] - case .HELTECWSLV3: - return ["firmware-heltec-wsl-v3-"] - case .NANOG2ULTRA: - return ["firmware-nano-g2-ultra-"] - case .RAK11310: - return ["firmware-rak11310-"] - case .RPIPICO: - return ["firmware-pico-"] - case .HELTECWIRELESSTRACKER: - return ["firmware-heltec-wireless-tracker-"] - case .HELTECWIRELESSPAPER: - return ["firmware-heltec-wireless-paper-"] - case .TDECK: - return ["firmware-t-echo-"] - case .TWATCHS3: - return ["firmware-t-watch-s3-"] - } - - } - func platform() -> HardwarePlatforms { - - switch self { - - case .UNSET: - return HardwarePlatforms.none - case .TLORAV2: - return HardwarePlatforms.esp32 - case .TLORAV1: - return HardwarePlatforms.esp32 - case .TLORAV211P6: - return HardwarePlatforms.esp32 - case .TBEAM: - return HardwarePlatforms.esp32 - case .HELTECV20: - return HardwarePlatforms.esp32 - case .TBEAMV0P7: - return HardwarePlatforms.esp32 - case .TECHO: - return HardwarePlatforms.nrf52 - case .TLORAV11P3: - return HardwarePlatforms.esp32 - case .RAK4631: - return HardwarePlatforms.nrf52 - case .HELTECV21: - return HardwarePlatforms.esp32 - case .HELTECV1: - return HardwarePlatforms.esp32 - case .DIYV1: - return HardwarePlatforms.esp32 - case .LILYGOTBEAMS3CORE: - return HardwarePlatforms.esp32 - case .RAK11200: - return HardwarePlatforms.esp32 - case .NANOG1: - return HardwarePlatforms.esp32 - case .TLORAV211P8: - return HardwarePlatforms.esp32 - case .TLORAT3S3: - return HardwarePlatforms.esp32 - case .NANOG1EXPLORER: - return HardwarePlatforms.esp32 - case .STATIONG1: - return HardwarePlatforms.esp32 - case .M5STACK: - return HardwarePlatforms.esp32 - case .HELTECV3: - return HardwarePlatforms.esp32 - case .HELTECWSLV3: - return HardwarePlatforms.esp32 - case .NANOG2ULTRA: - return HardwarePlatforms.nrf52 - case .RAK11310: - return HardwarePlatforms.piPico - case .RPIPICO: - return HardwarePlatforms.piPico - case .HELTECWIRELESSTRACKER: - return HardwarePlatforms.esp32 - case .HELTECWIRELESSPAPER: - return HardwarePlatforms.esp32 - case .TDECK: - return HardwarePlatforms.esp32 - case .TWATCHS3: - return HardwarePlatforms.esp32 - } - } - func protoEnumValue() -> HardwareModel { - - switch self { - - case .UNSET: - return HardwareModel.unset - case .TLORAV2: - return HardwareModel.tloraV2 - case .TLORAV1: - return HardwareModel.tloraV1 - case .TLORAV211P6: - return HardwareModel.tloraV211P6 - case .TBEAM: - return HardwareModel.tbeam - case .HELTECV20: - return HardwareModel.heltecV20 - case .TBEAMV0P7: - return HardwareModel.tbeamV0P7 - case .TECHO: - return HardwareModel.tEcho - case .TLORAV11P3: - return HardwareModel.tloraV11P3 - case .RAK4631: - return HardwareModel.rak4631 - case .HELTECV21: - return HardwareModel.heltecV21 - case .HELTECV1: - return HardwareModel.heltecV1 - case .DIYV1: - return HardwareModel.diyV1 - case .LILYGOTBEAMS3CORE: - return HardwareModel.lilygoTbeamS3Core - case .RAK11200: - return HardwareModel.rak11200 - case .NANOG1: - return HardwareModel.nanoG1 - case .TLORAV211P8: - return HardwareModel.tloraV211P8 - case .TLORAT3S3: - return HardwareModel.tloraT3S3 - case .NANOG1EXPLORER: - return HardwareModel.nanoG1Explorer - case .STATIONG1: - return HardwareModel.stationG1 - case .M5STACK: - return HardwareModel.m5Stack - case .HELTECV3: - return HardwareModel.heltecV3 - case .HELTECWSLV3: - return HardwareModel.heltecWslV3 - case .NANOG2ULTRA: - return HardwareModel.nanoG2Ultra - case .RAK11310: - return HardwareModel.rak11310 - case .RPIPICO: - return HardwareModel.rpiPico - case .HELTECWIRELESSTRACKER: - return HardwareModel.heltecWirelessTracker - case .HELTECWIRELESSPAPER: - return HardwareModel.heltecWirelessPaper - case .TDECK: - return HardwareModel.tDeck - case .TWATCHS3: - return HardwareModel.tWatchS3 - } - } -} - -enum HardwarePlatforms: String, CaseIterable, Identifiable { - case none - case esp32 - case nrf52 - case stm32 - case piPico - case linux - var id: String { self.rawValue } - var description: String { - switch self { - case .none: - return "None" - case .esp32: - return "Expressif ESP 32" - case .nrf52: - return "Nordic NRF52" - case .stm32: - return "ARM STM 32" - case .piPico: - return "Raspberrry Pi Pico" - case .linux: - return "Linux" - } - } -} diff --git a/Meshtastic/Enums/SerialConfigEnums.swift b/Meshtastic/Enums/SerialConfigEnums.swift index 770ddb19..046860f9 100644 --- a/Meshtastic/Enums/SerialConfigEnums.swift +++ b/Meshtastic/Enums/SerialConfigEnums.swift @@ -111,6 +111,7 @@ enum SerialModeTypes: Int, CaseIterable, Identifiable { case proto = 2 case txtmsg = 3 case nmea = 4 + case caltopo = 5 var id: Int { self.rawValue } var description: String { @@ -125,6 +126,8 @@ enum SerialModeTypes: Int, CaseIterable, Identifiable { return "serial.mode.txtmsg".localized case .nmea: return "serial.mode.nmea".localized + case .caltopo: + return "serial.mode.caltopo".localized } } func protoEnumValue() -> ModuleConfig.SerialConfig.Serial_Mode { @@ -141,6 +144,8 @@ enum SerialModeTypes: Int, CaseIterable, Identifiable { return ModuleConfig.SerialConfig.Serial_Mode.textmsg case .nmea: return ModuleConfig.SerialConfig.Serial_Mode.nmea + case .caltopo: + return ModuleConfig.SerialConfig.Serial_Mode.caltopo } } } diff --git a/Meshtastic/Extensions/Bundle.swift b/Meshtastic/Extensions/Bundle.swift new file mode 100644 index 00000000..b3bd85bf --- /dev/null +++ b/Meshtastic/Extensions/Bundle.swift @@ -0,0 +1,22 @@ +// +// Bundle.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 12/25/23. +// + +import Foundation + +extension Bundle { + public var appName: String { getInfo("CFBundleName") } + public var displayName: String { getInfo("CFBundleDisplayName") } + public var language: String { getInfo("CFBundleDevelopmentRegion") } + public var identifier: String { getInfo("CFBundleIdentifier") } + public var copyright: String { getInfo("NSHumanReadableCopyright").replacingOccurrences(of: "\\\\n", with: "\n") } + + public var appBuild: String { getInfo("CFBundleVersion") } + public var appVersionLong: String { getInfo("CFBundleShortVersionString") } + //public var appVersionShort: String { getInfo("CFBundleShortVersion") } + + fileprivate func getInfo(_ str: String) -> String { infoDictionary?[str] as? String ?? "⚠️" } +} diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index fd302375..0e7e4ae8 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -532,7 +532,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate tryClearExistingChannels() } // NodeInfo - if decodedInfo.nodeInfo.num > 0 && !invalidVersion { + if decodedInfo.nodeInfo.num > 0 {// && !invalidVersion { nowKnown = true let nodeInfo = nodeInfoPacket(nodeInfo: decodedInfo.nodeInfo, channel: decodedInfo.packet.channel, context: context!) @@ -690,6 +690,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if let neighborInfo = try? NeighborInfo(serializedData: decodedInfo.packet.decoded.payload) { MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App App UNHANDLED \(neighborInfo)") } + case .paxcounterApp: + MeshLogger.log("🕸️ MESH PACKET received for PAX Counter App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") case .UNRECOGNIZED: MeshLogger.log("🕸️ MESH PACKET received for Other App UNHANDLED \(try! decodedInfo.packet.jsonString())") case .max: @@ -757,18 +759,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Use context to pass the radio name with the timer // Use a RunLoop to prevent the timer from running on the main UI thread if UserDefaults.provideLocation { - let interval = UserDefaults.provideLocationInterval > 0 ? UserDefaults.provideLocationInterval : 30 - if positionTimer != nil { - } + let interval = UserDefaults.provideLocationInterval >= 10 ? UserDefaults.provideLocationInterval : 30 positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval(interval), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true) if positionTimer != nil { RunLoop.current.add(positionTimer!, forMode: .common) } } - return } - case FROMNUM_UUID: print("🗞️ BLE (Notify) characteristic, value will be read next") @@ -962,7 +960,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return success } - public func sendPosition(destNum: Int64, wantResponse: Bool) -> Bool { + public func sendPosition(channel: Int32, destNum: Int64, wantResponse: Bool) -> Bool { var success = false let fromNodeNum = connectedPeripheral.num var positionPacket = Position() @@ -1016,6 +1014,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var meshPacket = MeshPacket() meshPacket.to = UInt32(destNum) + meshPacket.channel = UInt32(channel) meshPacket.from = UInt32(fromNodeNum) var dataMessage = DataMessage() dataMessage.payload = try! positionPacket.serializedData() @@ -1040,7 +1039,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if connectedPeripheral != nil { // Send a position out to the mesh if "share location with the mesh" is enabled in settings if UserDefaults.provideLocation { - let _ = sendPosition(destNum: connectedPeripheral.num, wantResponse: false) + let _ = sendPosition(channel: 0, destNum: connectedPeripheral.num, wantResponse: false) } } } diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 7e172139..4742914a 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -8,20 +8,23 @@ import SwiftUI import CoreLocation - // Shared state that manages the `CLLocationManager` and `CLBackgroundActivitySession`. @available(iOS 17.0, macOS 14.0, *) @MainActor class LocationsHandler: ObservableObject { - + static let shared = LocationsHandler() // Create a single, shared instance of the object. private let manager: CLLocationManager private var background: CLBackgroundActivitySession? - var locationsArray: [CLLocation] var enableSmartPosition: Bool - //@Published var lastLocation = CLLocation() + @Published var locationsArray: [CLLocation] @Published var isStationary = false @Published var count = 0 + @Published var isRecording = false + @Published var isRecordingPaused = false + @Published var recordingStarted: Date? + @Published var distanceTraveled = 0.0 + @Published var elevationGain = 0.0 @Published var updatesStarted: Bool = UserDefaults.standard.bool(forKey: "liveUpdatesStarted") { @@ -55,7 +58,7 @@ import CoreLocation if !self.updatesStarted { break } // End location updates by breaking out of the loop. if let loc = update.location { self.isStationary = update.isStationary - self.count += 1 + var locationAdded: Bool if enableSmartPosition { locationAdded = addLocation(loc) @@ -64,8 +67,8 @@ import CoreLocation locationsArray.append(loc) locationAdded = true } - if !locationAdded { - //print("Bad Location \(self.count): \(loc)") + if locationAdded { + self.count += 1 } } } @@ -95,6 +98,16 @@ import CoreLocation print("Bad Location \(self.count): Horizontal Accuracy: \(location.horizontalAccuracy) \(location)") return false } + if isRecording { + if let lastLocation = locationsArray.last { + let distance = location.distance(from: lastLocation) + let gain = location.altitude - lastLocation.altitude + distanceTraveled += distance + if gain > 0 { + elevationGain += gain + } + } + } locationsArray.append(location) return true } @@ -103,7 +116,7 @@ import CoreLocation static var satsInView: Int { var sats = 0 - if let newLocation = shared.locationsArray.last{ + if let newLocation = shared.locationsArray.last { sats = 1 if newLocation.verticalAccuracy > 0 { sats = 4 diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 92139355..83cb25db 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -194,7 +194,6 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, context: NS } if fetchedNode.count > 0 { let newMetadata = DeviceMetadataEntity(context: context) - newMetadata.firmwareVersion = metadata.firmwareVersion newMetadata.deviceStateVersion = Int32(metadata.deviceStateVersion) newMetadata.canShutdown = metadata.canShutdown newMetadata.hasWifi = metadata.hasWifi_p @@ -290,7 +289,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje position.longitudeI = nodeInfo.position.longitudeI position.altitude = nodeInfo.position.altitude position.satsInView = Int32(nodeInfo.position.satsInView) - position.speed = Int32(nodeInfo.position.groundSpeed) + position.speed = Int32(nodeInfo.position.groundSpeed * UInt32(3.6)) position.heading = Int32(nodeInfo.position.groundTrack) position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time))) var newPostions = [PositionEntity]() diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 9d09b84f..550e3369 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV22.xcdatamodel + MeshtasticDataModelV 23.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 23.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 23.xcdatamodel/contents new file mode 100644 index 00000000..8085cfa4 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 23.xcdatamodel/contents @@ -0,0 +1,409 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index f39420e9..0794cbea 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -250,7 +250,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) position.longitudeI = positionMessage.longitudeI position.altitude = positionMessage.altitude position.satsInView = Int32(positionMessage.satsInView) - position.speed = Int32(positionMessage.groundSpeed) + position.speed = Int32(positionMessage.groundSpeed * UInt32(3.6)) position.heading = Int32(positionMessage.groundTrack) if positionMessage.timestamp != 0 { position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.timestamp))) diff --git a/Meshtastic/Protobufs/meshtastic/admin.pb.swift b/Meshtastic/Protobufs/meshtastic/admin.pb.swift index 75591fee..ab96c32c 100644 --- a/Meshtastic/Protobufs/meshtastic/admin.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/admin.pb.swift @@ -224,6 +224,17 @@ struct AdminMessage { set {payloadVariant = .getNodeRemoteHardwarePinsResponse(newValue)} } + /// + /// Enter (UF2) DFU mode + /// Only implemented on NRF52 currently + var enterDfuModeRequest: Bool { + get { + if case .enterDfuModeRequest(let v)? = payloadVariant {return v} + return false + } + set {payloadVariant = .enterDfuModeRequest(newValue)} + } + /// /// Set the owner for this node var setOwner: User { @@ -445,6 +456,10 @@ struct AdminMessage { /// Respond with the mesh's nodes with their available gpio pins for RemoteHardware module use case getNodeRemoteHardwarePinsResponse(NodeRemoteHardwarePinsResponse) /// + /// Enter (UF2) DFU mode + /// Only implemented on NRF52 currently + case enterDfuModeRequest(Bool) + /// /// Set the owner for this node case setOwner(User) /// @@ -579,6 +594,10 @@ struct AdminMessage { guard case .getNodeRemoteHardwarePinsResponse(let l) = lhs, case .getNodeRemoteHardwarePinsResponse(let r) = rhs else { preconditionFailure() } return l == r }() + case (.enterDfuModeRequest, .enterDfuModeRequest): return { + guard case .enterDfuModeRequest(let l) = lhs, case .enterDfuModeRequest(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 @@ -763,6 +782,10 @@ struct AdminMessage { /// /// TODO: REPLACE case detectionsensorConfig // = 11 + + /// + /// TODO: REPLACE + case paxcounterConfig // = 12 case UNRECOGNIZED(Int) init() { @@ -783,6 +806,7 @@ struct AdminMessage { case 9: self = .neighborinfoConfig case 10: self = .ambientlightingConfig case 11: self = .detectionsensorConfig + case 12: self = .paxcounterConfig default: self = .UNRECOGNIZED(rawValue) } } @@ -801,6 +825,7 @@ struct AdminMessage { case .neighborinfoConfig: return 9 case .ambientlightingConfig: return 10 case .detectionsensorConfig: return 11 + case .paxcounterConfig: return 12 case .UNRECOGNIZED(let i): return i } } @@ -840,6 +865,7 @@ extension AdminMessage.ModuleConfigType: CaseIterable { .neighborinfoConfig, .ambientlightingConfig, .detectionsensorConfig, + .paxcounterConfig, ] } @@ -926,6 +952,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 18: .standard(proto: "set_ham_mode"), 19: .standard(proto: "get_node_remote_hardware_pins_request"), 20: .standard(proto: "get_node_remote_hardware_pins_response"), + 21: .standard(proto: "enter_dfu_mode_request"), 32: .standard(proto: "set_owner"), 33: .standard(proto: "set_channel"), 34: .standard(proto: "set_config"), @@ -1141,6 +1168,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .getNodeRemoteHardwarePinsResponse(v) } }() + case 21: try { + var v: Bool? + try decoder.decodeSingularBoolField(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .enterDfuModeRequest(v) + } + }() case 32: try { var v: User? var hadOneofValue = false @@ -1368,6 +1403,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .getNodeRemoteHardwarePinsResponse(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 20) }() + case .enterDfuModeRequest?: try { + guard case .enterDfuModeRequest(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularBoolField(value: v, fieldNumber: 21) + }() case .setOwner?: try { guard case .setOwner(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 32) @@ -1466,6 +1505,7 @@ extension AdminMessage.ModuleConfigType: SwiftProtobuf._ProtoNameProviding { 9: .same(proto: "NEIGHBORINFO_CONFIG"), 10: .same(proto: "AMBIENTLIGHTING_CONFIG"), 11: .same(proto: "DETECTIONSENSOR_CONFIG"), + 12: .same(proto: "PAXCOUNTER_CONFIG"), ] } diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index ef91027c..5fc6a263 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -387,10 +387,7 @@ struct Config { var gpsUpdateInterval: UInt32 = 0 /// - /// How long should we try to get our position during each gps_update_interval attempt? (in seconds) - /// Or if zero, use the default of 30 seconds. - /// If we don't get a new gps fix in that time, the gps will be put into sleep until the next gps_update_rate - /// window. + /// Deprecated in favor of using smart / regular broadcast intervals as implicit attempt time var gpsAttemptTime: UInt32 = 0 /// @@ -1115,6 +1112,14 @@ struct Config { /// /// Ukraine 868mhz case ua868 // = 15 + + /// + /// Malaysia 433mhz + case my433 // = 16 + + /// + /// Malaysia 919mhz + case my919 // = 17 case UNRECOGNIZED(Int) init() { @@ -1139,6 +1144,8 @@ struct Config { case 13: self = .lora24 case 14: self = .ua433 case 15: self = .ua868 + case 16: self = .my433 + case 17: self = .my919 default: self = .UNRECOGNIZED(rawValue) } } @@ -1161,6 +1168,8 @@ struct Config { case .lora24: return 13 case .ua433: return 14 case .ua868: return 15 + case .my433: return 16 + case .my919: return 17 case .UNRECOGNIZED(let i): return i } } @@ -1420,6 +1429,8 @@ extension Config.LoRaConfig.RegionCode: CaseIterable { .lora24, .ua433, .ua868, + .my433, + .my919, ] } @@ -2325,6 +2336,8 @@ extension Config.LoRaConfig.RegionCode: SwiftProtobuf._ProtoNameProviding { 13: .same(proto: "LORA_24"), 14: .same(proto: "UA_433"), 15: .same(proto: "UA_868"), + 16: .same(proto: "MY_433"), + 17: .same(proto: "MY_919"), ] } diff --git a/Meshtastic/Protobufs/meshtastic/localonly.pb.swift b/Meshtastic/Protobufs/meshtastic/localonly.pb.swift index 1d96373e..6e215220 100644 --- a/Meshtastic/Protobufs/meshtastic/localonly.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/localonly.pb.swift @@ -255,6 +255,17 @@ struct LocalModuleConfig { /// Clears the value of `detectionSensor`. Subsequent reads from it will return its default value. mutating func clearDetectionSensor() {_uniqueStorage()._detectionSensor = nil} + /// + /// Paxcounter Config + var paxcounter: ModuleConfig.PaxcounterConfig { + get {return _storage._paxcounter ?? ModuleConfig.PaxcounterConfig()} + set {_uniqueStorage()._paxcounter = newValue} + } + /// Returns true if `paxcounter` has been explicitly set. + var hasPaxcounter: Bool {return _storage._paxcounter != nil} + /// Clears the value of `paxcounter`. Subsequent reads from it will return its default value. + mutating func clearPaxcounter() {_uniqueStorage()._paxcounter = 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 @@ -419,6 +430,7 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem 11: .standard(proto: "neighbor_info"), 12: .standard(proto: "ambient_lighting"), 13: .standard(proto: "detection_sensor"), + 14: .same(proto: "paxcounter"), 8: .same(proto: "version"), ] @@ -435,6 +447,7 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem var _neighborInfo: ModuleConfig.NeighborInfoConfig? = nil var _ambientLighting: ModuleConfig.AmbientLightingConfig? = nil var _detectionSensor: ModuleConfig.DetectionSensorConfig? = nil + var _paxcounter: ModuleConfig.PaxcounterConfig? = nil var _version: UInt32 = 0 static let defaultInstance = _StorageClass() @@ -454,6 +467,7 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem _neighborInfo = source._neighborInfo _ambientLighting = source._ambientLighting _detectionSensor = source._detectionSensor + _paxcounter = source._paxcounter _version = source._version } } @@ -486,6 +500,7 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem case 11: try { try decoder.decodeSingularMessageField(value: &_storage._neighborInfo) }() case 12: try { try decoder.decodeSingularMessageField(value: &_storage._ambientLighting) }() case 13: try { try decoder.decodeSingularMessageField(value: &_storage._detectionSensor) }() + case 14: try { try decoder.decodeSingularMessageField(value: &_storage._paxcounter) }() default: break } } @@ -537,6 +552,9 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem try { if let v = _storage._detectionSensor { try visitor.visitSingularMessageField(value: v, fieldNumber: 13) } }() + try { if let v = _storage._paxcounter { + try visitor.visitSingularMessageField(value: v, fieldNumber: 14) + } }() } try unknownFields.traverse(visitor: &visitor) } @@ -558,6 +576,7 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if _storage._neighborInfo != rhs_storage._neighborInfo {return false} if _storage._ambientLighting != rhs_storage._ambientLighting {return false} if _storage._detectionSensor != rhs_storage._detectionSensor {return false} + if _storage._paxcounter != rhs_storage._paxcounter {return false} if _storage._version != rhs_storage._version {return false} return true } diff --git a/Meshtastic/Protobufs/meshtastic/module_config.pb.swift b/Meshtastic/Protobufs/meshtastic/module_config.pb.swift index b1720e41..3aee0200 100644 --- a/Meshtastic/Protobufs/meshtastic/module_config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/module_config.pb.swift @@ -204,6 +204,16 @@ struct ModuleConfig { set {payloadVariant = .detectionSensor(newValue)} } + /// + /// TODO: REPLACE + var paxcounter: ModuleConfig.PaxcounterConfig { + get { + if case .paxcounter(let v)? = payloadVariant {return v} + return ModuleConfig.PaxcounterConfig() + } + set {payloadVariant = .paxcounter(newValue)} + } + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -245,6 +255,9 @@ struct ModuleConfig { /// /// TODO: REPLACE case detectionSensor(ModuleConfig.DetectionSensorConfig) + /// + /// TODO: REPLACE + case paxcounter(ModuleConfig.PaxcounterConfig) #if !swift(>=4.1) static func ==(lhs: ModuleConfig.OneOf_PayloadVariant, rhs: ModuleConfig.OneOf_PayloadVariant) -> Bool { @@ -300,6 +313,10 @@ struct ModuleConfig { guard case .detectionSensor(let l) = lhs, case .detectionSensor(let r) = rhs else { preconditionFailure() } return l == r }() + case (.paxcounter, .paxcounter): return { + guard case .paxcounter(let l) = lhs, case .paxcounter(let r) = rhs else { preconditionFailure() } + return l == r + }() default: return false } } @@ -551,6 +568,24 @@ struct ModuleConfig { init() {} } + /// + /// Config for the Paxcounter Module + struct PaxcounterConfig { + // 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. + + /// + /// Enable the Paxcounter Module + var enabled: Bool = false + + var paxcounterUpdateInterval: UInt32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + } + /// /// Serial Config struct SerialConfig { @@ -1177,6 +1212,7 @@ extension ModuleConfig.NeighborInfoConfig: @unchecked Sendable {} extension ModuleConfig.DetectionSensorConfig: @unchecked Sendable {} extension ModuleConfig.AudioConfig: @unchecked Sendable {} extension ModuleConfig.AudioConfig.Audio_Baud: @unchecked Sendable {} +extension ModuleConfig.PaxcounterConfig: @unchecked Sendable {} extension ModuleConfig.SerialConfig: @unchecked Sendable {} extension ModuleConfig.SerialConfig.Serial_Baud: @unchecked Sendable {} extension ModuleConfig.SerialConfig.Serial_Mode: @unchecked Sendable {} @@ -1217,6 +1253,7 @@ extension ModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 10: .standard(proto: "neighbor_info"), 11: .standard(proto: "ambient_lighting"), 12: .standard(proto: "detection_sensor"), + 13: .same(proto: "paxcounter"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1381,6 +1418,19 @@ extension ModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .detectionSensor(v) } }() + case 13: try { + var v: ModuleConfig.PaxcounterConfig? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .paxcounter(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .paxcounter(v) + } + }() default: break } } @@ -1440,6 +1490,10 @@ extension ModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .detectionSensor(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 12) }() + case .paxcounter?: try { + guard case .paxcounter(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 13) + }() case nil: break } try unknownFields.traverse(visitor: &visitor) @@ -1770,6 +1824,44 @@ extension ModuleConfig.AudioConfig.Audio_Baud: SwiftProtobuf._ProtoNameProviding ] } +extension ModuleConfig.PaxcounterConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = ModuleConfig.protoMessageName + ".PaxcounterConfig" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "enabled"), + 2: .standard(proto: "paxcounter_update_interval"), + ] + + 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) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.paxcounterUpdateInterval) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.enabled != false { + try visitor.visitSingularBoolField(value: self.enabled, fieldNumber: 1) + } + if self.paxcounterUpdateInterval != 0 { + try visitor.visitSingularUInt32Field(value: self.paxcounterUpdateInterval, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: ModuleConfig.PaxcounterConfig, rhs: ModuleConfig.PaxcounterConfig) -> Bool { + if lhs.enabled != rhs.enabled {return false} + if lhs.paxcounterUpdateInterval != rhs.paxcounterUpdateInterval {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension ModuleConfig.SerialConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = ModuleConfig.protoMessageName + ".SerialConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ diff --git a/Meshtastic/Protobufs/meshtastic/portnums.pb.swift b/Meshtastic/Protobufs/meshtastic/portnums.pb.swift index 6c32de74..319d48f9 100644 --- a/Meshtastic/Protobufs/meshtastic/portnums.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/portnums.pb.swift @@ -118,6 +118,11 @@ enum PortNum: SwiftProtobuf.Enum { /// ENCODING: IP Packet. Handled by the python API, firmware ignores this one and pases on. case ipTunnelApp // = 33 + /// + /// Paxcounter lib included in the firmware + /// ENCODING: protobuf + case paxcounterApp // = 34 + /// /// Provides a hardware serial interface to send and receive from the Meshtastic network. /// Connect to the RX/TX pins of a device with 38400 8N1. Packets received from the Meshtastic @@ -206,6 +211,7 @@ enum PortNum: SwiftProtobuf.Enum { case 10: self = .detectionSensorApp case 32: self = .replyApp case 33: self = .ipTunnelApp + case 34: self = .paxcounterApp case 64: self = .serialApp case 65: self = .storeForwardApp case 66: self = .rangeTestApp @@ -236,6 +242,7 @@ enum PortNum: SwiftProtobuf.Enum { case .detectionSensorApp: return 10 case .replyApp: return 32 case .ipTunnelApp: return 33 + case .paxcounterApp: return 34 case .serialApp: return 64 case .storeForwardApp: return 65 case .rangeTestApp: return 66 @@ -271,6 +278,7 @@ extension PortNum: CaseIterable { .detectionSensorApp, .replyApp, .ipTunnelApp, + .paxcounterApp, .serialApp, .storeForwardApp, .rangeTestApp, @@ -308,6 +316,7 @@ extension PortNum: SwiftProtobuf._ProtoNameProviding { 10: .same(proto: "DETECTION_SENSOR_APP"), 32: .same(proto: "REPLY_APP"), 33: .same(proto: "IP_TUNNEL_APP"), + 34: .same(proto: "PAXCOUNTER_APP"), 64: .same(proto: "SERIAL_APP"), 65: .same(proto: "STORE_FORWARD_APP"), 66: .same(proto: "RANGE_TEST_APP"), diff --git a/Meshtastic/Views/Bluetooth/Connect.swift b/Meshtastic/Views/Bluetooth/Connect.swift index d879667a..834ea5db 100644 --- a/Meshtastic/Views/Bluetooth/Connect.swift +++ b/Meshtastic/Views/Bluetooth/Connect.swift @@ -297,7 +297,9 @@ struct Connect: View { } } .onAppear(perform: { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } }) } #if canImport(ActivityKit) diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index c5207631..be590909 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -373,7 +373,7 @@ struct ChannelMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false) { + if bleManager.sendPosition(channel: Int32(channel.index), destNum: Int64(bleManager.emptyNodeNum), wantResponse: false) { print("Location Sent") } } @@ -390,7 +390,7 @@ struct ChannelMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false) { + if bleManager.sendPosition(channel: Int32(channel.index), destNum: Int64(bleManager.emptyNodeNum), wantResponse: false) { print("Location Sent") } } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 5ba9f3f8..8116f7fd 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -325,7 +325,7 @@ struct UserMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: user.num, wantResponse: true) { + if bleManager.sendPosition(channel: 0, destNum: user.num, wantResponse: true) { print("Location Sent") } } @@ -342,7 +342,7 @@ struct UserMessageList: View { focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: user.num, wantResponse: true) { + if bleManager.sendPosition(channel: 0, destNum: user.num, wantResponse: true) { print("Location Sent") } } diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index 33d2d92d..f4979cb7 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -47,12 +47,12 @@ struct PositionLog: View { } TableColumn("Speed") { position in let speed = Measurement(value: Double(position.speed), unit: UnitSpeed.kilometersPerHour) - Text(speed.formatted()) + Text(speed.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0))))) } TableColumn("Heading") { position in let degrees = Angle.degrees(Double(position.heading)) let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees) - Text("\(heading.formatted())") + Text(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0))))) } TableColumn("SNR") { position in Text("\(String(format: "%.2f", position.snr)) dB") diff --git a/Meshtastic/Views/Settings/About.swift b/Meshtastic/Views/Settings/About.swift index b395a9c0..9bcb5b72 100644 --- a/Meshtastic/Views/Settings/About.swift +++ b/Meshtastic/Views/Settings/About.swift @@ -17,11 +17,32 @@ struct AboutMeshtastic: View { List { Section(header: Text("What is Meshtastic?")) { - Text("An open source, off-grid, decentralized, mesh network built to run on affordable, low-power devices.") + Text("An open source, off-grid, decentralized, mesh network that runs on affordable, low-power radios.") .font(.title3) } Section(header: Text("Apple Apps")) { + + if locale.region?.identifier ?? "US" == "US" { + HStack { + Image("SOLAR_NODE") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 75) + .cornerRadius(5) + .padding() + VStack(alignment: .leading) { + Link("Buy Complete Radios", destination: URL(string: "http://garthvh.com")!) + .font(.title2) + Text("Get custom waterproof solar and detection sensor router nodes, aluminium desktop nodes and rugged handsets.") + .font(.callout) + } + } + } + Link("Sponsor App Development", destination: URL(string: "https://github.com/sponsors/garthvh")!) + .font(.title2) + Link("GitHub Repository", destination: URL(string: "https://github.com/meshtastic/Meshtastic-Apple")!) + .font(.title2) Button("Review the app") { if let scene = UIApplication.shared.connectedScenes .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { @@ -29,17 +50,14 @@ struct AboutMeshtastic: View { } } .font(.title2) - Link("Sponsor App Development", destination: URL(string: "https://github.com/sponsors/garthvh")!) - .font(.title2) - Link("GitHub Repository", destination: URL(string: "https://github.com/meshtastic/Meshtastic-Apple")!) - .font(.title2) - } - if locale.region?.identifier ?? "no locale" == "US" { - Section(header: Text("Get Devices")) { - Link("Buy Complete Radios", destination: URL(string: "http://garthvh.com")!) - .font(.title2) - } + + Text("Version: \(Bundle.main.appVersionLong) (\(Bundle.main.appBuild)) ") + + Text(Bundle.main.copyright) + .font(.system(size: 10, weight: .thin)) + .multilineTextAlignment(.center) } + Section(header: Text("Project information")) { Link("Website", destination: URL(string: "https://meshtastic.org")!) .font(.title2) diff --git a/Meshtastic/Views/Settings/AdminMessageList.swift b/Meshtastic/Views/Settings/AdminMessageList.swift index 190ff683..fc4fa5f8 100644 --- a/Meshtastic/Views/Settings/AdminMessageList.swift +++ b/Meshtastic/Views/Settings/AdminMessageList.swift @@ -72,7 +72,9 @@ struct AdminMessageList: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } } } } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index f42015fb..ba705afc 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -143,7 +143,13 @@ struct AppSettings: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if provideLocationInterval <= 0 { + provideLocationInterval = 30 + UserDefaults.provideLocationInterval = provideLocationInterval + } + if self.bleManager.context == nil { + self.bleManager.context = context + } } .onChange(of: blockRangeTest) { newBlockRangeTest in UserDefaults.blockRangeTest = newBlockRangeTest diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 737a90d3..15b4a508 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -285,7 +285,9 @@ struct Channels: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } } } } diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index ed79dc5a..8eb93d6e 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -137,7 +137,9 @@ struct BluetoothConfig: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } setBluetoothValues() // Need to request a BluetoothConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.bluetoothConfig == nil { diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 675ef69e..674c951f 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -103,7 +103,7 @@ struct DeviceConfig: View { } Section(header: Text("GPIO")) { Picker("Button GPIO", selection: $buttonGPIO) { - ForEach(0..<46) { + ForEach(0..<48) { if $0 == 0 { Text("unset") } else { @@ -113,7 +113,7 @@ struct DeviceConfig: View { } .pickerStyle(DefaultPickerStyle()) Picker("Buzzer GPIO", selection: $buzzerGPIO) { - ForEach(0..<46) { + ForEach(0..<48) { if $0 == 0 { Text("unset") } else { @@ -232,7 +232,9 @@ struct DeviceConfig: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } setDeviceValues() // Need to request a LoRaConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.deviceConfig == nil { diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index f600dda3..c102897c 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -192,7 +192,9 @@ struct DisplayConfig: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } setDisplayValues() // Need to request a LoRaConfig from the remote node before allowing changes diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index dccef435..ceb142e8 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -246,10 +246,10 @@ struct LoRaConfig: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } setLoRaValues() - // Need to request a LoRaConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.loRaConfig == nil { print("empty lora config") @@ -314,6 +314,11 @@ struct LoRaConfig: View { if newTxPower != node!.loRaConfig!.txPower { hasChanges = true } } } + .onChange(of: txEnabled) { newTxEnabled in + if node != nil && node!.loRaConfig != nil { + if newTxEnabled != node!.loRaConfig!.txEnabled { hasChanges = true } + } + } } func setLoRaValues() { self.hopLimit = Int(node?.loRaConfig?.hopLimit ?? 3) diff --git a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift index bd59c988..affc872f 100644 --- a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -130,7 +130,9 @@ struct AmbientLightingConfig: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } setAmbientLightingConfigValue() // Need to request a Ambient Lighting Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.ambientLightingConfig == nil { diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index 42a83ed3..ded4f262 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -124,7 +124,7 @@ struct CannedMessagesConfig: View { .disabled(configPreset > 0) Section(header: Text("Inputs")) { Picker("Pin A", selection: $inputbrokerPinA) { - ForEach(0..<46) { + ForEach(0..<48) { if $0 == 0 { Text("unset") } else { @@ -136,7 +136,7 @@ struct CannedMessagesConfig: View { Text("GPIO pin for rotary encoder A port.") .font(.caption) Picker("Pin B", selection: $inputbrokerPinB) { - ForEach(0..<46) { + ForEach(0..<48) { if $0 == 0 { Text("unset") } else { @@ -148,7 +148,7 @@ struct CannedMessagesConfig: View { Text("GPIO pin for rotary encoder B port.") .font(.caption) Picker("Press Pin", selection: $inputbrokerPinPress) { - ForEach(0..<46) { + ForEach(0..<48) { if $0 == 0 { Text("unset") } else { @@ -264,9 +264,10 @@ struct CannedMessagesConfig: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } setCannedMessagesValues() - // Need to request a CannedMessagesModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.cannedMessageConfig == nil { print("empty canned messages module config") diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index 64e98f2a..7b874aa0 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -139,7 +139,7 @@ struct DetectionSensorConfig: View { .listRowSeparator(.visible) .offset(y: -10) Picker("GPIO Pin to monitor", selection: $monitorPin) { - ForEach(0..<46) { + ForEach(0..<48) { if $0 == 0 { Text("unset") } else { @@ -240,9 +240,10 @@ struct DetectionSensorConfig: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } setDetectionSensorValues() - // Need to request a Detection Sensor Module Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.detectionSensorConfig == nil { print("empty detection sensor module config") diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index e1c0d900..512f09ae 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -99,7 +99,7 @@ struct ExternalNotificationConfig: View { Text("If enabled, the 'output' Pin will be pulled active high, disabled means active low.") .font(.caption) Picker("Output pin GPIO", selection: $output) { - ForEach(0..<46) { + ForEach(0..<48) { if $0 == 0 { Text("unset") } else { @@ -147,7 +147,7 @@ struct ExternalNotificationConfig: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Picker("Output pin buzzer GPIO ", selection: $outputBuzzer) { - ForEach(0..<46) { + ForEach(0..<48) { if $0 == 0 { Text("unset") } else { @@ -157,7 +157,7 @@ struct ExternalNotificationConfig: View { } .pickerStyle(DefaultPickerStyle()) Picker("Output pin vibra GPIO", selection: $outputVibra) { - ForEach(0..<46) { + ForEach(0..<48) { if $0 == 0 { Text("unset") } else { @@ -226,9 +226,10 @@ struct ExternalNotificationConfig: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } setExternalNotificationValues() - // Need to request a TelemetryModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.externalNotificationConfig == nil { print("empty external notification module config") diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 001ef1f7..6a6ba7e0 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -245,9 +245,10 @@ struct MQTTConfig: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } setMqttValues() - // Need to request a TelemetryModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.mqttConfig == nil { print("empty mqtt module config") diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index 746d3f51..d1de7ccf 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -117,9 +117,10 @@ struct RangeTestConfig: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } setRangeTestValues() - // Need to request a RangeTestModule Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.rangeTestConfig == nil { print("empty range test module config") diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index 4ca9bdc3..9af01055 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -115,7 +115,9 @@ struct RtttlConfig: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } setRtttLConfigValue() // Need to request a Rtttl Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && (node?.rtttlConfig == nil || node?.rtttlConfig?.ringtone?.count ?? 0 == 0) { diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index 4386b3ca..0e36460e 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -23,7 +23,10 @@ struct SerialConfig: View { @State var txd = 0 @State var baudRate = 0 @State var timeout = 0 + @State var overrideConsoleSerialPort = false @State var mode = 0 + + var body: some View { VStack { @@ -96,7 +99,7 @@ struct SerialConfig: View { Section(header: Text("GPIO")) { Picker("Receive data (rxd) GPIO pin", selection: $rxd) { - ForEach(0..<46) { + ForEach(0..<48) { if $0 == 0 { Text("unset") } else { @@ -107,7 +110,7 @@ struct SerialConfig: View { .pickerStyle(DefaultPickerStyle()) Picker("Transmit data (txd) GPIO pin", selection: $txd) { - ForEach(0..<46) { + ForEach(0..<48) { if $0 == 0 { Text("unset") } else { @@ -153,6 +156,7 @@ struct SerialConfig: View { sc.txd = UInt32(txd) sc.baud = SerialBaudRates(rawValue: baudRate)!.protoEnumValue() sc.timeout = UInt32(timeout) + sc.overrideConsoleSerialPort = overrideConsoleSerialPort sc.mode = SerialModeTypes(rawValue: mode)!.protoEnumValue() let adminMessageId = bleManager.saveSerialModuleConfig(config: sc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) @@ -176,8 +180,9 @@ struct SerialConfig: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } setSerialValues() // Need to request a SerialModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.serialConfig == nil { @@ -231,6 +236,13 @@ struct SerialConfig: View { if newTimeout != node!.serialConfig!.timeout { hasChanges = true } } } + .onChange(of: overrideConsoleSerialPort) { newOverrideConsoleSerialPort in + + if node != nil && node!.serialConfig != nil { + + if newOverrideConsoleSerialPort != node!.serialConfig!.overrideConsoleSerialPort { hasChanges = true } + } + } .onChange(of: mode) { newMode in if node != nil && node!.serialConfig != nil { @@ -248,6 +260,7 @@ struct SerialConfig: View { self.baudRate = Int(node?.serialConfig?.baudRate ?? 0) self.timeout = Int(node?.serialConfig?.timeout ?? 0) self.mode = Int(node?.serialConfig?.mode ?? 0) + self.overrideConsoleSerialPort = false // node?.serialConfig?.overrideConsoleSerialPort ?? false self.hasChanges = false } } diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForward.swift b/Meshtastic/Views/Settings/Config/Module/StoreForward.swift index cec01a74..7241442f 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForward.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForward.swift @@ -137,9 +137,10 @@ struct StoreForwardConfig: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } setDetectionSensorValues() - // Need to request a Detection Sensor Module Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.detectionSensorConfig == nil { print("empty store and forward module config") diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index f3cdb5ef..c3a48697 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -132,9 +132,10 @@ struct TelemetryConfig: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } setTelemetryValues() - // Need to request a TelemetryModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.telemetryConfig == nil { print("empty telemetry module config") diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index 34da6391..3d98e13d 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -166,9 +166,10 @@ struct NetworkConfig: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } setNetworkValues() - // Need to request a NetworkConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.networkConfig == nil { print("empty network config") diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 4b66a453..0775a16e 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -213,7 +213,7 @@ struct PositionConfig: View { if deviceGpsEnabled { Picker("GPS Receive GPIO", selection: $rxGpio) { - ForEach(0..<46) { + ForEach(0..<48) { if $0 == 0 { Text("unset") } else { @@ -223,7 +223,7 @@ struct PositionConfig: View { } .pickerStyle(DefaultPickerStyle()) Picker("GPS Transmit GPIO", selection: $txGpio) { - ForEach(0..<46) { + ForEach(0..<48) { if $0 == 0 { Text("unset") } else { @@ -233,7 +233,7 @@ struct PositionConfig: View { } .pickerStyle(DefaultPickerStyle()) Picker("GPS EN GPIO", selection: $gpsEnGpio) { - ForEach(0..<46) { + ForEach(0..<48) { if $0 == 0 { Text("unset") } else { @@ -276,7 +276,7 @@ struct PositionConfig: View { Button(buttonText) { if fixedPosition { - _ = bleManager.sendPosition(destNum: node!.num, wantResponse: true) + _ = bleManager.sendPosition(channel: 0, destNum: node?.num ?? 0, wantResponse: true) } let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) @@ -324,10 +324,10 @@ struct PositionConfig: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - - self.bleManager.context = context + if self.bleManager.context == nil { + self.bleManager.context = context + } setPositionValues() - // Need to request a PositionConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.positionConfig == nil { print("empty position config") diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 20985c3f..8878b0d9 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -2,15 +2,9 @@ // Firmware.swift // Meshtastic // -// Created by Garth Vander Houwen on 3/10/23. +// Copyright(c) by Garth Vander Houwen on 3/10/23. // -// -// About.swift -// Meshtastic -// -// Copyright(c) Garth Vander Houwen 10/6/22. -// import SwiftUI import StoreKit @@ -18,28 +12,32 @@ struct Firmware: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager var node: NodeInfoEntity? - @State var minimumVersion = "2.1.0" + @State var minimumVersion = "2.2.16" @State var version = "" - @State private var firmwareReleaseData: FirmwareRelease = FirmwareRelease() - var body: some View { - // NavigationSplitView { - NavigationStack { - let hwModel: HardwareModels = HardwareModels.allCases.first(where: { $0.rawValue == node?.user?.hwModel ?? "UNSET" }) ?? HardwareModels.UNSET - VStack(alignment: .leading) { - Text("Current Version: \(bleManager.connectedVersion)") - .font(.largeTitle) - Text("Your device supports the following firmware: ") - .font(.callout) - HStack { - ForEach(hwModel.firmwareStrings, id: \.self) { fs in - Text(fs).font(.callout) - } - } - .padding(.bottom) + @State private var currentDevice: DeviceHardware? - if hwModel.platform() == HardwarePlatforms.nrf52 { + var body: some View { + VStack { + VStack(alignment: .leading) { + let deviceString = currentDevice?.hwModelSlug.replacingOccurrences(of: "_", with: "") + + Text("Your Device Model: \(currentDevice?.displayName ?? "Unknown")") + .font(.largeTitle) + + VStack { + Image(deviceString ?? "UNSET") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 200, height: 200) + .cornerRadius(5) + } + Text("Current Firmware Version: \(bleManager.connectedVersion)") + .font(.title) + + if currentDevice?.architecture == Meshtastic.Architecture.nrf52840 { VStack(alignment: .leading) { - if hwModel == HardwareModels.RAK4631 { + /// RAK 4631 + if currentDevice?.hwModel == 9 { Text("nRF OTA Device Firmware Update App") .font(.title3) Text("You can update your Meshtastic device over bluetooth using the Nordic DFU app. This currently works for RAK NRF devices.") @@ -53,13 +51,13 @@ struct Firmware: View { .font(.callout) } } - } else if hwModel.platform() == HardwarePlatforms.esp32 { + } else if currentDevice?.architecture == Meshtastic.Architecture.esp32 { VStack(alignment: .leading) { Text("ESP32 Device Firmware Update") .font(.title3) Text("Currently the reccomended way to update ESP32 devices is using the web flasher from a chrome based browser. It does not work on mobile devices or over BLE.") .font(.caption) - Link("Web Flasher", destination: URL(string: "https://flasher.meshtastic.org")!) + Link("Web Flasher", destination: URL(string: "https://flash.meshtastic.org")!) .font(.callout) .padding(.bottom) Text("ESP 32 OTA update is a work in progress, click the button below to sent your device a reboot into ota admin message.") @@ -88,169 +86,170 @@ struct Firmware: View { .font(.title3) Text(node?.user?.hwModel ?? "UNSET") .font(.title3) - Text(hwModel.platform().description) - .font(.title3) + // Text(hwModel.platform().description) + // .font(.title3) } - }.padding() + } + .padding() VStack(alignment: .leading) { Text("Firmware Releases") .font(.title3) .padding([.leading, .trailing]) - List { - Section(header: Text("Stable")) { - ForEach(firmwareReleaseData.releases?.stable ?? [], id: \.id) { fr in - Link(destination: URL(string: fr.zipUrl ?? "")!) { - HStack { - Text(fr.title ?? "Unknown") - .font(.caption) - Spacer() - Image(systemName: "square.and.arrow.down") - .font(.title3) - } - } - } - } - Section("Alpha") { - ForEach(firmwareReleaseData.releases?.alpha ?? [], id: \.id) { fr in - Link(destination: URL(string: fr.zipUrl ?? "")!) { - HStack { - Text(fr.title ?? "Unknown") - .font(.caption) - Spacer() - Image(systemName: "square.and.arrow.down") - .font(.title3) - } - } - } - } - Section("Pull Requests") { - ForEach(firmwareReleaseData.pullRequests ?? [], id: \.id) { fr in - Link(destination: URL(string: fr.zipUrl ?? "")!) { - HStack { - Text(fr.title ?? "Unknown") - .font(.caption) - Spacer() - Image(systemName: "square.and.arrow.down") - .font(.title3) - } - } +// List { +// Section(header: Text("Stable")) { +// ForEach(firmwareReleaseData.releases?.stable ?? [], id: \.id) { fr in +// Link(destination: URL(string: fr.zipUrl ?? "")!) { +// HStack { +// Text(fr.title ?? "Unknown") +// .font(.caption) +// Spacer() +// Image(systemName: "square.and.arrow.down") +// .font(.title3) +// } +// } +// } +// } +// Section("Alpha") { +// ForEach(firmwareReleaseData.releases?.alpha ?? [], id: \.id) { fr in +// Link(destination: URL(string: fr.zipUrl ?? "")!) { +// HStack { +// Text(fr.title ?? "Unknown") +// .font(.caption) +// Spacer() +// Image(systemName: "square.and.arrow.down") +// .font(.title3) +// } +// } +// } +// } +// Section("Pull Requests") { +// ForEach(firmwareReleaseData.pullRequests ?? [], id: \.id) { fr in +// Link(destination: URL(string: fr.zipUrl ?? "")!) { +// HStack { +// Text(fr.title ?? "Unknown") +// .font(.caption) +// Spacer() +// Image(systemName: "square.and.arrow.down") +// .font(.title3) +// } +// } +// } +// } +// } + } + .padding(.bottom, 5) + .onAppear() { + Api().loadDeviceHardwareData { (hw) in + for device in hw { + print(device) + let currentHardware = node?.user?.hwModel ?? "UNSET" + let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "") + if deviceString == currentHardware { + print("Selected: \(device)") + currentDevice = device } } } +// Api().loadFirmwareReleaseData { (bks) in +// //sel = bks +// } } - .padding(.bottom, 5) - .onAppear(perform: loadData) .navigationTitle("Firmware Updates") .navigationBarTitleDisplayMode(.inline) } } - func loadData() { - guard let url = URL(string: "https://api.meshtastic.org/github/firmware/list") else { - return - } - let request = URLRequest(url: url) - URLSession.shared.dataTask(with: request) { data, _, _ in - if let data = data { - if let response_obj = try? JSONDecoder().decode(FirmwareRelease.self, from: data) { - DispatchQueue.main.async { - self.firmwareReleaseData = response_obj - } - } - } - }.resume() - } } -struct FirmwareRelease: Codable { - var releases: Releases? = Releases() - var pullRequests: [PullRequests]? = [] - enum CodingKeys: String, CodingKey { - case releases = "Releases" - case pullRequests = "Pull Requests" - } - init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - releases = try values.decodeIfPresent(Releases.self, forKey: .releases ) - pullRequests = try values.decodeIfPresent([PullRequests].self, forKey: .pullRequests ) - } - init() { - } -} - -struct Releases: Codable { - var stable: [Stable]? = [] - var alpha: [Alpha]? = [] - enum CodingKeys: String, CodingKey { - case stable = "Stable" - case alpha = "Alpha" - } - init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - stable = try values.decodeIfPresent([Stable].self, forKey: .stable ) - alpha = try values.decodeIfPresent([Alpha].self, forKey: .alpha ) - } - init() {} -} - -struct Alpha: Codable { - var id: String? - var title: String? - var pageUrl: String? - var zipUrl: String? - enum CodingKeys: String, CodingKey { - case id = "id" - case title = "title" - case pageUrl = "page_url" - case zipUrl = "zip_url" - } - init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - id = try values.decodeIfPresent(String.self, forKey: .id ) - title = try values.decodeIfPresent(String.self, forKey: .title ) - pageUrl = try values.decodeIfPresent(String.self, forKey: .pageUrl ) - zipUrl = try values.decodeIfPresent(String.self, forKey: .zipUrl ) - } - init() {} -} - -struct Stable: Codable { - var id: String? - var title: String? - var pageUrl: String? - var zipUrl: String? - enum CodingKeys: String, CodingKey { - case id = "id" - case title = "title" - case pageUrl = "page_url" - case zipUrl = "zip_url" - } - init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - id = try values.decodeIfPresent(String.self, forKey: .id ) - title = try values.decodeIfPresent(String.self, forKey: .title ) - pageUrl = try values.decodeIfPresent(String.self, forKey: .pageUrl ) - zipUrl = try values.decodeIfPresent(String.self, forKey: .zipUrl ) - } - init() {} -} - -struct PullRequests: Codable { - var id: String? - var title: String? - var pageUrl: String? - var zipUrl: String? - enum CodingKeys: String, CodingKey { - case id = "id" - case title = "title" - case pageUrl = "page_url" - case zipUrl = "zip_url" - } - init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - id = try values.decodeIfPresent(String.self, forKey: .id ) - title = try values.decodeIfPresent(String.self, forKey: .title ) - pageUrl = try values.decodeIfPresent(String.self, forKey: .pageUrl ) - zipUrl = try values.decodeIfPresent(String.self, forKey: .zipUrl ) - } - init() {} -} +//struct FirmwareRelease: Codable { +// var releases: Releases? = Releases() +// var pullRequests: [PullRequests]? = [] +// enum CodingKeys: String, CodingKey { +// case releases = "Releases" +// case pullRequests = "Pull Requests" +// } +// init(from decoder: Decoder) throws { +// let values = try decoder.container(keyedBy: CodingKeys.self) +// releases = try values.decodeIfPresent(Releases.self, forKey: .releases ) +// pullRequests = try values.decodeIfPresent([PullRequests].self, forKey: .pullRequests ) +// } +// init() { +// } +//} +// +//struct Releases: Codable { +// var stable: [Stable]? = [] +// var alpha: [Alpha]? = [] +// enum CodingKeys: String, CodingKey { +// case stable = "Stable" +// case alpha = "Alpha" +// } +// init(from decoder: Decoder) throws { +// let values = try decoder.container(keyedBy: CodingKeys.self) +// stable = try values.decodeIfPresent([Stable].self, forKey: .stable ) +// alpha = try values.decodeIfPresent([Alpha].self, forKey: .alpha ) +// } +// init() {} +//} +// +//struct Alpha: Codable { +// var id: String? +// var title: String? +// var pageUrl: String? +// var zipUrl: String? +// enum CodingKeys: String, CodingKey { +// case id = "id" +// case title = "title" +// case pageUrl = "page_url" +// case zipUrl = "zip_url" +// } +// init(from decoder: Decoder) throws { +// let values = try decoder.container(keyedBy: CodingKeys.self) +// id = try values.decodeIfPresent(String.self, forKey: .id ) +// title = try values.decodeIfPresent(String.self, forKey: .title ) +// pageUrl = try values.decodeIfPresent(String.self, forKey: .pageUrl ) +// zipUrl = try values.decodeIfPresent(String.self, forKey: .zipUrl ) +// } +// init() {} +//} +// +//struct Stable: Codable { +// var id: String? +// var title: String? +// var pageUrl: String? +// var zipUrl: String? +// enum CodingKeys: String, CodingKey { +// case id = "id" +// case title = "title" +// case pageUrl = "page_url" +// case zipUrl = "zip_url" +// } +// init(from decoder: Decoder) throws { +// let values = try decoder.container(keyedBy: CodingKeys.self) +// id = try values.decodeIfPresent(String.self, forKey: .id ) +// title = try values.decodeIfPresent(String.self, forKey: .title ) +// pageUrl = try values.decodeIfPresent(String.self, forKey: .pageUrl ) +// zipUrl = try values.decodeIfPresent(String.self, forKey: .zipUrl ) +// } +// init() {} +//} +// +//struct PullRequests: Codable { +// var id: String? +// var title: String? +// var pageUrl: String? +// var zipUrl: String? +// enum CodingKeys: String, CodingKey { +// case id = "id" +// case title = "title" +// case pageUrl = "page_url" +// case zipUrl = "zip_url" +// } +// init(from decoder: Decoder) throws { +// let values = try decoder.container(keyedBy: CodingKeys.self) +// id = try values.decodeIfPresent(String.self, forKey: .id ) +// title = try values.decodeIfPresent(String.self, forKey: .title ) +// pageUrl = try values.decodeIfPresent(String.self, forKey: .pageUrl ) +// zipUrl = try values.decodeIfPresent(String.self, forKey: .zipUrl ) +// } +// init() {} +//} diff --git a/Meshtastic/Views/Settings/FirmwareApi.swift b/Meshtastic/Views/Settings/FirmwareApi.swift new file mode 100644 index 00000000..5eb2f488 --- /dev/null +++ b/Meshtastic/Views/Settings/FirmwareApi.swift @@ -0,0 +1,63 @@ +// +// FirmwareApi.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 12/27/23. +// + +import Foundation + +//struct DeviceHardware: Codable { +// var hwModel: Int +// var hwModelSlug: String +// var platformioTarget: String +// var activelySupported: Bool +// var displayName: String +//} + +struct DeviceHardware: Codable { + let hwModel: Int + let hwModelSlug, platformioTarget: String + let architecture: Architecture + let activelySupported: Bool + let displayName: String +} + +enum Architecture: String, Codable { + case esp32 = "esp32" + case esp32C3 = "esp32-c3" + case esp32S3 = "esp32-s3" + case nrf52840 = "nrf52840" + case rp2040 = "rp2040" +} + +class Api : ObservableObject{ +// @Published var devices = [DeviceHardware]() + + func loadDeviceHardwareData(completion:@escaping ([DeviceHardware]) -> ()) { + guard let url = URL(string: "https://api.meshtastic.org/resource/deviceHardware") else { + print("Invalid url...") + return + } + URLSession.shared.dataTask(with: url) { data, response, error in + let deviceHardware = try! JSONDecoder().decode([DeviceHardware].self, from: data!) + //print(deviceHardware) + DispatchQueue.main.async { + completion(deviceHardware) + } + }.resume() + } +// func loadFirmwareReleaseData(completion:@escaping ([FirmwareRelease]) -> ()) { +// guard let url = URL(string: "https://api.meshtastic.org/github/firmware/list") else { +// print("Invalid url...") +// return +// } +// URLSession.shared.dataTask(with: url) { data, response, error in +// let deviceHardware = try! JSONDecoder().decode([FirmwareRelease].self, from: data!) +// print(deviceHardware) +// DispatchQueue.main.async { +// completion(deviceHardware) +// } +// }.resume() +// } +} diff --git a/Meshtastic/Views/Settings/GPSStatus.swift b/Meshtastic/Views/Settings/GPSStatus.swift index 931a7167..b9e18679 100644 --- a/Meshtastic/Views/Settings/GPSStatus.swift +++ b/Meshtastic/Views/Settings/GPSStatus.swift @@ -11,33 +11,37 @@ import CoreLocation @available(iOS 17.0, macOS 14.0, *) struct GPSStatus: View { + var largeFont: Font = .footnote + var smallFont: Font = .caption2 + @ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared var body: some View { if let newLocation = locationsHandler.locationsArray.last { - let horizontalAccuracy = Measurement(value: newLocation.horizontalAccuracy, unit: UnitLength.meters) - let verticalAccuracy = Measurement(value: newLocation.verticalAccuracy, unit: UnitLength.meters) - let altitiude = Measurement(value: newLocation.altitude, unit: UnitLength.meters) - let speed = Measurement(value: newLocation.speed, unit: UnitSpeed.kilometersPerHour) - let speedAccuracy = Measurement(value: newLocation.speedAccuracy, unit: UnitSpeed.metersPerSecond) - let courseAccuracy = Measurement(value: newLocation.courseAccuracy, unit: UnitAngle.degrees) + let horizontalAccuracy = Measurement(value: newLocation.horizontalAccuracy, unit: UnitLength.meters) + let verticalAccuracy = Measurement(value: newLocation.verticalAccuracy, unit: UnitLength.meters) + let altitiude = Measurement(value: newLocation.altitude, unit: UnitLength.meters) + let speed = Measurement(value: newLocation.speed, unit: UnitSpeed.kilometersPerHour) + let speedAccuracy = Measurement(value: newLocation.speedAccuracy, unit: UnitSpeed.metersPerSecond) + let courseAccuracy = Measurement(value: newLocation.courseAccuracy, unit: UnitAngle.degrees) + Label("Coordinate \(String(format: "%.5f", newLocation.coordinate.latitude)), \(String(format: "%.5f", newLocation.coordinate.longitude))", systemImage: "mappin") - .font(.footnote) + .font(largeFont) .textSelection(.enabled) HStack { Label("Accuracy \(horizontalAccuracy.formatted())", systemImage: "scope") - .font(.footnote) + .font(largeFont) Label("Sats Estimate \(LocationsHandler.satsInView)", systemImage: "sparkles") - .font(.footnote) + .font(largeFont) } HStack { if newLocation.verticalAccuracy > 0 { Label("Altitude \(altitiude.formatted())", systemImage: "mountain.2") - .font(.footnote) + .font(largeFont) } Label("Accuracy \(verticalAccuracy.formatted())", systemImage: "lines.measurement.vertical") - .font(.caption2) + .font(smallFont) } HStack { let degrees = Angle.degrees(newLocation.course) @@ -49,15 +53,15 @@ struct GPSStatus: View { .symbolRenderingMode(.hierarchical) .rotationEffect(degrees) } - .font(.footnote) + .font(largeFont) Label("Accuracy \(courseAccuracy.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))", systemImage: "safari") - .font(.caption2) + .font(smallFont) } HStack { Label("Speed \(speed.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))))", systemImage: "speedometer") - .font(.footnote) + .font(largeFont) Label("Accuracy \(speedAccuracy.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))))", systemImage: "gauge.with.dots.needle.bottom.50percent.badge.plus") - .font(.caption2) + .font(smallFont) } } } diff --git a/Meshtastic/Views/Settings/RouteRecorder.swift b/Meshtastic/Views/Settings/RouteRecorder.swift index d8253c97..086cb6e5 100644 --- a/Meshtastic/Views/Settings/RouteRecorder.swift +++ b/Meshtastic/Views/Settings/RouteRecorder.swift @@ -11,160 +11,277 @@ import MapKit import CoreLocation import CoreMotion -struct TimerDisplayObject { - var seconds: Int = 0 - var minutes: Int = 0 - var hours: Int = 0 - - var display: String { - if self.seconds == 0 { - "\(String(format: "%02d", self.hours)):\(String(format: "%02d", self.minutes)):00" - } else { - "\(String(format: "%02d", self.hours)):\(String(format: "%02d", self.minutes)):\(String(format: "%02d", self.seconds))" - } - } - - var timeMinuteCalculator: Float { Float(hours*60+seconds/60+minutes) } -} - @available(iOS 17.0, macOS 14.0, *) struct RouteRecorder: View { - @ObservedObject var locationsHandler = LocationsHandler.shared + @ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared @Environment(\.managedObjectContext) var context @State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic) - @State var isTimerRunning = false + //@State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true) + @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic) @State var isShowingDetails = false - @State var timer: Timer? @Namespace var namespace @Namespace var routerecorderscope - @State var timeElapsed: TimerDisplayObject = TimerDisplayObject() - @State var timerDisplay = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + @State var recording: RouteEntity? + @State var color: Color = .blue var body: some View { VStack { - VStack { - VStack { - Map(position: $position, scope: routerecorderscope) { - UserAnnotation() -// ForEach(locations, id: \.id) { location in -// Marker(location.name, systemImage: location.icon, coordinate: location.location) -// .tint(location.colour) -// } - } + ZStack { + Map(position: $position, scope: routerecorderscope) { + UserAnnotation() + /// Route Lines + let lineCoords = locationsHandler.locationsArray.compactMap({(position) -> CLLocationCoordinate2D in + return position.coordinate + }) + + let gradient = LinearGradient( + colors: [color], + startPoint: .leading, endPoint: .trailing + ) + let dashed = StrokeStyle( + lineWidth: 3, + lineCap: .round, lineJoin: .round, dash: [10, 10] + ) + MapPolyline(coordinates: lineCoords) + .stroke(gradient, style: dashed) + } - .mapScope(routerecorderscope) - .mapControls { - MapUserLocationButton() - MapCompass() - MapScaleView() - MapPitchToggle() - } - .mapStyle(.hybrid(elevation: .realistic, showsTraffic: true)) - .transition(.slide) - .mapControlVisibility(.visible) - .safeAreaInset(edge: .bottom) { - ZStack { - VStack { - HStack(spacing: 10) { - Spacer() - if isTimerRunning { - Button { - isShowingDetails = true - isTimerRunning = false - } label: { - Image(systemName: "pause.fill") - .frame(width: 60, height: 60) - } - .buttonStyle(.bordered) - .buttonBorderShape(.circle) - .matchedGeometryEffect(id: "Pause Button", in: namespace) - } else { - Button { - isShowingDetails = true - isTimerRunning = true - timeElapsed.seconds -= 1 - } label: { - Image(systemName: "play.fill") - .frame(width: 60, height: 60) - } - .buttonStyle(.bordered) - .buttonBorderShape(.circle) - .matchedGeometryEffect(id: "Play Button", in: namespace) - } - Spacer() - } - } - .onReceive(timerDisplay) { _ in - if isTimerRunning { - timeElapsed.seconds += 1 - if timeElapsed.seconds == 60 { - timeElapsed.seconds = 0 - timeElapsed.minutes += 1 - if timeElapsed.minutes == 60 { - timeElapsed.minutes = 0 - timeElapsed.hours += 1 - } - } + .mapStyle(mapStyle) + } + .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) + .mapScope(routerecorderscope) + .safeAreaInset(edge: .bottom) { + ZStack { + VStack { + HStack(spacing: 10) { + Spacer() + + Button { + isShowingDetails = true + } label: { + Image(systemName: locationsHandler.isRecording ? "record.circle.fill" : "record.circle") + .font(.system(size: 72)) + .symbolRenderingMode(.multicolor) + .foregroundColor(.red) } + .buttonStyle(.bordered) + .foregroundColor(.red) + .buttonBorderShape(.circle) + .matchedGeometryEffect(id: "Details Button", in: namespace) + + Spacer() } } - .padding() } - .sheet(isPresented: $isShowingDetails) { - NavigationStack { - VStack { - HStack { - Text(timeElapsed.display) - .font(.largeTitle) - Text("Time Elapseed") - .font(.callout) + .padding() + } + .sheet(isPresented: $isShowingDetails) { + NavigationStack { + VStack { + if locationsHandler.isRecording { + HStack (alignment: .center) { + Image(systemName: "record.circle.fill") + .symbolRenderingMode(.multicolor) + .font(.title) + .foregroundColor(.red) + Text("Recording route") + .font(.title) + Spacer() + Text("\(locationsHandler.count)") + .foregroundColor(.red) + .font(.title2) } .padding() + } else if locationsHandler.isRecordingPaused { + HStack (alignment: .center) { + + Image(systemName: "playpause") + .symbolRenderingMode(.multicolor) + .font(.title3) + .foregroundColor(.red) + Text("Route recording paused") + .font(.title) + } + .padding(.top) + } + + if locationsHandler.isRecording || locationsHandler.isRecordingPaused { Divider() - VStack(alignment: .leading) { - if let lastLocation = locationsHandler.locationsArray.last { - - let horizontalAccuracy = Measurement(value: lastLocation.horizontalAccuracy, unit: UnitLength.meters) - let verticalAccuracy = Measurement(value: lastLocation.verticalAccuracy, unit: UnitLength.meters) - let altitiude = Measurement(value: lastLocation.altitude, unit: UnitLength.meters) - let speed = Measurement(value: lastLocation.speed, unit: UnitSpeed.kilometersPerHour) - List { - Label("Coordinate \(String(format: "%.5f", lastLocation.coordinate.latitude)), \(String(format: "%.5f", lastLocation.coordinate.longitude))", systemImage: "mappin") - .textSelection(.enabled) - Label("Horizontal Accuracy \(horizontalAccuracy.formatted())", systemImage: "scope") - if lastLocation.verticalAccuracy > 0 { - Label("Altitude \(altitiude.formatted())", systemImage: "mountain.2") - } - Label("Vertical Accuracy \(verticalAccuracy.formatted())", systemImage: "lines.measurement.vertical") - Label("Satellites Estimate \(LocationHelper.satsInView)", systemImage: "sparkles") - Label("\(locationsHandler.isStationary ? "Moving" : "Stationary")", systemImage: locationsHandler.isStationary ? "figure.walk.motion" : "figure.stand") - if lastLocation.speedAccuracy > 0 { - Label("Speed \(speed.formatted())", systemImage: "speedometer") - } - if lastLocation.courseAccuracy > 0 { - /// Heading - let degrees = Angle.degrees(Double(lastLocation.course)) - Label { - let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees) - /// Text("Heading: \(heading.formatted())") - Text("Heading \(String(format: "%.2f", lastLocation.course))°") - .foregroundColor(.primary) - } icon: { - Image(systemName: "location.circle") - .symbolRenderingMode(.hierarchical) - .frame(width: 35) - .rotationEffect(degrees) - } + HStack { + VStack { + Text(locationsHandler.recordingStarted ?? Date(), style: .timer) + .font(.title) + .fixedSize() + Text("Time") + .font(.callout) + .fixedSize() + } + .padding(.horizontal) + Divider() + VStack { + let distance = Measurement(value: locationsHandler.distanceTraveled, unit: UnitLength.meters) + Text("\(distance.formatted())") + .font(.title) + .fixedSize() + Text("Distance") + .font(.callout) + .fixedSize() + } + .padding(.horizontal) + Divider() + VStack { + let gain = Measurement(value: locationsHandler.elevationGain, unit: UnitLength.meters) + Text(gain.formatted()) + .font(.title) + Text("Elev. Gain") + .font(.callout) + } + .padding(.horizontal) + } + .frame(maxHeight: 90) + } + Divider() + VStack(alignment: .leading) { + List { + GPSStatus(largeFont: .body, smallFont: .callout) + } + .listStyle(.plain) + HStack { + Spacer() + if !locationsHandler.isRecording && !locationsHandler.isRecordingPaused { + /// We are not recording or paused, show start recording button + Button { + locationsHandler.isRecording = true + locationsHandler.count = 0 + locationsHandler.distanceTraveled = 0.0 + locationsHandler.elevationGain = 0.0 + locationsHandler.locationsArray.removeAll() + locationsHandler.recordingStarted = Date() + let newRoute = RouteEntity(context: context) + newRoute.name = String("Route Recording") + newRoute.id = Int32.random(in: Int32(Int8.max) ... Int32.max) + newRoute.color = Int64(UIColor.random.hex) + newRoute.date = Date() + newRoute.enabled = false + color = Color(UIColor(hex: UInt32(newRoute.color))) + self.recording = newRoute + do { + try context.save() + print("💾 Saved a new route") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") } + } label: { + Label("start", systemImage: "play") } - .listStyle(.plain) + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + + } else if locationsHandler.isRecording { + /// We are recording show pause button + Button { + locationsHandler.isRecording = false + locationsHandler.isRecordingPaused = true + } label: { + Label("pause", systemImage: "pause") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + } else if locationsHandler.isRecordingPaused { + /// We are paused show resume button + Button { + locationsHandler.isRecording = true + locationsHandler.isRecordingPaused = false + } label: { + Label("resume", systemImage: "playpause") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + } + + if locationsHandler.isRecording || locationsHandler.isRecordingPaused { + /// We are recording or paused, show finish button + Button { + locationsHandler.isRecording = false + locationsHandler.isRecordingPaused = false + locationsHandler.distanceTraveled = 0.0 + locationsHandler.elevationGain = 0.0 + locationsHandler.locationsArray.removeAll() + locationsHandler.recordingStarted = nil + if let rec = recording { + rec.enabled = true + context.refresh(rec, mergeChanges:true) + } + + do { + try context.save() + print("💾 Saved a route finish") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") + } + } label: { + Label("finish", systemImage: "flag.checkered") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + } +#if targetEnvironment(macCatalyst) + Button(role: .cancel) { + isShowingDetails = false + } label: { + Label("close", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) +#endif + Spacer() + } + + } + } + } + .presentationDetents([.fraction(0.30), .fraction(0.65)]) + .presentationDragIndicator(.hidden) + .interactiveDismissDisabled(false) + .onChange(of: locationsHandler.locationsArray.last) { newLoc in + if locationsHandler.isRecording { + if let loc = newLoc { + if recording != nil { + let locationEntity = LocationEntity(context: context) + locationEntity.routeLocation = recording + locationEntity.id = Int32(locationsHandler.count) + locationEntity.altitude = Int32(loc.altitude) + locationEntity.heading = Int32(loc.course) + locationEntity.speed = Int32(loc.speed) + locationEntity.latitudeI = Int32(loc.coordinate.latitude * 1e7) + locationEntity.longitudeI = Int32(loc.coordinate.longitude * 1e7) + do { + try context.save() + print("💾 Saved a new route location") + //print("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num)") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Saving LocationEntity from the Route Recorder \(nsError)") } } } } - .presentationDetents([.fraction(0.6)]) - .presentationDragIndicator(.visible) } } } diff --git a/Meshtastic/Views/Settings/Routes.swift b/Meshtastic/Views/Settings/Routes.swift index 5039f724..44a65690 100644 --- a/Meshtastic/Views/Settings/Routes.swift +++ b/Meshtastic/Views/Settings/Routes.swift @@ -135,6 +135,7 @@ struct Routes: View { } } } + .badge(route.locations?.count ?? 0) .swipeActions { Button(role: .destructive) { context.delete(route) @@ -147,6 +148,7 @@ struct Routes: View { Label("delete", systemImage: "trash") } } + } .listStyle(.plain) } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index af2c8cc8..a7baadf0 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -313,16 +313,15 @@ struct Settings: View { } } .onAppear { - if self.bleManager.context == nil { - self.bleManager.context = context - } self.preferredNodeNum = UserDefaults.preferredPeripheralNum - self.selectedNode = Int(bleManager.connectedPeripheral != nil ? UserDefaults.preferredPeripheralNum : 0) + if selectedNode == 0 { + self.selectedNode = Int(bleManager.connectedPeripheral != nil ? UserDefaults.preferredPeripheralNum : 0) + } } .listStyle(GroupedListStyle()) .navigationTitle("settings") .navigationBarItems(leading: - MeshtasticLogo() + MeshtasticLogo() ) } detail: { diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index c9d64c05..34337470 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -190,7 +190,6 @@ struct UserConfig: View { ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") }) .onAppear { - self.bleManager.context = context self.shortName = node?.user?.shortName ?? "" self.longName = node?.user?.longName ?? "" self.isLicensed = node?.user?.isLicensed ?? false diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index 54c5d6f3..1b6b3060 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -84,6 +84,7 @@ "encrypted"="Verschlüsselt"; "external.notification"="Externe Benachrichtigung"; "external.notification.config"="Einstellungen der externen Benachrichtigung"; +"finish"="Finish"; "firmware.version"="Firmware Version"; "firmware.version.unsupported"="Nicht unterstützte Firmware Version erkannt. Kann nicht verbinden."; "gas"="Gas"; @@ -213,6 +214,7 @@ "on.boot"="Nur beim Starten"; "options"="Optionen"; "password"="Passwort"; +"pause"="Pause"; "phone.gps"="Telefon GPS"; "phone.gps.interval.description"="Wie häufig das Telefon den Standort an das Gerät sendet. Standortaktualisierungen an das Mesh werden vom Gerät verwaltet."; "position"="Position"; @@ -228,8 +230,10 @@ "reply"="Antworten"; "received.ack"="Empfangsbestätigung"; "received.ack.real"="Recipient Ack"; +"resume"="Resume"; "ringtone"="Ringtone"; "ringtone.config"="Ringtone Config"; +"route.recorder"="Route Recorder"; "routes"="Routes"; "routing.acknowledged"="Bestätigt"; "routing.noroute"="Keine Route"; @@ -264,6 +268,7 @@ "set.region"="Setze LoRa Region"; "standard"="Standard"; "standard.muted"="Standard Muted"; +"start"="Start"; "storeforward"="Store & Forward"; "storeforward.config"="Store & Forward Config"; "storeforward.heartbeat"="Send Heartbeat"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 355a9c26..aeb1b117 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -88,6 +88,7 @@ "encrypted"="Encrypted"; "external.notification"="External Notification"; "external.notification.config"="External Notification Config"; +"finish"="Finish"; "firmware.version"="Firmware Version"; "firmware.version.unsupported"="Unsupported Firmware Version Detected, unable to connect to device."; "gas"="Gas"; @@ -217,6 +218,7 @@ "on.boot"="On Boot Only"; "options"="Options"; "password"="Password"; +"pause"="Pause"; "phone.gps"="Phone GPS"; "phone.gps.interval.description"="How frequently your phone will send your location to the device, location updates to the mesh are managed by the device."; "position"="Position"; @@ -232,8 +234,10 @@ "reboot.node"="Reboot node?"; "received.ack"="Received Ack"; "received.ack.real"="Recipient Ack"; +"resume"="Resume"; "ringtone"="Ringtone"; "ringtone.config"="Ringtone Config"; +"route.recorder"="Route Recorder"; "routes"="Routes"; "routing.acknowledged"="Acknowledged"; "routing.noroute"="No Route"; @@ -268,6 +272,7 @@ "set.region"="Set LoRa Region"; "standard"="Standard"; "standard.muted"="Standard Muted"; +"start"="Start"; "storeforward"="Store & Forward"; "storeforward.config"="Store & Forward Config"; "storeforward.heartbeat"="Send Heartbeat"; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index 95ccadb5..db76ed47 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -86,6 +86,7 @@ "encrypted"="Zaszyfrowany"; "external.notification"="Zewnętrzne Powiadomienie"; "external.notification.config"="Konfiguracja Zewnętrznego Powiadomienia"; +"finish"="Finish"; "firmware.version"="Wersja Oprogramowania"; "firmware.version.unsupported"="Wykryto nieobsługiwany wersję oprogramowania, brak możliwości połączenia z urządzeniem."; "gas"="Gaz"; @@ -214,6 +215,7 @@ "on.boot"="Tylko przy uruchomieniu"; "options"="Opcje"; "password"="Hasło"; +"pause"="Pause"; "phone.gps"="GPS telefonu"; "phone.gps.interval.description"="Jak często Twój telefon będzie wysyłał swoją lokalizację do urządzenia, aktualizacje lokalizacji w sieci są zarządzane przez urządzenie."; "position"="Pozycja"; @@ -229,8 +231,10 @@ "reboot.node"="Uruchomić ponownie węzeł?"; "received.ack"="Odebrano potwierdzenie"; "received.ack.real"="Odbiorca potwierdzenia"; +"resume"="Resume"; "ringtone"="Dzwonek"; "ringtone.config"="Konfiguracja dzwonka"; +"route.recorder"="Route Recorder"; "routes"="Routes"; "routing.acknowledged"="Potwierdzono"; "routing.noroute"="Brak trasy"; @@ -265,6 +269,7 @@ "set.region"="Ustaw region LoRa"; "standard"="Standardowy"; "standard.muted"="Standardowy wyłączony"; +"start"="Start"; "storeforward"="Store & Forward"; "storeforward.config"="Store & Forward Config"; "storeforward.heartbeat"="Send Heartbeat"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 202a0871..ed5d4d3e 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -84,6 +84,7 @@ "encrypted"="加密"; "external.notification"="外部通知"; "external.notification.config"="外部通知配置"; +"finish"="Finish"; "firmware.version"="固件版本"; "firmware.version.unsupported"="检测到不支持的固件版本,无法连接到电台。"; "gas"="Gas"; @@ -213,6 +214,7 @@ "on.boot"="仅在启动时"; "options"="选项"; "password"="密码"; +"pause"="Pause"; "phone.gps"="手机 GPS"; "phone.gps.interval.description"="电台通过手机获取定位的时间间隔,但是向 Mesh 网络中刷新定位的时间间隔由电台控制。"; "position"="定位"; @@ -228,8 +230,10 @@ "reboot.node"="重启节点?"; "received.ack"="收到确认"; "received.ack.real"="收件人确认"; +"resume"="Resume"; "ringtone"="铃声"; "ringtone.config"="铃声设置"; +"route.recorder"="Route Recorder"; "routes"="Routes"; "routing.acknowledged"="确认"; "routing.noroute"="找不到目标"; @@ -264,6 +268,7 @@ "set.region"="设置 LoRa 区域"; "standard"="标准"; "standard.muted"="标准静音"; +"start"="Start"; "ssid"="SSID"; "storeforward"="储存 & 转发"; "storeforward.config"="储存 & 转发设置";