mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Add Bitrate Format assorted things. Fix battery display on node details make node list more friendly on iPad and mac
1252 lines
49 KiB
Swift
1252 lines
49 KiB
Swift
import Foundation
|
||
import CoreData
|
||
import CoreBluetooth
|
||
import SwiftUI
|
||
import MapKit
|
||
|
||
// ---------------------------------------------------------------------------------------
|
||
// Meshtastic BLE Device Manager
|
||
// ---------------------------------------------------------------------------------------
|
||
class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate {
|
||
|
||
static let shared = BLEManager()
|
||
|
||
private static var documentsFolder: URL {
|
||
do {
|
||
return try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||
} catch {
|
||
fatalError("Can't find documents directory.")
|
||
}
|
||
}
|
||
|
||
var context: NSManagedObjectContext?
|
||
|
||
var userSettings: UserSettings?
|
||
|
||
private var centralManager: CBCentralManager!
|
||
|
||
@Published var peripherals = [Peripheral]()
|
||
|
||
@Published var connectedPeripheral: Peripheral!
|
||
@Published var lastConnectionError: String
|
||
@Published var lastConnnectionVersion: String
|
||
|
||
@Published var isSwitchedOn: Bool = false
|
||
@Published var isScanning: Bool = false
|
||
@Published var isConnected: Bool = false
|
||
|
||
var timeoutTimer: Timer?
|
||
var timeoutTimerCount = 0
|
||
|
||
var positionTimer: Timer?
|
||
|
||
let broadcastNodeNum: UInt32 = 4294967295
|
||
|
||
/* Meshtastic Service Details */
|
||
var TORADIO_characteristic: CBCharacteristic!
|
||
var FROMRADIO_characteristic: CBCharacteristic!
|
||
var FROMNUM_characteristic: CBCharacteristic!
|
||
|
||
let meshtasticServiceCBUUID = CBUUID(string: "0x6BA1B218-15A8-461F-9FA8-5DCAE273EAFD")
|
||
let TORADIO_UUID = CBUUID(string: "0xF75C76D2-129E-4DAD-A1DD-7866124401E7")
|
||
let FROMRADIO_UUID = CBUUID(string: "0x8BA2BCC2-EE02-4A55-A531-C525C5E454D5")
|
||
let FROMNUM_UUID = CBUUID(string: "0xED9DA18C-A800-4F66-A670-AA7547E34453")
|
||
|
||
private var meshLoggingEnabled: Bool = false
|
||
let meshLog = documentsFolder.appendingPathComponent("meshlog.txt")
|
||
|
||
// MARK: init BLEManager
|
||
override init() {
|
||
|
||
self.meshLoggingEnabled = UserDefaults.standard.object(forKey: "meshActivityLog") as? Bool ?? false
|
||
self.lastConnectionError = ""
|
||
self.lastConnnectionVersion = "0.0.0"
|
||
super.init()
|
||
// let bleQueue: DispatchQueue = DispatchQueue(label: "CentralManager")
|
||
centralManager = CBCentralManager(delegate: self, queue: nil)
|
||
}
|
||
|
||
// MARK: Bluetooth enabled/disabled for the app
|
||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||
if central.state == .poweredOn {
|
||
|
||
isSwitchedOn = true
|
||
startScanning()
|
||
} else {
|
||
|
||
isSwitchedOn = false
|
||
}
|
||
}
|
||
|
||
// MARK: Scanning for BLE Devices
|
||
// Scan for nearby BLE devices using the Meshtastic BLE service ID
|
||
func startScanning() {
|
||
|
||
if isSwitchedOn {
|
||
|
||
centralManager.scanForPeripherals(withServices: [meshtasticServiceCBUUID], options: nil)
|
||
self.isScanning = self.centralManager.isScanning
|
||
|
||
print("✅ Scanning Started")
|
||
}
|
||
}
|
||
|
||
// Stop Scanning For BLE Devices
|
||
func stopScanning() {
|
||
|
||
if centralManager.isScanning {
|
||
|
||
self.centralManager.stopScan()
|
||
self.isScanning = self.centralManager.isScanning
|
||
print("🛑 Stopped Scanning")
|
||
}
|
||
}
|
||
|
||
// MARK: BLE Connect functions
|
||
/// The action after the timeout-timer has fired
|
||
///
|
||
/// - Parameters:
|
||
/// - timer: The time that fired the event
|
||
///
|
||
@objc func timeoutTimerFired(timer: Timer) {
|
||
guard let timerContext = timer.userInfo as? [String: String] else { return }
|
||
let name: String = timerContext["name", default: "Unknown"]
|
||
|
||
self.timeoutTimerCount += 1
|
||
|
||
if timeoutTimerCount == 5 {
|
||
|
||
if connectedPeripheral != nil {
|
||
|
||
self.centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral)
|
||
}
|
||
connectedPeripheral = nil
|
||
self.isConnected = false
|
||
|
||
self.lastConnectionError = "🚨 BLE Connection Timeout after making \(timeoutTimerCount) attempts to connect to \(name)."
|
||
print("🚨 BLE Connection Timeout after making \(timeoutTimerCount) attempts to connect to \(name).")
|
||
if meshLoggingEnabled { MeshLogger.log("🚨 BLE Connection Timeout after making \(timeoutTimerCount) attempts to connect to \(String(name)). This can occur when a device has been taken out of BLE range, or if a device is already connected to another phone, tablet or computer.") }
|
||
|
||
self.timeoutTimerCount = 0
|
||
self.timeoutTimer?.invalidate()
|
||
|
||
} else {
|
||
print("🚨 BLE Connecting 2 Second Timeout Timer Fired \(timeoutTimerCount) Time(s): \(name)")
|
||
if meshLoggingEnabled { MeshLogger.log("🚨 BLE Connecting 2 Second Timeout Timer Fired \(timeoutTimerCount) Time(s): \(name)") }
|
||
}
|
||
}
|
||
|
||
// Connect to a specific peripheral
|
||
func connectTo(peripheral: CBPeripheral) {
|
||
|
||
if meshLoggingEnabled { MeshLogger.log("✅ BLE Connecting: \(peripheral.name ?? "Unknown")") }
|
||
print("✅ BLE Connecting: \(peripheral.name ?? "Unknown")")
|
||
|
||
stopScanning()
|
||
|
||
if self.connectedPeripheral != nil {
|
||
if meshLoggingEnabled { MeshLogger.log("ℹ️ BLE Disconnecting from: \(self.connectedPeripheral.name) to connect to \(peripheral.name ?? "Unknown")") }
|
||
print("ℹ️ BLE Disconnecting from: \(self.connectedPeripheral.name) to connect to \(peripheral.name ?? "Unknown")")
|
||
self.disconnectPeripheral()
|
||
}
|
||
|
||
self.centralManager?.connect(peripheral)
|
||
|
||
// Use a timer to keep track of connecting peripherals, context to pass the radio name with the timer and the RunLoop to prevent
|
||
// the timer from running on the main UI thread
|
||
let context = ["name": "@\(peripheral.name ?? "Unknown")"]
|
||
self.timeoutTimer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(timeoutTimerFired), userInfo: context, repeats: true)
|
||
RunLoop.current.add(self.timeoutTimer!, forMode: .common)
|
||
}
|
||
|
||
// Disconnect Connected Peripheral
|
||
func disconnectPeripheral() {
|
||
|
||
guard let connectedPeripheral = connectedPeripheral else { return }
|
||
self.centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral)
|
||
self.isConnected = false
|
||
}
|
||
|
||
// Called each time a peripheral is discovered
|
||
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
||
|
||
var peripheralName: String = peripheral.name ?? "Unknown"
|
||
|
||
if let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String {
|
||
peripheralName = name
|
||
}
|
||
|
||
let newPeripheral = Peripheral(id: peripheral.identifier.uuidString, num: 0, name: peripheralName, shortName: String(peripheralName.suffix(3)), longName: peripheralName, firmwareVersion: "Unknown", rssi: RSSI.intValue, bitrate: nil, channelUtilization: nil, airTime: nil, lastUpdate: Date(), subscribed: false, peripheral: peripheral)
|
||
let peripheralIndex = peripherals.firstIndex(where: { $0.id == newPeripheral.id })
|
||
|
||
if peripheralIndex != nil && newPeripheral.peripheral.state != CBPeripheralState.connected {
|
||
|
||
peripherals[peripheralIndex!] = newPeripheral
|
||
peripherals.remove(at: peripheralIndex!)
|
||
peripherals.append(newPeripheral)
|
||
|
||
} else {
|
||
|
||
if newPeripheral.peripheral.state != CBPeripheralState.connected {
|
||
|
||
peripherals.append(newPeripheral)
|
||
print("ℹ️ Adding peripheral: \(peripheralName)")
|
||
}
|
||
}
|
||
|
||
let today = Date()
|
||
let fiveMinutesAgo = Calendar.current.date(byAdding: .minute, value: -5, to: today)!
|
||
peripherals.removeAll(where: { $0.lastUpdate <= fiveMinutesAgo})
|
||
}
|
||
|
||
// Called when a peripheral is connected
|
||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||
|
||
self.isConnected = true
|
||
|
||
// Invalidate and reset connection timer count, remove any connection errors
|
||
self.lastConnectionError = ""
|
||
self.timeoutTimer!.invalidate()
|
||
self.timeoutTimerCount = 0
|
||
|
||
// Map the peripheral to the connectedNode and connectedPeripheral ObservedObjects
|
||
connectedPeripheral = peripherals.filter({ $0.peripheral.identifier == peripheral.identifier }).first
|
||
connectedPeripheral.peripheral.delegate = self
|
||
|
||
// Discover Services
|
||
peripheral.discoverServices([meshtasticServiceCBUUID])
|
||
if meshLoggingEnabled { MeshLogger.log("✅ BLE Connected: \(peripheral.name ?? "Unknown")") }
|
||
print("✅ BLE Connected: \(peripheral.name ?? "Unknown")")
|
||
|
||
}
|
||
|
||
// Called when a Peripheral fails to connect
|
||
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
|
||
|
||
if meshLoggingEnabled { MeshLogger.log("🚫 BLE Failed to Connect: \(peripheral.name ?? "Unknown")") }
|
||
print("🚫 BLE Failed to Connect: \(peripheral.name ?? "Unknown")")
|
||
disconnectPeripheral()
|
||
}
|
||
|
||
// Disconnect Peripheral Event
|
||
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
||
// Start a scan so the disconnected peripheral is moved to the peripherals[] if it is awake
|
||
self.startScanning()
|
||
self.connectedPeripheral = nil
|
||
|
||
if let e = error {
|
||
|
||
// https://developer.apple.com/documentation/corebluetooth/cberror/code
|
||
let errorCode = (e as NSError).code
|
||
// unknown = 0,
|
||
|
||
if errorCode == 6 { // CBError.Code.connectionTimeout The connection has timed out unexpectedly.
|
||
|
||
// Happens when device is manually reset / powered off
|
||
// We will try and re-connect to this device
|
||
lastConnectionError = "🚨 \(e.localizedDescription) The app will automatically reconnect to the preferred radio if it reappears within 10 seconds."
|
||
if peripheral.identifier.uuidString == UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" {
|
||
if meshLoggingEnabled { MeshLogger.log("ℹ️ BLE Reconnecting: \(peripheral.name ?? "Unknown")") }
|
||
print("ℹ️ BLE Reconnecting: \(peripheral.name ?? "Unknown")")
|
||
self.connectTo(peripheral: peripheral)
|
||
}
|
||
} else if errorCode == 7 { // CBError.Code.peripheralDisconnected The specified device has disconnected from us.
|
||
|
||
// Seems to be what is received when a tbeam sleeps, immediately recconnecting does not work.
|
||
lastConnectionError = e.localizedDescription
|
||
|
||
print("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)")
|
||
if meshLoggingEnabled { MeshLogger.log("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)") }
|
||
} else if errorCode == 14 { // Peer removed pairing information
|
||
|
||
// Forgetting and reconnecting seems to be necessary so we need to show the user an error telling them to do that
|
||
lastConnectionError = "🚨 \(e.localizedDescription) This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re-connecting to the radio."
|
||
|
||
if meshLoggingEnabled { MeshLogger.log("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(lastConnectionError)") }
|
||
} else {
|
||
|
||
lastConnectionError = e.localizedDescription
|
||
|
||
print("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)")
|
||
if meshLoggingEnabled { MeshLogger.log("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)") }
|
||
}
|
||
} else {
|
||
|
||
// Disconnected without error which indicates user intent to disconnect
|
||
// Happens when swiping to disconnect
|
||
if meshLoggingEnabled { MeshLogger.log("ℹ️ BLE Disconnected: \(peripheral.name ?? "Unknown"): User Initiated Disconnect") }
|
||
print("ℹ️ BLE Disconnected: \(peripheral.name ?? "Unknown"): User Initiated Disconnect")
|
||
}
|
||
}
|
||
|
||
// MARK: Peripheral Services functions
|
||
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
||
|
||
if let e = error {
|
||
|
||
print("🚫 Discover Services error \(e)")
|
||
}
|
||
|
||
guard let services = peripheral.services else { return }
|
||
|
||
for service in services {
|
||
|
||
if service.uuid == meshtasticServiceCBUUID {
|
||
print("✅ Meshtastic service discovered OK")
|
||
if meshLoggingEnabled { MeshLogger.log("✅ BLE Service for Meshtastic discovered by \(peripheral.name ?? "Unknown")") }
|
||
//peripheral.discoverCharacteristics(nil, for: service)
|
||
peripheral.discoverCharacteristics([TORADIO_UUID, FROMRADIO_UUID, FROMNUM_UUID], for: service)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: Discover Characteristics Event
|
||
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
||
if let e = error {
|
||
|
||
print("🚫 Discover Characteristics error \(e)")
|
||
if meshLoggingEnabled { MeshLogger.log("🚫 BLE didDiscoverCharacteristicsFor error by \(peripheral.name ?? "Unknown") \(e)") }
|
||
}
|
||
|
||
guard let characteristics = service.characteristics else { return }
|
||
|
||
for characteristic in characteristics {
|
||
|
||
switch characteristic.uuid {
|
||
case TORADIO_UUID:
|
||
print("✅ TORADIO characteristic OK")
|
||
if meshLoggingEnabled { MeshLogger.log("✅ BLE did discover TORADIO characteristic for Meshtastic by \(peripheral.name ?? "Unknown")") }
|
||
TORADIO_characteristic = characteristic
|
||
var toRadio: ToRadio = ToRadio()
|
||
toRadio.wantConfigID = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||
let binaryData: Data = try! toRadio.serializedData()
|
||
peripheral.writeValue(binaryData, for: characteristic, type: .withResponse)
|
||
|
||
case FROMRADIO_UUID:
|
||
print("✅ FROMRADIO characteristic OK")
|
||
if meshLoggingEnabled { MeshLogger.log("✅ BLE did discover FROMRADIO characteristic for Meshtastic by \(peripheral.name ?? "Unknown")") }
|
||
FROMRADIO_characteristic = characteristic
|
||
peripheral.readValue(for: FROMRADIO_characteristic)
|
||
|
||
case FROMNUM_UUID:
|
||
print("✅ FROMNUM (Notify) characteristic OK")
|
||
if meshLoggingEnabled { MeshLogger.log("✅ BLE did discover FROMNUM (Notify) characteristic for Meshtastic by \(peripheral.name ?? "Unknown")") }
|
||
FROMNUM_characteristic = characteristic
|
||
peripheral.setNotifyValue(true, for: characteristic)
|
||
|
||
default:
|
||
break
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
|
||
|
||
print("ℹ️ didUpdateNotificationStateFor char: \(characteristic.uuid.uuidString) \(characteristic.isNotifying)")
|
||
if meshLoggingEnabled { MeshLogger.log("ℹ️ didUpdateNotificationStateFor char: \(characteristic.uuid.uuidString) \(characteristic.isNotifying)") }
|
||
|
||
if let errorText = error?.localizedDescription {
|
||
print("🚫 didUpdateNotificationStateFor error: \(errorText)")
|
||
}
|
||
}
|
||
|
||
// MARK: Data Read / Update Characteristic Event
|
||
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
||
|
||
|
||
if let e = error {
|
||
|
||
print("🚫 didUpdateValueFor Characteristic error \(e)")
|
||
|
||
let errorCode = (e as NSError).code
|
||
|
||
if errorCode == 5 { // CBATTErrorDomain Code=5 "Authentication is insufficient."
|
||
|
||
// BLE Pin connection error
|
||
// We will try and re-connect to this device
|
||
lastConnectionError = "🚫 BLE \(e.localizedDescription) Please try connecting again and check the PIN carefully."
|
||
if meshLoggingEnabled { MeshLogger.log("🚫 BLE \(e.localizedDescription) Please try connecting again and check the PIN carefully.") }
|
||
self.centralManager?.cancelPeripheralConnection(peripheral)
|
||
|
||
}
|
||
if errorCode == 15 { // CBATTErrorDomain Code=15 "Encryption is insufficient."
|
||
|
||
// BLE Pin connection error
|
||
// We will try and re-connect to this device
|
||
lastConnectionError = "🚫 BLE \(e.localizedDescription) This may be a Meshtastic Firmware bug affecting BLE 4.0 devices."
|
||
if meshLoggingEnabled { MeshLogger.log("🚫 BLE \(e.localizedDescription) Please try connecting again. You may need to forget the device under Settings > General > Bluetooth.") }
|
||
self.centralManager?.cancelPeripheralConnection(peripheral)
|
||
|
||
}
|
||
}
|
||
|
||
switch characteristic.uuid {
|
||
|
||
case FROMRADIO_UUID:
|
||
|
||
if characteristic.value == nil || characteristic.value!.isEmpty {
|
||
return
|
||
}
|
||
|
||
var decodedInfo = FromRadio()
|
||
|
||
decodedInfo = try! FromRadio(serializedData: characteristic.value!)
|
||
|
||
// MARK: Incoming MyInfo Packet
|
||
if decodedInfo.myInfo.myNodeNum != 0 {
|
||
|
||
let fetchMyInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MyInfoEntity")
|
||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(decodedInfo.myInfo.myNodeNum))
|
||
|
||
do {
|
||
let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as! [MyInfoEntity]
|
||
// Not Found Insert
|
||
if fetchedMyInfo.isEmpty {
|
||
let myInfo = MyInfoEntity(context: context!)
|
||
myInfo.myNodeNum = Int64(decodedInfo.myInfo.myNodeNum)
|
||
myInfo.hasGps = decodedInfo.myInfo.hasGps_p
|
||
myInfo.bitrate = decodedInfo.myInfo.bitrate
|
||
self.connectedPeripheral.bitrate = myInfo.bitrate
|
||
|
||
// Swift does strings weird, this does work to get the version without the github hash
|
||
let lastDotIndex = decodedInfo.myInfo.firmwareVersion.lastIndex(of: ".")
|
||
var version = decodedInfo.myInfo.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset: 6, in: decodedInfo.myInfo.firmwareVersion))]
|
||
version = version.dropLast()
|
||
myInfo.firmwareVersion = String(version)
|
||
lastConnnectionVersion = String(version)
|
||
|
||
myInfo.messageTimeoutMsec = Int32(bitPattern: decodedInfo.myInfo.messageTimeoutMsec)
|
||
myInfo.minAppVersion = Int32(bitPattern: decodedInfo.myInfo.minAppVersion)
|
||
myInfo.maxChannels = Int32(bitPattern: decodedInfo.myInfo.maxChannels)
|
||
self.connectedPeripheral.num = myInfo.myNodeNum
|
||
self.connectedPeripheral.firmwareVersion = myInfo.firmwareVersion ?? "Unknown"
|
||
self.connectedPeripheral.name = myInfo.bleName ?? "Unknown"
|
||
|
||
let fetchBCUserRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "UserEntity")
|
||
fetchBCUserRequest.predicate = NSPredicate(format: "num == %lld", Int64(decodedInfo.myInfo.myNodeNum))
|
||
|
||
do {
|
||
let fetchedUser = try context?.fetch(fetchBCUserRequest) as! [UserEntity]
|
||
|
||
if fetchedUser.isEmpty {
|
||
// Save the broadcast user if it does not exist
|
||
let bcu: UserEntity = UserEntity(context: context!)
|
||
bcu.shortName = "ALL"
|
||
bcu.longName = "All - Broadcast"
|
||
bcu.hwModel = "UNSET"
|
||
bcu.num = Int64(broadcastNodeNum)
|
||
bcu.userId = "BROADCASTNODE"
|
||
print("💾 Saved the All - Broadcast User")
|
||
}
|
||
|
||
//var settingsCalled = self.getSettings()
|
||
|
||
if false {
|
||
|
||
print("💾 Called Get Settings")
|
||
|
||
} else {
|
||
|
||
print("💥 Get Settings Call Failed")
|
||
}
|
||
|
||
|
||
} catch {
|
||
|
||
print("💥 Error Saving the All - Broadcast User")
|
||
}
|
||
|
||
} else {
|
||
|
||
fetchedMyInfo[0].myNodeNum = Int64(decodedInfo.myInfo.myNodeNum)
|
||
fetchedMyInfo[0].hasGps = decodedInfo.myInfo.hasGps_p
|
||
fetchedMyInfo[0].bitrate = decodedInfo.myInfo.bitrate
|
||
|
||
let lastDotIndex = decodedInfo.myInfo.firmwareVersion.lastIndex(of: ".")//.lastIndex(of: ".", offsetBy: -1)
|
||
var version = decodedInfo.myInfo.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset:6, in: decodedInfo.myInfo.firmwareVersion))]
|
||
version = version.dropLast()
|
||
fetchedMyInfo[0].firmwareVersion = String(version)
|
||
lastConnnectionVersion = String(version)
|
||
fetchedMyInfo[0].messageTimeoutMsec = Int32(bitPattern: decodedInfo.myInfo.messageTimeoutMsec)
|
||
fetchedMyInfo[0].minAppVersion = Int32(bitPattern: decodedInfo.myInfo.minAppVersion)
|
||
fetchedMyInfo[0].maxChannels = Int32(bitPattern: decodedInfo.myInfo.maxChannels)
|
||
self.connectedPeripheral.num = fetchedMyInfo[0].myNodeNum
|
||
self.connectedPeripheral.firmwareVersion = fetchedMyInfo[0].firmwareVersion ?? "Unknown"
|
||
self.connectedPeripheral.name = fetchedMyInfo[0].bleName ?? "Unknown"
|
||
self.connectedPeripheral.bitrate = fetchedMyInfo[0].bitrate
|
||
|
||
}
|
||
do {
|
||
|
||
try context!.save()
|
||
print("💾 Saved a myInfo for \(decodedInfo.myInfo.myNodeNum)")
|
||
if meshLoggingEnabled { MeshLogger.log("💾 Saved a myInfo for \(peripheral.name ?? String(decodedInfo.myInfo.myNodeNum))") }
|
||
|
||
} catch {
|
||
|
||
context!.rollback()
|
||
|
||
let nsError = error as NSError
|
||
print("💥 Error Saving Core Data MyInfoEntity: \(nsError)")
|
||
}
|
||
|
||
} catch {
|
||
|
||
print("💥 Fetch MyInfo Error")
|
||
}
|
||
|
||
// MARK: Share Location Position Update Timer
|
||
// Use context to pass the radio name with the timer
|
||
// Use a RunLoop to prevent the timer from running on the main UI thread
|
||
if userSettings?.provideLocation ?? false {
|
||
|
||
if self.positionTimer != nil {
|
||
self.positionTimer!.invalidate()
|
||
}
|
||
let context = ["name": "@\(peripheral.name ?? "Unknown")"]
|
||
self.positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval((userSettings?.provideLocationInterval ?? 900)), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true)
|
||
RunLoop.current.add(self.positionTimer!, forMode: .common)
|
||
}
|
||
}
|
||
|
||
// MARK: Incoming Node Info Packet
|
||
if decodedInfo.nodeInfo.num != 0 {
|
||
|
||
let fetchNodeRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||
fetchNodeRequest.predicate = NSPredicate(format: "num == %lld", Int64(decodedInfo.nodeInfo.num))
|
||
|
||
do {
|
||
|
||
let fetchedNode = try context?.fetch(fetchNodeRequest) as! [NodeInfoEntity]
|
||
// Not Found Insert
|
||
if fetchedNode.isEmpty && decodedInfo.nodeInfo.hasUser {
|
||
|
||
let newNode = NodeInfoEntity(context: context!)
|
||
newNode.id = Int64(decodedInfo.nodeInfo.num)
|
||
newNode.num = Int64(decodedInfo.nodeInfo.num)
|
||
|
||
if decodedInfo.nodeInfo.hasDeviceMetrics {
|
||
|
||
let telemetry = TelemetryEntity(context: context!)
|
||
|
||
telemetry.batteryLevel = Int32(decodedInfo.nodeInfo.deviceMetrics.batteryLevel)
|
||
telemetry.voltage = decodedInfo.nodeInfo.deviceMetrics.voltage
|
||
telemetry.channelUtilization = decodedInfo.nodeInfo.deviceMetrics.channelUtilization
|
||
self.connectedPeripheral.channelUtilization = telemetry.channelUtilization
|
||
telemetry.airUtilTx = decodedInfo.nodeInfo.deviceMetrics.airUtilTx
|
||
self.connectedPeripheral.airTime = decodedInfo.nodeInfo.deviceMetrics.airUtilTx
|
||
|
||
var newTelemetries = [TelemetryEntity]()
|
||
newTelemetries.append(telemetry)
|
||
newNode.telemetries? = NSOrderedSet(array: newTelemetries)
|
||
}
|
||
|
||
newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.nodeInfo.lastHeard)))
|
||
newNode.snr = decodedInfo.nodeInfo.snr
|
||
|
||
if self.connectedPeripheral != nil && self.connectedPeripheral.num == newNode.num {
|
||
|
||
if decodedInfo.nodeInfo.hasUser {
|
||
|
||
connectedPeripheral.name = decodedInfo.nodeInfo.user.longName
|
||
}
|
||
}
|
||
|
||
if decodedInfo.nodeInfo.hasUser {
|
||
|
||
let newUser = UserEntity(context: context!)
|
||
newUser.userId = decodedInfo.nodeInfo.user.id
|
||
newUser.num = Int64(decodedInfo.nodeInfo.num)
|
||
newUser.longName = decodedInfo.nodeInfo.user.longName
|
||
newUser.shortName = decodedInfo.nodeInfo.user.shortName
|
||
newUser.macaddr = decodedInfo.nodeInfo.user.macaddr
|
||
newUser.hwModel = String(describing: decodedInfo.nodeInfo.user.hwModel).uppercased()
|
||
newNode.user = newUser
|
||
}
|
||
|
||
let position = PositionEntity(context: context!)
|
||
position.latitudeI = decodedInfo.nodeInfo.position.latitudeI
|
||
position.longitudeI = decodedInfo.nodeInfo.position.longitudeI
|
||
position.altitude = decodedInfo.nodeInfo.position.altitude
|
||
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.nodeInfo.position.time)))
|
||
|
||
var newPostions = [PositionEntity]()
|
||
newPostions.append(position)
|
||
newNode.positions? = NSOrderedSet(array: newPostions)
|
||
|
||
// Look for a MyInfo
|
||
let fetchMyInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MyInfoEntity")
|
||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(decodedInfo.nodeInfo.num))
|
||
|
||
do {
|
||
|
||
let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as! [MyInfoEntity]
|
||
if fetchedMyInfo.count > 0 {
|
||
newNode.myInfo = fetchedMyInfo[0]
|
||
|
||
}
|
||
|
||
} catch {
|
||
print("💥 Fetch MyInfo Error")
|
||
}
|
||
|
||
} else if decodedInfo.nodeInfo.hasUser && decodedInfo.nodeInfo.num > 0 {
|
||
|
||
fetchedNode[0].id = Int64(decodedInfo.nodeInfo.num)
|
||
fetchedNode[0].num = Int64(decodedInfo.nodeInfo.num)
|
||
fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.nodeInfo.lastHeard)))
|
||
fetchedNode[0].snr = decodedInfo.nodeInfo.snr
|
||
|
||
if self.connectedPeripheral != nil && self.connectedPeripheral.num == fetchedNode[0].num {
|
||
|
||
if decodedInfo.nodeInfo.hasUser {
|
||
|
||
self.connectedPeripheral.name = fetchedNode[0].user!.longName ?? "Unknown"
|
||
}
|
||
}
|
||
|
||
if decodedInfo.nodeInfo.hasUser {
|
||
|
||
fetchedNode[0].user!.userId = decodedInfo.nodeInfo.user.id
|
||
fetchedNode[0].user!.num = Int64(decodedInfo.nodeInfo.num)
|
||
fetchedNode[0].user!.longName = decodedInfo.nodeInfo.user.longName
|
||
fetchedNode[0].user!.shortName = decodedInfo.nodeInfo.user.shortName
|
||
fetchedNode[0].user!.macaddr = decodedInfo.nodeInfo.user.macaddr
|
||
fetchedNode[0].user!.hwModel = String(describing: decodedInfo.nodeInfo.user.hwModel).uppercased()
|
||
}
|
||
|
||
if decodedInfo.nodeInfo.hasDeviceMetrics {
|
||
|
||
let newTelemetry = TelemetryEntity(context: context!)
|
||
|
||
newTelemetry.batteryLevel = Int32(decodedInfo.nodeInfo.deviceMetrics.batteryLevel)
|
||
newTelemetry.voltage = decodedInfo.nodeInfo.deviceMetrics.voltage
|
||
newTelemetry.channelUtilization = decodedInfo.nodeInfo.deviceMetrics.channelUtilization
|
||
self.connectedPeripheral.channelUtilization = newTelemetry.channelUtilization
|
||
newTelemetry.airUtilTx = decodedInfo.nodeInfo.deviceMetrics.airUtilTx
|
||
self.connectedPeripheral.airTime = decodedInfo.nodeInfo.deviceMetrics.airUtilTx
|
||
|
||
let mutableTelemetries = fetchedNode[0].telemetries!.mutableCopy() as! NSMutableOrderedSet
|
||
fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet
|
||
}
|
||
|
||
if decodedInfo.nodeInfo.hasPosition {
|
||
|
||
let position = PositionEntity(context: context!)
|
||
position.latitudeI = decodedInfo.nodeInfo.position.latitudeI
|
||
position.longitudeI = decodedInfo.nodeInfo.position.longitudeI
|
||
position.altitude = decodedInfo.nodeInfo.position.altitude
|
||
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.nodeInfo.position.time)))
|
||
|
||
let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet
|
||
|
||
fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet
|
||
|
||
}
|
||
|
||
// Look for a MyInfo
|
||
let fetchMyInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MyInfoEntity")
|
||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(decodedInfo.nodeInfo.num))
|
||
|
||
do {
|
||
|
||
let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as! [MyInfoEntity]
|
||
if fetchedMyInfo.count > 0 {
|
||
|
||
fetchedNode[0].myInfo = fetchedMyInfo[0]
|
||
}
|
||
|
||
} catch {
|
||
print("💥 Fetch MyInfo Error")
|
||
}
|
||
}
|
||
do {
|
||
|
||
try context!.save()
|
||
print("💾 Saved a nodeInfo for \(decodedInfo.nodeInfo.num)")
|
||
|
||
} catch {
|
||
|
||
context!.rollback()
|
||
|
||
let nsError = error as NSError
|
||
print("💥 Error Saving Core Data NodeInfoEntity: \(nsError)")
|
||
}
|
||
|
||
} catch {
|
||
|
||
print("💥 Fetch NodeInfoEntity Error")
|
||
}
|
||
|
||
if decodedInfo.nodeInfo.hasUser {
|
||
|
||
print("💾 BLE FROMRADIO received and nodeInfo saved for \(decodedInfo.nodeInfo.user.longName)")
|
||
if meshLoggingEnabled { MeshLogger.log("💾 BLE FROMRADIO received and nodeInfo saved for \(decodedInfo.nodeInfo.user.longName)") }
|
||
|
||
} else {
|
||
|
||
print("💾 BLE FROMRADIO received and nodeInfo saved for \(decodedInfo.nodeInfo.num)")
|
||
if meshLoggingEnabled { MeshLogger.log("💾 BLE FROMRADIO received and nodeInfo saved for \(decodedInfo.nodeInfo.num)") }
|
||
}
|
||
}
|
||
// Handle other packet types
|
||
if decodedInfo.packet.id != 0 {
|
||
|
||
do {
|
||
|
||
// MARK: Incoming Packet from the TEXTMESSAGE_APP
|
||
if decodedInfo.packet.decoded.portnum == PortNum.textMessageApp {
|
||
|
||
if let messageText = String(bytes: decodedInfo.packet.decoded.payload, encoding: .utf8) {
|
||
|
||
print("💬 BLE FROMRADIO received for text message app \(messageText)")
|
||
if meshLoggingEnabled { MeshLogger.log("💬 BLE FROMRADIO received for text message app \(messageText)") }
|
||
|
||
let messageUsers: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "UserEntity")
|
||
messageUsers.predicate = NSPredicate(format: "num IN %@", [decodedInfo.packet.to, decodedInfo.packet.from])
|
||
|
||
do {
|
||
|
||
let fetchedUsers = try context?.fetch(messageUsers) as! [UserEntity]
|
||
|
||
let newMessage = MessageEntity(context: context!)
|
||
newMessage.messageId = Int64(decodedInfo.packet.id)
|
||
newMessage.messageTimestamp = Int32(bitPattern: decodedInfo.packet.rxTime)
|
||
newMessage.receivedACK = false
|
||
newMessage.direction = "IN"
|
||
newMessage.isEmoji = decodedInfo.packet.decoded.emoji == 1
|
||
|
||
if decodedInfo.packet.decoded.replyID > 0 {
|
||
|
||
newMessage.replyID = Int64(decodedInfo.packet.decoded.replyID)
|
||
}
|
||
|
||
if decodedInfo.packet.to == broadcastNodeNum && fetchedUsers.count == 1 {
|
||
|
||
// Save the broadcast user if it does not exist
|
||
let bcu: UserEntity = UserEntity(context: context!)
|
||
bcu.shortName = "ALL"
|
||
bcu.longName = "All - Broadcast"
|
||
bcu.hwModel = "UNSET"
|
||
bcu.num = Int64(broadcastNodeNum)
|
||
bcu.userId = "BROADCASTNODE"
|
||
newMessage.toUser = bcu
|
||
|
||
} else {
|
||
|
||
newMessage.toUser = fetchedUsers.first(where: { $0.num == decodedInfo.packet.to })
|
||
}
|
||
|
||
newMessage.fromUser = fetchedUsers.first(where: { $0.num == decodedInfo.packet.from })
|
||
newMessage.messagePayload = messageText
|
||
|
||
do {
|
||
|
||
try context!.save()
|
||
print("💾 Saved a new message for \(decodedInfo.packet.id)")
|
||
if meshLoggingEnabled { MeshLogger.log("💾 Saved a new message for \(newMessage.messageId)") }
|
||
|
||
if newMessage.toUser != nil && newMessage.toUser!.num == self.broadcastNodeNum || self.connectedPeripheral != nil && self.connectedPeripheral.num == newMessage.toUser!.num {
|
||
|
||
// Create an iOS Notification for the received message and schedule it immediately
|
||
let manager = LocalNotificationManager()
|
||
|
||
manager.notifications = [
|
||
Notification(
|
||
id: ("notification.id.\(newMessage.messageId)"),
|
||
title: "\(newMessage.fromUser?.longName ?? "Unknown")",
|
||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")",
|
||
content: messageText)
|
||
]
|
||
manager.schedule()
|
||
if meshLoggingEnabled { MeshLogger.log("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown") \(messageText)") }
|
||
|
||
}
|
||
} catch {
|
||
|
||
context!.rollback()
|
||
|
||
let nsError = error as NSError
|
||
print("💥 Failed to save new MessageEntity \(nsError)")
|
||
}
|
||
|
||
} catch {
|
||
|
||
print("💥 Fetch Message To and From Users Error")
|
||
}
|
||
}
|
||
// MARK: Incoming NODEINFO_APP Packet
|
||
} else if decodedInfo.packet.decoded.portnum == PortNum.nodeinfoApp {
|
||
|
||
let fetchNodeInfoAppRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||
fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %lld", Int64(decodedInfo.packet.from))
|
||
|
||
do {
|
||
|
||
let fetchedNode = try context?.fetch(fetchNodeInfoAppRequest) as! [NodeInfoEntity]
|
||
|
||
if fetchedNode.count == 1 {
|
||
fetchedNode[0].id = Int64(decodedInfo.packet.from)
|
||
fetchedNode[0].num = Int64(decodedInfo.packet.from)
|
||
fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.packet.rxTime)))
|
||
fetchedNode[0].snr = decodedInfo.packet.rxSnr
|
||
|
||
} else {
|
||
return
|
||
}
|
||
do {
|
||
|
||
try context!.save()
|
||
|
||
if meshLoggingEnabled { MeshLogger.log("💾 Updated NodeInfo SNR \(decodedInfo.packet.rxSnr) and Time from Node Info App Packet For: \(fetchedNode[0].num)")}
|
||
print("💾 Updated NodeInfo SNR \(decodedInfo.packet.rxSnr) and Time from 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")
|
||
}
|
||
|
||
// MARK: Incoming Packet from the POSITION_APP
|
||
} else if decodedInfo.packet.decoded.portnum == PortNum.positionApp {
|
||
let fetchNodePositionRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||
fetchNodePositionRequest.predicate = NSPredicate(format: "num == %lld", Int64(decodedInfo.packet.from))
|
||
|
||
do {
|
||
|
||
let fetchedNode = try context?.fetch(fetchNodePositionRequest) as! [NodeInfoEntity]
|
||
|
||
if fetchedNode.count == 1 {
|
||
fetchedNode[0].id = Int64(decodedInfo.packet.from)
|
||
fetchedNode[0].num = Int64(decodedInfo.packet.from)
|
||
fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.packet.rxTime)))
|
||
fetchedNode[0].snr = decodedInfo.packet.rxSnr
|
||
|
||
if let positionMessage = try? Position(serializedData: decodedInfo.packet.decoded.payload) {
|
||
|
||
let position = PositionEntity(context: context!)
|
||
position.latitudeI = positionMessage.latitudeI
|
||
position.longitudeI = positionMessage.longitudeI
|
||
position.altitude = positionMessage.altitude
|
||
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time)))
|
||
|
||
let mutablePositions = fetchedNode[0].positions!.mutableCopy() as! NSMutableOrderedSet
|
||
mutablePositions.add(position)
|
||
|
||
fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet
|
||
}
|
||
|
||
} else {
|
||
|
||
return
|
||
}
|
||
do {
|
||
|
||
try context!.save()
|
||
|
||
if meshLoggingEnabled {
|
||
MeshLogger.log("💾 Updated NodeInfo Position Coordinates, SNR \(decodedInfo.packet.rxSnr) and Time from Position App Packet For: \(fetchedNode[0].num)")
|
||
}
|
||
print("💾 Updated NodeInfo Position Coordinates, SNR \(decodedInfo.packet.rxSnr) and Time from Position App Packet For:: \(fetchedNode[0].num)")
|
||
|
||
} catch {
|
||
|
||
context!.rollback()
|
||
|
||
let nsError = error as NSError
|
||
print("💥 Error Saving NodeInfoEntity from POSITION_APP \(nsError)")
|
||
}
|
||
} catch {
|
||
|
||
print("💥 Error Fetching NodeInfoEntity for POSITION_APP")
|
||
}
|
||
// MARK: Incoming ROUTING_APP Packet
|
||
} else if decodedInfo.packet.decoded.portnum == PortNum.routingApp {
|
||
|
||
|
||
if let routingMessage = try? Routing(serializedData: decodedInfo.packet.decoded.payload) {
|
||
print(decodedInfo.packet.decoded.requestID)
|
||
print(routingMessage)
|
||
|
||
let error = routingMessage.errorReason
|
||
|
||
var errorExplanation = "Unknown Routing Error"
|
||
|
||
switch error {
|
||
case Routing.Error.none:
|
||
errorExplanation = "This message is not a failure"
|
||
case Routing.Error.noRoute:
|
||
errorExplanation = "Our node doesn't have a route to the requested destination anymore."
|
||
case Routing.Error.gotNak:
|
||
errorExplanation = "We received a nak while trying to forward on your behalf"
|
||
case Routing.Error.timeout:
|
||
errorExplanation = "Timeout"
|
||
case Routing.Error.noInterface:
|
||
errorExplanation = "No suitable interface could be found for delivering this packet"
|
||
case Routing.Error.maxRetransmit:
|
||
errorExplanation = "We reached the max retransmission count (typically for naive flood routing)"
|
||
case Routing.Error.noChannel:
|
||
errorExplanation = "No suitable channel was found for sending this packet (i.e. was requested channel index disabled?)"
|
||
case Routing.Error.tooLarge:
|
||
errorExplanation = "The packet was too big for sending (exceeds interface MTU after encoding)"
|
||
case Routing.Error.noResponse:
|
||
errorExplanation = "The request had want_response set, the request reached the destination node, but no service on that node wants to send a response (possibly due to bad channel permissions)"
|
||
case Routing.Error.badRequest:
|
||
errorExplanation = "The application layer service on the remote node received your request, but considered your request somehow invalid"
|
||
case Routing.Error.notAuthorized:
|
||
errorExplanation = "The application layer service on the remote node received your request, but considered your request not authorized (i.e you did not send the request on the required bound channel)"
|
||
fallthrough
|
||
default:
|
||
print(error)
|
||
}
|
||
|
||
if meshLoggingEnabled { MeshLogger.log("🕸️ ROUTING PACKET received for RequestID: \(decodedInfo.packet.decoded.requestID) Error: \(errorExplanation)") }
|
||
print("🕸️ ROUTING PACKET received for RequestID: \(decodedInfo.packet.decoded.requestID) Error: \(errorExplanation)")
|
||
|
||
|
||
if routingMessage.errorReason == Routing.Error.none {
|
||
|
||
print("Priority ACK no Error")
|
||
|
||
let fetchMessageRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MessageEntity")
|
||
fetchMessageRequest.predicate = NSPredicate(format: "messageId == %lld", Int64(decodedInfo.packet.decoded.requestID))
|
||
|
||
do {
|
||
|
||
let fetchedMessage = try context?.fetch(fetchMessageRequest)[0] as? MessageEntity
|
||
|
||
if fetchedMessage != nil {
|
||
|
||
fetchedMessage!.receivedACK = true
|
||
fetchedMessage!.ackSNR = decodedInfo.packet.rxSnr
|
||
fetchedMessage!.ackTimestamp = Int32(decodedInfo.packet.rxTime)
|
||
fetchedMessage!.objectWillChange.send()
|
||
}
|
||
|
||
try context!.save()
|
||
|
||
if meshLoggingEnabled {
|
||
MeshLogger.log("💾 ACK Received and saved for MessageID \(decodedInfo.packet.decoded.requestID)")
|
||
}
|
||
print("💾 ACK Received and saved for MessageID \(decodedInfo.packet.decoded.requestID)")
|
||
|
||
} catch {
|
||
|
||
context!.rollback()
|
||
|
||
let nsError = error as NSError
|
||
print("💥 Error Saving ACK for message MessageID \(decodedInfo.packet.id) Error: \(nsError)")
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: Incoming TELEMETRY_APP Packet
|
||
} else if decodedInfo.packet.decoded.portnum == PortNum.telemetryApp {
|
||
|
||
|
||
if let telemetryMessage = try? Telemetry(serializedData: decodedInfo.packet.decoded.payload) {
|
||
|
||
let telemetry = TelemetryEntity(context: context!)
|
||
print(decodedInfo.packet.decoded.requestID)
|
||
print(telemetryMessage)
|
||
}
|
||
|
||
|
||
if meshLoggingEnabled { MeshLogger.log("ℹ️ MESH PACKET received for Telemetry App UNHANDLED \(try decodedInfo.packet.jsonString())") }
|
||
print("ℹ️ MESH PACKET received for Telemetry App UNHANDLED \(try decodedInfo.packet.jsonString())")
|
||
|
||
} else if decodedInfo.packet.decoded.portnum == PortNum.storeForwardApp {
|
||
|
||
if meshLoggingEnabled { MeshLogger.log("ℹ️ MESH PACKET received for Store Forward App UNHANDLED \(try decodedInfo.packet.jsonString())") }
|
||
print("ℹ️ MESH PACKET received for Admin App UNHANDLED \(try decodedInfo.packet.jsonString())")
|
||
|
||
} else if decodedInfo.packet.decoded.portnum == PortNum.adminApp {
|
||
|
||
if meshLoggingEnabled { MeshLogger.log("ℹ️ MESH PACKET received for Admin App UNHANDLED \(try decodedInfo.packet.jsonString())") }
|
||
print("ℹ️ MESH PACKET received for Admin App UNHANDLED \(try decodedInfo.packet.jsonString())")
|
||
|
||
} else {
|
||
|
||
if meshLoggingEnabled { MeshLogger.log("ℹ️ MESH PACKET received for Other App UNHANDLED \(try decodedInfo.packet.jsonString())") }
|
||
print("ℹ️ MESH PACKET received for Other App UNHANDLED \(try decodedInfo.packet.jsonString())")
|
||
}
|
||
|
||
} catch {
|
||
if meshLoggingEnabled { MeshLogger.log("⚰️ Fatal Error: Failed to decode json") }
|
||
print("⚰️ Fatal Error: Failed to decode json")
|
||
}
|
||
}
|
||
|
||
if decodedInfo.configCompleteID != 0 {
|
||
|
||
if meshLoggingEnabled { MeshLogger.log("🤜 BLE Config Complete Packet Id: \(decodedInfo.configCompleteID)") }
|
||
print("🤜 BLE Config Complete Packet Id: \(decodedInfo.configCompleteID)")
|
||
self.connectedPeripheral.subscribed = true
|
||
peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected })
|
||
}
|
||
|
||
case FROMNUM_UUID :
|
||
print("🚨 FROMNUM Characteristic UUID: \(characteristic.uuid)")
|
||
if characteristic.value == nil || characteristic.value!.isEmpty {
|
||
return
|
||
}
|
||
|
||
var decodedInfo = FromRadio()
|
||
|
||
//decodedInfo = try! FromRadio(serializedData: characteristic.value!)
|
||
|
||
default:
|
||
print("🚨 Unhandled Characteristic UUID: \(characteristic.uuid)")
|
||
}
|
||
peripheral.readValue(for: FROMRADIO_characteristic)
|
||
}
|
||
|
||
// Send Message
|
||
public func sendMessage(message: String, toUserNum: Int64, isEmoji: Bool, replyID: Int64) -> Bool {
|
||
|
||
var success = false
|
||
|
||
// Return false if we are not properly connected to a device, handle retry logic in the view for now
|
||
if connectedPeripheral == nil || connectedPeripheral!.peripheral.state != CBPeripheralState.connected {
|
||
|
||
self.disconnectPeripheral()
|
||
self.startScanning()
|
||
|
||
// Try and connect to the preferredPeripherial first
|
||
let preferredPeripheral = peripherals.filter({ $0.peripheral.identifier.uuidString == UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" }).first
|
||
if preferredPeripheral != nil && preferredPeripheral?.peripheral != nil {
|
||
connectTo(peripheral: preferredPeripheral!.peripheral)
|
||
}
|
||
print("🚫 Message Send Failed, not properly connected to \(preferredPeripheral?.name ?? "Unknown")")
|
||
if meshLoggingEnabled { MeshLogger.log("🚫 Message Send Failed, not properly connected to \(preferredPeripheral?.name ?? "Unknown")") }
|
||
|
||
success = false
|
||
|
||
} else if message.count < 1 {
|
||
|
||
// Don't send an empty message
|
||
print("🚫 Don't Send an Empty Message")
|
||
success = false
|
||
|
||
} else {
|
||
|
||
let fromUserNum: Int64 = self.connectedPeripheral.num
|
||
|
||
let messageUsers: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "UserEntity")
|
||
messageUsers.predicate = NSPredicate(format: "num IN %@", [fromUserNum, Int64(toUserNum)])
|
||
|
||
do {
|
||
|
||
let fetchedUsers = try context?.fetch(messageUsers) as! [UserEntity]
|
||
|
||
if fetchedUsers.isEmpty {
|
||
|
||
print("🚫 Message Users Not Found, Fail")
|
||
success = false
|
||
|
||
} else if fetchedUsers.count >= 1 {
|
||
|
||
let newMessage = MessageEntity(context: context!)
|
||
newMessage.messageId = Int64(UInt32.random(in: UInt32(UInt8.max)..<UInt32.max))
|
||
//newMessage.messageId = Int64(0xFF | UInt32.random(in: UInt32(UInt8.max)..<UInt32(1147483647)))
|
||
newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970)
|
||
newMessage.receivedACK = false
|
||
newMessage.direction = "IN"
|
||
newMessage.toUser = fetchedUsers.first(where: { $0.num == toUserNum })
|
||
newMessage.isEmoji = isEmoji
|
||
|
||
if replyID > 0 {
|
||
|
||
newMessage.replyID = replyID
|
||
}
|
||
if newMessage.toUser == nil {
|
||
|
||
let bcu: UserEntity = UserEntity(context: context!)
|
||
bcu.shortName = "ALL"
|
||
bcu.longName = "All - Broadcast"
|
||
bcu.hwModel = "UNSET"
|
||
bcu.num = Int64(broadcastNodeNum)
|
||
bcu.userId = "BROADCASTNODE"
|
||
newMessage.toUser = bcu
|
||
}
|
||
|
||
newMessage.fromUser = fetchedUsers.first(where: { $0.num == fromUserNum })
|
||
newMessage.messagePayload = message
|
||
|
||
let dataType = PortNum.textMessageApp
|
||
let payloadData: Data = message.data(using: String.Encoding.utf8)!
|
||
|
||
var dataMessage = DataMessage()
|
||
dataMessage.payload = payloadData
|
||
dataMessage.portnum = dataType
|
||
|
||
var meshPacket = MeshPacket()
|
||
meshPacket.id = UInt32(newMessage.messageId)
|
||
meshPacket.to = UInt32(toUserNum)
|
||
meshPacket.from = UInt32(fromUserNum)
|
||
meshPacket.decoded = dataMessage
|
||
meshPacket.decoded.emoji = isEmoji ? 1 : 0
|
||
if replyID > 0 {
|
||
meshPacket.decoded.replyID = UInt32(replyID)
|
||
}
|
||
meshPacket.wantAck = true
|
||
|
||
var toRadio: ToRadio!
|
||
toRadio = ToRadio()
|
||
toRadio.packet = meshPacket
|
||
|
||
let binaryData: Data = try! toRadio.serializedData()
|
||
|
||
if meshLoggingEnabled { MeshLogger.log("📲 New messageId \(newMessage.messageId) sent to \(newMessage.toUser?.longName! ?? "Unknown")") }
|
||
print("📲 New messageId \(newMessage.messageId) sent to \(newMessage.toUser?.longName! ?? "Unknown")")
|
||
|
||
if connectedPeripheral!.peripheral.state == CBPeripheralState.connected {
|
||
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
|
||
do {
|
||
|
||
try context!.save()
|
||
print("💾 Saved a new sent message to \(toUserNum)")
|
||
if meshLoggingEnabled { MeshLogger.log("💾 Saved a new sent message from \(connectedPeripheral.num) to \(toUserNum)") }
|
||
success = true
|
||
|
||
} catch {
|
||
|
||
context!.rollback()
|
||
|
||
let nsError = error as NSError
|
||
print("💥 Unresolved Core Data error in Send Message Function it is likely that your database is corrupted deleting and re-installing the app should clear the corrupted data. Error: \(nsError)")
|
||
if meshLoggingEnabled { MeshLogger.log("💥 Unresolved Core Data error \(nsError)") }
|
||
}
|
||
}
|
||
}
|
||
|
||
} catch {
|
||
|
||
}
|
||
}
|
||
return success
|
||
}
|
||
|
||
// Send Position
|
||
public func sendPosition(destNum: Int64, wantResponse: Bool) -> Bool {
|
||
|
||
var success = false
|
||
|
||
let fromNodeNum = connectedPeripheral.num
|
||
|
||
if fromNodeNum <= 0 || (LocationHelper.currentLocation.latitude == LocationHelper.DefaultLocation.latitude && LocationHelper.currentLocation.longitude == LocationHelper.DefaultLocation.longitude) {
|
||
|
||
return false
|
||
}
|
||
|
||
let fetchNode: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||
fetchNode.predicate = NSPredicate(format: "num == %lld", fromNodeNum)
|
||
|
||
do {
|
||
|
||
let fetchedNode = try context?.fetch(fetchNode) as! [NodeInfoEntity]
|
||
|
||
|
||
if fetchedNode.count == 1 {
|
||
|
||
var positionPacket = Position()
|
||
positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7)
|
||
positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7)
|
||
positionPacket.time = UInt32(LocationHelper.currentTimestamp.timeIntervalSince1970)
|
||
positionPacket.altitude = Int32(LocationHelper.currentAltitude)
|
||
|
||
var meshPacket = MeshPacket()
|
||
meshPacket.to = UInt32(destNum)
|
||
meshPacket.from = UInt32(connectedPeripheral.num)
|
||
meshPacket.wantAck = wantResponse
|
||
|
||
var dataMessage = DataMessage()
|
||
dataMessage.payload = try! positionPacket.serializedData()
|
||
dataMessage.portnum = PortNum.positionApp
|
||
|
||
meshPacket.decoded = dataMessage
|
||
|
||
var toRadio: ToRadio!
|
||
toRadio = ToRadio()
|
||
toRadio.packet = meshPacket
|
||
let binaryData: Data = try! toRadio.serializedData()
|
||
|
||
if meshLoggingEnabled { MeshLogger.log("📍 Sent a Position Packet from the phone to the device for node: \(fromNodeNum)") }
|
||
print("📍 Sent a Position Packet from the phone to the device for node: \(fromNodeNum)")
|
||
|
||
if connectedPeripheral!.peripheral.state == CBPeripheralState.connected {
|
||
|
||
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
|
||
success = true
|
||
}
|
||
}
|
||
|
||
} catch {
|
||
success = false
|
||
}
|
||
|
||
return success
|
||
}
|
||
|
||
@objc func positionTimerFired(timer: Timer) {
|
||
|
||
// Check for connected node
|
||
if connectedPeripheral != nil {
|
||
|
||
// Send a position out to the mesh if "share location with the mesh" is enabled in settings
|
||
if userSettings!.provideLocation {
|
||
|
||
let success = sendPosition(destNum: connectedPeripheral.num, wantResponse: false)
|
||
if !success {
|
||
|
||
print("Failed to send positon to device")
|
||
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: Device Settings
|
||
public func getSettings() -> Bool {
|
||
|
||
var adminPacket = AdminMessage()
|
||
//adminPacket.getRadioRequest = true
|
||
|
||
var meshPacket: MeshPacket = MeshPacket()
|
||
meshPacket.to = UInt32(connectedPeripheral.num)
|
||
meshPacket.from = UInt32(connectedPeripheral.num)
|
||
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||
meshPacket.priority = MeshPacket.Priority.reliable
|
||
meshPacket.wantAck = true
|
||
meshPacket.hopLimit = 0
|
||
|
||
var dataMessage = DataMessage()
|
||
dataMessage.payload = try! adminPacket.serializedData()
|
||
dataMessage.portnum = PortNum.adminApp
|
||
|
||
meshPacket.decoded = dataMessage
|
||
|
||
var toRadio: ToRadio!
|
||
toRadio = ToRadio()
|
||
toRadio.packet = meshPacket
|
||
|
||
let binaryData: Data = try! toRadio.serializedData()
|
||
|
||
if connectedPeripheral!.peripheral.state == CBPeripheralState.connected {
|
||
|
||
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
|
||
|
||
return true
|
||
}
|
||
|
||
return false
|
||
}
|
||
}
|