Merge pull request #624 from meshtastic/2.3.7_Working_Changes

2.3.7 working changes
This commit is contained in:
Garth Vander Houwen 2024-05-01 19:49:58 -07:00 committed by GitHub
commit ee22af34d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1108 additions and 290 deletions

View file

@ -50,6 +50,7 @@
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */; };
DD33DB622B3D27C7003E1EA0 /* FirmwareApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */; };
DD3501892852FC3B000FC853 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3501882852FC3B000FC853 /* Settings.swift */; };
DD354FD92BD96A0B0061A25F /* IAQScale.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD354FD82BD96A0B0061A25F /* IAQScale.swift */; };
DD3619152B1EF9F900C41C8C /* LocationsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */; };
DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */; };
DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */; };
@ -87,7 +88,7 @@
DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51FF298EE33B00D21B61 /* remote_hardware.pb.swift */; };
DD5E5212298EE33B00D21B61 /* apponly.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E5200298EE33B00D21B61 /* apponly.pb.swift */; };
DD5E5213298EE33B00D21B61 /* deviceonly.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E5201298EE33B00D21B61 /* deviceonly.pb.swift */; };
DD5E523F298F5A9E00D21B61 /* AirQualityIndexCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E523E298F5A9E00D21B61 /* AirQualityIndexCompact.swift */; };
DD5E523F298F5A9E00D21B61 /* AirQualityIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E523E298F5A9E00D21B61 /* AirQualityIndex.swift */; };
DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */; };
DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */; };
DD6193792863875F00E59241 /* SerialConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193782863875F00E59241 /* SerialConfig.swift */; };
@ -292,10 +293,12 @@
DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV22.xcdatamodel; sourceTree = "<group>"; };
DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewSwiftUI.swift; sourceTree = "<group>"; };
DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV19.xcdatamodel; sourceTree = "<group>"; };
DD31B04D2BDC6FD30024FA63 /* MeshtasticDataModelV 36.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 36.xcdatamodel"; sourceTree = "<group>"; };
DD31EC492B7F18B7006A3995 /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Localizable.strings; sourceTree = "<group>"; };
DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 23.xcdatamodel"; sourceTree = "<group>"; };
DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareApi.swift; sourceTree = "<group>"; };
DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
DD354FD82BD96A0B0061A25F /* IAQScale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAQScale.swift; sourceTree = "<group>"; };
DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV21.xcdatamodel; sourceTree = "<group>"; };
DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsHandler.swift; sourceTree = "<group>"; };
DD398EBD2B93F640002B4C51 /* MeshtasticDataModelV 29.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 29.xcdatamodel"; sourceTree = "<group>"; };
@ -339,7 +342,7 @@
DD5E51FF298EE33B00D21B61 /* remote_hardware.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = remote_hardware.pb.swift; sourceTree = "<group>"; };
DD5E5200298EE33B00D21B61 /* apponly.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = apponly.pb.swift; sourceTree = "<group>"; };
DD5E5201298EE33B00D21B61 /* deviceonly.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = deviceonly.pb.swift; sourceTree = "<group>"; };
DD5E523E298F5A9E00D21B61 /* AirQualityIndexCompact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirQualityIndexCompact.swift; sourceTree = "<group>"; };
DD5E523E298F5A9E00D21B61 /* AirQualityIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirQualityIndex.swift; sourceTree = "<group>"; };
DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalNotificationConfig.swift; sourceTree = "<group>"; };
DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = "<group>"; };
DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = "<group>"; };
@ -649,9 +652,11 @@
DD5E523D298F5A7D00D21B61 /* Weather */ = {
isa = PBXGroup;
children = (
DD5E523E298F5A9E00D21B61 /* AirQualityIndexCompact.swift */,
DD5E523E298F5A9E00D21B61 /* AirQualityIndex.swift */,
DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */,
DDA9515D2BC6F56F00CEA535 /* IndoorAirQuality.swift */,
DD41A61429AB0035003C5A37 /* NodeWeatherForecast.swift */,
DD354FD82BD96A0B0061A25F /* IAQScale.swift */,
);
path = Weather;
sourceTree = "<group>";
@ -921,7 +926,6 @@
DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */,
DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */,
DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */,
DDA9515D2BC6F56F00CEA535 /* IndoorAirQuality.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -1204,7 +1208,7 @@
DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */,
DD0E20FD2B87090400F2D100 /* clientonly.pb.swift in Sources */,
D93069082B81DF040066FBC8 /* SaveConfigButton.swift in Sources */,
DD5E523F298F5A9E00D21B61 /* AirQualityIndexCompact.swift in Sources */,
DD5E523F298F5A9E00D21B61 /* AirQualityIndex.swift in Sources */,
DD964FBF296E76EF007C176F /* WaypointFormMapKit.swift in Sources */,
DD3501892852FC3B000FC853 /* Settings.swift in Sources */,
DDDC22382BA92344002C44F1 /* MeshMapContent.swift in Sources */,
@ -1239,6 +1243,7 @@
DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */,
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */,
DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */,
DD354FD92BD96A0B0061A25F /* IAQScale.swift in Sources */,
DDDB445429F8AD1600EE2349 /* Data.swift in Sources */,
DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */,
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */,
@ -1578,7 +1583,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.3.6;
MARKETING_VERSION = 2.3.7;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1612,7 +1617,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.3.6;
MARKETING_VERSION = 2.3.7;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1685,7 +1690,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.3.6;
MARKETING_VERSION = 2.3.7;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1718,7 +1723,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.3.6;
MARKETING_VERSION = 2.3.7;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1820,6 +1825,7 @@
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
DD31B04D2BDC6FD30024FA63 /* MeshtasticDataModelV 36.xcdatamodel */,
DD268D8C2BCC7D11008073AE /* MeshtasticDataModelV 35.xcdatamodel */,
DDDBC87C2BC65682001E8DF7 /* MeshtasticDataModelV 34.xcdatamodel */,
DDF45C382BC46B16005ED5F2 /* MeshtasticDataModelV33.xcdatamodel */,
@ -1856,7 +1862,7 @@
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
);
currentVersion = DD268D8C2BCC7D11008073AE /* MeshtasticDataModelV 35.xcdatamodel */;
currentVersion = DD31B04D2BDC6FD30024FA63 /* MeshtasticDataModelV 36.xcdatamodel */;
name = Meshtastic.xcdatamodeld;
path = Meshtastic/Meshtastic.xcdatamodeld;
sourceTree = "<group>";

View file

@ -8,6 +8,86 @@
import Foundation
import SwiftUI
enum Aqi: Int, CaseIterable, Identifiable {
case good = 0
case moderate = 1
case sensitive = 2
case unhealthy = 3
case veryUnhealthy = 4
case hazardous = 5
var id: Int { self.rawValue }
var description: String {
switch self {
case .good:
return "Good"
case .moderate:
return "Moderate"
case .sensitive:
return "Unhealthy for Sensitive Groups"
case .unhealthy:
return "Unhealthy"
case .veryUnhealthy:
return "Very Unhealthy"
case .hazardous:
return "Hazardous"
}
}
var color: Color {
switch self {
case .good:
return .green
case .moderate:
return .yellow
case .sensitive:
return .orange
case .unhealthy:
return .red
case .veryUnhealthy:
return .purple
case .hazardous:
return .magenta
}
}
var range: Range<Int> {
switch self {
case .good:
return Range(0...50)
case .moderate:
return Range(51...100)
case .sensitive:
return Range(101...150)
case .unhealthy:
return Range(151...200)
case .veryUnhealthy:
return Range(201...300)
case .hazardous:
return Range(301...500)
}
}
static func getAqi(for value: Int) -> Aqi {
let aqi: Aqi
switch value {
case 0...50:
aqi = .good
case 51...100:
aqi = .moderate
case 101...150:
aqi = .sensitive
case 151...200:
aqi = .unhealthy
case 201...300:
aqi = .veryUnhealthy
case 301...500:
aqi = .hazardous
default:
fatalError("Invalid int value")
}
return aqi
}
}
enum Iaq: Int, CaseIterable, Identifiable {
case excellent = 0
case good = 1
@ -27,7 +107,7 @@ enum Iaq: Int, CaseIterable, Identifiable {
case .lightlyPolluted:
return "Lightly Polluted"
case .moderatelyPolluted:
return "Lightly Polluted"
return "Moderately Polluted"
case .heavilyPolluted:
return "Heavily Polluted"
case .severelyPolluted:
@ -49,11 +129,30 @@ enum Iaq: Int, CaseIterable, Identifiable {
case .heavilyPolluted:
return .red
case .severelyPolluted:
return .purple
return .magenta
case .extremelyPolluted:
return .brown
}
}
var range: Range<Int> {
switch self {
case .excellent:
return Range(0...50)
case .good:
return Range(51...100)
case .lightlyPolluted:
return Range(101...150)
case .moderatelyPolluted:
return Range(151...200)
case .heavilyPolluted:
return Range(201...250)
case .severelyPolluted:
return Range(251...350)
case .extremelyPolluted:
return Range(351...500)
}
}
static func getIaq(for value: Int) -> Iaq {
let iaq: Iaq
switch value {

View file

@ -32,7 +32,7 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
}
} else if metricsType == 1 {
// Create Environment Telemetry Header
csvString = "Temperature, Relative Humidity, Barometric Pressure, Indoor Air Quality, Gas Resistance, \("voltage".localized), \("current".localized), \("timestamp".localized)"
csvString = "Temperature, Relative Humidity, Barometric Pressure, Indoor Air Quality, Gas Resistance, \("timestamp".localized)"
for dm in telemetry {
if dm.metricsType == 1 {
csvString += "\n"
@ -46,10 +46,6 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
csvString += ", "
csvString += String(dm.gasResistance)
csvString += ", "
csvString += String(dm.voltage)
csvString += ", "
csvString += String(dm.current)
csvString += ", "
csvString += dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized
}
}

View file

@ -17,6 +17,7 @@ extension Color {
let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000
return (brightness > 0.5)
}
public static let magenta = Color(red: 0.50, green: 0.00, blue: 0.00)
}
extension UIColor {

View file

@ -18,4 +18,14 @@ extension ChannelEntity {
let unreadMessages = allPrivateMessages.filter{ ($0 as AnyObject).read == false }
return unreadMessages.count
}
var protoBuf: Channel {
var channel = Channel()
channel.index = self.index
channel.settings.name = self.name ?? ""
channel.settings.psk = self.psk ?? Data()
channel.role = Channel.Role(rawValue: Int(self.role)) ?? Channel.Role.secondary
channel.settings.moduleSettings.positionPrecision = UInt32(self.positionPrecision)
return channel
}
}

View file

@ -68,6 +68,7 @@ extension UserDefaults {
case enableSmartPosition
case newNodeNotifications
case lowBatteryNotifications
case channelMessageNotifications
case modemPreset
case firmwareVersion
case testIntEnum
@ -146,6 +147,9 @@ extension UserDefaults {
@UserDefault(.enableSmartPosition, defaultValue: false)
static var enableSmartPosition: Bool
@UserDefault(.channelMessageNotifications, defaultValue: true)
static var channelMessageNotifications: Bool
@UserDefault(.newNodeNotifications, defaultValue: true)
static var newNodeNotifications: Bool

View file

@ -151,12 +151,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
isConnecting = false
isConnected = false
isSubscribed = false
self.connectedPeripheral = nil
invalidVersion = false
connectedVersion = "0.0.0"
connectedPeripheral = nil
if timeoutTimer != nil {
timeoutTimer!.invalidate()
}
automaticallyReconnect = false
stopScanning()
startScanning()
}
@ -556,7 +559,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
tryClearExistingChannels()
}
// NodeInfo
if decodedInfo.nodeInfo.num > 0 {// && !invalidVersion {
if context != nil && decodedInfo.nodeInfo.num > 0 {// && !invalidVersion {
nowKnown = true
let nodeInfo = nodeInfoPacket(nodeInfo: decodedInfo.nodeInfo, channel: decodedInfo.packet.channel, context: context!)
@ -570,17 +573,17 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
}
// Channels
if decodedInfo.channel.isInitialized && connectedPeripheral != nil {
if context != nil && decodedInfo.channel.isInitialized && connectedPeripheral != nil {
nowKnown = true
channelPacket(channel: decodedInfo.channel, fromNum: Int64(truncatingIfNeeded: connectedPeripheral.num), context: context!)
}
// Config
if decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil {
if context != nil && decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil {
nowKnown = true
localConfig(config: decodedInfo.config, context: context!, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral.longName)
}
// Module Config
if decodedInfo.moduleConfig.isInitialized && !invalidVersion && self.connectedPeripheral?.num != 0{
if context != nil && decodedInfo.moduleConfig.isInitialized && !invalidVersion && self.connectedPeripheral?.num != 0{
nowKnown = true
moduleConfig(config: decodedInfo.moduleConfig, context: context!, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral?.num ?? 0), nodeLongName: self.connectedPeripheral.longName)
if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) {
@ -590,7 +593,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
}
}
// Device Metadata
if decodedInfo.metadata.firmwareVersion.count > 0 && !invalidVersion {
if context != nil && decodedInfo.metadata.firmwareVersion.count > 0 && !invalidVersion {
nowKnown = true
deviceMetadataPacket(metadata: decodedInfo.metadata, fromNum: connectedPeripheral.num, context: context!)
connectedPeripheral.firmwareVersion = decodedInfo.metadata.firmwareVersion
@ -994,37 +997,28 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
return success
}
public func getPositionFromPhoneGPS(channel: Int32, destNum: Int64) -> Position? {
public func getPositionFromPhoneGPS(destNum: Int64) -> Position? {
var positionPacket = Position()
let fetchChannelRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "ChannelEntity")
fetchChannelRequest.predicate = NSPredicate(format: "index == %lld", channel)
do {
guard let fetchedChannel = try context!.fetch(fetchChannelRequest) as? [ChannelEntity] else {
return nil
}
if #available(iOS 17.0, macOS 14.0, *) {
if let lastLocation = LocationsHandler.shared.locationsArray.last {
if fetchedChannel.count > 0 {
positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7)
positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7)
let timestamp = lastLocation.timestamp
positionPacket.time = UInt32(timestamp.timeIntervalSince1970)
positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970)
positionPacket.altitude = Int32(lastLocation.altitude)
positionPacket.satsInView = UInt32(LocationsHandler.satsInView)
positionPacket.precisionBits = UInt32(fetchedChannel[0].positionPrecision)
let currentSpeed = lastLocation.speed
if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) {
positionPacket.groundSpeed = UInt32(currentSpeed * 3.6)
}
let currentHeading = lastLocation.course
if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) {
positionPacket.groundTrack = UInt32(currentHeading)
}
positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7)
positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7)
let timestamp = lastLocation.timestamp
positionPacket.time = UInt32(timestamp.timeIntervalSince1970)
positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970)
positionPacket.altitude = Int32(lastLocation.altitude)
positionPacket.satsInView = UInt32(LocationsHandler.satsInView)
let currentSpeed = lastLocation.speed
if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) {
positionPacket.groundSpeed = UInt32(currentSpeed * 3.6)
}
let currentHeading = lastLocation.course
if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) {
positionPacket.groundTrack = UInt32(currentHeading)
}
}
@ -1032,9 +1026,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if destNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 {
return nil
}
if fetchedChannel.count <= 0 {
return nil
}
positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7)
positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7)
@ -1043,7 +1034,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970)
positionPacket.altitude = Int32(LocationHelper.shared.locationManager.location?.altitude ?? 0)
positionPacket.satsInView = UInt32(LocationHelper.satsInView)
positionPacket.precisionBits = UInt32(fetchedChannel[0].positionPrecision)
let currentSpeed = LocationHelper.shared.locationManager.location?.speed ?? 0
if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) {
positionPacket.groundSpeed = UInt32(currentSpeed * 3.6)
@ -1053,17 +1043,15 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
positionPacket.groundTrack = UInt32(currentHeading)
}
}
} catch {
return nil
}
return positionPacket
}
public func setFixedPosition(fromUser: UserEntity, channel: Int32) -> Bool {
var adminPacket = AdminMessage()
guard let positionPacket = getPositionFromPhoneGPS(channel: channel, destNum: fromUser.num) else {
guard let positionPacket = getPositionFromPhoneGPS(destNum: fromUser.num) else {
return false
}
adminPacket.setFixedPosition = positionPacket
@ -1109,7 +1097,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
public func sendPosition(channel: Int32, destNum: Int64, wantResponse: Bool) -> Bool {
var success = false
let fromNodeNum = connectedPeripheral.num
guard let positionPacket = getPositionFromPhoneGPS(channel: channel, destNum: destNum) else {
guard let positionPacket = getPositionFromPhoneGPS(destNum: destNum) else {
return false
}

View file

@ -8,6 +8,7 @@
import Foundation
import CoreData
import SwiftUI
import RegexBuilder
#if canImport(ActivityKit)
import ActivityKit
#endif
@ -160,12 +161,14 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo
newChannel.name = channel.settings.name
newChannel.role = Int32(channel.role.rawValue)
newChannel.psk = channel.settings.psk
newChannel.positionPrecision = Int32(truncatingIfNeeded: channel.settings.moduleSettings.positionPrecision)
if channel.settings.hasModuleSettings {
newChannel.positionPrecision = Int32(truncatingIfNeeded: channel.settings.moduleSettings.positionPrecision)
newChannel.mute = channel.settings.moduleSettings.isClientMuted
}
guard let mutableChannels = fetchedMyInfo[0].channels!.mutableCopy() as? NSMutableOrderedSet else {
return
}
if let oldChannel = mutableChannels.first(where: {($0 as AnyObject).index == newChannel.index }) as? ChannelEntity {
newChannel.mute = oldChannel.mute
let index = mutableChannels.index(of: oldChannel as Any)
mutableChannels.replaceObject(at: index, with: newChannel)
} else {
@ -772,7 +775,19 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connectedNode: Int64, storeForward: Bool = false, context: NSManagedObjectContext) {
var messageText = String(bytes: packet.decoded.payload, encoding: .utf8)
if !wantRangeTestPackets && (String(messageText ?? "seq ").starts(with: "seq ")) {
let rangeRef = Reference(Int.self)
let rangeTestRegex = Regex {
"seq "
TryCapture(as: rangeRef) {
OneOrMore(.digit)
} transform: { match in
Int(match)
}
}
let rangeTest = messageText?.contains(rangeTestRegex) ?? false && messageText?.starts(with: "seq ") ?? false
if !wantRangeTestPackets && rangeTest {
return
}
var storeForwardBroadcast = false
@ -841,26 +856,28 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
return
}
let appState = AppState.shared
if newMessage.fromUser != nil && newMessage.toUser != nil && !(newMessage.fromUser?.mute ?? false) {
if newMessage.fromUser != nil && newMessage.toUser != nil {
// Set Unread Message Indicators
if packet.to == connectedNode {
appState.unreadDirectMessages = newMessage.toUser?.unreadMessages ?? 0
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages
}
// Create an iOS Notification for the received DM message and schedule it immediately
let manager = LocalNotificationManager()
manager.notifications = [
Notification(
id: ("notification.id.\(newMessage.messageId)"),
title: "\(newMessage.fromUser?.longName ?? "unknown".localized)",
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
content: messageText!,
target: "message",
path: "meshtastic://open-dm?userid=\(newMessage.fromUser?.num ?? 0)&id=\(newMessage.messageId)"
)
]
manager.schedule()
print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)")
if !(newMessage.fromUser?.mute ?? false) {
// Create an iOS Notification for the received DM message and schedule it immediately
let manager = LocalNotificationManager()
manager.notifications = [
Notification(
id: ("notification.id.\(newMessage.messageId)"),
title: "\(newMessage.fromUser?.longName ?? "unknown".localized)",
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
content: messageText!,
target: "message",
path: "meshtastic://open-dm?userid=\(newMessage.fromUser?.num ?? 0)&id=\(newMessage.messageId)"
)
]
manager.schedule()
print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)")
}
} else if newMessage.fromUser != nil && newMessage.toUser == nil {
let fetchMyInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MyInfoEntity")
@ -878,7 +895,7 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec
if channel.index == newMessage.channel {
context.refresh(channel, mergeChanges: true)
}
if channel.index == newMessage.channel && !channel.mute {
if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications {
// Create an iOS Notification for the received private channel message and schedule it immediately
let manager = LocalNotificationManager()
manager.notifications = [

View file

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

View file

@ -0,0 +1,461 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22757" systemVersion="23F5064f" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="AmbientLightingConfigEntity" representedClassName="AmbientLightingConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="blue" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="current" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="green" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="ledState" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="red" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="ambientLightingConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="ambientLightingConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<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" attributeType="Boolean" defaultValueString="NO" 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="positionPrecision" optional="YES" attributeType="Integer 32" defaultValueString="32" usesScalarValueType="YES"/>
<attribute name="psk" optional="YES" attributeType="Binary"/>
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="uplinkEnabled" attributeType="Boolean" defaultValueString="NO" 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="disableTripleClick" 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="ledHeartbeatEnabled" optional="YES" attributeType="Boolean" defaultValueString="YES" 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"/>
<attribute name="tzdef" optional="YES" attributeType="String"/>
<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"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<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="units" 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="useI2SAsBuzzer" optional="YES" attributeType="Boolean" 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"/>
<fetchedProperty name="fetchedProperty" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="ExternalNotificationConfigEntity"/>
</fetchedProperty>
</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" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitudeI" 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="ignoreMqtt" optional="YES" attributeType="Boolean" defaultValueString="NO" 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="read" attributeType="Boolean" defaultValueString="NO" 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="mapPositionPrecision" optional="YES" attributeType="Integer 32" defaultValueString="13" usesScalarValueType="YES"/>
<attribute name="mapPublishIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="mapReportingEnabled" 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"/>
<fetchedProperty name="allMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="toUser == nil"/>
</fetchedProperty>
<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="favorite" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="hopsAway" 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"/>
<attribute name="viaMqtt" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<relationship name="ambientLightingConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="AmbientLightingConfigEntity" inverseName="ambientLightingConfigNode" inverseEntity="AmbientLightingConfigEntity"/>
<relationship name="bluetoothConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="BluetoothConfigEntity" inverseName="bluetoothConfigNode" inverseEntity="BluetoothConfigEntity"/>
<relationship name="cannedMessageConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="CannedMessageConfigEntity" inverseName="cannedMessagesConfigNode" inverseEntity="CannedMessageConfigEntity"/>
<relationship name="detectionSensorConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DetectionSensorConfigEntity" inverseName="detectionSensorConfigNode" inverseEntity="DetectionSensorConfigEntity"/>
<relationship name="deviceConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DeviceConfigEntity" inverseName="deviceConfigNode" inverseEntity="DeviceConfigEntity"/>
<relationship name="displayConfig" optional="YES" maxCount="1" deletionRule="Cascade" 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="pax" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PaxCounterEntity" inverseName="paxNode" inverseEntity="PaxCounterEntity"/>
<relationship name="paxCounterConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PaxCounterConfigEntity" inverseName="paxCounterConfigNode" inverseEntity="PaxCounterConfigEntity"/>
<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="powerConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PowerConfigEntity" inverseName="powerConfigNode" inverseEntity="PowerConfigEntity"/>
<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="storeForwardConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreForwardConfigEntity" inverseName="storeForwardConfigNode" inverseEntity="StoreForwardConfigEntity"/>
<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="traceRoutes" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteEntity" inverseName="node" inverseEntity="TraceRouteEntity"/>
<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="PaxCounterConfigEntity" representedClassName="PaxCounterConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="paxcounterUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="paxCounterConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="paxCounterConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="PaxCounterEntity" representedClassName="PaxCounterEntity" syncable="YES" codeGenerationType="class">
<attribute name="ble" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="uptime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wifi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="paxNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="pax" inverseEntity="NodeInfoEntity"/>
</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="gpsEnGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="gpsMode" 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="precisionBits" optional="YES" attributeType="Integer 32" defaultValueString="32" 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="PowerConfigEntity" representedClassName="PowerConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="adcMultiplierOverride" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
<attribute name="deviceBatteryInaAddress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isPowerSaving" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="lsSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="minWakeSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="onBatteryShutdownAfterSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="waitBluetoothSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="powerConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="powerConfig" 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 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="distance" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="elevationGain" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="endDate" 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" toMany="YES" deletionRule="Cascade" ordered="YES" 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="overrideConsoleSerialPort" optional="YES" attributeType="Boolean" defaultValueString="NO" 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="StoreForwardConfigEntity" representedClassName="StoreForwardConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="heartbeat" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="historyReturnMax" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="historyReturnWindow" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isRouter" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lastHeartbeat" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="lastRequest" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="records" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="storeForwardConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="storeForwardConfig" 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"/>
<attribute name="powerMeasurementEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="powerScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="powerUpdateInterval" 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="iaq" optional="YES" attributeType="Integer 32" defaultValueString="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="uptimeSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<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="TraceRouteEntity" representedClassName="TraceRouteEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="hasPositions" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="response" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="route" optional="YES" attributeType="Transformable" customClassName="[UInt32]"/>
<attribute name="routeText" optional="YES" attributeType="String"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="hops" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteHopEntity" inverseName="traceRoute" inverseEntity="TraceRouteHopEntity"/>
<relationship name="node" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="traceRoutes" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TraceRouteHopEntity" representedClassName="TraceRouteHopEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="time" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="traceRoute" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TraceRouteEntity" inverseName="hops" inverseEntity="TraceRouteEntity"/>
</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="lastMessage" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<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="numString" optional="YES" attributeType="String"/>
<attribute name="role" attributeType="Integer 32" 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 AND portNum != 10 "/>
</fetchedProperty>
<fetchedProperty name="detectionSensorMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(fromUser.num == $FETCH_SOURCE.num) AND portNum = 10"/>
</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"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
</model>

View file

@ -462,6 +462,7 @@ func upsertDeviceConfigPacket(config: Meshtastic.Config.DeviceConfig, nodeNum: I
newDeviceConfig.rebroadcastMode = Int32(config.rebroadcastMode.rawValue)
newDeviceConfig.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber)
newDeviceConfig.doubleTapAsButtonPress = config.doubleTapAsButtonPress
newDeviceConfig.ledHeartbeatEnabled = !config.ledHeartbeatDisabled
newDeviceConfig.isManaged = config.isManaged
newDeviceConfig.tzdef = config.tzdef
fetchedNode[0].deviceConfig = newDeviceConfig
@ -474,6 +475,7 @@ func upsertDeviceConfigPacket(config: Meshtastic.Config.DeviceConfig, nodeNum: I
fetchedNode[0].deviceConfig?.rebroadcastMode = Int32(config.rebroadcastMode.rawValue)
fetchedNode[0].deviceConfig?.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber)
fetchedNode[0].deviceConfig?.doubleTapAsButtonPress = config.doubleTapAsButtonPress
fetchedNode[0].deviceConfig?.ledHeartbeatEnabled = !config.ledHeartbeatDisabled
fetchedNode[0].deviceConfig?.isManaged = config.isManaged
fetchedNode[0].deviceConfig?.tzdef = config.tzdef
}

View file

@ -120,6 +120,11 @@ struct ModuleSettings {
/// Bits of precision for the location sent in position packets.
var positionPrecision: UInt32 = 0
///
/// Controls whether or not the phone / clients should mute the current channel
/// Useful for noisy public channels you don't necessarily want to disable
var isClientMuted: Bool = false
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
@ -311,6 +316,7 @@ extension ModuleSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement
static let protoMessageName: String = _protobuf_package + ".ModuleSettings"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .standard(proto: "position_precision"),
2: .standard(proto: "is_client_muted"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -320,6 +326,7 @@ extension ModuleSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularUInt32Field(value: &self.positionPrecision) }()
case 2: try { try decoder.decodeSingularBoolField(value: &self.isClientMuted) }()
default: break
}
}
@ -329,11 +336,15 @@ extension ModuleSettings: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement
if self.positionPrecision != 0 {
try visitor.visitSingularUInt32Field(value: self.positionPrecision, fieldNumber: 1)
}
if self.isClientMuted != false {
try visitor.visitSingularBoolField(value: self.isClientMuted, fieldNumber: 2)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: ModuleSettings, rhs: ModuleSettings) -> Bool {
if lhs.positionPrecision != rhs.positionPrecision {return false}
if lhs.isClientMuted != rhs.isClientMuted {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}

View file

@ -266,6 +266,18 @@ struct Connect: View {
.controlSize(.large)
.padding()
}
if bleManager.isConnecting {
Button(role: .destructive, action: {
bleManager.cancelPeripheralConnection()
}) {
Label("disconnect", systemImage: "antenna.radiowaves.left.and.right.slash")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
}
#endif
Spacer()
}

View file

@ -29,10 +29,8 @@ struct CircleText: View {
struct CircleText_Previews: PreviewProvider {
static var previews: some View {
HStack {
VStack {
VStack {
HStack {
CircleText(text: "N1", color: Color.yellow, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "8", color: Color.purple, circleSize: 80)
@ -41,17 +39,20 @@ struct CircleText_Previews: PreviewProvider {
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "🍔", color: Color.brown, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
}
HStack {
CircleText(text: "👻", color: Color.orange, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "🤙", color: Color.orange, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
}
VStack {
CircleText(text: "69", color: Color.green, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "WWWW", color: Color.cyan, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
}
HStack {
CircleText(text: "CW-A", color: Color.secondary)
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "CW-A", color: Color.secondary, circleSize: 80)
@ -60,7 +61,20 @@ struct CircleText_Previews: PreviewProvider {
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "IIII", color: Color.accentColor, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "LCP", color: Color.primary, circleSize: 80)
}
HStack {
CircleText(text: "🚗", color: Color.orange)
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "🔋", color: Color.indigo, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "🛢️", color: Color.orange, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
CircleText(text: "LCP", color: Color.indigo, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
}
HStack {
CircleText(text: "🤡", color: Color.red, circleSize: 80)
.previewLayout(.fixed(width: 300, height: 100))
}
}

View file

@ -1,125 +0,0 @@
//
// IndoorAirQuality.swift
// Meshtastic
//
// Copyright(c) by Garth Vander Houwen on 4/10/24.
//
import Foundation
import SwiftUI
enum IaqDisplayMode: Int, CaseIterable, Identifiable {
case pill = 0
case dot = 1
case text = 2
case gauge = 3
var id: Int { self.rawValue }
}
struct IndoorAirQuality: View {
var iaq: Int = 0
var displayMode: IaqDisplayMode = .pill
let gradient = Gradient(colors: [.green, .mint, .yellow, .orange, .red, .purple, .purple, .brown, .brown, .brown, .brown])
var body: some View {
let iaqEnum = Iaq.getIaq(for: iaq)
switch displayMode {
case .pill:
ZStack (alignment: .leading) {
RoundedRectangle(cornerRadius: 10)
.fill(iaqEnum.color)
.frame(width: 125, height: 30)
Label("IAQ \(iaq)", systemImage: iaq < 100 ? "aqi.low" : ((iaq > 100 && iaq < 201) ? "aqi.medium" : "aqi.high"))
.padding(.leading, 4)
}
case .dot:
VStack {
HStack {
Text("\(iaq)")
Circle()
.fill(iaqEnum.color)
.frame(width: 10, height: 10)
}
}
case .text:
Text(iaqEnum.description)
.font(.caption)
case .gauge:
Gauge(value: Double(iaq), in: 0...500) {
Text("IAQ")
.foregroundColor(iaqEnum.color)
} currentValueLabel: {
Text("\(Int(iaq))")
}
.tint(gradient)
.gaugeStyle(.accessoryCircular)
}
}
}
struct IndoorAirQuality_Previews: PreviewProvider {
static var previews: some View {
VStack {
Text(".pill")
.font(.title)
HStack {
VStack{
IndoorAirQuality(iaq: 6)
IndoorAirQuality(iaq: 51)
IndoorAirQuality(iaq: 101)
}
VStack {
IndoorAirQuality(iaq: 201)
IndoorAirQuality(iaq: 350)
IndoorAirQuality(iaq: 351)
}
}
Text(".dot")
.font(.title)
HStack {
VStack (alignment: .leading) {
IndoorAirQuality(iaq: 6, displayMode: .dot)
IndoorAirQuality(iaq: 51, displayMode: .dot)
IndoorAirQuality(iaq: 101, displayMode: .dot)
}
VStack (alignment: .leading) {
IndoorAirQuality(iaq: 201, displayMode: .dot)
IndoorAirQuality(iaq: 350, displayMode: .dot)
IndoorAirQuality(iaq: 351, displayMode: .dot)
}
}
Text(".text")
.font(.title)
IndoorAirQuality(iaq: 6, displayMode: .text)
IndoorAirQuality(iaq: 51, displayMode: .text)
IndoorAirQuality(iaq: 101, displayMode: .text)
IndoorAirQuality(iaq: 201, displayMode: .text)
IndoorAirQuality(iaq: 350, displayMode: .text)
IndoorAirQuality(iaq: 351, displayMode: .text)
Text(".gauge")
.font(.title)
HStack (alignment: .top) {
VStack{
IndoorAirQuality(iaq: 6, displayMode: .gauge)
IndoorAirQuality(iaq: 51, displayMode: .gauge)
IndoorAirQuality(iaq: 101, displayMode: .gauge)
IndoorAirQuality(iaq: 151, displayMode: .gauge)
}
VStack{
IndoorAirQuality(iaq: 201, displayMode: .gauge)
IndoorAirQuality(iaq: 251, displayMode: .gauge)
IndoorAirQuality(iaq: 301, displayMode: .gauge)
IndoorAirQuality(iaq: 350, displayMode: .gauge)
}
VStack{
IndoorAirQuality(iaq: 351, displayMode: .gauge)
IndoorAirQuality(iaq: 401, displayMode: .gauge)
IndoorAirQuality(iaq: 500, displayMode: .gauge)
}
}
}.previewLayout(.fixed(width: 300, height: 800))
}
}

View file

@ -0,0 +1,146 @@
//
// AQICircleDisplay.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 2/4/23.
//
import SwiftUI
enum AqiDisplayMode: Int, CaseIterable, Identifiable {
case pill = 0
case dot = 1
case text = 2
case gauge = 3
case gradient = 4
var id: Int { self.rawValue }
}
struct AirQualityIndex: View {
var aqi: Int
var displayMode: IaqDisplayMode = .pill
let gradient = Gradient(colors: [.green, .yellow, .orange, .red, .purple, .magenta])
var body: some View {
let aqiEnum = Aqi.getAqi(for: aqi)
switch displayMode {
case .pill:
ZStack (alignment: .leading) {
RoundedRectangle(cornerRadius: 10)
.fill(aqiEnum.color)
.frame(width: 125, height: 30)
Label("IAQ \(aqi)", systemImage: aqi < 100 ? "aqi.low" : ((aqi > 100 && aqi < 201) ? "aqi.medium" : "aqi.high"))
.padding(.leading, 4)
}
case .dot:
VStack {
HStack {
Text("\(aqi)")
Circle()
.fill(aqiEnum.color)
.frame(width: 10, height: 10)
}
}
case .text:
Text(aqiEnum.description)
.font(.caption)
case .gauge:
Gauge(value: Double(aqi), in: 0...500) {
Text("IAQ")
.foregroundColor(aqiEnum.color)
} currentValueLabel: {
Text("\(Int(aqi))")
}
.tint(gradient)
.gaugeStyle(.accessoryCircular)
case .gradient:
HStack {
Gauge(value: Double(aqi), in: 0...500) {
Text("IAQ")
.foregroundColor(aqiEnum.color)
} currentValueLabel: {
Text("IAQ ")+Text("\(Int(aqi))")
.foregroundColor(.gray)
}
.tint(gradient)
.gaugeStyle(.accessoryLinear)
Text(aqiEnum.description)
.font(.caption)
}
.padding([.leading, .trailing])
}
}
}
struct AirQualityIndex_Previews: PreviewProvider {
static var previews: some View {
VStack {
Text(".pill")
.font(.title2)
HStack {
AirQualityIndex(aqi: 6)
AirQualityIndex(aqi: 51)
}
HStack {
AirQualityIndex(aqi: 101)
AirQualityIndex(aqi: 151)
}
HStack {
AirQualityIndex(aqi: 201)
AirQualityIndex(aqi: 351)
}
Text(".dot")
.font(.title2)
HStack {
AirQualityIndex(aqi: 6, displayMode: .dot)
AirQualityIndex(aqi: 51, displayMode: .dot)
AirQualityIndex(aqi: 101, displayMode: .dot)
AirQualityIndex(aqi: 201, displayMode: .dot)
AirQualityIndex(aqi: 350, displayMode: .dot)
AirQualityIndex(aqi: 351, displayMode: .dot)
}
Text(".text")
.font(.title2)
HStack {
AirQualityIndex(aqi: 6, displayMode: .text)
AirQualityIndex(aqi: 51, displayMode: .text)
AirQualityIndex(aqi: 101, displayMode: .text)
}
HStack {
AirQualityIndex(aqi: 201, displayMode: .text)
AirQualityIndex(aqi: 350, displayMode: .text)
}
Text(".gauge")
.font(.title2)
HStack (alignment: .top) {
AirQualityIndex(aqi: 6, displayMode: .gauge)
AirQualityIndex(aqi: 51, displayMode: .gauge)
AirQualityIndex(aqi: 101, displayMode: .gauge)
AirQualityIndex(aqi: 151, displayMode: .gauge)
}
HStack (alignment: .top) {
AirQualityIndex(aqi: 201, displayMode: .gauge)
AirQualityIndex(aqi: 251, displayMode: .gauge)
AirQualityIndex(aqi: 301, displayMode: .gauge)
AirQualityIndex(aqi: 351, displayMode: .gauge)
}
HStack (alignment: .top) {
AirQualityIndex(aqi: 401, displayMode: .gauge)
AirQualityIndex(aqi: 500, displayMode: .gauge)
}
Text(".gradient")
.font(.title2)
AirQualityIndex(aqi: 6, displayMode: .gradient)
AirQualityIndex(aqi: 51, displayMode: .gradient)
AirQualityIndex(aqi: 101, displayMode: .gradient)
AirQualityIndex(aqi: 201, displayMode: .gradient)
AirQualityIndex(aqi: 351, displayMode: .gradient)
AirQualityIndex(aqi: 401, displayMode: .gradient)
AirQualityIndex(aqi: 500, displayMode: .gradient)
}.previewLayout(.fixed(width: 300, height: 800))
}
}

View file

@ -1,67 +0,0 @@
//
// AQICircleDisplay.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 2/4/23.
//
import SwiftUI
struct AirQualityIndexCompact: View {
var aqi: Int
var body: some View {
HStack(spacing: 0.5) {
Text("AQI \(aqi)")
.foregroundColor(.gray)
.padding(.trailing, 0)
.font(.caption)
if aqi > 0 && aqi < 51 {
// Good
Circle()
.fill(.green)
.frame(width: 10, height: 10)
} else if aqi > 50 && aqi < 101 {
// Satisfactory
Circle()
.fill(Color(red: 0, green: 0.9882, blue: 0.1804))
.frame(width: 10, height: 10)
} else if aqi > 100 && aqi < 201 {
// Moderate
Circle()
.fill(.yellow)
.frame(width: 10, height: 10)
} else if aqi > 200 && aqi < 301 {
// Poor
Circle()
.fill(.orange)
.frame(width: 10, height: 10)
} else if aqi > 300 && aqi < 401 {
// Very Poor
Circle()
.fill(.red)
.frame(width: 10, height: 10)
} else if aqi >= 401 {
// Very Poor
Circle()
.fill(Color(red: 0.8392, green: 0.0667, blue: 0))
.frame(width: 10, height: 10)
}
}
}
}
struct AQICircleDisplay_Previews: PreviewProvider {
static var previews: some View {
VStack {
AirQualityIndexCompact(aqi: 5)
AirQualityIndexCompact(aqi: 51)
AirQualityIndexCompact(aqi: 101)
AirQualityIndexCompact(aqi: 201)
AirQualityIndexCompact(aqi: 301)
AirQualityIndexCompact(aqi: 401)
}
}
}

View file

@ -0,0 +1,39 @@
//
// IAQScale.swift
// Meshtastic
//
// Created by Garth Vander Houwen on 4/24/24.
//
import SwiftUI
struct IAQScale: View {
var body: some View {
VStack(alignment:.leading) {
ForEach(Iaq.allCases) { iaq in
HStack {
RoundedRectangle(cornerRadius: 5)
.fill(iaq.color)
.frame(width: 30, height: 20)
Text(iaq.description)
.font(.callout)
}
}
}
.padding()
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
// .overlay(
// RoundedRectangle(cornerRadius: 20)
// .stroke(.secondary, lineWidth: 5)
// )
}
}
struct IAQSCalePreviews: PreviewProvider {
static var previews: some View {
VStack {
IAQScale()
}
}
}

View file

@ -0,0 +1,148 @@
//
// IndoorAirQuality.swift
// Meshtastic
//
// Copyright(c) by Garth Vander Houwen on 4/10/24.
//
import Foundation
import SwiftUI
enum IaqDisplayMode: Int, CaseIterable, Identifiable {
case pill = 0
case dot = 1
case text = 2
case gauge = 3
case gradient = 4
var id: Int { self.rawValue }
}
struct IndoorAirQuality: View {
var iaq: Int = 0
var displayMode: IaqDisplayMode = .pill
let gradient = Gradient(colors: [.green, .mint, .yellow, .orange, .red, .purple, .purple, .brown, .brown, .brown, .brown])
var body: some View {
let iaqEnum = Iaq.getIaq(for: iaq)
switch displayMode {
case .pill:
ZStack (alignment: .leading) {
RoundedRectangle(cornerRadius: 10)
.fill(iaqEnum.color)
.frame(width: 125, height: 30)
Label("IAQ \(iaq)", systemImage: iaq < 100 ? "aqi.low" : ((iaq > 100 && iaq < 201) ? "aqi.medium" : "aqi.high"))
.padding(.leading, 4)
}
case .dot:
VStack {
HStack {
Text("\(iaq)")
Circle()
.fill(iaqEnum.color)
.frame(width: 10, height: 10)
}
}
case .text:
Text(iaqEnum.description)
.font(.caption)
case .gauge:
Gauge(value: Double(iaq), in: 0...500) {
Text("IAQ")
.foregroundColor(iaqEnum.color)
} currentValueLabel: {
Text("\(Int(iaq))")
}
.tint(gradient)
.gaugeStyle(.accessoryCircular)
case .gradient:
HStack {
Gauge(value: Double(iaq), in: 0...500) {
Text("IAQ")
.foregroundColor(iaqEnum.color)
} currentValueLabel: {
Text("IAQ ")+Text("\(Int(iaq))")
.foregroundColor(.gray)
}
.tint(gradient)
.gaugeStyle(.accessoryLinear)
Text(iaqEnum.description)
.font(.caption)
}
.padding([.leading, .trailing])
}
}
}
struct IndoorAirQuality_Previews: PreviewProvider {
static var previews: some View {
VStack {
Text(".pill")
.font(.title2)
HStack {
IndoorAirQuality(iaq: 6)
IndoorAirQuality(iaq: 51)
}
HStack {
IndoorAirQuality(iaq: 101)
IndoorAirQuality(iaq: 201)
}
HStack {
IndoorAirQuality(iaq: 350)
IndoorAirQuality(iaq: 351)
}
Text(".dot")
.font(.title2)
HStack {
IndoorAirQuality(iaq: 6, displayMode: .dot)
IndoorAirQuality(iaq: 51, displayMode: .dot)
IndoorAirQuality(iaq: 101, displayMode: .dot)
IndoorAirQuality(iaq: 201, displayMode: .dot)
IndoorAirQuality(iaq: 350, displayMode: .dot)
IndoorAirQuality(iaq: 351, displayMode: .dot)
}
Text(".text")
.font(.title2)
HStack {
IndoorAirQuality(iaq: 6, displayMode: .text)
IndoorAirQuality(iaq: 51, displayMode: .text)
IndoorAirQuality(iaq: 101, displayMode: .text)
}
HStack {
IndoorAirQuality(iaq: 201, displayMode: .text)
IndoorAirQuality(iaq: 350, displayMode: .text)
IndoorAirQuality(iaq: 351, displayMode: .text)
}
Text(".gauge")
.font(.title2)
HStack (alignment: .top) {
IndoorAirQuality(iaq: 6, displayMode: .gauge)
IndoorAirQuality(iaq: 51, displayMode: .gauge)
IndoorAirQuality(iaq: 101, displayMode: .gauge)
IndoorAirQuality(iaq: 151, displayMode: .gauge)
}
HStack (alignment: .top) {
IndoorAirQuality(iaq: 201, displayMode: .gauge)
IndoorAirQuality(iaq: 251, displayMode: .gauge)
IndoorAirQuality(iaq: 301, displayMode: .gauge)
IndoorAirQuality(iaq: 351, displayMode: .gauge)
}
HStack (alignment: .top) {
IndoorAirQuality(iaq: 401, displayMode: .gauge)
IndoorAirQuality(iaq: 500, displayMode: .gauge)
}
Text(".gradient")
.font(.title2)
IndoorAirQuality(iaq: 6, displayMode: .gradient)
IndoorAirQuality(iaq: 51, displayMode: .gradient)
IndoorAirQuality(iaq: 101, displayMode: .gradient)
IndoorAirQuality(iaq: 201, displayMode: .gradient)
IndoorAirQuality(iaq: 351, displayMode: .gradient)
IndoorAirQuality(iaq: 401, displayMode: .gradient)
IndoorAirQuality(iaq: 500, displayMode: .gradient)
}.previewLayout(.fixed(width: 300, height: 800))
}
}

View file

@ -109,6 +109,24 @@ struct ChannelList: View {
Label("Delete Messages", systemImage: "trash")
}
}
Button {
channel.mute = !channel.mute
do {
let adminMessageId = bleManager.saveChannel(channel: channel.protoBuf, fromUser: node!.user!, toUser: node!.user!)
if adminMessageId > 0 {
context.refresh(channel, mergeChanges: true)
}
try context.save()
} catch {
context.rollback()
print("💥 Save Channel Mute Error")
}
} label: {
Label(channel.mute ? "Show Alerts" : "Hide Alerts", systemImage: channel.mute ? "bell" : "bell.slash")
}
}
.confirmationDialog(
"This conversation will be deleted.",

View file

@ -43,16 +43,24 @@ struct ChannelMessageList: View {
.padding(.trailing)
}
}
HStack(alignment: .top) {
HStack(alignment: .bottom) {
if currentUser { Spacer(minLength: 50) }
if !currentUser {
CircleText(text: message.fromUser?.shortName ?? "?", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 44)
.padding(.all, 5)
.offset(y: -5)
.offset(y: -7)
}
VStack(alignment: currentUser ? .trailing : .leading) {
let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue)
if !currentUser && message.fromUser != nil {
Text("\(message.fromUser?.longName ?? "unknown".localized ) (\(message.fromUser?.userId ?? "?"))")
.font(.caption)
.foregroundColor(.gray)
.offset(y: 8)
}
HStack {
MessageText(
message: message,
@ -67,7 +75,7 @@ struct ChannelMessageList: View {
RetryButton(message: message, destination: .channel(channel))
}
}
TapbackResponses(message: message) {
appState.unreadChannelMessages = myInfo.unreadMessages
UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages

View file

@ -25,6 +25,7 @@ struct DeviceConfig: View {
@State var rebroadcastMode = 0
@State var nodeInfoBroadcastSecs = 10800
@State var doubleTapAsButtonPress = false
@State var ledHeartbeatEnabled = true
@State var isManaged = false
@State var tzdef = ""
@ -58,6 +59,12 @@ struct DeviceConfig: View {
}
.pickerStyle(DefaultPickerStyle())
Toggle(isOn: $isManaged) {
Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath")
Text("Enabling Managed mode will restrict access to all radio configurations, such as short/long names, regions, channels, modules, etc. and will only be accessible through the Admin channel. To avoid being locked out, make sure the Admin channel is working properly before enabling it.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Picker("Node Info Broadcast Interval", selection: $nodeInfoBroadcastSecs ) {
ForEach(UpdateIntervals.allCases) { ui in
if ui.rawValue >= 3600 {
@ -66,15 +73,18 @@ struct DeviceConfig: View {
}
}
.pickerStyle(DefaultPickerStyle())
}
Section(header: Text("Hardware")) {
Toggle(isOn: $doubleTapAsButtonPress) {
Label("Double Tap as Button", systemImage: "hand.tap")
Text("Treat double tap on supported accelerometers as a user button press.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Toggle(isOn: $isManaged) {
Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath")
Text("Enabling Managed mode will restrict access to all radio configurations, such as short/long names, regions, channels, modules, etc. and will only be accessible through the Admin channel. To avoid being locked out, make sure the Admin channel is working properly before enabling it.")
Toggle(isOn: $ledHeartbeatEnabled) {
Label("LED Heartbeat", systemImage: "waveform.path.ecg")
Text("Controls the blinking LED on the device. For most devices this will control one of the up to 4 LEDS, the charger and GPS LEDs are not controllable.")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
@ -202,6 +212,7 @@ struct DeviceConfig: View {
dc.doubleTapAsButtonPress = doubleTapAsButtonPress
dc.isManaged = isManaged
dc.tzdef = tzdef
dc.ledHeartbeatDisabled = !ledHeartbeatEnabled
if isManaged {
serialEnabled = false
debugLogEnabled = false
@ -300,6 +311,7 @@ struct DeviceConfig: View {
nodeInfoBroadcastSecs = 3600
}
self.doubleTapAsButtonPress = node?.deviceConfig?.doubleTapAsButtonPress ?? false
self.ledHeartbeatEnabled = node?.deviceConfig?.ledHeartbeatEnabled ?? true
self.isManaged = node?.deviceConfig?.isManaged ?? false
if self.tzdef.isEmpty {
self.tzdef = TimeZone.current.posixDescription

View file

@ -398,21 +398,20 @@ struct MQTTConfig: View {
if !stateTopic.isEmpty {
nearbyTopics.append(stateTopic)
}
let countyTopic = defaultTopic + "/" + (placemark.subAdministrativeArea?.lowercased().replacingOccurrences(of: " ", with: "") ?? "")
let countyTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subAdministrativeArea?.lowercased().replacingOccurrences(of: " ", with: "") ?? "")
if !countyTopic.isEmpty {
nearbyTopics.append(countyTopic)
}
let cityTopic = defaultTopic + "/" + (placemark.locality?.lowercased().replacingOccurrences(of: " ", with: "") ?? "")
let cityTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.locality?.lowercased().replacingOccurrences(of: " ", with: "") ?? "")
if !cityTopic.isEmpty {
nearbyTopics.append(cityTopic)
}
let neightborhoodTopic = defaultTopic + "/" + (placemark.subLocality?.lowercased()
let neightborhoodTopic = defaultTopic + "/" + (placemark.administrativeArea ?? "") + "/" + (placemark.subLocality?.lowercased()
.replacingOccurrences(of: " ", with: "")
.replacingOccurrences(of: "'", with: "") ?? "")
if !neightborhoodTopic.isEmpty {
nearbyTopics.append(neightborhoodTopic)
}
}
else
{

View file

@ -12,7 +12,7 @@ struct Firmware: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
var node: NodeInfoEntity?
@State var minimumVersion = "2.3.5"
@State var minimumVersion = "2.3.7"
@State var version = ""
@State private var currentDevice: DeviceHardware?
@State private var latestStable: FirmwareRelease?

View file

@ -6,6 +6,16 @@
<string>Root</string>
<key>PreferenceSpecifiers</key>
<array>
<dict>
<key>Type</key>
<string>PSTitleValueSpecifier</string>
<key>DefaultValue</key>
<string></string>
<key>Title</key>
<string>Will share your phone GPS location with your node.</string>
<key>Key</key>
<string>shareLocationTitle</string>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
@ -20,7 +30,7 @@
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>Share Location Interval</string>
<string>Interval</string>
<key>Key</key>
<string>provideLocationInterval</string>
<key>Values</key>
@ -78,9 +88,9 @@
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Low Battery</string>
<string>Channel Messages</string>
<key>Key</key>
<string>lowBatteryNotifications</string>
<string>channelMessageNotifications</string>
<key>DefaultValue</key>
<true/>
</dict>
@ -94,6 +104,16 @@
<key>DefaultValue</key>
<true/>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Low Battery</string>
<key>Key</key>
<string>lowBatteryNotifications</string>
<key>DefaultValue</key>
<true/>
</dict>
</array>
</dict>
</plist>

@ -1 +0,0 @@
Subproject commit 86640f20db7b9b5be42949d18e8d96ad10d47a68