Hello!

Read on to explore the circuitry planning and logistics in the early stages of making the project. Excalidraw Link

The Block Diagram

Power supply edited to be two 400mAh batteries

Details

Hand mounted system

The Code

#include <Arduino.h>
#include <Wire.h> // I2C library
#include "Adafruit_TinyUSB.h"
#include <Adafruit_NeoPixel.h> // Neopixel LED library
#include <MAX30105.h> // MAX30105 or MAx30102 library
#include <spo2_algorithm.h> //SpO2 Sensor library
 
#define VBATPIN A6 // Battery voltage analog pin
#define NEOPIXELPIN 8 // Neopixel control pin
#define NUMPIXELS 1 // Number of neopixels
#define MAX_BRIGHTNESS 255 // for MAX30102
 
// Put variable declaration here
float VBat;
 
uint32_t irBuffer[100]; //infrared LED sensor data
uint32_t redBuffer[100];  //red LED sensor data
 
int32_t bufferLength = 100; //data length
int32_t spo2; //SPO2 value
int8_t validSPO2; //indicator to show if the SPO2 calculation is valid
int32_t heartRate; //heart rate value
int8_t validHeartRate; //indicator to show if the heart rate calculation is valid
 
// Put function declaration here
float getBattVoltage(int x);
void VBatIndicator();
void FillSensorBuffer();
 
// Initialize NeoPixel
Adafruit_NeoPixel pixel = Adafruit_NeoPixel(NUMPIXELS, NEOPIXELPIN, NEO_GRB + NEO_KHZ800);
 
//Initialise MAX30102 SpO2 Sensor
MAX30105 particleSensor;
 
void setup() {
  // put your setup code here, to run once:
 
  // Initialize NeoPixel
  pixel.begin();
  pixel.setBrightness(20); // Set brightness (0-255)
  pixel.show(); // Initialize all pixels to 'off'
 
  // Start Serial
  Serial.begin(115200);
  Serial.println("Outputting readings... NOW");
 
  Wire.begin(); // Initialize I2C
 
  // Initialize MAX30102
  if (!particleSensor.begin(Wire, I2C_SPEED_FAST)) {
    Serial.println("MAX30102 not found! Check wiring.");
    while (1); // Halt if sensor not found
  }
 
  Serial.println(F("Attach sensor to finger with rubber band. Press any key to start conversion"));
  while (Serial.available() == 0) ; //wait until user presses a key
  Serial.read();
 
  // Configure sensor for SpO2 monitoring
  particleSensor.setup(60, 4, 2, 100, 411, 4096); 
  /*
  Parameters: 
  LED brightness (0-255)
  Sample averaging (1, 2, 4, 8, 16, 32)
  LED mode (1 = Red, 2 = Red + IR)
  Sample rate (50, 100, 200, 400, 800, 1000, 1600, 3200)
  Pulse width (69, 118, 215, 411)
  ADC range (2048, 4096, 8192, 16384)
  */
}
 
void loop() {
  // put your main code here, to run repeatedly
  // Sequence to indicate battery status
  VBat =  getBattVoltage(analogRead(VBATPIN)); 
  VBatIndicator();
  Serial.println("hello i'm running");
  
  //read the first 100 samples, and determine the signal range
  static bool bufferfill = false;
  if (!bufferfill) {
      FillSensorBuffer();
      maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);
      bufferfill = true;
  }
  //dumping the first 25 sets of samples in the memory and shift the last 75 sets of samples to the top
  for (byte i = 25; i < 100; i++)
    {
      redBuffer[i - 25] = redBuffer[i];
      irBuffer[i - 25] = irBuffer[i];
    }
 
  //take 25 sets of samples before calculating the heart rate.
  for (byte i = 75; i < 100; i++)
  {
    while (particleSensor.available() == false) //do we have new data?
      particleSensor.check(); //Check the sensor for new data
 
    redBuffer[i] = particleSensor.getRed();
    irBuffer[i] = particleSensor.getIR();
    particleSensor.nextSample(); //We're finished with this sample so move to next sample
 
    //send samples and calculation result to terminal program through UART
    Serial.print(F("red="));
    Serial.print(redBuffer[i], DEC);
    Serial.print(F(", ir="));
    Serial.print(irBuffer[i], DEC);
 
    Serial.print(F(", HR="));
    Serial.print(heartRate, DEC);
 
    Serial.print(F(", HRvalid="));
    Serial.print(validHeartRate, DEC);
 
    Serial.print(F(", SPO2="));
    Serial.print(spo2, DEC);
 
    Serial.print(F(", SPO2Valid="));
    Serial.println(validSPO2, DEC);
    }
 
    //After gathering 25 new samples recalculate HR and SP02
    maxim_heart_rate_and_oxygen_saturation(irBuffer, bufferLength, redBuffer, &spo2, &validSPO2, &heartRate, &validHeartRate);
}
 
// put function definitions here
float getBattVoltage(int x) {
  float voltage = (x * 2 * 3.6) / 1024.0; 
  return constrain(voltage, 0.0, 4.2);  // LiPo max voltage is 4.2V
 
}
 
void VBatIndicator() {
    if (VBat > 4.1) {
    pixel.setPixelColor(0, pixel.Color(0, 255, 0)); // Green indicate full batt
  }
  else if (VBat < 3.4) {
    pixel.setPixelColor(0, pixel.Color(255, 0, 0)); // Red indicate low batt
  }
  else {
    pixel.setPixelColor(0, pixel.Color(0, 0, 255)); // Blue indicate otherwise
  }
  pixel.show();  // Call show() only once after setting the color
}
 
void FillSensorBuffer() {
  //read the first 100 samples, and determine the signal range
  Serial.println("Collecting samples...");
  //finger detection
  if (particleSensor.getIR() < 7000) {
      Serial.println("No finger detected");
      return;
  }
  for (byte i = 0 ; i < bufferLength ; i++)
  {
    while (particleSensor.available() == false) //do we have new data?
      particleSensor.check(); //Check the sensor for new data
 
    redBuffer[i] = particleSensor.getRed();
    irBuffer[i] = particleSensor.getIR();
    particleSensor.nextSample(); //We're finished with this sample so move to next sample
 
    // Print progress
    if (i % 10 == 0) {  // Print every 10th sample
      Serial.print("Progress: ");
      Serial.print(i);
      Serial.println("%");
    }
  }
  Serial.println("Samples Collected!");
}

Chest mounted system

  • Stretch of lower rib cage during breathing stretches Adafruit Conductive Rubber Sheet, increasing it’s resistance, creating a potential difference across a potential divider
  • This P.D. is theoretically related to the stretch and thus related to the breath quantity
  • This data will be calibrated to accurately represent the user’s breathing via calibration in app, normalising the P.D. to the fully exhaled data and fully inhaled data
  • An CMA-4544PF-W Electret Microphone together with a MAX4466 Microphone Amplifier is attached to a stethoscope to analyse internal breathing sounds as well, sending amplitude and frequency data back to the Adafruit Feather nRF52840 Sense for analysis. Primarily trying to catch the high pitch sound of Wheezing. (Latest addition!)
  • The Adafruit Feather nRF52840 Sense reads this data and sends this data together with sound and motion data from its inbuilt sound sensor and accelerometer to the user’s phone via Bluetooth Low Energy (BLE), UART communication protocol

The Code

#include <Arduino.h>
#include <Adafruit_NeoPixel.h> // NeoPixel LED library
#include "Adafruit_TinyUSB.h" 
#include <bluefruit.h>
#include <PDM.h>
#include <arduinoFFT.h>
#include <Adafruit_LSM6DS33.h>
 
#define VBATPIN A6 // Battery voltage analog pin
#define NEOPIXELPIN 8 // Neopixel control pin
#define NUMPIXELS 1 // Number of neopixels
#define FFTSAMPLES 256 // Fast Fourier Transform sample collection
#define FFTSAMPLING_FREQ 16000
#define WHEATBRIDGEPIN_A A3 
#define WHEATBRIDGEPIN_B A4
 
// Put variable declaration here
float VBat; // Voltage of battery
int32_t mic; 
float dominantFreq;
 
extern PDMClass PDM;
short PDMsampleBuffer[256];  // buffer to read PDMsamples into, each sample is 16-bits
volatile int PDMsamplesRead; // number of PDMsamples read
 
double vReal[FFTSAMPLES];
double vImag[FFTSAMPLES];
 
Adafruit_LSM6DS33 lsm6ds33;
const float ALPHA = 0.95;  // Low-pass filter coefficient (Higher values (closer to 1) mean slower gravity filtering but better noise reduction)
float gravityX = 0, gravityY = 0, gravityZ = 0;        // Filtered gravity components
float lastAccelX = 0, lastAccelY = 0, lastAccelZ = 0;  // Previous acceleration readings
float deltaX = 0, deltaY = 0, deltaZ = 0;              // Change in acceleration
 
/* Create FFT object */
ArduinoFFT<double> FFT = ArduinoFFT<double>(vReal, vImag, FFTSAMPLES, FFTSAMPLING_FREQ);
 
// BLE Service for Environmental Sensing
BLEService        envService("181A");  // Environmental Sensing BLE service
BLECharacteristic soundChar("2A71", BLERead | BLENotify);    // Sound level
BLECharacteristic freqChar("2A76", BLERead | BLENotify);     // Frequency
BLECharacteristic accelChar("2A73", BLERead | BLENotify);    // Acceleration
BLECharacteristic stretchChar("2A77", BLERead | BLENotify);  // Custom for stretch
BLEUart bleuart;
 
// Put your function declarations here
float getBattVoltage(int x);
void VBatIndicator();
float computeFFT();
float getFilteredAcceleration();
float getPotDiffWheat();
void setupBLE();
 
int32_t getPDMwave(int32_t samples);
void onPDMdata();
 
// Initialize NeoPixel
Adafruit_NeoPixel pixel = Adafruit_NeoPixel(NUMPIXELS, NEOPIXELPIN, NEO_GRB + NEO_KHZ800);
 
void setup() {
    // Put your code here to run once at setup
 
    // Initialize NeoPixel
    pixel.begin();
    pixel.setBrightness(2); // Set brightness (0-255)
 
    // Quick test of NeoPixel
    pixel.setPixelColor(0, pixel.Color(255, 0, 0));  // Red
    pixel.show();
    delay(500);
    pixel.setPixelColor(0, pixel.Color(0, 255, 0));  // Green
    pixel.show();
    delay(500);
    pixel.setPixelColor(0, pixel.Color(0, 0, 255));  // Blue
    pixel.show();
    delay(500);
    pixel.setPixelColor(0, pixel.Color(0, 0, 0));  // Blue
    pixel.show();
    delay(500);
 
    // Start Serial
    Serial.begin(115200);
    Serial.println("Sensors outputting information... NOW!");
 
    // Setup BLE
    setupBLE();
    Serial.println("Advertising BLE...");
 
    // Start Accelerometer
    lsm6ds33.begin_I2C();
    // configure accelerometer for cough detection
    lsm6ds33.setAccelRange(LSM6DS_ACCEL_RANGE_4_G);
    lsm6ds33.setAccelDataRate(LSM6DS_RATE_104_HZ);
    // Initialize gravity filter with first reading
    sensors_event_t accel;
    sensors_event_t gyro;
    sensors_event_t temp;
    lsm6ds33.getEvent(&accel, &gyro, &temp);
    gravityX = accel.acceleration.x;
    gravityY = accel.acceleration.y;
    gravityZ = accel.acceleration.z;
 
    // Start sound sensor
    PDM.onReceive(onPDMdata);
    PDM.begin(1, 16000);
}
 
