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
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.
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.
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.
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
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>