mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
249 lines
8.3 KiB
Swift
249 lines
8.3 KiB
Swift
import SwiftUI
|
|
import MeshtasticProtobufs
|
|
import OSLog
|
|
|
|
struct PowerConfig: View {
|
|
@Environment(\.managedObjectContext) private var context
|
|
@EnvironmentObject private var bleManager: BLEManager
|
|
@Environment(\.dismiss) private var goBack
|
|
|
|
let node: NodeInfoEntity?
|
|
|
|
@State private var isPowerSaving = false
|
|
|
|
@State private var shutdownOnPowerLoss = false
|
|
@State private var shutdownAfterSecs = 0
|
|
@State private var adcOverride = false
|
|
@State private var adcMultiplier: Float = 0.0
|
|
|
|
@State private var waitBluetoothSecs = 60
|
|
@State private var lsSecs = 300
|
|
@State private var minWakeSecs = 10
|
|
|
|
@State private var currentDevice: DeviceHardware?
|
|
|
|
@State private var hasChanges: Bool = false
|
|
@FocusState private var isFocused: Bool
|
|
|
|
var body: some View {
|
|
Form {
|
|
ConfigHeader(title: "config.power.title", config: \.powerConfig, node: node, onAppear: setPowerValues)
|
|
|
|
Section {
|
|
if (currentDevice?.architecture == .esp32 || currentDevice?.architecture == .esp32S3) || (currentDevice?.architecture == .nrf52840 && (node?.deviceConfig?.role ?? 0 == 5 || node?.deviceConfig?.role ?? 0 == 6)) {
|
|
Toggle(isOn: $isPowerSaving) {
|
|
Label("config.power.saving", systemImage: "bolt")
|
|
Text("config.power.saving.description")
|
|
}
|
|
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
|
}
|
|
Toggle(isOn: $shutdownOnPowerLoss) {
|
|
Label("config.power.shutdown.on.power.loss", systemImage: "power")
|
|
}
|
|
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
|
if shutdownOnPowerLoss {
|
|
Picker("config.power.shutdown.after.secs", selection: $shutdownAfterSecs) {
|
|
ForEach(PowerIntervals.allCases) { at in
|
|
Text(at.description)
|
|
}
|
|
}
|
|
.pickerStyle(DefaultPickerStyle())
|
|
}
|
|
} header: {
|
|
Text("config.power.settings")
|
|
}
|
|
if currentDevice?.architecture == .esp32 || currentDevice?.architecture == .esp32S3 {
|
|
Section {
|
|
Toggle(isOn: $adcOverride) {
|
|
Text("config.power.adc.override")
|
|
}
|
|
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
|
|
|
if adcOverride {
|
|
HStack {
|
|
Text("config.power.adc.multiplier")
|
|
Spacer()
|
|
FloatField(title: "config.power.adc.multiplier", number: $adcMultiplier) {
|
|
(2.0 ... 6.0).contains($0)
|
|
}
|
|
.focused($isFocused)
|
|
Spacer()
|
|
}
|
|
}
|
|
} header: {
|
|
Text("config.power.section.battery")
|
|
}
|
|
// Section {
|
|
// Picker("config.power.wait.bluetooth.secs", selection: $waitBluetoothSecs) {
|
|
// ForEach(PowerIntervals.allCases) {
|
|
// Text($0.description)
|
|
// }
|
|
// }
|
|
// .pickerStyle(DefaultPickerStyle())
|
|
//
|
|
// Picker("config.power.ls.secs", selection: $lsSecs) {
|
|
// ForEach(PowerIntervals.allCases) {
|
|
// Text($0.description)
|
|
// }
|
|
// }
|
|
// .pickerStyle(DefaultPickerStyle())
|
|
//
|
|
// Picker("config.power.min.wake.secs", selection: $minWakeSecs) {
|
|
// ForEach(PowerIntervals.allCases) {
|
|
// Text($0.description)
|
|
// }
|
|
// }
|
|
// .pickerStyle(DefaultPickerStyle())
|
|
//
|
|
// } header: {
|
|
// Text("config.power.section.sleep")
|
|
// }
|
|
}
|
|
}
|
|
.disabled(self.bleManager.connectedPeripheral == nil || node?.powerConfig == nil)
|
|
.navigationTitle("config.power.title")
|
|
.navigationBarItems(trailing: ZStack {
|
|
ConnectedDevice(
|
|
bluetoothOn: bleManager.isSwitchedOn,
|
|
deviceConnected: bleManager.connectedPeripheral != nil,
|
|
name: "\(bleManager.connectedPeripheral?.shortName ?? "?")"
|
|
)
|
|
})
|
|
.toolbar {
|
|
ToolbarItemGroup(placement: .keyboard) {
|
|
Spacer()
|
|
Button("dismiss.keyboard") {
|
|
isFocused = false
|
|
}
|
|
.font(.subheadline)
|
|
}
|
|
}
|
|
.onFirstAppear {
|
|
Api().loadDeviceHardwareData { (hw) in
|
|
for device in hw {
|
|
let currentHardware = node?.user?.hwModel ?? "UNSET"
|
|
let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "")
|
|
if deviceString == currentHardware {
|
|
currentDevice = device
|
|
}
|
|
}
|
|
}
|
|
// Need to request a NetworkConfig from the remote node before allowing changes
|
|
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
|
|
|
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
|
if let connectedNode {
|
|
if node.num != connectedNode.num {
|
|
if UserDefaults.enableAdministration {
|
|
/// 2.5 Administration with session passkey
|
|
let expiration = node.sessionExpiration ?? Date()
|
|
if expiration < Date() || node.powerConfig == nil {
|
|
Logger.mesh.info("⚙️ Empty or expired power config requesting via PKI admin")
|
|
_ = bleManager.requestPowerConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
|
}
|
|
} else {
|
|
/// Legacy Administration
|
|
Logger.mesh.info("☠️ Using insecure legacy admin, empty power config")
|
|
_ = bleManager.requestPowerConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.onChange(of: isPowerSaving) { oldIsPowerSaving, newIsPowerSaving in
|
|
if oldIsPowerSaving != newIsPowerSaving && newIsPowerSaving != node?.powerConfig?.isPowerSaving { hasChanges = true }
|
|
}
|
|
.onChange(of: shutdownOnPowerLoss) { _, newShutdownOnPowerLoss in
|
|
if newShutdownOnPowerLoss {
|
|
hasChanges = true
|
|
}
|
|
}
|
|
.onChange(of: shutdownAfterSecs) { oldShutdownAfterSecs, newShutdownAfterSecs in
|
|
if oldShutdownAfterSecs != newShutdownAfterSecs && newShutdownAfterSecs != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true }
|
|
}
|
|
.onChange(of: adcOverride) {
|
|
hasChanges = true
|
|
}
|
|
.onChange(of: adcMultiplier) { _, newAdcMultiplier in
|
|
if newAdcMultiplier != node?.powerConfig?.adcMultiplierOverride ?? -1 { hasChanges = true }
|
|
}
|
|
.onChange(of: waitBluetoothSecs) { oldWaitBluetoothSecs, newWaitBluetoothSecs in
|
|
if oldWaitBluetoothSecs != newWaitBluetoothSecs && newWaitBluetoothSecs != node?.powerConfig?.waitBluetoothSecs ?? -1 { hasChanges = true }
|
|
}
|
|
.onChange(of: lsSecs) { _, newLsSecs in
|
|
if newLsSecs != node?.powerConfig?.lsSecs ?? -1 { hasChanges = true }
|
|
}
|
|
.onChange(of: minWakeSecs) { _, newMinWakeSecs in
|
|
if newMinWakeSecs != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true }
|
|
}
|
|
|
|
SaveConfigButton(node: node, hasChanges: $hasChanges) {
|
|
guard let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context),
|
|
let fromUser = connectedNode.user,
|
|
let toUser = node?.user else {
|
|
return
|
|
}
|
|
|
|
var config = Config.PowerConfig()
|
|
config.isPowerSaving = isPowerSaving
|
|
config.onBatteryShutdownAfterSecs = shutdownOnPowerLoss ? UInt32(shutdownAfterSecs) : 0
|
|
config.adcMultiplierOverride = adcOverride ? adcMultiplier : 0
|
|
config.waitBluetoothSecs = UInt32(waitBluetoothSecs)
|
|
config.lsSecs = UInt32(lsSecs)
|
|
config.minWakeSecs = UInt32(minWakeSecs)
|
|
|
|
let adminMessageId = bleManager.savePowerConfig(
|
|
config: config,
|
|
fromUser: fromUser,
|
|
toUser: toUser,
|
|
adminIndex: connectedNode.myInfo?.adminIndex ?? 0
|
|
)
|
|
if adminMessageId > 0 {
|
|
// Should show a saved successfully alert once I know that to be true
|
|
// for now just disable the button after a successful save
|
|
hasChanges = false
|
|
goBack()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func setPowerValues() {
|
|
isPowerSaving = node?.powerConfig?.isPowerSaving ?? isPowerSaving
|
|
|
|
shutdownAfterSecs = Int(node?.powerConfig?.onBatteryShutdownAfterSecs ?? Int32(shutdownAfterSecs))
|
|
shutdownOnPowerLoss = shutdownAfterSecs != 0
|
|
|
|
adcMultiplier = node?.powerConfig?.adcMultiplierOverride ?? adcMultiplier
|
|
adcOverride = adcMultiplier != 0
|
|
|
|
waitBluetoothSecs = Int(node?.powerConfig?.waitBluetoothSecs ?? Int32(waitBluetoothSecs))
|
|
lsSecs = Int(node?.powerConfig?.lsSecs ?? Int32(lsSecs))
|
|
minWakeSecs = Int(node?.powerConfig?.minWakeSecs ?? Int32(minWakeSecs))
|
|
}
|
|
}
|
|
|
|
/// Helper view for isolating user float input that can be validated before being applied.
|
|
private struct FloatField: View {
|
|
let title: String
|
|
@Binding var number: Float
|
|
var isValid: (Float) -> Bool = { _ in true }
|
|
|
|
@State private var typingNumber: Float = 0.0
|
|
|
|
var body: some View {
|
|
TextField(title.localized, value: $typingNumber, format: .number)
|
|
.foregroundColor(.gray)
|
|
.multilineTextAlignment(.trailing)
|
|
.onChange(of: typingNumber) {
|
|
if isValid(typingNumber) {
|
|
number = typingNumber
|
|
} else {
|
|
typingNumber = number
|
|
}
|
|
}
|
|
.keyboardType(.decimalPad)
|
|
.onAppear {
|
|
typingNumber = number
|
|
}
|
|
}
|
|
}
|