Merge pull request #359 from meshtastic/offline_maps_updates

Crashfixes, weather radar overlays
This commit is contained in:
Garth Vander Houwen 2023-05-14 09:41:40 -07:00 committed by GitHub
commit 3c38280328
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 933 additions and 374 deletions

View file

@ -103,7 +103,6 @@
DDB75A142A0593E2006ED576 /* OfflineTileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */; };
DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A152A0594AD006ED576 /* TileOverlay.swift */; };
DDB75A1A2A05EB67006ED576 /* alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = DDB75A192A05EB67006ED576 /* alpha.png */; };
DDB75A1C2A076DFA006ED576 /* TilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A1B2A076DFA006ED576 /* TilesView.swift */; };
DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */; };
DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */; };
DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */; };
@ -293,8 +292,8 @@
DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineTileManager.swift; sourceTree = "<group>"; };
DDB75A152A0594AD006ED576 /* TileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileOverlay.swift; sourceTree = "<group>"; };
DDB75A192A05EB67006ED576 /* alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = alpha.png; sourceTree = "<group>"; };
DDB75A1B2A076DFA006ED576 /* TilesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TilesView.swift; sourceTree = "<group>"; };
DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaSignalStrengthIndicator.swift; sourceTree = "<group>"; };
DDB75A1F2A10766D006ED576 /* MeshtasticDataModelV13.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV13.xcdatamodel; sourceTree = "<group>"; };
DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV8.xcdatamodel; sourceTree = "<group>"; };
DDC2E15426CE248E0042C5E4 /* Meshtastic.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Meshtastic.app; sourceTree = BUILT_PRODUCTS_DIR; };
DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticApp.swift; sourceTree = "<group>"; };
@ -403,7 +402,6 @@
DD964FC32974767D007C176F /* MapViewFitExtension.swift */,
DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */,
DDDB443529F6287000EE2349 /* MapButtons.swift */,
DDB75A1B2A076DFA006ED576 /* TilesView.swift */,
);
path = Custom;
sourceTree = "<group>";
@ -1010,7 +1008,6 @@
DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */,
DDDB444229F8A88700EE2349 /* Double.swift in Sources */,
DD5E520F298EE33B00D21B61 /* cannedmessages.pb.swift in Sources */,
DDB75A1C2A076DFA006ED576 /* TilesView.swift in Sources */,
DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */,
DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */,
DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */,
@ -1301,7 +1298,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.1.9;
MARKETING_VERSION = 2.1.10;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1335,7 +1332,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.1.9;
MARKETING_VERSION = 2.1.10;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1454,7 +1451,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.1.9;
MARKETING_VERSION = 2.1.10;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -1485,7 +1482,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.1.9;
MARKETING_VERSION = 2.1.10;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
@ -1582,6 +1579,7 @@
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
DDB75A1F2A10766D006ED576 /* MeshtasticDataModelV13.xcdatamodel */,
DDB759E12A04B264006ED576 /* MeshtasticDataModelV12.xcdatamodel */,
DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */,
DDC94FC329CED7280082EA6E /* MeshtasticDataModelV10.xcdatamodel */,
@ -1595,7 +1593,7 @@
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
);
currentVersion = DDB759E12A04B264006ED576 /* MeshtasticDataModelV12.xcdatamodel */;
currentVersion = DDB75A1F2A10766D006ED576 /* MeshtasticDataModelV13.xcdatamodel */;
name = Meshtastic.xcdatamodeld;
path = Meshtastic/Meshtastic.xcdatamodeld;
sourceTree = "<group>";

View file

