Moved some things around

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>
This commit is contained in:
Ben Meadors 2026-04-07 13:51:06 -05:00 committed by GitHub
parent d3d5980cca
commit 3f9348e36f
6 changed files with 318 additions and 242 deletions

View file

@ -55318,7 +55318,6 @@
}
},
"This node does not support any configurable modules." : {
"extractionState" : "stale",
"localizations" : {
"da" : {
"stringUnit" : {

View file

@ -102,6 +102,7 @@
8EED425B7820DA4FEB40C375 /* CoTXMLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 748E4806582595DE80D455CD /* CoTXMLParser.swift */; };
9604373EEB96801AA89DF48C /* EXICodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D0A8ABAEF1E587683970927 /* EXICodec.swift */; };
A5339E2F74E83F8FC41EEE33 /* TAKServerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0618E6D0DF90B74EE32E6C06 /* TAKServerConfig.swift */; };
AA0001032F07A4B000600001 /* TAKModuleConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001042F07A4B000600001 /* TAKModuleConfig.swift */; };
AA0001012E2730EC00600001 /* ConnectViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00010022E2730EC0060000 /* ConnectViewTests.swift */; };
ABA8E6402E2F2A2300E27791 /* AppIconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA8E63F2E2F2A2300E27791 /* AppIconButton.swift */; };
ABB99DEB2E2EA1C500CFBD05 /* AppIconPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABB99DEA2E2EA1C500CFBD05 /* AppIconPicker.swift */; };
@ -351,6 +352,7 @@
01028778B8BFD81F7A039593 /* TAKConnection.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKConnection.swift; sourceTree = "<group>"; };
0618E6D0DF90B74EE32E6C06 /* TAKServerConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKServerConfig.swift; sourceTree = "<group>"; };
09936BEBD6D82479B2360FDC /* TAKCertificateManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TAKCertificateManager.swift; sourceTree = "<group>"; };
AA0001042F07A4B000600001 /* TAKModuleConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TAKModuleConfig.swift; sourceTree = "<group>"; };
108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContactQRDialog.swift; sourceTree = "<group>"; };
108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityToNodeInfo.swift; sourceTree = "<group>"; };
1D5AD8037A0D583C614B0597 /* Tools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tools.swift; sourceTree = "<group>"; };
@ -1085,6 +1087,7 @@
DD15E4F22B8BA56E00654F61 /* PaxCounterConfig.swift */,
DD6193782863875F00E59241 /* SerialConfig.swift */,
DDF6B2472A9AEBF500BA6931 /* StoreForwardConfig.swift */,
AA0001042F07A4B000600001 /* TAKModuleConfig.swift */,
DD415827285859C4009B0E59 /* TelemetryConfig.swift */,
);
path = Module;
@ -1946,6 +1949,7 @@
E3ED80145D0E873011982556 /* TAKServerManager.swift in Sources */,
FE508F9AF5AD5DA20AA64DBF /* AccessoryManager+TAK.swift in Sources */,
A5339E2F74E83F8FC41EEE33 /* TAKServerConfig.swift in Sources */,
AA0001032F07A4B000600001 /* TAKModuleConfig.swift in Sources */,
DCC919C6B47C15BB0795456C /* Tools.swift in Sources */,
8398407DBA32EE7CFC16A385 /* TAKDataPackageGenerator.swift in Sources */,
655AF7816E76D5F310DF87A6 /* FountainCodec.swift in Sources */,

View file

@ -1824,7 +1824,9 @@ extension AccessoryManager {
var adminPacket = AdminMessage()
adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.takConfig
adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data()
if fromUser != toUser {
adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data()
}
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(toUser.num)
meshPacket.from = UInt32(fromUser.num)

View file

@ -0,0 +1,264 @@
//
// TAKModuleConfig.swift
// Meshtastic
import SwiftUI
import CoreData
import OSLog
import MeshtasticProtobufs
struct TAKModuleConfig: View {
@Environment(\.managedObjectContext) private var context
@EnvironmentObject private var accessoryManager: AccessoryManager
@Environment(\.dismiss) private var goBack
let node: NodeInfoEntity?
@State private var hasChanges = false
@State private var team = Team.unspecifedColor.rawValue
@State private var role = MemberRole.unspecifed.rawValue
private var selectedTeam: Team {
Team(rawValue: team) ?? .unspecifedColor
}
private var selectedRole: MemberRole {
MemberRole(rawValue: role) ?? .unspecifed
}
private var deviceRole: DeviceRoles? {
guard let role = node?.deviceConfig?.role ?? node?.user?.role else { return nil }
return DeviceRoles(rawValue: Int(role))
}
private var isConnectedNode: Bool {
guard let node else { return false }
return node.num == accessoryManager.activeDeviceNum
}
var body: some View {
Form {
ConfigHeader(title: "TAK", config: \.takConfig, node: node, onAppear: setTAKValues)
if let deviceRole, deviceRole != .tak && deviceRole != .takTracker {
Section {
Text("These settings only apply when the device role is TAK or TAK Tracker.")
.font(.callout)
.foregroundColor(.orange)
}
}
Section(header: Text("Identity")) {
VStack(alignment: .leading) {
Picker("Team", selection: $team) {
ForEach(Team.allCases, id: \.rawValue) { teamOption in
Text(teamTitle(teamOption)).tag(teamOption.rawValue)
}
}
.pickerStyle(DefaultPickerStyle())
Text(teamHelpText(selectedTeam))
.foregroundColor(.gray)
.font(.callout)
}
VStack(alignment: .leading) {
Picker("Role", selection: $role) {
ForEach(MemberRole.allCases, id: \.rawValue) { roleOption in
Text(roleTitle(roleOption)).tag(roleOption.rawValue)
}
}
.pickerStyle(DefaultPickerStyle())
Text(roleHelpText(selectedRole))
.foregroundColor(.gray)
.font(.callout)
}
}
Section {
Text("These values are included in TAK position reports. Leave either setting at Default to let firmware use Cyan and Team Member.")
.foregroundColor(.gray)
.font(.callout)
}
}
.disabled(!accessoryManager.isConnected || (!isConnectedNode && node?.takConfig == nil))
.safeAreaInset(edge: .bottom, alignment: .center) {
HStack(spacing: 0) {
SaveConfigButton(node: node, hasChanges: $hasChanges) {
guard let connectedNode = getNodeInfo(id: accessoryManager.activeDeviceNum ?? -1, context: context),
let fromUser = connectedNode.user,
let toUser = node?.user else {
return
}
var config = ModuleConfig.TAKConfig()
config.team = selectedTeam
config.role = selectedRole
Task {
_ = try await accessoryManager.saveTAKModuleConfig(config: config, fromUser: fromUser, toUser: toUser)
Task { @MainActor in
hasChanges = false
goBack()
}
}
}
}
}
.navigationTitle("TAK Config")
.navigationBarItems(
trailing: ZStack {
ConnectedDevice(
deviceConnected: accessoryManager.isConnected,
name: accessoryManager.activeConnection?.device.shortName ?? "?"
)
}
)
.onAppear {
// Need to request a TAKModuleConfig from the connected node before allowing changes.
if let deviceNum = accessoryManager.activeDeviceNum,
let node,
node.num == deviceNum,
node.takConfig == nil {
let connectedNode = getNodeInfo(id: deviceNum, context: context)
if let connectedNode {
Task {
do {
Logger.mesh.info("⚙️ Empty TAK module config requesting from connected node")
try await accessoryManager.requestTAKModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
} catch {
Logger.mesh.error("🚨 TAK module config request failed: \(error.localizedDescription)")
}
}
}
}
}
.onFirstAppear {
if let deviceNum = accessoryManager.activeDeviceNum, let node {
let connectedNode = getNodeInfo(id: deviceNum, context: context)
if let connectedNode, node.num != deviceNum {
if UserDefaults.enableAdministration {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.takConfig == nil {
Task {
do {
Logger.mesh.info("⚙️ Empty or expired TAK module config requesting via PKI admin")
try await accessoryManager.requestTAKModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
} catch {
Logger.mesh.info("🚨 TAK module config request failed: \(error.localizedDescription)")
}
}
}
} else {
Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.")
}
}
}
}
.onChange(of: team) { _, newTeam in
if newTeam != Int(node?.takConfig?.team ?? Int32(Team.unspecifedColor.rawValue)) {
hasChanges = true
}
}
.onChange(of: role) { _, newRole in
if newRole != Int(node?.takConfig?.role ?? Int32(MemberRole.unspecifed.rawValue)) {
hasChanges = true
}
}
}
private func setTAKValues() {
team = Int(node?.takConfig?.team ?? Int32(Team.unspecifedColor.rawValue))
role = Int(node?.takConfig?.role ?? Int32(MemberRole.unspecifed.rawValue))
hasChanges = false
}
private func teamTitle(_ team: Team) -> String {
switch team {
case .unspecifedColor:
return "Default (Cyan)"
case .white:
return "White"
case .yellow:
return "Yellow"
case .orange:
return "Orange"
case .magenta:
return "Magenta"
case .red:
return "Red"
case .maroon:
return "Maroon"
case .purple:
return "Purple"
case .darkBlue:
return "Dark Blue"
case .blue:
return "Blue"
case .cyan:
return "Cyan"
case .teal:
return "Teal"
case .green:
return "Green"
case .darkGreen:
return "Dark Green"
case .brown:
return "Brown"
case .UNRECOGNIZED:
return "Unknown"
}
}
private func roleTitle(_ role: MemberRole) -> String {
switch role {
case .unspecifed:
return "Default (Team Member)"
case .teamMember:
return "Team Member"
case .teamLead:
return "Team Lead"
case .hq:
return "HQ"
case .sniper:
return "Sniper"
case .medic:
return "Medic"
case .forwardObserver:
return "Forward Observer"
case .rto:
return "RTO"
case .k9:
return "K9"
case .UNRECOGNIZED:
return "Unknown"
}
}
private func teamHelpText(_ team: Team) -> String {
switch team {
case .unspecifedColor:
return "Default uses Cyan."
case .UNRECOGNIZED:
return "Unknown team color."
default:
return "Shown to TAK clients as the \(teamTitle(team)) team color."
}
}
private func roleHelpText(_ role: MemberRole) -> String {
switch role {
case .unspecifed:
return "Default uses Team Member."
case .UNRECOGNIZED:
return "Unknown TAK role."
default:
return "Shown to TAK clients as the \(roleTitle(role)) role."
}
}
}
#Preview {
let context = PersistenceController.preview.container.viewContext
return TAKModuleConfig(node: nil)
.environmentObject(AccessoryManager.shared)
.environment(\.managedObjectContext, context)
}

