Merge pull request #269 from meshtastic/2.0.9_Working_changes

Version 2.0.9 Working Changes
This commit is contained in:
Garth Vander Houwen 2022-12-28 09:43:45 -08:00 committed by GitHub
commit 1f3333dd34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 780 additions and 206 deletions

View file

@ -232,6 +232,7 @@
DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeshtasticTests.swift; sourceTree = "<group>"; };
DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeText.swift; sourceTree = "<group>"; };
DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntityExtension.swift; sourceTree = "<group>"; };
DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = "<group>"; };
DDF924C926FBB953009FE055 /* ConnectedDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = "<group>"; };
/* 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 = "<group>";

View file

@ -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
}

View file

@ -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()

View file

@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>MeshtasticDataModelV3.xcdatamodel</string>
<string>MeshtasticDataModelV4.xcdatamodel</string>
</dict>
</plist>

View file

@ -0,0 +1,277 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22A400" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="BluetoothConfigEntity" representedClassName="BluetoothConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="fixedPin" optional="YES" attributeType="Integer 32" defaultValueString="123456" usesScalarValueType="YES"/>
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="bluetoothConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="bluetoothConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="CannedMessageConfigEntity" representedClassName="CannedMessageConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="inputbrokerEventCcw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="inputbrokerEventCw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="inputbrokerEventPress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="inputbrokerPinA" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="inputbrokerPinB" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
<attribute name="inputbrokerPinPress" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
<attribute name="rotary1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="updown1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<relationship name="cannedMessagesConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="cannedMessageConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="ChannelEntity" representedClassName="ChannelEntity" syncable="YES" codeGenerationType="class">
<attribute name="downlinkEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="index" attributeType="Integer 32" minValueString="0" maxValueString="13" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="psk" optional="YES" attributeType="Binary"/>
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="uplinkEnabled" attributeType="Boolean" usesScalarValueType="YES"/>
<relationship name="myInfoChannel" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="channels" inverseEntity="MyInfoEntity"/>
<fetchedProperty name="allPrivateMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="channel == $FETCH_SOURCE.index &amp;&amp; toUser == nil AND isEmoji == false"/>
</fetchedProperty>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="index"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="DeviceConfigEntity" representedClassName="DeviceConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="buttonGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="buzzerGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="debugLogEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="serialEnabled" optional="YES" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="deviceConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="deviceConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="DisplayConfigEntity" representedClassName="DisplayConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="compassNorthTop" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="flipScreen" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="gpsFormat" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="oledType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="screenCarouselInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="screenOnSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="displayConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="displayConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="ExternalNotificationConfigEntity" representedClassName="ExternalNotificationConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="active" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertBellBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertBellVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertMessage" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertMessageBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertMessageVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="nagTimeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="output" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="outputBuzzer" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="outputMilliseconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="outputVibra" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="usePWM" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="LoRaConfigEntity" representedClassName="LoRaConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="bandwidth" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="channelNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="codingRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="frequencyOffset" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="hopLimit" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="modemPreset" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="regionCode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="spreadFactor" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="txEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="txPower" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="usePreset" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<relationship name="loRaConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="loRaConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="MessageEntity" representedClassName="MessageEntity" syncable="YES" codeGenerationType="class">
<attribute name="ackError" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ackSNR" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="ackTimestamp" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="admin" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="adminDescription" optional="YES" attributeType="String"/>
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isEmoji" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="messageId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="messagePayload" optional="YES" attributeType="String" defaultValueString=""/>
<attribute name="messagePayloadMarkdown" optional="YES" attributeType="String"/>
<attribute name="messageTimestamp" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="receivedACK" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="replyID" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="fromUser" optional="YES" maxCount="1" deletionRule="Nullify" ordered="YES" destinationEntity="UserEntity" inverseName="sentMessages" inverseEntity="UserEntity"/>
<relationship name="toUser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="receivedMessages" inverseEntity="UserEntity"/>
<fetchedProperty name="tapbacks" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="replyID == $FETCH_SOURCE.messageId AND isEmoji == true"/>
</fetchedProperty>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="messageId"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="MQTTConfigEntity" representedClassName="MQTTConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String" maxValueString="30"/>
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="encryptionEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="jsonEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="password" optional="YES" attributeType="String" maxValueString="30"/>
<attribute name="username" optional="YES" attributeType="String" maxValueString="30"/>
<relationship name="mqttConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="mqttConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="MyInfoEntity" representedClassName="MyInfoEntity" syncable="YES" codeGenerationType="class">
<attribute name="bitrate" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="bleName" optional="YES" attributeType="String"/>
<attribute name="errorCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="firmwareVersion" attributeType="String"/>
<attribute name="hasGps" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hasWifi" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="maxChannels" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="messageTimeoutMsec" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="minAppVersion" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="myNodeNum" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="peripheralId" optional="YES" attributeType="String"/>
<attribute name="rebootCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="channels" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="ChannelEntity" inverseName="myInfoChannel" inverseEntity="ChannelEntity"/>
<relationship name="myInfoNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="myInfo" inverseEntity="NodeInfoEntity"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="myNodeNum"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="NetworkConfigEntity" representedClassName="NetworkConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="dns" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ethEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="gateway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ip" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ntpServer" optional="YES" attributeType="String"/>
<attribute name="subnet" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wifiEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="wifiMode" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="wifiPsk" optional="YES" attributeType="String" minValueString="0" maxValueString="60"/>
<attribute name="wifiSsid" optional="YES" attributeType="String" minValueString="0" maxValueString="30"/>
<relationship name="networkConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="networkConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="NodeInfoEntity" representedClassName="NodeInfoEntity" syncable="YES" codeGenerationType="class">
<attribute name="bleName" optional="YES" attributeType="String"/>
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="lastHeard" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="bluetoothConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="BluetoothConfigEntity" inverseName="bluetoothConfigNode" inverseEntity="BluetoothConfigEntity"/>
<relationship name="cannedMessageConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CannedMessageConfigEntity" inverseName="cannedMessagesConfigNode" inverseEntity="CannedMessageConfigEntity"/>
<relationship name="deviceConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceConfigEntity" inverseName="deviceConfigNode" inverseEntity="DeviceConfigEntity"/>
<relationship name="displayConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DisplayConfigEntity" inverseName="displayConfigNode" inverseEntity="DisplayConfigEntity"/>
<relationship name="externalNotificationConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ExternalNotificationConfigEntity" inverseName="externalNotificationConfigNode" inverseEntity="ExternalNotificationConfigEntity"/>
<relationship name="loRaConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LoRaConfigEntity" inverseName="loRaConfigNode" inverseEntity="LoRaConfigEntity"/>
<relationship name="mqttConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MQTTConfigEntity" inverseName="mqttConfigNode" inverseEntity="MQTTConfigEntity"/>
<relationship name="myInfo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="myInfoNode" inverseEntity="MyInfoEntity"/>
<relationship name="networkConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NetworkConfigEntity" inverseName="networkConfigNode" inverseEntity="NetworkConfigEntity"/>
<relationship name="positionConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PositionConfigEntity" inverseName="positionConfigNode" inverseEntity="PositionConfigEntity"/>
<relationship name="positions" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PositionEntity" inverseName="nodePosition" inverseEntity="PositionEntity"/>
<relationship name="rangeTestConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RangeTestConfigEntity" inverseName="rangeTestConfigNode" inverseEntity="RangeTestConfigEntity"/>
<relationship name="serialConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SerialConfigEntity" inverseName="serialConfigNode" inverseEntity="SerialConfigEntity"/>
<relationship name="telemetries" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TelemetryEntity" inverseName="nodeTelemetry" inverseEntity="TelemetryEntity"/>
<relationship name="telemetryConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TelemetryConfigEntity" inverseName="telemetryConfigNode" inverseEntity="TelemetryConfigEntity"/>
<relationship name="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="userNode" inverseEntity="UserEntity"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="num"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PositionConfigEntity" representedClassName="PositionConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="deviceGpsEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="fixedPosition" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="gpsAttemptTime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="gpsUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="positionBroadcastSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rxGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="smartPositionEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="txGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="positionConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positionConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="PositionEntity" representedClassName="PositionEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="satsInView" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="seqNo" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="nodePosition" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positions" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="RangeTestConfigEntity" representedClassName="RangeTestConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="save" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="sender" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
<relationship name="rangeTestConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rangeTestConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="SerialConfigEntity" representedClassName="SerialConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="baudRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="echo" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rxd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="timeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="txd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="serialConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="serialConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TelemetryConfigEntity" representedClassName="TelemetryConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="deviceUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="environmentDisplayFahrenheit" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="environmentMeasurementEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="environmentScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="environmentUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="telemetryConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetryConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TelemetryEntity" representedClassName="TelemetryEntity" syncable="YES" codeGenerationType="class">
<attribute name="airUtilTx" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="barometricPressure" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="batteryLevel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="channelUtilization" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="current" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="gasResistance" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="metricsType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="relativeHumidity" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="temperature" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="voltage" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="nodeTelemetry" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetries" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="UserEntity" representedClassName="UserEntity" syncable="YES" codeGenerationType="class">
<attribute name="hwModel" attributeType="String"/>
<attribute name="isLicensed" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="longName" attributeType="String"/>
<attribute name="macaddr" optional="YES" attributeType="Binary"/>
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="shortName" attributeType="String"/>
<attribute name="userId" attributeType="String"/>
<relationship name="receivedMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="toUser" inverseEntity="MessageEntity"/>
<relationship name="sentMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="fromUser" inverseEntity="MessageEntity"/>
<relationship name="userNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="user" inverseEntity="NodeInfoEntity"/>
<fetchedProperty name="adminMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(toUser.num == $FETCH_SOURCE.num) AND isEmoji == false AND admin = true"/>
</fetchedProperty>
<fetchedProperty name="allMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="((toUser.num == $FETCH_SOURCE.num) OR (fromUser.num == $FETCH_SOURCE.num)) AND toUser != nil AND fromUser != nil AND isEmoji == false AND admin = false"/>
</fetchedProperty>
</entity>
<entity name="WaypointEntity" representedClassName="WaypointEntity" syncable="YES" codeGenerationType="class">
<attribute name="expire" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="locked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="longDescription" optional="YES" attributeType="String" maxValueString="100"/>
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String" maxValueString="30"/>
</entity>
</model>

View file

@ -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<D: SwiftProtobuf.Decoder>(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
}

View file

@ -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)
}

View file

@ -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)

View file

@ -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 {
}
}
}
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}

View file

@ -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))

View file

@ -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
//

View file

@ -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 }
}
}
}