Introduction: Pandemic Ventilator

I am not an engineer, a roboticist, a mathematician, a medical person, an electrician, or a computer scientist.

I am a Maker.

There are many things about this project that still require refinement, and I will continue to develop it, however, this is the minimum viable product, suggestions on how to make improvements are welcome- especially to the code.

This is after many iterations of development, and many of my ideas have been attempted and discarded already.

I started with an idea. Then I drafted some potential courses of action, did a lot of research, and discovered that one of my courses of action is a near-perfect match to the MIT grad student project from 2010. The iteration documented here resembles the MIT project strongly, but in all of my research, I did not come across one single piece of documentation that included a build list or code.

There are a lot of forums out there with a lot of posters making suggestions and posting ideas, but not one piece of buildable documentation.

This is mine.

Supplies

1x Adafruit MPRLS Ported Pressure Sensor Breakout - 0 to 25 PSI

1x uxcell 2pcs Gray Steel Round Rod Turning Lathe Bars Tool 6mm x 200mm

2 pair uxcell Shaft Coupling 6mm to 6mm Bore L22xD14 Robot Motor Wheel Rigid Coupler Connector Silver Tone 2 Pcs

1x 2pcs BTS7960 43A High Power Motor Driver Module/Smart Car Driver Module for Arduino Current Limit

1x CQRobot Metal DC Geared Motor w/Encoder with Metal Mounting Bracket -12V/65RPM /58Kg.cm. 1 pair uxcell 6mm Inner Dia H15D15 Rigid Flange Coupling Motor Guide Shaft Coupler Motor Connector 2PCS for DIY Parts

1x uxcell 5pcs 10K OHM 3 Terminal Linear Taper Rotary Audio B Type Potentiometer

1x ALITOVE 12V 8A 96W Power Supply Adapter AC to DC Converter AC 110V ~ 240V to DC 12V 8amp Transformer LED Driver with 5.5x2.1mm DC Jack for 5050 3528 L

1x 19998784 PT# 845011 The Bag II Adult Ea by, Laerdal Medical Corp -19998784

1x Clean Cpap Premium Universal 6ft Cpap Tube 72 inch Tubing by Clean Cpap Hose Resmed Respironics Compatible

1x HiLetgo 3pcs ESP8266 NodeMCU CP2102 ESP-12E Internet WiFi Development Board Open Source Serial Wireless Module Works Great with Arduino IDE/Micropython (Large)

1x 50mm x70mm perfboard, pin headers and headers from DEYUE 60 Pcs PCB Perforated Printed Circuits Boards Kit | 28 Double-Sided Prototyping PCBs Circuit Boards | 20 Female/Male Header Connector Pin | 12 PCB Terminal Blocks and A Happy DIY

1x 2 of HiLetgo 0.96" I2C IIC SPI Serial 128X64 OLED LCD Display 4 Pin Font Color White

TUOFENG 22 awg Solid Wire-Solid Wire Kit-6 different colored 30 Feet spools 22 gauge Jumper wire- Hook up Wire Kit

1x 24inch x 48 inch x1/2 inch PVC board

1x 1v1/2inch x 3/4 inch x 8feet PVC board

Step 1: PVC Chassis

I selected PVC as my construction material. It is easy to work with, relatively low cost, and I already have all of the tools to work it.

I cut the PVC with a table saw and a miter saw. I used a 2-1/2 inch hole saw and a 3-1/2 inch hole saw for the round holes. I used a basic sander to round off the end of the actuator arm.

I assembled the chassis with construction screws and PVC cement.

The base and back are 14-inch x 8-inch.

The bag supports are 5-inch squares with 3-1/2 inch and 2-1/2 inch holes drilled center.

I cut off a single corner of each bag support at a 45° angle to support the circuit board backer.

The circuit board backer is 2-1/4 inch by 5-3/4 inch.

The third support is 1-1/2 inch wide by 5inch at its longest, cut at a 45° angle.