View file

@ -33,14 +33,48 @@ struct Settings: View {
// MARK: Helper
private var moduleConfigurationNode: NodeInfoEntity? {
let nodeNum = selectedNode > 0 ? selectedNode : preferredNodeNum
return nodes.first(where: { $0.num == nodeNum })
}
private var showsAnyModuleConfiguration: Bool {
isAnySupported([
.ambientlightingConfig,
.cannedmsgConfig,
.detectionsensorConfig,
.extnotifConfig,
.mqttConfig,
.rangetestConfig,
.paxcounterConfig,
.serialConfig,
.storeforwardConfig,
.telemetryConfig
]) || isTAKModuleSupported()
}
private func isModuleSupported(_ module: ExcludedModules) -> Bool {
return Int(nodes.first(where: { $0.num == preferredNodeNum })?.metadata?.excludedModules ?? Int32.zero) & module.rawValue == 0
return Int(moduleConfigurationNode?.metadata?.excludedModules ?? Int32.zero) & module.rawValue == 0
}
private func isAnySupported(_ modules: [ExcludedModules]) -> Bool {
return modules.map(isModuleSupported).contains(true)
}
private func isTAKModuleSupported() -> Bool {
guard let node = moduleConfigurationNode else { return false }
if node.takConfig != nil {
return true
}
guard let roleValue = node.deviceConfig?.role ?? node.user?.role,
let deviceRole = DeviceRoles(rawValue: Int(roleValue)) else {
return false
}
return deviceRole == .tak || deviceRole == .takTracker
}
// MARK: Views
var radioConfigurationSection: some View {
@ -276,13 +310,20 @@ struct Settings: View {
}
}
NavigationLink(value: SettingsNavigationState.takConfig) {
Label {
Text("TAK")
} icon: {
Image(systemName: "shield.checkered")
if isTAKModuleSupported() {
NavigationLink(value: SettingsNavigationState.takConfig) {
Label {
Text("TAK")
} icon: {
Image(systemName: "shield.checkered")
}
}
}
if !showsAnyModuleConfiguration {
Text("This node does not support any configurable modules.")
.foregroundColor(.secondary)
}
} header: {
Text("Module Configuration")
}

View file

@ -9,7 +9,6 @@ import SwiftUI
import UniformTypeIdentifiers
import OSLog
import CoreData
import MeshtasticProtobufs
enum CertificateImportType {
case p12
@ -564,236 +563,3 @@ struct ZipDocument: FileDocument {
FileWrapper(regularFileWithContents: data)
}
}
struct TAKModuleConfig: View {
@Environment(\.managedObjectContext) private var context
@EnvironmentObject private var accessoryManager: AccessoryManager
@Environment(\.dismiss) private var goBack
let node: NodeInfoEntity?
@State private var hasChanges = false
@State private var team = Team.unspecifedColor.rawValue
@State private var role = MemberRole.unspecifed.rawValue
private var selectedTeam: Team {
Team(rawValue: team) ?? .unspecifedColor
}
private var selectedRole: MemberRole {
MemberRole(rawValue: role) ?? .unspecifed
}
private var deviceRole: DeviceRoles? {
guard let role = node?.deviceConfig?.role else { return nil }
return DeviceRoles(rawValue: Int(role))
}
var body: some View {
Form {
ConfigHeader(title: "TAK", config: \.takConfig, node: node, onAppear: setTAKValues)
if let deviceRole, deviceRole != .tak && deviceRole != .takTracker {
Section {
Text("These settings only apply when the device role is TAK or TAK Tracker.")
.font(.callout)
.foregroundColor(.orange)
}
}
Section(header: Text("Identity")) {
VStack(alignment: .leading) {
Picker("Team", selection: $team) {
ForEach(Team.allCases, id: \.rawValue) { teamOption in
Text(teamTitle(teamOption)).tag(teamOption.rawValue)
}
}
.pickerStyle(DefaultPickerStyle())
Text(teamHelpText(selectedTeam))
.foregroundColor(.gray)
.font(.callout)
}
VStack(alignment: .leading) {
Picker("Role", selection: $role) {
ForEach(MemberRole.allCases, id: \.rawValue) { roleOption in
Text(roleTitle(roleOption)).tag(roleOption.rawValue)
}
}
.pickerStyle(DefaultPickerStyle())
Text(roleHelpText(selectedRole))
.foregroundColor(.gray)
.font(.callout)
}
}
Section {
Text("These values are included in TAK position reports. Leave either setting at Default to let firmware use Cyan and Team Member.")
.foregroundColor(.gray)
.font(.callout)
}
}
.disabled(!accessoryManager.isConnected || node?.takConfig == nil)
.safeAreaInset(edge: .bottom, alignment: .center) {
HStack(spacing: 0) {
SaveConfigButton(node: node, hasChanges: $hasChanges) {
guard let connectedNode = getNodeInfo(id: accessoryManager.activeDeviceNum ?? -1, context: context),
let fromUser = connectedNode.user,
let toUser = node?.user else {
return
}
var config = ModuleConfig.TAKConfig()
config.team = selectedTeam
config.role = selectedRole
Task {
_ = try await accessoryManager.saveTAKModuleConfig(config: config, fromUser: fromUser, toUser: toUser)
Task { @MainActor in
hasChanges = false
goBack()
}
}
}
}
}
.navigationTitle("TAK Config")
.navigationBarItems(
trailing: ZStack {
ConnectedDevice(
deviceConnected: accessoryManager.isConnected,
name: accessoryManager.activeConnection?.device.shortName ?? "?"
)
}
)
.onFirstAppear {
if let deviceNum = accessoryManager.activeDeviceNum, let node {
let connectedNode = getNodeInfo(id: deviceNum, context: context)
if let connectedNode, node.num != deviceNum {
if UserDefaults.enableAdministration {
let expiration = node.sessionExpiration ?? Date()
if expiration < Date() || node.takConfig == nil {
Task {
do {
Logger.mesh.info("⚙️ Empty or expired TAK module config requesting via PKI admin")
try await accessoryManager.requestTAKModuleConfig(fromUser: connectedNode.user!, toUser: node.user!)
} catch {
Logger.mesh.info("🚨 TAK module config request failed: \(error.localizedDescription)")
}
}
}
} else {
Logger.mesh.info("☠️ Using insecure legacy admin that is no longer supported, please upgrade your firmware.")
}
}
}
}
.onChange(of: team) { _, newTeam in
if newTeam != Int(node?.takConfig?.team ?? Int32(Team.unspecifedColor.rawValue)) {
hasChanges = true
}
}
.onChange(of: role) { _, newRole in
if newRole != Int(node?.takConfig?.role ?? Int32(MemberRole.unspecifed.rawValue)) {
hasChanges = true
}
}
}
private func setTAKValues() {
team = Int(node?.takConfig?.team ?? Int32(Team.unspecifedColor.rawValue))
role = Int(node?.takConfig?.role ?? Int32(MemberRole.unspecifed.rawValue))
hasChanges = false
}
private func teamTitle(_ team: Team) -> String {
switch team {
case .unspecifedColor:
return "Default (Cyan)"
case .white:
return "White"
case .yellow:
return "Yellow"
case .orange:
return "Orange"
case .magenta:
return "Magenta"
case .red:
return "Red"
case .maroon:
return "Maroon"
case .purple:
return "Purple"
case .darkBlue:
return "Dark Blue"
case .blue:
return "Blue"
case .cyan:
return "Cyan"
case .teal:
return "Teal"
case .green:
return "Green"
case .darkGreen:
return "Dark Green"
case .brown:
return "Brown"
case .UNRECOGNIZED:
return "Unknown"
}
}
private func roleTitle(_ role: MemberRole) -> String {
switch role {
case .unspecifed:
return "Default (Team Member)"
case .teamMember:
return "Team Member"
case .teamLead:
return "Team Lead"
case .hq:
return "HQ"
case .sniper:
return "Sniper"
case .medic:
return "Medic"
case .forwardObserver:
return "Forward Observer"
case .rto:
return "RTO"
case .k9:
return "K9"
case .UNRECOGNIZED:
return "Unknown"
}
}
private func teamHelpText(_ team: Team) -> String {
switch team {
case .unspecifedColor:
return "Default uses Cyan."
case .UNRECOGNIZED:
return "Unknown team color."
default:
return "Shown to TAK clients as the \(teamTitle(team)) team color."
}
}
private func roleHelpText(_ role: MemberRole) -> String {
switch role {
case .unspecifed:
return "Default uses Team Member."
case .UNRECOGNIZED:
return "Unknown TAK role."
default:
return "Shown to TAK clients as the \(roleTitle(role)) role."
}
}
}
#Preview {
let context = PersistenceController.preview.container.viewContext
return TAKModuleConfig(node: nil)
.environmentObject(AccessoryManager.shared)
.environment(\.managedObjectContext, context)
}