Skip to the content.

SmartSole

The SmartSole is a smart shoe sole designed to monitor physical activity by tracking steps, jumps, and running patterns using embedded sensors. It provides alerts through sound or vibrations.

Engineer School Area of Interest Grade
Aarav A Stratford Preparatory Computer Science Incoming Sophomore
SmartSole Final

First Modification

Description

In my first modification, I implemented a comprehensive command line interface (CLI) system that allows users to interact with the SmartSole through Bluetooth Low Energy (BLE) serial communication. This modification was much easier than other code segments I wrote for this project as it intersected with many past skills of mine. The CLI system takes in commands the user enters by text as well as any arguments separated by a single space.

The system features a robust command structure with 17 different commands covering sensor data retrieval, system control, and even entertainment features. Commands include getting the current step count, distance traveled in meters, gravity vector on the accelerometer, FSR pressure readings, orientation data, and more.

Command System Architecture

The CLI is built around a Command struct that maps command names to their corresponding functions:

struct Command {
  const char* name;
  void (*func)(const String& args);
};

To add new commands, you simply:

  1. Create a function with the signature void functionName(const String& args)
  2. Add an entry to the command array
  3. The system automatically handles command parsing and execution
void foo(const String& args) {
    //your code goes here
}

Command cmd[] = {
  {"foo", foo},
  // ... other commands
};

Available Commands

Sensor Data Commands:

System Commands:

Entertainment Commands:

Argument Parsing

The system includes a custom stoll() function for parsing long long integers from command arguments, with proper overflow handling and whitespace tolerance:

ll stoll(const String& s) {
  ll res = 0;
  int sn = 1;
  int i = 0;
  while (s[i]==' '||s[i]=='\t'||s[i]=='\n'||s[i]=='\r'||s[i]=='\f'||s[i]=='\v') i++;
  if (s[i]=='-') {sn = -1; i++;}
  else if (s[i]=='+') i++;
  while (s[i]>='0'&&s[i]<='9') {
    if (res>(LONG_LONG_MAX/10)||(res==(LONG_LONG_MAX/10)&&(s[i]-'0')>(LONG_LONG_MAX%10))) {
      res = LONG_LONG_MAX;
    }
    res = res*10+(s[i]-'0'); i++;
  }
  return sn*res;
}

Advanced Features

Matrix Exponentiation for Fibonacci: The fib command uses matrix exponentiation for efficient calculation of large Fibonacci numbers:

struct Matrix {
  ll m[2][2];
  Matrix operator*(const Matrix& b) const {
    Matrix res = {};
    for (int i=0; i<2; i++) for (int j=0; j<2; j++) for (int k=0; k<2; k++) 
      res.m[i][j] = (res.m[i][j]+m[i][k]*b.m[k][j])%MOD;
    return res;
  }
};

Random Number Generation: The system uses Mersenne Twister for high-quality random number generation:

std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int> rng(1, 6);

Integration with Existing Systems

The CLI seamlessly integrates with all existing SmartSole systems:

Challenges

Some challenges I had while implementing this modification included:

Argument Parsing: Arduino strings don’t have a standard way to parse for some data types, so I had to implement my own stoll() function for parsing long long integers. This required careful handling of overflow conditions and various edge cases like whitespace and sign characters.

Memory Management: Since this runs on an ESP32 with limited memory, I had to be careful about string allocations and avoid dynamic memory allocation that could cause memory leaks. I used static arrays and careful string handling to maintain system stability.

Command Structure Design: Designing a scalable command system that could easily accommodate new commands while maintaining good performance required careful consideration of the data structures used. I chose a simple array-based approach over more complex hash maps to avoid memory overhead.

Bluetooth Integration: Ensuring reliable communication between the CLI and Bluetooth serial interface required proper error handling and buffer management to prevent data loss or corruption.

Real-time Data Access: Providing real-time access to sensor data through commands required careful synchronization to ensure data consistency across multiple command calls.

Next Steps

For future enhancements to the CLI system, I plan to:

Command History: Implement a command history feature that allows users to recall and re-execute previous commands using arrow keys or a history command.

Scripting Support: Add the ability to create and execute simple scripts containing multiple commands, enabling automated data collection and analysis workflows.

Configuration Management: Add commands to save and load system configurations, allowing users to customize thresholds, alerts, and other parameters.

