Community project

Portable Lung Function Monitor

Arduino
Photo of Portable Lung Function Monitor
Generated with AI

Elias Abdelhakim Seid

Published October 26, 2026

This portable lung function monitor measures exhaled nitric oxide (FeNO) and airflow to help assess respiratory health. Built around an Arduino Uno, it combines a nitric oxide sensor and airflow sensor with real-time display on a compact OLED screen, all powered by a rechargeable LiPo battery with integrated charging and power management.

The guide provides a complete parts list, wiring diagram showing sensor connections through the ADS1115 analog-to-digital converter, step-by-step assembly instructions for the air path and sensor dividers, and calibrated Arduino firmware. Builders will learn to set up I2C communication, configure the dual sensors, and interpret FeNO and flow readings on the display.

Wiring diagram

Wiring diagram for Portable Lung Function Monitor

Gather all the parts

QtyComponent
1

ADS1115 16-Bit ADC Module

16-bit four-channel I2C analog-to-digital converter with programmable gain amplifier. Common ADS1115 breakouts expose MCU-facing SDA/SCL pins and four analog inputs for single-ended or differential measurements.

1

0.96" OLED Display 128x64 I2C (Blue)

Monochrome 0.96 inch OLED display with 128 x 64 blue pixels, driven by an SSD1306 controller over I2C (address 0x3C). It runs from 3.3 to 5 V and connects through a four-pin VCC, GND, SCL, SDA header.

1

LiPo 3.7V 1000mAh Battery

Single-cell LiPo pack, nominal 3.7 V, 1000 mAh. Default rechargeable choice for portable ESP32 / Pico projects. Pair with a TP4056 charger for safe USB recharging.

1

TP4056 Li-Ion/LiPo charger module with protection

TP4056 single-cell Li-Ion/LiPo linear charger module, 5V USB input, 1A charge current (programmable). Common variants ship with DW01 protection. Pair with battery_lipo_storage for the cell.

1

Adafruit Push-button Power Switch Breakout

Latching analog DC power switch breakout controlled by a momentary tactile button. No microcontroller needed; cleanly cuts power to a DC load when pressed.

1

Boost Converter

Small adjustable MT3608-style DC-DC boost converter module for stepping a lower DC input up to a higher rail such as 5V, 9V, or 12V. It is a power-path module with VIN/VOUT terminals, not a GPIO peripheral.

1

Push Button

Momentary push button switch

1

5 V Analog Nitric Oxide (NO) Sensor Module

An electrochemical sensor module that supplies an analog voltage representing nitric oxide concentration.

1

5 V Analog Airflow Sensor Module

A flow sensor module that supplies an analog voltage representing airflow through the mouthpiece path.

1

Disposable Spirometer Mouthpiece and Air Path

A replaceable breathing tube that guides exhaled air through the airflow sensor without being an electrical part.

1

10 kΩ Resistor for NO Sensor Divider

10 kΩ

A fixed resistor that reduces the NO sensor output voltage before the ADC.

1

20 kΩ Resistor for NO Sensor Divider

20 kΩ

A fixed resistor from the NO ADC junction to ground that completes the voltage divider.

1

10 kΩ Resistor for Airflow Sensor Divider

10 kΩ

A fixed resistor that reduces the airflow sensor output voltage before the ADC.

1

20 kΩ Resistor for Airflow Sensor Divider

20 kΩ

A fixed resistor from the airflow ADC junction to ground that completes the voltage divider.

Assemble it in 6 steps

1. ركّب مسار التنفس

ثبّت mouthpiece_1 بحيث يكون طرف الفم للخارج والطرف الآخر متصلًا ميكانيكيًا بمدخل airflow_sensor_1. اجعل مسار الهواء محكمًا ولا تجعل الرطوبة أو اللعاب يصلان إلى لوحة الحساس.

  • استخدم قطعة فم جديدة أو قابلة للتعقيم لكل مستخدم.
  • لا تستخدم هذا المسار لمشاركة النفس بين أشخاص؛ يمكن أن ينقل التلوث.

2. كوّن مقسمي جهد الحساسين

لكل حساس: صِل AOUT بالحساس إلى END1 للمقاومة 10 kΩ، ثم صِل END2 للمقاومة 10 kΩ ومدخل ADS1115 المقابل والطرف END1 للمقاومة 20 kΩ في نفس الصف. صِل END2 للمقاومة 20 kΩ إلى GND. استخدم no_series_r_1 وno_ground_r_1 مع AIN0، وflow_series_r_1 وflow_ground_r_1 مع AIN1.

  • المقاومات ليس لها اتجاه؛ القيمة 10 kΩ تحمل الألوان بني-أسود-برتقالي غالبًا، و20 kΩ أحمر-أسود-برتقالي غالبًا.
  • لا توصل خرج حساس 5 فولت مباشرة إلى ADS1115 العامل على 3.3 فولت؛ قد يتلف مدخل المحول.