void loop() {
    // Put your code here to loop
 
    // indicate battery level
    VBat =  getBattVoltage(analogRead(VBATPIN)); 
    VBatIndicator();
 
    // Get sound sensor data
    PDMsamplesRead = 0;
    mic = getPDMwave(FFTSAMPLES);
 
    // Copy samples to FFT arrays only after we have enough data
    for (int i = 0; i < FFTSAMPLES; i++) {
        vReal[i] = (double)PDMsampleBuffer[i];
        vImag[i] = 0.0;
    }
    // Compute FFT and get dominant frequency
    dominantFreq = computeFFT();
 
    // Get Jerk movement data
    float acceleration = getFilteredAcceleration();
 
    // Compute potential diff across wheat bridge
    float PotDiff = getPotDiffWheat();
 
    // Print data
    Serial.print("RMS amplitude: ");
    Serial.print(mic);
    Serial.print("  Dominant frequency: ");
    Serial.print(dominantFreq);
    Serial.print(" Hz  Jerking: ");
    Serial.print(acceleration);
    Serial.print(" m/s²  Stretch Coeff (P.D.): ");
    Serial.print(PotDiff);
    Serial.println("V");
    // Optional: Only print when significant movement detected
    if (acceleration > 2.0) {  // Threshold for significant movement
        Serial.println("*** Significant movement detected! ***");
    }
 
    // Send data over BLE
    if (Bluefruit.connected()) {
 
        // FOR ENVIRO SERVICE
 
        // Convert float values to byte arrays for BLE transmission
        uint8_t soundData[4], freqData[4], accelData[4], stretchData[4];
        memcpy(soundData, &mic, 4);
        memcpy(freqData, &dominantFreq, 4);
        memcpy(accelData, &acceleration, 4);
        memcpy(stretchData, &PotDiff, 4);
        
        // Send notifications
        soundChar.notify(soundData, 4);
        freqChar.notify(freqData, 4);
        accelChar.notify(accelData, 4);
        stretchChar.notify(stretchData, 4);
 
        // FOR UART
 
        // Format data string exactly like Serial Monitor
        char uartData[150];  // Increased buffer size to prevent overflow
        memset(uartData, 0, sizeof(uartData));  // Clear buffer
 
        // Split the data into multiple transmissions to ensure reliable transfer
        sprintf(uartData, "RMS amplitude: %d\n", (int)(mic));
        bleuart.write(uartData, strlen(uartData));
        
        sprintf(uartData, "Dominant frequency: %.2f Hz\n", dominantFreq);
        bleuart.write(uartData, strlen(uartData));
        
        sprintf(uartData, "Jerking: %.2f m/s²\n", acceleration);
        bleuart.write(uartData, strlen(uartData));
        
        sprintf(uartData, "Stretch Coeff (P.D.): %.3f V\n", PotDiff);
        bleuart.write(uartData, strlen(uartData));
 
        // Send movement alert if needed
        if (acceleration > 2.0) {
            bleuart.write("*** Significant movement detected! ***\n", 39);
        }
 
        // Add a separator line
        bleuart.write("----------------------------------------\n", 41);
 
        char plotterData[100];
        sprintf(plotterData, "%d,%d,%d,%d\n", 
            (int)(mic),              // Sound level
            (int)(dominantFreq),     // Frequency
            (int)(acceleration*100),  // Acceleration (scaled up for visibility)
            (int)(PotDiff*100));     // Stretch (scaled up for visibility)
        bleuart.write(plotterData, strlen(plotterData));
    }
 
    delay(100);
}
 
// Put your function definitions here
 