Web Interface: Develop a web-based interface that can be accessed through a smartphone browser, providing a more user-friendly alternative to the text-based CLI.

Third Milestone

Description

My final milestone focused on the final assembly of my product. In this milestone I created various 3D prints, like casings for my battery pack and ESP32. I also made various clip designs to attach the product components to. In the end I attached the battery to a ankle strap made of velcro and only used the clip for the ESP32. I also attached the FSR to the insole of the shoe with tape. I utilized the flex PCB to handle the connections from the ESP32 to the FSR.

3D Prints

In this milestone, I designed and printed several custom 3D parts to house and protect the electronics. These included casings for the battery pack and ESP32, as well as various clip designs to securely attach components to the shoe and ankle strap. The final design used a clip for the ESP32 and the battery attached to a velcro ankle strap for comfort and stability.

Shoe Clip

Each Clip Version
Figure#, The first clip from the left is my first attempt at making the clip, but it did not even attach to the shoe collar so I opted to remake it to be wider. The second clip was able to fit on the shoe's collar but lacked the strength and sleekness of the first design, so it was scrapped. The third clip was designed to be able to fit on the lip of the shoe rather than the collar, keeping it space efficient and strong.


Clip Design #2
Figure #, I was testing how I could modify the other clip, but it just came out looking like the Doordash logo


Clip CAD Drawing
Figure #, This is the other iteration's CAD drawing that had 2 legs of equal length. Through this I learned that having equal leg length is not as strong as having unequal leg length.

Battery Case

Battery Case
Figure #, This is the battery case that I did not end up using as an ankle strap was more convenient

ESP32 Case


ESP32 Case
Figure #, The only issue with the ESP Case was the size of the USB-C port being too small, so the ESP would not fit in nicely. Other than that, this case is very sturdy other than the lid which sometimes can fall off just due to small errors in the print


ESP Case Body CAD Drawing
Figure #, This is a CAD drawing of the ESP32's case body. All dimensions are in mm.


ESP Case Lid CAD Drawing
Figure #, This is a CAD drawing of the ESP32's case body. All dimensions are in mm.

Soldering & Flex PCB

I used a flexible PCB to connect the ESP32 to the FSR and other components, which made the wiring more robust and suitable for the constant movement inside a shoe. Soldering the connections onto the flex PCB was a delicate process, but it greatly improved the durability and reliability of the system. The breadboard and the PCB share the same layout of connections while vastly varying in size and strength as the breadboard had no soldered connections. All connections were tested for continuity and strength before final assembly.


Soldered Connections on PCB
Figure #, Here, the leftmost rail is the power rail while the middle rail is the ground rail. The rightmost section is reserved for the FSR wiring.


Soldered Connections on PCB
Figure #, This is the other side of the flex PCB with the components/wires on it. The wiring here is directly taken from the breadboard and adapted for the size constraint of the flex PCB.

Assembly & Integration

The final assembly involved attaching the battery to the ankle strap using velcro, mounting the ESP32 with the custom clip, and securing the FSR to the insole of the shoe with tape. The velcro ankle strap that is pictured below works by just wrapping around itself. I took particular care to ensure that all components were firmly attached. The flex PCB allowed for easy routing of wires and minimized bulk inside the shoe. If you would like to see the final image of the SmartSole please scroll up.

Soldered Connections on PCB
This is the inside of the strap, that fuzzy velcro piece is for the outside of the strap to attach to when it wraps around the ankle.


Soldered Connections on PCB
This is the spiky velcro outside of the strap.

Challenges

Some challenges I had while assembling the final product were soldering the connections on the flex PCB as I burned multiple joints off the flex PCB before I could connect two joints together with solder. Another challenge was finally deciding on a good design as both the clip and ankle attached devices had downsides I did not like, so I ended up just choosing a hybrid system between them where I attached the battery to the ankle and left the rest of the smaller components on a clip on the shoe.

Next Steps

In the future I hope to learn more about 3D printing and designing with CAD. I also want to learn more about electrical engineering to be able to build more complex products in the future.

Second Milestone

Description

This milestone focused on collecting sensor data and using a variety of visualizations to better understand and improve my design and code. I dedicated significant time to building a Bluetooth app for connecting to my ESP32, which proved to be much more challenging than anticipated. Attaching the device securely to my foot also required several iterations, but after some trial and error, I achieved a working prototype. With these foundations in place, I now have a functional system and am excited to keep refining it.

