diff --git a/.gitignore b/.gitignore index 77aab2fe..4a8064a3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ ## User settings xcuserdata/ +## Generated Protobufs +protobufs/ + ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) *.xcscmblueprint *.xccheckout diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 35f9264f..d287f783 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -104,6 +104,8 @@ DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A152A0594AD006ED576 /* TileOverlay.swift */; }; DDB75A1A2A05EB67006ED576 /* alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = DDB75A192A05EB67006ED576 /* alpha.png */; }; DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */; }; + DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */; }; + DDB75A232A13CDA9006ED576 /* BatteryLevelCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */; }; DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */; }; DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */; }; DDC2E15F26CE248F0042C5E4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */; }; @@ -294,6 +296,8 @@ DDB75A192A05EB67006ED576 /* alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = alpha.png; sourceTree = ""; }; DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaSignalStrengthIndicator.swift; sourceTree = ""; }; DDB75A1F2A10766D006ED576 /* MeshtasticDataModelV13.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV13.xcdatamodel; sourceTree = ""; }; + DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaSignalStrength.swift; sourceTree = ""; }; + DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryLevelCompact.swift; sourceTree = ""; }; DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV8.xcdatamodel; sourceTree = ""; }; DDC2E15426CE248E0042C5E4 /* Meshtastic.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Meshtastic.app; sourceTree = BUILT_PRODUCTS_DIR; }; DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticApp.swift; sourceTree = ""; }; @@ -694,6 +698,8 @@ DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */, DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */, DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */, + DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */, + DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */, ); path = Helpers; sourceTree = ""; @@ -1008,6 +1014,7 @@ DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */, DDDB444229F8A88700EE2349 /* Double.swift in Sources */, DD5E520F298EE33B00D21B61 /* cannedmessages.pb.swift in Sources */, + DDB75A232A13CDA9006ED576 /* BatteryLevelCompact.swift in Sources */, DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */, DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */, DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */, @@ -1060,6 +1067,7 @@ DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */, DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */, DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */, + DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */, DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */, DDB6ABE428B13FFF00384BA1 /* DisplayEnums.swift in Sources */, DD86D40A287F04F100BAEB7A /* InvalidVersion.swift in Sources */, @@ -1298,7 +1306,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.10; + MARKETING_VERSION = 2.1.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1332,7 +1340,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.10; + MARKETING_VERSION = 2.1.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1451,7 +1459,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.1.10; + MARKETING_VERSION = 2.1.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1482,7 +1490,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.1.10; + MARKETING_VERSION = 2.1.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/Contents.json b/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/Contents.json index 06ebbbb1..7f0651c4 100644 --- a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/Contents.json +++ b/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "play_store_icon_114px-2.png", + "filename" : "heltecwsl 2.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "play_store_icon_114px-3.png", + "filename" : "heltecwsl 1.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "play_store_icon_114px-4.png", + "filename" : "heltecwsl.png", "idiom" : "universal", "scale" : "3x" } diff --git a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/heltecwsl 1.png b/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/heltecwsl 1.png new file mode 100644 index 00000000..63ecd895 Binary files /dev/null and b/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/heltecwsl 1.png differ diff --git a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/heltecwsl 2.png b/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/heltecwsl 2.png new file mode 100644 index 00000000..63ecd895 Binary files /dev/null and b/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/heltecwsl 2.png differ diff --git a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/heltecwsl.png b/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/heltecwsl.png new file mode 100644 index 00000000..63ecd895 Binary files /dev/null and b/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/heltecwsl.png differ diff --git a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/play_store_icon_114px-2.png b/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/play_store_icon_114px-2.png deleted file mode 100644 index 79cf0e00..00000000 Binary files a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/play_store_icon_114px-2.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/play_store_icon_114px-3.png b/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/play_store_icon_114px-3.png deleted file mode 100644 index 79cf0e00..00000000 Binary files a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/play_store_icon_114px-3.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/play_store_icon_114px-4.png b/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/play_store_icon_114px-4.png deleted file mode 100644 index 79cf0e00..00000000 Binary files a/Meshtastic/Assets.xcassets/HELTECWSLV3.imageset/play_store_icon_114px-4.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/Contents.json b/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/Contents.json index 06ebbbb1..dc52568b 100644 --- a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/Contents.json +++ b/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/Contents.json @@ -1,17 +1,17 @@ { "images" : [ { - "filename" : "play_store_icon_114px-2.png", + "filename" : "tbeam_supreme 2.jpg", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "play_store_icon_114px-3.png", + "filename" : "tbeam_supreme 1.jpg", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "play_store_icon_114px-4.png", + "filename" : "tbeam_supreme.jpg", "idiom" : "universal", "scale" : "3x" } diff --git a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/play_store_icon_114px-2.png b/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/play_store_icon_114px-2.png deleted file mode 100644 index 79cf0e00..00000000 Binary files a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/play_store_icon_114px-2.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/play_store_icon_114px-3.png b/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/play_store_icon_114px-3.png deleted file mode 100644 index 79cf0e00..00000000 Binary files a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/play_store_icon_114px-3.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/play_store_icon_114px-4.png b/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/play_store_icon_114px-4.png deleted file mode 100644 index 79cf0e00..00000000 Binary files a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/play_store_icon_114px-4.png and /dev/null differ diff --git a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/tbeam_supreme 1.jpg b/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/tbeam_supreme 1.jpg new file mode 100644 index 00000000..47fa5964 Binary files /dev/null and b/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/tbeam_supreme 1.jpg differ diff --git a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/tbeam_supreme 2.jpg b/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/tbeam_supreme 2.jpg new file mode 100644 index 00000000..47fa5964 Binary files /dev/null and b/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/tbeam_supreme 2.jpg differ diff --git a/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/tbeam_supreme.jpg b/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/tbeam_supreme.jpg new file mode 100644 index 00000000..47fa5964 Binary files /dev/null and b/Meshtastic/Assets.xcassets/LILYGOTBEAMS3CORE.imageset/tbeam_supreme.jpg differ diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index 98e2b3b9..42f1c445 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -269,6 +269,12 @@ enum MapTileServer: String, CaseIterable, Identifiable { } } +enum OverlayType: String, CaseIterable, Equatable { + case tileServer + case geoJson + var localized: String { self.rawValue.localized } +} + enum MapOverlayServer: String, CaseIterable, Identifiable { case baseReReflectivityCurrent @@ -282,6 +288,29 @@ enum MapOverlayServer: String, CaseIterable, Identifiable { case mrmsHybridScanReflectivityComposite var id: String { self.rawValue } + var overlayType: OverlayType { + switch self { + + case .baseReReflectivityCurrent: + return .tileServer + case .baseReReflectivityOneHourAgo: + return .tileServer + case .echoTopsEetCurrent: + return .tileServer + case .echoTopsEetOneHourAgo: + return .tileServer + case .q2OneHourPrecipitation: + return .tileServer + case .q2TwentyFourHourPrecipitation: + return .tileServer + case .q2FortyEightHourPrecipitation: + return .tileServer + case .q2SeventyTwoHourPrecipitation: + return .tileServer + case .mrmsHybridScanReflectivityComposite: + return .tileServer + } + } var attribution: String { return "NEXRAD Weather tiles from Iowa State University Environmental Mesonet [OGC Web Services](https://mesonet.agron.iastate.edu/ogc/)." } diff --git a/Meshtastic/Protobufs/meshtastic/admin.pb.swift b/Meshtastic/Protobufs/meshtastic/admin.pb.swift index aa4a6481..80eab93f 100644 --- a/Meshtastic/Protobufs/meshtastic/admin.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/admin.pb.swift @@ -204,6 +204,26 @@ struct AdminMessage { set {payloadVariant = .setHamMode(newValue)} } + /// + /// Get the mesh's nodes with their available gpio pins for RemoteHardware module use + var getNodeRemoteHardwarePinsRequest: Bool { + get { + if case .getNodeRemoteHardwarePinsRequest(let v)? = payloadVariant {return v} + return false + } + set {payloadVariant = .getNodeRemoteHardwarePinsRequest(newValue)} + } + + /// + /// Respond with the mesh's nodes with their available gpio pins for RemoteHardware module use + var getNodeRemoteHardwarePinsResponse: NodeRemoteHardwarePinsResponse { + get { + if case .getNodeRemoteHardwarePinsResponse(let v)? = payloadVariant {return v} + return NodeRemoteHardwarePinsResponse() + } + set {payloadVariant = .getNodeRemoteHardwarePinsResponse(newValue)} + } + /// /// Set the owner for this node var setOwner: User { @@ -409,6 +429,12 @@ struct AdminMessage { /// Setup a node for licensed amateur (ham) radio operation case setHamMode(HamParameters) /// + /// Get the mesh's nodes with their available gpio pins for RemoteHardware module use + case getNodeRemoteHardwarePinsRequest(Bool) + /// + /// Respond with the mesh's nodes with their available gpio pins for RemoteHardware module use + case getNodeRemoteHardwarePinsResponse(NodeRemoteHardwarePinsResponse) + /// /// Set the owner for this node case setOwner(User) /// @@ -532,6 +558,14 @@ struct AdminMessage { guard case .setHamMode(let l) = lhs, case .setHamMode(let r) = rhs else { preconditionFailure() } return l == r }() + case (.getNodeRemoteHardwarePinsRequest, .getNodeRemoteHardwarePinsRequest): return { + guard case .getNodeRemoteHardwarePinsRequest(let l) = lhs, case .getNodeRemoteHardwarePinsRequest(let r) = rhs else { preconditionFailure() } + return l == r + }() + case (.getNodeRemoteHardwarePinsResponse, .getNodeRemoteHardwarePinsResponse): return { + guard case .getNodeRemoteHardwarePinsResponse(let l) = lhs, case .getNodeRemoteHardwarePinsResponse(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 @@ -803,12 +837,29 @@ struct HamParameters { init() {} } +/// +/// Response envelope for node_remote_hardware_pins +struct NodeRemoteHardwarePinsResponse { + // 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. + + /// + /// Nodes and their respective remote hardware GPIO pins + var nodeRemoteHardwarePins: [NodeRemoteHardwarePin] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + #if swift(>=5.5) && canImport(_Concurrency) extension AdminMessage: @unchecked Sendable {} extension AdminMessage.OneOf_PayloadVariant: @unchecked Sendable {} extension AdminMessage.ConfigType: @unchecked Sendable {} extension AdminMessage.ModuleConfigType: @unchecked Sendable {} extension HamParameters: @unchecked Sendable {} +extension NodeRemoteHardwarePinsResponse: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -835,6 +886,8 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat 16: .standard(proto: "get_device_connection_status_request"), 17: .standard(proto: "get_device_connection_status_response"), 18: .standard(proto: "set_ham_mode"), + 19: .standard(proto: "get_node_remote_hardware_pins_request"), + 20: .standard(proto: "get_node_remote_hardware_pins_response"), 32: .standard(proto: "set_owner"), 33: .standard(proto: "set_channel"), 34: .standard(proto: "set_config"), @@ -1028,6 +1081,27 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat self.payloadVariant = .setHamMode(v) } }() + case 19: try { + var v: Bool? + try decoder.decodeSingularBoolField(value: &v) + if let v = v { + if self.payloadVariant != nil {try decoder.handleConflictingOneOf()} + self.payloadVariant = .getNodeRemoteHardwarePinsRequest(v) + } + }() + case 20: try { + var v: NodeRemoteHardwarePinsResponse? + var hadOneofValue = false + if let current = self.payloadVariant { + hadOneofValue = true + if case .getNodeRemoteHardwarePinsResponse(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.payloadVariant = .getNodeRemoteHardwarePinsResponse(v) + } + }() case 32: try { var v: User? var hadOneofValue = false @@ -1239,6 +1313,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat guard case .setHamMode(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 18) }() + case .getNodeRemoteHardwarePinsRequest?: try { + guard case .getNodeRemoteHardwarePinsRequest(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularBoolField(value: v, fieldNumber: 19) + }() + case .getNodeRemoteHardwarePinsResponse?: try { + guard case .getNodeRemoteHardwarePinsResponse(let v)? = self.payloadVariant else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 20) + }() case .setOwner?: try { guard case .setOwner(let v)? = self.payloadVariant else { preconditionFailure() } try visitor.visitSingularMessageField(value: v, fieldNumber: 32) @@ -1382,3 +1464,35 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa return true } } + +extension NodeRemoteHardwarePinsResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".NodeRemoteHardwarePinsResponse" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "node_remote_hardware_pins"), + ] + + 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.decodeRepeatedMessageField(value: &self.nodeRemoteHardwarePins) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.nodeRemoteHardwarePins.isEmpty { + try visitor.visitRepeatedMessageField(value: self.nodeRemoteHardwarePins, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: NodeRemoteHardwarePinsResponse, rhs: NodeRemoteHardwarePinsResponse) -> Bool { + if lhs.nodeRemoteHardwarePins != rhs.nodeRemoteHardwarePins {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index 917fb889..32949e89 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -204,7 +204,7 @@ struct Config { /// /// Router device role. - /// Mesh packets will prefer to be routed over this node. This node will not be used by client apps. + /// Mesh packets will prefer to be routed over this node. This node will not be used by client apps. /// The wifi/ble radios and the oled screen will be put to sleep. /// This mode may still potentially have higher power usage due to it's preference in message rebroadcasting on the mesh. case router // = 2 @@ -217,7 +217,7 @@ struct Config { /// /// Repeater device role /// Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry - /// or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate. + /// or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate. case repeater // = 4 /// @@ -508,7 +508,7 @@ struct Config { /// /// Mesh Super Deep Sleep Timeout Seconds - /// While in Light Sleep if this value is exceeded we will lower into super deep sleep + /// While in Light Sleep if this value is exceeded we will lower into super deep sleep /// for sds_secs (default 1 year) or a button press /// 0 for default of two hours, MAXUINT for disabled var meshSdsTimeoutSecs: UInt32 = 0 @@ -674,7 +674,7 @@ struct Config { var autoScreenCarouselSecs: UInt32 = 0 /// - /// If this is set, the displayed compass will always point north. if unset, the old behaviour + /// If this is set, the displayed compass will always point north. if unset, the old behaviour /// (top of display is heading direction) is used. var compassNorthTop: Bool = false @@ -978,9 +978,9 @@ struct Config { var channelNum: UInt32 = 0 /// - /// If true, duty cycle limits will be exceeded and thus you're possibly not following + /// If true, duty cycle limits will be exceeded and thus you're possibly not following /// the local regulations if you're not a HAM. - /// Has no effect if the duty cycle of the used region is 100%. + /// Has no effect if the duty cycle of the used region is 100%. var overrideDutyCycle: Bool = false /// diff --git a/Meshtastic/Protobufs/meshtastic/connection_status.pb.swift b/Meshtastic/Protobufs/meshtastic/connection_status.pb.swift index b305533f..7092c3b3 100644 --- a/Meshtastic/Protobufs/meshtastic/connection_status.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/connection_status.pb.swift @@ -98,7 +98,7 @@ struct WifiConnectionStatus { mutating func clearStatus() {self._status = nil} /// - /// WiFi access point SSID + /// WiFi access point SSID var ssid: String = String() /// diff --git a/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift b/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift index 25a7b5ca..6a9448c3 100644 --- a/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift @@ -172,6 +172,13 @@ struct DeviceState { /// Clears the value of `rxWaypoint`. Subsequent reads from it will return its default value. mutating func clearRxWaypoint() {_uniqueStorage()._rxWaypoint = nil} + /// + /// The mesh's nodes with their available gpio pins for RemoteHardware module + var nodeRemoteHardwarePins: [NodeRemoteHardwarePin] { + get {return _storage._nodeRemoteHardwarePins} + set {_uniqueStorage()._nodeRemoteHardwarePins = newValue} + } + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -263,11 +270,41 @@ struct OEMStore { fileprivate var _oemLocalModuleConfig: LocalModuleConfig? = nil } +/// +/// RemoteHardwarePins associated with a node +struct NodeRemoteHardwarePin { + // 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. + + /// + /// The node_num exposing the available gpio pin + var nodeNum: UInt32 = 0 + + /// + /// The the available gpio pin for usage with RemoteHardware module + var pin: RemoteHardwarePin { + get {return _pin ?? RemoteHardwarePin()} + set {_pin = newValue} + } + /// Returns true if `pin` has been explicitly set. + var hasPin: Bool {return self._pin != nil} + /// Clears the value of `pin`. Subsequent reads from it will return its default value. + mutating func clearPin() {self._pin = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _pin: RemoteHardwarePin? = nil +} + #if swift(>=5.5) && canImport(_Concurrency) extension ScreenFonts: @unchecked Sendable {} extension DeviceState: @unchecked Sendable {} extension ChannelFile: @unchecked Sendable {} extension OEMStore: @unchecked Sendable {} +extension NodeRemoteHardwarePin: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -294,6 +331,7 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati 9: .standard(proto: "no_save"), 11: .standard(proto: "did_gps_reset"), 12: .standard(proto: "rx_waypoint"), + 13: .standard(proto: "node_remote_hardware_pins"), ] fileprivate class _StorageClass { @@ -306,6 +344,7 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati var _noSave: Bool = false var _didGpsReset: Bool = false var _rxWaypoint: MeshPacket? = nil + var _nodeRemoteHardwarePins: [NodeRemoteHardwarePin] = [] static let defaultInstance = _StorageClass() @@ -321,6 +360,7 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati _noSave = source._noSave _didGpsReset = source._didGpsReset _rxWaypoint = source._rxWaypoint + _nodeRemoteHardwarePins = source._nodeRemoteHardwarePins } } @@ -348,6 +388,7 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati case 9: try { try decoder.decodeSingularBoolField(value: &_storage._noSave) }() case 11: try { try decoder.decodeSingularBoolField(value: &_storage._didGpsReset) }() case 12: try { try decoder.decodeSingularMessageField(value: &_storage._rxWaypoint) }() + case 13: try { try decoder.decodeRepeatedMessageField(value: &_storage._nodeRemoteHardwarePins) }() default: break } } @@ -387,6 +428,9 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati try { if let v = _storage._rxWaypoint { try visitor.visitSingularMessageField(value: v, fieldNumber: 12) } }() + if !_storage._nodeRemoteHardwarePins.isEmpty { + try visitor.visitRepeatedMessageField(value: _storage._nodeRemoteHardwarePins, fieldNumber: 13) + } } try unknownFields.traverse(visitor: &visitor) } @@ -405,6 +449,7 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati if _storage._noSave != rhs_storage._noSave {return false} if _storage._didGpsReset != rhs_storage._didGpsReset {return false} if _storage._rxWaypoint != rhs_storage._rxWaypoint {return false} + if _storage._nodeRemoteHardwarePins != rhs_storage._nodeRemoteHardwarePins {return false} return true } if !storagesAreEqual {return false} @@ -529,3 +574,45 @@ extension OEMStore: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationB return true } } + +extension NodeRemoteHardwarePin: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".NodeRemoteHardwarePin" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "node_num"), + 2: .same(proto: "pin"), + ] + + 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.decodeSingularUInt32Field(value: &self.nodeNum) }() + case 2: try { try decoder.decodeSingularMessageField(value: &self._pin) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.nodeNum != 0 { + try visitor.visitSingularUInt32Field(value: self.nodeNum, fieldNumber: 1) + } + try { if let v = self._pin { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: NodeRemoteHardwarePin, rhs: NodeRemoteHardwarePin) -> Bool { + if lhs.nodeNum != rhs.nodeNum {return false} + if lhs._pin != rhs._pin {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index 1e054777..841396e0 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -2318,6 +2318,10 @@ struct DeviceMetadata { /// Device hardware model var hwModel: HardwareModel = .unset + /// + /// Has Remote Hardware enabled + var hasRemoteHardware_p: Bool = false + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -4036,6 +4040,7 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement 7: .same(proto: "role"), 8: .standard(proto: "position_flags"), 9: .standard(proto: "hw_model"), + 10: .same(proto: "hasRemoteHardware"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -4053,6 +4058,7 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement case 7: try { try decoder.decodeSingularEnumField(value: &self.role) }() case 8: try { try decoder.decodeSingularUInt32Field(value: &self.positionFlags) }() case 9: try { try decoder.decodeSingularEnumField(value: &self.hwModel) }() + case 10: try { try decoder.decodeSingularBoolField(value: &self.hasRemoteHardware_p) }() default: break } } @@ -4086,6 +4092,9 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement if self.hwModel != .unset { try visitor.visitSingularEnumField(value: self.hwModel, fieldNumber: 9) } + if self.hasRemoteHardware_p != false { + try visitor.visitSingularBoolField(value: self.hasRemoteHardware_p, fieldNumber: 10) + } try unknownFields.traverse(visitor: &visitor) } @@ -4099,6 +4108,7 @@ extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement if lhs.role != rhs.role {return false} if lhs.positionFlags != rhs.positionFlags {return false} if lhs.hwModel != rhs.hwModel {return false} + if lhs.hasRemoteHardware_p != rhs.hasRemoteHardware_p {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Protobufs/meshtastic/module_config.pb.swift b/Meshtastic/Protobufs/meshtastic/module_config.pb.swift index 87c90e8b..5df77866 100644 --- a/Meshtastic/Protobufs/meshtastic/module_config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/module_config.pb.swift @@ -20,6 +20,59 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } +enum RemoteHardwarePinType: SwiftProtobuf.Enum { + typealias RawValue = Int + + /// + /// Unset/unused + case unknown // = 0 + + /// + /// GPIO pin can be read (if it is high / low) + case digitalRead // = 1 + + /// + /// GPIO pin can be written to (high / low) + case digitalWrite // = 2 + case UNRECOGNIZED(Int) + + init() { + self = .unknown + } + + init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .digitalRead + case 2: self = .digitalWrite + default: self = .UNRECOGNIZED(rawValue) + } + } + + var rawValue: Int { + switch self { + case .unknown: return 0 + case .digitalRead: return 1 + case .digitalWrite: return 2 + case .UNRECOGNIZED(let i): return i + } + } + +} + +#if swift(>=4.2) + +extension RemoteHardwarePinType: CaseIterable { + // The compiler won't synthesize support with the UNRECOGNIZED case. + static var allCases: [RemoteHardwarePinType] = [ + .unknown, + .digitalRead, + .digitalWrite, + ] +} + +#endif // swift(>=4.2) + /// /// Module Config struct ModuleConfig { @@ -267,6 +320,14 @@ struct ModuleConfig { /// Whether the Module is enabled var enabled: Bool = false + /// + /// Whether the Module allows consumers to read / write to pins not defined in available_pins + var allowUndefinedPinAccess: Bool = false + + /// + /// Exposes the available pins to the mesh for reading and writing + var availablePins: [RemoteHardwarePin] = [] + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -641,7 +702,7 @@ struct ModuleConfig { var sender: UInt32 = 0 /// - /// Bool value indicating that this node should save a RangeTest.csv file. + /// Bool value indicating that this node should save a RangeTest.csv file. /// ESP32 Only var save: Bool = false @@ -891,7 +952,32 @@ extension ModuleConfig.CannedMessageConfig.InputEventChar: CaseIterable { #endif // swift(>=4.2) +/// +/// A GPIO pin definition for remote hardware module +struct RemoteHardwarePin { + // 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. + + /// + /// GPIO Pin number (must match Arduino) + var gpioPin: UInt32 = 0 + + /// + /// Name for the GPIO pin (i.e. Front gate, mailbox, etc) + var name: String = String() + + /// + /// Type of GPIO access available to consumers on the mesh + var type: RemoteHardwarePinType = .unknown + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + #if swift(>=5.5) && canImport(_Concurrency) +extension RemoteHardwarePinType: @unchecked Sendable {} extension ModuleConfig: @unchecked Sendable {} extension ModuleConfig.OneOf_PayloadVariant: @unchecked Sendable {} extension ModuleConfig.MQTTConfig: @unchecked Sendable {} @@ -907,12 +993,21 @@ extension ModuleConfig.RangeTestConfig: @unchecked Sendable {} extension ModuleConfig.TelemetryConfig: @unchecked Sendable {} extension ModuleConfig.CannedMessageConfig: @unchecked Sendable {} extension ModuleConfig.CannedMessageConfig.InputEventChar: @unchecked Sendable {} +extension RemoteHardwarePin: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "meshtastic" +extension RemoteHardwarePinType: SwiftProtobuf._ProtoNameProviding { + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "UNKNOWN"), + 1: .same(proto: "DIGITAL_READ"), + 2: .same(proto: "DIGITAL_WRITE"), + ] +} + extension ModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = _protobuf_package + ".ModuleConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -1187,6 +1282,8 @@ extension ModuleConfig.RemoteHardwareConfig: SwiftProtobuf.Message, SwiftProtobu static let protoMessageName: String = ModuleConfig.protoMessageName + ".RemoteHardwareConfig" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "enabled"), + 2: .standard(proto: "allow_undefined_pin_access"), + 3: .standard(proto: "available_pins"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1196,6 +1293,8 @@ extension ModuleConfig.RemoteHardwareConfig: SwiftProtobuf.Message, SwiftProtobu // 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.decodeSingularBoolField(value: &self.allowUndefinedPinAccess) }() + case 3: try { try decoder.decodeRepeatedMessageField(value: &self.availablePins) }() default: break } } @@ -1205,11 +1304,19 @@ extension ModuleConfig.RemoteHardwareConfig: SwiftProtobuf.Message, SwiftProtobu if self.enabled != false { try visitor.visitSingularBoolField(value: self.enabled, fieldNumber: 1) } + if self.allowUndefinedPinAccess != false { + try visitor.visitSingularBoolField(value: self.allowUndefinedPinAccess, fieldNumber: 2) + } + if !self.availablePins.isEmpty { + try visitor.visitRepeatedMessageField(value: self.availablePins, fieldNumber: 3) + } try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: ModuleConfig.RemoteHardwareConfig, rhs: ModuleConfig.RemoteHardwareConfig) -> Bool { if lhs.enabled != rhs.enabled {return false} + if lhs.allowUndefinedPinAccess != rhs.allowUndefinedPinAccess {return false} + if lhs.availablePins != rhs.availablePins {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1778,3 +1885,47 @@ extension ModuleConfig.CannedMessageConfig.InputEventChar: SwiftProtobuf._ProtoN 27: .same(proto: "BACK"), ] } + +extension RemoteHardwarePin: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".RemoteHardwarePin" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "gpio_pin"), + 2: .same(proto: "name"), + 3: .same(proto: "type"), + ] + + 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.decodeSingularUInt32Field(value: &self.gpioPin) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.name) }() + case 3: try { try decoder.decodeSingularEnumField(value: &self.type) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.gpioPin != 0 { + try visitor.visitSingularUInt32Field(value: self.gpioPin, fieldNumber: 1) + } + if !self.name.isEmpty { + try visitor.visitSingularStringField(value: self.name, fieldNumber: 2) + } + if self.type != .unknown { + try visitor.visitSingularEnumField(value: self.type, fieldNumber: 3) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: RemoteHardwarePin, rhs: RemoteHardwarePin) -> Bool { + if lhs.gpioPin != rhs.gpioPin {return false} + if lhs.name != rhs.name {return false} + if lhs.type != rhs.type {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/Meshtastic/Protobufs/meshtastic/portnums.pb.swift b/Meshtastic/Protobufs/meshtastic/portnums.pb.swift index d52e3777..d784a013 100644 --- a/Meshtastic/Protobufs/meshtastic/portnums.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/portnums.pb.swift @@ -124,10 +124,10 @@ enum PortNum: SwiftProtobuf.Enum { case zpsApp // = 68 /// - /// Used to let multiple instances of Linux native applications communicate + /// Used to let multiple instances of Linux native applications communicate /// as if they did using their LoRa chip. - /// Maintained by GitHub user GUVWAF. - /// Project files at https://github.com/GUVWAF/Meshtasticator + /// Maintained by GitHub user GUVWAF. + /// Project files at https://github.com/GUVWAF/Meshtasticator case simulatorApp // = 69 /// diff --git a/Meshtastic/Views/Helpers/BatteryLevelCompact.swift b/Meshtastic/Views/Helpers/BatteryLevelCompact.swift new file mode 100644 index 00000000..8cbd4764 --- /dev/null +++ b/Meshtastic/Views/Helpers/BatteryLevelCompact.swift @@ -0,0 +1,88 @@ +// +// BatteryIcon.swift +// Meshtastic +// +// Copyright Garth Vander Houwen 3/24/23. +// +import SwiftUI + +struct BatteryLevelCompact: View { + var batteryLevel: Int32? + var font: Font + var iconFont: Font + var color: Color + + var body: some View { + + HStack (alignment: .center, spacing: 0) { + if batteryLevel == 100 { + + Image(systemName: "battery.100.bolt") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + } else if batteryLevel! < 100 && batteryLevel! > 74 { + + Image(systemName: "battery.75") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + } else if batteryLevel! < 75 && batteryLevel! > 49 { + + Image(systemName: "battery.50") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + } else if batteryLevel! < 50 && batteryLevel! > 14 { + + Image(systemName: "battery.25") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + } else if batteryLevel! < 15 && batteryLevel! > 0 { + + Image(systemName: "battery.0") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + + } else if batteryLevel! == 0 { + + Image(systemName: "battery.0") + .font(iconFont) + .foregroundColor(.red) + .symbolRenderingMode(.hierarchical) + } else if batteryLevel! > 100 { + + Image(systemName: "powerplug") + .font(iconFont) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + } + + if batteryLevel ?? 0 > 100 { + Text("PWD") + .font(font) + } else if batteryLevel == 100 { + Text("CHG") + .font(font) + } else { + Text("\(batteryLevel ?? 0)%") + .font(font) + } + } + } +} + +struct BatteryLevelCompact_Previews: PreviewProvider { + static var previews: some View { + VStack { + BatteryLevelCompact(batteryLevel: 111, font: .footnote, iconFont: .callout, color: Color.accentColor) + BatteryLevelCompact(batteryLevel: 100, font: .footnote, iconFont: .callout, color: Color.accentColor) + BatteryLevelCompact(batteryLevel: 99, font: .footnote, iconFont: .callout, color: Color.accentColor) + BatteryLevelCompact(batteryLevel: 74, font: .footnote, iconFont: .callout, color: Color.accentColor) + BatteryLevelCompact(batteryLevel: 49, font: .footnote, iconFont: .callout, color: Color.accentColor) + BatteryLevelCompact(batteryLevel: 14, font: .footnote, iconFont: .callout, color: Color.accentColor) + } + } +} diff --git a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift new file mode 100644 index 00000000..38bf0dab --- /dev/null +++ b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift @@ -0,0 +1,96 @@ +// +// LoRaSignalStrength.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 5/15/23. +// +import Foundation +import SwiftUI + +struct LoRaSignalStrengthMeter: View { + + var snr: Float + var rssi: Int32 + var preset: ModemPresets + var compact: Bool + + var body: some View { + + let signalStrength = getLoRaSignalStrength(snr: snr, rssi: rssi, preset: preset) + let gradient = Gradient(colors: [.red, .orange, .yellow, .green]) + + if !compact { + VStack { + LoRaSignalStrengthIndicator(signalStrength: signalStrength) + Text("Signal \(signalStrength.description)").font(.footnote) + Text("SNR \(String(format: "%.2f", snr))dB") + .foregroundColor(getSnrColor(snr: snr, preset: ModemPresets.longFast)) + .font(.caption2) + Text("RSSI \(rssi)dB") + .foregroundColor(getRssiColor(rssi: rssi)) + .font(.caption2) + } + } else { + Gauge(value: Double(signalStrength.rawValue), in: 0...3) { + } currentValueLabel: { + Image(systemName: "dot.radiowaves.left.and.right") + .font(.caption) + Text("Signal \(signalStrength.description)") + .font(.caption) + } + .gaugeStyle(.accessoryLinear) + .tint(gradient) + .font(.caption) + } + } +} + +struct LoRaSignalStrengthMeter_Previews: PreviewProvider { + static var previews: some View { + ScrollView { + VStack { + HStack { + // Good + LoRaSignalStrengthMeter(snr: -10, rssi: -100, preset: ModemPresets.longFast, compact: false) + LoRaSignalStrengthMeter(snr: -17, rssi: -100, preset: ModemPresets.longFast, compact: false) + } + HStack { + // Fair + LoRaSignalStrengthMeter(snr: -9.5, rssi: -119, preset: ModemPresets.longFast, compact: false) + LoRaSignalStrengthMeter(snr: -17.5, rssi: -100, preset: ModemPresets.longFast, compact: false) + } + HStack { + // Bad + LoRaSignalStrengthMeter(snr: -11.25, rssi: -120, preset: ModemPresets.longFast, compact: false) + LoRaSignalStrengthMeter(snr: -12.75, rssi: -139, preset: ModemPresets.longFast, compact: false) + } + HStack { + LoRaSignalStrengthMeter(snr: -20.25, rssi: -128, preset: ModemPresets.longFast, compact: false) + LoRaSignalStrengthMeter(snr: -30, rssi: -120, preset: ModemPresets.longFast, compact: false) + } + HStack { + LoRaSignalStrengthMeter(snr: -15, rssi: -124, preset: ModemPresets.longFast, compact: false) + LoRaSignalStrengthMeter(snr: -17.25, rssi: -126, preset: ModemPresets.longFast, compact: false) + LoRaSignalStrengthMeter(snr: -19.5, rssi: -128, preset: ModemPresets.longFast, compact: false) + } + HStack { + // None + LoRaSignalStrengthMeter(snr: -26.0, rssi: -129, preset: ModemPresets.longFast, compact: false) + } + } + } + + VStack { + // Good + LoRaSignalStrengthMeter(snr: -10, rssi: -100, preset: ModemPresets.longFast, compact: true) + // Fair + LoRaSignalStrengthMeter(snr: -9.5, rssi: -119, preset: ModemPresets.longFast, compact: true) + // Bad + LoRaSignalStrengthMeter(snr: -12.75, rssi: -139, preset: ModemPresets.longFast, compact: true) + // None + LoRaSignalStrengthMeter(snr: -26.0, rssi: -128, preset: ModemPresets.longFast, compact: true) + } + } +} + + diff --git a/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift b/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift index 5f88d6d5..3df05659 100644 --- a/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift +++ b/Meshtastic/Views/Helpers/LoRaSignalStrengthIndicator.swift @@ -5,8 +5,6 @@ // Copyright Garth Vander Houwen 5/9/23. // -import Foundation - import Foundation import SwiftUI @@ -18,22 +16,25 @@ struct LoRaSignalStrengthIndicator: View { ForEach(0..<3) { bar in RoundedRectangle(cornerRadius: 3) .divided(amount: (CGFloat(bar) + 1) / CGFloat(3)) - .fill(getColor().opacity(bar <= signalStrength.rawValue ? 1 : 0.3)) + .fill(getColor(signalStrength: signalStrength).opacity(bar <= signalStrength.rawValue ? 1 : 0.3)) .frame(width: 8, height: 40) } } } +} - private func getColor() -> Color { - switch signalStrength { - case .none: - return Color.red - case .bad: - return Color.orange - case .fair: - return Color.yellow - case .good: - return Color.green +struct LoRaSignalStrengthIndicator_Previews: PreviewProvider { + static var previews: some View { + VStack { + let signalStrength = getLoRaSignalStrength(snr: -12.75, rssi: -139, preset: ModemPresets.longFast) + LoRaSignalStrengthIndicator(signalStrength: signalStrength) + Text("Signal \(signalStrength.description)").font(.footnote) + Text("SNR \(String(format: "%.2f", -12.75))dB") + .foregroundColor(getSnrColor(snr: -12.75, preset: ModemPresets.longFast)) + .font(.caption2) + Text("RSSI \(-139)dB") + .foregroundColor(getRssiColor(rssi: -139)) + .font(.caption2) } } } @@ -57,13 +58,26 @@ enum LoRaSignalStrength: Int { } } -func getLoRaSignalStrength(snr: Float, rssi: Int32) -> LoRaSignalStrength { +private func getColor(signalStrength: LoRaSignalStrength) -> Color { + switch signalStrength { + case .none: + return Color.red + case .bad: + return Color.orange + case .fair: + return Color.yellow + case .good: + return Color.green + } +} + +func getLoRaSignalStrength(snr: Float, rssi: Int32, preset: ModemPresets) -> LoRaSignalStrength { - if rssi > -115 && snr > -7 { + if rssi > -115 && snr > (preset.snrLimit()) { return .good - } else if rssi < -126 && snr < -15 { + } else if rssi < -126 && snr < (preset.snrLimit() - 7.5) { return .none - } else if rssi <= -120 || snr <= -13 { + } else if rssi <= -120 || snr <= (preset.snrLimit() - 5.5) { return .bad } else { return .fair @@ -86,14 +100,14 @@ func getRssiColor(rssi: Int32) -> Color { } } -func getSnrColor(snr: Float) -> Color { - if snr > -7 { +func getSnrColor(snr: Float, preset: ModemPresets) -> Color { + if snr > preset.snrLimit() { /// Good return .green - } else if snr < -7 && snr > -13 { + } else if snr < preset.snrLimit() && snr > (preset.snrLimit() - 5.5) { /// Fair return .yellow - } else if snr >= -14 { + } else if snr >= (preset.snrLimit() - 7.5) { /// Bad return .orange } else { diff --git a/Meshtastic/Views/Helpers/Node/NodeInfoView.swift b/Meshtastic/Views/Helpers/Node/NodeInfoView.swift index 0c3b95d4..701adb38 100644 --- a/Meshtastic/Views/Helpers/Node/NodeInfoView.swift +++ b/Meshtastic/Views/Helpers/Node/NodeInfoView.swift @@ -47,11 +47,11 @@ struct NodeInfoView: View { Divider() if node.snr != 0 { VStack(alignment: .center) { - let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi) + let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: ModemPresets.longModerate) LoRaSignalStrengthIndicator(signalStrength: signalStrength) Text("Signal \(signalStrength.description)").font(.title) Text("SNR \(String(format: "%.2f", node.snr))dB") - .foregroundColor(getSnrColor(snr: node.snr)) + .foregroundColor(getSnrColor(snr: node.snr, preset: ModemPresets.longModerate)) .font(.title3) Text("RSSI \(node.rssi)dB") .foregroundColor(getRssiColor(rssi: node.rssi)) @@ -156,11 +156,11 @@ struct NodeInfoView: View { if node.snr != 0 { Divider() VStack(alignment: .center) { - let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi) + let signalStrength = getLoRaSignalStrength(snr: node.snr, rssi: node.rssi, preset: ModemPresets.longModerate) LoRaSignalStrengthIndicator(signalStrength: signalStrength) Text("Signal \(signalStrength.description)").font(.footnote) Text("SNR \(String(format: "%.2f", node.snr))dB") - .foregroundColor(getSnrColor(snr: node.snr)) + .foregroundColor(getSnrColor(snr: node.snr, preset: ModemPresets.longModerate)) .font(.caption2) Text("RSSI \(node.rssi)dB") .foregroundColor(getRssiColor(rssi: node.rssi)) diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 4a242f88..d8c1d001 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -4,9 +4,18 @@ // // Copyright(c) Josh Pirihi & Garth Vander Houwen 1/16/22. +import Foundation import SwiftUI import MapKit +struct PolygonInfo: Codable { + let stroke: String? + let strokeWidth, strokeOpacity: Int? + let fill: String? + let fillOpacity: Double? + let title, subtitle: String? +} + func degreesToRadians(_ number: Double) -> Double { return number * .pi / 180 } @@ -29,7 +38,7 @@ struct MapViewSwiftUI: UIViewRepresentable { let showRouteLines: Bool let mapViewType: MKMapType = MKMapType.standard - + // Offline Map Tiles @AppStorage("lastUpdatedLocalMapFile") private var lastUpdatedLocalMapFile = 0 @State private var loadedLastUpdatedLocalMapFile = 0 @@ -65,6 +74,7 @@ struct MapViewSwiftUI: UIViewRepresentable { } // Other MKMapView Settings mapView.preferredConfiguration.elevationStyle = .realistic// .flat + mapView.pointOfInterestFilter = MKPointOfInterestFilter.excludingAll mapView.isPitchEnabled = true mapView.isRotateEnabled = true mapView.isScrollEnabled = true @@ -134,14 +144,7 @@ struct MapViewSwiftUI: UIViewRepresentable { } } - func makeUIView(context: Context) -> MKMapView { - currentMapLayer = nil - mapView.delegate = context.coordinator - self.configureMap(mapView: mapView) - return mapView - } - - func updateUIView(_ mapView: MKMapView, context: Context) { + private func setMbtilesOverlay(mapView: MKMapView) { // MBTiles Offline if UserDefaults.enableOfflineMaps && UserDefaults.enableOfflineMapsMBTiles { @@ -149,7 +152,7 @@ struct MapViewSwiftUI: UIViewRepresentable { if self.customMapOverlay != self.presentCustomMapOverlayHash || self.loadedLastUpdatedLocalMapFile != self.lastUpdatedLocalMapFile { mapView.removeOverlays(mapView.overlays) if self.customMapOverlay != nil { - + let fileManager = FileManager.default let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! let tilePath = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false).path @@ -169,15 +172,69 @@ struct MapViewSwiftUI: UIViewRepresentable { } } } + } + + private func setGeoJsonOverlay(mapView: MKMapView) { + + guard let geoJsonFileUrl = URL(string: "https://raw.githubusercontent.com/johan/world.geo.json/master/countries.geo.json"), // Bundle.main.url(forResource: "location", withExtension: "geojson"), + //guard let geoJsonFileUrl = URL(string: "https://hrbrmstr.github.io/noaa-alerts-sp-to-geojson/current-all.geojson"), + let geoJsonData = try? Data.init(contentsOf: geoJsonFileUrl) else { + fatalError("Failure to fetch the file.") + } + guard let objs = try? MKGeoJSONDecoder().decode(geoJsonData) as? [MKGeoJSONFeature] else { + fatalError("Wrong format") + } + // Parse the objects + objs.forEach { (feature) in + guard let geometry = feature.geometry.first, + let propData = feature.properties else { + return; + } + // Check if it is MKPolygon + if let polygon = geometry as? MKPolygon { + let polygonInfo = try? JSONDecoder.init().decode(PolygonInfo.self, from: propData) + mapView.addOverlay(polygon) + //self.view?.render(overlay: polygon, info: polygonInfo) + } + // Check if it is MKPolyline + if let polyline = geometry as? MKPolyline { + mapView.addOverlay(polyline, level: .aboveLabels) + //let polylineInfo = try? JSONDecoder.init().decode(PolylineInfo.self, from: propData) + //self.view?.render(overlay: polyline, info: polylineInfo) + } + + // Check if it is MKPointAnnotation + // if let annotation = geometry as? MKPointAnnotation { + // let info = try? JSONDecoder.init().decode(Info.self, from: propData) + // let storeAnnotation = StoreAnnotation.init(title: info?.name, + // subtitle: info?.subTitle, + // website: info?.website, + // coordinate: annotation.coordinate) + // self.view?.setAnnotations(annotations: [storeAnnotation]) + // } + } + } + + func makeUIView(context: Context) -> MKMapView { + currentMapLayer = nil + mapView.delegate = context.coordinator + self.configureMap(mapView: mapView) + return mapView + } + + func updateUIView(_ mapView: MKMapView, context: Context) { + + // Set MBTiles overlay layer + setMbtilesOverlay(mapView: mapView) // Set selected map base layer setMapBaseLayer(mapView: mapView) - // Set map overlay layer + // Set map tile server and weather overlay layers setMapOverlays(mapView: mapView) let latest = positions .filter { $0.latest == true } .sorted { $0.nodePosition?.num ?? 0 > $1.nodePosition?.num ?? -1 } - + // Node Route Lines if showRouteLines { // Remove all existing PolyLine Overlays @@ -437,7 +494,14 @@ struct MapViewSwiftUI: UIViewRepresentable { renderer.lineWidth = 8 return renderer } - return MKOverlayRenderer() + if let polygon = overlay as? MKPolygon { + let renderer = MKPolygonRenderer(polygon: polygon) + renderer.fillColor = UIColor.purple.withAlphaComponent(0.2) + renderer.strokeColor = .purple.withAlphaComponent(0.7) + + return renderer + } + return MKOverlayRenderer(overlay: overlay) } } } diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 917cac0a..8e3ad3c5 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -54,7 +54,7 @@ struct NodeDetail: View { var body: some View { let hwModelString = node.user?.hwModel ?? "UNSET" - + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context) NavigationStack { GeometryReader { bounds in VStack { @@ -84,19 +84,21 @@ struct NodeDetail: View { VStack(alignment: .leading) { Spacer() HStack(alignment: .bottom, spacing: 1) { - -// Picker("Map Type", selection: $mapType) { -// ForEach(MeshMapTypes.allCases) { map in -// Text(map.description) -// .tag(map.MKMapTypeValue()) -// } -// } -// .onChange(of: (mapType)) { newMapType in -// UserDefaults.mapType = Int(newMapType.rawValue) -// } -// .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) -// .pickerStyle(.menu) -// .padding(5) + Picker("Map Type", selection: $selectedMapLayer) { + ForEach(MapLayer.allCases, id: \.self) { layer in + if layer == MapLayer.offline && UserDefaults.enableOfflineMaps { + Text(layer.localized) + } else if layer != MapLayer.offline { + Text(layer.localized) + } + } + } + .onChange(of: (selectedMapLayer)) { newMapLayer in + UserDefaults.mapLayer = newMapLayer + } + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) + .pickerStyle(.menu) + .padding(5) VStack { Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName) .font(.caption) @@ -151,7 +153,7 @@ struct NodeDetail: View { if self.bleManager.connectedPeripheral != nil && node.metadata != nil { HStack { - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context) + if node.metadata?.canShutdown ?? false { Button(action: { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index f04291e2..251962e0 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -27,22 +27,31 @@ struct NodeList: View { var body: some View { NavigationSplitView { + let connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) + let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) List(nodes, id: \.self, selection: $selection) { node in if nodes.count == 0 { Text("no.nodes").font(.title) } else { NavigationLink(value: node) { let connected: Bool = (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num) - VStack(alignment: .leading) { + LazyVStack(alignment: .leading) { HStack { - CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 44 : 22, brightness: 0.0, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white) - .padding(.trailing, 5) + VStack(alignment: .leading) { + CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 44 : 22, brightness: 0.0, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white) + .padding(.trailing, 5) + let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) + if deviceMetrics?.count ?? 0 >= 1 { + let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity + BatteryLevelCompact(batteryLevel: mostRecent?.batteryLevel, font: .caption2, iconFont: .callout, color: .accentColor) + } + } VStack(alignment: .leading) { Text(node.user?.longName ?? "unknown".localized) .fontWeight(.medium) .font(.callout) if connected { - HStack { + HStack(alignment: .bottom) { Image(systemName: "repeat.circle.fill") .font(.callout) .symbolRenderingMode(.hierarchical) @@ -80,6 +89,11 @@ struct NodeList: View { LastHeardText(lastHeard: node.lastHeard) .font(.caption) } + if !connected { + HStack(alignment: .bottom) { let preset = ModemPresets(rawValue: Int(connectedNode?.loRaConfig?.modemPreset ?? 0)) + LoRaSignalStrengthMeter(snr: node.snr, rssi: node.rssi, preset: preset ?? ModemPresets.longFast, compact: true) + } + } } .frame(maxWidth: .infinity, alignment: .leading) } diff --git a/Meshtastic/Views/Nodes/NodeMap.swift b/Meshtastic/Views/Nodes/NodeMap.swift index 9ebe55ee..95a2aecd 100644 --- a/Meshtastic/Views/Nodes/NodeMap.swift +++ b/Meshtastic/Views/Nodes/NodeMap.swift @@ -178,14 +178,14 @@ struct NodeMap: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .onChange(of: (enableOfflineMaps)) { newEnableOfflineMaps in - UserDefaults.enableOfflineMaps = newEnableOfflineMaps - if !newEnableOfflineMaps { + UserDefaults.enableOfflineMaps = enableOfflineMaps + if !enableOfflineMaps { if self.selectedMapLayer == .offline { self.selectedMapLayer = .standard } } } - if UserDefaults.enableOfflineMaps { + if enableOfflineMaps { VStack (alignment: .leading) { if !enableOfflineMapsMBTiles { diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 0dc8e05d..55b6b423 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -262,54 +262,42 @@ struct DeviceConfig: View { } } .onChange(of: deviceRole) { newRole in - - if node != nil && node!.deviceConfig != nil { - + if node != nil && node?.deviceConfig != nil { if newRole != node!.deviceConfig!.role { hasChanges = true } } } .onChange(of: serialEnabled) { newSerial in - - if node != nil && node!.deviceConfig != nil { - + if node != nil && node?.deviceConfig != nil { if newSerial != node!.deviceConfig!.serialEnabled { hasChanges = true } } } .onChange(of: debugLogEnabled) { newDebugLog in - - if node != nil && node!.deviceConfig != nil { - + if node != nil && node?.deviceConfig != nil { if newDebugLog != node!.deviceConfig!.debugLogEnabled { hasChanges = true } } } .onChange(of: buttonGPIO) { newButtonGPIO in - - if node != nil && node!.deviceConfig != nil { - + if node != nil && node?.deviceConfig != nil { if newButtonGPIO != node!.deviceConfig!.buttonGpio { hasChanges = true } } } .onChange(of: buzzerGPIO) { newBuzzerGPIO in - - if node != nil && node!.deviceConfig != nil { - + if node != nil && node?.deviceConfig != nil { if newBuzzerGPIO != node!.deviceConfig!.buttonGpio { hasChanges = true } } } .onChange(of: rebroadcastMode) { newRebroadcastMode in - - if node != nil && node!.deviceConfig != nil { - + if node != nil && node?.deviceConfig != nil { if newRebroadcastMode != node!.deviceConfig!.rebroadcastMode { hasChanges = true } } } .onChange(of: doubleTapAsButtonPress) { newDoubleTapAsButtonPress in - if node != nil && node!.deviceConfig != nil { + if node != nil && node?.deviceConfig != nil { if newDoubleTapAsButtonPress != node!.deviceConfig!.doubleTapAsButtonPress { hasChanges = true } } } .onChange(of: isManaged) { newIsManaged in - if node != nil && node!.deviceConfig != nil { + if node != nil && node?.deviceConfig != nil { if newIsManaged != node!.deviceConfig!.isManaged { hasChanges = true } } }