I am building a 2ft (6.56m) x 4ft (13.12m) feeding trough made of HDPE plastic. It's lid is very light, and it has a 12v linear actuator that is pushing it from 10 degrees "closed" to 90 degrees "open".
It is designed to open at 7:00 am my local time, and close at 5:30pm my local time. (Thereabouts. I tweak the time occasionally).
It draws far more power than it has any business drawing. Around 0.12 amps. This is with sleep logic that I have to try to manage power draw. There is a 15ah battery and this should last weeks or months with how much power the system should need. But it would drain in around 3 days at this rate.
It is set up with the following key components:
Arduino Uno REV3
BTS7960
LM2596 Multi-Channel Buck Converter w/ Adj/5v/5v/3.3v
Button module
DS3231 AT24C32 IIC RTC Clock w/ LIR2032 battery installed
Terminal block for splicing button wires into dupont connections
12V 15Ah LiFePO4 Battery - Bioenno Power
The battery has an inline fuse attachment. It then has a voltage meter for tracking the voltage and amp usage rate at any given reading + overall usage since last plugged into the battery. The battery splits on a WAGO connector to go directly to the motor driver board directly with 16g wires in B+/B-, and into the buck converter.
The buck converter plugs into the Arduino via the ADJ outputs, which are set to approximately 9v, and plug into the Vin on the Arduino. I did this instead of the 5v as I need to be able to plug in my computer to the USB port without causing problems, and it was my understanding that the 5v would potentially cause problems when power also comes along the USB cord.
I had it powered this way previously, and didn't notice as large of a power draw, but this was also before a buck converter was added, or an RTC, and I was using a small 2ah test battery. The whole system looked different then.
The motor driver is connected via these pins to the Arduino. It is grounded with an extra Dupont ground wire to the buck converter:
RPWM = 9;
LPWM = 10;
REN = 6;
LEN = 7;
And this was, again, powered directly via the 16g battery power coming off of the WAGO splicer.
Then, I have a button, which will allow someone to override the current open/close of the trough lid with a button. It is currently plugged into the D3 port, though I also had it plugged into A4 when I didn't have wakeup/sleep logic in my code. It is connected to an Arduino ground and this D3 port. The button is connected to the Arduino via a terminal block that takes the ~22g wire and converts it into Dupont wires.
Also, the RTC module is connected to A4/A5 and 3.3v and GND on the Arduino. I've had some problems with it. It still does not seem to properly maintain the time when the system is powered off. I ordered a new, better RTC module as the one I had was very cheap. Will see if that helps. The rechargeable coin batteries are definitely good though, and what it calls for.
And of course, the motor driver then connects M+/M- to my 12v linear actuator (8 inch). An important note here! I step down the wire gauge from the battery's 12g wire, to a 16g wire, using a WAGO lever connector. It is impossible to fit the 12g wire tip into the corral for the B+/B- in the motor driver. It forced me to do a wire gauge step down to fit. I made the 16g portion as short as possible/about 4 inches.
Here is my entire code. I have never programmed in C++ before, so I apologize for any horrid code. I am a C# and Js/Nodejs guy.
Would an Arduino Mini be a reasonable step down to save on energy cost?
#include <Wire.h>
#include "RTClib.h"
#include <LowPower.h>
#define DEBUG_SERIAL 1 // Set to 0 for field deployment
#define SET_RTC_FROM_COMPILE 0 // Set to 0 when you do NOT want to change the time keeping modules time reference
// Time keeping when disconnected from development machine.
RTC_DS3231 rtc;
DateTime now;
DateTime sunrise;
DateTime sunset;
const int sunriseHourLocalUtc = 7;
const int sunriseMinuteLocalUtc = 0;
const int sunsetHourLocalUtc = 17;
const int sunsetMinuteLocalUtc = 20;
// How long to sleep after doing the scheduled action
const uint32_t afterSunriseSleepSeconds = 35280UL; // 9.8 hours
const uint32_t afterSunsetSleepSeconds = 49680UL; // 13.8 hours
// Pins BTS7960 (Motor Driver)
const int RPWM = 9;
const int LPWM = 10;
const int REN = 6;
const int LEN = 7;
// Volatile awake D3 Arduino pin for movement button.
const int openCloseButton = 3;
// Motion timings
const unsigned long actuatorExtendRetractTime = 15000UL; // 15 seconds
const unsigned long overrideTimeoutMs = 1800000UL; // 30 minutes
// Ignore button noise for first 2 minutes after boot
const unsigned long startupIgnoreButtonPressTimeout = 120000UL;
// ISR debounce (ms)
const unsigned long isrDebounceMs = 250UL;
// -----------------------------
// Feed Trough System States
// -----------------------------
unsigned long lastOverridenTimestamp = 0;
uint32_t currentSleepCycleTimeRemaining = 0;
bool isOpen = false;
bool isDaytimeAndLidShouldBeOpen = false;
bool isOpenStateOverriden = false;
bool needsInitializing = true;
bool actuatorIsActive = false;
volatile bool wakeEvent = false; // Set by ISR ALWAYS (means "something happened, wake up").
volatile unsigned long lastInterruptTime = 0; // Last time button reports press (may be false report in first two minutes).
bool buttonPressed = false; // Latched by loop when it decides a wake event is a valid press.
unsigned long currentMillis = 0;
void setup() {
#if DEBUG_SERIAL
Serial.begin(9600);
while (!Serial) {}
Serial.print("\nReset cause (MCUSR): ");
Serial.println(MCUSR, BIN);
MCUSR = 0;
#endif
Wire.begin();
if (!rtc.begin()) {
#if DEBUG_SERIAL
Serial.println("Couldn't find RTC");
#endif
}
#if SET_RTC_FROM_COMPILE
// One-time convenience: set RTC from compile time on purpose.
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
#if DEBUG_SERIAL
Serial.println("RTC adjusted from compile time (SET_RTC_FROM_COMPILE=1).");
#endif
#else
// Normal operation: do NOT adjust every boot.
if (rtc.lostPower()) {
#if DEBUG_SERIAL
Serial.println("RTC lost power (OSF set). Clock needs to be set once.");
#endif
// rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
#endif
pinMode(RPWM, OUTPUT);
pinMode(LPWM, OUTPUT);
pinMode(REN, OUTPUT);
pinMode(LEN, OUTPUT);
pinMode(openCloseButton, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(openCloseButton), buttonISR, FALLING);
digitalWrite(REN, HIGH);
digitalWrite(LEN, HIGH);
stopActuator();
}
void loop() {
currentMillis = millis();
now = rtc.now();
// TODO: Use timing module (when it works reliably) to determine "next day", and set sunrise and sunset just once a day.
sunrise = DateTime(now.year(), now.month(), now.day(), sunriseHourLocalUtc, sunriseMinuteLocalUtc, 0);
sunset = DateTime(now.year(), now.month(), now.day(), sunsetHourLocalUtc, sunsetMinuteLocalUtc, 0);
isDaytimeAndLidShouldBeOpen = (now >= sunrise && now < sunset);
// Convert ISR wake into a "real" press if appropriate
if (wakeEvent) {
noInterrupts();
wakeEvent = false;
interrupts();
// Ignore the first N ms after boot to avoid bogus wake noise
if (currentMillis >= startupIgnoreButtonPressTimeout) {
// Only accept a new press if button is currently released
// (prevents "holding down for too long" from retriggering)
if (digitalRead(openCloseButton) == LOW)
buttonPressed = true;
}
}
if (needsInitializing) {
#if DEBUG_SERIAL
Serial.println("\nInitializing: forcing lid CLOSED at startup.");
#endif
needsInitializing = false;
isOpenStateOverriden = false;
buttonPressed = false;
// Force known state: close lid
moveClose();
}
// If actuator is moving and a button press comes in, ignore it for now
if (actuatorIsActive)
buttonPressed = false;
const bool forceCancelOverride = isOpenStateOverriden &&
(currentMillis - lastOverridenTimestamp > overrideTimeoutMs);
// Manual button OR forced override timeout
if (buttonPressed || forceCancelOverride)
{
buttonPressed = false;
// If override timeout happened, we are cancelling override
if (forceCancelOverride) {
#if DEBUG_SERIAL
Serial.println("\nOverride timeout: returning lid to intended position.");
#endif
isOpenStateOverriden = false;
} else {
// Toggle override state on manual press
isOpenStateOverriden = !isOpenStateOverriden;
if (isOpenStateOverriden) {
lastOverridenTimestamp = currentMillis;
#if DEBUG_SERIAL
Serial.println("\nManual override ON.");
#endif
toggleLid();
} else {
#if DEBUG_SERIAL
Serial.println("\nManual override OFF.");
#endif
}
}
}
// Scheduled behavior only when NOT overridden
if (!isOpenStateOverriden && isDaytimeAndLidShouldBeOpen && !isOpen) {
#if DEBUG_SERIAL
Serial.println("\nScheduled: OPEN lid.");
#endif
moveOpen();
Serial.println("\nScheduled: OPEN lid.");
sleepForSeconds(afterSunriseSleepSeconds);
} else if (!isOpenStateOverriden && !isDaytimeAndLidShouldBeOpen && isOpen) {
moveClose();
sleepForSeconds(afterSunsetSleepSeconds);
}
delay(25);
}
// IMPORTANT: ISR should be tiny. No Serial. No long logic.
void buttonISR() {
unsigned long ms = millis();
if (ms - lastInterruptTime < isrDebounceMs)
return;
lastInterruptTime = ms;
// Always set wake event so sleep can break
wakeEvent = true;
}
void sleepForSeconds(uint32_t seconds) {
if (seconds > 0)
currentSleepCycleTimeRemaining = seconds;
wakeEvent = false;
#if DEBUG_SERIAL
Serial.print("Sleeping for seconds: ");
Serial.println(currentSleepCycleTimeRemaining);
#endif
while (currentSleepCycleTimeRemaining >= 2) {
// If anything woke us, stop sleeping immediately
if (wakeEvent)
break;
LowPower.powerDown(SLEEP_2S, ADC_OFF, BOD_OFF);
if (wakeEvent)
break;
currentSleepCycleTimeRemaining -= 2;
}
}
// -----------------------------
// ACTUATOR CONTROL
// -----------------------------
void toggleLid() {
if (isOpen)
moveClose();
else
moveOpen();
sleepForSeconds(0);
}
void moveClose() {
actuatorIsActive = true;
analogWrite(RPWM, 255);
analogWrite(LPWM, 0);
// Keep state consistent
isOpen = false;
#if DEBUG_SERIAL
printVariableStates();
#endif
delay(actuatorExtendRetractTime);
stopActuator();
}
void moveOpen() {
actuatorIsActive = true;
analogWrite(RPWM, 0);
analogWrite(LPWM, 255);
isOpen = true;
#if DEBUG_SERIAL
printVariableStates();
#endif
delay(actuatorExtendRetractTime);
stopActuator();
}
/*
Actuator has internal limit switches, but these can fail - especially with cheaper units.
We minimize chance of motor burnout by explicitly calling this about 2 seconds after open/close time should take.
Also, debris falling on lid, stopping extension or slowing it - we want it to give up if it meets resistance.
*/
void stopActuator() {
analogWrite(RPWM, 0);
analogWrite(LPWM, 0);
actuatorIsActive = false;
#if DEBUG_SERIAL
Serial.println("Motor STOP.");
#endif
}
void printVariableStates() {
Serial.println("\n----------------------");
Serial.println("DEBUG STATE");
Serial.print("Now: ");
Serial.println(now.timestamp());
Serial.print("Sunrise: ");
Serial.println(sunrise.timestamp());
Serial.print("Sunset: ");
Serial.println(sunset.timestamp());
Serial.print("isDaytimeAndLidShouldBeOpen: ");
Serial.println(isDaytimeAndLidShouldBeOpen);
Serial.print("isOpen: ");
Serial.println(isOpen);
Serial.print("isOpenStateOverriden: ");
Serial.println(isOpenStateOverriden);
Serial.print("lastOverridenTimestamp(ms): ");
Serial.println(lastOverridenTimestamp);
Serial.println("----------------------");
}
