import Foundation import CoreData import CoreBluetooth import SwiftUI import MapKit import MeshtasticProtobufs import CocoaMQTT import OSLog // --------------------------------------------------------------------------------------- // Meshtastic BLE Device Manager // --------------------------------------------------------------------------------------- class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate, ObservableObject { static var shared: BLEManager! // Singleton instance let appState: AppState let context: NSManagedObjectContext private var centralManager: CBCentralManager! @Published var peripherals: [Peripheral] = [] @Published var connectedPeripheral: Peripheral! @Published var lastConnectionError: String @Published var invalidVersion = false @Published var isSwitchedOn: Bool = false @Published var automaticallyReconnect: Bool = true @Published var mqttProxyConnected: Bool = false @Published var mqttError: String = "" public var minimumVersion = "2.3.15" public var connectedVersion: String public var isConnecting: Bool = false public var isConnected: Bool = false public var isSubscribed: Bool = false private var configNonce: UInt32 = 1 var timeoutTimer: Timer? var timeoutTimerCount = 0 var positionTimer: Timer? let mqttManager = MqttClientProxyManager.shared var wantRangeTestPackets = false var wantStoreAndForwardPackets = false /* Meshtastic Service Details */ var TORADIO_characteristic: CBCharacteristic! var FROMRADIO_characteristic: CBCharacteristic! var FROMNUM_characteristic: CBCharacteristic! var LEGACY_LOGRADIO_characteristic: CBCharacteristic! var LOGRADIO_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: "0x2C55E69E-4993-11ED-B878-0242AC120002") let EOL_FROMRADIO_UUID = CBUUID(string: "0x8BA2BCC2-EE02-4A55-A531-C525C5E454D5") let FROMNUM_UUID = CBUUID(string: "0xED9DA18C-A800-4F66-A670-AA7547E34453") let LEGACY_LOGRADIO_UUID = CBUUID(string: "0x6C6FD238-78FA-436B-AACF-15C5BE1EF2E2") let LOGRADIO_UUID = CBUUID(string: "0x5a3d6e49-06e6-4423-9944-e9de8cdf9547") // MARK: init private override init() { // Default initialization should not be used fatalError("Use setup(appState:context:) to initialize the singleton") } static func setup(appState: AppState, context: NSManagedObjectContext) { guard shared == nil else { Logger.services.warning("[BLE] BLEManager already initialized") return } shared = BLEManager(appState: appState, context: context) } private init(appState: AppState, context: NSManagedObjectContext) { self.appState = appState self.context = context self.lastConnectionError = "" self.connectedVersion = "0.0.0" super.init() centralManager = CBCentralManager(delegate: self, queue: nil) mqttManager.delegate = self } // 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: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) Logger.services.info("โœ… [BLE] Scanning Started") } } // Stop Scanning For BLE Devices func stopScanning() { if centralManager.isScanning { centralManager.stopScan() Logger.services.info("๐Ÿ›‘ [BLE] 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 self.lastConnectionError = "" if timeoutTimerCount == 10 { if connectedPeripheral != nil { self.centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) } connectedPeripheral = nil if self.timeoutTimer != nil { self.timeoutTimer!.invalidate() } self.isConnected = false self.isConnecting = false self.lastConnectionError = "๐Ÿšจ " + String.localizedStringWithFormat("Connection failed after %d attempts to connect to %@. You may need to forget your device under Settings > Bluetooth.".localized, timeoutTimerCount, name) Logger.services.error("\(self.lastConnectionError, privacy: .public)") self.timeoutTimerCount = 0 self.startScanning() } else { Logger.services.info("๐Ÿšจ [BLE] Connecting 2 Second Timeout Timer Fired \(self.timeoutTimerCount, privacy: .public) Time(s): \(name, privacy: .public)") } } // Connect to a specific peripheral func connectTo(peripheral: CBPeripheral) { stopScanning() DispatchQueue.main.async { self.isConnecting = true self.lastConnectionError = "" self.automaticallyReconnect = true } if connectedPeripheral != nil { Logger.services.info("โ„น๏ธ [BLE] Disconnecting from: \(self.connectedPeripheral.name, privacy: .public) to connect to \(peripheral.name ?? "Unknown", privacy: .public)") disconnectPeripheral() } centralManager?.connect(peripheral) // Invalidate any existing timer if timeoutTimer != nil { timeoutTimer!.invalidate() } // 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")"] timeoutTimer = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(timeoutTimerFired), userInfo: context, repeats: true) RunLoop.current.add(timeoutTimer!, forMode: .common) Logger.services.info("โ„น๏ธ BLE Connecting: \(peripheral.name ?? "Unknown", privacy: .public)") } // Disconnect Connected Peripheral func cancelPeripheralConnection() { if mqttProxyConnected { mqttManager.mqttClientProxy?.disconnect() } FROMRADIO_characteristic = nil isConnecting = false isConnected = false isSubscribed = false self.connectedPeripheral = nil invalidVersion = false connectedVersion = "0.0.0" connectedPeripheral = nil if timeoutTimer != nil { timeoutTimer!.invalidate() } automaticallyReconnect = false stopScanning() startScanning() } // Disconnect Connected Peripheral func disconnectPeripheral(reconnect: Bool = true) { guard let connectedPeripheral = connectedPeripheral else { return } if mqttProxyConnected { mqttManager.mqttClientProxy?.disconnect() } automaticallyReconnect = reconnect centralManager?.cancelPeripheralConnection(connectedPeripheral.peripheral) FROMRADIO_characteristic = nil isConnected = false isSubscribed = false invalidVersion = false connectedVersion = "0.0.0" stopScanning() startScanning() } // Called each time a peripheral is connected func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { isConnecting = false isConnected = true if UserDefaults.preferredPeripheralId.count < 1 { UserDefaults.preferredPeripheralId = peripheral.identifier.uuidString } // Invalidate and reset connection timer count timeoutTimerCount = 0 if timeoutTimer != nil { timeoutTimer!.invalidate() } // remove any connection errors self.lastConnectionError = "" // Map the peripheral to the connectedPeripheral ObservedObjects connectedPeripheral = peripherals.filter({ $0.peripheral.identifier == peripheral.identifier }).first if connectedPeripheral != nil { connectedPeripheral.peripheral.delegate = self } else { // we are null just disconnect and start over lastConnectionError = "๐Ÿšซ [BLE] Bluetooth connection error, please try again." disconnectPeripheral() return } // Discover Services peripheral.discoverServices([meshtasticServiceCBUUID]) Logger.services.info("โœ… [BLE] Connected: \(peripheral.name ?? "Unknown", privacy: .public)") } // Called when a Peripheral fails to connect func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { if let e = error { // https://developer.apple.com/documentation/corebluetooth/cberror/code let errorCode = (e as NSError).code cancelPeripheralConnection() 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 = "๐Ÿšจ " + String.localizedStringWithFormat("%@ This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re pairing the radio.".localized, e.localizedDescription) Logger.services.error("๐Ÿšจ [BLE] Failed to connect: \(peripheral.name ?? "Unknown".localized) Error Code: \(errorCode, privacy: .public) Error: \(self.lastConnectionError, privacy: .public)") } else { lastConnectionError = "๐Ÿšจ \(e.localizedDescription)" Logger.services.error("๐Ÿšจ [BLE] Failed to connect: \(peripheral.name ?? "Unknown".localized, privacy: .public) Error Code: \(errorCode, privacy: .public) Error: \(e.localizedDescription, privacy: .public)") } } } // Disconnect Peripheral Event func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { self.connectedPeripheral = nil self.isConnecting = false self.isConnected = false self.isSubscribed = false let manager = LocalNotificationManager() if let e = error { // https://developer.apple.com/documentation/corebluetooth/cberror/code let errorCode = (e as NSError).code if errorCode == 6 { // CBError.Code.connectionTimeout The connection has timed out unexpectedly. // Happens when device is manually reset / powered off lastConnectionError = "๐Ÿšจ" + String.localizedStringWithFormat("%@ The app will automatically reconnect to the preferred radio if it comes back in range.".localized, e.localizedDescription) Logger.services.error("๐Ÿšจ [BLE] Disconnected: \(peripheral.name ?? "Unknown".localized, privacy: .public) Error Code: \(errorCode, privacy: .public) Error: \(e.localizedDescription, privacy: .public)") } 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. if UserDefaults.preferredPeripheralId == peripheral.identifier.uuidString { manager.notifications = [ Notification( id: (peripheral.identifier.uuidString), title: "Radio Disconnected".localized, subtitle: "\(peripheral.name ?? "unknown".localized)", content: e.localizedDescription, target: "bluetooth", path: "meshtastic:///bluetooth" ) ] manager.schedule() } lastConnectionError = "๐Ÿšจ \("The specified device has disconnected from us".localized)" Logger.services.error("๐Ÿšจ [BLE] Disconnected: \(peripheral.name ?? "Unknown".localized, privacy: .public) Error Code: \(errorCode, privacy: .public) Error: \(e.localizedDescription, privacy: .public)") } 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 = "๐Ÿšจ " + String.localizedStringWithFormat("%@ This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re-connecting to the radio.".localized, e.localizedDescription) Logger.services.error("๐Ÿšจ [BLE] Disconnected: \(peripheral.name ?? "Unknown".localized) Error Code: \(errorCode, privacy: .public) Error: \(self.lastConnectionError, privacy: .public)") } else { if UserDefaults.preferredPeripheralId == peripheral.identifier.uuidString { manager.notifications = [ Notification( id: (peripheral.identifier.uuidString), title: "Radio Disconnected".localized, subtitle: "\(peripheral.name ?? "unknown".localized)", content: e.localizedDescription, target: "bluetooth", path: "meshtastic:///bluetooth" ) ] manager.schedule() } lastConnectionError = "๐Ÿšจ \(e.localizedDescription)" Logger.services.error("๐Ÿšจ [BLE] Disconnected: \(peripheral.name ?? "Unknown".localized, privacy: .public) Error Code: \(errorCode, privacy: .public) Error: \(e.localizedDescription, privacy: .public)") } } else { // Disconnected without error which indicates user intent to disconnect // Happens when swiping to disconnect Logger.services.info("โ„น๏ธ [BLE] Disconnected: \(peripheral.name ?? "Unknown".localized, privacy: .public): \(String(describing: "User Initiated Disconnect".localized), privacy: .public)") } // Start a scan so the disconnected peripheral is moved to the peripherals[] if it is awake self.startScanning() } // MARK: Peripheral Services functions func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { if let error { Logger.services.error("๐Ÿšซ [BLE] Discover Services error \(error.localizedDescription, privacy: .public)") } guard let services = peripheral.services else { return } for service in services where service.uuid == meshtasticServiceCBUUID { peripheral.discoverCharacteristics([TORADIO_UUID, FROMRADIO_UUID, FROMNUM_UUID, LEGACY_LOGRADIO_UUID, LOGRADIO_UUID], for: service) Logger.services.info("โœ… [BLE] Service for Meshtastic discovered by \(peripheral.name ?? "Unknown", privacy: .public)") } } // MARK: Discover Characteristics Event func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { if let error { Logger.services.error("๐Ÿšซ [BLE] Discover Characteristics error for \(peripheral.name ?? "Unknown", privacy: .public) \(error.localizedDescription, privacy: .public) disconnecting device") // Try and stop crashes when this error occurs disconnectPeripheral() return } guard let characteristics = service.characteristics else { return } for characteristic in characteristics { switch characteristic.uuid { case TORADIO_UUID: Logger.services.info("โœ… [BLE] did discover TORADIO characteristic for Meshtastic by \(peripheral.name ?? "Unknown", privacy: .public)") TORADIO_characteristic = characteristic case FROMRADIO_UUID: Logger.services.info("โœ… [BLE] did discover FROMRADIO characteristic for Meshtastic by \(peripheral.name ?? "Unknown", privacy: .public)") FROMRADIO_characteristic = characteristic peripheral.readValue(for: FROMRADIO_characteristic) case FROMNUM_UUID: Logger.services.info("โœ… [BLE] did discover FROMNUM (Notify) characteristic for Meshtastic by \(peripheral.name ?? "Unknown", privacy: .public)") FROMNUM_characteristic = characteristic peripheral.setNotifyValue(true, for: characteristic) case LEGACY_LOGRADIO_UUID: Logger.services.info("โœ… [BLE] did discover legacy LOGRADIO (Notify) characteristic for Meshtastic by \(peripheral.name ?? "Unknown", privacy: .public)") LEGACY_LOGRADIO_characteristic = characteristic peripheral.setNotifyValue(true, for: characteristic) case LOGRADIO_UUID: Logger.services.info("โœ… [BLE] did discover LOGRADIO (Notify) characteristic for Meshtastic by \(peripheral.name ?? "Unknown", privacy: .public)") LOGRADIO_characteristic = characteristic peripheral.setNotifyValue(true, for: characteristic) default: break } } if ![FROMNUM_characteristic, TORADIO_characteristic].contains(nil) { if mqttProxyConnected { mqttManager.mqttClientProxy?.disconnect() } sendWantConfig() } } // MARK: MqttClientProxyManagerDelegate Methods func onMqttConnected() { mqttProxyConnected = true mqttError = "" Logger.services.info("๐Ÿ“ฒ [MQTT Client Proxy] onMqttConnected now subscribing to \(self.mqttManager.topic, privacy: .public).") mqttManager.mqttClientProxy?.subscribe(mqttManager.topic) } func onMqttDisconnected() { mqttProxyConnected = false Logger.services.info("๐Ÿ“ฒ MQTT Disconnected") } func onMqttMessageReceived(message: CocoaMQTTMessage) { if message.topic.contains("/stat/") { return } var proxyMessage = MqttClientProxyMessage() proxyMessage.topic = message.topic proxyMessage.data = Data(message.payload) proxyMessage.retained = message.retained var toRadio: ToRadio! toRadio = ToRadio() toRadio.mqttClientProxyMessage = proxyMessage guard let binaryData: Data = try? toRadio.serializedData() else { return } if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) } } func onMqttError(message: String) { mqttProxyConnected = false mqttError = message Logger.services.info("๐Ÿ“ฒ [MQTT Client Proxy] onMqttError: \(message, privacy: .public)") } // MARK: Protobuf Methods func requestDeviceMetadata(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32, context: NSManagedObjectContext) -> Int64 { guard connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected else { return 0 } var adminPacket = AdminMessage() adminPacket.getDeviceMetadataRequest = true var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var success = false guard connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected else { return success } let fromNodeNum = connectedPeripheral.num let routePacket = RouteDiscovery() var meshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. 0 { let myInfo = myInfoPacket(myInfo: decodedInfo.myInfo, peripheralId: self.connectedPeripheral.id, context: context) if myInfo != nil { UserDefaults.preferredPeripheralNum = Int(myInfo?.myNodeNum ?? 0) connectedPeripheral.num = myInfo?.myNodeNum ?? 0 connectedPeripheral.name = myInfo?.bleName ?? "unknown".localized connectedPeripheral.longName = myInfo?.bleName ?? "unknown".localized let newConnection = Int64(UserDefaults.preferredPeripheralNum) != Int64(decodedInfo.myInfo.myNodeNum) if newConnection { // Onboard a new device connection here } } tryClearExistingChannels() } // NodeInfo if decodedInfo.nodeInfo.num > 0 { nowKnown = true if let nodeInfo = nodeInfoPacket(nodeInfo: decodedInfo.nodeInfo, channel: decodedInfo.packet.channel, context: context) { if self.connectedPeripheral != nil && self.connectedPeripheral.num == nodeInfo.num { if nodeInfo.user != nil { connectedPeripheral.shortName = nodeInfo.user?.shortName ?? "?" connectedPeripheral.longName = nodeInfo.user?.longName ?? "unknown".localized } } } } // Channels if decodedInfo.channel.isInitialized && connectedPeripheral != nil { nowKnown = true channelPacket(channel: decodedInfo.channel, fromNum: Int64(truncatingIfNeeded: connectedPeripheral.num), context: context) } // Config if decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil { nowKnown = true localConfig(config: decodedInfo.config, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral.longName) } // Module Config if decodedInfo.moduleConfig.isInitialized && !invalidVersion && self.connectedPeripheral?.num != 0 { nowKnown = true moduleConfig(config: decodedInfo.moduleConfig, context: context, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral?.num ?? 0), nodeLongName: self.connectedPeripheral.longName) if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) { if decodedInfo.moduleConfig.cannedMessage.enabled { _ = self.getCannedMessageModuleMessages(destNum: self.connectedPeripheral.num, wantResponse: true) } } } // Device Metadata if decodedInfo.metadata.firmwareVersion.count > 0 && !invalidVersion { nowKnown = true deviceMetadataPacket(metadata: decodedInfo.metadata, fromNum: connectedPeripheral.num, context: context) connectedPeripheral.firmwareVersion = decodedInfo.metadata.firmwareVersion let lastDotIndex = decodedInfo.metadata.firmwareVersion.lastIndex(of: ".") if lastDotIndex == nil { invalidVersion = true connectedVersion = "0.0.0" } else { let version = decodedInfo.metadata.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset: 6, in: decodedInfo.metadata.firmwareVersion))] nowKnown = true connectedVersion = String(version.dropLast()) UserDefaults.firmwareVersion = connectedVersion } let supportedVersion = connectedVersion == "0.0.0" || self.minimumVersion.compare(connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(connectedVersion, options: .numeric) == .orderedSame if !supportedVersion { invalidVersion = true lastConnectionError = "๐Ÿšจ" + "update.firmware".localized return } } // Log any other unknownApp calls if !nowKnown { Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for Unknown App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") } case .textMessageApp, .detectionSensorApp: textMessageAppPacket( packet: decodedInfo.packet, wantRangeTestPackets: wantRangeTestPackets, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context, appState: appState ) case .alertApp: textMessageAppPacket( packet: decodedInfo.packet, wantRangeTestPackets: wantRangeTestPackets, critical: true, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context, appState: appState ) case .remoteHardwareApp: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for Remote Hardware App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") case .positionApp: upsertPositionPacket(packet: decodedInfo.packet, context: context) case .waypointApp: waypointPacket(packet: decodedInfo.packet, context: context) case .nodeinfoApp: if !invalidVersion { upsertNodeInfoPacket(packet: decodedInfo.packet, context: context) } case .routingApp: if !invalidVersion { routingPacket(packet: decodedInfo.packet, connectedNodeNum: self.connectedPeripheral.num, context: context) } case .adminApp: adminAppPacket(packet: decodedInfo.packet, context: context) case .replyApp: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for Reply App handling as a text message") textMessageAppPacket( packet: decodedInfo.packet, wantRangeTestPackets: wantRangeTestPackets, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context, appState: appState ) case .ipTunnelApp: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for IP Tunnel App UNHANDLED UNHANDLED") case .serialApp: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for Serial App UNHANDLED UNHANDLED") case .storeForwardApp: storeAndForwardPacket(packet: decodedInfo.packet, connectedNodeNum: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context) case .rangeTestApp: if wantRangeTestPackets { textMessageAppPacket( packet: decodedInfo.packet, wantRangeTestPackets: true, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context, appState: appState ) } else { Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for Range Test App Range testing is disabled.") } case .telemetryApp: if !invalidVersion { telemetryPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context) } case .textMessageCompressedApp: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for Text Message Compressed App UNHANDLED") case .zpsApp: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for Zero Positioning System App UNHANDLED") case .privateApp: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for Private App UNHANDLED UNHANDLED") case .atakForwarder: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for ATAK Forwarder App UNHANDLED UNHANDLED") case .simulatorApp: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for Simulator App UNHANDLED UNHANDLED") case .audioApp: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for Audio App UNHANDLED UNHANDLED") case .tracerouteApp: if let routingMessage = try? RouteDiscovery(serializedBytes: decodedInfo.packet.decoded.payload) { let traceRoute = getTraceRoute(id: Int64(decodedInfo.packet.decoded.requestID), context: context) traceRoute?.response = true guard let connectedNode = getNodeInfo(id: Int64(connectedPeripheral.num), context: context) else { return } var hopNodes: [TraceRouteHopEntity] = [] let connectedHop = TraceRouteHopEntity(context: context) connectedHop.time = Date() connectedHop.num = connectedPeripheral.num connectedHop.name = connectedNode.user?.longName ?? "???" // If nil, set to unknown, INT8_MIN (-128) then divide by 4 connectedHop.snr = Float(routingMessage.snrBack.last ?? -128) / 4 if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { connectedHop.altitude = mostRecent.altitude connectedHop.latitudeI = mostRecent.latitudeI connectedHop.longitudeI = mostRecent.longitudeI traceRoute?.hasPositions = true } var routeString = "\(connectedNode.user?.longName ?? "???") --> " hopNodes.append(connectedHop) traceRoute?.hopsTowards = Int32(routingMessage.route.count) for (index, node) in routingMessage.route.enumerated() { var hopNode = getNodeInfo(id: Int64(node), context: context) if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { hopNode = createNodeInfo(num: Int64(node), context: context) } let traceRouteHop = TraceRouteHopEntity(context: context) traceRouteHop.time = Date() if routingMessage.snrTowards.count >= index + 1 { traceRouteHop.snr = Float(routingMessage.snrTowards[index]) / 4 } else { // If no snr in route, set unknown traceRouteHop.snr = -32 } if let hn = hopNode, hn.hasPositions { if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { traceRouteHop.altitude = mostRecent.altitude traceRouteHop.latitudeI = mostRecent.latitudeI traceRouteHop.longitudeI = mostRecent.longitudeI traceRoute?.hasPositions = true } } traceRouteHop.num = hopNode?.num ?? 0 if hopNode != nil { if decodedInfo.packet.rxTime > 0 { hopNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.packet.rxTime))) } } hopNodes.append(traceRouteHop) let hopName = hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized)) let mqttLabel = hopNode?.viaMqtt ?? false ? "MQTT " : "" let snrLabel = (traceRouteHop.snr != -32) ? String(traceRouteHop.snr) : "unknown ".localized routeString += "\(hopName) \(mqttLabel)(\(snrLabel)dB) --> " } let destinationHop = TraceRouteHopEntity(context: context) destinationHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized destinationHop.time = Date() // If nil, set to unknown, INT8_MIN (-128) then divide by 4 destinationHop.snr = Float(routingMessage.snrTowards.last ?? -128) / 4 destinationHop.num = traceRoute?.node?.num ?? 0 if let mostRecent = traceRoute?.node?.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { destinationHop.altitude = mostRecent.altitude destinationHop.latitudeI = mostRecent.latitudeI destinationHop.longitudeI = mostRecent.longitudeI traceRoute?.hasPositions = true } hopNodes.append(destinationHop) /// Add the destination node to the end of the route towards string and the beginning of the route back string routeString += "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) (\(destinationHop.snr != -32 ? String(destinationHop.snr) : "unknown ".localized)dB)" traceRoute?.routeText = routeString // Default to -1 only fill in if routeBack is valid below traceRoute?.hopsBack = -1 // Only if hopStart is set and there is an SNR entry if decodedInfo.packet.hopStart > 0 && routingMessage.snrBack.count > 0 { traceRoute?.hopsBack = Int32(routingMessage.routeBack.count) var routeBackString = "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) --> " for (index, node) in routingMessage.routeBack.enumerated() { var hopNode = getNodeInfo(id: Int64(node), context: context) if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 { hopNode = createNodeInfo(num: Int64(node), context: context) } let traceRouteHop = TraceRouteHopEntity(context: context) traceRouteHop.time = Date() traceRouteHop.back = true if routingMessage.snrBack.count >= index + 1 { traceRouteHop.snr = Float(routingMessage.snrBack[index]) / 4 } else { // If no snr in route, set to unknown traceRouteHop.snr = -32 } if let hn = hopNode, hn.hasPositions { if let mostRecent = hn.positions?.lastObject as? PositionEntity, mostRecent.time! >= Calendar.current.date(byAdding: .hour, value: -24, to: Date())! { traceRouteHop.altitude = mostRecent.altitude traceRouteHop.latitudeI = mostRecent.latitudeI traceRouteHop.longitudeI = mostRecent.longitudeI traceRoute?.hasPositions = true } } traceRouteHop.num = hopNode?.num ?? 0 if hopNode != nil { if decodedInfo.packet.rxTime > 0 { hopNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.packet.rxTime))) } } hopNodes.append(traceRouteHop) let hopName = hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized)) let mqttLabel = hopNode?.viaMqtt ?? false ? "MQTT " : "" let snrLabel = (traceRouteHop.snr != -32) ? String(traceRouteHop.snr) : "unknown ".localized routeBackString += "\(hopName) \(mqttLabel)(\(snrLabel)dB) --> " } // If nil, set to unknown, INT8_MIN (-128) then divide by 4 let snrBackLast = Float(routingMessage.snrBack.last ?? -128) / 4 routeBackString += "\(connectedNode.user?.longName ?? String(connectedNode.num.toHex())) (\(snrBackLast != -32 ? String(snrBackLast) : "unknown ".localized)dB)" traceRoute?.routeBackText = routeBackString } traceRoute?.hops = NSOrderedSet(array: hopNodes) traceRoute?.time = Date() if let tr = traceRoute { let manager = LocalNotificationManager() manager.notifications = [ Notification( id: (UUID().uuidString), title: "Traceroute Complete", subtitle: "TR received back from \(destinationHop.name ?? "unknown")", content: "Hops from: \(tr.hopsTowards), Hops back: \(tr.hopsBack)\n\(tr.routeText ?? "unknown".localized)\n\(tr.routeBackText ?? "unknown".localized)", target: "nodes", path: "meshtastic:///nodes?nodenum=\(connectedNode.user?.num ?? 0)" ) ] manager.schedule() } do { try context.save() Logger.data.info("๐Ÿ’พ Saved Trace Route") } catch { context.rollback() let nsError = error as NSError Logger.data.error("Error Updating Core Data TraceRouteHop: \(nsError, privacy: .public)") } let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.route %@".localized, routeString) Logger.mesh.info("๐Ÿชง \(logString, privacy: .public)") } case .neighborinfoApp: if let neighborInfo = try? NeighborInfo(serializedBytes: decodedInfo.packet.decoded.payload) { Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for Neighbor Info App UNHANDLED \((try? neighborInfo.jsonString()) ?? "JSON Decode Failure", privacy: .public)") } case .paxcounterApp: paxCounterPacket(packet: decodedInfo.packet, context: context) case .mapReportApp: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received Map Report App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") case .UNRECOGNIZED: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received UNRECOGNIZED App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") case .max: Logger.services.info("MAX PORT NUM OF 511") case .atakPlugin: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for ATAK Plugin App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") case .powerstressApp: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for Power Stress App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") case .reticulumTunnelApp: Logger.mesh.info("๐Ÿ•ธ๏ธ MESH PACKET received for Reticulum Tunnel App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)") } if decodedInfo.configCompleteID != 0 && decodedInfo.configCompleteID == configNonce { invalidVersion = false lastConnectionError = "" isSubscribed = true Logger.mesh.info("๐Ÿคœ [BLE] Want Config Complete. ID:\(decodedInfo.configCompleteID, privacy: .public)") if sendTime() { } peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected }) // Config conplete returns so we don't read the characteristic again /// MQTT Client Proxy and RangeTest and Store and Forward interest if connectedPeripheral.num > 0 { let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(connectedPeripheral.num)) do { let fetchedNodeInfo = try context.fetch(fetchNodeInfoRequest) if fetchedNodeInfo.count == 1 { // Subscribe to Mqtt Client Proxy if enabled if fetchedNodeInfo[0].mqttConfig != nil && fetchedNodeInfo[0].mqttConfig?.enabled ?? false && fetchedNodeInfo[0].mqttConfig?.proxyToClientEnabled ?? false { mqttManager.connectFromConfigSettings(node: fetchedNodeInfo[0]) } else { if mqttProxyConnected { mqttManager.mqttClientProxy?.disconnect() } } // Set initial unread message badge states appState.unreadChannelMessages = fetchedNodeInfo[0].myInfo?.unreadMessages ?? 0 appState.unreadDirectMessages = fetchedNodeInfo[0].user?.unreadMessages ?? 0 } if fetchedNodeInfo.count == 1 && fetchedNodeInfo[0].rangeTestConfig?.enabled == true { wantRangeTestPackets = true } if fetchedNodeInfo.count == 1 && fetchedNodeInfo[0].storeForwardConfig?.enabled == true { wantStoreAndForwardPackets = true } } catch { Logger.data.error("Failed to find a node info for the connected node \(error.localizedDescription, privacy: .public)") } } // 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 UserDefaults.provideLocation { let interval = UserDefaults.provideLocationInterval >= 10 ? UserDefaults.provideLocationInterval : 30 positionTimer = Timer.scheduledTimer(timeInterval: TimeInterval(interval), target: self, selector: #selector(positionTimerFired), userInfo: context, repeats: true) if positionTimer != nil { RunLoop.current.add(positionTimer!, forMode: .common) } } return } case FROMNUM_UUID: Logger.services.info("๐Ÿ—ž๏ธ [BLE] (Notify) characteristic value will be read next") default: Logger.services.error("๐Ÿšซ Unhandled Characteristic UUID: \(characteristic.uuid, privacy: .public)") } if FROMRADIO_characteristic != nil { // Either Read the config complete value or from num notify value peripheral.readValue(for: FROMRADIO_characteristic) } } public func sendMessage(message: String, toUserNum: Int64, channel: Int32, 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.preferredPeripheralId as String }).first if preferredPeripheral != nil && preferredPeripheral?.peripheral != nil { connectTo(peripheral: preferredPeripheral!.peripheral) } let nodeName = connectedPeripheral?.peripheral.name ?? "unknown".localized let logString = String.localizedStringWithFormat("mesh.log.textmessage.send.failed %@".localized, nodeName) Logger.mesh.info("๐Ÿšซ \(logString, privacy: .public)") success = false } else if message.count < 1 { // Don't send an empty message Logger.mesh.info("๐Ÿšซ Don't Send an Empty Message") success = false } else { let fromUserNum: Int64 = self.connectedPeripheral.num let messageUsers = UserEntity.fetchRequest() messageUsers.predicate = NSPredicate(format: "num IN %@", [fromUserNum, Int64(toUserNum)]) do { let fetchedUsers = try context.fetch(messageUsers) if fetchedUsers.isEmpty { Logger.data.error("๐Ÿšซ 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).. 0 { newMessage.toUser = fetchedUsers.first(where: { $0.num == toUserNum }) newMessage.toUser?.lastMessage = Date() if newMessage.toUser?.pkiEncrypted ?? false { newMessage.publicKey = newMessage.toUser?.publicKey newMessage.pkiEncrypted = true } } newMessage.fromUser = fetchedUsers.first(where: { $0.num == fromUserNum }) newMessage.isEmoji = isEmoji newMessage.admin = false newMessage.channel = channel if replyID > 0 { newMessage.replyID = replyID } newMessage.messagePayload = message newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: message) newMessage.read = true let dataType = PortNum.textMessageApp var messageQuotesReplaced = message.replacingOccurrences(of: "โ€™", with: "'") messageQuotesReplaced = message.replacingOccurrences(of: "โ€", with: "\"") let payloadData: Data = messageQuotesReplaced.data(using: String.Encoding.utf8)! var dataMessage = DataMessage() dataMessage.payload = payloadData dataMessage.portnum = dataType var meshPacket = MeshPacket() if newMessage.toUser?.pkiEncrypted ?? false { meshPacket.pkiEncrypted = true meshPacket.publicKey = newMessage.toUser?.publicKey ?? Data() } meshPacket.id = UInt32(newMessage.messageId) if toUserNum > 0 { meshPacket.to = UInt32(toUserNum) } else { meshPacket.to = Constants.maximumNodeNum } meshPacket.channel = UInt32(channel) 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 guard let binaryData: Data = try? toRadio.serializedData() else { return false } if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) let logString = String.localizedStringWithFormat("mesh.log.textmessage.sent %@ %@ %@".localized, String(newMessage.messageId), fromUserNum.toHex(), toUserNum.toHex()) Logger.mesh.info("๐Ÿ’ฌ \(logString, privacy: .public)") do { try context.save() Logger.data.info("๐Ÿ’พ Saved a new sent message from \(self.connectedPeripheral.num.toHex(), privacy: .public) to \(toUserNum.toHex(), privacy: .public)") success = true } catch { context.rollback() let nsError = error as NSError Logger.data.error("Unresolved Core Data error in Send Message Function your database is corrupted running a node db reset should clean up the data. Error: \(nsError, privacy: .public)") } } } } catch { Logger.data.error("๐Ÿ’ฅ Send message failure \(self.connectedPeripheral.num.toHex(), privacy: .public) to \(toUserNum.toHex(), privacy: .public)") } } return success } public func sendWaypoint(waypoint: Waypoint) -> Bool { if waypoint.latitudeI == 373346000 && waypoint.longitudeI == -1220090000 { return false } var success = false let fromNodeNum = UInt32(connectedPeripheral.num) var meshPacket = MeshPacket() meshPacket.to = Constants.maximumNodeNum meshPacket.from = fromNodeNum meshPacket.wantAck = true var dataMessage = DataMessage() do { dataMessage.payload = try waypoint.serializedData() } catch { // Could not serialiaze the payload return false } dataMessage.portnum = PortNum.waypointApp meshPacket.decoded = dataMessage var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket guard let binaryData: Data = try? toRadio.serializedData() else { return false } let logString = String.localizedStringWithFormat("mesh.log.waypoint.sent %@".localized, String(fromNodeNum)) Logger.mesh.info("๐Ÿ“ \(logString, privacy: .public)") if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) success = true let wayPointEntity = getWaypoint(id: Int64(waypoint.id), context: context) wayPointEntity.id = Int64(waypoint.id) wayPointEntity.name = waypoint.name.count >= 1 ? waypoint.name : "Dropped Pin" wayPointEntity.longDescription = waypoint.description_p wayPointEntity.icon = Int64(waypoint.icon) wayPointEntity.latitudeI = waypoint.latitudeI wayPointEntity.longitudeI = waypoint.longitudeI if waypoint.expire > 1 { wayPointEntity.expire = Date.init(timeIntervalSince1970: Double(waypoint.expire)) } else { wayPointEntity.expire = nil } if waypoint.lockedTo > 0 { wayPointEntity.locked = Int64(waypoint.lockedTo) } else { wayPointEntity.locked = 0 } if wayPointEntity.created == nil { wayPointEntity.created = Date() } else { wayPointEntity.lastUpdated = Date() } do { try context.save() Logger.data.info("๐Ÿ’พ Updated Waypoint from Waypoint App Packet From: \(fromNodeNum.toHex(), privacy: .public)") } catch { context.rollback() let nsError = error as NSError Logger.data.error("Error Saving NodeInfoEntity from WAYPOINT_APP \(nsError, privacy: .public)") } } return success } @MainActor public func getPositionFromPhoneGPS(destNum: Int64, fixedPosition: Bool) -> Position? { var positionPacket = Position() guard let lastLocation = LocationsHandler.shared.locationsArray.last else { return nil } if lastLocation == CLLocation(latitude: 0, longitude: 0) { return nil } positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7) positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7) let timestamp = lastLocation.timestamp positionPacket.time = UInt32(timestamp.timeIntervalSince1970) positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970) positionPacket.altitude = Int32(lastLocation.altitude) positionPacket.satsInView = UInt32(LocationsHandler.satsInView) let currentSpeed = lastLocation.speed if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { positionPacket.groundSpeed = UInt32(currentSpeed) } let currentHeading = lastLocation.course if (currentHeading > 0 && currentHeading <= 360) && (!currentHeading.isNaN || !currentHeading.isInfinite) { positionPacket.groundTrack = UInt32(currentHeading) } /// Set location source for time if !fixedPosition { /// From GPS treat time as good positionPacket.locationSource = Position.LocSource.locExternal } else { /// From GPS, but time can be old and have drifted positionPacket.locationSource = Position.LocSource.locManual } return positionPacket } @MainActor public func setFixedPosition(fromUser: UserEntity, channel: Int32) -> Bool { var adminPacket = AdminMessage() guard let positionPacket = getPositionFromPhoneGPS(destNum: fromUser.num, fixedPosition: true) else { return false } adminPacket.setFixedPosition = positionPacket var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(fromUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.removeFixedPosition = true var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(fromUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { let fromNodeNum = connectedPeripheral.num guard let positionPacket = getPositionFromPhoneGPS(destNum: destNum, fixedPosition: false) else { Logger.services.error("Unable to get position data from device GPS to send to node") return false } var meshPacket = MeshPacket() meshPacket.to = UInt32(destNum) meshPacket.channel = UInt32(channel) meshPacket.from = UInt32(fromNodeNum) var dataMessage = DataMessage() if let serializedData: Data = try? positionPacket.serializedData() { dataMessage.payload = serializedData dataMessage.portnum = PortNum.positionApp dataMessage.wantResponse = wantResponse meshPacket.decoded = dataMessage } else { Logger.services.error("Failed to serialize position packet data") return false } var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket guard let binaryData: Data = try? toRadio.serializedData() else { Logger.services.error("Failed to serialize position packet") return false } if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) let logString = String.localizedStringWithFormat("mesh.log.sharelocation %@".localized, String(fromNodeNum)) Logger.services.debug("๐Ÿ“ \(logString, privacy: .public)") return true } else { Logger.services.error("Device no longer connected. Unable to send position information.") return false } } @MainActor @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 UserDefaults.provideLocation { _ = sendPosition(channel: 0, destNum: connectedPeripheral.num, wantResponse: false) } } } public func sendTime() -> Bool { var adminPacket = AdminMessage() adminPacket.setTimeOnly = UInt32(Date().timeIntervalSince1970) var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(self.connectedPeripheral.num) meshPacket.from = UInt32(self.connectedPeripheral.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.shutdownSeconds = 5 if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.rebootSeconds = 5 if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.rebootOtaSeconds = 5 if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.enterDfuModeRequest = true if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.factoryResetConfig = 5 if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.nodedbReset = 5 if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = 0 // UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. 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) success = true } } else if connectedPeripheral != nil && isSubscribed { success = true } return success } public func getChannel(channel: Channel, fromUser: UserEntity, toUser: UserEntity) -> Int64 { var adminPacket = AdminMessage() adminPacket.getChannelRequest = UInt32(channel.index + 1) var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setChannel = channel var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { if isConnected { var i: Int32 = 0 var myInfo: MyInfoEntity // Before we get started delete the existing channels from the myNodeInfo if !addChannels { tryClearExistingChannels() } let decodedString = base64UrlString.base64urlToBase64() if let decodedData = Data(base64Encoded: decodedString) { do { let channelSet: ChannelSet = try ChannelSet(serializedBytes: decodedData) for cs in channelSet.settings { if addChannels { // We are trying to add a channel so lets get the last index let fetchMyInfoRequest = MyInfoEntity.fetchRequest() fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedPeripheral.num)) do { let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) if fetchedMyInfo.count == 1 { i = Int32(fetchedMyInfo[0].channels?.count ?? -1) myInfo = fetchedMyInfo[0] // Bail out if the index is negative or bigger than our max of 8 if i < 0 || i > 8 { return false } // Bail out if there are no channels or if the same channel name already exists guard let mutableChannels = myInfo.channels!.mutableCopy() as? NSMutableOrderedSet else { return false } if mutableChannels.first(where: {($0 as AnyObject).name == cs.name }) is ChannelEntity { return false } } } catch { Logger.data.error("Failed to find a node MyInfo to save these channels to: \(error.localizedDescription, privacy: .public)") } } var chan = Channel() if i == 0 { chan.role = Channel.Role.primary } else { chan.role = Channel.Role.secondary } chan.settings = cs chan.index = i i += 1 var adminPacket = AdminMessage() adminPacket.setChannel = chan var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(connectedPeripheral.num) meshPacket.from = UInt32(connectedPeripheral.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setOwner = config if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.removeByNodenum = UInt32(node.num) var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(connectedNodeNum) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.setFavoriteNode = UInt32(node.num) var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(connectedNodeNum) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.removeFavoriteNode = UInt32(node.num) var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(connectedNodeNum) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.setIgnoredNode = UInt32(node.num) var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(connectedNodeNum) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.removeIgnoredNode = UInt32(node.num) var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(connectedNodeNum) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setHamMode = ham if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.bluetooth = config if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.device = config if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.display = config if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) if adminIndex > 0 { meshPacket.channel = UInt32(adminIndex) } meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.lora = config if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.position = config if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.power = config var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.network = config if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.security = config if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.ambientLighting = config if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.cannedMessage = config if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setCannedMessageModuleMessages = messages if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.detectionSensor = config if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.externalNotification = config if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.paxcounter = config if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.channel = UInt32(adminIndex) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setRingtoneMessage = ringtone if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.mqtt = config if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.rangeTest = config if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.serial = config if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.storeForward = config if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.telemetry = config if fromUser != toUser { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getChannelRequest = channelIndex var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getCannedMessageModuleMessagesRequest = true var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(destNum) meshPacket.from = UInt32(connectedPeripheral.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.bluetoothConfig if UserDefaults.enableAdministration { adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.deviceConfig var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.displayConfig var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.loraConfig var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.networkConfig var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.positionConfig var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.powerConfig var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getConfigRequest = AdminMessage.ConfigType.securityConfig var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.ambientlightingConfig var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.cannedmsgConfig var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.extnotifConfig var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.paxcounterConfig var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getRingtoneRequest = true var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.rangetestConfig var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.mqttConfig var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.detectionsensorConfig var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.serialConfig var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.storeforwardConfig var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.telemetryConfig adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var toRadio: ToRadio! toRadio = ToRadio() toRadio.packet = meshPacket guard let binaryData: Data = try? toRadio.serializedData() else { return false } if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) Logger.mesh.debug("\(adminDescription, privacy: .public)") return true } return false } public func requestStoreAndForwardClientHistory(fromUser: UserEntity, toUser: UserEntity) -> Bool { /// send a request for ClientHistory with a time period matching the heartbeat var sfPacket = StoreAndForward() sfPacket.rr = StoreAndForward.RequestResponse.clientHistory sfPacket.history.window = UInt32(toUser.userNode?.storeForwardConfig?.historyReturnWindow ?? 120) sfPacket.history.lastRequest = UInt32(toUser.userNode?.storeForwardConfig?.lastRequest ?? 0) var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..