From 2f9351c6535a2dde0d053cd8099389471c73880d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 14 Jan 2024 19:54:00 -0800 Subject: [PATCH 01/11] Bump version, get firmware list from the json api --- Meshtastic.xcodeproj/project.pbxproj | 8 +- Meshtastic/Views/Settings/Firmware.swift | 135 +++++--------------- Meshtastic/Views/Settings/FirmwareApi.swift | 60 +++++---- Meshtastic/Views/Settings/Settings.swift | 39 +++--- 4 files changed, 89 insertions(+), 153 deletions(-) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 5ad009b7..bb725735 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1491,7 +1491,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.18; + MARKETING_VERSION = 2.2.19; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1525,7 +1525,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.18; + MARKETING_VERSION = 2.2.19; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1647,7 +1647,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.18; + MARKETING_VERSION = 2.2.19; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1680,7 +1680,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.18; + MARKETING_VERSION = 2.2.19; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 8878b0d9..0e929dca 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -17,23 +17,42 @@ struct Firmware: View { @State private var currentDevice: DeviceHardware? var body: some View { + + let supportedVersion = bleManager.connectedVersion == "0.0.0" || self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame VStack { VStack(alignment: .leading) { let deviceString = currentDevice?.hwModelSlug.replacingOccurrences(of: "_", with: "") - Text("Your Device Model: \(currentDevice?.displayName ?? "Unknown")") - .font(.largeTitle) - + HStack { + VStack { + Image(systemName: currentDevice?.activelySupported ?? false ? "checkmark.seal.fill" : "x.circle") + .font(.largeTitle) + .foregroundStyle(currentDevice?.activelySupported ?? false ? .green : .red) + Text( currentDevice?.activelySupported ?? false ? "Supported" : "Unsupported") + .foregroundStyle(.gray) + .font(.caption2) + } + Text("Device Model: \(currentDevice?.displayName ?? "Unknown")") + .font(.largeTitle) + } VStack { Image(deviceString ?? "UNSET") .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 200, height: 200) + .frame(width: 300, height: 300) .cornerRadius(5) } - Text("Current Firmware Version: \(bleManager.connectedVersion)") - .font(.title) - + if supportedVersion { + Text("Your Firmware is up to date") + .font(.title) + Text("Current Firmware Version: \(bleManager.connectedVersion)") + .font(.title2) + } else { + Text("Your Firmware is out of date") + .font(.title) + Text("Current Firmware Version: \(bleManager.connectedVersion), Minimium Firmware Version: \(minimumVersion)") + .font(.title2) + } if currentDevice?.architecture == Meshtastic.Architecture.nrf52840 { VStack(alignment: .leading) { /// RAK 4631 @@ -141,115 +160,19 @@ struct Firmware: View { .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 -// } + Api().loadFirmwareReleaseData { (bks) in + //sel = bks + } } .navigationTitle("Firmware Updates") .navigationBarTitleDisplayMode(.inline) } } } - -//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() {} -//} diff --git a/Meshtastic/Views/Settings/FirmwareApi.swift b/Meshtastic/Views/Settings/FirmwareApi.swift index 5eb2f488..ab480849 100644 --- a/Meshtastic/Views/Settings/FirmwareApi.swift +++ b/Meshtastic/Views/Settings/FirmwareApi.swift @@ -7,14 +7,6 @@ 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 @@ -31,9 +23,30 @@ enum Architecture: String, Codable { case rp2040 = "rp2040" } +struct FirmwareReleases: Codable { + let releases: Releases + let pullRequests: [PullRequest] +} + +struct PullRequest: Codable { + let id, title: String + let pageURL: String + let zipURL: String + + enum CodingKeys: String, CodingKey { + case id, title + case pageURL = "page_url" + case zipURL = "zip_url" + } +} + +// MARK: - Releases +struct Releases: Codable { + let stable, alpha: [PullRequest] +} + 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...") @@ -47,17 +60,18 @@ class Api : ObservableObject{ } }.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() -// } + + func loadFirmwareReleaseData(completion:@escaping (FirmwareReleases) -> ()) { + 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 firmwareReleases = try! JSONDecoder().decode(FirmwareReleases.self, from: data!) + print(firmwareReleases) + DispatchQueue.main.async { + completion(firmwareReleases) + } + }.resume() + } } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index a7baadf0..886acf8c 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -69,15 +69,14 @@ struct Settings: View { Text("routes") } .tag(SettingsSidebar.routes) - -// NavigationLink { -// RouteRecorder() -// } label: { -// Image(systemName: "record.circle") -// .symbolRenderingMode(.hierarchical) -// Text("route.recorder") -// } -// .tag(SettingsSidebar.routeRecorder) + NavigationLink { + RouteRecorder() + } label: { + Image(systemName: "record.circle") + .symbolRenderingMode(.hierarchical) + Text("route.recorder") + } + .tag(SettingsSidebar.routeRecorder) } let node = nodes.first(where: { $0.num == preferredNodeNum }) @@ -299,17 +298,17 @@ struct Settings: View { } .tag(SettingsSidebar.adminMessageLog) } -// Section(header: Text("Firmware")) { -// NavigationLink { -// Firmware(node: nodes.first(where: { $0.num == preferredNodeNum })) -// } label: { -// Image(systemName: "arrow.up.arrow.down.square") -// .symbolRenderingMode(.hierarchical) -// Text("Firmware Updates") -// } -// .tag(SettingsSidebar.about) -// .disabled(selectedNode > 0 && selectedNode != preferredNodeNum) -// } + Section(header: Text("Firmware")) { + NavigationLink { + Firmware(node: nodes.first(where: { $0.num == preferredNodeNum })) + } label: { + Image(systemName: "arrow.up.arrow.down.square") + .symbolRenderingMode(.hierarchical) + Text("Firmware Updates") + } + .tag(SettingsSidebar.about) + .disabled(selectedNode > 0 && selectedNode != preferredNodeNum) + } } } .onAppear { From ca423e614e2cdd7b5512d605535da4199a4e9551 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 14 Jan 2024 20:26:45 -0800 Subject: [PATCH 02/11] Add enter dfu mode button for rak devices on firmware view --- Meshtastic/Helpers/BLEManager.swift | 20 ++++ Meshtastic/Views/Settings/Firmware.swift | 146 +++++++++++++---------- 2 files changed, 106 insertions(+), 60 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 0e7e4ae8..2f262eb4 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1107,6 +1107,26 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return false } + public func sendEnterDfuMode(fromUser: UserEntity, toUser: UserEntity) -> Bool { + var adminPacket = AdminMessage() + adminPacket.enterDfuModeRequest = true + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.factoryReset = 5 diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 0e929dca..3e6a56ef 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -12,17 +12,17 @@ struct Firmware: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager var node: NodeInfoEntity? - @State var minimumVersion = "2.2.16" + @State var minimumVersion = "2.2.17" @State var version = "" @State private var currentDevice: DeviceHardware? - + var body: some View { let supportedVersion = bleManager.connectedVersion == "0.0.0" || self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame - VStack { + ScrollView { VStack(alignment: .leading) { let deviceString = currentDevice?.hwModelSlug.replacingOccurrences(of: "_", with: "") - + HStack { VStack { Image(systemName: currentDevice?.activelySupported ?? false ? "checkmark.seal.fill" : "x.circle") @@ -34,6 +34,7 @@ struct Firmware: View { } Text("Device Model: \(currentDevice?.displayName ?? "Unknown")") .font(.largeTitle) + .fixedSize(horizontal: false, vertical: true) } VStack { Image(deviceString ?? "UNSET") @@ -42,6 +43,7 @@ struct Firmware: View { .frame(width: 300, height: 300) .cornerRadius(5) } + if supportedVersion { Text("Your Firmware is up to date") .font(.title) @@ -49,20 +51,44 @@ struct Firmware: View { .font(.title2) } else { Text("Your Firmware is out of date") - .font(.title) - Text("Current Firmware Version: \(bleManager.connectedVersion), Minimium Firmware Version: \(minimumVersion)") + .fixedSize(horizontal: false, vertical: true) + .foregroundStyle(.red) .font(.title2) + .padding(.bottom) + Text("Current Firmware Version: \(bleManager.connectedVersion), Minimium Firmware Version: \(minimumVersion)") + .fixedSize(horizontal: false, vertical: true) + .font(.title3) + .padding(.bottom) } + if currentDevice?.architecture == Meshtastic.Architecture.nrf52840 { VStack(alignment: .leading) { /// 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.") - .font(.caption) + Text("You can update your Meshtastic device over bluetooth using the Nordic DFU app.") + .fixedSize(horizontal: false, vertical: true) + .font(.callout) Link("Get NRF DFU from the App Store", destination: URL(string: "https://apps.apple.com/us/app/nrf-device-firmware-update/id1624454660")!) .font(.callout) + .padding(.bottom) + Link("Drag & Drop Firmware Update", destination: URL(string: "https://meshtastic.org/docs/getting-started/flashing-firmware/nrf52/drag-n-drop")!) + .font(.callout) + + Button { + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) + if connectedNode != nil { + if !bleManager.sendEnterDfuMode(fromUser: connectedNode!.user!, toUser: node!.user!) { + print("Enter DFU Failed") + } + } + } label: { + Label("Enter DFU Mode", systemImage: "square.and.arrow.down") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.regular) + .padding(5) + Spacer() } else { Text("OTA Updates are not supported on the this NRF Device.") .font(.title3) @@ -105,56 +131,56 @@ struct Firmware: View { .font(.title3) Text(node?.user?.hwModel ?? "UNSET") .font(.title3) - // Text(hwModel.platform().description) - // .font(.title3) + Text ( currentDevice?.architecture.rawValue ?? "UNKNOWN") + .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) -// } -// } -// } -// } -// } + // 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) + // } + // } + // } + // } + // } } .padding(.bottom, 5) .onAppear() { @@ -167,9 +193,9 @@ struct Firmware: View { } } } - Api().loadFirmwareReleaseData { (bks) in - //sel = bks - } + //Api().loadFirmwareReleaseData { (bks) in + //sel = bks + //} } .navigationTitle("Firmware Updates") .navigationBarTitleDisplayMode(.inline) From 30dc0874bcc076cd9a11e909932210c0d9c3a9d0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 14 Jan 2024 21:00:54 -0800 Subject: [PATCH 03/11] Fix some store and forwared admin config bugs --- Meshtastic/Helpers/BLEManager.swift | 27 ++++++++ .../Settings/Config/Module/StoreForward.swift | 10 +-- Meshtastic/Views/Settings/Firmware.swift | 67 ++++++++++++------- 3 files changed, 73 insertions(+), 31 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 2f262eb4..d65053af 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -2277,6 +2277,33 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return false } + public func requestStoreAndForwardModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { + + var adminPacket = AdminMessage() + adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.storeforwardConfig + + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForward.swift b/Meshtastic/Views/Settings/Config/Module/StoreForward.swift index 7241442f..483ff4cc 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForward.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForward.swift @@ -44,7 +44,7 @@ struct StoreForwardConfig: View { Text("Remote administration for: \(node?.user?.longName ?? "Unknown")") .font(.title3) .onAppear { - setDetectionSensorValues() + setStoreAndForwardValues() } } } else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 { @@ -140,13 +140,13 @@ struct StoreForwardConfig: View { if self.bleManager.context == nil { self.bleManager.context = context } - setDetectionSensorValues() + setStoreAndForwardValues() // Need to request a Detection Sensor Module Config from the remote node before allowing changes - if bleManager.connectedPeripheral != nil && node?.detectionSensorConfig == nil { + if bleManager.connectedPeripheral != nil && node?.storeForwardConfig == nil { print("empty store and forward module config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if node != nil && connectedNode != nil { - _ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + _ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) } } } @@ -176,7 +176,7 @@ struct StoreForwardConfig: View { } } } - func setDetectionSensorValues() { + func setStoreAndForwardValues() { self.enabled = (node?.storeForwardConfig?.enabled ?? false) self.heartbeat = (node?.storeForwardConfig?.heartbeat ?? true) self.records = Int(node?.storeForwardConfig?.records ?? 50) diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 3e6a56ef..7395af18 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -46,9 +46,14 @@ struct Firmware: View { if supportedVersion { Text("Your Firmware is up to date") - .font(.title) - Text("Current Firmware Version: \(bleManager.connectedVersion)") + .fixedSize(horizontal: false, vertical: true) + .foregroundStyle(.green) .font(.title2) + .padding(.bottom) + Text("Current Firmware Version: \(bleManager.connectedVersion)") + .fixedSize(horizontal: false, vertical: true) + .font(.title3) + .padding(.bottom) } else { Text("Your Firmware is out of date") .fixedSize(horizontal: false, vertical: true) @@ -60,35 +65,45 @@ struct Firmware: View { .font(.title3) .padding(.bottom) } - + Divider() + Text("How to update Firmware") + .fixedSize(horizontal: false, vertical: true) + .font(.title2) + .padding(.bottom) if currentDevice?.architecture == Meshtastic.Architecture.nrf52840 { VStack(alignment: .leading) { + + Text("Drag & Drop is the reccomended way to update firmware for NRF devices.") + .fixedSize(horizontal: false, vertical: true) + .foregroundStyle(.gray) + .font(.caption) + Link("Drag & Drop Firmware Update", destination: URL(string: "https://meshtastic.org/docs/getting-started/flashing-firmware/nrf52/drag-n-drop")!) + .font(.callout) + + Button { + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) + if connectedNode != nil { + if !bleManager.sendEnterDfuMode(fromUser: connectedNode!.user!, toUser: node!.user!) { + print("Enter DFU Failed") + } + } + } label: { + Label("Enter DFU Mode", systemImage: "square.and.arrow.down") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.regular) + .padding(5) + Spacer() /// RAK 4631 if currentDevice?.hwModel == 9 { - Text("You can update your Meshtastic device over bluetooth using the Nordic DFU app.") + Text("You can also update your Meshtastic device over bluetooth using the Nordic DFU app.") .fixedSize(horizontal: false, vertical: true) - .font(.callout) + .foregroundStyle(.gray) + .font(.caption) Link("Get NRF DFU from the App Store", destination: URL(string: "https://apps.apple.com/us/app/nrf-device-firmware-update/id1624454660")!) .font(.callout) .padding(.bottom) - Link("Drag & Drop Firmware Update", destination: URL(string: "https://meshtastic.org/docs/getting-started/flashing-firmware/nrf52/drag-n-drop")!) - .font(.callout) - - Button { - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) - if connectedNode != nil { - if !bleManager.sendEnterDfuMode(fromUser: connectedNode!.user!, toUser: node!.user!) { - print("Enter DFU Failed") - } - } - } label: { - Label("Enter DFU Mode", systemImage: "square.and.arrow.down") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.regular) - .padding(5) - Spacer() } else { Text("OTA Updates are not supported on the this NRF Device.") .font(.title3) @@ -193,9 +208,9 @@ struct Firmware: View { } } } - //Api().loadFirmwareReleaseData { (bks) in - //sel = bks - //} + Api().loadFirmwareReleaseData { (fw) in + + } } .navigationTitle("Firmware Updates") .navigationBarTitleDisplayMode(.inline) From 1437f1b5e429a3ae287d2058fb2dbf0c4a661bf4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 14 Jan 2024 22:03:02 -0800 Subject: [PATCH 04/11] Make channel key text so selection works properly --- Meshtastic/Views/Settings/Channels.swift | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 15b4a508..39d69d8e 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -37,7 +37,7 @@ struct Channels: View { var body: some View { - NavigationStack { + VStack { List { if node != nil && node?.myInfo != nil { ForEach(node?.myInfo?.channels?.array as? [ChannelEntity] ?? [], id: \.self) { (channel: ChannelEntity) in @@ -167,16 +167,17 @@ struct Channels: View { HStack(alignment: .top) { Text("Key") Spacer() - TextField( - "", - text: $channelKey, - axis: .vertical - ) - .foregroundColor(Color.gray) - .disabled(true) - + Text(channelKey) + .foregroundColor(Color.gray) + .textSelection(.enabled) +// TextField( +// "", +// text: $channelKey, +// axis: .vertical +// ) +// .foregroundColor(Color.gray) +// .disabled(true) } - .textSelection(.enabled) Picker("Channel Role", selection: $channelRole) { if channelRole == 1 { Text("Primary").tag(1) @@ -192,9 +193,6 @@ struct Channels: View { Toggle("Downlink Enabled", isOn: $downlink) .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } - // .onSubmit { - // validate(name: channelName) - // } .onChange(of: channelName) { _ in hasChanges = true } From eafa351266a710232804a19a161ea728665c807d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 14 Jan 2024 23:54:10 -0800 Subject: [PATCH 05/11] Handle metadata coming in before nodeinfo Clean up firmware view --- Meshtastic/Helpers/MeshPackets.swift | 54 ++++++---- Meshtastic/Views/Settings/About.swift | 4 - Meshtastic/Views/Settings/Firmware.swift | 108 ++++++++------------ Meshtastic/Views/Settings/FirmwareApi.swift | 7 +- 4 files changed, 82 insertions(+), 91 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 83cb25db..7c8ceb7f 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -192,29 +192,43 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, context: NS guard let fetchedNode = try context.fetch(fetchedNodeRequest) as? [NodeInfoEntity] else { return } + let newMetadata = DeviceMetadataEntity(context: context) + newMetadata.deviceStateVersion = Int32(metadata.deviceStateVersion) + newMetadata.canShutdown = metadata.canShutdown + newMetadata.hasWifi = metadata.hasWifi_p + newMetadata.hasBluetooth = metadata.hasBluetooth_p + newMetadata.hasEthernet = metadata.hasEthernet_p + newMetadata.role = Int32(metadata.role.rawValue) + newMetadata.positionFlags = Int32(metadata.positionFlags) + // Swift does strings weird, this does work to get the version without the github hash + let lastDotIndex = metadata.firmwareVersion.lastIndex(of: ".") + var version = metadata.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset: 6, in: metadata.firmwareVersion))] + version = version.dropLast() + newMetadata.firmwareVersion = String(version) if fetchedNode.count > 0 { - let newMetadata = DeviceMetadataEntity(context: context) - newMetadata.deviceStateVersion = Int32(metadata.deviceStateVersion) - newMetadata.canShutdown = metadata.canShutdown - newMetadata.hasWifi = metadata.hasWifi_p - newMetadata.hasBluetooth = metadata.hasBluetooth_p - newMetadata.hasEthernet = metadata.hasEthernet_p - newMetadata.role = Int32(metadata.role.rawValue) - newMetadata.positionFlags = Int32(metadata.positionFlags) - // Swift does strings weird, this does work to get the version without the github hash - let lastDotIndex = metadata.firmwareVersion.lastIndex(of: ".") - var version = metadata.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset: 6, in: metadata.firmwareVersion))] - version = version.dropLast() - newMetadata.firmwareVersion = String(version) fetchedNode[0].metadata = newMetadata - - do { - try context.save() - } catch { - print("Failed to save device metadata") - } - print("💾 Updated Device Metadata from Admin App Packet For: \(fromNum)") + } else { + + let newNode = NodeInfoEntity(context: context) + newNode.id = Int64(fromNum) + newNode.num = Int64(fromNum) + let newUser = UserEntity(context: context) + newUser.num = Int64(fromNum) + let userId = String(format:"%2X", fromNum) + newUser.userId = "!\(userId)" + let last4 = String(userId.suffix(4)) + newUser.longName = "Meshtastic \(last4)" + newUser.shortName = last4 + newUser.hwModel = "UNSET" + newNode.user = newUser + newNode.metadata = newMetadata } + do { + try context.save() + } catch { + print("Failed to save device metadata") + } + print("💾 Updated Device Metadata from Admin App Packet For: \(fromNum)") } catch { context.rollback() let nsError = error as NSError diff --git a/Meshtastic/Views/Settings/About.swift b/Meshtastic/Views/Settings/About.swift index 9bcb5b72..be1b4dc2 100644 --- a/Meshtastic/Views/Settings/About.swift +++ b/Meshtastic/Views/Settings/About.swift @@ -52,10 +52,6 @@ struct AboutMeshtastic: View { .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")) { diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 7395af18..c7123198 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -15,6 +15,8 @@ struct Firmware: View { @State var minimumVersion = "2.2.17" @State var version = "" @State private var currentDevice: DeviceHardware? + @State private var latestStable: FirmwareRelease? + @State private var latestAlpha: FirmwareRelease? var body: some View { @@ -70,30 +72,55 @@ struct Firmware: View { .fixedSize(horizontal: false, vertical: true) .font(.title2) .padding(.bottom) + + Text("Get the latest stable firmware") + .fixedSize(horizontal: false, vertical: true) + .font(.callout) + Link("\(latestStable?.title ?? "unknown".localized)", destination: URL(string: "\(latestStable?.zipURL ?? "https://meshtastic.org")")!) + .font(.caption) + Link("Release Notes", destination: URL(string: "\(latestStable?.pageURL ?? "https://meshtastic.org")")!) + .font(.caption) + .padding(.bottom) + + Text("Get the latest alpha firmware") + .fixedSize(horizontal: false, vertical: true) + .font(.callout) + Link("\(latestAlpha?.title ?? "unknown".localized)", destination: URL(string: "\(latestAlpha?.zipURL ?? "https://meshtastic.org")")!) + .font(.caption) + Link("Release Notes", destination: URL(string: "\(latestAlpha?.pageURL ?? "https://meshtastic.org")")!) + .font(.caption) + .padding(.bottom) + if currentDevice?.architecture == Meshtastic.Architecture.nrf52840 { VStack(alignment: .leading) { - Text("Drag & Drop is the reccomended way to update firmware for NRF devices.") + Text("Drag & Drop is the reccomended way to update firmware for NRF devices. If your iPhone or iPad is USB-C it will work with your regular USB-C charging cable, for lightning devices you need the Apple Lightning to USB camera adaptor.") .fixedSize(horizontal: false, vertical: true) .foregroundStyle(.gray) .font(.caption) - Link("Drag & Drop Firmware Update", destination: URL(string: "https://meshtastic.org/docs/getting-started/flashing-firmware/nrf52/drag-n-drop")!) - .font(.callout) - - Button { - let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) - if connectedNode != nil { - if !bleManager.sendEnterDfuMode(fromUser: connectedNode!.user!, toUser: node!.user!) { - print("Enter DFU Failed") + Link("Drag & Drop Firmware Update Documentation", destination: URL(string: "https://meshtastic.org/docs/getting-started/flashing-firmware/nrf52/drag-n-drop")!) + .font(.caption) + .padding(.bottom) + VStack { + Text("If it is hard to access your device's reset button enter DFU mode here.") + .fixedSize(horizontal: false, vertical: true) + .foregroundStyle(.gray) + .font(.caption) + Button { + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) + if connectedNode != nil { + if !bleManager.sendEnterDfuMode(fromUser: connectedNode!.user!, toUser: node!.user!) { + print("Enter DFU Failed") + } } + } label: { + Label("Enter DFU Mode", systemImage: "square.and.arrow.down") } - } label: { - Label("Enter DFU Mode", systemImage: "square.and.arrow.down") + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.regular) + .padding(5) } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.regular) - .padding(5) Spacer() /// RAK 4631 if currentDevice?.hwModel == 9 { @@ -115,7 +142,7 @@ struct Firmware: View { 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.") + Text("Currently the reccomended way to update ESP32 devices is using the web flasher on a desktop computer from a chrome based browser. It does not work on mobile devices or over BLE.") .font(.caption) Link("Web Flasher", destination: URL(string: "https://flash.meshtastic.org")!) .font(.callout) @@ -151,52 +178,6 @@ struct Firmware: View { } } .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) - // } - // } - // } - // } - // } - } .padding(.bottom, 5) .onAppear() { Api().loadDeviceHardwareData { (hw) in @@ -209,7 +190,8 @@ struct Firmware: View { } } Api().loadFirmwareReleaseData { (fw) in - + latestStable = fw.releases.stable.first + latestAlpha = fw.releases.alpha.first } } .navigationTitle("Firmware Updates") diff --git a/Meshtastic/Views/Settings/FirmwareApi.swift b/Meshtastic/Views/Settings/FirmwareApi.swift index ab480849..70fc1501 100644 --- a/Meshtastic/Views/Settings/FirmwareApi.swift +++ b/Meshtastic/Views/Settings/FirmwareApi.swift @@ -25,10 +25,10 @@ enum Architecture: String, Codable { struct FirmwareReleases: Codable { let releases: Releases - let pullRequests: [PullRequest] + let pullRequests: [FirmwareRelease] } -struct PullRequest: Codable { +struct FirmwareRelease: Codable { let id, title: String let pageURL: String let zipURL: String @@ -42,7 +42,7 @@ struct PullRequest: Codable { // MARK: - Releases struct Releases: Codable { - let stable, alpha: [PullRequest] + let stable, alpha: [FirmwareRelease] } class Api : ObservableObject{ @@ -68,7 +68,6 @@ class Api : ObservableObject{ } URLSession.shared.dataTask(with: url) { data, response, error in let firmwareReleases = try! JSONDecoder().decode(FirmwareReleases.self, from: data!) - print(firmwareReleases) DispatchQueue.main.async { completion(firmwareReleases) } From bf1b374dc59594704c0a5787a2ce300fedfa26f9 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 14 Jan 2024 23:57:35 -0800 Subject: [PATCH 06/11] Clean up JSON classes --- Meshtastic/Views/Settings/FirmwareApi.swift | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Meshtastic/Views/Settings/FirmwareApi.swift b/Meshtastic/Views/Settings/FirmwareApi.swift index 70fc1501..f020e514 100644 --- a/Meshtastic/Views/Settings/FirmwareApi.swift +++ b/Meshtastic/Views/Settings/FirmwareApi.swift @@ -7,6 +7,7 @@ import Foundation +/// Device Hardware API struct DeviceHardware: Codable { let hwModel: Int let hwModelSlug, platformioTarget: String @@ -14,7 +15,6 @@ struct DeviceHardware: Codable { let activelySupported: Bool let displayName: String } - enum Architecture: String, Codable { case esp32 = "esp32" case esp32C3 = "esp32-c3" @@ -23,11 +23,14 @@ enum Architecture: String, Codable { case rp2040 = "rp2040" } +/// Firmware Release Lists struct FirmwareReleases: Codable { let releases: Releases let pullRequests: [FirmwareRelease] } - +struct Releases: Codable { + let stable, alpha: [FirmwareRelease] +} struct FirmwareRelease: Codable { let id, title: String let pageURL: String @@ -40,11 +43,6 @@ struct FirmwareRelease: Codable { } } -// MARK: - Releases -struct Releases: Codable { - let stable, alpha: [FirmwareRelease] -} - class Api : ObservableObject{ func loadDeviceHardwareData(completion:@escaping ([DeviceHardware]) -> ()) { @@ -54,7 +52,6 @@ class Api : ObservableObject{ } 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) } From 97e69af44ae1c01e005b341541d4b727ebe865b8 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 15 Jan 2024 18:13:31 -0800 Subject: [PATCH 07/11] go back to .cardinal interpolationmode --- Meshtastic/Helpers/BLEManager.swift | 2 ++ Meshtastic/Views/Nodes/DeviceMetricsLog.swift | 4 +--- Meshtastic/Views/Settings/Firmware.swift | 8 +++++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index d65053af..15604737 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -1116,10 +1116,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Date: Tue, 16 Jan 2024 08:55:23 -0800 Subject: [PATCH 08/11] Method to create empty node info --- .../CoreData/NodeInfoEntityExtension.swift | 19 +++++++++++++++++++ Meshtastic/Helpers/BLEManager.swift | 5 ++++- Meshtastic/Helpers/MeshPackets.swift | 14 +------------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index 09673104..00175801 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -6,6 +6,7 @@ // import Foundation +import CoreData extension NodeInfoEntity { @@ -37,4 +38,22 @@ extension NodeInfoEntity { } return false } + +} + +public func createNodeInfo(num: Int64, context: NSManagedObjectContext) -> NodeInfoEntity { + + let newNode = NodeInfoEntity(context: context) + newNode.id = Int64(num) + newNode.num = Int64(num) + let newUser = UserEntity(context: context) + newUser.num = Int64(num) + let userId = String(format:"%2X", num) + newUser.userId = "!\(userId)" + let last4 = String(userId.suffix(4)) + newUser.longName = "Meshtastic \(last4)" + newUser.shortName = last4 + newUser.hwModel = "UNSET" + newNode.user = newUser + return newNode } diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 15604737..26014859 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -649,7 +649,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var routeString = "You --> " var hopNodes: [TraceRouteHopEntity] = [] for node in routingMessage.route { - let hopNode = getNodeInfo(id: Int64(node), context: context!) + var hopNode = getNodeInfo(id: Int64(node), context: context!) + if hopNode == nil { + hopNode = createNodeInfo(num: Int64(node), context: context!) + } let traceRouteHop = TraceRouteHopEntity(context: context!) traceRouteHop.time = Date() if hopNode?.hasPositions ?? false { diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 7c8ceb7f..3fab145b 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -208,19 +208,7 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, context: NS if fetchedNode.count > 0 { fetchedNode[0].metadata = newMetadata } else { - - let newNode = NodeInfoEntity(context: context) - newNode.id = Int64(fromNum) - newNode.num = Int64(fromNum) - let newUser = UserEntity(context: context) - newUser.num = Int64(fromNum) - let userId = String(format:"%2X", fromNum) - newUser.userId = "!\(userId)" - let last4 = String(userId.suffix(4)) - newUser.longName = "Meshtastic \(last4)" - newUser.shortName = last4 - newUser.hwModel = "UNSET" - newNode.user = newUser + let newNode = createNodeInfo(num: Int64(fromNum), context: context) newNode.metadata = newMetadata } do { From 056dabfadbb6c37f134ea00a96eb201e780f586b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 16 Jan 2024 15:50:59 -0800 Subject: [PATCH 09/11] Export route to CSV --- Meshtastic/Export/WriteCsvFile.swift | 24 ++++++++++++++++++ Meshtastic/Views/Settings/Routes.swift | 32 +++++++++++++++++++++++- Meshtastic/Views/Settings/Settings.swift | 6 ++++- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Export/WriteCsvFile.swift b/Meshtastic/Export/WriteCsvFile.swift index c246fa40..bdd516a9 100644 --- a/Meshtastic/Export/WriteCsvFile.swift +++ b/Meshtastic/Export/WriteCsvFile.swift @@ -96,3 +96,27 @@ func positionToCsvFile(positions: [PositionEntity]) -> String { } return csvString } + + +func routeToCsvFile(locations: [LocationEntity]) -> String { + var csvString: String = "" + let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) + let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "") + // Create Position Header + csvString = "Id, Latitude, Longitude, Altitude, Speed, Heading" + for loc in locations { + csvString += "\n" + csvString += String(loc.id) + csvString += ", " + csvString += String((loc.latitude ?? 0)) + csvString += ", " + csvString += String(loc.longitude ?? 0) + csvString += ", " + csvString += String(loc.altitude) + csvString += ", " + csvString += String(loc.speed) + csvString += ", " + csvString += String(loc.heading) + } + return csvString +} diff --git a/Meshtastic/Views/Settings/Routes.swift b/Meshtastic/Views/Settings/Routes.swift index 44a65690..6c1de5a0 100644 --- a/Meshtastic/Views/Settings/Routes.swift +++ b/Meshtastic/Views/Settings/Routes.swift @@ -18,6 +18,8 @@ struct Routes: View { @State private var selectedRoute: RouteEntity? @State private var importing = false @State private var isShowingBadFileAlert = false + @State var isExporting = false + @State var exportString = "" @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "enabled", ascending: false), NSSortDescriptor(key: "name", ascending: true), NSSortDescriptor(key: "date", ascending: false)], animation: .default) @@ -33,7 +35,7 @@ struct Routes: View { .padding() .alert(isPresented: $isShowingBadFileAlert) { - Alert(title: Text("Not a valid route file"), message: Text("Your route file must have both Latitude and Longitude."), dismissButton: .default(Text("OK"))) + Alert(title: Text("Not a valid route file"), message: Text("Your route file must have both Latitude and Longitude columns and headers."), dismissButton: .default(Text("OK"))) } .fileImporter( isPresented: $importing, @@ -187,8 +189,36 @@ struct Routes: View { .stroke(Color(UIColor(hex: UInt32(selectedRoute?.color ?? 0))), style: dashed) } .frame(maxWidth: .infinity, maxHeight: .infinity) + .safeAreaInset(edge: .bottom, alignment: UIDevice.current.userInterfaceIdiom == .phone ? .leading : .trailing) { + Button { + exportString = routeToCsvFile(locations: selectedRoute!.locations!.array as? [LocationEntity] ?? []) + isExporting = true + } label: { + Label("save", systemImage: "square.and.arrow.down") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + .padding(.leading) + } + } }.navigationTitle(" \(selectedRoute?.name ?? "Unknown Route") \(selectedRoute?.locations?.count ?? 0) points") } + .fileExporter( + isPresented: $isExporting, + document: CsvDocument(emptyCsv: exportString), + contentType: .commaSeparatedText, + defaultFilename: String("\(selectedRoute?.name ?? "Route") Log"), + onCompletion: { result in + if case .success = result { + print("Route log download succeeded.") + self.isExporting = false + } else { + print("Route log download failed: \(result).") + } + } + ) } } diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 886acf8c..87d23bd3 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -313,7 +313,11 @@ struct Settings: View { } .onAppear { self.preferredNodeNum = UserDefaults.preferredPeripheralNum - if selectedNode == 0 { + if nodes.count > 1 { + if selectedNode == 0 { + self.selectedNode = Int(bleManager.connectedPeripheral != nil ? UserDefaults.preferredPeripheralNum : 0) + } + } else { self.selectedNode = Int(bleManager.connectedPeripheral != nil ? UserDefaults.preferredPeripheralNum : 0) } } From 5c9d16a01881515ad2cf8c3add85cf5a8c0ad1e0 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 16 Jan 2024 16:58:28 -0800 Subject: [PATCH 10/11] Make route recorder map fill the top of the screen --- Meshtastic/Views/Settings/RouteRecorder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/RouteRecorder.swift b/Meshtastic/Views/Settings/RouteRecorder.swift index 086cb6e5..601f2a49 100644 --- a/Meshtastic/Views/Settings/RouteRecorder.swift +++ b/Meshtastic/Views/Settings/RouteRecorder.swift @@ -49,7 +49,6 @@ struct RouteRecorder: View { } .mapStyle(mapStyle) } - .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) .mapScope(routerecorderscope) .safeAreaInset(edge: .bottom) { ZStack { @@ -285,5 +284,6 @@ struct RouteRecorder: View { } } } + .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) } } From 6bc2aeefbdf43869d487bdc6d0d289c7c882f4d8 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 16 Jan 2024 19:08:33 -0800 Subject: [PATCH 11/11] Hide route recorder --- Meshtastic/Views/Settings/RouteRecorder.swift | 572 +++++++++--------- Meshtastic/Views/Settings/Settings.swift | 16 +- 2 files changed, 294 insertions(+), 294 deletions(-) diff --git a/Meshtastic/Views/Settings/RouteRecorder.swift b/Meshtastic/Views/Settings/RouteRecorder.swift index 601f2a49..342d7aca 100644 --- a/Meshtastic/Views/Settings/RouteRecorder.swift +++ b/Meshtastic/Views/Settings/RouteRecorder.swift @@ -1,289 +1,289 @@ +//// +//// Routes.swift +//// Meshtastic +//// +//// Created by Garth Vander Houwen on 11/21/23. +//// // -// Routes.swift -// Meshtastic +//import SwiftUI +//import CoreData +//import MapKit +//import CoreLocation +//import CoreMotion // -// Created by Garth Vander Houwen on 11/21/23. +//@available(iOS 17.0, macOS 14.0, *) +//struct RouteRecorder: View { +// +// @ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared +// @Environment(\.managedObjectContext) var context +// @State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic) +// //@State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true) +// @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic) +// @State var isShowingDetails = false +// @Namespace var namespace +// @Namespace var routerecorderscope +// @State var recording: RouteEntity? +// @State var color: Color = .blue +// +// var body: some View { +// VStack { +// ZStack { +// Map(position: $position, scope: routerecorderscope) { +// UserAnnotation() +// /// Route Lines +// let lineCoords = locationsHandler.locationsArray.compactMap({(position) -> CLLocationCoordinate2D in +// return position.coordinate +// }) +// +// let gradient = LinearGradient( +// colors: [color], +// startPoint: .leading, endPoint: .trailing +// ) +// let dashed = StrokeStyle( +// lineWidth: 3, +// lineCap: .round, lineJoin: .round, dash: [10, 10] +// ) +// MapPolyline(coordinates: lineCoords) +// .stroke(gradient, style: dashed) // - -import SwiftUI -import CoreData -import MapKit -import CoreLocation -import CoreMotion - -@available(iOS 17.0, macOS 14.0, *) -struct RouteRecorder: View { - - @ObservedObject var locationsHandler: LocationsHandler = LocationsHandler.shared - @Environment(\.managedObjectContext) var context - @State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic) - //@State var mapStyle: MapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: .all, showsTraffic: true) - @State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic) - @State var isShowingDetails = false - @Namespace var namespace - @Namespace var routerecorderscope - @State var recording: RouteEntity? - @State var color: Color = .blue - - var body: some View { - VStack { - ZStack { - Map(position: $position, scope: routerecorderscope) { - UserAnnotation() - /// Route Lines - let lineCoords = locationsHandler.locationsArray.compactMap({(position) -> CLLocationCoordinate2D in - return position.coordinate - }) - - let gradient = LinearGradient( - colors: [color], - startPoint: .leading, endPoint: .trailing - ) - let dashed = StrokeStyle( - lineWidth: 3, - lineCap: .round, lineJoin: .round, dash: [10, 10] - ) - MapPolyline(coordinates: lineCoords) - .stroke(gradient, style: dashed) - - } - .mapStyle(mapStyle) - } - .mapScope(routerecorderscope) - .safeAreaInset(edge: .bottom) { - ZStack { - VStack { - HStack(spacing: 10) { - Spacer() - - Button { - isShowingDetails = true - } label: { - Image(systemName: locationsHandler.isRecording ? "record.circle.fill" : "record.circle") - .font(.system(size: 72)) - .symbolRenderingMode(.multicolor) - .foregroundColor(.red) - } - .buttonStyle(.bordered) - .foregroundColor(.red) - .buttonBorderShape(.circle) - .matchedGeometryEffect(id: "Details Button", in: namespace) - - Spacer() - } - } - } - .padding() - } - .sheet(isPresented: $isShowingDetails) { - NavigationStack { - VStack { - if locationsHandler.isRecording { - HStack (alignment: .center) { - Image(systemName: "record.circle.fill") - .symbolRenderingMode(.multicolor) - .font(.title) - .foregroundColor(.red) - Text("Recording route") - .font(.title) - Spacer() - Text("\(locationsHandler.count)") - .foregroundColor(.red) - .font(.title2) - } - .padding() - } else if locationsHandler.isRecordingPaused { - HStack (alignment: .center) { - - Image(systemName: "playpause") - .symbolRenderingMode(.multicolor) - .font(.title3) - .foregroundColor(.red) - Text("Route recording paused") - .font(.title) - } - .padding(.top) - } - - if locationsHandler.isRecording || locationsHandler.isRecordingPaused { - Divider() - HStack { - VStack { - Text(locationsHandler.recordingStarted ?? Date(), style: .timer) - .font(.title) - .fixedSize() - Text("Time") - .font(.callout) - .fixedSize() - } - .padding(.horizontal) - Divider() - VStack { - let distance = Measurement(value: locationsHandler.distanceTraveled, unit: UnitLength.meters) - Text("\(distance.formatted())") - .font(.title) - .fixedSize() - Text("Distance") - .font(.callout) - .fixedSize() - } - .padding(.horizontal) - Divider() - VStack { - let gain = Measurement(value: locationsHandler.elevationGain, unit: UnitLength.meters) - Text(gain.formatted()) - .font(.title) - Text("Elev. Gain") - .font(.callout) - } - .padding(.horizontal) - } - .frame(maxHeight: 90) - } - Divider() - VStack(alignment: .leading) { - List { - GPSStatus(largeFont: .body, smallFont: .callout) - } - .listStyle(.plain) - HStack { - Spacer() - if !locationsHandler.isRecording && !locationsHandler.isRecordingPaused { - /// We are not recording or paused, show start recording button - Button { - locationsHandler.isRecording = true - locationsHandler.count = 0 - locationsHandler.distanceTraveled = 0.0 - locationsHandler.elevationGain = 0.0 - locationsHandler.locationsArray.removeAll() - locationsHandler.recordingStarted = Date() - let newRoute = RouteEntity(context: context) - newRoute.name = String("Route Recording") - newRoute.id = Int32.random(in: Int32(Int8.max) ... Int32.max) - newRoute.color = Int64(UIColor.random.hex) - newRoute.date = Date() - newRoute.enabled = false - color = Color(UIColor(hex: UInt32(newRoute.color))) - self.recording = newRoute - do { - try context.save() - print("💾 Saved a new route") - } catch { - context.rollback() - let nsError = error as NSError - print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") - } - } label: { - Label("start", systemImage: "play") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) - - } else if locationsHandler.isRecording { - /// We are recording show pause button - Button { - locationsHandler.isRecording = false - locationsHandler.isRecordingPaused = true - } label: { - Label("pause", systemImage: "pause") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) - } else if locationsHandler.isRecordingPaused { - /// We are paused show resume button - Button { - locationsHandler.isRecording = true - locationsHandler.isRecordingPaused = false - } label: { - Label("resume", systemImage: "playpause") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) - } - - if locationsHandler.isRecording || locationsHandler.isRecordingPaused { - /// We are recording or paused, show finish button - Button { - locationsHandler.isRecording = false - locationsHandler.isRecordingPaused = false - locationsHandler.distanceTraveled = 0.0 - locationsHandler.elevationGain = 0.0 - locationsHandler.locationsArray.removeAll() - locationsHandler.recordingStarted = nil - if let rec = recording { - rec.enabled = true - context.refresh(rec, mergeChanges:true) - } - - do { - try context.save() - print("💾 Saved a route finish") - } catch { - context.rollback() - let nsError = error as NSError - print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") - } - } label: { - Label("finish", systemImage: "flag.checkered") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) - } -#if targetEnvironment(macCatalyst) - Button(role: .cancel) { - isShowingDetails = false - } label: { - Label("close", systemImage: "xmark") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) -#endif - Spacer() - } - - } - } - } - .presentationDetents([.fraction(0.30), .fraction(0.65)]) - .presentationDragIndicator(.hidden) - .interactiveDismissDisabled(false) - .onChange(of: locationsHandler.locationsArray.last) { newLoc in - if locationsHandler.isRecording { - if let loc = newLoc { - if recording != nil { - let locationEntity = LocationEntity(context: context) - locationEntity.routeLocation = recording - locationEntity.id = Int32(locationsHandler.count) - locationEntity.altitude = Int32(loc.altitude) - locationEntity.heading = Int32(loc.course) - locationEntity.speed = Int32(loc.speed) - locationEntity.latitudeI = Int32(loc.coordinate.latitude * 1e7) - locationEntity.longitudeI = Int32(loc.coordinate.longitude * 1e7) - do { - try context.save() - print("💾 Saved a new route location") - //print("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num)") - } catch { - context.rollback() - let nsError = error as NSError - print("💥 Error Saving LocationEntity from the Route Recorder \(nsError)") - } - } - } - } - } - } - } - .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) - } -} +// } +// .mapStyle(mapStyle) +// } +// .mapScope(routerecorderscope) +// .safeAreaInset(edge: .bottom) { +// ZStack { +// VStack { +// HStack(spacing: 10) { +// Spacer() +// +// Button { +// isShowingDetails = true +// } label: { +// Image(systemName: locationsHandler.isRecording ? "record.circle.fill" : "record.circle") +// .font(.system(size: 72)) +// .symbolRenderingMode(.multicolor) +// .foregroundColor(.red) +// } +// .buttonStyle(.bordered) +// .foregroundColor(.red) +// .buttonBorderShape(.circle) +// .matchedGeometryEffect(id: "Details Button", in: namespace) +// +// Spacer() +// } +// } +// } +// .padding() +// } +// .sheet(isPresented: $isShowingDetails) { +// NavigationStack { +// VStack { +// if locationsHandler.isRecording { +// HStack (alignment: .center) { +// Image(systemName: "record.circle.fill") +// .symbolRenderingMode(.multicolor) +// .font(.title) +// .foregroundColor(.red) +// Text("Recording route") +// .font(.title) +// Spacer() +// Text("\(locationsHandler.count)") +// .foregroundColor(.red) +// .font(.title2) +// } +// .padding() +// } else if locationsHandler.isRecordingPaused { +// HStack (alignment: .center) { +// +// Image(systemName: "playpause") +// .symbolRenderingMode(.multicolor) +// .font(.title3) +// .foregroundColor(.red) +// Text("Route recording paused") +// .font(.title) +// } +// .padding(.top) +// } +// +// if locationsHandler.isRecording || locationsHandler.isRecordingPaused { +// Divider() +// HStack { +// VStack { +// Text(locationsHandler.recordingStarted ?? Date(), style: .timer) +// .font(.title) +// .fixedSize() +// Text("Time") +// .font(.callout) +// .fixedSize() +// } +// .padding(.horizontal) +// Divider() +// VStack { +// let distance = Measurement(value: locationsHandler.distanceTraveled, unit: UnitLength.meters) +// Text("\(distance.formatted())") +// .font(.title) +// .fixedSize() +// Text("Distance") +// .font(.callout) +// .fixedSize() +// } +// .padding(.horizontal) +// Divider() +// VStack { +// let gain = Measurement(value: locationsHandler.elevationGain, unit: UnitLength.meters) +// Text(gain.formatted()) +// .font(.title) +// Text("Elev. Gain") +// .font(.callout) +// } +// .padding(.horizontal) +// } +// .frame(maxHeight: 90) +// } +// Divider() +// VStack(alignment: .leading) { +// List { +// GPSStatus(largeFont: .body, smallFont: .callout) +// } +// .listStyle(.plain) +// HStack { +// Spacer() +// if !locationsHandler.isRecording && !locationsHandler.isRecordingPaused { +// /// We are not recording or paused, show start recording button +// Button { +// locationsHandler.isRecording = true +// locationsHandler.count = 0 +// locationsHandler.distanceTraveled = 0.0 +// locationsHandler.elevationGain = 0.0 +// locationsHandler.locationsArray.removeAll() +// locationsHandler.recordingStarted = Date() +// let newRoute = RouteEntity(context: context) +// newRoute.name = String("Route Recording") +// newRoute.id = Int32.random(in: Int32(Int8.max) ... Int32.max) +// newRoute.color = Int64(UIColor.random.hex) +// newRoute.date = Date() +// newRoute.enabled = false +// color = Color(UIColor(hex: UInt32(newRoute.color))) +// self.recording = newRoute +// do { +// try context.save() +// print("💾 Saved a new route") +// } catch { +// context.rollback() +// let nsError = error as NSError +// print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") +// } +// } label: { +// Label("start", systemImage: "play") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding(.bottom) +// +// } else if locationsHandler.isRecording { +// /// We are recording show pause button +// Button { +// locationsHandler.isRecording = false +// locationsHandler.isRecordingPaused = true +// } label: { +// Label("pause", systemImage: "pause") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding(.bottom) +// } else if locationsHandler.isRecordingPaused { +// /// We are paused show resume button +// Button { +// locationsHandler.isRecording = true +// locationsHandler.isRecordingPaused = false +// } label: { +// Label("resume", systemImage: "playpause") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding(.bottom) +// } +// +// if locationsHandler.isRecording || locationsHandler.isRecordingPaused { +// /// We are recording or paused, show finish button +// Button { +// locationsHandler.isRecording = false +// locationsHandler.isRecordingPaused = false +// locationsHandler.distanceTraveled = 0.0 +// locationsHandler.elevationGain = 0.0 +// locationsHandler.locationsArray.removeAll() +// locationsHandler.recordingStarted = nil +// if let rec = recording { +// rec.enabled = true +// context.refresh(rec, mergeChanges:true) +// } +// +// do { +// try context.save() +// print("💾 Saved a route finish") +// } catch { +// context.rollback() +// let nsError = error as NSError +// print("💥 Error Saving RouteEntity from the Route Recorder \(nsError)") +// } +// } label: { +// Label("finish", systemImage: "flag.checkered") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding(.bottom) +// } +//#if targetEnvironment(macCatalyst) +// Button(role: .cancel) { +// isShowingDetails = false +// } label: { +// Label("close", systemImage: "xmark") +// } +// .buttonStyle(.bordered) +// .buttonBorderShape(.capsule) +// .controlSize(.large) +// .padding(.bottom) +//#endif +// Spacer() +// } +// +// } +// } +// } +// .presentationDetents([.fraction(0.30), .fraction(0.65)]) +// .presentationDragIndicator(.hidden) +// .interactiveDismissDisabled(false) +// .onChange(of: locationsHandler.locationsArray.last) { newLoc in +// if locationsHandler.isRecording { +// if let loc = newLoc { +// if recording != nil { +// let locationEntity = LocationEntity(context: context) +// locationEntity.routeLocation = recording +// locationEntity.id = Int32(locationsHandler.count) +// locationEntity.altitude = Int32(loc.altitude) +// locationEntity.heading = Int32(loc.course) +// locationEntity.speed = Int32(loc.speed) +// locationEntity.latitudeI = Int32(loc.coordinate.latitude * 1e7) +// locationEntity.longitudeI = Int32(loc.coordinate.longitude * 1e7) +// do { +// try context.save() +// print("💾 Saved a new route location") +// //print("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num)") +// } catch { +// context.rollback() +// let nsError = error as NSError +// print("💥 Error Saving LocationEntity from the Route Recorder \(nsError)") +// } +// } +// } +// } +// } +// } +// } +// .ignoresSafeArea(.all, edges: [.top, .leading, .trailing]) +// } +//} diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 87d23bd3..66d1e02e 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -69,14 +69,14 @@ struct Settings: View { Text("routes") } .tag(SettingsSidebar.routes) - NavigationLink { - RouteRecorder() - } label: { - Image(systemName: "record.circle") - .symbolRenderingMode(.hierarchical) - Text("route.recorder") - } - .tag(SettingsSidebar.routeRecorder) +// NavigationLink { +// RouteRecorder() +// } label: { +// Image(systemName: "record.circle") +// .symbolRenderingMode(.hierarchical) +// Text("route.recorder") +// } +// .tag(SettingsSidebar.routeRecorder) } let node = nodes.first(where: { $0.num == preferredNodeNum })