sparkfun / OpenLog_Artemis

The OpenLog Artemis is an open source datalogger the comes preprogrammed to automatically log IMU, GPS, serial data, and various pressure, humidity, and distance sensors. All without writing a single line of code!
https://www.sparkfun.com/products/15846
Other
88 stars 47 forks source link

BLE enabled on the current hardware (OLA v1.18) #66

Closed u382514 closed 3 years ago

u382514 commented 3 years ago

Just got a hold of this little gem of technology and was wondering if, at some point, the firmware can/will be updated to open up BLE and that the current hardware will be able to handle it or if it's going to require a new buy.

Thanks!

u382514 commented 3 years ago

Just following up: I was able to get BLE enabled by upgrading the core to 2.0.3 and the hardware indeed accepted it. Just getting a basic example compiled using the ICM 20948 library was quite an effort though. You folks will have your work cut out for you porting OLA.

PaulZC commented 3 years ago

Hi Brandon (@u382514 ), That's great news - glad that is working for you, Paul

userqt commented 1 year ago

@u382514 , I am curious how did you manage to make the BLE working on the OLA? I am not familiar with how can the core be updated. Would it be possible if you can share some info on this?

u382514 commented 1 year ago

Ooof, that was so long ago. It involved multiple bottles of scotch and a bruised and battered ego. What I remember is dealing with a lot of compile errors and just going one by one until I got it to work with ArduinoBLE. Then I slowly wrote the OLA code back in to the bare minimum requirements I needed which was the onboard ICM20948 and the SD Card. I ended up writing my own implementation of ICM20948 for the DMP and probably some other valid reason that escapes me.

Seems like they've long since upgraded to a core that can handle BLE but don't seem to see any progress on implementing BLE yet in the firmware? Adding ArduinoBLE now should be fairly trivial I would think.

I may be able to go in some time this weekend and do a quick upgrade to one the OLA boards I have lying around and report back. Such a great board I bought way more than I needed.

u382514 commented 1 year ago

I was able to get this to work with no problems. A whole lot of very nice updates have occurred to this repo since 2021.

  1. First off, I downloaded a fresh version of Arduino IDE
  2. Next, I had to install SparkFun Apollo3 Boards (version 2.2.1). To do that I needed to add a url for the board to show up for install. Go to settings and add the following URL to the "Additional boards manager URLs": https://raw.githubusercontent.com/sparkfun/Arduino_Apollo3/main/package_sparkfun_apollo3_index.json
  3. Go to Tools -> Boards -> Board Manager and filter on Apollo3. Install SparkFun Apollo3 Boards version 2.2.1.
  4. I plugged my OpenLog board into the usb-c port, and selected the port. For the board I used RedBoard Artemis ATP
  5. I then downloaded this whole repository and opened up the OLA_IMU_Basics sketch under Firmware -> Test Sketches
  6. I made sure this compiled and worked on the OpenLog board. Worked like a charm
  7. I then made the changes to the OLA_IMU_Basics sketch to enable BLE and send over a Temp via BLE (full sketch below). Compiled and worked like a charm
  8. To test I used an app called BluetoothLE on my iPhone

I hope this helps.

/****************************************************************
 * Testing basic functionality of the ICM 20948 on the OpenLog Artemis.
 * Select "SparkFun Apollo3" \ SparkFun RedBoard Artemis ATP" as the board.
 * 
 * Based on:
 * 
 * Example1_Basics.ino
 * ICM 20948 Arduino Library Demo 
 * Use the default configuration to stream 9-axis IMU data
 * Owen Lyke @ SparkFun Electronics
 * Original Creation Date: April 17 2019
 * 
 * This code is beerware; if you see me (or any other SparkFun employee) at the
 * local, and you've found our code helpful, please buy us a round!
 * 
 * Distributed as-is; no warranty is given.
 ***************************************************************/

// OLA Specifics:
const byte PIN_IMU_POWER = 27; // The Red SparkFun version of the OLA (V10) uses pin 27
//const byte PIN_IMU_POWER = 22; // The Black SparkX version of the OLA (X04) uses pin 22
const byte PIN_IMU_INT = 37;
const byte PIN_IMU_CHIP_SELECT = 44;
const byte PIN_SPI_SCK = 5;
const byte PIN_SPI_CIPO = 6;
const byte PIN_SPI_COPI = 7;

#include <ArduinoBLE.h>
#include "ICM_20948.h"  // Click here to get the library: http://librarymanager/All#SparkFun_ICM_20948_IMU

#define USE_SPI       // Uncomment this to use SPI

#define SERIAL_PORT Serial

#define SPI_PORT SPI    // Your desired SPI port.       Used only when "USE_SPI" is defined
#define CS_PIN PIN_IMU_CHIP_SELECT // Which pin you connect CS to. Used only when "USE_SPI" is defined. OLA uses pin 44.

#define WIRE_PORT Wire  // Your desired Wire port.      Used when "USE_SPI" is not defined
#define AD0_VAL   1     // The value of the last bit of the I2C address. 
                        // On the SparkFun 9DoF IMU breakout the default is 1, and when 
                        // the ADR jumper is closed the value becomes 0

#ifdef USE_SPI
  ICM_20948_SPI myICM;  // If using SPI create an ICM_20948_SPI object
#else
  ICM_20948_I2C myICM;  // Otherwise create an ICM_20948_I2C object
#endif

//U382514 - Added Service and Characteristics
BLEService myBLEService("19B10000-E8F2-537E-4F6C-D104768A1214");
BLEFloatCharacteristic myCharacteristic("19B10001-E8F2-537E-4F6C-D104768A1214", BLERead | BLEWrite | BLENotify);

void setup() {

  //U382514 - Add BLE Setup
  if (!BLE.begin()) {
    Serial.println("starting BLE failed!");

    while (1);
  }

  // set advertised local name and service UUID:
  BLE.setLocalName("MYBLESERVICE");
  BLE.setAdvertisedService(myBLEService);

  // add the characteristic to the service
  myBLEService.addCharacteristic(myCharacteristic);

  // add service
  BLE.addService(myBLEService);

  // start advertising
  BLE.advertise();
  //U382514 - End BLE Setup

#ifdef USE_SPI
  SPI_PORT.begin();

  pinMode(PIN_IMU_CHIP_SELECT, OUTPUT);
  digitalWrite(PIN_IMU_CHIP_SELECT, HIGH); //Be sure IMU is deselected

  enableCIPOpullUp(); // Enable CIPO pull-up on the OLA

  //There is a quirk in v2.1 of the Apollo3 mbed core which means that the first SPI transaction will
  //disable the pull-up on CIPO. We need to do a fake transaction and then re-enable the pull-up
  //to work around this...
#if defined(ARDUINO_ARCH_MBED)
  SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0)); // Do a fake transaction
  SPI.endTransaction();
  enableCIPOpullUp(); // Re-enable the CIPO pull-up
#endif

#else
  WIRE_PORT.begin();
  WIRE_PORT.setClock(400000);
#endif

  //Reset ICM by power cycling it
  imuPowerOff();

  delay(10);

  imuPowerOn(); // Enable power for the OLA IMU

  delay(100); // Wait for the IMU to power up

  SERIAL_PORT.begin(115200);
  while(!SERIAL_PORT){};

  bool initialized = false;
  while( !initialized ){

#ifdef USE_SPI
    myICM.begin( CS_PIN, SPI_PORT ); 
#else
    myICM.begin( WIRE_PORT, AD0_VAL );
#endif

    SERIAL_PORT.print( F("Initialization of the sensor returned: ") );
    SERIAL_PORT.println( myICM.statusString() );
    if( myICM.status != ICM_20948_Stat_Ok ){
      SERIAL_PORT.println( "Trying again..." );
      delay(500);
    }else{
      initialized = true;
    }
  }
}

