PikOS is a clean-room, local-first firmware for the ULANZI TC001 32×8 RGB pixel clock. Its primary job is to run a flexible set of information apps on the ESP32 itself, with a browser used for configuration, testing, and OTA.
It does not use an external PikOS cloud, telemetry, or a required home automation server.
- Clock with NTP synchronization and DS3231 fallback
- Internal SHT3x temperature/humidity app
- Netatmo Weather Station app with OAuth token refresh, indoor/outdoor temperature and humidity, wind and rain readings, and dry-to-raining alerts
- Up to six configurable RSS/Atom feeds with automatically fetched, locally cached favicon icons and favicon-derived text colours
- Keyless Yahoo stock quotes, Frankfurter daily forex reference rates, and Kraken Bitcoin OHLC data, with optional Twelve Data
- Elering day-ahead energy prices with low/average/high daily bands, VAT, and a countdown to the next meaningful price-band change
- Add/remove named calendars from private read-only iCal addresses, each with its own icon, merged by start time with compact days/hours/minutes countdowns and alerts for exported reminders
- Up to 12 background alarms plus an interactive Counter app that works as a stopwatch or countdown timer without depending on Wi-Fi
- Experimental, account-free Tallinn stop departures with up to 16 saved stop/line/icon combinations, two cached departures each, and local countdowns
- Add/remove account-free Reddit top-post feeds with an icon per subreddit
- Two-stage market views with text followed by continuous 23×7 charts: selectable Yahoo history and Elering time windows beside the selected icon
- Add/remove multi-zone clocks with searchable IANA-style city presets, daylight-saving rules, half-hour offsets, and an icon for each timezone
- Static, ticker, split-flap, slide, rainbow, pixel-rain, and nearest-position “magic move” animation per local app; icons participate in each transition
- Mixed-case Estonian-capable pixel font and word-aware animation pages
- Per-state and per-item selection from the LaMetric 8×8 icon gallery, including animated icons, weather/energy/battery variants, timezone, RSS-feed and ticker pairings, an offline cache, and built-in fallbacks
- Estonian and English device/web-interface text
- Automatic hue-stable LDR brightness with configurable calibration, gamma, limits, and temporal dithering
- Filtered battery estimation with a persistent observed ADC profile, a five-level vertical gauge, optional low-battery brightness saver, and an option to add the Battery app to the rotation only when charge is low
- Manual brightness override and 180° display orientation
- Deterministic semantic and favicon-derived colours
- Scheduled or idle sleep, button wake, display-off and fixed-minimum-brightness StandBy clock modes, app durations, and message limits
- Fair automatic rotation with persistent per-app cursors, so timezones, headlines, tickers, events, and posts resume where they left off
- A visible two-tier synchronization app every 1, 5, or 10 slideshow loops: one bounded provider queue refreshes stored snapshots and lightweight app logic derives the current electricity/calendar state from timestamps
- A single, paced 64 fps render path on its own task, with button sampling isolated from web, Wi-Fi, TLS, and sensor work
- Selectable per-app feature tones, six assignable custom RTTTL tune slots, a simple volume slider, and measured low-volume note compensation
- Browser-based settings with native light/dark appearance, visual animation choices, one auditionable named sound library, a main-page notification centre, a full-height horizontal app carousel, per-app storage health, a compressed 32×8 digital twin, on-screen button remote control, browser-rendered motion, realistic pixel borders, and OTA
- Optional LAN notification and pixel-frame ingestion protected by a generated API key; experimental remote-display protocols are not exposed in the normal UI
The verified TC001 hardware map is:
| Function | GPIO |
|---|---|
| 256-pixel WS2812B matrix | 32 |
| Left / middle / right buttons | 26 / 27 / 14 |
| Buzzer | 15 |
| Battery ADC | 34 |
| Light ADC | 35 |
| I²C SDA / SCL | 21 / 22 |
| SHT3x / DS3231 | 0x44 / 0x68 |
The map agrees with the existing AWTRIX 3 hardware documentation and the Tasmota TC001 template.
Install PlatformIO, then:
pio runThe resulting image is .pio/build/tc001/firmware.bin. The current RC build
occupies about three quarters of the 1.875 MiB OTA slot.
The packaged release contains:
dist/ulanzi-pikos-0.22.0-rc6-full.bin— bootloader, partition table, and firmware merged for the first serial installation at address0x0;dist/ulanzi-pikos-0.22.0-rc6-ota.bin— application-only image for PikOS's browser updater; anddist/SHA256SUMS— integrity hashes for both images.
Before replacing the factory firmware, make a backup. With the clock switched on and connected through a USB-C data cable:
esptool.py --port /dev/your-port read_flash 0x0 0x400000 tc001-factory.binThen flash PikOS:
pio run --target upload --upload-port /dev/your-portOr install the packaged full image without PlatformIO:
esptool.py --chip esp32 --port /dev/your-port write_flash 0x0 \
dist/ulanzi-pikos-0.22.0-rc6-full.binFor animation performance testing, PikOS exposes the compact
/api/v1/performance endpoint. The included monitor checks effective render
FPS, missed 16.67 ms deadlines, worst frame gap, and LED transmission time:
python3 tools/monitor_framerate.py pikos.local --seconds 60
python3 tools/monitor_framerate.py pikos.local --seconds 60 --stress-ui
python3 tools/monitor_visual_integrity.py pikos.local --app rss --seconds 30
python3 tools/monitor_button_latency.py pikos.local --mode api --stress-page
python3 tools/stress_sync.py pikos.local --cycles 5To restore a full 4 MB backup later:
esptool.py --port /dev/your-port write_flash 0x0 tc001-factory.binKeep the backup private: it can contain saved Wi‑Fi credentials and other factory settings. A full-flash restore replaces PikOS, its settings, and its partition table.
The TC001 includes a CH340 USB/serial bridge. A CH340 driver may be needed on older operating systems. Do not disconnect power while erasing or flashing.
- PikOS first tries the saved Wi‑Fi network.
- Without credentials, or after a 15-second connection failure, it creates
PikOS-XXXXXX. - Read the unique
PASS …value scrolling on the matrix and use it to join the setup network. - Open
http://192.168.4.1/. - Enter Wi‑Fi credentials, choose English or Estonian, and save.
- On the home network, use the IP shown by a long middle-button press or in the browser status pane.
Secret fields are returned blank by the settings API. A blank secret field means “keep the stored value.” Wi-Fi, Netatmo, and market credentials are stored in small dedicated NVS entries rather than inside the larger rotating settings document, so an unrelated configuration rollback cannot silently erase them. The status API also reports a numeric Wi-Fi disconnect reason and a human-readable diagnosis.
| Action | Result |
|---|---|
| Left / right press | Previous / next app and enter sticky manual mode |
| Middle press | Advance once and resume automatic rotation |
| Middle long press | Show Wi‑Fi/AP address |
| Left long press | Queue a refresh of all enabled online apps |
| Any button during sleep | Wake for the configured interval |
Left and right swap automatically when 180° orientation is enabled.
Each app can be enabled independently and assigned static, ticker, flip,
slide, rainbow, rain, or magic animation. Split-flap and static layouts
paginate on word boundaries. Rain drops the old pixels away individually and
lets the next page fall in; magic pairs new pixels with the nearest old pixel
and moves them into place. Automatic rotation gives every app the same
configured time budget, then finishes the active page before changing apps;
in sticky manual mode, multi-item apps continue indefinitely.
Online sources refresh in a dedicated Sync app after the selected number of
complete slideshow loops. Its small serialized jobs keep TLS and parser memory
bounded on the ESP32 while the Earth, red bidirectional signal, satellite, and
provider progress bar make the pause explicit. Between syncs, energy prices,
chart position, price band, calendar countdowns, and alarms are derived from
the stored timestamped data without network work. A newly detected top headline is shown
immediately and can play its selected tone. With the ticker animation, the
feed icon travels with the headline so the full 32-pixel width becomes
available after it leaves the display.
Magic Move preserves matching characters as whole glyphs between pages, then
moves only the remaining unmatched pixels to their nearest new positions.
Large prices compact only when necessary: currencies stay in front, so
52673 USD becomes $53K, while the narrow numeric font lets $8,5K fit.
The otherwise unused bottom row is a mode indicator: a subtle grey timeline
means automatic rotation; in manual mode, the full 32-pixel width is divided
continuously between enabled apps and the brighter segment marks the pinned
app. During Sync, progress advances pixel by pixel using learned provider
timings instead of jumping once per provider.
The energy app reads official Elering day-ahead prices, converts EUR/MWh to cents/kWh, optionally adds VAT, and divides each local day into low, average, and high price thirds. The compact view shows the current price, communicates its band with the icon and colour, then shows the selected today, next-12-hour, next-24-hour, or today-and-tomorrow chart. Every graph segment uses the same daily price-band thresholds: green is cheap, yellow is average, and red is expensive.
The settings page searches the
LaMetric icon gallery and assigns one
icon ID inside each app card. The selected climate provider has low/medium/high
temperature and humidity icons; markets pair each ticker with its own icon; and
energy has low/average/high icons. PikOS downloads selected 8×8 frame data in
the background and caches a
compact copy in SPIFFS. For RSS, an ID of 0 fetches the feed favicon at
runtime; for markets it generates a compact ticker/currency badge; other
zero-ID fields select their built-in fallback. RSS text colour follows the
cached feed icon's dominant saturated colour; temperature, market, battery,
energy, and clock text keep predictable semantic colours.
The buzzer accepts standard monophonic RTTTL strings in six configurable slots. Each slot can be selected independently for RSS, energy changes, hourly, remote notification, and button events, and tested from the browser before saving. PWM duty controls volume. The bundled 99% pitch calibration and volume-aware high-note equalization were measured from the TC001 with a Sennheiser Profile microphone so changing the single volume slider changes loudness without reshaping the tune as much.
The single desktop setup helper handles both supported account flows:
python3 tools/pikos_credentials.py netatmo
python3 tools/pikos_credentials.py garminIt prompts for secrets locally, writes no token store, emits copyable settings JSON, and never prints passwords, MFA codes, authorization codes, or short-lived access tokens. The JSON contains the persistent credentials PikOS needs, so clear the terminal after pasting it into the matching app card.
For Netatmo setup, see docs/NETATMO.md. Market setup is in docs/MARKETS.md, and energy-price behavior is in docs/ENERGY.md. The experimental free Garmin Connect route is in docs/garmin-connect-activity.md, while the experimental Tallinn departures contract and future routing choices are in docs/travel-app.md. The optional LAN API is documented in docs/API.md.
- Provider HTTPS connections authenticate server certificates against an embedded 122-root public CA bundle. Browser reads never return Wi‑Fi, Netatmo, Garmin, calendar, market, or remote API credentials.
- Settings, controls, sound tests, restart, and OTA require an HttpOnly per-boot browser session plus explicit same-origin intent. Remote notifications and raw frames use a separate constant-time API-key check.
- The portal remains a trusted-LAN appliance without a user login, and local browser traffic is plain HTTP. Do not expose port 80 to the internet; use a trusted network or isolated IoT VLAN. Physical flash access can still reveal NVS credentials because stock TC001 hardware does not provide PikOS with secure boot or flash encryption.
- Release checks include 52 automated contracts, an ESP32 build, secret scan, browser interaction/overflow checks, and hardware FPS/button/sync monitors.
- PikOS has been flashed and exercised on the target TC001. Each release image is still a test build until its display behavior and online services have been checked on the physical clock.
See SECURITY.md for the trust model and reporting process.
include/ Public firmware modules and embedded web UI
src/ Firmware implementation
scripts/ Deterministic embedded-asset generators
tools/ Unified account setup and LAN diagnostic helpers
docs/ Integration and API guides
platformio.ini Reproducible ESP32 build
partitions.csv Dual 1.875 MB OTA slots
MIT. See LICENSE.
