LumaDesk.
Hello, useful.
I want my first LCD hello world to grow into something useful: a focus companion that responds to my desk lighting. I use the ELEGOO Mega 2560 R3 Most Complete Starter Kit as the starting point.
My project notebook · inputs → state → feedback
LUMADESK / INTERACTIVE PREVIEW
A0700 REF0700
A SMALL SYSTEM WITH A REAL PURPOSE
Make study time tangible.
I want to keep a focus interval off my phone and on my desk. The display shows the countdown, a button pauses it, and the RGB indicator makes the state visible. I use a photoresistor to notice a drop from my chosen lighting reference and prompt a lamp check.
Start or pause a focus session. A completed focus interval prepares a 5-minute break. Each next phase waits for a button press.
Set the reference with your desk lit as you prefer. A moving average and two thresholds reduce flicker as lighting changes.
I use serial readings, test results, photos, and debugging notes to explain what happens and why.
How I interpret the reading: the LDR is not a lux meter, the chosen reference is subjective, and this is not a medical, ergonomic, or productivity measurement device. It does not switch lamps or connect to Wi-Fi, a phone, or the cloud.
CHECK THE ACTUAL KIT REVISION
Start with what’s in the box.
I check each part against my kit’s packing list. Labels and RGB leg order can vary, so I follow the printed labels and the tutorial for the exact component.
| Quantity | Part | Purpose |
|---|---|---|
| 1 | Mega 2560 R3 + USB cable | Controller and USB power |
| 1 each | Breadboard, jumper wires | Connections; number depends on layout |
| 1 | LCD1602, 16-pin parallel type | Two-line display; not the I²C-backpack version |
| 1 | 10 kΩ potentiometer | LCD contrast |
| 1 | Photoresistor (LDR) | Relative light sensor |
| 1 | 10 kΩ fixed resistor | LDR voltage divider |
| 1 | RGB LED | Status indicator; verify common-cathode type |
| 4 | 220 Ω resistors | One per RGB channel; one for LCD backlight |
| 2 | Momentary push buttons | Start/Pause and Calibrate/Reset |
| Optional: 1 each | Small passive piezo + 1 kΩ resistor | Quiet completion cue; omit in silent mode |
I break the work into three layers: board and LCD; controls and sensor; integration and measurement. Each checkpoint gives me a way to isolate a problem.
POWER OFF BEFORE MOVING WIRES
Wire by the labels.
Use the Mega’s USB connection for power. Keep the breadboard power supply, battery, motors, relay, and external adapters disconnected. First identify LCD pin 1 and the RGB common leg; do not infer them from a photo or a rotated component.
Logical connection map, not a breadboard placement drawing. The tables below are the pin-by-pin source of truth.
LCD1602 connections
| LCD pin | Connect to | Note |
|---|---|---|
| 1 · VSS | GND | Display ground |
| 2 · VDD | 5V | Logic supply |
| 3 · VO | 10 kΩ pot middle pin / wiper | Pot outer pins → 5V and GND |
| 4 · RS | Mega D22 | Register select |
| 5 · RW | GND | Write-only operation |
| 6 · E | Mega D23 | Enable |
| 7–10 · D0–D3 | Leave unconnected | Unused in 4-bit mode |
| 11 · D4 | Mega D24 | Data |
| 12 · D5 | Mega D25 | Data |
| 13 · D6 | Mega D26 | Data |
| 14 · D7 | Mega D27 | Data |
| 15 · A / LED+ | 5V through 220 Ω | Backlight current limiting |
| 16 · K / LED− | GND | Backlight return |
Controls, light sensor, and indicator
| Part | Connection | Check |
|---|---|---|
| LDR divider | 5V → LDR → junction → 10 kΩ → GND | Junction → Mega A0. Brighter light should raise the reading. |
| Start button | Mega D2 → switch → GND | INPUT_PULLUP; pressed = LOW. No 5V wire to button. |
| Cal button | Mega D3 → switch → GND | Short tap calibrates; hold 1.5 seconds resets session. |
| RGB red | Mega D5 → 220 Ω → red anode | Identify the color leg from the kit pinout. |
| RGB green | Mega D6 → 220 Ω → green anode | Use a separate resistor, not one in the common leg. |
| RGB blue | Mega D7 → 220 Ω → blue anode | Common cathode → GND; confirm your actual part. |
| Optional passive piezo | Mega D8 → 1 kΩ → (+); (−) → GND | Use only the small passive piezo; no speaker or motor. |
| Power rails | Mega 5V → + rail; Mega GND → − rail | All module grounds must share GND; check for split rails. |
COMMON_ANODE=true and connect its common to 5V. Never omit the channel resistors.GET ONE THING WORKING AT A TIME
From first upload to first system.
0. Prove the board works
Install Arduino IDE from the official Arduino website. Connect the Mega with a data-capable USB cable. In Tools → Board, choose Arduino AVR Boards → Arduino Mega or Mega 2560; choose ATmega2560 if a processor menu appears. Select the port that appears when connected. Upload File → Examples → 01.Basics → Blink before wiring peripherals.
If no port appears, check the cable and the board’s USB interface documentation. Do not install drivers from an unknown download site.
1. Say “Hello, Ori!”
Unplug USB. Wire only the LCD and its contrast potentiometer using the LCD table. Reconnect and open HelloOri/HelloOri.ino from the extracted ZIP. Install the official LiquidCrystal library through Library Manager if it is missing. This is a parallel LCD sketch; do not substitute LiquidCrystal_I2C.
Upload, then turn the contrast knob slowly. The display should show Hello, Ori! and Let’s build. Get this checkpoint working before proceeding.
// Stage 1 only: bring up the LCD before attaching sensors and controls.
// Use the LCD wiring table in BUILD_GUIDE.html. Other components may stay disconnected.
#include <LiquidCrystal.h>
LiquidCrystal lcd(22,23,24,25,26,27);
void setup(){lcd.begin(16,2);lcd.print("Hello, Ori!");lcd.setCursor(0,1);lcd.print("Let's build.");}
void loop(){}
2. Add the controls and RGB indicator
Unplug USB and add the two buttons and the three LED channel resistors. Open LumaDesk/LumaDesk.ino and keep FocusCore.h beside it in the same folder. Set ENABLE_BUZZER=false if no passive piezo is connected. Upload. The timer and controls can be checked before the LDR is added; ignore light readings until its wiring is complete.
Tap Start to run, tap again to pause, and tap again to resume. Green means focus is running; magenta means paused; blue means ready or break. Yellow has priority when the calibrated light condition is low.
3. Add the light divider and calibrate
Unplug USB, wire the LDR and fixed 10 kΩ resistor, then reconnect. Open Serial Monitor at 115200 baud. Check that the ADC reading rises when the sensor is illuminated and falls when shaded. Arrange your preferred desk lighting, wait a second for the eight-sample average to settle, and tap Cal.
The reference is saved in EEPROM. Readings below 40 or above 980 are rejected as calibration points. If that happens, change the light reaching the LDR and check the divider wiring.
4. Make the whole loop visible
For a quick test, change DEMO_MODE=true and upload again: focus lasts 30 seconds, break 10 seconds. A completed phase does not automatically start the next one. The optional passive piezo gives a short cue. Hold Cal for 1.5 seconds to reset the timer and session count without deleting the light reference.
Restore DEMO_MODE=false and upload again for normal 25/5 use. Saving source changes on your computer alone does not update the board.
WHAT TO UNDERSTAND, NOT JUST COPY
The engineering underneath.
Buttons and an analog sensor become inputs to a state machine. The LCD, LED, and optional buzzer reflect the resulting state.
The sketch checks elapsed millis() instead of using long delay() calls. Buttons, display, and sensor continue to update while time is counted.
Eight ADC samples are averaged. Low-light mode enters below 65% of the reference ADC and clears at 75%: hysteresis avoids rapid color changes near one threshold.
The percentages compare ADC numbers, not actual illumination. A photoresistor is nonlinear, component tolerances vary, and the ADC may saturate. The device has no reliable automatic sensor-disconnection detection. Calibration helps personalize the threshold; it does not make a calibrated light meter.
Read the serial telemetry
ms,phase,state,remaining_s,adc,reference,dim,cycles
One record is emitted per second while a computer is connected. reference=0 means no saved reference has been loaded or set. Save your own real log from Serial Monitor. There is no on-board log storage, and power loss resets the timer and session counter.
COMPLETE SOURCE, WITH A SMALL TESTABLE CORE
Open it. Read it. Change it.
The complete package includes both sketches, FocusCore.h, this guide, the connection map, and a hardware test checklist. Download the ZIP so the sketch folder structure stays intact.
Read the complete application sketch
/*
LumaDesk v1.0 — Guided starter build for Ori Alsheh, prepared 2026-10-21.
Build status: planned; no physical assembly or hardware validation is claimed.
Target: Arduino Mega or Mega 2560 / ATmega2560, USB power only.
Libraries: LiquidCrystal (install via Library Manager if missing), EEPROM (AVR core).
See BUILD_GUIDE.html for every connection and staged bring-up checks.
*/
#include <Arduino.h>
#include <stdio.h>
#include <string.h>
#include <LiquidCrystal.h>
#include <EEPROM.h>
#include "FocusCore.h"
// Use the Mega's digital pins labeled 22..27, NOT the LCD pin numbers.
LiquidCrystal lcd(22,23,24,25,26,27); // RS,E,D4,D5,D6,D7
constexpr uint8_t START_PIN=2, CAL_PIN=3, RED_PIN=5, GREEN_PIN=6, BLUE_PIN=7;
constexpr uint8_t BUZZER_PIN=8, LIGHT_PIN=A0;
constexpr bool ENABLE_BUZZER=true; // Set false if you omit the optional passive piezo.
constexpr bool COMMON_ANODE=false; // Kit tutorial uses common cathode. Verify your part.
constexpr bool DEMO_MODE=false; // true: 30-second focus / 10-second break. Restore false for use.
LumaDesk::Timer timer(DEMO_MODE?30000UL:1500000UL,DEMO_MODE?10000UL:300000UL);
LumaDesk::Button startButton,calButton;
LumaDesk::Light light;
uint16_t readings[8],referenceAdc=500,filteredAdc=0;
uint32_t totalAdc=0,lastSample=0,lastDisplay=0,lastLog=0,messageAt=0;
uint8_t sampleIndex=0;
bool calibrated=false,messageVisible=false;
const char* message="";
void rgb(bool red,bool green,bool blue){
digitalWrite(RED_PIN,(red!=COMMON_ANODE)?HIGH:LOW);
digitalWrite(GREEN_PIN,(green!=COMMON_ANODE)?HIGH:LOW);
digitalWrite(BLUE_PIN,(blue!=COMMON_ANODE)?HIGH:LOW);
}
void chirp(unsigned int frequency,unsigned long duration){if(ENABLE_BUZZER)tone(BUZZER_PIN,frequency,duration);}
void showMessage(const char* text,uint32_t now){message=text;messageAt=now;messageVisible=true;}
void loadReference(){
const uint16_t saved=(uint16_t)EEPROM.read(2)|((uint16_t)EEPROM.read(3)<<8);
calibrated=EEPROM.read(0)==0x4C && EEPROM.read(1)==0x44 && saved>=40 && saved<=980;
if(calibrated)referenceAdc=saved;
}
void calibrate(uint32_t now){
// Reject extremes; at saturation a change in light may be invisible.
if(filteredAdc<40||filteredAdc>980){showMessage("REF OUT OF RANGE",now);chirp(350,120);return;}
referenceAdc=filteredAdc;calibrated=true;light.dim=false;
// Invalidate marker before changing data; update() avoids rewriting unchanged bytes.
EEPROM.update(0,0);EEPROM.update(2,referenceAdc&0xFF);EEPROM.update(3,referenceAdc>>8);
EEPROM.update(1,0x44);EEPROM.update(0,0x4C);
showMessage("REFERENCE SAVED",now);chirp(1600,80);
}
void display(uint32_t now){
if(messageVisible && (uint32_t)(now-messageAt)>=1800)messageVisible=false;
char first[17],second[17];
const uint32_t seconds=(timer.remainingMs+999UL)/1000UL;
const char* mode=timer.phase==LumaDesk::Phase::Focus?"FCS":"BRK";
const char* state=timer.running?"GO":(timer.started?"PAUSE":"READY");
snprintf(first,sizeof(first),"%s %02lu:%02lu %s",mode,(unsigned long)(seconds/60),(unsigned long)(seconds%60),state);
if(messageVisible)snprintf(second,sizeof(second),"%s",message);
else if(!calibrated)snprintf(second,sizeof(second),"SET LIGHT REF");
else if(light.dim)snprintf(second,sizeof(second),"LOW LIGHT A%04u",filteredAdc);
else snprintf(second,sizeof(second),"A%04u REF%04u",filteredAdc,referenceAdc);
// Pad the rest of each row to erase stale characters without lcd.clear() flicker.
lcd.setCursor(0,0);lcd.print(first);for(uint8_t i=strlen(first);i<16;i++)lcd.print(' ');
lcd.setCursor(0,1);lcd.print(second);for(uint8_t i=strlen(second);i<16;i++)lcd.print(' ');
}
void setup(){
pinMode(START_PIN,INPUT_PULLUP);pinMode(CAL_PIN,INPUT_PULLUP);
pinMode(RED_PIN,OUTPUT);pinMode(GREEN_PIN,OUTPUT);pinMode(BLUE_PIN,OUTPUT);
if(ENABLE_BUZZER)pinMode(BUZZER_PIN,OUTPUT);
rgb(false,false,true);Serial.begin(115200);
lcd.begin(16,2);loadReference();
for(uint8_t i=0;i<8;i++){readings[i]=analogRead(LIGHT_PIN);totalAdc+=readings[i];}
filteredAdc=totalAdc/8;timer.reset(millis());
showMessage("Hello, Ori!",millis());
Serial.println(F("ms,phase,state,remaining_s,adc,reference,dim,cycles"));
}
void loop(){
const uint32_t now=millis();
if(timer.tick(now)){chirp(1800,300);showMessage(timer.phase==LumaDesk::Phase::Break?"FOCUS COMPLETE":"BREAK COMPLETE",now);}
const auto startEvent=startButton.update(digitalRead(START_PIN)==LOW,now);
if(startEvent==LumaDesk::Event::ShortPress){timer.toggle(now);chirp(1200,50);}
// Holding Start does nothing; short release toggles the timer.
const auto calEvent=calButton.update(digitalRead(CAL_PIN)==LOW,now);
if(calEvent==LumaDesk::Event::ShortPress)calibrate(now);
if(calEvent==LumaDesk::Event::LongPress){timer.reset(now);showMessage("SESSION RESET",now);chirp(600,100);}
if((uint32_t)(now-lastSample)>=100){
lastSample=now;totalAdc-=readings[sampleIndex];readings[sampleIndex]=analogRead(LIGHT_PIN);
totalAdc+=readings[sampleIndex];sampleIndex=(sampleIndex+1)%8;filteredAdc=totalAdc/8;
light.update(filteredAdc,referenceAdc,calibrated);
}
if(light.dim)rgb(true,true,false); // Yellow: check desk lighting, never an automatic lamp control.
else if(timer.running && timer.phase==LumaDesk::Phase::Focus)rgb(false,true,false);
else if(timer.started && !timer.running)rgb(true,false,true); // Magenta: paused.
else rgb(false,false,true); // Blue: ready or break.
if((uint32_t)(now-lastDisplay)>=200){lastDisplay=now;display(now);}
if((uint32_t)(now-lastLog)>=1000){
lastLog=now;Serial.print(now);Serial.print(',');
Serial.print(timer.phase==LumaDesk::Phase::Focus?F("focus"):F("break"));Serial.print(',');
Serial.print(timer.running?F("running"):(timer.started?F("paused"):F("ready")));Serial.print(',');
Serial.print((timer.remainingMs+999UL)/1000UL);Serial.print(',');Serial.print(filteredAdc);Serial.print(',');
Serial.print(calibrated?referenceAdc:0);Serial.print(',');Serial.print(light.dim?1:0);Serial.print(',');Serial.println(timer.cycles);
}
}
VALIDATION.txt inside the download for the actual tool versions and results.TURN A TUTORIAL INTO YOUR OWN WORK
Build it. Test it. Show the evidence.
Use TEST_CHECKLIST.md from the ZIP to record actual outcomes. Test pause for 10 seconds, a complete accelerated focus/break cycle, calibration and power-cycle persistence, shaded/unshaded readings, and the normal 25-minute interval against a stopwatch. I record the observed values and the conditions for each test.
Show the real board and connections. Add a readable close-up of the light divider.
A short video should show a button press, pause/resume, a light change, and a phase transition.
Write down what failed, how you isolated it, and why your correction worked.
Choose a change, explain the trade-off, and test it. I explain what I change and how I check it.
Choose one extension after the baseline works
- Adjustable session length: add a setup menu using buttons; keep contrast control separate.
- Measured sensor behavior: record ADC readings at fixed lamp positions and discuss repeatability. Use a reference lux meter before making any lux claim.
- Cleaner packaging: design a desktop enclosure and record accessibility and assembly decisions.
I keep my project notebook in Portfolio Studio, where I can add measurements, photos, debugging notes, and new design decisions.
WHEN SOMETHING DOESN’T WORK
Debug one layer at a time.
| Symptom | First checks |
|---|---|
| Backlight, no characters | Adjust contrast. Check LCD RW→GND and VO→pot wiper. Verify LCD pin 1 and RS/E/data pin order. |
| No backlight | Unplug. Check LCD 15/16 polarity, 220 Ω resistor, 5V/GND, and split breadboard rails. |
| Button always pressed | Check internal pin pairs and placement. The switch must connect the input to GND only when pressed. |
| ADC stuck near 0 or 1023 | Check A0 at the divider junction and the fixed 10 kΩ resistor. Change illumination; check for a short or open wire. |
| Unexpected LED colors | Verify RGB common type and color-leg order. Check a separate resistor for each channel. |
| No sound | Confirm a passive piezo is used, polarity/wiring, and ENABLE_BUZZER=true. Sound is optional. |
| Compile error: FocusCore.h missing | Extract the ZIP, open LumaDesk/LumaDesk.ino, and keep the header inside that same folder. |
PRIMARY REFERENCES
Keep the references nearby.
- ELEGOO — Mega 2560 Most Complete Starter Kit tutorial
- Arduino — LCD wiring and LiquidCrystal
- Arduino — input pull-ups
- Arduino — button debounce
- Arduino — millis timing
My project notebook · October 2026. The browser demo is simulated; measurement records belong in the project log.