3. صِل الشاشة والمحول بالـ Uno

صِل VDD في ads1115_1 وVCC في oled_1 إلى 3.3V (power)، وصِل طرفي GND إلى GND (ground). صِل SDA في القطعتين إلى A4/GPIO18 (data)، وSCL في القطعتين إلى A5/GPIO19 (clock). صِل VCC في no_sensor_1 وairflow_sensor_1 إلى 5V (power)، وأرضيهما إلى GND (ground).

  • اقرأ الملصقات المطبوعة على الشاشة؛ بعض الشاشات المشابهة تغير ترتيب أرجلها.
  • تأكد أن VCC وGND غير متبادلين — عكسهما قد يتلف الشاشة أو الحساس.

4. ركّب البطارية ومفتاح التشغيل

صِل +V في lipo_1 إلى B+ في charger_1 وGND إلى B-. صِل OUT+ في charger_1 إلى IN في power_switch_1 وOUT- إلى GND فيه. صِل OUT في power_switch_1 إلى VIN+ في boost_1 وGND إلى VIN-. صِل زر power_button_1 بين BTN وGND في power_switch_1.

  • اضبط خرج boost_1 بملتيميتر إلى 5.0 فولت قبل توصيل Arduino Uno.
  • لا تقصر سلكي البطارية ولا تشحن بطارية ليثيوم تالفة أو منتفخة؛ ذلك قد يسبب سخونة أو حريقًا.

5. غذِّ لوحة Arduino

بعد ضبط boost_1، صِل VOUT+ فيه إلى دبوس 5V في Arduino Uno (power)، وصِل VOUT- إلى GND في Arduino Uno (ground). رتّب الأسلاك بعيدًا عن مسار الفم والهواء.

  • اضغط زر power_button_1 لتشغيل أو إيقاف التغذية؛ شاشة OLED يجب أن تعرض رسالة البدء.
  • لا توصل خرج الرافع إلى دبوس VIN؛ هذا التصميم يخرج 5 فولت منظمًا ويجب أن يدخل دبوس 5V فقط.

6. عاير قبل أي قراءة

شغّل الجهاز في هواء نظيف واترك حساس NO يسخن بالزمن الذي يطلبه مصنع الحساس. عدّل معاملات المعايرة في البرنامج باستخدام نقطة الصفر ومعيار تدفق وغاز مرجعيين موثقين لطراز الحساس لديك.

  • العرض يضع نجمة بجانب FeNO ليذكّرك أن الرقم تقديري إلى أن تُدخل معايرة حسّاسك.
  • لا تستخدم الأرقام المعروضة لاتخاذ قرار علاجي أو لتغيير دواء؛ هذا نموذج هندسي وليس جهاز FeNO طبيًا معتمدًا.

Review all connections

1. Connections between "lipo_1" and "Arduino"

Functionlipo_1Arduino
power+V → TP4056 Li-Ion/LiPo charger module with protection B+EXT
groundGND → TP4056 Li-Ion/LiPo charger module with protection B-EXT

2. Connections between "charger_1" and "Arduino"

Functioncharger_1Arduino
powerIN+ → 5 V USB charging inputEXT
groundIN- → USB charging input groundEXT
powerOUT+ → Adafruit Push-button Power Switch Breakout INEXT
groundOUT- → Adafruit Push-button Power Switch Breakout GNDEXT

3. Connections between "power_switch_1" and "Arduino"

Functionpower_switch_1Arduino
powerOUT → Boost Converter VIN+EXT
groundGND → Boost Converter VIN-EXT

4. Connections between "power_button_1" and "Arduino"

Functionpower_button_1Arduino
digitalSIGNAL → Adafruit Push-button Power Switch Breakout BTNEXT
groundGND → Adafruit Push-button Power Switch Breakout GNDEXT

5. Connections between "boost_1" and "Arduino"

Functionboost_1Arduino
powerVOUT+ → Regulated 5 V supply rail5V
groundVOUT- → Common ground railGND

6. Connections between "ads1115_1" and "Arduino"

Functionads1115_1Arduino
powerVDD → 3.3 V ADC supply to keep I2C safe3V3
groundGND → ADC groundGND
i2cSDA → Shared I2C dataGPIO 18
i2cSCL → Shared I2C clockGPIO 19

7. Connections between "oled_1" and "Arduino"

Functionoled_1Arduino
powerVCC → 3.3 V display power3V3
groundGND → Display groundGND
i2cSDA → Shared I2C dataGPIO 18
i2cSCL → Shared I2C clockGPIO 19

