Merge pull request #317 from meshtastic/2.0.15_Working_Changes

2.0.15 working changes
This commit is contained in:
Garth Vander Houwen 2023-02-22 16:37:20 -08:00 committed by GitHub
commit b6ef36f736
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 825 additions and 264 deletions

View file

@ -38,7 +38,6 @@
DD4F23CD28779A3C001D37CB /* EnvironmentMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */; };
DD5394FC276993AD00AD86B1 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = DD5394FB276993AD00AD86B1 /* SwiftProtobuf */; };
DD5394FE276BA0EF00AD86B1 /* PositionEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */; };
DD539502276DAA6A00AD86B1 /* MapLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD539501276DAA6A00AD86B1 /* MapLocation.swift */; };
DD58C5F22919AD3C00D5BEFB /* ChannelEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */; };
DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5D0A9B2931B9F200F7EA61 /* EthernetModes.swift */; };
DD5E5202298EE33B00D21B61 /* admin.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51F0298EE33B00D21B61 /* admin.pb.swift */; };
@ -50,7 +49,6 @@
DD5E5208298EE33B00D21B61 /* rtttl.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51F6298EE33B00D21B61 /* rtttl.pb.swift */; };
DD5E5209298EE33B00D21B61 /* module_config.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51F7298EE33B00D21B61 /* module_config.pb.swift */; };
DD5E520A298EE33B00D21B61 /* channel.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51F8298EE33B00D21B61 /* channel.pb.swift */; };
DD5E520B298EE33B00D21B61 /* device_metadata.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51F9298EE33B00D21B61 /* device_metadata.pb.swift */; };
DD5E520C298EE33B00D21B61 /* portnums.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51FA298EE33B00D21B61 /* portnums.pb.swift */; };
DD5E520D298EE33B00D21B61 /* storeforward.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51FB298EE33B00D21B61 /* storeforward.pb.swift */; };
DD5E520E298EE33B00D21B61 /* mqtt.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51FC298EE33B00D21B61 /* mqtt.pb.swift */; };
@ -60,7 +58,6 @@
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 */; };
DD5E523A298EFA5300D21B61 /* TelemetryWeather.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E5239298EFA5300D21B61 /* TelemetryWeather.swift */; };
DD5E523C298F02D400D21B61 /* LicensedUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E523B298F02D400D21B61 /* LicensedUser.swift */; };
DD5E523F298F5A9E00D21B61 /* AirQualityIndexCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E523E298F5A9E00D21B61 /* AirQualityIndexCompact.swift */; };
DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */; };
DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */; };
@ -170,7 +167,6 @@
DD4A911D2708C65400501B7E /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentMetricsLog.swift; sourceTree = "<group>"; };
DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionEntityExtension.swift; sourceTree = "<group>"; };
DD539501276DAA6A00AD86B1 /* MapLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLocation.swift; sourceTree = "<group>"; };
DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelEntityExtension.swift; sourceTree = "<group>"; };
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV2.xcdatamodel; sourceTree = "<group>"; };
DD5D0A9B2931B9F200F7EA61 /* EthernetModes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthernetModes.swift; sourceTree = "<group>"; };
@ -184,7 +180,6 @@
DD5E51F6298EE33B00D21B61 /* rtttl.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = rtttl.pb.swift; sourceTree = "<group>"; };
DD5E51F7298EE33B00D21B61 /* module_config.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = module_config.pb.swift; sourceTree = "<group>"; };
DD5E51F8298EE33B00D21B61 /* channel.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = channel.pb.swift; sourceTree = "<group>"; };
DD5E51F9298EE33B00D21B61 /* device_metadata.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = device_metadata.pb.swift; sourceTree = "<group>"; };
DD5E51FA298EE33B00D21B61 /* portnums.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = portnums.pb.swift; sourceTree = "<group>"; };
DD5E51FB298EE33B00D21B61 /* storeforward.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = storeforward.pb.swift; sourceTree = "<group>"; };
DD5E51FC298EE33B00D21B61 /* mqtt.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = mqtt.pb.swift; sourceTree = "<group>"; };
@ -194,7 +189,6 @@
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>"; };
DD5E5239298EFA5300D21B61 /* TelemetryWeather.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryWeather.swift; sourceTree = "<group>"; };
DD5E523B298F02D400D21B61 /* LicensedUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicensedUser.swift; sourceTree = "<group>"; };
DD5E523E298F5A9E00D21B61 /* AirQualityIndexCompact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirQualityIndexCompact.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>"; };
@ -239,6 +233,7 @@
DDB6ABE128B13FB500384BA1 /* PositionConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionConfigEnums.swift; sourceTree = "<group>"; };
DDB6ABE328B13FFF00384BA1 /* DisplayEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayEnums.swift; sourceTree = "<group>"; };
DDB6ABE528B1406100384BA1 /* LoraConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoraConfigEnums.swift; 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>"; };
DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = ../Assets.xcassets; sourceTree = "<group>"; };
@ -343,7 +338,6 @@
DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */,
DD4A911D2708C65400501B7E /* AppSettings.swift */,
DDA0B6B1294CDC55001356EC /* Channels.swift */,
DD5E523B298F02D400D21B61 /* LicensedUser.swift */,
DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */,
DD86D40B287F401000BAEB7A /* SaveChannelQRCode.swift */,
DD3501882852FC3B000FC853 /* Settings.swift */,
@ -366,7 +360,6 @@
DD5E51F6298EE33B00D21B61 /* rtttl.pb.swift */,
DD5E51F7298EE33B00D21B61 /* module_config.pb.swift */,
DD5E51F8298EE33B00D21B61 /* channel.pb.swift */,
DD5E51F9298EE33B00D21B61 /* device_metadata.pb.swift */,
DD5E51FA298EE33B00D21B61 /* portnums.pb.swift */,
DD5E51FB298EE33B00D21B61 /* storeforward.pb.swift */,
DD5E51FC298EE33B00D21B61 /* mqtt.pb.swift */,
@ -548,7 +541,6 @@
isa = PBXGroup;
children = (
DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */,
DD539501276DAA6A00AD86B1 /* MapLocation.swift */,
DD35018A2852FC79000FC853 /* UserSettings.swift */,
);
path = Model;
@ -850,7 +842,6 @@
DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */,
DD8ED9C52898D51F00B3B0AB /* NetworkConfig.swift in Sources */,
DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */,
DD5E520B298EE33B00D21B61 /* device_metadata.pb.swift in Sources */,
DD2E65262767A01F00E45FC5 /* NodeDetail.swift in Sources */,
DD1925B928CDA93900720036 /* SerialConfigEnums.swift in Sources */,
DD86D4112881D16900BAEB7A /* WriteCsvFile.swift in Sources */,
@ -858,7 +849,6 @@
DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */,
DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */,
DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */,
DD5E523C298F02D400D21B61 /* LicensedUser.swift in Sources */,
DDB6ABE428B13FFF00384BA1 /* DisplayEnums.swift in Sources */,
DD86D40A287F04F100BAEB7A /* InvalidVersion.swift in Sources */,
DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */,
@ -879,7 +869,6 @@
DD5E5210298EE33B00D21B61 /* telemetry.pb.swift in Sources */,
DD5E5205298EE33B00D21B61 /* mesh.pb.swift in Sources */,
DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */,
DD539502276DAA6A00AD86B1 /* MapLocation.swift in Sources */,
DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */,
DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */,
DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */,
@ -1075,7 +1064,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.0.14;
MARKETING_VERSION = 2.0.15;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1108,7 +1097,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.0.14;
MARKETING_VERSION = 2.0.15;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1284,6 +1273,7 @@
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */,
DD5E51CC2986643400D21B61 /* MeshtasticDataModelV7.xcdatamodel */,
DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */,
DD457BC4295D5E35004BCE4D /* MeshtasticDataModelV5.xcdatamodel */,
@ -1292,7 +1282,7 @@
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
);
currentVersion = DD5E51CC2986643400D21B61 /* MeshtasticDataModelV7.xcdatamodel */;
currentVersion = DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */;
name = Meshtastic.xcdatamodeld;
path = Meshtastic/Meshtastic.xcdatamodeld;
sourceTree = "<group>";

View file

@ -35,6 +35,27 @@ enum KeyboardType: Int, CaseIterable, Identifiable {
}
}
enum CenteringMode: Int, CaseIterable, Identifiable {
case allAnnotations = 0
case allPositions = 1
case clientGps = 2
var id: Int { self.rawValue }
var description: String {
get {
switch self {
case .allAnnotations:
return "All Annotations"// NSLocalizedString("default", comment: "Default Keyboard")
case .allPositions:
return "All Node Postions"// NSLocalizedString("ascii.capable", comment: "ASCII Capable Keyboard")
case .clientGps:
return "Client GPS"//NSLocalizedString("email.address", comment: "Email Address Keyboard")
}
}
}
}
enum MeshMapType: String, CaseIterable, Identifiable {
case standard = "standard"

View file

@ -8,7 +8,7 @@ import MapKit
// Meshtastic BLE Device Manager
// ---------------------------------------------------------------------------------------
class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
private static var documentsFolder: URL {
do {
return try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
@ -16,11 +16,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
fatalError("Can't find documents directory.")
}
}
var context: NSManagedObjectContext?
var userSettings: UserSettings?
private var centralManager: CBCentralManager!
private let restoreKey = "Meshtastic.BLE.Manager"
@Published var peripherals: [Peripheral] = []
@Published var connectedPeripheral: Peripheral!
@Published var lastConnectionError: String
@ -35,35 +36,36 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
public var isConnected: Bool = false
public var isSubscribed: Bool = false
private var configNonce: UInt32 = 1
var timeoutTimer: Timer?
var timeoutTimerCount = 0
var timeoutTimerRuns = 0
var positionTimer: Timer?
let emptyNodeNum: UInt32 = 4294967295
/* Meshtastic Service Details */
var TORADIO_characteristic: CBCharacteristic!
var FROMRADIO_characteristic: CBCharacteristic!
var FROMNUM_characteristic: CBCharacteristic!
let meshtasticServiceCBUUID = CBUUID(string: "0x6BA1B218-15A8-461F-9FA8-5DCAE273EAFD")
let TORADIO_UUID = CBUUID(string: "0xF75C76D2-129E-4DAD-A1DD-7866124401E7")
let FROMRADIO_UUID = CBUUID(string: "0x2C55E69E-4993-11ED-B878-0242AC120002")
let EOL_FROMRADIO_UUID = CBUUID(string: "0x8BA2BCC2-EE02-4A55-A531-C525C5E454D5")
let FROMNUM_UUID = CBUUID(string: "0xED9DA18C-A800-4F66-A670-AA7547E34453")
//private var meshLoggingEnabled: Bool = true
let meshLog = documentsFolder.appendingPathComponent("meshlog.txt")
// MARK: init BLEManager
override init() {
self.lastConnectionError = ""
self.connectedVersion = "0.0.0"
super.init()
centralManager = CBCentralManager(delegate: self, queue: nil)
//centralManager = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionRestoreIdentifierKey: restoreKey])
}
// MARK: Scanning for BLE Devices
// Scan for nearby BLE devices using the Meshtastic BLE service ID
func startScanning() {
@ -130,6 +132,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
print(" BLE Disconnecting from: \(connectedPeripheral.name) to connect to \(peripheral.name ?? "Unknown")")
disconnectPeripheral()
}
centralManager?.connect(peripheral)
// Invalidate any existing timer
if timeoutTimer != nil {
@ -495,6 +498,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
}
}
}
// Device Metadata
if decodedInfo.metadata.firmwareVersion.count > 0 && !invalidVersion {
nowKnown = true
deviceMetadataPacket(metadata: decodedInfo.metadata, fromNum: connectedPeripheral.num, context: context!)
}
// Log any other unknownApp calls
if !nowKnown { MeshLogger.log("🕸️ MESH PACKET received for Unknown App UNHANDLED \(try! decodedInfo.packet.jsonString())") }
@ -674,7 +683,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject {
newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: message)
let dataType = PortNum.textMessageApp
let payloadData: Data = message.data(using: String.Encoding.utf8)!
var messageQuotesReplaced = message.replacingOccurrences(of: "", with: "'")
messageQuotesReplaced = message.replacingOccurrences(of: "", with: "\"")
let payloadData: Data = messageQuotesReplaced.data(using: String.Encoding.utf8)!
var dataMessage = DataMessage()
dataMessage.payload = payloadData
@ -1999,4 +2010,39 @@ extension BLEManager: CBCentralManagerDelegate {
let visibleDuration = Calendar.current.date(byAdding: .second, value: -5, to: today)!
self.peripherals.removeAll(where: { $0.lastUpdate < visibleDuration})
}
// func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
//
// guard let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] else {
// return
// }
// print(peripherals)
// if peripherals.count > 0 {
// //connectedPeripheral.peripheral = peripherals[0]
// // 5
// //connectedPeripheral.peripheral.delegate = self
//
// for peripheral in peripherals {
//
// switch peripheral.state {
// case .connecting: // I've only seen this happen when
// // re-launching attached to Xcode.
// print("Xcode Restore")
//
// case .connected: // Store for connection / requesting
// // notifications when BT starts.
// print("Actual restore")
// //centralManager.connect(peripheral)
// default: break
// }
//
//
//
// // connectedPeripheral.peripheral
// //connectedPeripheral.peripheral = peripheral
// //connectedPeripheral.peripheral.delegate = self
// }
// }
// print("willRestoreState Hit!")
// }
}

View file

