Repository files navigation

thinclock

A thin, config-driven ESP32 firmware for LED matrix displays. JSON in, pixels out.

The firmware is intentionally dumb — it fetches JSON from a URL and renders what it's told. The server is smart — screen definitions are composable JS modules that can pull data from any source. Everything on the display is defined externally. No reflashing to change what it shows.

Quick Start

git clone <repo>cd thinclock
npm install
cp .env.example .env # edit with your WiFi, timezone, API keys
npm run flash # build & upload firmware to Ulanzi TC001
npm run dev # start config server

On first boot, open serial monitor and paste the JSON line the server prints. Device reboots, connects, and starts rendering.

Architecture

┌─────────────────────────────────────────────────────┐
│ thinclock device (ESP32 + 32×8 LED matrix) │
│ - Fetches config JSON every 30s │
│ - Renders layers at 50fps │
│ - Onboard sensors, buttons, buzzer │
│ - HTTP API: /notify, /timer, /beep, /sensors │
└──────────────────────┬──────────────────────────────┘
│ HTTP (JSON)
┌──────────────────────┴──────────────────────────────┐
│ thinclock server (Node.js) │
│ - Screen modules define what to display │
│ - Alert engine monitors conditions │
│ - Adapters pull data (weather, HA, APIs) │
│ - Management API for enable/disable/scheduling │
└─────────────────────────────────────────────────────┘

Features

Display Engine (Firmware)

  • Layer-based compositing — screens are stacks of typed layers rendered in order
  • Particle system — configurable emitters, gravity, collision, rocket/burst, edge behavior
  • Tweens — animate any layer property (x, y, opacity) with easing and looping
  • Gradients — horizontal, vertical, diagonal with color stops and opacity
  • Native pixel font — 3×5 and 5×7 digit/letter renderers with tight spacing
  • Icon rendering — inline hex pixel data, animation frames, color remapping
  • Gauges — vertical bar, horizontal bar, dot indicators with color ranges
  • Crossfade transitions — smooth blending between screens with both animating
  • Opacity & blend modes — per-layer opacity, additive blending (for stars, glow)
  • Scrolling text — bounce, left (banner), auto modes with edge fade

Notifications & Timer

  • Push notifications via HTTP — colored indicator dots, scrollable text viewer
  • Timer with breathing indicator dot that accelerates as time runs out
  • Pomodoro — work/break cycles with phase indicator bar
  • Alert beeps — single, double, triple, alarm, or custom frequency patterns
  • Buzzer API — trigger any beep pattern via HTTP

Buttons & Input

  • Short press / long press detection with audio feedback
  • Left/Right — navigate screens (short), reserved (long)
  • Middle — open notification viewer (short), context action (long)
  • LDR "button" — cover light sensor to pause/resume timer
  • Server events — all presses POST to server for custom handling

Server

  • Screen modules — self-contained JS files, auto-discovered
  • Scheduling — per-screen time/date/month restrictions
  • Priority sorting — higher priority screens appear first in rotation
  • Night mode — reduced brightness + night-only screens during configured hours
  • Alert engine — screen modules declare conditions, server evaluates and notifies
  • Management API — enable/disable screens, inspect state
  • Weather — OpenWeatherMap integration with dynamic rain/snow particles
  • AQI — air quality with EPA color scale
  • Home Assistant adapter — placeholder for HA WebSocket integration

Project Structure

thinclock/
├── firmware/ # ESP32 PlatformIO project
│ ├── src/ # main, display, particles, gauge, sensors, config
│ ├── include/ # headers
│ └── platformio.ini
├── server/ # Node.js config server
│ ├── screens/ # Screen modules (drop in to add!)
│ ├── adapters/ # Data adapters (HA, etc.)
│ └── lib/ # Registry, alerts, icon helpers
├── homeassistant/ # HA custom component
├── docs/ # Format spec, API docs
└── package.json # Project orchestrator

Scripts

CommandDescription
npm run devStart config server
npm run buildCompile firmware
npm run flashCompile and upload
npm run flash:forceErase flash + upload
npm run eraseErase ESP32 flash
npm run monitorSerial monitor (115200)

Documentation

Hardware

ComponentDetail
BoardESP32-D0WD (Ulanzi TC001)
Display32×8 WS2812 addressable LEDs
LED PinGPIO 32
BuzzerGPIO 15
ButtonsGPIO 26 (L), 27 (M), 14 (R)
LDRGPIO 35
I2CSDA 21, SCL 22
Temp/HumidityBME280/BMP280/HTU21DF/SHT31 (auto-detect)

Environment Variables

# Required
WIFI_SSID=your_network
WIFI_PASS=your_password
SERVER_PORT=3000
TIMEZONE=-5
# Display
BRIGHTNESS=40
BRIGHTNESS_NIGHT=10
NIGHT_HOURS=22,23,0,1,2,3,4,5,6
TIME_FORMAT=12h
TEMP_UNIT=F
ALLOW_BEEPING=true
# Screen management
SCREEN_MODE=auto # auto, manual, all
MAX_SCREENS=8
# SCREEN_ALLOWLIST=clock,weather,sensors# SCREEN_BLOCKLIST=water-fill# Device (for server→device communication)
DEVICE_IP=192.168.x.x
# Weather (OpenWeatherMap free tier)
OWM_API_KEY=your_key
OWM_CITY=Sacramento,CA,US
# Home Assistant (optional)
HA_URL=http://homeassistant.local:8123
HA_TOKEN=your_long_lived_token

Included Screens

ScreenTypeScheduleDescription
Awtrix ClockutilityalwaysCalendar icon + clock + week dots
WeatherutilityalwaysDynamic icon + temp + rain/snow particles
Air QualityutilityalwaysEPA color bar + AQI value
Network MonitorutilityalwaysPing latency + WiFi signal gauge
Sensor DashboardutilityalwaysLight gauge + temperature
PomodoroutilityalwaysWork/break timer with phase bar
Night Clocknight10pm-7amDim red clock with sunrise transition
Bouncing BallsfunalwaysColliding 2×2 rainbow particles
FireworksseasonalJuly 1-4Rockets burst at apex
Winter SceneseasonalNov-FebTree + snowman + snow particles
CampfireambienteveningFire + moon + stars + tent
Ocean WavesambientalwaysGradient water + foam particles
StarfieldambientalwaysParallax warp from center
FireambientalwaysRising flame particles
Lava LampambientalwaysWarm rising blobs
Ambient GradientmoodeveningSlowly shifting color diagonal

License

MIT

About

A custom firmware for the Ulanzi TC001 with an emphasis on simple external data sourcing

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

