Leonardo Marquez
Published © MIT

A Modular Wearable Development Platform

A modular, stackable, wearable electronics development platform for rapid prototyping, proof of concept or learning.

IntermediateWork in progress8 hours250
A Modular Wearable Development Platform

Things used in this project

Hardware components

Seeed Studio XIAO nRF52840 Sense (XIAO BLE Sense)
Seeed Studio XIAO nRF52840 Sense (XIAO BLE Sense)
×1
SparkFun Photodetector Breakout
×1
0.96" OLED 64x128 Display Module
ElectroPeak 0.96" OLED 64x128 Display Module
×1
Inertial Measurement Unit (IMU) (6 deg of freedom)
Inertial Measurement Unit (IMU) (6 deg of freedom)
×1
Perfboards
×3

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

Display Module Enclosure

Core Module Enclosure

Heart Rate Module Enclosure

Schematics

Core Module

Display Module

Heart Rate Module

Code

Basic Fitness Tracker

Arduino
/*********************************************************************
 This is an example of a fitness tracker using the Modular Wearable
 Electronics platform.
*********************************************************************/

/*********************************************************************
 This is an example for our nRF52 based Bluefruit LE modules

 Pick one up today in the adafruit shop!

 Adafruit invests time and resources providing this open source code,
 please support Adafruit and open-source hardware by purchasing
 products from Adafruit!

 MIT license, check LICENSE for more information
 All text above, and the splash screen below must be included in
 any redistribution
*********************************************************************/

#include <bluefruit.h>
#include <Adafruit_LittleFS.h>
#include <InternalFileSystem.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include "MAX30105.h"
#include "heartRate.h"

// OLED display dimensions in pixels
#define SCREEN_WIDTH   128
#define SCREEN_HEIGHT  64

// Reset pin (-1 = share Arduino reset pin)
#define OLED_RESET     -1

// I2C address: 0x3D for 128x64, 0x3C for 128x32
#define SCREEN_ADDRESS 0x3C

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

#define USE_SERIAL

// BLE services
BLEDfu  bledfu;   // OTA DFU service
BLEDis  bledis;   // Device information
BLEUart bleuart;  // UART over BLE
BLEBas  blebas;   // Battery

// Accelerometer / Gyro
Adafruit_MPU6050 mpu;

float previousVector = 0;
int   steps          = 0;

const float threshold    = 3.0;   // Step detection threshold (6–12 typical; adjust based on testing)
const int   debounceDelay = 400;  // Minimum ms between detected steps

// Heart rate sensor
MAX30105 particleSensor;

const byte RATE_SIZE = 4;         // Number of readings to average
byte  rates[RATE_SIZE];           // Heart rate sample buffer
byte  rateSpot     = 0;
long  lastBeat     = 0;           // Timestamp of last detected beat (ms)

float beatsPerMinute = 0;
int   beatAvg        = 0;

// ----------------------------------------------------------------------------

void setup()
{
#ifdef USE_SERIAL
  Serial.begin(115200);
  while (!Serial) {
    delay(10);  // Wait for serial console (Leonardo, Zero, etc.)
  }
  Serial.println("Bluefruit52 BLEUART Example");
  Serial.println("---------------------------\n");
#endif

  // Initialize OLED display (generates 3.3 V internally via SSD1306_SWITCHCAPVCC)
  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
#ifdef USE_SERIAL
    Serial.println(F("SSD1306 allocation failed"));
#endif
    for (;;);  // Halt
  }
  display.clearDisplay();
  display.display();

  // BLE setup — all config calls must precede begin()
  Bluefruit.autoConnLed(true);
  Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
  Bluefruit.begin();
  Bluefruit.setTxPower(4);  // See bluefruit.h for supported values
  Bluefruit.Periph.setConnectCallback(connect_callback);
  Bluefruit.Periph.setDisconnectCallback(disconnect_callback);

  // OTA DFU should be added first
  bledfu.begin();

  // Device information service
  bledis.setManufacturer("Adafruit Industries");
  bledis.setModel("Bluefruit Feather52");
  bledis.begin();

  // BLE UART service
  bleuart.begin();

  // Battery service (report 100%)
  blebas.begin();
  blebas.write(100);

  startAdv();

#ifdef USE_SERIAL
  Serial.println("Please use Adafruit's Bluefruit LE app to connect in UART mode");
  Serial.println("Once connected, enter character(s) that you wish to send");
