We live in a hot area where it can get quite unpleasant even at night, so we often leave the shutters open during the night and close them early in the morning when the sun comes out.
I wanted to automate the closing of the shutters without connecting to the 220V electricity system so I devised an apparatus that would mechanically close the shutters by pressing on the electricity switches. The smarts behind this solution is an Arduino device that acts like an alarm clock but instead of sounding an alarm, closes the shutters :)
The code behind the solution is generic and can be used as a trigger to any other operations / events you might want to perform. It is basically a digital clock where you set the trigger time (alarm) and an event is fired on that time.
In this instructable I will not describe the mechanical apparatus that turns off the shutters and focus on the Arduino circuit and code since it is general purpose and can be reused in other projects as well.
The materials are very simple::
Note that you can change the code to use different digital pins.
Here is the diagram for the entire project:
There are 4 buttons in this diagram:
The switch is used to indicate whether the trigger is ON or OFF.
The RTC module (DS1302) uses a library called DS1302 that you need to install on your Arduino IDE. It provides all the basic functions to control the module.
The RTC module connects using 5 wires:
The RTC comes with 31 bytes of RAM that can be used by applications. I used this memory space to store the trigger (alarm) time as well as an indication if the alarm is set or not. The advantage of this approach is that even if you disconnect your Arduino from the power supply, this data remains available (this is a non volatile memory since the module has a separate battery).
As you can see in the code, I defined 3 offsets within the RAM. You can change these offset if you like.
///////////////////////// DS1302 RTC<br>#define RTC_CE_PIN 2 #define RTC_DATA_PIN 3 #define RTC_SCLK_PIN 4 #define RAM_SIZE_BYTES 31 /* Don't change - value from library */ #define RAM_TRIGGER_SET_BYTE 30 #define RAM_ALARM_HOUR_BYTE 0 #define RAM_ALARM_MINUTE_BYTE 4 DS1302 gRtc (RTC_CE_PIN, RTC_DATA_PIN, RTC_SCLK_PIN); RTCTime gRTCTime; // Stores the current time
Initializing RTC is very simple. Create a DS1302 instance and do the following in the setup function:
gRtc.halt(false); gRtc.writeProtect(false);
To get the current time use:
RTCTime gRTCTime; // defined globally Time t = gRtc.getTime(); gRTCTime.setTime(&t);
As you can see, I converted the Time class to a RTCTime class (implementation included in the project) that provides additional methods and capabilities.
You can use the RTCTime class to check time intervals (has the time passed, is it before...), to increment or decrement hours and minutes, to check if the time is valid (sometime the RTC module outputs garbage) and to format it as a string.
class RTCTime : public Time { public: RTCTime(); RTCTime(Time *t); bool setTime(Time *t); bool isPassed(Time *tm); bool isBefore(Time *tm); void increaseHour(); void decreaseHour(); void decreaseMinute(); bool isValid(); void toStringHHMM(char buffer[], char *desc = ""); void toStringHHMMSS(char buffer[], char *desc = ""); void toString(char buffer[]); };
The DS1302 library provides 2 methods for reading and writing data to the module's RAM. The first is a burst mode where you write 31 bytes at ones and the other, which I used, is handling a single byte every time.
Here is an example of how to read a specific byte from RAM (in this case this is the flag that indicates whether the trigger is on or off)
////////////////////////////////////////////////////////////////////////// // Loads the trigger (alarm) time from DS1302 RAM // t - pointer to a time class ////////////////////////////////////////////////////////////////////////// void loadTriggerTime(RTCTime *t) { t->hour = gRtc.peek(RAM_ALARM_HOUR_BYTE); t->min = gRtc.peek(RAM_ALARM_MINUTE_BYTE); }
Writing data is very simple as well:
////////////////////////////////////////////////////////////////////////// // Sets the trigger to on and off // on - true for on ////////////////////////////////////////////////////////////////////////// void setTriggerOnOff(bool on) { gRtc.poke(RAM_TRIGGER_SET_BYTE,(on ? 1 : 0)); }
I used an 0.96' mono screen using the great u8g2 library. The screen requires 4 wires:
You need to choose the right u8g2 class for the specific screen that you are using and initialize it (see the u8g2 examples for all the options). Here is the one that I used:
#define LCD_CLK_PIN 10 #define LCD_DAT_PIN 9 U8G2_SSD1306_128X64_NONAME_F_SW_I2C gLCDScreen(U8G2_R0, LCD_CLK_PIN, LCD_DAT_PIN, U8X8_PIN_NONE);
This creates a class instance (gLCDScreen) through which we will control the screen.
The LCDFuncs.ino file contains all the functions for interacting with the screen. Note that every time before accessing the screen, the prepareLCD() function is called to prepare the screen for drawing.
void prepareLCD() { gLCDScreen.enableUTF8Print(); gLCDScreen.setFont(u8g2_font_ncenB08_tf); gLCDScreen.setFontDirection(0); gLCDScreen.setFontMode(0); gLCDScreen.setFontPosTop(); }
The application implements a screen saver functionality that turns off the screen after a certain time of no activity.
The screen saver can be disabled using the following define statement:
#define SCREEN_SAVER_ON true
Here are the two functions controlling the screen saver
////////////////////////////////////////////////////////////////////////// // Reset the screen saver timer to now ////////////////////////////////////////////////////////////////////////// void resetScreenSaverTime() { gScreenOn = true; gLastClickMillis = millis(); gLCDScreen.setPowerSave(0); clearDisplay(); delay(DELAY_RESET_SAVER); } ////////////////////////////////////////////////////////////////////////// // Starts the screen saver ////////////////////////////////////////////////////////////////////////// void startScreenSaver() { if (gScreenOn && SCREEN_SAVER_ON) { gLCDScreen.setPowerSave(1); gScreenOn = false; } }
We use the library's function setPowerSave() to turn on and off the screen but manage the timing by ourselves.
Writing to the screen is straightforward. I created a few functions that write to different portions of the screen. Here is one of them that prints the current time:
void printLCD_CurrentTime(bool autoDelete) { if (!gScreenOn) // If screen saver on, return return; RTCTime t; Time now = gRtc.getTime(); t.setTime(&now); if (!t.isValid()) { CONSOLE(F("Current time is not valid - aborting print")); return; } // Print current time char buffer[TIME_STR_BUFFER_SIZE]; t.toStringHHMM(buffer); gLCDScreen.setFont(LCD_LARGE_TIME_FONT); int col = LCD_Centralize(buffer); gLCDScreen.drawUTF8(col,LCD_LARGE_TIME_ROW, buffer); // Print trigger time loadTriggerTime(&t); t.toStringHHMM(buffer); gLCDScreen.setFont(LCD_TIME_FONT); gLCDScreen.drawUTF8(LCD_TIME_COL,LCD_TRIGGER_ROW, buffer); if (gTriggerOn) gLCDScreen.drawUTF8(LCD_TRIGGER_COL,LCD_TRIGGER_ROW, "ON"); else gLCDScreen.drawStr(LCD_TRIGGER_COL,LCD_TRIGGER_ROW, "OFF"); LCD_DrawScreenSaverStatus(); gLCDScreen.sendBuffer(); if (autoDelete) { delay(TIME_TO_AUTO_CLEAR_LCD); gLCDScreen.clear(); } }
Servo motors are easy to use with Arduino using the Servo library. We initialize the motor during setup()
///////////////////////// Servo #define SERVO_PIN 5 #define SERVO_CLOSED_POSITION 180 #define SERVO_INIT_POSITION 0 Servo shutterServo;
and move it when an event is fired:
void closeShutters() { if (gShuterClosed) return; shutterServo.attach(SERVO_PIN); shutterServo.write(SERVO_CLOSED_POSITION); delay(1000); shutterServo.detach(); gShuterClosed = true; } void initServo() { shutterServo.attach(SERVO_PIN); shutterServo.write(SERVO_INIT_POSITION); delay(1000); shutterServo.detach(); gShuterClosed = false; }
Note that after moving the servo to a new position, we detach from it to cut off the power. This will also eliminate the noise coming out of the Servo engine even when not in use.
In the main loop, we take several steps:
If either the SET TIME button or the SET TRIGGER button are clicked, we start an infinite while loop in which we detect the UP, DOWN and SET buttons and act accordingly to set hour, minutes and save the new time.
Here is the function that manages the loop for setting the time:
void handleSetTime() { CONSOLE("handle set time"); // Print header on LCD printLCD_Header("Set Time (hour)",true); // Show the current ime RTCTime theTime; theTime.setTime(&(gRtc.getTime())); printLCD_SetTime(&theTime,false); // Wait for additional input bool hourSet = true; while (true) { // Increase time if (digitalRead(BUTTON_UP) == BUTTON_PRESSED) { if (hourSet) theTime.increaseHour(); else theTime.increaseMinute(); printLCD_SetTime(&theTime, false); } // Decrease time if (digitalRead(BUTTON_DOWN) == BUTTON_PRESSED) { if (hourSet) theTime.decreaseHour(); else theTime.decreaseMinute(); printLCD_SetTime(&theTime,false); } // if the set button was pressed and we are setting minutes, // we need to save the new time if (!hourSet && digitalRead(BUTTON_SET) == BUTTON_PRESSED) { CONSOLE("Save time"); gRtc.setTime(theTime.hour, theTime.min, 0); printLCD_Header("Time saved",true); printLCD_SetTime(&theTime,false); delay(TIME_SHOW_DELAY); resetScreenSaverTime(); clearDisplay(); return; } // Set button is pressed and we are in Hour settings, move to minute settings if (hourSet && digitalRead(BUTTON_SET) == BUTTON_PRESSED) { hourSet = false; clearDisplay(); gRtc.setTime(theTime.hour, theTime.min, 0); printLCD_Header("Set Time (min)",true); RTCTime theTime; theTime.setTime(&(gRtc.getTime())); printLCD_SetTime(&theTime,false); delay(RTC_DELAY); } } // while }
We use 4 TACT buttons and one toggle switch. All of them are initialized with INPUT_PULLUP to use the Arduino's internal pull-up resistors, eliminating the need to add resistors to the circuit.
Setting up the buttons:
// Init GPIO pins pinMode(BUTTON_SET, INPUT_PULLUP); pinMode(BUTTON_SET_TRIGGER, INPUT_PULLUP); pinMode(BUTTON_UP, INPUT_PULLUP); pinMode(BUTTON_DOWN, INPUT_PULLUP); pinMode(BUTTON_TRIGGER_ON, INPUT_PULLUP);
Reading the button's state:
#define BUTTON_PRESSED 0 bool setButtonPressed = (digitalRead(BUTTON_SET) == BUTTON_PRESSED);