It is 21h00 on Saturday the 16th of May 2020 and I've decided to make a later entry with a project idea I hatched only 30 minutes ago. The reason is that I had read earlier today that the NanoRama competition deadline had been extended and it was only yesterday that I had given up in despair with my original project.
This time the plan is simple. It starts with a hockey stick...
Then prove that you can really rapid prototype with an Arduino Nano 33 BLE board within 48 hours.
So, much like the popular TV series 24, I've got 24 hours to update my progress.
To be continued...
The mission
It is 23h00 and the mission requirements for the Hack in a Hurry ("HinaH") Hockey Stick project have now been flushed out.
The Arduino Nano 33 BLE board comes with a 9DOF motion and orientation-sensing LSM9DS1 SiP (system-in-package) from ST Microelectronics. The mission is to attach the Arduino Nano 33 BLE board to the hockey stick and use BLE to transmit the live motion sensing data (linear acceleration and angular motion) to a remote central controller. The purpose would then be to use this data to evaluate the hockey players technique when trying to hit a target with a hockey ball.
The small target area (or goal) will be monitored by a BBC microbit which will have at least 1 or 2 proximity sensors attached. These proximity sensor will determine the degree of accuracy (left or right of a plumb line) of the ball strike. The LED's on the BBC microbit will be used to provide user live feedback. The number of successful strikes and degree of accuracy data can then be related back to the central controller.
Mission preparation
It's 07h00 Sunday 17th of May 2020. Errr, it's Sunday and the kids are still sleeping so why wake them up...
It's now 09h00 Sunday 17th of May 2020. Ah feeling much better. Breakfast is done and the first cup of coffee has been had. It is time to get cracking on with project preparation. First, I needed to work out a quick plan for the morning sprint:
1. Kit out the hockey stick with battery pack and the Arduino Nano board.
2. Download the Arduino LSM9DS1 library and run through the examples.
3. Do some research on visual outputs for motion data (had seen some videos before using Processing for graphic output).
Completing step 1 did not take too long. I had plenty of packaging foam and duct tape lying around so I was good to go.
{gallery:autoplay=false} Arduino Nano 33 BLE on Hockey Stick |
---|
Onto step 2.
The Arduino IDE provides a very handy Library Manager tool. Typing in "LSM9DS1" gave me a list of options to choose from:
As you can see, I installed the Arduino version.
The curious thing I found, following the installation of the library, was where the LSM9DS1 examples are placed when you have the examples list open within the IDE. It took a bit of head scratching... only to find them right under my nose.
I combined the SimpleAccelerometer and SimpleGyroscope examples together and opened up the SerialPlotter tool to review my data. All looked good at face value.
Onto step 3.
I wanted to use the rest of my time before a late lunch to explore Processing and 3d rendering as wanted to see if I could create a simple tool to visualise the data. The ultimate aim would be to show data on a web browser and use some JavaScript framework and library to display the data but this would take me too long. The nice thing about Processing is that there are quite a few good libraries about not and there are even a few YouTube videos I found that demonstrated how to visualise IMU data via Processing.
Well, it turns out there is more to this than meets the eye and I was not able to get something working straight away from my serial data stream. So I put this on hold.
Developing the Arduino Nano 33 BLE application
For my second sprint of the day, I decided to concentrate my efforts on the BLE side.
To create BLE Applications on the Arduino Nano 33 BLE board requires the ArduinoBLE library. This library is based off the BLEPeripheral library, which I was quite familiar with already.
My aim was to create a BLE peripheral application which had it's own custom service. I started with the "Button LED" example provided as this used a custom service and two custom characteristics too. All I had to do was modify the variable names to make them more meaningful and then change the properties to suit. The big change for my application was that I wanted to transmit a byte array for all my movement data. For this you have to define your characteristic slightly differently.
BLEService MovementService("19B10010-E8F2-537E-4F6C-D104768A1214"); // create our custom GATT service // create movement data characteristic (this is a Byte Array) and allow remote device to use Notify BLECharacteristic moveDataCharacteristic("19B10011-E8F2-537E-4F6C-D104768A1214", BLERead | BLENotify, 24, true); // create acceleration & gyro sample rate (Hz) characteristic and allow remote device to get notifications BLEShortCharacteristic SampleRateCharacteristic("19B10012-E8F2-537E-4F6C-D104768A1214", BLERead | BLEWrite);
The next challenge was working out how to format the data as the only option provided within BLE for sending multiple amounts of data is in a byte array. Thankfully I had crossed this hurdle before as this is handled for you in the health thermometer BLE service. The BLE health thermometer measurement characteristic is defined by something called "IEEE11073float" and Adafruit had some code for this via one of their Bluefruit library examples.
uint32_t float2IEEE11073(double data, uint8_t output[4]) { uint32_t result = MDER_NaN; if (isnan(data)) { goto finally; }/* else if (data > MDER_FLOAT_MAX) { result = MDER_POSITIVE_INFINITY; goto finally; } else if (data < MDER_FLOAT_MIN) { result = MDER_NEGATIVE_INFINITY; goto finally; } else if (data >= -MDER_FLOAT_EPSILON && data <= MDER_FLOAT_EPSILON) { result = 0; goto finally; }*/ double sgn; sgn = data > 0 ? +1 : -1; double mantissa; mantissa = fabs(data); int32_t exponent; exponent = 0; // Note: 10**x exponent, not 2**x // scale up if number is too big while (mantissa > MDER_FLOAT_MANTISSA_MAX) { mantissa /= 10.0; ++exponent; if (exponent > MDER_FLOAT_EXPONENT_MAX) { // argh, should not happen if (sgn > 0) { result = MDER_POSITIVE_INFINITY; } else { result = MDER_NEGATIVE_INFINITY; } goto finally; } } // scale down if number is too small while (mantissa < 1) { mantissa *= 10; --exponent; if (exponent < MDER_FLOAT_EXPONENT_MIN) { // argh, should not happen result = 0; goto finally; } } // scale down if number needs more precision double smantissa; smantissa = round(mantissa * MDER_FLOAT_PRECISION); double rmantissa; rmantissa = round(mantissa) * MDER_FLOAT_PRECISION; double mdiff; mdiff = abs(smantissa - rmantissa); while (mdiff > 0.5 && exponent > MDER_FLOAT_EXPONENT_MIN && (mantissa * 10) <= MDER_FLOAT_MANTISSA_MAX) { mantissa *= 10; --exponent; smantissa = round(mantissa * MDER_FLOAT_PRECISION); rmantissa = round(mantissa) * MDER_FLOAT_PRECISION; mdiff = abs(smantissa - rmantissa); } uint32_t int_mantissa; int_mantissa = (int) round(sgn * mantissa); result = (exponent << 24) | (int_mantissa & 0xFFFFFF); #ifdef DEBUG //Serial.print("E ");Serial.print(exponent); //Serial.print(" | M ");Serial.print(int_mantissa); //Serial.print(" | R ");Serial.println(result); #endif finally: if ( output ) memcpy(output, &result, 4); return result; }
Fingers crossed this works as by using this method, I would be saving a huge amount of time.
It was time to get stuck in with a bit of coding and some prelim testing for the rest of the day.
/** * @filename : Element14_NanoRama_Nano33BLE_HinaH_HockeyStick.ino * @brief : This software example creates a BLE peripheral with * a custom service that contains 2 custom characteristics * 1. Central to use NOTIFY to receive Acceleration and Gyro data * 2. Central to use READ to receive Acceleration/Gyro sample rate. They are made to be the same * Central can also use WRITE to change the sampling rate. * * @hardware : Arduino Nano 33 BLE (nRF52840) + built-in Arduino LSM9DS1 sensor * * @author : Gerrikoio * * Copyright (C) Gerrikoio for Application 17 May 2020 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documnetation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include <ArduinoBLE.h> #include <Arduino_LSM9DS1.h> #include "IEEE11073float.h" #define DEBUG uint16_t SampleRateHz = 0; BLEService MovementService("19B10010-E8F2-537E-4F6C-D104768A1214"); // create our custom GATT service // create movement data characteristic (this is a Byte Array) and allow remote device to use Notify BLECharacteristic moveDataCharacteristic("19B10011-E8F2-537E-4F6C-D104768A1214", BLERead | BLENotify, 24, true); // create acceleration & gyro sample rate (Hz) characteristic and allow remote device to get notifications BLEShortCharacteristic SampleRateCharacteristic("19B10012-E8F2-537E-4F6C-D104768A1214", BLERead | BLEWrite); void setup() { #ifdef DEBUG Serial.begin(38400); while (!Serial); #endif //pinMode(LED_PWR, OUTPUT); digitalWrite(LED_PWR, HIGH); // begin BLE module initialization if (!BLE.begin()) { #ifdef DEBUG Serial.println("starting BLE failed!"); #endif while (1); } // begin IMU module initialization if (!IMU.begin()) { #ifdef DEBUG Serial.println("Failed to initialize IMU!"); #endif while (1); } SampleRateHz = (uint16_t)IMU.accelerationSampleRate(); if (SampleRateHz != (uint16_t)IMU.gyroscopeSampleRate()) { #ifdef DEBUG Serial.print(F("Gyroscope sample is not the same as Accelerometer sample rate = ")); Serial.print(SampleRateHz); Serial.println(" Hz"); #endif } else { #ifdef DEBUG Serial.print(F("The Acceleration & Gyro sample rate is ")); Serial.print(SampleRateHz); Serial.println(" Hz"); #endif } #ifdef DEBUG Serial.println(); #endif // set the local name peripheral advertises BLE.setLocalName("HaniH2020"); // set the UUID for the service this peripheral advertises: BLE.setAdvertisedService(MovementService); // add the characteristics to the service MovementService.addCharacteristic(moveDataCharacteristic); MovementService.addCharacteristic(SampleRateCharacteristic); // add the GATT service BLE.addService(MovementService); // Create a temporary array for the movement data uint8_t mArray[24]; memset(mArray, '\0', 24); moveDataCharacteristic.writeValue(mArray, 24); SampleRateCharacteristic.writeValue(SampleRateHz); // start advertising BLE.advertise(); #ifdef DEBUG Serial.println("Bluetooth device active, waiting for connections..."); #endif } void loop() { // wait for a BLE central BLEDevice central = BLE.central(); // if a central is connected to the peripheral: if (central) { #ifdef DEBUG Serial.print("Connected to central: "); // print the central's BT address: Serial.println(central.address()); #endif // check the battery level every 200ms // while the central is connected: while (central.connected()) { // Update the BLE data updateMovementData(); } #ifdef DEBUG // when the central disconnects, turn off the LED: Serial.print("Disconnected from central: "); Serial.println(central.address()); #endif } } void updateMovementData() { /* Read the current data from the Arduino LSM9DS1 sensor. */ float Mvmnt[6]; memset(Mvmnt, '\0', 6); uint8_t BLE_mArray[24]; memset(BLE_mArray, '\0', 24); if (IMU.accelerationAvailable()) { IMU.readAcceleration(Mvmnt[0], Mvmnt[1], Mvmnt[2]); #ifdef DEBUG //Serial.print("Ax "); Serial.print(Mvmnt[0]); Serial.print(", "); Serial.print(Mvmnt[1]); Serial.print(", "); Serial.print(Mvmnt[2]); #endif } if (IMU.gyroscopeAvailable()) { IMU.readGyroscope(Mvmnt[3], Mvmnt[4], Mvmnt[5]); #ifdef DEBUG Serial.print(", "); Serial.print(Mvmnt[3]); Serial.print(", "); Serial.print(Mvmnt[4]); Serial.print(", "); Serial.println(Mvmnt[5]); #endif } #ifdef DEBUG Serial.flush(); #endif float2IEEE11073(double(Mvmnt[0]), &BLE_mArray[0]); float2IEEE11073(double(Mvmnt[1]), &BLE_mArray[4]); float2IEEE11073(double(Mvmnt[2]), &BLE_mArray[8]); float2IEEE11073(double(Mvmnt[3]), &BLE_mArray[12]); float2IEEE11073(double(Mvmnt[4]), &BLE_mArray[16]); float2IEEE11073(double(Mvmnt[5]), &BLE_mArray[20]); moveDataCharacteristic.writeValue(BLE_mArray, 24); }
Success!
I now was able to use my phone to connect with my Arduino Nano 33 BLE board and transmit a data array and inform the user of the sample rate. Here is a screenshot when using the nRFconnect app.
Well, it is now 23h30 and time to call it a day.
Completing the Arduino Day Mission
It's 09h00 on Monday 18th of May 2020 and there's a ton of other business to get through first but the brain is keen to get working on this again. Naturally, we all get ahead of ourselves with ambition.
It is now lunchtime on Monday and I was thinking about how best to capture the BLE data using my laptop and decided to keep it within the Arduino family, so to speak, and use an ESP32 device as there is plenty of Arduino code examples about.
To create a BLE central device there is an ESP32 example called "bleclient.ino", which you can readily work off. The nice thing about this example is that you can get the ESP32 to scan and automatically connect with peripheral BLE devices that are advertising a particular service UUID. So, this is what I did. Then all you need to do is add in the GATT service and characteristics for my HinaH Hockey Stick nano 33 device and you are good to go. Well, sort of.
The ESP32 uses a notify callback function, but here you need to be careful not to overload this function with code as it very quickly gets snarled up and your ESP32 simply freezes. This happened to me a couple of times but once I had it cleaned up it seemed to work fine.
/*================================================== * CALLBACK FUNCTIONS *================================================== */ static void notifyCallback(BLERemoteCharacteristic* pBLERemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) { float MvmtData[6]; // Grab the movement data memset(MvmtData, '\0', sizeof(MvmtData)); if (length == 24) { //MvmntCntr++; uint8_t y = 0; for (uint8_t x = 0; x < length; x+=4) { MvmtData[y] = IEEE11073_2float(pData+x); y++; } for (y = 0; y < 6; y++) { Serial.print(MvmtData[y], 2); if (y < 5) Serial.print(", "); } Serial.println(""); } }
Then the next big challenge I had today was converting the 4-byte IEEE-11073 back to a float value.
I had never really created the full code logic for this (I had taken a short cut previously for temperature where I ignored converting the negative values and this was also done using MIT App Inventor using blocks!). This took me a good few hours to work it out for my ESP32 Arduino code. And here it is. It is rather simple once you know how:
/*================================================== * LOCAL FUNCTIONS *================================================== */ float IEEE11073_2float(uint8_t *dat) { int32_t Mantissa = (dat[2] << 16 | dat[1] << 8 | dat[0]); uint8_t Neg = bitRead(dat[2],7); int8_t fExp = dat[3]; if (Neg) Mantissa |= 255 << 24; return (float(Mantissa) * pow(10, fExp)); }
Then for readability of the serial output, I added in a button which allows the user to manually enable the movement data notifications:
// If we are connected to a peer BLE Server, update the characteristic each time we are reached // with the current time since boot. if (connected) { if (!NotifyEnable) { // We wait for a button press if (digitalRead(BTN_PIN)) { // See if we can enable Notifications for the movement the characteristic. if(pHinaMovementCharacteristic->canNotify()) { Serial.println("Movement data Notification Enabled"); Serial.flush(); NotifyEnable = true; pHinaMovementCharacteristic->registerForNotify(notifyCallback); } } } } else if (doScan) { BLEDevice::getScan()->start(0); // this is just eample to start scan after disconnect, most likely there is better way to do it in arduino }
And finally, I got to do my first live test:
I then cut and pasted that serial monitor data into Excel and created two charts, one of Acceleration (g) and one for Angular Velocity (°/s).
{gallery} Movement Data |
---|
There was nothing untoward that I could see from the BLE data received. I was able to obtain 6DOF data updates every 0.01 seconds or so, which is pretty good in my opinion.
So I am rather pleased with the result and how short it took to get this far. The gadget goal device using the BBC microbit can wait for another day as the spotlight needs to remain on the Arduino Nano 33 BLE board. It is tiny and makes movement analysis quick and simple.
I really enjoyed this 48 hour journey. Till next time...
Top Comments