• Models
  • Contests
  • Slicer
  • Login
  • Start Here
    thingiverse-iconprintables-iconcults3d-iconmakerworld-iconmyminifactory-icon

    3D GO

    3D ModelsContestsCollectionsSaved ModelsOn a mobile device?

3D GO

Privacy Policy
30-Day ESP8266 Fish Feeder for aquarium 3D Printer File Image 1
30-Day ESP8266 Fish Feeder for aquarium 3D Printer File Image 2
30-Day ESP8266 Fish Feeder for aquarium 3D Printer File Image 3
30-Day ESP8266 Fish Feeder for aquarium 3D Printer File Image 4
30-Day ESP8266 Fish Feeder for aquarium 3D Printer File Image 5
30-Day ESP8266 Fish Feeder for aquarium 3D Printer File Thumbnail 1
30-Day ESP8266 Fish Feeder for aquarium 3D Printer File Thumbnail 2
30-Day ESP8266 Fish Feeder for aquarium 3D Printer File Thumbnail 3
30-Day ESP8266 Fish Feeder for aquarium 3D Printer File Thumbnail 4
30-Day ESP8266 Fish Feeder for aquarium 3D Printer File Thumbnail 5

30-Day ESP8266 Fish Feeder for aquarium

Rospakos avatarRospakos

July 11, 2025

makerworld-icon
DescriptionCommentsTags

Description

V2 of the project is HERE 

V2 OF THE CODE IS INCLUDED BELLOW. You need to reflash re html too, it is slightly changed

 v1 also left here for comparison purposes. 

🐟 30-Day ESP8266 Fish Feeder

— Cheap, Functional, and Done
by rospakos
*(Engineer, not a linguist — most of this was written with ChatGPT)

💡 What Is This?

A fully 3D-printed automatic fish feeder that reliably drops food every set number of hours and minutes — powered by a €2 stepper motor and an ESP8266.

It rotates a tray with 31 slots (good for a full month of feeding) using 66 steps per rotation, controlled from a simple web interface.

🧪 100% functional. 0% fancy.

I clipped it to my tank, filled it using a funnel I found on MakerWorld (not mine, but it fits the tray slots perfectly), LINK  and it’s been running for weeks without issues.

⚙️ Features

🕹️ Fully adjustable feeding interval (hours & minutes)
🔄 31-slot tray, rotates 66 steps per feed
📶 Wi-Fi hotspot interface — no app, no internet
📱 Mobile control panel
⏩ Manual step buttons (1, 5, 10, 20 forward/backward)
📊 Status monitor:
 → Last fed time
 → Time since last feed
 → Time until next feed
🔥 Auto coil shutdown after 3s (reduces heating)
♻️ Reset button — returns to default (24h, 66 steps)
⚡ Power-loss safe — auto-starts feeding every 24h on reboot

 

🧠 Why I Built It

Because the cheapest commercial feeder that could do something similar cost over €50, and I had an ESP and stepper motor lying around.

Also — I like functional ugly things that just do the job.

This isn’t perfect. But it worked on the first try, and I didn’t feel like improving it because… well, it just works.

I’ll probably never touch it again — unless someone messages me for help, then I might actually do something.

 

🖼️ Construction

The feeder has a rotating tray with 31 food slots and 2mm walls between them. The tray fits directly onto the stepper shaft with a tight fit, and the rest of the structure clips onto your aquarium edge.

Everything was printed on a Bambu A1 Mini using PLA. No supports, no fancy materials — just basic, fast prints.

Some remarks:

  • 🧩 The lid is sandwiched between the stepper motor and the cross-shaped support part — no fasteners needed.
  • 🔁 The center pin is not just for looks — it helps the carousel rotate smoothly. Don’t glue it — it needs to spin!
  • 👀 The lid could be printed in transparent filament to see the food inside…
    But honestly? I was too lazy to load the AMS, so I just went with whatever was already loaded. Meh, it works

No screws. No unnecessary hardware. Just clever tolerances and a drop of glue on the feet (okay, you got me).

🧰 What You’ll Need

🧠 ESP8266 NodeMCU V3
⚙️ 28BYJ-48 stepper + ULN2003 driver
🔌 5V power supply (≥500mA)
🖨️ 3D-printed parts:
 → Tray, carousel, mount, lid
 → (Optional) Funnel from another MakerWorld user (linked it — works great)