@ -72,6 +72,23 @@ extension Int {
}
}
extension UIImage {
func rotate(radians: Float) -> UIImage? {
var newSize = CGRect(origin: CGPoint.zero, size: self.size).applying(CGAffineTransform(rotationAngle: CGFloat(radians))).size
newSize.width = floor(newSize.width)
newSize.height = floor(newSize.height)
UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale)
let context = UIGraphicsGetCurrentContext()!
context.translateBy(x: newSize.width/2, y: newSize.height/2)
context.rotate(by: CGFloat(radians))
self.draw(in: CGRect(x: -self.size.width/2, y: -self.size.height/2, width: self.size.width, height: self.size.height))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
}
extension String {
func base64urlToBase64() -> String {

View file

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

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22D49" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22D68" 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"/>

View file

@ -0,0 +1,301 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22D68" 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="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="oledType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="screenCarouselInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="screenOnSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="displayConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="displayConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="ExternalNotificationConfigEntity" representedClassName="ExternalNotificationConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="active" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertBellBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertBellVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertMessage" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertMessageBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="alertMessageVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="nagTimeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="output" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="outputBuzzer" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="outputMilliseconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="outputVibra" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="usePWM" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="LoRaConfigEntity" representedClassName="LoRaConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="bandwidth" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="channelNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="codingRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="frequencyOffset" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="hopLimit" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="modemPreset" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="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="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="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="fromUser" optional="YES" maxCount="1" deletionRule="Nullify" ordered="YES" destinationEntity="UserEntity" inverseName="sentMessages" inverseEntity="UserEntity"/>
<relationship name="toUser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="receivedMessages" inverseEntity="UserEntity"/>
<fetchedProperty name="tapbacks" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="replyID == $FETCH_SOURCE.messageId AND isEmoji == true"/>
</fetchedProperty>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="messageId"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="MQTTConfigEntity" representedClassName="MQTTConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String" maxValueString="30"/>
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="encryptionEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="jsonEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="password" optional="YES" attributeType="String" maxValueString="30"/>
<attribute name="username" optional="YES" attributeType="String" maxValueString="30"/>
<relationship name="mqttConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="mqttConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="MyInfoEntity" representedClassName="MyInfoEntity" syncable="YES" codeGenerationType="class">
<attribute name="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="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="serialConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SerialConfigEntity" inverseName="serialConfigNode" inverseEntity="SerialConfigEntity"/>
<relationship name="telemetries" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TelemetryEntity" inverseName="nodeTelemetry" inverseEntity="TelemetryEntity"/>
<relationship name="telemetryConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TelemetryConfigEntity" inverseName="telemetryConfigNode" inverseEntity="TelemetryConfigEntity"/>
<relationship name="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="userNode" inverseEntity="UserEntity"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="num"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PositionConfigEntity" representedClassName="PositionConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="deviceGpsEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="fixedPosition" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="gpsAttemptTime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="gpsUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="positionBroadcastSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rxGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="smartPositionEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="txGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="positionConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positionConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="PositionEntity" representedClassName="PositionEntity" syncable="YES" codeGenerationType="class">
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="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="satsInView" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="seqNo" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<relationship name="nodePosition" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positions" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="RangeTestConfigEntity" representedClassName="RangeTestConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="save" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="sender" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
<relationship name="rangeTestConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rangeTestConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="SerialConfigEntity" representedClassName="SerialConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="baudRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="echo" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rxd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="timeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="txd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="serialConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="serialConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TelemetryConfigEntity" representedClassName="TelemetryConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="deviceUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="environmentDisplayFahrenheit" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="environmentMeasurementEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="environmentScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="environmentUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="telemetryConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetryConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="TelemetryEntity" representedClassName="TelemetryEntity" syncable="YES" codeGenerationType="class">
<attribute name="airUtilTx" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="barometricPressure" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="batteryLevel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="channelUtilization" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="current" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="gasResistance" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="metricsType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="relativeHumidity" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="temperature" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="voltage" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="nodeTelemetry" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetries" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="UserEntity" representedClassName="UserEntity" syncable="YES" codeGenerationType="class">
<attribute name="hwModel" attributeType="String"/>
<attribute name="isLicensed" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="longName" attributeType="String"/>
<attribute name="macaddr" optional="YES" attributeType="Binary"/>
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="shortName" attributeType="String"/>
<attribute name="userId" attributeType="String"/>
<relationship name="receivedMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="toUser" inverseEntity="MessageEntity"/>
<relationship name="sentMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="fromUser" inverseEntity="MessageEntity"/>
<relationship name="userNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="user" inverseEntity="NodeInfoEntity"/>
<fetchedProperty name="adminMessages" optional="YES">
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(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

@ -1,15 +0,0 @@
//
// MapLocation.swift
// MeshtasticApple
//
// Created by Garth Vander Houwen on 12/17/21.
//
import Foundation
import MapKit
struct MapLocation: Identifiable {
let id = UUID()
let name: String
let coordinate: CLLocationCoordinate2D
}

View file

@ -45,6 +45,18 @@ class UserSettings: ObservableObject {
UserDefaults.standard.set(meshMapType, forKey: "meshMapType")
}
}
@Published var meshMapCenteringMode: Int {
didSet {
UserDefaults.standard.set(meshMapCenteringMode, forKey: "meshMapCenteringMode")
UserDefaults.standard.synchronize()
}
}
@Published var meshMapRecentering: Bool {
didSet {
UserDefaults.standard.set(meshMapCenteringMode, forKey: "meshMapRecentering")
UserDefaults.standard.synchronize()
}
}
@Published var meshMapCustomTileServer: String {
didSet {
UserDefaults.standard.set(meshMapCustomTileServer, forKey: "meshMapCustomTileServer")
@ -59,7 +71,9 @@ class UserSettings: ObservableObject {
self.provideLocation = UserDefaults.standard.object(forKey: "provideLocation") as? Bool ?? false
self.provideLocationInterval = UserDefaults.standard.object(forKey: "provideLocationInterval") as? Int ?? 900
self.keyboardType = UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0
self.meshMapType = UserDefaults.standard.string(forKey: "meshMapType") ?? "hybrid"
self.meshMapType = UserDefaults.standard.string(forKey: "meshMapType") ?? "standard"
self.meshMapCenteringMode = UserDefaults.standard.object(forKey: "meshMapCenteringMode") as? Int ?? 0
self.meshMapRecentering = UserDefaults.standard.object(forKey: "meshMapRecentering") as? Bool ?? true
self.meshMapCustomTileServer = UserDefaults.standard.string(forKey: "meshMapCustomTileServer") ?? ""
}
}

View file

@ -52,6 +52,6 @@ extension PositionEntity {
extension PositionEntity: MKAnnotation {
public var coordinate: CLLocationCoordinate2D { nodeCoordinate ?? LocationHelper.DefaultLocation }
public var title: String? { nodePosition?.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown") }
public var title: String? { nodePosition?.user?.shortName ?? NSLocalizedString("unknown", comment: "Unknown") }
public var subtitle: String? { time?.formatted() }
}

View file

@ -119,7 +119,18 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
let fetchedNode = try context.fetch(fetchNodePositionRequest) as! [NodeInfoEntity]
if fetchedNode.count == 1 {
// Unset the current latest position for this node
let fetchCurrentLatestPositionsRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "PositionEntity")
fetchCurrentLatestPositionsRequest.predicate = NSPredicate(format: "nodePosition.num == %lld && latest = true", Int64(packet.from))
let fetchedPositions = try context.fetch(fetchCurrentLatestPositionsRequest) as! [PositionEntity]
if fetchedPositions.count > 0 {
for position in fetchedPositions {
position.latest = false
}
}
let position = PositionEntity(context: context)
position.latest = true
position.snr = packet.rxSnr
position.seqNo = Int32(positionMessage.seqNumber)
position.latitudeI = positionMessage.latitudeI
@ -134,12 +145,14 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time)))
}
let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet
mutablePositions.add(position)
fetchedNode[0].id = Int64(packet.from)
fetchedNode[0].num = Int64(packet.from)
fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time)))
fetchedNode[0].snr = packet.rxSnr
fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet
do {
try context.save()
print("💾 Updated Node Position Coordinates, SNR and Time from Position App Packet For: \(fetchedNode[0].num)")

View file

@ -794,6 +794,10 @@ struct HamParameters {
/// Ensure your radio is capable of operating of the selected frequency before setting this.
var frequency: Float = 0
///
/// Optional short name of user
var shortName: String = String()
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
@ -1335,6 +1339,7 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa
1: .standard(proto: "call_sign"),
2: .standard(proto: "tx_power"),
3: .same(proto: "frequency"),
4: .standard(proto: "short_name"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -1346,6 +1351,7 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa
case 1: try { try decoder.decodeSingularStringField(value: &self.callSign) }()
case 2: try { try decoder.decodeSingularInt32Field(value: &self.txPower) }()
case 3: try { try decoder.decodeSingularFloatField(value: &self.frequency) }()
case 4: try { try decoder.decodeSingularStringField(value: &self.shortName) }()
default: break
}
}
@ -1361,6 +1367,9 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa
if self.frequency != 0 {
try visitor.visitSingularFloatField(value: self.frequency, fieldNumber: 3)
}
if !self.shortName.isEmpty {
try visitor.visitSingularStringField(value: self.shortName, fieldNumber: 4)
}
try unknownFields.traverse(visitor: &visitor)
}
@ -1368,6 +1377,7 @@ extension HamParameters: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementa
if lhs.callSign != rhs.callSign {return false}
if lhs.txPower != rhs.txPower {return false}
if lhs.frequency != rhs.frequency {return false}
if lhs.shortName != rhs.shortName {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}

View file

@ -172,6 +172,11 @@ struct Config {
/// Sets the role of node
var rebroadcastMode: Config.DeviceConfig.RebroadcastMode = .all
///
/// Send our nodeinfo this often
/// Defaults to 900 Seconds (15 minutes)
var nodeInfoBroadcastSecs: UInt32 = 0
var unknownFields = SwiftProtobuf.UnknownStorage()
///
@ -1561,6 +1566,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
4: .standard(proto: "button_gpio"),
5: .standard(proto: "buzzer_gpio"),
6: .standard(proto: "rebroadcast_mode"),
7: .standard(proto: "node_info_broadcast_secs"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -1575,6 +1581,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
case 4: try { try decoder.decodeSingularUInt32Field(value: &self.buttonGpio) }()
case 5: try { try decoder.decodeSingularUInt32Field(value: &self.buzzerGpio) }()
case 6: try { try decoder.decodeSingularEnumField(value: &self.rebroadcastMode) }()
case 7: try { try decoder.decodeSingularUInt32Field(value: &self.nodeInfoBroadcastSecs) }()
default: break
}
}
@ -1599,6 +1606,9 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
if self.rebroadcastMode != .all {
try visitor.visitSingularEnumField(value: self.rebroadcastMode, fieldNumber: 6)
}
if self.nodeInfoBroadcastSecs != 0 {
try visitor.visitSingularUInt32Field(value: self.nodeInfoBroadcastSecs, fieldNumber: 7)
}
try unknownFields.traverse(visitor: &visitor)
}
@ -1609,6 +1619,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
if lhs.buttonGpio != rhs.buttonGpio {return false}
if lhs.buzzerGpio != rhs.buzzerGpio {return false}
if lhs.rebroadcastMode != rhs.rebroadcastMode {return false}
if lhs.nodeInfoBroadcastSecs != rhs.nodeInfoBroadcastSecs {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}

View file

@ -1,157 +0,0 @@
// DO NOT EDIT.
// swift-format-ignore-file
//
// Generated by the Swift generator plugin for the protocol buffer compiler.
// Source: meshtastic/device_metadata.proto
//
// For information on using the generated types, please see the documentation:
// https://github.com/apple/swift-protobuf/
import Foundation
import SwiftProtobuf
// If the compiler emits an error on this type, it is because this file
// was generated by a version of the `protoc` Swift plug-in that is
// incompatible with the version of SwiftProtobuf to which you are linking.
// Please ensure that you are building against the same version of the API
// that was used to generate this file.
fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
typealias Version = _2
}
///
/// Device metadata response
struct DeviceMetadata {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
///
/// Device firmware version string
var firmwareVersion: String = String()
///
/// Device state version
var deviceStateVersion: UInt32 = 0
///
/// Indicates whether the device can shutdown CPU natively or via power management chip
var canShutdown: Bool = false
///
/// Indicates that the device has native wifi capability
var hasWifi_p: Bool = false
///
/// Indicates that the device has native bluetooth capability
var hasBluetooth_p: Bool = false
///
/// Indicates that the device has an ethernet peripheral
var hasEthernet_p: Bool = false
///
/// Indicates that the device's role in the mesh
var role: Config.DeviceConfig.Role = .client
///
/// Indicates the device's current enabled position flags
var positionFlags: UInt32 = 0
///
/// Device hardware model
var hwModel: HardwareModel = .unset
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
#if swift(>=5.5) && canImport(_Concurrency)
extension DeviceMetadata: @unchecked Sendable {}
#endif // swift(>=5.5) && canImport(_Concurrency)
// MARK: - Code below here is support for the SwiftProtobuf runtime.
fileprivate let _protobuf_package = "meshtastic"
extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".DeviceMetadata"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .standard(proto: "firmware_version"),
2: .standard(proto: "device_state_version"),
3: .same(proto: "canShutdown"),
4: .same(proto: "hasWifi"),
5: .same(proto: "hasBluetooth"),
6: .same(proto: "hasEthernet"),
7: .same(proto: "role"),
8: .standard(proto: "position_flags"),
9: .standard(proto: "hw_model"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.firmwareVersion) }()
case 2: try { try decoder.decodeSingularUInt32Field(value: &self.deviceStateVersion) }()
case 3: try { try decoder.decodeSingularBoolField(value: &self.canShutdown) }()
case 4: try { try decoder.decodeSingularBoolField(value: &self.hasWifi_p) }()
case 5: try { try decoder.decodeSingularBoolField(value: &self.hasBluetooth_p) }()
case 6: try { try decoder.decodeSingularBoolField(value: &self.hasEthernet_p) }()
case 7: try { try decoder.decodeSingularEnumField(value: &self.role) }()
case 8: try { try decoder.decodeSingularUInt32Field(value: &self.positionFlags) }()
case 9: try { try decoder.decodeSingularEnumField(value: &self.hwModel) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if !self.firmwareVersion.isEmpty {
try visitor.visitSingularStringField(value: self.firmwareVersion, fieldNumber: 1)
}
if self.deviceStateVersion != 0 {
try visitor.visitSingularUInt32Field(value: self.deviceStateVersion, fieldNumber: 2)
}
if self.canShutdown != false {
try visitor.visitSingularBoolField(value: self.canShutdown, fieldNumber: 3)
}
if self.hasWifi_p != false {
try visitor.visitSingularBoolField(value: self.hasWifi_p, fieldNumber: 4)
}
if self.hasBluetooth_p != false {
try visitor.visitSingularBoolField(value: self.hasBluetooth_p, fieldNumber: 5)
}
if self.hasEthernet_p != false {
try visitor.visitSingularBoolField(value: self.hasEthernet_p, fieldNumber: 6)
}
if self.role != .client {
try visitor.visitSingularEnumField(value: self.role, fieldNumber: 7)
}
if self.positionFlags != 0 {
try visitor.visitSingularUInt32Field(value: self.positionFlags, fieldNumber: 8)
}
if self.hwModel != .unset {
try visitor.visitSingularEnumField(value: self.hwModel, fieldNumber: 9)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: DeviceMetadata, rhs: DeviceMetadata) -> Bool {
if lhs.firmwareVersion != rhs.firmwareVersion {return false}
if lhs.deviceStateVersion != rhs.deviceStateVersion {return false}
if lhs.canShutdown != rhs.canShutdown {return false}
if lhs.hasWifi_p != rhs.hasWifi_p {return false}
if lhs.hasBluetooth_p != rhs.hasBluetooth_p {return false}
if lhs.hasEthernet_p != rhs.hasEthernet_p {return false}
if lhs.role != rhs.role {return false}
if lhs.positionFlags != rhs.positionFlags {return false}
if lhs.hwModel != rhs.hwModel {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}

View file

@ -158,6 +158,10 @@ enum HardwareModel: SwiftProtobuf.Enum {
/// New BETAFPV ELRS Micro TX Module 2.4G with ESP32 CPU
case betafpv2400Tx // = 45
///
/// BetaFPV ExpressLRS "Nano" TX Module 900MHz with ESP32 CPU
case betafpv900NanoTx // = 46
///
/// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
case privateHw // = 255
@ -201,6 +205,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
case 43: self = .heltecV3
case 44: self = .heltecWslV3
case 45: self = .betafpv2400Tx
case 46: self = .betafpv900NanoTx
case 255: self = .privateHw
default: self = .UNRECOGNIZED(rawValue)
}
@ -240,6 +245,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
case .heltecV3: return 43
case .heltecWslV3: return 44
case .betafpv2400Tx: return 45
case .betafpv900NanoTx: return 46
case .privateHw: return 255
case .UNRECOGNIZED(let i): return i
}
@ -284,6 +290,7 @@ extension HardwareModel: CaseIterable {
.heltecV3,
.heltecWslV3,
.betafpv2400Tx,
.betafpv900NanoTx,
.privateHw,
]
}
@ -1949,6 +1956,16 @@ struct FromRadio {
set {_uniqueStorage()._payloadVariant = .xmodemPacket(newValue)}
}
///
/// Device metadata message
var metadata: DeviceMetadata {
get {
if case .metadata(let v)? = _storage._payloadVariant {return v}
return DeviceMetadata()
}
set {_uniqueStorage()._payloadVariant = .metadata(newValue)}
}
var unknownFields = SwiftProtobuf.UnknownStorage()
///
@ -1995,6 +2012,9 @@ struct FromRadio {
///
/// File Transfer Chunk
case xmodemPacket(XModem)
///
/// Device metadata message
case metadata(DeviceMetadata)
#if !swift(>=4.1)
static func ==(lhs: FromRadio.OneOf_PayloadVariant, rhs: FromRadio.OneOf_PayloadVariant) -> Bool {
@ -2046,6 +2066,10 @@ struct FromRadio {
guard case .xmodemPacket(let l) = lhs, case .xmodemPacket(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.metadata, .metadata): return {
guard case .metadata(let l) = lhs, case .metadata(let r) = rhs else { preconditionFailure() }
return l == r
}()
default: return false
}
}
@ -2192,6 +2216,54 @@ struct Compressed {
init() {}
}
///
/// Device metadata response
struct DeviceMetadata {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
///
/// Device firmware version string
var firmwareVersion: String = String()
///
/// Device state version
var deviceStateVersion: UInt32 = 0
///
/// Indicates whether the device can shutdown CPU natively or via power management chip
var canShutdown: Bool = false
///
/// Indicates that the device has native wifi capability
var hasWifi_p: Bool = false
///
/// Indicates that the device has native bluetooth capability
var hasBluetooth_p: Bool = false
///
/// Indicates that the device has an ethernet peripheral
var hasEthernet_p: Bool = false
///
/// Indicates that the device's role in the mesh
var role: Config.DeviceConfig.Role = .client
///
/// Indicates the device's current enabled position flags
var positionFlags: UInt32 = 0
///
/// Device hardware model
var hwModel: HardwareModel = .unset
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
#if swift(>=5.5) && canImport(_Concurrency)
extension HardwareModel: @unchecked Sendable {}
extension Constants: @unchecked Sendable {}
@ -2220,6 +2292,7 @@ extension FromRadio.OneOf_PayloadVariant: @unchecked Sendable {}
extension ToRadio: @unchecked Sendable {}
extension ToRadio.OneOf_PayloadVariant: @unchecked Sendable {}
extension Compressed: @unchecked Sendable {}
extension DeviceMetadata: @unchecked Sendable {}
#endif // swift(>=5.5) && canImport(_Concurrency)
// MARK: - Code below here is support for the SwiftProtobuf runtime.
@ -2260,6 +2333,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding {
43: .same(proto: "HELTEC_V3"),
44: .same(proto: "HELTEC_WSL_V3"),
45: .same(proto: "BETAFPV_2400_TX"),
46: .same(proto: "BETAFPV_900_NANO_TX"),
255: .same(proto: "PRIVATE_HW"),
]
}
@ -3401,6 +3475,7 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
10: .same(proto: "channel"),
11: .same(proto: "queueStatus"),
12: .same(proto: "xmodemPacket"),
13: .same(proto: "metadata"),
]
fileprivate class _StorageClass {
@ -3566,6 +3641,19 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
_storage._payloadVariant = .xmodemPacket(v)
}
}()
case 13: try {
var v: DeviceMetadata?
var hadOneofValue = false
if let current = _storage._payloadVariant {
hadOneofValue = true
if case .metadata(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
_storage._payloadVariant = .metadata(v)
}
}()
default: break
}
}
@ -3626,6 +3714,10 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
guard case .xmodemPacket(let v)? = _storage._payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 12)
}()
case .metadata?: try {
guard case .metadata(let v)? = _storage._payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 13)
}()
case nil: break
}
}
@ -3781,3 +3873,83 @@ extension Compressed: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio
return true
}
}
extension DeviceMetadata: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = _protobuf_package + ".DeviceMetadata"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .standard(proto: "firmware_version"),
2: .standard(proto: "device_state_version"),
3: .same(proto: "canShutdown"),
4: .same(proto: "hasWifi"),
5: .same(proto: "hasBluetooth"),
6: .same(proto: "hasEthernet"),
7: .same(proto: "role"),
8: .standard(proto: "position_flags"),
9: .standard(proto: "hw_model"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.firmwareVersion) }()
case 2: try { try decoder.decodeSingularUInt32Field(value: &self.deviceStateVersion) }()
case 3: try { try decoder.decodeSingularBoolField(value: &self.canShutdown) }()
case 4: try { try decoder.decodeSingularBoolField(value: &self.hasWifi_p) }()
case 5: try { try decoder.decodeSingularBoolField(value: &self.hasBluetooth_p) }()
case 6: try { try decoder.decodeSingularBoolField(value: &self.hasEthernet_p) }()
case 7: try { try decoder.decodeSingularEnumField(value: &self.role) }()
case 8: try { try decoder.decodeSingularUInt32Field(value: &self.positionFlags) }()
case 9: try { try decoder.decodeSingularEnumField(value: &self.hwModel) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if !self.firmwareVersion.isEmpty {
try visitor.visitSingularStringField(value: self.firmwareVersion, fieldNumber: 1)
}
if self.deviceStateVersion != 0 {
try visitor.visitSingularUInt32Field(value: self.deviceStateVersion, fieldNumber: 2)
}
if self.canShutdown != false {
try visitor.visitSingularBoolField(value: self.canShutdown, fieldNumber: 3)
}
if self.hasWifi_p != false {
try visitor.visitSingularBoolField(value: self.hasWifi_p, fieldNumber: 4)
}
if self.hasBluetooth_p != false {
try visitor.visitSingularBoolField(value: self.hasBluetooth_p, fieldNumber: 5)
}
if self.hasEthernet_p != false {
try visitor.visitSingularBoolField(value: self.hasEthernet_p, fieldNumber: 6)
}
if self.role != .client {
try visitor.visitSingularEnumField(value: self.role, fieldNumber: 7)
}
if self.positionFlags != 0 {
try visitor.visitSingularUInt32Field(value: self.positionFlags, fieldNumber: 8)
}
if self.hwModel != .unset {
try visitor.visitSingularEnumField(value: self.hwModel, fieldNumber: 9)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: DeviceMetadata, rhs: DeviceMetadata) -> Bool {
if lhs.firmwareVersion != rhs.firmwareVersion {return false}
if lhs.deviceStateVersion != rhs.deviceStateVersion {return false}
if lhs.canShutdown != rhs.canShutdown {return false}
if lhs.hasWifi_p != rhs.hasWifi_p {return false}
if lhs.hasBluetooth_p != rhs.hasBluetooth_p {return false}
if lhs.hasEthernet_p != rhs.hasEthernet_p {return false}
if lhs.role != rhs.role {return false}
if lhs.positionFlags != rhs.positionFlags {return false}
if lhs.hwModel != rhs.hwModel {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}

View file

@ -22,10 +22,10 @@ struct Connect: View {
@State var isUnsetRegion = false
@State var invalidFirmwareVersion = false
var body: some View {
var body: some View {
NavigationStack {
VStack {
VStack {
List {
if bleManager.isSwitchedOn {
Section(header: Text("connected.radio").font(.title)) {
@ -202,7 +202,7 @@ struct Connect: View {
HStack(alignment: .center) {
Spacer()
#if targetEnvironment(macCatalyst)
if bleManager.connectedPeripheral != nil {
@ -226,15 +226,15 @@ struct Connect: View {
}
#endif
Spacer()
}
}
.padding(.bottom, 10)
}
.navigationTitle("bluetooth")
}
.navigationTitle("bluetooth")
.navigationBarItems(leading: MeshtasticLogo(), trailing:
ZStack {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
})
}
}
.sheet(isPresented: $invalidFirmwareVersion, onDismiss: didDismissSheet) {
InvalidVersion(minimumVersion: self.bleManager.minimumVersion, version: self.bleManager.connectedVersion)
.presentationDetents([.large])
@ -267,7 +267,7 @@ struct Connect: View {
}
}
}
.onAppear(perform: {
.onAppear(perform: {
self.bleManager.context = context
self.bleManager.userSettings = userSettings
@ -285,7 +285,7 @@ struct Connect: View {
isPreferredRadio = false
}
})
}
}
func didDismissSheet() {
bleManager.disconnectPeripheral(reconnect: false)
}

View file

@ -25,7 +25,7 @@ struct ContentView: View {
TabView(selection: $selection) {
if userSettings.preferredNodeNum > 0 {
if userSettings.preferredPeripheralId.count > 0 {
Contacts()
.tabItem {

View file

@ -7,6 +7,10 @@
import SwiftUI
import MapKit
func degreesToRadians(_ number: Double) -> Double {
return number * .pi / 180
}
struct MapViewSwiftUI: UIViewRepresentable {
var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void
@ -15,7 +19,10 @@ struct MapViewSwiftUI: UIViewRepresentable {
let positions: [PositionEntity]
let waypoints: [WaypointEntity]
let mapViewType: MKMapType
let centeringMode: CenteringMode
let centerOnPositionsOnly: Bool
@AppStorage("meshMapRecentering") private var recenter = false
// Offline Maps
//make this view dependent on the UserDefault that is updated when importing a new map file
@ -28,28 +35,54 @@ struct MapViewSwiftUI: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
// Parameters
mapView.mapType = mapViewType
mapView.addAnnotations(waypoints)
if centerOnPositionsOnly {
mapView.fit(annotations: positions, andShow: true)
} else {
// Logic to manage the map centering options
switch centeringMode {
case .allAnnotations:
mapView.addAnnotations(positions)
mapView.fitAllAnnotations()
case .allPositions:
mapView.fit(annotations: positions, andShow: true)
case .clientGps:
let span = MKCoordinateSpan(latitudeDelta: 0.003, longitudeDelta: 0.003)
let center = CLLocationCoordinate2D(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)
let region = MKCoordinateRegion(center: center, span: span)
mapView.setRegion(region, animated: true)
mapView.setUserTrackingMode(.followWithHeading, animated: true)
mapView.addAnnotations(positions)
}
mapView.mapType = mapViewType
mapView.setUserTrackingMode(.none, animated: true)
// Other MKMapView Settings
mapView.showsUserLocation = true
mapView.preferredConfiguration.elevationStyle = .realistic
mapView.isPitchEnabled = true
mapView.isRotateEnabled = true
mapView.isScrollEnabled = true
mapView.isZoomEnabled = true
mapView.showsBuildings = true
mapView.showsCompass = true
mapView.showsScale = true
mapView.showsTraffic = true
mapView.showsUserLocation = true
#if targetEnvironment(macCatalyst)
#if targetEnvironment(macCatalyst)
// Show the default always visible compass and the mac only controls
mapView.showsCompass = true
mapView.showsZoomControls = true
#endif
mapView.showsPitchControl = true
#else
#if os(iOS)
// Hide the default compass that only appears when you are not going north and instead always show the compass in the bottom right corner of the map
mapView.showsCompass = false
let compassButton = MKCompassButton(mapView: mapView) // Make a new compass
compassButton.compassVisibility = .visible // Make it visible
mapView.addSubview(compassButton) // Add it to the view
compassButton.translatesAutoresizingMaskIntoConstraints = false
compassButton.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -5).isActive = true
compassButton.bottomAnchor.constraint(equalTo: mapView.bottomAnchor, constant: -25).isActive = true
#endif
#endif
mapView.delegate = context.coordinator
return mapView
}
@ -82,8 +115,27 @@ struct MapViewSwiftUI: UIViewRepresentable {
DispatchQueue.main.async {
mapView.removeAnnotations(mapView.annotations)
mapView.addAnnotations(positions)
mapView.addAnnotations(waypoints)
switch centeringMode {
case .allAnnotations:
mapView.addAnnotations(positions)
if recenter {
mapView.fitAllAnnotations()
}
case .allPositions:
if recenter {
mapView.fit(annotations: positions, andShow: true)
} else {
mapView.addAnnotations(positions)
}
case .clientGps:
mapView.addAnnotations(positions)
if recenter {
let span = MKCoordinateSpan(latitudeDelta: 0.003, longitudeDelta: 0.003)
let region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude), span: span)
mapView.setRegion(region, animated: true)
}
}
}
}
@ -114,20 +166,18 @@ struct MapViewSwiftUI: UIViewRepresentable {
switch annotation {
case _ as MKClusterAnnotation:
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "nodeGroup") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "NodeGroup")
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "nodeGroup") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "WaypointGroup")
annotationView.markerTintColor = .brown//.systemRed
annotationView.displayPriority = .defaultLow
annotationView.tag = -1
return annotationView
case let positionAnnotation as PositionEntity:
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Node")
let reuseID = String(positionAnnotation.nodePosition?.num ?? 0) + "-" + String(positionAnnotation.time?.timeIntervalSince1970 ?? 0)
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "node") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: reuseID )
annotationView.tag = -1
annotationView.canShowCallout = true
annotationView.glyphText = "📟"
let latest = parent.positions.last(where: { $0.nodePosition?.num ?? 0 == positionAnnotation.nodePosition?.num ?? -1 })
if latest == positionAnnotation {
if positionAnnotation.latest {
annotationView.markerTintColor = .systemRed
annotationView.displayPriority = .required
annotationView.titleVisibility = .visible
@ -136,19 +186,33 @@ struct MapViewSwiftUI: UIViewRepresentable {
annotationView.markerTintColor = UIColor(.indigo)
annotationView.displayPriority = .defaultHigh
annotationView.titleVisibility = .adaptive
annotationView.clusteringIdentifier = "nodeGroup"
}
annotationView.tag = -1
annotationView.canShowCallout = true
annotationView.titleVisibility = .adaptive
let leftIcon = UIImageView(image: annotationView.glyphText?.image())
leftIcon.backgroundColor = UIColor(.indigo)
annotationView.leftCalloutAccessoryView = leftIcon
let subtitle = UILabel()
subtitle.text = "Latitude: \(String(format: "%.5f", positionAnnotation.coordinate.latitude)) \n"
subtitle.text = "Long Name: \(positionAnnotation.nodePosition?.user?.longName ?? "Unknown") \n"
subtitle.text! += "Latitude: \(String(format: "%.5f", positionAnnotation.coordinate.latitude)) \n"
subtitle.text! += "Longitude: \(String(format: "%.5f", positionAnnotation.coordinate.longitude)) \n"
let distanceFormatter = MKDistanceFormatter()
subtitle.text! += "Altitude: \(distanceFormatter.string(fromDistance: Double(positionAnnotation.altitude))) \n"
if positionAnnotation.nodePosition?.metadata != nil {
if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.client ||
DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.clientMute ||
DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.routerClient{
annotationView.glyphImage = UIImage(systemName: "flipphone")
} else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.repeater {
annotationView.glyphImage = UIImage(systemName: "repeat")
} else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.router {
annotationView.glyphImage = UIImage(systemName: "wifi.router.fill")
} else if DeviceRoles(rawValue: Int(positionAnnotation.nodePosition!.metadata?.role ?? 0)) == DeviceRoles.tracker {
annotationView.glyphImage = UIImage(systemName: "location.viewfinder")
}
let pf = PositionFlags(rawValue: Int(positionAnnotation.nodePosition?.metadata?.positionFlags ?? 3))
if pf.contains(.Satsinview) {
subtitle.text! += "Sats in view: \(String(positionAnnotation.satsInView)) \n"
@ -162,8 +226,12 @@ struct MapViewSwiftUI: UIViewRepresentable {
subtitle.text! += "Speed: \(formatter.string(from: Measurement(value: Double(positionAnnotation.speed), unit: UnitSpeed.kilometersPerHour))) \n"
}
if pf.contains(.Heading) {
annotationView.glyphImage = UIImage(systemName: "location.north.fill")?.rotate(radians: Float(degreesToRadians(Double(positionAnnotation.heading))))
subtitle.text! += "Heading: \(String(positionAnnotation.heading)) \n"
}
} else {
annotationView.glyphImage = UIImage(systemName: "flipphone")
}
subtitle.text! += positionAnnotation.time?.formatted() ?? "Unknown \n"
subtitle.numberOfLines = 0
@ -173,7 +241,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
annotationView.rightCalloutAccessoryView = detailsIcon
return annotationView
case let waypointAnnotation as WaypointEntity:
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "waypoint") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "Waypoint")
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "waypoint") as? MKMarkerAnnotationView ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: String(waypointAnnotation.id))
annotationView.tag = Int(waypointAnnotation.id)
annotationView.isEnabled = true
annotationView.canShowCallout = true
@ -243,9 +311,9 @@ struct MapViewSwiftUI: UIViewRepresentable {
}
public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let index = self.overlays.firstIndex(where: { overlay_ in overlay_.shape.hash == overlay.hash }) {
let unwrappedOverlay = self.overlays[index]
if let circleOverlay = unwrappedOverlay.shape as? MKCircle {
let renderer = MKCircleRenderer(circle: circleOverlay)

View file

@ -13,6 +13,7 @@ struct NodeDetail: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
@Environment(\.colorScheme) var colorScheme: ColorScheme
@AppStorage("meshMapType") private var meshMapType = "standard"
@State private var mapType: MKMapType = .standard
@State var waypointCoordinate: CLLocationCoordinate2D?
@State var editingWaypoint: Int = 0
@ -39,6 +40,7 @@ struct NodeDetail: View {
/// The current weather condition for the city.
@State private var condition: WeatherCondition?
@State private var temperature: Measurement<UnitTemperature>?
@State private var humidity: Int?
@State private var symbolName: String = "cloud.fill"
@State private var attributionLink: URL?
@ -65,7 +67,9 @@ struct NodeDetail: View {
editingWaypoint = wpId
presentingWaypointForm = true
}
}, positions: annotations, waypoints: Array(waypoints), mapViewType: mapType,
}, positions: annotations, waypoints: Array(waypoints),
mapViewType: mapType,
centeringMode: .allPositions,
centerOnPositionsOnly: true,
customMapOverlay: self.customMapOverlay,
overlays: self.overlays
@ -84,8 +88,12 @@ struct NodeDetail: View {
.pickerStyle(.menu)
.padding(5)
VStack {
Label(temperature?.formatted() ?? "??", systemImage: symbolName)
Label(temperature?.formatted(.measurement(width: .narrow)) ?? "??", systemImage: symbolName)
.font(.caption)
Label("\(humidity ?? 0)%", systemImage: "humidity")
.font(.caption2)
}
.padding(10)
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
@ -351,12 +359,11 @@ struct NodeDetail: View {
}
}
if (self.bleManager.connectedPeripheral != nil && self.bleManager.connectedPeripheral.num == node.num)
|| (self.bleManager.connectedPeripheral != nil && node.metadata != nil) {
if (self.bleManager.connectedPeripheral != nil && node.metadata != nil) {
HStack {
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
if node.metadata?.canShutdown ?? false {
if node.metadata?.canShutdown ?? false || hwModelString == "RAK4631" {//node.metadata?.hwModel ?? "UNSET" == "RAK4631" {
Button(action: {
showingShutdownConfirm = true
@ -436,6 +443,28 @@ struct NodeDetail: View {
})
.onAppear {
self.bleManager.context = context
switch meshMapType {
case "standard":
mapType = .standard
break
case "mutedStandard":
mapType = .mutedStandard
break
case "hybrid":
mapType = .hybrid
break
case "hybridFlyover":
mapType = .hybridFlyover
break
case "satellite":
mapType = .satellite
break
case "satelliteFlyover":
mapType = .satelliteFlyover
break
default:
mapType = .hybridFlyover
}
}
.task(id: node.num) {
do {
@ -447,6 +476,7 @@ struct NodeDetail: View {
let weather = try await WeatherService.shared.weather(for: mostRecent.nodeLocation!)
condition = weather.currentWeather.condition
temperature = weather.currentWeather.temperature
humidity = Int(weather.currentWeather.humidity * 100)
symbolName = weather.currentWeather.symbolName
let attribution = try await WeatherService.shared.attribution

View file

@ -29,6 +29,9 @@ struct NodeMap: View {
}
}
}
@AppStorage("meshMapType") private var meshMapType = "hybridFlyover"
@AppStorage("meshMapCenteringMode") private var meshMapCenteringMode = 0
//&& nodePosition != nil
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)],
predicate: NSPredicate(format: "time >= %@ && nodePosition != nil", Calendar.current.startOfDay(for: Date()) as NSDate), animation: .none)
@ -41,6 +44,7 @@ struct NodeMap: View {
private var waypoints: FetchedResults<WaypointEntity>
@State private var mapType: MKMapType = .standard
@State private var mapCenteringMode: CenteringMode = .allAnnotations
@State var waypointCoordinate: CLLocationCoordinate2D = LocationHelper.DefaultLocation
@State var editingWaypoint: Int = 0
@State private var presentingWaypointForm = false
@ -71,7 +75,8 @@ struct NodeMap: View {
}
}, positions: Array(positions),
waypoints: Array(waypoints),
mapViewType: mapType ?? MKMapType.standard,
mapViewType: mapType,
centeringMode: mapCenteringMode,
centerOnPositionsOnly: false,
customMapOverlay: self.customMapOverlay,
overlays: self.overlays
@ -109,6 +114,29 @@ struct NodeMap: View {
.onAppear(perform: {
self.bleManager.context = context
self.bleManager.userSettings = userSettings
mapCenteringMode = CenteringMode(rawValue: meshMapCenteringMode) ?? CenteringMode.allAnnotations
switch meshMapType {
case "standard":
mapType = .standard
break
case "mutedStandard":
mapType = .mutedStandard
break
case "hybrid":
mapType = .hybrid
break
case "hybridFlyover":
mapType = .hybridFlyover
break
case "satellite":
mapType = .satellite
break
case "satelliteFlyover":
mapType = .satelliteFlyover
break
default:
mapType = .hybridFlyover
}
})
}
}

View file

@ -13,10 +13,6 @@ struct AppSettings: View {
@State private var isPresentingCoreDataResetConfirm = false
@State private var preferredDeviceConnected = false
var perferredPeripheral: String {
UserDefaults.standard.object(forKey: "preferredPeripheralName") as? String ?? ""
}
var body: some View {
VStack {
Form {
@ -39,13 +35,6 @@ struct AppSettings: View {
}
}
.pickerStyle(DefaultPickerStyle())
Picker("map.type", selection: $userSettings.meshMapType) {
ForEach(MeshMapType.allCases) { map in
Text(map.description)
}
}
.pickerStyle(DefaultPickerStyle())
}
@ -70,6 +59,31 @@ struct AppSettings: View {
.listRowSeparator(.visible)
}
}
Section(header: Text("global map options")) {
Picker("map.type", selection: $userSettings.meshMapType) {
ForEach(MeshMapType.allCases) { map in
Text(map.description)
}
}
.pickerStyle(DefaultPickerStyle())
}
Section(header: Text("mesh map options")) {
Picker("map.centering", selection: $userSettings.meshMapCenteringMode) {
ForEach(CenteringMode.allCases) { cm in
Text(cm.description)
}
}
.pickerStyle(DefaultPickerStyle())
Toggle(isOn: $userSettings.meshMapRecentering) {
Label("map.recentering", systemImage: "rectangle.center.inset.filled")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
}
HStack {
Button {

View file

@ -1,8 +0,0 @@
//
// LicensedUser.swift
// Meshtastic
//
// Created by Garth Vander Houwen on 2/4/23.
//
import Foundation

View file

@ -81,7 +81,7 @@ struct Settings: View {
let connectedNode = nodes.first(where: { $0.num == connectedNodeNum })
connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.num : 0)
if node?.metadata == nil && node!.num != connectedNodeNum {
if node?.metadata == nil {
let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context)
if adminMessageId > 0 {

View file

@ -288,7 +288,7 @@ struct ShareChannels: View {
loRaConfig.usePreset = node?.loRaConfig?.usePreset ?? true
loRaConfig.channelNum = UInt32(node?.loRaConfig?.channelNum ?? 0)
channelSet.loraConfig = loRaConfig
if node != nil && node?.myInfo != nil {
if node?.myInfo?.channels != nil && node?.myInfo?.channels?.count ?? 0 > 0 {
for ch in node!.myInfo!.channels!.array as! [ChannelEntity] {
if ch.role > 0 {

View file

@ -48,8 +48,8 @@ struct UserConfig: View {
.onChange(of: longName, perform: { value in
let totalBytes = longName.utf8.count
// Only mess with the value if it is too big
if totalBytes > 36 {
let firstNBytes = Data(longName.utf8.prefix(36))
if totalBytes > (isLicensed ? 8 : 36) {
let firstNBytes = Data(longName.utf8.prefix(isLicensed ? 8 : 36))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
// Set the longName back to the last place where it was the right size
longName = maxBytesString
@ -59,7 +59,7 @@ struct UserConfig: View {
}
.keyboardType(.default)
.disableAutocorrection(true)
Text("\(String(isLicensed ? "Call Sign" : "Long Name")) can be up to 36 bytes long.")
Text("\(String(isLicensed ? "Call Sign" : "Long Name")) can be up to \(isLicensed ? "8" : "36") bytes long.")
.font(.caption2)
HStack {

View file

@ -137,6 +137,8 @@
"lora"="LoRa";
"lora.config"="LoRa Einstellungen";
"map"="Mesh Karte";
"map.centering"="Centering";
"map.recentering"="Automatic Re-centering";
"map.type"="kartentyp";
"mesh.log"="Mesh Log";
"mesh.log.bluetooth.config %@"="Bluetooth Konfiguration empfangen: %@";

View file

@ -137,7 +137,9 @@
"lora"="LoRa";
"lora.config"="LoRa Config";
"map"="Mesh Map";
"map.type"="Map Type";
"map.type"="Default Type";
"map.centering"="Centering Mode";
"map.recentering"="Automatic Re-centering";
"mesh.log"="Mesh Log";
"mesh.log.bluetooth.config %@"="Bluetooth config received: %@";
"mesh.log.cannedmessage.config %@"="Canned Message module config received: %@";

View file

@ -137,6 +137,8 @@
"lora"="LoRa";
"lora.config"="LoRa 配置";
"map"="Mesh 地图";
"map.centering"="Centering";
"map.recentering"="Automatic Re-centering";
"map.type"="地图类型";
"mesh.log"="Mesh 日志";
"mesh.log.bluetooth.config %@"="Bluetooth config received: %@";