Meshtastic-Apple/Meshtastic/Views/Bluetooth/Connect.swift

340 lines
12 KiB
Swift
Raw Normal View History

2021-08-20 07:56:05 -07:00
//
2021-11-29 21:35:23 -08:00
// Connect.swift
// Meshtastic Apple
2021-08-20 07:56:05 -07:00
//
// Copyright(c) Garth Vander Houwen 8/18/21.
2021-08-20 07:56:05 -07:00
//
import SwiftUI
import MapKit
import CoreData
2021-08-20 07:56:05 -07:00
import CoreLocation
import CoreBluetooth
2023-03-01 23:41:10 -08:00
#if canImport(ActivityKit)
import ActivityKit
#endif
2021-08-20 07:56:05 -07:00
2021-09-18 17:10:22 -07:00
struct Connect: View {
2023-03-06 10:33:18 -08:00
2021-12-12 17:17:46 -08:00
@Environment(\.managedObjectContext) var context
@EnvironmentObject var bleManager: BLEManager
//@EnvironmentObject var userSettings: UserSettings
2023-03-06 10:33:18 -08:00
@State var node: NodeInfoEntity?
@State var isUnsetRegion = false
2022-09-27 06:34:13 -07:00
@State var invalidFirmwareVersion = false
2023-03-01 23:41:10 -08:00
@State var liveActivityStarted = false
@State var presentingSwitchPreferredPeripheral = false
@State var selectedPeripherialId = ""
2023-03-06 10:33:18 -08:00
init () {
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.getNotificationSettings(completionHandler: { (settings) in
if settings.authorizationStatus == .notDetermined {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
if success {
print("Notifications are all set!")
} else if let error = error {
print(error.localizedDescription)
}
}
}
})
}
2023-03-01 23:41:10 -08:00
var body: some View {
2023-03-06 10:33:18 -08:00
2022-10-17 19:26:52 -07:00
NavigationStack {
VStack {
List {
if bleManager.isSwitchedOn {
2023-03-01 23:41:10 -08:00
Section(header: Text("connected.radio").font(.title)) {
if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == .connected {
HStack {
VStack(alignment: .center) {
CircleText(text: node?.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node?.num ?? 0))), circleSize: 80, fontSize: (node?.user?.shortName ?? "???").isEmoji() ? 52 : 30, textColor: UIColor(hex: UInt32(node?.num ?? 0)).isLight() ? .black : .white )
}
.padding(.trailing)
VStack(alignment: .leading) {
if node != nil {
2023-03-03 22:29:31 -08:00
Text(bleManager.connectedPeripheral.longName).font(.title2)
}
Text("ble.name").font(.callout)+Text(": \(bleManager.connectedPeripheral.peripheral.name ?? "unknown".localized)")
.font(.callout).foregroundColor(Color.gray)
if node != nil {
Text("firmware.version").font(.callout)+Text(": \(node?.metadata?.firmwareVersion ?? "unknown".localized)")
.font(.callout).foregroundColor(Color.gray)
}
if bleManager.isSubscribed {
Text("subscribed").font(.callout)
.foregroundColor(.green)
} else {
Text("communicating").font(.callout)
.foregroundColor(.orange)
}
}
}
.font(.caption)
.foregroundColor(Color.gray)
2023-03-01 23:41:10 -08:00
.padding([.top, .bottom])
.swipeActions {
2023-03-06 10:33:18 -08:00
2023-03-01 23:41:10 -08:00
Button(role: .destructive) {
if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == CBPeripheralState.connected {
bleManager.disconnectPeripheral(reconnect: false)
}
} label: {
Label("disconnect", systemImage: "antenna.radiowaves.left.and.right.slash")
2021-10-17 15:08:12 -07:00
}
}
2023-03-06 10:33:18 -08:00
.contextMenu {
2023-03-01 23:41:10 -08:00
if node != nil {
2023-03-03 20:28:56 -08:00
#if !targetEnvironment(macCatalyst)
2023-03-01 23:41:10 -08:00
if #available(iOS 16.2, *) {
Button {
if !liveActivityStarted {
2023-03-03 20:28:56 -08:00
#if canImport(ActivityKit)
2023-03-01 23:41:10 -08:00
print("Start live activity.")
startNodeActivity()
2023-03-03 20:28:56 -08:00
#endif
2023-03-01 23:41:10 -08:00
} else {
2023-03-03 20:28:56 -08:00
#if canImport(ActivityKit)
2023-03-01 23:41:10 -08:00
print("Stop live activity.")
endActivity()
2023-03-03 20:28:56 -08:00
#endif
2023-03-01 23:41:10 -08:00
}
} label: {
2023-03-26 09:46:51 -07:00
Label("mesh.live.activity", systemImage: liveActivityStarted ? "stop" : "play")
2023-03-01 23:41:10 -08:00
}
}
2023-03-03 20:28:56 -08:00
#endif
2023-03-01 23:41:10 -08:00
Text("Num: \(String(node!.num))")
Text("Short Name: \(node?.user?.shortName ?? "????")")
Text("Long Name: \(node?.user?.longName ?? "unknown".localized)")
2023-03-01 23:41:10 -08:00
Text("BLE RSSI: \(bleManager.connectedPeripheral.rssi)")
}
}
2023-03-01 23:41:10 -08:00
if isUnsetRegion {
HStack {
NavigationLink {
LoRaConfig(node: node)
} label: {
Label("set.region", systemImage: "globe.americas.fill")
.foregroundColor(.red)
.font(.title)
2022-10-12 15:26:25 -07:00
}
}
}
} else {
2023-03-06 10:33:18 -08:00
2023-03-01 23:41:10 -08:00
if bleManager.isConnecting {
HStack {
Image(systemName: "antenna.radiowaves.left.and.right")
2023-03-03 22:29:31 -08:00
.resizable()
2023-03-01 23:41:10 -08:00
.symbolRenderingMode(.hierarchical)
2023-03-03 22:29:31 -08:00
.foregroundColor(.orange)
.frame(width: 60, height: 60)
2023-03-01 23:41:10 -08:00
.padding(.trailing)
if bleManager.timeoutTimerCount == 0 {
Text("connecting")
2023-03-03 22:29:31 -08:00
.font(.title2)
2023-03-01 23:41:10 -08:00
.foregroundColor(.orange)
} else {
2023-03-01 23:41:10 -08:00
VStack {
2023-03-06 10:33:18 -08:00
2023-03-01 23:41:10 -08:00
Text("Connection Attempt \(bleManager.timeoutTimerCount) of 10")
.font(.callout)
.foregroundColor(.orange)
}
}
}
2023-03-01 23:41:10 -08:00
.padding()
2023-03-06 10:33:18 -08:00
2023-03-01 23:41:10 -08:00
} else {
2023-03-06 10:33:18 -08:00
2023-03-01 23:41:10 -08:00
if bleManager.lastConnectionError.count > 0 {
Text(bleManager.lastConnectionError).font(.callout).foregroundColor(.red)
}
HStack {
Image(systemName: "antenna.radiowaves.left.and.right.slash")
2023-03-03 22:29:31 -08:00
.resizable()
2023-03-01 23:41:10 -08:00
.symbolRenderingMode(.hierarchical)
2023-03-03 22:29:31 -08:00
.foregroundColor(.red)
.frame(width: 60, height: 60)
2023-03-01 23:41:10 -08:00
.padding(.trailing)
Text("not.connected").font(.title3)
}
2023-03-01 23:41:10 -08:00
.padding()
}
}
2023-03-01 23:41:10 -08:00
}
.textCase(nil)
2023-03-06 10:33:18 -08:00
2023-03-01 23:41:10 -08:00
if !self.bleManager.isConnected {
Section(header: Text("available.radios").font(.title)) {
ForEach(bleManager.peripherals.filter({ $0.peripheral.state == CBPeripheralState.disconnected }).sorted(by: { $0.name < $1.name })) { peripheral in
2023-03-01 23:41:10 -08:00
HStack {
if UserDefaults.preferredPeripheralId == peripheral.peripheral.identifier.uuidString {
2023-03-14 20:45:09 -07:00
Image(systemName: "star.fill")
.imageScale(.large).foregroundColor(.yellow)
.padding(.trailing)
} else {
Image(systemName: "circle.fill")
.imageScale(.large).foregroundColor(.gray)
.padding(.trailing)
}
2023-03-01 23:41:10 -08:00
Button(action: {
if UserDefaults.preferredPeripheralId.count > 0 && peripheral.peripheral.identifier.uuidString != UserDefaults.preferredPeripheralId {
presentingSwitchPreferredPeripheral = true
selectedPeripherialId = peripheral.peripheral.identifier.uuidString
2023-03-01 23:41:10 -08:00
} else {
self.bleManager.connectTo(peripheral: peripheral.peripheral)
2023-03-01 23:41:10 -08:00
}
}) {
2023-03-14 20:45:09 -07:00
Text(peripheral.name).font(.callout)
2023-03-01 23:41:10 -08:00
}
Spacer()
VStack {
SignalStrengthIndicator(signalStrength: peripheral.getSignalStrength())
}
}.padding([.bottom, .top])
}
2023-03-03 22:54:46 -08:00
}
.confirmationDialog("Connecting to a new radio will clear all local app data on the phone.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) {
2023-03-06 10:33:18 -08:00
2023-03-03 22:54:46 -08:00
Button("Connect to new radio?", role: .destructive) {
bleManager.stopScanning()
bleManager.connectedPeripheral = nil
UserDefaults.preferredPeripheralId = ""
2023-03-03 22:54:46 -08:00
if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == CBPeripheralState.connected {
bleManager.disconnectPeripheral()
}
2023-03-06 10:33:18 -08:00
2023-03-03 22:54:46 -08:00
clearCoreDataDatabase(context: context)
2023-03-06 10:33:18 -08:00
let radio = bleManager.peripherals.first(where: { $0.peripheral.identifier.uuidString == selectedPeripherialId})
2023-03-03 22:54:46 -08:00
bleManager.connectTo(peripheral: radio!.peripheral)
presentingSwitchPreferredPeripheral = false
selectedPeripherialId = ""
}
}
.textCase(nil)
2023-03-01 23:41:10 -08:00
}
2023-03-06 10:33:18 -08:00
} else {
Text("bluetooth.off")
.foregroundColor(.red)
.font(.title)
}
}
2023-03-06 10:33:18 -08:00
2022-09-28 15:44:42 -07:00
HStack(alignment: .center) {
2022-06-22 23:54:49 -07:00
Spacer()
2023-03-03 20:28:56 -08:00
#if targetEnvironment(macCatalyst)
2022-06-22 23:54:49 -07:00
if bleManager.connectedPeripheral != nil {
Button(role: .destructive, action: {
if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.peripheral.state == CBPeripheralState.connected {
bleManager.disconnectPeripheral(reconnect: false)
}
2022-06-22 23:54:49 -07:00
}) {
2022-12-12 21:19:22 -08:00
Label("disconnect", systemImage: "antenna.radiowaves.left.and.right.slash")
}
2022-06-22 23:54:49 -07:00
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
}
2023-03-03 20:28:56 -08:00
#endif
2023-03-01 23:41:10 -08:00
Spacer()
}
.padding(.bottom, 10)
}
.navigationTitle("bluetooth")
2022-10-17 18:52:49 -07:00
.navigationBarItems(leading: MeshtasticLogo(), trailing:
2023-03-01 23:41:10 -08:00
ZStack {
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????")
})
}
2023-03-06 10:33:18 -08:00
.sheet(isPresented: $invalidFirmwareVersion, onDismiss: didDismissSheet) {
2022-09-27 06:34:13 -07:00
InvalidVersion(minimumVersion: self.bleManager.minimumVersion, version: self.bleManager.connectedVersion)
2022-10-07 19:07:36 -07:00
.presentationDetents([.large])
.presentationDragIndicator(.automatic)
}
2023-03-06 10:33:18 -08:00
.onChange(of: (self.bleManager.invalidVersion)) { _ in
2022-09-27 06:34:13 -07:00
invalidFirmwareVersion = self.bleManager.invalidVersion
}
.onChange(of: (self.bleManager.isSubscribed)) { sub in
2023-03-06 10:33:18 -08:00
if UserDefaults.preferredPeripheralId.count > 0 && sub {
2023-03-06 10:33:18 -08:00
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
2023-03-04 08:52:40 -08:00
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(bleManager.connectedPeripheral?.num ?? -1))
2023-03-06 10:33:18 -08:00
do {
2023-03-06 15:30:10 -08:00
guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else {
return
}
// Found a node, check it for a region
if !fetchedNode.isEmpty {
node = fetchedNode[0]
if node!.loRaConfig != nil && node!.loRaConfig?.regionCode ?? 0 == RegionCodes.unset.rawValue {
isUnsetRegion = true
} else {
isUnsetRegion = false
}
}
} catch {
2023-03-06 10:33:18 -08:00
}
}
}
.onAppear(perform: {
2022-10-17 18:52:49 -07:00
self.bleManager.context = context
})
}
#if canImport(ActivityKit)
2023-03-03 20:08:04 -08:00
func startNodeActivity() {
if #available(iOS 16.2, *) {
liveActivityStarted = true
let timerSeconds = 60
let deviceMetrics = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
2023-03-06 10:33:18 -08:00
2023-03-03 20:08:04 -08:00
let activityAttributes = MeshActivityAttributes(nodeNum: Int(node?.num ?? 0), name: node?.user?.longName ?? "unknown")
2023-03-06 10:33:18 -08:00
2023-03-03 20:08:04 -08:00
let future = Date(timeIntervalSinceNow: Double(timerSeconds))
2023-03-06 10:33:18 -08:00
2023-03-14 13:01:35 -07:00
let initialContentState = MeshActivityAttributes.ContentState(timerRange: Date.now...future, connected: true, channelUtilization: mostRecent?.channelUtilization ?? 0.0, airtime: mostRecent?.airUtilTx ?? 0.0, batteryLevel: UInt32(mostRecent?.batteryLevel ?? 0))
2023-03-06 10:33:18 -08:00
2023-03-03 20:08:04 -08:00
let activityContent = ActivityContent(state: initialContentState, staleDate: Calendar.current.date(byAdding: .minute, value: 2, to: Date())!)
2023-03-06 10:33:18 -08:00
2023-03-03 20:08:04 -08:00
do {
let myActivity = try Activity<MeshActivityAttributes>.request(attributes: activityAttributes, content: activityContent,
pushType: nil)
print(" Requested MyActivity live activity. ID: \(myActivity.id)")
} catch let error {
print("Error requesting live activity: \(error.localizedDescription)")
}
2023-03-01 23:41:10 -08:00
}
}
2023-03-06 10:33:18 -08:00
2023-03-03 20:08:04 -08:00
func endActivity() {
liveActivityStarted = false
Task {
if #available(iOS 16.2, *) {
for activity in Activity<MeshActivityAttributes>.activities {
// Check if this is the activity associated with this order.
if activity.attributes.nodeNum == node?.num ?? 0 {
await activity.end(nil, dismissalPolicy: .immediate)
}
2023-03-01 23:41:10 -08:00
}
}
}
}
#endif
2023-03-06 10:33:18 -08:00
2022-09-27 06:34:13 -07:00
func didDismissSheet() {
bleManager.disconnectPeripheral(reconnect: false)
2022-09-27 06:34:13 -07:00
}
2021-08-20 07:56:05 -07:00
}