// calculate RMS amplitude from samples
int32_t getPDMwave(int32_t samples) {
    long sum = 0;
    int count = 0;
    const int16_t AMPLITUDE_THRESHOLD = 10;  // Adjust this threshold based on testing to eliminate noise
    int16_t max_amplitude = 0;
    int16_t min_amplitude = 0;
 
    while (samples > 0) {
        if (!PDMsamplesRead) {
            yield();
            continue;
        }
        for (int i = 0; i < PDMsamplesRead; i++) {
            // Only process samples above noise threshold
            if (abs(PDMsampleBuffer[i]) > AMPLITUDE_THRESHOLD) {
                // Track min/max for peak-to-peak calculation
                if (PDMsampleBuffer[i] > max_amplitude) max_amplitude = PDMsampleBuffer[i];
                if (PDMsampleBuffer[i] < min_amplitude) min_amplitude = PDMsampleBuffer[i];
                
                // Square each sample and add to sum for RMS
                sum += (long)PDMsampleBuffer[i] * PDMsampleBuffer[i];
                count++;
            }
            samples--;
        }
        PDMsamplesRead = 0;
    }
 
    // Only return RMS if we have significant signal
    int32_t peak_to_peak = max_amplitude - min_amplitude;
    if (peak_to_peak > AMPLITUDE_THRESHOLD * 2) {
        return (count > 0) ? sqrt(sum / count) : 0;
    }
    return 0;  // Return 0 if signal is too weak
}
 
void onPDMdata() { // initialise PDM data reading
  // query the number of bytes available
  int bytesAvailable = PDM.available();
 
  // read into the sample buffer
  PDM.read(PDMsampleBuffer, bytesAvailable);
 
  // 16-bit, 2 bytes per sample
  PDMsamplesRead = bytesAvailable / 2;
}
 
float getBattVoltage(int x) { 
  float voltage = (x * 2 * 3.6) / 1024.0; 
  return constrain(voltage, 0.0, 4.2);  // LiPo max voltage is 4.2V
}
 
void VBatIndicator() { // alter neopixel colour to show battery status
  if (VBat > 4.1) {
    pixel.setPixelColor(0, pixel.Color(0, 255, 0)); // Green indicate full batt
  }
  else if (VBat < 3.4) {
    pixel.setPixelColor(0, pixel.Color(255, 0, 0)); // Red indicate low batt
  }
  else {
    pixel.setPixelColor(0, pixel.Color(0, 0, 255)); // Blue indicate otherwise
  }
  pixel.show();  // Call show() only once after setting the color
}
 
float computeFFT() { // compute frequency function
    // Compute FFT
    FFT.windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD);
    FFT.compute(FFT_FORWARD);
    FFT.complexToMagnitude();
 
    // Calculate dominant frequency
    double peak = 0;
    int peakIndex = 0;
    const double NOISE_THRESHOLD = 600;  // Adjust this value based on testing to eliminate noise
    
    for (int i = 1; i < (FFTSAMPLES/2); i++) {
        if (vReal[i] > peak) {
            peak = vReal[i];
            peakIndex = i;
        }
    }
    // Print dominant frequency
    if (peak > NOISE_THRESHOLD) {
        float dominantFreq = (peakIndex * 1.0 * FFTSAMPLING_FREQ) / FFTSAMPLES;
        return dominantFreq;
    }
    else {
        return 0.0;  // Return 0 if no significant peak found
    }
 
}
 
