NordicSemiconductor / IOS-CoreBluetooth-Mock

Mocking library for CoreBluetooth framework.
BSD 3-Clause "New" or "Revised" License
225 stars 51 forks source link

CBServices Not Refreshed on Connect #66

Closed jason-gabriele closed 2 years ago

jason-gabriele commented 2 years ago

Hello.

I'm trying to implement a smarter way of using iOS's service cache so I don't need to call discoverServices on every connection. I've noticed that iOS seems to recreate the CBService and CBCharacteristic objects on each connection, even if the underlying details haven't changed. If I use the native CoreBluetooth objects, I can simply refresh my cached objects and issue read/writes without needing to call discoverServices on each connection. However, CoreBluetoothMock doesn't update the objects unless you specifically call discoverServices. Would it be possible to call smartCopy on connect to update the object references?

Thanks

Here's an example of my testing just using the native CoreBluetooth:

Previous Connection:
<CBCharacteristic: 0x28062b6c0, UUID = ...
<CBCharacteristic: 0x28062b720, UUID = ...
<CBCharacteristic: 0x28062b7e0, UUID = ...
<CBCharacteristic: 0x28062b780, UUID = ...

Current Connection:
<CBCharacteristic: 0x280605740, UUID = ...
<CBCharacteristic: 0x280605bc0, UUID = ...
<CBCharacteristic: 0x2806060a0, UUID = ...
<CBCharacteristic: 0x280604ea0, UUID = ...

You can see the pointer is changing on each connect

philips77 commented 2 years ago

I can simply refresh my cached objects and issue read/writes without needing to call discoverServices on each connection

How do you obtain the new services? Are they available using from CBPeripheral?

Could you paste some code snippet?

jason-gabriele commented 2 years ago

I had run an experiment where I saw the peripheral.services coming from onConnect was prepopulated, but now using the same code, I cannot get it consistently. I tried looking at packet logger and it seems that most discoverServices() calls are using the system cache anyway (though I cannot get rid of the 2803 device info check). So, I guess I don't need any changes.

Thanks

kscheff commented 2 years ago

My App chokes on this issue, since I am not getting all data.

Here is the class which accesses the characteristics:

class DeviceService {

  var device: Device
  var service: CBService

  init(device: Device, service: CBService) {
    self.device = device
    self.service = service
  }

  var characteristics: [CBUUID: CBCharacteristic] {
    get {
      var characteristics = [CBUUID: CBCharacteristic]()
      if let services = service.characteristics {
        for characteristic in services as [CBCharacteristic] {
          characteristics[characteristic.uuid] = characteristic
        }
      }
      return characteristics
    }
  }
}

I am getting back:

(lldb) print myDeviceServices?.services?[UUIDS.deviceInfoServiceUUID]?.characteristics
([xxx.CBUUID : xxx.CBCharacteristic]?) $R11 = 0 key/value pairs

Running on the native stack the result is quite different:

(lldb) print myDeviceServices?.services?[UUIDS.deviceInfoServiceUUID]?.characteristics
([CBUUID : CBCharacteristic]?) $R0 = 5 key/value pairs {
  [0] = {
    key = 0x000000028022aa80 {
      baseNSObject@0 = {
        isa = CBUUID
      }
      _bytes = ""
      _type = '\0'
    }
    value = 0x00000002826447e0 {
      baseCBAttribute@0 = {
        baseNSObject@0 = {
          isa = CBCharacteristic
        }
        _UUID = 0x000000028022aa80
      }
      _isBroadcasted = false
      _isNotifying = false
      _service = 0x00000002817f04c0
      _properties = 2
      _value = 0x0000000000000000
      _descriptors = 0x0000000000000000
      _valueTimestamp = 0
      _peripheral = 0x0000000283355180
      _handle = 0xbdb2ab539a9ef274 Int64(19)
      _valueHandle = 0xbdb2ab539a9ef1f4 Int64(20)
    }
  }
  [1] = {
    key = 0x000000028022aa00 {
      baseNSObject@0 = {
        isa = CBUUID
      }
      _bytes = ""
      _type = '\0'
    }
    value = 0x0000000282644ba0 {
      baseCBAttribute@0 = {
        baseNSObject@0 = {
          isa = CBCharacteristic
        }
        _UUID = 0x000000028022aa00
      }
      _isBroadcasted = false
      _isNotifying = false
      _service = 0x00000002817f04c0
      _properties = 2
      _value = 0x0000000000000000
      _descriptors = 0x0000000000000000
      _valueTimestamp = 0
      _peripheral = 0x0000000283355180
      _handle = 0xbdb2ab539a9ef374 Int64(17)
      _valueHandle = 0xbdb2ab539a9ef2f4 Int64(18)
    }
  }
  [2] = {
    key = 0x000000028022a680 {
      baseNSObject@0 = {
        isa = CBUUID
      }
      _bytes = ""
      _type = '\0'
    }
    value = 0x0000000282644360 {
      baseCBAttribute@0 = {
        baseNSObject@0 = {
          isa = CBCharacteristic
        }
        _UUID = 0x000000028022a680
      }
      _isBroadcasted = false
      _isNotifying = false
      _service = 0x00000002817f04c0
      _properties = 2
      _value = 0x0000000000000000
      _descriptors = 0x0000000000000000
      _valueTimestamp = 0
      _peripheral = 0x0000000283355180
      _handle = 0xbdb2ab539a9ef074 Int64(23)
      _valueHandle = 0xbdb2ab539a9ef7f4 Int64(24)
    }
  }
  [3] = {
    key = 0x000000028022aa20 {
      baseNSObject@0 = {
        isa = CBUUID
      }
      _bytes = ""
      _type = '\0'
    }
    value = 0x0000000282644300 {
      baseCBAttribute@0 = {
        baseNSObject@0 = {
          isa = CBCharacteristic
        }
        _UUID = 0x000000028022aa20
      }
      _isBroadcasted = false
      _isNotifying = false
      _service = 0x00000002817f04c0
      _properties = 2
      _value = 0x0000000000000000
      _descriptors = 0x0000000000000000
      _valueTimestamp = 0
      _peripheral = 0x0000000283355180
      _handle = 0xbdb2ab539a9ef174 Int64(21)
      _valueHandle = 0xbdb2ab539a9ef0f4 Int64(22)
    }
  }
  [4] = {
    key = 0x000000028022a400 {
      baseNSObject@0 = {
        isa = CBUUID
      }
      _bytes = ""
      _type = '\0'
    }
    value = 0x0000000282644720 {
      baseCBAttribute@0 = {
        baseNSObject@0 = {
          isa = CBCharacteristic
        }
        _UUID = 0x000000028022a400
      }
      _isBroadcasted = false
      _isNotifying = false
      _service = 0x00000002817f04c0
      _properties = 2
      _value = 0x0000000000000000
      _descriptors = 0x0000000000000000
      _valueTimestamp = 0
      _peripheral = 0x0000000283355180
      _handle = 0xbdb2ab539a9ef774 Int64(25)
      _valueHandle = 0xbdb2ab539a9ef6f4 Int64(26)
    }
  }
}

This access method is already some 5 years in service and proven, I don't want to change the service discovery.

kscheff commented 2 years ago

OK, tracked down the issue: the native peripheral didDiscoverServices callback delivers under peripheral.services all services including all characteristics on the very first discovery call.

My code was then walking through all services and called for each service .discoverCharacteristics:

  func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
    characteristics = 0
    logger.debug("discovered \(peripheral.services!.count) services")
    for service in peripheral.services! {
      let thisService = service as CBService
      serviceList[service.uuid] = DeviceService(device: self, service: thisService)
      peripheral.discoverCharacteristics([], for: thisService)
      characteristics += 1
      logger.debug("discover characteristics for #\(characteristics), \(service.uuid)")
    }
  }

Then it collects each service:

  func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
    logger.debug("discovered charactersitic #\(characteristics), \(service.uuid) -> \(service.characteristics.map {$0.description + "/"} ) ")
// --> saving service on my list was missing
    serviceList[service.uuid] = DeviceService(device: self, service: service)
// <--
    characteristics -= 1
    if serviceList[service.uuid] != nil && characteristics == 0 {
      isPopulated = true
      logger.debug("discovering finished... calling back")
      serviceCallbacks.call(self.serviceList)
    }
  }

My original code was missing to save this in the device serviceList, so it got discarded before. This code portion was untouched the last years, so I am sure that the native stack delivers the characteristics and manages them under the hood. A "smarter copy" inside mock code would not help in such case. One solution would be to call .discoverCharacteristics automatically when .discoverServices is called and then return a combined list much like what the native stack delivers.