/*
ESP32 DIY Ventilator System with LCD Display
Reads sensor data from Serial (simulated patient monitoring)
Displays ventilation parameters and patient vital signs on 20x4 LCD
- Reads JSON lines from simulated sensors: {"spo2":97,"hr":72,"rr":16,"pip":18,"peep":5}
- Controls ventilation parameters (tidal volume, respiration rate, I:E ratio)
- Displays alarms and system status
Designed for Wokwi simulation
Enhanced for professionalism, conciseness, and better performance.
*/
#include <ArduinoJson.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// ------------- Configuration -------------
#define LCD_I2C_ADDRESS 0x27
#define LCD_COLS 20
#define LCD_ROWS 4
#define SERIAL_BAUD 115200
#define BUTTON_UP_PIN 13
#define BUTTON_DOWN_PIN 12
#define BUTTON_SELECT_PIN 14
#define BUTTON_START_PIN 27
#define ALARM_LED_PIN 26
#define VENTILATOR_PIN 25
#define DEBOUNCE_DELAY 200
#define UPDATE_INTERVAL 1000
#define ALARM_TIMEOUT 5000
// Ventilation defaults
#define DEFAULT_RESP_RATE 16.0f
#define DEFAULT_TIDAL_VOL 500
#define DEFAULT_IE_RATIO 1.5f
#define DEFAULT_PIP_LIMIT 25
#define DEFAULT_PEEP 5
// ------------- Enums and Structs -------------
enum SystemState { STATE_IDLE, STATE_RUNNING, STATE_ALARM, STATE_SETUP };
enum ParameterSelect { PARAM_RESP_RATE, PARAM_TIDAL_VOL, PARAM_IE_RATIO, PARAM_PIP_LIMIT, PARAM_PEEP, PARAM_COUNT };
struct VentParams {
float respRate;
int tidalVolume;
float ieRatio;
int pipLimit;
int peep;
};
struct PatientData {
int spo2, hr, rr, pip, peep;
};
struct Alarms {
bool lowSpO2, highPIP, lowPEEP, disconnect;
};
// ------------- Globals -------------
LiquidCrystal_I2C lcd(LCD_I2C_ADDRESS, LCD_COLS, LCD_ROWS);
VentParams ventParams = {DEFAULT_RESP_RATE, DEFAULT_TIDAL_VOL, DEFAULT_IE_RATIO, DEFAULT_PIP_LIMIT, DEFAULT_PEEP};
PatientData patient = {0};
Alarms alarms = {false};
SystemState state = STATE_IDLE;
ParameterSelect selectedParam = PARAM_RESP_RATE;
unsigned long lastUpdate = 0, lastSerialRead = 0, lastButtonCheck = 0, cycleStart = 0;
bool inInspiration = false, paramChanged = false;
const char* paramLabels[PARAM_COUNT] = {"RR", "TV", "I:E", "PIP", "PEEP"};
// ------------- Initialization -------------
void initHardware() {
Serial.begin(SERIAL_BAUD);
Wire.begin();
lcd.init();
lcd.backlight();
pinMode(BUTTON_UP_PIN, INPUT_PULLUP);
pinMode(BUTTON_DOWN_PIN, INPUT_PULLUP);
pinMode(BUTTON_SELECT_PIN, INPUT_PULLUP);
pinMode(BUTTON_START_PIN, INPUT_PULLUP);
pinMode(ALARM_LED_PIN, OUTPUT);
pinMode(VENTILATOR_PIN, OUTPUT);
}
void showWelcomeScreen() {
lcd.clear();
lcd.setCursor(0, 0); lcd.print(" DIY VENTILATOR ");
lcd.setCursor(0, 1); lcd.print(" Medical Device ");
lcd.setCursor(0, 2); lcd.print(" Initializing... ");
delay(2000);
lcd.clear();
lcd.setCursor(0, 0); lcd.print(" WARNING: MEDICAL ");
lcd.setCursor(0, 1); lcd.print("DEVICE - FOR EDUC.");
lcd.setCursor(0, 2); lcd.print("PURPOSES ONLY. NOT");
lcd.setCursor(0, 3); lcd.print(" FOR CLINICAL USE.");
delay(3000);
}
// ------------- LCD Display Functions -------------
void updateDisplay() {
lcd.clear();
switch (state) {
case STATE_IDLE: displayIdle(); break;
case STATE_RUNNING: displayRunning(); break;
case STATE_ALARM: displayAlarm(); break;
case STATE_SETUP: displaySetup(); break;
}
}
void displayIdle() {
lcd.setCursor(0, 0); lcd.print("VENT: IDLE ");
lcd.setCursor(0, 1); lcd.printf("SpO2:%d%% HR:%d ", patient.spo2, patient.hr);
lcd.setCursor(0, 2); lcd.print("Press START to run");
lcd.setCursor(0, 3); lcd.printf("RR:%.0f TV:%dmL", ventParams.respRate, ventParams.tidalVolume);
}
void displayRunning() {
lcd.setCursor(0, 0); lcd.printf("VENT: RUN %s ", inInspiration ? "INSP" : "EXP ");
lcd.setCursor(0, 1); lcd.printf("SpO2:%d%% HR:%d%s", patient.spo2, patient.hr, alarms.lowSpO2 ? "!" : " ");
lcd.setCursor(0, 2); lcd.printf("RR:%.0f TV:%d I:E:%.1f", ventParams.respRate, ventParams.tidalVolume, ventParams.ieRatio);
lcd.setCursor(0, 3); lcd.printf("PIP:%d PEEP:%d%s", patient.pip, patient.peep, (alarms.highPIP || alarms.lowPEEP) ? "!" : " ");
}
void displayAlarm() {
lcd.setCursor(0, 0); lcd.print("**** ALARM ****");
lcd.setCursor(0, 1);
if (alarms.lowSpO2) lcd.printf("LOW SpO2: %d%% ", patient.spo2);
else if (alarms.highPIP) lcd.printf("HIGH PIP: %dcmH2O", patient.pip);
else if (alarms.lowPEEP) lcd.printf("LOW PEEP: %dcmH2O", patient.peep);
else if (alarms.disconnect) lcd.print("SENSOR DISCONNECT");
lcd.setCursor(0, 2); lcd.print("Check patient and");
lcd.setCursor(0, 3); lcd.print("ventilator system");
}
void displaySetup() {
lcd.setCursor(0, 0); lcd.printf("SETUP: %s", paramLabels[selectedParam]);
lcd.setCursor(0, 1); lcd.print("Current: ");
switch (selectedParam) {
case PARAM_RESP_RATE: lcd.printf("%.0f bpm", ventParams.respRate); break;
case PARAM_TIDAL_VOL: lcd.printf("%d mL", ventParams.tidalVolume); break;
case PARAM_IE_RATIO: lcd.printf("%.1f I:E", ventParams.ieRatio); break;
case PARAM_PIP_LIMIT: lcd.printf("%d cmH2O", ventParams.pipLimit); break;
case PARAM_PEEP: lcd.printf("%d cmH2O", ventParams.peep); break;
}
lcd.setCursor(0, 2); lcd.print("Use UP/DOWN to adj");
lcd.setCursor(0, 3); lcd.print("SELECT to confirm");
}
// ------------- Ventilator Control -------------
void controlVentilator() {
if (state != STATE_RUNNING) {
digitalWrite(VENTILATOR_PIN, LOW);
inInspiration = false;
return;
}
unsigned long now = millis();
float cycleTime = 60000.0f / ventParams.respRate;
float inspTime = cycleTime / (1.0f + ventParams.ieRatio);
if (!inInspiration) {
inInspiration = true;
cycleStart = now;
digitalWrite(VENTILATOR_PIN, HIGH);
} else if (now - cycleStart > inspTime) {
inInspiration = false;
digitalWrite(VENTILATOR_PIN, LOW);
}
}
// ------------- Alarm Handling -------------
void checkAlarms() {
alarms.lowSpO2 = (patient.spo2 > 0 && patient.spo2 < 90);
alarms.highPIP = (patient.pip > ventParams.pipLimit);
alarms.lowPEEP = (patient.peep > 0 && patient.peep < ventParams.peep - 2);
alarms.disconnect = (millis() - lastSerialRead > ALARM_TIMEOUT);
bool anyAlarm = alarms.lowSpO2 || alarms.highPIP || alarms.lowPEEP || alarms.disconnect;
digitalWrite(ALARM_LED_PIN, anyAlarm);
if (anyAlarm && state == STATE_RUNNING) {
state = STATE_ALARM;
updateDisplay();
}
}
// ------------- Button Handling -------------
void handleButtons() {
unsigned long now = millis();
if (now - lastButtonCheck < DEBOUNCE_DELAY) return;
lastButtonCheck = now;
if (!digitalRead(BUTTON_START_PIN)) {
state = (state == STATE_IDLE || state == STATE_ALARM) ? STATE_RUNNING : STATE_IDLE;
updateDisplay();
delay(300);
}
if (!digitalRead(BUTTON_SELECT_PIN) && state == STATE_IDLE) {
state = STATE_SETUP;
selectedParam = PARAM_RESP_RATE;
updateDisplay();
delay(300);
}
if (state == STATE_SETUP) {
if (!digitalRead(BUTTON_UP_PIN)) adjustParam(true);
else if (!digitalRead(BUTTON_DOWN_PIN)) adjustParam(false);
else if (!digitalRead(BUTTON_SELECT_PIN)) {
state = STATE_IDLE;
updateDisplay();
delay(300);
}
}
}
void adjustParam(bool increase) {
switch (selectedParam) {
case PARAM_RESP_RATE: ventParams.respRate = constrain(ventParams.respRate + (increase ? 1 : -1), 6, 40); break;
case PARAM_TIDAL_VOL: ventParams.tidalVolume = constrain(ventParams.tidalVolume + (increase ? 50 : -50), 200, 1000); break;
case PARAM_IE_RATIO: ventParams.ieRatio = constrain(ventParams.ieRatio + (increase ? 0.1f : -0.1f), 0.5f, 3.0f); break;
case PARAM_PIP_LIMIT: ventParams.pipLimit = constrain(ventParams.pipLimit + (increase ? 1 : -1), 15, 40); break;
case PARAM_PEEP: ventParams.peep = constrain(ventParams.peep + (increase ? 1 : -1), 3, 15); break;
}
paramChanged = true;
updateDisplay();
delay(200);
}
// ------------- Serial Data Processing -------------
void handleSerial() {
static String buffer;
while (Serial.available()) {
char c = Serial.read();
if (c == '\n') {
processJSON(buffer);
buffer = "";
lastSerialRead = millis();
} else {
buffer += c;
if (buffer.length() > 256) buffer = "";
}
}
}
void processJSON(const String &line) {
StaticJsonDocument<256> doc;
if (deserializeJson(doc, line) == DeserializationError::Ok) {
patient.spo2 = doc["spo2"] | patient.spo2;
patient.hr = doc["hr"] | patient.hr;
patient.rr = doc["rr"] | patient.rr;
patient.pip = doc["pip"] | patient.pip;
patient.peep = doc["peep"] | patient.peep;
Serial.printf("Received: SpO2=%d%%, HR=%d, PIP=%dcmH2O\n", patient.spo2, patient.hr, patient.pip);
if (state == STATE_RUNNING || state == STATE_IDLE) updateDisplay();
}
}
// ------------- Main Functions -------------
void setup() {
initHardware();
showWelcomeScreen();
updateDisplay();
Serial.println("DIY Ventilator System Ready.");
Serial.println("Expected JSON: {\"spo2\":97,\"hr\":72,\"rr\":16,\"pip\":18,\"peep\":5}");
}
void loop() {
handleSerial();
handleButtons();
controlVentilator();
checkAlarms();
unsigned long now = millis();
if (now - lastUpdate > UPDATE_INTERVAL) {
lastUpdate = now;
if (state == STATE_RUNNING) updateDisplay();
if (state == STATE_RUNNING) {
Serial.printf("Running - RR:%.0f, TV:%d, Phase:%s\n", ventParams.respRate, ventParams.tidalVolume, inInspiration ? "INSP" : "EXP");
}
}
}