Compare commits

...
Sign in to create a new pull request.

101 commits

Author SHA1 Message Date
dc8e348aa3
fix: Set explicit littlefs version tag
All checks were successful
BTClock CI / build (push) Successful in 23m39s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 23s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 1m1s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 20s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 1m2s
BTClock CI / release (push) Successful in 14s
2025-02-19 15:38:54 +01:00
e4ac3c5c94
feat: switch to replaceable events for nostr source
Some checks failed
BTClock CI / build (push) Failing after 5m31s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Has been skipped
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Has been skipped
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Has been skipped
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Has been skipped
BTClock CI / release (push) Has been skipped
2025-02-19 15:15:53 +01:00
0b1a362b53
chore: dependency updates 2025-02-19 14:12:16 +01:00
3265eec308
chore: update dependencies and make eventsource use static jsondocument 2025-01-20 12:05:48 +01:00
678a4ba099
fix: Set WiFi country to NL for scanning 2025-01-19 22:32:04 +01:00
9ea0210864
fix: set better defaults for frontlight enabled devices 2025-01-16 00:30:40 +01:00
b01003f075
fix: Never write to LED0
All checks were successful
BTClock CI / build (push) Successful in 24m32s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 37s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 22s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 36s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 21s
BTClock CI / release (push) Successful in 11s
2025-01-15 22:09:05 +01:00
1083a3222b
Add local public pool support
All checks were successful
BTClock CI / build (push) Successful in 22m59s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 34s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 22s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 32s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 19s
BTClock CI / release (push) Successful in 11s
2025-01-08 02:14:33 +01:00
963f3b10b7
Update WebUI
All checks were successful
BTClock CI / build (push) Successful in 21m25s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 33s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 21s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 31s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 19s
BTClock CI / release (push) Successful in 10s
2025-01-06 01:30:46 +01:00
bf64b2f64f
Merge root certificates 2025-01-06 01:27:13 +01:00
1d61453563
Revert to esp websocket client because websocketsClient does not work 2025-01-06 01:13:09 +01:00
e330984ba2
Refactor BlockNotify to a class, use websocketsClient 2025-01-06 00:43:31 +01:00
ebbec75e6b
Fix V2 message parsing 2025-01-06 00:01:34 +01:00
e19cad05bc
Update ESPAsyncWebserver 2025-01-05 23:15:34 +01:00
7195b7d343
Rewrite price notify to websocketsclient 2025-01-05 23:13:25 +01:00
0999dd08ad
Remove deprecated ArduinoJson methods 2025-01-05 23:13:05 +01:00
178748b94d
WebUI update 2025-01-05 22:47:13 +01:00
b435552c92
fix: verify block update 2025-01-05 22:12:39 +01:00
a6a8b5a071
Refactor EPD code to EPDManager class 2025-01-05 22:11:53 +01:00
d023643090
Refactor LedHandler to a class 2025-01-05 21:19:28 +01:00
ac13098824
Refactor mining pool stats fetch to a class 2025-01-05 20:24:13 +01:00
c91428dd5f
Refactor BitAxeFetch to a class 2025-01-05 20:14:55 +01:00
fa15e46d34
Add compressed sat symbol font file 2025-01-05 20:07:56 +01:00
cd4dea9d34
Add explicit LED frequency, compress sats symbol font 2025-01-05 18:50:19 +01:00
b4864b1db6
Improve bitaxe handling code 2025-01-05 18:08:21 +01:00
b8428e1650 Add LED effect when retrying time synchronization 2024-12-31 11:51:34 +01:00
aea69d54b6 Add tag and HW revision to setup screen, change setup SSID same as hostname 2024-12-31 11:33:50 +01:00
2e1b15e688 Add Do Not Disturb feature
All checks were successful
BTClock CI / build (push) Successful in 21m1s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 31s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 21s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 28s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 19s
BTClock CI / release (push) Successful in 11s
2024-12-30 02:04:18 +01:00
13c8e67b4c More efficient handling of multiple fonts, restore data bugfix
All checks were successful
BTClock CI / build (push) Successful in 12m16s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 31s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 21s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 29s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 19s
BTClock CI / release (push) Successful in 39s
2024-12-30 00:53:50 +01:00
90d91ba216 Compress fonts and add Oswald font 2024-12-29 23:28:22 +01:00
73a20cf9a7 Screen handler bugfix for multi currency
All checks were successful
BTClock CI / build (push) Successful in 21m36s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 34s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 33s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 32s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 32s
BTClock CI / release (push) Successful in 12s
2024-12-29 17:11:34 +01:00
833d46fa5a Improve WebUI feedback 2024-12-29 01:31:33 +01:00
64e518bf58 Improve data source selection, clean up unused preferences
All checks were successful
BTClock CI / build (push) Successful in 27m2s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 21s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 36s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 20s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 30s
BTClock CI / release (push) Successful in 12s
2024-12-28 18:19:31 +01:00
bc3e5afe51 Add debug log setting and custom endpoint settings 2024-12-28 16:29:34 +01:00
190d650887 Merge branch 'main' of https://git.btclock.dev/btclock/btclock_v3 2024-12-28 13:42:13 +01:00
b7ff9d8101 Better handling of unexpected pool stats responses, add CKPool 2024-12-28 13:40:08 +01:00
832d343db9 Merge pull request 'Configure Renovate' (#6) from renovate/configure into main
Reviewed-on: #6
2024-12-27 09:04:50 +00:00
Ticktock Depbot
4140b05a7d Add renovate.json 2024-12-27 08:28:10 +00:00
10fe5b5053 Fix function definition and rev.b definition
All checks were successful
BTClock CI / build (push) Successful in 20m44s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 30s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 21s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 28s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 19s
BTClock CI / release (push) Successful in 39s
2024-12-27 00:41:41 +01:00
cff6131fc4 Update WebUI and convert ButtonHandler to class
Some checks failed
BTClock CI / build (push) Failing after 19m28s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Has been skipped
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Has been skipped
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Has been skipped
BTClock CI / release (push) Has been skipped
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Has been skipped
2024-12-27 00:05:45 +01:00
17fef80253 Make ScreenHandler class, fix button handling 2024-12-26 23:08:46 +01:00
698c3a3a43 More code optimizations, remove unnecessary checks 2024-12-26 22:20:30 +01:00
66c662e1fd Use own nostrduino fork, improve nostr code, WebUI update 2024-12-26 21:43:50 +01:00
957a947bc5 More clean-up and bugfixes
All checks were successful
BTClock CI / build (push) Successful in 14m23s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 20s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 27s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 25s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 19s
BTClock CI / release (push) Successful in 36s
2024-12-21 17:53:35 +01:00
c44626cb42 Unclutter main.cpp 2024-12-21 17:46:10 +01:00
4fdd6b6b4f Fix button handling 2024-12-21 17:36:07 +01:00
8a818c66a0 Update readme
All checks were successful
BTClock CI / build (push) Successful in 10m20s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 20s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 27s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 25s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 19s
BTClock CI / release (push) Successful in 35s
2024-12-21 01:15:59 +01:00
9889e983ec Update WebUI 2024-12-21 00:51:26 +01:00
03dbb8add6 Small improvements for epd 2024-12-21 00:50:57 +01:00
753838b122 Switch to new ArduinoJson methods 2024-12-21 00:15:08 +01:00
46c0f3a22b Improve webserver functionality 2024-12-21 00:01:27 +01:00
fb70d435a9 Get mining pool logos by download 2024-12-20 23:02:54 +01:00
7bcb24bab0 Mining pool bugfixes, improvements and icon optimizations
All checks were successful
BTClock CI / build (push) Successful in 17m35s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 27s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 20s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 25s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 19s
BTClock CI / release (push) Successful in 36s
2024-12-20 14:11:26 +01:00
e8a7b221cb Update WebUI for new mining pools
All checks were successful
BTClock CI / build (push) Successful in 14m22s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 27s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 20s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 25s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 20s
BTClock CI / release (push) Successful in 34s
2024-12-20 04:07:46 +01:00
aeee5238b3 Improve mining pool interface, added GoBrrr pool, Public Pool and Satoshi Radio pool 2024-12-20 04:00:09 +01:00
f613c7e9a1 Update WebUI
All checks were successful
BTClock CI / build (push) Successful in 20m17s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 22s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 20s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 20s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 20s
BTClock CI / release (push) Successful in 16s
2024-12-20 01:33:10 +01:00
c7ea2f3e4d Merge pull request '[Feature] Adds mining pool screens' (#5) from kdmukai/btclock_v3:mining_stats_v2 into main
Reviewed-on: #5
2024-12-20 00:11:01 +00:00
814cd234a9 Big refactor of mining pool support, optimization of existing icons 2024-12-20 01:08:03 +01:00
f9aa593f0b Removed pool name consts; reduce Preferences hits 2024-12-19 13:54:28 -06:00
01ef6daf9f Update screen id consts 2024-12-19 13:53:16 -06:00
8f9307d1e4 Remove whitespace 2024-12-18 21:34:52 -06:00
e758659a4a Remove platformio edit 2024-12-18 21:31:05 -06:00
be224d1f91 Restoring .gitmodule link to main webui repo 2024-12-18 21:17:19 -06:00
72e5ee6580 README update 2024-12-18 21:17:19 -06:00
c3af0b4d36 Docs update; info on mining pool stats config 2024-12-18 21:17:19 -06:00
fabc6c1d28 bugfix for long preferences key 2024-12-18 21:17:19 -06:00
e175b5f2f5 syncing to my forked webui submodule 2024-12-18 21:17:19 -06:00
2bc5984f6f Add mining pool daily/expected earnings 2024-12-18 21:17:19 -06:00
1bd465b33a rebuilt with larger partitions 2024-12-18 21:17:19 -06:00
ae2e6656df Fix LittleFS sha256 generation 2024-12-18 22:52:50 +01:00
c989169ff4 Fix workflow and auto updater webui filenames 2024-12-18 22:20:40 +01:00
1a4bc9b711 Merge branch 'main' of https://git.btclock.dev/btclock/btclock_v3
Some checks failed
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Has been cancelled
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Has been cancelled
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Has been cancelled
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Has been cancelled
BTClock CI / release (push) Has been cancelled
BTClock CI / build (push) Has been cancelled
2024-12-18 21:40:53 +01:00
0dcde59fb4 Fix workflow 2024-12-18 21:40:18 +01:00
de8fe2e26e Fix preaction script 2024-12-18 20:41:42 +01:00
af4c466659 Switch to leaner MCP23017 library, create new aligned partition tables 2024-12-18 19:47:03 +01:00
83d293c58e Add bitaxe logo to WebUI
All checks were successful
BTClock CI / build (push) Successful in 18m16s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 26s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 21s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 24s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 19s
BTClock CI / release (push) Successful in 10s
2024-12-18 01:32:11 +01:00
da25c7de90 Merge remote-tracking branch 'keith/bitaxe_logo' 2024-12-18 01:27:05 +01:00
4a52fc0bf2 Add vertical screen description option 2024-12-18 00:50:20 +01:00
db0ec01c86 Add bitaxe logo 2024-12-17 08:53:00 -06:00
34b77ea105 Update WebUI 2024-12-12 23:12:59 +01:00
dbf2c53083 Adapted tests for Mow Units
All checks were successful
BTClock CI / build (push) Successful in 25m14s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 20s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 32s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 19s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 29s
BTClock CI / release (push) Successful in 29s
2024-12-10 15:18:04 +01:00
2a116d97ed Add frontlight off when dark setting
Some checks failed
BTClock CI / build (push) Failing after 1m39s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Has been skipped
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Has been skipped
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Has been skipped
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Has been skipped
BTClock CI / release (push) Has been skipped
2024-12-10 15:13:17 +01:00
3b6f1db3c5 Merge pull request 'Mow Units: No rounding' (#3) from kdmukai/btclock_v3:mow_units_no_rounding into main
Reviewed-on: #3
2024-12-10 13:26:23 +00:00
9ada991ab1 Update utils.cpp 2024-12-09 16:05:55 -06:00
132aa835cd Mow Units no rounding! 2024-12-09 15:58:39 -06:00
d6604d28d6 Update some initial values
All checks were successful
BTClock CI / build (push) Successful in 27m16s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 20s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 30s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 20s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 28s
BTClock CI / release (push) Successful in 26s
2024-12-05 18:28:46 +01:00
33c06c86a1 Prepare sats per dollar for 1B amounts 2024-12-05 18:22:59 +01:00
f0f591a16f Make better use of the screens in compact suffix mode 2024-12-05 05:21:14 +01:00
41b5fcf1c1 Bugfix for suffix compact mode
All checks were successful
BTClock CI / build (push) Successful in 22m18s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 20s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 30s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 19s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 27s
BTClock CI / release (push) Successful in 25s
2024-12-05 04:32:32 +01:00
981895d315 Convert partition table to hex, update WebUI 2024-11-29 01:22:07 +01:00
031b506fed Fix partition tables for bigger flash sizes 2024-11-28 19:21:15 +01:00
4cda081d05 Add suffix compact mode, added extra zap notify settings, WebUI cleanup
All checks were successful
BTClock CI / build (push) Successful in 15m59s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 21s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 28s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 20s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 25s
BTClock CI / release (push) Successful in 11s
2024-11-28 18:22:07 +01:00
2951055f68 Add requirements.txt for CI
All checks were successful
BTClock CI / build (push) Successful in 20m55s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 21s
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 28s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 20s
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 26s
BTClock CI / release (push) Successful in 12s
2024-11-27 11:41:58 +01:00
d37307cccf Add Mow mode notation and setting
Some checks failed
BTClock CI / build (push) Failing after 30s
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Has been skipped
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Has been skipped
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Has been skipped
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Has been skipped
BTClock CI / release (push) Has been skipped
2024-11-27 11:33:12 +01:00
239297c26d Add own badges to README 2024-11-26 21:48:28 +01:00
3b47c81cfe Webserver dependency update and testing new CI flow
All checks were successful
/ build (push) Successful in 15m45s
/ merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Successful in 28s
/ merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Successful in 25s
/ merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Successful in 26s
/ merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Successful in 25s
/ release (push) Successful in 23s
2024-11-26 20:50:32 +01:00
9f3351f85b Add Forgejo CI action
Some checks failed
/ merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Has been cancelled
/ merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Has been cancelled
/ merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Has been cancelled
/ merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Has been cancelled
/ release (push) Has been cancelled
/ build (push) Has been cancelled
Remove report action

Fix release step

Add platformio and node_modules cache

Try with less targets

Disable more

Restore all variants

Remove verbose and feature branch run
2024-11-26 20:26:01 +01:00
1ccd5f18fb Update WebUI
Some checks failed
BTClock CI / build (push) Has been cancelled
BTClock CI / merge (map[name:btclock_rev_b version:esp32s3], 213epd) (push) Has been cancelled
BTClock CI / merge (map[name:btclock_v8 version:esp32s3], 213epd) (push) Has been cancelled
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 213epd) (push) Has been cancelled
BTClock CI / merge (map[name:lolin_s3_mini version:esp32s3], 29epd) (push) Has been cancelled
BTClock CI / release (push) Has been cancelled
2024-11-26 00:03:07 +01:00
82dd70a38d Use CA bundle instead of single certificates, make auto update URL configurable 2024-11-25 23:54:02 +01:00
a614bd15db Restore Nostr Zap subscription after loss of connection 2024-10-23 00:43:53 +02:00
b0ec0685a1 Bugfix for too short WiFi-setup passwords 2024-10-03 00:26:14 +02:00
90 changed files with 9869 additions and 11453 deletions

View file

@ -0,0 +1,190 @@
name: "BTClock CI"
on:
push:
tags:
- "*"
workflow_dispatch:
jobs:
build:
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:js-22.04
permissions:
contents: write
checks: write
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: yarn
cache-dependency-path: "**/yarn.lock"
- uses: actions/cache@v4
with:
path: |
~/.cache/pip
~/.platformio/.cache
~/data/node_modules
.pio
data/node_modules
key: ${{ runner.os }}-pio
- uses: actions/setup-python@v5
with:
python-version: "3.9"
cache: "pip"
- name: Get current date
id: dateAndTime
shell: bash
run: echo "dateAndTime=$(date +'%Y-%m-%d-%H:%M')" >> $GITHUB_OUTPUT
- name: Install PlatformIO Core
shell: bash
run: pip install --upgrade platformio
- name: Run unit tests
shell: bash
run: mkdir -p junit-reports && pio test -e native_test_only --junit-output-path junit-reports/
- name: Build BTClock firmware
shell: bash
run: pio run
- name: Build BTClock filesystem
shell: bash
run: pio run --target buildfs
- name: Copy bootloader to output folder
run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin .pio
- name: Upload artifacts
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
with:
include-hidden-files: true
retention-days: 1
name: prepared-outputs
path: .pio/**/*.bin
merge:
runs-on: docker
container:
image: ghcr.io/catthehacker/ubuntu:js-22.04
permissions:
contents: write
checks: write
needs: build
continue-on-error: true
strategy:
matrix:
chip:
- name: lolin_s3_mini
version: esp32s3
- name: btclock_rev_b
version: esp32s3
- name: btclock_v8
version: esp32s3
epd_variant: [213epd, 29epd]
exclude:
- chip: { name: btclock_rev_b, version: esp32s3 }
epd_variant: 29epd
- chip: { name: btclock_v8, version: esp32s3 }
epd_variant: 29epd
steps:
- uses: https://code.forgejo.org/forgejo/download-artifact@v4
with:
name: prepared-outputs
path: .pio
- name: Install esptools.py
run: pip install --upgrade esptool
- name: Create merged firmware binary
shell: bash
run: |
mkdir -p ${{ matrix.chip.name }}_${{ matrix.epd_variant }}
if [ "${{ matrix.chip.name }}" == "btclock_v8" ]; then
esptool.py --chip ${{ matrix.chip.version }} merge_bin \
-o ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/${{ matrix.chip.name }}_${{ matrix.epd_variant }}.bin \
--flash_mode dio \
--flash_freq 80m \
--flash_size 16MB \
0x0000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/bootloader.bin \
0x8000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/partitions.bin \
0xe000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/ota_data_initial.bin \
0x10000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/firmware.bin \
0xDF0000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/littlefs_16MB.bin
elif [ "${{ matrix.chip.name }}" == "btclock_rev_b" ]; then
esptool.py --chip ${{ matrix.chip.version }} merge_bin \
-o ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/${{ matrix.chip.name }}_${{ matrix.epd_variant }}.bin \
--flash_mode dio \
--flash_freq 80m \
--flash_size 8MB \
0x0000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/bootloader.bin \
0x8000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/partitions.bin \
0xe000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/ota_data_initial.bin \
0x10000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/firmware.bin \
0x6F0000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/littlefs_8MB.bin;
else
esptool.py --chip ${{ matrix.chip.version }} merge_bin \
-o ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/${{ matrix.chip.name }}_${{ matrix.epd_variant }}.bin \
--flash_mode dio \
0x0000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/bootloader.bin \
0x8000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/partitions.bin \
0xe000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/ota_data_initial.bin \
0x10000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/firmware.bin \
0x380000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/littlefs_4MB.bin
# Adjust the offset for littlefs or other files as needed for the original case
fi
- name: Create checksum for firmware
shell: bash
run: shasum -a 256 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/firmware.bin | awk '{print $1}' > ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/${{ matrix.chip.name }}_${{ matrix.epd_variant }}_firmware.bin.sha256
- name: Create checksum for merged binary
shell: bash
run: shasum -a 256 ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/${{ matrix.chip.name }}_${{ matrix.epd_variant }}.bin | awk '{print $1}' > ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/${{ matrix.chip.name }}_${{ matrix.epd_variant }}.bin.sha256
- name: Create checksum for littlefs partition
shell: bash
run: |
fs_file=$(find .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }} -name "littlefs*.bin")
echo $fs_file
fs_name=$(basename "$fs_file")
shasum -a 256 "$fs_file" | awk '{print $1}' > "${{ matrix.chip.name }}_${{ matrix.epd_variant }}/${fs_name}.sha256"
cat "${{ matrix.chip.name }}_${{ matrix.epd_variant }}/${fs_name}.sha256"
- name: Copy all artifacts to output folder
run: cp .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/*.bin .pio/boot_app0.bin ${{ matrix.chip.name }}_${{ matrix.epd_variant }}
- name: Create OTA binary file
run: mv ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/firmware.bin ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/${{ matrix.chip.name }}_${{ matrix.epd_variant }}_firmware.bin
- name: Upload artifacts
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
with:
name: build-${{ matrix.chip.name }}-${{ matrix.epd_variant }}
path: |
${{ matrix.chip.name }}_${{ matrix.epd_variant }}/*.bin
${{ matrix.chip.name }}_${{ matrix.epd_variant }}/*.sha256
release:
runs-on: docker
permissions:
contents: write
checks: write
needs: merge
steps:
- name: Download matrix outputs
uses: https://code.forgejo.org/forgejo/download-artifact@v4
with:
pattern: build-*
merge-multiple: false
path: temp
- name: Copy files
run: |
mkdir -p release
find temp -type f \( -name "*.bin" -o -name "*.sha256" \) -exec cp -f {} release/ \;
- name: Create release
uses: https://code.forgejo.org/actions/forgejo-release@v2.4.0
with:
url: "https://git.btclock.dev"
repo: "${{ github.repository }}"
direction: upload
tag: "${{ github.ref_name }}"
sha: "${{ github.sha }}"
release-dir: release
token: ${{ secrets.TOKEN }}
override: ${{ github.ref_type != 'tag' && github.ref_name != 'main' }}
prerelease: ${{ github.ref_type != 'tag' && github.ref_name != 'main' }}
release-notes-assistant: false

1
.gitignore vendored
View file

@ -11,3 +11,4 @@ data/node_modules
node_modules node_modules
.DS_Store .DS_Store
*.bin *.bin
ci/cache

2
.gitmodules vendored
View file

@ -1,3 +1,3 @@
[submodule "data"] [submodule "data"]
path = data path = data
url = https://github.com/btclock/webui.git url = https://git.btclock.dev/btclock/webui.git

View file

@ -1,6 +1,8 @@
# BTClock v3 # BTClock v3
[![BTClock CI](https://github.com/btclock/btclock_v3/actions/workflows/tagging.yml/badge.svg)](https://github.com/btclock/btclock_v3/actions/workflows/tagging.yml) [![Latest release](https://git.btclock.dev/btclock/btclock_v3/badges/release.svg)](https://git.btclock.dev/btclock/btclock_v3/releases/latest)
[![BTClock CI](https://git.btclock.dev/btclock/btclock_v3/badges/workflows/push.yaml/badge.svg)](https://git.btclock.dev/btclock/btclock_v3/actions?workflow=push.yaml&actor=0&status=0)
Software for the BTClock project. Software for the BTClock project.
@ -14,15 +16,44 @@ Biggest differences with v2 are:
New features: New features:
- BitAxe integration - BitAxe integration
- Zap notifier - Nostr Zap notifier
- - Multiple mining pool stats integrations
"Steal focus on new block" means that when a new block is mined, the display will switch to the block height screen if it's not on it already. "Steal focus on new block" means that when a new block is mined, the display will switch to the block height screen if it's not on it already.
Most [information](https://github.com/btclock/btclock_v2/wiki) about BTClock v2 is still valid for this version. See the [docs](https://git.btclock.dev/btclock/docs) repo for more information and building instructions.
**NOTE**: The software assumes that the hardware is run in a controlled private network. ~~The Web UI and the OTA update mechanism are not password protected and accessible to anyone in the network. Also, since the device only fetches numbers through WebSockets it will skip server certificate verification to save resources.~~ Since 3.2.0 the WebUI is password protectable and all certificates are verified. OTA update mechanism is not password-protected. **NOTE**: The software assumes that the hardware is run in a controlled private network. ~~The Web UI and the OTA update mechanism are not password protected and accessible to anyone in the network. Also, since the device only fetches numbers through WebSockets it will skip server certificate verification to save resources.~~ Since 3.2.0 the WebUI is password protectable and all certificates are verified. OTA update mechanism is not password-protected.
## Building ## Building
Use PlatformIO to build it yourself. Make sure you fetch the [WebUI](https://github.com/btclock/webui) submodule. Use PlatformIO to build it yourself. Make sure you fetch the [WebUI](https://git.btclock.dev/btclock/webui) submodule.
## Mining pool stats
Enable mining pool stats by accessing your btclock's web UI (point a web browser at the device's IP address).
Under Settings -> Extra Features: toggle Enable Mining Pool Stats.
New options will appear. Select your mining pool and enter your pool username (Ocean) or api key (Braiins).
The Mining Pool Earnings screen displays:
* Braiins: Today's mining reward thus far
* Ocean: Your estimated earnings if the pool were to find a block right now
For solo mining pools, there are no earning estimations. Your username is the onchain withdrawal address, without the worker name.
### Braiins Pool integration
Create an API key based on the steps [here](https://academy.braiins.com/en/braiins-pool/monitoring/#api-configuration).
The key's permissions should be:
* Web Access: no
* API Access: yes
* Access Permissions: Read-only
Copy the token that is created for the new key. Enter this as your "Mining Pool username or api key" in the btclock web UI.
### Ocean integration
Your "Mining Pool username" is just the onchain withdrawal address that you specify when pointing your miners at Ocean.

15
ci/Dockerfile Normal file
View file

@ -0,0 +1,15 @@
# Use the official Python 3.9 image as the base
FROM python:3.9-slim
# Set the working directory
WORKDIR /workspace
RUN apt-get update && apt-get install -y git
# Install PlatformIO
RUN pip install platformio
WORKDIR /usr/src
CMD ["platformio", "run"]

2
data

@ -1 +1 @@
Subproject commit 95aa9d67d17dde32dab3216810e7b4e4086feb03 Subproject commit 0116cd68cdfdf383823f74e0f9665a1700cf0500

View file

@ -4,6 +4,6 @@ dependencies:
source: source:
type: idf type: idf
version: 4.4.7 version: 4.4.7
manifest_hash: 841ba2a95f4b32d39636bf4fdf07c48a2052f71c9728a5b7f263083a3b430a4f manifest_hash: 1d4ef353a86901733b106a1897b186dbf9fc091a4981f0560ea2f6899b7a3d44
target: esp32s3 target: esp32s3
version: 1.0.0 version: 1.0.0

View file

@ -1,14 +1,20 @@
#include "bitaxe_handler.hpp" #include "bitaxe_handler.hpp"
std::array<std::string, NUM_SCREENS> parseBitaxeHashRate(std::string text) std::array<std::string, NUM_SCREENS> parseBitaxeHashRate(uint64_t hashrate)
{ {
std::array<std::string, NUM_SCREENS> ret; std::array<std::string, NUM_SCREENS> ret;
ret.fill(""); // Initialize all elements to empty strings ret.fill(""); // Initialize all elements to empty strings
std::size_t textLength = text.length(); // Convert hashrate to GH/s and round to nearest integer
double hashRateGH = static_cast<double>(hashrate) / std::pow(10, getHashrateMultiplier('G'));
std::string hashRateStr = std::to_string(static_cast<uint64_t>(std::round(hashRateGH)));
// Place the icons
ret[0] = "mdi:bitaxe";
ret[NUM_SCREENS - 1] = "GH/S";
// Calculate the position where the digits should start // Calculate the position where the digits should start
// Account for the position of the "mdi:pickaxe" and the "GH/S" label std::size_t textLength = hashRateStr.length();
std::size_t startIndex = NUM_SCREENS - 1 - textLength; std::size_t startIndex = NUM_SCREENS - 1 - textLength;
// Insert the "mdi:pickaxe" icon just before the digits // Insert the "mdi:pickaxe" icon just before the digits
@ -17,34 +23,64 @@ std::array<std::string, NUM_SCREENS> parseBitaxeHashRate(std::string text)
ret[startIndex - 1] = "mdi:pickaxe"; ret[startIndex - 1] = "mdi:pickaxe";
} }
// Place the digits // Place each digit
for (std::size_t i = 0; i < textLength; ++i) for (std::size_t i = 0; i < textLength; ++i)
{ {
ret[startIndex + i] = text.substr(i, 1); ret[startIndex + i] = std::string(1, hashRateStr[i]);
} }
ret[NUM_SCREENS - 1] = "GH/S";
ret[0] = "BIT/AXE";
return ret; return ret;
} }
std::array<std::string, NUM_SCREENS> parseBitaxeBestDiff(std::string text) std::array<std::string, NUM_SCREENS> parseBitaxeBestDiff(uint64_t difficulty)
{ {
std::array<std::string, NUM_SCREENS> ret; std::array<std::string, NUM_SCREENS> ret;
std::uint32_t firstIndex = 0; ret.fill("");
if (text.length() < NUM_SCREENS) // Add icons at the start
{ ret[0] = "mdi:bitaxe";
text.insert(text.begin(), NUM_SCREENS - text.length(), ' ');
ret[0] = "BIT/AXE";
ret[1] = "mdi:rocket"; ret[1] = "mdi:rocket";
firstIndex = 2;
if (difficulty == 0) {
ret[NUM_SCREENS - 1] = "0";
return ret;
} }
for (std::uint8_t i = firstIndex; i < NUM_SCREENS; i++) // Find the appropriate suffix and format the number
const std::pair<char, int> suffixes[] = {
{'Q', 15}, {'T', 12}, {'G', 9}, {'M', 6}, {'K', 3}
};
std::string text;
for (const auto& suffix : suffixes) {
if (difficulty >= std::pow(10, suffix.second)) {
double value = difficulty / std::pow(10, suffix.second);
char buffer[32];
snprintf(buffer, sizeof(buffer), "%.1f", value);
text = buffer;
// Remove trailing zeros and decimal point if not needed
if (text.find('.') != std::string::npos) {
text = text.substr(0, text.find_last_not_of('0') + 1);
if (text.back() == '.') {
text.pop_back();
}
}
text += suffix.first;
break;
}
}
if (text.empty()) {
text = std::to_string(difficulty);
}
// Calculate start position to right-align the text
std::size_t startIndex = NUM_SCREENS - text.length();
// Place the formatted difficulty string
for (std::size_t i = 0; i < text.length() && (startIndex + i) < NUM_SCREENS; ++i)
{ {
ret[i] = text[i]; ret[startIndex + i] = std::string(1, text[i]);
} }
return ret; return ret;

View file

@ -1,5 +1,7 @@
#include <array> #include <array>
#include <string> #include <string>
#include <cstdint>
#include "utils.hpp"
std::array<std::string, NUM_SCREENS> parseBitaxeHashRate(std::string text); std::array<std::string, NUM_SCREENS> parseBitaxeHashRate(uint64_t hashrate);
std::array<std::string, NUM_SCREENS> parseBitaxeBestDiff(std::string text); std::array<std::string, NUM_SCREENS> parseBitaxeBestDiff(uint64_t difficulty);

View file

@ -67,33 +67,72 @@ char getCurrencyChar(const std::string& input)
return CURRENCY_USD; // Assuming USD is the default for unknown inputs return CURRENCY_USD; // Assuming USD is the default for unknown inputs
} }
std::array<std::string, NUM_SCREENS> parsePriceData(std::uint32_t price, char currencySymbol, bool useSuffixFormat) std::array<std::string, NUM_SCREENS> parsePriceData(std::uint32_t price, char currencySymbol, bool useSuffixFormat, bool mowMode, bool shareDot)
{ {
std::array<std::string, NUM_SCREENS> ret; std::array<std::string, NUM_SCREENS> ret;
std::string priceString; std::string priceString;
if (std::to_string(price).length() >= NUM_SCREENS || useSuffixFormat) if (std::to_string(price).length() >= NUM_SCREENS || useSuffixFormat)
{ {
priceString = getCurrencySymbol(currencySymbol) + formatNumberWithSuffix(price, NUM_SCREENS - 2); int numScreens = shareDot || mowMode ? NUM_SCREENS - 1 : NUM_SCREENS - 2;
priceString = getCurrencySymbol(currencySymbol) + formatNumberWithSuffix(price, numScreens, mowMode);
} }
else else
{ {
priceString = getCurrencySymbol(currencySymbol) + std::to_string(price); priceString = getCurrencySymbol(currencySymbol) + std::to_string(price);
} }
std::uint32_t firstIndex = 0; std::uint32_t firstIndex = 0;
if (priceString.length() < (NUM_SCREENS)) if ((shareDot && priceString.length() <= (NUM_SCREENS)) || priceString.length() < (NUM_SCREENS))
{ {
priceString.insert(priceString.begin(), NUM_SCREENS - priceString.length(), ' '); priceString.insert(priceString.begin(), NUM_SCREENS - priceString.length(), ' ');
if (mowMode)
{
ret[0] = "MOW/UNITS";
}
else
{
ret[0] = "BTC/" + getCurrencyCode(currencySymbol); ret[0] = "BTC/" + getCurrencyCode(currencySymbol);
}
firstIndex = 1; firstIndex = 1;
} }
size_t dotPosition = priceString.find('.');
if (shareDot && dotPosition != std::string::npos && dotPosition > 0)
{
std::vector<std::string> tempArray;
if (dotPosition != std::string::npos && dotPosition > 0)
{
for (size_t i = 0; i < priceString.length(); ++i)
{
if (i == dotPosition - 1)
{
tempArray.push_back(std::string(1, priceString[i]) + ".");
++i; // Skip the dot in the next iteration
}
else
{
tempArray.push_back(std::string(1, priceString[i]));
}
}
// Copy from tempArray to ret
for (std::uint32_t i = firstIndex; i < NUM_SCREENS && i - firstIndex < tempArray.size(); ++i)
{
ret[i] = tempArray[i - firstIndex];
}
}
}
else
{
for (std::uint32_t i = firstIndex; i < NUM_SCREENS; i++) for (std::uint32_t i = firstIndex; i < NUM_SCREENS; i++)
{ {
ret[i] = priceString[i]; ret[i] = std::string(1, priceString[i]);
} }
}
return ret; return ret;
} }
@ -107,9 +146,26 @@ std::array<std::string, NUM_SCREENS> parseSatsPerCurrency(std::uint32_t price,ch
if (priceString.length() < (NUM_SCREENS)) if (priceString.length() < (NUM_SCREENS))
{ {
priceString.insert(priceString.begin(), NUM_SCREENS - priceString.length(), ' '); // Check if price is greater than 1 billion
if (price >= 100000000)
{
double satsPerCurrency = (1.0 / static_cast<double>(price)) * 1e8; // Calculate satoshis
std::ostringstream oss;
oss << std::fixed << std::setprecision(3) << satsPerCurrency; // Format with 3 decimal places
priceString = oss.str();
}
else
{
priceString = std::to_string(static_cast<int>(round(1.0 / static_cast<double>(price) * 1e8))); // Default formatting
}
if (currencySymbol != CURRENCY_USD) // Pad the string with spaces if necessary
if (priceString.length() < NUM_SCREENS)
{
priceString.insert(priceString.begin(), NUM_SCREENS - priceString.length(), ' ');
}
if (currencySymbol != CURRENCY_USD || price >= 100000000) // no time anymore when earlier than 1
ret[0] = "SATS/" + getCurrencyCode(currencySymbol); ret[0] = "SATS/" + getCurrencyCode(currencySymbol);
else else
ret[0] = "MSCW/TIME"; ret[0] = "MSCW/TIME";
@ -292,9 +348,9 @@ emscripten::val parseBlockHeightArray(std::uint32_t blockHeight)
return arrayToStringArray(parseBlockHeight(blockHeight)); return arrayToStringArray(parseBlockHeight(blockHeight));
} }
emscripten::val parsePriceDataArray(std::uint32_t price, const std::string &currencySymbol, bool useSuffixFormat = false) emscripten::val parsePriceDataArray(std::uint32_t price, const std::string &currencySymbol, bool useSuffixFormat = false, bool mowMode = false, bool shareDot = false)
{ {
return arrayToStringArray(parsePriceData(price, currencySymbol[0], useSuffixFormat)); return arrayToStringArray(parsePriceData(price, currencySymbol[0], useSuffixFormat, mowMode, shareDot));
} }
emscripten::val parseHalvingCountdownArray(std::uint32_t blockHeight, bool asBlocks) emscripten::val parseHalvingCountdownArray(std::uint32_t blockHeight, bool asBlocks)

View file

@ -2,6 +2,7 @@
#include <string> #include <string>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <vector>
#include "utils.hpp" #include "utils.hpp"
@ -19,7 +20,7 @@ const std::string CURRENCY_CODE_JPY = "JPY";
const std::string CURRENCY_CODE_AUD = "AUD"; const std::string CURRENCY_CODE_AUD = "AUD";
const std::string CURRENCY_CODE_CAD = "CAD"; const std::string CURRENCY_CODE_CAD = "CAD";
std::array<std::string, NUM_SCREENS> parsePriceData(std::uint32_t price, char currency, bool useSuffixFormat = false); std::array<std::string, NUM_SCREENS> parsePriceData(std::uint32_t price, char currency, bool useSuffixFormat = false, bool mowMode = false, bool shareDot = false);
std::array<std::string, NUM_SCREENS> parseSatsPerCurrency(std::uint32_t price, char currencySymbol, bool withSatsSymbol); std::array<std::string, NUM_SCREENS> parseSatsPerCurrency(std::uint32_t price, char currencySymbol, bool withSatsSymbol);
std::array<std::string, NUM_SCREENS> parseBlockHeight(std::uint32_t blockHeight); std::array<std::string, NUM_SCREENS> parseBlockHeight(std::uint32_t blockHeight);
std::array<std::string, NUM_SCREENS> parseHalvingCountdown(std::uint32_t blockHeight, bool asBlocks); std::array<std::string, NUM_SCREENS> parseHalvingCountdown(std::uint32_t blockHeight, bool asBlocks);

View file

@ -29,6 +29,11 @@ double getSupplyAtBlock(std::uint32_t blockNr)
} }
std::string formatNumberWithSuffix(std::uint64_t num, int numCharacters) std::string formatNumberWithSuffix(std::uint64_t num, int numCharacters)
{
return formatNumberWithSuffix(num, numCharacters, false);
}
std::string formatNumberWithSuffix(std::uint64_t num, int numCharacters, bool mowMode)
{ {
static char result[20]; // Adjust size as needed static char result[20]; // Adjust size as needed
const long long quadrillion = 1000000000000000LL; const long long quadrillion = 1000000000000000LL;
@ -56,30 +61,53 @@ std::string formatNumberWithSuffix(std::uint64_t num, int numCharacters)
numDouble /= billion; numDouble /= billion;
suffix = 'B'; suffix = 'B';
} }
else if (num >= million || numDigits > 6) else if (num >= million || numDigits > 6 || (mowMode && num >= thousand))
{ {
numDouble /= million; numDouble /= million;
suffix = 'M'; suffix = 'M';
} }
else if (num >= thousand || numDigits > 3) else if (!mowMode && (num >= thousand || numDigits > 3))
{ {
numDouble /= thousand; numDouble /= thousand;
suffix = 'K'; suffix = 'K';
} }
else else if (!mowMode)
{ {
snprintf(result, sizeof(result), "%llu", (unsigned long long)num); snprintf(result, sizeof(result), "%llu", (unsigned long long)num);
// sprintf(result, "%llu", (unsigned long long)num);
return result; return result;
} }
else // mowMode is true and num < 1000
{
numDouble /= million;
suffix = 'M';
}
// Add suffix // Add suffix
int len = snprintf(result, sizeof(result), "%.0f%c", numDouble, suffix); int len;
// If there's room, add decimal places // Mow Mode always uses string truncation to avoid rounding
std::string mowAsString = std::to_string(numDouble);
if (mowMode) {
// Default to one decimal place
len = snprintf(result, sizeof(result), "%s%c", mowAsString.substr(0, mowAsString.find(".") + 2).c_str(), suffix);
}
else
{
len = snprintf(result, sizeof(result), "%.0f%c", numDouble, suffix);
}
// If there's room, add more decimal places
if (len < numCharacters) if (len < numCharacters)
{ {
snprintf(result, sizeof(result), "%.*f%c", numCharacters - len - 1, numDouble, suffix); int restLen = mowMode ? numCharacters - len : numCharacters - len - 1;
if (mowMode) {
snprintf(result, sizeof(result), "%s%c", mowAsString.substr(0, mowAsString.find(".") + 2 + restLen).c_str(), suffix);
}
else
{
snprintf(result, sizeof(result), "%.*f%c", restLen, numDouble, suffix);
}
} }
return result; return result;
@ -136,3 +164,93 @@ int64_t getAmountInSatoshis(std::string bolt11) {
return satoshis; return satoshis;
} }
void parseHashrateString(const std::string& hashrate, std::string& label, std::string& output, unsigned int maxCharacters) {
// Handle empty string or "0" cases
if (hashrate.empty() || hashrate == "0") {
label = "H/S";
output = "0";
return;
}
size_t suffixLength = 0;
if (hashrate.length() > 21) {
label = "ZH/S";
suffixLength = 21;
} else if (hashrate.length() > 18) {
label = "EH/S";
suffixLength = 18;
} else if (hashrate.length() > 15) {
label = "PH/S";
suffixLength = 15;
} else if (hashrate.length() > 12) {
label = "TH/S";
suffixLength = 12;
} else if (hashrate.length() > 9) {
label = "GH/S";
suffixLength = 9;
} else if (hashrate.length() > 6) {
label = "MH/S";
suffixLength = 6;
} else if (hashrate.length() > 3) {
label = "KH/S";
suffixLength = 3;
} else {
label = "H/S";
suffixLength = 0;
}
double value = std::stod(hashrate) / std::pow(10, suffixLength);
// Calculate integer part length
int integerPartLength = std::to_string(static_cast<int>(value)).length();
// Calculate remaining space for decimals
int remainingSpace = maxCharacters - integerPartLength;
char buffer[32];
if (remainingSpace <= 0)
{
// No space for decimals, just round to integer
snprintf(buffer, sizeof(buffer), "%.0f", value);
}
else
{
// Space for decimal point and some decimals
snprintf(buffer, sizeof(buffer), "%.*f", remainingSpace - 1, value);
}
// Remove trailing zeros and decimal point if necessary
output = buffer;
if (output.find('.') != std::string::npos)
{
output = output.substr(0, output.find_last_not_of('0') + 1);
if (output.back() == '.')
{
output.pop_back();
}
}
}
int getHashrateMultiplier(char unit) {
if (unit == '0')
return 0;
static const std::unordered_map<char, int> multipliers = {
{'Z', 21}, {'E', 18}, {'P', 15}, {'T', 12},
{'G', 9}, {'M', 6}, {'K', 3}
};
return multipliers.at(unit);
}
int getDifficultyMultiplier(char unit) {
if (unit == '0')
return 0;
static const std::unordered_map<char, int> multipliers = {
{'Q', 15}, {'T', 12}, {'B', 9}, {'M', 6}, {'K', 3}, {'G', 9},
{'q', 15}, {'t', 12}, {'b', 9}, {'m', 6}, {'k', 3}, {'g', 9}
};
return multipliers.at(unit);
}

View file

@ -5,10 +5,16 @@
#include <cstdint> #include <cstdint>
#include <sstream> #include <sstream>
#include <iomanip> #include <iomanip>
#include <unordered_map>
int modulo(int x,int N); int modulo(int x,int N);
double getSupplyAtBlock(std::uint32_t blockNr); double getSupplyAtBlock(std::uint32_t blockNr);
std::string formatNumberWithSuffix(std::uint64_t num, int numCharacters = 4); std::string formatNumberWithSuffix(std::uint64_t num, int numCharacters = 4);
std::string formatNumberWithSuffix(std::uint64_t num, int numCharacters, bool mowMode);
int64_t getAmountInSatoshis(std::string bolt11); int64_t getAmountInSatoshis(std::string bolt11);
void parseHashrateString(const std::string& hashrate, std::string& label, std::string& output, unsigned int maxCharacters);
int getHashrateMultiplier(char unit);
int getDifficultyMultiplier(char unit);

20
maintainers.yaml Normal file
View file

@ -0,0 +1,20 @@
identifier: BTClock
maintainers:
- npub1k5f85zx0xdskyayqpfpc0zq6n7vwqjuuxugkayk72fgynp34cs3qfcvqg2
relays:
- wss://relay.noderunners.network/
- wss://nostr.sathoarder.com/
- wss://offchain.pub/
- wss://nostr3.daedaluslabs.io/
- wss://nostr4.daedaluslabs.io/
- wss://nostr.dbtc.link/
- wss://purplepag.es/
- wss://nos.lol/
- wss://nostr1.daedaluslabs.io/
- wss://nostr.noderunners.network/
- wss://nostr.lnbitcoin.cz/
- wss://relay.primal.net/
- wss://relay.damus.io
- wss://nostr-relay.derekross.me/
- wss://nostr2.azzamo.net/
- wss://nostr2.daedaluslabs.io/

View file

@ -1,7 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags # Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 36K, 20K, nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 56K, 8K, otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 64K, 1700K, app0, app, ota_0, 0x10000, 0x1b8000,
app1, app, ota_1, , 1700K, app1, app, ota_1, , 0x1b8000,
spiffs, data, spiffs, , 400K, spiffs, data, spiffs, , 0x66C00,
coredump, data, coredump,, 64K, coredump, data, coredump,, 0x10000,

1 # Name Type SubType Offset Size Flags
2 nvs data nvs 36K 0x9000 20K 0x5000
3 otadata data ota 56K 0xe000 8K 0x2000
4 app0 app ota_0 64K 0x10000 1700K 0x1b8000
5 app1 app ota_1 1700K 0x1b8000
6 spiffs data spiffs 400K 0x66C00
7 coredump data coredump 64K 0x10000

View file

@ -1,7 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags # Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 36K, 20K, nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 56K, 8K, otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 64K, 4096K, app0, app, ota_0, 0x10000, 0x6F0000,
app1, app, ota_1, , 4096K, app1, app, ota_1, , 0x6F0000,
spiffs, data, spiffs, , 400K, spiffs, data, spiffs, , 0x200000,
coredump, data, coredump,, 64K, coredump, data, coredump,, 0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 36K 0x9000 20K 0x5000
3 otadata data ota 56K 0xe000 8K 0x2000
4 app0 app ota_0 64K 0x10000 4096K 0x6F0000
5 app1 app ota_1 4096K 0x6F0000
6 spiffs data spiffs 400K 0x200000
7 coredump data coredump 64K 0x10000

View file

@ -1,7 +1,7 @@
# Name, Type, SubType, Offset, Size, Flags # Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 36K, 20K, nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 56K, 8K, otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 64K, 1700K, app0, app, ota_0, 0x10000, 0x370000,
app1, app, ota_1, , 1700K, app1, app, ota_1, , 0x370000,
spiffs, data, spiffs, , 400K, spiffs, data, spiffs, , 0xCD000,
coredump, data, coredump,, 64K, coredump, data, coredump,, 0x10000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 36K 0x9000 20K 0x5000
3 otadata data ota 56K 0xe000 8K 0x2000
4 app0 app ota_0 64K 0x10000 1700K 0x370000
5 app1 app ota_1 1700K 0x370000
6 spiffs data spiffs 400K 0xCD000
7 coredump data coredump 64K 0x10000

View file

@ -7,40 +7,44 @@
; ;
; Please visit documentation for the other options and examples ; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html ; https://docs.platformio.org/page/projectconf.html
[platformio] [platformio]
data_dir = data/build_gz data_dir = data/build_gz
default_envs = lolin_s3_mini_213epd, lolin_s3_mini_29epd, btclock_rev_b_213epd, btclock_v8_213epd default_envs = lolin_s3_mini_213epd, lolin_s3_mini_29epd, btclock_rev_b_213epd, btclock_v8_213epd
[env] [env]
[btclock_base] [btclock_base]
platform = espressif32 @ ^6.8.1 platform = espressif32 @ ^6.10.0
framework = arduino, espidf framework = arduino, espidf
monitor_speed = 115200 monitor_speed = 115200
monitor_filters = esp32_exception_decoder, colorize monitor_filters = esp32_exception_decoder, colorize
board_build.filesystem = littlefs board_build.filesystem = littlefs
extra_scripts = post:scripts/extra_script.py extra_scripts = pre:scripts/pre_script.py, post:scripts/extra_script.py
platform_packages =
earlephilhower/tool-mklittlefs-rp2040-earlephilhower
board_build.embed_files =
x509_crt_bundle
build_flags = build_flags =
!python scripts/git_rev.py !python scripts/git_rev.py
-DLAST_BUILD_TIME=$UNIX_TIME -DLAST_BUILD_TIME=$UNIX_TIME
-DARDUINO_USB_CDC_ON_BOOT -DARDUINO_USB_CDC_ON_BOOT
-DCORE_DEBUG_LEVEL=0 -DCORE_DEBUG_LEVEL=0
-D CONFIG_ASYNC_TCP_STACK_SIZE=16384
-fexceptions -fexceptions
build_unflags = build_unflags =
-Werror=all -Werror=all
-fno-exceptions -fno-exceptions
lib_deps = lib_deps =
https://github.com/joltwallet/esp_littlefs.git https://github.com/joltwallet/esp_littlefs.git#v1.16.4
bblanchon/ArduinoJson@^7.1.0 bblanchon/ArduinoJson@^7.3.0
mathieucarbou/ESPAsyncWebServer @ 3.3.1 esp32async/ESPAsyncWebServer @ 3.7.0
adafruit/Adafruit BusIO@^1.16.1 robtillaart/MCP23017@^0.9.0
adafruit/Adafruit MCP23017 Arduino Library@^2.3.2 adafruit/Adafruit NeoPixel@^1.12.4
adafruit/Adafruit NeoPixel@^1.12.3 https://github.com/dsbaars/universal_pin#feature/mcp23017_rt
https://github.com/dsbaars/universal_pin
https://github.com/dsbaars/GxEPD2#universal_pin https://github.com/dsbaars/GxEPD2#universal_pin
https://github.com/tzapu/WiFiManager.git#v2.0.17 https://github.com/tzapu/WiFiManager.git#v2.0.17
rblb/Nostrduino@1.2.8 https://github.com/dsbaars/nostrduino#feature/fix-btclock
[env:lolin_s3_mini] [env:lolin_s3_mini]
extends = btclock_base extends = btclock_base
@ -58,12 +62,14 @@ build_flags =
-D IS_HW_REV_A -D IS_HW_REV_A
build_unflags = build_unflags =
${btclock_base.build_unflags} ${btclock_base.build_unflags}
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
[env:btclock_rev_b] [env:btclock_rev_b]
extends = btclock_base extends = btclock_base
board = btclock_rev_b board = btclock_rev_b
board_build.partitions = partition.csv board_build.partitions = partition_8mb.csv
build_flags = build_flags =
${btclock_base.build_flags} ${btclock_base.build_flags}
-D MCP_INT_PIN=8 -D MCP_INT_PIN=8
@ -73,15 +79,19 @@ build_flags =
-D I2C_SDA_PIN=35 -D I2C_SDA_PIN=35
-D I2C_SCK_PIN=36 -D I2C_SCK_PIN=36
-D HAS_FRONTLIGHT -D HAS_FRONTLIGHT
-D PCA_OE_PIN=45 -D PCA_OE_PIN=48
-D PCA_I2C_ADDR=0x42 -D PCA_I2C_ADDR=0x42
-D IS_HW_REV_B -D IS_HW_REV_B
lib_deps = lib_deps =
${btclock_base.lib_deps} ${btclock_base.lib_deps}
robtillaart/PCA9685@^0.7.1 robtillaart/PCA9685@^0.7.1
claws/BH1750@^1.3.0 claws/BH1750@^1.3.0
build_unflags = build_unflags =
${btclock_base.build_unflags} ${btclock_base.build_unflags}
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
[env:lolin_s3_mini_213epd] [env:lolin_s3_mini_213epd]
extends = env:lolin_s3_mini extends = env:lolin_s3_mini
@ -91,7 +101,10 @@ build_flags =
-D USE_QR -D USE_QR
-D VERSION_EPD_2_13 -D VERSION_EPD_2_13
-D HW_REV=\"REV_A_EPD_2_13\" -D HW_REV=\"REV_A_EPD_2_13\"
-D CONFIG_ARDUINO_MAIN_TASK_STACK_SIZE=16384
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
[env:btclock_rev_b_213epd] [env:btclock_rev_b_213epd]
extends = env:btclock_rev_b extends = env:btclock_rev_b
@ -101,6 +114,10 @@ build_flags =
-D USE_QR -D USE_QR
-D VERSION_EPD_2_13 -D VERSION_EPD_2_13
-D HW_REV=\"REV_B_EPD_2_13\" -D HW_REV=\"REV_B_EPD_2_13\"
-D CONFIG_ARDUINO_MAIN_TASK_STACK_SIZE=16384
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
[env:lolin_s3_mini_29epd] [env:lolin_s3_mini_29epd]
extends = env:lolin_s3_mini extends = env:lolin_s3_mini
@ -110,6 +127,9 @@ build_flags =
-D USE_QR -D USE_QR
-D VERSION_EPD_2_9 -D VERSION_EPD_2_9
-D HW_REV=\"REV_A_EPD_2_9\" -D HW_REV=\"REV_A_EPD_2_9\"
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
[env:btclock_rev_b_29epd] [env:btclock_rev_b_29epd]
extends = env:btclock_rev_b extends = env:btclock_rev_b
@ -119,6 +139,9 @@ build_flags =
-D USE_QR -D USE_QR
-D VERSION_EPD_2_9 -D VERSION_EPD_2_9
-D HW_REV=\"REV_B_EPD_2_9\" -D HW_REV=\"REV_B_EPD_2_9\"
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
[env:btclock_v8] [env:btclock_v8]
extends = btclock_base extends = btclock_base
@ -145,6 +168,9 @@ build_flags =
-D MCP2_A2_PIN=14 -D MCP2_A2_PIN=14
build_unflags = build_unflags =
${btclock_base.build_unflags} ${btclock_base.build_unflags}
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
[env:btclock_v8_213epd] [env:btclock_v8_213epd]
extends = env:btclock_v8 extends = env:btclock_v8
@ -154,6 +180,9 @@ build_flags =
-D USE_QR -D USE_QR
-D VERSION_EPD_2_13 -D VERSION_EPD_2_13
-D HW_REV=\"REV_V8_EPD_2_13\" -D HW_REV=\"REV_V8_EPD_2_13\"
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
[env:native_test_only] [env:native_test_only]
platform = native platform = native
@ -164,3 +193,8 @@ build_flags =
-D NEOPIXEL_PIN=34 -D NEOPIXEL_PIN=34
-D NEOPIXEL_COUNT=4 -D NEOPIXEL_COUNT=4
-D NUM_SCREENS=7 -D NUM_SCREENS=7
-D UNITY_TEST
-std=gnu++17
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216

6
renovate.json Normal file
View file

@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}

1
requirements.txt Normal file
View file

@ -0,0 +1 @@
platformio

View file

@ -1,10 +1,13 @@
Import("env") Import("env")
import os import os
import gzip import gzip
from shutil import copyfileobj, rmtree from shutil import copyfileobj, rmtree, copyfile, copytree
from pathlib import Path from pathlib import Path
import subprocess import subprocess
revision = ( revision = (
subprocess.check_output(["git", "rev-parse", "HEAD"]) subprocess.check_output(["git", "rev-parse", "HEAD"])
.strip() .strip()
@ -26,7 +29,7 @@ def process_directory(input_dir, output_dir):
Path(output_root).mkdir(parents=True, exist_ok=True) Path(output_root).mkdir(parents=True, exist_ok=True)
for file in files: for file in files:
# if file.endswith(('.html', '.css', '.js')): # if not file.endswith(('.bin')):
input_file_path = os.path.join(root, file) input_file_path = os.path.join(root, file)
output_file_path = os.path.join(output_root, file + '.gz') output_file_path = os.path.join(output_root, file + '.gz')
gzip_file(input_file_path, output_file_path) gzip_file(input_file_path, output_file_path)
@ -38,10 +41,85 @@ def process_directory(input_dir, output_dir):
# Build web interface before building FS # Build web interface before building FS
def before_buildfs(source, target, env): def before_buildfs(source, target, env):
env.Execute("cd data && yarn && yarn postinstall && yarn build") env.Execute("cd data && yarn && yarn postinstall && yarn build")
input_directory = 'data/dist' input_directory = 'data/dist'
output_directory = 'data/build_gz' output_directory = 'data/build_gz'
# copytree("assets", "data/dist/assets")
process_directory(input_directory, output_directory) process_directory(input_directory, output_directory)
def get_fs_partition_size(env):
import csv
# Get partition table path - first try custom, then default
board_config = env.BoardConfig()
partition_table = board_config.get("build.partitions", "default.csv")
# Handle default partition table path
if partition_table == "default.csv" or partition_table == "huge_app.csv":
partition_table = os.path.join(env.PioPlatform().get_package_dir("framework-arduinoespressif32"),
"tools", "partitions", partition_table)
# Parse CSV to find spiffs/littlefs partition
with open(partition_table, 'r') as f:
for row in csv.reader(f):
if len(row) < 5:
continue
# Remove comments and whitespace
row = [cell.strip().split('#')[0] for cell in row]
# Check if this is a spiffs or littlefs partition
if row[0].startswith(('spiffs', 'littlefs')):
# Size is in hex format
return int(row[4], 16)
return 0
def get_littlefs_used_size(binary_path):
mklittlefs_path = os.path.join(env.PioPlatform().get_package_dir("tool-mklittlefs-rp2040-earlephilhower"), "mklittlefs")
try:
result = subprocess.run([mklittlefs_path, '-l', binary_path], capture_output=True, text=True)
if result.returncode == 0:
# Parse the output to sum up file sizes
total_size = 0
for line in result.stdout.splitlines():
if line.strip() and not line.startswith('<dir>') and not line.startswith('Creation'):
# Each line format: size filename
size = line.split()[0]
total_size += int(size)
return total_size
except Exception as e:
print(f"Error getting filesystem size: {e}")
return 0
def after_littlefs(source, target, env):
binary_path = str(target[0])
partition_size = get_fs_partition_size(env)
used_size = get_littlefs_used_size(binary_path)
percentage = (used_size / partition_size) * 100
bar_width = 50
filled = int(bar_width * percentage / 100)
bar = '=' * filled + '-' * (bar_width - filled)
print(f"\nLittleFS Actual Usage: [{bar}] {percentage:.1f}% ({used_size}/{partition_size} bytes)")
flash_size = env.BoardConfig().get("upload.flash_size", "4MB")
fs_image_name = f"littlefs_{flash_size}"
env.Replace(ESP32_FS_IMAGE_NAME=fs_image_name)
env.Replace(ESP8266_FS_IMAGE_NAME=fs_image_name)
os.environ["PUBLIC_BASE_URL"] = "" os.environ["PUBLIC_BASE_URL"] = ""
env.AddPreAction("$BUILD_DIR/littlefs.bin", before_buildfs) fs_name = env.get("ESP32_FS_IMAGE_NAME", "littlefs.bin")
# Or alternatively:
# fs_name = env.get("FSTOOLNAME", "littlefs.bin")
# Use the variable in the pre-action
env.AddPreAction(f"$BUILD_DIR/{fs_name}.bin", before_buildfs)
env.AddPostAction(f"$BUILD_DIR/{fs_name}.bin", after_littlefs)
# LittleFS Actual Usage: [==============================--------------------] 60.4% (254165/420864 bytes)
# LittleFS Actual Usage: [==============================--------------------] 60.2% (253476/420864 bytes)
# 372736 used

7
scripts/pre_script.py Normal file
View file

@ -0,0 +1,7 @@
Import("env")
flash_size = env.BoardConfig().get("upload.flash_size", "4MB")
fs_image_name = f"littlefs_{flash_size}"
env.Replace(ESP32_FS_IMAGE_NAME=fs_image_name)
env.Replace(ESP8266_FS_IMAGE_NAME=fs_image_name)

View file

@ -1,384 +1,191 @@
#pragma once
#include <Adafruit_GFX.h> #include <Adafruit_GFX.h>
#include <Arduino.h> #include <Arduino.h>
#include "fonts.hpp"
const uint8_t Antonio_SemiBold20pt7bBitmaps[] PROGMEM = { const uint8_t Antonio_SemiBold20pt7bBitmaps_Gzip[] = {
0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x66, 0x66, 0x66, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xa5, 0x57,
0x66, 0x66, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xEF, 0x7B, 0xDE, 0xF7, 0x4f, 0x6f, 0xe3, 0xba, 0x11, 0xa7, 0xaa, 0xe2, 0xf1, 0x1d, 0x16, 0xe6,
0x98, 0xC6, 0x00, 0x03, 0x8F, 0x01, 0xC7, 0x80, 0xE3, 0x80, 0xF1, 0xC0, 0xf5, 0x1d, 0xbc, 0x62, 0x3f, 0xc2, 0xeb, 0xcd, 0x8b, 0x55, 0xac, 0xaf,
0x70, 0xE0, 0x38, 0x70, 0x1C, 0x78, 0x0E, 0x3C, 0x07, 0x1C, 0x07, 0x8E, 0xf2, 0x4e, 0x7b, 0x76, 0xb0, 0x40, 0x57, 0x46, 0x94, 0x48, 0x81, 0x81,
0x03, 0x87, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x38, 0xE0, 0x3C, 0xea, 0x52, 0xac, 0xaf, 0x5b, 0x60, 0x9b, 0x7c, 0x8d, 0x2d, 0x10, 0x6c,
0x70, 0x1E, 0x38, 0x0E, 0x1C, 0x07, 0x0E, 0x03, 0x8F, 0x0F, 0xFF, 0xF7, 0x64, 0x08, 0x78, 0xbe, 0x45, 0x5f, 0x60, 0x11, 0x53, 0x10, 0x50, 0x5f,
0xFF, 0xF8, 0xF1, 0xC0, 0x70, 0xE0, 0x38, 0x70, 0x1C, 0x78, 0x0E, 0x3C, 0x8a, 0x8a, 0x86, 0x80, 0x4a, 0x41, 0x14, 0xb1, 0x3f, 0xd2, 0x72, 0xfe,
0x0F, 0x1C, 0x07, 0x8E, 0x03, 0x87, 0x01, 0xC3, 0x80, 0xE3, 0xC0, 0x71, 0x6c, 0xb2, 0xd9, 0xd7, 0x96, 0xa4, 0x39, 0x43, 0x8a, 0x96, 0x86, 0x33,
0xE0, 0x78, 0xE0, 0x00, 0x06, 0x00, 0x30, 0x01, 0x80, 0x0C, 0x00, 0x60, 0xc3, 0xdf, 0x0c, 0x89, 0xea, 0xcb, 0x1b, 0x5d, 0x08, 0xd3, 0x6c, 0x79,
0x1F, 0xC1, 0xFF, 0x9F, 0xFC, 0xFB, 0xF7, 0x87, 0xBC, 0x3D, 0xE1, 0xEF, 0x70, 0x59, 0x7f, 0xf8, 0x95, 0xd8, 0x73, 0x6b, 0x19, 0x15, 0xd1, 0x26,
0x0F, 0x78, 0x7B, 0xE3, 0xDF, 0x00, 0x7C, 0x01, 0xF8, 0x0F, 0xE0, 0x3F, 0x99, 0x88, 0xd1, 0x64, 0xe8, 0x0f, 0x5c, 0x3a, 0xa4, 0xef, 0xed, 0x98,
0x80, 0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x3E, 0xF0, 0xF7, 0x87, 0xFC, 0x3F, 0x9b, 0xe5, 0xed, 0x48, 0xb8, 0x13, 0x67, 0x34, 0x18, 0xd2, 0x81, 0x3d,
0xE1, 0xFF, 0x0F, 0xFC, 0x7B, 0xFF, 0xCF, 0xFE, 0x3F, 0xE0, 0xFE, 0x01, 0x67, 0xaa, 0x56, 0xcd, 0xdd, 0x3a, 0x66, 0xd6, 0x59, 0x69, 0x54, 0x24,
0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0x1F, 0x80, 0x01, 0xC0, 0xfb, 0xc2, 0x17, 0xe4, 0x07, 0xf2, 0xb3, 0x15, 0xbd, 0x20, 0xbf, 0xf0,
0x07, 0xFE, 0x00, 0x3C, 0x00, 0x7F, 0xE0, 0x03, 0xC0, 0x0F, 0x9F, 0x00, 0x85, 0x3a, 0x6d, 0xaf, 0xeb, 0xf8, 0x6c, 0x2f, 0x2f, 0x99, 0x7f, 0x50,
0x38, 0x00, 0xF0, 0xF0, 0x07, 0x80, 0x0F, 0x0F, 0x00, 0x70, 0x00, 0xF0, 0xac, 0x48, 0x60, 0x35, 0x4c, 0x78, 0x51, 0x60, 0x49, 0x9a, 0x8c, 0x65,
0xF0, 0x0F, 0x00, 0x0F, 0x0F, 0x00, 0xF0, 0x00, 0xF0, 0xF0, 0x0E, 0x00, 0x1d, 0xb7, 0x5e, 0xae, 0x58, 0x7b, 0xa0, 0xb2, 0xce, 0x13, 0x9d, 0x95,
0x0F, 0x0F, 0x01, 0xE0, 0x00, 0xF0, 0xF0, 0x1C, 0x00, 0x0F, 0x0F, 0x03, 0x0c, 0xc8, 0xc4, 0x8e, 0x86, 0x84, 0x47, 0x56, 0x42, 0x3b, 0xe2, 0x92,
0xC0, 0x60, 0xF0, 0xF0, 0x3C, 0x3F, 0xCF, 0x0F, 0x03, 0x87, 0xFE, 0xF0, 0x50, 0xd8, 0x09, 0x3b, 0x25, 0x23, 0x22, 0x25, 0x8d, 0x18, 0x23, 0x13,
0xF0, 0x78, 0xFF, 0xEF, 0x0F, 0x07, 0x8F, 0x0E, 0xF0, 0xF0, 0xF0, 0xF0, 0x30, 0x8c, 0x80, 0x91, 0x60, 0x06, 0x60, 0x2c, 0x01, 0x66, 0x08, 0xc6,
0xFF, 0x0F, 0x0F, 0x0E, 0x0F, 0xF0, 0xF0, 0xE0, 0xE0, 0xF7, 0xDF, 0x1E, 0x4e, 0x7e, 0x91, 0xd2, 0xf5, 0x32, 0x66, 0xc7, 0x9d, 0x94, 0xbe, 0x2a,
0x0E, 0x0F, 0x7F, 0xE1, 0xE0, 0xE0, 0xF3, 0xFC, 0x1C, 0x0E, 0x0F, 0x1F, 0x19, 0x9d, 0x0f, 0x24, 0x8a, 0x62, 0x6c, 0xc0, 0xa4, 0x14, 0xa2, 0x5e,
0x83, 0xC0, 0xE0, 0xF0, 0x00, 0x38, 0x0E, 0x0F, 0x00, 0x07, 0x80, 0xE0, 0x39, 0x03, 0x16, 0xe6, 0x42, 0x54, 0xed, 0x70, 0xc0, 0xf8, 0x2c, 0x11,
0xF0, 0x00, 0x78, 0x0E, 0x0F, 0x00, 0x07, 0x00, 0xE0, 0xF0, 0x00, 0xF0, 0x92, 0x8c, 0x06, 0x8c, 0xd0, 0x08, 0x8c, 0xaf, 0x19, 0x02, 0x46, 0x82,
0x0E, 0x0F, 0x00, 0x0E, 0x00, 0xE0, 0xF0, 0x01, 0xE0, 0x0F, 0x0F, 0x00, 0x19, 0x80, 0xb1, 0x04, 0xbe, 0xe2, 0x90, 0x8d, 0xb0, 0x12, 0x66, 0x24,
0x1E, 0x00, 0xF1, 0xE0, 0x01, 0xC0, 0x0F, 0xFE, 0x00, 0x3C, 0x00, 0x7F, 0x49, 0xec, 0xc8, 0x6e, 0x6c, 0x61, 0x77, 0x44, 0x45, 0x81, 0x74, 0x86,
0xC0, 0x03, 0x80, 0x03, 0xF8, 0x03, 0xE0, 0x03, 0xFE, 0x00, 0xFF, 0x80, 0x34, 0xb6, 0xf2, 0xc4, 0x9f, 0x80, 0x99, 0x93, 0x2a, 0xda, 0x13, 0xcc,
0x7C, 0xF0, 0x1E, 0x1C, 0x07, 0x87, 0x01, 0xE1, 0xC0, 0x78, 0x70, 0x1E, 0xb7, 0x5a, 0x72, 0x04, 0xd1, 0x69, 0x62, 0x4b, 0xd2, 0xfd, 0x18, 0xfe,
0x1C, 0x07, 0x8F, 0x00, 0xF3, 0x80, 0x3D, 0xE0, 0x0F, 0x78, 0x01, 0xFC, 0xd9, 0x2b, 0xe6, 0x6e, 0x3d, 0x9f, 0xe6, 0xad, 0x1f, 0xba, 0x7c, 0x6e,
0x00, 0x7E, 0x00, 0x1F, 0x80, 0x07, 0xC0, 0x03, 0xF0, 0x00, 0xFE, 0x08, 0x17, 0x89, 0x94, 0x63, 0x87, 0xc7, 0x6a, 0xa3, 0x5a, 0x6f, 0x1d, 0x6f,
0x7F, 0x86, 0x3F, 0xE3, 0x8F, 0x3C, 0xF7, 0x8F, 0x79, 0xE1, 0xFC, 0x78, 0x84, 0x52, 0xf5, 0x9f, 0xde, 0xbc, 0x19, 0x2b, 0x95, 0x39, 0xae, 0xbf,
0x7F, 0x3C, 0x1F, 0x8F, 0x03, 0xE3, 0xC0, 0xF0, 0xF0, 0x3E, 0x1E, 0x1F, 0x29, 0x96, 0xf3, 0x27, 0xc8, 0x75, 0x9d, 0x27, 0x9b, 0x0a, 0xc3, 0xec,
0x87, 0xFF, 0xF1, 0xFF, 0xFC, 0x3F, 0xE7, 0x87, 0xF1, 0xE0, 0xFF, 0xFF, 0x74, 0x1c, 0x5c, 0x55, 0xeb, 0x47, 0xe4, 0xa0, 0x2e, 0x3f, 0x40, 0xf2,
0xF7, 0x76, 0x66, 0x66, 0x3E, 0xFF, 0xFF, 0xCF, 0x1E, 0x3C, 0x78, 0xF1, 0xe1, 0xef, 0xf7, 0x5f, 0x5d, 0x5c, 0x1f, 0xaa, 0x45, 0x6b, 0x0b, 0xaf,
0xE3, 0xC7, 0x8F, 0x1E, 0x3C, 0x78, 0xF1, 0xE3, 0xC7, 0x8F, 0x1E, 0x3C, 0x28, 0xd5, 0xdb, 0xbf, 0xfe, 0x4a, 0x7f, 0x1c, 0x42, 0x5f, 0x50, 0x15,
0x78, 0xF1, 0xE3, 0xC7, 0x8F, 0x1E, 0x3C, 0x78, 0xF1, 0xE3, 0xFB, 0xF7, 0x9a, 0x36, 0xa0, 0xd6, 0x5c, 0xcf, 0xff, 0x65, 0xfd, 0xc7, 0x8f, 0x09,
0xE1, 0xC0, 0xF1, 0xF3, 0xF1, 0xE3, 0xC7, 0xCF, 0x9F, 0x3E, 0x7C, 0xF9, 0x28, 0x44, 0x20, 0x0d, 0x8d, 0x5c, 0x4b, 0x0c, 0x88, 0xd4, 0x34, 0x71,
0xF3, 0xE7, 0xCF, 0x9F, 0x3E, 0x7C, 0xF9, 0xF3, 0xE7, 0xCF, 0x9F, 0x3E, 0x34, 0x1d, 0xd9, 0x5b, 0xea, 0x1b, 0xca, 0x88, 0xa6, 0xd0, 0x87, 0xa6,
0x7C, 0xF9, 0xF3, 0xE7, 0xCF, 0x9F, 0x3E, 0x7B, 0xF7, 0xEF, 0x98, 0x00, 0xae, 0xa1, 0x34, 0x22, 0x4c, 0x9b, 0x56, 0xd5, 0x73, 0x63, 0xc0, 0x26,
0x07, 0x00, 0x1C, 0x04, 0x71, 0x39, 0xCE, 0xFB, 0x7D, 0xFF, 0xC1, 0xFC, 0x4c, 0x3b, 0x2e, 0x55, 0xfc, 0xed, 0xc1, 0xd9, 0x61, 0x5d, 0x76, 0x9e,
0x03, 0xE0, 0x3F, 0xE3, 0xEF, 0xFF, 0x73, 0x99, 0xC6, 0x07, 0x08, 0x1C, 0x54, 0xc4, 0x9e, 0xa5, 0x85, 0x96, 0xe6, 0x94, 0xc5, 0x69, 0x2e, 0x9b,
0x00, 0x70, 0x00, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0x60, 0xcc, 0x9f, 0x62, 0x12, 0x76, 0xac, 0x78, 0xa3, 0xca, 0x79, 0x70,
0xFF, 0xF0, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0x90, 0x5e, 0x3a, 0xdb, 0x97, 0xc1, 0x83, 0x7c, 0x9a, 0xb8, 0xb6, 0x70,
0x8C, 0xE7, 0x31, 0x9C, 0xC0, 0xFF, 0xFF, 0xFF, 0xE0, 0xFF, 0xFF, 0x00, 0x2c, 0xc8, 0x04, 0xb1, 0x03, 0xb8, 0x90, 0x2d, 0xb8, 0x96, 0x34, 0x00,
0xF8, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0E, 0x00, 0xF0, 0x07, 0x80, 0x3C, 0xed, 0xe0, 0x93, 0x11, 0x4b, 0x54, 0xac, 0x78, 0xe7, 0x5f, 0xe7, 0x65,
0x01, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x38, 0x03, 0xC0, 0x1E, 0x00, 0x7c, 0xe6, 0xc8, 0x83, 0x85, 0xa0, 0x91, 0x03, 0xc1, 0x85, 0x67, 0x35,
0xF0, 0x07, 0x80, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x0F, 0x00, 0x78, 0x54, 0xf0, 0x84, 0x91, 0x31, 0xf1, 0x2d, 0x41, 0xf1, 0x2c, 0x5c, 0x28,
0x03, 0xC0, 0x1E, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x3C, 0x01, 0x4d, 0x0e, 0xf2, 0xf5, 0xea, 0xb4, 0xf5, 0x44, 0x48, 0xb4, 0x4f, 0x7a,
0xE0, 0x0F, 0x00, 0x78, 0x07, 0x80, 0x00, 0x0F, 0xC1, 0xFF, 0x9F, 0xFC, 0xa4, 0xb5, 0x25, 0x4f, 0x42, 0xab, 0xfd, 0x41, 0xbe, 0x4e, 0x4a, 0xfb,
0xFF, 0xF7, 0x8F, 0xFC, 0x3F, 0xE1, 0xFF, 0x0F, 0xF8, 0x7F, 0xC3, 0xFE, 0x6c, 0x28, 0xdf, 0x2e, 0x32, 0xe6, 0x8e, 0x24, 0x2c, 0x32, 0x74, 0x27,
0x1F, 0xF0, 0xFF, 0x87, 0xFC, 0x3F, 0xE1, 0xFF, 0x0F, 0xF8, 0x7F, 0xC3, 0x9b, 0x34, 0x63, 0x23, 0xb7, 0x3f, 0x41, 0x44, 0xc2, 0x3b, 0xe1, 0x1b,
0xFE, 0x1F, 0xF0, 0xFF, 0x87, 0xFC, 0x3F, 0xE1, 0xFF, 0x0F, 0xF8, 0x7F, 0x20, 0xaa, 0xec, 0xd0, 0x7a, 0x0d, 0x33, 0x52, 0xb1, 0x16, 0xc3, 0xab,
0xC3, 0xFE, 0x1F, 0xF0, 0xFF, 0x87, 0xBC, 0x7D, 0xF7, 0xEF, 0xFE, 0x3F, 0x92, 0xcb, 0xcd, 0x6e, 0x4a, 0x32, 0xb8, 0xa2, 0x69, 0xfc, 0xba, 0xec,
0xF0, 0xFF, 0x00, 0x03, 0x83, 0xC3, 0xE3, 0xFF, 0xFF, 0xFF, 0xFF, 0x9F, 0xc2, 0xb4, 0x81, 0xa8, 0x33, 0xc5, 0x5b, 0xff, 0x0a, 0xa2, 0x7e, 0x76,
0x0F, 0x87, 0xC3, 0xE1, 0xF0, 0xF8, 0x7C, 0x3E, 0x1F, 0x0F, 0x87, 0xC3, 0x9a, 0x83, 0xbc, 0x8c, 0xc6, 0xa4, 0xb1, 0xcb, 0xb9, 0xf2, 0x3a, 0x75,
0xE1, 0xF0, 0xF8, 0x7C, 0x3E, 0x1F, 0x0F, 0x87, 0xC3, 0xE1, 0xF0, 0xF8, 0xbd, 0x9d, 0x0c, 0x73, 0x15, 0x77, 0x7c, 0x47, 0xa6, 0xf9, 0x5a, 0x9d,
0x7C, 0x3E, 0x1F, 0x0F, 0x87, 0xC0, 0x0F, 0x81, 0xFF, 0x1F, 0xF8, 0xFF, 0x42, 0x8b, 0x61, 0xb4, 0x13, 0x20, 0x80, 0x39, 0x22, 0x08, 0x40, 0x13,
0xEF, 0x8F, 0x7C, 0x7B, 0xC3, 0xDE, 0x1E, 0xF0, 0xFF, 0x87, 0xFC, 0x3F, 0xb3, 0xc9, 0x27, 0x78, 0x4e, 0xf4, 0x59, 0x83, 0x0e, 0x1a, 0x3d, 0x85,
0xE1, 0xEF, 0x0F, 0x78, 0x78, 0x07, 0xC0, 0x3C, 0x03, 0xE0, 0x1E, 0x01, 0x0f, 0x10, 0x1c, 0x4a, 0xde, 0x5e, 0x2b, 0x58, 0x6e, 0xd1, 0x31, 0x19,
0xF0, 0x0F, 0x00, 0xF8, 0x07, 0x80, 0x7C, 0x07, 0xC0, 0x3E, 0x03, 0xE0, 0xce, 0x5a, 0x2e, 0x60, 0x9f, 0xa9, 0x5a, 0xb6, 0x5e, 0xa1, 0x4e, 0xf6,
0x1F, 0x00, 0xF0, 0x07, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0xFE, 0xFF, 0xF7, 0x36, 0x8a, 0x36, 0xf7, 0x9e, 0xd0, 0xd6, 0x33, 0x96, 0xeb, 0x08, 0x87,
0xFF, 0x80, 0x0F, 0xC0, 0xFF, 0x87, 0xFF, 0x1F, 0xFE, 0x78, 0xFB, 0xE1, 0x49, 0x6e, 0x60, 0x12, 0x1f, 0x26, 0xbd, 0x7d, 0xda, 0x2f, 0x5d, 0xae,
0xEF, 0x87, 0xBC, 0x1E, 0xF0, 0x7B, 0xC1, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x3a, 0x55, 0x29, 0xed, 0x48, 0xbb, 0x27, 0xac, 0x9d, 0xd6, 0xe6, 0x50,
0x78, 0x03, 0xE0, 0x3F, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0xC0, 0x0F, 0x00, 0x13, 0x78, 0x1e, 0x41, 0x31, 0x40, 0xa3, 0x22, 0xcd, 0x12, 0x20, 0x4a,
0x3E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0xBC, 0x1E, 0xF0, 0x7F, 0xC1, 0xFF, 0xf5, 0xf1, 0xe2, 0xed, 0x07, 0xf2, 0x87, 0x17, 0xf4, 0x58, 0x76, 0xde,
0x07, 0xBC, 0x1E, 0xF0, 0x7B, 0xE1, 0xE7, 0xDF, 0x9F, 0xFC, 0x3F, 0xE0, 0xe9, 0x52, 0x36, 0x43, 0x5b, 0x1c, 0x51, 0xe9, 0xd9, 0x0d, 0xb7, 0xc4,
0x7F, 0x00, 0x01, 0xF0, 0x07, 0xC0, 0x3F, 0x00, 0xFC, 0x03, 0xF0, 0x1F, 0x0b, 0xa2, 0x77, 0xd8, 0xf6, 0xff, 0x4a, 0xa2, 0x9f, 0xec, 0x28, 0x60,
0xC0, 0x7F, 0x01, 0xFC, 0x06, 0xF0, 0x3B, 0xC0, 0xEF, 0x03, 0xBC, 0x1C, 0x49, 0x47, 0x65, 0x68, 0x0b, 0x97, 0xcd, 0x36, 0x6d, 0xe7, 0xc5, 0x89,
0xF0, 0x73, 0xC1, 0xCF, 0x0F, 0x3C, 0x38, 0xF0, 0xE3, 0xC7, 0x8F, 0x1C, 0xfc, 0xe9, 0x77, 0x78, 0x53, 0xdc, 0x86, 0x65, 0xb7, 0x29, 0xf9, 0x46,
0x3C, 0x70, 0xF1, 0xC3, 0xCF, 0x0F, 0x38, 0x3C, 0xFF, 0xFF, 0xFF, 0xFF, 0xf5, 0xcd, 0x92, 0xf0, 0x55, 0xf8, 0x45, 0xeb, 0xcd, 0xa4, 0x0b, 0x87,
0xFF, 0xFF, 0xFF, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00, 0xF0, 0x45, 0x33, 0xdf, 0xec, 0x79, 0x3b, 0x21, 0x5e, 0x44, 0x20, 0x12, 0x5e,
0x03, 0xC0, 0xFF, 0xEF, 0xFE, 0xFF, 0xEF, 0xFE, 0xF0, 0x0F, 0x00, 0xF0, 0xdf, 0x30, 0x6d, 0x3a, 0x0d, 0x25, 0x64, 0xe4, 0x93, 0xe1, 0x88, 0xc7,
0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF3, 0x0F, 0xFC, 0xFF, 0xEF, 0xFE, 0xF9, 0xae, 0x57, 0x9c, 0x38, 0x9b, 0x8b, 0xe1, 0x68, 0x3d, 0x18, 0xbe, 0x7b,
0xEF, 0x1F, 0xF0, 0xF1, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0F, 0x00, 0x3f, 0xe0, 0x4b, 0xca, 0x8a, 0x59, 0xbc, 0x59, 0xa4, 0x8d, 0xc8, 0xdb,
0xF0, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x0F, 0xF0, 0xFF, 0x1F, 0xFB, 0xc9, 0xa4, 0x1b, 0x8d, 0xc2, 0xa1, 0x7e, 0xf0, 0x6a, 0x49, 0x87, 0xc5,
0xEF, 0xFE, 0x7F, 0xC3, 0xF8, 0x0F, 0xC0, 0xFF, 0x83, 0xFF, 0x1F, 0xFC, 0xf2, 0x62, 0xf2, 0xef, 0xce, 0x0d, 0x3d, 0x87, 0x33, 0x7a, 0x4c, 0xf0,
0x78, 0xF9, 0xE1, 0xEF, 0x87, 0xBE, 0x1E, 0xF8, 0x7B, 0xE1, 0xEF, 0x80, 0x62, 0xa0, 0x12, 0xbc, 0x01, 0x10, 0x30, 0xa1, 0xaa, 0x21, 0x0a, 0x2a,
0x3E, 0x00, 0xF8, 0x03, 0xEF, 0x8F, 0xFF, 0x3F, 0xFE, 0xFF, 0xFB, 0xE1, 0x93, 0x84, 0x26, 0x54, 0xb0, 0xbb, 0x2a, 0x07, 0xd2, 0xd9, 0xd5, 0x89,
0xEF, 0x87, 0xBE, 0x1E, 0xF8, 0x7F, 0xE1, 0xFF, 0x87, 0xFE, 0x1F, 0xF8, 0xe3, 0x0f, 0x7d, 0x77, 0x57, 0x51, 0x02, 0x90, 0xb0, 0x45, 0xed, 0xc2,
0x7F, 0xE1, 0xFF, 0x87, 0xFE, 0x1F, 0xF8, 0x79, 0xE1, 0xE7, 0xFF, 0x9F, 0xce, 0x77, 0x1a, 0x54, 0x2c, 0xe3, 0x92, 0x2b, 0x5b, 0xc1, 0x29, 0x5a,
0xFE, 0x3F, 0xF0, 0x7F, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x79, 0x9d, 0x96, 0xc6, 0x63, 0x17, 0x20, 0x63, 0x8c, 0x32, 0xe5, 0x35,
0x00, 0x7C, 0x01, 0xE0, 0x0F, 0x80, 0x3C, 0x00, 0xF0, 0x07, 0xC0, 0x1E, 0x50, 0x7b, 0xc6, 0xfa, 0xc9, 0x96, 0xdf, 0x3a, 0x75, 0x5a, 0xaa, 0xf3,
0x00, 0x78, 0x03, 0xE0, 0x0F, 0x80, 0x3C, 0x00, 0xF0, 0x07, 0xC0, 0x1E, 0x56, 0x15, 0x8a, 0xb0, 0x24, 0x4c, 0xe1, 0x34, 0xc1, 0x54, 0xbb, 0x10,
0x00, 0x78, 0x03, 0xE0, 0x0F, 0x80, 0x3C, 0x00, 0xF0, 0x07, 0xC0, 0x1F, 0x6b, 0x3c, 0xa1, 0x66, 0x86, 0x18, 0x5f, 0xe8, 0x5d, 0xe2, 0x76, 0x12,
0x00, 0x7C, 0x01, 0xF0, 0x07, 0x80, 0x1E, 0x00, 0xF8, 0x03, 0xE0, 0x0F, 0x84, 0xfb, 0x87, 0xf9, 0xba, 0x9c, 0xc3, 0xe2, 0x5e, 0xa4, 0xa8, 0xf2,
0x80, 0x3E, 0x00, 0xF8, 0x00, 0x1F, 0xC1, 0xFF, 0x1F, 0xFC, 0xFB, 0xFF, 0x60, 0x71, 0x1c, 0xe2, 0xc5, 0x23, 0x8b, 0x3f, 0x35, 0x08, 0x15, 0xdc,
0x8F, 0xFC, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0x60, 0xdd, 0x12, 0x63, 0x69, 0xe9, 0x02, 0x93, 0x6f, 0x3b, 0x33, 0x95,
0xFF, 0x87, 0xBC, 0x79, 0xFF, 0xC7, 0xFC, 0x3F, 0xE3, 0xFF, 0x9E, 0x3D, 0x3c, 0x98, 0x33, 0x8b, 0x1e, 0x2f, 0x35, 0x33, 0xf6, 0xc3, 0xa5, 0xba,
0xF1, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x23, 0x4c, 0x6c, 0x77, 0x72, 0x78, 0x4f, 0x5a, 0xd6, 0x6f, 0xe1, 0x4a,
0x1F, 0xE0, 0xFF, 0x07, 0xFC, 0x3F, 0xF7, 0xEF, 0xFE, 0x3F, 0xF0, 0xFE, 0xe9, 0x93, 0xc1, 0x1e, 0xec, 0x84, 0xf9, 0x7b, 0x79, 0x5d, 0x42, 0x47,
0x00, 0x1F, 0x81, 0xFF, 0x1F, 0xFD, 0xFF, 0xEF, 0x8F, 0x78, 0x7F, 0xC3, 0xff, 0xf4, 0x7e, 0x36, 0x7a, 0xe9, 0xd5, 0xf3, 0x80, 0xf4, 0xe5, 0xc9,
0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0x67, 0x5b, 0x22, 0xd5, 0xd7, 0x05, 0xd8, 0x93, 0x8c, 0x81, 0x41, 0x06,
0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0xC7, 0xDF, 0xFE, 0xFF, 0xF3, 0xFF, 0x80, 0x75, 0x9e, 0x1c, 0xdc, 0x83, 0x40, 0x1d, 0xf7, 0xea, 0xac, 0x0d, 0xf3,
0x3C, 0x01, 0xE0, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x0F, 0x8e, 0xc8, 0x3d, 0x71, 0xbd, 0x58, 0xcf, 0x32, 0x76, 0xe2, 0xb8, 0xae,
0xFC, 0x79, 0xF7, 0xCF, 0xFE, 0x3F, 0xE0, 0xFE, 0x00, 0xFF, 0xFF, 0xF0, 0xdf, 0x6c, 0xf2, 0x75, 0x9a, 0xc5, 0x27, 0x2c, 0x70, 0x9a, 0x3d, 0x11,
0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x26, 0xca, 0xea, 0xec, 0x8e, 0x9e, 0xe9, 0xa1, 0x2b, 0xa7, 0x72, 0x93,
0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xF3, 0x9C, 0xCE, 0x73, 0x98, 0x17, 0xe9, 0x72, 0x19, 0xcf, 0xd9, 0x09, 0x77, 0x1d, 0x7f, 0x2c, 0x0f,
0x00, 0x20, 0x0C, 0x07, 0x81, 0xF0, 0xFE, 0x3F, 0x9F, 0xC7, 0xF0, 0xF8, 0x45, 0xf0, 0x48, 0x25, 0x4f, 0x76, 0x46, 0xa1, 0x8d, 0xa5, 0x22, 0xde,
0x1C, 0x03, 0xE0, 0x7E, 0x07, 0xF0, 0x3F, 0x03, 0xF8, 0x1F, 0x01, 0xE0, 0xd8, 0x2a, 0xf1, 0xda, 0x5d, 0x17, 0x76, 0x54, 0x09, 0x74, 0x9d, 0x08,
0x0C, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xff, 0xc5, 0x3a, 0xa9, 0xca, 0x41, 0x37, 0x29, 0xd7, 0xa6, 0x73, 0xba,
0xFF, 0xFF, 0xC0, 0x80, 0x18, 0x03, 0x80, 0x7C, 0x0F, 0xC0, 0xFE, 0x07, 0x69, 0x56, 0x7c, 0x54, 0xaf, 0xb2, 0xea, 0xa3, 0x7a, 0x9d, 0x55, 0x8d,
0xF0, 0x7F, 0x03, 0xE0, 0x3C, 0x0F, 0x83, 0xF1, 0xFC, 0xFE, 0x3F, 0x87, 0xe2, 0xf3, 0xcd, 0x6d, 0x67, 0x22, 0xa8, 0xe9, 0x10, 0xb7, 0x95, 0xdd,
0xC0, 0xF0, 0x18, 0x02, 0x00, 0x00, 0x1F, 0x87, 0xFC, 0x7F, 0xEF, 0xFE, 0x6e, 0xb5, 0x8b, 0xf3, 0x0d, 0x34, 0x2f, 0x14, 0x1c, 0xa5, 0x55, 0xd5,
0xF1, 0xEF, 0x1F, 0xF1, 0xFF, 0x1F, 0xF1, 0xFF, 0x1F, 0xF1, 0xFF, 0x1F, 0x4d, 0x56, 0x9f, 0x5f, 0xaa, 0xd7, 0xe0, 0x56, 0xcb, 0x90, 0xb7, 0xa1,
0x01, 0xF0, 0x1E, 0x01, 0xE0, 0x3E, 0x03, 0xE0, 0xFC, 0x3F, 0x83, 0xF0, 0x54, 0x29, 0x16, 0x18, 0x25, 0xe2, 0x0f, 0xd2, 0x4b, 0x26, 0xb7, 0xc6,
0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcb, 0xef, 0x1b, 0xef, 0x19, 0x02, 0x37, 0xac, 0x55, 0xef, 0x86, 0x71,
0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0x00, 0x3F, 0x80, 0xef, 0x86, 0x5f, 0x7b, 0x1e, 0x54, 0xac, 0xce, 0x6f, 0x54, 0xd6, 0xf8,
0x00, 0xFF, 0xF0, 0x00, 0xFF, 0xFC, 0x00, 0xF8, 0x0F, 0x00, 0xF0, 0x03, 0x7d, 0xc0, 0x72, 0xfb, 0x28, 0x65, 0x06, 0xe4, 0xff, 0xf9, 0xa6, 0x83,
0xC0, 0xF0, 0x00, 0xF0, 0xF0, 0x00, 0x38, 0x78, 0x00, 0x1C, 0x38, 0x1F, 0x83, 0x8c, 0x38, 0x61, 0x13, 0x6c, 0x55, 0x03, 0xb2, 0xbc, 0x5e, 0xdc,
0x87, 0x3C, 0x3F, 0xE3, 0x9E, 0x1E, 0xF1, 0xCE, 0x1C, 0x38, 0xE7, 0x0E, 0x9d, 0x3d, 0x73, 0xbe, 0xb8, 0xa7, 0xcf, 0xd7, 0xed, 0xd9, 0x7b, 0x82,
0x1C, 0x77, 0x8E, 0x0E, 0x1F, 0xC7, 0x07, 0x0F, 0xE3, 0x83, 0x87, 0xF1, 0x48, 0x20, 0x7c, 0xaa, 0x83, 0xd1, 0x54, 0x83, 0x39, 0x6f, 0xf0, 0xf6,
0xC1, 0xC3, 0xF8, 0xE0, 0xE1, 0xFC, 0x70, 0x70, 0xFE, 0x38, 0x38, 0x7F, 0x78, 0xc5, 0x8f, 0x48, 0x6b, 0x35, 0xb6, 0x64, 0x82, 0x47, 0x1e, 0xf2,
0x1C, 0x1C, 0x77, 0x8E, 0x0E, 0x39, 0xC7, 0x07, 0x1C, 0xE3, 0xC7, 0xCE, 0x85, 0x2b, 0x4c, 0x5e, 0xc2, 0x55, 0x45, 0x3d, 0xbb, 0xdc, 0x7a, 0x6c,
0x70, 0xF6, 0xFE, 0x3C, 0x7F, 0x3F, 0x1E, 0x1F, 0x0F, 0x07, 0x81, 0x00, 0xbf, 0x69, 0x53, 0x58, 0xd4, 0xfc, 0xf6, 0xf6, 0x1b, 0x76, 0xf8, 0xd4,
0x03, 0xC0, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x1F, 0x80, 0x01, 0xe9, 0xbc, 0x26, 0x8c, 0x00, 0x33, 0x06, 0x93, 0x1a, 0xc7, 0xd7,
0x70, 0x07, 0xFF, 0xF8, 0x00, 0xFF, 0xFE, 0x00, 0x1F, 0xF0, 0x00, 0x07, 0xd5, 0x20, 0x52, 0xe0, 0xf6, 0xc5, 0x0c, 0x9d, 0x5d, 0x9d, 0x38, 0x00,
0xC0, 0x07, 0xE0, 0x0F, 0xE0, 0x0F, 0xE0, 0x0F, 0xE0, 0x0F, 0xE0, 0x0F, 0x31, 0xb6, 0xab, 0x00, 0x35, 0xda, 0x57, 0x38, 0xdf, 0xae, 0x39, 0x52,
0xE0, 0x0F, 0xF0, 0x0E, 0xF0, 0x1E, 0xF0, 0x1E, 0xF0, 0x1E, 0xF0, 0x1E, 0x3b, 0xad, 0xe3, 0x1f, 0x7d, 0xdd, 0x80, 0x9f, 0xa3, 0x7a, 0xe8, 0xd6,
0xF0, 0x1E, 0x70, 0x1E, 0x78, 0x1C, 0x78, 0x3C, 0x78, 0x3C, 0x78, 0x3C, 0xae, 0xbb, 0xbe, 0xdf, 0xce, 0x5e, 0x16, 0x9f, 0x5e, 0xa6, 0xba, 0xf1,
0x78, 0x3C, 0x78, 0x3C, 0x3C, 0x3C, 0x3C, 0x7C, 0x3C, 0x78, 0x3C, 0x7F, 0xbe, 0x35, 0xba, 0x2d, 0x1a, 0x76, 0x8c, 0x26, 0xef, 0x37, 0x78, 0x53,
0xFC, 0x7F, 0xFC, 0x7F, 0xFE, 0x7F, 0xFE, 0x78, 0x1E, 0xF8, 0x1E, 0xF8, 0x4d, 0x4f, 0x1c, 0xdf, 0x2f, 0x0a, 0xc4, 0x65, 0x57, 0x86, 0xc7, 0x1d,
0x1E, 0xF0, 0x1E, 0xF0, 0x1F, 0xF0, 0x1F, 0xFF, 0x03, 0xFF, 0x8F, 0xFF, 0xd5, 0x39, 0x25, 0xd4, 0x0a, 0xa3, 0x6b, 0x50, 0x83, 0xde, 0x43, 0x0b,
0x3F, 0xFC, 0xF0, 0xFB, 0xC3, 0xEF, 0x07, 0xBC, 0x1E, 0xF0, 0x7B, 0xC1, 0x93, 0x9e, 0xa8, 0x10, 0xad, 0x5d, 0x7f, 0x3f, 0x5f, 0xc7, 0x27, 0x03,
0xEF, 0x07, 0xBC, 0x3E, 0xF0, 0xFB, 0xC3, 0xCF, 0xFF, 0x3F, 0xF8, 0xFF, 0xfc, 0x4f, 0x36, 0xcc, 0xe7, 0xbe, 0x13, 0x38, 0xae, 0xe3, 0x8e, 0x5d,
0xF3, 0xFF, 0xCF, 0x0F, 0xBC, 0x1E, 0xF0, 0x7B, 0xC1, 0xEF, 0x07, 0xFC, 0x34, 0x30, 0xd8, 0x11, 0xd3, 0x15, 0x7b, 0xa1, 0xba, 0x0a, 0xfb, 0x3b,
0x1F, 0xF0, 0x7F, 0xC1, 0xFF, 0x07, 0xBC, 0x1E, 0xF0, 0x7B, 0xC3, 0xEF, 0x35, 0xac, 0x15, 0xda, 0x2e, 0xca, 0xb8, 0x34, 0x41, 0x1a, 0xe2, 0x1b,
0xFF, 0xBF, 0xFC, 0xFF, 0xE3, 0xFF, 0x00, 0x0F, 0xC0, 0x7F, 0xC3, 0xFF, 0x03, 0x48, 0xfe, 0x68, 0xf6, 0x0e, 0x34, 0xe4, 0x33, 0x45, 0x3f, 0x76,
0x9F, 0xFE, 0x7C, 0x79, 0xE1, 0xFF, 0x87, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0xec, 0xc4, 0xa7, 0x64, 0x4b, 0x22, 0x4d, 0x5c, 0x4d, 0x2c, 0x43, 0x06,
0xFF, 0x83, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0x0F, 0x80, 0x3E, 0x00, 0xF8, 0x96, 0xd0, 0x84, 0x19, 0xe2, 0x6b, 0x62, 0x7c, 0xe3, 0x99, 0xa2, 0x1f,
0x03, 0xE0, 0x0F, 0x80, 0x3E, 0x00, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xFE, 0x27, 0x70, 0xb0, 0x08, 0x8a, 0xb0, 0xbf, 0xd0, 0xb3, 0x81, 0x0f, 0x4c,
0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xFE, 0x1F, 0x78, 0x7D, 0xE1, 0xE7, 0x11, 0xc0, 0x14, 0x8d, 0x28, 0x0e, 0x73, 0x35, 0xe4, 0x04, 0x46, 0xac,
0xEF, 0x8F, 0xFE, 0x3F, 0xF0, 0x3F, 0x80, 0xFF, 0x07, 0xFF, 0x3F, 0xFD, 0xab, 0xf5, 0xfb, 0xd1, 0x7e, 0xfa, 0xc0, 0x53, 0x9d, 0xc6, 0xa4, 0xe7,
0xFF, 0xEF, 0x0F, 0xF8, 0x7F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x21, 0x6b, 0xc3, 0x6a, 0x7d, 0xc9, 0xbf, 0x4a, 0x42, 0x0e, 0x8b, 0x5a,
0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xad, 0x94, 0x17, 0xec, 0x8e, 0xdb, 0xb4, 0x58, 0x9d, 0x76, 0x0a, 0xd9,
0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0xc3, 0xf3, 0xc1, 0xa1, 0x85, 0xd2, 0xba, 0xba, 0x3e, 0xd5, 0x89, 0xe3,
0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x7F, 0xFF, 0xDF, 0xFE, 0xFF, 0x1c, 0x89, 0x63, 0x7c, 0x76, 0x97, 0x1e, 0xde, 0x03, 0xcf, 0x7b, 0xa8,
0xE7, 0xFC, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x3C, 0x0F, 0x03, 0xb9, 0x5e, 0x19, 0xd4, 0x34, 0xf9, 0x8b, 0x4e, 0x63, 0x0e, 0x2b, 0x7c,
0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xFF, 0xFF, 0x37, 0xbc, 0x79, 0x20, 0xcc, 0x93, 0x04, 0xe7, 0x6c, 0x05, 0x09, 0xdb,
0xFF, 0xFF, 0xFF, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x43, 0x69, 0x82, 0xdf, 0xdd, 0xb6, 0x0e, 0xcc, 0x73, 0x63, 0x8e, 0xce,
0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0x04, 0xbf, 0x3e, 0xef, 0x8a, 0xe1, 0x4e, 0xd3, 0x6a, 0xad, 0x21, 0x4b,
0xFF, 0xFF, 0xFF, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xd0, 0x74, 0xd3, 0x06, 0x0e, 0x9d, 0xeb, 0x45, 0x1b, 0xe1, 0x3b, 0xf4,
0xC0, 0xF0, 0x3C, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xC0, 0xF0, 0xf8, 0x5b, 0x5d, 0xc4, 0x03, 0xa5, 0x55, 0x01, 0xd4, 0x78, 0x56, 0x96,
0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x6c, 0x2b, 0x8b, 0xde, 0xc3, 0x34, 0x5d, 0x23, 0x2c, 0xc3, 0x11, 0x77,
0x03, 0xC0, 0xF0, 0x3C, 0x00, 0x0F, 0xE0, 0x7F, 0xC3, 0xFF, 0x9F, 0xFE, 0xba, 0xab, 0x20, 0xa8, 0x52, 0xe9, 0xf7, 0x02, 0x6b, 0x3f, 0x40, 0xca,
0x7C, 0x7D, 0xE0, 0xFF, 0x83, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0x4e, 0x1e, 0x86, 0x22, 0x89, 0x3b, 0x82, 0x20, 0xec, 0x1b, 0x77, 0x83,
0xFE, 0x0F, 0xF8, 0x03, 0xE0, 0x0F, 0x80, 0x3E, 0x00, 0xF9, 0xFF, 0xE7, 0xe0, 0x4a, 0xad, 0x3e, 0xdf, 0x25, 0xa9, 0xdb, 0xd4, 0x00, 0x69, 0x03,
0xFF, 0x9F, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xFE, 0x0F, 0xF8, 0xd7, 0x37, 0x8b, 0x6c, 0xee, 0xee, 0x49, 0x0d, 0x3a, 0x8d, 0x87, 0x44,
0x3F, 0xE0, 0xFF, 0x83, 0xFE, 0x0F, 0x78, 0x3D, 0xE1, 0xF7, 0xEF, 0xCF, 0x54, 0x63, 0x8f, 0xc4, 0x71, 0xc1, 0xad, 0x23, 0xd0, 0x54, 0xc3, 0x5a,
0xFF, 0x3F, 0xEC, 0x3F, 0x30, 0xF0, 0x7F, 0xC1, 0xFF, 0x07, 0xFC, 0x1F, 0x98, 0x3c, 0x8e, 0x7d, 0x55, 0x5e, 0xad, 0xb2, 0xd2, 0xb0, 0x0d, 0x12,
0xF0, 0x7F, 0xC1, 0xFF, 0x07, 0xFC, 0x1F, 0xF0, 0x7F, 0xC1, 0xFF, 0x07, 0x48, 0x08, 0x37, 0x6b, 0x1d, 0x26, 0xfd, 0xff, 0x91, 0x75, 0xab, 0xf5,
0xFC, 0x1F, 0xF0, 0x7F, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xf9, 0x39, 0xde, 0x15, 0x7f, 0x7f, 0xf3, 0x66, 0x80, 0xfc, 0x3e, 0x9c,
0xFF, 0x07, 0xFC, 0x1F, 0xF0, 0x7F, 0xC1, 0xFF, 0x07, 0xFC, 0x1F, 0xF0, 0xdd, 0x83, 0xd4, 0x6f, 0xba, 0xc1, 0xe5, 0x9d, 0x45, 0xab, 0xe5, 0x39,
0x7F, 0xC1, 0xFF, 0x07, 0xFC, 0x1F, 0xF0, 0x7F, 0xC1, 0xFF, 0x07, 0xFC, 0x82, 0x84, 0x9a, 0x7f, 0x27, 0x8b, 0xc1, 0x6d, 0x43, 0x21, 0xca, 0x1c,
0x1F, 0xF0, 0x7F, 0xC1, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xdd, 0x86, 0x0f, 0xa7, 0x37, 0xef, 0x73, 0xae, 0xb6, 0x25, 0xfe, 0x61,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xF8, 0x5d, 0xab, 0x4c, 0xf1, 0x9d, 0xa7, 0x5a, 0xb2, 0xba, 0xc6, 0xa6, 0x96,
0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xb3, 0x85, 0xc0, 0x16, 0x1d, 0x46, 0x1f, 0x31, 0x84, 0x23, 0xd3, 0xba,
0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x42, 0xf4, 0xf0, 0xcd, 0xfd, 0xa6, 0x5e, 0x7e, 0x26, 0xfa, 0x7a, 0x12,
0x00, 0xF8, 0x07, 0xC0, 0x3E, 0x01, 0xF0, 0x0F, 0xF8, 0x7F, 0xC3, 0xFE, 0x1d, 0x99, 0x5b, 0x4a, 0x74, 0x98, 0x97, 0xfd, 0xd1, 0xa8, 0x4b, 0xdc,
0x1F, 0xF0, 0xFF, 0x87, 0xFC, 0x3F, 0xE1, 0xEF, 0x0F, 0x78, 0x7B, 0xF7, 0xb2, 0x16, 0x2d, 0x19, 0xb3, 0x99, 0x68, 0xc6, 0x4c, 0xbb, 0x65, 0x66,
0xCF, 0xFC, 0x7F, 0xE1, 0xFE, 0x00, 0xF0, 0x3D, 0xE0, 0xFB, 0xC1, 0xE7, 0xb8, 0x27, 0x3b, 0xdc, 0x2d, 0x52, 0xf9, 0x5f, 0xe0, 0xff, 0x61, 0x05,
0x83, 0xCF, 0x0F, 0x9E, 0x1E, 0x3C, 0x3C, 0x78, 0xF8, 0xF1, 0xE1, 0xE7, 0xb9, 0x4b, 0x0e, 0x10, 0x35, 0x31, 0x0b, 0x5a, 0x05, 0x52, 0x0e, 0xfd,
0xC3, 0xCF, 0x87, 0x9E, 0x0F, 0x7C, 0x1E, 0xF8, 0x3D, 0xE0, 0x7F, 0xC0, 0x7d, 0xdc, 0x75, 0x06, 0x80, 0xd6, 0x2a, 0xbd, 0xc0, 0x65, 0x47, 0x54,
0xFF, 0x01, 0xFE, 0x03, 0xFE, 0x07, 0xBC, 0x0F, 0x7C, 0x1E, 0xF8, 0x3C, 0xc7, 0x97, 0xd4, 0xe7, 0x02, 0x77, 0x22, 0x8a, 0xab, 0x91, 0xb9, 0x21,
0xF0, 0x79, 0xF0, 0xF1, 0xE1, 0xE3, 0xC3, 0xC7, 0xC7, 0x87, 0x8F, 0x0F, 0x01, 0xec, 0xfc, 0x30, 0x4e, 0xdb, 0x31, 0xcf, 0x65, 0xcd, 0xe2, 0x4f,
0x9E, 0x1F, 0x3C, 0x1E, 0x78, 0x3E, 0xF0, 0x7D, 0xE0, 0x7C, 0xF0, 0x3C, 0xc1, 0xab, 0x34, 0xbf, 0xe0, 0xef, 0xa7, 0x5f, 0xde, 0x5e, 0xac, 0x4f,
0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xde, 0xb9, 0xd5, 0xdf, 0x8b, 0xbf, 0x65, 0xc3, 0x7f, 0x4c, 0xd6, 0xaf,
0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0xe3, 0xfd, 0x2f, 0xaf, 0xdf, 0xe7, 0x5f, 0xde, 0xb1, 0xe2, 0x53, 0xf8,
0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x0F, 0x32, 0x6d, 0x3a, 0x08, 0x1a, 0x70, 0x5b, 0x34, 0x9c, 0x26, 0xcd, 0x98,
0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF8, 0x01, 0xFF, 0x80, 0x1F, 0xF8, 0x03, 0x2e, 0xe4, 0x98, 0x1d, 0x8b, 0x80, 0x20, 0x58, 0xe2, 0xc3, 0xae, 0x6f,
0xFF, 0xC0, 0x3F, 0xFC, 0x03, 0xFF, 0xC0, 0x3F, 0xFC, 0x03, 0xFF, 0xC0, 0xbe, 0xb8, 0xfd, 0x94, 0xfe, 0x46, 0x9f, 0xa1, 0x9a, 0x0f, 0x9b, 0x00,
0x7F, 0xFE, 0x07, 0xFF, 0xE0, 0x7F, 0xFE, 0x07, 0xFE, 0xE0, 0x7F, 0xEE, 0x69, 0xae, 0xc4, 0x99, 0xf6, 0xe2, 0x00, 0x70, 0xae, 0x68, 0xd7, 0x8b,
0x0F, 0xFE, 0xF0, 0xFF, 0xEF, 0x0E, 0xFE, 0x70, 0xEF, 0xE7, 0x0E, 0xFE, 0xac, 0x49, 0x1a, 0x33, 0x23, 0x32, 0x10, 0x7f, 0x28, 0xde, 0x1e, 0x5f,
0x70, 0xEF, 0xE7, 0x1E, 0xFE, 0x79, 0xCF, 0xE3, 0x9C, 0xFF, 0x39, 0xCF, 0xd0, 0x4f, 0x78, 0x4f, 0xa4, 0xdf, 0xb3, 0x15, 0xd9, 0xdc, 0x81, 0xcc,
0xF3, 0x9C, 0xFF, 0x3B, 0xCF, 0xF3, 0xF8, 0xFF, 0x1F, 0x8F, 0xF1, 0xF8, 0xab, 0x49, 0x58, 0xde, 0xa8, 0x73, 0xe9, 0xd0, 0x64, 0xfb, 0xb3, 0x93,
0xFF, 0x1F, 0x8F, 0xF1, 0xF8, 0xFF, 0x1F, 0x8F, 0xF0, 0xF0, 0xFF, 0x0F, 0x66, 0xf7, 0xe3, 0x80, 0x5f, 0xf3, 0x33, 0x18, 0xa1, 0xb1, 0xe0, 0xe8,
0x0F, 0xF0, 0xF0, 0xFF, 0x0F, 0x0F, 0xE0, 0x3F, 0x80, 0xFF, 0x03, 0xFC, 0xe1, 0xb9, 0x6f, 0x8e, 0x3c, 0xa8, 0xb9, 0x63, 0x5f, 0x61, 0xc1, 0xa2,
0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xFF, 0x0F, 0xFC, 0x3F, 0xF0, 0xFF, 0xf5, 0x68, 0xf2, 0xe8, 0xb0, 0x20, 0x0f, 0xf3, 0xd8, 0x02, 0xfb, 0x7c,
0xE3, 0xFF, 0x8F, 0xFE, 0x3F, 0xFC, 0xFF, 0xF3, 0xFD, 0xCF, 0xF7, 0xBF, 0xb0, 0x9a, 0xcf, 0x64, 0xe0, 0xf1, 0xe5, 0x83, 0x39, 0x58, 0x16, 0x86,
0xDE, 0xFF, 0x3B, 0xFC, 0xFF, 0xF3, 0xDF, 0xC7, 0x7F, 0x1F, 0xFC, 0x7F, 0x23, 0x0e, 0x3d, 0xad, 0x56, 0xf0, 0xb9, 0x52, 0xe5, 0xb6, 0xf8, 0x0f,
0xF0, 0xFF, 0xC3, 0xFF, 0x0F, 0xFC, 0x1F, 0xF0, 0x7F, 0xC1, 0xFF, 0x03, 0x50, 0xdd, 0x7d, 0xcc, 0xa9, 0x11, 0x00, 0x00
0xFC, 0x0F, 0xF0, 0x3F, 0xC0, 0x70, 0x0F, 0xE0, 0x7F, 0xC3, 0xFF, 0x9F, };
0xFE, 0x7C, 0x7D, 0xE1, 0xFF, 0x83, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF,
0x83, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xFE, 0x0F, 0xF8, 0x3F,
0xE0, 0xFF, 0x83, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xFE, 0x0F,
0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xFE, 0x0F, 0x78, 0x7D, 0xE1, 0xF7, 0xFF,
0x8F, 0xFE, 0x3F, 0xF0, 0x3F, 0x80, 0xFF, 0x87, 0xFF, 0x3F, 0xFD, 0xFF,
0xEF, 0x0F, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F,
0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x1F, 0xF0, 0xFF, 0xFF, 0xBF,
0xFD, 0xFF, 0xCF, 0xF8, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x80,
0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07,
0x80, 0x00, 0x0F, 0xE0, 0x7F, 0xC3, 0xFF, 0x9F, 0xFE, 0x7C, 0x7D, 0xE1,
0xFF, 0x83, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xFE, 0x0F, 0xF8,
0x3F, 0xE0, 0xFF, 0x83, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xFE,
0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF,
0x83, 0xFE, 0x0F, 0x78, 0x7D, 0xE1, 0xF7, 0xFF, 0x8F, 0xFE, 0x3F, 0xF0,
0x3F, 0x80, 0x1E, 0x00, 0x3C, 0x00, 0x78, 0x01, 0xE0, 0x03, 0x00, 0xFF,
0x83, 0xFF, 0x8F, 0xFF, 0x3F, 0xFE, 0xF0, 0xFB, 0xC1, 0xEF, 0x07, 0xBC,
0x1E, 0xF0, 0x7B, 0xC1, 0xEF, 0x07, 0xBC, 0x1E, 0xF0, 0x7B, 0xC3, 0xEF,
0x1F, 0x3F, 0xFC, 0xFF, 0xE3, 0xFF, 0xCF, 0x0F, 0xBC, 0x1E, 0xF0, 0x7B,
0xC1, 0xEF, 0x07, 0xBC, 0x1E, 0xF0, 0x7B, 0xC1, 0xEF, 0x07, 0xBC, 0x1E,
0xF0, 0x7B, 0xC1, 0xEF, 0x07, 0xBC, 0x1E, 0xF0, 0x7B, 0xC1, 0xF0, 0x0F,
0xC0, 0xFF, 0xC3, 0xFF, 0x1F, 0xFE, 0x78, 0x79, 0xE1, 0xEF, 0x87, 0xBE,
0x1F, 0xF8, 0x7D, 0xE1, 0xF7, 0x87, 0xDF, 0x1F, 0x7E, 0x00, 0xFC, 0x01,
0xF8, 0x03, 0xF0, 0x0F, 0xE0, 0x1F, 0x80, 0x3F, 0x00, 0x7E, 0x00, 0xF9,
0xE1, 0xF7, 0x87, 0xDE, 0x0F, 0x78, 0x3D, 0xE0, 0xF7, 0x83, 0xDE, 0x0F,
0x78, 0x3D, 0xE1, 0xF7, 0xEF, 0x8F, 0xFE, 0x3F, 0xF0, 0x3F, 0x80, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x80, 0xF8, 0x0F, 0x80, 0xF8, 0x0F,
0x80, 0xF8, 0x0F, 0x80, 0xF8, 0x0F, 0x80, 0xF8, 0x0F, 0x80, 0xF8, 0x0F,
0x80, 0xF8, 0x0F, 0x80, 0xF8, 0x0F, 0x80, 0xF8, 0x0F, 0x80, 0xF8, 0x0F,
0x80, 0xF8, 0x0F, 0x80, 0xF8, 0x0F, 0x80, 0xF8, 0x0F, 0x80, 0xF8, 0x0F,
0x80, 0xF8, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0,
0xFF, 0x83, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xFE, 0x0F, 0xF8,
0x3F, 0xE0, 0xFF, 0x83, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xFE,
0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF,
0x83, 0xFE, 0x0F, 0x78, 0x3D, 0xE1, 0xF7, 0xEF, 0xCF, 0xFE, 0x3F, 0xF8,
0x7F, 0x80, 0xF0, 0x1F, 0xF0, 0x1E, 0xF0, 0x1E, 0xF0, 0x1E, 0xF8, 0x1E,
0x78, 0x1E, 0x78, 0x1E, 0x78, 0x3C, 0x78, 0x3C, 0x78, 0x3C, 0x7C, 0x3C,
0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x78, 0x3C, 0x78, 0x3C, 0x78,
0x1E, 0x78, 0x1E, 0x78, 0x1E, 0x78, 0x1E, 0x78, 0x1E, 0x70, 0x1E, 0xF0,
0x0E, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0xF0, 0x0F, 0xE0,
0x0F, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0xF0, 0x3C,
0x0F, 0xF0, 0x3C, 0x0F, 0xF0, 0x3C, 0x0F, 0xF0, 0x3C, 0x1E, 0xF0, 0x3C,
0x1E, 0x78, 0x3E, 0x1E, 0x78, 0x7E, 0x1E, 0x78, 0x7E, 0x1E, 0x78, 0x7E,
0x1E, 0x78, 0x7E, 0x1E, 0x78, 0x7E, 0x1E, 0x78, 0x7E, 0x1E, 0x38, 0x7F,
0x1C, 0x38, 0xF7, 0x1C, 0x3C, 0xF7, 0x3C, 0x3C, 0xE7, 0x3C, 0x3C, 0xE7,
0x3C, 0x3C, 0xE7, 0x3C, 0x3C, 0xE7, 0x3C, 0x3C, 0xE7, 0xBC, 0x1D, 0xE3,
0xB8, 0x1D, 0xC3, 0xB8, 0x1D, 0xC3, 0xB8, 0x1F, 0xC3, 0xB8, 0x1F, 0xC3,
0xB8, 0x1F, 0xC3, 0xF8, 0x1F, 0xC3, 0xF8, 0x1F, 0xC1, 0xF8, 0x0F, 0x81,
0xF8, 0x0F, 0x81, 0xF0, 0x0F, 0x81, 0xF0, 0x0F, 0x81, 0xF0, 0x0F, 0x81,
0xF0, 0x0F, 0x81, 0xF0, 0xF0, 0x3F, 0xC0, 0xF7, 0x07, 0x9E, 0x1E, 0x78,
0x78, 0xE3, 0xE3, 0xCF, 0x0F, 0x3C, 0x3C, 0xF0, 0x7F, 0x81, 0xFE, 0x07,
0xF8, 0x0F, 0xE0, 0x3F, 0x00, 0xFC, 0x01, 0xF0, 0x07, 0x80, 0x3E, 0x00,
0xF8, 0x03, 0xF0, 0x0F, 0xC0, 0x7F, 0x01, 0xFE, 0x07, 0xF8, 0x3F, 0xE0,
0xF3, 0xC3, 0xCF, 0x0F, 0x3C, 0x78, 0x71, 0xE1, 0xE7, 0x87, 0x9E, 0x0E,
0xF0, 0x3F, 0xC0, 0xF0, 0xF8, 0x0F, 0x78, 0x1F, 0x78, 0x1E, 0x7C, 0x1E,
0x3C, 0x1E, 0x3C, 0x3E, 0x3C, 0x3C, 0x3E, 0x3C, 0x1E, 0x3C, 0x1E, 0x78,
0x1E, 0x78, 0x0F, 0x78, 0x0F, 0x78, 0x0F, 0xF0, 0x0F, 0xF0, 0x07, 0xF0,
0x07, 0xF0, 0x07, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0,
0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0,
0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0, 0x03, 0xE0,
0x7F, 0xF7, 0xFF, 0x7F, 0xF7, 0xFF, 0x01, 0xF0, 0x1E, 0x01, 0xE0, 0x3E,
0x03, 0xE0, 0x3C, 0x07, 0xC0, 0x7C, 0x07, 0xC0, 0x78, 0x0F, 0x80, 0xF8,
0x0F, 0x80, 0xF0, 0x1F, 0x01, 0xF0, 0x1E, 0x01, 0xE0, 0x3E, 0x03, 0xE0,
0x3C, 0x07, 0xC0, 0x7C, 0x07, 0xC0, 0x78, 0x0F, 0x80, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0,
0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0,
0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xFF,
0xFF, 0xFF, 0xF0, 0x1E, 0x03, 0xC0, 0x78, 0x07, 0x00, 0xF0, 0x1E, 0x03,
0xC0, 0x78, 0x07, 0x80, 0xF0, 0x1E, 0x03, 0xC0, 0x3C, 0x07, 0x80, 0xF0,
0x1E, 0x01, 0xC0, 0x3C, 0x07, 0x80, 0xF0, 0x0E, 0x01, 0xE0, 0x3C, 0x07,
0x80, 0xF0, 0x0F, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0x78, 0x0F, 0x01, 0xE0,
0x3C, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F,
0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F,
0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0xFF, 0xFF, 0xFF,
0x0F, 0xC0, 0x1F, 0x80, 0x3F, 0x80, 0x7F, 0x01, 0xFE, 0x03, 0xDC, 0x07,
0xBC, 0x0E, 0x78, 0x3C, 0xF0, 0x79, 0xE0, 0xF1, 0xE1, 0xE3, 0xC7, 0x87,
0x8F, 0x0F, 0x1E, 0x0F, 0x3C, 0x1E, 0xF8, 0x3D, 0xE0, 0x7C, 0xFF, 0xFF,
0xFF, 0xFF, 0xF0, 0xF9, 0xE7, 0x8E, 0x38, 0x71, 0xC3, 0x0F, 0xC0, 0xFF,
0xC3, 0xFF, 0x1F, 0xFE, 0x78, 0x79, 0xE1, 0xEF, 0x87, 0xBE, 0x1E, 0xF8,
0x7C, 0x01, 0xF0, 0x07, 0xC0, 0x7F, 0x0F, 0xFC, 0x7F, 0xF3, 0xE7, 0xDE,
0x1F, 0xF8, 0x7F, 0xE1, 0xFF, 0x87, 0xFE, 0x1F, 0xF8, 0x7F, 0xE1, 0xFF,
0x87, 0xFE, 0x1F, 0xF8, 0x7D, 0xE3, 0xF7, 0xFF, 0xDF, 0xFF, 0x3F, 0x7C,
0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00, 0x79, 0xE3, 0xDF, 0x9F,
0xFE, 0xFF, 0xFF, 0xC7, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1,
0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F,
0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xFC,
0x7F, 0xF7, 0xFF, 0xFE, 0xF7, 0xF7, 0x9F, 0x00, 0x0F, 0xC1, 0xFF, 0x8F,
0xFC, 0xFF, 0xF7, 0x87, 0xBC, 0x3F, 0xE1, 0xFF, 0x0F, 0xF8, 0x7F, 0xC3,
0xFE, 0x1F, 0xF0, 0x0F, 0x80, 0x7C, 0x03, 0xE0, 0x1F, 0x00, 0xF8, 0x07,
0xC0, 0x3E, 0x1F, 0xF0, 0xFF, 0x87, 0xFC, 0x3F, 0xE1, 0xEF, 0x0F, 0x78,
0x7B, 0xE7, 0xDF, 0xFC, 0x7F, 0xE1, 0xFE, 0x00, 0x00, 0x7C, 0x01, 0xF0,
0x07, 0xC0, 0x1F, 0x00, 0x7C, 0x7D, 0xF3, 0xFF, 0xDF, 0xFF, 0x7F, 0xFD,
0xE1, 0xFF, 0x87, 0xFE, 0x1F, 0xF8, 0x7F, 0xE1, 0xFF, 0x87, 0xFE, 0x1F,
0xF8, 0x7F, 0xE1, 0xFF, 0x87, 0xFE, 0x1F, 0xF8, 0x7F, 0xE1, 0xFF, 0x87,
0xFE, 0x1F, 0xF8, 0x7F, 0xE1, 0xFF, 0x87, 0xFE, 0x1F, 0xF8, 0x7D, 0xE1,
0xF7, 0xDF, 0xDF, 0xFF, 0x3F, 0xFC, 0x7D, 0xF0, 0x0F, 0xC0, 0x7F, 0xC3,
0xFF, 0x1F, 0xFE, 0x78, 0x79, 0xE1, 0xEF, 0x87, 0xBE, 0x1E, 0xF8, 0x7B,
0xE1, 0xFF, 0x87, 0xFE, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00,
0xF8, 0x03, 0xE0, 0x0F, 0x87, 0xBE, 0x1E, 0xF8, 0x7B, 0xE1, 0xEF, 0x87,
0x9E, 0x1E, 0x78, 0x79, 0xF3, 0xE7, 0xFF, 0x0F, 0xFC, 0x1F, 0xE0, 0x07,
0xC3, 0xF1, 0xFC, 0x7C, 0x1E, 0x07, 0x8F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1,
0xE0, 0x78, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x1E, 0x07, 0x81, 0xE0, 0x78,
0x1E, 0x07, 0x81, 0xE0, 0x78, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x1E, 0x07,
0x81, 0xE0, 0x78, 0x1E, 0x07, 0x80, 0x1F, 0x7C, 0xFF, 0xF7, 0xFF, 0xDF,
0xFF, 0x78, 0x7D, 0xE1, 0xFF, 0x87, 0xFE, 0x1F, 0xF8, 0x7F, 0xE1, 0xFF,
0x87, 0xFE, 0x1F, 0xF8, 0x7F, 0xE1, 0xFF, 0x87, 0xFE, 0x1F, 0xF8, 0x7F,
0xE1, 0xFF, 0x87, 0xFE, 0x1F, 0xF8, 0x7D, 0xE1, 0xF7, 0xCF, 0xDF, 0xFF,
0x3F, 0xFC, 0x7D, 0xF0, 0x07, 0xC0, 0x1F, 0x00, 0x79, 0xC3, 0xE7, 0xFF,
0xBF, 0xFC, 0x3F, 0xC0, 0xF0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x0F, 0x00,
0x79, 0xF3, 0xDF, 0xDF, 0xFF, 0xFF, 0xFF, 0xC3, 0xFC, 0x1F, 0xE0, 0xFF,
0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F, 0xE0,
0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F,
0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xC0,
0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x3C, 0x78, 0xF1, 0xE0, 0x00, 0x0F,
0x9F, 0x3E, 0x7C, 0xF9, 0xF3, 0xE7, 0xCF, 0x9F, 0x3E, 0x7C, 0xF9, 0xF3,
0xE7, 0xCF, 0x9F, 0x3E, 0x7C, 0xF9, 0xF3, 0xE7, 0xCF, 0x9F, 0x3E, 0x7C,
0xF9, 0xFF, 0xDF, 0xBE, 0x00, 0xF0, 0x03, 0xC0, 0x0F, 0x00, 0x3C, 0x00,
0xF0, 0x03, 0xC1, 0xFF, 0x07, 0xBC, 0x1E, 0xF0, 0xFB, 0xC3, 0xCF, 0x1F,
0x3C, 0x78, 0xF1, 0xE3, 0xCF, 0x8F, 0x3C, 0x3D, 0xF0, 0xF7, 0x83, 0xDE,
0x0F, 0xF8, 0x3F, 0xC0, 0xFF, 0x83, 0xDE, 0x0F, 0x78, 0x3D, 0xF0, 0xF3,
0xC3, 0xCF, 0x8F, 0x1E, 0x3C, 0x7C, 0xF0, 0xF3, 0xC3, 0xCF, 0x0F, 0xBC,
0x1E, 0xF0, 0x7F, 0xC0, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF3, 0xE1,
0xF3, 0xDF, 0xCF, 0xEF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0xFC, 0x3F,
0xC1, 0xE0, 0xFF, 0x07, 0x83, 0xFC, 0x1E, 0x0F, 0xF0, 0x78, 0x3F, 0xC1,
0xE0, 0xFF, 0x07, 0x83, 0xFC, 0x1E, 0x0F, 0xF0, 0x78, 0x3F, 0xC1, 0xE0,
0xFF, 0x07, 0x83, 0xFC, 0x1E, 0x0F, 0xF0, 0x78, 0x3F, 0xC1, 0xE0, 0xFF,
0x07, 0x83, 0xFC, 0x1E, 0x0F, 0xF0, 0x78, 0x3F, 0xC1, 0xE0, 0xFF, 0x07,
0x83, 0xFC, 0x1E, 0x0F, 0xF0, 0x78, 0x3F, 0xC1, 0xE0, 0xFF, 0x07, 0x83,
0xFC, 0x1E, 0x0F, 0xF0, 0x78, 0x3C, 0xF3, 0xE7, 0xBF, 0xBF, 0xFF, 0xFF,
0xFF, 0x87, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC, 0x1F,
0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83, 0xFC,
0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F, 0x83,
0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0x80, 0x0F, 0xC0, 0x7F, 0x83, 0xFF, 0x1F,
0xFE, 0x78, 0x79, 0xE1, 0xEF, 0x87, 0xBE, 0x1F, 0xF8, 0x7F, 0xE1, 0xFF,
0x87, 0xFE, 0x1F, 0xF8, 0x7F, 0xE1, 0xFF, 0x87, 0xFE, 0x1F, 0xF8, 0x7F,
0xE1, 0xFF, 0x87, 0xFE, 0x1F, 0xF8, 0x7F, 0xE1, 0xFF, 0x87, 0xDE, 0x1E,
0x78, 0x79, 0xF3, 0xE7, 0xFF, 0x0F, 0xFC, 0x1F, 0xE0, 0xF3, 0xC7, 0xBF,
0xBF, 0xFD, 0xFF, 0xFF, 0x8F, 0xF8, 0x7F, 0xC1, 0xFE, 0x0F, 0xF0, 0x7F,
0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F, 0xF0,
0x7F, 0x83, 0xFC, 0x1F, 0xE0, 0xFF, 0x07, 0xF8, 0x3F, 0xC1, 0xFE, 0x0F,
0xF8, 0xFF, 0xEF, 0xFF, 0xFD, 0xFF, 0xEF, 0x7E, 0x78, 0x03, 0xC0, 0x1E,
0x00, 0xF0, 0x07, 0x80, 0x00, 0x1E, 0x7C, 0xFF, 0xF7, 0xFF, 0xDF, 0xFF,
0x78, 0x7F, 0xE1, 0xFF, 0x87, 0xFE, 0x1F, 0xF8, 0x7F, 0xE1, 0xFF, 0x87,
0xFE, 0x1F, 0xF8, 0x7F, 0xE1, 0xFF, 0x87, 0xFE, 0x1F, 0xF8, 0x7F, 0xE1,
0xFF, 0x87, 0xFE, 0x1F, 0xF8, 0x7F, 0xE1, 0xFF, 0x87, 0xFE, 0x1F, 0x78,
0x7D, 0xF7, 0xF7, 0xFF, 0xCF, 0xFF, 0x1F, 0x7C, 0x01, 0xF0, 0x07, 0xC0,
0x1F, 0x00, 0x7C, 0x01, 0xF0, 0xF3, 0xFB, 0xFF, 0xFF, 0xFF, 0xC7, 0x83,
0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0,
0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E,
0x0F, 0x00, 0x1F, 0x83, 0xFE, 0x1F, 0xF9, 0xFF, 0xCF, 0x0F, 0x78, 0x7B,
0xC3, 0xDE, 0x1E, 0xF0, 0xF7, 0xC7, 0xBE, 0x00, 0xF8, 0x07, 0xE0, 0x1F,
0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7D, 0xE1, 0xEF, 0x0F,
0xF8, 0x7F, 0xC3, 0xFE, 0x1F, 0xF0, 0xF7, 0xEF, 0x9F, 0xFC, 0xFF, 0xC1,
0xFC, 0x00, 0x3E, 0x0F, 0x83, 0xE0, 0xF8, 0x3E, 0x0F, 0x8F, 0xFF, 0xFF,
0xFF, 0xCF, 0x83, 0xE0, 0xF8, 0x3E, 0x0F, 0x83, 0xE0, 0xF8, 0x3E, 0x0F,
0x83, 0xE0, 0xF8, 0x3E, 0x0F, 0x83, 0xE0, 0xF8, 0x3E, 0x0F, 0x83, 0xE0,
0xF8, 0x3E, 0x0F, 0x83, 0xF0, 0xFF, 0x1F, 0xC3, 0xF0, 0xF8, 0x3F, 0xE0,
0xFF, 0x83, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xFE, 0x0F, 0xF8,
0x3F, 0xE0, 0xFF, 0x83, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xFE,
0x0F, 0xF8, 0x3F, 0xE0, 0xFF, 0x83, 0xFE, 0x0F, 0xF8, 0x3F, 0xE0, 0xFF,
0x83, 0xFE, 0x0F, 0x78, 0x7D, 0xF3, 0xF7, 0xFF, 0xCF, 0xEF, 0x1F, 0x3C,
0xF0, 0x7F, 0xC1, 0xEF, 0x07, 0xBC, 0x1E, 0x78, 0x79, 0xE1, 0xE7, 0x87,
0x9E, 0x1C, 0x78, 0x71, 0xE3, 0xC7, 0x8F, 0x0E, 0x3C, 0x3C, 0xF0, 0xF3,
0xC3, 0xCE, 0x0F, 0x38, 0x3C, 0xE0, 0xF3, 0x81, 0xDE, 0x07, 0x78, 0x1F,
0xE0, 0x7F, 0x01, 0xFC, 0x07, 0xF0, 0x1F, 0xC0, 0x3F, 0x00, 0xFC, 0x03,
0xF0, 0x0F, 0x80, 0xF0, 0x78, 0x7F, 0x87, 0xC3, 0xFC, 0x3E, 0x1F, 0xE1,
0xF0, 0xF7, 0x0F, 0x87, 0xB8, 0x7C, 0x39, 0xC3, 0xE1, 0xCE, 0x1F, 0x8E,
0x79, 0xDC, 0x73, 0xCE, 0xE7, 0x9E, 0x77, 0x3C, 0xF3, 0xB9, 0xE3, 0x9D,
0xCF, 0x1C, 0xE6, 0x70, 0xE7, 0x3B, 0x87, 0x71, 0xDC, 0x3B, 0x8E, 0xE1,
0xDC, 0x77, 0x0F, 0xE3, 0xB8, 0x7F, 0x1D, 0xC3, 0xF8, 0xFE, 0x0F, 0x83,
0xE0, 0x7C, 0x1F, 0x03, 0xE0, 0xF8, 0x1F, 0x07, 0xC0, 0xF8, 0x3E, 0x07,
0xC1, 0xF0, 0x3E, 0x0F, 0x81, 0xE0, 0x7C, 0x00, 0xF0, 0x7B, 0xC1, 0xE7,
0x87, 0x9E, 0x3C, 0x78, 0xF0, 0xF3, 0xC3, 0xCE, 0x0F, 0x78, 0x1F, 0xE0,
0x7F, 0x01, 0xFC, 0x03, 0xF0, 0x0F, 0x80, 0x3E, 0x00, 0xF8, 0x03, 0xE0,
0x0F, 0x80, 0x7F, 0x01, 0xFC, 0x07, 0xF0, 0x3D, 0xE0, 0xF7, 0x83, 0xDE,
0x1E, 0x3C, 0x78, 0xF1, 0xE3, 0xCF, 0x07, 0xBC, 0x1E, 0xF0, 0x7C, 0xF0,
0x3F, 0xC0, 0xFF, 0x07, 0xFE, 0x1E, 0x78, 0x79, 0xE1, 0xE7, 0x87, 0x9E,
0x1E, 0x78, 0x79, 0xE1, 0xC3, 0x87, 0x0F, 0x3C, 0x3C, 0xF0, 0xF3, 0xC3,
0xCF, 0x0F, 0x3C, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0xB8, 0x1F, 0xE0,
0x7F, 0x80, 0xFC, 0x03, 0xF0, 0x0F, 0xC0, 0x3F, 0x00, 0xFC, 0x03, 0xF0,
0x07, 0x80, 0x1E, 0x00, 0xF8, 0x1F, 0xE0, 0x7F, 0x01, 0xFC, 0x00, 0x7F,
0xEF, 0xFD, 0xFF, 0xBF, 0xF0, 0x1E, 0x07, 0xC0, 0xF0, 0x1E, 0x07, 0xC0,
0xF0, 0x1E, 0x03, 0xC0, 0xF8, 0x1E, 0x03, 0xC0, 0xF8, 0x1E, 0x03, 0xC0,
0xF8, 0x1F, 0x03, 0xC0, 0x78, 0x1F, 0x03, 0xC0, 0x78, 0x1F, 0xFF, 0xFF,
0xFF, 0xFF, 0xFE, 0x07, 0xC3, 0xF1, 0xFC, 0x7E, 0x1E, 0x07, 0x81, 0xE0,
0x78, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x1E, 0x07, 0x81, 0xE0, 0xF8, 0x7E,
0x3F, 0x0F, 0x83, 0xF0, 0xFE, 0x0F, 0x81, 0xE0, 0x78, 0x1E, 0x07, 0x81,
0xE0, 0x78, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x1E, 0x07, 0xC1, 0xFC, 0x3F,
0x07, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0xF8, 0x3F, 0x0F,
0xC1, 0xF8, 0x3E, 0x07, 0x81, 0xE0, 0x78, 0x1E, 0x07, 0x81, 0xE0, 0x78,
0x1E, 0x07, 0x81, 0xE0, 0x78, 0x1F, 0x83, 0xF0, 0x7C, 0x3F, 0x1F, 0xC7,
0x81, 0xE0, 0x78, 0x1E, 0x07, 0x81, 0xE0, 0x78, 0x1E, 0x07, 0x81, 0xE0,
0x78, 0x3E, 0x0F, 0x8F, 0xC3, 0xF0, 0xF8, 0x00, 0x1E, 0x07, 0x9F, 0xF3,
0xDF, 0xFF, 0xDF, 0xFF, 0xEF, 0xFF, 0xE1, 0x03, 0xE0};
const GFXglyph Antonio_SemiBold20pt7bGlyphs[] PROGMEM = { const GFXglyph Antonio_SemiBold20pt7bGlyphs[] PROGMEM = {
{0, 1, 1, 8, 0, 0}, // 0x20 ' ' {0, 1, 1, 8, 0, 0}, // 0x20 ' '
@ -477,8 +284,13 @@ const GFXglyph Antonio_SemiBold20pt7bGlyphs[] PROGMEM = {
{4461, 10, 37, 14, 2, -33}, // 0x7D '}' {4461, 10, 37, 14, 2, -33}, // 0x7D '}'
{4508, 17, 6, 21, 2, -21}}; // 0x7E '~' {4508, 17, 6, 21, 2, -21}}; // 0x7E '~'
const GFXfont Antonio_SemiBold20pt7b PROGMEM = { // Font properties
(uint8_t *)Antonio_SemiBold20pt7bBitmaps, static constexpr FontData Antonio_SemiBold20pt7b_Properties = {
(GFXglyph *)Antonio_SemiBold20pt7bGlyphs, 0x20, 0x7E, 51}; Antonio_SemiBold20pt7bBitmaps_Gzip,
Antonio_SemiBold20pt7bGlyphs,
// Approx. 5193 bytes sizeof(Antonio_SemiBold20pt7bBitmaps_Gzip),
4521, // Original size
0x20, // First char
0x7E, // Last char
51 // yAdvance
};

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,97 @@
#pragma once #pragma once
#include "antonio-semibold20.h" #include <Adafruit_GFX.h>
//#include "antonio-semibold30.h" #include <Arduino.h>
#include "antonio-semibold40.h" #include <rom/miniz.h>
#include "antonio-semibold90.h"
#include "sats-symbol.h"
//#include "icons.h"
// #include "oswald-20.h" // Font metadata structure
// #include "oswald-30.h" struct FontData {
// #include "oswald-90.h" const uint8_t* compressedData;
// #include "ubuntu-italic40.h" const GFXglyph* glyphs;
// #include "ubuntu-italic60.h" const size_t compressedSize;
// #include "ubuntu-italic70.h" const size_t originalSize;
const uint16_t first;
const uint16_t last;
const uint8_t yAdvance;
};
// Font name constants
namespace FontNames {
static const String ANTONIO = "antonio";
static const String OSWALD = "oswald";
static const std::array<String, 2> AVAILABLE_FONTS = {
ANTONIO,
OSWALD
};
static const std::array<String, 2>& getAvailableFonts() {
return AVAILABLE_FONTS;
}
}
class FontLoader {
public:
static GFXfont* loadCompressedFont(const FontData& fontData) {
return loadCompressedFont(
fontData.compressedData,
fontData.glyphs,
fontData.compressedSize,
fontData.originalSize,
fontData.first,
fontData.last,
fontData.yAdvance
);
}
static GFXfont* loadCompressedFont(
const uint8_t* compressedData,
const GFXglyph* glyphs,
const size_t compressedSize,
const size_t originalSize,
const uint16_t first,
const uint16_t last,
const uint8_t yAdvance)
{
uint8_t* decompressedData = (uint8_t*)malloc(originalSize);
if (!decompressedData) {
Serial.println(F("Failed to allocate memory for font decompression"));
return nullptr;
}
size_t decompressedSize = originalSize;
if (GzipDecompressor::decompressData(compressedData,
compressedSize,
decompressedData,
&decompressedSize))
{
GFXfont* font = (GFXfont*)malloc(sizeof(GFXfont));
if (!font) {
free(decompressedData);
Serial.println(F("Failed to allocate memory for font structure"));
return nullptr;
}
font->bitmap = decompressedData;
font->glyph = (GFXglyph*)glyphs;
font->first = first;
font->last = last;
font->yAdvance = yAdvance;
return font;
}
Serial.println(F("Font decompression failed"));
free(decompressedData);
return nullptr;
}
static void unloadFont(GFXfont* font) {
if (font) {
if (font->bitmap) {
free((void*)font->bitmap);
}
free(font);
}
}
};

323
src/fonts/oswald-medium20.h Normal file
View file

@ -0,0 +1,323 @@
#pragma once
#include <Adafruit_GFX.h>
#include <Arduino.h>
#include "fonts.hpp"
const uint8_t Oswald_Medium20pt7bBitmaps_Gzip[] = {
0x1f,0x8b,0x08,0x00,0xf7,0xa4,0x71,0x67,0x02,0xff,0xad,0x58,
0xcf,0x8e,0xdb,0xb8,0x19,0xa7,0xa0,0xa2,0xbc,0x04,0xe1,0xb5,
0x87,0x81,0x98,0x37,0xe8,0x1e,0xb3,0x80,0x22,0xf6,0x51,0xda,
0x37,0x48,0xb1,0x87,0x3a,0x18,0x45,0xd4,0xc0,0x07,0xdf,0x56,
0x2f,0xb0,0x88,0x5f,0x63,0x81,0xa6,0x19,0x1a,0x2e,0xe0,0x4b,
0xb1,0x7e,0x81,0x36,0xa6,0xa1,0x83,0x6f,0x2b,0x1a,0x3e,0x98,
0x86,0x39,0x64,0x7f,0x94,0xec,0x19,0x4f,0x32,0x49,0x16,0x45,
0xc5,0xb1,0xf4,0xf1,0xff,0xc7,0xef,0xef,0x8f,0x43,0x42,0x7c,
0x8e,0xff,0xde,0x7c,0xff,0xcb,0x0f,0x3f,0x6d,0xbe,0xff,0xd7,
0x8b,0x24,0x56,0xbd,0x5c,0xbe,0xe9,0x5e,0x6d,0x7e,0xde,0x67,
0xed,0x4f,0x3f,0xfc,0x48,0x58,0x43,0x27,0xe3,0xc5,0xac,0x55,
0x5b,0x3d,0x32,0xf9,0xa8,0xc8,0xb3,0x8c,0x31,0xd1,0xcf,0x34,
0x5b,0x6d,0xcd,0x68,0x94,0xe7,0x32,0xdc,0x86,0x55,0xe8,0xc2,
0xae,0x9d,0xad,0xb5,0x31,0x23,0x9b,0x8f,0xb2,0x9c,0x65,0xb4,
0xa1,0x8b,0xf1,0x7c,0xd6,0x6a,0x52,0x93,0x3f,0x91,0x17,0xc4,
0x27,0x61,0x16,0xb6,0xc1,0xba,0xca,0xf1,0x8a,0xdd,0xd2,0xd5,
0xb8,0x53,0xa9,0x49,0x1c,0x09,0x44,0x28,0x66,0xd2,0x33,0x81,
0x96,0x8a,0x88,0x9c,0x79,0x1a,0xc6,0x61,0x76,0xd4,0x77,0xd6,
0x49,0x2f,0x3c,0x0b,0xa9,0x27,0x7f,0x20,0xcf,0xc8,0xef,0x49,
0x4a,0x58,0x9d,0x11,0x66,0x69,0x4d,0x43,0xa2,0xd2,0xa0,0x5f,
0x13,0x6b,0x73,0x92,0x17,0x8c,0x30,0x3a,0xae,0x53,0xb0,0x41,
0xc0,0x19,0xc9,0xb3,0x2b,0x34,0x34,0x24,0x05,0xff,0xc4,0x58,
0x4d,0x8a,0xe2,0x25,0x61,0x21,0x23,0x49,0x58,0x4c,0xac,0x58,
0xcf,0x43,0xaa,0xb6,0x41,0x93,0x7c,0x64,0xc9,0xf3,0x22,0x23,
0x69,0x33,0x51,0x49,0xbb,0x36,0x64,0x64,0x4b,0x72,0x55,0x70,
0xc2,0xd8,0x44,0xa5,0x73,0x34,0x68,0x3b,0x22,0x23,0x8c,0xc8,
0x68,0xa8,0xe9,0x4d,0x50,0x89,0x12,0x86,0x18,0x6a,0xa8,0x4e,
0x03,0x09,0x86,0x7b,0xda,0x6a,0x5b,0x72,0x36,0x5e,0x9b,0xb2,
0x60,0x93,0x7a,0x6b,0x78,0x9e,0x74,0x44,0xe8,0xd4,0xe2,0x30,
0xac,0x4e,0x6d,0x1e,0x26,0x1f,0xb6,0xfb,0x95,0xd8,0x6e,0x3e,
0x14,0xfb,0xb9,0xb7,0x72,0x4a,0x77,0xba,0x2c,0xf9,0x26,0xf8,
0x10,0x96,0xce,0x06,0x06,0x91,0xfe,0xfa,0xab,0xa6,0x8c,0x8b,
0xaa,0x2c,0xed,0x97,0x9e,0xb2,0x2c,0x85,0xe0,0x54,0x97,0x82,
0xcf,0x35,0x36,0xc3,0x9e,0x33,0x63,0xe3,0x77,0x02,0x46,0xef,
0x89,0x81,0x62,0xcd,0x72,0x31,0x57,0xac,0x7e,0x99,0xa8,0x1f,
0xff,0xf8,0x37,0x6c,0xd0,0x5a,0x56,0x7b,0x36,0x3a,0xb6,0x3f,
0x3e,0xfb,0x73,0x5a,0x53,0xf2,0x9c,0x5c,0x91,0x97,0xe4,0x75,
0xaf,0x50,0x7f,0x5f,0x25,0x3a,0x51,0x68,0xf8,0xe9,0xd7,0x3d,
0xc1,0xc7,0xe1,0x57,0x93,0x51,0xaa,0xae,0x88,0x66,0xf8,0xd6,
0x79,0xa2,0x9f,0x93,0xd7,0x14,0x5f,0x95,0x41,0x08,0xe4,0x65,
0x8a,0xef,0xd0,0x75,0x95,0xe0,0x7b,0xee,0xa2,0x86,0x41,0x6b,
0x4d,0x58,0x84,0xdd,0xfa,0x68,0xde,0x5a,0xe1,0x38,0xea,0xbd,
0x56,0x83,0x0e,0x46,0x7e,0xa1,0x61,0x45,0x97,0x8b,0x0d,0x0c,
0xc5,0x48,0xc3,0x4d,0x3a,0xdb,0xde,0x45,0xe6,0x2a,0x4e,0x67,
0xa6,0xfc,0xca,0xcb,0x70,0x27,0xbc,0xf4,0x52,0x3a,0x61,0xf9,
0xa9,0x08,0x22,0x48,0x01,0xf9,0x3b,0xe2,0x12,0x9b,0x9a,0x14,
0xea,0xa2,0x8a,0x29,0x5e,0xf3,0x5a,0xa0,0x39,0x76,0x40,0xfa,
0x7d,0x61,0x46,0x58,0x09,0x3b,0x73,0x95,0x15,0xf6,0xb4,0x02,
0x89,0xf3,0xab,0xd4,0xa5,0x16,0xc5,0xc1,0xfa,0xaa,0xbe,0xe5,
0x7e,0x75,0xb0,0x5b,0x05,0x8f,0x0d,0x1d,0x37,0xc4,0x13,0x51,
0x33,0x4d,0x6d,0xe2,0x49,0xa8,0x85,0xce,0x2c,0xfd,0x90,0x74,
0xf5,0x4e,0xe7,0x36,0x2b,0x68,0x33,0x6e,0xb5,0xb1,0x79,0xd4,
0x0d,0x28,0x1b,0xce,0x0f,0xb8,0x83,0x6d,0x68,0x62,0xef,0x89,
0x33,0x3f,0xc1,0x5b,0xf2,0x50,0x0e,0x26,0x58,0xe8,0xe1,0xc4,
0x1d,0xe1,0xe7,0x72,0xe6,0xc4,0x15,0xd2,0xf7,0x07,0x37,0x83,
0xcc,0x27,0x61,0xd1,0xee,0xf4,0x01,0x32,0x2f,0x7a,0x67,0x22,
0xbc,0x66,0xff,0xa4,0xa1,0x0d,0x87,0x68,0x6d,0x15,0x8f,0xce,
0xf7,0x48,0xe6,0xd3,0x5e,0xe6,0xeb,0x41,0xe6,0x67,0xe6,0x5c,
0xaa,0x59,0x5d,0x24,0x96,0x2a,0x4e,0xca,0xc4,0x30,0x55,0x10,
0x9b,0x6a,0x5e,0x83,0xa6,0x4a,0x44,0xfa,0xa1,0x3b,0x35,0xac,
0x66,0x26,0xb2,0x22,0x7d,0x55,0xb9,0xe2,0x2c,0x43,0x27,0xca,
0xa2,0xac,0xb0,0x87,0x1d,0xfa,0xca,0xe2,0x42,0x3f,0xa7,0x11,
0xe0,0xdd,0x0b,0xc7,0xa0,0x9d,0xd3,0x98,0xca,0xdd,0xeb,0xe0,
0x61,0x14,0x54,0x1b,0x64,0x10,0x2b,0x36,0xbd,0x38,0xbd,0xb8,
0x3c,0x7b,0xb4,0x55,0x12,0x9f,0x34,0x46,0xa7,0xfb,0x1a,0x21,
0xd1,0xbc,0xaf,0xfe,0xfa,0xee,0x35,0x79,0xf1,0x8c,0x8e,0xb7,
0x90,0xc0,0x42,0x1b,0x9e,0x3a,0xd1,0x7a,0xae,0x2c,0x25,0x2f,
0x86,0xf3,0x9a,0x61,0x70,0xef,0x0a,0xf5,0x33,0x82,0xf3,0x06,
0x0a,0xcd,0x4a,0x62,0x04,0xdb,0xf9,0xd0,0x68,0xfb,0x1c,0x91,
0x8c,0x69,0xb9,0x0d,0x7b,0x44,0xbb,0x00,0x41,0xe3,0x58,0x42,
0x95,0xc4,0x26,0x91,0x77,0xd8,0x94,0x83,0x18,0x20,0x99,0x92,
0x98,0x44,0xa7,0x8a,0xd6,0xc3,0x8a,0xbd,0xf1,0x51,0xcd,0x14,
0x41,0x68,0x43,0xfc,0x51,0x84,0x07,0x8b,0x1f,0x4c,0x06,0xb6,
0x28,0xa0,0x77,0x4e,0x08,0xcc,0x8d,0xe4,0x3c,0xd9,0x34,0x2c,
0x39,0x2e,0x9a,0x71,0x68,0x17,0x37,0xc1,0xec,0x67,0xd6,0x5c,
0xcf,0xec,0xe8,0x95,0xb6,0x79,0x66,0x6c,0xc1,0x46,0xb0,0x74,
0x57,0xb0,0xb1,0xcf,0xe8,0xcc,0xb3,0x54,0x75,0xf4,0x46,0xef,
0xc7,0xca,0x5c,0xc3,0xf5,0xdf,0x18,0xeb,0x72,0x2b,0x7d,0x9e,
0xf3,0xce,0x65,0x74,0xef,0xd8,0x8d,0xb1,0xa9,0x22,0x24,0x89,
0x27,0x2b,0xa3,0x1c,0xf0,0x63,0x08,0xc7,0x69,0x98,0x2a,0x9c,
0x53,0x83,0x07,0x83,0x58,0xa8,0x48,0x6a,0x88,0x44,0x0f,0x42,
0xb0,0x84,0x16,0x13,0xc4,0x61,0xcd,0x5c,0xb2,0xaf,0x0b,0x43,
0x57,0xe4,0xa8,0x8b,0x92,0x2e,0x6b,0x6b,0x38,0x4f,0x5b,0xed,
0x86,0x28,0x27,0x11,0xaf,0xda,0xe0,0x64,0x68,0x6e,0xb6,0x46,
0x14,0x69,0xa7,0xde,0x5a,0x76,0x9b,0xec,0x95,0x08,0x5a,0xa0,
0x6f,0x17,0x9c,0x93,0xb7,0x68,0x3e,0x9a,0xc2,0x21,0x74,0x0f,
0x04,0xbf,0x0d,0xcb,0x30,0x87,0x2d,0x07,0xc1,0x3a,0x15,0x8c,
0x70,0x08,0xe0,0x41,0xc1,0xe8,0x06,0x02,0xf6,0x17,0x42,0x1c,
0xd3,0x06,0xb8,0x52,0x0c,0x0c,0xdb,0xe0,0x2a,0x3e,0xa5,0xa7,
0x75,0x6e,0xd3,0xd0,0xcf,0xc2,0x49,0x18,0x72,0x07,0x79,0x20,
0xb0,0x45,0xec,0xea,0xc7,0x74,0xea,0x60,0xab,0xc8,0x20,0x2c,
0x5c,0x98,0x4b,0x7e,0xe8,0xb0,0xe9,0x69,0xf0,0x37,0x89,0xd9,
0xd1,0xc0,0x58,0xc4,0xc0,0xcf,0xc9,0x2b,0x4e,0x2a,0xbe,0xff,
0xa0,0x53,0x38,0x2c,0xbf,0xbc,0x6c,0xec,0x3f,0xe1,0xd1,0xa3,
0xaa,0xd4,0xf0,0xda,0x51,0x2d,0x12,0xcb,0x50,0x09,0x2b,0x0f,
0x33,0xba,0x1d,0x2a,0x8f,0x7a,0x50,0x39,0x9f,0xdd,0xe3,0xec,
0xe9,0x23,0x9e,0x2f,0x8e,0x2c,0x03,0x47,0x68,0x85,0x80,0x2e,
0x64,0x88,0x31,0x77,0x16,0x8e,0xb2,0xf4,0xdb,0xf0,0x5e,0x2c,
0x3f,0x11,0xef,0x67,0xc4,0x03,0x77,0x5f,0x1e,0x13,0x89,0xf0,
0xc4,0xf3,0x8d,0x78,0x3c,0x33,0x4e,0x60,0xde,0xd1,0x21,0xf8,
0xb2,0xd5,0xcd,0xce,0x54,0x15,0x5b,0xce,0xb6,0xb6,0x12,0x6c,
0xd2,0x1a,0x58,0xd1,0x74,0x8c,0xb6,0xd8,0x05,0x05,0x61,0xa3,
0x3a,0x58,0x1e,0xd2,0xa3,0xc6,0xb0,0xc5,0xcc,0x62,0xee,0x64,
0x1d,0x57,0xc0,0xd8,0x38,0x11,0x1a,0xe5,0xb7,0xe9,0x1e,0x23,
0x3f,0x11,0xff,0x37,0x3f,0x27,0x6e,0x2b,0x72,0x67,0x31,0x9f,
0xad,0x94,0x94,0xc9,0x9d,0xa3,0x7b,0xc3,0x57,0x2a,0xc8,0xf1,
0x9d,0x67,0x7b,0x2b,0x56,0xba,0xbb,0x9e,0xfc,0xa3,0xcb,0xf6,
0xef,0xaf,0x3f,0x6e,0xbb,0xeb,0xc5,0xab,0x4d,0xbe,0x7f,0xb7,
0xfb,0x78,0xbd,0x3c,0x74,0x62,0xf3,0x73,0x68,0x5a,0x2f,0x1a,
0xeb,0x45,0x6b,0xc3,0x64,0xee,0x39,0xb3,0xa5,0x58,0x9b,0x30,
0x19,0xfb,0xe7,0xcc,0x32,0x47,0x7d,0x1a,0x6e,0x7a,0xdb,0xb5,
0xc2,0xf3,0x10,0xa3,0x6f,0x1b,0xd5,0x12,0x3e,0xca,0x4e,0xec,
0x57,0x87,0x0e,0x81,0x5b,0x40,0x57,0x0d,0x2c,0x7f,0x7d,0x8e,
0xb8,0x51,0x55,0x35,0x64,0x0b,0x03,0x65,0x17,0xda,0xbe,0xfd,
0x6d,0x86,0xf9,0x40,0xdc,0x45,0x4b,0x6f,0x4e,0x96,0x6e,0x04,
0xc8,0x1d,0xd2,0x84,0xb8,0x4d,0x3f,0xb5,0xf4,0x5b,0xd6,0x85,
0x63,0xcc,0x21,0xa0,0x1f,0xbb,0xce,0x67,0xc4,0xff,0x8b,0x9f,
0x98,0xd4,0x54,0x12,0x73,0x1b,0x25,0x44,0x45,0x3f,0xbc,0xe7,
0x2e,0x3c,0x38,0xff,0x30,0xf5,0xc4,0x1d,0x34,0xce,0x16,0xed,
0xd6,0xba,0x42,0xb0,0xe5,0x7c,0x67,0x9c,0xe3,0x82,0x76,0xf0,
0xc3,0xea,0xec,0xfc,0x9c,0xe9,0x21,0x53,0xfb,0xca,0x9e,0xb2,
0x0c,0x2c,0x0c,0xe9,0x57,0x22,0x76,0x09,0x8d,0x0c,0x88,0x5c,
0x9d,0x0c,0x0d,0x62,0x74,0x9f,0x85,0xbc,0x88,0x18,0x32,0x66,
0x99,0x7b,0x03,0xd6,0x08,0xdb,0x69,0x04,0x04,0x35,0x07,0x8b,
0x7d,0x14,0xff,0x56,0xf5,0xb7,0xcb,0xe1,0x53,0x62,0xff,0x48,
0x32,0x96,0x02,0x0c,0xd7,0x6f,0x0d,0x2f,0xd9,0x2a,0xdd,0xa8,
0x83,0x29,0x0a,0xd6,0x4c,0xda,0x01,0xbf,0xd1,0xc9,0x6c,0xab,
0x4b,0xcb,0x0b,0x04,0xdd,0x9d,0x2e,0x0c,0x7b,0x1f,0x31,0xa6,
0x8a,0x27,0x03,0xb8,0x90,0x35,0x8f,0xa0,0x72,0x50,0x96,0xcd,
0xa8,0xa7,0x37,0xfb,0x1b,0xfd,0x46,0xdb,0xa2,0x14,0x6c,0xca,
0xe6,0x9b,0xb9,0x39,0x18,0x17,0x2b,0x0d,0x9b,0xcf,0x17,0xd6,
0x1c,0x90,0x73,0xe5,0x94,0x4d,0x37,0xf3,0xcd,0xf7,0xe6,0xf0,
0x4b,0x99,0xed,0x3e,0xd2,0xf7,0xfb,0x9b,0xee,0x95,0x96,0x4b,
0xcb,0xd7,0x9e,0x5a,0x79,0xe3,0xb9,0x16,0x13,0xc3,0xd6,0x80,
0x3a,0x12,0xee,0xaa,0x44,0xf4,0x1d,0x38,0x10,0x52,0x19,0xce,
0x9f,0xea,0x12,0xe6,0x64,0x2d,0xe2,0xff,0x1c,0x49,0x88,0xd3,
0x8d,0x2a,0x2d,0x9b,0x42,0x77,0xdc,0xa6,0x31,0x89,0xd1,0x1e,
0xe4,0x2a,0xb0,0x25,0x09,0x20,0x1b,0x98,0x54,0xdc,0xa5,0x5d,
0xfd,0xd6,0xf6,0x29,0xc3,0x02,0xbc,0xcd,0xb5,0xeb,0xa7,0x68,
0x04,0x95,0xae,0x3e,0x18,0xa8,0x75,0x40,0xcc,0x73,0xdd,0x2f,
0x8c,0x83,0x5a,0xe4,0x9a,0xa0,0x99,0xed,0xd3,0x4f,0xbf,0x16,
0x8b,0xc8,0x88,0x13,0xe0,0xa2,0x12,0xf0,0x28,0x89,0x06,0x45,
0xd5,0x65,0x93,0xf4,0xe1,0x0e,0x76,0x03,0x38,0x12,0xb3,0x6f,
0x89,0x3c,0xab,0x23,0x59,0x0c,0x24,0xeb,0x49,0x77,0x22,0x49,
0x24,0xa1,0x43,0x76,0x11,0xc3,0x1e,0x63,0xe6,0x2f,0x11,0xc1,
0x9b,0x14,0xf7,0x0d,0xbc,0xae,0x00,0x77,0xcf,0x2f,0x7d,0x7a,
0xe5,0xe7,0x17,0x01,0x2e,0x3e,0xbd,0x5e,0x26,0xfd,0x36,0xda,
0x8c,0xf2,0x0c,0x37,0x92,0xd9,0x57,0x09,0x19,0x3c,0xb0,0x82,
0x06,0x40,0x30,0x11,0x41,0x65,0xa3,0x6c,0x54,0x94,0x79,0x0e,
0xdc,0x54,0xd8,0x73,0xe4,0x05,0xec,0xcf,0x8b,0xec,0x39,0x83,
0xf1,0x6b,0x44,0x97,0xcd,0x72,0xc5,0x3e,0xf0,0x12,0xe7,0x4c,
0x23,0xee,0x69,0xf7,0xcb,0xe6,0x1d,0xaa,0xf6,0xad,0x39,0xb6,
0x11,0x9b,0x2c,0x37,0x4d,0x53,0x7f,0x66,0xc0,0xdb,0xb6,0xdb,
0xa0,0x73,0x75,0x02,0x2e,0x46,0x6a,0x58,0xe3,0x38,0xd0,0x87,
0xea,0x3c,0x74,0xab,0xf0,0xc1,0x97,0x30,0x01,0xa4,0x96,0x5b,
0x6f,0x11,0xb0,0x26,0x9e,0x5b,0x19,0x91,0x5e,0x0f,0xf8,0xfa,
0x1a,0x1a,0x43,0x21,0x6d,0x98,0x79,0x12,0x41,0xf4,0x19,0xde,
0xf4,0xa8,0x3a,0x8f,0x0a,0xc1,0xd3,0x78,0x78,0x9d,0x95,0x06,
0x2b,0xcc,0x31,0x9c,0x5d,0x56,0x97,0x01,0xf7,0xc5,0xe9,0xed,
0x79,0x93,0xe3,0x69,0x93,0xe1,0xa4,0x3d,0xaa,0x3c,0x6d,0xe2,
0x87,0x4d,0xe8,0xda,0xc9,0x29,0xb2,0x05,0x00,0x4d,0x3f,0x06,
0x17,0x27,0x90,0x05,0x55,0x36,0xda,0xe6,0x25,0x09,0xc4,0x75,
0x63,0xff,0x12,0xde,0x07,0x5f,0x54,0x9f,0xeb,0xb2,0x77,0x48,
0x84,0x5c,0x8d,0xbb,0x49,0x0d,0x07,0x94,0x08,0xd7,0xb3,0x60,
0xb4,0x7b,0xcd,0xde,0x03,0xac,0xc7,0x00,0x84,0x38,0x3d,0x0e,
0x4f,0x88,0x6e,0xb7,0xe9,0x31,0x5f,0x3c,0xd5,0xd3,0xa2,0xbb,
0xaf,0x0e,0xb8,0xf3,0xb3,0x74,0xa9,0x78,0x13,0x03,0x1b,0x10,
0xd8,0x40,0xb8,0x4a,0x3c,0x41,0x40,0x30,0x9d,0xbe,0x44,0xfe,
0xa7,0x12,0x91,0x6e,0x85,0xe2,0xac,0x3d,0xd8,0xa3,0x39,0x02,
0xd5,0x44,0xf4,0x1e,0x8c,0x3b,0xdd,0x0a,0x51,0x62,0x3c,0xec,
0x23,0x62,0xf8,0xc2,0x83,0x61,0x47,0xe7,0x4f,0x30,0xa6,0xe2,
0xf0,0xe4,0xff,0xe5,0xef,0xb0,0x3b,0x02,0xb8,0x40,0x73,0xec,
0x2b,0x1a,0xee,0xab,0xf5,0xa7,0x56,0xf4,0xd9,0x27,0x88,0x41,
0xc1,0x76,0xbb,0xdf,0x45,0x50,0x3d,0x79,0x52,0xa2,0xb1,0xba,
0x0e,0x7b,0x58,0xae,0x2c,0xca,0x47,0x81,0x99,0x64,0x95,0xec,
0xaf,0x87,0x5f,0xe3,0x66,0x13,0xa2,0x47,0xac,0xfa,0x2b,0xdf,
0xbd,0xad,0x5a,0x29,0x3a,0xcc,0xf4,0xf2,0x09,0x23,0x02,0x89,
0x68,0x86,0x45,0xb8,0x2f,0xaf,0xd7,0x61,0xcc,0x49,0x95,0xb8,
0xd4,0x51,0x0b,0x07,0x8d,0x46,0xfe,0x9d,0x9c,0x85,0xc9,0xea,
0xe4,0x00,0xe7,0x59,0xfd,0x15,0xda,0x3d,0xb1,0x96,0xa0,0x2e,
0x34,0xc6,0x3f,0x71,0xfa,0x4f,0x64,0x21,0xc3,0xdd,0xdd,0x7e,
0xa3,0x5c,0xef,0xc6,0xeb,0xfd,0xbc,0x9b,0x34,0x53,0x5e,0xe0,
0xc4,0xf6,0xa0,0x77,0xb3,0x4d,0x2f,0x0c,0xb0,0x80,0x50,0xad,
0x64,0xed,0xe3,0x51,0x88,0x19,0xc9,0xf9,0xdc,0x17,0x7c,0x6b,
0xf6,0x4d,0xf3,0xbe,0x7c,0xd3,0xee,0x96,0xfc,0xdd,0x1b,0xb7,
0xeb,0x36,0xd3,0xbf,0xe5,0xd7,0x7f,0x6f,0xff,0xe3,0xb3,0x8d,
0x09,0x62,0xb2,0x75,0xa2,0xd1,0x4e,0xd2,0x16,0x57,0x99,0x1b,
0x5b,0xb2,0xb1,0x2e,0x71,0x67,0xe3,0x65,0x56,0x14,0x45,0x2e,
0x72,0x5e,0x72,0x1b,0x97,0x8e,0xff,0xcf,0x78,0x08,0x44,0x08,
0x45,0xc8,0x23,0x79,0x35,0x04,0x22,0x8b,0x41,0x28,0xa0,0xf3,
0x22,0xce,0x2a,0xf2,0xac,0xcc,0x62,0xd3,0x88,0x8d,0xd8,0x79,
0x76,0x5f,0x52,0x3d,0x14,0xa1,0x50,0x10,0x36,0xe4,0x31,0x62,
0x5d,0xc3,0x81,0x6e,0x81,0x72,0x55,0x91,0x3e,0x26,0xa1,0x1c,
0x8c,0x50,0x74,0xb1,0x75,0xb8,0x15,0x8c,0x91,0xc8,0x1e,0x5e,
0xbc,0x41,0xde,0x10,0x8f,0xdb,0xf0,0x72,0x7c,0x61,0x5c,0xf2,
0x94,0x85,0x0b,0xd6,0xe2,0x37,0x07,0x72,0x3c,0xbf,0x00,0x45,
0x31,0x9e,0x2d,0xb6,0x26,0x26,0x9b,0x8b,0x0e,0xe0,0xb9,0x66,
0x4e,0xf8,0xef,0xc4,0x2f,0x21,0x48,0xff,0xc2,0x91,0xef,0xfe,
0x0b,0x06,0x62,0xc2,0xe3,0x11,0x13,0x00,0x00,
};
const GFXglyph Oswald_Medium20pt7bGlyphs[] PROGMEM = {
{ 0, 1, 1, 10, 0, 0 }, // 0x20 ' '
{ 1, 5, 32, 9, 2, -31 }, // 0x21 '!'
{ 21, 11, 11, 12, 1, -31 }, // 0x22 '"'
{ 37, 17, 32, 20, 2, -31 }, // 0x23 '#'
{ 105, 17, 39, 19, 1, -34 }, // 0x24 '$'
{ 188, 34, 32, 37, 2, -31 }, // 0x25 '%'
{ 324, 19, 32, 23, 2, -31 }, // 0x26 '&'
{ 400, 4, 11, 6, 1, -31 }, // 0x27 '''
{ 406, 8, 39, 13, 3, -31 }, // 0x28 '('
{ 445, 9, 39, 12, 1, -31 }, // 0x29 ')'
{ 489, 13, 14, 16, 2, -31 }, // 0x2A '*'
{ 512, 15, 16, 17, 1, -23 }, // 0x2B '+'
{ 542, 5, 10, 8, 2, -4 }, // 0x2C ','
{ 549, 10, 3, 12, 1, -12 }, // 0x2D '-'
{ 553, 5, 5, 9, 2, -4 }, // 0x2E '.'
{ 557, 13, 32, 16, 1, -31 }, // 0x2F '/'
{ 609, 17, 32, 21, 2, -31 }, // 0x30 '0'
{ 677, 10, 32, 15, 1, -31 }, // 0x31 '1'
{ 717, 16, 32, 19, 2, -31 }, // 0x32 '2'
{ 781, 16, 32, 19, 2, -31 }, // 0x33 '3'
{ 845, 18, 32, 20, 1, -31 }, // 0x34 '4'
{ 917, 16, 32, 19, 2, -31 }, // 0x35 '5'
{ 981, 17, 32, 20, 2, -31 }, // 0x36 '6'
{ 1049, 14, 32, 16, 1, -31 }, // 0x37 '7'
{ 1105, 16, 32, 20, 2, -31 }, // 0x38 '8'
{ 1169, 16, 32, 20, 2, -31 }, // 0x39 '9'
{ 1233, 6, 18, 9, 2, -20 }, // 0x3A ':'
{ 1247, 6, 25, 10, 2, -21 }, // 0x3B ';'
{ 1266, 11, 17, 15, 2, -24 }, // 0x3C '<'
{ 1290, 13, 11, 17, 2, -21 }, // 0x3D '='
{ 1308, 12, 17, 15, 2, -24 }, // 0x3E '>'
{ 1334, 15, 32, 19, 2, -31 }, // 0x3F '?'
{ 1394, 33, 37, 36, 2, -31 }, // 0x40 '@'
{ 1547, 19, 32, 20, 1, -31 }, // 0x41 'A'
{ 1623, 18, 32, 22, 2, -31 }, // 0x42 'B'
{ 1695, 18, 32, 21, 2, -31 }, // 0x43 'C'
{ 1767, 18, 32, 22, 2, -31 }, // 0x44 'D'
{ 1839, 14, 32, 17, 2, -31 }, // 0x45 'E'
{ 1895, 13, 32, 16, 2, -31 }, // 0x46 'F'
{ 1947, 18, 32, 22, 2, -31 }, // 0x47 'G'
{ 2019, 18, 32, 23, 2, -31 }, // 0x48 'H'
{ 2091, 5, 32, 11, 3, -31 }, // 0x49 'I'
{ 2111, 10, 33, 13, 0, -31 }, // 0x4A 'J'
{ 2153, 19, 32, 21, 2, -31 }, // 0x4B 'K'
{ 2229, 14, 32, 16, 2, -31 }, // 0x4C 'L'
{ 2285, 22, 32, 27, 2, -31 }, // 0x4D 'M'
{ 2373, 17, 32, 21, 2, -31 }, // 0x4E 'N'
{ 2441, 18, 32, 22, 2, -31 }, // 0x4F 'O'
{ 2513, 18, 32, 21, 2, -31 }, // 0x50 'P'
{ 2585, 18, 38, 22, 2, -31 }, // 0x51 'Q'
{ 2671, 18, 32, 22, 2, -31 }, // 0x52 'R'
{ 2743, 16, 32, 19, 2, -31 }, // 0x53 'S'
{ 2807, 15, 32, 17, 1, -31 }, // 0x54 'T'
{ 2867, 18, 32, 22, 2, -31 }, // 0x55 'U'
{ 2939, 18, 32, 20, 1, -31 }, // 0x56 'V'
{ 3011, 26, 32, 28, 1, -31 }, // 0x57 'W'
{ 3115, 19, 32, 20, 0, -31 }, // 0x58 'X'
{ 3191, 19, 32, 19, 0, -31 }, // 0x59 'Y'
{ 3267, 15, 32, 17, 1, -31 }, // 0x5A 'Z'
{ 3327, 9, 39, 13, 2, -31 }, // 0x5B '['
{ 3371, 13, 32, 16, 1, -31 }, // 0x5C '\'
{ 3423, 9, 39, 12, 1, -31 }, // 0x5D ']'
{ 3467, 16, 13, 18, 1, -31 }, // 0x5E '^'
{ 3493, 14, 4, 14, 0, 3 }, // 0x5F '_'
{ 3500, 8, 8, 11, 2, -31 }, // 0x60 '`'
{ 3508, 15, 23, 17, 1, -22 }, // 0x61 'a'
{ 3552, 15, 32, 19, 2, -31 }, // 0x62 'b'
{ 3612, 14, 23, 17, 2, -22 }, // 0x63 'c'
{ 3653, 15, 32, 19, 2, -31 }, // 0x64 'd'
{ 3713, 14, 23, 17, 2, -22 }, // 0x65 'e'
{ 3754, 11, 31, 12, 1, -30 }, // 0x66 'f'
{ 3797, 18, 31, 18, 1, -23 }, // 0x67 'g'
{ 3867, 15, 32, 19, 2, -31 }, // 0x68 'h'
{ 3927, 6, 31, 10, 2, -30 }, // 0x69 'i'
{ 3951, 9, 37, 10, -1, -30 }, // 0x6A 'j'
{ 3993, 16, 32, 18, 2, -31 }, // 0x6B 'k'
{ 4057, 6, 32, 10, 2, -31 }, // 0x6C 'l'
{ 4081, 24, 23, 28, 2, -22 }, // 0x6D 'm'
{ 4150, 15, 23, 19, 2, -22 }, // 0x6E 'n'
{ 4194, 14, 23, 18, 2, -22 }, // 0x6F 'o'
{ 4235, 15, 30, 19, 2, -22 }, // 0x70 'p'
{ 4292, 15, 30, 19, 2, -22 }, // 0x71 'q'
{ 4349, 11, 23, 14, 2, -22 }, // 0x72 'r'
{ 4381, 14, 23, 16, 1, -22 }, // 0x73 's'
{ 4422, 11, 29, 13, 1, -28 }, // 0x74 't'
{ 4462, 14, 23, 19, 2, -22 }, // 0x75 'u'
{ 4503, 15, 23, 16, 0, -22 }, // 0x76 'v'
{ 4547, 21, 23, 23, 1, -22 }, // 0x77 'w'
{ 4608, 16, 23, 16, 0, -22 }, // 0x78 'x'
{ 4654, 16, 29, 16, 0, -22 }, // 0x79 'y'
{ 4712, 13, 23, 15, 1, -22 }, // 0x7A 'z'
{ 4750, 10, 40, 13, 2, -31 }, // 0x7B '{'
{ 4800, 4, 38, 10, 3, -31 }, // 0x7C '|'
{ 4819, 10, 40, 14, 2, -31 }, // 0x7D '}'
{ 4869, 16, 6, 18, 1, -18 } }; // 0x7E '~'
// const GFXfont Oswald_Medium20pt7b PROGMEM = {
// (uint8_t *)Oswald_Medium20pt7bBitmaps,
// (GFXglyph *)Oswald_Medium20pt7bGlyphs,
// 0x20, 0x7E, 58 };
// // Approx. 5553 bytes
// Font properties
static constexpr FontData Oswald_Medium20pt7b_Properties = {
Oswald_Medium20pt7bBitmaps_Gzip,
Oswald_Medium20pt7bGlyphs,
sizeof(Oswald_Medium20pt7bBitmaps_Gzip),
5553, // Original size
0x20, // First char
0x7E, // Last char
58 // yAdvance
};

497
src/fonts/oswald-medium30.h Normal file
View file

@ -0,0 +1,497 @@
#pragma once
#include <Adafruit_GFX.h>
#include <Arduino.h>
#include "fonts.hpp"
const uint8_t Oswald_Medium30pt7bBitmaps_Gzip[] = {
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcd, 0x5a,
0xbb, 0x72, 0x23, 0x3b, 0x92, 0x05, 0xb7, 0x8c, 0x72, 0x26, 0x1a, 0x63,
0x5e, 0x63, 0xe2, 0x62, 0x3f, 0x61, 0xcc, 0x36, 0xee, 0x08, 0xf7, 0x53,
0xf6, 0x13, 0xc6, 0x6c, 0x43, 0x4b, 0x94, 0x82, 0x86, 0xbc, 0xd1, 0x27,
0xf0, 0x53, 0x04, 0x86, 0x0c, 0x99, 0xfc, 0x83, 0x65, 0x31, 0x68, 0xd0,
0x14, 0x14, 0x32, 0x08, 0x86, 0x4a, 0xc8, 0x3d, 0x27, 0xc1, 0xa7, 0xd4,
0x52, 0xf7, 0xbd, 0xdd, 0x3b, 0xb1, 0x25, 0x50, 0x55, 0x85, 0x42, 0xe1,
0x91, 0x48, 0x9c, 0x3c, 0x99, 0x28, 0x23, 0x3c, 0x0a, 0x8f, 0xf1, 0xee,
0xb8, 0xac, 0xc7, 0x6f, 0x7a, 0x7c, 0xd6, 0xc3, 0x18, 0x23, 0xbb, 0xa3,
0x78, 0x71, 0x72, 0xb3, 0xb8, 0x7f, 0x58, 0x6d, 0x96, 0xcf, 0xe9, 0x25,
0x8f, 0x87, 0x8b, 0xb1, 0xbb, 0xb0, 0xd3, 0x76, 0x3e, 0x59, 0xcf, 0x56,
0xfd, 0x63, 0xff, 0x25, 0xfd, 0x66, 0x46, 0x83, 0xeb, 0x42, 0xdb, 0xbb,
0x49, 0x6e, 0xfb, 0x61, 0x94, 0xbd, 0x19, 0xdb, 0xe8, 0x26, 0x09, 0x37,
0x0d, 0x6e, 0x8a, 0x8b, 0xbe, 0x4d, 0x76, 0x96, 0x9b, 0x34, 0x36, 0x28,
0xe9, 0xdb, 0x25, 0x2a, 0x0e, 0x22, 0x0b, 0x91, 0x8d, 0xc8, 0x0b, 0x6e,
0x76, 0xaf, 0x15, 0x1b, 0xfd, 0x35, 0x4a, 0x0e, 0x28, 0x69, 0xf7, 0x3d,
0x90, 0x5b, 0x91, 0x07, 0x91, 0x67, 0xf4, 0xa5, 0xd6, 0x79, 0x6c, 0xc0,
0xb2, 0x81, 0x80, 0x3a, 0xd1, 0x40, 0x7f, 0xda, 0x40, 0xef, 0xee, 0x72,
0x9b, 0x0a, 0x4b, 0x9a, 0xbf, 0x19, 0xf3, 0xc9, 0x98, 0xd6, 0x98, 0xa6,
0x33, 0xa3, 0x68, 0xda, 0xc1, 0x38, 0xe9, 0x9c, 0xf4, 0x4e, 0xb2, 0x43,
0x95, 0x22, 0x76, 0x90, 0x79, 0x76, 0xeb, 0xdc, 0x3e, 0x0d, 0xcd, 0x4b,
0x19, 0x15, 0x31, 0x12, 0xba, 0x70, 0x1b, 0xff, 0xe6, 0x92, 0xb1, 0x83,
0x69, 0x8a, 0x19, 0x49, 0x67, 0xa4, 0x37, 0x7e, 0x30, 0x56, 0x4c, 0x23,
0xd1, 0x48, 0x7a, 0x73, 0xd3, 0xd6, 0x62, 0x01, 0xc5, 0xf0, 0x5a, 0xfe,
0xd2, 0xc8, 0x30, 0x12, 0x54, 0x25, 0x5d, 0x90, 0x58, 0x9b, 0xb3, 0xcf,
0x43, 0xbb, 0x95, 0xeb, 0x21, 0x50, 0xaa, 0xc5, 0x4a, 0x69, 0xa4, 0x18,
0x24, 0x37, 0x18, 0x93, 0x8c, 0xf9, 0xfc, 0xaa, 0xaf, 0x68, 0x1a, 0xb9,
0xa8, 0xfd, 0xd2, 0x98, 0x20, 0xc6, 0x76, 0xe8, 0x78, 0x42, 0x16, 0xdf,
0xba, 0xe4, 0x0c, 0xf5, 0xcc, 0xa2, 0xb0, 0x0c, 0xc4, 0xdc, 0x21, 0x6b,
0xdc, 0x64, 0x66, 0xc5, 0x30, 0x62, 0x96, 0xed, 0xc7, 0x78, 0x71, 0x34,
0x20, 0xcb, 0xf6, 0xfe, 0x2a, 0xa1, 0xc6, 0x16, 0x02, 0x32, 0xbe, 0x2b,
0xc8, 0x6a, 0x93, 0x9b, 0xa1, 0xfa, 0xd2, 0x64, 0x64, 0xd9, 0x18, 0x6c,
0xc4, 0x73, 0xcb, 0x2c, 0x3f, 0xc9, 0x17, 0xe8, 0x44, 0x66, 0x16, 0x1a,
0x5e, 0x22, 0xcb, 0x4a, 0xbe, 0x68, 0x30, 0x18, 0x4c, 0x10, 0x86, 0x21,
0xb3, 0x1c, 0xa4, 0x93, 0xfe, 0x02, 0xd9, 0xa3, 0xd8, 0xde, 0x61, 0x98,
0x66, 0x94, 0xc3, 0x0d, 0x0a, 0x5e, 0xd8, 0xbe, 0xe0, 0xdd, 0xbb, 0xec,
0x22, 0xb3, 0x9a, 0xac, 0x59, 0x01, 0x59, 0xb3, 0xa1, 0xed, 0x91, 0xe5,
0x3b, 0x0c, 0x16, 0xea, 0xe0, 0xd0, 0x83, 0x58, 0x38, 0x40, 0x14, 0x65,
0xa7, 0xd0, 0x5f, 0x4c, 0x4e, 0xd4, 0x57, 0xb2, 0xbe, 0xe2, 0xd1, 0x9f,
0xde, 0x60, 0x2e, 0x67, 0x78, 0x25, 0x37, 0x03, 0xb2, 0x1c, 0xfa, 0xc3,
0xac, 0x56, 0x98, 0x45, 0xa9, 0x20, 0xab, 0xc5, 0x1c, 0xa0, 0x6e, 0xdf,
0x1b, 0xde, 0x72, 0x1a, 0x32, 0x24, 0xd5, 0x43, 0xfa, 0x9d, 0x45, 0x39,
0xb1, 0xa9, 0x4d, 0xbe, 0xf3, 0x57, 0x03, 0xeb, 0x63, 0xcd, 0x68, 0xa3,
0x41, 0xab, 0x2e, 0x16, 0x68, 0x5c, 0x4f, 0x0d, 0x72, 0xf7, 0xc9, 0x14,
0x6f, 0x9a, 0xe7, 0x6c, 0xec, 0x02, 0x93, 0x98, 0x75, 0x2e, 0x6d, 0xa6,
0xb0, 0x4c, 0xc3, 0x1e, 0x27, 0x37, 0x91, 0x6e, 0xf0, 0xa5, 0x5d, 0x49,
0xf2, 0x6e, 0x31, 0xdb, 0x96, 0xb1, 0x9b, 0xaf, 0x72, 0x09, 0x76, 0xbd,
0x4e, 0xc1, 0xfb, 0x2b, 0xd9, 0x0e, 0xad, 0xdc, 0xf4, 0x1e, 0xdd, 0x94,
0x55, 0x6e, 0xa1, 0xf4, 0xae, 0x2f, 0x23, 0xb9, 0x86, 0x92, 0xb9, 0xa5,
0x14, 0xe8, 0xf1, 0x13, 0x7e, 0x37, 0xf2, 0x34, 0x78, 0xb1, 0x3d, 0xf4,
0xc1, 0xfc, 0xd3, 0x7c, 0xa6, 0x7a, 0x2f, 0x6e, 0xc7, 0x2f, 0xcf, 0xeb,
0xf9, 0xf4, 0xe2, 0x32, 0x99, 0x2f, 0x36, 0x16, 0x9b, 0xe4, 0x66, 0x08,
0x6d, 0x46, 0x87, 0x51, 0x03, 0xba, 0xce, 0xb3, 0xe0, 0x7e, 0x86, 0x47,
0x61, 0x72, 0xc8, 0xc4, 0x48, 0x38, 0x0a, 0xbd, 0xd9, 0x3f, 0xa9, 0xc5,
0xf6, 0x8f, 0xeb, 0xbf, 0x62, 0x07, 0xdf, 0x0b, 0xc5, 0xfb, 0x2b, 0x56,
0x0f, 0xcb, 0x66, 0x1f, 0x71, 0xef, 0xf4, 0x7d, 0x3e, 0xd9, 0x55, 0x32,
0x9c, 0x55, 0xf2, 0xb5, 0x9b, 0x93, 0x86, 0xfd, 0x8c, 0x67, 0x3c, 0x9c,
0x0c, 0xe1, 0xae, 0xf0, 0x51, 0x0f, 0xfd, 0x1d, 0x74, 0x22, 0x0c, 0xd4,
0xcf, 0x76, 0xbf, 0x5c, 0x7e, 0x7e, 0x5a, 0xcd, 0x65, 0x1c, 0x38, 0xc8,
0x7b, 0x49, 0x58, 0x20, 0x0e, 0x93, 0x42, 0x6d, 0x7f, 0x4e, 0x76, 0x1e,
0x87, 0x71, 0xbb, 0xcc, 0x9f, 0xac, 0xf9, 0xfb, 0x7f, 0x72, 0x2a, 0x9b,
0x9e, 0x0a, 0x7e, 0x71, 0x7e, 0xd9, 0xc8, 0xe9, 0x91, 0xde, 0x16, 0x38,
0x5c, 0xee, 0xcb, 0xb4, 0xad, 0x75, 0x17, 0x97, 0x5f, 0xfe, 0x7e, 0xf2,
0xd6, 0xfe, 0x82, 0x65, 0x2f, 0x8d, 0xeb, 0x4e, 0x4f, 0xb6, 0x6b, 0xd2,
0xab, 0xd3, 0x18, 0xa7, 0xd1, 0xe9, 0x29, 0xe2, 0x74, 0xf1, 0xea, 0xd4,
0xc6, 0x51, 0x3e, 0x3d, 0xa1, 0x27, 0xfe, 0xec, 0xe4, 0x28, 0x08, 0x23,
0x1d, 0x74, 0xb1, 0xe1, 0x3a, 0x97, 0x2b, 0x91, 0xa5, 0x76, 0x66, 0x90,
0x49, 0x09, 0x51, 0x7c, 0x0c, 0x0b, 0x60, 0x42, 0xc2, 0x12, 0x82, 0x32,
0xa3, 0xcc, 0x11, 0x2c, 0xbc, 0xfc, 0xd0, 0x83, 0x97, 0xd2, 0x14, 0x19,
0x49, 0x98, 0x89, 0x7f, 0x84, 0xbe, 0xc9, 0xb5, 0xc8, 0x4c, 0x24, 0x2a,
0x0a, 0x45, 0xe2, 0x15, 0x30, 0xd1, 0x8c, 0xb1, 0x16, 0x72, 0x78, 0x3a,
0x91, 0xee, 0x3d, 0xa6, 0xdf, 0xf5, 0xe8, 0x56, 0x27, 0xa3, 0xd2, 0x0c,
0x6d, 0xfe, 0x73, 0xb7, 0x26, 0x44, 0x8c, 0x19, 0x5d, 0x1b, 0x69, 0xb3,
0x18, 0x73, 0xc6, 0x98, 0xd1, 0x19, 0x8c, 0xf9, 0x36, 0x7a, 0x8c, 0xf9,
0x21, 0xd9, 0x4d, 0x6e, 0x9f, 0x0f, 0x58, 0x4b, 0x31, 0xe1, 0x2d, 0xe3,
0x23, 0x97, 0x30, 0x12, 0x60, 0x97, 0xc8, 0x4b, 0x14, 0x65, 0x6a, 0x33,
0x31, 0xbb, 0x55, 0x20, 0xae, 0x09, 0x4a, 0x54, 0x13, 0xdf, 0xac, 0x09,
0xef, 0xef, 0xaa, 0x50, 0x7b, 0xb1, 0x50, 0x93, 0xb1, 0x51, 0xab, 0x01,
0xfb, 0x02, 0x94, 0x8d, 0x87, 0x99, 0x98, 0x68, 0xaf, 0xb6, 0x58, 0x64,
0x72, 0x55, 0xd0, 0xe3, 0xdb, 0x2a, 0xbe, 0xde, 0x42, 0x45, 0x59, 0x66,
0x18, 0x01, 0xc1, 0x50, 0x77, 0xe8, 0x34, 0xb1, 0xe2, 0x86, 0xaf, 0x03,
0xe8, 0xa3, 0xc2, 0x3b, 0x0d, 0x08, 0x40, 0xb8, 0x63, 0x5f, 0xd1, 0x4b,
0xf4, 0x6c, 0xff, 0x86, 0x74, 0x5e, 0xb1, 0x5e, 0xab, 0xda, 0x0d, 0x70,
0xc4, 0x91, 0xfb, 0xc4, 0x99, 0xb8, 0xdf, 0xcf, 0x04, 0x9a, 0x8b, 0x44,
0x2d, 0xa8, 0x30, 0x2d, 0x8c, 0xda, 0x0d, 0x0c, 0x97, 0x23, 0x8b, 0x04,
0x1f, 0x35, 0x2c, 0xbd, 0x09, 0x6a, 0xaa, 0x1a, 0x58, 0x80, 0xff, 0xc6,
0xda, 0xb9, 0x8d, 0xcd, 0x26, 0x9b, 0x01, 0x0b, 0x68, 0xda, 0xb7, 0xab,
0xc1, 0x0c, 0xa1, 0x73, 0x37, 0xa9, 0x5d, 0x16, 0x33, 0x78, 0x98, 0x4d,
0x98, 0x5e, 0x81, 0x1d, 0xee, 0xb1, 0x20, 0x5b, 0x5d, 0xfa, 0x0a, 0x0b,
0x3d, 0xd6, 0x29, 0x16, 0xa9, 0xbc, 0x3d, 0x12, 0xa5, 0x05, 0xd9, 0x2a,
0x68, 0xf6, 0x0a, 0x72, 0x18, 0x2e, 0xc6, 0xc3, 0xc1, 0x9c, 0x3e, 0x0a,
0x98, 0xbe, 0xaf, 0x26, 0xbc, 0x00, 0x45, 0x3a, 0x4f, 0xa5, 0x26, 0x1f,
0x0b, 0x75, 0x9d, 0x53, 0xaf, 0xa9, 0x40, 0xe9, 0x45, 0x67, 0xbb, 0xe8,
0x84, 0xbf, 0x4d, 0xf2, 0x3a, 0x69, 0x79, 0x4c, 0x90, 0xb6, 0xe5, 0x69,
0xab, 0xa1, 0xe0, 0x3d, 0xa5, 0x46, 0xdb, 0x57, 0x95, 0x8c, 0x4b, 0x60,
0xbf, 0xb0, 0xf6, 0xd3, 0x09, 0x2d, 0x9c, 0xf6, 0x6e, 0x5e, 0x57, 0x43,
0xe6, 0xb8, 0x8e, 0xf3, 0x89, 0x61, 0xb9, 0xd5, 0x60, 0x37, 0x72, 0xfd,
0x2c, 0x2b, 0xea, 0x00, 0xfb, 0x26, 0xa1, 0x97, 0xdb, 0xde, 0xeb, 0x6a,
0xdc, 0x29, 0x00, 0x7b, 0x29, 0xe7, 0xd3, 0x59, 0xf5, 0x75, 0x40, 0xa7,
0x30, 0x9d, 0x80, 0xc5, 0x1b, 0x55, 0xa4, 0xd3, 0x85, 0xa5, 0x88, 0xf4,
0x56, 0xce, 0xb4, 0x4a, 0x6d, 0x42, 0xeb, 0xe8, 0x89, 0x7b, 0x75, 0x53,
0x28, 0xef, 0x7a, 0x13, 0x47, 0x5f, 0xbd, 0x01, 0xa0, 0x1c, 0x6e, 0xda,
0x93, 0x1b, 0x73, 0x72, 0x23, 0x87, 0x1b, 0x2c, 0x89, 0x56, 0xbb, 0x0b,
0xb2, 0xe1, 0x29, 0x35, 0x62, 0x33, 0x2c, 0x00, 0x12, 0x05, 0x7a, 0x2e,
0xdf, 0x80, 0x34, 0xd2, 0xa7, 0x93, 0xc1, 0xaf, 0x07, 0xf2, 0x13, 0x0a,
0xe0, 0xfc, 0xf5, 0x59, 0x7d, 0xf7, 0xf8, 0x62, 0xd9, 0xa7, 0xb3, 0xda,
0xae, 0x84, 0xf5, 0x90, 0xd6, 0xf0, 0xad, 0x5a, 0x55, 0xb3, 0x9b, 0x2a,
0x60, 0x60, 0x3a, 0x4c, 0x55, 0xcf, 0xa9, 0x02, 0x03, 0xda, 0x4d, 0xd5,
0x11, 0x0f, 0x5e, 0x23, 0xda, 0x11, 0x28, 0x4a, 0xf3, 0x22, 0x4d, 0x09,
0x6b, 0x81, 0x16, 0x4f, 0x75, 0x11, 0x71, 0xbe, 0x03, 0xf8, 0xda, 0xdd,
0xeb, 0xb9, 0xc5, 0x7a, 0x3c, 0x81, 0x16, 0xac, 0xbc, 0x6e, 0x37, 0x55,
0x6f, 0x30, 0xf0, 0x38, 0x55, 0xe6, 0x70, 0xec, 0x18, 0x67, 0x2a, 0xbb,
0xc3, 0x9c, 0x1f, 0x7b, 0x42, 0xfc, 0xe9, 0xd3, 0xaf, 0x17, 0xc3, 0x97,
0x7f, 0x1a, 0xf3, 0x3b, 0x2c, 0xf8, 0x25, 0xa6, 0x22, 0xc4, 0x90, 0x3c,
0xc6, 0xed, 0x8b, 0x2f, 0x16, 0xe0, 0x0b, 0x2b, 0xe8, 0x23, 0x2e, 0xa4,
0x0f, 0x05, 0xf2, 0x0c, 0xe0, 0x05, 0x28, 0xd3, 0xd2, 0xc2, 0x80, 0xd9,
0xfd, 0x7e, 0xa2, 0x1b, 0xa7, 0x4d, 0x9c, 0xdb, 0x3a, 0x68, 0xd3, 0x67,
0xda, 0xa3, 0x0c, 0xf9, 0x79, 0x48, 0xf3, 0x8a, 0x0b, 0xe0, 0x0a, 0x33,
0x03, 0x3e, 0x03, 0x23, 0x64, 0x93, 0x85, 0x94, 0x1d, 0xe4, 0x71, 0x03,
0x35, 0xec, 0x50, 0xa6, 0x43, 0xe1, 0xce, 0x80, 0xb0, 0x01, 0x4c, 0x4e,
0x27, 0x11, 0x93, 0xc2, 0x34, 0x91, 0x30, 0x3a, 0x9b, 0x32, 0x88, 0xe6,
0xcb, 0x61, 0xf9, 0x51, 0x75, 0x0a, 0x97, 0x0a, 0x52, 0xab, 0xa0, 0x0b,
0x00, 0x06, 0xba, 0x01, 0xfc, 0x38, 0x7d, 0x0a, 0xbb, 0xc3, 0x57, 0xd2,
0xe9, 0xf1, 0xd5, 0x02, 0xb5, 0xcc, 0xb8, 0x8e, 0x8f, 0xd7, 0x2d, 0x66,
0x41, 0x25, 0x3d, 0xe8, 0x35, 0x99, 0x1b, 0xfe, 0x67, 0xc2, 0x69, 0x2b,
0x0a, 0x84, 0xa4, 0x71, 0xe0, 0x5c, 0x99, 0x8d, 0x43, 0xb8, 0x9e, 0xc5,
0xc1, 0xda, 0xc8, 0x30, 0x21, 0x42, 0xa8, 0x7e, 0x03, 0x80, 0x06, 0xfd,
0x87, 0x0a, 0x2e, 0x26, 0x29, 0x90, 0x10, 0x8e, 0xc1, 0xff, 0x21, 0x9f,
0x6b, 0xac, 0x60, 0xf0, 0xb3, 0x0c, 0xbe, 0x73, 0x95, 0x87, 0x66, 0xb0,
0xf1, 0xc2, 0xc3, 0x3a, 0x25, 0x3b, 0x8d, 0xc0, 0xc3, 0x66, 0x9d, 0x40,
0x8c, 0xcd, 0x16, 0xdc, 0xd2, 0xc5, 0x0b, 0x30, 0x65, 0xb8, 0x24, 0xd3,
0x0e, 0x4b, 0xba, 0x59, 0x83, 0x12, 0x8e, 0xf1, 0x00, 0x8c, 0xad, 0x1b,
0x83, 0xe2, 0xa0, 0xc0, 0x2d, 0xca, 0xe7, 0xf6, 0x01, 0x0f, 0xca, 0xe8,
0x09, 0x0f, 0xc0, 0x5a, 0xf5, 0x01, 0x78, 0x1e, 0xe8, 0xb6, 0xbb, 0x21,
0xc0, 0x82, 0xbd, 0x80, 0xaa, 0xc1, 0xe5, 0xc0, 0xcc, 0xcd, 0xb2, 0xeb,
0x80, 0x27, 0x28, 0xe3, 0xc0, 0xf4, 0x50, 0x27, 0x66, 0xa6, 0x43, 0xdf,
0xee, 0x40, 0x41, 0x7b, 0x90, 0x5a, 0x4e, 0x24, 0x8f, 0xa0, 0xff, 0xa1,
0xa7, 0x14, 0x48, 0xcd, 0xc2, 0x60, 0x99, 0x95, 0xaa, 0x8c, 0x6a, 0xa9,
0x6c, 0x7e, 0xad, 0xea, 0x08, 0x2f, 0x40, 0x21, 0x04, 0x76, 0x00, 0x42,
0xd2, 0x1b, 0x55, 0x54, 0x55, 0x1c, 0x50, 0x6c, 0x43, 0x71, 0xf1, 0x2d,
0x47, 0xb4, 0x06, 0x33, 0xc6, 0xa5, 0xcf, 0xea, 0x95, 0x38, 0xae, 0x8c,
0xac, 0xe4, 0xd8, 0x63, 0xfa, 0x80, 0xf7, 0x0e, 0xcc, 0x16, 0x5a, 0xdd,
0x6e, 0x30, 0xb7, 0xb7, 0x5d, 0xf3, 0x02, 0xa0, 0x99, 0x03, 0x83, 0x7c,
0xd7, 0x3e, 0x42, 0x63, 0x6f, 0x7a, 0x32, 0x6c, 0xb7, 0xca, 0xf0, 0xd7,
0xfa, 0x36, 0x09, 0xdd, 0x80, 0x66, 0x50, 0x16, 0x6b, 0x40, 0x02, 0xc1,
0xfc, 0x22, 0x0c, 0x0a, 0xc6, 0x0a, 0xc5, 0x82, 0xc6, 0x41, 0xd2, 0xb8,
0xf4, 0x8a, 0x82, 0xb0, 0x3a, 0x60, 0xb4, 0x26, 0x5c, 0xe3, 0x05, 0x1f,
0xdd, 0x0a, 0x2f, 0xa0, 0x8a, 0x0c, 0x4b, 0xb5, 0x86, 0xfe, 0xfa, 0x88,
0x35, 0x6c, 0xb0, 0xa0, 0x47, 0x9c, 0x75, 0xc7, 0x85, 0x8e, 0x45, 0x41,
0xff, 0x87, 0x0b, 0xb9, 0xf8, 0x7b, 0xb9, 0xcf, 0x2d, 0xcc, 0x1a, 0x60,
0xdb, 0x3e, 0x61, 0x6e, 0x61, 0xd6, 0xe6, 0xa9, 0xd9, 0x16, 0x13, 0xb0,
0x8a, 0xd6, 0xa8, 0xb2, 0x3e, 0x5a, 0x61, 0x46, 0xc3, 0x95, 0xd4, 0xb7,
0xd0, 0x30, 0x20, 0x83, 0x68, 0x42, 0x07, 0x6e, 0x35, 0xb4, 0x78, 0x2b,
0xcc, 0x53, 0xfb, 0x82, 0xb7, 0x6e, 0x23, 0x40, 0x80, 0xca, 0xee, 0x45,
0xfd, 0x04, 0xa1, 0xb7, 0x97, 0xe8, 0x34, 0xd5, 0x47, 0x0d, 0xf1, 0x6c,
0xd1, 0x7b, 0x90, 0x1f, 0x2c, 0x0d, 0xa2, 0x03, 0x49, 0x8a, 0xe7, 0x80,
0x8a, 0x0a, 0x51, 0xd1, 0x0d, 0x58, 0x41, 0xb3, 0x89, 0xe6, 0xd6, 0xc5,
0x6d, 0x09, 0xfb, 0x80, 0x25, 0x2c, 0xab, 0xac, 0x3e, 0x1b, 0x2d, 0xc3,
0x9b, 0x1b, 0x2e, 0x13, 0xb3, 0xa3, 0x05, 0x1f, 0xdc, 0xbc, 0x5f, 0x01,
0x61, 0x0b, 0x6c, 0x21, 0x4c, 0xb3, 0x5f, 0x55, 0xe3, 0x89, 0xc1, 0x35,
0x15, 0x10, 0x39, 0x8f, 0x0e, 0x95, 0x60, 0xa6, 0x88, 0xae, 0x14, 0x7b,
0x52, 0xef, 0x5c, 0xe6, 0xf0, 0x2c, 0x20, 0x00, 0x0c, 0xea, 0x36, 0x02,
0x33, 0xdb, 0xea, 0x77, 0x2a, 0xd4, 0xfd, 0xec, 0x1b, 0x20, 0x6b, 0x6d,
0x47, 0x1b, 0xad, 0x5d, 0x54, 0x4f, 0x56, 0x1a, 0x15, 0xf4, 0x5b, 0x8e,
0x21, 0xd5, 0x40, 0xbd, 0xf7, 0x0f, 0xd2, 0xcf, 0x27, 0xff, 0x3e, 0x2e,
0x7c, 0xf2, 0xda, 0x47, 0x47, 0x04, 0x82, 0x41, 0xf5, 0xa0, 0xce, 0x30,
0x0b, 0x36, 0x01, 0xd8, 0xfc, 0x79, 0x16, 0xed, 0xbe, 0x97, 0x35, 0x6d,
0xff, 0x14, 0xa2, 0x7b, 0xa7, 0xd4, 0x47, 0x59, 0xd4, 0x92, 0x20, 0x27,
0x33, 0x01, 0x3d, 0x72, 0x83, 0x5f, 0x01, 0xfc, 0x43, 0xe7, 0xa1, 0x87,
0x50, 0x78, 0x4c, 0xa4, 0x7b, 0xa0, 0x07, 0x63, 0x54, 0x6f, 0x28, 0xc6,
0x8e, 0xcb, 0x9c, 0xaa, 0x00, 0x68, 0xb3, 0x59, 0x09, 0x2c, 0x34, 0x02,
0xfc, 0xd1, 0xec, 0x2a, 0xa1, 0x24, 0x67, 0xaa, 0x8c, 0x5f, 0xd3, 0x5e,
0x89, 0x56, 0xf6, 0x8a, 0x6d, 0x9f, 0x07, 0xaa, 0x8a, 0x7f, 0x18, 0x2c,
0x29, 0xcc, 0x54, 0x9e, 0x1f, 0x45, 0xc6, 0x4e, 0xe6, 0x33, 0x79, 0xcc,
0xe8, 0x8a, 0xf9, 0x8d, 0xb8, 0xfe, 0x71, 0x1d, 0xdf, 0xf3, 0xe8, 0xbd,
0xe3, 0x4f, 0x57, 0x78, 0xfe, 0xe8, 0x0f, 0x1e, 0x4d, 0x69, 0xe1, 0xb6,
0xb8, 0xe4, 0x7b, 0x90, 0xb0, 0xab, 0x1f, 0xb8, 0x1d, 0xe9, 0x8d, 0xdf,
0xc0, 0x4b, 0x98, 0x0a, 0x5c, 0xf6, 0x04, 0x44, 0x23, 0x44, 0x51, 0xac,
0x15, 0x4a, 0xec, 0x23, 0x26, 0x13, 0x5c, 0x12, 0xfe, 0x3b, 0x40, 0x1e,
0x90, 0x48, 0x07, 0x98, 0x5e, 0x30, 0x30, 0xff, 0x0a, 0xb7, 0xfe, 0x1e,
0xbc, 0x1a, 0x6f, 0xf9, 0xde, 0xad, 0x07, 0xc5, 0xa8, 0xdb, 0xfe, 0x14,
0x77, 0x14, 0x43, 0x1c, 0x17, 0x38, 0x96, 0x2f, 0x98, 0xcc, 0x16, 0x4b,
0xdb, 0xf7, 0xfe, 0x0e, 0x6f, 0xa1, 0xfd, 0x56, 0x1d, 0x6f, 0x56, 0x48,
0xd7, 0xdd, 0x2f, 0xd1, 0x1d, 0x6d, 0xcb, 0x66, 0xb0, 0x16, 0xd7, 0x83,
0xa4, 0xee, 0x35, 0x09, 0x88, 0x26, 0x07, 0x44, 0xfb, 0xce, 0xe5, 0xf1,
0xc3, 0xff, 0xce, 0x0f, 0xb2, 0x80, 0x29, 0x18, 0xe6, 0x1a, 0x6c, 0x73,
0x0b, 0x3b, 0x5d, 0x02, 0xa0, 0x1c, 0x19, 0xa0, 0x64, 0x06, 0xd0, 0x18,
0x91, 0x81, 0x59, 0x9d, 0x62, 0x82, 0x91, 0xe1, 0xb7, 0x50, 0xce, 0x12,
0x80, 0xce, 0x53, 0x8c, 0x78, 0x8d, 0xc1, 0x23, 0xc3, 0x8d, 0x41, 0x82,
0x16, 0x53, 0x69, 0x36, 0x6b, 0x89, 0x2f, 0xdb, 0x97, 0xe4, 0xc7, 0xe1,
0xd2, 0x4e, 0xa7, 0x8b, 0xc9, 0x7a, 0xbd, 0x59, 0x6e, 0xb7, 0xdb, 0x3c,
0x1e, 0x8f, 0x2f, 0xdc, 0x74, 0x7a, 0x73, 0x7d, 0xbf, 0x5e, 0x3d, 0x3d,
0x6e, 0xf3, 0x30, 0x8c, 0xc7, 0xde, 0xbb, 0xe9, 0xf5, 0xfc, 0x7e, 0xbd,
0x7c, 0x5a, 0x6e, 0x73, 0x40, 0x11, 0x57, 0x90, 0x21, 0xd7, 0xeb, 0x5e,
0x90, 0xe1, 0xd3, 0x78, 0x0c, 0x12, 0x34, 0x9d, 0x48, 0x0b, 0x03, 0x3e,
0xdb, 0x32, 0x9e, 0x31, 0x86, 0x5c, 0x17, 0x57, 0xa5, 0xdd, 0xc0, 0x3e,
0xbf, 0x30, 0x88, 0x34, 0x86, 0xb1, 0x96, 0x0e, 0x04, 0xac, 0xf7, 0x70,
0x18, 0x6c, 0x84, 0x47, 0xc5, 0x54, 0xf6, 0x49, 0x6a, 0xea, 0x34, 0xc5,
0x7d, 0xea, 0x35, 0xa5, 0x7d, 0xca, 0x9a, 0xd4, 0xe5, 0x29, 0x45, 0xd3,
0x38, 0x94, 0xa0, 0xc9, 0x6b, 0x24, 0xcf, 0xed, 0x93, 0xd5, 0xd4, 0x6a,
0x6a, 0xf6, 0x49, 0xb9, 0x6f, 0x39, 0x32, 0xe7, 0xda, 0xb4, 0xaf, 0xe9,
0x35, 0x92, 0x64, 0x45, 0x92, 0xf0, 0x54, 0x91, 0x64, 0x91, 0xce, 0x14,
0xe9, 0x04, 0x49, 0xbc, 0xfc, 0xb4, 0x47, 0xaa, 0xf2, 0x31, 0xcc, 0x07,
0x97, 0xab, 0x39, 0x9d, 0xa9, 0x8d, 0x57, 0xdc, 0xb7, 0xc4, 0x2c, 0xf5,
0xce, 0x40, 0x25, 0xdf, 0x5a, 0x1d, 0x5a, 0x83, 0x50, 0xad, 0xce, 0xf3,
0x50, 0x55, 0x3f, 0x2a, 0xf3, 0x3f, 0x18, 0x8d, 0x87, 0x9d, 0xd1, 0x68,
0xe8, 0x95, 0xe3, 0x9d, 0x6a, 0xd7, 0xb2, 0xd6, 0x96, 0xc9, 0x0e, 0x45,
0x9d, 0x4d, 0x28, 0xdd, 0x9f, 0xbd, 0xf9, 0xff, 0x21, 0x41, 0x79, 0x5f,
0x82, 0x51, 0xe3, 0xbb, 0xe8, 0x70, 0xb3, 0x3b, 0x31, 0xaa, 0x68, 0x18,
0x8d, 0x62, 0xc4, 0xb7, 0x31, 0xa7, 0xa4, 0x48, 0xf6, 0xa4, 0x68, 0x26,
0xeb, 0x03, 0x29, 0xd2, 0xea, 0x3b, 0x88, 0xb2, 0x39, 0xd2, 0x1b, 0x0a,
0x7e, 0xb1, 0xe7, 0x4b, 0x80, 0x2b, 0xac, 0x32, 0x9d, 0x96, 0xeb, 0xea,
0x62, 0x41, 0x17, 0x41, 0xc5, 0x7a, 0x85, 0xab, 0x0a, 0x31, 0xb3, 0x03,
0xe6, 0xf4, 0xe1, 0x8e, 0xb1, 0x9f, 0x4e, 0x6c, 0x72, 0x4b, 0x05, 0x39,
0x0f, 0x02, 0x06, 0xe3, 0x2b, 0xf0, 0x9b, 0x6b, 0x85, 0xaf, 0x80, 0x47,
0x63, 0x2c, 0xb9, 0x7a, 0x66, 0xa9, 0x2a, 0xc2, 0x75, 0xf1, 0x4b, 0x54,
0xa6, 0x0e, 0x96, 0x9b, 0xe7, 0xf6, 0x89, 0x0e, 0x57, 0x87, 0xde, 0xfe,
0xda, 0x56, 0xc3, 0x16, 0x7a, 0x8d, 0x9d, 0xd7, 0xc8, 0x86, 0x2b, 0xfb,
0x78, 0x06, 0x15, 0x4a, 0x76, 0x19, 0xf1, 0x24, 0xe8, 0xe1, 0x13, 0x03,
0xe8, 0xb0, 0x83, 0x30, 0x69, 0xaa, 0x4c, 0x70, 0x4f, 0x19, 0x20, 0x83,
0x14, 0x80, 0x9c, 0xf4, 0xb9, 0x6f, 0x72, 0x58, 0xa9, 0x91, 0x1c, 0x9c,
0x86, 0x71, 0x30, 0xe3, 0x70, 0x19, 0x76, 0xea, 0xf9, 0x95, 0xe3, 0x10,
0x24, 0xfa, 0xf7, 0xa7, 0x9f, 0x65, 0x1d, 0xff, 0xc4, 0xa3, 0x66, 0xa7,
0x18, 0xdf, 0x5c, 0xd4, 0x1d, 0xa1, 0x1e, 0x9e, 0x80, 0x04, 0xfa, 0xcc,
0x86, 0xac, 0x7b, 0x0d, 0xb5, 0xf2, 0x1d, 0x3c, 0x00, 0x9c, 0xa2, 0xbb,
0x1b, 0x46, 0x8c, 0x2b, 0xcd, 0x06, 0x3a, 0x01, 0x3c, 0x41, 0x7d, 0xe8,
0x00, 0x10, 0x56, 0xbb, 0xd2, 0x6a, 0x88, 0x5e, 0xe0, 0x13, 0xd1, 0x05,
0x63, 0x84, 0x1d, 0xed, 0xc2, 0xd5, 0x49, 0xe8, 0x08, 0x3c, 0x89, 0x0c,
0x68, 0x9b, 0xf6, 0xcd, 0x16, 0x6c, 0x75, 0x1a, 0xd5, 0xd3, 0x58, 0xc4,
0xd1, 0x8b, 0x86, 0xb3, 0x6a, 0x74, 0x5e, 0x15, 0x6a, 0xa7, 0x0c, 0x56,
0x74, 0x97, 0x83, 0xbe, 0x4b, 0xa8, 0x6a, 0x00, 0xe5, 0xe1, 0x89, 0xbe,
0x4d, 0x61, 0x3c, 0x89, 0xb1, 0x3b, 0x0d, 0x4d, 0xd0, 0x7d, 0x47, 0xb3,
0x0c, 0x3a, 0x6e, 0x19, 0x75, 0x5c, 0x33, 0xec, 0x08, 0xf7, 0xae, 0x63,
0x28, 0x63, 0x9f, 0xe5, 0x13, 0xda, 0x96, 0xd9, 0xe0, 0x61, 0x1a, 0x52,
0x81, 0x8d, 0xb8, 0x83, 0xb1, 0x40, 0xe7, 0x98, 0x05, 0xf3, 0x91, 0x0a,
0xec, 0xc8, 0x1d, 0x0c, 0x4a, 0x68, 0x1f, 0x9f, 0x56, 0xd9, 0xdd, 0xcf,
0xe1, 0xce, 0xb9, 0x0b, 0x78, 0x76, 0x03, 0x9c, 0xbc, 0xa7, 0x27, 0xf8,
0x7b, 0xf3, 0xf9, 0xbc, 0x0f, 0xde, 0xc1, 0xeb, 0xcb, 0x97, 0x43, 0xf3,
0xbc, 0xdc, 0x24, 0x8b, 0x95, 0x15, 0x3d, 0x04, 0x89, 0x7e, 0x07, 0x2c,
0xd6, 0x9e, 0xd1, 0x28, 0xa8, 0xb0, 0x1b, 0xa0, 0xc0, 0x70, 0xff, 0x31,
0x27, 0x91, 0x33, 0x02, 0x85, 0x77, 0x20, 0xe3, 0x18, 0x81, 0x43, 0x56,
0x17, 0xfa, 0x06, 0xe0, 0x0f, 0xb2, 0x07, 0x92, 0x07, 0xa6, 0x90, 0xe8,
0xcd, 0xc7, 0x46, 0x7d, 0xf8, 0xd4, 0x68, 0x98, 0x80, 0xb1, 0x1c, 0x0f,
0xf2, 0x61, 0x1f, 0x92, 0xba, 0x3e, 0xf7, 0xea, 0x43, 0x81, 0x27, 0x34,
0xd9, 0x33, 0x78, 0x0f, 0x8f, 0x92, 0x82, 0x86, 0xc3, 0x0f, 0xd1, 0x62,
0x85, 0xb6, 0x90, 0x69, 0x80, 0x4c, 0xd5, 0x11, 0xd8, 0x0b, 0x73, 0xb4,
0x97, 0x62, 0x7f, 0x10, 0x1f, 0x61, 0x32, 0x72, 0x09, 0x32, 0x53, 0x37,
0x45, 0x52, 0x0d, 0x5b, 0xa6, 0xba, 0x70, 0x32, 0xdd, 0xab, 0x16, 0xec,
0x17, 0xac, 0xc4, 0x2e, 0x0b, 0x9d, 0x3e, 0xc7, 0x00, 0x22, 0xa0, 0x22,
0x86, 0x09, 0xf0, 0xc0, 0x77, 0xea, 0xcf, 0xa1, 0x77, 0xf7, 0x89, 0x70,
0xe3, 0x1e, 0xe8, 0xc0, 0x71, 0x63, 0x45, 0xb9, 0x3e, 0x34, 0x64, 0xe7,
0x9b, 0x55, 0x18, 0x21, 0x7f, 0x51, 0x70, 0x51, 0xc8, 0xe1, 0xbe, 0x8a,
0x53, 0x8f, 0x8d, 0x8e, 0x25, 0xd6, 0x33, 0x03, 0x0e, 0x96, 0x35, 0x0c,
0xb5, 0xf9, 0x3d, 0x18, 0x8c, 0xd8, 0xbb, 0x1d, 0x4e, 0xec, 0x7a, 0xfd,
0x01, 0x4b, 0xfe, 0x8e, 0x47, 0x81, 0xf1, 0xde, 0x8d, 0x46, 0x80, 0xf5,
0xb2, 0x67, 0x14, 0x4d, 0xc3, 0x64, 0x3e, 0x8e, 0x34, 0x52, 0xa6, 0x41,
0x38, 0x46, 0xd9, 0x18, 0x53, 0x63, 0x04, 0x8d, 0xc1, 0x33, 0x06, 0xd1,
0x34, 0x24, 0xa7, 0x01, 0xb8, 0x5a, 0xba, 0x7c, 0x58, 0x3a, 0x1f, 0x4b,
0xbf, 0xc3, 0x56, 0x75, 0xad, 0xf8, 0xee, 0xff, 0xe0, 0x54, 0xeb, 0x8f,
0xe8, 0x42, 0x57, 0xb7, 0x23, 0xea, 0x5e, 0x06, 0xae, 0x7d, 0xdd, 0xe5,
0xb0, 0x75, 0x0f, 0x02, 0xd7, 0xe3, 0xdd, 0x7e, 0xc4, 0xa1, 0xe0, 0xf0,
0x7d, 0x05, 0xe3, 0xeb, 0x82, 0xdd, 0x88, 0xaa, 0xb6, 0xc3, 0xd6, 0xa1,
0x65, 0x30, 0xe5, 0xe7, 0x9f, 0x76, 0xf5, 0x47, 0x2c, 0xfa, 0x91, 0xc6,
0xfc, 0x29, 0x6e, 0xa8, 0x6a, 0x82, 0xfa, 0x36, 0x9b, 0xbe, 0x7d, 0x88,
0x00, 0x12, 0xef, 0xba, 0x82, 0xd5, 0xea, 0x5b, 0x2e, 0xea, 0xe2, 0xba,
0x01, 0xef, 0x96, 0xe6, 0x19, 0xe6, 0x98, 0x30, 0x74, 0x7e, 0x0c, 0x02,
0xe5, 0x26, 0x00, 0xb4, 0x69, 0xdc, 0xc6, 0xc1, 0x5e, 0x25, 0xae, 0x50,
0xc6, 0x2f, 0x5a, 0x8d, 0x2a, 0xcb, 0xed, 0x3a, 0xc3, 0x6a, 0x49, 0x98,
0xbc, 0x14, 0xc0, 0xa1, 0x4e, 0x2d, 0x17, 0x26, 0xa3, 0x97, 0x9e, 0x5e,
0xad, 0x7b, 0x78, 0x2c, 0x30, 0x87, 0xe2, 0x77, 0x05, 0x16, 0x51, 0x9d,
0x5d, 0x4b, 0x43, 0xb8, 0x94, 0xb0, 0x50, 0x6d, 0x5b, 0xcc, 0x4b, 0x70,
0x8f, 0x40, 0x97, 0xf4, 0xd7, 0x5d, 0x38, 0x6d, 0x17, 0x48, 0x4b, 0x54,
0x12, 0x54, 0x1a, 0xba, 0xaf, 0x64, 0xdf, 0xe7, 0x70, 0x9b, 0x35, 0x0a,
0x0d, 0x57, 0x7a, 0x33, 0xc0, 0xf0, 0xc1, 0x03, 0x81, 0x7d, 0x67, 0x6c,
0x0e, 0xe6, 0x0b, 0x46, 0x0e, 0xc4, 0x35, 0x6b, 0x24, 0x0f, 0x53, 0x1e,
0xb9, 0xe5, 0xfa, 0x95, 0x6c, 0x46, 0x5e, 0x05, 0xcc, 0xa0, 0x7d, 0x16,
0xb7, 0x51, 0x8e, 0x35, 0x05, 0x13, 0x58, 0x0c, 0xdc, 0x8e, 0xfd, 0x0f,
0x2e, 0x2c, 0xcf, 0xb8, 0xab, 0xe5, 0x68, 0xb7, 0x82, 0x35, 0x4b, 0x6c,
0x0c, 0x80, 0x64, 0x1d, 0x41, 0x1d, 0x4c, 0xd5, 0xf7, 0x3a, 0xf2, 0xe3,
0xe5, 0xa1, 0x00, 0x07, 0xce, 0xd7, 0x4a, 0xb8, 0xcf, 0xca, 0xe0, 0xb4,
0xf9, 0x08, 0xe0, 0x6b, 0x40, 0xbf, 0xbe, 0x73, 0xb0, 0x33, 0x74, 0xa8,
0x88, 0x63, 0xc8, 0x05, 0x0e, 0xeb, 0x73, 0x71, 0x9b, 0xec, 0x8e, 0x03,
0xbe, 0x62, 0x3f, 0xc0, 0x9a, 0xc9, 0x0a, 0x1d, 0xdb, 0x05, 0x17, 0x8a,
0x1a, 0x30, 0x3e, 0xcb, 0xde, 0x97, 0xe6, 0x80, 0xc5, 0x3e, 0xab, 0xe0,
0xd6, 0x0c, 0x09, 0x03, 0xaa, 0xc4, 0xc4, 0x3a, 0xda, 0x11, 0x49, 0x38,
0x46, 0xfb, 0xb8, 0x1b, 0x6d, 0x39, 0x1f, 0xed, 0xa9, 0x72, 0x9c, 0x0f,
0x7c, 0x37, 0xda, 0x81, 0xa3, 0x5d, 0xbc, 0x33, 0x5a, 0x54, 0xd0, 0xe2,
0xcf, 0xf2, 0x2f, 0x1d, 0xff, 0x4e, 0x6b, 0x3d, 0xcd, 0xff, 0x23, 0x7f,
0xa8, 0x1d, 0xe6, 0xe4, 0xb7, 0x46, 0xe0, 0xdf, 0x28, 0x11, 0x84, 0xc9,
0xee, 0xab, 0xfd, 0xed, 0x7d, 0xdc, 0xaf, 0xff, 0xf6, 0x68, 0x7f, 0xf7,
0x68, 0x30, 0x2b, 0x2c, 0xe2, 0x96, 0x34, 0xee, 0xe4, 0x00, 0xa4, 0xc3,
0x44, 0x58, 0xda, 0x86, 0xca, 0x33, 0x49, 0x37, 0x69, 0x4f, 0x89, 0xdd,
0xd4, 0x6b, 0x06, 0xda, 0x61, 0xbf, 0xa8, 0x50, 0xd9, 0x99, 0x30, 0xe9,
0x47, 0x79, 0x6c, 0xdc, 0x3d, 0x4e, 0xc5, 0xb8, 0x1b, 0x92, 0x41, 0x65,
0x92, 0x0c, 0x6b, 0x90, 0x97, 0x82, 0xea, 0xd2, 0x59, 0x79, 0xa5, 0x1f,
0xef, 0x5e, 0x8a, 0x9f, 0x17, 0x09, 0xcf, 0x55, 0x1a, 0x5b, 0x92, 0xf2,
0x53, 0x65, 0xfb, 0xc1, 0xcb, 0x7a, 0x9c, 0x06, 0xe4, 0x3f, 0x38, 0x4e,
0xf7, 0xae, 0xf7, 0x71, 0xea, 0x6f, 0x6c, 0x71, 0xbf, 0x7b, 0xb3, 0xe4,
0x94, 0xdc, 0xbe, 0xc8, 0x43, 0x09, 0xab, 0xd2, 0x76, 0x67, 0x9b, 0x0e,
0x89, 0x8a, 0xfe, 0x66, 0xa7, 0xe9, 0xf8, 0x80, 0x8e, 0x20, 0xf7, 0xb3,
0xa0, 0xe7, 0x20, 0xe2, 0xa8, 0x17, 0x42, 0xba, 0xce, 0xa8, 0x95, 0xfb,
0x2b, 0xba, 0x17, 0xbc, 0xdf, 0x80, 0xda, 0x6f, 0xee, 0x42, 0x8b, 0x6f,
0x3b, 0x59, 0x80, 0xc1, 0x25, 0x7f, 0x97, 0x61, 0x60, 0xf1, 0x86, 0x6e,
0xb6, 0xd7, 0x4b, 0x86, 0x42, 0x41, 0xd2, 0x50, 0x15, 0xb0, 0x09, 0x6a,
0x6e, 0xeb, 0x9e, 0xc7, 0xb7, 0x25, 0xf2, 0xea, 0x30, 0x4d, 0xf7, 0xc9,
0x3e, 0x16, 0x78, 0x1a, 0x21, 0x71, 0xc7, 0xf4, 0xb6, 0x1c, 0x9f, 0x4d,
0x84, 0x32, 0xa7, 0xf4, 0x81, 0x37, 0x64, 0x05, 0xff, 0xee, 0x8b, 0x24,
0xf0, 0x6f, 0xe4, 0x61, 0x43, 0xd7, 0x91, 0xc7, 0x94, 0x7e, 0xe3, 0x4f,
0x51, 0x22, 0x5e, 0xa6, 0x1d, 0x58, 0x9c, 0x42, 0xe3, 0x82, 0xd0, 0xf8,
0x47, 0xea, 0xd9, 0x83, 0x85, 0x1c, 0xc1, 0x02, 0x0f, 0xfb, 0x0a, 0x16,
0x82, 0xb9, 0x62, 0xde, 0x5a, 0x51, 0x79, 0x91, 0xe4, 0xf6, 0x04, 0xac,
0xbf, 0x81, 0x74, 0x35, 0xfb, 0xa4, 0x34, 0xe1, 0x32, 0x87, 0x05, 0x23,
0x97, 0x80, 0xd0, 0x2c, 0x01, 0x66, 0x69, 0x32, 0xfc, 0xe5, 0x43, 0xdc,
0x35, 0x76, 0x35, 0x84, 0x87, 0xa7, 0x1d, 0x19, 0xa2, 0x1b, 0xf0, 0x43,
0x52, 0x03, 0x22, 0xec, 0x98, 0x95, 0x7f, 0xa6, 0x79, 0xf9, 0xe5, 0x43,
0x20, 0x80, 0x5c, 0x6f, 0x30, 0xee, 0x07, 0xbe, 0xc2, 0x63, 0x05, 0x0b,
0x16, 0xc1, 0x1a, 0x7b, 0x38, 0x0b, 0x99, 0x7b, 0x38, 0x28, 0x05, 0x4f,
0xf3, 0x9b, 0x19, 0x4a, 0xea, 0xa8, 0x15, 0x41, 0xee, 0x30, 0x84, 0xf5,
0xb6, 0x58, 0x90, 0xf3, 0x30, 0x7a, 0x1a, 0x3e, 0xb9, 0xf4, 0x3b, 0xf7,
0xe6, 0x49, 0x20, 0xc9, 0x10, 0x62, 0xbb, 0xbb, 0x34, 0xdc, 0xe3, 0x53,
0xea, 0xd0, 0x77, 0xf2, 0x2b, 0x68, 0x81, 0x5b, 0x74, 0xa5, 0xb4, 0x1b,
0x58, 0x7a, 0x86, 0x07, 0xee, 0x38, 0xa4, 0xe8, 0xb2, 0xf9, 0x0b, 0x58,
0x1d, 0xa9, 0xb9, 0x72, 0x74, 0x90, 0xf5, 0x48, 0x8a, 0x6e, 0x4f, 0x76,
0xcb, 0x4b, 0xcd, 0x78, 0x53, 0xe2, 0xbd, 0x8c, 0xa0, 0x2e, 0x82, 0xc5,
0xd2, 0x81, 0x33, 0xe0, 0xd3, 0x4f, 0xd2, 0x54, 0x5e, 0xce, 0x74, 0xa3,
0x82, 0xc7, 0x02, 0xd8, 0x8a, 0x65, 0x61, 0xd3, 0x7f, 0xa9, 0x35, 0xda,
0x95, 0x84, 0x91, 0xdb, 0x0e, 0xf0, 0x7d, 0xc2, 0x74, 0x59, 0x02, 0x5d,
0x1c, 0x38, 0x38, 0x76, 0x49, 0x46, 0x42, 0xaa, 0x04, 0xb7, 0x06, 0x4e,
0x0d, 0x5c, 0x1a, 0xf8, 0x38, 0xcd, 0x13, 0x3f, 0xbe, 0xf1, 0xe8, 0x3d,
0xdd, 0x18, 0x78, 0xd0, 0x70, 0x0e, 0x02, 0x7c, 0x15, 0x5c, 0xd2, 0x5d,
0xf0, 0x47, 0xe9, 0xf1, 0x6b, 0x9f, 0xf1, 0xee, 0x17, 0xea, 0x2f, 0xec,
0x7e, 0x63, 0x6f, 0xf3, 0xf9, 0xef, 0x26, 0x17, 0xfc, 0x06, 0x3f, 0xbd,
0x1c, 0xdc, 0xeb, 0xdf, 0xff, 0xfc, 0x23, 0xbb, 0xf2, 0x8f, 0x6c, 0x8b,
0x3f, 0xff, 0xc1, 0x39, 0xc5, 0x8f, 0x5b, 0x99, 0xa9, 0xdd, 0xff, 0x32,
0x7f, 0x54, 0x00, 0x38, 0x93, 0x99, 0x1f, 0x67, 0x71, 0x67, 0xa9, 0xc0,
0xb7, 0xc3, 0x18, 0xe0, 0xc9, 0x61, 0x44, 0x79, 0x0c, 0x1f, 0xc8, 0x3e,
0xf1, 0x6b, 0x00, 0xc7, 0x30, 0x8c, 0xc6, 0xd1, 0x72, 0x9d, 0xf6, 0xba,
0x5d, 0x09, 0x1f, 0x88, 0x23, 0x19, 0xb8, 0x91, 0x26, 0x4a, 0x0b, 0x51,
0x74, 0xd1, 0xfb, 0xdb, 0x38, 0x26, 0x0f, 0x99, 0x64, 0xb8, 0xa8, 0xe2,
0x80, 0xbb, 0x57, 0xa5, 0xc0, 0x2b, 0xa5, 0x17, 0x03, 0x37, 0x98, 0xb4,
0x0b, 0x96, 0xd5, 0x93, 0x5c, 0x6a, 0x9a, 0x0d, 0xee, 0x90, 0xee, 0x06,
0xbb, 0xca, 0x87, 0xd4, 0xae, 0x52, 0xbb, 0xd9, 0xa5, 0x86, 0x09, 0x76,
0x15, 0xbe, 0x4d, 0x4d, 0xfb, 0x4f, 0x45, 0xb4, 0x37, 0xfb, 0xcf, 0x53,
0x6a, 0xd2, 0x0d, 0x40, 0xa1, 0x3b, 0xe6, 0xf4, 0xab, 0x3f, 0xa7, 0x7e,
0x85, 0xc3, 0xd2, 0xc5, 0xaa, 0xda, 0xd0, 0x5d, 0xe1, 0x45, 0x52, 0xdd,
0xe7, 0x52, 0xa0, 0x1c, 0xce, 0x2f, 0xc5, 0x9c, 0x5d, 0x72, 0x9b, 0x8d,
0x97, 0xa7, 0x47, 0xc2, 0xc4, 0x41, 0xb2, 0x00, 0xf4, 0xa5, 0xb4, 0xdf,
0xf1, 0x51, 0x0f, 0x8c, 0x92, 0x16, 0xc3, 0xe2, 0x0c, 0xfc, 0xeb, 0x20,
0x8a, 0x6f, 0x7c, 0xf2, 0x83, 0xbf, 0x06, 0x70, 0xd0, 0xa2, 0xa1, 0x26,
0xff, 0x61, 0xa3, 0x23, 0x94, 0x77, 0xa4, 0x0b, 0x8d, 0x95, 0xa9, 0xd4,
0xa7, 0x7d, 0xff, 0x2f, 0xef, 0xff, 0x1a, 0x10, 0x5f, 0x2e, 0x31, 0x36,
0xdd, 0x94, 0x43, 0xfe, 0x07, 0xef, 0xb2, 0x66, 0x87, 0x36, 0xd0, 0x52,
0x8f, 0xf6, 0xe8, 0xd3, 0xf8, 0xfe, 0xaf, 0x92, 0xea, 0x57, 0x4f, 0xb0,
0x76, 0xff, 0xb2, 0xe5, 0x17, 0x7e, 0x60, 0xd4, 0xd7, 0x8f, 0x70, 0xe8,
0xc9, 0x41, 0x67, 0x34, 0xae, 0x07, 0xd5, 0x02, 0x5e, 0x2d, 0xd1, 0x52,
0x53, 0x2c, 0xbd, 0x61, 0x88, 0x9a, 0x3b, 0x9c, 0x98, 0x19, 0x2a, 0x6a,
0x88, 0xee, 0x11, 0xb7, 0xf7, 0xa0, 0x5c, 0xdc, 0xb9, 0xa5, 0x3b, 0xaa,
0x9e, 0x2d, 0x63, 0xe7, 0x91, 0xc1, 0x29, 0xba, 0xa6, 0xf4, 0x4b, 0xbf,
0xf5, 0xf4, 0xb4, 0x2a, 0xd6, 0x9c, 0x43, 0x6d, 0x68, 0x52, 0x46, 0x68,
0xd7, 0x32, 0x70, 0x01, 0x63, 0x48, 0x33, 0xa5, 0x11, 0xc2, 0x56, 0xc3,
0x37, 0x35, 0x72, 0xa2, 0xc4, 0x8e, 0x5f, 0x9f, 0xea, 0x97, 0x4d, 0x40,
0x48, 0x24, 0x8d, 0x40, 0x27, 0x4d, 0xfd, 0x18, 0x53, 0xbb, 0x4f, 0xe6,
0xbd, 0x24, 0x04, 0x99, 0xb3, 0xf4, 0x41, 0xe1, 0x57, 0xc9, 0xf3, 0xab,
0xc6, 0xf3, 0x64, 0x34, 0x36, 0xb9, 0x8f, 0x25, 0x45, 0x0d, 0x01, 0xd4,
0x68, 0xc6, 0xa0, 0xd1, 0x23, 0xc6, 0x20, 0xa1, 0x40, 0x35, 0x6c, 0xa4,
0x1f, 0x66, 0x6a, 0xbc, 0xa8, 0xdd, 0xd6, 0x78, 0xd1, 0x0b, 0xa4, 0xb4,
0x88, 0x23, 0x39, 0x0f, 0x14, 0x1d, 0x23, 0x44, 0xf0, 0xc8, 0x35, 0xd2,
0xd5, 0x9f, 0x9c, 0x9a, 0xb3, 0xe0, 0xea, 0x47, 0xcf, 0x8e, 0xa7, 0xff,
0x05, 0xbc, 0x47, 0xa4, 0x19, 0x06, 0x2d, 0x00, 0x00
};
const GFXglyph Oswald_Medium30pt7bGlyphs[] PROGMEM = {
{ 0, 1, 1, 14, 0, 0 }, // 0x20 ' '
{ 1, 8, 48, 14, 3, -47 }, // 0x21 '!'
{ 49, 17, 17, 19, 1, -47 }, // 0x22 '"'
{ 86, 26, 48, 30, 2, -47 }, // 0x23 '#'
{ 242, 25, 59, 29, 2, -52 }, // 0x24 '$'
{ 427, 51, 48, 56, 2, -47 }, // 0x25 '%'
{ 733, 29, 49, 34, 3, -47 }, // 0x26 '&'
{ 911, 7, 17, 9, 1, -47 }, // 0x27 '''
{ 926, 13, 59, 19, 4, -47 }, // 0x28 '('
{ 1022, 13, 59, 17, 2, -47 }, // 0x29 ')'
{ 1118, 21, 21, 24, 2, -47 }, // 0x2A '*'
{ 1174, 22, 24, 25, 2, -35 }, // 0x2B '+'
{ 1240, 8, 16, 13, 2, -7 }, // 0x2C ','
{ 1256, 14, 6, 18, 2, -19 }, // 0x2D '-'
{ 1267, 8, 8, 13, 3, -7 }, // 0x2E '.'
{ 1275, 19, 48, 23, 2, -47 }, // 0x2F '/'
{ 1389, 25, 49, 31, 3, -47 }, // 0x30 '0'
{ 1543, 15, 48, 22, 2, -47 }, // 0x31 '1'
{ 1633, 25, 48, 29, 2, -47 }, // 0x32 '2'
{ 1783, 25, 49, 29, 2, -47 }, // 0x33 '3'
{ 1937, 27, 48, 30, 2, -47 }, // 0x34 '4'
{ 2099, 24, 49, 29, 3, -47 }, // 0x35 '5'
{ 2246, 25, 49, 31, 3, -47 }, // 0x36 '6'
{ 2400, 21, 48, 24, 1, -47 }, // 0x37 '7'
{ 2526, 24, 49, 30, 3, -47 }, // 0x38 '8'
{ 2673, 25, 49, 31, 2, -47 }, // 0x39 '9'
{ 2827, 7, 28, 14, 4, -31 }, // 0x3A ':'
{ 2852, 8, 38, 15, 4, -32 }, // 0x3B ';'
{ 2890, 18, 25, 23, 2, -36 }, // 0x3C '<'
{ 2947, 19, 16, 25, 3, -31 }, // 0x3D '='
{ 2985, 18, 25, 23, 3, -36 }, // 0x3E '>'
{ 3042, 24, 48, 28, 2, -47 }, // 0x3F '?'
{ 3186, 50, 56, 55, 3, -47 }, // 0x40 '@'
{ 3536, 29, 48, 31, 1, -47 }, // 0x41 'A'
{ 3710, 27, 48, 33, 4, -47 }, // 0x42 'B'
{ 3872, 26, 49, 32, 3, -47 }, // 0x43 'C'
{ 4032, 26, 48, 33, 4, -47 }, // 0x44 'D'
{ 4188, 20, 48, 25, 4, -47 }, // 0x45 'E'
{ 4308, 19, 48, 24, 4, -47 }, // 0x46 'F'
{ 4422, 27, 49, 33, 3, -47 }, // 0x47 'G'
{ 4588, 27, 48, 34, 4, -47 }, // 0x48 'H'
{ 4750, 8, 48, 16, 4, -47 }, // 0x49 'I'
{ 4798, 15, 49, 19, 1, -47 }, // 0x4A 'J'
{ 4890, 27, 48, 31, 4, -47 }, // 0x4B 'K'
{ 5052, 20, 48, 25, 4, -47 }, // 0x4C 'L'
{ 5172, 34, 48, 40, 3, -47 }, // 0x4D 'M'
{ 5376, 24, 48, 32, 4, -47 }, // 0x4E 'N'
{ 5520, 27, 49, 33, 3, -47 }, // 0x4F 'O'
{ 5686, 26, 48, 31, 4, -47 }, // 0x50 'P'
{ 5842, 27, 57, 33, 3, -47 }, // 0x51 'Q'
{ 6035, 27, 48, 33, 4, -47 }, // 0x52 'R'
{ 6197, 26, 49, 29, 2, -47 }, // 0x53 'S'
{ 6357, 24, 48, 25, 1, -47 }, // 0x54 'T'
{ 6501, 27, 49, 33, 3, -47 }, // 0x55 'U'
{ 6667, 28, 48, 30, 1, -47 }, // 0x56 'V'
{ 6835, 38, 48, 42, 2, -47 }, // 0x57 'W'
{ 7063, 28, 48, 29, 1, -47 }, // 0x58 'X'
{ 7231, 27, 48, 29, 1, -47 }, // 0x59 'Y'
{ 7393, 22, 48, 25, 2, -47 }, // 0x5A 'Z'
// Euro sign ([) - ASCII code 91
{ 11030, 30, 49, 31, 0, -47 }, // 0x5B '['
// Backslash placeholder - ASCII code 92
{ 0, 0, 0, 0, 0, 0 }, // 0x5C '\'
// Pound sign (]) - ASCII code 93
{ 11214, 24, 48, 26, 1, -47 }, // 0x5D ']'
// Yen sign (^) - ASCII code 94
{ 11358, 28, 48, 27, 0, -47 }, // 0x5E '^'
{ 7905, 21, 6, 21, 0, 4 }, // 0x5F '_'
{ 7921, 11, 12, 17, 3, -47 }, // 0x60 '`'
{ 7938, 22, 35, 26, 1, -33 }, // 0x61 'a'
{ 8035, 23, 49, 28, 3, -47 }, // 0x62 'b'
{ 8176, 22, 35, 26, 2, -33 }, // 0x63 'c'
{ 8273, 23, 49, 28, 2, -47 }, // 0x64 'd'
{ 8414, 22, 35, 26, 2, -33 }, // 0x65 'e'
{ 8511, 16, 46, 18, 1, -45 }, // 0x66 'f'
{ 8603, 28, 46, 28, 1, -34 }, // 0x67 'g'
{ 8764, 22, 48, 28, 3, -47 }, // 0x68 'h'
{ 8896, 8, 46, 15, 3, -45 }, // 0x69 'i'
{ 8942, 13, 56, 15, -1, -45 }, // 0x6A 'j'
{ 9033, 25, 48, 28, 3, -47 }, // 0x6B 'k'
{ 9183, 8, 48, 15, 4, -47 }, // 0x6C 'l'
{ 9231, 36, 35, 42, 3, -34 }, // 0x6D 'm'
{ 9389, 22, 34, 28, 3, -33 }, // 0x6E 'n'
{ 9483, 22, 35, 27, 2, -33 }, // 0x6F 'o'
{ 9580, 23, 45, 28, 3, -33 }, // 0x70 'p'
{ 9710, 22, 45, 28, 3, -33 }, // 0x71 'q'
{ 9834, 17, 34, 21, 3, -33 }, // 0x72 'r'
{ 9907, 21, 35, 24, 1, -33 }, // 0x73 's'
{ 9999, 17, 44, 19, 1, -43 }, // 0x74 't'
{ 10093, 22, 35, 28, 3, -33 }, // 0x75 'u'
{ 10190, 22, 34, 24, 1, -33 }, // 0x76 'v'
{ 10284, 32, 34, 35, 1, -33 }, // 0x77 'w'
{ 10420, 23, 34, 24, 1, -33 }, // 0x78 'x'
{ 10518, 24, 43, 25, 0, -33 }, // 0x79 'y'
{ 10647, 18, 34, 22, 2, -33 }, // 0x7A 'z'
{ 10724, 15, 59, 20, 3, -47 }, // 0x7B '{'
{ 10835, 7, 58, 15, 4, -47 }, // 0x7C '|'
{ 10886, 16, 59, 21, 2, -47 }, // 0x7D '}'
{ 11004, 23, 9, 27, 2, -28 } }; // 0x7E '~'
// const GFXfont Oswald_Medium30pt7b PROGMEM = {
// (uint8_t *)Oswald_Medium30pt7bBitmaps,
// (GFXglyph *)Oswald_Medium30pt7bGlyphs,
// 0x20, 0x7E, 87 };
// Approx. 11702 bytes
// Font properties
static constexpr FontData Oswald_Medium30pt7b_Properties = {
Oswald_Medium30pt7bBitmaps_Gzip,
Oswald_Medium30pt7bGlyphs,
sizeof(Oswald_Medium30pt7bBitmaps_Gzip),
11526, // Original size
0x20, // First char
0x7E, // Last char
87 // yAdvance
};

1083
src/fonts/oswald-medium80.h Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,201 +1,235 @@
const uint8_t Satoshi_Symbol90pt7bBitmaps[] PROGMEM = { #pragma once
0x00, 0x00, 0x00, 0x00, 0x0F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, #include <Adafruit_GFX.h>
0x00, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, #include <Arduino.h>
0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFE, 0x00, #include "fonts.hpp"
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, const uint8_t Satoshi_Symbol90pt7bBitmaps_Gzip[] = {
0x00, 0x00, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x63, 0x60,
0x00, 0x00, 0x0F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x60, 0xe0, 0xff, 0xc7, 0x00, 0x05, 0xcc, 0xff, 0x1b, 0x60, 0xcc,
0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xff, 0x0f, 0x60, 0x2c, 0xfb, 0x1f, 0x30, 0xd6, 0x60, 0x53, 0x38, 0x28,
0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x81, 0xfd, 0x7f, 0x3a, 0x83, 0x81, 0xf6, 0x30, 0x1a, 0xe0, 0xa7, 0xb7,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFE, 0x00, 0x00, 0x00, 0xff, 0x0f, 0x0c, 0xb4, 0x8f, 0x51, 0x01, 0x33, 0xbd, 0xfd, 0xff, 0x61,
0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x7d, 0x8c, 0x17, 0x0c, 0x8e, 0x92, 0x82, 0x44, 0x85, 0x54, 0x8f,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x88, 0xe1, 0xec, 0xff, 0xa1, 0x56, 0xf2, 0xff, 0xff, 0x03, 0x61,
0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc8, 0xff, 0x87, 0x3a, 0xc7, 0x1e, 0x16, 0x8d, 0xf5, 0x30, 0xa7, 0xc2,
0x0F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x12, 0x36, 0xe3, 0x7f, 0xa8, 0xeb, 0x98, 0x47, 0x8d, 0x19, 0x35, 0x66,
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x1b, 0x43, 0x65, 0xf0, 0x00, 0x00, 0xc7, 0x63, 0x9f, 0x4b, 0xde,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, };
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // unsigned int satoshi_bin_gz_len = 123;
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // const uint8_t Satoshi_Symbol90pt7bBitmaps[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFE, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x0F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF8, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFE, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x0F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x0F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x00, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFE, // 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x0F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFE, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFC, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x1F, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x3F, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xE0, // 0x0F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, // 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x00,
0x00, 0x00, 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFE, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x1F, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, // 0x00, 0x00, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xE0, 0x00, // 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFE,
0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00,
0x00, 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x00, 0x00, 0x00,
0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFE, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x0F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x1F, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, // 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xE0, 0x00, 0x00, // 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x01, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFE, 0x00, 0x00, 0x00, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x1F, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF0, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xE0, 0x00, 0x00, 0x00, // 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFE,
0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFC, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x0F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, // 0x3F, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xE0,
0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF0, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
0x00, 0x00, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, // 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFE, 0x00,
0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x1F, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F,
0x00, 0x0F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, // 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xE0, 0x00,
0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF0, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF,
0x00, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, // 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFE, 0x00, 0x00,
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x07, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x1F, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF,
0x0F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xF8, // 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xE0, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF0, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x7F, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF,
0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x80, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFE, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x07, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, // 0x1F, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF0,
0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xF8, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xE0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x7F, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, // 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x00,
0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x80, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x0F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F,
0x00, 0x07, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, // 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF0, 0x00,
0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xF8, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF,
0x00, 0x7F, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, // 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00,
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x0F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF,
0x07, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFC, // 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF0, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xF8, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF,
0x7F, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xC0, // 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, // 0x0F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xF8,
0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFC, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x3F, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, // 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x80,
0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xC0, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, // 0x00, 0x00, 0x07, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F,
0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, // 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xF8, 0x00,
0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFC, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x7F, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x80, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x07, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xF8, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // 0x00, 0x7F, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF,
0xFF, 0xE0 }; // 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x07, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFC,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xF8, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x7F, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xC0,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07,
// 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFC, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x3F, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F,
// 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xC0, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF,
// 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFC, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
// 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
// 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
// 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
// 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
// 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
// 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
// 0xFF, 0xE0 };
const GFXglyph Satoshi_Symbol90pt7bGlyphs[] PROGMEM = { const GFXglyph Satoshi_Symbol90pt7bGlyphs[] PROGMEM = {
{ 0, 82, 127, 99, 8, -126 }, { 1302, 71, 109, 93, 0, -117 } }; // 0x53 'S' { 0, 82, 127, 99, 8, -126 }, { 1302, 71, 109, 93, 0, -117 } }; // 0x53 'S'
const GFXfont Satoshi_Symbol90pt7b PROGMEM = { // const GFXfont Satoshi_Symbol90pt7b PROGMEM = {
(uint8_t *)Satoshi_Symbol90pt7bBitmaps, // (uint8_t *)Satoshi_Symbol90pt7bBitmaps,
(GFXglyph *)Satoshi_Symbol90pt7bGlyphs, // (GFXglyph *)Satoshi_Symbol90pt7bGlyphs,
0x53, 0x53, 192 }; // 0x53, 0x53, 192 };
// Font properties
static constexpr FontData Satoshi_Symbol90pt7b_Properties = {
Satoshi_Symbol90pt7bBitmaps_Gzip,
Satoshi_Symbol90pt7bGlyphs,
sizeof(Satoshi_Symbol90pt7bBitmaps_Gzip),
2270, // Original size
0x53, // First char
0x53, // Last char
192 // yAdvance
};
// Approx. 2284 bytes // Approx. 2284 bytes

File diff suppressed because it is too large Load diff

View file

@ -1,24 +1,19 @@
#include "bitaxe_fetch.hpp" #include "bitaxe_fetch.hpp"
TaskHandle_t bitaxeFetchTaskHandle; void BitAxeFetch::taskWrapper(void* pvParameters) {
BitAxeFetch::getInstance().task();
std::string bitaxeHashrate;
std::string bitaxeBestDiff;
std::string getBitAxeHashRate()
{
return bitaxeHashrate;
} }
std::string getBitaxeBestDiff() uint64_t BitAxeFetch::getHashRate() const {
{ return hashrate;
return bitaxeBestDiff;
} }
void taskBitaxeFetch(void *pvParameters) uint64_t BitAxeFetch::getBestDiff() const {
{ return bestDiff;
for (;;) }
{
void BitAxeFetch::task() {
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
HTTPClient http; HTTPClient http;
@ -28,34 +23,38 @@ void taskBitaxeFetch(void *pvParameters)
int httpCode = http.GET(); int httpCode = http.GET();
if (httpCode == 200) if (httpCode == 200) {
{
String payload = http.getString(); String payload = http.getString();
JsonDocument doc; JsonDocument doc;
deserializeJson(doc, payload); deserializeJson(doc, payload);
bitaxeHashrate = std::to_string(static_cast<int>(std::round(doc["hashRate"].as<float>())));
bitaxeBestDiff = doc["bestDiff"].as<std::string>();
if (workQueue != nullptr && (getCurrentScreen() == SCREEN_BITAXE_HASHRATE || getCurrentScreen() == SCREEN_BITAXE_BESTDIFF)) // Convert GH/s to H/s (multiply by 10^9)
{ float hashRateGH = doc["hashRate"].as<float>();
hashrate = static_cast<uint64_t>(std::round(hashRateGH * std::pow(10, getHashrateMultiplier('G'))));
// Parse difficulty string and convert to uint64_t
std::string diffStr = doc["bestDiff"].as<std::string>();
char diffUnit = diffStr[diffStr.length() - 1];
if (std::isalpha(diffUnit)) {
float diffValue = std::stof(diffStr.substr(0, diffStr.length() - 1));
bestDiff = static_cast<uint64_t>(std::round(diffValue * std::pow(10, getDifficultyMultiplier(diffUnit))));
} else {
bestDiff = std::stoull(diffStr);
}
if (workQueue != nullptr && (ScreenHandler::getCurrentScreen() == SCREEN_BITAXE_HASHRATE || ScreenHandler::getCurrentScreen() == SCREEN_BITAXE_BESTDIFF)) {
WorkItem priceUpdate = {TASK_BITAXE_UPDATE, 0}; WorkItem priceUpdate = {TASK_BITAXE_UPDATE, 0};
xQueueSend(workQueue, &priceUpdate, portMAX_DELAY); xQueueSend(workQueue, &priceUpdate, portMAX_DELAY);
} }
} } else {
else Serial.print(F("Error retrieving BitAxe data. HTTP status code: "));
{
Serial.print(
F("Error retrieving BitAxe data. HTTP status code: "));
Serial.println(httpCode); Serial.println(httpCode);
Serial.println(bitaxeApiUrl); Serial.println(bitaxeApiUrl);
} }
} }
} }
void setupBitaxeFetchTask() void BitAxeFetch::setup() {
{ xTaskCreate(taskWrapper, "bitaxeFetch", (3 * 1024), NULL, tskIDLE_PRIORITY, &taskHandle);
xTaskCreate(taskBitaxeFetch, "bitaxeFetch", (6 * 1024), NULL, tskIDLE_PRIORITY, xTaskNotifyGive(taskHandle);
&bitaxeFetchTaskHandle);
xTaskNotifyGive(bitaxeFetchTaskHandle);
} }

View file

@ -2,14 +2,33 @@
#include <Arduino.h> #include <Arduino.h>
#include <HTTPClient.h> #include <HTTPClient.h>
#include <utils.hpp>
#include "lib/config.hpp" #include "lib/config.hpp"
#include "lib/shared.hpp" #include "lib/shared.hpp"
extern TaskHandle_t bitaxeFetchTaskHandle; class BitAxeFetch {
public:
static BitAxeFetch& getInstance() {
static BitAxeFetch instance;
return instance;
}
void setupBitaxeFetchTask(); void setup();
void taskBitaxeFetch(void *pvParameters); uint64_t getHashRate() const;
uint64_t getBestDiff() const;
static void taskWrapper(void* pvParameters);
TaskHandle_t getTaskHandle() const { return taskHandle; }
std::string getBitAxeHashRate(); private:
std::string getBitaxeBestDiff(); BitAxeFetch() = default;
~BitAxeFetch() = default;
BitAxeFetch(const BitAxeFetch&) = delete;
BitAxeFetch& operator=(const BitAxeFetch&) = delete;
void task();
TaskHandle_t taskHandle = nullptr;
uint64_t hashrate = 0;
uint64_t bestDiff = 0;
};

View file

@ -1,13 +1,14 @@
#include "block_notify.hpp" #include "block_notify.hpp"
char *wsServer; // Initialize static members
esp_websocket_client_handle_t blockNotifyClient = NULL; esp_websocket_client_handle_t BlockNotify::wsClient = nullptr;
uint currentBlockHeight = 860000; uint32_t BlockNotify::currentBlockHeight = 878000;
uint blockMedianFee = 1; uint16_t BlockNotify::blockMedianFee = 1;
bool blockNotifyInit = false; bool BlockNotify::notifyInit = false;
unsigned long int lastBlockUpdate; unsigned long int BlockNotify::lastBlockUpdate = 0;
TaskHandle_t BlockNotify::taskHandle = nullptr;
const char *mempoolWsCert = R"EOF( const char* BlockNotify::mempoolWsCert = R"EOF(
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB
iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
@ -42,22 +43,106 @@ VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB
L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG
jjxDah2nGN59PRbxYvnKkKj9 jjxDah2nGN59PRbxYvnKkKj9
-----END CERTIFICATE----- -----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
)EOF"; )EOF";
void setupBlockNotify() void BlockNotify::onWebsocketEvent(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) {
esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data;
BlockNotify& instance = BlockNotify::getInstance();
switch (event_id) {
case WEBSOCKET_EVENT_CONNECTED:
{ {
notifyInit = true;
Serial.print(F("Connected to "));
Serial.println(preferences.getString("mempoolInstance", DEFAULT_MEMPOOL_INSTANCE));
JsonDocument doc;
doc["action"] = "want";
JsonArray dataArray = doc.createNestedArray("data");
dataArray.add("blocks");
dataArray.add("mempool-blocks");
String sub;
serializeJson(doc, sub);
esp_websocket_client_send_text(wsClient, sub.c_str(), sub.length(), portMAX_DELAY);
break;
}
case WEBSOCKET_EVENT_DATA:
instance.onWebsocketMessage(data);
break;
case WEBSOCKET_EVENT_DISCONNECTED:
Serial.println(F("Mempool.space WS Connection Closed"));
break;
case WEBSOCKET_EVENT_ERROR:
Serial.println(F("Mempool.space WS Connection Error"));
break;
}
}
void BlockNotify::onWebsocketMessage(esp_websocket_event_data_t *data) {
JsonDocument doc;
JsonDocument filter;
filter["block"]["height"] = true;
filter["mempool-blocks"][0]["medianFee"] = true;
deserializeJson(doc, (char*)data->data_ptr, DeserializationOption::Filter(filter));
if (doc["block"].is<JsonObject>()) {
JsonObject block = doc["block"];
if (block["height"].as<uint>() != currentBlockHeight) {
processNewBlock(block["height"].as<uint>());
}
}
else if (doc["mempool-blocks"].is<JsonArray>()) {
JsonArray blockInfo = doc["mempool-blocks"].as<JsonArray>();
uint medianFee = (uint)round(blockInfo[0]["medianFee"].as<double>());
processNewBlockFee(medianFee);
}
}
void BlockNotify::setup() {
IPAddress result; IPAddress result;
int dnsErr = -1; int dnsErr = -1;
String mempoolInstance = String mempoolInstance = preferences.getString("mempoolInstance", DEFAULT_MEMPOOL_INSTANCE);
preferences.getString("mempoolInstance", DEFAULT_MEMPOOL_INSTANCE);
while (dnsErr != 1 && !strchr(mempoolInstance.c_str(), ':')) while (dnsErr != 1 && !strchr(mempoolInstance.c_str(), ':')) {
{
dnsErr = WiFi.hostByName(mempoolInstance.c_str(), result); dnsErr = WiFi.hostByName(mempoolInstance.c_str(), result);
if (dnsErr != 1) if (dnsErr != 1) {
{
Serial.print(mempoolInstance); Serial.print(mempoolInstance);
Serial.println(F("mempool DNS could not be resolved")); Serial.println(F("mempool DNS could not be resolved"));
WiFi.reconnect(); WiFi.reconnect();
@ -66,151 +151,69 @@ void setupBlockNotify()
} }
// Get current block height through regular API // Get current block height through regular API
int blockFetch = getBlockFetch(); int blockFetch = fetchLatestBlock();
if (blockFetch > currentBlockHeight) if (blockFetch > currentBlockHeight)
currentBlockHeight = blockFetch; currentBlockHeight = blockFetch;
if (currentBlockHeight != -1) if (currentBlockHeight != -1) {
{
lastBlockUpdate = esp_timer_get_time() / 1000000; lastBlockUpdate = esp_timer_get_time() / 1000000;
} }
if (workQueue != nullptr) if (workQueue != nullptr) {
{
WorkItem blockUpdate = {TASK_BLOCK_UPDATE, 0}; WorkItem blockUpdate = {TASK_BLOCK_UPDATE, 0};
xQueueSend(workQueue, &blockUpdate, portMAX_DELAY); xQueueSend(workQueue, &blockUpdate, portMAX_DELAY);
} }
if (!preferences.getBool("fetchEurPrice", DEFAULT_FETCH_EUR_PRICE) && preferences.getBool("ownDataSource", DEFAULT_OWN_DATA_SOURCE)) const bool useSSL = preferences.getBool("mempoolSecure", DEFAULT_MEMPOOL_SECURE);
{ const String protocol = useSSL ? "wss" : "ws";
return; String wsUri = protocol + "://" + mempoolInstance + "/api/v1/ws";
}
// std::strcpy(wsServer, String("wss://" + mempoolInstance +
// "/api/v1/ws").c_str());
const String protocol = preferences.getBool("mempoolSecure", DEFAULT_MEMPOOL_SECURE) ? "wss" : "ws";
String mempoolUri = protocol + "://" + preferences.getString("mempoolInstance", DEFAULT_MEMPOOL_INSTANCE) + "/api/v1/ws";
esp_websocket_client_config_t config = { esp_websocket_client_config_t config = {
// .uri = "wss://mempool.space/api/v1/ws",
.task_stack = (6*1024), .task_stack = (6*1024),
.user_agent = USER_AGENT .user_agent = USER_AGENT
}; };
if (preferences.getBool("mempoolSecure", DEFAULT_MEMPOOL_SECURE)) { if (useSSL) {
config.cert_pem = mempoolWsCert; config.cert_pem = mempoolWsCert;
} }
config.uri = mempoolUri.c_str(); config.uri = wsUri.c_str();
Serial.printf("Connecting to %s\r\n", preferences.getString("mempoolInstance", DEFAULT_MEMPOOL_INSTANCE)); Serial.printf("Connecting to %s\r\n", mempoolInstance.c_str());
blockNotifyClient = esp_websocket_client_init(&config); wsClient = esp_websocket_client_init(&config);
esp_websocket_register_events(blockNotifyClient, WEBSOCKET_EVENT_ANY, esp_websocket_register_events(wsClient, WEBSOCKET_EVENT_ANY, onWebsocketEvent, wsClient);
onWebsocketBlockEvent, blockNotifyClient); esp_websocket_client_start(wsClient);
esp_websocket_client_start(blockNotifyClient);
} }
void onWebsocketBlockEvent(void *handler_args, esp_event_base_t base,
int32_t event_id, void *event_data)
void BlockNotify::processNewBlock(uint32_t newBlockHeight) {
if (newBlockHeight <= currentBlockHeight)
{ {
esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data;
const String sub = "{\"action\": \"want\", \"data\":[\"blocks\", \"mempool-blocks\"]}";
switch (event_id)
{
case WEBSOCKET_EVENT_CONNECTED:
blockNotifyInit = true;
Serial.println(F("Connected to Mempool.space WebSocket"));
Serial.println(sub);
if (esp_websocket_client_send_text(blockNotifyClient, sub.c_str(),
sub.length(), portMAX_DELAY) == -1)
{
Serial.println(F("Mempool.space WS Block Subscribe Error"));
}
break;
case WEBSOCKET_EVENT_DATA:
onWebsocketBlockMessage(data);
break;
case WEBSOCKET_EVENT_ERROR:
Serial.println(F("Mempool.space WS Connnection error"));
break;
case WEBSOCKET_EVENT_DISCONNECTED:
Serial.println(F("Mempool.space WS Connnection Closed"));
break;
}
}
void onWebsocketBlockMessage(esp_websocket_event_data_t *event_data)
{
JsonDocument doc;
JsonDocument filter;
filter["block"]["height"] = true;
filter["mempool-blocks"][0]["medianFee"] = true;
deserializeJson(doc, (char *)event_data->data_ptr, DeserializationOption::Filter(filter));
// if (error) {
// Serial.print("deserializeJson() failed: ");
// Serial.println(error.c_str());
// return;
// }
if (doc.containsKey("block"))
{
JsonObject block = doc["block"];
if (block["height"].as<uint>() == currentBlockHeight) {
return; return;
} }
processNewBlock(block["height"].as<uint>());
}
else if (doc.containsKey("mempool-blocks"))
{
JsonArray blockInfo = doc["mempool-blocks"].as<JsonArray>();
uint medianFee = (uint)round(blockInfo[0]["medianFee"].as<double>());
processNewBlockFee(medianFee);
}
doc.clear();
}
void processNewBlock(uint newBlockHeight) {
if (newBlockHeight < currentBlockHeight)
return;
currentBlockHeight = newBlockHeight; currentBlockHeight = newBlockHeight;
// Serial.printf("New block found: %d\r\n", block["height"].as<uint>());
preferences.putUInt("blockHeight", currentBlockHeight);
lastBlockUpdate = esp_timer_get_time() / 1000000; lastBlockUpdate = esp_timer_get_time() / 1000000;
if (workQueue != nullptr) if (workQueue != nullptr)
{ {
WorkItem blockUpdate = {TASK_BLOCK_UPDATE, 0}; WorkItem blockUpdate = {TASK_BLOCK_UPDATE, 0};
xQueueSend(workQueue, &blockUpdate, portMAX_DELAY); xQueueSend(workQueue, &blockUpdate, portMAX_DELAY);
// xTaskNotifyGive(blockUpdateTaskHandle); }
if (getCurrentScreen() != SCREEN_BLOCK_HEIGHT && if (ScreenHandler::getCurrentScreen() != SCREEN_BLOCK_HEIGHT &&
preferences.getBool("stealFocus", DEFAULT_STEAL_FOCUS)) preferences.getBool("stealFocus", DEFAULT_STEAL_FOCUS))
{ {
uint64_t timerPeriod = 0; uint64_t timerPeriod = 0;
if (isTimerActive()) if (isTimerActive())
{ {
// store timer periode before making inactive to prevent artifacts
timerPeriod = getTimerSeconds(); timerPeriod = getTimerSeconds();
esp_timer_stop(screenRotateTimer); esp_timer_stop(screenRotateTimer);
} }
setCurrentScreen(SCREEN_BLOCK_HEIGHT); ScreenHandler::setCurrentScreen(SCREEN_BLOCK_HEIGHT);
if (timerPeriod > 0) if (timerPeriod > 0)
{ {
esp_timer_start_periodic(screenRotateTimer, esp_timer_start_periodic(screenRotateTimer,
@ -222,18 +225,16 @@ void processNewBlock(uint newBlockHeight) {
if (preferences.getBool("ledFlashOnUpd", DEFAULT_LED_FLASH_ON_UPD)) if (preferences.getBool("ledFlashOnUpd", DEFAULT_LED_FLASH_ON_UPD))
{ {
vTaskDelay(pdMS_TO_TICKS(250)); // Wait until screens are updated vTaskDelay(pdMS_TO_TICKS(250)); // Wait until screens are updated
queueLedEffect(LED_FLASH_BLOCK_NOTIFY); getLedHandler().queueEffect(LED_FLASH_BLOCK_NOTIFY);
}
} }
} }
void processNewBlockFee(uint newBlockFee) { void BlockNotify::processNewBlockFee(uint16_t newBlockFee) {
if (blockMedianFee == newBlockFee) if (blockMedianFee == newBlockFee)
{ {
return; return;
} }
// Serial.printf("New median fee: %d\r\n", medianFee);
blockMedianFee = newBlockFee; blockMedianFee = newBlockFee;
if (workQueue != nullptr) if (workQueue != nullptr)
@ -243,106 +244,82 @@ void processNewBlockFee(uint newBlockFee) {
} }
} }
uint getBlockHeight() { return currentBlockHeight; } uint32_t BlockNotify::getBlockHeight() const {
return currentBlockHeight;
}
void setBlockHeight(uint newBlockHeight) void BlockNotify::setBlockHeight(uint32_t newBlockHeight)
{ {
currentBlockHeight = newBlockHeight; currentBlockHeight = newBlockHeight;
} }
uint getBlockMedianFee() { return blockMedianFee; } uint16_t BlockNotify::getBlockMedianFee() const {
return blockMedianFee;
}
void setBlockMedianFee(uint newBlockMedianFee) void BlockNotify::setBlockMedianFee(uint16_t newBlockMedianFee)
{ {
blockMedianFee = newBlockMedianFee; blockMedianFee = newBlockMedianFee;
} }
bool isBlockNotifyConnected() bool BlockNotify::isConnected() const
{ {
if (blockNotifyClient == NULL) if (wsClient == NULL)
return false; return false;
return esp_websocket_client_is_connected(blockNotifyClient); return esp_websocket_client_is_connected(wsClient);
} }
bool getBlockNotifyInit() bool BlockNotify::isInitialized() const
{ {
return blockNotifyInit; return notifyInit;
} }
void stopBlockNotify() void BlockNotify::stop()
{ {
if (blockNotifyClient == NULL) if (wsClient == NULL)
return; return;
esp_websocket_client_close(blockNotifyClient, pdMS_TO_TICKS(5000)); esp_websocket_client_close(wsClient, portMAX_DELAY);
esp_websocket_client_stop(blockNotifyClient); esp_websocket_client_stop(wsClient);
esp_websocket_client_destroy(blockNotifyClient); esp_websocket_client_destroy(wsClient);
wsClient = NULL;
blockNotifyClient = NULL;
} }
void restartBlockNotify() void BlockNotify::restart()
{ {
stopBlockNotify(); stop();
setup();
if (blockNotifyClient == NULL) {
setupBlockNotify();
return;
} }
// esp_websocket_client_close(blockNotifyClient, pdMS_TO_TICKS(5000)); int BlockNotify::fetchLatestBlock() {
// esp_websocket_client_stop(blockNotifyClient);
// esp_websocket_client_start(blockNotifyClient);
}
int getBlockFetch()
{
try { try {
WiFiClientSecure client; String mempoolInstance = preferences.getString("mempoolInstance", DEFAULT_MEMPOOL_INSTANCE);
if (preferences.getBool("mempoolSecure", DEFAULT_MEMPOOL_SECURE)) {
client.setCACert(mempoolWsCert);
}
String mempoolInstance =
preferences.getString("mempoolInstance", DEFAULT_MEMPOOL_INSTANCE);
// Get current block height through regular API
HTTPClient http;
const String protocol = preferences.getBool("mempoolSecure", DEFAULT_MEMPOOL_SECURE) ? "https" : "http"; const String protocol = preferences.getBool("mempoolSecure", DEFAULT_MEMPOOL_SECURE) ? "https" : "http";
String url = protocol + "://" + mempoolInstance + "/api/blocks/tip/height";
if (preferences.getBool("mempoolSecure", DEFAULT_MEMPOOL_SECURE)) HTTPClient* http = HttpHelper::begin(url);
http.begin(client, protocol + "://" + mempoolInstance + "/api/blocks/tip/height"); Serial.println("Fetching block height from " + url);
else int httpCode = http->GET();
http.begin(protocol + "://" + mempoolInstance + "/api/blocks/tip/height");
Serial.println("Fetching block height from " + protocol + "://" + mempoolInstance + "/api/blocks/tip/height"); if (httpCode > 0 && httpCode == HTTP_CODE_OK) {
int httpCode = http.GET(); String blockHeightStr = http->getString();
HttpHelper::end(http);
if (httpCode > 0 && httpCode == HTTP_CODE_OK)
{
String blockHeightStr = http.getString();
return blockHeightStr.toInt(); return blockHeightStr.toInt();
} else { }
HttpHelper::end(http);
Serial.println("HTTP code" + String(httpCode)); Serial.println("HTTP code" + String(httpCode));
return 0; } catch (...) {
Serial.println(F("An exception occurred while trying to get the latest block"));
} }
}
catch (...) {
Serial.println(F("An exception occured while trying to get the latest block"));
}
return 2203; // B-T-C return 2203; // B-T-C
} }
uint getLastBlockUpdate() uint BlockNotify::getLastBlockUpdate() const
{ {
return lastBlockUpdate; return lastBlockUpdate;
} }
void setLastBlockUpdate(uint lastUpdate) void BlockNotify::setLastBlockUpdate(uint lastUpdate)
{ {
lastBlockUpdate = lastUpdate; lastBlockUpdate = lastUpdate;
} }

View file

@ -5,7 +5,6 @@
#include <HTTPClient.h> #include <HTTPClient.h>
#include <esp_timer.h> #include <esp_timer.h>
#include <esp_websocket_client.h> #include <esp_websocket_client.h>
#include <cstring> #include <cstring>
#include <string> #include <string>
@ -14,28 +13,53 @@
#include "lib/timers.hpp" #include "lib/timers.hpp"
#include "lib/shared.hpp" #include "lib/shared.hpp"
// using namespace websockets; class BlockNotify {
public:
static BlockNotify& getInstance() {
static BlockNotify instance;
return instance;
}
void setupBlockNotify(); // Delete copy constructor and assignment operator
BlockNotify(const BlockNotify&) = delete;
void operator=(const BlockNotify&) = delete;
void onWebsocketBlockEvent(void *handler_args, esp_event_base_t base, // Block notification setup and control
int32_t event_id, void *event_data); void setup();
void onWebsocketBlockMessage(esp_websocket_event_data_t *event_data); void stop();
void restart();
bool isConnected() const;
bool isInitialized() const;
void setBlockHeight(uint newBlockHeight); // Block height management
uint getBlockHeight(); void setBlockHeight(uint32_t newBlockHeight);
uint32_t getBlockHeight() const;
void setBlockMedianFee(uint blockMedianFee); // Block fee management
uint getBlockMedianFee(); void setBlockMedianFee(uint16_t blockMedianFee);
uint16_t getBlockMedianFee() const;
bool isBlockNotifyConnected(); // Block processing
void stopBlockNotify(); void processNewBlock(uint32_t newBlockHeight);
void restartBlockNotify(); void processNewBlockFee(uint16_t newBlockFee);
void processNewBlock(uint newBlockHeight); // Block fetch and update tracking
void processNewBlockFee(uint newBlockFee); int fetchLatestBlock();
uint getLastBlockUpdate() const;
bool getBlockNotifyInit();
uint getLastBlockUpdate();
int getBlockFetch();
void setLastBlockUpdate(uint lastUpdate); void setLastBlockUpdate(uint lastUpdate);
private:
BlockNotify() = default; // Private constructor for singleton
void setupTask();
static void onWebsocketEvent(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data);
void onWebsocketMessage(esp_websocket_event_data_t *data);
static const char* mempoolWsCert;
static esp_websocket_client_handle_t wsClient;
static uint32_t currentBlockHeight;
static uint16_t blockMedianFee;
static bool notifyInit;
static unsigned long int lastBlockUpdate;
static TaskHandle_t taskHandle;
};

View file

@ -1,59 +1,101 @@
#include "button_handler.hpp" #include "button_handler.hpp"
TaskHandle_t buttonTaskHandle = NULL; // Initialize static members
const TickType_t debounceDelay = pdMS_TO_TICKS(50); TaskHandle_t ButtonHandler::buttonTaskHandle = NULL;
TickType_t lastDebounceTime = 0; ButtonState ButtonHandler::buttonStates[4] = {};
#ifdef IS_BTCLOCK_V8 #ifdef IS_BTCLOCK_V8
#define BTN_1 0 #define BTN_1 256
#define BTN_2 1 #define BTN_2 512
#define BTN_3 2 #define BTN_3 1024
#define BTN_4 3 #define BTN_4 2048
#else #else
#define BTN_1 3 #define BTN_1 2048
#define BTN_2 2 #define BTN_2 1024
#define BTN_3 1 #define BTN_3 512
#define BTN_4 0 #define BTN_4 256
#endif #endif
void buttonTask(void *parameter) { void ButtonHandler::buttonTask(void *parameter) {
while (1) { while (1) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
std::lock_guard<std::mutex> lock(mcpMutex);
TickType_t currentTime = xTaskGetTickCount(); TickType_t currentTime = xTaskGetTickCount();
if ((currentTime - lastDebounceTime) >= debounceDelay) {
lastDebounceTime = currentTime; std::lock_guard<std::mutex> lock(mcpMutex);
if (!digitalRead(MCP_INT_PIN)) { if (!digitalRead(MCP_INT_PIN)) {
uint pin = mcp1.getLastInterruptPin(); uint16_t intFlags = mcp1.getInterruptFlagRegister();
uint16_t intCap = mcp1.getInterruptCaptureRegister();
switch (pin) { // Check button states
case BTN_1: if (intFlags & BTN_1) handleButtonPress(0);
if (intFlags & BTN_2) handleButtonPress(1);
if (intFlags & BTN_3) handleButtonPress(2);
if (intFlags & BTN_4) handleButtonPress(3);
// Check for button releases
for (int i = 0; i < 4; i++) {
if (buttonStates[i].isPressed) {
bool currentlyPressed = false;
switch (i) {
case 0: currentlyPressed = (intCap & BTN_1); break;
case 1: currentlyPressed = (intCap & BTN_2); break;
case 2: currentlyPressed = (intCap & BTN_3); break;
case 3: currentlyPressed = (intCap & BTN_4); break;
}
if (!currentlyPressed) {
handleButtonRelease(i);
}
}
}
}
// Clear interrupt state
while (!digitalRead(MCP_INT_PIN)) {
mcp1.getInterruptCaptureRegister();
delay(1);
}
}
}
void ButtonHandler::handleButtonPress(int buttonIndex) {
TickType_t currentTime = xTaskGetTickCount();
ButtonState &state = buttonStates[buttonIndex];
if ((currentTime - state.lastPressTime) >= debounceDelay) {
state.isPressed = true;
state.lastPressTime = currentTime;
}
}
void ButtonHandler::handleButtonRelease(int buttonIndex) {
ButtonState &state = buttonStates[buttonIndex];
if (!state.isPressed) return; // Ignore if button wasn't pressed
state.isPressed = false;
handleSingleClick(buttonIndex);
}
void ButtonHandler::handleSingleClick(int buttonIndex) {
switch (buttonIndex) {
case 0:
toggleTimerActive(); toggleTimerActive();
break; break;
case BTN_2: case 1:
nextScreen(); ScreenHandler::nextScreen();
break; break;
case BTN_3: case 2:
previousScreen(); ScreenHandler::previousScreen();
break; break;
case BTN_4: case 3:
showSystemStatusScreen(); ScreenHandler::showSystemStatusScreen();
break; break;
} }
} }
mcp1.clearInterrupts();
} else {
}
// Very ugly, but for some reason this is necessary
while (!digitalRead(MCP_INT_PIN)) {
mcp1.clearInterrupts();
}
}
}
void IRAM_ATTR handleButtonInterrupt() { void IRAM_ATTR ButtonHandler::handleButtonInterrupt() {
BaseType_t xHigherPriorityTaskWoken = pdFALSE; BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xTaskNotifyFromISR(buttonTaskHandle, 0, eNoAction, &xHigherPriorityTaskWoken); xTaskNotifyFromISR(buttonTaskHandle, 0, eNoAction, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken == pdTRUE) { if (xHigherPriorityTaskWoken == pdTRUE) {
@ -61,9 +103,8 @@ void IRAM_ATTR handleButtonInterrupt() {
} }
} }
void setupButtonTask() { void ButtonHandler::setup() {
xTaskCreate(buttonTask, "ButtonTask", 3072, NULL, tskIDLE_PRIORITY, xTaskCreate(buttonTask, "ButtonTask", 3072, NULL, tskIDLE_PRIORITY,
&buttonTaskHandle); // Create the FreeRTOS task &buttonTaskHandle);
// Use interrupt instead of task attachInterrupt(MCP_INT_PIN, handleButtonInterrupt, FALLING);
attachInterrupt(MCP_INT_PIN, handleButtonInterrupt, CHANGE);
} }

View file

@ -6,8 +6,49 @@
#include "lib/shared.hpp" #include "lib/shared.hpp"
#include "lib/timers.hpp" #include "lib/timers.hpp"
extern TaskHandle_t buttonTaskHandle; // Track timing for each button
struct ButtonState {
TickType_t lastPressTime = 0;
TickType_t pressStartTime = 0;
bool isPressed = false;
uint8_t clickCount = 0;
bool longPressHandled = false;
};
void buttonTask(void *pvParameters); class ButtonHandler {
void IRAM_ATTR handleButtonInterrupt(); private:
void setupButtonTask(); static const TickType_t debounceDelay = pdMS_TO_TICKS(50);
static const TickType_t doubleClickDelay = pdMS_TO_TICKS(1000); // Maximum time between clicks for double click
static const TickType_t longPressDelay = pdMS_TO_TICKS(1500); // Time to hold for long press
static ButtonState buttonStates[4];
static TaskHandle_t buttonTaskHandle;
// Button handlers
static void handleButtonPress(int buttonIndex);
static void handleButtonRelease(int buttonIndex);
static void handleSingleClick(int buttonIndex);
static void handleDoubleClick(int buttonIndex);
static void handleLongPress(int buttonIndex);
// Task function
static void buttonTask(void *pvParameters);
public:
static void setup();
static void IRAM_ATTR handleButtonInterrupt();
static void suspendTask() { if (buttonTaskHandle != NULL) vTaskSuspend(buttonTaskHandle); }
static void resumeTask() { if (buttonTaskHandle != NULL) vTaskResume(buttonTaskHandle); }
#ifdef IS_BTCLOCK_V8
static const uint16_t BTN_1 = 256;
static const uint16_t BTN_2 = 512;
static const uint16_t BTN_3 = 1024;
static const uint16_t BTN_4 = 2048;
#else
static const uint16_t BTN_1 = 2048;
static const uint16_t BTN_2 = 1024;
static const uint16_t BTN_3 = 512;
static const uint16_t BTN_4 = 256;
#endif
};

View file

@ -1,11 +1,14 @@
#include "config.hpp" #include "config.hpp"
#include "led_handler.hpp"
#define MAX_ATTEMPTS_WIFI_CONNECTION 20 #define MAX_ATTEMPTS_WIFI_CONNECTION 20
// zlib_turbo zt;
Preferences preferences; Preferences preferences;
Adafruit_MCP23X17 mcp1; MCP23017 mcp1(0x20);
#ifdef IS_BTCLOCK_V8 #ifdef IS_BTCLOCK_V8
Adafruit_MCP23X17 mcp2; MCP23017 mcp2(0x21);
#endif #endif
#ifdef HAS_FRONTLIGHT #ifdef HAS_FRONTLIGHT
@ -23,30 +26,49 @@ void addScreenMapping(int value, const char *name)
screenMappings.push_back({value, name}); screenMappings.push_back({value, name});
} }
void setupDataSource()
{
DataSourceType dataSource = getDataSource();
bool zapNotifyEnabled = preferences.getBool("nostrZapNotify", DEFAULT_ZAP_NOTIFY_ENABLED);
// Setup Nostr if it's either the data source or zap notifications are enabled
if (dataSource == NOSTR_SOURCE || zapNotifyEnabled) {
setupNostrNotify(dataSource == NOSTR_SOURCE, zapNotifyEnabled);
setupNostrTask();
}
// Setup other data sources if Nostr is not the data source
if (dataSource != NOSTR_SOURCE) {
xTaskCreate(setupWebsocketClients, "setupWebsocketClients", 8192, NULL,
tskIDLE_PRIORITY, NULL);
}
}
void setup() void setup()
{ {
setupPreferences(); setupPreferences();
setupHardware(); setupHardware();
setupDisplays(); EPDManager::getInstance().initialize();
if (preferences.getBool("ledTestOnPower", DEFAULT_LED_TEST_ON_POWER)) if (preferences.getBool("ledTestOnPower", DEFAULT_LED_TEST_ON_POWER))
{ {
queueLedEffect(LED_POWER_TEST); auto& ledHandler = getLedHandler();
ledHandler.queueEffect(LED_POWER_TEST);
} }
{ {
std::lock_guard<std::mutex> lockMcp(mcpMutex); std::lock_guard<std::mutex> lockMcp(mcpMutex);
if (mcp1.digitalRead(3) == LOW) if (mcp1.read1(3) == LOW)
{ {
preferences.putBool("wifiConfigured", false); preferences.putBool("wifiConfigured", false);
preferences.remove("txPower"); preferences.remove("txPower");
WiFi.eraseAP(); WiFi.eraseAP();
queueLedEffect(LED_EFFECT_WIFI_ERASE_SETTINGS); auto& ledHandler = getLedHandler();
ledHandler.queueEffect(LED_EFFECT_WIFI_ERASE_SETTINGS);
} }
} }
{ {
if (mcp1.digitalRead(0) == LOW) if (mcp1.read1(0) == LOW)
{ {
// Then loop forever to prevent anything else from writing to the screen // Then loop forever to prevent anything else from writing to the screen
while (true) while (true)
@ -54,10 +76,11 @@ void setup()
delay(1000); delay(1000);
} }
} }
else if (mcp1.digitalRead(1) == LOW) else if (mcp1.read1(1) == LOW)
{ {
preferences.clear(); preferences.clear();
queueLedEffect(LED_EFFECT_WIFI_ERASE_SETTINGS); auto& ledHandler = getLedHandler();
ledHandler.queueEffect(LED_EFFECT_WIFI_ERASE_SETTINGS);
nvs_flash_erase(); nvs_flash_erase();
delay(1000); delay(1000);
@ -66,6 +89,7 @@ void setup()
} }
setupWifi(); setupWifi();
// loadIcons();
setupWebserver(); setupWebserver();
@ -75,45 +99,58 @@ void setup()
setupTasks(); setupTasks();
setupTimers(); setupTimers();
if (preferences.getBool("useNostr", DEFAULT_USE_NOSTR) || preferences.getBool("nostrZapNotify", DEFAULT_ZAP_NOTIFY_ENABLED)) // Setup data sources (includes Nostr zap notifications if enabled)
{ setupDataSource();
setupNostrNotify(preferences.getBool("useNostr", DEFAULT_USE_NOSTR), preferences.getBool("nostrZapNotify", DEFAULT_ZAP_NOTIFY_ENABLED));
setupNostrTask();
}
if (!preferences.getBool("useNostr", DEFAULT_USE_NOSTR))
{
xTaskCreate(setupWebsocketClients, "setupWebsocketClients", 8192, NULL,
tskIDLE_PRIORITY, NULL);
}
if (preferences.getBool("bitaxeEnabled", DEFAULT_BITAXE_ENABLED)) if (preferences.getBool("bitaxeEnabled", DEFAULT_BITAXE_ENABLED))
{ {
setupBitaxeFetchTask(); BitAxeFetch::getInstance().setup();
} }
setupButtonTask(); if (preferences.getBool("miningPoolStats", DEFAULT_MINING_POOL_STATS_ENABLED))
{
MiningPoolStatsFetch::getInstance().setup();
}
ButtonHandler::setup();
setupOTA(); setupOTA();
waitUntilNoneBusy(); EPDManager::getInstance().waitUntilNoneBusy();
#ifdef HAS_FRONTLIGHT #ifdef HAS_FRONTLIGHT
if (!preferences.getBool("flAlwaysOn", DEFAULT_FL_ALWAYS_ON)) if (!preferences.getBool("flAlwaysOn", DEFAULT_FL_ALWAYS_ON))
{ {
frontlightFadeOutAll(preferences.getUInt("flEffectDelay"), true); auto& ledHandler = getLedHandler();
ledHandler.frontlightFadeOutAll(preferences.getUInt("flEffectDelay"), true);
flArray.allOFF(); flArray.allOFF();
} }
#endif #endif
forceFullRefresh(); EPDManager::getInstance().forceFullRefresh();
} }
void setupWifi() void setupWifi()
{ {
WiFi.onEvent(WiFiEvent); WiFi.onEvent(WiFiEvent);
// wifi_country_t country = {
// .cc = "NL",
// .schan = 1,
// .nchan = 13,
// .policy = WIFI_COUNTRY_POLICY_MANUAL
// };
// esp_err_t err = esp_wifi_set_country(&country);
// if (err != ESP_OK) {
// Serial.printf("Failed to set country: %d\n", err);
// }
WiFi.setAutoConnect(true); WiFi.setAutoConnect(true);
WiFi.setAutoReconnect(true); WiFi.setAutoReconnect(true);
WiFi.begin(); WiFi.begin();
if (preferences.getInt("txPower", DEFAULT_TX_POWER)) if (preferences.getInt("txPower", DEFAULT_TX_POWER))
{ {
if (WiFi.setTxPower( if (WiFi.setTxPower(
@ -127,12 +164,13 @@ void setupWifi()
// if (!preferences.getBool("wifiConfigured", DEFAULT_WIFI_CONFIGURED) // if (!preferences.getBool("wifiConfigured", DEFAULT_WIFI_CONFIGURED)
{ {
queueLedEffect(LED_EFFECT_WIFI_WAIT_FOR_CONFIG); auto& ledHandler = getLedHandler();
ledHandler.queueEffect(LED_EFFECT_WIFI_WAIT_FOR_CONFIG);
bool buttonPress = false; bool buttonPress = false;
{ {
std::lock_guard<std::mutex> lockMcp(mcpMutex); std::lock_guard<std::mutex> lockMcp(mcpMutex);
buttonPress = (mcp1.digitalRead(2) == LOW); buttonPress = (mcp1.read1(2) == LOW);
} }
{ {
@ -140,33 +178,34 @@ void setupWifi()
byte mac[6]; byte mac[6];
WiFi.macAddress(mac); WiFi.macAddress(mac);
String softAP_SSID = String softAP_SSID = getMyHostname();
String("BTClock" + String(mac[5], 16) + String(mac[1], 16));
WiFi.setHostname(softAP_SSID.c_str()); WiFi.setHostname(softAP_SSID.c_str());
String softAP_password = replaceAmbiguousChars( String softAP_password = replaceAmbiguousChars(
base64::encode(String(mac[2], 16) + String(mac[4], 16) + base64::encode(String(mac[2], 16) + String(mac[4], 16) +
String(mac[5], 16) + String(mac[1], 16)) String(mac[5], 16) + String(mac[1], 16) + String(mac[3], 16))
.substring(2, 10)); .substring(2, 10));
wm.setConfigPortalTimeout(preferences.getUInt("wpTimeout", DEFAULT_WP_TIMEOUT)); wm.setConfigPortalTimeout(preferences.getUInt("wpTimeout", DEFAULT_WP_TIMEOUT));
wm.setWiFiAutoReconnect(false); wm.setWiFiAutoReconnect(false);
wm.setDebugOutput(false); wm.setDebugOutput(false);
wm.setCountry("NL");
wm.setConfigPortalBlocking(true); wm.setConfigPortalBlocking(true);
wm.setAPCallback([&](WiFiManager *wifiManager) wm.setAPCallback([&](WiFiManager *wifiManager)
{ {
// Serial.printf("Entered config mode:ip=%s, ssid='%s', pass='%s'\n", Serial.printf("Entered config mode:ip=%s, ssid='%s', pass='%s'\n",
// WiFi.softAPIP().toString().c_str(), WiFi.softAPIP().toString().c_str(),
// wifiManager->getConfigPortalSSID().c_str(), wifiManager->getConfigPortalSSID().c_str(),
// softAP_password.c_str()); softAP_password.c_str());
// delay(6000); // delay(6000);
setFgColor(GxEPD_BLACK); EPDManager::getInstance().setForegroundColor(GxEPD_BLACK);
setBgColor(GxEPD_WHITE); EPDManager::getInstance().setBackgroundColor(GxEPD_WHITE);
const String qrText = "qrWIFI:S:" + wifiManager->getConfigPortalSSID() + const String qrText = "qrWIFI:S:" + wifiManager->getConfigPortalSSID() +
";T:WPA;P:" + softAP_password.c_str() + ";;"; ";T:WPA;P:" + softAP_password.c_str() + ";;";
const String explainText = "*SSID: *\r\n" + const String explainText = "*SSID: *\r\n" +
wifiManager->getConfigPortalSSID() + wifiManager->getConfigPortalSSID() +
"\r\n\r\n*Password:*\r\n" + softAP_password; "\r\n\r\n*Password:*\r\n" + softAP_password +
"\r\n\r\n*Hostname*:\r\n" + getMyHostname();
// Set the UNIX timestamp // Set the UNIX timestamp
time_t timestamp = LAST_BUILD_TIME; // Example timestamp: March 7, 2021 00:00:00 UTC time_t timestamp = LAST_BUILD_TIME; // Example timestamp: March 7, 2021 00:00:00 UTC
@ -176,67 +215,36 @@ void setupWifi()
// Format the date // Format the date
char formattedDate[20]; char formattedDate[20];
strftime(formattedDate, sizeof(formattedDate), "%y-%m-%d\r\n%H:%M:%S", timeinfo); strftime(formattedDate, sizeof(formattedDate), "%y-%m-%d\r\n%H:%M:%S", timeinfo);
String hwStr = String(HW_REV);
hwStr.replace("_EPD_", "\r\nEPD_");
std::array<String, NUM_SCREENS> epdContent = { std::array<String, NUM_SCREENS> epdContent = {
"Welcome!", "Welcome!",
"Bienvenidos!", "Bienvenidos!",
"To setup\r\nscan QR or\r\nconnect\r\nmanually", "To setup\r\nscan QR or\r\nconnect\r\nmanually",
"Para\r\nconfigurar\r\nescanear QR\r\no conectar\r\nmanualmente", "Para\r\nconfigurar\r\nescanear QR\r\no conectar\r\nmanualmente",
explainText, explainText,
"*Hostname*:\r\n" + getMyHostname() + "\r\n\r\n" + "*FW build date:*\r\n" + formattedDate, "*HW version:*\r\n" + hwStr +
#ifdef GIT_TAG
"\r\n\r\n*SW Version:*\r\n" + GIT_TAG +
#endif
"\r\n\r\n*FW build date:*\r\n" + formattedDate,
qrText}; qrText};
setEpdContent(epdContent); });
EPDManager::getInstance().setContent(epdContent); });
wm.setSaveConfigCallback([]() wm.setSaveConfigCallback([]()
{ {
preferences.putBool("wifiConfigured", true); preferences.putBool("wifiConfigured", true);
delay(1000); delay(1000);
// just restart after succes // just restart after success
ESP.restart(); }); ESP.restart(); });
bool ac = wm.autoConnect(softAP_SSID.c_str(), softAP_password.c_str()); bool ac = wm.autoConnect(softAP_SSID.c_str(), softAP_password.c_str());
// waitUntilNoneBusy();
// std::array<String, NUM_SCREENS> epdContent = {"Welcome!",
// "Bienvenidos!", "Use\r\nweb-interface\r\nto configure", "Use\r\nla
// interfaz web\r\npara configurar", "Or
// restart\r\nwhile\r\nholding\r\n2nd button\r\r\nto start\r\n QR-config",
// "O reinicie\r\nmientras\r\n mantiene presionado\r\nel segundo
// botón\r\r\npara iniciar\r\nQR-config", ""}; setEpdContent(epdContent);
// esp_task_wdt_init(30, false);
// uint count = 0;
// while (WiFi.status() != WL_CONNECTED)
// {
// if (Serial.available() > 0)
// {
// uint8_t b = Serial.read();
// if (parse_improv_serial_byte(x_position, b, x_buffer,
// onImprovCommandCallback, onImprovErrorCallback))
// {
// x_buffer[x_position++] = b;
// }
// else
// {
// x_position = 0;
// }
// }
// count++;
// if (count > 2000000) {
// queueLedEffect(LED_EFFECT_HEARTBEAT);
// count = 0;
// }
// }
// esp_task_wdt_deinit();
// esp_task_wdt_reset();
} }
EPDManager::getInstance().setForegroundColor(preferences.getUInt("fgColor", isWhiteVersion() ? GxEPD_BLACK : GxEPD_WHITE));
EPDManager::getInstance().setBackgroundColor(preferences.getUInt("bgColor", isWhiteVersion() ? GxEPD_WHITE : GxEPD_BLACK));
setFgColor(preferences.getUInt("fgColor", isWhiteVersion() ? GxEPD_BLACK : GxEPD_WHITE));
setBgColor(preferences.getUInt("bgColor", isWhiteVersion() ? GxEPD_WHITE : GxEPD_BLACK));
} }
// else // else
// { // {
@ -257,6 +265,8 @@ void syncTime()
while (!getLocalTime(&timeinfo)) while (!getLocalTime(&timeinfo))
{ {
auto& ledHandler = getLedHandler();
ledHandler.queueEffect(LED_EFFECT_CONFIGURING);
configTime(preferences.getInt("gmtOffset", DEFAULT_TIME_OFFSET_SECONDS), 0, configTime(preferences.getInt("gmtOffset", DEFAULT_TIME_OFFSET_SECONDS), 0,
NTP_SERVER); NTP_SERVER);
delay(500); delay(500);
@ -270,20 +280,44 @@ void setupPreferences()
{ {
preferences.begin("btclock", false); preferences.begin("btclock", false);
setFgColor(preferences.getUInt("fgColor", DEFAULT_FG_COLOR)); EPDManager::getInstance().setForegroundColor(preferences.getUInt("fgColor", DEFAULT_FG_COLOR));
setBgColor(preferences.getUInt("bgColor", DEFAULT_BG_COLOR)); EPDManager::getInstance().setBackgroundColor(preferences.getUInt("bgColor", DEFAULT_BG_COLOR));
setBlockHeight(preferences.getUInt("blockHeight", INITIAL_BLOCK_HEIGHT)); BlockNotify::getInstance().setBlockHeight(preferences.getUInt("blockHeight", INITIAL_BLOCK_HEIGHT));
setPrice(preferences.getUInt("lastPrice", INITIAL_LAST_PRICE), CURRENCY_USD); setPrice(preferences.getUInt("lastPrice", INITIAL_LAST_PRICE), CURRENCY_USD);
if (preferences.getBool("ownDataSource", DEFAULT_OWN_DATA_SOURCE)) if (!preferences.isKey("enableDebugLog")) {
setCurrentCurrency(preferences.getUChar("lastCurrency", CURRENCY_USD)); preferences.putBool("enableDebugLog", DEFAULT_ENABLE_DEBUG_LOG);
else }
setCurrentCurrency(CURRENCY_USD);
if (!preferences.isKey("dataSource")) {
preferences.putUChar("dataSource", DEFAULT_DATA_SOURCE);
}
// Initialize custom endpoint settings if not set
if (!preferences.isKey("customEndpoint")) {
preferences.putString("customEndpoint", DEFAULT_CUSTOM_ENDPOINT);
}
if (!preferences.isKey("customEndpointDisableSSL")) {
preferences.putBool("customEndpointDisableSSL", DEFAULT_CUSTOM_ENDPOINT_DISABLE_SSL);
}
// Set currency based on data source
DataSourceType dataSource = static_cast<DataSourceType>(preferences.getUChar("dataSource", DEFAULT_DATA_SOURCE));
if (dataSource == BTCLOCK_SOURCE || dataSource == CUSTOM_SOURCE) {
ScreenHandler::setCurrentCurrency(preferences.getUChar("lastCurrency", CURRENCY_USD));
} else {
ScreenHandler::setCurrentCurrency(CURRENCY_USD);
}
if (!preferences.isKey("flDisable")) { if (!preferences.isKey("flDisable")) {
preferences.putBool("flDisable", isWhiteVersion() ? false : true); preferences.putBool("flDisable", isWhiteVersion() ? false : true);
} }
if (!preferences.isKey("gitReleaseUrl")) {
preferences.putString("gitReleaseUrl", DEFAULT_GIT_RELEASE_URL);
}
if (!preferences.isKey("fgColor")) { if (!preferences.isKey("fgColor")) {
preferences.putUInt("fgColor", isWhiteVersion() ? GxEPD_BLACK : GxEPD_WHITE); preferences.putUInt("fgColor", isWhiteVersion() ? GxEPD_BLACK : GxEPD_WHITE);
preferences.putUInt("bgColor", isWhiteVersion() ? GxEPD_WHITE : GxEPD_BLACK); preferences.putUInt("bgColor", isWhiteVersion() ? GxEPD_WHITE : GxEPD_BLACK);
@ -323,6 +357,14 @@ void setupPreferences()
addScreenMapping(SCREEN_BITAXE_HASHRATE, "BitAxe Hashrate"); addScreenMapping(SCREEN_BITAXE_HASHRATE, "BitAxe Hashrate");
addScreenMapping(SCREEN_BITAXE_BESTDIFF, "BitAxe Best Difficulty"); addScreenMapping(SCREEN_BITAXE_BESTDIFF, "BitAxe Best Difficulty");
} }
if (preferences.getBool("miningPoolStats", DEFAULT_MINING_POOL_STATS_ENABLED))
{
addScreenMapping(SCREEN_MINING_POOL_STATS_HASHRATE, "Mining Pool Hashrate");
if (MiningPoolStatsFetch::getInstance().getPool()->supportsDailyEarnings()) {
addScreenMapping(SCREEN_MINING_POOL_STATS_EARNINGS, "Mining Pool Earnings");
}
}
} }
String replaceAmbiguousChars(String input) String replaceAmbiguousChars(String input)
@ -338,77 +380,17 @@ String replaceAmbiguousChars(String input)
return input; return input;
} }
// void addCurrencyMappings(const std::vector<std::string>& currencies)
// {
// for (const auto& currency : currencies)
// {
// int satsPerCurrencyScreen;
// int btcTickerScreen;
// int marketCapScreen;
// // Determine the corresponding screen IDs based on the currency code
// if (currency == "USD")
// {
// satsPerCurrencyScreen = SCREEN_SATS_PER_CURRENCY_USD;
// btcTickerScreen = SCREEN_BTC_TICKER_USD;
// marketCapScreen = SCREEN_MARKET_CAP_USD;
// }
// else if (currency == "EUR")
// {
// satsPerCurrencyScreen = SCREEN_SATS_PER_CURRENCY_EUR;
// btcTickerScreen = SCREEN_BTC_TICKER_EUR;
// marketCapScreen = SCREEN_MARKET_CAP_EUR;
// }
// else if (currency == "GBP")
// {
// satsPerCurrencyScreen = SCREEN_SATS_PER_CURRENCY_GBP;
// btcTickerScreen = SCREEN_BTC_TICKER_GBP;
// marketCapScreen = SCREEN_MARKET_CAP_GBP;
// }
// else if (currency == "JPY")
// {
// satsPerCurrencyScreen = SCREEN_SATS_PER_CURRENCY_JPY;
// btcTickerScreen = SCREEN_BTC_TICKER_JPY;
// marketCapScreen = SCREEN_MARKET_CAP_JPY;
// }
// else if (currency == "AUD")
// {
// satsPerCurrencyScreen = SCREEN_SATS_PER_CURRENCY_AUD;
// btcTickerScreen = SCREEN_BTC_TICKER_AUD;
// marketCapScreen = SCREEN_MARKET_CAP_AUD;
// }
// else if (currency == "CAD")
// {
// satsPerCurrencyScreen = SCREEN_SATS_PER_CURRENCY_CAD;
// btcTickerScreen = SCREEN_BTC_TICKER_CAD;
// marketCapScreen = SCREEN_MARKET_CAP_CAD;
// }
// else
// {
// continue; // Unknown currency, skip it
// }
// // Create the string locally to ensure it persists
// std::string satsPerCurrencyString = "Sats per " + currency;
// std::string btcTickerString = "Ticker " + currency;
// std::string marketCapString = "Market Cap " + currency;
// // Pass the c_str() to the function
// addScreenMapping(satsPerCurrencyScreen, satsPerCurrencyString.c_str());
// addScreenMapping(btcTickerScreen, btcTickerString.c_str());
// addScreenMapping(marketCapScreen, marketCapString.c_str());
// }
// }
void setupWebsocketClients(void *pvParameters) void setupWebsocketClients(void *pvParameters)
{ {
if (preferences.getBool("ownDataSource", DEFAULT_OWN_DATA_SOURCE)) DataSourceType dataSource = getDataSource();
if (dataSource == BTCLOCK_SOURCE || dataSource == CUSTOM_SOURCE)
{ {
V2Notify::setupV2Notify(); V2Notify::setupV2Notify();
} }
else else if (dataSource == THIRD_PARTY_SOURCE)
{ {
setupBlockNotify(); BlockNotify::getInstance().setup();
setupPriceNotify(); setupPriceNotify();
} }
@ -425,13 +407,14 @@ void setupTimers()
void finishSetup() void finishSetup()
{ {
auto& ledHandler = getLedHandler();
if (preferences.getBool("ledStatus", DEFAULT_LED_STATUS)) if (preferences.getBool("ledStatus", DEFAULT_LED_STATUS))
{ {
restoreLedState(); ledHandler.restoreLedState();
} }
else else
{ {
clearLeds(); ledHandler.clear();
} }
} }
@ -480,18 +463,9 @@ void setupHardware()
Serial.println(F("Error loading WebUI")); Serial.println(F("Error loading WebUI"));
} }
// if (!LittleFS.exists("/qr.txt")) // Initialize LED handler
// { auto& ledHandler = getLedHandler();
// File f = LittleFS.open("/qr.txt", "w"); ledHandler.setup();
// if(f) {
// } else {
// Serial.println(F("Can't write QR to FS"));
// }
// }
setupLeds();
WiFi.setHostname(getMyHostname().c_str()); WiFi.setHostname(getMyHostname().c_str());
if (!psramInit()) if (!psramInit())
@ -503,29 +477,34 @@ void setupHardware()
Wire.begin(I2C_SDA_PIN, I2C_SCK_PIN, 400000); Wire.begin(I2C_SDA_PIN, I2C_SCK_PIN, 400000);
if (!mcp1.begin_I2C(0x20)) if (!mcp1.begin()) {
{
Serial.println(F("Error MCP23017 1")); Serial.println(F("Error MCP23017 1"));
} else {
// while (1)
// ;
}
else
{
pinMode(MCP_INT_PIN, INPUT_PULLUP); pinMode(MCP_INT_PIN, INPUT_PULLUP);
mcp1.setupInterrupts(false, false, LOW);
for (int i = 0; i < 4; i++) // Enable mirrored interrupts (both INTA and INTB pins signal any interrupt)
{ if (!mcp1.mirrorInterrupts(true)) {
mcp1.pinMode(i, INPUT_PULLUP); Serial.println(F("Error setting up mirrored interrupts"));
mcp1.setupInterruptPin(i, LOW);
} }
#ifndef IS_BTCLOCK_V8
for (int i = 8; i <= 14; i++) // Configure all 4 button pins as inputs with pullups and interrupts
{ for (int i = 0; i < 4; i++) {
mcp1.pinMode(i, OUTPUT); if (!mcp1.pinMode1(i, INPUT_PULLUP)) {
Serial.printf("Error setting pin %d to input pull up\n", i);
} }
#endif // Enable interrupt on CHANGE for each pin
if (!mcp1.enableInterrupt(i, CHANGE)) {
Serial.printf("Error enabling interrupt for pin %d\n", i);
}
}
// Set interrupt pins as open drain with active-low polarity
if (!mcp1.setInterruptPolarity(2)) { // 2 = Open drain
Serial.println(F("Error setting interrupt polarity"));
}
// Clear any pending interrupts
mcp1.getInterruptCaptureRegister();
} }
#ifdef IS_HW_REV_B #ifdef IS_HW_REV_B
@ -534,7 +513,7 @@ void setupHardware()
#endif #endif
#ifdef IS_BTCLOCK_V8 #ifdef IS_BTCLOCK_V8
if (!mcp2.begin_I2C(0x21)) if (!mcp2.begin())
{ {
Serial.println(F("Error MCP23017 2")); Serial.println(F("Error MCP23017 2"));
@ -544,7 +523,8 @@ void setupHardware()
#endif #endif
#ifdef HAS_FRONTLIGHT #ifdef HAS_FRONTLIGHT
setupFrontlight(); // Initialize frontlight through LedHandler
ledHandler.initializeFrontlight();
Wire.beginTransmission(0x5C); Wire.beginTransmission(0x5C);
byte error = Wire.endTransmission(); byte error = Wire.endTransmission();
@ -566,6 +546,7 @@ void setupHardware()
void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info) void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
{ {
static bool first_connect = true; static bool first_connect = true;
auto& ledHandler = getLedHandler(); // Get ledHandler reference once at the start
Serial.printf("[WiFi-event] event: %d\n", event); Serial.printf("[WiFi-event] event: %d\n", event);
@ -591,7 +572,7 @@ void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
if (!first_connect) if (!first_connect)
{ {
Serial.println(F("Disconnected from WiFi access point")); Serial.println(F("Disconnected from WiFi access point"));
queueLedEffect(LED_EFFECT_WIFI_CONNECT_ERROR); ledHandler.queueEffect(LED_EFFECT_WIFI_CONNECT_ERROR);
uint8_t reason = info.wifi_sta_disconnected.reason; uint8_t reason = info.wifi_sta_disconnected.reason;
if (reason) if (reason)
Serial.printf("Disconnect reason: %s, ", Serial.printf("Disconnect reason: %s, ",
@ -607,13 +588,13 @@ void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
Serial.print("Obtained IP address: "); Serial.print("Obtained IP address: ");
Serial.println(WiFi.localIP()); Serial.println(WiFi.localIP());
if (!first_connect) if (!first_connect)
queueLedEffect(LED_EFFECT_WIFI_CONNECT_SUCCESS); ledHandler.queueEffect(LED_EFFECT_WIFI_CONNECT_SUCCESS);
first_connect = false; first_connect = false;
break; break;
} }
case ARDUINO_EVENT_WIFI_STA_LOST_IP: case ARDUINO_EVENT_WIFI_STA_LOST_IP:
Serial.println(F("Lost IP address and IP address is reset to 0")); Serial.println(F("Lost IP address and IP address is reset to 0"));
queueLedEffect(LED_EFFECT_WIFI_CONNECT_ERROR); ledHandler.queueEffect(LED_EFFECT_WIFI_CONNECT_ERROR);
WiFi.reconnect(); WiFi.reconnect();
break; break;
case ARDUINO_EVENT_WIFI_AP_START: case ARDUINO_EVENT_WIFI_AP_START:
@ -663,29 +644,6 @@ uint getLastTimeSync()
} }
#ifdef HAS_FRONTLIGHT #ifdef HAS_FRONTLIGHT
void setupFrontlight()
{
if (!flArray.begin(PCA9685_MODE1_AUTOINCR | PCA9685_MODE1_ALLCALL, PCA9685_MODE2_TOTEMPOLE))
{
Serial.println(F("FL driver error"));
return;
}
Serial.println(F("FL driver active"));
if (!preferences.isKey("flMaxBrightness"))
{
preferences.putUInt("flMaxBrightness", DEFAULT_FL_MAX_BRIGHTNESS);
}
if (!preferences.isKey("flEffectDelay"))
{
preferences.putUInt("flEffectDelay", DEFAULT_FL_EFFECT_DELAY);
}
if (!preferences.isKey("flFlashOnUpd"))
{
preferences.putBool("flFlashOnUpd", DEFAULT_FL_FLASH_ON_UPDATE);
}
}
float getLightLevel() float getLightLevel()
{ {
@ -787,3 +745,28 @@ const char* getFirmwareFilename() {
return ""; return "";
} }
} }
const char* getWebUiFilename() {
if (HW_REV == "REV_B_EPD_2_13") {
return "littlefs_8MB.bin";
} else if (HW_REV == "REV_A_EPD_2_13") {
return "littlefs_4MB.bin";
} else if (HW_REV == "REV_A_EPD_2_9") {
return "littlefs_4MB.bin";
} else {
return "littlefs_4MB.bin";
}
}
bool debugLogEnabled()
{
return preferences.getBool("enableDebugLog", DEFAULT_ENABLE_DEBUG_LOG);
}
DataSourceType getDataSource() {
return static_cast<DataSourceType>(preferences.getUChar("dataSource", DEFAULT_DATA_SOURCE));
}
void setDataSource(DataSourceType source) {
preferences.putUChar("dataSource", static_cast<uint8_t>(source));
}

View file

@ -1,6 +1,6 @@
#pragma once #pragma once
#include <Adafruit_MCP23X17.h> #include <MCP23017.h>
#include <Arduino.h> #include <Arduino.h>
#include <Preferences.h> #include <Preferences.h>
#include <WiFiClientSecure.h> #include <WiFiClientSecure.h>
@ -18,6 +18,7 @@
#include "lib/ota.hpp" #include "lib/ota.hpp"
#include "lib/nostr_notify.hpp" #include "lib/nostr_notify.hpp"
#include "lib/bitaxe_fetch.hpp" #include "lib/bitaxe_fetch.hpp"
#include "lib/mining_pool_stats_fetch.hpp"
#include "lib/v2_notify.hpp" #include "lib/v2_notify.hpp"
@ -30,9 +31,11 @@
#include "BH1750.h" #include "BH1750.h"
#endif #endif
#include "shared.hpp"
#include "defaults.hpp"
#define NTP_SERVER "pool.ntp.org" #define NTP_SERVER "pool.ntp.org"
#define DEFAULT_TIME_OFFSET_SECONDS 3600 #define DEFAULT_TIME_OFFSET_SECONDS 3600
#define USER_AGENT "BTClock/3.0"
#ifndef MCP_DEV_ADDR #ifndef MCP_DEV_ADDR
#define MCP_DEV_ADDR 0x20 #define MCP_DEV_ADDR 0x20
#endif #endif
@ -49,10 +52,10 @@ void setupTimers();
void finishSetup(); void finishSetup();
void setupMcp(); void setupMcp();
#ifdef HAS_FRONTLIGHT #ifdef HAS_FRONTLIGHT
void setupFrontlight(); extern BH1750 bh1750;
extern bool hasLuxSensor;
float getLightLevel(); float getLightLevel();
bool hasLightLevel(); bool hasLightLevel();
extern PCA9685 flArray;
#endif #endif
String getMyHostname(); String getMyHostname();
@ -77,6 +80,8 @@ String getHwRev();
bool isWhiteVersion(); bool isWhiteVersion();
String getFsRev(); String getFsRev();
bool debugLogEnabled();
void addScreenMapping(int value, const char* name); void addScreenMapping(int value, const char* name);
// void addScreenMapping(int value, const String& name); // void addScreenMapping(int value, const String& name);
// void addScreenMapping(int value, const std::string& name); // void addScreenMapping(int value, const std::string& name);
@ -84,3 +89,19 @@ void addScreenMapping(int value, const char* name);
int findScreenIndexByValue(int value); int findScreenIndexByValue(int value);
String replaceAmbiguousChars(String input); String replaceAmbiguousChars(String input);
const char* getFirmwareFilename(); const char* getFirmwareFilename();
const char* getWebUiFilename();
// void loadIcons();
extern Preferences preferences;
extern MCP23017 mcp1;
#ifdef IS_BTCLOCK_V8
extern MCP23017 mcp2;
#endif
#ifdef HAS_FRONTLIGHT
extern PCA9685 flArray;
#endif
// Expose DataSourceType enum
extern DataSourceType getDataSource();
extern void setDataSource(DataSourceType source);

View file

@ -1,4 +1,6 @@
#define INITIAL_BLOCK_HEIGHT 851500 #pragma once
#define INITIAL_BLOCK_HEIGHT 876600
#define INITIAL_LAST_PRICE 50000 #define INITIAL_LAST_PRICE 50000
#define DEFAULT_TX_POWER 0 #define DEFAULT_TX_POWER 0
@ -16,10 +18,10 @@
#define DEFAULT_SUFFIX_PRICE false #define DEFAULT_SUFFIX_PRICE false
#define DEFAULT_DISABLE_LEDS false #define DEFAULT_DISABLE_LEDS false
#define DEFAULT_DISABLE_FL false #define DEFAULT_DISABLE_FL false
#define DEFAULT_OWN_DATA_SOURCE true #define DEFAULT_MOW_MODE false
#define DEFAULT_STAGING_SOURCE false #define DEFAULT_SUFFIX_SHARE_DOT false
#define DEFAULT_V2_SOURCE_CURRENCY CURRENCY_USD
#define DEFAULT_V2_SOURCE_CURRENCY CURRENCY_USD
#define DEFAULT_TIME_OFFSET_SECONDS 3600 #define DEFAULT_TIME_OFFSET_SECONDS 3600
@ -42,8 +44,10 @@
#define DEFAULT_FL_EFFECT_DELAY 15 #define DEFAULT_FL_EFFECT_DELAY 15
#define DEFAULT_LUX_LIGHT_TOGGLE 128 #define DEFAULT_LUX_LIGHT_TOGGLE 128
#define DEFAULT_FL_ALWAYS_ON false #define DEFAULT_FL_OFF_WHEN_DARK true
#define DEFAULT_FL_FLASH_ON_UPDATE false
#define DEFAULT_FL_ALWAYS_ON true
#define DEFAULT_FL_FLASH_ON_UPDATE true
#define DEFAULT_LED_STATUS false #define DEFAULT_LED_STATUS false
#define DEFAULT_TIMER_ACTIVE true #define DEFAULT_TIMER_ACTIVE true
@ -53,11 +57,45 @@
#define DEFAULT_BITAXE_ENABLED false #define DEFAULT_BITAXE_ENABLED false
#define DEFAULT_BITAXE_HOSTNAME "bitaxe1" #define DEFAULT_BITAXE_HOSTNAME "bitaxe1"
#define DEFAULT_MINING_POOL_STATS_ENABLED false
#define DEFAULT_MINING_POOL_NAME "ocean"
#define DEFAULT_MINING_POOL_USER "38Qkkei3SuF1Eo45BaYmRHUneRD54yyTFy" // Random actual Ocean hasher
#define DEFAULT_LOCAL_POOL_ENDPOINT "umbrel.local:2019"
#define DEFAULT_ZAP_NOTIFY_ENABLED false #define DEFAULT_ZAP_NOTIFY_ENABLED false
#define DEFAULT_ZAP_NOTIFY_PUBKEY "b5127a08cf33616274800a4387881a9f98e04b9c37116e92de5250498635c422" #define DEFAULT_ZAP_NOTIFY_PUBKEY "b5127a08cf33616274800a4387881a9f98e04b9c37116e92de5250498635c422"
#define DEFAULT_LED_FLASH_ON_ZAP true
#define DEFAULT_FL_FLASH_ON_ZAP true
#define DEFAULT_FONT_NAME "antonio"
#define DEFAULT_HTTP_AUTH_ENABLED false #define DEFAULT_HTTP_AUTH_ENABLED false
#define DEFAULT_HTTP_AUTH_USERNAME "btclock" #define DEFAULT_HTTP_AUTH_USERNAME "btclock"
#define DEFAULT_HTTP_AUTH_PASSWORD "satoshi" #define DEFAULT_HTTP_AUTH_PASSWORD "satoshi"
#define DEFAULT_ACTIVE_CURRENCIES "USD,EUR,JPY" #define DEFAULT_ACTIVE_CURRENCIES "USD,EUR,JPY"
#define DEFAULT_GIT_RELEASE_URL "https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/releases/latest"
#define DEFAULT_VERTICAL_DESC true
#define DEFAULT_MINING_POOL_LOGOS_URL "https://git.btclock.dev/btclock/mining-pool-logos/raw/branch/main"
#define DEFAULT_ENABLE_DEBUG_LOG false
#define DEFAULT_DISABLE_FL false
#define DEFAULT_CUSTOM_ENDPOINT "ws-staging.btclock.dev"
#define DEFAULT_CUSTOM_ENDPOINT_DISABLE_SSL false
#define DEFAULT_MOW_MODE false
// Define data source types
enum DataSourceType {
BTCLOCK_SOURCE = 0, // BTClock's own data source
THIRD_PARTY_SOURCE = 1, // Third party data sources like mempool.space
NOSTR_SOURCE = 2, // Nostr data source
CUSTOM_SOURCE = 3 // Custom data source endpoint
};
#define DEFAULT_DATA_SOURCE BTCLOCK_SOURCE
#ifndef DEFAULT_BOOT_TEXT
#define DEFAULT_BOOT_TEXT "BTCLOCK"
#endif

View file

@ -1,502 +1,372 @@
#include "epd.hpp" #include "epd.hpp"
// Initialize static members
#ifdef IS_BTCLOCK_REV_B #ifdef IS_BTCLOCK_REV_B
Native_Pin EPD_CS[NUM_SCREENS] = { Native_Pin EPDManager::EPD_DC(14);
Native_Pin(2), std::array<Native_Pin, NUM_SCREENS> EPDManager::EPD_CS = {
Native_Pin(4), Native_Pin(2), Native_Pin(4), Native_Pin(6), Native_Pin(10),
Native_Pin(6), Native_Pin(38), Native_Pin(21), Native_Pin(17)
Native_Pin(10),
Native_Pin(38),
Native_Pin(21),
Native_Pin(17),
}; };
Native_Pin EPD_BUSY[NUM_SCREENS] = { std::array<Native_Pin, NUM_SCREENS> EPDManager::EPD_BUSY = {
Native_Pin(3), Native_Pin(3), Native_Pin(5), Native_Pin(7), Native_Pin(9),
Native_Pin(5), Native_Pin(37), Native_Pin(18), Native_Pin(16)
Native_Pin(7),
Native_Pin(9),
Native_Pin(37),
Native_Pin(18),
Native_Pin(16),
}; };
MCP23X17_Pin EPD_RESET_MPD[NUM_SCREENS] = { std::array<MCP23X17_Pin, NUM_SCREENS> EPDManager::EPD_RESET = {
MCP23X17_Pin(mcp1, 8), MCP23X17_Pin(mcp1, 8), MCP23X17_Pin(mcp1, 9), MCP23X17_Pin(mcp1, 10),
MCP23X17_Pin(mcp1, 9), MCP23X17_Pin(mcp1, 11), MCP23X17_Pin(mcp1, 12), MCP23X17_Pin(mcp1, 13),
MCP23X17_Pin(mcp1, 10), MCP23X17_Pin(mcp1, 14)
MCP23X17_Pin(mcp1, 11),
MCP23X17_Pin(mcp1, 12),
MCP23X17_Pin(mcp1, 13),
MCP23X17_Pin(mcp1, 14),
}; };
#elif defined(IS_BTCLOCK_V8)
Native_Pin EPD_DC = Native_Pin(14); Native_Pin EPDManager::EPD_DC(38);
#elif IS_BTCLOCK_V8 std::array<MCP23X17_Pin, NUM_SCREENS> EPDManager::EPD_BUSY = {
Native_Pin EPD_DC = Native_Pin(38); MCP23X17_Pin(mcp1, 8), MCP23X17_Pin(mcp1, 9), MCP23X17_Pin(mcp1, 10),
MCP23X17_Pin(mcp1, 11), MCP23X17_Pin(mcp1, 12), MCP23X17_Pin(mcp1, 13),
MCP23X17_Pin EPD_BUSY[NUM_SCREENS] = { MCP23X17_Pin(mcp1, 14), MCP23X17_Pin(mcp1, 4)
MCP23X17_Pin(mcp1, 8),
MCP23X17_Pin(mcp1, 9),
MCP23X17_Pin(mcp1, 10),
MCP23X17_Pin(mcp1, 11),
MCP23X17_Pin(mcp1, 12),
MCP23X17_Pin(mcp1, 13),
MCP23X17_Pin(mcp1, 14),
MCP23X17_Pin(mcp1, 4),
}; };
std::array<MCP23X17_Pin, NUM_SCREENS> EPDManager::EPD_CS = {
MCP23X17_Pin EPD_CS[NUM_SCREENS] = {
MCP23X17_Pin(mcp2, 8), MCP23X17_Pin(mcp2, 10), MCP23X17_Pin(mcp2, 12), MCP23X17_Pin(mcp2, 8), MCP23X17_Pin(mcp2, 10), MCP23X17_Pin(mcp2, 12),
MCP23X17_Pin(mcp2, 14), MCP23X17_Pin(mcp2, 0), MCP23X17_Pin(mcp2, 2), MCP23X17_Pin(mcp2, 14), MCP23X17_Pin(mcp2, 0), MCP23X17_Pin(mcp2, 2),
MCP23X17_Pin(mcp2, 4), MCP23X17_Pin(mcp2, 6)}; MCP23X17_Pin(mcp2, 4), MCP23X17_Pin(mcp2, 6)
};
MCP23X17_Pin EPD_RESET_MPD[NUM_SCREENS] = { std::array<MCP23X17_Pin, NUM_SCREENS> EPDManager::EPD_RESET = {
MCP23X17_Pin(mcp2, 9), MCP23X17_Pin(mcp2, 9), MCP23X17_Pin(mcp2, 11), MCP23X17_Pin(mcp2, 13),
MCP23X17_Pin(mcp2, 11), MCP23X17_Pin(mcp2, 15), MCP23X17_Pin(mcp2, 1), MCP23X17_Pin(mcp2, 3),
MCP23X17_Pin(mcp2, 13), MCP23X17_Pin(mcp2, 5), MCP23X17_Pin(mcp2, 7)
MCP23X17_Pin(mcp2, 15),
MCP23X17_Pin(mcp2, 1),
MCP23X17_Pin(mcp2, 3),
MCP23X17_Pin(mcp2, 5),
MCP23X17_Pin(mcp2, 7),
}; };
#else #else
Native_Pin EPD_CS[NUM_SCREENS] = { Native_Pin EPDManager::EPD_DC(14);
Native_Pin(2), std::array<Native_Pin, NUM_SCREENS> EPDManager::EPD_CS = {
Native_Pin(4), Native_Pin(2), Native_Pin(4), Native_Pin(6), Native_Pin(10),
Native_Pin(6), Native_Pin(33), Native_Pin(21), Native_Pin(17)
Native_Pin(10),
Native_Pin(33),
Native_Pin(21),
Native_Pin(17),
#if NUM_SCREENS == 9
// MCP23X17_Pin(mcp2, 7),
Native_Pin(-1),
Native_Pin(-1),
#endif
}; };
Native_Pin EPD_BUSY[NUM_SCREENS] = { std::array<Native_Pin, NUM_SCREENS> EPDManager::EPD_BUSY = {
Native_Pin(3), Native_Pin(3), Native_Pin(5), Native_Pin(7), Native_Pin(9),
Native_Pin(5), Native_Pin(37), Native_Pin(18), Native_Pin(16)
Native_Pin(7),
Native_Pin(9),
Native_Pin(37),
Native_Pin(18),
Native_Pin(16),
}; };
MCP23X17_Pin EPD_RESET_MPD[NUM_SCREENS] = { std::array<MCP23X17_Pin, NUM_SCREENS> EPDManager::EPD_RESET = {
MCP23X17_Pin(mcp1, 8), MCP23X17_Pin(mcp1, 8), MCP23X17_Pin(mcp1, 9), MCP23X17_Pin(mcp1, 10),
MCP23X17_Pin(mcp1, 9), MCP23X17_Pin(mcp1, 11), MCP23X17_Pin(mcp1, 12), MCP23X17_Pin(mcp1, 13),
MCP23X17_Pin(mcp1, 10), MCP23X17_Pin(mcp1, 14)
MCP23X17_Pin(mcp1, 11),
MCP23X17_Pin(mcp1, 12),
MCP23X17_Pin(mcp1, 13),
MCP23X17_Pin(mcp1, 14),
}; };
Native_Pin EPD_DC = Native_Pin(14);
#endif #endif
GxEPD2_BW<EPD_CLASS, EPD_CLASS::HEIGHT> displays[NUM_SCREENS] = { EPDManager& EPDManager::getInstance() {
EPD_CLASS(&EPD_CS[0], &EPD_DC, &EPD_RESET_MPD[0], &EPD_BUSY[0]), static EPDManager instance;
EPD_CLASS(&EPD_CS[1], &EPD_DC, &EPD_RESET_MPD[1], &EPD_BUSY[1]), return instance;
EPD_CLASS(&EPD_CS[2], &EPD_DC, &EPD_RESET_MPD[2], &EPD_BUSY[2]), }
EPD_CLASS(&EPD_CS[3], &EPD_DC, &EPD_RESET_MPD[3], &EPD_BUSY[3]),
EPD_CLASS(&EPD_CS[4], &EPD_DC, &EPD_RESET_MPD[4], &EPD_BUSY[4]), EPDManager::EPDManager()
EPD_CLASS(&EPD_CS[5], &EPD_DC, &EPD_RESET_MPD[5], &EPD_BUSY[5]), : currentContent{}
EPD_CLASS(&EPD_CS[6], &EPD_DC, &EPD_RESET_MPD[6], &EPD_BUSY[6]), , content{}
, lastFullRefresh{}
, tasks{}
, updateQueue{nullptr}
, antonioFonts{nullptr, nullptr, nullptr}
, oswaldFonts{nullptr, nullptr, nullptr}
, fontSmall{nullptr}
, fontBig{nullptr}
, fontMedium{nullptr}
, fontSatsymbol{nullptr}
, bgColor{GxEPD_BLACK}
, fgColor{GxEPD_WHITE}
, displays{
#ifdef IS_BTCLOCK_V8 #ifdef IS_BTCLOCK_V8
EPD_CLASS(&EPD_CS[7], &EPD_DC, &EPD_RESET_MPD[7], &EPD_BUSY[7]), EPD_CLASS(&EPD_CS[0], &EPD_DC, &EPD_RESET[0], &EPD_BUSY[0]),
#endif EPD_CLASS(&EPD_CS[1], &EPD_DC, &EPD_RESET[1], &EPD_BUSY[1]),
}; EPD_CLASS(&EPD_CS[2], &EPD_DC, &EPD_RESET[2], &EPD_BUSY[2]),
EPD_CLASS(&EPD_CS[3], &EPD_DC, &EPD_RESET[3], &EPD_BUSY[3]),
std::array<String, NUM_SCREENS> currentEpdContent; EPD_CLASS(&EPD_CS[4], &EPD_DC, &EPD_RESET[4], &EPD_BUSY[4]),
std::array<String, NUM_SCREENS> epdContent; EPD_CLASS(&EPD_CS[5], &EPD_DC, &EPD_RESET[5], &EPD_BUSY[5]),
uint32_t lastFullRefresh[NUM_SCREENS]; EPD_CLASS(&EPD_CS[6], &EPD_DC, &EPD_RESET[6], &EPD_BUSY[6]),
TaskHandle_t tasks[NUM_SCREENS]; EPD_CLASS(&EPD_CS[7], &EPD_DC, &EPD_RESET[7], &EPD_BUSY[7])
// TaskHandle_t epdTaskHandle = NULL;
#define UPDATE_QUEUE_SIZE 14
QueueHandle_t updateQueue;
// SemaphoreHandle_t epdUpdateSemaphore[NUM_SCREENS];
int fgColor = GxEPD_WHITE;
int bgColor = GxEPD_BLACK;
#define FONT_SMALL Antonio_SemiBold20pt7b
#define FONT_BIG Antonio_SemiBold90pt7b
#define FONT_MEDIUM Antonio_SemiBold40pt7b
#define FONT_SATSYMBOL Satoshi_Symbol90pt7b
std::mutex epdUpdateMutex;
std::mutex epdMutex[NUM_SCREENS];
uint8_t qrcode[800];
#ifdef IS_BTCLOCK_V8
#define EPD_TASK_STACK_SIZE 4096
#else #else
#define EPD_TASK_STACK_SIZE 2048 EPD_CLASS(&EPD_CS[0], &EPD_DC, &EPD_RESET[0], &EPD_BUSY[0]),
EPD_CLASS(&EPD_CS[1], &EPD_DC, &EPD_RESET[1], &EPD_BUSY[1]),
EPD_CLASS(&EPD_CS[2], &EPD_DC, &EPD_RESET[2], &EPD_BUSY[2]),
EPD_CLASS(&EPD_CS[3], &EPD_DC, &EPD_RESET[3], &EPD_BUSY[3]),
EPD_CLASS(&EPD_CS[4], &EPD_DC, &EPD_RESET[4], &EPD_BUSY[4]),
EPD_CLASS(&EPD_CS[5], &EPD_DC, &EPD_RESET[5], &EPD_BUSY[5]),
EPD_CLASS(&EPD_CS[6], &EPD_DC, &EPD_RESET[6], &EPD_BUSY[6])
#endif #endif
}
{
}
void forceFullRefresh() EPDManager::~EPDManager() {
{ // Clean up tasks
for (uint i = 0; i < NUM_SCREENS; i++) for (auto& task : tasks) {
{ if (task != nullptr) {
lastFullRefresh[i] = NULL; vTaskDelete(task);
} }
} }
void refreshFromMemory() // Clean up queue
{ if (updateQueue != nullptr) {
for (uint i = 0; i < NUM_SCREENS; i++) vQueueDelete(updateQueue);
{
int *taskParam = new int;
*taskParam = i;
xTaskCreate(
[](void *pvParameters)
{
const int epdIndex = *(int *)pvParameters;
delete (int *)pvParameters;
displays[epdIndex].refresh(false);
vTaskDelete(NULL);
},
"PrepareUpd", 4096, taskParam, tskIDLE_PRIORITY, NULL);
}
} }
void setupDisplays() // Clean up fonts
{ delete antonioFonts.big;
delete antonioFonts.medium;
delete antonioFonts.small;
delete oswaldFonts.big;
delete oswaldFonts.medium;
delete oswaldFonts.small;
}
void EPDManager::initialize() {
// Load fonts based on preference
String fontName = preferences.getString("fontName", DEFAULT_FONT_NAME);
loadFonts(fontName);
// Initialize displays
std::lock_guard<std::mutex> lockMcp(mcpMutex); std::lock_guard<std::mutex> lockMcp(mcpMutex);
for (auto& display : displays) {
for (uint i = 0; i < NUM_SCREENS; i++) display.init(0, true, 30);
{
displays[i].init(0, true, 30);
} }
// Create update queue and task
updateQueue = xQueueCreate(UPDATE_QUEUE_SIZE, sizeof(UpdateDisplayTaskItem)); updateQueue = xQueueCreate(UPDATE_QUEUE_SIZE, sizeof(UpdateDisplayTaskItem));
xTaskCreate(prepareDisplayUpdateTask, "PrepareUpd", EPD_TASK_STACK_SIZE * 2, nullptr, 11, nullptr);
xTaskCreate(prepareDisplayUpdateTask, "PrepareUpd", EPD_TASK_STACK_SIZE, NULL, 11, NULL); // Create display update tasks
for (size_t i = 0; i < NUM_SCREENS; i++) {
for (uint i = 0; i < NUM_SCREENS; i++) auto* taskParam = new int(i);
{ xTaskCreate(updateDisplayTask, ("EpdUpd" + String(i)).c_str(), EPD_TASK_STACK_SIZE,
// epdUpdateSemaphore[i] = xSemaphoreCreateBinary(); taskParam, 11, &tasks[i]);
// xSemaphoreGive(epdUpdateSemaphore[i]);
int *taskParam = new int;
*taskParam = i;
xTaskCreate(updateDisplay, ("EpdUpd" + String(i)).c_str(), EPD_TASK_STACK_SIZE, taskParam,
11, &tasks[i]); // create task
} }
// Hold lower button to enable "storage mode" (prevents burn-in of ePaper displays) // Check for storage mode (prevents burn-in)
if (mcp1.digitalRead(0) == LOW) if (mcp1.read1(0) == LOW) {
{ setForegroundColor(GxEPD_BLACK);
setFgColor(GxEPD_BLACK); setBackgroundColor(GxEPD_WHITE);
setBgColor(GxEPD_WHITE); content.fill("");
} else {
// Initialize with custom text or default
String customText = preferences.getString("displayText", DEFAULT_BOOT_TEXT);
std::array<String, NUM_SCREENS> newContent;
newContent.fill(" ");
epdContent.fill(""); for (size_t i = 0; i < std::min(customText.length(), (size_t)NUM_SCREENS); i++) {
} newContent[i] = String(customText[i]);
else
{
#ifdef IS_BTCLOCK_V8
epdContent = {"B", "T", "C", "L", "O", "C", "K", "v8"};
#else
epdContent = {"B", "T", "C", "L", "O", "C", "K"};
#endif
} }
setEpdContent(epdContent); content = newContent;
} }
void setEpdContent(std::array<String, NUM_SCREENS> newEpdContent) setContent(content);
{
setEpdContent(newEpdContent, false);
} }
void setEpdContent(std::array<std::string, NUM_SCREENS> newEpdContent) void EPDManager::loadFonts(const String& fontName) {
{ if (fontName == FontNames::ANTONIO) {
std::array<String, NUM_SCREENS> conv; // Load Antonio fonts
antonioFonts.big = FontLoader::loadCompressedFont(Antonio_SemiBold90pt7b_Properties);
antonioFonts.medium = FontLoader::loadCompressedFont(Antonio_SemiBold40pt7b_Properties);
antonioFonts.small = FontLoader::loadCompressedFont(Antonio_SemiBold20pt7b_Properties);
for (size_t i = 0; i < newEpdContent.size(); ++i) fontBig = antonioFonts.big;
{ fontMedium = antonioFonts.medium;
conv[i] = String(newEpdContent[i].c_str()); fontSmall = antonioFonts.small;
} else if (fontName == FontNames::OSWALD) {
// Load Oswald fonts
oswaldFonts.big = FontLoader::loadCompressedFont(Oswald_Medium80pt7b_Properties);
oswaldFonts.medium = FontLoader::loadCompressedFont(Oswald_Medium30pt7b_Properties);
oswaldFonts.small = FontLoader::loadCompressedFont(Oswald_Medium20pt7b_Properties);
fontBig = oswaldFonts.big;
fontMedium = oswaldFonts.medium;
fontSmall = oswaldFonts.small;
} }
return setEpdContent(conv); fontSatsymbol = FontLoader::loadCompressedFont(Satoshi_Symbol90pt7b_Properties);
} }
void setEpdContent(std::array<String, NUM_SCREENS> newEpdContent, void EPDManager::forceFullRefresh() {
bool forceUpdate) std::fill(lastFullRefresh.begin(), lastFullRefresh.end(), 0);
{ }
std::lock_guard<std::mutex> lock(epdUpdateMutex);
void EPDManager::setContent(const std::array<String, NUM_SCREENS>& newContent, bool forceUpdate) {
std::lock_guard<std::mutex> lock(updateMutex);
waitUntilNoneBusy(); waitUntilNoneBusy();
for (uint i = 0; i < NUM_SCREENS; i++) for (size_t i = 0; i < NUM_SCREENS; i++) {
{ if (newContent[i].compareTo(currentContent[i]) != 0 || forceUpdate) {
if (newEpdContent[i].compareTo(currentEpdContent[i]) != 0 || forceUpdate) content[i] = newContent[i];
{ UpdateDisplayTaskItem dispUpdate{static_cast<char>(i)};
epdContent[i] = newEpdContent[i];
UpdateDisplayTaskItem dispUpdate = {i};
xQueueSend(updateQueue, &dispUpdate, portMAX_DELAY); xQueueSend(updateQueue, &dispUpdate, portMAX_DELAY);
} }
} }
} }
void prepareDisplayUpdateTask(void *pvParameters) void EPDManager::setContent(const std::array<std::string, NUM_SCREENS>& newContent) {
{ std::array<String, NUM_SCREENS> conv;
UpdateDisplayTaskItem receivedItem; for (size_t i = 0; i < newContent.size(); ++i) {
conv[i] = String(newContent[i].c_str());
while (1)
{
// Wait for a work item to be available in the queue
if (xQueueReceive(updateQueue, &receivedItem, portMAX_DELAY))
{
uint epdIndex = receivedItem.dispNum;
std::lock_guard<std::mutex> lock(epdMutex[epdIndex]);
// displays[epdIndex].init(0, false); // Little longer reset duration
// because of MCP
bool updatePartial = true;
if (epdContent[epdIndex].length() > 1 && strstr(epdContent[epdIndex].c_str(), "/") != NULL)
{
String top = epdContent[epdIndex].substring(
0, epdContent[epdIndex].indexOf("/"));
String bottom = epdContent[epdIndex].substring(
epdContent[epdIndex].indexOf("/") + 1);
splitText(epdIndex, top, bottom, updatePartial);
}
else if (epdContent[epdIndex].startsWith(F("qr")))
{
renderQr(epdIndex, epdContent[epdIndex], updatePartial);
}
else if (epdContent[epdIndex].startsWith(F("mdi")))
{
renderIcon(epdIndex, epdContent[epdIndex], updatePartial);
}
else if (epdContent[epdIndex].length() > 5)
{
renderText(epdIndex, epdContent[epdIndex], updatePartial);
}
else
{
if (epdContent[epdIndex].length() > 1 && epdContent[epdIndex].indexOf(".") == -1)
{
if (epdContent[epdIndex].equals("STS"))
{
showDigit(epdIndex, 'S', updatePartial,
&FONT_SATSYMBOL);
}
else
{
showChars(epdIndex, epdContent[epdIndex], updatePartial,
&FONT_MEDIUM);
}
}
else
{
showDigit(epdIndex, epdContent[epdIndex].c_str()[0], updatePartial,
&FONT_BIG);
} }
setContent(conv);
} }
xTaskNotifyGive(tasks[epdIndex]); std::array<String, NUM_SCREENS> EPDManager::getCurrentContent() const {
} return currentContent;
}
} }
extern "C" void updateDisplay(void *pvParameters) noexcept void EPDManager::waitUntilNoneBusy() {
{ for (size_t i = 0; i < NUM_SCREENS; i++) {
const int epdIndex = *(int *)pvParameters; uint32_t count = 0;
delete (int *)pvParameters; while (EPD_BUSY[i].digitalRead()) {
for (;;)
{
// Wait for the task notification
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
std::lock_guard<std::mutex> lock(epdMutex[epdIndex]);
{
std::lock_guard<std::mutex> lockMcp(mcpMutex);
displays[epdIndex].init(0, false, 40);
}
uint count = 0;
while (EPD_BUSY[epdIndex].digitalRead() == HIGH || count < 10)
{
vTaskDelay(pdMS_TO_TICKS(100));
count++; count++;
} vTaskDelay(BUSY_RETRY_DELAY);
bool updatePartial = true;
// Full Refresh every x minutes
if (!lastFullRefresh[epdIndex] ||
(millis() - lastFullRefresh[epdIndex]) >
(preferences.getUInt("fullRefreshMin",
DEFAULT_MINUTES_FULL_REFRESH) *
60 * 1000))
{
updatePartial = false;
}
char tries = 0;
while (tries < 3)
{
if (displays[epdIndex].displayWithReturn(updatePartial))
{
displays[epdIndex].powerOff();
currentEpdContent[epdIndex] = epdContent[epdIndex];
if (!updatePartial)
lastFullRefresh[epdIndex] = millis();
if (eventSourceTaskHandle != NULL)
xTaskNotifyGive(eventSourceTaskHandle);
if (count == BUSY_TIMEOUT_COUNT) {
vTaskDelay(pdMS_TO_TICKS(100));
} else if (count > BUSY_TIMEOUT_COUNT + 5) {
log_e("Display %d busy timeout", i);
break; break;
} }
vTaskDelay(pdMS_TO_TICKS(100));
tries++;
} }
} }
} }
void splitText(const uint dispNum, const String &top, const String &bottom, void EPDManager::setupDisplay(uint dispNum, const GFXfont* font) {
bool partial)
{
displays[dispNum].setRotation(2); displays[dispNum].setRotation(2);
displays[dispNum].setFont(&FONT_SMALL); displays[dispNum].setFont(font);
displays[dispNum].setTextColor(getFgColor()); displays[dispNum].setTextColor(fgColor);
displays[dispNum].fillScreen(bgColor);
}
void EPDManager::splitText(uint dispNum, const String& top, const String& bottom, bool partial) {
if (preferences.getBool("verticalDesc", DEFAULT_VERTICAL_DESC) && dispNum == 0) {
displays[dispNum].setRotation(1);
} else {
displays[dispNum].setRotation(2);
}
displays[dispNum].setFont(fontSmall);
displays[dispNum].setTextColor(fgColor);
// Top text // Top text
int16_t ttbx, ttby; int16_t ttbx, ttby;
uint16_t ttbw, ttbh; uint16_t ttbw, ttbh;
displays[dispNum].getTextBounds(top, 0, 0, &ttbx, &ttby, &ttbw, &ttbh); displays[dispNum].getTextBounds(top, 0, 0, &ttbx, &ttby, &ttbw, &ttbh);
uint16_t tx = ((displays[dispNum].width() - ttbw) / 2) - ttbx; uint16_t tx = ((displays[dispNum].width() - ttbw) / 2) - ttbx;
uint16_t ty = uint16_t ty = ((displays[dispNum].height() - ttbh) / 2) - ttby - ttbh / 2 - 12;
((displays[dispNum].height() - ttbh) / 2) - ttby - ttbh / 2 - 12;
// Bottom text // Bottom text
int16_t tbbx, tbby; int16_t tbbx, tbby;
uint16_t tbbw, tbbh; uint16_t tbbw, tbbh;
displays[dispNum].getTextBounds(bottom, 0, 0, &tbbx, &tbby, &tbbw, &tbbh); displays[dispNum].getTextBounds(bottom, 0, 0, &tbbx, &tbby, &tbbw, &tbbh);
uint16_t bx = ((displays[dispNum].width() - tbbw) / 2) - tbbx; uint16_t bx = ((displays[dispNum].width() - tbbw) / 2) - tbbx;
uint16_t by = uint16_t by = ((displays[dispNum].height() - tbbh) / 2) - tbby + tbbh / 2 + 12;
((displays[dispNum].height() - tbbh) / 2) - tbby + tbbh / 2 + 12;
// Make separator as wide as the shortest text. // Make separator as wide as the shortest text
uint16_t lineWidth, lineX; uint16_t lineWidth = (tbbw < ttbh) ? tbbw : ttbw;
if (tbbw < ttbh) uint16_t lineX = round((displays[dispNum].width() - lineWidth) / 2);
lineWidth = tbbw;
else
lineWidth = ttbw;
lineX = round((displays[dispNum].width() - lineWidth) / 2);
displays[dispNum].fillScreen(getBgColor()); displays[dispNum].fillScreen(bgColor);
displays[dispNum].setCursor(tx, ty); displays[dispNum].setCursor(tx, ty);
displays[dispNum].print(top); displays[dispNum].print(top);
displays[dispNum].fillRoundRect(lineX, displays[dispNum].height() / 2 - 3, displays[dispNum].fillRoundRect(lineX, displays[dispNum].height() / 2 - 3,
lineWidth, 6, 3, getFgColor()); lineWidth, 6, 3, fgColor);
displays[dispNum].setCursor(bx, by); displays[dispNum].setCursor(bx, by);
displays[dispNum].print(bottom); displays[dispNum].print(bottom);
} }
void showDigit(const uint dispNum, char chr, bool partial, void EPDManager::showDigit(uint dispNum, char chr, bool partial, const GFXfont* font) {
const GFXfont *font)
{
String str(chr); String str(chr);
if (chr == '.') {
if (chr == '.')
{
str = "!"; str = "!";
} }
displays[dispNum].setRotation(2);
displays[dispNum].setFont(font); setupDisplay(dispNum, font);
displays[dispNum].setTextColor(getFgColor());
int16_t tbx, tby; int16_t tbx, tby;
uint16_t tbw, tbh; uint16_t tbw, tbh;
displays[dispNum].getTextBounds(str, 0, 0, &tbx, &tby, &tbw, &tbh); displays[dispNum].getTextBounds(str, 0, 0, &tbx, &tby, &tbw, &tbh);
// center the bounding box by transposition of the origin:
uint16_t x = ((displays[dispNum].width() - tbw) / 2) - tbx; uint16_t x = ((displays[dispNum].width() - tbw) / 2) - tbx;
uint16_t y = ((displays[dispNum].height() - tbh) / 2) - tby; uint16_t y = ((displays[dispNum].height() - tbh) / 2) - tby;
// if (str.equals("."))
// {
// // int16_t yAdvance = font->yAdvance;
// // uint8_t charIndex = 46 - font->first;
// // GFXglyph *glyph = (&font->glyph)[charIndex];
// int16_t tbx2, tby2;
// uint16_t tbw2, tbh2;
// displays[dispNum].getTextBounds(".!", 0, 0, &tbx2, &tby2, &tbw2, &tbh2);
// y = ((displays[dispNum].height() - tbh2) / 2) - tby2;
// // Serial.print("yAdvance");
// // Serial.println(yAdvance);
// // if (glyph != nullptr) {
// // Serial.print("height");
// // Serial.println(glyph->height);
// // Serial.print("yOffset");
// // Serial.println(glyph->yOffset);
// // }
// // y = 250-99+18+19;
// }
displays[dispNum].fillScreen(getBgColor());
displays[dispNum].setCursor(x, y); displays[dispNum].setCursor(x, y);
displays[dispNum].print(str); displays[dispNum].print(str);
if (chr == '.') if (chr == '.') {
{ displays[dispNum].fillRect(0, 0, displays[dispNum].width(),
displays[dispNum].fillRect(x, y, displays[dispNum].width(), round(displays[dispNum].height() * 0.9), getBgColor()); round(displays[dispNum].height() * 0.67), bgColor);
}
} }
// displays[dispNum].setCursor(10, 3); void EPDManager::showChars(uint dispNum, const String& chars, bool partial, const GFXfont* font) {
// displays[dispNum].setFont(&FONT_SMALL); setupDisplay(dispNum, font);
// displays[dispNum].setTextColor(getFgColor());
// displays[dispNum].println("Y = " + y);
}
void showChars(const uint dispNum, const String &chars, bool partial,
const GFXfont *font)
{
displays[dispNum].setRotation(2);
displays[dispNum].setFont(font);
displays[dispNum].setTextColor(getFgColor());
int16_t tbx, tby; int16_t tbx, tby;
uint16_t tbw, tbh; uint16_t tbw, tbh;
displays[dispNum].getTextBounds(chars, 0, 0, &tbx, &tby, &tbw, &tbh); displays[dispNum].getTextBounds(chars, 0, 0, &tbx, &tby, &tbw, &tbh);
// center the bounding box by transposition of the origin:
// Center the bounding box by transposition of the origin
uint16_t x = ((displays[dispNum].width() - tbw) / 2) - tbx; uint16_t x = ((displays[dispNum].width() - tbw) / 2) - tbx;
uint16_t y = ((displays[dispNum].height() - tbh) / 2) - tby; uint16_t y = ((displays[dispNum].height() - tbh) / 2) - tby;
displays[dispNum].fillScreen(getBgColor());
for (size_t i = 0; i < chars.length(); i++) {
char c = chars[i];
if (c == '.' || c == ',') {
// For the dot, calculate its specific descent
GFXglyph* dotGlyph = &font->glyph[c - font->first];
int16_t dotDescent = dotGlyph->yOffset;
// Draw the dot with adjusted y-position
displays[dispNum].setCursor(x, y + dotDescent + dotGlyph->height + 8);
displays[dispNum].print(c);
} else {
// For other characters, use the original y-position
displays[dispNum].setCursor(x, y); displays[dispNum].setCursor(x, y);
displays[dispNum].print(chars); displays[dispNum].print(c);
} }
int getBgColor() { return bgColor; } // Move x-position for the next character
x += font->glyph[c - font->first].xAdvance;
int getFgColor() { return fgColor; }
void setBgColor(int color) { bgColor = color; }
void setFgColor(int color) { fgColor = color; }
std::array<String, NUM_SCREENS> getCurrentEpdContent()
{
return currentEpdContent;
} }
void renderText(const uint dispNum, const String &text, bool partial) }
{
bool EPDManager::renderIcon(uint dispNum, const String& text, bool partial) {
displays[dispNum].setRotation(2);
displays[dispNum].setPartialWindow(0, 0, displays[dispNum].width(),
displays[dispNum].height());
displays[dispNum].fillScreen(bgColor);
displays[dispNum].setTextColor(fgColor);
uint iconIndex = 0;
uint width = 122;
uint height = 122;
if (text.endsWith("rocket")) {
iconIndex = 1;
} else if (text.endsWith("lnbolt")) {
iconIndex = 2;
} else if (text.endsWith("bitaxe")) {
width = 88;
height = 220;
iconIndex = 3;
} else if (text.endsWith("miningpool")) {
LogoData logo = MiningPoolStatsFetch::getInstance().getLogo();
if (logo.size == 0) {
Serial.println(F("No logo found"));
return false;
}
int x_offset = (displays[dispNum].width() - logo.width) / 2;
int y_offset = (displays[dispNum].height() - logo.height) / 2;
displays[dispNum].drawInvertedBitmap(x_offset, y_offset, logo.data,
logo.width, logo.height, fgColor);
return true;
}
int x_offset = (displays[dispNum].width() - width) / 2;
int y_offset = (displays[dispNum].height() - height) / 2;
displays[dispNum].drawInvertedBitmap(x_offset, y_offset, epd_icons_allArray[iconIndex],
width, height, fgColor);
return true;
}
void EPDManager::renderText(uint dispNum, const String& text, bool partial) {
displays[dispNum].setRotation(2); displays[dispNum].setRotation(2);
displays[dispNum].setPartialWindow(0, 0, displays[dispNum].width(), displays[dispNum].setPartialWindow(0, 0, displays[dispNum].width(),
displays[dispNum].height()); displays[dispNum].height());
@ -506,73 +376,45 @@ void renderText(const uint dispNum, const String &text, bool partial)
std::stringstream ss; std::stringstream ss;
ss.str(text.c_str()); ss.str(text.c_str());
std::string line; std::string line;
while (std::getline(ss, line, '\n')) while (std::getline(ss, line, '\n')) {
{ if (line.rfind("*", 0) == 0) {
if (line.rfind("*", 0) == 0)
{
line.erase(std::remove(line.begin(), line.end(), '*'), line.end()); line.erase(std::remove(line.begin(), line.end(), '*'), line.end());
displays[dispNum].setFont(&FreeSansBold9pt7b); displays[dispNum].setFont(&FreeSansBold9pt7b);
displays[dispNum].println(line.c_str()); } else {
}
else
{
displays[dispNum].setFont(&FreeSans9pt7b); displays[dispNum].setFont(&FreeSans9pt7b);
}
displays[dispNum].println(line.c_str()); displays[dispNum].println(line.c_str());
} }
} }
}
void renderIcon(const uint dispNum, const String &text, bool partial) void EPDManager::renderQr(uint dispNum, const String& text, bool partial) {
{
displays[dispNum].setRotation(2);
displays[dispNum].setPartialWindow(0, 0, displays[dispNum].width(),
displays[dispNum].height());
displays[dispNum].fillScreen(getBgColor());
displays[dispNum].setTextColor(getFgColor());
uint iconIndex = 0;
if (text.endsWith("rocket")) {
iconIndex = 1;
}
if (text.endsWith("lnbolt")) {
iconIndex = 3;
}
displays[dispNum].drawInvertedBitmap(0,0, epd_icons_allArray[iconIndex], 122, 250, getFgColor());
}
void renderQr(const uint dispNum, const String &text, bool partial)
{
#ifdef USE_QR #ifdef USE_QR
// Dynamically allocate QR buffer
uint8_t* qrcode = (uint8_t*)malloc(qrcodegen_BUFFER_LEN_MAX);
if (!qrcode) {
log_e("Failed to allocate QR buffer");
return;
}
uint8_t tempBuffer[800]; uint8_t tempBuffer[800];
bool ok = qrcodegen_encodeText( bool ok = qrcodegen_encodeText(
text.substring(2).c_str(), tempBuffer, qrcode, qrcodegen_Ecc_LOW, text.substring(2).c_str(), tempBuffer, qrcode, qrcodegen_Ecc_LOW,
qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true); qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true);
if (ok) {
const int size = qrcodegen_getSize(qrcode); const int size = qrcodegen_getSize(qrcode);
const int padding = floor(float(displays[dispNum].width() - (size * 4)) / 2); const int padding = floor(float(displays[dispNum].width() - (size * 4)) / 2);
const int paddingY = const int paddingY = floor(float(displays[dispNum].height() - (size * 4)) / 2);
floor(float(displays[dispNum].height() - (size * 4)) / 2);
displays[dispNum].setRotation(2);
displays[dispNum].setRotation(2);
displays[dispNum].setPartialWindow(0, 0, displays[dispNum].width(), displays[dispNum].setPartialWindow(0, 0, displays[dispNum].width(),
displays[dispNum].height()); displays[dispNum].height());
displays[dispNum].fillScreen(GxEPD_WHITE); displays[dispNum].fillScreen(GxEPD_WHITE);
const int border = 0;
for (int y = -border; y < size * 4 + border; y++) for (int y = 0; y < size * 4; y++) {
{ for (int x = 0; x < size * 4; x++) {
for (int x = -border; x < size * 4 + border; x++)
{
displays[dispNum].drawPixel( displays[dispNum].drawPixel(
padding + x, paddingY + y, padding + x, paddingY + y,
qrcodegen_getModule(qrcode, floor(float(x) / 4), floor(float(y) / 4)) qrcodegen_getModule(qrcode, floor(float(x) / 4), floor(float(y) / 4))
@ -580,28 +422,116 @@ void renderQr(const uint dispNum, const String &text, bool partial)
: GxEPD_WHITE); : GxEPD_WHITE);
} }
} }
}
free(qrcode);
#endif #endif
} }
void waitUntilNoneBusy() int16_t EPDManager::calculateDescent(const GFXfont* font) {
{ int16_t maxDescent = 0;
for (int i = 0; i < NUM_SCREENS; i++) for (uint16_t i = font->first; i <= font->last; i++) {
{ GFXglyph* glyph = &font->glyph[i - font->first];
uint count = 0; int16_t descent = glyph->yOffset;
while (EPD_BUSY[i].digitalRead()) if (descent > maxDescent) {
{ maxDescent = descent;
count++;
vTaskDelay(10);
if (count == 200)
{
// displays[i].init(0, false);
vTaskDelay(100);
} }
else if (count > 205) }
return maxDescent;
}
void EPDManager::updateDisplayTask(void* pvParameters) noexcept {
auto& instance = EPDManager::getInstance();
const int epdIndex = *(int*)pvParameters;
delete (int*)pvParameters;
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
std::lock_guard<std::mutex> lock(instance.displayMutexes[epdIndex]);
{ {
Serial.printf("Busy timeout %d", i); std::lock_guard<std::mutex> lockMcp(mcpMutex);
instance.displays[epdIndex].init(0, false, 40);
}
uint32_t count = 0;
while (instance.EPD_BUSY[epdIndex].digitalRead() == HIGH || count < 10) {
vTaskDelay(pdMS_TO_TICKS(100));
count++;
}
bool updatePartial = true;
if (!instance.lastFullRefresh[epdIndex] ||
(millis() - instance.lastFullRefresh[epdIndex]) >
(preferences.getUInt("fullRefreshMin", DEFAULT_MINUTES_FULL_REFRESH) * 60 * 1000)) {
updatePartial = false;
}
char tries = 0;
while (tries < 3) {
if (instance.displays[epdIndex].displayWithReturn(updatePartial)) {
instance.displays[epdIndex].powerOff();
instance.currentContent[epdIndex] = instance.content[epdIndex];
if (!updatePartial) {
instance.lastFullRefresh[epdIndex] = millis();
}
if (eventSourceTaskHandle != nullptr) {
xTaskNotifyGive(eventSourceTaskHandle);
}
break; break;
} }
vTaskDelay(pdMS_TO_TICKS(100));
tries++;
}
}
}
void EPDManager::prepareDisplayUpdateTask(void* pvParameters) {
auto& instance = EPDManager::getInstance();
UpdateDisplayTaskItem receivedItem;
for (;;) {
if (xQueueReceive(instance.updateQueue, &receivedItem, portMAX_DELAY)) {
uint epdIndex = receivedItem.dispNum;
std::lock_guard<std::mutex> lock(instance.displayMutexes[epdIndex]);
bool updatePartial = true;
if (instance.content[epdIndex].length() > 1 &&
strstr(instance.content[epdIndex].c_str(), "/") != nullptr) {
String top = instance.content[epdIndex].substring(
0, instance.content[epdIndex].indexOf("/"));
String bottom = instance.content[epdIndex].substring(
instance.content[epdIndex].indexOf("/") + 1);
instance.splitText(epdIndex, top, bottom, updatePartial);
} else if (instance.content[epdIndex].startsWith(F("qr"))) {
instance.renderQr(epdIndex, instance.content[epdIndex], updatePartial);
} else if (instance.content[epdIndex].startsWith(F("mdi"))) {
if (!instance.renderIcon(epdIndex, instance.content[epdIndex], updatePartial)) {
continue;
}
} else if (instance.content[epdIndex].length() > 5) {
instance.renderText(epdIndex, instance.content[epdIndex], updatePartial);
} else {
if (instance.content[epdIndex].length() == 2) {
instance.showChars(epdIndex, instance.content[epdIndex], updatePartial, instance.fontBig);
} else if (instance.content[epdIndex].length() > 1 &&
instance.content[epdIndex].indexOf(".") == -1) {
if (instance.content[epdIndex].equals("STS")) {
instance.showDigit(epdIndex, 'S', updatePartial, instance.fontSatsymbol);
} else {
instance.showChars(epdIndex, instance.content[epdIndex], updatePartial,
instance.fontMedium);
}
} else {
instance.showDigit(epdIndex, instance.content[epdIndex].c_str()[0],
updatePartial, instance.fontBig);
}
}
xTaskNotifyGive(instance.tasks[epdIndex]);
} }
} }
} }

View file

@ -3,55 +3,133 @@
#include <Fonts/FreeSans9pt7b.h> #include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSansBold9pt7b.h> #include <Fonts/FreeSansBold9pt7b.h>
#include <GxEPD2_BW.h> #include <GxEPD2_BW.h>
#include "gzip_decompressor.hpp"
#include <mcp23x17_pin.hpp> #include <mcp23x17_pin.hpp>
#include <mutex> #include <mutex>
#include <native_pin.hpp> #include <native_pin.hpp>
#include <regex> #include <regex>
#include <array>
#include <memory>
#include "fonts/fonts.hpp" #include "fonts/fonts.hpp"
#include "lib/config.hpp" #include "lib/config.hpp"
#include "lib/shared.hpp" #include "lib/shared.hpp"
#include "icons/icons.h" #include "icons/icons.h"
#include "mining_pool_stats_fetch.hpp"
// Font includes
#include "../fonts/antonio-semibold20.h"
#include "../fonts/antonio-semibold40.h"
#include "../fonts/antonio-semibold90.h"
// Oswald fonts
#include "../fonts/oswald-medium20.h"
#include "../fonts/oswald-medium30.h"
#include "../fonts/oswald-medium80.h"
#include "../fonts/sats-symbol.h"
#ifdef USE_QR #ifdef USE_QR
#include "qrcodegen.h" #include "qrcodegen.h"
#endif #endif
// extern TaskHandle_t epdTaskHandle;
typedef struct { struct UpdateDisplayTaskItem {
char dispNum; char dispNum;
} UpdateDisplayTaskItem; };
struct FontFamily {
GFXfont* big;
GFXfont* medium;
GFXfont* small;
};
class EPDManager {
public:
static EPDManager& getInstance();
// Delete copy constructor and assignment operator
EPDManager(const EPDManager&) = delete;
EPDManager& operator=(const EPDManager&) = delete;
void initialize();
void forceFullRefresh(); void forceFullRefresh();
void refreshFromMemory(); void loadFonts(const String& fontName);
void setupDisplays(); void setContent(const std::array<String, NUM_SCREENS>& newContent, bool forceUpdate = false);
void setContent(const std::array<std::string, NUM_SCREENS>& newContent);
std::array<String, NUM_SCREENS> getCurrentContent() const;
void splitText(const uint dispNum, const String &top, const String &bottom, int getBackgroundColor() const { return bgColor; }
bool partial); int getForegroundColor() const { return fgColor; }
void setBackgroundColor(int color) { bgColor = color; }
void showDigit(const uint dispNum, char chr, bool partial, const GFXfont *font); void setForegroundColor(int color) { fgColor = color; }
void showChars(const uint dispNum, const String &chars, bool partial,
const GFXfont *font);
extern "C" void updateDisplay(void *pvParameters) noexcept;
void updateDisplayAlt(int epdIndex);
void prepareDisplayUpdateTask(void *pvParameters);
int getBgColor();
int getFgColor();
void setBgColor(int color);
void setFgColor(int color);
void renderIcon(const uint dispNum, const String &text, bool partial);
void renderText(const uint dispNum, const String &text, bool partial);
void renderQr(const uint dispNum, const String &text, bool partial);
void setEpdContent(std::array<String, NUM_SCREENS> newEpdContent,
bool forceUpdate);
void setEpdContent(std::array<String, NUM_SCREENS> newEpdContent);
void setEpdContent(std::array<std::string, NUM_SCREENS> newEpdContent);
std::array<String, NUM_SCREENS> getCurrentEpdContent();
void waitUntilNoneBusy(); void waitUntilNoneBusy();
private:
EPDManager(); // Private constructor for singleton
~EPDManager(); // Private destructor
void setupDisplay(uint dispNum, const GFXfont* font);
void splitText(uint dispNum, const String& top, const String& bottom, bool partial);
void showDigit(uint dispNum, char chr, bool partial, const GFXfont* font);
void showChars(uint dispNum, const String& chars, bool partial, const GFXfont* font);
bool renderIcon(uint dispNum, const String& text, bool partial);
void renderText(uint dispNum, const String& text, bool partial);
void renderQr(uint dispNum, const String& text, bool partial);
int16_t calculateDescent(const GFXfont* font);
static void updateDisplayTask(void* pvParameters) noexcept;
static void prepareDisplayUpdateTask(void* pvParameters);
// Member variables
std::array<String, NUM_SCREENS> currentContent;
std::array<String, NUM_SCREENS> content;
std::array<uint32_t, NUM_SCREENS> lastFullRefresh;
std::array<TaskHandle_t, NUM_SCREENS> tasks;
QueueHandle_t updateQueue;
FontFamily antonioFonts;
FontFamily oswaldFonts;
const GFXfont* fontSmall;
const GFXfont* fontBig;
const GFXfont* fontMedium;
const GFXfont* fontSatsymbol;
int bgColor;
int fgColor;
std::mutex updateMutex;
std::array<std::mutex, NUM_SCREENS> displayMutexes;
// Pin configurations based on board version
#ifdef IS_BTCLOCK_REV_B
static Native_Pin EPD_DC;
static std::array<Native_Pin, NUM_SCREENS> EPD_CS;
static std::array<Native_Pin, NUM_SCREENS> EPD_BUSY;
static std::array<MCP23X17_Pin, NUM_SCREENS> EPD_RESET;
#elif defined(IS_BTCLOCK_V8)
static Native_Pin EPD_DC;
static std::array<MCP23X17_Pin, NUM_SCREENS> EPD_BUSY;
static std::array<MCP23X17_Pin, NUM_SCREENS> EPD_CS;
static std::array<MCP23X17_Pin, NUM_SCREENS> EPD_RESET;
#else
static Native_Pin EPD_DC;
static std::array<Native_Pin, NUM_SCREENS> EPD_CS;
static std::array<Native_Pin, NUM_SCREENS> EPD_BUSY;
static std::array<MCP23X17_Pin, NUM_SCREENS> EPD_RESET;
#endif
// Display array
std::array<GxEPD2_BW<EPD_CLASS, EPD_CLASS::HEIGHT>, NUM_SCREENS> displays;
static constexpr size_t UPDATE_QUEUE_SIZE = 14;
static constexpr uint32_t BUSY_TIMEOUT_COUNT = 200;
static constexpr TickType_t BUSY_RETRY_DELAY = pdMS_TO_TICKS(10);
static constexpr size_t EPD_TASK_STACK_SIZE =
#ifdef IS_BTCLOCK_V8
4096
#else
2048
#endif
;
};

View file

@ -0,0 +1,49 @@
#pragma once
#include "rom/miniz.h"
#include <Arduino.h>
class GzipDecompressor {
public:
static bool decompressData(const uint8_t* input, size_t inputSize,
uint8_t* output, size_t* outputSize) {
if (!input || !output || !outputSize || inputSize < 18) { // Minimum gzip size
return false;
}
tinfl_decompressor* decomp = new tinfl_decompressor;
if (!decomp) {
return false;
}
tinfl_init(decomp);
size_t inPos = 10; // Skip gzip header
size_t outPos = 0;
while (inPos < inputSize - 8) { // -8 for footer
size_t inBytes = inputSize - inPos - 8;
size_t outBytes = *outputSize - outPos;
tinfl_status status = tinfl_decompress(decomp,
&input[inPos], &inBytes,
output, &output[outPos], &outBytes,
TINFL_FLAG_HAS_MORE_INPUT | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF);
inPos += inBytes;
outPos += outBytes;
if (status == TINFL_STATUS_DONE) {
*outputSize = outPos;
delete decomp;
return true;
} else if (status < 0) {
delete decomp;
return false;
}
}
delete decomp;
return false;
}
};

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,7 @@
#include <Arduino.h> #include <Arduino.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/task.h> #include <freertos/task.h>
#include <memory>
#include "lib/shared.hpp" #include "lib/shared.hpp"
#include "lib/webserver.hpp" #include "lib/webserver.hpp"
@ -15,53 +16,80 @@
#define NEOPIXEL_COUNT 4 #define NEOPIXEL_COUNT 4
#endif #endif
// LED effect constants
const int LED_FLASH_ERROR = 0; const int LED_FLASH_ERROR = 0;
const int LED_FLASH_SUCCESS = 1; const int LED_FLASH_SUCCESS = 1;
const int LED_FLASH_UPDATE = 2; const int LED_FLASH_UPDATE = 2;
const int LED_FLASH_BLOCK_NOTIFY = 3; const int LED_EFFECT_CONFIGURING = 10;
const int LED_EFFECT_START_TIMER = 4; const int LED_FLASH_BLOCK_NOTIFY = 4;
const int LED_EFFECT_PAUSE_TIMER = 5; const int LED_EFFECT_START_TIMER = 5;
const int LED_EFFECT_HEARTBEAT = 6; const int LED_EFFECT_PAUSE_TIMER = 6;
const int LED_EFFECT_HEARTBEAT = 7;
const int LED_EFFECT_WIFI_WAIT_FOR_CONFIG = 100; const int LED_EFFECT_WIFI_WAIT_FOR_CONFIG = 100;
const int LED_EFFECT_WIFI_CONNECTING = 101; const int LED_EFFECT_WIFI_CONNECTING = 101;
const int LED_EFFECT_WIFI_CONNECT_ERROR = 102; const int LED_EFFECT_WIFI_CONNECT_ERROR = 102;
const int LED_EFFECT_WIFI_CONNECT_SUCCESS = 103; const int LED_EFFECT_WIFI_CONNECT_SUCCESS = 103;
const int LED_EFFECT_WIFI_ERASE_SETTINGS = 104; const int LED_EFFECT_WIFI_ERASE_SETTINGS = 104;
const int LED_PROGRESS_25 = 200; const int LED_PROGRESS_25 = 200;
const int LED_PROGRESS_50 = 201; const int LED_PROGRESS_50 = 201;
const int LED_PROGRESS_75 = 202; const int LED_PROGRESS_75 = 202;
const int LED_PROGRESS_100 = 203; const int LED_PROGRESS_100 = 203;
const int LED_DATA_PRICE_ERROR = 300; const int LED_DATA_PRICE_ERROR = 300;
const int LED_DATA_BLOCK_ERROR = 301; const int LED_DATA_BLOCK_ERROR = 301;
const int LED_EFFECT_NOSTR_ZAP = 400; const int LED_EFFECT_NOSTR_ZAP = 400;
const int LED_FLASH_IDENTIFY = 990; const int LED_FLASH_IDENTIFY = 990;
const int LED_POWER_TEST = 999; const int LED_POWER_TEST = 999;
extern TaskHandle_t ledTaskHandle;
extern Adafruit_NeoPixel pixels;
void ledTask(void *pvParameters); // Do Not Disturb mode settings
void setupLeds(); struct DNDTimeRange {
void setupLedTask(); uint8_t startHour;
void blinkDelay(int d, int times); uint8_t startMinute;
void blinkDelayColor(int d, int times, uint r, uint g, uint b); uint8_t endHour;
void blinkDelayTwoColor(int d, int times, uint32_t c1, uint32_t c2); uint8_t endMinute;
void clearLeds(); };
void saveLedState();
void restoreLedState(); class LedHandler {
QueueHandle_t getLedTaskQueue(); public:
bool queueLedEffect(uint effect); static LedHandler& getInstance();
// Delete copy constructor and assignment operator
LedHandler(const LedHandler&) = delete;
LedHandler& operator=(const LedHandler&) = delete;
void setup();
void setupTask();
bool queueEffect(uint effect);
void clear();
void setLights(int r, int g, int b); void setLights(int r, int g, int b);
void setLights(uint32_t color); void setLights(uint32_t color);
void ledRainbow(int wait); void saveLedState();
void ledTheaterChaseRainbow(int wait); void restoreLedState();
void ledTheaterChase(uint32_t color, int wait); QueueHandle_t getTaskQueue() const { return ledTaskQueue; }
Adafruit_NeoPixel getPixels(); Adafruit_NeoPixel& getPixels() { return pixels; }
// DND methods
void setDNDEnabled(bool enabled);
void setDNDTimeBasedEnabled(bool enabled);
void setDNDTimeRange(uint8_t startHour, uint8_t startMinute, uint8_t endHour, uint8_t endMinute);
bool isDNDActive() const;
bool isTimeInDNDRange(uint8_t hour, uint8_t minute) const;
// DND getters
bool isDNDEnabled() const { return dndEnabled; }
bool isDNDTimeBasedEnabled() const { return dndTimeBasedEnabled; }
uint8_t getDNDStartHour() const { return dndTimeRange.startHour; }
uint8_t getDNDStartMinute() const { return dndTimeRange.startMinute; }
uint8_t getDNDEndHour() const { return dndTimeRange.endHour; }
uint8_t getDNDEndMinute() const { return dndTimeRange.endMinute; }
// Effect methods
void rainbow(int wait);
void theaterChase(uint32_t color, int wait);
void theaterChaseRainbow(int wait);
void lightningStrike(); void lightningStrike();
void blinkDelay(int d, int times);
void blinkDelayColor(int d, int times, uint r, uint g, uint b);
void blinkDelayTwoColor(int d, int times, const uint32_t& c1, const uint32_t& c2);
#ifdef HAS_FRONTLIGHT #ifdef HAS_FRONTLIGHT
void frontlightFlash(int flDelayTime); void frontlightFlash(int flDelayTime);
@ -69,17 +97,39 @@ void frontlightFadeInAll();
void frontlightFadeOutAll(); void frontlightFadeOutAll();
void frontlightFadeIn(uint num); void frontlightFadeIn(uint num);
void frontlightFadeOut(uint num); void frontlightFadeOut(uint num);
std::vector<uint16_t> frontlightGetStatus(); std::vector<uint16_t> frontlightGetStatus();
void frontlightSetBrightness(uint brightness); void frontlightSetBrightness(uint brightness);
bool frontlightIsOn(); bool frontlightIsOn() const { return frontlightOn; }
void frontlightFadeInAll(int flDelayTime, bool staggered = false);
void frontlightFadeInAll(int flDelayTime); void frontlightFadeOutAll(int flDelayTime, bool staggered = false);
void frontlightFadeInAll(int flDelayTime, bool staggered);
void frontlightFadeOutAll(int flDelayTime);
void frontlightFadeOutAll(int flDelayTime, bool staggered);
void frontlightFadeIn(uint num, int flDelayTime); void frontlightFadeIn(uint num, int flDelayTime);
void frontlightFadeOut(uint num, int flDelayTime); void frontlightFadeOut(uint num, int flDelayTime);
void initializeFrontlight();
#endif #endif
private:
LedHandler(); // Private constructor for singleton
void loadDNDSettings();
static void ledTask(void* pvParameters);
Adafruit_NeoPixel pixels;
TaskHandle_t ledTaskHandle;
QueueHandle_t ledTaskQueue;
uint ledTaskParams;
// DND members
bool dndEnabled;
bool dndTimeBasedEnabled;
DNDTimeRange dndTimeRange;
#ifdef HAS_FRONTLIGHT
static constexpr uint16_t FL_FADE_STEP = 25;
bool frontlightOn;
bool flInTransition;
#endif
};
// Global accessor function
inline LedHandler& getLedHandler() {
return LedHandler::getInstance();
}

View file

@ -0,0 +1,43 @@
#include "brains_pool.hpp"
void BraiinsPool::prepareRequest(HTTPClient &http) const
{
http.addHeader("Pool-Auth-Token", poolUser.c_str());
}
std::string BraiinsPool::getApiUrl() const
{
return "https://pool.braiins.com/accounts/profile/json/btc/";
}
PoolStats BraiinsPool::parseResponse(const JsonDocument &doc) const
{
try
{
if (doc["btc"].isNull())
{
return PoolStats{
.hashrate = "0",
.dailyEarnings = 0};
}
std::string unit = doc["btc"]["hash_rate_unit"].as<std::string>();
static const std::unordered_map<std::string, int> multipliers = {
{"Zh/s", 21}, {"Eh/s", 18}, {"Ph/s", 15}, {"Th/s", 12}, {"Gh/s", 9}, {"Mh/s", 6}, {"Kh/s", 3}};
int multiplier = multipliers.at(unit);
float hashValue = doc["btc"]["hash_rate_5m"].as<float>();
return PoolStats{
.hashrate = std::to_string(static_cast<int>(std::round(hashValue))) + std::string(multiplier, '0'),
.dailyEarnings = static_cast<int64_t>(doc["btc"]["today_reward"].as<float>() * 100000000)};
}
catch (const std::exception &e)
{
Serial.printf("Error parsing %s response: %s\n", getPoolName().c_str(), e.what());
return PoolStats{
.hashrate = "0",
.dailyEarnings = std::nullopt};
}
}

View file

@ -0,0 +1,33 @@
#pragma once
#include "lib/mining_pool/mining_pool_interface.hpp"
#include <icons/icons.h>
#include <utils.hpp>
class BraiinsPool : public MiningPoolInterface
{
public:
void setPoolUser(const std::string &user) override { poolUser = user; }
void prepareRequest(HTTPClient &http) const override;
std::string getApiUrl() const override;
PoolStats parseResponse(const JsonDocument &doc) const override;
bool supportsDailyEarnings() const override { return true; }
bool hasLogo() const override { return true; }
std::string getDisplayLabel() const override { return "BRAIINS/POOL"; } // Fallback if needed
std::string getDailyEarningsLabel() const override { return "sats/earned"; }
std::string getLogoFilename() const override {
return "braiins.bin";
}
std::string getPoolName() const override {
return "braiins";
}
int getLogoWidth() const override {
return 37;
}
int getLogoHeight() const override {
return 230;
}
};

View file

@ -0,0 +1,47 @@
#include "ckpool.hpp"
void CKPool::prepareRequest(HTTPClient &http) const
{
// Empty as CKPool doesn't need special headers
}
std::string CKPool::getApiUrl() const
{
return getBaseUrl() + "/users/" + poolUser;
}
PoolStats CKPool::parseResponse(const JsonDocument &doc) const
{
try
{
std::string hashrateStr = doc["hashrate1m"].as<std::string>();
// Special case for "0"
if (hashrateStr == "0") {
return PoolStats{
.hashrate = "0",
.dailyEarnings = std::nullopt
};
}
char unit = hashrateStr.back();
std::string value = hashrateStr.substr(0, hashrateStr.size() - 1);
int multiplier = getHashrateMultiplier(unit);
double hashrate = std::stod(value) * std::pow(10, multiplier);
char buffer[32];
snprintf(buffer, sizeof(buffer), "%.0f", hashrate);
return PoolStats{
.hashrate = buffer,
.dailyEarnings = std::nullopt};
}
catch (const std::exception &e)
{
Serial.printf("Error parsing %s response: %s\n", getPoolName().c_str(), e.what());
return PoolStats{
.hashrate = "0",
.dailyEarnings = std::nullopt};
}
}

View file

@ -0,0 +1,25 @@
#pragma once
#include "lib/mining_pool/mining_pool_interface.hpp"
#include <utils.hpp>
class CKPool : public MiningPoolInterface {
public:
void setPoolUser(const std::string& user) override { poolUser = user; }
void prepareRequest(HTTPClient& http) const override;
std::string getApiUrl() const override;
PoolStats parseResponse(const JsonDocument& doc) const override;
bool supportsDailyEarnings() const override { return false; }
std::string getDailyEarningsLabel() const override { return ""; }
bool hasLogo() const override { return false; }
std::string getDisplayLabel() const override { return "CK/POOL"; }
std::string getPoolName() const override {
return "ckpool";
}
protected:
virtual std::string getBaseUrl() const {
return "https://solo.ckpool.org";
}
};

View file

@ -0,0 +1,16 @@
#pragma once
#include "ckpool.hpp"
class EUCKPool : public CKPool {
public:
std::string getDisplayLabel() const override { return "CK/POOL"; }
std::string getPoolName() const override {
return "eu_ckpool";
}
protected:
std::string getBaseUrl() const override {
return "https://eusolo.ckpool.org";
}
};

View file

@ -0,0 +1,6 @@
// src/noderunners/noderunners_pool.cpp
#include "gobrrr_pool.hpp"
std::string GoBrrrPool::getApiUrl() const {
return "https://pool.gobrrr.me/api/client/" + poolUser;
}

View file

@ -0,0 +1,30 @@
#pragma once
#include "lib/mining_pool/mining_pool_interface.hpp"
#include "lib/mining_pool/public_pool/public_pool.hpp"
#include <icons/icons.h>
class GoBrrrPool : public PublicPool {
public:
std::string getApiUrl() const override;
bool hasLogo() const override { return true; }
std::string getDisplayLabel() const override { return "GOBRRR/POOL"; }
std::string getLogoFilename() const override {
return "gobrrr.bin";
}
std::string getPoolName() const override {
return "gobrrr_pool";
}
int getLogoWidth() const override {
return 122;
}
int getLogoHeight() const override {
return 122;
}
};

View file

@ -0,0 +1,11 @@
#pragma once
#include <cstdint>
#include <stddef.h>
struct LogoData {
const uint8_t* data;
size_t width;
size_t height;
size_t size;
};

View file

@ -0,0 +1,18 @@
#include "mining_pool_interface.hpp"
#include "pool_factory.hpp"
LogoData MiningPoolInterface::getLogo() const {
if (!hasLogo()) {
return LogoData{nullptr, 0, 0, 0};
}
// Check if logo exists
String logoPath = String(PoolFactory::getLogosDir()) + "/" + String(getPoolName().c_str()) + "_logo.bin";
if (!LittleFS.exists(logoPath)) {
return LogoData{nullptr, 0, 0, 0};
}
// Now load the logo (whether it was just downloaded or already existed)
return PoolFactory::loadLogoFromFS(getPoolName(), this);
}

View file

@ -0,0 +1,35 @@
#pragma once
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include "pool_stats.hpp"
#include "logo_data.hpp"
#include "lib/shared.hpp"
class MiningPoolInterface {
public:
virtual ~MiningPoolInterface() = default;
virtual void setPoolUser(const std::string& user) = 0;
virtual void prepareRequest(HTTPClient& http) const = 0;
virtual std::string getApiUrl() const = 0;
virtual PoolStats parseResponse(const JsonDocument& doc) const = 0;
virtual bool hasLogo() const = 0;
virtual LogoData getLogo() const;
virtual std::string getDisplayLabel() const = 0;
virtual bool supportsDailyEarnings() const = 0;
virtual std::string getDailyEarningsLabel() const = 0;
virtual std::string getLogoFilename() const { return ""; }
virtual std::string getPoolName() const = 0;
virtual int getLogoWidth() const { return 0; }
virtual int getLogoHeight() const { return 0; }
std::string getLogoUrl() const {
if (!hasLogo() || getLogoFilename().empty()) {
return "";
}
std::string baseUrl = preferences.getString("poolLogosUrl", DEFAULT_MINING_POOL_LOGOS_URL).c_str();
return baseUrl + "/" + getLogoFilename().c_str();
}
protected:
std::string poolUser;
};

View file

@ -0,0 +1,95 @@
#include "mining_pool_stats_handler.hpp"
std::array<std::string, NUM_SCREENS> parseMiningPoolStatsHashRate(const std::string& hashrate, const MiningPoolInterface& pool)
{
std::array<std::string, NUM_SCREENS> ret;
ret.fill(""); // Initialize all elements to empty strings
std::string label;
std::string output;
parseHashrateString(hashrate, label, output, 4);
std::size_t textLength = output.length();
// Calculate the position where the digits should start
// Account for the position of the mining pool logo and the hashrate label
std::size_t startIndex = NUM_SCREENS - 1 - textLength;
// Insert the pickaxe icon just before the digits
if (startIndex > 0)
{
ret[startIndex - 1] = "mdi:pickaxe";
}
// Place the digits
for (std::size_t i = 0; i < textLength; ++i)
{
ret[startIndex + i] = output.substr(i, 1);
}
ret[NUM_SCREENS - 1] = label;
if (pool.hasLogo()) {
ret[0] = "mdi:miningpool";
} else {
ret[0] = pool.getDisplayLabel();
}
return ret;
}
std::array<std::string, NUM_SCREENS> parseMiningPoolStatsDailyEarnings(int sats, std::string label, const MiningPoolInterface& pool)
{
std::array<std::string, NUM_SCREENS> ret;
ret.fill(""); // Initialize all elements to empty strings
std::string satsDisplay = std::to_string(sats);
if (sats >= 100000000) {
// A whale mining 1+ BTC per day! No decimal points; whales scoff at such things.
label = "BTC" + label.substr(4);
satsDisplay = satsDisplay.substr(0, satsDisplay.length() - 8);
} else if (sats >= 10000000) {
// 10.0M to 99.9M you get one decimal point
satsDisplay = satsDisplay.substr(0, satsDisplay.length() - 6) + "." + satsDisplay[2] + "M";
} else if (sats >= 1000000) {
// 1.00M to 9.99M you get two decimal points
satsDisplay = satsDisplay.substr(0, satsDisplay.length() - 6) + "." + satsDisplay.substr(2, 2) + "M";
} else if (sats >= 100000) {
// 100K to 999K you get no extra precision
satsDisplay = satsDisplay.substr(0, satsDisplay.length() - 3) + "K";
} else if (sats >= 10000) {
// 10.0K to 99.9K you get one decimal point
satsDisplay = satsDisplay.substr(0, satsDisplay.length() - 3) + "." + satsDisplay[2] + "K";
} else {
// Pleb miner! 4 digit or fewer sats will fit as-is. no-op.
}
std::size_t textLength = satsDisplay.length();
// Calculate the position where the digits should start
// Account for the position of the mining pool logo
std::size_t startIndex = NUM_SCREENS - 1 - textLength;
// Insert the pickaxe icon just before the digits if there's room
if (startIndex > 0)
{
ret[startIndex - 1] = "mdi:pickaxe";
}
// Place the digits
for (std::size_t i = 0; i < textLength; ++i)
{
ret[startIndex + i] = satsDisplay.substr(i, 1);
}
ret[NUM_SCREENS - 1] = label;
if (pool.hasLogo()) {
ret[0] = "mdi:miningpool";
} else {
ret[0] = pool.getDisplayLabel();
}
return ret;
}

View file

@ -0,0 +1,11 @@
#include <array>
#include <string>
#include <iostream>
#include <utils.hpp>
#ifndef UNITY_TEST
#include "lib/mining_pool/mining_pool_interface.hpp"
#endif
std::array<std::string, NUM_SCREENS> parseMiningPoolStatsHashRate(const std::string& hashrate, const MiningPoolInterface& pool);
std::array<std::string, NUM_SCREENS> parseMiningPoolStatsDailyEarnings(int sats, std::string label, const MiningPoolInterface& pool);

View file

@ -0,0 +1,48 @@
// src/noderunners/noderunners_pool.cpp
#include "noderunners_pool.hpp"
void NoderunnersPool::prepareRequest(HTTPClient &http) const
{
// Empty as Noderunners doesn't need special headers
}
std::string NoderunnersPool::getApiUrl() const
{
return "https://pool.noderunners.network/api/v1/users/" + poolUser;
}
PoolStats NoderunnersPool::parseResponse(const JsonDocument &doc) const
{
try
{
std::string hashrateStr = doc["hashrate1m"].as<std::string>();
// Special case for "0"
if (hashrateStr == "0") {
return PoolStats{
.hashrate = "0",
.dailyEarnings = std::nullopt
};
}
char unit = hashrateStr.back();
std::string value = hashrateStr.substr(0, hashrateStr.size() - 1);
int multiplier = getHashrateMultiplier(unit);
double hashrate = std::stod(value) * std::pow(10, multiplier);
char buffer[32];
snprintf(buffer, sizeof(buffer), "%.0f", hashrate);
return PoolStats{
.hashrate = buffer,
.dailyEarnings = std::nullopt};
}
catch (const std::exception &e)
{
Serial.printf("Error parsing %s response: %s\n", getPoolName().c_str(), e.what());
return PoolStats{
.hashrate = "0",
.dailyEarnings = std::nullopt};
}
}

View file

@ -0,0 +1,33 @@
#pragma once
#include "lib/mining_pool/mining_pool_interface.hpp"
#include <icons/icons.h>
#include <utils.hpp>
class NoderunnersPool : public MiningPoolInterface {
public:
void setPoolUser(const std::string& user) override { poolUser = user; }
void prepareRequest(HTTPClient& http) const override;
std::string getApiUrl() const override;
PoolStats parseResponse(const JsonDocument& doc) const override;
bool supportsDailyEarnings() const override { return false; }
std::string getDailyEarningsLabel() const override { return ""; }
bool hasLogo() const override { return true; }
std::string getDisplayLabel() const override { return "NODE/RUNNERS"; } // Fallback if needed
std::string getLogoFilename() const override {
return "noderunners.bin";
}
std::string getPoolName() const override {
return "noderunners";
}
int getLogoWidth() const override {
return 122;
}
int getLogoHeight() const override {
return 122;
}
};

View file

@ -0,0 +1,29 @@
#include "ocean_pool.hpp"
void OceanPool::prepareRequest(HTTPClient &http) const
{
// Empty as Ocean doesn't need special headers
}
std::string OceanPool::getApiUrl() const
{
return "https://api.ocean.xyz/v1/statsnap/" + poolUser;
}
PoolStats OceanPool::parseResponse(const JsonDocument &doc) const
{
try
{
return PoolStats{
.hashrate = doc["result"]["hashrate_300s"].as<std::string>(),
.dailyEarnings = static_cast<int64_t>(
doc["result"]["estimated_earn_next_block"].as<float>() * 100000000)};
}
catch (const std::exception &e)
{
Serial.printf("Error parsing %s response: %s\n", getPoolName().c_str(), e.what());
return PoolStats{
.hashrate = "0",
.dailyEarnings = std::nullopt};
}
}

View file

@ -0,0 +1,31 @@
#pragma once
#include "lib/mining_pool/mining_pool_interface.hpp"
#include <icons/icons.h>
class OceanPool : public MiningPoolInterface {
public:
void setPoolUser(const std::string& user) override { poolUser = user; }
void prepareRequest(HTTPClient& http) const override;
std::string getApiUrl() const override;
PoolStats parseResponse(const JsonDocument& doc) const override;
bool hasLogo() const override { return true; }
std::string getDisplayLabel() const override { return "OCEAN/POOL"; } // Fallback if needed
bool supportsDailyEarnings() const override { return true; }
std::string getDailyEarningsLabel() const override { return "sats/block"; }
std::string getLogoFilename() const override {
return "ocean.bin";
}
std::string getPoolName() const override {
return "ocean";
}
int getLogoWidth() const override {
return 122;
}
int getLogoHeight() const override {
return 122;
}
};

View file

@ -0,0 +1,140 @@
#include "pool_factory.hpp"
const char* PoolFactory::MINING_POOL_NAME_OCEAN = "ocean";
const char* PoolFactory::MINING_POOL_NAME_NODERUNNERS = "noderunners";
const char* PoolFactory::MINING_POOL_NAME_BRAIINS = "braiins";
const char* PoolFactory::MINING_POOL_NAME_SATOSHI_RADIO = "satoshi_radio";
const char* PoolFactory::MINING_POOL_NAME_PUBLIC_POOL = "public_pool";
const char* PoolFactory::MINING_POOL_NAME_LOCAL_PUBLIC_POOL = "local_public_pool";
const char* PoolFactory::MINING_POOL_NAME_GOBRRR_POOL = "gobrrr_pool";
const char* PoolFactory::MINING_POOL_NAME_CKPOOL = "ckpool";
const char* PoolFactory::MINING_POOL_NAME_EU_CKPOOL = "eu_ckpool";
const char* PoolFactory::LOGOS_DIR = "/logos";
std::unique_ptr<MiningPoolInterface> PoolFactory::createPool(const std::string& poolName) {
static const std::unordered_map<std::string, std::function<std::unique_ptr<MiningPoolInterface>()>> poolFactories = {
{MINING_POOL_NAME_OCEAN, []() { return std::make_unique<OceanPool>(); }},
{MINING_POOL_NAME_NODERUNNERS, []() { return std::make_unique<NoderunnersPool>(); }},
{MINING_POOL_NAME_BRAIINS, []() { return std::make_unique<BraiinsPool>(); }},
{MINING_POOL_NAME_SATOSHI_RADIO, []() { return std::make_unique<SatoshiRadioPool>(); }},
{MINING_POOL_NAME_PUBLIC_POOL, []() { return std::make_unique<PublicPool>(); }},
{MINING_POOL_NAME_LOCAL_PUBLIC_POOL, []() { return std::make_unique<LocalPublicPool>(); }},
{MINING_POOL_NAME_GOBRRR_POOL, []() { return std::make_unique<GoBrrrPool>(); }},
{MINING_POOL_NAME_CKPOOL, []() { return std::make_unique<CKPool>(); }},
{MINING_POOL_NAME_EU_CKPOOL, []() { return std::make_unique<EUCKPool>(); }}
};
auto it = poolFactories.find(poolName);
if (it == poolFactories.end()) {
return nullptr;
}
return it->second();
}
void PoolFactory::downloadPoolLogo(const std::string& poolName, const MiningPoolInterface* poolInterface)
{
const int MAX_RETRIES = 5;
const int RETRY_DELAY_MS = 1000; // 1 second between retries
if (!poolInterface || !poolInterface->hasLogo()) {
Serial.println(F("No pool interface or logo"));
return;
}
// Ensure logos directory exists
if (!LittleFS.exists(LOGOS_DIR)) {
LittleFS.mkdir(LOGOS_DIR);
}
String logoPath = String(LOGOS_DIR) + "/" + String(poolName.c_str()) + "_logo.bin";
// Only download if the logo doesn't exist
if (!LittleFS.exists(logoPath)) {
// Clean up logos directory first
File root = LittleFS.open(LOGOS_DIR, "r");
if (root) {
File file = root.openNextFile();
while (file) {
String path = file.path();
file.close();
LittleFS.remove(path);
file = root.openNextFile();
}
root.close();
}
// Download new logo with retries
std::string logoUrl = poolInterface->getLogoUrl();
if (!logoUrl.empty()) {
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
Serial.printf("Downloading pool logo (attempt %d of %d)...\n", attempt, MAX_RETRIES);
HTTPClient http;
http.setUserAgent(USER_AGENT);
http.begin(logoUrl.c_str());
int httpCode = http.GET();
if (httpCode == 200) {
File file = LittleFS.open(logoPath, "w");
if (file) {
http.writeToStream(&file);
file.close();
Serial.println(F("Logo downloaded successfully"));
http.end();
return; // Success!
}
}
http.end();
if (attempt < MAX_RETRIES) {
Serial.printf("Failed to download logo, HTTP code: %d. Retrying...\n", httpCode);
vTaskDelay(pdMS_TO_TICKS(RETRY_DELAY_MS));
} else {
Serial.printf("Failed to download logo after %d attempts\n", MAX_RETRIES);
}
}
}
} else {
Serial.println(F("Logo already exists"));
}
}
LogoData PoolFactory::loadLogoFromFS(const std::string& poolName, const MiningPoolInterface* poolInterface)
{
// Initialize with dimensions from the pool interface
LogoData logo = {nullptr,
0,
0,
0};
String logoPath = String(LOGOS_DIR) + "/" + String(poolName.c_str()) + "_logo.bin";
if (!LittleFS.exists(logoPath)) {
return logo;
}
// Only set dimensions if file exists
logo.width = static_cast<size_t>(poolInterface->getLogoWidth());
logo.height = static_cast<size_t>(poolInterface->getLogoHeight());
File file = LittleFS.open(logoPath, "r");
if (!file) {
return logo;
}
size_t size = file.size();
uint8_t* buffer = new uint8_t[size];
if (file.read(buffer, size) == size) {
logo.data = buffer;
logo.size = size;
} else {
delete[] buffer;
logo.data = nullptr;
logo.size = 0;
}
file.close();
return logo;
}

View file

@ -0,0 +1,65 @@
#pragma once
#include "mining_pool_interface.hpp"
#include <memory>
#include <string>
#include "lib/shared.hpp"
#include "lib/config.hpp"
#include "noderunners/noderunners_pool.hpp"
#include "braiins/brains_pool.hpp"
#include "ocean/ocean_pool.hpp"
#include "satoshi_radio/satoshi_radio_pool.hpp"
#include "public_pool/public_pool.hpp"
#include "public_pool/local_public_pool.hpp"
#include "gobrrr_pool/gobrrr_pool.hpp"
#include "ckpool/ckpool.hpp"
#include "ckpool/eu_ckpool.hpp"
#include <LittleFS.h>
#include <HTTPClient.h>
class PoolFactory {
public:
static const char* getLogosDir() { return LOGOS_DIR; }
static std::unique_ptr<MiningPoolInterface> createPool(const std::string& poolName);
static std::vector<std::string> getAvailablePools() {
return {
MINING_POOL_NAME_OCEAN,
MINING_POOL_NAME_NODERUNNERS,
MINING_POOL_NAME_SATOSHI_RADIO,
MINING_POOL_NAME_BRAIINS,
MINING_POOL_NAME_PUBLIC_POOL,
MINING_POOL_NAME_LOCAL_PUBLIC_POOL,
MINING_POOL_NAME_GOBRRR_POOL,
MINING_POOL_NAME_CKPOOL,
MINING_POOL_NAME_EU_CKPOOL
};
}
static std::string getAvailablePoolsAsString() {
const auto pools = getAvailablePools();
std::string result;
for (size_t i = 0; i < pools.size(); ++i) {
result += pools[i];
if (i < pools.size() - 1) {
result += ", ";
}
}
return result;
}
static void downloadPoolLogo(const std::string& poolName, const MiningPoolInterface* poolInterface);
static LogoData loadLogoFromFS(const std::string& poolName, const MiningPoolInterface* poolInterface);
private:
static const char* MINING_POOL_NAME_OCEAN;
static const char* MINING_POOL_NAME_NODERUNNERS;
static const char* MINING_POOL_NAME_BRAIINS;
static const char* MINING_POOL_NAME_SATOSHI_RADIO;
static const char* MINING_POOL_NAME_PUBLIC_POOL;
static const char* MINING_POOL_NAME_LOCAL_PUBLIC_POOL;
static const char* MINING_POOL_NAME_GOBRRR_POOL;
static const char* MINING_POOL_NAME_CKPOOL;
static const char* MINING_POOL_NAME_EU_CKPOOL;
static const char* LOGOS_DIR;
};

View file

@ -0,0 +1,10 @@
#pragma once
#include <string>
#include <optional>
struct PoolStats {
std::string hashrate;
std::optional<int64_t> dailyEarnings;
};

View file

@ -0,0 +1,11 @@
#include "local_public_pool.hpp"
#include "lib/shared.hpp"
#include "lib/defaults.hpp"
std::string LocalPublicPool::getEndpoint() const {
return preferences.getString("localPoolEndpoint", DEFAULT_LOCAL_POOL_ENDPOINT).c_str();
}
std::string LocalPublicPool::getApiUrl() const {
return "http://" + getEndpoint() + "/api/client/" + poolUser;
}

View file

@ -0,0 +1,11 @@
#pragma once
#include "public_pool.hpp"
class LocalPublicPool : public PublicPool {
public:
std::string getApiUrl() const override;
std::string getDisplayLabel() const override { return "LOCAL/POOL"; }
private:
std::string getEndpoint() const;
};

View file

@ -0,0 +1,32 @@
// src/noderunners/noderunners_pool.cpp
#include "public_pool.hpp"
std::string PublicPool::getApiUrl() const
{
return "https://public-pool.io:40557/api/client/" + poolUser;
}
PoolStats PublicPool::parseResponse(const JsonDocument &doc) const
{
uint64_t totalHashrate = 0;
try
{
for (JsonVariantConst worker : doc["workers"].as<JsonArrayConst>())
{
totalHashrate += static_cast<uint64_t>(std::llround(worker["hashRate"].as<double>()));
}
}
catch (const std::exception &e)
{
Serial.printf("Error parsing %s response: %s\n", getPoolName().c_str(), e.what());
return PoolStats{
.hashrate = "0",
.dailyEarnings = std::nullopt};
}
return PoolStats{
.hashrate = std::to_string(totalHashrate),
.dailyEarnings = std::nullopt // Public Pool doesn't support daily earnings
};
}

View file

@ -0,0 +1,15 @@
#pragma once
#include "lib/mining_pool/mining_pool_interface.hpp"
#include "lib/mining_pool/noderunners/noderunners_pool.hpp"
#include <icons/icons.h>
class PublicPool : public NoderunnersPool {
public:
std::string getApiUrl() const override;
bool hasLogo() const override { return false; }
std::string getDisplayLabel() const override { return "PUBLIC/POOL"; }
PoolStats parseResponse(const JsonDocument& doc) const override;
};

View file

@ -0,0 +1,7 @@
// src/noderunners/noderunners_pool.cpp
#include "satoshi_radio_pool.hpp"
std::string SatoshiRadioPool::getApiUrl() const
{
return "https://pool.satoshiradio.nl/api/v1/users/" + poolUser;
}

View file

@ -0,0 +1,14 @@
#pragma once
#include "lib/mining_pool/mining_pool_interface.hpp"
#include "lib/mining_pool/noderunners/noderunners_pool.hpp"
#include <icons/icons.h>
class SatoshiRadioPool : public NoderunnersPool {
public:
std::string getApiUrl() const override;
bool hasLogo() const override { return false; }
std::string getDisplayLabel() const override { return "SATOSHI/RADIO"; } // Fallback if needed
};

View file

@ -0,0 +1,121 @@
#include "mining_pool_stats_fetch.hpp"
void MiningPoolStatsFetch::taskWrapper(void* pvParameters) {
MiningPoolStatsFetch::getInstance().task();
}
void MiningPoolStatsFetch::downloadLogoTaskWrapper(void* pvParameters) {
MiningPoolStatsFetch::getInstance().downloadLogoTask();
}
std::string MiningPoolStatsFetch::getHashRate() const {
return hashrate;
}
int MiningPoolStatsFetch::getDailyEarnings() const {
return dailyEarnings;
}
MiningPoolInterface* MiningPoolStatsFetch::getPool() {
if (!currentPool) {
std::string poolName = preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME).c_str();
currentPool = PoolFactory::createPool(poolName);
}
return currentPool.get();
}
const MiningPoolInterface* MiningPoolStatsFetch::getPool() const {
return currentPool.get();
}
LogoData MiningPoolStatsFetch::getLogo() const {
if (const auto* pool = getPool()) {
return pool->getLogo();
}
return LogoData{};
}
void MiningPoolStatsFetch::task() {
std::string poolName = preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME).c_str();
auto* poolInterface = getPool();
if (!poolInterface) return;
std::string poolUser = preferences.getString("miningPoolUser", DEFAULT_MINING_POOL_USER).c_str();
// Main stats fetching loop
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
HTTPClient http;
http.setUserAgent(USER_AGENT);
poolInterface->setPoolUser(poolUser);
std::string apiUrl = poolInterface->getApiUrl();
http.begin(apiUrl.c_str());
if (debugLogEnabled()) {
Serial.printf("Fetching mining pool stats from %s\r\n", apiUrl.c_str());
}
poolInterface->prepareRequest(http);
int httpCode = http.GET();
if (httpCode == 200) {
String payload = http.getString();
JsonDocument doc;
deserializeJson(doc, payload);
if (debugLogEnabled()) {
Serial.printf("Mining pool stats response: %s\r\n", payload.c_str());
}
PoolStats stats = poolInterface->parseResponse(doc);
hashrate = stats.hashrate;
if (debugLogEnabled()) {
Serial.printf("Mining pool stats parsed hashrate: %s\r\n", stats.hashrate.c_str());
}
dailyEarnings = stats.dailyEarnings ? *stats.dailyEarnings : 0;
if (workQueue != nullptr && (ScreenHandler::getCurrentScreen() == SCREEN_MINING_POOL_STATS_HASHRATE ||
ScreenHandler::getCurrentScreen() == SCREEN_MINING_POOL_STATS_EARNINGS)) {
WorkItem priceUpdate = {TASK_MINING_POOL_STATS_UPDATE, 0};
xQueueSend(workQueue, &priceUpdate, portMAX_DELAY);
}
} else {
Serial.print(F("Error retrieving mining pool data. HTTP status code: "));
Serial.println(httpCode);
}
}
}
void MiningPoolStatsFetch::downloadLogoTask() {
std::string poolName = preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME).c_str();
auto* poolInterface = getPool();
if (!poolInterface) return;
PoolFactory::downloadPoolLogo(poolName, poolInterface);
// If we're on the mining pool stats screen, trigger a display update
if (ScreenHandler::getCurrentScreen() == SCREEN_MINING_POOL_STATS_HASHRATE) {
WorkItem priceUpdate = {TASK_MINING_POOL_STATS_UPDATE, 0};
xQueueSend(workQueue, &priceUpdate, portMAX_DELAY);
}
xTaskNotifyGive(taskHandle);
vTaskDelete(NULL);
}
void MiningPoolStatsFetch::setup() {
xTaskCreate(downloadLogoTaskWrapper,
"logoDownload",
(6 * 1024),
NULL,
tskIDLE_PRIORITY,
NULL);
xTaskCreate(taskWrapper,
"miningPoolStatsFetch",
(6 * 1024),
NULL,
tskIDLE_PRIORITY,
&taskHandle);
}

View file

@ -0,0 +1,45 @@
#pragma once
#include <Arduino.h>
#include <HTTPClient.h>
#include <utils.hpp>
#include <memory>
#include "lib/config.hpp"
#include "lib/shared.hpp"
#include "lib/mining_pool/mining_pool_interface.hpp"
#include "mining_pool/pool_factory.hpp"
class MiningPoolStatsFetch {
public:
static MiningPoolStatsFetch& getInstance() {
static MiningPoolStatsFetch instance;
return instance;
}
void setup();
std::string getHashRate() const;
int getDailyEarnings() const;
TaskHandle_t getTaskHandle() const { return taskHandle; }
static void taskWrapper(void* pvParameters);
static void downloadLogoTaskWrapper(void* pvParameters);
// Pool interface methods
MiningPoolInterface* getPool();
const MiningPoolInterface* getPool() const;
LogoData getLogo() const;
private:
MiningPoolStatsFetch() = default;
~MiningPoolStatsFetch() = default;
MiningPoolStatsFetch(const MiningPoolStatsFetch&) = delete;
MiningPoolStatsFetch& operator=(const MiningPoolStatsFetch&) = delete;
void task();
void downloadLogoTask();
TaskHandle_t taskHandle = nullptr;
std::string hashrate;
int dailyEarnings = 0;
std::unique_ptr<MiningPoolInterface> currentPool;
};

View file

@ -1,19 +1,24 @@
#include "nostr_notify.hpp" #include "nostr_notify.hpp"
#include "led_handler.hpp"
std::vector<nostr::NostrPool *> pools; std::vector<nostr::NostrPool *> pools;
nostr::Transport *transport; nostr::Transport *transport;
TaskHandle_t nostrTaskHandle = NULL; TaskHandle_t nostrTaskHandle = NULL;
boolean nostrIsConnected = false; boolean nostrIsConnected = false;
boolean nostrIsSubscribed = false;
boolean nostrIsSubscribing = true;
String subIdZap;
void setupNostrNotify(bool asDatasource, bool zapNotify) void setupNostrNotify(bool asDatasource, bool zapNotify)
{ {
nostr::esp32::ESP32Platform::initNostr(false); nostr::esp32::ESP32Platform::initNostr(false);
time_t now; // time_t now;
time(&now); // time(&now);
struct tm *utcTimeInfo; // struct tm *utcTimeInfo;
utcTimeInfo = gmtime(&now); // utcTimeInfo = gmtime(&now);
time_t utcNow = mktime(utcTimeInfo); // time_t utcNow = mktime(utcTimeInfo);
time_t timestamp60MinutesAgo = utcNow - 3600; // time_t timestamp60MinutesAgo = utcNow - 3600;
try try
{ {
@ -23,24 +28,11 @@ void setupNostrNotify(bool asDatasource, bool zapNotify)
String pubKey = preferences.getString("nostrPubKey"); String pubKey = preferences.getString("nostrPubKey");
pools.push_back(pool); pools.push_back(pool);
std::vector<std::map<NostrString, std::initializer_list<NostrString>>> filters; std::vector<nostr::NostrRelay *> *relays = pool->getConnectedRelays();
if (zapNotify) if (zapNotify)
{ {
String subIdZap = pool->subscribeMany( subscribeZaps(pool, relay, 60);
{relay},
{
{
{"kinds", {"9735"}},
{"limit", {"1"}},
{"since", {String(timestamp60MinutesAgo)}},
{"#p", {preferences.getString("nostrZapPubkey", DEFAULT_ZAP_NOTIFY_PUBKEY)}},
},
},
handleNostrZapCallback,
onNostrSubscriptionClosed,
onNostrSubscriptionEose);
Serial.println("[ Nostr ] Subscribing to Zap Notifications");
} }
if (asDatasource) if (asDatasource)
@ -49,36 +41,33 @@ void setupNostrNotify(bool asDatasource, bool zapNotify)
{relay}, {relay},
{// First filter {// First filter
{ {
{"kinds", {"1"}}, {"kinds", {"12203"}},
{"since", {String(timestamp60MinutesAgo)}}, {"since", {String(getMinutesAgo(60))}},
{"authors", {pubKey}}, {"authors", {pubKey}},
}}, }},
handleNostrEventCallback, handleNostrEventCallback,
onNostrSubscriptionClosed, onNostrSubscriptionClosed,
onNostrSubscriptionEose); onNostrSubscriptionEose
);
Serial.println("[ Nostr ] Subscribing to Nostr Data Feed"); Serial.println(F("[ Nostr ] Subscribing to Nostr Data Feed"));
} }
std::vector<nostr::NostrRelay *> *relays = pool->getConnectedRelays();
for (nostr::NostrRelay *relay : *relays) for (nostr::NostrRelay *relay : *relays)
{ {
Serial.println("[ Nostr ] Registering to connection events of: " + relay->getUrl()); Serial.println("[ Nostr ] Registering to connection events of: " + relay->getUrl());
relay->getConnection()->addConnectionStatusListener([&](const nostr::ConnectionStatus &status) relay->getConnection()->addConnectionStatusListener([](const nostr::ConnectionStatus &status)
{ {
String sstatus="UNKNOWN"; static const char* STATUS_STRINGS[] = {"UNKNOWN", "CONNECTED", "DISCONNECTED", "ERROR"};
if(status==nostr::ConnectionStatus::CONNECTED){ int statusIndex = static_cast<int>(status);
nostrIsConnected = true;
sstatus="CONNECTED"; nostrIsConnected = (status == nostr::ConnectionStatus::CONNECTED);
}else if(status==nostr::ConnectionStatus::DISCONNECTED){ if (!nostrIsConnected) {
nostrIsConnected = false; nostrIsSubscribed = false;
sstatus="DISCONNECTED";
}else if(status==nostr::ConnectionStatus::ERROR){
sstatus = "ERROR";
} }
//Serial.println("[ Nostr ] Connection status changed: " + sstatus);
}); });
} }
} }
catch (const std::exception &e) catch (const std::exception &e)
{ {
@ -88,8 +77,12 @@ void setupNostrNotify(bool asDatasource, bool zapNotify)
void nostrTask(void *pvParameters) void nostrTask(void *pvParameters)
{ {
int blockFetch = getBlockFetch(); DataSourceType dataSource = getDataSource();
processNewBlock(blockFetch); if(dataSource == NOSTR_SOURCE) {
auto& blockNotify = BlockNotify::getInstance();
int blockFetch = blockNotify.fetchLatestBlock();
blockNotify.processNewBlock(blockFetch);
}
while (1) while (1)
{ {
@ -98,6 +91,10 @@ void nostrTask(void *pvParameters)
// Run internal loop: refresh relays, complete pending connections, send // Run internal loop: refresh relays, complete pending connections, send
// pending messages // pending messages
pool->loop(); pool->loop();
if (!nostrIsSubscribed && !nostrIsSubscribing) {
Serial.println(F("Not subscribed"));
subscribeZaps(pool, preferences.getString("nostrRelay"), 1);
}
} }
vTaskDelay(pdMS_TO_TICKS(100)); vTaskDelay(pdMS_TO_TICKS(100));
} }
@ -105,7 +102,7 @@ void nostrTask(void *pvParameters)
void setupNostrTask() void setupNostrTask()
{ {
xTaskCreate(nostrTask, "nostrTask", 16384, NULL, 10, &nostrTaskHandle); xTaskCreate(nostrTask, "nostrTask", 8192, NULL, 10, &nostrTaskHandle);
} }
boolean nostrConnected() boolean nostrConnected()
@ -125,93 +122,170 @@ void onNostrSubscriptionEose(const String &subId)
// This is the callback that will be called when the subscription is // This is the callback that will be called when the subscription is
// EOSE // EOSE
Serial.println("[ Nostr ] Subscription EOSE: " + subId); Serial.println("[ Nostr ] Subscription EOSE: " + subId);
nostrIsSubscribing = false;
nostrIsSubscribed = true;
} }
void handleNostrEventCallback(const String &subId, nostr::SignedNostrEvent *event) void handleNostrEventCallback(const String &subId, nostr::SignedNostrEvent *event)
{ {
// Received events callback, we can access the event content with
// event->getContent() Here you should handle the event, for this
// test we will just serialize it and print to console
JsonDocument doc; JsonDocument doc;
JsonArray arr = doc["data"].to<JsonArray>(); JsonArray arr = doc["data"].to<JsonArray>();
event->toSendableEvent(arr); event->toSendableEvent(arr);
// Access the second element which is the object
// Early return if array is invalid
if (arr.size() < 2 || !arr[1].is<JsonObject>()) {
return;
}
JsonObject obj = arr[1].as<JsonObject>(); JsonObject obj = arr[1].as<JsonObject>();
JsonArray tags = obj["tags"].as<JsonArray>(); JsonArray tags = obj["tags"].as<JsonArray>();
if (!tags) {
// Flag to check if the tag was found return;
bool tagFound = false;
uint medianFee = 0;
String typeValue;
// Iterate over the tags array
for (JsonArray tag : tags)
{
// Check if the tag is an array with two elements
if (tag.size() == 2)
{
const char *key = tag[0];
const char *value = tag[1];
// Check if the key is "type" and the value is "priceUsd"
if (strcmp(key, "type") == 0 && (strcmp(value, "priceUsd") == 0 || strcmp(value, "blockHeight") == 0))
{
typeValue = value;
tagFound = true;
} }
else if (strcmp(key, "medianFee") == 0)
{ // Use direct value access instead of multiple comparisons
String typeValue;
uint medianFee = 0;
uint blockHeight = 0;
for (JsonArray tag : tags) {
if (tag.size() != 2) continue;
const char *key = tag[0];
if (!key) continue;
// Use switch for better performance on string comparisons
switch (key[0]) {
case 't': // type
if (strcmp(key, "type") == 0) {
const char *value = tag[1];
if (value) typeValue = value;
}
break;
case 'm': // medianFee
if (strcmp(key, "medianFee") == 0) {
medianFee = tag[1].as<uint>(); medianFee = tag[1].as<uint>();
} }
break;
case 'b': // blockHeight
if (strcmp(key, "block") == 0) {
blockHeight = tag[1].as<uint>();
} }
break;
} }
if (tagFound)
{
if (typeValue.equals("priceUsd"))
{
processNewPrice(obj["content"].as<uint>(), CURRENCY_USD);
}
else if (typeValue.equals("blockHeight"))
{
processNewBlock(obj["content"].as<uint>());
} }
if (medianFee != 0) // Process the data
if (!typeValue.isEmpty()) {
if (typeValue == "priceUsd") {
processNewPrice(obj["content"].as<uint>(), CURRENCY_USD);
if (blockHeight != 0) {
auto& blockNotify = BlockNotify::getInstance();
blockNotify.processNewBlock(blockHeight);
}
}
else if (typeValue == "blockHeight") {
auto& blockNotify = BlockNotify::getInstance();
blockNotify.processNewBlock(obj["content"].as<uint>());
}
if (medianFee != 0) {
auto& blockNotify = BlockNotify::getInstance();
blockNotify.processNewBlockFee(medianFee);
}
}
}
time_t getMinutesAgo(int min) {
time_t now;
time(&now);
return now - (min * 60);
}
void subscribeZaps(nostr::NostrPool *pool, const String &relay, int minutesAgo) {
if (subIdZap) {
pool->closeSubscription(subIdZap);
}
nostrIsSubscribing = true;
subIdZap = pool->subscribeMany(
{relay},
{ {
processNewBlockFee(medianFee); {
} {"kinds", {"9735"}},
} {"limit", {"1"}},
{"since", {String(getMinutesAgo(minutesAgo))}},
{"#p", {preferences.getString("nostrZapPubkey", DEFAULT_ZAP_NOTIFY_PUBKEY) }},
// {"#p", [&]() {
// std::initializer_list<NostrString> pubkeys;
// String pubkeysStr = preferences.getString("nostrZapPubkeys", "");
// if (pubkeysStr.length() > 0) {
// // Assuming pubkeys are comma-separated
// char* str = strdup(pubkeysStr.c_str());
// char* token = strtok(str, ",");
// std::vector<NostrString> keys;
// while (token != NULL) {
// keys.push_back(String(token));
// token = strtok(NULL, ",");
// }
// free(str);
// return std::initializer_list<NostrString>(keys.begin(), keys.end());
// }
// // Return default if no pubkeys found
// return std::initializer_list<NostrString>{
// preferences.getString("nostrZapPubkey", DEFAULT_ZAP_NOTIFY_PUBKEY)
// };
// }()},
},
},
handleNostrZapCallback,
onNostrSubscriptionClosed,
onNostrSubscriptionEose);
Serial.println("[ Nostr ] Subscribing to Zap Notifications since " + String(getMinutesAgo(minutesAgo)));
} }
void handleNostrZapCallback(const String &subId, nostr::SignedNostrEvent *event) { void handleNostrZapCallback(const String &subId, nostr::SignedNostrEvent *event) {
// Received events callback, we can access the event content with
// event->getContent() Here you should handle the event, for this
// test we will just serialize it and print to console
JsonDocument doc; JsonDocument doc;
JsonArray arr = doc["data"].to<JsonArray>(); JsonArray arr = doc["data"].to<JsonArray>();
event->toSendableEvent(arr); event->toSendableEvent(arr);
// Access the second element which is the object
// Early return if invalid
if (arr.size() < 2 || !arr[1].is<JsonObject>()) {
return;
}
JsonObject obj = arr[1].as<JsonObject>(); JsonObject obj = arr[1].as<JsonObject>();
JsonArray tags = obj["tags"].as<JsonArray>(); JsonArray tags = obj["tags"].as<JsonArray>();
if (!tags) {
return;
}
uint64_t zapAmount = 0;
String zapPubkey;
for (JsonArray tag : tags) {
if (tag.size() != 2) continue;
// Iterate over the tags array
for (JsonArray tag : tags)
{
// Check if the tag is an array with two elements
if (tag.size() == 2)
{
const char *key = tag[0]; const char *key = tag[0];
const char *value = tag[1]; const char *value = tag[1];
if (!key || !value) continue;
if (strcmp(key, "bolt11") == 0) if (key[0] == 'b' && strcmp(key, "bolt11") == 0) {
zapAmount = getAmountInSatoshis(std::string(value));
}
else if (key[0] == 'p' && strcmp(key, "p") == 0) {
zapPubkey = value;
}
}
if (zapAmount == 0) return;
std::array<std::string, NUM_SCREENS> textEpdContent = parseZapNotify(zapAmount, preferences.getBool("useSatsSymbol", DEFAULT_USE_SATS_SYMBOL));
if (debugLogEnabled())
{ {
Serial.print(F("Got a zap of ")); Serial.printf("Got a zap of %llu sats for %s\n", zapAmount, zapPubkey.c_str());
}
int64_t satsAmount = getAmountInSatoshis(std::string(value));
Serial.print(satsAmount);
Serial.println(F(" sats"));
std::array<std::string, NUM_SCREENS> textEpdContent = parseZapNotify(satsAmount, preferences.getBool("useSatsSymbol", DEFAULT_USE_SATS_SYMBOL));
uint64_t timerPeriod = 0; uint64_t timerPeriod = 0;
if (isTimerActive()) if (isTimerActive())
@ -220,17 +294,33 @@ void handleNostrZapCallback(const String &subId, nostr::SignedNostrEvent *event)
timerPeriod = getTimerSeconds(); timerPeriod = getTimerSeconds();
esp_timer_stop(screenRotateTimer); esp_timer_stop(screenRotateTimer);
} }
setCurrentScreen(SCREEN_CUSTOM); ScreenHandler::setCurrentScreen(SCREEN_CUSTOM);
setEpdContent(textEpdContent); EPDManager::getInstance().setContent(textEpdContent);
vTaskDelay(pdMS_TO_TICKS(315 * NUM_SCREENS) + pdMS_TO_TICKS(250)); vTaskDelay(pdMS_TO_TICKS(315 * NUM_SCREENS) + pdMS_TO_TICKS(250));
queueLedEffect(LED_EFFECT_NOSTR_ZAP); if (preferences.getBool("ledFlashOnZap", DEFAULT_LED_FLASH_ON_ZAP))
{
getLedHandler().queueEffect(LED_EFFECT_NOSTR_ZAP);
}
if (timerPeriod > 0) if (timerPeriod > 0)
{ {
esp_timer_start_periodic(screenRotateTimer, esp_timer_start_periodic(screenRotateTimer,
timerPeriod * usPerSecond); timerPeriod * usPerSecond);
} }
} }
}
} // void onNostrEvent(const String &subId, const nostr::Event &event) {
} // // This is the callback that will be called when a new event is received
// if (event.kind == 9735) {
// // Parse the zap amount from the event
// uint16_t amount = parseZapAmount(event);
// if (amount > 0) {
// std::array<std::string, NUM_SCREENS> zapContent = parseZapNotify(amount, true);
// EPDManager::getInstance().setContent(zapContent);
// if (preferences.getBool("ledFlashOnUpd", DEFAULT_LED_FLASH_ON_UPD)) {
// getLedHandler().queueEffect(LED_FLASH_BLOCK_NOTIFY);
// }
// }
// }
// }

View file

@ -25,3 +25,6 @@ void handleNostrZapCallback(const String &subId, nostr::SignedNostrEvent *event)
void onNostrSubscriptionClosed(const String &subId, const String &reason); void onNostrSubscriptionClosed(const String &subId, const String &reason);
void onNostrSubscriptionEose(const String &subId); void onNostrSubscriptionEose(const String &subId);
time_t getMinutesAgo(int min);
void subscribeZaps(nostr::NostrPool *pool, const String &relay, int minutesAgo);

View file

@ -1,4 +1,5 @@
#include "ota.hpp" #include "ota.hpp"
#include "led_handler.hpp"
TaskHandle_t taskOtaHandle = NULL; TaskHandle_t taskOtaHandle = NULL;
bool isOtaUpdating = false; bool isOtaUpdating = false;
@ -31,6 +32,9 @@ void setupOTA()
void onOTAProgress(unsigned int progress, unsigned int total) void onOTAProgress(unsigned int progress, unsigned int total)
{ {
uint percentage = progress / (total / 100); uint percentage = progress / (total / 100);
auto& ledHandler = getLedHandler();
auto& pixels = ledHandler.getPixels();
pixels.fill(pixels.Color(0, 255, 0)); pixels.fill(pixels.Color(0, 255, 0));
if (percentage < 100) if (percentage < 100)
{ {
@ -53,10 +57,10 @@ void onOTAProgress(unsigned int progress, unsigned int total)
void onOTAStart() void onOTAStart()
{ {
forceFullRefresh(); EPDManager::getInstance().forceFullRefresh();
std::array<String, NUM_SCREENS> epdContent = {"U", "P", "D", "A", std::array<String, NUM_SCREENS> epdContent = {"U", "P", "D", "A",
"T", "E", "!"}; "T", "E", "!"};
setEpdContent(epdContent); EPDManager::getInstance().setContent(epdContent);
// Stop all timers // Stop all timers
esp_timer_stop(screenRotateTimer); esp_timer_stop(screenRotateTimer);
esp_timer_stop(minuteTimer); esp_timer_stop(minuteTimer);
@ -64,15 +68,14 @@ void onOTAStart()
// Stop or suspend all tasks // Stop or suspend all tasks
// vTaskSuspend(priceUpdateTaskHandle); // vTaskSuspend(priceUpdateTaskHandle);
// vTaskSuspend(blockUpdateTaskHandle); // vTaskSuspend(blockUpdateTaskHandle);
vTaskSuspend(workerTaskHandle);
vTaskSuspend(taskScreenRotateTaskHandle); vTaskSuspend(taskScreenRotateTaskHandle);
vTaskSuspend(workerTaskHandle);
// vTaskSuspend(ledTaskHandle); vTaskSuspend(eventSourceTaskHandle);
vTaskSuspend(buttonTaskHandle); ButtonHandler::suspendTask();
// stopWebServer(); // stopWebServer();
stopBlockNotify(); auto& blockNotify = BlockNotify::getInstance();
stopPriceNotify(); blockNotify.stop();
} }
void handleOTATask(void *parameter) void handleOTATask(void *parameter)
@ -84,15 +87,16 @@ void handleOTATask(void *parameter)
if (xQueueReceive(otaQueue, &msg, 0) == pdTRUE) if (xQueueReceive(otaQueue, &msg, 0) == pdTRUE)
{ {
if (msg.updateType == UPDATE_ALL) { if (msg.updateType == UPDATE_ALL) {
queueLedEffect(LED_FLASH_UPDATE); isOtaUpdating = true;
getLedHandler().queueEffect(LED_FLASH_UPDATE);
int resultWebUi = downloadUpdateHandler(UPDATE_WEBUI); int resultWebUi = downloadUpdateHandler(UPDATE_WEBUI);
queueLedEffect(LED_FLASH_UPDATE); getLedHandler().queueEffect(LED_FLASH_UPDATE);
int resultFw = downloadUpdateHandler(UPDATE_FIRMWARE); int resultFw = downloadUpdateHandler(UPDATE_FIRMWARE);
if (resultWebUi == 0 && resultFw == 0) { if (resultWebUi == 0 && resultFw == 0) {
ESP.restart(); ESP.restart();
} else { } else {
queueLedEffect(LED_FLASH_ERROR); getLedHandler().queueEffect(LED_FLASH_ERROR);
vTaskDelay(pdMS_TO_TICKS(3000)); vTaskDelay(pdMS_TO_TICKS(3000));
ESP.restart(); ESP.restart();
} }
@ -106,9 +110,12 @@ void handleOTATask(void *parameter)
ReleaseInfo getLatestRelease(const String &fileToDownload) ReleaseInfo getLatestRelease(const String &fileToDownload)
{ {
String releaseUrl = "https://api.github.com/repos/btclock/btclock_v3/releases/latest"; String releaseUrl = preferences.getString("gitReleaseUrl");
WiFiClientSecure client; WiFiClientSecure client;
client.setCACert(github_root_ca); // client.setCACert(isrg_root_x1cert);
client.setCACertBundle(rootca_crt_bundle_start);
HTTPClient http; HTTPClient http;
http.begin(client, releaseUrl); http.begin(client, releaseUrl);
http.setUserAgent(USER_AGENT); http.setUserAgent(USER_AGENT);
@ -153,7 +160,7 @@ ReleaseInfo getLatestRelease(const String &fileToDownload)
int downloadUpdateHandler(char updateType) int downloadUpdateHandler(char updateType)
{ {
WiFiClientSecure client; WiFiClientSecure client;
client.setCACert(github_root_ca); client.setCACertBundle(rootca_crt_bundle_start);
HTTPClient http; HTTPClient http;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
@ -168,19 +175,18 @@ int downloadUpdateHandler(char updateType)
break; break;
case UPDATE_WEBUI: case UPDATE_WEBUI:
{ {
latestRelease = getLatestRelease("littlefs.bin"); latestRelease = getLatestRelease(getWebUiFilename());
// updateWebUi(latestRelease.fileUrl, U_SPIFFS); // updateWebUi(latestRelease.fileUrl, U_SPIFFS);
// return 0; // return 0;
} }
break; break;
} }
// First, download the expected SHA256 // First, download the expected SHA256
String expectedSHA256 = downloadSHA256(latestRelease.checksumUrl); String expectedSHA256 = downloadSHA256(latestRelease.checksumUrl);
if (expectedSHA256.isEmpty()) if (expectedSHA256.isEmpty())
{ {
Serial.println("Failed to get SHA256 checksum. Aborting update."); Serial.println(F("Failed to get SHA256 checksum. Aborting update."));
return false; return false;
} }
@ -216,7 +222,7 @@ int downloadUpdateHandler(char updateType)
if (bytesRead != contentLength) if (bytesRead != contentLength)
{ {
Serial.println("Failed to read entire firmware"); Serial.println(F("Failed to read entire firmware"));
free(firmware); free(firmware);
return false; return false;
} }
@ -224,14 +230,14 @@ int downloadUpdateHandler(char updateType)
// Calculate SHA256 // Calculate SHA256
String calculated_sha256 = calculateSHA256(firmware, contentLength); String calculated_sha256 = calculateSHA256(firmware, contentLength);
Serial.print("Calculated checksum: "); Serial.print(F("Calculated checksum: "));
Serial.println(calculated_sha256); Serial.println(calculated_sha256);
Serial.print("Expected checksum: "); Serial.print(F("Expected checksum: "));
Serial.println(expectedSHA256); Serial.println(expectedSHA256);
if (calculated_sha256 != expectedSHA256) if (calculated_sha256 != expectedSHA256)
{ {
Serial.println("Checksum mismatch. Aborting update."); Serial.println(F("Checksum mismatch. Aborting update."));
free(firmware); free(firmware);
return false; return false;
} }
@ -257,15 +263,15 @@ int downloadUpdateHandler(char updateType)
if (Update.end()) if (Update.end())
{ {
Serial.println("OTA done!"); Serial.println(F("OTA done!"));
if (Update.isFinished()) if (Update.isFinished())
{ {
Serial.println("Update successfully completed. Rebooting."); Serial.println(F("Update successfully completed. Rebooting."));
// ESP.restart(); // ESP.restart();
} }
else else
{ {
Serial.println("Update not finished? Something went wrong!"); Serial.println(F("Update not finished? Something went wrong!"));
free(firmware); free(firmware);
return 503; return 503;
} }
@ -279,14 +285,14 @@ int downloadUpdateHandler(char updateType)
} }
else else
{ {
Serial.println("Not enough space to begin OTA"); Serial.println(F("Not enough space to begin OTA"));
free(firmware); free(firmware);
return 503; return 503;
} }
} }
else else
{ {
Serial.println("Invalid content length"); Serial.println(F("Invalid content length"));
return 503; return 503;
} }
} }
@ -303,7 +309,7 @@ int downloadUpdateHandler(char updateType)
void updateWebUi(String latestRelease, int command) void updateWebUi(String latestRelease, int command)
{ {
WiFiClientSecure client; WiFiClientSecure client;
client.setCACert(github_root_ca); client.setCACertBundle(rootca_crt_bundle_start);
HTTPClient http; HTTPClient http;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.begin(client, latestRelease); http.begin(client, latestRelease);
@ -336,7 +342,7 @@ void updateWebUi(String latestRelease, int command)
Serial.println(calculated_sha256); Serial.println(calculated_sha256);
if ((command == U_FLASH && expectedSHA256.equals(calculated_sha256)) || command == U_SPIFFS) if ((command == U_FLASH && expectedSHA256.equals(calculated_sha256)) || command == U_SPIFFS)
{ {
Serial.println("Checksum verified. Proceeding with update."); Serial.println(F("Checksum verified. Proceeding with update."));
Update.onProgress(onOTAProgress); Update.onProgress(onOTAProgress);
@ -347,38 +353,38 @@ void updateWebUi(String latestRelease, int command)
Update.write(buffer, contentLength); Update.write(buffer, contentLength);
if (Update.end()) if (Update.end())
{ {
Serial.println("Update complete. Rebooting."); Serial.println(F("Update complete. Rebooting."));
ESP.restart(); ESP.restart();
} }
else else
{ {
Serial.println("Error in update process."); Serial.println(F("Error in update process."));
} }
} }
else else
{ {
Serial.println("Not enough space to begin OTA"); Serial.println(F("Not enough space to begin OTA"));
} }
} }
else else
{ {
Serial.println("Checksum mismatch. Aborting update."); Serial.println(F("Checksum mismatch. Aborting update."));
} }
} }
else else
{ {
Serial.println("Error downloading firmware"); Serial.println(F("Error downloading firmware"));
} }
free(buffer); free(buffer);
} }
else else
{ {
Serial.println("Not enough memory to allocate buffer"); Serial.println(F("Not enough memory to allocate buffer"));
} }
} }
else else
{ {
Serial.println("Invalid content length"); Serial.println(F("Invalid content length"));
} }
} }
else else
@ -416,12 +422,12 @@ String downloadSHA256(const String &sha256Url)
{ {
if (sha256Url.isEmpty()) if (sha256Url.isEmpty())
{ {
Serial.println("Failed to get SHA256 file URL"); Serial.println(F("Failed to get SHA256 file URL"));
return ""; return "";
} }
WiFiClientSecure client; WiFiClientSecure client;
client.setCACert(github_root_ca); client.setCACertBundle(rootca_crt_bundle_start);
HTTPClient http; HTTPClient http;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.begin(client, sha256Url); http.begin(client, sha256Url);

View file

@ -1,151 +1,65 @@
#include "price_notify.hpp" #include "price_notify.hpp"
const char *wsOwnServerPrice = "wss://ws.btclock.dev/ws?assets=bitcoin";
const char *wsOwnServerV2 = "wss://ws-staging.btclock.dev/api/v2/ws";
const char *wsServerPrice = "wss://ws.coincap.io/prices?assets=bitcoin"; const char *wsServerPrice = "wss://ws.coincap.io/prices?assets=bitcoin";
const char* coincapWsCert = R"EOF( WebSocketsClient webSocket;
-----BEGIN CERTIFICATE----- uint currentPrice = 90000;
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
)EOF";
// WebsocketsClient client;
esp_websocket_client_handle_t clientPrice = NULL;
esp_websocket_client_config_t config;
uint currentPrice = 50000;
unsigned long int lastPriceUpdate; unsigned long int lastPriceUpdate;
bool priceNotifyInit = false; bool priceNotifyInit = false;
std::map<char, std::uint64_t> currencyMap; std::map<char, std::uint64_t> currencyMap;
std::map<char, unsigned long int> lastUpdateMap; std::map<char, unsigned long int> lastUpdateMap;
WebSocketsClient priceNotifyWs; TaskHandle_t priceNotifyTaskHandle;
void onWebsocketPriceEvent(WStype_t type, uint8_t * payload, size_t length);
void setupPriceNotify() void setupPriceNotify()
{ {
if (preferences.getBool("ownDataSource", DEFAULT_OWN_DATA_SOURCE)) webSocket.beginSSL("ws.coincap.io", 443, "/prices?assets=bitcoin");
{ webSocket.onEvent([](WStype_t type, uint8_t * payload, size_t length) {
config = {.uri = wsOwnServerPrice, onWebsocketPriceEvent(type, payload, length);
.user_agent = USER_AGENT}; });
} webSocket.setReconnectInterval(5000);
else webSocket.enableHeartbeat(15000, 3000, 2);
{
config = {.uri = wsServerPrice, setupPriceNotifyTask();
.user_agent = USER_AGENT};
config.cert_pem = coincapWsCert;
config.task_stack = (6*1024);
} }
clientPrice = esp_websocket_client_init(&config); void onWebsocketPriceEvent(WStype_t type, uint8_t * payload, size_t length) {
esp_websocket_register_events(clientPrice, WEBSOCKET_EVENT_ANY, switch(type) {
onWebsocketPriceEvent, clientPrice); case WStype_DISCONNECTED:
esp_websocket_client_start(clientPrice); Serial.println(F("Price WS Connection Closed"));
break;
// priceNotifyWs.beginSSL("ws.coincap.io", 443, "/prices?assets=bitcoin"); case WStype_CONNECTED:
// priceNotifyWs.onEvent(onWebsocketPriceEvent);
// priceNotifyWs.setReconnectInterval(5000);
// priceNotifyWs.enableHeartbeat(15000, 3000, 2);
}
// void onWebsocketPriceEvent(WStype_t type, uint8_t * payload, size_t length) {
// switch(type) {
// case WStype_DISCONNECTED:
// Serial.printf("[WSc] Disconnected!\n");
// break;
// case WStype_CONNECTED:
// {
// Serial.printf("[WSc] Connected to url: %s\n", payload);
// break;
// }
// case WStype_TEXT:
// String message = String((char*)payload);
// onWebsocketPriceMessage(message);
// break;
// case WStype_BIN:
// break;
// case WStype_ERROR:
// case WStype_FRAGMENT_TEXT_START:
// case WStype_FRAGMENT_BIN_START:
// case WStype_FRAGMENT:
// case WStype_PING:
// case WStype_PONG:
// case WStype_FRAGMENT_FIN:
// break;
// }
// }
void onWebsocketPriceEvent(void *handler_args, esp_event_base_t base,
int32_t event_id, void *event_data)
{ {
esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data; Serial.println("Connected to " + String(wsServerPrice));
switch (event_id)
{
case WEBSOCKET_EVENT_CONNECTED:
Serial.println("Connected to " + String(config.uri) + " WebSocket");
priceNotifyInit = true; priceNotifyInit = true;
break;
case WEBSOCKET_EVENT_DATA:
onWebsocketPriceMessage(data);
if (preferences.getBool("ownDataSource", DEFAULT_OWN_DATA_SOURCE))
{
onWebsocketBlockMessage(data);
}
break;
case WEBSOCKET_EVENT_ERROR:
Serial.println(F("Price WS Connnection error"));
break;
case WEBSOCKET_EVENT_DISCONNECTED:
Serial.println(F("Price WS Connnection Closed"));
break; break;
} }
} case WStype_TEXT:
void onWebsocketPriceMessage(esp_websocket_event_data_t *event_data)
{ {
JsonDocument doc; JsonDocument doc;
deserializeJson(doc, (char *)payload);
deserializeJson(doc, (char *)event_data->data_ptr); if (doc["bitcoin"].is<JsonObject>())
if (doc.containsKey("bitcoin"))
{ {
if (currentPrice != doc["bitcoin"].as<long>()) if (currentPrice != doc["bitcoin"].as<long>())
{ {
processNewPrice(doc["bitcoin"].as<long>(), CURRENCY_USD); processNewPrice(doc["bitcoin"].as<long>(), CURRENCY_USD);
} }
} }
break;
}
case WStype_BIN:
break;
case WStype_ERROR:
case WStype_FRAGMENT_TEXT_START:
case WStype_FRAGMENT_BIN_START:
case WStype_FRAGMENT:
case WStype_PING:
case WStype_PONG:
case WStype_FRAGMENT_FIN:
break;
}
} }
void processNewPrice(uint newPrice, char currency) void processNewPrice(uint newPrice, char currency)
@ -157,23 +71,42 @@ void processNewPrice(uint newPrice, char currency)
if (lastUpdateMap.find(currency) == lastUpdateMap.end() || if (lastUpdateMap.find(currency) == lastUpdateMap.end() ||
(currentTime - lastUpdateMap[currency]) > minSecPriceUpd) (currentTime - lastUpdateMap[currency]) > minSecPriceUpd)
{ {
// const unsigned long oldPrice = currentPrice;
currencyMap[currency] = newPrice; currencyMap[currency] = newPrice;
// if (lastUpdateMap[currency] == 0 ||
// (currentTime - lastUpdateMap[currency]) > 120) // Store price in preferences if enough time has passed
// { if (lastUpdateMap[currency] == 0 || (currentTime - lastUpdateMap[currency]) > 120)
// preferences.putUInt("lastPrice", currentPrice); {
// } String prefKey = String("lastPrice_") + getCurrencyCode(currency).c_str();
preferences.putUInt(prefKey.c_str(), newPrice);
}
lastUpdateMap[currency] = currentTime; lastUpdateMap[currency] = currentTime;
// if (abs((int)(oldPrice-currentPrice)) > round(0.0015*oldPrice)) {
if (workQueue != nullptr && (getCurrentScreen() == SCREEN_BTC_TICKER || if (workQueue != nullptr && (ScreenHandler::getCurrentScreen() == SCREEN_BTC_TICKER ||
getCurrentScreen() == SCREEN_SATS_PER_CURRENCY || ScreenHandler::getCurrentScreen() == SCREEN_SATS_PER_CURRENCY ||
getCurrentScreen() == SCREEN_MARKET_CAP)) ScreenHandler::getCurrentScreen() == SCREEN_MARKET_CAP))
{ {
WorkItem priceUpdate = {TASK_PRICE_UPDATE, currency}; WorkItem priceUpdate = {TASK_PRICE_UPDATE, currency};
xQueueSend(workQueue, &priceUpdate, portMAX_DELAY); xQueueSend(workQueue, &priceUpdate, portMAX_DELAY);
} }
//} }
}
void loadStoredPrices()
{
// Load prices for all supported currencies
std::vector<std::string> currencies = getAvailableCurrencies();
for (const std::string &currency : currencies) {
// Get first character as the currency identifier
String prefKey = String("lastPrice_") + currency.c_str();
uint storedPrice = preferences.getUInt(prefKey.c_str(), 0);
if (storedPrice > 0) {
currencyMap[getCurrencyChar(currency)] = storedPrice;
// Initialize lastUpdateMap to 0 so next update will store immediately
lastUpdateMap[getCurrencyChar(currency)] = 0;
}
} }
} }
@ -203,9 +136,7 @@ void setPrice(uint newPrice, char currency)
bool isPriceNotifyConnected() bool isPriceNotifyConnected()
{ {
if (clientPrice == NULL) return webSocket.isConnected();
return false;
return esp_websocket_client_is_connected(clientPrice);
} }
bool getPriceNotifyInit() bool getPriceNotifyInit()
@ -215,24 +146,30 @@ bool getPriceNotifyInit()
void stopPriceNotify() void stopPriceNotify()
{ {
if (clientPrice == NULL) webSocket.disconnect();
return; if (priceNotifyTaskHandle != NULL) {
esp_websocket_client_close(clientPrice, pdMS_TO_TICKS(5000)); vTaskDelete(priceNotifyTaskHandle);
esp_websocket_client_stop(clientPrice); priceNotifyTaskHandle = NULL;
esp_websocket_client_destroy(clientPrice); }
clientPrice = NULL;
} }
void restartPriceNotify() void restartPriceNotify()
{ {
stopPriceNotify(); stopPriceNotify();
if (clientPrice == NULL)
{
setupPriceNotify(); setupPriceNotify();
return;
} }
// esp_websocket_client_close(clientPrice, pdMS_TO_TICKS(5000));
// esp_websocket_client_stop(clientPrice); void taskPriceNotify(void *pvParameters)
// esp_websocket_client_start(clientPrice); {
for (;;)
{
webSocket.loop();
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void setupPriceNotifyTask()
{
xTaskCreate(taskPriceNotify, "priceNotify", (6 * 1024), NULL, tskIDLE_PRIORITY,
&priceNotifyTaskHandle);
} }

View file

@ -2,24 +2,22 @@
#include <Arduino.h> #include <Arduino.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <esp_websocket_client.h> #include <WebSocketsClient.h>
#include "block_notify.hpp"
#include <string> #include <string>
#include "lib/screen_handler.hpp" #include "lib/screen_handler.hpp"
extern TaskHandle_t priceNotifyTaskHandle;
void setupPriceNotify(); void setupPriceNotify();
void setupPriceNotifyTask();
void taskPriceNotify(void *pvParameters);
void onWebsocketPriceEvent(void *handler_args, esp_event_base_t base, void onWebsocketPriceEvent(WStype_t type, uint8_t * payload, size_t length);
int32_t event_id, void *event_data);
//void onWebsocketPriceEvent(WStype_t type, uint8_t * payload, size_t length);
void onWebsocketPriceMessage(esp_websocket_event_data_t *event_data);
uint getPrice(char currency); uint getPrice(char currency);
void setPrice(uint newPrice, char currency); void setPrice(uint newPrice, char currency);
//void processNewPrice(uint newPrice);
void processNewPrice(uint newPrice, char currency); void processNewPrice(uint newPrice, char currency);
bool isPriceNotifyConnected(); bool isPriceNotifyConnected();
@ -28,3 +26,4 @@ void restartPriceNotify();
bool getPriceNotifyInit(); bool getPriceNotifyInit();
uint getLastPriceUpdate(char currency); uint getLastPriceUpdate(char currency);
void loadStoredPrices();

View file

@ -1,156 +1,32 @@
#include "screen_handler.hpp" #include "screen_handler.hpp"
// TaskHandle_t priceUpdateTaskHandle;
// TaskHandle_t blockUpdateTaskHandle;
// TaskHandle_t timeUpdateTaskHandle;
TaskHandle_t taskScreenRotateTaskHandle; TaskHandle_t taskScreenRotateTaskHandle;
TaskHandle_t workerTaskHandle; TaskHandle_t workerTaskHandle;
std::array<std::string, NUM_SCREENS> taskEpdContent = {};
std::string priceString;
#define WORK_QUEUE_SIZE 10
QueueHandle_t workQueue = NULL; QueueHandle_t workQueue = NULL;
uint currentScreen = SCREEN_BLOCK_HEIGHT; // Initialize static members
uint currentCurrency = CURRENCY_USD; uint ScreenHandler::currentScreen = SCREEN_BLOCK_HEIGHT;
uint ScreenHandler::currentCurrency = CURRENCY_USD;
void workerTask(void *pvParameters) { std::array<std::string, NUM_SCREENS> taskEpdContent = {};
WorkItem receivedItem;
while (1) { // Convert existing functions to static member functions
// Wait for a work item to be available in the queue void ScreenHandler::setCurrentScreen(uint newScreen) {
if (xQueueReceive(workQueue, &receivedItem, portMAX_DELAY)) {
// Process the work item based on its type
switch (receivedItem.type) {
case TASK_BITAXE_UPDATE: {
if (getCurrentScreen() == SCREEN_BITAXE_HASHRATE) {
taskEpdContent =
parseBitaxeHashRate(getBitAxeHashRate());
} else if (getCurrentScreen() == SCREEN_BITAXE_BESTDIFF) {
taskEpdContent =
parseBitaxeBestDiff(getBitaxeBestDiff());
}
setEpdContent(taskEpdContent);
}
break;
case TASK_PRICE_UPDATE: {
uint currency = getCurrentCurrency();
uint price = getPrice(currency);
if (getCurrentScreen() == SCREEN_BTC_TICKER) {
taskEpdContent = parsePriceData(price, currency, preferences.getBool("suffixPrice", DEFAULT_SUFFIX_PRICE));
} else if (getCurrentScreen() == SCREEN_SATS_PER_CURRENCY) {
taskEpdContent = parseSatsPerCurrency(price, currency, preferences.getBool("useSatsSymbol", DEFAULT_USE_SATS_SYMBOL));
} else {
taskEpdContent =
parseMarketCap(getBlockHeight(), price, currency,
preferences.getBool("mcapBigChar", DEFAULT_MCAP_BIG_CHAR));
}
setEpdContent(taskEpdContent);
break;
}
case TASK_FEE_UPDATE: {
if (getCurrentScreen() == SCREEN_BLOCK_FEE_RATE) {
taskEpdContent = parseBlockFees(static_cast<std::uint16_t>(getBlockMedianFee()));
setEpdContent(taskEpdContent);
}
break;
}
case TASK_BLOCK_UPDATE: {
if (getCurrentScreen() != SCREEN_HALVING_COUNTDOWN) {
taskEpdContent = parseBlockHeight(getBlockHeight());
} else {
taskEpdContent = parseHalvingCountdown(getBlockHeight(), preferences.getBool("useBlkCountdown", DEFAULT_USE_BLOCK_COUNTDOWN));
}
if (getCurrentScreen() == SCREEN_HALVING_COUNTDOWN ||
getCurrentScreen() == SCREEN_BLOCK_HEIGHT) {
setEpdContent(taskEpdContent);
}
break;
}
case TASK_TIME_UPDATE: {
if (getCurrentScreen() == SCREEN_TIME) {
time_t currentTime;
struct tm timeinfo;
time(&currentTime);
localtime_r(&currentTime, &timeinfo);
std::string timeString;
String minute = String(timeinfo.tm_min);
if (minute.length() < 2) {
minute = "0" + minute;
}
timeString =
std::to_string(timeinfo.tm_hour) + ":" + minute.c_str();
timeString.insert(timeString.begin(),
NUM_SCREENS - timeString.length(), ' ');
taskEpdContent[0] = std::to_string(timeinfo.tm_mday) + "/" +
std::to_string(timeinfo.tm_mon + 1);
for (uint i = 1; i < NUM_SCREENS; i++) {
taskEpdContent[i] = timeString[i];
}
setEpdContent(taskEpdContent);
}
break;
}
// Add more cases for additional task types
}
}
}
}
void taskScreenRotate(void *pvParameters) {
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
nextScreen();
}
}
void setupTasks() {
workQueue = xQueueCreate(WORK_QUEUE_SIZE, sizeof(WorkItem));
xTaskCreate(workerTask, "workerTask", 4096, NULL, tskIDLE_PRIORITY,
&workerTaskHandle);
xTaskCreate(taskScreenRotate, "rotateScreen", 4096, NULL, tskIDLE_PRIORITY,
&taskScreenRotateTaskHandle);
waitUntilNoneBusy();
if (findScreenIndexByValue(preferences.getUInt("currentScreen", DEFAULT_CURRENT_SCREEN)) != -1)
setCurrentScreen(preferences.getUInt("currentScreen", DEFAULT_CURRENT_SCREEN));
}
uint getCurrentScreen() { return currentScreen; }
void setCurrentScreen(uint newScreen) {
if (newScreen != SCREEN_CUSTOM) { if (newScreen != SCREEN_CUSTOM) {
preferences.putUInt("currentScreen", newScreen); preferences.putUInt("currentScreen", newScreen);
} }
currentScreen = newScreen; currentScreen = newScreen;
switch (currentScreen) { switch (currentScreen) {
case SCREEN_TIME: { case SCREEN_TIME: {
WorkItem timeUpdate = {TASK_TIME_UPDATE, 0}; WorkItem timeUpdate = {TASK_TIME_UPDATE, 0};
xQueueSend(workQueue, &timeUpdate, portMAX_DELAY); xQueueSend(workQueue, &timeUpdate, portMAX_DELAY);
// xTaskNotifyGive(timeUpdateTaskHandle);
break; break;
} }
case SCREEN_HALVING_COUNTDOWN: case SCREEN_HALVING_COUNTDOWN:
case SCREEN_BLOCK_HEIGHT: { case SCREEN_BLOCK_HEIGHT: {
WorkItem blockUpdate = {TASK_BLOCK_UPDATE, 0}; WorkItem blockUpdate = {TASK_BLOCK_UPDATE, 0};
xQueueSend(workQueue, &blockUpdate, portMAX_DELAY); xQueueSend(workQueue, &blockUpdate, portMAX_DELAY);
// xTaskNotifyGive(blockUpdateTaskHandle);
break; break;
} }
case SCREEN_MARKET_CAP: case SCREEN_MARKET_CAP:
@ -158,7 +34,6 @@ void setCurrentScreen(uint newScreen) {
case SCREEN_BTC_TICKER: { case SCREEN_BTC_TICKER: {
WorkItem priceUpdate = {TASK_PRICE_UPDATE, 0}; WorkItem priceUpdate = {TASK_PRICE_UPDATE, 0};
xQueueSend(workQueue, &priceUpdate, portMAX_DELAY); xQueueSend(workQueue, &priceUpdate, portMAX_DELAY);
// xTaskNotifyGive(priceUpdateTaskHandle);
break; break;
} }
case SCREEN_BLOCK_FEE_RATE: { case SCREEN_BLOCK_FEE_RATE: {
@ -177,12 +52,28 @@ void setCurrentScreen(uint newScreen) {
} }
break; break;
} }
case SCREEN_MINING_POOL_STATS_HASHRATE:
case SCREEN_MINING_POOL_STATS_EARNINGS: {
if (preferences.getBool("miningPoolStats", DEFAULT_MINING_POOL_STATS_ENABLED)) {
WorkItem miningPoolStatsUpdate = {TASK_MINING_POOL_STATS_UPDATE, 0};
xQueueSend(workQueue, &miningPoolStatsUpdate, portMAX_DELAY);
} else {
setCurrentScreen(SCREEN_BLOCK_HEIGHT);
return;
}
break;
}
} }
if (eventSourceTaskHandle != NULL) xTaskNotifyGive(eventSourceTaskHandle); if (eventSourceTaskHandle != NULL) xTaskNotifyGive(eventSourceTaskHandle);
} }
bool isCurrencySpecific(uint screen) { void ScreenHandler::setCurrentCurrency(char currency) {
currentCurrency = currency;
preferences.putUChar("lastCurrency", currency);
}
bool ScreenHandler::isCurrencySpecific(uint screen) {
switch (screen) { switch (screen) {
case SCREEN_BTC_TICKER: case SCREEN_BTC_TICKER:
case SCREEN_SATS_PER_CURRENCY: case SCREEN_SATS_PER_CURRENCY:
@ -193,98 +84,104 @@ bool isCurrencySpecific(uint screen) {
} }
} }
void nextScreen() { bool ScreenHandler::handleCurrencyRotation(bool forward) {
int currentIndex = findScreenIndexByValue(getCurrentScreen()); if ((getDataSource() == BTCLOCK_SOURCE || getDataSource() == CUSTOM_SOURCE) && isCurrencySpecific(getCurrentScreen())) {
std::vector<ScreenMapping> screenMappings = getScreenNameMap();
if (preferences.getBool("ownDataSource", DEFAULT_OWN_DATA_SOURCE) && isCurrencySpecific(getCurrentScreen())) {
std::vector<std::string> ac = getActiveCurrencies(); std::vector<std::string> ac = getActiveCurrencies();
if (ac.empty()) return false;
std::string curCode = getCurrencyCode(getCurrentCurrency()); std::string curCode = getCurrencyCode(getCurrentCurrency());
if (getCurrencyCode(getCurrentCurrency()) != ac.back()) {
auto it = std::find(ac.begin(), ac.end(), curCode); auto it = std::find(ac.begin(), ac.end(), curCode);
if (it != ac.end()) {
size_t index = std::distance(ac.begin(), it); if (it == ac.end()) {
setCurrentCurrency(getCurrencyChar(ac.at(index+1))); // Current currency not found in active currencies - initialize based on direction
setCurrentCurrency(getCurrencyChar(forward ? ac.front() : ac.back()));
setCurrentScreen(getCurrentScreen()); setCurrentScreen(getCurrentScreen());
return; return true;
} else if (forward && curCode != ac.back()) {
// Moving forward and not at last currency
setCurrentCurrency(getCurrencyChar(ac.at(std::distance(ac.begin(), it) + 1)));
setCurrentScreen(getCurrentScreen());
return true;
} else if (!forward && curCode != ac.front()) {
// Moving backward and not at first currency
setCurrentCurrency(getCurrencyChar(ac.at(std::distance(ac.begin(), it) - 1)));
setCurrentScreen(getCurrentScreen());
return true;
} }
// If we're at the last/first currency of current screen, let nextScreen/previousScreen handle it
return false;
} }
return false;
}
int ScreenHandler::findNextVisibleScreen(int currentScreen, bool forward) {
std::vector<ScreenMapping> screenMappings = getScreenNameMap();
int newScreen;
if (forward) {
newScreen = (currentScreen < screenMappings.size() - 1) ?
screenMappings[currentScreen + 1].value : screenMappings.front().value;
} else {
newScreen = (currentScreen > 0) ?
screenMappings[currentScreen - 1].value : screenMappings.back().value;
}
String key = "screen" + String(newScreen) + "Visible";
while (!preferences.getBool(key.c_str(), true)) {
currentScreen = findScreenIndexByValue(newScreen);
if (forward) {
newScreen = (currentScreen < screenMappings.size() - 1) ?
screenMappings[currentScreen + 1].value : screenMappings.front().value;
} else {
newScreen = (currentScreen > 0) ?
screenMappings[currentScreen - 1].value : screenMappings.back().value;
}
key = "screen" + String(newScreen) + "Visible";
}
return newScreen;
}
void ScreenHandler::nextScreen() {
if (handleCurrencyRotation(true)) return;
int currentIndex = findScreenIndexByValue(getCurrentScreen());
int nextScreen = findNextVisibleScreen(currentIndex, true);
// If moving from a currency-specific screen to another currency-specific screen
// reset to first currency
if (isCurrencySpecific(getCurrentScreen()) && isCurrencySpecific(nextScreen)) {
std::vector<std::string> ac = getActiveCurrencies();
if (!ac.empty()) {
setCurrentCurrency(getCurrencyChar(ac.front())); setCurrentCurrency(getCurrencyChar(ac.front()));
} }
int newCurrentScreen;
if (currentIndex < screenMappings.size() - 1) {
newCurrentScreen = (screenMappings[currentIndex + 1].value);
} else {
newCurrentScreen = screenMappings.front().value;
} }
String key = "screen" + String(newCurrentScreen) + "Visible"; setCurrentScreen(nextScreen);
while (!preferences.getBool(key.c_str(), true)) {
currentIndex = findScreenIndexByValue(newCurrentScreen);
if (currentIndex < screenMappings.size() - 1) {
newCurrentScreen = (screenMappings[currentIndex + 1].value);
} else {
newCurrentScreen = screenMappings.front().value;
} }
key = "screen" + String(newCurrentScreen) + "Visible"; void ScreenHandler::previousScreen() {
} if (handleCurrencyRotation(false)) return;
setCurrentScreen(newCurrentScreen);
}
void previousScreen() {
int currentIndex = findScreenIndexByValue(getCurrentScreen()); int currentIndex = findScreenIndexByValue(getCurrentScreen());
std::vector<ScreenMapping> screenMappings = getScreenNameMap(); int prevScreen = findNextVisibleScreen(currentIndex, false);
if (preferences.getBool("ownDataSource", DEFAULT_OWN_DATA_SOURCE) && isCurrencySpecific(getCurrentScreen())) { // If moving from a currency-specific screen to another currency-specific screen
// reset to last currency
if (isCurrencySpecific(getCurrentScreen()) && isCurrencySpecific(prevScreen)) {
std::vector<std::string> ac = getActiveCurrencies(); std::vector<std::string> ac = getActiveCurrencies();
std::string curCode = getCurrencyCode(getCurrentCurrency()); if (!ac.empty()) {
if (getCurrencyCode(getCurrentCurrency()) != ac.front()) {
auto it = std::find(ac.begin(), ac.end(), curCode);
if (it != ac.end()) {
size_t index = std::distance(ac.begin(), it);
setCurrentCurrency(getCurrencyChar(ac.at(index-1)));
setCurrentScreen(getCurrentScreen());
return;
}
}
setCurrentCurrency(getCurrencyChar(ac.back())); setCurrentCurrency(getCurrencyChar(ac.back()));
}
} }
setCurrentScreen(prevScreen);
int newCurrentScreen;
if (currentIndex > 0) {
newCurrentScreen = screenMappings[currentIndex - 1].value;
} else {
newCurrentScreen = screenMappings.back().value;
} }
String key = "screen" + String(newCurrentScreen) + "Visible"; void ScreenHandler::showSystemStatusScreen() {
while (!preferences.getBool(key.c_str(), true)) {
int currentIndex = findScreenIndexByValue(newCurrentScreen);
if (currentIndex > 0) {
newCurrentScreen = screenMappings[currentIndex - 1].value;
} else {
newCurrentScreen = screenMappings.back().value;
}
key = "screen" + String(newCurrentScreen) + "Visible";
}
setCurrentScreen(newCurrentScreen);
}
void showSystemStatusScreen() {
std::array<String, NUM_SCREENS> sysStatusEpdContent; std::array<String, NUM_SCREENS> sysStatusEpdContent;
std::fill(sysStatusEpdContent.begin(), sysStatusEpdContent.end(), ""); std::fill(sysStatusEpdContent.begin(), sysStatusEpdContent.end(), "");
String ipAddr = WiFi.localIP().toString(); String ipAddr = WiFi.localIP().toString();
String subNet = WiFi.subnetMask().toString(); String subNet = WiFi.subnetMask().toString();
@ -306,14 +203,140 @@ void showSystemStatusScreen() {
String((int)round(ESP.getFreeHeap() / 1024)) + "/" + String((int)round(ESP.getFreeHeap() / 1024)) + "/" +
(int)round(ESP.getHeapSize() / 1024); (int)round(ESP.getHeapSize() / 1024);
setCurrentScreen(SCREEN_CUSTOM); setCurrentScreen(SCREEN_CUSTOM);
setEpdContent(sysStatusEpdContent); EPDManager::getInstance().setContent(sysStatusEpdContent);
} }
void setCurrentCurrency(char currency) { // Keep these as free functions
currentCurrency = currency; void workerTask(void *pvParameters) {
preferences.putUChar("lastCurrency", currency); WorkItem receivedItem;
while (1) {
if (xQueueReceive(workQueue, &receivedItem, portMAX_DELAY)) {
uint currentScreenValue = ScreenHandler::getCurrentScreen();
switch (receivedItem.type) {
case TASK_BITAXE_UPDATE: {
if (currentScreenValue != SCREEN_BITAXE_HASHRATE &&
currentScreenValue != SCREEN_BITAXE_BESTDIFF) break;
taskEpdContent = (currentScreenValue == SCREEN_BITAXE_HASHRATE) ?
parseBitaxeHashRate(BitAxeFetch::getInstance().getHashRate()) :
parseBitaxeBestDiff(BitAxeFetch::getInstance().getBestDiff());
EPDManager::getInstance().setContent(taskEpdContent);
break;
} }
uint getCurrentCurrency() { case TASK_MINING_POOL_STATS_UPDATE: {
return currentCurrency; if (currentScreenValue != SCREEN_MINING_POOL_STATS_HASHRATE &&
currentScreenValue != SCREEN_MINING_POOL_STATS_EARNINGS) break;
taskEpdContent = (currentScreenValue == SCREEN_MINING_POOL_STATS_HASHRATE) ?
parseMiningPoolStatsHashRate(MiningPoolStatsFetch::getInstance().getHashRate(), *MiningPoolStatsFetch::getInstance().getPool()) :
parseMiningPoolStatsDailyEarnings(MiningPoolStatsFetch::getInstance().getDailyEarnings(),
MiningPoolStatsFetch::getInstance().getPool()->getDailyEarningsLabel(),
*MiningPoolStatsFetch::getInstance().getPool());
EPDManager::getInstance().setContent(taskEpdContent);
break;
}
case TASK_PRICE_UPDATE: {
uint currency = ScreenHandler::getCurrentCurrency();
uint price = getPrice(currency);
if (currentScreenValue == SCREEN_BTC_TICKER) {
taskEpdContent = parsePriceData(price, currency, preferences.getBool("suffixPrice", DEFAULT_SUFFIX_PRICE),
preferences.getBool("mowMode", DEFAULT_MOW_MODE),
preferences.getBool("suffixShareDot", DEFAULT_SUFFIX_SHARE_DOT)
);
} else if (currentScreenValue == SCREEN_SATS_PER_CURRENCY) {
taskEpdContent = parseSatsPerCurrency(price, currency, preferences.getBool("useSatsSymbol", DEFAULT_USE_SATS_SYMBOL));
} else {
auto& blockNotify = BlockNotify::getInstance();
taskEpdContent = parseMarketCap(blockNotify.getBlockHeight(), price, currency, preferences.getBool("mcapBigChar", DEFAULT_MCAP_BIG_CHAR));
}
EPDManager::getInstance().setContent(taskEpdContent);
break;
}
case TASK_FEE_UPDATE: {
if (currentScreenValue == SCREEN_BLOCK_FEE_RATE) {
auto& blockNotify = BlockNotify::getInstance();
taskEpdContent = parseBlockFees(static_cast<std::uint16_t>(blockNotify.getBlockMedianFee()));
EPDManager::getInstance().setContent(taskEpdContent);
}
break;
}
case TASK_BLOCK_UPDATE: {
if (currentScreenValue != SCREEN_HALVING_COUNTDOWN) {
auto& blockNotify = BlockNotify::getInstance();
taskEpdContent = parseBlockHeight(blockNotify.getBlockHeight());
} else {
auto& blockNotify = BlockNotify::getInstance();
taskEpdContent = parseHalvingCountdown(blockNotify.getBlockHeight(), preferences.getBool("useBlkCountdown", DEFAULT_USE_BLOCK_COUNTDOWN));
}
if (currentScreenValue == SCREEN_HALVING_COUNTDOWN ||
currentScreenValue == SCREEN_BLOCK_HEIGHT) {
EPDManager::getInstance().setContent(taskEpdContent);
}
break;
}
case TASK_TIME_UPDATE: {
if (currentScreenValue == SCREEN_TIME) {
time_t currentTime;
struct tm timeinfo;
time(&currentTime);
localtime_r(&currentTime, &timeinfo);
std::string timeString;
String minute = String(timeinfo.tm_min);
if (minute.length() < 2) {
minute = "0" + minute;
}
timeString =
std::to_string(timeinfo.tm_hour) + ":" + minute.c_str();
timeString.insert(timeString.begin(),
NUM_SCREENS - timeString.length(), ' ');
taskEpdContent[0] = std::to_string(timeinfo.tm_mday) + "/" +
std::to_string(timeinfo.tm_mon + 1);
for (uint i = 1; i < NUM_SCREENS; i++) {
taskEpdContent[i] = timeString[i];
}
EPDManager::getInstance().setContent(taskEpdContent);
}
break;
}
// Add more cases for additional task types
}
}
}
}
void taskScreenRotate(void *pvParameters) {
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
ScreenHandler::nextScreen();
}
}
void setupTasks() {
workQueue = xQueueCreate(WORK_QUEUE_SIZE, sizeof(WorkItem));
loadStoredPrices();
xTaskCreate(workerTask, "workerTask", 4096, NULL, tskIDLE_PRIORITY,
&workerTaskHandle);
xTaskCreate(taskScreenRotate, "rotateScreen", 4096, NULL, tskIDLE_PRIORITY,
&taskScreenRotateTaskHandle);
if (findScreenIndexByValue(preferences.getUInt("currentScreen", DEFAULT_CURRENT_SCREEN)) != -1)
ScreenHandler::setCurrentScreen(preferences.getUInt("currentScreen", DEFAULT_CURRENT_SCREEN));
}
void cleanup() {
vQueueDelete(workQueue);
// Add any other cleanup needed
} }

View file

@ -6,16 +6,15 @@
#include <data_handler.hpp> #include <data_handler.hpp>
#include <bitaxe_handler.hpp> #include <bitaxe_handler.hpp>
#include "lib/mining_pool/mining_pool_stats_handler.hpp"
#include "lib/epd.hpp" #include "lib/epd.hpp"
#include "lib/shared.hpp" #include "lib/shared.hpp"
// extern TaskHandle_t priceUpdateTaskHandle; #define WORK_QUEUE_SIZE 10
// extern TaskHandle_t blockUpdateTaskHandle;
// extern TaskHandle_t timeUpdateTaskHandle;
extern TaskHandle_t workerTaskHandle; extern TaskHandle_t workerTaskHandle;
extern TaskHandle_t taskScreenRotateTaskHandle; extern TaskHandle_t taskScreenRotateTaskHandle;
extern QueueHandle_t workQueue; extern QueueHandle_t workQueue;
typedef enum { typedef enum {
@ -23,7 +22,8 @@ typedef enum {
TASK_BLOCK_UPDATE, TASK_BLOCK_UPDATE,
TASK_FEE_UPDATE, TASK_FEE_UPDATE,
TASK_TIME_UPDATE, TASK_TIME_UPDATE,
TASK_BITAXE_UPDATE TASK_BITAXE_UPDATE,
TASK_MINING_POOL_STATS_UPDATE
} TaskType; } TaskType;
typedef struct { typedef struct {
@ -31,24 +31,26 @@ typedef struct {
char data; char data;
} WorkItem; } WorkItem;
class ScreenHandler {
private:
static uint currentScreen;
static uint currentCurrency;
public:
static uint getCurrentScreen() { return currentScreen; }
static uint getCurrentCurrency() { return currentCurrency; }
static void setCurrentScreen(uint newScreen);
static void setCurrentCurrency(char currency);
static void nextScreen();
static void previousScreen();
static void showSystemStatusScreen();
static bool isCurrencySpecific(uint screen);
static bool handleCurrencyRotation(bool forward);
static int findNextVisibleScreen(int currentScreen, bool forward);
};
// Keep as free functions since they deal with FreeRTOS tasks
void workerTask(void *pvParameters); void workerTask(void *pvParameters);
uint getCurrentScreen();
void setCurrentScreen(uint newScreen);
void nextScreen();
void previousScreen();
void showSystemStatusScreen();
// void taskPriceUpdate(void *pvParameters);
// void taskBlockUpdate(void *pvParameters);
// void taskTimeUpdate(void *pvParameters);
void taskScreenRotate(void *pvParameters); void taskScreenRotate(void *pvParameters);
void setupTasks(); void setupTasks();
void setCurrentCurrency(char currency); void cleanup();
uint getCurrentCurrency();

View file

@ -1,44 +1,79 @@
#include "shared.hpp" #include "shared.hpp"
const char *github_root_ca = // const char *github_root_ca =
"-----BEGIN CERTIFICATE-----\n" // "-----BEGIN CERTIFICATE-----\n"
"MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL\n" // "MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL\n"
"MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl\n" // "MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl\n"
"eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT\n" // "eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT\n"
"JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx\n" // "JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx\n"
"MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT\n" // "MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT\n"
"Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg\n" // "Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg\n"
"VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm\n" // "VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm\n"
"aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo\n" // "aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo\n"
"I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng\n" // "I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng\n"
"o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G\n" // "o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G\n"
"A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD\n" // "A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD\n"
"VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB\n" // "VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB\n"
"zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW\n" // "zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW\n"
"RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg=\n" // "RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg=\n"
"-----END CERTIFICATE-----\n" // "-----END CERTIFICATE-----\n"
"-----BEGIN CERTIFICATE-----\n" // "-----BEGIN CERTIFICATE-----\n"
"MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\n" // "MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\n"
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" // "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\n" // "d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\n"
"MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\n" // "MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\n"
"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n" // "MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n"
"b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n" // "b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n"
"9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n" // "9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n"
"2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n" // "2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n"
"1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\n" // "1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\n"
"q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\n" // "q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\n"
"tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\n" // "tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\n"
"vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\n" // "vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\n"
"BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\n" // "BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\n"
"5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\n" // "5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\n"
"1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\n" // "1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\n"
"NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\n" // "NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\n"
"Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\n" // "Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\n"
"8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\n" // "8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\n"
"pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\n" // "pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\n"
"MrY=\n" // "MrY=\n"
"-----END CERTIFICATE-----\n"; // "-----END CERTIFICATE-----\n";
// const char* isrg_root_x1cert = R"EOF(
// -----BEGIN CERTIFICATE-----
// MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
// TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
// cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
// WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
// ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
// MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
// h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
// 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
// A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
// T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
// B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
// B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
// KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
// OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
// jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
// qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
// rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
// HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
// hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
// ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
// 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
// NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
// ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
// TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
// jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
// oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
// 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
// mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
// emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
// -----END CERTIFICATE-----
// )EOF";
#ifdef TEST_SCREENS #ifdef TEST_SCREENS
uint8_t input_buffer[3 * input_buffer_pixels]; // up to depth 24 uint8_t input_buffer[3 * input_buffer_pixels]; // up to depth 24
@ -109,3 +144,40 @@ String calculateSHA256(WiFiClient *stream, size_t contentLength) {
return result; return result;
} }
// uint8_t* getOceanIcon() {
// zlib_turbo zt;
// int iUncompSize = zt.gzip_info((uint8_t *)ocean_logo_comp, ocean_logo_size);
// uint8_t *pUncompressed;
// pUncompressed = (uint8_t *)malloc(iUncompSize+4);
// zt.gunzip((uint8_t *)ocean_logo_comp, ocean_logo_size, pUncompressed);
// }
WiFiClientSecure HttpHelper::secureClient;
WiFiClient HttpHelper::insecureClient;
bool HttpHelper::certBundleSet = false;
HTTPClient* HttpHelper::begin(const String& url) {
HTTPClient* http = new HTTPClient();
if (url.startsWith("https://")) {
if (!certBundleSet) {
secureClient.setCACertBundle(rootca_crt_bundle_start);
certBundleSet = true;
}
http->begin(secureClient, url);
} else {
http->begin(insecureClient, url);
}
http->setUserAgent(USER_AGENT);
return http;
}
void HttpHelper::end(HTTPClient* http) {
if (http) {
http->end();
delete http;
}
}

View file

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <Adafruit_MCP23X17.h> #include "MCP23017.h"
// #include <zlib_turbo.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <WiFiClientSecure.h> #include <WiFiClientSecure.h>
#include <Preferences.h> #include <Preferences.h>
@ -9,16 +10,22 @@
#include <GxEPD2.h> #include <GxEPD2.h>
#include <GxEPD2_BW.h> #include <GxEPD2_BW.h>
#include <mbedtls/md.h> #include <mbedtls/md.h>
#include "esp_crt_bundle.h"
#include <Update.h> #include <Update.h>
#include <HTTPClient.h>
#include <mutex> #include <mutex>
#include <utils.hpp> #include <utils.hpp>
#include <array>
#include <string>
#include "defaults.hpp" #include "defaults.hpp"
extern Adafruit_MCP23X17 mcp1; #define USER_AGENT "BTClock/3.0"
extern MCP23017 mcp1;
#ifdef IS_BTCLOCK_V8 #ifdef IS_BTCLOCK_V8
extern Adafruit_MCP23X17 mcp2; extern MCP23017 mcp2;
#endif #endif
extern Preferences preferences; extern Preferences preferences;
extern std::mutex mcpMutex; extern std::mutex mcpMutex;
@ -40,24 +47,16 @@ const PROGMEM int SCREEN_BLOCK_FEE_RATE = 6;
const PROGMEM int SCREEN_SATS_PER_CURRENCY = 10; const PROGMEM int SCREEN_SATS_PER_CURRENCY = 10;
const PROGMEM int SCREEN_BTC_TICKER = 20; const PROGMEM int SCREEN_BTC_TICKER = 20;
// const PROGMEM int SCREEN_BTC_TICKER_USD = 20;
// const PROGMEM int SCREEN_BTC_TICKER_EUR = 21;
// const PROGMEM int SCREEN_BTC_TICKER_GBP = 22;
// const PROGMEM int SCREEN_BTC_TICKER_JPY = 23;
// const PROGMEM int SCREEN_BTC_TICKER_AUD = 24;
// const PROGMEM int SCREEN_BTC_TICKER_CAD = 25;
const PROGMEM int SCREEN_MARKET_CAP = 30; const PROGMEM int SCREEN_MARKET_CAP = 30;
// const PROGMEM int SCREEN_MARKET_CAP_USD = 30;
// const PROGMEM int SCREEN_MARKET_CAP_EUR = 31; const PROGMEM int SCREEN_MINING_POOL_STATS_HASHRATE = 70;
// const PROGMEM int SCREEN_MARKET_CAP_GBP = 32; const PROGMEM int SCREEN_MINING_POOL_STATS_EARNINGS = 71;
// const PROGMEM int SCREEN_MARKET_CAP_JPY = 33;
// const PROGMEM int SCREEN_MARKET_CAP_AUD = 34;
// const PROGMEM int SCREEN_MARKET_CAP_CAD = 35;
const PROGMEM int SCREEN_BITAXE_HASHRATE = 80; const PROGMEM int SCREEN_BITAXE_HASHRATE = 80;
const PROGMEM int SCREEN_BITAXE_BESTDIFF = 81; const PROGMEM int SCREEN_BITAXE_BESTDIFF = 81;
const PROGMEM int SCREEN_COUNTDOWN = 98; const PROGMEM int SCREEN_COUNTDOWN = 98;
const PROGMEM int SCREEN_CUSTOM = 99; const PROGMEM int SCREEN_CUSTOM = 99;
const int SCREEN_COUNT = 7; const int SCREEN_COUNT = 7;
@ -68,7 +67,16 @@ const PROGMEM int screens[SCREEN_COUNT] = {
const int usPerSecond = 1000000; const int usPerSecond = 1000000;
const int usPerMinute = 60 * usPerSecond; const int usPerMinute = 60 * usPerSecond;
extern const char *github_root_ca; // extern const char *github_root_ca;
// extern const char *isrg_root_x1cert;
extern const uint8_t rootca_crt_bundle_start[] asm("_binary_x509_crt_bundle_start");
// extern const uint8_t ocean_logo_comp[] asm("_binary_ocean_gz_start");
// extern const uint8_t ocean_logo_comp_end[] asm("_binary_ocean_gz_end");
// uint8_t* getOceanIcon();
// const size_t ocean_logo_size = ocean_logo_comp_end - ocean_logo_comp;
const PROGMEM char UPDATE_FIRMWARE = U_FLASH; const PROGMEM char UPDATE_FIRMWARE = U_FLASH;
const PROGMEM char UPDATE_WEBUI = U_SPIFFS; const PROGMEM char UPDATE_WEBUI = U_SPIFFS;
@ -81,3 +89,36 @@ struct ScreenMapping {
String calculateSHA256(uint8_t* data, size_t len); String calculateSHA256(uint8_t* data, size_t len);
String calculateSHA256(WiFiClient *stream, size_t contentLength); String calculateSHA256(WiFiClient *stream, size_t contentLength);
namespace ArduinoJson {
template <typename T>
struct Converter<std::vector<T>> {
static void toJson(const std::vector<T>& src, JsonVariant dst) {
JsonArray array = dst.to<JsonArray>();
for (T item : src)
array.add(item);
}
};
template <size_t N>
struct Converter<std::array<String, N>> {
static void toJson(const std::array<String, N>& src, JsonVariant dst) {
JsonArray array = dst.to<JsonArray>();
for (const String& item : src) {
array.add(item);
}
}
};
}
class HttpHelper {
public:
static HTTPClient* begin(const String& url);
static void end(HTTPClient* http);
private:
static WiFiClientSecure secureClient;
static bool certBundleSet;
static WiFiClient insecureClient;
};

View file

@ -1,4 +1,5 @@
#include "timers.hpp" #include "timers.hpp"
#include "led_handler.hpp"
esp_timer_handle_t screenRotateTimer; esp_timer_handle_t screenRotateTimer;
esp_timer_handle_t minuteTimer; esp_timer_handle_t minuteTimer;
@ -49,11 +50,11 @@ void setTimerActive(bool status) {
if (status) { if (status) {
esp_timer_start_periodic(screenRotateTimer, esp_timer_start_periodic(screenRotateTimer,
getTimerSeconds() * usPerSecond); getTimerSeconds() * usPerSecond);
queueLedEffect(LED_EFFECT_START_TIMER); getLedHandler().queueEffect(LED_EFFECT_START_TIMER);
preferences.putBool("timerActive", true); preferences.putBool("timerActive", true);
} else { } else {
esp_timer_stop(screenRotateTimer); esp_timer_stop(screenRotateTimer);
queueLedEffect(LED_EFFECT_PAUSE_TIMER); getLedHandler().queueEffect(LED_EFFECT_PAUSE_TIMER);
preferences.putBool("timerActive", false); preferences.putBool("timerActive", false);
} }
@ -68,8 +69,14 @@ void IRAM_ATTR minuteTimerISR(void *arg) {
WorkItem timeUpdate = {TASK_TIME_UPDATE, 0}; WorkItem timeUpdate = {TASK_TIME_UPDATE, 0};
xQueueSendFromISR(workQueue, &timeUpdate, &xHigherPriorityTaskWoken); xQueueSendFromISR(workQueue, &timeUpdate, &xHigherPriorityTaskWoken);
if (bitaxeFetchTaskHandle != NULL) { TaskHandle_t bitaxeHandle = BitAxeFetch::getInstance().getTaskHandle();
vTaskNotifyGiveFromISR(bitaxeFetchTaskHandle, &xHigherPriorityTaskWoken); if (bitaxeHandle != NULL) {
vTaskNotifyGiveFromISR(bitaxeHandle, &xHigherPriorityTaskWoken);
}
TaskHandle_t miningPoolHandle = MiningPoolStatsFetch::getInstance().getTaskHandle();
if (miningPoolHandle != NULL) {
vTaskNotifyGiveFromISR(miningPoolHandle, &xHigherPriorityTaskWoken);
} }
if (xHigherPriorityTaskWoken == pdTRUE) { if (xHigherPriorityTaskWoken == pdTRUE) {

View file

@ -8,25 +8,36 @@ namespace V2Notify
TaskHandle_t v2NotifyTaskHandle; TaskHandle_t v2NotifyTaskHandle;
String currentHostname;
void setupV2Notify() void setupV2Notify()
{ {
String hostname = "ws.btclock.dev"; String hostname = "ws.btclock.dev";
if (preferences.getBool("stagingSource", DEFAULT_STAGING_SOURCE)) if (getDataSource() == CUSTOM_SOURCE)
{ {
Serial.println(F("Connecting to V2 staging source")); Serial.println(F("Connecting to custom source"));
hostname = "ws-staging.btclock.dev"; hostname = preferences.getString("ceEndpoint", DEFAULT_CUSTOM_ENDPOINT);
bool useSSL = !preferences.getBool("ceDisableSSL", DEFAULT_CUSTOM_ENDPOINT_DISABLE_SSL);
if (useSSL) {
webSocket.beginSSL(hostname, 443, "/api/v2/ws");
} else {
webSocket.begin(hostname, 80, "/api/v2/ws");
}
} }
else else
{ {
Serial.println(F("Connecting to V2 source")); Serial.println(F("Connecting to V2 source"));
webSocket.beginSSL(hostname, 443, "/api/v2/ws");
} }
webSocket.beginSSL(hostname, 443, "/api/v2/ws");
webSocket.onEvent(V2Notify::onWebsocketV2Event); webSocket.onEvent(V2Notify::onWebsocketV2Event);
webSocket.setReconnectInterval(5000); webSocket.setReconnectInterval(5000);
webSocket.enableHeartbeat(15000, 3000, 2); webSocket.enableHeartbeat(15000, 3000, 2);
V2Notify::setupV2NotifyTask(); V2Notify::setupV2NotifyTask();
currentHostname = hostname;
} }
void onWebsocketV2Event(WStype_t type, uint8_t *payload, size_t length) void onWebsocketV2Event(WStype_t type, uint8_t *payload, size_t length)
@ -34,11 +45,14 @@ namespace V2Notify
switch (type) switch (type)
{ {
case WStype_DISCONNECTED: case WStype_DISCONNECTED:
Serial.printf("[WSc] Disconnected!\n"); Serial.print(F("[WSc] Disconnected!\n"));
break; break;
case WStype_CONNECTED: case WStype_CONNECTED:
{ {
Serial.printf("[WSc] Connected to url: %s\n", payload); Serial.print(F("[WSc] Connected to "));
Serial.print(currentHostname);
Serial.print(F(": "));
Serial.println((char *)payload);
JsonDocument response; JsonDocument response;
@ -81,7 +95,8 @@ namespace V2Notify
break; break;
} }
case WStype_TEXT: case WStype_TEXT:
Serial.printf("[WSc] get text: %s\n", payload); Serial.print(F("[WSc] get text: "));
Serial.println((char *)payload);
// send message to server // send message to server
// webSocket.sendTXT("message here"); // webSocket.sendTXT("message here");
@ -91,6 +106,11 @@ namespace V2Notify
JsonDocument doc; JsonDocument doc;
DeserializationError error = deserializeMsgPack(doc, payload, length); DeserializationError error = deserializeMsgPack(doc, payload, length);
if (error) {
Serial.println(F("Error deserializing message"));
break;
}
V2Notify::handleV2Message(doc); V2Notify::handleV2Message(doc);
break; break;
} }
@ -107,24 +127,33 @@ namespace V2Notify
void handleV2Message(JsonDocument doc) void handleV2Message(JsonDocument doc)
{ {
if (doc.containsKey("blockheight")) if (doc["blockheight"].is<uint>())
{ {
uint newBlockHeight = doc["blockheight"].as<uint>(); uint newBlockHeight = doc["blockheight"].as<uint>();
if (newBlockHeight == getBlockHeight()) if (newBlockHeight == BlockNotify::getInstance().getBlockHeight())
{ {
return; return;
} }
processNewBlock(newBlockHeight); if (debugLogEnabled()) {
Serial.print(F("processNewBlock "));
Serial.println(newBlockHeight);
} }
else if (doc.containsKey("blockfee")) BlockNotify::getInstance().processNewBlock(newBlockHeight);
}
else if (doc["blockfee"].is<uint>())
{ {
uint medianFee = doc["blockfee"].as<uint>(); uint medianFee = doc["blockfee"].as<uint>();
processNewBlockFee(medianFee); if (debugLogEnabled()) {
Serial.print(F("processNewBlockFee "));
Serial.println(medianFee);
} }
else if (doc.containsKey("price"))
BlockNotify::getInstance().processNewBlockFee(medianFee);
}
else if (doc["price"].is<JsonObject>())
{ {
// Iterate through the key-value pairs of the "price" object // Iterate through the key-value pairs of the "price" object
@ -143,7 +172,7 @@ namespace V2Notify
for (;;) for (;;)
{ {
webSocket.loop(); webSocket.loop();
vTaskDelay(10 / portTICK_PERIOD_MS); vTaskDelay(pdMS_TO_TICKS(10));
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,7 @@
#include "lib/price_notify.hpp" #include "lib/price_notify.hpp"
#include "lib/screen_handler.hpp" #include "lib/screen_handler.hpp"
#include "webserver/OneParamRewrite.hpp" #include "webserver/OneParamRewrite.hpp"
#include "lib/mining_pool/pool_factory.hpp"
extern TaskHandle_t eventSourceTaskHandle; extern TaskHandle_t eventSourceTaskHandle;
@ -27,8 +28,7 @@ void onApiStatus(AsyncWebServerRequest *request);
void onApiSystemStatus(AsyncWebServerRequest *request); void onApiSystemStatus(AsyncWebServerRequest *request);
void onApiSetWifiTxPower(AsyncWebServerRequest *request); void onApiSetWifiTxPower(AsyncWebServerRequest *request);
void onApiScreenNext(AsyncWebServerRequest *request); void onApiScreenControl(AsyncWebServerRequest *request);
void onApiScreenPrevious(AsyncWebServerRequest *request);
void onApiShowScreen(AsyncWebServerRequest *request); void onApiShowScreen(AsyncWebServerRequest *request);
void onApiShowCurrency(AsyncWebServerRequest *request); void onApiShowCurrency(AsyncWebServerRequest *request);
@ -67,6 +67,10 @@ void eventSourceTask(void *pvParameters);
void onApiStopDataSources(AsyncWebServerRequest *request); void onApiStopDataSources(AsyncWebServerRequest *request);
void onApiRestartDataSources(AsyncWebServerRequest *request); void onApiRestartDataSources(AsyncWebServerRequest *request);
void onApiDNDStatus(AsyncWebServerRequest *request);
void onApiDNDEnable(AsyncWebServerRequest *request);
void onApiDNDDisable(AsyncWebServerRequest *request);
#ifdef HAS_FRONTLIGHT #ifdef HAS_FRONTLIGHT
void onApiFrontlightOn(AsyncWebServerRequest *request); void onApiFrontlightOn(AsyncWebServerRequest *request);
void onApiFrontlightFlash(AsyncWebServerRequest *request); void onApiFrontlightFlash(AsyncWebServerRequest *request);

View file

@ -18,156 +18,146 @@
#define WEBSERVER_H #define WEBSERVER_H
#include "ESPAsyncWebServer.h" #include "ESPAsyncWebServer.h"
#include "lib/config.hpp" #include "lib/config.hpp"
#include "lib/led_handler.hpp"
#include "lib/block_notify.hpp"
uint wifiLostConnection; uint wifiLostConnection;
uint priceNotifyLostConnection = 0; uint priceNotifyLostConnection = 0;
uint blockNotifyLostConnection = 0; uint blockNotifyLostConnection = 0;
// char ptrTaskList[1500];
extern "C" void app_main() int64_t getUptime() {
{ return esp_timer_get_time() / 1000000;
initArduino(); }
Serial.begin(115200); void handlePriceNotifyDisconnection() {
setup(); if (priceNotifyLostConnection == 0) {
priceNotifyLostConnection = getUptime();
Serial.println(F("Lost price notification connection, trying to reconnect..."));
}
while (true) if ((getUptime() - priceNotifyLostConnection) > 300) { // 5 minutes timeout
{ Serial.println(F("Price notification connection lost for 5 minutes, restarting handler..."));
// vTaskList(ptrTaskList); restartPriceNotify();
// Serial.println(F("**********************************")); priceNotifyLostConnection = 0;
// Serial.println(F("Task State Prio Stack Num")); }
// Serial.println(F("**********************************")); }
// Serial.print(ptrTaskList);
// Serial.println(F("**********************************"));
if (eventSourceTaskHandle != NULL)
xTaskNotifyGive(eventSourceTaskHandle);
int64_t currentUptime = esp_timer_get_time() / 1000000; void handleBlockNotifyDisconnection() {
; if (blockNotifyLostConnection == 0) {
blockNotifyLostConnection = getUptime();
Serial.println(F("Lost block notification connection, trying to reconnect..."));
}
if (!getIsOTAUpdating()) if ((getUptime() - blockNotifyLostConnection) > 300) { // 5 minutes timeout
{ Serial.println(F("Block notification connection lost for 5 minutes, restarting handler..."));
auto& blockNotify = BlockNotify::getInstance();
blockNotify.restart();
blockNotifyLostConnection = 0;
}
}
void handleFrontlight() {
#ifdef HAS_FRONTLIGHT #ifdef HAS_FRONTLIGHT
if (hasLightLevel()) { if (hasLightLevel() && preferences.getUInt("luxLightToggle", DEFAULT_LUX_LIGHT_TOGGLE) != 0) {
if (preferences.getUInt("luxLightToggle", DEFAULT_LUX_LIGHT_TOGGLE) != 0) uint lightLevel = getLightLevel();
{ uint luxThreshold = preferences.getUInt("luxLightToggle", DEFAULT_LUX_LIGHT_TOGGLE);
if (hasLightLevel() && getLightLevel() <= 2) auto& ledHandler = getLedHandler();
{
if (frontlightIsOn()) { if (lightLevel <= 1 && preferences.getBool("flOffWhenDark", DEFAULT_FL_OFF_WHEN_DARK)) {
frontlightFadeOutAll(); if (ledHandler.frontlightIsOn()) ledHandler.frontlightFadeOutAll();
} } else if (lightLevel < luxThreshold && !ledHandler.frontlightIsOn()) {
} ledHandler.frontlightFadeInAll();
else if (hasLightLevel() && getLightLevel() < preferences.getUInt("luxLightToggle", DEFAULT_LUX_LIGHT_TOGGLE) && !frontlightIsOn()) } else if (ledHandler.frontlightIsOn() && lightLevel > luxThreshold) {
{ ledHandler.frontlightFadeOutAll();
frontlightFadeInAll();
}
else if (frontlightIsOn() && getLightLevel() > preferences.getUInt("luxLightToggle", DEFAULT_LUX_LIGHT_TOGGLE))
{
frontlightFadeOutAll();
}
} }
} }
#endif #endif
if (!WiFi.isConnected())
{
if (!wifiLostConnection)
{
wifiLostConnection = currentUptime;
Serial.println(F("Lost WiFi connection, trying to reconnect..."));
} }
if ((currentUptime - wifiLostConnection) > 600) void checkWiFiConnection() {
{ if (!WiFi.isConnected()) {
if (!wifiLostConnection) {
wifiLostConnection = getUptime();
Serial.println(F("Lost WiFi connection, trying to reconnect..."));
}
if ((getUptime() - wifiLostConnection) > 600) {
Serial.println(F("Still no connection after 10 minutes, restarting...")); Serial.println(F("Still no connection after 10 minutes, restarting..."));
delay(2000); delay(2000);
ESP.restart(); ESP.restart();
} }
WiFi.begin(); WiFi.begin();
} } else if (wifiLostConnection) {
else if (wifiLostConnection)
{
wifiLostConnection = 0; wifiLostConnection = 0;
Serial.println(F("Connection restored, reset timer.")); Serial.println(F("Connection restored, reset timer."));
} }
if (getPriceNotifyInit() && !preferences.getBool("fetchEurPrice", DEFAULT_FETCH_EUR_PRICE) && !isPriceNotifyConnected())
{
priceNotifyLostConnection++;
Serial.println(F("Lost price data connection..."));
queueLedEffect(LED_DATA_PRICE_ERROR);
// if price WS connection does not come back after 6*5 seconds, destroy and recreate
if (priceNotifyLostConnection > 6)
{
Serial.println(F("Restarting price handler..."));
restartPriceNotify();
// setupPriceNotify();
priceNotifyLostConnection = 0;
}
}
else if (priceNotifyLostConnection > 0 && isPriceNotifyConnected())
{
priceNotifyLostConnection = 0;
} }
if (getBlockNotifyInit() && !isBlockNotifyConnected()) void checkMissedBlocks() {
{
blockNotifyLostConnection++;
Serial.println(F("Lost block data connection..."));
queueLedEffect(LED_DATA_BLOCK_ERROR);
// if mempool WS connection does not come back after 6*5 seconds, destroy and recreate
if (blockNotifyLostConnection > 6)
{
Serial.println(F("Restarting block handler..."));
restartBlockNotify();
// setupBlockNotify();
blockNotifyLostConnection = 0;
}
}
else if (blockNotifyLostConnection > 0 && isBlockNotifyConnected())
{
blockNotifyLostConnection = 0;
}
// if more than 5 price updates are missed, there is probably something wrong, reconnect
if ((getLastPriceUpdate(CURRENCY_USD) - currentUptime) > (preferences.getUInt("minSecPriceUpd", DEFAULT_SECONDS_BETWEEN_PRICE_UPDATE) * 5))
{
Serial.println(F("Detected 5 missed price updates... restarting price handler."));
restartPriceNotify();
// setupPriceNotify();
priceNotifyLostConnection = 0;
}
// If after 45 minutes no mempool blocks, check the rest API
if ((getLastBlockUpdate() - currentUptime) > 45 * 60)
{
Serial.println(F("Long time (45 min) since last block, checking if I missed anything...")); Serial.println(F("Long time (45 min) since last block, checking if I missed anything..."));
int currentBlock = getBlockFetch(); auto& blockNotify = BlockNotify::getInstance();
if (currentBlock != -1) int currentBlock = blockNotify.fetchLatestBlock();
{ if (currentBlock != -1) {
if (currentBlock != getBlockHeight()) if (currentBlock != blockNotify.getBlockHeight()) {
{
Serial.println(F("Detected stuck block height... restarting block handler.")); Serial.println(F("Detected stuck block height... restarting block handler."));
// Mempool source stuck, restart blockNotify.restart();
restartBlockNotify();
// setupBlockNotify();
} }
// set last block update so it doesn't fetch for 45 minutes blockNotify.setLastBlockUpdate(getUptime());
setLastBlockUpdate(currentUptime);
} }
} }
if (currentUptime - getLastTimeSync() > 24 * 60 * 60) void monitorDataConnections() {
{ // Price notification monitoring
if (getPriceNotifyInit() && !preferences.getBool("fetchEurPrice", DEFAULT_FETCH_EUR_PRICE) && !isPriceNotifyConnected()) {
handlePriceNotifyDisconnection();
} else if (priceNotifyLostConnection > 0 && isPriceNotifyConnected()) {
priceNotifyLostConnection = 0;
}
// Block notification monitoring
auto& blockNotify = BlockNotify::getInstance();
if (blockNotify.isInitialized() && !blockNotify.isConnected()) {
handleBlockNotifyDisconnection();
} else if (blockNotifyLostConnection > 0 && blockNotify.isConnected()) {
blockNotifyLostConnection = 0;
}
// Check for missed price updates
if ((getLastPriceUpdate(CURRENCY_USD) - getUptime()) > (preferences.getUInt("minSecPriceUpd", DEFAULT_SECONDS_BETWEEN_PRICE_UPDATE) * 5)) {
Serial.println(F("Detected 5 missed price updates... restarting price handler."));
restartPriceNotify();
priceNotifyLostConnection = 0;
}
// Check for missed blocks
if ((blockNotify.getLastBlockUpdate() - getUptime()) > 45 * 60) {
checkMissedBlocks();
}
}
extern "C" void app_main() {
initArduino();
Serial.begin(115200);
setup();
bool thirdPartySource = getDataSource() == THIRD_PARTY_SOURCE;
while (true) {
if (eventSourceTaskHandle != NULL) {
xTaskNotifyGive(eventSourceTaskHandle);
}
if (!getIsOTAUpdating()) {
handleFrontlight();
checkWiFiConnection();
if (thirdPartySource) {
monitorDataConnections();
}
if (getUptime() - getLastTimeSync() > 24 * 60 * 60) {
Serial.println(F("Last time update is longer than 24 hours ago, sync again")); Serial.println(F("Last time update is longer than 24 hours ago, sync again"));
syncTime(); syncTime();
}; }
} }
vTaskDelay(pdMS_TO_TICKS(5000)); vTaskDelay(pdMS_TO_TICKS(5000));

View file

@ -0,0 +1,75 @@
#include <bitaxe_handler.hpp>
#include <unity.h>
template<size_t N>
std::string joinArrayWithBrackets(const std::array<std::string, N>& arr, const std::string& separator = " ") {
std::ostringstream result;
for (size_t i = 0; i < N; ++i) {
if (i > 0) {
result << separator;
}
result << '[' << arr[i] << ']';
}
return result.str();
}
void setUp(void)
{
// set stuff up here
}
void tearDown(void)
{
// clean stuff up here
}
void test_BitaxeParseHashrate(void)
{
std::array<std::string, NUM_SCREENS> output = parseBitaxeHashRate(656130000000);
std::string joined = joinArrayWithBrackets(output);
TEST_ASSERT_EQUAL_STRING_MESSAGE("mdi:bitaxe", output[0].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("6", output[NUM_SCREENS - 4].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("5", output[NUM_SCREENS - 3].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("6", output[NUM_SCREENS - 2].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("GH/S", output[NUM_SCREENS - 1].c_str(), joined.c_str());
}
void test_BitaxeParseBestDiff(void)
{
std::array<std::string, NUM_SCREENS> output = parseBitaxeBestDiff(15800000000);
std::string joined = joinArrayWithBrackets(output);
TEST_ASSERT_EQUAL_STRING_MESSAGE("mdi:bitaxe", output[0].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("1", output[NUM_SCREENS - 5].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("5", output[NUM_SCREENS - 4].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE(".", output[NUM_SCREENS - 3].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("8", output[NUM_SCREENS - 2].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("G", output[NUM_SCREENS - 1].c_str(), joined.c_str());
}
// not needed when using generate_test_runner.rb
int runUnityTests(void)
{
UNITY_BEGIN();
RUN_TEST(test_BitaxeParseHashrate);
RUN_TEST(test_BitaxeParseBestDiff);
return UNITY_END();
}
int main(void)
{
return runUnityTests();
}
extern "C" void app_main()
{
runUnityTests();
}

View file

@ -33,6 +33,17 @@ void test_CorrectSatsPerDollarConversion(void)
TEST_ASSERT_EQUAL_STRING("4", output[NUM_SCREENS - 1].c_str()); TEST_ASSERT_EQUAL_STRING("4", output[NUM_SCREENS - 1].c_str());
} }
void test_SatsPerDollarAfter1B(void)
{
std::array<std::string, NUM_SCREENS> output = parseSatsPerCurrency(120000000, CURRENCY_USD, false);
TEST_ASSERT_EQUAL_STRING("SATS/USD", output[0].c_str());
TEST_ASSERT_EQUAL_STRING("0", output[NUM_SCREENS - 5].c_str());
TEST_ASSERT_EQUAL_STRING(".", output[NUM_SCREENS - 4].c_str());
TEST_ASSERT_EQUAL_STRING("8", output[NUM_SCREENS - 3].c_str());
TEST_ASSERT_EQUAL_STRING("3", output[NUM_SCREENS - 2].c_str());
TEST_ASSERT_EQUAL_STRING("3", output[NUM_SCREENS - 1].c_str());
}
void test_CorrectSatsPerPoundConversion(void) void test_CorrectSatsPerPoundConversion(void)
{ {
std::array<std::string, NUM_SCREENS> output = parseSatsPerCurrency(37253, CURRENCY_GBP, false); std::array<std::string, NUM_SCREENS> output = parseSatsPerCurrency(37253, CURRENCY_GBP, false);
@ -86,6 +97,81 @@ void test_PriceOf1MillionUsd(void)
TEST_ASSERT_EQUAL_STRING("M", output[NUM_SCREENS - 1].c_str()); TEST_ASSERT_EQUAL_STRING("M", output[NUM_SCREENS - 1].c_str());
} }
void test_PriceSuffixMode(void)
{
std::array<std::string, NUM_SCREENS> output = parsePriceData(93000, '$', true, false);
TEST_ASSERT_EQUAL_STRING("BTC/USD", output[0].c_str());
TEST_ASSERT_EQUAL_STRING("9", output[NUM_SCREENS - 5].c_str());
TEST_ASSERT_EQUAL_STRING("3", output[NUM_SCREENS - 4].c_str());
TEST_ASSERT_EQUAL_STRING(".", output[NUM_SCREENS - 3].c_str());
TEST_ASSERT_EQUAL_STRING("0", output[NUM_SCREENS - 2].c_str());
TEST_ASSERT_EQUAL_STRING("K", output[NUM_SCREENS - 1].c_str());
}
void test_PriceSuffixModeCompact1(void)
{
std::array<std::string, NUM_SCREENS> output = parsePriceData(100000, '$', true, false, true);
std::string joined = joinArrayWithBrackets(output);
TEST_ASSERT_EQUAL_STRING_MESSAGE("BTC/USD", output[0].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("$", output[NUM_SCREENS - 6].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("1", output[NUM_SCREENS - 5].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("0", output[NUM_SCREENS - 4].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("0.", output[NUM_SCREENS - 3].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("0", output[NUM_SCREENS - 2].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("K", output[NUM_SCREENS - 1].c_str(), joined.c_str());
}
void test_PriceSuffixModeCompact2(void)
{
std::array<std::string, NUM_SCREENS> output = parsePriceData(1000000, '$', true, false, true);
std::string joined = joinArrayWithBrackets(output);
TEST_ASSERT_EQUAL_STRING_MESSAGE("BTC/USD", output[0].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("$", output[NUM_SCREENS - 6].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("1.", output[NUM_SCREENS - 5].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("0", output[NUM_SCREENS - 4].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("0", output[NUM_SCREENS - 3].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("0", output[NUM_SCREENS - 2].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("M", output[NUM_SCREENS - 1].c_str(), joined.c_str());
}
void test_PriceSuffixModeMow(void)
{
std::array<std::string, NUM_SCREENS> output = parsePriceData(93600, '$', true, true);
std::string joined = joinArrayWithBrackets(output);
TEST_ASSERT_EQUAL_STRING_MESSAGE("$", output[0].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE(".", output[NUM_SCREENS - 5].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("0", output[NUM_SCREENS - 4].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("9", output[NUM_SCREENS - 3].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("3", output[NUM_SCREENS - 2].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("M", output[NUM_SCREENS - 1].c_str(), joined.c_str());
}
void test_PriceSuffixModeMowCompact(void)
{
std::array<std::string, NUM_SCREENS> output = parsePriceData(93600, '$', true, true, true);
std::string joined = joinArrayWithBrackets(output);
TEST_ASSERT_EQUAL_STRING_MESSAGE("MOW/UNITS", output[0].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("$", output[NUM_SCREENS - 6].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("0.", output[NUM_SCREENS - 5].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("0", output[NUM_SCREENS - 4].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("9", output[NUM_SCREENS - 3].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("3", output[NUM_SCREENS - 2].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("M", output[NUM_SCREENS - 1].c_str(), joined.c_str());
}
void test_McapLowerUsd(void) void test_McapLowerUsd(void)
{ {
std::array<std::string, NUM_SCREENS> output = parseMarketCap(810000, 26000, '$', true); std::array<std::string, NUM_SCREENS> output = parseMarketCap(810000, 26000, '$', true);
@ -192,6 +278,7 @@ int runUnityTests(void)
UNITY_BEGIN(); UNITY_BEGIN();
RUN_TEST(test_CorrectSatsPerDollarConversion); RUN_TEST(test_CorrectSatsPerDollarConversion);
RUN_TEST(test_CorrectSatsPerPoundConversion); RUN_TEST(test_CorrectSatsPerPoundConversion);
RUN_TEST(test_SatsPerDollarAfter1B);
RUN_TEST(test_SixCharacterBlockHeight); RUN_TEST(test_SixCharacterBlockHeight);
RUN_TEST(test_SevenCharacterBlockHeight); RUN_TEST(test_SevenCharacterBlockHeight);
RUN_TEST(test_FeeRateDisplay); RUN_TEST(test_FeeRateDisplay);
@ -203,6 +290,11 @@ int runUnityTests(void)
RUN_TEST(test_Mcap1TrillionEurSmallChars); RUN_TEST(test_Mcap1TrillionEurSmallChars);
RUN_TEST(test_Mcap1TrillionJpy); RUN_TEST(test_Mcap1TrillionJpy);
RUN_TEST(test_Mcap1TrillionJpySmallChars); RUN_TEST(test_Mcap1TrillionJpySmallChars);
RUN_TEST(test_PriceSuffixMode);
RUN_TEST(test_PriceSuffixModeCompact1);
RUN_TEST(test_PriceSuffixModeCompact2);
RUN_TEST(test_PriceSuffixModeMow);
RUN_TEST(test_PriceSuffixModeMowCompact);
return UNITY_END(); return UNITY_END();
} }

View file

@ -0,0 +1,75 @@
#include <utils.hpp>
#include <unity.h>
void test_parseMiningPoolStatsHashRate1dot34TH(void)
{
std::string hashrate;
std::string label;
std::string output;
parseHashrateString("1340000000000", label, output, 4);
TEST_ASSERT_EQUAL_STRING_MESSAGE("TH/S", label.c_str(), label.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("1.34", output.c_str(), output.c_str());
}
void test_parseMiningPoolStatsHashRate645GH(void)
{
std::string hashrate = "645000000000";
std::string label;
std::string output;
parseHashrateString(hashrate, label, output, 4);
TEST_ASSERT_EQUAL_STRING_MESSAGE("GH/S", label.c_str(), label.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("645", output.c_str(), output.c_str());
}
void test_parseMiningPoolStatsHashRateEmpty(void)
{
std::string hashrate = "";
std::string label;
std::string output;
parseHashrateString(hashrate, label, output, 4);
TEST_ASSERT_EQUAL_STRING_MESSAGE("H/S", label.c_str(), label.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("0", output.c_str(), output.c_str());
}
void test_parseMiningPoolStatsHashRateZero(void)
{
std::string hashrate = "0";
std::string label;
std::string output;
parseHashrateString(hashrate, label, output, 4);
TEST_ASSERT_EQUAL_STRING_MESSAGE("H/S", label.c_str(), label.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("0", output.c_str(), output.c_str());
}
// not needed when using generate_test_runner.rb
int runUnityTests(void)
{
UNITY_BEGIN();
RUN_TEST(test_parseMiningPoolStatsHashRate1dot34TH);
RUN_TEST(test_parseMiningPoolStatsHashRate645GH);
RUN_TEST(test_parseMiningPoolStatsHashRateZero);
RUN_TEST(test_parseMiningPoolStatsHashRateEmpty);
return UNITY_END();
}
int main(void)
{
return runUnityTests();
}
extern "C" void app_main()
{
runUnityTests();
}

BIN
x509_crt_bundle Normal file

Binary file not shown.