Three probes, one XIAO: the turtle-tank water monitor, from pin map to calibration
The three-sensor water monitor from Smart Turtle Tank in one page: the six XIAO ESP32-C3 pins, the firmware that reads a DS18B20, a TDS probe and a turbidity probe without lying to you, why the maths lives on the server, and the one-time calibration routine.

A turtle tank changes slowly, and the three things that go wrong with it (water too cold, water getting dirty, water going cloudy) are all things a cheap probe can turn into a number. The build in Smart Turtle Tank uses a Seeed XIAO ESP32-C3 and three sensors: a DS18B20 waterproof temperature probe, a Keyestudio TDS probe and a Keyestudio turbidity probe. This post is the whole build on one page, the way the three new video tutorials walk through it: pin map, firmware, server maths, calibration.
The six pins
Gear mentioned (As an Amazon Associate I earn from qualifying purchases. Affiliate links.) Seeed Studio XIAO ESP32C3 · BOJACK DS18B20 waterproof temperature sensor module kit · KEYESTUDIO TDS Meter Probe V1 sensor module · KEYESTUDIO Turbidity Sensor Module V1 · HiLetgo DC 0-25V voltage sensor module · REXQualis 400-point solderless breadboards · ELEGOO 120 pcs Dupont jumper wires · All the gear the books recommend
The XIAO has one GND pin and one 3V3 pin, and four boards need each of them, so the breadboard in the parts list is a power strip and nothing more: a black jumper from GND into the minus rail, an orange jumper from 3V3 into the plus rail, and a strip of tape by the plus rail that says "3V3, not 5V".
| XIAO pin | GPIO | Connects to | Device |
|---|---|---|---|
| 3V3 | - | + line, then VCC / + | Temp adapter, TDS board |
| GND | - | - line, then all grounds | Temp, TDS, turbidity, divider |
| 5V | - | V | Turbidity board only |
| D4 | GPIO6 | DAT | Temp adapter |
| D1 | GPIO3 (analog) | A | TDS board |
| D2 | GPIO4 (analog) | S (output) | Voltage divider |
The rule behind the table: a sensor's power can come from 5V if it needs it, but anything that goes back into a D pin must be at or under 3.3 V. The temperature adapter and the TDS board run on 3V3 and stay under the limit on their own. The turbidity board needs 5 V and outputs up to about 4.5 V, which is why its S pin goes into a divide-by-5 module and the module's output goes to D2. That one wire gets its own post and its own video; the short version is "never straight into the XIAO".
Keep the USB-C cable unplugged the whole time you wire. It goes in last.
The firmware, in the pieces you will change
A PlatformIO project for the XIAO needs three libraries and two build flags:
[env:xiao_c3]
platform = espressif32
board = seeed_xiao_esp32c3
framework = arduino
monitor_speed = 115200
build_flags =
-DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1
lib_deps =
paulstoffregen/OneWire
milesburton/DallasTemperature
bblanchon/ArduinoJson@^7
The board name picks the pin names; with a different board, D4 means a different pin. The two build flags make Serial work over the XIAO's own USB-C port.
The pin map in code matches the table:
#define TEMP_PIN D4 // GPIO6, DS18B20 DAT (adapter has the pull-up)
#define TDS_PIN D1 // GPIO3, ADC1
#define TURB_PIN D2 // GPIO4, ADC1, through the /5 divider module
#define TURB_RATIO 5.0f // 2.0f if you used the two-resistor divider
Both analog pins are on ADC1 on purpose: on an ESP32, ADC2 stops working while Wi-Fi is on, and ADC1 does not. Read them with analogReadMilliVolts(), which applies the chip's factory calibration and returns millivolts.
The reading loop takes the two analog probes as a median of 32 reads, not an average:
static float medianMv(int pin) { // 32 reads, keep the median
uint16_t v[32];
for (int i = 0; i < 32; i++) { v[i] = analogReadMilliVolts(pin); delay(10); }
std::sort(v, v + 32);
return (v[15] + v[16]) / 2.0f;
}
The filter pump and the lamp's timer put spikes on the water and on the supply. Two spikes in 32 reads pull an average up by a lot and the median not at all.
The temperature probe needs a different kind of care. The Dallas library returns -127 when the probe is not connected and 85.0 at power-on before the first conversion finishes. Both are "no reading", not readings:
static void sampleOnce() {
temp.requestTemperatures(); // ~750 ms at 12-bit
float t = temp.getTempCByIndex(0);
if (t > -100 && t != 85.0f) { accT += t; nT++; } // drop -127 and 85
accTds += medianMv(TDS_PIN) / 1000.0f;
accTurb += medianMv(TURB_PIN) / 1000.0f * TURB_RATIO;
nA++;
}
Read one probe at a time. The TDS and turbidity probes both put a small voltage into the same water, and reading them together can make the TDS number jumpy.
Every 30 seconds the board samples; every five minutes it averages the good samples into one reading and sends it as JSON over HTTPS, with raw volts for TDS and turbidity and degrees C for temperature. A reading that could not be sent stays in a RAM ring buffer of 96 readings and goes out, oldest first, when the network is back.
Why the maths is on the server
The firmware never turns volts into ppm or into a clarity percentage. The server does, with calibration constants stored as settings. A recalibration is then a settings change, not a reflash, and every old reading can be reinterpreted because the raw volts were stored too.
TDS, temperature-compensated, is the formula the Keyestudio and DFRobot boards document:
coeff = 1 + 0.02 * (T - 25)
Vc = V / coeff
ppm = (133.42*Vc^3 - 255.86*Vc^2 + 857.39*Vc) * 0.5 * K
Turbidity is reported as a clarity percentage rather than NTU, because the cheap probe is not accurate enough for real NTU. It is a straight line between two points you measure yourself:
sensorV = adcV * 5 (the divider ratio)
clarity % = 100 * (V - V_dirty) / (V_clear - V_dirty), clamped to 0-100
Both TDS and clarity are trend meters. The app labels them that way, and a liquid test kit is still the answer when either of them moves.
Calibrating once
Write the numbers down somewhere that survives; they are settings on the server and nowhere else.
- Temperature: the DS18B20 and a reference aquarium thermometer in the same cup of water for five minutes. The difference is
temp_offset_c. Expect a few tenths of a degree; the probe is rated +/-0.5 C. - TDS: a cup of tank water, the probe in it, a handheld TDS pen in it. Set K so the server's ppm matches the pen, or use a 342 ppm calibration solution.
- Turbidity: a dark cup of clean tap water gives V_clear. The same cup with a pinch of soil stirred in gives V_dirty. The probe reads higher in clear water, so V_clear is the bigger number.
- Baseline: with the tank freshly changed, press Water changed in the app. That stores the current TDS as the baseline the alerts work from, because an absolute TDS number depends on your tap water and what matters is how far it has climbed since the last change.
Set the send interval to one minute from the server while you do this, then back to five.
Gear mentioned (As an Amazon Associate I earn from qualifying purchases. Affiliate links.) Stick-on aquarium thermometer · Liquid test kit for ammonia, nitrite and nitrate · All the gear the books recommend
The readings that mean a wiring mistake
- Temperature shows null, -127 or 85: a probe wire is loose on DAT, VCC or GND. Re-seat them and check the adapter is on 3V3. 85 is the power-on value, which means the probe never got a successful read.
- TDS reads 0 in the tank, or jumps around: the probe cable is unplugged, or both probes are being read together. Read one at a time.
- Turbidity near 0 V, or reading over 3.3 V at D2: is the switch on A? Is S going into the divider's VCC terminal? Are the divider's minus pin and GND terminal on the rail? A switch left on D gives a flat reading no matter what the water does.
- Nothing arrives at the server: serial monitor first. Is Wi-Fi up, is NTP time set? A 401 is the wrong key, a 400 is bad JSON.
- Clarity drifts every time the lamp switches: the probe is in the lamp's beam. Move the carrier or shade the probe.
Checklist
- Power strip: GND to the minus rail, 3V3 to the plus rail, tape that says "3V3, not 5V".
- Temperature: probe wires into the adapter by letter; VCC and GND from the rails; DAT to D4.
- TDS: cable to the rails (red to 3V3); A to D1; probe plugged in.
- Turbidity: switch on A; V from the XIAO's 5V pin; G to the minus rail; S into the divider's VCC terminal; divider GND terminal and minus pin to the minus rail; divider S pin to D2; plus pin empty.
- Serial-only sketch first: temperature sensible, TDS near zero in air, turbidity higher in clear water.
- Then the real firmware, then the four calibration steps, then Water changed.
Watch the series
The first video (the temperature probe) is embedded above. The TDS probe video is at https://youtube.com/shorts/Z3tRstZTqas and the turbidity probe video (the 5V trap and the divider) is at https://youtube.com/shorts/4EJy9Svk83Q.