💻 Arduino IDE with ESP libraries

⚠️ Electronics box?
Right now it’s all open and wired — it works.
I’ll probably print a small box someday… but I’m an engineer, and this is functional enough for now 😅

🔌 Wiring

ESP8266 PinULN2003 IN#
D1IN1
D2IN3
D3IN2
D4IN4

 

(Matching this wiring ensures smooth stepping.)

I also connected the Vcc and Ground from the motor driver to the VV and G of the ESP. Common ground is important, ask me how I know :D

🌐 Web Interface

Your ESP boots as a Wi-Fi hotspot:

  • SSID: FishFeeder
  • Password: fishfeed123
  • Access: http://192.168.4.1

From there, you can:

🐟 Feed Now
▶️ Start Feeding
⏹️ Stop Feeding
⏩ Manual Step Control (fwd/back: 1–20 steps)
🕒 Set Feed Interval (in hours & minutes)
🔁 Reset Feeder
📊 View Live Status:

  • Last feed time
  • Time since / until next
  • Current settings

⚡ What Happens If Power Cuts?

It reboots into default mode:
✔️ Starts a new countdown to feed in 24h
✔️ Keeps feeding every 24h after that

So your fish won’t be left hungry.

🧾 Setup & Code

📦 Libraries Needed (Arduino IDE):

  • ESP8266WiFi
  • ESP8266WebServer
  • Stepper
  • FS / SPIFFS

🛠️ Setup Steps:

  1. Flash the .ino to ESP
  2. Upload index.html to SPIFFS
  3. Plug it in
  4. Done 🎉

🧠 Code Overview

It’s split into two parts:

  • fishfeeder.ino — handles logic, scheduling, motor control
  • index.html — the mobile-friendly interface

Yes, ChatGPT helped make it cleaner — but the logic, design, and testing are mine.

 

🎯 Final Thoughts

  • 💸 Costs under €5
  • 🖨️ Fully printed in PLA (no supports)
  • 🐢 Works slowly but surely — like the fish it feeds

If you have a different glass thickness, motor, or idea — remix it or message me.
I probably won’t upgrade this unless someone asks.
But if this saves your fish from starvation while you're on holiday, I’d call that a win.

❤️ Drop a like if it helps.
🔁 Remix it if you want.


🔌 Engineer, not artist — but hey, it works.

Boost MeBoost if you liked the model. Helps buying some extra filament

THIS is the new version of the code that eliminates the motor lagging back soimetimes + a bit of a changed UI

 

🧾 fishfeeder.ino

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <Stepper.h>
#include <FS.h>

// Stepper setup (28BYJ-48 with ULN2003)
const int stepsPerRevolution = 2048;
Stepper myStepper(stepsPerRevolution, D1, D3, D2, D4); // IN1 IN2 IN3 IN4

// Feeder settings (defaults)
int stepCount = 66;
int intervalHours = 24;
int intervalMinutes = 0;
bool active = true; // auto-start after power-up

unsigned long lastFeedTime = 0;    // millis() of last feed completion
unsigned long intervalMillis = 24UL * 3600000UL;

// Physical feed button
const int feedButtonPin = D5; // wire button between D5 and GND
const unsigned long buttonDebounceMs = 300;

// Coil auto-disable
unsigned long lastMoveTime = 0;
const unsigned long coilTimeout = 3000; // 3 seconds
bool coilsActive = false;

// Feeding flag
bool feeding = false;

ESP8266WebServer server(80);

// ----- Helper: enable/disable coils -----
void enableStepper() {
 coilsActive = true;
 lastMoveTime = millis();
}

void disableStepper() {
 digitalWrite(D1, LOW);
 digitalWrite(D2, LOW);
 digitalWrite(D3, LOW);
 digitalWrite(D4, LOW);
 coilsActive = false;
}

// ----- Vibration / jerk trick -----
void vibrationTrick() {
 delay(500);  // wait before starting
 myStepper.setSpeed(3);
 for (int i = 0; i < 3; i++) {
   myStepper.step(20);   // forward
   delay(500);
   myStepper.step(-40);  // backward
   delay(500);
   myStepper.step(20);   // forward
   delay(500);
 }
}

// ----- Feed action -----
void doFeed() {
 if (feeding) return; // prevent re-entry
 feeding = true;

 enableStepper();
 myStepper.setSpeed(3);
 myStepper.step(stepCount);
 vibrationTrick();
 lastMoveTime = millis();

 feeding = false;
}

