I am making a Pomodoro lamp with a touch sensor as the input and two LEDs (warm and white) as the output. I have already created some of the functions needed for it to work. Now I need to build the Pomodoro logic and arrange everything in order. I’ll first explain how I expect it to work, and then I’ll share the functions and features I’ve already made.
How it is supposed to work:- When power is turned on, the brightness of the warm LED should smoothly increase to the default brightness.
- Then, a double tap should take the device into the Pomodoro time-setting mode. In this mode, single Tap will cycle through the preset durations: 15, 30, 45, 60, 75, and 90 minutes.
- Once the time is selected, the set time should be conveyed to the user using only the two LEDs (no extra components).
- If the user doesn’t touch the device for 10 seconds, it should go into standby mode.
- To confirm and exit the time-setting mode, the user should double tap again.
After confirmation, the Pomodoro timer starts:
- The warm LED should slowly fade out while the white LED fades in.
- In the last 10 seconds of the timer, the white LED should fade out and the warm LED should fade back in.
- After confirmation, the Pomodoro timer starts:The warm LED should slowly fade out while the white LED fades in.In the last 10 seconds of the timer, the white LED should fade out and the warm LED should fade back in.
- After the work session ends, the warm LED should glow steadily for 5 minutes (break time).
- Single tap to exit work
- When the 5-minute break ends, the warm LED should enter a breathing animation until the user taps the sensor—this indicates the user is back.
After the tap, the warm LED stays on for 30 seconds. During this window:
- The user can change the Pomodoro duration (same logic as before).
- If the user is changing the time, the timer should not start.
- If untouched for 10 seconds, the device should go into standby mode.
- A double tap exits the time-setting mode and starts the next Pomodoro cycle.
- After the tap, the warm LED stays on for 30 seconds. During this window:The user can change the Pomodoro duration (same logic as before).If the user is changing the time, the timer should not start.If untouched for 10 seconds, the device should go into standby mode.A double tap exits the time-setting mode and starts the next Pomodoro cycle.
- Additionally, when the device is in standby or not in time-setting mode, single Tap should cycle through brightness levels.
- Long press will be used for tuning off and turning on the device.
I will also have a program to use it as a normal lap.
- In this case the lamp will tun on with the same cycle as mentioned above.
- The device turning on with the warm light on.
- And double tap will toggle between the warm and cool light
- The long press will tun off and turn on the device.
- And single tap will cycle through the brightness on the device
Note: the brightness toggle won't work when the device is in a transaction state that is the when the light is switching from one color to another this will be same for the pomodoro setting as well as for the normal lap setting.
Design Motivation & InspirationThe inspiration for this project came from a personal challenge I often faced while working—either overworking without taking breaks or, at times, not working for a sufficient duration. To address this, I planned a device that encourages better work–break balance.
From the beginning, I wanted the design to be simple and intuitive, with a single input and a single output. The device communicates with the user using light as the primary feedback mechanism, avoiding screens, sounds, or complex interactions. This approach was chosen to ensure that the device is non-distracting and easy to use during focused work sessions.
A strong emphasis was also placed on aesthetics. The device is designed to function not only as a productivity aid but also as a decorative object—something that can comfortably sit on a user’s desk as a showpiece rather than appearing purely functional or technical.
The following images show some of the lamp designs referenced from Pinterest, which influenced the overall form, material choice, and visual language of the project.
During the initial development stage, a XIAO RP2040 microcontroller was used for testing and troubleshooting. For the final version, this was replaced with a smaller and more cost-effective microcontroller, such as an ATtiny, since the device requires only three GPIO pins. Using a more powerful microcontroller would be unnecessary for this application.
The input and output components of the device include a touch sensor (external module) and a 12 V LED light source. Because the microcontroller operates at 3.3 V logic, two N-channel MOSFETs are used to safely interface and control the 12 V LED strip.
Power is supplied to the device through a DC power jack, into which a 12 V adapter is connected. The PCB includes a voltage step-down circuit to convert 12 V to 5 V, which is then used as the input supply for the microcontroller.
The PCB is designed as a single-sided board incorporating all required components. The touch sensor is implemented using a separate, commercially available module rather than being integrated directly onto the PCB.
The PCB was fabricated using a PCB milling machine.
The image below shows the schematic of the board, followed by an image of the PCB traces layout and the 3D model of the board.
+----+-----------------------------------------+-----+
| No | Component Name | Qty |
+----+-----------------------------------------+-----+
| 1 | Electrolytic Capacitor 100µF (Panasonic)| 1 |
| 2 | Capacitor 10µF | 1 |
| 3 | Capacitor 1µF | 1 |
| 4 | Resistor 1kΩ | 2 |
| 5 | Resistor 10kΩ | 2 |
| 6 | Diode | 2 |
| 7 | AMS1117‑5.0 Voltage Regulator | 1 |
| 8 | XIAO RP2040 (SMD Module) | 1 |
| 9 | MOSFET AO3400A | 2 |
| 10 | Warm LED strip | 1 |
| 11 | Cool LED Strip | 1 |
| 12 | Power Jack 2x5.5mm | 1 |
| 13 | TTP223 Capacitive Touch Sensor | 1 |
+----+-----------------------------------------+-----+For the construction of the board, most of the components—except the XIAO and capacitive sensor —were sourced from scrap available in our lab. These scrap boards had been lying unused for a long time. One of the main challenges faced during this process was identifying and selecting the correct components, particularly resistors and capacitors, from the available scrap.
These are all the components that were taken from scrap, and they were assembled onto the board.
As the next step, I assembled the XIAO ESP32-C6 onto the board. Since this was the only controller available, it was used even though it is overpowered for this application. This also allowed me to experiment with OTA (Over-The-Air) updates on the board. In addition, the sensor and all LED strips were soldered onto the board. The connections for the warm and cool LEDs were clearly marked on the board as W and C respectively.
The lamp was designed using Fusion 360 and Rhinoceros (Rhino). The lamp shade was developed in Rhino using Grasshopper, allowing a parametric approach to control the form and geometry of the shade. The image below shows the Grasshopper node tree used to generate the 3D model of the shade. This parametric setup made it easy to iterate and refine the design by adjusting key parameters.
The following images show the lamp shade model. The image on the left represents the standard viewport view, while the image on the right shows the same model in rendered view, highlighting the overall form and surface quality.
All other components, including the base, internal mounts, and top cover, were designed in Fusion 360. Fusion was chosen for these parts due to its strength in precise mechanical design and assembly planning. A sectional view is shown below to clearly illustrate the internal layout and how the PCB, PVC pipe, and other components are assembled inside the base.
Finally, the following images show rendered views of the complete lamp from different angles, representing the final appearance and overall design intent of the project.
The lamp shade is constructed using 3D printing, while the base can either be 3D printed or made from wood as an alternative for improved aesthetics. A PVC pipe is used to hold the LED strips and sensor, providing structural support between the base and the shade.
The touch sensor is positioned at the top of the lamp, as shown in the design, allowing easy user interaction. The power input is placed at the bottom of the base, where a 12 V DC adapter can be connected for powering the device.
For 3D-printed parts, PLA is used as the printing material. The shade is printed using matte white PLA to achieve better light diffusion. The base is printed in black PLA or any preferred color, then sanded, post-processed, and painted black for a refined finish.
The image shows the final assembled desk lamp prototype. The lamp features a touch-sensitive input at the top, which is used to adjust brightness during charging, toggle work time, and power the lamp on or off. A 12 V DC power input is provided at the base of the lamp. The cylindrical enclosure diffuses the light evenly, creating a soft and uniform glow suitable for desk and ambient lighting applications.
1. Prepare the PVC Pipe :
- Cut the 32 mm diameter PVC pipe to the length specified in the design.
- Create a slot at the top of the pipe as shown in the design drawings. This slot is used to secure the 3D-printed sensor mount.
- Drill a small hole on the side of the PVC pipe near the bottom. This hole will be used to route the sensor wires down to the PCB.
2. Prepare and Mount the Sensor :
- Cut the required length of wires for the sensor.
- Solder the wires to the sensor before mounting it.
- Fix the sensor into the 3D-printed top mount.
- Insert the mounted sensor into the slot on top of the PVC pipe.
- Route the sensor wires down through the PVC pipe and out through the side hole.
3. Attach the LED Strips
- Wrap the LED strips evenly around the PVC pipe as per the design.
- Secure the LED strips using their adhesive backing or glue if required.
- Route the LED wires toward the bottom of the pipe.
4. Assemble the PCB
- Solder all electronic components onto the PCB.
- Solder the sensor wires and LED strip wires to their respective pads on the PCB.
- Upload the firmware to the microcontroller at this stage before final assembly.
5. Mount the PCB and Pipe to the Base :
- Place the assembled PCB into the base, aligning it with the provided locating pins.
- Insert and fix the PVC pipe into the base.
- Ensure that no wires are pinched or stressed during placement.
6. Power-On Test :
Power the device and verify:
- Sensor is detected correctly
- LED strips function as expected
- Power the device and verify:Sensor is detected correctlyLED strips function as expected
- If any issues are found, fix them before proceeding.
7. Final Assembly :
- Glue the top cover to the shade.
- Mount the completed shade assembly onto the main body.
- Allow the glue to cure fully before use.
Once all steps are completed, the device is ready for use.
Assembly VideoPROGRAMSIn the programming phase, two separate programs were developed. The first program is a simple table lamp control program. The second program implements the Pomodoro timer, which was the intended functionality of this project. For this reason, the project was named POMO.
Simple Lamp Program/*******************************************************
* Smart Lamp with Touch Control + OTA Update
*
* Features:
* - Warm / Cold LED control
* - Single tap → change brightness
* - Double tap → switch warm ↔ cold LED
* - Long press → turn lamp ON / OFF
* - Smooth brightness transitions
* - Over-the-air (OTA) firmware updates via WiFi
*******************************************************/
#include <Arduino.h>
#include <WiFi.h>
#include <ArduinoOTA.h>
#include "Brightnes_A_to_B_Function.h"
#include "Touch_Function.h"
/* ================= Pin Definitions ================= */
#define Warm_LED D9 // Warm white LED pin
#define Cold_LED D10 // Cold white LED pin
#define Touch D7 // Touch input pin
/* ================= Timing ================= */
const unsigned startup_cycle_Time = 3000; // Startup animation duration (ms)
/* ================= Brightness Variables ================= */
int Default_Brightness = 200; // Current target brightness
int Last_Brightness = 0; // Previous brightness (used for smooth transition)
int current_Brightness_warm = 0; // Current PWM value for warm LED
int current_Brightness_cool = 0; // Current PWM value for cold LED
int LED = Warm_LED; // Currently active LED (Warm by default)
bool lampOn = true; // Lamp power state
/* ================= WiFi Credentials ================= */
const char* ssid = "SSID";
const char* password = "PASSWORD";
/* ==================================================== */
/* ======================== SETUP ===================== */
/* ==================================================== */
void setup() {
Serial.begin(115200);
Serial.println("Booting...");
/* ---------- WiFi Connection ---------- */
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
unsigned long counter = millis();
delay(500);
Serial.print(".");
// Exit loop if WiFi takes too long
if (counter > 20000) {
Serial.println("\nWiFi connection failed. Continuing without WiFi.");
break;
}
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWoo Hoo! Connected to WiFi");
}
/* ---------- Pin Configuration ---------- */
pinMode(Warm_LED, OUTPUT);
pinMode(Cold_LED, OUTPUT);
pinMode(Touch, INPUT);
/* ---------- Startup Animation ---------- */
Serial.println("Startup animation");
// Fade in warm LED
Brightness_a_b(0, Default_Brightness, Warm_LED, startup_cycle_Time);
// Fade in cold LED
Brightness_a_b(0, Default_Brightness, Cold_LED, startup_cycle_Time);
// Fade out cold LED (leave warm ON)
Brightness_a_b(Default_Brightness, 0, Cold_LED, startup_cycle_Time);
Serial.println("Device Ready");
/* ---------- OTA Setup ---------- */
ArduinoOTA.begin();
}
/* ==================================================== */
/* ========================= LOOP ===================== */
/* ==================================================== */
void loop() {
// Handle OTA update requests
ArduinoOTA.handle();
// Read and process touch input
updateTouch(Touch);
/* ---------- Double Tap: Switch LED ---------- */
if (doubleTap && lampOn) {
Serial.println("Double tap detected");
// Fade out current LED
Brightness_a_b(Default_Brightness, 0, LED, 1500);
// Toggle between warm and cold LED
LED = (LED == Warm_LED) ? Cold_LED : Warm_LED;
// Fade in new LED
Brightness_a_b(0, Default_Brightness, LED, 1500);
doubleTap = false;
}
/* ---------- Single Tap: Change Brightness ---------- */
if (singleTap && lampOn) {
Serial.println("Single tap detected");
Last_Brightness = Default_Brightness;
Default_Brightness += 51; // Increase brightness in steps
// Wrap brightness back to low value
if (Default_Brightness > 255) {
Default_Brightness = 51;
}
// Smooth brightness transition
Brightness_a_b(Last_Brightness, Default_Brightness, LED, 100);
singleTap = false;
}
/* ---------- Long Press: Lamp OFF ---------- */
if (longPressActive && lampOn) {
Serial.println("Long press → Lamp OFF");
Brightness_a_b(Default_Brightness, 0, LED, 1500);
lampOn = false;
longPressActive = false;
}
/* ---------- Long Press: Lamp ON ---------- */
if (longPressActive && !lampOn) {
Serial.println("Long press → Lamp ON");
Brightness_a_b(0, Default_Brightness, LED, 1500);
lampOn = true;
longPressActive = false;
}
}
/* ==================================================== */
/* ========== Brightness Transition Function =========== */
/* ==================================================== */
// Smoothly fades LED brightness from value A to B
void Brightness_a_b(int a, int b, int ledPin, unsigned transitionTime) {
unsigned long startTime = millis();
while (millis() - startTime < transitionTime) {
// Progress value from 0 → 1
float progress = float(millis() - startTime) / transitionTime;
// Sinusoidal easing for smooth fade
float brightness = (sin(progress * PI - PI / 2) + 1.0) / 2.0;
// Calculate PWM value
int pwmValue = a + (b - a) * brightness;
analogWrite(ledPin, pwmValue);
// Store and print current brightness
if (ledPin == Warm_LED) {
current_Brightness_warm = pwmValue;
}
if (ledPin == Cold_LED) {
current_Brightness_cool = pwmValue;
}
delay(1); // Small delay for smooth animation
}
}
/* ==================================================== */
/* ================ Touch Event System ================= */
/* ==================================================== */
/* ---------- Output Events ---------- */
bool singleTap = false;
bool doubleTap = false;
bool longPressActive = false;
bool longPressReleased = false;
unsigned long longPressTime = 0;
/* ---------- Internal State ---------- */
static bool isPressed = false;
static bool longPressStarted = false;
static unsigned long pressStart = 0;
static unsigned long lastReleaseTime = 0;
static int tapCount = 0;
/* ---------- Touch Settings ---------- */
const unsigned long doubleTapGap = 300; // Max gap between taps (ms)
const unsigned long longPressThreshold = 600; // Time for long press (ms)
/* ---------- Touch Processing Function ---------- */
void updateTouch(int pin) {
bool raw = digitalRead(pin);
// Detect press start
if (raw && !isPressed) {
isPressed = true;
pressStart = millis();
longPressStarted = false;
}
// While finger is touching
if (raw && isPressed) {
longPressTime = millis() - pressStart;
// Detect long press start
if (!longPressStarted && longPressTime >= longPressThreshold) {
longPressStarted = true;
longPressActive = true;
Serial.println("Long press detected");
}
}
// Detect release
if (!raw && isPressed) {
isPressed = false;
if (longPressStarted) {
longPressActive = false;
longPressReleased = true;
tapCount = 0; // Prevent tap detection
Serial.println("Long press released");
} else {
tapCount++;
lastReleaseTime = millis();
}
}
// Decide between single and double tap
if (tapCount > 0 && (millis() - lastReleaseTime) > doubleTapGap) {
if (tapCount == 1) singleTap = true;
if (tapCount >= 2) doubleTap = true;
tapCount = 0;
}
}Pomodoro Lamp Program
#include <Arduino.h>
/* =====================================================
PIN DEFINITIONS
===================================================== */
#define Warm_LED D9
#define Cold_LED D10
#define Touch D7
/* =====================================================
TOUCH OUTPUT EVENTS (GLOBAL)
===================================================== */
bool singleTap = false;
bool doubleTap = false;
bool tripleTap = false;
bool longPressActive = false;
bool longPressReleased = false;
unsigned long longPressTime = 0;
/* =====================================================
LAMP & SYSTEM STATES
===================================================== */
bool Warm_State = false;
bool Cool_State = false;
bool Touch_State = false;
bool pomodoromode = false;
bool lampOn = true;
/* =====================================================
BRIGHTNESS VARIABLES
===================================================== */
int Default_Brightness = 200;
int current_Brightness_warm = 0;
int current_Brightness_cool = 0;
int Last_Brightness = 0;
int LED = Warm_LED;
/* =====================================================
STARTUP SETTINGS
===================================================== */
const unsigned startup_cycle_Time = 3000;
/* =====================================================
BREATHING VARIABLES
===================================================== */
bool breathing_in_progress = false;
float breathPhase = 0.0;
unsigned long lastUpdate = 0;
const int breathSpeed = 4000;
int currentBrightness = 0;
int maxBrightness = 255;
int minBrightness = 0;
int ledPinGlobal = -1;
/* =====================================================
POMODORO VARIABLES
===================================================== */
const unsigned long NO_INPUT_EXIT_TIME = 10000;
unsigned long lastTouchTime = 0;
bool pomodoroTimeSettingMode = false;
bool pomodoroActive = false;
bool In_Work = false;
bool In_Break = false;
bool Exceeded_break_time = false;
bool Breathing = false;
unsigned long workStartTime = 0;
unsigned long breakStartTime = 0;
unsigned long breakDuration = 30000;
unsigned long Work_Time = 30000;
unsigned long workDuration = 30000;
static unsigned long lastMinutePrint = 0;
bool A = false;
bool B = false;
/* =====================================================
TOUCH FUNCTION
===================================================== */
void updateTouch(int pin) {
static bool isPressed = false;
static bool longPressStarted = false;
static unsigned long pressStart = 0;
static unsigned long lastReleaseTime = 0;
static int tapCount = 0;
const unsigned long doubleTapGap = 300;
const unsigned long longPressThreshold = 600;
bool raw = digitalRead(pin);
if (raw && !isPressed) {
isPressed = true;
pressStart = millis();
longPressStarted = false;
}
if (raw && isPressed) {
longPressTime = millis() - pressStart;
if (!longPressStarted && longPressTime >= longPressThreshold) {
longPressStarted = true;
longPressActive = true;
Serial.println("long press detected");
}
}
if (!raw && isPressed) {
isPressed = false;
if (longPressStarted) {
longPressActive = false;
longPressReleased = true;
tapCount = 0;
} else {
tapCount++;
lastReleaseTime = millis();
}
}
if (tapCount > 0 && millis() - lastReleaseTime > doubleTapGap) {
if (tapCount == 1) singleTap = true;
if (tapCount == 2) doubleTap = true;
if (tapCount >= 3) tripleTap = true;
tapCount = 0;
}
}
/* =====================================================
BRIGHTNESS A → B
===================================================== */
void Brightness_a_b(int a, int b, int ledPin, unsigned transitionTime) {
unsigned long startTime = millis();
while (millis() - startTime < transitionTime) {
float progress = float(millis() - startTime) / transitionTime;
float brightness = (sin(progress * PI - PI / 2) + 1.0) / 2.0;
int pwmValue = a + (b - a) * brightness;
analogWrite(ledPin, pwmValue);
if (ledPin == Warm_LED) current_Brightness_warm = pwmValue;
if (ledPin == Cold_LED) current_Brightness_cool = pwmValue;
delay(1);
}
}
/* =====================================================
BREATHING FUNCTIONS
===================================================== */
void breathingFromDefault(int ledPin, int minB, int maxB) {
breathing_in_progress = true;
breathPhase = 0.0;
lastUpdate = millis();
ledPinGlobal = ledPin;
minBrightness = minB;
maxBrightness = maxB;
}
void breathingLoop() {
if (!breathing_in_progress || ledPinGlobal == -1) return;
unsigned long now = millis();
float delta = (now - lastUpdate) / float(breathSpeed);
lastUpdate = now;
breathPhase += delta * 2 * PI;
if (breathPhase > 2 * PI) breathPhase -= 2 * PI;
float wave = (sin(breathPhase - PI / 2) + 1.0) / 2.0;
currentBrightness = minBrightness + (maxBrightness - minBrightness) * wave;
analogWrite(ledPinGlobal, currentBrightness);
}
void stopBreathing() {
breathing_in_progress = false;
ledPinGlobal = -1;
}
/* =====================================================
POMODORO FUNCTION
===================================================== */
void Pomodoro() {
lastTouchTime = millis();
if (pomodoromode) {
pomodoroTimeSettingMode = true;
if (singleTap && pomodoroTimeSettingMode) {
Work_Time += 600000;
singleTap = false;
}
if (doubleTap) {
pomodoroActive = true;
pomodoroTimeSettingMode = false;
workDuration = Work_Time;
doubleTap = false;
}
}
if (pomodoroActive && !In_Work) {
In_Work = true;
workStartTime = millis();
}
if (In_Work && millis() - workStartTime >= workDuration) {
In_Work = false;
In_Break = true;
breakStartTime = millis();
}
if (In_Break && millis() - breakStartTime >= breakDuration) {
breathingLoop();
Breathing = true;
}
if (singleTap && Breathing) {
stopBreathing();
In_Break = false;
Breathing = false;
pomodoroActive = false;
pomodoromode = false;
singleTap = false;
}
}
/* =====================================================
SETUP
===================================================== */
void setup() {
Serial.begin(9600);
pinMode(Warm_LED, OUTPUT);
pinMode(Cold_LED, OUTPUT);
pinMode(Touch, INPUT);
Brightness_a_b(0, Default_Brightness, Warm_LED, startup_cycle_Time);
Brightness_a_b(0, Default_Brightness, Cold_LED, startup_cycle_Time);
Brightness_a_b(Default_Brightness, 0, Cold_LED, startup_cycle_Time);
breathingFromDefault(Warm_LED, 10, Default_Brightness);
}
/* =====================================================
LOOP
===================================================== */
void loop() {
updateTouch(Touch);
if (doubleTap && lampOn && !pomodoromode) {
Brightness_a_b(Default_Brightness, 0, LED, 1500);
LED = (LED == Warm_LED) ? Cold_LED : Warm_LED;
Brightness_a_b(0, Default_Brightness, LED, 1500);
doubleTap = false;
}
if (singleTap && lampOn && !pomodoromode) {
Last_Brightness = Default_Brightness;
Default_Brightness += 51;
if (Default_Brightness > 255) Default_Brightness = 51;
Brightness_a_b(Last_Brightness, Default_Brightness, LED, 100);
singleTap = false;
}
if (longPressActive && lampOn && !pomodoromode) {
Brightness_a_b(Default_Brightness, 0, LED, 1500);
lampOn = false;
longPressActive = false;
}
if (longPressActive && !lampOn && !pomodoromode) {
Brightness_a_b(0, Default_Brightness, LED, 1500);
lampOn = true;
longPressActive = false;
}
if (tripleTap && !pomodoromode) {
pomodoromode = true;
tripleTap = false;
}
if (pomodoromode) Pomodoro();
}Future of the Project
As mentioned earlier, the next step of this project is to design a cheaper version of the board using an ATtiny microcontroller. The board shown on the right is designed around the ATtiny412, which significantly reduces the overall cost of the system.
After presenting the project to friends and peers, it was suggested that the lamp could be made more visually appealing by adding color. Based on this feedback, a new board was designed to support NeoPixel LEDs using a XIAO board, which is shown on the left.
The documentation will be updated once the development and testing of these boards are completed.







_t9PF3orMPd.png?auto=compress%2Cformat&w=40&h=40&fit=fillmax&bg=fff&dpr=2)

Comments