Introduction: Accurate VO2 Max for Zwift and Strava

Human bodies are engines that utilize oxygen to burn fuel. Humans are not built with gauges and checking to see how the human engine is running is usually based on things we find easy to measure--pulse, Blood pressure. These do not directly tell us how the engine is burning its fuel. Measuring the amount of oxygen used with exertion provides better information on how hard the bodies engine is working--the equivalent of looking at the gauges on your cars dashboard. My previous work on measuring the components of exercise physiology were encumbering and not elegant enough to make them easily portable: https://www.instructables.com/Real-VO2Max-Measure... So after being introduced to the joys of Zwift bicycling with a Wahoo trainer I built a device that enables you to accurately add VO2 Max to the bluetooth information displayed on the Zwift screen animation while you are peddling along. The device also easily pairs with the Strava app for providing VO2 Max data for all your trail excursions. It substitutes VO2 max information for the heart rate sensor data in both of these popular exercise programs. The device is portable and lightweight with Wifi and Bluetooth capabilities and is easily worn with a modified 3M mask designed to be comfortable for long term use. I have tested the device for other sports including cross-country skiing and skating but its portability and wireless transmission capacity make it amenable to just about any sport other than swimming. The device also communicates with an App for the iPhone which enables graphing and long term data storage and download for ancillary calculations. The output includes continuous output of VO2, calories consumed, volumes of expired gas, as well as performing such functions as Basal Metabolic Rate. The device is easily made for about $100. Parts are all readily available and the case is 3D printed. It can be assembled in about 10 minutes. We tested the device in a physiology lab against a $60,000 machine and found it gave the about the same results.

Step 1: Gather Your Parts

There are only four main parts to the unit. A differential pressure sensor, an oxygen sensor--both connected by I2C to a TTGO Esp32 microcontroller with screen. A Lipo Battery with switch completes the unit. The original unit included a Laser CO2 sensor and an analogue version of the differential pressure sensor. The digital version of the sensor was found to be much more accurate and with a better range of 1--250 Pa.

1. TTGO T-Display ESP32 CP2104 WiFi bluetooth Module 1.14 Inch LCD Development Board $11

2. Omron--D6F-PH0025AD2--$40 from Digikey

3. Gravity: I2C Oxygen Sensor--$50 from DF Robot

4. Lipo Battery -- $5 1000 Mah

5. Switch ON/OFF ---$1

6. PARTICULATE RESPIRATR MASK -- 3M $20 Digikey (you don't really care about the filtration on this mask they are removed)

Step 2: 3D Print Your Parts

All parts are printed in PLA. All files are included. No support was used except for the computer housing. The body of the unit consists of three parts--the most important being the venturi tube with ports for both the differential pressure sensor and the oxygen sensor. The measurements of the internal structure of the throat are carefully laid out and measured and cannot be changed or it will drastically effect the results of the output. The venturi tube is nested into the body enclosure. The third part is the access door/ computer housing which is held on by two 3mm screws. A small retaining shield is also included to seal off the computer and make assembly easier.

Step 3: Wire It

The actual wiring for this project is minimal. It consists of connecting two I2C devices to the computer and supplying them with power and ground. They each have different I2C addresses and the pull-up resistors are included in the DF Robot O2 sensor. The O2 sensor is carefully marked for which wires go where, however, pay carefully attention to the wiring diagram included above for the Omron sensor before wiring. (Other Omron sensors of the same type have different wiring patterns!) Both of the sensors take 3 Volts which is obtained off of the TTGO board. This ESP32 board has I2C inputs on pins 21( SDA) and 22(SCL). Multiple options on the board are available for G and Power (3V). The battery supply for voltage is delivered to the small battery connector on the back of the board interrupted by a simple on/off switch. To enable charging you must have the button in the on position and provide power through the USB-C connector. I used a 1000Mah battery which easily powers the unit for several hours. The computer body is carefully designed to fit the battery snugly and you might want to measure it before ordering--there is some variation in size.

Step 4: Build It

The only unique part of the assembly is mounting of the Omron sensor. Two passages located in the venturi tube connect the sensor with the 3D printed ports within the venturi tube. These were built to accommodate two pieces of standard aquarium tubing with an inside diameter of 4mm. This tubing is designed to have a very snug fit into these holes. The tubes sink to a predetermined depth and are then trimmed to accommodate the two fluted ends of the sensor. Please see detailed pictures to clarify the description. There is also a polarity to these input tubes to the sensor--the sensor must be mounted per the photo to have a low and high pressure sensor input to be correct. A bead of hot glue on the tubes will hold them in position. The Oxygen sensor tube friction fits into its holder in the venturi tube--you might want to put a bead of hot glue on the unit to keep it in position. A defect of this sensor allows the chemical unit to separate very easily from the digital measuring unit. This is done on purpose as the O2 sensor head--like all O2 sensors must be replaced about every two years. Threaded inserts for 3 mm screws are heat inserted into the 3D printed holes in the body of the unit. The battery is placed into position in the wall of the main body. The On/Off switch is superglued into the hole in the bottom of the main body. When the two sensors are mounted into correct position in the venturi tube it is hot glued into the main body as shown. The TTGO board with attached wiring is hot glued into position in the side wall. Make sure the buttons are visible through their respective holes and the USB-C connector is open in the hole. The cover is placed over the wiring and sealed with hot glue. The wires are carefully organized and the case closed with two 3mm screws. The button housings are superglued over their respective holes after inserting the flanged buttons. The 3M mask is modified by removing the filters from the side holes which are input. The output valve at the center of the mask has a small plastic cover over it. This is clipped off and removed. The adapter unit that you printed is then glued to the front of the mask.

Step 5: Program It

There are two programs that run the unit. Both are Arduino IDE based. FinalZwiftConnect with files DFRobot_OxygenSensor.cpp and DFRobot_OxygenSensor.h are used when the unit is broadcasting VO2 max to either Zwift or Strava. This is based on Andress Spiess work in YouTube #174. After loading, the initial screen will request the users wt in pounds. Another screen with Zwift will appear and the button with "go" will initiate the session. The loop function checks the Omron sensor for a pressure drop and initiates a time function to calculate the total volume of air moving past the sensor using Bernoulli's equation. Every five seconds if there is no breath the seeO function is called to check the O2 level. At thirty second intervals the goFigure function calculates the minute volume of expired O2 and calculated levels of CO2 and VO2 Max. The program sends the bluetooth characteristic to the Zwift receiver on either your Mac or Apple TV masqueraded as the Heart Rate which approximates a typical VO2 max level. The ESP32 screen then presents alternatively Time, VO2, VO2Max, Cal burned, Max Cal/Day, O2 level and Volume(Liters of O2 used).

The alternative program is FinalSensirionScreen with above DFRobot_OxygenSensor.cpp and DFRobot_OxygenSensor.h as well as Sensirion_GadgetBle_Lib.cpp and Sensirion_GadgetBle_Lib.h. This is a wonderful App(https://apps.apple.com/us/app/sensirion-myambience/id1529131572) that allows you to connect the sensor to your iPhone and collect data, graph it and distribute the data by text or email. The App is designed for collecting data from a CO2 sensor so you have to spoof it by sending the Volume Minute of O2 to the CO2 level screen, the VO2 max to the Temp screen and the O2 level to the Humidity screen. These three pieces of data can then be saved and then downloaded to a spreadsheet program of your choice. The function of the two programs other than their output is identical including the screen output on the ESP32. The App sends out the data in a .edf format which is problematic from a Mac standpoint. Relabel the file as a ".csv" file and this allows you to import it in a regular fashion. Turn on the app only after the unit has been running otherwise it will tend to collect enormous amounts of empty data sets that have to be culled. The two graph screen shots above are from the Sensirion App. The first is treadmill output(VO2 max) the second is X-country skiing(Cal/Day).

Step 6: Testing It

Several compromises were made in the design of the unit: no CO2 sensor and it's totally compact and weights only 4 oz. Physiology labs that are normally used for testing VO2 max cost upwards of $60,000 and are certainly not portable. They have sensors that measure the same things only on a much finer level. But since what you're trying to study may not really require this degree of granularity perhaps our machine will be adequate. CO2 sensor data helps refine the difference between volume inhaled and exhaled which are not exactly the same. Volumes of these two gasses relative to unchanging amount of Nitrogen in room air makes this possible, but is this a really significant difference? Humans are a fairly subjective test animal and within the limits of testing humans do these things matter? We brought our unit to the University of Alaska physiology lab and compared it. All machines are benchmarked for checking a standardized volume with a 3 liter syringe. The labs unit passed with all measurements being +/- 50 cc. Our unit did very well with all measurements within +/- 100cc. An error this small over liters of air/min is not that significant. Standardized gas (O2 16%) was then passed through our O2 sensor and found to be off by only 0.16%. We then ran our test subject through two levels of testing on the treadmill. The results from both O2 volume and VO2 readings were nearly identical. While there was significant minute to minute variability in the VO2 output from the lab machine the averages per minute turned out to be about the same with each instrument. A more thorough test would involve more test subjects and extended periods of testing of each instrument.

Step 7: Using It

The adapter unit on the mask is threaded and allows the corresponding portion of the unit to be screwed into it. It is designed so that the computer unit and the oxygen sensor are off to the right when looking at the mask front on. This prevents fluid from dripping down into the O2 sensor head. Programing is done through the USB-C connector. Charging the battery is accomplished by turning on the unit and then plugging it in. When using the unit with either Strava or Zwift you have to pair the unit with the software that you are using. Make sure bluetooth is enabled on the phone that you have Strava on then go to the record function at the bottom of the screen. Once you push it a Heart icon will appear at the bottom--just push it to bring up the bluetooth pairing screen and find the unit. It will then pair. For the Zwift unit the pairing is just as simple. When the screen that pairs the bike trainer cadence and power meters appears the hear rate monitor will also appear. This will pair the unit with the screen output. On subsequent pairings it should automatically synch. The data from your bike ride can then be graphed and played with in the same manner as your power output and cadence. The graphs above are the VO2 max output graphed on the Zwift App. When using the unit with the Sensirion App for x-country skiing, biking and skating just turn on the device add your weight and push the go button. Then turn on the Sensirion App and it will automatically pair with the unit and start recording data. This is a way of studying many aspects of the human physiology engine that is now portable and cheaply done. How hard is any participant in a sport working? Soft data like pulse rate may offer some clues but the actual amount of energy being consumed will reveal much more about the function of the inner machine. Most VO2 max equations currently available on watches and trainers are based on these soft findings--now they can be based on real data. Since this type of testing has not been widely available due to limitations in size and expense this unit will open up the potential for studying how people improve with conditioning or reveal if they're truly working near their maximum output. The unit is also useful for studying basal metabolic rate. This is the amount of energy that is used by the body in doing nothing. The cost of just being alive. It is hard to study because it should be done with the participant sleeping or resting. Modern studies using doubly labeled water have revealed incredible incites to energy usage among hunter gatherers and office workers. These units could also be used to provide more insight to exertion than the counting of steps. I have found that with the calculations for energy expended from the Oxygen utilization algorithm is slightly higher than that calculated by the work/watt output of the Zwift App algorithm. So good news: more pizza slices from your ride!

Step 8: Additional Upgrades

A wonderful innovator from Bielefeld, Germany has worked with his son to develop the hardware and the software to improve the design of the VO2 max instrument. I have included the photos of his new output and modifications as well as his much superior software. His name is Ulli Rissel and while I encouraged him to write up a whole new Instructable he wanted me to just add it to this one. Here is a description of his changes:

Great project!Here is my version with numerous modifications:

1.I added a barometric sensor (BMP180) to calculate the air density. The calculation of VO2 should also be correct in the mountains thanks to the barometric sensor. The BMP180 sensor is connected to the I2C bus like the other sensors. I glued it between the two sensors on the Venturi tube. In Arduino, the library "Adafruit_BMP085" must be added.2.The VO2max and VEmin calculation is carried out with moving averages and measurement of each individual breath. This allows current measured values every 5 seconds. 3.The displays of the values can be switched during the measurement.4.If the sensor reaches its limit (at approx. 4.5L/s corresponding to a VEmin of 136L/min!) this is displayed. However, the error should only be minimal. If the limit is displayed too often, the Venturi nozzle would have to be modified. 5.A warning appears if the O2 sensor measures less than 20% VO2% at startup. Then the sensor (or room) needs to be ventilated before moving on. If "Continue" is pressed anyway, the value is set to 20.9%.6.Battery indicator: The voltage is displayed with background color to the state of charge. White: USB or charge, Green: Battery full, Yellow: Battery approx. 50%, Red: Battery approx. 20%. The display takes place at the top left. A 1100mAh Lipo lasts about 10h!7.Kcal are now calculated correctly with their own integral timer.8.DEMO mode when the top button is pressed when powered on. It simulates about 28L of respiratory minute volume with respiratory rate of 12 at 4% CO2. This allows you to test the data transfer.9.Reset by pressing both buttons. This works after the 3L check.10.The volume measurement was measured with a 3L calibration pump and a calibration factor was programmed. The volume measurement can be checked in the first 10 seconds with a real-time display. I tested 2 sensors and for practical reasons set the average calibration factor 0.92 for volume measurement. You are welcome to customize this in the program itself (is easy to find).11.The data is sent every 5 seconds as csv via USB cable and Bluetooth serial. Data transmission with cable and BT starts immediately after "Press to start". With Excel Datastream, a display can be made in real time.BT on Excel works really well, even with instant graphical representation. You can also send the data to the APP Bluetooth Serial Monitor on an Android mobile phone and process or send it from there.

As you can see his additions are truly great. But wait they get even better:

I've made some further changes:
Optionally, there is a Venturi nozzle with 20mm for trained athletes. The 16 mm version generates errors due to a too low maximum flow. The current software is set for 16mm and can also be used for 20mm. The necessary adjustment can be found in the commentary at the top of the program. Vo2 is sent via BLE as a heart rate, for example for Zwift, Strava, Wahoo or Garmin. With prolonged operation and high performance, O2 sensor failures occur caused by condensation. The sensor then displays too low O2 concentrations and VO2 is calculated too high. It gets a little better when the sensor is covered with a piece of an FFP2 mask. For this purpose, the diameter for the O2 sensor has been extended to 21mm. You can cut out a 5cm circle from an FFP2 mask, place it on the opening and push the sensor in. It holds without glue. Some thoughts on the calculation: The common spirometers measure the inspiration volume Vi in the supply line, not Ve. However, the concentration of O2 and CO2 is measured expiratory. Therefore, Ve must be calculated in order to correctly record the O2 consumption. Three factors come into play: 1) The volume shrinks minimally if less CO2 is produced than O2 is absorbed (i.e. at quotient <1). This can be corrected via the Haldane formula. However, the deviations are minimal. 2) Much more important is that BTPS conditions prevail in the expiration air (35°C, 95% humidity, actual ambient pressure). 3) The measurement has to be converted to STPD conditions (0 °C, 1013.25 hPa, 0% humidity). VO2 is defined under STPD conditions. (This was the missing part!) According to my research, the conversion is nothing more than the ratio of the density of BTPS (actually measured pressure, 35°C, 95% humidity) to STPD. The density of STPD is constant = 1.2922 kg/m³. The density of BTPS can be easily calculated, whereby instead of the gas constant 287.058, the constant for moist air is used for BTPS (about 292.8). The constant fluctuates in our temperature range depending on pressure only by a few percent, the density only in the per mille range, if 292.8 is not adjusted. The density indicates how many molecules are in the air. And now it becomes relatively simple: We measure Ve directly, so no further conversion is required. This results in: VO2 = Ve * O2diff% * Density BTPS / 1.2922. Done! By the way, according to the spirometry instructions, VO2 is only calculated from the O2 difference. CO2 is only used to calculate the quotient and for the Haldane formula, which we do not need.

Anything Goes Contest 2021

Runner Up in the
Anything Goes Contest 2021