Skip to the content
2 layers240 × 240 mmQlock250mm, the printed circuit board in 3D

Word Clock with xiao Esp32S3+ and WS2812B LED strips

Size
240 × 240 mm
Copper layers
2
Thickness
1.6 mm
Parts
237
Nets
151
Pads
740
Tracks, vias
440, 60
Pours
16
Routed
Yes
KiCad
KiCad 10.0 (file 20260206)
Created
15 Aug 2026
Updated
4 Sep 2026

Overview

Qlock250mm is an open-source printed circuit board by FranzKugler, from the FranzKugler/QlockThreeW32 repository on GitHub. It is a 2-layer board of 240 × 240 mm with 237 parts on 151 nets, routed with 440 tracks and 60 vias.

It is built around the 74AHCT1G125SE, TSL25911FN and WS2812B. Its parts include 117 capacitors, 114 diodes, 3 resistors and 3 ICs. It belongs with the lEDs and displays boards and the ESP32 boards of this library.

Main components

3

README

README.md

QlockThreeW32

ESP32 firmware for a word clock — a grid of letters, backlit so that the current time reads as a sentence. The display changes every five minutes; four corner LEDs fill in the minutes in between.

text
ES IST FÜNF NACH ZWEI          IT IS FIVE PAST TWO

Ten languages and dialects, a configuration web interface served by the clock itself, and firmware updates through the browser. No cloud, no app, no account — everything the clock needs it carries, and it works on a network with no internet access at all.


What it does

  • Ten languages: German (standard, Swabian, Bavarian, Saxon), Swiss German, English, French, Italian, Dutch, Spanish. Each has its own irregular grammar — French and Italian agree the hour, the German dialects differ on viertel / dreiviertel.
  • Configuration in the browser, served from the clock. The interface switches its own language along with the clock's.
  • Colour as hue, saturation and brightness, beside a live preview of the real face — the lit letters are read back out of the clock's own frame buffer, not guessed at in the browser.
  • Automatic brightness from an ambient light sensor, on a curve the clock learns from the way you use the slider — and, on a calibrated clock, colour-aware: the same setting emits far less light in deep blue than in green, and the automatic knows it. There is no "remember this" button.
  • Time from NTP, with a region/place picker over both daylight-saving changeover rules, which stay editable by hand underneath.
  • Firmware and interface updates from the browser, or automatically from a release channel.
  • Expert mode — a password in front of the update, debug and storage tabs.
  • A few debug displays: seconds counter, LED test pattern.

Hardware

ControllerSeeed XIAO ESP32-S3 Plus (16 MB flash)
LEDs114 × WS2812B on one strip, data on GPIO 4
Layout11 × 10 letters = 110, plus 4 corner LEDs
Light sensorams TSL2591 (0x29) or Vishay VEML7700 (0x10) on I²C, SDA/SCL default to GPIO 5/6 — optional

The strip starts at the bottom right letter, meanders left, then up one row and back to the right, and ends at the top right. The four corner LEDs follow it, in the order bottom right, top right, top left, bottom left.

text
index 0    bottom right   (the R of the German panel)
index 109  top right      (the F)
110 … 113  the corners

LedDriverWS2812FastLED::physicalFor(row, col) is the only place that computes this, and the /lab interface can check any claim about it against the strip in seconds. Which frame buffer row is which corner is a separate question and lives at Renderer::setCorners — the path there goes through two remappings that partly cancel, and it was settled by watching a clock rather than by reading code.

Either light sensor works, and the choice is made at run time — the firmware tries each in turn at boot and keeps the first that answers, so one build serves every clock and a chip swapped on the bench needs no rebuild. The TSL2591 is asked first: the two do not share an address, so a clock carrying both answers twice, and the more sensitive one is the one to keep. With no sensor fitted, the automatic hides itself rather than offering a switch that does nothing.

The board and the case are in hardware/ — a KiCad project and an OpenSCAD model. The letter mask is not drawn by hand: scripts/panels.py reads the ten rows out of the firmware's own language files and writes panels.scad, so the sheet that gets milled and the words the clock lights cannot drift apart.

Earlier revisions supported other boards and LED drivers; the project has since been consolidated onto this single target.

Building

Two halves are built separately: the firmware, and the web interface that is flashed into the clock's filesystem.

sh
# web interface -> data/
npm install
npm run build

# firmware
pio run                  # build
pio run -t upload        # flash the firmware
pio run -t uploadfs      # flash data/ as the filesystem image

Both have to be flashed once over USB. After that, updates can go through the browser.

The board definition in PlatformIO describes the 8 MB XIAO ESP32-S3. This is the Plus with 16 MB, so platformio.ini overrides the flash size and points at partitions.csv: two 6.5 MB OTA slots and a 3.5 MB filesystem.

Credentials