8. Connections between "no_sensor_1" and "Arduino"

Functionno_sensor_1Arduino
powerVCC → NO sensor supply5V
groundGND → NO sensor groundGND
analogAOUT → 10 kΩ Resistor for NO Sensor Divider END1EXT

9. Connections between "no_series_r_1" and "Arduino"

Functionno_series_r_1Arduino
analogEND2 → ADS1115 16-Bit ADC Module AIN0EXT

10. Connections between "no_ground_r_1" and "Arduino"

Functionno_ground_r_1Arduino
analogEND1 → ADS1115 16-Bit ADC Module AIN0EXT
groundEND2 → NO divider lower leg to groundGND

11. Connections between "airflow_sensor_1" and "Arduino"

Functionairflow_sensor_1Arduino
powerVCC → Airflow sensor supply5V
groundGND → Airflow sensor groundGND
analogAOUT → 10 kΩ Resistor for Airflow Sensor Divider END1EXT

12. Connections between "flow_series_r_1" and "Arduino"

Functionflow_series_r_1Arduino
analogEND2 → ADS1115 16-Bit ADC Module AIN1EXT

13. Connections between "flow_ground_r_1" and "Arduino"

Functionflow_ground_r_1Arduino
analogEND1 → ADS1115 16-Bit ADC Module AIN1EXT
groundEND2 → Airflow divider lower leg to groundGND

Deploy the firmware

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_ADS1X15.h>

#define I2C_SDA_PIN 18
#define I2C_SCL_PIN 19

const uint8_t SCREEN_WIDTH = 128;
const uint8_t SCREEN_HEIGHT = 64;
const uint8_t OLED_RESET = 255;
const float DIVIDER_RECONSTRUCTION = 1.5f;  // 10 kΩ over 20 kΩ divider

// Replace these two values with the calibration supplied for YOUR NO sensor.
const float NO_ZERO_VOLTS = 0.0f;
const float FENO_PPB_PER_VOLT = 1.0f;

// Replace these values with the airflow module's calibration values.
const float FLOW_ZERO_VOLTS = 0.0f;
const float FLOW_LPM_PER_VOLT = 10.0f;
const unsigned long UPDATE_INTERVAL_MS = 500;

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
Adafruit_ADS1115 ads;
bool adcReady = false;
unsigned long lastUpdateMs = 0;

float clampNonNegative(float value) {
  return value < 0.0f ? 0.0f : value;
}

void drawStatus(float sensorNoVolts, float fenoPpb, float flowLpm) {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println(F("Spirometer / FeNO"));
  display.drawLine(0, 10, 127, 10, SSD1306_WHITE);

  display.setCursor(0, 16);
  display.print(F("Flow: "));
  display.print(flowLpm, 2);
  display.println(F(" L/min"));

  display.setCursor(0, 30);
  display.print(F("NO:   "));
  display.print(sensorNoVolts * 1000.0f, 1);
  display.println(F(" mV"));

  display.setCursor(0, 44);
  display.print(F("FeNO: "));
  display.print(fenoPpb, 1);
  display.println(F(" ppb*"));

  display.setCursor(0, 56);
  display.println(F("*Calibration required"));
  display.display();
}

void drawAdcError() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println(F("ADS1115 not found"));
  display.println();
  display.println(F("Check 3.3V, GND,"));
  display.println(F("SDA=A4, SCL=A5"));
  display.display();
}

void setup() {
  Wire.begin();
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.println(F("Starting monitor..."));
  display.display();

  adcReady = ads.begin(0x48);
  if (adcReady) {
    ads.setGain(GAIN_ONE); // ±4.096 V range; divider keeps the input below 3.3 V.
  } else {
    drawAdcError();
  }
  delay(800);
}

void loop() {
  if (!adcReady) {
    delay(250);
    return;
  }

  unsigned long now = millis();
  if (now - lastUpdateMs < UPDATE_INTERVAL_MS) {
    delay(20);
    return;
  }
  lastUpdateMs = now;

  int16_t noCounts = ads.readADC_SingleEnded(0);
  int16_t flowCounts = ads.readADC_SingleEnded(1);

  float noSensorVolts = ads.computeVolts(noCounts) * DIVIDER_RECONSTRUCTION;
  float flowSensorVolts = ads.computeVolts(flowCounts) * DIVIDER_RECONSTRUCTION;
  float fenoPpb = clampNonNegative((noSensorVolts - NO_ZERO_VOLTS) * FENO_PPB_PER_VOLT);
  float flowLpm = clampNonNegative((flowSensorVolts - FLOW_ZERO_VOLTS) * FLOW_LPM_PER_VOLT);

  drawStatus(noSensorVolts, fenoPpb, flowLpm);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik