{"id":344,"date":"2025-06-30T17:35:17","date_gmt":"2025-06-30T17:35:17","guid":{"rendered":"https:\/\/www.eatmaja.co.id\/eka\/?p=344"},"modified":"2025-06-30T17:35:17","modified_gmt":"2025-06-30T17:35:17","slug":"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-aeratio","status":"publish","type":"post","link":"https:\/\/www.eatmaja.co.id\/eka\/2025\/06\/30\/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-aeratio\/","title":{"rendered":"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."},"content":{"rendered":"<p>\/*<br \/>\n&nbsp; Open\u2010Pond Ultrasonic Catfish Counter<br \/>\n&nbsp; &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;<br \/>\n&nbsp; Counts catfish echoes in a 1 m\u2013deep biofloc pond (~50 mL\/L solids) under<br \/>\n&nbsp; 10 L\/min aeration without using a flume. Implements:<\/p>\n<p>&nbsp;&nbsp;&nbsp; \u2022 Automatic baseline calibration (pond bottom distance)&nbsp;<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 Burst of PING_COUNT pings \u2192 median\u2010filter gating&nbsp;<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 Fish\/gas\u2010bubble separation via distance gating &amp; median filter&nbsp;<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 DROP_CM below bottom \u2192 fish event&nbsp;<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 DEBOUNCE_MS to avoid double counts&nbsp;<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 Periodic re\u2010baseline every RECALIBRATE_INTERVAL_S&nbsp;<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 Logs \u201cFish #n @ t.s\u201d over Serial (swap-in SD\/MQTT easily)<\/p>\n<p>&nbsp; Hardware (HC-SR04):<br \/>\n&nbsp;&nbsp;&nbsp; TRIG \u2192 D7<br \/>\n&nbsp;&nbsp;&nbsp; ECHO \u2192 D6<br \/>\n&nbsp;&nbsp;&nbsp; VCC&nbsp; \u2192 5 V<br \/>\n&nbsp;&nbsp;&nbsp; GND&nbsp; \u2192 GND<br \/>\n*\/<\/p>\n<p>#include &lt;Arduino.h&gt;<\/p>\n<p>\/\/ \u2500\u2500 USER CONFIG \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\n\/\/ pins<br \/>\nconst uint8_t PIN_TRIG = 7;<br \/>\nconst uint8_t PIN_ECHO = 6;<\/p>\n<p>\/\/ signal\u2010processing parameters<br \/>\nconst uint8_t&nbsp; MAX_BASELINE_SAMPLES&nbsp;&nbsp; = 30;&nbsp;&nbsp; \/\/ samples to calibrate bottom<br \/>\nconst uint8_t&nbsp; PING_COUNT&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 7;&nbsp;&nbsp;&nbsp; \/\/ pings per detection burst<br \/>\nconst float&nbsp;&nbsp;&nbsp; DROP_CM&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 8.0;&nbsp; \/\/ drop from bottom \u2192 fish<br \/>\nconst float&nbsp;&nbsp;&nbsp; MIN_DIST_CM&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 20.0; \/\/ ignore echoes closer than this<br \/>\nconst float&nbsp;&nbsp;&nbsp; MAX_DIST_CM&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 90.0; \/\/ ignore echoes deeper than this<br \/>\nconst uint16_t DEBOUNCE_MS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 300;&nbsp; \/\/ lockout after a detection<br \/>\nconst uint32_t RECALIBRATE_INTERVAL_S = 600;&nbsp; \/\/ re\u2010baseline every 10 min<\/p>\n<p>\/\/ runtime state<br \/>\nfloat&nbsp;&nbsp;&nbsp; bottomDist&nbsp;&nbsp; = 0.0;&nbsp;&nbsp; \/\/ calibrated pond\u2010bottom distance (cm)<br \/>\nfloat&nbsp;&nbsp;&nbsp; detectThresh = 0.0;&nbsp;&nbsp; \/\/ bottomDist &#8211; DROP_CM<br \/>\nuint32_t fishCount&nbsp;&nbsp;&nbsp; = 0;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ total fish counted<br \/>\nuint32_t lastFishTS&nbsp;&nbsp; = 0;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ timestamp of last detection<br \/>\nuint32_t lastRecalTS&nbsp; = 0;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ timestamp of last re\u2010baseline<\/p>\n<p>void setup() {<br \/>\n&nbsp; Serial.begin(115200);<br \/>\n&nbsp; pinMode(PIN_TRIG, OUTPUT);<br \/>\n&nbsp; pinMode(PIN_ECHO, INPUT);<\/p>\n<p>&nbsp; Serial.println(F(&#8220;\\n\u27f3 Calibrating bottom baseline\u2026&#8221;));<br \/>\n&nbsp; bottomDist&nbsp;&nbsp; = calibrateBaseline();<br \/>\n&nbsp; detectThresh = bottomDist &#8211; DROP_CM;<br \/>\n&nbsp; lastRecalTS&nbsp; = millis();<\/p>\n<p>&nbsp; Serial.printf(&#8220;BottomDist=%.1f cm \u2192 fish if &lt; %.1f cm\\n&#x25b6; Start counting\\n\\n&#8221;,<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; bottomDist, detectThresh);<br \/>\n}<\/p>\n<p>void loop() {<br \/>\n&nbsp; uint32_t now = millis();<\/p>\n<p>&nbsp; \/\/ 1) Periodic re\u2010baseline<br \/>\n&nbsp; if (now &#8211; lastRecalTS &gt;= RECALIBRATE_INTERVAL_S * 1000UL) {<br \/>\n&nbsp;&nbsp;&nbsp; Serial.println(F(&#8220;\u27f3 Recalibrating baseline\u2026&#8221;));<br \/>\n&nbsp;&nbsp;&nbsp; bottomDist&nbsp;&nbsp; = calibrateBaseline();<br \/>\n&nbsp;&nbsp;&nbsp; detectThresh = bottomDist &#8211; DROP_CM;<br \/>\n&nbsp;&nbsp;&nbsp; lastRecalTS&nbsp; = now;<br \/>\n&nbsp;&nbsp;&nbsp; Serial.printf(&#8220;New BottomDist=%.1f cm thresh&lt;%.1f cm\\n\\n&#8221;,<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; bottomDist, detectThresh);<br \/>\n&nbsp; }<\/p>\n<p>&nbsp; \/\/ 2) Burst of ultrasonic pings<br \/>\n&nbsp; float readings[PING_COUNT];<br \/>\n&nbsp; for (uint8_t i = 0; i &lt; PING_COUNT; i++) {<br \/>\n&nbsp;&nbsp;&nbsp; readings[i] = measureDistance();<br \/>\n&nbsp;&nbsp;&nbsp; delay(20);&nbsp; \/\/ ~50 Hz burst to outrun bubble motion<br \/>\n&nbsp; }<\/p>\n<p>&nbsp; \/\/ 3) Median\u2010filter to reject transient bubble\/floc echoes<br \/>\n&nbsp; float med = medianFilter(readings, PING_COUNT);<\/p>\n<p>&nbsp; \/\/ 4) Distance gating + fish detection + debounce<br \/>\n&nbsp; if (med &gt; MIN_DIST_CM &amp;&amp; med &lt; detectThresh<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &amp;&amp; (now &#8211; lastFishTS &gt; DEBOUNCE_MS)) {<br \/>\n&nbsp;&nbsp;&nbsp; fishCount++;<br \/>\n&nbsp;&nbsp;&nbsp; lastFishTS = now;<br \/>\n&nbsp;&nbsp;&nbsp; Serial.printf(&#8220;&#x1f41f; Fish #%lu @ %.2f s\\n&#8221;,<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fishCount, now \/ 1000.0f);<br \/>\n&nbsp; }<\/p>\n<p>&nbsp; delay(100);<br \/>\n}<\/p>\n<p>\/\/ \u2500\u2500 UTILITIES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<\/p>\n<p>\/\/ send one HC-SR04 ping; return cm or -1 on timeout<br \/>\nfloat measureDistance() {<br \/>\n&nbsp; digitalWrite(PIN_TRIG, LOW);<br \/>\n&nbsp; delayMicroseconds(2);<br \/>\n&nbsp; digitalWrite(PIN_TRIG, HIGH);<br \/>\n&nbsp; delayMicroseconds(10);<br \/>\n&nbsp; digitalWrite(PIN_TRIG, LOW);<\/p>\n<p>&nbsp; long us = pulseIn(PIN_ECHO, HIGH, 30000);&nbsp; \/\/ 30 ms timeout<br \/>\n&nbsp; if (us == 0) return -1.0;<br \/>\n&nbsp; return (us \/ 2.0) \/ 29.1;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ \u00b5s\u2192cm<br \/>\n}<\/p>\n<p>\/\/ average MAX_BASELINE_SAMPLES valid pings for bottomDist<br \/>\nfloat calibrateBaseline() {<br \/>\n&nbsp; float sum = 0;<br \/>\n&nbsp; uint8_t cnt = 0;<br \/>\n&nbsp; while (cnt &lt; MAX_BASELINE_SAMPLES) {<br \/>\n&nbsp;&nbsp;&nbsp; float d = measureDistance();<br \/>\n&nbsp;&nbsp;&nbsp; if (d &gt; 0) {<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sum += d;<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cnt++;<br \/>\n&nbsp;&nbsp;&nbsp; }<br \/>\n&nbsp;&nbsp;&nbsp; delay(100);<br \/>\n&nbsp; }<br \/>\n&nbsp; return sum \/ cnt;<br \/>\n}<\/p>\n<p>\/\/ simple insertion sort \u2192 return median element<br \/>\nfloat medianFilter(float *arr, uint8_t n) {<br \/>\n&nbsp; for (uint8_t i = 1; i &lt; n; i++) {<br \/>\n&nbsp;&nbsp;&nbsp; float key = arr[i];<br \/>\n&nbsp;&nbsp;&nbsp; int8_t j = i &#8211; 1;<br \/>\n&nbsp;&nbsp;&nbsp; while (j &gt;= 0 &amp;&amp; arr[j] &gt; key) {<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; arr[j + 1] = arr[j];<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; j&#8211;;<br \/>\n&nbsp;&nbsp;&nbsp; }<br \/>\n&nbsp;&nbsp;&nbsp; arr[j + 1] = key;<br \/>\n&nbsp; }<br \/>\n&nbsp; return arr[n \/ 2];<br \/>\n}<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Signal-Processing Tip<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>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.<\/li>\n\n\n\n<li>Only count echoes between MINDISTCM and detectThresh to ignore surface\/bottom echoes.<\/li>\n\n\n\n<li>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.<\/li>\n<\/ul>\n\n\n<p>\/*<br \/>\n\u00a0 Open-Pond Ultrasonic Catfish Counter<br \/>\n\u00a0 &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8211;<br \/>\n\u00a0 Counts catfish echoes in a 1 m-deep biofloc pond (~50 mL\/L solids)<br \/>\n\u00a0 under 10 L\/min aeration\u2014no flume required.<\/p>\n<p>\u00a0 \u2022 Auto-calibrates bottom distance (MAX_BASELINE_SAMPLES)<br \/>\n\u00a0 \u2022 Fires a burst of PING_COUNT pings \u2192 median-filter gating<br \/>\n\u00a0 \u2022 Only counts echoes between MIN_DIST_CM and (bottomDist-DROP_CM)<br \/>\n\u00a0 \u2022 DEBOUNCE_MS locks out after each detection<br \/>\n\u00a0 \u2022 Periodic re-baseline every RECALIBRATE_INTERVAL_S<br \/>\n\u00a0 \u2022 Logs \u201cFish #n @ t.s\u201d on Serial (swap for SD\/MQTT as needed)<\/p>\n<p>\u00a0 HC-SR04 wiring:<br \/>\n\u00a0\u00a0\u00a0 TRIG \u2192 D7<br \/>\n\u00a0\u00a0\u00a0 ECHO \u2192 D6<br \/>\n\u00a0\u00a0\u00a0 VCC\u00a0 \u2192 5 V<br \/>\n\u00a0\u00a0\u00a0 GND\u00a0 \u2192 GND<br \/>\n*\/<\/p>\n<p>#include &lt;Arduino.h&gt;<\/p>\n<p>\/\/ \u2500\u2500 USER CONFIG \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\nconst uint8_t\u00a0 PIN_TRIG\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 = 7;\u00a0\u00a0\u00a0\u00a0\u00a0 \/\/ HC-SR04 TRIG pin<br \/>\nconst uint8_t\u00a0 PIN_ECHO\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 = 6;\u00a0\u00a0\u00a0\u00a0\u00a0 \/\/ HC-SR04 ECHO pin<\/p>\n<p>const uint8_t\u00a0 MAX_BASELINE_SAMPLES\u00a0\u00a0 = 30;\u00a0\u00a0\u00a0\u00a0 \/\/ samples for bottom calibration<br \/>\nconst uint8_t\u00a0 PING_COUNT\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 = 7;\u00a0\u00a0\u00a0\u00a0\u00a0 \/\/ pings per detection burst<br \/>\nconst float\u00a0\u00a0\u00a0 DROP_CM\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 = 8.0;\u00a0\u00a0\u00a0 \/\/ drop below bottom \u2192 fish event<br \/>\nconst float\u00a0\u00a0\u00a0 MIN_DIST_CM\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 = 20.0;\u00a0\u00a0 \/\/ ignore echoes closer than this (cm)<br \/>\nconst float\u00a0\u00a0\u00a0 MAX_DIST_CM\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 = 95.0;\u00a0\u00a0 \/\/ ignore echoes deeper than this (cm)<br \/>\nconst uint16_t DEBOUNCE_MS\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 = 300;\u00a0\u00a0\u00a0 \/\/ ms lockout per fish<br \/>\nconst uint32_t RECALIBRATE_INTERVAL_S = 600;\u00a0\u00a0\u00a0 \/\/ seconds between re\u2010baselines<\/p>\n<p>\/\/ \u2500\u2500 RUNTIME STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\nfloat\u00a0\u00a0\u00a0 bottomDist\u00a0\u00a0 = 0.0;\u00a0\u00a0 \/\/ calibrated pond bottom distance (cm)<br \/>\nfloat\u00a0\u00a0\u00a0 detectThresh = 0.0;\u00a0\u00a0 \/\/ bottomDist \u2212 DROP_CM<br \/>\nuint32_t fishCount\u00a0\u00a0\u00a0 = 0;\u00a0\u00a0\u00a0\u00a0 \/\/ total fish counted<br \/>\nuint32_t lastFishTS\u00a0\u00a0 = 0;\u00a0\u00a0\u00a0\u00a0 \/\/ millis() of last count<br \/>\nuint32_t lastRecalTS\u00a0 = 0;\u00a0\u00a0\u00a0\u00a0 \/\/ millis() of last baseline<\/p>\n<p>\/\/ \u2500\u2500 SETUP \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\nvoid setup() {<br \/>\n\u00a0 Serial.begin(115200);<br \/>\n\u00a0 pinMode(PIN_TRIG, OUTPUT);<br \/>\n\u00a0 pinMode(PIN_ECHO, INPUT);<\/p>\n<p>\u00a0 Serial.println(F(&#8220;\\n\u27f3 Calibrating bottom baseline\u2026&#8221;));<br \/>\n\u00a0 bottomDist\u00a0\u00a0 = calibrateBaseline();<br \/>\n\u00a0 detectThresh = bottomDist &#8211; DROP_CM;<br \/>\n\u00a0 lastRecalTS\u00a0 = millis();<\/p>\n<p>\u00a0 Serial.printf(&#8220;BottomDist=%.1f cm \u2192 count if &lt; %.1f cm\\n&#x25b6; Counting started\\n\\n&#8221;,<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 bottomDist, detectThresh);<br \/>\n}<\/p>\n<p>\/\/ \u2500\u2500 MAIN LOOP \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\nvoid loop() {<br \/>\n\u00a0 uint32_t now = millis();<\/p>\n<p>\u00a0 \/\/ 1) Periodic bottom re-calibration<br \/>\n\u00a0 if (now &#8211; lastRecalTS &gt;= RECALIBRATE_INTERVAL_S * 1000UL) {<br \/>\n\u00a0\u00a0\u00a0 Serial.println(F(&#8220;\u27f3 Re-calibrating bottom baseline\u2026&#8221;));<br \/>\n\u00a0\u00a0\u00a0 bottomDist\u00a0\u00a0 = calibrateBaseline();<br \/>\n\u00a0\u00a0\u00a0 detectThresh = bottomDist &#8211; DROP_CM;<br \/>\n\u00a0\u00a0\u00a0 lastRecalTS\u00a0 = now;<br \/>\n\u00a0\u00a0\u00a0 Serial.printf(&#8220;New BottomDist=%.1f cm \u2192 thresh&lt;%.1f cm\\n\\n&#8221;,<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 bottomDist, detectThresh);<br \/>\n\u00a0 }<\/p>\n<p>\u00a0 \/\/ 2) Burst of ultrasonic pings<br \/>\n\u00a0 float readings[PING_COUNT];<br \/>\n\u00a0 for (uint8_t i = 0; i &lt; PING_COUNT; i++) {<br \/>\n\u00a0\u00a0\u00a0 readings[i] = measureDistance();<br \/>\n\u00a0\u00a0\u00a0 delay(20);\u00a0 \/\/ \u224850 Hz to outrun bubble motion<br \/>\n\u00a0 }<\/p>\n<p>\u00a0 \/\/ 3) Median-filter gating<br \/>\n\u00a0 float med = medianFilter(readings, PING_COUNT);<\/p>\n<p>\u00a0 \/\/ 4) Fish detection + debounce + distance gating<br \/>\n\u00a0 if (med &gt; MIN_DIST_CM &amp;&amp; med &lt; detectThresh &amp;&amp;<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0 (now &#8211; lastFishTS &gt; DEBOUNCE_MS)) {<br \/>\n\u00a0\u00a0\u00a0 fishCount++;<br \/>\n\u00a0\u00a0\u00a0 lastFishTS = now;<br \/>\n\u00a0\u00a0\u00a0 Serial.printf(&#8220;\uf41f Fish #%lu @ %.2f s\\n&#8221;,<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 fishCount, now \/ 1000.0f);<br \/>\n\u00a0 }<\/p>\n<p>\u00a0 delay(100);<br \/>\n}<\/p>\n<p>\/\/ \u2500\u2500 UTILITY FUNCTIONS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<\/p>\n<p>\/\/ Send one HC-SR04 ping \u2192 return cm or \u20131 on timeout<br \/>\nfloat measureDistance() {<br \/>\n\u00a0 digitalWrite(PIN_TRIG, LOW);<br \/>\n\u00a0 delayMicroseconds(2);<br \/>\n\u00a0 digitalWrite(PIN_TRIG, HIGH);<br \/>\n\u00a0 delayMicroseconds(10);<br \/>\n\u00a0 digitalWrite(PIN_TRIG, LOW);<\/p>\n<p>\u00a0 long duration = pulseIn(PIN_ECHO, HIGH, 30000);\u00a0 \/\/ 30 ms timeout<br \/>\n\u00a0 if (duration == 0) return -1.0;<br \/>\n\u00a0 return (duration \/ 2.0) \/ 29.1;\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \/\/ \u00b5s \u2192 cm<br \/>\n}<\/p>\n<p>\/\/ Calibrate bottom distance: average MAX_BASELINE_SAMPLES valid readings<br \/>\nfloat calibrateBaseline() {<br \/>\n\u00a0 float sum = 0;<br \/>\n\u00a0 uint8_t cnt = 0;<br \/>\n\u00a0 while (cnt &lt; MAX_BASELINE_SAMPLES) {<br \/>\n\u00a0\u00a0\u00a0 float d = measureDistance();<br \/>\n\u00a0\u00a0\u00a0 if (d &gt; 0 &amp;&amp; d &lt; MAX_DIST_CM) {<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0 sum += d;<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0 cnt++;<br \/>\n\u00a0\u00a0\u00a0 }<br \/>\n\u00a0\u00a0\u00a0 delay(100);<br \/>\n\u00a0 }<br \/>\n\u00a0 return sum \/ cnt;<br \/>\n}<\/p>\n<p>\/\/ Simple insertion sort + return middle element as median<br \/>\nfloat medianFilter(float *arr, uint8_t n) {<br \/>\n\u00a0 for (uint8_t i = 1; i &lt; n; i++) {<br \/>\n\u00a0\u00a0\u00a0 float key = arr[i];<br \/>\n\u00a0\u00a0\u00a0 int8_t j = i &#8211; 1;<br \/>\n\u00a0\u00a0\u00a0 while (j &gt;= 0 &amp;&amp; arr[j] &gt; key) {<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0 arr[j + 1] = arr[j];<br \/>\n\u00a0\u00a0\u00a0\u00a0\u00a0 j&#8211;;<br \/>\n\u00a0\u00a0\u00a0 }<br \/>\n\u00a0\u00a0\u00a0 arr[j + 1] = key;<br \/>\n\u00a0 }<br \/>\n\u00a0 return arr[n \/ 2];<br \/>\n}<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Signal-Processing Tip<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Fire a burst of 7 pings at ~50 Hz and take the median: this rejects one-off spikes from rising bubbles or dense floc.<\/li>\n\n\n\n<li>Gate out echoes closer than MINDISTCM (surface bubbles) and deeper than detectThresh (pond bottom), so only fish returns remain.<\/li>\n\n\n\n<li>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.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Here\u2019s a turnkey Arduino sketch for counting catfish passing single-file through a 6\u20138 cm flume, optimized for a 1 m-deep biofloc pond (\u224850 mL\/L solids) with 10 L\/min aeration. It:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Calibrates a dynamic baseline to the opposite wall every 10 min<\/li>\n\n\n\n<li>Fires bursts of ultrasonic pings (you\u2019ll lose many bubble echoes)<\/li>\n\n\n\n<li>Uses a median filter across each burst to reject transient floc\/bubble spikes<\/li>\n\n\n\n<li>Flags a fish when the median drops by DROP_CM<\/li>\n\n\n\n<li>Debounces to ensure one count per fish<\/li>\n\n\n\n<li>Logs \u201cFish #n @ t s\u201d over Serial (swap in SD or MQTT as needed)<\/li>\n<\/ul>\n\n\n<p>\/*<br \/>\n&nbsp; Biofloc Ultrasonic Catfish Counter<br \/>\n&nbsp; Pond depth: 1 m&nbsp; |&nbsp; Biofloc: ~50 mL\/L&nbsp; |&nbsp; Aeration: 10 L\/min<\/p>\n<p>&nbsp; Hardware:<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 HC-SR04 ultrasonic sensor (2\u2013400 cm range)<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 Mounted across a 6\u20138 cm PVC\/acrylic flume<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 TRIG \u2192 D7, ECHO \u2192 D6, VCC \u2192 5 V, GND \u2192 GND<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 (Optional bubble-skirt 3D-printed around transducer)<\/p>\n<p>&nbsp; Features:<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 Auto baseline calibration (MAX_READS samples)<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 Burst of PINGS readings \u2192 median filter for bubble\/floc rejection<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 DROP_CM below baseline \u2192 fish event<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 DEBOUNCE_MS to avoid double-counts<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 Re-calibrate baseline every RECALIBRATE_S seconds<br \/>\n*\/<\/p>\n<p>#include &lt;Arduino.h&gt;<\/p>\n<p>\/\/ USER CONFIGURATION<br \/>\nconst uint8_t&nbsp; PIN_TRIG&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 7;<br \/>\nconst uint8_t&nbsp; PIN_ECHO&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 6;<\/p>\n<p>const uint8_t&nbsp; MAX_READS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 30;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ samples for baseline<br \/>\nconst uint8_t&nbsp; PINGS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 7;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ pings per detection burst<br \/>\nconst float&nbsp;&nbsp;&nbsp; DROP_CM&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 6.0;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ cm drop = fish event<br \/>\nconst uint16_t DEBOUNCE_MS&nbsp;&nbsp;&nbsp; = 250;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ ms ignore window<br \/>\nconst uint32_t RECALIBRATE_S&nbsp; = 600;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ seconds between auto-recalibrations<\/p>\n<p>\/\/ RUNTIME STATE<br \/>\nfloat&nbsp;&nbsp;&nbsp; baselineDist&nbsp; = 0.0;<br \/>\nfloat&nbsp;&nbsp;&nbsp; detectThresh&nbsp; = 0.0;<br \/>\nuint32_t fishCount&nbsp;&nbsp;&nbsp;&nbsp; = 0;<br \/>\nuint32_t lastFishTS&nbsp;&nbsp;&nbsp; = 0;<br \/>\nuint32_t lastRecalTS&nbsp;&nbsp; = 0;<\/p>\n<p>void setup() {<br \/>\n&nbsp; Serial.begin(115200);<br \/>\n&nbsp; pinMode(PIN_TRIG, OUTPUT);<br \/>\n&nbsp; pinMode(PIN_ECHO, INPUT);<\/p>\n<p>&nbsp; Serial.println(F(&#8220;\\n\u27f3 Calibrating baseline\u2026&#8221;));<br \/>\n&nbsp; baselineDist = calibrateBaseline();<br \/>\n&nbsp; detectThresh = baselineDist &#8211; DROP_CM;<br \/>\n&nbsp; lastRecalTS&nbsp; = millis();<\/p>\n<p>&nbsp; Serial.printf(&#8220;Baseline = %.1f cm&nbsp; \u2192 detect if &lt; %.1f cm\\n&#8221;,<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; baselineDist, detectThresh);<br \/>\n&nbsp; Serial.println(F(&#8220;&#x25b6; Start counting\\n&#8221;));<br \/>\n}<\/p>\n<p>void loop() {<br \/>\n&nbsp; uint32_t now = millis();<\/p>\n<p>&nbsp; \/\/ Auto-recalibrate baseline periodically<br \/>\n&nbsp; if (now &#8211; lastRecalTS &gt;= RECALIBRATE_S * 1000UL) {<br \/>\n&nbsp;&nbsp;&nbsp; Serial.println(F(&#8220;\u27f3 Re-calibrating baseline\u2026&#8221;));<br \/>\n&nbsp;&nbsp;&nbsp; baselineDist = calibrateBaseline();<br \/>\n&nbsp;&nbsp;&nbsp; detectThresh = baselineDist &#8211; DROP_CM;<br \/>\n&nbsp;&nbsp;&nbsp; lastRecalTS&nbsp; = now;<br \/>\n&nbsp;&nbsp;&nbsp; Serial.printf(&#8220;New baseline = %.1f cm&nbsp; Thresh = &lt; %.1f cm\\n\\n&#8221;,<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; baselineDist, detectThresh);<br \/>\n&nbsp; }<\/p>\n<p>&nbsp; \/\/ 1) Burst-ping and store readings<br \/>\n&nbsp; float buf[PINGS];<br \/>\n&nbsp; for (uint8_t i = 0; i &lt; PINGS; i++) {<br \/>\n&nbsp;&nbsp;&nbsp; buf[i] = measureDist();<br \/>\n&nbsp;&nbsp;&nbsp; delay(20);&nbsp; \/\/ ~50 Hz burst to outrun bubble motion<br \/>\n&nbsp; }<\/p>\n<p>&nbsp; \/\/ 2) Median filter to reject transient bubble\/floc spikes<br \/>\n&nbsp; float med = median(buf, PINGS);<\/p>\n<p>&nbsp; \/\/ 3) Fish detection &amp; debounce<br \/>\n&nbsp; if (med &gt; 0 &amp;&amp; med &lt; detectThresh<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &amp;&amp; (now &#8211; lastFishTS &gt; DEBOUNCE_MS)) {<br \/>\n&nbsp;&nbsp;&nbsp; fishCount++;<br \/>\n&nbsp;&nbsp;&nbsp; lastFishTS = now;<br \/>\n&nbsp;&nbsp;&nbsp; Serial.printf(&#8220;Fish #%lu @ %.2f s\\n&#8221;,<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fishCount, now \/ 1000.0f);<br \/>\n&nbsp; }<\/p>\n<p>&nbsp; delay(100);<br \/>\n}<\/p>\n<p>\/\/ SENDS ONE PING, RETURNS cm OR \u20131 ON TIMEOUT<br \/>\nfloat measureDist() {<br \/>\n&nbsp; digitalWrite(PIN_TRIG, LOW);<br \/>\n&nbsp; delayMicroseconds(2);<br \/>\n&nbsp; digitalWrite(PIN_TRIG, HIGH);<br \/>\n&nbsp; delayMicroseconds(10);<br \/>\n&nbsp; digitalWrite(PIN_TRIG, LOW);<\/p>\n<p>&nbsp; long us = pulseIn(PIN_ECHO, HIGH, 30000); \/\/ 30 ms timeout<br \/>\n&nbsp; if (us == 0) return -1.0;<br \/>\n&nbsp; return (us \/ 2.0) \/ 29.1;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ \u00b5s\u2192cm<br \/>\n}<\/p>\n<p>\/\/ AVERAGES MAX_READS VALID PINGS FOR BASELINE<br \/>\nfloat calibrateBaseline() {<br \/>\n&nbsp; float sum = 0;<br \/>\n&nbsp; uint8_t cnt = 0;<br \/>\n&nbsp; while (cnt &lt; MAX_READS) {<br \/>\n&nbsp;&nbsp;&nbsp; float d = measureDist();<br \/>\n&nbsp;&nbsp;&nbsp; if (d &gt; 0) {<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sum += d;<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cnt++;<br \/>\n&nbsp;&nbsp;&nbsp; }<br \/>\n&nbsp;&nbsp;&nbsp; delay(100);<br \/>\n&nbsp; }<br \/>\n&nbsp; return sum \/ cnt;<br \/>\n}<\/p>\n<p>\/\/ SIMPLE INSERTION SORT \u2192 RETURN MIDDLE ELEMENT<br \/>\nfloat median(float *arr, uint8_t n) {<br \/>\n&nbsp; for (uint8_t i = 1; i &lt; n; i++) {<br \/>\n&nbsp;&nbsp;&nbsp; float key = arr[i];<br \/>\n&nbsp;&nbsp;&nbsp; int8_t j = i &#8211; 1;<br \/>\n&nbsp;&nbsp;&nbsp; while (j &gt;= 0 &amp;&amp; arr[j] &gt; key) {<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; arr[j+1] = arr[j];<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; j&#8211;;<br \/>\n&nbsp;&nbsp;&nbsp; }<br \/>\n&nbsp;&nbsp;&nbsp; arr[j+1] = key;<br \/>\n&nbsp; }<br \/>\n&nbsp; return arr[n\/2];<br \/>\n}<\/p>\n\n\n<p>\/*<br \/>\n&nbsp; Biofloc Ultrasonic Catfish Counter<br \/>\n&nbsp; Pond depth: 1 m | Aeration: 10 L\/min | Floc: ~50 mL\/L<\/p>\n<p>&nbsp; \u2013 HC-SR04 mounted across a 6\u20138 cm PVC flume<br \/>\n&nbsp; \u2013 TRIG\u2192D7, ECHO\u2192D6, VCC\u21925 V, GND\u2192GND<br \/>\n&nbsp; \u2013 Optional bubble-skirt around transducer<\/p>\n<p>&nbsp; Features:<br \/>\n&nbsp;&nbsp; 1. Auto baseline (MAX_READS)<br \/>\n&nbsp;&nbsp; 2. Burst of PINGS \u2192 median filter<br \/>\n&nbsp;&nbsp; 3. DROP_CM below baseline \u2192 fish event<br \/>\n&nbsp;&nbsp; 4. DEBOUNCE_MS per-fish<br \/>\n&nbsp;&nbsp; 5. Re-baseline every RECALIBRATE_S seconds<br \/>\n*\/<\/p>\n<p>#include &lt;Arduino.h&gt;<\/p>\n<p>\/\/ USER SETTINGS<br \/>\nconst uint8_t PIN_TRIG&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 7;<br \/>\nconst uint8_t PIN_ECHO&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 6;<br \/>\nconst uint8_t MAX_READS&nbsp;&nbsp;&nbsp;&nbsp; = 30;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ baseline samples<br \/>\nconst uint8_t PINGS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 7;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ pings per burst<br \/>\nconst float&nbsp;&nbsp; DROP_CM&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 6.0;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ cm drop = fish event<br \/>\nconst uint16_t DEBOUNCE_MS&nbsp; = 250;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ ms ignore window<br \/>\nconst uint32_t RECALIB_S&nbsp;&nbsp;&nbsp; = 600;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ baseline recal every 10 min<\/p>\n<p>\/\/ STATE<br \/>\nfloat&nbsp;&nbsp; baselineDist = 0, detectThresh = 0;<br \/>\nuint32_t fishCount = 0, lastFishTS = 0, lastRecalTS = 0;<\/p>\n<p>void setup() {<br \/>\n&nbsp; Serial.begin(115200);<br \/>\n&nbsp; pinMode(PIN_TRIG, OUTPUT);<br \/>\n&nbsp; pinMode(PIN_ECHO, INPUT);<\/p>\n<p>&nbsp; Serial.println(&#8220;\\n\u27f3 Calibrating baseline\u2026&#8221;);<br \/>\n&nbsp; baselineDist&nbsp; = calibrateBaseline();<br \/>\n&nbsp; detectThresh&nbsp; = baselineDist &#8211; DROP_CM;<br \/>\n&nbsp; lastRecalTS&nbsp;&nbsp; = millis();<\/p>\n<p>&nbsp; Serial.printf(&#8220;Baseline=%.1f cm \u2192 thresh&lt;%.1f cm\\n&#x25b6; Starting counts\\n\\n&#8221;,<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; baselineDist, detectThresh);<br \/>\n}<\/p>\n<p>void loop() {<br \/>\n&nbsp; uint32_t now = millis();<\/p>\n<p>&nbsp; \/\/ periodic baseline recalibration<br \/>\n&nbsp; if (now &#8211; lastRecalTS &gt;= RECALIB_S*1000UL) {<br \/>\n&nbsp;&nbsp;&nbsp; Serial.println(&#8220;\u27f3 Re-calibrating baseline\u2026&#8221;);<br \/>\n&nbsp;&nbsp;&nbsp; baselineDist = calibrateBaseline();<br \/>\n&nbsp;&nbsp;&nbsp; detectThresh = baselineDist &#8211; DROP_CM;<br \/>\n&nbsp;&nbsp;&nbsp; lastRecalTS&nbsp; = now;<br \/>\n&nbsp;&nbsp;&nbsp; Serial.printf(&#8220;New baseline=%.1f cm thresh&lt;%.1f cm\\n\\n&#8221;,<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; baselineDist, detectThresh);<br \/>\n&nbsp; }<\/p>\n<p>&nbsp; \/\/ 1) burst-ping<br \/>\n&nbsp; float buf[PINGS];<br \/>\n&nbsp; for (uint8_t i = 0; i &lt; PINGS; i++) {<br \/>\n&nbsp;&nbsp;&nbsp; buf[i] = measureDist();<br \/>\n&nbsp;&nbsp;&nbsp; delay(20);&nbsp; \/\/ 50 Hz<br \/>\n&nbsp; }<\/p>\n<p>&nbsp; \/\/ 2) median filter<br \/>\n&nbsp; float m = median(buf, PINGS);<\/p>\n<p>&nbsp; \/\/ 3) detect + debounce<br \/>\n&nbsp; if (m &gt; 0 &amp;&amp; m &lt; detectThresh &amp;&amp; now &#8211; lastFishTS &gt; DEBOUNCE_MS) {<br \/>\n&nbsp;&nbsp;&nbsp; fishCount++;<br \/>\n&nbsp;&nbsp;&nbsp; lastFishTS = now;<br \/>\n&nbsp;&nbsp;&nbsp; Serial.printf(&#8220;&#x1f41f; #%lu @ %.2f s\\n&#8221;, fishCount, now\/1000.0f);<br \/>\n&nbsp; }<\/p>\n<p>&nbsp; delay(100);<br \/>\n}<\/p>\n<p>\/\/ \u2014 measureDist() \u2014 one HC-SR04 ping \u2192 cm or \u20131<br \/>\nfloat measureDist() {<br \/>\n&nbsp; digitalWrite(PIN_TRIG, LOW);&nbsp; delayMicroseconds(2);<br \/>\n&nbsp; digitalWrite(PIN_TRIG, HIGH); delayMicroseconds(10);<br \/>\n&nbsp; digitalWrite(PIN_TRIG, LOW);<br \/>\n&nbsp; long us = pulseIn(PIN_ECHO, HIGH, 30000);<br \/>\n&nbsp; if (!us) return -1;<br \/>\n&nbsp; return (us\/2.0) \/ 29.1;<br \/>\n}<\/p>\n<p>\/\/ \u2014 calibrateBaseline() \u2014 avg of MAX_READS valid pings<br \/>\nfloat calibrateBaseline() {<br \/>\n&nbsp; float sum = 0; uint8_t cnt = 0;<br \/>\n&nbsp; while (cnt &lt; MAX_READS) {<br \/>\n&nbsp;&nbsp;&nbsp; float d = measureDist();<br \/>\n&nbsp;&nbsp;&nbsp; if (d &gt; 0) { sum += d; cnt++; }<br \/>\n&nbsp;&nbsp;&nbsp; delay(100);<br \/>\n&nbsp; }<br \/>\n&nbsp; return sum \/ cnt;<br \/>\n}<\/p>\n<p>\/\/ \u2014 median() \u2014 insertion-sort &amp; return arr[n\/2]<br \/>\nfloat median(float *arr, uint8_t n) {<br \/>\n&nbsp; for (uint8_t i = 1; i &lt; n; i++) {<br \/>\n&nbsp;&nbsp;&nbsp; float k = arr[i]; int8_t j = i-1;<br \/>\n&nbsp;&nbsp;&nbsp; while (j&gt;=0 &amp;&amp; arr[j] &gt; k) { arr[j+1] = arr[j]; j&#8211;; }<br \/>\n&nbsp;&nbsp;&nbsp; arr[j+1] = k;<br \/>\n&nbsp; }<br \/>\n&nbsp; return arr[n\/2];<br \/>\n}<\/p>\n\n\n<p>\/*<br \/>\n&nbsp; Biofloc Ultrasonic Catfish Counter<br \/>\n&nbsp; &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-<br \/>\n&nbsp; Counts catfish passing single\u2010file through a 6\u20138cm flume in a 1m-deep biofloc pond<br \/>\n&nbsp; (\u224850 mL\/L solids) with 10 L\/min aeration.&nbsp;<br \/>\n&nbsp; Implements:<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 Auto baseline calibration (MAX_READS samples)&nbsp;<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 Burst of PINGS ultrasonic pings \u2192 median filter for bubble\/floc rejection&nbsp;<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 DROP_CM below baseline \u2192 fish event&nbsp;<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 DEBOUNCE_MS to avoid double-counts&nbsp;<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 Periodic baseline re-calibration every RECALIBRATE_S seconds&nbsp;<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 Serial log: \u201cFish #n @ t.s\u201d (swap for SD or MQTT as needed)&nbsp;<br \/>\n*\/<\/p>\n<p>#include &lt;Arduino.h&gt;<\/p>\n<p>\/\/ \u2500\u2500 USER CONFIG \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\nconst uint8_t&nbsp; PIN_TRIG&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 7;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ HC-SR04 TRIG pin<br \/>\nconst uint8_t&nbsp; PIN_ECHO&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 6;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ HC-SR04 ECHO pin<\/p>\n<p>const uint8_t&nbsp; MAX_READS&nbsp;&nbsp;&nbsp;&nbsp; = 30;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ calibration pings count<br \/>\nconst uint8_t&nbsp; PINGS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 7;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ pings per detection burst<br \/>\nconst float&nbsp;&nbsp;&nbsp; DROP_CM&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 6.0;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ cm drop from baseline \u2192 fish<br \/>\nconst uint16_t DEBOUNCE_MS&nbsp;&nbsp; = 250;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ ms ignore after a count<br \/>\nconst uint32_t RECALIBRATE_S = 600;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ seconds between auto-recalibrations<\/p>\n<p>\/\/ \u2500\u2500 RUNTIME STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\nfloat&nbsp;&nbsp;&nbsp; baselineDist&nbsp; = 0.0;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ calibrated avg distance (cm)<br \/>\nfloat&nbsp;&nbsp;&nbsp; detectThresh&nbsp; = 0.0;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ baselineDist \u2013 DROP_CM<br \/>\nuint32_t fishCount&nbsp;&nbsp;&nbsp;&nbsp; = 0;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ total fish counted<br \/>\nuint32_t lastFishTS&nbsp;&nbsp;&nbsp; = 0;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ timestamp of last count<br \/>\nuint32_t lastRecalTS&nbsp;&nbsp; = 0;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ timestamp of last recalibration<\/p>\n<p>\/\/ \u2500\u2500 SETUP \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\nvoid setup() {<br \/>\n&nbsp; Serial.begin(115200);<br \/>\n&nbsp; pinMode(PIN_TRIG, OUTPUT);<br \/>\n&nbsp; pinMode(PIN_ECHO, INPUT);<\/p>\n<p>&nbsp; Serial.println(F(&#8220;\\n\u27f3 Calibrating baseline distance\u2026&#8221;));<br \/>\n&nbsp; baselineDist = calibrateBaseline();<br \/>\n&nbsp; detectThresh = baselineDist &#8211; DROP_CM;<br \/>\n&nbsp; lastRecalTS&nbsp; = millis();<\/p>\n<p>&nbsp; Serial.printf(&#8220;Baseline = %.1f cm&nbsp;&nbsp; \u2192 detect if &lt; %.1f cm\\n&#8221;,<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; baselineDist, detectThresh);<br \/>\n&nbsp; Serial.println(F(&#8220;&#x25b6; Starting fish counting\u2026\\n&#8221;));<br \/>\n}<\/p>\n<p>\/\/ \u2500\u2500 MAIN LOOP \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\nvoid loop() {<br \/>\n&nbsp; uint32_t now = millis();<\/p>\n<p>&nbsp; \/\/ 1) Periodic re-calibration<br \/>\n&nbsp; if (now &#8211; lastRecalTS &gt;= RECALIBRATE_S * 1000UL) {<br \/>\n&nbsp;&nbsp;&nbsp; Serial.println(F(&#8220;\\n\u27f3 Re-calibrating baseline\u2026&#8221;));<br \/>\n&nbsp;&nbsp;&nbsp; baselineDist = calibrateBaseline();<br \/>\n&nbsp;&nbsp;&nbsp; detectThresh = baselineDist &#8211; DROP_CM;<br \/>\n&nbsp;&nbsp;&nbsp; lastRecalTS&nbsp; = now;<br \/>\n&nbsp;&nbsp;&nbsp; Serial.printf(&#8220;New baseline = %.1f cm&nbsp;&nbsp; Thresh = &lt; %.1f cm\\n\\n&#8221;,<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; baselineDist, detectThresh);<br \/>\n&nbsp; }<\/p>\n<p>&nbsp; \/\/ 2) Burst of ultrasonic pings<br \/>\n&nbsp; float readings[PINGS];<br \/>\n&nbsp; for (uint8_t i = 0; i &lt; PINGS; i++) {<br \/>\n&nbsp;&nbsp;&nbsp; readings[i] = measureDistance();<br \/>\n&nbsp;&nbsp;&nbsp; delay(20);&nbsp; \/\/ 50 Hz burst to outrun bubble movement<br \/>\n&nbsp; }<\/p>\n<p>&nbsp; \/\/ 3) Median filter to reject transient echoes<br \/>\n&nbsp; float med = medianFilter(readings, PINGS);<\/p>\n<p>&nbsp; \/\/ 4) Fish detection + debounce<br \/>\n&nbsp; if (med &gt; 0 &amp;&amp; med &lt; detectThresh &amp;&amp;<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; (now &#8211; lastFishTS &gt; DEBOUNCE_MS)) {<br \/>\n&nbsp;&nbsp;&nbsp; fishCount++;<br \/>\n&nbsp;&nbsp;&nbsp; lastFishTS = now;<br \/>\n&nbsp;&nbsp;&nbsp; Serial.printf(&#8220;&#x1f41f; Fish #%lu @ %.2f s\\n&#8221;,<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fishCount, now \/ 1000.0f);<br \/>\n&nbsp; }<\/p>\n<p>&nbsp; delay(100);<br \/>\n}<\/p>\n<p>\/\/ \u2500\u2500 FUNCTIONS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<\/p>\n<p>\/\/ measureDistance(): sends an HC-SR04 ping, returns cm or \u20131 on timeout<br \/>\nfloat measureDistance() {<br \/>\n&nbsp; digitalWrite(PIN_TRIG, LOW);<br \/>\n&nbsp; delayMicroseconds(2);<br \/>\n&nbsp; digitalWrite(PIN_TRIG, HIGH);<br \/>\n&nbsp; delayMicroseconds(10);<br \/>\n&nbsp; digitalWrite(PIN_TRIG, LOW);<\/p>\n<p>&nbsp; long duration = pulseIn(PIN_ECHO, HIGH, 30000);&nbsp; \/\/ 30 ms timeout<br \/>\n&nbsp; if (duration == 0) return -1.0;<br \/>\n&nbsp; return (duration \/ 2.0) \/ 29.1;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ \u00b5s\u2192cm<br \/>\n}<\/p>\n<p>\/\/ calibrateBaseline(): averages MAX_READS valid readings for baseline<br \/>\nfloat calibrateBaseline() {<br \/>\n&nbsp; float sum = 0;<br \/>\n&nbsp; uint8_t count = 0;<br \/>\n&nbsp; while (count &lt; MAX_READS) {<br \/>\n&nbsp;&nbsp;&nbsp; float d = measureDistance();<br \/>\n&nbsp;&nbsp;&nbsp; if (d &gt; 0) {<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sum += d;<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; count++;<br \/>\n&nbsp;&nbsp;&nbsp; }<br \/>\n&nbsp;&nbsp;&nbsp; delay(100);<br \/>\n&nbsp; }<br \/>\n&nbsp; return sum \/ count;<br \/>\n}<\/p>\n<p>\/\/ medianFilter(): simple insertion sort \u2192 returns middle element<br \/>\nfloat medianFilter(float *arr, uint8_t n) {<br \/>\n&nbsp; for (uint8_t i = 1; i &lt; n; i++) {<br \/>\n&nbsp;&nbsp;&nbsp; float key = arr[i];<br \/>\n&nbsp;&nbsp;&nbsp; int8_t j = i &#8211; 1;<br \/>\n&nbsp;&nbsp;&nbsp; while (j &gt;= 0 &amp;&amp; arr[j] &gt; key) {<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; arr[j + 1] = arr[j];<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; j&#8211;;<br \/>\n&nbsp;&nbsp;&nbsp; }<br \/>\n&nbsp;&nbsp;&nbsp; arr[j + 1] = key;<br \/>\n&nbsp; }<br \/>\n&nbsp; return arr[n \/ 2];<br \/>\n}<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Signal-Processing Tip<br>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.<\/p>\n\n\n<p>\/*<br \/>\n&nbsp; Biofloc Ultrasonic Catfish Counter<br \/>\n&nbsp; Pond depth: 1 m, biofloc ~50 mL\/L, heavy aeration<\/p>\n<p>&nbsp; Hardware:<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 HC-SR04 or equivalent narrow-beam ultrasonic module<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 Mounted across a 6\u20138 cm flume channel (PVC\/acrylic)<br \/>\n&nbsp;&nbsp;&nbsp; \u2022 TRIG \u2192 D7, ECHO \u2192 D6, VCC \u2192 5 V, GND \u2192 GND<\/p>\n<p>&nbsp; Features:<br \/>\n&nbsp;&nbsp;&nbsp; 1) Automatic baseline calibration (MAX_READS samples)<br \/>\n&nbsp;&nbsp;&nbsp; 2) Detection bursts of PINGS readings \u2192 median filtering<br \/>\n&nbsp;&nbsp;&nbsp; 3) Dynamic threshold: DROP_CM below baseline<br \/>\n&nbsp;&nbsp;&nbsp; 4) DEBOUNCE_MS to avoid double-counts<br \/>\n&nbsp;&nbsp;&nbsp; 5) Runtime re-calibration every RECALIBRATE_S seconds<br \/>\n*\/<\/p>\n<p>#include &lt;Arduino.h&gt;<\/p>\n<p>\/\/\u2500\u2500 USER CONFIG \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\nconst uint8_t&nbsp; PIN_TRIG&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 7;<br \/>\nconst uint8_t&nbsp; PIN_ECHO&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 6;<\/p>\n<p>const uint8_t&nbsp; MAX_READS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 30;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ baseline samples<br \/>\nconst uint8_t&nbsp; PINGS&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 7;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ pings per detection burst<br \/>\nconst float&nbsp;&nbsp;&nbsp; DROP_CM&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; = 6.0;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ cm drop = fish event<br \/>\nconst uint16_t DEBOUNCE_MS&nbsp;&nbsp;&nbsp;&nbsp; = 250;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ ms ignore window<br \/>\nconst uint32_t RECALIBRATE_S&nbsp;&nbsp; = 600;&nbsp;&nbsp;&nbsp;&nbsp; \/\/ re-calibrate baseline every 10 min<\/p>\n<p>\/\/\u2500\u2500 RUNTIME STATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\nfloat&nbsp;&nbsp; baselineDist&nbsp;&nbsp; = 0.0;<br \/>\nfloat&nbsp;&nbsp; detectThresh&nbsp;&nbsp; = 0.0;<br \/>\nuint32_t fishCount&nbsp;&nbsp;&nbsp;&nbsp; = 0;<br \/>\nuint32_t lastFishTS&nbsp;&nbsp;&nbsp; = 0;<br \/>\nuint32_t lastRecalTS&nbsp;&nbsp; = 0;<\/p>\n<p>\/\/\u2500\u2500 SETUP \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\nvoid setup() {<br \/>\n&nbsp; Serial.begin(115200);<br \/>\n&nbsp; pinMode(PIN_TRIG, OUTPUT);<br \/>\n&nbsp; pinMode(PIN_ECHO, INPUT);<\/p>\n<p>&nbsp; Serial.println(F(&#8220;\\n\u2014 Calibrating baseline distance \u2014&#8221;));<br \/>\n&nbsp; baselineDist = calibrateBaseline();<br \/>\n&nbsp; detectThresh = baselineDist &#8211; DROP_CM;<br \/>\n&nbsp; lastRecalTS&nbsp; = millis();<\/p>\n<p>&nbsp; Serial.printf(&#8220;Baseline: %.1f cm&nbsp; \u2794&nbsp; Threshold: &lt; %.1f cm\\n&#8221;,<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; baselineDist, detectThresh);<br \/>\n&nbsp; Serial.println(F(&#8220;\u2014 START COUNTING \u2014\\n&#8221;));<br \/>\n}<\/p>\n<p>\/\/\u2500\u2500 LOOP \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\nvoid loop() {<br \/>\n&nbsp; uint32_t now = millis();<\/p>\n<p>&nbsp; \/\/ Periodic baseline re-calibration<br \/>\n&nbsp; if (now &#8211; lastRecalTS &gt;= RECALIBRATE_S * 1000UL) {<br \/>\n&nbsp;&nbsp;&nbsp; baselineDist = calibrateBaseline();<br \/>\n&nbsp;&nbsp;&nbsp; detectThresh = baselineDist &#8211; DROP_CM;<br \/>\n&nbsp;&nbsp;&nbsp; lastRecalTS&nbsp; = now;<br \/>\n&nbsp;&nbsp;&nbsp; Serial.printf(&#8220;Re-calibrated baseline: %.1f cm&nbsp; Thresh: &lt; %.1f cm\\n&#8221;,<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; baselineDist, detectThresh);<br \/>\n&nbsp; }<\/p>\n<p>&nbsp; \/\/ 1) Burst pings \u2192 store readings<br \/>\n&nbsp; float buf[PINGS];<br \/>\n&nbsp; for (uint8_t i = 0; i &lt; PINGS; i++) {<br \/>\n&nbsp;&nbsp;&nbsp; buf[i] = measureDist();<br \/>\n&nbsp;&nbsp;&nbsp; delay(20);&nbsp; \/\/ 50 Hz ping burst<br \/>\n&nbsp; }<\/p>\n<p>&nbsp; \/\/ 2) Median filter to reject bubbles\/floc<br \/>\n&nbsp; float med = median(buf, PINGS);<\/p>\n<p>&nbsp; \/\/ 3) Fish detection + debounce<br \/>\n&nbsp; if (med &gt; 0 &amp;&amp; med &lt; detectThresh &amp;&amp; (now &#8211; lastFishTS &gt; DEBOUNCE_MS)) {<br \/>\n&nbsp;&nbsp;&nbsp; fishCount++;<br \/>\n&nbsp;&nbsp;&nbsp; lastFishTS = now;<br \/>\n&nbsp;&nbsp;&nbsp; Serial.printf(&#8220;Fish #%lu @ %.2f s\\n&#8221;,<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; fishCount, now \/ 1000.0f);<br \/>\n&nbsp; }<\/p>\n<p>&nbsp; delay(100);<br \/>\n}<\/p>\n<p>\/\/\u2500\u2500 MEASURE DISTANCE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\n\/\/ Sends one HC-SR04 ping, returns distance in cm or -1 on timeout<br \/>\nfloat measureDist() {<br \/>\n&nbsp; digitalWrite(PIN_TRIG, LOW);<br \/>\n&nbsp; delayMicroseconds(2);<br \/>\n&nbsp; digitalWrite(PIN_TRIG, HIGH);<br \/>\n&nbsp; delayMicroseconds(10);<br \/>\n&nbsp; digitalWrite(PIN_TRIG, LOW);<\/p>\n<p>&nbsp; long us = pulseIn(PIN_ECHO, HIGH, 30000);&nbsp; \/\/ 30 ms timeout<br \/>\n&nbsp; if (us == 0) return -1.0;<br \/>\n&nbsp; return (us \/ 2.0) \/ 29.1;<br \/>\n}<\/p>\n<p>\/\/\u2500\u2500 BASELINE CALIBRATION \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\n\/\/ Averages MAX_READS valid pings for a stable baseline<br \/>\nfloat calibrateBaseline() {<br \/>\n&nbsp; float sum = 0;<br \/>\n&nbsp; uint8_t cnt = 0;<br \/>\n&nbsp; while (cnt &lt; MAX_READS) {<br \/>\n&nbsp;&nbsp;&nbsp; float d = measureDist();<br \/>\n&nbsp;&nbsp;&nbsp; if (d &gt; 0) {<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; sum += d;<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; cnt++;<br \/>\n&nbsp;&nbsp;&nbsp; }<br \/>\n&nbsp;&nbsp;&nbsp; delay(100);<br \/>\n&nbsp; }<br \/>\n&nbsp; return sum \/ cnt;<br \/>\n}<\/p>\n<p>\/\/\u2500\u2500 MEDIAN FILTER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br \/>\n\/\/ Simple insertion sort + return middle value<br \/>\nfloat median(float *arr, uint8_t n) {<br \/>\n&nbsp; for (uint8_t i = 1; i &lt; n; i++) {<br \/>\n&nbsp;&nbsp;&nbsp; float key = arr[i];<br \/>\n&nbsp;&nbsp;&nbsp; int8_t j = i &#8211; 1;<br \/>\n&nbsp;&nbsp;&nbsp; while (j &gt;= 0 &amp;&amp; arr[j] &gt; key) {<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; arr[j+1] = arr[j];<br \/>\n&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; j&#8211;;<br \/>\n&nbsp;&nbsp;&nbsp; }<br \/>\n&nbsp;&nbsp;&nbsp; arr[j+1] = key;<br \/>\n&nbsp; }<br \/>\n&nbsp; return arr[n\/2];<br \/>\n}<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It:<br>\u2022 Calibrates a dynamic baseline to the back-wall reflector&nbsp; <br>\u2022 Fires bursts of PINGS ultrasonic readings, applies a median filter to reject bubble\/floc spikes&nbsp; <br>\u2022 Flags a \u201cfish event\u201d when the median drops by DROP_CM&nbsp; <br>\u2022 Debounces so you get exactly one count per crossing&nbsp; <br>\u2022 Logs \u201cFish #n @ t s\u201d to Serial (swap for SD\/MQTT as needed)&nbsp; <\/p>\n","protected":false},"excerpt":{"rendered":"<p>Signal-Processing Tip Signal-Processing Tip Here\u2019s a turnkey Arduino sketch for counting catfish passing single-file through a 6\u20138 cm flume, optimized for a 1 m-deep biofloc pond (\u224850 mL\/L solids) with&hellip;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2],"tags":[],"class_list":["post-344","post","type-post","status-publish","format-standard","hentry","category-bioflok"],"_links":{"self":[{"href":"https:\/\/www.eatmaja.co.id\/eka\/wp-json\/wp\/v2\/posts\/344","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.eatmaja.co.id\/eka\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.eatmaja.co.id\/eka\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.eatmaja.co.id\/eka\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.eatmaja.co.id\/eka\/wp-json\/wp\/v2\/comments?post=344"}],"version-history":[{"count":0,"href":"https:\/\/www.eatmaja.co.id\/eka\/wp-json\/wp\/v2\/posts\/344\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.eatmaja.co.id\/eka\/wp-json\/wp\/v2\/media?parent=344"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.eatmaja.co.id\/eka\/wp-json\/wp\/v2\/categories?post=344"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.eatmaja.co.id\/eka\/wp-json\/wp\/v2\/tags?post=344"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}