thinclock

A thin, config-driven ESP32 firmware for LED matrix displays. JSON in, pixels out.

The firmware is intentionally dumb — it fetches JSON from a URL and renders what it's told. The server is smart — screen definitions are composable JS modules that can pull data from any source. Everything on the display is defined externally. No reflashing to change what it shows.

Quick Start

git clone <repo>cd thinclock
npm install
cp .env.example .env # edit with your WiFi, timezone, API keys
npm run flash # build & upload firmware to Ulanzi TC001
npm run dev # start config server

On first boot, open serial monitor and paste the JSON line the server prints. Device reboots, connects, and starts rendering.

Architecture

┌─────────────────────────────────────────────────────┐
│ thinclock device (ESP32 + 32×8 LED matrix) │
│ - Fetches config JSON every 30s │
│ - Renders layers at 50fps │
│ - Onboard sensors, buttons, buzzer │
│ - HTTP API: /notify, /timer, /beep, /sensors │
└──────────────────────┬──────────────────────────────┘
│ HTTP (JSON)
┌──────────────────────┴──────────────────────────────┐
│ thinclock server (Node.js) │
│ - Screen modules define what to display │
│ - Alert engine monitors conditions │
│ - Adapters pull data (weather, HA, APIs) │
│ - Management API for enable/disable/scheduling │
└─────────────────────────────────────────────────────┘

Features

Display Engine (Firmware)

  • Layer-based compositing — screens are stacks of typed layers rendered in order
  • Particle system — configurable emitters, gravity, collision, rocket/burst, edge behavior
  • Tweens — animate any layer property (x, y, opacity) with easing and looping
  • Gradients — horizontal, vertical, diagonal with color stops and opacity
  • Native pixel font — 3×5 and 5×7 digit/letter renderers with tight spacing
  • Icon rendering — inline hex pixel data, animation frames, color remapping
  • Gauges — vertical bar, horizontal bar, dot indicators with color ranges
  • Crossfade transitions — smooth blending between screens with both animating
  • Opacity & blend modes — per-layer opacity, additive blending (for stars, glow)
  • Scrolling text — bounce, left (banner), auto modes with edge fade

Notifications & Timer

  • Push notifications via HTTP — colored indicator dots, scrollable text viewer
  • Timer with breathing indicator dot that accelerates as time runs out
  • Pomodoro — work/break cycles with phase indicator bar
  • Alert beeps — single, double, triple, alarm, or custom frequency patterns
  • Buzzer API — trigger any beep pattern via HTTP

Buttons & Input

  • Short press / long press detection with audio feedback
  • Left/Right — navigate screens (short), reserved (long)
  • Middle — open notification viewer (short), context action (long)
  • LDR "button" — cover light sensor to pause/resume timer
  • Server events — all presses POST to server for custom handling

Server

  • Screen modules — self-contained JS files, auto-discovered
  • Scheduling — per-screen time/date/month restrictions
  • Priority sorting — higher priority screens appear first in rotation
  • Night mode — reduced brightness + night-only screens during configured hours
  • Alert engine — screen modules declare conditions, server evaluates and notifies
  • Management API — enable/disable screens, inspect state
  • Weather — OpenWeatherMap integration with dynamic rain/snow particles
  • AQI — air quality with EPA color scale
  • Home Assistant adapter — placeholder for HA WebSocket integration

Project Structure

thinclock/
├── firmware/ # ESP32 PlatformIO project
│ ├── src/ # main, display, particles, gauge, sensors, config
│ ├── include/ # headers
│ └── platformio.ini
├── server/ # Node.js config server
│ ├── screens/ # Screen modules (drop in to add!)
│ ├── adapters/ # Data adapters (HA, etc.)
│ └── lib/ # Registry, alerts, icon helpers
├── homeassistant/ # HA custom component
├── docs/ # Format spec, API docs
└── package.json # Project orchestrator

Scripts

CommandDescription
npm run devStart config server
npm run buildCompile firmware
npm run flashCompile and upload
npm run flash:forceErase flash + upload
npm run eraseErase ESP32 flash
npm run monitorSerial monitor (115200)

Documentation

Hardware

ComponentDetail
BoardESP32-D0WD (Ulanzi TC001)
Display32×8 WS2812 addressable LEDs
LED PinGPIO 32
BuzzerGPIO 15
ButtonsGPIO 26 (L), 27 (M), 14 (R)
LDRGPIO 35
I2CSDA 21, SCL 22
Temp/HumidityBME280/BMP280/HTU21DF/SHT31 (auto-detect)

Environment Variables

# Required
WIFI_SSID=your_network
WIFI_PASS=your_password
SERVER_PORT=3000
TIMEZONE=-5
# Display
BRIGHTNESS=40
BRIGHTNESS_NIGHT=10
NIGHT_HOURS=22,23,0,1,2,3,4,5,6
TIME_FORMAT=12h
TEMP_UNIT=F
ALLOW_BEEPING=true
# Screen management
SCREEN_MODE=auto # auto, manual, all
MAX_SCREENS=8
# SCREEN_ALLOWLIST=clock,weather,sensors# SCREEN_BLOCKLIST=water-fill# Device (for server→device communication)
DEVICE_IP=192.168.x.x
# Weather (OpenWeatherMap free tier)
OWM_API_KEY=your_key
OWM_CITY=Sacramento,CA,US
# Home Assistant (optional)
HA_URL=http://homeassistant.local:8123
HA_TOKEN=your_long_lived_token

Included Screens

ScreenTypeScheduleDescription
Awtrix ClockutilityalwaysCalendar icon + clock + week dots
WeatherutilityalwaysDynamic icon + temp + rain/snow particles
Air QualityutilityalwaysEPA color bar + AQI value
Network MonitorutilityalwaysPing latency + WiFi signal gauge
Sensor DashboardutilityalwaysLight gauge + temperature
PomodoroutilityalwaysWork/break timer with phase bar
Night Clocknight10pm-7amDim red clock with sunrise transition
Bouncing BallsfunalwaysColliding 2×2 rainbow particles
FireworksseasonalJuly 1-4Rockets burst at apex
Winter SceneseasonalNov-FebTree + snowman + snow particles
CampfireambienteveningFire + moon + stars + tent
Ocean WavesambientalwaysGradient water + foam particles
StarfieldambientalwaysParallax warp from center
FireambientalwaysRising flame particles
Lava LampambientalwaysWarm rising blobs
Ambient GradientmoodeveningSlowly shifting color diagonal

License

MIT

About

A custom firmware for the Ulanzi TC001 with an emphasis on simple external data sourcing

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

