Web Bluetooth on MetaMotionR

I'm trying to create a web app using the web bluetooth framework to connect to the device and visualise gyroscope and accelerator data to the browser  in real time.. afterwards i want to send the captured data to a database of some sort.  I realise that the device  uses a custom protocol, not standard services and characteristics and  there is no documentation about the GATT or anything related to the matter. So i'm trying to reverse engineer ,using wireshark and a ble-sniffer the connection, between the metabase app and the device. I got how to turn on the sensors and start them sending me notifications. 

 The values that i get from the notification from only accelerator turned on are something like: 




I don't have a clue on how to decode these values into actual accelerator values. Tried to split the value by 2,4,8 bytes and try to get the float32 of them  but nothing seems to make sense..the only thing i know is that the accelerator sends a 16bit value.. Anyone who has an idea how to decode this or how even the device encodes this and by what criteria would be savior of mine.. 

Thanks everyone in advance who may take look into this !    


  • edited March 2020

    Hey @Dimark1394,

    We use a completely custom protocol called MetaWear over the Bluetooth link. It doesn't exactly use the same format you would see on a heart rate monitor with expected characteristics and addresses.

    That means, you must use our APIs to interface with the sensor. However, we have provided skeleton APIs where the interaction layers are present but you can plug in the bluetooth library of your choice. You should therefore take a look at our C and C# libraries. The Javascript library could potentially be used as well.

    Can I also ask what you are trying to accomplish? You want to visualize data in the browser but you can already do that if you simply upload the sensor data in the cloud through your smartphone. I.E our MetaCloud.

    If you want to be able to interact with the sensor through the browser directly (set it to log and download the data when done downloading) then you will have to use our APIs as I mentioned above.

  • Well this project is part of a university project which will be included under my thesis ! The goal is to be able to interact with the device directly from a browser through the Web Bluetooth framework and send those data to a data base for other experiments!

    So what you are saying is that there is no way of doing this using this method? I ve actually found a connection between the temperature data and the values received from the notifications as they are much simpler to decode! I just need a similar connection with acceleretor and the gyr values from the notifications so i can change them to actual values!
    In short browser connects to the device , you click a button it starts logging , visualise the data in real time on browser (i.e charts) , end session then send the capturerd data to a custom database!

    Thanks again for your response!

  • Yes, as I said, it is possible but you need to use our APIs for the BLE packet decoding.

  • Hi, my first post, so please be gentle as I don't come from a coding background, but more design. My goal is to do a few experiments with the MetaWear device using it as a game controller. I would like to do so in the browser by connecting via Web Bluetooth API. As I don't want to make an app, I am struggling to use the official SDK. First experiment is reasonably successful: https://www.youtube.com/watch?v=T0RrmM0LYgc

    As far as I can tell I am picking up the stream from Sensor Fusion set to NDOF (Nine Degrees of Freedom) Output of Euler Angles. I am accessing this service from the device: 326a9000-85cb-9195-d9dd-464cfbbae75a, then getting data from this characteristic: 326a9006-85cb-9195-d9dd-464cfbbae75a.

    The data view (if I'm using this term correctly) in hexadecimal is for example: 0x1908700c82438ba32cc068449ec0700c8243 As it's streaming this value is constantly changing... except for the first four 1908 which I'm guessing is the Sensor Fusion and Euler? then come 8 numbers/letters which are the same as the last 8 in the sequence... don't know why? Which leaves the middle 8.

    Through trial and error I have made my experiment work, but I don't know if my deductions are correct or if it's a total fluke:
    Skipping the first 6 (pairs – bytes?) values (littleEndian set to true?) for Roll: value.getFloat32(6, 4, true);
    Then getting the middle 4 values for Pitch: value.getFloat32(10, 4, true);
    Then getting the final set of 4 values for Yaw: value.getFloat32(14, 4, true);

    (although the four values after 1908 are the same as the last four, I can't get them to work as Yaw)

    The short and long of it is this: Is there a cheat sheet for data values from the sensor, so instead of trial and error, I can find out how to get data from, for example the accelerometer? 'first 4 values for X, next 4 values for Y' etc. Should the values be obtained with Float32 or getUint8 or other? Is there some documentation for this?

    I can't figure out how to pick up the motion data. The ultimate aim would be to mimic device motion and orientation on screen in 3d.

    Thank you for your help.

    Other things I looked at: Missing w of Quaterion in Sensor Fusion @JpEncausse (I haven't managed to get Qurtenions to work yet)





  • edited April 2022

    The value from characteristic 326a9006-85cb-9195-d9dd-464cfbbae75a for linear acceleration seems to start with 19 0A as the first two bytes of the hex code. I only applied x and y to the cube, locking the z axis as it was drifting too much.


    At the moment it looks like it's just acting like a spring, bouncing the cube back when acceleration gets closer to zero. Not sure how to convert it into position that is retained, without bounce-back effect.

    Otherwise it seems similar to Euler angles with acceleration x = event.target.value.getFloat32(6, 4, true);
    acceleration y = event.target.value.getFloat32(10, 4, true); and acceleration in z = event.target.value.getFloat32(2, 4, true);

  • @glebski

    Our Cpp SDK is considered the reference implementation for the MetaWear protocol, everything you can want to know about the protocol is there, written in code.

    If there is really no way to do Cpp bindings for your language, I would highly recommend picking one of our scripting language SDKs, that way you can play with the configuration and settings for your application, and then see what the corresponding commands and responses are.

    To at least answer some basic protocol questions, the first byte is always the driver/module endpoint, the second byte a register endpoint for that driver module.

    Accel/Gyro data is 3-tuples of 16-bit little endian signed integers -- (x,y,z).
    Euler angles are 4-tuples of 32-bit little endian floats -- (heading, pitch, roll, yaw).
    Quaternions are 4-tuples of 32-bit little endian floats -- (w, x, y, z).
    Linear accel is a 3-tuple of 32-bit little endian floats -- (x, y, z).

    Here are some examples of where you would see quaternion related data types in the Cpp SDK.


  • Thank you @Matt that's very useful to know. I have managed to implement quaternions and trigger the LED as seen here https://www.youtube.com/watch?v=raY-X5jkJVA, but still feel that it would be good to have a go-to source or documentation to know what each tuple does (I hope I'm using tuple correctly in this context).

    For example, now I would like to trigger an event with the mechanical switch. I'm sure I can find out which BLE service corresponds to it, and I believe the module endpoint is 01, but if there was a reference for it, then it would be much easier.

    Thanks again. I'll carry on experimenting.

  • Would you be willing to share your code that you used to show the orientation of the sensor from quaternions?

  • Hi @mhcohen56 sure, although I'm not sure what I'm doing is 100% correct.

            {filters: [{name: 'MetaWear'}],
              optionalServices: ['0000180a-0000-1000-8000-00805f9b34fb', '0000180f-0000-1000-8000-00805f9b34fb', '326a9000-85cb-9195-d9dd-464cfbbae75a'],
          .then(device => {
            return device.gatt.connect();
          .then(server => {
            return server.getPrimaryServices();
          .then(services => {
            services.forEach(service => {
                service.getCharacteristics().then(characteristics => {
                    characteristics.forEach(characteristic => {
                      if (characteristic.uuid === '326a9006-85cb-9195-d9dd-464cfbbae75a') {
                        window.coordinates = characteristic;                
                        window.coordinates.addEventListener('characteristicvaluechanged', coordinatesChanged);
                      if (characteristic.uuid === '326a9001-85cb-9195-d9dd-464cfbbae75a') {
                        window.led = characteristic;
                      if (characteristic.uuid === '00002a19-0000-1000-8000-00805f9b34fb') {
                        window.battery = characteristic;                
                        window.battery.addEventListener('characteristicvaluechanged', batteryChanged);
        function coordinatesChanged(event) {
          let value = event.target.value;
          let a = [];
          // Convert raw data bytes to hex values
          for (let i = 0; i < value.byteLength; i++) {
            a.push(('00' + value.getUint8(i).toString(16)).slice(-2));
          if (a[0] === '19' && a[1] === '07') {
            document.querySelector('#valueHEX').textContent = 'Quartenion: ' + ' ' + a.join(' ');
            let w = event.target.value.getFloat32(14, 4, true);
            document.querySelector('#w').textContent = w.toFixed(2);
            let qx = event.target.value.getFloat32(6, 4, true);
            document.querySelector('#qx').textContent = qx.toFixed(2);
            let qy = event.target.value.getFloat32(2, 4, true);
            document.querySelector('#qy').textContent = qy.toFixed(2);
            let qz = event.target.value.getFloat32(10, 4, true);
            document.querySelector('#qz').textContent = qz.toFixed(2);
         if (a[0] === '19' && a[1] === '0a') {       
          document.querySelector('#valueHEX').textContent = 'Linear Acceleration: ' + ' ' + a.join(' ');
            let y = event.target.value.getFloat32(10, 4, true);
            //document.querySelector('#y').textContent = y.toFixed(2);
            let z = event.target.value.getFloat32(2, 4, true);
            document.querySelector('#z').textContent = z.toFixed(2);
            let x = event.target.value.getFloat32(6, 4, true);
            //document.querySelector('#x').textContent = x.toFixed(2);                 
        //EULER ANGLES
          if (a[0] === '19' && a[1] === '08') {
           document.querySelector('#valueHEX').textContent = 'Euler: ' + ' ' + a.join(' ');
          let yaw = event.target.value.getFloat32(14, 2, true);  
          document.querySelector('#yaw').textContent = yaw;
          let roll = event.target.value.getFloat32(12, 2, true);  
          document.querySelector('#roll').textContent = roll;
          let pitch = event.target.value.getFloat32(10, 2, true);  
          document.querySelector('#pitch').textContent = pitch;
Sign In or Register to comment.