void loop() {

  if( myICM.dataReady() ){
    myICM.getAGMT();                // The values are only updated when you call 'getAGMT'
//    printRawAGMT( myICM.agmt );     // Uncomment this to see the raw values, taken directly from the agmt structure
    printScaledAGMT( myICM.agmt);   // This function takes into account the sclae settings from when the measurement was made to calculate the values with units
    delay(30);
  }else{
    Serial.println("Waiting for data");
    delay(500);
  }

  //U382514 - Send something through BLE
  BLEDevice central = BLE.central();
  if (central) {
    if (central.connected()) {
      myCharacteristic.writeValue(myICM.temp());
      delay(500);
    }
  }

}

// Below here are some helper functions to print the data nicely!

void printPaddedInt16b( int16_t val ){
  if(val > 0){
    SERIAL_PORT.print(" ");
    if(val < 10000){ SERIAL_PORT.print("0"); }
    if(val < 1000 ){ SERIAL_PORT.print("0"); }
    if(val < 100  ){ SERIAL_PORT.print("0"); }
    if(val < 10   ){ SERIAL_PORT.print("0"); }
  }else{
    SERIAL_PORT.print("-");
    if(abs(val) < 10000){ SERIAL_PORT.print("0"); }
    if(abs(val) < 1000 ){ SERIAL_PORT.print("0"); }
    if(abs(val) < 100  ){ SERIAL_PORT.print("0"); }
    if(abs(val) < 10   ){ SERIAL_PORT.print("0"); }
  }
  SERIAL_PORT.print(abs(val));
}

void printRawAGMT( ICM_20948_AGMT_t agmt){
  SERIAL_PORT.print("RAW. Acc [ ");
  printPaddedInt16b( agmt.acc.axes.x );
  SERIAL_PORT.print(", ");
  printPaddedInt16b( agmt.acc.axes.y );
  SERIAL_PORT.print(", ");
  printPaddedInt16b( agmt.acc.axes.z );
  SERIAL_PORT.print(" ], Gyr [ ");
  printPaddedInt16b( agmt.gyr.axes.x );
  SERIAL_PORT.print(", ");
  printPaddedInt16b( agmt.gyr.axes.y );
  SERIAL_PORT.print(", ");
  printPaddedInt16b( agmt.gyr.axes.z );
  SERIAL_PORT.print(" ], Mag [ ");
  printPaddedInt16b( agmt.mag.axes.x );
  SERIAL_PORT.print(", ");
  printPaddedInt16b( agmt.mag.axes.y );
  SERIAL_PORT.print(", ");
  printPaddedInt16b( agmt.mag.axes.z );
  SERIAL_PORT.print(" ], Tmp [ ");
  printPaddedInt16b( agmt.tmp.val );
  SERIAL_PORT.print(" ]");
  SERIAL_PORT.println();
}

void printFormattedFloat(float val, uint8_t leading, uint8_t decimals){
  float aval = abs(val);
  if(val < 0){
    SERIAL_PORT.print("-");
  }else{
    SERIAL_PORT.print(" ");
  }
  for( uint8_t indi = 0; indi < leading; indi++ ){
    uint32_t tenpow = 0;
    if( indi < (leading-1) ){
      tenpow = 1;
    }
    for(uint8_t c = 0; c < (leading-1-indi); c++){
      tenpow *= 10;
    }
    if( aval < tenpow){
      SERIAL_PORT.print("0");
    }else{
      break;
    }
  }
  if(val < 0){
    SERIAL_PORT.print(-val, decimals);
  }else{
    SERIAL_PORT.print(val, decimals);
  }
}

void printScaledAGMT( ICM_20948_AGMT_t agmt){
  SERIAL_PORT.print("Scaled. Acc (mg) [ ");
  printFormattedFloat( myICM.accX(), 5, 2 );
  SERIAL_PORT.print(", ");
  printFormattedFloat( myICM.accY(), 5, 2 );
  SERIAL_PORT.print(", ");
  printFormattedFloat( myICM.accZ(), 5, 2 );
  SERIAL_PORT.print(" ], Gyr (DPS) [ ");
  printFormattedFloat( myICM.gyrX(), 5, 2 );
  SERIAL_PORT.print(", ");
  printFormattedFloat( myICM.gyrY(), 5, 2 );
  SERIAL_PORT.print(", ");
  printFormattedFloat( myICM.gyrZ(), 5, 2 );
  SERIAL_PORT.print(" ], Mag (uT) [ ");
  printFormattedFloat( myICM.magX(), 5, 2 );
  SERIAL_PORT.print(", ");
  printFormattedFloat( myICM.magY(), 5, 2 );
  SERIAL_PORT.print(", ");
  printFormattedFloat( myICM.magZ(), 5, 2 );
  SERIAL_PORT.print(" ], Tmp (C) [ ");
  printFormattedFloat( myICM.temp(), 5, 2 );
  SERIAL_PORT.print(" ]");
  SERIAL_PORT.println();
}

void imuPowerOn()
{
  pinMode(PIN_IMU_POWER, OUTPUT);
  digitalWrite(PIN_IMU_POWER, HIGH);
}
void imuPowerOff()
{
  pinMode(PIN_IMU_POWER, OUTPUT);
  digitalWrite(PIN_IMU_POWER, LOW);
}

#if defined(ARDUINO_ARCH_MBED) // updated for v2.1.0 of the Apollo3 core
bool enableCIPOpullUp()
{
  //Add 1K5 pull-up on CIPO
  am_hal_gpio_pincfg_t cipoPinCfg = g_AM_BSP_GPIO_IOM0_MISO;
  cipoPinCfg.ePullup = AM_HAL_GPIO_PIN_PULLUP_1_5K;
  pin_config(PinName(PIN_SPI_CIPO), cipoPinCfg);
  return (true);
}
#else
bool enableCIPOpullUp()
{
  //Add CIPO pull-up
  ap3_err_t retval = AP3_OK;
  am_hal_gpio_pincfg_t cipoPinCfg = AP3_GPIO_DEFAULT_PINCFG;
  cipoPinCfg.uFuncSel = AM_HAL_PIN_6_M0MISO;
  cipoPinCfg.ePullup = AM_HAL_GPIO_PIN_PULLUP_1_5K;
  cipoPinCfg.eDriveStrength = AM_HAL_GPIO_PIN_DRIVESTRENGTH_12MA;
  cipoPinCfg.eGPOutcfg = AM_HAL_GPIO_PIN_OUTCFG_PUSHPULL;
  cipoPinCfg.uIOMnum = AP3_SPI_IOM;
  padMode(MISO, cipoPinCfg, &retval);
  return (retval == AP3_OK);
}
#endif
userqt commented 1 year ago

This is amazing, thank you so much. It saved me a lot of time. I was able to compile and upload the sample you provided, it worked right away. I used an android app called nRF Connect. I was able to see it listed as MYBLESERVICE and was able to connect to it - awesome!!!

If I may ask you:

How do I make sure that I received the correct data? As an experiment I did this: myCharacteristic.writeValue(1234567890); In the app, I see that it received the following: (0x) 06-2C-93-4E. Is this binary displayed as hex? How do I decode this?

The other thing I am not sure is how do I get this data to my application? At the moment, I am using the USB and listening to the serial port: "COM3", baud 115200. Now with BLE, I am not sure how to read data from it.

What I would ideally like to do is send text just like the IMU prints stuff to the console in the arduino IDE, but writeValue is able to write only float.

Again thanks a lot and enjoy your day.

u382514 commented 1 year ago

You're receiving back a byte array. So you'll have to look at whatever language/framework you're working in to determine how to convert that byte array back into the data it represents.

For instance, if I was building a Flutter app (Dart), using the Float Characteristic I'm returning for the temp I might convert it like this:

final bytes = Uint8List.fromList(byteArrayDataFromBLECharacteristic);
final bytesData = ByteData.sublistView(bytes);
double celcius = bytesData.getFloat32(0, Endian.little);
double farenheit = (celcius * (9 / 5)) + 32;