thinclock

A thin, config-driven ESP32 firmware for LED matrix displays. JSON in, pixels out.

The firmware is intentionally dumb — it fetches JSON from a URL and renders what it's told. The server is smart — screen definitions are composable JS modules that can pull data from any source. Everything on the display is defined externally. No reflashing to change what it shows.

Quick Start

git clone <repo>cd thinclock
npm install
cp .env.example .env # edit with your WiFi, timezone, API keys
npm run flash # build & upload firmware to Ulanzi TC001
npm run dev # start config server

On first boot, open serial monitor and paste the JSON line the server prints. Device reboots, connects, and starts rendering.

Architecture

┌─────────────────────────────────────────────────────┐
│ thinclock device (ESP32 + 32×8 LED matrix) │
│ - Fetches config JSON every 30s │
│ - Renders layers at 50fps │
│ - Onboard sensors, buttons, buzzer │
│ - HTTP API: /notify, /timer, /beep, /sensors │
└──────────────────────┬──────────────────────────────┘
│ HTTP (JSON)
┌──────────────────────┴──────────────────────────────┐
│ thinclock server (Node.js) │
│ - Screen modules define what to display │
│ - Alert engine monitors conditions │
│ - Adapters pull data (weather, HA, APIs) │
│ - Management API for enable/disable/scheduling │
└─────────────────────────────────────────────────────┘

Features

Display Engine (Firmware)

  • Layer-based compositing — screens are stacks of typed layers rendered in order
  • Particle system — configurable emitters, gravity, collision, rocket/burst, edge behavior
  • Tweens — animate any layer property (x, y, opacity) with easing and looping
  • Gradients — horizontal, vertical, diagonal with color stops and opacity
  • Native pixel font — 3×5 and 5×7 digit/letter renderers with tight spacing
  • Icon rendering — inline hex pixel data, animation frames, color remapping
  • Gauges — vertical bar, horizontal bar, dot indicators with color ranges
  • Crossfade transitions — smooth blending between screens with both animating
  • Opacity & blend modes — per-layer opacity, additive blending (for stars, glow)
  • Scrolling text — bounce, left (banner), auto modes with edge fade

Notifications & Timer

  • Push notifications via HTTP — colored indicator dots, scrollable text viewer
  • Timer with breathing indicator dot that accelerates as time runs out
  • Pomodoro — work/break cycles with phase indicator bar
  • Alert beeps — single, double, triple, alarm, or custom frequency patterns
  • Buzzer API — trigger any beep pattern via HTTP

Buttons & Input

  • Short press / long press detection with audio feedback
  • Left/Right — navigate screens (short), reserved (long)
  • Middle — open notification viewer (short), context action (long)
  • LDR "button" — cover light sensor to pause/resume timer
  • Server events — all presses POST to server for custom handling

Server

  • Screen modules — self-contained JS files, auto-discovered
  • Scheduling — per-screen time/date/month restrictions
  • Priority sorting — higher priority screens appear first in rotation
  • Night mode — reduced brightness + night-only screens during configured hours
  • Alert engine — screen modules declare conditions, server evaluates and notifies
  • Management API — enable/disable screens, inspect state
  • Weather — OpenWeatherMap integration with dynamic rain/snow particles
  • AQI — air quality with EPA color scale
  • Home Assistant adapter — placeholder for HA WebSocket integration

Project Structure

thinclock/
├── firmware/ # ESP32 PlatformIO project
│ ├── src/ # main, display, particles, gauge, sensors, config
│ ├── include/ # headers
│ └── platformio.ini
├── server/ # Node.js config server
│ ├── screens/ # Screen modules (drop in to add!)
│ ├── adapters/ # Data adapters (HA, etc.)
│ └── lib/ # Registry, alerts, icon helpers
├── homeassistant/ # HA custom component
├── docs/ # Format spec, API docs
└── package.json # Project orchestrator

Scripts

CommandDescription
npm run devStart config server
npm run buildCompile firmware
npm run flashCompile and upload
npm run flash:forceErase flash + upload
npm run eraseErase ESP32 flash
npm run monitorSerial monitor (115200)

Documentation

Hardware

ComponentDetail
BoardESP32-D0WD (Ulanzi TC001)
Display32×8 WS2812 addressable LEDs
LED PinGPIO 32
BuzzerGPIO 15
ButtonsGPIO 26 (L), 27 (M), 14 (R)
LDRGPIO 35
I2CSDA 21, SCL 22
Temp/HumidityBME280/BMP280/HTU21DF/SHT31 (auto-detect)

Environment Variables

# Required
WIFI_SSID=your_network
WIFI_PASS=your_password
SERVER_PORT=3000
TIMEZONE=-5
# Display
BRIGHTNESS=40
BRIGHTNESS_NIGHT=10
NIGHT_HOURS=22,23,0,1,2,3,4,5,6
TIME_FORMAT=12h
TEMP_UNIT=F
ALLOW_BEEPING=true
# Screen management
SCREEN_MODE=auto # auto, manual, all
MAX_SCREENS=8
# SCREEN_ALLOWLIST=clock,weather,sensors# SCREEN_BLOCKLIST=water-fill# Device (for server→device communication)
DEVICE_IP=192.168.x.x
# Weather (OpenWeatherMap free tier)
OWM_API_KEY=your_key
OWM_CITY=Sacramento,CA,US
# Home Assistant (optional)
HA_URL=http://homeassistant.local:8123
HA_TOKEN=your_long_lived_token

Included Screens

ScreenTypeScheduleDescription
Awtrix ClockutilityalwaysCalendar icon + clock + week dots
WeatherutilityalwaysDynamic icon + temp + rain/snow particles
Air QualityutilityalwaysEPA color bar + AQI value
Network MonitorutilityalwaysPing latency + WiFi signal gauge
Sensor DashboardutilityalwaysLight gauge + temperature
PomodoroutilityalwaysWork/break timer with phase bar
Night Clocknight10pm-7amDim red clock with sunrise transition
Bouncing BallsfunalwaysColliding 2×2 rainbow particles
FireworksseasonalJuly 1-4Rockets burst at apex
Winter SceneseasonalNov-FebTree + snowman + snow particles
CampfireambienteveningFire + moon + stars + tent
Ocean WavesambientalwaysGradient water + foam particles
StarfieldambientalwaysParallax warp from center
FireambientalwaysRising flame particles
Lava LampambientalwaysWarm rising blobs
Ambient GradientmoodeveningSlowly shifting color diagonal

License

MIT

About

A custom firmware for the Ulanzi TC001 with an emphasis on simple external data sourcing

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

thinclock

A thin, config-driven ESP32 firmware for LED matrix displays. JSON in, pixels out.

The firmware is intentionally dumb — it fetches JSON from a URL and renders what it's told. The server is smart — screen definitions are composable JS modules that can pull data from any source. Everything on the display is defined externally. No reflashing to change what it shows.

Quick Start

git clone <repo>cd thinclock
npm install
cp .env.example .env # edit with your WiFi, timezone, API keys
npm run flash # build & upload firmware to Ulanzi TC001
npm run dev # start config server

On first boot, open serial monitor and paste the JSON line the server prints. Device reboots, connects, and starts rendering.

Architecture

┌─────────────────────────────────────────────────────┐
│ thinclock device (ESP32 + 32×8 LED matrix) │
│ - Fetches config JSON every 30s │
│ - Renders layers at 50fps │
│ - Onboard sensors, buttons, buzzer │
│ - HTTP API: /notify, /timer, /beep, /sensors │
└──────────────────────┬──────────────────────────────┘
│ HTTP (JSON)
┌──────────────────────┴──────────────────────────────┐
│ thinclock server (Node.js) │
│ - Screen modules define what to display │
│ - Alert engine monitors conditions │
│ - Adapters pull data (weather, HA, APIs) │
│ - Management API for enable/disable/scheduling │
└─────────────────────────────────────────────────────┘

Features

Display Engine (Firmware)

  • Layer-based compositing — screens are stacks of typed layers rendered in order
  • Particle system — configurable emitters, gravity, collision, rocket/burst, edge behavior
  • Tweens — animate any layer property (x, y, opacity) with easing and looping
  • Gradients — horizontal, vertical, diagonal with color stops and opacity
  • Native pixel font — 3×5 and 5×7 digit/letter renderers with tight spacing
  • Icon rendering — inline hex pixel data, animation frames, color remapping
  • Gauges — vertical bar, horizontal bar, dot indicators with color ranges
  • Crossfade transitions — smooth blending between screens with both animating
  • Opacity & blend modes — per-layer opacity, additive blending (for stars, glow)
  • Scrolling text — bounce, left (banner), auto modes with edge fade

Notifications & Timer

  • Push notifications via HTTP — colored indicator dots, scrollable text viewer
  • Timer with breathing indicator dot that accelerates as time runs out
  • Pomodoro — work/break cycles with phase indicator bar
  • Alert beeps — single, double, triple, alarm, or custom frequency patterns
  • Buzzer API — trigger any beep pattern via HTTP

Buttons & Input

  • Short press / long press detection with audio feedback
  • Left/Right — navigate screens (short), reserved (long)
  • Middle — open notification viewer (short), context action (long)
  • LDR "button" — cover light sensor to pause/resume timer
  • Server events — all presses POST to server for custom handling

Server

  • Screen modules — self-contained JS files, auto-discovered
  • Scheduling — per-screen time/date/month restrictions
  • Priority sorting — higher priority screens appear first in rotation
  • Night mode — reduced brightness + night-only screens during configured hours
  • Alert engine — screen modules declare conditions, server evaluates and notifies
  • Management API — enable/disable screens, inspect state
  • Weather — OpenWeatherMap integration with dynamic rain/snow particles
  • AQI — air quality with EPA color scale
  • Home Assistant adapter — placeholder for HA WebSocket integration

Project Structure

thinclock/
├── firmware/ # ESP32 PlatformIO project
│ ├── src/ # main, display, particles, gauge, sensors, config
│ ├── include/ # headers
│ └── platformio.ini
├── server/ # Node.js config server
│ ├── screens/ # Screen modules (drop in to add!)
│ ├── adapters/ # Data adapters (HA, etc.)
│ └── lib/ # Registry, alerts, icon helpers
├── homeassistant/ # HA custom component
├── docs/ # Format spec, API docs
└── package.json # Project orchestrator

Scripts

CommandDescription
npm run devStart config server
npm run buildCompile firmware
npm run flashCompile and upload
npm run flash:forceErase flash + upload
npm run eraseErase ESP32 flash
npm run monitorSerial monitor (115200)

Documentation

Hardware

ComponentDetail
BoardESP32-D0WD (Ulanzi TC001)
Display32×8 WS2812 addressable LEDs
LED PinGPIO 32
BuzzerGPIO 15
ButtonsGPIO 26 (L), 27 (M), 14 (R)
LDRGPIO 35
I2CSDA 21, SCL 22
Temp/HumidityBME280/BMP280/HTU21DF/SHT31 (auto-detect)

Environment Variables

# Required
WIFI_SSID=your_network
WIFI_PASS=your_password
SERVER_PORT=3000
TIMEZONE=-5
# Display
BRIGHTNESS=40
BRIGHTNESS_NIGHT=10
NIGHT_HOURS=22,23,0,1,2,3,4,5,6
TIME_FORMAT=12h
TEMP_UNIT=F
ALLOW_BEEPING=true
# Screen management
SCREEN_MODE=auto # auto, manual, all
MAX_SCREENS=8
# SCREEN_ALLOWLIST=clock,weather,sensors# SCREEN_BLOCKLIST=water-fill# Device (for server→device communication)
DEVICE_IP=192.168.x.x
# Weather (OpenWeatherMap free tier)
OWM_API_KEY=your_key
OWM_CITY=Sacramento,CA,US
# Home Assistant (optional)
HA_URL=http://homeassistant.local:8123
HA_TOKEN=your_long_lived_token

Included Screens

ScreenTypeScheduleDescription
Awtrix ClockutilityalwaysCalendar icon + clock + week dots
WeatherutilityalwaysDynamic icon + temp + rain/snow particles
Air QualityutilityalwaysEPA color bar + AQI value
Network MonitorutilityalwaysPing latency + WiFi signal gauge
Sensor DashboardutilityalwaysLight gauge + temperature
PomodoroutilityalwaysWork/break timer with phase bar
Night Clocknight10pm-7amDim red clock with sunrise transition
Bouncing BallsfunalwaysColliding 2×2 rainbow particles
FireworksseasonalJuly 1-4Rockets burst at apex
Winter SceneseasonalNov-FebTree + snowman + snow particles
CampfireambienteveningFire + moon + stars + tent
Ocean WavesambientalwaysGradient water + foam particles
StarfieldambientalwaysParallax warp from center
FireambientalwaysRising flame particles
Lava LampambientalwaysWarm rising blobs
Ambient GradientmoodeveningSlowly shifting color diagonal

License

MIT

About

A custom firmware for the Ulanzi TC001 with an emphasis on simple external data sourcing

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

