We are ScapeCrunch, the place where planted aquarium hobbyists come to build relationships and support each other. When you're tired of doom scrolling, you've found your home here.
I have finally put in use a ph probe for the first time ever. Its an Atlas double junction lab grade probe. Expensive but should be worth it.
I can now dial in CO2 and have much needed analytics (hooked up through home assistant) to see what's going on.
My question is does it matter that much where the probe is placed in the tank? The one in question is 4ft ~70G.
I guess if you had excellent CO2 distribution throughout the tank it wouldn't matter so much.
However I am still running with an inline CO2 diffuser for the moment (which is not a great start I know). The Outflow of the cannister is back left and the ph probe is back right - probably the least amount of flow in the entire tank. The cable on the probe isn't super long so it had to be placed there unless I can get an extension. The only other bit of pertinent information is I have a wave maker basically above it pushing water straight towards the front to create a little surface agitation and to also help distribute the surface layer and the lower layers.
I wonder would moving the wave maker maybe to the left a little and angle it a bit to the right so it bounces of the front and allows a higher chance to distribute CO2 rich water to the back corner?
See below the tank in question. There is no large hardscape obstructing the flow. The probe is basically sitting behind that angel fish and rotala on the right
Sounds like you have it furthest from CO2 output and where your flow is the least. That's where I think I'd try to put it.
All your plant look like they're going crazy, so you're doing something right.
I have finally put in use a ph probe for the first time ever. Its an Atlas double junction lab grade probe. Expensive but should be worth it.
I can now dial in CO2 and have much needed analytics (hooked up through home assistant) to see what's going on.
My question is does it matter that much where the probe is placed in the tank? The one in question is 4ft ~70G.
I guess if you had excellent CO2 distribution throughout the tank it wouldn't matter so much.
However I am still running with an inline CO2 diffuser for the moment (which is not a great start I know). The Outflow of the cannister is back left and the ph probe is back right - probably the least amount of flow in the entire tank. The cable on the probe isn't super long so it had to be placed there unless I can get an extension. The only other bit of pertinent information is I have a wave maker basically above it pushing water straight towards the front to create a little surface agitation and to also help distribute the surface layer and the lower layers.
I wonder would moving the wave maker maybe to the left a little and angle it a bit to the right so it bounces of the front and allows a higher chance to distribute CO2 rich water to the back corner?
See below the tank in question. There is no large hardscape obstructing the flow. The probe is basically sitting behind that angel fish and rotala on the right
I have tried a bunch of spots in my tank, even in the filter lines where it currently lives, and don’t see any difference of stable readings.
If you’re using a ph controller, how close the probe is to the co2 source will change how reactive it is and how quickly the controller switches the solenoid off/on. I put mine inline on the filter inlet, my co2 reactor is on the outlet, so technically as far away as possible. I want mine less reactive, preventing the quick on/off cycles, but I have my needle valve set very conservatively so ph changes are purposely slow.
My question is does it matter that much where the probe is placed in the tank? The one in question is 4ft ~70G.
I guess if you had excellent CO2 distribution throughout the tank it wouldn't matter so much.
Nice tank @Aquajack! I've built my pH monitor myself, but I get more noisy readings when the probe or wire gets too close to electric objects like lights and 230V wiring. I find my device not useable when wave/flow makers are in the tank. But the measurements isolated in a jar are super accurate.
You might try whether your pH probe and wiring are subject to the same issue I'm having.
Thanks all. I guess the main point is not the absolute number but the stability and pattern over time. Sounds like being further away from the CO2 outlet is a good thing.
The ph does rise and fall very slowly and is stable once it reaches equilibrium. I did the full 3 point calibration before dropping it in so I know the probe itself is rock solid. I've only had it running for a day or so. After a couple more days I may turn the wave maker off for a day to see if it makes any difference.
This was my first thought. I think pH probes will always work best with reactors, since 100% of the CO2 injected is dissolving into the water column and has a direct correlation with pH, whereas CO2 mist will always have some level of error in terms of pH since not all of the bubbles are dissolved, yet plants can still use them.
Also, I think it's easier to get even saturation around the tank than with gravity-fighting bubbles, no matter how fine the mist.
I'm sure it'll still work fine, but I'd make sure to place the pH probe somewhere that isn't dead-current, especially with a diffuser.
Nice tank @Aquajack! I've built my pH monitor myself, but I get more noisy readings when the probe or wire gets too close to electric objects like lights and 230V wiring. I find my device not useable when wave/flow makers are in the tank. But the measurements isolated in a jar are super accurate.
You might try whether your pH probe and wiring are subject to the same issue I'm having.
Are you isolating the ph chip? Atlas’ Isolated carrier board makes a huge difference. I have my probe resting on the filter motor, with the sensing end in the 16mm filter line of a Biomaster 850, and get zero noise with only 20 seconds of tau software smoothing. I do also have the board in a corner of my enclosure and ensure the probe lines don’t run parallel to power cables, but I believe the isolation board is doing the heavy lifting
If you aren’t using any software smoothing, even the most isolated and well installed ph probe will get some jumps in readings, it’s the nature of the technology in our acidic low tds environments.
Median of 3, where it rejects odd readings and uses a rolling 3 sample buffer gets rid of the odd spikes. Using an exponential moving average also slows the rise and fall and it’s easy to implement in code, with a changing tau setting in seconds. With experimenting, 20 seconds is about right to get good reactivity speed without overshoot/bounceback behaviour.
Happy to share code if you want to give it a try!
Are you isolating the ph chip? Atlas’ Isolated carrier board makes a huge difference. I have my probe resting on the filter motor, with the sensing end in the 16mm filter line of a Biomaster 850, and get zero noise with only 20 seconds of tau software smoothing. I do also have the board in a corner of my enclosure and ensure the probe lines don’t run parallel to power cables, but I believe the isolation board is doing the heavy lifting
Yes using the isolated carrier board.
The readings themselves are actually really smooth. There is no jumps or anything.
I think i was just expecting a little bigger drop between when the CO2 turns on and about when it hits equilibrium. (Only around 0.6).
I also have a KH 0 > 1 so that may influence the absolute number too.
If you aren’t using any software smoothing, even the most isolated and well installed ph probe will get some jumps in readings, it’s the nature of the technology in our acidic low tds environments.
Median of 3, where it rejects odd readings and uses a rolling 3 sample buffer gets rid of the odd spikes. Using an exponential moving average also slows the rise and fall and it’s easy to implement in code, with a changing tau setting in seconds. With experimenting, 20 seconds is about right to get good reactivity speed without overshoot/bounceback behaviour.
Happy to share code if you want to give it a try!
This was my first thought. I think pH probes will always work best with reactors, since 100% of the CO2 injected is dissolving into the water column and has a direct correlation with pH, whereas CO2 mist will always have some level of error in terms of pH since not all of the bubbles are dissolved, yet plants can still use them.
Also, I think it's easier to get even saturation around the tank than with gravity-fighting bubbles, no matter how fine the mist.
I'm sure it'll still work fine, but I'd make sure to place the pH probe somewhere that isn't dead-current, especially with a diffuser.
Also, the other disadvantage of inline diffusers is the internal membrane does get clogged over time and reduces efficiency. I think this one is overdue for a clean. I took another one out today and the inside of the ceramic cylinder was completely covered in detritus.
See below from the last 24 hours. Ignore the actual number but the line itself is relatively smooth. You can see the exact point when CO2 kicks in.
Here is the pH setup, smoothing and calibration code I use.
I had AI summarise my notes as re-reading them weeks later, they looked like the rantings of a crazy person
Platform: ESP32-S3, Atlas Scientific EZO-pH circuit, I2CContext: planted CO2 tank, 0.5 dKH water, so small CO2 changes move pH fast
Two related pieces from an aquarium controller build: how raw pH readings get smoothed for display and logging without slowing down control, and the two-point calibration wizard.
Part 1: Smoothing
The problem
A pH probe read every few seconds produces two kinds of noise:
Single-sample spikes: electrical transients, a nearby sensor's excitation pulse, a momentary I2C hiccup
Fast jitter: genuine small oscillation around the true value
Logging or displaying this raw makes trends unreadable and records outliers as if they were real chemistry. The fix can't touch anything used for control though, since reacting fast to a genuine change is the point of a CO2 controller.
Architecture: median-of-3, then a time-aware EMA
float phMed3Buf[3] = {0, 0, 0};
int phMed3Idx = 0;
bool phMed3Filled = false;
float phSmoothed = -1;
unsigned long phSmoothLastUpdate = 0;
float phSmoothTauSec = 25.0f; // runtime-adjustable, not compiled in
float median3(float a, float b, float c) {
if (a > b) { float t = a; a = b; b = t; }
if (b > c) { float t = b; b = c; c = t; }
if (a > b) { float t = a; a = b; b = t; }
return b;
}
unsigned long now = millis();
if (phSmoothLastUpdate == 0) {
phSmoothed = medianVal; // seed directly, no crawl up from zero at boot
} else {
float dtSec = (now - phSmoothLastUpdate) / 1000.0f;
float alpha = 1.0f - expf(-dtSec / phSmoothTauSec);
phSmoothed = alpha * medianVal + (1.0f - alpha) * phSmoothed;
}
phSmoothLastUpdate = now;
}
New readings enter a rolling 3-sample buffer. The median of the three is taken, which deletes any single-sample spike outright, an average lets one absurd sample drag the result, a median just outvotes it. The median then feeds an exponential moving average.
Why the EMA is time-aware
A textbook EMA blends a fixed fraction of each new sample into the running average. That's wrong here because this controller polls every 10s in normal operation but every 2s while a calibration screen is open. A fixed blend ratio would smooth completely differently depending on which mode is running.
The blend weight is computed from actual elapsed time instead:
alpha = 1 - exp(-dt / tau)
"25 seconds of smoothing" means 25 seconds of real time regardless of read cadence. Tau is the only tuning knob, in seconds, adjustable at runtime. Roughly: after one tau of sustained change, the smoothed value has closed about 63% of the gap to the new reality, after three taus it's essentially arrived.
Who reads which value
The real design decision, not the math:
Display, web UI, Grafana, daily min/max/avg, permanent log use Smoothed. A spike shouldn't be recorded forever as if it were real chemistry.
CO2 solenoid control uses Raw. Reacting fast to genuine change is the point. CO2 reactor lag already dwarfs the smoothing window, so raw control costs nothing in stability.
Rate-of-change safety alert (regulator dump detection) uses Raw. Smoothing a value watched specifically for sudden change would defeat the alert.
Both raw and smoothed get logged side by side, raw for diagnosing, smoothed for glancing at a graph.
Edge cases
Seed the EMA directly from the first reading, not from zero, avoids a slow crawl up from nothing at boot
Separate sensor types get separate filter state and separate taus rather than shared machinery. pH runs 25s, a TDS sensor on the same board runs 600s (10min) by default, since TDS has no fast physical driver the way CO2 injection gives pH one
Part 2: Two-point calibration wizard
The EZO-pH circuit supports up to 3-point calibration (Cal,low / Cal,mid / Cal,high), but this build only does mid plus low. The tank's operating range (pH 5.2 to 5.8, a consequence of very low KH) sits well below neutral, so a pH 7/pH 4 two-point cal brackets the actual working range better than adding a high point would.
Flow
Probe picker: the build rotates between multiple physical probes, each with its own calibration history stored separately
Temperature probe reminder: confirms the temperature probe is sitting in the same reference solution as the pH probe
Mid capture: Cal,mid,7.00 sent once the reading settles in pH 7 buffer
Mid check: compares this capture's neutral mV offset against the probe's own history
Swap prompt: a deliberate confirmation screen between buffers, so the low point can't get captured before the probe has actually moved to the new buffer
Low capture: Cal,low,4.00
Low check: same drift comparison, against both percent slope and mV history
Final approve or redo: commits to EEPROM and Grafana only on explicit confirm, redo discards and restarts the wizard
Mid point capture
if (oledMode == OLED_CAL_MID) {
if (oledSelectEdge) {
oledSelectEdge = false;
char cmd[24]; snprintf(cmd, sizeof(cmd), "Cal,mid,%.2f", calMidValue);
ezoSendBlocking(cmd, 1000);
ezoCalCooldownUntil = millis() + 3000; // see note below
// The neutral mV offset is real the moment mid is set, since it IS
// the pH 7 measurement. Query it now and judge it against history
// before any more work is sunk into the low point. Acid/base slope
// stay at their uncalibrated values until low exists.
newSlopeNeutralMv = -999;
float dumpA = -1, dumpB = -1;
ezoQuerySlope(dumpA, dumpB, newSlopeNeutralMv);
oledMode = OLED_CAL_MID_CHECK;
}
}
The 3000ms cooldown exists because of a real failure caught once during testing: the write froze right after completing, no error on the debug console, needing a longer power cycle to clear rather than a quick reset. Cal writes commit to the EZO chip's own non-volatile storage, a slower physical process than reading a live value. The initial send only confirms the command was accepted, not that the internal flash write finished. Resuming normal polling immediately risked hitting the chip mid-write. The cooldown makes every subsequent read wait it out.
// Acid% becomes real the moment the second point exists, so query
// the full set now. Nothing is stored yet, only the final approve
// after reinstall commits to EEPROM and Grafana.
newSlopeAcid = newSlopeBase = -1;
ezoQuerySlope(newSlopeAcid, newSlopeBase, newSlopeNeutralMv);
// Third field is the neutral mV offset, parsed defensively: if a
// firmware revision only ever returned two fields, the first two
// still land rather than the whole query failing.
char* comma3 = strchr(comma2 + 1, ',');
if (comma3) neutralMv = atof(comma3 + 1);
return true;
}
Slope,? returns three values, not two: acid%, base%, and neutral mV. Easy to miss reading the datasheet quickly. Base% is meaningless here since no high-point calibration is ever run, so it gets parsed but ignored for diagnosis.
What slope and offset actually tell you about probe health
A healthy pH probe follows the Nernst equation closely: roughly 59.16mV change per pH unit at 25°C, with the reading landing near 0mV at pH 7. Two things degrade independently as a probe ages, and the EZO reports both separately.
Slope (acid%, and base% if you calibrate a high point) is how closely the probe's actual mV-per-pH-unit response matches that ideal 59.16mV, expressed as a percentage of ideal. 95 to 100% is a healthy probe. Below roughly 85% and the glass membrane is tiring, the probe still works but is losing sensitivity, readings get progressively less trustworthy as this number falls.
Neutral mV offset is where the probe reads zero relative to true pH 7. A brand new probe sits close to 0mV. A large offset, tens of millivolts either direction, isn't automatically a fault. A probe can carry a large but stable offset indefinitely and remain perfectly usable, since the two-point calibration corrects for it mathematically either way. What actually matters is stability over time: logging this value at every calibration and comparing against the probe's own history catches a genuinely drifting offset, one that keeps moving further from its own baseline calibration to calibration, which is the real warning sign, distinct from a large offset that stays put.
In short: slope tells you if the probe has lost sensitivity, offset tells you if it's still measuring the same thing consistently. Both need a calibration history per physical probe to actually mean anything, a single reading in isolation only tells you the number, not whether it's the same number as last time.
Built with the DallasTemperature/OneWire library for temp and Atlas's I2C command set for pH. The smoothing architecture in Part 1 is sensor-agnostic.
This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
By continuing to use this site, you are consenting to our use of cookies.