@ -137,7 +137,7 @@ enum MapLayer: String, CaseIterable, Equatable {
var localized: String { self.rawValue.localized }
}
enum MapTileServerLinks: String, CaseIterable, Identifiable {
enum MapTileServer: String, CaseIterable, Identifiable {
case openStreetMap
case openStreetMapDE
@ -254,11 +254,11 @@ enum MapTileServerLinks: String, CaseIterable, Identifiable {
case .openStreetMapHot:
return [Int](0...18)
case .usgsTopo:
return [Int](6...15)
return [Int](6...16)
case .usgsImageryTopo:
return [Int](6...15)
return [Int](6...16)
case .usgsImageryOnly:
return [Int](6...15)
return [Int](6...16)
case .terrain:
return [Int](0...15)
case .toner:
@ -268,3 +268,87 @@ enum MapTileServerLinks: String, CaseIterable, Identifiable {
}
}
}
enum MapOverlayServer: String, CaseIterable, Identifiable {
case baseReReflectivityCurrent
case baseReReflectivityOneHourAgo
case echoTopsEetCurrent
case echoTopsEetOneHourAgo
case q2OneHourPrecipitation
case q2TwentyFourHourPrecipitation
case q2FortyEightHourPrecipitation
case q2SeventyTwoHourPrecipitation
case mrmsHybridScanReflectivityComposite
var id: String { self.rawValue }
var attribution: String {
return "NEXRAD Weather tiles from Iowa State University Environmental Mesonet [OGC Web Services](https://mesonet.agron.iastate.edu/ogc/)."
}
var description: String {
switch self {
case .baseReReflectivityCurrent:
return "Base Reflectivity current"
case .baseReReflectivityOneHourAgo:
return "Base Reflectivity one hour ago"
case .echoTopsEetCurrent:
return "Echo Tops EET current"
case .echoTopsEetOneHourAgo:
return "Echo Tops EET one hour ago"
case .q2OneHourPrecipitation:
return "Q2 1 Hour Precipitation"
case .q2TwentyFourHourPrecipitation:
return "Q2 24 Hour Precipitation"
case .q2FortyEightHourPrecipitation:
return "Q2 48 Hour Precipitation"
case .q2SeventyTwoHourPrecipitation:
return "Q2 72 Hour Precipitation"
case .mrmsHybridScanReflectivityComposite:
return "MRMS Hybrid-Scan Reflectivity Composite"
}
}
var tileUrl: String {
switch self {
case .baseReReflectivityCurrent:
return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-n0q-900913/{z}/{x}/{y}"
case .baseReReflectivityOneHourAgo:
return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-n0q-900913-m55m/{z}/{x}/{y}"
case .echoTopsEetCurrent:
return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-eet-900913/{z}/{x}/{y}"
case .echoTopsEetOneHourAgo:
return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-eet-900913-m55m/{z}/{x}/{y}"
case .q2OneHourPrecipitation:
return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/q2-n1p-900913/{z}/{x}/{y}"
case .q2TwentyFourHourPrecipitation:
return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/q2-p24h-900913/{z}/{x}/{y}"
case .q2FortyEightHourPrecipitation:
return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/q2-p48h-900913/{z}/{x}/{y}"
case .q2SeventyTwoHourPrecipitation:
return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/q2-p72h-900913/{z}/{x}/{y}"
case .mrmsHybridScanReflectivityComposite:
return "https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/q2-hsr-900913/{z}/{x}/{y}"
}
}
var zoomRange: [Int] {
switch self {
case .baseReReflectivityCurrent:
return [Int](0...18)
case .baseReReflectivityOneHourAgo:
return [Int](0...18)
case .echoTopsEetCurrent:
return [Int](0...18)
case .echoTopsEetOneHourAgo:
return [Int](0...18)
case .q2OneHourPrecipitation:
return [Int](0...18)
case .q2TwentyFourHourPrecipitation:
return [Int](0...18)
case .q2FortyEightHourPrecipitation:
return [Int](0...18)
case .q2SeventyTwoHourPrecipitation:
return [Int](0...18)
case .mrmsHybridScanReflectivityComposite:
return [Int](0...18)
}
}
}

View file

@ -135,6 +135,26 @@ enum ModemPresets: Int, CaseIterable, Identifiable {
return "Short Range - Fast"
}
}
func snrLimit() -> Float {
switch self {
case .longFast:
return -17.5
case .longSlow:
return -7.5
case .longModerate:
return -17.5
case .vLongSlow:
return -20
case .medSlow:
return -15
case .medFast:
return -12.5
case .shortSlow:
return -10
case .shortFast:
return -7.5
}
}
func protoEnumValue() -> Config.LoRaConfig.ModemPreset {
switch self {
case .longFast:

View file

@ -38,7 +38,11 @@ extension String {
return false
} else {
let characters = Array(self)
return characters[0].isEmoji
if characters.count <= 0 {
return false
} else {
return characters[0].isEmoji
}
}
}

View file

@ -115,16 +115,35 @@ extension UserDefaults {
}
}
static var mapTileServer: MapTileServerLinks {
static var mapTileServer: MapTileServer {
get {
MapTileServerLinks(rawValue: UserDefaults.standard.string(forKey: "mapTileServer") ?? MapTileServerLinks.openStreetMap.rawValue) ?? MapTileServerLinks.openStreetMap
MapTileServer(rawValue: UserDefaults.standard.string(forKey: "mapTileServer") ?? MapTileServer.openStreetMap.rawValue) ?? MapTileServer.openStreetMap
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: "mapTileServer")
}
}
static var enableOverlayServer: Bool {
get {
UserDefaults.standard.bool(forKey: "enableOverlayServer")
}
set {
UserDefaults.standard.set(newValue, forKey: "enableOverlayServer")
}
}
static var mapOverlayServer: MapOverlayServer {
get {
MapOverlayServer(rawValue: UserDefaults.standard.string(forKey: "mapOverlayServer") ?? MapOverlayServer.baseReReflectivityCurrent.rawValue) ?? MapOverlayServer.baseReReflectivityCurrent
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: "mapOverlayServer")
}
}
static var mapTilesAboveLabels: Bool {
get {
UserDefaults.standard.bool(forKey: "mapTilesAboveLabels")

View file

@ -784,12 +784,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
positionPacket.timestamp = UInt32(LocationHelper.currentTimestamp.timeIntervalSince1970)
positionPacket.altitude = Int32(LocationHelper.currentAltitude)
positionPacket.satsInView = UInt32(LocationHelper.satsInView)
if LocationHelper.currentSpeed >= 0 {
if !LocationHelper.currentSpeed.isNaN || !LocationHelper.currentSpeed.isInfinite {
positionPacket.groundSpeed = UInt32(LocationHelper.currentSpeed * 3.6)
}
if LocationHelper.currentHeading >= 0 {
if (!LocationHelper.currentHeading.isNaN || !LocationHelper.currentHeading.isInfinite) {
positionPacket.groundTrack = UInt32(LocationHelper.currentHeading)
}
var meshPacket = MeshPacket()
meshPacket.to = UInt32(destNum)
meshPacket.from = UInt32(fromNodeNum)

View file

@ -22,7 +22,7 @@ class OfflineTileManager: ObservableObject {
}
// MARK: - Private properties
private var overlay: MKTileOverlay { MKTileOverlay(urlTemplate: UserDefaults.mapTileServer.tileUrl.count > 1 ? UserDefaults.mapTileServer.tileUrl : MapTileServerLinks.openStreetMap.tileUrl) }
private var overlay: MKTileOverlay { MKTileOverlay(urlTemplate: UserDefaults.mapTileServer.tileUrl.count > 1 ? UserDefaults.mapTileServer.tileUrl : MapTileServer.openStreetMap.tileUrl) }
private var documentsDirectory: URL { fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! }
@ -48,6 +48,23 @@ class OfflineTileManager: ObservableObject {
return Double(count) * size
}
func getDownloadedSize(for mapTileLink: MapTileServer) -> Double {
var accumulatedSize: UInt64 = 0
let mapTiles = try! fileManager.contentsOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"), includingPropertiesForKeys: [])
let matchingTiles = mapTiles.filter { fileName in
let fileNameLower = fileName.absoluteString
return fileNameLower.contains(mapTileLink.id)
}
print("Deleting \(matchingTiles.count) tiles for \(mapTileLink.id)")
for tile in matchingTiles {
let url = documentsDirectory.appendingPathComponent(tile.absoluteString)
accumulatedSize += (try? url.regularFileAllocatedSize()) ?? 0
}
return Double(accumulatedSize)
}
func getDownloadedSize(for boundingBox: MKMapRect) -> Double {
let paths = self.computeTileOverlayPaths(boundingBox: boundingBox)
var accumulatedSize: UInt64 = 0
@ -64,6 +81,19 @@ class OfflineTileManager: ObservableObject {
createDirectoriesIfNecessary()
}
func remove(for mapTileLink: MapTileServer) {
let mapTiles = try! fileManager.contentsOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"), includingPropertiesForKeys: [])
let matchingTiles = mapTiles.filter { fileName in
let fileNameLower = fileName.absoluteString
return fileNameLower.contains(mapTileLink.id)
}
print("Deleting \(matchingTiles.count) tiles for \(mapTileLink.id)")
for tile in matchingTiles {
try? fileManager.removeItem(at: tile.absoluteURL)
}
}
func remove(for boundingBox: MKMapRect) {
let paths = self.computeTileOverlayPaths(boundingBox: boundingBox)
for path in paths {

View file

@ -18,6 +18,7 @@ class NetworkManager {
pathMonitor.pathUpdateHandler = {
guard $0.status == .satisfied else {
// No network available
print("Network Not available")
return pathMonitor.cancel()
}
pathMonitor.cancel()

View file

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

View file

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

View file

@ -50,6 +50,7 @@ extension PositionEntity {
var annotaton: MKPointAnnotation {
let pointAnn = MKPointAnnotation()
if nodeCoordinate != nil {
pointAnn.coordinate = nodeCoordinate!
}

View file

@ -325,6 +325,7 @@ func upsertDeviceConfigPacket(config: Meshtastic.Config.DeviceConfig, nodeNum: I
newDeviceConfig.rebroadcastMode = Int32(config.rebroadcastMode.rawValue)
newDeviceConfig.nodeInfoBroadcastSecs = Int32(config.nodeInfoBroadcastSecs)
newDeviceConfig.doubleTapAsButtonPress = config.doubleTapAsButtonPress
newDeviceConfig.isManaged = config.isManaged
fetchedNode[0].deviceConfig = newDeviceConfig
} else {
fetchedNode[0].deviceConfig?.role = Int32(config.role.rawValue)
@ -333,8 +334,9 @@ func upsertDeviceConfigPacket(config: Meshtastic.Config.DeviceConfig, nodeNum: I
fetchedNode[0].deviceConfig?.buttonGpio = Int32(config.buttonGpio)
fetchedNode[0].deviceConfig?.buzzerGpio = Int32(config.buzzerGpio)
fetchedNode[0].deviceConfig?.rebroadcastMode = Int32(config.rebroadcastMode.rawValue)
fetchedNode[0].deviceConfig?.doubleTapAsButtonPress = config.doubleTapAsButtonPress
fetchedNode[0].deviceConfig?.nodeInfoBroadcastSecs = Int32(config.nodeInfoBroadcastSecs)
fetchedNode[0].deviceConfig?.doubleTapAsButtonPress = config.doubleTapAsButtonPress
fetchedNode[0].deviceConfig?.isManaged = config.isManaged
}
do {
try context.save()

View file

@ -181,6 +181,11 @@ struct Config {
/// Treat double tap interrupt on supported accelerometers as a button press if set to true
var doubleTapAsButtonPress: Bool = false
///
/// If true, device is considered to be "managed" by a mesh administrator
/// Clients should then limit available configuration and administrative options inside the user interface
var isManaged: Bool = false
var unknownFields = SwiftProtobuf.UnknownStorage()
///
@ -1592,6 +1597,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
6: .standard(proto: "rebroadcast_mode"),
7: .standard(proto: "node_info_broadcast_secs"),
8: .standard(proto: "double_tap_as_button_press"),
9: .standard(proto: "is_managed"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -1608,6 +1614,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
case 6: try { try decoder.decodeSingularEnumField(value: &self.rebroadcastMode) }()
case 7: try { try decoder.decodeSingularUInt32Field(value: &self.nodeInfoBroadcastSecs) }()
case 8: try { try decoder.decodeSingularBoolField(value: &self.doubleTapAsButtonPress) }()
case 9: try { try decoder.decodeSingularBoolField(value: &self.isManaged) }()
default: break
}
}
@ -1638,6 +1645,9 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
if self.doubleTapAsButtonPress != false {
try visitor.visitSingularBoolField(value: self.doubleTapAsButtonPress, fieldNumber: 8)
}
if self.isManaged != false {
try visitor.visitSingularBoolField(value: self.isManaged, fieldNumber: 9)
}
try unknownFields.traverse(visitor: &visitor)
}
@ -1650,6 +1660,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
if lhs.rebroadcastMode != rhs.rebroadcastMode {return false}
if lhs.nodeInfoBroadcastSecs != rhs.nodeInfoBroadcastSecs {return false}
if lhs.doubleTapAsButtonPress != rhs.doubleTapAsButtonPress {return false}
if lhs.isManaged != rhs.isManaged {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}

View file

@ -19,7 +19,8 @@ struct MapViewSwiftUI: UIViewRepresentable {
let mapView = MKMapView()
// Parameters
var selectedMapLayer: MapLayer
let selectedMapLayer: MapLayer
let selectedWeatherLayer: MapOverlayServer = UserDefaults.mapOverlayServer
let positions: [PositionEntity]
let waypoints: [WaypointEntity]
@ -116,6 +117,17 @@ struct MapViewSwiftUI: UIViewRepresentable {
default:
mapView.mapType = .standard
}
// Weather radar
if UserDefaults.enableOverlayServer {
let locale = Locale.current
if locale.region?.identifier ?? "no locale" == "US" {
let overlay = MKTileOverlay(urlTemplate: selectedWeatherLayer.tileUrl)
overlay.canReplaceMapContent = false
overlay.minimumZ = selectedWeatherLayer.zoomRange.startIndex
overlay.maximumZ = selectedWeatherLayer.zoomRange.endIndex
mapView.addOverlay(overlay, level: .aboveLabels)
}
}
}
func makeUIView(context: Context) -> MKMapView {

View file

@ -1,49 +0,0 @@
//
// TilesView.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen on 5/6/23.
//
import SwiftUI
import MapKit
struct TilesView: View {
@ObservedObject var tileManager = OfflineTileManager.shared
@State var totalDownloadedTileSize = ""
var body: some View {
Button(action: {
tileManager.removeAll()
totalDownloadedTileSize = tileManager.getAllDownloadedSize()
print("delete all tiles")
}) {
HStack {
Image(systemName: "trash")
.font(.callout)
.foregroundColor(.red)
Text("\("map.tiles.delete".localized) (\(totalDownloadedTileSize))")
.font(.callout)
.foregroundColor(.red)
Spacer()
}
}
.onAppear(perform: {
totalDownloadedTileSize = tileManager.getAllDownloadedSize()
})
}
}
// MARK: Previews
struct TilesView_Previews: PreviewProvider {
static var previews: some View {
TilesView()
.previewLayout(.fixed(width: 300, height: 80))
.environment(\.colorScheme, .light)
}
}

View file

@ -167,7 +167,7 @@ struct WaypointFormView: View {
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.controlSize(.regular)
.disabled(bleManager.connectedPeripheral == nil)
.padding(.bottom)
@ -178,7 +178,7 @@ struct WaypointFormView: View {
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.controlSize(.regular)
.padding(.bottom)
if coordinate.waypointId > 0 {
@ -230,7 +230,7 @@ struct WaypointFormView: View {
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.controlSize(.regular)
.padding(.bottom)
}
}

View file

@ -11,62 +11,64 @@ import CoreLocation
import CoreData
struct NodeMap: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@ObservedObject var tileManager = OfflineTileManager.shared
@State var selectedMapLayer: MapLayer = UserDefaults.mapLayer
@State var enableMapRecentering: Bool = UserDefaults.enableMapRecentering
@State var enableMapRouteLines: Bool = UserDefaults.enableMapRouteLines
@State var enableMapNodeHistoryPins: Bool = UserDefaults.enableMapNodeHistoryPins
@State var enableOfflineMaps: Bool = UserDefaults.enableOfflineMaps
@State var selectedTileServer: MapTileServerLinks = UserDefaults.mapTileServer
@State var selectedTileServer: MapTileServer = UserDefaults.mapTileServer
@State var enableOfflineMapsMBTiles: Bool = UserDefaults.enableOfflineMapsMBTiles
@State var enableOverlayServer: Bool = UserDefaults.enableOverlayServer
@State var selectedOverlayServer: MapOverlayServer = UserDefaults.mapOverlayServer
@State var mapTilesAboveLabels: Bool = UserDefaults.mapTilesAboveLabels
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)],
predicate: NSPredicate(format: "time >= %@ && nodePosition != nil", Calendar.current.startOfDay(for: Date()) as NSDate), animation: .none)
private var positions: FetchedResults<PositionEntity>
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
predicate: NSPredicate(
format: "expire == nil || expire >= %@", Date() as NSDate
), animation: .none)
private var waypoints: FetchedResults<WaypointEntity>
@State var waypointCoordinate: WaypointCoordinate?
@State var selectedTracking: UserTrackingModes = .none
@State var isPresentingInfoSheet: Bool = false
@State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay(
mapName: "offlinemap",
tileType: "png",
canReplaceMapContent: true
)
mapName: "offlinemap",
tileType: "png",
canReplaceMapContent: true
)
var body: some View {
NavigationStack {
ZStack {
MapViewSwiftUI(
onLongPress: { coord in
onLongPress: { coord in
waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0)
}, onWaypointEdit: { wpId in
if wpId > 0 {
waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId))
}
},
selectedMapLayer: selectedMapLayer,
positions: Array(positions),
waypoints: Array(waypoints),
userTrackingMode: selectedTracking.MKUserTrackingModeValue(),
showNodeHistory: enableMapNodeHistoryPins,
showRouteLines: enableMapRouteLines,
customMapOverlay: self.customMapOverlay
selectedMapLayer: selectedMapLayer,
positions: Array(positions),
waypoints: Array(waypoints),
userTrackingMode: selectedTracking.MKUserTrackingModeValue(),
showNodeHistory: enableMapNodeHistoryPins,
showRouteLines: enableMapRouteLines,
customMapOverlay: self.customMapOverlay
)
VStack(alignment: .trailing) {
@ -84,9 +86,9 @@ struct NodeMap: View {
.ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
.frame(maxHeight: .infinity)
.sheet(item: $waypointCoordinate, content: { wpc in
WaypointFormView(coordinate: wpc)
.presentationDetents([.medium, .large])
.presentationDragIndicator(.automatic)
WaypointFormView(coordinate: wpc)
.presentationDetents([.medium, .large])
.presentationDragIndicator(.automatic)
})
.sheet(isPresented: $isPresentingInfoSheet) {
VStack {
@ -137,16 +139,47 @@ struct NodeMap: View {
self.enableMapRouteLines.toggle()
UserDefaults.enableMapRouteLines = self.enableMapRouteLines
}
let locale = Locale.current
if locale.region?.identifier ?? "no locale" == "US" {
Toggle(isOn: $enableOverlayServer) {
Label("Show Weather", systemImage: "cloud.fill")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.onTapGesture {
self.enableOverlayServer.toggle()
UserDefaults.enableOverlayServer = self.enableOverlayServer
}
if enableOverlayServer {
Picker(selection: $selectedOverlayServer,
label: Text("Radar")) {
ForEach(MapOverlayServer.allCases, id: \.self) { mos in
Text(mos.description)
.font(.footnote)
}
}
.pickerStyle(DefaultPickerStyle())
.onChange(of: (selectedOverlayServer)) { newSelectedOverlayServer in
UserDefaults.mapOverlayServer = newSelectedOverlayServer
}
Text(LocalizedStringKey(selectedOverlayServer.attribution))
.font(.footnote)
.foregroundColor(.gray)
.padding(0)
}
}
}
Section(header: Text("Offline Maps")) {
Toggle(isOn: $enableOfflineMaps) {
Text("Enable Offline Maps")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
.onTapGesture {
self.enableOfflineMaps.toggle()
UserDefaults.enableOfflineMaps = self.enableOfflineMaps
if !self.enableOfflineMaps {
.onChange(of: (enableOfflineMaps)) { newEnableOfflineMaps in
UserDefaults.enableOfflineMaps = newEnableOfflineMaps
if !newEnableOfflineMaps {
if self.selectedMapLayer == .offline {
self.selectedMapLayer = .standard
}
@ -159,15 +192,14 @@ struct NodeMap: View {
Picker(selection: $selectedTileServer,
label: Text("Tile Server")) {
ForEach(MapTileServerLinks.allCases, id: \.self) { tsl in
ForEach(MapTileServer.allCases, id: \.self) { tsl in
Text(tsl.description)
}
}
.pickerStyle(DefaultPickerStyle())
.onChange(of: (selectedTileServer)) { newSelectedTileServer in
UserDefaults.mapTileServer = newSelectedTileServer
selectedMapLayer = .standard
}
.pickerStyle(DefaultPickerStyle())
.onChange(of: (selectedTileServer)) { newSelectedTileServer in
UserDefaults.mapTileServer = newSelectedTileServer
}
Text("Attribution:")
.fontWeight(.semibold)
.font(.footnote)
@ -214,7 +246,7 @@ struct NodeMap: View {
.padding(.bottom)
#endif
}
.presentationDetents([UserDefaults.enableOfflineMaps ? .large : .medium])
.presentationDetents([UserDefaults.enableOfflineMaps || UserDefaults.enableOverlayServer ? .large : .medium])
.presentationDragIndicator(.visible)
}
}

View file

@ -8,11 +8,14 @@ struct AppSettings: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@ObservedObject var tileManager = OfflineTileManager.shared
@State var totalDownloadedTileSize = ""
@StateObject var locationHelper = LocationHelper()
@State var meshtasticUsername: String = UserDefaults.meshtasticUsername
@State var provideLocation: Bool = UserDefaults.provideLocation
@State var provideLocationInterval: Int = UserDefaults.provideLocationInterval
@State private var isPresentingCoreDataResetConfirm = false
@State private var isPresentingDeleteMapTilesConfirm = false
var body: some View {
VStack {
@ -40,8 +43,8 @@ struct AppSettings: View {
.font(.footnote)
}
Label("Coordinate \(String(format: "%.5f", locationHelper.locationManager.location?.coordinate.latitude ?? 0)), \(String(format: "%.5f", locationHelper.locationManager.location?.coordinate.longitude ?? 0))", systemImage: "mappin")
.font(.footnote)
.textSelection(.enabled)
.font(.footnote)
.textSelection(.enabled)
if locationHelper.locationManager.location?.verticalAccuracy ?? 0 > 0 {
Label("Altitude \(altitiude.formatted())", systemImage: "mountain.2")
.font(.footnote)
@ -55,56 +58,90 @@ struct AppSettings: View {
.font(.footnote)
}
}
Section(header: Text("Location Settings")) {
Toggle(isOn: $provideLocation) {
Label("provide.location", systemImage: "location.circle.fill")
.font(.footnote)
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
if UserDefaults.provideLocation {
Picker("update.interval", selection: $provideLocationInterval) {
ForEach(LocationUpdateInterval.allCases) { lu in
Text(lu.description)
VStack {
Picker("update.interval", selection: $provideLocationInterval) {
ForEach(LocationUpdateInterval.allCases) { lu in
Text(lu.description)
}
}
.pickerStyle(DefaultPickerStyle())
.onChange(of: (provideLocationInterval)) { newProvideLocationInterval in
UserDefaults.provideLocationInterval = newProvideLocationInterval
}
Text("phone.gps.interval.description")
.font(.caption2)
.foregroundColor(.gray)
}
}
}
Section(header: Text("App Data")) {
Button {
isPresentingCoreDataResetConfirm = true
} label: {
Label("clear.app.data", systemImage: "trash")
.foregroundColor(.red)
}
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingCoreDataResetConfirm,
titleVisibility: .visible
) {
Button("Erase all app data?", role: .destructive) {
bleManager.disconnectPeripheral()
clearCoreDataDatabase(context: context)
UserDefaults.standard.reset()
UserDefaults.standard.synchronize()
}
}
}
if totalDownloadedTileSize != "0MB" {
Section(header: Text("Map Tile Data")) {
Button {
isPresentingDeleteMapTilesConfirm = true
} label: {
Label("\("map.tiles.delete".localized) (\(totalDownloadedTileSize))", systemImage: "trash")
.foregroundColor(.red)
}
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingDeleteMapTilesConfirm,
titleVisibility: .visible
) {
Button("Delete all map tiles?", role: .destructive) {
tileManager.removeAll()
totalDownloadedTileSize = tileManager.getAllDownloadedSize()
print("delete all tiles")
}
}
.pickerStyle(DefaultPickerStyle())
.onChange(of: (provideLocationInterval)) { newProvideLocationInterval in
UserDefaults.provideLocationInterval = newProvideLocationInterval
}
Text("phone.gps.interval.description")
.font(.caption2)
.foregroundColor(.gray)
}
}
TilesView()
}
HStack {
Button {
isPresentingCoreDataResetConfirm = true
} label: {
Label("clear.app.data", systemImage: "trash")
.foregroundColor(.red)
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
.confirmationDialog(
"are.you.sure",
isPresented: $isPresentingCoreDataResetConfirm,
titleVisibility: .visible
) {
Button("Erase all app data?", role: .destructive) {
bleManager.disconnectPeripheral()
clearCoreDataDatabase(context: context)
UserDefaults.standard.reset()
UserDefaults.standard.synchronize()
ForEach(MapTileServer.allCases, id: \.self) { tsl in
Button {
tileManager.remove(for: tsl)
totalDownloadedTileSize = tileManager.getAllDownloadedSize()
} label: {
Label("Delete \(tsl.description) Tiles", systemImage: "trash")
.foregroundColor(.red)
.font(.footnote)
}
}
}
}
}
.onAppear(perform: {
totalDownloadedTileSize = tileManager.getAllDownloadedSize()
})
}
.navigationTitle("app.settings")
.navigationBarItems(trailing:

View file

@ -26,6 +26,7 @@ struct DeviceConfig: View {
@State var debugLogEnabled = false
@State var rebroadcastMode = 0
@State var doubleTapAsButtonPress = false
@State var isManaged = false
var body: some View {
@ -88,6 +89,13 @@ struct DeviceConfig: View {
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Text("Treat double tap on supported accelerometers as a user button press.")
.font(.caption)
Toggle(isOn: $isManaged) {
Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
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.")
.font(.caption)
}
Section(header: Text("Debug")) {
@ -217,6 +225,7 @@ struct DeviceConfig: View {
dc.buzzerGpio = UInt32(buzzerGPIO)
dc.rebroadcastMode = RebroadcastModes(rawValue: rebroadcastMode)?.protoEnumValue() ?? RebroadcastModes.all.protoEnumValue()
dc.doubleTapAsButtonPress = doubleTapAsButtonPress
dc.isManaged = isManaged
let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
if adminMessageId > 0 {
@ -247,7 +256,7 @@ struct DeviceConfig: View {
if bleManager.connectedPeripheral != nil && node?.deviceConfig == nil {
print("empty device config")
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
if node != nil && connectedNode != nil {
if node != nil && connectedNode != nil && connectedNode?.user != nil {
_ = bleManager.requestDeviceConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
}
}
@ -295,12 +304,15 @@ struct DeviceConfig: View {
}
}
.onChange(of: doubleTapAsButtonPress) { newDoubleTapAsButtonPress in
if node != nil && node!.deviceConfig != nil {
if newDoubleTapAsButtonPress != node!.deviceConfig!.doubleTapAsButtonPress { hasChanges = true }
}
}
.onChange(of: isManaged) { newIsManaged in
if node != nil && node!.deviceConfig != nil {
if newIsManaged != node!.deviceConfig!.isManaged { hasChanges = true }
}
}
}
func setDeviceValues() {
self.deviceRole = Int(node?.deviceConfig?.role ?? 0)
@ -310,6 +322,7 @@ struct DeviceConfig: View {
self.buzzerGPIO = Int(node?.deviceConfig?.buzzerGpio ?? 0)
self.rebroadcastMode = Int(node?.deviceConfig?.rebroadcastMode ?? 0)
self.doubleTapAsButtonPress = node?.deviceConfig?.doubleTapAsButtonPress ?? false
self.isManaged = node?.deviceConfig?.isManaged ?? false
self.hasChanges = false
}
}

View file

@ -8,7 +8,7 @@
import SwiftUI
struct Settings: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "user.longName", ascending: true)], animation: .default)
@ -16,9 +16,9 @@ struct Settings: View {
@State private var selectedNode: Int = 0
@State private var connectedNodeNum: Int = 0
@State private var initialLoad: Bool = true
@State private var selection: SettingsSidebar = .about
enum SettingsSidebar {
case appSettings
case shareChannels
@ -41,7 +41,7 @@ struct Settings: View {
case adminMessageLog
case about
}
var body: some View {
NavigationSplitView {
List {
@ -50,7 +50,7 @@ struct Settings: View {
} label: {
Image(systemName: "questionmark.app")
.symbolRenderingMode(.hierarchical)
Text("about.meshtastic")
}
.tag(SettingsSidebar.about)
@ -63,227 +63,231 @@ struct Settings: View {
}
.tag(SettingsSidebar.appSettings)
let node = nodes.first(where: { $0.num == connectedNodeNum })
Section("Configure") {
Picker("Configuring Node", selection: $selectedNode) {
if selectedNode == 0 {
Text("Connect to a Node").tag(0)
}
ForEach(nodes) { node in
if node.num == bleManager.connectedPeripheral?.num ?? 0 {
Text("BLE Config: \(node.user?.longName ?? "unknown".localized)")
.tag(Int(node.num))
} else if node.metadata != nil {
Text("Remote Config: \(node.user?.longName ?? "unknown".localized)")
.tag(Int(node.num))
} else {
Text("Request Admin: \(node.user?.longName ?? "unknown".localized)")
.tag(Int(node.num))
if !(node?.deviceConfig?.isManaged ?? false) {
Section("Configure") {
Picker("Configuring Node", selection: $selectedNode) {
if selectedNode == 0 {
Text("Connect to a Node").tag(0)
}
ForEach(nodes) { node in
if node.num == bleManager.connectedPeripheral?.num ?? 0 {
Text("BLE Config: \(node.user?.longName ?? "unknown".localized)")
.tag(Int(node.num))
} else if node.metadata != nil {
Text("Remote Config: \(node.user?.longName ?? "unknown".localized)")
.tag(Int(node.num))
} else {
Text("Request Admin: \(node.user?.longName ?? "unknown".localized)")
.tag(Int(node.num))
}
}
}
}
.pickerStyle(.automatic)
.labelsHidden()
.onChange(of: selectedNode) { newValue in
if selectedNode > 0 {
let node = nodes.first(where: { $0.num == newValue })
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
if node?.metadata == nil {
let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context)
if adminMessageId > 0 {
print("Sent node metadata request from node details")
.pickerStyle(.automatic)
.labelsHidden()
.onChange(of: selectedNode) { newValue in
if selectedNode > 0 {
let node = nodes.first(where: { $0.num == newValue })
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0)
if connectedNode != nil && node?.metadata == nil {
let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context)
if adminMessageId > 0 {
print("Sent node metadata request from node details")
}
}
}
}
}
}
Section("radio.configuration") {
NavigationLink {
ShareChannels(node: nodes.first(where: { $0.num == connectedNodeNum }))
} label: {
Image(systemName: "qrcode")
.symbolRenderingMode(.hierarchical)
Text("share.channels")
Section("radio.configuration") {
NavigationLink {
ShareChannels(node: nodes.first(where: { $0.num == connectedNodeNum }))
} label: {
Image(systemName: "qrcode")
.symbolRenderingMode(.hierarchical)
Text("share.channels")
}
.tag(SettingsSidebar.shareChannels)
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
NavigationLink {
UserConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "person.crop.rectangle.fill")
.symbolRenderingMode(.hierarchical)
Text("user")
}
.tag(SettingsSidebar.userConfig)
NavigationLink {
LoRaConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "dot.radiowaves.left.and.right")
.symbolRenderingMode(.hierarchical)
Text("lora")
}
.tag(SettingsSidebar.loraConfig)
NavigationLink {
Channels(node: nodes.first(where: { $0.num == connectedNodeNum }))
} label: {
Image(systemName: "fibrechannel")
.symbolRenderingMode(.hierarchical)
Text("channels")
}
.tag(SettingsSidebar.channelConfig)
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
NavigationLink {
BluetoothConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "antenna.radiowaves.left.and.right")
.symbolRenderingMode(.hierarchical)
Text("bluetooth")
}
.tag(SettingsSidebar.bluetoothConfig)
NavigationLink {
DeviceConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "flipphone")
.symbolRenderingMode(.hierarchical)
Text("device")
}
.tag(SettingsSidebar.deviceConfig)
NavigationLink {
DisplayConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "display")
.symbolRenderingMode(.hierarchical)
Text("display")
}
.tag(SettingsSidebar.displayConfig)
NavigationLink {
NetworkConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "network")
.symbolRenderingMode(.hierarchical)
Text("network")
}
.tag(SettingsSidebar.networkConfig)
NavigationLink {
PositionConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "location")
.symbolRenderingMode(.hierarchical)
Text("position")
}
.tag(SettingsSidebar.positionConfig)
}
.tag(SettingsSidebar.shareChannels)
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
NavigationLink {
UserConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "person.crop.rectangle.fill")
.symbolRenderingMode(.hierarchical)
Text("user")
Section("module.configuration") {
NavigationLink {
CannedMessagesConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "list.bullet.rectangle.fill")
.symbolRenderingMode(.hierarchical)
Text("canned.messages")
}
.tag(SettingsSidebar.cannedMessagesConfig)
NavigationLink {
ExternalNotificationConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "megaphone")
.symbolRenderingMode(.hierarchical)
Text("external.notification")
}
.tag(SettingsSidebar.externalNotificationConfig)
NavigationLink {
MQTTConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "dot.radiowaves.right")
.symbolRenderingMode(.hierarchical)
Text("mqtt")
}
.tag(SettingsSidebar.mqttConfig)
NavigationLink {
RangeTestConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "point.3.connected.trianglepath.dotted")
.symbolRenderingMode(.hierarchical)
Text("range.test")
}
NavigationLink {
RtttlConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "music.note.list")
.symbolRenderingMode(.hierarchical)
Text("ringtone")
}
.tag(SettingsSidebar.ringtoneConfig)
NavigationLink {
SerialConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "terminal")
.symbolRenderingMode(.hierarchical)
Text("serial")
}
.tag(SettingsSidebar.serialConfig)
NavigationLink {
TelemetryConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "chart.xyaxis.line")
.symbolRenderingMode(.hierarchical)
Text("telemetry")
}
.tag(SettingsSidebar.telemetryConfig)
}
.tag(SettingsSidebar.userConfig)
NavigationLink {
LoRaConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "dot.radiowaves.left.and.right")
.symbolRenderingMode(.hierarchical)
Text("lora")
Section(header: Text("logging")) {
NavigationLink {
MeshLog()
} label: {
Image(systemName: "list.bullet.rectangle")
.symbolRenderingMode(.hierarchical)
Text("mesh.log")
}
.tag(SettingsSidebar.meshLog)
NavigationLink {
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
AdminMessageList(user: connectedNode?.user)
} label: {
Image(systemName: "building.columns")
.symbolRenderingMode(.hierarchical)
Text("admin.log")
}
.tag(SettingsSidebar.adminMessageLog)
}
.tag(SettingsSidebar.loraConfig)
NavigationLink {
Channels(node: nodes.first(where: { $0.num == connectedNodeNum }))
} label: {
Image(systemName: "fibrechannel")
.symbolRenderingMode(.hierarchical)
Text("channels")
Section(header: Text("Firmware")) {
NavigationLink {
Firmware(node: nodes.first(where: { $0.num == connectedNodeNum }))
} label: {
Image(systemName: "arrow.up.arrow.down.square")
.symbolRenderingMode(.hierarchical)
Text("Firmware Updates")
}
.tag(SettingsSidebar.about)
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
}
.tag(SettingsSidebar.channelConfig)
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
NavigationLink {
BluetoothConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "antenna.radiowaves.left.and.right")
.symbolRenderingMode(.hierarchical)
Text("bluetooth")
}
.tag(SettingsSidebar.bluetoothConfig)
NavigationLink {
DeviceConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "flipphone")
.symbolRenderingMode(.hierarchical)
Text("device")
}
.tag(SettingsSidebar.deviceConfig)
NavigationLink {
DisplayConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "display")
.symbolRenderingMode(.hierarchical)
Text("display")
}
.tag(SettingsSidebar.displayConfig)
NavigationLink {
NetworkConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "network")
.symbolRenderingMode(.hierarchical)
Text("network")
}
.tag(SettingsSidebar.networkConfig)
NavigationLink {
PositionConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "location")
.symbolRenderingMode(.hierarchical)
Text("position")
}
.tag(SettingsSidebar.positionConfig)
}
Section("module.configuration") {
NavigationLink {
CannedMessagesConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "list.bullet.rectangle.fill")
.symbolRenderingMode(.hierarchical)
Text("canned.messages")
}
.tag(SettingsSidebar.cannedMessagesConfig)
NavigationLink {
ExternalNotificationConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "megaphone")
.symbolRenderingMode(.hierarchical)
Text("external.notification")
}
.tag(SettingsSidebar.externalNotificationConfig)
NavigationLink {
MQTTConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "dot.radiowaves.right")
.symbolRenderingMode(.hierarchical)
Text("mqtt")
}
.tag(SettingsSidebar.mqttConfig)
NavigationLink {
RangeTestConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "point.3.connected.trianglepath.dotted")
.symbolRenderingMode(.hierarchical)
Text("range.test")
}
NavigationLink {
RtttlConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "music.note.list")
.symbolRenderingMode(.hierarchical)
Text("ringtone")
}
.tag(SettingsSidebar.ringtoneConfig)
NavigationLink {
SerialConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "terminal")
.symbolRenderingMode(.hierarchical)
Text("serial")
}
.tag(SettingsSidebar.serialConfig)
NavigationLink {
TelemetryConfig(node: nodes.first(where: { $0.num == selectedNode }))
} label: {
Image(systemName: "chart.xyaxis.line")
.symbolRenderingMode(.hierarchical)
Text("telemetry")
}
.tag(SettingsSidebar.telemetryConfig)
}
Section(header: Text("logging")) {
NavigationLink {
MeshLog()
} label: {
Image(systemName: "list.bullet.rectangle")
.symbolRenderingMode(.hierarchical)
Text("mesh.log")
}
.tag(SettingsSidebar.meshLog)
NavigationLink {
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
AdminMessageList(user: connectedNode?.user)
} label: {
Image(systemName: "building.columns")
.symbolRenderingMode(.hierarchical)
Text("admin.log")
}
.tag(SettingsSidebar.adminMessageLog)
}
Section(header: Text("Firmware")) {
NavigationLink {
Firmware(node: nodes.first(where: { $0.num == connectedNodeNum }))
} label: {
Image(systemName: "arrow.up.arrow.down.square")
.symbolRenderingMode(.hierarchical)
Text("Firmware Updates")
}
.tag(SettingsSidebar.about)
.disabled(selectedNode > 0 && selectedNode != connectedNodeNum)
}
}
.onAppear {
@ -297,11 +301,11 @@ struct Settings: View {
.listStyle(GroupedListStyle())
.navigationTitle("settings")
.navigationBarItems(leading:
MeshtasticLogo()
MeshtasticLogo()
)
}
detail: {
Text("select.menu.item")
}
detail: {
Text("select.menu.item")
}
}
}

View file

@ -139,7 +139,7 @@
"map"="Mesh Map";
"map.type"="Default Type";
"map.centering"="Centering Mode";
"map.tiles.delete"="Delete Cached Map Tiles";
"map.tiles.delete"="Delete All Map Tiles";
"map.recentering"="Automatic Re-centering";
"map.usertrackingmode"="User tracking mode";
"map.usertrackingmode.follow"="Follow";
@ -207,7 +207,7 @@
"position"="Position";
"position.config"="Position Config";
"preferred.radio"="Preferred Radio";
"provide.location"="Provide location to mesh";
"provide.location"="Share location";
"radio.configuration"="Radio Configuration";
"range.test"="Range Test";
"range.test.config"="Range Test Config";