thinclock

A thin, config-driven ESP32 firmware for LED matrix displays. JSON in, pixels out.

The firmware is intentionally dumb — it fetches JSON from a URL and renders what it's told. The server is smart — screen definitions are composable JS modules that can pull data from any source. Everything on the display is defined externally. No reflashing to change what it shows.

Quick Start

git clone <repo>cd thinclock
npm install
cp .env.example .env # edit with your WiFi, timezone, API keys
npm run flash # build & upload firmware to Ulanzi TC001
npm run dev # start config server

On first boot, open serial monitor and paste the JSON line the server prints. Device reboots, connects, and starts rendering.

Architecture

┌─────────────────────────────────────────────────────┐
│ thinclock device (ESP32 + 32×8 LED matrix) │
│ - Fetches config JSON every 30s │
│ - Renders layers at 50fps │
│ - Onboard sensors, buttons, buzzer │
│ - HTTP API: /notify, /timer, /beep, /sensors │
└──────────────────────┬──────────────────────────────┘
│ HTTP (JSON)
┌──────────────────────┴──────────────────────────────┐
│ thinclock server (Node.js) │
│ - Screen modules define what to display │
│ - Alert engine monitors conditions │
│ - Adapters pull data (weather, HA, APIs) │
│ - Management API for enable/disable/scheduling │
└─────────────────────────────────────────────────────┘

Features

Display Engine (Firmware)

  • Layer-based compositing — screens are stacks of typed layers rendered in order
  • Particle system — configurable emitters, gravity, collision, rocket/burst, edge behavior
  • Tweens — animate any layer property (x, y, opacity) with easing and looping
  • Gradients — horizontal, vertical, diagonal with color stops and opacity
  • Native pixel font — 3×5 and 5×7 digit/letter renderers with tight spacing
  • Icon rendering — inline hex pixel data, animation frames, color remapping
  • Gauges — vertical bar, horizontal bar, dot indicators with color ranges
  • Crossfade transitions — smooth blending between screens with both animating
  • Opacity & blend modes — per-layer opacity, additive blending (for stars, glow)
  • Scrolling text — bounce, left (banner), auto modes with edge fade

Notifications & Timer

  • Push notifications via HTTP — colored indicator dots, scrollable text viewer
  • Timer with breathing indicator dot that accelerates as time runs out
  • Pomodoro — work/break cycles with phase indicator bar
  • Alert beeps — single, double, triple, alarm, or custom frequency patterns
  • Buzzer API — trigger any beep pattern via HTTP

Buttons & Input

  • Short press / long press detection with audio feedback
  • Left/Right — navigate screens (short), reserved (long)
  • Middle — open notification viewer (short), context action (long)
  • LDR "button" — cover light sensor to pause/resume timer
  • Server events — all presses POST to server for custom handling

Server

  • Screen modules — self-contained JS files, auto-discovered
  • Scheduling — per-screen time/date/month restrictions
  • Priority sorting — higher priority screens appear first in rotation
  • Night mode — reduced brightness + night-only screens during configured hours
  • Alert engine — screen modules declare conditions, server evaluates and notifies
  • Management API — enable/disable screens, inspect state
  • Weather — OpenWeatherMap integration with dynamic rain/snow particles
  • AQI — air quality with EPA color scale
  • Home Assistant adapter — placeholder for HA WebSocket integration

Project Structure

thinclock/
├── firmware/ # ESP32 PlatformIO project
│ ├── src/ # main, display, particles, gauge, sensors, config
│ ├── include/ # headers
│ └── platformio.ini
├── server/ # Node.js config server
│ ├── screens/ # Screen modules (drop in to add!)
│ ├── adapters/ # Data adapters (HA, etc.)
│ └── lib/ # Registry, alerts, icon helpers
├── homeassistant/ # HA custom component
├── docs/ # Format spec, API docs
└── package.json # Project orchestrator

Scripts

CommandDescription
npm run devStart config server
npm run buildCompile firmware
npm run flashCompile and upload
npm run flash:forceErase flash + upload
npm run eraseErase ESP32 flash
npm run monitorSerial monitor (115200)

Documentation

Hardware

ComponentDetail
BoardESP32-D0WD (Ulanzi TC001)
Display32×8 WS2812 addressable LEDs
LED PinGPIO 32
BuzzerGPIO 15
ButtonsGPIO 26 (L), 27 (M), 14 (R)
LDRGPIO 35
I2CSDA 21, SCL 22
Temp/HumidityBME280/BMP280/HTU21DF/SHT31 (auto-detect)

Environment Variables

# Required
WIFI_SSID=your_network
WIFI_PASS=your_password
SERVER_PORT=3000
TIMEZONE=-5
# Display
BRIGHTNESS=40
BRIGHTNESS_NIGHT=10
NIGHT_HOURS=22,23,0,1,2,3,4,5,6
TIME_FORMAT=12h
TEMP_UNIT=F
ALLOW_BEEPING=true
# Screen management
SCREEN_MODE=auto # auto, manual, all
MAX_SCREENS=8
# SCREEN_ALLOWLIST=clock,weather,sensors# SCREEN_BLOCKLIST=water-fill# Device (for server→device communication)
DEVICE_IP=192.168.x.x
# Weather (OpenWeatherMap free tier)
OWM_API_KEY=your_key
OWM_CITY=Sacramento,CA,US
# Home Assistant (optional)
HA_URL=http://homeassistant.local:8123
HA_TOKEN=your_long_lived_token

Included Screens

ScreenTypeScheduleDescription
Awtrix ClockutilityalwaysCalendar icon + clock + week dots
WeatherutilityalwaysDynamic icon + temp + rain/snow particles
Air QualityutilityalwaysEPA color bar + AQI value
Network MonitorutilityalwaysPing latency + WiFi signal gauge
Sensor DashboardutilityalwaysLight gauge + temperature
PomodoroutilityalwaysWork/break timer with phase bar
Night Clocknight10pm-7amDim red clock with sunrise transition
Bouncing BallsfunalwaysColliding 2×2 rainbow particles
FireworksseasonalJuly 1-4Rockets burst at apex
Winter SceneseasonalNov-FebTree + snowman + snow particles
CampfireambienteveningFire + moon + stars + tent
Ocean WavesambientalwaysGradient water + foam particles
StarfieldambientalwaysParallax warp from center
FireambientalwaysRising flame particles
Lava LampambientalwaysWarm rising blobs
Ambient GradientmoodeveningSlowly shifting color diagonal

License

MIT

About

A custom firmware for the Ulanzi TC001 with an emphasis on simple external data sourcing

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

