Merge pull request #445 from meshtastic/2.2.18_Working_Changes

2.2.18 working changes
This commit is contained in:
Garth Vander Houwen 2024-01-14 12:01:54 -08:00 committed by GitHub
commit 238cadff06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1345 additions and 761 deletions

View file

@ -28,7 +28,7 @@
DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553562855B02500E55709 /* LoRaConfig.swift */; };
DD2553592855B52700E55709 /* PositionConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553582855B52700E55709 /* PositionConfig.swift */; };
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */; };
DD2DC2C029BCD8AB003B383C /* HardwareModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */; };
DD33DB622B3D27C7003E1EA0 /* FirmwareApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */; };
DD3501892852FC3B000FC853 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3501882852FC3B000FC853 /* Settings.swift */; };
DD3619152B1EF9F900C41C8C /* LocationsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */; };
DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */; };
@ -180,6 +180,7 @@
DDF6B2482A9AEBF500BA6931 /* StoreForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForward.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 */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -245,7 +246,8 @@
DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV22.xcdatamodel; sourceTree = "<group>"; };
DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewSwiftUI.swift; sourceTree = "<group>"; };
DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV19.xcdatamodel; sourceTree = "<group>"; };
DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HardwareModels.swift; sourceTree = "<group>"; };
DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 23.xcdatamodel"; sourceTree = "<group>"; };
DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirmwareApi.swift; sourceTree = "<group>"; };
DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV21.xcdatamodel; sourceTree = "<group>"; };
DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsHandler.swift; sourceTree = "<group>"; };
@ -421,6 +423,7 @@
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>"; };
DDFFA7462B3A7F3C004730DB /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -548,6 +551,7 @@
DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */,
DDA0B6B1294CDC55001356EC /* Channels.swift */,
DDD6EEAE29BC024700383354 /* Firmware.swift */,
DD33DB612B3D27C7003E1EA0 /* FirmwareApi.swift */,
DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */,
DD86D40B287F401000BAEB7A /* SaveChannelQRCode.swift */,
DD3501882852FC3B000FC853 /* Settings.swift */,
@ -661,7 +665,6 @@
DD1925B828CDA93900720036 /* SerialConfigEnums.swift */,
DD994B68295F88B60013760A /* IntervalEnums.swift */,
DD5E5239298EFA5300D21B61 /* TelemetryWeather.swift */,
DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */,
);
path = Enums;
sourceTree = "<group>";
@ -905,6 +908,7 @@
DDB75A0E2A05920E006ED576 /* FileManager.swift */,
DDB75A102A059258006ED576 /* Url.swift */,
DD1933772B084F4200771CD5 /* Measurement.swift */,
DDFFA7462B3A7F3C004730DB /* Bundle.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -1123,6 +1127,7 @@
buildActionMask = 2147483647;
files = (
DDDB444829F8A9C900EE2349 /* String.swift in Sources */,
DDFFA7472B3A7F3C004730DB /* Bundle.swift in Sources */,
DD5E520C298EE33B00D21B61 /* portnums.pb.swift in Sources */,
DD457188293C7E63000C49FB /* BLESignalStrengthIndicator.swift in Sources */,
DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */,
@ -1186,6 +1191,7 @@
DDDB444A29F8AA3A00EE2349 /* CLLocationCoordinate2D.swift in Sources */,
DD41582628582E9B009B0E59 /* DeviceConfig.swift in Sources */,
DD007BAE2AA4E91200F5FA12 /* MyInfoEntityExtension.swift in Sources */,
DD33DB622B3D27C7003E1EA0 /* FirmwareApi.swift in Sources */,
DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */,
DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */,
DD5E5207298EE33B00D21B61 /* connection_status.pb.swift in Sources */,
@ -1209,7 +1215,6 @@
DD47E3D626F17ED900029299 /* CircleText.swift in Sources */,
DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */,
DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */,
DD2DC2C029BCD8AB003B383C /* HardwareModels.swift in Sources */,
DDB6ABD928B0A4BA00384BA1 /* BluetoothModes.swift in Sources */,
DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */,
DD2553592855B52700E55709 /* PositionConfig.swift in Sources */,
@ -1486,7 +1491,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.2.17;
MARKETING_VERSION = 2.2.18;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1520,7 +1525,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 2.2.17;
MARKETING_VERSION = 2.2.18;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = YES;
@ -1642,7 +1647,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.17;
MARKETING_VERSION = 2.2.18;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1675,7 +1680,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.17;
MARKETING_VERSION = 2.2.18;
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -1786,6 +1791,7 @@
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */,
DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */,
DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */,
DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */,
@ -1809,7 +1815,7 @@
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
);
currentVersion = DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */;
currentVersion = DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */;
name = Meshtastic.xcdatamodeld;
path = Meshtastic/Meshtastic.xcdatamodeld;
sourceTree = "<group>";

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "solarnode.jpeg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "solarnode 1.jpeg",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "solar_node.jpeg",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View file

@ -1,334 +0,0 @@
//
// HardwareModels.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 3/11/23.
//
import Foundation
enum HardwareModels: String, CaseIterable, Identifiable {
case UNSET
case TLORAV2
case TLORAV1
case TLORAV211P6
case TBEAM
case HELTECV20
case TBEAMV0P7
case TECHO
case TLORAV11P3
case RAK4631
case HELTECV21
case HELTECV1
case DIYV1
case LILYGOTBEAMS3CORE
case RAK11200
case NANOG1
case TLORAV211P8
case TLORAT3S3
case NANOG1EXPLORER
case STATIONG1
case M5STACK
case HELTECV3
case HELTECWSLV3
case NANOG2ULTRA
case RAK11310
case RPIPICO
case HELTECWIRELESSTRACKER
case HELTECWIRELESSPAPER
case TDECK
case TWATCHS3
var id: String { self.rawValue }
var description: String {
switch self {
case .UNSET:
return "unset".localized
case .TLORAV2:
return "TLoRa V2"
case .TLORAV1:
return "TLoRa V1"
case .TLORAV211P6:
return "TLoRa V2.1.1.6"
case .TBEAM:
return "TBeam"
case .HELTECV20:
return "HELTEC V2.0"
case .TBEAMV0P7:
return "TBeam 0.7"
case .TECHO:
return "TEcho"
case .TLORAV11P3:
return "TLORA V1.1.3"
case .RAK4631:
return "RAK 4631 NRF"
case .HELTECV21:
return "HELTEC V2.1"
case .HELTECV1:
return "HELTEC V1"
case .DIYV1:
return "Hydra 1W DIY"
case .LILYGOTBEAMS3CORE:
return "TBEAM S3"
case .RAK11200:
return "RAK 11200 ESP32"
case .NANOG1:
return "Nano G1"
case .TLORAV211P8:
return "TLoRa V2.1.1.8"
case .TLORAT3S3:
return "TLoRa T3 S3"
case .NANOG1EXPLORER:
return "Nano G1 Explorer"
case .STATIONG1:
return "Station G1"
case .M5STACK:
return "M5 Stack"
case .HELTECV3:
return "Heltec V3"
case .HELTECWSLV3:
return "Heltec wireless stick lite V3"
case .NANOG2ULTRA:
return "Nano G2 Ultra"
case .RAK11310:
return "RAK 11310 Pi Pico"
case .RPIPICO:
return "Pi Pico"
case .HELTECWIRELESSTRACKER:
return "Heltec Wireless Tracker"
case .HELTECWIRELESSPAPER:
return "Heltec Wireless Paper"
case .TDECK:
return "T-Deck"
case .TWATCHS3:
return "T-Watch S3"
}
}
var firmwareStrings: [String] {
switch self {
case .UNSET:
return []
case .TLORAV2:
return ["firmware-tlora-v2-"]
case .TLORAV1:
return ["firmware-tlora-v1-"]
case .TLORAV211P6:
return ["firmware-tlora-v2-1-1.6-"]
case .TBEAM:
return ["firmware-tbeam-"]
case .HELTECV20:
return ["firmware-heltec-v2.0-"]
case .TBEAMV0P7:
return ["firmware-tbeam0.7-"]
case .TECHO:
return ["firmware-t-echo-"]
case .TLORAV11P3:
return ["firmware-tlora_v1_3-"]
case .RAK4631:
return ["firmware-rak4631-", "firmware-rak4631_eink-"]
case .HELTECV21:
return ["firmware-heltec-v2.1-"]
case .HELTECV1:
return ["firmware-heltec-v1-"]
case .DIYV1:
return ["firmware-meshtastic-diy-v1"]
case .LILYGOTBEAMS3CORE:
return ["firmware-tbeam-s3-core-"]
case .RAK11200:
return ["firmware-rak11200-"]
case .NANOG1:
return ["firmware-nano-g1-"]
case .TLORAV211P8:
return ["firmware-tlora-v2-1-1.8-"]
case .TLORAT3S3:
return ["firmware-tlora-t3s3-v1-"]
case .NANOG1EXPLORER:
return ["firmware-nano-g1-explorer-"]
case .STATIONG1:
return ["firmware-station-g1-"]
case .M5STACK:
return ["firmware-m5stack-core-", "firmware-m5stack-coreink-"]
case .HELTECV3:
return ["firmware-heltec-v3-"]
case .HELTECWSLV3:
return ["firmware-heltec-wsl-v3-"]
case .NANOG2ULTRA:
return ["firmware-nano-g2-ultra-"]
case .RAK11310:
return ["firmware-rak11310-"]
case .RPIPICO:
return ["firmware-pico-"]
case .HELTECWIRELESSTRACKER:
return ["firmware-heltec-wireless-tracker-"]
case .HELTECWIRELESSPAPER:
return ["firmware-heltec-wireless-paper-"]
case .TDECK:
return ["firmware-t-echo-"]
case .TWATCHS3:
return ["firmware-t-watch-s3-"]
}
}
func platform() -> HardwarePlatforms {
switch self {
case .UNSET:
return HardwarePlatforms.none
case .TLORAV2:
return HardwarePlatforms.esp32
case .TLORAV1:
return HardwarePlatforms.esp32
case .TLORAV211P6:
return HardwarePlatforms.esp32
case .TBEAM:
return HardwarePlatforms.esp32
case .HELTECV20:
return HardwarePlatforms.esp32
case .TBEAMV0P7:
return HardwarePlatforms.esp32
case .TECHO:
return HardwarePlatforms.nrf52
case .TLORAV11P3:
return HardwarePlatforms.esp32
case .RAK4631:
return HardwarePlatforms.nrf52
case .HELTECV21:
return HardwarePlatforms.esp32
case .HELTECV1:
return HardwarePlatforms.esp32
case .DIYV1:
return HardwarePlatforms.esp32
case .LILYGOTBEAMS3CORE:
return HardwarePlatforms.esp32
case .RAK11200:
return HardwarePlatforms.esp32
case .NANOG1:
return HardwarePlatforms.esp32
case .TLORAV211P8:
return HardwarePlatforms.esp32
case .TLORAT3S3:
return HardwarePlatforms.esp32
case .NANOG1EXPLORER:
return HardwarePlatforms.esp32
case .STATIONG1:
return HardwarePlatforms.esp32
case .M5STACK:
return HardwarePlatforms.esp32
case .HELTECV3:
return HardwarePlatforms.esp32
case .HELTECWSLV3:
return HardwarePlatforms.esp32
case .NANOG2ULTRA:
return HardwarePlatforms.nrf52
case .RAK11310:
return HardwarePlatforms.piPico
case .RPIPICO:
return HardwarePlatforms.piPico
case .HELTECWIRELESSTRACKER:
return HardwarePlatforms.esp32
case .HELTECWIRELESSPAPER:
return HardwarePlatforms.esp32
case .TDECK:
return HardwarePlatforms.esp32
case .TWATCHS3:
return HardwarePlatforms.esp32
}
}
func protoEnumValue() -> HardwareModel {
switch self {
case .UNSET:
return HardwareModel.unset
case .TLORAV2:
return HardwareModel.tloraV2
case .TLORAV1:
return HardwareModel.tloraV1
case .TLORAV211P6:
return HardwareModel.tloraV211P6
case .TBEAM:
return HardwareModel.tbeam
case .HELTECV20:
return HardwareModel.heltecV20
case .TBEAMV0P7:
return HardwareModel.tbeamV0P7
case .TECHO:
return HardwareModel.tEcho
case .TLORAV11P3:
return HardwareModel.tloraV11P3
case .RAK4631:
return HardwareModel.rak4631
case .HELTECV21:
return HardwareModel.heltecV21
case .HELTECV1:
return HardwareModel.heltecV1
case .DIYV1:
return HardwareModel.diyV1
case .LILYGOTBEAMS3CORE:
return HardwareModel.lilygoTbeamS3Core
case .RAK11200:
return HardwareModel.rak11200
case .NANOG1:
return HardwareModel.nanoG1
case .TLORAV211P8:
return HardwareModel.tloraV211P8
case .TLORAT3S3:
return HardwareModel.tloraT3S3
case .NANOG1EXPLORER:
return HardwareModel.nanoG1Explorer
case .STATIONG1:
return HardwareModel.stationG1
case .M5STACK:
return HardwareModel.m5Stack
case .HELTECV3:
return HardwareModel.heltecV3
case .HELTECWSLV3:
return HardwareModel.heltecWslV3
case .NANOG2ULTRA:
return HardwareModel.nanoG2Ultra
case .RAK11310:
return HardwareModel.rak11310
case .RPIPICO:
return HardwareModel.rpiPico
case .HELTECWIRELESSTRACKER:
return HardwareModel.heltecWirelessTracker
case .HELTECWIRELESSPAPER:
return HardwareModel.heltecWirelessPaper
case .TDECK:
return HardwareModel.tDeck
case .TWATCHS3:
return HardwareModel.tWatchS3
}
}
}
enum HardwarePlatforms: String, CaseIterable, Identifiable {
case none
case esp32
case nrf52
case stm32
case piPico
case linux
var id: String { self.rawValue }
var description: String {
switch self {
case .none:
return "None"
case .esp32:
return "Expressif ESP 32"
case .nrf52:
return "Nordic NRF52"
case .stm32:
return "ARM STM 32"
case .piPico:
return "Raspberrry Pi Pico"
case .linux:
return "Linux"
}
}
}

View file

@ -111,6 +111,7 @@ enum SerialModeTypes: Int, CaseIterable, Identifiable {
case proto = 2
case txtmsg = 3
case nmea = 4
case caltopo = 5
var id: Int { self.rawValue }
var description: String {
@ -125,6 +126,8 @@ enum SerialModeTypes: Int, CaseIterable, Identifiable {
return "serial.mode.txtmsg".localized
case .nmea:
return "serial.mode.nmea".localized
case .caltopo:
return "serial.mode.caltopo".localized
}
}
func protoEnumValue() -> ModuleConfig.SerialConfig.Serial_Mode {
@ -141,6 +144,8 @@ enum SerialModeTypes: Int, CaseIterable, Identifiable {
return ModuleConfig.SerialConfig.Serial_Mode.textmsg
case .nmea:
return ModuleConfig.SerialConfig.Serial_Mode.nmea
case .caltopo:
return ModuleConfig.SerialConfig.Serial_Mode.caltopo
}
}
}

View file

@ -0,0 +1,22 @@
//
// Bundle.swift
// Meshtastic
//
// Created by Garth Vander Houwen on 12/25/23.
//
import Foundation
extension Bundle {
public var appName: String { getInfo("CFBundleName") }
public var displayName: String { getInfo("CFBundleDisplayName") }
public var language: String { getInfo("CFBundleDevelopmentRegion") }
public var identifier: String { getInfo("CFBundleIdentifier") }
public var copyright: String { getInfo("NSHumanReadableCopyright").replacingOccurrences(of: "\\\\n", with: "\n") }
public var appBuild: String { getInfo("CFBundleVersion") }
public var appVersionLong: String { getInfo("CFBundleShortVersionString") }
//public var appVersionShort: String { getInfo("CFBundleShortVersion") }
fileprivate func getInfo(_ str: String) -> String { infoDictionary?[str] as? String ?? "⚠️" }
}

View file

@ -532,7 +532,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
tryClearExistingChannels()
}
// NodeInfo
if decodedInfo.nodeInfo.num > 0 && !invalidVersion {
if decodedInfo.nodeInfo.num > 0 {// && !invalidVersion {
nowKnown = true
let nodeInfo = nodeInfoPacket(nodeInfo: decodedInfo.nodeInfo, channel: decodedInfo.packet.channel, context: context!)
@ -690,6 +690,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if let neighborInfo = try? NeighborInfo(serializedData: decodedInfo.packet.decoded.payload) {
MeshLogger.log("🕸️ MESH PACKET received for Neighbor Info App App UNHANDLED \(neighborInfo)")
}
case .paxcounterApp:
MeshLogger.log("🕸️ MESH PACKET received for PAX Counter App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
case .UNRECOGNIZED:
MeshLogger.log("🕸️ MESH PACKET received for Other App UNHANDLED \(try! decodedInfo.packet.jsonString())")
case .max:
@ -757,18 +759,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
// Use context to pass the radio name with the timer
// Use a RunLoop to prevent the timer from running on the main UI thread
if UserDefaults.provideLocation {
let interval = UserDefaults.provideLocationInterval > 0 ? UserDefaults.provideLocationInterval : 30
if positionTimer != nil {
}
let interval = UserDefaults.provideLocationInterval >= 10 ? UserDefaults.provideLocationInterval : 30
positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval(interval), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true)
if positionTimer != nil {
RunLoop.current.add(positionTimer!, forMode: .common)
}
}
return
}
case FROMNUM_UUID:
print("🗞️ BLE (Notify) characteristic, value will be read next")
@ -962,7 +960,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
return success
}
public func sendPosition(destNum: Int64, wantResponse: Bool) -> Bool {
public func sendPosition(channel: Int32, destNum: Int64, wantResponse: Bool) -> Bool {
var success = false
let fromNodeNum = connectedPeripheral.num
var positionPacket = Position()
@ -1016,6 +1014,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var meshPacket = MeshPacket()
meshPacket.to = UInt32(destNum)
meshPacket.channel = UInt32(channel)
meshPacket.from = UInt32(fromNodeNum)
var dataMessage = DataMessage()
dataMessage.payload = try! positionPacket.serializedData()
@ -1040,7 +1039,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
if connectedPeripheral != nil {
// Send a position out to the mesh if "share location with the mesh" is enabled in settings
if UserDefaults.provideLocation {
let _ = sendPosition(destNum: connectedPeripheral.num, wantResponse: false)
let _ = sendPosition(channel: 0, destNum: connectedPeripheral.num, wantResponse: false)
}
}
}

View file

@ -8,20 +8,23 @@
import SwiftUI
import CoreLocation
// Shared state that manages the `CLLocationManager` and `CLBackgroundActivitySession`.
@available(iOS 17.0, macOS 14.0, *)
@MainActor class LocationsHandler: ObservableObject {
static let shared = LocationsHandler() // Create a single, shared instance of the object.
private let manager: CLLocationManager
private var background: CLBackgroundActivitySession?
var locationsArray: [CLLocation]
var enableSmartPosition: Bool
//@Published var lastLocation = CLLocation()
@Published var locationsArray: [CLLocation]
@Published var isStationary = false
@Published var count = 0
@Published var isRecording = false
@Published var isRecordingPaused = false
@Published var recordingStarted: Date?
@Published var distanceTraveled = 0.0
@Published var elevationGain = 0.0
@Published
var updatesStarted: Bool = UserDefaults.standard.bool(forKey: "liveUpdatesStarted") {
@ -55,7 +58,7 @@ import CoreLocation
if !self.updatesStarted { break } // End location updates by breaking out of the loop.
if let loc = update.location {
self.isStationary = update.isStationary
self.count += 1
var locationAdded: Bool
if enableSmartPosition {
locationAdded = addLocation(loc)
@ -64,8 +67,8 @@ import CoreLocation
locationsArray.append(loc)
locationAdded = true
}
if !locationAdded {
//print("Bad Location \(self.count): \(loc)")
if locationAdded {
self.count += 1
}
}
}
@ -95,6 +98,16 @@ import CoreLocation
print("Bad Location \(self.count): Horizontal Accuracy: \(location.horizontalAccuracy) \(location)")
return false
}
if isRecording {
if let lastLocation = locationsArray.last {
let distance = location.distance(from: lastLocation)
let gain = location.altitude - lastLocation.altitude
distanceTraveled += distance
if gain > 0 {
elevationGain += gain
}
}
}
locationsArray.append(location)
return true
}
@ -103,7 +116,7 @@ import CoreLocation
static var satsInView: Int {
var sats = 0
if let newLocation = shared.locationsArray.last{
if let newLocation = shared.locationsArray.last {
sats = 1
if newLocation.verticalAccuracy > 0 {
sats = 4

View file

@ -194,7 +194,6 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, context: NS
}
if fetchedNode.count > 0 {
let newMetadata = DeviceMetadataEntity(context: context)
newMetadata.firmwareVersion = metadata.firmwareVersion
newMetadata.deviceStateVersion = Int32(metadata.deviceStateVersion)
newMetadata.canShutdown = metadata.canShutdown
newMetadata.hasWifi = metadata.hasWifi_p
@ -290,7 +289,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
position.longitudeI = nodeInfo.position.longitudeI
position.altitude = nodeInfo.position.altitude
position.satsInView = Int32(nodeInfo.position.satsInView)
position.speed = Int32(nodeInfo.position.groundSpeed)
position.speed = Int32(nodeInfo.position.groundSpeed * UInt32(3.6))
position.heading = Int32(nodeInfo.position.groundTrack)
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time)))
var newPostions = [PositionEntity]()

View file

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

View file

@ -0,0 +1,409 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22522" systemVersion="23D5033f" 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 &amp;&amp; toUser == nil AND isEmoji == false"/>
</fetchedProperty>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="index"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="DetectionSensorConfigEntity" representedClassName="DetectionSensorConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="detectionTriggeredHigh" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="minimumBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="monitorPin" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="stateBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="usePullup" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<relationship name="detectionSensorConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="detectionSensorConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="DeviceConfigEntity" representedClassName="DeviceConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="buttonGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="buzzerGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="debugLogEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="disableTripleClick" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="doubleTapAsButtonPress" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="isManaged" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="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="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"/>
<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="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="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>

View file

@ -250,7 +250,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
position.longitudeI = positionMessage.longitudeI
position.altitude = positionMessage.altitude
position.satsInView = Int32(positionMessage.satsInView)
position.speed = Int32(positionMessage.groundSpeed)
position.speed = Int32(positionMessage.groundSpeed * UInt32(3.6))
position.heading = Int32(positionMessage.groundTrack)
if positionMessage.timestamp != 0 {
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.timestamp)))

View file

@ -224,6 +224,17 @@ struct AdminMessage {
set {payloadVariant = .getNodeRemoteHardwarePinsResponse(newValue)}
}
///
/// Enter (UF2) DFU mode
/// Only implemented on NRF52 currently
var enterDfuModeRequest: Bool {
get {
if case .enterDfuModeRequest(let v)? = payloadVariant {return v}
return false
}
set {payloadVariant = .enterDfuModeRequest(newValue)}
}
///
/// Set the owner for this node
var setOwner: User {
@ -445,6 +456,10 @@ struct AdminMessage {
/// Respond with the mesh's nodes with their available gpio pins for RemoteHardware module use
case getNodeRemoteHardwarePinsResponse(NodeRemoteHardwarePinsResponse)
///
/// Enter (UF2) DFU mode
/// Only implemented on NRF52 currently
case enterDfuModeRequest(Bool)
///
/// Set the owner for this node
case setOwner(User)
///
@ -579,6 +594,10 @@ struct AdminMessage {
guard case .getNodeRemoteHardwarePinsResponse(let l) = lhs, case .getNodeRemoteHardwarePinsResponse(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.enterDfuModeRequest, .enterDfuModeRequest): return {
guard case .enterDfuModeRequest(let l) = lhs, case .enterDfuModeRequest(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.setOwner, .setOwner): return {
guard case .setOwner(let l) = lhs, case .setOwner(let r) = rhs else { preconditionFailure() }
return l == r
@ -763,6 +782,10 @@ struct AdminMessage {
///
/// TODO: REPLACE
case detectionsensorConfig // = 11
///
/// TODO: REPLACE
case paxcounterConfig // = 12
case UNRECOGNIZED(Int)
init() {
@ -783,6 +806,7 @@ struct AdminMessage {
case 9: self = .neighborinfoConfig
case 10: self = .ambientlightingConfig
case 11: self = .detectionsensorConfig
case 12: self = .paxcounterConfig
default: self = .UNRECOGNIZED(rawValue)
}
}
@ -801,6 +825,7 @@ struct AdminMessage {
case .neighborinfoConfig: return 9
case .ambientlightingConfig: return 10
case .detectionsensorConfig: return 11
case .paxcounterConfig: return 12
case .UNRECOGNIZED(let i): return i
}
}
@ -840,6 +865,7 @@ extension AdminMessage.ModuleConfigType: CaseIterable {
.neighborinfoConfig,
.ambientlightingConfig,
.detectionsensorConfig,
.paxcounterConfig,
]
}
@ -926,6 +952,7 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
18: .standard(proto: "set_ham_mode"),
19: .standard(proto: "get_node_remote_hardware_pins_request"),
20: .standard(proto: "get_node_remote_hardware_pins_response"),
21: .standard(proto: "enter_dfu_mode_request"),
32: .standard(proto: "set_owner"),
33: .standard(proto: "set_channel"),
34: .standard(proto: "set_config"),
@ -1141,6 +1168,14 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
self.payloadVariant = .getNodeRemoteHardwarePinsResponse(v)
}
}()
case 21: try {
var v: Bool?
try decoder.decodeSingularBoolField(value: &v)
if let v = v {
if self.payloadVariant != nil {try decoder.handleConflictingOneOf()}
self.payloadVariant = .enterDfuModeRequest(v)
}
}()
case 32: try {
var v: User?
var hadOneofValue = false
@ -1368,6 +1403,10 @@ extension AdminMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
guard case .getNodeRemoteHardwarePinsResponse(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 20)
}()
case .enterDfuModeRequest?: try {
guard case .enterDfuModeRequest(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularBoolField(value: v, fieldNumber: 21)
}()
case .setOwner?: try {
guard case .setOwner(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 32)
@ -1466,6 +1505,7 @@ extension AdminMessage.ModuleConfigType: SwiftProtobuf._ProtoNameProviding {
9: .same(proto: "NEIGHBORINFO_CONFIG"),
10: .same(proto: "AMBIENTLIGHTING_CONFIG"),
11: .same(proto: "DETECTIONSENSOR_CONFIG"),
12: .same(proto: "PAXCOUNTER_CONFIG"),
]
}

View file

@ -387,10 +387,7 @@ struct Config {
var gpsUpdateInterval: UInt32 = 0
///
/// How long should we try to get our position during each gps_update_interval attempt? (in seconds)
/// Or if zero, use the default of 30 seconds.
/// If we don't get a new gps fix in that time, the gps will be put into sleep until the next gps_update_rate
/// window.
/// Deprecated in favor of using smart / regular broadcast intervals as implicit attempt time
var gpsAttemptTime: UInt32 = 0
///
@ -1115,6 +1112,14 @@ struct Config {
///
/// Ukraine 868mhz
case ua868 // = 15
///
/// Malaysia 433mhz
case my433 // = 16
///
/// Malaysia 919mhz
case my919 // = 17
case UNRECOGNIZED(Int)
init() {
@ -1139,6 +1144,8 @@ struct Config {
case 13: self = .lora24
case 14: self = .ua433
case 15: self = .ua868
case 16: self = .my433
case 17: self = .my919
default: self = .UNRECOGNIZED(rawValue)
}
}
@ -1161,6 +1168,8 @@ struct Config {
case .lora24: return 13
case .ua433: return 14
case .ua868: return 15
case .my433: return 16
case .my919: return 17
case .UNRECOGNIZED(let i): return i
}
}
@ -1420,6 +1429,8 @@ extension Config.LoRaConfig.RegionCode: CaseIterable {
.lora24,
.ua433,
.ua868,
.my433,
.my919,
]
}
@ -2325,6 +2336,8 @@ extension Config.LoRaConfig.RegionCode: SwiftProtobuf._ProtoNameProviding {
13: .same(proto: "LORA_24"),
14: .same(proto: "UA_433"),
15: .same(proto: "UA_868"),
16: .same(proto: "MY_433"),
17: .same(proto: "MY_919"),
]
}

View file

@ -255,6 +255,17 @@ struct LocalModuleConfig {
/// Clears the value of `detectionSensor`. Subsequent reads from it will return its default value.
mutating func clearDetectionSensor() {_uniqueStorage()._detectionSensor = nil}
///
/// Paxcounter Config
var paxcounter: ModuleConfig.PaxcounterConfig {
get {return _storage._paxcounter ?? ModuleConfig.PaxcounterConfig()}
set {_uniqueStorage()._paxcounter = newValue}
}
/// Returns true if `paxcounter` has been explicitly set.
var hasPaxcounter: Bool {return _storage._paxcounter != nil}
/// Clears the value of `paxcounter`. Subsequent reads from it will return its default value.
mutating func clearPaxcounter() {_uniqueStorage()._paxcounter = nil}
///
/// A version integer used to invalidate old save files when we make
/// incompatible changes This integer is set at build time and is private to
@ -419,6 +430,7 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
11: .standard(proto: "neighbor_info"),
12: .standard(proto: "ambient_lighting"),
13: .standard(proto: "detection_sensor"),
14: .same(proto: "paxcounter"),
8: .same(proto: "version"),
]
@ -435,6 +447,7 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
var _neighborInfo: ModuleConfig.NeighborInfoConfig? = nil
var _ambientLighting: ModuleConfig.AmbientLightingConfig? = nil
var _detectionSensor: ModuleConfig.DetectionSensorConfig? = nil
var _paxcounter: ModuleConfig.PaxcounterConfig? = nil
var _version: UInt32 = 0
static let defaultInstance = _StorageClass()
@ -454,6 +467,7 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
_neighborInfo = source._neighborInfo
_ambientLighting = source._ambientLighting
_detectionSensor = source._detectionSensor
_paxcounter = source._paxcounter
_version = source._version
}
}
@ -486,6 +500,7 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
case 11: try { try decoder.decodeSingularMessageField(value: &_storage._neighborInfo) }()
case 12: try { try decoder.decodeSingularMessageField(value: &_storage._ambientLighting) }()
case 13: try { try decoder.decodeSingularMessageField(value: &_storage._detectionSensor) }()
case 14: try { try decoder.decodeSingularMessageField(value: &_storage._paxcounter) }()
default: break
}
}
@ -537,6 +552,9 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
try { if let v = _storage._detectionSensor {
try visitor.visitSingularMessageField(value: v, fieldNumber: 13)
} }()
try { if let v = _storage._paxcounter {
try visitor.visitSingularMessageField(value: v, fieldNumber: 14)
} }()
}
try unknownFields.traverse(visitor: &visitor)
}
@ -558,6 +576,7 @@ extension LocalModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
if _storage._neighborInfo != rhs_storage._neighborInfo {return false}
if _storage._ambientLighting != rhs_storage._ambientLighting {return false}
if _storage._detectionSensor != rhs_storage._detectionSensor {return false}
if _storage._paxcounter != rhs_storage._paxcounter {return false}
if _storage._version != rhs_storage._version {return false}
return true
}

View file

@ -204,6 +204,16 @@ struct ModuleConfig {
set {payloadVariant = .detectionSensor(newValue)}
}
///
/// TODO: REPLACE
var paxcounter: ModuleConfig.PaxcounterConfig {
get {
if case .paxcounter(let v)? = payloadVariant {return v}
return ModuleConfig.PaxcounterConfig()
}
set {payloadVariant = .paxcounter(newValue)}
}
var unknownFields = SwiftProtobuf.UnknownStorage()
///
@ -245,6 +255,9 @@ struct ModuleConfig {
///
/// TODO: REPLACE
case detectionSensor(ModuleConfig.DetectionSensorConfig)
///
/// TODO: REPLACE
case paxcounter(ModuleConfig.PaxcounterConfig)
#if !swift(>=4.1)
static func ==(lhs: ModuleConfig.OneOf_PayloadVariant, rhs: ModuleConfig.OneOf_PayloadVariant) -> Bool {
@ -300,6 +313,10 @@ struct ModuleConfig {
guard case .detectionSensor(let l) = lhs, case .detectionSensor(let r) = rhs else { preconditionFailure() }
return l == r
}()
case (.paxcounter, .paxcounter): return {
guard case .paxcounter(let l) = lhs, case .paxcounter(let r) = rhs else { preconditionFailure() }
return l == r
}()
default: return false
}
}
@ -551,6 +568,24 @@ struct ModuleConfig {
init() {}
}
///
/// Config for the Paxcounter Module
struct PaxcounterConfig {
// SwiftProtobuf.Message conformance is added in an extension below. See the
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
///
/// Enable the Paxcounter Module
var enabled: Bool = false
var paxcounterUpdateInterval: UInt32 = 0
var unknownFields = SwiftProtobuf.UnknownStorage()
init() {}
}
///
/// Serial Config
struct SerialConfig {
@ -1177,6 +1212,7 @@ extension ModuleConfig.NeighborInfoConfig: @unchecked Sendable {}
extension ModuleConfig.DetectionSensorConfig: @unchecked Sendable {}
extension ModuleConfig.AudioConfig: @unchecked Sendable {}
extension ModuleConfig.AudioConfig.Audio_Baud: @unchecked Sendable {}
extension ModuleConfig.PaxcounterConfig: @unchecked Sendable {}
extension ModuleConfig.SerialConfig: @unchecked Sendable {}
extension ModuleConfig.SerialConfig.Serial_Baud: @unchecked Sendable {}
extension ModuleConfig.SerialConfig.Serial_Mode: @unchecked Sendable {}
@ -1217,6 +1253,7 @@ extension ModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
10: .standard(proto: "neighbor_info"),
11: .standard(proto: "ambient_lighting"),
12: .standard(proto: "detection_sensor"),
13: .same(proto: "paxcounter"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -1381,6 +1418,19 @@ extension ModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
self.payloadVariant = .detectionSensor(v)
}
}()
case 13: try {
var v: ModuleConfig.PaxcounterConfig?
var hadOneofValue = false
if let current = self.payloadVariant {
hadOneofValue = true
if case .paxcounter(let m) = current {v = m}
}
try decoder.decodeSingularMessageField(value: &v)
if let v = v {
if hadOneofValue {try decoder.handleConflictingOneOf()}
self.payloadVariant = .paxcounter(v)
}
}()
default: break
}
}
@ -1440,6 +1490,10 @@ extension ModuleConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementat
guard case .detectionSensor(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 12)
}()
case .paxcounter?: try {
guard case .paxcounter(let v)? = self.payloadVariant else { preconditionFailure() }
try visitor.visitSingularMessageField(value: v, fieldNumber: 13)
}()
case nil: break
}
try unknownFields.traverse(visitor: &visitor)
@ -1770,6 +1824,44 @@ extension ModuleConfig.AudioConfig.Audio_Baud: SwiftProtobuf._ProtoNameProviding
]
}
extension ModuleConfig.PaxcounterConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = ModuleConfig.protoMessageName + ".PaxcounterConfig"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "enabled"),
2: .standard(proto: "paxcounter_update_interval"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
while let fieldNumber = try decoder.nextFieldNumber() {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularBoolField(value: &self.enabled) }()
case 2: try { try decoder.decodeSingularUInt32Field(value: &self.paxcounterUpdateInterval) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if self.enabled != false {
try visitor.visitSingularBoolField(value: self.enabled, fieldNumber: 1)
}
if self.paxcounterUpdateInterval != 0 {
try visitor.visitSingularUInt32Field(value: self.paxcounterUpdateInterval, fieldNumber: 2)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: ModuleConfig.PaxcounterConfig, rhs: ModuleConfig.PaxcounterConfig) -> Bool {
if lhs.enabled != rhs.enabled {return false}
if lhs.paxcounterUpdateInterval != rhs.paxcounterUpdateInterval {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension ModuleConfig.SerialConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = ModuleConfig.protoMessageName + ".SerialConfig"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [

View file

@ -118,6 +118,11 @@ enum PortNum: SwiftProtobuf.Enum {
/// ENCODING: IP Packet. Handled by the python API, firmware ignores this one and pases on.
case ipTunnelApp // = 33
///
/// Paxcounter lib included in the firmware
/// ENCODING: protobuf
case paxcounterApp // = 34
///
/// Provides a hardware serial interface to send and receive from the Meshtastic network.
/// Connect to the RX/TX pins of a device with 38400 8N1. Packets received from the Meshtastic
@ -206,6 +211,7 @@ enum PortNum: SwiftProtobuf.Enum {
case 10: self = .detectionSensorApp
case 32: self = .replyApp
case 33: self = .ipTunnelApp
case 34: self = .paxcounterApp
case 64: self = .serialApp
case 65: self = .storeForwardApp
case 66: self = .rangeTestApp
@ -236,6 +242,7 @@ enum PortNum: SwiftProtobuf.Enum {
case .detectionSensorApp: return 10
case .replyApp: return 32
case .ipTunnelApp: return 33
case .paxcounterApp: return 34
case .serialApp: return 64
case .storeForwardApp: return 65
case .rangeTestApp: return 66
@ -271,6 +278,7 @@ extension PortNum: CaseIterable {
.detectionSensorApp,
.replyApp,
.ipTunnelApp,
.paxcounterApp,
.serialApp,
.storeForwardApp,
.rangeTestApp,
@ -308,6 +316,7 @@ extension PortNum: SwiftProtobuf._ProtoNameProviding {
10: .same(proto: "DETECTION_SENSOR_APP"),
32: .same(proto: "REPLY_APP"),
33: .same(proto: "IP_TUNNEL_APP"),
34: .same(proto: "PAXCOUNTER_APP"),
64: .same(proto: "SERIAL_APP"),
65: .same(proto: "STORE_FORWARD_APP"),
66: .same(proto: "RANGE_TEST_APP"),

View file

@ -297,7 +297,9 @@ struct Connect: View {
}
}
.onAppear(perform: {
self.bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
})
}
#if canImport(ActivityKit)

View file

@ -373,7 +373,7 @@ struct ChannelMessageList: View {
focusedField = nil
replyMessageId = 0
if sendPositionWithMessage {
if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false) {
if bleManager.sendPosition(channel: Int32(channel.index), destNum: Int64(bleManager.emptyNodeNum), wantResponse: false) {
print("Location Sent")
}
}
@ -390,7 +390,7 @@ struct ChannelMessageList: View {
focusedField = nil
replyMessageId = 0
if sendPositionWithMessage {
if bleManager.sendPosition(destNum: Int64(channel.index), wantResponse: false) {
if bleManager.sendPosition(channel: Int32(channel.index), destNum: Int64(bleManager.emptyNodeNum), wantResponse: false) {
print("Location Sent")
}
}

View file

@ -325,7 +325,7 @@ struct UserMessageList: View {
focusedField = nil
replyMessageId = 0
if sendPositionWithMessage {
if bleManager.sendPosition(destNum: user.num, wantResponse: true) {
if bleManager.sendPosition(channel: 0, destNum: user.num, wantResponse: true) {
print("Location Sent")
}
}
@ -342,7 +342,7 @@ struct UserMessageList: View {
focusedField = nil
replyMessageId = 0
if sendPositionWithMessage {
if bleManager.sendPosition(destNum: user.num, wantResponse: true) {
if bleManager.sendPosition(channel: 0, destNum: user.num, wantResponse: true) {
print("Location Sent")
}
}

View file

@ -47,12 +47,12 @@ struct PositionLog: View {
}
TableColumn("Speed") { position in
let speed = Measurement(value: Double(position.speed), unit: UnitSpeed.kilometersPerHour)
Text(speed.formatted())
Text(speed.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))))
}
TableColumn("Heading") { position in
let degrees = Angle.degrees(Double(position.heading))
let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees)
Text("\(heading.formatted())")
Text(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))
}
TableColumn("SNR") { position in
Text("\(String(format: "%.2f", position.snr)) dB")

View file

@ -17,11 +17,32 @@ struct AboutMeshtastic: View {
List {
Section(header: Text("What is Meshtastic?")) {
Text("An open source, off-grid, decentralized, mesh network built to run on affordable, low-power devices.")
Text("An open source, off-grid, decentralized, mesh network that runs on affordable, low-power radios.")
.font(.title3)
}
Section(header: Text("Apple Apps")) {
if locale.region?.identifier ?? "US" == "US" {
HStack {
Image("SOLAR_NODE")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 75)
.cornerRadius(5)
.padding()
VStack(alignment: .leading) {
Link("Buy Complete Radios", destination: URL(string: "http://garthvh.com")!)
.font(.title2)
Text("Get custom waterproof solar and detection sensor router nodes, aluminium desktop nodes and rugged handsets.")
.font(.callout)
}
}
}
Link("Sponsor App Development", destination: URL(string: "https://github.com/sponsors/garthvh")!)
.font(.title2)
Link("GitHub Repository", destination: URL(string: "https://github.com/meshtastic/Meshtastic-Apple")!)
.font(.title2)
Button("Review the app") {
if let scene = UIApplication.shared.connectedScenes
.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
@ -29,17 +50,14 @@ struct AboutMeshtastic: View {
}
}
.font(.title2)
Link("Sponsor App Development", destination: URL(string: "https://github.com/sponsors/garthvh")!)
.font(.title2)
Link("GitHub Repository", destination: URL(string: "https://github.com/meshtastic/Meshtastic-Apple")!)
.font(.title2)
}
if locale.region?.identifier ?? "no locale" == "US" {
Section(header: Text("Get Devices")) {
Link("Buy Complete Radios", destination: URL(string: "http://garthvh.com")!)
.font(.title2)
}
Text("Version: \(Bundle.main.appVersionLong) (\(Bundle.main.appBuild)) ")
Text(Bundle.main.copyright)
.font(.system(size: 10, weight: .thin))
.multilineTextAlignment(.center)
}
Section(header: Text("Project information")) {
Link("Website", destination: URL(string: "https://meshtastic.org")!)
.font(.title2)

View file

@ -72,7 +72,9 @@ struct AdminMessageList: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
}
}
}

View file

@ -143,7 +143,13 @@ struct AppSettings: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
if provideLocationInterval <= 0 {
provideLocationInterval = 30
UserDefaults.provideLocationInterval = provideLocationInterval
}
if self.bleManager.context == nil {
self.bleManager.context = context
}
}
.onChange(of: blockRangeTest) { newBlockRangeTest in
UserDefaults.blockRangeTest = newBlockRangeTest

View file

@ -285,7 +285,9 @@ struct Channels: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
}
}
}

View file

@ -137,7 +137,9 @@ struct BluetoothConfig: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
setBluetoothValues()
// Need to request a BluetoothConfig from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.bluetoothConfig == nil {

View file

@ -103,7 +103,7 @@ struct DeviceConfig: View {
}
Section(header: Text("GPIO")) {
Picker("Button GPIO", selection: $buttonGPIO) {
ForEach(0..<46) {
ForEach(0..<48) {
if $0 == 0 {
Text("unset")
} else {
@ -113,7 +113,7 @@ struct DeviceConfig: View {
}
.pickerStyle(DefaultPickerStyle())
Picker("Buzzer GPIO", selection: $buzzerGPIO) {
ForEach(0..<46) {
ForEach(0..<48) {
if $0 == 0 {
Text("unset")
} else {
@ -232,7 +232,9 @@ struct DeviceConfig: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
setDeviceValues()
// Need to request a LoRaConfig from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.deviceConfig == nil {

View file

@ -192,7 +192,9 @@ struct DisplayConfig: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
setDisplayValues()
// Need to request a LoRaConfig from the remote node before allowing changes

View file

@ -246,10 +246,10 @@ struct LoRaConfig: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
setLoRaValues()
// Need to request a LoRaConfig from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.loRaConfig == nil {
print("empty lora config")
@ -314,6 +314,11 @@ struct LoRaConfig: View {
if newTxPower != node!.loRaConfig!.txPower { hasChanges = true }
}
}
.onChange(of: txEnabled) { newTxEnabled in
if node != nil && node!.loRaConfig != nil {
if newTxEnabled != node!.loRaConfig!.txEnabled { hasChanges = true }
}
}
}
func setLoRaValues() {
self.hopLimit = Int(node?.loRaConfig?.hopLimit ?? 3)

View file

@ -130,7 +130,9 @@ struct AmbientLightingConfig: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
setAmbientLightingConfigValue()
// Need to request a Ambient Lighting Config from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.ambientLightingConfig == nil {

View file

@ -124,7 +124,7 @@ struct CannedMessagesConfig: View {
.disabled(configPreset > 0)
Section(header: Text("Inputs")) {
Picker("Pin A", selection: $inputbrokerPinA) {
ForEach(0..<46) {
ForEach(0..<48) {
if $0 == 0 {
Text("unset")
} else {
@ -136,7 +136,7 @@ struct CannedMessagesConfig: View {
Text("GPIO pin for rotary encoder A port.")
.font(.caption)
Picker("Pin B", selection: $inputbrokerPinB) {
ForEach(0..<46) {
ForEach(0..<48) {
if $0 == 0 {
Text("unset")
} else {
@ -148,7 +148,7 @@ struct CannedMessagesConfig: View {
Text("GPIO pin for rotary encoder B port.")
.font(.caption)
Picker("Press Pin", selection: $inputbrokerPinPress) {
ForEach(0..<46) {
ForEach(0..<48) {
if $0 == 0 {
Text("unset")
} else {
@ -264,9 +264,10 @@ struct CannedMessagesConfig: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
setCannedMessagesValues()
// Need to request a CannedMessagesModuleConfig from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.cannedMessageConfig == nil {
print("empty canned messages module config")

View file

@ -139,7 +139,7 @@ struct DetectionSensorConfig: View {
.listRowSeparator(.visible)
.offset(y: -10)
Picker("GPIO Pin to monitor", selection: $monitorPin) {
ForEach(0..<46) {
ForEach(0..<48) {
if $0 == 0 {
Text("unset")
} else {
@ -240,9 +240,10 @@ struct DetectionSensorConfig: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
setDetectionSensorValues()
// Need to request a Detection Sensor Module Config from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.detectionSensorConfig == nil {
print("empty detection sensor module config")

View file

@ -99,7 +99,7 @@ struct ExternalNotificationConfig: View {
Text("If enabled, the 'output' Pin will be pulled active high, disabled means active low.")
.font(.caption)
Picker("Output pin GPIO", selection: $output) {
ForEach(0..<46) {
ForEach(0..<48) {
if $0 == 0 {
Text("unset")
} else {
@ -147,7 +147,7 @@ struct ExternalNotificationConfig: View {
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Picker("Output pin buzzer GPIO ", selection: $outputBuzzer) {
ForEach(0..<46) {
ForEach(0..<48) {
if $0 == 0 {
Text("unset")
} else {
@ -157,7 +157,7 @@ struct ExternalNotificationConfig: View {
}
.pickerStyle(DefaultPickerStyle())
Picker("Output pin vibra GPIO", selection: $outputVibra) {
ForEach(0..<46) {
ForEach(0..<48) {
if $0 == 0 {
Text("unset")
} else {
@ -226,9 +226,10 @@ struct ExternalNotificationConfig: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
setExternalNotificationValues()
// Need to request a TelemetryModuleConfig from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.externalNotificationConfig == nil {
print("empty external notification module config")

View file

@ -245,9 +245,10 @@ struct MQTTConfig: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
setMqttValues()
// Need to request a TelemetryModuleConfig from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.mqttConfig == nil {
print("empty mqtt module config")

View file

@ -117,9 +117,10 @@ struct RangeTestConfig: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
setRangeTestValues()
// Need to request a RangeTestModule Config from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.rangeTestConfig == nil {
print("empty range test module config")

View file

@ -115,7 +115,9 @@ struct RtttlConfig: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
setRtttLConfigValue()
// Need to request a Rtttl Config from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && (node?.rtttlConfig == nil || node?.rtttlConfig?.ringtone?.count ?? 0 == 0) {

View file

@ -23,7 +23,10 @@ struct SerialConfig: View {
@State var txd = 0
@State var baudRate = 0
@State var timeout = 0
@State var overrideConsoleSerialPort = false
@State var mode = 0
var body: some View {
VStack {
@ -96,7 +99,7 @@ struct SerialConfig: View {
Section(header: Text("GPIO")) {
Picker("Receive data (rxd) GPIO pin", selection: $rxd) {
ForEach(0..<46) {
ForEach(0..<48) {
if $0 == 0 {
Text("unset")
} else {
@ -107,7 +110,7 @@ struct SerialConfig: View {
.pickerStyle(DefaultPickerStyle())
Picker("Transmit data (txd) GPIO pin", selection: $txd) {
ForEach(0..<46) {
ForEach(0..<48) {
if $0 == 0 {
Text("unset")
} else {
@ -153,6 +156,7 @@ struct SerialConfig: View {
sc.txd = UInt32(txd)
sc.baud = SerialBaudRates(rawValue: baudRate)!.protoEnumValue()
sc.timeout = UInt32(timeout)
sc.overrideConsoleSerialPort = overrideConsoleSerialPort
sc.mode = SerialModeTypes(rawValue: mode)!.protoEnumValue()
let adminMessageId = bleManager.saveSerialModuleConfig(config: sc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
@ -176,8 +180,9 @@ struct SerialConfig: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
setSerialValues()
// Need to request a SerialModuleConfig from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.serialConfig == nil {
@ -231,6 +236,13 @@ struct SerialConfig: View {
if newTimeout != node!.serialConfig!.timeout { hasChanges = true }
}
}
.onChange(of: overrideConsoleSerialPort) { newOverrideConsoleSerialPort in
if node != nil && node!.serialConfig != nil {
if newOverrideConsoleSerialPort != node!.serialConfig!.overrideConsoleSerialPort { hasChanges = true }
}
}
.onChange(of: mode) { newMode in
if node != nil && node!.serialConfig != nil {
@ -248,6 +260,7 @@ struct SerialConfig: View {
self.baudRate = Int(node?.serialConfig?.baudRate ?? 0)
self.timeout = Int(node?.serialConfig?.timeout ?? 0)
self.mode = Int(node?.serialConfig?.mode ?? 0)
self.overrideConsoleSerialPort = false // node?.serialConfig?.overrideConsoleSerialPort ?? false
self.hasChanges = false
}
}

View file

@ -137,9 +137,10 @@ struct StoreForwardConfig: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
setDetectionSensorValues()
// Need to request a Detection Sensor Module Config from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.detectionSensorConfig == nil {
print("empty store and forward module config")

View file

@ -132,9 +132,10 @@ struct TelemetryConfig: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
setTelemetryValues()
// Need to request a TelemetryModuleConfig from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.telemetryConfig == nil {
print("empty telemetry module config")

View file

@ -166,9 +166,10 @@ struct NetworkConfig: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
setNetworkValues()
// Need to request a NetworkConfig from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.networkConfig == nil {
print("empty network config")

View file

@ -213,7 +213,7 @@ struct PositionConfig: View {
if deviceGpsEnabled {
Picker("GPS Receive GPIO", selection: $rxGpio) {
ForEach(0..<46) {
ForEach(0..<48) {
if $0 == 0 {
Text("unset")
} else {
@ -223,7 +223,7 @@ struct PositionConfig: View {
}
.pickerStyle(DefaultPickerStyle())
Picker("GPS Transmit GPIO", selection: $txGpio) {
ForEach(0..<46) {
ForEach(0..<48) {
if $0 == 0 {
Text("unset")
} else {
@ -233,7 +233,7 @@ struct PositionConfig: View {
}
.pickerStyle(DefaultPickerStyle())
Picker("GPS EN GPIO", selection: $gpsEnGpio) {
ForEach(0..<46) {
ForEach(0..<48) {
if $0 == 0 {
Text("unset")
} else {
@ -276,7 +276,7 @@ struct PositionConfig: View {
Button(buttonText) {
if fixedPosition {
_ = bleManager.sendPosition(destNum: node!.num, wantResponse: true)
_ = bleManager.sendPosition(channel: 0, destNum: node?.num ?? 0, wantResponse: true)
}
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
@ -324,10 +324,10 @@ struct PositionConfig: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
if self.bleManager.context == nil {
self.bleManager.context = context
}
setPositionValues()
// Need to request a PositionConfig from the remote node before allowing changes
if bleManager.connectedPeripheral != nil && node?.positionConfig == nil {
print("empty position config")

View file

@ -2,15 +2,9 @@
// Firmware.swift
// Meshtastic
//
// Created by Garth Vander Houwen on 3/10/23.
// Copyright(c) by Garth Vander Houwen on 3/10/23.
//
//
// About.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 10/6/22.
//
import SwiftUI
import StoreKit
@ -18,28 +12,32 @@ struct Firmware: View {
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
var node: NodeInfoEntity?
@State var minimumVersion = "2.1.0"
@State var minimumVersion = "2.2.16"
@State var version = ""
@State private var firmwareReleaseData: FirmwareRelease = FirmwareRelease()
var body: some View {
// NavigationSplitView {
NavigationStack {
let hwModel: HardwareModels = HardwareModels.allCases.first(where: { $0.rawValue == node?.user?.hwModel ?? "UNSET" }) ?? HardwareModels.UNSET
VStack(alignment: .leading) {
Text("Current Version: \(bleManager.connectedVersion)")
.font(.largeTitle)
Text("Your device supports the following firmware: ")
.font(.callout)
HStack {
ForEach(hwModel.firmwareStrings, id: \.self) { fs in
Text(fs).font(.callout)
}
}
.padding(.bottom)
@State private var currentDevice: DeviceHardware?
if hwModel.platform() == HardwarePlatforms.nrf52 {
var body: some View {
VStack {
VStack(alignment: .leading) {
let deviceString = currentDevice?.hwModelSlug.replacingOccurrences(of: "_", with: "")
Text("Your Device Model: \(currentDevice?.displayName ?? "Unknown")")
.font(.largeTitle)
VStack {
Image(deviceString ?? "UNSET")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200, height: 200)
.cornerRadius(5)
}
Text("Current Firmware Version: \(bleManager.connectedVersion)")
.font(.title)
if currentDevice?.architecture == Meshtastic.Architecture.nrf52840 {
VStack(alignment: .leading) {
if hwModel == HardwareModels.RAK4631 {
/// RAK 4631
if currentDevice?.hwModel == 9 {
Text("nRF OTA Device Firmware Update App")
.font(.title3)
Text("You can update your Meshtastic device over bluetooth using the Nordic DFU app. This currently works for RAK NRF devices.")
@ -53,13 +51,13 @@ struct Firmware: View {
.font(.callout)
}
}
} else if hwModel.platform() == HardwarePlatforms.esp32 {
} else if currentDevice?.architecture == Meshtastic.Architecture.esp32 {
VStack(alignment: .leading) {
Text("ESP32 Device Firmware Update")
.font(.title3)
Text("Currently the reccomended way to update ESP32 devices is using the web flasher from a chrome based browser. It does not work on mobile devices or over BLE.")
.font(.caption)
Link("Web Flasher", destination: URL(string: "https://flasher.meshtastic.org")!)
Link("Web Flasher", destination: URL(string: "https://flash.meshtastic.org")!)
.font(.callout)
.padding(.bottom)
Text("ESP 32 OTA update is a work in progress, click the button below to sent your device a reboot into ota admin message.")
@ -88,169 +86,170 @@ struct Firmware: View {
.font(.title3)
Text(node?.user?.hwModel ?? "UNSET")
.font(.title3)
Text(hwModel.platform().description)
.font(.title3)
// Text(hwModel.platform().description)
// .font(.title3)
}
}.padding()
}
.padding()
VStack(alignment: .leading) {
Text("Firmware Releases")
.font(.title3)
.padding([.leading, .trailing])
List {
Section(header: Text("Stable")) {
ForEach(firmwareReleaseData.releases?.stable ?? [], id: \.id) { fr in
Link(destination: URL(string: fr.zipUrl ?? "")!) {
HStack {
Text(fr.title ?? "Unknown")
.font(.caption)
Spacer()
Image(systemName: "square.and.arrow.down")
.font(.title3)
}
}
}
}
Section("Alpha") {
ForEach(firmwareReleaseData.releases?.alpha ?? [], id: \.id) { fr in
Link(destination: URL(string: fr.zipUrl ?? "")!) {
HStack {
Text(fr.title ?? "Unknown")
.font(.caption)
Spacer()
Image(systemName: "square.and.arrow.down")
.font(.title3)
}
}
}
}
Section("Pull Requests") {
ForEach(firmwareReleaseData.pullRequests ?? [], id: \.id) { fr in
Link(destination: URL(string: fr.zipUrl ?? "")!) {
HStack {
Text(fr.title ?? "Unknown")
.font(.caption)
Spacer()
Image(systemName: "square.and.arrow.down")
.font(.title3)
}
}
// List {
// Section(header: Text("Stable")) {
// ForEach(firmwareReleaseData.releases?.stable ?? [], id: \.id) { fr in
// Link(destination: URL(string: fr.zipUrl ?? "")!) {
// HStack {
// Text(fr.title ?? "Unknown")
// .font(.caption)
// Spacer()
// Image(systemName: "square.and.arrow.down")
// .font(.title3)
// }
// }
// }
// }
// Section("Alpha") {
// ForEach(firmwareReleaseData.releases?.alpha ?? [], id: \.id) { fr in
// Link(destination: URL(string: fr.zipUrl ?? "")!) {
// HStack {
// Text(fr.title ?? "Unknown")
// .font(.caption)
// Spacer()
// Image(systemName: "square.and.arrow.down")
// .font(.title3)
// }
// }
// }
// }
// Section("Pull Requests") {
// ForEach(firmwareReleaseData.pullRequests ?? [], id: \.id) { fr in
// Link(destination: URL(string: fr.zipUrl ?? "")!) {
// HStack {
// Text(fr.title ?? "Unknown")
// .font(.caption)
// Spacer()
// Image(systemName: "square.and.arrow.down")
// .font(.title3)
// }
// }
// }
// }
// }
}
.padding(.bottom, 5)
.onAppear() {
Api().loadDeviceHardwareData { (hw) in
for device in hw {
print(device)
let currentHardware = node?.user?.hwModel ?? "UNSET"
let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "")
if deviceString == currentHardware {
print("Selected: \(device)")
currentDevice = device
}
}
}
// Api().loadFirmwareReleaseData { (bks) in
// //sel = bks
// }
}
.padding(.bottom, 5)
.onAppear(perform: loadData)
.navigationTitle("Firmware Updates")
.navigationBarTitleDisplayMode(.inline)
}
}
func loadData() {
guard let url = URL(string: "https://api.meshtastic.org/github/firmware/list") else {
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, _, _ in
if let data = data {
if let response_obj = try? JSONDecoder().decode(FirmwareRelease.self, from: data) {
DispatchQueue.main.async {
self.firmwareReleaseData = response_obj
}
}
}
}.resume()
}
}
struct FirmwareRelease: Codable {
var releases: Releases? = Releases()
var pullRequests: [PullRequests]? = []
enum CodingKeys: String, CodingKey {
case releases = "Releases"
case pullRequests = "Pull Requests"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
releases = try values.decodeIfPresent(Releases.self, forKey: .releases )
pullRequests = try values.decodeIfPresent([PullRequests].self, forKey: .pullRequests )
}
init() {
}
}
struct Releases: Codable {
var stable: [Stable]? = []
var alpha: [Alpha]? = []
enum CodingKeys: String, CodingKey {
case stable = "Stable"
case alpha = "Alpha"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
stable = try values.decodeIfPresent([Stable].self, forKey: .stable )
alpha = try values.decodeIfPresent([Alpha].self, forKey: .alpha )
}
init() {}
}
struct Alpha: Codable {
var id: String?
var title: String?
var pageUrl: String?
var zipUrl: String?
enum CodingKeys: String, CodingKey {
case id = "id"
case title = "title"
case pageUrl = "page_url"
case zipUrl = "zip_url"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(String.self, forKey: .id )
title = try values.decodeIfPresent(String.self, forKey: .title )
pageUrl = try values.decodeIfPresent(String.self, forKey: .pageUrl )
zipUrl = try values.decodeIfPresent(String.self, forKey: .zipUrl )
}
init() {}
}
struct Stable: Codable {
var id: String?
var title: String?
var pageUrl: String?
var zipUrl: String?
enum CodingKeys: String, CodingKey {
case id = "id"
case title = "title"
case pageUrl = "page_url"
case zipUrl = "zip_url"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(String.self, forKey: .id )
title = try values.decodeIfPresent(String.self, forKey: .title )
pageUrl = try values.decodeIfPresent(String.self, forKey: .pageUrl )
zipUrl = try values.decodeIfPresent(String.self, forKey: .zipUrl )
}
init() {}
}
struct PullRequests: Codable {
var id: String?
var title: String?
var pageUrl: String?
var zipUrl: String?
enum CodingKeys: String, CodingKey {
case id = "id"
case title = "title"
case pageUrl = "page_url"
case zipUrl = "zip_url"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(String.self, forKey: .id )
title = try values.decodeIfPresent(String.self, forKey: .title )
pageUrl = try values.decodeIfPresent(String.self, forKey: .pageUrl )
zipUrl = try values.decodeIfPresent(String.self, forKey: .zipUrl )
}
init() {}
}
//struct FirmwareRelease: Codable {
// var releases: Releases? = Releases()
// var pullRequests: [PullRequests]? = []
// enum CodingKeys: String, CodingKey {
// case releases = "Releases"
// case pullRequests = "Pull Requests"
// }
// init(from decoder: Decoder) throws {
// let values = try decoder.container(keyedBy: CodingKeys.self)
// releases = try values.decodeIfPresent(Releases.self, forKey: .releases )
// pullRequests = try values.decodeIfPresent([PullRequests].self, forKey: .pullRequests )
// }
// init() {
// }
//}
//
//struct Releases: Codable {
// var stable: [Stable]? = []
// var alpha: [Alpha]? = []
// enum CodingKeys: String, CodingKey {
// case stable = "Stable"
// case alpha = "Alpha"
// }
// init(from decoder: Decoder) throws {
// let values = try decoder.container(keyedBy: CodingKeys.self)
// stable = try values.decodeIfPresent([Stable].self, forKey: .stable )
// alpha = try values.decodeIfPresent([Alpha].self, forKey: .alpha )
// }
// init() {}
//}
//
//struct Alpha: Codable {
// var id: String?
// var title: String?
// var pageUrl: String?
// var zipUrl: String?
// enum CodingKeys: String, CodingKey {
// case id = "id"
// case title = "title"
// case pageUrl = "page_url"
// case zipUrl = "zip_url"
// }
// init(from decoder: Decoder) throws {
// let values = try decoder.container(keyedBy: CodingKeys.self)
// id = try values.decodeIfPresent(String.self, forKey: .id )
// title = try values.decodeIfPresent(String.self, forKey: .title )
// pageUrl = try values.decodeIfPresent(String.self, forKey: .pageUrl )
// zipUrl = try values.decodeIfPresent(String.self, forKey: .zipUrl )
// }
// init() {}
//}
//
//struct Stable: Codable {
// var id: String?
// var title: String?
// var pageUrl: String?
// var zipUrl: String?
// enum CodingKeys: String, CodingKey {
// case id = "id"
// case title = "title"
// case pageUrl = "page_url"
// case zipUrl = "zip_url"
// }
// init(from decoder: Decoder) throws {
// let values = try decoder.container(keyedBy: CodingKeys.self)
// id = try values.decodeIfPresent(String.self, forKey: .id )
// title = try values.decodeIfPresent(String.self, forKey: .title )
// pageUrl = try values.decodeIfPresent(String.self, forKey: .pageUrl )
// zipUrl = try values.decodeIfPresent(String.self, forKey: .zipUrl )
// }
// init() {}
//}
//
//struct PullRequests: Codable {
// var id: String?
// var title: String?
// var pageUrl: String?
// var zipUrl: String?
// enum CodingKeys: String, CodingKey {
// case id = "id"
// case title = "title"
// case pageUrl = "page_url"
// case zipUrl = "zip_url"
// }
// init(from decoder: Decoder) throws {
// let values = try decoder.container(keyedBy: CodingKeys.self)
// id = try values.decodeIfPresent(String.self, forKey: .id )
// title = try values.decodeIfPresent(String.self, forKey: .title )
// pageUrl = try values.decodeIfPresent(String.self, forKey: .pageUrl )
// zipUrl = try values.decodeIfPresent(String.self, forKey: .zipUrl )
// }
// init() {}
//}

View file

@ -0,0 +1,63 @@
//
// FirmwareApi.swift
// Meshtastic
//
// Created by Garth Vander Houwen on 12/27/23.
//
import Foundation
//struct DeviceHardware: Codable {
// var hwModel: Int
// var hwModelSlug: String
// var platformioTarget: String
// var activelySupported: Bool
// var displayName: String
//}
struct DeviceHardware: Codable {
let hwModel: Int
let hwModelSlug, platformioTarget: String
let architecture: Architecture
let activelySupported: Bool
let displayName: String
}
enum Architecture: String, Codable {
case esp32 = "esp32"
case esp32C3 = "esp32-c3"
case esp32S3 = "esp32-s3"
case nrf52840 = "nrf52840"
case rp2040 = "rp2040"
}
class Api : ObservableObject{
// @Published var devices = [DeviceHardware]()
func loadDeviceHardwareData(completion:@escaping ([DeviceHardware]) -> ()) {
guard let url = URL(string: "https://api.meshtastic.org/resource/deviceHardware") else {
print("Invalid url...")
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
let deviceHardware = try! JSONDecoder().decode([DeviceHardware].self, from: data!)
//print(deviceHardware)
DispatchQueue.main.async {
completion(deviceHardware)
}
}.resume()
}
// func loadFirmwareReleaseData(completion:@escaping ([FirmwareRelease]) -> ()) {
// guard let url = URL(string: "https://api.meshtastic.org/github/firmware/list") else {
// print("Invalid url...")
// return
// }
// URLSession.shared.dataTask(with: url) { data, response, error in
// let deviceHardware = try! JSONDecoder().decode([FirmwareRelease].self, from: data!)
// print(deviceHardware)
// DispatchQueue.main.async {
// completion(deviceHardware)
// }
// }.resume()
// }
}

View file

@ -11,33 +11,37 @@ import CoreLocation
@available(iOS 17.0, macOS 14.0, *)
struct GPSStatus: View {
var largeFont: Font = .footnote
var smallFont: Font = .caption2
@ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared
var body: some View {
if let newLocation = locationsHandler.locationsArray.last {
let horizontalAccuracy = Measurement(value: newLocation.horizontalAccuracy, unit: UnitLength.meters)
let verticalAccuracy = Measurement(value: newLocation.verticalAccuracy, unit: UnitLength.meters)
let altitiude = Measurement(value: newLocation.altitude, unit: UnitLength.meters)
let speed = Measurement(value: newLocation.speed, unit: UnitSpeed.kilometersPerHour)
let speedAccuracy = Measurement(value: newLocation.speedAccuracy, unit: UnitSpeed.metersPerSecond)
let courseAccuracy = Measurement(value: newLocation.courseAccuracy, unit: UnitAngle.degrees)
let horizontalAccuracy = Measurement(value: newLocation.horizontalAccuracy, unit: UnitLength.meters)
let verticalAccuracy = Measurement(value: newLocation.verticalAccuracy, unit: UnitLength.meters)
let altitiude = Measurement(value: newLocation.altitude, unit: UnitLength.meters)
let speed = Measurement(value: newLocation.speed, unit: UnitSpeed.kilometersPerHour)
let speedAccuracy = Measurement(value: newLocation.speedAccuracy, unit: UnitSpeed.metersPerSecond)
let courseAccuracy = Measurement(value: newLocation.courseAccuracy, unit: UnitAngle.degrees)
Label("Coordinate \(String(format: "%.5f", newLocation.coordinate.latitude)), \(String(format: "%.5f", newLocation.coordinate.longitude))", systemImage: "mappin")
.font(.footnote)
.font(largeFont)
.textSelection(.enabled)
HStack {
Label("Accuracy \(horizontalAccuracy.formatted())", systemImage: "scope")
.font(.footnote)
.font(largeFont)
Label("Sats Estimate \(LocationsHandler.satsInView)", systemImage: "sparkles")
.font(.footnote)
.font(largeFont)
}
HStack {
if newLocation.verticalAccuracy > 0 {
Label("Altitude \(altitiude.formatted())", systemImage: "mountain.2")
.font(.footnote)
.font(largeFont)
}
Label("Accuracy \(verticalAccuracy.formatted())", systemImage: "lines.measurement.vertical")
.font(.caption2)
.font(smallFont)
}
HStack {
let degrees = Angle.degrees(newLocation.course)
@ -49,15 +53,15 @@ struct GPSStatus: View {
.symbolRenderingMode(.hierarchical)
.rotationEffect(degrees)
}
.font(.footnote)
.font(largeFont)
Label("Accuracy \(courseAccuracy.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))", systemImage: "safari")
.font(.caption2)
.font(smallFont)
}
HStack {
Label("Speed \(speed.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))))", systemImage: "speedometer")
.font(.footnote)
.font(largeFont)
Label("Accuracy \(speedAccuracy.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))))", systemImage: "gauge.with.dots.needle.bottom.50percent.badge.plus")
.font(.caption2)
.font(smallFont)
}
}
}

View file

@ -11,160 +11,277 @@ import MapKit
import CoreLocation
import CoreMotion
struct TimerDisplayObject {
var seconds: Int = 0
var minutes: Int = 0
var hours: Int = 0
var display: String {
if self.seconds == 0 {
"\(String(format: "%02d", self.hours)):\(String(format: "%02d", self.minutes)):00"
} else {
"\(String(format: "%02d", self.hours)):\(String(format: "%02d", self.minutes)):\(String(format: "%02d", self.seconds))"
}
}
var timeMinuteCalculator: Float { Float(hours*60+seconds/60+minutes) }
}
@available(iOS 17.0, macOS 14.0, *)
struct RouteRecorder: View {
@ObservedObject var locationsHandler = LocationsHandler.shared
@ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared
@Environment(\.managedObjectContext) var context
@State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic)
@State var isTimerRunning = false
//@State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true)
@State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic)
@State var isShowingDetails = false
@State var timer: Timer?
@Namespace var namespace
@Namespace var routerecorderscope
@State var timeElapsed: TimerDisplayObject = TimerDisplayObject()
@State var timerDisplay = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@State var recording: RouteEntity?
@State var color: Color = .blue
var body: some View {
VStack {
VStack {
VStack {
Map(position: $position, scope: routerecorderscope) {
UserAnnotation()
// ForEach(locations, id: \.id) { location in
// Marker(location.name, systemImage: location.icon, coordinate: location.location)
// .tint(location.colour)
// }
}
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)
}
.mapScope(routerecorderscope)
.mapControls {
MapUserLocationButton()
MapCompass()
MapScaleView()
MapPitchToggle()
}
.mapStyle(.hybrid(elevation: .realistic, showsTraffic: true))
.transition(.slide)
.mapControlVisibility(.visible)
.safeAreaInset(edge: .bottom) {
ZStack {
VStack {
HStack(spacing: 10) {
Spacer()
if isTimerRunning {
Button {
isShowingDetails = true
isTimerRunning = false
} label: {
Image(systemName: "pause.fill")
.frame(width: 60, height: 60)
}
.buttonStyle(.bordered)
.buttonBorderShape(.circle)
.matchedGeometryEffect(id: "Pause Button", in: namespace)
} else {
Button {
isShowingDetails = true
isTimerRunning = true
timeElapsed.seconds -= 1
} label: {
Image(systemName: "play.fill")
.frame(width: 60, height: 60)
}
.buttonStyle(.bordered)
.buttonBorderShape(.circle)
.matchedGeometryEffect(id: "Play Button", in: namespace)
}
Spacer()
}
}
.onReceive(timerDisplay) { _ in
if isTimerRunning {
timeElapsed.seconds += 1
if timeElapsed.seconds == 60 {
timeElapsed.seconds = 0
timeElapsed.minutes += 1
if timeElapsed.minutes == 60 {
timeElapsed.minutes = 0
timeElapsed.hours += 1
}
}
.mapStyle(mapStyle)
}
.ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
.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 {
HStack {
Text(timeElapsed.display)
.font(.largeTitle)
Text("Time Elapseed")
.font(.callout)
.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()
VStack(alignment: .leading) {
if let lastLocation = locationsHandler.locationsArray.last {
let horizontalAccuracy = Measurement(value: lastLocation.horizontalAccuracy, unit: UnitLength.meters)
let verticalAccuracy = Measurement(value: lastLocation.verticalAccuracy, unit: UnitLength.meters)
let altitiude = Measurement(value: lastLocation.altitude, unit: UnitLength.meters)
let speed = Measurement(value: lastLocation.speed, unit: UnitSpeed.kilometersPerHour)
List {
Label("Coordinate \(String(format: "%.5f", lastLocation.coordinate.latitude)), \(String(format: "%.5f", lastLocation.coordinate.longitude))", systemImage: "mappin")
.textSelection(.enabled)
Label("Horizontal Accuracy \(horizontalAccuracy.formatted())", systemImage: "scope")
if lastLocation.verticalAccuracy > 0 {
Label("Altitude \(altitiude.formatted())", systemImage: "mountain.2")
}
Label("Vertical Accuracy \(verticalAccuracy.formatted())", systemImage: "lines.measurement.vertical")
Label("Satellites Estimate \(LocationHelper.satsInView)", systemImage: "sparkles")
Label("\(locationsHandler.isStationary ? "Moving" : "Stationary")", systemImage: locationsHandler.isStationary ? "figure.walk.motion" : "figure.stand")
if lastLocation.speedAccuracy > 0 {
Label("Speed \(speed.formatted())", systemImage: "speedometer")
}
if lastLocation.courseAccuracy > 0 {
/// Heading
let degrees = Angle.degrees(Double(lastLocation.course))
Label {
let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees)
/// Text("Heading: \(heading.formatted())")
Text("Heading \(String(format: "%.2f", lastLocation.course))°")
.foregroundColor(.primary)
} icon: {
Image(systemName: "location.circle")
.symbolRenderingMode(.hierarchical)
.frame(width: 35)
.rotationEffect(degrees)
}
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")
}
.listStyle(.plain)
.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)
.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)")
}
}
}
}
.presentationDetents([.fraction(0.6)])
.presentationDragIndicator(.visible)
}
}
}

View file

@ -135,6 +135,7 @@ struct Routes: View {
}
}
}
.badge(route.locations?.count ?? 0)
.swipeActions {
Button(role: .destructive) {
context.delete(route)
@ -147,6 +148,7 @@ struct Routes: View {
Label("delete", systemImage: "trash")
}
}
}
.listStyle(.plain)
}

View file

@ -313,16 +313,15 @@ struct Settings: View {
}
}
.onAppear {
if self.bleManager.context == nil {
self.bleManager.context = context
}
self.preferredNodeNum = UserDefaults.preferredPeripheralNum
self.selectedNode = Int(bleManager.connectedPeripheral != nil ? UserDefaults.preferredPeripheralNum : 0)
if selectedNode == 0 {
self.selectedNode = Int(bleManager.connectedPeripheral != nil ? UserDefaults.preferredPeripheralNum : 0)
}
}
.listStyle(GroupedListStyle())
.navigationTitle("settings")
.navigationBarItems(leading:
MeshtasticLogo()
MeshtasticLogo()
)
}
detail: {

View file

@ -190,7 +190,6 @@ struct UserConfig: View {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
})
.onAppear {
self.bleManager.context = context
self.shortName = node?.user?.shortName ?? ""
self.longName = node?.user?.longName ?? ""
self.isLicensed = node?.user?.isLicensed ?? false

View file

@ -84,6 +84,7 @@
"encrypted"="Verschlüsselt";
"external.notification"="Externe Benachrichtigung";
"external.notification.config"="Einstellungen der externen Benachrichtigung";
"finish"="Finish";
"firmware.version"="Firmware Version";
"firmware.version.unsupported"="Nicht unterstützte Firmware Version erkannt. Kann nicht verbinden.";
"gas"="Gas";
@ -213,6 +214,7 @@
"on.boot"="Nur beim Starten";
"options"="Optionen";
"password"="Passwort";
"pause"="Pause";
"phone.gps"="Telefon GPS";
"phone.gps.interval.description"="Wie häufig das Telefon den Standort an das Gerät sendet. Standortaktualisierungen an das Mesh werden vom Gerät verwaltet.";
"position"="Position";
@ -228,8 +230,10 @@
"reply"="Antworten";
"received.ack"="Empfangsbestätigung";
"received.ack.real"="Recipient Ack";
"resume"="Resume";
"ringtone"="Ringtone";
"ringtone.config"="Ringtone Config";
"route.recorder"="Route Recorder";
"routes"="Routes";
"routing.acknowledged"="Bestätigt";
"routing.noroute"="Keine Route";
@ -264,6 +268,7 @@
"set.region"="Setze LoRa Region";
"standard"="Standard";
"standard.muted"="Standard Muted";
"start"="Start";
"storeforward"="Store & Forward";
"storeforward.config"="Store & Forward Config";
"storeforward.heartbeat"="Send Heartbeat";

View file

@ -88,6 +88,7 @@
"encrypted"="Encrypted";
"external.notification"="External Notification";
"external.notification.config"="External Notification Config";
"finish"="Finish";
"firmware.version"="Firmware Version";
"firmware.version.unsupported"="Unsupported Firmware Version Detected, unable to connect to device.";
"gas"="Gas";
@ -217,6 +218,7 @@
"on.boot"="On Boot Only";
"options"="Options";
"password"="Password";
"pause"="Pause";
"phone.gps"="Phone GPS";
"phone.gps.interval.description"="How frequently your phone will send your location to the device, location updates to the mesh are managed by the device.";
"position"="Position";
@ -232,8 +234,10 @@
"reboot.node"="Reboot node?";
"received.ack"="Received Ack";
"received.ack.real"="Recipient Ack";
"resume"="Resume";
"ringtone"="Ringtone";
"ringtone.config"="Ringtone Config";
"route.recorder"="Route Recorder";
"routes"="Routes";
"routing.acknowledged"="Acknowledged";
"routing.noroute"="No Route";
@ -268,6 +272,7 @@
"set.region"="Set LoRa Region";
"standard"="Standard";
"standard.muted"="Standard Muted";
"start"="Start";
"storeforward"="Store & Forward";
"storeforward.config"="Store & Forward Config";
"storeforward.heartbeat"="Send Heartbeat";

View file

@ -86,6 +86,7 @@
"encrypted"="Zaszyfrowany";
"external.notification"="Zewnętrzne Powiadomienie";
"external.notification.config"="Konfiguracja Zewnętrznego Powiadomienia";
"finish"="Finish";
"firmware.version"="Wersja Oprogramowania";
"firmware.version.unsupported"="Wykryto nieobsługiwany wersję oprogramowania, brak możliwości połączenia z urządzeniem.";
"gas"="Gaz";
@ -214,6 +215,7 @@
"on.boot"="Tylko przy uruchomieniu";
"options"="Opcje";
"password"="Hasło";
"pause"="Pause";
"phone.gps"="GPS telefonu";
"phone.gps.interval.description"="Jak często Twój telefon będzie wysyłał swoją lokalizację do urządzenia, aktualizacje lokalizacji w sieci są zarządzane przez urządzenie.";
"position"="Pozycja";
@ -229,8 +231,10 @@
"reboot.node"="Uruchomić ponownie węzeł?";
"received.ack"="Odebrano potwierdzenie";
"received.ack.real"="Odbiorca potwierdzenia";
"resume"="Resume";
"ringtone"="Dzwonek";
"ringtone.config"="Konfiguracja dzwonka";
"route.recorder"="Route Recorder";
"routes"="Routes";
"routing.acknowledged"="Potwierdzono";
"routing.noroute"="Brak trasy";
@ -265,6 +269,7 @@
"set.region"="Ustaw region LoRa";
"standard"="Standardowy";
"standard.muted"="Standardowy wyłączony";
"start"="Start";
"storeforward"="Store & Forward";
"storeforward.config"="Store & Forward Config";
"storeforward.heartbeat"="Send Heartbeat";

View file

@ -84,6 +84,7 @@
"encrypted"="加密";
"external.notification"="外部通知";
"external.notification.config"="外部通知配置";
"finish"="Finish";
"firmware.version"="固件版本";
"firmware.version.unsupported"="检测到不支持的固件版本,无法连接到电台。";
"gas"="Gas";
@ -213,6 +214,7 @@
"on.boot"="仅在启动时";
"options"="选项";
"password"="密码";
"pause"="Pause";
"phone.gps"="手机 GPS";
"phone.gps.interval.description"="电台通过手机获取定位的时间间隔,但是向 Mesh 网络中刷新定位的时间间隔由电台控制。";
"position"="定位";
@ -228,8 +230,10 @@
"reboot.node"="重启节点?";
"received.ack"="收到确认";
"received.ack.real"="收件人确认";
"resume"="Resume";
"ringtone"="铃声";
"ringtone.config"="铃声设置";
"route.recorder"="Route Recorder";
"routes"="Routes";
"routing.acknowledged"="确认";
"routing.noroute"="找不到目标";
@ -264,6 +268,7 @@
"set.region"="设置 LoRa 区域";
"standard"="标准";
"standard.muted"="标准静音";
"start"="Start";
"ssid"="SSID";
"storeforward"="储存 & 转发";
"storeforward.config"="储存 & 转发设置";