Merge pull request #645 from meshtastic/2.3.9_Working_Changes

2.3.9 working changes
This commit is contained in:
Garth Vander Houwen 2024-05-28 13:52:01 -07:00 committed by GitHub
commit 5c997be297
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 429 additions and 220 deletions

View file

@ -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 = "";

View file

@ -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

View file

@ -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
}

View file

@ -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()

View file

@ -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()

View file

@ -35,7 +35,7 @@
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<string>Editor</string>
<key>CFBundleURLIconFile</key>
<string>alpha</string>
<key>CFBundleURLName</key>

View file

@ -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"/>

View file

@ -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?
}

View file

@ -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()

View file

@ -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 {

View file

@ -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"),
]
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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)
}

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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 {

View file

@ -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)
},

View file

@ -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)

View file

@ -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

View file

@ -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: {

View file

@ -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")

View file

@ -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?

View file

@ -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 {

View file

@ -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)
}
}