mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
393 lines
17 KiB
Swift
393 lines
17 KiB
Swift
import Foundation
|
|
import CoreData
|
|
import CoreBluetooth
|
|
import SwiftUI
|
|
|
|
//---------------------------------------------------------------------------------------
|
|
// Meshtastic BLE Device Manager
|
|
//---------------------------------------------------------------------------------------
|
|
class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate {
|
|
|
|
// Data
|
|
@ObservedObject private var meshData : MeshData
|
|
@ObservedObject private var messageData : MessageData
|
|
private var centralManager: CBCentralManager!
|
|
@Published var connectedPeripheral: CBPeripheral!
|
|
@Published var peripheralArray = [CBPeripheral]()
|
|
@Published var connectedNodeInfo: Peripheral!
|
|
@Published var connectedNode: NodeInfoModel!
|
|
//private var rssiArray = [NSNumber]()
|
|
private var timer = Timer()
|
|
@Published var isSwitchedOn = false
|
|
@Published var peripherals = [Peripheral]()
|
|
|
|
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")
|
|
|
|
override init() {
|
|
self.meshData = MeshData()
|
|
self.messageData = MessageData()
|
|
super.init()
|
|
centralManager = CBCentralManager(delegate: self, queue: nil)
|
|
centralManager.delegate = self
|
|
|
|
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------
|
|
// Check for Bluetooth Connectivity
|
|
//---------------------------------------------------------------------------------------
|
|
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
if central.state == .poweredOn {
|
|
isSwitchedOn = true
|
|
}
|
|
else {
|
|
isSwitchedOn = false
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------
|
|
// Scan for nearby BLE devices using the Meshtastic BLE service ID
|
|
//---------------------------------------------------------------------------------------
|
|
func startScanning() {
|
|
// Remove Existing Data
|
|
peripherals.removeAll()
|
|
peripheralArray.removeAll()
|
|
//rssiArray.removeAll()
|
|
// Start Scanning
|
|
print("Start Scanning")
|
|
centralManager.scanForPeripherals(withServices: [meshtasticServiceCBUUID])
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------
|
|
// Stop Scanning For BLE Devices
|
|
//---------------------------------------------------------------------------------------
|
|
func stopScanning() {
|
|
print("Stop Scanning")
|
|
self.centralManager.stopScan()
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------
|
|
// Connect to a Device via UUID
|
|
//---------------------------------------------------------------------------------------
|
|
func connectToDevice(id: String) {
|
|
connectedPeripheral = peripheralArray.filter({ $0.identifier.uuidString == id }).first
|
|
connectedNodeInfo = Peripheral(id: connectedPeripheral.identifier.uuidString, name: connectedPeripheral.name ?? "Unknown", rssi: 0, myInfo: nil)
|
|
self.centralManager?.connect(connectedPeripheral!)
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------
|
|
// Disconnect Device function
|
|
//---------------------------------------------------------------------------------------
|
|
func disconnectDevice(){
|
|
if connectedPeripheral != nil {
|
|
self.centralManager?.cancelPeripheralConnection(connectedPeripheral!)
|
|
}
|
|
}
|
|
|
|
|
|
//---------------------------------------------------------------------------------------
|
|
// Set Owner function
|
|
//---------------------------------------------------------------------------------------
|
|
public func setOwner(myUser: User)
|
|
{
|
|
var toRadio: ToRadio!
|
|
toRadio = ToRadio()
|
|
//toRadio.setOwner = myUser
|
|
|
|
let binaryData: Data = try! toRadio.serializedData()
|
|
if (self.connectedPeripheral.state == CBPeripheralState.connected)
|
|
{
|
|
connectedPeripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
|
|
//MasterViewController.shared.DebugPrint2View(text: "Owner set to device" + "\n\r")
|
|
}
|
|
else
|
|
{
|
|
connectToDevice(id: self.connectedPeripheral.identifier.uuidString)
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------
|
|
// Discover Peripheral Event
|
|
//---------------------------------------------------------------------------------------
|
|
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
|
|
|
|
print(peripheral)
|
|
if peripheralArray.contains(peripheral) {
|
|
print("Duplicate Found.")
|
|
} else {
|
|
print("Adding peripheral: " + ((peripheral.name != nil) ? peripheral.name! : "(null)"));
|
|
peripheralArray.append(peripheral)
|
|
//rssiArray.append(RSSI)
|
|
}
|
|
|
|
var peripheralName: String!
|
|
peripheralName = peripheral.name
|
|
if peripheral.name == nil {
|
|
if let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String {
|
|
peripheralName = name
|
|
}
|
|
else {
|
|
peripheralName = "Unknown"
|
|
}
|
|
}
|
|
|
|
let newPeripheral = Peripheral(id: peripheral.identifier.uuidString, name: peripheralName, rssi: RSSI.intValue, myInfo: nil)
|
|
//print(newPeripheral)
|
|
peripherals.append(newPeripheral)
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------
|
|
// Connect Peripheral Event
|
|
//---------------------------------------------------------------------------------------
|
|
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
print("Peripheral connected: " + peripheral.name!)
|
|
peripheral.delegate = self
|
|
peripheral.discoverServices(nil)
|
|
self.startScanning()
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------
|
|
// Disconnect Peripheral Event
|
|
//---------------------------------------------------------------------------------------
|
|
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?)
|
|
{
|
|
if let e = error {
|
|
|
|
print("Central disconnected because \(e)")
|
|
// connectToDevice(id: peripheral.identifier.uuidString)
|
|
} else {
|
|
print("Central disconnected! (no error)")
|
|
}
|
|
|
|
if(peripheral.identifier == connectedPeripheral.identifier){
|
|
connectedPeripheral = nil
|
|
connectedNodeInfo = nil
|
|
connectedNode = nil
|
|
}
|
|
print("Peripheral disconnected: " + peripheral.name!)
|
|
self.startScanning()
|
|
}
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------------------------
|
|
// Discover Services Event
|
|
//---------------------------------------------------------------------------------------
|
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
|
|
|
guard let services = peripheral.services else { return }
|
|
|
|
for service in services
|
|
{
|
|
print("Service discovered: " + service.uuid.uuidString)
|
|
|
|
if (service.uuid == meshtasticServiceCBUUID)
|
|
{
|
|
print ("Meshtastic service OK")
|
|
|
|
peripheral.discoverCharacteristics(nil, for: service)
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------
|
|
// Discover Characteristics Event
|
|
//---------------------------------------------------------------------------------------
|
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?)
|
|
{
|
|
guard let characteristics = service.characteristics else { return }
|
|
|
|
for characteristic in characteristics {
|
|
|
|
switch characteristic.uuid
|
|
{
|
|
case TORADIO_UUID:
|
|
print("TORADIO characteristic OK")
|
|
TORADIO_characteristic = characteristic
|
|
var toRadio: ToRadio = ToRadio()
|
|
toRadio.wantConfigID = 32168
|
|
let binaryData: Data = try! toRadio.serializedData()
|
|
peripheral.writeValue(binaryData, for: characteristic, type: .withResponse)
|
|
break
|
|
|
|
case FROMRADIO_UUID:
|
|
print("FROMRADIO characteristic OK")
|
|
FROMRADIO_characteristic = characteristic
|
|
peripheral.readValue(for: FROMRADIO_characteristic)
|
|
break
|
|
|
|
case FROMNUM_UUID:
|
|
print("FROMNUM (Notify) characteristic OK")
|
|
FROMNUM_characteristic = characteristic
|
|
peripheral.setNotifyValue(true, for: characteristic)
|
|
break
|
|
|
|
default:
|
|
break
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------
|
|
// Data Read / Update Characteristic Event
|
|
//---------------------------------------------------------------------------------------
|
|
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?)
|
|
{
|
|
switch characteristic.uuid
|
|
{
|
|
case FROMNUM_UUID:
|
|
peripheral.readValue(for: FROMRADIO_characteristic)
|
|
|
|
case FROMRADIO_UUID:
|
|
if (characteristic.value == nil || characteristic.value!.isEmpty)
|
|
{
|
|
return
|
|
}
|
|
//print(characteristic.value ?? "no value")
|
|
//let byteArray = [UInt8](characteristic.value!)
|
|
//print(characteristic.value?.hexDescription ?? "no value")
|
|
var decodedInfo = FromRadio()
|
|
|
|
decodedInfo = try! FromRadio(serializedData: characteristic.value!)
|
|
//print(decodedInfo)
|
|
|
|
if decodedInfo.myInfo.myNodeNum != 0
|
|
{
|
|
print("Save a myInfo")
|
|
do {
|
|
print(try decodedInfo.myInfo.jsonString())
|
|
|
|
// Create a MyInfoModel
|
|
let myInfoModel = MyInfoModel(
|
|
myNodeNum: decodedInfo.myInfo.myNodeNum,
|
|
hasGps: decodedInfo.myInfo.hasGps_p,
|
|
numBands: decodedInfo.myInfo.numBands,
|
|
maxChannels: decodedInfo.myInfo.maxChannels,
|
|
firmwareVersion: decodedInfo.myInfo.firmwareVersion,
|
|
rebootCount: decodedInfo.myInfo.rebootCount,
|
|
messageTimeoutMsec: decodedInfo.myInfo.messageTimeoutMsec,
|
|
minAppVersion: decodedInfo.myInfo.minAppVersion)
|
|
// Save it to the connected nodeInfo
|
|
connectedNodeInfo.myInfo = myInfoModel
|
|
// Save it to the connected node
|
|
connectedNode = meshData.nodes.first(where: {$0.id == decodedInfo.myInfo.myNodeNum})
|
|
if connectedNode != nil {
|
|
connectedNode.myInfo = myInfoModel
|
|
}
|
|
meshData.save()
|
|
|
|
} catch {
|
|
fatalError("Failed to decode json")
|
|
}
|
|
}
|
|
|
|
if decodedInfo.nodeInfo.num != 0
|
|
{
|
|
print("Save a nodeInfo")
|
|
do {
|
|
|
|
if meshData.nodes.contains(where: {$0.id == decodedInfo.nodeInfo.num}) {
|
|
|
|
let nodeIndex = meshData.nodes.firstIndex(where: { $0.id == decodedInfo.nodeInfo.num })
|
|
meshData.nodes.remove(at: nodeIndex!)
|
|
meshData.save()
|
|
}
|
|
|
|
meshData.nodes.append(
|
|
NodeInfoModel(
|
|
num: decodedInfo.nodeInfo.num,
|
|
user: NodeInfoModel.User(
|
|
id: decodedInfo.nodeInfo.user.id,
|
|
longName: decodedInfo.nodeInfo.user.longName,
|
|
shortName: decodedInfo.nodeInfo.user.shortName,
|
|
//macaddr: decodedInfo.nodeInfo.user.macaddr,
|
|
hwModel: String(describing: decodedInfo.nodeInfo.user.hwModel)
|
|
.uppercased()),
|
|
|
|
position: NodeInfoModel.Position(
|
|
latitudeI: decodedInfo.nodeInfo.position.latitudeI,
|
|
longitudeI: decodedInfo.nodeInfo.position.longitudeI,
|
|
altitude: decodedInfo.nodeInfo.position.altitude,
|
|
batteryLevel: decodedInfo.nodeInfo.position.batteryLevel,
|
|
time: decodedInfo.nodeInfo.position.time),
|
|
|
|
lastHeard: decodedInfo.nodeInfo.lastHeard,
|
|
snr: decodedInfo.nodeInfo.snr)
|
|
)
|
|
meshData.save()
|
|
|
|
print(try decodedInfo.nodeInfo.jsonString())
|
|
} catch {
|
|
fatalError("Failed to decode json")
|
|
}
|
|
}
|
|
|
|
if decodedInfo.packet.id != 0
|
|
{
|
|
|
|
do {
|
|
|
|
if decodedInfo.packet.decoded.portnum == PortNum.textMessageApp {
|
|
if let messageText = String(bytes: decodedInfo.packet.decoded.payload, encoding: .utf8) {
|
|
print(messageText)
|
|
print(try decodedInfo.packet.jsonString())
|
|
|
|
messageData.messages.append(
|
|
MessageModel(messageId: decodedInfo.packet.id, messageTimeStamp: Int64(decodedInfo.packet.rxTime), fromUserId: decodedInfo.packet.from, toUserId: decodedInfo.packet.to, fromUserLongName: "From Long Name ", toUserLongName: "To Long Name", fromUserShortName: "FLN", toUserShortName: "TLN", receivedACK: decodedInfo.packet.decoded.wantResponse, messagePayload: messageText, direction: "IN"))
|
|
messageData.save()
|
|
} else {
|
|
print("not a valid UTF-8 sequence")
|
|
}
|
|
|
|
}
|
|
else if decodedInfo.packet.decoded.portnum == PortNum.nodeinfoApp {
|
|
if let nodeInfoPayload = String(bytes: decodedInfo.packet.decoded.payload, encoding: .utf8) {
|
|
print(nodeInfoPayload)
|
|
} else {
|
|
print("not a valid UTF-8 sequence")
|
|
print(try decodedInfo.packet.jsonString())
|
|
}
|
|
|
|
}
|
|
else if decodedInfo.packet.decoded.portnum == PortNum.positionApp {
|
|
if let nodeInfoPayload = String(bytes: decodedInfo.packet.decoded.payload, encoding: .utf8) {
|
|
print(nodeInfoPayload)
|
|
} else {
|
|
print("not a valid UTF-8 sequence")
|
|
print(try decodedInfo.packet.jsonString())
|
|
}
|
|
}
|
|
else
|
|
{
|
|
print("Save a packet")
|
|
print(try decodedInfo.packet.jsonString())
|
|
}
|
|
|
|
} catch {
|
|
fatalError("Failed to decode json")
|
|
}
|
|
}
|
|
|
|
if decodedInfo.configCompleteID != 0 {
|
|
print(decodedInfo)
|
|
meshData.load()
|
|
messageData.load()
|
|
}
|
|
|
|
default:
|
|
print("Unhandled Characteristic UUID: \(characteristic.uuid)")
|
|
}
|
|
|
|
peripheral.readValue(for: FROMRADIO_characteristic)
|
|
}
|
|
}
|
|
|
|
|