thinclock

A thin, config-driven ESP32 firmware for LED matrix displays. JSON in, pixels out.

The firmware is intentionally dumb — it fetches JSON from a URL and renders what it's told. The server is smart — screen definitions are composable JS modules that can pull data from any source. Everything on the display is defined externally. No reflashing to change what it shows.

Quick Start

git clone <repo>cd thinclock
npm install
cp .env.example .env # edit with your WiFi, timezone, API keys
npm run flash # build & upload firmware to Ulanzi TC001
npm run dev # start config server

On first boot, open serial monitor and paste the JSON line the server prints. Device reboots, connects, and starts rendering.

Architecture

┌─────────────────────────────────────────────────────┐
│ thinclock device (ESP32 + 32×8 LED matrix) │
│ - Fetches config JSON every 30s │
│ - Renders layers at 50fps │
│ - Onboard sensors, buttons, buzzer │
│ - HTTP API: /notify, /timer, /beep, /sensors │
└──────────────────────┬──────────────────────────────┘
│ HTTP (JSON)
┌──────────────────────┴──────────────────────────────┐
│ thinclock server (Node.js) │
│ - Screen modules define what to display │
│ - Alert engine monitors conditions │
│ - Adapters pull data (weather, HA, APIs) │
│ - Management API for enable/disable/scheduling │
└─────────────────────────────────────────────────────┘

Features

Display Engine (Firmware)

  • Layer-based compositing — screens are stacks of typed layers rendered in order
  • Particle system — configurable emitters, gravity, collision, rocket/burst, edge behavior
  • Tweens — animate any layer property (x, y, opacity) with easing and looping
  • Gradients — horizontal, vertical, diagonal with color stops and opacity
  • Native pixel font — 3×5 and 5×7 digit/letter renderers with tight spacing
  • Icon rendering — inline hex pixel data, animation frames, color remapping
  • Gauges — vertical bar, horizontal bar, dot indicators with color ranges
  • Crossfade transitions — smooth blending between screens with both animating
  • Opacity & blend modes — per-layer opacity, additive blending (for stars, glow)
  • Scrolling text — bounce, left (banner), auto modes with edge fade

Notifications & Timer

  • Push notifications via HTTP — colored indicator dots, scrollable text viewer
  • Timer with breathing indicator dot that accelerates as time runs out
  • Pomodoro — work/break cycles with phase indicator bar
  • Alert beeps — single, double, triple, alarm, or custom frequency patterns
  • Buzzer API — trigger any beep pattern via HTTP

Buttons & Input

  • Short press / long press detection with audio feedback
  • Left/Right — navigate screens (short), reserved (long)
  • Middle — open notification viewer (short), context action (long)
  • LDR "button" — cover light sensor to pause/resume timer
  • Server events — all presses POST to server for custom handling

Server

  • Screen modules — self-contained JS files, auto-discovered
  • Scheduling — per-screen time/date/month restrictions
  • Priority sorting — higher priority screens appear first in rotation
  • Night mode — reduced brightness + night-only screens during configured hours
  • Alert engine — screen modules declare conditions, server evaluates and notifies
  • Management API — enable/disable screens, inspect state
  • Weather — OpenWeatherMap integration with dynamic rain/snow particles
  • AQI — air quality with EPA color scale
  • Home Assistant adapter — placeholder for HA WebSocket integration

Project Structure

thinclock/
├── firmware/ # ESP32 PlatformIO project
│ ├── src/ # main, display, particles, gauge, sensors, config
│ ├── include/ # headers
│ └── platformio.ini
├── server/ # Node.js config server
│ ├── screens/ # Screen modules (drop in to add!)
│ ├── adapters/ # Data adapters (HA, etc.)
│ └── lib/ # Registry, alerts, icon helpers
├── homeassistant/ # HA custom component
├── docs/ # Format spec, API docs
└── package.json # Project orchestrator

Scripts

CommandDescription
npm run devStart config server
npm run buildCompile firmware
npm run flashCompile and upload
npm run flash:forceErase flash + upload
npm run eraseErase ESP32 flash
npm run monitorSerial monitor (115200)

Documentation

Hardware

ComponentDetail
BoardESP32-D0WD (Ulanzi TC001)
Display32×8 WS2812 addressable LEDs
LED PinGPIO 32
BuzzerGPIO 15
ButtonsGPIO 26 (L), 27 (M), 14 (R)
LDRGPIO 35
I2CSDA 21, SCL 22
Temp/HumidityBME280/BMP280/HTU21DF/SHT31 (auto-detect)

Environment Variables

# Required
WIFI_SSID=your_network
WIFI_PASS=your_password
SERVER_PORT=3000
TIMEZONE=-5
# Display
BRIGHTNESS=40
BRIGHTNESS_NIGHT=10
NIGHT_HOURS=22,23,0,1,2,3,4,5,6
TIME_FORMAT=12h
TEMP_UNIT=F
ALLOW_BEEPING=true
# Screen management
SCREEN_MODE=auto # auto, manual, all
MAX_SCREENS=8
# SCREEN_ALLOWLIST=clock,weather,sensors# SCREEN_BLOCKLIST=water-fill# Device (for server→device communication)
DEVICE_IP=192.168.x.x
# Weather (OpenWeatherMap free tier)
OWM_API_KEY=your_key
OWM_CITY=Sacramento,CA,US
# Home Assistant (optional)
HA_URL=http://homeassistant.local:8123
HA_TOKEN=your_long_lived_token

Included Screens

ScreenTypeScheduleDescription
Awtrix ClockutilityalwaysCalendar icon + clock + week dots
WeatherutilityalwaysDynamic icon + temp + rain/snow particles
Air QualityutilityalwaysEPA color bar + AQI value
Network MonitorutilityalwaysPing latency + WiFi signal gauge
Sensor DashboardutilityalwaysLight gauge + temperature
PomodoroutilityalwaysWork/break timer with phase bar
Night Clocknight10pm-7amDim red clock with sunrise transition
Bouncing BallsfunalwaysColliding 2×2 rainbow particles
FireworksseasonalJuly 1-4Rockets burst at apex
Winter SceneseasonalNov-FebTree + snowman + snow particles
CampfireambienteveningFire + moon + stars + tent
Ocean WavesambientalwaysGradient water + foam particles
StarfieldambientalwaysParallax warp from center
FireambientalwaysRising flame particles
Lava LampambientalwaysWarm rising blobs
Ambient GradientmoodeveningSlowly shifting color diagonal

License

MIT

About

A custom firmware for the Ulanzi TC001 with an emphasis on simple external data sourcing

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

thinclock

A thin, config-driven ESP32 firmware for LED matrix displays. JSON in, pixels out.

The firmware is intentionally dumb — it fetches JSON from a URL and renders what it's told. The server is smart — screen definitions are composable JS modules that can pull data from any source. Everything on the display is defined externally. No reflashing to change what it shows.

Quick Start

git clone <repo>cd thinclock
npm install
cp .env.example .env # edit with your WiFi, timezone, API keys
npm run flash # build & upload firmware to Ulanzi TC001
npm run dev # start config server

On first boot, open serial monitor and paste the JSON line the server prints. Device reboots, connects, and starts rendering.

Architecture

┌─────────────────────────────────────────────────────┐
│ thinclock device (ESP32 + 32×8 LED matrix) │
│ - Fetches config JSON every 30s │
│ - Renders layers at 50fps │
│ - Onboard sensors, buttons, buzzer │
│ - HTTP API: /notify, /timer, /beep, /sensors │
└──────────────────────┬──────────────────────────────┘
│ HTTP (JSON)
┌──────────────────────┴──────────────────────────────┐
│ thinclock server (Node.js) │
│ - Screen modules define what to display │
│ - Alert engine monitors conditions │
│ - Adapters pull data (weather, HA, APIs) │
│ - Management API for enable/disable/scheduling │
└─────────────────────────────────────────────────────┘

Features

Display Engine (Firmware)

  • Layer-based compositing — screens are stacks of typed layers rendered in order
  • Particle system — configurable emitters, gravity, collision, rocket/burst, edge behavior
  • Tweens — animate any layer property (x, y, opacity) with easing and looping
  • Gradients — horizontal, vertical, diagonal with color stops and opacity
  • Native pixel font — 3×5 and 5×7 digit/letter renderers with tight spacing
  • Icon rendering — inline hex pixel data, animation frames, color remapping
  • Gauges — vertical bar, horizontal bar, dot indicators with color ranges
  • Crossfade transitions — smooth blending between screens with both animating
  • Opacity & blend modes — per-layer opacity, additive blending (for stars, glow)
  • Scrolling text — bounce, left (banner), auto modes with edge fade

Notifications & Timer

  • Push notifications via HTTP — colored indicator dots, scrollable text viewer
  • Timer with breathing indicator dot that accelerates as time runs out
  • Pomodoro — work/break cycles with phase indicator bar
  • Alert beeps — single, double, triple, alarm, or custom frequency patterns
  • Buzzer API — trigger any beep pattern via HTTP

Buttons & Input

  • Short press / long press detection with audio feedback
  • Left/Right — navigate screens (short), reserved (long)
  • Middle — open notification viewer (short), context action (long)
  • LDR "button" — cover light sensor to pause/resume timer
  • Server events — all presses POST to server for custom handling

Server

  • Screen modules — self-contained JS files, auto-discovered
  • Scheduling — per-screen time/date/month restrictions
  • Priority sorting — higher priority screens appear first in rotation
  • Night mode — reduced brightness + night-only screens during configured hours
  • Alert engine — screen modules declare conditions, server evaluates and notifies
  • Management API — enable/disable screens, inspect state
  • Weather — OpenWeatherMap integration with dynamic rain/snow particles
  • AQI — air quality with EPA color scale
  • Home Assistant adapter — placeholder for HA WebSocket integration

Project Structure

thinclock/
├── firmware/ # ESP32 PlatformIO project
│ ├── src/ # main, display, particles, gauge, sensors, config
│ ├── include/ # headers
│ └── platformio.ini
├── server/ # Node.js config server
│ ├── screens/ # Screen modules (drop in to add!)
│ ├── adapters/ # Data adapters (HA, etc.)
│ └── lib/ # Registry, alerts, icon helpers
├── homeassistant/ # HA custom component
├── docs/ # Format spec, API docs
└── package.json # Project orchestrator

Scripts

CommandDescription
npm run devStart config server
npm run buildCompile firmware
npm run flashCompile and upload
npm run flash:forceErase flash + upload
npm run eraseErase ESP32 flash
npm run monitorSerial monitor (115200)

Documentation

Hardware

ComponentDetail
BoardESP32-D0WD (Ulanzi TC001)
Display32×8 WS2812 addressable LEDs
LED PinGPIO 32
BuzzerGPIO 15
ButtonsGPIO 26 (L), 27 (M), 14 (R)
LDRGPIO 35
I2CSDA 21, SCL 22
Temp/HumidityBME280/BMP280/HTU21DF/SHT31 (auto-detect)

Environment Variables

# Required
WIFI_SSID=your_network
WIFI_PASS=your_password
SERVER_PORT=3000
TIMEZONE=-5
# Display
BRIGHTNESS=40
BRIGHTNESS_NIGHT=10
NIGHT_HOURS=22,23,0,1,2,3,4,5,6
TIME_FORMAT=12h
TEMP_UNIT=F
ALLOW_BEEPING=true
# Screen management
SCREEN_MODE=auto # auto, manual, all
MAX_SCREENS=8
# SCREEN_ALLOWLIST=clock,weather,sensors# SCREEN_BLOCKLIST=water-fill# Device (for server→device communication)
DEVICE_IP=192.168.x.x
# Weather (OpenWeatherMap free tier)
OWM_API_KEY=your_key
OWM_CITY=Sacramento,CA,US
# Home Assistant (optional)
HA_URL=http://homeassistant.local:8123
HA_TOKEN=your_long_lived_token

Included Screens

ScreenTypeScheduleDescription
Awtrix ClockutilityalwaysCalendar icon + clock + week dots
WeatherutilityalwaysDynamic icon + temp + rain/snow particles
Air QualityutilityalwaysEPA color bar + AQI value
Network MonitorutilityalwaysPing latency + WiFi signal gauge
Sensor DashboardutilityalwaysLight gauge + temperature
PomodoroutilityalwaysWork/break timer with phase bar
Night Clocknight10pm-7amDim red clock with sunrise transition
Bouncing BallsfunalwaysColliding 2×2 rainbow particles
FireworksseasonalJuly 1-4Rockets burst at apex
Winter SceneseasonalNov-FebTree + snowman + snow particles
CampfireambienteveningFire + moon + stars + tent
Ocean WavesambientalwaysGradient water + foam particles
StarfieldambientalwaysParallax warp from center
FireambientalwaysRising flame particles
Lava LampambientalwaysWarm rising blobs
Ambient GradientmoodeveningSlowly shifting color diagonal

License

MIT

About

A custom firmware for the Ulanzi TC001 with an emphasis on simple external data sourcing

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

