diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 2ad60579..4b935710 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -240,6 +240,7 @@ DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; DD2553562855B02500E55709 /* LoRaConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaConfig.swift; sourceTree = ""; }; DD2553582855B52700E55709 /* PositionConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionConfig.swift; sourceTree = ""; }; + DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV22.xcdatamodel; sourceTree = ""; }; DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewSwiftUI.swift; sourceTree = ""; }; DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV19.xcdatamodel; sourceTree = ""; }; DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HardwareModels.swift; sourceTree = ""; }; @@ -1481,7 +1482,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.14; + MARKETING_VERSION = 2.2.15; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1515,7 +1516,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.14; + MARKETING_VERSION = 2.2.15; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1637,7 +1638,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.14; + MARKETING_VERSION = 2.2.15; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1670,7 +1671,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.14; + MARKETING_VERSION = 2.2.15; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1781,6 +1782,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */, DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */, DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */, DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */, @@ -1803,7 +1805,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */; + currentVersion = DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Enums/DeviceEnums.swift b/Meshtastic/Enums/DeviceEnums.swift index 25938a50..7e97aea7 100644 --- a/Meshtastic/Enums/DeviceEnums.swift +++ b/Meshtastic/Enums/DeviceEnums.swift @@ -70,6 +70,27 @@ enum DeviceRoles: Int, CaseIterable, Identifiable { return "device.role.lostandfound".localized } } + + var systemName: String { + switch self { + case .client: + return "iphone.gen3.radiowaves.left.and.right" + case .clientMute: + return "speaker.slash" + case .router, .routerClient, .repeater: + return "wifi.router" + case .tracker: + return "mappin.and.ellipse.circle" + case .sensor: + return "sensor" + case .tak: + return "shield.checkered" + case .clientHidden: + return "eye.slash" + case .lostAndFound: + return "map" + } + } func protoEnumValue() -> Config.DeviceConfig.Role { switch self { diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index 4ea8d2be..00b5b6e3 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -10,7 +10,6 @@ import Foundation extension UserDefaults { enum Keys: String, CaseIterable { case enableRangeTest - case meshtasticUsername case preferredPeripheralId case preferredPeripheralNum case provideLocation @@ -40,14 +39,6 @@ extension UserDefaults { UserDefaults.standard.set(newValue, forKey: "blockRangeTest") } } - static var meshtasticUsername: String { - get { - UserDefaults.standard.string(forKey: "meshtasticUsername") ?? "" - } - set { - UserDefaults.standard.set(newValue, forKey: "meshtasticUsername") - } - } static var preferredPeripheralId: String { get { UserDefaults.standard.string(forKey: "preferredPeripheralId") ?? "" diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 8f38858b..57a78ced 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -416,6 +416,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate traceRoute.altitude = mostRecent.altitude traceRoute.latitudeI = mostRecent.latitudeI traceRoute.longitudeI = mostRecent.longitudeI + traceRoute.hasPositions = true } } do { @@ -456,6 +457,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let binaryData: Data = try! toRadio.serializedData() connectedPeripheral!.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) // Either Read the config complete value or from num notify value + guard connectedPeripheral != nil else { return } connectedPeripheral!.peripheral.readValue(for: FROMRADIO_characteristic) } } @@ -646,23 +648,30 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } else { var routeString = "You --> " var hopNodes: [TraceRouteHopEntity] = [] -// for node in routingMessage.route { -// let hopNode = getNodeInfo(id: Int64(node), context: context!) -// let traceRouteHop = TraceRouteHopEntity(context: context!) -// traceRouteHop.time = Date() -// let mostRecent = hopNode?.positions?.lastObject as! PositionEntity -// if mostRecent.time! >= Calendar.current.date(byAdding: .minute, value: -60, to: Date())! { -// traceRouteHop.altitude = mostRecent.altitude -// traceRouteHop.latitudeI = mostRecent.latitudeI -// traceRouteHop.longitudeI = mostRecent.longitudeI -// traceRouteHop.name = hopNode?.user?.longName ?? "unknown".localized -// } -// traceRouteHop.num = hopNode?.num ?? 0 -// if hopNode != nil { -// hopNodes.append(traceRouteHop) -// } -// routeString += "\(hopNode?.user?.longName ?? "unknown".localized) --> " -// } + for node in routingMessage.route { + let hopNode = getNodeInfo(id: Int64(node), context: context!) + let traceRouteHop = TraceRouteHopEntity(context: context!) + traceRouteHop.time = Date() + if hopNode?.hasPositions ?? false { + let mostRecent = hopNode?.positions?.lastObject as! PositionEntity + if mostRecent.time! >= Calendar.current.date(byAdding: .minute, value: -60, to: Date())! { + traceRouteHop.altitude = mostRecent.altitude + traceRouteHop.latitudeI = mostRecent.latitudeI + traceRouteHop.longitudeI = mostRecent.longitudeI + traceRouteHop.name = hopNode?.user?.longName ?? "unknown".localized + } else { + traceRoute?.hasPositions = false + } + } else { + traceRoute?.hasPositions = false + } + traceRouteHop.num = hopNode?.num ?? 0 + if hopNode != nil { + hopNodes.append(traceRouteHop) + } + routeString += "\(hopNode?.user?.longName ?? "unknown".localized) -->" + } + routeString += traceRoute?.node?.user?.longName ?? "unknown".localized traceRoute?.routeText = routeString traceRoute?.hops = NSOrderedSet(array: hopNodes) do { @@ -673,8 +682,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let nsError = error as NSError print("💥 Error Updating Core Data TraceRouteHOp: \(nsError)") } - - routeString += "\(decodedInfo.packet.from)" let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.route %@".localized, routeString) MeshLogger.log("🪧 \(logString)") } @@ -946,9 +953,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var success = false let fromNodeNum = connectedPeripheral.num var positionPacket = Position() - + if #available(iOS 17.0, macOS 14.0, *) { - if fromNodeNum <= 0 { + + /// Throw out crappy locations and only send a position if we are connected to a device + if fromNodeNum <= 0 || LocationsHandler.shared.lastLocation.horizontalAccuracy < 0 || LocationsHandler.shared.lastLocation.horizontalAccuracy > 100 { return false } positionPacket.latitudeI = Int32(LocationsHandler.shared.lastLocation.coordinate.latitude * 1e7) @@ -1004,7 +1013,7 @@ 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)) - MeshLogger.log("📍 \(logString)") + print("📍 \(logString)") } return success } @@ -1282,6 +1291,38 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return 0 } + public func removeNode(node: NodeInfoEntity, connectedNodeNum: Int64) -> Bool { + var adminPacket = AdminMessage() + adminPacket.removeByNodenum = UInt32(node.num) + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(connectedNodeNum) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setHamMode = ham diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 9c5626c6..cd9707af 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -16,6 +16,8 @@ import CoreLocation static let shared = LocationsHandler() // Create a single, shared instance of the object. private let manager: CLLocationManager private var background: CLBackgroundActivitySession? + var locationsArray: [CLLocation] + var enableSmartPosition: Bool @Published var lastLocation = CLLocation() @Published var isStationary = false @@ -36,6 +38,9 @@ import CoreLocation private init() { self.manager = CLLocationManager() // Creating a location manager instance is safe to call here in `MainActor`. + locationsArray = [CLLocation]() + enableSmartPosition = true + self.manager.distanceFilter = 5 } func startLocationUpdates() { @@ -53,7 +58,16 @@ import CoreLocation self.lastLocation = loc self.isStationary = update.isStationary self.count += 1 - //print("Location \(self.count): \(self.lastLocation)") + var locationAdded: Bool + if enableSmartPosition { + locationAdded = addLocation(loc) + } else { + locationsArray.append(loc) + locationAdded = true + } + if !locationAdded { + print("Bad Location \(self.count): \(loc)") + } } } } catch { @@ -68,6 +82,21 @@ import CoreLocation self.updatesStarted = false } + func addLocation(_ location: CLLocation) -> Bool { + let age = -location.timestamp.timeIntervalSinceNow + if age > 10 { + return false + } + if location.horizontalAccuracy < 0 { + return false + } + if location.horizontalAccuracy > 100 { + return false + } + locationsArray.append(location) + return true + } + static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090) static var satsInView: Int { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 12116f95..9d09b84f 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV21.xcdatamodel + MeshtasticDataModelV22.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV22.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV22.xcdatamodel/contents new file mode 100644 index 00000000..3b7675c0 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV22.xcdatamodel/contents @@ -0,0 +1,408 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 852b2504..5dee2b41 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -60,7 +60,8 @@ struct MeshtasticAppleApp: App { print("User wants to import a MBTILES offline map file: \(self.incomingUrl?.absoluteString ?? "No Tiles link")") } - if UserDefaults.mapUseLegacy { + /// Only do the map tiles stuff if it is enabled + if UserDefaults.enableOfflineMapsMBTiles { /// we are expecting a .mbtiles map file that contains raster data /// save it to the documents directory, and name it offline_map.mbtiles let fileManager = FileManager.default diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 198226bc..2e6ed8a2 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -129,6 +129,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newUser.num = Int64(packet.from) newUser.longName = newUserMessage.longName newUser.shortName = newUserMessage.shortName + newUser.role = Int32(newUserMessage.role.rawValue) newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased() newNode.user = newUser } @@ -177,6 +178,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].user!.num = Int64(nodeInfoMessage.num) fetchedNode[0].user!.longName = nodeInfoMessage.user.longName fetchedNode[0].user!.shortName = nodeInfoMessage.user.shortName + fetchedNode[0].user!.role = Int32(nodeInfoMessage.user.role.rawValue) fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() } else { if (fetchedNode[0].user == nil) { @@ -586,9 +588,8 @@ func upsertPositionConfigPacket(config: Meshtastic.Config.PositionConfig, nodeNu newPositionConfig.deviceGpsEnabled = config.gpsEnabled newPositionConfig.rxGpio = Int32(config.rxGpio) newPositionConfig.txGpio = Int32(config.txGpio) + newPositionConfig.gpsEnGpio = Int32(config.gpsEnGpio) newPositionConfig.fixedPosition = config.fixedPosition - newPositionConfig.gpsUpdateInterval = Int32(config.gpsUpdateInterval) - newPositionConfig.gpsAttemptTime = Int32(config.gpsAttemptTime) newPositionConfig.positionBroadcastSeconds = Int32(truncatingIfNeeded: config.positionBroadcastSecs) newPositionConfig.broadcastSmartMinimumIntervalSecs = Int32(config.broadcastSmartMinimumIntervalSecs) newPositionConfig.broadcastSmartMinimumDistance = Int32(config.broadcastSmartMinimumDistance) @@ -599,9 +600,8 @@ func upsertPositionConfigPacket(config: Meshtastic.Config.PositionConfig, nodeNu fetchedNode[0].positionConfig?.deviceGpsEnabled = config.gpsEnabled fetchedNode[0].positionConfig?.rxGpio = Int32(config.rxGpio) fetchedNode[0].positionConfig?.txGpio = Int32(config.txGpio) + fetchedNode[0].positionConfig?.gpsEnGpio = Int32(config.gpsEnGpio) fetchedNode[0].positionConfig?.fixedPosition = config.fixedPosition - fetchedNode[0].positionConfig?.gpsUpdateInterval = Int32(config.gpsUpdateInterval) - fetchedNode[0].positionConfig?.gpsAttemptTime = Int32(config.gpsAttemptTime) fetchedNode[0].positionConfig?.positionBroadcastSeconds = Int32(config.positionBroadcastSecs) fetchedNode[0].positionConfig?.broadcastSmartMinimumIntervalSecs = Int32(config.broadcastSmartMinimumIntervalSecs) fetchedNode[0].positionConfig?.broadcastSmartMinimumDistance = Int32(config.broadcastSmartMinimumDistance) @@ -837,6 +837,7 @@ func upsertExternalNotificationModuleConfigPacket(config: Meshtastic.ModuleConfi newExternalNotificationConfig.outputVibra = Int32(config.outputVibra) newExternalNotificationConfig.outputMilliseconds = Int32(config.outputMs) newExternalNotificationConfig.nagTimeout = Int32(config.nagTimeout) + newExternalNotificationConfig.useI2SAsBuzzer = config.useI2SAsBuzzer fetchedNode[0].externalNotificationConfig = newExternalNotificationConfig } else { @@ -854,6 +855,7 @@ func upsertExternalNotificationModuleConfigPacket(config: Meshtastic.ModuleConfi fetchedNode[0].externalNotificationConfig?.outputVibra = Int32(config.outputVibra) fetchedNode[0].externalNotificationConfig?.outputMilliseconds = Int32(config.outputMs) fetchedNode[0].externalNotificationConfig?.nagTimeout = Int32(config.nagTimeout) + fetchedNode[0].externalNotificationConfig?.useI2SAsBuzzer = config.useI2SAsBuzzer } do { diff --git a/Meshtastic/Views/Messages/ChannelList.swift b/Meshtastic/Views/Messages/ChannelList.swift index a956f8ff..e6c8f99d 100644 --- a/Meshtastic/Views/Messages/ChannelList.swift +++ b/Meshtastic/Views/Messages/ChannelList.swift @@ -26,119 +26,116 @@ struct ChannelList: View { let dateFormatString = (localeDateFormat ?? "MM/dd/YY") VStack { - List { - // Display Contacts for the rest of the non admin channels - if node != nil && node!.myInfo != nil && node!.myInfo!.channels != nil { - ForEach(node!.myInfo!.channels!.array as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in - if channel.name?.lowercased() ?? "" != "admin" && channel.name?.lowercased() ?? "" != "gpio" && channel.name?.lowercased() ?? "" != "serial" { + // Display Contacts for the rest of the non admin channels + if node != nil && node!.myInfo != nil && node!.myInfo!.channels != nil { + List(node!.myInfo!.channels!.array as! [ChannelEntity], id: \.self, selection: $channelSelection) { (channel: ChannelEntity) in + if channel.name?.lowercased() ?? "" != "admin" && channel.name?.lowercased() ?? "" != "gpio" && channel.name?.lowercased() ?? "" != "serial" { - NavigationLink(destination: ChannelMessageList(myInfo: node!.myInfo!, channel: channel)) { + NavigationLink(destination: ChannelMessageList(myInfo: node!.myInfo!, channel: channel)) { - let mostRecent = channel.allPrivateMessages.last(where: { $0.channel == channel.index }) - let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) - let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0 - let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0 + let mostRecent = channel.allPrivateMessages.last(where: { $0.channel == channel.index }) + let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) + let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0 + let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0 - - ZStack { - Image(systemName: "circle.fill") - .opacity(channel.unreadMessages > 0 ? 1 : 0) - .font(.system(size: 10)) - .foregroundColor(.accentColor) - .brightness(0.2) - } - CircleText(text: String(channel.index), color: .accentColor) + ZStack { + Image(systemName: "circle.fill") + .opacity(channel.unreadMessages > 0 ? 1 : 0) + .font(.system(size: 10)) + .foregroundColor(.accentColor) .brightness(0.2) - - VStack(alignment: .leading){ - HStack{ - if channel.name?.isEmpty ?? false { - if channel.role == 1 { - Text(String("PrimaryChannel").camelCaseToWords()) - .font(.headline) - } else { - Text(String("Channel \(channel.index)").camelCaseToWords()) - .font(.headline) - } + } + CircleText(text: String(channel.index), color: .accentColor) + .brightness(0.2) + + VStack(alignment: .leading){ + HStack{ + if channel.name?.isEmpty ?? false { + if channel.role == 1 { + Text(String("PrimaryChannel").camelCaseToWords()) + .font(.headline) } else { - Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords()) + Text(String("Channel \(channel.index)").camelCaseToWords()) .font(.headline) } - - Spacer() - - if channel.allPrivateMessages.count > 0 { - - if lastMessageDay == currentDay { - Text(lastMessageTime, style: .time ) - .font(.footnote) - .foregroundColor(.secondary) - } else if lastMessageDay == (currentDay - 1) { - Text("Yesterday") - .font(.footnote) - .foregroundColor(.secondary) - } else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) { - Text(lastMessageTime.formattedDate(format: dateFormatString)) - .font(.footnote) - .foregroundColor(.secondary) - } else if lastMessageDay < (currentDay - 1800) { - Text(lastMessageTime.formattedDate(format: dateFormatString)) - .font(.footnote) - .foregroundColor(.secondary) - } - } -// Image(systemName: "chevron.forward") -// .font(.caption) -// .foregroundColor(.secondary) + } else { + Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords()) + .font(.headline) } + Spacer() + if channel.allPrivateMessages.count > 0 { - HStack(alignment: .top) { - Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")") - //.font(.system(size: 16)) + + if lastMessageDay == currentDay { + Text(lastMessageTime, style: .time ) + .font(.footnote) + .foregroundColor(.secondary) + } else if lastMessageDay == (currentDay - 1) { + Text("Yesterday") + .font(.footnote) + .foregroundColor(.secondary) + } else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) { + Text(lastMessageTime.formattedDate(format: dateFormatString)) + .font(.footnote) + .foregroundColor(.secondary) + } else if lastMessageDay < (currentDay - 1800) { + Text(lastMessageTime.formattedDate(format: dateFormatString)) .font(.footnote) .foregroundColor(.secondary) } } +// Image(systemName: "chevron.forward") +// .font(.caption) +// .foregroundColor(.secondary) } - } - .frame(height: 62) - .contextMenu { + if channel.allPrivateMessages.count > 0 { - Button(role: .destructive) { - isPresentingDeleteChannelMessagesConfirm = true - channelSelection = channel - } label: { - Label("Delete Messages", systemImage: "trash") + HStack(alignment: .top) { + Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")") + //.font(.system(size: 16)) + .font(.footnote) + .foregroundColor(.secondary) } } } - .confirmationDialog( - "This conversation will be deleted.", - isPresented: $isPresentingDeleteChannelMessagesConfirm, - titleVisibility: .visible - ) { + } + .frame(height: 62) + .contextMenu { + if channel.allPrivateMessages.count > 0 { Button(role: .destructive) { - deleteChannelMessages(channel: channelSelection!, context: context) - context.refresh(node!.myInfo!, mergeChanges: true) - UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages - channelSelection = nil + isPresentingDeleteChannelMessagesConfirm = true + channelSelection = channel } label: { - Text("delete") - } - } - .onAppear { - if self.bleManager.context == nil { - self.bleManager.context = context + Label("Delete Messages", systemImage: "trash") } } } + .confirmationDialog( + "This conversation will be deleted.", + isPresented: $isPresentingDeleteChannelMessagesConfirm, + titleVisibility: .visible + ) { + Button(role: .destructive) { + deleteChannelMessages(channel: channelSelection!, context: context) + context.refresh(node!.myInfo!, mergeChanges: true) + UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages + channelSelection = nil + } label: { + Text("delete") + } + } + .onAppear { + if self.bleManager.context == nil { + self.bleManager.context = context + } + } } - .padding([.top, .bottom]) } + .padding([.top, .bottom]) + .listStyle(.plain) } - .listStyle(.plain) - .navigationTitle("channels") } + .navigationTitle("channels") } } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index aa1b1a20..1fc7f5a7 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -24,8 +24,8 @@ struct ChannelMessageList: View { var maxbytes = 228 @FocusState var focusedField: Field? - @StateObject var myInfo: MyInfoEntity - @StateObject var channel: ChannelEntity + @ObservedObject var myInfo: MyInfoEntity + @ObservedObject var channel: ChannelEntity @State var showDeleteMessageAlert = false @State private var deleteMessageId: Int64 = 0 @State private var replyMessageId: Int64 = 0 @@ -241,6 +241,7 @@ struct ChannelMessageList: View { }, secondaryButton: .cancel()) } .onAppear { + self.focusedField = .messageText if !message.read { message.read = true do { @@ -291,15 +292,7 @@ struct ChannelMessageList: View { Button { let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" sendPositionWithMessage = true - if UserDefaults.meshtasticUsername.count > 0 { - - typingMessage += "📍 " + UserDefaults.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: { Text("share.position") Image(systemName: "mappin.and.ellipse") @@ -354,13 +347,7 @@ struct ChannelMessageList: View { Button { let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" sendPositionWithMessage = true - if UserDefaults.meshtasticUsername.count > 0 { - - typingMessage = "📍 " + UserDefaults.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: { Image(systemName: "mappin.and.ellipse") diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 39207c6b..77afb05e 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -218,6 +218,7 @@ struct UserMessageList: View { }, secondaryButton: .cancel()) } .onAppear { + self.focusedField = .messageText if !message.read { message.read = true do { @@ -257,13 +258,7 @@ struct UserMessageList: View { Button { let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" sendPositionWithMessage = true - - if UserDefaults.meshtasticUsername.count > 0 { - typingMessage = "📍 " + UserDefaults.meshtasticUsername + " has shared their position with you from node " + userLongName + " and requested a response with your position." - } else { - typingMessage = "📍 " + userLongName + " has shared their position and requested a response with your position." - } - + typingMessage = "📍 " + userLongName + " has shared their position and requested a response with your position." } label: { Text("share.position") Image(systemName: "mappin.and.ellipse") @@ -306,13 +301,7 @@ struct UserMessageList: View { Button { let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" sendPositionWithMessage = true - - if UserDefaults.meshtasticUsername.count > 0 { - typingMessage = "📍 " + UserDefaults.meshtasticUsername + " has shared their position with you from node " + userLongName + " and requested a response with your position." - } else { - typingMessage = "📍 " + userLongName + " has shared their position and requested a response with your position." - } - + typingMessage = "📍 " + userLongName + " has shared their position and requested a response with your position." } label: { Image(systemName: "mappin.and.ellipse") .symbolRenderingMode(.hierarchical) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index c0df0167..a61b9269 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -83,6 +83,16 @@ struct NodeList: View { Label("Trace Route", systemImage: "signpost.right.and.left") } } + if bleManager.connectedPeripheral != nil { + Button (role: .destructive) { + let success = bleManager.removeNode(node: node, connectedNodeNum: Int64(connectedNodeNum)) + if !success { + print("Failed to delete node \(node.user?.longName ?? "unknown".localized)") + } + } label: { + Label("Delete Node", systemImage: "trash") + } + } } } .alert( diff --git a/Meshtastic/Views/Nodes/TraceRouteLog.swift b/Meshtastic/Views/Nodes/TraceRouteLog.swift index 7b525932..a5e00a8b 100644 --- a/Meshtastic/Views/Nodes/TraceRouteLog.swift +++ b/Meshtastic/Views/Nodes/TraceRouteLog.swift @@ -34,7 +34,7 @@ struct TraceRouteLog: View { List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in Label { - Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hops?.count == 0) Hops") : "No Response")") + Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hops?.count ?? 0) \(route.hops?.count ?? 0 == 1 ? "Hop": "Hops")") : "No Response")") } icon: { Image(systemName: route.response ? (route.hops?.count == 0 && route.response ? "person.line.dotted.person" : "point.3.connected.trianglepath.dotted") : "person.slash") .symbolRenderingMode(.hierarchical) @@ -64,7 +64,7 @@ struct TraceRouteLog: View { return hop.coordinate ?? LocationHelper.DefaultLocation }) if selectedRoute?.response ?? false { - if selectedRoute?.coordinate != nil && (selectedRoute?.node?.positions?.count ?? 0 > 0 || false ) { + if selectedRoute?.hasPositions ?? false { Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { Annotation("You", coordinate: selectedRoute?.coordinate ?? LocationHelper.DefaultLocation) { ZStack { diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index f7a3bf16..d1c18930 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -10,7 +10,6 @@ struct AppSettings: View { @ObservedObject var tileManager = OfflineTileManager.shared @State var totalDownloadedTileSize = "" @StateObject var locationHelper = LocationHelper() - @State var meshtasticUsername: String = UserDefaults.meshtasticUsername @State var provideLocation: Bool = UserDefaults.provideLocation @State var blockRangeTest: Bool = UserDefaults.blockRangeTest @State var useLegacyMap: Bool = UserDefaults.mapUseLegacy @@ -20,16 +19,6 @@ struct AppSettings: View { var body: some View { VStack { Form { - Section(header: Text("user.details")) { - HStack { - Label("Name", systemImage: "person.crop.rectangle.fill") - TextField("Username", text: $meshtasticUsername) - .foregroundColor(.gray) - } - .keyboardType(.asciiCapable) - .disableAutocorrection(true) - .listRowSeparator(.visible) - } Section(header: Text("options")) { Toggle(isOn: $blockRangeTest) { @@ -147,9 +136,6 @@ struct AppSettings: View { .onChange(of: blockRangeTest) { newBlockRangeTest in UserDefaults.blockRangeTest = newBlockRangeTest } - .onChange(of: (meshtasticUsername)) { newMeshtasticUsername in - UserDefaults.meshtasticUsername = newMeshtasticUsername - } .onChange(of: provideLocation) { newProvideLocation in UserDefaults.provideLocation = newProvideLocation if bleManager.connectedPeripheral != nil { diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 682581f4..675ef69e 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -206,6 +206,7 @@ struct DeviceConfig: View { dc.debugLogEnabled = debugLogEnabled dc.buttonGpio = UInt32(buttonGPIO) dc.buzzerGpio = UInt32(buzzerGPIO) + //dc.gpsEnGpio = UInt32(gpsEnGPIO) dc.rebroadcastMode = RebroadcastModes(rawValue: rebroadcastMode)?.protoEnumValue() ?? RebroadcastModes.all.protoEnumValue() dc.doubleTapAsButtonPress = doubleTapAsButtonPress dc.isManaged = isManaged diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index 0c27ee33..e1c0d900 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -30,6 +30,7 @@ struct ExternalNotificationConfig: View { @State var outputVibra = 0 @State var outputMilliseconds = 0 @State var nagTimeout = 0 + @State var useI2SAsBuzzer = false var body: some View { VStack { @@ -79,6 +80,12 @@ struct ExternalNotificationConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Text("Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead.") .font(.caption) + Toggle(isOn: $useI2SAsBuzzer) { + Label("Use I2S As Buzzer", systemImage: "light.beacon.max.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("Enables devices with native I2S audio output to use the RTTTL over speaker like a buzzer. T-Watch S3 and T-Deck for example have this capability.") + .font(.caption) } Section(header: Text("Advanced GPIO Options")) { Section(header: Text("Primary GPIO") @@ -199,6 +206,7 @@ struct ExternalNotificationConfig: View { enc.outputVibra = UInt32(outputVibra) enc.outputMs = UInt32(outputMilliseconds) enc.usePwm = usePWM + enc.useI2SAsBuzzer = useI2SAsBuzzer let adminMessageId = bleManager.saveExternalNotificationModuleConfig(config: enc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true @@ -300,6 +308,11 @@ struct ExternalNotificationConfig: View { if newNagTimeout != node!.externalNotificationConfig!.nagTimeout { hasChanges = true } } } + .onChange(of: useI2SAsBuzzer) { newUseI2SAsBuzzer in + if node != nil && node!.externalNotificationConfig != nil { + if newUseI2SAsBuzzer != node!.externalNotificationConfig!.useI2SAsBuzzer { hasChanges = true } + } + } } func setExternalNotificationValues() { self.enabled = node?.externalNotificationConfig?.enabled ?? false @@ -316,6 +329,7 @@ struct ExternalNotificationConfig: View { self.outputMilliseconds = Int(node?.externalNotificationConfig?.outputMilliseconds ?? 0) self.nagTimeout = Int(node?.externalNotificationConfig?.nagTimeout ?? 0) self.usePWM = node?.externalNotificationConfig?.usePWM ?? false + self.useI2SAsBuzzer = node?.externalNotificationConfig?.useI2SAsBuzzer ?? false self.hasChanges = false } } diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 746926dd..4b66a453 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -38,6 +38,7 @@ struct PositionConfig: View { @State var deviceGpsEnabled = true @State var rxGpio = 0 @State var txGpio = 0 + @State var gpsEnGpio = 0 @State var fixedPosition = false @State var positionBroadcastSeconds = 0 @State var broadcastSmartMinimumDistance = 0 @@ -231,6 +232,18 @@ struct PositionConfig: View { } } .pickerStyle(DefaultPickerStyle()) + Picker("GPS EN GPIO", selection: $gpsEnGpio) { + ForEach(0..<46) { + if $0 == 0 { + Text("unset") + } else { + Text("Pin \($0)") + } + } + } + .pickerStyle(DefaultPickerStyle()) + Text("(Re)define PIN_GPS_EN for your board.") + .font(.caption) } else { Toggle(isOn: $fixedPosition) { Label("Fixed Position", systemImage: "location.square.fill") @@ -277,6 +290,7 @@ struct PositionConfig: View { pc.broadcastSmartMinimumDistance = UInt32(broadcastSmartMinimumDistance) pc.rxGpio = UInt32(rxGpio) pc.txGpio = UInt32(txGpio) + pc.gpsEnGpio = UInt32(gpsEnGpio) var pf: PositionFlags = [] if includeAltitude { pf.insert(.Altitude) } if includeAltitudeMsl { pf.insert(.AltitudeMsl) } @@ -338,6 +352,11 @@ struct PositionConfig: View { if newTxGpio != node!.positionConfig!.txGpio { hasChanges = true } } } + .onChange(of: txGpio) { newGpsEnGpio in + if node != nil && node!.positionConfig != nil { + if newGpsEnGpio != node!.positionConfig!.gpsEnGpio { hasChanges = true } + } + } .onChange(of: smartPositionEnabled) { newSmartPositionEnabled in if node != nil && node!.positionConfig != nil { if newSmartPositionEnabled != node!.positionConfig!.smartPositionEnabled { hasChanges = true } @@ -424,6 +443,7 @@ struct PositionConfig: View { self.deviceGpsEnabled = node?.positionConfig?.deviceGpsEnabled ?? true self.rxGpio = Int(node?.positionConfig?.rxGpio ?? 0) self.txGpio = Int(node?.positionConfig?.txGpio ?? 0) + self.gpsEnGpio = Int(node?.positionConfig?.gpsEnGpio ?? 0) self.fixedPosition = node?.positionConfig?.fixedPosition ?? false self.positionBroadcastSeconds = Int(node?.positionConfig?.positionBroadcastSeconds ?? 900) self.broadcastSmartMinimumIntervalSecs = Int(node?.positionConfig?.broadcastSmartMinimumIntervalSecs ?? 30) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 69a8cc96..af2c8cc8 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -316,8 +316,8 @@ struct Settings: View { if self.bleManager.context == nil { self.bleManager.context = context } - self.preferredNodeNum = UserDefaults.preferredPeripheralNum// Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) - selectedNode = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) + self.preferredNodeNum = UserDefaults.preferredPeripheralNum + self.selectedNode = Int(bleManager.connectedPeripheral != nil ? UserDefaults.preferredPeripheralNum : 0) } .listStyle(GroupedListStyle()) .navigationTitle("settings")