Disclaimer:
First, here I will only explain the general ideas and only briefly explain how things work. But I will try my best to link useful resources which I used during this project.
Second, the Arduino - Oplà IoT Starter-Kit was provided by Arduino SRL in the context of the Arduino Cloud Games. I am very thankful for the opportunity to participate, the challenge kept me focused during the many problems I encountered on the way.
Part I. The MotivationWhen you read this Story, it is safe to assume that you are not a natural athlete who can do any sports with ease, else you would spend your time on the field, in the water, or on the track to train for the next big sports event. For us normal humans, it can be extremely difficult to acquire some good mechanisms, such as the perfect kick, throw, or even run. We normally need feedback during our training to improve our performance (regardless of the field). Most of the time this requires a second person with a trained eye to tell you what is going wrong. My project aims to replace this second person with technology. The sport I enjoy the most is baseball, some of you may not fully understand the rules, but everyone knows the picture of some huge guy with a wooden bat in their hand smashing a ball out of the park (If not enjoy this Video).
To archive such good swings, a lot has to come together, great hand-eye coordination, good strength, timing, and mechanics. The last two can be trained and observed without even contacting the ball, just by the dynamics of the bat. With the ideal swing, the bat hits a ball such that it has an exit velocity of up to 160 km/h and a launch angle of about 10°-25°.
So the hitters' goal is to accelerate the bat as fast as possible around the rotational axis (oneself) while having a slight upward motion going through the hitting zone, easy right?
Part II. The GoalYes, it is, if you can fully feel/know the dynamics of the bat during your swing. This is where the coach comes into play, he can see the movement and tell you what went wrong or good. Or you could just measure the dynamics of the bat with a microcontroller and read the stats from a display.
The Idea to measure the dynamics of the bat with an Inertial Measurement Unit (IMU) is not new [Link], there are even some commercial products available. But neither would it be fun to play around with a small Blackbox nor do I want to spend 150$ on something I can build myself for much more money :). Before we build our system lets set the goals:
The sensor side, goalsforthe BatSense:
- It will measure the dynamics of Bat using an IMU. More specifically, it will measure the acceleration (ax, ay, az) and the angular velocity (gx, gy, gz) of the bat at the end of the handle. Later we can calculate the dynamic stats of the sweet spot
- A big question was the sampling rate, since the average swing duration is about 150ms [Link], my initial thought was that acquisition of roughly 100 sampling points of the swing would be good. Therefore a sampling rate of about 670 Hz is required.
- This bat motion needs to be transformed in the reference frame of the batter, for this one can use an Attitude Heading Reference System Filters (AHRS). In this project I will use the Madwick filter, it is reasonably fast and accurate, and easy to implement.
- Since we want to acquire the bat data as fast as possible the BatSense only collects the data and transmits it to the Batstation via Bluetooth.
Processing and publishing,thegoalsfortheBatStation,
- The BatStation receives the measured data via Bluetooth and temporarily stores it into the RAM, a long-term goal would be to save the data on an SD-Card for later, further, analysis.
- The data needs to be preprocessed, for instance, it needs to be accounted for such things as roll-over, the AHRS-Filter will give values between +- 180° and so we will see roll-over from 180° -> -180° and vice versa. Also some smoothening will be applied by implementing a moving average filter.
- Next, we need to calculate the dynamic properties such as the maximum angular velocity around the z-axis inside the hitting zone as well as the launch angle.
- These statistics are then displayed on the carrier display and uploaded to the cloud.
I have divided the following build parts into four sections: two hardware and two software segments, one each for the BatSense and the BatStation.
Part III. Building the Hardware of the SystemTheBatSense
As described before, the BatSense gonna be placed inside the baseball bat handle, therefore we need a small device including a mobile power source. For the MCU I have chosen the Nano 33 BLE since it has a build-in BLE-Module (my initial choice for the data transfer) and an IMU.
For the power supply, I opted for a small LiPo-Battery typically used in iPod Shuffle. It has a max Voltage of 3, 7 V, which more than enough for our use case. The battery is connected to a charging circuit, which not only is used to charge the battery but also connects to the load.
In an illusion of long-term use per charge, I thought it would be good to be able to turn the BatSense ON and OFF. For the switching I placed a Pushbutton Power Switch between the Arduino and the charging circuit, a simple mechanical switch would be smaller but hey I love latching power switches.
Unfortunately, in the middle of the project, it turned out that I can not easily use the BLE module which is integrated into the Nano 33 BLE for data transfer. Therefore, I had to improvise at short notice and have decided on a data transfer via external Bluetooth modules (HC-05). So, I set up two HC-05 Modules in Master/Slave configuration [Link] and connect one module each to the Serial1-Pins [Link] of the two MCUs. A String which is written to Serial1 port of the BatSense than can be read at the Serial1 Port of the BatStation and vice versa. Quite neat, with that the BatSense is fully assembled
TheBatStation
Next is the BatStation, for its MCU I use the MKR Wifi 1010 supplied with the IoT-Kit. Since the BatStation will be stationary, I intend to use a USB-Powerbank as the power supply and we only have to connect the HC-05 module to the Serial1 Pins of the MKR.
Since the corresponding Pin 13 and 14 are intended to be used to control the relays of the IoT-Carrier I cut these pins of the header, I don't want to hear the relays switching with every Serial command coming from the Bluetooth module.
Those of you paying attention to detail will have noticed two things: Besides the RX/TX and power connection a fifth lead connected to the two HC-05 modules. This is connected to the reset pin of the HC-05 Module, I planned to implement a little power saving by turning the Bluetooth module only ON when needed, but didn't get to implement this feature into the script. So no further mentioning of this here. And second, in comparison to common tutorials on HC-05 Modules, I didn't use a resistance division circuit to archive a level shift at the RX/TX pins, this is not required since both the MKR Wifi and the Nano BLE are operating at 3, 3V so all save.
Nextstep,doestheBatSensefitintothehandle?
But first sanity checks:
- BatSense assembled, check,
- BatStation assembled, check,
- Quick function test (Battery charging, All Sensors readable, Data-Transfer via Serial1, Functions of IoT-Carrier Board working), check
So last thing to do is to mount the BatSense into the Bat. For this, I carefully extended the hole in the composite handle to produce some space for the BatSense
As you can see from the pictures, it doesn't quite fit in the handpiece and I can't safely expand the hole any further. Since I also do not have time to redesign the BatSense anymore, it will partially stick out for the time being.
Part IV. Make the Hardware work with SoftwareBatSense
About the Software of the BatSense, it can be split into three steps,
- Data acquisition of the IMU-Sensor
- Data transformation by applying the AHRS-Filter
- Data transfer via Bluetooth
Before I describe the individual function blocks I quickly explain the sequence within the loop. The loop can be split into two phases: In the first, we wait for measurement initialization. And in the second we measure if the measurement was initialized before.
For the initialization, the bat needs to be held still at an angle of about 45° to the ground. We can use the still condition to compensate for long-term drifts of the AHRS filter since for a still bat the angles can be calculated by the accelerations.
After the init, a short delay is added to give the batter time to get into the ready position.
// Init if bat is still and init measurement is false
if (fabs(gx) + fabs(gy) + fabs(gz) <= threshold && init_measure == false)
{
// What is the starting Angle of bat
phi = asin(-ax); // in rad
//Start Measurement only when Bat is held in right Angle of about 45°
if (phi >= 0.61 && phi <= 0.96)
{
i = 0; //reset running variable for measurement
init_measure = true; // Flag for measurement
delay(2000); // short break for init at bat station
}
}
After initializing the measurement, we measure the dynamics of the bat. To keep the delay between two measurement points as small as possible, I decided to measure all points of one swing in a loop and store the measurement points in arrays. The data is recorded in a while loop, this allows precise timing between the single measuring points.
// If initialized measure
if (init_measure == true)
{
// Loop measurement during cycle, only read IMU, apply filter and store data
// I use a while loop for waiting condition within loop
while (i < buffersize)
{
microsNow = micros();
if (microsNow - microsPrevious > microsPerReading) //
{
// Set Start of "last" measurement
microsPrevious = microsNow;
// measure values here and store data in arrays[i]
i++;
}
}
After all data points have been recorded, the timing is uncritically and we can send the data without worry. Lastly, a short waiting time is built-in.
// After all data points are acquired, stop measurement flag
init_measure = false; // reset init_measure
// Send Data after measurement, loop over buffersize
for (int i = 0; i < buffersize; i++)
{
//Compose and send Data String
}
// Short delay after each hit
delay(deadTime);
}
For the first function block, the data acquisition of the bat dynamics with the IMU I didn't use the common LSM9DS1-Libary of the Arduino database but the Version of Femme Verbeck [Link]. It has the advantage that one can change the Sampling Frequency (Output Data Rate (ODR)) from a measly 119 Hz to up to 476 Hz, which is almost the desired Value. Plus it also offers some nice calibrations parameters so Win-Win.
Here is a simple script that saves the IMU-Values
#include <LSM9DS1.h>
// Timingvariables
unsigned long microsPerReading, microsPrevious, microsDelta;
// IMU Variable
float gyroScale = 1; //Dont know why but it seams to work
float ax, ay, az;
float gx, gy, gz;
void setup()
{
if (!IMU.begin()) {
//Serial.println("Failed to initialize IMU!");
}
// Set IMu ranges
IMU.setAccelFS(3);
IMU.setAccelODR(5); //476 Hz
IMU.setGyroODR(5); //476 Hz
IMU.setGyroFS(2);
// Corrections
IMU.setAccelOffset(-0.012575, -0.028891, -0.002182);
IMU.setAccelSlope (1.004128, 1.008253, 1.001848);
IMU.accelUnit= GRAVITY; // or METERPERSECOND2
IMU.setGyroOffset (1.041443, -0.870422, 1.643921);
IMU.setGyroSlope (2, 2, 2);
IMU.gyroUnit= DEGREEPERSECOND;
// Timings for the read out
microsPerReading = 1000000 / 476;
microsPrevious = micros();
}
void loop()
{
// Read the Gyro and Accel Value and store them in gx,gy,...,az
IMU.readGyro(gx, gy, gz);
IMU.readAccel(ax, ay, az);
}
Next is the AHRS-Filter, as stated I used the MadgwickAHRS library, it is very simple to use. For simpler code examples let's assume we can access the IMU Values gx, gy, gz, ax, ay, az, without the upper code. Some nice info on the calculate roll, pitch and yaw angles can be found here [Link]. For This project, the most important is the yaw angle, since it is connected to the Bat speed.
#include <MadgwickAHRS.h> // Filter for the inertia transformation
#define sign(x) ((x) < 0 ? -1 : ((x) > 0 ? 1 : 0)) // the signum function
// Timingvariables
unsigned long microsPerReading, microsPrevious, microsDelta;
// values for orientation are stored in arrays :
int const buffersize = 1000;
float roll[buffersize];
float pitch[buffersize];
float yaw[buffersize];
long times[buffersize];
// Init Variable
int threshold = 15;
bool init_measure = false;
// initialize the filter:
Madgwick filter;
void setup() {
filter.begin(476.00); // Sets the filter speed
}
void loop()
{// If measurement is initialized
if (init_measure == true)
{
// Loop measurement during cycle, only read IMU, apply filter and store data
while (i < buffersize) //while loop for waiting condition
{
microsNow = micros();
if (microsNow - microsPrevious > microsPerReading) //
{
// Set Start of "last" measurement
microsPrevious = microsNow;
//Read current values of IMU
IMU.readGyro(gx, gy, gz);
IMU.readAccel(ax, ay, az);
// Apply filter to new values
filter.updateIMU(gx, gy, gz, ax, ay, az);
// Save Values in array
yaw[i] = filter.getYaw()-180-deltatheta;
roll[i] = filter.getRoll();
pitch[i] = filter.getPitch();
times[i] = (microsPrevious - cycleStart);
i++;
}
}
// After all values are acquired delete roll-over
// to delete roll over first check if angle changed more than 250°
int threshold_rollover=250;
for (int i = 1; i < buffersize; i++)
{ // loop over all values in array buffer
// if true, subtract or add (for this signum function) 360° to current value
if(abs(yaw[i]-yaw[i-1])>threshold_rollover)
{
yaw[i]=yaw[i]-sign(yaw[i]-yaw[i-1])*360;
}
if(abs(roll[i]-roll[i-1])>threshold_rollover)
{
roll[i]=roll[i]-sign(roll[i]-roll[i-1])*360;
}
if(abs(pitch[i]-pitch[i-1])>threshold_rollover)
{
pitch[i]=pitch[i]-sign(pitch[i]-pitch[i-1])*360;
}
}
}
The last step is sending the Data, for this I compose a String with the four values: times[i], roll[i], pitch[i], yaw[i])), with delimiter between the values ';' and for the end of line '/'.
String compose_data(float val1, float val2, float val3, float val4)
{
char delimiter = ';';
String s_times = String(val1, 0);
String s_roll = String(val2, 1);
String s_pitch = String(val3, 1);
String s_yawn = String(val4, 1);
String temp_compos = s_times + delimiter + s_roll + delimiter + s_pitch + delimiter + s_yawn + delimiter+'/';
return temp_compos;
}
And send the strings for each index of the arrays one by one by printing it to Serial1.
// Send Data after measurement, loop over buffersize
for (int i = 0; i < buffersize; i++)
{
//Compose and send Data String
Serial1.print(compose_data( times[i], roll[i], pitch[i], yaw[i]));
Serial1.flush();
// For debugging its sometimes good to write the Value also to Serial
//Serial.println(compose_data( 0, roll[i], pitch[i], yaw[i]));
}
Here is one important note. During my work, I got confused between Serial.Print and Serial.Write. The Serial.Write function can be used to directly write an INT up to a value of 255. In my tests, I implement exactly this case and all looked fine while testing. Going to real data, nothing worked it looked like I get a lot of 40's 50's Values. This comes from the circumstances, that if you send anything else it will be split in Bytes and you have to write a Byte2Char function. I wasted a lot of time on this problem until I noticed that I can simply use the Serial.Print() function and decompose the values later.
So if you don't receive your values but some curios 2 digited values you got some bytes :D. Else, Hurray all done you now should get continuous data whenever you tilted the sensor by about 45°.
Next to theBatStation
And again I try to split the functions of the BatStation in some useful way. Let's start with the setup of the device, you need two libraries for the carrier board and the cloud
#include "thingProperties.h" // All properties are safed here
#include <Arduino_MKRIoTCarrier.h> // lib for the carrier board
For the carrier, you should define your LED colors, as it is much easier to work with these
// colors for LED
uint32_t redColor = carrier.leds.Color( 0, 255, 0);
uint32_t greenColor = carrier.leds.Color( 255, 0, 0);
uint32_t blueColor = carrier.leds.Color( 0, 0, 255);
uint32_t yellowColor = carrier.leds.Color( 255, 255, 0);
uint32_t offColor = carrier.leds.Color( 0, 0, 0);
In the setup the properties, like shared variables and Network info will be initialized by initProperties();
and you need to connect to the Cloud ArduinoCloud.begin(ArduinoIoTPreferredConnection);
and regularly update the cloud ArduinoCloud.update();
Last we need to set up the display and the buttons
CARRIER_CASE = true;
carrier.begin();
carrier.display.setRotation(0);
carrier.display.fillScreen(ST77XX_WHITE);
carrier.display.setTextColor(ST77XX_RED);
Within the loop we need to update the Button status to register presses with carrier.Buttons.update();
Now we can start with getting the data. Since we get all data in one string, a wrote a struct object, to be able to store all Infos
// Stuct of batdata
struct dynamic
{
int times;
float roll, pitch, yaw;
};
dynamic bat;
Then at the start of the loop, we check if there is some data available with Serial1.available, and parse the Data by the delimiters
void loop()
{if (Serial1.available())
{
bat = parseSerial(Serial1.readStringUntil('/'));
}
}
//Function to parse String into Data
dynamic parseSerial(String serialData)
{ serialData.toCharArray(serialBuffer, SERIAL_BUFFER_SIZE_1);
int val1 = atoi(strtok(serialBuffer, ";"));
float val2 = atof(strtok(NULL, ";"));
float val3 = atof(strtok(NULL, ";"));
float val4 = atof(strtok(NULL, ";"));
return {val1, val2, val3, val4};
}
Then if the bat.times is zero the data collection is started
if (bat.times == 0 && initBat == false)
{
....
But first, we use the init delay of the BatSense to indicate the initialization to the Batter. The lights will turn blue one by one with all 5 five on after 2 seconds.
for (int k = 1; k < 6; k++)
{
// Indicate Initialization by lighting up increasin number of led
carrier.leds.fill(blueColor, 0, k);
carrier.leds.show();
delay(400);
}
Then we store the data as long as it is available. For this, I run a for loop over the buffersize. Since I had a lot of problems with the remaining amount of RAM I had to directly calculate the change of the angles and store these values.
for (i=0;
{
// Check if Data is available
if (Serial1.available())
{
// read Data
bat = parseSerial(Serial1.readStringUntil('/'));
// current Data
times[i] = bat.times;
if (i == 0)
{
droll[0] = 0;
dpitch[0] = 0;
dyaw[0] = 0;
}
else
{
dtimes = (times[i] - times[i - 1]);
droll[i] = 10*(bat.roll - pastRoll)/(dtimes/1000);
dpitch[i] = (bat.pitch - pastPitch)/(dtimes/1000);
dyaw[i] = (bat.yaw - pastYaw)/(dtimes/1000);
}
// Save last Value for next iteration
pastRoll = bat.roll;
pastPitch = bat.pitch;
pastYaw = bat.yaw;
}
}
When all data points are collected reset collection and indicate completion by yellow LED's
if (initBat == true || i == bufferSize - 1 )
{
// Reset Initialization
carrier.leds.fill(yellowColor, 0, 5);
carrier.leds.show();
initBat = false;
i = 0;
measuredData = true;
}
After this step I had the same problem as many others, the total loop can take longer than 10s, so I needed to place a short update to the Cloud to prevent resets. // Hint to the arduino guys please let us specify the updaterate :)
ArduinoCloud.update();
After the data collection comes the filtering, for my initial project I try a moving average filter:
for (int n = 1; n < bufferSize-1; n++)
{
// Save current value for next loop
currentdroll=droll[n];
currentdpitch=dpitch[n];
currentdyaw=dyaw[n] ;
// calculate average over 3 adjacent values
droll[n] = (pastdroll+droll[n]+droll[n+1])/3 ;
dpitch[n] = (pastdpitch+dpitch[n]+dpitch[n+1])/3;
dyaw[n] = (pastdyaw+dyaw[n]+dyaw[n+1])/3;
// past over old value
pastdroll=currentdroll;
pastdpitch=currentdpitch;
pastdyaw=currentdyaw;
}
Now to the interesting part, calculating the dynamic properties. And again a problem, I wanted to use the initial angle of the bat to determine the optimal position, but since I ran out of RAM I had to change plans. Using some information on the bat swing.
For the following calculations, we assume that the bat speed is the highest around the optimal hitting point. This is reasonable since initially the bat speed increases and just after the bat passes the home plate the bat is raised up and a lot of the speed is going into an upwards motion, and by this the speed the yaw angle changes will decrease.
So for our calculations we I need to determine the index of the maximum acceleration, for which I use a simple function.
// Function to find index of maximum value
int getIndexOfMaximumValue(float* measuredArray) {
int maxIndex = 0;
float max = measuredArray[maxIndex];
for (int i = 1; i < bufferSize; i++)
{
if (max < measuredArray[i])
{
max = measuredArray[i];
maxIndex = i;
}
return maxIndex;
}
And then calculate the corresponding stats of the Bats at this point are
maxYaw = dyaw[indexofMax];//dyaw[indexofMax];
rollAtMax = droll[indexofMax];
launchAngle = dpitch[indexofMax];
// publish data
maxVelo = maxYaw * (batLength / (39.37)) * 3.6; //grad per second to kilometer per hour;
Since the velocity of the end of the bat depends on its length, we need to be able to simply change this between two batters. The length can be changed either via the cloud app or by a push buttons
// Push Button 4 to decrease batLength by one
if (carrier.Buttons.onTouchDown(TOUCH4))
{
if (batLength > 30 )
{
--batLength;
printDynamics(maxVelo, launchAngle, rollAtMax);
}
}
// Push Button 5 to increase batLength by one
if (carrier.Buttons.onTouchDown(TOUCH3))
{
if (batLength < 35 )
{
++batLength;
printDynamics(maxVelo, launchAngle, rollAtMax);
}
}
Now we have the information we want and need to feed them back to the batter. Once all Infos are collected they are printed on the display of the IoT Carrier and synchronized with the cloud
printDynamics(maxVelo, launchAngle, rollAtMax);
//Function to print values on carrier display
void printDynamics(float val1, float val2, float val3)
{
carrier.display.fillScreen(ST77XX_WHITE);
carrier.display.setTextColor(ST77XX_RED);
carrier.display.setCursor(60, 30);
carrier.display.setTextSize(2);
carrier.display.print("Bat Station");
carrier.display.setCursor(30, 60);
carrier.display.print("Bat Length:");
carrier.display.setCursor(160, 60);
carrier.display.print(String(batLength));
carrier.display.setCursor(30, 100);
carrier.display.print("Init Swings:");
carrier.display.setCursor(180, 100);
carrier.display.print(String(meancounter + 1) + "/5");
carrier.display.setCursor(30, 120);
carrier.display.print("Max Velo:");
carrier.display.setCursor(140, 120);
carrier.display.print(String(val1, 1));
carrier.display.setCursor(30, 140);
carrier.display.print("Launch:");
carrier.display.setCursor(140, 140);
carrier.display.print(String(val2, 1));
carrier.display.setCursor(30, 160);
carrier.display.print("Roll");
carrier.display.setCursor(140, 160);
carrier.display.print(String(val3, 1));
}
But these are just numbers what do they mean? I thought feedback like, your swing was good or bad would be much more useful. So let's create more useful feedback.
One of my personal problems is, that my bat speed without a target or from a tee is good (okay let's say not bad). But when I face a pitcher my bat speed reduces drastically since I try to adapt my timing by adapting the bat speed. This is really bad behavior, one way to quantify this problem is to compare the actual speed to an optimal situation, for this the BatStation stores an average value.
// Calculate mean if meancounter is below 5
if (meancounter < 5)
{
velo[meancounter] = abs(maxVelo);
meancounter++;
if (meancounter == 5)
{
meanVelo = average(velo); // stores meanvalue of array
}
}
and compares this average
float average (float * array) // assuming array is a float
{
float sum = 0 ; //
for (int i = 0 ; i < sizeof(array) ; i++)
{
sum += array [i];
} ;
float avg = sum/sizeof(array);
return avg ;
}
to your current velo.
if (meancounter == 5)
{
// This line is just for the cloud
currentVelo=abs(maxVelo)/meanVelo*100;
// check if current Velo is at least 90% of average init swings
if (abs(maxVelo) >= meanVelo - 0.1 * meanVelo)
{
BlinkGreen();
}
else
{
BlinkRed();
}
}
Yet, You don't have to reach the mean value but at least 90%, I found this is my personal sweetspot. At the beginning of a session, I perform 5 free swings and then do my batting practice. If one reaches the velocity goal you get a green Blink else a red Blink of the carrier board. This is easy to see for you, the entire team, and maybe some planes above you (man these LED's are bright).
// Blink red
void BlinkRed()
{
for (int i = 0; i < 11; i++)
{
carrier.leds.fill(redColor, 0, 5);
carrier.leds.show();
delay(200);
carrier.leds.fill(offColor, 0, 5);
carrier.leds.show();
delay(200);
}
}
// Blink green
void BlinkGreen()
{
for (int i = 0; i < 11; i++)
{
carrier.leds.fill(greenColor, 0, 5);
carrier.leds.show();
delay(200);
carrier.leds.fill(offColor, 0, 5);
carrier.leds.show();
delay(200);
}
}
With a push of a button, we can reset the mean value and the next 5 swings will be used to calculate a new average.
// Push Button 0 to init mean measurement
if (carrier.Buttons.onTouchDown(TOUCH0))
{
meancounter = 0; // sets mean count to zero inits averaging
printDynamics(maxVelo, launchAngle, rollAtMax);
}
I also wanted to indicate the roll of the barrel but due to my limited time, these were all features I could implement so far.
Andfor final demonstration in action, I planned to show some footage from our training session today, but of course last-minute nothing wants to work. Six hours later I found out that the HC-05 of the BatSense was broken, and a quick replacement later it worked.
And I could shoot a quick demonstration
But not for long, this was the first shot, (still with cat on the couch), but bad luck hit me during recharging the BatSense, the Bat fell and broke the USB connector off.
So no more power for the Bat and I have to live with this video great ... .
Part V. ProblemsTheBLEandWIFidilema
As quickly noted before I wasn't able to use both WiFi and BLE connection on the MKR Wifi, which is a Hardware limit. In the great Discord-Channel of the Cloud Games it was mentioned that switching between both modems would be feasible [Link]. I have chosen not to go this road, since the data rate with BLE was anyhow quite limited and many problems came with it. One good idea could be to shift from the Nano 33 BLE to the Nano 33 IoT and go to a full Wifi communication
IamoutofRAM
One of my main problems with the BatStation atm is the available RAM to store the incoming data. I had to reduce the data acquisition time down to only 2s to not crash the BatStation. A little bit more ram would be nice to allow 5s storage and the original IMU data for debugging and maybe coming ML applications. And the BatSense can do this as it is much lighter programmed.
Solution? The Portenta H7 with 8MB RAM :D, or reduce the functionality of the BatStation. So far I don't know how much RAM is reserved for the cloud service but an Offline Version would be my go-to option.
MessyData
And a very nasty software/hardware problem, some swings are not correctly registered. So far I can't say if this is a Problem on the BatSense or the BatStation but some fast swings are registered much slower. But this could also be a problem which is solved with more RAM.
Part V. Room for improvementAt last, we have reached the last part, and what remains is the question "What next?" In all honesty, it must be said that the system is not yet suitable for training use.The output data is still error-prone, the measurement time is limited and the BatSense does not yet fit completely into the handle. The project implemented here is more a proof-of-principle than a usable system.
So let's set some goals for the future:
- improve the data acquisition rate, maybe also outsource the AHRS-Filter to the Batstation if I get the RAM problem fixed. Or use an external IMU Sensor, which can buffer the readings into external memory.
- improve the data processing on the BatStation Site, I think that the problems are more remaining Bugs than real problems but let's see,
- decrease the size of the BatSense, the Nicola Sense ME is on my wishlist, and with it, it should be feasible to decrease the size drastically.
I think these are enough goals for my next vacation, and more will definitely come when the system is in active use.
In that spirit, I want to say to my fellow Could Games participants:
In my opinion, a project is never fully finished it is just sometimes getting out of the beta phase.
Comments