Merge branch '2.5.19' into cleanup-nodelist-view

This commit is contained in:
Garth Vander Houwen 2025-02-11 08:13:47 -08:00 committed by GitHub
commit 2e7e0da729
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 4904 additions and 119 deletions

1
.gitignore vendored
View file

@ -4,6 +4,7 @@
## User settings
xcuserdata/
SupportingFiles/
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint

File diff suppressed because it is too large Load diff

View file

@ -486,6 +486,7 @@
DDCDC69A29467643004C1DDA /* MeshtasticDataModelV3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV3.xcdatamodel; sourceTree = "<group>"; };
DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConfig.swift; sourceTree = "<group>"; };
DDD28D372C0CD2670063CFA3 /* MeshtasticDataModelV 37.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 37.xcdatamodel"; sourceTree = "<group>"; };
DDD3A2B22D5127CF0045EB48 /* ci_pre_xcodebuild.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = ci_pre_xcodebuild.sh; sourceTree = "<group>"; };
DDD43FE22A78C8900083A3E9 /* MqttClientProxyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MqttClientProxyManager.swift; sourceTree = "<group>"; };
DDD5BB082C285DDC007E03CA /* AppLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLog.swift; sourceTree = "<group>"; };
DDD5BB0A2C285E45007E03CA /* LogDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDetail.swift; sourceTree = "<group>"; };
@ -917,6 +918,7 @@
DDC2E14B26CE248E0042C5E4 = {
isa = PBXGroup;
children = (
DDD3A2B12D5127B40045EB48 /* ci_scripts */,
DDDBC87A2BC62E4E001E8DF7 /* Settings.bundle */,
25AECD4E2C2F723200862C8E /* Localizable.xcstrings */,
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */,
@ -1073,6 +1075,14 @@
path = Persistence;
sourceTree = "<group>";
};
DDD3A2B12D5127B40045EB48 /* ci_scripts */ = {
isa = PBXGroup;
children = (
DDD3A2B22D5127CF0045EB48 /* ci_pre_xcodebuild.sh */,
);
path = ci_scripts;
sourceTree = "<group>";
};
DDD43FE12A78C86B0083A3E9 /* Mqtt */ = {
isa = PBXGroup;
children = (
@ -1776,7 +1786,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.5.18;
MARKETING_VERSION = 2.5.19;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1810,7 +1820,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.5.18;
MARKETING_VERSION = 2.5.19;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1842,7 +1852,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.5.18;
MARKETING_VERSION = 2.5.19;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1875,7 +1885,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.5.18;
MARKETING_VERSION = 2.5.19;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View file

@ -78,10 +78,15 @@
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "publicMqttUsername"
key = "PUBLIC_MQTT_USERNAME"
value = "meshdev"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "PUBLIC_MQTT_PASSWORD"
value = "large4cats"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction

View file

@ -0,0 +1,61 @@
//
// NavigateToNodeIntent.swift
// Meshtastic
//
// Created by Benjamin Faershtein on 2/8/25.
//
import Foundation
import AppIntents
import CoreLocation
import CoreData
import UIKit
@available(iOS 16.4, *)
struct NavigateToNodeIntent: ForegroundContinuableIntent {
static var title: LocalizedStringResource = "Navigate to Node Position"
static var openAppWhenRun: Bool = false
@Parameter(title: "Node Number")
var nodeNum: Int
@MainActor
func perform() async throws -> some IntentResult & ProvidesDialog {
if !BLEManager.shared.isConnected {
throw AppIntentErrors.AppIntentError.notConnected
}
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "NodeInfoEntity")
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
do {
guard let fetchedNode = try PersistenceController.shared.container.viewContext.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity],
fetchedNode.count == 1 else {
throw $nodeNum.needsValueError("Could not find node")
}
let nodeInfo = fetchedNode[0]
if let latitude = nodeInfo.latestPosition?.coordinate.latitude,
let longitude = nodeInfo.latestPosition?.coordinate.longitude {
let url = URL(string: "maps://?saddr=&daddr=\(latitude),\(longitude)")
if let mapURL = url, UIApplication.shared.canOpenURL(mapURL) {
// Request to continue in foreground before opening the app
try await requestToContinueInForeground()
// Open Apple Maps for navigation
UIApplication.shared.open(mapURL, options: [:], completionHandler: nil)
return .result(dialog: "Navigating to node location.")
} else {
throw AppIntentErrors.AppIntentError.message("Unable to open Apple Maps.")
}
} else {
throw AppIntentErrors.AppIntentError.message("Node does not have a recorded position.")
}
} catch {
throw AppIntentErrors.AppIntentError.message("Failed to fetch node data.")
}
}
}

View file

@ -8,14 +8,6 @@
import Foundation
import Charts
extension Measurement where UnitType == UnitAngle {
func reciprocal() -> Measurement {
var recip = self.converted(to: .degrees)
recip.value = (recip.value + 180).truncatingRemainder(dividingBy: 360)
return recip.converted(to: self.unit)
}
}
struct PlottableMeasurement<UnitType: Unit> {
var measurement: Measurement<UnitType>
}

View file

@ -145,6 +145,5 @@ import OSLog
}
return sats
}
}

View file

@ -682,7 +682,6 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
let logString = String.localizedStringWithFormat("mesh.log.telemetry.received %@".localized, String(packet.from))
MeshLogger.log("📈 \(logString)")
if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) && telemetryMessage.variant != Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) {
/// Other unhandled telemetry packets
return
@ -739,7 +738,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
Logger.statistics.info("📈 [Mesh Statistics] Channel Utilization: \(telemetryMessage.localStats.channelUtilization, privacy: .public) Airtime: \(telemetryMessage.localStats.airUtilTx, privacy: .public) Packets Sent: \(telemetryMessage.localStats.numPacketsTx, privacy: .public) Packets Received: \(telemetryMessage.localStats.numPacketsRx, privacy: .public) Bad Packets Received: \(telemetryMessage.localStats.numPacketsRxBad, privacy: .public) Nodes Online: \(telemetryMessage.localStats.numOnlineNodes, privacy: .public) of \(telemetryMessage.localStats.numTotalNodes, privacy: .public) nodes for Node: \(packet.from.toHex(), privacy: .public)")
} else if telemetryMessage.variant == Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) {
Logger.data.info("📈 [Power Metrics] Received for Node: \(packet.from.toHex(), privacy: .public)")
if telemetryMessage.powerMetrics.hasCh1Voltage {
telemetry.powerCh1Voltage = telemetryMessage.powerMetrics.ch1Voltage
telemetry.metricsType = 2

View file

@ -37,17 +37,15 @@ class MqttClientProxyManager {
defaultServerPort = Int(fullHost.components(separatedBy: ":")[1]) ?? (useSsl ? 8883 : 1883)
}
}
if let host = host {
let port = defaultServerPort
var username = node.mqttConfig?.username
var password = node.mqttConfig?.password
if host == defaultServerAddress {
// username = ProcessInfo.processInfo.environment["publicMqttUsername"]
// password = ProcessInfo.processInfo.environment["publicMqttPsk"]
useSsl = false
}
// if host == defaultServerAddress {
//username = ProcessInfo.processInfo.environment["PUBLIC_MQTT_USERNAME"]
//password = ProcessInfo.processInfo.environment["PUBLIC_MQTT_PASSWORD"]
// }
let root = node.mqttConfig?.root?.count ?? 0 > 0 ? node.mqttConfig?.root : "msh"
let prefix = root!
topic = prefix + "/2/e" + "/#"

View file

@ -17,8 +17,7 @@ struct PowerMetrics: View {
LazyVGrid(columns: gridItemLayout) {
if(metric.powerCh1Voltage != nil) {
if metric.powerCh1Voltage != nil {
PowerMetricCompactWidget(
type: .voltage,
value: metric.powerCh1Voltage,
@ -26,7 +25,7 @@ struct PowerMetrics: View {
)
}
if(metric.powerCh1Current != nil) {
if metric.powerCh1Current != nil {
PowerMetricCompactWidget(
type: .current,
value: metric.powerCh1Current,
@ -34,7 +33,7 @@ struct PowerMetrics: View {
)
}
if(metric.powerCh2Voltage != nil) {
if metric.powerCh2Voltage != nil {
PowerMetricCompactWidget(
type: .voltage,
value: metric.powerCh2Voltage,
@ -42,7 +41,7 @@ struct PowerMetrics: View {
)
}
if(metric.powerCh2Current != nil) {
if metric.powerCh2Current != nil {
PowerMetricCompactWidget(
type: .current,
value: metric.powerCh2Current,
@ -50,7 +49,7 @@ struct PowerMetrics: View {
)
}
if(metric.powerCh3Voltage != nil) {
if metric.powerCh3Voltage != nil {
PowerMetricCompactWidget(
type: .voltage,
value: metric.powerCh3Voltage,
@ -58,7 +57,7 @@ struct PowerMetrics: View {
)
}
if(metric.powerCh3Current != nil) {
if metric.powerCh3Current != nil {
PowerMetricCompactWidget(
type: .current,
value: metric.powerCh3Current,

View file

@ -127,13 +127,17 @@ struct ChannelMessageList: View {
}
}
}
.padding([.top])
.scrollDismissesKeyboard(.immediately)
.scrollDismissesKeyboard(.interactively)
.onFirstAppear {
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
}
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
}
}
.onChange(of: channel.allPrivateMessages) {
withAnimation {
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)

View file

@ -115,13 +115,17 @@ struct UserMessageList: View {
}
}
}
.padding([.top])
.scrollDismissesKeyboard(.immediately)
.scrollDismissesKeyboard(.interactively)
.onFirstAppear {
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
}
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
}
}
.onChange(of: user.messageList) {
withAnimation {
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)

View file

@ -0,0 +1,58 @@
//
// NavigateToButton.swift
// Meshtastic
//
// Created by Benjamin Faershtein on 2/8/25.
//
import SwiftUI
import CoreLocation
import CoreData
import OSLog
struct NavigateToButton: View {
var node: NodeInfoEntity
var body: some View {
Button {
guard let userNum = node.user?.num else {
Logger.services.error("NavigateToAction: Selected node does not exist")
return
}
Logger.services.info("Fetching NodeInfoEntity for userNum: \(userNum)")
let fetchRequest: NSFetchRequest<NodeInfoEntity> = NSFetchRequest(entityName: "NodeInfoEntity")
fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(userNum))
do {
let fetchedNodes = try PersistenceController.shared.container.viewContext.fetch(fetchRequest)
guard let nodeInfo = fetchedNodes.first else {
Logger.services.error("NavigateToAction: Node with userNum \(userNum) not found in Core Data")
return
}
if let latitude = nodeInfo.latestPosition?.latitude,
let longitude = nodeInfo.latestPosition?.longitude {
if let url = URL(string: "maps://?saddr=&daddr=\(latitude),\(longitude)") {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else {
Logger.services.error("Failed to create URL for navigation")
}
} else {
Logger.services.warning("NavigateToAction: Node \(userNum) has invalid or missing coordinates")
}
} catch {
Logger.services.error("NavigateToAction: Failed to fetch node with userNum \(userNum): \(error.localizedDescription)")
}
} label: {
Label {
Text("Navigate to node")
} icon: {
Image(systemName: "map")
.symbolRenderingMode(.hierarchical)
}
}
}
}

View file

@ -143,7 +143,7 @@ struct PositionPopover: View {
/// Heading
let degrees = Angle.degrees(Double(position.heading))
Label {
let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees).reciprocal()
let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees)
Text("Heading: \(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))")
} icon: {
Image(systemName: "location.north")

View file

@ -260,7 +260,7 @@ struct NodeDetail: View {
}
.disabled(!node.hasDeviceMetrics)
NavigationLink{
NavigationLink {
PowerMetricsLog(node: node)
} label: {
Label {
@ -378,6 +378,9 @@ struct NodeDetail: View {
node: node
)
}
if node.hasPositions {
NavigateToButton(node: node)
}
IgnoreNodeButton(
bleManager: bleManager,
context: context,

View file

@ -52,8 +52,9 @@ struct PositionLog: View {
}
TableColumn("Heading") { position in
let degrees = Angle.degrees(Double(position.heading))
let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees).reciprocal()
let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees)
Text(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))
.textSelection(.enabled)
}
TableColumn("SNR") { position in
Text("\(String(format: "%.2f", position.snr)) dB")
@ -63,6 +64,8 @@ struct PositionLog: View {
}
.width(min: 180)
}
.textSelection(.enabled)
} else {
ScrollView {

View file

@ -64,7 +64,7 @@ struct MQTTConfig: View {
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
if enabled && proxyToClientEnabled && node!.mqttConfig!.proxyToClientEnabled == true {
if enabled && proxyToClientEnabled && node?.mqttConfig?.proxyToClientEnabled ?? false == true {
Toggle(isOn: $mqttConnected) {
Label(mqttConnected ? "mqtt.disconnect".localized : "mqtt.connect".localized, systemImage: "server.rack")
if bleManager.mqttError.count > 0 {
@ -194,6 +194,7 @@ struct MQTTConfig: View {
}
.keyboardType(.default)
.scrollDismissesKeyboard(.interactively)
HStack {
Label("password", systemImage: "wallet.pass")
TextField("password", text: $password)
@ -214,11 +215,13 @@ struct MQTTConfig: View {
.keyboardType(.default)
.scrollDismissesKeyboard(.interactively)
.listRowSeparator(/*@START_MENU_TOKEN@*/.visible/*@END_MENU_TOKEN@*/)
Toggle(isOn: $tlsEnabled) {
Label("TLS Enabled", systemImage: "checkmark.shield.fill")
Text("Your MQTT Server must support TLS.")
if !proxyToClientEnabled {
Toggle(isOn: $tlsEnabled) {
Label("TLS Enabled", systemImage: "checkmark.shield.fill")
Text("Your MQTT Server must support TLS.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
}
Text("For all Mqtt functionality other than the map report you must also set uplink and downlink for each channel you want to bridge over Mqtt.")
@ -269,6 +272,7 @@ struct MQTTConfig: View {
.onChange(of: proxyToClientEnabled) { _, newProxyToClientEnabled in
if newProxyToClientEnabled {
jsonEnabled = false
tlsEnabled = false
}
if newProxyToClientEnabled != node?.mqttConfig?.proxyToClientEnabled { hasChanges = true }
}

View file

@ -22,7 +22,7 @@ struct GPSStatus: View {
let altitiude = Measurement(value: newLocation.altitude, unit: UnitLength.meters)
let speed = Measurement(value: newLocation.speed, unit: UnitSpeed.kilometersPerHour)
let speedAccuracy = Measurement(value: newLocation.speedAccuracy, unit: UnitSpeed.metersPerSecond)
let courseAccuracy = Measurement(value: newLocation.courseAccuracy, unit: UnitAngle.degrees).reciprocal()
let courseAccuracy = Measurement(value: newLocation.courseAccuracy, unit: UnitAngle.degrees)
Label("Coordinate \(String(format: "%.5f", newLocation.coordinate.latitude)), \(String(format: "%.5f", newLocation.coordinate.longitude))", systemImage: "mappin")
.font(largeFont)
@ -45,7 +45,7 @@ struct GPSStatus: View {
HStack {
let degrees = Angle.degrees(newLocation.course)
Label {
let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees).reciprocal()
let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees)
Text("Heading: \(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))")
} icon: {
Image(systemName: "location.north")

16
ci_scripts/ci_pre_xcodebuild.sh Executable file
View file

@ -0,0 +1,16 @@
#!/bin/sh
echo "Stage: PRE-Xcode Build is activated .... "
# Move to the place where the scripts are located.
# This is important because the position of the subsequently mentioned files depend of this origin.
cd $CI_PRIMARY_REPOSITORY_PATH/ci_scripts || exit 1
# Write a JSON File containing all the environment variables and secrets.
printf "{\"PUBLIC_MQTT_USERNAME\":\"%s\",\"PUBLIC_MQTT_PASSWORD\":\"%s\"}" "$PUBLIC_MQTT_USERNAME" "$PUBLIC_MQTT_PASSWORD" >> .\ $CI_PRIMARY_REPOSITORY_PATH/SupportingFiles/secrets.json
echo "Wrote Secrets.json file."
echo "Stage: PRE-Xcode Build is DONE .... "
exit 0