Meshtastic-Apple/Meshtastic/Views/Settings/Config/Module/TAKModuleConfig.swift

269 lines
7.1 KiB
Swift
Raw Permalink Normal View History

2026-04-07 13:51:06 -05:00
//
// TAKModuleConfig.swift
// Meshtastic
import SwiftUI
2026-04-16 12:10:00 -07:00
import SwiftData
2026-04-07 13:51:06 -05:00
import OSLog
import MeshtasticProtobufs
struct TAKModuleConfig: View {
2026-04-16 12:10:00 -07:00
@Environment(\.modelContext) private var context
2026-04-07 13:51:06 -05:00
@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))
}
var body: some View {
Form {
ConfigHeader(title: "TAK", config: \.takConfig, node: node, onAppear: setTAKValues)
2026-04-08 13:26:57 -05:00
if accessoryManager.isConnected, node?.takConfig == nil {
Section {
HStack(spacing: 12) {
ProgressView()
Text("Loading TAK config from the node.")
.foregroundColor(.secondary)
}
}
}
2026-04-07 13:51:06 -05:00
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)
}
}
2026-04-08 13:26:57 -05:00
.disabled(!accessoryManager.isConnected || node?.takConfig == nil)
2026-04-07 13:51:06 -05:00
.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 {
2026-04-16 12:10:00 -07:00
TAKModuleConfig(node: nil)
2026-04-07 13:51:06 -05:00
.environmentObject(AccessoryManager.shared)
2026-04-16 12:10:00 -07:00
.modelContainer(PersistenceController.preview.container)
2026-04-07 13:51:06 -05:00
}