8000 IOS-197: Meatnet connection logic by jjohnstz · Pull Request #72 · combustion-inc/combustion-ios-ble · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

IOS-197: Meatnet connection logic #72

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
8000
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions Sources/CombustionBLE/ConnectionManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// ConnectionManager.swift

/*--
MIT License

Copyright (c) 2021 Combustion Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--*/

import Foundation

class ConnectionManager {

/// Tracks whether MeatNet is enabled.
var meatNetEnabled : Bool = false

/// Tracks whether DFU mode is enabled.
var dfuModeEnabled : Bool = false

private var connectionTimers: [String: Timer] = [:]
private var lastStatusUpdate: [String: Date] = [:]

/// Number of seconds after which a direct connection should be made to probe
private let PROBE_STATUS_STALE_TIMEOUT = 10.0

func receivedProbeAdvertising(_ probe: Probe?) {
guard let probe = probe else { return }

var probeStatusStale = true

if let lastUpdateTime = lastStatusUpdate[probe.serialNumberString] {
probeStatusStale = Date().timeIntervalSince(lastUpdateTime) > PROBE_STATUS_STALE_TIMEOUT
}

// Always connect to probe if DFU mode is enabled
if dfuModeEnabled {
probe.connect()
}
else if meatNetEnabled && // Otherwise if MeatNet is enabled and the probe data is stale, then connect to it
probeStatusStale &&
(probe.connectionState != .connected) &&
(connectionTimers[probe.serialNumberString] == nil) {

// Start timer to connect to probe after delay
connectionTimers[probe.serialNumberString] = Timer.scheduledTimer(withTimeInterval: 3, repeats: false, block: { [weak self] _ in

if let probe = self?.getProbeWithSerial(probe.serialNumberString) {
probe.connect()
}

// Clear timer
self?.connectionTimers[probe.serialNumberString] = nil
})
}
}

func receivedProbeAdvertising(_ probe: Probe?, from node: MeatNetNode) {
// When meatnet is enabled, try to connect to all Nodes.
if meatNetEnabled {
node.connect()
}
}

func receivedStatusFor(_ probe: Probe, directConnection: Bool) {
lastStatusUpdate[probe.serialNumberString] = Date()

// if receiving status from meatnet and DFU disabled, then disconnect from probe
if !directConnection && meatNetEnabled && !dfuModeEnabled {

if let probe = getProbeWithSerial(probe.serialNumberString),
probe.connectionState == .connected {
probe.disconnect()
}
}
}

private func getProbeWithSerial(_ serial: String) -> Probe? {
let probes = DeviceManager.shared.getProbes()

return probes.filter { $0.serialNumberString == serial}.first
}
}
127 changes: 69 additions & 58 deletions Sources/CombustionBLE/DeviceManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ import CoreBluetooth
/// (either via Bluetooth or from a list in the Cloud)
public class DeviceManager : ObservableObject {

/// Serial Number value indicating 'No Probe'
private let INVALID_PROBE_SERIAL_NUMBER = 0

/// Singleton accessor for class
public static let shared = DeviceManager()

public enum Constants {
public static let MINIMUM_PREDICTION_SETPOINT_CELSIUS = 0.0
public static let MAXIMUM_PREDICTION_SETPOINT_CELSIUS = 100.0

/// Serial Number value indicating 'No Probe'
static let INVALID_PROBE_SERIAL_NUMBER = 0
}

/// Dictionary of discovered devices.
Expand All @@ -54,12 +54,12 @@ public class DeviceManager : ObservableObject {
let handler: (Bool) -> Void
}

/// Tracks whether MeatNet is enabled.
private var meatNetEnabled : Bool = false;

/// Handler for messages from Probe
private let messageHandlers = MessageHandlers()

/// Connection manager to handle BLE connection logic
private let connectionManager = ConnectionManager()

public func addSimulatedProbe() {
addDevice(device: SimulatedProbe())
}
Expand All @@ -70,7 +70,12 @@ public class DeviceManager : ObservableObject {

/// Enables MeatNet repeater network.
public func enableMeatNet() {
meatNetEnabled = true;
connectionManager.meatNetEnabled = true
}

/// Enables DFU mode
public func enableDFUMode(_ enable: Bool) {
connectionManager.dfuModeEnabled = enable
}

/// Private initializer to enforce singleton
Expand Down Expand Up @@ -111,8 +116,8 @@ public class DeviceManager : ObservableObject {

/// Returns list of MeatNet nodes
/// - returns: List of all MeatNet nodes
public func getDisplays() -> [MeatNetNode] {
if meatNetEnabled {
public func getMeatnetNodes() -> [MeatNetNode] {
if connectionManager.meatNetEnabled {
return Array(devices.values).compactMap { device in
return device as? MeatNetNode
}
Expand Down Expand Up @@ -142,19 +147,21 @@ public class DeviceManager : ObservableObject {
/// Gets the best Node for communicating with a Probe.
func getBestNodeForProbe(serialNumber: UInt32) -> MeatNetNode? {
var foundNode : MeatNetNode? = nil
// Check Nodes to which we are connected to see if they have a route to the Probe
for (_, device) in devices {
if let node = device as? MeatNetNode {
// Check multiple Nodes and choose the one with the best RSSI to this device.
var foundRssi = Device.MIN_RSSI
if let _ = node.getNetworkedProbe(serialNumber: serialNumber) {
if node.rssi > foundRssi {
foundNode = node
foundRssi = node.rssi
}
var foundRssi = Device.MIN_RSSI

let meatnetNodes = getMeatnetNodes()

for node in meatnetNodes {
// Check Nodes to which we are connected to see if they have a route to the Probe
if node.connectionState == .connected {
// Choose node with the best RSSI that has connection to probe
if node.hasConnectionToProbe(serialNumber) && node.rssi > foundRssi {
foundNode = node
foundRssi = node.rssi
}
}
}

return foundNode
}

Expand Down Expand Up @@ -484,11 +491,15 @@ extension DeviceManager : BleManagerDelegate {
// Update Probe Device from direct status notification
guard let probe = findDeviceByBleIdentifier(bleIdentifier: identifier) as? Probe else { return }
probe.updateProbeStatus(deviceStatus: status)

connectionManager.receivedStatusFor(probe, directConnection: true)
}

func updateDeviceWithNodeStatus(serialNumber: UInt32, status: ProbeStatus, hopCount: HopCount) {
guard let probe = findProbeBySerialNumber(serialNumber: serialNumber) else { return }
probe.updateProbeStatus(deviceStatus: status, hopCount: hopCount)

connectionManager.receivedStatusFor(probe, directConnection: false)
}

func handleBootloaderAdvertising(advertisingName: String, rssi: NSNumber, peripheral: CBPeripheral) {
Expand All @@ -514,11 +525,12 @@ extension DeviceManager : BleManagerDelegate {
/// - param rssi - Signal strength to Probe (only present if advertising is directly from Probe)
/// - param identifier - BLE identifier (only present if advertising is directly from Probe)
/// - return Probe that was updated or added, if any
func updateProbeWithAdvertising(advertising: AdvertisingData, isConnectable: Bool?, rssi: NSNumber?, identifier: UUID?) -> Probe? {
private func updateProbeWithAdvertising(advertising: AdvertisingData, isConnectable: Bool?,
rssi: NSNumber?, identifier: UUID?) -> Probe? {
var foundProbe : Probe? = nil

// If this advertising data was from a Probe, attempt to find its Device entry by its serial number.
if advertising.serialNumber != INVALID_PROBE_SERIAL_NUMBER {
if advertising.serialNumber != Constants.INVALID_PROBE_SERIAL_NUMBER {
let uniqueIdentifier = String(advertising.serialNumber)
if let probe = devices[uniqueIdentifier] as? Probe {
// If we already have an entry for this Probe, update its information.
Expand All @@ -532,11 +544,6 @@ extension DeviceManager : BleManagerDelegate {
}
}

// If MeatNet is enabled, try to connect to all probes
if(meatNetEnabled) {
foundProbe?.connect()
}

return foundProbe
}

Expand All @@ -548,37 +555,41 @@ extension DeviceManager : BleManagerDelegate {
func updateDeviceWithAdvertising(advertising: AdvertisingData, isConnectable: Bool, rssi: NSNumber, identifier: UUID) {
switch(advertising.type) {
case .probe:
let _ = updateProbeWithAdvertising(advertising: advertising, isConnectable: isConnectable, rssi: rssi, identifier: identifier)

// Create or update probe with advertising data
let probe = updateProbeWithAdvertising(advertising: advertising, isConnectable: isConnectable, rssi: rssi, identifier: identifier)

// Notify connection manager
connectionManager.receivedProbeAdvertising(probe)

case .meatNetNode:
// If this advertising data was from a Node, attempt to find its Device entry by its BLE identifier.
if(meatNetEnabled) {
let uniqueIdentifier = identifier.uuidString
if let node = devices[uniqueIdentifier] as? MeatNetNode {
node.updateWithAdvertising(advertising, isConnectable: isConnectable, RSSI: rssi)

// If MeatNet is enabled, try to connect to all Nodes.
node.connect()

// Also update the probe associated with this advertising data
if let probe = updateProbeWithAdvertising(advertising: advertising, isConnectable: nil, rssi: nil, identifier: nil) {
node.updateNetworkedProbe(probe: probe)
}

} else {
let node = MeatNetNode(advertising, isConnectable: isConnectable, RSSI: rssi, identifier: identifier)
addDevice(device: node)

// If MeatNet is enabled, try to connect to all Nodes.
node.connect()

// Also update the probe associated with this advertising data
if let probe = updateProbeWithAdvertising(advertising: advertising, isConnectable: nil, rssi: nil, identifier: nil) {
node.updateNetworkedProbe(probe: probe)
}
}

// if meatnet is not enabled, then ignore advertising from meatnet nodes
if(!connectionManager.meatNetEnabled) {
return
}

let meatnetNode: MeatNetNode

// Update node if it is in device list
if let node = devices[identifier.uuidString] as? MeatNetNode {
node.updateWithAdvertising(advertising, isConnectable: isConnectable, RSSI: rssi)
meatnetNode = node
} else {
// Create node and add to device list
meatnetNode = MeatNetNode(advertising, isConnectable: isConnectable, RSSI: rssi, identifier: identifier)
addDevice(device: meatnetNode)
}

// Update the probe associated with this advertising data
let probe = updateProbeWithAdvertising(advertising: advertising, isConnectable: nil, rssi: nil, identifier: nil)

// Add probe to meatnet node
meatnetNode.updateNetworkedProbe(probe: probe)

// Notify connection manager
connectionManager.receivedProbeAdvertising(probe, from: meatnetNode)

case .unknown:
print("Found device with unknown type")

Expand All @@ -587,7 +598,7 @@ extension DeviceManager : BleManagerDelegate {
}

/// Finds Device (Node or Probe) by specified BLE identifier.
func findDeviceByBleIdentifier(bleIdentifier: UUID) -> Device? {
private func findDeviceByBleIdentifier(bleIdentifier: UUID) -> Device? {
var foundDevice : Device? = nil
if let device = devices[bleIdentifier.uuidString] {
// This was a MeatNet Node as it was stored by its BLE UUID.
Expand All @@ -608,7 +619,7 @@ extension DeviceManager : BleManagerDelegate {
return foundDevice
}

func findProbeBySerialNumber(serialNumber: UInt32) -> Probe? {
private func findProbeBySerialNumber(serialNumber: UInt32) -> Probe? {
var foundProbe : Probe? = nil

if let probe = devices[String(serialNumber)] as? Probe {
Expand Down Expand Up @@ -673,7 +684,7 @@ extension DeviceManager : BleManagerDelegate {
/// - MARK: Probe Direct Message Handling
//////////////////////////////////////////////

func handleProbeUARTResponse(identifier: UUID, response: Response) {
private func handleProbeUARTResponse(identifier: UUID, response: Response) {
if let logResponse = response as? LogResponse {
updateDeviceWithLogResponse(identifier: identifier, logResponse: logResponse)
}
Expand Down Expand Up @@ -714,7 +725,7 @@ extension DeviceManager : BleManagerDelegate {
/// - MARK: Node/MeatNet Direct Message Handling
///////////////////////////////////////

func handleNodeUARTResponse(identifier: UUID, response: NodeResponse) {
private func handleNodeUARTResponse(identifier: UUID, response: NodeResponse) {
// print("Received Response from Node: \(response)")

if let setPredictionResponse = response as? NodeSetPredictionResponse {
Expand Down Expand Up @@ -742,7 +753,7 @@ extension DeviceManager : BleManagerDelegate {
}
}

func handleNodeUARTRequest(identifier: UUID, request: NodeRequest) {
private func handleNodeUARTRequest(identifier: UUID, request: NodeRequest) {
// print("CombustionBLE : Received Request from Node: \(request)")
if let statusRequest = request as? NodeProbeStatusRequest, let probeStatus = statusRequest.probeStatus, let hopCount = statusRequest.hopCount {
// Update the Probe based on the information that was received
Expand Down
2 changes: 1 addition & 1 deletion Sources/CombustionBLE/Devices/Device.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public class Device : ObservableObject {

// If we were disconnected and we should be maintaining a connection, attempt to reconnect.
if(maintainingConnection && (connectionState == .disconnected || connectionState == .failed)) {
DeviceManager.shared.connectToDevice(self)
connect()
}
}

Expand Down
10 changes: 6 additions & 4 deletions Sources/CombustionBLE/Devices/MeatNetNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@ public class MeatNetNode: Device {
}

/// Adds Probe to dictionary of networked Probes, if not already present.
func updateNetworkedProbe(probe : Probe) {
func updateNetworkedProbe(probe: Probe?) {
guard let probe = probe else { return }

probes[probe.serialNumber] = probe
}

/// Returns probe associated with the specified serial number, if it's in this list.
func getNetworkedProbe(serialNumber: UInt32) -> Probe? {
return probes[serialNumber]
/// Returns true if node has connection to probe.
func hasConnectionToProbe(_ serialNumber: UInt32) -> Bool {
return probes[serialNumber] != nil
}

/// Special handling for MeatNetNode model info. Need to decode model info string
Expand Down
0