The motor controller supports (not shown) are 1/2 inch square by 1-3/4 inch.

The actuator arm is 6-1/4 inch cut from the 1-1/2inchx3/4 inch board. It has been cut to 1-inch width.

Step 2: Electronics

After breadboarding, I moved the plan to a perfboard, marking the board with permanent markers. The circuit paths and the headers are shown in the picture, the NODEMCU header rows are not shown in this picture.

I soldered header rows to the perfboard to mount the NODEMCU microcontroller.

I soldered another header row to attach the OLED and the motor-controller pins

The OLED display SCL is attached to Pin D2, OLED SDA is attached to D3, VCC to 3v, and GND to ground.

The Potentiometer center pin is attached to AO, set as analog read, the potentiometer two outer pins are attached to 3v and GND,

The pressure sensor SCL pin is attached to pins D2, SDA pin is attached to D3, Vcc to 3v and GND to ground.

The motor controller is connected to pins D5 and D6, set as digital out, 3v and GND. These pins can also be used as PWM, but with the motor I am using I could only get proper performance at PWM 1000, which is the same as digital HIGH.

You may note that I've not included physical controls, due to my limitations and the pin limitations of the NODEMCU I opted for an APP-based control, using Blynk to monitor and send/ receive variables. Suggestions are welcome.

( I had initially built a 4 button control, but that uses 4 pins, 2 of which I needed for the motor-controller, plus power) One refinement I intend to work is using a rotary encoder with push-button to drive the menu, I assess that it will only require 2 pins that are available plus power).

You may also notice in the photo the 12v-5v buck converter is not present- I had a bad buck converter, and at the time of the photo I was using a 5v wall adapter for the microcontroller and the 12v power directly attached to the motor-controller.

Step 3: Mechanics

I am using a single drive shaft coupled to the motor output shaft at one end and the potentiometer at the other end. The actuator arm is coupled to the shaft with two wheel adapters.

In this iteration, I am using two pins through the wheel adapters and the actuator arm to allow the wheel adapters to turn the actuator arm. This works as a poor-mans clutch allowing me to de-couple the arm from the shaft as needed. Once all code calibrations are finalized these pins can be replaced with bolts.

Using a rotary tool I machined flat surfaces on the shaft to allow the set screws to engage and prevent shaft slippage.

You may note the lack of any bearing surface for the shaft, by good fortune the wheel adapters sit on the base and provide shaft support. I could have added additional bearings, but this seemed like a less complex solution.

A note about aligning the shaft from motor to potentiometer. I've made the potentiometer mount using aluminum angle stock. Ensure the mount is the same width and the center of the hole is positioned to match the output shaft of the motor. This will make aligning the motor, shaft, and potentiometer much easier.

I connected the CPAP hose to the bag and the mask. The Adafruit pressure sensor is mounted by using a drill and 5/132nd drill bit to make a hole through the silicone piece at the end of the hose. The sensor air port is pushed into the hole and I've secured it tightly (but gently) with a zip tie around the hose.

Step 4: App

To simplify the controls and preserve available pins on the micro-controller I choose to use an APP for monitoring and setting variables.

I've used the Blynk app to accomplish this. I realize this may not be the best austere-environment solution, but for this stage of my build it reduced some complexity and otherwise achieves my design goals.

Virtual Pins described Left to right, top to bottom

Label, Widget type, Virtual Pin, Code variable

Version- Text input- V7 - version Control

Start/Stop- Button slider- V1- runStop

Pressure- SuperChart- V10- pressure

Pressure- Value Display- V10- pressure

BPM- Numeric Input - V2 - BPMRate

TidalVolume - Numeric INput - V3- TidalVolume

PEEP- Numeric Input- V4- PEEP

LowReleased- Numeric Input- V9- lowReleased

HighCompressed- Numeric Input- V8- highCompressed

Step 5: The Code- I Know That I Do Not Know What I Do Not Know-

*This is my code so far.  

Im sure the professionals will take a look and determine what I already know:

 That I dont know what I am doing,

This code is not complete, I am still working through some details, 

and I welcome any expert advice that helps me learn and make a better end result.

Thanks for looking,   If You use my code I would really like to hear from you, 

I'd like to see your end  result.  Stay safe, stay well.  -Drew

#define BLYNK_PRINT Serial 
#define analogPin A0;
#define PCP1 14        // NodeMCU  pin D3-GPIO0
#define PCP2 12        // NodeMCU  pin D4-GPIO2
#include <blynksimpleesp8266.h>
//#include <onewire.h>
//#include <spi.h>
#include <wire.h>//adafruit I2C
#include "Adafruit_MPRLS.h"//presuure sensor
#include <adafruit_gfx.h>  /display
#include <adafruit_ssd1306.h> //display
#define RESET_PIN  -1  
#define EOC_PIN    -1  
Adafruit_MPRLS mpr = Adafruit_MPRLS(RESET_PIN, EOC_PIN);
BlynkTimer timer;
Adafruit_SSD1306 display(-1);
char auth[] = "you need a blynk project authentication code";
char ssid[] = "wirelessSSID";
char pass[] = "wifi password";
float BPMCalc;
int BPMMotor;
float BPMRate;
int PEEP;
int runStop;
int TidalVolume;
float pressure;
int highCompressed;
int lowReleased;
int strokeAvg;
int mPosition;
int motorReverse;
float pressure_hPa;
float interval;
// motorPosition
int mPos;
int waitingCount;
unsigned long previousMillis;
unsigned long currentMillis;
String motorDirection = "Stopped";
// end motorPosition
String versionControl = "08f";
BLYNK_WRITE(V1) {  
  runStop = param.asInt();
  startSwitch(); 
}
BLYNK_WRITE(V2) {  
  BPMRate = param.asInt();
  startSwitch();
}
BLYNK_WRITE(V3) {  
  TidalVolume = param.asInt();
  startSwitch();   
}
BLYNK_WRITE(V4) {  
  PEEP = param.asInt();
  startSwitch();   
}
BLYNK_WRITE(V9) {  
  lowReleased = param.asInt();
}
BLYNK_WRITE(V8) {  
  highCompressed = param.asInt();
}
BLYNK_WRITE(V10) {  
  pressure = param.asFloat();
}
void setup() {
	Serial.begin(115200);
	Blynk.begin(auth, ssid, pass);
	while (Blynk.connect() == false) {}       // Wait until connected    
	display.begin(SSD1306_SWITCHCAPVCC, 0x3C);    // initialize with the I2C addr 0x3C
	display.clearDisplay();  
	display.display();                // clears the lcd
	timer.setInterval(1000, pressureCheck);
	if (! mpr.begin()) {
	Serial.println("check pressure sensor");
	while (1) {
	  delay(10);
	}
	}
	Serial.println("starting");
	// sets the arm rotation limits
	highCompressed = 410;
	Blynk.virtualWrite(8, highCompressed);  
	lowReleased = 273;
	Blynk.virtualWrite(8, lowReleased); 
	strokeAvg = (highCompressed + lowReleased) / 2;
	Blynk.virtualWrite(7, versionControl);
	// sets the breath per minute
	BPMCalc = (30000.000/ BPMRate);
	TidalVolume = 500;
	PEEP = 100;
	splashScreen();
	timer.setInterval(3300, startSwitch);
	timer.setInterval(100, motorPosition);
	pinMode (PCP1, OUTPUT);
	pinMode (PCP2, OUTPUT);
}
void startSwitch() {
	if (runStop == 1) {
	menuMain();
	Blynk.virtualWrite(1, runStop);            
	} else if (runStop != 1) {
	runStop = 0;
	Blynk.virtualWrite(1, runStop);
	display.clearDisplay(); 
	display.setTextSize(2);
	display.setTextColor(WHITE);
	display.setCursor(1,10);
	display.print(versionControl);
	display.println(": OFF");
	display.display(); 
  }
}
void splashScreen() {
	Serial.println(versionControl);
	display.clearDisplay(); 
	display.setTextSize(2);
	display.setTextColor(WHITE);
	display.setCursor(0,10);
	display.println(versionControl);
	display.display(); 
}
void menuMain() {
	display.clearDisplay();   
	display.setTextSize(1);
	display.setTextColor(WHITE);
	display.setCursor(0,24);
	display.println("BPM");
	display.setCursor(50,24);
	display.println("TV");
	display.setCursor(98,24);
	display.println("PE");
	display.setTextSize(2);
	display.setCursor(0,1);
	display.println(BPMRate);
	//  Serial.println("BPM is:");
	//  Serial.println(BPMRate);  
	display.setCursor(40,1);
	display.println(TidalVolume);
	//  Serial.println("TV is:");
	//  Serial.println(TidalVolume);  
	display.setCursor(90,1);
	display.println(PEEP);
	//  Serial.println("PEEP is:");
	//  Serial.println(PEEP);
	display.display();
}
void pressureCheck() {
    pressure_hPa = mpr.readPressure();
    pressure = pressure_hPa / 68.947572932;
	Serial.println ("BPMRate");Serial.println (BPMRate);
    Serial.println ("pressure");Serial.println (pressure);
        Blynk.virtualWrite(10, pressure);
}
void motorPosition() {
	BPMCalc = 30/ BPMRate;
	mPos = analogRead(A0);
    Blynk.virtualWrite(1, runStop);
	if (runStop != 1) {
    motorControl(4);
    waitingCount = 0 ;
    Serial.print("Stopped from button at: ");
    Serial.println(mPos);
	} else {
    if (motorDirection == "Stopped"){ //Deal with restarting while in the middle of a stroke by releasing
      if (mPos > strokeAvg) {
        motorControl(1); //release
        Serial.print("Startup release. Position: ");
        Serial.println(mPos);
      } else if (mPos < strokeAvg) {
        motorControl(0); //set compress
        Serial.print("Startup compress. Position: ");
        Serial.println(mPos);
      }
    } else if ((motorDirection == "Compressing") && (mPos >= highCompressed)) { // if motor is compressing and has hit end of range, switch direction
      motorControl(1); //release
      Serial.print("Compress limit hit, releasing with no pause at: ");
      Serial.println(mPos);
	} else if ((motorDirection == "Releasing") && (mPos <= lowReleased)) { // if motor is releasing and hit top of range, stop and set wait
      motorControl(3); //set wait
      Serial.print("Release limit hit, resting for ");
      Serial.print ("BPMCalc"); Serial.println (BPMCalc);
      Serial.println(" seconds");
      previousMillis = millis();
    } else if (motorDirection == "Waiting") {
              BPMCalc = (30000.000/ BPMRate);
              Serial.println("Rest done, starting to compress");
              previousMillis = millis();
              motorControl(0);
              } 
            }
        }
    }
int motorControl(int action) {
  if (action == 0) {
    digitalWrite(PCP2,HIGH);
    digitalWrite(PCP1, 0 );
    motorDirection = "Compressing";
  } else if (action == 1) {
    digitalWrite(PCP2, 0 );
    digitalWrite(PCP1, HIGH);//HIGH
    motorDirection = "Releasing";
  } else if (action == 3) {
    digitalWrite(PCP2, 0 );
    digitalWrite(PCP1, 0);
    motorDirection = "Waiting";
  } else {
    digitalWrite(PCP2, 0 );
    digitalWrite(PCP1, 0 );
    motorDirection = "Stopped";
  }
}
void loop() {
  Blynk.run();
  timer.run();
currentMillis = millis();
startSwitch();
}</p>