PlatformIO firmware for an Arduino-based nitrox / oxygen analyzer with a 128x64 SSD1306 OLED, ADS1115 ADC, buzzer feedback, EEPROM-backed calibration, and a single-button UI. The code is originally based on Eunjae Im's OLED nitrox analyzer project.
This firmware accompanies the Divetech nitrox analyzer enclosure designed by Tony Land and is intended to build cleanly in Visual Studio Code with current PlatformIO and Adafruit libraries.
![]() |
![]() |
![]() |
![]() |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
The current project target is an Arduino Nano ATmega328P with the new bootloader configuration.
- Reads the oxygen sensor through an ADS1115 differential input
- Smooths sensor readings with a lightweight local moving average
- Displays live O2 percentage, max reading, sensor millivolts, and MOD values
- Stores calibration data and user settings (buzzer, pO2 selection, MOD units) in EEPROM so they survive power cycles
- Uses a single button hold-menu for lock, calibration, pO2 selection, buzzer toggle, MOD unit toggle, and max-clear actions
- Uses centered OLED layouts and subsetted Adafruit GFX fonts to preserve the condensed UI while reducing flash usage
- Skips redundant OLED redraws to reduce display flicker and unnecessary work
- Powers itself down after 5 minutes of inactivity, with a 10-second cancellable on-screen warning; a button press wakes it straight back to the live reading
- Arduino Nano ATmega328P (new bootloader)
- SSD1306 OLED display, 128x64, I2C, address
0x3C - ADS1115 ADC
- Oxygen sensor wired to ADS1115 differential input
A0/A1 - Push button on digital pin
2 - Buzzer on digital pin
3
Connection details, based on the original wiring notes:
- ADS1115:
VDDto5V,GNDtoGND,SCLtoA5,SDAtoA4 - OLED display:
VCCto5V,GNDtoGND,SCLtoA5,SDAtoA4 - Push button: one side to
GND, the other toD2 - Buzzer: positive to
D3, negative toGND - Oxygen sensor: positive to ADS1115
A0, negative to ADS1115A1 - Battery: positive to
VINthrough the rocker switch, negative toGND
The OLED and ADS1115 share the same I2C bus, so both modules connect to the Nano's A4/SDA and A5/SCL pins.
The original ejlabs notes also caution that a 9V battery is a poor long-term power choice for Arduino projects and that reverse polarity on VIN can damage the onboard regulator.
The printable enclosure is based on Tony Land's Divetech nitrox analyzer housing and was remixed to fit commonly available 0.96" 128x64 OLED modules whose display glass is larger than the original front-panel cutout.
The STL files are available from:
- GitHub: hardware/3d-models
- Printables: Divetech nitrox analyzer enclosure
Assembly Photos:
The Printables model lists these materials for the physical build, with example source links for several parts:
- Arduino Nano
.96 inch128x64OLED screen- ADS1115 analog-to-digital converter
- Rocker switch,
13 x 19 mmpanel mount - Push button,
12 mmdiameter 9 mmelectronic buzzer6M3x10 mm hex-head screws4M2x10 mm hex-head screws- Molex terminals
9Vbattery clip connector9Vbattery- O2 sensor (with Molex 3-Pin Connector)
- Wire and solder
- Tap: lock screen
- Hold for the menu-entry delay: enter the hold menu at
CAL - Keep holding: cycle through
CAL->PO2->BUZ->MOD->MAX-> normal screen, then repeat - Release while a menu label is shown: run that action
- Release while the normal screen is shown during the cycle: exit without changing anything
- Hold for
1second while powering on: reset the saved settings (buzzer, pO2, MOD units) to defaults; calibration is kept. Waking from auto power-off never triggers this reset. - Press during the auto power-off countdown: cancel the shutdown and stay on
- Press after auto power-off: wake the analyzer; it skips the splash screen and returns straight to the live reading
o2-analyzer/
├── .gitignore
├── hardware/
│ ├── 3d-models/
│ │ ├── back_cap.stl
│ │ ├── back_half.stl
│ │ ├── battery-sensor_cover.stl
│ │ ├── flow_restrictor.stl
│ │ ├── flow_restrictor_10mm.stl
│ │ ├── flow_restrictor_115mm.stl
│ │ ├── flow_restrictor_12mm.stl
│ │ ├── flow_restrictor_95mm.stl
│ │ ├── flow_restrictor_9mm.stl
│ │ ├── front_half_v2.stl
│ │ └── front_upper_cover.stl
│ └── assembly/
│ ├── assembly-[1-7].jpg
│ ├── assembly-8.png
│ └── README.md
├── images/
│ ├── nitrox_analyzer-[1-5].jpeg
│ ├── oled-arduino-nitrox-analyzer.png
│ └── oled_screenshot_[1-8].png
├── include/
│ ├── display_ui.h
│ ├── FreeSans9pt7bSubset.h
│ ├── FreeSansBold18pt7bSubset.h
│ └── settings.h
├── LICENSE
├── platformio.ini
├── README.md
├── src/
│ ├── display_ui.cpp
│ └── main.cpp
└── tools/
└── oled_capture.py
- src/display_ui.cpp: OLED rendering and text/layout helpers
- include/display_ui.h: display snapshot model and rendering function declarations
- src/main.cpp: firmware logic, UI rendering, button handling, calibration, and sensor processing
- include/FreeSans9pt7bSubset.h: subset small UI font
- include/FreeSansBold18pt7bSubset.h: subset large percentage font
- include/settings.h: firmware configuration constants for pins, timings, display settings, and calibration defaults
- .gitignore: ignores PlatformIO build outputs, editor settings, and macOS metadata
- hardware/assembly/README.md: assembly image gallery
- hardware/3d-models: printable enclosure and accessory STL files
- platformio.ini: PlatformIO target, libraries, and build flags
- tools/oled_capture.py: converts a captured SSD1306 framebuffer dump into a PNG image
Current PlatformIO configuration:
[platformio]
default_envs = nano
[env]
framework = arduino
monitor_speed = 57600
upload_speed = 115200
build_flags =
-D SSD1306_NO_SPLASH
-D SERIAL_RX_BUFFER_SIZE=16
-D SERIAL_TX_BUFFER_SIZE=16
lib_deps =
adafruit/Adafruit GFX Library @ ^1.12.6
adafruit/Adafruit SSD1306 @ ^2.5.16
adafruit/Adafruit ADS1X15 @ ^2.6.2
[env:nano]
platform = atmelavr
board = nanoatmega328newInstall PlatformIO with either the VS Code PlatformIO extension or the CLI:
python3 -m pip install --user platformioTo clone and build this firmware from Visual Studio Code:
- Install Visual Studio Code.
- Install the following extensions from the VS Code marketplace:
- PlatformIO IDE for dependency management, building, uploading, and serial monitoring.
- C/C++ for code navigation and IntelliSense.
- Ensure
gitis installed on your machine. - Open the VS Code Command Palette and run
Git: Clone. - Paste the repository URL and choose a local folder.
- Open the cloned
o2-analyzerfolder in VS Code. - Wait for PlatformIO to finish initializing the project and installing toolchains and libraries.
You can also clone from a terminal:
git clone https://github.com/kedube/o2-analyzer.git
cd o2-analyzer
code .After the folder is open in VS Code:
- Click the PlatformIO alien-head icon in the Activity Bar.
- Under
PROJECT TASKS>nano, runBuild. - Connect the Arduino Nano over USB.
- Run
Uploadto flash the firmware. - Run
Monitorto open the serial console at57600baud.
PlatformIO uses the default nano environment defined in platformio.ini.
This project has two main configuration layers: PlatformIO project settings in platformio.ini and firmware settings in include/settings.h.
Common options you may want to change in platformio.ini:
default_envs: selects the default build target. The current default isnano.board: selects the Arduino board definition. The current board isnanoatmega328new.monitor_speed: sets the serial monitor baud rate. The firmware uses57600.upload_speed: sets the upload baud rate. The current value is115200.upload_port: optional manual serial port override if auto-detection fails.build_flags: compile-time defines passed to the build.SSD1306_NO_SPLASHis enabled to save flash.SERIAL_RX_BUFFER_SIZEandSERIAL_TX_BUFFER_SIZE: reduced to16bytes to preserve SRAM for the SSD1306 framebuffer on the ATmega328P.lib_deps: PlatformIO-managed library dependencies.
Example manual serial port configuration:
[env:nano]
platform = atmelavr
board = nanoatmega328new
upload_port = /dev/tty.usbserial-0001Hardware and behavior settings are defined in include/settings.h. After changing these values, rebuild and upload the firmware.
kScreenAddress: I2C address for the SSD1306 display. Default:0x3C.kButtonPin: button input pin. Default:2.kBuzzerPin: buzzer output pin. Default:3.kBuzzerEnabledByDefault: sets whether the buzzer starts enabled on first boot, before a saved setting exists. Default:true.kBootDebugLogging: enables verbose startup logging over serial for boot diagnostics. Default:false.kModInFeetByDefault: sets whether MOD values default to feet instead of meters on first boot, before a saved setting exists. Default:true.kSerialBaudRate: serial monitor and screenshot capture baud rate. Default:57600.kScreenshotCommand: serial command character used to request a display dump. Default:s.kOledReset: OLED reset pin passed to the display driver. Default:4.kRaSize: moving-average sample window for sensor smoothing. Default:20.kSensorSampleIntervalMs: how often a new sensor sample is added to the moving average. Default:50ms.kMenuEntryHoldSeconds: button hold time before the menu appears. Default:2seconds.kMenuStepIntervalMs: time each menu slot stays active before advancing to the next label or back to the normal screen. Default:1100ms.kStatusScreenMs: how long temporary status screens remain visible. Default:1200ms.kLockScreenMs: lock-screen display duration. Default:5000ms.kSplashScreenMs: startup splash screen duration. Default:1200ms.kAutoPowerOffMs: inactivity time before the analyzer powers itself down. Default:5minutes.kPowerOffWarningSeconds: countdown length of the cancellable auto power-off warning screen. Default:10seconds.kActivityDeltaTenths: O2 change (in tenths of a percent) that counts as activity and defers auto power-off. Default:3(0.3%).kAirCalibrationPercent: oxygen percentage used for air calibration. Default:20.9.kDefaultMinPo2: selected working pO2 on first boot, before a saved setting exists. Default:1.40.kDefaultMaxPo2: default maximum pO2 used in MOD calculations. Default:1.60.kMinValidCalibrationandkMaxValidCalibration: accepted calibration range guardrails.kCalibrationAddressandkSettingsAddress: EEPROM byte offsets of the calibration record and the persisted user-settings record (buzzer, pO2, MOD units). Each record is validated with its own magic byte (kCalibrationMagic,kSettingsMagic).kWakeMarkerAddress: EEPROM byte flagging the next reboot as an auto power-off wake, so the held wake press is not mistaken for a settings reset.kSettingsResetHoldMs: how long the button must be held during power-on to reset the saved settings. Default:1000ms.
Change these only if your hardware wiring, display address, or operating assumptions differ from the current build.
From the repository root:
platformio runplatformio run --target uploadIf PlatformIO does not auto-detect the serial port on your machine, add upload_port to platformio.ini.
platformio device monitor --baud 57600The repository includes tools/oled_capture.py, a small host-side utility that converts a 128x64 Adafruit SSD1306 framebuffer dump into a PNG.
It accepts any of these inputs:
- A raw
1024-byte framebuffer file in Adafruit SSD1306 page order. - An ASCII hex dump containing the same
1024bytes. - A serial frame with the header
OLED_FRAME 128 64followed by1024raw bytes.
The firmware supports live screenshot capture over USB serial. It listens at 57600 baud and sends the current OLED framebuffer when it receives the s command.
Convert a saved raw framebuffer dump into a PNG:
python3 tools/oled_capture.py oled-frame.bin --output oled-screenshot.pngConvert a hex dump into a larger image:
python3 tools/oled_capture.py oled-frame.txt --format hex --scale 6 --output oled-screenshot.pngCapture a live screenshot directly from the analyzer:
python3 -m pip install pyserial
python3 tools/oled_capture.py --serial /dev/tty.usbserial-0001 --output oled-screenshot.pngThe script sends s automatically before waiting for the frame. If you change the firmware command character, pass the new value with --command.
PlatformIO installs these libraries automatically:
- Adafruit GFX Library
- Adafruit SSD1306
- Adafruit ADS1X15
Core Arduino libraries used directly by the firmware:
- Wire
- EEPROM
- Calibration is based on ambient air and uses
20.9%oxygen as the reference point. - Calibration is stored in EEPROM with a validation marker.
- If saved calibration data is missing or invalid, the firmware forces a new calibration during startup.
- The firmware constrains calibration input to a sane range before saving it.
The firmware only uses two small EEPROM records plus a wake-marker byte (calibration at bytes 0-2, user settings at bytes 8-10, wake marker at byte 12), and the records can be refreshed without an erase: hold the button for 1 second during power-on to reset settings, and run CAL from the hold menu to overwrite calibration. For a full wipe, upload a temporary sketch that writes 0xFF to all 1024 bytes, then re-upload the analyzer firmware. Note that avrdude cannot touch EEPROM over plain USB serial because the optiboot bootloader does not support it; direct EEPROM access requires an ISP programmer.
- The splash bitmap in Adafruit SSD1306 is disabled at build time to save flash.
- The small and large UI fonts are subsetted to only the glyphs used by the analyzer screens.
- Display updates are cached so unchanged frames are not redrawn.
- Buzzer, pO2 selection, and MOD unit preferences are written to EEPROM when changed from the hold menu and restored at boot. Holding the button for
1second during power-on resets them to defaults without touching calibration. - Sensor samples are collected every
50ms independently of the200ms display refresh, so the moving average settles in about one second. - The ADS1115 runs in continuous conversion mode, so sensor reads return the latest completed conversion instead of blocking while a new one runs.
- The main loop puts the CPU into idle sleep between timer ticks to reduce battery drain.
- After
5minutes without a button press or a meaningful O2 change, the firmware first shows a beepingAUTO OFFcountdown for10seconds; pressing the button during the countdown cancels the shutdown. If it runs out, the firmware showsAUTO OFF, blanks the display, drops the ADS1115 back to its powered-down single-shot mode, and puts the MCU into power-down sleep. Pressing the button wakes it through a watchdog reset; a wake marker in EEPROM makes the wake boot skip the splash screen and return straight to the live reading, and ensures the held wake press is never mistaken for the hold-to-reset-settings gesture. Note the Nano's onboard regulator and power LED still draw a few milliamps, so the rocker switch remains the true off switch; auto power-off is a failsafe for a forgotten unit. - Current builds are comfortably within ATmega328P limits.
This project sits on top of earlier mechanical and firmware work:
- The enclosure remix is based on Tony Land's Divetech nitrox analyzer housing.
- The firmware originates from Eunjae Im's OLED nitrox analyzer project and has been updated here for current OLED hardware, newer Adafruit libraries, and a smoother VS Code and PlatformIO workflow.
- The Printables project page for this build is OLED modification for Divetech nitrox analyzer.
Background references:
- http://ejlabs.net/arduino-oled-nitrox-analyzer
- https://web.archive.org/web/20240414033725/https://www.divetech.com/post/the-20-nitrox-analyzer
Licensed under GNU GPL v3. See LICENSE.













