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

Puck.js onAccel callback speed is slow #2349

Closed
mihir-chauhan opened this issue Apr 13, 2023 · 14 comments
Closed

Puck.js onAccel callback speed is slow #2349

mihir-chauhan opened this issue Apr 13, 2023 · 14 comments

Comments

@mihir-chauhan
Copy link

Hello,

I was trying to use a Puck.js v2.1 to broadcast accelerometer and gyro data to a mobile phone app at around 1kHz, but I can only get 50Hz. I am trying to double-integrate acceleration to get the world-frame to pose (position and orientation). I tried setting this function to 6600Puck.accelOn(6600); which matched one of the data rates supported by the accelerometer, but it still was around 45-50Hz which is not desired. How can I either increase the data rate through Javascript OR modify the existing firmware C code and reflash the Puck.js? If I want to modify the existing firmware what are the steps to do this? I believe the code base is here? https://github.com/espruino/Espruino. How would building binaries/installing the code work?

Here is my JS code.

var accelOn = false;
var batteryLevel = Puck.getBatteryPercentage();
  
NRF.nfcURL("Puck.js 9e03");

setWatch(function() {
  if (accelOn) {
    Puck.accelOff();
    accelOn = false;
    NRF.nfcRaw(new Uint8Array([193, 1, 0, 0, 0, 13, 85, 3, 80, 117, 99, 107, 46, 106, 115, 32, 57, 101, 48, 51]));
    batteryLevel = Puck.getBatteryPercentage();
  } else {
    Puck.accelOn(6660);
    accelOn = true;
  }
}, BTN, {edge:"rising", repeat:1, debounce:20});


NRF.setServices({
  0x2713: {
    "e482a929-e02a-4e8a-8296-cca819ebe0c6": {
      value: String(Puck.getTemperature()),
      maxLen: 20,
      broadcast: true, // optional, default is false
      readable: true,   // optional, default is false
      writable: false,   // optional, default is false
      notify: true,   // optional, default is false
      indicate: true,   // optional, default is false
      description: "Accelerometer X",  // optional, default is null,
    },
    "cad35f0b-8a1a-442c-b249-4698e582fcf4": {
      value: String(Puck.getTemperature()),
      maxLen: 20,
      broadcast: true, // optional, default is false
      readable: true,   // optional, default is false
      writable: false,   // optional, default is false
      notify: true,   // optional, default is false
      indicate: true,   // optional, default is false
      description: "Accelerometer Y",  // optional, default is null,
    }, 
    "4249a4c7-9a03-428d-818a-24a7c5aefb4a": {
      value: String(Puck.getTemperature()),
      maxLen: 20,
      broadcast: true, // optional, default is false
      readable: true,   // optional, default is false
      writable: false,   // optional, default is false
      notify: true,   // optional, default is false
      indicate: true,   // optional, default is false
      description: "Accelerometer Z",  // optional, default is null,
    },
    "54e832b7-714e-4aa5-ada9-e21dd2006844": {
      value: String(Puck.getTemperature()),
      maxLen: 20,
      broadcast: true, // optional, default is false
      readable: true,   // optional, default is false
      writable: false,   // optional, default is false
      notify: true,   // optional, default is false
      indicate: true,   // optional, default is false
      description: "Gyroscope X",  // optional, default is null,
    },
    "cbbb698d-878e-473a-9625-fbe0243ac8e1": {
      value: String(Puck.getTemperature()),
      maxLen: 20,
      broadcast: true, // optional, default is false
      readable: true,   // optional, default is false
      writable: false,   // optional, default is false
      notify: true,   // optional, default is false
      indicate: true,   // optional, default is false
      description: "Gyroscope Y",  // optional, default is null,
    }, 
    "6db27825-05fa-4984-9fed-568edd9f1fe8": {
      value: String(Puck.getTemperature()),
      maxLen: 20,
      broadcast: true, // optional, default is false
      readable: true,   // optional, default is false
      writable: false,   // optional, default is false
      notify: true,   // optional, default is false
      indicate: true,   // optional, default is false
      description: "Gyroscope Z",  // optional, default is null,
    },
    "1bdf76b4-006a-4e20-8a09-fb87b8f4b29d": {
      value: String(batteryLevel),
      maxLen: 20,
      broadcast: true, // optional, default is false
      readable: true,   // optional, default is false
      writable: false,   // optional, default is false
      notify: true,   // optional, default is false
      indicate: true,   // optional, default is false
      description: "Battery Level",  // optional, default is null,
    }
  }
});

NRF.setAdvertising({
  0x2713: undefined
});

Puck.on('accel', function(a) {
  NRF.updateServices({
    0x2713: {
      "e482a929-e02a-4e8a-8296-cca819ebe0c6": {
        value: String(a.acc.x),
        notify: true,
        readable: true
      },
      "cad35f0b-8a1a-442c-b249-4698e582fcf4": {
        value: String(a.acc.y),
        notify: true,
        readable: true
      },
      "4249a4c7-9a03-428d-818a-24a7c5aefb4a": {
        value: String(a.acc.z),
        notify: true,
        readable: true
      },
      "54e832b7-714e-4aa5-ada9-e21dd2006844": {
        value: String(a.gyro.x),
        notify: true,
        readable: true
      },
      "cbbb698d-878e-473a-9625-fbe0243ac8e1": {
        value: String(a.gyro.y),
        notify: true,
        readable: true
      },
      "6db27825-05fa-4984-9fed-568edd9f1fe8": {
        value: String(a.gyro.z),
        notify: true,
        readable: true
      },
      "1bdf76b4-006a-4e20-8a09-fb87b8f4b29d": {
        value: String(batteryLevel),
        notify: true,
        readable: true
      }
    }
  });
});
@mariusGundersen
Copy link
Contributor

I seriously doubt you can get 1khz through Bluetooth, especially using only service advertising. You could perhaps try to send it through the uart, streaming the data over Bluetooth as fast as possible.

@mariusGundersen
Copy link
Contributor

You could have a look at this example code, it used the Bangle but should work with the Puck as well: https://www.espruino.com/Bangle.js+Data+Streaming

@gfwilliams
Copy link
Member

Hi - I don't believe the problem is with the Puck.js accelerometer speed - it's just that Bluetooth LE isn't capable of sending the data that fast the way you are attempting to do it

Bluetooth works on a connection interval, and the fastest this can be is 7.5ms (130Hz) according to the spec. While you can trigger more than one notification per connection interval I think the max might be around 5, and since you're updating multiple characteristics that will really eat into it.

What I'd suggest is:

  • Send the data as raw binary - sending it as human readable text is needlessly wasting data bandwidth
  • Send it all in one characteristic
  • Send multiple samples in each update - so to get 1kHz you'd have to send maybe 8 samples worth at once

As @mariusGundersen says, the best way to do all the above is really just to use the built-in UART since Espruino will automatically batch updates into the minimum amount of packets required.

Something along the lines of:

Puck.on('accel', function(a) {
  Bluetooth.write((new Int16Array([a.acc.x,a.acc.y,a.acc.z,a.gyro.x,a.gyro.y,a.gyro.z])).buffer);
});

is probably all you need. I wouldn't send the battery level 1000 times a second as you're just wasting time and bandwidth again (you could keep doing that with a characteristic as you were, but update it once a minute)

If I want to modify the existing firmware what are the steps to do this? I believe the code base is here? https://github.com/espruino/Espruino

Yes - there are some READMEs in the repo about how to build it, as well as some tutorials on the website about adding functionality. But as above it's not really an issue with Espruino at this point, it's to do with what you're trying to do with Bluetooth.

@gfwilliams
Copy link
Member

Just to add, in the example I send the data as 16 bit values, but if you were able to do with just 8 bits (losing a bit of precision) that would halve the amount of data you needed to send and so would make life a lot easier.

@mihir-chauhan
Copy link
Author

mihir-chauhan commented Apr 18, 2023

Thank you @mariusGundersen and @gfwilliams for your input. I will try these out and let you know! Appreciate your help.
Edit: closing issue for now

@mihir-chauhan
Copy link
Author

Also for others who may stumble upon this...I found out after reading the datasheets of the accelerometer and digging into the Espruino code for acceleration on the puck and it seems to be that the Puck's accelerometer is being put into low-power consumption mode which has a max polling rate of only 52Hz, but by doing the following two lines after Puck.accelOn(interval):

    Puck.accelOn(1660);
    Puck.accelWr('0x15', '0x00');
    Puck.accelWr('0x16', '0x00');

It should put the accelerometer chip into "high-performance operating mode enabled" which can allow up to 6.66kHz, but the max the puck can handle (based on documentation) is 1.66Hz. Note that battery consumption will be more, however.

I believe this (setting accel/gyro to low power) happens here in the Espruino code:

unsigned char buf[2];
buf[0] = 0x15; buf[1]=0x10; // CTRL6-C - XL_HM_MODE=1, low power accelerometer
jsi2cWrite(&i2cAccel, ACCEL_ADDR, 2, buf, true);
buf[0] = 0x16; buf[1]=0x80; // CTRL6-C - G_HM_MODE=1, low power gyro
jsi2cWrite(&i2cAccel, ACCEL_ADDR, 2, buf, true);
buf[0] = 0x18; buf[1]=0x38; // CTRL9_XL Acc X, Y, Z axes enabled
jsi2cWrite(&i2cAccel, ACCEL_ADDR, 2, buf, true);
buf[0] = 0x10; buf[1]=reg | 0b00001011; // CTRL1_XL Accelerometer, +-4g, 50Hz AA filter
jsi2cWrite(&i2cAccel, ACCEL_ADDR, 2, buf, true);
buf[0] = 0x11; buf[1]=gyro ? reg : 0; // CTRL2_G Gyro, 250 dps, no 125dps limit
jsi2cWrite(&i2cAccel, ACCEL_ADDR, 2, buf, true);
buf[0] = 0x12; buf[1]=0x44; // CTRL3_C, BDU, irq active high, push pull, auto-inc
jsi2cWrite(&i2cAccel, ACCEL_ADDR, 2, buf, true);
buf[0] = 0x0D; buf[1]=3; // INT1_CTRL - Gyro/accel data ready IRQ
jsi2cWrite(&i2cAccel, ACCEL_ADDR, 2, buf, true);

@gfwilliams
Copy link
Member

Ok, thanks! So we should ideally not set those low power mode flags above 52Hz? I could make a change for that...

@mihir-chauhan
Copy link
Author

mihir-chauhan commented Apr 18, 2023

Yes that would be great because in low power mode, as per the accelerometer/gyro datasheet it will only go up to 50Hz, which was what I was originally receiving even if I did Puck.accelOn(1660);. Thanks

@gfwilliams
Copy link
Member

I did have a quick try and:

n=0;
Puck.on("accel", ()=>{"jit" n++});
setInterval(function() {
  console.log(n);
  n=0;
},1000);
Puck.accelOn(208)

With firmware 2v17 it does give you 208Hz as expected, but for me any higher and it tops out at ~350Hz even at very high sample rates. Setting the register values as you have done had no effect for me.

Looking again at Table 52. Accelerometer ODR register setting and the one for Gyro it's actually fine as it was. It's saying that actually with that low power flag set it'll automatically set the correct operation mode depending on what frequency is requested.

Digging into it further, with it topping out at ~350Hz, around 80% of the time is spent in the accel_read routine which literally does nothing except read the data via I2C from the accelerometer, so about the only way to actually make it go faster would be to switch to using hardware I2C (currently we use software)

I've had a quick play around and have swapped it to hardware I2C, and that gets us to ~740Hz with only about 25% of the time spent on I2C, so it's definitely an improvement, but it's still not 1kHz - and I'm not sure if it's worth putting that code in as some users will be upset that the only available hardware I2C device disappears if they use the accelerometer. I've pushed it as a PUCKJS_HW_I2C_ACCEL branch though.

Potentially it could be added conditionally - if the poll rate is above 50Hz it starts using hardware I2C or something like that...

@mihir-chauhan
Copy link
Author

The 740Hz is a great improvement, I will try out the hardware I2C. One question I had is if I was storing n amount of the data in a list and then sending all the data through Bluetooth when it was full, would the Bluetooth send function be "blocking" in the sense that I won't be able to handle any of the new data during the send time period and lose that data? Or would there be a way to still store the data while sending through Bluetooth. Thanks for your help

@gfwilliams
Copy link
Member

If you use Bluetooth.write (with the UART) then the function only blocks when the output buffer is full, otherwise it returns right away. The output buffer is around 1kB I think so even if you just do Bluetooth.write with the data as it comes in (without worrying about buffering) as I'd done in #2349 (comment) you should be fine.

@mihir-chauhan
Copy link
Author

mihir-chauhan commented Apr 23, 2023

Thanks. When I try to build on WSL (Windows Subsystem for Linux), I am able to get bootloader_espruino_2v17_puckjs.elf and bootloader_espruino_2v17_puckjs.hex. I am not sure what exactly to do in order to package these as it says to upload a ZIP for nRF52 boards. Also, what are all the parameters and respective values I need in the following command line action?: nrfutil pkg generate --application bootloader_espruino_2v17_puckjs.hex puckjs_bootloader.zip, such as hardware version, etc?

@gfwilliams
Copy link
Member

Hi - you don't need to flash the bootloader firmware - I'm not sure quite how you ended up with only that built.

You could try DFU_UPDATE_BUILD=1 BOARD=PUCKJS RELEASE=1 make and that will automatically spit out a DFU zip for the application that you can then use with the normal 'DFU' app on iOS/Android.

However, GitHub automatically builds the firmware if github actions is enabled (it is on the main repo). That used to be only for the 'master' branch, but I've just enabled it for all new branches, so you should now be able to download via:

https://github.com/espruino/Espruino/actions?query=branch%3APUCKJS_HW_I2C_ACCEL

For instance in https://github.com/espruino/Espruino/actions/runs/4784020729 scroll right down to Artefacts and click PUCKJS and you should get the DFU zip built for you.

@mihir-chauhan
Copy link
Author

mihir-chauhan commented Apr 25, 2023

Thanks for your help @gfwilliams, I was able to update the firmware through the iOS app (Web IDE and other Web Bluetooth Secure DFU had error at last step). Just FYI: I am able to get a max of 833Hz when there is just the counter, but when I do the Bluetooth.write((new Int16Array([a.acc.x,a.acc.y,a.acc.z,a.gyro.x,a.gyro.y,a.gyro.z])).buffer); it drops down to 348Hz.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants