From 22bbd04db5e28105adc6af4f704de01bff417136 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 1 Oct 2021 08:33:11 -0700 Subject: [PATCH] Clean up null values on details pages, add some enhanced bluetooth cleanup --- MeshtasticClient/Helpers/BLEManager.swift | 177 +++++++++++++++--- MeshtasticClient/Model/MessageModel.swift | 6 +- .../Views/Bluetooth/Connect.swift | 2 +- .../Views/Helpers/BatteryIcon.swift | 9 +- .../Views/Helpers/MessageBubble.swift | 15 +- .../Views/Messages/Messages.swift | 11 +- MeshtasticClient/Views/Nodes/NodeDetail.swift | 66 ++++--- MeshtasticClient/Views/Nodes/NodeMap.swift | 2 +- MeshtasticClient/Views/Nodes/NodeRow.swift | 11 +- 9 files changed, 228 insertions(+), 71 deletions(-) diff --git a/MeshtasticClient/Helpers/BLEManager.swift b/MeshtasticClient/Helpers/BLEManager.swift index fe78d529..2797a3bb 100644 --- a/MeshtasticClient/Helpers/BLEManager.swift +++ b/MeshtasticClient/Helpers/BLEManager.swift @@ -19,6 +19,8 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph @Published var lastConnectedNode: String //private var rssiArray = [NSNumber]() private var timer = Timer() + private var broadcastNodeId: UInt32 = 4294967295 + @Published var isSwitchedOn = false @Published var peripherals = [Peripheral]() @@ -51,47 +53,96 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph isSwitchedOn = false } } - - //--------------------------------------------------------------------------------------- - // Scan for nearby BLE devices using the Meshtastic BLE service ID - //--------------------------------------------------------------------------------------- + /* + * 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() + print("Scanning Stopped") } //--------------------------------------------------------------------------------------- // Connect to a Device via UUID //--------------------------------------------------------------------------------------- func connectToDevice(id: String) { + cleanup() connectedPeripheral = peripheralArray.filter({ $0.identifier.uuidString == id }).first + + connectedNodeInfo = Peripheral(id: connectedPeripheral.identifier.uuidString, name: connectedPeripheral.name ?? "Unknown", rssi: 0, myInfo: nil) lastConnectedNode = id self.centralManager?.connect(connectedPeripheral!) } - //--------------------------------------------------------------------------------------- - // Disconnect Device function - //--------------------------------------------------------------------------------------- + /* + * Disconnect Device function + */ func disconnectDevice(){ - if connectedPeripheral != nil { - self.centralManager?.cancelPeripheralConnection(connectedPeripheral!) - } + + cleanup() } + /* + * Send Broadcast Message + */ + public func sendMessage(message: String) -> Bool + { var success = true + if connectedPeripheral == nil || connectedPeripheral!.state != CBPeripheralState.connected { + success = false + } + else { + + let messageModel = MessageModel(messageId: 0, messageTimeStamp: UInt32(Date().timeIntervalSince1970), fromUserId: self.connectedNode.id, toUserId: broadcastNodeId, fromUserLongName: self.connectedNode.user.longName, toUserLongName: "Broadcast", fromUserShortName: self.connectedNode.user.shortName, toUserShortName: "BC", receivedACK: false, messagePayload: message, direction: "OUT") + 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.to = broadcastNodeId + meshPacket.decoded = dataMessage + meshPacket.wantAck = true + + var toRadio: ToRadio! + toRadio = ToRadio() + toRadio.packet = meshPacket + + let binaryData: Data = try! toRadio.serializedData() + if (connectedPeripheral!.state == CBPeripheralState.connected) + { + connectedPeripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) + + messageData.messages.append(messageModel) + messageData.save() + + } + else + { + connectToDevice(id: lastConnectedNode) + } + + } + return success + } //--------------------------------------------------------------------------------------- // Set Owner function @@ -114,12 +165,45 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } } - //--------------------------------------------------------------------------------------- - // Discover Peripheral Event - //--------------------------------------------------------------------------------------- + /* + * Call this when things either go wrong, or you're done with the connection. + * This cancels any subscriptions if there are any, or straight disconnects if not. + * (didUpdateNotificationStateForCharacteristic will cancel the connection if a subscription is involved) + */ + private func cleanup() { + // Don't do anything if we're not connected + guard let connectedPeripheral = connectedPeripheral, + case .connected = connectedPeripheral.state else { return } + + for service in (connectedPeripheral.services ?? [] as [CBService]) { + for characteristic in (service.characteristics ?? [] as [CBCharacteristic]) { + if characteristic.uuid == FROMNUM_UUID && characteristic.isNotifying { + // It is notifying, so unsubscribe + self.connectedPeripheral?.setNotifyValue(false, for: characteristic) + } + } + } + + centralManager.cancelPeripheralConnection(connectedPeripheral) + } + + /* + * This callback happens whenever a peripheral that is advertising the Meshtastic Service UUID is found. + * We check the RSSI, to make sure it's close enough that we're interested in it, and if it is, + * we start the connection process + */ func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { - - print(peripheral) + + // Reject if the signal strength if it is too low + guard RSSI.intValue >= -100 + else { + print("Discovered perhiperal not in expected range, at %d", RSSI.intValue) + return + } + print("Discovered %s at %d", String(describing: peripheral.name), RSSI.intValue) + + + if peripheralArray.contains(peripheral) { print("Duplicate Found.") } else { @@ -154,6 +238,25 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph self.startScanning() } + + /* + * If the connection fails for whatever reason, we need to deal with it. + */ + func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { + + // print("Failed to connect to \(String(describing: peripheral.name!))", peripheral, String(describing: error)) + // let errorCode = (error! as! NSError).userInfo + // print("Central disconnected because \(errorCode)") + + if let e = error { + let errorDetails = (e as NSError) + print("Central disconnected because \(errorDetails.localizedDescription)") + } else { + print("Central disconnected! (no error)") + } + cleanup() + } + //--------------------------------------------------------------------------------------- // Disconnect Peripheral Event //--------------------------------------------------------------------------------------- @@ -168,9 +271,9 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } if(peripheral.identifier == connectedPeripheral.identifier){ - connectedPeripheral = nil - connectedNodeInfo = nil - connectedNode = nil + // connectedPeripheral = nil + // connectedNodeInfo = nil + // connectedNode = nil } print("Peripheral disconnected: " + peripheral.name!) self.startScanning() @@ -237,16 +340,23 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } } - //--------------------------------------------------------------------------------------- - // Data Read / Update Characteristic Event - //--------------------------------------------------------------------------------------- + + /* + * Callback lets us know that data has arrived via a notification on the characteristic + */ func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { + // Handle Error + if let error = error { + print("Error discovering characteristics: %s", error.localizedDescription) + cleanup() + return + } switch characteristic.uuid { case FROMNUM_UUID: peripheral.readValue(for: FROMRADIO_characteristic) - + print(characteristic.value ?? "no value") case FROMRADIO_UUID: if (characteristic.value == nil || characteristic.value!.isEmpty) { @@ -282,6 +392,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph connectedNode = meshData.nodes.first(where: {$0.id == decodedInfo.myInfo.myNodeNum}) if connectedNode != nil { connectedNode.myInfo = myInfoModel + connectedNode.update(from: connectedNode.data) } meshData.save() @@ -297,9 +408,19 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph 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() + // Found a matching node lets update it + let nodeMatch = meshData.nodes.first(where: { $0.id == decodedInfo.nodeInfo.num }) + if nodeMatch?.lastHeard ?? 0 > decodedInfo.nodeInfo.lastHeard { + let nodeIndex = meshData.nodes.firstIndex(where: { $0.id == decodedInfo.nodeInfo.num }) + meshData.nodes.remove(at: nodeIndex!) + meshData.save() + } + else { + + // Data is older than what the app already has + return + } + } meshData.nodes.append( @@ -346,7 +467,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph let fromUser = meshData.nodes.first(where: { $0.id == decodedInfo.packet.from }) var toUserLongName: String = "Broadcast" - var toUserShortName: String = "BRD" + var toUserShortName: String = "BC" if decodedInfo.packet.to != broadcastNodeId { @@ -356,7 +477,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } messageData.messages.append( - MessageModel(messageId: decodedInfo.packet.id, messageTimeStamp: Int64(decodedInfo.packet.rxTime), fromUserId: decodedInfo.packet.from, toUserId: decodedInfo.packet.to, fromUserLongName: fromUser?.user.longName ?? "Unknown", toUserLongName: toUserLongName, fromUserShortName: fromUser?.user.shortName ?? "???", toUserShortName: toUserShortName, receivedACK: decodedInfo.packet.decoded.wantResponse, messagePayload: messageText, direction: "IN")) + MessageModel(messageId: decodedInfo.packet.id, messageTimeStamp: decodedInfo.packet.rxTime, fromUserId: decodedInfo.packet.from, toUserId: decodedInfo.packet.to, fromUserLongName: fromUser?.user.longName ?? "Unknown", toUserLongName: toUserLongName, fromUserShortName: fromUser?.user.shortName ?? "???", toUserShortName: toUserShortName, receivedACK: decodedInfo.packet.decoded.wantResponse, messagePayload: messageText, direction: "IN")) messageData.save() } else { diff --git a/MeshtasticClient/Model/MessageModel.swift b/MeshtasticClient/Model/MessageModel.swift index 34b4c85a..ec844a79 100644 --- a/MeshtasticClient/Model/MessageModel.swift +++ b/MeshtasticClient/Model/MessageModel.swift @@ -10,7 +10,7 @@ struct MessageModel : Identifiable, Codable { let id: UUID var messageId: UInt32 - var messageTimestamp: Int64 + var messageTimestamp: UInt32 var fromUserId: UInt32 var toUserId: UInt32 var fromUserLongName: String @@ -21,7 +21,7 @@ struct MessageModel : Identifiable, Codable var messagePayload: String var direction: String - init(id: UUID = UUID(), messageId: UInt32, messageTimeStamp: Int64, fromUserId: UInt32, toUserId: UInt32, fromUserLongName: String, toUserLongName: String, fromUserShortName: String, toUserShortName: String, receivedACK: Bool, messagePayload: String, direction: String) + init(id: UUID = UUID(), messageId: UInt32, messageTimeStamp: UInt32, fromUserId: UInt32, toUserId: UInt32, fromUserLongName: String, toUserLongName: String, fromUserShortName: String, toUserShortName: String, receivedACK: Bool, messagePayload: String, direction: String) { self.id = id self.messageId = messageId @@ -52,7 +52,7 @@ extension MessageModel { struct Data { var id: UUID var messageId: UInt32 - var messageTimestamp: Int64 + var messageTimestamp: UInt32 var fromUserId: UInt32 var toUserId: UInt32 var fromUserLongName: String diff --git a/MeshtasticClient/Views/Bluetooth/Connect.swift b/MeshtasticClient/Views/Bluetooth/Connect.swift index f5ec8e27..b6601b12 100644 --- a/MeshtasticClient/Views/Bluetooth/Connect.swift +++ b/MeshtasticClient/Views/Bluetooth/Connect.swift @@ -26,7 +26,7 @@ struct Connect: View { List { Section(header: Text("Connected Device").font(.title)) { - if(bleManager.connectedPeripheral != nil){ + if bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.state == .connected { HStack { Image(systemName: "antenna.radiowaves.left.and.right") .symbolRenderingMode(.hierarchical) diff --git a/MeshtasticClient/Views/Helpers/BatteryIcon.swift b/MeshtasticClient/Views/Helpers/BatteryIcon.swift index 8697276a..cf106f61 100644 --- a/MeshtasticClient/Views/Helpers/BatteryIcon.swift +++ b/MeshtasticClient/Views/Helpers/BatteryIcon.swift @@ -9,7 +9,7 @@ struct BatteryIcon: View { if batteryLevel == 100 { - Image(systemName: "battery.100") + Image(systemName: "battery.100.bolt") .font(font) .foregroundColor(color) .symbolRenderingMode(.hierarchical) @@ -35,6 +35,13 @@ struct BatteryIcon: View { .foregroundColor(color) .symbolRenderingMode(.hierarchical) } + else if batteryLevel! == 0 { + + Image(systemName: "powerplug") + .font(font) + .foregroundColor(color) + .symbolRenderingMode(.hierarchical) + } else { Image(systemName: "battery.0") diff --git a/MeshtasticClient/Views/Helpers/MessageBubble.swift b/MeshtasticClient/Views/Helpers/MessageBubble.swift index 4e75aab4..c8877227 100644 --- a/MeshtasticClient/Views/Helpers/MessageBubble.swift +++ b/MeshtasticClient/Views/Helpers/MessageBubble.swift @@ -6,8 +6,11 @@ struct MessageBubble: View { var time: Int32 var shortName: String + var body: some View { + + HStack (alignment: .top) { CircleText(text: shortName, color: isCurrentUser ? Color.blue : Color(.darkGray)).padding(.all, 5) @@ -19,10 +22,16 @@ struct MessageBubble: View { .background(isCurrentUser ? Color.blue : Color(.darkGray)) .cornerRadius(10) HStack (spacing: 4) { - let messageDate = Date(timeIntervalSince1970: TimeInterval(time)) + + let messageDate = Date(timeIntervalSince1970: TimeInterval(time)) - Text(messageDate, style: .date).font(.caption2).foregroundColor(.gray) - Text(messageDate, style: .time).font(.caption2).foregroundColor(.gray) + if time != 0 { + Text(messageDate, style: .date).font(.caption2).foregroundColor(.gray) + Text(messageDate, style: .time).font(.caption2).foregroundColor(.gray) + } + else { + Text("Unknown").font(.caption2).foregroundColor(.gray) + } } .padding(.bottom, 10) } diff --git a/MeshtasticClient/Views/Messages/Messages.swift b/MeshtasticClient/Views/Messages/Messages.swift index 82b743ff..439fe8bd 100644 --- a/MeshtasticClient/Views/Messages/Messages.swift +++ b/MeshtasticClient/Views/Messages/Messages.swift @@ -86,7 +86,12 @@ struct Messages: View { .overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1)) .padding(.bottom, 15) - Button(action: sendMessage) { + Button(action: { + if bleManager.sendMessage(message: typingMessage) { + typingMessage = "" + } + + } ) { Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.blue) } @@ -128,7 +133,3 @@ struct Messages: View { } } } -func sendMessage() { - //chatHelper.sendMessage(Message(content: typingMessage, user: DataSource.secondUser)) - // typingMessage = "" - } diff --git a/MeshtasticClient/Views/Nodes/NodeDetail.swift b/MeshtasticClient/Views/Nodes/NodeDetail.swift index 6842e427..729cbb8e 100644 --- a/MeshtasticClient/Views/Nodes/NodeDetail.swift +++ b/MeshtasticClient/Views/Nodes/NodeDetail.swift @@ -36,7 +36,7 @@ struct NodeDetail: View { let annotations = [MapLocation(name: node.user.shortName, coordinate: node.position.coordinate!)] - Map(coordinateRegion: regionBinding, showsUserLocation: true, userTrackingMode: .constant(.none), annotationItems: annotations) { location in + Map(coordinateRegion: regionBinding, showsUserLocation: true, userTrackingMode: .none, annotationItems: annotations) { location in MapAnnotation( coordinate: location.coordinate, content: { @@ -63,26 +63,34 @@ struct NodeDetail: View { } .padding([.leading, .trailing, .bottom]) Divider() - VStack(alignment: .center) { - - Image(systemName: "waveform.path") - .font(.title) - .foregroundColor(.blue) - .symbolRenderingMode(.hierarchical) - Text("SNR").font(.title2).fixedSize() - Text(String(node.snr ?? 0)) - .font(.title2) - .foregroundColor(.gray) + if node.snr! > 0 { + VStack(alignment: .center) { + + Image(systemName: "waveform.path") + .font(.title) + .foregroundColor(.blue) + .symbolRenderingMode(.hierarchical) + Text("SNR").font(.title2).fixedSize() + Text(String(node.snr ?? 0)) + .font(.title2) + .foregroundColor(.gray) + } + Divider() } - Divider() VStack(alignment: .center) { BatteryIcon(batteryLevel: node.position.batteryLevel, font: .title, color: Color.blue) - Text("Battery").font(.title2).fixedSize() - Text(String(node.position.batteryLevel!) + "%") - .font(.title2) - .foregroundColor(.gray) - .symbolRenderingMode(.hierarchical) - } + if node.position.batteryLevel! > 0 { + Text("Battery").font(.title2).fixedSize() + Text(String(node.position.batteryLevel!) + "%") + .font(.title2) + .foregroundColor(.gray) + .symbolRenderingMode(.hierarchical) + } + else { + Text("Powered").font(.title2) + } + } + }.padding(4) Divider() HStack { @@ -97,15 +105,21 @@ struct NodeDetail: View { } .padding() Divider() - HStack{ + + if node.lastHeard > 0 { - Image(systemName: "clock").font(.title2).foregroundColor(.blue) - let lastHeard = Date(timeIntervalSince1970: TimeInterval(node.lastHeard)) - Text("Last Heard:").font(.title3) - Text(lastHeard, style: .relative).font(.title3) - Text("ago").font(.title3) - }.padding() - Divider() + HStack{ + + Image(systemName: "clock").font(.title2).foregroundColor(.blue) + let lastHeard = Date(timeIntervalSince1970: TimeInterval(node.lastHeard)) + //Text("Last Heard:").font(.title3) + //Text(lastHeard, style: .relative).font(.title3) + //Text("ago").font(.title3) + Text("Last Heard: \(lastHeard, style: .relative) ago").font(.title3) + }.padding() + Divider() + } + if node.position.coordinate != nil { HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/, spacing: 14) { Image(systemName: "mappin").font(.title).foregroundColor(.blue) diff --git a/MeshtasticClient/Views/Nodes/NodeMap.swift b/MeshtasticClient/Views/Nodes/NodeMap.swift index 0f5cf6f5..69e9ae11 100644 --- a/MeshtasticClient/Views/Nodes/NodeMap.swift +++ b/MeshtasticClient/Views/Nodes/NodeMap.swift @@ -29,7 +29,7 @@ struct NodeMap: View { } var body: some View { - let location = LocationHelper.currentLocation + let location = LocationHelper.currentLocation let currentCoordinatePosition = CLLocationCoordinate2D(latitude: location.latitude, longitude: location.longitude) let regionBinding = Binding( get: { diff --git a/MeshtasticClient/Views/Nodes/NodeRow.swift b/MeshtasticClient/Views/Nodes/NodeRow.swift index 02309516..7ec04e0b 100644 --- a/MeshtasticClient/Views/Nodes/NodeRow.swift +++ b/MeshtasticClient/Views/Nodes/NodeRow.swift @@ -17,9 +17,14 @@ struct NodeRow: View { HStack (alignment: .bottom){ Image(systemName: "timer").font(.headline).foregroundColor(.blue).symbolRenderingMode(.hierarchical) - let lastHeard = Date(timeIntervalSince1970: TimeInterval(node.lastHeard)) - Text("Last Heard:").font(.headline).foregroundColor(.gray) - Text(lastHeard, style: .relative).font(.headline).foregroundColor(.gray) + + if node.lastHeard > 0 { + let lastHeard = Date(timeIntervalSince1970: TimeInterval(node.lastHeard)) + Text("Last Heard: \(lastHeard, style: .relative) ago").font(.subheadline).foregroundColor(.gray) + } + else { + Text("Last Heard: Unknown").font(.subheadline).foregroundColor(.gray) + } } }.padding([.leading, .top, .bottom]) }