diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 0d6c3898..50e4621a 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -232,6 +232,7 @@ DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeshtasticTests.swift; sourceTree = ""; }; DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeText.swift; sourceTree = ""; }; DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntityExtension.swift; sourceTree = ""; }; + DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = ""; }; DDF924C926FBB953009FE055 /* ConnectedDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1000,7 +1001,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.8; + MARKETING_VERSION = 2.0.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1033,7 +1034,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.0.8; + MARKETING_VERSION = 2.0.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1209,11 +1210,12 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */, DDCDC69A29467643004C1DDA /* MeshtasticDataModelV3.xcdatamodel */, DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DDCDC69A29467643004C1DDA /* MeshtasticDataModelV3.xcdatamodel */; + currentVersion = DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index acd11c67..24cfae62 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -313,6 +313,35 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { connectedPeripheral!.peripheral.readValue(for: FROMRADIO_characteristic) } + func sendTraceRouteRequest(destNum: Int64, wantResponse: Bool) -> Bool { + + var success = false + let fromNodeNum = connectedPeripheral.num + + let routePacket = RouteDiscovery() + + var meshPacket = MeshPacket() + meshPacket.to = UInt32(destNum) + meshPacket.from = UInt32(fromNodeNum)//0 // Send 0 as from from phone to device to avoid warning about client trying to set node num + var dataMessage = DataMessage() + dataMessage.payload = try! routePacket.serializedData() + dataMessage.portnum = PortNum.tracerouteApp + dataMessage.wantResponse = wantResponse + meshPacket.decoded = dataMessage + + var toRadio: ToRadio! + toRadio = ToRadio() + toRadio.packet = meshPacket + let binaryData: Data = try! toRadio.serializedData() + + if connectedPeripheral!.peripheral.state == CBPeripheralState.connected { + connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) + success = true + MeshLogger.log("🪧 Sent a Trace Route Packet to node: \(destNum).") + } + return success + } + func sendWantConfig() { guard (connectedPeripheral!.peripheral.state == CBPeripheralState.connected) else { return } @@ -504,7 +533,20 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { case .audioApp: MeshLogger.log("ℹ️ MESH PACKET received for Audio App UNHANDLED \(try! decodedInfo.packet.jsonString())") case .tracerouteApp: - MeshLogger.log("ℹ️ MESH PACKET received for Trace Route App UNHANDLED \(try! decodedInfo.packet.jsonString())") + if let routingMessage = try? RouteDiscovery(serializedData: decodedInfo.packet.decoded.payload) { + + if routingMessage.route.count == 0 { + MeshLogger.log("🪧 Trace Route request sent to \(decodedInfo.packet.from) was recieived directly.") + } else { + + var routeString = "🪧 Trace Route request returned: \(decodedInfo.packet.to) --> " + for node in routingMessage.route { + routeString += "\(node) --> " + } + routeString += "\(decodedInfo.packet.from)" + MeshLogger.log(routeString) + } + } case .UNRECOGNIZED(_): MeshLogger.log("ℹ️ MESH PACKET received for Other App UNHANDLED \(try! decodedInfo.packet.jsonString())") case .max: @@ -620,6 +662,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { newMessage.replyID = replyID } newMessage.messagePayload = message + newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: message) let dataType = PortNum.textMessageApp let payloadData: Data = message.data(using: String.Encoding.utf8)! @@ -995,7 +1038,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { if let decodedData = Data(base64Encoded: decodedString) { do { let channelSet: ChannelSet = try ChannelSet(serializedData: decodedData) - print(channelSet) var i:Int32 = 0 for cs in channelSet.settings { var chan = Channel() @@ -1052,6 +1094,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { MeshLogger.log("✈️ Sent a LoRaConfig for: \(String(self.connectedPeripheral.num))") } return true + } catch { return false } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index f947ecbf..f6336b9a 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -9,6 +9,33 @@ import Foundation import CoreData import SwiftUI +func generateMessageMarkdown (message: String) -> String { + + let types: NSTextCheckingResult.CheckingType = [.address, .link, .phoneNumber] + let detector = try! NSDataDetector(types: types.rawValue) + let matches = detector.matches(in: message, options: [], range: NSRange(location: 0, length: message.utf16.count)) + var messageWithMarkdown = message + if matches.count > 0 { + + for match in matches { + guard let range = Range(match.range, in: message) else { continue } + if match.resultType == .address { + let address = message[range] + let urlEncodedAddress = address.addingPercentEncoding(withAllowedCharacters: .alphanumerics) + messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: address, with: "[\(address)](http://maps.apple.com/?address=\(urlEncodedAddress ?? ""))") + } else if match.resultType == .phoneNumber { + let phone = messageWithMarkdown[range] + messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: phone, with: "[\(phone)](tel:\(phone))") + } else if match.resultType == .link { + let url = messageWithMarkdown[range] + let absoluteUrl = match.url?.absoluteString ?? "" + messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: url, with: "[\(String(match.url?.host ?? "Link"))\(String(match.url?.path ?? ""))](\(absoluteUrl))") + } + } + } + return messageWithMarkdown +} + func localConfig (config: Config, context:NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) { // We don't care about any of the Power settings, config is available for everyting else @@ -409,20 +436,36 @@ func moduleConfig (config: ModuleConfig, context:NSManagedObjectContext, nodeNum if fetchedNode[0].externalNotificationConfig == nil { let newExternalNotificationConfig = ExternalNotificationConfigEntity(context: context) newExternalNotificationConfig.enabled = config.externalNotification.enabled + newExternalNotificationConfig.usePWM = config.externalNotification.usePwm newExternalNotificationConfig.alertBell = config.externalNotification.alertBell + newExternalNotificationConfig.alertBellBuzzer = config.externalNotification.alertBellBuzzer + newExternalNotificationConfig.alertBellVibra = config.externalNotification.alertBellVibra newExternalNotificationConfig.alertMessage = config.externalNotification.alertMessage + newExternalNotificationConfig.alertMessageBuzzer = config.externalNotification.alertMessageBuzzer + newExternalNotificationConfig.alertMessageVibra = config.externalNotification.alertMessageVibra newExternalNotificationConfig.active = config.externalNotification.active newExternalNotificationConfig.output = Int32(config.externalNotification.output) + newExternalNotificationConfig.outputBuzzer = Int32(config.externalNotification.outputBuzzer) + newExternalNotificationConfig.outputVibra = Int32(config.externalNotification.outputVibra) newExternalNotificationConfig.outputMilliseconds = Int32(config.externalNotification.outputMs) + newExternalNotificationConfig.nagTimeout = Int32(config.externalNotification.nagTimeout) fetchedNode[0].externalNotificationConfig = newExternalNotificationConfig } else { fetchedNode[0].externalNotificationConfig?.enabled = config.externalNotification.enabled + fetchedNode[0].externalNotificationConfig?.usePWM = config.externalNotification.usePwm fetchedNode[0].externalNotificationConfig?.alertBell = config.externalNotification.alertBell + fetchedNode[0].externalNotificationConfig?.alertBellBuzzer = config.externalNotification.alertBellBuzzer + fetchedNode[0].externalNotificationConfig?.alertBellVibra = config.externalNotification.alertBellVibra fetchedNode[0].externalNotificationConfig?.alertMessage = config.externalNotification.alertMessage + fetchedNode[0].externalNotificationConfig?.alertMessageBuzzer = config.externalNotification.alertMessageBuzzer + fetchedNode[0].externalNotificationConfig?.alertMessageVibra = config.externalNotification.alertMessageVibra fetchedNode[0].externalNotificationConfig?.active = config.externalNotification.active fetchedNode[0].externalNotificationConfig?.output = Int32(config.externalNotification.output) + fetchedNode[0].externalNotificationConfig?.outputBuzzer = Int32(config.externalNotification.outputBuzzer) + fetchedNode[0].externalNotificationConfig?.outputVibra = Int32(config.externalNotification.outputVibra) fetchedNode[0].externalNotificationConfig?.outputMilliseconds = Int32(config.externalNotification.outputMs) + fetchedNode[0].externalNotificationConfig?.nagTimeout = Int32(config.externalNotification.nagTimeout) } do { @@ -681,7 +724,6 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO myInfoEntity.hasGps = myInfo.hasGps_p myInfoEntity.hasWifi = myInfo.hasWifi_p myInfoEntity.bitrate = myInfo.bitrate - // Swift does strings weird, this does work to get the version without the github hash let lastDotIndex = myInfo.firmwareVersion.lastIndex(of: ".") var version = myInfo.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset: 6, in: myInfo.firmwareVersion))] @@ -692,15 +734,12 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO myInfoEntity.maxChannels = Int32(bitPattern: myInfo.maxChannels) do { - try context.save() MeshLogger.log("💾 Saved a new myInfo for node number: \(String(myInfo.myNodeNum))") return myInfoEntity } catch { - context.rollback() - let nsError = error as NSError print("💥 Error Inserting New Core Data MyInfoEntity: \(nsError)") } @@ -711,7 +750,6 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO fetchedMyInfo[0].myNodeNum = Int64(myInfo.myNodeNum) fetchedMyInfo[0].hasGps = myInfo.hasGps_p fetchedMyInfo[0].bitrate = myInfo.bitrate - let lastDotIndex = myInfo.firmwareVersion.lastIndex(of: ".")//.lastIndex(of: ".", offsetBy: -1) var version = myInfo.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset:6, in: myInfo.firmwareVersion))] version = version.dropLast() @@ -721,20 +759,16 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO fetchedMyInfo[0].maxChannels = Int32(bitPattern: myInfo.maxChannels) do { - try context.save() MeshLogger.log("💾 Updated myInfo for node number: \(String(myInfo.myNodeNum))") return fetchedMyInfo[0] } catch { - context.rollback() - let nsError = error as NSError print("💥 Error Updating Core Data MyInfoEntity: \(nsError)") } } - } catch { print("💥 Fetch MyInfo Error") @@ -1109,7 +1143,6 @@ func positionPacket (packet: MeshPacket, context: NSManagedObjectContext) { } func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) { - print("Routing packet", packet) if let routingMessage = try? Routing(serializedData: packet.decoded.payload) { @@ -1285,8 +1318,9 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM if fetchedUsers.first(where: { $0.num == packet.from }) != nil { newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) } - newMessage.messagePayload = messageText + newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: messageText) + newMessage.fromUser?.objectWillChange.send() newMessage.toUser?.objectWillChange.send() @@ -1320,6 +1354,10 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM do { let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as! [MyInfoEntity] for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] { + if channel.index == newMessage.channel { + context.refresh(channel, mergeChanges: true) + } + if channel.index == newMessage.channel && !channel.mute { // Create an iOS Notification for the received private channel message and schedule it immediately let manager = LocalNotificationManager() diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index d57dfe2f..a8bccd83 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV3.xcdatamodel + MeshtasticDataModelV4.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV4.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV4.xcdatamodel/contents new file mode 100644 index 00000000..11a72c34 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV4.xcdatamodel/contents @@ -0,0 +1,277 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Protobufs/module_config.pb.swift b/Meshtastic/Protobufs/module_config.pb.swift index 3f0709a2..19fa66f7 100644 --- a/Meshtastic/Protobufs/module_config.pb.swift +++ b/Meshtastic/Protobufs/module_config.pb.swift @@ -481,33 +481,71 @@ struct ModuleConfig { // methods supported on all messages. /// - /// Preferences for the ExternalNotificationModule + /// Enable the ExternalNotificationModule var enabled: Bool = false /// - /// TODO: REPLACE + /// When using in On/Off mode, keep the output on for this many + /// milliseconds. Default 1000ms (1 second). var outputMs: UInt32 = 0 /// - /// TODO: REPLACE + /// Define the output pin GPIO setting Defaults to + /// EXT_NOTIFY_OUT if set for the board. + /// In standalone devices this pin should drive the LED to match the UI. var output: UInt32 = 0 /// - /// TODO: REPLACE + /// Optional: Define a secondary output pin for a vibra motor + /// This is used in standalone devices to match the UI. + var outputVibra: UInt32 = 0 + + /// + /// Optional: Define a tertiary output pin for an active buzzer + /// This is used in standalone devices to to match the UI. + var outputBuzzer: UInt32 = 0 + + /// + /// IF this is true, the 'output' Pin will be pulled active high, false + /// means active low. var active: Bool = false /// - /// TODO: REPLACE + /// True: Alert when a text message arrives (output) var alertMessage: Bool = false /// - /// TODO: REPLACE + /// True: Alert when a text message arrives (output_vibra) + var alertMessageVibra: Bool = false + + /// + /// True: Alert when a text message arrives (output_buzzer) + var alertMessageBuzzer: Bool = false + + /// + /// True: Alert when the bell character is received (output) var alertBell: Bool = false /// - /// TODO: REPLACE + /// True: Alert when the bell character is received (output_vibra) + var alertBellVibra: Bool = false + + /// + /// True: Alert when the bell character is received (output_buzzer) + var alertBellBuzzer: Bool = false + + /// + /// use a PWM output instead of a simple on/off output. This will ignore + /// the 'output', 'output_ms' and 'active' settings and use the + /// device.buzzer_gpio instead. var usePwm: Bool = false + /// + /// The notification will toggle with 'output_ms' for this time of seconds. + /// Default is 0 which means don't repeat at all. 60 would mean blink + /// and/or beep for 60 seconds + var nagTimeout: UInt32 = 0 + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -1248,10 +1286,17 @@ extension ModuleConfig.ExternalNotificationConfig: SwiftProtobuf.Message, SwiftP 1: .same(proto: "enabled"), 2: .standard(proto: "output_ms"), 3: .same(proto: "output"), + 8: .standard(proto: "output_vibra"), + 9: .standard(proto: "output_buzzer"), 4: .same(proto: "active"), 5: .standard(proto: "alert_message"), + 10: .standard(proto: "alert_message_vibra"), + 11: .standard(proto: "alert_message_buzzer"), 6: .standard(proto: "alert_bell"), + 12: .standard(proto: "alert_bell_vibra"), + 13: .standard(proto: "alert_bell_buzzer"), 7: .standard(proto: "use_pwm"), + 14: .standard(proto: "nag_timeout"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1267,6 +1312,13 @@ extension ModuleConfig.ExternalNotificationConfig: SwiftProtobuf.Message, SwiftP case 5: try { try decoder.decodeSingularBoolField(value: &self.alertMessage) }() case 6: try { try decoder.decodeSingularBoolField(value: &self.alertBell) }() case 7: try { try decoder.decodeSingularBoolField(value: &self.usePwm) }() + case 8: try { try decoder.decodeSingularUInt32Field(value: &self.outputVibra) }() + case 9: try { try decoder.decodeSingularUInt32Field(value: &self.outputBuzzer) }() + case 10: try { try decoder.decodeSingularBoolField(value: &self.alertMessageVibra) }() + case 11: try { try decoder.decodeSingularBoolField(value: &self.alertMessageBuzzer) }() + case 12: try { try decoder.decodeSingularBoolField(value: &self.alertBellVibra) }() + case 13: try { try decoder.decodeSingularBoolField(value: &self.alertBellBuzzer) }() + case 14: try { try decoder.decodeSingularUInt32Field(value: &self.nagTimeout) }() default: break } } @@ -1294,6 +1346,27 @@ extension ModuleConfig.ExternalNotificationConfig: SwiftProtobuf.Message, SwiftP if self.usePwm != false { try visitor.visitSingularBoolField(value: self.usePwm, fieldNumber: 7) } + if self.outputVibra != 0 { + try visitor.visitSingularUInt32Field(value: self.outputVibra, fieldNumber: 8) + } + if self.outputBuzzer != 0 { + try visitor.visitSingularUInt32Field(value: self.outputBuzzer, fieldNumber: 9) + } + if self.alertMessageVibra != false { + try visitor.visitSingularBoolField(value: self.alertMessageVibra, fieldNumber: 10) + } + if self.alertMessageBuzzer != false { + try visitor.visitSingularBoolField(value: self.alertMessageBuzzer, fieldNumber: 11) + } + if self.alertBellVibra != false { + try visitor.visitSingularBoolField(value: self.alertBellVibra, fieldNumber: 12) + } + if self.alertBellBuzzer != false { + try visitor.visitSingularBoolField(value: self.alertBellBuzzer, fieldNumber: 13) + } + if self.nagTimeout != 0 { + try visitor.visitSingularUInt32Field(value: self.nagTimeout, fieldNumber: 14) + } try unknownFields.traverse(visitor: &visitor) } @@ -1301,10 +1374,17 @@ extension ModuleConfig.ExternalNotificationConfig: SwiftProtobuf.Message, SwiftP if lhs.enabled != rhs.enabled {return false} if lhs.outputMs != rhs.outputMs {return false} if lhs.output != rhs.output {return false} + if lhs.outputVibra != rhs.outputVibra {return false} + if lhs.outputBuzzer != rhs.outputBuzzer {return false} if lhs.active != rhs.active {return false} if lhs.alertMessage != rhs.alertMessage {return false} + if lhs.alertMessageVibra != rhs.alertMessageVibra {return false} + if lhs.alertMessageBuzzer != rhs.alertMessageBuzzer {return false} if lhs.alertBell != rhs.alertBell {return false} + if lhs.alertBellVibra != rhs.alertBellVibra {return false} + if lhs.alertBellBuzzer != rhs.alertBellBuzzer {return false} if lhs.usePwm != rhs.usePwm {return false} + if lhs.nagTimeout != rhs.nagTimeout {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 68f407e3..ba3c638a 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -9,7 +9,7 @@ import SwiftUI import CoreData struct ChannelMessageList: View { - + @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @EnvironmentObject var userSettings: UserSettings @@ -29,7 +29,7 @@ struct ChannelMessageList: View { @State private var deleteMessageId: Int64 = 0 @State private var replyMessageId: Int64 = 0 @State private var sendPositionWithMessage: Bool = false - + var body: some View { NavigationStack { ScrollViewReader { scrollView in @@ -60,7 +60,10 @@ struct ChannelMessageList: View { .offset(y: -5) } VStack(alignment: currentUser ? .trailing : .leading) { - Text(message.messagePayload ?? "EMPTY MESSAGE") + let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE")) + let linkBlue = Color(red: 0.4627, green: 0.8392, blue: 1) /* #76d6ff */ + Text(markdownText) + .tint(linkBlue) .padding(10) .foregroundColor(.white) .background(currentUser ? .accentColor : Color(.gray)) @@ -241,7 +244,7 @@ struct ChannelMessageList: View { typingMessage = "📍 " + userLongName + " has shared their position with you." } - + } label: { Text("share.position") Image(systemName: "mappin.and.ellipse") @@ -257,7 +260,7 @@ struct ChannelMessageList: View { } #endif HStack(alignment: .top) { - + ZStack { let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0) TextField("message", text: $typingMessage, axis: .vertical) @@ -293,13 +296,13 @@ struct ChannelMessageList: View { typingMessage = "📍 " + userLongName + " has shared their position with you." } - + } label: { Image(systemName: "mappin.and.ellipse") .symbolRenderingMode(.hierarchical) .imageScale(.large).foregroundColor(.accentColor) } - + ProgressView("\(NSLocalizedString("bytes", comment: "")): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) .frame(width: 130) .padding(5) @@ -313,18 +316,18 @@ struct ChannelMessageList: View { .frame(minHeight: 50) .keyboardShortcut(.defaultAction) .onSubmit { - #if targetEnvironment(macCatalyst) + #if targetEnvironment(macCatalyst) if bleManager.sendMessage(message: typingMessage, toUserNum: 0, channel: channel.index, isEmoji: false, replyID: replyMessageId) { typingMessage = "" focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: Int64(channel.index), wantAck: true) { + if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false) { print("Location Sent") } } } - #endif + #endif } Text(typingMessage).opacity(0).padding(.all, 0) } diff --git a/Meshtastic/Views/Messages/Contacts.swift b/Meshtastic/Views/Messages/Contacts.swift index 308e4188..d009d5f6 100644 --- a/Meshtastic/Views/Messages/Contacts.swift +++ b/Meshtastic/Views/Messages/Contacts.swift @@ -23,6 +23,7 @@ struct Contacts: View { @State private var selection: UserEntity? = nil // Nothing selected by default. @State private var isPresentingDeleteChannelMessagesConfirm: Bool = false @State private var isPresentingDeleteUserMessagesConfirm: Bool = false + @State private var isPresentingTraceRouteSentAlert = false var body: some View { @@ -194,6 +195,14 @@ struct Contacts: View { } label: { Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash") } + Button { + let success = bleManager.sendTraceRouteRequest(destNum: user.num, wantResponse: true) + if success { + isPresentingTraceRouteSentAlert = true + } + } label: { + Label("Trace Route", systemImage: "signpost.right.and.left") + } if user.messageList.count > 0 { Button(role: .destructive) { isPresentingDeleteUserMessagesConfirm = true @@ -202,12 +211,21 @@ struct Contacts: View { } } } + .alert( + "Trace Route Sent", + isPresented: $isPresentingTraceRouteSentAlert + ) + { + Button("OK", role: .cancel) { } + } + message: { + Text("This could take a while, response will appear in the mesh log.") + } .confirmationDialog( "This conversation will be deleted.", isPresented: $isPresentingDeleteUserMessagesConfirm, titleVisibility: .visible ) { - Button(role: .destructive) { deleteUserMessages(user: user, context: context) context.refresh(node!.user!, mergeChanges: true) diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 6dda3220..83fd25f3 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -9,7 +9,7 @@ import SwiftUI import CoreData struct UserMessageList: View { - + @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @EnvironmentObject var userSettings: UserSettings @@ -28,8 +28,8 @@ struct UserMessageList: View { @State private var deleteMessageId: Int64 = 0 @State private var replyMessageId: Int64 = 0 @State private var sendPositionWithMessage: Bool = false - - var body: some View { + + var body: some View { NavigationStack { ScrollViewReader { scrollView in ScrollView { @@ -37,7 +37,7 @@ struct UserMessageList: View { ForEach( user.messageList ) { (message: MessageEntity) in if user.num != userSettings.preferredNodeNum { let currentUser: Bool = (userSettings.preferredNodeNum == message.fromUser?.num ? true : false) - + if message.replyID > 0 { let messageReply = user.messageList.first(where: { $0.messageId == message.replyID }) HStack { @@ -61,7 +61,11 @@ struct UserMessageList: View { .offset(y: -5) } VStack(alignment: currentUser ? .trailing : .leading) { - Text(message.messagePayload ?? "EMPTY MESSAGE") + let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE")) + + let linkBlue = Color(red: 0.4627, green: 0.8392, blue: 1) /* #76d6ff */ + Text(markdownText) + .tint(linkBlue) .padding(10) .foregroundColor(.white) .background(currentUser ? .accentColor : Color(.gray)) @@ -256,7 +260,7 @@ struct UserMessageList: View { .padding(.trailing) } #endif - + HStack(alignment: .top) { ZStack { let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0) @@ -310,18 +314,18 @@ struct UserMessageList: View { .frame(minHeight: 50) .keyboardShortcut(.defaultAction) .onSubmit { - #if targetEnvironment(macCatalyst) + #if targetEnvironment(macCatalyst) if bleManager.sendMessage(message: typingMessage, toUserNum: user.num, channel: 0, isEmoji: false, replyID: replyMessageId) { typingMessage = "" focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendPosition(destNum: user.num, wantAck: true) { + if bleManager.sendPosition(destNum: user.num, wantResponse: true) { print("Location Sent") } } } - #endif + #endif } Text(typingMessage).opacity(0).padding(.all, 0) } @@ -361,5 +365,5 @@ struct UserMessageList: View { } } } - } + } } diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index 198f1f72..903d94b3 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -75,43 +75,50 @@ struct DeviceMetricsLog: View { } else { ScrollView { - Grid(alignment: .topLeading, horizontalSpacing: 2) { + + let columns = [ + GridItem(), + GridItem(), + GridItem(), + GridItem(), + GridItem(.fixed(120)) + ] + LazyVGrid(columns: columns, alignment: .leading, spacing: 1) { GridRow { Text("Batt") - .font(.callout) + .font(.caption) .fontWeight(.bold) Text("Voltage") - .font(.callout) + .font(.caption) .fontWeight(.bold) Text("ChUtil") - .font(.callout) + .font(.caption) .fontWeight(.bold) Text("AirTm") - .font(.callout) + .font(.caption) .fontWeight(.bold) Text("Timestamp") - .font(.callout) + .font(.caption) .fontWeight(.bold) } - Divider() ForEach(node.telemetries!.reversed() as! [TelemetryEntity], id: \.self) { (dm: TelemetryEntity) in if dm.metricsType == 0 { GridRow { if dm.batteryLevel == 0 { Text("USB") - .font(.callout) + .font(.caption) } else { Text("\(String(dm.batteryLevel))%") - .font(.callout) + .font(.caption) } Text(String(dm.voltage)) - .font(.callout) + .font(.caption) Text("\(String(format: "%.2f", dm.channelUtilization))%") - .font(.callout) + .font(.caption) Text("\(String(format: "%.2f", dm.airUtilTx))%") - .font(.callout) + .font(.caption) Text(dm.time?.formattedDate(format: "MM/dd/yy hh:mm") ?? "Unknown time") - .font(.callout) + .font(.caption) } } } diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index 9b8c803a..18f72249 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -63,8 +63,14 @@ struct EnvironmentMetricsLog: View { } } else { ScrollView { - - Grid(alignment: .topLeading, horizontalSpacing: 2) { + let columns = [ + GridItem(), + GridItem(), + GridItem(), + GridItem(), + GridItem(.fixed(115)) + ] + LazyVGrid(columns: columns, alignment: .leading, spacing: 1) { GridRow { @@ -80,17 +86,10 @@ struct EnvironmentMetricsLog: View { Text("Gas") .font(.caption) .fontWeight(.bold) - Text("DC") - .font(.caption) - .fontWeight(.bold) - Text("Volt") - .font(.caption) - .fontWeight(.bold) Text("Timestamp") .font(.caption) .fontWeight(.bold) } - Divider() ForEach(node.telemetries!.reversed() as! [TelemetryEntity], id: \.self) { (em: TelemetryEntity) in if em.metricsType == 1 { @@ -105,10 +104,6 @@ struct EnvironmentMetricsLog: View { .font(.caption) Text("\(String(format: "%.2f", em.gasResistance))") .font(.caption) - Text("\(String(format: "%.2f", em.current))") - .font(.caption) - Text("\(String(format: "%.2f", em.voltage))") - .font(.caption) Text(em.time?.formattedDate(format: "MM/dd/yy hh:mm") ?? "Unknown time") .font(.caption) } diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index 46333fe1..4554c7b9 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -58,7 +58,14 @@ struct PositionLog: View { ScrollView { // Use a grid on iOS as a table only shows a single column - Grid(alignment: .topLeading, horizontalSpacing: 2) { + let columns = [ + GridItem(.fixed(95)), + GridItem(.fixed(95)), + GridItem(), + GridItem(), + GridItem(.fixed(115)) + ] + LazyVGrid(columns: columns, alignment: .leading, spacing: 1) { GridRow { @@ -78,7 +85,6 @@ struct PositionLog: View { .font(.caption2) .fontWeight(.bold) } - Divider() ForEach(node.positions!.reversed() as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in GridRow { Text(String(format: "%.6f", mappin.latitude ?? 0)) diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 7f73dc12..486c3693 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -1,9 +1,9 @@ -// -// ShareChannel.swift -// MeshtasticApple -// -// Copyright(c) Garth Vander Houwen 4/8/22. -// +//// +//// ShareChannel.swift +//// MeshtasticApple +//// +//// Copyright(c) Garth Vander Houwen 4/8/22. +//// //import SwiftUI //import CoreData // diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index 69896e77..c3a9b9fe 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -7,7 +7,8 @@ import SwiftUI enum OutputIntervals: Int, CaseIterable, Identifiable { - + + case unset = 0 case oneSecond = 1000 case twoSeconds = 2000 case threeSeconds = 3000 @@ -17,12 +18,14 @@ enum OutputIntervals: Int, CaseIterable, Identifiable { case fifteenSeconds = 15000 case thirtySeconds = 30000 case oneMinute = 60000 - + var id: Int { self.rawValue } var description: String { get { switch self { - + + case .unset: + return "Unset" case .oneSecond: return "One Second" case .twoSeconds: @@ -58,151 +61,249 @@ struct ExternalNotificationConfig: View { @State var hasChanges = false @State var enabled = false @State var alertBell = false + @State var alertBellBuzzer = false + @State var alertBellVibra = false @State var alertMessage = false + @State var alertMessageBuzzer = false + @State var alertMessageVibra = false @State var active = false - @State var usePWM = true + @State var usePWM = false @State var output = 0 + @State var outputBuzzer = 0 + @State var outputVibra = 0 @State var outputMilliseconds = 0 + @State var nagTimeout = 0 var body: some View { - VStack { - Form { - Section(header: Text("options")) { - Toggle(isOn: $enabled) { - Label("enabled", systemImage: "megaphone") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $alertBell) { - Label("Alert when receiving a bell", systemImage: "bell") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $alertMessage) { - Label("Alert when receiving a message", systemImage: "message") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $usePWM) { - Label("Use PWM Buzzer", systemImage: "light.beacon.max.fill") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Form { + Section(header: Text("options")) { + Toggle(isOn: $enabled) { + Label("enabled", systemImage: "megaphone") } - if !usePWM { - Section(header: Text("GPIO")) { - Toggle(isOn: $active) { - Label("Active", systemImage: "togglepower") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("Specifies whether the external circuit is triggered when the device's GPIO is low or high.") - .font(.caption) - .listRowSeparator(.visible) - Picker("GPIO to monitor", selection: $output) { - ForEach(0..<40) { - if $0 == 0 { - Text("Unset") - } else { - Text("Pin \($0)") - } - } - } - .pickerStyle(DefaultPickerStyle()) - Text("Specifies the GPIO that your external circuit is attached to on the device.") - .font(.caption) - Picker("GPIO Output Duration", selection: $outputMilliseconds ) { - ForEach(OutputIntervals.allCases) { oi in - Text(oi.description) - } - } - .pickerStyle(DefaultPickerStyle()) - Text("Specifies how long the monitored GPIO should output.") - .font(.caption) + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $alertBell) { + Label("Alert when receiving a bell", systemImage: "bell") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $alertMessage) { + Label("Alert when receiving a message", systemImage: "message") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $usePWM) { + Label("Use PWM Buzzer", systemImage: "light.beacon.max.fill") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead.") + .font(.caption) + } + if !usePWM { + Section(header: Text("Primary GPIO")) { + Toggle(isOn: $active) { + Label("Active", systemImage: "togglepower") } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Text("If enabled, the 'output' Pin will be pulled active high, disabled means active low.") + .font(.caption) + Picker("Output pin GPIO", selection: $output) { + ForEach(0..<40) { + if $0 == 0 { + Text("Unset") + } else { + Text("Pin \($0)") + } + } + } + .pickerStyle(DefaultPickerStyle()) + Picker("GPIO Output Duration", selection: $outputMilliseconds ) { + ForEach(OutputIntervals.allCases) { oi in + Text(oi.description) + } + } + .pickerStyle(DefaultPickerStyle()) + Text("When using in GPIO mode, keep the output on for this long. ") + .font(.caption) + Picker("Nag timeout", selection: $nagTimeout ) { + ForEach(OutputIntervals.allCases) { oi in + Text(oi.description) + } + } + .pickerStyle(DefaultPickerStyle()) + Text("Specifies how long the monitored GPIO should output.") + .font(.caption) } - } - .disabled(bleManager.connectedPeripheral == nil) - Button { - isPresentingSaveConfirm = true - } label: { - Label("save", systemImage: "square.and.arrow.down") - } - .disabled(bleManager.connectedPeripheral == nil || !hasChanges) - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .confirmationDialog( - "are.you.sure", - isPresented: $isPresentingSaveConfirm, - titleVisibility: .visible - ) { - Button("Save External Notification Module Config to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") { - var enc = ModuleConfig.ExternalNotificationConfig() - enc.enabled = enabled - enc.alertBell = alertBell - enc.alertMessage = alertMessage - enc.active = active - enc.output = UInt32(output) - enc.outputMs = UInt32(outputMilliseconds) - enc.usePwm = usePWM - let adminMessageId = bleManager.saveExternalNotificationModuleConfig(config: enc, fromUser: node!.user!, toUser: node!.user!) - 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() + Section(header: Text("Optional GPIO")) { + Toggle(isOn: $alertBellBuzzer) { + Label("Alert GPIO buzzer when receiving a bell", systemImage: "bell") } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $alertBellVibra) { + Label("Alert GPIO vibra motor when receiving a bell", systemImage: "bell") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $alertMessageBuzzer) { + Label("Alert GPIO buzzer when receiving a message", systemImage: "message") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $alertMessageBuzzer) { + Label("Alert GPIO vibra motor when receiving a message", systemImage: "message") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Picker("Output pin buzzer GPIO ", selection: $outputBuzzer) { + ForEach(0..<40) { + if $0 == 0 { + Text("Unset") + } else { + Text("Pin \($0)") + } + } + } + .pickerStyle(DefaultPickerStyle()) + Picker("Output pin vibra GPIO", selection: $outputVibra) { + ForEach(0..<40) { + if $0 == 0 { + Text("Unset") + } else { + Text("Pin \($0)") + } + } + } + .pickerStyle(DefaultPickerStyle()) } } - .navigationTitle("external.notification.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") - }) - .onAppear { - self.bleManager.context = context - self.enabled = node?.externalNotificationConfig?.enabled ?? false - self.alertBell = node?.externalNotificationConfig?.alertBell ?? false - self.alertMessage = node?.externalNotificationConfig?.alertMessage ?? false - self.active = node?.externalNotificationConfig?.active ?? false - self.output = Int(node?.externalNotificationConfig?.output ?? 0) - self.outputMilliseconds = Int(node?.externalNotificationConfig?.outputMilliseconds ?? 0) - self.usePWM = node?.externalNotificationConfig?.usePWM ?? true - self.hasChanges = false - } - .onChange(of: enabled) { newEnabled in - if node != nil && node!.externalNotificationConfig != nil { - if newEnabled != node!.externalNotificationConfig!.enabled { hasChanges = true } + } + .disabled(bleManager.connectedPeripheral == nil) + Button { + isPresentingSaveConfirm = true + } label: { + Label("save", systemImage: "square.and.arrow.down") + } + .disabled(bleManager.connectedPeripheral == nil || !hasChanges) + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .confirmationDialog( + "are.you.sure", + isPresented: $isPresentingSaveConfirm, + titleVisibility: .visible + ) { + Button("Save External Notification Module Config to \(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown")?") { + var enc = ModuleConfig.ExternalNotificationConfig() + enc.enabled = enabled + enc.alertBell = alertBell + enc.alertBellBuzzer = alertBellBuzzer + enc.alertBellVibra = alertBellVibra + enc.alertMessage = alertMessage + enc.alertMessageBuzzer = alertMessageBuzzer + enc.alertMessageVibra = alertMessageVibra + enc.active = active + enc.output = UInt32(output) + enc.outputBuzzer = UInt32(outputBuzzer) + enc.outputVibra = UInt32(outputVibra) + enc.outputMs = UInt32(outputMilliseconds) + enc.usePwm = usePWM + let adminMessageId = bleManager.saveExternalNotificationModuleConfig(config: enc, fromUser: node!.user!, toUser: node!.user!) + 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() } } - .onChange(of: alertBell) { newAlertBell in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertBell != node!.externalNotificationConfig!.alertBell { hasChanges = true } - } + } + .navigationTitle("external.notification.config") + .navigationBarItems(trailing: + ZStack { + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") + }) + .onAppear { + self.bleManager.context = context + self.enabled = node?.externalNotificationConfig?.enabled ?? false + self.alertBell = node?.externalNotificationConfig?.alertBell ?? false + self.alertBellBuzzer = node?.externalNotificationConfig?.alertBellBuzzer ?? false + self.alertBellVibra = node?.externalNotificationConfig?.alertBellVibra ?? false + self.alertMessage = node?.externalNotificationConfig?.alertMessage ?? false + self.alertMessageBuzzer = node?.externalNotificationConfig?.alertMessageBuzzer ?? false + self.alertMessageVibra = node?.externalNotificationConfig?.alertMessageVibra ?? false + self.active = node?.externalNotificationConfig?.active ?? false + self.output = Int(node?.externalNotificationConfig?.output ?? 0) + self.outputBuzzer = Int(node?.externalNotificationConfig?.outputBuzzer ?? 0) + self.outputVibra = Int(node?.externalNotificationConfig?.outputVibra ?? 0) + self.outputMilliseconds = Int(node?.externalNotificationConfig?.outputMilliseconds ?? 0) + self.nagTimeout = Int(node?.externalNotificationConfig?.nagTimeout ?? 0) + self.usePWM = node?.externalNotificationConfig?.usePWM ?? false + self.hasChanges = false + } + .onChange(of: enabled) { newEnabled in + if node != nil && node!.externalNotificationConfig != nil { + if newEnabled != node!.externalNotificationConfig!.enabled { hasChanges = true } } - .onChange(of: alertMessage) { newAlertMessage in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertMessage != node!.externalNotificationConfig!.alertMessage { hasChanges = true } - } + } + .onChange(of: alertBell) { newAlertBell in + if node != nil && node!.externalNotificationConfig != nil { + if newAlertBell != node!.externalNotificationConfig!.alertBell { hasChanges = true } } - .onChange(of: active) { newActuve in - if node != nil && node!.externalNotificationConfig != nil { - if newActuve != node!.externalNotificationConfig!.active { hasChanges = true } - } + } + .onChange(of: alertBellBuzzer) { newAlertBellBuzzer in + if node != nil && node!.externalNotificationConfig != nil { + if newAlertBellBuzzer != node!.externalNotificationConfig!.alertBellBuzzer { hasChanges = true } } - .onChange(of: output) { newOutput in - if node != nil && node!.externalNotificationConfig != nil { - if newOutput != node!.externalNotificationConfig!.output { hasChanges = true } - } + } + .onChange(of: alertBellVibra) { newAlertBellVibra in + if node != nil && node!.externalNotificationConfig != nil { + if newAlertBellVibra != node!.externalNotificationConfig!.alertBellVibra { hasChanges = true } } - .onChange(of: outputMilliseconds) { newOutputMs in - if node != nil && node!.externalNotificationConfig != nil { - if newOutputMs != node!.externalNotificationConfig!.outputMilliseconds { hasChanges = true } - } + } + .onChange(of: alertMessage) { newAlertMessage in + if node != nil && node!.externalNotificationConfig != nil { + if newAlertMessage != node!.externalNotificationConfig!.alertMessage { hasChanges = true } } - .onChange(of: usePWM) { newUsePWM in - if node != nil && node!.externalNotificationConfig != nil { - if newUsePWM != node!.externalNotificationConfig!.usePWM { hasChanges = true } - } + } + .onChange(of: alertMessageBuzzer) { newAlertMessageBuzzer in + if node != nil && node!.externalNotificationConfig != nil { + if newAlertMessageBuzzer != node!.externalNotificationConfig!.alertMessageBuzzer { hasChanges = true } + } + } + .onChange(of: alertMessageVibra) { newAlertMessageVibra in + if node != nil && node!.externalNotificationConfig != nil { + if newAlertMessageVibra != node!.externalNotificationConfig!.alertMessageVibra { hasChanges = true } + } + } + .onChange(of: active) { newActive in + if node != nil && node!.externalNotificationConfig != nil { + if newActive != node!.externalNotificationConfig!.active { hasChanges = true } + } + } + .onChange(of: output) { newOutput in + if node != nil && node!.externalNotificationConfig != nil { + if newOutput != node!.externalNotificationConfig!.output { hasChanges = true } + } + } + .onChange(of: output) { newOutputBuzzer in + if node != nil && node!.externalNotificationConfig != nil { + if newOutputBuzzer != node!.externalNotificationConfig!.outputBuzzer { hasChanges = true } + } + } + .onChange(of: output) { newOutputVibra in + if node != nil && node!.externalNotificationConfig != nil { + if newOutputVibra != node!.externalNotificationConfig!.outputVibra { hasChanges = true } + } + } + .onChange(of: outputMilliseconds) { newOutputMs in + if node != nil && node!.externalNotificationConfig != nil { + if newOutputMs != node!.externalNotificationConfig!.outputMilliseconds { hasChanges = true } + } + } + .onChange(of: usePWM) { newUsePWM in + if node != nil && node!.externalNotificationConfig != nil { + if newUsePWM != node!.externalNotificationConfig!.usePWM { hasChanges = true } + } + } + .onChange(of: nagTimeout) { newNagTimeout in + if node != nil && node!.externalNotificationConfig != nil { + if newNagTimeout != node!.externalNotificationConfig!.nagTimeout { hasChanges = true } } } }