Merge pull request #1315 from meshtastic/2.6.13

2.6.13 Working Changes
This commit is contained in:
Garth Vander Houwen 2025-07-16 23:45:02 -07:00 committed by GitHub
commit 204e6c10f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 170 additions and 279 deletions

View file

@ -36646,33 +36646,8 @@
}
}
},
"The Router roles are designed for high vantage locations like mountaintops and towers. This node needs to be able to have a good direct connection to most of the nodes on the network or else this will significantly hurt the network." : {
"localizations" : {
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "I ruoli di router sono progettati per posizioni elevate, come le cime delle montagne e le torri. Questo nodo deve essere in grado di avere una buona connessione diretta con la maggior parte dei nodi della rete, altrimenti danneggia significativamente la rete."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "ルーター役割は山頂や塔のような見晴らしの良い高所での使用を想定して設計されています。このノードは、ネットワーク内の大部分のノードと良好な直接接続を保持できる必要があります。そうでなければ、ネットワークに深刻な影響を与えることになります。"
}
},
"sr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Рутер улоге су намењене локацијама са високим положајем као што су врхови планина и куле. Овај чвор мора имати добру директну везу са већином чворова у мрежи, иначе ће то значајно оштетити мрежу."
}
},
"zh-Hant-TW" : {
"stringUnit" : {
"state" : "translated",
"value" : "Router 模式專為高處位置(如山頂或塔台)設計。此節點必須能夠與網路中的大多數節點保持良好的直接連線,否則將會嚴重影響整體網路效能。"
}
}
}
"The Router roles are only for high vantage locations like mountaintops and towers with few nearby nodes, not for use in urban areas. Improper use will hurt your local mesh." : {
},
"The secondary public key authorized to send admin messages to this node." : {
"localizations" : {

View file

@ -1295,7 +1295,7 @@
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1540;
LastUpgradeCheck = 1630;
LastUpgradeCheck = 1640;
TargetAttributes = {
25F5D5C62C4375A8008036E3 = {
CreatedOnToolsVersion = 15.4;
@ -1713,6 +1713,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@ -1778,6 +1779,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@ -1856,7 +1858,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.6.12;
MARKETING_VERSION = 2.6.13;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1889,7 +1891,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.6.12;
MARKETING_VERSION = 2.6.13;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1920,7 +1922,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.12;
MARKETING_VERSION = 2.6.13;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1952,7 +1954,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.6.12;
MARKETING_VERSION = 2.6.13;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View file

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

View file

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

View file

@ -11,10 +11,8 @@ import AppIntents
struct FactoryResetNodeIntent: AppIntent {
static var title: LocalizedStringResource = "Factory Reset"
static var description: IntentDescription = "Perform a factory reset on the node you are connected to"
@Parameter(title: "Hard Reset", description: "In addition to Config, Keys and BLE bonds will be wiped", default: false)
var hardReset: Bool
@Parameter(title: "Provide Confirmation", description: "Show a confirmation dialog before performing the factory reset", default: true)
var provideConfirmation: Bool

View file

@ -15,7 +15,6 @@ struct RestartNodeIntent: AppIntent {
func perform() async throws -> some IntentResult {
if !BLEManager.shared.isConnected {
throw AppIntentErrors.AppIntentError.notConnected
}

View file

@ -11,7 +11,7 @@ import AppIntents
import MeshtasticProtobufs
struct SendWaypointIntent: AppIntent {
var defaultDate = Date.now.addingTimeInterval(60 * 480)
static var title = LocalizedStringResource("Send a Waypoint")
@ -83,11 +83,9 @@ struct SendWaypointIntent: AppIntent {
newWaypoint.icon = unicode
newWaypoint.name = name
newWaypoint.description_p = description
if let expirationDate = expiration {
newWaypoint.expire = UInt32(expirationDate.timeIntervalSince1970)
}
if isLocked {
if let connectedPeripheral = BLEManager.shared.connectedPeripheral {
newWaypoint.lockedTo = UInt32(connectedPeripheral.num)

View file

@ -158,12 +158,13 @@ extension UserDefaults {
@UserDefault(.mapReportingOptIn, defaultValue: false)
static var mapReportingOptIn: Bool
@UserDefault(.usageDataAndCrashReporting, defaultValue: true)
static var usageDataAndCrashReporting: Bool
@UserDefault(.firstLaunch, defaultValue: true)
static var firstLaunch: Bool
@UserDefault(.showDeviceOnboarding, defaultValue: false)
static var showDeviceOnboarding: Bool

View file

@ -818,10 +818,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
nowKnown = true
moduleConfig(config: decodedInfo.moduleConfig, context: context, nodeNum: Int64(truncatingIfNeeded: cp.num), nodeLongName: cp.longName)
if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) {
if decodedInfo.moduleConfig.cannedMessage.enabled {
_ = self.getCannedMessageModuleMessages(destNum: cp.num, wantResponse: true)
}
_ = self.getCannedMessageModuleMessages(destNum: cp.num, wantResponse: true)
}
if decodedInfo.config.payloadVariant == Config.OneOf_PayloadVariant.device(decodedInfo.config.device) {
var dc = decodedInfo.config.device
@ -1145,8 +1142,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
sendWantConfig()
}
// MARK: Share Location Position Update Timer
// Use context to pass the radio name with the timer
// Use a RunLoop to prevent the timer from running on the main UI thread
@ -1162,7 +1157,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == NONCE_ONLY_DB {
Logger.mesh.info("🤜 [BLE] Want Config DB Complete. ID:\(decodedInfo.configCompleteID, privacy: .public)")
}
case FROMNUM_UUID:
Logger.services.info("🗞️ [BLE] (Notify) characteristic value will be read next")
default:

View file

@ -11,15 +11,11 @@ import TipKit
import MeshtasticProtobufs
struct ContactURLHandler {
static var minimumContactVersion = "2.6.9"
static func handleContactUrl(url: URL, bleManager: BLEManager) {
let supportedVersion = UserDefaults.firmwareVersion == "0.0.0" ||
minimumContactVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedAscending ||
minimumContactVersion.compare(UserDefaults.firmwareVersion, options: .numeric) == .orderedSame
if !supportedVersion {
let alertController = UIAlertController(
title: "Firmware Upgrade Required",

View file

@ -46,9 +46,7 @@ struct MeshtasticAppleApp: App {
trackingConsent: UserDefaults.usageDataAndCrashReporting ? .granted : .notGranted,
)
DatadogCrashReporting.CrashReporting.enable()
Logs.enable()
Trace.enable(
with: Trace.Configuration(
sampleRate: 100, networkInfoEnabled: true // 100% sampling for development/testing, reduce for production

View file

@ -50,7 +50,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
case "messageNotification.thumbsUpAction":
if let channel = userInfo["channel"] as? Int32,
let replyID = userInfo["messageId"] as? Int64 {
let tapbackResponse = !BLEManager.shared.sendMessage (
let tapbackResponse = !BLEManager.shared.sendMessage(
message: Tapbacks.thumbsUp.emojiString,
toUserNum: userInfo["userNum"] as? Int64 ?? 0,
channel: channel,
@ -64,7 +64,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
case "messageNotification.thumbsDownAction":
if let channel = userInfo["channel"] as? Int32,
let replyID = userInfo["messageId"] as? Int64 {
let tapbackResponse = !BLEManager.shared.sendMessage (
let tapbackResponse = !BLEManager.shared.sendMessage(
message: Tapbacks.thumbsDown.emojiString,
toUserNum: userInfo["userNum"] as? Int64 ?? 0,
channel: channel,
@ -79,7 +79,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
if let userInput = (response as? UNTextInputNotificationResponse)?.userText,
let channel = userInfo["channel"] as? Int32,
let replyID = userInfo["messageId"] as? Int64 {
let tapbackResponse = !BLEManager.shared.sendMessage (
let tapbackResponse = !BLEManager.shared.sendMessage(
message: userInput,
toUserNum: userInfo["userNum"] as? Int64 ?? 0,
channel: channel,

View file

@ -1369,7 +1369,6 @@ func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nod
do {
try context.save()
Logger.data.info("💾 [RangeTestConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)")
} catch {
context.rollback()
let nsError = error as NSError

View file

@ -7,19 +7,18 @@
import SwiftUI
struct ChannelLock: View {
@ObservedObject var channel: ChannelEntity
var body: some View {
/// Unencrypted - using no key at all or a known 1 byte key
if channel.psk?.hexDescription.count ?? 0 < 3 {
let preciseLoction = 17...32
// Using precise location and have MQTT uplink enabled
if channel.uplinkEnabled && preciseLoction ~= (Int(channel.positionPrecision)) {
if channel.uplinkEnabled && preciseLoction ~= (Int(channel.positionPrecision)) {
Image(systemName: "lock.open.trianglebadge.exclamationmark.fill")
.foregroundColor(.red)
// Using precise location
} else if preciseLoction ~= (Int(channel.positionPrecision)) {
} else if preciseLoction ~= (Int(channel.positionPrecision)) {
Image(systemName: "lock.open.fill")
.foregroundColor(.red)
// Just unencrypted without any location or MQTT

View file

@ -54,13 +54,13 @@ struct ConnectedDevice: View {
} else {
// Create a container for Bluetooth off state
HStack {
Text("bluetooth.off".localized)
Text("Bluetooth is off".localized)
.font(.subheadline)
.foregroundColor(.red)
.accessibilityHidden(true)
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("bluetooth.off".localized)
.accessibilityLabel("Bluetooth is off".localized)
}
}
}

View file

@ -10,9 +10,7 @@ struct MessageText: View {
locale: Locale.current
)
static let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mm:ss:a")
@Environment(\.managedObjectContext) var context
let message: MessageEntity
let tapBackDestination: MessageDestination
let isCurrentUser: Bool
@ -21,7 +19,6 @@ struct MessageText: View {
@State private var saveChannels = false
@State private var channelSettings: String?
@State private var addChannels = false
@State private var isShowingDeleteConfirmation = false
var body: some View {
@ -41,10 +38,10 @@ struct MessageText: View {
HStack {
Spacer()
Image(systemName: "lock.circle.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .green)
.font(.system(size: 20))
.offset(x: 8, y: 8)
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .green)
.font(.system(size: 20))
.offset(x: 8, y: 8)
}
}
}
@ -56,10 +53,10 @@ struct MessageText: View {
HStack {
Spacer()
Image(systemName: "envelope.circle.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .gray)
.font(.system(size: 20))
.offset(x: 8, y: 8)
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .gray)
.font(.system(size: 20))
.offset(x: 8, y: 8)
}
}
}
@ -89,39 +86,32 @@ struct MessageText: View {
}
.environment(\.openURL, OpenURLAction { url in
channelSettings = nil
if url.absoluteString.lowercased().contains("meshtastic.org/v/#") {
// Handle contact URL
ContactURLHandler.handleContactUrl(url: url, bleManager: BLEManager.shared)
return .handled // Prevent default browser opening
} else if url.absoluteString.lowercased().contains("meshtastic.org/e/") {
// Handle channel URL
let components = url.absoluteString.components(separatedBy: "#")
guard !components.isEmpty, let lastComponent = components.last else {
Logger.services.error("No valid components found in channel URL: \(url.absoluteString, privacy: .public)")
return .discarded
}
self.addChannels = Bool(url.query?.contains("add=true") ?? false)
guard let lastComponent = components.last else {
Logger.services.error("Channel URL missing fragment component: \(url.absoluteString, privacy: .public)")
self.channelSettings = nil
return .discarded
}
self.channelSettings = lastComponent.components(separatedBy: "?").first ?? ""
Logger.services.debug("Add Channel: \(self.addChannels, privacy: .public)")
self.saveChannels = true
Logger.mesh.debug("Opening Channel Settings URL: \(url.absoluteString, privacy: .public)")
return .handled // Prevent default browser opening
}
return .systemAction // Open other URLs in browser
})
// Display sheet for channel settings
if url.absoluteString.lowercased().contains("meshtastic.org/v/#") {
// Handle contact URL
ContactURLHandler.handleContactUrl(url: url, bleManager: BLEManager.shared)
return .handled // Prevent default browser opening
} else if url.absoluteString.lowercased().contains("meshtastic.org/e/") {
// Handle channel URL
let components = url.absoluteString.components(separatedBy: "#")
guard !components.isEmpty, let lastComponent = components.last else {
Logger.services.error("No valid components found in channel URL: \(url.absoluteString, privacy: .public)")
return .discarded
}
self.addChannels = Bool(url.query?.contains("add=true") ?? false)
guard let lastComponent = components.last else {
Logger.services.error("Channel URL missing fragment component: \(url.absoluteString, privacy: .public)")
self.channelSettings = nil
return .discarded
}
self.channelSettings = lastComponent.components(separatedBy: "?").first ?? ""
Logger.services.debug("Add Channel: \(self.addChannels, privacy: .public)")
self.saveChannels = true
Logger.mesh.debug("Opening Channel Settings URL: \(url.absoluteString, privacy: .public)")
return .handled // Prevent default browser opening
}
return .systemAction // Open other URLs in browser
})
// Display sheet for channel settings
.sheet(isPresented: Binding(
get: {
saveChannels && !(channelSettings == nil)

View file

@ -19,22 +19,17 @@ struct NodeDetail: View {
var modemPreset: ModemPresets = ModemPresets(
rawValue: UserDefaults.modemPreset
) ?? ModemPresets.longFast
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@State private var showingShutdownConfirm: Bool = false
@State private var showingRebootConfirm: Bool = false
@State private var dateFormatRelative: Bool = true
// The node the device is currently connected to
var connectedNode: NodeInfoEntity?
// The node information being displayed on the detail screen
@ObservedObject
var node: NodeInfoEntity
var columnVisibility = NavigationSplitViewVisibility.all
var body: some View {
NavigationStack {
List {
@ -42,7 +37,6 @@ struct NodeDetail: View {
id: bleManager.connectedPeripheral?.num ?? -1,
context: context
)
Section("Hardware") {
NodeInfoItem(node: node)
}
@ -106,10 +100,9 @@ struct NodeDetail: View {
}
Spacer()
Text(String(node.num))
.textSelection(.enabled)
.textSelection(.enabled)
}
.accessibilityElement(children: .combine)
HStack {
Label {
Text("User Id")
@ -119,10 +112,9 @@ struct NodeDetail: View {
}
Spacer()
Text(node.num.toHex())
.textSelection(.enabled)
.textSelection(.enabled)
}
.accessibilityElement(children: .combine)
if node.user?.keyMatch ?? false {
if let publicKey = node.user?.publicKey {
HStack {
@ -134,7 +126,7 @@ struct NodeDetail: View {
}
Spacer()
Button(action: {
context.perform{
context.perform {
UIPasteboard.general.string = publicKey.base64EncodedString()
}
}) {
@ -147,7 +139,6 @@ struct NodeDetail: View {
.accessibilityElement(children: .combine)
}
}
if let metadata = node.metadata {
HStack {
Label {
@ -157,12 +148,10 @@ struct NodeDetail: View {
.symbolRenderingMode(.multicolor)
}
Spacer()
Text(metadata.firmwareVersion ?? "Unknown".localized)
}
.accessibilityElement(children: .combine)
}
if let role = node.user?.role, let deviceRole = DeviceRoles(rawValue: Int(role)) {
HStack {
Label {
@ -189,7 +178,6 @@ struct NodeDetail: View {
}
.accessibilityElement(children: .combine)
}
if let dm = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity, let uptimeSeconds = dm.uptimeSeconds {
HStack {
Label {
@ -200,7 +188,6 @@ struct NodeDetail: View {
.symbolRenderingMode(.hierarchical)
}
Spacer()
let now = Date.now
let later = now + TimeInterval(uptimeSeconds)
let uptime = (now..<later).formatted(.components(style: .narrow))
@ -209,7 +196,6 @@ struct NodeDetail: View {
}
.accessibilityElement(children: .combine)
}
if let firstHeard = node.firstHeard, firstHeard.timeIntervalSince1970 > 0 && firstHeard < Calendar.current.date(byAdding: .year, value: 1, to: Date())! {
HStack {
Label {
@ -232,7 +218,6 @@ struct NodeDetail: View {
dateFormatRelative.toggle()
}
}
if let lastHeard = node.lastHeard, lastHeard.timeIntervalSince1970 > 0 && lastHeard < Calendar.current.date(byAdding: .year, value: 1, to: Date())! {
HStack {
Label {
@ -242,7 +227,6 @@ struct NodeDetail: View {
.symbolRenderingMode(.multicolor)
}
Spacer()
if dateFormatRelative, let text = Self.relativeFormatter.string(for: lastHeard) {
if lastHeard.formatted() != "Unknown Age".localized {
Text(text)
@ -259,7 +243,6 @@ struct NodeDetail: View {
}
}
}
// Note, as you add widgets, you should add to the `hasDataForLatestPositions` array
// This will make sure the "Environment" section is only displayed when the node has a position
// to use with WeatherKit, or has actual data in the most recent EnvironmentMetrics entity
@ -298,7 +281,7 @@ struct NodeDetail: View {
let windGust = node.latestEnvironmentMetrics?.windGust.map { Measurement(value: Double($0), unit: UnitSpeed.metersPerSecond) }
let direction = cardinalValue(from: Double(node.latestEnvironmentMetrics?.windDirection ?? 0))
WindCompactWidget(speed: windSpeedMeasurement.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))),
gust: node.latestEnvironmentMetrics?.windGust ?? 0.0 > 0.0 ? windGust?.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))) : "", direction: direction)
gust: node.latestEnvironmentMetrics?.windGust ?? 0.0 > 0.0 ? windGust?.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))) : "", direction: direction)
}
if let rainfall1h = node.latestEnvironmentMetrics?.rainfall1H {
let locale = NSLocale.current as NSLocale
@ -370,7 +353,6 @@ struct NodeDetail: View {
}
}
.disabled(!node.hasDeviceMetrics)
NavigationLink {
NodeMapSwiftUI(node: node, showUserLocation: connectedNode?.num ?? 0 == node.num)
} label: {
@ -382,7 +364,6 @@ struct NodeDetail: View {
}
}
.disabled(!node.hasPositions)
NavigationLink {
PositionLog(node: node)
} label: {
@ -394,7 +375,6 @@ struct NodeDetail: View {
}
}
.disabled(!node.hasPositions)
NavigationLink {
EnvironmentMetricsLog(node: node)
} label: {
@ -406,7 +386,6 @@ struct NodeDetail: View {
}
}
.disabled(!node.hasEnvironmentMetrics)
NavigationLink {
TraceRouteLog(node: node)
} label: {
@ -418,7 +397,6 @@ struct NodeDetail: View {
}
}
.disabled(node.traceRoutes?.count ?? 0 == 0)
NavigationLink {
PowerMetricsLog(node: node)
} label: {
@ -430,7 +408,6 @@ struct NodeDetail: View {
}
}
.disabled(!node.hasPowerMetrics)
NavigationLink {
DetectionSensorLog(node: node)
} label: {
@ -442,7 +419,6 @@ struct NodeDetail: View {
}
}
.disabled(!node.hasDetectionSensorMetrics)
if node.hasPax {
NavigationLink {
PaxCounterLog(node: node)
@ -457,7 +433,6 @@ struct NodeDetail: View {
.disabled(!node.hasPax)
}
}
Section("Actions") {
if let user = node.user {
NodeAlertsButton(
@ -466,7 +441,6 @@ struct NodeDetail: View {
user: user
)
}
if let connectedNode {
FavoriteNodeButton(
bleManager: bleManager,
@ -491,7 +465,7 @@ struct NodeDetail: View {
}
if node.hasPositions {
NavigateToButton(node: node)
}
}
IgnoreNodeButton(
bleManager: bleManager,
context: context,
@ -506,7 +480,6 @@ struct NodeDetail: View {
}
}
}
if let metadata = node.metadata,
let connectedNode,
self.bleManager.connectedPeripheral != nil {
@ -529,7 +502,6 @@ struct NodeDetail: View {
}
}
}
if metadata.canShutdown {
Button {
showingShutdownConfirm = true
@ -549,7 +521,6 @@ struct NodeDetail: View {
}
}
}
Button {
showingRebootConfirm = true
} label: {

View file

@ -32,7 +32,7 @@ struct DeviceOnboarding: View {
var welcomeView: some View {
VStack {
ScrollView(.vertical, showsIndicators: false) {
ScrollView(.vertical) {
VStack {
// Title
title
@ -78,110 +78,114 @@ struct DeviceOnboarding: View {
}
var notificationView: some View {
VStack {
ScrollView(.vertical) {
VStack {
Text("App Notifications")
.font(.largeTitle.bold())
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
VStack(alignment: .leading, spacing: 16) {
Text("Send Notifications")
.font(.title2.bold())
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
makeRow(
icon: "message",
title: "Incoming Messages",
subtitle: "Meshtastic notifications for channel messages and direct messages"
)
makeRow(
icon: "flipphone",
title: "New Nodes",
subtitle: "Allow Meshtastic to send notifications for messages, newly discovered nodes and low battery alerts for the connected device."
)
makeRow(
icon: "battery.25percent",
title: "Low Battery",
subtitle: "Allow Meshtastic to send notifications for messages, newly discovered nodes and low battery alerts for the connected device."
)
Text("Critical Alerts")
.font(.title2.bold())
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
makeRow(
icon: "exclamationmark.triangle.fill",
subtitle: "Select packets sent as critical will ignore the mute switch and Do Not Disturb settings in the OS notification center."
)
}
.padding()
Spacer()
Button {
Task {
await requestNotificationsPermissions()
await goToNextStep(after: .notifications)
VStack {
Text("App Notifications")
.font(.largeTitle.bold())
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
}
} label: {
Text("Configure notification permissions")
.frame(maxWidth: .infinity)
Spacer()
VStack(alignment: .leading, spacing: 16) {
Text("Send Notifications")
.font(.title2.bold())
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
makeRow(
icon: "message",
title: "Incoming Messages",
subtitle: "Meshtastic notifications for channel messages and direct messages"
)
makeRow(
icon: "flipphone",
title: "New Nodes",
subtitle: "Allow Meshtastic to send notifications for messages, newly discovered nodes and low battery alerts for the connected device."
)
makeRow(
icon: "battery.25percent",
title: "Low Battery",
subtitle: "Allow Meshtastic to send notifications for messages, newly discovered nodes and low battery alerts for the connected device."
)
Text("Critical Alerts")
.font(.title2.bold())
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
makeRow(
icon: "exclamationmark.triangle.fill",
subtitle: "Select packets sent as critical will ignore the mute switch and Do Not Disturb settings in the OS notification center."
)
}
.padding()
Spacer()
Button {
Task {
await requestNotificationsPermissions()
await goToNextStep(after: .notifications)
}
} label: {
Text("Configure notification permissions")
.frame(maxWidth: .infinity)
}
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.buttonStyle(.borderedProminent)
}
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.buttonStyle(.borderedProminent)
}
}
var locationView: some View {
VStack {
ScrollView(.vertical) {
VStack {
Text("Phone Location")
.font(.largeTitle.bold())
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
}
VStack(alignment: .leading, spacing: 16) {
Text("Meshtastic uses your phone's location to enable a number of features. You can update your location permissions at any time from Settings > App Settings > Open Settings.")
.font(.body.bold())
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
makeRow(
icon: "location",
title: "Share Location",
subtitle: "Use your phone GPS to send locations to your node to instead of using a hardware GPS on your node."
)
makeRow(
icon: "lines.measurement.horizontal",
title: "Distance Measurements",
subtitle: "Used to display the distance between your phone and other Meshtastic nodes where positions are available."
)
makeRow(
icon: "line.3.horizontal.decrease.circle",
title: "Distance Filters",
subtitle: "Filter the node list and mesh map based on proximity to your phone."
)
makeRow(
icon: "mappin",
title: "Mesh Map Location",
subtitle: "Enables the blue location dot for your phone in the mesh map."
)
}
.padding()
Spacer()
Button {
Task {
await requestLocationPermissions()
VStack {
Text("Phone Location")
.font(.largeTitle.bold())
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
}
} label: {
Text("Configure Location Permissions")
.frame(maxWidth: .infinity)
VStack(alignment: .leading, spacing: 16) {
Text("Meshtastic uses your phone's location to enable a number of features. You can update your location permissions at any time from Settings > App Settings > Open Settings.")
.font(.body.bold())
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
makeRow(
icon: "location",
title: "Share Location",
subtitle: "Use your phone GPS to send locations to your node to instead of using a hardware GPS on your node."
)
makeRow(
icon: "lines.measurement.horizontal",
title: "Distance Measurements",
subtitle: "Used to display the distance between your phone and other Meshtastic nodes where positions are available."
)
makeRow(
icon: "line.3.horizontal.decrease.circle",
title: "Distance Filters",
subtitle: "Filter the node list and mesh map based on proximity to your phone."
)
makeRow(
icon: "mappin",
title: "Mesh Map Location",
subtitle: "Enables the blue location dot for your phone in the mesh map."
)
}
.padding()
Spacer()
Button {
Task {
await requestLocationPermissions()
}
} label: {
Text("Configure Location Permissions")
.frame(maxWidth: .infinity)
}
.padding()
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.buttonStyle(.borderedProminent)
}
.padding()
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.buttonStyle(.borderedProminent)
}
}

View file

@ -241,7 +241,6 @@ struct Channels: View {
#endif
}
}
if node?.myInfo?.channels?.array.count ?? 0 < 8 && node != nil {
Button {

View file

@ -21,7 +21,6 @@ struct DeviceConfig: View {
@State private var isPresentingFactoryResetConfirm = false
@State var hasChanges = false
@State var deviceRole = 0
@State private var pendingDeviceRole = 0
@State var buzzerGPIO = 0
@State var buttonGPIO = 0
@State var rebroadcastMode = 0
@ -31,7 +30,6 @@ struct DeviceConfig: View {
@State var tripleClickAsAdHocPing = true
@State var tzdef = ""
@State private var showRouterWarning = false
@State private var confirmWarning = false
var body: some View {
VStack {
@ -45,15 +43,9 @@ struct DeviceConfig: View {
Text(dr.name).tag(dr.rawValue as Int)
}
}
.onChange(of: deviceRole) { oldValue, newValue in
if !confirmWarning {
if [2, 11].contains(newValue) {
pendingDeviceRole = newValue
deviceRole = oldValue // Reset selection until confirmed
showRouterWarning = true
}
} else {
confirmWarning = false
.onChange(of: deviceRole) { _, newRole in
if hasChanges && [DeviceRoles.router.rawValue, DeviceRoles.routerLate.rawValue, DeviceRoles.repeater.rawValue].contains(newRole) {
showRouterWarning = true
}
}
.confirmationDialog(
@ -63,15 +55,13 @@ struct DeviceConfig: View {
) {
Button("Confirm") {
deviceRole = pendingDeviceRole
pendingDeviceRole = 0
confirmWarning = true
hasChanges = true
}
Button("Cancel", role: .cancel) {
pendingDeviceRole = 0
setDeviceValues()
}
} message: {
Text("The Router roles are designed for high vantage locations like mountaintops and towers. This node needs to be able to have a good direct connection to most of the nodes on the network or else this will significantly hurt the network.")
Text("The Router roles are only for high vantage locations like mountaintops and towers with few nearby nodes, not for use in urban areas. Improper use will hurt your local mesh.")
}
Text(DeviceRoles(rawValue: deviceRole)?.description ?? "")
.foregroundColor(.gray)
@ -334,5 +324,6 @@ struct DeviceConfig: View {
self.tripleClickAsAdHocPing = node?.deviceConfig?.tripleClickAsAdHocPing ?? false
self.ledHeartbeatEnabled = node?.deviceConfig?.ledHeartbeatEnabled ?? true
self.tzdef = node?.deviceConfig?.tzdef ?? ""
hasChanges = false
}
}

View file

@ -12,18 +12,14 @@ import MeshtasticProtobufs
struct SaveChannelQRCode: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.managedObjectContext) var context
let channelSetLink: String
var addChannels: Bool = false
var bleManager: BLEManager
@State private var showError: Bool = false
@State private var errorMessage: String = ""
@State private var connectedToDevice: Bool = false
@State private var loraChanges: [String] = []
@State private var okToMQTT: Bool = false
var body: some View {
VStack {
Text("\(addChannels ? "Add" : "Replace all") Channels?")
@ -47,7 +43,6 @@ struct SaveChannelQRCode: View {
}
.padding()
}
if showError {
Text(errorMessage.isEmpty ? "Channels being added from the QR code did not save. When adding channels the names must be unique." : errorMessage)
.fixedSize(horizontal: false, vertical: true)
@ -55,7 +50,6 @@ struct SaveChannelQRCode: View {
.font(.callout)
.padding()
}
HStack {
if !showError {
Button {
@ -72,7 +66,6 @@ struct SaveChannelQRCode: View {
} else {
channelData = channelSetLink
}
let success = bleManager.saveChannelSet(base64UrlString: channelData, addChannels: addChannels, okToMQTT: okToMQTT)
if success {
dismiss()
@ -119,11 +112,8 @@ struct SaveChannelQRCode: View {
fetchLoRaConfigChanges()
}
}
private func extractChannelDataFromURL(_ urlString: String) -> String? {
Logger.data.info("Extracting channel data from URL: \(urlString)")
if let url = URL(string: urlString) {
// Get the fragment (part after #)
if let fragment = url.fragment, !fragment.isEmpty {
@ -131,7 +121,6 @@ struct SaveChannelQRCode: View {
return fragment
}
}
// Fallback: manually extract everything after the last #
if let hashIndex = urlString.lastIndex(of: "#") {
let startIndex = urlString.index(after: hashIndex)
@ -141,11 +130,9 @@ struct SaveChannelQRCode: View {
return channelData
}
}
Logger.data.error("Failed to extract channel data from URL: \(urlString)")
return nil
}
private func fetchLoRaConfigChanges() {
var currentLoRaConfig: Config.LoRaConfig?
@ -163,13 +150,10 @@ struct SaveChannelQRCode: View {
// Assume it's already the base64 data
channelData = channelSetLink
}
Logger.data.info("Processing channel data: \(channelData)")
// Fetch current LoRa config from Core Data
let fetchRequest = NodeInfoEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(bleManager.connectedPeripheral?.num ?? 0))
do {
let nodes = try context.fetch(fetchRequest)
if let node = nodes.first {
@ -178,7 +162,6 @@ struct SaveChannelQRCode: View {
} catch {
Logger.data.error("Failed to fetch NodeInfoEntity: \(error.localizedDescription, privacy: .public)")
}
// Decode base64url string
let decodedString = channelData.base64urlToBase64()
guard let decodedData = Data(base64Encoded: decodedString) else {
@ -187,7 +170,6 @@ struct SaveChannelQRCode: View {
showError = true
return
}
do {
let channelSet = try ChannelSet(serializedBytes: decodedData)
let newLoRaConfig = channelSet.loraConfig
@ -244,7 +226,6 @@ struct SaveChannelQRCode: View {
} else {
// Compare against default values when no current config exists
let defaultConfig = getDefaultLoRaConfig()
if newLoRaConfig.hopLimit != defaultConfig.hopLimit {
changes.append("Hop Limit: \(defaultConfig.hopLimit) -> \(newLoRaConfig.hopLimit)")
}
@ -287,16 +268,13 @@ struct SaveChannelQRCode: View {
changes.append("Ignore MQTT: \(defaultConfig.ignoreMqtt) -> \(newLoRaConfig.ignoreMqtt)")
}
}
loraChanges = changes
} catch {
Logger.data.error("Failed to decode ChannelSet: \(error.localizedDescription, privacy: .public)")
errorMessage = "Failed to decode channel configuration"
showError = true
}
}
private func getDefaultLoRaConfig() -> Config.LoRaConfig {
var config = Config.LoRaConfig()
config.hopLimit = 3
@ -316,7 +294,6 @@ struct SaveChannelQRCode: View {
return config
}
}
extension LoRaConfigEntity {
func toProto() -> Config.LoRaConfig {
var config = Config.LoRaConfig()

View file

@ -62,7 +62,7 @@
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Smart Position</string>
<string>Accurate Locations Only</string>
<key>Key</key>
<string>enableSmartPosition</string>
<key>DefaultValue</key>