#endif

  // Initialize MPU-6050
  if (!mpu.begin()) {
#ifdef USE_SERIAL
    Serial.println("Failed to find MPU6050 chip");
#endif
    while (1) {
      delay(10);
    }
  }
  mpu.setAccelerometerRange(MPU6050_RANGE_16_G);
  mpu.setGyroRange(MPU6050_RANGE_250_DEG);
  mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);

  // Initialize MAX30105 heart rate sensor
  if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) {
#ifdef USE_SERIAL
    Serial.println("MAX30101 was not found. Please check wiring/power.");
#endif
    while (1);
  }
  Serial.println("Place your index finger on the sensor with steady pressure.");

  // Sensor configuration
  byte ledBrightness = 0xFF;   // 0 = off, 255 = 50 mA
  byte sampleAverage = 4;      // Options: 1, 2, 4, 8, 16, 32
  byte ledMode       = 3;      // 1 = Red, 2 = Red + IR, 3 = Red + IR + Green
  int  sampleRate    = 3200;   // Options: 50, 100, 200, 400, 800, 1000, 1600, 3200
  int  pulseWidth    = 411;    // Options: 69, 118, 215, 411
  int  adcRange      = 4096;   // Options: 2048, 4096, 8192, 16384

  particleSensor.setup(ledBrightness, sampleAverage, ledMode, sampleRate, pulseWidth, adcRange);
  particleSensor.setPulseAmplitudeRed(0xFF);
  particleSensor.setPulseAmplitudeGreen(0xFF);
}

// ----------------------------------------------------------------------------

void startAdv(void)
{
  // Build advertising packet
  Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
  Bluefruit.Advertising.addTxPower();
  Bluefruit.Advertising.addService(bleuart);  // Include bleuart 128-bit UUID

  // Device name goes in the scan response (no room in advertising packet)
  Bluefruit.ScanResponse.addName();

  /* Advertising parameters:
   *   - Auto-restart on disconnect
   *   - Fast mode: 20 ms interval for 30 s, then slow mode: 152.5 ms
   *   - start(0) advertises indefinitely until connected
   *
   * For recommended intervals see:
   * https://developer.apple.com/library/content/qa/qa1931/_index.html
   */
  Bluefruit.Advertising.restartOnDisconnect(true);
  Bluefruit.Advertising.setInterval(32, 244);  // Units of 0.625 ms
  Bluefruit.Advertising.setFastTimeout(30);    // Seconds in fast mode
  Bluefruit.Advertising.start(0);
}

// ----------------------------------------------------------------------------

void loop()
{
  // Read accelerometer / gyro
  sensors_event_t a, g, temp;
  mpu.getEvent(&a, &g, &temp);

  // Compute acceleration vector magnitude
  float vector = sqrt(a.acceleration.x * a.acceleration.x +
                      a.acceleration.y * a.acceleration.y +
                      a.acceleration.z * a.acceleration.z);

  float delta = abs(vector - previousVector);  // Change since last sample

  // Detect a step when the delta exceeds the threshold
  if (delta > threshold) {
    steps++;
#ifdef USE_SERIAL
    Serial.print("Step detected! Total: ");
    Serial.println(steps);
#endif
    delay(debounceDelay);  // Debounce
  }

  previousVector = vector;

  // Read heart rate sensor (green channel)
  long irValue = particleSensor.getGreen();

  if (checkForBeat(irValue) == true) {
    long beatDelta = millis() - lastBeat;
    lastBeat = millis();

    beatsPerMinute = 60 / (beatDelta / 1000.0);

    if (beatsPerMinute < 255 && beatsPerMinute > 20) {
      rates[rateSpot++] = (byte)beatsPerMinute;  // Store reading
      rateSpot %= RATE_SIZE;                     // Wrap index

      // Compute rolling average
      beatAvg = 0;
      for (byte x = 0; x < RATE_SIZE; x++) {
        beatAvg += rates[x];
      }
      beatAvg /= RATE_SIZE;
    }
  }

  // Update OLED display
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 20);
  display.print(F("Steps:"));
  display.print(steps);
  display.setCursor(0, 40);
  display.print(F("HR:"));
  display.print(beatAvg);
  display.display();

  // Send data over BLE UART
  char out_string[20];
  sprintf(out_string, "Steps:%d, HR:%d", steps, beatAvg);
  bleuart.print(out_string);

#ifdef USE_SERIAL
  Serial.print(irValue);
  Serial.print(",");
  Serial.print(beatsPerMinute);
  Serial.print(",");
  Serial.print(beatAvg);
  Serial.println();
#endif
}

// ----------------------------------------------------------------------------
// BLE callbacks
// ----------------------------------------------------------------------------

// Invoked when a central device connects
void connect_callback(uint16_t conn_handle)
{
  BLEConnection* connection = Bluefruit.Connection(conn_handle);

  char central_name[32] = { 0 };
  connection->getPeerName(central_name, sizeof(central_name));

#ifdef USE_SERIAL
  Serial.print("Connected to ");
  Serial.println(central_name);
#endif
}

// Invoked when a connection is dropped
// @param reason  BLE_HCI_STATUS_CODE from ble_hci.h
void disconnect_callback(uint16_t conn_handle, uint8_t reason)
{
  (void) conn_handle;
  (void) reason;

#ifdef USE_SERIAL
  Serial.println();
  Serial.print("Disconnected, reason = 0x");
  Serial.println(reason, HEX);
#endif
}

Credits

Leonardo Marquez
1 project • 0 followers

Comments