thinclock

A thin, config-driven ESP32 firmware for LED matrix displays. JSON in, pixels out.

The firmware is intentionally dumb — it fetches JSON from a URL and renders what it's told. The server is smart — screen definitions are composable JS modules that can pull data from any source. Everything on the display is defined externally. No reflashing to change what it shows.

Quick Start

git clone <repo>cd thinclock
npm install
cp .env.example .env # edit with your WiFi, timezone, API keys
npm run flash # build & upload firmware to Ulanzi TC001
npm run dev # start config server

On first boot, open serial monitor and paste the JSON line the server prints. Device reboots, connects, and starts rendering.

Architecture

┌─────────────────────────────────────────────────────┐
│ thinclock device (ESP32 + 32×8 LED matrix) │
│ - Fetches config JSON every 30s │
│ - Renders layers at 50fps │
│ - Onboard sensors, buttons, buzzer │
│ - HTTP API: /notify, /timer, /beep, /sensors │
└──────────────────────┬──────────────────────────────┘
│ HTTP (JSON)
┌──────────────────────┴──────────────────────────────┐
│ thinclock server (Node.js) │
│ - Screen modules define what to display │
│ - Alert engine monitors conditions │
│ - Adapters pull data (weather, HA, APIs) │
│ - Management API for enable/disable/scheduling │
└─────────────────────────────────────────────────────┘

Features

Display Engine (Firmware)

  • Layer-based compositing — screens are stacks of typed layers rendered in order
  • Particle system — configurable emitters, gravity, collision, rocket/burst, edge behavior
  • Tweens — animate any layer property (x, y, opacity) with easing and looping
  • Gradients — horizontal, vertical, diagonal with color stops and opacity
  • Native pixel font — 3×5 and 5×7 digit/letter renderers with tight spacing
  • Icon rendering — inline hex pixel data, animation frames, color remapping
  • Gauges — vertical bar, horizontal bar, dot indicators with color ranges
  • Crossfade transitions — smooth blending between screens with both animating
  • Opacity & blend modes — per-layer opacity, additive blending (for stars, glow)
  • Scrolling text — bounce, left (banner), auto modes with edge fade

Notifications & Timer

  • Push notifications via HTTP — colored indicator dots, scrollable text viewer
  • Timer with breathing indicator dot that accelerates as time runs out
  • Pomodoro — work/break cycles with phase indicator bar
  • Alert beeps — single, double, triple, alarm, or custom frequency patterns
  • Buzzer API — trigger any beep pattern via HTTP

Buttons & Input

  • Short press / long press detection with audio feedback
  • Left/Right — navigate screens (short), reserved (long)
  • Middle — open notification viewer (short), context action (long)
  • LDR "button" — cover light sensor to pause/resume timer
  • Server events — all presses POST to server for custom handling

Server

  • Screen modules — self-contained JS files, auto-discovered
  • Scheduling — per-screen time/date/month restrictions
  • Priority sorting — higher priority screens appear first in rotation
  • Night mode — reduced brightness + night-only screens during configured hours
  • Alert engine — screen modules declare conditions, server evaluates and notifies
  • Management API — enable/disable screens, inspect state
  • Weather — OpenWeatherMap integration with dynamic rain/snow particles
  • AQI — air quality with EPA color scale
  • Home Assistant adapter — placeholder for HA WebSocket integration

Project Structure

thinclock/
├── firmware/ # ESP32 PlatformIO project
│ ├── src/ # main, display, particles, gauge, sensors, config
│ ├── include/ # headers
│ └── platformio.ini
├── server/ # Node.js config server
│ ├── screens/ # Screen modules (drop in to add!)
│ ├── adapters/ # Data adapters (HA, etc.)
│ └── lib/ # Registry, alerts, icon helpers
├── homeassistant/ # HA custom component
├── docs/ # Format spec, API docs
└── package.json # Project orchestrator

Scripts

CommandDescription
npm run devStart config server
npm run buildCompile firmware
npm run flashCompile and upload
npm run flash:forceErase flash + upload
npm run eraseErase ESP32 flash
npm run monitorSerial monitor (115200)

Documentation

Hardware

ComponentDetail
BoardESP32-D0WD (Ulanzi TC001)
Display32×8 WS2812 addressable LEDs
LED PinGPIO 32
BuzzerGPIO 15
ButtonsGPIO 26 (L), 27 (M), 14 (R)
LDRGPIO 35
I2CSDA 21, SCL 22
Temp/HumidityBME280/BMP280/HTU21DF/SHT31 (auto-detect)

Environment Variables

# Required
WIFI_SSID=your_network
WIFI_PASS=your_password
SERVER_PORT=3000
TIMEZONE=-5
# Display
BRIGHTNESS=40
BRIGHTNESS_NIGHT=10
NIGHT_HOURS=22,23,0,1,2,3,4,5,6
TIME_FORMAT=12h
TEMP_UNIT=F
ALLOW_BEEPING=true
# Screen management
SCREEN_MODE=auto # auto, manual, all
MAX_SCREENS=8
# SCREEN_ALLOWLIST=clock,weather,sensors# SCREEN_BLOCKLIST=water-fill# Device (for server→device communication)
DEVICE_IP=192.168.x.x
# Weather (OpenWeatherMap free tier)
OWM_API_KEY=your_key
OWM_CITY=Sacramento,CA,US
# Home Assistant (optional)
HA_URL=http://homeassistant.local:8123
HA_TOKEN=your_long_lived_token

Included Screens

ScreenTypeScheduleDescription
Awtrix ClockutilityalwaysCalendar icon + clock + week dots
WeatherutilityalwaysDynamic icon + temp + rain/snow particles
Air QualityutilityalwaysEPA color bar + AQI value
Network MonitorutilityalwaysPing latency + WiFi signal gauge
Sensor DashboardutilityalwaysLight gauge + temperature
PomodoroutilityalwaysWork/break timer with phase bar
Night Clocknight10pm-7amDim red clock with sunrise transition
Bouncing BallsfunalwaysColliding 2×2 rainbow particles
FireworksseasonalJuly 1-4Rockets burst at apex
Winter SceneseasonalNov-FebTree + snowman + snow particles
CampfireambienteveningFire + moon + stars + tent
Ocean WavesambientalwaysGradient water + foam particles
StarfieldambientalwaysParallax warp from center
FireambientalwaysRising flame particles
Lava LampambientalwaysWarm rising blobs
Ambient GradientmoodeveningSlowly shifting color diagonal

License

MIT

About

A custom firmware for the Ulanzi TC001 with an emphasis on simple external data sourcing

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages