An easy-to-use kitchen timer with wireless charging.
BUILD TIME: 90 MINUTES
DIFFICULTY RATING: Intermediate
Have you noticed that many modern kitchen appliance manufacturers make the timer function in their product infuriating and impractical to operate? By the end of this project, you’ll have a kitchen timer that’s dead easy to use, with a cool hidden feature: wireless charging!
THE BROAD OVERVIEW
Many kitchen appliances have timers. But as pressure has built on manufacturers to make their products “smarter” and sleeker, oftentimes, usability has completely lost out. For example, the author’s oven has capacitive buttons. Not only does the timer only set in increments of either 5 seconds or 5 minutes, but steam from the oven regularly presses the front buttons, causing unwanted behaviour.
The oven works perfectly well in every other regard, so replacing the oven is obviously not a smart option. Instead, we could use a separate timer. These are cheaply available, but often have cryptic, or no, instructions, amongst other downsides.
For this reason we’ve designed an Arduino-based touchscreen kitchen timer that’s customisable, but most importantly it's dead-easy to use.
HOW IT WORKS
The timer consists of an Arduino UNO or equivalent, a touchscreen shield, a rechargeable battery in the form of a modified USB powerbank, a Qi charging receiver, a power button, a buzzer, and a clean 3D-printed case.
It doesn’t require any additional electronic components or circuitry beyond some wires, so construction is fairly easy. There is some slightly fiddly but straightforward soldering required. Everything fits in a 3D-printed case which can then be made water-resistant with some silicone sealant if desired.
The focus in designing this timer was to Keep-It-Simple (Stupid), with minimum thought required to perform any function. At least this applies to the interface - the code to make things simple can be deceptively complex.
There’s no clock, so no backup battery or settings screens. A homescreen allows you to select ‘Stopwatch’ or ‘Timer’, and then each has simple but reliable function with colourful labelled buttons. The timer can be set in as little as two taps, and when you’re done, you can simply power it off.
POWERING THE TIMER
The main limitation is powering the timer. Oven timers are plugged into the mains, and tiny LCD timers need next to no power at all. An Arduino with a touchscreen wouldn’t last more than a few hours on small single-use batteries, so a rechargeable solution was chosen.
Additionally by using a USB powerbank, we don’t have to worry about converting the 3.7V cell voltage to 5V for the Arduino, nor design a charging and protection circuit. The powerbank’s tiny PCB handles all that itself. The cost is comparable to a boost converter anyway, so we really get more bang for the buck, and it includes the battery and charge controller we need too!
Leaving the timer with a Micro-USB port to charge was considered and could be a possible modification if it suits your use, but keep in mind any port is likely to get food gunk in it if used in the kitchen.
RECHARGING WIRELESSLY
Wireless charging, predominantly through the Qi protocol, has rapidly gained popularity and support in smartphones, smartwatches, and other devices.
Charging pads can be found for under $50, which will charge an iPhone 8 (and above) in about the same time it would take when plugged-in. However, we’re still missing a vital component here — a way for our timer to receive the electromagnetic field of the Qi pad and convert it into 5V power to charge the powerbank.
Fortunately, the earlier uptake of Qi support, particularly in Asia, has resulted in Qi ‘receiver pads’ being made for all manner of mobile phones that don’t support the technology natively.
These are simply stuck on the back of the phone, or inside the back cover (remember when you could take the back cover off a phone without $100 in tools?!) and connect to either the Micro-USB or Lightning port of the phone, or internal copper pads, in the case of the internal converters.
While a Micro-USB version would seem perfect for the hidden powerbank in this project, unfortunately physical limitations prevent that from working. We need the powerbank to sit in the bottom of the case, and using such a receiver would place it smack-bang in the centre, where the Arduino and display are.
For this reason, we used a bare-copper-pad version, designed for the Samsung Galaxy S3. This one is available at Jaycar Electronics, but only while stocks last (the S3 isn’t exactly the most recent model). Thankfully, they remain available across the internet, and at phone accessory stores.
The Build:
Parts Required: | Jaycar | ||
---|---|---|---|
1 × Arduino UNO or Equivalent | XC4410 | ||
1 × 2.8" Touchscreen Shield | XC4630 | ||
1 × Single-18650 USB Powerbank without Switch | MB3717 | ||
1 × Qi Wireless Charging Receiver Pad | MB3664 | ||
1 × 5VDC-compatible Miniature Buzzer | AB3459 | ||
1 × Latching Button (200mA+ Rated, Water Resistant, 20mm Mounting Hole) | SP0743 | ||
Hookup Wire | WH3032 |
* The Altronics LCD screen uses a different IC to drive the LCD and may require different TFT drivers.
† The mounting hole for this switch may be smaller than the specified 20mm.
THE TOUCHSCREEN: It is important to note there are some important requirements for the LCD shield. Unfortunately, there are a great many of these shields that look very similar but use a wide variety of controller chips. In this project, we’ll be using the Jaycar model for ease of reproduction, but it’s likely others will work, provided you find a version of the Adafruit TFT library that works with them. This project doesn’t use the scrolling features, which may not be present depending on controller IC. For help, a good source of info can be found at http://misc.ws/2015/01/24/lcd-touch-screen-information/
Also check that the LCDs are positioned the same if you want to use the 3D print files without modification.
THE POWERBANK: The powerbank chosen for this project, the Jaycar MB3717, was selected for the rare fact it doesn’t require pressing a button to activate the output. If you use a different model, ensure it also has no button to activate.
THE BUZZER: The Jaycar buzzer used, while cited for 9-14V operation, works just fine at 5V and is loud enough, without having an unpleasant tone. You may substitute with any other buzzer that fits and pulls <20mA at 5V.
CONSTRUCTION
The first step is to modify the powerbank. Remove the internals from of the outer case, with the help of a plastic card if necessary.
Remove the 18650 lithium cell and the small cover hiding the circuit board, then lift out the board and battery contacts, starting with the spring end (tilt it upwards to get the USB connectors out).
Carefully clamp the PCB in place, and solder two wires (about 10cm long) to the two spots connected to the Micro-USB charging connector’s Ground and 5V pins. As the pins on the connector are too tiny, we used the connector chassis (which is connected to the ground pin) and one side of the decoupling capacitor next to the connector. Carefully check you haven’t bridged any pins on the IC or other components.
To check you’ve connected them correctly, take the board out of the clamp (to avoid shorting anything), and carefully connect 5V across the two wires (checking polarity, of course — in this case, blue = ground, yellow = 5V). You should see the red LED light up on the side of the board with the USB-A connector (even without the battery cell installed). If all went well, continue. If not, quickly disconnect the power, desolder the wires, and try again, perhaps with a magnifying glass.
Replace the board in the clamp, but with the larger USB-A connector facing up this time. Next solder two other wires (in different colours) to the Ground and 5V pins of the output USB-A connector. Angle the positive wire to follow the path of the negative wire, on the left of the spring terminal.
Replace the board in the plastic holder, guiding the wires as shown. Tilt the board so that the negative spring terminal is upwards, for the USB connectors to fit. There should be room for both sets of wires to fit past the board, and the terminal spring wires should fit back into their slots.
Next, we’ll wire the buzzer. First, place the display shield on the Arduino (if you haven’t already). Because the display itself overlaps the pin headers, you’ll want to clamp or stand it upright. Solder the negative buzzer wire to ground (2nd pin in on the left header on the top), and the positive to Pin 13 (3rd pin in). Keep enough slack that you can position the buzzer in the space next to the Arduino’s USB port. If you’re not using single core wire that’ll hold its shape, consider adding a dab of hot glue to the buzzer and the Arduino’s USB connector to keep it in position.
We want the latching power button to sit inline with the 5V supply from the power bank to the Arduino. Because of space restrictions in the enclosure, we’ll solder directly to the button terminals, rather than using spade lugs. This means soldering it will have to wait until it’s installed in the box. For now, solder a fresh ~10cm wire to the 5V pin on the headers on the bottom of the display shield (3rd from the left). This will be the 5V coming from the switch.
Next, solder the ground wire from the power bank to one of the ground pins on the shield close by (4th and 5th from the left).
Solder the two charging wires to the corresponding pads of the Qi receiver. If you’re unsure, place the receiver on a Qi charger, and measure polarity of the 5V across the pads with a multimeter, then label them.
Now is a good time to test the setup works. Place the rechargeable cell in the power bank slot and hold the ends of two 5V wires together. The Arduino should start up happily. If you’ve already uploaded the code, you should see the home screen appear.
Take the 3D printed enclosure “lid” (actually the bottom), and as shown, hot glue (or similar) the Qi receiver pad in. Before you glue, check that the solder pads won’t short on the bottom of the Arduino. Try to minimise the space between the pad and the lid.
Next, place the Arduino and shield on the 3 pegs that hold it in place, and place a dab of hot glue on each to ensure it can’t move.
Place hot glue along the centre of the lower space for the power bank. Press it in, ensuring it doesn’t overhang the side of the lid.
If you’re using threaded inserts to hold the lid on, take the top piece and carefully use a soldering iron on low heat (about 250°C) to press them into the screw posts until they’re flush.
Insert the button into its mounting cut-out, rotating it so the lugs don’t overlap the display. Bend the spade lugs outward, so they aren’t taller than the screw posts. Solder the two 5V wires to two of the lugs. If you’re uncertain, check that they form a circuit when the button is latched with a multimeter beforehand.
Check whether the lid will fit into the top piece. You may need to cut off one button lug to fit the Arduino’s USB port in next to it.
Fit the lid in place, then insert and tighten the screws. Turn the box over, and press the button to turn it on. If everything worked, you should get your timer!
Note: Don’t be worried that the blue LED stays on when the timer is turned off. The powerbank turns itself off after about 20 seconds.
Now turn the timer off, and place it on a Qi charging pad. If your 3D printer filament isn’t opaque, you should be able to see it light up red and charge the lithium cell.
DC Charging
If you don't want to use Qi charging for some reason, you can effectively replace the Qi pad with a DC connector and connect to your favourite 5V DC source.
Indeed, if you have a convenient way to power it you can omit the battery and charging circuit entirely, and simply provide 5V power. No changes are required for the code, and you can continue with everything else without modification.
However, one major advantage of the rechargeable unit is that it's totally portable for when you want to step outside into the garden, are waiting in the pool while your pie cooks in summer, or whatever else you're doing.
Qi charging really is the most convenient option however.
THE CODE/SETUP
This project relies on only the libraries needed to run the display controller and resistive touchscreen: the Adafruit-GFX, MCUFriend-kbv, and TouchScreen libraries. However, the Jaycar XC4630 screen we’re using doesn’t work natively with the publicly available version of the libraries, so instead, it’s important you install the modified version from the Jaycar product page: https://www.jaycar.com.au/p/XC4630
Select “Downloads” and “Download Software”. Install the libraries contained in the ZIP file into your Arduino libraries folder to continue.
The structure of the code consists of our main sketch file, Kitchen_Timer.ino, which includes Render.h, which itself includes Touch.h, which includes DataTypes.h and Config.h. Effectively, this simply allows separation of different areas of code into separate files. Render.h handles low level drawing of buttons and text, and Touch.h handles reading touches, and keeping track of the Touch Items, which we’ll get to later. DataTypes.h has some special types used in our rendering code, and Config.h holds all the configuration.
For a detailed walkthrough of Render.h and Touch.h, check out the online version of this article. We take an extensive look through how the touch sensing and screen rendering works, it just wasn't really practical to put it all here, considering it's "optional reading". However, reading through it will certainly increase your understanding of how the touchscreen system works with Arduino, and will open the door for more advanced interfacing down the road.
Now let's take a look at some of the available options and configurables, as well as our own code to make it all work.
CONFIGURING CONFIG.H
Since the LCD shield has a set pinout, we don’t need to specify the pins. So after importing our libraries, the only pin configuration is the buzzer.
Pin 13 is just about the only pin not already used for some function, so it was chosen for the buzzer.
#define BUZZER_PIN 13
For debugging, we may want to slow down the main loop sometimes. But 1 millisecond delay is a sensible normal value. Any higher than 10, and touch may start to become unreliable.
#define LOOP_DELAY 1
The resistive touchscreen works by “borrowing” four pins (2 digital, 2 analogue) that are also used by the LCD, to read the resistances on both axis. While the Touchscreen library handles most of the actual reading, we have to reset the pin modes of the analogue pins to ensure the LCD keeps working, and translate the raw X and Y axis touch values to their pixel equivalents.
MAIN SKETCH SETUP (KITCHEN_TIMER.INO)
#include <SPI.h> // f.k. for Arduino-1.5.2
#include "Adafruit_GFX.h" // Hardware library
#include <MCUFRIEND_kbv.h>
#include <TouchScreen.h>
MCUFRIEND_kbv tft;
#include "Render.h"
enum State : uint8_t {
StateUndefined = 0,// Undefined = mystery
StateHome = 1,// "Stopwatch"/"Timer" screen
StateStopwatchStopped = 32, // not counting
StateStopwatchGoing, // Stopwatch counting
StateTimerSet = 64, // Timer setting screen
StateTimerGoing, // Timer counting down
StateTimerRinging, // Timer ringing, counting up from 0
};
State state = StateUndefined;
State prevState = StateUndefined;
The sketch file starts by including the libraries, creating the tft object, and including Render.h. It defines the main state variables and an enum of possible states. These are vital to the state machine which runs the main loop of the timer.
//Misc
#define STATE_ALL_B_BACK 128 // Back button
// Home
#define STATE_HOME_B_STOPWATCH 1
#define STATE_HOME_B_TIMER 2 // Stopwatch
#define STATE_SW_B_RESET 1
#define STATE_SW_B_GOSTOP 2 // Timer set
#define STATE_TIMER_B_10SEC 1
#define STATE_TIMER_B_1MIN 2
#define STATE_TIMER_B_10MIN 3
#define STATE_TIMER_B_1HOUR 4
#define STATE_TIMER_B_START 5 // Timer going
#define STATE_TIMERG_B_STOP 1
Identifiers for buttons are defined, for the drawing functions to come.
void drawHome() {
tft.fillScreen(BLACK);
clearTouchItems();
addButton(MRect(0, 0, 4, 6), BUTTON_COLOUR_
ORANGE, BUTTON_TSIZE_L, "Stopnwatch",
STATE_HOME_B_STOPWATCH);
addButton(MRect(4, 0, 4, 6),
BUTTON_COLOUR_PURPLE, BUTTON_TSIZE_L,
"Timer", STATE_HOME_B_TIMER);
}
void drawStopwatch(bool going) {
if (going) {
addButton(MRect(4, 3, 4, 3),
BUTTON_COLOUR_RED, BUTTON_TSIZE_L,
"Stop", NO_TOUCH_REG);
// NO_TOUCH_REG = don’t register for touch
// events, just a redraw.
} else {
tft.fillScreen(BLACK);
clearTouchItems();
addButton(MRect(4, 3, 4, 3),
BUTTON_COLOUR_GREEN, BUTTON_TSIZE_L,
"Go", STATE_SW_B_GOSTOP);
addButton(MRect(0, 4, 2, 2),
BUTTON_COLOUR_GREY, BUTTON_TSIZE_L,
"<", STATE_ALL_B_BACK);
addButton(MRect(2, 4, 2, 2),
BUTTON_COLOUR_BLUE, BUTTON_TSIZE_M,
"Rst", STATE_SW_B_RESET);
}
}
These functions illustrate how we build the screen’s contents. The drawStopwatch() function takes an argument that causes it to draw a “Stop” button over the existing “Go” button, without redrawing the entire screen. This is important when redrawing takes a visible amount of time.
Thanks to the addButton() function we wrote, adding buttons with different colours, text, and grid positions is super easy. Note it’s important to call clearTouchItems() each time we erase the screen, so there aren’t ‘phantom buttons’.
void drawStopwatchTime(uint32_t seconds) {
uint8_t secs = seconds % 60ull;
uint8_t mins = (seconds / 60ull) % 60ull;
uint8_t hrs = (seconds / 3600ull);
String str;
if (hrs < 10) str += " ";
str += hrs;
str += ":";
if (mins < 10) str += "0";
str += mins;
str += ":";
if (secs < 10) str += "0";
str += secs;
addTextBG(MRect(0, 0, 8, 3), WHITE, BLACK, 6, str.c_str());
}
This function takes a value in seconds, and builds the time string, then renders it on screen. In this situation, we’re using the high-level Arduino String type, then calling .c_str() on it, to pass a char array to our rendering functions. When we draw the string, it’s done with a black background colour, so the digits don’t build up on top of each other.
void setup() {
// Enable this line for touch debugging
//Serial.begin(115200);
pinMode(BUZZER_PIN, OUTPUT);
// 0x7783 is the identifier for the Jaycar
// Duinotech 2.8" touchscreen display. Other
// displays may have other controllers and
// hence other identifiers needed.
tft.begin(0x7783);
tft.setRotation(1); // Landscape
tft.fillScreen(BLACK);
state = StateHome;
}
// All these variables keep track of the time
// something happened. For that reason they’re 32
// bit (millisecond timestamp size)
uint32_t swStart, swPause, tick;
uint32_t timerSetTime;
uint32_t timerStopConfirm;
The setup() function is quite simple. It sets the buzzer’s pinMode, sets up the TFT ready to use, and sets what state to start in.
void loop() {
touch = checkTouch();
// STATE MACHINE
switch (state) {
The state machine works by keeping track of the current state of the device, and the previous state the last time the loop ran. It takes an input of any current touch. This allows it to act on three main initiatives: what screen we’re on, whether we’ve only just moved to this screen, and whether a button is touched. Various timer variables are the extent of ad hoc state.
Note, that any time the state is changed, it is immediately followed by a return statement. This results in starting at the top of loop() again. This bypasses setting prevState = state, and thus allows the distinction of the first iteration of a new state.
case StateHome:
if (prevState != state) { // Repaint
drawHome();
break;
}
if (touch == STATE_HOME_B_STOPWATCH) {
state = StateStopwatchStopped;
return;
} else if (touch == STATE_HOME_B_TIMER) {
state = StateTimerSet;
return;
}
break;
The first case is for the home screen. It has two touch actions, to change to the stopwatch or timer states.
case StateStopwatchStopped:
if (prevState != state) { // Repaint
drawStopwatch(false); // Not going
if (swPause == 0) { // Not paused
swStart = 0;
drawStopwatchTime(0);
} else { // Paused
drawStopwatchTime((swPause - swStart)
/ 1000ull);
}
break;
}
if (touch == STATE_ALL_B_BACK) {
state = StateHome;
return;
} else if (touch == STATE_SW_B_RESET) {
swPause = 0; // Remove any paused value
prevState = StateUndefined;
// Little bit of a hack to force the stopwatch
// to reset. By making prevState different,
// we make sure it displays as if fresh.
return;
} else if (touch == STATE_SW_B_GOSTOP) {
state = StateStopwatchGoing;
return;
}
break;
The second action attached to this button is for when the stopwatch is already running.
case StateStopwatchGoing:
if (prevState != state) { // Repaint
if (swPause == 0) {
swStart = millis();
} else {
swStart += (millis() - swPause); // Move the start time forward equal to the time // we paused, so the stopwatch time is the same.
swPause = 0;
}
tick = swStart + 1000ull;
drawStopwatch(true);
break;
}
if (millis() >= tick) {
tick += 1000ull;
// This method means we just bump the updating
// time by 1000, each time 1000 ms have elapsed!
drawStopwatchTime((millis() -
swStart) / 1000ull);
}
if (touch == STATE_ALL_B_BACK) {
state = StateHome;
return;
} else if (touch == STATE_SW_B_RESET) {
state = StateStopwatchStopped;
swStart = millis();
return;
} else if (touch == STATE_SW_B_GOSTOP) {
state = StateStopwatchStopped;
swPause = millis();
return;
}
break;
These are the two states of the stopwatch feature. StateStopwatchGoing’s section includes the use of millis() and three timer variables (swStart, swPause and tick) to accurately count time.
The first stores the time the stopwatch was started. swPause stores the time the stopwatch was paused. If it isn’t 0 when the stopwatch starts, the swStart variable is changed by the time spend paused — thus acting as if it wasn’t paused, and continuing to count correctly. Tick is used to only redraw the stopwatch time if a full second has passed.
case StateTimerSet:
if (prevState != state) { // Repaint
drawTimerSet();
if (prevState != StateTimerGoing) {
// Retain time when going back to set screen
timerSetTime = 0;
}
break;
}
if (touch == STATE_ALL_B_BACK) {
state = StateHome;
return;
} else if (touch == STATE_TIMER_B_START) {
if (timerSetTime != 0) {
state = StateTimerGoing;
}
return;
} else if (touch == STATE_TIMER_B_10MIN) {
timerSetTime += 10 * 60;
drawTimerSetTime(timerSetTime);
} else if (touch == STATE_TIMER_B_1MIN) {
timerSetTime += 1 * 60;
drawTimerSetTime(timerSetTime);
} else if (touch == STATE_TIMER_B_1HOUR) {
timerSetTime += 60 * 60;
drawTimerSetTime(timerSetTime);
} else if (touch == STATE_TIMER_B_START) {
state = StateTimerGoing;
return;
}
break;
The timer uses a different method to the stopwatch to keep time. Instead of storing the timestamp it was started, it actually ticks the timer’s value down. Tick is reused to ensure this happens every second.
case StateTimerGoing:
if (prevState != state) { // Repaint
drawTimerGoing();
drawTimerGoingTime(timerSetTime, false);
tick = millis();
break;
}
if (millis() >= tick) {
tick += 1000;
timerSetTime -= 1; // Tick down 1 second
if (timerSetTime == 0) {
// Finished timer!
state = StateTimerRinging;
return;
}
drawTimerGoingTime(timerSetTime, false);
if (timerStopConfirm != 0 && millis() -
timerStopConfirm > 3000) {
drawTimerStopConfirm(false);
// Undraw confirmation after 3s
timerStopConfirm = 0;
}
}
if (touch == STATE_ALL_B_BACK) {
state = StateTimerSet;
return;
} else if (touch == STATE_TIMERG_B_STOP) {
if (timerStopConfirm == 0) {
drawTimerStopConfirm(true);
timerStopConfirm = millis();
} else {
state = StateHome;
return;
}
}
break;
The timer’s “Stop” button doesn’t stop it immediately. It instead draws a confirmation “Sure?” Button, and requires a second touch to end the timer, to prevent accidentally stopping it. timerStopConfirm is another timer variable, used to revert the stop button from the “Sure?” confirmation back to “Stop” after 3 seconds.
case StateTimerRinging:
if (prevState != state) { // Repaint
if (timerStopConfirm != 0) {
drawTimerStopConfirm(false);
// Undraw confirmation when flipping
timerStopConfirm = 0;
}
drawTimerGoingUp();
drawTimerGoingTime(timerSetTime, true);
digitalWrite(BUZZER_PIN, HIGH);
break;
}
if (millis() >= tick) {
tick += 1000;
timerSetTime += 1; // Tick up 1 second
digitalWrite(BUZZER_PIN,
!(timerSetTime % 3 == 2));
// Quick method for 2s on, 1s off buzzer tone.
drawTimerGoingTime(timerSetTime, true);
// Only render on the minute.
}
if (touch == STATE_ALL_B_BACK) {
digitalWrite(BUZZER_PIN, LOW);
state = StateTimerSet;
return;
} else if (touch == STATE_TIMERG_B_STOP) {
digitalWrite(BUZZER_PIN, LOW);
state = StateHome;
return;
}
break;
The final state to handle is when the timer runs out. This involves flipping the time so it counts up and activating the buzzer. The logic to draw the time is mostly the same, and the “Stop” button no longer requires confirmation.
default:
break;
}
prevState = state;
delay(LOOP_DELAY);
}
The last part of the sketch closes the state machine case statement, sets prevState, and delays a short time.
There you have it! Once you load the code it should all fire up and you're ready to go! Operation is self explanatory so we really don't need to go over it here.
As you can see - making devices that allow simple human function can take far more code - even for the little things. Creating expected behaviour as far as what a human expects, takes substantial planning and consideration, as well as testing.
Raw functionality can be achieved, but simple things like confirmation screens which don't disrupt the current operation (while still cleanly exiting or returning once a human response is received) can take substantial additional code and finesse.
What can start out as 100 lines of code to create a simple timer, can quickly balloon to almost 400 lines of code as is used here (that doesn't include libraries or anything else either). We tend not to post substantial code these days, but explanation of this approach and handling of events we felt was useful to do here.
Take some time to peruse through the code in the digital resources to gain a full understanding of what's going on too.
WHERE TO FROM HERE?
While the project was envisioned as a simple timer, there are definitely features that could be added to make it even more useful.
- Add commonly used timer settings to the timer screen for your favourite recipes.
- Use a WiFi-enabled Arduino to send a notification to your mobile phone when the timer is up (see our Secret Code article on using Pushbullet in this issue).
- Make a multi-channel timer so you can monitor more than one thing at a time.