Remove GPS Update interval

Remove GPS Attempt Time options
Don't show positions with empty times in the elevation graph
Hook up ambient lighitng config
This commit is contained in:
Garth Vander Houwen 2023-11-28 20:03:08 -08:00
parent 1c4b0c0cb3
commit 67900bfa51
9 changed files with 293 additions and 102 deletions

View file

@ -41,6 +41,7 @@
DD457188293C7E63000C49FB /* BLESignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */; };
DD4640202AFF10F4002A5ECB /* WaypointForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD46401F2AFF10F4002A5ECB /* WaypointForm.swift */; };
DD47E3D626F17ED900029299 /* CircleText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3D526F17ED900029299 /* CircleText.swift */; };
DD4975A52B147BA90026544E /* AmbientLightingConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4975A42B147BA90026544E /* AmbientLightingConfig.swift */; };
DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A911D2708C65400501B7E /* AppSettings.swift */; };
DD4F23CD28779A3C001D37CB /* EnvironmentMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */; };
DD5394FC276993AD00AD86B1 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = DD5394FB276993AD00AD86B1 /* SwiftProtobuf */; };
@ -254,6 +255,7 @@
DD457BC4295D5E35004BCE4D /* MeshtasticDataModelV5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV5.xcdatamodel; sourceTree = "<group>"; };
DD46401F2AFF10F4002A5ECB /* WaypointForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointForm.swift; sourceTree = "<group>"; };
DD47E3D526F17ED900029299 /* CircleText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleText.swift; sourceTree = "<group>"; };
DD4975A42B147BA90026544E /* AmbientLightingConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmbientLightingConfig.swift; sourceTree = "<group>"; };
DD4A911D2708C65400501B7E /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentMetricsLog.swift; sourceTree = "<group>"; };
DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionEntityExtension.swift; sourceTree = "<group>"; };
@ -592,6 +594,7 @@
DD61937B2863877A00E59241 /* Module */ = {
isa = PBXGroup;
children = (
DD4975A42B147BA90026544E /* AmbientLightingConfig.swift */,
DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */,
DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */,
DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */,
@ -1216,6 +1219,7 @@
DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */,
DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */,
DDB6ABE428B13FFF00384BA1 /* DisplayEnums.swift in Sources */,
DD4975A52B147BA90026544E /* AmbientLightingConfig.swift in Sources */,
DD86D40A287F04F100BAEB7A /* InvalidVersion.swift in Sources */,
DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */,
DD5E5212298EE33B00D21B61 /* apponly.pb.swift in Sources */,

View file

@ -53,52 +53,18 @@ enum GpsFormats: Int, CaseIterable, Identifiable {
}
}
enum GpsUpdateIntervals: Int, CaseIterable, Identifiable {
enum GpsAttemptTimes: Int, CaseIterable, Identifiable {
case fiveSeconds = 5
case tenSeconds = 10
case fifteenSeconds = 15
case twentySeconds = 20
case twentyFiveSeconds = 25
case thirtySeconds = 30
case fortyFiveSeconds = 45
case oneMinute = 60
case twoMinutes = 120
case fiveMinutes = 300
case tenMinutes = 600
case fifteenMinutes = 900
case thirtyMinutes = 1800
case oneHour = 3600
case sixHours = 21600
case twelveHours = 43200
case twentyFourHours = 86400
case maxInt32 = 2147483647
var id: Int { self.rawValue }
var description: String {
switch self {
case .fiveSeconds:
return "interval.five.seconds".localized
case .tenSeconds:
return "interval.ten.seconds".localized
case .fifteenSeconds:
return "interval.fifteen.seconds".localized
case .twentySeconds:
return "interval.twenty.seconds".localized
case .twentyFiveSeconds:
return "interval.twentyfive.seconds".localized
case .thirtySeconds:
return "interval.thirty.seconds".localized
case .fortyFiveSeconds:
return "interval.fortyfive.seconds".localized
case .oneMinute:
return "interval.one.minute".localized
case .twoMinutes:
return "interval.two.minutes".localized
case .fiveMinutes:
return "interval.five.minutes".localized
case .tenMinutes:
return "interval.ten.minutes".localized
case .fifteenMinutes:
return "interval.fifteen.minutes".localized
case .thirtyMinutes:
@ -111,57 +77,6 @@ enum GpsUpdateIntervals: Int, CaseIterable, Identifiable {
return "interval.twelve.hours".localized
case .twentyFourHours:
return "interval.twentyfour.hours".localized
case .maxInt32:
return "on.boot"
}
}
}
enum GpsAttemptTimes: Int, CaseIterable, Identifiable {
case twoSeconds = 2
case fiveSeconds = 5
case tenSeconds = 10
case fifteenSeconds = 15
case twentySeconds = 20
case twentyFiveSeconds = 25
case thirtySeconds = 30
case fortyFiveSeconds = 45
case oneMinute = 60
case twoMinutes = 120
case fiveMinutes = 300
case tenMinutes = 600
case fifteenMinutes = 900
var id: Int { self.rawValue }
var description: String {
switch self {
case .twoSeconds:
return "interval.two.seconds".localized
case .fiveSeconds:
return "interval.five.seconds".localized
case .tenSeconds:
return "interval.ten.seconds".localized
case .fifteenSeconds:
return "interval.fifteen.seconds".localized
case .twentySeconds:
return "interval.twenty.seconds".localized
case .twentyFiveSeconds:
return "interval.twentyfive.seconds".localized
case .thirtySeconds:
return "interval.thirty.seconds".localized
case .fortyFiveSeconds:
return "interval.fortyfive.seconds".localized
case .oneMinute:
return "interval.one.minute".localized
case .twoMinutes:
return "interval.two.minutes".localized
case .fiveMinutes:
return "interval.five.minutes".localized
case .tenMinutes:
return "interval.ten.minutes".localized
case .fifteenMinutes:
return "interval.fifteen.minutes".localized
}
}
}

View file

@ -1359,6 +1359,33 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
return 0
}
public func saveAmbientLightingModuleConfig(config: ModuleConfig.AmbientLightingConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 {
var adminPacket = AdminMessage()
adminPacket.setModuleConfig.ambientLighting = config
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(toUser.num)
meshPacket.from = UInt32(fromUser.num)
meshPacket.channel = UInt32(adminIndex)
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.wantAck = true
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Ambient Lighting Module Config for \(toUser.longName ?? "unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
upsertAmbientLightingModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!)
return Int64(meshPacket.id)
}
return 0
}
public func saveCannedMessageModuleConfig(config: ModuleConfig.CannedMessageConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 {
var adminPacket = AdminMessage()
@ -1858,6 +1885,33 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
return false
}
public func requestAmbientLightingConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool {
var adminPacket = AdminMessage()
adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.ambientlightingConfig
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(toUser.num)
meshPacket.from = UInt32(fromUser.num)
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.channel = UInt32(adminIndex)
meshPacket.wantAck = true
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
dataMessage.wantResponse = true
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested Ambient Lighting Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
return true
}
return false
}
public func requestCannedMessagesModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool {
var adminPacket = AdminMessage()

View file

@ -56,7 +56,9 @@ func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int6
func moduleConfig (config: ModuleConfig, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) {
if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(config.cannedMessage) {
if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.ambientLighting(config.ambientLighting) {
upsertAmbientLightingModuleConfigPacket(config: config.ambientLighting, nodeNum: nodeNum, context: context)
} else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(config.cannedMessage) {
upsertCannedMessagesModuleConfigPacket(config: config.cannedMessage, nodeNum: nodeNum, context: context)
} else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.detectionSensor(config.detectionSensor) {
upsertDetectionSensorModuleConfigPacket(config: config.detectionSensor, nodeNum: nodeNum, context: context)
@ -472,7 +474,9 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
}
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getModuleConfigResponse(adminMessage.getModuleConfigResponse) {
let moduleConfig = adminMessage.getModuleConfigResponse
if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(moduleConfig.cannedMessage) {
if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.ambientLighting(moduleConfig.ambientLighting) {
upsertAmbientLightingModuleConfigPacket(config: moduleConfig.ambientLighting, nodeNum: Int64(packet.from), context: context)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(moduleConfig.cannedMessage) {
upsertCannedMessagesModuleConfigPacket(config: moduleConfig.cannedMessage, nodeNum: Int64(packet.from), context: context)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.detectionSensor(moduleConfig.detectionSensor) {
upsertDetectionSensorModuleConfigPacket(config: moduleConfig.detectionSensor, nodeNum: Int64(packet.from), context: context)

View file

@ -624,6 +624,62 @@ func upsertPositionConfigPacket(config: Meshtastic.Config.PositionConfig, nodeNu
}
}
func upsertAmbientLightingModuleConfigPacket(config: Meshtastic.ModuleConfig.AmbientLightingConfig, nodeNum: Int64, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.ambientlighting.config %@".localized, String(nodeNum))
MeshLogger.log("🏮 \(logString)")
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
do {
guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else {
return
}
// Found a node, save Ambient Lighting Config
if !fetchedNode.isEmpty {
if fetchedNode[0].cannedMessageConfig == nil {
let newAmbientLightingConfig = AmbientLightingConfigEntity(context: context)
newAmbientLightingConfig.ledState = config.ledState
newAmbientLightingConfig.current = Int32(config.current)
newAmbientLightingConfig.red = Int32(config.red)
newAmbientLightingConfig.green = Int32(config.green)
newAmbientLightingConfig.blue = Int32(config.blue)
fetchedNode[0].ambientLightingConfig = newAmbientLightingConfig
} else {
if fetchedNode[0].ambientLightingConfig == nil {
fetchedNode[0].ambientLightingConfig = AmbientLightingConfigEntity(context: context)
}
fetchedNode[0].ambientLightingConfig?.ledState = config.ledState
fetchedNode[0].ambientLightingConfig?.current = Int32(config.current)
fetchedNode[0].ambientLightingConfig?.red = Int32(config.red)
fetchedNode[0].ambientLightingConfig?.green = Int32(config.green)
fetchedNode[0].ambientLightingConfig?.blue = Int32(config.blue)
}
do {
try context.save()
print("💾 Updated Ambient Lighting Module Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data AmbientLightingConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Ambient Lighting Module Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data AmbientLightingConfigEntity failed: \(nsError)")
}
}
func upsertCannedMessagesModuleConfigPacket(config: Meshtastic.ModuleConfig.CannedMessageConfig, nodeNum: Int64, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.cannedmessage.config %@".localized, String(nodeNum))

View file

@ -23,8 +23,10 @@ struct PositionAltitudeChart: View {
@State private var lineWidth = 2.0
var body: some View {
let fiveYearsAgo = Calendar.current.date(byAdding: .year, value: -5, to: Date())
let nodePositions = Array(node.positions!) as! [PositionEntity]
let data = nodePositions.map { PositionAltitude(time: $0.time ?? Date(), altitude: Measurement(value: Double($0.altitude), unit: .meters) ) }
let filteredPositions = nodePositions.filter({$0.time != nil && ($0.time ?? fiveYearsAgo!) > fiveYearsAgo!})
let data = filteredPositions.map { PositionAltitude(time: $0.time ?? Date(), altitude: Measurement(value: Double($0.altitude), unit: .meters) ) }
GroupBox(label: Label("Altitude", systemImage: "mountain.2")) {
Chart(data, id: \.time) {

View file

@ -0,0 +1,157 @@
//
// AmbientLightingConfig.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 11/26/23
//
import SwiftUI
@available(iOS 17.0, macOS 14.0, *)
struct AmbientLightingConfig: View {
@Environment(\.self) var environment
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@Environment(\.dismiss) private var goBack
var node: NodeInfoEntity?
@State private var isPresentingSaveConfirm: Bool = false
@State var hasChanges = false
@State var ledState: Bool = false
@State var current = 10
@State var red = 0
@State var green = 0
@State var blue = 0
@State private var color = Color(red: 51, green: 199, blue: 88) // Color(.sRGB, red: 0.98, green: 0.9, blue: 0.2)
@State private var components: Color.Resolved?
var body: some View {
VStack {
Form {
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
Text("There has been no response to a request for device metadata over the admin channel for this node.")
.font(.callout)
.foregroundColor(.orange)
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
// Let users know what is going on if they are using remote admin and don't have the config yet
if node?.rtttlConfig == nil {
Text("Ambient Lighting config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
.font(.callout)
.foregroundColor(.orange)
} else {
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
.font(.title3)
.onAppear {
setAmbientLightingConfigValue()
}
}
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
.font(.title3)
} else {
Text("Please connect to a radio to configure settings.")
.font(.callout)
.foregroundColor(.orange)
}
Section(header: Text("options")) {
VStack {
Toggle(isOn: $ledState) {
Label("LED State", systemImage: ledState ? "lightbulb.led.fill" : "lightbulb.led")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
HStack {
Image(systemName: "eyedropper")
.foregroundColor(.accentColor)
ColorPicker("Color", selection: $color, supportsOpacity: false)
.padding(5)
}
HStack {
Image(systemName: "directcurrent")
.foregroundColor(.accentColor)
Stepper("Current: \(current)", value: $current, in: 0...31, step: 1)
.padding(5)
}
}
.onChange(of: color, initial: true) {
components = color.resolve(in: environment)
hasChanges = true
}
}
}
//.disabled(self.bleManager.connectedPeripheral == nil || node?.ambientLightingConfig == nil)
Button {
isPresentingSaveConfirm = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
}
.disabled(self.bleManager.connectedPeripheral == nil || !hasChanges)
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingSaveConfirm,
titleVisibility: .visible
) {
let nodeName = node?.user?.longName ?? "unknown".localized
let buttonText = String.localizedStringWithFormat("save.config %@".localized, nodeName)
Button(buttonText) {
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
if connectedNode != nil {
var al = ModuleConfig.AmbientLightingConfig()
al.ledState = ledState
al.current = UInt32(current)
if let components {
al.red = UInt32(components.red * 255)
al.green = UInt32(components.green * 255)
al.blue = UInt32(components.blue * 255)
}
let adminMessageId = bleManager.saveAmbientLightingModuleConfig(config: al, fromUser: connectedNode!.user!, toUser: node!.user!, 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()
}
}
}
}
message: {
Text("config.save.confirm")
}
.navigationTitle("ambient.lighting.config")
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
setAmbientLightingConfigValue()
// Need to request a Ambient Lighting Config from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.ambientLightingConfig == nil {
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
if node != nil && connectedNode != nil {
_ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
}
}
}
.onChange(of: ledState) { newLedState in
if node != nil && node!.ambientLightingConfig != nil {
if newLedState != node!.ambientLightingConfig!.ledState { hasChanges = true }
}
}
}
}
func setAmbientLightingConfigValue() {
self.ledState = node?.ambientLightingConfig?.ledState ?? false
self.current = Int(node?.ambientLightingConfig?.current ?? 10)
color = Color(red: Double((node?.ambientLightingConfig?.red ?? 255) / 255),
green: Double((node?.ambientLightingConfig?.green ?? 255) / 255),
blue: Double((node?.ambientLightingConfig?.blue ?? 255) / 255))
self.hasChanges = false
}
}

View file

@ -212,13 +212,7 @@ struct PositionConfig: View {
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
if deviceGpsEnabled {
Picker("Update Interval", selection: $gpsUpdateInterval) {
ForEach(GpsUpdateIntervals.allCases) { ui in
Text(ui.description)
}
}
Text("How often should we try to get a GPS position.")
.font(.caption)
Picker("Attempt Time", selection: $gpsAttemptTime) {
ForEach(GpsAttemptTimes.allCases) { at in
Text(at.description)
@ -286,7 +280,6 @@ struct PositionConfig: View {
pc.positionBroadcastSmartEnabled = smartPositionEnabled
pc.gpsEnabled = deviceGpsEnabled
pc.fixedPosition = fixedPosition
pc.gpsUpdateInterval = UInt32(gpsUpdateInterval)
pc.gpsAttemptTime = UInt32(gpsAttemptTime)
pc.positionBroadcastSecs = UInt32(positionBroadcastSeconds)
pc.broadcastSmartMinimumIntervalSecs = UInt32(broadcastSmartMinimumIntervalSecs)
@ -359,11 +352,6 @@ struct PositionConfig: View {
if newGpsAttemptTime != node!.positionConfig!.gpsAttemptTime { hasChanges = true }
}
}
.onChange(of: gpsUpdateInterval) { newGpsUpdateInterval in
if node != nil && node!.positionConfig != nil {
if newGpsUpdateInterval != node!.positionConfig!.gpsUpdateInterval { hasChanges = true }
}
}
.onChange(of: smartPositionEnabled) { newSmartPositionEnabled in
if node != nil && node!.positionConfig != nil {
if newSmartPositionEnabled != node!.positionConfig!.smartPositionEnabled { hasChanges = true }

View file

@ -27,6 +27,7 @@ struct Settings: View {
case displayConfig
case networkConfig
case positionConfig
case ambientLightingConfig
case cannedMessagesConfig
case detectionSensorConfig
case externalNotificationConfig
@ -187,6 +188,16 @@ struct Settings: View {
.tag(SettingsSidebar.positionConfig)
}
Section("module.configuration") {
if #available(iOS 17.0, macOS 14.0, *) {
NavigationLink {
AmbientLightingConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "light.max")
.symbolRenderingMode(.hierarchical)
Text("ambient.lighting")
}
.tag(SettingsSidebar.ambientLightingConfig)
}
NavigationLink {
CannedMessagesConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {