Full arduino codebase ultrasonic counts (without flume-base) with signal-processing for better fish/bubble separation for counting catfish in biofloc environment where pond is 1 meter deep and aeration is 10 liters/minute and floc is 50ml/liter.
/*
Open‐Pond Ultrasonic Catfish Counter
———————————–
Counts catfish echoes in a 1 m–deep biofloc pond (~50 mL/L solids) under
10 L/min aeration without using a flume. Implements:
• Automatic baseline calibration (pond bottom distance)
• Burst of PING_COUNT pings → median‐filter gating
• Fish/gas‐bubble separation via distance gating & median filter
• DROP_CM below bottom → fish event
• DEBOUNCE_MS to avoid double counts
• Periodic re‐baseline every RECALIBRATE_INTERVAL_S
• Logs “Fish #n @ t.s” over Serial (swap-in SD/MQTT easily)
Hardware (HC-SR04):
TRIG → D7
ECHO → D6
VCC → 5 V
GND → GND
*/
#include <Arduino.h>
// ── USER CONFIG ──────────────────────────────────────────────────────────────
// pins
const uint8_t PIN_TRIG = 7;
const uint8_t PIN_ECHO = 6;
// signal‐processing parameters
const uint8_t MAX_BASELINE_SAMPLES = 30; // samples to calibrate bottom
const uint8_t PING_COUNT = 7; // pings per detection burst
const float DROP_CM = 8.0; // drop from bottom → fish
const float MIN_DIST_CM = 20.0; // ignore echoes closer than this
const float MAX_DIST_CM = 90.0; // ignore echoes deeper than this
const uint16_t DEBOUNCE_MS = 300; // lockout after a detection
const uint32_t RECALIBRATE_INTERVAL_S = 600; // re‐baseline every 10 min
// runtime state
float bottomDist = 0.0; // calibrated pond‐bottom distance (cm)
float detectThresh = 0.0; // bottomDist – DROP_CM
uint32_t fishCount = 0; // total fish counted
uint32_t lastFishTS = 0; // timestamp of last detection
uint32_t lastRecalTS = 0; // timestamp of last re‐baseline
void setup() {
Serial.begin(115200);
pinMode(PIN_TRIG, OUTPUT);
pinMode(PIN_ECHO, INPUT);
Serial.println(F(“\n⟳ Calibrating bottom baseline…”));
bottomDist = calibrateBaseline();
detectThresh = bottomDist – DROP_CM;
lastRecalTS = millis();
Serial.printf(“BottomDist=%.1f cm → fish if < %.1f cm\n▶ Start counting\n\n”,
bottomDist, detectThresh);
}
void loop() {
uint32_t now = millis();
// 1) Periodic re‐baseline
if (now – lastRecalTS >= RECALIBRATE_INTERVAL_S * 1000UL) {
Serial.println(F(“⟳ Recalibrating baseline…”));
bottomDist = calibrateBaseline();
detectThresh = bottomDist – DROP_CM;
lastRecalTS = now;
Serial.printf(“New BottomDist=%.1f cm thresh<%.1f cm\n\n”,
bottomDist, detectThresh);
}
// 2) Burst of ultrasonic pings
float readings[PING_COUNT];
for (uint8_t i = 0; i < PING_COUNT; i++) {
readings[i] = measureDistance();
delay(20); // ~50 Hz burst to outrun bubble motion
}
// 3) Median‐filter to reject transient bubble/floc echoes
float med = medianFilter(readings, PING_COUNT);
// 4) Distance gating + fish detection + debounce
if (med > MIN_DIST_CM && med < detectThresh
&& (now – lastFishTS > DEBOUNCE_MS)) {
fishCount++;
lastFishTS = now;
Serial.printf(“🐟 Fish #%lu @ %.2f s\n”,
fishCount, now / 1000.0f);
}
delay(100);
}
// ── UTILITIES ────────────────────────────────────────────────────────────────
// send one HC-SR04 ping; return cm or -1 on timeout
float measureDistance() {
digitalWrite(PIN_TRIG, LOW);
delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(PIN_TRIG, LOW);
long us = pulseIn(PIN_ECHO, HIGH, 30000); // 30 ms timeout
if (us == 0) return -1.0;
return (us / 2.0) / 29.1; // µs→cm
}
// average MAX_BASELINE_SAMPLES valid pings for bottomDist
float calibrateBaseline() {
float sum = 0;
uint8_t cnt = 0;
while (cnt < MAX_BASELINE_SAMPLES) {
float d = measureDistance();
if (d > 0) {
sum += d;
cnt++;
}
delay(100);
}
return sum / cnt;
}
// simple insertion sort → return median element
float medianFilter(float *arr, uint8_t n) {
for (uint8_t i = 1; i < n; i++) {
float key = arr[i];
int8_t j = i – 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j–;
}
arr[j + 1] = key;
}
return arr[n / 2];
}
Signal-Processing Tip
- Fire a burst of 7 pings at ~50 Hz and take the median: this rejects one-off high/low spikes from fast-rising bubbles or dense floc clumps.
- Only count echoes between MINDISTCM and detectThresh to ignore surface/bottom echoes.
- Periodically re-baseline (every 10 min) to track slow drift in temperature or floc buildup, keeping your DROP_CM threshold accurate under heavy aeration and 50 mL/L solids.
/*
Open-Pond Ultrasonic Catfish Counter
———————————–
Counts catfish echoes in a 1 m-deep biofloc pond (~50 mL/L solids)
under 10 L/min aeration—no flume required.
• Auto-calibrates bottom distance (MAX_BASELINE_SAMPLES)
• Fires a burst of PING_COUNT pings → median-filter gating
• Only counts echoes between MIN_DIST_CM and (bottomDist-DROP_CM)
• DEBOUNCE_MS locks out after each detection
• Periodic re-baseline every RECALIBRATE_INTERVAL_S
• Logs “Fish #n @ t.s” on Serial (swap for SD/MQTT as needed)
HC-SR04 wiring:
TRIG → D7
ECHO → D6
VCC → 5 V
GND → GND
*/
#include <Arduino.h>
// ── USER CONFIG ──────────────────────────────────────────────────────────────
const uint8_t PIN_TRIG = 7; // HC-SR04 TRIG pin
const uint8_t PIN_ECHO = 6; // HC-SR04 ECHO pin
const uint8_t MAX_BASELINE_SAMPLES = 30; // samples for bottom calibration
const uint8_t PING_COUNT = 7; // pings per detection burst
const float DROP_CM = 8.0; // drop below bottom → fish event
const float MIN_DIST_CM = 20.0; // ignore echoes closer than this (cm)
const float MAX_DIST_CM = 95.0; // ignore echoes deeper than this (cm)
const uint16_t DEBOUNCE_MS = 300; // ms lockout per fish
const uint32_t RECALIBRATE_INTERVAL_S = 600; // seconds between re‐baselines
// ── RUNTIME STATE ────────────────────────────────────────────────────────────
float bottomDist = 0.0; // calibrated pond bottom distance (cm)
float detectThresh = 0.0; // bottomDist − DROP_CM
uint32_t fishCount = 0; // total fish counted
uint32_t lastFishTS = 0; // millis() of last count
uint32_t lastRecalTS = 0; // millis() of last baseline
// ── SETUP ────────────────────────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
pinMode(PIN_TRIG, OUTPUT);
pinMode(PIN_ECHO, INPUT);
Serial.println(F(“\n⟳ Calibrating bottom baseline…”));
bottomDist = calibrateBaseline();
detectThresh = bottomDist – DROP_CM;
lastRecalTS = millis();
Serial.printf(“BottomDist=%.1f cm → count if < %.1f cm\n▶ Counting started\n\n”,
bottomDist, detectThresh);
}
// ── MAIN LOOP ────────────────────────────────────────────────────────────────
void loop() {
uint32_t now = millis();
// 1) Periodic bottom re-calibration
if (now – lastRecalTS >= RECALIBRATE_INTERVAL_S * 1000UL) {
Serial.println(F(“⟳ Re-calibrating bottom baseline…”));
bottomDist = calibrateBaseline();
detectThresh = bottomDist – DROP_CM;
lastRecalTS = now;
Serial.printf(“New BottomDist=%.1f cm → thresh<%.1f cm\n\n”,
bottomDist, detectThresh);
}
// 2) Burst of ultrasonic pings
float readings[PING_COUNT];
for (uint8_t i = 0; i < PING_COUNT; i++) {
readings[i] = measureDistance();
delay(20); // ≈50 Hz to outrun bubble motion
}
// 3) Median-filter gating
float med = medianFilter(readings, PING_COUNT);
// 4) Fish detection + debounce + distance gating
if (med > MIN_DIST_CM && med < detectThresh &&
(now – lastFishTS > DEBOUNCE_MS)) {
fishCount++;
lastFishTS = now;
Serial.printf(“ Fish #%lu @ %.2f s\n”,
fishCount, now / 1000.0f);
}
delay(100);
}
// ── UTILITY FUNCTIONS ─────────────────────────────────────────────────────────
// Send one HC-SR04 ping → return cm or –1 on timeout
float measureDistance() {
digitalWrite(PIN_TRIG, LOW);
delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(PIN_TRIG, LOW);
long duration = pulseIn(PIN_ECHO, HIGH, 30000); // 30 ms timeout
if (duration == 0) return -1.0;
return (duration / 2.0) / 29.1; // µs → cm
}
// Calibrate bottom distance: average MAX_BASELINE_SAMPLES valid readings
float calibrateBaseline() {
float sum = 0;
uint8_t cnt = 0;
while (cnt < MAX_BASELINE_SAMPLES) {
float d = measureDistance();
if (d > 0 && d < MAX_DIST_CM) {
sum += d;
cnt++;
}
delay(100);
}
return sum / cnt;
}
// Simple insertion sort + return middle element as median
float medianFilter(float *arr, uint8_t n) {
for (uint8_t i = 1; i < n; i++) {
float key = arr[i];
int8_t j = i – 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j–;
}
arr[j + 1] = key;
}
return arr[n / 2];
}
Signal-Processing Tip
- Fire a burst of 7 pings at ~50 Hz and take the median: this rejects one-off spikes from rising bubbles or dense floc.
- Gate out echoes closer than MINDISTCM (surface bubbles) and deeper than detectThresh (pond bottom), so only fish returns remain.
- Re-baseline every 10 min to track slow drifts in temperature or floc buildup, keeping your DROP_CM threshold accurate under 10 L/min aeration.
Here’s a turnkey Arduino sketch for counting catfish passing single-file through a 6–8 cm flume, optimized for a 1 m-deep biofloc pond (≈50 mL/L solids) with 10 L/min aeration. It:
- Calibrates a dynamic baseline to the opposite wall every 10 min
- Fires bursts of ultrasonic pings (you’ll lose many bubble echoes)
- Uses a median filter across each burst to reject transient floc/bubble spikes
- Flags a fish when the median drops by DROP_CM
- Debounces to ensure one count per fish
- Logs “Fish #n @ t s” over Serial (swap in SD or MQTT as needed)
/*
Biofloc Ultrasonic Catfish Counter
Pond depth: 1 m | Biofloc: ~50 mL/L | Aeration: 10 L/min
Hardware:
• HC-SR04 ultrasonic sensor (2–400 cm range)
• Mounted across a 6–8 cm PVC/acrylic flume
• TRIG → D7, ECHO → D6, VCC → 5 V, GND → GND
• (Optional bubble-skirt 3D-printed around transducer)
Features:
• Auto baseline calibration (MAX_READS samples)
• Burst of PINGS readings → median filter for bubble/floc rejection
• DROP_CM below baseline → fish event
• DEBOUNCE_MS to avoid double-counts
• Re-calibrate baseline every RECALIBRATE_S seconds
*/
#include <Arduino.h>
// USER CONFIGURATION
const uint8_t PIN_TRIG = 7;
const uint8_t PIN_ECHO = 6;
const uint8_t MAX_READS = 30; // samples for baseline
const uint8_t PINGS = 7; // pings per detection burst
const float DROP_CM = 6.0; // cm drop = fish event
const uint16_t DEBOUNCE_MS = 250; // ms ignore window
const uint32_t RECALIBRATE_S = 600; // seconds between auto-recalibrations
// RUNTIME STATE
float baselineDist = 0.0;
float detectThresh = 0.0;
uint32_t fishCount = 0;
uint32_t lastFishTS = 0;
uint32_t lastRecalTS = 0;
void setup() {
Serial.begin(115200);
pinMode(PIN_TRIG, OUTPUT);
pinMode(PIN_ECHO, INPUT);
Serial.println(F(“\n⟳ Calibrating baseline…”));
baselineDist = calibrateBaseline();
detectThresh = baselineDist – DROP_CM;
lastRecalTS = millis();
Serial.printf(“Baseline = %.1f cm → detect if < %.1f cm\n”,
baselineDist, detectThresh);
Serial.println(F(“▶ Start counting\n”));
}
void loop() {
uint32_t now = millis();
// Auto-recalibrate baseline periodically
if (now – lastRecalTS >= RECALIBRATE_S * 1000UL) {
Serial.println(F(“⟳ Re-calibrating baseline…”));
baselineDist = calibrateBaseline();
detectThresh = baselineDist – DROP_CM;
lastRecalTS = now;
Serial.printf(“New baseline = %.1f cm Thresh = < %.1f cm\n\n”,
baselineDist, detectThresh);
}
// 1) Burst-ping and store readings
float buf[PINGS];
for (uint8_t i = 0; i < PINGS; i++) {
buf[i] = measureDist();
delay(20); // ~50 Hz burst to outrun bubble motion
}
// 2) Median filter to reject transient bubble/floc spikes
float med = median(buf, PINGS);
// 3) Fish detection & debounce
if (med > 0 && med < detectThresh
&& (now – lastFishTS > DEBOUNCE_MS)) {
fishCount++;
lastFishTS = now;
Serial.printf(“Fish #%lu @ %.2f s\n”,
fishCount, now / 1000.0f);
}
delay(100);
}
// SENDS ONE PING, RETURNS cm OR –1 ON TIMEOUT
float measureDist() {
digitalWrite(PIN_TRIG, LOW);
delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(PIN_TRIG, LOW);
long us = pulseIn(PIN_ECHO, HIGH, 30000); // 30 ms timeout
if (us == 0) return -1.0;
return (us / 2.0) / 29.1; // µs→cm
}
// AVERAGES MAX_READS VALID PINGS FOR BASELINE
float calibrateBaseline() {
float sum = 0;
uint8_t cnt = 0;
while (cnt < MAX_READS) {
float d = measureDist();
if (d > 0) {
sum += d;
cnt++;
}
delay(100);
}
return sum / cnt;
}
// SIMPLE INSERTION SORT → RETURN MIDDLE ELEMENT
float median(float *arr, uint8_t n) {
for (uint8_t i = 1; i < n; i++) {
float key = arr[i];
int8_t j = i – 1;
while (j >= 0 && arr[j] > key) {
arr[j+1] = arr[j];
j–;
}
arr[j+1] = key;
}
return arr[n/2];
}
/*
Biofloc Ultrasonic Catfish Counter
Pond depth: 1 m | Aeration: 10 L/min | Floc: ~50 mL/L
– HC-SR04 mounted across a 6–8 cm PVC flume
– TRIG→D7, ECHO→D6, VCC→5 V, GND→GND
– Optional bubble-skirt around transducer
Features:
1. Auto baseline (MAX_READS)
2. Burst of PINGS → median filter
3. DROP_CM below baseline → fish event
4. DEBOUNCE_MS per-fish
5. Re-baseline every RECALIBRATE_S seconds
*/
#include <Arduino.h>
// USER SETTINGS
const uint8_t PIN_TRIG = 7;
const uint8_t PIN_ECHO = 6;
const uint8_t MAX_READS = 30; // baseline samples
const uint8_t PINGS = 7; // pings per burst
const float DROP_CM = 6.0; // cm drop = fish event
const uint16_t DEBOUNCE_MS = 250; // ms ignore window
const uint32_t RECALIB_S = 600; // baseline recal every 10 min
// STATE
float baselineDist = 0, detectThresh = 0;
uint32_t fishCount = 0, lastFishTS = 0, lastRecalTS = 0;
void setup() {
Serial.begin(115200);
pinMode(PIN_TRIG, OUTPUT);
pinMode(PIN_ECHO, INPUT);
Serial.println(“\n⟳ Calibrating baseline…”);
baselineDist = calibrateBaseline();
detectThresh = baselineDist – DROP_CM;
lastRecalTS = millis();
Serial.printf(“Baseline=%.1f cm → thresh<%.1f cm\n▶ Starting counts\n\n”,
baselineDist, detectThresh);
}
void loop() {
uint32_t now = millis();
// periodic baseline recalibration
if (now – lastRecalTS >= RECALIB_S*1000UL) {
Serial.println(“⟳ Re-calibrating baseline…”);
baselineDist = calibrateBaseline();
detectThresh = baselineDist – DROP_CM;
lastRecalTS = now;
Serial.printf(“New baseline=%.1f cm thresh<%.1f cm\n\n”,
baselineDist, detectThresh);
}
// 1) burst-ping
float buf[PINGS];
for (uint8_t i = 0; i < PINGS; i++) {
buf[i] = measureDist();
delay(20); // 50 Hz
}
// 2) median filter
float m = median(buf, PINGS);
// 3) detect + debounce
if (m > 0 && m < detectThresh && now – lastFishTS > DEBOUNCE_MS) {
fishCount++;
lastFishTS = now;
Serial.printf(“🐟 #%lu @ %.2f s\n”, fishCount, now/1000.0f);
}
delay(100);
}
// — measureDist() — one HC-SR04 ping → cm or –1
float measureDist() {
digitalWrite(PIN_TRIG, LOW); delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH); delayMicroseconds(10);
digitalWrite(PIN_TRIG, LOW);
long us = pulseIn(PIN_ECHO, HIGH, 30000);
if (!us) return -1;
return (us/2.0) / 29.1;
}
// — calibrateBaseline() — avg of MAX_READS valid pings
float calibrateBaseline() {
float sum = 0; uint8_t cnt = 0;
while (cnt < MAX_READS) {
float d = measureDist();
if (d > 0) { sum += d; cnt++; }
delay(100);
}
return sum / cnt;
}
// — median() — insertion-sort & return arr[n/2]
float median(float *arr, uint8_t n) {
for (uint8_t i = 1; i < n; i++) {
float k = arr[i]; int8_t j = i-1;
while (j>=0 && arr[j] > k) { arr[j+1] = arr[j]; j–; }
arr[j+1] = k;
}
return arr[n/2];
}
/*
Biofloc Ultrasonic Catfish Counter
———————————-
Counts catfish passing single‐file through a 6–8cm flume in a 1m-deep biofloc pond
(≈50 mL/L solids) with 10 L/min aeration.
Implements:
• Auto baseline calibration (MAX_READS samples)
• Burst of PINGS ultrasonic pings → median filter for bubble/floc rejection
• DROP_CM below baseline → fish event
• DEBOUNCE_MS to avoid double-counts
• Periodic baseline re-calibration every RECALIBRATE_S seconds
• Serial log: “Fish #n @ t.s” (swap for SD or MQTT as needed)
*/
#include <Arduino.h>
// ── USER CONFIG ──────────────────────────────────────────────────────────────
const uint8_t PIN_TRIG = 7; // HC-SR04 TRIG pin
const uint8_t PIN_ECHO = 6; // HC-SR04 ECHO pin
const uint8_t MAX_READS = 30; // calibration pings count
const uint8_t PINGS = 7; // pings per detection burst
const float DROP_CM = 6.0; // cm drop from baseline → fish
const uint16_t DEBOUNCE_MS = 250; // ms ignore after a count
const uint32_t RECALIBRATE_S = 600; // seconds between auto-recalibrations
// ── RUNTIME STATE ────────────────────────────────────────────────────────────
float baselineDist = 0.0; // calibrated avg distance (cm)
float detectThresh = 0.0; // baselineDist – DROP_CM
uint32_t fishCount = 0; // total fish counted
uint32_t lastFishTS = 0; // timestamp of last count
uint32_t lastRecalTS = 0; // timestamp of last recalibration
// ── SETUP ────────────────────────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
pinMode(PIN_TRIG, OUTPUT);
pinMode(PIN_ECHO, INPUT);
Serial.println(F(“\n⟳ Calibrating baseline distance…”));
baselineDist = calibrateBaseline();
detectThresh = baselineDist – DROP_CM;
lastRecalTS = millis();
Serial.printf(“Baseline = %.1f cm → detect if < %.1f cm\n”,
baselineDist, detectThresh);
Serial.println(F(“▶ Starting fish counting…\n”));
}
// ── MAIN LOOP ────────────────────────────────────────────────────────────────
void loop() {
uint32_t now = millis();
// 1) Periodic re-calibration
if (now – lastRecalTS >= RECALIBRATE_S * 1000UL) {
Serial.println(F(“\n⟳ Re-calibrating baseline…”));
baselineDist = calibrateBaseline();
detectThresh = baselineDist – DROP_CM;
lastRecalTS = now;
Serial.printf(“New baseline = %.1f cm Thresh = < %.1f cm\n\n”,
baselineDist, detectThresh);
}
// 2) Burst of ultrasonic pings
float readings[PINGS];
for (uint8_t i = 0; i < PINGS; i++) {
readings[i] = measureDistance();
delay(20); // 50 Hz burst to outrun bubble movement
}
// 3) Median filter to reject transient echoes
float med = medianFilter(readings, PINGS);
// 4) Fish detection + debounce
if (med > 0 && med < detectThresh &&
(now – lastFishTS > DEBOUNCE_MS)) {
fishCount++;
lastFishTS = now;
Serial.printf(“🐟 Fish #%lu @ %.2f s\n”,
fishCount, now / 1000.0f);
}
delay(100);
}
// ── FUNCTIONS ────────────────────────────────────────────────────────────────
// measureDistance(): sends an HC-SR04 ping, returns cm or –1 on timeout
float measureDistance() {
digitalWrite(PIN_TRIG, LOW);
delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(PIN_TRIG, LOW);
long duration = pulseIn(PIN_ECHO, HIGH, 30000); // 30 ms timeout
if (duration == 0) return -1.0;
return (duration / 2.0) / 29.1; // µs→cm
}
// calibrateBaseline(): averages MAX_READS valid readings for baseline
float calibrateBaseline() {
float sum = 0;
uint8_t count = 0;
while (count < MAX_READS) {
float d = measureDistance();
if (d > 0) {
sum += d;
count++;
}
delay(100);
}
return sum / count;
}
// medianFilter(): simple insertion sort → returns middle element
float medianFilter(float *arr, uint8_t n) {
for (uint8_t i = 1; i < n; i++) {
float key = arr[i];
int8_t j = i – 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j–;
}
arr[j + 1] = key;
}
return arr[n / 2];
}
Signal-Processing Tip
By firing 7 quick pings at ~50 Hz and taking the median distance, you suppress one-off echoes from fast-rising bubbles or dense floc clumps. Meanwhile, periodic re-baseline calibration tracks slow drifts (temperature, floc buildup) so your DROP_CM threshold stays tuned over long runs under 10 L/min aeration.
/*
Biofloc Ultrasonic Catfish Counter
Pond depth: 1 m, biofloc ~50 mL/L, heavy aeration
Hardware:
• HC-SR04 or equivalent narrow-beam ultrasonic module
• Mounted across a 6–8 cm flume channel (PVC/acrylic)
• TRIG → D7, ECHO → D6, VCC → 5 V, GND → GND
Features:
1) Automatic baseline calibration (MAX_READS samples)
2) Detection bursts of PINGS readings → median filtering
3) Dynamic threshold: DROP_CM below baseline
4) DEBOUNCE_MS to avoid double-counts
5) Runtime re-calibration every RECALIBRATE_S seconds
*/
#include <Arduino.h>
//── USER CONFIG ─────────────────────────────────────────────────────────────
const uint8_t PIN_TRIG = 7;
const uint8_t PIN_ECHO = 6;
const uint8_t MAX_READS = 30; // baseline samples
const uint8_t PINGS = 7; // pings per detection burst
const float DROP_CM = 6.0; // cm drop = fish event
const uint16_t DEBOUNCE_MS = 250; // ms ignore window
const uint32_t RECALIBRATE_S = 600; // re-calibrate baseline every 10 min
//── RUNTIME STATE ────────────────────────────────────────────────────────────
float baselineDist = 0.0;
float detectThresh = 0.0;
uint32_t fishCount = 0;
uint32_t lastFishTS = 0;
uint32_t lastRecalTS = 0;
//── SETUP ────────────────────────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
pinMode(PIN_TRIG, OUTPUT);
pinMode(PIN_ECHO, INPUT);
Serial.println(F(“\n— Calibrating baseline distance —”));
baselineDist = calibrateBaseline();
detectThresh = baselineDist – DROP_CM;
lastRecalTS = millis();
Serial.printf(“Baseline: %.1f cm ➔ Threshold: < %.1f cm\n”,
baselineDist, detectThresh);
Serial.println(F(“— START COUNTING —\n”));
}
//── LOOP ─────────────────────────────────────────────────────────────────────
void loop() {
uint32_t now = millis();
// Periodic baseline re-calibration
if (now – lastRecalTS >= RECALIBRATE_S * 1000UL) {
baselineDist = calibrateBaseline();
detectThresh = baselineDist – DROP_CM;
lastRecalTS = now;
Serial.printf(“Re-calibrated baseline: %.1f cm Thresh: < %.1f cm\n”,
baselineDist, detectThresh);
}
// 1) Burst pings → store readings
float buf[PINGS];
for (uint8_t i = 0; i < PINGS; i++) {
buf[i] = measureDist();
delay(20); // 50 Hz ping burst
}
// 2) Median filter to reject bubbles/floc
float med = median(buf, PINGS);
// 3) Fish detection + debounce
if (med > 0 && med < detectThresh && (now – lastFishTS > DEBOUNCE_MS)) {
fishCount++;
lastFishTS = now;
Serial.printf(“Fish #%lu @ %.2f s\n”,
fishCount, now / 1000.0f);
}
delay(100);
}
//── MEASURE DISTANCE ──────────────────────────────────────────────────────────
// Sends one HC-SR04 ping, returns distance in cm or -1 on timeout
float measureDist() {
digitalWrite(PIN_TRIG, LOW);
delayMicroseconds(2);
digitalWrite(PIN_TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(PIN_TRIG, LOW);
long us = pulseIn(PIN_ECHO, HIGH, 30000); // 30 ms timeout
if (us == 0) return -1.0;
return (us / 2.0) / 29.1;
}
//── BASELINE CALIBRATION ─────────────────────────────────────────────────────
// Averages MAX_READS valid pings for a stable baseline
float calibrateBaseline() {
float sum = 0;
uint8_t cnt = 0;
while (cnt < MAX_READS) {
float d = measureDist();
if (d > 0) {
sum += d;
cnt++;
}
delay(100);
}
return sum / cnt;
}
//── MEDIAN FILTER ─────────────────────────────────────────────────────────────
// Simple insertion sort + return middle value
float median(float *arr, uint8_t n) {
for (uint8_t i = 1; i < n; i++) {
float key = arr[i];
int8_t j = i – 1;
while (j >= 0 && arr[j] > key) {
arr[j+1] = arr[j];
j–;
}
arr[j+1] = key;
}
return arr[n/2];
}
It:
• Calibrates a dynamic baseline to the back-wall reflector
• Fires bursts of PINGS ultrasonic readings, applies a median filter to reject bubble/floc spikes
• Flags a “fish event” when the median drops by DROP_CM
• Debounces so you get exactly one count per crossing
• Logs “Fish #n @ t s” to Serial (swap for SD/MQTT as needed)