When you have fish in an outdoor pond and have no one to feed them during your holidays, it is useful to have some kind of thing that does the job automatically and keeps you updated about its status and activities via the Internet, thus a Smart Fish Feeder.
This is the reason why I started this project to build an automatic fish feeder myself.
In the development process I made several prototype versions for the mechanical parts of the project and finally ended with the version described here. This mechanic solution could be improved or packaged differently. The electronics and the software developed for this project can however be used for all kinds of variations.
The basic idea for this project was simply to make something to turn automatically using a stepper motor controlled by an ARDUINO (in this project an ESP32 was chosen)
So, I started to make some sample 3D prints that could fit with a stepper motor that I already had available.
After some iterations and building everything together, the picture on the cover page became the final result of this project:
My ambitions were a little bit more than just turning a stepper motor at fixed intervals.
The design of the fish feeder is based on the following requirements:
- It should feed the fish at intervals that can be selected
- The amount of food to be given should also be changeable
- When the food storage is empty, a warning message should be sent by email
- After each feeding action an email should be sent to the user
- It should be able to operate in outdoor environments such as wind, rain or sun
- It should be working on solar power and report the charging status of the rechargeable 18650 battery.
- For connecting to the internet, a Wi-Fi access point (AP) should be present in the vicinity of the fish pond.
- An LED should show the Wi-Fi connection status
- It should have enough food storage for 3 weeks of operation
- There should be a function for manually operated feeding
Here you can watch the final result in action: https://youtu.be/yl0P2KJpHnk
In the following tutorial, you will read how this fish feeder has been built.
I wish you good reading, and I hope it is inspiring you to build something similar.
Step 1: System DesignThe Fish Feeder system design is shown in the below diagram:
The system, basically consists of the following main parts:
- The Solar panel with built-in 18650 battery
- An ESP32 microcontroller(MCU)
- The Stepper Motor driving the worm in a piece of pipe
- The silo storing the food with IR sensor
- Another 18650 battery with BMS to power the MCU and stepper motor
- Two Reed switches to enable manual feeding as well as resetting the system
- A blue LED for indicating the Wi-Fi connection status
All electronics for power supply to the MCU as well as stepper motor and its driver are built inside the fish feeder housing.
Since the system is powered by a solar panel, provisions have been made to save on battery energy. For instance, the system is put into a “deep sleep” mode with only the built-in real-time clock (RTC) working. The driving lines to the stepper motor are switched low after the stepper motor has done its job.
Measurements performed to the operational system show that during Deep sleep there is (still) a power consumption of 7 mA. During full operation with the stepper motor turning some 320 mA are drawn from the battery. The 7 mA in deep sleep are probably due to the on-board red LED of the ESP32.
A number of mechanical parts are 3D printed. The complete system is contained in an IKEA food storage box that is closed with some clamps on the cover.
After removing the cover, a power switch is accessible and a USB cable can be connected to the ESP32 for replacing the software with a new revision if needed.
A blue LED is installed to indicate the status of the Wi-Fi connection with the AP.
Step 2: Making the Fish Feeder2.1 Things usedThe following materials are used for the Fish Feeder: See Things
2.2 The Electronic DesignThe electronic design is shown in the following circuit diagram.
The power supply is provided by a solar panel with built-in 18650 battery. This battery is charged in daytime by solar energy, while at night the energy from this battery is transferred to another 18650 battery via a step-up convertor (from 3, 7 V to 5V. The battery management system from this battery provides 3 * 5V output at 2 A each and also 3 * 3, 3 V. One 5V output is used to supply the ESP32 (Vin) and another 5V output is used for the stepper motor driver. To monitor the charging status of the 18650 battery an analogue GPIO pin of the ESP32 is used to measure the voltage across a resistor divider network connected to the + of the 18650 battery. The reason for the resistor divider is that a GPIO pin of the ESP32 can accept a maximum of 3, 3 V, whereas the 18650 battery (nominal voltage 3, 7 V) at 100% charge provides 4, 2 V as output.
Above diagram has been made using EasyEDA.
The Microcontroller used in this project is the ESP32 (made by NodeMCU):
Note that the pin layout of the ESP32, as shown in above diagram is different from the reality. It has been modified to make the diagram more easy to comprehend. The real pin allocation is shown in below diagram:
The Stepper Motor and driver that have been used in this project are depicted below:
The worm construction for propelling the food inside a PVC tube has been designed with Fusion 360 and printed on a Creality CR10S Pro.
The connector to the stepper motor is also 3D printed and the “borehole” fits exactly with the shaft of the stepper motor.
3D design of worm construction connected to the stepper motor
The breadboard set up used for testing and building the software with the ARDUINO IDE, is shown in the following pictures:
Not visible is a 2A fuse, which has been mounted inside the Solar Panel.
The components from the breadboard are transferred onto a small (5 * 7 cm) single sided prototype board, which is then mounted on a piece of IKEA plastic cutting board:
An overview of all the electronic and mechanical and parts inside the transparent IKEA box is shown in the pictures below:
An opening is made in the box for a PVC tube to pass through (inside the PVC tube is the “worm-construction”. At the end of the PVC tube a 3D printed C-shaped pipe is connected for letting the food fall in the water.
The complete building plate has been cut out of the IKEA cutting board and made to fit in the IKEA box. The electronic components are also mounted on pieces of IKEA board.
For fixing the solar panel, a special adapter has been 3D designed and printed.
The food storage is provided by a 3D printed silo like construction that fits within the constraints of the IKEA box. The volume of the silo could be enlarged in a future design but requires a bigger box.
The two reed switches (NO, Normally Open) are mounted on each side of the transparent box so that they can be activated from the outside with a small magnet.
The IR TX and IR RX diodes are fitted in two 5mm holes in the 3D printed Silo. When there is still food in the silo the IR transmitted light is not detected by the IR receiver, so that the ESP32 GPIO pin “sees” a HIGH. If the food is finished the IR receiving diode is conducting and the signal at the GPIO pin is LOW.
The whole construction is mounted on an old lamp foot which is heavy enough to make the fish feeder able to withstand strong winds.
The following flowchart shows the high level design of the Fish Feeder software:
Flowchart made with ClickCharts of NCH Software
In the setup phase a number of functions are called: the WAKEUP_ACTION and the SEND_EMAIL function (in relation with the smtpCallback).
The “LOOP” function itself is empty and will not be used (not visible in the flowchart)
After the first time that the setup sequence has been executed, the ESP32 goes into DEEP SLEEP to save the battery energy.
The system can wake up for 3 reasons:
- By activating the manual feed switch
- After the set sleep time (in this case 86400 sec) has lapsed (the RTC in the ESP32 remains operational during deep sleep)
- When the reed switch RESET has been activated, the whole process starts from the beginning and the boot counter is reset to zero.
The Setup() function is further detailed in below picture:
The function WAKEUP_ACTION is further decomposed in the following flowchart:
Depending on one of three wakeup_reasons:
- Reset switch activated
- Manual Feed switch activated
- The set Time after previous activity lapsed (e.g. 24 hours)
a relevant textMsg is composed. This text message will be send after completion of the function WAKEUP_ACTION by the SEND_EMAIL () function.
Examples of the text messages that are composed, are listed below:
- The message that is send after the Reset switch has been activated:
- The kind of message that is send after the Manual feed switch has been activated and the food in the silo is finished:
- The message that is send after the wakeup is caused by the sleep timer, when, after feeding, there is still food available in the silo (as detected by the IR sensor:
The complete ARDUINO sketch for the Fish Feeder comprises 341 lines of code. The complete listing is included as part of this tutorial, the ARDUINO code is provided with an ample number of comments, which should help in understanding the programme.
Throughout the Sketch a lot of print commands are included in order to help during the development and to be able to monitor the process on the Serial Monitor of the Arduino IDE (running on the PC).
An example of the messages that appear on the Serial Monitor is listed below:
The yellow marked text is user and application specific and depending in the specific user settings in the code.
2.5 Aspects that require further clarificationSome parts of the Sketch deserve some additional clarifications. This is done in the paragraphs below, with the relevant lines of the code given between brackets.
2.5.1 The boot counterThe boot counter is a variable that keeps track of the number of times that the setup() function (line 125) has been passed since a system reset.
The bootcounter is initialised in (line 82) with RTC_DATA_ATTR int bootCount = 0; After each hard RESET the boot counter will be reset to 0.
Each time the setup() function is activated the integer bootCount is increased by 1 (line 136). The applicable boot number is comprised in each applicable email that is send with the SEND_EMAIL () function (line 195 and 288 – 340). This is done by means of the code: ", Boot number: " + String(bootCount) (in lines 227 – 233)
2.5.2 How to measure the lapsed timeThe time between two Fish Feeder activities is computed by getting time information (via Wi-Fi) from a NTP (Network Time Protocol) server on the Internet using the Library <NTPClient.h> (line 70 - 73). In this particular Sketch from: "nl.pool.ntp.org". For various countries specific NTP servers exist. More information can be found on https://www.ntppool.org .
The relevant time and date information is obtained from the NTP server (lines 160 – 180). Of particular interest is the so-called EpochTime (line 173). This is the time elapsed in seconds since 00:00:00 UTC on 1 January 1970 at the moment the information is taken from the NTP server.
Every time an activity of the Fish Feeder takes place the epoch time (in seconds) is read and allocated to CurrentEpochTime (line 174). The difference between the CurrentEpochTime and the PrevEpochTime is calculated as DeltaTime (line 176). This DeltaTime represents the time in seconds that has lapsed since the previous Fish Feeder activity. The previous Epoch time is read from the EEPROM memory where it is stored since the last activity (line 180). The storage in EEPROM is necessary to survive the deep sleep effects. The Deltatime is the most accurate time possible between two events.
After some experiments, I found that the RTC (Real Time Clock) that runs in microseconds has some fluctuations in accuracy (between 9 and 11 minutes over a period of 24 hours in my case). This is approximately some 0, 7 % error. The fluctuations could be caused by small deviations of the crystal frequency of the ESP32 due to temperature or supply voltage changes. In other words, the time for deep sleep, as set in line 76 is not always exactly 86400 seconds or 24 hours. Thus a function to correct the deep sleep time has been implemented, as follows:
2.5.3 Sleep Time correctionIn lines 272 – 277 a procedure made to calculate a correction (in seconds) for the wakeup time (in microseconds) based on the NTP data received and the calculated DeltaTime. The correction is calculated every time that the Fish Feeder wakes up from deep sleep caused by the wake-up timer (line 258). The correction is only calculated when the DeltaTime is within + or – 10 % of the required TimeToSleep. (lines 273 – 274). The calculated SleepCorrect value is stored in EEPROM and used for correcting the next value of WakeUp_microsecs (line 198).
This way it is possible to get the feeding moment for the fish every day at (approximately) the same time of the day.
2.5.4 Battery State of Charge measurementThe applied 18650 battery is of the type Li-Ion. That means that the Battery voltage when fully charged amounts to 4, 2 V or a State of Charge (SoC) of 100% (without load connected). When the battery voltage has dropped to approx. 3V the SoC is equal to 0%.
The SoC curve between 0% and 100% is however not linear. In various literature all kinds of curves can be found. One set of values that represents the typical SoC curve is depicted below:
When put in a graph, the curve looks like this:
In order to get an indication about the SoC of the 18650 battery in the software, one of the ADC (Analogue to Digital Convertor) inputs of the ESP32 can be used. However, the ESP32 ADC pins can only accept up to 3, 3 V. Beyond that voltage the ADC runs the risk of being burnt. The solution to solve this is pretty simple, namely the use of a voltage divider resistor network will solve the problem. This way the max voltage of 4, 2 V can be reduced to 3, 3 V max. See the following diagram:
The value of the resistors used is calculated as follows:
Using Ohm’s law, the following formula applies:
With Vu=3, 3 V and Vi=4, 2 V it can be calculated that R1 = 0, 27272727 * R2
If we select 330 kΩ for R2 then follows that R1 should be 90 kΩ. However, this is a non-standard value, that can easily be made by putting two 180 kΩ resistors in parallel. With this selection the current drawn from the 18650 battery is at an acceptable low level of 10 µA (4, 2 V/ 4200000).
This way the maximum voltage at the ADC input of the ESP32 is reduced to 3, 3 V (instead of the 4, 2 V when directly connected to the 18650 battery).
The below diagram shows the complete power supply for the Fish Feeder in detail, as well as the resistor voltage divider connected to the + of the 18650 battery and GPIO34 (ADC input) for sensing the State of Charge of the battery.
Now we still have to deal with the non-linearities in the SoC curve. We will solve this in the software by approximating the curve by splitting it in 3 more-or-less linear segments.
Given the fact that the ADC’s of the ESPs 32 (1 of 6 that can be used in combination with Wi-Fi) work with 12 bits resolution, this means that a voltage between 0 and 3, 3 V is represented by values between 0 and 4095.
This means that the voltages of the Li-Ion battery belonging to a SoC of between 0 and 100% will be represented by Battery values between 2925 (0%) and 4095 (100%), as can be seen in the following table:
For each of the 3 color segments we assume that the curve within the boundaries of the segments is linear (which we know it is not exactly correct, but a good enough approximation).
The code for this approximation can be found in the Arduino Sketch in lines 182 – 191, as follows:
Battery = analogRead(BatteryPin); // Getting the Battery voltage from the + pole of the 18650 battery by using a 12 bits ADC input of the ESP32 (GPIO34) via a resistor divider network
// 4095 represents 100% charged, equivalent to 3.3 V at the input of the ADC at GPIO pin 34; reached when the Li-Ion battery is at 4.2 V
if (Battery <= 2925) {BattPerc = 0 ; } // the following code is used for approximating the theoretical State of Charge curve of a Li-Ion battery
else if (Battery >= 4090) {BattPerc = 100 ; }
else if (Battery >= 3871 && Battery < 4090 ) {BattPerc = map ( Battery, 3871, 4090, 80 , 100) ; } // green segment
else if (Battery >= 3607 && Battery < 3871 ) {BattPerc = map ( Battery, 3607, 3871, 20 , 79 ) ; } // yellow segment
else if (Battery > 2925 && Battery < 3607 ) {BattPerc = map ( Battery, 2925, 3607, 0 , 19 ) ; } // red segment
All values measured below 2925 are resulting in a BattPerc of 0% and all values above 4090 as 100%. The values measured in the segments as described above are mapped (in a linear way) between the boundaries of the segments (green, yellow or red)
2.5.5 The IR sensor for food detectionFor detecting the presence or absence of fish food in the silo (see 2.3) a IR transmitting and an IR receiving diode have been built in the silo in two 5 mm holes.
When there is food in the silo the IR light emitted by the transmitting diode will not be detected by the receiving diode, which will therefore not be conducting and consequently GPIO15 (see below diagram) will see a HIGH.
The transmitting diode is triggered with a PWM (Pulse Width Modulation) signal with a frequency of 3800 Hz. (lines 93-98)
const int IR_TX_Pin = 4; // GPIO04 is connected to the (white) transmitting IR LED
const int IR_RX_Pin = 15; // GPIO15 is connected to the (black) receiving IR LED
const int Freq = 3800; // constants to set the parameters for the PWM signal for the IR TX LED
const int Channel = 3;
const int Bits = 10;
The PWM signal at the output of GPIO 4 has with a dutycycle of 50% and thus becomes a square wave with voltage levels of 0 and 3, 3 V. (lines -132-134)
ledcSetup(Channel, Freq, Bits); // PWM controls for the IR_TX LED
ledcAttachPin(IR_TX_Pin, Channel); // connect the IR_TX_Pin with PWM channel
ledcWrite(Channel, 512); // starts sending PWM signal to the IR_TX LED at a duty cycle of 50% (512 is half of 1024)
During every wakeup action in the setup () function the status of the food is measured (LOW or HIGH) (lines 242 - 243)
Food_State = digitalRead (IR_RX_Pin); // check if IR LED receives a signal (dark= LED not conducting thus RX pin is HIGH which means there is still food)
if (Food_State == LOW) {Serial.print ("Food_State is: "); Serial.print (Food_State); Serial.println ("The Food is finished") ; textMsg = textMsgNoFoodForManualFishFeeding.c_str(); Serial.print("textMsg = "); Serial.println (textMsg); Serial.println();}
When there is no food in the silo the receiving IR diode starts switching on an off at a rate of 3800 Hz. As a result hereof GPIO15 will detect a LOW level signal, see the electrical diagram below.
The reason for using a 3800 Hz PWM signal is to avoid possible errors due to thermal effects inside the Fish Feeder’s housing.
2.5.6 The Email libraryFor sending email messages the following library is used:
It should be downloaded from https://github.com/mobizt/ESP-Mail-Client and installed in the Arduino IDE.
A very good tutorial made by Rui Santos, on how to do this, can be found: https://randomnerdtutorials.com/esp32-send-email-smtp-server-arduino-ide/
The smtp host used here in this project can be found on https://www.smtp2go.com , here you can make an account, it is an excellent service that works fantastic.
Certain data need to be filled in for your own smtp account as well as the data for the email recipient. (lines 54-68)
#define WIFI_SSID "your own SSID" // fill in your own WiFi ssid
#define WIFI_PASSWORD "your own password" // fill in your own password
#define SMTP_HOST "mail.smtp2go.com" // Replace with data of your own SMTP server credentials
#define SMTP_PORT 465
#define AUTHOR_EMAIL "person@email.com" // put here your own login credentials for the SMTP host
#define AUTHOR_PASSWORD "your smtp password"
#define RECIPIENT_EMAIL "someone@gmail.com" // email adres of the recipient of emails from the Fish Feeder (email of owner of the Automatic Fish Feeder)
SMTPSession smtp; // The SMTP Session object used for Email sending; contains config and data to send
void smtpCallback(SMTP_Status status); // Callback function to get the Email sending status
The various text messages that can be send by email are composed in the WAKEUP_ACTION () function (line 224), as follows: (lines 226-233)
//////////////////////preparing the various textMsg strings, each message will be ending with "."
String textMsgNoFoodForAutomaticFishFeeding = "The fish could not be fed automatically today " + String (Day[CurrentDayOfWeek]) + ", at " + NTPTime + "\r\n" + " The Time lapsed since last feeding time is: " + String(DeltaHours) + " Hours" + ", The food is finished, please refill " + "\r\n" + "Battery SoC = " + String (BattPerc) + " %" + " , Boot number: " + String(bootCount) + "\r\n" + " The SleepCorrection in seconds is: " + String(SleepCorrect) + "\r\n" + "-- Best Regards from FishFeeder-- ." + "\r\n"; // text message in case automatic feeding is finished and no more food available
String textMsgFishFed = "The fish have been fed automatically today " + String (Day[CurrentDayOfWeek]) + ", at " + NTPTime + "\r\n" + "The time lapsed since last feeding time is: " + String(DeltaHours) + " Hours" + ", There is still food available " + "\r\n" + "Battery SoC = " + String (BattPerc) + " %" + " , Boot number: " + String(bootCount) + "\r\n" + " The SleepCorrection in seconds is: " + String(SleepCorrect) + "\r\n" + "-- Best Regards from FishFeeder-- ." + "\r\n"; // text message in case automatic feeding is finished and still food available
String textMsgAutomaticFishFedNoMoreFood = "The fish have been fed automatically today " + String (Day[CurrentDayOfWeek]) + ", at " + NTPTime + "\r\n" + " The time lapsed since last feeding time is: " + String(DeltaHours) + " Hours" + ", But now the food is finished, please refill " + "\r\n" + "Battery SoC = " + String (BattPerc) + " %" + " , Boot number: " + String(bootCount) + "\r\n" + + " The SleepCorrection in seconds is: " + String(SleepCorrect) + "\r\n" "-- Best Regards from FishFeeder-- ." + "\r\n"; // text message in case automatic feeding is finished and no more food available
String textMsgNoFoodForManualFishFeeding = "The fish could not be fed manually today " + String (Day[CurrentDayOfWeek]) + ", at " + NTPTime + "\r\n" + "The food is finished, please refill" + "\r\n" + "The time lapsed since last activity is: " + String(DeltaHours) + " Hours" + "\r\n" + "Battery SoC = " + String (BattPerc) + " %" + " , Boot number: " + String(bootCount) + "\r\n" + "-- Best Regards from FishFeeder-- ." + "\r\n"; // text message in case manual feeding not possible because there is no food available
String textMsgManualFishFeeding = "The fish have been fed manually today " + String (Day[CurrentDayOfWeek]) + ", at " + NTPTime + "\r\n" + "there is still food available " + "\r\n" + "The time lapsed since last activity is: " + String(DeltaHours) + " Hours" + "\r\n" + "Battery SoC = " + String (BattPerc) + " %" + " , Boot number: " + String(bootCount) + "\r\n" + "-- Best Regards from FishFeeder-- ." + "\r\n"; // text message in case of manual feeding and still food available
String textMsgManualFishFeedingNoMoreFood = "The fish have been fed manually today " + String (Day[CurrentDayOfWeek]) + ", at " + NTPTime + "\r\n" + " But now the food is finished, please refill" + "\r\n" + "The time lapsed since last activity is: " + String(DeltaHours) + " Hours" + "\r\n" + "Battery SoC = " + String (BattPerc) + " %" + " , Boot number: " + String(bootCount) + "\r\n" + "-- Best Regards from FishFeeder-- ." + "\r\n"; // text message in case manual feeding no more food available
String textMsgFishFeederReset = "The fishfeeder has been reset today " + String (Day[CurrentDayOfWeek]) + ", at " + NTPTime + "\r\n" + "The time lapsed since last activity is: " + String(DeltaHours) + " Hours" + "\r\n" + "The next feeding moment will be " + String(HoursToSleep) + " Hours from now" + "\r\n" + "Battery SoC = " + String (BattPerc) + " %" + " , Boot number: " + String(bootCount) + "\r\n" + "-- Best Regards from FishFeeder-- ." + "\r\n"; // text message in case of RESET
Depending on the wakeup_reason and the result of the switch command (lines 235 - 239):
esp_sleep_wakeup_cause_t wakeup_reason;
wakeup_reason = esp_sleep_get_wakeup_cause();
switch(wakeup_reason)
{
One of the composed messages will be allocated to the variable: textMsg by means of, e.g.:
textMsg = textMsgFishFed.c_str();
and will be send by email in the function SEND_EMAIL (): (lines 306 - 308)
message.text.content = textMsg.c_str(); // make the text message based on the applicable textMsg
message.text.charSet = "us-ascii";
message.text.transfer_encoding = Content_Transfer_Encoding::enc_7bit;
In the SEND_EMAIL function a number of data that are allocated already to a number of variables in lines 56 – 64 will be assigned to a set of session parameters and in lines 301, 303 the name and the subject of the message are defined. In line 304 the proper name of the person receiving the email needs to be filled in:
The smtpCallback function serves to get the kind of data as listed above.
To operate the Fish Feeder properly, the following should be done:
- Make sure the switch on the outside of the solar panel is set to “on” and the power button inside the box also switched on.
- Place the solar panel under the most optimum angle (for your Country; in the Netherlands this is 36 degr. elevation and 5 degr. SSW)
- The system can be reset by means of a reed switch to be activated with a permanent magnet on the outside of the IKEA box.
- To activate the manual feed function another reed switch has to be activated. The number of portions to be given for manual feeding is determined by:
and
In case of automatic feeding after time lapse line 259 is relevant:
- A blue LED indicates the status of the WiFi connection (3 states: ‘Off’, “connected”, “trying to connect”. In case no connection with the Wi-Fi access point can be made the blue LED will remain flashing.:
- In that case check if you have filled in the proper settings for your Wi_Fi access point (AP) and if you have enough Wi-Fi signal at the pond site.
In case the connection to the Wi-Fi AP is successful, line 157 will be executed and the blue LED will blink continuously until the ESP32 goes into deep sleep.
- Make the appropriate settings in the following lines:
- Decide on the feeding interval time and fill in:
- Select the appropriate NTP server for your country in line 73:
This was my biggest ARDUINO project (so far), it was time consuming, but fun developing and building it, especially when you see the happy fish after they have received their daily food.
The Wi-Fi connection works fantastic as well as the ESP_Mail_Client.h library. The messages are sent nicely.
Of course, there is always room for improvement. One of the improvements I have in mind is to find another (bigger) box for the housing because the amount of food that can be stored inside the silo is not enough for 3 weeks reserve.
Also writing this tutorial for you was a pleasant thing to do.
Have fun reading and/or making your own Smart Fish Feeder!
Comments