mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
* Use node location for weather not user
* Firmware update view * Update battery gauge to handle new numbers from device
This commit is contained in:
parent
aef8e612e3
commit
7d478f71fd
8 changed files with 402 additions and 94 deletions
|
|
@ -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 = "<group>"; };
|
||||
DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConfig.swift; sourceTree = "<group>"; };
|
||||
DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeshtasticTests.swift; sourceTree = "<group>"; };
|
||||
DDD6EEAE29BC024700383354 /* Firmware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Firmware.swift; sourceTree = "<group>"; };
|
||||
DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeText.swift; sourceTree = "<group>"; };
|
||||
DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntityExtension.swift; sourceTree = "<group>"; };
|
||||
DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV9.xcdatamodel; sourceTree = "<group>"; };
|
||||
|
|
@ -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 */,
|
||||
|
|
|
|||
|
|
@ -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)..<UInt32.max)
|
||||
meshPacket.priority = MeshPacket.Priority.reliable
|
||||
meshPacket.wantAck = true
|
||||
meshPacket.channel = UInt32(adminIndex)
|
||||
var dataMessage = DataMessage()
|
||||
dataMessage.payload = try! adminPacket.serializedData()
|
||||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
let messageDescription = "🚀 Sent Reboot OTA Admin Message to: \(toUser.longName ?? NSLocalizedString("unknown", comment: "")) from: \(fromUser.longName ?? NSLocalizedString("unknown", comment: ""))"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func sendFactoryReset(fromUser: UserEntity, toUser: UserEntity) -> Bool {
|
||||
var adminPacket = AdminMessage()
|
||||
|
|
|
|||
|
|
@ -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<NSFetchRequestResult> = 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) {
|
||||
|
|
|
|||
|
|
@ -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<NSFetchRequestResult> = 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))
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
282
Meshtastic/Views/Settings/Firmware.swift
Normal file
282
Meshtastic/Views/Settings/Firmware.swift
Normal file
|
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue