From 8156777fea3f5995344044fd7daf6eb1948c1f6b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 8 Apr 2024 11:41:54 -0700 Subject: [PATCH] Timezone --- Meshtastic.xcodeproj/project.pbxproj | 8 +- Meshtastic/Extensions/TimeZone.swift | 72 +++ .../Meshtastic.xcdatamodeld/.xccurrentversion | 2 +- .../contents | 462 ++++++++++++++ Meshtastic/Persistence/UpdateCoreData.swift | 2 + .../Protobufs/meshtastic/config.pb.swift | 10 + .../Protobufs/meshtastic/deviceonly.pb.swift | 594 +++++++++--------- Meshtastic/Protobufs/meshtastic/mesh.pb.swift | 17 + .../Views/Settings/Config/DeviceConfig.swift | 34 +- protobufs | 2 +- 10 files changed, 902 insertions(+), 301 deletions(-) create mode 100644 Meshtastic/Extensions/TimeZone.swift create mode 100644 Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV33.xcdatamodel/contents diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index dadd5c76..ace31a47 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -202,6 +202,7 @@ DDE5B4062B227E3200FCDD05 /* TraceRouteEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */; }; DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */; }; DDF45C342BC1A48E005ED5F2 /* MQTTIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF45C332BC1A48E005ED5F2 /* MQTTIcon.swift */; }; + DDF45C372BC46A5A005ED5F2 /* TimeZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF45C362BC46A5A005ED5F2 /* TimeZone.swift */; }; DDF6B2482A9AEBF500BA6931 /* StoreForwardConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForwardConfig.swift */; }; DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; }; DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */; }; @@ -480,6 +481,8 @@ DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = ""; }; DDF45C332BC1A48E005ED5F2 /* MQTTIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MQTTIcon.swift; sourceTree = ""; }; DDF45C352BC465B2005ED5F2 /* se */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = se; path = se.lproj/Localizable.strings; sourceTree = ""; }; + DDF45C362BC46A5A005ED5F2 /* TimeZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZone.swift; sourceTree = ""; }; + DDF45C382BC46B16005ED5F2 /* MeshtasticDataModelV33.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV33.xcdatamodel; sourceTree = ""; }; DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV17.xcdatamodel; sourceTree = ""; }; DDF6B2472A9AEBF500BA6931 /* StoreForwardConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreForwardConfig.swift; sourceTree = ""; }; DDF6B24B2A9C2FC800BA6931 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; @@ -994,6 +997,7 @@ DDB75A102A059258006ED576 /* Url.swift */, DD1933772B084F4200771CD5 /* Measurement.swift */, DDFFA7462B3A7F3C004730DB /* Bundle.swift */, + DDF45C362BC46A5A005ED5F2 /* TimeZone.swift */, ); path = Extensions; sourceTree = ""; @@ -1297,6 +1301,7 @@ DD3619152B1EF9F900C41C8C /* LocationsHandler.swift in Sources */, DDDB444A29F8AA3A00EE2349 /* CLLocationCoordinate2D.swift in Sources */, DD41582628582E9B009B0E59 /* DeviceConfig.swift in Sources */, + DDF45C372BC46A5A005ED5F2 /* TimeZone.swift in Sources */, DD007BAE2AA4E91200F5FA12 /* MyInfoEntityExtension.swift in Sources */, DD33DB622B3D27C7003E1EA0 /* FirmwareApi.swift in Sources */, DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */, @@ -1918,6 +1923,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DDF45C382BC46B16005ED5F2 /* MeshtasticDataModelV33.xcdatamodel */, DD9681A22BBB22BE00FD2C47 /* MeshtasticDataModelV32.xcdatamodel */, DDDCD5712BB3246500BE6B60 /* MeshtasticDataModelV 31.xcdatamodel */, DD9A1A912BA2D2D3001E602E /* MeshtasticDataModelV 30.xcdatamodel */, @@ -1951,7 +1957,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD9681A22BBB22BE00FD2C47 /* MeshtasticDataModelV32.xcdatamodel */; + currentVersion = DDF45C382BC46B16005ED5F2 /* MeshtasticDataModelV33.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Extensions/TimeZone.swift b/Meshtastic/Extensions/TimeZone.swift new file mode 100644 index 00000000..593c3d09 --- /dev/null +++ b/Meshtastic/Extensions/TimeZone.swift @@ -0,0 +1,72 @@ +// +// TimeZone.swift +// Meshtastic +// +// Copyright(C) Garth Vander Houwen 4/8/24. +// +import Foundation + +extension TimeZone { + var posixDescription: String { + if let nextDate = nextDaylightSavingTimeTransition, let afterDate = nextDaylightSavingTimeTransition(after: nextDate) { + // This timezone observes DST + + // Get the transition dates to/from standard/DST + let stdDate: Date + let dstDate: Date + if isDaylightSavingTime(for: nextDate) { + stdDate = afterDate + dstDate = nextDate + } else { + stdDate = nextDate + dstDate = afterDate + } + + // Append the standard abbreviation + var res = posixAbbreviation(for: stdDate) + // Append the standard offset + res += posixOffset(for: stdDate) + // Append the DST abbreviation + res += posixAbbreviation(for: dstDate) + + // Append the DST offset if it's not 1 hour different + let diff = secondsFromGMT(for: stdDate) - secondsFromGMT(for: dstDate) + if abs(diff) != 3600 { + res += posixOffset(for: dstDate) + } + + // Get month, weekday ordinal, weekday, hour, minutes, and second + // weekday gets returned as 1-based but we need 0-based + // The hour is based on the post-transition time but we need the pre-transition time + var cal = Calendar(identifier: .gregorian) + cal.timeZone = self + let stdcomps = cal.dateComponents([.month, .weekdayOrdinal, .weekday, .hour, .minute, .second], from: stdDate) + let dstcomps = cal.dateComponents([.month, .weekdayOrdinal, .weekday, .hour, .minute, .second], from: dstDate) + + res += String(format: ",M%d.%d.%d/%d:%02d:%02d", dstcomps.month!, dstcomps.weekdayOrdinal!, dstcomps.weekday! - 1, dstcomps.hour! - 1, dstcomps.minute!, dstcomps.second!) + res += String(format: ",M%d.%d.%d/%d:%02d:%02d", stdcomps.month!, stdcomps.weekdayOrdinal!, stdcomps.weekday! - 1, stdcomps.hour! + 1, stdcomps.minute!, stdcomps.second!) + + return res + } else { + // This timezone does not observe DST + return "\(posixAbbreviation())\(posixOffset())" + } + } + + private func posixAbbreviation(for date: Date = Date()) -> String { + let abrev = abbreviation(for: date) ?? "" // We never actually get "" for any TimeZone identifier + // Many abbreviations come in the form "GMT+X" or "GMT-X" + return abrev.hasPrefix("GMT") ? "GMT" : abrev + } + + private func posixOffset(for date: Date = Date()) -> String { + // The POSIX offset is the opposite of the GMT offset + let secs = 0 - secondsFromGMT(for: date) + let h = secs / 3600 + let m = abs(secs) % 3600 / 60 + let s = abs(secs) % 60 + + // Show the hour, only show the minutes and seconds if non-zero + return "\(h)\(m == 0 && s == 0 ? "" : ":\(String(format: "%02d", m))")\(s == 0 ? "" : ":\(String(format: "%02d", s))")" + } +} diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 75e1f5e3..7da03638 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV32.xcdatamodel + MeshtasticDataModelV33.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV33.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV33.xcdatamodel/contents new file mode 100644 index 00000000..892de6ae --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV33.xcdatamodel/contents @@ -0,0 +1,462 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 8841e7c2..1ac1b6e2 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -437,6 +437,7 @@ func upsertDeviceConfigPacket(config: Meshtastic.Config.DeviceConfig, nodeNum: I newDeviceConfig.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber) newDeviceConfig.doubleTapAsButtonPress = config.doubleTapAsButtonPress newDeviceConfig.isManaged = config.isManaged + newDeviceConfig.tzdef = config.tzdef fetchedNode[0].deviceConfig = newDeviceConfig } else { fetchedNode[0].deviceConfig?.role = Int32(config.role.rawValue) @@ -448,6 +449,7 @@ func upsertDeviceConfigPacket(config: Meshtastic.Config.DeviceConfig, nodeNum: I fetchedNode[0].deviceConfig?.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber) fetchedNode[0].deviceConfig?.doubleTapAsButtonPress = config.doubleTapAsButtonPress fetchedNode[0].deviceConfig?.isManaged = config.isManaged + fetchedNode[0].deviceConfig?.tzdef = config.tzdef } do { try context.save() diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index cb99fded..c0e81a83 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -190,6 +190,10 @@ struct Config { /// Disables the triple-press of user button to enable or disable GPS var disableTripleClick: Bool = false + /// + /// POSIX Timezone definition string from https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv. + var tzdef: String = String() + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -1734,6 +1738,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl 8: .standard(proto: "double_tap_as_button_press"), 9: .standard(proto: "is_managed"), 10: .standard(proto: "disable_triple_click"), + 11: .same(proto: "tzdef"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1752,6 +1757,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl case 8: try { try decoder.decodeSingularBoolField(value: &self.doubleTapAsButtonPress) }() case 9: try { try decoder.decodeSingularBoolField(value: &self.isManaged) }() case 10: try { try decoder.decodeSingularBoolField(value: &self.disableTripleClick) }() + case 11: try { try decoder.decodeSingularStringField(value: &self.tzdef) }() default: break } } @@ -1788,6 +1794,9 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if self.disableTripleClick != false { try visitor.visitSingularBoolField(value: self.disableTripleClick, fieldNumber: 10) } + if !self.tzdef.isEmpty { + try visitor.visitSingularStringField(value: self.tzdef, fieldNumber: 11) + } try unknownFields.traverse(visitor: &visitor) } @@ -1802,6 +1811,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if lhs.doubleTapAsButtonPress != rhs.doubleTapAsButtonPress {return false} if lhs.isManaged != rhs.isManaged {return false} if lhs.disableTripleClick != rhs.disableTripleClick {return false} + if lhs.tzdef != rhs.tzdef {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift b/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift index 3080ae9f..433bffd3 100644 --- a/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/deviceonly.pb.swift @@ -21,7 +21,7 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP } /// -/// TODO: REPLACE +/// Font sizes for the device screen enum ScreenFonts: SwiftProtobuf.Enum { typealias RawValue = Int @@ -75,6 +75,140 @@ extension ScreenFonts: CaseIterable { #endif // swift(>=4.2) +/// +/// Position with static location information only for NodeDBLite +struct PositionLite { + // 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 new preferred location encoding, multiply by 1e-7 to get degrees + /// in floating point + var latitudeI: Int32 = 0 + + /// + /// TODO: REPLACE + var longitudeI: Int32 = 0 + + /// + /// In meters above MSL (but see issue #359) + var altitude: Int32 = 0 + + /// + /// This is usually not sent over the mesh (to save space), but it is sent + /// from the phone so that the local device can set its RTC If it is sent over + /// the mesh (because there are devices on the mesh without GPS), it will only + /// be sent by devices which has a hardware GPS clock. + /// seconds since 1970 + var time: UInt32 = 0 + + /// + /// TODO: REPLACE + var locationSource: Position.LocSource = .locUnset + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +struct NodeInfoLite { + // 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 number + var num: UInt32 { + get {return _storage._num} + set {_uniqueStorage()._num = newValue} + } + + /// + /// The user info for this node + var user: User { + get {return _storage._user ?? User()} + set {_uniqueStorage()._user = newValue} + } + /// Returns true if `user` has been explicitly set. + var hasUser: Bool {return _storage._user != nil} + /// Clears the value of `user`. Subsequent reads from it will return its default value. + mutating func clearUser() {_uniqueStorage()._user = nil} + + /// + /// This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true. + /// Position.time now indicates the last time we received a POSITION from that node. + var position: PositionLite { + get {return _storage._position ?? PositionLite()} + set {_uniqueStorage()._position = newValue} + } + /// Returns true if `position` has been explicitly set. + var hasPosition: Bool {return _storage._position != nil} + /// Clears the value of `position`. Subsequent reads from it will return its default value. + mutating func clearPosition() {_uniqueStorage()._position = nil} + + /// + /// Returns the Signal-to-noise ratio (SNR) of the last received message, + /// as measured by the receiver. Return SNR of the last received message in dB + var snr: Float { + get {return _storage._snr} + set {_uniqueStorage()._snr = newValue} + } + + /// + /// Set to indicate the last time we received a packet from this node + var lastHeard: UInt32 { + get {return _storage._lastHeard} + set {_uniqueStorage()._lastHeard = newValue} + } + + /// + /// The latest device metrics for the node. + var deviceMetrics: DeviceMetrics { + get {return _storage._deviceMetrics ?? DeviceMetrics()} + set {_uniqueStorage()._deviceMetrics = newValue} + } + /// Returns true if `deviceMetrics` has been explicitly set. + var hasDeviceMetrics: Bool {return _storage._deviceMetrics != nil} + /// Clears the value of `deviceMetrics`. Subsequent reads from it will return its default value. + mutating func clearDeviceMetrics() {_uniqueStorage()._deviceMetrics = nil} + + /// + /// local channel index we heard that node on. Only populated if its not the default channel. + var channel: UInt32 { + get {return _storage._channel} + set {_uniqueStorage()._channel = newValue} + } + + /// + /// True if we witnessed the node over MQTT instead of LoRA transport + var viaMqtt: Bool { + get {return _storage._viaMqtt} + set {_uniqueStorage()._viaMqtt = newValue} + } + + /// + /// Number of hops away from us this node is (0 if adjacent) + var hopsAway: UInt32 { + get {return _storage._hopsAway} + set {_uniqueStorage()._hopsAway = newValue} + } + + /// + /// True if node is in our favorites list + /// Persists between NodeDB internal clean ups + var isFavorite: Bool { + get {return _storage._isFavorite} + set {_uniqueStorage()._isFavorite = newValue} + } + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} + + fileprivate var _storage = _StorageClass.defaultInstance +} + /// /// This message is never sent over the wire, but it is used for serializing DB /// state to flash in the device code @@ -187,140 +321,6 @@ struct DeviceState { fileprivate var _storage = _StorageClass.defaultInstance } -struct NodeInfoLite { - // 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 number - var num: UInt32 { - get {return _storage._num} - set {_uniqueStorage()._num = newValue} - } - - /// - /// The user info for this node - var user: User { - get {return _storage._user ?? User()} - set {_uniqueStorage()._user = newValue} - } - /// Returns true if `user` has been explicitly set. - var hasUser: Bool {return _storage._user != nil} - /// Clears the value of `user`. Subsequent reads from it will return its default value. - mutating func clearUser() {_uniqueStorage()._user = nil} - - /// - /// This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true. - /// Position.time now indicates the last time we received a POSITION from that node. - var position: PositionLite { - get {return _storage._position ?? PositionLite()} - set {_uniqueStorage()._position = newValue} - } - /// Returns true if `position` has been explicitly set. - var hasPosition: Bool {return _storage._position != nil} - /// Clears the value of `position`. Subsequent reads from it will return its default value. - mutating func clearPosition() {_uniqueStorage()._position = nil} - - /// - /// Returns the Signal-to-noise ratio (SNR) of the last received message, - /// as measured by the receiver. Return SNR of the last received message in dB - var snr: Float { - get {return _storage._snr} - set {_uniqueStorage()._snr = newValue} - } - - /// - /// Set to indicate the last time we received a packet from this node - var lastHeard: UInt32 { - get {return _storage._lastHeard} - set {_uniqueStorage()._lastHeard = newValue} - } - - /// - /// The latest device metrics for the node. - var deviceMetrics: DeviceMetrics { - get {return _storage._deviceMetrics ?? DeviceMetrics()} - set {_uniqueStorage()._deviceMetrics = newValue} - } - /// Returns true if `deviceMetrics` has been explicitly set. - var hasDeviceMetrics: Bool {return _storage._deviceMetrics != nil} - /// Clears the value of `deviceMetrics`. Subsequent reads from it will return its default value. - mutating func clearDeviceMetrics() {_uniqueStorage()._deviceMetrics = nil} - - /// - /// local channel index we heard that node on. Only populated if its not the default channel. - var channel: UInt32 { - get {return _storage._channel} - set {_uniqueStorage()._channel = newValue} - } - - /// - /// True if we witnessed the node over MQTT instead of LoRA transport - var viaMqtt: Bool { - get {return _storage._viaMqtt} - set {_uniqueStorage()._viaMqtt = newValue} - } - - /// - /// Number of hops away from us this node is (0 if adjacent) - var hopsAway: UInt32 { - get {return _storage._hopsAway} - set {_uniqueStorage()._hopsAway = newValue} - } - - /// - /// True if node is in our favorites list - /// Persists between NodeDB internal clean ups - var isFavorite: Bool { - get {return _storage._isFavorite} - set {_uniqueStorage()._isFavorite = newValue} - } - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} - - fileprivate var _storage = _StorageClass.defaultInstance -} - -/// -/// Position with static location information only for NodeDBLite -struct PositionLite { - // 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 new preferred location encoding, multiply by 1e-7 to get degrees - /// in floating point - var latitudeI: Int32 = 0 - - /// - /// TODO: REPLACE - var longitudeI: Int32 = 0 - - /// - /// In meters above MSL (but see issue #359) - var altitude: Int32 = 0 - - /// - /// This is usually not sent over the mesh (to save space), but it is sent - /// from the phone so that the local device can set its RTC If it is sent over - /// the mesh (because there are devices on the mesh without GPS), it will only - /// be sent by devices which has a hardware GPS clock. - /// seconds since 1970 - var time: UInt32 = 0 - - /// - /// TODO: REPLACE - var locationSource: Position.LocSource = .locUnset - - var unknownFields = SwiftProtobuf.UnknownStorage() - - init() {} -} - /// /// The on-disk saved channels struct ChannelFile { @@ -407,9 +407,9 @@ struct OEMStore { #if swift(>=5.5) && canImport(_Concurrency) extension ScreenFonts: @unchecked Sendable {} -extension DeviceState: @unchecked Sendable {} -extension NodeInfoLite: @unchecked Sendable {} extension PositionLite: @unchecked Sendable {} +extension NodeInfoLite: @unchecked Sendable {} +extension DeviceState: @unchecked Sendable {} extension ChannelFile: @unchecked Sendable {} extension OEMStore: @unchecked Sendable {} #endif // swift(>=5.5) && canImport(_Concurrency) @@ -426,141 +426,57 @@ extension ScreenFonts: SwiftProtobuf._ProtoNameProviding { ] } -extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".DeviceState" +extension PositionLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".PositionLite" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 2: .standard(proto: "my_node"), - 3: .same(proto: "owner"), - 5: .standard(proto: "receive_queue"), - 8: .same(proto: "version"), - 7: .standard(proto: "rx_text_message"), - 9: .standard(proto: "no_save"), - 11: .standard(proto: "did_gps_reset"), - 12: .standard(proto: "rx_waypoint"), - 13: .standard(proto: "node_remote_hardware_pins"), - 14: .standard(proto: "node_db_lite"), + 1: .standard(proto: "latitude_i"), + 2: .standard(proto: "longitude_i"), + 3: .same(proto: "altitude"), + 4: .same(proto: "time"), + 5: .standard(proto: "location_source"), ] - fileprivate class _StorageClass { - var _myNode: MyNodeInfo? = nil - var _owner: User? = nil - var _receiveQueue: [MeshPacket] = [] - var _version: UInt32 = 0 - var _rxTextMessage: MeshPacket? = nil - var _noSave: Bool = false - var _didGpsReset: Bool = false - var _rxWaypoint: MeshPacket? = nil - var _nodeRemoteHardwarePins: [NodeRemoteHardwarePin] = [] - var _nodeDbLite: [NodeInfoLite] = [] - - static let defaultInstance = _StorageClass() - - private init() {} - - init(copying source: _StorageClass) { - _myNode = source._myNode - _owner = source._owner - _receiveQueue = source._receiveQueue - _version = source._version - _rxTextMessage = source._rxTextMessage - _noSave = source._noSave - _didGpsReset = source._didGpsReset - _rxWaypoint = source._rxWaypoint - _nodeRemoteHardwarePins = source._nodeRemoteHardwarePins - _nodeDbLite = source._nodeDbLite - } - } - - fileprivate mutating func _uniqueStorage() -> _StorageClass { - if !isKnownUniquelyReferenced(&_storage) { - _storage = _StorageClass(copying: _storage) - } - return _storage - } - mutating func decodeMessage(decoder: inout D) throws { - _ = _uniqueStorage() - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - 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 2: try { try decoder.decodeSingularMessageField(value: &_storage._myNode) }() - case 3: try { try decoder.decodeSingularMessageField(value: &_storage._owner) }() - case 5: try { try decoder.decodeRepeatedMessageField(value: &_storage._receiveQueue) }() - case 7: try { try decoder.decodeSingularMessageField(value: &_storage._rxTextMessage) }() - case 8: try { try decoder.decodeSingularUInt32Field(value: &_storage._version) }() - 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) }() - case 14: try { try decoder.decodeRepeatedMessageField(value: &_storage._nodeDbLite) }() - default: break - } + 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.decodeSingularSFixed32Field(value: &self.latitudeI) }() + case 2: try { try decoder.decodeSingularSFixed32Field(value: &self.longitudeI) }() + case 3: try { try decoder.decodeSingularInt32Field(value: &self.altitude) }() + case 4: try { try decoder.decodeSingularFixed32Field(value: &self.time) }() + case 5: try { try decoder.decodeSingularEnumField(value: &self.locationSource) }() + default: break } } } func traverse(visitor: inout V) throws { - try withExtendedLifetime(_storage) { (_storage: _StorageClass) in - // 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 - try { if let v = _storage._myNode { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() - try { if let v = _storage._owner { - try visitor.visitSingularMessageField(value: v, fieldNumber: 3) - } }() - if !_storage._receiveQueue.isEmpty { - try visitor.visitRepeatedMessageField(value: _storage._receiveQueue, fieldNumber: 5) - } - try { if let v = _storage._rxTextMessage { - try visitor.visitSingularMessageField(value: v, fieldNumber: 7) - } }() - if _storage._version != 0 { - try visitor.visitSingularUInt32Field(value: _storage._version, fieldNumber: 8) - } - if _storage._noSave != false { - try visitor.visitSingularBoolField(value: _storage._noSave, fieldNumber: 9) - } - if _storage._didGpsReset != false { - try visitor.visitSingularBoolField(value: _storage._didGpsReset, fieldNumber: 11) - } - 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) - } - if !_storage._nodeDbLite.isEmpty { - try visitor.visitRepeatedMessageField(value: _storage._nodeDbLite, fieldNumber: 14) - } + if self.latitudeI != 0 { + try visitor.visitSingularSFixed32Field(value: self.latitudeI, fieldNumber: 1) + } + if self.longitudeI != 0 { + try visitor.visitSingularSFixed32Field(value: self.longitudeI, fieldNumber: 2) + } + if self.altitude != 0 { + try visitor.visitSingularInt32Field(value: self.altitude, fieldNumber: 3) + } + if self.time != 0 { + try visitor.visitSingularFixed32Field(value: self.time, fieldNumber: 4) + } + if self.locationSource != .locUnset { + try visitor.visitSingularEnumField(value: self.locationSource, fieldNumber: 5) } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: DeviceState, rhs: DeviceState) -> Bool { - if lhs._storage !== rhs._storage { - let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in - let _storage = _args.0 - let rhs_storage = _args.1 - if _storage._myNode != rhs_storage._myNode {return false} - if _storage._owner != rhs_storage._owner {return false} - if _storage._receiveQueue != rhs_storage._receiveQueue {return false} - if _storage._version != rhs_storage._version {return false} - if _storage._rxTextMessage != rhs_storage._rxTextMessage {return false} - 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} - if _storage._nodeDbLite != rhs_storage._nodeDbLite {return false} - return true - } - if !storagesAreEqual {return false} - } + static func ==(lhs: PositionLite, rhs: PositionLite) -> Bool { + if lhs.latitudeI != rhs.latitudeI {return false} + if lhs.longitudeI != rhs.longitudeI {return false} + if lhs.altitude != rhs.altitude {return false} + if lhs.time != rhs.time {return false} + if lhs.locationSource != rhs.locationSource {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -706,57 +622,141 @@ extension NodeInfoLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat } } -extension PositionLite: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".PositionLite" +extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = _protobuf_package + ".DeviceState" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .standard(proto: "latitude_i"), - 2: .standard(proto: "longitude_i"), - 3: .same(proto: "altitude"), - 4: .same(proto: "time"), - 5: .standard(proto: "location_source"), + 2: .standard(proto: "my_node"), + 3: .same(proto: "owner"), + 5: .standard(proto: "receive_queue"), + 8: .same(proto: "version"), + 7: .standard(proto: "rx_text_message"), + 9: .standard(proto: "no_save"), + 11: .standard(proto: "did_gps_reset"), + 12: .standard(proto: "rx_waypoint"), + 13: .standard(proto: "node_remote_hardware_pins"), + 14: .standard(proto: "node_db_lite"), ] + fileprivate class _StorageClass { + var _myNode: MyNodeInfo? = nil + var _owner: User? = nil + var _receiveQueue: [MeshPacket] = [] + var _version: UInt32 = 0 + var _rxTextMessage: MeshPacket? = nil + var _noSave: Bool = false + var _didGpsReset: Bool = false + var _rxWaypoint: MeshPacket? = nil + var _nodeRemoteHardwarePins: [NodeRemoteHardwarePin] = [] + var _nodeDbLite: [NodeInfoLite] = [] + + static let defaultInstance = _StorageClass() + + private init() {} + + init(copying source: _StorageClass) { + _myNode = source._myNode + _owner = source._owner + _receiveQueue = source._receiveQueue + _version = source._version + _rxTextMessage = source._rxTextMessage + _noSave = source._noSave + _didGpsReset = source._didGpsReset + _rxWaypoint = source._rxWaypoint + _nodeRemoteHardwarePins = source._nodeRemoteHardwarePins + _nodeDbLite = source._nodeDbLite + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + 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.decodeSingularSFixed32Field(value: &self.latitudeI) }() - case 2: try { try decoder.decodeSingularSFixed32Field(value: &self.longitudeI) }() - case 3: try { try decoder.decodeSingularInt32Field(value: &self.altitude) }() - case 4: try { try decoder.decodeSingularFixed32Field(value: &self.time) }() - case 5: try { try decoder.decodeSingularEnumField(value: &self.locationSource) }() - default: break + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + 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 2: try { try decoder.decodeSingularMessageField(value: &_storage._myNode) }() + case 3: try { try decoder.decodeSingularMessageField(value: &_storage._owner) }() + case 5: try { try decoder.decodeRepeatedMessageField(value: &_storage._receiveQueue) }() + case 7: try { try decoder.decodeSingularMessageField(value: &_storage._rxTextMessage) }() + case 8: try { try decoder.decodeSingularUInt32Field(value: &_storage._version) }() + 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) }() + case 14: try { try decoder.decodeRepeatedMessageField(value: &_storage._nodeDbLite) }() + default: break + } } } } func traverse(visitor: inout V) throws { - if self.latitudeI != 0 { - try visitor.visitSingularSFixed32Field(value: self.latitudeI, fieldNumber: 1) - } - if self.longitudeI != 0 { - try visitor.visitSingularSFixed32Field(value: self.longitudeI, fieldNumber: 2) - } - if self.altitude != 0 { - try visitor.visitSingularInt32Field(value: self.altitude, fieldNumber: 3) - } - if self.time != 0 { - try visitor.visitSingularFixed32Field(value: self.time, fieldNumber: 4) - } - if self.locationSource != .locUnset { - try visitor.visitSingularEnumField(value: self.locationSource, fieldNumber: 5) + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + // 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 + try { if let v = _storage._myNode { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() + try { if let v = _storage._owner { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + if !_storage._receiveQueue.isEmpty { + try visitor.visitRepeatedMessageField(value: _storage._receiveQueue, fieldNumber: 5) + } + try { if let v = _storage._rxTextMessage { + try visitor.visitSingularMessageField(value: v, fieldNumber: 7) + } }() + if _storage._version != 0 { + try visitor.visitSingularUInt32Field(value: _storage._version, fieldNumber: 8) + } + if _storage._noSave != false { + try visitor.visitSingularBoolField(value: _storage._noSave, fieldNumber: 9) + } + if _storage._didGpsReset != false { + try visitor.visitSingularBoolField(value: _storage._didGpsReset, fieldNumber: 11) + } + 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) + } + if !_storage._nodeDbLite.isEmpty { + try visitor.visitRepeatedMessageField(value: _storage._nodeDbLite, fieldNumber: 14) + } } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: PositionLite, rhs: PositionLite) -> Bool { - if lhs.latitudeI != rhs.latitudeI {return false} - if lhs.longitudeI != rhs.longitudeI {return false} - if lhs.altitude != rhs.altitude {return false} - if lhs.time != rhs.time {return false} - if lhs.locationSource != rhs.locationSource {return false} + static func ==(lhs: DeviceState, rhs: DeviceState) -> Bool { + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._myNode != rhs_storage._myNode {return false} + if _storage._owner != rhs_storage._owner {return false} + if _storage._receiveQueue != rhs_storage._receiveQueue {return false} + if _storage._version != rhs_storage._version {return false} + if _storage._rxTextMessage != rhs_storage._rxTextMessage {return false} + 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} + if _storage._nodeDbLite != rhs_storage._nodeDbLite {return false} + return true + } + if !storagesAreEqual {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 a881d288..b868d7d7 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -256,6 +256,15 @@ enum HardwareModel: SwiftProtobuf.Enum { /// Older "V1.0" Variant case heltecWirelessTrackerV10 // = 58 + /// + /// unPhone with ESP32-S3, TFT touchscreen, LSM6DS3TR-C accelerometer and gyroscope + case unphone // = 59 + + /// + /// Teledatics TD-LORAC NRF52840 based M.2 LoRA module + /// Compatible with the TD-WRLS development board + case tdLorac // = 60 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. @@ -323,6 +332,8 @@ enum HardwareModel: SwiftProtobuf.Enum { case 56: self = .chatter2 case 57: self = .heltecWirelessPaperV10 case 58: self = .heltecWirelessTrackerV10 + case 59: self = .unphone + case 60: self = .tdLorac case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -384,6 +395,8 @@ enum HardwareModel: SwiftProtobuf.Enum { case .chatter2: return 56 case .heltecWirelessPaperV10: return 57 case .heltecWirelessTrackerV10: return 58 + case .unphone: return 59 + case .tdLorac: return 60 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -450,6 +463,8 @@ extension HardwareModel: CaseIterable { .chatter2, .heltecWirelessPaperV10, .heltecWirelessTrackerV10, + .unphone, + .tdLorac, .privateHw, ] } @@ -2745,6 +2760,8 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 56: .same(proto: "CHATTER_2"), 57: .same(proto: "HELTEC_WIRELESS_PAPER_V1_0"), 58: .same(proto: "HELTEC_WIRELESS_TRACKER_V1_0"), + 59: .same(proto: "UNPHONE"), + 60: .same(proto: "TD_LORAC"), 255: .same(proto: "PRIVATE_HW"), ] } diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 64e5b330..cc4ee5ec 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -26,6 +26,7 @@ struct DeviceConfig: View { @State var nodeInfoBroadcastSecs = 10800 @State var doubleTapAsButtonPress = false @State var isManaged = false + @State var tzdef = "" var body: some View { VStack { @@ -86,6 +87,26 @@ struct DeviceConfig: View { Label("Debug Log", systemImage: "ant.fill") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + VStack(alignment: .leading) { + HStack { + Label("Time Zone", systemImage: "clock.badge.exclamationmark") + TextField("Time Zone", text: $tzdef) + .foregroundColor(.gray) + .onChange(of: tzdef, perform: { _ in + let totalBytes = tzdef.utf8.count + // Only mess with the value if it is too big + if totalBytes > 63 { + tzdef = String(tzdef.dropLast()) + } + }) + .foregroundColor(.gray) + } + .keyboardType(.default) + .disableAutocorrection(true) + Text("Time zone for dates on the device screen and log.") + .foregroundColor(.gray) + .font(.callout) + } } Section(header: Text("GPIO")) { Picker("Button GPIO", selection: $buttonGPIO) { @@ -179,6 +200,7 @@ struct DeviceConfig: View { dc.nodeInfoBroadcastSecs = UInt32(nodeInfoBroadcastSecs) dc.doubleTapAsButtonPress = doubleTapAsButtonPress dc.isManaged = isManaged + dc.tzdef = tzdef if isManaged { serialEnabled = false debugLogEnabled = false @@ -259,6 +281,11 @@ struct DeviceConfig: View { if newIsManaged != node!.deviceConfig!.isManaged { hasChanges = true } } } + .onChange(of: tzdef) { newTzdef in + if node != nil && node?.deviceConfig != nil { + if newTzdef != node!.deviceConfig!.tzdef { hasChanges = true } + } + } } func setDeviceValues() { self.deviceRole = Int(node?.deviceConfig?.role ?? 0) @@ -273,6 +300,11 @@ struct DeviceConfig: View { } self.doubleTapAsButtonPress = node?.deviceConfig?.doubleTapAsButtonPress ?? false self.isManaged = node?.deviceConfig?.isManaged ?? false - self.hasChanges = false + if self.tzdef.isEmpty { + self.tzdef = TimeZone.current.posixDescription + self.hasChanges = true + } else { + self.hasChanges = false + } } } diff --git a/protobufs b/protobufs index e6b4c590..68720ed8 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit e6b4c590e7c489306c9c44e3ad1fcf62a3efd288 +Subproject commit 68720ed8dbcb2c055e3d1ecd4f78d60692f59493