mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge pull request #645 from meshtastic/2.3.9_Working_Changes
2.3.9 working changes
This commit is contained in:
commit
5c997be297
28 changed files with 429 additions and 220 deletions
|
|
@ -1583,7 +1583,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.3.8;
|
||||
MARKETING_VERSION = 2.3.9;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1617,7 +1617,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.3.8;
|
||||
MARKETING_VERSION = 2.3.9;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1690,7 +1690,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.3.8;
|
||||
MARKETING_VERSION = 2.3.9;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1723,7 +1723,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.3.8;
|
||||
MARKETING_VERSION = 2.3.9;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ extension PositionEntity {
|
|||
|
||||
static func allPositionsFetchRequest() -> NSFetchRequest<PositionEntity> {
|
||||
let request: NSFetchRequest<PositionEntity> = PositionEntity.fetchRequest()
|
||||
request.fetchLimit = 100
|
||||
request.fetchLimit = 1000
|
||||
request.returnsObjectsAsFaults = false
|
||||
request.includesSubentities = true
|
||||
request.returnsDistinctResults = true
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
extension UserEntity {
|
||||
|
||||
|
|
@ -27,3 +28,16 @@ extension UserEntity {
|
|||
return unreadMessages.count
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func createUser(num: Int64, context: NSManagedObjectContext) -> UserEntity {
|
||||
let newUser = UserEntity(context: context)
|
||||
newUser.num = Int64(num)
|
||||
let userId = String(format:"%2X", num)
|
||||
newUser.userId = "!\(userId)"
|
||||
let last4 = String(userId.suffix(4))
|
||||
newUser.longName = "Meshtastic \(last4)"
|
||||
newUser.shortName = last4
|
||||
newUser.hwModel = "UNSET"
|
||||
return newUser
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
var context: NSManagedObjectContext?
|
||||
|
||||
static let shared = BLEManager()
|
||||
//var userSettings: UserSettings?
|
||||
private var centralManager: CBCentralManager!
|
||||
@Published var peripherals: [Peripheral] = []
|
||||
@Published var connectedPeripheral: Peripheral!
|
||||
|
|
@ -40,7 +39,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
var timeoutTimer: Timer?
|
||||
var timeoutTimerCount = 0
|
||||
var positionTimer: Timer?
|
||||
var lastPosition: CLLocationCoordinate2D?
|
||||
static let emptyNodeNum: UInt32 = 4294967295
|
||||
let mqttManager = MqttClientProxyManager.shared
|
||||
var wantRangeTestPackets = false
|
||||
|
|
@ -1002,52 +1000,45 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
public func getPositionFromPhoneGPS(destNum: Int64) -> Position? {
|
||||
var positionPacket = Position()
|
||||
do {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
|
||||
if let lastLocation = LocationsHandler.shared.locationsArray.last {
|
||||
|
||||
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 * 3.6)
|
||||
}
|
||||
let currentHeading = lastLocation.course
|
||||
if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) {
|
||||
positionPacket.groundTrack = UInt32(currentHeading)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
if destNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
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 * 3.6)
|
||||
}
|
||||
let currentHeading = LocationHelper.shared.locationManager.location?.course ?? 0
|
||||
if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) {
|
||||
positionPacket.groundTrack = UInt32(currentHeading)
|
||||
}
|
||||
guard let lastLocation = LocationsHandler.shared.locationsArray.last else {
|
||||
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)
|
||||
}
|
||||
|
||||
} 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)
|
||||
}
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
return positionPacket
|
||||
}
|
||||
|
|
@ -1328,33 +1319,43 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
if isConnected {
|
||||
|
||||
var i: Int32 = 0
|
||||
var myInfo: MyInfoEntity
|
||||
// Before we get started delete the existing channels from the myNodeInfo
|
||||
if !addChannels {
|
||||
tryClearExistingChannels()
|
||||
} else {
|
||||
// We are trying to add a channel so lets get the last index
|
||||
let fetchMyInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MyInfoEntity")
|
||||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedPeripheral.num))
|
||||
do {
|
||||
let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as? [MyInfoEntity] ?? []
|
||||
if fetchedMyInfo.count == 1 {
|
||||
if addChannels {
|
||||
i = Int32(fetchedMyInfo[0].channels?.count ?? -1)
|
||||
// Bail out if the index is negative or bigger than our max of 8
|
||||
if i < 0 || i > 8 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Failed to find a node MyInfo to save these channels to")
|
||||
}
|
||||
}
|
||||
|
||||
let decodedString = base64UrlString.base64urlToBase64()
|
||||
if let decodedData = Data(base64Encoded: decodedString) {
|
||||
do {
|
||||
let channelSet: ChannelSet = try ChannelSet(serializedData: decodedData)
|
||||
for cs in channelSet.settings {
|
||||
if addChannels {
|
||||
// We are trying to add a channel so lets get the last index
|
||||
let fetchMyInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MyInfoEntity")
|
||||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedPeripheral.num))
|
||||
do {
|
||||
let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as? [MyInfoEntity] ?? []
|
||||
if fetchedMyInfo.count == 1 {
|
||||
i = Int32(fetchedMyInfo[0].channels?.count ?? -1)
|
||||
myInfo = fetchedMyInfo[0]
|
||||
// Bail out if the index is negative or bigger than our max of 8
|
||||
if i < 0 || i > 8 {
|
||||
return false
|
||||
}
|
||||
// Bail out if there are no channels or if the same channel name already exists
|
||||
guard let mutableChannels = myInfo.channels!.mutableCopy() as? NSMutableOrderedSet else {
|
||||
return false
|
||||
}
|
||||
if mutableChannels.first(where: {($0 as AnyObject).name == cs.name }) is ChannelEntity {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Failed to find a node MyInfo to save these channels to")
|
||||
}
|
||||
}
|
||||
|
||||
var chan = Channel()
|
||||
if i == 0 {
|
||||
chan.role = Channel.Role.primary
|
||||
|
|
@ -1364,6 +1365,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
chan.settings = cs
|
||||
chan.index = i
|
||||
i += 1
|
||||
|
||||
var adminPacket = AdminMessage()
|
||||
adminPacket.setChannel = chan
|
||||
var meshPacket: MeshPacket = MeshPacket()
|
||||
|
|
@ -2623,6 +2625,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
newMessage.fromUser = fromUser
|
||||
newMessage.toUser = toUser
|
||||
|
||||
|
||||
do {
|
||||
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
|
||||
try context!.save()
|
||||
|
|
|
|||
|
|
@ -294,16 +294,8 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
newUser.isLicensed = nodeInfo.user.isLicensed
|
||||
newUser.role = Int32(nodeInfo.user.role.rawValue)
|
||||
newNode.user = newUser
|
||||
} else {
|
||||
let newUser = UserEntity(context: context)
|
||||
newUser.num = Int64(nodeInfo.num)
|
||||
newUser.numString = String(nodeInfo.num)
|
||||
let userId = String(format:"%2X", nodeInfo.num)
|
||||
newUser.userId = "!\(userId)"
|
||||
let last4 = String(userId.suffix(4))
|
||||
newUser.longName = "Meshtastic \(last4)"
|
||||
newUser.shortName = last4
|
||||
newUser.hwModel = "UNSET"
|
||||
} else if nodeInfo.num > Int16.max {
|
||||
let newUser = createUser(num: Int64(nodeInfo.num), context: context)
|
||||
newNode.user = newUser
|
||||
}
|
||||
|
||||
|
|
@ -315,7 +307,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
position.longitudeI = nodeInfo.position.longitudeI
|
||||
position.altitude = nodeInfo.position.altitude
|
||||
position.satsInView = Int32(nodeInfo.position.satsInView)
|
||||
position.speed = Int32(nodeInfo.position.groundSpeed * UInt32(3.6))
|
||||
position.speed = Int32(nodeInfo.position.groundSpeed)
|
||||
position.heading = Int32(nodeInfo.position.groundTrack)
|
||||
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time)))
|
||||
var newPostions = [PositionEntity]()
|
||||
|
|
@ -369,15 +361,9 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
fetchedNode[0].user!.role = Int32(nodeInfo.user.role.rawValue)
|
||||
fetchedNode[0].user!.hwModel = String(describing: nodeInfo.user.hwModel).uppercased()
|
||||
} else {
|
||||
if (fetchedNode[0].user == nil) {
|
||||
let newUser = UserEntity(context: context)
|
||||
newUser.num = Int64(nodeInfo.num)
|
||||
let userId = String(format:"%2X", nodeInfo.num)
|
||||
newUser.userId = "!\(userId)"
|
||||
let last4 = String(userId.suffix(4))
|
||||
newUser.longName = "Meshtastic \(last4)"
|
||||
newUser.shortName = last4
|
||||
newUser.hwModel = "UNSET"
|
||||
if (fetchedNode[0].user == nil && nodeInfo.num > Int16.max) {
|
||||
|
||||
let newUser = createUser(num: Int64(nodeInfo.num), context: context)
|
||||
fetchedNode[0].user = newUser
|
||||
}
|
||||
}
|
||||
|
|
@ -738,7 +724,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
|
|||
subtitle: "AKA \(telemetry.nodeTelemetry?.user?.shortName ?? "UNK")",
|
||||
content: "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining.",
|
||||
target: "nodes",
|
||||
path: "meshtastic://nodes/\(telemetry.nodeTelemetry?.num ?? 0)/devicetelemetrylog"
|
||||
path: "meshtastic://nodes?nodenum=\(telemetry.nodeTelemetry?.num ?? 0)"
|
||||
)
|
||||
]
|
||||
manager.schedule()
|
||||
|
|
@ -871,8 +857,8 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
|
|||
title: "\(newMessage.fromUser?.longName ?? "unknown".localized)",
|
||||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
|
||||
content: messageText!,
|
||||
target: "message",
|
||||
path: "meshtastic://open-dm?userid=\(newMessage.fromUser?.num ?? 0)&id=\(newMessage.messageId)"
|
||||
target: "messages",
|
||||
path: "meshtastic://messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)"
|
||||
)
|
||||
]
|
||||
manager.schedule()
|
||||
|
|
@ -904,8 +890,8 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
|
|||
title: "\(newMessage.fromUser?.longName ?? "unknown".localized)",
|
||||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
|
||||
content: messageText!,
|
||||
target: "message",
|
||||
path: "meshtastic://messages/channel/\(newMessage.messageId)")
|
||||
target: "messages",
|
||||
path: "meshtastic://messages?channel=\(newMessage.channel)&messageId=\(newMessage.messageId)")
|
||||
]
|
||||
manager.schedule()
|
||||
print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)")
|
||||
|
|
@ -972,9 +958,10 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
subtitle: "\(icon) \(waypoint.name ?? "Dropped Pin")",
|
||||
content: "\(waypoint.longDescription ?? "\(latitude), \(longitude)")",
|
||||
target: "map",
|
||||
path: "meshtastic://open-waypoint?id=\(waypoint.id)"
|
||||
path: "meshtastic://map?waypontid=\(waypoint.id)"
|
||||
)
|
||||
]
|
||||
print("meshtastic://map?waypontid=\(waypoint.id)")
|
||||
manager.schedule()
|
||||
} catch {
|
||||
context.rollback()
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLIconFile</key>
|
||||
<string>alpha</string>
|
||||
<key>CFBundleURLName</key>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23F5064f" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23F5074a" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="AmbientLightingConfigEntity" representedClassName="AmbientLightingConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="blue" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="current" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ import CoreData
|
|||
import TipKit
|
||||
#endif
|
||||
|
||||
@available(iOS 17.0, *)
|
||||
@main
|
||||
struct MeshtasticAppleApp: App {
|
||||
|
||||
|
||||
@UIApplicationDelegateAdaptor(MeshtasticAppDelegate.self) var appDelegate
|
||||
let persistenceController = PersistenceController.shared
|
||||
@ObservedObject private var bleManager: BLEManager = BLEManager.shared
|
||||
|
|
@ -28,7 +29,7 @@ struct MeshtasticAppleApp: App {
|
|||
.environmentObject(bleManager)
|
||||
.sheet(isPresented: $saveChannels) {
|
||||
SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", addChannels: addChannels, bleManager: bleManager)
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in
|
||||
|
|
@ -62,6 +63,16 @@ struct MeshtasticAppleApp: App {
|
|||
}
|
||||
self.saveChannels = true
|
||||
print("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")")
|
||||
} else if url.absoluteString.lowercased().contains("meshtastic://") {
|
||||
appState.navigationPath = url.absoluteString
|
||||
let path = appState.navigationPath ?? ""
|
||||
if path.starts(with: "meshtastic://map") {
|
||||
AppState.shared.tabSelection = Tab.map
|
||||
} else if path.starts(with: "meshtastic://nodes") {
|
||||
AppState.shared.tabSelection = Tab.nodes
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
saveChannels = false
|
||||
print("User wants to import a MBTILES offline map file: \(self.incomingUrl?.absoluteString ?? "No Tiles link")")
|
||||
|
|
@ -157,6 +168,6 @@ class AppState: ObservableObject {
|
|||
@Published var unreadDirectMessages: Int = 0
|
||||
@Published var unreadChannelMessages: Int = 0
|
||||
@Published var firmwareVersion: String = "0.0.0"
|
||||
@Published var connectedNode: NodeInfoEntity?
|
||||
//@Published var connectedNode: NodeInfoEntity?
|
||||
@Published var navigationPath: String?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,10 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
class MeshtasticAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
print("🚀 Meshtstic Apple App launched!")
|
||||
// Default User Default Values
|
||||
UserDefaults.standard.register(defaults: ["blockRangeTest" : true])
|
||||
UserDefaults.standard.register(defaults: ["meshMapRecentering" : true])
|
||||
UserDefaults.standard.register(defaults: ["meshMapShowNodeHistory" : true])
|
||||
UserDefaults.standard.register(defaults: ["meshMapShowRouteLines" : true])
|
||||
|
|
@ -27,19 +26,22 @@ class MeshtasticAppDelegate: NSObject, UIApplicationDelegate, UNUserNotification
|
|||
}
|
||||
return true
|
||||
}
|
||||
// Lets us show the notification in the app in the foreground
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
completionHandler([.list, .banner, .sound])
|
||||
}
|
||||
// This method is called when user clicked on the notification
|
||||
// This method is called when a user clicks on the notification
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
let targetValue = userInfo["target"] as? String
|
||||
AppState.shared.navigationPath = userInfo["path"] as? String
|
||||
print("\(AppState.shared.navigationPath ?? "EMPTY")")
|
||||
let deepLink = userInfo["path"] as? String
|
||||
|
||||
AppState.shared.navigationPath = deepLink
|
||||
if targetValue == "map" {
|
||||
AppState.shared.tabSelection = Tab.map
|
||||
} else if targetValue == "message" {
|
||||
} else if targetValue == "messages" {
|
||||
AppState.shared.tabSelection = Tab.messages
|
||||
} else if targetValue == "node" {
|
||||
} else if targetValue == "nodes" {
|
||||
AppState.shared.tabSelection = Tab.nodes
|
||||
}
|
||||
completionHandler()
|
||||
|
|
|
|||
|
|
@ -120,7 +120,6 @@ public func clearCoreDataDatabase(context: NSManagedObjectContext, includeRoutes
|
|||
deleteRequest = NSBatchDeleteRequest(fetchRequest: query)
|
||||
} else if !includeRoutes {
|
||||
if !(entityName.contains("RouteEntity") || entityName.contains("LocationEntity")) {
|
||||
print(entity.name?.lowercased())
|
||||
deleteRequest = NSBatchDeleteRequest(fetchRequest: query)
|
||||
}
|
||||
}
|
||||
|
|
@ -161,22 +160,15 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) {
|
||||
newNode.hopsAway = Int32(nodeInfoMessage.hopsAway)
|
||||
newNode.favorite = nodeInfoMessage.isFavorite
|
||||
} else if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart {
|
||||
newNode.hopsAway = Int32(packet.hopStart - packet.hopLimit)
|
||||
}
|
||||
|
||||
if let newUserMessage = try? User(serializedData: packet.decoded.payload) {
|
||||
|
||||
if newUserMessage.id.isEmpty {
|
||||
let newUser = UserEntity(context: context)
|
||||
newUser.num = Int64(packet.from)
|
||||
let userId = String(format:"%2X", packet.from)
|
||||
newUser.userId = "!\(userId)"
|
||||
let last4 = String(userId.suffix(4))
|
||||
newUser.longName = "Meshtastic \(last4)"
|
||||
newUser.shortName = last4
|
||||
newUser.hwModel = "UNSET"
|
||||
newNode.user = newUser
|
||||
|
||||
if newUserMessage.id.isEmpty {
|
||||
if packet.from > Int16.max {
|
||||
let newUser = createUser(num: Int64(packet.from), context: context)
|
||||
newNode.user = newUser
|
||||
}
|
||||
} else {
|
||||
|
||||
let newUser = UserEntity(context: context)
|
||||
|
|
@ -197,27 +189,22 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
title: "New Node",
|
||||
subtitle: "\(newUser.longName ?? "unknown".localized)",
|
||||
content: "New Node has been discovered",
|
||||
target: "nodeInfo",
|
||||
path: "meshtastic://nodeInfo"
|
||||
target: "nodes",
|
||||
path: "meshtastic://nodes?nodenum=\(newUser.num)"
|
||||
)
|
||||
]
|
||||
manager.schedule()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let newUser = UserEntity(context: context)
|
||||
newUser.num = Int64(packet.from)
|
||||
let userId = String(format:"%2X", packet.from)
|
||||
newUser.userId = "!\(userId)"
|
||||
let last4 = String(userId.suffix(4))
|
||||
newUser.longName = "Meshtastic \(last4)"
|
||||
newUser.shortName = last4
|
||||
newUser.hwModel = "UNSET"
|
||||
newNode.user = newUser
|
||||
if packet.from > Int16.max {
|
||||
let newUser = createUser(num: Int64(packet.from), context: context)
|
||||
fetchedNode[0].user = newUser
|
||||
}
|
||||
}
|
||||
|
||||
if newNode.user == nil {
|
||||
print("Nil User on nodeinfo")
|
||||
if newNode.user == nil && packet.from > Int16.max {
|
||||
newNode.user = createUser(num: Int64(packet.from), context: context)
|
||||
}
|
||||
|
||||
let myInfoEntity = MyInfoEntity(context: context)
|
||||
|
|
@ -274,14 +261,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit)
|
||||
}
|
||||
if (fetchedNode[0].user == nil) {
|
||||
let newUser = UserEntity(context: context)
|
||||
newUser.num = Int64(packet.from)
|
||||
let userId = String(format:"%2X", packet.from)
|
||||
newUser.userId = "!\(userId)"
|
||||
let last4 = String(userId.suffix(4))
|
||||
newUser.longName = "Meshtastic \(last4)"
|
||||
newUser.shortName = last4
|
||||
newUser.hwModel = "UNSET"
|
||||
let newUser = createUser(num: Int64(packet.from), context: context)
|
||||
fetchedNode[0].user! = newUser
|
||||
}
|
||||
do {
|
||||
|
|
@ -338,7 +318,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
position.longitudeI = positionMessage.longitudeI
|
||||
position.altitude = positionMessage.altitude
|
||||
position.satsInView = Int32(positionMessage.satsInView)
|
||||
position.speed = Int32(positionMessage.groundSpeed * UInt32(3.6))
|
||||
position.speed = Int32(positionMessage.groundSpeed)
|
||||
position.heading = Int32(positionMessage.groundTrack)
|
||||
position.precisionBits = Int32(positionMessage.precisionBits)
|
||||
if positionMessage.timestamp != 0 {
|
||||
|
|
|
|||
|
|
@ -278,6 +278,16 @@ enum HardwareModel: SwiftProtobuf.Enum {
|
|||
/// Adafruit NRF52840 feather express with SX1262, SSD1306 OLED and NEO6M GPS
|
||||
case twcMeshV4 // = 62
|
||||
|
||||
///
|
||||
/// NRF52_PROMICRO_DIY
|
||||
/// Promicro NRF52840 with SX1262/LLCC68, SSD1306 OLED and NEO6M GPS
|
||||
case nrf52PromicroDiy // = 63
|
||||
|
||||
///
|
||||
/// RadioMaster 900 Bandit Nano, https://www.radiomasterrc.com/products/bandit-nano-expresslrs-rf-module
|
||||
/// ESP32-D0WDQ6 With SX1276/SKY66122, SSD1306 OLED and No GPS
|
||||
case radiomaster900BanditNano // = 64
|
||||
|
||||
///
|
||||
/// ------------------------------------------------------------------------------------------------------------------------------------------
|
||||
/// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
|
||||
|
|
@ -350,6 +360,8 @@ enum HardwareModel: SwiftProtobuf.Enum {
|
|||
case 60: self = .tdLorac
|
||||
case 61: self = .cdebyteEoraS3
|
||||
case 62: self = .twcMeshV4
|
||||
case 63: self = .nrf52PromicroDiy
|
||||
case 64: self = .radiomaster900BanditNano
|
||||
case 255: self = .privateHw
|
||||
default: self = .UNRECOGNIZED(rawValue)
|
||||
}
|
||||
|
|
@ -416,6 +428,8 @@ enum HardwareModel: SwiftProtobuf.Enum {
|
|||
case .tdLorac: return 60
|
||||
case .cdebyteEoraS3: return 61
|
||||
case .twcMeshV4: return 62
|
||||
case .nrf52PromicroDiy: return 63
|
||||
case .radiomaster900BanditNano: return 64
|
||||
case .privateHw: return 255
|
||||
case .UNRECOGNIZED(let i): return i
|
||||
}
|
||||
|
|
@ -487,6 +501,8 @@ extension HardwareModel: CaseIterable {
|
|||
.tdLorac,
|
||||
.cdebyteEoraS3,
|
||||
.twcMeshV4,
|
||||
.nrf52PromicroDiy,
|
||||
.radiomaster900BanditNano,
|
||||
.privateHw,
|
||||
]
|
||||
}
|
||||
|
|
@ -2787,6 +2803,8 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding {
|
|||
60: .same(proto: "TD_LORAC"),
|
||||
61: .same(proto: "CDEBYTE_EORA_S3"),
|
||||
62: .same(proto: "TWC_MESH_V4"),
|
||||
63: .same(proto: "NRF52_PROMICRO_DIY"),
|
||||
64: .same(proto: "RADIOMASTER_900_BANDIT_NANO"),
|
||||
255: .same(proto: "PRIVATE_HW"),
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -618,6 +618,14 @@ struct ModuleConfig {
|
|||
|
||||
var paxcounterUpdateInterval: UInt32 = 0
|
||||
|
||||
///
|
||||
/// WiFi RSSI threshold. Defaults to -80
|
||||
var wifiThreshold: Int32 = 0
|
||||
|
||||
///
|
||||
/// BLE RSSI threshold. Defaults to -80
|
||||
var bleThreshold: Int32 = 0
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
|
@ -1921,6 +1929,8 @@ extension ModuleConfig.PaxcounterConfig: SwiftProtobuf.Message, SwiftProtobuf._M
|
|||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "enabled"),
|
||||
2: .standard(proto: "paxcounter_update_interval"),
|
||||
3: .standard(proto: "wifi_threshold"),
|
||||
4: .standard(proto: "ble_threshold"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
|
|
@ -1931,6 +1941,8 @@ extension ModuleConfig.PaxcounterConfig: SwiftProtobuf.Message, SwiftProtobuf._M
|
|||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularBoolField(value: &self.enabled) }()
|
||||
case 2: try { try decoder.decodeSingularUInt32Field(value: &self.paxcounterUpdateInterval) }()
|
||||
case 3: try { try decoder.decodeSingularInt32Field(value: &self.wifiThreshold) }()
|
||||
case 4: try { try decoder.decodeSingularInt32Field(value: &self.bleThreshold) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
|
@ -1943,12 +1955,20 @@ extension ModuleConfig.PaxcounterConfig: SwiftProtobuf.Message, SwiftProtobuf._M
|
|||
if self.paxcounterUpdateInterval != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: self.paxcounterUpdateInterval, fieldNumber: 2)
|
||||
}
|
||||
if self.wifiThreshold != 0 {
|
||||
try visitor.visitSingularInt32Field(value: self.wifiThreshold, fieldNumber: 3)
|
||||
}
|
||||
if self.bleThreshold != 0 {
|
||||
try visitor.visitSingularInt32Field(value: self.bleThreshold, fieldNumber: 4)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
static func ==(lhs: ModuleConfig.PaxcounterConfig, rhs: ModuleConfig.PaxcounterConfig) -> Bool {
|
||||
if lhs.enabled != rhs.enabled {return false}
|
||||
if lhs.paxcounterUpdateInterval != rhs.paxcounterUpdateInterval {return false}
|
||||
if lhs.wifiThreshold != rhs.wifiThreshold {return false}
|
||||
if lhs.bleThreshold != rhs.bleThreshold {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,6 +92,30 @@ enum TelemetrySensorType: SwiftProtobuf.Enum {
|
|||
///
|
||||
/// RCWL-9620 Doppler Radar Distance Sensor, used for water level detection
|
||||
case rcwl9620 // = 16
|
||||
|
||||
///
|
||||
/// Sensirion High accuracy temperature and humidity
|
||||
case sht4X // = 17
|
||||
|
||||
///
|
||||
/// VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor.
|
||||
case veml7700 // = 18
|
||||
|
||||
///
|
||||
/// MLX90632 non-contact IR temperature sensor.
|
||||
case mlx90632 // = 19
|
||||
|
||||
///
|
||||
/// TI OPT3001 Ambient Light Sensor
|
||||
case opt3001 // = 20
|
||||
|
||||
///
|
||||
/// Lite On LTR-390UV-01 UV Light Sensor
|
||||
case ltr390Uv // = 21
|
||||
|
||||
///
|
||||
/// AMS TSL25911FN RGB Light Sensor
|
||||
case tsl25911Fn // = 22
|
||||
case UNRECOGNIZED(Int)
|
||||
|
||||
init() {
|
||||
|
|
@ -117,6 +141,12 @@ enum TelemetrySensorType: SwiftProtobuf.Enum {
|
|||
case 14: self = .ina3221
|
||||
case 15: self = .bmp085
|
||||
case 16: self = .rcwl9620
|
||||
case 17: self = .sht4X
|
||||
case 18: self = .veml7700
|
||||
case 19: self = .mlx90632
|
||||
case 20: self = .opt3001
|
||||
case 21: self = .ltr390Uv
|
||||
case 22: self = .tsl25911Fn
|
||||
default: self = .UNRECOGNIZED(rawValue)
|
||||
}
|
||||
}
|
||||
|
|
@ -140,6 +170,12 @@ enum TelemetrySensorType: SwiftProtobuf.Enum {
|
|||
case .ina3221: return 14
|
||||
case .bmp085: return 15
|
||||
case .rcwl9620: return 16
|
||||
case .sht4X: return 17
|
||||
case .veml7700: return 18
|
||||
case .mlx90632: return 19
|
||||
case .opt3001: return 20
|
||||
case .ltr390Uv: return 21
|
||||
case .tsl25911Fn: return 22
|
||||
case .UNRECOGNIZED(let i): return i
|
||||
}
|
||||
}
|
||||
|
|
@ -168,6 +204,12 @@ extension TelemetrySensorType: CaseIterable {
|
|||
.ina3221,
|
||||
.bmp085,
|
||||
.rcwl9620,
|
||||
.sht4X,
|
||||
.veml7700,
|
||||
.mlx90632,
|
||||
.opt3001,
|
||||
.ltr390Uv,
|
||||
.tsl25911Fn,
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -245,6 +287,14 @@ struct EnvironmentMetrics {
|
|||
/// RCWL9620 Doppler Radar Distance Sensor, used for water level detection. Float value in mm.
|
||||
var distance: Float = 0
|
||||
|
||||
///
|
||||
/// VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor.
|
||||
var lux: Float = 0
|
||||
|
||||
///
|
||||
/// VEML7700 high accuracy white light(irradiance) not calibrated digital 16-bit resolution sensor.
|
||||
var whiteLux: Float = 0
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
|
@ -479,6 +529,12 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding {
|
|||
14: .same(proto: "INA3221"),
|
||||
15: .same(proto: "BMP085"),
|
||||
16: .same(proto: "RCWL9620"),
|
||||
17: .same(proto: "SHT4X"),
|
||||
18: .same(proto: "VEML7700"),
|
||||
19: .same(proto: "MLX90632"),
|
||||
20: .same(proto: "OPT3001"),
|
||||
21: .same(proto: "LTR390UV"),
|
||||
22: .same(proto: "TSL25911FN"),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -549,6 +605,8 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple
|
|||
6: .same(proto: "current"),
|
||||
7: .same(proto: "iaq"),
|
||||
8: .same(proto: "distance"),
|
||||
9: .same(proto: "lux"),
|
||||
10: .standard(proto: "white_lux"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
|
|
@ -565,6 +623,8 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple
|
|||
case 6: try { try decoder.decodeSingularFloatField(value: &self.current) }()
|
||||
case 7: try { try decoder.decodeSingularUInt32Field(value: &self.iaq) }()
|
||||
case 8: try { try decoder.decodeSingularFloatField(value: &self.distance) }()
|
||||
case 9: try { try decoder.decodeSingularFloatField(value: &self.lux) }()
|
||||
case 10: try { try decoder.decodeSingularFloatField(value: &self.whiteLux) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
|
@ -595,6 +655,12 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple
|
|||
if self.distance != 0 {
|
||||
try visitor.visitSingularFloatField(value: self.distance, fieldNumber: 8)
|
||||
}
|
||||
if self.lux != 0 {
|
||||
try visitor.visitSingularFloatField(value: self.lux, fieldNumber: 9)
|
||||
}
|
||||
if self.whiteLux != 0 {
|
||||
try visitor.visitSingularFloatField(value: self.whiteLux, fieldNumber: 10)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
|
@ -607,6 +673,8 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple
|
|||
if lhs.current != rhs.current {return false}
|
||||
if lhs.iaq != rhs.iaq {return false}
|
||||
if lhs.distance != rhs.distance {return false}
|
||||
if lhs.lux != rhs.lux {return false}
|
||||
if lhs.whiteLux != rhs.whiteLux {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 17.0, *)
|
||||
struct ContentView: View {
|
||||
|
||||
@StateObject var appState = AppState.shared
|
||||
var body: some View {
|
||||
TabView(selection: $appState.tabSelection) {
|
||||
|
|
@ -54,14 +56,21 @@ struct ContentView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
//#Preview {
|
||||
// if #available(iOS 17.0, *) {
|
||||
// // ContentView(deepLinkManager: .init())
|
||||
// } else {
|
||||
// // Fallback on earlier versions
|
||||
// }
|
||||
//}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
//struct ContentView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ContentView()
|
||||
// }
|
||||
//}
|
||||
|
||||
enum Tab {
|
||||
enum Tab: Hashable {
|
||||
case contacts
|
||||
case messages
|
||||
case map
|
||||
|
|
|
|||
|
|
@ -16,12 +16,20 @@ struct CircleText: View {
|
|||
Circle()
|
||||
.fill(color)
|
||||
.frame(width: circleSize, height: circleSize)
|
||||
#if os(macOS)
|
||||
Text(text)
|
||||
.textCase(.uppercase)
|
||||
.frame(width: circleSize * 0.95, height: circleSize * 0.95, alignment: .center)
|
||||
.foregroundColor(color.isLight() ? .black : .white)
|
||||
.font(.system(size: 8000))
|
||||
.minimumScaleFactor(0.001)
|
||||
.frame(width: circleSize * 0.95, height: circleSize * 0.95, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
|
||||
.font(.system(size: 3000))
|
||||
.minimumScaleFactor(0.001)
|
||||
#else
|
||||
Text(text)
|
||||
.frame(width: circleSize * 0.95, height: circleSize * 0.95, alignment: .center)
|
||||
.foregroundColor(color.isLight() ? .black : .white)
|
||||
.font(.system(size: 5000))
|
||||
.minimumScaleFactor(0.001)
|
||||
#endif
|
||||
|
||||
}
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import TipKit
|
|||
#endif
|
||||
|
||||
struct Messages: View {
|
||||
|
||||
|
||||
@StateObject var appState = AppState.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
|
@ -66,6 +66,26 @@ struct Messages: View {
|
|||
.navigationTitle("messages")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.navigationBarItems(leading: MeshtasticLogo())
|
||||
.onChange(of: (appState.navigationPath)) { newPath in
|
||||
|
||||
if ((newPath?.hasPrefix("meshtastic://messages")) != nil) {
|
||||
|
||||
if let urlComponent = URLComponents(string: newPath ?? "") {
|
||||
let queryItems = urlComponent.queryItems
|
||||
let messageId = queryItems?.first(where: { $0.name == "messageId" })?.value
|
||||
let channel = queryItems?.first(where: { $0.name == "channel" })?.value
|
||||
|
||||
if channel == nil {
|
||||
print("Channel not found")
|
||||
}
|
||||
else {
|
||||
print("Channel \(channel)")
|
||||
// selectedNode = nodes.first(where: { $0.num == Int64(nodeNum ?? "-1") })
|
||||
// AppState.shared.navigationPath = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ struct UserList: View {
|
|||
|
||||
private var users: FetchedResults<UserEntity>
|
||||
@State var node: NodeInfoEntity?
|
||||
@State var selectedUserNum: Int64?
|
||||
@State private var userSelection: UserEntity? // Nothing selected by default.
|
||||
@State private var isPresentingDeleteUserMessagesConfirm: Bool = false
|
||||
|
||||
|
|
@ -203,6 +204,10 @@ struct UserList: View {
|
|||
.onChange(of: distanceFilter) { _ in
|
||||
searchUserList()
|
||||
}
|
||||
.onChange(of: selectedUserNum) { newUserNum in
|
||||
userSelection = users.first(where: { $0.num == newUserNum })
|
||||
print(userSelection)
|
||||
}
|
||||
.onAppear {
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import MapKit
|
|||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct MeshMapContent: MapContent {
|
||||
|
||||
@StateObject var appState = AppState.shared
|
||||
/// Parameters
|
||||
@Binding var showUserLocation: Bool
|
||||
@AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false
|
||||
|
|
@ -39,13 +40,14 @@ struct MeshMapContent: MapContent {
|
|||
|
||||
@MapContentBuilder
|
||||
var meshMap: some MapContent {
|
||||
let lineCoords = Array(positions).compactMap({(position) -> CLLocationCoordinate2D in
|
||||
return position.nodeCoordinate ?? LocationsHandler.DefaultLocation
|
||||
let loraNodes = positions.filter { $0.nodePosition?.viaMqtt ?? true == false }
|
||||
let loraCoords = Array(loraNodes).compactMap({(position) -> CLLocationCoordinate2D in
|
||||
return position.nodeCoordinate ?? LocationsHandler.DefaultLocation
|
||||
})
|
||||
/// Convex Hull
|
||||
if showConvexHull {
|
||||
if lineCoords.count > 0 {
|
||||
let hull = lineCoords.getConvexHull()
|
||||
if loraCoords.count > 0 {
|
||||
let hull = loraCoords.getConvexHull()
|
||||
MapPolygon(coordinates: hull)
|
||||
.stroke(.blue, lineWidth: 3)
|
||||
.foregroundStyle(.indigo.opacity(0.4))
|
||||
|
|
@ -92,6 +94,7 @@ struct MeshMapContent: MapContent {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/// Node History and Route Lines for favorites
|
||||
if position.nodePosition?.favorite ?? false {
|
||||
if showRouteLines {
|
||||
|
|
@ -142,7 +145,7 @@ struct MeshMapContent: MapContent {
|
|||
}
|
||||
}
|
||||
/// Reduced Precision Map Circles
|
||||
if 11...16 ~= position.precisionBits {
|
||||
if 10...19 ~= position.precisionBits {
|
||||
let pp = PositionPrecision(rawValue: Int(position.precisionBits))
|
||||
let radius : CLLocationDistance = pp?.precisionMeters ?? 0
|
||||
if radius > 0.0 {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ struct NodeMapContent: MapContent {
|
|||
let lineCoords = positionArray.compactMap({(position) -> CLLocationCoordinate2D in
|
||||
return position.nodeCoordinate ?? LocationsHandler.DefaultLocation
|
||||
})
|
||||
|
||||
/// Node Color from node.num
|
||||
let nodeColor = UIColor(hex: UInt32(node.num))
|
||||
|
||||
|
|
@ -49,7 +50,7 @@ struct NodeMapContent: MapContent {
|
|||
let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 771))
|
||||
let headingDegrees = Angle.degrees(Double(position.heading))
|
||||
/// Reduced Precision Map Circle
|
||||
if position.latest && 11...16 ~= position.precisionBits {
|
||||
if position.latest && 10...19 ~= position.precisionBits {
|
||||
let pp = PositionPrecision(rawValue: Int(position.precisionBits))
|
||||
let radius : CLLocationDistance = pp?.precisionMeters ?? 0
|
||||
if radius > 0.0 {
|
||||
|
|
@ -58,13 +59,17 @@ struct NodeMapContent: MapContent {
|
|||
.stroke(.white, lineWidth: 2)
|
||||
}
|
||||
}
|
||||
let loraNodes = positions.filter { $0.nodePosition?.viaMqtt ?? true == false }
|
||||
let loraCoords = Array(loraNodes).compactMap({(position) -> CLLocationCoordinate2D in
|
||||
return position.nodeCoordinate ?? LocationsHandler.DefaultLocation
|
||||
})
|
||||
/// Convex Hull
|
||||
if showConvexHull {
|
||||
if lineCoords.count > 0 {
|
||||
let hull = lineCoords.getConvexHull()
|
||||
if loraCoords.count > 0 {
|
||||
let hull = loraCoords.getConvexHull()
|
||||
MapPolygon(coordinates: hull)
|
||||
.stroke(Color(nodeColor.darker()), lineWidth: 3)
|
||||
.foregroundStyle(Color(nodeColor).opacity(0.4))
|
||||
.stroke(.blue, lineWidth: 3)
|
||||
.foregroundStyle(.indigo.opacity(0.4))
|
||||
}
|
||||
}
|
||||
/// Route Lines
|
||||
|
|
|
|||
|
|
@ -87,13 +87,13 @@ struct NodeMapSwiftUI: View {
|
|||
switch selectedMapLayer {
|
||||
case .standard:
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
mapStyle = MapStyle.standard(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
mapStyle = MapStyle.standard(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
case .hybrid:
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
mapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
case .satellite:
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
mapStyle = MapStyle.imagery(elevation: .realistic)
|
||||
mapStyle = MapStyle.imagery(elevation: .flat)
|
||||
case .offline:
|
||||
return
|
||||
}
|
||||
|
|
@ -118,13 +118,13 @@ struct NodeMapSwiftUI: View {
|
|||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
switch selectedMapLayer {
|
||||
case .standard:
|
||||
mapStyle = MapStyle.standard(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
mapStyle = MapStyle.standard(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
case .hybrid:
|
||||
mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
mapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
case .satellite:
|
||||
mapStyle = MapStyle.imagery(elevation: .realistic)
|
||||
mapStyle = MapStyle.imagery(elevation: .flat)
|
||||
case .offline:
|
||||
mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
mapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
}
|
||||
mostRecent = node.positions?.lastObject as? PositionEntity
|
||||
if node.positions?.count ?? 0 > 1 {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ struct NodeDetail: View {
|
|||
Label(
|
||||
title: {
|
||||
Text("\("uptime".localized)")
|
||||
.font(.title2)+Text(": \(components)")
|
||||
.font(.title3)+Text(": \(components)")
|
||||
.font(.title3)
|
||||
.foregroundColor(Color.gray)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -35,9 +35,11 @@ struct MeshMap: View {
|
|||
@State var selectedPosition: PositionEntity?
|
||||
@State var editingWaypoint: WaypointEntity?
|
||||
@State var selectedWaypoint: WaypointEntity?
|
||||
@State var selectedWaypointId: String?
|
||||
@State var newWaypointCoord: CLLocationCoordinate2D?
|
||||
@State var isMeshMap = true
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
NavigationStack {
|
||||
|
|
@ -106,33 +108,33 @@ struct MeshMap: View {
|
|||
.sheet(isPresented: $isEditingSettings) {
|
||||
MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap)
|
||||
}
|
||||
.onChange(of: (appState.navigationPath)) { newPath in
|
||||
|
||||
if ((newPath?.hasPrefix("meshtastic://open-waypoint")) != nil) {
|
||||
guard let url = URL(string: appState.navigationPath ?? "NONE") else {
|
||||
print("Invalid URL")
|
||||
return
|
||||
}
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
|
||||
print("Invalid URL Components")
|
||||
return
|
||||
}
|
||||
guard let action = components.host, action == "open-waypoint" else {
|
||||
print("Unknown waypoint URL action")
|
||||
return
|
||||
}
|
||||
guard let waypointId = components.queryItems?.first(where: { $0.name == "id" })?.value else {
|
||||
print("Waypoint id not found")
|
||||
return
|
||||
}
|
||||
// guard let waypoint = waypoints.first(where: { $0.id == Int64(waypointId) }) else {
|
||||
// print("Waypoint not found")
|
||||
// .onChange(of: (appState.navigationPath)) { newPath in
|
||||
//
|
||||
// if ((newPath?.hasPrefix("meshtastic://open-waypoint")) != nil) {
|
||||
// guard let url = URL(string: appState.navigationPath ?? "NONE") else {
|
||||
// print("Invalid URL")
|
||||
// return
|
||||
// }
|
||||
// guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
|
||||
// print("Invalid URL Components")
|
||||
// return
|
||||
// }
|
||||
//showWaypoints = true
|
||||
//position = .camera(MapCamera(centerCoordinate: waypoint.coordinate, distance: 1000, heading: 0, pitch: 60))
|
||||
}
|
||||
}
|
||||
// guard let action = components.host, action == "open-waypoint" else {
|
||||
// print("Unknown waypoint URL action")
|
||||
// return
|
||||
// }
|
||||
// guard let waypointId = components.queryItems?.first(where: { $0.name == "id" })?.value else {
|
||||
// print("Waypoint id not found")
|
||||
// return
|
||||
// }
|
||||
//// guard let waypoint = waypoints.first(where: { $0.id == Int64(waypointId) }) else {
|
||||
//// print("Waypoint not found")
|
||||
//// return
|
||||
//// }
|
||||
// //showWaypoints = true
|
||||
// //position = .camera(MapCamera(centerCoordinate: waypoint.coordinate, distance: 1000, heading: 0, pitch: 60))
|
||||
// }
|
||||
// }
|
||||
.onChange(of: (selectedMapLayer)) { newMapLayer in
|
||||
switch selectedMapLayer {
|
||||
case .standard:
|
||||
|
|
@ -174,6 +176,10 @@ struct MeshMap: View {
|
|||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
|
||||
// let wayPointEntity = getWaypoint(id: Int64(deepLinkManager.waypointId) ?? -1, context: context)
|
||||
//if wayPointEntity.id > 0 {
|
||||
// position = .camera(MapCamera(centerCoordinate: wayPointEntity.coordinate, distance: 1000, heading: 0, pitch: 60))
|
||||
switch selectedMapLayer {
|
||||
case .standard:
|
||||
mapStyle = MapStyle.standard(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import CoreLocation
|
|||
|
||||
struct NodeList: View {
|
||||
|
||||
@StateObject var appState = AppState.shared
|
||||
@State private var columnVisibility = NavigationSplitViewVisibility.all
|
||||
@State private var selectedNode: NodeInfoEntity?
|
||||
@State private var isPresentingTraceRouteSentAlert = false
|
||||
|
|
@ -44,6 +45,14 @@ struct NodeList: View {
|
|||
var body: some View {
|
||||
NavigationSplitView(columnVisibility: $columnVisibility) {
|
||||
|
||||
// HStack {
|
||||
// Button("Open Node") {
|
||||
// UIApplication
|
||||
// .shared
|
||||
// .open(URL(string: "meshtastic://nodes?nodeNum=530606484")!)
|
||||
// }
|
||||
// }
|
||||
|
||||
let connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
|
||||
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
|
||||
List(nodes, id: \.self, selection: $selectedNode) { node in
|
||||
|
|
@ -203,6 +212,7 @@ struct NodeList: View {
|
|||
.disableAutocorrection(true)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count)))
|
||||
|
||||
.listStyle(.plain)
|
||||
.confirmationDialog(
|
||||
|
||||
|
|
@ -213,12 +223,11 @@ struct NodeList: View {
|
|||
Button("Delete Node") {
|
||||
let deleteNode = getNodeInfo(id: deleteNodeId, context: context)
|
||||
if connectedNode != nil {
|
||||
|
||||
}
|
||||
if deleteNode != nil {
|
||||
let success = bleManager.removeNode(node: deleteNode!, connectedNodeNum: Int64(connectedNodeNum))
|
||||
if !success {
|
||||
print("Failed to delete node \(deleteNode?.user?.longName ?? "unknown".localized)")
|
||||
if deleteNode != nil {
|
||||
let success = bleManager.removeNode(node: deleteNode!, connectedNodeNum: Int64(connectedNodeNum))
|
||||
if !success {
|
||||
print("Failed to delete node \(deleteNode?.user?.longName ?? "unknown".localized)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -305,6 +314,26 @@ struct NodeList: View {
|
|||
.onChange(of: distanceFilter) { _ in
|
||||
searchNodeList()
|
||||
}
|
||||
.onChange(of: (appState.navigationPath)) { newPath in
|
||||
|
||||
guard let deepLink = newPath else {
|
||||
return
|
||||
}
|
||||
if deepLink.hasPrefix("meshtastic://nodes") {
|
||||
|
||||
if let urlComponent = URLComponents(string: deepLink) {
|
||||
let queryItems = urlComponent.queryItems
|
||||
let nodeNum = queryItems?.first(where: { $0.name == "nodenum" })?.value
|
||||
if nodeNum == nil {
|
||||
print("nodeNum not found")
|
||||
}
|
||||
else {
|
||||
selectedNode = nodes.first(where: { $0.num == Int64(nodeNum ?? "-1") })
|
||||
AppState.shared.navigationPath = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ struct ChannelForm: View {
|
|||
if !preciseLocation {
|
||||
VStack(alignment: .leading) {
|
||||
Label("Approximate Location", systemImage: "location.slash.circle.fill")
|
||||
Slider(value: $positionPrecision, in: 11...18, step: 1) {
|
||||
Slider(value: $positionPrecision, in: 10...19, step: 1) {
|
||||
} minimumValueLabel: {
|
||||
Image(systemName: "minus")
|
||||
} maximumValueLabel: {
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ struct PositionConfig: View {
|
|||
.font(.callout)
|
||||
}
|
||||
}
|
||||
if gpsMode != 1 {
|
||||
if gpsMode != 1 && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? -1 {
|
||||
VStack(alignment: .leading) {
|
||||
Toggle(isOn: $fixedPosition) {
|
||||
Label("Fixed Position", systemImage: "location.square.fill")
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ struct Firmware: View {
|
|||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
var node: NodeInfoEntity?
|
||||
@State var minimumVersion = "2.3.8"
|
||||
@State var minimumVersion = "2.3.9"
|
||||
@State var version = ""
|
||||
@State private var currentDevice: DeviceHardware?
|
||||
@State private var latestStable: FirmwareRelease?
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ struct SaveChannelQRCode: View {
|
|||
var channelSetLink: String
|
||||
var addChannels: Bool = false
|
||||
var bleManager: BLEManager
|
||||
@State var showError: Bool = false
|
||||
@State var connectedToDevice = false
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -20,26 +21,48 @@ struct SaveChannelQRCode: View {
|
|||
Text("\(addChannels ? "Add" : "Replace all") Channels?")
|
||||
.font(.title)
|
||||
Text("These settings will \(addChannels ? "add" : "replace all") channels. The current LoRa Config will be replaced. After everything saves your device will reboot.")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundColor(.gray)
|
||||
.font(.title3)
|
||||
.padding()
|
||||
|
||||
if showError {
|
||||
Text("Channels being added from the QR code did not save. When adding channels the names must be unique.")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundColor(.red)
|
||||
.font(.callout)
|
||||
.padding()
|
||||
}
|
||||
HStack {
|
||||
|
||||
Button {
|
||||
let success = bleManager.saveChannelSet(base64UrlString: channelSetLink, addChannels: addChannels)
|
||||
if success {
|
||||
dismiss()
|
||||
if !showError {
|
||||
Button {
|
||||
let success = bleManager.saveChannelSet(base64UrlString: channelSetLink, addChannels: addChannels)
|
||||
if success {
|
||||
dismiss()
|
||||
} else {
|
||||
showError = true
|
||||
}
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding()
|
||||
.disabled(!connectedToDevice)
|
||||
} else {
|
||||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("cancel", systemImage: "xmark")
|
||||
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding()
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding()
|
||||
.disabled(!connectedToDevice)
|
||||
|
||||
|
||||
#if targetEnvironment(macCatalyst)
|
||||
Button {
|
||||
|
|
|
|||
|
|
@ -278,8 +278,6 @@ struct ShareChannels: View {
|
|||
channelSettings.name = ch.name!
|
||||
channelSettings.psk = ch.psk!
|
||||
channelSettings.id = UInt32(ch.id)
|
||||
channelSettings.uplinkEnabled = ch.uplinkEnabled
|
||||
channelSettings.downlinkEnabled = ch.downlinkEnabled
|
||||
channelSet.settings.append(channelSettings)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue