Search code examples
python-3.xgattbluetooth-peripheral

How to Mimic nRF Connect (for Android) Actions to Pygatt Script?


I'm using nRF Connect for Android to test a BLE peripheral. The peripheral is a BSX Insight residual muscle oxygen monitor whose software application is no longer functional or supported by the manufacturer. Thus, my only option to use my device (BSX) is to write my own control software. I've written a Python 3.7 script that I run within a tkinter routine on my 64-bit Win 10 laptop. Also, I'm using the Pygatt library and a BLED112 BT dongle.

I can connect to the peripheral, read and write values just fine to characteristics, but I'm sure that the "conversion" from the process used in nRF Connect and to my script is incomplete and inefficient. So the first thing I'd like to confirm is that the correct respective functions from Pygatt are used. Once I'm using the correct functions from Pygatt, then I can compare respective outputs for the two data (characteristic values) streams that I want to capture and store.

The basic process in nRF Connect:
1. scan
2. select/connect the BSX Insight
3. expose the service and characteristics of interest
4. enable CCCDs
5. write the "start data" values (04-02)

These are the process command results from the nRF Connect log file. Starting with number four:
4.
D 09:04:54.491 gatt.setCharacteristicNotification(00002a37-0000-1000-8000-00805f9b34fb, true) 11
D 09:04:54.496 gatt.setCharacteristicNotification(2e4ee00b-d9f0-5490-ff4b-d17374c433ef, true) 20x
D 09:04:54.499 gatt.setCharacteristicNotification(2e4ee00d-d9f0-5490-ff4b-d17374c433ef, true) 25x
D 09:04:54.516 gatt.setCharacteristicNotification(2e4ee00e-d9f0-5490-ff4b-d17374c433ef, true) 32x
D 09:04:54.519 gatt.setCharacteristicNotification(00002a63-0000-1000-8000-00805f9b34fb, true) 36
D 09:04:54.523 gatt.setCharacteristicNotification(00002a53-0000-1000-8000-00805f9b34fb, true) 40

The above resulted from using the nRF command "Enable CCCDs." Basically every characteristic that could be enabled was enabled which is fine. The 'x' are the three that I need enabled. The others are extra. Note, I've annotated the respective handles for these UUIDs on the end of the line.

  1. V 09:05:39.211 Writing command to characteristic 2e4ee00a-d9f0-5490-ff4b-d17374c433ef
    D 09:05:39.211 gatt.writeCharacteristic(2e4ee00a-d9f0-5490-ff4b-d17374c433ef, value=0x0402)
    I 09:05:39.214 Data written to 2e4ee00a-d9f0-5490-ff4b-d17374c433ef, value: (0x) 04-02
    A 09:05:39.214 "(0x) 04-02" sent

Number five is where I write 0402 to the UUID above. This action sends the data/value streams from:

  • 2e4ee00d-d9f0-5490-ff4b-d17374c433ef, with a descriptor handle 26
  • 2e4ee00e-d9f0-5490-ff4b-d17374c433ef, with a descriptor handle 33

Once I've done the basic steps above in nRF Connect, the two characteristic value streams become active, and I can immediately see the converted values in my Garmin Edge 810 head unit.

So attempting to duplicate the same process within my tkinter snippet:

