Merge pull request #954 from meshtastic/2.5.8

2.5.8
This commit is contained in:
Garth Vander Houwen 2024-10-06 09:58:04 -07:00 committed by GitHub
commit d4f4aa4d08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
79 changed files with 866 additions and 1304 deletions

View file

@ -11481,9 +11481,6 @@
},
"Map Tile Data" : {
},
"Map Type" : {
},
"map.centering" : {
"extractionState" : "manual",
@ -18948,9 +18945,6 @@
},
"Select a Trace Route" : {
},
"Select something to view" : {
},
"select.contact" : {
"extractionState" : "manual",
@ -19139,6 +19133,9 @@
},
"Send a message to a certain meshtastic channel" : {
},
"Send a position on the primary channel when the user button is triple clicked." : {
},
"Send a shutdown to the node you are connected to" : {
@ -21895,6 +21892,9 @@
},
"Treat double tap on supported accelerometers as a user button press." : {
},
"Triple Click Ad Hoc Ping" : {
},
"Try Again" : {

View file

@ -1165,7 +1165,7 @@
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1540;
LastUpgradeCheck = 1540;
LastUpgradeCheck = 1600;
TargetAttributes = {
25F5D5C62C4375A8008036E3 = {
CreatedOnToolsVersion = 15.4;
@ -1680,7 +1680,6 @@
DDC2E17F26CE248F0042C5E4 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
@ -1695,7 +1694,7 @@
INFOPLIST_FILE = Meshtastic/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Meshtastic;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1715,7 +1714,6 @@
DDC2E18026CE248F0042C5E4 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
@ -1730,7 +1728,7 @@
INFOPLIST_FILE = Meshtastic/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Meshtastic;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
LastUpgradeVersion = "1600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1540"
LastUpgradeVersion = "1600"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"

View file

@ -15,7 +15,7 @@ class AppIntentErrors {
var localizedStringResource: LocalizedStringResource {
switch self {
case let .message(message):
case let .message(message):
Logger.services.error("App Intent: \(message)")
return "Error: \(message)"
case .notConnected:

View file

@ -11,10 +11,10 @@ import AppIntents
struct FactoryResetNodeIntent: AppIntent {
static var title: LocalizedStringResource = "Factory Reset"
static var description: IntentDescription = "Perform a factory reset on the node you are connected to"
func perform() async throws -> some IntentResult {
// Request user confirmation before performing the factory reset
try await requestConfirmation(result: .result(dialog: "Are you sure you want to factory reset the node?"),confirmationActionName: ConfirmationActionName
try await requestConfirmation(result: .result(dialog: "Are you sure you want to factory reset the node?"), confirmationActionName: ConfirmationActionName
.custom(acceptLabel: "Factory Reset", acceptAlternatives: [], denyLabel: "Cancel", denyAlternatives: [], destructive: true))
// Ensure the node is connected
@ -27,7 +27,7 @@ struct FactoryResetNodeIntent: AppIntent {
let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext),
let fromUser = connectedNode.user,
let toUser = connectedNode.user {
// Attempt to send a factory reset command, throw an error if it fails
if !BLEManager.shared.sendFactoryReset(fromUser: fromUser, toUser: toUser) {
throw AppIntentErrors.AppIntentError.message("Failed to perform factory reset")

View file

@ -15,37 +15,36 @@ struct MessageChannelIntent: AppIntent {
@Parameter(title: "Message")
var messageContent: String
@Parameter(title: "Channel",controlStyle: .stepper, inclusiveRange: (lowerBound: 0, upperBound: 7))
@Parameter(title: "Channel", controlStyle: .stepper, inclusiveRange: (lowerBound: 0, upperBound: 7))
var channelNumber: Int
static var parameterSummary: some ParameterSummary {
Summary("Send \(\.$messageContent) to \(\.$channelNumber)")
}
func perform() async throws -> some IntentResult {
if (!BLEManager.shared.isConnected){
if !BLEManager.shared.isConnected {
throw AppIntentErrors.AppIntentError.notConnected
}
// Check if channel number is between 1 and 7
guard (0...7).contains(channelNumber) else {
throw $channelNumber.needsValueError("Channel number must be between 0 and 7.")
}
// Convert messageContent to data and check its length
guard let messageData = messageContent.data(using: .utf8) else {
throw AppIntentErrors.AppIntentError.message("Failed to encode message content")
}
if messageData.count > 200 {
throw $messageContent.needsValueError("Message content exceeds 200 bytes.")
}
if(!BLEManager.shared.sendMessage(message: messageContent, toUserNum: 0, channel: Int32(channelNumber), isEmoji: false, replyID: 0)){
if !BLEManager.shared.sendMessage(message: messageContent, toUserNum: 0, channel: Int32(channelNumber), isEmoji: false, replyID: 0) {
throw AppIntentErrors.AppIntentError.message("Failed to send message")
}
return .result()
return .result()
}
}

View file

@ -14,44 +14,38 @@ struct NodePositionIntent: AppIntent {
@Parameter(title: "Node Number")
var nodeNum: Int
static var title: LocalizedStringResource = "Get Node Position"
static var description: IntentDescription = "Fetch the latest position of a cetain node"
func perform() async throws -> some IntentResult & ReturnsValue<CLPlacemark> {
if (!BLEManager.shared.isConnected) {
if !BLEManager.shared.isConnected {
throw AppIntentErrors.AppIntentError.notConnected
}
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "NodeInfoEntity")
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
do {
guard let fetchedNode = try PersistenceController.shared.container.viewContext.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity], fetchedNode.count == 1 else {
throw $nodeNum.needsValueError("Could not find node")
}
let nodeInfo = fetchedNode[0]
nodeInfo.latestEnvironmentMetrics?.batteryLevel
if let latitude = nodeInfo.latestPosition?.coordinate.latitude,
let longitude = nodeInfo.latestPosition?.coordinate.longitude {
let nodeLocation = CLLocation(latitude: latitude, longitude: longitude)
// Reverse geocode the CLLocation to get a CLPlacemark
let geocoder = CLGeocoder()
let placemarks = try await geocoder.reverseGeocodeLocation(nodeLocation)
if let placemark = placemarks.first {
return .result(value: placemark)
} else {
throw AppIntentErrors.AppIntentError.message("Error Reverse Geocoding Location")
}
} else {
throw AppIntentErrors.AppIntentError.message("Node does not have positions")
}
} catch {
throw AppIntentErrors.AppIntentError.message("Fetch Failure")
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "NodeInfoEntity")
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
do {
guard let fetchedNode = try PersistenceController.shared.container.viewContext.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity], fetchedNode.count == 1 else {
throw $nodeNum.needsValueError("Could not find node")
}
}
}
let nodeInfo = fetchedNode[0]
if let latitude = nodeInfo.latestPosition?.coordinate.latitude,
let longitude = nodeInfo.latestPosition?.coordinate.longitude {
let nodeLocation = CLLocation(latitude: latitude, longitude: longitude)
// Reverse geocode the CLLocation to get a CLPlacemark
let geocoder = CLGeocoder()
let placemarks = try await geocoder.reverseGeocodeLocation(nodeLocation)
if let placemark = placemarks.first {
return .result(value: placemark)
} else {
throw AppIntentErrors.AppIntentError.message("Error Reverse Geocoding Location")
}
} else {
throw AppIntentErrors.AppIntentError.message("Node does not have positions")
}
} catch {
throw AppIntentErrors.AppIntentError.message("Fetch Failure")
}
}
}

View file

@ -16,10 +16,10 @@ struct SendWaypointIntent: AppIntent {
@Parameter(title: "Name", default: "Dropped Pin")
var nameParameter: String?
@Parameter(title: "Description", default: "")
var descriptionParameter: String?
@Parameter(title: "Emoji", default: "📍")
var emojiParameter: String?
@ -27,19 +27,19 @@ struct SendWaypointIntent: AppIntent {
var locationParameter: CLPlacemark
func perform() async throws -> some IntentResult {
if (!BLEManager.shared.isConnected){
if !BLEManager.shared.isConnected {
throw AppIntentErrors.AppIntentError.notConnected
}
// Provide default values if parameters are nil
let name = nameParameter ?? "Dropped Pin"
let description = descriptionParameter ?? ""
let emoji = emojiParameter ?? "📍"
// Validate name length
if name.utf8.count > 30 {
throw $nameParameter.needsValueError("Name must be less than 30 bytes")
}
// Validate description length
if description.utf8.count > 100 {
throw $descriptionParameter.needsValueError("Description must be less than 100 bytes")
@ -60,7 +60,6 @@ struct SendWaypointIntent: AppIntent {
newWaypoint.longitudeI = Int32(longitude * 10_000_000)
}
newWaypoint.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
// Unicode scalar value for the icon emoji string
let unicodeScalers = emoji.unicodeScalars
@ -69,12 +68,9 @@ struct SendWaypointIntent: AppIntent {
newWaypoint.icon = unicode
newWaypoint.name = name
newWaypoint.description_p = description
if(!BLEManager.shared.sendWaypoint(waypoint: newWaypoint)){
if !BLEManager.shared.sendWaypoint(waypoint: newWaypoint) {
throw AppIntentErrors.AppIntentError.message("Failed to Send Waypoint")
}
return .result()
}

View file

@ -177,7 +177,6 @@ enum Iaq: Int, CaseIterable, Identifiable {
}
}
// Default of 0 is Client
enum MetricsTypes: Int, CaseIterable, Identifiable {

View file

@ -54,31 +54,28 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
let LOGRADIO_UUID = CBUUID(string: "0x5a3d6e49-06e6-4423-9944-e9de8cdf9547")
// MARK: init
private override init() {
// Default initialization should not be used
fatalError("Use setup(appState:context:) to initialize the singleton")
}
// Default initialization should not be used
fatalError("Use setup(appState:context:) to initialize the singleton")
}
static func setup(appState: AppState, context: NSManagedObjectContext) {
guard shared == nil else {
print("BLEManager already initialized")
return
}
shared = BLEManager(appState: appState, context: context)
static func setup(appState: AppState, context: NSManagedObjectContext) {
guard shared == nil else {
Logger.services.warning("[BLE] BLEManager already initialized")
return
}
shared = BLEManager(appState: appState, context: context)
}
private init(appState: AppState, context: NSManagedObjectContext) {
self.appState = appState
self.context = context
self.lastConnectionError = ""
self.connectedVersion = "0.0.0"
super.init()
centralManager = CBCentralManager(delegate: self, queue: nil)
mqttManager.delegate = self
}
private init(appState: AppState, context: NSManagedObjectContext) {
self.appState = appState
self.context = context
self.lastConnectionError = ""
self.connectedVersion = "0.0.0"
super.init()
centralManager = CBCentralManager(delegate: self, queue: nil)
mqttManager.delegate = self
}
// MARK: Scanning for BLE Devices
// Scan for nearby BLE devices using the Meshtastic BLE service ID
@ -464,7 +461,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
do {
let fetchedNodes = try context.fetch(nodes)
let receivingNode = fetchedNodes.first(where: { $0.num == destNum })
let connectedNode = fetchedNodes.first(where: { $0.num == self.connectedPeripheral.num })
traceRoute.id = Int64(meshPacket.id)
traceRoute.time = Date()
traceRoute.node = receivingNode
@ -1202,65 +1198,37 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
@MainActor
public func getPositionFromPhoneGPS(destNum: Int64, fixedPosition: Bool) -> Position? {
var positionPacket = Position()
if #available(iOS 17.0, macOS 14.0, *) {
guard let lastLocation = LocationsHandler.shared.locationsArray.last else {
return nil
}
guard let lastLocation = LocationsHandler.shared.locationsArray.last else {
return nil
}
if lastLocation == CLLocation(latitude: 0, longitude: 0) {
return nil
}
positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7)
positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7)
let timestamp = lastLocation.timestamp
positionPacket.time = UInt32(timestamp.timeIntervalSince1970)
positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970)
positionPacket.altitude = Int32(lastLocation.altitude)
positionPacket.satsInView = UInt32(LocationsHandler.satsInView)
let currentSpeed = lastLocation.speed
if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) {
positionPacket.groundSpeed = UInt32(currentSpeed)
}
let currentHeading = lastLocation.course
if (currentHeading > 0 && currentHeading <= 360) && (!currentHeading.isNaN || !currentHeading.isInfinite) {
positionPacket.groundTrack = UInt32(currentHeading)
}
/// Set location source for time
if !fixedPosition {
/// From GPS treat time as good
positionPacket.locationSource = Position.LocSource.locExternal
} else {
/// From GPS, but time can be old and have drifted
positionPacket.locationSource = Position.LocSource.locManual
}
if lastLocation == CLLocation(latitude: 0, longitude: 0) {
return nil
}
positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7)
positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7)
let timestamp = lastLocation.timestamp
positionPacket.time = UInt32(timestamp.timeIntervalSince1970)
positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970)
positionPacket.altitude = Int32(lastLocation.altitude)
positionPacket.satsInView = UInt32(LocationsHandler.satsInView)
let currentSpeed = lastLocation.speed
if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) {
positionPacket.groundSpeed = UInt32(currentSpeed)
}
let currentHeading = lastLocation.course
if (currentHeading > 0 && currentHeading <= 360) && (!currentHeading.isNaN || !currentHeading.isInfinite) {
positionPacket.groundTrack = UInt32(currentHeading)
}
/// Set location source for time
if !fixedPosition {
/// From GPS treat time as good
positionPacket.locationSource = Position.LocSource.locExternal
} else {
positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7)
positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7)
let timestamp = LocationHelper.shared.locationManager.location?.timestamp ?? Date()
positionPacket.time = UInt32(timestamp.timeIntervalSince1970)
positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970)
positionPacket.altitude = Int32(LocationHelper.shared.locationManager.location?.altitude ?? 0)
positionPacket.satsInView = UInt32(LocationHelper.satsInView)
let currentSpeed = LocationHelper.shared.locationManager.location?.speed ?? 0
if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) {
positionPacket.groundSpeed = UInt32(currentSpeed)
}
let currentHeading = LocationHelper.shared.locationManager.location?.course ?? 0
if (currentHeading > 0 && currentHeading <= 360) && (!currentHeading.isNaN || !currentHeading.isInfinite) {
positionPacket.groundTrack = UInt32(currentHeading)
}
/// Set location source for time
if !fixedPosition {
/// From GPS treat time as good
positionPacket.locationSource = Position.LocSource.locExternal
} else {
/// From GPS, but time can be old and have drifted
positionPacket.locationSource = Position.LocSource.locManual
}
/// From GPS, but time can be old and have drifted
positionPacket.locationSource = Position.LocSource.locManual
}
return positionPacket
}
@ -1645,7 +1613,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
let decodedString = base64UrlString.base64urlToBase64()
if let decodedData = Data(base64Encoded: decodedString) {
do {
let channelSet: ChannelSet = try ChannelSet(serializedData: decodedData)
let channelSet: ChannelSet = try ChannelSet(serializedBytes: decodedData)
for cs in channelSet.settings {
if addChannels {
// We are trying to add a channel so lets get the last index
@ -2017,10 +1985,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
let messageDescription = "🛟 Saved LoRa Config for \(toUser.longName ?? "unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey,context: context)
upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context)
return Int64(meshPacket.id)
}
return 0
}
@ -3223,7 +3190,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
func storeAndForwardPacket(packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) {
if let storeAndForwardMessage = try? StoreAndForward(serializedData: packet.decoded.payload) {
if let storeAndForwardMessage = try? StoreAndForward(serializedBytes: packet.decoded.payload) {
// Handle each of the store and forward request / response messages
switch storeAndForwardMessage.rr {
case .unset:
@ -3349,7 +3316,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
// MARK: - CB Central Manager implmentation
extension BLEManager: CBCentralManagerDelegate {
// MARK: Bluetooth enabled/disabled
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == CBManagerState.poweredOn {
@ -3359,9 +3326,8 @@ extension BLEManager: CBCentralManagerDelegate {
} else {
isSwitchedOn = false
}
var status = ""
switch central.state {
case .poweredOff:
status = "BLE is powered off"
@ -3380,10 +3346,9 @@ extension BLEManager: CBCentralManagerDelegate {
}
Logger.services.info("📜 [BLE] Bluetooth status: \(status)")
}
// Called each time a peripheral is discovered
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
if self.automaticallyReconnect && peripheral.identifier.uuidString == UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" {
self.connectTo(peripheral: peripheral)
Logger.services.info("✅ [BLE] Reconnecting to prefered peripheral: \(peripheral.name ?? "Unknown", privacy: .public)")
@ -3391,7 +3356,6 @@ extension BLEManager: CBCentralManagerDelegate {
let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String
let device = Peripheral(id: peripheral.identifier.uuidString, num: 0, name: name ?? "Unknown", shortName: "?", longName: name ?? "Unknown", firmwareVersion: "Unknown", rssi: RSSI.intValue, lastUpdate: Date(), peripheral: peripheral)
let index = peripherals.map { $0.peripheral }.firstIndex(of: peripheral)
if let peripheralIndex = index {
peripherals[peripheralIndex] = device
} else {

View file

@ -5,16 +5,9 @@ import OSLog
class LocalNotificationManager {
var notifications = [Notification]()
let thumbsUpAction = UNNotificationAction(identifier: "messageNotification.thumbsUpAction", title:
"👍 \(Tapbacks.thumbsUp.description)", options: [])
let thumbsDownAction = UNNotificationAction(identifier: "messageNotification.thumbsDownAction", title:
"👎 \(Tapbacks.thumbsDown.description)", options: [])
let replyInputAction = UNTextInputNotificationAction(
identifier: "messageNotification.replyInputAction",
title: "reply".localized,
options: [])
let thumbsUpAction = UNNotificationAction(identifier: "messageNotification.thumbsUpAction", title: "👍 \(Tapbacks.thumbsUp.description)", options: [])
let thumbsDownAction = UNNotificationAction(identifier: "messageNotification.thumbsDownAction", title: "👎 \(Tapbacks.thumbsDown.description)", options: [])
let replyInputAction = UNTextInputNotificationAction(identifier: "messageNotification.replyInputAction", title: "reply".localized, options: [])
// Step 1 Request Permissions for notifications
private func requestAuthorization() {
@ -43,13 +36,13 @@ class LocalNotificationManager {
private func scheduleNotifications() {
let messageNotificationCategory = UNNotificationCategory(
identifier: "messageNotificationCategory",
actions: [thumbsUpAction, thumbsDownAction,replyInputAction],
actions: [thumbsUpAction, thumbsDownAction, replyInputAction],
intentIdentifiers: [],
options: .customDismissAction
)
UNUserNotificationCenter.current().setNotificationCategories([messageNotificationCategory])
for notification in notifications {
let content = UNMutableNotificationContent()
content.subtitle = notification.subtitle
@ -75,7 +68,6 @@ class LocalNotificationManager {
content.userInfo["userNum"] = notification.userNum
}
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger)

View file

@ -10,7 +10,6 @@ import CoreLocation
import OSLog
// Shared state that manages the `CLLocationManager` and `CLBackgroundActivitySession`.
@available(iOS 17.0, macOS 14.0, *)
@MainActor class LocationsHandler: ObservableObject {
static let shared = LocationsHandler() // Create a single, shared instance of the object.

View file

@ -50,7 +50,6 @@ func generateMessageMarkdown (message: String) -> String {
func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) {
let remote = nodeNum != UserDefaults.preferredPeripheralNum
if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) {
upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: nodeNum, context: context)
} else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) {
@ -454,11 +453,11 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
if let adminMessage = try? AdminMessage(serializedData: packet.decoded.payload) {
if let adminMessage = try? AdminMessage(serializedBytes: packet.decoded.payload) {
if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getCannedMessageModuleMessagesResponse(adminMessage.getCannedMessageModuleMessagesResponse) {
if let cmmc = try? CannedMessageModuleConfig(serializedData: packet.decoded.payload) {
if let cmmc = try? CannedMessageModuleConfig(serializedBytes: packet.decoded.payload) {
if !cmmc.messages.isEmpty {
@ -581,7 +580,7 @@ func paxCounterPacket (packet: MeshPacket, context: NSManagedObjectContext) {
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
if let paxMessage = try? Paxcount(serializedData: packet.decoded.payload) {
if let paxMessage = try? Paxcount(serializedBytes: packet.decoded.payload) {
let newPax = PaxCounterEntity(context: context)
newPax.ble = Int32(truncatingIfNeeded: paxMessage.ble)
@ -611,7 +610,7 @@ func paxCounterPacket (packet: MeshPacket, context: NSManagedObjectContext) {
func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) {
if let routingMessage = try? Routing(serializedData: packet.decoded.payload) {
if let routingMessage = try? Routing(serializedBytes: packet.decoded.payload) {
let routingError = RoutingError(rawValue: routingMessage.errorReason.rawValue)
@ -833,7 +832,7 @@ func textMessageAppPacket(
}
var storeForwardBroadcast = false
if storeForward {
if let storeAndForwardMessage = try? StoreAndForward(serializedData: packet.decoded.payload) {
if let storeAndForwardMessage = try? StoreAndForward(serializedBytes: packet.decoded.payload) {
messageText = String(bytes: storeAndForwardMessage.text, encoding: .utf8)
if storeAndForwardMessage.rr == .routerTextBroadcast {
storeForwardBroadcast = true
@ -993,7 +992,6 @@ func textMessageAppPacket(
}
}
func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.waypoint.received %@".localized, String(packet.from))
@ -1004,7 +1002,7 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
do {
if let waypointMessage = try? Waypoint(serializedData: packet.decoded.payload) {
if let waypointMessage = try? Waypoint(serializedBytes: packet.decoded.payload) {
let fetchedWaypoint = try context.fetch(fetchWaypointRequest)
if fetchedWaypoint.isEmpty {
let waypoint = WaypointEntity(context: context)

View file

@ -69,6 +69,7 @@
<attribute name="rebroadcastMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="serialEnabled" optional="YES" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="tripleClickAsAdHocPing" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="tzdef" optional="YES" attributeType="String"/>
<relationship name="deviceConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="deviceConfig" inverseEntity="NodeInfoEntity"/>
</entity>

View file

@ -3,11 +3,8 @@
import SwiftUI
import CoreData
import OSLog
#if canImport(TipKit)
import TipKit
#endif
@available(iOS 17.0, *)
@main
struct MeshtasticAppleApp: App {
@ -111,26 +108,24 @@ struct MeshtasticAppleApp: App {
}
})
.task {
if #available(iOS 17.0, macOS 14.0, *) {
#if DEBUG
/// Optionally, call `Tips.resetDatastore()` before `Tips.configure()` to reset the state of all tips. This will allow tips to re-appear even after they have been dismissed by the user.
/// This is for testing only, and should not be enabled in release builds.
try? Tips.resetDatastore()
#endif
#if DEBUG
/// Optionally, call `Tips.resetDatastore()` before `Tips.configure()` to reset the state of all tips. This will allow tips to re-appear even after they have been dismissed by the user.
/// This is for testing only, and should not be enabled in release builds.
try? Tips.resetDatastore()
#endif
try? Tips.configure(
[
// Reset which tips have been shown and what parameters have been tracked, useful during testing and for this sample project
.datastoreLocation(.applicationDefault),
// When should the tips be presented? If you use .immediate, they'll all be presented whenever a screen with a tip appears.
// You can adjust this on per tip level as well
.displayFrequency(.immediate)
]
)
}
try? Tips.configure(
[
// Reset which tips have been shown and what parameters have been tracked, useful during testing and for this sample project
.datastoreLocation(.applicationDefault),
// When should the tips be presented? If you use .immediate, they'll all be presented whenever a screen with a tip appears.
// You can adjust this on per tip level as well
.displayFrequency(.immediate)
]
)
}
}
.onChange(of: scenePhase) { (newScenePhase) in
.onChange(of: scenePhase) { (_, newScenePhase) in
switch newScenePhase {
case .background:
Logger.services.info("🎬 [App] Scene is in the background")

View file

@ -9,9 +9,9 @@ import SwiftUI
import OSLog
class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, ObservableObject {
var router: Router?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
Logger.services.info("🚀 [App] Meshtstic Apple App launched!")
// Default User Default Values
@ -19,14 +19,11 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
UserDefaults.standard.register(defaults: ["meshMapShowNodeHistory": true])
UserDefaults.standard.register(defaults: ["meshMapShowRouteLines": true])
UNUserNotificationCenter.current().delegate = self
if #available(iOS 17.0, macOS 14.0, *) {
let locationsHandler = LocationsHandler.shared
locationsHandler.startLocationUpdates()
// If a background activity session was previously active, reinstantiate it after the background launch.
if locationsHandler.backgroundActivity {
locationsHandler.backgroundActivity = true
}
let locationsHandler = LocationsHandler.shared
locationsHandler.startLocationUpdates()
// If a background activity session was previously active, reinstantiate it after the background launch.
if locationsHandler.backgroundActivity {
locationsHandler.backgroundActivity = true
}
return true
}
@ -38,7 +35,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
) {
completionHandler([.list, .banner, .sound])
}
// This method is called when a user clicks on the notification
func userNotificationCenter(
_ center: UNUserNotificationCenter,
@ -46,11 +43,10 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
withCompletionHandler completionHandler: @escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo
switch response.actionIdentifier {
case UNNotificationDefaultActionIdentifier:
break
case "messageNotification.thumbsUpAction":
if let channel = userInfo["channel"] as? Int32,
let replyID = userInfo["messageId"] as? Int64 {
@ -65,8 +61,6 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
} else {
Logger.services.error("Failed to retrieve channel or messageId from userInfo")
}
break
case "messageNotification.thumbsDownAction":
if let channel = userInfo["channel"] as? Int32,
let replyID = userInfo["messageId"] as? Int64 {
@ -81,8 +75,6 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
} else {
Logger.services.error("Failed to retrieve channel or messageId from userInfo")
}
break
case "messageNotification.replyInputAction":
if let userInput = (response as? UNTextInputNotificationResponse)?.userText,
let channel = userInfo["channel"] as? Int32,
@ -98,12 +90,10 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
} else {
Logger.services.error("Failed to retrieve user input, channel, or messageId from userInfo")
}
break
default:
break
}
if let targetValue = userInfo["target"] as? String,
let deepLink = userInfo["path"] as? String,
let url = URL(string: deepLink) {
@ -112,7 +102,6 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
} else {
Logger.services.error("Failed to handle notification response: \(userInfo)")
}
completionHandler()
}
}

View file

@ -159,12 +159,12 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
if packet.to == Constants.maximumNodeNum || packet.to == UserDefaults.preferredPeripheralNum {
newNode.channel = Int32(packet.channel)
}
if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) {
if let nodeInfoMessage = try? NodeInfo(serializedBytes: packet.decoded.payload) {
newNode.hopsAway = Int32(nodeInfoMessage.hopsAway)
newNode.favorite = nodeInfoMessage.isFavorite
}
if let newUserMessage = try? User(serializedData: packet.decoded.payload) {
if let newUserMessage = try? User(serializedBytes: packet.decoded.payload) {
if newUserMessage.id.isEmpty {
if packet.from > Constants.minimumNodeNum {
@ -254,7 +254,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
fetchedNode[0].channel = Int32(packet.channel)
}
if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) {
if let nodeInfoMessage = try? NodeInfo(serializedBytes: packet.decoded.payload) {
fetchedNode[0].hopsAway = Int32(nodeInfoMessage.hopsAway)
fetchedNode[0].favorite = nodeInfoMessage.isFavorite
@ -320,7 +320,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
do {
if let positionMessage = try? Position(serializedData: packet.decoded.payload) {
if let positionMessage = try? Position(serializedBytes: packet.decoded.payload) {
/// Don't save empty position packets from null island or apple park
if (positionMessage.longitudeI != 0 && positionMessage.latitudeI != 0) && (positionMessage.latitudeI != 373346000 && positionMessage.longitudeI != -1220090000) {
@ -462,24 +462,24 @@ func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessi
if fetchedNode[0].deviceConfig == nil {
let newDeviceConfig = DeviceConfigEntity(context: context)
newDeviceConfig.role = Int32(config.role.rawValue)
newDeviceConfig.serialEnabled = config.serialEnabled
newDeviceConfig.buttonGpio = Int32(config.buttonGpio)
newDeviceConfig.buzzerGpio = Int32(config.buzzerGpio)
newDeviceConfig.rebroadcastMode = Int32(config.rebroadcastMode.rawValue)
newDeviceConfig.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber)
newDeviceConfig.doubleTapAsButtonPress = config.doubleTapAsButtonPress
newDeviceConfig.tripleClickAsAdHocPing = !config.disableTripleClick
newDeviceConfig.ledHeartbeatEnabled = !config.ledHeartbeatDisabled
newDeviceConfig.isManaged = config.isManaged
newDeviceConfig.tzdef = config.tzdef
fetchedNode[0].deviceConfig = newDeviceConfig
} else {
fetchedNode[0].deviceConfig?.role = Int32(config.role.rawValue)
fetchedNode[0].deviceConfig?.serialEnabled = config.serialEnabled
fetchedNode[0].deviceConfig?.buttonGpio = Int32(config.buttonGpio)
fetchedNode[0].deviceConfig?.buzzerGpio = Int32(config.buzzerGpio)
fetchedNode[0].deviceConfig?.rebroadcastMode = Int32(config.rebroadcastMode.rawValue)
fetchedNode[0].deviceConfig?.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber)
fetchedNode[0].deviceConfig?.doubleTapAsButtonPress = config.doubleTapAsButtonPress
fetchedNode[0].deviceConfig?.tripleClickAsAdHocPing = !config.disableTripleClick
fetchedNode[0].deviceConfig?.ledHeartbeatEnabled = !config.ledHeartbeatDisabled
fetchedNode[0].deviceConfig?.isManaged = config.isManaged
fetchedNode[0].deviceConfig?.tzdef = config.tzdef

View file

@ -5,11 +5,8 @@
// Copyright(c) Garth Vander Houwen 8/31/23.
//
import SwiftUI
#if canImport(TipKit)
import TipKit
#endif
@available(iOS 17.0, macOS 14.0, *)
struct BluetoothConnectionTip: Tip {
var id: String {

View file

@ -5,11 +5,8 @@
// Copyright(c) Garth Vander Houwen 8/31/23.
//
import SwiftUI
#if canImport(TipKit)
import TipKit
#endif
@available(iOS 17.0, macOS 14.0, *)
struct ShareChannelsTip: Tip {
var id: String {
@ -26,7 +23,6 @@
}
}
@available(iOS 17.0, macOS 14.0, *)
struct CreateChannelsTip: Tip {
var id: String {
@ -43,7 +39,6 @@ struct CreateChannelsTip: Tip {
}
}
@available(iOS 17.0, macOS 14.0, *)
struct AdminChannelTip: Tip {
var id: String {

View file

@ -5,11 +5,8 @@
// Copyright(c) Garth Vander Houwen 9/15/23.
//
import SwiftUI
#if canImport(TipKit)
import TipKit
#endif
@available(iOS 17.0, macOS 14.0, *)
struct MessagesTip: Tip {
var id: String {

View file

@ -11,9 +11,7 @@ import CoreData
import CoreLocation
import CoreBluetooth
import OSLog
#if canImport(TipKit)
import TipKit
#endif
#if canImport(ActivityKit)
import ActivityKit
#endif
@ -49,9 +47,7 @@ struct Connect: View {
if bleManager.isSwitchedOn {
Section(header: Text("connected.radio").font(.title)) {
if let connectedPeripheral = bleManager.connectedPeripheral, connectedPeripheral.peripheral.state == .connected {
if #available(iOS 17.0, macOS 14.0, *) {
TipView(BluetoothConnectionTip(), arrowEdge: .bottom)
}
TipView(BluetoothConnectionTip(), arrowEdge: .bottom)
VStack(alignment: .leading) {
HStack {
VStack(alignment: .center) {
@ -78,12 +74,10 @@ struct Connect: View {
.foregroundColor(.green)
} else {
HStack {
if #available(iOS 17.0, macOS 14.0, *) {
Image(systemName: "square.stack.3d.down.forward")
.symbolRenderingMode(.multicolor)
.symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3))
.foregroundColor(.orange)
}
Image(systemName: "square.stack.3d.down.forward")
.symbolRenderingMode(.multicolor)
.symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3))
.foregroundColor(.orange)
Text("communicating").font(.callout)
.foregroundColor(.orange)
}
@ -307,10 +301,10 @@ struct Connect: View {
.presentationDetents([.large])
.presentationDragIndicator(.automatic)
}
.onChange(of: (self.bleManager.invalidVersion)) { _ in
.onChange(of: self.bleManager.invalidVersion) {
invalidFirmwareVersion = self.bleManager.invalidVersion
}
.onChange(of: (self.bleManager.isSubscribed)) { sub in
.onChange(of: self.bleManager.isSubscribed) { _, sub in
if UserDefaults.preferredPeripheralId.count > 0 && sub {

View file

@ -4,7 +4,6 @@
import SwiftUI
@available(iOS 17.0, *)
struct ContentView: View {
@ObservedObject
var appState: AppState
@ -39,19 +38,11 @@ struct ContentView: View {
}
.tag(NavigationState.Tab.nodes)
if #available(iOS 17.0, macOS 14.0, *), !UserDefaults.mapUseLegacy {
MeshMap(router: appState.router)
.tabItem {
Label("map", systemImage: "map")
}
.tag(NavigationState.Tab.map)
} else {
NodeMap(router: appState.router)
.tabItem {
Label("map", systemImage: "map")
}
.tag(NavigationState.Tab.map)
}
MeshMap(router: appState.router)
.tabItem {
Label("map", systemImage: "map")
}
.tag(NavigationState.Tab.map)
Settings(
router: appState.router

View file

@ -1,164 +1,164 @@
////
//// NodeMapControl.swift
//// Meshtastic
////
//// Created by Garth Vander Houwen on 9/9/23.
////
//import SwiftUI
//import CoreLocation
//import MapKit
//import WeatherKit
//import OSLog
//
// NodeMapControl.swift
// Meshtastic
//struct NodeMapMapkit: View {
//
// Created by Garth Vander Houwen on 9/9/23.
// @Environment(\.managedObjectContext) var context
// @EnvironmentObject var bleManager: BLEManager
// /// Weather
// /// The current weather condition for the city.
// @State private var condition: WeatherCondition?
// @State private var temperature: Measurement<UnitTemperature>?
// @State private var humidity: Int?
// @State private var symbolName: String = "cloud.fill"
// @State private var attributionLink: URL?
// @State private var attributionLogo: URL?
//
import SwiftUI
import CoreLocation
import MapKit
import WeatherKit
import OSLog
struct NodeMapMapkit: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
/// Weather
/// The current weather condition for the city.
@State private var condition: WeatherCondition?
@State private var temperature: Measurement<UnitTemperature>?
@State private var humidity: Int?
@State private var symbolName: String = "cloud.fill"
@State private var attributionLink: URL?
@State private var attributionLogo: URL?
@Environment(\.colorScheme) var colorScheme: ColorScheme
@AppStorage("meshMapType") private var meshMapType = 0
@AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false
@AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false
@State private var selectedMapLayer: MapLayer = .standard
@State var waypointCoordinate: WaypointCoordinate?
@State var editingWaypoint: Int = 0
@State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay(
mapName: "offlinemap",
tileType: "png",
canReplaceMapContent: true
)
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
predicate: NSPredicate(
format: "expire == nil || expire >= %@", Date() as NSDate
), animation: .none)
private var waypoints: FetchedResults<WaypointEntity>
@ObservedObject var node: NodeInfoEntity
var body: some View {
NavigationStack {
GeometryReader { bounds in
VStack {
if node.hasPositions {
ZStack {
let positionArray = node.positions?.array as? [PositionEntity] ?? []
let lastTenThousand = Array(positionArray.prefix(10000))
// let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) }
ZStack {
MapViewSwiftUI(onLongPress: { coord in
waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0)
}, onWaypointEdit: { wpId in
if wpId > 0 {
waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId))
}
},
selectedMapLayer: selectedMapLayer,
positions: lastTenThousand,
waypoints: Array(waypoints),
userTrackingMode: MKUserTrackingMode.none,
showNodeHistory: meshMapShowNodeHistory,
showRouteLines: meshMapShowRouteLines,
customMapOverlay: self.customMapOverlay
)
VStack(alignment: .leading) {
Spacer()
HStack(alignment: .bottom, spacing: 1) {
Picker("Map Type", selection: $selectedMapLayer) {
ForEach(MapLayer.allCases, id: \.self) { layer in
if layer == MapLayer.offline && UserDefaults.enableOfflineMaps {
Text(layer.localized)
} else if layer != MapLayer.offline {
Text(layer.localized)
}
}
}
.onChange(of: (selectedMapLayer)) { newMapLayer in
UserDefaults.mapLayer = newMapLayer
}
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.pickerStyle(.menu)
.padding(5)
VStack {
VStack {
Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName)
.font(.caption)
Label("\(humidity ?? 0)%", systemImage: "humidity")
.font(.caption2)
AsyncImage(url: attributionLogo) { image in
image
.resizable()
.scaledToFit()
} placeholder: {
ProgressView()
.controlSize(.mini)
}
.frame(height: 10)
Link("Other data sources", destination: attributionLink ?? URL(string: "https://weather-data.apple.com/legal-attribution.html")!)
.font(.caption2)
}
.padding(5)
}
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.padding(5)
.task {
do {
if node.hasPositions {
let mostRecent = node.positions?.lastObject as? PositionEntity
let weather = try await WeatherService.shared.weather(for: mostRecent?.nodeLocation ?? CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude))
condition = weather.currentWeather.condition
temperature = weather.currentWeather.temperature
humidity = Int(weather.currentWeather.humidity * 100)
symbolName = weather.currentWeather.symbolName
let attribution = try await WeatherService.shared.attribution
attributionLink = attribution.legalPageURL
attributionLogo = colorScheme == .light ? attribution.combinedMarkLightURL : attribution.combinedMarkDarkURL
}
} catch {
Logger.services.error("Could not gather weather information: \(error.localizedDescription)")
condition = .clear
symbolName = "cloud.fill"
}
}
}
}
}
.ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
.frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.65)
}
} else {
HStack {
}
.padding([.top], 20)
}
}
.edgesIgnoringSafeArea([.leading, .trailing])
.sheet(item: $waypointCoordinate, content: { wpc in
WaypointFormMapKit(coordinate: wpc)
.presentationDetents([.medium, .large])
.presentationDragIndicator(.automatic)
})
.navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline)
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(
bluetoothOn: bleManager.isSwitchedOn,
deviceConnected: bleManager.connectedPeripheral != nil,
name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
}
.padding(.bottom, 2)
}
}
}
// @Environment(\.colorScheme) var colorScheme: ColorScheme
// @AppStorage("meshMapType") private var meshMapType = 0
// @AppStorage("meshMapShowNodeHistory") private var meshMapShowNodeHistory = false
// @AppStorage("meshMapShowRouteLines") private var meshMapShowRouteLines = false
// @State private var selectedMapLayer: MapLayer = .standard
// @State var waypointCoordinate: WaypointCoordinate?
// @State var editingWaypoint: Int = 0
// @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay(
// mapName: "offlinemap",
// tileType: "png",
// canReplaceMapContent: true
// )
// @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
// predicate: NSPredicate(
// format: "expire == nil || expire >= %@", Date() as NSDate
// ), animation: .none)
// private var waypoints: FetchedResults<WaypointEntity>
// @ObservedObject var node: NodeInfoEntity
//
// var body: some View {
//
// NavigationStack {
// GeometryReader { bounds in
// VStack {
// if node.hasPositions {
// ZStack {
// let positionArray = node.positions?.array as? [PositionEntity] ?? []
// let lastTenThousand = Array(positionArray.prefix(10000))
// // let todaysPositions = positionArray.filter { $0.time! >= Calendar.current.startOfDay(for: Date()) }
// ZStack {
// MapViewSwiftUI(onLongPress: { coord in
// waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0)
// }, onWaypointEdit: { wpId in
// if wpId > 0 {
// waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId))
// }
// },
// selectedMapLayer: selectedMapLayer,
// positions: lastTenThousand,
// waypoints: Array(waypoints),
// userTrackingMode: MKUserTrackingMode.none,
// showNodeHistory: meshMapShowNodeHistory,
// showRouteLines: meshMapShowRouteLines,
// customMapOverlay: self.customMapOverlay
// )
// VStack(alignment: .leading) {
// Spacer()
// HStack(alignment: .bottom, spacing: 1) {
// Picker("Map Type", selection: $selectedMapLayer) {
// ForEach(MapLayer.allCases, id: \.self) { layer in
// if layer == MapLayer.offline && UserDefaults.enableOfflineMaps {
// Text(layer.localized)
// } else if layer != MapLayer.offline {
// Text(layer.localized)
// }
// }
// }
// .onChange(of: (selectedMapLayer)) { newMapLayer in
// UserDefaults.mapLayer = newMapLayer
// }
// .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
// .pickerStyle(.menu)
// .padding(5)
// VStack {
// VStack {
// Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName)
// .font(.caption)
//
// Label("\(humidity ?? 0)%", systemImage: "humidity")
// .font(.caption2)
//
// AsyncImage(url: attributionLogo) { image in
// image
// .resizable()
// .scaledToFit()
// } placeholder: {
// ProgressView()
// .controlSize(.mini)
// }
// .frame(height: 10)
//
// Link("Other data sources", destination: attributionLink ?? URL(string: "https://weather-data.apple.com/legal-attribution.html")!)
// .font(.caption2)
// }
// .padding(5)
//
// }
// .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
// .padding(5)
// .task {
// do {
// if node.hasPositions {
// let mostRecent = node.positions?.lastObject as? PositionEntity
// let weather = try await WeatherService.shared.weather(for: mostRecent?.nodeLocation ?? CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude))
// condition = weather.currentWeather.condition
// temperature = weather.currentWeather.temperature
// humidity = Int(weather.currentWeather.humidity * 100)
// symbolName = weather.currentWeather.symbolName
// let attribution = try await WeatherService.shared.attribution
// attributionLink = attribution.legalPageURL
// attributionLogo = colorScheme == .light ? attribution.combinedMarkLightURL : attribution.combinedMarkDarkURL
// }
// } catch {
// Logger.services.error("Could not gather weather information: \(error.localizedDescription)")
// condition = .clear
// symbolName = "cloud.fill"
// }
// }
// }
// }
// }
// .ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
// .frame(idealWidth: bounds.size.width, minHeight: bounds.size.height / 1.65)
// }
// } else {
// HStack {
// }
// .padding([.top], 20)
// }
// }
// .edgesIgnoringSafeArea([.leading, .trailing])
// .sheet(item: $waypointCoordinate, content: { wpc in
// WaypointFormMapKit(coordinate: wpc)
// .presentationDetents([.medium, .large])
// .presentationDragIndicator(.automatic)
// })
// .navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline)
// .navigationBarItems(trailing:
// ZStack {
// ConnectedDevice(
// bluetoothOn: bleManager.isSwitchedOn,
// deviceConnected: bleManager.connectedPeripheral != nil,
// name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
// })
// }
// .padding(.bottom, 2)
// }
// }
//}

View file

@ -51,7 +51,7 @@ struct WaypointFormMapKit: View {
axis: .vertical
)
.foregroundColor(Color.gray)
.onChange(of: name, perform: { _ in
.onChange(of: name) {
var totalBytes = name.utf8.count
// Only mess with the value if it is too big
while totalBytes > 30 {
@ -61,7 +61,7 @@ struct WaypointFormMapKit: View {
if totalBytes > 30 {
name = String(name.dropLast())
}
})
}
}
HStack {
Text("Description")
@ -72,14 +72,14 @@ struct WaypointFormMapKit: View {
axis: .vertical
)
.foregroundColor(Color.gray)
.onChange(of: description, perform: { _ in
.onChange(of: description) {
var totalBytes = description.utf8.count
// Only mess with the value if it is too big
while totalBytes > 100 {
description = String(description.dropLast())
totalBytes = description.utf8.count
}
})
}
}
HStack {
Text("Icon")
@ -87,7 +87,7 @@ struct WaypointFormMapKit: View {
EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji")
.font(.title)
.focused($iconIsFocused)
.onChange(of: icon) { value in
.onChange(of: icon) { _, value in
// If you have anything other than emojis in your string make it empty
if !value.onlyEmojis() {

View file

@ -134,11 +134,11 @@ struct ChannelMessageList: View {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
}
}
.onChange(of: channel.allPrivateMessages, perform: { _ in
.onChange(of: channel.allPrivateMessages) {
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
}
})
}
}
TextMessageField(

View file

@ -47,23 +47,14 @@ struct MessageText: View {
let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue)
if tapBackDestination.overlaySensorMessage {
VStack {
if #available(iOS 17.0, macOS 14.0, *) {
isDetectionSensorMessage ? Image(systemName: "sensor.fill")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
.foregroundStyle(Color.orange)
.symbolRenderingMode(.multicolor)
.symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3))
.offset(x: 20, y: -20)
: nil
} else {
isDetectionSensorMessage ? Image(systemName: "sensor.fill")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
.foregroundStyle(Color.orange)
.offset(x: 20, y: -20)
: nil
}
isDetectionSensorMessage ? Image(systemName: "sensor.fill")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
.foregroundStyle(Color.orange)
.symbolRenderingMode(.multicolor)
.symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3))
.offset(x: 20, y: -20)
: nil
}
} else {
EmptyView()

View file

@ -8,9 +8,7 @@
import SwiftUI
import CoreData
import OSLog
#if canImport(TipKit)
import TipKit
#endif
struct Messages: View {
@ -65,9 +63,7 @@ struct Messages: View {
}
}
if #available(iOS 17.0, macOS 14.0, *) {
TipView(MessagesTip(), arrowEdge: .top)
}
TipView(MessagesTip(), arrowEdge: .top)
}
.navigationTitle("messages")
.navigationBarTitleDisplayMode(.large)
@ -91,7 +87,7 @@ struct Messages: View {
} else if case .directMessages = router.navigationState.messages {
Text("Select a conversation")
}
}.onChange(of: router.navigationState) { _ in
}.onChange(of: router.navigationState) {
setupNavigationState()
}
}

View file

@ -30,14 +30,14 @@ struct TextMessageField: View {
HStack(alignment: .top) {
ZStack {
TextField("message", text: $typingMessage, axis: .vertical)
.onChange(of: typingMessage, perform: { value in
.onChange(of: typingMessage) { _, value in
totalBytes = value.utf8.count
// Only mess with the value if it is too big
while totalBytes > Self.maxbytes {
typingMessage = String(typingMessage.dropLast())
totalBytes = typingMessage.utf8.count
}
})
}
.keyboardType(.default)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {

View file

@ -8,9 +8,7 @@
import SwiftUI
import CoreData
import OSLog
#if canImport(TipKit)
import TipKit
#endif
struct UserList: View {
@ -201,12 +199,12 @@ struct UserList: View {
.sheet(isPresented: $showingHelp) {
DirectMessagesHelp()
}
.onChange(of: searchText) { _ in
.onChange(of: searchText) {
Task {
await searchUserList()
}
}
.onChange(of: viaLora) { _ in
.onChange(of: viaLora) {
if !viaLora && !viaMqtt {
viaMqtt = true
}
@ -214,7 +212,7 @@ struct UserList: View {
await searchUserList()
}
}
.onChange(of: viaMqtt) { _ in
.onChange(of: viaMqtt) {
if !viaLora && !viaMqtt {
viaLora = true
}
@ -222,27 +220,27 @@ struct UserList: View {
await searchUserList()
}
}
.onChange(of: [deviceRoles]) { _ in
.onChange(of: [deviceRoles]) {
Task {
await searchUserList()
}
}
.onChange(of: hopsAway) { _ in
.onChange(of: hopsAway) {
Task {
await searchUserList()
}
}
.onChange(of: [boolFilters]) { _ in
.onChange(of: [boolFilters]) {
Task {
await searchUserList()
}
}
.onChange(of: maxDistance) { _ in
.onChange(of: maxDistance) {
Task {
await searchUserList()
}
}
.onChange(of: isPkiEncrypted) { _ in
.onChange(of: isPkiEncrypted) {
Task {
await searchUserList()
}

View file

@ -122,11 +122,11 @@ struct UserMessageList: View {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
}
}
.onChange(of: user.messageList, perform: { _ in
.onChange(of: user.messageList) {
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
}
})
}
}
TextMessageField(

View file

@ -36,32 +36,31 @@ struct DeviceMetricsLog: View {
.sorted { $0.time! < $1.time! }
if chartData.count > 0 {
GroupBox(label: Label("\(deviceMetrics.count) Readings Total", systemImage: "chart.xyaxis.line")) {
if #available(iOS 17.0, macOS 14.0, *) {
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(.linear)
Plot {
PointMark(
x: .value("x", point.time!),
y: .value("y", point.channelUtilization)
)
.symbolSize(25)
}
.accessibilityLabel("Line Series")
.accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)")
.foregroundStyle(channelUtilizationChartColor)
if let chartSelection {
RuleMark(x: .value("Second", chartSelection, unit: .second))
.foregroundStyle(.tertiary.opacity(0.5))
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(.linear)
Plot {
PointMark(
x: .value("x", point.time!),
y: .value("y", point.channelUtilization)
)
.symbolSize(25)
}
.accessibilityLabel("Line Series")
.accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)")
.foregroundStyle(channelUtilizationChartColor)
if let chartSelection {
RuleMark(x: .value("Second", chartSelection, unit: .second))
.foregroundStyle(.tertiary.opacity(0.5))
// .annotation(
// position: .automatic,
// overflowResolution: .init(x: .fit, y: .disabled)
@ -75,91 +74,37 @@ struct DeviceMetricsLog: View {
// .foregroundStyle(Color.accentColor.opacity(0.2))
// }
// }
}
RuleMark(y: .value("Network Status Orange", 25))
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10]))
.foregroundStyle(.orange)
RuleMark(y: .value("Network Status Red", 50))
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10]))
.foregroundStyle(.red)
Plot {
PointMark(
x: .value("x", point.time!),
y: .value("y", point.airUtilTx)
)
.symbolSize(25)
}
.accessibilityLabel("Line Series")
.accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)")
.foregroundStyle(airtimeChartColor)
}
}
.chartXAxis(content: {
AxisMarks(position: .top)
})
.chartXAxis(.automatic)
.chartXSelection(value: $chartSelection)
.chartYScale(domain: 0...100)
.chartForegroundStyleScale([
idiom == .phone ? "Battery" : "Battery Level": batteryChartColor,
"Channel Utilization": channelUtilizationChartColor,
"Airtime": airtimeChartColor
])
.chartLegend(position: .automatic, alignment: .bottom)
} else {
// Fallback on earlier versions
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(.linear)
Plot {
PointMark(
x: .value("x", point.time!),
y: .value("y", point.channelUtilization)
)
.symbolSize(25)
}
.accessibilityLabel("Line Series")
.accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)")
.foregroundStyle(channelUtilizationChartColor)
RuleMark(y: .value("Network Status Orange", 25))
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10]))
.foregroundStyle(.orange)
RuleMark(y: .value("Network Status Red", 50))
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10]))
.foregroundStyle(.red)
Plot {
PointMark(
x: .value("x", point.time!),
y: .value("y", point.airUtilTx)
)
.symbolSize(25)
}
.accessibilityLabel("Line Series")
.accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)")
.foregroundStyle(airtimeChartColor)
RuleMark(y: .value("Network Status Orange", 25))
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10]))
.foregroundStyle(.orange)
RuleMark(y: .value("Network Status Red", 50))
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10]))
.foregroundStyle(.red)
Plot {
PointMark(
x: .value("x", point.time!),
y: .value("y", point.airUtilTx)
)
.symbolSize(25)
}
.accessibilityLabel("Line Series")
.accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)")
.foregroundStyle(airtimeChartColor)
}
.chartXAxis(content: {
AxisMarks(position: .top)
})
.chartXAxis(.automatic)
.chartYScale(domain: 0...100)
.chartForegroundStyleScale([
idiom == .phone ? "Battery" : "Battery Level": batteryChartColor,
"Channel Utilization": channelUtilizationChartColor,
"Airtime": airtimeChartColor
])
.chartLegend(position: .automatic, alignment: .bottom)
}
.chartXAxis(content: {
AxisMarks(position: .top)
})
.chartXAxis(.automatic)
.chartXSelection(value: $chartSelection)
.chartYScale(domain: 0...100)
.chartForegroundStyleScale([
idiom == .phone ? "Battery" : "Battery Level": batteryChartColor,
"Channel Utilization": channelUtilizationChartColor,
"Airtime": airtimeChartColor
])
.chartLegend(position: .automatic, alignment: .bottom)
}
.frame(minHeight: 240)
}
@ -262,18 +207,14 @@ struct DeviceMetricsLog: View {
.padding(.bottom)
.padding(.trailing)
}
.onChange(of: selection) { newSelection in
.onChange(of: selection) { _, newSelection in
guard let metrics = deviceMetrics.first(where: { $0.id == newSelection }) else {
return
}
chartSelection = metrics.time
}
} else {
if #available (iOS 17, *) {
ContentUnavailableView("No Device Metrics", systemImage: "slash.circle")
} else {
Text("No Device Metrics")
}
ContentUnavailableView("No Device Metrics", systemImage: "slash.circle")
}
}
.navigationTitle("device.metrics.log")

View file

@ -193,11 +193,7 @@ struct EnvironmentMetricsLog: View {
}
} else {
if #available (iOS 17, *) {
ContentUnavailableView("No Environment Metrics", systemImage: "slash.circle")
} else {
Text("No Environment Metrics")
}
ContentUnavailableView("No Environment Metrics", systemImage: "slash.circle")
}
}

View file

@ -8,7 +8,6 @@
import SwiftUI
import MapKit
@available(iOS 17.0, macOS 14.0, *)
struct MeshMapContent: MapContent {
/// Parameters

View file

@ -8,7 +8,6 @@ import SwiftUI
import MapKit
import CoreData
@available(iOS 17.0, macOS 14.0, *)
struct NodeMapContent: MapContent {
@ObservedObject var node: NodeInfoEntity

View file

@ -6,11 +6,8 @@
//
import SwiftUI
#if canImport(MapKit)
import MapKit
#endif
@available(iOS 17.0, macOS 14.0, *)
struct MapSettingsForm: View {
@Environment(\.dismiss) private var dismiss
@State private var currentDetent = PresentationDetent.medium
@ -39,7 +36,7 @@ struct MapSettingsForm: View {
.pickerStyle(SegmentedPickerStyle())
.padding(.top, 5)
.padding(.bottom, 5)
.onChange(of: mapLayer) { newMapLayer in
.onChange(of: mapLayer) { _, newMapLayer in
UserDefaults.mapLayer = newMapLayer
}
if meshMap {
@ -53,7 +50,7 @@ struct MapSettingsForm: View {
}
.pickerStyle(DefaultPickerStyle())
}
.onChange(of: meshMapDistance) { newMeshMapDistance in
.onChange(of: meshMapDistance) { _, newMeshMapDistance in
UserDefaults.meshMapDistance = newMeshMapDistance
}
Toggle(isOn: $waypoints) {

View file

@ -7,11 +7,8 @@
import SwiftUI
import CoreLocation
#if canImport(MapKit)
import MapKit
#endif
@available(iOS 17.0, macOS 14.0, *)
struct NodeMapSwiftUI: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@ -83,7 +80,7 @@ struct NodeMapSwiftUI: View {
}
.sheet(isPresented: $isEditingSettings) {
MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap)
.onChange(of: (selectedMapLayer)) { newMapLayer in
.onChange(of: (selectedMapLayer)) { _, newMapLayer in
switch selectedMapLayer {
case .standard:
UserDefaults.mapLayer = newMapLayer

View file

@ -7,16 +7,13 @@
import SwiftUI
import Charts
#if canImport(MapKit)
import MapKit
#endif
struct PositionAltitude {
let time: Date
var altitude: Measurement<UnitLength>
}
@available(iOS 17.0, macOS 14.0, *)
struct PositionAltitudeChart: View {
@Environment(\.dismiss) private var dismiss
@ObservedObject var node: NodeInfoEntity

View file

@ -8,7 +8,6 @@
import SwiftUI
import MapKit
@available(iOS 17.0, macOS 14.0, *)
struct PositionPopover: View {
@ObservedObject var locationsHandler = LocationsHandler.shared
@ -96,7 +95,6 @@ struct PositionPopover: View {
}
/// Altitude
Label {
let formatter = MeasurementFormatter()
let distanceInMeters = Measurement(value: Double(position.altitude), unit: UnitLength.meters)
let distanceInFeet = distanceInMeters.converted(to: UnitLength.feet)
if Locale.current.measurementSystem == .metric {
@ -215,20 +213,12 @@ struct PositionPopover: View {
.padding(.bottom)
}
if position.nodePosition?.hasDetectionSensorMetrics ?? false {
if #available(iOS 17.0, macOS 14.0, *) {
Image(systemName: "sensor.fill")
.symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3))
.symbolRenderingMode(.hierarchical)
.foregroundColor(.accentColor)
.font(.largeTitle)
.padding(.bottom)
} else {
Image(systemName: "sensor.fill")
.symbolRenderingMode(.hierarchical)
.foregroundColor(.accentColor)
.font(.largeTitle)
.padding(.bottom)
}
Image(systemName: "sensor.fill")
.symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3))
.symbolRenderingMode(.hierarchical)
.foregroundColor(.accentColor)
.font(.largeTitle)
.padding(.bottom)
}
BatteryGauge(node: position.nodePosition!)
}

View file

@ -65,14 +65,14 @@ struct WaypointForm: View {
axis: .vertical
)
.foregroundColor(Color.gray)
.onChange(of: name, perform: { _ in
.onChange(of: name) {
var totalBytes = name.utf8.count
// Only mess with the value if it is too big
while totalBytes > 30 {
name = String(name.dropLast())
totalBytes = name.utf8.count
}
})
}
}
HStack {
Text("Description")
@ -83,14 +83,14 @@ struct WaypointForm: View {
axis: .vertical
)
.foregroundColor(Color.gray)
.onChange(of: description, perform: { _ in
.onChange(of: description) {
var totalBytes = description.utf8.count
// Only mess with the value if it is too big
while totalBytes > 100 {
description = String(description.dropLast())
totalBytes = description.utf8.count
}
})
}
}
HStack {
Text("Icon")
@ -98,7 +98,7 @@ struct WaypointForm: View {
EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji")
.font(.title)
.focused($iconIsFocused)
.onChange(of: icon) { value in
.onChange(of: icon) { _, value in
// If you have anything other than emojis in your string make it empty
if !value.onlyEmojis() {

View file

@ -221,11 +221,7 @@ struct NodeDetail: View {
.disabled(!node.hasDeviceMetrics)
NavigationLink {
if #available (iOS 17, macOS 14, *) {
NodeMapSwiftUI(node: node, showUserLocation: connectedNode?.num ?? 0 == node.num)
} else {
NodeMapMapkit(node: node)
}
NodeMapSwiftUI(node: node, showUserLocation: connectedNode?.num ?? 0 == node.num)
} label: {
Label {
Text("Node Map")
@ -260,19 +256,17 @@ struct NodeDetail: View {
}
.disabled(!node.hasEnvironmentMetrics)
if #available(iOS 17.0, macOS 14.0, *) {
NavigationLink {
TraceRouteLog(node: node)
} label: {
Label {
Text("Trace Route Log")
} icon: {
Image(systemName: "signpost.right.and.left")
.symbolRenderingMode(.multicolor)
}
NavigationLink {
TraceRouteLog(node: node)
} label: {
Label {
Text("Trace Route Log")
} icon: {
Image(systemName: "signpost.right.and.left")
.symbolRenderingMode(.multicolor)
}
.disabled(node.traceRoutes?.count ?? 0 == 0)
}
.disabled(node.traceRoutes?.count ?? 0 == 0)
NavigationLink {
DetectionSensorLog(node: node)

View file

@ -101,36 +101,9 @@ struct NodeListItem: View {
if node.positions?.count ?? 0 > 0 && connectedNode != node.num {
HStack {
if let lastPostion = node.positions?.lastObject as? PositionEntity {
if #available(iOS 17.0, macOS 14.0, *) {
if let currentLocation = LocationsHandler.shared.locationsArray.last {
let myCoord = CLLocation(latitude: currentLocation.coordinate.latitude, longitude: currentLocation.coordinate.longitude)
if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationsHandler.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationsHandler.DefaultLocation.latitude {
let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
let metersAway = nodeCoord.distance(from: myCoord)
Image(systemName: "lines.measurement.horizontal")
.font(.callout)
.symbolRenderingMode(.multicolor)
.frame(width: 30)
DistanceText(meters: metersAway)
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
.foregroundColor(.gray)
let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord)
let headingDegrees = Angle.degrees(trueBearing)
Image(systemName: "location.north")
.font(.callout)
.symbolRenderingMode(.multicolor)
.clipShape(Circle())
.rotationEffect(headingDegrees)
let heading = Measurement(value: trueBearing, unit: UnitAngle.degrees)
Text("\(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))")
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
.foregroundColor(.gray)
}
}
} else {
let myCoord = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)
if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude {
if let currentLocation = LocationsHandler.shared.locationsArray.last {
let myCoord = CLLocation(latitude: currentLocation.coordinate.latitude, longitude: currentLocation.coordinate.longitude)
if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationsHandler.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationsHandler.DefaultLocation.latitude {
let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
let metersAway = nodeCoord.distance(from: myCoord)
Image(systemName: "lines.measurement.horizontal")
@ -139,7 +112,7 @@ struct NodeListItem: View {
.frame(width: 30)
DistanceText(meters: metersAway)
.font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption)
.foregroundColor(.secondary)
.foregroundColor(.gray)
let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord)
let headingDegrees = Angle.degrees(trueBearing)
Image(systemName: "location.north")
@ -211,13 +184,11 @@ struct NodeListItem: View {
.font(.callout)
.frame(width: 30)
}
if #available(iOS 17.0, macOS 14.0, *) {
if node.hasTraceRoutes {
Image(systemName: "signpost.right.and.left")
.symbolRenderingMode(.hierarchical)
.font(.callout)
.frame(width: 30)
}
if node.hasTraceRoutes {
Image(systemName: "signpost.right.and.left")
.symbolRenderingMode(.hierarchical)
.font(.callout)
.frame(width: 30)
}
}
}

View file

@ -10,11 +10,8 @@ import CoreData
import CoreLocation
import Foundation
import OSLog
#if canImport(MapKit)
import MapKit
#endif
@available(iOS 17.0, macOS 14.0, *)
struct MeshMap: View {
@Environment(\.managedObjectContext) var context
@ -142,7 +139,7 @@ struct MeshMap: View {
guard case .map = router.navigationState.selectedTab else { return }
// TODO: handle deep link for waypoints
}
.onChange(of: selectedMapLayer) { newMapLayer in
.onChange(of: selectedMapLayer) { _, newMapLayer in
switch selectedMapLayer {
case .standard:
UserDefaults.mapLayer = newMapLayer
@ -186,17 +183,6 @@ struct MeshMap: View {
.tint(Color(UIColor.secondarySystemBackground))
.foregroundColor(.accentColor)
.buttonStyle(.borderedProminent)
// Button(action: {
// withAnimation {
// editingFilters = !editingFilters
// }
// }) {
// Image(systemName: !editingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill")
// .padding(.vertical, 5)
// }
// .tint(Color(UIColor.secondarySystemBackground))
// .foregroundColor(.accentColor)
// .buttonStyle(.borderedProminent)
}
.controlSize(.regular)
.padding(5)

View file

@ -274,26 +274,18 @@ struct NodeList: View {
)
}
} else {
if #available (iOS 17, *) {
ContentUnavailableView("select.node", systemImage: "flipphone")
} else {
Text("select.node")
}
ContentUnavailableView("select.node", systemImage: "flipphone")
}
} detail: {
if #available (iOS 17, *) {
ContentUnavailableView("", systemImage: "line.3.horizontal")
} else {
Text("Select something to view")
}
ContentUnavailableView("", systemImage: "line.3.horizontal")
}
.navigationSplitViewStyle(.balanced)
.onChange(of: searchText) { _ in
.onChange(of: searchText) {
Task {
await searchNodeList()
}
}
.onChange(of: viaLora) { _ in
.onChange(of: viaLora) {
if !viaLora && !viaMqtt {
viaMqtt = true
}
@ -301,7 +293,7 @@ struct NodeList: View {
await searchNodeList()
}
}
.onChange(of: viaMqtt) { _ in
.onChange(of: viaMqtt) {
if !viaLora && !viaMqtt {
viaLora = true
}
@ -309,32 +301,32 @@ struct NodeList: View {
await searchNodeList()
}
}
.onChange(of: [boolFilters]) { _ in
.onChange(of: [boolFilters]) {
Task {
await searchNodeList()
}
}
.onChange(of: [deviceRoles]) { _ in
.onChange(of: [deviceRoles]) {
Task {
await searchNodeList()
}
}
.onChange(of: hopsAway) { _ in
.onChange(of: hopsAway) {
Task {
await searchNodeList()
}
}
.onChange(of: maxDistance) { _ in
.onChange(of: maxDistance) {
Task {
await searchNodeList()
}
}
.onChange(of: distanceFilter) { _ in
.onChange(of: distanceFilter) {
Task {
await searchNodeList()
}
}
.onChange(of: router.navigationState) { _ in
.onChange(of: router.navigationState) {
if let selected = router.navigationState.nodeListSelectedNodeNum {
self.selectedNode = getNodeInfo(id: selected, context: context)
} else {

View file

@ -26,8 +26,6 @@ struct NodeMap: View {
@State var selectedOverlayServer: MapOverlayServer = UserDefaults.mapOverlayServer
@State var mapTilesAboveLabels: Bool = UserDefaults.mapTilesAboveLabels
let fromDate: NSDate = Calendar.current.date(byAdding: .month, value: -1, to: Date())! as NSDate
// @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)],
// predicate: NSPredicate(format: "time >= %@ && nodePosition != nil", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate), animation: .none)
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)],
predicate: NSPredicate(format: "nodePosition != nil", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate), animation: .none)
private var positions: FetchedResults<PositionEntity>
@ -97,7 +95,7 @@ struct NodeMap: View {
}
}
.pickerStyle(SegmentedPickerStyle())
.onChange(of: (selectedMapLayer)) { newMapLayer in
.onChange(of: selectedMapLayer) { _, newMapLayer in
UserDefaults.mapLayer = newMapLayer
}
.padding(.top, 5)
@ -144,10 +142,10 @@ struct NodeMap: View {
.font(.footnote)
}
}
.pickerStyle(DefaultPickerStyle())
.onChange(of: (selectedOverlayServer)) { newSelectedOverlayServer in
UserDefaults.mapOverlayServer = newSelectedOverlayServer
}
.pickerStyle(DefaultPickerStyle())
.onChange(of: (selectedOverlayServer)) { _, newSelectedOverlayServer in
UserDefaults.mapOverlayServer = newSelectedOverlayServer
}
Text(LocalizedStringKey(selectedOverlayServer.attribution))
.font(.footnote)
.foregroundColor(.gray)
@ -160,7 +158,7 @@ struct NodeMap: View {
Text("Enable Offline Maps")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.onChange(of: enableOfflineMaps) { newEnableOfflineMaps in
.onChange(of: enableOfflineMaps) { _, newEnableOfflineMaps in
UserDefaults.enableOfflineMaps = newEnableOfflineMaps
if !enableOfflineMaps {
if self.selectedMapLayer == .offline {
@ -176,10 +174,10 @@ struct NodeMap: View {
Text(tsl.description)
}
}
.pickerStyle(DefaultPickerStyle())
.onChange(of: (selectedTileServer)) { newSelectedTileServer in
UserDefaults.mapTileServer = newSelectedTileServer
}
.pickerStyle(DefaultPickerStyle())
.onChange(of: (selectedTileServer)) { _, newSelectedTileServer in
UserDefaults.mapTileServer = newSelectedTileServer
}
Text("Attribution:")
.fontWeight(.semibold)
.font(.footnote)

View file

@ -196,11 +196,7 @@ struct PaxCounterLog: View {
.padding(.trailing)
}
} else {
if #available (iOS 17, *) {
ContentUnavailableView("paxcounter.content.unavailable", systemImage: "slash.circle")
} else {
Text("paxcounter.content.unavailable")
}
ContentUnavailableView("paxcounter.content.unavailable", systemImage: "slash.circle")
}
}
.navigationTitle("paxcounter.log")

View file

@ -166,11 +166,7 @@ struct PositionLog: View {
)
} else {
if #available (iOS 17, *) {
ContentUnavailableView("No Positions", systemImage: "mappin.slash")
} else {
Text("No Positions")
}
ContentUnavailableView("No Positions", systemImage: "mappin.slash")
}
}
.navigationTitle("Position Log \(node.positions?.count ?? 0) Points")

View file

@ -8,11 +8,8 @@
import SwiftUI
import CoreData
import OSLog
#if canImport(MapKit)
import MapKit
#endif
@available(iOS 17.0, macOS 14.0, *)
struct TraceRouteLog: View {
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@ObservedObject var locationsHandler = LocationsHandler.shared

View file

@ -22,9 +22,7 @@ struct AppData: View {
VStack {
Section(header: Text("phone.gps")) {
if #available(iOS 17.0, macOS 14.0, *) {
GPSStatus()
}
GPSStatus()
}
Divider()
Button(action: {
@ -69,8 +67,6 @@ struct AppData: View {
let container = NSPersistentContainer(name: "Meshtastic")
do {
try container.restorePersistentStore(from: file.absoluteURL)
let request = MyInfoEntity.fetchRequest()
try context.fetch(request)
UserDefaults.preferredPeripheralId = ""
UserDefaults.preferredPeripheralNum = Int(file.pathComponents[(idiom == .phone || idiom == .pad) ? 9 : 10]) ?? 0
Logger.data.notice("🗂️ Restored a core data backup to backup/\(UserDefaults.preferredPeripheralNum, privacy: .public)")

View file

@ -8,8 +8,6 @@
import SwiftUI
import OSLog
/// Needed for TableColumnForEach
@available(iOS 17.0, macOS 14.0, *)
struct AppLog: View {
@State private var logs: [OSLogEntryLog] = []
@ -133,25 +131,25 @@ struct AppLog: View {
logs.sort(using: sortOrder)
}
}
.onChange(of: searchText) { _ in
.onChange(of: searchText) {
Task {
await logs = searchAppLogs()
logs.sort(using: sortOrder)
}
}
.onChange(of: [categories]) { _ in
.onChange(of: [categories]) {
Task {
await logs = searchAppLogs()
logs.sort(using: sortOrder)
}
}
.onChange(of: [levels]) { _ in
.onChange(of: [levels]) {
Task {
await logs = searchAppLogs()
logs.sort(using: sortOrder)
}
}
.onChange(of: selection) { newSelection in
.onChange(of: selection) { _, newSelection in
presentingErrorDetails = true
let log = logs.first {
$0.id == newSelection
@ -216,7 +214,6 @@ struct AppLog: View {
}
}
@available(iOS 17.0, macOS 14.0, *)
extension AppLog {
@MainActor
private func searchAppLogs() async -> [OSLogEntryLog] {

View file

@ -10,9 +10,7 @@ import MapKit
import MeshtasticProtobufs
import OSLog
import SwiftUI
#if canImport(TipKit)
import TipKit
#endif
func generateChannelKey(size: Int) -> String {
var keyData = Data(count: size)
@ -62,9 +60,7 @@ struct Channels: View {
VStack {
List {
if #available(iOS 17.0, macOS 14.0, *) {
TipView(CreateChannelsTip(), arrowEdge: .bottom)
}
TipView(CreateChannelsTip(), arrowEdge: .bottom)
if node != nil && node?.myInfo != nil {
ForEach(node?.myInfo?.channels?.array as? [ChannelEntity] ?? [], id: \.self) { (channel: ChannelEntity) in
Button(action: {

View file

@ -6,9 +6,7 @@
//
import SwiftUI
#if canImport(MapKit)
import MapKit
#endif
struct ChannelForm: View {
@ -41,7 +39,7 @@ struct ChannelForm: View {
.disableAutocorrection(true)
.keyboardType(.alphabet)
.foregroundColor(Color.gray)
.onChange(of: channelName, perform: { _ in
.onChange(of: channelName) {
channelName = channelName.replacing(" ", with: "")
var totalBytes = channelName.utf8.count
// Only mess with the value if it is too big
@ -50,7 +48,7 @@ struct ChannelForm: View {
totalBytes = channelName.utf8.count
}
hasChanges = true
})
}
}
HStack {
Picker("Key Size", selection: $channelKeySize) {
@ -99,7 +97,7 @@ struct ChannelForm: View {
, lineWidth: 2.0)
)
.onChange(of: channelKey, perform: { _ in
.onChange(of: channelKey) {
let tempKey = Data(base64Encoded: channelKey) ?? Data()
if tempKey.count == channelKeySize || channelKeySize == -1 {
@ -108,7 +106,7 @@ struct ChannelForm: View {
hasValidKey = false
}
hasChanges = true
})
}
.disabled(channelKeySize <= 0)
}
HStack {
@ -148,7 +146,7 @@ struct ChannelForm: View {
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.disabled(!supportedVersion)
.listRowSeparator(.visible)
.onChange(of: preciseLocation) { pl in
.onChange(of: preciseLocation) { _, pl in
if pl == false {
positionPrecision = 14
}
@ -186,10 +184,10 @@ struct ChannelForm: View {
.listRowSeparator(.visible)
}
}
.onChange(of: channelName) { _ in
.onChange(of: channelName) {
hasChanges = true
}
.onChange(of: channelKeySize) { _ in
.onChange(of: channelKeySize) {
if channelKeySize == -1 {
channelKey = "AQ=="
} else {
@ -198,10 +196,10 @@ struct ChannelForm: View {
}
hasChanges = true
}
.onChange(of: channelKey) { _ in
.onChange(of: channelKey) {
hasChanges = true
}
.onChange(of: channelKeySize) { _ in
.onChange(of: channelKeySize) {
if channelKeySize == -1 {
if channelRole == 0 {
preciseLocation = false
@ -209,10 +207,10 @@ struct ChannelForm: View {
channelKey = "AQ=="
}
}
.onChange(of: channelRole) { _ in
.onChange(of: channelRole) {
hasChanges = true
}
.onChange(of: preciseLocation) { loc in
.onChange(of: preciseLocation) { _, loc in
if loc == true {
if channelKey == "AQ==" {
preciseLocation = false
@ -225,10 +223,10 @@ struct ChannelForm: View {
}
hasChanges = true
}
.onChange(of: positionPrecision) { _ in
.onChange(of: positionPrecision) {
hasChanges = true
}
.onChange(of: positionsEnabled) { pe in
.onChange(of: positionsEnabled) { _, pe in
if pe {
if positionPrecision == 0 {
positionPrecision = 14
@ -238,10 +236,10 @@ struct ChannelForm: View {
}
hasChanges = true
}
.onChange(of: uplink) { _ in
.onChange(of: uplink) {
hasChanges = true
}
.onChange(of: downlink) { _ in
.onChange(of: downlink) {
hasChanges = true
}
.onFirstAppear {

View file

@ -45,7 +45,7 @@ struct BluetoothConfig: View {
Label("bluetooth.mode.fixedpin", systemImage: "wallet.pass")
TextField("bluetooth.mode.fixedpin", text: $fixedPin)
.foregroundColor(.gray)
.onChange(of: fixedPin, perform: { _ in
.onChange(of: fixedPin) {
// Don't let the first character be 0 because it will get stripped when saving a UInt32
if fixedPin.first == "0" {
fixedPin = fixedPin.replacing("0", with: "")
@ -59,7 +59,7 @@ struct BluetoothConfig: View {
} else if fixedPin.utf8.count < pinLength {
shortPin = true
}
})
}
.foregroundColor(.gray)
}
.keyboardType(.decimalPad)
@ -121,14 +121,14 @@ struct BluetoothConfig: View {
}
}
}
.onChange(of: enabled) {
if $0 != node?.bluetoothConfig?.enabled { hasChanges = true }
.onChange(of: enabled) { oldEnabled, newEnabled in
if oldEnabled != newEnabled && newEnabled != node?.bluetoothConfig?.enabled { hasChanges = true }
}
.onChange(of: mode) {
if $0 != node?.bluetoothConfig?.mode ?? -1 { hasChanges = true }
.onChange(of: mode) { oldNode, newNode in
if oldNode != newNode && newNode != node?.bluetoothConfig?.mode ?? -1 { hasChanges = true }
}
.onChange(of: fixedPin) { newFixedPin in
if newFixedPin != String(node?.bluetoothConfig?.fixedPin ?? -1) { hasChanges = true }
.onChange(of: fixedPin) { oldFixedPin, newFixedPin in
if oldFixedPin != newFixedPin && newFixedPin != String(node?.bluetoothConfig?.fixedPin ?? -1) { hasChanges = true }
}
}
func setBluetoothValues() {

View file

@ -23,11 +23,11 @@ struct DeviceConfig: View {
@State var deviceRole = 0
@State var buzzerGPIO = 0
@State var buttonGPIO = 0
@State var serialEnabled = true
@State var rebroadcastMode = 0
@State var nodeInfoBroadcastSecs = 10800
@State var doubleTapAsButtonPress = false
@State var ledHeartbeatEnabled = true
@State var tripleClickAsAdHocPing = true
@State var tzdef = ""
var body: some View {
@ -77,6 +77,12 @@ struct DeviceConfig: View {
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $tripleClickAsAdHocPing) {
Label("Triple Click Ad Hoc Ping", systemImage: "map.pin")
Text("Send a position on the primary channel when the user button is triple clicked.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $ledHeartbeatEnabled) {
Label("LED Heartbeat", systemImage: "waveform.path.ecg")
Text("Controls the blinking LED on the device. For most devices this will control one of the up to 4 LEDS, the charger and GPS LEDs are not controllable.")
@ -84,23 +90,19 @@ struct DeviceConfig: View {
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
Section(header: Text("Debug")) {
Toggle(isOn: $serialEnabled) {
Label("Serial Console", systemImage: "terminal")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
VStack(alignment: .leading) {
HStack {
Label("Time Zone", systemImage: "clock.badge.exclamationmark")
TextField("Time Zone", text: $tzdef, axis: .vertical)
.foregroundColor(.gray)
.onChange(of: tzdef, perform: { _ in
.onChange(of: tzdef) {
var totalBytes = tzdef.utf8.count
// Only mess with the value if it is too big
while totalBytes > 63 {
tzdef = String(tzdef.dropLast())
totalBytes = tzdef.utf8.count
}
})
}
.foregroundColor(.gray)
}
.keyboardType(.default)
@ -194,12 +196,12 @@ struct DeviceConfig: View {
if connectedNode != nil {
var dc = Config.DeviceConfig()
dc.role = DeviceRoles(rawValue: deviceRole)!.protoEnumValue()
dc.serialEnabled = serialEnabled
dc.buttonGpio = UInt32(buttonGPIO)
dc.buzzerGpio = UInt32(buzzerGPIO)
dc.rebroadcastMode = RebroadcastModes(rawValue: rebroadcastMode)?.protoEnumValue() ?? RebroadcastModes.all.protoEnumValue()
dc.nodeInfoBroadcastSecs = UInt32(nodeInfoBroadcastSecs)
dc.doubleTapAsButtonPress = doubleTapAsButtonPress
dc.disableTripleClick = !tripleClickAsAdHocPing
dc.tzdef = tzdef
dc.ledHeartbeatDisabled = !ledHeartbeatEnabled
let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
@ -247,39 +249,36 @@ struct DeviceConfig: View {
}
}
}
.onChange(of: deviceRole) {
if $0 != node?.deviceConfig?.role ?? -1 { hasChanges = true }
.onChange(of: deviceRole) { oldRole, newRole in
if oldRole != newRole && newRole != node?.deviceConfig?.role ?? -1 { hasChanges = true }
}
.onChange(of: serialEnabled) {
if $0 != node?.deviceConfig?.serialEnabled { hasChanges = true }
.onChange(of: buttonGPIO) { oldButtonGPIO, newButtonGPIO in
if oldButtonGPIO != newButtonGPIO && newButtonGPIO != node?.deviceConfig?.buttonGpio ?? -1 { hasChanges = true }
}
.onChange(of: buttonGPIO) { newButtonGPIO in
if newButtonGPIO != node?.deviceConfig?.buttonGpio ?? -1 { hasChanges = true }
.onChange(of: buzzerGPIO) { oldBuzzerGPIO, newBuzzerGPIO in
if oldBuzzerGPIO != newBuzzerGPIO && newBuzzerGPIO != node?.deviceConfig?.buzzerGpio ?? -1 { hasChanges = true }
}
.onChange(of: buzzerGPIO) { newBuzzerGPIO in
if newBuzzerGPIO != node?.deviceConfig?.buttonGpio ?? -1 { hasChanges = true }
.onChange(of: rebroadcastMode) { oldRebroadcastMode, newRebroadcastMode in
if oldRebroadcastMode != newRebroadcastMode && newRebroadcastMode != node?.deviceConfig?.rebroadcastMode ?? -1 { hasChanges = true }
}
.onChange(of: rebroadcastMode) { newRebroadcastMode in
if newRebroadcastMode != node?.deviceConfig?.rebroadcastMode ?? -1 { hasChanges = true }
.onChange(of: nodeInfoBroadcastSecs) { oldNodeInfoBroadcastSecs, newNodeInfoBroadcastSecs in
if oldNodeInfoBroadcastSecs != newNodeInfoBroadcastSecs && newNodeInfoBroadcastSecs != node?.deviceConfig?.nodeInfoBroadcastSecs ?? -1 { hasChanges = true }
}
.onChange(of: nodeInfoBroadcastSecs) { newNodeInfoBroadcastSecs in
if newNodeInfoBroadcastSecs != node?.deviceConfig?.nodeInfoBroadcastSecs ?? -1 { hasChanges = true }
.onChange(of: doubleTapAsButtonPress) { oldDoubleTapAsButtonPress, newDoubleTapAsButtonPress in
if oldDoubleTapAsButtonPress != newDoubleTapAsButtonPress && newDoubleTapAsButtonPress != node?.deviceConfig?.doubleTapAsButtonPress ?? false { hasChanges = true }
}
.onChange(of: doubleTapAsButtonPress) {
if $0 != node?.deviceConfig?.doubleTapAsButtonPress { hasChanges = true }
.onChange(of: tripleClickAsAdHocPing) { oldTripleClickAsAdHocPing, newTripleClickAsAdHocPing in
if oldTripleClickAsAdHocPing != newTripleClickAsAdHocPing && newTripleClickAsAdHocPing != node?.deviceConfig?.tripleClickAsAdHocPing ?? false { hasChanges = true }
}
.onChange(of: tzdef) { newTzdef in
if newTzdef != node?.deviceConfig?.tzdef { hasChanges = true }
.onChange(of: tzdef) { oldTzdef, newTzdef in
if oldTzdef != newTzdef && newTzdef != node?.deviceConfig?.tzdef { hasChanges = true }
}
.onChange(of: ledHeartbeatEnabled) { newLedHeartbeatEnabled in
if node != nil && node?.deviceConfig != nil {
if newLedHeartbeatEnabled != node!.deviceConfig!.ledHeartbeatEnabled { hasChanges = true }
}
.onChange(of: ledHeartbeatEnabled) { oldLedHeartbeatEnabled, newLedHeartbeatEnabled in
if oldLedHeartbeatEnabled != newLedHeartbeatEnabled && newLedHeartbeatEnabled != node?.deviceConfig?.ledHeartbeatEnabled ?? false { hasChanges = true }
}
}
func setDeviceValues() {
self.deviceRole = Int(node?.deviceConfig?.role ?? 0)
self.serialEnabled = (node?.deviceConfig?.serialEnabled ?? true)
self.buttonGPIO = Int(node?.deviceConfig?.buttonGpio ?? 0)
self.buzzerGPIO = Int(node?.deviceConfig?.buzzerGpio ?? 0)
self.rebroadcastMode = Int(node?.deviceConfig?.rebroadcastMode ?? 0)
@ -288,6 +287,7 @@ struct DeviceConfig: View {
nodeInfoBroadcastSecs = 3600
}
self.doubleTapAsButtonPress = node?.deviceConfig?.doubleTapAsButtonPress ?? false
self.tripleClickAsAdHocPing = node?.deviceConfig?.tripleClickAsAdHocPing ?? false
self.ledHeartbeatEnabled = node?.deviceConfig?.ledHeartbeatEnabled ?? true
self.tzdef = node?.deviceConfig?.tzdef ?? ""
if self.tzdef.isEmpty {

View file

@ -184,32 +184,32 @@ struct DisplayConfig: View {
}
}
}
.onChange(of: screenOnSeconds) { newScreenSecs in
if newScreenSecs != node?.displayConfig?.screenOnSeconds ?? -1 { hasChanges = true }
.onChange(of: screenOnSeconds) { oldScreenSecs, newScreenSecs in
if oldScreenSecs != newScreenSecs && newScreenSecs != node?.displayConfig?.screenOnSeconds ?? -1 { hasChanges = true }
}
.onChange(of: screenCarouselInterval) { newCarouselSecs in
if newCarouselSecs != node?.displayConfig?.screenCarouselInterval ?? -1 { hasChanges = true }
.onChange(of: screenCarouselInterval) { oldCarouselSecs, newCarouselSecs in
if oldCarouselSecs != newCarouselSecs && newCarouselSecs != node?.displayConfig?.screenCarouselInterval ?? -1 { hasChanges = true }
}
.onChange(of: compassNorthTop) {
if $0 != node?.displayConfig?.compassNorthTop { hasChanges = true }
.onChange(of: compassNorthTop) { oldCompassNorthTop, newCompassNorthTop in
if oldCompassNorthTop != newCompassNorthTop && newCompassNorthTop != node?.displayConfig?.compassNorthTop { hasChanges = true }
}
.onChange(of: wakeOnTapOrMotion) {
if $0 != node?.displayConfig?.wakeOnTapOrMotion { hasChanges = true }
.onChange(of: wakeOnTapOrMotion) { oldWakeOnTapOrMotion, newWakeOnTapOrMotion in
if oldWakeOnTapOrMotion != newWakeOnTapOrMotion && newWakeOnTapOrMotion != node?.displayConfig?.wakeOnTapOrMotion { hasChanges = true }
}
.onChange(of: gpsFormat) { newGpsFormat in
if newGpsFormat != node?.displayConfig?.gpsFormat ?? -1 { hasChanges = true }
.onChange(of: gpsFormat) { oldGpsFormat, newGpsFormat in
if oldGpsFormat != newGpsFormat && newGpsFormat != node?.displayConfig?.gpsFormat ?? -1 { hasChanges = true }
}
.onChange(of: flipScreen) {
if $0 != node?.displayConfig?.flipScreen { hasChanges = true }
.onChange(of: flipScreen) { oldFlipScreen, newFlipScreen in
if oldFlipScreen != newFlipScreen && newFlipScreen != node?.displayConfig?.flipScreen { hasChanges = true }
}
.onChange(of: oledType) { newOledType in
if newOledType != node?.displayConfig?.oledType ?? -1 { hasChanges = true }
.onChange(of: oledType) { oldOledType, newOledType in
if oldOledType != newOledType && newOledType != node?.displayConfig?.oledType ?? -1 { hasChanges = true }
}
.onChange(of: displayMode) { newDisplayMode in
if newDisplayMode != node?.displayConfig?.displayMode ?? -1 { hasChanges = true }
.onChange(of: displayMode) { oldDisplayMode, newDisplayMode in
if oldDisplayMode != newDisplayMode && newDisplayMode != node?.displayConfig?.displayMode ?? -1 { hasChanges = true }
}
.onChange(of: units) { newUnits in
if newUnits != node?.displayConfig?.units ?? -1 { hasChanges = true }
.onChange(of: units) { oldUnits, newUnits in
if oldUnits != newUnits && newUnits != node?.displayConfig?.units ?? -1 { hasChanges = true }
}
}
func setDisplayValues() {

View file

@ -259,47 +259,47 @@ struct LoRaConfig: View {
}
}
}
.onChange(of: region) { newRegion in
.onChange(of: region) { _, newRegion in
if newRegion != node?.loRaConfig?.regionCode ?? -1 { hasChanges = true }
}
.onChange(of: usePreset) {
if $0 != node?.loRaConfig?.usePreset { hasChanges = true }
.onChange(of: usePreset) { _, newPreset in
if newPreset != node?.loRaConfig?.usePreset { hasChanges = true }
}
.onChange(of: modemPreset) { newModemPreset in
.onChange(of: modemPreset) { _, newModemPreset in
if newModemPreset != node?.loRaConfig?.modemPreset ?? -1 { hasChanges = true }
}
.onChange(of: hopLimit) { newHopLimit in
.onChange(of: hopLimit) { _, newHopLimit in
if newHopLimit != node?.loRaConfig?.hopLimit ?? -1 { hasChanges = true }
}
.onChange(of: channelNum) { newChannelNum in
.onChange(of: channelNum) { _, newChannelNum in
if newChannelNum != node?.loRaConfig?.channelNum ?? -1 { hasChanges = true }
}
.onChange(of: bandwidth) { newBandwidth in
.onChange(of: bandwidth) { _, newBandwidth in
if newBandwidth != node?.loRaConfig?.bandwidth ?? -1 { hasChanges = true }
}
.onChange(of: codingRate) { newCodingRate in
.onChange(of: codingRate) { _, newCodingRate in
if newCodingRate != node?.loRaConfig?.codingRate ?? -1 { hasChanges = true }
}
.onChange(of: spreadFactor) { newSpreadFactor in
.onChange(of: spreadFactor) { _, newSpreadFactor in
if newSpreadFactor != node?.loRaConfig?.spreadFactor ?? -1 { hasChanges = true }
}
.onChange(of: rxBoostedGain) {
if $0 != node?.loRaConfig?.sx126xRxBoostedGain { hasChanges = true }
.onChange(of: rxBoostedGain) { _, newRxBoostedGain in
if newRxBoostedGain != node?.loRaConfig?.sx126xRxBoostedGain { hasChanges = true }
}
.onChange(of: overrideFrequency) { newOverrideFrequency in
.onChange(of: overrideFrequency) { _, newOverrideFrequency in
if newOverrideFrequency != node?.loRaConfig?.overrideFrequency { hasChanges = true }
}
.onChange(of: txPower) { newTxPower in
.onChange(of: txPower) { _, newTxPower in
if newTxPower != node?.loRaConfig?.txPower ?? -1 { hasChanges = true }
}
.onChange(of: txEnabled) {
if $0 != node?.loRaConfig?.txEnabled { hasChanges = true }
.onChange(of: txEnabled) { _, newTxEnabled in
if newTxEnabled != node?.loRaConfig?.txEnabled { hasChanges = true }
}
.onChange(of: ignoreMqtt) {
if $0 != node?.loRaConfig?.ignoreMqtt { hasChanges = true }
.onChange(of: ignoreMqtt) { _, newIgnoreMqtt in
if newIgnoreMqtt != node?.loRaConfig?.ignoreMqtt { hasChanges = true }
}
.onChange(of: okToMqtt) {
if $0 != node?.loRaConfig?.okToMqtt { hasChanges = true }
.onChange(of: okToMqtt) { _, newOkToMqtt in
if newOkToMqtt != node?.loRaConfig?.okToMqtt { hasChanges = true }
}
}
func setLoRaValues() {

View file

@ -8,7 +8,6 @@ import MeshtasticProtobufs
import SwiftUI
import OSLog
@available(iOS 17.0, macOS 14.0, *)
struct AmbientLightingConfig: View {
@Environment(\.self) var environment
@Environment(\.managedObjectContext) var context
@ -107,20 +106,14 @@ struct AmbientLightingConfig: View {
}
}
}
.onChange(of: ledState) {
if let val = node?.ambientLightingConfig?.ledState {
hasChanges = $0 != val
}
.onChange(of: ledState) { _, newLedState in
if newLedState != node?.ambientLightingConfig?.ledState { hasChanges = true }
}
.onChange(of: current) {
if let val = node?.ambientLightingConfig?.current {
hasChanges = $0 != val
}
.onChange(of: current) { _, newCurrent in
if newCurrent != node?.ambientLightingConfig?.current ?? 10 { hasChanges = true }
}
.onChange(of: color) { c in
if color != c {
hasChanges = true
}
.onChange(of: color) { oldColor, newColor in
if oldColor != newColor { hasChanges = true }
}
}
}

View file

@ -71,8 +71,7 @@ struct CannedMessagesConfig: View {
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: messages, perform: { _ in
.onChange(of: messages) {
var totalBytes = messages.utf8.count
// Only mess with the value if it is too big
while totalBytes > 198 {
@ -80,7 +79,7 @@ struct CannedMessagesConfig: View {
totalBytes = messages.utf8.count
}
hasMessagesChanges = true
})
}
.foregroundColor(.gray)
}
.keyboardType(.default)
@ -255,7 +254,7 @@ struct CannedMessagesConfig: View {
}
}
}
.onChange(of: configPreset) { newPreset in
.onChange(of: configPreset) { _, newPreset in
if newPreset == 1 {
@ -284,55 +283,35 @@ struct CannedMessagesConfig: View {
hasChanges = true
}
.onChange(of: enabled) {
if let val = node?.cannedMessageConfig?.enabled {
hasChanges = $0 != val
}
.onChange(of: enabled) { _, newEnabled in
if newEnabled != node?.cannedMessageConfig?.enabled { hasChanges = true }
}
.onChange(of: sendBell) {
if let val = node?.cannedMessageConfig?.sendBell {
hasChanges = $0 != val
}
.onChange(of: sendBell) { _, newSendBell in
if newSendBell != node?.cannedMessageConfig?.sendBell { hasChanges = true }
}
.onChange(of: rotary1Enabled) {
if let val = node?.cannedMessageConfig?.rotary1Enabled {
hasChanges = $0 != val
}
.onChange(of: rotary1Enabled) { _, newRotary1Enabled in
if newRotary1Enabled != node?.cannedMessageConfig?.rotary1Enabled { hasChanges = true }
}
.onChange(of: updown1Enabled) {
if let val = node?.cannedMessageConfig?.updown1Enabled {
hasChanges = $0 != val
}
.onChange(of: updown1Enabled) { _, newUpdown1Enabled in
if newUpdown1Enabled != node?.cannedMessageConfig?.updown1Enabled { hasChanges = true }
}
.onChange(of: inputbrokerPinA) { newPinA in
if node != nil && node!.cannedMessageConfig != nil {
if newPinA != node!.cannedMessageConfig!.inputbrokerPinA { hasChanges = true }
}
.onChange(of: inputbrokerPinA) { _, newPinA in
if newPinA != node?.cannedMessageConfig?.inputbrokerPinA ?? -1 { hasChanges = true }
}
.onChange(of: inputbrokerPinB) { newPinB in
if node != nil && node!.cannedMessageConfig != nil {
if newPinB != node!.cannedMessageConfig!.inputbrokerPinB { hasChanges = true }
}
.onChange(of: inputbrokerPinB) { _, newPinB in
if newPinB != node?.cannedMessageConfig?.inputbrokerPinB ?? -1 { hasChanges = true }
}
.onChange(of: inputbrokerPinPress) { newPinPress in
if node != nil && node!.cannedMessageConfig != nil {
if newPinPress != node!.cannedMessageConfig!.inputbrokerPinPress { hasChanges = true }
}
.onChange(of: inputbrokerPinPress) { _, newPinPress in
if newPinPress != node?.cannedMessageConfig?.inputbrokerPinPress ?? -1 { hasChanges = true }
}
.onChange(of: inputbrokerEventCw) { newKeyA in
if node != nil && node!.cannedMessageConfig != nil {
if newKeyA != node!.cannedMessageConfig!.inputbrokerEventCw { hasChanges = true }
}
.onChange(of: inputbrokerEventCw) { _, newKeyA in
if newKeyA != node?.cannedMessageConfig?.inputbrokerEventCw ?? -1 { hasChanges = true }
}
.onChange(of: inputbrokerEventCcw) { newKeyB in
if node != nil && node!.cannedMessageConfig != nil {
if newKeyB != node!.cannedMessageConfig!.inputbrokerEventCcw { hasChanges = true }
}
.onChange(of: inputbrokerEventCcw) { _, newKeyB in
if newKeyB != node?.cannedMessageConfig?.inputbrokerEventCcw ?? -1 { hasChanges = true }
}
.onChange(of: inputbrokerEventPress) { newKeyPress in
if node != nil && node!.cannedMessageConfig != nil {
if newKeyPress != node!.cannedMessageConfig!.inputbrokerEventPress { hasChanges = true }
}
.onChange(of: inputbrokerEventPress) { _, newKeyPress in
if newKeyPress != node?.cannedMessageConfig?.inputbrokerEventPress ?? -1 { hasChanges = true }
}
}
}

View file

@ -91,14 +91,14 @@ struct DetectionSensorConfig: View {
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: name, perform: { _ in
.onChange(of: name) {
var totalBytes = name.utf8.count
// Only mess with the value if it is too big
while totalBytes > 20 {
name = String(name.dropLast())
totalBytes = name.utf8.count
}
})
}
}
.listRowSeparator(.hidden)
Text("Friendly name used to format message sent to mesh. Example: A name \"Motion\" would result in a message \"Motion detected\"")
@ -210,47 +210,31 @@ struct DetectionSensorConfig: View {
}
}
}
.onChange(of: enabled) {
if let val = node?.detectionSensorConfig?.enabled {
hasChanges = $0 != val
}
.onChange(of: enabled) { _, newEnabled in
if newEnabled != node?.detectionSensorConfig?.enabled { hasChanges = true }
}
.onChange(of: sendBell) {
if let val = node?.detectionSensorConfig?.sendBell {
hasChanges = $0 != val
}
.onChange(of: sendBell) { _, newSendBell in
if newSendBell != node?.detectionSensorConfig?.sendBell { hasChanges = true }
}
.onChange(of: detectionTriggeredHigh) { newDetectionTriggeredHigh in
if node != nil && node?.detectionSensorConfig != nil {
if newDetectionTriggeredHigh != node!.detectionSensorConfig!.detectionTriggeredHigh { hasChanges = true }
}
.onChange(of: detectionTriggeredHigh) { _, newDetectionTriggeredHigh in
if newDetectionTriggeredHigh != node?.detectionSensorConfig?.detectionTriggeredHigh { hasChanges = true }
}
.onChange(of: usePullup) {
if let val = node?.detectionSensorConfig?.usePullup {
hasChanges = $0 != val
}
.onChange(of: usePullup) { _, newUsePullup in
if newUsePullup != node?.detectionSensorConfig?.usePullup { hasChanges = true }
}
.onChange(of: name) { newName in
if node != nil && node?.detectionSensorConfig != nil {
if newName != node!.detectionSensorConfig!.name { hasChanges = true }
}
.onChange(of: name) { _, newName in
if newName != node?.detectionSensorConfig?.name ?? "" { hasChanges = true }
}
.onChange(of: monitorPin) { newMonitorPin in
if node != nil && node?.detectionSensorConfig != nil {
if newMonitorPin != node!.detectionSensorConfig!.monitorPin { hasChanges = true }
}
.onChange(of: monitorPin) { _, newMonitorPin in
if newMonitorPin != node?.detectionSensorConfig?.monitorPin ?? 0 { hasChanges = true }
}
.onChange(of: minimumBroadcastSecs) { newMinimumBroadcastSecs in
if node != nil && node?.detectionSensorConfig != nil {
if newMinimumBroadcastSecs != node!.detectionSensorConfig!.minimumBroadcastSecs { hasChanges = true }
}
.onChange(of: minimumBroadcastSecs) { _, newMinimumBroadcastSecs in
if newMinimumBroadcastSecs != node?.detectionSensorConfig?.minimumBroadcastSecs ?? 0 { hasChanges = true }
}
.onChange(of: stateBroadcastSecs) { newStateBroadcastSecs in
if node != nil && node?.detectionSensorConfig != nil {
if newStateBroadcastSecs != node!.detectionSensorConfig!.stateBroadcastSecs { hasChanges = true }
}
.onChange(of: stateBroadcastSecs) { _, newStateBroadcastSecs in
if newStateBroadcastSecs != node?.detectionSensorConfig?.stateBroadcastSecs ?? 0 { hasChanges = true }
}
.onChange(of: detectionNotificationsEnabled) { newDetectionNotificationsEnabled in
.onChange(of: detectionNotificationsEnabled) { _, newDetectionNotificationsEnabled in
UserDefaults.enableDetectionNotifications = newDetectionNotificationsEnabled
}
}

View file

@ -220,80 +220,50 @@ struct ExternalNotificationConfig: View {
}
}
}
.onChange(of: enabled) {
if let val = node?.externalNotificationConfig?.enabled {
hasChanges = $0 != val
}
.onChange(of: enabled) { _, newEnabled in
if newEnabled != node?.externalNotificationConfig?.enabled { hasChanges = true }
}
.onChange(of: alertBell) {
if let val = node?.externalNotificationConfig?.alertBell {
hasChanges = $0 != val
}
.onChange(of: alertBell) { _, newAlertBell in
if newAlertBell != node?.externalNotificationConfig?.alertBell { hasChanges = true }
}
.onChange(of: alertBellBuzzer) {
if let val = node?.externalNotificationConfig?.alertBellBuzzer {
hasChanges = $0 != val
}
.onChange(of: alertBellBuzzer) { _, newAlertBellBuzzer in
if newAlertBellBuzzer != node?.externalNotificationConfig?.alertBellBuzzer { hasChanges = true }
}
.onChange(of: alertBellVibra) {
if let val = node?.externalNotificationConfig?.alertBellVibra {
hasChanges = $0 != val
}
.onChange(of: alertBellVibra) { _, newAlertBellVibra in
if newAlertBellVibra != node?.externalNotificationConfig?.alertBellVibra { hasChanges = true }
}
.onChange(of: alertMessage) {
if let val = node?.externalNotificationConfig?.alertMessage {
hasChanges = $0 != val
}
.onChange(of: alertMessage) { _, newAlertMessage in
if newAlertMessage != node?.externalNotificationConfig?.alertMessage { hasChanges = true }
}
.onChange(of: alertMessageBuzzer) {
if let val = node?.externalNotificationConfig?.alertMessageBuzzer {
hasChanges = $0 != val
}
.onChange(of: alertMessageBuzzer) { _, newAlertMessageBuzzer in
if newAlertMessageBuzzer != node?.externalNotificationConfig?.alertMessageBuzzer { hasChanges = true }
}
.onChange(of: alertMessageVibra) {
if let val = node?.externalNotificationConfig?.alertMessageVibra {
hasChanges = $0 != val
}
.onChange(of: alertMessageVibra) { _, newAlertMessageVibra in
if newAlertMessageVibra != node?.externalNotificationConfig?.alertMessageVibra { hasChanges = true }
}
.onChange(of: active) {
if let val = node?.externalNotificationConfig?.active {
hasChanges = $0 != val
}
.onChange(of: active) { _, newActive in
if newActive != node?.externalNotificationConfig?.active { hasChanges = true }
}
.onChange(of: output) { newOutput in
if node != nil && node!.externalNotificationConfig != nil {
if newOutput != node!.externalNotificationConfig!.output { hasChanges = true }
}
.onChange(of: output) { _, newOutput in
if newOutput != node?.externalNotificationConfig?.output ?? -1 { hasChanges = true }
}
.onChange(of: output) { newOutputBuzzer in
if node != nil && node!.externalNotificationConfig != nil {
if newOutputBuzzer != node!.externalNotificationConfig!.outputBuzzer { hasChanges = true }
}
.onChange(of: output) { _, newOutputBuzzer in
if newOutputBuzzer != node?.externalNotificationConfig?.outputBuzzer ?? -1 { hasChanges = true }
}
.onChange(of: output) { newOutputVibra in
if node != nil && node!.externalNotificationConfig != nil {
if newOutputVibra != node!.externalNotificationConfig!.outputVibra { hasChanges = true }
}
.onChange(of: output) { _, newOutputVibra in
if newOutputVibra != node?.externalNotificationConfig?.outputVibra ?? -1 { hasChanges = true }
}
.onChange(of: outputMilliseconds) { newOutputMs in
if node != nil && node!.externalNotificationConfig != nil {
if newOutputMs != node!.externalNotificationConfig!.outputMilliseconds { hasChanges = true }
}
.onChange(of: outputMilliseconds) { _, newOutputMs in
if newOutputMs != node?.externalNotificationConfig?.outputMilliseconds ?? -1 { hasChanges = true }
}
.onChange(of: usePWM) {
if let val = node?.externalNotificationConfig?.usePWM {
hasChanges = $0 != val
}
.onChange(of: usePWM) { _, newPWM in
if newPWM != node?.externalNotificationConfig?.usePWM { hasChanges = true }
}
.onChange(of: nagTimeout) { newNagTimeout in
if node != nil && node!.externalNotificationConfig != nil {
if newNagTimeout != node!.externalNotificationConfig!.nagTimeout { hasChanges = true }
}
.onChange(of: nagTimeout) { _, newNagTimeout in
if newNagTimeout != node?.externalNotificationConfig?.nagTimeout ?? -1 { hasChanges = true }
}
.onChange(of: useI2SAsBuzzer) {
if let val = node?.externalNotificationConfig?.useI2SAsBuzzer {
hasChanges = $0 != val
}
.onChange(of: useI2SAsBuzzer) { _, newUseI2SAsBuzzer in
if newUseI2SAsBuzzer != node?.externalNotificationConfig?.useI2SAsBuzzer { hasChanges = true }
}
}
func setExternalNotificationValues() {

View file

@ -123,14 +123,14 @@ struct MQTTConfig: View {
Label("Root Topic", systemImage: "tree")
TextField("Root Topic", text: $root)
.foregroundColor(.gray)
.onChange(of: root, perform: { _ in
.onChange(of: root) {
var totalBytes = root.utf8.count
// Only mess with the value if it is too big
while totalBytes > 30 {
root = String(root.dropLast())
totalBytes = root.utf8.count
}
})
}
.foregroundColor(.gray)
}
.keyboardType(.asciiCapable)
@ -162,7 +162,7 @@ struct MQTTConfig: View {
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: address, perform: { _ in
.onChange(of: address) {
var totalBytes = address.utf8.count
// Only mess with the value if it is too big
while totalBytes > 62 {
@ -170,7 +170,7 @@ struct MQTTConfig: View {
totalBytes = address.utf8.count
}
hasChanges = true
})
}
.keyboardType(.default)
}
.autocorrectionDisabled()
@ -181,7 +181,7 @@ struct MQTTConfig: View {
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: username, perform: { _ in
.onChange(of: username) {
var totalBytes = username.utf8.count
// Only mess with the value if it is too big
while totalBytes > 62 {
@ -189,7 +189,7 @@ struct MQTTConfig: View {
totalBytes = username.utf8.count
}
hasChanges = true
})
}
.foregroundColor(.gray)
}
.keyboardType(.default)
@ -200,7 +200,7 @@ struct MQTTConfig: View {
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: password, perform: { _ in
.onChange(of: password) {
var totalBytes = password.utf8.count
// Only mess with the value if it is too big
while totalBytes > 62 {
@ -208,7 +208,7 @@ struct MQTTConfig: View {
totalBytes = password.utf8.count
}
hasChanges = true
})
}
.foregroundColor(.gray)
}
.keyboardType(.default)
@ -262,55 +262,47 @@ struct MQTTConfig: View {
)
}
)
.onChange(of: enabled) {
if $0 != node?.mqttConfig?.enabled { hasChanges = true }
.onChange(of: enabled) { _, newEnabled in
if newEnabled != node?.mqttConfig?.enabled { hasChanges = true }
}
.onChange(of: proxyToClientEnabled) { newProxyToClientEnabled in
.onChange(of: proxyToClientEnabled) { _, newProxyToClientEnabled in
if newProxyToClientEnabled {
jsonEnabled = false
}
if newProxyToClientEnabled != node?.mqttConfig?.proxyToClientEnabled { hasChanges = true }
}
.onChange(of: address) { newAddress in
if node != nil && node?.mqttConfig != nil {
if newAddress != node!.mqttConfig!.address { hasChanges = true }
}
.onChange(of: address) { _, newAddress in
if newAddress != node?.mqttConfig?.address ?? "" { hasChanges = true }
}
.onChange(of: username) { newUsername in
if node != nil && node?.mqttConfig != nil {
if newUsername != node!.mqttConfig!.username { hasChanges = true }
}
if newUsername != node?.mqttConfig?.username ?? "" { hasChanges = true }
}
.onChange(of: password) { newPassword in
if node != nil && node?.mqttConfig != nil {
if newPassword != node!.mqttConfig!.password { hasChanges = true }
}
if newPassword != node?.mqttConfig?.password ?? "" { hasChanges = true }
}
.onChange(of: root) { newRoot in
if node != nil && node?.mqttConfig != nil {
if newRoot != node!.mqttConfig!.root { hasChanges = true }
}
if newRoot != node?.mqttConfig?.root ?? "" { hasChanges = true }
}
.onChange(of: selectedTopic) { newSelectedTopic in
.onChange(of: selectedTopic) { _, newSelectedTopic in
root = newSelectedTopic
}
.onChange(of: encryptionEnabled) {
if $0 != node?.mqttConfig?.encryptionEnabled { hasChanges = true }
.onChange(of: encryptionEnabled) { _, newEncryptionEnabled in
if newEncryptionEnabled != node?.mqttConfig?.encryptionEnabled { hasChanges = true }
}
.onChange(of: jsonEnabled) { newJsonEnabled in
.onChange(of: jsonEnabled) { _, newJsonEnabled in
if newJsonEnabled {
proxyToClientEnabled = false
}
if newJsonEnabled != node?.mqttConfig?.jsonEnabled { hasChanges = true }
}
.onChange(of: tlsEnabled) { newTlsEnabled in
.onChange(of: tlsEnabled) { _, newTlsEnabled in
if address.lowercased() == "mqtt.meshtastic.org" {
tlsEnabled = false
} else {
if newTlsEnabled != node?.mqttConfig?.tlsEnabled { hasChanges = true }
}
}
.onChange(of: mqttConnected) { newMqttConnected in
.onChange(of: mqttConnected) { _, newMqttConnected in
if newMqttConnected == false {
if bleManager.mqttProxyConnected {
bleManager.mqttManager.disconnect()
@ -321,13 +313,11 @@ struct MQTTConfig: View {
}
}
}
.onChange(of: mapReportingEnabled) {
if $0 != node?.mqttConfig?.mapReportingEnabled { hasChanges = true }
.onChange(of: mapReportingEnabled) { _, newMapReportingEnabled in
if newMapReportingEnabled != node?.mqttConfig?.mapReportingEnabled { hasChanges = true }
}
.onChange(of: mapPublishIntervalSecs) { newMapPublishIntervalSecs in
if node != nil && node?.mqttConfig != nil {
if newMapPublishIntervalSecs != node!.mqttConfig!.mapPublishIntervalSecs { hasChanges = true }
}
.onChange(of: mapPublishIntervalSecs) { _, newMapPublishIntervalSecs in
if newMapPublishIntervalSecs != node?.mqttConfig?.mapPublishIntervalSecs ?? -1 { hasChanges = true }
}
.onFirstAppear {
// Need to request a MqttModuleConfig from the remote node before allowing changes
@ -353,52 +343,50 @@ struct MQTTConfig: View {
}
func setMqttValues() {
if #available(iOS 17.0, macOS 14.0, *) {
nearbyTopics = []
let geocoder = CLGeocoder()
if LocationsHandler.shared.locationsArray.count > 0 {
let region = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0))?.topic
defaultTopic = "msh/" + (region ?? "UNSET")
geocoder.reverseGeocodeLocation(LocationsHandler.shared.locationsArray.first!, completionHandler: {(placemarks, error) in
if let error {
Logger.services.error("Failed to reverse geocode location: \(error.localizedDescription)")
return
}
nearbyTopics = []
let geocoder = CLGeocoder()
if LocationsHandler.shared.locationsArray.count > 0 {
let region = RegionCodes(rawValue: Int(node?.loRaConfig?.regionCode ?? 0))?.topic
defaultTopic = "msh/" + (region ?? "UNSET")
geocoder.reverseGeocodeLocation(LocationsHandler.shared.locationsArray.first!, completionHandler: {(placemarks, error) in
if let error {
Logger.services.error("Failed to reverse geocode location: \(error.localizedDescription)")
return
if let placemarks = placemarks, let placemark = placemarks.first {
let cc = locale.region?.identifier ?? "UNK"
/// Country Topic unless you are US
if placemark.isoCountryCode ?? "unknown" != cc {
let countryTopic = defaultTopic + "/" + (placemark.isoCountryCode ?? "")
if !countryTopic.isEmpty {
nearbyTopics.append(countryTopic)
}
}
if let placemarks = placemarks, let placemark = placemarks.first {
let cc = locale.region?.identifier ?? "UNK"
/// Country Topic unless you are US
if placemark.isoCountryCode ?? "unknown" != cc {
let countryTopic = defaultTopic + "/" + (placemark.isoCountryCode ?? "")
if !countryTopic.isEmpty {
nearbyTopics.append(countryTopic)
}
}
let stateTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "")
if !stateTopic.isEmpty {
nearbyTopics.append(stateTopic)
}
let countyTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subAdministrativeArea?.lowercased().replacingOccurrences(of: " ", with: "") ?? "")
if !countyTopic.isEmpty {
nearbyTopics.append(countyTopic)
}
let cityTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.locality?.lowercased().replacingOccurrences(of: " ", with: "") ?? "")
if !cityTopic.isEmpty {
nearbyTopics.append(cityTopic)
}
let neightborhoodTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subLocality?.lowercased()
.replacingOccurrences(of: " ", with: "")
.replacingOccurrences(of: "'", with: "") ?? "")
if !neightborhoodTopic.isEmpty {
nearbyTopics.append(neightborhoodTopic)
}
} else {
Logger.services.debug("No Location")
let stateTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "")
if !stateTopic.isEmpty {
nearbyTopics.append(stateTopic)
}
})
}
let countyTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subAdministrativeArea?.lowercased().replacingOccurrences(of: " ", with: "") ?? "")
if !countyTopic.isEmpty {
nearbyTopics.append(countyTopic)
}
let cityTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.locality?.lowercased().replacingOccurrences(of: " ", with: "") ?? "")
if !cityTopic.isEmpty {
nearbyTopics.append(cityTopic)
}
let neightborhoodTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subLocality?.lowercased()
.replacingOccurrences(of: " ", with: "")
.replacingOccurrences(of: "'", with: "") ?? "")
if !neightborhoodTopic.isEmpty {
nearbyTopics.append(neightborhoodTopic)
}
} else {
Logger.services.debug("No Location")
}
})
}
self.enabled = node?.mqttConfig?.enabled ?? false
self.proxyToClientEnabled = node?.mqttConfig?.proxyToClientEnabled ?? false
self.address = node?.mqttConfig?.address ?? ""

View file

@ -79,11 +79,11 @@ struct PaxCounterConfig: View {
}
}
}
.onChange(of: enabled) {
if $0 != node?.paxCounterConfig?.enabled { hasChanges = true }
.onChange(of: enabled) { oldEnabled, newEnabled in
if oldEnabled != newEnabled && newEnabled != node?.paxCounterConfig?.enabled { hasChanges = true }
}
.onChange(of: paxcounterUpdateInterval) {
if $0 != node?.paxCounterConfig?.updateInterval ?? -1 { hasChanges = true }
.onChange(of: paxcounterUpdateInterval) { oldPaxcounterUpdateInterval, newPaxcounterUpdateInterval in
if oldPaxcounterUpdateInterval != newPaxcounterUpdateInterval && newPaxcounterUpdateInterval != node?.paxCounterConfig?.updateInterval ?? -1 { hasChanges = true }
}
SaveConfigButton(node: node, hasChanges: $hasChanges) {

View file

@ -102,14 +102,14 @@ struct RangeTestConfig: View {
}
}
}
.onChange(of: enabled) {
if $0 != node?.rangeTestConfig?.enabled { hasChanges = true }
.onChange(of: enabled) { _, newEnabled in
if newEnabled != node?.rangeTestConfig?.enabled { hasChanges = true }
}
.onChange(of: save) {
if $0 != node?.rangeTestConfig?.save { hasChanges = true }
.onChange(of: save) { _, newSave in
if newSave != node?.rangeTestConfig?.save { hasChanges = true }
}
.onChange(of: sender) {
if $0 != node?.rangeTestConfig?.sender ?? -1 { hasChanges = true }
.onChange(of: sender) { _, newSender in
if newSender != node?.rangeTestConfig?.sender ?? -1 { hasChanges = true }
}
}
}

View file

@ -31,14 +31,14 @@ struct RtttlConfig: View {
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: ringtone, perform: { _ in
.onChange(of: ringtone) {
var totalBytes = ringtone.utf8.count
// Only mess with the value if it is too big
while totalBytes > 228 {
ringtone = String(ringtone.dropLast())
totalBytes = ringtone.utf8.count
}
})
}
.foregroundColor(.gray)
}
.keyboardType(.default)
@ -93,7 +93,7 @@ struct RtttlConfig: View {
}
}
}
.onChange(of: ringtone) { newRingtone in
.onChange(of: ringtone) { _, newRingtone in
if node != nil && node!.rtttlConfig != nil {
if newRingtone != node!.rtttlConfig!.ringtone { hasChanges = true }
}

View file

@ -157,41 +157,29 @@ struct SerialConfig: View {
}
}
}
.onChange(of: enabled) {
if $0 != node?.serialConfig?.enabled { hasChanges = true }
.onChange(of: enabled) { oldEnabled, newEnabled in
if oldEnabled != newEnabled && newEnabled != node?.serialConfig?.enabled ?? false { hasChanges = true }
}
.onChange(of: echo) {
if $0 != node?.serialConfig?.echo { hasChanges = true }
.onChange(of: echo) { oldEcho, newEcho in
if oldEcho != newEcho && newEcho != node?.serialConfig?.echo ?? false { hasChanges = true }
}
.onChange(of: rxd) { newRxd in
if node != nil && node!.serialConfig != nil {
if newRxd != node!.serialConfig!.rxd { hasChanges = true }
}
.onChange(of: rxd) { oldRxd, newRxd in
if oldRxd != newRxd && newRxd != node?.serialConfig?.rxd ?? -1 { hasChanges = true }
}
.onChange(of: txd) { newTxd in
if node != nil && node!.serialConfig != nil {
if newTxd != node!.serialConfig!.txd { hasChanges = true }
}
.onChange(of: txd) { oldTxd, newTxd in
if oldTxd != newTxd && newTxd != node?.serialConfig?.txd ?? -1 { hasChanges = true }
}
.onChange(of: baudRate) { newBaud in
if node != nil && node!.serialConfig != nil {
if newBaud != node!.serialConfig!.baudRate { hasChanges = true }
}
.onChange(of: baudRate) { oldBaud, newBaud in
if oldBaud != newBaud && newBaud != node?.serialConfig?.baudRate ?? -1 { hasChanges = true }
}
.onChange(of: timeout) { newTimeout in
if node != nil && node!.serialConfig != nil {
if newTimeout != node!.serialConfig!.timeout { hasChanges = true }
}
.onChange(of: timeout) { oldTimeout, newTimeout in
if oldTimeout != newTimeout && newTimeout != node?.serialConfig?.timeout ?? -1 { hasChanges = true }
}
.onChange(of: overrideConsoleSerialPort) { newOverrideConsoleSerialPort in
if node != nil && node!.serialConfig != nil {
if newOverrideConsoleSerialPort != node!.serialConfig!.overrideConsoleSerialPort { hasChanges = true }
}
.onChange(of: overrideConsoleSerialPort) { oldOverrideConsoleSerialPort, newOverrideConsoleSerialPort in
if oldOverrideConsoleSerialPort != newOverrideConsoleSerialPort && newOverrideConsoleSerialPort != node?.serialConfig?.overrideConsoleSerialPort ?? false { hasChanges = true }
}
.onChange(of: mode) { newMode in
if node != nil && node!.serialConfig != nil {
if newMode != node!.serialConfig!.mode { hasChanges = true }
}
.onChange(of: mode) { oldMode, newMode in
if oldMode != newMode && newMode != node?.serialConfig?.mode ?? -1 { hasChanges = true }
}
}
}

View file

@ -167,35 +167,23 @@ struct StoreForwardConfig: View {
}
}
}
.onChange(of: enabled) { newEnabled in
if node != nil && node?.storeForwardConfig != nil {
if newEnabled != node!.storeForwardConfig!.enabled { hasChanges = true }
}
.onChange(of: enabled) { oldEnabled, newEnabled in
if oldEnabled != newEnabled && newEnabled != node!.storeForwardConfig!.enabled { hasChanges = true }
}
.onChange(of: isRouter) { newIsRouter in
if node != nil && node?.storeForwardConfig != nil {
if newIsRouter != node!.storeForwardConfig!.isRouter { hasChanges = true }
}
.onChange(of: isRouter) { oldIsRouter, newIsRouter in
if oldIsRouter != newIsRouter && newIsRouter != node!.storeForwardConfig!.isRouter { hasChanges = true }
}
.onChange(of: heartbeat) { newHeartbeat in
if node != nil && node?.storeForwardConfig != nil {
if newHeartbeat != node!.storeForwardConfig!.heartbeat { hasChanges = true }
}
.onChange(of: heartbeat) { oldHeartbeat, newHeartbeat in
if oldHeartbeat != newHeartbeat && newHeartbeat != node?.storeForwardConfig?.heartbeat ?? true { hasChanges = true }
}
.onChange(of: records) { newRecords in
if node != nil && node?.storeForwardConfig != nil {
if newRecords != node!.storeForwardConfig!.records { hasChanges = true }
}
.onChange(of: records) { oldRecords, newRecords in
if oldRecords != newRecords && newRecords != node!.storeForwardConfig?.records ?? -1 { hasChanges = true }
}
.onChange(of: historyReturnMax) { newHistoryReturnMax in
if node != nil && node?.storeForwardConfig != nil {
if newHistoryReturnMax != node!.storeForwardConfig!.historyReturnMax { hasChanges = true }
}
.onChange(of: historyReturnMax) { oldHistoryReturnMax, newHistoryReturnMax in
if oldHistoryReturnMax != newHistoryReturnMax && newHistoryReturnMax != node!.storeForwardConfig?.historyReturnMax ?? -1 { hasChanges = true }
}
.onChange(of: historyReturnWindow) { newHistoryReturnWindow in
if node != nil && node?.storeForwardConfig != nil {
if newHistoryReturnWindow != node!.storeForwardConfig!.historyReturnWindow { hasChanges = true }
}
.onChange(of: historyReturnWindow) { oldHistoryReturnWindow, newHistoryReturnWindow in
if oldHistoryReturnWindow != newHistoryReturnWindow && newHistoryReturnWindow != node!.storeForwardConfig?.historyReturnWindow ?? -1 { hasChanges = true }
}
}
func setStoreAndForwardValues() {

View file

@ -155,45 +155,29 @@ struct TelemetryConfig: View {
}
}
}
.onChange(of: deviceUpdateInterval) { newDeviceInterval in
if node != nil && node?.telemetryConfig != nil {
if newDeviceInterval != node!.telemetryConfig!.deviceUpdateInterval { hasChanges = true }
}
.onChange(of: deviceUpdateInterval) { _, newDeviceInterval in
if newDeviceInterval != node?.telemetryConfig?.deviceUpdateInterval ?? -1 { hasChanges = true }
}
.onChange(of: environmentUpdateInterval) { newEnvInterval in
if node != nil && node?.telemetryConfig != nil {
if newEnvInterval != node!.telemetryConfig!.environmentUpdateInterval { hasChanges = true }
}
.onChange(of: environmentUpdateInterval) { _, newEnvInterval in
if newEnvInterval != node?.telemetryConfig?.environmentUpdateInterval ?? -1 { hasChanges = true }
}
.onChange(of: environmentMeasurementEnabled) { newEnvEnabled in
if node != nil && node?.telemetryConfig != nil {
if newEnvEnabled != node!.telemetryConfig!.environmentMeasurementEnabled { hasChanges = true }
}
.onChange(of: environmentMeasurementEnabled) { _, newEnvEnabled in
if newEnvEnabled != node?.telemetryConfig?.environmentMeasurementEnabled { hasChanges = true }
}
.onChange(of: environmentScreenEnabled) { newEnvScreenEnabled in
if node!.telemetryConfig != nil {
if newEnvScreenEnabled != node!.telemetryConfig!.environmentScreenEnabled { hasChanges = true }
}
.onChange(of: environmentScreenEnabled) { _, newEnvScreenEnabled in
if newEnvScreenEnabled != node?.telemetryConfig?.environmentScreenEnabled { hasChanges = true }
}
.onChange(of: environmentDisplayFahrenheit) { newEnvDisplayF in
if node != nil && node?.telemetryConfig != nil {
if newEnvDisplayF != node!.telemetryConfig!.environmentDisplayFahrenheit { hasChanges = true }
}
.onChange(of: environmentDisplayFahrenheit) { _, newEnvDisplayF in
if newEnvDisplayF != node?.telemetryConfig?.environmentDisplayFahrenheit { hasChanges = true }
}
.onChange(of: powerMeasurementEnabled) { newPowerMeasurementEnabled in
if node != nil && node?.telemetryConfig != nil {
if newPowerMeasurementEnabled != node!.telemetryConfig!.powerMeasurementEnabled { hasChanges = true }
}
.onChange(of: powerMeasurementEnabled) { _, newPowerMeasurementEnabled in
if newPowerMeasurementEnabled != node?.telemetryConfig?.powerMeasurementEnabled { hasChanges = true }
}
.onChange(of: powerUpdateInterval) { newPowerUpdateInterval in
if node != nil && node?.telemetryConfig != nil {
if newPowerUpdateInterval != node!.telemetryConfig!.powerUpdateInterval { hasChanges = true }
}
.onChange(of: powerUpdateInterval) { _, newPowerUpdateInterval in
if newPowerUpdateInterval != node?.telemetryConfig?.powerUpdateInterval ?? -1 { hasChanges = true }
}
.onChange(of: powerScreenEnabled) { newPowerScreenEnabled in
if node != nil && node?.telemetryConfig != nil {
if newPowerScreenEnabled != node!.telemetryConfig!.powerScreenEnabled { hasChanges = true }
}
.onChange(of: powerScreenEnabled) { _, newPowerScreenEnabled in
if newPowerScreenEnabled != node?.telemetryConfig?.powerScreenEnabled { hasChanges = true }
}
}
}

View file

@ -45,7 +45,7 @@ struct NetworkConfig: View {
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: wifiSsid, perform: { _ in
.onChange(of: wifiSsid) {
var totalBytes = wifiSsid.utf8.count
// Only mess with the value if it is too big
while totalBytes > 32 {
@ -53,7 +53,7 @@ struct NetworkConfig: View {
totalBytes = wifiSsid.utf8.count
}
hasChanges = true
})
}
.foregroundColor(.gray)
}
.keyboardType(.default)
@ -63,7 +63,7 @@ struct NetworkConfig: View {
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: wifiPsk, perform: { _ in
.onChange(of: wifiPsk) {
var totalBytes = wifiPsk.utf8.count
// Only mess with the value if it is too big
while totalBytes > 63 {
@ -71,7 +71,7 @@ struct NetworkConfig: View {
totalBytes = wifiPsk.utf8.count
}
hasChanges = true
})
}
.foregroundColor(.gray)
}
.keyboardType(.default)
@ -151,20 +151,20 @@ struct NetworkConfig: View {
}
}
}
.onChange(of: wifiEnabled) {
if $0 != node?.networkConfig?.wifiEnabled { hasChanges = true }
.onChange(of: wifiEnabled) { _, newEnabled in
if newEnabled != node?.networkConfig?.wifiEnabled { hasChanges = true }
}
.onChange(of: wifiSsid) { newSSID in
.onChange(of: wifiSsid) { _, newSSID in
if newSSID != node?.networkConfig?.wifiSsid { hasChanges = true }
}
.onChange(of: wifiPsk) { newPsk in
.onChange(of: wifiPsk) { _, newPsk in
if newPsk != node?.networkConfig?.wifiPsk { hasChanges = true }
}
.onChange(of: wifiMode) {
if $0 != node?.networkConfig?.wifiMode ?? -1 { hasChanges = true }
.onChange(of: wifiMode) { _, newMode in
if newMode != node?.networkConfig?.wifiMode ?? -1 { hasChanges = true }
}
.onChange(of: ethEnabled) {
if $0 != node?.networkConfig?.ethEnabled { hasChanges = true }
.onChange(of: ethEnabled) { _, newEthEnabled in
if newEthEnabled != node?.networkConfig?.ethEnabled { hasChanges = true }
}
}
func setNetworkValues() {

View file

@ -398,7 +398,7 @@ struct PositionConfig: View {
}
}
}
.onChange(of: fixedPosition) { newFixed in
.onChange(of: fixedPosition) { _, newFixed in
if supportedVersion {
if let positionConfig = node?.positionConfig {
/// Fixed Position is off to start
@ -411,37 +411,37 @@ struct PositionConfig: View {
}
}
}
.onChange(of: gpsMode) { newGpsMode in
.onChange(of: gpsMode) { _, newGpsMode in
if newGpsMode != node?.positionConfig?.gpsMode ?? 0 { hasChanges = true }
}
.onChange(of: rxGpio) { newRxGpio in
.onChange(of: rxGpio) { _, newRxGpio in
if newRxGpio != node?.positionConfig?.rxGpio ?? 0 { hasChanges = true }
}
.onChange(of: txGpio) { newTxGpio in
.onChange(of: txGpio) { _, newTxGpio in
if newTxGpio != node?.positionConfig?.txGpio ?? 0 { hasChanges = true }
}
.onChange(of: gpsEnGpio) { newGpsEnGpio in
.onChange(of: gpsEnGpio) { _, newGpsEnGpio in
if newGpsEnGpio != node?.positionConfig?.gpsEnGpio ?? 0 { hasChanges = true }
}
.onChange(of: smartPositionEnabled) { newSmartPositionEnabled in
.onChange(of: smartPositionEnabled) { _, newSmartPositionEnabled in
if newSmartPositionEnabled != node?.positionConfig?.smartPositionEnabled { hasChanges = true }
}
.onChange(of: positionBroadcastSeconds) { newPositionBroadcastSeconds in
.onChange(of: positionBroadcastSeconds) { _, newPositionBroadcastSeconds in
if newPositionBroadcastSeconds != node?.positionConfig?.positionBroadcastSeconds ?? 0 { hasChanges = true }
}
.onChange(of: broadcastSmartMinimumIntervalSecs) { newBroadcastSmartMinimumIntervalSecs in
.onChange(of: broadcastSmartMinimumIntervalSecs) { _, newBroadcastSmartMinimumIntervalSecs in
if newBroadcastSmartMinimumIntervalSecs != node?.positionConfig?.broadcastSmartMinimumIntervalSecs ?? 0 { hasChanges = true }
}
.onChange(of: broadcastSmartMinimumDistance) { newBroadcastSmartMinimumDistance in
.onChange(of: broadcastSmartMinimumDistance) { _, newBroadcastSmartMinimumDistance in
if newBroadcastSmartMinimumDistance != node?.positionConfig?.broadcastSmartMinimumDistance ?? 0 { hasChanges = true }
}
.onChange(of: gpsUpdateInterval) { newGpsUpdateInterval in
.onChange(of: gpsUpdateInterval) { _, newGpsUpdateInterval in
if newGpsUpdateInterval != node?.positionConfig?.gpsUpdateInterval ?? 0 { hasChanges = true }
}
}
func handlePositionFlagtChanges() {
guard let positionConfig = node?.positionConfig else { return }
guard (node?.positionConfig) != nil else { return }
let pf = PositionFlags(rawValue: self.positionFlags)
hasChanges =
pf.contains(.Altitude) ||

View file

@ -148,31 +148,31 @@ struct PowerConfig: View {
}
}
}
.onChange(of: isPowerSaving) {
if $0 != node?.powerConfig?.isPowerSaving { hasChanges = true }
.onChange(of: isPowerSaving) { oldIsPowerSaving, newIsPowerSaving in
if oldIsPowerSaving != newIsPowerSaving && newIsPowerSaving != node?.powerConfig?.isPowerSaving { hasChanges = true }
}
.onChange(of: shutdownOnPowerLoss) { newShutdownOnPowerLoss in
.onChange(of: shutdownOnPowerLoss) { _, newShutdownOnPowerLoss in
if newShutdownOnPowerLoss {
hasChanges = true
}
}
.onChange(of: shutdownAfterSecs) {
if $0 != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true }
.onChange(of: shutdownAfterSecs) { oldShutdownAfterSecs, newShutdownAfterSecs in
if oldShutdownAfterSecs != newShutdownAfterSecs && newShutdownAfterSecs != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true }
}
.onChange(of: adcOverride) { _ in
.onChange(of: adcOverride) {
hasChanges = true
}
.onChange(of: adcMultiplier) { newAdcMultiplier in
if newAdcMultiplier != node?.powerConfig?.adcMultiplierOverride ?? -1 { hasChanges = true }
.onChange(of: adcMultiplier) { _, newAdcMultiplier in
if newAdcMultiplier != node?.powerConfig?.adcMultiplierOverride ?? -1 { hasChanges = true }
}
.onChange(of: waitBluetoothSecs) {
if $0 != node?.powerConfig?.waitBluetoothSecs ?? -1 { hasChanges = true }
.onChange(of: waitBluetoothSecs) { oldWaitBluetoothSecs, newWaitBluetoothSecs in
if oldWaitBluetoothSecs != newWaitBluetoothSecs && newWaitBluetoothSecs != node?.powerConfig?.waitBluetoothSecs ?? -1 { hasChanges = true }
}
.onChange(of: lsSecs) {
if $0 != node?.powerConfig?.lsSecs ?? -1 { hasChanges = true }
.onChange(of: lsSecs) { _, newLsSecs in
if newLsSecs != node?.powerConfig?.lsSecs ?? -1 { hasChanges = true }
}
.onChange(of: minWakeSecs) {
if $0 != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true }
.onChange(of: minWakeSecs) { _, newMinWakeSecs in
if newMinWakeSecs != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true }
}
SaveConfigButton(node: node, hasChanges: $hasChanges) {
@ -232,13 +232,13 @@ private struct FloatField: View {
TextField(title.localized, value: $typingNumber, format: .number)
.foregroundColor(.gray)
.multilineTextAlignment(.trailing)
.onChange(of: typingNumber, perform: { _ in
.onChange(of: typingNumber) {
if isValid(typingNumber) {
number = typingNumber
} else {
typingNumber = number
}
})
}
.keyboardType(.decimalPad)
.onAppear {
typingNumber = number

View file

@ -106,19 +106,19 @@ struct SecurityConfig: View {
name: "\(bleManager.connectedPeripheral?.shortName ?? "?")"
)
})
.onChange(of: isManaged) {
if $0 != node?.securityConfig?.isManaged { hasChanges = true }
.onChange(of: isManaged) { _, newIsManaged in
if newIsManaged != node?.securityConfig?.isManaged { hasChanges = true }
}
.onChange(of: serialEnabled) {
if $0 != node?.securityConfig?.serialEnabled { hasChanges = true }
.onChange(of: serialEnabled) { _, newSerialEnabled in
if newSerialEnabled != node?.securityConfig?.serialEnabled { hasChanges = true }
}
.onChange(of: debugLogApiEnabled) {
if $0 != node?.securityConfig?.debugLogApiEnabled { hasChanges = true }
.onChange(of: debugLogApiEnabled) { _, newDebugLogApiEnabled in
if newDebugLogApiEnabled != node?.securityConfig?.debugLogApiEnabled { hasChanges = true }
}
.onChange(of: adminChannelEnabled) {
if $0 != node?.securityConfig?.adminChannelEnabled { hasChanges = true }
.onChange(of: adminChannelEnabled) { _, newAdminChannelEnabled in
if newAdminChannelEnabled != node?.securityConfig?.adminChannelEnabled { hasChanges = true }
}
.onChange(of: publicKey) { _ in
.onChange(of: publicKey) {
let tempKey = Data(base64Encoded: publicKey) ?? Data()
if tempKey.count == 32 {
hasValidPublicKey = true
@ -127,7 +127,7 @@ struct SecurityConfig: View {
}
hasChanges = true
}
.onChange(of: privateKey) { _ in
.onChange(of: privateKey) {
let tempKey = Data(base64Encoded: privateKey) ?? Data()
if tempKey.count == 32 {
hasValidPrivateKey = true
@ -136,7 +136,7 @@ struct SecurityConfig: View {
}
hasChanges = true
}
.onChange(of: adminKey) { key in
.onChange(of: adminKey) { _, key in
let tempKey = Data(base64Encoded: key) ?? Data()
if key.isEmpty {
hasValidAdminKey = true

View file

@ -13,7 +13,7 @@ struct Firmware: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
var node: NodeInfoEntity?
@State var minimumVersion = "2.4.2"
@State var minimumVersion = "2.5.4"
@State var version = ""
@State private var currentDevice: DeviceHardware?
@State private var latestStable: FirmwareRelease?

View file

@ -8,7 +8,6 @@
import SwiftUI
import CoreLocation
@available(iOS 17.0, macOS 14.0, *)
struct GPSStatus: View {
var largeFont: Font = .footnote

View file

@ -12,7 +12,6 @@ import CoreLocation
import CoreMotion
import OSLog
@available(iOS 17.0, macOS 14.0, *)
struct RouteRecorder: View {
@ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared
@ -284,7 +283,7 @@ struct RouteRecorder: View {
.onDisappear(perform: {
UIApplication.shared.isIdleTimerDisabled = false
})
.onChange(of: locationsHandler.locationsArray.last) { newLoc in
.onChange(of: locationsHandler.locationsArray.last) { _, newLoc in
if locationsHandler.isRecording {
if let loc = newLoc {
if recording != nil {

View file

@ -10,7 +10,6 @@ import CoreData
import MapKit
import OSLog
@available(iOS 17.0, macOS 14.0, *)
struct Routes: View {
@State private var columnVisibility = NavigationSplitViewVisibility.doubleColumn
@ -58,7 +57,6 @@ struct Routes: View {
}
do {
guard let fileContent = String(data: try Data(contentsOf: selectedFile), encoding: .utf8) else { return }
let routeName = selectedFile.lastPathComponent.dropLast(4)
let lines = fileContent.components(separatedBy: "\n")
@ -176,14 +174,14 @@ struct Routes: View {
axis: .vertical
)
.foregroundColor(Color.gray)
.onChange(of: name, perform: { _ in
.onChange(of: name) {
var totalBytes = name.utf8.count
// Only mess with the value if it is too big
while totalBytes > 100 {
name = String(name.dropLast())
totalBytes = name.utf8.count
}
})
}
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "point.topleft.filled.down.to.point.bottomright.curvepath")
@ -237,16 +235,16 @@ struct Routes: View {
.controlSize(.large)
.disabled(!hasChanges)
}
.onChange(of: name) { _ in
.onChange(of: name) {
hasChanges = true
}
.onChange(of: notes) { _ in
.onChange(of: notes) {
hasChanges = true
}
.onChange(of: enabled) { _ in
.onChange(of: enabled) {
hasChanges = true
}
.onChange(of: color) { _ in
.onChange(of: color) {
hasChanges = true
}
Map {

View file

@ -7,9 +7,7 @@
import SwiftUI
import OSLog
#if canImport(TipKit)
import TipKit
#endif
struct Settings: View {
@Environment(\.managedObjectContext) var context
@ -156,13 +154,11 @@ struct Settings: View {
var moduleConfigurationSection: some View {
Section("module.configuration") {
if #available(iOS 17.0, macOS 14.0, *) {
NavigationLink(value: SettingsNavigationState.ambientLighting) {
Label {
Text("ambient.lighting")
} icon: {
Image(systemName: "light.max")
}
NavigationLink(value: SettingsNavigationState.ambientLighting) {
Label {
Text("ambient.lighting")
} icon: {
Image(systemName: "light.max")
}
}
@ -323,22 +319,20 @@ struct Settings: View {
Image(systemName: "gearshape")
}
}
if #available(iOS 17.0, macOS 14.0, *) {
NavigationLink(value: SettingsNavigationState.routes) {
Label {
Text("routes")
} icon: {
Image(systemName: "road.lanes.curved.right")
}
NavigationLink(value: SettingsNavigationState.routes) {
Label {
Text("routes")
} icon: {
Image(systemName: "road.lanes.curved.right")
}
}
NavigationLink(value: SettingsNavigationState.routeRecorder) {
Label {
Text("route.recorder")
} icon: {
Image(systemName: "record.circle")
.foregroundColor(.red)
}
NavigationLink(value: SettingsNavigationState.routeRecorder) {
Label {
Text("route.recorder")
} icon: {
Image(systemName: "record.circle")
.foregroundColor(.red)
}
}
@ -392,7 +386,7 @@ struct Settings: View {
}
}
.pickerStyle(.navigationLink)
.onChange(of: selectedNode) { newValue in
.onChange(of: selectedNode) { _, newValue in
if selectedNode > 0 {
let node = nodes.first(where: { $0.num == newValue })
let connectedNode = nodes.first(where: { $0.num == preferredNodeNum })
@ -405,9 +399,7 @@ struct Settings: View {
}
}
}
if #available(iOS 17.0, macOS 14.0, *) {
TipView(AdminChannelTip(), arrowEdge: .top)
}
TipView(AdminChannelTip(), arrowEdge: .top)
} else {
if bleManager.connectedPeripheral != nil {
Text("Connected Node \(node?.user?.longName ?? "unknown".localized)")
@ -418,9 +410,7 @@ struct Settings: View {
radioConfigurationSection
deviceConfigurationSection
moduleConfigurationSection
if #available (iOS 17.0, *) {
loggingSection
}
loggingSection
#if DEBUG
developersSection
#endif
@ -435,13 +425,9 @@ struct Settings: View {
case .appSettings:
AppSettings()
case .routes:
if #available(iOS 17.0, *) {
Routes()
}
Routes()
case .routeRecorder:
if #available(iOS 17.0, *) {
RouteRecorder()
}
RouteRecorder()
case .lora:
LoRaConfig(node: nodes.first(where: { $0.num == selectedNode }))
case .channels:
@ -463,9 +449,7 @@ struct Settings: View {
case .power:
PowerConfig(node: nodes.first(where: { $0.num == selectedNode }))
case .ambientLighting:
if #available(iOS 17.0, macOS 14.0, *) {
AmbientLightingConfig(node: node)
}
AmbientLightingConfig(node: node)
case .cannedMessages:
CannedMessagesConfig(node: nodes.first(where: { $0.num == selectedNode }))
case .detectionSensor:
@ -491,16 +475,14 @@ struct Settings: View {
case .meshLog:
MeshLog()
case .debugLogs:
if #available(iOS 17.0, macOS 14.0, *) {
AppLog()
}
AppLog()
case .appFiles:
AppData()
case .firmwareUpdates:
Firmware(node: node)
}
}
.onChange(of: UserDefaults.preferredPeripheralNum ) { newConnectedNode in
.onChange(of: UserDefaults.preferredPeripheralNum ) { _, newConnectedNode in
preferredNodeNum = newConnectedNode
if nodes.count > 1 {
if selectedNode == 0 {

View file

@ -8,10 +8,7 @@ import SwiftUI
import CoreData
import CoreImage.CIFilterBuiltins
import MeshtasticProtobufs
#if canImport(TipKit)
import TipKit
#endif
struct QrCodeImage {
let context = CIContext()
@ -55,10 +52,8 @@ struct ShareChannels: View {
var body: some View {
if #available(iOS 17.0, macOS 14.0, *) {
VStack {
TipView(ShareChannelsTip(), arrowEdge: .bottom)
}
VStack {
TipView(ShareChannelsTip(), arrowEdge: .bottom)
}
GeometryReader { bounds in
let smallest = min(bounds.size.width, bounds.size.height)
@ -240,15 +235,15 @@ struct ShareChannels: View {
.onAppear {
generateChannelSet()
}
.onChange(of: includeChannel0) { _ in generateChannelSet() }
.onChange(of: includeChannel1) { _ in generateChannelSet() }
.onChange(of: includeChannel2) { _ in generateChannelSet() }
.onChange(of: includeChannel3) { _ in generateChannelSet() }
.onChange(of: includeChannel4) { _ in generateChannelSet() }
.onChange(of: includeChannel5) { _ in generateChannelSet() }
.onChange(of: includeChannel6) { _ in generateChannelSet() }
.onChange(of: includeChannel7) { _ in generateChannelSet() }
.onChange(of: replaceChannels) { _ in generateChannelSet() }
.onChange(of: includeChannel0) { generateChannelSet() }
.onChange(of: includeChannel1) { generateChannelSet() }
.onChange(of: includeChannel2) { generateChannelSet() }
.onChange(of: includeChannel3) { generateChannelSet() }
.onChange(of: includeChannel4) { generateChannelSet() }
.onChange(of: includeChannel5) { generateChannelSet() }
.onChange(of: includeChannel6) { generateChannelSet() }
.onChange(of: includeChannel7) { generateChannelSet() }
.onChange(of: replaceChannels) { generateChannelSet() }
}
}
func generateChannelSet() {

View file

@ -49,14 +49,14 @@ struct UserConfig: View {
Label(isLicensed ? "Call Sign" : "Long Name", systemImage: "person.crop.rectangle.fill")
TextField("Long Name", text: $longName)
.onChange(of: longName, perform: { _ in
.onChange(of: longName) {
var totalBytes = longName.utf8.count
// Only mess with the value if it is too big
while totalBytes > (isLicensed ? 6 : 36) {
longName = String(longName.dropLast())
totalBytes = longName.utf8.count
}
})
}
}
.keyboardType(.default)
.disableAutocorrection(true)
@ -74,14 +74,14 @@ struct UserConfig: View {
Label("Short Name", systemImage: "circlebadge.fill")
TextField("Short Name", text: $shortName)
.foregroundColor(.gray)
.onChange(of: shortName, perform: { _ in
.onChange(of: shortName) {
var totalBytes = shortName.utf8.count
// Only mess with the value if it is too big
if totalBytes > 4 {
shortName = String(shortName.dropLast())
totalBytes = shortName.utf8.count
}
})
}
.foregroundColor(.gray)
}
.keyboardType(.default)
@ -197,17 +197,17 @@ struct UserConfig: View {
self.overrideFrequency = node?.loRaConfig?.overrideFrequency ?? 0.00
self.hasChanges = false
}
.onChange(of: shortName) { newShort in
.onChange(of: shortName) { _, newShort in
if node != nil && node!.user != nil {
if newShort != node?.user!.shortName { hasChanges = true }
}
}
.onChange(of: longName) { newLong in
.onChange(of: longName) { _, newLong in
if node != nil && node!.user != nil {
if newLong != node?.user!.longName { hasChanges = true }
}
}
.onChange(of: isLicensed) { newIsLicensed in
.onChange(of: isLicensed) { _, newIsLicensed in
if node != nil && node!.user != nil {
if newIsLicensed != node?.user!.isLicensed {
hasChanges = true
@ -219,10 +219,10 @@ struct UserConfig: View {
}
}
}
.onChange(of: overrideFrequency) { _ in
.onChange(of: overrideFrequency) {
hasChanges = true
}
.onChange(of: txPower) { _ in
.onChange(of: txPower) {
hasChanges = true
}
}

View file

@ -120,7 +120,6 @@ struct WidgetsLiveActivity: Widget {
}
}
struct WidgetsLiveActivity_Previews: PreviewProvider {
static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G")
static let state = MeshActivityAttributes.ContentState(uptimeSeconds: 600, channelUtilization: 1.2, airtime: 3.5, sentPackets: 12587, receivedPackets: 12555, badReceivedPackets: 800, nodesOnline: 99, totalNodes: 100, timerRange: Date.now...Date(timeIntervalSinceNow: 300))