diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 4e131874..2f7851d6 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -127,6 +127,7 @@ DDDE5A1129AFE69700490C6C /* MeshActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDE5A0F29AFE69700490C6C /* MeshActivityAttributes.swift */; }; DDDE5A1329AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; DDDE5A1429AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; + DDDEE5E129DA3E1100A8E078 /* NodeInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDEE5E029DA3E1100A8E078 /* NodeInfoView.swift */; }; DDE0F7C5295F77B700B8AAB3 /* AppSettingsEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */; }; DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; }; DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */; }; @@ -305,6 +306,8 @@ DDDE5A0429AF163E00490C6C /* WidgetsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetsExtension.entitlements; sourceTree = ""; }; DDDE5A0F29AFE69700490C6C /* MeshActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshActivityAttributes.swift; sourceTree = ""; }; DDDE5A1229AFEAB900490C6C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + DDDEE5E029DA3E1100A8E078 /* NodeInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoView.swift; sourceTree = ""; }; + DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV11.xcdatamodel; sourceTree = ""; }; DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsEnums.swift; sourceTree = ""; }; DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = ""; }; DDF924C926FBB953009FE055 /* ConnectedDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = ""; }; @@ -632,6 +635,7 @@ DDC2E18D26CE25CB0042C5E4 /* Helpers */ = { isa = PBXGroup; children = ( + DDDEE5DF29DA3DA000A8E078 /* Node */, DD5E523D298F5A7D00D21B61 /* Weather */, DD47E3D526F17ED900029299 /* CircleText.swift */, DDF924C926FBB953009FE055 /* ConnectedDevice.swift */, @@ -689,6 +693,14 @@ path = Widgets; sourceTree = ""; }; + DDDEE5DF29DA3DA000A8E078 /* Node */ = { + isa = PBXGroup; + children = ( + DDDEE5E029DA3E1100A8E078 /* NodeInfoView.swift */, + ); + path = Node; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -938,6 +950,7 @@ DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */, DD47E3CE26F103C600029299 /* NodeList.swift in Sources */, DD5E520A298EE33B00D21B61 /* channel.pb.swift in Sources */, + DDDEE5E129DA3E1100A8E078 /* NodeInfoView.swift in Sources */, DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */, DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */, DD47E3D626F17ED900029299 /* CircleText.swift in Sources */, @@ -1199,7 +1212,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.4; + MARKETING_VERSION = 2.1.6; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1233,7 +1246,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.4; + MARKETING_VERSION = 2.1.6; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1480,6 +1493,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */, DDC94FC329CED7280082EA6E /* MeshtasticDataModelV10.xcdatamodel */, DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */, DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */, @@ -1491,7 +1505,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DDC94FC329CED7280082EA6E /* MeshtasticDataModelV10.xcdatamodel */; + currentVersion = DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Helpers/Extensions.swift b/Meshtastic/Helpers/Extensions.swift index 9345d7ea..f4b6a0ae 100644 --- a/Meshtastic/Helpers/Extensions.swift +++ b/Meshtastic/Helpers/Extensions.swift @@ -12,7 +12,7 @@ extension Character { extension CLLocationCoordinate2D { /// Returns distance from coordianate in meters. /// - Parameter from: coordinate which will be used as end point. - /// - Returns: Returns distance in meters. + /// - Returns: distance in meters. func distance(from: CLLocationCoordinate2D) -> CLLocationDistance { let from = CLLocation(latitude: from.latitude, longitude: from.longitude) let to = CLLocation(latitude: self.latitude, longitude: self.longitude) @@ -20,6 +20,36 @@ extension CLLocationCoordinate2D { } } +extension Color { + /// Returns a boolean for a SwiftUI Color to determine what color of text to use + /// - Returns: true if the color is light + func isLight() -> Bool { + guard let components = cgColor?.components, components.count > 2 else {return false} + let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000 + return (brightness > 0.5) + } +} + +extension UIColor { + /// Returns a boolean for a UIColor to determine what color of text to use + /// - Returns: true if the color is light + func isLight() -> Bool { + guard let components = cgColor.components, components.count > 2 else {return false} + let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000 + return (brightness > 0.5) + } + /// Returns a UIColor from a UInt32 value + /// - Parameter hex: UInt32 value to convert to a color + /// - Returns: UIColor + convenience init(hex: UInt32) { + let red = CGFloat((hex & 0xFF0000) >> 16) + let green = CGFloat((hex & 0x00FF00) >> 8) + let blue = CGFloat((hex & 0x0000FF)) + //print("\(red) - \(green) - \(blue)") + self.init(red: red/255.0, green: green/255.0, blue: blue/255.0, alpha: 1.0) + } +} + extension Data { var macAddressString: String { let mac: String = reduce("") {$0 + String(format: "%02x:", $1)} @@ -135,3 +165,25 @@ extension String { } } } + +extension UserDefaults { + + enum Keys: String, CaseIterable { + case meshtasticUsername + case preferredPeripheralId + case provideLocation + case provideLocationInterval + case keyboardType + case meshMapType + case meshMapCenteringMode + case meshMapRecentering + case meshMapCustomTileServer + case meshMapUserTrackingMode + case meshMapShowNodeHistory + case meshMapShowRouteLines + } + + func reset() { + Keys.allCases.forEach { removeObject(forKey: $0.rawValue) } + } +} diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 597d88d8..da1b2edd 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -251,7 +251,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje let newNode = NodeInfoEntity(context: context) newNode.id = Int64(nodeInfo.num) newNode.num = Int64(nodeInfo.num) - newNode.channel = Int32(channel) + newNode.channel = Int32(nodeInfo.channel) if nodeInfo.hasDeviceMetrics { let telemetry = TelemetryEntity(context: context) @@ -321,7 +321,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje fetchedNode[0].num = Int64(nodeInfo.num) fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) fetchedNode[0].snr = nodeInfo.snr - fetchedNode[0].channel = Int32(channel) + fetchedNode[0].channel = Int32(nodeInfo.channel) if nodeInfo.hasUser { @@ -766,23 +766,25 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM guard let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as? [MyInfoEntity] else { return } - for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { - if channel.index == newMessage.channel { - context.refresh(channel, mergeChanges: true) - } - - if channel.index == newMessage.channel && !channel.mute { - // Create an iOS Notification for the received private channel message and schedule it immediately - let manager = LocalNotificationManager() - manager.notifications = [ - Notification( - id: ("notification.id.\(newMessage.messageId)"), - title: "\(newMessage.fromUser?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))", - subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")", - content: messageText) - ] - manager.schedule() - print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))") + if !fetchedMyInfo.isEmpty { + for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { + if channel.index == newMessage.channel { + context.refresh(channel, mergeChanges: true) + } + + if channel.index == newMessage.channel && !channel.mute { + // Create an iOS Notification for the received private channel message and schedule it immediately + let manager = LocalNotificationManager() + manager.notifications = [ + Notification( + id: ("notification.id.\(newMessage.messageId)"), + title: "\(newMessage.fromUser?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))", + subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")", + content: messageText) + ] + manager.schedule() + print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))") + } } } } catch { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index ff97f8c7..f9b5a5e7 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV10.xcdatamodel + MeshtasticDataModelV11.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV11.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV11.xcdatamodel/contents new file mode 100644 index 00000000..689184f3 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV11.xcdatamodel/contents @@ -0,0 +1,332 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index d881e082..6bb33b13 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -117,7 +117,11 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newNode.num = Int64(packet.from) newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) newNode.snr = packet.rxSnr - newNode.channel = Int32(packet.channel) + + if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { + newNode.channel = Int32(nodeInfoMessage.channel) + } + if let newUserMessage = try? User(serializedData: packet.decoded.payload) { let newUser = UserEntity(context: context) newUser.userId = newUserMessage.id @@ -134,9 +138,10 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].num = Int64(packet.from) fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) fetchedNode[0].snr = packet.rxSnr - fetchedNode[0].channel = Int32(packet.channel) if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { + + fetchedNode[0].channel = Int32(nodeInfoMessage.channel) if nodeInfoMessage.hasDeviceMetrics { let telemetry = TelemetryEntity(context: context) telemetry.batteryLevel = Int32(nodeInfoMessage.deviceMetrics.batteryLevel) diff --git a/Meshtastic/Protobufs/meshtastic/channel.pb.swift b/Meshtastic/Protobufs/meshtastic/channel.pb.swift index f635dddd..a3a89d0d 100644 --- a/Meshtastic/Protobufs/meshtastic/channel.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/channel.pb.swift @@ -21,11 +21,9 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP } /// -/// Full settings (center freq, spread factor, pre-shared secret key etc...) -/// needed to configure a radio for speaking on a particular channel This -/// information can be encoded as a QRcode/url so that other users can configure +/// This information can be encoded as a QRcode/url so that other users can configure /// their radio to join the same channel. -/// A note about how channel names are shown to users: channelname-Xy +/// A note about how channel names are shown to users: channelname-X /// poundsymbol is a prefix used to indicate this is a channel name (idea from @professr). /// Where X is a letter from A-Z (base 26) representing a hash of the PSK for this /// channel - so that if the user changes anything about the channel (which does @@ -35,8 +33,6 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP /// The PSK is hashed into this letter by "0x41 + [xor all bytes of the psk ] modulo 26" /// This also allows the option of someday if people have the PSK off (zero), the /// users COULD type in a channel name and be able to talk. -/// Y is a lower case letter from a-z that represents the channel 'speed' settings -/// (for some future definition of speed) /// FIXME: Add description of multi-channel support and how primary vs secondary channels are used. /// FIXME: explain how apps use channels for security. /// explain how remote settings and remote gpio are managed as an example @@ -57,7 +53,7 @@ struct ChannelSettings { /// because they are listed in this source code. /// Those bytes are mapped using the following scheme: /// `0` = No crypto - /// `1` = The special "default" channel key: {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0xbf} + /// `1` = The special "default" channel key: {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01} /// `2` through 10 = The default channel key, except with 1 through 9 added to the last byte. /// Shown to user as simple1 through 10 var psk: Data = Data() diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index 4f92bedd..8c502d60 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -177,6 +177,10 @@ struct Config { /// Defaults to 900 Seconds (15 minutes) var nodeInfoBroadcastSecs: UInt32 = 0 + /// + /// Treat double tap interrupt on supported accelerometers as a button press if set to true + var doubleTapAsButtonPress: Bool = false + var unknownFields = SwiftProtobuf.UnknownStorage() /// @@ -1587,6 +1591,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl 5: .standard(proto: "buzzer_gpio"), 6: .standard(proto: "rebroadcast_mode"), 7: .standard(proto: "node_info_broadcast_secs"), + 8: .standard(proto: "double_tap_as_button_press"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1602,6 +1607,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl case 5: try { try decoder.decodeSingularUInt32Field(value: &self.buzzerGpio) }() case 6: try { try decoder.decodeSingularEnumField(value: &self.rebroadcastMode) }() case 7: try { try decoder.decodeSingularUInt32Field(value: &self.nodeInfoBroadcastSecs) }() + case 8: try { try decoder.decodeSingularBoolField(value: &self.doubleTapAsButtonPress) }() default: break } } @@ -1629,6 +1635,9 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if self.nodeInfoBroadcastSecs != 0 { try visitor.visitSingularUInt32Field(value: self.nodeInfoBroadcastSecs, fieldNumber: 7) } + if self.doubleTapAsButtonPress != false { + try visitor.visitSingularBoolField(value: self.doubleTapAsButtonPress, fieldNumber: 8) + } try unknownFields.traverse(visitor: &visitor) } @@ -1640,6 +1649,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl if lhs.buzzerGpio != rhs.buzzerGpio {return false} if lhs.rebroadcastMode != rhs.rebroadcastMode {return false} if lhs.nodeInfoBroadcastSecs != rhs.nodeInfoBroadcastSecs {return false} + if lhs.doubleTapAsButtonPress != rhs.doubleTapAsButtonPress {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 914b5f9f..5c5f151c 100644 --- a/Meshtastic/Protobufs/meshtastic/module_config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/module_config.pb.swift @@ -242,6 +242,10 @@ struct ModuleConfig { /// Whether to send / consume json packets on MQTT var jsonEnabled: Bool = false + /// + /// If true, we attempt to establish a secure connection using TLS + var tlsEnabled: Bool = false + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -1109,6 +1113,7 @@ extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._Message 4: .same(proto: "password"), 5: .standard(proto: "encryption_enabled"), 6: .standard(proto: "json_enabled"), + 7: .standard(proto: "tls_enabled"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1123,6 +1128,7 @@ extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._Message case 4: try { try decoder.decodeSingularStringField(value: &self.password) }() case 5: try { try decoder.decodeSingularBoolField(value: &self.encryptionEnabled) }() case 6: try { try decoder.decodeSingularBoolField(value: &self.jsonEnabled) }() + case 7: try { try decoder.decodeSingularBoolField(value: &self.tlsEnabled) }() default: break } } @@ -1147,6 +1153,9 @@ extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._Message if self.jsonEnabled != false { try visitor.visitSingularBoolField(value: self.jsonEnabled, fieldNumber: 6) } + if self.tlsEnabled != false { + try visitor.visitSingularBoolField(value: self.tlsEnabled, fieldNumber: 7) + } try unknownFields.traverse(visitor: &visitor) } @@ -1157,6 +1166,7 @@ extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._Message if lhs.password != rhs.password {return false} if lhs.encryptionEnabled != rhs.encryptionEnabled {return false} if lhs.jsonEnabled != rhs.jsonEnabled {return false} + if lhs.tlsEnabled != rhs.tlsEnabled {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Views/Helpers/CircleText.swift b/Meshtastic/Views/Helpers/CircleText.swift index b4b51515..30d40406 100644 --- a/Meshtastic/Views/Helpers/CircleText.swift +++ b/Meshtastic/Views/Helpers/CircleText.swift @@ -11,6 +11,7 @@ struct CircleText: View { var circleSize: CGFloat? = 60 var fontSize: CGFloat? = 20 var brightness: Double? = 0 + var textColor: Color? = .white var body: some View { @@ -21,7 +22,7 @@ struct CircleText: View { .fill(color) .brightness(brightness ?? 0) .frame(width: circleSize, height: circleSize) - Text(text).textCase(.uppercase).font(font).foregroundColor(.white).fixedSize() + Text(text).textCase(.uppercase).font(font).foregroundColor(textColor).fixedSize() .frame(width: circleSize, height: circleSize, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/).offset(x: 0, y: 0) } } diff --git a/Meshtastic/Views/Helpers/LastHeardText.swift b/Meshtastic/Views/Helpers/LastHeardText.swift index 1aaaa959..3a207b6d 100644 --- a/Meshtastic/Views/Helpers/LastHeardText.swift +++ b/Meshtastic/Views/Helpers/LastHeardText.swift @@ -10,7 +10,7 @@ struct LastHeardText: View { let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) var body: some View { if lastHeard != nil && lastHeard! >= sixMonthsAgo! { - Text("heard")+Text(": \(lastHeard!, style: .relative) ")+Text("ago") + Text("heard")+Text(" \(lastHeard!, style: .relative) ")+Text("ago") } else { Text("unknown.age") } diff --git a/Meshtastic/Views/Helpers/Node/NodeInfoView.swift b/Meshtastic/Views/Helpers/Node/NodeInfoView.swift new file mode 100644 index 00000000..2f9064ef --- /dev/null +++ b/Meshtastic/Views/Helpers/Node/NodeInfoView.swift @@ -0,0 +1,285 @@ +// +// NodeInfoView.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 4/2/23. +// + +// +// DistanceText.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 8/19/22. +// + +import SwiftUI +import CoreLocation +import MapKit + +struct NodeInfoView: View { + + var node: NodeInfoEntity + + var body: some View { + + let hwModelString = node.user?.hwModel ?? "UNSET" + + Divider() + if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { + HStack { + VStack(alignment: .center) { + CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 75, fontSize: 24, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white ) + } + Divider() + VStack { + if node.user != nil { + Image(hwModelString) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 100, height: 100) + .cornerRadius(5) + + Text(String(hwModelString)) + .foregroundColor(.gray) + .font(.largeTitle).fixedSize() + } + } + + if node.snr > 0 { + Divider() + VStack(alignment: .center) { + + Image(systemName: "waveform.path") + .font(.title) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + .padding(.bottom, 10) + Text("SNR").font(.largeTitle).fixedSize() + Text("\(String(format: "%.2f", node.snr)) dB") + .font(.largeTitle) + .foregroundColor(.gray) + .fixedSize() + } + } + let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) + if deviceMetrics?.count ?? 0 >= 1 { + + let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity + Divider() + VStack(alignment: .center) { + BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0)) + if mostRecent?.voltage ?? 0 > 0.0 { + + Text(String(format: "%.2f", mostRecent?.voltage ?? 0.0) + " V") + .font(.title) + .foregroundColor(.gray) + .fixedSize() + } + } + .padding() + } + } + .padding() + + Divider() + HStack(alignment: .center) { + + VStack { + HStack { + Image(systemName: "person") + .font(.title) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("user").font(.title)+Text(":").font(.title) + } + Text("!\(String(format: "%02x", node.num))") + .font(.title).foregroundColor(.gray) + } + Divider() + VStack { + HStack { + Image(systemName: "number") + .font(.title2) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("Node Number:").font(.title) + } + Text(String(node.num)).font(.title).foregroundColor(.gray) + } + Divider() + VStack { + HStack { + Image(systemName: "globe") + .font(.title) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("MAC Address: ").font(.title) + + } + Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address")) + .font(.title) + .foregroundColor(.gray) + } + Divider() + VStack { + HStack { + Image(systemName: "clock.badge.checkmark.fill") + .font(.title) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("heard.last").font(.title)+Text(":").font(.title) + + } + DateTimeText(dateTime: node.lastHeard) + .font(.title3) + .foregroundColor(.gray) + } + } + Divider() + + } else { + + HStack { + + VStack(alignment: .center) { + CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: 20, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white ) + } + Divider() + VStack { + if node.user != nil { + Image(node.user!.hwModel ?? NSLocalizedString("unset", comment: "Unset")) + .resizable() + .frame(width: 75, height: 75) + .cornerRadius(5) + Text(String(node.user!.hwModel ?? NSLocalizedString("unset", comment: "Unset"))) + .font(.callout).fixedSize() + } + } + + if node.snr > 0 { + Divider() + VStack(alignment: .center) { + + Image(systemName: "waveform.path") + .font(.title) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("SNR").font(.title2).fixedSize() + Text("\(String(format: "%.2f", node.snr)) dB") + .font(.title2) + .foregroundColor(.gray) + .fixedSize() + } + } + + let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) + if deviceMetrics?.count ?? 0 >= 1 { + + let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity + Divider() + VStack(alignment: .center) { + BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0)) + if mostRecent?.voltage ?? 0 > 0 { + + Text(String(format: "%.2f", mostRecent?.voltage ?? 0) + " V") + .font(.callout) + .foregroundColor(.gray) + .fixedSize() + } + } + } + } + Divider() + HStack(alignment: .center) { + VStack { + HStack { + Image(systemName: "person") + .font(.title2) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("User Id:").font(.title2) + } + Text(node.user?.userId ?? "??????").font(.title3).foregroundColor(.gray) + } + Divider() + VStack { + HStack { + Image(systemName: "number") + .font(.title2) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("Node Number:").font(.title2) + } + Text(String(node.num)).font(.title3).foregroundColor(.gray) + } + } + Divider() + HStack { + Image(systemName: "globe") + .font(.headline) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("MAC Address: ") + Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address")).foregroundColor(.gray) + } + .padding([.bottom], 10) + Divider() + } + + VStack { + + if (node.positions?.count ?? 0) > 0 { + + NavigationLink { + PositionLog(node: node) + } label: { + + Image(systemName: "building.columns") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Position Log") + .font(.title3) + } + .fixedSize(horizontal: false, vertical: true) + Divider() + } + + if (node.telemetries?.count ?? 0) > 0 { + + NavigationLink { + DeviceMetricsLog(node: node) + } label: { + + Image(systemName: "flipphone") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Device Metrics Log") + .font(.title3) + } + Divider() + NavigationLink { + EnvironmentMetricsLog(node: node) + } label: { + + Image(systemName: "chart.xyaxis.line") + .symbolRenderingMode(.hierarchical) + .font(.title) + + Text("Environment Metrics Log") + .font(.title3) + } + Divider() + } + } + } +} +struct NodeInfoView_Previews: PreviewProvider { + static var previews: some View { + + VStack { + + } + } +} diff --git a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift index 86f8c0a8..269b913a 100644 --- a/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/Map/Custom/MapViewSwiftUI.swift @@ -16,8 +16,6 @@ struct MapViewSwiftUI: UIViewRepresentable { var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void var onWaypointEdit: (_ waypointId: Int ) -> Void let mapView = MKMapView() - let lineColors: [UIColor] = [UIColor.systemIndigo, UIColor.yellow, UIColor.white, UIColor.red, UIColor.purple, UIColor.orange, UIColor.magenta, UIColor.lightGray, UIColor.green, UIColor.gray, UIColor.systemMint, UIColor.darkGray, UIColor.cyan, UIColor.brown, UIColor.blue, UIColor.black, UIColor.systemPink, - UIColor.systemTeal] // Parameters let positions: [PositionEntity] let waypoints: [WaypointEntity] @@ -142,7 +140,7 @@ struct MapViewSwiftUI: UIViewRepresentable { return position.nodeCoordinate! }) let polyline = MKPolyline(coordinates: lineCoords, count: nodePositions.count) - polyline.title = "\(String(position.nodePosition?.num ?? 0))-\(String(lineIndex))" + polyline.title = "\(String(position.nodePosition?.num ?? 0))" mapView.addOverlay(polyline) lineIndex += 1 // There are 18 colors for lines, start over if we are at index 17 @@ -199,7 +197,7 @@ struct MapViewSwiftUI: UIViewRepresentable { annotationView.displayPriority = .required annotationView.titleVisibility = .visible } else { - annotationView.markerTintColor = UIColor(.indigo) + annotationView.markerTintColor = UIColor(hex: UInt32(positionAnnotation.nodePosition?.num ?? 0)) annotationView.displayPriority = .defaultHigh annotationView.titleVisibility = .adaptive } @@ -351,10 +349,10 @@ struct MapViewSwiftUI: UIViewRepresentable { } else { if let routePolyline = overlay as? MKPolyline { - let titleString = routePolyline.title ?? "None-0" + let titleString = routePolyline.title ?? "0" let index = Int(titleString.components(separatedBy: "-").last ?? "0") let renderer = MKPolylineRenderer(polyline: routePolyline) - renderer.strokeColor = parent.lineColors[index ?? 0] + renderer.strokeColor = UIColor(hex: UInt32(titleString) ?? 0) renderer.lineWidth = 8 return renderer } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 3297f28f..0934b690 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -57,7 +57,7 @@ struct ChannelMessageList: View { HStack(alignment: .top) { if currentUser { Spacer(minLength: 50) } if !currentUser { - CircleText(text: message.fromUser?.shortName ?? "????", color: currentUser ? .accentColor : Color(.gray), circleSize: 44, fontSize: 14) + CircleText(text: message.fromUser?.shortName ?? "????", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 44, fontSize: 14, textColor: UIColor(hex: UInt32(message.fromUser?.num ?? 0)).isLight() ? .black : .white) .padding(.all, 5) .offset(y: -5) } @@ -233,16 +233,29 @@ struct ChannelMessageList: View { #if targetEnvironment(macCatalyst) HStack { Spacer() + + Button { + let bell = "🔔 Alert Bell Character! \u{7}" + print(bell) + typingMessage += bell + + } label: { + Text("Alert Bell") + Image(systemName: "bell.fill") + .symbolRenderingMode(.hierarchical) + .imageScale(.large).foregroundColor(.accentColor) + } + Spacer() Button { let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" sendPositionWithMessage = true if userSettings.meshtasticUsername.count > 0 { - typingMessage = "📍 " + userSettings.meshtasticUsername + " has shared their position with you from node " + userLongName + typingMessage += "📍 " + userSettings.meshtasticUsername + " has shared their position with you from node " + userLongName } else { - typingMessage = "📍 " + userLongName + " has shared their position with you." + typingMessage += "📍 " + userLongName + " has shared their position with you." } } label: { @@ -285,6 +298,18 @@ struct ChannelMessageList: View { } .font(.subheadline) Spacer() + Button { + let bell = "🔔 Alert Bell Character! \u{7}" + print(bell) + typingMessage += bell + + } label: { + Text("Alert") + Image(systemName: "bell.fill") + .symbolRenderingMode(.hierarchical) + .imageScale(.large).foregroundColor(.accentColor) + } + Spacer() Button { let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" sendPositionWithMessage = true diff --git a/Meshtastic/Views/Messages/Contacts.swift b/Meshtastic/Views/Messages/Contacts.swift index 3a193e4f..8292d080 100644 --- a/Meshtastic/Views/Messages/Contacts.swift +++ b/Meshtastic/Views/Messages/Contacts.swift @@ -45,7 +45,7 @@ struct Contacts: View { let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0 VStack(alignment: .leading) { HStack { - CircleText(text: String(channel.index), color: .accentColor, circleSize: 52, fontSize: 40, brightness: 0.1) + CircleText(text: String(channel.index), color: .accentColor, circleSize: 60, fontSize: 42, brightness: 0.1) .padding(.trailing, 5) VStack { HStack { @@ -150,7 +150,7 @@ struct Contacts: View { HStack { VStack { HStack { - CircleText(text: user.shortName ?? "???", color: .accentColor, circleSize: 52, fontSize: 16, brightness: 0.1) + CircleText(text: user.shortName ?? "???", color: Color(UIColor(hex: UInt32(user.num))), circleSize: 60, fontSize: 18, textColor: UIColor(hex: UInt32(user.num)).isLight() ? .black : .white) .padding(.trailing, 5) VStack { HStack { diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index ee6ee822..fa2661be 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -57,11 +57,6 @@ struct UserMessageList: View { } HStack(alignment: .top) { if currentUser { Spacer(minLength: 50) } - if !currentUser { - CircleText(text: message.fromUser?.shortName ?? "????", color: currentUser ? .accentColor : Color(.gray), circleSize: 44, fontSize: 14) - .padding(.all, 5) - .offset(y: -5) - } VStack(alignment: currentUser ? .trailing : .leading) { let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE")) @@ -360,7 +355,7 @@ struct UserMessageList: View { .toolbar { ToolbarItem(placement: .principal) { HStack { - CircleText(text: user.shortName ?? "???", color: .accentColor, circleSize: 44, fontSize: 14).fixedSize() + CircleText(text: user.shortName ?? "???", color: Color(UIColor(hex: UInt32(user.num))), circleSize: 44, fontSize: 14, textColor: UIColor(hex: UInt32(user.num)).isLight() ? .black : .white ).fixedSize() Text(user.longName ?? NSLocalizedString("unknown", comment: "Unknown")).font(.headline) } } diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 3ed3ad81..8451da12 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -8,75 +8,114 @@ import SwiftUI import Charts struct DeviceMetricsLog: View { - + @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager - + @State private var isPresentingClearLogConfirm: Bool = false @State var isExporting = false @State var exportString = "" + + @State private var batteryChartColor: Color = .blue + @State private var airtimeChartColor: Color = .orange + @State private var channelUtilizationChartColor: Color = .green var node: NodeInfoEntity - + var body: some View { + + let oneDayAgo = Calendar.current.date(byAdding: .day, value: -1, to: Date()) + let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).reversed() as? [TelemetryEntity] ?? [] + let chartData = deviceMetrics + .filter { $0.time != nil && $0.time! >= oneDayAgo! } + .sorted { $0.time! < $1.time! } + NavigationStack { - let oneDayAgo = Calendar.current.date(byAdding: .day, value: -3, to: Date()) - let data = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0 && time !=nil && time >= %@", oneDayAgo! as CVarArg)) ?? [] - if data.count > 0 { - GroupBox(label: Label("battery.level.trend", systemImage: "battery.100")) { - Chart(data.array as? [TelemetryEntity] ?? [], id: \.self) { - LineMark( - x: .value("Hour", $0.time!.formattedDate(format: "ha")), - y: .value("Value", $0.batteryLevel) - ) - PointMark( - x: .value("Hour", $0.time!.formattedDate(format: "ha")), - y: .value("Value", $0.batteryLevel) - ) + + if chartData.count > 0 { + GroupBox(label: Label("\(deviceMetrics.count) Readings Total", systemImage: "chart.xyaxis.line")) { + + Chart { + + ForEach(chartData, id: \.self) { point in + + Plot { + LineMark( + x: .value("x", point.time!), + y: .value("y", point.batteryLevel) + ) + } + .accessibilityLabel("Line Series") + .accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)") + .foregroundStyle(batteryChartColor) + .interpolationMethod(.cardinal) + + Plot { + PointMark( + x: .value("x", point.time!), + y: .value("y", point.channelUtilization) + ) + } + .accessibilityLabel("Line Series") + .accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)") + .foregroundStyle(channelUtilizationChartColor) + + RuleMark(y: .value("Limit", 10)) + .lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10])) + .foregroundStyle(airtimeChartColor) + + Plot { + PointMark( + x: .value("x", point.time!), + y: .value("y", point.airUtilTx) + ) + } + .accessibilityLabel("Line Series") + .accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)") + .foregroundStyle(airtimeChartColor) + } } - .frame(height: 150) + .chartXAxis(content: { + AxisMarks(position: .top) + }) + .chartXAxis(.automatic) + .chartForegroundStyleScale([ + "Battery Level" : .blue, + "Channel Utilization": .green, + "Airtime": .orange + ]) + .chartLegend(position: .automatic, alignment: .bottom) } + .frame(minHeight: 250) } let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "") if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { + // Add a table for mac and ipad - Table(node.telemetries?.reversed() as? [TelemetryEntity] ?? []) { - + //Table(Array(deviceMetrics),id: \.self) { + Table(deviceMetrics) { TableColumn("battery.level") { dm in - if dm.metricsType == 0 { - if dm.batteryLevel == 0 { - Text("Powered") - } else { - - Text("\(String(dm.batteryLevel))%") - } + if dm.batteryLevel > 100 { + Text("Powered") + } else { + Text("\(String(dm.batteryLevel))%") } } TableColumn("voltage") { dm in - if dm.metricsType == 0 { - Text("\(String(format: "%.2f", dm.voltage))") - } + Text("\(String(format: "%.2f", dm.voltage))") } TableColumn("channel.utilization") { dm in - if dm.metricsType == 0 { - Text(String(format: "%.2f", dm.channelUtilization)) - } + Text(String(format: "%.2f", dm.channelUtilization)) } TableColumn("airtime") { dm in - if dm.metricsType == 0 { - Text("\(String(format: "%.2f", dm.airUtilTx))%") - } + Text("\(String(format: "%.2f", dm.airUtilTx))%") } TableColumn("timestamp") { dm in - if dm.metricsType == 0 { - Text(dm.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")) - } + Text(dm.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")) } } - } else { ScrollView { - let columns = [ GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1), GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1), @@ -102,26 +141,23 @@ struct DeviceMetricsLog: View { .font(.caption) .fontWeight(.bold) } - ForEach(node.telemetries?.reversed() as? [TelemetryEntity] ?? [], id: \.self) { (dm: TelemetryEntity) in - if dm.metricsType == 0 { - GridRow { - if dm.batteryLevel == 111 { - Text("USB") - .font(.caption) - } else { - Text("\(String(dm.batteryLevel))%") - .font(.caption) - } - Text(String(dm.voltage)) + ForEach(deviceMetrics) { dm in + GridRow { + if dm.batteryLevel > 100 { + Text("PWD") .font(.caption) - Text("\(String(format: "%.2f", dm.channelUtilization))%") + } else { + Text("\(String(dm.batteryLevel))%") .font(.caption) - Text("\(String(format: "%.2f", dm.airUtilTx))%") - .font(.caption) - - Text(dm.time?.formattedDate(format: dateFormatString) ?? "Unknown time") - .font(.caption2) } + Text(String(dm.voltage)) + .font(.caption) + Text("\(String(format: "%.2f", dm.channelUtilization))%") + .font(.caption) + Text("\(String(format: "%.2f", dm.airUtilTx))%") + .font(.caption) + Text(dm.time?.formattedDate(format: dateFormatString) ?? "Unknown time") + .font(.caption2) } } } @@ -182,7 +218,6 @@ struct DeviceMetricsLog: View { if case .success = result { print("Device metrics log download succeeded.") self.isExporting = false - } else { print("Device metrics log download failed: \(result).") } diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index 46a16dd8..e9655414 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -19,47 +19,36 @@ struct EnvironmentMetricsLog: View { var node: NodeInfoEntity var body: some View { + + let environmentMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 1")).reversed() as? [TelemetryEntity] ?? [] NavigationStack { let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "") + Text("\(environmentMetrics.count) Readings") if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { // Add a table for mac and ipad - Table(node.telemetries!.reversed() as! [TelemetryEntity]) { + Table(environmentMetrics) { TableColumn("Temperature") { em in - if em.metricsType == 1 { - Text(em.temperature.formattedTemperature()) - } + Text(em.temperature.formattedTemperature()) } TableColumn("Humidity") { em in - if em.metricsType == 1 { - Text("\(String(format: "%.2f", em.relativeHumidity))%") - } + Text("\(String(format: "%.2f", em.relativeHumidity))%") } TableColumn("Barometric Pressure") { em in - if em.metricsType == 1 { - Text("\(String(format: "%.2f", em.barometricPressure)) hPa") - } + Text("\(String(format: "%.2f", em.barometricPressure)) hPa") } TableColumn("gas.resistance") { em in - if em.metricsType == 1 { - Text("\(String(format: "%.2f", em.gasResistance)) ohms") - } + Text("\(String(format: "%.2f", em.gasResistance)) ohms") } TableColumn("current") { em in - if em.metricsType == 1 { - Text("\(String(format: "%.2f", em.current))") - } + Text("\(String(format: "%.2f", em.current))") } TableColumn("voltage") { em in - if em.metricsType == 1 { - Text("\(String(format: "%.2f", em.voltage))") - } + Text("\(String(format: "%.2f", em.voltage))") } TableColumn("timestamp") { em in - if em.metricsType == 1 { - Text(em.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")) - } + Text(em.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")) } } } else { @@ -92,21 +81,18 @@ struct EnvironmentMetricsLog: View { } ForEach(node.telemetries?.reversed() as? [TelemetryEntity] ?? [], id: \.self) { (em: TelemetryEntity) in - if em.metricsType == 1 { + GridRow { - GridRow { - - Text(em.temperature.formattedTemperature()) - .font(.caption) - Text("\(String(format: "%.2f", em.relativeHumidity))%") - .font(.caption) - Text("\(String(format: "%.2f", em.barometricPressure))") - .font(.caption) - Text("\(String(format: "%.2f", em.gasResistance))") - .font(.caption) - Text(em.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")) - .font(.caption2) - } + Text(em.temperature.formattedTemperature()) + .font(.caption) + Text("\(String(format: "%.2f", em.relativeHumidity))%") + .font(.caption) + Text("\(String(format: "%.2f", em.barometricPressure))") + .font(.caption) + Text("\(String(format: "%.2f", em.gasResistance))") + .font(.caption) + Text(em.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: "")) + .font(.caption2) } } } diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index 24634d6c..8508dfd7 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -140,257 +140,8 @@ struct NodeDetail: View { .padding([.top], 20) } - ScrollView { - Divider() - if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { - HStack { - VStack(alignment: .center) { - CircleText(text: node.user?.shortName ?? "???", color: .accentColor, circleSize: 75, fontSize: 26) - } - Divider() - VStack { - if node.user != nil { - Image(hwModelString) - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 100, height: 100) - .cornerRadius(5) - - Text(String(hwModelString)) - .foregroundColor(.gray) - .font(.largeTitle).fixedSize() - } - } - - if node.snr > 0 { - Divider() - VStack(alignment: .center) { - - Image(systemName: "waveform.path") - .font(.title) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - .padding(.bottom, 10) - Text("SNR").font(.largeTitle).fixedSize() - Text("\(String(format: "%.2f", node.snr)) dB") - .font(.largeTitle) - .foregroundColor(.gray) - .fixedSize() - } - } - let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) - if deviceMetrics?.count ?? 0 >= 1 { - - let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity - Divider() - VStack(alignment: .center) { - BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0)) - if mostRecent?.voltage ?? 0 > 0.0 { - - Text(String(format: "%.2f", mostRecent?.voltage ?? 0.0) + " V") - .font(.title) - .foregroundColor(.gray) - .fixedSize() - } - } - .padding() - } - } - .padding() - - Divider() - HStack(alignment: .center) { - - VStack { - HStack { - Image(systemName: "person") - .font(.title) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("user").font(.title)+Text(":").font(.title) - } - Text("!\(String(format: "%02x", node.num))") - .font(.title).foregroundColor(.gray) - } - Divider() - VStack { - HStack { - Image(systemName: "number") - .font(.title2) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("Node Number:").font(.title) - } - Text(String(node.num)).font(.title).foregroundColor(.gray) - } - Divider() - VStack { - HStack { - Image(systemName: "globe") - .font(.title) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("MAC Address: ").font(.title) - - } - Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address")) - .font(.title) - .foregroundColor(.gray) - } - Divider() - VStack { - HStack { - Image(systemName: "clock.badge.checkmark.fill") - .font(.title) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("heard.last").font(.title)+Text(":").font(.title) - - } - DateTimeText(dateTime: node.lastHeard) - .font(.title3) - .foregroundColor(.gray) - } - } - Divider() - - } else { - - HStack { - - VStack(alignment: .center) { - CircleText(text: node.user?.shortName ?? "???", color: .accentColor) - } - Divider() - VStack { - if node.user != nil { - Image(node.user!.hwModel ?? NSLocalizedString("unset", comment: "Unset")) - .resizable() - .frame(width: 75, height: 75) - .cornerRadius(5) - Text(String(node.user!.hwModel ?? NSLocalizedString("unset", comment: "Unset"))) - .font(.callout).fixedSize() - } - } - - if node.snr > 0 { - Divider() - VStack(alignment: .center) { - - Image(systemName: "waveform.path") - .font(.title) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("SNR").font(.title2).fixedSize() - Text("\(String(format: "%.2f", node.snr)) dB") - .font(.title2) - .foregroundColor(.gray) - .fixedSize() - } - } - - let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) - if deviceMetrics?.count ?? 0 >= 1 { - - let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity - Divider() - VStack(alignment: .center) { - BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0)) - if mostRecent?.voltage ?? 0 > 0 { - - Text(String(format: "%.2f", mostRecent?.voltage ?? 0) + " V") - .font(.callout) - .foregroundColor(.gray) - .fixedSize() - } - } - } - } - Divider() - HStack(alignment: .center) { - VStack { - HStack { - Image(systemName: "person") - .font(.title2) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("User Id:").font(.title2) - } - Text(node.user?.userId ?? "??????").font(.title3).foregroundColor(.gray) - } - Divider() - VStack { - HStack { - Image(systemName: "number") - .font(.title2) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("Node Number:").font(.title2) - } - Text(String(node.num)).font(.title3).foregroundColor(.gray) - } - } - Divider() - HStack { - Image(systemName: "globe") - .font(.headline) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("MAC Address: ") - Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address")).foregroundColor(.gray) - } - .padding([.bottom], 10) - Divider() - } - - VStack { - - if (node.positions?.count ?? 0) > 0 { - - NavigationLink { - PositionLog(node: node) - } label: { - - Image(systemName: "building.columns") - .symbolRenderingMode(.hierarchical) - .font(.title) - - Text("Position Log") - .font(.title3) - } - .fixedSize(horizontal: false, vertical: true) - Divider() - } - - if (node.telemetries?.count ?? 0) > 0 { - - NavigationLink { - DeviceMetricsLog(node: node) - } label: { - - Image(systemName: "flipphone") - .symbolRenderingMode(.hierarchical) - .font(.title) - - Text("Device Metrics Log") - .font(.title3) - } - Divider() - NavigationLink { - EnvironmentMetricsLog(node: node) - } label: { - - Image(systemName: "chart.xyaxis.line") - .symbolRenderingMode(.hierarchical) - .font(.title) - - Text("Environment Metrics Log") - .font(.title3) - } - Divider() - } - } - + ScrollView() { + NodeInfoView(node: node) if self.bleManager.connectedPeripheral != nil && node.metadata != nil { HStack { diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 983b9846..c2c96d68 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -28,7 +28,7 @@ struct NodeList: View { var body: some View { NavigationSplitView { - List(nodes, id: \.self, selection: $selection) { node in + List(nodes, id: \.self, selection: $selection) { node in if nodes.count == 0 { Text("no.nodes").font(.title) } else { @@ -36,7 +36,7 @@ struct NodeList: View { let connected: Bool = (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num) VStack(alignment: .leading) { HStack { - CircleText(text: node.user?.shortName ?? "???", color: .accentColor, circleSize: 52, fontSize: 16, brightness: 0.1) + CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: 20, brightness: 0.0, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white) .padding(.trailing, 5) VStack(alignment: .leading) { Text(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown")).font(.headline) @@ -64,6 +64,15 @@ struct NodeList: View { } } } + if node.channel > 0 { + HStack(alignment: .bottom) { + Image(systemName: "fibrechannel") + .font(.title3) + .symbolRenderingMode(.hierarchical) + Text("Channel: \(node.channel)") + .font(.subheadline) + } + } HStack(alignment: .bottom) { Image(systemName: "clock.badge.checkmark.fill") .font(.title3) diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index ec83fae9..dae61991 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -117,6 +117,8 @@ struct AppSettings: View { Button("Erase all app data?", role: .destructive) { bleManager.disconnectPeripheral() clearCoreDataDatabase(context: context) + UserDefaults.standard.reset() + UserDefaults.standard.synchronize() } } } diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 468e770d..e9e5406c 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -100,7 +100,7 @@ struct DeviceConfig: View { Section(header: Text("GPIO")) { Picker("Button GPIO", selection: $buttonGPIO) { - ForEach(0..<40) { + ForEach(0..<46) { if $0 == 0 { Text("unset") } else { @@ -110,7 +110,7 @@ struct DeviceConfig: View { } .pickerStyle(DefaultPickerStyle()) Picker("Buzzer GPIO", selection: $buzzerGPIO) { - ForEach(0..<40) { + ForEach(0..<46) { if $0 == 0 { Text("unset") } else { @@ -207,6 +207,7 @@ struct DeviceConfig: View { dc.debugLogEnabled = debugLogEnabled dc.buttonGpio = UInt32(buttonGPIO) dc.buzzerGpio = UInt32(buzzerGPIO) + dc.rebroadcastMode = RebroadcastModes(rawValue: rebroadcastMode)?.protoEnumValue() ?? RebroadcastModes.all.protoEnumValue() let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { @@ -277,6 +278,13 @@ struct DeviceConfig: View { if newBuzzerGPIO != node!.deviceConfig!.buttonGpio { hasChanges = true } } } + .onChange(of: rebroadcastMode) { newRebroadcastMode in + + if node != nil && node!.deviceConfig != nil { + + if newRebroadcastMode != node!.deviceConfig!.rebroadcastMode { hasChanges = true } + } + } } func setDeviceValues() { self.deviceRole = Int(node?.deviceConfig?.role ?? 0) @@ -284,6 +292,7 @@ struct DeviceConfig: View { self.debugLogEnabled = node?.deviceConfig?.debugLogEnabled ?? false self.buttonGPIO = Int(node?.deviceConfig?.buttonGpio ?? 0) self.buzzerGPIO = Int(node?.deviceConfig?.buzzerGpio ?? 0) + self.rebroadcastMode = Int(node?.deviceConfig?.rebroadcastMode ?? 0) self.hasChanges = false } } diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index 38debac0..8e2ac4d0 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -125,7 +125,7 @@ struct CannedMessagesConfig: View { .disabled(configPreset > 0) Section(header: Text("Inputs")) { Picker("Pin A", selection: $inputbrokerPinA) { - ForEach(0..<40) { + ForEach(0..<46) { if $0 == 0 { Text("unset") } else { @@ -137,7 +137,7 @@ struct CannedMessagesConfig: View { Text("GPIO pin for rotary encoder A port.") .font(.caption) Picker("Pin B", selection: $inputbrokerPinB) { - ForEach(0..<40) { + ForEach(0..<46) { if $0 == 0 { Text("unset") } else { @@ -149,7 +149,7 @@ struct CannedMessagesConfig: View { Text("GPIO pin for rotary encoder B port.") .font(.caption) Picker("Press Pin", selection: $inputbrokerPinPress) { - ForEach(0..<40) { + ForEach(0..<46) { if $0 == 0 { Text("unset") } else { diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index 2765b46d..b34189d8 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -92,7 +92,7 @@ struct ExternalNotificationConfig: View { Text("If enabled, the 'output' Pin will be pulled active high, disabled means active low.") .font(.caption) Picker("Output pin GPIO", selection: $output) { - ForEach(0..<40) { + ForEach(0..<46) { if $0 == 0 { Text("unset") } else { @@ -140,7 +140,7 @@ struct ExternalNotificationConfig: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Picker("Output pin buzzer GPIO ", selection: $outputBuzzer) { - ForEach(0..<40) { + ForEach(0..<46) { if $0 == 0 { Text("unset") } else { @@ -150,7 +150,7 @@ struct ExternalNotificationConfig: View { } .pickerStyle(DefaultPickerStyle()) Picker("Output pin vibra GPIO", selection: $outputVibra) { - ForEach(0..<40) { + ForEach(0..<46) { if $0 == 0 { Text("unset") } else { diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index 44d03ee9..45665dea 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -67,18 +67,18 @@ struct RangeTestConfig: View { Label("save", systemImage: "square.and.arrow.down.fill") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - .disabled(!(node != nil && node!.myInfo?.hasWifi ?? false)) + .disabled(!(node != nil && node?.metadata?.hasWifi ?? false)) Text("Saves a CSV with the range test message details, currently only available on ESP32 devices with a web server.") .font(.caption) } } - .disabled(self.bleManager.connectedPeripheral == nil || node?.positionConfig == nil || !(node != nil && node!.myInfo?.hasWifi ?? false)) + .disabled(self.bleManager.connectedPeripheral == nil || node?.rangeTestConfig == nil || !(node != nil && node?.metadata?.hasWifi ?? false)) Button { isPresentingSaveConfirm = true } label: { Label("save", systemImage: "square.and.arrow.down") } - .disabled(bleManager.connectedPeripheral == nil || !hasChanges || !(node?.myInfo?.hasWifi ?? false)) + .disabled(bleManager.connectedPeripheral == nil || !hasChanges || !(node?.metadata?.hasWifi ?? false)) .buttonStyle(.bordered) .buttonBorderShape(.capsule) .controlSize(.large) diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index 6f5cc238..6a2a49b7 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -98,7 +98,7 @@ struct SerialConfig: View { Section(header: Text("GPIO")) { Picker("Receive data (rxd) GPIO pin", selection: $rxd) { - ForEach(0..<40) { + ForEach(0..<46) { if $0 == 0 { Text("unset") } else { @@ -109,7 +109,7 @@ struct SerialConfig: View { .pickerStyle(DefaultPickerStyle()) Picker("Transmit data (txd) GPIO pin", selection: $txd) { - ForEach(0..<40) { + ForEach(0..<46) { if $0 == 0 { Text("unset") } else { diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index 21c48653..6386b9d7 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -1,5 +1,5 @@ // -// WiFiConfig.swift +// NetworkConfig.swift // Meshtastic // // Copyright (c) Garth Vander Houwen 8/1/2022 @@ -55,64 +55,68 @@ struct NetworkConfig: View { .font(.callout) .foregroundColor(.orange) } - Section(header: Text("WiFi Options (ESP32 Only)")) { - Toggle(isOn: $wifiEnabled) { - Label("enabled", systemImage: "wifi") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - HStack { - Label("ssid", systemImage: "network") - TextField("ssid", text: $wifiSsid) - .foregroundColor(.gray) - .autocapitalization(.none) - .disableAutocorrection(true) - .onChange(of: wifiSsid, perform: { _ in - let totalBytes = wifiSsid.utf8.count - // Only mess with the value if it is too big - if totalBytes > 32 { - let firstNBytes = Data(wifiSsid.utf8.prefix(32)) - if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { - // Set the shortName back to the last place where it was the right size - wifiSsid = maxBytesString + if (node != nil && node?.metadata?.hasWifi ?? false) { + Section(header: Text("WiFi Options")) { + Toggle(isOn: $wifiEnabled) { + Label("enabled", systemImage: "wifi") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + HStack { + Label("ssid", systemImage: "network") + TextField("ssid", text: $wifiSsid) + .foregroundColor(.gray) + .autocapitalization(.none) + .disableAutocorrection(true) + .onChange(of: wifiSsid, perform: { _ in + let totalBytes = wifiSsid.utf8.count + // Only mess with the value if it is too big + if totalBytes > 32 { + let firstNBytes = Data(wifiSsid.utf8.prefix(32)) + if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { + // Set the shortName back to the last place where it was the right size + wifiSsid = maxBytesString + } } - } - hasChanges = true - }) - .foregroundColor(.gray) - } - .keyboardType(.default) - HStack { - Label("password", systemImage: "wallet.pass") - TextField("password", text: $wifiPsk) - .foregroundColor(.gray) - .autocapitalization(.none) - .disableAutocorrection(true) - .onChange(of: wifiPsk, perform: { _ in - let totalBytes = wifiPsk.utf8.count - // Only mess with the value if it is too big - if totalBytes > 63 { - let firstNBytes = Data(wifiPsk.utf8.prefix(63)) - if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { - // Set the shortName back to the last place where it was the right size - wifiPsk = maxBytesString + hasChanges = true + }) + .foregroundColor(.gray) + } + .keyboardType(.default) + HStack { + Label("password", systemImage: "wallet.pass") + TextField("password", text: $wifiPsk) + .foregroundColor(.gray) + .autocapitalization(.none) + .disableAutocorrection(true) + .onChange(of: wifiPsk, perform: { _ in + let totalBytes = wifiPsk.utf8.count + // Only mess with the value if it is too big + if totalBytes > 63 { + let firstNBytes = Data(wifiPsk.utf8.prefix(63)) + if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { + // Set the shortName back to the last place where it was the right size + wifiPsk = maxBytesString + } } - } - hasChanges = true - }) - .foregroundColor(.gray) + hasChanges = true + }) + .foregroundColor(.gray) + } + .keyboardType(.default) + Text("Enabling WiFi will disable the bluetooth connection to the app.") + .font(.callout) } - .keyboardType(.default) - Text("Enabling WiFi will disable the bluetooth connection to the app.") - .font(.callout) + } - .disabled(!(node != nil && node!.myInfo?.hasWifi ?? false)) - Section(header: Text("Ethernet Options")) { - Toggle(isOn: $ethEnabled) { - Label("enabled", systemImage: "network") + if (node != nil && node?.metadata?.hasEthernet ?? false) { + Section(header: Text("Ethernet Options")) { + Toggle(isOn: $ethEnabled) { + Label("enabled", systemImage: "network") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("Enabling Ethernet will disable the bluetooth connection to the app.") + .font(.callout) } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("Enabling Ethernet will disable the bluetooth connection to the app.") - .font(.callout) } } .scrollDismissesKeyboard(.interactively) diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 55d61401..b00366a8 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -236,7 +236,7 @@ struct PositionConfig: View { .font(.caption) Picker("GPS Receive GPIO", selection: $rxGpio) { - ForEach(0..<40) { + ForEach(0..<46) { if $0 == 0 { Text("unset") } else { @@ -247,7 +247,7 @@ struct PositionConfig: View { .pickerStyle(DefaultPickerStyle()) Picker("GPS Transmit GPIO", selection: $txGpio) { - ForEach(0..<40) { + ForEach(0..<46) { if $0 == 0 { Text("unset") } else { diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index d42c9caa..851303bc 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -18,7 +18,6 @@ "available.radios"="Geräte in der Nähe"; "automatic.detection"="Automatische erkennung"; "battery.level"="Batterie Ladung"; -"battery.level.trend"="Batterie Ladungstrend"; "ble.name"="BLE Name";"ble.connection.timeout %d %@"="Verbindung nach %d Versuchen zu %@ fehlgeschlagen. Evtl. hilft es, die Verbindung unter Einstellungen > Bluetooth manuell zu löschen."; "ble.connection.timeout %d %@"="Verbindung nach %d Versuchen zu %@ fehlgeschlagen. Evtl. hilft es, die Verbindung unter Einstellungen > Bluetooth manuell zu löschen."; "ble.errorcode.6 %@"="%@ Die App wird automatisch zum präferierten Gerät wiederverbinden, sobald es in Reichweite kommt."; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 8183f1cf..53253d8f 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -18,7 +18,6 @@ "available.radios"="Available Radios"; "automatic.detection"="Automatic Detection"; "battery.level"="Battery Level"; -"battery.level.trend"="Battery Level Trend"; "ble.name"="BLE Name"; "ble.connection.timeout %d %@"="Connection failed after %d attempts to connect to %@. You may need to forget your device under Settings > Bluetooth."; "ble.errorcode.6 %@"="%@ The app will automatically reconnect to the preferred radio if it comes back in range."; @@ -51,7 +50,7 @@ "config.save.confirm"="After config values save the node will reboot."; "communicating"="Communicating with device. ."; "connected.radio"="Connected Radio"; -"connected"="Currently Connected"; +"connected"="Connected"; "connecting"="Connecting . ."; "contacts"="Contacts"; "copy"="Copy"; @@ -69,7 +68,7 @@ "device.role.repeater"="Repeater - Mesh packets will prefer to be routed over this node. This role eliminates unnecessary overhead such as NodeInfo, DeviceTelemetry, and any other mesh packet, resulting in the device not appearing as part of the network. Please see Rebroadcast Mode for additional settings specific to this role."; "device.role.tracker"="Tracker - For use with devices intended as a GPS tracker. Position packets sent from this device will be higher priority, with position broadcasting every two minutes. Smart Position Broadcast will default to off."; "direct.messages"="Direct Messages"; -"dismiss.keyboard"="Dismiss Keyboard"; +"dismiss.keyboard"="Dismiss"; "display"="Display (Device Screen)"; "display.config"="Display Config"; "distance"="Distance"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index f0f54149..1fad751b 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -18,7 +18,6 @@ "available.radios"="可用的电台"; "automatic.detection"="自动识别"; "battery.level"="电池电量"; -"battery.level.trend"="电池电量趋势"; "ble.name"="蓝牙名称"; "ble.connection.timeout %d %@"="尝试连接%@失败,你可能需要在系统设置的蓝牙选项中忽略该电台。"; "ble.errorcode.6 %@"="%@ 如果在首选电台的旁边,App 将会自动重连。";