Respiratory rate is a vital sign that helps evaluate lung and overall health. Traditionally, people measure it manually with a stopwatch or by watching recorded videos to count breaths per minute. However, these methods can lead to human error, take too much time, and are not practical for ongoing monitoring.
this video from Respiratory Therapy Zone youtube channel show how if lung test with video or timer manual
from this video I have idea to make automated solution using M5Stack hardware. It allows for real-time measurement and visualization of breathing patterns, along with added feedback features.
The goal of SpiroStack is to provide a portable and user-friendly tool that performs better than traditional manual testing.
Step 1 - HardwareIn this project I use m5 StickC, and presure sensor MPS20N0040D-S (0-40KPa Digital Barometric Water Air Pressure)
pin connection
- VCC (sensor) to VCC (M5StickC)
- OUT (sensor) to G33 (M5StickC)
- SCK (sensor) to G32 (M5StickC)
- GMD (sensor) to GND (M5StickC)
For connector to M5StickC, I use connector type 4pin HY2.0
Step 2 - SoftwareFor software installed I resume to 4 part
- Install Software Arduino IDE
- Install M5Stack Board (https://docs.m5stack.com/en/arduino/arduino_board)
- Install Library M5StickC(https://docs.m5stack.com/en/arduino/arduino_library)
- Test Code to show intro SpiroStack in Oled
#include <M5StickC.h>
void setup() {
M5.begin();
M5.Lcd.setRotation(0);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(8, 20);
M5.Lcd.println("SPIRO");
M5.Lcd.setCursor(8, 40);
M5.Lcd.println("STACK");
M5.Lcd.setTextSize(1);
M5.Lcd.setCursor(2, 120);
M5.Lcd.println("Press Button");
M5.Lcd.setCursor(10, 130);
M5.Lcd.println("to START");
}
void loop() {
}
Result in M5StickC
- Read data Sensor MPS20N0040D-S, for this sensor I use library from habuenav (https://github.com/habuenav/Breath/tree/main), so next step is install library breath in arduino
- try simple code to read sensor
#include <Breath.h>
#define OUTPUT_PIN 33
#define CLOCK_PIN 32
Breath breath(OUTPUT_PIN, CLOCK_PIN);
void setup() {
Serial.begin(115200);
breath.setResistance(3);//Adjust the blowing
breath.setMaxOut(127); //Set the maximum output value
}
void loop() {
int16_t breathValue = breath.read();
Serial.println(breathValue);
delay(100);
}
data result for inflate and deflate sensor
if data positive is mean inflate (exhale), if data negative is mean deflate( inhale), if data zero is hold (nothing)
- Combine from test oled and test breath
Before combine program I have scenario in SpiroStack
How its SpiroStack Work
- Button A Pressed → "Ready", The system initializes and shows a Ready status.
- Inhale → Status Appears, As the user begins inhaling, the gadget detects it and shows a Breathing In status.
- Hold → Timer Starts & Runs in Real Time, The moment the user holds the breath, a timer starts and counts up in real time.
- Exhale → Timer Stopped + Result & Category Shown, Timer stops upon exhaling by user. Last duration and a result classification (e.g., Excellent, Good, Average, Weak) shown.
Coding SpiroStack
#include <M5StickC.h>
#include <Breath.h>
#define OUTPUT_PIN 33
#define CLOCK_PIN 32
Breath breath(OUTPUT_PIN, CLOCK_PIN);
// ========== Pengaturan ==========
const int threshold = 3; // ambang exhale (+) & inhale (-)
const int deadband = 2; // zona tenang dekat nol untuk deteksi Hold
const bool INVERT_SIGN = false; // set true jika tanda kebalik (opsional)
const unsigned long INHALE_TRIGGER_MS = 150; // durasi minimal sinyal inhale
const unsigned long HOLD_STABLE_MS = 300; // stabil di deadband untuk mulai hitung
const unsigned long EXHALE_TRIGGER_MS = 150; // durasi minimal sinyal exhale
// ========== State flags ==========
bool testReady = false;
bool inhaleDetected = false;
bool holding = false;
bool exhaleDetected = false;
// ========== Timing ==========
unsigned long startTime = 0; // waktu mulai hold
unsigned long holdTime = 0; // detik
unsigned long inhaleSince = 0;
unsigned long holdSince = 0;
unsigned long exhaleSince = 0;
// Forward declaration
void showResult(unsigned long seconds);
void setup() {
Serial.begin(115200);
M5.begin();
// Layar portrait + font besar
M5.Lcd.setRotation(0);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(8, 20);
M5.Lcd.println("SPIRO");
M5.Lcd.setCursor(8, 40);
M5.Lcd.println("STACK");
M5.Lcd.setTextSize(1);
M5.Lcd.setCursor(2, 120);
M5.Lcd.println("Press Button");
M5.Lcd.setCursor(10, 130);
M5.Lcd.println("to START");
breath.setResistance(3); // sensitivitas napas (1-5)
breath.setMaxOut(127);
Serial.println("=== BREATH HOLD TEST ===");
Serial.println("Tekan BtnA untuk Ready...");
}
void loop() {
M5.update();
// ---- Tombol A -> Ready ----
if (M5.BtnA.wasPressed()) {
testReady = true;
inhaleDetected = false;
holding = false;
exhaleDetected = false;
inhaleSince = holdSince = exhaleSince = 0;
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextSize(1);
M5.Lcd.setCursor(25, 20);
M5.Lcd.println("READY");
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(11, 60);
M5.Lcd.println("TAKE");
M5.Lcd.setCursor(2, 80);
M5.Lcd.println("Breath");
M5.Lcd.setCursor(18, 100);
M5.Lcd.println("and");
M5.Lcd.setCursor(14, 120);
M5.Lcd.println("Hold");
Serial.println("Status: READY");
}
// ---- Baca sensor ----
int16_t raw = breath.read();
int16_t breathValue = INVERT_SIGN ? -raw : raw;
// Tampilkan sedikit debug di serial (opsional, bisa dikomentari)
// Serial.printf("breathValue: %d\n", breathValue);
if (!testReady) {
delay(30);
return;
}
// ---- Deteksi INHALE (nilai negatif) ----
if (!inhaleDetected && !holding) {
if (breathValue <= -threshold) {
if (inhaleSince == 0) inhaleSince = millis();
if (millis() - inhaleSince >= INHALE_TRIGGER_MS) {
inhaleDetected = true;
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(10, 20);
M5.Lcd.println("Inhale");
M5.Lcd.setCursor(10, 40);
M5.Lcd.println("Detected!");
M5.Lcd.setTextSize(1);
M5.Lcd.setCursor(0, 60);
M5.Lcd.println("...Hold Breath...");
Serial.println("Inhale detected");
holdSince = 0; // siap cek stabil utk Hold
}
} else {
inhaleSince = 0;
}
}
// ---- Setelah inhale, tunggu stabil (deadband) untuk mulai HOLD ----
if (inhaleDetected && !holding && !exhaleDetected) {
if (abs(breathValue) <= deadband) {
if (holdSince == 0) holdSince = millis();
if (millis() - holdSince >= HOLD_STABLE_MS) {
holding = true;
startTime = millis();
M5.Lcd.setTextSize(1);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(0, 20);
M5.Lcd.println("Hold Breath");
Serial.println("Hold started");
}
} else {
holdSince = 0;
}
}
// ---- Saat HOLD: update timer & pantau EXHALE (nilai positif) ----
if (holding && !exhaleDetected) {
holdTime = (millis() - startTime) / 1000;
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(10, 70);
M5.Lcd.printf("Time:");
M5.Lcd.setTextSize(3);
M5.Lcd.setCursor(10, 90);
M5.Lcd.println(holdTime);
Serial.printf("Holding... %lus\n", holdTime);
// Deteksi exhale untuk stop
if (breathValue >= threshold) {
if (exhaleSince == 0) exhaleSince = millis();
if (millis() - exhaleSince >= EXHALE_TRIGGER_MS) {
exhaleDetected = true;
holding = false;
testReady = false;
showResult(holdTime);
}
} else {
exhaleSince = 0;
}
}
delay(30);
}
// ---- Hasil + kategori (tetap seperti versi kamu) ----
void showResult(unsigned long seconds) {
String kategori;
if (seconds < 20) kategori = "Rendah";
else if (seconds < 40) kategori = "Normal";
else if (seconds < 60) kategori = "Baik";
else kategori = "Sangat Baik";
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextSize(3);
M5.Lcd.setCursor(10, 40);
M5.Lcd.printf("Hasil: %lus", seconds);
M5.Lcd.setCursor(10, 90);
M5.Lcd.println("Kategori:");
M5.Lcd.setCursor(10, 130);
M5.Lcd.println(kategori);
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(10, 180);
M5.Lcd.println("BtnA = tes ulang");
Serial.println("=== TEST RESULT ===");
Serial.printf("Waktu tahan napas: %lus\n", seconds);
Serial.print("Kategori: ");
Serial.println(kategori);
Serial.println("===================");
}
Result from coding
SpiroStack code ready to use, now we must design in mask, I use tinkercad to design SpiroStack Cover
Fitting in mask
Step4-Add Hepafilter in Mask(additional)
While designing SpiroStack, I realized that safety and hygiene are just as important as accuracy. Direct breathing into a sensor could transmit germs, dust, or other harmful particles to the device. To counter this, I added a HEPA filter inside the hole of the airflow of the mask.
Testing SpiroStack
Comments