# this function fires from the 'On' button click event
def powerON():
    powerON_buttonevent = 1
    print(f"\tpowerON_buttonevent OK {powerON_buttonevent}")

    # Connect to the BSX Insight
    try:
        adapter = pygatt.BGAPIBackend()       # serial_port='COM3'
        adapter.start()
        device = adapter.connect('0C:EF:AF:81:0B:76', address_type=pygatt.BLEAddressType.public)
        print(f"\tConnected: {device}")
    except:
        print(f"BSX Insight connection failure")
    finally:
        # adapter.stop()
        pass

    # Enable only these CCCDs
    try:
        device.char_write_handle(21, bytearray([0x01, 0x00]), wait_for_response=True)
        device.char_write_handle(26, bytearray([0x01, 0x00]), wait_for_response=True)
        device.char_write_handle(33, bytearray([0x01, 0x00]), wait_for_response=True)

        print(f"\te00b DESC: {device.char_read_long_handle(21)}")       # notifiy e00b
        print(f"\te00d DESC: {device.char_read_long_handle(26)}")       # notify e00d SmO2
        print(f"\te00e DESC: {device.char_read_long_handle(33)}")       # notify e00e tHb

        # Here's where I tested functions from Pygatt...
        # print(f"\t{device.get_handle('UUID_here')}")     # function works
        # print(f"\tvalue_handle/characteristic_config_handle:            {device._notification_handles('UUID_here')}")       # function works
        # print(f"{device.char_read('UUID_here')}")
        # print(f"{device.char_read_long_handle(handle_here)}")        # function works
    except:
        print(f"CCCD write value failure")
    finally:
        # adapter.stop()
        pass

    # Enable the data streams
    try:
        device.char_write('2e4ee00a-d9f0-5490-ff4b-d17374c433ef', bytearray([0x04, 0x02]), wait_for_response=True)    # function works
        print(f"\te00a Power ON: {device.char_read('2e4ee00e-d9f0-5490-ff4b-d17374c433ef')}")
    except:
        print(f"e00a Power ON write failure")
    finally:
        # adapter.stop()
        pass

    # Subscribe to SmO2 and tHb UUIDs
    try:
        def data_handler(handle, value):
            """
            Indication and notification come asynchronously, we use this function to
            handle them either one at the time as they come.
            :param handle:
            :param value:
            :return:
            """
            if handle == 25:
                print(f"\tSmO2: {value} Handle: {handle}")
            elif handle == 32:
                print(f"\ttHb: {value} Handle: {handle}")
            else:
                print(f"\tvalue: {value}, handle: {handle}")

        device.subscribe("2e4ee00d-d9f0-5490-ff4b-d17374c433ef", callback=data_handler, indication=False, wait_for_response=True)
        device.subscribe("2e4ee00e-d9f0-5490-ff4b-d17374c433ef", callback=data_handler, indication=False, wait_for_response=True)
        print(f"\tSuccess 2e4ee00d: {device.char_read('2e4ee00d-d9f0-5490-ff4b-d17374c433ef')}")
        print(f"\tSuccess 2e4ee00e: {device.char_read('2e4ee00e-d9f0-5490-ff4b-d17374c433ef')}")

        # this statement causes a run-on continuity when enabled
        # while True:
        #     sleep(1)

    except:
        print("e00d/e00e subscribe failure")
    finally:
        adapter.stop()
        # pass  

Problem: in the output window of my Atom editor, the two data streams start as expected. For example:

I 09:05:39.983 Notification received from 2e4ee00d-d9f0-5490-ff4b-d17374c433ef, value: (0x) 00- 00-00-00-C0-FF-00-00-C0-FF-84-65-B4-3B-9E-AB-83-3C-FF-03

and...

I 09:05:39.984 Notification received from 2e4ee00e-d9f0-5490-ff4b-d17374c433ef, value: (0x) 1C-00-00-FF-03-FF-0F-63-00-00-00-00-00-00-16-32-00-00-00-00

I'll see about seven to ten lines of data before the "stream" stops. There'll be a gap of about 20 seconds, and then a big dump of values. This is different from the output from nRF Connect, which is immediate and continous.

I have the logs from nRF Connect and Python...but I'm not sure which log entry points to the cause of the stop. Might this issue be related to the Peripheral Preferred Connection Parameters? The nRF Connect property read shows:

  • ConnectionInterval = 50ms~100ms
  • SlaveLatency = 1
  • SuperTimeoutMonitor = 200

The Python log entry shows this:

INFO:pygatt.backends.bgapi.bgapi:Connection status: handle=0x0, flags=5, address=0xb'760b81afef0c', connection interval=75.000000ms, timeout=1000, latency=0 intervals, bonding=0xff

Thoughts anyone? (And truly, thanks in advance.)


Solution

  • I've answered my questions. I now have to solve the new problem of why my tKinter dialog is "not responding" as a separate issue. Thanks All Edit 3/31/2020: I re-wrote the script using pyQt and now have a functional app.