javascript - Cum să analizezi „Datele bicicletei de interior” folosind web bluetooth


Am reușit să obțin valorile valide „Indoor Bike Data” folosind „nRF Connect for Mobile”.

  • Viteză instantanee în (km/h)
  • Cadență instantanee în (/min)
  • Nivel de rezistență (fără unități)
  • Putere instantanee în (W)

Problema este că atunci când încerc să obțin valorile „Indoor Bike Data” folosind web bluetooth, primesc datele într-un format DataView și nu sunt sigur cum să parsez valorile într-un mod înțeles.

Am citit câteva alte răspunsuri de pe Stack Overflow și am făcut niște presupuneri aleatorii și am reușit să obțin „Nivelul de Rezistență” folosind codul de mai jos

dataView.getInt16(6, true)

Nu sunt sigur de ce folosirea lui 6 și true a reușit să obțină „Nivelul de rezistență”

Am încercat numere aleatorii, dar nu am reușit să obțin un număr care să arate valid pentru

  1. Viteză instantanee în (km/h)
  2. Cadență instantanee în (/min)
  3. Putere instantanee în (W)

Pot primi ajutor pentru a analiza cele trei numere de mai sus prin analizarea inputului dataView pe care îl primesc de la dispozitivul BLE pentru bicicleta de interior?

Mulțumesc!

Mai jos este codul pentru cum am obținut dataView de la dispozitivul BLE de bicicletă de interior.

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();
};


MAI JOS ESTE DUPĂ CE AM PRIVIT RĂSPUNSUL DE LA @Michael Kotzjan

Am aruncat o privire la linkul oferit de @Michael Kotzjan și după câteva încercări am reușit să obțin steaguri rulând codul de mai jos

// 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 arăta astfel:

// 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
// ...

Se pare că valorile de mai sus ale flagului true îmi spun că Cadenta instantanee prezentă, Nivelul rezistenței prezent și Puterea instantanee prezentă sunt disponibile.

Problema mea a fost să obțin valoarea acelor câmpuri și să potrivesc valoarea cu datele din „nRF Connect for Mobile”.

Am ghicit orb numere fără nicio înțelegere și am reușit să potrivesc numerele de ieșire cu „nRF Connect for Mobile” folosind codul de mai jos

    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("|")
        );
      }
    );

Chiar dacă am obținut numărul dorit, vreau totuși să știu de ce a funcționat?

De exemplu, pentru cadența: dataView.getUint16(3, true)/512 de ce este offset-ul de octet: 3 și de ce trebuie să împart la 512? Pentru a obține rotații pe minut?

Deplasările în octeți pentru nivelul de rezistență și putere sunt 6 și 8 și nu sunt sigur unde și cum să obțin deplasările în octeți?

Răspuns #1

Puteți căuta serviciul dumneavoastră în Documentul cu Numere Atribuite furnizat de Bluetooth SIG. INDOOR_BIKE_DATA_UUID este un UUID standard cu reprezentarea pe 16 biți 0x2ad2. Documentul cu Numere Atribuite arată acest UUID ca Indoor Bike Data, care face parte din Serviciul Fitness Machine. Specificația serviciului conține o secțiune referitoare la Indoor Bike Data:

Caracteristica Indoor Bike Data este utilizată pentru a trimite date legate de antrenament către Client de la o bicicletă indoor (Server). În valoarea caracteristicii este inclus un câmp Flags (pentru a arăta prezența câmpurilor opționale) și, în funcție de conținutul câmpului Flags, poate include unul sau mai multe câmpuri opționale așa cum sunt definite pe pagina web a numerelor atribuite de Bluetooth SIG.

Asta înseamnă că trebuie să citești câmpul de flaguri pentru a afla care câmpuri de date sunt prezente pe bicicleta ta și să le gestionezi corespunzător. Toate informațiile despre tipurile și lungimile câmpurilor de date pot fi găsite în documentație.

Răspunsul #2

Îmi pare rău, abia am văzut întrebarea ta. Pariez că ai răspuns deja la următoarea întrebare, dar m-am gândit să postez puțin ceea ce știu pentru cititorii viitori. Nu sunt expert, dar acesta este cel mai bun rezumat al ceea ce am învățat citind date de la echipamentele FTMS. UUID-ul pentru bicicleta de interior prin FTMS este 0x2ad2 - așa cum menționează @MichaelKotzjan. Odată ce asculți aceste date (ceea ce pare că faci foarte bine) este nevoie de puțină decriptare pentru a obține date utilizabile. Această pagină explică structura steagului + a datelor mai bine decât orice altceva am găsit:

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

Tabelul 1 listează câmpurile de flaguri, indicând ce valori vor urma, apoi Tabelul 2 listează valorile în sine, împreună cu lungimea/tipul lor și orice indicație despre modul în care trebuie tratată valoarea. De exemplu, Instantaneous Cadence uint16 0,5 /min înseamnă că ar trebui să citiți 16 biți apoi să împărțiți la 2 pentru RPM, deoarece valoarea returnată va fi de 0,5 rotații pe minut. De asemenea, Instantaneous Speed uint16 0,01 Km/h înseamnă că ar trebui să citiți 16 biți apoi să împărțiți la 100 pentru a obține Km/h și să împărțiți la 1,609 pentru a obține mph. În cele din urmă, dacă flagurile indică faptul că o valoare nu este prezentă, nu o numărați atunci când calculați offset-ul. De exemplu, Ave Cadence ar începe după al 48-lea bit (3 * 16) dacă Ins Speed & Ave Speed & Inst Cad ar fi toate prezente, dar ar începe după al 32-lea bit (2 * 16) dacă ar fi prezente doar Ins Speed & Ave Speed. Acest exemplu în javascript arată cum să parcurgeți datele pentru un monitor de ritm cardiac.

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;
    }

Sper că asta te ajută pe tine sau pe un cititor viitor!