1

I am using the Sample CustomGATTSvc code to familiarise myself with the GATT interface on the Movesense pod and have run into a problem when trying to add more services to the code. My project utlimately requires the ability to:

  1. Get and Set the RTC on the pod
  2. Use Datalogger and Logbook to store and retrieve accelerometer data.

All of this must be achieved using the GATT interface as we are looking to develop the mobile app in Cordova, which I understand does not have the Movesense library support.

As a first step I have tried to add a second service to the Health Thermometer Service that already exists in the sample code. I looked to implement the Current Time Service (everything related to Current Time Service is encased in a #define CURRENT_TIME_SVC) -

#define THERMOMETER_SERV_UUID           0x1809  // Health Thermometer
#define CURRENT_TIME_SERV_UUID          0x1805  // Current Time Service

const uint16_t measCharUUID16 = 0x2A1C;
const uint16_t intervalCharUUID16 = 0x2A21;
const uint16_t healthThermometerSvcUUID16 = THERMOMETER_SERV_UUID; // Health Temperature probe

#ifdef CURRENT_TIME_SVC
const uint16_t timeCharUUID16 = 0x2A2C; //Random UUID for Current Time
const uint16_t timeSvcUUID16 = CURRENT_TIME_SERV_UUID;  //Current Time
#endif

In the configGattSvc() function I configure the services and characteristics as follows:

  WB_RES::GattSvc customGattThermometerSvc;
  WB_RES::GattChar characteristics[2];
  WB_RES::GattChar &measChar = characteristics[0];
  WB_RES::GattChar &intervalChar = characteristics[1];
  //const uint16_t healthThermometerSvcUUID16 = 0x1809;

#ifdef CURRENT_TIME_SVC
  WB_RES::GattSvc customGattTimeSvc;
  WB_RES::GattChar characteristicsTime[1];
  WB_RES::GattChar &timeChar = characteristicsTime[0];
  //const uint16_t timeSvcUUID16 = 0x1805;
#endif

  // Define the CMD characteristics
  WB_RES::GattProperty measCharProp = WB_RES::GattProperty::INDICATE;
  WB_RES::GattProperty intervalCharProps[2] = {WB_RES::GattProperty::READ, WB_RES::GattProperty::WRITE};
  WB_RES::GattProperty timeCharProps[3] = {WB_RES::GattProperty::READ, WB_RES::GattProperty::WRITE, WB_RES::GattProperty::NOTIFY};

  measChar.props = whiteboard::MakeArray<WB_RES::GattProperty>( &measCharProp, 1);
  measChar.uuid = whiteboard::MakeArray<uint8_t>( reinterpret_cast<const uint8_t*>(&measCharUUID16), 2);

  intervalChar.props = whiteboard::MakeArray<WB_RES::GattProperty>( intervalCharProps, 2);
  intervalChar.uuid = whiteboard::MakeArray<uint8_t>( reinterpret_cast<const uint8_t*>(&intervalCharUUID16), 2);
  intervalChar.initial_value = whiteboard::MakeArray<uint8_t>( reinterpret_cast<const uint8_t*>(&mMeasIntervalSecs), 2);

#ifdef CURRENT_TIME_SVC
  timeChar.props = whiteboard::MakeArray<WB_RES::GattProperty>( timeCharProps, 3);
  timeChar.uuid = whiteboard::MakeArray<uint8_t>( reinterpret_cast<const uint8_t*>(&timeCharUUID16), 2);
  timeChar.initial_value = whiteboard::MakeArray<uint8_t>( reinterpret_cast<const uint8_t*>(&mTimeSecs), 2);
#endif

  // Combine chars to service
  customGattThermometerSvc.uuid = whiteboard::MakeArray<uint8_t>( reinterpret_cast<const uint8_t*>(&healthThermometerSvcUUID16), 2);
  customGattThermometerSvc.chars = whiteboard::MakeArray<WB_RES::GattChar>(characteristics, 2);

  // Create custom service
  asyncPost(WB_RES::LOCAL::COMM_BLE_GATTSVC(), AsyncRequestOptions::Empty, customGattThermometerSvc);

#ifdef CURRENT_TIME_SVC
  // Combine Time chars to service
  customGattTimeSvc.uuid = whiteboard::MakeArray<uint8_t>( reinterpret_cast<const uint8_t*>(&timeSvcUUID16), 2);
  customGattTimeSvc.chars = whiteboard::MakeArray<WB_RES::GattChar>(characteristicsTime, 1);
  // Create custom service
  asyncPost(WB_RES::LOCAL::COMM_BLE_GATTSVC(), AsyncRequestOptions::Empty, customGattTimeSvc);
#endif

In onGetResult I have extended the code to incorporate the Current Time Service and Characteristic subscription as follows:

void CustomGATTSvcClient::onGetResult(whiteboard::RequestId requestId, whiteboard::ResourceId resourceId, whiteboard::Result resultCode, const whiteboard::Value& rResultData)
{
  DEBUGLOG("CustomGATTSvcClient::onGetResult");
  switch(resourceId.localResourceId)
  {
    case WB_RES::LOCAL::COMM_BLE_GATTSVC_SVCHANDLE::LID:
    {
      const WB_RES::GattSvc &svc = rResultData.convertTo<const WB_RES::GattSvc &>();
      for (size_t i=0; i<svc.chars.size(); i++) {
        const WB_RES::GattChar &c = svc.chars[i];
        uint16_t uuid16 = *reinterpret_cast<const uint16_t*>(&(c.uuid[0]));

        if(uuid16 == measCharUUID16)
        mMeasCharHandle = c.handle.hasValue() ? c.handle.getValue() : 0;
        else if(uuid16 == intervalCharUUID16)
        mIntervalCharHandle = c.handle.hasValue() ? c.handle.getValue() : 0;
        #ifdef CURRENT_TIME_SVC
        else if(uuid16 == timeCharUUID16)
        mTimeCharHandle = c.handle.hasValue() ? c.handle.getValue() : 0;
        #endif
      }

      if (!mIntervalCharHandle || !mMeasCharHandle)
      {
        DEBUGLOG("ERROR: Not all chars were configured!");
        return;
      }
#ifdef CURRENT_TIME_SVC
      if (!mTimeCharHandle)
      {
        DEBUGLOG("ERROR: Not all chars were configured!");
        return;
      }
#endif
      char pathBuffer[32]= {'\0'};
      snprintf(pathBuffer, sizeof(pathBuffer), "/Comm/Ble/GattSvc/%d/%d", mTemperatureSvcHandle, mIntervalCharHandle);
      getResource(pathBuffer, mIntervalCharResource);
      snprintf(pathBuffer, sizeof(pathBuffer), "/Comm/Ble/GattSvc/%d/%d", mTemperatureSvcHandle, mMeasCharHandle);
      getResource(pathBuffer, mMeasCharResource);
#ifdef CURRENT_TIME_SVC
      snprintf(pathBuffer, sizeof(pathBuffer), "/Comm/Ble/GattSvc/%d/%d", mTimeSvcHandle, mTimeCharHandle);
      getResource(pathBuffer, mTimeCharResource);
#endif

      // Subscribe to listen to intervalChar notifications (someone writes new value to intervalChar)
      asyncSubscribe(mIntervalCharResource, AsyncRequestOptions::Empty);
      // Subscribe to listen to measChar notifications (someone enables/disables the INDICATE characteristic)
      asyncSubscribe(mMeasCharResource, AsyncRequestOptions::Empty);
#ifdef CURRENT_TIME_SVC
      // Subscribe to listen to timeChar notifications (someone writes new value to timeChar)
      asyncSubscribe(mTimeCharResource, AsyncRequestOptions::Empty);
#endif
    }
    break;

    case WB_RES::LOCAL::MEAS_TEMP::LID:
    {
      // Temperature result or error
      if (resultCode == whiteboard::HTTP_CODE_OK)
      {
        WB_RES::TemperatureValue value = rResultData.convertTo<WB_RES::TemperatureValue>();
        float temperature = value.measurement;

        // Convert K to C
        temperature -= 273.15;

        // Return data
        //uint8_t buffer[5]; // 1 byte or flags, 4 for FLOAT "in Celsius" value
        uint8_t buffer[5];
        buffer[0]=0;
        // convert normal float to IEEE-11073 "medical" FLOAT type into buffer
        floatToFLOAT(temperature, &buffer[1]);

        // Write the result to measChar. This results INDICATE to be triggered in GATT service
        WB_RES::Characteristic newMeasCharValue;
        newMeasCharValue.bytes = whiteboard::MakeArray<uint8_t>(buffer, sizeof(buffer));
        asyncPut(WB_RES::LOCAL::COMM_BLE_GATTSVC_SVCHANDLE_CHARHANDLE(), AsyncRequestOptions::Empty, mTemperatureSvcHandle,
        mMeasCharHandle, newMeasCharValue);
      }
    }
    break;
#ifdef CURRENT_TIME_SVC
    case WB_RES::LOCAL::TIME::LID:
    {
      // Return with the RTC Time
      if (resultCode == whiteboard::HTTP_CODE_OK)
      {
        WB_RES::DetailedTime value = rResultData.convertTo<WB_RES::DetailedTime>();
        int64 tm = value.utcTime;
        uint8_t buffer[2];
        // Could have an endian issue here, will have to check once connection works
        buffer[0] - (tm&0xFF00)>>8;
        buffer[1] = tm &0xFF;
        // Here we need to get the current time and then Put it back to the device connected

        WB_RES::Characteristic newTimeCharValue;
        newTimeCharValue.bytes = whiteboard::MakeArray<uint8_t>(buffer, sizeof(buffer));
        asyncPut(WB_RES::LOCAL::COMM_BLE_GATTSVC_SVCHANDLE_CHARHANDLE(), AsyncRequestOptions::Empty, mTimeSvcHandle,
        mTimeCharHandle, newTimeCharValue);
      }
    }
    break;
#endif
  }
}

I have added the Time Local Resource in the onGetResult as I'm still not certain how I will be able to "get" the RTC time from the pod. How do I access the /Time resource?

onNotify has been amended as follows:

void CustomGATTSvcClient::onNotify(whiteboard::ResourceId resourceId, const whiteboard::Value& value, const whiteboard::ParameterList& rParameters)
{
  switch(resourceId.localResourceId)
  {
    case WB_RES::LOCAL::COMM_BLE_GATTSVC_SVCHANDLE_CHARHANDLE::LID:
    {
      WB_RES::LOCAL::COMM_BLE_GATTSVC_SVCHANDLE_CHARHANDLE::SUBSCRIBE::ParameterListRef parameterRef(rParameters);
      if (parameterRef.getCharHandle() == mIntervalCharHandle)
      {
        const WB_RES::Characteristic &charValue = value.convertTo<const WB_RES::Characteristic &>();
        uint16_t interval = *reinterpret_cast<const uint16_t*>(&charValue.bytes[0]);
        DEBUGLOG(": mMeasCharResource: len: %d, new interval: %d", charValue.bytes.size(), interval);
        // Update the interval
        if (interval >= 1 && interval <= 65535)
        mMeasIntervalSecs = interval;
        // restart timer if exists
        if (mMeasurementTimer != whiteboard::ID_INVALID_TIMER) {
          stopTimer(mMeasurementTimer);
          mMeasurementTimer = startTimer(mMeasIntervalSecs*1000, true);
        }
      }
      else if (parameterRef.getCharHandle() == mMeasCharHandle)
      {
        const WB_RES::Characteristic &charValue = value.convertTo<const WB_RES::Characteristic &>();
        bool bNotificationsEnabled = charValue.notifications.hasValue() ? charValue.notifications.getValue() : false;
        DEBUGLOG(": mMeasCharHandle. bNotificationsEnabled: %d", bNotificationsEnabled);
        // Start or stop the timer
        if (mMeasurementTimer != whiteboard::ID_INVALID_TIMER)
        {
          stopTimer(mMeasurementTimer);
          mMeasurementTimer = whiteboard::ID_INVALID_TIMER;
        }
        if (bNotificationsEnabled)
        mMeasurementTimer = startTimer(mMeasIntervalSecs*1000, true);
      }
#ifdef CURRENT_TIME_SVC
      else if (parameterRef.getCharHandle() == mTimeCharHandle)
      {
        // Received Time information!
        const WB_RES::Characteristic &charValue = value.convertTo<const WB_RES::Characteristic &>();
        uint16_t tm = *reinterpret_cast<const uint16_t*>(&charValue.bytes[0]);
        DEBUGLOG(": mMeasCharResource: len: %d, new interval: %d", charValue.bytes.size(), tm);
        // Update the interval
        mTimeSecs = tm;
      }
#endif
    }
    break;
  }
}

So far I think the code should be correct and working, but I have a problem with the last snippet of code from onPostResult:

void CustomGATTSvcClient::onPostResult(whiteboard::RequestId requestId, whiteboard::ResourceId resourceId, whiteboard::Result resultCode, const whiteboard::Value& rResultData)
{
  DEBUGLOG("CustomGATTSvcClient::onPostResult: %d", resultCode);
  if (resultCode == whiteboard::HTTP_CODE_CREATED) {
#if 1
  // This is the code that I propose using when having more than one service but it doesn't seem to work.

    const WB_RES::GattSvc &svc = rResultData.convertTo<const WB_RES::GattSvc &>();
    uint16_t uuid16 = *reinterpret_cast<const uint16_t*>(&(svc.uuid[0]));

    if(uuid16 == healthThermometerSvcUUID16) {
      mTemperatureSvcHandle = (int32_t)rResultData.convertTo<uint16_t>();
      DEBUGLOG("Custom Gatt service was created. handle: %d", mTemperatureSvcHandle);
      asyncGet(WB_RES::LOCAL::COMM_BLE_GATTSVC_SVCHANDLE(), AsyncRequestOptions::Empty, mTemperatureSvcHandle);
    }
    #ifdef CURRENT_TIME_SVC
    else if(uuid16 == timeSvcUUID16) {
      mTimeSvcHandle = (int32_t)rResultData.convertTo<uint16_t>();
      DEBUGLOG("Custom Gatt service was created. handle: %d", mTimeSvcHandle);
      asyncGet(WB_RES::LOCAL::COMM_BLE_GATTSVC_SVCHANDLE(), AsyncRequestOptions::Empty, mTimeSvcHandle);
    }
    #endif
#else
    // This is the code that does work with a single Service but is doesn't work as soon as I add a second service.
    // Custom Gatt service was created
    mTemperatureSvcHandle = (int32_t)rResultData.convertTo<uint16_t>();
    DEBUGLOG("Custom Gatt service was created. handle: %d", mTemperatureSvcHandle);

    // Request more info about created svc so we get the char handles
    asyncGet(WB_RES::LOCAL::COMM_BLE_GATTSVC_SVCHANDLE(), AsyncRequestOptions::Empty, mTemperatureSvcHandle);
#endif
  }
}

You will see that I have a #if 1 ..(code1).. #else ..(code2).. #endif in this snippet of code. The code in the first #if 1 section (code1) is what I have written to work with the two GATT Services that the code creates. I feel that since there are two services, a test must be conducted (with the service UUID) to determine which service is being addressed. The code after the #else (code2) is what comes from the original sample code that just used the health temp service.

When I compile using (code1), everything compiles fine, but I can't seem to Subscribe to the health-temperature service. When I switch to using (code2), the health temperature service works fine and I can Subscribe to it without a problem.

To test the GATT interface I am using Bluetility, a Bluetooth Low Energy browser. https://github.com/jnross/Bluetility

My questions are as follows:

  1. All my code compiles fine, but as soon as I use (code1) I cannot subscribe to and get the Health Temp Service to work. What am I doing wrong? (code2) wont work since with more than one service since it only works with the Health Temp service.
  2. I have started to implement the Current Time Service but until I fix Q1 above, uncertain how to implement the code to get and set the current time.
  3. Once successful with the above, I'll start figuring out how I can add another service that will gain access to the Accelerometer Resource ("/Meas/Acc/13") and also use the Datalogger to store the Accelerometer data and Logbook to extract it in that same GATT Service.

I would appreciate anyone's help that is able to point me on the right path to achieving my ultimate goal. Thank in advance.

phuclv
  • 37,963
  • 15
  • 156
  • 475
vkh
  • 11
  • 2
  • Did you manage to create the other service to access the Accelerometer Resource ("/Meas/Acc/13")? I am also trying to do that but can't figure it out which gatt service and characteristic codes to use – Jorge Jiménez Jul 04 '19 at 12:25

1 Answers1

0

I think you have type mismatch in the response handling.

void CustomGATTSvcClient::onPostResult(...)
    {
      if (resultCode == whiteboard::HTTP_CODE_CREATED) {
      const WB_RES::GattSvc &svc = rResultData.convertTo<const WB_RES::GattSvc &>();

-> https://bitbucket.org/suunto/movesense-device-lib/src/master/MovesenseCoreLib/resources/movesense-api/comm/ble_gattsvc.yaml

defines /Comm/Ble/GattSvc POST request with code 201(created) response type to be GattSvcHandle which is integer instead of struct WB_RES::GattSvc

hence also the following comparision logic fails.

Solution proposal: Can you try for your comparision logic in onPostResult:

if(mTemperatureSvcHandle == 0) {
      mTemperatureSvcHandle = (int32_t)rResultData.convertTo<uint16_t>();
      asyncGet(WB_RES::LOCAL::COMM_BLE_GATTSVC_SVCHANDLE(), AsyncRequestOptions::Empty, mTemperatureSvcHandle);
    }
    else {
      mTimeSvcHandle = (int32_t)rResultData.convertTo<uint16_t>();
      asyncGet(WB_RES::LOCAL::COMM_BLE_GATTSVC_SVCHANDLE(), AsyncRequestOptions::Empty, mTimeSvcHandle);}

(This solution is just a workaround to test if this solves your issues in running two service simultaneously, as this does not take account that the post is asynchronous and in the end you'll probably need some more elegant way of identifying the service related to the postResult)