diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 4963df39..4b87f75d 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1423,7 +1423,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.8; + MARKETING_VERSION = 2.2.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1457,7 +1457,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.8; + MARKETING_VERSION = 2.2.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1579,7 +1579,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.8; + MARKETING_VERSION = 2.2.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1612,7 +1612,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.8; + MARKETING_VERSION = 2.2.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Enums/AppSettingsEnums.swift b/Meshtastic/Enums/AppSettingsEnums.swift index dd0a94ed..d2d13979 100644 --- a/Meshtastic/Enums/AppSettingsEnums.swift +++ b/Meshtastic/Enums/AppSettingsEnums.swift @@ -85,6 +85,7 @@ enum UserTrackingModes: Int, CaseIterable, Identifiable { } enum LocationUpdateInterval: Int, CaseIterable, Identifiable { + case fiveSeconds = 5 case tenSeconds = 10 case fifteenSeconds = 15 case thirtySeconds = 30 @@ -96,6 +97,8 @@ enum LocationUpdateInterval: Int, CaseIterable, Identifiable { var id: Int { self.rawValue } var description: String { switch self { + case .fiveSeconds: + return "interval.five.seconds".localized case .tenSeconds: return "interval.ten.seconds".localized case .fifteenSeconds: diff --git a/Meshtastic/Extensions/Date.swift b/Meshtastic/Extensions/Date.swift index 29a60320..224f0b03 100644 --- a/Meshtastic/Extensions/Date.swift +++ b/Meshtastic/Extensions/Date.swift @@ -8,9 +8,6 @@ import Foundation extension Date { - static var currentTimeStamp: Int64 { - return Int64(Date().timeIntervalSince1970 * 1000) - } func formattedDate(format: String) -> String { let dateformat = DateFormatter() diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 48305399..97e99b98 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -123,9 +123,6 @@ extension UserDefaults { UserDefaults.standard.set(newValue, forKey: "enableMapPointsOfInterest") } } - - - static var enableOfflineMaps: Bool { get { UserDefaults.standard.bool(forKey: "enableOfflineMaps") diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 351c899e..c724662e 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -144,6 +144,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate func disconnectPeripheral(reconnect: Bool = true) { guard let connectedPeripheral = connectedPeripheral else { return } + if mqttProxyConnected { + mqttManager.mqttClientProxy?.disconnect() + } automaticallyReconnect = reconnect centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) FROMRADIO_characteristic = nil @@ -272,6 +275,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } if ![FROMNUM_characteristic, TORADIO_characteristic].contains(nil) { + if mqttProxyConnected { + mqttManager.mqttClientProxy?.disconnect() + } sendWantConfig() } } @@ -290,7 +296,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate func onMqttMessageReceived(message: CocoaMQTTMessage) { - print("📲 Mqtt Client Proxy onMqttMessageReceived for topic: \(message.topic)") + if message.topic.contains("/stat/") { return } @@ -305,7 +311,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let binaryData: Data = try! toRadio.serializedData() if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) - print("📲 Sent Mqtt client proxy message to the connected device.") + } } @@ -443,7 +449,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate payload: [UInt8](decodedInfo.mqttClientProxyMessage.data), retained: decodedInfo.mqttClientProxyMessage.retained ) - print("📲 Publish Mqtt client proxy message received on FromRadio to the Mqtt server \(message)") mqttManager.mqttClientProxy?.publish(message) } @@ -635,10 +640,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Use context to pass the radio name with the timer // Use a RunLoop to prevent the timer from running on the main UI thread if UserDefaults.provideLocation { + let interval = UserDefaults.provideLocationInterval > 0 ? UserDefaults.provideLocationInterval : 30 if positionTimer != nil { - positionTimer!.invalidate() } - positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval((UserDefaults.provideLocationInterval )), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true) + positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval((UserDefaults.provideLocationInterval)), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true) if positionTimer != nil { RunLoop.current.add(positionTimer!, forMode: .common) } @@ -787,7 +792,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.from = fromNodeNum meshPacket.wantAck = true var dataMessage = DataMessage() - dataMessage.payload = try! waypoint.serializedData() + do { + dataMessage.payload = try waypoint.serializedData() + } + catch { + // Could not serialiaze the payload + return false + } + dataMessage.portnum = PortNum.waypointApp meshPacket.decoded = dataMessage var toRadio: ToRadio! @@ -870,7 +882,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) success = true let logString = String.localizedStringWithFormat("mesh.log.sharelocation %@".localized, String(fromNodeNum)) - print(positionPacket) MeshLogger.log("📍 \(logString)") } return success diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index b01808fa..1ae63a83 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -258,6 +258,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) newNode.snr = nodeInfo.snr if nodeInfo.hasUser { + let newUser = UserEntity(context: context) newUser.userId = nodeInfo.user.id newUser.num = Int64(nodeInfo.num) @@ -265,9 +266,19 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newUser.shortName = nodeInfo.user.shortName newUser.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() newNode.user = newUser + } else { + let newUser = UserEntity(context: context) + newUser.num = Int64(nodeInfo.num) + let userId = String(format:"%2X", nodeInfo.num) + newUser.userId = "!\(userId)" + let last4 = String(userId.suffix(4)) + newUser.longName = "Meshtastic \(last4)" + newUser.shortName = last4 + newUser.hwModel = "UNSET" + newNode.user = newUser } - if nodeInfo.position.longitudeI > 0 || nodeInfo.position.latitudeI > 0 && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) { + if (nodeInfo.position.longitudeI != 0 && nodeInfo.position.latitudeI != 0) && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) { let position = PositionEntity(context: context) position.latest = true position.seqNo = Int32(nodeInfo.position.seqNumber) @@ -306,7 +317,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje } catch { print("💥 Fetch MyInfo Error") } - } else if nodeInfo.hasUser && nodeInfo.num > 0 { + } else if nodeInfo.num > 0 { fetchedNode[0].id = Int64(nodeInfo.num) fetchedNode[0].num = Int64(nodeInfo.num) @@ -323,6 +334,18 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje fetchedNode[0].user!.longName = nodeInfo.user.longName fetchedNode[0].user!.shortName = nodeInfo.user.shortName fetchedNode[0].user!.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() + } else { + if (fetchedNode[0].user == nil) { + let newUser = UserEntity(context: context) + newUser.num = Int64(nodeInfo.num) + let userId = String(format:"%2X", nodeInfo.num) + newUser.userId = "!\(userId)" + let last4 = String(userId.suffix(4)) + newUser.longName = "Meshtastic \(last4)" + newUser.shortName = last4 + newUser.hwModel = "UNSET" + fetchedNode[0].user = newUser + } } if nodeInfo.hasDeviceMetrics { @@ -340,7 +363,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje if nodeInfo.hasPosition { - if nodeInfo.position.longitudeI > 0 || nodeInfo.position.latitudeI > 0 && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) { + if (nodeInfo.position.longitudeI != 0 && nodeInfo.position.latitudeI != 0) && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) { let position = PositionEntity(context: context) position.latitudeI = nodeInfo.position.latitudeI diff --git a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift index 4df407b5..2d7dc6c1 100644 --- a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift +++ b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift @@ -22,6 +22,7 @@ class MqttClientProxyManager { weak var delegate: MqttClientProxyManagerDelegate? var mqttClientProxy: CocoaMQTT? var topic = "msh/2/c" + var debugLog = false func connectFromConfigSettings(node: NodeInfoEntity) { let defaultServerAddress = "mqtt.meshtastic.org" let useSsl = node.mqttConfig?.tlsEnabled == true @@ -58,9 +59,9 @@ class MqttClientProxyManager { mqttClient.password = password mqttClient.keepAlive = 60 mqttClient.cleanSession = cleanSession -#if DEBUG - mqttClient.logLevel = .debug -#endif + if debugLog { + mqttClient.logLevel = .debug + } mqttClient.willMessage = CocoaMQTTMessage(topic: "/will", string: "dieout") mqttClient.autoReconnect = true mqttClient.delegate = self @@ -82,7 +83,9 @@ class MqttClientProxyManager { } func publish(message: String, topic: String, qos: CocoaMQTTQoS) { mqttClientProxy?.publish(topic, withString: message, qos: qos) - print("📲 MQTT Client Proxy publish for: " + topic) + if debugLog { + print("📲 MQTT Client Proxy publish for: " + topic) + } } func disconnect() { if let client = mqttClientProxy { @@ -130,15 +133,21 @@ extension MqttClientProxyManager: CocoaMQTTDelegate { delegate?.onMqttDisconnected() } func mqtt(_ mqtt: CocoaMQTT, didPublishMessage message: CocoaMQTTMessage, id: UInt16) { - print("📲 MQTT Client Proxy didPublishMessage from MqttClientProxyManager: \(message)") + if debugLog { + print("📲 MQTT Client Proxy didPublishMessage from MqttClientProxyManager: \(message)") + } } func mqtt(_ mqtt: CocoaMQTT, didPublishAck id: UInt16) { - print("📲 MQTT Client Proxy didPublishAck from MqttClientProxyManager: \(id)") + if debugLog { + print("📲 MQTT Client Proxy didPublishAck from MqttClientProxyManager: \(id)") + } } public func mqtt(_ mqtt: CocoaMQTT, didReceiveMessage message: CocoaMQTTMessage, id: UInt16) { delegate?.onMqttMessageReceived(message: message) - print("📲 MQTT Client Proxy message received on topic: \(message.topic)") + if debugLog { + print("📲 MQTT Client Proxy message received on topic: \(message.topic)") + } } func mqtt(_ mqtt: CocoaMQTT, didSubscribeTopics success: NSDictionary, failed: [String]) { print("📲 MQTT Client Proxy didSubscribeTopics: \(success.allKeys.count) topics. failed: \(failed.count) topics") diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 18c9463d..7c79d265 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -178,6 +178,18 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].user!.longName = nodeInfoMessage.user.longName fetchedNode[0].user!.shortName = nodeInfoMessage.user.shortName fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() + } else { + if (fetchedNode[0].user == nil) { + let newUser = UserEntity(context: context) + newUser.num = Int64(nodeInfoMessage.num) + let userId = String(format:"%2X", nodeInfoMessage.num) + newUser.userId = "!\(userId)" + let last4 = String(userId.suffix(4)) + newUser.longName = "Meshtastic \(last4)" + newUser.shortName = last4 + newUser.hwModel = "UNSET" + fetchedNode[0].user! = newUser + } } } do { @@ -225,7 +237,6 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) position.latest = false } } - print("Incoming position message: \n \(positionMessage)") let position = PositionEntity(context: context) position.latest = true position.snr = packet.rxSnr diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index f33e0eb7..d04e9003 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -237,6 +237,13 @@ struct Config { /// When used in conjunction with power.is_power_saving = true, nodes will wake up, /// send environment telemetry, and then sleep for telemetry.environment_update_interval seconds. case sensor // = 6 + + /// + /// TAK device role + /// Used for nodes dedicated for connection to an ATAK EUD. + /// Turns off many of the routine broadcasts to favor CoT packet stream + /// from the Meshtastic ATAK plugin -> IMeshService -> Node + case tak // = 7 case UNRECOGNIZED(Int) init() { @@ -252,6 +259,7 @@ struct Config { case 4: self = .repeater case 5: self = .tracker case 6: self = .sensor + case 7: self = .tak default: self = .UNRECOGNIZED(rawValue) } } @@ -265,6 +273,7 @@ struct Config { case .repeater: return 4 case .tracker: return 5 case .sensor: return 6 + case .tak: return 7 case .UNRECOGNIZED(let i): return i } } @@ -1285,6 +1294,7 @@ extension Config.DeviceConfig.Role: CaseIterable { .repeater, .tracker, .sensor, + .tak, ] } @@ -1692,6 +1702,7 @@ extension Config.DeviceConfig.Role: SwiftProtobuf._ProtoNameProviding { 4: .same(proto: "REPEATER"), 5: .same(proto: "TRACKER"), 6: .same(proto: "SENSOR"), + 7: .same(proto: "TAK"), ] } diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index c4d94c49..cc2388a8 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -208,6 +208,10 @@ enum HardwareModel: SwiftProtobuf.Enum { /// Heltec HT-CT62 with ESP32-C3 CPU and SX1262 LoRa case heltecHt62 // = 53 + /// + /// EBYTE SPI LoRa module and ESP32-S3 + case ebyteEsp32S3 // = 54 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// 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. @@ -265,6 +269,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case 51: self = .tWatchS3 case 52: self = .picomputerS3 case 53: self = .heltecHt62 + case 54: self = .ebyteEsp32S3 case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -316,6 +321,7 @@ enum HardwareModel: SwiftProtobuf.Enum { case .tWatchS3: return 51 case .picomputerS3: return 52 case .heltecHt62: return 53 + case .ebyteEsp32S3: return 54 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -372,6 +378,7 @@ extension HardwareModel: CaseIterable { .tWatchS3, .picomputerS3, .heltecHt62, + .ebyteEsp32S3, .privateHw, ] } @@ -2534,6 +2541,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 51: .same(proto: "T_WATCH_S3"), 52: .same(proto: "PICOMPUTER_S3"), 53: .same(proto: "HELTEC_HT62"), + 54: .same(proto: "EBYTE_ESP32_S3"), 255: .same(proto: "PRIVATE_HW"), ] } diff --git a/Meshtastic/Views/Helpers/CircleText.swift b/Meshtastic/Views/Helpers/CircleText.swift index bd2f0b0d..da51cf8d 100644 --- a/Meshtastic/Views/Helpers/CircleText.swift +++ b/Meshtastic/Views/Helpers/CircleText.swift @@ -21,7 +21,7 @@ struct CircleText: View { .foregroundColor(color.isLight() ? .black : .white) .font(.system(size: 500)) .minimumScaleFactor(0.001) - .frame(width: circleSize * 0.94, height: circleSize * 0.94, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) + .frame(width: circleSize * 0.95, height: circleSize * 0.95, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) } .aspectRatio(1, contentMode: .fit) } diff --git a/Meshtastic/Views/Helpers/LastHeardText.swift b/Meshtastic/Views/Helpers/LastHeardText.swift index 97acce81..02c23f9f 100644 --- a/Meshtastic/Views/Helpers/LastHeardText.swift +++ b/Meshtastic/Views/Helpers/LastHeardText.swift @@ -17,7 +17,7 @@ struct LastHeardText: View { var body: some View { if lastHeard != nil && lastHeard! >= sixMonthsAgo! { - Text("heard")+Text(" \(LastHeardText.formatter.localizedString(for: lastHeard!, relativeTo: Date.now))") + Text(lastHeard?.formatted() ?? "unknown.age".localized) } else { Text("unknown.age") } diff --git a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift index 444083ae..f1fee8bb 100644 --- a/Meshtastic/Views/Helpers/LoRaSignalStrength.swift +++ b/Meshtastic/Views/Helpers/LoRaSignalStrength.swift @@ -31,9 +31,9 @@ struct LoRaSignalStrengthMeter: View { Gauge(value: Double(signalStrength.rawValue), in: 0...3) { } currentValueLabel: { Image(systemName: "dot.radiowaves.left.and.right") - .font(.caption) + .font(.callout) Text("Signal \(signalStrength.description)") - .font(.caption) + .font(.callout) } .gaugeStyle(.accessoryLinear) .tint(gradient) diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 7f4afcdb..a4b9f1e9 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -26,14 +26,14 @@ struct NodeListItem: View { let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) if deviceMetrics?.count ?? 0 >= 1 { let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity - BatteryLevelCompact(batteryLevel: mostRecent?.batteryLevel, font: .caption2, iconFont: .callout, color: .accentColor) + BatteryLevelCompact(batteryLevel: mostRecent?.batteryLevel, font: .caption, iconFont: .callout, color: .accentColor) } } VStack(alignment: .leading) { HStack { Text(node.user?.longName ?? "unknown".localized) .fontWeight(.medium) - .font(.callout) + .font(.headline) if node.user?.vip ?? false { Spacer() Image(systemName: "star.fill") @@ -43,19 +43,19 @@ struct NodeListItem: View { if connected { HStack { Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") - .font(.footnote) + .font(.callout) .symbolRenderingMode(.hierarchical) .foregroundColor(.green) - Text("connected").font(.caption) + Text("connected").font(.callout) } } HStack { Image(systemName: node.isOnline ? "checkmark.circle.fill" : "moon.circle.fill") - .font(.footnote) + .font(.callout) .symbolRenderingMode(.hierarchical) .foregroundColor(node.isOnline ? .green : .orange) LastHeardText(lastHeard: node.lastHeard) - .font(.caption) + .font(.callout) } if node.positions?.count ?? 0 > 0 && connectedNode != node.num { HStack { @@ -65,19 +65,19 @@ struct NodeListItem: View { let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude) let metersAway = nodeCoord.distance(from: myCoord) Image(systemName: "lines.measurement.horizontal") - .font(.footnote) + .font(.callout) .symbolRenderingMode(.hierarchical) - DistanceText(meters: metersAway).font(.caption) + DistanceText(meters: metersAway).font(.callout) } } } if node.channel > 0 { HStack { Image(systemName: "fibrechannel") - .font(.footnote) + .font(.callout) .symbolRenderingMode(.hierarchical) Text("Channel: \(node.channel)") - .font(.caption) + .font(.callout) } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift index 584d3b53..44812c90 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeMapSwiftUI.swift @@ -20,7 +20,6 @@ struct NodeMapSwiftUI: View { @ObservedObject var node: NodeInfoEntity @State var showUserLocation: Bool = false @State var positions: [PositionEntity] = [] - //@State var waypoints: [WaypointEntity] = [] /// Map State User Defaults @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false @AppStorage("meshMapShowRouteLines") private var showRouteLines = false @@ -30,24 +29,20 @@ struct NodeMapSwiftUI: View { @AppStorage("mapLayer") private var selectedMapLayer: MapLayer = .hybrid // Map Configuration @Namespace var mapScope - @State private var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true) - @State private var position = MapCameraPosition.automatic - @State private var scene: MKLookAroundScene? - @State private var isLookingAround = false - @State private var isEditingSettings = false - @State private var selected: PositionEntity? - @State private var selectedWaypoint: WaypointEntity? - @State private var selectedWaypointRect: CGRect = .zero - @State private var selectedWaypointPoint: CGPoint = .zero - @State private var showingPositionPopover = false - @State private var showingWaypointPopover = false + @State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true) + @State var position = MapCameraPosition.automatic + @State var scene: MKLookAroundScene? + @State var isLookingAround = false + @State var isEditingSettings = false + @State var selected: PositionEntity? + @State var selectedWaypoint: WaypointEntity? + @State var showingPositionPopover = false @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], predicate: NSPredicate( format: "expire == nil || expire >= %@", Date() as NSDate ), animation: .none) private var waypoints: FetchedResults - @State var waypoiintSelectionRect: CGRect = .zero var body: some View { @@ -90,15 +85,10 @@ struct NodeMapSwiftUI: View { Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) { ZStack { CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 35) - .onTapGesture(coordinateSpace: .global) { location in + + .onTapGesture(coordinateSpace: .named("nodemap")) { location in print("Tapped at \(location)") let pinLocation = reader.convert(location, from: .local) - print(pinLocation) - let size = CGSize(width: 1, height: 50) - let rect = CGRect(origin: location, size: size) - selectedWaypointRect = rect - selectedWaypointPoint = location - showingWaypointPopover = true selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint) } } @@ -203,12 +193,11 @@ struct NodeMapSwiftUI: View { .padding(.horizontal, 20) } } - .popover(item: $selectedWaypoint, attachmentAnchor: .rect(.rect(selectedWaypointRect)), arrowEdge: .bottom) { selection in - //.popover(isPresented: $showingWaypointPopover, arrowEdge: .bottom) { + .sheet(item: $selectedWaypoint) { selection in WaypointPopover(waypoint: selection) + .presentationDetents([.fraction(0.3), .medium]) .padding() .opacity(0.8) - .presentationCompactAdaptation(.popover) } .sheet(isPresented: $isEditingSettings) { VStack { @@ -293,9 +282,7 @@ struct NodeMapSwiftUI: View { .padding() #endif } - .presentationDetents([.fraction(0.60)]) - //.presentationDetents([.medium, .large]) - .presentationDragIndicator(.automatic) + .presentationDetents([.fraction(0.4), .medium]) } .onChange(of: node) { let mostRecent = node.positions?.lastObject as? PositionEntity @@ -353,10 +340,11 @@ struct NodeMapSwiftUI: View { } #if targetEnvironment(macCatalyst) - MapZoomStepper(scope: mapScope) - .mapControlVisibility(.visible) - MapPitchSlider(scope: mapScope) - .mapControlVisibility(.visible) + /// Hide non fuctional catalyst controls +// MapZoomStepper(scope: mapScope) +// .mapControlVisibility(.visible) +// MapPitchSlider(scope: mapScope) +// .mapControlVisibility(.visible) #endif } .controlSize(.regular) diff --git a/Meshtastic/Views/Nodes/Helpers/WaypointPopover.swift b/Meshtastic/Views/Nodes/Helpers/WaypointPopover.swift index 1c4d8f8d..3eae5eb8 100644 --- a/Meshtastic/Views/Nodes/Helpers/WaypointPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/WaypointPopover.swift @@ -9,12 +9,13 @@ import SwiftUI import MapKit struct WaypointPopover: View { + @Environment(\.dismiss) private var dismiss var waypoint: WaypointEntity let distanceFormatter = MKDistanceFormatter() var body: some View { VStack { HStack { - CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.blue) + CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange) Text(waypoint.name ?? "?") .font(.title3) if waypoint.locked > 0 { @@ -31,7 +32,6 @@ struct WaypointPopover: View { Label { Text(waypoint.longDescription ?? "") .foregroundColor(.primary) - .font(.footnote) .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) } icon: { @@ -40,43 +40,57 @@ struct WaypointPopover: View { .frame(width: 35) } .padding(.bottom, 5) + Divider() } + /// Coordinate + Label { + Text("Coordinates: \(String(format: "%.6f", waypoint.coordinate.latitude)), \(String(format: "%.6f", waypoint.coordinate.longitude))") + //.font(.footnote) + .textSelection(.enabled) + .foregroundColor(.primary) + } icon: { + Image(systemName: "mappin.and.ellipse") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + .padding(.bottom, 5) + Divider() /// Created Label { Text("Created: \(waypoint.created?.formatted() ?? "?")") .foregroundColor(.primary) - .font(.footnote) } icon: { Image(systemName: "clock.badge.checkmark") .symbolRenderingMode(.hierarchical) .frame(width: 35) } .padding(.bottom, 5) + Divider() /// Updated if waypoint.lastUpdated != nil { Label { Text("Updated: \(waypoint.lastUpdated?.formatted() ?? "?")") .foregroundColor(.primary) - .font(.footnote) } icon: { Image(systemName: "clock.arrow.circlepath") .symbolRenderingMode(.hierarchical) .frame(width: 35) } .padding(.bottom, 5) + Divider() } - /// Updated + /// Expires if waypoint.expire != nil { Label { Text("Expires: \(waypoint.expire?.formatted() ?? "?")") .foregroundColor(.primary) - .font(.footnote) } icon: { Image(systemName: "clock.badge.xmark") .symbolRenderingMode(.hierarchical) .frame(width: 35) } .padding(.bottom, 5) + Divider() } /// Distance if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 { @@ -84,14 +98,26 @@ struct WaypointPopover: View { Label { Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") .foregroundColor(.primary) - .font(.footnote) } icon: { Image(systemName: "lines.measurement.horizontal") .symbolRenderingMode(.hierarchical) .frame(width: 35) } + .padding(.bottom, 5) + Divider() } } + #if targetEnvironment(macCatalyst) + Button { + dismiss() + } label: { + Label("close", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + #endif } .tag(waypoint.id) } diff --git a/Meshtastic/Views/Settings/AdminMessageList.swift b/Meshtastic/Views/Settings/AdminMessageList.swift index bb624dc0..190ff683 100644 --- a/Meshtastic/Views/Settings/AdminMessageList.swift +++ b/Meshtastic/Views/Settings/AdminMessageList.swift @@ -22,7 +22,9 @@ struct AdminMessageList: View { var body: some View { let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmmssa", options: 0, locale: Locale.current) + let localeTimeFormat = DateFormatter.dateFormat(fromTemplate: "h:mm:ss a", options: 0, locale: Locale.current) let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss a") + let timeFormatString = (localeTimeFormat ?? "h:mm:ss a") List { if user != nil { @@ -55,7 +57,7 @@ struct AdminMessageList: View { } if am.receivedACK && am.ackTimestamp > 0 { - Text(" \(Date(timeIntervalSince1970: TimeInterval(am.ackTimestamp)).formattedDate(format: "h:mm:ss a"))") + Text(" \(Date(timeIntervalSince1970: TimeInterval(am.ackTimestamp)).formattedDate(format: timeFormatString))") .foregroundColor(am.realACK ? .gray : .orange) .font(.caption2) } diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index 4370f95e..ed79dc5a 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -44,7 +44,7 @@ struct BluetoothConfig: View { setBluetoothValues() } } - } else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 { + } else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? -1 { Text("Configuration for: \(node?.user?.longName ?? "Unknown")") .font(.title3) } else { diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 8a84f474..a4972a90 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -14,7 +14,6 @@ struct Settings: View { private var nodes: FetchedResults @State private var selectedNode: Int = 0 @State private var connectedNodeNum: Int = 0 - @State private var initialLoad: Bool = true @State private var selection: SettingsSidebar = .about enum SettingsSidebar { case appSettings @@ -59,39 +58,44 @@ struct Settings: View { } .tag(SettingsSidebar.appSettings) let node = nodes.first(where: { $0.num == connectedNodeNum }) + let hasAdmin = node?.myInfo?.adminIndex ?? 0 > 0 ? true : false if !(node?.deviceConfig?.isManaged ?? false) { Section("Configure") { - Picker("Configuring Node", selection: $selectedNode) { - if selectedNode == 0 { - Text("Connect to a Node").tag(0) - } - ForEach(nodes) { node in - if node.num == bleManager.connectedPeripheral?.num ?? 0 { - Text("BLE Config: \(node.user?.longName ?? "unknown".localized)") - .tag(Int(node.num)) - } else if node.metadata != nil { - Text("Remote Config: \(node.user?.longName ?? "unknown".localized)") - .tag(Int(node.num)) - } else { - Text("Request Admin: \(node.user?.longName ?? "unknown".localized)") - .tag(Int(node.num)) + if hasAdmin { + Picker("Configuring Node", selection: $selectedNode) { + if selectedNode == 0 { + Text("Connect to a Node").tag(0) } - } - } - .pickerStyle(.automatic) - .labelsHidden() - .onChange(of: selectedNode) { newValue in - if selectedNode > 0 { - let node = nodes.first(where: { $0.num == newValue }) - let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) - connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) - if connectedNode != nil && connectedNode?.user != nil && connectedNode?.myInfo != nil && node?.user != nil && node?.metadata == nil { - let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) - if adminMessageId > 0 { - print("Sent node metadata request from node details") + ForEach(nodes) { node in + if node.num == bleManager.connectedPeripheral?.num ?? 0 { + Text("BLE Config: \(node.user?.longName ?? "unknown".localized)") + .tag(Int(node.num)) + } else if node.metadata != nil { + Text("Remote Config: \(node.user?.longName ?? "unknown".localized)") + .tag(Int(node.num)) + } else if hasAdmin { + Text("Request Admin: \(node.user?.longName ?? "unknown".localized)") + .tag(Int(node.num)) } } } + .pickerStyle(.automatic) + .labelsHidden() + .onChange(of: selectedNode) { newValue in + if selectedNode > 0 { + let node = nodes.first(where: { $0.num == newValue }) + let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) + connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) + if connectedNode != nil && connectedNode?.user != nil && connectedNode?.myInfo != nil && node?.user != nil && node?.metadata == nil { + let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) + if adminMessageId > 0 { + print("Sent node metadata request from node details") + } + } + } + } + } else { + Text("Configuring Node \(node?.user?.longName ?? "unknown".localized)") } } Section("radio.configuration") { @@ -276,12 +280,11 @@ struct Settings: View { } } .onAppear { - self.bleManager.context = context - self.connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) - if initialLoad { - selectedNode = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) - initialLoad = false + if self.bleManager.context == nil { + self.bleManager.context = context } + self.connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) + selectedNode = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) } .listStyle(GroupedListStyle()) .navigationTitle("settings") diff --git a/Meshtastic/Views/Settings/UserConfig.swift b/Meshtastic/Views/Settings/UserConfig.swift index ba7b5d74..47a108e8 100644 --- a/Meshtastic/Views/Settings/UserConfig.swift +++ b/Meshtastic/Views/Settings/UserConfig.swift @@ -23,7 +23,7 @@ struct UserConfig: View { @State private var isPresentingSaveConfirm: Bool = false @State var hasChanges = false @State var shortName = "" - @State var longName = "" + @State var longName: String = "" @State var isLicensed = false @State var overrideDutyCycle = false @State var overrideFrequency: Float = 0.0 @@ -157,10 +157,11 @@ struct UserConfig: View { } } else { var ham = HamParameters() - // ham.shortName = shortName + ham.shortName = shortName ham.callSign = longName ham.txPower = Int32(txPower) ham.frequency = overrideFrequency + print(ham) let adminMessageId = bleManager.saveLicensedUser(ham: ham, fromUser: connectedUser, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { hasChanges = false @@ -201,7 +202,14 @@ struct UserConfig: View { } .onChange(of: isLicensed) { newIsLicensed in if node != nil && node!.user != nil { - if newIsLicensed != node?.user!.isLicensed { hasChanges = true } + if newIsLicensed != node?.user!.isLicensed { + hasChanges = true + if newIsLicensed { + if node?.user?.longName?.count ?? 0 > 8 { + longName = "" + } + } + } } } .onChange(of: overrideFrequency) { _ in