Copy src/Secrets.example.h to src/Secrets.h and put in your own values — it is not in version control. The build works without it, it just leaves network flashing via espota unprotected and says so with a compiler warning.

First start

With no WiFi configured the clock opens an access point called QlockThreeW32. Connect to it and a captive portal asks for the network. Afterwards the clock is reachable at http://qlockthreew32.local or by its address in the network.

The clock can be renamed in the WLAN tab, which changes all six places the name appears — mDNS, the DHCP name the router lists, the setup access point, the espota target, the RemoteDebug host and the heading of the interface. It restarts to do it, and says so.

Four tabs are open to anyone on the network:

Tab
Displayoperating mode, language, corner LED direction and colour
Colourhue, saturation, brightness, the automatic, and a preview of the face
Time zoneNTP server, a region/place picker, and both changeover rules
WLANstatus, scan, switching networks, renaming the clock

Switching networks is deliberately careful: the clock tries the new credentials, and if they do not come up within twenty seconds it returns to the previous network by itself, so a typo cannot lock you out.

Expert mode

Three more tabs — Update, Debug and Storage — are behind a password, because flashing the clock, reading its log and rummaging through its filesystem are not things every guest on the network should be able to do.

The way in is the address bar: http://<clock>/#expert. There is no chip in the tab row, on purpose; a visible one would only invite guessing.

  • A clock that has never been given a password takes the first one offered, and keeps it. Until then it is exactly as open as every clock was before this existed.
  • It is a mode, not a login: one flag in NVS, so the password crosses the wire once instead of on every poll of the log. While unlocked, nothing is protected — hence the visible "lock again" button.
  • The way back from a forgotten password is the plug. The enrolment can be cleared within five minutes of a power-on reset, which a restart from the clock's own update tab does not count as. Anyone who can pull the plug can read the flash over USB anyway, so this admits what is already true rather than pretending otherwise.
  • Five wrong answers stop the endpoint for five minutes.

What it is worth: it keeps out someone who joins the network and goes looking. It does not keep out someone watching the traffic — there is no TLS on the clock — and it does not keep out anyone holding it.

Once unlocked:

Tab
Updateinstalled versions, browser upload, release channel and automatic installs
Debuguptime, reset reason, heap, and the clock's last 200 log lines
Storagea file explorer over LittleFS, and over NVS drawn as a tree

The lab interface

/lab/* hands a script direct control of every pixel and of the light sensor — raw colours, no gamma, no smoothing, and a whole measurement series in one request. scripts/lab.py is the client:

sh
python scripts/lab.py <address> find   # which cell is the light sensor in
python scripts/lab.py <address> ir     # does the infrared channel see the room
python scripts/lab.py <address> map    # coupling from every cell to the sensor

It is there because a word clock whose sensor can see its own face cannot regulate on it, and how badly is a question of geometry that differs from clock to clock. On the first clock it ran on it found the sensor in four minutes, behind an unused filler letter three rows from the bottom.

Behind expert mode, and it never touches a setting: the mode it uses cannot be stored, cannot be picked in the display tab, and ends with a restart.

Two more screens have no chip either. #luminance draws what the automatic brightness has learned — deliberately outside expert mode, since there is no secret in a brightness curve. #expert is the one above.

Automatic brightness

Switch it on in the colour tab and the display follows the room. The curve is a straight line in log light, because perception is roughly logarithmic and the range to cover spans decades:

text
brightness = slope × log10(lux) + offset      held between 20 % and 100 %

There is no "remember this" button, and that is the design. Moving the slider is the only signal anyone ever gives about whether the automatic got it right, so the nudge is the calibration:

  1. Move the brightness slider while the automatic is on. The automatic steps aside at once — nobody wants the clock arguing back mid-drag.
  2. Ten seconds after the last move, the clock keeps the pair (light now, brightness asked for) and fits a new line through everything it has kept.

The switch keeps saying "automatic" throughout, because it still is: it is being taught, not turned off. The timer is in the firmware, so closing the tab mid-adjustment does not lose the point.

Ten points are kept, in NVS. A new one replaces a near neighbour rather than joining the queue, or ten corrections made in one evening would push the one daylight point out and collapse the line onto a single lighting condition. If the points sit too close together to say anything about steepness, only the level is re-fitted and the slope stands. A slope that would make a darker room brighter is refused outright.

The reset button in the colour tab throws it all away and restores a cautious default line.

Colour-aware, on a calibrated clock

That single line is the fallback. A clock shipped with a measured optical profile knows that a percentage is not an amount of light — the same setting emits far less in deep blue than in the green this clock normally shows — and regulates on a small model of the diffuser and LEDs instead: one line shared by every colour, plus how each hue departs from it. Nudging the slider still teaches it, now per colour, and a taught correction only overrides the factory measurement where it actually disagrees with it — elsewhere the factory number keeps speaking for itself, which is why a handful of evening corrections in one colour do not retune the whole curve.

#luminance — reachable without unlocking expert mode, since there is no secret in a brightness curve — draws the model as an interactive 3D surface: hue running round, ambient light out from the centre (logarithmically, brighter towards the middle), brightness as the height, with every taught correction marked on it. A collapsible "fit parameters" section beside it has the numbers themselves, for anyone curious.

The two stores

The clock keeps things in two places, and the difference decides what survives an update:

  • LittleFS, 3.5 MB — the web interface, the timezone list, the icons. A filesystem update overwrites it whole.
  • NVS — the settings, the expert password, the brightness curve. An update leaves it untouched, which is exactly why they live there.

The Storage tab shows both. NVS is not a filesystem and is drawn as one anyway: namespaces as folders, keys as files, .json or .bin depending on what the value turns out to be. Files can be downloaded, uploaded and edited in place, JSON laid out on the way in and written back compact. Right-click, or press and hold on a touch screen, for the menu.

Two things it will not do. The expert password hash is listed but never handed out — it is the one secret whose leak would outlive the unlock that leaked it. And deleting is not recursive, because a file explorer that empties a tree on one click is how the web interface gets deleted by someone who meant to tidy up.

Clearing NVS resets the clock to defaults. Reflashing the filesystem no longer does.

Flashing a blank chip

franzkugler.github.io/QlockThreeW32 flashes a factory-fresh Seeed XIAO ESP32-S3 Plus straight from Chrome, Edge or Opera — no PlatformIO, no Python, nothing to install. It writes the whole chip (bootloader, partition table, firmware and the configuration interface) from whatever the latest stable release is, using ESP Web Tools over the Web Serial API. Once a clock is running this firmware, updates go through its own Update tab instead — see the next section.

The page is docs/ in this repository, published as a GitHub Pages site by the release workflow on every stable release; see CLAUDE.md for why it cannot simply point at the release's own download URLs.

Updating over the air

There are two images, and they are independent: firmware.bin (the program) and littlefs.bin (the web interface). Changing the interface and only flashing the firmware leaves the old interface in place.

Ready-built images come from the releases page, in two channels:

  • stable — tagged versions, built from a v* tag.
  • edge — a rolling build of the latest commit on main, replaced on every push. Untested by definition.

The clock can poll its channel by itself and install what it finds. Automatic installs are off by default and only ever run between 02:00 and 05:00 local, so the face never goes dark in the evening.

Locally the images are produced by pio run and pio run -t buildfs in .pio/build/seeed_xiao_esp32s3/.

Upload either image in the Update tab — the clock recognises which is which from the image itself (ESP32 program images start with the magic byte 0xE9) and picks the right partition.

The checksum is verified before the boot partition is switched, so an interrupted upload does no harm: the clock simply keeps running the version it has. There is no automatic rollback for an image that flashes correctly but then crashes — that case needs a USB cable.

The update endpoints are behind expert mode, along with the log and the storage tab. They were open by design while none of them existed as a web page.

Layout of the repository

text
src/            firmware (PlatformIO / Arduino-ESP32)
  Renderer.*      hour/minute -> which words light up, no hardware involved
  languages/      one file per language: its letters, its words, its grammar
  LedDriver*      WS2812B output, colour and brightness
  LightSensor.*   TSL2591 / VEML7700 behind one interface, sampled on core 0
  Luminance.*     the brightness curve, and how it is learned
  FactoryProfile.*    the colour-aware model, as pure arithmetic
  FactoryLuminance.*  loads and validates the measured profile from LittleFS
  ResidualStore.*     what an owner has taught it, refit online against the profile
  Settings.*      persistence in NVS
  WebRoutes.*     the HTTP handlers; OtaUpdate, FileRoutes, NvsRoutes beside it
web/            configuration interface (Svelte 5 + Vite), built into data/
hardware/       KiCad project, and the OpenSCAD case and letter mask
scripts/        generators: the letter mask, the timezone list, the app icons
server.js       mock of the clock's REST API, for developing the UI without hardware

Everything in scripts/ writes files that are committed, so neither a build nor a 3D print needs Python. Edit the generator, never its output.

For UI work, run the mock and the dev server side by side — the interface then behaves as it does on the device, on a desktop browser:

sh
npm run mock     # REST API on :8080
npm run dev      # Vite with hot reload on :5173

CLAUDE.md goes into the architecture in more detail.

Origins

This is a long-running hobby project, and not the beginning of the story. It descends from Qlockthree by Christian Aschoff — firmware for a self-built QLOCKTWO-style clock, written for the Arduino/ATmega328 from November 2011 onwards and maintained through version 3.4.9. Much of the rendering code here is still his, as the file headers record.

The upstream firmware is not on GitHub itself; it is published as ZIP archives on that page. Several people have mirrored and continued it, among them bracci/Qlockthree, schwabe/qlockthree and cactus-online/Qlockthree. bracci/QlockWiFive and ch570512/Qlockwork are separate ESP firmwares for the same hardware idea.

Beyond that, the Swiss German variant goes back to Thomas Schuler and the Dutch one to Rudolf Klimesch.

This repository takes that lineage to an ESP32-S3, and adds the web interface, the WiFi handling and the over-the-air updates.

License

CC BY-NC-SA 3.0 — Attribution, NonCommercial, ShareAlike. See LICENSE.

This is inherited, not chosen: Christian Aschoff puts the original firmware under CC BY-NC-SA 3.0, and the ShareAlike term carries over to anything built on it. In short — keep the attributions, do not use it commercially, and pass derivatives on under the same terms.

Parts

237
QtyValueFootprintReferences
115100nCapacitor_SMD:C_0603_1608MetricC1, C10, C11, C100, C101, C102, C103, C104, C105, C106, C107, C108, C109, C110, C111, C112 +99
114WS2812BLED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mmD1, D10, D11, D100, D101, D102, D103, D104, D105, D106, D107, D108, D109, D110, D111, D112 +98
21k5Resistor_SMD:R_0402_1005MetricR2, R3
11000uFCapacitor_SMD:CP_Elec_10x10C117
11uFCapacitor_SMD:C_0603_1608MetricC116
1330Resistor_SMD:R_0402_1005MetricR1
174AHCT1G125Package_TO_SOT_SMD:SOT-23-5U2
1TSL25911FNOptoDevice:AMS_TSL25911FNU3
1XIAO-ESP32-S3-Plus-SMDseed_xiao:XIAO-ESP32-S3-Plus-SMDU1

Similar boards

  • 2 layers1.3 × 49 mmSunbie, the printed circuit board in 3D

    Sunbie

    avycado13/sunbie

    24 parts
  • 2 layers52.8 × 71.8 mmESPBoosted, the printed circuit board in 3D

    ESPBoosted

    schoolllex/ESPBoosted

    A custom, feature-rich ESP32-WROOM-32 development board featuring built-in environmental sensors, power management, and expansion interfaces.

    51 parts
  • 2 layers99.1 × 88.9 mmPanel, the printed circuit board in 3D

    Panel

    bfrigon/alarm-clock-v3

    Alarm clock (v3)

    122 parts18CC-BY-SA-4.0
  • 2 layers68 × 100 mmKeyboard featherwing, the printed circuit board in 3D

    Keyboard featherwing

    arturo182/keyboard_featherwing_hw

    The Keyboard FeatherWing is an easy way to add a handheld Qwerty keyboard and a 2.6” color display (+ more) to your project! It fits well in your hands and is great for typing with your thumbs, early 2000s-style.

    58 parts43
  • 2 layersTopBoard, the printed circuit board in 3D

    TopBoard

    Libre-Talk/Wave

    Smart Speaker satellite

    184 parts22
  • 4 layers51.8 × 68.3 mmButton Matrix RP2040, the printed circuit board in 3D

    Button Matrix RP2040

    hazenluk/RP2040_AS3394

    An AS3394 Voice Card with built in RP2040

    229 parts3
  • 2 layers435 × 123 mmNec pc-8801-type a, the printed circuit board in 3D

    Nec pc-8801-type a

    djvs/nec-pc-8801-type-a

    NEC PC 8801 Type A keyboard PCB (work in progress)

    268 parts2CC-BY-SA-4.0
  • 2 layers290 × 210 mmT8-Sequencer, the printed circuit board in 3D

    T8-Sequencer

    lluisestape-upc/T8-Sequencer

    16-step ESP32 groovebox: two-layer 290x210 mm KiCad board, DRC-clean and fabrication-ready, with the Python scripts that generated and repaired it

    104 parts2MIT

Questions

How big is the Qlock250mm PCB?

It measures 240 × 240 mm and has 2 copper layers, 1.6 mm thick.

What parts does the Qlock250mm use?

237 parts, built around the 74AHCT1G125SE, TSL25911FN and WS2812B. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.

Where can I download the Qlock250mm KiCad files?

From the FranzKugler/QlockThreeW32 repository on GitHub, at the commit this page was made from: the board is hardware/Qlock250mm/pcb/Qlock250mm/Qlock250mm.kicad_pcb and its schematic hardware/Qlock250mm/pcb/Qlock250mm/Qlock250mm.kicad_sch. PCBKit opens the board in the browser, with nothing to install.

Can I use the Qlock250mm design in my own project?

Its repository states no licence, so its author keeps all rights: ask FranzKugler before reusing the design.