Skip to content
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

Some characteristics are missing if performing characteristic discovery with the LBLEClient class using LBLE library #91

Closed
sfyang opened this issue Feb 7, 2018 · 3 comments
Assignees
Labels

Comments

@sfyang
Copy link

sfyang commented Feb 7, 2018

This issue has been briefly mentioned and demonstrated in the issue report of #90.

Consider the following modified version of the 'ConnectPeripheral.ino' example from LBLE library, which inherits the LBLEServiceInfo struct and puts the characteristic list into the struct (thus probably solves #90 to some extend), and also extends the LBLEClient class to use the new ABLEServiceInfo struct:

/*
  This example scans nearby BLE peripherals and prints the peripherals found.

  created Mar 2017 by MediaTek Labs
*/

#include <LBLE.h>
#include <LBLECentral.h>
#include <iterator>

extern "C" {
#include <bt_gattc.h>
}

struct ABLEServiceInfo : LBLEServiceInfo
{
  std::map<LBLEUuid, uint16_t> m_characteristics;
};

class ABLEClient : public LBLEClient
{
public:
  bool connect(const LBLEAddress& address)
  {
    bool done = LBLEClient::connect(address);

    if (done) {
      discoverServices();
    }
  }

  int getCharacteristicCount(int index)
  {
    if (index < 0 || index >= getServiceCount()) {
      return 0;
    }

    return m_services[index].m_characteristics.size();
  }

  LBLEUuid getCharacteristicUuid(int svcIndex, int chrIndex)
  {
    if (svcIndex < 0 || svcIndex >= getServiceCount() || chrIndex < 0 || chrIndex >= getCharacteristicCount(svcIndex)) {
      return LBLEUuid();
    }
    auto c = m_services[svcIndex].m_characteristics.begin();
    std::advance(c, chrIndex);

    return c->first;
  }

protected:
  std::vector<ABLEServiceInfo> m_services;

  int discoverServices()
  {
    if (!connected()) {
      return 0;
    }

    bt_gattc_discover_primary_service_req_t searchRequest = {
      .opcode = BT_ATT_OPCODE_READ_BY_GROUP_TYPE_REQUEST,
      .starting_handle = 0x0001,
      .ending_handle = 0xFFFF,
      .type16 = BT_GATT_UUID16_PRIMARY_SERVICE
    };

    bool shouldContinue = false;
    bool done = false;

    do {
      if (!connected()) {
        break;
      }

      done = waitAndProcessEvent(
        [this, &searchRequest]()
        {
          bt_gattc_discover_primary_service(m_connection, &searchRequest);
        },
        BT_GATTC_DISCOVER_PRIMARY_SERVICE,
        [this, &searchRequest, &shouldContinue](bt_msg_type_t, bt_status_t status, void* buf)
        {
          const bt_gattc_read_by_group_type_rsp_t* rsp = (bt_gattc_read_by_group_type_rsp_t*)buf;
          uint16_t endIndex = 0, startIndex = 0;
          uint16_t uuid16 = 0;
          bt_uuid_t uuid128;
          const uint8_t *attrDataList = rsp->att_rsp->attribute_data_list;
          const uint32_t entryLength = rsp->att_rsp->length;
          const uint8_t entryCount = (rsp->length - 2) / entryLength;
          for (int i = 0; i < entryCount; i++) {
            memcpy(&startIndex, attrDataList + i * entryLength, 2);
            memcpy(&endIndex, attrDataList + i * entryLength + 2, 2);

            ABLEServiceInfo serviceInfo;

            if (entryLength == 6) {
              memcpy(&uuid16, attrDataList + i * entryLength + 4, entryLength - 4);
              serviceInfo.uuid = LBLEUuid(uuid16);
            } else {
              memcpy(&uuid128.uuid, attrDataList + i * entryLength + 4, entryLength - 4);
              serviceInfo.uuid = LBLEUuid(uuid128);
            }

            serviceInfo.startHandle = startIndex;
            serviceInfo.endHandle = endIndex;

            m_services.push_back(serviceInfo);
          }

          shouldContinue = (status == BT_ATT_ERRCODE_CONTINUE);

          searchRequest.starting_handle = endIndex;
        });
    } while (shouldContinue && done);

    if (m_services.size()) {
      discoverCharacteristics();
    }

    return m_services.size();
  }

  int discoverCharacteristics()
  {
    int ret = 0;
    if (!connected()) {
      return 0;
    }

    for (size_t i = 0; i < getServiceCount(); i++) {
      ret += discoverCharacteristicsOfService(m_services[i]);
    }

    return ret;
  }

  int discoverCharacteristicsOfService(ABLEServiceInfo& s)
  {
    if (!connected()) {
      return 0;
    }

    bt_gattc_discover_charc_req_t searchRequest = {
      .opcode = BT_ATT_OPCODE_READ_BY_TYPE_REQUEST,
      .starting_handle = s.startHandle,
      .ending_handle = s.endHandle,
      .type = {0}
    };
    uint16_t charUuid = BT_GATT_UUID16_CHARC;
    bt_uuid_load(&searchRequest.type, (void*)&charUuid, 2);

    bool shouldContinue = false;
    bool done = false;

    s.m_characteristics.clear();

    do {
      if (!connected()) {
        break;
      }

      done = waitAndProcessEvent(
        [this, &searchRequest]()
        {
          bt_gattc_discover_charc(m_connection, &searchRequest);
        },
        BT_GATTC_DISCOVER_CHARC,
        [this, &s, &searchRequest, &shouldContinue](bt_msg_type_t, bt_status_t status, void* buf)
        {
          const bt_gattc_read_by_type_rsp_t* rsp = (bt_gattc_read_by_type_rsp_t*)buf;
          uint16_t attributeHandle = 0, valueHandle = 0;
          uint8_t properties = 0;
          uint16_t uuid16 = 0;
          bt_uuid_t uuid128;
          const uint8_t *attrDataList = rsp->att_rsp->attribute_data_list;
          const uint32_t entryLength = rsp->att_rsp->length;
          const uint8_t entryCount = (rsp->length - 2) / entryLength;
          for (int i = 0; i < entryCount; i++) {
            memcpy(&attributeHandle, attrDataList + i * rsp->att_rsp->length, 2);
            memcpy(&properties, attrDataList + i * rsp->att_rsp->length + 2, 1);
            memcpy(&valueHandle, attrDataList + i * rsp->att_rsp->length + 3, 2);

            if (rsp->att_rsp->length < 20) {
              memcpy(&uuid16, attrDataList + i * rsp->att_rsp->length + 5, 2);
              s.m_characteristics.insert(std::make_pair(LBLEUuid(uuid16), valueHandle));
            } else {
              memcpy(&uuid128.uuid, attrDataList + i * entryLength + 5, 16);
              s.m_characteristics.insert(std::make_pair(LBLEUuid(uuid128), valueHandle));
            }

          }
          
          shouldContinue = (status == BT_ATT_ERRCODE_CONTINUE) && (entryCount > 0);

          searchRequest.starting_handle = valueHandle;
        });
    } while (shouldContinue && done);

    return done;
  }
};

ABLEClient client;

void setup() {
  //Initialize serial
  Serial.begin(9600);

  // Initialize BLE subsystem
  Serial.println("BLE begin");
  LBLE.begin();
  while (!LBLE.ready()) {
    delay(10);
  }
  Serial.println("BLE ready");

  // start scanning nearby advertisements
  LBLECentral.scan();
}

void printDeviceInfo(int i) {
  Serial.print(i);
  Serial.print("\t");
  Serial.print(LBLECentral.getAddress(i));
  Serial.print("\t");
  Serial.print(LBLECentral.getAdvertisementFlag(i), HEX);
  Serial.print("\t");
  Serial.print(LBLECentral.getRSSI(i));
  Serial.print("\t");
  const String name = LBLECentral.getName(i);
  Serial.print(name);
  if(name.length() == 0)
  {
    Serial.print("(Unknown)");
  }
  Serial.print(" by ");
  const String manu = LBLECentral.getManufacturer(i);
  Serial.print(manu);
  Serial.print(", service: ");
  if (!LBLECentral.getServiceUuid(i).isEmpty()) {
    Serial.print(LBLECentral.getServiceUuid(i));
  } else {
    Serial.print("(no service info)");
  }

  if (LBLECentral.isIBeacon(i)) {
    LBLEUuid uuid;
    uint16_t major = 0, minor = 0;
    int8_t txPower = 0;
    LBLECentral.getIBeaconInfo(i, uuid, major, minor, txPower);

    Serial.print(" ");
    Serial.print("iBeacon->");
    Serial.print("  UUID: ");
    Serial.print(uuid);
    Serial.print("\tMajor:");
    Serial.print(major);
    Serial.print("\tMinor:");
    Serial.print(minor);
    Serial.print("\ttxPower:");
    Serial.print(txPower);
  }

  Serial.println();
}

int searching = 1;

enum AppState
{
  SEARCHING,    // We scan nearby devices and provide a list for user to choose from
  CONNECTING,   // User has choose the device to connect to
  CONNECTED     // We have connected to the device
};

void loop() {
  static AppState state = SEARCHING;
  static LBLEAddress serverAddress;

  // check if we're forcefully disconnected.
  if(state == CONNECTED)
  {
    if(!client.connected())
    {
      Serial.println("disconnected from remote device");
      state = SEARCHING;
    }
  }
  
  switch(state)
  {
  case SEARCHING:
    {
      // wait for a while
      Serial.println("state=SEARCHING");
      for(int i = 0; i < 10; ++i)
      {
        delay(1000);
        Serial.print(".");
      }
      // enumerate advertisements found.
      Serial.print("Peripherals found = ");
      Serial.println(LBLECentral.getPeripheralCount());
      Serial.println("idx\taddress\t\t\tflag\tRSSI");
      for (int i = 0; i < LBLECentral.getPeripheralCount(); ++i) {
        printDeviceInfo(i);
      }

      // waiting for user input
      Serial.println("Select the index of device to connect to: ");
      while(!Serial.available())
      {
        delay(100);
      }

      const int idx = Serial.parseInt();

      if(idx < 0 || idx >= LBLECentral.getPeripheralCount())
      {
        Serial.println("wrong index, keep scanning devices.");
      }
      else
      {
        serverAddress = LBLECentral.getBLEAddress(idx);
        Serial.print("Connect to device with address ");
        Serial.println(serverAddress);
        // we must stop scan before connecting to devices
        LBLECentral.stopScan();
        state = CONNECTING;
      }
    }
    break;
  case CONNECTING:
  {
    Serial.println("state=CONNECTING");
    client.connect(serverAddress);
    if(client.connected())
    {
      state = CONNECTED;
    }
    else
    {
      Serial.println("can't connect");
    }
  }
  break;
  case CONNECTED:
  {
    Serial.println("state=CONNECTED");

    // display all services of the remote device
    const int serviceCount = client.getServiceCount();
    Serial.println("available services = ");
    for(int i = 0; i < serviceCount; ++i)
    {
      Serial.print("\t - ");
      const String serviceName = client.getServiceName(i);
      if(serviceName.length())
      {
        Serial.print("[");
        Serial.print(serviceName);
        Serial.print("] ");
      }
      Serial.println(client.getServiceUuid(i));

      int characteristicCount = client.getCharacteristicCount(i);
      Serial.println("\t\t characteristics =");
      for (int j = 0; j < characteristicCount; j++) {
        Serial.print("\t\t - ");
        Serial.println(client.getCharacteristicUuid(i, j));
      }
    }

    // read the device manufacture - 
    // first we check if "Device Information"(0x180A) service is available:
    if(client.hasService(0x180A))
    {
      const String name = client.readCharacteristicString(LBLEUuid(0x2A29));
      if(name.length() > 0)
      {
        Serial.print("manufacturer=");
        Serial.println(name);
      }
    }
    else
    {
      Serial.println("No device information found");
    }

    // check if there is "Link Loss (0x1803)" available.
    // This service is usually used by the Proximity profile
    // https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.link_loss.xml
    if(client.hasService(0x1803))
    {
      Serial.println("Link Loss service found");
      
      // 0x2A06 (https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.alert_level.xml)
      // is a uint_8 characteristic that we can read and write
      Serial.print("Alert level = ");
      char c = client.readCharacteristicChar(0x2A06);
      Serial.println((int)c);

      Serial.println("set alert level to HIGH(2)");
      client.writeCharacteristicChar(LBLEUuid(0x2A06), 2);
    }
    
    // enter idle state.
    while(true)
    {
      delay(100);
    }
  }
  break;
  }
}

Upload the compiled code to a LinkIt 7697 HDK, connect to the same BLE peripheral as #90, and get the following output from Arduino Serial Monitor:

BLE begin
BLE ready
state=SEARCHING
..........Peripherals found = 13
idx	address			flag	RSSI
4	FC:86:7A:4E:E5:F1(RAN)	6	-45	Nordic  by Apple, Inc., service: (no service info)
Select the index of device to connect to: 
Connect to device with address FC:86:7A:4E:E5:F1(RAN)
state=CONNECTING
state=CONNECTED
available services = 
	 - [generic_access] 0x1800
		 characteristics =
		 - 0x2a00
		 - 0x2a01
		 - 0x2a04
		 - 0x2aa6
	 - [generic_attribute] 0x1801
		 characteristics =
	 - 0000003E-0000-1000-8000-0026BB765291
		 characteristics =
		 - 00000014-0000-1000-8000-0026BB765291
		 - E604E95D-A759-4817-87D3-AA005083A0D1
	 - 000000A2-0000-1000-8000-0026BB765291
		 characteristics =
		 - 00000037-0000-1000-8000-0026BB765291
		 - E604E95D-A759-4817-87D3-AA005083A0D1
	 - 00000055-0000-1000-8000-0026BB765291
		 characteristics =
		 - 0000004C-0000-1000-8000-0026BB765291
		 - E604E95D-A759-4817-87D3-AA005083A0D1
	 - 00001530-1212-EFDE-1523-785FEABCD123
		 characteristics =
		 - 000000A5-0000-1000-8000-0026BB765291
		 - E604E95D-A759-4817-87D3-AA005083A0D1
	 - 00000045-0000-1000-8000-0026BB765291
		 characteristics =
		 - 000000A5-0000-1000-8000-0026BB765291
		 - E604E95D-A759-4817-87D3-AA005083A0D1
	 - 00000044-0000-1000-8000-0026BB765291
		 characteristics =
		 - 00000005-0000-1000-8000-0026BB765291
		 - E604E95D-A759-4817-87D3-AA005083A0D1
No device information found

Scan the same device using the following node.js code:

/*
 *
 * Copyright (C) 2018 Frank Yang
 *
 * This is free software, licensed under the GNU General Public License v3.
 *
 * This code connect to the specified peripheral and perform service
 * and characteristic discovery, and then dump the result in a similar
 * way to the ConnectPeripheral.ino example from the LBLE library of
 * the LinkIt 7697 Arduino BSP package.
*/
const noble = require('noble');

var target;

if (process.argv[2]) {
	target = process.argv[2].toLowerCase();
} else {
	console.log('Usage: ' + process.argv[1] + ' peripheral_address');
	process.exit(1);
}

function connect_target_device(peripheral, target)
{
	if (peripheral.id == target || peripheral.address == target) {
		noble.stopScanning();
		explore_peripheral(peripheral);
	}
}

function explore_peripheral(peripheral)
{
	console.log('Connect to device with address: ' + peripheral.address + '(' + peripheral.addressType + ')');

	peripheral.on('disconnect', function() {
		console.log('Device ' + peripheral.address + '(' + peripheral.addressType + ') disconnected.');
		process.exit(0);
	});

	peripheral.on('exploreCompleted', function(peripheral) {
		dump_peripheral(peripheral);
	});

	peripheral.connect(function(error) {
		if (error) {
			console.log('ERROR: unable to connect to ' + peripheral.address + '(' + peripheral.addressType + '): ' + error.toString());
			process.exit(1);
		}

		discover_services(peripheral);
	});
}

function discover_services(peripheral)
{
	peripheral.service_list = {};
	
	peripheral.discoverServices([], function(error, services) {
		var svc;
		var i;

		for (i in services) {
			svc = services[i];

			peripheral.service_list[svc.uuid] = {name: svc.name, characteristics:[]};

			discover_characteristics(svc, peripheral, (i == (services.length - 1)));
		}
	});
}

function discover_characteristics(service, peripheral, last)
{
	var characteristic_list = peripheral.service_list[service.uuid].characteristics;

	service.discoverCharacteristics([], function(error, characteristics) {
		var chr;
		var i;

		for (i in characteristics) {
			chr = characteristics[i];

			characteristic_list.push(chr.uuid);

			if (last && (i == (characteristics.length - 1))) {
				peripheral.emit('exploreCompleted', peripheral);
			}
		}
	});
}

function dump_peripheral(peripheral)
{
	var i, j;

	console.log('available services =');
	for (i in peripheral.service_list) {
		if (peripheral.service_list[i].name) {
			console.log('\t - [' + peripheral.service_list[i].name + '] ' + i);
		} else {
			console.log('\t - ' + i);
		}
		console.log('\t\t characteristics =');
		for (j = 0; j < peripheral.service_list[i].characteristics.length; j++) {
			console.log('\t\t - ' +peripheral.service_list[i].characteristics[j]);
		}
	}
}

noble.on('stateChange', function(state) {
	if (state == 'poweredOn') {
		noble.startScanning();
	} else {
		noble.stopScanning();
	}
});

noble.on('discover', function(peripheral) {
	connect_target_device(peripheral, target);
});

And the output is:

$ node main.js FC:86:7A:4E:E5:F1
Connect to device with address: fc:86:7a:4e:e5:f1(random)
available services =
	 - [Generic Access] 1800
		 characteristics =
		 - 2a00
		 - 2a01
		 - 2a04
		 - 2aa6
	 - [Generic Attribute] 1801
		 characteristics =
	 - 0000003e0000100080000026bb765291
		 characteristics =
		 - e604e95da759481787d3aa005083a0d1
		 - 000000140000100080000026bb765291
		 - 000000200000100080000026bb765291
		 - 000000210000100080000026bb765291
		 - 000000230000100080000026bb765291
		 - 000000300000100080000026bb765291
		 - 000000520000100080000026bb765291
		 - 000000530000100080000026bb765291
	 - 000000a20000100080000026bb765291
		 characteristics =
		 - e604e95da759481787d3aa005083a0d1
		 - 000000370000100080000026bb765291
	 - 000000550000100080000026bb765291
		 characteristics =
		 - e604e95da759481787d3aa005083a0d1
		 - 0000004c0000100080000026bb765291
		 - 0000004e0000100080000026bb765291
		 - 0000004f0000100080000026bb765291
		 - 000000500000100080000026bb765291
	 - 000015301212efde1523785feabcd123
		 characteristics =
		 - e604e95da759481787d3aa005083a0d1
		 - 000000a50000100080000026bb765291
		 - 000015311212efde1523785feabcd123
	 - 000000450000100080000026bb765291
		 characteristics =
		 - e604e95da759481787d3aa005083a0d1
		 - 000000a50000100080000026bb765291
		 - 0000001d0000100080000026bb765291
		 - 0000001e0000100080000026bb765291
		 - 000000230000100080000026bb765291
	 - 000000440000100080000026bb765291
		 characteristics =
		 - e604e95da759481787d3aa005083a0d1
		 - 000000050000100080000026bb765291
		 - 0000001f0000100080000026bb765291
		 - 0000000e0000100080000026bb765291
		 - 000000370000100080000026bb765291
		 - 000000220000100080000026bb765291
		 - 000000010000100080000026bb765291
		 - 000000190000100080000026bb765291
		 - 0000001a0000100080000026bb765291
		 - 0000001c0000100080000026bb765291
Device fc:86:7a:4e:e5:f1(random) disconnected.

Comparing the output of these two cases, it is obvious that the scan result got from the LBLE library lacks lots of 128-bit UUID characteristics. For services containing more than 2 characteristics with 128-bit UUID, at most two of them are returned.

However, if further extending the LBLEClient class by adding methods for descriptor discovery, one can find that those missing characteristics, along with some attribute used by ATT, will be returned when waiting for BT_GATTC_DISCOVER_CHARC_DESCRIPTOR event after making a bt_gattc_discover_charc_descriptor() call, which by itself is another erroneous behavior of the LBLE library.

It looks like something is not quite right in the FreeRTOS glue code used by LBLE library.

@pablosun pablosun self-assigned this Feb 8, 2018
@pablosun pablosun added the bug label Feb 8, 2018
@pablosun
Copy link
Contributor

pablosun commented Feb 9, 2018

I can reproduce this locally. I'll check with the underlying module owners for further investigation. From the error code, it seems that bt_gattc_discover_charc reports error 0x3FFFFF6(connection in use) and callbacks with status code 0x81 when querying for the 3rd argument.

@pablosun
Copy link
Contributor

pablosun commented Feb 12, 2018

Instead of calling bt_gattc_discover_charc again after receiving BT_GATTC_DISCOVER_CHARC with BT_ATT_ERRCODE_CONTINUE, we should simply wait for successive events and keep inserting them into the characteristic list.

The attached modified version of ABLE is a quick workaround that lists all the 128-bit characteristics of the chosen device.
ABLE_multiple_event.zip

The modified version above involves:

  • A duplicated "Multiple" version of EventBlockerMultiple and waitAndProcessEventMultiple, which changes the implementation of EventBlocker::isOnce and EventBlocker::onEvent. The closure / functor object passed from client have to return a bool value that indicates whether or not all the events have arrived.
  • Change the event handler closure in discoverCharacteristicsOfService such that it returns false when receiving BT_ATT_ERRCODE_CONTINUE.

@sfyang
Copy link
Author

sfyang commented Feb 12, 2018

I can confirm that, with the "Multiple" variant of both the EventBlocker class and the corresponding waitAndProcessEvent() helper, a properly modified and extended LBLEClient class with method for descriptor discovery can now work correctly for both characteristic and descriptor discovery.

pablosun added a commit that referenced this issue Feb 14, 2018
 * #89: use `equal_range` when searching for elements in STL multimap, instead of using `find`. `find` is not guaranteed to return the first element in the equal range.

 * #90: Add a new set of interfaces to `LBLEClient` that allows user to identify a characteristic by using service index and characteristic index, instead of using UUID. Note that it is possible for a device to have multiple characteristics with the same UUID.

 * #90: Add a new example `EnumerateCharacteristic.ino` that uses the new indices-based interface of `LBLEClient` to list all the services and characteristics in the connected BLE device.

 * #90: Use `bt_gattc_read_charc` instead of `bt_gattc_read_using_charc_uuid` to read characteristics.

 * #91: when calling `bt_gattc_discover_charc`, we need to wait for multiple `BT_GATTC_DISCOVER_CHARC` event. We added new helper function `waitAndProcessEventMultiple` that supports such event waiting behavior.

 * Refactored `LBLEValueBuffer` to support re-interpreting its raw buffer content into String, float, int, and char types.
pablosun added a commit that referenced this issue Feb 26, 2018
## Bug Fixes

* #89: use `equal_range` when searching for elements in STL multimap, instead of using `find`. `find` is not guaranteed to return the first element in the equal range.

 * #90: Add a new set of interfaces to `LBLEClient` that allows user to identify a characteristic by using service index and characteristic index, instead of using UUID. Note that it is possible for a device to have multiple characteristics with the same UUID.

 * #90: Add a new example `EnumerateCharacteristic.ino` that uses the new indices-based interface of `LBLEClient` to list all the services and characteristics in the connected BLE device.

 * #90: Use `bt_gattc_read_charc` instead of `bt_gattc_read_using_charc_uuid` to read characteristics.

 * #91: when calling `bt_gattc_discover_charc`, we need to wait for multiple `BT_GATTC_DISCOVER_CHARC` event. We added new helper function `waitAndProcessEventMultiple` that supports such event waiting behavior.

 * Refactored `LBLEValueBuffer` to support re-interpreting its raw buffer content into String, float, int, and char types.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants