Pomodor Timer
I recently started using the Pomodoro technique to help me focus on tasks. I find it helps me to break up my day into manageable chunks and helps me to focus on the task at hand. I have been using a simple timer on my phone but I thought it would be fun to write my own.
I was looking through my drawer of random electronics and I found a waveshare 2.13 inch e-ink display. I thought this would be a perfect project for it. I don't need, or particularly want, a bright screen so an e-ink display would be perfect.
I could write a simple timer and have it display on the screen. I could also add some buttons to control the timer. I could even add a buzzer to alert me when the timer is up.
I'm using a raspberry pi zero w for this project as its small and has wifi built in and I have a few lying about.
Parts List
- Raspberry Pi Zero W
- Waveshare 2.13 inch e-ink display
- 3 buttons
- Speaker
- 3D printed case
Software
As of writing this im currently still working on the software. I started by extracting the necessary libraries from the examples provided by Waveshare, specifically epd2in13_V3.py
and epdconfig.py
and I made a basic countdown timer and rendered it on the screen, I used the font file provided in the examples, this was just to test the maximum font size really, and to test that I could actually write to the screen.
One issue I had was that the countdown rendered on the screen was not updating correctly due to the time it took in the loop. I had to calculate the time it took to render the screen and sleep for the remaining time of the second. This was a simple fix but it took me a while to figure out.
as I said im not a python dev so apologies if this is a mess.
#!/usr/bin/python
# -*- coding:utf-8 -*-
import sys
import os
import logging
import epd2in13_V3
import time
from PIL import Image, ImageDraw, ImageFont
import traceback
logging.basicConfig(level=logging.DEBUG)
def format_time(seconds):
minutes = seconds // 60
seconds = seconds % 60
return f"{minutes:02}:{seconds:02}"
try:
epd = epd2in13_V3.EPD()
epd.init()
font_size = 100
font = ImageFont.truetype(('Font.ttc'), font_size)
time_image = Image.new('1', (epd.height, epd.width), 255)
time_draw = ImageDraw.Draw(time_image)
epd.displayPartBaseImage(epd.getbuffer(time_image))
countdown_seconds = 5 * 60 # 5 minutes in seconds
while countdown_seconds >= 0:
loop_start_time = time.time()
current_time = format_time(countdown_seconds)
time_draw.rectangle((0, 0, epd.height, epd.width), fill=255)
text_width, text_height = time_draw.textsize(current_time, font=font)
x = (epd.height - text_width) // 2
y = (epd.width - text_height) // 2
time_draw.text((x, y), current_time, font=font, fill=0)
epd.displayPartial(epd.getbuffer(time_image))
# Calculate the remaining time in the countdown
countdown_seconds -= 1
# Calculate the elapsed time and sleep for the remaining time of the second
elapsed_time = time.time() - loop_start_time
sleep_time = max(0, 1.0 - elapsed_time)
time.sleep(sleep_time)
epd.init()
except IOError as e:
logging.info(e)
except KeyboardInterrupt:
logging.info("ctrl + c:")
epd.Clear(0xFF)
epd2in13_V3.epdconfig.module_exit(cleanup=True)
exit()
Success! I was worried the framerate would be too slow but it seems to be updating fast enough for my needs.
As well as adding buttons to this to stop and start the timer, I also want a web interface, so I can do it from my phone/laptop. Also my buttons have yet to arrive so this gave me something to do!
I added the flask library to the project and started a simple web server to control the timer. I added a start, stop and a reset button to the web interface. I also added a set time form to set the time of the timer. I also added a route to get the current time of the timer, this is so I can update the web interface with the current time shown on the device.
#!/usr/bin/python
# -*- coding:utf-8 -*-
import sys
import os
import logging
import epd2in13_V3
import time
from PIL import Image, ImageDraw, ImageFont
import traceback
from flask import Flask, render_template, request, redirect, url_for, jsonify
from threading import Thread
logging.basicConfig(level=logging.DEBUG)
app = Flask(__name__)
countdown_seconds = 5 * 60 # 5 minutes in seconds
timer_running = False
epd = epd2in13_V3.EPD()
def format_time(seconds):
minutes = seconds // 60
seconds = seconds % 60
return f"{minutes:02}:{seconds:02}"
def run_countdown():
global countdown_seconds, timer_running
epd.init()
font_size = 100
font = ImageFont.truetype(('Font.ttc'), font_size)
time_image = Image.new('1', (epd.height, epd.width), 255)
time_draw = ImageDraw.Draw(time_image)
epd.displayPartBaseImage(epd.getbuffer(time_image))
while timer_running and countdown_seconds >= 0:
loop_start_time = time.time()
current_time = format_time(countdown_seconds)
time_draw.rectangle((0, 0, epd.height, epd.width), fill=255)
text_width, text_height = time_draw.textsize(current_time, font=font)
x = (epd.height - text_width) // 2
y = (epd.width - text_height) // 2
time_draw.text((x, y), current_time, font=font, fill=0)
epd.displayPartial(epd.getbuffer(time_image))
countdown_seconds -= 1
elapsed_time = time.time() - loop_start_time
sleep_time = max(0, 1.0 - elapsed_time)
time.sleep(sleep_time)
logging.info("Clear...")
epd.init()
@app.route('/')
def index():
return render_template('index.html', countdown_time=format_time(countdown_seconds), timer_running=timer_running)
@app.route('/start', methods=['POST'])
def start_timer():
global timer_running
if not timer_running:
timer_running = True
countdown_thread = Thread(target=run_countdown)
countdown_thread.start()
return redirect(url_for('index'))
@app.route('/stop', methods=['POST'])
def stop_timer():
global timer_running
timer_running = False
return redirect(url_for('index'))
@app.route('/reset', methods=['POST'])
def reset_timer():
global countdown_seconds, timer_running
timer_running = False
countdown_seconds = 5 * 60 # Reset to 5 minutes
epd.Clear(0xFF)
return redirect(url_for('index'))
@app.route('/set_time', methods=['POST'])
def set_time():
global countdown_seconds, timer_running
timer_running = False # Stop the timer if running
minutes = int(request.form['minutes'])
seconds = int(request.form['seconds'])
countdown_seconds = minutes * 60 + seconds
return redirect(url_for('index'))
@app.route('/time')
def get_time():
global countdown_seconds, timer_running
return jsonify(time=format_time(countdown_seconds), running=timer_running)
if __name__ == '__main__':
try:
app.run(host='0.0.0.0', port=5000)
except IOError as e:
logging.info(e)
except KeyboardInterrupt:
logging.info("ctrl + c:")
epd2in13_V3.epdconfig.module_exit(cleanup=True)
exit()
The html is pretty simple, it lives in a templates
folder in the project. Theres a little bit of javascript to update the time every second. I also disable the start button if the timer is running and disable the stop button if the timer is not running. It's all very basic at the moment but it's functional. Theres currently no styling etc but that will come later.
<!DOCTYPE html>
<html>
<head>
<title>Countdown Timer</title>
<script>
function updateTime() {
fetch("/time")
.then((response) => response.json())
.then((data) => {
document.getElementById("timer").innerText = data.time;
document.getElementById("startBtn").disabled = data.running;
document.getElementById("stopBtn").disabled = !data.running;
});
}
function startUpdatingTime() {
updateTime();
setInterval(updateTime, 1000); // Update every second
}
window.onload = startUpdatingTime;
</script>
</head>
<body>
<h1>Countdown Timer</h1>
<p id="timer">{{ countdown_time }}</p>
<form action="/start" method="post">
<button id="startBtn" type="submit">Start</button>
</form>
<form action="/stop" method="post">
<button id="stopBtn" type="submit">Stop</button>
</form>
<form action="/reset" method="post">
<button type="submit">Reset</button>
</form>
<h2>Set Countdown Time</h2>
<form action="/set_time" method="post">
<label for="minutes">Minutes:</label>
<input type="number" id="minutes" name="minutes" min="0" required />
<label for="seconds">Seconds:</label>
<input
type="number"
id="seconds"
name="seconds"
min="0"
max="59"
required
/>
<button type="submit">Set Time</button>
</form>
</body>
</html>
The end result is:
I'll continue to update this post as I go, the buttons and speaker are supposed to arrive today but we'll see...