← All projects

PROJECT 001 · DESIGN & BUILD GUIDE

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

Ready · press Start

Reference: 700 ADC. Cover the virtual sensor by lowering the slider.

BROWSER SIMULATION · accelerated 30s / 10s cycle.
Hardware defaults to 25 min / 5 min. No board is connected; these values are simulated, not measured. The sketch smooths readings; this preview updates instantly.

My starting question: how can a few simple inputs make a useful desk device? In this guide I explain the system, share AI-assisted source, and make the behavior easy to explore.

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.

01 / FOCUS25 minutes, one task.

Start or pause a focus session. A completed focus interval prepares a 5-minute break. Each next phase waits for a button press.

02 / SENSEA reference, not a guess.

Set the reference with your desk lit as you prefer. A moving average and two thresholds reduce flicker as lighting changes.

03 / EXPLAINEvidence you can show.

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.

QuantityPartPurpose
1Mega 2560 R3 + USB cableController and USB power
1 eachBreadboard, jumper wiresConnections; number depends on layout
1LCD1602, 16-pin parallel typeTwo-line display; not the I²C-backpack version
110 kΩ potentiometerLCD contrast
1Photoresistor (LDR)Relative light sensor
110 kΩ fixed resistorLDR voltage divider
1RGB LEDStatus indicator; verify common-cathode type
4220 Ω resistorsOne per RGB channel; one for LCD backlight
2Momentary push buttonsStart/Pause and Calibrate/Reset
Optional: 1 eachSmall passive piezo + 1 kΩ resistorQuiet 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.

LumaDesk functional connection map, with LCD, LDR voltage divider, two buttons, RGB indicator and optional passive piezo connected to Mega 2560

Logical connection map, not a breadboard placement drawing. The tables below are the pin-by-pin source of truth.

LCD1602 connections

LCD pinConnect toNote
1 · VSSGNDDisplay ground
2 · VDD5VLogic supply
3 · VO10 kΩ pot middle pin / wiperPot outer pins → 5V and GND
4 · RSMega D22Register select
5 · RWGNDWrite-only operation
6 · EMega D23Enable
7–10 · D0–D3Leave unconnectedUnused in 4-bit mode
11 · D4Mega D24Data
12 · D5Mega D25Data
13 · D6Mega D26Data
14 · D7Mega D27Data
15 · A / LED+5V through 220 ΩBacklight current limiting
16 · K / LED−GNDBacklight return

Controls, light sensor, and indicator

PartConnectionCheck
LDR divider5V → LDR → junction → 10 kΩ → GNDJunction → Mega A0. Brighter light should raise the reading.
Start buttonMega D2 → switch → GNDINPUT_PULLUP; pressed = LOW. No 5V wire to button.
Cal buttonMega D3 → switch → GNDShort tap calibrates; hold 1.5 seconds resets session.
RGB redMega D5 → 220 Ω → red anodeIdentify the color leg from the kit pinout.
RGB greenMega D6 → 220 Ω → green anodeUse a separate resistor, not one in the common leg.
RGB blueMega D7 → 220 Ω → blue anodeCommon cathode → GND; confirm your actual part.
Optional passive piezoMega D8 → 1 kΩ → (+); (−) → GNDUse only the small passive piezo; no speaker or motor.
Power railsMega 5V → + rail; Mega GND → − railAll module grounds must share GND; check for split rails.
Two easy wiring traps: four-leg push buttons contain two permanently joined pairs; use the sides that connect only when pressed, typically across the breadboard’s center gap. The RGB map assumes a common-cathode LED. If the part is confirmed common-anode, set 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.

Inputs → state → outputs

Buttons and an analog sensor become inputs to a state machine. The LCD, LED, and optional buzzer reflect the resulting state.

Responsive timing

The sketch checks elapsed millis() instead of using long delay() calls. Buttons, display, and sensor continue to update while time is counted.

Stable decisions

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);
  }
}
Software checks: both sketches compiled for the Mega 2560 / ATmega2560. Logic tests cover pause/resume, phase completion, 32-bit timer rollover, button debounce, long-press handling, and light hysteresis. I use these checks alongside the hardware measurement worksheet. See 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.

MY PROJECT LOGActual build photo

Show the real board and connections. Add a readable close-up of the light divider.

MY PROJECT LOGA working demonstration

A short video should show a button press, pause/resume, a light change, and a phase transition.

MY PROJECT LOGA fault and its fix

Write down what failed, how you isolated it, and why your correction worked.

MY PROJECT LOGAn original improvement

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.

SymptomFirst checks
Backlight, no charactersAdjust contrast. Check LCD RW→GND and VO→pot wiper. Verify LCD pin 1 and RS/E/data pin order.
No backlightUnplug. Check LCD 15/16 polarity, 220 Ω resistor, 5V/GND, and split breadboard rails.
Button always pressedCheck internal pin pairs and placement. The switch must connect the input to GND only when pressed.
ADC stuck near 0 or 1023Check A0 at the divider junction and the fixed 10 kΩ resistor. Change illumination; check for a short or open wire.
Unexpected LED colorsVerify RGB common type and color-leg order. Check a separate resistor for each channel.
No soundConfirm a passive piezo is used, polarity/wiring, and ENABLE_BUZZER=true. Sound is optional.
Compile error: FocusCore.h missingExtract the ZIP, open LumaDesk/LumaDesk.ino, and keep the header inside that same folder.

PRIMARY REFERENCES

Keep the references nearby.

My project notebook · October 2026. The browser demo is simulated; measurement records belong in the project log.