- C++ 51.9%
- C 43.6%
- Python 1.6%
- CMake 1.5%
- Shell 1.4%
|
All checks were successful
Lint / format (push) Successful in 30s
Lint / tidy (push) Successful in 3m16s
BTClock CI / host-tests (push) Successful in 2m6s
BTClock CI / webui-and-lfs (push) Successful in 3m20s
BTClock CI / build (btclock_rev_b_213epd) (push) Successful in 1m53s
BTClock CI / build (lolin_s3_mini_213epd) (push) Successful in 1m57s
BTClock CI / build (lolin_s3_mini_29epd) (push) Successful in 2m9s
BTClock CI / build (btclock_v8_213epd) (push) Successful in 13m51s
BTClock CI / release (push) Successful in 1m37s
The release `.sha256` files follow the standard shasum output format — `<64-char hex> <filename>\n` — but downloadSHA256() returned the whole trimmed body, so expectedSHA256 ended up being something like "bcd371…d31 lolin_s3_mini_213epd_firmware.bin". Comparing that against the 64-char hex we compute in calculateSHA256() / verifyStagedPartition SHA256() never matched. Latent since v3.x. The WebUI path silently appeared to work because Update.begin(U_SPIFFS) writes to the LittleFS partition incrementally during the loop; Update.abort() on the SHA mismatch did not undo the writes, so the new bundle was already in flash on the next boot. The firmware path through esp_https_ota properly stages into a passive app slot and only flips the boot pointer on _finish — abort-on-mismatch correctly left the running app untouched, which is how this bug got visible after the Phase 1 OTA refactor. Fix: split off the first whitespace-delimited token after trim(). Verified end-to-end on Rev A: triggered /api/firmware/auto_update with gitRev=7e17c1c (the post-3.5.1 fix-commit build) and watched the device transition to gitRev=d8fc2ef (3.5.1 release commit) via OTA download + SHA verify + esp_https_ota_finish + ESP.restart(). |
||
|---|---|---|
| .forgejo/workflows | ||
| .github/workflows | ||
| .vscode | ||
| data@249601477e | ||
| docs | ||
| lib | ||
| main | ||
| scripts | ||
| tests | ||
| .clang-tidy | ||
| .gitignore | ||
| .gitmodules | ||
| arduino_libraries.json | ||
| CMakeLists.txt | ||
| dependencies.lock | ||
| LICENSE.txt | ||
| maintainers.yaml | ||
| partition.csv | ||
| partition_8mb.csv | ||
| partition_16mb.csv | ||
| README.md | ||
| renovate.json | ||
| sdkconfig.defaults | ||
| sdkconfig.defaults.btclock_rev_b_213epd | ||
| sdkconfig.defaults.btclock_v8_213epd | ||
| sdkconfig.defaults.lolin_s3_mini_29epd | ||
| sdkconfig.defaults.lolin_s3_mini_213epd | ||
BTClock v3
Software for the BTClock project. Built on the ESP-IDF with Arduino as a library, using WebSockets for all live data and making heavy use of native timers and interrupts.
See the docs repo for user-facing
information and flashing instructions. Developer docs (architecture, build envs,
API reference, testing, preferences) live in docs/ in this
repo.
Features
-
Screens, each individually toggleable and shown in rotation with a configurable interval:
- Block height
- Clock
- Halving countdown
- Block fee rate
- Sats per currency
- BTC ticker (price)
- Market cap
- Bitcoin supply
- Bitaxe hashrate / best difficulty (when Bitaxe is enabled)
- Mining pool hashrate / earnings (when mining pool stats are enabled)
-
Steal focus on new block — display jumps to the block height screen when a new block is mined, with an LED flash in a color you choose.
-
Multiple data sources, each supporting the full list of configured currencies on the currency-dependent screens:
- The default BTClock source
- Any public mempool.space instance, including
self-hosted. Prices come from Kraken and populate every currency in
actCurrencies, not just USD. - A Nostr relay
- A custom WebSocket endpoint that speaks the BTClock data format — see ws-go-server for a self-hostable reference implementation
-
Bitaxe integration — fetch hashrate and best difficulty from a local Bitaxe miner and show it on its own screens.
-
Mining pool stats — hashrate and (where the pool exposes it) earnings screens. Supported pools:
- Braiins
- Ocean
- Noderunners
- Satoshi Radio
- public-pool.io
- Self-hosted Public Pool
- GoBrrr Pool
- CKPool
- EU CKPool
Noderunners and Satoshi Radio additionally expose a pool-wide hashrate; a "Show pool-wide hashrate" toggle switches the screen from per-user to global without needing a username.
-
Nostr Zap notifier — flash the LEDs (and frontlight on supported hardware) and optionally switch the screen when a zap lands on a given pubkey.
-
Frontlight control — brightness, effect speed, flash on block/zap, always-on, and auto-off when the ambient light sensor reports a dark room (on hardware that has one).
-
Time-based Do Not Disturb — dim/disable the display between a set start and end time.
-
Customisation — inverted colors, multiple fonts, LED brightness and flash color, currencies shown on the currency-dependent screens, big characters for market cap, block countdown mode, sats symbol, price suffix formatting, MoW mode (Samson-Mow-style price in millions, e.g.
0.1Minstead of100k), Moscow Time toggle that switches the USD sats-per-currency screen between the classic "MSCW/TIME" label and the generic "SATS/USD" used by the other currencies, and more. -
Multi-language WebUI — English, Dutch, German, Spanish.
-
Over-the-air updates — ESP OTA via the WebUI, and ArduinoOTA for push updates over the network.
-
mDNS discovery so you can reach the device by hostname.
Security: the device is meant to run on a trusted private network. Since 3.2.0 the WebUI can be password-protected and the firmware verifies server certificates on outbound requests. Since 3.4.0 the ArduinoOTA push-update mechanism can be password-protected too.
Building
Built with ESP-IDF 5.5 and arduino-esp32 as a managed component. The short version:
git clone --recurse-submodules https://git.btclock.dev/btclock/btclock_v3.git
cd btclock_v3
source ~/esp/esp-idf/export.sh # IDF v5.5 installed at ~/esp/esp-idf
./scripts/build.sh # all four shipping variants
Full build, flash, and OTA instructions are in
docs/BUILD.md. Tests are in
docs/TESTING.md.
Mining pool stats
Enable mining pool stats from the WebUI under Settings → Extra Features → Enable Mining Pool Stats, then pick your pool and enter your pool username or API key.
The Mining Pool Earnings screen displays:
- Braiins: today's mining reward so far.
- Ocean: your estimated earnings if the pool were to find a block right now.
For solo pools there are no earnings estimations; your username is the on-chain withdrawal address you configured at the pool, without the worker name.
Braiins Pool integration
Create an API key using the steps here. The key's permissions should be:
- Web Access: no
- API Access: yes
- Access Permissions: Read-only
Copy the token and enter it as your "Mining Pool username or api key" in the WebUI.
Ocean integration
Your "Mining Pool username" is the on-chain withdrawal address you specify when pointing your miners at Ocean.
Self-hosted Public Pool
Select Local Public Pool as the mining pool and enter the endpoint of
your node's pool API (for example umbrel.local:2019). The WebUI has a
"Test" button that verifies it can reach the instance.