float getFilteredAcceleration() { // Filter out gravity from accelerometer and obtain aggregated acceleration across all 3 axes
    sensors_event_t accel;
    sensors_event_t gyro;
    sensors_event_t temp;
    lsm6ds33.getEvent(&accel, &gyro, &temp);
    
    // Low-pass filter to extract gravity components for all axes
    gravityX = ALPHA * gravityX + (1.0 - ALPHA) * accel.acceleration.x;
    gravityY = ALPHA * gravityY + (1.0 - ALPHA) * accel.acceleration.y;
    gravityZ = ALPHA * gravityZ + (1.0 - ALPHA) * accel.acceleration.z;
    
    // High-pass filter to get dynamic acceleration
    float dynamicAccelX = accel.acceleration.x - gravityX;
    float dynamicAccelY = accel.acceleration.y - gravityY;
    float dynamicAccelZ = accel.acceleration.z - gravityZ;
    
    // Calculate total magnitude of acceleration change vector
    float totalDelta = sqrt(
        pow(dynamicAccelX - lastAccelX, 2) +
        pow(dynamicAccelY - lastAccelY, 2) +
        pow(dynamicAccelZ - lastAccelZ, 2)
    );
    
    // Update last readings
    lastAccelX = dynamicAccelX;
    lastAccelY = dynamicAccelY;
    lastAccelZ = dynamicAccelZ;
    
    return totalDelta;  // Return magnitude of acceleration change
}
 
float getPotDiffWheat() { // calculate the potential difference across two point on wheatstone bridge
    const int SAMPLES = 10;  // Number of samples to average
    float sumA = 0;
    float sumB = 0;
    
    // Take multiple readings
    for(int i = 0; i < SAMPLES; i++) {
        sumA += analogRead(WHEATBRIDGEPIN_A);
        sumB += analogRead(WHEATBRIDGEPIN_B);
        delay(1);  // Short delay between readings
    }
    
    // Calculate average and convert to voltage
    float voltageA = (sumA / SAMPLES * 3.3) / 1024.0;
    float voltageB = (sumB / SAMPLES * 3.3) / 1024.0;
    
    return fabsf(voltageA - voltageB);
}
 
void setupBLE() {
    // Initialize Bluefruit with maximum connections as Peripheral
    Bluefruit.begin(1, 0);
    Bluefruit.setName("AsthmaAlly_Chest");
    
    // Configure and Start UART Service
    bleuart.begin();
 
    // Configure and Start Environmental Service
    envService.begin();
    
    // Configure the characteristics for ENVIRO SERVICE
    soundChar.setProperties(CHR_PROPS_READ | CHR_PROPS_NOTIFY);
    soundChar.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
    soundChar.setFixedLen(4);
    soundChar.begin();
    
    freqChar.setProperties(CHR_PROPS_READ | CHR_PROPS_NOTIFY);
    freqChar.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
    freqChar.setFixedLen(4);
    freqChar.begin();
    
    accelChar.setProperties(CHR_PROPS_READ | CHR_PROPS_NOTIFY);
    accelChar.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
    accelChar.setFixedLen(4);
    accelChar.begin();
    
    stretchChar.setProperties(CHR_PROPS_READ | CHR_PROPS_NOTIFY);
    stretchChar.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
    stretchChar.setFixedLen(4);
    stretchChar.begin();
    
    // Start advertising
    Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
    Bluefruit.Advertising.addService(bleuart);     // Add UART service advertising
    Bluefruit.Advertising.addService(envService);  // ENVIRO service advertising
    Bluefruit.Advertising.addName();
    Bluefruit.Advertising.restartOnDisconnect(true);
    Bluefruit.Advertising.setInterval(32, 244);    // in unit of 0.625 ms
    Bluefruit.Advertising.setFastTimeout(30);      // number of seconds in fast mode
    Bluefruit.Advertising.start(0);                // 0 = Don't stop advertising after n seconds
}

Mobile devices

  • Our app takes in these data (SpO2, heart rate, sound, motion, breath) to deduce signs of imminent asthma attack
    • Wheezing (Sound Characteristics)
    • Coughing (Sound + Jerk Motion)
    • Low blood oxygen (SpO2)
    • Elevated heart rate
  • The app will also analyse location data, which can eliminate false alarms
    • E.g. GPS movement indicates running Heart rate sensor data dropped in significance

Circuit Schematics

Breadboard Prototyping

Check out the data output @ Machine Learning & Data Processing