Resolve merge conflicts for PR #1603 (TAK server improvements) (#1645)

* Delete Messages fix

* Bump version to 2.7.9

* Bump widgets version

* TAK Server channel index picker

Create a settings picker for the TAK Server's channel index. This allows users to specify TAK traffic to use the non-primary channel to help reduce channel congestion.

* Changed capitalization from 'environment' to 'Environment' for section header. (#1591)

* Add Danish (da) translations — resolves merge conflicts from PR #1609 (#1612)

* Initial plan

* Add Danish (da) translations from PR #1609

Resolves merge conflicts from PR #1609 by adding Danish translations to the
Localizable.xcstrings file. The PR adds Danish translation strings throughout
the app while preserving all existing translations for other languages.

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

* Migrate test project to Swift Testing and add connect view and router tests (#1643)

* Migrate to Swift Testing and add connect view tests

- Convert RouterTests.swift from XCTest to Swift Testing (@Suite, @Test, #expect, #require)
- Create ConnectViewTests.swift with tests for connect view child types:
  - Device struct (creation, signal strength, RSSI, description, codable)
  - TransportType enum (cases, raw values, codable)
  - ConnectionState enum (equality, codable)
  - BLESignalStrength enum (raw values, init)
  - TransportStatus enum (equality)
  - NavigationState (defaults, tabs, sub-states)
  - InvalidVersion view (creation with versions)
  - ConnectedDevice view (connected/disconnected/MQTT states)
  - CircleText view (default/custom sizes, emoji)
  - BatteryCompact view (levels, nil, charging, plugged in)
  - SignalStrengthIndicator view (dimensions, strength levels)
- Update Xcode project to include new test file

Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/d7bb7a89-2105-4fcb-96bc-7ec794467c74

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

* Fix signal strength test boundary conditions

The getSignalStrength() method uses NSNumber.compare(.orderedDescending),
which is a strict greater-than check. Fix the boundary test cases:
- RSSI -65 is .normal (not .strong), since -65 is not > -65
- RSSI -85 is .weak (not .normal), since -85 is not > -85
- Add -64 → .strong and -84 → .normal as adjacent boundary tests

Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/4fcbc01e-cbea-4d11-b2c0-e923c6730d69

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

* Improve and complete router tests with comprehensive coverage

Added tests for:
- Custom initial state
- Invalid scheme / unknown path handling (state unchanged)
- navigateToNodeDetail public method
- Messages edge cases: channelId only, userNum only, messageId only, non-numeric params
- Nodes with non-numeric nodenum
- Map with both nodenum+waypointId (nodeId priority), non-numeric params
- Parameterized settings test covering all 31 SettingsNavigationState cases
- State transitions: consecutive routes, invalid scheme preserves existing state

Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/f69b7352-21aa-494c-8864-31fc0f4b21b8

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

* Localizable update

* Merge translations file

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>
Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com>

* Fix merge conflicts in PR #1614 (Spanish translations) (#1644)

* 20% of strings translated to spanish

* add more translations

* add rest of translations

* small fixes

---------

Co-authored-by: Joel Pérez Izquierdo <joelperez91@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

* fix typo in hop limit option description (#1631)

O hop -> 0 hop

---------

Co-authored-by: Jake-B <jake-b@users.noreply.github.com>
Co-authored-by: Garth Vander Houwen <garthvh@yahoo.com>
Co-authored-by: niccellular <79813408+niccellular@users.noreply.github.com>
Co-authored-by: Austin Hargis <25471876+austinhargis@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>
Co-authored-by: Joel Pérez Izquierdo <joelperez91@gmail.com>
Co-authored-by: axunes <axunes@axunes.net>
This commit is contained in:
Copilot 2026-04-02 08:28:37 -07:00 committed by GitHub
parent 6ee6579cdb
commit 54ff386c03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 13949 additions and 185 deletions

View file

@ -157,6 +157,8 @@ final class TAKMeshtasticBridge {
return
}
let channel = UInt32(TAKServerManager.shared.channel)
// Determine send method based on CoT type
let sendMethod = GenericCoTHandler.shared.classifySendMethod(for: cotMessage)
@ -169,7 +171,7 @@ final class TAKMeshtasticBridge {
}
do {
try await accessoryManager.sendTAKPacket(takPacket)
try await accessoryManager.sendTAKPacket(takPacket, channel: channel)
Logger.tak.info("Sent TAKPacket to mesh: \(cotMessage.type)")
} catch {
Logger.tak.error("Failed to send TAKPacket to mesh: \(error.localizedDescription)")
@ -179,7 +181,7 @@ final class TAKMeshtasticBridge {
// Use EXI compression on ATAK_FORWARDER port (257)
GenericCoTHandler.shared.accessoryManager = accessoryManager
do {
try await GenericCoTHandler.shared.sendGenericCoT(cotMessage)
try await GenericCoTHandler.shared.sendGenericCoT(cotMessage, channel: channel)
Logger.tak.info("Sent generic CoT to mesh via ATAK_FORWARDER: \(cotMessage.type)")
} catch {
Logger.tak.error("Failed to send generic CoT to mesh: \(error.localizedDescription)")

View file

@ -72,6 +72,8 @@ final class TAKServerManager: ObservableObject {
// MARK: - Configuration (persisted via AppStorage)
@AppStorage("takServerChannel") var channel: Int = 0
@AppStorage("takServerEnabled") var enabled = false {
didSet {
Task {

View file

@ -156,10 +156,16 @@ extension MeshPackets {
nonisolated public func deleteChannelMessages(channel: ChannelEntity, context: NSManagedObjectContext) {
do {
let objects = channel.allPrivateMessages
// Copied logic from ChannelEntity.allPrivateMessages, which is always on the MainActor
// But this code may not be on the MainActor.
let fetchRequest = MessageEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "channel == %ld AND toUser == nil AND isEmoji == false", channel.index)
let objects = (try? context.fetch(fetchRequest)) ?? [MessageEntity]()
for object in objects {
context.delete(object)
}
try context.save()
} catch let error as NSError {
Logger.data.error("\(error.localizedDescription, privacy: .public)")

View file

@ -162,8 +162,13 @@ struct ChannelList: View {
Button(role: .destructive) {
Task {
await MeshPackets.shared.deleteChannelMessages(channel: channelToDeleteMessages!)
context.refresh(myInfo, mergeChanges: true)
channelToDeleteMessages = nil
await MainActor.run {
context.refresh(channel, mergeChanges: true)
context.refresh(myInfo, mergeChanges: true)
// Reset state
channelToDeleteMessages = nil
}
}
} label: {
Text("Delete")

View file

@ -70,7 +70,7 @@ struct AppSettings: View {
}
#endif
}
Section(header: Text("environment")) {
Section(header: Text("Environment")) {
VStack(alignment: .leading) {
Toggle(isOn: $environmentEnableWeatherKit) {
Label("Weather Conditions", systemImage: "cloud.sun")

View file

@ -142,7 +142,7 @@ struct LoRaConfig: View {
.tag($0)
}
}
Text("Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. O hop broadcast messages will not get ACKs.")
Text("Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. 0 hop broadcast messages will not get ACKs.")
.foregroundColor(.gray)
.font(.callout)
}

View file

@ -8,6 +8,7 @@
import SwiftUI
import UniformTypeIdentifiers
import OSLog
import CoreData
enum CertificateImportType {
case p12
@ -15,6 +16,15 @@ enum CertificateImportType {
}
struct TAKServerConfig: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var accessoryManager: AccessoryManager
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \ChannelEntity.index, ascending: true)],
predicate: NSPredicate(format: "role > 0"),
animation: .default
) private var channels: FetchedResults<ChannelEntity>
@StateObject private var takServer = TAKServerManager.shared
@EnvironmentObject var accessoryManager: AccessoryManager
@Environment(\.managedObjectContext) var context
@ -267,6 +277,17 @@ struct TAKServerConfig: View {
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
if !channels.isEmpty {
Picker(selection: $takServer.channel) {
ForEach(channels, id: \.index) { channel in
channelLabel(channel)
.tag(Int(channel.index))
}
} label: {
Label("TAK Channel Index", systemImage: "bubble.left.and.bubble.right")
}
}
if takServer.isRunning {
Button {
Task {
@ -279,7 +300,7 @@ struct TAKServerConfig: View {
} header: {
Text("Configuration")
} footer: {
Text("Secure mTLS connection on port 8089. Both server and client certificates are required.")
Text("Secure mTLS connection on port 8089. Both server and client certificates are required. TAK Channel Index selects the channel index where TAK messages will be sent.")
}
}
@ -407,6 +428,21 @@ struct TAKServerConfig: View {
}
// MARK: - Channel Label
@ViewBuilder
private func channelLabel(_ channel: ChannelEntity) -> some View {
if channel.name?.isEmpty ?? false {
if channel.role == 1 {
Text(String("PrimaryChannel").camelCaseToWords())
} else {
Text(String("Channel \(channel.index)").camelCaseToWords())
}
} else {
Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords())
}
}
// MARK: - Import Handlers
private func handleP12Import(_ result: Result<[URL], Error>) {