diff --git a/Meshtastic/Protobufs/meshtastic/clientonly.pb.swift b/Meshtastic/Protobufs/meshtastic/clientonly.pb.swift index 050c719d..64fd71c0 100644 --- a/Meshtastic/Protobufs/meshtastic/clientonly.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/clientonly.pb.swift @@ -94,8 +94,22 @@ struct DeviceProfile { fileprivate var _moduleConfig: LocalModuleConfig? = nil } +/// +/// A heartbeat message is sent by a node to indicate that it is still alive. +/// This is currently only needed to keep serial connections alive. +struct Heartbeat { + // 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. + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + #if swift(>=5.5) && canImport(_Concurrency) extension DeviceProfile: @unchecked Sendable {} +extension Heartbeat: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -161,3 +175,22 @@ extension DeviceProfile: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa return true } } + +extension Heartbeat: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".Heartbeat" + static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + mutating func decodeMessage(decoder: inout D) throws { + while let _ = try decoder.nextFieldNumber() { + } + } + + func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: Heartbeat, rhs: Heartbeat) -> Bool { + 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 3aee0200..b688479b 100644 --- a/Meshtastic/Protobufs/meshtastic/module_config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/module_config.pb.swift @@ -376,6 +376,43 @@ struct ModuleConfig { /// If true, we can use the connected phone / client to proxy messages to MQTT instead of a direct connection var proxyToClientEnabled: Bool = false + /// + /// If true, we will periodically report unencrypted information about our node to a map via MQTT + var mapReportingEnabled: Bool = false + + /// + /// Settings for reporting information about our node to a map via MQTT + var mapReportSettings: ModuleConfig.MapReportSettings { + get {return _mapReportSettings ?? ModuleConfig.MapReportSettings()} + set {_mapReportSettings = newValue} + } + /// Returns true if `mapReportSettings` has been explicitly set. + var hasMapReportSettings: Bool {return self._mapReportSettings != nil} + /// Clears the value of `mapReportSettings`. Subsequent reads from it will return its default value. + mutating func clearMapReportSettings() {self._mapReportSettings = nil} + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _mapReportSettings: ModuleConfig.MapReportSettings? = nil + } + + /// + /// Settings for reporting unencrypted information about our node to a map via MQTT + struct MapReportSettings { + // 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. + + /// + /// How often we should report our info to the map (in seconds) + var publishIntervalSecs: UInt32 = 0 + + /// + /// Bits of precision for the location sent (default of 32 is full precision). + var positionPrecision: UInt32 = 0 + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -1207,6 +1244,7 @@ extension RemoteHardwarePinType: @unchecked Sendable {} extension ModuleConfig: @unchecked Sendable {} extension ModuleConfig.OneOf_PayloadVariant: @unchecked Sendable {} extension ModuleConfig.MQTTConfig: @unchecked Sendable {} +extension ModuleConfig.MapReportSettings: @unchecked Sendable {} extension ModuleConfig.RemoteHardwareConfig: @unchecked Sendable {} extension ModuleConfig.NeighborInfoConfig: @unchecked Sendable {} extension ModuleConfig.DetectionSensorConfig: @unchecked Sendable {} @@ -1518,6 +1556,8 @@ extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._Message 7: .standard(proto: "tls_enabled"), 8: .same(proto: "root"), 9: .standard(proto: "proxy_to_client_enabled"), + 10: .standard(proto: "map_reporting_enabled"), + 11: .standard(proto: "map_report_settings"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1535,12 +1575,18 @@ extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._Message case 7: try { try decoder.decodeSingularBoolField(value: &self.tlsEnabled) }() case 8: try { try decoder.decodeSingularStringField(value: &self.root) }() case 9: try { try decoder.decodeSingularBoolField(value: &self.proxyToClientEnabled) }() + case 10: try { try decoder.decodeSingularBoolField(value: &self.mapReportingEnabled) }() + case 11: try { try decoder.decodeSingularMessageField(value: &self._mapReportSettings) }() 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.enabled != false { try visitor.visitSingularBoolField(value: self.enabled, fieldNumber: 1) } @@ -1568,6 +1614,12 @@ extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._Message if self.proxyToClientEnabled != false { try visitor.visitSingularBoolField(value: self.proxyToClientEnabled, fieldNumber: 9) } + if self.mapReportingEnabled != false { + try visitor.visitSingularBoolField(value: self.mapReportingEnabled, fieldNumber: 10) + } + try { if let v = self._mapReportSettings { + try visitor.visitSingularMessageField(value: v, fieldNumber: 11) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -1581,6 +1633,46 @@ extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._Message if lhs.tlsEnabled != rhs.tlsEnabled {return false} if lhs.root != rhs.root {return false} if lhs.proxyToClientEnabled != rhs.proxyToClientEnabled {return false} + if lhs.mapReportingEnabled != rhs.mapReportingEnabled {return false} + if lhs._mapReportSettings != rhs._mapReportSettings {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension ModuleConfig.MapReportSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = ModuleConfig.protoMessageName + ".MapReportSettings" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "publish_interval_secs"), + 2: .standard(proto: "position_precision"), + ] + + 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.publishIntervalSecs) }() + case 2: try { try decoder.decodeSingularUInt32Field(value: &self.positionPrecision) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if self.publishIntervalSecs != 0 { + try visitor.visitSingularUInt32Field(value: self.publishIntervalSecs, fieldNumber: 1) + } + if self.positionPrecision != 0 { + try visitor.visitSingularUInt32Field(value: self.positionPrecision, fieldNumber: 2) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: ModuleConfig.MapReportSettings, rhs: ModuleConfig.MapReportSettings) -> Bool { + if lhs.publishIntervalSecs != rhs.publishIntervalSecs {return false} + if lhs.positionPrecision != rhs.positionPrecision {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Protobufs/meshtastic/mqtt.pb.swift b/Meshtastic/Protobufs/meshtastic/mqtt.pb.swift index 73fe4c30..43000bd1 100644 --- a/Meshtastic/Protobufs/meshtastic/mqtt.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mqtt.pb.swift @@ -55,8 +55,75 @@ struct ServiceEnvelope { fileprivate var _packet: MeshPacket? = nil } +/// +/// Information about a node intended to be reported unencrypted to a map using MQTT. +struct MapReport { + // 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. + + /// + /// A full name for this user, i.e. "Kevin Hester" + var longName: String = String() + + /// + /// A VERY short name, ideally two characters. + /// Suitable for a tiny OLED screen + var shortName: String = String() + + /// + /// Role of the node that applies specific settings for a particular use-case + var role: Config.DeviceConfig.Role = .client + + /// + /// Hardware model of the node, i.e. T-Beam, Heltec V3, etc... + var hwModel: HardwareModel = .unset + + /// + /// Device firmware version string + var firmwareVersion: String = String() + + /// + /// The region code for the radio (US, CN, EU433, etc...) + var region: Config.LoRaConfig.RegionCode = .unset + + /// + /// Modem preset used by the radio (LongFast, MediumSlow, etc...) + var modemPreset: Config.LoRaConfig.ModemPreset = .longFast + + /// + /// Whether the node has a channel with default PSK and name (LongFast, MediumSlow, etc...) + /// and it uses the default frequency slot given the region and modem preset. + var hasDefaultChannel_p: Bool = false + + /// + /// Latitude: multiply by 1e-7 to get degrees in floating point + var latitudeI: Int32 = 0 + + /// + /// Longitude: multiply by 1e-7 to get degrees in floating point + var longitudeI: Int32 = 0 + + /// + /// Altitude in meters above MSL + var altitude: Int32 = 0 + + /// + /// Indicates the bits of precision for latitude and longitude set by the sending node + var positionPrecision: UInt32 = 0 + + /// + /// Number of online nodes (heard in the last 2 hours) this node has in its list that were received locally (not via MQTT) + var numOnlineLocalNodes: UInt32 = 0 + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + #if swift(>=5.5) && canImport(_Concurrency) extension ServiceEnvelope: @unchecked Sendable {} +extension MapReport: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -110,3 +177,107 @@ extension ServiceEnvelope: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen return true } } + +extension MapReport: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".MapReport" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .standard(proto: "long_name"), + 2: .standard(proto: "short_name"), + 3: .same(proto: "role"), + 4: .standard(proto: "hw_model"), + 5: .standard(proto: "firmware_version"), + 6: .same(proto: "region"), + 7: .standard(proto: "modem_preset"), + 8: .standard(proto: "has_default_channel"), + 9: .standard(proto: "latitude_i"), + 10: .standard(proto: "longitude_i"), + 11: .same(proto: "altitude"), + 12: .standard(proto: "position_precision"), + 13: .standard(proto: "num_online_local_nodes"), + ] + + 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.decodeSingularStringField(value: &self.longName) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.shortName) }() + case 3: try { try decoder.decodeSingularEnumField(value: &self.role) }() + case 4: try { try decoder.decodeSingularEnumField(value: &self.hwModel) }() + case 5: try { try decoder.decodeSingularStringField(value: &self.firmwareVersion) }() + case 6: try { try decoder.decodeSingularEnumField(value: &self.region) }() + case 7: try { try decoder.decodeSingularEnumField(value: &self.modemPreset) }() + case 8: try { try decoder.decodeSingularBoolField(value: &self.hasDefaultChannel_p) }() + case 9: try { try decoder.decodeSingularSFixed32Field(value: &self.latitudeI) }() + case 10: try { try decoder.decodeSingularSFixed32Field(value: &self.longitudeI) }() + case 11: try { try decoder.decodeSingularInt32Field(value: &self.altitude) }() + case 12: try { try decoder.decodeSingularUInt32Field(value: &self.positionPrecision) }() + case 13: try { try decoder.decodeSingularUInt32Field(value: &self.numOnlineLocalNodes) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.longName.isEmpty { + try visitor.visitSingularStringField(value: self.longName, fieldNumber: 1) + } + if !self.shortName.isEmpty { + try visitor.visitSingularStringField(value: self.shortName, fieldNumber: 2) + } + if self.role != .client { + try visitor.visitSingularEnumField(value: self.role, fieldNumber: 3) + } + if self.hwModel != .unset { + try visitor.visitSingularEnumField(value: self.hwModel, fieldNumber: 4) + } + if !self.firmwareVersion.isEmpty { + try visitor.visitSingularStringField(value: self.firmwareVersion, fieldNumber: 5) + } + if self.region != .unset { + try visitor.visitSingularEnumField(value: self.region, fieldNumber: 6) + } + if self.modemPreset != .longFast { + try visitor.visitSingularEnumField(value: self.modemPreset, fieldNumber: 7) + } + if self.hasDefaultChannel_p != false { + try visitor.visitSingularBoolField(value: self.hasDefaultChannel_p, fieldNumber: 8) + } + if self.latitudeI != 0 { + try visitor.visitSingularSFixed32Field(value: self.latitudeI, fieldNumber: 9) + } + if self.longitudeI != 0 { + try visitor.visitSingularSFixed32Field(value: self.longitudeI, fieldNumber: 10) + } + if self.altitude != 0 { + try visitor.visitSingularInt32Field(value: self.altitude, fieldNumber: 11) + } + if self.positionPrecision != 0 { + try visitor.visitSingularUInt32Field(value: self.positionPrecision, fieldNumber: 12) + } + if self.numOnlineLocalNodes != 0 { + try visitor.visitSingularUInt32Field(value: self.numOnlineLocalNodes, fieldNumber: 13) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: MapReport, rhs: MapReport) -> Bool { + if lhs.longName != rhs.longName {return false} + if lhs.shortName != rhs.shortName {return false} + if lhs.role != rhs.role {return false} + if lhs.hwModel != rhs.hwModel {return false} + if lhs.firmwareVersion != rhs.firmwareVersion {return false} + if lhs.region != rhs.region {return false} + if lhs.modemPreset != rhs.modemPreset {return false} + if lhs.hasDefaultChannel_p != rhs.hasDefaultChannel_p {return false} + if lhs.latitudeI != rhs.latitudeI {return false} + if lhs.longitudeI != rhs.longitudeI {return false} + if lhs.altitude != rhs.altitude {return false} + if lhs.positionPrecision != rhs.positionPrecision {return false} + if lhs.numOnlineLocalNodes != rhs.numOnlineLocalNodes {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 937ff635..c8948d7d 100644 --- a/Meshtastic/Protobufs/meshtastic/portnums.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/portnums.pb.swift @@ -181,6 +181,10 @@ enum PortNum: SwiftProtobuf.Enum { /// Portnum for payloads from the official Meshtastic ATAK plugin case atakPlugin // = 72 + /// + /// Provides unencrypted information about a node for consumption by a map via MQTT + case mapReportApp // = 73 + /// /// Private applications should use portnums >= 256. /// To simplify initial development and testing you can use "PRIVATE_APP" @@ -226,6 +230,7 @@ enum PortNum: SwiftProtobuf.Enum { case 70: self = .tracerouteApp case 71: self = .neighborinfoApp case 72: self = .atakPlugin + case 73: self = .mapReportApp case 256: self = .privateApp case 257: self = .atakForwarder case 511: self = .max @@ -258,6 +263,7 @@ enum PortNum: SwiftProtobuf.Enum { case .tracerouteApp: return 70 case .neighborinfoApp: return 71 case .atakPlugin: return 72 + case .mapReportApp: return 73 case .privateApp: return 256 case .atakForwarder: return 257 case .max: return 511 @@ -295,6 +301,7 @@ extension PortNum: CaseIterable { .tracerouteApp, .neighborinfoApp, .atakPlugin, + .mapReportApp, .privateApp, .atakForwarder, .max, @@ -334,6 +341,7 @@ extension PortNum: SwiftProtobuf._ProtoNameProviding { 70: .same(proto: "TRACEROUTE_APP"), 71: .same(proto: "NEIGHBORINFO_APP"), 72: .same(proto: "ATAK_PLUGIN"), + 73: .same(proto: "MAP_REPORT_APP"), 256: .same(proto: "PRIVATE_APP"), 257: .same(proto: "ATAK_FORWARDER"), 511: .same(proto: "MAX"), diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index e1e87b6a..38ce2e1b 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -43,7 +43,7 @@ struct DeviceMetricsLog: View { .accessibilityLabel("Line Series") .accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)") .foregroundStyle(batteryChartColor) - .interpolationMethod(.cardinal) + .interpolationMethod(.linear) Plot { PointMark( diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 2ce168ac..97a7225a 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -166,8 +166,8 @@ struct MQTTConfig: View { .onChange(of: root, perform: { _ in let totalBytes = root.utf8.count // Only mess with the value if it is too big - if totalBytes > 15 { - let firstNBytes = Data(root.utf8.prefix(15)) + if totalBytes > 30 { + let firstNBytes = Data(root.utf8.prefix(30)) if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { // Set the shortName back to the last place where it was the right size root = maxBytesString diff --git a/protobufs b/protobufs index 5a97acb1..7e3ee8cd 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 5a97acb17543a10e114675a205e3274a83e721af +Subproject commit 7e3ee8cd96740910d0611433cb9a05a7a692568c