Diagrams & Visualizations

To gain deeper insights into the data and system behavior, I created several types of diagrams and visualizations:

1. Linear Magnitude Analysis

The linear acceleration magnitude (linmag) was the most important signal for detecting steps, jumps, and other activities. I focused on several visualizations and analyses to understand and tune my algorithms:

a. Rolling Statistics on Linear Magnitude
Linear Magnitude Rolling Stats

I calculated the linear acceleration magnitude and overlaid rolling mean and standard deviation statistics. This made it much easier to spot steps and repetitive motions, and to set robust thresholds for event detection.

b. Time Series Decomposition of Linear Magnitude
Decomposition of Linear Magnitude

I broke down the raw linmag data into trend, seasonality, and noise components. This decomposition helped me identify patterns in walking, running, and jumping, and filter out irrelevant fluctuations.

c. Power Spectral Density (PSD) and FFT of Linear Magnitude
PSD and FFT of Linear Magnitude

I performed a frequency analysis (FFT and power spectral density) on the linmag signal. This revealed dominant frequencies corresponding to step rates and helped distinguish between walking, running, and jumping. It was a great help for tuning my detection algorithms and understanding the periodicity of different activities.


2. Orientation and 3D Motion Visualizations

While linmag was the primary focus, I also used other visualizations to understand the device’s behavior:

a. Accelerometer Orientation
Accelerometer Orientation GIF

A dynamic visualization showing a 3D cube representing the accelerometer’s orientation over time as I moved. This helped me intuitively understand how the device tracked foot motion and rotations.

b. 3D Acceleration Space & Trajectory
3D Acceleration Trajectory

By plotting the acceleration data in 3D space, I could visualize the trajectory of my foot during different activities. This was especially useful for distinguishing between steps, jumps, and other movements. I also mapped the orientation data (pitch, roll, yaw) in a 3D plot to observe how the foot’s orientation changed during various activities, which helped in fine-tuning the movement detection algorithms.

3. Electrical Schematic

Milestone 1 Schematic

This schematic shows the wiring and electrical connections for my project during testing. It helped ensure all components were connected correctly and provided a reference for troubleshooting hardware issues.

Challenges

Some of the biggest challenges I faced during this milestone included:

Next Steps

Moving forward, I plan to make the hardware setup more robust and user-friendly by designing custom housings for the electronic components and soldering all connections onto a flexible PCB. This will improve durability and comfort, making the device much more practical for everyday use.

First Milestone

Description

Hardware

The ESP32

Software

In my first milestone, I focused on the algorithms that would power my project in the future. I built a few algorithms:

  1. Movement Detection

    This algorithm is designed to optimize power usage by determining whether the user is moving. If the user is stationary, there is no need to check for steps, since movement is a prerequisite for taking a step. The algorithm works by calculating the change in pitch and roll angles between the current and previous readings. If either the change in pitch or roll exceeds a specified threshold (measured in degrees), the system considers the user to be moving. In simpler terms, if the accelerometer detects a significant enough change in orientation, movement is registered. While developing this algorithm, I faced challenges with the AHRS (Attitude and Heading Reference System), which sometimes failed to accurately determine the accelerometer’s orientation.

     static float lastRoll = roll, lastPitch = pitch;            //values to find the respective delta values
    
     const float threshold = 5.0f;                               //degrees, the amount of movement required to register movement
    
     float deltaRoll = fabs(roll-lastRoll);                      //we take the delta roll
    
     float deltaPitch = fabs(pitch-lastPitch);                   //then we take the delta pitch
    
     if (deltaRoll>threshold||deltaPitch>threshold) {
                                                                 //the above line checks if either delta pitch or 
                                                                 //delta roll exceeds the threshold
    
         if (!isMoving) {                                        //if we are not already moving
    
             isMoving = 1;                                       //if we have exceeded the threshold then we must be moving
                                                                 //so our flag must be true here
    
                                                                 //an action can be performed here
         }
     } else {                                                    //otherwise
    
         if (isMoving) {                                         //if we are moving
    
             isMoving = 0;                                       //this is set to false so that next iteration we can
                                                                 //still detect movement as not changing it would be a
                                                                 //1 way switch rather than a 2 way switch
    
                                                                 //an action can be performed here as well
         }
     }
     lastRoll = roll;                                            //shift last roll for the next iteration
     lastPitch = pitch;                                          //shift last pitch for next iteration
    

    Time Complexity: \(O\left(1\right)\)

    Space Complexity: \(O\left(1\right)\)

  2. Step Detection

    This algorithm is used to estimate distance currently and will be used in the future to estimate health. It serves as a step detection algorithm using accelerometer data. The algorithm computes the magnitude of the acceleration vector and estimates the gravitational component. This gravity estimate is subtracted from the raw magnitude to obtain a filtered signal representing the true acceleration. The algorithm then calculates a dynamic threshold based on the average magnitude of recent samples. The algorithm looks for valleys and peaks in the filtered signal that meet specific criteria for step detection: the amplitude must exceed the threshold, the time interval between steps must be less than around 250 ms, and a valid valley must precede the peak. When all these conditions are satisfied, a step is registered. I encountered a lot of challenges when developing this algorithm such as the math behind the low pass filter, and implementing the gravity filtration with the AHRS gravity filtration system.

     void update(const sensors_event_t& accel) {
         float magnitude = sqrt(
             accel.acceleration.x * accel.acceleration.x +
             accel.acceleration.y * accel.acceleration.y +
             accel.acceleration.z * accel.acceleration.z        //we calculate the magnitude of acceleration using sqrt(ax^2 + ay^2 + az^2)
         );
    
         gravity = alpha * gravity + (1 - alpha) * magnitude;   //then we apply low pass filter to estimate gravity
    
         float filteredMagnitude = magnitude - gravity;         //we remove gravity from raw acceleration to get filtered magnitude
    
         sum += fabs(filteredMagnitude);                        //then accumulate the absolute filtered magnitude (for average)
         count++;                                               //increment the sample count (for average)
    
         if (count >= 50) {                                     //every 50 samples, update the dynamic threshold (50 is arbitrary)
             float average = sum / count;                       //we get the average magnitude
             threshold = baseThreshold + average * 0.2f;        //then we set the threshold based on baseline and average
             sum = 0.0f; count = 0;                             //finally reset sum and count for next window
         }
    
         unsigned long now = millis();                          //we get current time in milliseconds
    
         static float lastValley = 0.0f;                        //last valley value, initialized to 0, could also be -inf
         static float lastPeak = 0.0f;                          //last peak value, initialized to 0, could also be -inf
            
         static bool detectValley = 0;                          //a flag for detecting a valley
    
         //detect valley: previous magnitude > last magnitude < current filtered magnitude, and last magnitude is less than 
         if (previousMagnitude > lastMagnitude && lastMagnitude < filteredMagnitude && lastMagnitude < -threshold * 0.5f) {
             lastValley = lastMagnitude; detectValley = true;
         }
    
         //detect peak: previous magnitude < last magnitude > current filtered magnitude, and last magnitude exceeds threshold
         bool detectPeak = (previousMagnitude < lastMagnitude) && (lastMagnitude > filteredMagnitude) && (lastMagnitude > threshold);
    
         bool amplitudeOk = (lastMagnitude - lastValley) > (threshold * 0.5f); //check if amplitude between last peak and last valley is above threshold
    
         //if a valid peak is detected, amplitude is sufficient, enough time has passed since the last step, and a valley was detected
         if (detectPeak && amplitudeOk && (now - lastStepTime) > minStepInterval && detectValley) {
             lastStepTime = now;                                //we update last step time
    
             stepCount++;                                       //then increment step count
    
             detectValley = 0;                                  //then reset valley detection flag
    
             lastPeak = lastMagnitude;                          //we store last peak value
         }
    
         previousMagnitude = lastMagnitude;                     //finally update previous magnitude for next iteration
         lastMagnitude = filteredMagnitude;                     //and update last magnitude for next iteration
     }
    

    Time Complexity: \(O\left(1\right)\)

    Space Complexity: \(O\left(1\right)\)

  3. Distance Estimation

    This algorithm estimates the distance traveled by the user by combining step detection from the pedometer with dynamic stride length estimation. It calculates stride length based on the peak acceleration detected during each step, then multiplies the stride length by the number of steps taken to update the total distance. This approach adapts to the user’s walking or running style for more accurate distance measurement. The only challenge I faced when creating the distance estimation algorithm was deriving the formula that I used to calculate the user’s stride length.

     float strideLength = 0.7f;                                  //initial stride length   (meters)
     float distance = 0.0f;                                      //initial total distance  (meters)
    
     int stepCount = pedometer.getStepCount();                   //we get the current step count from the pedometer
     int lastStepCount = 0;                                      //initialize our last step count so we can compare
    
     static float maxLinearMagnitude = 0.0f;                     //we also need to the maximum linear acceleration magnitude since the last step
    
                                                                 //we update the maximum linear magnitude if the current value is higher
     if (linearMagnitude > maxLinearMagnitude)
         maxLinearMagnitude = linearMagnitude;                   //set the new higher value as the new max
    
     if (stepCount > lastStepCount) {                            //if a new step has been detected (step count increased)
                                                                 //we will be estimating step length based on the peak acceleration (maxLinearMagnitude)
                                                                 //formula: base length + scale * (peak acceleration - 1g)
                                                                 //the base length is 0.45 because that is the average stride length in meters
                                                                 //the scale is 0.25 because of empirical testing
                                                                 //9.80665 is an exact value for gravity on earth (conversion for m/s^2 to g)
    
         float newStrideLength = 0.45f + 0.25f * (maxLinearMagnitude / 9.80665f - 1.0f);
    
         if (newStrideLength < 0.3f) newStrideLength = 0.3f;     //lower bound clamp
         if (newStrideLength > 1.2f) newStrideLength = 1.2f;     //upper bound clamp
    
         strideLength = newStrideLength;                         //replace the previous stride length with our current stride length
    
                                                                 //calculate how many steps were taken since the last update
         int StepsTaken = stepCount - lastStepCount;
    
         distance += stepsTaken * strideLength;                  //add the distance for these steps to the total distance
    
         lastStepCount = stepCount;                              //update the last step count
    
         maxLinearMagnitude = 0.0f;                              //finally reset the maximum linear magnitude for the next iteration
     }
    

    Time Complexity: \(O\left(1\right)\)

    Space Complexity: \(O\left(1\right)\)

  4. FSR Variance

    This algorithm is used to determine if the user’s foot, which is pushing down on the FSR (Force Sensitive Resistor), is not applying consistent force. It uses a sliding window approach to calculate variance in the force readings over time. The algorithm maintains a circular buffer of the last 10 FSR readings and computes both the average and variance of these values. When the variance exceeds a predetermined threshold, it indicates that the user’s foot pressure is inconsistent, which could signal improper form or instability. The system also includes a stability check to detect sudden large changes in force readings, and if the readings become stable again, it resets the window to the current value to establish a new baseline. This variance-based approach provides a quantitative measure of foot pressure consistency during exercise.

     float fsrWindow[10];                                        //the sliding window array
                                                                 //the size is the number of samples in the window
    
     int fsrIndex = 0;                                           //a pointer to the current index of the window
    
     bool fsrFullWindow = 0;                                     //flag for if the window is full
    
     int fsrValue = analogRead(fsr);                             //read the analog output of the fsr sensor (0-4095 on esp32)
    
     fsrWindow[fsrIndex++] = fsrValue;                           //add that fsr analog value to the window at the index+1 to move it to empty space
        
     if (fsrIndex>=10) {                                         //if our index is now bigger than our window size, our window is full
         fsrIndex = 0; fsrFullWIndow = true;                     //we reset the index and flag our window to be full
     }
    
     float fsrAverage = 0, fsrVariance = 0;                      //we initialize average and variance variables to track and calculate them
    
     int n = fsrFullWindow ? 10 : fsrIndex;                      //n here is the number of valid samples in the window (10 if full, otherwise its fsrIndex)
    
     for (int i = 0; i < n; i++) fsrAverage += fsrWindow[i];     //sum all values in the window to compute the average
    
     fsrAverage /= n;                                            //divide by n to get the average FSR value
    
     for (int i = 0; i < n; i++)                                 //now, for each value in the window
         fsrVariance += (fsrWindow[i] - fsrAverage) *            //subtract the average and square the result, then sum
                        (fsrWindow[i] - fsrAverage);
    
     fsrVariance /= n;                                           //divide by n to get the variance (average squared deviation)
    
     bool fsrStable = true;                                      //flag for if our fsr reading is stable (we assume it is right now)
    
     for (int i = 0; i < n; i++) {                               //for each value in the window
         if (abs(fsrWindow[i] - fsrValue) > 500) {               //if the difference between the current window sample 
                                                                 //and the first read value is greater than 500 (arbitrary)
    
             fsrStable = 0;                                      //then we do not have a stable fsr window and need to change the flag
             break;                                              //exit the loop once we have concluded its unstable
         }
     }
     if (fsrStable&&fsrFullWindow) {                             //if we are stable and our window is full
         for (int i = 0; i < 10; i++) fsrWindow[i] = fsrValue;   //we iterate through the entire window and reset to the initial value
         fsrAverage = fsrValue;                                  //then the average gets reset as the initial value as well
         fsrVariance = 0;                                        //since we are resetting variance needs to be restored to 0
     }
    
     const float FSR_VAR_THRESHOLD = 175175.0f;                  //completely arbitrary value for the variance threshold
    
     if (fsrFullWindow&&fsrVariance>FSR_VAR_THRESHOLD) playTone(750);
                                                                 //if our window is full, and our variance exceeds the threshold, we buzz
    
     else stopTone();                                            //otherwise we stop the tone
    

    Time Complexity: \(O\left(1\right)\)

    Space Complexity: \(O\left(1\right)\)

Challenges

My main challenge in this milestone was learning how to use the AHRS and accelerometer in the beginning. Learning how the sensors worked and how their libraries were written and what each function did was a lot to do in my first week, but after that I only encountered a few hiccups here and there.

Next Steps

In my next milestone I am aiming to assemble my whole project by beginning to attach parts and build schematics/diagrams. I may also alter my algorithms later to work with multiple sensors at once rather than just the accelerometer because the implementing the pedometer would be much easier

How the Sensors Work

The SmartSole uses several key sensors to monitor physical activity and provide feedback. Here’s how each sensor works and contributes to the system:

Accelerometer (LSM6DS3TR-C)

The accelerometer measures linear acceleration in three axes (X, Y, Z) and is the primary sensor for detecting movement patterns. It works by detecting changes in velocity over time:

Gyroscope (LSM6DS3TR-C)

The gyroscope measures angular velocity (rotational speed) around the three axes and provides complementary data to the accelerometer:

Magnetometer (LIS3MDL)

The magnetometer detects the Earth’s magnetic field and provides heading information:

Force Sensitive Resistor (FSR)

The FSR is a polymer sheet with both electrically conducting and non-conducting particles present on the sensing film. When force is applied to the surface, the particles touch the conducting electrodes, causing the resistance of the film to decrease. This change in resistance can be measured and used to sense pressure.

Starter Milestone

Description

My starter project is a retro arcade console that allows the user to play classic video games like Tetris, Snake, and Space Invaders. The screen is constructed from two 8x8 dot matrices on the top left and has a screen capable of displaying three digits in the top right. The system can be powered by the battery pack on the back of the device or by using the Micro USB port near to the number screen. The device creates sound by using a buzzer below the red power switch. There are six yellow buttons below those components which control the elements on the screen in various games.

Device Image

Challenges

Some challenges I encountered while working on this project were properly soldering the parts to the board as it was time consuming and was the hardest part of this project. I also discovered near the end of the project that I had attached the battery pack backwards and had to spend time fixing my mistake.

Next Steps

I will begin working on my intensive project after this Starter Milestone.

Bill of Materials

Part Name Purpose Price Qty Required Link
ESP32 Main microcontroller for processing sensor data and handling communication $9.99 1 Link
Adafruit LSM6DS3TR-C + LIS3MDL (Accelerometer/Gyroscope/Magnetometer) Measures acceleration and gyroscopic data to detect steps, jumps, and running $19.95 1 Link
Force Sensitive Resistor (FSR) Sensor - Long 200mm Detects pressure changes to monitor footfalls and activity intensity $5.99 2 Link
Vibration Motor Module (5 Pack) Gives haptic feedback to alert the user while running $24.50 1 Link
SeeedStudio Grove Buzzer Module Emits sound alerts for user notifications $1.90 1 Link
Fully Flexible PCB Integrates all electronic components into a flexible form factor suitable for insoles that are constantly changing shape $35.00 1 Link

Works Cited

ESP32 Case