* Use node location for weather not user

* Firmware update view
* Update battery gauge to handle new numbers from device
This commit is contained in:
Garth Vander Houwen 2023-03-10 19:41:26 -08:00
parent aef8e612e3
commit 7d478f71fd
8 changed files with 402 additions and 94 deletions

View file

@ -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 */,

View file

@ -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()

View file

@ -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) {

View file

@ -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))

View file

@ -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 {

View file

@ -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)

View 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() {
}
}

View file

@ -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 {