In the example I gave I'm using a Float Characteristic specifically, but there are other incarnations you can use. You'll have to go through the ArduinoBLE documents to see what they all do. If I wanted to send a string through, I would change the Arduino Sketch to probably use a generic BLECharacteristic like below.

//BLEFloatCharacteristic myCharacteristic("19B10001-E8F2-537E-4F6C-D104768A1214", BLERead | BLEWrite | BLENotify);
BLECharacteristic myCharacteristic("19B10001-E8F2-537E-4F6C-D104768A1214", BLERead | BLEWrite | BLENotify, 12); 

//... code skipped for brevity

//myCharacteristic.writeValue(myICM.temp());
char buf[12];
String helloWorld = "Hello World";
helloWorld.toCharArray(buf, 12);
myCharacteristic.writeValue(buf);

and then in my Flutter app I would convert it like so...

final helloWorld = utf8.decode(byteArrayDataFromBLECharacteristic);
userqt commented 1 year ago

I am trying to send three floats to my application (I am using C#). In arduino I used: char res[40]; sprintf(res, "%.1f,%.1f,%.1f", 1.2, 5.6, 2.5); myCharacteristic.writeValue(res);

And then in C#: Encoding.UTF8..GetString(bytes)

This however, returns me %.1f,%.1f,%.

How can I send those floats correctly?

EDIT: I just found some good way to split the floating point values: https://forum.arduino.cc/t/concatenating-multiple-float-variables-with-limiter-to-char-array/311300/2

But in the end I still get no complete result: bytes: 1.199,5.599,

userqt commented 1 year ago

Ok, nvm, I was able to make it work. The byte array was not complete, coz I had a size limit less than I needed. Seems I forgot to increase the size of the characteristic during my experiments.

Thank you for the help @u382514.

u382514 commented 1 year ago

Awesome! Glad to be of service. Check out ArduinoJson. I've had pretty good luck getting data in a structured format using this package to use in apps to cut down on coding time. This makes even more of a difference when SENDING data to the device via BLE. This package magically handles encoding just about anything needed for a Json format (including floated) without having to deal with other work-a-arounds. Obviously everything comes at a cost and BLE can be quite slow when it comes to real-time updates. Pros and cons for convenience.

userqt commented 1 year ago

@u382514 , also very good suggestion, I will take a look into the ArduinoJson, would defintely need it.

I am experiencing some lags and I am not sure what is the cause of that. I see that there is a delay(500). If I disable it then the board turns off after connection.

Is there some specific way to handle rapid flow of data from the board to my application? The data (I am sending quaternion transformations) should always be available to the characteristic and the app should always just read it. I could add an optmization to send the data if there is change for example, but I am afraid that won't be enough. It is not clear to me if the loop method is the place for writing values or I have to listen for events with a handler in my application? I am quite new to all this. I need the board to operate as a controller operated by the user in a constant manner and send the data produced during the user input.

Do you perhaps know or have some directions how this can be done?

u382514 commented 1 year ago

These devices have a lot of noise by nature, so it's very common to add filters (like the Kalman Filter) to smooth out the data. Sometimes it's better to do it directly on the board and sometimes it's better to do that in your application. I usually do it in the application because I store the real-time data on the SD Card for offline analytics (more granular and accurate).

I use BLE for real-time UI changes like graphs, tiles, etc at relatively constant intervals. Trying to use the BLE information of an IMU in a 3d viewer for instance may just not work so well with the BLE limitations. BLE just isn't going to get that kind of throughput/accuracy that direct to SD/offline processing is going to give you. You can buffer data in the loop and at set intervals flush through BLE. I'm assuming you're using stream/subscribing to the characteristic in your application, so what you see is pretty much what you get.

If you need more throughput you're going to have to pair the board up with a wifi board or something like that.

userqt commented 1 year ago

I figured it out, it was a setting in my application plugin, which I had to enable, in order to make the communication between the two more frequent.