Skip to content

Navigation Menu

Sign in
Sign up

Repository files navigation

APAPHX2_ADS1115

Arduino library for APADevices PHX v2 pool monitoring board
pH and ORP/RX measurement using the ADS1115 16-bit ADC with full isolation, two-point calibration, temperature compensation and non-blocking state machine design.


Contents


What this library does

The APADevices PHX v2 board measures two water quality parameters simultaneously:

Channel Measurement Range Accuracy ADC gain
pH Acid/base balance 0 – 14 pH Β±0.002 pH Gain 2 (Β±2.048V, 62.5ΞΌV LSB)
ORP/RX Oxidation-reduction potential Β±2000 mV Β±0.5 mV Gain 1 (Β±4.096V, 125ΞΌV LSB)

Both probes connect to isolated, precision analog frontends on the PHX v2 board. The ADS1115 16-bit delta-sigma ADC reads each channel differentially β€” the library handles all register configuration, timing, calibration and unit conversion.

Key features:

  • Non-blocking state machine β€” measurement never blocks loop()
  • Two-point calibration with EEPROM persistence β€” survives power cycles
  • Temperature compensation for pH (Pasco 2001 formula, normalised to 25Β°C)
  • Rolling average filter β€” configurable window 1–10 readings
  • Message callback β€” route library messages to Serial, LCD or any display
  • Compile-time + runtime debug system
  • Zero heap allocation after begin() β€” safe on AVR

Hardware overview

The PHX v2 board contains two fully isolated measurement modules on a single PCB:

  • pH module β€” ADS1115 at I2C address 0x49, Gain 2
  • ORP/RX module β€” ADS1115 at I2C address 0x48, Gain 1

Each module has its own ADUM1251 I2C isolator and LMP7721 precision op-amp frontend. Both share the same I2C bus on the MCU side but are electrically isolated from each other and from the MCU on the analog side.

Power domains

The board has two separate power domains that must both be connected:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ MCU side (logic) β”‚ Analog side (isolated) β”‚
β”‚ Connector P1 β”‚ Connector CN2 β”‚
β”‚ β”‚ β”‚
β”‚ Vcc-MC1: 3.3V or 5V β”‚ +12V IN: external 12V DC β”‚
β”‚ GND-MC1: MCU ground β”‚ 12V/GND: 12V ground β”‚
β”‚ SCL, SDA: I2C bus β”‚ β”‚
β”‚ (same voltage as MCU) β”‚ Powers all analog circuits β”‚
β”‚ β”‚ behind ADUM1251 isolators β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

⚠️ Both power connections are required. The board will not function with only one supply connected.

⚠️ The two grounds (GND-MC1 and 12V/GND) are galvanically isolated. Do not connect them together.


Wiring

Connector P1 β€” IDC ×ば぀5 pin header (2.54mm pitch)

×ば぀5 pin header (2.54mm pitch)" href="#connector-p1--idc-25-pin-header-254mm-pitch">
P1 Pin β”‚ Signal β”‚ Connect to
────────┼─────────────┼──────────────────────────────────────
 10 β”‚ GND-MC1 β”‚ MCU GND
 9 β”‚ Vcc-MC1 β”‚ MCU supply (3.3V or 5V β€” match MCU)
 8 β”‚ SCL-MC1 β”‚ MCU I2C SCL
 7 β”‚ SDA-MC1 β”‚ MCU I2C SDA
 6 β”‚ RX-Alert β”‚ MCU pin (optional β€” ALERT/RDY from ORP ADS1115)
 5 β”‚ pH-Alert β”‚ MCU pin (optional β€” ALERT/RDY from pH ADS1115)
 4 β”‚ 12V GND β”‚ 12V supply negative (tied to pin 2)
 3 β”‚ 12V+ β”‚ 12V supply positive (tied to pin 1)
 2 β”‚ 12V GND β”‚ 12V supply negative (tied to pin 4)
 1 β”‚ 12V+ β”‚ 12V supply positive (tied to pin 3)

Pins 1+3 and 2+4 are paired β€” connect both pins of each pair to the 12V supply.

Connector CN2 β€” KF2510 2-pin (alternative 12V connection)

CN2 Pin β”‚ Signal β”‚ Connect to
─────────┼──────────┼──────────────────
 1 β”‚ 12V+ β”‚ 12V supply positive
 2 β”‚ 12V/GND β”‚ 12V supply negative

Use either P1 (pins 1–4) or CN2 for the 12V supply β€” both connect to the same internal rail.

I2C pullup jumper H3

The board includes onboard 4.7kΞ© I2C pullup resistors (R36, R37).
Enable them with jumper H3 if your MCU board does not already have I2C pullups.
Disable H3 if external pullups are present to avoid parallel resistance lowering the pullup value.

Minimal wiring example (Arduino Uno)

Arduino Uno β†’ PHX v2 P1
─────────────────────────
GND β†’ Pin 10 (GND-MC1)
5V β†’ Pin 9 (Vcc-MC1)
A5 (SCL) β†’ Pin 8 (SCL-MC1)
A4 (SDA) β†’ Pin 7 (SDA-MC1)
External PSU β†’ PHX v2 CN2
─────────────────────────
12V+ β†’ Pin 1
GND β†’ Pin 2

H3 jumper: ON (enable onboard pullups, unless your Uno shield has them)


Installation

Arduino IDE (Library Manager)

  1. Open Sketch β†’ Include Library β†’ Manage Libraries
  2. Search for APAPHX2_ADS1115
  3. Click Install

Arduino IDE (manual)

  1. Download the repository as ZIP from GitHub
  2. Open Sketch β†’ Include Library β†’ Add .ZIP Library
  3. Select the downloaded ZIP

PlatformIO

; platformio.ini
lib_deps =
 https://github.com/apadevices/APAPHX2_ADS1115

Quick start

#include "APAPHX2_ADS1115.h"
ADS1115_PHX_PH phSensor(0x49); // pH β€” I2C address 0x49
ADS1115_PHX_RX rxSensor(0x48); // ORP β€” I2C address 0x48
void setup() {
 Serial.begin(115200);
 phSensor.begin();
 rxSensor.begin();
 if (!phSensor.isCalibrated()) {
 Serial.println("pH not calibrated β€” run calibration first");
 }
 if (!rxSensor.isCalibrated()) {
 Serial.println("ORP not calibrated β€” run calibration first");
 }
}
void loop() {
 // Non-blocking measurement
 static PHXConfig cfg;
 static bool cfgReady = false;
 if (!cfgReady) {
 cfg.samples = 10;
 cfg.delay_ms = 5;
 cfg.avg_buffer = 1;
 cfgReady = true;
 }
 phSensor.startReading(cfg);
 while (phSensor.getState() != PHXState::IDLE) {
 phSensor.updateReading();
 }
 rxSensor.startReading(cfg);
 while (rxSensor.getState() != PHXState::IDLE) {
 rxSensor.updateReading();
 }
 Serial.print("pH: ");
 Serial.print(phSensor.getLastReading(), 3);
 Serial.print(" ORP: ");
 Serial.print(rxSensor.getLastReading(), 1);
 Serial.println(" mV");
 delay(2000);
}

Note: begin() automatically loads calibration from EEPROM. If no valid calibration is stored, readings return raw mV until calibration is performed.


Calibration

Calibration is the most important step for accurate measurements. The library uses two-point calibration β€” two known reference solutions are measured and the library maps all subsequent readings between them.

Why calibration matters

Electrochemical probes do not produce a universal voltage for a given pH or ORP value. Each probe has a unique offset and slope that depends on its age, condition and temperature. Without calibration the library returns raw differential mV from the ADC β€” useful for diagnostics but not for chemistry.

What you need

pH calibration:

  • pH 4.0 buffer solution (liquid β€” avoid powder, insufficient precision)
  • pH 7.0 buffer solution (liquid)
  • Distilled or deionised water for rinsing

ORP/RX calibration:

  • 475 mV ORP reference solution
  • 650 mV ORP reference solution

Any commercially available liquid standard buffer solution works. No specific brand is required.

Calibration process β€” guided API (recommended)

The library provides a simple two-step guided calibration. Each step blocks for approximately 200 seconds while the probe equilibrates in the buffer solution β€” this wait is mandatory for accurate results.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Calibration timeline (per point) β”‚
β”‚ β”‚
β”‚ Place probe β†’ Press enter β†’ [200s soak] β†’ [stable check] β”‚
β”‚ ↑ β”‚
β”‚ "Cal: wait 200s..." β”‚ probe equilibrates β”‚
β”‚ ↓ β”‚
β”‚ "Calibration: stable!" β”‚
β”‚ Result stored in RAM β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
#include "APAPHX2_ADS1115.h"
ADS1115_PHX_PH phSensor(0x49);
// Route library messages to Serial
void onMessage(const __FlashStringHelper* m) { Serial.println(m); }
void setup() {
 Serial.begin(115200);
 phSensor.begin();
 phSensor.setMessageCallback(onMessage);
 // ── STEP 1: First buffer solution ───────────────────────
 Serial.println("Rinse probe with distilled water.");
 Serial.println("Place probe in pH 4.0 buffer. Press ENTER when ready.");
 waitForEnter();
 float mV1 = phSensor.calibratePoint1(4.0f);
 Serial.print("Point 1 captured: "); Serial.print(mV1, 3);
 Serial.println(" mV");
 // ── STEP 2: Second buffer solution ──────────────────────
 Serial.println("Rinse probe with distilled water.");
 Serial.println("Place probe in pH 7.0 buffer. Press ENTER when ready.");
 waitForEnter();
 bool ok = phSensor.calibratePoint2(7.0f);
 if (ok) {
 phSensor.saveCalibration(); // persist to EEPROM
 Serial.println("Calibration saved.");
 } else {
 Serial.println("Calibration failed β€” check buffer solutions.");
 }
}
void waitForEnter() {
 while (Serial.available()) Serial.read();
 while (!Serial.available()) { ; }
 while (Serial.available()) Serial.read();
}
void loop() { }

Calibration process β€” advanced API

For custom workflows, each step can be called individually:

// Capture stable mV reading (blocking β€” ~200s minimum)
float mV = phSensor.calibratePHXReading();
// Build calibration struct manually
PHX_Calibration cal;
cal.ref1_mV = mV_at_buffer1;
cal.ref1_value = 4.0f;
cal.ref2_mV = mV_at_buffer2;
cal.ref2_value = 7.0f;
// Validate and store to RAM
bool ok = phSensor.calibratePHX(cal);
// Persist to EEPROM (call explicitly β€” not automatic)
if (ok) phSensor.saveCalibration();

Important calibration notes

  • The 200-second soak is mandatory. pH electrodes need 60–180 seconds to equilibrate in a new buffer solution. Starting the stability check too early produces a wrong calibration that looks valid but gives incorrect readings.
  • Calibration is NOT auto-saved. After calibratePoint2() succeeds, you must call saveCalibration() explicitly. This is by design β€” it lets you verify the calibration before committing to EEPROM.
  • Auto-load on power-up. begin() automatically loads the last saved calibration from EEPROM. If no valid calibration is found, isCalibrated() returns false and readings return raw mV.
  • Recalibrate after sensor change. Any probe replacement requires fresh calibration. The saved EEPROM data is specific to each probe.
  • ORP calibration follows the same process β€” use ADS1115_PHX_RX instance with 475 mV and 650 mV reference solutions.

Calibration status check

phSensor.begin();
if (phSensor.isCalibrated()) {
 PHX_Calibration cal = phSensor.getCalibration();
 Serial.print("ref1: "); Serial.print(cal.ref1_mV);
 Serial.print(" mV β†’ "); Serial.println(cal.ref1_value);
 Serial.print("ref2: "); Serial.print(cal.ref2_mV);
 Serial.print(" mV β†’ "); Serial.println(cal.ref2_value);
} else {
 Serial.println("No calibration found.");
}

EEPROM layout

Calibration data is stored at fixed addresses:

Instance Base address Occupies
pH (ADS1115_PHX_PH) 128 128–144
ORP (ADS1115_PHX_RX) 161 161–177

Custom base addresses can be set via the constructor. Minimum EEPROM size required: 178 bytes (all AVR devices with EEPROM satisfy this).


API reference

Constructors

// pH sensor β€” default address 0x49, EEPROM base 128
ADS1115_PHX_PH phSensor(0x49);
// ORP/RX sensor β€” default address 0x48, EEPROM base 161
ADS1115_PHX_RX rxSensor(0x48);
// With ALERT pin
ADS1115_PHX_PH phSensor(0x49, A0);
// With custom EEPROM base (advanced β€” multiple boards)
ADS1115_PHX_PH phSensor(0x49, ADS1115_PHX::NO_ALERT, 200);

Initialisation

sensor.begin(); // init I2C, load calibration from EEPROM
sensor.begin(false); // skip Wire.begin() β€” if Wire already started

Non-blocking state machine

PHXConfig cfg;
cfg.samples = 10; // ADC samples per cycle (1–25)
cfg.delay_ms = 5; // pause between samples in ms (0 = immediate)
cfg.avg_buffer = 1; // rolling average window (1 = off, 2–10 = active)
sensor.startReading(cfg); // begin cycle
sensor.updateReading(); // call repeatedly in loop()
sensor.cancelReading(); // abort current cycle
PHXState s = sensor.getState(); // IDLE / COLLECTING / PROCESSING
bool done = sensor.isReadingComplete();
float val = sensor.getLastReading(); // calibrated pH or ORP mV
float raw = sensor.getLastRawMV(); // raw differential mV (pre-calibration)

Calibration

// Guided β€” recommended
float mV = sensor.calibratePoint1(4.0f); // step 1: first buffer
bool ok = sensor.calibratePoint2(7.0f); // step 2: second buffer + finalise
// Low-level
float mV = sensor.calibratePHXReading(); // blocking stable reading
bool ok = sensor.calibratePHX(cal); // validate + store to RAM
bool ok = sensor.saveCalibration(); // write to EEPROM
bool ok = sensor.loadCalibration(); // read from EEPROM
// Status
bool cal = sensor.isCalibrated();
PHX_Calibration cal = sensor.getCalibration();

Temperature compensation (pH only)

sensor.enableTemperatureCompensation(true);
sensor.setTemperature(25.0f); // Β°C, valid range 0–50Β°C
float t = sensor.getCurrentTemperature();
bool e = sensor.isTemperatureCompensationEnabled();

Rolling average

sensor.setRollingAverage(5); // window size 2–10 (1 = off)
sensor.clearRollingAverage(); // reset ring, keep window
bool ready = sensor.isRollingAverageReady(); // true once ring is full

ADC configuration

sensor.setGain(ADS1115_GAIN_1); // default: GAIN_2 for pH, GAIN_1 for ORP
sensor.setDataRate(ADS1115_DR_128); // default: 128 SPS (~7.8ms/conversion)
int16_t raw = sensor.readADC(); // single blocking read
float range = sensor.getVoltageRange(); // Β±V for current gain

Errors

PHXError e = sensor.getLastError();
// PHXError::NONE / PH_LOW / PH_HIGH / RX_LOW / RX_HIGH
// TEMP_INVALID / CALIB_INVALID

Debug system

The debug system is compile-time gated β€” it adds zero overhead when disabled.

Enable debug output

Uncomment the #define in APAPHX2_ADS1115.h:

// In APAPHX2_ADS1115.h:
#define ADS1115_DEBUG // <-- uncomment to enable debug output

Then enable at runtime and optionally redirect the output stream:

phSensor.enableDebug(true);
phSensor.setDebugStream(Serial); // default β€” any Stream works
 // e.g. Serial1, SoftwareSerial

When enabled, the library prints detailed diagnostics for every operation β€” I2C register writes, sample-by-sample ADC values, calibration windows, EEPROM operations and rolling average state. This is intended for Serial Monitor use during development.

Note: Debug output is Stream-based (Serial, UART). It is verbose and not suitable for LCD display. For user-facing messages on LCD, use setMessageCallback() instead.

Note: #define ADS1115_DEBUG must be set at compile time. The runtime enableDebug() toggle only works when the define is active.


Message callback

The library emits three short user-facing messages during calibration. By default the library is completely silent. Register a callback to receive these messages and route them to any output device.

All messages are ≀ 20 characters β€” designed to fit one row of a ×ば぀4 LCD.

Message When
"Cal: wait 200s..." Probe soak started
"Calibration: stable!" Stable reading captured
"Cal: timeout-check! " Stability timeout β€” check probe

Serial example

void onMessage(const __FlashStringHelper* m) {
 Serial.println(m);
}
phSensor.setMessageCallback(onMessage);
rxSensor.setMessageCallback(onMessage);

LCD example (LiquidCrystal_I2C)

LiquidCrystal_I2C lcd(0x27, 20, 4);
void onMessage(const __FlashStringHelper* m) {
 lcd.setCursor(0, 3); // bottom row
 lcd.print(F(" ")); // clear row
 lcd.setCursor(0, 3);
 lcd.print(m);
}
phSensor.setMessageCallback(onMessage);

Important notes

  • The callback receives library status messages only β€” calibration progress, stable confirmation, timeout warning.
  • User interaction prompts ("place probe in buffer", "press button") are always the responsibility of your sketch, not the library. The library has no knowledge of your input method (Serial, button, touchscreen).
  • If setMessageCallback() is never called, the library is completely silent β€” no Serial.begin() required.

Platform notes

AVR (Arduino Uno, Nano, Mega)

Fully supported. Zero heap allocation after begin(). Tested on Arduino Mega 2560.

Minimum recommended board: Arduino Uno / Nano (32KB flash, 2KB SRAM).
The library uses ~500 bytes SRAM at runtime (fixed buffers, no heap).

#include <Wire.h>
#include "APAPHX2_ADS1115.h"
// Wire runs at 100kHz by default on AVR β€” no changes needed

ESP8266 / ESP32

Supported. The calibration soak loop includes yield() calls to feed the watchdog.

#include <Wire.h>
#include "APAPHX2_ADS1115.h"
// Wire.begin(SDA_PIN, SCL_PIN) if using non-default pins

STM32 (STM32duino core)

Supported with STM32duino Arduino core.

#include <Wire.h>
#include "APAPHX2_ADS1115.h"
// Wire.setSDA(PB7); Wire.setSCL(PB6); // if needed for your board
// Wire.begin();

I2C speed

The ADUM1251 isolator supports both Standard-mode (100kHz) and Fast-mode (400kHz).
The library defaults to 100kHz which is sufficient for all PHX measurement needs.
To enable 400kHz:

Wire.begin();
Wire.setClock(400000);
phSensor.begin(false); // false = skip Wire.begin() inside library
rxSensor.begin(false);

Examples

# Name Class What it demonstrates
01 BasicReading Simple readADC(), begin(), direct blocking read
02 StateMachine Simple Non-blocking startReading() / updateReading()
03 DualSensor Simple pH + ORP together, continuous loop pattern
04 Calibration Middle Full guided calibration with Serial prompts
05 TemperatureComp Middle Temperature compensation for pH
06 RollingAverage Middle Rolling average filter, warm-up, window sizing
07 DebugOutput Advanced Debug system, setDebugStream(), runtime toggle
08 LCDDisplay Advanced setMessageCallback() with LCD ×ば぀4
09 PoolMonitor Advanced Complete pool automation sketch

All examples are in the examples/ folder, organised by class:

examples/
 simple/
 01_BasicReading/
 02_StateMachine/
 03_DualSensor/
 middle/
 04_Calibration/
 05_TemperatureComp/
 06_RollingAverage/
 advanced/
 07_DebugOutput/
 08_LCDDisplay/
 09_PoolMonitor/

Board quick spec

Parameter Value
pH ADC ADS1115, 16-bit, I2C 0x49
ORP ADC ADS1115, 16-bit, I2C 0x48
pH frontend LMP7721 precision op-amp
ORP frontend LMP7721 precision op-amp
Isolation ADUM1251 I2C isolator (per channel)
MCU supply 3.3V or 5V (match MCU logic level)
Analog supply 12V DC external
Max analog supply 16V (PolyPTC fuse rated)
I2C speed 100kHz standard, 400kHz fast-mode supported
pH gain Gain 2 (Β±2.048V), LSB = 62.5ΞΌV
ORP gain Gain 1 (Β±4.096V), LSB = 125ΞΌV
pH resolution ~947 counts/pH unit
ORP resolution ~8 counts/mV
pH accuracy Β±0.002 pH (calibrated, probe settled)
ORP accuracy Β±0.5 mV (calibrated, probe settled)
Electrode slope check 40–70 mV/pH accepted (>95% Nernst = excellent)
EEPROM usage 17 bytes per sensor (pH: 128–144, ORP: 161–177)
Onboard I2C pullups 4.7kΞ© ×ば぀ 2, switchable via H3 jumper
Reverse polarity protection Schottky diode on 12V rail
Overcurrent protection PolyPTC fuse 500mA
Transient protection TVS 15V/24.4V on 12V rail

License

MIT License β€” see LICENSE file.

Copyright Β© 2025 APADevices [@kecup]

About

Arduino library for APADevices PHX v2 pool monitoring board. pH and ORP measurement via ADS1115 16-bit ADC with full galvanic isolation, guided two-point calibration, EEPROM persistence, temperature compensation, non-blocking state machine and rolling average filter. Supports AVR, ESP8266, ESP32 and STM32.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /