mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge pull request #485 from meshtastic/2.2.22_Working_Changes
2.2.22 working changes
This commit is contained in:
commit
402e0d584f
35 changed files with 1477 additions and 702 deletions
|
|
@ -10,8 +10,13 @@
|
|||
6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */; };
|
||||
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */; };
|
||||
6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */; };
|
||||
B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B399E8A32B6F486400E4488E /* RetryButton.swift */; };
|
||||
B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; };
|
||||
C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */; };
|
||||
C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; };
|
||||
D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */; };
|
||||
D9C983A02B79D0E800BDBE6A /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */; };
|
||||
D9C983A22B79D1A600BDBE6A /* RequestPositionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C983A12B79D1A600BDBE6A /* RequestPositionButton.swift */; };
|
||||
DD007BAE2AA4E91200F5FA12 /* MyInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */; };
|
||||
DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */; };
|
||||
DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */ = {isa = PBXBuildFile; productRef = DD0D3D212A55CEB10066DB71 /* CocoaMQTT */; };
|
||||
|
|
@ -177,7 +182,7 @@
|
|||
DDE5B4042B2279A700FCDD05 /* TraceRouteLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE5B4032B2279A700FCDD05 /* TraceRouteLog.swift */; };
|
||||
DDE5B4062B227E3200FCDD05 /* TraceRouteEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */; };
|
||||
DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */; };
|
||||
DDF6B2482A9AEBF500BA6931 /* StoreForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */; };
|
||||
DDF6B2482A9AEBF500BA6931 /* StoreForwardConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForwardConfig.swift */; };
|
||||
DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; };
|
||||
DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */; };
|
||||
DDFFA7472B3A7F3C004730DB /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFFA7462B3A7F3C004730DB /* Bundle.swift */; };
|
||||
|
|
@ -226,9 +231,15 @@
|
|||
6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = "<group>"; };
|
||||
6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = "<group>"; };
|
||||
A65FA974296876BF00A97686 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = "<group>"; };
|
||||
B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = "<group>"; };
|
||||
C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMBTileOverlay.swift; sourceTree = "<group>"; };
|
||||
D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageSize.swift; sourceTree = "<group>"; };
|
||||
D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = "<group>"; };
|
||||
D9C983A12B79D1A600BDBE6A /* RequestPositionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestPositionButton.swift; sourceTree = "<group>"; };
|
||||
DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoEntityExtension.swift; sourceTree = "<group>"; };
|
||||
DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityExtension.swift; sourceTree = "<group>"; };
|
||||
DD05296F2B77F454008E44CD /* MeshtasticDataModelV 26.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 26.xcdatamodel"; sourceTree = "<group>"; };
|
||||
DD0E9C222A30CE3A00580CBB /* MeshtasticDataModelV14.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV14.xcdatamodel; sourceTree = "<group>"; };
|
||||
DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminMessageList.swift; sourceTree = "<group>"; };
|
||||
DD13AA482AB73BF400BA0C98 /* PositionPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionPopover.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -421,7 +432,7 @@
|
|||
DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteRecorder.swift; sourceTree = "<group>"; };
|
||||
DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = "<group>"; };
|
||||
DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV17.xcdatamodel; sourceTree = "<group>"; };
|
||||
DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreForward.swift; sourceTree = "<group>"; };
|
||||
DDF6B2472A9AEBF500BA6931 /* StoreForwardConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreForwardConfig.swift; sourceTree = "<group>"; };
|
||||
DDF6B24B2A9C2FC800BA6931 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
DDF924C926FBB953009FE055 /* ConnectedDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = "<group>"; };
|
||||
DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentConditionsCompact.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -486,6 +497,17 @@
|
|||
path = Custom;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D9C9839E2B79D0C600BDBE6A /* TextMessageField */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B3E905B02B71F7F300654D07 /* TextMessageField.swift */,
|
||||
D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */,
|
||||
D9C983A12B79D1A600BDBE6A /* RequestPositionButton.swift */,
|
||||
D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */,
|
||||
);
|
||||
path = TextMessageField;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DD007BB12AA59B9A00F5FA12 /* CoreData */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -624,7 +646,7 @@
|
|||
DD41582928585C32009B0E59 /* RangeTestConfig.swift */,
|
||||
DDC94FCD29CF55310082EA6E /* RtttlConfig.swift */,
|
||||
DD6193782863875F00E59241 /* SerialConfig.swift */,
|
||||
DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */,
|
||||
DDF6B2472A9AEBF500BA6931 /* StoreForwardConfig.swift */,
|
||||
DD415827285859C4009B0E59 /* TelemetryConfig.swift */,
|
||||
);
|
||||
path = Module;
|
||||
|
|
@ -817,11 +839,13 @@
|
|||
DDC2E18B26CE25A70042C5E4 /* Messages */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D9C9839E2B79D0C600BDBE6A /* TextMessageField */,
|
||||
DDB8F4132A9EE5F000230ECE /* ChannelList.swift */,
|
||||
DD798B062915928D005217CD /* ChannelMessageList.swift */,
|
||||
DDB8F40F2A9EE5B400230ECE /* Messages.swift */,
|
||||
DDB8F4112A9EE5DD00230ECE /* UserList.swift */,
|
||||
DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */,
|
||||
B399E8A32B6F486400E4488E /* RetryButton.swift */,
|
||||
);
|
||||
path = Messages;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1161,6 +1185,7 @@
|
|||
DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */,
|
||||
DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */,
|
||||
DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */,
|
||||
D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */,
|
||||
DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */,
|
||||
DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */,
|
||||
DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */,
|
||||
|
|
@ -1175,6 +1200,7 @@
|
|||
DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */,
|
||||
DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */,
|
||||
DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */,
|
||||
B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */,
|
||||
DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */,
|
||||
DDDB26482AACD6D1003AFCB7 /* NodeMapMapkit.swift in Sources */,
|
||||
DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */,
|
||||
|
|
@ -1237,6 +1263,7 @@
|
|||
DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */,
|
||||
DDDE5A1029AFE69700490C6C /* MeshActivityAttributes.swift in Sources */,
|
||||
DD1925B928CDA93900720036 /* SerialConfigEnums.swift in Sources */,
|
||||
D9C983A02B79D0E800BDBE6A /* AlertButton.swift in Sources */,
|
||||
DD86D4112881D16900BAEB7A /* WriteCsvFile.swift in Sources */,
|
||||
DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */,
|
||||
DD86D40F2881BE4C00BAEB7A /* CsvDocument.swift in Sources */,
|
||||
|
|
@ -1270,16 +1297,18 @@
|
|||
DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */,
|
||||
DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */,
|
||||
DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */,
|
||||
D9C983A22B79D1A600BDBE6A /* RequestPositionButton.swift in Sources */,
|
||||
DDDB26442AAC0206003AFCB7 /* NodeDetail.swift in Sources */,
|
||||
DD5E5210298EE33B00D21B61 /* telemetry.pb.swift in Sources */,
|
||||
DD77093F2AA1B146007A8BF0 /* UIColor.swift in Sources */,
|
||||
DD5E5205298EE33B00D21B61 /* mesh.pb.swift in Sources */,
|
||||
DDF6B2482A9AEBF500BA6931 /* StoreForward.swift in Sources */,
|
||||
DDF6B2482A9AEBF500BA6931 /* StoreForwardConfig.swift in Sources */,
|
||||
DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */,
|
||||
DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */,
|
||||
DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */,
|
||||
DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */,
|
||||
DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */,
|
||||
B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */,
|
||||
DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */,
|
||||
DD5E5204298EE33B00D21B61 /* xmodem.pb.swift in Sources */,
|
||||
DDE5B4062B227E3200FCDD05 /* TraceRouteEntityExtension.swift in Sources */,
|
||||
|
|
@ -1493,7 +1522,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.21;
|
||||
MARKETING_VERSION = 2.2.22;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1527,7 +1556,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.21;
|
||||
MARKETING_VERSION = 2.2.22;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1649,7 +1678,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.21;
|
||||
MARKETING_VERSION = 2.2.22;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1682,7 +1711,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.21;
|
||||
MARKETING_VERSION = 2.2.22;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1793,6 +1822,7 @@
|
|||
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
DD05296F2B77F454008E44CD /* MeshtasticDataModelV 26.xcdatamodel */,
|
||||
DD23D9AB2B7133F6003F5CBE /* MeshtasticDataModelV 25.xcdatamodel */,
|
||||
DDB234392B5CA9B000DA6FB1 /* MeshtasticDataModelV 24.xcdatamodel */,
|
||||
DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */,
|
||||
|
|
@ -1819,7 +1849,7 @@
|
|||
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
|
||||
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
|
||||
);
|
||||
currentVersion = DD23D9AB2B7133F6003F5CBE /* MeshtasticDataModelV 25.xcdatamodel */;
|
||||
currentVersion = DD05296F2B77F454008E44CD /* MeshtasticDataModelV 26.xcdatamodel */;
|
||||
name = Meshtastic.xcdatamodeld;
|
||||
path = Meshtastic/Meshtastic.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ enum RegionCodes: Int, CaseIterable, Identifiable {
|
|||
case ua868 = 15
|
||||
case my_433 = 16
|
||||
case my_919 = 17
|
||||
case sg_923 = 18
|
||||
case lora24 = 13
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
|
|
@ -67,6 +68,8 @@ enum RegionCodes: Int, CaseIterable, Identifiable {
|
|||
return "Malaysia 433mhz"
|
||||
case .my_919:
|
||||
return "Malaysia 919mhz"
|
||||
case .sg_923:
|
||||
return "Singapore 923mhz"
|
||||
}
|
||||
}
|
||||
var dutyCycle: Int {
|
||||
|
|
@ -107,6 +110,8 @@ enum RegionCodes: Int, CaseIterable, Identifiable {
|
|||
return 100
|
||||
case .my_919:
|
||||
return 100
|
||||
case .sg_923:
|
||||
return 100
|
||||
}
|
||||
}
|
||||
func protoEnumValue() -> Config.LoRaConfig.RegionCode {
|
||||
|
|
@ -148,6 +153,8 @@ enum RegionCodes: Int, CaseIterable, Identifiable {
|
|||
return Config.LoRaConfig.RegionCode.my433
|
||||
case .my_919:
|
||||
return Config.LoRaConfig.RegionCode.my919
|
||||
case .sg_923:
|
||||
return Config.LoRaConfig.RegionCode.sg923
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ extension NodeInfoEntity {
|
|||
return traceRoutes?.count ?? 0 > 0
|
||||
}
|
||||
|
||||
var isStoreForwardRouter: Bool {
|
||||
return storeForwardConfig?.isRouter ?? false
|
||||
}
|
||||
|
||||
var isOnline: Bool {
|
||||
let fifteenMinutesAgo = Calendar.current.date(byAdding: .minute, value: -15, to: Date())
|
||||
if lastHeard?.compare(fifteenMinutesAgo!) == .orderedDescending {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
var timeoutTimerCount = 0
|
||||
var positionTimer: Timer?
|
||||
var lastPosition: CLLocationCoordinate2D?
|
||||
let emptyNodeNum: UInt32 = 4294967295
|
||||
static let emptyNodeNum: UInt32 = 4294967295
|
||||
let mqttManager = MqttClientProxyManager.shared
|
||||
var wantRangeTestPackets = false
|
||||
var wantStoreAndForwardPackets = false
|
||||
|
|
@ -695,7 +695,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
case .neighborinfoApp:
|
||||
if let neighborInfo = try? NeighborInfo(serializedData: decodedInfo.packet.decoded.payload) {
|
||||
MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App App UNHANDLED \(neighborInfo)")
|
||||
MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App UNHANDLED")
|
||||
// MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App UNHANDLED \(neighborInfo)")
|
||||
}
|
||||
case .paxcounterApp:
|
||||
MeshLogger.log("🕸️ MESH PACKET received for PAX Counter App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
|
||||
|
|
@ -703,6 +704,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
MeshLogger.log("🕸️ MESH PACKET received for Other App UNHANDLED \(try! decodedInfo.packet.jsonString())")
|
||||
case .max:
|
||||
print("MAX PORT NUM OF 511")
|
||||
case .atakPlugin:
|
||||
MeshLogger.log("🕸️ MESH PACKET received for ATAK Plugin App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
|
||||
}
|
||||
|
||||
if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == configNonce {
|
||||
|
|
@ -713,7 +716,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected })
|
||||
// Config conplete returns so we don't read the characteristic again
|
||||
|
||||
/// MQTT Client Proxy and RangeTest interest
|
||||
/// MQTT Client Proxy and RangeTest and Store and Forward interest
|
||||
if connectedPeripheral.num > 0 {
|
||||
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
|
|
@ -863,7 +866,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
if toUserNum > 0 {
|
||||
meshPacket.to = UInt32(toUserNum)
|
||||
} else {
|
||||
meshPacket.to = emptyNodeNum
|
||||
meshPacket.to = Self.emptyNodeNum
|
||||
}
|
||||
meshPacket.channel = UInt32(channel)
|
||||
meshPacket.from = UInt32(fromUserNum)
|
||||
|
|
@ -910,7 +913,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
var success = false
|
||||
let fromNodeNum = UInt32(connectedPeripheral.num)
|
||||
var meshPacket = MeshPacket()
|
||||
meshPacket.to = emptyNodeNum
|
||||
meshPacket.to = Self.emptyNodeNum
|
||||
meshPacket.from = fromNodeNum
|
||||
meshPacket.wantAck = true
|
||||
var dataMessage = DataMessage()
|
||||
|
|
@ -2367,9 +2370,40 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
return false
|
||||
}
|
||||
|
||||
public func requestStoreAndForwardClientHistory(fromUser: UserEntity, toUser: UserEntity) -> Bool {
|
||||
|
||||
/// send a request for ClientHistory with a time period matching the heartbeat
|
||||
var sfPacket = StoreAndForward()
|
||||
sfPacket.rr = StoreAndForward.RequestResponse.clientHistory
|
||||
sfPacket.history.window = UInt32(toUser.userNode?.storeForwardConfig?.historyReturnWindow ?? 120)
|
||||
sfPacket.history.lastRequest = UInt32(toUser.userNode?.storeForwardConfig?.lastRequest ?? 0)
|
||||
var meshPacket: MeshPacket = MeshPacket()
|
||||
meshPacket.to = UInt32(toUser.num)
|
||||
meshPacket.from = UInt32(fromUser.num)
|
||||
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
meshPacket.priority = MeshPacket.Priority.reliable
|
||||
meshPacket.wantAck = true
|
||||
var dataMessage = DataMessage()
|
||||
dataMessage.payload = try! sfPacket.serializedData()
|
||||
dataMessage.portnum = PortNum.storeForwardApp
|
||||
dataMessage.wantResponse = true
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
var toRadio: ToRadio!
|
||||
toRadio = ToRadio()
|
||||
toRadio.packet = meshPacket
|
||||
let binaryData: Data = try! toRadio.serializedData()
|
||||
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
|
||||
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
|
||||
print("📮 Sent a request for a Store & Forward Client History to \(toUser.num) for the last \(120) minutes.")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func storeAndForwardPacket(packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) {
|
||||
if let storeAndForwardMessage = try? StoreAndForward(serializedData: packet.decoded.payload) {
|
||||
// Request Response
|
||||
// Handle each of the store and forward request / response messages
|
||||
switch storeAndForwardMessage.rr {
|
||||
case .unset:
|
||||
MeshLogger.log("📮 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
|
||||
|
|
@ -2377,31 +2411,29 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
MeshLogger.log("☠️ Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
|
||||
case .routerHeartbeat:
|
||||
/// When we get a router heartbeat we know there is a store and forward node on the network
|
||||
/// Check if it is the primary S&F Router
|
||||
/// Check if it is the primary S&F Router and save the timestamp of the last heartbeat so that we can show the request message history menu item on node long press if the router has been seen recently
|
||||
if (storeAndForwardMessage.heartbeat.secondary == 0) {
|
||||
/// send a request for ClientHistory with a time period matching the heartbeat
|
||||
var sfPacket = StoreAndForward()
|
||||
sfPacket.rr = StoreAndForward.RequestResponse.clientHistory
|
||||
sfPacket.history.window = 120 // storeAndForwardMessage.heartbeat.period
|
||||
var meshPacket: MeshPacket = MeshPacket()
|
||||
meshPacket.to = UInt32(packet.from)
|
||||
meshPacket.from = UInt32(connectedNodeNum)
|
||||
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
meshPacket.priority = MeshPacket.Priority.reliable
|
||||
meshPacket.wantAck = true
|
||||
var dataMessage = DataMessage()
|
||||
dataMessage.payload = try! sfPacket.serializedData()
|
||||
dataMessage.portnum = PortNum.storeForwardApp
|
||||
dataMessage.wantResponse = true
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
var toRadio: ToRadio!
|
||||
toRadio = ToRadio()
|
||||
toRadio.packet = meshPacket
|
||||
let binaryData: Data = try! toRadio.serializedData()
|
||||
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
|
||||
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
|
||||
print("📮 Sent a request for a Store & Forward Client History to \(packet.from) for the last \(storeAndForwardMessage.heartbeat.period) seconds.")
|
||||
guard let routerNode = getNodeInfo(id: Int64(packet.from), context: context) else {
|
||||
return
|
||||
}
|
||||
if routerNode.storeForwardConfig != nil {
|
||||
routerNode.storeForwardConfig?.enabled = true
|
||||
routerNode.storeForwardConfig?.isRouter = storeAndForwardMessage.heartbeat.secondary == 0
|
||||
routerNode.storeForwardConfig?.lastHeartbeat = Date()
|
||||
} else {
|
||||
let newConfig = StoreForwardConfigEntity(context: context)
|
||||
newConfig.enabled = true
|
||||
newConfig.isRouter = storeAndForwardMessage.heartbeat.secondary == 0
|
||||
newConfig.lastHeartbeat = Date()
|
||||
routerNode.storeForwardConfig = newConfig
|
||||
}
|
||||
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
print("💥 Save Store and Forward Router Error")
|
||||
}
|
||||
}
|
||||
MeshLogger.log("💓 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
|
||||
|
|
@ -2412,6 +2444,24 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
case .routerBusy:
|
||||
MeshLogger.log("🐝 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
|
||||
case .routerHistory:
|
||||
/// Set the Router History Last Request Value
|
||||
guard let routerNode = getNodeInfo(id: Int64(packet.from), context: context) else {
|
||||
return
|
||||
}
|
||||
if routerNode.storeForwardConfig != nil {
|
||||
routerNode.storeForwardConfig?.lastRequest = Int32(storeAndForwardMessage.history.lastRequest)
|
||||
} else {
|
||||
let newConfig = StoreForwardConfigEntity(context: context)
|
||||
newConfig.lastRequest = Int32(storeAndForwardMessage.history.lastRequest)
|
||||
routerNode.storeForwardConfig = newConfig
|
||||
}
|
||||
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
context.rollback()
|
||||
print("💥 Save Store and Forward Router Error")
|
||||
}
|
||||
MeshLogger.log("📜 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
|
||||
case .routerStats:
|
||||
MeshLogger.log("📊 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
|
||||
|
|
@ -2428,8 +2478,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
case .clientAbort:
|
||||
MeshLogger.log("🛑 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
|
||||
case .UNRECOGNIZED:
|
||||
textMessageAppPacket(packet: packet, connectedNode: connectedNodeNum, context: context)
|
||||
MeshLogger.log("📮 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
|
||||
case .routerTextDirect:
|
||||
MeshLogger.log("💬 Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
|
||||
textMessageAppPacket(packet: packet, connectedNode: connectedNodeNum, storeForward: true, context: context)
|
||||
case .routerTextBroadcast:
|
||||
MeshLogger.log("✉️ Store and Forward \(storeAndForwardMessage.rr) message received \(storeAndForwardMessage)")
|
||||
textMessageAppPacket(packet: packet, connectedNode: connectedNodeNum, storeForward: true, context: context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ class OfflineTileManager: ObservableObject {
|
|||
if fileManager.fileExists(atPath: tilesUrl.path) {
|
||||
return tilesUrl
|
||||
} else {
|
||||
if UserDefaults.enableOfflineMaps { // Get and persist newTile
|
||||
if UserDefaults.enableOfflineMaps, UserDefaults.mapTileServer.zoomRange.contains(path.z) { // Get and persist newTile
|
||||
return persistLocally(path: path)
|
||||
} else { // Else display empty tile (transparent over Maps tiles)
|
||||
return Bundle.main.url(forResource: "alpha", withExtension: "png")!
|
||||
|
|
@ -148,8 +148,9 @@ class OfflineTileManager: ObservableObject {
|
|||
try data.write(to: filename)
|
||||
} catch {
|
||||
print("💀 Save Tile Error = \(error)")
|
||||
return url
|
||||
}
|
||||
return url
|
||||
return filename
|
||||
}
|
||||
private func filterTilesAlreadyExisting(paths: [MKTileOverlayPath]) -> [MKTileOverlayPath] {
|
||||
paths.filter {
|
||||
|
|
|
|||
|
|
@ -721,9 +721,20 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
|
|||
}
|
||||
}
|
||||
|
||||
func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) {
|
||||
func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, storeForward: Bool = false, context: NSManagedObjectContext) {
|
||||
|
||||
if let messageText = String(bytes: packet.decoded.payload, encoding: .utf8) {
|
||||
var messageText = String(bytes: packet.decoded.payload, encoding: .utf8)
|
||||
var storeForwardBroadcast = false
|
||||
if storeForward {
|
||||
if let storeAndForwardMessage = try? StoreAndForward(serializedData: packet.decoded.payload) {
|
||||
messageText = String(bytes: storeAndForwardMessage.text, encoding: .utf8)
|
||||
if storeAndForwardMessage.rr == .routerTextBroadcast {
|
||||
storeForwardBroadcast = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if messageText?.count ?? 0 > 0 {
|
||||
|
||||
MeshLogger.log("💬 \("mesh.log.textmessage.received".localized)")
|
||||
|
||||
|
|
@ -753,13 +764,15 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
|
|||
}
|
||||
|
||||
if fetchedUsers.first(where: { $0.num == packet.to }) != nil && packet.to != 4294967295 {
|
||||
newMessage.toUser = fetchedUsers.first(where: { $0.num == packet.to })
|
||||
if !storeForwardBroadcast {
|
||||
newMessage.toUser = fetchedUsers.first(where: { $0.num == packet.to })
|
||||
}
|
||||
}
|
||||
if fetchedUsers.first(where: { $0.num == packet.from }) != nil {
|
||||
newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from })
|
||||
}
|
||||
newMessage.messagePayload = messageText
|
||||
newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: messageText)
|
||||
newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: messageText!)
|
||||
if packet.to != 4294967295 && newMessage.fromUser != nil {
|
||||
newMessage.fromUser?.lastMessage = Date()
|
||||
}
|
||||
|
|
@ -790,7 +803,7 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
|
|||
id: ("notification.id.\(newMessage.messageId)"),
|
||||
title: "\(newMessage.fromUser?.longName ?? "unknown".localized)",
|
||||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
|
||||
content: messageText,
|
||||
content: messageText!,
|
||||
target: "message",
|
||||
path: "meshtastic://open-dm?userid=\(newMessage.fromUser?.num ?? 0)&id=\(newMessage.messageId)"
|
||||
)
|
||||
|
|
@ -822,7 +835,7 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
|
|||
id: ("notification.id.\(newMessage.messageId)"),
|
||||
title: "\(newMessage.fromUser?.longName ?? "unknown".localized)",
|
||||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
|
||||
content: messageText,
|
||||
content: messageText!,
|
||||
target: "message",
|
||||
path: "meshtastic://messages/channel/\(newMessage.messageId)")
|
||||
]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>MeshtasticDataModelV 25.xcdatamodel</string>
|
||||
<string>MeshtasticDataModelV 26.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,415 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22522" systemVersion="23D56" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="AmbientLightingConfigEntity" representedClassName="AmbientLightingConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="blue" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="current" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="green" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ledState" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="red" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="ambientLightingConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="ambientLightingConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="BluetoothConfigEntity" representedClassName="BluetoothConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="fixedPin" optional="YES" attributeType="Integer 32" defaultValueString="123456" usesScalarValueType="YES"/>
|
||||
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="bluetoothConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="bluetoothConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="CannedMessageConfigEntity" representedClassName="CannedMessageConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventCcw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventCw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventPress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinA" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinB" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinPress" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="messages" optional="YES" attributeType="String" minValueString="0" maxValueString="198"/>
|
||||
<attribute name="rotary1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="updown1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="cannedMessagesConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="cannedMessageConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="ChannelEntity" representedClassName="ChannelEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="downlinkEnabled" 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 && toUser == nil AND isEmoji == false"/>
|
||||
</fetchedProperty>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="index"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="DetectionSensorConfigEntity" representedClassName="DetectionSensorConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="detectionTriggeredHigh" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="minimumBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="monitorPin" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="stateBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="usePullup" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="detectionSensorConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="detectionSensorConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DeviceConfigEntity" representedClassName="DeviceConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="buttonGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="buzzerGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="debugLogEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="disableTripleClick" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="doubleTapAsButtonPress" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="isManaged" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="nodeInfoBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rebroadcastMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="serialEnabled" optional="YES" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="deviceConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="deviceConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DeviceMetadataEntity" representedClassName="DeviceMetadataEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="canShutdown" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceStateVersion" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="firmwareVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="hasBluetooth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasEthernet" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasWifi" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hwModel" optional="YES" attributeType="String"/>
|
||||
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="metadataNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="metadata" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DisplayConfigEntity" representedClassName="DisplayConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="compassNorthTop" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="displayMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="flipScreen" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsFormat" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="headingBold" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="oledType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="screenCarouselInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="screenOnSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="units" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wakeOnTapOrMotion" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="displayConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="displayConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="ExternalNotificationConfigEntity" representedClassName="ExternalNotificationConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="active" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBellBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBellVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessage" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessageBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessageVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="nagTimeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="output" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputBuzzer" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputMilliseconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputVibra" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="useI2SAsBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="usePWM" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
|
||||
<fetchedProperty name="fetchedProperty" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="ExternalNotificationConfigEntity"/>
|
||||
</fetchedProperty>
|
||||
</entity>
|
||||
<entity name="LocationEntity" representedClassName="LocationEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="routeLocation" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RouteEntity" inverseName="locations" inverseEntity="RouteEntity"/>
|
||||
</entity>
|
||||
<entity name="LoRaConfigEntity" representedClassName="LoRaConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bandwidth" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="channelNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="codingRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="frequencyOffset" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hopLimit" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ignoreMqtt" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="modemPreset" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideDutyCycle" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideFrequency" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="regionCode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="spreadFactor" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sx126xRxBoostedGain" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="txEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="txPower" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="usePreset" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<relationship name="loRaConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="loRaConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="MessageEntity" representedClassName="MessageEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ackError" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ackSNR" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="ackTimestamp" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="admin" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="adminDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isEmoji" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="messageId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="messagePayload" optional="YES" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="messagePayloadMarkdown" optional="YES" attributeType="String"/>
|
||||
<attribute name="messageTimestamp" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="portNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="read" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="realACK" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="receivedACK" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="receivedTimestamp" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="replyID" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="fromUser" optional="YES" maxCount="1" deletionRule="Nullify" ordered="YES" destinationEntity="UserEntity" inverseName="sentMessages" inverseEntity="UserEntity"/>
|
||||
<relationship name="toUser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="receivedMessages" inverseEntity="UserEntity"/>
|
||||
<fetchedProperty name="tapbacks" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="replyID == $FETCH_SOURCE.messageId AND isEmoji == true"/>
|
||||
</fetchedProperty>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="messageId"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="MQTTConfigEntity" representedClassName="MQTTConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="encryptionEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="jsonEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="password" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<attribute name="proxyToClientEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="root" optional="YES" attributeType="String" defaultValueString="msh"/>
|
||||
<attribute name="tlsEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="username" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<relationship name="mqttConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="mqttConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="MyInfoEntity" representedClassName="MyInfoEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="adminIndex" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="bleName" optional="YES" attributeType="String"/>
|
||||
<attribute name="minAppVersion" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="myNodeNum" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="peripheralId" optional="YES" attributeType="String"/>
|
||||
<attribute name="rebootCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="channels" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="ChannelEntity" inverseName="myInfoChannel" inverseEntity="ChannelEntity"/>
|
||||
<relationship name="myInfoNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="myInfo" inverseEntity="NodeInfoEntity"/>
|
||||
<fetchedProperty name="allMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="toUser == nil"/>
|
||||
</fetchedProperty>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="myNodeNum"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="NetworkConfigEntity" representedClassName="NetworkConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="dns" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ethEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="gateway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ip" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ntpServer" optional="YES" attributeType="String"/>
|
||||
<attribute name="subnet" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiMode" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiPsk" optional="YES" attributeType="String" minValueString="0" maxValueString="60"/>
|
||||
<attribute name="wifiSsid" optional="YES" attributeType="String" minValueString="0" maxValueString="30"/>
|
||||
<relationship name="networkConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="networkConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="NodeInfoEntity" representedClassName="NodeInfoEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bleName" optional="YES" attributeType="String"/>
|
||||
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="detection" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="environment" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="peripheralId" optional="YES" attributeType="String"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="viaMqtt" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="ambientLightingConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AmbientLightingConfigEntity" inverseName="ambientLightingConfigNode" inverseEntity="AmbientLightingConfigEntity"/>
|
||||
<relationship name="bluetoothConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="BluetoothConfigEntity" inverseName="bluetoothConfigNode" inverseEntity="BluetoothConfigEntity"/>
|
||||
<relationship name="cannedMessageConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CannedMessageConfigEntity" inverseName="cannedMessagesConfigNode" inverseEntity="CannedMessageConfigEntity"/>
|
||||
<relationship name="detectionSensorConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DetectionSensorConfigEntity" inverseName="detectionSensorConfigNode" inverseEntity="DetectionSensorConfigEntity"/>
|
||||
<relationship name="deviceConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceConfigEntity" inverseName="deviceConfigNode" inverseEntity="DeviceConfigEntity"/>
|
||||
<relationship name="displayConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DisplayConfigEntity" inverseName="displayConfigNode" inverseEntity="DisplayConfigEntity"/>
|
||||
<relationship name="externalNotificationConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ExternalNotificationConfigEntity" inverseName="externalNotificationConfigNode" inverseEntity="ExternalNotificationConfigEntity"/>
|
||||
<relationship name="loRaConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LoRaConfigEntity" inverseName="loRaConfigNode" inverseEntity="LoRaConfigEntity"/>
|
||||
<relationship name="metadata" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceMetadataEntity" inverseName="metadataNode" inverseEntity="DeviceMetadataEntity"/>
|
||||
<relationship name="mqttConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MQTTConfigEntity" inverseName="mqttConfigNode" inverseEntity="MQTTConfigEntity"/>
|
||||
<relationship name="myInfo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="myInfoNode" inverseEntity="MyInfoEntity"/>
|
||||
<relationship name="networkConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NetworkConfigEntity" inverseName="networkConfigNode" inverseEntity="NetworkConfigEntity"/>
|
||||
<relationship name="positionConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PositionConfigEntity" inverseName="positionConfigNode" inverseEntity="PositionConfigEntity"/>
|
||||
<relationship name="positions" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PositionEntity" inverseName="nodePosition" inverseEntity="PositionEntity"/>
|
||||
<relationship name="rangeTestConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RangeTestConfigEntity" inverseName="rangeTestConfigNode" inverseEntity="RangeTestConfigEntity"/>
|
||||
<relationship name="rtttlConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RTTTLConfigEntity" inverseName="rtttlConfigNode" inverseEntity="RTTTLConfigEntity"/>
|
||||
<relationship name="serialConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SerialConfigEntity" inverseName="serialConfigNode" inverseEntity="SerialConfigEntity"/>
|
||||
<relationship name="storeForwardConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreForwardConfigEntity" inverseName="storeForwardConfigNode" inverseEntity="StoreForwardConfigEntity"/>
|
||||
<relationship name="telemetries" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TelemetryEntity" inverseName="nodeTelemetry" inverseEntity="TelemetryEntity"/>
|
||||
<relationship name="telemetryConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TelemetryConfigEntity" inverseName="telemetryConfigNode" inverseEntity="TelemetryConfigEntity"/>
|
||||
<relationship name="traceRoutes" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteEntity" inverseName="node" inverseEntity="TraceRouteEntity"/>
|
||||
<relationship name="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="userNode" inverseEntity="UserEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="num"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PositionConfigEntity" representedClassName="PositionConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="broadcastSmartMinimumDistance" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="broadcastSmartMinimumIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceGpsEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="fixedPosition" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsAttemptTime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsEnGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="positionBroadcastSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rxGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="smartPositionEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="txGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="positionConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positionConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PositionEntity" representedClassName="PositionEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latest" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="satsInView" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="seqNo" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="nodePosition" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positions" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="RangeTestConfigEntity" representedClassName="RangeTestConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="save" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="sender" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<relationship name="rangeTestConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rangeTestConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="RouteEntity" representedClassName="RouteEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="color" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="notes" optional="YES" attributeType="String"/>
|
||||
<relationship name="locations" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="LocationEntity" inverseName="routeLocation" inverseEntity="LocationEntity"/>
|
||||
</entity>
|
||||
<entity name="RTTTLConfigEntity" representedClassName="RTTTLConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ringtone" optional="YES" attributeType="String" maxValueString="228" defaultValueString=""/>
|
||||
<relationship name="rtttlConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rtttlConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="SerialConfigEntity" representedClassName="SerialConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="baudRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="echo" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideConsoleSerialPort" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="rxd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="timeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="txd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="serialConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="serialConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="StoreForwardConfigEntity" representedClassName="StoreForwardConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="heartbeat" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="historyReturnMax" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="historyReturnWindow" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isRouter" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastHeartbeat" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="lastRequest" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="records" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="storeForwardConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="storeForwardConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TelemetryConfigEntity" representedClassName="TelemetryConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="deviceUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentDisplayFahrenheit" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentMeasurementEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="telemetryConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetryConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TelemetryEntity" representedClassName="TelemetryEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="airUtilTx" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="barometricPressure" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="batteryLevel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="channelUtilization" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="current" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="gasResistance" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="metricsType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="relativeHumidity" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="temperature" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="voltage" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="nodeTelemetry" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetries" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TraceRouteEntity" representedClassName="TraceRouteEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hasPositions" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="response" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="route" optional="YES" attributeType="Transformable" customClassName="[UInt32]"/>
|
||||
<attribute name="routeText" optional="YES" attributeType="String"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="hops" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteHopEntity" inverseName="traceRoute" inverseEntity="TraceRouteHopEntity"/>
|
||||
<relationship name="node" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="traceRoutes" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TraceRouteHopEntity" representedClassName="TraceRouteHopEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="traceRoute" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TraceRouteEntity" inverseName="hops" inverseEntity="TraceRouteEntity"/>
|
||||
</entity>
|
||||
<entity name="UserEntity" representedClassName="UserEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hwModel" attributeType="String"/>
|
||||
<attribute name="isLicensed" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastMessage" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="longName" attributeType="String"/>
|
||||
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="shortName" attributeType="String"/>
|
||||
<attribute name="userId" attributeType="String"/>
|
||||
<attribute name="vip" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="receivedMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="toUser" inverseEntity="MessageEntity"/>
|
||||
<relationship name="sentMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="fromUser" inverseEntity="MessageEntity"/>
|
||||
<relationship name="userNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="user" inverseEntity="NodeInfoEntity"/>
|
||||
<fetchedProperty name="adminMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(fromUser.num == $FETCH_SOURCE.num) AND isEmoji == false AND admin = true"/>
|
||||
</fetchedProperty>
|
||||
<fetchedProperty name="allMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="((toUser.num == $FETCH_SOURCE.num) OR (fromUser.num == $FETCH_SOURCE.num)) AND toUser != nil AND fromUser != nil AND isEmoji == false AND admin = false AND portNum != 10 "/>
|
||||
</fetchedProperty>
|
||||
<fetchedProperty name="detectionSensorMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(fromUser.num == $FETCH_SOURCE.num) AND portNum = 10"/>
|
||||
</fetchedProperty>
|
||||
</entity>
|
||||
<entity name="WaypointEntity" representedClassName="WaypointEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="created" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="expire" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="icon" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="locked" attributeType="Integer 64" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="longDescription" optional="YES" attributeType="String" maxValueString="100"/>
|
||||
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String" minValueString="1" maxValueString="30"/>
|
||||
</entity>
|
||||
</model>
|
||||
|
|
@ -57,17 +57,14 @@ class PersistenceController {
|
|||
guard let url = self.container.persistentStoreDescriptions.first?.url else { return }
|
||||
|
||||
let persistentStoreCoordinator = self.container.persistentStoreCoordinator
|
||||
|
||||
do {
|
||||
|
||||
try persistentStoreCoordinator.destroyPersistentStore(at: url, ofType: NSSQLiteStoreType, options: nil)
|
||||
print("💥 CoreData database truncated. All app data has been erased.")
|
||||
|
||||
|
||||
do {
|
||||
try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil)
|
||||
} catch let error {
|
||||
print("💣 Failed to re-create CoreData database: " + error.localizedDescription)
|
||||
try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil)
|
||||
}
|
||||
|
||||
} catch let error {
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
newNode.snr = packet.rxSnr
|
||||
newNode.rssi = packet.rxRssi
|
||||
newNode.viaMqtt = packet.viaMqtt
|
||||
newNode.channel = Int32(packet.channel)
|
||||
if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) {
|
||||
newNode.channel = Int32(nodeInfoMessage.channel)
|
||||
print(packet.channel)
|
||||
|
|
@ -163,6 +164,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
fetchedNode[0].snr = packet.rxSnr
|
||||
fetchedNode[0].rssi = packet.rxRssi
|
||||
fetchedNode[0].viaMqtt = packet.viaMqtt
|
||||
fetchedNode[0].channel = Int32(packet.channel)
|
||||
|
||||
if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) {
|
||||
fetchedNode[0].channel = Int32(nodeInfoMessage.channel)
|
||||
|
|
@ -280,6 +282,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
fetchedNode[0].snr = packet.rxSnr
|
||||
fetchedNode[0].rssi = packet.rxRssi
|
||||
fetchedNode[0].viaMqtt = packet.viaMqtt
|
||||
fetchedNode[0].channel = Int32(packet.channel)
|
||||
fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet
|
||||
|
||||
do {
|
||||
|
|
@ -369,7 +372,7 @@ func upsertDeviceConfigPacket(config: Meshtastic.Config.DeviceConfig, nodeNum: I
|
|||
newDeviceConfig.buttonGpio = Int32(config.buttonGpio)
|
||||
newDeviceConfig.buzzerGpio = Int32(config.buzzerGpio)
|
||||
newDeviceConfig.rebroadcastMode = Int32(config.rebroadcastMode.rawValue)
|
||||
newDeviceConfig.nodeInfoBroadcastSecs = Int32(config.nodeInfoBroadcastSecs)
|
||||
newDeviceConfig.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber)
|
||||
newDeviceConfig.doubleTapAsButtonPress = config.doubleTapAsButtonPress
|
||||
newDeviceConfig.isManaged = config.isManaged
|
||||
fetchedNode[0].deviceConfig = newDeviceConfig
|
||||
|
|
@ -380,7 +383,7 @@ func upsertDeviceConfigPacket(config: Meshtastic.Config.DeviceConfig, nodeNum: I
|
|||
fetchedNode[0].deviceConfig?.buttonGpio = Int32(config.buttonGpio)
|
||||
fetchedNode[0].deviceConfig?.buzzerGpio = Int32(config.buzzerGpio)
|
||||
fetchedNode[0].deviceConfig?.rebroadcastMode = Int32(config.rebroadcastMode.rawValue)
|
||||
fetchedNode[0].deviceConfig?.nodeInfoBroadcastSecs = Int32(config.nodeInfoBroadcastSecs)
|
||||
fetchedNode[0].deviceConfig?.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber)
|
||||
fetchedNode[0].deviceConfig?.doubleTapAsButtonPress = config.doubleTapAsButtonPress
|
||||
fetchedNode[0].deviceConfig?.isManaged = config.isManaged
|
||||
}
|
||||
|
|
@ -618,7 +621,7 @@ func upsertPositionConfigPacket(config: Meshtastic.Config.PositionConfig, nodeNu
|
|||
fetchedNode[0].positionConfig?.txGpio = Int32(config.txGpio)
|
||||
fetchedNode[0].positionConfig?.gpsEnGpio = Int32(config.gpsEnGpio)
|
||||
fetchedNode[0].positionConfig?.fixedPosition = config.fixedPosition
|
||||
fetchedNode[0].positionConfig?.positionBroadcastSeconds = Int32(config.positionBroadcastSecs)
|
||||
fetchedNode[0].positionConfig?.positionBroadcastSeconds = Int32(truncatingIfNeeded: config.positionBroadcastSecs)
|
||||
fetchedNode[0].positionConfig?.broadcastSmartMinimumIntervalSecs = Int32(config.broadcastSmartMinimumIntervalSecs)
|
||||
fetchedNode[0].positionConfig?.broadcastSmartMinimumDistance = Int32(config.broadcastSmartMinimumDistance)
|
||||
fetchedNode[0].positionConfig?.gpsAttemptTime = 900
|
||||
|
|
|
|||
|
|
@ -198,64 +198,63 @@ struct Config {
|
|||
typealias RawValue = Int
|
||||
|
||||
///
|
||||
/// Client device role
|
||||
/// Description: App connected or stand alone messaging device.
|
||||
/// Technical Details: Default Role
|
||||
case client // = 0
|
||||
|
||||
///
|
||||
/// Client Mute device role
|
||||
/// Same as a client except packets will not hop over this node, does not contribute to routing packets for mesh.
|
||||
/// Description: Device that does not forward packets from other devices.
|
||||
case clientMute // = 1
|
||||
|
||||
///
|
||||
/// Router device role.
|
||||
/// Mesh packets will prefer to be routed over this node. This node will not be used by client apps.
|
||||
/// The wifi/ble radios and the oled screen will be put to sleep.
|
||||
/// Description: Infrastructure node for extending network coverage by relaying messages. Visible in Nodes list.
|
||||
/// Technical Details: Mesh packets will prefer to be routed over this node. This node will not be used by client apps.
|
||||
/// The wifi radio and the oled screen will be put to sleep.
|
||||
/// This mode may still potentially have higher power usage due to it's preference in message rebroadcasting on the mesh.
|
||||
case router // = 2
|
||||
|
||||
///
|
||||
/// Router Client device role
|
||||
/// Mesh packets will prefer to be routed over this node. The Router Client can be used as both a Router and an app connected Client.
|
||||
/// Description: Combination of both ROUTER and CLIENT. Not for mobile devices.
|
||||
case routerClient // = 3
|
||||
|
||||
///
|
||||
/// Repeater device role
|
||||
/// Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry
|
||||
/// Description: Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list.
|
||||
/// Technical Details: Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry
|
||||
/// or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate.
|
||||
case repeater // = 4
|
||||
|
||||
///
|
||||
/// Tracker device role
|
||||
/// Position Mesh packets will be prioritized higher and sent more frequently by default.
|
||||
/// Description: Broadcasts GPS position packets as priority.
|
||||
/// Technical Details: Position Mesh packets will be prioritized higher and sent more frequently by default.
|
||||
/// When used in conjunction with power.is_power_saving = true, nodes will wake up,
|
||||
/// send position, and then sleep for position.position_broadcast_secs seconds.
|
||||
case tracker // = 5
|
||||
|
||||
///
|
||||
/// Sensor device role
|
||||
/// Telemetry Mesh packets will be prioritized higher and sent more frequently by default.
|
||||
/// Description: Broadcasts telemetry packets as priority.
|
||||
/// Technical Details: Telemetry Mesh packets will be prioritized higher and sent more frequently by default.
|
||||
/// When used in conjunction with power.is_power_saving = true, nodes will wake up,
|
||||
/// send environment telemetry, and then sleep for telemetry.environment_update_interval seconds.
|
||||
case sensor // = 6
|
||||
|
||||
///
|
||||
/// TAK device role
|
||||
/// Used for nodes dedicated for connection to an ATAK EUD.
|
||||
/// Description: Optimized for ATAK system communication, reduces routine broadcasts.
|
||||
/// Technical Details: Used for nodes dedicated for connection to an ATAK EUD.
|
||||
/// Turns off many of the routine broadcasts to favor CoT packet stream
|
||||
/// from the Meshtastic ATAK plugin -> IMeshService -> Node
|
||||
case tak // = 7
|
||||
|
||||
///
|
||||
/// Client Hidden device role
|
||||
/// Used for nodes that "only speak when spoken to"
|
||||
/// Description: Device that only broadcasts as needed for stealth or power savings.
|
||||
/// Technical Details: Used for nodes that "only speak when spoken to"
|
||||
/// Turns all of the routine broadcasts but allows for ad-hoc communication
|
||||
/// Still rebroadcasts, but with local only rebroadcast mode (known meshes only)
|
||||
/// Can be used for clandestine operation or to dramatically reduce airtime / power consumption
|
||||
case clientHidden // = 8
|
||||
|
||||
///
|
||||
/// Lost and Found device role
|
||||
/// Used to automatically send a text message to the mesh
|
||||
/// Description: Broadcasts location as message to default channel regularly for to assist with device recovery.
|
||||
/// Technical Details: Used to automatically send a text message to the mesh
|
||||
/// with the current position of the device on a frequent interval:
|
||||
/// "I'm lost! Position: lat / long"
|
||||
case lostAndFound // = 9
|
||||
|
|
@ -419,6 +418,10 @@ struct Config {
|
|||
/// Set where GPS is enabled, disabled, or not present
|
||||
var gpsMode: Config.PositionConfig.GpsMode = .disabled
|
||||
|
||||
///
|
||||
/// Set GPS precision in bits per channel, or 0 for disabled
|
||||
var channelPrecision: [UInt32] = []
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
///
|
||||
|
|
@ -1168,6 +1171,10 @@ struct Config {
|
|||
///
|
||||
/// Malaysia 919mhz
|
||||
case my919 // = 17
|
||||
|
||||
///
|
||||
/// Singapore 923mhz
|
||||
case sg923 // = 18
|
||||
case UNRECOGNIZED(Int)
|
||||
|
||||
init() {
|
||||
|
|
@ -1194,6 +1201,7 @@ struct Config {
|
|||
case 15: self = .ua868
|
||||
case 16: self = .my433
|
||||
case 17: self = .my919
|
||||
case 18: self = .sg923
|
||||
default: self = .UNRECOGNIZED(rawValue)
|
||||
}
|
||||
}
|
||||
|
|
@ -1218,6 +1226,7 @@ struct Config {
|
|||
case .ua868: return 15
|
||||
case .my433: return 16
|
||||
case .my919: return 17
|
||||
case .sg923: return 18
|
||||
case .UNRECOGNIZED(let i): return i
|
||||
}
|
||||
}
|
||||
|
|
@ -1488,6 +1497,7 @@ extension Config.LoRaConfig.RegionCode: CaseIterable {
|
|||
.ua868,
|
||||
.my433,
|
||||
.my919,
|
||||
.sg923,
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -1831,6 +1841,7 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
11: .standard(proto: "broadcast_smart_minimum_interval_secs"),
|
||||
12: .standard(proto: "gps_en_gpio"),
|
||||
13: .standard(proto: "gps_mode"),
|
||||
14: .standard(proto: "channel_precision"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
|
|
@ -1852,6 +1863,7 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
case 11: try { try decoder.decodeSingularUInt32Field(value: &self.broadcastSmartMinimumIntervalSecs) }()
|
||||
case 12: try { try decoder.decodeSingularUInt32Field(value: &self.gpsEnGpio) }()
|
||||
case 13: try { try decoder.decodeSingularEnumField(value: &self.gpsMode) }()
|
||||
case 14: try { try decoder.decodeRepeatedUInt32Field(value: &self.channelPrecision) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
|
@ -1897,6 +1909,9 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
if self.gpsMode != .disabled {
|
||||
try visitor.visitSingularEnumField(value: self.gpsMode, fieldNumber: 13)
|
||||
}
|
||||
if !self.channelPrecision.isEmpty {
|
||||
try visitor.visitPackedUInt32Field(value: self.channelPrecision, fieldNumber: 14)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
|
@ -1914,6 +1929,7 @@ extension Config.PositionConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageIm
|
|||
if lhs.broadcastSmartMinimumIntervalSecs != rhs.broadcastSmartMinimumIntervalSecs {return false}
|
||||
if lhs.gpsEnGpio != rhs.gpsEnGpio {return false}
|
||||
if lhs.gpsMode != rhs.gpsMode {return false}
|
||||
if lhs.channelPrecision != rhs.channelPrecision {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
|
@ -2416,6 +2432,7 @@ extension Config.LoRaConfig.RegionCode: SwiftProtobuf._ProtoNameProviding {
|
|||
15: .same(proto: "UA_868"),
|
||||
16: .same(proto: "MY_433"),
|
||||
17: .same(proto: "MY_919"),
|
||||
18: .same(proto: "SG_923"),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -130,6 +130,10 @@ enum HardwareModel: SwiftProtobuf.Enum {
|
|||
/// Canary Radio Company - CanaryOne: https://canaryradio.io/products/canaryone
|
||||
case canaryone // = 29
|
||||
|
||||
///
|
||||
/// Waveshare RP2040 LoRa - https://www.waveshare.com/rp2040-lora.htm
|
||||
case rp2040Lora // = 30
|
||||
|
||||
///
|
||||
/// ---------------------------------------------------------------------------
|
||||
/// Less common/prototype boards listed here (needs one more byte over the air)
|
||||
|
|
@ -198,7 +202,8 @@ enum HardwareModel: SwiftProtobuf.Enum {
|
|||
|
||||
///
|
||||
/// Heltec Wireless Tracker with ESP32-S3 CPU, built-in GPS, and TFT
|
||||
case heltecWirelessTracker // = 48
|
||||
/// Newer V1.1, version is written on the PCB near the display.
|
||||
case heltecWirelessTrackerV11 // = 48
|
||||
|
||||
///
|
||||
/// Heltec Wireless Paper with ESP32-S3 CPU and E-Ink display
|
||||
|
|
@ -242,6 +247,11 @@ enum HardwareModel: SwiftProtobuf.Enum {
|
|||
/// Flex connector marking is FPC-7528B
|
||||
case heltecWirelessPaperV10 // = 57
|
||||
|
||||
///
|
||||
/// Heltec Wireless Tracker with ESP32-S3 CPU, built-in GPS, and TFT
|
||||
/// Older "V1.0" Variant
|
||||
case heltecWirelessTrackerV10 // = 58
|
||||
|
||||
///
|
||||
/// ------------------------------------------------------------------------------------------------------------------------------------------
|
||||
/// 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.
|
||||
|
|
@ -280,6 +290,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
|
|||
case 27: self = .senseloraRp2040
|
||||
case 28: self = .senseloraS3
|
||||
case 29: self = .canaryone
|
||||
case 30: self = .rp2040Lora
|
||||
case 32: self = .loraRelayV1
|
||||
case 33: self = .nrf52840Dk
|
||||
case 34: self = .ppr
|
||||
|
|
@ -296,7 +307,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
|
|||
case 45: self = .betafpv2400Tx
|
||||
case 46: self = .betafpv900NanoTx
|
||||
case 47: self = .rpiPico
|
||||
case 48: self = .heltecWirelessTracker
|
||||
case 48: self = .heltecWirelessTrackerV11
|
||||
case 49: self = .heltecWirelessPaper
|
||||
case 50: self = .tDeck
|
||||
case 51: self = .tWatchS3
|
||||
|
|
@ -306,6 +317,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
|
|||
case 55: self = .esp32S3Pico
|
||||
case 56: self = .chatter2
|
||||
case 57: self = .heltecWirelessPaperV10
|
||||
case 58: self = .heltecWirelessTrackerV10
|
||||
case 255: self = .privateHw
|
||||
default: self = .UNRECOGNIZED(rawValue)
|
||||
}
|
||||
|
|
@ -338,6 +350,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
|
|||
case .senseloraRp2040: return 27
|
||||
case .senseloraS3: return 28
|
||||
case .canaryone: return 29
|
||||
case .rp2040Lora: return 30
|
||||
case .loraRelayV1: return 32
|
||||
case .nrf52840Dk: return 33
|
||||
case .ppr: return 34
|
||||
|
|
@ -354,7 +367,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
|
|||
case .betafpv2400Tx: return 45
|
||||
case .betafpv900NanoTx: return 46
|
||||
case .rpiPico: return 47
|
||||
case .heltecWirelessTracker: return 48
|
||||
case .heltecWirelessTrackerV11: return 48
|
||||
case .heltecWirelessPaper: return 49
|
||||
case .tDeck: return 50
|
||||
case .tWatchS3: return 51
|
||||
|
|
@ -364,6 +377,7 @@ enum HardwareModel: SwiftProtobuf.Enum {
|
|||
case .esp32S3Pico: return 55
|
||||
case .chatter2: return 56
|
||||
case .heltecWirelessPaperV10: return 57
|
||||
case .heltecWirelessTrackerV10: return 58
|
||||
case .privateHw: return 255
|
||||
case .UNRECOGNIZED(let i): return i
|
||||
}
|
||||
|
|
@ -401,6 +415,7 @@ extension HardwareModel: CaseIterable {
|
|||
.senseloraRp2040,
|
||||
.senseloraS3,
|
||||
.canaryone,
|
||||
.rp2040Lora,
|
||||
.loraRelayV1,
|
||||
.nrf52840Dk,
|
||||
.ppr,
|
||||
|
|
@ -417,7 +432,7 @@ extension HardwareModel: CaseIterable {
|
|||
.betafpv2400Tx,
|
||||
.betafpv900NanoTx,
|
||||
.rpiPico,
|
||||
.heltecWirelessTracker,
|
||||
.heltecWirelessTrackerV11,
|
||||
.heltecWirelessPaper,
|
||||
.tDeck,
|
||||
.tWatchS3,
|
||||
|
|
@ -427,6 +442,7 @@ extension HardwareModel: CaseIterable {
|
|||
.esp32S3Pico,
|
||||
.chatter2,
|
||||
.heltecWirelessPaperV10,
|
||||
.heltecWirelessTrackerV10,
|
||||
.privateHw,
|
||||
]
|
||||
}
|
||||
|
|
@ -2589,6 +2605,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding {
|
|||
27: .same(proto: "SENSELORA_RP2040"),
|
||||
28: .same(proto: "SENSELORA_S3"),
|
||||
29: .same(proto: "CANARYONE"),
|
||||
30: .same(proto: "RP2040_LORA"),
|
||||
32: .same(proto: "LORA_RELAY_V1"),
|
||||
33: .same(proto: "NRF52840DK"),
|
||||
34: .same(proto: "PPR"),
|
||||
|
|
@ -2605,7 +2622,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding {
|
|||
45: .same(proto: "BETAFPV_2400_TX"),
|
||||
46: .same(proto: "BETAFPV_900_NANO_TX"),
|
||||
47: .same(proto: "RPI_PICO"),
|
||||
48: .same(proto: "HELTEC_WIRELESS_TRACKER"),
|
||||
48: .same(proto: "HELTEC_WIRELESS_TRACKER_V1_1"),
|
||||
49: .same(proto: "HELTEC_WIRELESS_PAPER"),
|
||||
50: .same(proto: "T_DECK"),
|
||||
51: .same(proto: "T_WATCH_S3"),
|
||||
|
|
@ -2615,6 +2632,7 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding {
|
|||
55: .same(proto: "ESP32_S3_PICO"),
|
||||
56: .same(proto: "CHATTER_2"),
|
||||
57: .same(proto: "HELTEC_WIRELESS_PAPER_V1_0"),
|
||||
58: .same(proto: "HELTEC_WIRELESS_TRACKER_V1_0"),
|
||||
255: .same(proto: "PRIVATE_HW"),
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,6 +176,11 @@ enum PortNum: SwiftProtobuf.Enum {
|
|||
/// ENCODING: Protobuf
|
||||
case neighborinfoApp // = 71
|
||||
|
||||
///
|
||||
/// ATAK Plugin
|
||||
/// Portnum for payloads from the official Meshtastic ATAK plugin
|
||||
case atakPlugin // = 72
|
||||
|
||||
///
|
||||
/// Private applications should use portnums >= 256.
|
||||
/// To simplify initial development and testing you can use "PRIVATE_APP"
|
||||
|
|
@ -220,6 +225,7 @@ enum PortNum: SwiftProtobuf.Enum {
|
|||
case 69: self = .simulatorApp
|
||||
case 70: self = .tracerouteApp
|
||||
case 71: self = .neighborinfoApp
|
||||
case 72: self = .atakPlugin
|
||||
case 256: self = .privateApp
|
||||
case 257: self = .atakForwarder
|
||||
case 511: self = .max
|
||||
|
|
@ -251,6 +257,7 @@ enum PortNum: SwiftProtobuf.Enum {
|
|||
case .simulatorApp: return 69
|
||||
case .tracerouteApp: return 70
|
||||
case .neighborinfoApp: return 71
|
||||
case .atakPlugin: return 72
|
||||
case .privateApp: return 256
|
||||
case .atakForwarder: return 257
|
||||
case .max: return 511
|
||||
|
|
@ -287,6 +294,7 @@ extension PortNum: CaseIterable {
|
|||
.simulatorApp,
|
||||
.tracerouteApp,
|
||||
.neighborinfoApp,
|
||||
.atakPlugin,
|
||||
.privateApp,
|
||||
.atakForwarder,
|
||||
.max,
|
||||
|
|
@ -325,6 +333,7 @@ extension PortNum: SwiftProtobuf._ProtoNameProviding {
|
|||
69: .same(proto: "SIMULATOR_APP"),
|
||||
70: .same(proto: "TRACEROUTE_APP"),
|
||||
71: .same(proto: "NEIGHBORINFO_APP"),
|
||||
72: .same(proto: "ATAK_PLUGIN"),
|
||||
256: .same(proto: "PRIVATE_APP"),
|
||||
257: .same(proto: "ATAK_FORWARDER"),
|
||||
511: .same(proto: "MAX"),
|
||||
|
|
|
|||
|
|
@ -66,13 +66,13 @@ struct StoreAndForward {
|
|||
}
|
||||
|
||||
///
|
||||
/// Empty Payload
|
||||
var empty: Bool {
|
||||
/// Text from history message.
|
||||
var text: Data {
|
||||
get {
|
||||
if case .empty(let v)? = variant {return v}
|
||||
return false
|
||||
if case .text(let v)? = variant {return v}
|
||||
return Data()
|
||||
}
|
||||
set {variant = .empty(newValue)}
|
||||
set {variant = .text(newValue)}
|
||||
}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
|
@ -90,8 +90,8 @@ struct StoreAndForward {
|
|||
/// TODO: REPLACE
|
||||
case heartbeat(StoreAndForward.Heartbeat)
|
||||
///
|
||||
/// Empty Payload
|
||||
case empty(Bool)
|
||||
/// Text from history message.
|
||||
case text(Data)
|
||||
|
||||
#if !swift(>=4.1)
|
||||
static func ==(lhs: StoreAndForward.OneOf_Variant, rhs: StoreAndForward.OneOf_Variant) -> Bool {
|
||||
|
|
@ -111,8 +111,8 @@ struct StoreAndForward {
|
|||
guard case .heartbeat(let l) = lhs, case .heartbeat(let r) = rhs else { preconditionFailure() }
|
||||
return l == r
|
||||
}()
|
||||
case (.empty, .empty): return {
|
||||
guard case .empty(let l) = lhs, case .empty(let r) = rhs else { preconditionFailure() }
|
||||
case (.text, .text): return {
|
||||
guard case .text(let l) = lhs, case .text(let r) = rhs else { preconditionFailure() }
|
||||
return l == r
|
||||
}()
|
||||
default: return false
|
||||
|
|
@ -160,6 +160,14 @@ struct StoreAndForward {
|
|||
/// Router is responding to a request for stats.
|
||||
case routerStats // = 7
|
||||
|
||||
///
|
||||
/// Router sends a text message from its history that was a direct message.
|
||||
case routerTextDirect // = 8
|
||||
|
||||
///
|
||||
/// Router sends a text message from its history that was a broadcast.
|
||||
case routerTextBroadcast // = 9
|
||||
|
||||
///
|
||||
/// Client is an in error state.
|
||||
case clientError // = 64
|
||||
|
|
@ -200,6 +208,8 @@ struct StoreAndForward {
|
|||
case 5: self = .routerBusy
|
||||
case 6: self = .routerHistory
|
||||
case 7: self = .routerStats
|
||||
case 8: self = .routerTextDirect
|
||||
case 9: self = .routerTextBroadcast
|
||||
case 64: self = .clientError
|
||||
case 65: self = .clientHistory
|
||||
case 66: self = .clientStats
|
||||
|
|
@ -220,6 +230,8 @@ struct StoreAndForward {
|
|||
case .routerBusy: return 5
|
||||
case .routerHistory: return 6
|
||||
case .routerStats: return 7
|
||||
case .routerTextDirect: return 8
|
||||
case .routerTextBroadcast: return 9
|
||||
case .clientError: return 64
|
||||
case .clientHistory: return 65
|
||||
case .clientStats: return 66
|
||||
|
|
@ -268,11 +280,11 @@ struct StoreAndForward {
|
|||
var heartbeat: Bool = false
|
||||
|
||||
///
|
||||
/// Is the heartbeat enabled on the server?
|
||||
/// Maximum number of messages the server will return.
|
||||
var returnMax: UInt32 = 0
|
||||
|
||||
///
|
||||
/// Is the heartbeat enabled on the server?
|
||||
/// Maximum history window in minutes the server will return messages from.
|
||||
var returnWindow: UInt32 = 0
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
|
@ -296,7 +308,8 @@ struct StoreAndForward {
|
|||
var window: UInt32 = 0
|
||||
|
||||
///
|
||||
/// The window of messages that was used to filter the history client requested
|
||||
/// Index in the packet history of the last message sent in a previous request to the server.
|
||||
/// Will be sent to the client before sending the history and can be set in a subsequent request to avoid getting packets the server already sent to the client.
|
||||
var lastRequest: UInt32 = 0
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
|
@ -312,7 +325,7 @@ struct StoreAndForward {
|
|||
// methods supported on all messages.
|
||||
|
||||
///
|
||||
/// Number of that will be sent to the client
|
||||
/// Period in seconds that the heartbeat is sent out that will be sent to the client
|
||||
var period: UInt32 = 0
|
||||
|
||||
///
|
||||
|
|
@ -340,6 +353,8 @@ extension StoreAndForward.RequestResponse: CaseIterable {
|
|||
.routerBusy,
|
||||
.routerHistory,
|
||||
.routerStats,
|
||||
.routerTextDirect,
|
||||
.routerTextBroadcast,
|
||||
.clientError,
|
||||
.clientHistory,
|
||||
.clientStats,
|
||||
|
|
@ -371,7 +386,7 @@ extension StoreAndForward: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen
|
|||
2: .same(proto: "stats"),
|
||||
3: .same(proto: "history"),
|
||||
4: .same(proto: "heartbeat"),
|
||||
5: .same(proto: "empty"),
|
||||
5: .same(proto: "text"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
|
|
@ -421,11 +436,11 @@ extension StoreAndForward: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen
|
|||
}
|
||||
}()
|
||||
case 5: try {
|
||||
var v: Bool?
|
||||
try decoder.decodeSingularBoolField(value: &v)
|
||||
var v: Data?
|
||||
try decoder.decodeSingularBytesField(value: &v)
|
||||
if let v = v {
|
||||
if self.variant != nil {try decoder.handleConflictingOneOf()}
|
||||
self.variant = .empty(v)
|
||||
self.variant = .text(v)
|
||||
}
|
||||
}()
|
||||
default: break
|
||||
|
|
@ -454,9 +469,9 @@ extension StoreAndForward: SwiftProtobuf.Message, SwiftProtobuf._MessageImplemen
|
|||
guard case .heartbeat(let v)? = self.variant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 4)
|
||||
}()
|
||||
case .empty?: try {
|
||||
guard case .empty(let v)? = self.variant else { preconditionFailure() }
|
||||
try visitor.visitSingularBoolField(value: v, fieldNumber: 5)
|
||||
case .text?: try {
|
||||
guard case .text(let v)? = self.variant else { preconditionFailure() }
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 5)
|
||||
}()
|
||||
case nil: break
|
||||
}
|
||||
|
|
@ -481,6 +496,8 @@ extension StoreAndForward.RequestResponse: SwiftProtobuf._ProtoNameProviding {
|
|||
5: .same(proto: "ROUTER_BUSY"),
|
||||
6: .same(proto: "ROUTER_HISTORY"),
|
||||
7: .same(proto: "ROUTER_STATS"),
|
||||
8: .same(proto: "ROUTER_TEXT_DIRECT"),
|
||||
9: .same(proto: "ROUTER_TEXT_BROADCAST"),
|
||||
64: .same(proto: "CLIENT_ERROR"),
|
||||
65: .same(proto: "CLIENT_HISTORY"),
|
||||
66: .same(proto: "CLIENT_STATS"),
|
||||
|
|
|
|||
|
|
@ -216,7 +216,14 @@ struct Connect: View {
|
|||
if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == CBPeripheralState.connected {
|
||||
bleManager.disconnectPeripheral()
|
||||
}
|
||||
PersistenceController.shared.clearDatabase()
|
||||
context.reset()
|
||||
do {
|
||||
PersistenceController.shared.clearDatabase()
|
||||
|
||||
} catch let error {
|
||||
print("💣 Failed to re-create CoreData database: " + error.localizedDescription)
|
||||
}
|
||||
|
||||
let radio = bleManager.peripherals.first(where: { $0.peripheral.identifier.uuidString == selectedPeripherialId })
|
||||
if radio != nil {
|
||||
bleManager.connectTo(peripheral: radio!.peripheral)
|
||||
|
|
|
|||
|
|
@ -62,12 +62,15 @@ struct BatteryLevelCompact: View {
|
|||
}
|
||||
if batteryLevel > 100 {
|
||||
Text("PWD")
|
||||
.foregroundStyle(.gray)
|
||||
.font(font)
|
||||
} else if batteryLevel == 100 {
|
||||
Text("CHG")
|
||||
.foregroundStyle(.gray)
|
||||
.font(font)
|
||||
} else {
|
||||
Text("\(batteryLevel)%")
|
||||
.foregroundStyle(.gray)
|
||||
.font(font)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,13 +33,15 @@ struct LoRaSignalStrengthMeter: View {
|
|||
Gauge(value: Double(signalStrength.rawValue), in: 0...3) {
|
||||
} currentValueLabel: {
|
||||
Image(systemName: "dot.radiowaves.left.and.right")
|
||||
.font(.callout)
|
||||
.font(.caption)
|
||||
Text("Signal \(signalStrength.description)")
|
||||
.font(.callout)
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.gaugeStyle(.accessoryLinear)
|
||||
.tint(gradient)
|
||||
.font(.caption)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,27 +9,18 @@ import SwiftUI
|
|||
import CoreData
|
||||
|
||||
struct ChannelMessageList: View {
|
||||
|
||||
@StateObject var appState = AppState.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
enum Field: Hashable {
|
||||
case messageText
|
||||
}
|
||||
|
||||
// Keyboard State
|
||||
@State var typingMessage: String = ""
|
||||
@State private var totalBytes = 0
|
||||
var maxbytes = 228
|
||||
@FocusState var focusedField: Field?
|
||||
@FocusState var messageFieldFocused: Bool
|
||||
|
||||
@ObservedObject var myInfo: MyInfoEntity
|
||||
@ObservedObject var channel: ChannelEntity
|
||||
@State var showDeleteMessageAlert = false
|
||||
@State private var deleteMessageId: Int64 = 0
|
||||
@State private var replyMessageId: Int64 = 0
|
||||
@State private var sendPositionWithMessage: Bool = false
|
||||
@AppStorage("preferredPeripheralNum") private var preferredPeripheralNum = -1
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -115,7 +106,7 @@ struct ChannelMessageList: View {
|
|||
}
|
||||
Button(action: {
|
||||
self.replyMessageId = message.messageId
|
||||
self.focusedField = .messageText
|
||||
self.messageFieldFocused = true
|
||||
print("I want to reply to \(message.messageId)")
|
||||
}) {
|
||||
Text("reply")
|
||||
|
|
@ -233,6 +224,11 @@ struct ChannelMessageList: View {
|
|||
}
|
||||
.padding(.bottom)
|
||||
.id(channel.allPrivateMessages.firstIndex(of: message))
|
||||
|
||||
if currentUser && (message.ackError == 5 || message.ackError == 3) {
|
||||
RetryButton(message: message)
|
||||
}
|
||||
|
||||
if !currentUser {
|
||||
Spacer(minLength: 50)
|
||||
}
|
||||
|
|
@ -288,134 +284,14 @@ struct ChannelMessageList: View {
|
|||
}
|
||||
})
|
||||
}
|
||||
#if targetEnvironment(macCatalyst)
|
||||
HStack {
|
||||
Spacer()
|
||||
Button {
|
||||
let bell = "🔔 Alert Bell Character! \u{7}"
|
||||
print(bell)
|
||||
typingMessage += bell
|
||||
|
||||
} label: {
|
||||
Text("Alert Bell")
|
||||
Image(systemName: "bell.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
}
|
||||
Spacer()
|
||||
Button {
|
||||
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
|
||||
sendPositionWithMessage = true
|
||||
typingMessage += "📍 " + userLongName + " has shared their position with you."
|
||||
} label: {
|
||||
Text("share.position")
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
}
|
||||
ProgressView("\("bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes))
|
||||
.frame(width: 130)
|
||||
.padding(5)
|
||||
.font(.subheadline)
|
||||
.accentColor(.accentColor)
|
||||
.padding(.trailing)
|
||||
|
||||
TextMessageField(
|
||||
destination: .channel(channel.index),
|
||||
replyMessageId: $replyMessageId,
|
||||
isFocused: $messageFieldFocused
|
||||
) {
|
||||
context.refresh(channel, mergeChanges: true)
|
||||
}
|
||||
#endif
|
||||
HStack(alignment: .top) {
|
||||
|
||||
ZStack {
|
||||
TextField("message", text: $typingMessage, axis: .vertical)
|
||||
.onChange(of: typingMessage, perform: { value in
|
||||
totalBytes = value.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > maxbytes {
|
||||
let firstNBytes = Data(typingMessage.utf8.prefix(maxbytes))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the message back to the last place where it was the right size
|
||||
typingMessage = maxBytesString
|
||||
} else {
|
||||
print("not a valid UTF-8 sequence")
|
||||
}
|
||||
}
|
||||
})
|
||||
.keyboardType(.default)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .keyboard) {
|
||||
Button("dismiss.keyboard") {
|
||||
focusedField = nil
|
||||
}
|
||||
.font(.subheadline)
|
||||
Spacer()
|
||||
Button {
|
||||
let bell = "🔔 Alert Bell Character! \u{7}"
|
||||
print(bell)
|
||||
typingMessage += bell
|
||||
|
||||
} label: {
|
||||
Text("Alert")
|
||||
Image(systemName: "bell.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
}
|
||||
Spacer()
|
||||
Button {
|
||||
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
|
||||
sendPositionWithMessage = true
|
||||
typingMessage = "📍 " + userLongName + " has shared their position with you."
|
||||
|
||||
} label: {
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
}
|
||||
|
||||
ProgressView("\("bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes))
|
||||
.frame(width: 130)
|
||||
.padding(5)
|
||||
.font(.subheadline)
|
||||
.accentColor(.accentColor)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.focused($focusedField, equals: .messageText)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(minHeight: 50)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.onSubmit {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
if bleManager.sendMessage(message: typingMessage, toUserNum: 0, channel: channel.index, isEmoji: false, replyID: replyMessageId) {
|
||||
typingMessage = ""
|
||||
focusedField = nil
|
||||
replyMessageId = 0
|
||||
if sendPositionWithMessage {
|
||||
if bleManager.sendPosition(channel: Int32(channel.index), destNum: Int64(bleManager.emptyNodeNum), wantResponse: false) {
|
||||
print("Location Sent")
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Text(typingMessage).opacity(0).padding(.all, 0)
|
||||
}
|
||||
.overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1))
|
||||
.padding(.bottom, 15)
|
||||
Button(action: {
|
||||
if bleManager.sendMessage(message: typingMessage, toUserNum: 0, channel: channel.index, isEmoji: false, replyID: replyMessageId) {
|
||||
typingMessage = ""
|
||||
focusedField = nil
|
||||
replyMessageId = 0
|
||||
if sendPositionWithMessage {
|
||||
if bleManager.sendPosition(channel: Int32(channel.index), destNum: Int64(bleManager.emptyNodeNum), wantResponse: false) {
|
||||
print("Location Sent")
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.accentColor)
|
||||
}
|
||||
|
||||
}
|
||||
.padding(.all, 15)
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
|
|
|
|||
54
Meshtastic/Views/Messages/RetryButton.swift
Normal file
54
Meshtastic/Views/Messages/RetryButton.swift
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import SwiftUI
|
||||
|
||||
struct RetryButton: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
let message: MessageEntity
|
||||
@State var isShowingConfirmation = false
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
isShowingConfirmation = true
|
||||
} label: {
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
.foregroundColor(.gray)
|
||||
.frame(height: 30)
|
||||
.padding(.top, 5)
|
||||
}
|
||||
.confirmationDialog(
|
||||
"This message was likely not delivered.",
|
||||
isPresented: $isShowingConfirmation,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("Try Again") {
|
||||
guard bleManager.connectedPeripheral?.peripheral.state == .connected else {
|
||||
return
|
||||
}
|
||||
let messageID = message.messageId
|
||||
let payload = message.messagePayload ?? ""
|
||||
let userNum = message.toUser?.num ?? 0
|
||||
let channel = message.channel
|
||||
let isEmoji = message.isEmoji
|
||||
let replyID = message.replyID
|
||||
context.delete(message)
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
print("Failed to delete message \(messageID)")
|
||||
}
|
||||
if !bleManager.sendMessage(
|
||||
message: payload,
|
||||
toUserNum: userNum,
|
||||
channel: channel,
|
||||
isEmoji: isEmoji,
|
||||
replyID: replyID
|
||||
) {
|
||||
// Best effort, unlikely since we already checked BLE state
|
||||
print("Failed to resend message \(messageID)")
|
||||
}
|
||||
}
|
||||
Button("Cancel", role: .cancel) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
Meshtastic/Views/Messages/TextMessageField/AlertButton.swift
Normal file
21
Meshtastic/Views/Messages/TextMessageField/AlertButton.swift
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import SwiftUI
|
||||
|
||||
struct AlertButton: View {
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
Text("Alert")
|
||||
Image(systemName: "bell.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AlertButtonPreview: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AlertButton {}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import SwiftUI
|
||||
|
||||
struct RequestPositionButton: View {
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
.padding(.trailing)
|
||||
}
|
||||
}
|
||||
|
||||
struct RequestPositionButtonPreview: PreviewProvider {
|
||||
static var previews: some View {
|
||||
RequestPositionButton {}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
import SwiftUI
|
||||
|
||||
struct TextMessageField: View {
|
||||
static let maxbytes = 228
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
let destination: Destination
|
||||
@Binding var replyMessageId: Int64
|
||||
@FocusState.Binding var isFocused: Bool
|
||||
let onSubmit: () -> Void
|
||||
|
||||
enum Destination {
|
||||
case user(Int64)
|
||||
case channel(Int32)
|
||||
}
|
||||
|
||||
@State private var typingMessage: String = ""
|
||||
@State private var totalBytes = 0
|
||||
@State private var sendPositionWithMessage = false
|
||||
|
||||
var body: some View {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
HStack {
|
||||
if destination.showAlertButton {
|
||||
Spacer()
|
||||
AlertButton { typingMessage += "🔔 Alert Bell Character! \u{7}" }
|
||||
}
|
||||
Spacer()
|
||||
RequestPositionButton(action: requestPosition)
|
||||
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes).padding(.trailing)
|
||||
}
|
||||
#endif
|
||||
|
||||
HStack(alignment: .top) {
|
||||
ZStack {
|
||||
TextField("message", text: $typingMessage, axis: .vertical)
|
||||
.onChange(of: typingMessage, perform: { value in
|
||||
totalBytes = value.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > Self.maxbytes {
|
||||
let firstNBytes = Data(typingMessage.utf8.prefix(Self.maxbytes))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the message back to the last place where it was the right size
|
||||
typingMessage = maxBytesString
|
||||
} else {
|
||||
print("not a valid UTF-8 sequence")
|
||||
}
|
||||
}
|
||||
})
|
||||
.keyboardType(.default)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .keyboard) {
|
||||
Button("dismiss.keyboard") {
|
||||
isFocused = false
|
||||
}
|
||||
.font(.subheadline)
|
||||
|
||||
if destination.showAlertButton {
|
||||
Spacer()
|
||||
AlertButton { typingMessage += "🔔 Alert Bell Character! \u{7}" }
|
||||
}
|
||||
|
||||
Spacer()
|
||||
RequestPositionButton(action: requestPosition)
|
||||
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.focused($isFocused)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(minHeight: 50)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.onSubmit {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
sendMessage()
|
||||
#endif
|
||||
}
|
||||
|
||||
Text(typingMessage)
|
||||
.opacity(0)
|
||||
.padding(.all, 0)
|
||||
}
|
||||
.overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1))
|
||||
.padding(.bottom, 15)
|
||||
|
||||
Button(action: sendMessage) {
|
||||
Image(systemName: "arrow.up.circle.fill")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
.padding(.all, 15)
|
||||
}
|
||||
|
||||
private func requestPosition() {
|
||||
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
|
||||
sendPositionWithMessage = true
|
||||
typingMessage = "📍 " + userLongName + " \(destination.positionShareMessage)."
|
||||
}
|
||||
|
||||
private func sendMessage() {
|
||||
let messageSent = bleManager.sendMessage(
|
||||
message: typingMessage,
|
||||
toUserNum: destination.userNum,
|
||||
channel: destination.channelNum,
|
||||
isEmoji: false,
|
||||
replyID: replyMessageId
|
||||
)
|
||||
if messageSent {
|
||||
typingMessage = ""
|
||||
isFocused = false
|
||||
replyMessageId = 0
|
||||
onSubmit()
|
||||
if sendPositionWithMessage {
|
||||
let positionSent = bleManager.sendPosition(
|
||||
channel: destination.channelNum,
|
||||
destNum: destination.positionDestNum,
|
||||
wantResponse: destination.wantPositionResponse
|
||||
)
|
||||
if positionSent {
|
||||
print("Location Sent")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension TextMessageField.Destination {
|
||||
var positionShareMessage: String {
|
||||
switch self {
|
||||
case .user: return "has shared their position and requested a response with your position"
|
||||
case .channel: return "has shared their position with you"
|
||||
}
|
||||
}
|
||||
|
||||
var userNum: Int64 {
|
||||
switch self {
|
||||
case let .user(num): return num
|
||||
case .channel: return 0
|
||||
}
|
||||
}
|
||||
|
||||
var channelNum: Int32 {
|
||||
switch self {
|
||||
case .user: return 0
|
||||
case let .channel(num): return num
|
||||
}
|
||||
}
|
||||
|
||||
var positionDestNum: Int64 {
|
||||
switch self {
|
||||
case let .user(num): return num
|
||||
case .channel: return Int64(BLEManager.emptyNodeNum)
|
||||
}
|
||||
}
|
||||
|
||||
var showAlertButton: Bool {
|
||||
switch self {
|
||||
case .user: return false
|
||||
case .channel: return true
|
||||
}
|
||||
}
|
||||
|
||||
var wantPositionResponse: Bool {
|
||||
switch self {
|
||||
case .user: return true
|
||||
case .channel: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import SwiftUI
|
||||
|
||||
struct TextMessageSize: View {
|
||||
let maxbytes: Int
|
||||
let totalBytes: Int
|
||||
|
||||
var body: some View {
|
||||
ProgressView("\("bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes))
|
||||
.frame(width: 130)
|
||||
.padding(5)
|
||||
.font(.subheadline)
|
||||
.accentColor(.accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
struct TextMessageSizePreview: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TextMessageSize(maxbytes: 228, totalBytes: 100)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,25 +9,17 @@ import SwiftUI
|
|||
import CoreData
|
||||
|
||||
struct UserMessageList: View {
|
||||
|
||||
@StateObject var appState = AppState.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
enum Field: Hashable {
|
||||
case messageText
|
||||
}
|
||||
// Keyboard State
|
||||
@State var typingMessage: String = ""
|
||||
@State private var totalBytes = 0
|
||||
var maxbytes = 228
|
||||
@FocusState var focusedField: Field?
|
||||
@FocusState var messageFieldFocused: Bool
|
||||
// View State Items
|
||||
@ObservedObject var user: UserEntity
|
||||
@State var showDeleteMessageAlert = false
|
||||
@State private var deleteMessageId: Int64 = 0
|
||||
@State private var replyMessageId: Int64 = 0
|
||||
@State private var sendPositionWithMessage: Bool = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
|
|
@ -88,7 +80,7 @@ struct UserMessageList: View {
|
|||
}
|
||||
Button(action: {
|
||||
self.replyMessageId = message.messageId
|
||||
self.focusedField = .messageText
|
||||
self.messageFieldFocused = true
|
||||
print("I want to reply to \(message.messageId)")
|
||||
}) {
|
||||
Text("reply")
|
||||
|
|
@ -210,6 +202,11 @@ struct UserMessageList: View {
|
|||
}
|
||||
.padding(.bottom)
|
||||
.id(user.messageList.firstIndex(of: message))
|
||||
|
||||
if currentUser && (message.receivedACK && !message.realACK) {
|
||||
RetryButton(message: message)
|
||||
}
|
||||
|
||||
if !currentUser {
|
||||
Spacer(minLength: 50)
|
||||
}
|
||||
|
|
@ -265,107 +262,14 @@ struct UserMessageList: View {
|
|||
}
|
||||
})
|
||||
}
|
||||
#if targetEnvironment(macCatalyst)
|
||||
HStack {
|
||||
Spacer()
|
||||
Button {
|
||||
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
|
||||
sendPositionWithMessage = true
|
||||
typingMessage = "📍 " + userLongName + " has shared their position and requested a response with your position."
|
||||
} label: {
|
||||
Text("share.position")
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
}
|
||||
ProgressView("\("bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes))
|
||||
.frame(width: 130)
|
||||
.padding(5)
|
||||
.font(.subheadline)
|
||||
.accentColor(.accentColor)
|
||||
.padding(.trailing)
|
||||
}
|
||||
#endif
|
||||
|
||||
HStack(alignment: .top) {
|
||||
ZStack {
|
||||
TextField("message", text: $typingMessage, axis: .vertical)
|
||||
.onChange(of: typingMessage, perform: { value in
|
||||
totalBytes = value.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > maxbytes {
|
||||
let firstNBytes = Data(typingMessage.utf8.prefix(maxbytes))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the message back to the last place where it was the right size
|
||||
typingMessage = maxBytesString
|
||||
} else {
|
||||
print("not a valid UTF-8 sequence")
|
||||
}
|
||||
}
|
||||
})
|
||||
.keyboardType(.default)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .keyboard) {
|
||||
Button("dismiss.keyboard") {
|
||||
focusedField = nil
|
||||
}
|
||||
.font(.subheadline)
|
||||
Spacer()
|
||||
Button {
|
||||
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
|
||||
sendPositionWithMessage = true
|
||||
typingMessage = "📍 " + userLongName + " has shared their position and requested a response with your position."
|
||||
} label: {
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
}
|
||||
ProgressView("\("bytes".localized): \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes))
|
||||
.frame(width: 130)
|
||||
.padding(5)
|
||||
.font(.subheadline)
|
||||
.accentColor(.accentColor)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.focused($focusedField, equals: .messageText)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(minHeight: 50)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.onSubmit {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
if bleManager.sendMessage(message: typingMessage, toUserNum: user.num, channel: 0, isEmoji: false, replyID: replyMessageId) {
|
||||
typingMessage = ""
|
||||
focusedField = nil
|
||||
replyMessageId = 0
|
||||
if sendPositionWithMessage {
|
||||
if bleManager.sendPosition(channel: 0, destNum: user.num, wantResponse: true) {
|
||||
print("Location Sent")
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Text(typingMessage).opacity(0).padding(.all, 0)
|
||||
}
|
||||
.overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1))
|
||||
.padding(.bottom, 15)
|
||||
Button(action: {
|
||||
if bleManager.sendMessage(message: typingMessage, toUserNum: user.num, channel: 0, isEmoji: false, replyID: replyMessageId) {
|
||||
typingMessage = ""
|
||||
focusedField = nil
|
||||
replyMessageId = 0
|
||||
if sendPositionWithMessage {
|
||||
if bleManager.sendPosition(channel: 0, destNum: user.num, wantResponse: true) {
|
||||
print("Location Sent")
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.accentColor)
|
||||
}
|
||||
TextMessageField(
|
||||
destination: .user(user.num),
|
||||
replyMessageId: $replyMessageId,
|
||||
isFocused: $messageFieldFocused
|
||||
) {
|
||||
context.refresh(user, mergeChanges: true)
|
||||
}
|
||||
.padding(.all, 15)
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ struct NodeListItem: View {
|
|||
.symbolRenderingMode(.hierarchical)
|
||||
.foregroundColor(node.isOnline ? .green : .orange)
|
||||
LastHeardText(lastHeard: node.lastHeard)
|
||||
.font(.callout)
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
HStack {
|
||||
let role = DeviceRoles(rawValue: Int(node.user?.role ?? 0))
|
||||
|
|
@ -58,8 +59,20 @@ struct NodeListItem: View {
|
|||
.font(.callout)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("Role: \(role?.name ?? "unknown".localized)")
|
||||
.font(.callout)
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
if node.isStoreForwardRouter {
|
||||
HStack {
|
||||
Image(systemName: "envelope.arrow.triangle.branch")
|
||||
.font(.callout)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("storeforward".localized)
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
if node.positions?.count ?? 0 > 0 && connectedNode != node.num {
|
||||
HStack {
|
||||
let lastPostion = node.positions!.reversed()[0] as! PositionEntity
|
||||
|
|
@ -72,7 +85,8 @@ struct NodeListItem: View {
|
|||
Image(systemName: "lines.measurement.horizontal")
|
||||
.font(.callout)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
DistanceText(meters: metersAway).font(.callout)
|
||||
DistanceText(meters: metersAway).font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -84,7 +98,8 @@ struct NodeListItem: View {
|
|||
Image(systemName: "lines.measurement.horizontal")
|
||||
.font(.callout)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
DistanceText(meters: metersAway).font(.callout)
|
||||
DistanceText(meters: metersAway).font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -95,14 +110,16 @@ struct NodeListItem: View {
|
|||
.font(.callout)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("Channel: \(node.channel)")
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
.font(.caption)
|
||||
}
|
||||
if node.viaMqtt && connectedNode != node.num {
|
||||
Image(systemName: "network")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.callout)
|
||||
Text("Via MQTT")
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
if !connected {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ struct NodeList: View {
|
|||
@State private var columnVisibility = NavigationSplitViewVisibility.all
|
||||
@State private var selectedNode: NodeInfoEntity?
|
||||
@State private var isPresentingTraceRouteSentAlert = false
|
||||
@State private var isPresentingClientHistorySentAlert = false
|
||||
@State private var isPresentingDeleteNodeAlert = false
|
||||
@State private var deleteNodeId: Int64 = 0
|
||||
|
||||
|
|
@ -84,6 +85,17 @@ struct NodeList: View {
|
|||
} label: {
|
||||
Label("Trace Route", systemImage: "signpost.right.and.left")
|
||||
}
|
||||
if node.isStoreForwardRouter {
|
||||
|
||||
Button {
|
||||
let success = bleManager.requestStoreAndForwardClientHistory(fromUser: connectedNode!.user!, toUser: node.user!)
|
||||
if success {
|
||||
isPresentingClientHistorySentAlert = true
|
||||
}
|
||||
} label: {
|
||||
Label("Client History", systemImage: "envelope.arrow.triangle.branch")
|
||||
}
|
||||
}
|
||||
}
|
||||
if bleManager.connectedPeripheral != nil {
|
||||
Button (role: .destructive) {
|
||||
|
|
@ -103,6 +115,14 @@ struct NodeList: View {
|
|||
} message: {
|
||||
Text("This could take a while, response will appear in the trace route log for the node it was sent to.")
|
||||
}
|
||||
.alert(
|
||||
"Client History Request Sent",
|
||||
isPresented: $isPresentingClientHistorySentAlert
|
||||
) {
|
||||
Button("OK", role: .cancel) { }
|
||||
} message: {
|
||||
Text("Any missed messages will be delivered again.")
|
||||
}
|
||||
}
|
||||
.searchable(text: nodesQuery, prompt: "Find a node")
|
||||
.navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count)))
|
||||
|
|
|
|||
|
|
@ -223,7 +223,7 @@ struct NodeMap: View {
|
|||
.padding(.bottom)
|
||||
#endif
|
||||
}
|
||||
.presentationDetents([UserDefaults.enableOfflineMaps || UserDefaults.enableOverlayServer ? .large : .medium])
|
||||
.presentationDetents([enableOfflineMaps || enableOverlayServer ? .large : .medium])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ struct DeviceConfig: View {
|
|||
@State var serialEnabled = true
|
||||
@State var debugLogEnabled = false
|
||||
@State var rebroadcastMode = 0
|
||||
@State var nodeInfoBroadcastSecs = 900
|
||||
@State var doubleTapAsButtonPress = false
|
||||
@State var isManaged = false
|
||||
|
||||
|
|
@ -77,6 +78,15 @@ struct DeviceConfig: View {
|
|||
Text(RebroadcastModes(rawValue: rebroadcastMode)?.description ?? "")
|
||||
.foregroundColor(.gray)
|
||||
.font(.caption)
|
||||
Picker("Node Info Broadcast Interval", selection: $nodeInfoBroadcastSecs ) {
|
||||
ForEach(UpdateIntervals.allCases) { ui in
|
||||
if ui.rawValue >= 3600 {
|
||||
Text(ui.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
.padding(.top, 10)
|
||||
Toggle(isOn: $doubleTapAsButtonPress) {
|
||||
Label("Double Tap as Button", systemImage: "hand.tap")
|
||||
}
|
||||
|
|
@ -206,8 +216,8 @@ struct DeviceConfig: View {
|
|||
dc.debugLogEnabled = debugLogEnabled
|
||||
dc.buttonGpio = UInt32(buttonGPIO)
|
||||
dc.buzzerGpio = UInt32(buzzerGPIO)
|
||||
//dc.gpsEnGpio = UInt32(gpsEnGPIO)
|
||||
dc.rebroadcastMode = RebroadcastModes(rawValue: rebroadcastMode)?.protoEnumValue() ?? RebroadcastModes.all.protoEnumValue()
|
||||
dc.nodeInfoBroadcastSecs = UInt32(nodeInfoBroadcastSecs)
|
||||
dc.doubleTapAsButtonPress = doubleTapAsButtonPress
|
||||
dc.isManaged = isManaged
|
||||
let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
|
|
@ -275,6 +285,11 @@ struct DeviceConfig: View {
|
|||
if newRebroadcastMode != node!.deviceConfig!.rebroadcastMode { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: nodeInfoBroadcastSecs) { newNodeInfoBroadcastSecs in
|
||||
if node != nil && node?.deviceConfig != nil {
|
||||
if newNodeInfoBroadcastSecs != node!.deviceConfig!.nodeInfoBroadcastSecs { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: doubleTapAsButtonPress) { newDoubleTapAsButtonPress in
|
||||
if node != nil && node?.deviceConfig != nil {
|
||||
if newDoubleTapAsButtonPress != node!.deviceConfig!.doubleTapAsButtonPress { hasChanges = true }
|
||||
|
|
@ -293,6 +308,7 @@ struct DeviceConfig: View {
|
|||
self.buttonGPIO = Int(node?.deviceConfig?.buttonGpio ?? 0)
|
||||
self.buzzerGPIO = Int(node?.deviceConfig?.buzzerGpio ?? 0)
|
||||
self.rebroadcastMode = Int(node?.deviceConfig?.rebroadcastMode ?? 0)
|
||||
self.nodeInfoBroadcastSecs = Int(node?.deviceConfig?.nodeInfoBroadcastSecs ?? 900)
|
||||
self.doubleTapAsButtonPress = node?.deviceConfig?.doubleTapAsButtonPress ?? false
|
||||
self.isManaged = node?.deviceConfig?.isManaged ?? false
|
||||
self.hasChanges = false
|
||||
|
|
|
|||
|
|
@ -161,9 +161,9 @@ struct LoRaConfig: View {
|
|||
.font(.caption)
|
||||
|
||||
HStack {
|
||||
Text("LoRa Channel Number")
|
||||
Text("Frequency Slot")
|
||||
.fixedSize()
|
||||
TextField("Channel Number", value: $channelNum, formatter: formatter)
|
||||
TextField("Frequency Slot", value: $channelNum, formatter: formatter)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .keyboard) {
|
||||
Button("dismiss.keyboard") {
|
||||
|
|
@ -175,6 +175,7 @@ struct LoRaConfig: View {
|
|||
.keyboardType(.decimalPad)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.focused($focusedField, equals: .channelNum)
|
||||
.disabled(overrideFrequency > 0.0)
|
||||
}
|
||||
Text("This determines the actual frequency you are transmitting on in the band. If set to 0 this value will be calculated automatically based on the primary channel name.")
|
||||
.font(.caption)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ struct StoreForwardConfig: View {
|
|||
@State var hasChanges: Bool = false
|
||||
/// Enable the Store and Forward Module
|
||||
@State var enabled = false
|
||||
/// Is a S&F Router
|
||||
@State var isRouter = false
|
||||
/// Send a Heartbeat
|
||||
@State var heartbeat: Bool = false
|
||||
/// Number of Records
|
||||
|
|
@ -56,38 +58,71 @@ struct StoreForwardConfig: View {
|
|||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "envelope.arrow.triangle.branch")
|
||||
Text("Enables the store and forward module. Store and forward must be enabled on both client and router devices.")
|
||||
.font(.caption)
|
||||
}
|
||||
Toggle(isOn: $heartbeat) {
|
||||
Label("storeforward.heartbeat", systemImage: "waveform.path.ecg")
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
if enabled {
|
||||
HStack {
|
||||
Picker(selection: $isRouter, label: Text("Role")) {
|
||||
Text("Client")
|
||||
.tag(false)
|
||||
Text("Router")
|
||||
.tag(true)
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
.padding(.top, 5)
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
}
|
||||
Picker("Number of records", selection: $records) {
|
||||
Text("unset").tag(0)
|
||||
Text("25").tag(25)
|
||||
Text("50").tag(50)
|
||||
Text("75").tag(75)
|
||||
Text("100").tag(100)
|
||||
VStack {
|
||||
if isRouter {
|
||||
Text("Store and forward router devices must also be in the router or router client device role and requires a ESP32 device with PSRAM.")
|
||||
.font(.caption)
|
||||
} else {
|
||||
Text("Store and forward clients can request history from routers on the network.")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("History Return Max", selection: $historyReturnMax ) {
|
||||
Text("unset").tag(0)
|
||||
Text("25").tag(25)
|
||||
Text("50").tag(50)
|
||||
Text("75").tag(75)
|
||||
Text("100").tag(100)
|
||||
}
|
||||
|
||||
if isRouter {
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $heartbeat) {
|
||||
Label("storeforward.heartbeat", systemImage: "waveform.path.ecg")
|
||||
}
|
||||
Picker("Number of records", selection: $records) {
|
||||
Text("unset").tag(0)
|
||||
Text("25").tag(25)
|
||||
Text("50").tag(50)
|
||||
Text("75").tag(75)
|
||||
Text("100").tag(100)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("History Return Max", selection: $historyReturnMax ) {
|
||||
Text("unset").tag(0)
|
||||
Text("25").tag(25)
|
||||
Text("50").tag(50)
|
||||
Text("75").tag(75)
|
||||
Text("100").tag(100)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("History Return Window", selection: $historyReturnWindow ) {
|
||||
Text("unset").tag(0)
|
||||
Text("One Minute").tag(60)
|
||||
Text("Five Minutes").tag(300)
|
||||
Text("Ten Minutes").tag(600)
|
||||
Text("Fifteen Minutes").tag(900)
|
||||
Text("Thirty Minutes").tag(1800)
|
||||
Text("One Hour").tag(3600)
|
||||
Text("Two Hours").tag(7200)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("History Return Window", selection: $historyReturnWindow ) {
|
||||
Text("unset").tag(0)
|
||||
Text("One Minute").tag(60)
|
||||
Text("Five Minutes").tag(300)
|
||||
Text("Ten Minutes").tag(600)
|
||||
Text("Fifteen Minutes").tag(900)
|
||||
Text("Thirty Minutes").tag(1800)
|
||||
Text("One Hour").tag(3600)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
|
|
@ -113,6 +148,18 @@ struct StoreForwardConfig: View {
|
|||
let nodeName = node?.user?.longName ?? "unknown".localized
|
||||
let buttonText = String.localizedStringWithFormat("save.config %@".localized, nodeName)
|
||||
Button(buttonText) {
|
||||
|
||||
/// Let the user set isRouter for the connected node, for nodes on the mesh set isRouter based
|
||||
/// on receipt of a primary heartbeat
|
||||
if connectedNode?.num ?? 0 == node?.num ?? -1 {
|
||||
connectedNode?.storeForwardConfig?.isRouter = isRouter
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
print("Failed to save isRouter")
|
||||
}
|
||||
}
|
||||
|
||||
var sfc = ModuleConfig.StoreForwardConfig()
|
||||
sfc.enabled = self.enabled
|
||||
sfc.heartbeat = self.heartbeat
|
||||
|
|
@ -125,7 +172,8 @@ struct StoreForwardConfig: View {
|
|||
// for now just disable the button after a successful save
|
||||
hasChanges = false
|
||||
goBack()
|
||||
} }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
message: {
|
||||
|
|
@ -140,7 +188,7 @@ struct StoreForwardConfig: View {
|
|||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
setStoreAndForwardValues()
|
||||
|
||||
// Need to request a Detection Sensor Module Config from the remote node before allowing changes
|
||||
if bleManager.connectedPeripheral != nil && node?.storeForwardConfig == nil {
|
||||
print("empty store and forward module config")
|
||||
|
|
@ -149,10 +197,16 @@ struct StoreForwardConfig: View {
|
|||
_ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
setStoreAndForwardValues()
|
||||
}
|
||||
.onChange(of: enabled) { newEnabled in
|
||||
if node != nil && node?.detectionSensorConfig != nil {
|
||||
if newEnabled != node!.detectionSensorConfig!.enabled { hasChanges = true }
|
||||
if node != nil && node?.storeForwardConfig != nil {
|
||||
if newEnabled != node!.storeForwardConfig!.enabled { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: isRouter) { newIsRouter in
|
||||
if node != nil && node?.storeForwardConfig != nil {
|
||||
if newIsRouter != node!.storeForwardConfig!.isRouter { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: heartbeat) { newHeartbeat in
|
||||
|
|
@ -178,10 +232,11 @@ struct StoreForwardConfig: View {
|
|||
}
|
||||
func setStoreAndForwardValues() {
|
||||
self.enabled = (node?.storeForwardConfig?.enabled ?? false)
|
||||
self.isRouter = (node?.storeForwardConfig?.isRouter ?? false)
|
||||
self.heartbeat = (node?.storeForwardConfig?.heartbeat ?? true)
|
||||
self.records = Int(node?.storeForwardConfig?.records ?? 50)
|
||||
self.historyReturnMax = Int(node?.storeForwardConfig?.historyReturnMax ?? 100)
|
||||
self.historyReturnWindow = Int(node?.storeForwardConfig?.historyReturnWindow ?? 300)
|
||||
self.historyReturnWindow = Int(node?.storeForwardConfig?.historyReturnWindow ?? 7200)
|
||||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
|
@ -108,7 +108,9 @@ struct PositionConfig: View {
|
|||
|
||||
Picker("Position Broadcast Interval", selection: $positionBroadcastSeconds) {
|
||||
ForEach(UpdateIntervals.allCases) { at in
|
||||
Text(at.description)
|
||||
if at.rawValue >= 900 {
|
||||
Text(at.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ struct Firmware: View {
|
|||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
var node: NodeInfoEntity?
|
||||
@State var minimumVersion = "2.2.17"
|
||||
@State var minimumVersion = "2.2.21"
|
||||
@State var version = ""
|
||||
@State private var currentDevice: DeviceHardware?
|
||||
@State private var latestStable: FirmwareRelease?
|
||||
|
|
|
|||
|
|
@ -1,295 +1,295 @@
|
|||
////
|
||||
//// Routes.swift
|
||||
//// Meshtastic
|
||||
////
|
||||
//// Created by Garth Vander Houwen on 11/21/23.
|
||||
////
|
||||
//
|
||||
//import SwiftUI
|
||||
//import CoreData
|
||||
//import MapKit
|
||||
//import CoreLocation
|
||||
//import CoreMotion
|
||||
// Routes.swift
|
||||
// Meshtastic
|
||||
//
|
||||
//@available(iOS 17.0, macOS 14.0, *)
|
||||
//struct RouteRecorder: View {
|
||||
//
|
||||
// @ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared
|
||||
// @Environment(\.managedObjectContext) var context
|
||||
// @State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic)
|
||||
// //@State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true)
|
||||
// @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic)
|
||||
// @State var isShowingDetails = false
|
||||
// @Namespace var namespace
|
||||
// @Namespace var routerecorderscope
|
||||
// @State var recording: RouteEntity?
|
||||
// @State var color: Color = .blue
|
||||
//
|
||||
// var body: some View {
|
||||
// VStack {
|
||||
// ZStack {
|
||||
// Map(position: $position, scope: routerecorderscope) {
|
||||
// UserAnnotation()
|
||||
// /// Route Lines
|
||||
// let lineCoords = locationsHandler.locationsArray.compactMap({(position) -> CLLocationCoordinate2D in
|
||||
// return position.coordinate
|
||||
// })
|
||||
//
|
||||
// let gradient = LinearGradient(
|
||||
// colors: [color],
|
||||
// startPoint: .leading, endPoint: .trailing
|
||||
// )
|
||||
// let dashed = StrokeStyle(
|
||||
// lineWidth: 3,
|
||||
// lineCap: .round, lineJoin: .round, dash: [10, 10]
|
||||
// )
|
||||
// MapPolyline(coordinates: lineCoords)
|
||||
// .stroke(gradient, style: dashed)
|
||||
// Created by Garth Vander Houwen on 11/21/23.
|
||||
//
|
||||
// }
|
||||
// .mapStyle(mapStyle)
|
||||
// }
|
||||
// .mapScope(routerecorderscope)
|
||||
// .safeAreaInset(edge: .bottom) {
|
||||
// ZStack {
|
||||
// VStack {
|
||||
// HStack(spacing: 10) {
|
||||
// Spacer()
|
||||
//
|
||||
// Button {
|
||||
// isShowingDetails = true
|
||||
// } label: {
|
||||
// Image(systemName: locationsHandler.isRecording ? "record.circle.fill" : "record.circle")
|
||||
// .font(.system(size: 72))
|
||||
// .symbolRenderingMode(.multicolor)
|
||||
// .foregroundColor(.red)
|
||||
// }
|
||||
// .buttonStyle(.bordered)
|
||||
// .foregroundColor(.red)
|
||||
// .buttonBorderShape(.circle)
|
||||
// .matchedGeometryEffect(id: "Details Button", in: namespace)
|
||||
//
|
||||
// Spacer()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .padding()
|
||||
// }
|
||||
// .sheet(isPresented: $isShowingDetails) {
|
||||
// NavigationStack {
|
||||
// VStack {
|
||||
// if locationsHandler.isRecording {
|
||||
// HStack (alignment: .center) {
|
||||
// Image(systemName: "record.circle.fill")
|
||||
// .symbolRenderingMode(.multicolor)
|
||||
// .font(.title)
|
||||
// .foregroundColor(.red)
|
||||
// Text("Recording route")
|
||||
// .font(.title)
|
||||
// Spacer()
|
||||
// Text("\(locationsHandler.count)")
|
||||
// .foregroundColor(.red)
|
||||
// .font(.title2)
|
||||
// }
|
||||
// .padding()
|
||||
// } else if locationsHandler.isRecordingPaused {
|
||||
// HStack (alignment: .center) {
|
||||
//
|
||||
// Image(systemName: "playpause")
|
||||
// .symbolRenderingMode(.multicolor)
|
||||
// .font(.title3)
|
||||
// .foregroundColor(.red)
|
||||
// Text("Route recording paused")
|
||||
// .font(.title)
|
||||
// }
|
||||
// .padding(.top)
|
||||
// }
|
||||
//
|
||||
// if locationsHandler.isRecording || locationsHandler.isRecordingPaused {
|
||||
// Divider()
|
||||
// HStack {
|
||||
// VStack {
|
||||
// Text(locationsHandler.recordingStarted ?? Date(), style: .timer)
|
||||
// .font(.title)
|
||||
// .fixedSize()
|
||||
// Text("Time")
|
||||
// .font(.callout)
|
||||
// .fixedSize()
|
||||
// }
|
||||
// .padding(.horizontal)
|
||||
// Divider()
|
||||
// VStack {
|
||||
// let distance = Measurement(value: locationsHandler.distanceTraveled, unit: UnitLength.meters)
|
||||
// Text("\(distance.formatted())")
|
||||
// .font(.title)
|
||||
// .fixedSize()
|
||||
// Text("Distance")
|
||||
// .font(.callout)
|
||||
// .fixedSize()
|
||||
// }
|
||||
// .padding(.horizontal)
|
||||
// Divider()
|
||||
// VStack {
|
||||
// let gain = Measurement(value: locationsHandler.elevationGain, unit: UnitLength.meters)
|
||||
// Text(gain.formatted())
|
||||
// .font(.title)
|
||||
// Text("Elev. Gain")
|
||||
// .font(.callout)
|
||||
// }
|
||||
// .padding(.horizontal)
|
||||
// }
|
||||
// .frame(maxHeight: 90)
|
||||
// }
|
||||
// Divider()
|
||||
// VStack(alignment: .leading) {
|
||||
// List {
|
||||
// GPSStatus(largeFont: .body, smallFont: .callout)
|
||||
// }
|
||||
// .listStyle(.plain)
|
||||
// HStack {
|
||||
// Spacer()
|
||||
// if !locationsHandler.isRecording && !locationsHandler.isRecordingPaused {
|
||||
// /// We are not recording or paused, show start recording button
|
||||
// Button {
|
||||
// locationsHandler.isRecording = true
|
||||
// locationsHandler.count = 0
|
||||
// locationsHandler.distanceTraveled = 0.0
|
||||
// locationsHandler.elevationGain = 0.0
|
||||
// locationsHandler.locationsArray.removeAll()
|
||||
// locationsHandler.recordingStarted = Date()
|
||||
// let newRoute = RouteEntity(context: context)
|
||||
// newRoute.name = String("Route Recording")
|
||||
// newRoute.id = Int32.random(in: Int32(Int8.max) ... Int32.max)
|
||||
// newRoute.color = Int64(UIColor.random.hex)
|
||||
// newRoute.date = Date()
|
||||
// newRoute.enabled = false
|
||||
// color = Color(UIColor(hex: UInt32(newRoute.color)))
|
||||
// self.recording = newRoute
|
||||
// do {
|
||||
// try context.save()
|
||||
// print("💾 Saved a new route")
|
||||
// } catch {
|
||||
// context.rollback()
|
||||
// let nsError = error as NSError
|
||||
// print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)")
|
||||
// }
|
||||
// } label: {
|
||||
// Label("start", systemImage: "play")
|
||||
// }
|
||||
// .buttonStyle(.bordered)
|
||||
// .buttonBorderShape(.capsule)
|
||||
// .controlSize(.large)
|
||||
// .padding(.bottom)
|
||||
//
|
||||
// } else if locationsHandler.isRecording {
|
||||
// /// We are recording show pause button
|
||||
// Button {
|
||||
// locationsHandler.isRecording = false
|
||||
// locationsHandler.isRecordingPaused = true
|
||||
// } label: {
|
||||
// Label("pause", systemImage: "pause")
|
||||
// }
|
||||
// .buttonStyle(.bordered)
|
||||
// .buttonBorderShape(.capsule)
|
||||
// .controlSize(.large)
|
||||
// .padding(.bottom)
|
||||
// } else if locationsHandler.isRecordingPaused {
|
||||
// /// We are paused show resume button
|
||||
// Button {
|
||||
// locationsHandler.isRecording = true
|
||||
// locationsHandler.isRecordingPaused = false
|
||||
// } label: {
|
||||
// Label("resume", systemImage: "playpause")
|
||||
// }
|
||||
// .buttonStyle(.bordered)
|
||||
// .buttonBorderShape(.capsule)
|
||||
// .controlSize(.large)
|
||||
// .padding(.bottom)
|
||||
// }
|
||||
//
|
||||
// if locationsHandler.isRecording || locationsHandler.isRecordingPaused {
|
||||
// /// We are recording or paused, show finish button
|
||||
// Button {
|
||||
// locationsHandler.isRecording = false
|
||||
// locationsHandler.isRecordingPaused = false
|
||||
// locationsHandler.distanceTraveled = 0.0
|
||||
// locationsHandler.elevationGain = 0.0
|
||||
// locationsHandler.locationsArray.removeAll()
|
||||
// locationsHandler.recordingStarted = nil
|
||||
// if let rec = recording {
|
||||
// rec.enabled = true
|
||||
// context.refresh(rec, mergeChanges:true)
|
||||
// }
|
||||
//
|
||||
// do {
|
||||
// try context.save()
|
||||
// print("💾 Saved a route finish")
|
||||
// } catch {
|
||||
// context.rollback()
|
||||
// let nsError = error as NSError
|
||||
// print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)")
|
||||
// }
|
||||
// } label: {
|
||||
// Label("finish", systemImage: "flag.checkered")
|
||||
// }
|
||||
// .buttonStyle(.bordered)
|
||||
// .buttonBorderShape(.capsule)
|
||||
// .controlSize(.large)
|
||||
// .padding(.bottom)
|
||||
// }
|
||||
//#if targetEnvironment(macCatalyst)
|
||||
// Button(role: .cancel) {
|
||||
// isShowingDetails = false
|
||||
// } label: {
|
||||
// Label("close", systemImage: "xmark")
|
||||
// }
|
||||
// .buttonStyle(.bordered)
|
||||
// .buttonBorderShape(.capsule)
|
||||
// .controlSize(.large)
|
||||
// .padding(.bottom)
|
||||
//#endif
|
||||
// Spacer()
|
||||
// }
|
||||
//
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .presentationDetents([.fraction(0.30), .fraction(0.65)])
|
||||
// .presentationDragIndicator(.hidden)
|
||||
// .interactiveDismissDisabled(false)
|
||||
// .onAppear {
|
||||
// UIApplication.shared.isIdleTimerDisabled = true
|
||||
// }
|
||||
// .onDisappear(perform: {
|
||||
// UIApplication.shared.isIdleTimerDisabled = false
|
||||
// })
|
||||
// .onChange(of: locationsHandler.locationsArray.last) { newLoc in
|
||||
// if locationsHandler.isRecording {
|
||||
// if let loc = newLoc {
|
||||
// if recording != nil {
|
||||
// let locationEntity = LocationEntity(context: context)
|
||||
// locationEntity.routeLocation = recording
|
||||
// locationEntity.id = Int32(locationsHandler.count)
|
||||
// locationEntity.altitude = Int32(loc.altitude)
|
||||
// locationEntity.heading = Int32(loc.course)
|
||||
// locationEntity.speed = Int32(loc.speed)
|
||||
// locationEntity.latitudeI = Int32(loc.coordinate.latitude * 1e7)
|
||||
// locationEntity.longitudeI = Int32(loc.coordinate.longitude * 1e7)
|
||||
// do {
|
||||
// try context.save()
|
||||
// print("💾 Saved a new route location")
|
||||
// //print("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num)")
|
||||
// } catch {
|
||||
// context.rollback()
|
||||
// let nsError = error as NSError
|
||||
// print("💥 Error Saving LocationEntity from the Route Recorder \(nsError)")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
|
||||
// }
|
||||
//}
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import MapKit
|
||||
import CoreLocation
|
||||
import CoreMotion
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct RouteRecorder: View {
|
||||
|
||||
@ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic)
|
||||
//@State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true)
|
||||
@State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic)
|
||||
@State var isShowingDetails = false
|
||||
@Namespace var namespace
|
||||
@Namespace var routerecorderscope
|
||||
@State var recording: RouteEntity?
|
||||
@State var color: Color = .blue
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ZStack {
|
||||
Map(position: $position, scope: routerecorderscope) {
|
||||
UserAnnotation()
|
||||
/// Route Lines
|
||||
let lineCoords = locationsHandler.locationsArray.compactMap({(position) -> CLLocationCoordinate2D in
|
||||
return position.coordinate
|
||||
})
|
||||
|
||||
let gradient = LinearGradient(
|
||||
colors: [color],
|
||||
startPoint: .leading, endPoint: .trailing
|
||||
)
|
||||
let dashed = StrokeStyle(
|
||||
lineWidth: 3,
|
||||
lineCap: .round, lineJoin: .round, dash: [10, 10]
|
||||
)
|
||||
MapPolyline(coordinates: lineCoords)
|
||||
.stroke(gradient, style: dashed)
|
||||
|
||||
}
|
||||
.mapStyle(mapStyle)
|
||||
}
|
||||
.mapScope(routerecorderscope)
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
ZStack {
|
||||
VStack {
|
||||
HStack(spacing: 10) {
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
isShowingDetails = true
|
||||
} label: {
|
||||
Image(systemName: locationsHandler.isRecording ? "record.circle.fill" : "record.circle")
|
||||
.font(.system(size: 72))
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.foregroundColor(.red)
|
||||
.buttonBorderShape(.circle)
|
||||
.matchedGeometryEffect(id: "Details Button", in: namespace)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.sheet(isPresented: $isShowingDetails) {
|
||||
NavigationStack {
|
||||
VStack {
|
||||
if locationsHandler.isRecording {
|
||||
HStack (alignment: .center) {
|
||||
Image(systemName: "record.circle.fill")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.font(.title)
|
||||
.foregroundColor(.red)
|
||||
Text("Recording route")
|
||||
.font(.title)
|
||||
Spacer()
|
||||
Text("\(locationsHandler.count)")
|
||||
.foregroundColor(.red)
|
||||
.font(.title2)
|
||||
}
|
||||
.padding()
|
||||
} else if locationsHandler.isRecordingPaused {
|
||||
HStack (alignment: .center) {
|
||||
|
||||
Image(systemName: "playpause")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.font(.title3)
|
||||
.foregroundColor(.red)
|
||||
Text("Route recording paused")
|
||||
.font(.title)
|
||||
}
|
||||
.padding(.top)
|
||||
}
|
||||
|
||||
if locationsHandler.isRecording || locationsHandler.isRecordingPaused {
|
||||
Divider()
|
||||
HStack {
|
||||
VStack {
|
||||
Text(locationsHandler.recordingStarted ?? Date(), style: .timer)
|
||||
.font(.title)
|
||||
.fixedSize()
|
||||
Text("Time")
|
||||
.font(.callout)
|
||||
.fixedSize()
|
||||
}
|
||||
.padding(.horizontal)
|
||||
Divider()
|
||||
VStack {
|
||||
let distance = Measurement(value: locationsHandler.distanceTraveled, unit: UnitLength.meters)
|
||||
Text("\(distance.formatted())")
|
||||
.font(.title)
|
||||
.fixedSize()
|
||||
Text("Distance")
|
||||
.font(.callout)
|
||||
.fixedSize()
|
||||
}
|
||||
.padding(.horizontal)
|
||||
Divider()
|
||||
VStack {
|
||||
let gain = Measurement(value: locationsHandler.elevationGain, unit: UnitLength.meters)
|
||||
Text(gain.formatted())
|
||||
.font(.title)
|
||||
Text("Elev. Gain")
|
||||
.font(.callout)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.frame(maxHeight: 90)
|
||||
}
|
||||
Divider()
|
||||
VStack(alignment: .leading) {
|
||||
List {
|
||||
GPSStatus(largeFont: .body, smallFont: .callout)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
HStack {
|
||||
Spacer()
|
||||
if !locationsHandler.isRecording && !locationsHandler.isRecordingPaused {
|
||||
/// We are not recording or paused, show start recording button
|
||||
Button {
|
||||
locationsHandler.isRecording = true
|
||||
locationsHandler.count = 0
|
||||
locationsHandler.distanceTraveled = 0.0
|
||||
locationsHandler.elevationGain = 0.0
|
||||
locationsHandler.locationsArray.removeAll()
|
||||
locationsHandler.recordingStarted = Date()
|
||||
let newRoute = RouteEntity(context: context)
|
||||
newRoute.name = String("Route Recording")
|
||||
newRoute.id = Int32.random(in: Int32(Int8.max) ... Int32.max)
|
||||
newRoute.color = Int64(UIColor.random.hex)
|
||||
newRoute.date = Date()
|
||||
newRoute.enabled = false
|
||||
color = Color(UIColor(hex: UInt32(newRoute.color)))
|
||||
self.recording = newRoute
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Saved a new route")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)")
|
||||
}
|
||||
} label: {
|
||||
Label("start", systemImage: "play")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding(.bottom)
|
||||
|
||||
} else if locationsHandler.isRecording {
|
||||
/// We are recording show pause button
|
||||
Button {
|
||||
locationsHandler.isRecording = false
|
||||
locationsHandler.isRecordingPaused = true
|
||||
} label: {
|
||||
Label("pause", systemImage: "pause")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding(.bottom)
|
||||
} else if locationsHandler.isRecordingPaused {
|
||||
/// We are paused show resume button
|
||||
Button {
|
||||
locationsHandler.isRecording = true
|
||||
locationsHandler.isRecordingPaused = false
|
||||
} label: {
|
||||
Label("resume", systemImage: "playpause")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding(.bottom)
|
||||
}
|
||||
|
||||
if locationsHandler.isRecording || locationsHandler.isRecordingPaused {
|
||||
/// We are recording or paused, show finish button
|
||||
Button {
|
||||
locationsHandler.isRecording = false
|
||||
locationsHandler.isRecordingPaused = false
|
||||
locationsHandler.distanceTraveled = 0.0
|
||||
locationsHandler.elevationGain = 0.0
|
||||
locationsHandler.locationsArray.removeAll()
|
||||
locationsHandler.recordingStarted = nil
|
||||
if let rec = recording {
|
||||
rec.enabled = true
|
||||
context.refresh(rec, mergeChanges:true)
|
||||
}
|
||||
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Saved a route finish")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)")
|
||||
}
|
||||
} label: {
|
||||
Label("finish", systemImage: "flag.checkered")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding(.bottom)
|
||||
}
|
||||
#if targetEnvironment(macCatalyst)
|
||||
Button(role: .cancel) {
|
||||
isShowingDetails = false
|
||||
} label: {
|
||||
Label("close", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding(.bottom)
|
||||
#endif
|
||||
Spacer()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.fraction(0.30), .fraction(0.65)])
|
||||
.presentationDragIndicator(.hidden)
|
||||
.interactiveDismissDisabled(false)
|
||||
.onAppear {
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
}
|
||||
.onDisappear(perform: {
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
})
|
||||
.onChange(of: locationsHandler.locationsArray.last) { newLoc in
|
||||
if locationsHandler.isRecording {
|
||||
if let loc = newLoc {
|
||||
if recording != nil {
|
||||
let locationEntity = LocationEntity(context: context)
|
||||
locationEntity.routeLocation = recording
|
||||
locationEntity.id = Int32(locationsHandler.count)
|
||||
locationEntity.altitude = Int32(loc.altitude)
|
||||
locationEntity.heading = Int32(loc.course)
|
||||
locationEntity.speed = Int32(loc.speed)
|
||||
locationEntity.latitudeI = Int32(loc.coordinate.latitude * 1e7)
|
||||
locationEntity.longitudeI = Int32(loc.coordinate.longitude * 1e7)
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Saved a new route location")
|
||||
//print("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num)")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Saving LocationEntity from the Route Recorder \(nsError)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,14 +69,14 @@ struct Settings: View {
|
|||
Text("routes")
|
||||
}
|
||||
.tag(SettingsSidebar.routes)
|
||||
// NavigationLink {
|
||||
// RouteRecorder()
|
||||
// } label: {
|
||||
// Image(systemName: "record.circle")
|
||||
// .symbolRenderingMode(.hierarchical)
|
||||
// Text("route.recorder")
|
||||
// }
|
||||
// .tag(SettingsSidebar.routeRecorder)
|
||||
NavigationLink {
|
||||
RouteRecorder()
|
||||
} label: {
|
||||
Image(systemName: "record.circle")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("route.recorder")
|
||||
}
|
||||
.tag(SettingsSidebar.routeRecorder)
|
||||
}
|
||||
|
||||
let node = nodes.first(where: { $0.num == preferredNodeNum })
|
||||
|
|
|
|||
|
|
@ -69,16 +69,16 @@
|
|||
"device.config"="Device Config";
|
||||
"device.metrics.delete"="Delete all device metrics?";
|
||||
"device.metrics.log"="Device Metrics Log";
|
||||
"device.role.client"="App connected or stand alone messaging client.";
|
||||
"device.role.clientmute"="Client that does not forward packets from other devices.";
|
||||
"device.role.clienthidden"="Client that only broadcasts as needed for stealth or power savings.";
|
||||
"device.role.tracker"="Prioritizes broadcasting GPS position packets.";
|
||||
"device.role.lostandfound"="Broadcasts location as message to default channel regularly for to assist with node recovery.";
|
||||
"device.role.sensor"="Prioritizes broadcasting telemetry packets.";
|
||||
"device.role.client"="App connected or stand alone messaging device.";
|
||||
"device.role.clientmute"="Device that does not forward packets from other devices.";
|
||||
"device.role.clienthidden"="Device that only broadcasts as needed for stealth or power savings.";
|
||||
"device.role.tracker"="Broadcasts GPS position packets as priority.";
|
||||
"device.role.lostandfound"="Broadcasts location as message to default channel regularly for to assist with device recovery.";
|
||||
"device.role.sensor"="Broadcasts telemetry packets as priority.";
|
||||
"device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts.";
|
||||
"device.role.repeater"="Infrastructure node for extending mesh network coverage by relaying messages with minimal overhead. Not visible in Nodes list. Best positioned in strategic locations to maximize the network's overall coverage. Device is not shown in topology.";
|
||||
"device.role.router"="Infrastructure node for on extending mesh network coverage by relaying messages. Visible in Nodes list. Best positioned in strategic locations to maximize the network's overall coverage. Device is shown in topology.";
|
||||
"device.role.routerclient"="Combination of both ROUTER and CLIENT. Not for mobile nodes.";
|
||||
"device.role.repeater"="Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list.";
|
||||
"device.role.router"="Infrastructure node for extending network coverage by relaying messages. Visible in Nodes list.";
|
||||
"device.role.routerclient"="Combination of both ROUTER and CLIENT. Not for mobile devices.";
|
||||
"direct.messages"="Direct Messages";
|
||||
"dismiss.keyboard"="Dismiss";
|
||||
"display"="Display (Device Screen)";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue