How to parse "Indoor Bike Data" using web bluetooth

619 views Asked by At

I was able to get the valid "Indoor Bike Data" values by using "nRF Connect for Mobile".

  • Instantaneous Speed in (km/h)
  • Instantaneous Cadence in (/min)
  • Resistance Level in (unitless)
  • Instantaneous Power in (W)

Issue is when I try to get "Indoor Bike Data" values using web bluetooth, I get the data in a DataView format which I am not sure how to parse the understandable values from.

I read some other stack overflow answers and did some random guesses and was able to get the "Resistance Level" by using below code

dataView.getInt16(6, true)

Not sure why using 6 and true was able to get the "Resistance Level"

I tried random numbers but was not able to get valid looking number for

  1. Instantaneous Speed in (km/h)
  2. Instantaneous Cadence in (/min)
  3. Instantaneous Power in (W)

Can I get help parsing above three numbers by parsing the dataView input that I am getting from indoor bike BLE device?

Thanks!

Below is the code for how I got dataView from indoor bike BLE device.

const FITNESS_MACHINE_SERVICE_UUID = "00001826-0000-1000-8000-00805f9b34fb";
const INDOOR_BIKE_DATA_UUID = "00002ad2-0000-1000-8000-00805f9b34fb";

const handleClick = async () => {
  const indoorBikeDevice = await navigator.bluetooth.requestDevice({
    filters: [{ name: "MG03" }],
    optionalServices: [FITNESS_MACHINE_SERVICE_UUID],
  });

  if (!indoorBikeDevice.gatt) return;

  const server = await indoorBikeDevice.gatt.connect();
  const service = await server.getPrimaryService(FITNESS_MACHINE_SERVICE_UUID);
  const characteristic = await service.getCharacteristic(INDOOR_BIKE_DATA_UUID);

  characteristic.addEventListener(
    "characteristicvaluechanged",
    async (event) => {
      const dataView = (event.target as any).value as DataView;
      console.log("dataView: ", dataView);

      const resistanceLevel = dataView.getInt16(6, true);
      console.log("resistanceLevel: ", resistanceLevel);
    }
  );

  characteristic.startNotifications();
};


BELOW IS AFTER LOOKING AT RESPONSE FROM @Michael Kotzjan

I looked at the link @Michael Kotzjan provided and after few trials I was able to get flags by running code below

// GATT_Specification_Supplement_v8.pdf
// 3.124.1 Flags field: The bits of this field are defined below.
for (let i = 0; i < 16; i++) {
  console.log("flags[" + i + "] = " + !!((flags >>> i) & 1));
}

console.log looked like below:

// flags[0] = false
// flags[1] = false
// flags[2] = true (Instantaneous Cadence present)
// flags[3] = false
// flags[4] = false
// flags[5] = true (Resistance Level present)
// flags[6] = true (Instantaneous Power present)
// flags[7] = false
// flags[8] = false
// flags[9] = false
// flags[10] = false
// flags[11] = false
// flags[12] = false
// ...

It seems like above true flag values are telling me that Instantaneous Cadence present, Resistance Level present, and Instantaneous Power present are available.

My issue was getting the value of those field and matching the value to the data from "nRF Connect for Mobile".

I blindly guessed numbers without any understanding and was able to match output numbers to "nRF Connect for Mobile" with the code below

    characteristic.addEventListener(
      "characteristicvaluechanged",
      async (event) => {
        const dataView = (event.target as any).value as DataView;

        const instantaneousCadence = dataView.getUint16(3, true) / 512;
        const resistanceLevel = dataView.getUint8(6);
        const instantaneousPower = dataView.getInt16(8, true);

        console.log(
          [instantaneousCadence, resistanceLevel, instantaneousPower].join("|")
        );
      }
    );

Even if I got the desired number, I still want to know why it worked?

For example, for the cadence: dataView.getUint16(3, true) / 512 why is the byte offset: 3 and I need to divided by 512? to get the rev/min?

byte offsets for resistance level and power are 6 and 8 and I am not sure where and how to get byte offsets?

2

There are 2 answers

1
Michael Kotzjan On BEST ANSWER

You can search for your service in the Assigned Numbers Document provided by the Bluetooth SIG. Your INDOOR_BIKE_DATA_UUID is a standard UUID with the 16-Bit representation of 0x2ad2. The Assigned Numbers Document shows this UUID as Indoor Bike Data, which is part of the Fitness Machine Service. The service specification contains a section regarding Indoor Bike Data:

The Indoor Bike Data characteristic is used to send training-related data to the Client from an indoor bike (Server). Included in the characteristic value is a Flags field (for showing the presence of optional fields), and depending upon the contents of the Flags field, it may include one or more optional fields as defined on the Bluetooth SIG Assigned Numbers webpage.

That means you need to read out the flags field to figure out which data fields are present on your bike and handle them accordingly. All information about the types and lengths of the data fields can be found in the documentation.

0
Dave Norfleet On

Sorry I just saw your question. I bet you've already answered your followup but thought I'd post the little I know for future readers. I'm no expert, but this is my best recount of what I've learned in reading data from FTMS equipment. The UUID for the indoor bike via FTMS is 0x2ad2 - as @MichaelKotzjan mentions. Once listening for this data (which you seem to be doing just fine) it takes a little ciphering' to get usable data. This page lays out the flag + data structure better than anything else I found:

https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides/ibd-0000001051005923

Table 1 lists the flag fields, indicating which values will follow, then Table 2 lists the values themselves with their length/type and any indication on how the value needs to be handled. For instance, Instantaneous Cadence uint16 0.5 /min means you should read 16 bits then divide by 2 for RPM, as the returned value will be 0.5 rev per min. Also, Instantaneous Speed uint16 0.01 Km/h means you should read 16 bits then divide by 100 to get Km/h and divide by 1.609 to get mph. Lastly, if the flags indicate a value is not there, don't count it as you number the offset. For instance, Ave Cadence would start after the 48th bit (3 * 16) if Ins Speed & Ave Speed & Inst Cad were all there, but would start after the 32nd bit (2 * 16) if only Ins Speed & Save Speed were present. This javascript example shows you how to step through the data for a heart rate monitor.

parseHeartRate(value) {
      // DataView return
      value = value.buffer ? value : new DataView(value);
      let flags = value.getUint8(0);
      let rate16Bits = flags & 0x1;
      let result = {};
      let index = 1;
      if (rate16Bits) {
        result.heartRate = value.getUint16(index, /*littleEndian=*/true);
        index += 2;
      } else {
        result.heartRate = value.getUint8(index);
        index += 1;
      }
      let contactDetected = flags & 0x2;
      let contactSensorPresent = flags & 0x4;
      if (contactSensorPresent) {
        result.contactDetected = !!contactDetected;
      }
      let energyPresent = flags & 0x8;
      if (energyPresent) {
        result.energyExpended = value.getUint16(index, /*littleEndian=*/true);
        index += 2;
      }
      let rrIntervalPresent = flags & 0x10;
      if (rrIntervalPresent) {
        let rrIntervals = [];
        for (; index + 1 < value.byteLength; index += 2) {
          rrIntervals.push(value.getUint16(index, /*littleEndian=*/true));
        }
        result.rrIntervals = rrIntervals;
      }
      return result;
    }

Hope that helps you or some future reader!