// ----- Web endpoints -----
void handleRoot() {
 File f = SPIFFS.open("/index.html", "r");
 if (!f) {
   server.send(500, "text/plain", "Missing index.html");
   return;
 }
 server.streamFile(f, "text/html");
 f.close();
}

void handleFeed() {
 doFeed();              // feed once
 lastFeedTime = millis();
 server.send(200, "text/plain", "Fed");
}

void handleStart() {
 active = true;
 lastFeedTime = millis();
 server.send(200, "text/plain", "Started");
}

void handleStop() {
 active = false;
 server.send(200, "text/plain", "Stopped");
}

void handleStep() {
 String dir = server.arg("dir");
 int count = server.hasArg("count") ? server.arg("count").toInt() : 1;
 if (count < 1 || count > 200) count = 1;
 enableStepper();
 myStepper.setSpeed(3);
 myStepper.step((dir == "fwd" ? 1 : -1) * count);
 lastMoveTime = millis();
 server.send(200, "text/plain", "Stepped " + String(count));
}

void handleSet() {
 if (server.hasArg("intervalH")) intervalHours = server.arg("intervalH").toInt();
 if (server.hasArg("intervalM")) intervalMinutes = server.arg("intervalM").toInt();
 if (server.hasArg("steps")) stepCount = server.arg("steps").toInt();

 intervalMillis = (unsigned long)intervalHours * 3600000UL + (unsigned long)intervalMinutes * 60000UL;
 server.send(200, "text/plain", "Settings updated");
}

void handleReset() {
 intervalHours = 24;
 intervalMinutes = 0;
 stepCount = 66;
 intervalMillis = 24UL * 3600000UL;
 active = true;
 lastFeedTime = millis();
 server.send(200, "text/plain", "Reset to defaults");
}

void handleStatus() {
 unsigned long now = millis();
 if (lastFeedTime == 0) lastFeedTime = now;

 unsigned long elapsedSec = (now - lastFeedTime) / 1000;
 unsigned long nextFeedSec = 0;
 if (active) {
   unsigned long since = now - lastFeedTime;
   if (intervalMillis > since) nextFeedSec = (intervalMillis - since) / 1000;
   else nextFeedSec = 0;
 }

 String json = "{";
 json += "\"active\":" + String(active ? "true" : "false") + ",";
 json += "\"sinceSeconds\":" + String(elapsedSec) + ",";
 json += "\"nextSeconds\":" + String(nextFeedSec) + ",";
 json += "\"intervalH\":" + String(intervalHours) + ",";
 json += "\"intervalM\":" + String(intervalMinutes) + ",";
 json += "\"steps\":" + String(stepCount);
 json += "}";
 server.send(200, "application/json", json);
}

// ----- WiFi AP -----
void setupWiFi() {
 WiFi.mode(WIFI_AP);
 WiFi.softAP("FishFeeder", "fishfeed123");
 Serial.println("WiFi started. Connect to: http://192.168.4.1");
}

// ----- setup -----
void setup() {
 Serial.begin(115200);
 setupWiFi();

 if (!SPIFFS.begin()) {
   Serial.println("SPIFFS mount failed");
 }

 pinMode(D1, OUTPUT);
 pinMode(D2, OUTPUT);
 pinMode(D3, OUTPUT);
 pinMode(D4, OUTPUT);
 pinMode(feedButtonPin, INPUT_PULLUP);
 disableStepper();

 server.on("/", handleRoot);
 server.on("/feed", handleFeed);
 server.on("/start", handleStart);
 server.on("/stop", handleStop);
 server.on("/step", handleStep);
 server.on("/set", handleSet);
 server.on("/reset", handleReset);
 server.on("/status.json", handleStatus);
 server.begin();
 Serial.println("Server started");

 if (lastFeedTime == 0) lastFeedTime = millis();
 intervalMillis = (unsigned long)intervalHours * 3600000UL + (unsigned long)intervalMinutes * 60000UL;
}

// ----- Physical button handler -----
void handlePhysicalButton() {
 static bool lastButtonState = HIGH;
 static unsigned long lastPressTime = 0;

 bool currentState = digitalRead(feedButtonPin);
 unsigned long now = millis();

 // Falling edge with debounce
 if (lastButtonState == HIGH && currentState == LOW && (now - lastPressTime) > buttonDebounceMs) {
   lastPressTime = now;
   Serial.println("Physical button pressed -> triggering UI feed");
   handleFeed(); // Call the exact same function as the web button
 }

 lastButtonState = currentState;
}

// ----- main loop -----
void loop() {
 server.handleClient();

 // Check physical button
 handlePhysicalButton();

 unsigned long now = millis();

 // Automatic scheduled feeding
 if (active && !feeding && (now - lastFeedTime >= intervalMillis)) {
   doFeed();
   lastFeedTime = millis();
 }

 // Auto-disable coils
 if (coilsActive && (now - lastMoveTime > coilTimeout)) {
   disableStepper();
 }
}

 

 

🌐 index.html

<!DOCTYPE html>
<html>
<head>
 <meta charset="UTF-8" />
 <meta name="viewport" content="width=device-width, initial-scale=1" />
 <title>Fish Feeder</title>
 <style>
   body { font-family: sans-serif; text-align: center; padding: 12px; background: #f8fafc; color: #111; }
   h1 { margin-bottom: 6px; }
   button { display:block; margin:8px auto; padding:12px 20px; font-size:16px; background:#0b78d1; color:#fff; border:none; border-radius:8px; cursor:pointer; max-width:300px; width:90%; }
   .row-buttons { display:flex; justify-content:center; gap:8px; flex-wrap:wrap; margin:8px 0; }
   .row-buttons button { flex:1 1 40%; min-width:80px; }
   input { padding:8px; margin:5px; font-size:15px; width:110px; }
   .status { margin-top:18px; font-size:15px; }
   .small { font-size:12px; color:#555; }
 </style>
</head>
<body>
 <h1>🐟 Fish Feeder</h1>

 <button onclick="fetchAction('/feed', 'Fed')">Feed Now</button>
 <button onclick="fetchAction('/start', 'Auto feeding started')">Start Feeding</button>
 <button onclick="fetchAction('/stop', 'Auto feeding stopped')">Stop Feeding</button>
 <button onclick="fetchAction('/reset', 'Reset to defaults')">Reset Feeder</button>

 <h3>Step Backward</h3>
 <div class="row-buttons">
   <button onclick="step('rev',1)">⬅️ 1</button>
   <button onclick="step('rev',5)">⬅️ 5</button>
   <button onclick="step('rev',10)">⬅️10</button>
   <button onclick="step('rev',20)">⬅️20</button>
 </div>

 <h3>Step Forward</h3>
 <div class="row-buttons">
   <button onclick="step('fwd',1)">➡️ 1</button>
   <button onclick="step('fwd',5)">➡️ 5</button>
   <button onclick="step('fwd',10)">➡️10</button>
   <button onclick="step('fwd',20)">➡️20</button>
 </div>

 <h3>Settings</h3>
 <div>
   <input id="intervalH" type="number" placeholder="Hours" min="0" />
   <input id="intervalM" type="number" placeholder="Minutes" min="0" max="59" />
 </div>
 <div>
   <input id="steps" type="number" placeholder="Steps" min="1" />
   <button onclick="setValues()">Set</button>
 </div>

 <div class="status" id="status">Loading status...</div>

 <script>
   function fetchAction(url, msg) {
     fetch(url).then(r => r.text()).then(() => { alert(msg); updateStatus(); }).catch(()=>alert('Error contacting feeder'));
   }

   function step(dir,count) {
     fetch(`/step?dir=${dir}&count=${count}`)
       .then(()=>{ alert('Stepped'); updateStatus(); })
       .catch(()=>alert('Step failed'));
   }

   function setValues() {
     const h = document.getElementById('intervalH').value || 0;
     const m = document.getElementById('intervalM').value || 0;
     const s = document.getElementById('steps').value || 66;
     fetch(`/set?intervalH=${h}&intervalM=${m}&steps=${s}`)
       .then(()=>{ alert('Settings updated'); updateStatus(); })
       .catch(()=>alert('Failed to update settings'));
   }

   function formatHHMMSS(totalSeconds) {
     if (isNaN(totalSeconds) || totalSeconds < 0) totalSeconds = 0;
     const h = Math.floor(totalSeconds / 3600);
     const m = Math.floor((totalSeconds % 3600) / 60);
     const s = Math.floor(totalSeconds % 60);
     return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
   }

   function updateStatus() {
     fetch('/status.json')
       .then(res => res.json())
       .then(data => {
         const since = formatHHMMSS(data.sinceSeconds);
         const next = formatHHMMSS(data.nextSeconds);
         document.getElementById('status').innerHTML = `
           <b>Status:</b> ${data.active ? '🟢 Active' : '🔴 Stopped'}<br>
           <b>Since last feed:</b> ${since} ago<br>
           <b>Next feed in:</b> ${next}<br>
           <b>Interval:</b> ${data.intervalH}h ${data.intervalM}m<br>
           <b>Step count:</b> ${data.steps}
         `;
       })
       .catch(()=>{ document.getElementById('status').innerText = 'Failed to load status'; });
   }

   updateStatus();
   setInterval(updateStatus, 8000);
 </script>
</body>
</html>
 

 

 

 

BELLOW IS THE OLD VERSION OF THE CODE. USE THIS ONE IF YOU ENCOUTER PROBLEMS WITH THE NEW ONE 

🧾 fishfeeder.ino

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <Stepper.h>
#include <FS.h>

// Stepper setup
const int stepsPerRevolution = 2048;
Stepper myStepper(stepsPerRevolution, D1, D3, D2, D4); // IN1 IN2 IN3 IN4

// Feeder settings
int stepCount = 66;
int intervalHours = 24;
int intervalMinutes = 0;

bool active = false;
unsigned long lastFeedTime = 0;
unsigned long intervalMillis = 24UL * 3600000UL;

unsigned long lastStepTime = 0;
bool stepperEnabled = false;

ESP8266WebServer server(80);

// Feed the fish
void doFeed() {
 if (!active) return;
 enableStepper();
 myStepper.setSpeed(3); // ~2s for 66 steps
 myStepper.step(stepCount);
 lastFeedTime = millis();
 lastStepTime = millis();
}

// Enable motor coils
void enableStepper() {
 stepperEnabled = true;
 lastStepTime = millis();
}

// Disable coils by setting outputs to LOW
void disableStepper() {
 digitalWrite(D1, LOW);
 digitalWrite(D2, LOW);
 digitalWrite(D3, LOW);
 digitalWrite(D4, LOW);
 stepperEnabled = false;
}

// Convert seconds to hh:mm:ss string
String formatTime(unsigned long seconds) {
 int h = seconds / 3600;
 int m = (seconds % 3600) / 60;
 int s = seconds % 60;
 char buf[16];
 sprintf(buf, "%02d:%02d:%02d", h, m, s);
 return String(buf);
}

// Web handlers
void handleRoot() {
 File f = SPIFFS.open("/index.html", "r");
 if (!f) {
   server.send(500, "text/plain", "Missing index.html");
   return;
 }
 server.streamFile(f, "text/html");
 f.close();
}

void handleFeed() {
 doFeed();
 server.send(200, "text/plain", "Fed");
}

void handleStart() {
 active = true;
 lastFeedTime = millis();
 server.send(200, "text/plain", "Started");
}

void handleStop() {
 active = false;
 server.send(200, "text/plain", "Stopped");
}

void handleStep() {
 String dir = server.arg("dir");
 int count = server.hasArg("count") ? server.arg("count").toInt() : 1;
 if (count < 1 || count > 200) count = 1;

 enableStepper();
 myStepper.setSpeed(3);
 myStepper.step((dir == "fwd" ? 1 : -1) * count);
 lastStepTime = millis();
 server.send(200, "text/plain", "Stepped " + String(count));
}

void handleSet() {
 if (server.hasArg("intervalH")) intervalHours = server.arg("intervalH").toInt();
 if (server.hasArg("intervalM")) intervalMinutes = server.arg("intervalM").toInt();
 if (server.hasArg("steps")) stepCount = server.arg("steps").toInt();

 intervalMillis = (intervalHours * 3600000UL) + (intervalMinutes * 60000UL);
 server.send(200, "text/plain", "Settings updated");
}

void handleReset() {
 intervalHours = 24;
 intervalMinutes = 0;
 stepCount = 66;
 intervalMillis = 24UL * 3600000UL;
 lastFeedTime = millis();
 server.send(200, "text/plain", "Feeder reset");
}

void handleStatus() {
 unsigned long now = millis();
 unsigned long sinceFeed = (now - lastFeedTime) / 1000;
 unsigned long nextFeedIn = active ? (intervalMillis > (now - lastFeedTime) ? (intervalMillis - (now - lastFeedTime)) / 1000 : 0) : 0;
 time_t lastFeedUnix = (now / 1000) - sinceFeed; // Approximate

 String json = "{";
 json += "\"active\":" + String(active ? "true" : "false") + ",";
 json += "\"lastFeedUnix\":" + String(lastFeedUnix) + ",";
 json += "\"sinceFeed\":" + String(sinceFeed) + ",";
 json += "\"nextFeedSeconds\":" + String(nextFeedIn) + ",";
 json += "\"intervalHours\":" + String(intervalHours) + ",";
 json += "\"intervalMinutes\":" + String(intervalMinutes) + ",";
 json += "\"steps\":" + String(stepCount);
 json += "}";

 server.send(200, "application/json", json);
}

// Wi-Fi AP mode
void setupWiFi() {
 WiFi.mode(WIFI_AP);
 WiFi.softAP("FishFeeder", "fishfeed123");
 Serial.println("WiFi started. Connect to: http://192.168.4.1");
}

void setup() {
 Serial.begin(115200);
 setupWiFi();

 if (!SPIFFS.begin()) {
   Serial.println("SPIFFS mount failed");
   return;
 }

 pinMode(D1, OUTPUT);
 pinMode(D2, OUTPUT);
 pinMode(D3, OUTPUT);
 pinMode(D4, OUTPUT);
 disableStepper();

 server.on("/", handleRoot);
 server.on("/feed", handleFeed);
 server.on("/start", handleStart);
 server.on("/stop", handleStop);
 server.on("/step", handleStep);
 server.on("/set", handleSet);
 server.on("/reset", handleReset);
 server.on("/status.json", handleStatus);

 server.begin();
 Serial.println("Server started");

 // Auto start feeding on boot
 active = true;
 lastFeedTime = millis();

 intervalMillis = (intervalHours * 3600000UL) + (intervalMinutes * 60000UL);
}

void loop() {
 server.handleClient();

 if (active && millis() - lastFeedTime >= intervalMillis) {
   doFeed();
 }

 if (stepperEnabled && millis() - lastStepTime > 3000) {
   disableStepper();
 }
}
 

 

🌐 index.html

 

<!DOCTYPE html>
<html>
<head>
 <meta charset="UTF-8" />
 <meta name="viewport" content="width=device-width, initial-scale=1" />
 <title>Fish Feeder</title>
 <style>
   body {
     font-family: sans-serif;
     text-align: center;
     padding: 15px;
     background: #f5f5f5;
   }
   h1 {
     margin-bottom: 10px;
   }
   button {
     display: block;
     margin: 8px auto;
     padding: 14px 24px;
     font-size: 18px;
     width: 90%;
     max-width: 300px;
     background-color: #007BFF;
     color: white;
     border: none;
     border-radius: 8px;
     cursor: pointer;
   }
   button:active {
     background-color: #0056b3;
   }
   .row-buttons {
     display: flex;
     justify-content: center;
     flex-wrap: wrap;
     gap: 10px;
     margin: 10px 0;
   }
   .row-buttons button {
     flex: 1 1 40%;
     min-width: 80px;
   }
   input {
     padding: 8px;
     margin: 5px;
     font-size: 16px;
     width: 100px;
   }
   .status {
     margin-top: 20px;
     font-size: 15px;
   }
 </style>
</head>
<body>

 <h1>🐠 Fish Feeder</h1>

 <button onclick="action('/feed', '✅ Fed Successfully!')">Feed Now</button>
 <button onclick="action('/start', '✅ Feeding Cycle Started')">Start Feeding</button>
 <button onclick="action('/stop', '🛑 Feeding Stopped')">Stop Feeding</button>
 <button onclick="action('/reset', '🔄 Feeder Reset')">Reset Feeder</button>

 <h3>Step Backward</h3>
 <div class="row-buttons">
   <button onclick="step('rev', 1)">⬅️ 1</button>
   <button onclick="step('rev', 5)">⬅️ 5</button>
   <button onclick="step('rev', 10)">⬅️ 10</button>
   <button onclick="step('rev', 20)">⬅️ 20</button>
 </div>

 <h3>Step Forward</h3>
 <div class="row-buttons">
   <button onclick="step('fwd', 1)">➡️ 1</button>
   <button onclick="step('fwd', 5)">➡️ 5</button>
   <button onclick="step('fwd', 10)">➡️ 10</button>
   <button onclick="step('fwd', 20)">➡️ 20</button>
 </div>

 <h3>Settings</h3>
 <input id="intervalH" type="number" placeholder="Hours" />
 <input id="intervalM" type="number" placeholder="Minutes" />
 <input id="steps" type="number" placeholder="Steps" />
 <button onclick="setValues()">Set</button>

 <div class="status" id="status">
   Loading status...
 </div>

 <script>
   function action(url, msg) {
     fetch(url)
       .then(r => r.text())
       .then(() => {
         alert(msg);
         updateStatus();
       })
       .catch(() => alert("⚠️ Error contacting feeder"));
   }

   function step(dir, count) {
     fetch(`/step?dir=${dir}&count=${count}`)
       .then(() => {
         alert(`✅ Stepped ${dir === 'fwd' ? 'forward' : 'backward'} ${count} steps`);
         updateStatus();
       })
       .catch(() => alert("⚠️ Step command failed"));
   }

   function setValues() {
     const h = document.getElementById("intervalH").value || 0;
     const m = document.getElementById("intervalM").value || 0;
     const s = document.getElementById("steps").value || 66;
     fetch(`/set?intervalH=${h}&intervalM=${m}&steps=${s}`)
       .then(() => alert("✅ Settings updated!"))
       .catch(() => alert("⚠️ Failed to send settings"));
   }

   function updateStatus() {
     fetch("/status.json")
       .then(res => res.json())
       .then(data => {
         const sinceFeed = formatSeconds(data.sinceFeed);
         const nextFeed = formatSeconds(data.nextFeedSeconds);
         const lastFeedTime = new Date(data.lastFeedUnix * 1000).toLocaleString();
         document.getElementById("status").innerHTML = `
           <b>Status:</b> ${data.active ? "🟢 Active" : "🔴 Stopped"}<br>
           <b>Last Fed:</b> ${lastFeedTime}<br>
           <b>Since Last Feed:</b> ${sinceFeed}<br>
           <b>Next Feed In:</b> ${nextFeed}<br>
           <b>Interval:</b> ${data.intervalHours}h ${data.intervalMinutes || 0}m<br>
           <b>Step Count:</b> ${data.steps}
         `;
       })
       .catch(() => {
         document.getElementById("status").innerHTML = "⚠️ Failed to load status";
       });
   }

   function formatSeconds(sec) {
     const h = Math.floor(sec / 3600);
     const m = Math.floor((sec % 3600) / 60);
     const s = sec % 60;
     return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
   }

   updateStatus();
   setInterval(updateStatus, 10000);
 </script>

</body>
</html>
 

 

 

 

 

License:

MakerWorld Exclusive License

Related Models

Digital Sundial preview image

Digital Sundial

Mojoptix profile image

Mojoptix

67,432

ratgdo32, v2.53i, v2.52i, v2.5i, v2.5, & v2.0 PCB Case / Enclosure preview image

ratgdo32, v2.53i, v2.52i, v2.5i, v2.5, & v2.0 PCB Case / Enclosure

samwiseg0 profile image

samwiseg0

277

NFC Sticker Spender mit integriertem Aufroller preview image

NFC Sticker Spender mit integriertem Aufroller

Yannmarch profile image

Yannmarch

433

Keychain / Smartphone Stand preview image

Keychain / Smartphone Stand

Shira profile image

Shira

36,003

Case for ESP32-C3 SuperMini and LD2410C preview image

Case for ESP32-C3 SuperMini and LD2410C

TheProjectWheel profile image

TheProjectWheel

247

NFC Tag Reader: ESP8266/32-C6/C3 Supermini + PN532 preview image

NFC Tag Reader: ESP8266/32-C6/C3 Supermini + PN532

Fabrice profile image

Fabrice

138

Dragon Door  Knocker preview image

Dragon Door Knocker

Shira profile image

Shira

19,630

Presence Sensor Case - ESP32 C3 Super Mini + LD2410C Radar mmWave Sensor for smart home preview image

Presence Sensor Case - ESP32 C3 Super Mini + LD2410C Radar mmWave Sensor for smart home

maggoh profile image

maggoh

212