Merge pull request #374 from meshtastic/2.2.1_Working_Changes

2.2.1 working changes
This commit is contained in:
Garth Vander Houwen 2023-08-24 22:32:56 -07:00 committed by GitHub
commit 42a4057a4e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 1866 additions and 688 deletions

View file

@ -7,6 +7,9 @@
objects = {
/* Begin PBXBuildFile section */
6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */; };
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */; };
6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */; };
C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */; };
C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; };
DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */ = {isa = PBXBuildFile; productRef = DD0D3D212A55CEB10066DB71 /* CocoaMQTT */; };
@ -115,6 +118,7 @@
DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */; };
DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */; };
DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC3B273283F411B00AC321C /* LastHeardText.swift */; };
DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */; };
DDC4D568275499A500A4208E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC4D567275499A500A4208E /* Persistence.swift */; };
DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */; };
DDC94FC229CE063B0082EA6E /* BatteryLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */; };
@ -194,6 +198,9 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAppDelegate.swift; sourceTree = "<group>"; };
6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = "<group>"; };
6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = "<group>"; };
A65FA974296876BF00A97686 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMBTileOverlay.swift; sourceTree = "<group>"; };
DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV14.xcdatamodel; sourceTree = "<group>"; };
@ -318,6 +325,8 @@
DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationHelper.swift; sourceTree = "<group>"; };
DDC3B273283F411B00AC321C /* LastHeardText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastHeardText.swift; sourceTree = "<group>"; };
DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorConfig.swift; sourceTree = "<group>"; };
DDC4CA012A8DAA3800CE201C /* MeshtasticDataModelV16.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV16.xcdatamodel; sourceTree = "<group>"; };
DDC4D567275499A500A4208E /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
DDC94FC029CE063B0082EA6E /* BatteryLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryLevel.swift; sourceTree = "<group>"; };
DDC94FC329CED7280082EA6E /* MeshtasticDataModelV10.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV10.xcdatamodel; sourceTree = "<group>"; };
@ -428,6 +437,7 @@
DD90860D26F69BAE00DC5189 /* NodeMap.swift */,
DD73FD1028750779000852D6 /* PositionLog.swift */,
DD14E72D2A82A614006E39BC /* RemoteHardware.swift */,
6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */,
);
path = Nodes;
sourceTree = "<group>";
@ -517,6 +527,7 @@
DDC94FCD29CF55310082EA6E /* RtttlConfig.swift */,
DD6193782863875F00E59241 /* SerialConfig.swift */,
DD415827285859C4009B0E59 /* TelemetryConfig.swift */,
DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */,
);
path = Module;
sourceTree = "<group>";
@ -621,6 +632,7 @@
DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */,
DDC2E16526CE248F0042C5E4 /* Info.plist */,
DDC2E15D26CE248F0042C5E4 /* Preview Content */,
6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */,
);
path = Meshtastic;
sourceTree = "<group>";
@ -739,6 +751,7 @@
DD964FC52975DBFD007C176F /* QueryCoreData.swift */,
DD3CC6C128EB9D4900FA9159 /* UpdateCoreData.swift */,
DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */,
6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */,
);
path = Persistence;
sourceTree = "<group>";
@ -1002,6 +1015,7 @@
DD3501892852FC3B000FC853 /* Settings.swift in Sources */,
DDDB443629F6287000EE2349 /* MapButtons.swift in Sources */,
DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */,
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */,
DD5E5203298EE33B00D21B61 /* config.pb.swift in Sources */,
DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */,
DDA6B2EB28420A7B003E8C16 /* NodeAnnotation.swift in Sources */,
@ -1032,6 +1046,7 @@
DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */,
DD5E5209298EE33B00D21B61 /* module_config.pb.swift in Sources */,
DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */,
6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */,
DDDB444229F8A88700EE2349 /* Double.swift in Sources */,
DD5E520F298EE33B00D21B61 /* cannedmessages.pb.swift in Sources */,
DDB75A232A13CDA9006ED576 /* BatteryLevelCompact.swift in Sources */,
@ -1054,6 +1069,7 @@
DD5E520D298EE33B00D21B61 /* storeforward.pb.swift in Sources */,
DD882F5D2772E4640005BF05 /* Contacts.swift in Sources */,
DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */,
6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */,
DD47E3CE26F103C600029299 /* NodeList.swift in Sources */,
DD5E520A298EE33B00D21B61 /* channel.pb.swift in Sources */,
DDDEE5E129DA3E1100A8E078 /* NodeInfoView.swift in Sources */,
@ -1109,6 +1125,7 @@
DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */,
DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */,
DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */,
DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */,
DD5E5210298EE33B00D21B61 /* telemetry.pb.swift in Sources */,
DD5E5205298EE33B00D21B61 /* mesh.pb.swift in Sources */,
DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */,
@ -1237,7 +1254,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@ -1293,7 +1310,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
@ -1323,12 +1340,12 @@
INFOPLIST_FILE = Meshtastic/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Meshtastic;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.2.0;
MARKETING_VERSION = 2.2.2;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1357,12 +1374,12 @@
INFOPLIST_FILE = Meshtastic/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Meshtastic;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.2.0;
MARKETING_VERSION = 2.2.2;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1382,7 +1399,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = GCH7VS5Y9R;
INFOPLIST_FILE = MeshtasticTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1407,7 +1424,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = GCH7VS5Y9R;
INFOPLIST_FILE = MeshtasticTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1428,6 +1445,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = GCH7VS5Y9R;
INFOPLIST_FILE = MeshtasticUITests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1448,6 +1466,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = GCH7VS5Y9R;
INFOPLIST_FILE = MeshtasticUITests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1468,6 +1487,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = Widgets/WidgetsExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GCH7VS5Y9R;
@ -1475,15 +1495,16 @@
INFOPLIST_FILE = Widgets/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Widgets;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.0;
MARKETING_VERSION = 2.2.2;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
@ -1499,6 +1520,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = Widgets/WidgetsExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GCH7VS5Y9R;
@ -1506,15 +1528,16 @@
INFOPLIST_FILE = Widgets/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Widgets;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.2;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.0;
MARKETING_VERSION = 2.2.2;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
@ -1622,6 +1645,7 @@
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
DDC4CA012A8DAA3800CE201C /* MeshtasticDataModelV16.xcdatamodel */,
DD14E72C2A80738F006E39BC /* MeshtasticDataModelV15.xcdatamodel */,
DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */,
DDB75A1F2A10766D006ED576 /* MeshtasticDataModelV13.xcdatamodel */,
@ -1638,7 +1662,7 @@
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
);
currentVersion = DD14E72C2A80738F006E39BC /* MeshtasticDataModelV15.xcdatamodel */;
currentVersion = DDC4CA012A8DAA3800CE201C /* MeshtasticDataModelV16.xcdatamodel */;
name = Meshtastic.xcdatamodeld;
path = Meshtastic/Meshtastic.xcdatamodeld;
sourceTree = "<group>";

View file

@ -53,6 +53,21 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
return csvString
}
func detectionsToCsv(detections: [MessageEntity]) -> String {
var csvString: String = ""
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
// Create Header
csvString = "Detection event, \("timestamp".localized)"
for d in detections {
csvString += "\n"
csvString += d.messagePayload ?? "Detection"
csvString += ", "
csvString += d.timestamp.formattedDate(format: dateFormatString).localized
}
return csvString
}
func positionToCsvFile(positions: [PositionEntity]) -> String {
var csvString: String = ""
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)

View file

@ -10,45 +10,6 @@ import CocoaMQTT
// ---------------------------------------------------------------------------------------
class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate, ObservableObject {
// MqttClientProxyManagerDelegate
func onMqttConnected() {
mqttManager.status = .connected
print("📲 Mqtt Client Proxy onMqttConnected now subscribing to \(mqttManager.topic).")
mqttManager.mqttClientProxy?.subscribe(mqttManager.topic)
}
func onMqttDisconnected() {
mqttManager.status = .disconnected
print("MQTT Disconnected")
}
func onMqttMessageReceived(message: CocoaMQTTMessage) {
print("📲 Mqtt Client Proxy onMqttMessageReceived for topic: \(message.topic)")
if message.topic.contains("/stat/") {
return
}
var proxyMessage = MqttClientProxyMessage()
proxyMessage.topic = message.topic
proxyMessage.data = Data(message.payload)
proxyMessage.retained = message.retained
var toRadio: ToRadio!
toRadio = ToRadio()
toRadio.mqttClientProxyMessage = proxyMessage
let binaryData: Data = try! toRadio.serializedData()
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
print("📲 Sent Mqtt client proxy message to the connected device.")
}
}
func onMqttError(message: String) {
print("MQTT Error")
}
private static var documentsFolder: URL {
do {
return try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
@ -66,6 +27,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
@Published var invalidVersion = false
@Published var isSwitchedOn: Bool = false
@Published var automaticallyReconnect: Bool = true
@Published var mqttProxyConnected: Bool = false
public var minimumVersion = "2.0.0"
public var connectedVersion: String
public var isConnecting: Bool = false
@ -78,6 +40,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var lastPosition: CLLocationCoordinate2D?
let emptyNodeNum: UInt32 = 4294967295
let mqttManager = MqttClientProxyManager.shared
var wantRangeTestPackets = false
/* Meshtastic Service Details */
var TORADIO_characteristic: CBCharacteristic!
var FROMRADIO_characteristic: CBCharacteristic!
@ -312,6 +275,45 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
}
// MARK: MqttClientProxyManagerDelegate Methods
func onMqttConnected() {
mqttProxyConnected = true
print("📲 Mqtt Client Proxy onMqttConnected now subscribing to \(mqttManager.topic).")
mqttManager.mqttClientProxy?.subscribe(mqttManager.topic)
}
func onMqttDisconnected() {
mqttProxyConnected = false
print("MQTT Disconnected")
}
func onMqttMessageReceived(message: CocoaMQTTMessage) {
print("📲 Mqtt Client Proxy onMqttMessageReceived for topic: \(message.topic)")
if message.topic.contains("/stat/") {
return
}
var proxyMessage = MqttClientProxyMessage()
proxyMessage.topic = message.topic
proxyMessage.data = Data(message.payload)
proxyMessage.retained = message.retained
var toRadio: ToRadio!
toRadio = ToRadio()
toRadio.mqttClientProxyMessage = proxyMessage
let binaryData: Data = try! toRadio.serializedData()
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
print("📲 Sent Mqtt client proxy message to the connected device.")
}
}
func onMqttError(message: String) {
mqttProxyConnected = false
print("📲 Mqtt Client Proxy onMqttError: \(message)")
}
// MARK: Protobuf Methods
func requestDeviceMetadata(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32, context: NSManagedObjectContext) -> Int64 {
guard connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected else { return 0 }
@ -503,7 +505,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if decodedInfo.metadata.firmwareVersion.count > 0 && !invalidVersion {
nowKnown = true
deviceMetadataPacket(metadata: decodedInfo.metadata, fromNum: connectedPeripheral.num, context: context!)
connectedPeripheral.firmwareVersion = decodedInfo.metadata.firmwareVersion ?? "unknown".localized
connectedPeripheral.firmwareVersion = decodedInfo.metadata.firmwareVersion
let lastDotIndex = decodedInfo.metadata.firmwareVersion.lastIndex(of: ".")
@ -528,7 +530,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
// Log any other unknownApp calls
if !nowKnown { MeshLogger.log("🕸️ MESH PACKET received for Unknown App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")") }
case .textMessageApp:
case .textMessageApp, .detectionSensorApp:
textMessageAppPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!)
case .remoteHardwareApp:
MeshLogger.log("🕸️ MESH PACKET received for Remote Hardware App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
@ -549,9 +551,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
case .serialApp:
MeshLogger.log("🕸️ MESH PACKET received for Serial App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
case .storeForwardApp:
MeshLogger.log("🕸️ MESH PACKET received for Store Forward App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
storeAndForwardPacket(packet: decodedInfo.packet, connectedNodeNum: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!)
case .rangeTestApp:
MeshLogger.log("🕸️ MESH PACKET received for Range Test App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
if wantRangeTestPackets {
textMessageAppPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!)
}
else {
MeshLogger.log("🕸️ MESH PACKET received for Range Test App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
}
case .telemetryApp:
if !invalidVersion { telemetryPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) }
case .textMessageCompressedApp:
@ -599,7 +606,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected })
// Config conplete returns so we don't read the characteristic again
/// MQTT Client Proxy
/// MQTT Client Proxy and RangeTest interest
if connectedPeripheral.num > 0 {
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
@ -607,12 +614,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
do {
let fetchedNodeInfo = try context?.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] ?? []
if fetchedNodeInfo.count == 1 && fetchedNodeInfo[0].mqttConfig != nil {
//Subscribe to Mqtt Client Proxy if enabled
if fetchedNodeInfo[0].mqttConfig?.proxyToClientEnabled ?? false {
mqttManager.connectFromConfigSettings(node: fetchedNodeInfo[0])
}
}
if fetchedNodeInfo.count == 1 && fetchedNodeInfo[0].rangeTestConfig?.enabled == true {
wantRangeTestPackets = true;
}
} catch {
print("Failed to find a node info for the connected node")
}
@ -658,7 +668,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
self.startScanning()
// Try and connect to the preferredPeripherial first
let preferredPeripheral = peripherals.filter({ $0.peripheral.identifier.uuidString == UserDefaults.preferredPeripheralId as? String ?? "" }).first
let preferredPeripheral = peripherals.filter({ $0.peripheral.identifier.uuidString == UserDefaults.preferredPeripheralId as String }).first
if preferredPeripheral != nil && preferredPeripheral?.peripheral != nil {
connectTo(peripheral: preferredPeripheral!.peripheral)
}
@ -1533,6 +1543,32 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
return 0
}
public func saveDetectionSensorModuleConfig(config: ModuleConfig.DetectionSensorConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 {
var adminPacket = AdminMessage()
adminPacket.setModuleConfig.detectionSensor = config
var meshPacket: MeshPacket = MeshPacket()
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.to = UInt32(toUser.num)
meshPacket.from = UInt32(fromUser.num)
meshPacket.channel = UInt32(adminIndex)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.wantAck = true
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "Saved Telemetry Module Config for \(toUser.longName ?? "unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
upsertDetectionSensorModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!)
return Int64(meshPacket.id)
}
return 0
}
public func getChannel(channelIndex: UInt32, fromUser: UserEntity, toUser: UserEntity, wantResponse: Bool) -> Bool {
var adminPacket = AdminMessage()
@ -1901,6 +1937,34 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
return false
}
public func requestDetectionSensorModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool {
var adminPacket = AdminMessage()
adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.detectionsensorConfig
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(toUser.num)
meshPacket.from = UInt32(fromUser.num)
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.channel = UInt32(adminIndex)
meshPacket.wantAck = true
var dataMessage = DataMessage()
dataMessage.payload = try! adminPacket.serializedData()
dataMessage.portnum = PortNum.adminApp
dataMessage.wantResponse = true
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested Detection Sensor Module Config on admin channel \(adminIndex) for node: \(String(connectedPeripheral.num))"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
return true
}
return false
}
public func requestSerialModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool {
var adminPacket = AdminMessage()

View file

@ -10,16 +10,16 @@ class LocalNotificationManager {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
if granted == true && error == nil {
self.scheduleNotifications()
self.scheduleNotifications()
}
}
}
func schedule() {
func schedule() {
UNUserNotificationCenter.current().getNotificationSettings { settings in
switch settings.authorizationStatus {
case .notDetermined:
self.requestAuthorization()
self.requestAuthorization()
case .authorized, .provisional:
self.scheduleNotifications()
default:
@ -37,6 +37,9 @@ class LocalNotificationManager {
content.body = notification.content
content.sound = .default
content.interruptionLevel = .timeSensitive
if notification.target != nil {
content.userInfo["target"] = notification.target
}
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger)
@ -65,4 +68,5 @@ struct Notification {
var title: String
var subtitle: String
var content: String
var target: String?
}

View file

@ -68,6 +68,8 @@ func moduleConfig (config: ModuleConfig, context: NSManagedObjectContext, nodeNu
upsertSerialModuleConfigPacket(config: config.serial, nodeNum: nodeNum, context: context)
} else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.telemetry(config.telemetry) {
upsertTelemetryModuleConfigPacket(config: config.telemetry, nodeNum: nodeNum, context: context)
} else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.detectionSensor(config.detectionSensor) {
upsertDetectionSensorModuleConfigPacket(config: config.detectionSensor, nodeNum: nodeNum, context: context)
}
}
@ -90,7 +92,6 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO
myInfoEntity.peripheralId = peripheralId
myInfoEntity.myNodeNum = Int64(myInfo.myNodeNum)
myInfoEntity.rebootCount = Int32(myInfo.rebootCount)
myInfoEntity.minAppVersion = Int32(bitPattern: myInfo.minAppVersion)
do {
try context.save()
print("💾 Saved a new myInfo for node number: \(String(myInfo.myNodeNum))")
@ -105,7 +106,6 @@ func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedO
fetchedMyInfo[0].peripheralId = peripheralId
fetchedMyInfo[0].myNodeNum = Int64(myInfo.myNodeNum)
fetchedMyInfo[0].rebootCount = Int32(myInfo.rebootCount)
fetchedMyInfo[0].minAppVersion = Int32(bitPattern: myInfo.minAppVersion)
do {
try context.save()
@ -471,7 +471,8 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.telemetry(moduleConfig.telemetry) {
upsertTelemetryModuleConfigPacket(config: moduleConfig.telemetry, nodeNum: Int64(packet.from), context: context)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.detectionSensor(moduleConfig.detectionSensor) {
upsertDetectionSensorModuleConfigPacket(config: moduleConfig.detectionSensor, nodeNum: Int64(packet.from), context: context)
}
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getRingtoneResponse(adminMessage.getRingtoneResponse) {
@ -580,6 +581,47 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana
}
}
func storeAndForwardPacket(packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) {
if let storeAndForwardMessage = try? StoreAndForward(serializedData: packet.decoded.payload) {
// RequestResponse
switch storeAndForwardMessage.rr {
case .unset:
MeshLogger.log("📮 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .routerError:
MeshLogger.log("☠️ Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .routerHeartbeat:
// Query any messages since the heartbeat.period. Send their ids to the store and forward node.
MeshLogger.log("💓 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .routerPing:
MeshLogger.log("🏓 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .routerPong:
MeshLogger.log("🏓 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .routerBusy:
MeshLogger.log("🐝 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .routerHistory:
MeshLogger.log("📜 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .routerStats:
MeshLogger.log("📊 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .clientError:
MeshLogger.log("☠️ Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .clientHistory:
MeshLogger.log("📜 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .clientStats:
MeshLogger.log("📊 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .clientPing:
MeshLogger.log("🏓 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .clientPong:
MeshLogger.log("🏓 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .clientAbort:
MeshLogger.log("🛑 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
case .UNRECOGNIZED(_):
MeshLogger.log("📮 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
}
}
}
func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) {
if let telemetryMessage = try? Telemetry(serializedData: packet.decoded.payload) {
@ -644,6 +686,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
let content = UNMutableNotificationContent()
content.title = "Critically Low Battery!"
content.body = "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining."
content.userInfo["target"] = "node"
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let uuidString = UUID().uuidString
let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger)
@ -659,7 +702,6 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
}
// Update our live activity if there is one running, not available on mac iOS >= 16.2
#if !targetEnvironment(macCatalyst)
if #available(iOS 16.2, *) {
let oneMinuteLater = Calendar.current.date(byAdding: .minute, value: (Int(1) ), to: Date())!
let date = Date.now...oneMinuteLater
@ -675,7 +717,6 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
print("Updated live activity.")
}
}
}
#endif
}
} catch {
@ -703,11 +744,13 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
let newMessage = MessageEntity(context: context)
newMessage.messageId = Int64(packet.id)
newMessage.messageTimestamp = Int32(bitPattern: packet.rxTime)
newMessage.receivedTimestamp = Int32(Date().timeIntervalSince1970)
newMessage.receivedACK = false
newMessage.snr = packet.rxSnr
newMessage.rssi = packet.rxRssi
newMessage.isEmoji = packet.decoded.emoji == 1
newMessage.channel = Int32(packet.channel)
newMessage.portNum = Int32(packet.decoded.portnum.rawValue)
if packet.decoded.replyID > 0 {
newMessage.replyID = Int64(packet.decoded.replyID)
@ -734,7 +777,6 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
messageSaved = true
if messageSaved {
if newMessage.fromUser != nil && newMessage.toUser != nil && !(newMessage.fromUser?.mute ?? false) {
// Create an iOS Notification for the received DM message and schedule it immediately
let manager = LocalNotificationManager()
@ -743,7 +785,9 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
id: ("notification.id.\(newMessage.messageId)"),
title: "\(newMessage.fromUser?.longName ?? "unknown".localized)",
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")",
content: messageText)
content: messageText,
target: "message"
)
]
manager.schedule()
print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)")
@ -770,7 +814,8 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
id: ("notification.id.\(newMessage.messageId)"),
title: "\(newMessage.fromUser?.longName ?? "unknown".localized)",
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")",
content: messageText)
content: messageText,
target: "message")
]
manager.schedule()
print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)")
@ -825,7 +870,21 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
waypoint.created = Date()
do {
try context.save()
print("💾 Updated Node Waypoint App Packet For: \(waypoint.id)")
print("💾 Added Node Waypoint App Packet For: \(waypoint.id)")
let manager = LocalNotificationManager()
let icon = String(UnicodeScalar(Int(waypoint.icon)) ?? "📍")
let latitude = Double(waypoint.latitudeI) / 1e7
let longitude = Double(waypoint.longitudeI) / 1e7
manager.notifications = [
Notification(
id: ("notification.id.\(waypoint.id)"),
title: "New Waypoint Received",
subtitle: "\(icon) \(waypoint.name ?? "Dropped Pin")",
content: "\(waypoint.longDescription ?? "\(latitude), \(longitude)")",
target: "map"
)
]
manager.schedule()
} catch {
context.rollback()
let nsError = error as NSError

View file

@ -16,37 +16,14 @@ protocol MqttClientProxyManagerDelegate: AnyObject {
}
class MqttClientProxyManager {
enum ConnectionStatus {
case connecting
case connected
case disconnecting
case disconnected
case error
case none
}
enum MqttQos: Int {
case atMostOnce = 0
case atLeastOnce = 1
case exactlyOnce = 2
}
// Singleton Instance
static let shared = MqttClientProxyManager()
private static let defaultKeepAliveInterval: Int32 = 60
weak var delegate: MqttClientProxyManagerDelegate?
var status = ConnectionStatus.none
var mqttClientProxy: CocoaMQTT?
var topic = "msh/2/c"
private init() {
}
func connectFromConfigSettings(node: NodeInfoEntity) {
let defaultServerAddress = "mqtt.meshtastic.org"
@ -81,8 +58,6 @@ class MqttClientProxyManager {
return
}
status = .connecting
let clientId = "MeshtasticAppleMqttProxy-" + String(ProcessInfo().processIdentifier)
mqttClientProxy = CocoaMQTT(clientID: clientId, host: host, port: UInt16(port))
@ -103,17 +78,14 @@ class MqttClientProxyManager {
let success = mqttClient.connect()
if !success {
delegate?.onMqttError(message: "Mqtt connect error")
status = .error
}
} else {
delegate?.onMqttError(message: "Mqtt initialization error")
status = .error
}
}
func subscribe(topic: String, qos: MqttQos) {
func subscribe(topic: String, qos: CocoaMQTTQoS) {
print("📲 MQTT Client Proxy subscribed to: " + topic)
let qos = CocoaMQTTQoS(rawValue :UInt8(qos.rawValue))!
mqttClientProxy?.subscribe(topic, qos: qos)
}
@ -122,21 +94,16 @@ class MqttClientProxyManager {
print("📲 MQTT Client Proxy unsubscribe for: " + topic)
}
func publish(message: String, topic: String, qos: MqttQos) {
let qos = CocoaMQTTQoS(rawValue :UInt8(qos.rawValue))!
func publish(message: String, topic: String, qos: CocoaMQTTQoS) {
mqttClientProxy?.publish(topic, withString: message, qos: qos)
print("📲 MQTT Client Proxy publish for: " + topic)
}
func disconnect() {
//MqttSettings.shared.isConnected = false
if let client = mqttClientProxy {
status = .disconnecting
client.disconnect()
print("📲 MQTT Client Proxy Disconnected")
} else {
status = .disconnected
}
}
}
@ -169,22 +136,16 @@ extension MqttClientProxyManager: CocoaMQTTDelegate {
}
print(errorDescription)
delegate?.onMqttError(message: errorDescription)
//self.disconnect() // Stop reconnecting
//mqttSettings.isConnected = false // Disable automatic connect on start
self.disconnect()
}
self.status = ack == .accept ? ConnectionStatus.connected : ConnectionStatus.error // Set AFTER sending onMqttError (so the delegate can detect that was an error while establishing connection)
}
func mqttDidDisconnect(_ mqtt: CocoaMQTT, withError err: Error?) {
print("mqttDidDisconnect: \(err?.localizedDescription ?? "")")
if let error = err, status == .connecting {
if let error = err {
delegate?.onMqttError(message: error.localizedDescription)
}
status = err == nil ? .disconnected : .error
delegate?.onMqttDisconnected()
}

View file

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

View file

@ -0,0 +1,346 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22F82" 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="messages" optional="YES" attributeType="String" minValueString="0" maxValueString="198"/>
<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="DetectionSensorConfigEntity" representedClassName="DetectionSensorConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="detectionTriggeredHigh" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="minimumBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="monitorPin" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="stateBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="usePullup" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<relationship name="detectionSensorConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="detectionSensorConfig" inverseEntity="NodeInfoEntity"/>
</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="doubleTapAsButtonPress" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="isManaged" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="nodeInfoBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rebroadcastMode" optional="YES" attributeType="Integer 32" defaultValueString="0" 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="DeviceMetadataEntity" representedClassName="DeviceMetadataEntity" syncable="YES" codeGenerationType="class">
<attribute name="canShutdown" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="deviceStateVersion" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="firmwareVersion" optional="YES" attributeType="String"/>
<attribute name="hasBluetooth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hasEthernet" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hasWifi" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="hwModel" optional="YES" attributeType="String"/>
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="metadataNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="metadata" 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="displayMode" optional="YES" attributeType="Integer 32" defaultValueString="0" 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="headingBold" optional="YES" attributeType="Boolean" defaultValueString="YES" 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"/>
<attribute name="wakeOnTapOrMotion" optional="YES" attributeType="Boolean" defaultValueString="NO" 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="LocationEntity" representedClassName="LocationEntity" 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="id" attributeType="Integer 32" usesScalarValueType="YES"/>
<attribute name="latitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="routeLocation" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RouteEntity" inverseName="locations" inverseEntity="RouteEntity"/>
</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="overrideDutyCycle" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="overrideFrequency" optional="YES" attributeType="Float" defaultValueString="0.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="sx126xRxBoostedGain" optional="YES" attributeType="Boolean" 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="portNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="realACK" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="receivedACK" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="receivedTimestamp" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="replyID" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rssi" optional="YES" attributeType="Integer 32" 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="proxyToClientEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="root" optional="YES" attributeType="String" defaultValueString="msh"/>
<attribute name="tlsEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<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="adminIndex" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="bleName" optional="YES" attributeType="String"/>
<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" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="peripheralId" optional="YES" attributeType="String"/>
<attribute name="rssi" optional="YES" attributeType="Integer 32" 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="detectionSensorConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DetectionSensorConfigEntity" inverseName="detectionSensorConfigNode" inverseEntity="DetectionSensorConfigEntity"/>
<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="metadata" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceMetadataEntity" inverseName="metadataNode" inverseEntity="DeviceMetadataEntity"/>
<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="rtttlConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RTTTLConfigEntity" inverseName="rtttlConfigNode" inverseEntity="RTTTLConfigEntity"/>
<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="broadcastSmartMinimumDistance" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="broadcastSmartMinimumIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<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="latest" optional="YES" attributeType="Boolean" defaultValueString="NO" 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="rssi" 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="RouteEntity" representedClassName="RouteEntity" syncable="YES" codeGenerationType="class">
<attribute name="color" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="notes" optional="YES" attributeType="String"/>
<relationship name="locations" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LocationEntity" inverseName="routeLocation" inverseEntity="LocationEntity"/>
</entity>
<entity name="RTTTLConfigEntity" representedClassName="RTTTLConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="ringtone" optional="YES" attributeType="String" maxValueString="228" defaultValueString=""/>
<relationship name="rtttlConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rtttlConfig" 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="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="snr" 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="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="(fromUser.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="created" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="expire" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="icon" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="locked" attributeType="Integer 64" 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" attributeType="String" minValueString="1" maxValueString="30"/>
</entity>
</model>

View file

@ -4,8 +4,8 @@ import SwiftUI
import CoreData
@main
struct MeshtasticAppleApp: App {
struct MeshtasticAppleApp : App {
@UIApplicationDelegateAdaptor(MeshtasticAppDelegate.self) var appDelegate
let persistenceController = PersistenceController.shared
@ObservedObject private var bleManager: BLEManager = BLEManager()
@Environment(\.scenePhase) var scenePhase
@ -13,10 +13,11 @@ struct MeshtasticAppleApp: App {
@State var saveChannels = false
@State var incomingUrl: URL?
@State var channelSettings: String?
@StateObject var appState = AppState.shared
var body: some Scene {
WindowGroup {
ContentView()
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(bleManager)
.sheet(isPresented: $saveChannels) {
@ -45,7 +46,6 @@ struct MeshtasticAppleApp: App {
print("Some sort of URL was received \(url)")
self.incomingUrl = url
if url.absoluteString.lowercased().contains("meshtastic.org/e/#") {
if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") {
self.channelSettings = components.last!
@ -115,5 +115,11 @@ struct MeshtasticAppleApp: App {
print("💥 Apple must have changed something")
}
}
}
}
}
class AppState: ObservableObject {
static let shared = AppState()
@Published var tabSelection: Tab = .ble
}

View file

@ -0,0 +1,38 @@
//
// MeshtasticAppDelegate.swift
// Meshtastic
//
// Created by Ben on 8/20/23.
//
import SwiftUI
class MeshtasticAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("App launched!")
UNUserNotificationCenter.current().delegate = self
return true
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
}
// This method is called when user clicked on the notification
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void)
{
let userInfo = response.notification.request.content.userInfo
let targetValue = userInfo["target"] as? String
if targetValue == "map" {
AppState.shared.tabSelection = Tab.map
}
else if targetValue == "message" {
AppState.shared.tabSelection = Tab.messages
}
else if targetValue == "node" {
AppState.shared.tabSelection = Tab.nodes
}
completionHandler()
}
}

View file

@ -0,0 +1,21 @@
//
// MessageEntityExtension.swift
// Meshtastic
//
// Created by Ben on 8/22/23.
//
import Foundation
import CoreData
import CoreLocation
import MapKit
import SwiftUI
extension MessageEntity {
var timestamp: Date {
let time = messageTimestamp <= 0 ? receivedTimestamp : messageTimestamp
return Date(timeIntervalSince1970: TimeInterval(time))
}
}

View file

@ -60,3 +60,23 @@ public func getWaypoint(id: Int64, context: NSManagedObjectContext) -> WaypointE
}
return WaypointEntity(context: context)
}
public func getDetectionSensorMessages(nodeNum: Int64?, context: NSManagedObjectContext) -> [MessageEntity] {
let fetchDetectionMessagesPredicate: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MessageEntity")
fetchDetectionMessagesPredicate.predicate = NSPredicate(format: "portNum == %d", Int32(PortNum.detectionSensorApp.rawValue))
do {
let fetched = try context.fetch(fetchDetectionMessagesPredicate) as? [MessageEntity] ?? []
if nodeNum == nil {
return fetched.reversed()
}
return fetched.filter { message in
return message.fromUser?.num == nodeNum!
}.reversed()
}
catch {
return []
}
}

View file

@ -972,3 +972,64 @@ func upsertTelemetryModuleConfigPacket(config: Meshtastic.ModuleConfig.Telemetry
print("💥 Fetching node for core data TelemetryConfigEntity failed: \(nsError)")
}
}
func upsertDetectionSensorModuleConfigPacket(config: Meshtastic.ModuleConfig.DetectionSensorConfig, nodeNum: Int64, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.detectionsensor.config %@".localized, String(nodeNum))
MeshLogger.log("📈 \(logString)")
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
do {
guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else {
return
}
// Found a node, save Detection Sensor Config
if !fetchedNode.isEmpty {
if fetchedNode[0].detectionSensorConfig == nil {
let newConfig = DetectionSensorConfigEntity(context: context)
newConfig.enabled = config.enabled
newConfig.sendBell = config.sendBell
newConfig.name = config.name
newConfig.monitorPin = Int32(config.monitorPin)
newConfig.detectionTriggeredHigh = config.detectionTriggeredHigh
newConfig.usePullup = config.usePullup
newConfig.minimumBroadcastSecs = Int32(config.minimumBroadcastSecs)
newConfig.stateBroadcastSecs = Int32(config.stateBroadcastSecs)
fetchedNode[0].detectionSensorConfig = newConfig
} else {
fetchedNode[0].detectionSensorConfig?.enabled = config.enabled
fetchedNode[0].detectionSensorConfig?.sendBell = config.sendBell
fetchedNode[0].detectionSensorConfig?.name = config.name
fetchedNode[0].detectionSensorConfig?.monitorPin = Int32(config.monitorPin)
fetchedNode[0].detectionSensorConfig?.usePullup = config.usePullup
fetchedNode[0].detectionSensorConfig?.detectionTriggeredHigh = config.detectionTriggeredHigh
fetchedNode[0].detectionSensorConfig?.minimumBroadcastSecs = Int32(config.minimumBroadcastSecs)
fetchedNode[0].detectionSensorConfig?.stateBroadcastSecs = Int32(config.stateBroadcastSecs)
}
do {
try context.save()
print("💾 Updated Detection Sensor Module Config for node number: \(String(nodeNum))")
} catch {
context.rollback()
let nsError = error as NSError
print("💥 Error Updating Core Data DetectionSensorConfigEntity: \(nsError)")
}
} else {
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Detection Sensor Module Config")
}
} catch {
let nsError = error as NSError
print("💥 Fetching node for core data DetectionSensorConfigEntity failed: \(nsError)")
}
}

View file

@ -734,6 +734,18 @@ struct AdminMessage {
///
/// TODO: REPLACE
case remotehardwareConfig // = 8
///
/// TODO: REPLACE
case neighborinfoConfig // = 9
///
/// TODO: REPLACE
case ambientlightingConfig // = 10
///
/// TODO: REPLACE
case detectionsensorConfig // = 11
case UNRECOGNIZED(Int)
init() {
@ -751,6 +763,9 @@ struct AdminMessage {
case 6: self = .cannedmsgConfig
case 7: self = .audioConfig
case 8: self = .remotehardwareConfig
case 9: self = .neighborinfoConfig
case 10: self = .ambientlightingConfig
case 11: self = .detectionsensorConfig
default: self = .UNRECOGNIZED(rawValue)
}
}
@ -766,6 +781,9 @@ struct AdminMessage {
case .cannedmsgConfig: return 6
case .audioConfig: return 7
case .remotehardwareConfig: return 8
case .neighborinfoConfig: return 9
case .ambientlightingConfig: return 10
case .detectionsensorConfig: return 11
case .UNRECOGNIZED(let i): return i
}
}
@ -802,6 +820,9 @@ extension AdminMessage.ModuleConfigType: CaseIterable {
.cannedmsgConfig,
.audioConfig,
.remotehardwareConfig,
.neighborinfoConfig,
.ambientlightingConfig,
.detectionsensorConfig,
]
}
@ -1412,6 +1433,9 @@ extension AdminMessage.ModuleConfigType: SwiftProtobuf._ProtoNameProviding {
6: .same(proto: "CANNEDMSG_CONFIG"),
7: .same(proto: "AUDIO_CONFIG"),
8: .same(proto: "REMOTEHARDWARE_CONFIG"),
9: .same(proto: "NEIGHBORINFO_CONFIG"),
10: .same(proto: "AMBIENTLIGHTING_CONFIG"),
11: .same(proto: "DETECTIONSENSOR_CONFIG"),
]
}

View file

@ -368,7 +368,7 @@ struct Config {
var broadcastSmartMinimumDistance: UInt32 = 0
///
/// The minumum number of seconds (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled
/// The minimum number of seconds (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled
var broadcastSmartMinimumIntervalSecs: UInt32 = 0
var unknownFields = SwiftProtobuf.UnknownStorage()
@ -506,13 +506,6 @@ struct Config {
/// 0 for default of 1 minute
var waitBluetoothSecs: UInt32 = 0
///
/// Mesh Super Deep Sleep Timeout Seconds
/// While in Light Sleep if this value is exceeded we will lower into super deep sleep
/// for sds_secs (default 1 year) or a button press
/// 0 for default of two hours, MAXUINT for disabled
var meshSdsTimeoutSecs: UInt32 = 0
///
/// Super Deep Sleep Seconds
/// While in Light Sleep if mesh_sds_timeout_secs is exceeded we will lower into super deep sleep
@ -1805,7 +1798,6 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple
2: .standard(proto: "on_battery_shutdown_after_secs"),
3: .standard(proto: "adc_multiplier_override"),
4: .standard(proto: "wait_bluetooth_secs"),
5: .standard(proto: "mesh_sds_timeout_secs"),
6: .standard(proto: "sds_secs"),
7: .standard(proto: "ls_secs"),
8: .standard(proto: "min_wake_secs"),
@ -1822,7 +1814,6 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple
case 2: try { try decoder.decodeSingularUInt32Field(value: &self.onBatteryShutdownAfterSecs) }()
case 3: try { try decoder.decodeSingularFloatField(value: &self.adcMultiplierOverride) }()
case 4: try { try decoder.decodeSingularUInt32Field(value: &self.waitBluetoothSecs) }()
case 5: try { try decoder.decodeSingularUInt32Field(value: &self.meshSdsTimeoutSecs) }()
case 6: try { try decoder.decodeSingularUInt32Field(value: &self.sdsSecs) }()
case 7: try { try decoder.decodeSingularUInt32Field(value: &self.lsSecs) }()
case 8: try { try decoder.decodeSingularUInt32Field(value: &self.minWakeSecs) }()
@ -1845,9 +1836,6 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple
if self.waitBluetoothSecs != 0 {
try visitor.visitSingularUInt32Field(value: self.waitBluetoothSecs, fieldNumber: 4)
}
if self.meshSdsTimeoutSecs != 0 {
try visitor.visitSingularUInt32Field(value: self.meshSdsTimeoutSecs, fieldNumber: 5)
}
if self.sdsSecs != 0 {
try visitor.visitSingularUInt32Field(value: self.sdsSecs, fieldNumber: 6)
}
@ -1868,7 +1856,6 @@ extension Config.PowerConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImple
if lhs.onBatteryShutdownAfterSecs != rhs.onBatteryShutdownAfterSecs {return false}
if lhs.adcMultiplierOverride != rhs.adcMultiplierOverride {return false}
if lhs.waitBluetoothSecs != rhs.waitBluetoothSecs {return false}
if lhs.meshSdsTimeoutSecs != rhs.meshSdsTimeoutSecs {return false}
if lhs.sdsSecs != rhs.sdsSecs {return false}
if lhs.lsSecs != rhs.lsSecs {return false}
if lhs.minWakeSecs != rhs.minWakeSecs {return false}

View file

@ -108,14 +108,6 @@ struct DeviceState {
/// Clears the value of `owner`. Subsequent reads from it will return its default value.
mutating func clearOwner() {_uniqueStorage()._owner = nil}
///
/// Deprecated in 2.1.x
/// Old node_db. See NodeInfoLite node_db_lite
var nodeDb: [NodeInfo] {
get {return _storage._nodeDb}
set {_uniqueStorage()._nodeDb = newValue}
}
///
/// Received packets saved for delivery to the phone
var receiveQueue: [MeshPacket] {
@ -446,7 +438,6 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
2: .standard(proto: "my_node"),
3: .same(proto: "owner"),
4: .standard(proto: "node_db"),
5: .standard(proto: "receive_queue"),
8: .same(proto: "version"),
7: .standard(proto: "rx_text_message"),
@ -460,7 +451,6 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
fileprivate class _StorageClass {
var _myNode: MyNodeInfo? = nil
var _owner: User? = nil
var _nodeDb: [NodeInfo] = []
var _receiveQueue: [MeshPacket] = []
var _version: UInt32 = 0
var _rxTextMessage: MeshPacket? = nil
@ -477,7 +467,6 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
init(copying source: _StorageClass) {
_myNode = source._myNode
_owner = source._owner
_nodeDb = source._nodeDb
_receiveQueue = source._receiveQueue
_version = source._version
_rxTextMessage = source._rxTextMessage
@ -506,7 +495,6 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
switch fieldNumber {
case 2: try { try decoder.decodeSingularMessageField(value: &_storage._myNode) }()
case 3: try { try decoder.decodeSingularMessageField(value: &_storage._owner) }()
case 4: try { try decoder.decodeRepeatedMessageField(value: &_storage._nodeDb) }()
case 5: try { try decoder.decodeRepeatedMessageField(value: &_storage._receiveQueue) }()
case 7: try { try decoder.decodeSingularMessageField(value: &_storage._rxTextMessage) }()
case 8: try { try decoder.decodeSingularUInt32Field(value: &_storage._version) }()
@ -533,9 +521,6 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
try { if let v = _storage._owner {
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
} }()
if !_storage._nodeDb.isEmpty {
try visitor.visitRepeatedMessageField(value: _storage._nodeDb, fieldNumber: 4)
}
if !_storage._receiveQueue.isEmpty {
try visitor.visitRepeatedMessageField(value: _storage._receiveQueue, fieldNumber: 5)
}
@ -571,7 +556,6 @@ extension DeviceState: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementati
let rhs_storage = _args.1
if _storage._myNode != rhs_storage._myNode {return false}
if _storage._owner != rhs_storage._owner {return false}
if _storage._nodeDb != rhs_storage._nodeDb {return false}
if _storage._receiveQueue != rhs_storage._receiveQueue {return false}
if _storage._version != rhs_storage._version {return false}
if _storage._rxTextMessage != rhs_storage._rxTextMessage {return false}

View file

@ -233,6 +233,28 @@ struct LocalModuleConfig {
/// Clears the value of `neighborInfo`. Subsequent reads from it will return its default value.
mutating func clearNeighborInfo() {_uniqueStorage()._neighborInfo = nil}
///
/// The part of the config that is specific to the Ambient Lighting module
var ambientLighting: ModuleConfig.AmbientLightingConfig {
get {return _storage._ambientLighting ?? ModuleConfig.AmbientLightingConfig()}
set {_uniqueStorage()._ambientLighting = newValue}
}
/// Returns true if `ambientLighting` has been explicitly set.
var hasAmbientLighting: Bool {return _storage._ambientLighting != nil}
/// Clears the value of `ambientLighting`. Subsequent reads from it will return its default value.
mutating func clearAmbientLighting() {_uniqueStorage()._ambientLighting = nil}
///
/// The part of the config that is specific to the Detection Sensor module
var detectionSensor: ModuleConfig.DetectionSensorConfig {
get {return _storage._detectionSensor ?? ModuleConfig.DetectionSensorConfig()}
set {_uniqueStorage()._detectionSensor = newValue}
}
/// Returns true if `detectionSensor` has been explicitly set.
var hasDetectionSensor: Bool {return _storage._detectionSensor != nil}
/// Clears the value of `detectionSensor`. Subsequent reads from it will return its default value.
mutating func clearDetectionSensor() {_uniqueStorage()._detectionSensor = nil}
///
/// A version integer used to invalidate old save files when we make
/// incompatible changes This integer is set at build time and is private to
@ -395,6 +417,8 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
9: .same(proto: "audio"),
10: .standard(proto: "remote_hardware"),
11: .standard(proto: "neighbor_info"),
12: .standard(proto: "ambient_lighting"),
13: .standard(proto: "detection_sensor"),
8: .same(proto: "version"),
]
@ -409,6 +433,8 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
var _audio: ModuleConfig.AudioConfig? = nil
var _remoteHardware: ModuleConfig.RemoteHardwareConfig? = nil
var _neighborInfo: ModuleConfig.NeighborInfoConfig? = nil
var _ambientLighting: ModuleConfig.AmbientLightingConfig? = nil
var _detectionSensor: ModuleConfig.DetectionSensorConfig? = nil
var _version: UInt32 = 0
static let defaultInstance = _StorageClass()
@ -426,6 +452,8 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
_audio = source._audio
_remoteHardware = source._remoteHardware
_neighborInfo = source._neighborInfo
_ambientLighting = source._ambientLighting
_detectionSensor = source._detectionSensor
_version = source._version
}
}
@ -456,6 +484,8 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
case 9: try { try decoder.decodeSingularMessageField(value: &_storage._audio) }()
case 10: try { try decoder.decodeSingularMessageField(value: &_storage._remoteHardware) }()
case 11: try { try decoder.decodeSingularMessageField(value: &_storage._neighborInfo) }()
case 12: try { try decoder.decodeSingularMessageField(value: &_storage._ambientLighting) }()
case 13: try { try decoder.decodeSingularMessageField(value: &_storage._detectionSensor) }()
default: break
}
}
@ -501,6 +531,12 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
try { if let v = _storage._neighborInfo {
try visitor.visitSingularMessageField(value: v, fieldNumber: 11)
} }()
try { if let v = _storage._ambientLighting {
try visitor.visitSingularMessageField(value: v, fieldNumber: 12)
} }()
try { if let v = _storage._detectionSensor {
try visitor.visitSingularMessageField(value: v, fieldNumber: 13)
} }()
}
try unknownFields.traverse(visitor: &visitor)
}
@ -520,6 +556,8 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
if _storage._audio != rhs_storage._audio {return false}
if _storage._remoteHardware != rhs_storage._remoteHardware {return false}
if _storage._neighborInfo != rhs_storage._neighborInfo {return false}
if _storage._ambientLighting != rhs_storage._ambientLighting {return false}
if _storage._detectionSensor != rhs_storage._detectionSensor {return false}
if _storage._version != rhs_storage._version {return false}
return true
}

File diff suppressed because it is too large Load diff

View file

@ -194,6 +194,16 @@ struct ModuleConfig {
set {payloadVariant = .ambientLighting(newValue)}
}
///
/// TODO: REPLACE
var detectionSensor: ModuleConfig.DetectionSensorConfig {
get {
if case .detectionSensor(let v)? = payloadVariant {return v}
return ModuleConfig.DetectionSensorConfig()
}
set {payloadVariant = .detectionSensor(newValue)}
}
var unknownFields = SwiftProtobuf.UnknownStorage()
///
@ -232,6 +242,9 @@ struct ModuleConfig {
///
/// TODO: REPLACE
case ambientLighting(ModuleConfig.AmbientLightingConfig)
///
/// TODO: REPLACE
case detectionSensor(ModuleConfig.DetectionSensorConfig)
#if !swift(>=4.1)
static func ==(lhs: ModuleConfig.OneOf_PayloadVariant, rhs: ModuleConfig.OneOf_PayloadVariant) -> Bool {
@ -283,6 +296,10 @@ struct ModuleConfig {
guard case .ambientLighting(let l) = lhs, case .ambientLighting(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.detectionSensor, .detectionSensor): return {
guard case .detectionSensor(let l) = lhs, case .detectionSensor(let r) = rhs else { preconditionFailure() }
return l == r
}()
default: return false
}
}
@ -392,6 +409,57 @@ struct ModuleConfig {
init() {}
}
///
/// Detection Sensor Module Config
struct DetectionSensorConfig {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
///
/// Whether the Module is enabled
var enabled: Bool = false
///
/// Interval in seconds of how often we can send a message to the mesh when a state change is detected
var minimumBroadcastSecs: UInt32 = 0
///
/// Interval in seconds of how often we should send a message to the mesh with the current state regardless of changes
/// When set to 0, only state changes will be broadcasted
/// Works as a sort of status heartbeat for peace of mind
var stateBroadcastSecs: UInt32 = 0
///
/// Send ASCII bell with alert message
/// Useful for triggering ext. notification on bell
var sendBell: Bool = false
///
/// Friendly name used to format message sent to mesh
/// Example: A name "Motion" would result in a message "Motion detected"
/// Maximum length of 20 characters
var name: String = String()
///
/// GPIO pin to monitor for state changes
var monitorPin: UInt32 = 0
///
/// Whether or not the GPIO pin state detection is triggered on HIGH (1)
/// Otherwise LOW (0)
var detectionTriggeredHigh: Bool = false
///
/// Whether or not use INPUT_PULLUP mode for GPIO pin
/// Only applicable if the board uses pull-up resistors on the pin
var usePullup: Bool = false
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
///
/// Audio Config for codec2 voice
struct AudioConfig {
@ -1080,6 +1148,7 @@ extension ModuleConfig.OneOf_PayloadVariant: @unchecked Sendable {}
extension ModuleConfig.MQTTConfig: @unchecked Sendable {}
extension ModuleConfig.RemoteHardwareConfig: @unchecked Sendable {}
extension ModuleConfig.NeighborInfoConfig: @unchecked Sendable {}
extension ModuleConfig.DetectionSensorConfig: @unchecked Sendable {}
extension ModuleConfig.AudioConfig: @unchecked Sendable {}
extension ModuleConfig.AudioConfig.Audio_Baud: @unchecked Sendable {}
extension ModuleConfig.SerialConfig: @unchecked Sendable {}
@ -1121,6 +1190,7 @@ extension ModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
9: .standard(proto: "remote_hardware"),
10: .standard(proto: "neighbor_info"),
11: .standard(proto: "ambient_lighting"),
12: .standard(proto: "detection_sensor"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -1272,6 +1342,19 @@ extension ModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
self.payloadVariant = .ambientLighting(v)
}
}()
case 12: try {
var v: ModuleConfig.DetectionSensorConfig?
var hadOneofValue = false
if let current = self.payloadVariant {
hadOneofValue = true
if case .detectionSensor(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
self.payloadVariant = .detectionSensor(v)
}
}()
default: break
}
}
@ -1327,6 +1410,10 @@ extension ModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
guard case .ambientLighting(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 11)
}()
case .detectionSensor?: try {
guard case .detectionSensor(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 12)
}()
case nil: break
}
try unknownFields.traverse(visitor: &visitor)
@ -1501,6 +1588,80 @@ extension ModuleConfig.NeighborInfoConfig: SwiftProtobuf.Message, SwiftProtobuf.
}
}
extension ModuleConfig.DetectionSensorConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = ModuleConfig.protoMessageName + ".DetectionSensorConfig"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "enabled"),
2: .standard(proto: "minimum_broadcast_secs"),
3: .standard(proto: "state_broadcast_secs"),
4: .standard(proto: "send_bell"),
5: .same(proto: "name"),
6: .standard(proto: "monitor_pin"),
7: .standard(proto: "detection_triggered_high"),
8: .standard(proto: "use_pullup"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularBoolField(value: &self.enabled) }()
case 2: try { try decoder.decodeSingularUInt32Field(value: &self.minimumBroadcastSecs) }()
case 3: try { try decoder.decodeSingularUInt32Field(value: &self.stateBroadcastSecs) }()
case 4: try { try decoder.decodeSingularBoolField(value: &self.sendBell) }()
case 5: try { try decoder.decodeSingularStringField(value: &self.name) }()
case 6: try { try decoder.decodeSingularUInt32Field(value: &self.monitorPin) }()
case 7: try { try decoder.decodeSingularBoolField(value: &self.detectionTriggeredHigh) }()
case 8: try { try decoder.decodeSingularBoolField(value: &self.usePullup) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if self.enabled != false {
try visitor.visitSingularBoolField(value: self.enabled, fieldNumber: 1)
}
if self.minimumBroadcastSecs != 0 {
try visitor.visitSingularUInt32Field(value: self.minimumBroadcastSecs, fieldNumber: 2)
}
if self.stateBroadcastSecs != 0 {
try visitor.visitSingularUInt32Field(value: self.stateBroadcastSecs, fieldNumber: 3)
}
if self.sendBell != false {
try visitor.visitSingularBoolField(value: self.sendBell, fieldNumber: 4)
}
if !self.name.isEmpty {
try visitor.visitSingularStringField(value: self.name, fieldNumber: 5)
}
if self.monitorPin != 0 {
try visitor.visitSingularUInt32Field(value: self.monitorPin, fieldNumber: 6)
}
if self.detectionTriggeredHigh != false {
try visitor.visitSingularBoolField(value: self.detectionTriggeredHigh, fieldNumber: 7)
}
if self.usePullup != false {
try visitor.visitSingularBoolField(value: self.usePullup, fieldNumber: 8)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: ModuleConfig.DetectionSensorConfig, rhs: ModuleConfig.DetectionSensorConfig) -> Bool {
if lhs.enabled != rhs.enabled {return false}
if lhs.minimumBroadcastSecs != rhs.minimumBroadcastSecs {return false}
if lhs.stateBroadcastSecs != rhs.stateBroadcastSecs {return false}
if lhs.sendBell != rhs.sendBell {return false}
if lhs.name != rhs.name {return false}
if lhs.monitorPin != rhs.monitorPin {return false}
if lhs.detectionTriggeredHigh != rhs.detectionTriggeredHigh {return false}
if lhs.usePullup != rhs.usePullup {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension ModuleConfig.AudioConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = ModuleConfig.protoMessageName + ".AudioConfig"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [

View file

@ -40,60 +40,81 @@ enum PortNum: SwiftProtobuf.Enum {
/// Deprecated: do not use in new code (formerly called OPAQUE)
/// A message sent from a device outside of the mesh, in a form the mesh does not understand
/// NOTE: This must be 0, because it is documented in IMeshService.aidl to be so
/// ENCODING: binary undefined
case unknownApp // = 0
///
/// A simple UTF-8 text message, which even the little micros in the mesh
/// can understand and show on their screen eventually in some circumstances
/// even signal might send messages in this form (see below)
/// ENCODING: UTF-8 Plaintext (?)
case textMessageApp // = 1
///
/// Reserved for built-in GPIO/example app.
/// See remote_hardware.proto/HardwareMessage for details on the message sent/received to this port number
/// ENCODING: Protobuf
case remoteHardwareApp // = 2
///
/// The built-in position messaging app.
/// Payload is a [Position](/docs/developers/protobufs/api#position) message
/// ENCODING: Protobuf
case positionApp // = 3
///
/// The built-in user info app.
/// Payload is a [User](/docs/developers/protobufs/api#user) message
/// ENCODING: Protobuf
case nodeinfoApp // = 4
///
/// Protocol control packets for mesh protocol use.
/// Payload is a [Routing](/docs/developers/protobufs/api#routing) message
/// ENCODING: Protobuf
case routingApp // = 5
///
/// Admin control packets.
/// Payload is a [AdminMessage](/docs/developers/protobufs/api#adminmessage) message
/// ENCODING: Protobuf
case adminApp // = 6
///
/// Compressed TEXT_MESSAGE payloads.
/// ENCODING: UTF-8 Plaintext (?) with Unishox2 Compression
/// NOTE: The Device Firmware converts a TEXT_MESSAGE_APP to TEXT_MESSAGE_COMPRESSED_APP if the compressed
/// payload is shorter. There's no need for app developers to do this themselves. Also the firmware will decompress
/// any incoming TEXT_MESSAGE_COMPRESSED_APP payload and convert to TEXT_MESSAGE_APP.
case textMessageCompressedApp // = 7
///
/// Waypoint payloads.
/// Payload is a [Waypoint](/docs/developers/protobufs/api#waypoint) message
/// ENCODING: Protobuf
case waypointApp // = 8
///
/// Audio Payloads.
/// Encapsulated codec2 packets. On 2.4 GHZ Bandwidths only for now
/// ENCODING: codec2 audio frames
/// NOTE: audio frames contain a 3 byte header (0xc0 0xde 0xc2) and a one byte marker for the decompressed bitrate.
/// This marker comes from the 'moduleConfig.audio.bitrate' enum minus one.
case audioApp // = 9
///
/// Same as Text Message but originating from Detection Sensor Module.
case detectionSensorApp // = 10
///
/// Provides a 'ping' service that replies to any packet it receives.
/// Also serves as a small example module.
/// ENCODING: ASCII Plaintext
case replyApp // = 32
///
/// Used for the python IP tunnel feature
/// ENCODING: IP Packet. Handled by the python API, firmware ignores this one and pases on.
case ipTunnelApp // = 33
///
@ -102,26 +123,31 @@ enum PortNum: SwiftProtobuf.Enum {
/// network is forwarded to the RX pin while sending a packet to TX will go out to the Mesh network.
/// Maximum packet size of 240 bytes.
/// Module is disabled by default can be turned on by setting SERIAL_MODULE_ENABLED = 1 in SerialPlugh.cpp.
/// ENCODING: binary undefined
case serialApp // = 64
///
/// STORE_FORWARD_APP (Work in Progress)
/// Maintained by Jm Casler (MC Hamster) : jm@casler.org
/// ENCODING: Protobuf
case storeForwardApp // = 65
///
/// Optional port for messages for the range test module.
/// ENCODING: ASCII Plaintext
case rangeTestApp // = 66
///
/// Provides a format to send and receive telemetry data from the Meshtastic network.
/// Maintained by Charles Crossan (crossan007) : crossan007@gmail.com
/// ENCODING: Protobuf
case telemetryApp // = 67
///
/// Experimental tools for estimating node position without a GPS
/// Maintained by Github user a-f-G-U-C (a Meshtastic contributor)
/// Project files at https://github.com/a-f-G-U-C/Meshtastic-ZPS
/// ENCODING: arrays of int64 fields
case zpsApp // = 68
///
@ -129,15 +155,18 @@ enum PortNum: SwiftProtobuf.Enum {
/// as if they did using their LoRa chip.
/// Maintained by GitHub user GUVWAF.
/// Project files at https://github.com/GUVWAF/Meshtasticator
/// ENCODING: Protobuf (?)
case simulatorApp // = 69
///
/// Provides a traceroute functionality to show the route a packet towards
/// a certain destination would take on the mesh.
/// ENCODING: Protobuf
case tracerouteApp // = 70
///
/// Aggregates edge info for the network by sending out a list of each node's neighbors
/// ENCODING: Protobuf
case neighborinfoApp // = 71
///
@ -148,6 +177,7 @@ enum PortNum: SwiftProtobuf.Enum {
///
/// ATAK Forwarder Module https://github.com/paulmandal/atak-forwarder
/// ENCODING: libcotshrink
case atakForwarder // = 257
///
@ -171,6 +201,7 @@ enum PortNum: SwiftProtobuf.Enum {
case 7: self = .textMessageCompressedApp
case 8: self = .waypointApp
case 9: self = .audioApp
case 10: self = .detectionSensorApp
case 32: self = .replyApp
case 33: self = .ipTunnelApp
case 64: self = .serialApp
@ -200,6 +231,7 @@ enum PortNum: SwiftProtobuf.Enum {
case .textMessageCompressedApp: return 7
case .waypointApp: return 8
case .audioApp: return 9
case .detectionSensorApp: return 10
case .replyApp: return 32
case .ipTunnelApp: return 33
case .serialApp: return 64
@ -234,6 +266,7 @@ extension PortNum: CaseIterable {
.textMessageCompressedApp,
.waypointApp,
.audioApp,
.detectionSensorApp,
.replyApp,
.ipTunnelApp,
.serialApp,
@ -270,6 +303,7 @@ extension PortNum: SwiftProtobuf._ProtoNameProviding {
7: .same(proto: "TEXT_MESSAGE_COMPRESSED_APP"),
8: .same(proto: "WAYPOINT_APP"),
9: .same(proto: "AUDIO_APP"),
10: .same(proto: "DETECTION_SENSOR_APP"),
32: .same(proto: "REPLY_APP"),
33: .same(proto: "IP_TUNNEL_APP"),
64: .same(proto: "SERIAL_APP"),

View file

@ -50,7 +50,7 @@ struct Connect: View {
if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == .connected {
HStack {
VStack(alignment: .center) {
CircleText(text: node?.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node?.num ?? 0))), circleSize: 80, fontSize: (node?.user?.shortName ?? "???").isEmoji() ? 52 : 30, textColor: UIColor(hex: UInt32(node?.num ?? 0)).isLight() ? .black : .white )
CircleText(text: node?.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node?.num ?? 0))), circleSize: 90, fontSize: (node?.user?.shortName ?? "???").isEmoji() ? 52 : (node?.user?.shortName?.count ?? 0 == 4 ? 26 : 36), textColor: UIColor(hex: UInt32(node?.num ?? 0)).isLight() ? .black : .white )
}
.padding(.trailing)
VStack(alignment: .leading) {
@ -89,22 +89,20 @@ struct Connect: View {
if node != nil {
#if !targetEnvironment(macCatalyst)
if #available(iOS 16.2, *) {
Button {
if !liveActivityStarted {
Button {
if !liveActivityStarted {
#if canImport(ActivityKit)
print("Start live activity.")
startNodeActivity()
#endif
} else {
#if canImport(ActivityKit)
print("Start live activity.")
startNodeActivity()
#endif
} else {
#if canImport(ActivityKit)
print("Stop live activity.")
endActivity()
#endif
}
} label: {
Label("mesh.live.activity", systemImage: liveActivityStarted ? "stop" : "play")
print("Stop live activity.")
endActivity()
#endif
}
} label: {
Label("mesh.live.activity", systemImage: liveActivityStarted ? "stop" : "play")
}
#endif
Text("Num: \(String(node!.num))")
@ -251,7 +249,7 @@ struct Connect: View {
.navigationTitle("bluetooth")
.navigationBarItems(leading: MeshtasticLogo(), trailing:
ZStack {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????", mqttProxyConnected: bleManager.mqttProxyConnected)
})
}
.sheet(isPresented: $invalidFirmwareVersion, onDismiss: didDismissSheet) {
@ -293,40 +291,36 @@ struct Connect: View {
}
#if canImport(ActivityKit)
func startNodeActivity() {
if #available(iOS 16.2, *) {
liveActivityStarted = true
let timerSeconds = 60
let deviceMetrics = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
liveActivityStarted = true
let timerSeconds = 60
let deviceMetrics = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
let activityAttributes = MeshActivityAttributes(nodeNum: Int(node?.num ?? 0), name: node?.user?.longName ?? "unknown")
let activityAttributes = MeshActivityAttributes(nodeNum: Int(node?.num ?? 0), name: node?.user?.longName ?? "unknown")
let future = Date(timeIntervalSinceNow: Double(timerSeconds))
let future = Date(timeIntervalSinceNow: Double(timerSeconds))
let initialContentState = MeshActivityAttributes.ContentState(timerRange: Date.now...future, connected: true, channelUtilization: mostRecent?.channelUtilization ?? 0.0, airtime: mostRecent?.airUtilTx ?? 0.0, batteryLevel: UInt32(mostRecent?.batteryLevel ?? 0))
let initialContentState = MeshActivityAttributes.ContentState(timerRange: Date.now...future, connected: true, channelUtilization: mostRecent?.channelUtilization ?? 0.0, airtime: mostRecent?.airUtilTx ?? 0.0, batteryLevel: UInt32(mostRecent?.batteryLevel ?? 0))
let activityContent = ActivityContent(state: initialContentState, staleDate: Calendar.current.date(byAdding: .minute, value: 2, to: Date())!)
let activityContent = ActivityContent(state: initialContentState, staleDate: Calendar.current.date(byAdding: .minute, value: 2, to: Date())!)
do {
let myActivity = try Activity<MeshActivityAttributes>.request(attributes: activityAttributes, content: activityContent,
pushType: nil)
print(" Requested MyActivity live activity. ID: \(myActivity.id)")
} catch let error {
print("Error requesting live activity: \(error.localizedDescription)")
}
do {
let myActivity = try Activity<MeshActivityAttributes>.request(attributes: activityAttributes, content: activityContent,
pushType: nil)
print(" Requested MyActivity live activity. ID: \(myActivity.id)")
} catch let error {
print("Error requesting live activity: \(error.localizedDescription)")
}
}
func endActivity() {
liveActivityStarted = false
Task {
if #available(iOS 16.2, *) {
for activity in Activity<MeshActivityAttributes>.activities {
// Check if this is the activity associated with this order.
if activity.attributes.nodeNum == node?.num ?? 0 {
await activity.end(nil, dismissalPolicy: .immediate)
}
for activity in Activity<MeshActivityAttributes>.activities {
// Check if this is the activity associated with this order.
if activity.attributes.nodeNum == node?.num ?? 0 {
await activity.end(nil, dismissalPolicy: .immediate)
}
}
}

View file

@ -5,22 +5,11 @@
import SwiftUI
struct ContentView: View {
@State private var selection: Tab = .ble
enum Tab {
case contacts
case messages
case map
case ble
case nodes
case settings
}
@StateObject var appState = AppState.shared
var body: some View {
TabView(selection: $selection) {
TabView(selection: $appState.tabSelection) {
Contacts()
.tabItem {
Label("messages", systemImage: "message")
@ -55,3 +44,12 @@ struct ContentView_Previews: PreviewProvider {
ContentView()
}
}
enum Tab {
case contacts
case messages
case map
case ble
case nodes
case settings
}

View file

@ -30,7 +30,7 @@ struct CircleText: View {
struct CircleText_Previews: PreviewProvider {
static var previews: some View {
CircleText(text: "RDDN", color: Color.accentColor)
CircleText(text: "MOMO", color: Color.accentColor)
.previewLayout(.fixed(width: 300, height: 100))
}
}

View file

@ -9,12 +9,21 @@ struct ConnectedDevice: View {
var bluetoothOn: Bool
var deviceConnected: Bool
var name: String
var mqttProxyConnected: Bool = false
var body: some View {
HStack {
if bluetoothOn {
if bluetoothOn {
if deviceConnected && mqttProxyConnected {
if mqttProxyConnected {
Image(systemName: "iphone.gen3.radiowaves.left.and.right.circle.fill")
.imageScale(.large)
.foregroundColor(.green)
.symbolRenderingMode(.hierarchical)
}
}
if deviceConnected {
Image(systemName: "antenna.radiowaves.left.and.right.circle.fill")
.imageScale(.large)
@ -27,7 +36,6 @@ struct ConnectedDevice: View {
.imageScale(.medium)
.foregroundColor(.red)
.symbolRenderingMode(.hierarchical)
}
} else {
Text("bluetooth.off").font(.subheadline).foregroundColor(.red)
@ -38,10 +46,10 @@ struct ConnectedDevice: View {
struct ConnectedDevice_Previews: PreviewProvider {
static var previews: some View {
ConnectedDevice(bluetoothOn: true, deviceConnected: false, name: "Yellow Beam")
ConnectedDevice(bluetoothOn: true, deviceConnected: true, name: "MEMO", mqttProxyConnected: true)
.previewLayout(.fixed(width: 80, height: 70))
ConnectedDevice(bluetoothOn: true, deviceConnected: false, name: "Yellow Beam")
ConnectedDevice(bluetoothOn: true, deviceConnected: false, name: "86D4", mqttProxyConnected: false)
.previewLayout(.fixed(width: 80, height: 70))
}

View file

@ -243,6 +243,19 @@ struct NodeInfoView: View {
}
Divider()
}
NavigationLink {
DetectionSensorLog(node: node)
} label: {
Image(systemName: "sensor")
.symbolRenderingMode(.hierarchical)
.font(.title)
Text("Detection Sensor Log")
.font(.title3)
}
.fixedSize(horizontal: false, vertical: true)
Divider()
}
}
}

View file

@ -63,12 +63,24 @@ struct ChannelMessageList: View {
VStack(alignment: currentUser ? .trailing : .leading) {
let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE"))
let linkBlue = Color(red: 0.4627, green: 0.8392, blue: 1) /* #76d6ff */
let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue)
Text(markdownText)
.tint(linkBlue)
.padding(10)
.foregroundColor(.white)
.background(currentUser ? .accentColor : Color(.gray))
.cornerRadius(15)
.overlay(
VStack {
isDetectionSensorMessage ? Image(systemName: "sensor.fill")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
.foregroundStyle(Color.orange)
.offset(x: 20, y: -20)
: nil
}
)
.contextMenu {
VStack {
Text("channel")+Text(": \(message.channel)")
@ -185,6 +197,9 @@ struct ChannelMessageList: View {
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.font(.caption2).foregroundColor(.red)
} else if isDetectionSensorMessage {
let messageDate = message.timestamp
Text(" \(messageDate.formattedDate(format: dateFormatString))").font(.caption2).foregroundColor(.gray)
}
}
}

View file

@ -0,0 +1,157 @@
//
// DetectionSensorLog.swift
// Meshtastic
//
// Created by Ben on 8/22/23.
//
import SwiftUI
import Charts
struct DetectionSensorLog: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@State private var isPresentingClearLogConfirm: Bool = false
@State var isExporting = false
@State var exportString = ""
var node: NodeInfoEntity
var body: some View {
let oneDayAgo = Calendar.current.date(byAdding: .day, value: -1, to: Date())
let detections = getDetectionSensorMessages(nodeNum: node.num, context: context)
let chartData = detections
.filter { $0.timestamp >= oneDayAgo! }
.sorted { $0.timestamp < $1.timestamp }
NavigationStack {
if chartData.count > 0 {
GroupBox(label: Label("\(detections.count) Total Detection Events", systemImage: "sensor")) {
Chart {
ForEach(chartData, id: \.self) { point in
Plot {
BarMark(
x: .value("x", point.timestamp),
y: .value("y", 1)
)
}
.accessibilityLabel("Bar Series")
.accessibilityValue("X: \(point.timestamp), Y: \(1)")
.interpolationMethod(.cardinal)
.foregroundStyle(
.linearGradient(
colors: [.green, .yellow, .orange, .red],
startPoint: .bottom,
endPoint: .top
)
)
.alignsMarkStylesWithPlotArea()
}
}
.chartXAxis(content: {
AxisMarks(position: .top)
// AxisMarks(position: .top, values: .stride(by: .hour)) { date in
// AxisValueLabel(format: .dateTime.hour())
// }
})
.chartXAxis(.automatic)
.chartYScale(domain: 0...20)
.chartForegroundStyleScale([
"Detection events" : .green,
])
.chartLegend(position: .automatic, alignment: .bottom)
}
.frame(minHeight: 250)
}
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
// Add a table for mac and ipad
Table(detections) {
TableColumn("Detection event") { d in
Text(d.messagePayload ?? "Detected")
}
TableColumn("timestamp") { d in
Text(d.timestamp.formattedDate(format: dateFormatString))
}
.width(min: 180)
}
} else {
ScrollView {
let columns = [
GridItem(),
GridItem()
]
LazyVGrid(columns: columns, alignment: .leading, spacing: 1) {
GridRow {
Text("Detection")
.font(.caption)
.fontWeight(.bold)
Text("timestamp")
.font(.caption)
.fontWeight(.bold)
}
ForEach(detections) { d in
GridRow {
Text(d.messagePayload ?? "Detected")
Text(d.timestamp.formattedDate(format: dateFormatString))
.font(.caption)
}
}
}
.padding(.leading, 15)
.padding(.trailing, 5)
}
}
}
HStack {
Button {
exportString = detectionsToCsv(detections: chartData)
isExporting = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
.padding(.trailing)
}
.navigationTitle("detection.sensor.log")
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
})
.onAppear {
self.bleManager.context = context
}
.fileExporter(
isPresented: $isExporting,
document: CsvDocument(emptyCsv: exportString),
contentType: .commaSeparatedText,
defaultFilename: String("\(node.user?.longName ?? "Node") \("detection.sensor.log".localized)"),
onCompletion: { result in
if case .success = result {
print("Detections metrics log download succeeded.")
self.isExporting = false
} else {
print("Detections log download failed: \(result).")
}
}
)
}
}
//
//struct DetectionSensorLog_Previews: PreviewProvider {
// static var previews: some View {
// DetectionSensorLog()
// }
//}

View file

@ -47,8 +47,7 @@ struct DeviceMetricsLog: View {
.accessibilityLabel("Line Series")
.accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)")
.foregroundStyle(batteryChartColor)
.interpolationMethod(.cardinal)
//.interpolationMethod(.catmullRom(alpha: 1.0))
.interpolationMethod(.catmullRom(alpha: 1.0))
Plot {
PointMark(
@ -181,7 +180,7 @@ struct DeviceMetricsLog: View {
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
.padding(.trailing)
.padding(.leading)
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingClearLogConfirm,
@ -195,6 +194,7 @@ struct DeviceMetricsLog: View {
}
}
}
Button {
exportString = telemetryToCsvFile(telemetry: deviceMetrics, metricsType: 0)
isExporting = true

View file

@ -169,7 +169,7 @@ struct EnvironmentMetricsLog: View {
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
.padding(.trailing)
.padding(.leading)
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingClearLogConfirm,
@ -191,7 +191,7 @@ struct EnvironmentMetricsLog: View {
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
.padding(.leading)
.padding(.trailing)
}
.navigationTitle("Environment Metrics Log")
.navigationBarTitleDisplayMode(.inline)

View file

@ -12,6 +12,16 @@ import SwiftUI
import CoreLocation
struct NodeList: View {
@State private var searchText = ""
var nodesQuery: Binding<String> {
Binding {
searchText
} set: { newValue in
searchText = newValue
nodes.nsPredicate = newValue.isEmpty ? nil : NSPredicate(format: "user.longName CONTAINS[c] %@ OR user.shortName CONTAINS[c] %@", newValue, newValue)
}
}
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@ -38,7 +48,7 @@ struct NodeList: View {
LazyVStack(alignment: .leading) {
HStack {
VStack(alignment: .leading) {
CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 44 : 22, brightness: 0.0, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white)
CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: (node.user?.shortName ?? "???").isEmoji() ? 44 : (node.user?.shortName?.count ?? 0 == 4 ? 19 : 26), brightness: 0.0, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white)
.padding(.trailing, 5)
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
if deviceMetrics?.count ?? 0 >= 1 {
@ -116,5 +126,7 @@ struct NodeList: View {
Text("select.node")
}
}
.searchable(text: nodesQuery, prompt: "Find a node")
}
}

View file

@ -16,6 +16,7 @@ struct NodeMap: View {
@EnvironmentObject var bleManager: BLEManager
@ObservedObject var tileManager = OfflineTileManager.shared
@StateObject var appState = AppState.shared
@State var selectedMapLayer: MapLayer = UserDefaults.mapLayer
@State var enableMapRecentering: Bool = UserDefaults.enableMapRecentering

View file

@ -127,7 +127,7 @@ struct PositionLog: View {
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
.padding(.trailing)
.padding(.leading)
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingClearLogConfirm,
@ -154,7 +154,7 @@ struct PositionLog: View {
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding(.bottom)
.padding(.leading)
.padding(.trailing)
}
.fileExporter(
isPresented: $isExporting,

View file

@ -151,7 +151,7 @@ struct DeviceConfig: View {
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.padding(.leading)
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingNodeDBResetConfirm,
@ -174,7 +174,7 @@ struct DeviceConfig: View {
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.padding(.trailing)
.confirmationDialog(
"All device and app data will be deleted. You will also need to forget your devices under Settings > Bluetooth.",
isPresented: $isPresentingFactoryResetConfirm,

View file

@ -144,7 +144,7 @@ struct LoRaConfig: View {
}
}
.pickerStyle(DefaultPickerStyle())
Text("Sets the maximum number of hops, default is 3. Increasing hops also increases air time utilization and should be used carefully.")
Text("Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully.")
.font(.caption)
HStack {

View file

@ -0,0 +1,243 @@
//
// DetectionSensorModule.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 8/16/23.
//
import SwiftUI
struct DetectionSensorConfig: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@Environment(\.dismiss) private var goBack
var node: NodeInfoEntity?
@State private var isPresentingSaveConfirm: Bool = false
@State var hasChanges: Bool = false
@State var enabled = false
/// DetectionSensorModule will sends a bell character with the messages.
@State var sendBell: Bool = false
@State var name: String = ""
@State var detectionTriggeredHigh: Bool = true
@State var usePullup: Bool = false
@State var minimumBroadcastSecs = 0
@State var stateBroadcastSecs = 0
@State var monitorPin = 0
var body: some View {
Form {
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
Text("There has been no response to a request for device metadata over the admin channel for this node.")
.font(.callout)
.foregroundColor(.orange)
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
// Let users know what is going on if they are using remote admin and don't have the config yet
if node?.detectionSensorConfig == nil {
Text("Detection Sensor config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
.font(.callout)
.foregroundColor(.orange)
} else {
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
.font(.title3)
.onAppear {
setDetectionSensorValues()
}
}
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
.font(.title3)
} else {
Text("Please connect to a radio to configure settings.")
.font(.callout)
.foregroundColor(.orange)
}
Section(header: Text("options")) {
Toggle(isOn: $enabled) {
Label("enabled", systemImage: "dot.radiowaves.right")
}
Toggle(isOn: $sendBell) {
Label("Send Bell", systemImage: "bell")
}
TextField("Friendly name (sent for detection alerts text messages)", text: $name, axis: .vertical)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: name, perform: { _ in
let totalBytes = name.utf8.count
// Only mess with the value if it is too big
if totalBytes > 20 {
let firstNBytes = Data(name.utf8.prefix(20))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the shortName back to the last place where it was the right size
name = maxBytesString
}
}
})
.foregroundColor(.gray)
}
Section(header: Text("Sensor option")) {
Picker("GPIO Pin to monitor", selection: $monitorPin) {
ForEach(0..<46) {
if $0 == 0 {
Text("unset")
} else {
Text("Pin \($0)")
}
}
}
.pickerStyle(DefaultPickerStyle())
Toggle(isOn: $detectionTriggeredHigh) {
Label("Detection trigger High", systemImage: "dial.high")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $usePullup) {
Label("Uses pullup resistor", systemImage: "arrow.up.to.line")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
Section(header: Text("update.interval")) {
Picker("Minimum time between detection broadcasts", selection: $minimumBroadcastSecs) {
ForEach(UpdateIntervals.allCases) { ui in
Text(ui.description).tag(ui.rawValue)
}
}
.pickerStyle(DefaultPickerStyle())
Text("Mininum time between detection broadcasts. Default is 45 seconds.")
.font(.caption)
Picker("State Broadcast Interval", selection: $stateBroadcastSecs) {
Text("Never").tag(0)
ForEach(UpdateIntervals.allCases) { ui in
Text(ui.description).tag(ui.rawValue)
}
}
.pickerStyle(DefaultPickerStyle())
Text("How often to send detection sensor state to mesh regardless of detection. Default is Never.")
.font(.caption)
}
}
.scrollDismissesKeyboard(.interactively)
.disabled(self.bleManager.connectedPeripheral == nil || node?.detectionSensorConfig == 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
) {
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
if connectedNode != nil {
let nodeName = node?.user?.longName ?? "unknown".localized
let buttonText = String.localizedStringWithFormat("save.config %@".localized, nodeName)
Button(buttonText) {
var dsc = ModuleConfig.DetectionSensorConfig()
dsc.enabled = self.enabled
dsc.sendBell = self.sendBell
dsc.name = self.name
dsc.monitorPin = UInt32(self.monitorPin)
dsc.detectionTriggeredHigh = self.detectionTriggeredHigh
dsc.usePullup = self.usePullup
dsc.minimumBroadcastSecs = UInt32(self.minimumBroadcastSecs)
dsc.stateBroadcastSecs = UInt32(self.stateBroadcastSecs)
let adminMessageId = bleManager.saveDetectionSensorModuleConfig(config: dsc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
if adminMessageId > 0 {
// Should show a saved successfully alert once I know that to be true
// for now just disable the button after a successful save
hasChanges = false
goBack()
} }
}
}
message: {
Text("config.save.confirm")
}
.navigationTitle("detection.sensor.config")
.navigationBarItems(trailing:
ZStack {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
})
.onAppear {
self.bleManager.context = context
setDetectionSensorValues()
// Need to request a Detection Sensor Module Config from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.detectionSensorConfig == nil {
print("empty detection sensor module config")
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
if node != nil && connectedNode != nil {
_ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
}
}
}
.onChange(of: enabled) { newEnabled in
if node != nil && node?.detectionSensorConfig != nil {
if newEnabled != node!.detectionSensorConfig!.enabled { hasChanges = true }
}
}
.onChange(of: sendBell) { newSendBell in
if node != nil && node?.detectionSensorConfig != nil {
if newSendBell != node!.detectionSensorConfig!.sendBell { hasChanges = true }
}
}
.onChange(of: detectionTriggeredHigh) { newDetectionTriggeredHigh in
if node != nil && node?.detectionSensorConfig != nil {
if newDetectionTriggeredHigh != node!.detectionSensorConfig!.detectionTriggeredHigh { hasChanges = true }
}
}
.onChange(of: usePullup) { newUsePullup in
if node != nil && node?.detectionSensorConfig != nil {
if newUsePullup != node!.detectionSensorConfig!.usePullup { hasChanges = true }
}
}
.onChange(of: name) { newName in
if node != nil && node?.detectionSensorConfig != nil {
if newName != node!.detectionSensorConfig!.name { hasChanges = true }
}
}
.onChange(of: monitorPin) { newMonitorPin in
if node != nil && node?.detectionSensorConfig != nil {
if newMonitorPin != node!.detectionSensorConfig!.monitorPin { hasChanges = true }
}
}
.onChange(of: minimumBroadcastSecs) { newMinimumBroadcastSecs in
if node != nil && node?.detectionSensorConfig != nil {
if newMinimumBroadcastSecs != node!.detectionSensorConfig!.minimumBroadcastSecs { hasChanges = true }
}
}
.onChange(of: stateBroadcastSecs) { newStateBroadcastSecs in
if node != nil && node?.detectionSensorConfig != nil {
if newStateBroadcastSecs != node!.detectionSensorConfig!.stateBroadcastSecs { hasChanges = true }
}
}
}
func setDetectionSensorValues() {
self.enabled = (node?.detectionSensorConfig?.enabled ?? false)
self.sendBell = (node?.detectionSensorConfig?.sendBell ?? false)
self.name = (node?.detectionSensorConfig?.name ?? "")
self.monitorPin = Int(node?.detectionSensorConfig?.monitorPin ?? 0)
self.usePullup = (node?.detectionSensorConfig?.usePullup ?? false)
self.detectionTriggeredHigh = (node?.detectionSensorConfig?.detectionTriggeredHigh ?? true)
self.minimumBroadcastSecs = Int(node?.detectionSensorConfig?.minimumBroadcastSecs ?? 45)
self.stateBroadcastSecs = Int(node?.detectionSensorConfig?.stateBroadcastSecs ?? 0)
self.hasChanges = false
}
}

View file

@ -65,6 +65,8 @@ struct MQTTConfig: View {
Label("mqtt.clientproxy", systemImage: "iphone.radiowaves.left.and.right")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("If both MQTT and the client proxy are enabled your device will utalize an available network connection to connect to the specified MQTT server.")
.font(.caption2)
Toggle(isOn: $encryptionEnabled) {
@ -82,8 +84,6 @@ struct MQTTConfig: View {
Label("JSON Enabled", systemImage: "ellipsis.curlybraces")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("JSON mode is not reccomended it is incomplete and unstable.")
.font(.caption2)
}
Section(header: Text("Custom Server")) {
HStack {
@ -187,7 +187,7 @@ struct MQTTConfig: View {
Text("The root topic to use for MQTT messages. Default is \"msh\". This is useful if you want to use a single MQTT server for multiple meshtastic networks and separate them via ACLs")
.font(.caption2)
}
Text("WiFi or Ethernet must also be enabled for MQTT to work. You can set uplink and downlink for each channel.")
Text("You can set uplink and downlink for each channel.")
.font(.callout)
}
.scrollDismissesKeyboard(.interactively)

View file

@ -72,13 +72,13 @@ struct RangeTestConfig: View {
.font(.caption)
}
}
.disabled(self.bleManager.connectedPeripheral == nil || node?.rangeTestConfig == nil || !(node != nil && node?.metadata?.hasWifi ?? false))
.disabled(self.bleManager.connectedPeripheral == nil || node?.rangeTestConfig == nil)
Button {
isPresentingSaveConfirm = true
} label: {
Label("save", systemImage: "square.and.arrow.down")
}
.disabled(bleManager.connectedPeripheral == nil || !hasChanges || !(node?.metadata?.hasWifi ?? false))
.disabled(bleManager.connectedPeripheral == nil || !hasChanges)
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)

View file

@ -67,23 +67,24 @@ struct MeshLog: View {
print(error)
}
} label: {
Label("Clear Log", systemImage: "trash.fill")
Label("Clear", systemImage: "trash.fill")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
Spacer()
.padding(.bottom)
.padding(.leading)
Button {
isExporting = true
} label: {
Label("Save Log", systemImage: "square.and.arrow.down")
Label("Save", systemImage: "square.and.arrow.down")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.padding(.bottom)
.padding(.trailing)
Spacer()
}
.padding(.bottom, 10)

View file

@ -31,6 +31,7 @@ struct Settings: View {
case networkConfig
case positionConfig
case cannedMessagesConfig
case detectionSensorConfig
case externalNotificationConfig
case mqttConfig
case rangeTestConfig
@ -203,6 +204,17 @@ struct Settings: View {
}
.tag(SettingsSidebar.cannedMessagesConfig)
NavigationLink {
DetectionSensorConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "sensor")
.symbolRenderingMode(.hierarchical)
Text("detection.sensor")
}
.tag(SettingsSidebar.detectionSensorConfig)
NavigationLink {
ExternalNotificationConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {

View file

@ -8,7 +8,6 @@
import WidgetKit
import SwiftUI
@available(iOSApplicationExtension 16.2, *)
@main
struct WidgetsBundle: WidgetBundle {
var body: some Widget {

View file

@ -194,6 +194,7 @@
"name"="Name";
"network"="Netzwerk";
"network.config"="Netzwerkeinstellungen";
"nodes"="Nodes";
"nodes %@"="Nodes (%@)";
"no.nodes"="Keine Meshtastic Nodes gefunden";
"not.connected"="Kein Gerät verbunden";

View file

@ -57,6 +57,9 @@
"current"="Current";
"default"="Default";
"delete"="Delete";
"detection.sensor"="Detection Sensor";
"detection.sensor.config"="Detection Sensor Config";
"detection.sensor.log"="Detection Sensor Log";
"device"="Device";
"device.config"="Device Config";
"device.metrics.delete"="Delete all device metrics?";
@ -194,6 +197,7 @@
"name"="Name";
"network"="Network";
"network.config"="Network Config";
"nodes"="Nodes";
"nodes %@"="Nodes (%@)";
"no.nodes"="No Meshtastic Nodes Found";
"not.connected"="No device connected";

4
thebenternify.sh Executable file
View file

@ -0,0 +1,4 @@
#!/bin/bash
sed -i '' -e 's/GCH7VS5Y9R/6YF6QJH524/g' ./Meshtastic.xcodeproj/project.pbxproj
sed -i '' -e 's/gvh.Meshtastic/thebentern.Meshtastic/g' ./Meshtastic.xcodeproj/project.pbxproj

4
unthebenternify.sh Executable file
View file

@ -0,0 +1,4 @@
#!/bin/bash
sed -i '' -e 's/6YF6QJH524/GCH7VS5Y9R/g' ./Meshtastic.xcodeproj/project.pbxproj
sed -i '' -e 's/thebentern.Meshtastic/gvh.Meshtastic/g' ./Meshtastic.xcodeproj/project.pbxproj

View file

@ -194,6 +194,7 @@
"name"="名称";
"network"="网络";
"network.config"="网络配置";
"nodes"="节点";
"nodes %@"="节点 (%@)";
"no.nodes"="未找到 Meshtastic 节点";
"not.connected"="未连接到电台";