diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 0e9838e2..0a73ceda 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -110,6 +110,7 @@ DDCDC6CB29481FCC004C1DDA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DDCDC6CD29481FCC004C1DDA /* Localizable.strings */; }; DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */; }; DDD3BBD5292D763200D609B3 /* MeshtasticTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */; }; + DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD6EEAE29BC024700383354 /* Firmware.swift */; }; DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */; }; DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */; }; DDDE59F529AF163D00490C6C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD41A61C29AE7E8E003C5A37 /* WidgetKit.framework */; }; @@ -284,6 +285,7 @@ DDCDC6CE294821AD004C1DDA /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConfig.swift; sourceTree = ""; }; DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeshtasticTests.swift; sourceTree = ""; }; + DDD6EEAE29BC024700383354 /* Firmware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Firmware.swift; sourceTree = ""; }; DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeText.swift; sourceTree = ""; }; DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntityExtension.swift; sourceTree = ""; }; DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV9.xcdatamodel; sourceTree = ""; }; @@ -385,6 +387,7 @@ DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */, DD4A911D2708C65400501B7E /* AppSettings.swift */, DDA0B6B1294CDC55001356EC /* Channels.swift */, + DDD6EEAE29BC024700383354 /* Firmware.swift */, DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */, DD86D40B287F401000BAEB7A /* SaveChannelQRCode.swift */, DD3501882852FC3B000FC853 /* Settings.swift */, @@ -891,6 +894,7 @@ DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */, DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */, DDC4D568275499A500A4208E /* Persistence.swift in Sources */, + DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */, DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */, DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index aa1b5dad..7180a887 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -503,7 +503,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { case .waypointApp: waypointPacket(packet: decodedInfo.packet, context: context!) case .nodeinfoApp: - if !invalidVersion { nodeInfoAppPacket(packet: decodedInfo.packet, context: context!) } + if !invalidVersion { upsertNodeInfoPacket(packet: decodedInfo.packet, context: context!) } case .routingApp: if !invalidVersion { routingPacket(packet: decodedInfo.packet, connectedNodeNum: self.connectedPeripheral.num, context: context!) } case .adminApp: @@ -873,6 +873,27 @@ class BLEManager: NSObject, CBPeripheralDelegate, ObservableObject { } return false } + + public func sendRebootOta(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { + var adminPacket = AdminMessage() + adminPacket.rebootOtaSeconds = 5 + 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/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 26124884..60a2ac4b 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -396,92 +396,6 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje return nil } -func nodeInfoAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { - - let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.nodeinfo.received %@", comment: "Node info received for: %@"), String(packet.from)) - MeshLogger.log("📟 \(logString)") - - guard packet.from > 0 else { return } - - let fetchNodeInfoAppRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") - fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) - - do { - - let fetchedNode = try context.fetch(fetchNodeInfoAppRequest) as? [NodeInfoEntity] ?? [] - - // Not Found Insert - if fetchedNode.count == 0 { - - let newNode = NodeInfoEntity(context: context) - newNode.id = Int64(packet.from) - newNode.num = Int64(packet.from) - newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) - newNode.snr = packet.rxSnr - newNode.channel = Int32(packet.channel) - if let newUserMessage = try? User(serializedData: packet.decoded.payload) { - - let newUser = UserEntity(context: context) - newUser.userId = newUserMessage.id - newUser.num = Int64(packet.from) - newUser.longName = newUserMessage.longName - newUser.shortName = newUserMessage.shortName - newUser.macaddr = newUserMessage.macaddr - newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased() - newNode.user = newUser - } - - if let telemetryMessage = try? Telemetry(serializedData: packet.decoded.payload) { - let telemetry = TelemetryEntity(context: context) - telemetry.batteryLevel = Int32(telemetryMessage.deviceMetrics.batteryLevel) - telemetry.voltage = telemetryMessage.deviceMetrics.voltage - telemetry.channelUtilization = telemetryMessage.deviceMetrics.channelUtilization - telemetry.airUtilTx = telemetryMessage.deviceMetrics.airUtilTx - var newTelemetries = [TelemetryEntity]() - newTelemetries.append(telemetry) - newNode.telemetries? = NSOrderedSet(array: newTelemetries) - } - } else { - fetchedNode[0].id = Int64(packet.from) - fetchedNode[0].num = Int64(packet.from) - fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) - fetchedNode[0].snr = packet.rxSnr - fetchedNode[0].channel = Int32(packet.channel) - - if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { - if nodeInfoMessage.hasDeviceMetrics { - let telemetry = TelemetryEntity(context: context) - telemetry.batteryLevel = Int32(nodeInfoMessage.deviceMetrics.batteryLevel) - telemetry.voltage = nodeInfoMessage.deviceMetrics.voltage - telemetry.channelUtilization = nodeInfoMessage.deviceMetrics.channelUtilization - telemetry.airUtilTx = nodeInfoMessage.deviceMetrics.airUtilTx - var newTelemetries = [TelemetryEntity]() - newTelemetries.append(telemetry) - fetchedNode[0].telemetries? = NSOrderedSet(array: newTelemetries) - } - if nodeInfoMessage.hasUser { - fetchedNode[0].user!.userId = nodeInfoMessage.user.id - fetchedNode[0].user!.num = Int64(nodeInfoMessage.num) - fetchedNode[0].user!.longName = nodeInfoMessage.user.longName - fetchedNode[0].user!.shortName = nodeInfoMessage.user.shortName - fetchedNode[0].user!.macaddr = nodeInfoMessage.user.macaddr - fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() - } - } - do { - try context.save() - print("💾 Updated NodeInfo from Node Info App Packet For: \(fetchedNode[0].num)") - } catch { - context.rollback() - let nsError = error as NSError - print("💥 Error Saving NodeInfoEntity from NODEINFO_APP \(nsError)") - } - } - } catch { - print("💥 Error Fetching NodeInfoEntity for NODEINFO_APP") - } -} - func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { if let adminMessage = try? AdminMessage(serializedData: packet.decoded.payload) { diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 6ba49d8e..4207560a 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -97,6 +97,79 @@ public func clearCoreDataDatabase(context: NSManagedObjectContext) { } } +func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.nodeinfo.received %@", comment: "Node info received for: %@"), String(packet.from)) + MeshLogger.log("📟 \(logString)") + + guard packet.from > 0 else { return } + + let fetchNodeInfoAppRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from)) + + do { + + let fetchedNode = try context.fetch(fetchNodeInfoAppRequest) as? [NodeInfoEntity] ?? [] + if fetchedNode.count == 0 { + // Not Found Insert + let newNode = NodeInfoEntity(context: context) + newNode.id = Int64(packet.from) + newNode.num = Int64(packet.from) + newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) + newNode.snr = packet.rxSnr + newNode.channel = Int32(packet.channel) + if let newUserMessage = try? User(serializedData: packet.decoded.payload) { + let newUser = UserEntity(context: context) + newUser.userId = newUserMessage.id + newUser.num = Int64(packet.from) + newUser.longName = newUserMessage.longName + newUser.shortName = newUserMessage.shortName + newUser.macaddr = newUserMessage.macaddr + newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased() + newNode.user = newUser + } + } else { + // Update an existing node + fetchedNode[0].id = Int64(packet.from) + fetchedNode[0].num = Int64(packet.from) + fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) + fetchedNode[0].snr = packet.rxSnr + fetchedNode[0].channel = Int32(packet.channel) + + if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { + if nodeInfoMessage.hasDeviceMetrics { + let telemetry = TelemetryEntity(context: context) + telemetry.batteryLevel = Int32(nodeInfoMessage.deviceMetrics.batteryLevel) + telemetry.voltage = nodeInfoMessage.deviceMetrics.voltage + telemetry.channelUtilization = nodeInfoMessage.deviceMetrics.channelUtilization + telemetry.airUtilTx = nodeInfoMessage.deviceMetrics.airUtilTx + var newTelemetries = [TelemetryEntity]() + newTelemetries.append(telemetry) + fetchedNode[0].telemetries? = NSOrderedSet(array: newTelemetries) + } + if nodeInfoMessage.hasUser { + fetchedNode[0].user!.userId = nodeInfoMessage.user.id + fetchedNode[0].user!.num = Int64(nodeInfoMessage.num) + fetchedNode[0].user!.longName = nodeInfoMessage.user.longName + fetchedNode[0].user!.shortName = nodeInfoMessage.user.shortName + fetchedNode[0].user!.macaddr = nodeInfoMessage.user.macaddr + fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased() + } + } + do { + try context.save() + print("💾 Updated NodeInfo from Node Info App Packet For: \(fetchedNode[0].num)") + } catch { + context.rollback() + let nsError = error as NSError + print("💥 Error Saving NodeInfoEntity from NODEINFO_APP \(nsError)") + } + } + } catch { + print("💥 Error Fetching NodeInfoEntity for NODEINFO_APP") + } +} + func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat(NSLocalizedString("mesh.log.position.received %@", comment: "Position Packet received from node: %@"), String(packet.from)) diff --git a/Meshtastic/Views/Helpers/BatteryGauge.swift b/Meshtastic/Views/Helpers/BatteryGauge.swift index 83e04e5f..a52de95d 100644 --- a/Meshtastic/Views/Helpers/BatteryGauge.swift +++ b/Meshtastic/Views/Helpers/BatteryGauge.swift @@ -10,7 +10,7 @@ import Charts struct BatteryGauge: View { @State var batteryLevel = 0.0 - private let minValue = 1.0 + private let minValue = 0.0 private let maxValue = 100.00 var body: some View { diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index d13bd286..c095fd4d 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -104,7 +104,9 @@ struct NodeDetail: View { Text("Today's Weather Forecast") .font(.title) .padding() - NodeWeatherForecastView(location: CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude) ) + let nodeLocation = node.positions?.lastObject as! PositionEntity + + NodeWeatherForecastView(location: CLLocation(latitude: nodeLocation.nodeCoordinate!.latitude, longitude: nodeLocation.nodeCoordinate!.longitude) ) .frame(height: 250) } #else @@ -112,7 +114,9 @@ struct NodeDetail: View { Text("Today's Weather Forecast") .font(.title) .padding() - NodeWeatherForecastView(location: CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude) ) + + let nodeLocation = node.positions?.lastObject as! PositionEntity + NodeWeatherForecastView(location: CLLocation(latitude: nodeLocation.nodeCoordinate!.latitude, longitude: nodeLocation.nodeCoordinate!.longitude) ) .frame(height: 250) .presentationDetents([.medium]) .presentationDragIndicator(.automatic) diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift new file mode 100644 index 00000000..ffbd1bf2 --- /dev/null +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -0,0 +1,282 @@ +// +// Firmware.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 3/10/23. +// + +// +// About.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 10/6/22. +// +import SwiftUI +import StoreKit + +struct Firmware: View { + + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + + var node: NodeInfoEntity? + + @State private var firmwareReleaseData: FirmwareRelease = FirmwareRelease() + + var body: some View { + //NavigationSplitView { + NavigationStack { + VStack (alignment: .leading) { + Text("nRF 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) + 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([.leading, .trailing, .bottom]) + 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")!) + .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.") + .font(.caption) + HStack(alignment: .center){ + Spacer() + Button { + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) + if connectedNode != nil { + if !bleManager.sendRebootOta(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex) { + print("Reboot Failed") + } else { + bleManager.disconnectPeripheral(reconnect: false) + } + } + } label: { + Label("Send Reboot OTA", systemImage: "square.and.arrow.down") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.regular) + .padding(5) + Spacer() + } + } + .padding([.leading, .trailing]) + .padding(.bottom, 5) + VStack (alignment: .leading) { + Text("Firmware Releases") + .font(.title3) + .padding([.leading, .trailing]) + List { + Section(header: Text("Stable")) { + ForEach(firmwareReleaseData.releases?.stable ?? [], id: \.id) { fr in + HStack() { + Link(fr.title ?? "Unknown", destination: URL(string: fr.pageUrl ?? "")!) + .font(.caption) + Spacer() + Link(destination: URL(string: fr.zipUrl ?? "")!) { + VStack { + Image(systemName: "square.and.arrow.down") + .font(.title3) + } + } + } + } + } + Section("Alpha") { + ForEach(firmwareReleaseData.releases?.alpha ?? [], id: \.id) { fr in + HStack() { + Link(fr.title ?? "Unknown", destination: URL(string: fr.pageUrl ?? "")!) + .font(.caption) + Spacer() + Link(destination: URL(string: fr.zipUrl ?? "")!) { + VStack { + Image(systemName: "square.and.arrow.down") + .font(.title3) + } + } + } + } + } + Section("Pull Requests") { + ForEach(firmwareReleaseData.pullRequests ?? [], id: \.id) { fr in + HStack() { + Link(fr.title ?? "Unknown", destination: URL(string: fr.pageUrl ?? "")!) + .font(.caption) + Spacer() + Link(destination: URL(string: fr.zipUrl ?? "")!) { + VStack { + Image(systemName: "square.and.arrow.down") + .font(.title3) + } + } + } + } + } + } + } + .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, response, error 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 = "pullRequests" + } + + 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? = nil + var title : String? = nil + var pageUrl : String? = nil + var zipUrl : String? = nil + + 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? = nil + var title : String? = nil + var pageUrl : String? = nil + var zipUrl : String? = nil + + 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? = nil + var title : String? = nil + var pageUrl : String? = nil + var zipUrl : String? = nil + + 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/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index edcba954..6074fb0c 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -45,6 +45,15 @@ struct Settings: View { var body: some View { NavigationSplitView { List { + NavigationLink { + AboutMeshtastic() + } label: { + Image(systemName: "questionmark.app") + .symbolRenderingMode(.hierarchical) + + Text("about.meshtastic") + } + .tag(SettingsSidebar.about) NavigationLink { AppSettings() } label: { @@ -260,16 +269,17 @@ struct Settings: View { } .tag(SettingsSidebar.adminMessageLog) } - Section(header: Text("about")) { + Section(header: Text("Firmware")) { NavigationLink { - AboutMeshtastic() + Firmware(node: nodes.first(where: { $0.num == connectedNodeNum })) } label: { - Image(systemName: "questionmark.app") + Image(systemName: "arrow.up.arrow.down.square") .symbolRenderingMode(.hierarchical) - Text("about.meshtastic") + Text("Firmware Updates") } .tag(SettingsSidebar.about) + .disabled(selectedNode > 0 && selectedNode != connectedNodeNum) } } .onAppear {