1
0
Fork 0
forked from btclock/btclock_v3

Compare commits

...

157 commits
PCB2 ... main

Author SHA1 Message Date
Djuri Baars
73a20cf9a7 Screen handler bugfix for multi currency 2024-12-29 17:11:34 +01:00
Djuri Baars
833d46fa5a Improve WebUI feedback 2024-12-29 01:31:33 +01:00
Djuri Baars
64e518bf58 Improve data source selection, clean up unused preferences 2024-12-28 18:19:31 +01:00
Djuri Baars
bc3e5afe51 Add debug log setting and custom endpoint settings 2024-12-28 16:29:34 +01:00
Djuri Baars
190d650887 Merge branch 'main' of https://git.btclock.dev/btclock/btclock_v3 2024-12-28 13:42:13 +01:00
Djuri Baars
b7ff9d8101 Better handling of unexpected pool stats responses, add CKPool 2024-12-28 13:40:08 +01:00
832d343db9 Merge pull request 'Configure Renovate' () from renovate/configure into main
Reviewed-on: 
2024-12-27 09:04:50 +00:00
Ticktock Depbot
4140b05a7d Add renovate.json 2024-12-27 08:28:10 +00:00
Djuri Baars
10fe5b5053 Fix function definition and rev.b definition 2024-12-27 00:41:41 +01:00
Djuri Baars
cff6131fc4 Update WebUI and convert ButtonHandler to class 2024-12-27 00:05:45 +01:00
Djuri Baars
17fef80253 Make ScreenHandler class, fix button handling 2024-12-26 23:08:46 +01:00
Djuri Baars
698c3a3a43 More code optimizations, remove unnecessary checks 2024-12-26 22:20:30 +01:00
Djuri Baars
66c662e1fd Use own nostrduino fork, improve nostr code, WebUI update 2024-12-26 21:43:50 +01:00
Djuri Baars
957a947bc5 More clean-up and bugfixes 2024-12-21 17:53:35 +01:00
Djuri Baars
c44626cb42 Unclutter main.cpp 2024-12-21 17:46:10 +01:00
Djuri Baars
4fdd6b6b4f Fix button handling 2024-12-21 17:36:07 +01:00
Djuri Baars
8a818c66a0 Update readme 2024-12-21 01:15:59 +01:00
Djuri Baars
9889e983ec Update WebUI 2024-12-21 00:51:26 +01:00
Djuri Baars
03dbb8add6 Small improvements for epd 2024-12-21 00:50:57 +01:00
Djuri Baars
753838b122 Switch to new ArduinoJson methods 2024-12-21 00:15:08 +01:00
Djuri Baars
46c0f3a22b Improve webserver functionality 2024-12-21 00:01:27 +01:00
Djuri Baars
fb70d435a9 Get mining pool logos by download 2024-12-20 23:02:54 +01:00
Djuri Baars
7bcb24bab0 Mining pool bugfixes, improvements and icon optimizations 2024-12-20 14:11:26 +01:00
Djuri Baars
e8a7b221cb Update WebUI for new mining pools 2024-12-20 04:07:46 +01:00
Djuri Baars
aeee5238b3 Improve mining pool interface, added GoBrrr pool, Public Pool and Satoshi Radio pool 2024-12-20 04:00:09 +01:00
Djuri Baars
f613c7e9a1 Update WebUI 2024-12-20 01:33:10 +01:00
c7ea2f3e4d Merge pull request '[Feature] Adds mining pool screens' () from kdmukai/btclock_v3:mining_stats_v2 into main
Reviewed-on: 
2024-12-20 00:11:01 +00:00
Djuri Baars
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
Djuri Baars
ae2e6656df Fix LittleFS sha256 generation 2024-12-18 22:52:50 +01:00
Djuri Baars
c989169ff4 Fix workflow and auto updater webui filenames 2024-12-18 22:20:40 +01:00
Djuri Baars
1a4bc9b711 Merge branch 'main' of https://git.btclock.dev/btclock/btclock_v3 2024-12-18 21:40:53 +01:00
Djuri Baars
0dcde59fb4 Fix workflow 2024-12-18 21:40:18 +01:00
Djuri Baars
de8fe2e26e Fix preaction script 2024-12-18 20:41:42 +01:00
Djuri Baars
af4c466659 Switch to leaner MCP23017 library, create new aligned partition tables 2024-12-18 19:47:03 +01:00
Djuri Baars
83d293c58e Add bitaxe logo to WebUI 2024-12-18 01:32:11 +01:00
Djuri Baars
da25c7de90 Merge remote-tracking branch 'keith/bitaxe_logo' 2024-12-18 01:27:05 +01:00
Djuri Baars
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
Djuri Baars
34b77ea105 Update WebUI 2024-12-12 23:12:59 +01:00
Djuri Baars
dbf2c53083 Adapted tests for Mow Units 2024-12-10 15:18:04 +01:00
Djuri Baars
2a116d97ed Add frontlight off when dark setting 2024-12-10 15:13:17 +01:00
3b6f1db3c5 Merge pull request 'Mow Units: No rounding' () from kdmukai/btclock_v3:mow_units_no_rounding into main
Reviewed-on: 
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
Djuri Baars
d6604d28d6 Update some initial values 2024-12-05 18:28:46 +01:00
Djuri Baars
33c06c86a1 Prepare sats per dollar for 1B amounts 2024-12-05 18:22:59 +01:00
Djuri Baars
f0f591a16f Make better use of the screens in compact suffix mode 2024-12-05 05:21:14 +01:00
Djuri Baars
41b5fcf1c1 Bugfix for suffix compact mode 2024-12-05 04:32:32 +01:00
Djuri Baars
981895d315 Convert partition table to hex, update WebUI 2024-11-29 01:22:07 +01:00
Djuri Baars
031b506fed Fix partition tables for bigger flash sizes 2024-11-28 19:21:15 +01:00
Djuri Baars
4cda081d05 Add suffix compact mode, added extra zap notify settings, WebUI cleanup 2024-11-28 18:22:07 +01:00
Djuri Baars
2951055f68 Add requirements.txt for CI 2024-11-27 11:41:58 +01:00
Djuri Baars
d37307cccf Add Mow mode notation and setting 2024-11-27 11:33:12 +01:00
Djuri Baars
239297c26d Add own badges to README 2024-11-26 21:48:28 +01:00
Djuri Baars
3b47c81cfe Webserver dependency update and testing new CI flow 2024-11-26 20:50:32 +01:00
Djuri Baars
9f3351f85b Add Forgejo CI action
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
Djuri Baars
1ccd5f18fb Update WebUI 2024-11-26 00:03:07 +01:00
Djuri Baars
82dd70a38d Use CA bundle instead of single certificates, make auto update URL configurable 2024-11-25 23:54:02 +01:00
Djuri Baars
a614bd15db Restore Nostr Zap subscription after loss of connection 2024-10-23 00:43:53 +02:00
Djuri Baars
b0ec0685a1 Bugfix for too short WiFi-setup passwords 2024-10-03 00:26:14 +02:00
Djuri Baars
85579e98cf Bugfixes for stack size and market cap with small chars, write tests for the small char mcap 2024-09-28 00:03:18 +02:00
Djuri Baars
e8a9e253f7 Fix matrix expansion 2024-09-21 18:40:44 +02:00
Djuri Baars
8b72f2f6b3 Fix merged binary for v8 2024-09-21 18:25:13 +02:00
Djuri Baars
ff0d8f5a0a Make BTClock v8 board working again 2024-09-21 15:58:07 +02:00
Djuri Baars
41bf2480ce Dependency updates and fix URL rewrites 2024-09-18 02:40:24 +02:00
Djuri Baars
ff50acf913 Fix WebUI currency converter, reorganize some code 2024-09-18 02:00:10 +02:00
Djuri Baars
5dd47c2275 Fixes for Rev. B black PCB 2024-09-16 21:50:28 +02:00
Djuri Baars
630943ec54 Bring back snprintf for ledstate 2024-09-12 02:22:29 +02:00
Djuri Baars
18bac7dcc7 Remove old settings from sdkconfig and add optimizations 2024-09-12 02:18:44 +02:00
Djuri Baars
023ff29131 Add single-click auto-update functionality 2024-09-11 20:27:40 +02:00
Djuri Baars
d00c216126 GitHub Workflow addition 2024-09-11 18:56:06 +02:00
Djuri Baars
283469dc4c Fix github action 2024-09-11 18:07:34 +02:00
Djuri Baars
5d5b09f56c Prepare for automatic OTA updates 2024-09-11 17:40:44 +02:00
Djuri Baars
1f2110fc5a Make zap notify more lightning like, verify SSL certificates, remove price fetch code 2024-09-11 03:23:41 +02:00
Djuri Baars
5425ea7fbf Improve setup-WiFi password generation 2024-09-10 11:12:14 +02:00
Djuri Baars
7a1ce54248 Fix WebUI credentials not being saved 2024-09-10 10:21:02 +02:00
Djuri Baars
f42cd250fe Add some tests for multi-currency 2024-09-09 16:52:23 +02:00
Djuri Baars
849e5ce439 Finish V2 MsgPack WebSocket API implementation 2024-09-09 15:13:41 +02:00
Djuri Baars
c276d32807 Implement multi-currency support with MsgPack 2024-09-05 14:00:15 +02:00
Djuri Baars
a4ff5a2f75 Add WebUI authentication 2024-09-03 01:36:44 +02:00
Djuri Baars
00ac808731 Fix GitHub action 2024-09-02 23:28:09 +02:00
Djuri Baars
6cf464c3e3 Dependency upgrade and cleanup 2024-09-02 23:04:23 +02:00
Djuri Baars
a31a42511f Fix disabled screen skipping 2024-09-02 22:44:23 +02:00
Djuri Baars
478c951ffb Improved multi-currency support, improved WebUI checks 2024-08-31 23:11:48 +02:00
Djuri Baars
9c67f769d3 WebUI improvements for small screens 2024-08-31 18:06:42 +02:00
Djuri Baars
65496fbb29 WebUI improvements 2024-08-31 17:22:30 +02:00
Djuri Baars
2777637355 Improve Nostr Zap functionality, BitAxe integration bugfix 2024-08-31 15:45:19 +02:00
Djuri Baars
e39a0ccc14 Add zap notify functionality 2024-08-24 16:27:55 +03:00
Djuri Baars
99e622eeef Lock dependency 2024-08-19 01:53:16 +02:00
Djuri Baars
0a08c5f9ea Begin support for other currencies 2024-08-15 00:37:50 +02:00
Djuri Baars
b13c7242a6 Added BitAxe support 2024-07-29 20:49:46 +02:00
Djuri Baars
ca1c7178f1 Dependency upgrade 2024-07-15 16:49:21 +02:00
Djuri Baars
19559727c9 Add Nostr Relay connection status 2024-07-15 16:36:51 +02:00
Djuri Baars
60593de785 Nostr library update and subscription improvement 2024-07-11 22:21:28 +02:00
Djuri Baars
87b22e5851 Nostr data source implementation 2024-07-11 22:08:42 +02:00
Djuri Baars
8e71f29d10 Use constants for setting default values, bring back wifi config portal timeout setting 2024-07-11 14:08:37 +02:00
Djuri Baars
1d710ba7f7 Improve rev. B startup flow, webUI fixes 2024-07-10 01:08:42 +02:00
Djuri Baars
4f4e37ec3c Fix workflow 2024-06-29 16:40:21 +02:00
Djuri Baars
ac02e1470d Bugfix for light sensor 2024-06-29 02:33:01 +02:00
Djuri Baars
fb67893f85 Add BH1750 lux sensor support 2024-06-29 02:19:25 +02:00
Djuri Baars
a9489c30f6 Upgrade and pin dependencies 2024-06-28 17:56:08 +02:00
Djuri Baars
24c3b46365 Allow custom mempool instance 2024-06-28 17:36:46 +02:00
Djuri Baars
262eae22dc Fix for EUR fetch in combination with own datasource, WebUI: detect timezone from browser 2024-06-28 15:18:40 +02:00
Djuri Baars
a8baa085c7 Fix setting defaults for stealing focus and block countdown 2024-06-09 11:31:25 +02:00
Djuri Baars
08929eb552 Fix tagging script, show tag in UI 2024-06-09 01:07:04 +02:00
Djuri Baars
2a8e391342 Add web UI flasher functionality 2024-06-09 00:45:46 +02:00
Djuri Baars
313efb7604 Better handling of steal screen, add firmware web update functionality 2024-06-08 15:07:15 +02:00
Djuri Baars
491618dd78 Fix light control API functionality, changed light setting endpoints 2024-06-08 11:35:23 +02:00
Djuri Baars
474ddbb086 Update github workflow actions 2024-06-08 01:17:20 +02:00
Djuri Baars
9ede0f4dc3 Web UI improvements, better frontlight functionality 2024-06-08 01:00:52 +02:00
Djuri Baars
32e40e2cb7 Fix frontlight 2024-06-07 15:18:34 +02:00
Djuri Baars
82e80f66e2 Explicit CDC on boot 2024-05-25 00:46:08 +02:00
Djuri Baars
f7599cb0ff Dependency update 2024-05-19 17:51:43 +02:00
Djuri Baars
88615ce248 Add version detection 2024-05-19 17:49:39 +02:00
Djuri Baars
ba0594959e Add filesystem commit identification and identify api call 2024-05-19 01:32:45 +02:00
Djuri Baars
db1523bef1 Missing defines 2024-05-19 00:11:08 +02:00
Djuri Baars
18139a9907 Better artifact file naming in workflow 2024-05-18 23:32:06 +02:00
Djuri Baars
e008383ab1 Update GH workflow 2024-05-18 23:15:54 +02:00
Djuri Baars
858241bd57 Add HW revision to mDNS for updates 2024-05-18 23:00:08 +02:00
Djuri Baars
ef7d629e8c Switch own data source domain, fix small block notification bug 2024-05-13 11:59:02 +02:00
Djuri Baars
9cdbcc6046 More emscripten methods 2024-05-10 01:09:07 +02:00
Djuri Baars
d4a05e2f36 Add Emscripten support to data handler 2024-05-09 15:43:44 +02:00
Djuri Baars
e0283d98ca Use own data source for price and block data 2024-05-09 01:02:40 +02:00
Djuri Baars
efaab00fb4 WebUI: Add German translation, switch to own price source 2024-05-08 23:54:17 +02:00
Djuri Baars
a2fa0a12a8 Dependency updates 2024-04-28 16:47:57 +02:00
Djuri Baars
4da04ca3ee Fix turn off LEDs 2024-04-27 16:48:06 +02:00
Djuri Baars
ad0800c233 Further improvements for connection recovery 2024-04-16 15:17:34 +02:00
Djuri Baars
2ef56c1938 Improve lost data detection 2024-04-12 00:17:40 +02:00
Djuri Baars
91fc474a1f Add second block source check 2024-03-30 11:40:58 +01:00
Djuri Baars
f84ae969d4 Upload all to webflasher 2024-03-18 21:23:25 +01:00
Djuri Baars
a3c31da425 Fix paths in workflow 2024-03-18 21:01:55 +01:00
Djuri Baars
7310e8509a Fix web flasher files 2024-03-18 20:50:27 +01:00
Djuri Baars
8068716f51 More LittleFS fixes 2024-03-18 20:28:41 +01:00
Djuri Baars
46133c2a42 Add missing files 2024-03-18 20:24:10 +01:00
Djuri Baars
55accd07f3 Fix LitteFS dependency error 2024-03-18 20:22:58 +01:00
Djuri Baars
3521d8c856 Correct job dependencies in workflow 2024-03-18 19:34:25 +01:00
Djuri Baars
d58c38c8c4 Add 2.9 inch EPD support 2024-03-18 19:32:34 +01:00
Djuri Baars
23ef2a64cc Add "storage mode" to protect EPDs (lowest button while booting) 2024-03-18 17:17:04 +01:00
Djuri Baars
37c57b7d97 Add extra check for missing price updates 2024-03-17 23:16:15 +01:00
Djuri Baars
3e00f1b4a6 Add functionality to disable all LEDs 2024-03-11 21:21:15 +01:00
Djuri Baars
969d2137c3 Fix for fee rate screen 2024-03-10 20:37:45 +01:00
Djuri Baars
2ca85ff479 Upgrade ArduinoJson to version 7, add Block Fee Rate screen 2024-03-10 20:24:55 +01:00
Djuri Baars
c49b8edcb8 Add sats symbol option, add countdown in blocks, add decimal point for market cap, add hostname to setup screen 2024-03-10 12:35:20 +01:00
Djuri Baars
e4a39de5fc Fix lost data connection detection 2024-01-31 23:45:26 +01:00
Djuri Baars
9cb4b97146 Monitor WS disconnects and try to reconnect 2024-01-27 15:54:31 +01:00
Djuri Baars
95c4aa3cad Update dependencies 2024-01-27 15:25:35 +01:00
102 changed files with 10147 additions and 2288 deletions
.forgejo/workflows
.github
actions/install-build
workflows
.gitignore.gitmodulesCMakeLists.txtREADME.md
boards
ci
datadependencies.lock
lib/btclock
maintainers.yamlpartition.csvpartition_16mb.csvpartition_8mb.csvplatformio.inirenovate.jsonrequirements.txt
scripts
sdkconfig.defaults
src
test/test_datahandler

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

View file

@ -9,14 +9,14 @@ runs:
node-version: lts/*
cache: yarn
cache-dependency-path: '**/yarn.lock'
- uses: actions/cache@v3
- uses: actions/cache@v4
with:
path: |
~/.cache/pip
~/.platformio/.cache
~/data/node_modules
key: ${{ runner.os }}-pio
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: '3.9'
- name: Get current date
@ -37,7 +37,7 @@ runs:
detailed_summary: true
- name: Build BTClock firmware
shell: bash
run: pio run -e lolin_s3_mini_qr
run: pio run
- name: Build BTClock filesystem
shell: bash
run: pio run -e lolin_s3_mini_qr --target buildfs
run: pio run --target buildfs

View file

@ -1,9 +1,9 @@
name: BTClock CI
on:
on:
push:
tags:
- '*'
- "*"
jobs:
build:
@ -17,49 +17,138 @@ jobs:
submodules: recursive
- name: "Install and build"
uses: ./.github/actions/install-build
- name: Copy bootloader to output folder
run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin .pio
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
include-hidden-files: true
retention-days: 1
name: prepared-outputs
path: .pio/**/*.bin
merge:
runs-on: ubuntu-latest
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: actions/download-artifact@v4
with:
name: prepared-outputs
path: .pio
- name: Install esptools.py
run: pip install --upgrade esptool
# - name: Create merged firmware binary
# run: mkdir -p ${{ matrix.chip.name }}_${{ matrix.epd_variant }} && 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/boot_app0.bin 0x10000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/firmware.bin 0x369000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/littlefs.bin
- name: Create merged firmware binary
run: mkdir -p output && esptool.py --chip esp32s3 merge_bin -o output/full-firmware.bin --flash_mode dio 0x0000 .pio/build/lolin_s3_mini_qr/bootloader.bin 0x8000 .pio/build/lolin_s3_mini_qr/partitions.bin 0xe000 ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin 0x10000 .pio/build/lolin_s3_mini_qr/firmware.bin 0x369000 .pio/build/lolin_s3_mini_qr/littlefs.bin
run: |
if [ "${{ matrix.chip.name }}" == "btclock_v8" ]; then
mkdir -p ${{ matrix.chip.name }}_${{ matrix.epd_variant }} && \
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 \
0x810000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/littlefs.bin;
else
# Original command for other cases
mkdir -p ${{ matrix.chip.name }}_${{ matrix.epd_variant }} && \
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 \
0x369000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/littlefs.bin
# Adjust the offset for littlefs or other files as needed for the original case
fi
- name: Create checksum for firmware
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
run: shasum -a 256 output/full-firmware.bin | awk '{print $1}' > output/full-firmware.sha256
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: Write commit hash to file
run: echo $GITHUB_SHA > output/commit.txt
- name: Write build date to file
run: echo "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" > output/date.txt
- name: Create checksum for littlefs partition
run: shasum -a 256 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/littlefs.bin | awk '{print $1}' > ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/littlefs.bin.sha256
- name: Copy all artifacts to output folder
run: cp .pio/build/lolin_s3_mini_qr/*.bin ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin output
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: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.chip.name }}-${{ matrix.epd_variant }}
path: |
.pio/build/lolin_s3_mini_qr/*.bin
output/full-firmware.bin
output/full-firmware.sha256
${{ matrix.chip.name }}_${{ matrix.epd_variant }}/*.bin
${{ matrix.chip.name }}_${{ matrix.epd_variant }}/*.sha256
release:
runs-on: ubuntu-latest
permissions:
contents: write
checks: write
needs: merge
steps:
- name: Download matrix outputs
uses: actions/download-artifact@v4
with:
pattern: build-*
merge-multiple: false
- name: Write commit hash to file
run: echo $GITHUB_SHA > commit.txt
- name: Write build date to file
run: echo "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" > date.txt
- name: Create release
uses: ncipollo/release-action@v1
with:
artifacts: "output/full-firmware.bin,output/full-firmware.sha256,.pio/build/lolin_s3_mini_qr/*.bin"
artifacts: "**/*.bin,**/*.sha256"
allowUpdates: true
removeArtifacts: true
makeLatest: true
# - name: Create release
# uses: ncipollo/release-action@v1
# with:
# artifacts: "output/full-firmware.bin,output/full-firmware.sha256,.pio/build/lolin_s3_mini_qr/*.bin"
# allowUpdates: true
# removeArtifacts: true
# makeLatest: true
- name: Pushes full-firmware.bin to web flasher
id: push_directory
uses: cpina/github-action-push-to-another-repository@main
env:
SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }}
with:
source-directory: output/
source-directory: .
target-directory: firmware_v3/
destination-github-username: 'btclock'
destination-repository-name: 'web-flasher'
target-branch: btclock
destination-github-username: "btclock"
destination-repository-name: "web-flasher"
target-branch: main
user-name: ${{github.actor}}
user-email: ${{github.actor}}@users.noreply.github.com
user-email: ${{github.actor}}@users.noreply.github.com

3
.gitignore vendored
View file

@ -10,4 +10,5 @@ data/.yarn
data/node_modules
node_modules
.DS_Store
*.bin
*.bin
ci/cache

2
.gitmodules vendored
View file

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

View file

@ -1,4 +1,7 @@
cmake_minimum_required(VERSION 3.16.0)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
list(APPEND EXTRA_COMPONENT_DIRS managed_components)
get_filename_component(configName "${CMAKE_BINARY_DIR}" NAME)
list(APPEND EXTRA_COMPONENT_DIRS "${CMAKE_SOURCE_DIR}/.pio/libdeps/${configName}/esp_littlefs")
project(btclock_espidf)

View file

@ -1,6 +1,8 @@
# 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.
@ -12,12 +14,46 @@ Biggest differences with v2 are:
- Added market capitalization screen
- LED flash on new block (and focus to block height screen on new block)
New features:
- BitAxe integration
- 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.
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.
**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
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.

60
boards/btclock_rev_b.json Normal file
View file

@ -0,0 +1,60 @@
{
"build": {
"arduino":{
"ldscript": "esp32s3_out.ld",
"partitions": "default_8MB.csv"
},
"core": "esp32",
"extra_flags": [
"-DBOARD_HAS_PSRAM",
"-DARDUINO_BTCLOCK_REV_B",
"-DARDUINO_ESP32S3_DEV",
"-DIS_BTCLOCK_REV_B",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1",
"-DARDUINO_USB_CDC_ON_BOOT=1"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",
"flash_mode": "qio",
"espidf": {
"sdkconfig_path": "boards"
},
"hwids": [
[
"0x303A",
"0x1001"
]
],
"mcu": "esp32s3",
"variant": "esp32s3"
},
"connectivity": [
"bluetooth",
"wifi"
],
"debug": {
"default_tool": "esp-builtin",
"onboard_tools": [
"esp-builtin"
],
"openocd_target": "esp32s3.cfg"
},
"frameworks": [
"arduino",
"espidf"
],
"name": "BTClock (rev. B)",
"upload": {
"flash_size": "8MB",
"maximum_ram_size": 327680,
"maximum_size": 8388608,
"use_1200bps_touch": true,
"wait_for_upload_port": true,
"require_upload_port": true,
"speed": 460800
},
"url": "http://github.com/btclock",
"vendor": "BTClock"
}

View file

@ -10,7 +10,7 @@
"-DBOARD_HAS_PSRAM",
"-DARDUINO_BTCLOCK",
"-DARDUINO_ESP32S3_DEV",
"-DIS_BTCLOCK_S3",
"-DIS_BTCLOCK_V8",
"-DARDUINO_USB_MODE=1",
"-DARDUINO_RUNNING_CORE=1",
"-DARDUINO_EVENT_RUNNING_CORE=1",
@ -20,8 +20,8 @@
"f_flash": "80000000L",
"flash_mode": "qio",
"psram_type": "opi",
"espidf": {
"sdkconfig_path": "boards"
"esp-idf": {
"sdkconfig_path": "boards/sdkconfig.btclock_v8"
},
"hwids": [
[

1598
boards/sdkconfig.btclock_v8 Normal file

File diff suppressed because it is too large Load diff

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 3f20d67f1abc10b20ddecfb5aa0ff4eb78c4c149
Subproject commit 48e585d4ec12bbc441499936d7cbf53d4307b9ec

View file

@ -1,15 +1,9 @@
dependencies:
esp_littlefs:
component_hash: null
source:
path: /Users/padjuri/src/btclock_espidf/btclock_espidf/managed_components/esp_littlefs
type: local
version: 1.10.2
idf:
component_hash: null
source:
type: idf
version: 4.4.5
manifest_hash: 4796491ac0ef21bc9e7da581f1db6c59f92d6096be0ffd2d5fa4f2645943c54a
version: 4.4.7
manifest_hash: cd2f3ee15e776d949eb4ea4eddc8f39b30c2a7905050850eed01ab4928143cff
target: esp32s3
version: 1.0.0

View file

@ -0,0 +1,52 @@
#include "bitaxe_handler.hpp"
std::array<std::string, NUM_SCREENS> parseBitaxeHashRate(std::string text)
{
std::array<std::string, NUM_SCREENS> ret;
ret.fill(""); // Initialize all elements to empty strings
std::size_t textLength = text.length();
// Calculate the position where the digits should start
// Account for the position of the "mdi:pickaxe" and the "GH/S" label
std::size_t startIndex = NUM_SCREENS - 1 - textLength;
// Insert the "mdi: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] = text.substr(i, 1);
}
ret[NUM_SCREENS - 1] = "GH/S";
ret[0] = "mdi:bitaxe";
return ret;
}
std::array<std::string, NUM_SCREENS> parseBitaxeBestDiff(std::string text)
{
std::array<std::string, NUM_SCREENS> ret;
std::uint32_t firstIndex = 0;
if (text.length() < NUM_SCREENS)
{
text.insert(text.begin(), NUM_SCREENS - text.length(), ' ');
ret[0] = "mdi:bitaxe";
ret[1] = "mdi:rocket";
firstIndex = 2;
}
for (std::uint8_t i = firstIndex; i < NUM_SCREENS; i++)
{
ret[i] = text[i];
}
return ret;
}

View file

@ -0,0 +1,5 @@
#include <array>
#include <string>
std::array<std::string, NUM_SCREENS> parseBitaxeHashRate(std::string text);
std::array<std::string, NUM_SCREENS> parseBitaxeBestDiff(std::string text);

View file

@ -1,60 +1,186 @@
#include "data_handler.hpp"
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/bind.h>
#endif
std::array<std::string, NUM_SCREENS> parsePriceData(std::uint32_t price, char currencySymbol)
char getCurrencySymbol(char input)
{
switch (input)
{
case CURRENCY_EUR:
return '[';
break;
case CURRENCY_GBP:
return ']';
break;
case CURRENCY_JPY:
return '^';
break;
case CURRENCY_AUD:
case CURRENCY_CAD:
case CURRENCY_USD:
return '$';
break;
default:
return input;
}
}
std::string getCurrencyCode(char input)
{
switch (input)
{
case CURRENCY_EUR:
return CURRENCY_CODE_EUR;
break;
case CURRENCY_GBP:
return CURRENCY_CODE_GBP;
break;
case CURRENCY_JPY:
return CURRENCY_CODE_JPY;
break;
case CURRENCY_AUD:
return CURRENCY_CODE_AUD;
break;
case CURRENCY_CAD:
return CURRENCY_CODE_CAD;
break;
default:
return CURRENCY_CODE_USD;
}
}
char getCurrencyChar(const std::string& input)
{
if (input == "EUR")
return CURRENCY_EUR;
else if (input == "GBP")
return CURRENCY_GBP;
else if (input == "JPY")
return CURRENCY_JPY;
else if (input == "AUD")
return CURRENCY_AUD;
else if (input == "CAD")
return CURRENCY_CAD;
else
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, bool mowMode, bool shareDot)
{
std::array<std::string, NUM_SCREENS> ret;
std::string priceString;
if (std::to_string(price).length() >= NUM_SCREENS) {
priceString = formatNumberWithSuffix(price);
} else {
priceString = currencySymbol + std::to_string(price);
if (std::to_string(price).length() >= NUM_SCREENS || useSuffixFormat)
{
int numScreens = shareDot || mowMode ? NUM_SCREENS - 1 : NUM_SCREENS - 2;
priceString = getCurrencySymbol(currencySymbol) + formatNumberWithSuffix(price, numScreens, mowMode);
}
else
{
priceString = getCurrencySymbol(currencySymbol) + std::to_string(price);
}
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(), ' ');
if (currencySymbol == '[')
if (mowMode)
{
ret[0] = "BTC/EUR";
ret[0] = "MOW/UNITS";
}
else
{
ret[0] = "BTC/USD";
ret[0] = "BTC/" + getCurrencyCode(currencySymbol);
}
firstIndex = 1;
}
size_t dotPosition = priceString.find('.');
for (std::uint32_t i = firstIndex; i < NUM_SCREENS; i++)
if (shareDot && dotPosition != std::string::npos && dotPosition > 0)
{
ret[i] = priceString[i];
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++)
{
ret[i] = std::string(1, priceString[i]);
}
}
return ret;
}
std::array<std::string, NUM_SCREENS> parseSatsPerCurrency(std::uint32_t price, char currencySymbol)
std::array<std::string, NUM_SCREENS> parseSatsPerCurrency(std::uint32_t price,char currencySymbol, bool withSatsSymbol)
{
std::array<std::string, NUM_SCREENS> ret;
std::string priceString = std::to_string(int(round(1 / float(price) * 10e7)));
std::uint32_t firstIndex = 0;
std::uint8_t insertSatSymbol = NUM_SCREENS - priceString.length() - 1;
if (priceString.length() < (NUM_SCREENS))
{
priceString.insert(priceString.begin(), NUM_SCREENS - priceString.length(), ' ');
if (currencySymbol == '[')
// Check if price is greater than 1 billion
if (price >= 100000000)
{
ret[0] = "SATS/EUR";
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
{
ret[0] = "MSCW/TIME";
priceString = std::to_string(static_cast<int>(round(1.0 / static_cast<double>(price) * 1e8))); // Default formatting
}
// 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);
else
ret[0] = "MSCW/TIME";
firstIndex = 1;
for (std::uint32_t i = firstIndex; i < NUM_SCREENS; i++)
{
ret[i] = priceString[i];
}
if (withSatsSymbol)
{
ret[insertSatSymbol] = "STS";
}
}
return ret;
}
@ -80,24 +206,67 @@ std::array<std::string, NUM_SCREENS> parseBlockHeight(std::uint32_t blockHeight)
return ret;
}
std::array<std::string, NUM_SCREENS> parseHalvingCountdown(std::uint32_t blockHeight)
std::array<std::string, NUM_SCREENS> parseBlockFees(std::uint16_t blockFees)
{
std::array<std::string, NUM_SCREENS> ret;
std::string blockFeesString = std::to_string(blockFees);
std::uint32_t firstIndex = 0;
if (blockFeesString.length() < NUM_SCREENS)
{
blockFeesString.insert(blockFeesString.begin(), NUM_SCREENS - blockFeesString.length() - 1, ' ');
ret[0] = "FEE/RATE";
firstIndex = 1;
}
for (std::uint8_t i = firstIndex; i < NUM_SCREENS - 1; i++)
{
ret[i] = blockFeesString[i];
}
ret[NUM_SCREENS - 1] = "sat/vB";
return ret;
}
std::array<std::string, NUM_SCREENS> parseHalvingCountdown(std::uint32_t blockHeight, bool asBlocks)
{
std::array<std::string, NUM_SCREENS> ret;
const std::uint32_t nextHalvingBlock = 210000 - (blockHeight % 210000);
const std::uint32_t minutesToHalving = nextHalvingBlock * 10;
const int years = floor(minutesToHalving / 525600);
const int days = floor((minutesToHalving - (years * 525600)) / (24 * 60));
const int hours = floor((minutesToHalving - (years * 525600) - (days * (24 * 60))) / 60);
const int mins = floor(minutesToHalving - (years * 525600) - (days * (24 * 60)) - (hours * 60));
ret[0] = "BIT/COIN";
ret[1] = "HALV/ING";
ret[(NUM_SCREENS - 5)] = std::to_string(years) + "/YRS";
ret[(NUM_SCREENS - 4)] = std::to_string(days) + "/DAYS";
ret[(NUM_SCREENS - 3)] = std::to_string(hours) + "/HRS";
ret[(NUM_SCREENS - 2)] = std::to_string(mins) + "/MINS";
ret[(NUM_SCREENS - 1)] = "TO/GO";
if (asBlocks)
{
std::string blockNrString = std::to_string(nextHalvingBlock);
std::uint32_t firstIndex = 0;
if (blockNrString.length() < NUM_SCREENS)
{
blockNrString.insert(blockNrString.begin(), NUM_SCREENS - blockNrString.length(), ' ');
ret[0] = "HAL/VING";
firstIndex = 1;
}
for (std::uint32_t i = firstIndex; i < NUM_SCREENS; i++)
{
ret[i] = blockNrString[i];
}
}
else
{
const int years = floor(minutesToHalving / 525600);
const int days = floor((minutesToHalving - (years * 525600)) / (24 * 60));
const int hours = floor((minutesToHalving - (years * 525600) - (days * (24 * 60))) / 60);
const int mins = floor(minutesToHalving - (years * 525600) - (days * (24 * 60)) - (hours * 60));
ret[0] = "BIT/COIN";
ret[1] = "HAL/VING";
ret[(NUM_SCREENS - 5)] = std::to_string(years) + "/YRS";
ret[(NUM_SCREENS - 4)] = std::to_string(days) + "/DAYS";
ret[(NUM_SCREENS - 3)] = std::to_string(hours) + "/HRS";
ret[(NUM_SCREENS - 2)] = std::to_string(mins) + "/MINS";
ret[(NUM_SCREENS - 1)] = "TO/GO";
}
return ret;
}
@ -107,21 +276,16 @@ std::array<std::string, NUM_SCREENS> parseMarketCap(std::uint32_t blockHeight, s
std::array<std::string, NUM_SCREENS> ret;
std::uint32_t firstIndex = 0;
double supply = getSupplyAtBlock(blockHeight);
int64_t marketCap = static_cast<std::int64_t>(supply * double(price));
if (currencySymbol == '[')
{
ret[0] = "EUR/MCAP";
}
else
{
ret[0] = "USD/MCAP";
}
uint64_t marketCap = static_cast<std::uint64_t>(supply * double(price));
ret[0] = getCurrencyCode(currencySymbol) + "/MCAP";
if (bigChars)
{
firstIndex = 1;
std::string priceString = currencySymbol + formatNumberWithSuffix(marketCap);
// Serial.print("Market cap: ");
// Serial.println(marketCap);
std::string priceString = currencySymbol + formatNumberWithSuffix(marketCap, (NUM_SCREENS - 2));
priceString.insert(priceString.begin(), NUM_SCREENS - priceString.length(), ' ');
for (std::uint32_t i = firstIndex; i < NUM_SCREENS; i++)
@ -148,7 +312,7 @@ std::array<std::string, NUM_SCREENS> parseMarketCap(std::uint32_t blockHeight, s
ret[i] = "";
}
ret[NUM_SCREENS - groups - 1] = " $ ";
ret[NUM_SCREENS - groups - 1] = std::string(" ") + currencySymbol + " ";
for (std::uint32_t i = 0; i < groups; i++)
{
ret[(NUM_SCREENS - groups + i)] = stringValue.substr(i * 3, 3).c_str();
@ -156,4 +320,71 @@ std::array<std::string, NUM_SCREENS> parseMarketCap(std::uint32_t blockHeight, s
}
return ret;
}
}
#ifdef __EMSCRIPTEN__
emscripten::val arrayToStringArray(const std::array<std::string, NUM_SCREENS> &arr)
{
emscripten::val jsArray = emscripten::val::array();
for (const auto &str : arr)
{
jsArray.call<void>("push", str);
}
return jsArray;
}
emscripten::val vectorToStringArray(const std::vector<std::string> &vec)
{
emscripten::val jsArray = emscripten::val::array();
for (size_t i = 0; i < vec.size(); ++i)
{
jsArray.set(i, vec[i]);
}
return jsArray;
}
emscripten::val parseBlockHeightArray(std::uint32_t blockHeight)
{
return arrayToStringArray(parseBlockHeight(blockHeight));
}
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, mowMode, shareDot));
}
emscripten::val parseHalvingCountdownArray(std::uint32_t blockHeight, bool asBlocks)
{
return arrayToStringArray(parseHalvingCountdown(blockHeight, asBlocks));
}
emscripten::val parseMarketCapArray(std::uint32_t blockHeight, std::uint32_t price, const std::string &currencySymbol, bool bigChars)
{
return arrayToStringArray(parseMarketCap(blockHeight, price, currencySymbol[0], bigChars));
}
emscripten::val parseBlockFeesArray(std::uint16_t blockFees)
{
return arrayToStringArray(parseBlockFees(blockFees));
}
emscripten::val parseSatsPerCurrencyArray(std::uint32_t price, const std::string &currencySymbol, bool withSatsSymbol)
{
return arrayToStringArray(parseSatsPerCurrency(price, currencySymbol[0], withSatsSymbol));
}
EMSCRIPTEN_BINDINGS(my_module)
{
// emscripten::register_vector<std::string>("StringList");
emscripten::function("parseBlockHeight", &parseBlockHeightArray);
emscripten::function("parseHalvingCountdown", &parseHalvingCountdownArray);
emscripten::function("parseMarketCap", &parseMarketCapArray);
emscripten::function("parseBlockFees", &parseBlockFeesArray);
emscripten::function("parseSatsPerCurrency", &parseSatsPerCurrencyArray);
emscripten::function("parsePriceData", &parsePriceDataArray);
emscripten::function("arrayToStringArray", &arrayToStringArray);
emscripten::function("vectorToStringArray", &vectorToStringArray);
}
#endif

View file

@ -2,11 +2,31 @@
#include <string>
#include <cmath>
#include <cstdint>
#include <vector>
#include "utils.hpp"
std::array<std::string, NUM_SCREENS> parsePriceData(std::uint32_t price, char currencySymbol);
std::array<std::string, NUM_SCREENS> parseSatsPerCurrency(std::uint32_t price, char currencySymbol);
const char CURRENCY_USD = '$';
const char CURRENCY_EUR = '[';
const char CURRENCY_GBP = ']';
const char CURRENCY_JPY = '^';
const char CURRENCY_AUD = '_';
const char CURRENCY_CAD = '`';
const std::string CURRENCY_CODE_USD = "USD";
const std::string CURRENCY_CODE_EUR = "EUR";
const std::string CURRENCY_CODE_GBP = "GBP";
const std::string CURRENCY_CODE_JPY = "JPY";
const std::string CURRENCY_CODE_AUD = "AUD";
const std::string CURRENCY_CODE_CAD = "CAD";
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> parseBlockHeight(std::uint32_t blockHeight);
std::array<std::string, NUM_SCREENS> parseHalvingCountdown(std::uint32_t blockHeight);
std::array<std::string, NUM_SCREENS> parseMarketCap(std::uint32_t blockHeight, std::uint32_t price, char currencySymbol, bool bigChars);
std::array<std::string, NUM_SCREENS> parseHalvingCountdown(std::uint32_t blockHeight, bool asBlocks);
std::array<std::string, NUM_SCREENS> parseMarketCap(std::uint32_t blockHeight, std::uint32_t price, char currencySymbol, bool bigChars);
std::array<std::string, NUM_SCREENS> parseBlockFees(std::uint16_t blockFees);
char getCurrencySymbol(char input);
std::string getCurrencyCode(char input);
char getCurrencyChar(const std::string& input);

View file

@ -0,0 +1,24 @@
#include "nostrdisplay_handler.hpp"
std::array<std::string, NUM_SCREENS> parseZapNotify(std::uint16_t amount, bool withSatsSymbol)
{
std::string text = std::to_string(amount);
std::size_t textLength = text.length();
std::size_t startIndex = NUM_SCREENS - textLength;
std::array<std::string, NUM_SCREENS> textEpdContent = {"ZAP", "mdi-lnbolt", "", "", "", "", ""};
// Insert the sats symbol just before the digits
if (startIndex > 0 && withSatsSymbol)
{
textEpdContent[startIndex - 1] = "STS";
}
// Place the digits
for (std::size_t i = 0; i < textLength; i++)
{
textEpdContent[startIndex + i] = text.substr(i, 1);
}
return textEpdContent;
}

View file

@ -0,0 +1,5 @@
#include <array>
#include <string>
#include "utils.hpp"
std::array<std::string, NUM_SCREENS> parseZapNotify(std::uint16_t amount, bool withSatsSymbol);

View file

@ -5,18 +5,21 @@ int modulo(int x, int N)
return (x % N + N) % N;
}
double getSupplyAtBlock(std::uint32_t blockNr) {
if (blockNr >= 33 * 210000) {
double getSupplyAtBlock(std::uint32_t blockNr)
{
if (blockNr >= 33 * 210000)
{
return 20999999.9769;
}
}
const int initialBlockReward = 50; // Initial block reward
const int halvingInterval = 210000; // Number of blocks before halving
const int halvingInterval = 210000; // Number of blocks before halving
int halvingCount = blockNr / halvingInterval;
double totalBitcoinInCirculation = 0;
for (int i = 0; i < halvingCount; ++i) {
for (int i = 0; i < halvingCount; ++i)
{
totalBitcoinInCirculation += halvingInterval * initialBlockReward * std::pow(0.5, i);
}
@ -25,24 +28,218 @@ double getSupplyAtBlock(std::uint32_t blockNr) {
return totalBitcoinInCirculation;
}
std::string formatNumberWithSuffix(std::uint64_t num) {
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
const long long quadrillion = 1000000000000000LL;
const long long trillion = 1000000000000LL;
const long long billion = 1000000000;
const long long million = 1000000;
const long long thousand = 1000;
if (num >= quadrillion) {
return std::to_string(num / quadrillion) + "Q";
} else if (num >= trillion) {
return std::to_string(num / trillion) + "T";
} else if (num >= billion) {
return std::to_string(num / billion) + "B";
} else if (num >= million) {
return std::to_string(num / million) + "M";
} else if (num >= thousand) {
return std::to_string(num / thousand) + "K";
} else {
return std::to_string(num);
double numDouble = (double)num;
int numDigits = (int)log10(num) + 1;
char suffix;
if (num >= quadrillion || numDigits > 15)
{
numDouble /= quadrillion;
suffix = 'Q';
}
else if (num >= trillion || numDigits > 12)
{
numDouble /= trillion;
suffix = 'T';
}
else if (num >= billion || numDigits > 9)
{
numDouble /= billion;
suffix = 'B';
}
else if (num >= million || numDigits > 6 || (mowMode && num >= thousand))
{
numDouble /= million;
suffix = 'M';
}
else if (!mowMode && (num >= thousand || numDigits > 3))
{
numDouble /= thousand;
suffix = 'K';
}
else if (!mowMode)
{
snprintf(result, sizeof(result), "%llu", (unsigned long long)num);
return result;
}
else // mowMode is true and num < 1000
{
numDouble /= million;
suffix = 'M';
}
// Add suffix
int len;
// 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)
{
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;
}
/**
* Get sat amount from a bolt11 invoice
*
* Based on https://github.com/lnbits/nostr-zap-lamp/blob/main/nostrZapLamp/nostrZapLamp.ino
*/
int64_t getAmountInSatoshis(std::string bolt11) {
int64_t number = -1;
char multiplier = ' ';
for (unsigned int i = 0; i < bolt11.length(); ++i) {
if (isdigit(bolt11[i])) {
number = 0;
while (isdigit(bolt11[i])) {
number = number * 10 + (bolt11[i] - '0');
++i;
}
for (unsigned int j = i; j < bolt11.length(); ++j) {
if (isalpha(bolt11[j])) {
multiplier = bolt11[j];
break;
}
}
break;
}
}
if (number == -1 || multiplier == ' ') {
return -1;
}
int64_t satoshis = number;
switch (multiplier) {
case 'm':
satoshis *= 100000; // 0.001 * 100,000,000
break;
case 'u':
satoshis *= 100; // 0.000001 * 100,000,000
break;
case 'n':
satoshis /= 10; // 0.000000001 * 100,000,000
break;
case 'p':
satoshis /= 10000; // 0.000000000001 * 100,000,000
break;
default:
return -1;
}
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);
}

View file

@ -3,9 +3,17 @@
#include <string>
#include <cmath>
#include <cstdint>
#include <sstream>
#include <iomanip>
#include <unordered_map>
int modulo(int x,int N);
double getSupplyAtBlock(std::uint32_t blockNr);
std::string formatNumberWithSuffix(std::uint64_t num);
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);
void parseHashrateString(const std::string& hashrate, std::string& label, std::string& output, unsigned int maxCharacters);
int getHashrateMultiplier(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
nvs, data, nvs, 36K, 20K,
otadata, data, ota, 56K, 8K,
app0, app, ota_0, 64K, 1700K,
app1, app, ota_1, , 1700K,
spiffs, data, spiffs, , 400K,
coredump, data, coredump,, 64K,
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x1b8000,
app1, app, ota_1, , 0x1b8000,
spiffs, data, spiffs, , 0x66C00,
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
nvs, data, nvs, 36K, 20K,
otadata, data, ota, 56K, 8K,
app0, app, ota_0, 64K, 4096K,
app1, app, ota_1, , 4096K,
spiffs, data, spiffs, , 3072K,
coredump, data, coredump,, 64K,
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x6F0000,
app1, app, ota_1, , 0x6F0000,
spiffs, data, spiffs, , 0x200000,
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 3072K 0x200000
7 coredump data coredump 64K 0x10000

7
partition_8mb.csv Normal file
View file

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

View file

@ -7,92 +7,190 @@
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[platformio]
data_dir = data/build_gz
default_envs = lolin_s3_mini_213epd, lolin_s3_mini_29epd, btclock_rev_b_213epd, btclock_v8_213epd
[env]
[btclock_base]
platform = https://github.com/platformio/platform-espressif32.git
framework = arduino, espidf
platform = espressif32 @ ^6.9.0
framework = arduino, espidf
monitor_speed = 115200
monitor_filters = esp32_exception_decoder, colorize
board_build.filesystem = littlefs
extra_scripts = post:scripts/extra_script.py
build_flags =
!python scripts/git_rev.py
-DLAST_BUILD_TIME=$UNIX_TIME
-DARDUINO_USB_CDC_ON_BOOT
-DCORE_DEBUG_LEVEL=0
-fexceptions
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 =
!python scripts/git_rev.py
-DLAST_BUILD_TIME=$UNIX_TIME
-DARDUINO_USB_CDC_ON_BOOT
-DCORE_DEBUG_LEVEL=0
-fexceptions
build_unflags =
-Werror=all
-fno-exceptions
-Werror=all
-fno-exceptions
lib_deps =
bblanchon/ArduinoJson@^6.21.3
esphome/Improv@^1.2.3
esphome/ESPAsyncWebServer-esphome@^3.1.0
adafruit/Adafruit BusIO@^1.14.5
adafruit/Adafruit MCP23017 Arduino Library@^2.3.2
adafruit/Adafruit NeoPixel@^1.11.0
https://github.com/dsbaars/universal_pin
https://github.com/dsbaars/GxEPD2#universal_pin
https://github.com/tzapu/WiFiManager.git#v2.0.16-rc.2
https://github.com/joltwallet/esp_littlefs.git
bblanchon/ArduinoJson@^7.2.1
mathieucarbou/ESPAsyncWebServer @ 3.3.23
robtillaart/MCP23017@^0.8.0
adafruit/Adafruit NeoPixel@^1.12.3
https://github.com/dsbaars/universal_pin#feature/mcp23017_rt
https://github.com/dsbaars/GxEPD2#universal_pin
https://github.com/tzapu/WiFiManager.git#v2.0.17
https://github.com/dsbaars/nostrduino#feature/fix-btclock
[env:lolin_s3_mini]
extends = btclock_base
board = lolin_s3_mini
board_build.partitions = partition.csv
build_flags =
${btclock_base.build_flags}
-D MCP_INT_PIN=8
-D NEOPIXEL_PIN=34
-D NEOPIXEL_COUNT=4
-D NUM_SCREENS=7
-D I2C_SDA_PIN=35
-D I2C_SCK_PIN=36
build_flags =
${btclock_base.build_flags}
-D MCP_INT_PIN=8
-D NEOPIXEL_PIN=34
-D NEOPIXEL_COUNT=4
-D NUM_SCREENS=7
-D I2C_SDA_PIN=35
-D I2C_SCK_PIN=36
-DARDUINO_USB_CDC_ON_BOOT=1
-D IS_HW_REV_A
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_qr]
[env:btclock_rev_b]
extends = btclock_base
board = btclock_rev_b
board_build.partitions = partition_8mb.csv
build_flags =
${btclock_base.build_flags}
-D MCP_INT_PIN=8
-D NEOPIXEL_PIN=15
-D NEOPIXEL_COUNT=4
-D NUM_SCREENS=7
-D I2C_SDA_PIN=35
-D I2C_SCK_PIN=36
-D HAS_FRONTLIGHT
-D PCA_OE_PIN=45
-D PCA_I2C_ADDR=0x42
-D IS_HW_REV_B
lib_deps =
${btclock_base.lib_deps}
robtillaart/PCA9685@^0.7.1
claws/BH1750@^1.3.0
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]
extends = env:lolin_s3_mini
test_framework = unity
build_flags =
${env:lolin_s3_mini.build_flags}
-D USE_QR
build_flags =
${env:lolin_s3_mini.build_flags}
-D USE_QR
-D VERSION_EPD_2_13
-D HW_REV=\"REV_A_EPD_2_13\"
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
[env:btclock_s3]
extends = btclock_base
board = btclock
board_build.partitions = partition_16mb.csv
[env:btclock_rev_b_213epd]
extends = env:btclock_rev_b
test_framework = unity
build_flags =
${btclock_base.build_flags}
-D MCP_INT_PIN=4
-D NEOPIXEL_PIN=5
-D NEOPIXEL_COUNT=4
-D NUM_SCREENS=8
-D SPI_SDA_PIN=11
-D SPI_SCK_PIN=12
-D I2C_SDA_PIN=1
-D I2C_SCK_PIN=2
-D MCP_RESET_PIN=21
-D MCP1_A0_PIN=6
-D MCP1_A1_PIN=7
-D MCP1_A2_PIN=8
-D MCP2_A0_PIN=9
-D MCP2_A1_PIN=10
-D MCP2_A2_PIN=14
build_flags =
${env:btclock_rev_b.build_flags}
-D USE_QR
-D VERSION_EPD_2_13
-D HW_REV=\"REV_B_EPD_2_13\"
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
[env:lolin_s3_mini_29epd]
extends = env:lolin_s3_mini
test_framework = unity
build_flags =
${env:lolin_s3_mini.build_flags}
-D USE_QR
-D VERSION_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]
extends = env:btclock_rev_b
test_framework = unity
build_flags =
${env:btclock_rev_b.build_flags}
-D USE_QR
-D VERSION_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]
extends = btclock_base
board = btclock_v8
board_build.partitions = partition_16mb.csv
board_build.flash_mode = qio
test_framework = unity
build_flags =
${btclock_base.build_flags}
-D MCP_INT_PIN=4
-D NEOPIXEL_PIN=5
-D NEOPIXEL_COUNT=4
-D NUM_SCREENS=8
-D SPI_SDA_PIN=11
-D SPI_SCK_PIN=12
-D I2C_SDA_PIN=1
-D I2C_SCK_PIN=2
-D MCP_RESET_PIN=21
-D MCP1_A0_PIN=6
-D MCP1_A1_PIN=7
-D MCP1_A2_PIN=8
-D MCP2_A0_PIN=9
-D MCP2_A1_PIN=10
-D MCP2_A2_PIN=14
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]
extends = env:btclock_v8
test_framework = unity
build_flags =
${env:btclock_v8.build_flags}
-D USE_QR
-D VERSION_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]
platform = native
test_framework = unity
build_flags =
${btclock_base.build_flags}
-D MCP_INT_PIN=8
-D NEOPIXEL_PIN=34
-D NEOPIXEL_COUNT=4
-D NUM_SCREENS=7
build_flags =
${btclock_base.build_flags}
-D MCP_INT_PIN=8
-D NEOPIXEL_PIN=34
-D NEOPIXEL_COUNT=4
-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,8 +1,18 @@
Import("env")
import os
import gzip
from shutil import copyfileobj, rmtree
from shutil import copyfileobj, rmtree, copyfile, copytree
from pathlib import Path
import subprocess
revision = (
subprocess.check_output(["git", "rev-parse", "HEAD"])
.strip()
.decode("utf-8")
)
def gzip_file(input_file, output_file):
with open(input_file, 'rb') as f_in:
@ -19,20 +29,97 @@ def process_directory(input_dir, output_dir):
Path(output_root).mkdir(parents=True, exist_ok=True)
for file in files:
# if file.endswith(('.html', '.css', '.js')):
# if not file.endswith(('.bin')):
input_file_path = os.path.join(root, file)
output_file_path = os.path.join(output_root, file + '.gz')
gzip_file(input_file_path, output_file_path)
print(f'Compressed: {input_file_path} -> {output_file_path}')
file_path = os.path.join(output_dir, "fs_hash.txt")
with open(file_path, "w") as file:
file.write(revision)
# Build web interface before building FS
def before_buildfs(source, target, env):
env.Execute("cd data && yarn && yarn postinstall && yarn build")
input_directory = 'data/dist'
output_directory = 'data/build_gz'
# copytree("assets", "data/dist/assets")
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"] = ""
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

View file

@ -48,8 +48,8 @@ class Listener(ServiceListener):
#arguments = [f"-i {str()} -f -r"]
namespace = argparse.Namespace(
esp_ip=info.parsed_addresses()[0],
image=f"{os.getcwd()}/.pio/build/lolin_s3_mini_qr/firmware.bin",
littlefs=f"{os.getcwd()}/.pio/build/lolin_s3_mini_qr/littlefs.bin",
image=f"{os.getcwd()}/.pio/build/lolin_s3_mini_213epd/firmware.bin",
littlefs=f"{os.getcwd()}/.pio/build/lolin_s3_mini_213epd/littlefs.bin",
progress=True
)
if (str(info.properties.get(b"version").decode())) != "3.0":
@ -64,7 +64,7 @@ class Listener(ServiceListener):
print("Different version, going to update")
#espota.serve(namespace.esp_ip, "0.0.0.0", 3232, random.randint(10000,60000), "", namespace.littlefs, SPIFFS)
#espota.serve(namespace.esp_ip, "0.0.0.0", 3232, random.randint(10000,60000), "", namespace.image, FLASH)
espota.serve(namespace.esp_ip, "0.0.0.0", 3232, random.randint(10000,60000), "", namespace.image, FLASH)
#print(arguments)
#logging.basicConfig(level = logging.DEBUG, format = '%(asctime)-8s [%(levelname)s]: %(message)s', datefmt = '%H:%M:%S')

View file

@ -5,4 +5,17 @@ revision = (
.strip()
.decode("utf-8")
)
print("'-DGIT_REV=\"%s\"'" % revision)
try:
tag = (
subprocess.check_output(["git", "describe", "--tags", "--exact-match"])
.strip()
.decode("utf-8")
)
git_tag_define = '\'-DGIT_TAG=\"%s\"\'' % tag
except subprocess.CalledProcessError:
git_tag_define = ''
print("'-DGIT_REV=\"%s\"'" % revision)
if git_tag_define:
print(git_tag_define)

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

@ -7,8 +7,8 @@ CONFIG_MBEDTLS_KEY_EXCHANGE_PSK=y
#CONFIG_FREERTOS_USE_TRACE_FACILITY=y
#CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS=y
#CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=n
CONFIG_ESP_TLS_INSECURE=y
CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY=y
#CONFIG_ESP_TLS_INSECURE=y
#CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY=y
CONFIG_HEAP_CORRUPTION_DETECTION=CONFIG_HEAP_POISONING_LIGHT
CONFIG_HEAP_POISONING_LIGHT=y
@ -16,18 +16,14 @@ CONFIG_HEAP_POISONING_LIGHT=y
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y
CONFIG_BOOTLOADER_LOG_LEVEL=0
CONFIG_LOG_BOOTLOADER_LEVEL_NONE=y
CONFIG_BOOTLOADER_LOG_LEVEL_NONE=y
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
CONFIG_LOG_DEFAULT_LEVEL_NONE=y
CONFIG_LOG_DEFAULT_LEVEL=0
CONFIG_LOG_MAXIMUM_LEVEL=0
CONFIG_LOG_BOOTLOADER_LEVEL_NONE=y
CONFIG_LOG_BOOTLOADER_LEVEL=0
CONFIG_CXX_EXCEPTIONS=y
CONFIG_COMPILER_CXX_EXCEPTIONS=y
#CONFIG_BOOTLOADER_WDT_ENABLE=n
#CONFIG_TASK_WDT=n
#CONFIG_ESP_TASK_WDT=n
#Required for BTClock
#CONFIG_SPIRAM_MODE_OCT=y
@ -42,12 +38,11 @@ CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=12
CONFIG_ESP32_WIFI_RX_BA_WIN=6
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=3120
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
CONFIG_RTC_CLK_CAL_CYCLES=576
CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=y
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=3120
CONFIG_ESP_SYSTEM_MEMPROT_FEATURE=n
CONFIG_SPIRAM_CACHE_WORKAROUND=y
CONFIG_COMPILER_OPTIMIZATION_PERF=y
CONFIG_COMPILER_OPTIMIZATION_LEVEL_RELEASE=y
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
#CONFIG_NEWLIB_NANO_FORMAT=y

View file

@ -1526,7 +1526,78 @@ const uint8_t Antonio_SemiBold40pt7bBitmaps[] PROGMEM = {
0x3F, 0xE0, 0x3F, 0xE0, 0xFF, 0xC0, 0x7F, 0xFF, 0xFF, 0x80, 0xFF, 0xFF,
0xFE, 0x00, 0xFF, 0xFF, 0xFC, 0x00, 0xFF, 0xFF, 0xF0, 0x00, 0xFF, 0xFF,
0xC0, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xF8, 0x00, 0x00, 0x1F, 0x80,
0x00};
0x00,
// euro
0x00, 0x07, 0xFC, 0x00, 0x00, 0x3F, 0xFF, 0x00, 0x01, 0xFF, 0xFF, 0x00,
0x07, 0xFF, 0xFF, 0x00, 0x1F, 0xFF, 0xFF, 0x00, 0x3F, 0xFF, 0xFF, 0x00,
0xFF, 0xFF, 0xFE, 0x01, 0xFF, 0xFF, 0xFE, 0x07, 0xFE, 0x07, 0xFC, 0x0F,
0xF8, 0x0F, 0xF8, 0x1F, 0xF0, 0x0F, 0xF8, 0x3F, 0xE0, 0x1F, 0xF0, 0xFF,
0x80, 0x3F, 0xE1, 0xFF, 0x00, 0x7F, 0xC3, 0xFE, 0x00, 0xFF, 0x87, 0xFC,
0x00, 0xFF, 0x0F, 0xF8, 0x01, 0xFE, 0x1F, 0xF0, 0x03, 0xFC, 0x3F, 0xE0,
0x07, 0xF8, 0x7F, 0xC0, 0x0F, 0xF0, 0xFF, 0x80, 0x1F, 0xE1, 0xFF, 0x00,
0x3F, 0xC3, 0xFE, 0x00, 0x7F, 0x87, 0xFC, 0x00, 0xFF, 0x0F, 0xF8, 0x01,
0xFE, 0x1F, 0xF0, 0x03, 0xFF, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF, 0xFF, 0x80,
0x0F, 0xFF, 0xFF, 0x00, 0x1F, 0xFF, 0xFE, 0x00, 0x3F, 0xFF, 0xFC, 0x00,
0x07, 0xFC, 0x00, 0x00, 0x0F, 0xF8, 0x00, 0x00, 0x1F, 0xF0, 0x00, 0x00,
0x3F, 0xE0, 0x00, 0x07, 0xFF, 0xFF, 0x80, 0x0F, 0xFF, 0xFF, 0x00, 0x1F,
0xFF, 0xFE, 0x00, 0x3F, 0xFF, 0xFC, 0x00, 0x7F, 0xFF, 0xF8, 0x00, 0x0F,
0xF8, 0x00, 0x00, 0x1F, 0xF0, 0x03, 0xFC, 0x3F, 0xE0, 0x07, 0xF8, 0x7F,
0xC0, 0x0F, 0xF0, 0xFF, 0x80, 0x1F, 0xE1, 0xFF, 0x00, 0x3F, 0xC3, 0xFE,
0x00, 0x7F, 0x87, 0xFC, 0x00, 0xFF, 0x0F, 0xF8, 0x01, 0xFE, 0x1F, 0xF0,
0x03, 0xFC, 0x3F, 0xE0, 0x07, 0xF8, 0x7F, 0xC0, 0x0F, 0xF0, 0xFF, 0x80,
0x1F, 0xE1, 0xFF, 0x00, 0x7F, 0xC3, 0xFE, 0x00, 0xFF, 0x87, 0xFC, 0x01,
0xFF, 0x0F, 0xF8, 0x03, 0xFE, 0x0F, 0xF8, 0x07, 0xFC, 0x1F, 0xF0, 0x0F,
0xF0, 0x3F, 0xE0, 0x3F, 0xE0, 0x3F, 0xE0, 0xFF, 0xC0, 0x7F, 0xFF, 0xFF,
0x80, 0xFF, 0xFF, 0xFE, 0x00, 0xFF, 0xFF, 0xFC, 0x00, 0xFF, 0xFF, 0xF0,
0x00, 0xFF, 0xFF, 0xC0, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xF8, 0x00,
0x00, 0x3F, 0x80, 0x00,
// pound
0x00, 0x0F, 0xFC, 0x00, 0x01, 0xFF, 0xFC, 0x00, 0x0F, 0xFF, 0xFC, 0x00,
0x7F, 0xFF, 0xF8, 0x03, 0xFF, 0xFF, 0xE0, 0x1F, 0xFF, 0xFF, 0xC0, 0x7F,
0xFF, 0xFF, 0x83, 0xFF, 0x03, 0xFE, 0x0F, 0xF8, 0x07, 0xF8, 0x3F, 0xC0,
0x1F, 0xE1, 0xFF, 0x00, 0x3F, 0xC7, 0xFC, 0x00, 0xFF, 0x1F, 0xE0, 0x03,
0xFC, 0x7F, 0x80, 0x0F, 0xF1, 0xFE, 0x00, 0x3F, 0xC7, 0xF8, 0x00, 0xFF,
0x1F, 0xF0, 0x03, 0xFC, 0x7F, 0xC0, 0x0F, 0xF1, 0xFF, 0x00, 0x3F, 0xC3,
0xFC, 0x00, 0xFF, 0x0F, 0xF0, 0x00, 0x00, 0x3F, 0xE0, 0x00, 0x00, 0xFF,
0x80, 0x00, 0x01, 0xFE, 0x00, 0x00, 0x07, 0xF8, 0x00, 0x00, 0x1F, 0xF0,
0x00, 0x00, 0x7F, 0xC0, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x03, 0xFC, 0x00,
0x00, 0x0F, 0xF8, 0x00, 0x00, 0x3F, 0xE0, 0x00, 0x3F, 0xFF, 0xFF, 0xF8,
0xFF, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0xFE, 0x3F,
0xFF, 0xFF, 0xF8, 0xFF, 0xFF, 0xFF, 0xE0, 0x03, 0xFE, 0x00, 0x00, 0x0F,
0xF8, 0x00, 0x00, 0x3F, 0xE0, 0x00, 0x00, 0x7F, 0x80, 0x00, 0x01, 0xFE,
0x00, 0x00, 0x07, 0xFC, 0x00, 0x00, 0x1F, 0xF0, 0x00, 0x00, 0x7F, 0xC0,
0x00, 0x01, 0xFF, 0x00, 0x00, 0x07, 0xFC, 0x00, 0x00, 0x1F, 0xF0, 0x00,
0x00, 0x7F, 0xC0, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x07, 0xFC, 0x00, 0x00,
0x1F, 0xF0, 0x00, 0x00, 0x7F, 0xC0, 0x00, 0x03, 0xFE, 0x00, 0x00, 0x0F,
0xF8, 0x00, 0x00, 0x3F, 0xE0, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x0F, 0xF8,
0x00, 0x00, 0x7F, 0xC0, 0x00, 0x03, 0xFE, 0x00, 0x00, 0x1F, 0xF0, 0x00,
0x01, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xDF, 0xFF, 0xFF, 0xFF,
0x7F, 0xFF, 0xFF, 0xFD, 0xFF, 0xFF, 0xFF, 0xF7, 0xFF, 0xFF, 0xFF, 0xDF,
0xFF, 0xFF, 0xFF,
// yen
0xFF, 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x07, 0xFB, 0xFE, 0x00, 0x1F, 0xF3,
0xFC, 0x00, 0x3F, 0xE7, 0xFC, 0x00, 0x7F, 0xCF, 0xF8, 0x00, 0xFF, 0x0F,
0xF0, 0x03, 0xFE, 0x1F, 0xE0, 0x07, 0xFC, 0x3F, 0xE0, 0x0F, 0xF0, 0x7F,
0xC0, 0x1F, 0xE0, 0x7F, 0x80, 0x7F, 0xC0, 0xFF, 0x80, 0xFF, 0x81, 0xFF,
0x01, 0xFE, 0x01, 0xFE, 0x03, 0xFC, 0x03, 0xFC, 0x07, 0xF8, 0x07, 0xFC,
0x1F, 0xF0, 0x0F, 0xF8, 0x3F, 0xC0, 0x0F, 0xF0, 0x7F, 0x80, 0x1F, 0xE0,
0xFF, 0x00, 0x3F, 0xE3, 0xFC, 0x00, 0x3F, 0xC7, 0xF8, 0x00, 0x7F, 0x8F,
0xF0, 0x00, 0xFF, 0x9F, 0xE0, 0x00, 0xFF, 0x7F, 0x80, 0x01, 0xFE, 0xFF,
0x00, 0x03, 0xFD, 0xFE, 0x00, 0x07, 0xFF, 0xFC, 0x00, 0x07, 0xFF, 0xF0,
0x00, 0x0F, 0xFF, 0xE0, 0x00, 0x1F, 0xFF, 0xC0, 0x00, 0x1F, 0xFF, 0x00,
0x00, 0x3F, 0xFE, 0x00, 0x00, 0x7F, 0xFC, 0x00, 0x00, 0xFF, 0xF8, 0x00,
0x00, 0xFF, 0xE0, 0x00, 0x01, 0xFF, 0xC0, 0x00, 0x03, 0xFF, 0x80, 0x07,
0xFF, 0xFF, 0xFE, 0x0F, 0xFF, 0xFF, 0xFC, 0x1F, 0xFF, 0xFF, 0xF8, 0x3F,
0xFF, 0xFF, 0xF0, 0x7F, 0xFF, 0xFF, 0xE0, 0x00, 0x7F, 0xC0, 0x00, 0x00,
0xFF, 0x80, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x03, 0xFE, 0x00, 0x00, 0x07,
0xFC, 0x00, 0x00, 0x0F, 0xF8, 0x00, 0x3F, 0xFF, 0xFF, 0xF0, 0x7F, 0xFF,
0xFF, 0xE0, 0xFF, 0xFF, 0xFF, 0xC1, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF,
0xFF, 0x00, 0x03, 0xFE, 0x00, 0x00, 0x07, 0xFC, 0x00, 0x00, 0x0F, 0xF8,
0x00, 0x00, 0x1F, 0xF0, 0x00, 0x00, 0x3F, 0xE0, 0x00, 0x00, 0x7F, 0xC0,
0x00, 0x00, 0xFF, 0x80, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x03, 0xFE, 0x00,
0x00, 0x07, 0xFC, 0x00, 0x00, 0x0F, 0xF8, 0x00, 0x00, 0x1F, 0xF0, 0x00,
0x00, 0x3F, 0xE0, 0x00, 0x00, 0x7F, 0xC0, 0x00
};
const GFXglyph Antonio_SemiBold40pt7bGlyphs[] PROGMEM = {
{0, 1, 1, 17, 0, 0}, // 0x20 ' '
@ -1588,10 +1659,10 @@ const GFXglyph Antonio_SemiBold40pt7bGlyphs[] PROGMEM = {
{10963, 28, 67, 32, 2, -66}, // 0x58 'X'
{11198, 31, 67, 32, 1, -66}, // 0x59 'Y'
{11458, 23, 67, 27, 3, -66}, // 0x5A 'Z'
{11651, 17, 70, 26, 6, -66}, // 0x5B '['
{11800, 24, 67, 30, 3, -66}, // 0x5C '\'
{12001, 16, 70, 26, 4, -66}, // 0x5D ']'
{12141, 29, 35, 37, 4, -66}, // 0x5E '^'
{18021, 31, 69, 37, 2, -67}, // 0x5B '[' --> euro { 18290, 31, 69, 37, 2, -67 } was {11651, 17, 70, 26, 6, -66}
{11800, 24, 67, 30, 3, -66}, // 0x5C '\'
{18557, 30, 68, 36, 3, -67 }, // 0x5D ']' --> pound { 0, 30, 68, 36, 3, -67 } was {12001, 16, 70, 26, 4, -66}
{18812, 31, 67, 32, 1, -66 }, // 0x5E '^' --> yen { 0, 31, 67, 32, 1, -66 } was {12141, 29, 35, 37, 4, -66
{12268, 25, 7, 29, 2, 2}, // 0x5F '_'
{12290, 12, 15, 16, 2, -77}, // 0x60 '`'
{12313, 27, 59, 36, 4, -57}, // 0x61 'a'
@ -1623,10 +1694,13 @@ const GFXglyph Antonio_SemiBold40pt7bGlyphs[] PROGMEM = {
{17509, 20, 75, 27, 4, -67}, // 0x7B '{'
{17697, 9, 75, 21, 6, -70}, // 0x7C '|'
{17782, 19, 75, 27, 4, -67}, // 0x7D '}'
{17961, 34, 14, 43, 4, -44}, {18021, 31, 69, 37, 2, -67}}; // 0x7E '~'
{17961, 34, 14, 43, 4, -44}}; // 0x7E '~'
//, {18021, 31, 69, 37, 2, -67}
const GFXfont Antonio_SemiBold40pt7b PROGMEM = {
(uint8_t *)Antonio_SemiBold40pt7bBitmaps,
(GFXglyph *)Antonio_SemiBold40pt7bGlyphs, 0x20, 0x7E, 101};
(GFXglyph *)Antonio_SemiBold40pt7bGlyphs, 0x20, 0x7E, 100};
// Approx. 18961 bytes

View file

@ -5029,7 +5029,231 @@ const uint8_t Antonio_SemiBold90pt7bBitmaps[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00,
0x00, 0x1F, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF,
0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00,
0x00};
0x00,
// Pound sign (start 60325)
0x00, 0x00, 0x00, 0x07, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F,
0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xE0,
0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x03,
0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x80, 0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x00,
0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x03, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xF0, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x07, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xE0, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x0F, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xF8, 0x00, 0x7F, 0xFF, 0xFF, 0x00, 0x1F, 0xFF, 0xFF, 0x80, 0x0F,
0xFF, 0xFF, 0x80, 0x00, 0xFF, 0xFF, 0xF0, 0x03, 0xFF, 0xFF, 0xE0, 0x00,
0x0F, 0xFF, 0xFE, 0x00, 0x7F, 0xFF, 0xF8, 0x00, 0x00, 0xFF, 0xFF, 0xE0,
0x0F, 0xFF, 0xFE, 0x00, 0x00, 0x1F, 0xFF, 0xFC, 0x01, 0xFF, 0xFF, 0xC0,
0x00, 0x01, 0xFF, 0xFF, 0x80, 0x7F, 0xFF, 0xF8, 0x00, 0x00, 0x3F, 0xFF,
0xF0, 0x0F, 0xFF, 0xFE, 0x00, 0x00, 0x07, 0xFF, 0xFE, 0x01, 0xFF, 0xFF,
0xC0, 0x00, 0x00, 0x7F, 0xFF, 0xC0, 0x3F, 0xFF, 0xF8, 0x00, 0x00, 0x0F,
0xFF, 0xFC, 0x07, 0xFF, 0xFF, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x80, 0xFF,
0xFF, 0xE0, 0x00, 0x00, 0x3F, 0xFF, 0xF0, 0x1F, 0xFF, 0xF8, 0x00, 0x00,
0x07, 0xFF, 0xFE, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xC0,
0x7F, 0xFF, 0xE0, 0x00, 0x00, 0x1F, 0xFF, 0xF8, 0x0F, 0xFF, 0xFC, 0x00,
0x00, 0x01, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x3F, 0xFF,
0xE0, 0x3F, 0xFF, 0xF0, 0x00, 0x00, 0x07, 0xFF, 0xFC, 0x07, 0xFF, 0xFE,
0x00, 0x00, 0x00, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x1F,
0xFF, 0xF0, 0x1F, 0xFF, 0xFC, 0x00, 0x00, 0x03, 0xFF, 0xFE, 0x03, 0xFF,
0xFF, 0x80, 0x00, 0x00, 0x7F, 0xFF, 0xC0, 0x7F, 0xFF, 0xF0, 0x00, 0x00,
0x0F, 0xFF, 0xF8, 0x0F, 0xFF, 0xFE, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x01,
0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x3F, 0xFF, 0xE0, 0x3F, 0xFF, 0xF8, 0x00,
0x00, 0x07, 0xFF, 0xFC, 0x03, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0xFF, 0xFF,
0x80, 0x7F, 0xFF, 0xF0, 0x00, 0x00, 0x1F, 0xFF, 0xF0, 0x0F, 0xFF, 0xFE,
0x00, 0x00, 0x03, 0xFF, 0xFE, 0x01, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x7F,
0xFF, 0xC0, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF,
0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xF0, 0x00, 0x00,
0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFC, 0x00,
0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00,
0x00, 0x7F, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x1F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF,
0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF8, 0x00, 0x00,
0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFC, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x3F, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x0F, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF,
0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF8, 0x00, 0x00,
0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xE0, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC,
0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFE, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x7F,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x03, 0xFF, 0xFF,
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xF0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x0F, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF,
0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFC, 0x00, 0x00,
0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
0x7F, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFE, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x1F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF,
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xF0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x0F, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF,
0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFC, 0x00, 0x00,
0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
0x7F, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFE, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x3F, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x1F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF,
0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xF0, 0x00, 0x00,
0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF8, 0x00,
0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFC,
0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF,
0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0x80, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F,
0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0x80, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x3F, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0x80,
0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF,
0x80, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF,
0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0x80, 0x00, 0x00,
0x00, 0x00, 0x01, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF,
0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFE, 0x00, 0x00,
0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x07,
0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x9F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF9, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,
0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xCF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xF9, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0,
// yen-sign (start 61607)
0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFE, 0xFF, 0xFF, 0xE0,
0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF9, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00,
0xFF, 0xFF, 0xF1, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xE3,
0xFF, 0xFF, 0x80, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x87, 0xFF, 0xFF, 0x00,
0x00, 0x00, 0x07, 0xFF, 0xFF, 0x0F, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x1F,
0xFF, 0xFE, 0x0F, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xF8, 0x1F,
0xFF, 0xFC, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xF0, 0x3F, 0xFF, 0xFC, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xE0, 0x3F, 0xFF, 0xF8, 0x00, 0x00, 0x03, 0xFF,
0xFF, 0xC0, 0x7F, 0xFF, 0xF0, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0x00, 0xFF,
0xFF, 0xE0, 0x00, 0x00, 0x0F, 0xFF, 0xFE, 0x01, 0xFF, 0xFF, 0xE0, 0x00,
0x00, 0x1F, 0xFF, 0xFC, 0x01, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x7F, 0xFF,
0xF8, 0x03, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0xFF, 0xFF, 0xE0, 0x07, 0xFF,
0xFF, 0x80, 0x00, 0x01, 0xFF, 0xFF, 0xC0, 0x07, 0xFF, 0xFF, 0x00, 0x00,
0x03, 0xFF, 0xFF, 0x80, 0x0F, 0xFF, 0xFE, 0x00, 0x00, 0x0F, 0xFF, 0xFE,
0x00, 0x1F, 0xFF, 0xFC, 0x00, 0x00, 0x1F, 0xFF, 0xFC, 0x00, 0x3F, 0xFF,
0xFC, 0x00, 0x00, 0x3F, 0xFF, 0xF8, 0x00, 0x3F, 0xFF, 0xF8, 0x00, 0x00,
0x7F, 0xFF, 0xF0, 0x00, 0x7F, 0xFF, 0xF0, 0x00, 0x01, 0xFF, 0xFF, 0xC0,
0x00, 0xFF, 0xFF, 0xE0, 0x00, 0x03, 0xFF, 0xFF, 0x80, 0x00, 0xFF, 0xFF,
0xE0, 0x00, 0x07, 0xFF, 0xFF, 0x00, 0x01, 0xFF, 0xFF, 0xC0, 0x00, 0x0F,
0xFF, 0xFE, 0x00, 0x03, 0xFF, 0xFF, 0x80, 0x00, 0x3F, 0xFF, 0xF8, 0x00,
0x07, 0xFF, 0xFF, 0x80, 0x00, 0x7F, 0xFF, 0xF0, 0x00, 0x07, 0xFF, 0xFF,
0x00, 0x00, 0xFF, 0xFF, 0xE0, 0x00, 0x0F, 0xFF, 0xFE, 0x00, 0x01, 0xFF,
0xFF, 0x80, 0x00, 0x1F, 0xFF, 0xFC, 0x00, 0x03, 0xFF, 0xFF, 0x00, 0x00,
0x1F, 0xFF, 0xFC, 0x00, 0x0F, 0xFF, 0xFE, 0x00, 0x00, 0x3F, 0xFF, 0xF8,
0x00, 0x1F, 0xFF, 0xFC, 0x00, 0x00, 0x7F, 0xFF, 0xF0, 0x00, 0x3F, 0xFF,
0xF0, 0x00, 0x00, 0x7F, 0xFF, 0xF0, 0x00, 0x7F, 0xFF, 0xE0, 0x00, 0x00,
0xFF, 0xFF, 0xE0, 0x01, 0xFF, 0xFF, 0xC0, 0x00, 0x01, 0xFF, 0xFF, 0xC0,
0x03, 0xFF, 0xFF, 0x80, 0x00, 0x03, 0xFF, 0xFF, 0x80, 0x07, 0xFF, 0xFE,
0x00, 0x00, 0x03, 0xFF, 0xFF, 0x80, 0x0F, 0xFF, 0xFC, 0x00, 0x00, 0x07,
0xFF, 0xFF, 0x00, 0x3F, 0xFF, 0xF8, 0x00, 0x00, 0x0F, 0xFF, 0xFE, 0x00,
0x7F, 0xFF, 0xE0, 0x00, 0x00, 0x0F, 0xFF, 0xFC, 0x00, 0xFF, 0xFF, 0xC0,
0x00, 0x00, 0x1F, 0xFF, 0xFC, 0x01, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x3F,
0xFF, 0xF8, 0x07, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xF0, 0x0F,
0xFF, 0xFC, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xF0, 0x1F, 0xFF, 0xF8, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xE0, 0x3F, 0xFF, 0xF0, 0x00, 0x00, 0x01, 0xFF,
0xFF, 0xC0, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0x81, 0xFF,
0xFF, 0x80, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x07, 0xFF, 0xFF, 0x07, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x0F, 0xFF,
0xFE, 0x1F, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFE, 0x3F, 0xFF,
0xF0, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFC, 0x7F, 0xFF, 0xE0, 0x00, 0x00,
0x00, 0x3F, 0xFF, 0xF8, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x3F, 0xFF,
0xF3, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xF7, 0xFF, 0xFE,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xEF, 0xFF, 0xFC, 0x00, 0x00, 0x00,
0x00, 0xFF, 0xFF, 0xDF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF,
0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0,
0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00,
0x07, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF,
0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xF8, 0x00,
0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00,
0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF,
0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x01,
0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF,
0xF0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00,
0x00, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x0F,
0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFE,
0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x00,
0x00, 0x00, 0x3F, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F,
0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xC0,
0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00,
0x00, 0x01, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF,
0xFF, 0xFC, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFC, 0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x7F,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xE0, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xC0, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x07, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFE, 0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC,
0x00, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x7F, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFE, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x7F, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xF0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x03, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07,
0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F,
0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xF0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x03, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xE0, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x03, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x07, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,
0x00, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x3F, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xF0, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0,
0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00,
0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xF8,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0x80,
0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x1F, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F,
0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xF8, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF,
0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0x80, 0x00,
0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x1F, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF,
0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xF8, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF,
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0x80, 0x00, 0x00,
0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x1F, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFC,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xF8, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xC0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00,
0x00, 0x00, 0x0F, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00
};
const GFXglyph Antonio_SemiBold90pt7bGlyphs[] PROGMEM = {
{0, 1, 1, 38, 0, 0}, // 0x20 ' '
@ -5046,7 +5270,7 @@ const GFXglyph Antonio_SemiBold90pt7bGlyphs[] PROGMEM = {
{9988, 53, 55, 63, 5, -94}, // 0x2B '+'
{10353, 23, 50, 41, 10, -20}, // 0x2C ','
{10497, 39, 14, 60, 8, -75}, // 0x2D '-'
{10566, 17, 19, 46, 14, -18}, // 0x2E '.'
{10566, 17, 19, 46, 14, 106}, // 0x2E '.'
{10607, 55, 152, 66, 6, 105}, // 0x2F '/'
{11652, 59, 155, 79, 10, 104}, // 0x30 '0'
{12796, 41, 151, 67, 8, 106}, // 0x31 '1'
@ -5090,11 +5314,16 @@ const GFXglyph Antonio_SemiBold90pt7bGlyphs[] PROGMEM = {
{53398, 108, 151, 116, 4, 106}, // 0x57 'W'
{55437, 64, 151, 72, 4, 106}, // 0x58 'X'
{56645, 71, 151, 73, 1, 106}, // 0x59 'Y'
{57986, 52, 151, 61, 6, 106},
{58968, 70, 155, 84, 5, 104}}; // 0x5A 'Z'
{57986, 52, 151, 61, 6, 106}, // 0x5A 'Z'
{58968, 70, 155, 84, 5, 104}, // Euro-sign as [
{0, 0, 0, 0, 0, 0}, // Skip backslash
{60325, 67, 153, 82, 7, 104 }, // Pound-sign as ]
{61607, 71, 151, 73, 1, 106 }, // Yen-sign as ^
};
const GFXfont Antonio_SemiBold90pt7b PROGMEM = {
(uint8_t *)Antonio_SemiBold90pt7bBitmaps,
(GFXglyph *)Antonio_SemiBold90pt7bGlyphs, 0x20, 0x5B, 228};
(GFXglyph *)Antonio_SemiBold90pt7bGlyphs, 0x20, 0x5E, 231};
// Approx. 60745 bytes

View file

@ -1,9 +1,12 @@
#pragma once
#include "antonio-semibold20.h"
#include "antonio-semibold30.h"
//#include "antonio-semibold30.h"
#include "antonio-semibold40.h"
#include "antonio-semibold90.h"
#include "sats-symbol.h"
//#include "icons.h"
// #include "oswald-20.h"
// #include "oswald-30.h"
// #include "oswald-90.h"

201
src/fonts/sats-symbol.h Normal file
View file

@ -0,0 +1,201 @@
const uint8_t Satoshi_Symbol90pt7bBitmaps[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x0F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F,
0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFE, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x0F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF,
0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF8, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFE, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0F, 0xFE, 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, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
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, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 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,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 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,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 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,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 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,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 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, 0x80, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF,
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFE,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xE0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x0F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 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, 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, 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, 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, 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, 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, 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 = {
{ 0, 82, 127, 99, 8, -126 }, { 1302, 71, 109, 93, 0, -117 } }; // 0x53 'S'
const GFXfont Satoshi_Symbol90pt7b PROGMEM = {
(uint8_t *)Satoshi_Symbol90pt7bBitmaps,
(GFXglyph *)Satoshi_Symbol90pt7bGlyphs,
0x53, 0x53, 192 };
// Approx. 2284 bytes

542
src/icons/icons.cpp Normal file
View file

@ -0,0 +1,542 @@
#include "icons.h"
// 'lightning-bolt', 122x122px
const unsigned char epd_icons_lightning_bolt [] PROGMEM = {
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0
};
// 'rocket-launch', 122x122px
const unsigned char epd_icons_rocket_launch [] PROGMEM = {
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x01, 0xf8, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x07, 0xfe, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x0f, 0xff, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x1f, 0xff, 0x80, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x1f, 0xff, 0x80, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3f, 0xff, 0xc0, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x3f, 0xff, 0xc0, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x3f, 0xff, 0xc0, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x3f, 0xff, 0xc0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xfe, 0xf8, 0x00, 0x00, 0x3f, 0xff, 0xc0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xf1, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xc0, 0x01, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xc1, 0xf0, 0x00, 0x00, 0x3f, 0xff, 0xc0, 0x01, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0x03, 0xe0, 0x00, 0x00, 0x1f, 0xff, 0x80, 0x01, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xfc, 0x03, 0xc0, 0x00, 0x00, 0x0f, 0xff, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xf0, 0x07, 0xc0, 0x00, 0x00, 0x07, 0xfe, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0x80, 0x0f, 0x80, 0x00, 0x00, 0x03, 0xfc, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xfe, 0x00, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xf8, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xe0, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0x80, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0x80, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xe0, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xf8, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xc0, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xf0, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xfc, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xf8, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xf0, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xe0, 0x7e, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xc0, 0xff, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0x81, 0xff, 0x80, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0x03, 0xff, 0xc0, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xfe, 0x07, 0xe7, 0xe0, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xfc, 0x0f, 0xc3, 0xf0, 0x00, 0x00, 0xff, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xf8, 0x1f, 0x81, 0xf8, 0x00, 0x03, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xf0, 0x3f, 0x00, 0xfc, 0x00, 0x0f, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xe0, 0x7e, 0x00, 0x7e, 0x00, 0x3f, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xc0, 0xfc, 0x00, 0x3f, 0x80, 0xff, 0x81, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0x81, 0xf8, 0x00, 0x7f, 0xff, 0xfe, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0x03, 0xf0, 0x00, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0x07, 0xe0, 0x01, 0xf3, 0xff, 0xe0, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0x0f, 0xc0, 0x03, 0xe0, 0xff, 0x80, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0x1f, 0x80, 0x07, 0xc0, 0xff, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0x3f, 0x00, 0x0f, 0x81, 0xff, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0x7e, 0x00, 0x1f, 0x03, 0xff, 0x80, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xfc, 0x00, 0x3e, 0x07, 0xff, 0x80, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xf8, 0x00, 0x7c, 0x0f, 0xff, 0x80, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xf0, 0x00, 0xf8, 0x1f, 0xff, 0xc0, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xe0, 0x01, 0xf0, 0x3f, 0xff, 0xc0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xc0, 0x03, 0xe0, 0x7f, 0xff, 0xe0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0x80, 0x07, 0xc0, 0xff, 0xff, 0xe0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0x00, 0x0f, 0x81, 0xff, 0xff, 0xf0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0x00, 0x1f, 0x03, 0xff, 0xff, 0xf0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0x00, 0x3e, 0x07, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0x00, 0x7c, 0x0f, 0xff, 0xff, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0x00, 0xf8, 0x1f, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0x01, 0xf0, 0x3f, 0xff, 0xff, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0
};
// 'pickaxe', 122x122px
const unsigned char epd_icons_pickaxe [] PROGMEM = {
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xe7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xc1, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xfb, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x31, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0xc7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xe0, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x7f, 0xf8, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x3f, 0xfd, 0x80, 0x03, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x1f, 0xff, 0xc0, 0x03, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x0f, 0xff, 0xe0, 0x01, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x07, 0xff, 0xf0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x07, 0xff, 0xf8, 0x00, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0f, 0xff, 0xf8, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x1f, 0xff, 0xfc, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x3f, 0xff, 0xfe, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x7f, 0xff, 0xfe, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0xff, 0xff, 0xff, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x01, 0xff, 0xff, 0xff, 0x80, 0x1f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x03, 0xff, 0xff, 0xff, 0x80, 0x0f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x07, 0xff, 0xff, 0xff, 0xc0, 0x0f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xe0, 0x0f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf0, 0x07, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xf8, 0x0f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xe0, 0x01, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xc0, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0x80, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xfe, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xfc, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xf8, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xf0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xe0, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xc0, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0x80, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xfe, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xfc, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xf8, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xf0, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xe0, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xc0, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0x80, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xfe, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xfc, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xf8, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xf0, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xe0, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xc0, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0x80, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0x80, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xc0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xe0, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xf9, 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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
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, 0xff, 0xff, 0xff, 0xc0,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0
};
const unsigned char epd_icons_bitaxe_logo [] PROGMEM = {
// 'bitaxe_dark copy', 88x220px
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x3e, 0xff, 0xff, 0xfd, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0xff, 0xff, 0xfc, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xfe,
0x00, 0xff, 0xff, 0xfe, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x7f, 0xff, 0xff, 0x00,
0x07, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x3f, 0xff, 0xff, 0xc0, 0x07, 0xff, 0xff, 0xff, 0xff,
0xf8, 0x00, 0x3f, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x1f, 0xff, 0xff,
0xf0, 0x03, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x0f, 0xff, 0xff, 0xf8, 0x03, 0xff, 0xff, 0xff,
0xff, 0xf0, 0x00, 0x07, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x03, 0xff,
0xff, 0xfc, 0x00, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xfc, 0x00, 0x7f, 0xff,
0xff, 0xff, 0xe0, 0x07, 0xf8, 0x7f, 0xff, 0xfd, 0xfc, 0x3f, 0xff, 0xff, 0xff, 0xe0, 0x07, 0xfe,
0x3f, 0xff, 0xff, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0xc0, 0x0f, 0xff, 0x1f, 0xff, 0xff, 0xf8, 0x07,
0xff, 0xff, 0xff, 0xc0, 0x0f, 0xff, 0xcf, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xff, 0xff, 0x80, 0x0f,
0xff, 0xe7, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xff, 0xff, 0x80, 0x1f, 0xff, 0xf3, 0xff, 0xff, 0xf0,
0x01, 0xff, 0xff, 0xff, 0xc0, 0x1f, 0xff, 0xf3, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf0,
0x1f, 0xff, 0xf9, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xf8, 0x3f, 0xff, 0xfd, 0xff, 0xff,
0xe0, 0x07, 0xff, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xfd, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xff,
0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00,
0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff,
0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00,
0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff,
0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f,
0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xfc, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0xff, 0xff, 0xfe,
0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 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, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xfd, 0xff, 0xff, 0xfe, 0x7f,
0xff, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xfc, 0x7f, 0xff, 0xff,
0xff, 0xfc, 0x00, 0x00, 0x7f, 0x3f, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x7e,
0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x0f, 0xff,
0xff, 0xff, 0xf0, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00,
0x3c, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x03,
0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0x80, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0f, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xf8,
0x1f, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xfc, 0x1f, 0xff, 0xf0, 0x03, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x07,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xc0, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80,
0x0f, 0xff, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xfd, 0xff, 0xff, 0x80, 0x1f, 0xff, 0xfc, 0xff, 0xff,
0xff, 0xff, 0xfc, 0xff, 0xff, 0x00, 0x1f, 0xff, 0xfc, 0x7f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00,
0x00, 0x3f, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f,
0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xf0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00,
0x01, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xc0,
0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0xf8, 0x00,
0x00, 0x0f, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x01, 0xf8, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff,
0xe0, 0x0f, 0xff, 0xf1, 0xf8, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xf8, 0x1f, 0xff, 0xf9, 0xfc,
0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xfc, 0x1f, 0xff, 0xf9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0x3f, 0xff, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff,
0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xfd, 0xff, 0xff, 0xfc, 0x7f, 0xff, 0xff, 0xff, 0xfe, 0x3f,
0xff, 0xf8, 0xff, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x1f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xfc,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff,
0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff,
0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xf7, 0xff, 0xff, 0xfc, 0x3f, 0xff,
0xff, 0xff, 0xe0, 0x07, 0xff, 0xf3, 0xff, 0xff, 0xfc, 0x1f, 0xff, 0xff, 0xff, 0xc0, 0x0f, 0xff,
0xf3, 0xff, 0xff, 0xf8, 0x07, 0xff, 0xff, 0xff, 0xc0, 0x0f, 0xff, 0xf3, 0xff, 0xff, 0xf8, 0x01,
0xff, 0xff, 0xff, 0x80, 0x0f, 0xff, 0xf9, 0xff, 0xff, 0xf0, 0x01, 0xff, 0xff, 0xff, 0x80, 0x1f,
0xff, 0xf9, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xff, 0xff, 0xc0, 0x1f, 0xff, 0xfc, 0xff, 0xff, 0xf0,
0x03, 0xff, 0xff, 0xff, 0xf0, 0x1f, 0xff, 0xfe, 0x3f, 0xff, 0xe0, 0x03, 0xff, 0xff, 0xff, 0xf8,
0x3f, 0xff, 0x7e, 0x1f, 0xff, 0xe0, 0x07, 0xff, 0xff, 0xff, 0xfe, 0x3f, 0xfc, 0xff, 0x03, 0xff,
0xc0, 0x07, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0x80, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff,
0xff, 0x80, 0x01, 0xff, 0xc0, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0x80, 0x03, 0xff, 0xe0,
0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x0f, 0xff, 0xe0, 0x00, 0x00, 0x1f, 0xff, 0xff,
0xff, 0xff, 0xe0, 0x1f, 0xff, 0xf0, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x3f, 0xff,
0xf8, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x7f, 0xff, 0xf8, 0x00, 0x00, 0x3f, 0xff,
0xff, 0xff, 0xff, 0xf0, 0x7f, 0xff, 0xf8, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xf0, 0xff,
0xff, 0xfc, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xf0, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x7f,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
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, 0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x8f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0xff, 0xff, 0xff, 0xff, 0x87, 0xff, 0xff, 0xff, 0xff,
0xff, 0xfc, 0xff, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xfd, 0xff, 0xff, 0xfc, 0x7f, 0xff, 0xff,
0xff, 0x83, 0xff, 0xff, 0xf8, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff,
0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0f, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0x80,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xf8, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0x83, 0xff, 0xff, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xc3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xfd, 0xff, 0xff,
0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xf9, 0xff, 0xff, 0xf8, 0x7f,
0xff, 0xf9, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xf0, 0xff, 0xf8, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xe0, 0x7f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x07, 0xff, 0xc0, 0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0x80, 0x1f, 0xf0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0x80, 0x1f, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x03, 0xff, 0xc0, 0x3f, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xe0, 0x3f,
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xf0, 0x7f, 0x80, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x1f, 0xff, 0xf9, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff,
0xff, 0xf0, 0x1f, 0xff, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x1f, 0xff, 0xfd,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 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, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x3f, 0xff, 0xff, 0xf3, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xfe, 0x3f, 0xff, 0xff, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x1f, 0xff,
0xff, 0xfc, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x1f, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xff,
0xef, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x03, 0xf8, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xfc, 0x00,
0x00, 0x01, 0xf8, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x01, 0xf8, 0x00, 0x00,
0x00, 0x07, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xf8,
0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x60, 0x00,
0x00, 0x00, 0x03, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff,
0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f,
0xff, 0xe0, 0x07, 0xff, 0xff, 0x80, 0x0f, 0xff, 0xff, 0xfc, 0x3f, 0xff, 0xe0, 0x07, 0xff, 0xff,
0x80, 0x1f, 0xff, 0xff, 0xfc, 0x1f, 0xff, 0xc0, 0x07, 0xff, 0xff, 0x80, 0x1f, 0xff, 0xff, 0xf8,
0x0f, 0xff, 0xc0, 0x0f, 0xff, 0xff, 0x00, 0x1f, 0xff, 0xff, 0xf8, 0x07, 0xff, 0x80, 0x0f, 0xff,
0xff, 0x00, 0x3f, 0xff, 0xff, 0xf8, 0x03, 0xff, 0x80, 0x1f, 0xff, 0xff, 0x80, 0x3f, 0xff, 0xff,
0xf0, 0x01, 0xff, 0xc0, 0x1f, 0xff, 0xff, 0xc0, 0x3f, 0xff, 0xff, 0xf0, 0x01, 0xff, 0xf0, 0x1f,
0xff, 0xff, 0xf0, 0x7f, 0xff, 0xff, 0xf0, 0x03, 0xff, 0xf8, 0x3f, 0xff, 0xff, 0xf8, 0x7f, 0xff,
0xff, 0xe0, 0x03, 0xff, 0xfc, 0x3f, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xff, 0xe0, 0x07, 0xff, 0xfe,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x07, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff,
0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f,
0xff, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xe0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x7f, 0xff, 0x80, 0x00, 0xff, 0xff, 0xff, 0xe0, 0x3f, 0xfe, 0x00, 0x7f, 0xff, 0x80, 0x03, 0xff,
0xff, 0xff, 0xf8, 0xff, 0xfe, 0x00, 0xff, 0xff, 0xc0, 0x07, 0xff, 0xff, 0xff, 0xfd, 0xff, 0xfe,
0x00, 0xff, 0xff, 0xc0, 0x0f, 0xff, 0xff, 0xff, 0xfd, 0xff, 0xfe, 0x00, 0xff, 0xff, 0xe0, 0x1f,
0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x01, 0xff, 0xff, 0xf0, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff,
0xfe, 0x01, 0xff, 0xff, 0xf8, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x01, 0xff, 0xff, 0xfc,
0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x01, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff,
0xff, 0xfe, 0x03, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xff, 0xff,
0xff, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x03, 0xff, 0xff, 0xff, 0x9f, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0x03, 0xff, 0xff, 0xff, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x81, 0xff,
0xff, 0xff, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x81, 0xff, 0xff, 0xff, 0xf9, 0xff, 0xff,
0xbf, 0xff, 0xff, 0xff, 0xc1, 0xff, 0xff, 0xff, 0xfc, 0xff, 0xff, 0xbf, 0xff, 0xff, 0xff, 0xe0,
0xff, 0xff, 0xff, 0xff, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x7f, 0xff, 0xff, 0xff, 0x9f,
0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x3f, 0xe7, 0xff, 0xff, 0xe7, 0xfd, 0xff, 0xff, 0xff, 0xff,
0xfe, 0x00, 0x1f, 0xff, 0xff, 0xfc, 0x73, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x7f, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff
};
// Array of all bitmaps for convenience. (Total bytes used to store images in PROGMEM = 8032)
const int epd_icons_allArray_LEN = 4;
const unsigned char* epd_icons_allArray[epd_icons_allArray_LEN] = {
epd_icons_pickaxe,
epd_icons_rocket_launch,
epd_icons_lightning_bolt,
epd_icons_bitaxe_logo
};

10
src/icons/icons.h Normal file
View file

@ -0,0 +1,10 @@
#pragma once
#ifndef ICONS_H
#define ICONS_H
#include <Arduino.h>
extern const unsigned char* epd_icons_allArray[];
#endif // ICONS_H

View file

@ -1,6 +1,3 @@
dependencies:
# Required IDF version
idf: ">=4.4"
esp_littlefs:
git: https://github.com/joltwallet/esp_littlefs.git
idf: ">=4.4"

61
src/lib/bitaxe_fetch.cpp Normal file
View file

@ -0,0 +1,61 @@
#include "bitaxe_fetch.hpp"
TaskHandle_t bitaxeFetchTaskHandle;
std::string bitaxeHashrate;
std::string bitaxeBestDiff;
std::string getBitAxeHashRate()
{
return bitaxeHashrate;
}
std::string getBitaxeBestDiff()
{
return bitaxeBestDiff;
}
void taskBitaxeFetch(void *pvParameters)
{
for (;;)
{
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
HTTPClient http;
http.setUserAgent(USER_AGENT);
String bitaxeApiUrl = "http://" + preferences.getString("bitaxeHostname", DEFAULT_BITAXE_HOSTNAME) + "/api/system/info";
http.begin(bitaxeApiUrl.c_str());
int httpCode = http.GET();
if (httpCode == 200)
{
String payload = http.getString();
JsonDocument doc;
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 && (ScreenHandler::getCurrentScreen() == SCREEN_BITAXE_HASHRATE || ScreenHandler::getCurrentScreen() == SCREEN_BITAXE_BESTDIFF))
{
WorkItem priceUpdate = {TASK_BITAXE_UPDATE, 0};
xQueueSend(workQueue, &priceUpdate, portMAX_DELAY);
}
}
else
{
Serial.print(
F("Error retrieving BitAxe data. HTTP status code: "));
Serial.println(httpCode);
Serial.println(bitaxeApiUrl);
}
}
}
void setupBitaxeFetchTask()
{
xTaskCreate(taskBitaxeFetch, "bitaxeFetch", (3 * 1024), NULL, tskIDLE_PRIORITY,
&bitaxeFetchTaskHandle);
xTaskNotifyGive(bitaxeFetchTaskHandle);
}

15
src/lib/bitaxe_fetch.hpp Normal file
View file

@ -0,0 +1,15 @@
#pragma once
#include <Arduino.h>
#include <HTTPClient.h>
#include "lib/config.hpp"
#include "lib/shared.hpp"
extern TaskHandle_t bitaxeFetchTaskHandle;
void setupBitaxeFetchTask();
void taskBitaxeFetch(void *pvParameters);
std::string getBitAxeHashRate();
std::string getBitaxeBestDiff();

View file

@ -2,65 +2,62 @@
char *wsServer;
esp_websocket_client_handle_t blockNotifyClient = NULL;
uint currentBlockHeight = 816000;
uint currentBlockHeight = 873400;
uint blockMedianFee = 1;
bool blockNotifyInit = false;
unsigned long int lastBlockUpdate;
// const char *mempoolWsCert = R"(-----BEGIN CERTIFICATE-----
// MIIHfTCCBmWgAwIBAgIRANFX3mhqRYDt1NFuENoSyaAwDQYJKoZIhvcNAQELBQAw
// gZUxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO
// BgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoTD1NlY3RpZ28gTGltaXRlZDE9MDsGA1UE
// AxM0U2VjdGlnbyBSU0EgT3JnYW5pemF0aW9uIFZhbGlkYXRpb24gU2VjdXJlIFNl
// cnZlciBDQTAeFw0yMzA3MjQwMDAwMDBaFw0yNDA4MjIyMzU5NTlaMFcxCzAJBgNV
// BAYTAkpQMQ4wDAYDVQQIEwVUb2t5bzEgMB4GA1UEChMXTUVNUE9PTCBTUEFDRSBD
// Ty4sIExURC4xFjAUBgNVBAMTDW1lbXBvb2wuc3BhY2UwggEiMA0GCSqGSIb3DQEB
// AQUAA4IBDwAwggEKAoIBAQCqmiPRWgo58d25R0biQjAksXMq5ciH7z7ZQo2w2AbB
// rHxpnlIry74b9S4wRY5UJeYmd6ZwA76NdSioDvxTJc29bLplY+Ftmfc4ET0zYb2k
// Fi86z7GOWb6Ezor/qez9uMM9cxd021Bvcs0/2OrL6Sgp66u9keDZv9NyvFPpXfuR
// tdV2r4HF57VJqZn105PN4k80kNWgDbae8aw+BuUNvQYKEe71yfB7Bh6zSh9pCSfM
// I6pIJdQzoada2uY1dQMoJeIq8qKNKqAPKGsH5McemUT5ZIKU/tjk3nfX0pz/sQa4
// CN7tLH6UeUlctei92GFd6Xtn7RbKLhDUbc4Sq02Cc9iXAgMBAAGjggQDMIID/zAf
// BgNVHSMEGDAWgBQX2dYlJ2f5McJJQ9kwNkSMbKlP6zAdBgNVHQ4EFgQUXkxoddJ6
// rKobsbmDdtuCK1ywXuIwDgYDVR0PAQH/BAQDAgWgMAwGA1UdEwEB/wQCMAAwHQYD
// VR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEoGA1UdIARDMEEwNQYMKwYBBAGy
// MQECAQMEMCUwIwYIKwYBBQUHAgEWF2h0dHBzOi8vc2VjdGlnby5jb20vQ1BTMAgG
// BmeBDAECAjBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLnNlY3RpZ28uY29t
// L1NlY3RpZ29SU0FPcmdhbml6YXRpb25WYWxpZGF0aW9uU2VjdXJlU2VydmVyQ0Eu
// Y3JsMIGKBggrBgEFBQcBAQR+MHwwVQYIKwYBBQUHMAKGSWh0dHA6Ly9jcnQuc2Vj
// dGlnby5jb20vU2VjdGlnb1JTQU9yZ2FuaXphdGlvblZhbGlkYXRpb25TZWN1cmVT
// ZXJ2ZXJDQS5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29t
// MIIBgAYKKwYBBAHWeQIEAgSCAXAEggFsAWoAdwB2/4g/Crb7lVHCYcz1h7o0tKTN
// uyncaEIKn+ZnTFo6dAAAAYmc9m/gAAAEAwBIMEYCIQD8XOozx411S/bnZambGjTB
// yTcr2fCmggUfQLSmqksD5gIhAIjiEMg0o1VSuQW31gWzfzL6idCkIZeSKN104cdp
// xa4SAHcA2ra/az+1tiKfm8K7XGvocJFxbLtRhIU0vaQ9MEjX+6sAAAGJnPZwPwAA
// BAMASDBGAiEA2sPTZTzvxewzQ8vk36+BWAKuJS7AvJ5W3clvfwCa8OUCIQC74ekT
// Ged2fqQE4sVy74aS6HRA2ihC9VLtNrASJx1YjQB2AO7N0GTV2xrOxVy3nbTNE6Iy
// h0Z8vOzew1FIWUZxH7WbAAABiZz2cA8AAAQDAEcwRQIgEklH7wYCFuuJIFUHX5PY
// /vZ3bDoxOp+061PT3caa+rICIQC0abgfGlBKiHxp47JZxnW3wcVqWdiYX4ViLm9H
// xfx4ljCBxgYDVR0RBIG+MIG7gg1tZW1wb29sLnNwYWNlghMqLmZtdC5tZW1wb29s
// LnNwYWNlghMqLmZyYS5tZW1wb29sLnNwYWNlgg8qLm1lbXBvb2wuc3BhY2WCEyou
// dGs3Lm1lbXBvb2wuc3BhY2WCEyoudmExLm1lbXBvb2wuc3BhY2WCDGJpc3EubWFy
// a2V0c4IKYmlzcS5uaW5qYYIObGlxdWlkLm5ldHdvcmuCDGxpcXVpZC5wbGFjZYIN
// bWVtcG9vbC5uaW5qYTANBgkqhkiG9w0BAQsFAAOCAQEAFvOSRnlHDfq9C8acjZEG
// 5XIqjNYigyWyjOvx83of6Z3PBKkAZB5D/UHBPp+jBDJiEb/QXC7Z7Y7kpuvnoVib
// b4jDc0RjGEsxL+3F7cSw26m3wILJhhHooGZRmFY4GOAeCZtYCOTzJsiZvFpDoQjU
// hTBxtaps05z0Ly9/eYvkXnjnBNROZJVR+KYHlq4TIoGNc4q4KvpfHv2I/vhS2M1e
// bECNNPEyRxHGKdXXO3huocE7aVKpy+JDR6cWwDu6hpdc1j/SCDqdTDFQ7McHOrqA
// fpPh4FcfePMh7Mqxtg2pSs5pXPtiP0ZjLgxd7HbAXct8Y+/jGk+k3sx3SeYXVimr
// ew==
// -----END CERTIFICATE-----)";
void setupBlockNotify() {
// currentBlockHeight = preferences.getUInt("blockHeight", 816000);
const char *mempoolWsCert = R"EOF(
-----BEGIN CERTIFICATE-----
MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB
iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV
BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw
MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV
BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU
aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy
dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK
AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B
3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY
tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/
Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2
VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT
79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6
c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT
Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l
c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee
UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE
Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd
BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G
A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF
Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO
VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3
ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs
8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR
iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze
Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ
XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/
qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB
VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB
L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG
jjxDah2nGN59PRbxYvnKkKj9
-----END CERTIFICATE-----
)EOF";
void setupBlockNotify()
{
IPAddress result;
int dnsErr = -1;
String mempoolInstance =
preferences.getString("mempoolInstance", DEFAULT_MEMPOOL_INSTANCE);
while (dnsErr != 1) {
while (dnsErr != 1 && !strchr(mempoolInstance.c_str(), ':'))
{
dnsErr = WiFi.hostByName(mempoolInstance.c_str(), result);
if (dnsErr != 1) {
if (dnsErr != 1)
{
Serial.print(mempoolInstance);
Serial.println(F("mempool DNS could not be resolved"));
WiFi.reconnect();
@ -69,119 +66,260 @@ void setupBlockNotify() {
}
// Get current block height through regular API
HTTPClient *http = new HTTPClient();
http->begin("https://" + mempoolInstance + "/api/blocks/tip/height");
int httpCode = http->GET();
int blockFetch = getBlockFetch();
if (httpCode > 0 && httpCode == HTTP_CODE_OK) {
String blockHeightStr = http->getString();
currentBlockHeight = blockHeightStr.toInt();
// xTaskNotifyGive(blockUpdateTaskHandle);
if (workQueue != nullptr) {
WorkItem blockUpdate = {TASK_BLOCK_UPDATE, 0};
xQueueSend(workQueue, &blockUpdate, portMAX_DELAY);
}
if (blockFetch > currentBlockHeight)
currentBlockHeight = blockFetch;
if (currentBlockHeight != -1)
{
lastBlockUpdate = esp_timer_get_time() / 1000000;
}
if (workQueue != nullptr)
{
WorkItem blockUpdate = {TASK_BLOCK_UPDATE, 0};
xQueueSend(workQueue, &blockUpdate, portMAX_DELAY);
}
// 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 = {
.uri = "wss://mempool.space/api/v1/ws",
// .task_stack = (6*1024),
// .cert_pem = mempoolWsCert,
.user_agent = USER_AGENT,
// .uri = "wss://mempool.space/api/v1/ws",
.task_stack = (6*1024),
.user_agent = USER_AGENT
};
if (preferences.getBool("mempoolSecure", DEFAULT_MEMPOOL_SECURE)) {
config.cert_pem = mempoolWsCert;
}
config.uri = mempoolUri.c_str();
Serial.printf("Connecting to %s\r\n", preferences.getString("mempoolInstance", DEFAULT_MEMPOOL_INSTANCE));
blockNotifyClient = esp_websocket_client_init(&config);
esp_websocket_register_events(blockNotifyClient, WEBSOCKET_EVENT_ANY,
onWebsocketEvent, blockNotifyClient);
onWebsocketBlockEvent, blockNotifyClient);
esp_websocket_client_start(blockNotifyClient);
}
void onWebsocketEvent(void *handler_args, esp_event_base_t base,
int32_t event_id, void *event_data) {
void onWebsocketBlockEvent(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;
const String sub = "{\"action\": \"want\", \"data\":[\"blocks\"]}";
switch (event_id) {
case WEBSOCKET_EVENT_CONNECTED:
Serial.println(F("Connected to Mempool.space WebSocket"));
const String sub = "{\"action\": \"want\", \"data\":[\"blocks\", \"mempool-blocks\"]}";
switch (event_id)
{
case WEBSOCKET_EVENT_CONNECTED:
blockNotifyInit = true;
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"));
}
Serial.println(F("Connected to Mempool.space WebSocket"));
break;
case WEBSOCKET_EVENT_DATA:
onWebsocketMessage(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;
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 onWebsocketMessage(esp_websocket_event_data_t *event_data) {
SpiRamJsonDocument doc(event_data->data_len);
void onWebsocketBlockMessage(esp_websocket_event_data_t *event_data)
{
JsonDocument doc;
deserializeJson(doc, (char *)event_data->data_ptr);
JsonDocument filter;
filter["block"]["height"] = true;
filter["mempool-blocks"][0]["medianFee"] = true;
if (doc.containsKey("block")) {
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"];
currentBlockHeight = block["height"].as<uint>();
Serial.printf("New block found: %d\r\n", block["height"].as<uint>());
preferences.putUInt("blockHeight", currentBlockHeight);
if (workQueue != nullptr) {
WorkItem blockUpdate = {TASK_BLOCK_UPDATE, 0};
xQueueSend(workQueue, &blockUpdate, portMAX_DELAY);
// xTaskNotifyGive(blockUpdateTaskHandle);
if (getCurrentScreen() != SCREEN_BLOCK_HEIGHT &&
preferences.getBool("stealFocus", true)) {
uint64_t timerPeriod = 0;
if (isTimerActive()) {
// store timer periode before making inactive to prevent artifacts
timerPeriod = getTimerSeconds();
esp_timer_stop(screenRotateTimer);
}
setCurrentScreen(SCREEN_BLOCK_HEIGHT);
if (timerPeriod > 0) {
esp_timer_start_periodic(screenRotateTimer,
timerPeriod * usPerSecond);
}
}
if (getCurrentScreen() == SCREEN_BLOCK_HEIGHT &&
preferences.getBool("ledFlashOnUpd", false)) {
vTaskDelay(pdMS_TO_TICKS(250)); // Wait until screens are updated
queueLedEffect(LED_FLASH_BLOCK_NOTIFY);
}
if (block["height"].as<uint>() == currentBlockHeight) {
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;
// Serial.printf("New block found: %d\r\n", block["height"].as<uint>());
preferences.putUInt("blockHeight", currentBlockHeight);
lastBlockUpdate = esp_timer_get_time() / 1000000;
if (workQueue != nullptr)
{
WorkItem blockUpdate = {TASK_BLOCK_UPDATE, 0};
xQueueSend(workQueue, &blockUpdate, portMAX_DELAY);
// xTaskNotifyGive(blockUpdateTaskHandle);
if (ScreenHandler::getCurrentScreen() != SCREEN_BLOCK_HEIGHT &&
preferences.getBool("stealFocus", DEFAULT_STEAL_FOCUS))
{
uint64_t timerPeriod = 0;
if (isTimerActive())
{
// store timer periode before making inactive to prevent artifacts
timerPeriod = getTimerSeconds();
esp_timer_stop(screenRotateTimer);
}
ScreenHandler::setCurrentScreen(SCREEN_BLOCK_HEIGHT);
if (timerPeriod > 0)
{
esp_timer_start_periodic(screenRotateTimer,
timerPeriod * usPerSecond);
}
vTaskDelay(pdMS_TO_TICKS(315*NUM_SCREENS)); // Extra delay because of screen switching
}
if (preferences.getBool("ledFlashOnUpd", DEFAULT_LED_FLASH_ON_UPD))
{
vTaskDelay(pdMS_TO_TICKS(250)); // Wait until screens are updated
queueLedEffect(LED_FLASH_BLOCK_NOTIFY);
}
}
}
void processNewBlockFee(uint newBlockFee) {
if (blockMedianFee == newBlockFee)
{
return;
}
// Serial.printf("New median fee: %d\r\n", medianFee);
blockMedianFee = newBlockFee;
if (workQueue != nullptr)
{
WorkItem blockUpdate = {TASK_FEE_UPDATE, 0};
xQueueSend(workQueue, &blockUpdate, portMAX_DELAY);
}
}
uint getBlockHeight() { return currentBlockHeight; }
void setBlockHeight(uint newBlockHeight) {
void setBlockHeight(uint newBlockHeight)
{
currentBlockHeight = newBlockHeight;
}
bool isBlockNotifyConnected() {
if (blockNotifyClient == NULL) return false;
uint getBlockMedianFee() { return blockMedianFee; }
void setBlockMedianFee(uint newBlockMedianFee)
{
blockMedianFee = newBlockMedianFee;
}
bool isBlockNotifyConnected()
{
if (blockNotifyClient == NULL)
return false;
return esp_websocket_client_is_connected(blockNotifyClient);
}
void stopBlockNotify() {
bool getBlockNotifyInit()
{
return blockNotifyInit;
}
void stopBlockNotify()
{
if (blockNotifyClient == NULL)
return;
esp_websocket_client_close(blockNotifyClient, pdMS_TO_TICKS(5000));
esp_websocket_client_stop(blockNotifyClient);
esp_websocket_client_destroy(blockNotifyClient);
blockNotifyClient = NULL;
}
void restartBlockNotify()
{
stopBlockNotify();
if (blockNotifyClient == NULL) {
setupBlockNotify();
return;
}
// esp_websocket_client_close(blockNotifyClient, pdMS_TO_TICKS(5000));
// esp_websocket_client_stop(blockNotifyClient);
// esp_websocket_client_start(blockNotifyClient);
}
int getBlockFetch() {
try {
String mempoolInstance = preferences.getString("mempoolInstance", DEFAULT_MEMPOOL_INSTANCE);
const String protocol = preferences.getBool("mempoolSecure", DEFAULT_MEMPOOL_SECURE) ? "https" : "http";
String url = protocol + "://" + mempoolInstance + "/api/blocks/tip/height";
HTTPClient* http = HttpHelper::begin(url);
Serial.println("Fetching block height from " + url);
int httpCode = http->GET();
if (httpCode > 0 && httpCode == HTTP_CODE_OK) {
String blockHeightStr = http->getString();
HttpHelper::end(http);
return blockHeightStr.toInt();
}
HttpHelper::end(http);
Serial.println("HTTP code" + String(httpCode));
} catch (...) {
Serial.println(F("An exception occurred while trying to get the latest block"));
}
return 2203; // B-T-C
}
uint getLastBlockUpdate()
{
return lastBlockUpdate;
}
void setLastBlockUpdate(uint lastUpdate)
{
lastBlockUpdate = lastUpdate;
}

View file

@ -11,17 +11,31 @@
#include "lib/led_handler.hpp"
#include "lib/screen_handler.hpp"
#include "lib/timers.hpp"
#include "lib/shared.hpp"
// using namespace websockets;
void setupBlockNotify();
void onWebsocketEvent(void *handler_args, esp_event_base_t base,
void onWebsocketBlockEvent(void *handler_args, esp_event_base_t base,
int32_t event_id, void *event_data);
void onWebsocketMessage(esp_websocket_event_data_t *event_data);
void onWebsocketBlockMessage(esp_websocket_event_data_t *event_data);
void setBlockHeight(uint newBlockHeight);
uint getBlockHeight();
void setBlockMedianFee(uint blockMedianFee);
uint getBlockMedianFee();
bool isBlockNotifyConnected();
void stopBlockNotify();
void stopBlockNotify();
void restartBlockNotify();
void processNewBlock(uint newBlockHeight);
void processNewBlockFee(uint newBlockFee);
bool getBlockNotifyInit();
uint getLastBlockUpdate();
int getBlockFetch();
void setLastBlockUpdate(uint lastUpdate);

View file

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

View file

@ -4,9 +4,51 @@
#include "lib/screen_handler.hpp"
#include "lib/shared.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);
void IRAM_ATTR handleButtonInterrupt();
void setupButtonTask();
class ButtonHandler {
private:
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
};

File diff suppressed because it is too large Load diff

View file

@ -1,55 +1,103 @@
#pragma once;
#include <Adafruit_MCP23X17.h>
#pragma once
#include <MCP23017.h>
#include <Arduino.h>
#include <Preferences.h>
#include <WiFiClientSecure.h>
#include <WiFiManager.h>
#include <base64.h>
#include <esp_task_wdt.h>
#include <nvs_flash.h>
#include <map>
#include "lib/block_notify.hpp"
#include "lib/button_handler.hpp"
#include "lib/epd.hpp"
#include "lib/improv.hpp"
// #include "lib/improv.hpp"
#include "lib/led_handler.hpp"
#include "lib/ota.hpp"
#include "lib/nostr_notify.hpp"
#include "lib/bitaxe_fetch.hpp"
#include "lib/mining_pool_stats_fetch.hpp"
#include "lib/v2_notify.hpp"
#include "lib/price_notify.hpp"
#include "lib/screen_handler.hpp"
#include "lib/shared.hpp"
#include "lib/webserver.hpp"
#ifdef HAS_FRONTLIGHT
#include "PCA9685.h"
#include "BH1750.h"
#endif
#include "shared.hpp"
#include "defaults.hpp"
#define NTP_SERVER "pool.ntp.org"
#define DEFAULT_MEMPOOL_INSTANCE "mempool.space"
#define TIME_OFFSET_SECONDS 3600
#define USER_AGENT "BTClock/2.0"
#define DEFAULT_TIME_OFFSET_SECONDS 3600
#ifndef MCP_DEV_ADDR
#define MCP_DEV_ADDR 0x20
#define DEFAULT_SECONDS_BETWEEN_PRICE_UPDATE 30
#define DEFAULT_MINUTES_FULL_REFRESH 60
#endif
#define DEFAULT_FG_COLOR GxEPD_WHITE
#define DEFAULT_BG_COLOR GxEPD_BLACK
void setup();
void setupTime();
void syncTime();
uint getLastTimeSync();
void setupPreferences();
void setupWebsocketClients(void *pvParameters);
void setupHardware();
void tryImprovSetup();
void setupWifi();
void setupTimers();
void finishSetup();
void setupMcp();
#ifdef HAS_FRONTLIGHT
void setupFrontlight();
float getLightLevel();
bool hasLightLevel();
extern PCA9685 flArray;
#endif
String getMyHostname();
std::vector<std::string> getScreenNameMap();
std::vector<ScreenMapping> getScreenNameMap();
std::vector<std::string> getLocalUrl();
bool improv_connectWifi(std::string ssid, std::string password);
void improvGetAvailableWifiNetworks();
bool onImprovCommandCallback(improv::ImprovCommand cmd);
void onImprovErrorCallback(improv::Error err);
void improv_set_state(improv::State state);
void improv_send_response(std::vector<uint8_t> &response);
void improv_set_error(improv::Error error);
// bool improv_connectWifi(std::string ssid, std::string password);
// void improvGetAvailableWifiNetworks();
// bool onImprovCommandCallback(improv::ImprovCommand cmd);
// void onImprovErrorCallback(improv::Error err);
// void improv_set_state(improv::State state);
// void improv_send_response(std::vector<uint8_t> &response);
// void improv_set_error(improv::Error error);
//void addCurrencyMappings(const std::vector<std::string>& currencies);
std::vector<std::string> getActiveCurrencies();
std::vector<std::string> getAvailableCurrencies();
void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info);
bool isActiveCurrency(std::string &currency);
void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info);
String getHwRev();
bool isWhiteVersion();
String getFsRev();
bool debugLogEnabled();
void addScreenMapping(int value, const char* name);
// void addScreenMapping(int value, const String& name);
// void addScreenMapping(int value, const std::string& name);
int findScreenIndexByValue(int value);
String replaceAmbiguousChars(String input);
const char* getFirmwareFilename();
const char* getWebUiFilename();
// void loadIcons();
extern Preferences preferences;
extern MCP23017 mcp1;
#ifdef IS_BTCLOCK_V8
extern MCP23017 mcp2;
#endif
// Expose DataSourceType enum
extern DataSourceType getDataSource();
extern void setDataSource(DataSourceType source);

95
src/lib/defaults.hpp Normal file
View file

@ -0,0 +1,95 @@
#pragma once
#define INITIAL_BLOCK_HEIGHT 876600
#define INITIAL_LAST_PRICE 50000
#define DEFAULT_TX_POWER 0
#define DEFAULT_MEMPOOL_SECURE true
#define DEFAULT_LED_TEST_ON_POWER true
#define DEFAULT_LED_FLASH_ON_UPD false
#define DEFAULT_LED_BRIGHTNESS 128
#define DEFAULT_STEAL_FOCUS false
#define DEFAULT_MCAP_BIG_CHAR true
#define DEFAULT_MDNS_ENABLED true
#define DEFAULT_OTA_ENABLED true
#define DEFAULT_FETCH_EUR_PRICE false
#define DEFAULT_USE_SATS_SYMBOL false
#define DEFAULT_USE_BLOCK_COUNTDOWN true
#define DEFAULT_SUFFIX_PRICE false
#define DEFAULT_DISABLE_LEDS false
#define DEFAULT_DISABLE_FL false
#define DEFAULT_MOW_MODE false
#define DEFAULT_SUFFIX_SHARE_DOT false
#define DEFAULT_V2_SOURCE_CURRENCY CURRENCY_USD
#define DEFAULT_TIME_OFFSET_SECONDS 3600
#define DEFAULT_HOSTNAME_PREFIX "btclock"
#define DEFAULT_MEMPOOL_INSTANCE "mempool.space"
#define DEFAULT_USE_NOSTR false
#define DEFAULT_NOSTR_NPUB "642317135fd4c4205323b9dea8af3270657e62d51dc31a657c0ec8aab31c6288"
#define DEFAULT_NOSTR_RELAY "wss://relay.primal.net"
#define DEFAULT_SECONDS_BETWEEN_PRICE_UPDATE 30
#define DEFAULT_MINUTES_FULL_REFRESH 60
#define DEFAULT_FG_COLOR GxEPD_WHITE
#define DEFAULT_BG_COLOR GxEPD_BLACK
#define DEFAULT_WP_TIMEOUT 15*60
#define DEFAULT_FL_MAX_BRIGHTNESS 2048
#define DEFAULT_FL_EFFECT_DELAY 15
#define DEFAULT_LUX_LIGHT_TOGGLE 128
#define DEFAULT_FL_OFF_WHEN_DARK true
#define DEFAULT_FL_ALWAYS_ON false
#define DEFAULT_FL_FLASH_ON_UPDATE false
#define DEFAULT_LED_STATUS false
#define DEFAULT_TIMER_ACTIVE true
#define DEFAULT_TIMER_SECONDS 1800
#define DEFAULT_CURRENT_SCREEN 0
#define DEFAULT_BITAXE_ENABLED false
#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_ZAP_NOTIFY_ENABLED false
#define DEFAULT_ZAP_NOTIFY_PUBKEY "b5127a08cf33616274800a4387881a9f98e04b9c37116e92de5250498635c422"
#define DEFAULT_LED_FLASH_ON_ZAP true
#define DEFAULT_FL_FLASH_ON_ZAP true
#define DEFAULT_HTTP_AUTH_ENABLED false
#define DEFAULT_HTTP_AUTH_USERNAME "btclock"
#define DEFAULT_HTTP_AUTH_PASSWORD "satoshi"
#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

View file

@ -1,6 +1,65 @@
#include "epd.hpp"
#ifndef IS_BTCLOCK_S3
#ifdef IS_BTCLOCK_REV_B
Native_Pin EPD_CS[NUM_SCREENS] = {
Native_Pin(2),
Native_Pin(4),
Native_Pin(6),
Native_Pin(10),
Native_Pin(38),
Native_Pin(21),
Native_Pin(17),
};
Native_Pin EPD_BUSY[NUM_SCREENS] = {
Native_Pin(3),
Native_Pin(5),
Native_Pin(7),
Native_Pin(9),
Native_Pin(37),
Native_Pin(18),
Native_Pin(16),
};
MCP23X17_Pin EPD_RESET_MPD[NUM_SCREENS] = {
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),
};
Native_Pin EPD_DC = Native_Pin(14);
#elif IS_BTCLOCK_V8
Native_Pin EPD_DC = Native_Pin(38);
MCP23X17_Pin EPD_BUSY[NUM_SCREENS] = {
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),
};
MCP23X17_Pin EPD_CS[NUM_SCREENS] = {
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, 4), MCP23X17_Pin(mcp2, 6)};
MCP23X17_Pin EPD_RESET_MPD[NUM_SCREENS] = {
MCP23X17_Pin(mcp2, 9),
MCP23X17_Pin(mcp2, 11),
MCP23X17_Pin(mcp2, 13),
MCP23X17_Pin(mcp2, 15),
MCP23X17_Pin(mcp2, 1),
MCP23X17_Pin(mcp2, 3),
MCP23X17_Pin(mcp2, 5),
MCP23X17_Pin(mcp2, 7),
};
#else
Native_Pin EPD_CS[NUM_SCREENS] = {
Native_Pin(2),
Native_Pin(4),
@ -16,48 +75,37 @@ Native_Pin EPD_CS[NUM_SCREENS] = {
#endif
};
Native_Pin EPD_BUSY[NUM_SCREENS] = {
Native_Pin(3), Native_Pin(5), Native_Pin(7), Native_Pin(9),
Native_Pin(37), Native_Pin(18), Native_Pin(16),
Native_Pin(3),
Native_Pin(5),
Native_Pin(7),
Native_Pin(9),
Native_Pin(37),
Native_Pin(18),
Native_Pin(16),
};
MCP23X17_Pin EPD_RESET_MPD[NUM_SCREENS] = {
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, 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),
};
Native_Pin EPD_DC = Native_Pin(14);
#else
Native_Pin EPD_DC = Native_Pin(38);
MCP23X17_Pin EPD_BUSY[NUM_SCREENS] = {
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),
};
MCP23X17_Pin EPD_CS[NUM_SCREENS] = {
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, 4), MCP23X17_Pin(mcp2, 6)};
MCP23X17_Pin EPD_RESET_MPD[NUM_SCREENS] = {
MCP23X17_Pin(mcp2, 9), MCP23X17_Pin(mcp2, 11), MCP23X17_Pin(mcp2, 13),
MCP23X17_Pin(mcp2, 15), MCP23X17_Pin(mcp2, 1), MCP23X17_Pin(mcp2, 3),
MCP23X17_Pin(mcp2, 5), MCP23X17_Pin(mcp2, 7),
};
#endif
GxEPD2_BW<GxEPD2_213_B74, GxEPD2_213_B74::HEIGHT> displays[NUM_SCREENS] = {
GxEPD2_213_B74(&EPD_CS[0], &EPD_DC, &EPD_RESET_MPD[0], &EPD_BUSY[0]),
GxEPD2_213_B74(&EPD_CS[1], &EPD_DC, &EPD_RESET_MPD[1], &EPD_BUSY[1]),
GxEPD2_213_B74(&EPD_CS[2], &EPD_DC, &EPD_RESET_MPD[2], &EPD_BUSY[2]),
GxEPD2_213_B74(&EPD_CS[3], &EPD_DC, &EPD_RESET_MPD[3], &EPD_BUSY[3]),
GxEPD2_213_B74(&EPD_CS[4], &EPD_DC, &EPD_RESET_MPD[4], &EPD_BUSY[4]),
GxEPD2_213_B74(&EPD_CS[5], &EPD_DC, &EPD_RESET_MPD[5], &EPD_BUSY[5]),
GxEPD2_213_B74(&EPD_CS[6], &EPD_DC, &EPD_RESET_MPD[6], &EPD_BUSY[6]),
#ifdef IS_BTCLOCK_S3
GxEPD2_213_B74(&EPD_CS[7], &EPD_DC, &EPD_RESET_MPD[6], &EPD_BUSY[7]),
GxEPD2_BW<EPD_CLASS, EPD_CLASS::HEIGHT> displays[NUM_SCREENS] = {
EPD_CLASS(&EPD_CS[0], &EPD_DC, &EPD_RESET_MPD[0], &EPD_BUSY[0]),
EPD_CLASS(&EPD_CS[1], &EPD_DC, &EPD_RESET_MPD[1], &EPD_BUSY[1]),
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]),
EPD_CLASS(&EPD_CS[5], &EPD_DC, &EPD_RESET_MPD[5], &EPD_BUSY[5]),
EPD_CLASS(&EPD_CS[6], &EPD_DC, &EPD_RESET_MPD[6], &EPD_BUSY[6]),
#ifdef IS_BTCLOCK_V8
EPD_CLASS(&EPD_CS[7], &EPD_DC, &EPD_RESET_MPD[7], &EPD_BUSY[7]),
#endif
};
@ -78,68 +126,85 @@ 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];
void forceFullRefresh() {
for (uint i = 0; i < NUM_SCREENS; i++) {
#ifdef IS_BTCLOCK_V8
#define EPD_TASK_STACK_SIZE 4096
#else
#define EPD_TASK_STACK_SIZE 2048
#endif
#define BUSY_TIMEOUT_COUNT 200
#define BUSY_RETRY_DELAY pdMS_TO_TICKS(10)
void forceFullRefresh()
{
for (uint i = 0; i < NUM_SCREENS; i++)
{
lastFullRefresh[i] = NULL;
}
}
void refreshFromMemory() {
for (uint i = 0; i < NUM_SCREENS; i++) {
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() {
void setupDisplays()
{
std::lock_guard<std::mutex> lockMcp(mcpMutex);
for (uint i = 0; i < NUM_SCREENS; i++) {
for (uint i = 0; i < NUM_SCREENS; i++)
{
displays[i].init(0, true, 30);
}
updateQueue = xQueueCreate(UPDATE_QUEUE_SIZE, sizeof(UpdateDisplayTaskItem));
xTaskCreate(prepareDisplayUpdateTask, "PrepareUpd", 4096, NULL, 11, NULL);
xTaskCreate(prepareDisplayUpdateTask, "PrepareUpd", EPD_TASK_STACK_SIZE*2, NULL, 11, NULL);
for (uint i = 0; i < NUM_SCREENS; i++) {
for (uint i = 0; i < NUM_SCREENS; i++)
{
// epdUpdateSemaphore[i] = xSemaphoreCreateBinary();
// xSemaphoreGive(epdUpdateSemaphore[i]);
int *taskParam = new int;
*taskParam = i;
xTaskCreate(updateDisplay, ("EpdUpd" + String(i)).c_str(), 2048, taskParam,
11, &tasks[i]); // create task
xTaskCreate(updateDisplay, ("EpdUpd" + String(i)).c_str(), EPD_TASK_STACK_SIZE, taskParam,
11, &tasks[i]); // create task
}
epdContent = {"B", "T", "C", "L", "O", "C", "K"};
// Hold lower button to enable "storage mode" (prevents burn-in of ePaper displays)
if (mcp1.read1(0) == LOW)
{
setFgColor(GxEPD_BLACK);
setBgColor(GxEPD_WHITE);
epdContent.fill("");
}
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);
}
void setEpdContent(std::array<String, NUM_SCREENS> newEpdContent) {
void setEpdContent(std::array<String, NUM_SCREENS> newEpdContent)
{
setEpdContent(newEpdContent, false);
}
void setEpdContent(std::array<std::string, NUM_SCREENS> newEpdContent) {
void setEpdContent(std::array<std::string, NUM_SCREENS> newEpdContent)
{
std::array<String, NUM_SCREENS> conv;
for (size_t i = 0; i < newEpdContent.size(); ++i) {
for (size_t i = 0; i < newEpdContent.size(); ++i)
{
conv[i] = String(newEpdContent[i].c_str());
}
@ -147,13 +212,16 @@ void setEpdContent(std::array<std::string, NUM_SCREENS> newEpdContent) {
}
void setEpdContent(std::array<String, NUM_SCREENS> newEpdContent,
bool forceUpdate) {
bool forceUpdate)
{
std::lock_guard<std::mutex> lock(epdUpdateMutex);
waitUntilNoneBusy();
for (uint i = 0; i < NUM_SCREENS; i++) {
if (newEpdContent[i].compareTo(currentEpdContent[i]) != 0 || forceUpdate) {
for (uint i = 0; i < NUM_SCREENS; i++)
{
if (newEpdContent[i].compareTo(currentEpdContent[i]) != 0 || forceUpdate)
{
epdContent[i] = newEpdContent[i];
UpdateDisplayTaskItem dispUpdate = {i};
xQueueSend(updateQueue, &dispUpdate, portMAX_DELAY);
@ -161,12 +229,15 @@ void setEpdContent(std::array<String, NUM_SCREENS> newEpdContent,
}
}
void prepareDisplayUpdateTask(void *pvParameters) {
void prepareDisplayUpdateTask(void *pvParameters)
{
UpdateDisplayTaskItem receivedItem;
while (1) {
while (1)
{
// Wait for a work item to be available in the queue
if (xQueueReceive(updateQueue, &receivedItem, portMAX_DELAY)) {
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
@ -174,21 +245,50 @@ void prepareDisplayUpdateTask(void *pvParameters) {
bool updatePartial = true;
if (strstr(epdContent[epdIndex].c_str(), "/") != NULL) {
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"))) {
}
else if (epdContent[epdIndex].startsWith(F("qr")))
{
renderQr(epdIndex, epdContent[epdIndex], updatePartial);
} else if (epdContent[epdIndex].length() > 5) {
}
else if (epdContent[epdIndex].startsWith(F("mdi")))
{
bool updated = renderIcon(epdIndex, epdContent[epdIndex], updatePartial);
if (!updated) {
continue;
}
}
else if (epdContent[epdIndex].length() > 5)
{
renderText(epdIndex, epdContent[epdIndex], updatePartial);
} else {
if (epdContent[epdIndex].length() > 1) {
showChars(epdIndex, epdContent[epdIndex], updatePartial,
&FONT_MEDIUM);
} else {
}
else
{
if (epdContent[epdIndex].length() == 2) {
showChars(epdIndex, epdContent[epdIndex], updatePartial, &FONT_BIG);
}
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);
}
@ -199,11 +299,13 @@ void prepareDisplayUpdateTask(void *pvParameters) {
}
}
extern "C" void updateDisplay(void *pvParameters) noexcept {
extern "C" void updateDisplay(void *pvParameters) noexcept
{
const int epdIndex = *(int *)pvParameters;
delete (int *)pvParameters;
for (;;) {
for (;;)
{
// Wait for the task notification
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
@ -215,7 +317,8 @@ extern "C" void updateDisplay(void *pvParameters) noexcept {
displays[epdIndex].init(0, false, 40);
}
uint count = 0;
while (EPD_BUSY[epdIndex].digitalRead() == HIGH || count < 10) {
while (EPD_BUSY[epdIndex].digitalRead() == HIGH || count < 10)
{
vTaskDelay(pdMS_TO_TICKS(100));
count++;
}
@ -227,16 +330,20 @@ extern "C" void updateDisplay(void *pvParameters) noexcept {
(millis() - lastFullRefresh[epdIndex]) >
(preferences.getUInt("fullRefreshMin",
DEFAULT_MINUTES_FULL_REFRESH) *
60 * 1000)) {
60 * 1000))
{
updatePartial = false;
}
char tries = 0;
while (tries < 3) {
if (displays[epdIndex].displayWithReturn(updatePartial)) {
while (tries < 3)
{
if (displays[epdIndex].displayWithReturn(updatePartial))
{
displays[epdIndex].powerOff();
currentEpdContent[epdIndex] = epdContent[epdIndex];
if (!updatePartial) lastFullRefresh[epdIndex] = millis();
if (!updatePartial)
lastFullRefresh[epdIndex] = millis();
if (eventSourceTaskHandle != NULL)
xTaskNotifyGive(eventSourceTaskHandle);
@ -251,8 +358,13 @@ extern "C" void updateDisplay(void *pvParameters) noexcept {
}
void splitText(const uint dispNum, const String &top, const String &bottom,
bool partial) {
displays[dispNum].setRotation(2);
bool partial)
{
if(preferences.getBool("verticalDesc", DEFAULT_VERTICAL_DESC) && dispNum == 0) {
displays[dispNum].setRotation(1);
} else {
displays[dispNum].setRotation(2);
}
displays[dispNum].setFont(&FONT_SMALL);
displays[dispNum].setTextColor(getFgColor());
@ -289,37 +401,83 @@ void splitText(const uint dispNum, const String &top, const String &bottom,
displays[dispNum].print(bottom);
}
void showDigit(const uint dispNum, char chr, bool partial,
const GFXfont *font) {
String str(chr);
displays[dispNum].setRotation(2);
displays[dispNum].setFont(font);
displays[dispNum].setTextColor(getFgColor());
int16_t tbx, tby;
uint16_t 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 y = ((displays[dispNum].height() - tbh) / 2) - tby;
displays[dispNum].fillScreen(getBgColor());
displays[dispNum].setCursor(x, y);
displays[dispNum].print(str);
// Consolidate common display setup code into a helper function
void setupDisplay(const uint dispNum, const GFXfont *font) {
displays[dispNum].setRotation(2);
displays[dispNum].setFont(font);
displays[dispNum].setTextColor(getFgColor());
displays[dispNum].fillScreen(getBgColor());
}
void showDigit(const uint dispNum, char chr, bool partial, const GFXfont *font) {
String str(chr);
if (chr == '.') {
str = "!";
}
setupDisplay(dispNum, font);
int16_t tbx, tby;
uint16_t tbw, tbh;
displays[dispNum].getTextBounds(str, 0, 0, &tbx, &tby, &tbw, &tbh);
uint16_t x = ((displays[dispNum].width() - tbw) / 2) - tbx;
uint16_t y = ((displays[dispNum].height() - tbh) / 2) - tby;
displays[dispNum].setCursor(x, y);
displays[dispNum].print(str);
if (chr == '.') {
displays[dispNum].fillRect(x, y, displays[dispNum].width(),
round(displays[dispNum].height() * 0.9), getBgColor());
}
}
int16_t calculateDescent(const GFXfont *font) {
int16_t maxDescent = 0;
for (uint16_t i = font->first; i <= font->last; i++) {
GFXglyph *glyph = &font->glyph[i - font->first];
int16_t descent = glyph->yOffset;
if (descent > maxDescent) {
maxDescent = descent;
}
}
return maxDescent;
}
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());
const GFXfont *font)
{
setupDisplay(dispNum, font);
int16_t tbx, tby;
uint16_t tbw, tbh;
displays[dispNum].getTextBounds(chars, 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 y = ((displays[dispNum].height() - tbh) / 2) - tby;
displays[dispNum].fillScreen(getBgColor());
displays[dispNum].setCursor(x, y);
displays[dispNum].print(chars);
for (int 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].print(c);
}
// Move x-position for the next character
x += font->glyph[c - font->first].xAdvance;
}
}
int getBgColor() { return bgColor; }
@ -330,10 +488,12 @@ void setBgColor(int color) { bgColor = color; }
void setFgColor(int color) { fgColor = color; }
std::array<String, NUM_SCREENS> getCurrentEpdContent() {
std::array<String, NUM_SCREENS> getCurrentEpdContent()
{
return currentEpdContent;
}
void renderText(const uint dispNum, const String &text, bool partial) {
void renderText(const uint dispNum, const String &text, bool partial)
{
displays[dispNum].setRotation(2);
displays[dispNum].setPartialWindow(0, 0, displays[dispNum].width(),
displays[dispNum].height());
@ -346,20 +506,77 @@ void renderText(const uint dispNum, const String &text, bool partial) {
std::string line;
while (std::getline(ss, line, '\n')) {
if (line.rfind("*", 0) == 0) {
while (std::getline(ss, line, '\n'))
{
if (line.rfind("*", 0) == 0)
{
line.erase(std::remove(line.begin(), line.end(), '*'), line.end());
displays[dispNum].setFont(&FreeSansBold9pt7b);
displays[dispNum].println(line.c_str());
} else {
}
else
{
displays[dispNum].setFont(&FreeSans9pt7b);
displays[dispNum].println(line.c_str());
}
}
}
void renderQr(const uint dispNum, const String &text, bool partial) {
bool renderIcon(const 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;
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 = getMiningPoolLogo();
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;
// Close the file
displays[dispNum].drawInvertedBitmap(x_offset,y_offset, logo.data, logo.width, logo.height, getFgColor());
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, getFgColor());
return true;
// displays[dispNum].drawInvertedBitmap(0,0, getOceanIcon(), 122, 250, getFgColor());
}
void renderQr(const uint dispNum, const String &text, bool partial)
{
#ifdef USE_QR
uint8_t tempBuffer[800];
@ -379,8 +596,10 @@ void renderQr(const uint dispNum, const String &text, bool partial) {
displays[dispNum].fillScreen(GxEPD_WHITE);
const int border = 0;
for (int y = -border; y < size * 4 + border; y++) {
for (int x = -border; x < size * 4 + border; x++) {
for (int y = -border; y < size * 4 + border; y++)
{
for (int x = -border; x < size * 4 + border; x++)
{
displays[dispNum].drawPixel(
padding + x, paddingY + y,
qrcodegen_getModule(qrcode, floor(float(x) / 4), floor(float(y) / 4))
@ -391,17 +610,20 @@ void renderQr(const uint dispNum, const String &text, bool partial) {
#endif
}
void waitUntilNoneBusy() {
for (int i = 0; i < NUM_SCREENS; i++) {
void waitUntilNoneBusy()
{
for (int i = 0; i < NUM_SCREENS; i++)
{
uint count = 0;
while (EPD_BUSY[i].digitalRead()) {
while (EPD_BUSY[i].digitalRead())
{
count++;
vTaskDelay(10);
if (count == 200) {
// displays[i].init(0, false);
vTaskDelay(100);
} else if (count > 205) {
Serial.printf("Busy timeout %d", i);
vTaskDelay(BUSY_RETRY_DELAY);
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;
}
}

View file

@ -4,6 +4,7 @@
#include <Fonts/FreeSansBold9pt7b.h>
#include <GxEPD2_BW.h>
#include <mcp23x17_pin.hpp>
#include <mutex>
#include <native_pin.hpp>
@ -12,6 +13,8 @@
#include "fonts/fonts.hpp"
#include "lib/config.hpp"
#include "lib/shared.hpp"
#include "icons/icons.h"
#include "mining_pool_stats_fetch.hpp"
#ifdef USE_QR
#include "qrcodegen.h"
@ -23,7 +26,6 @@ typedef struct {
} UpdateDisplayTaskItem;
void forceFullRefresh();
void refreshFromMemory();
void setupDisplays();
void splitText(const uint dispNum, const String &top, const String &bottom,
@ -42,6 +44,7 @@ int getFgColor();
void setBgColor(int color);
void setFgColor(int color);
bool 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);

View file

@ -1,143 +0,0 @@
#include "improv.h"
namespace improv {
ImprovCommand parse_improv_data(const std::vector<uint8_t> &data,
bool check_checksum) {
return parse_improv_data(data.data(), data.size(), check_checksum);
}
ImprovCommand parse_improv_data(const uint8_t *data, size_t length,
bool check_checksum) {
ImprovCommand improv_command;
Command command = (Command)data[0];
uint8_t data_length = data[1];
if (data_length != length - 2 - check_checksum) {
improv_command.command = UNKNOWN;
return improv_command;
}
if (check_checksum) {
uint8_t checksum = data[length - 1];
uint32_t calculated_checksum = 0;
for (uint8_t i = 0; i < length - 1; i++) {
calculated_checksum += data[i];
}
if ((uint8_t)calculated_checksum != checksum) {
improv_command.command = BAD_CHECKSUM;
return improv_command;
}
}
if (command == WIFI_SETTINGS) {
uint8_t ssid_length = data[2];
uint8_t ssid_start = 3;
size_t ssid_end = ssid_start + ssid_length;
uint8_t pass_length = data[ssid_end];
size_t pass_start = ssid_end + 1;
size_t pass_end = pass_start + pass_length;
std::string ssid(data + ssid_start, data + ssid_end);
std::string password(data + pass_start, data + pass_end);
return {.command = command, .ssid = ssid, .password = password};
}
improv_command.command = command;
return improv_command;
}
bool parse_improv_serial_byte(size_t position, uint8_t byte,
const uint8_t *buffer,
std::function<bool(ImprovCommand)> &&callback,
std::function<void(Error)> &&on_error) {
if (position == 0) return byte == 'I';
if (position == 1) return byte == 'M';
if (position == 2) return byte == 'P';
if (position == 3) return byte == 'R';
if (position == 4) return byte == 'O';
if (position == 5) return byte == 'V';
if (position == 6) return byte == IMPROV_SERIAL_VERSION;
if (position <= 8) return true;
uint8_t type = buffer[7];
uint8_t data_len = buffer[8];
if (position <= 8 + data_len) return true;
if (position == 8 + data_len + 1) {
uint8_t checksum = 0x00;
for (size_t i = 0; i < position; i++) checksum += buffer[i];
if (checksum != byte) {
on_error(ERROR_INVALID_RPC);
return false;
}
if (type == TYPE_RPC) {
auto command = parse_improv_data(&buffer[9], data_len, false);
return callback(command);
}
}
return false;
}
std::vector<uint8_t> build_rpc_response(Command command,
const std::vector<std::string> &datum,
bool add_checksum) {
std::vector<uint8_t> out;
uint32_t length = 0;
out.push_back(command);
for (const auto &str : datum) {
uint8_t len = str.length();
length += len + 1;
out.push_back(len);
out.insert(out.end(), str.begin(), str.end());
}
out.insert(out.begin() + 1, length);
if (add_checksum) {
uint32_t calculated_checksum = 0;
for (uint8_t byte : out) {
calculated_checksum += byte;
}
out.push_back(calculated_checksum);
}
return out;
}
#ifdef ARDUINO
std::vector<uint8_t> build_rpc_response(Command command,
const std::vector<String> &datum,
bool add_checksum) {
std::vector<uint8_t> out;
uint32_t length = 0;
out.push_back(command);
for (const auto &str : datum) {
uint8_t len = str.length();
length += len;
out.push_back(len);
out.insert(out.end(), str.begin(), str.end());
}
out.insert(out.begin() + 1, length);
if (add_checksum) {
uint32_t calculated_checksum = 0;
for (uint8_t byte : out) {
calculated_checksum += byte;
}
out.push_back(calculated_checksum);
}
return out;
}
#endif // ARDUINO
} // namespace improv

View file

@ -1,86 +0,0 @@
#pragma once
#ifdef ARDUINO
#include <Arduino.h>
#endif // ARDUINO
#include <cstdint>
#include <functional>
#include <string>
#include <vector>
namespace improv {
static const char *const SERVICE_UUID = "00467768-6228-2272-4663-277478268000";
static const char *const STATUS_UUID = "00467768-6228-2272-4663-277478268001";
static const char *const ERROR_UUID = "00467768-6228-2272-4663-277478268002";
static const char *const RPC_COMMAND_UUID =
"00467768-6228-2272-4663-277478268003";
static const char *const RPC_RESULT_UUID =
"00467768-6228-2272-4663-277478268004";
static const char *const CAPABILITIES_UUID =
"00467768-6228-2272-4663-277478268005";
enum Error : uint8_t {
ERROR_NONE = 0x00,
ERROR_INVALID_RPC = 0x01,
ERROR_UNKNOWN_RPC = 0x02,
ERROR_UNABLE_TO_CONNECT = 0x03,
ERROR_NOT_AUTHORIZED = 0x04,
ERROR_UNKNOWN = 0xFF,
};
enum State : uint8_t {
STATE_STOPPED = 0x00,
STATE_AWAITING_AUTHORIZATION = 0x01,
STATE_AUTHORIZED = 0x02,
STATE_PROVISIONING = 0x03,
STATE_PROVISIONED = 0x04,
};
enum Command : uint8_t {
UNKNOWN = 0x00,
WIFI_SETTINGS = 0x01,
IDENTIFY = 0x02,
GET_CURRENT_STATE = 0x02,
GET_DEVICE_INFO = 0x03,
GET_WIFI_NETWORKS = 0x04,
BAD_CHECKSUM = 0xFF,
};
static const uint8_t CAPABILITY_IDENTIFY = 0x01;
static const uint8_t IMPROV_SERIAL_VERSION = 1;
enum ImprovSerialType : uint8_t {
TYPE_CURRENT_STATE = 0x01,
TYPE_ERROR_STATE = 0x02,
TYPE_RPC = 0x03,
TYPE_RPC_RESPONSE = 0x04
};
struct ImprovCommand {
Command command;
std::string ssid;
std::string password;
};
ImprovCommand parse_improv_data(const std::vector<uint8_t> &data,
bool check_checksum = true);
ImprovCommand parse_improv_data(const uint8_t *data, size_t length,
bool check_checksum = true);
bool parse_improv_serial_byte(size_t position, uint8_t byte,
const uint8_t *buffer,
std::function<bool(ImprovCommand)> &&callback,
std::function<void(Error)> &&on_error);
std::vector<uint8_t> build_rpc_response(Command command,
const std::vector<std::string> &datum,
bool add_checksum = true);
#ifdef ARDUINO
std::vector<uint8_t> build_rpc_response(Command command,
const std::vector<String> &datum,
bool add_checksum = true);
#endif // ARDUINO
} // namespace improv

View file

@ -5,121 +5,440 @@ QueueHandle_t ledTaskQueue = NULL;
Adafruit_NeoPixel pixels(NEOPIXEL_COUNT, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);
uint ledTaskParams;
void ledTask(void *parameter) {
while (1) {
if (ledTaskQueue != NULL) {
if (xQueueReceive(ledTaskQueue, &ledTaskParams, portMAX_DELAY) ==
pdPASS) {
uint32_t oldLights[NEOPIXEL_COUNT];
#ifdef HAS_FRONTLIGHT
constexpr uint16_t FL_FADE_STEP = 25;
// get current state
for (int i = 0; i < NEOPIXEL_COUNT; i++) {
oldLights[i] = pixels.getPixelColor(i);
bool frontlightOn = false;
bool flInTransition = false;
void frontlightFlash(int flDelayTime)
{
if (preferences.getBool("flDisable"))
return;
if (frontlightOn)
{
frontlightFadeOutAll(flDelayTime, true);
frontlightFadeInAll(flDelayTime, true);
}
else
{
frontlightFadeInAll(flDelayTime, true);
frontlightFadeOutAll(flDelayTime, true);
}
}
void frontlightFadeInAll()
{
frontlightFadeInAll(preferences.getUInt("flEffectDelay"));
}
void frontlightFadeOutAll()
{
frontlightFadeOutAll(preferences.getUInt("flEffectDelay"));
}
void frontlightFadeIn(uint num)
{
frontlightFadeIn(num, preferences.getUInt("flEffectDelay"));
}
void frontlightFadeOut(uint num)
{
frontlightFadeOut(num, preferences.getUInt("flEffectDelay"));
}
void frontlightSetBrightness(uint brightness)
{
if (brightness > 4096)
{
return;
}
for (int ledPin = 0; ledPin <= NUM_SCREENS; ledPin++)
{
flArray.setPWM(ledPin, 0, brightness);
}
}
void frontlightFadeInAll(int flDelayTime)
{
frontlightFadeInAll(flDelayTime, false);
}
void frontlightFadeInAll(int flDelayTime, bool staggered)
{
if (preferences.getBool("flDisable") || frontlightIsOn() || flInTransition)
return;
flInTransition = true;
const int maxBrightness = preferences.getUInt("flMaxBrightness");
if (staggered)
{
int step = FL_FADE_STEP;
int staggerDelay = flDelayTime / NUM_SCREENS;
for (int dutyCycle = 0; dutyCycle <= maxBrightness + (NUM_SCREENS - 1) * maxBrightness / NUM_SCREENS; dutyCycle += step)
{
for (int ledPin = 0; ledPin < NUM_SCREENS; ledPin++)
{
int ledBrightness = dutyCycle - ledPin * maxBrightness / NUM_SCREENS;
if (ledBrightness < 0)
ledBrightness = 0;
else if (ledBrightness > maxBrightness)
ledBrightness = maxBrightness;
flArray.setPWM(ledPin + 1, 0, ledBrightness);
}
vTaskDelay(pdMS_TO_TICKS(staggerDelay));
}
}
else
{
for (int dutyCycle = 0; dutyCycle <= maxBrightness; dutyCycle += FL_FADE_STEP)
{
for (int ledPin = 0; ledPin <= NUM_SCREENS; ledPin++)
{
flArray.setPWM(ledPin, 0, dutyCycle);
}
vTaskDelay(pdMS_TO_TICKS(flDelayTime));
}
}
frontlightOn = true;
flInTransition = false;
}
void frontlightFadeOutAll(int flDelayTime)
{
frontlightFadeOutAll(flDelayTime, false);
}
void frontlightFadeOutAll(int flDelayTime, bool staggered)
{
if (preferences.getBool("flDisable"))
return;
if (!frontlightIsOn())
return;
if (flInTransition)
return;
flInTransition = true;
if (staggered)
{
int maxBrightness = preferences.getUInt("flMaxBrightness");
int step = FL_FADE_STEP;
int staggerDelay = flDelayTime / NUM_SCREENS;
for (int dutyCycle = maxBrightness; dutyCycle >= 0; dutyCycle -= step)
{
for (int ledPin = 0; ledPin < NUM_SCREENS; ledPin++)
{
int ledBrightness = dutyCycle - (NUM_SCREENS - 1 - ledPin) * maxBrightness / NUM_SCREENS;
if (ledBrightness < 0)
ledBrightness = 0;
else if (ledBrightness > maxBrightness)
ledBrightness = maxBrightness;
flArray.setPWM(ledPin + 1, 0, ledBrightness);
}
vTaskDelay(pdMS_TO_TICKS(staggerDelay));
}
}
else
{
for (int dutyCycle = preferences.getUInt("flMaxBrightness"); dutyCycle >= 0; dutyCycle -= FL_FADE_STEP)
{
for (int ledPin = 0; ledPin <= NUM_SCREENS; ledPin++)
{
flArray.setPWM(ledPin, 0, dutyCycle);
}
vTaskDelay(pdMS_TO_TICKS(flDelayTime));
}
}
flArray.allOFF();
frontlightOn = false;
flInTransition = false;
}
std::vector<uint16_t> frontlightGetStatus()
{
std::vector<uint16_t> statuses;
for (int ledPin = 1; ledPin <= NUM_SCREENS; ledPin++)
{
uint16_t a = 0, b = 0;
flArray.getPWM(ledPin, &a, &b);
statuses.push_back(round(b - a / 4096));
}
return statuses;
}
bool frontlightIsOn()
{
return frontlightOn;
}
void frontlightFadeIn(uint num, int flDelayTime)
{
if (preferences.getBool("flDisable"))
return;
for (int dutyCycle = 0; dutyCycle <= preferences.getUInt("flMaxBrightness"); dutyCycle += 5)
{
flArray.setPWM(num, 0, dutyCycle);
vTaskDelay(pdMS_TO_TICKS(flDelayTime));
}
}
void frontlightFadeOut(uint num, int flDelayTime)
{
if (preferences.getBool("flDisable"))
return;
if (!frontlightIsOn())
return;
for (int dutyCycle = preferences.getUInt("flMaxBrightness"); dutyCycle >= 0; dutyCycle -= 5)
{
flArray.setPWM(num, 0, dutyCycle);
vTaskDelay(pdMS_TO_TICKS(flDelayTime));
}
}
#endif
void ledTask(void *parameter)
{
while (1)
{
if (ledTaskQueue != NULL)
{
if (xQueueReceive(ledTaskQueue, &ledTaskParams, portMAX_DELAY) ==
pdPASS)
{
if (preferences.getBool("disableLeds", DEFAULT_DISABLE_LEDS))
{
continue;
}
switch (ledTaskParams) {
case LED_POWER_TEST:
ledRainbow(20);
pixels.clear();
break;
case LED_EFFECT_WIFI_CONNECT_ERROR:
blinkDelayTwoColor(100, 3, pixels.Color(8, 161, 236),
pixels.Color(255, 0, 0));
break;
case LED_FLASH_ERROR:
blinkDelayColor(250, 3, 255, 0, 0);
break;
case LED_EFFECT_HEARTBEAT:
blinkDelayColor(150, 2, 0, 0, 255);
break;
case LED_EFFECT_WIFI_CONNECT_SUCCESS:
case LED_FLASH_SUCCESS:
blinkDelayColor(150, 3, 0, 255, 0);
break;
case LED_PROGRESS_100:
pixels.setPixelColor(0, pixels.Color(0, 255, 0));
case LED_PROGRESS_75:
pixels.setPixelColor(1, pixels.Color(0, 255, 0));
case LED_PROGRESS_50:
pixels.setPixelColor(2, pixels.Color(0, 255, 0));
case LED_PROGRESS_25:
pixels.setPixelColor(3, pixels.Color(0, 255, 0));
pixels.show();
break;
case LED_FLASH_UPDATE:
break;
case LED_FLASH_BLOCK_NOTIFY:
blinkDelayTwoColor(250, 3, pixels.Color(224, 67, 0),
pixels.Color(8, 2, 0));
break;
case LED_EFFECT_WIFI_WAIT_FOR_CONFIG:
blinkDelayTwoColor(100, 1, pixels.Color(8, 161, 236),
pixels.Color(156, 225, 240));
break;
case LED_EFFECT_WIFI_ERASE_SETTINGS:
blinkDelay(100, 3);
break;
case LED_EFFECT_WIFI_CONNECTING:
for (int i = NEOPIXEL_COUNT; i >= 0; i--) {
for (int j = NEOPIXEL_COUNT; j >= 0; j--) {
if (j == i) {
pixels.setPixelColor(i, pixels.Color(16, 197, 236));
} else {
pixels.setPixelColor(j, pixels.Color(0, 0, 0));
}
}
pixels.show();
vTaskDelay(pdMS_TO_TICKS(100));
std::array<uint32_t, NEOPIXEL_COUNT> oldLights;
// get current state
for (int i = 0; i < NEOPIXEL_COUNT; i++)
{
oldLights[i] = pixels.getPixelColor(i);
}
#ifdef HAS_FRONTLIGHT
uint flDelayTime = preferences.getUInt("flEffectDelay");
#endif
switch (ledTaskParams)
{
case LED_POWER_TEST:
#ifdef HAS_FRONTLIGHT
frontlightFadeInAll(preferences.getUInt("flEffectDelay"), true);
#endif
ledRainbow(20);
pixels.clear();
break;
case LED_EFFECT_WIFI_CONNECT_ERROR:
blinkDelayTwoColor(100, 3, pixels.Color(8, 161, 236),
pixels.Color(255, 0, 0));
break;
case LED_FLASH_ERROR:
blinkDelayColor(250, 3, 255, 0, 0);
break;
case LED_EFFECT_HEARTBEAT:
blinkDelayColor(150, 2, 0, 0, 255);
break;
case LED_DATA_BLOCK_ERROR:
blinkDelayColor(150, 2, 128, 0, 128);
break;
case LED_DATA_PRICE_ERROR:
blinkDelayColor(150, 2, 177, 90, 31);
break;
case LED_FLASH_IDENTIFY:
blinkDelayTwoColor(100, 2, pixels.Color(255, 0, 0),
pixels.Color(0, 255, 255));
blinkDelayTwoColor(100, 2, pixels.Color(0, 255, 0),
pixels.Color(0, 0, 255));
break;
case LED_EFFECT_WIFI_CONNECT_SUCCESS:
case LED_FLASH_SUCCESS:
blinkDelayColor(150, 3, 0, 255, 0);
break;
case LED_PROGRESS_100:
pixels.setPixelColor(0, pixels.Color(0, 255, 0));
case LED_PROGRESS_75:
pixels.setPixelColor(1, pixels.Color(0, 255, 0));
case LED_PROGRESS_50:
pixels.setPixelColor(2, pixels.Color(0, 255, 0));
case LED_PROGRESS_25:
pixels.setPixelColor(3, pixels.Color(0, 255, 0));
pixels.show();
break;
case LED_EFFECT_NOSTR_ZAP:
{
#ifdef HAS_FRONTLIGHT
bool frontlightWasOn = false;
if (preferences.getBool("flFlashOnZap", DEFAULT_FL_FLASH_ON_ZAP))
{
if (frontlightOn)
{
frontlightWasOn = true;
frontlightFadeOutAll(flDelayTime, true);
}
break;
case LED_EFFECT_PAUSE_TIMER:
for (int i = NEOPIXEL_COUNT; i >= 0; i--) {
for (int j = NEOPIXEL_COUNT; j >= 0; j--) {
uint32_t c = pixels.Color(0, 0, 0);
if (i == j) c = pixels.Color(0, 255, 0);
pixels.setPixelColor(j, c);
}
pixels.show();
delay(100);
else
{
frontlightFadeInAll(flDelayTime, true);
}
pixels.setPixelColor(0, pixels.Color(255, 0, 0));
pixels.show();
}
#endif
for (int flash = 0; flash < random(7, 10); flash++)
{
lightningStrike();
delay(random(50, 150));
}
// blinkDelayColor(250, 3, 142, 48, 235);
// blinkDelayTwoColor(250, 3, pixels.Color(142, 48, 235),
// pixels.Color(169, 21, 255));
#ifdef HAS_FRONTLIGHT
if (preferences.getBool("flFlashOnZap", DEFAULT_FL_FLASH_ON_ZAP))
{
vTaskDelay(pdMS_TO_TICKS(10));
if (frontlightWasOn)
{
frontlightFadeInAll(flDelayTime, true);
}
else
{
frontlightFadeOutAll(flDelayTime, true);
}
}
#endif
break;
}
case LED_FLASH_UPDATE:
blinkDelayTwoColor(250, 3, pixels.Color(0, 230, 0),
pixels.Color(230, 230, 0));
break;
case LED_FLASH_BLOCK_NOTIFY:
{
#ifdef HAS_FRONTLIGHT
bool frontlightWasOn = false;
delay(900);
pixels.clear();
pixels.show();
break;
case LED_EFFECT_START_TIMER:
pixels.clear();
pixels.setPixelColor((NEOPIXEL_COUNT - 1), pixels.Color(255, 0, 0));
pixels.show();
delay(900);
for (int i = NEOPIXEL_COUNT; i--; i > 0) {
for (int j = NEOPIXEL_COUNT; j--; j > 0) {
uint32_t c = pixels.Color(0, 0, 0);
if (i == j) c = pixels.Color(0, 255, 0);
pixels.setPixelColor(j, c);
if (preferences.getBool("flFlashOnUpd", DEFAULT_FL_FLASH_ON_UPDATE))
{
if (frontlightOn)
{
frontlightWasOn = true;
frontlightFadeOutAll(flDelayTime, true);
}
else
{
frontlightFadeInAll(flDelayTime, true);
}
}
#endif
blinkDelayTwoColor(250, 3, pixels.Color(224, 67, 0),
pixels.Color(8, 2, 0));
#ifdef HAS_FRONTLIGHT
if (preferences.getBool("flFlashOnUpd", DEFAULT_FL_FLASH_ON_UPDATE))
{
vTaskDelay(pdMS_TO_TICKS(10));
if (frontlightWasOn)
{
frontlightFadeInAll(flDelayTime, true);
}
else
{
frontlightFadeOutAll(flDelayTime, true);
}
}
#endif
break;
}
case LED_EFFECT_WIFI_WAIT_FOR_CONFIG:
blinkDelayTwoColor(100, 1, pixels.Color(8, 161, 236),
pixels.Color(156, 225, 240));
break;
case LED_EFFECT_WIFI_ERASE_SETTINGS:
blinkDelay(100, 3);
break;
case LED_EFFECT_WIFI_CONNECTING:
for (int i = NEOPIXEL_COUNT; i >= 0; i--)
{
for (int j = NEOPIXEL_COUNT; j >= 0; j--)
{
if (j == i)
{
pixels.setPixelColor(i, pixels.Color(16, 197, 236));
}
pixels.show();
delay(100);
else
{
pixels.setPixelColor(j, pixels.Color(0, 0, 0));
}
}
pixels.show();
vTaskDelay(pdMS_TO_TICKS(100));
}
break;
case LED_EFFECT_PAUSE_TIMER:
for (int i = NEOPIXEL_COUNT; i >= 0; i--)
{
for (int j = NEOPIXEL_COUNT; j >= 0; j--)
{
uint32_t c = pixels.Color(0, 0, 0);
if (i == j)
c = pixels.Color(0, 255, 0);
pixels.setPixelColor(j, c);
}
pixels.clear();
pixels.show();
break;
delay(100);
}
pixels.setPixelColor(0, pixels.Color(255, 0, 0));
pixels.show();
delay(900);
pixels.clear();
pixels.show();
break;
case LED_EFFECT_START_TIMER:
pixels.clear();
pixels.setPixelColor((NEOPIXEL_COUNT - 1), pixels.Color(255, 0, 0));
pixels.show();
delay(900);
for (int i = NEOPIXEL_COUNT; i--; i > 0)
{
for (int j = NEOPIXEL_COUNT; j--; j > 0)
{
uint32_t c = pixels.Color(0, 0, 0);
if (i == j)
c = pixels.Color(0, 255, 0);
pixels.setPixelColor(j, c);
}
pixels.show();
delay(100);
}
pixels.clear();
pixels.show();
break;
}
// revert to previous state unless power test
for (int i = 0; i < NEOPIXEL_COUNT; i++) {
for (int i = 0; i < NEOPIXEL_COUNT; i++)
{
pixels.setPixelColor(i, oldLights[i]);
}
@ -129,14 +448,17 @@ void ledTask(void *parameter) {
}
}
void setupLeds() {
void setupLeds()
{
pixels.begin();
pixels.setBrightness(preferences.getUInt("ledBrightness", 128));
pixels.setBrightness(preferences.getUInt("ledBrightness", DEFAULT_LED_BRIGHTNESS));
pixels.clear();
pixels.show();
setupLedTask();
if (preferences.getBool("ledTestOnPower", true)) {
while (!ledTaskQueue) {
if (preferences.getBool("ledTestOnPower", DEFAULT_LED_TEST_ON_POWER))
{
while (!ledTaskQueue)
{
delay(1);
// wait until queue is available
}
@ -144,14 +466,17 @@ void setupLeds() {
}
}
void setupLedTask() {
void setupLedTask()
{
ledTaskQueue = xQueueCreate(5, sizeof(uint));
xTaskCreate(ledTask, "LedTask", 2048, NULL, tskIDLE_PRIORITY, &ledTaskHandle);
xTaskCreate(ledTask, "LedTask", 2048, NULL, 10, &ledTaskHandle);
}
void blinkDelay(int d, int times) {
for (int j = 0; j < times; j++) {
void blinkDelay(int d, int times)
{
for (int j = 0; j < times; j++)
{
pixels.setPixelColor(0, pixels.Color(255, 0, 0));
pixels.setPixelColor(1, pixels.Color(0, 255, 0));
pixels.setPixelColor(2, pixels.Color(255, 0, 0));
@ -170,9 +495,12 @@ void blinkDelay(int d, int times) {
pixels.show();
}
void blinkDelayColor(int d, int times, uint r, uint g, uint b) {
for (int j = 0; j < times; j++) {
for (int i = 0; i < NEOPIXEL_COUNT; i++) {
void blinkDelayColor(int d, int times, uint r, uint g, uint b)
{
for (int j = 0; j < times; j++)
{
for (int i = 0; i < NEOPIXEL_COUNT; i++)
{
pixels.setPixelColor(i, pixels.Color(r, g, b));
}
@ -187,15 +515,19 @@ void blinkDelayColor(int d, int times, uint r, uint g, uint b) {
pixels.show();
}
void blinkDelayTwoColor(int d, int times, uint32_t c1, uint32_t c2) {
for (int j = 0; j < times; j++) {
for (int i = 0; i < NEOPIXEL_COUNT; i++) {
void blinkDelayTwoColor(int d, int times, const uint32_t& c1, const uint32_t& c2)
{
for (int j = 0; j < times; j++)
{
for (int i = 0; i < NEOPIXEL_COUNT; i++)
{
pixels.setPixelColor(i, c1);
}
pixels.show();
vTaskDelay(pdMS_TO_TICKS(d));
for (int i = 0; i < NEOPIXEL_COUNT; i++) {
for (int i = 0; i < NEOPIXEL_COUNT; i++)
{
pixels.setPixelColor(i, c2);
}
pixels.show();
@ -205,7 +537,8 @@ void blinkDelayTwoColor(int d, int times, uint32_t c1, uint32_t c2) {
pixels.show();
}
void clearLeds() {
void clearLeds()
{
preferences.putBool("ledStatus", false);
pixels.clear();
pixels.show();
@ -213,24 +546,31 @@ void clearLeds() {
void setLights(int r, int g, int b) { setLights(pixels.Color(r, g, b)); }
void setLights(uint32_t color) {
void setLights(uint32_t color)
{
bool ledStatus = true;
for (int i = 0; i < NEOPIXEL_COUNT; i++) {
for (int i = 0; i < NEOPIXEL_COUNT; i++)
{
pixels.setPixelColor(i, color);
}
pixels.show();
if (color == pixels.Color(0, 0, 0)) {
if (color == pixels.Color(0, 0, 0))
{
ledStatus = false;
} else {
}
else
{
saveLedState();
}
preferences.putBool("ledStatus", ledStatus);
}
void saveLedState() {
for (int i = 0; i < pixels.numPixels(); i++) {
void saveLedState()
{
for (int i = 0; i < pixels.numPixels(); i++)
{
int pixelColor = pixels.getPixelColor(i);
char key[12];
snprintf(key, 12, "%s%d", "ledColor_", i);
@ -240,8 +580,10 @@ void saveLedState() {
xTaskNotifyGive(eventSourceTaskHandle);
}
void restoreLedState() {
for (int i = 0; i < pixels.numPixels(); i++) {
void restoreLedState()
{
for (int i = 0; i < pixels.numPixels(); i++)
{
char key[12];
snprintf(key, 12, "%s%d", "ledColor_", i);
uint pixelColor = preferences.getUInt(key, pixels.Color(0, 0, 0));
@ -253,8 +595,10 @@ void restoreLedState() {
QueueHandle_t getLedTaskQueue() { return ledTaskQueue; }
bool queueLedEffect(uint effect) {
if (ledTaskQueue == NULL) {
bool queueLedEffect(uint effect)
{
if (ledTaskQueue == NULL)
{
return false;
}
@ -262,13 +606,15 @@ bool queueLedEffect(uint effect) {
xQueueSend(ledTaskQueue, &flashType, portMAX_DELAY);
}
void ledRainbow(int wait) {
void ledRainbow(int wait)
{
// Hue of first pixel runs 5 complete loops through the color wheel.
// Color wheel has a range of 65536 but it's OK if we roll over, so
// just count from 0 to 5*65536. Adding 256 to firstPixelHue each time
// means we'll make 5*65536/256 = 1280 passes through this loop:
for (long firstPixelHue = 0; firstPixelHue < 5 * 65536;
firstPixelHue += 256) {
firstPixelHue += 256)
{
// strip.rainbow() can take a single argument (first pixel hue) or
// optionally a few extras: number of rainbow repetitions (default 1),
// saturation and value (brightness) (both 0-255, similar to the
@ -277,45 +623,78 @@ void ledRainbow(int wait) {
pixels.rainbow(firstPixelHue);
// Above line is equivalent to:
// strip.rainbow(firstPixelHue, 1, 255, 255, true);
pixels.show(); // Update strip with new contents
pixels.show(); // Update strip with new contents
delayMicroseconds(wait);
// vTaskDelay(pdMS_TO_TICKS(wait)); // Pause for a moment
}
}
void ledTheaterChase(uint32_t color, int wait) {
for (int a = 0; a < 10; a++) { // Repeat 10 times...
for (int b = 0; b < 3; b++) { // 'b' counts from 0 to 2...
pixels.clear(); // Set all pixels in RAM to 0 (off)
void ledTheaterChase(uint32_t color, int wait)
{
for (int a = 0; a < 10; a++)
{ // Repeat 10 times...
for (int b = 0; b < 3; b++)
{ // 'b' counts from 0 to 2...
pixels.clear(); // Set all pixels in RAM to 0 (off)
// 'c' counts up from 'b' to end of strip in steps of 3...
for (int c = b; c < pixels.numPixels(); c += 3) {
pixels.setPixelColor(c, color); // Set pixel 'c' to value 'color'
for (int c = b; c < pixels.numPixels(); c += 3)
{
pixels.setPixelColor(c, color); // Set pixel 'c' to value 'color'
}
pixels.show(); // Update strip with new contents
vTaskDelay(pdMS_TO_TICKS(wait)); // Pause for a moment
pixels.show(); // Update strip with new contents
vTaskDelay(pdMS_TO_TICKS(wait)); // Pause for a moment
}
}
}
void ledTheaterChaseRainbow(int wait) {
int firstPixelHue = 0; // First pixel starts at red (hue 0)
for (int a = 0; a < 30; a++) { // Repeat 30 times...
for (int b = 0; b < 3; b++) { // 'b' counts from 0 to 2...
pixels.clear(); // Set all pixels in RAM to 0 (off)
void ledTheaterChaseRainbow(int wait)
{
int firstPixelHue = 0; // First pixel starts at red (hue 0)
for (int a = 0; a < 30; a++)
{ // Repeat 30 times...
for (int b = 0; b < 3; b++)
{ // 'b' counts from 0 to 2...
pixels.clear(); // Set all pixels in RAM to 0 (off)
// 'c' counts up from 'b' to end of strip in increments of 3...
for (int c = b; c < pixels.numPixels(); c += 3) {
for (int c = b; c < pixels.numPixels(); c += 3)
{
// hue of pixel 'c' is offset by an amount to make one full
// revolution of the color wheel (range 65536) along the length
// of the strip (strip.numPixels() steps):
int hue = firstPixelHue + c * 65536L / pixels.numPixels();
uint32_t color = pixels.gamma32(pixels.ColorHSV(hue)); // hue -> RGB
pixels.setPixelColor(c, color); // Set pixel 'c' to value 'color'
uint32_t color = pixels.gamma32(pixels.ColorHSV(hue)); // hue -> RGB
pixels.setPixelColor(c, color); // Set pixel 'c' to value 'color'
}
pixels.show(); // Update strip with new contents
vTaskDelay(pdMS_TO_TICKS(wait)); // Pause for a moment
firstPixelHue += 65536 / 90; // One cycle of color wheel over 90 frames
pixels.show(); // Update strip with new contents
vTaskDelay(pdMS_TO_TICKS(wait)); // Pause for a moment
firstPixelHue += 65536 / 90; // One cycle of color wheel over 90 frames
}
}
}
Adafruit_NeoPixel getPixels() { return pixels; }
void lightningStrike()
{
uint32_t PURPLE = pixels.Color(128, 0, 128);
uint32_t YELLOW = pixels.Color(255, 226, 41);
// Randomly choose which LEDs to light up
for (int i = 0; i < pixels.numPixels(); i++)
{
if (random(2) == 0)
{ // 50% chance for each LED
pixels.setPixelColor(i, YELLOW);
}
else
{
pixels.setPixelColor(i, PURPLE);
}
}
pixels.show();
delay(random(10, 50)); // Flash duration
// Return to purple background
// setAllPixels(PURPLE);
}
Adafruit_NeoPixel getPixels() { return pixels; }

View file

@ -27,10 +27,19 @@ const int LED_EFFECT_WIFI_CONNECTING = 101;
const int LED_EFFECT_WIFI_CONNECT_ERROR = 102;
const int LED_EFFECT_WIFI_CONNECT_SUCCESS = 103;
const int LED_EFFECT_WIFI_ERASE_SETTINGS = 104;
const int LED_PROGRESS_25 = 200;
const int LED_PROGRESS_50 = 201;
const int LED_PROGRESS_75 = 202;
const int LED_PROGRESS_100 = 203;
const int LED_DATA_PRICE_ERROR = 300;
const int LED_DATA_BLOCK_ERROR = 301;
const int LED_EFFECT_NOSTR_ZAP = 400;
const int LED_FLASH_IDENTIFY = 990;
const int LED_POWER_TEST = 999;
extern TaskHandle_t ledTaskHandle;
extern Adafruit_NeoPixel pixels;
@ -40,7 +49,7 @@ void setupLeds();
void setupLedTask();
void blinkDelay(int d, int times);
void blinkDelayColor(int d, int times, uint r, uint g, uint b);
void blinkDelayTwoColor(int d, int times, uint32_t c1, uint32_t c2);
void blinkDelayTwoColor(int d, int times, const uint32_t& c1, const uint32_t& c2);
void clearLeds();
void saveLedState();
void restoreLedState();
@ -51,4 +60,26 @@ void setLights(uint32_t color);
void ledRainbow(int wait);
void ledTheaterChaseRainbow(int wait);
void ledTheaterChase(uint32_t color, int wait);
Adafruit_NeoPixel getPixels();
Adafruit_NeoPixel getPixels();
void lightningStrike();
#ifdef HAS_FRONTLIGHT
void frontlightFlash(int flDelayTime);
void frontlightFadeInAll();
void frontlightFadeOutAll();
void frontlightFadeIn(uint num);
void frontlightFadeOut(uint num);
std::vector<uint16_t> frontlightGetStatus();
void frontlightSetBrightness(uint brightness);
bool frontlightIsOn();
void frontlightFadeInAll(int flDelayTime);
void frontlightFadeInAll(int flDelayTime, bool staggered);
void frontlightFadeOutAll(int flDelayTime);
void frontlightFadeOutAll(int flDelayTime, bool staggered);
void frontlightFadeIn(uint num, int flDelayTime);
void frontlightFadeOut(uint num, int flDelayTime);
#endif

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,138 @@
#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_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_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,62 @@
#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 "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_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_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,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,137 @@
#include "mining_pool_stats_fetch.hpp"
TaskHandle_t miningPoolStatsFetchTaskHandle;
std::string miningPoolName;
std::string miningPoolStatsHashrate;
int miningPoolStatsDailyEarnings;
std::string getMiningPoolStatsHashRate()
{
return miningPoolStatsHashrate;
}
int getMiningPoolStatsDailyEarnings()
{
return miningPoolStatsDailyEarnings;
}
void taskMiningPoolStatsFetch(void *pvParameters)
{
std::string poolName = preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME).c_str();
auto poolInterface = PoolFactory::createPool(poolName);
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);
miningPoolStatsHashrate = stats.hashrate;
if (debugLogEnabled())
{
Serial.printf("Mining pool stats parsed hashrate: %s\r\n", stats.hashrate.c_str());
}
if (stats.dailyEarnings)
{
miningPoolStatsDailyEarnings = *stats.dailyEarnings;
}
else
{
miningPoolStatsDailyEarnings = 0; // or any other default value
}
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 downloadMiningPoolLogoTask(void *pvParameters) {
std::string poolName = preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME).c_str();
auto poolInterface = PoolFactory::createPool(poolName);
PoolFactory::downloadPoolLogo(poolName, poolInterface.get());
// 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(miningPoolStatsFetchTaskHandle);
vTaskDelete(NULL);
}
void setupMiningPoolStatsFetchTask()
{
xTaskCreate(downloadMiningPoolLogoTask,
"logoDownload",
(6 * 1024),
NULL,
tskIDLE_PRIORITY,
NULL);
xTaskCreate(taskMiningPoolStatsFetch,
"miningPoolStatsFetch",
(6 * 1024),
NULL,
tskIDLE_PRIORITY,
&miningPoolStatsFetchTaskHandle);
}
std::unique_ptr<MiningPoolInterface>& getMiningPool()
{
static std::unique_ptr<MiningPoolInterface> currentMiningPool;
if (!currentMiningPool) {
std::string poolName = preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME).c_str();
currentMiningPool = PoolFactory::createPool(poolName);
}
return currentMiningPool;
}
LogoData getMiningPoolLogo()
{
LogoData logo = getMiningPool()->getLogo();
return logo;
}

View file

@ -0,0 +1,19 @@
#pragma once
#include <Arduino.h>
#include <HTTPClient.h>
#include "mining_pool/pool_factory.hpp"
#include "lib/config.hpp"
#include "lib/shared.hpp"
extern TaskHandle_t miningPoolStatsFetchTaskHandle;
void setupMiningPoolStatsFetchTask();
void taskMiningPoolStatsFetch(void *pvParameters);
std::string getMiningPoolStatsHashRate();
int getMiningPoolStatsDailyEarnings();
std::unique_ptr<MiningPoolInterface>& getMiningPool();
LogoData getMiningPoolLogo();

296
src/lib/nostr_notify.cpp Normal file
View file

@ -0,0 +1,296 @@
#include "nostr_notify.hpp"
std::vector<nostr::NostrPool *> pools;
nostr::Transport *transport;
TaskHandle_t nostrTaskHandle = NULL;
boolean nostrIsConnected = false;
boolean nostrIsSubscribed = false;
boolean nostrIsSubscribing = true;
String subIdZap;
void setupNostrNotify(bool asDatasource, bool zapNotify)
{
nostr::esp32::ESP32Platform::initNostr(false);
// time_t now;
// time(&now);
// struct tm *utcTimeInfo;
// utcTimeInfo = gmtime(&now);
// time_t utcNow = mktime(utcTimeInfo);
// time_t timestamp60MinutesAgo = utcNow - 3600;
try
{
transport = nostr::esp32::ESP32Platform::getTransport();
nostr::NostrPool *pool = new nostr::NostrPool(transport);
String relay = preferences.getString("nostrRelay");
String pubKey = preferences.getString("nostrPubKey");
pools.push_back(pool);
std::vector<nostr::NostrRelay *> *relays = pool->getConnectedRelays();
if (zapNotify)
{
subscribeZaps(pool, relay, 60);
}
if (asDatasource)
{
String subId = pool->subscribeMany(
{relay},
{// First filter
{
{"kinds", {"1"}},
{"since", {String(getMinutesAgo(60))}},
{"authors", {pubKey}},
}},
handleNostrEventCallback,
onNostrSubscriptionClosed,
onNostrSubscriptionEose
);
Serial.println(F("[ Nostr ] Subscribing to Nostr Data Feed"));
}
for (nostr::NostrRelay *relay : *relays)
{
Serial.println("[ Nostr ] Registering to connection events of: " + relay->getUrl());
relay->getConnection()->addConnectionStatusListener([](const nostr::ConnectionStatus &status)
{
static const char* STATUS_STRINGS[] = {"UNKNOWN", "CONNECTED", "DISCONNECTED", "ERROR"};
int statusIndex = static_cast<int>(status);
nostrIsConnected = (status == nostr::ConnectionStatus::CONNECTED);
if (!nostrIsConnected) {
nostrIsSubscribed = false;
}
});
}
}
catch (const std::exception &e)
{
Serial.println("[ Nostr ] Error: " + String(e.what()));
}
}
void nostrTask(void *pvParameters)
{
DataSourceType dataSource = getDataSource();
if(dataSource == NOSTR_SOURCE) {
int blockFetch = getBlockFetch();
processNewBlock(blockFetch);
}
while (1)
{
for (nostr::NostrPool *pool : pools)
{
// Run internal loop: refresh relays, complete pending connections, send
// pending messages
pool->loop();
if (!nostrIsSubscribed && !nostrIsSubscribing) {
Serial.println(F("Not subscribed"));
subscribeZaps(pool, preferences.getString("nostrRelay"), 1);
}
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void setupNostrTask()
{
xTaskCreate(nostrTask, "nostrTask", 8192, NULL, 10, &nostrTaskHandle);
}
boolean nostrConnected()
{
return nostrIsConnected;
}
void onNostrSubscriptionClosed(const String &subId, const String &reason)
{
// This is the callback that will be called when the subscription is
// closed
Serial.println("[ Nostr ] Subscription closed: " + reason);
}
void onNostrSubscriptionEose(const String &subId)
{
// This is the callback that will be called when the subscription is
// EOSE
Serial.println("[ Nostr ] Subscription EOSE: " + subId);
nostrIsSubscribing = false;
nostrIsSubscribed = true;
}
void handleNostrEventCallback(const String &subId, nostr::SignedNostrEvent *event)
{
JsonDocument doc;
JsonArray arr = doc["data"].to<JsonArray>();
event->toSendableEvent(arr);
// Early return if array is invalid
if (arr.size() < 2 || !arr[1].is<JsonObject>()) {
return;
}
JsonObject obj = arr[1].as<JsonObject>();
JsonArray tags = obj["tags"].as<JsonArray>();
if (!tags) {
return;
}
// Use direct value access instead of multiple comparisons
String typeValue;
uint medianFee = 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>();
}
break;
}
}
// Process the data
if (!typeValue.isEmpty()) {
if (typeValue == "priceUsd") {
processNewPrice(obj["content"].as<uint>(), CURRENCY_USD);
}
else if (typeValue == "blockHeight") {
processNewBlock(obj["content"].as<uint>());
}
if (medianFee != 0) {
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},
{
{
{"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) {
JsonDocument doc;
JsonArray arr = doc["data"].to<JsonArray>();
event->toSendableEvent(arr);
// Early return if invalid
if (arr.size() < 2 || !arr[1].is<JsonObject>()) {
return;
}
JsonObject obj = arr[1].as<JsonObject>();
JsonArray tags = obj["tags"].as<JsonArray>();
if (!tags) {
return;
}
uint64_t zapAmount = 0;
String zapPubkey;
for (JsonArray tag : tags) {
if (tag.size() != 2) continue;
const char *key = tag[0];
const char *value = tag[1];
if (!key || !value) continue;
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.printf("Got a zap of %llu sats for %s\n", zapAmount, zapPubkey.c_str());
}
uint64_t timerPeriod = 0;
if (isTimerActive())
{
// store timer periode before making inactive to prevent artifacts
timerPeriod = getTimerSeconds();
esp_timer_stop(screenRotateTimer);
}
ScreenHandler::setCurrentScreen(SCREEN_CUSTOM);
setEpdContent(textEpdContent);
vTaskDelay(pdMS_TO_TICKS(315 * NUM_SCREENS) + pdMS_TO_TICKS(250));
if (preferences.getBool("ledFlashOnZap", DEFAULT_LED_FLASH_ON_ZAP))
{
queueLedEffect(LED_EFFECT_NOSTR_ZAP);
}
if (timerPeriod > 0)
{
esp_timer_start_periodic(screenRotateTimer,
timerPeriod * usPerSecond);
}
}

30
src/lib/nostr_notify.hpp Normal file
View file

@ -0,0 +1,30 @@
#pragma once
#include "shared.hpp"
#include <ArduinoJson.h>
#include <nostrdisplay_handler.hpp>
#include <string>
#include "esp32/ESP32Platform.h"
#include "NostrEvent.h"
#include "NostrPool.h"
#include "price_notify.hpp"
#include "block_notify.hpp"
#include "lib/timers.hpp"
void setupNostrNotify(bool asDatasource, bool zapNotify);
void nostrTask(void *pvParameters);
void setupNostrTask();
boolean nostrConnected();
void handleNostrEventCallback(const String &subId, nostr::SignedNostrEvent *event);
void handleNostrZapCallback(const String &subId, nostr::SignedNostrEvent *event);
void onNostrSubscriptionClosed(const String &subId, const String &reason);
void onNostrSubscriptionEose(const String &subId);
time_t getMinutesAgo(int min);
void subscribeZaps(nostr::NostrPool *pool, const String &relay, int minutesAgo);

View file

@ -1,9 +1,15 @@
#include "ota.hpp"
TaskHandle_t taskOtaHandle = NULL;
bool isOtaUpdating = false;
QueueHandle_t otaQueue;
void setupOTA() {
if (preferences.getBool("otaEnabled", true)) {
void setupOTA()
{
if (preferences.getBool("otaEnabled", DEFAULT_OTA_ENABLED))
{
ArduinoOTA.onStart(onOTAStart);
ArduinoOTA.onProgress(onOTAProgress);
@ -15,31 +21,38 @@ void setupOTA() {
ArduinoOTA.setRebootOnSuccess(false);
ArduinoOTA.begin();
// downloadUpdate();
otaQueue = xQueueCreate(1, sizeof(UpdateMessage));
xTaskCreate(handleOTATask, "handleOTA", 4096, NULL, tskIDLE_PRIORITY,
xTaskCreate(handleOTATask, "handleOTA", 8192, NULL, 20,
&taskOtaHandle);
}
}
void onOTAProgress(unsigned int progress, unsigned int total) {
void onOTAProgress(unsigned int progress, unsigned int total)
{
uint percentage = progress / (total / 100);
pixels.fill(pixels.Color(0, 255, 0));
if (percentage < 100) {
if (percentage < 100)
{
pixels.setPixelColor(0, pixels.Color(0, 0, 0));
}
if (percentage < 75) {
if (percentage < 75)
{
pixels.setPixelColor(1, pixels.Color(0, 0, 0));
}
if (percentage < 50) {
if (percentage < 50)
{
pixels.setPixelColor(2, pixels.Color(0, 0, 0));
}
if (percentage < 25) {
if (percentage < 25)
{
pixels.setPixelColor(3, pixels.Color(0, 0, 0));
}
pixels.show();
}
void onOTAStart() {
void onOTAStart()
{
forceFullRefresh();
std::array<String, NUM_SCREENS> epdContent = {"U", "P", "D", "A",
"T", "E", "!"};
@ -47,97 +60,385 @@ void onOTAStart() {
// Stop all timers
esp_timer_stop(screenRotateTimer);
esp_timer_stop(minuteTimer);
isOtaUpdating = true;
// Stop or suspend all tasks
// vTaskSuspend(priceUpdateTaskHandle);
// vTaskSuspend(blockUpdateTaskHandle);
vTaskSuspend(workerTaskHandle);
vTaskSuspend(taskScreenRotateTaskHandle);
vTaskSuspend(workerTaskHandle);
vTaskSuspend(eventSourceTaskHandle);
ButtonHandler::suspendTask();
vTaskSuspend(ledTaskHandle);
vTaskSuspend(buttonTaskHandle);
stopWebServer();
// stopWebServer();
stopBlockNotify();
stopPriceNotify();
}
void handleOTATask(void *parameter) {
for (;;) {
ArduinoOTA.handle(); // Allow OTA updates to occur
vTaskDelay(pdMS_TO_TICKS(2500));
}
}
void handleOTATask(void *parameter)
{
UpdateMessage msg;
void downloadUpdate() {
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
http.setUserAgent(USER_AGENT);
for (;;)
{
if (xQueueReceive(otaQueue, &msg, 0) == pdTRUE)
{
if (msg.updateType == UPDATE_ALL) {
isOtaUpdating = true;
queueLedEffect(LED_FLASH_UPDATE);
int resultWebUi = downloadUpdateHandler(UPDATE_WEBUI);
queueLedEffect(LED_FLASH_UPDATE);
int resultFw = downloadUpdateHandler(UPDATE_FIRMWARE);
// Send HTTP request to CoinGecko API
http.useHTTP10(true);
http.begin(client,
"https://api.github.com/repos/btclock/btclock_v3/releases/latest");
int httpCode = http.GET();
if (httpCode == 200) {
// WiFiClient * stream = http->getStreamPtr();
StaticJsonDocument<64> filter;
JsonObject filter_assets_0 = filter["assets"].createNestedObject();
filter_assets_0["name"] = true;
filter_assets_0["browser_download_url"] = true;
SpiRamJsonDocument doc(1536);
DeserializationError error = deserializeJson(
doc, http.getStream(), DeserializationOption::Filter(filter));
if (error) {
Serial.print("deserializeJson() failed: ");
Serial.println(error.c_str());
return;
}
String downloadUrl;
for (JsonObject asset : doc["assets"].as<JsonArray>()) {
if (asset["name"].as<String>().compareTo("firmware.bin") == 0) {
downloadUrl = asset["browser_download_url"].as<String>();
break;
if (resultWebUi == 0 && resultFw == 0) {
ESP.restart();
} else {
queueLedEffect(LED_FLASH_ERROR);
vTaskDelay(pdMS_TO_TICKS(3000));
ESP.restart();
}
}
}
Serial.printf("Download update from %s", downloadUrl);
// esp_http_client_config_t config = {
// .url = CONFIG_FIRMWARE_UPGRADE_URL,
// };
// esp_https_ota_config_t ota_config = {
// .http_config = &config,
// };
// esp_err_t ret = esp_https_ota(&ota_config);
// if (ret == ESP_OK)
// {
// esp_restart();
// }
ArduinoOTA.handle(); // Allow OTA updates to occur
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
void onOTAError(ota_error_t error) {
Serial.println("\nOTA update error, restarting");
ReleaseInfo getLatestRelease(const String &fileToDownload)
{
String releaseUrl = preferences.getString("gitReleaseUrl");
WiFiClientSecure client;
// client.setCACert(isrg_root_x1cert);
client.setCACertBundle(rootca_crt_bundle_start);
HTTPClient http;
http.begin(client, releaseUrl);
http.setUserAgent(USER_AGENT);
int httpCode = http.GET();
ReleaseInfo info = {"", ""};
if (httpCode > 0)
{
String payload = http.getString();
JsonDocument doc;
deserializeJson(doc, payload);
JsonArray assets = doc["assets"];
for (JsonObject asset : assets)
{
String assetName = asset["name"].as<String>();
if (assetName == fileToDownload)
{
info.fileUrl = asset["browser_download_url"].as<String>();
}
else if (assetName == fileToDownload + ".sha256")
{
info.checksumUrl = asset["browser_download_url"].as<String>();
}
if (!info.fileUrl.isEmpty() && !info.checksumUrl.isEmpty())
{
break;
}
}
Serial.printf("Latest release URL: %s\r\n", info.fileUrl.c_str());
Serial.printf("Checksum URL: %s\r\n", info.checksumUrl.c_str());
}
http.end();
return info;
}
int downloadUpdateHandler(char updateType)
{
WiFiClientSecure client;
client.setCACertBundle(rootca_crt_bundle_start);
HTTPClient http;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
ReleaseInfo latestRelease;
switch (updateType)
{
case UPDATE_FIRMWARE:
{
latestRelease = getLatestRelease(getFirmwareFilename());
}
break;
case UPDATE_WEBUI:
{
latestRelease = getLatestRelease(getWebUiFilename());
// updateWebUi(latestRelease.fileUrl, U_SPIFFS);
// return 0;
}
break;
}
// First, download the expected SHA256
String expectedSHA256 = downloadSHA256(latestRelease.checksumUrl);
if (expectedSHA256.isEmpty())
{
Serial.println(F("Failed to get SHA256 checksum. Aborting update."));
return false;
}
http.begin(client, latestRelease.fileUrl);
http.setUserAgent(USER_AGENT);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK)
{
int contentLength = http.getSize();
if (contentLength > 0)
{
// Allocate memory to store the firmware
uint8_t *firmware = (uint8_t *)malloc(contentLength);
if (!firmware)
{
Serial.println(F("Not enough memory to store firmware"));
return false;
}
WiFiClient *stream = http.getStreamPtr();
size_t bytesRead = 0;
while (bytesRead < contentLength)
{
size_t available = stream->available();
if (available)
{
size_t readBytes = stream->readBytes(firmware + bytesRead, available);
bytesRead += readBytes;
}
yield(); // Allow background tasks to run
}
if (bytesRead != contentLength)
{
Serial.println(F("Failed to read entire firmware"));
free(firmware);
return false;
}
// Calculate SHA256
String calculated_sha256 = calculateSHA256(firmware, contentLength);
Serial.print(F("Calculated checksum: "));
Serial.println(calculated_sha256);
Serial.print(F("Expected checksum: "));
Serial.println(expectedSHA256);
if (calculated_sha256 != expectedSHA256)
{
Serial.println(F("Checksum mismatch. Aborting update."));
free(firmware);
return false;
}
Update.onProgress(onOTAProgress);
if (Update.begin(contentLength, updateType))
{
onOTAStart();
size_t written = Update.write(firmware, contentLength);
if (written == contentLength)
{
Serial.println("Written : " + String(written) + " successfully");
free(firmware);
}
else
{
Serial.println("Written only : " + String(written) + "/" + String(contentLength) + ". Retry?");
free(firmware);
return 503;
}
if (Update.end())
{
Serial.println(F("OTA done!"));
if (Update.isFinished())
{
Serial.println(F("Update successfully completed. Rebooting."));
// ESP.restart();
}
else
{
Serial.println(F("Update not finished? Something went wrong!"));
free(firmware);
return 503;
}
}
else
{
Serial.println("Error Occurred. Error #: " + String(Update.getError()));
free(firmware);
return 503;
}
}
else
{
Serial.println(F("Not enough space to begin OTA"));
free(firmware);
return 503;
}
}
else
{
Serial.println(F("Invalid content length"));
return 503;
}
}
else
{
Serial.printf("HTTP error: %d\n", httpCode);
return 503;
}
http.end();
return 0;
}
void updateWebUi(String latestRelease, int command)
{
WiFiClientSecure client;
client.setCACertBundle(rootca_crt_bundle_start);
HTTPClient http;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.begin(client, latestRelease);
http.setUserAgent(USER_AGENT);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK)
{
int contentLength = http.getSize();
if (contentLength > 0)
{
uint8_t *buffer = (uint8_t *)malloc(contentLength);
if (buffer)
{
WiFiClient *stream = http.getStreamPtr();
size_t written = stream->readBytes(buffer, contentLength);
if (written == contentLength)
{
String expectedSHA256 = "";
if (command == U_FLASH)
{
expectedSHA256 = downloadSHA256(getFirmwareFilename());
Serial.print("Expected checksum: ");
Serial.println(expectedSHA256);
}
String calculated_sha256 = calculateSHA256(buffer, contentLength);
Serial.print("Checksum is ");
Serial.println(calculated_sha256);
if ((command == U_FLASH && expectedSHA256.equals(calculated_sha256)) || command == U_SPIFFS)
{
Serial.println(F("Checksum verified. Proceeding with update."));
Update.onProgress(onOTAProgress);
if (Update.begin(contentLength, command))
{
onOTAStart();
Update.write(buffer, contentLength);
if (Update.end())
{
Serial.println(F("Update complete. Rebooting."));
ESP.restart();
}
else
{
Serial.println(F("Error in update process."));
}
}
else
{
Serial.println(F("Not enough space to begin OTA"));
}
}
else
{
Serial.println(F("Checksum mismatch. Aborting update."));
}
}
else
{
Serial.println(F("Error downloading firmware"));
}
free(buffer);
}
else
{
Serial.println(F("Not enough memory to allocate buffer"));
}
}
else
{
Serial.println(F("Invalid content length"));
}
}
else
{
Serial.print(httpCode);
Serial.println("Error on HTTP request");
}
}
void onOTAError(ota_error_t error)
{
Serial.println(F("\nOTA update error, restarting"));
Wire.end();
SPI.end();
isOtaUpdating = false;
delay(1000);
ESP.restart();
}
void onOTAComplete()
{
Serial.println(F("\nOTA update finished"));
Wire.end();
SPI.end();
delay(1000);
ESP.restart();
}
void onOTAComplete() {
Serial.println("\nOTA update finished");
Wire.end();
SPI.end();
delay(1000);
ESP.restart();
bool getIsOTAUpdating()
{
return isOtaUpdating;
}
String downloadSHA256(const String &sha256Url)
{
if (sha256Url.isEmpty())
{
Serial.println(F("Failed to get SHA256 file URL"));
return "";
}
WiFiClientSecure client;
client.setCACertBundle(rootca_crt_bundle_start);
HTTPClient http;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.begin(client, sha256Url);
http.setUserAgent(USER_AGENT);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK)
{
String sha256 = http.getString();
sha256.trim(); // Remove any whitespace or newline characters
return sha256;
}
else
{
Serial.printf("Failed to download SHA256 file. HTTP error: %d\n", httpCode);
return "";
}
}

View file

@ -1,13 +1,38 @@
#pragma once
#include <Arduino.h>
#include <ArduinoOTA.h>
#include "lib/config.hpp"
#include "lib/shared.hpp"
#include "lib/timers.hpp"
#ifndef UPDATE_MESSAGE_HPP
#define UPDATE_MESSAGE_HPP
typedef struct {
char updateType;
} UpdateMessage;
#endif
extern QueueHandle_t otaQueue;
struct ReleaseInfo {
String fileUrl;
String checksumUrl;
};
void setupOTA();
void onOTAStart();
void handleOTATask(void *parameter);
void onOTAProgress(unsigned int progress, unsigned int total);
void downloadUpdate();
// void downloadUpdate();
void onOTAError(ota_error_t error);
void onOTAComplete();
int downloadUpdateHandler(char updateType);
ReleaseInfo getLatestRelease(const String& fileToDownload);
bool getIsOTAUpdating();
void updateWebUi(String latestRelease, int command);
String downloadSHA256(const String& filename);

View file

@ -1,57 +0,0 @@
#include "price_fetch.hpp"
const PROGMEM char *cgApiUrl =
"https://api.coingecko.com/api/v3/simple/"
"price?ids=bitcoin&vs_currencies=usd%2Ceur";
TaskHandle_t priceFetchTaskHandle;
void taskPriceFetch(void *pvParameters) {
WiFiClientSecure *client = new WiFiClientSecure;
client->setInsecure();
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
HTTPClient *http = new HTTPClient();
http->setUserAgent(USER_AGENT);
// Send HTTP request to CoinGecko API
http->begin(*client, cgApiUrl);
int httpCode = http->GET();
// Parse JSON response and extract average price
uint usdPrice, eurPrice;
if (httpCode == 200) {
String payload = http->getString();
StaticJsonDocument<96> doc;
deserializeJson(doc, payload);
// usdPrice = doc["bitcoin"]["usd"];
eurPrice = doc["bitcoin"]["eur"].as<uint>();
setPrice(eurPrice);
if (workQueue != nullptr && (getCurrentScreen() == SCREEN_BTC_TICKER ||
getCurrentScreen() == SCREEN_MSCW_TIME ||
getCurrentScreen() == SCREEN_MARKET_CAP)) {
WorkItem priceUpdate = {TASK_PRICE_UPDATE, 0};
xQueueSend(workQueue, &priceUpdate, portMAX_DELAY);
}
preferences.putUInt("lastPrice", eurPrice);
} else {
Serial.print(
F("Error retrieving BTC/USD price (CoinGecko). HTTP status code: "));
Serial.println(httpCode);
if (httpCode == -1) {
WiFi.reconnect();
}
}
}
}
void setupPriceFetchTask() {
xTaskCreate(taskPriceFetch, "priceFetch", (6 * 1024), NULL, tskIDLE_PRIORITY,
&priceFetchTaskHandle);
xTaskNotifyGive(priceFetchTaskHandle);
}

View file

@ -1,10 +0,0 @@
#include <Arduino.h>
#include <HTTPClient.h>
#include "lib/config.hpp"
#include "lib/shared.hpp"
extern TaskHandle_t priceFetchTaskHandle;
void setupPriceFetchTask();
void taskPriceFetch(void *pvParameters);

View file

@ -2,116 +2,190 @@
const char *wsServerPrice = "wss://ws.coincap.io/prices?assets=bitcoin";
// const char* coinCapWsCert = R"(-----BEGIN CERTIFICATE-----
// MIIFMjCCBNmgAwIBAgIQBtgXvFyc28MsvQ1HjCnXJTAKBggqhkjOPQQDAjBKMQsw
// CQYDVQQGEwJVUzEZMBcGA1UEChMQQ2xvdWRmbGFyZSwgSW5jLjEgMB4GA1UEAxMX
// Q2xvdWRmbGFyZSBJbmMgRUNDIENBLTMwHhcNMjMwNTEwMDAwMDAwWhcNMjQwNTA5
// MjM1OTU5WjB1MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQG
// A1UEBxMNU2FuIEZyYW5jaXNjbzEZMBcGA1UEChMQQ2xvdWRmbGFyZSwgSW5jLjEe
// MBwGA1UEAxMVc25pLmNsb3VkZmxhcmVzc2wuY29tMFkwEwYHKoZIzj0CAQYIKoZI
// zj0DAQcDQgAEpvFIXzQKHuqTo+IE6c6sB4p0PMXK1KsseEGf2UN/CNRhG5hO7lr8
// JtXrPZkawWBysZxOsEoetkPrDHMugCLfXKOCA3QwggNwMB8GA1UdIwQYMBaAFKXO
// N+rrsHUOlGeItEX62SQQh5YfMB0GA1UdDgQWBBShsZDJohaR1a5E0Qj7yblZjKDC
// gDA6BgNVHREEMzAxggwqLmNvaW5jYXAuaW+CCmNvaW5jYXAuaW+CFXNuaS5jbG91
// ZGZsYXJlc3NsLmNvbTAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUH
// AwEGCCsGAQUFBwMCMHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6Ly9jcmwzLmRpZ2lj
// ZXJ0LmNvbS9DbG91ZGZsYXJlSW5jRUNDQ0EtMy5jcmwwN6A1oDOGMWh0dHA6Ly9j
// cmw0LmRpZ2ljZXJ0LmNvbS9DbG91ZGZsYXJlSW5jRUNDQ0EtMy5jcmwwPgYDVR0g
// BDcwNTAzBgZngQwBAgIwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3dy5kaWdpY2Vy
// dC5jb20vQ1BTMHYGCCsGAQUFBwEBBGowaDAkBggrBgEFBQcwAYYYaHR0cDovL29j
// c3AuZGlnaWNlcnQuY29tMEAGCCsGAQUFBzAChjRodHRwOi8vY2FjZXJ0cy5kaWdp
// Y2VydC5jb20vQ2xvdWRmbGFyZUluY0VDQ0NBLTMuY3J0MAwGA1UdEwEB/wQCMAAw
// ggF+BgorBgEEAdZ5AgQCBIIBbgSCAWoBaAB1AO7N0GTV2xrOxVy3nbTNE6Iyh0Z8
// vOzew1FIWUZxH7WbAAABiAPnoRAAAAQDAEYwRAIgAP2W09OozuhmKeKKMsaVBcae
// o+nPHF1WUWk0i387YYYCIDIM1Wll7/4O3GNx2/Fx9bC6pi69Uya4pLxsCfW3fZMe
// AHYASLDja9qmRzQP5WoC+p0w6xxSActW3SyB2bu/qznYhHMAAAGIA+eg+QAABAMA
// RzBFAiEAuNpSqrbx47gYBgBMz5M6q0CnV/WMJqWQOxYFKrwfwVACIH3nCs4bKToT
// e+MiBrqSDaekixk4kPFEQESO9qHCkWY5AHcA2ra/az+1tiKfm8K7XGvocJFxbLtR
// hIU0vaQ9MEjX+6sAAAGIA+eg1gAABAMASDBGAiEAolCFl2IfbOHUPAOxoi4BLclS
// v9FVXb7LwIvTuCfyrEQCIQDcvehwhV9XGopKGl17F2LYYKI7hvlO3RmpPZQJt1da
// MDAKBggqhkjOPQQDAgNHADBEAiAXRWZ/JVMsfpSFFTHQHUSqRnQ/7cCOWx+9svIy
// mYnFZQIgHMEG0Cm7O4cn5KUzKOsTwwK+2U15s/jPUQi2n2IDTEM=
// -----END CERTIFICATE-----)";
// WebsocketsClient client;
esp_websocket_client_handle_t clientPrice = NULL;
uint currentPrice = 30000;
esp_websocket_client_config_t config;
uint currentPrice = 90000;
unsigned long int lastPriceUpdate;
bool priceNotifyInit = false;
std::map<char, std::uint64_t> currencyMap;
std::map<char, unsigned long int> lastUpdateMap;
WebSocketsClient priceNotifyWs;
void setupPriceNotify() {
// currentPrice = preferences.get("lastPrice", 30000);
void setupPriceNotify()
{
config = {.uri = wsServerPrice,
.user_agent = USER_AGENT};
config.cert_pem = isrg_root_x1cert;
config.task_stack = (6*1024);
esp_websocket_client_config_t config = {.uri = wsServerPrice,
// .task_stack = (7*1024),
// .cert_pem = coinCapWsCert,
.user_agent = USER_AGENT};
clientPrice = esp_websocket_client_init(&config);
esp_websocket_register_events(clientPrice, WEBSOCKET_EVENT_ANY,
onWebsocketPriceEvent, clientPrice);
esp_websocket_client_start(clientPrice);
// priceNotifyWs.beginSSL("ws.coincap.io", 443, "/prices?assets=bitcoin");
// 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) {
int32_t event_id, void *event_data)
{
esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data;
switch (event_id) {
case WEBSOCKET_EVENT_CONNECTED:
Serial.println(F("Connected to CoinCap.io WebSocket"));
break;
case WEBSOCKET_EVENT_DATA:
onWebsocketPriceMessage(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;
switch (event_id)
{
case WEBSOCKET_EVENT_CONNECTED:
Serial.println("Connected to " + String(config.uri) + " WebSocket");
priceNotifyInit = true;
break;
case WEBSOCKET_EVENT_DATA:
onWebsocketPriceMessage(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;
}
}
void onWebsocketPriceMessage(esp_websocket_event_data_t *event_data) {
SpiRamJsonDocument doc(event_data->data_len);
void onWebsocketPriceMessage(esp_websocket_event_data_t *event_data)
{
JsonDocument doc;
deserializeJson(doc, (char *)event_data->data_ptr);
if (doc.containsKey("bitcoin")) {
if (currentPrice != doc["bitcoin"].as<long>()) {
uint minSecPriceUpd = preferences.getUInt(
"minSecPriceUpd", DEFAULT_SECONDS_BETWEEN_PRICE_UPDATE);
uint currentTime = esp_timer_get_time() / 1000000;
if (lastPriceUpdate == 0 ||
(currentTime - lastPriceUpdate) > minSecPriceUpd) {
// const unsigned long oldPrice = currentPrice;
currentPrice = doc["bitcoin"].as<uint>();
preferences.putUInt("lastPrice", currentPrice);
lastPriceUpdate = currentTime;
// if (abs((int)(oldPrice-currentPrice)) > round(0.0015*oldPrice)) {
if (workQueue != nullptr && (getCurrentScreen() == SCREEN_BTC_TICKER ||
getCurrentScreen() == SCREEN_MSCW_TIME ||
getCurrentScreen() == SCREEN_MARKET_CAP)) {
WorkItem priceUpdate = {TASK_PRICE_UPDATE, 0};
xQueueSend(workQueue, &priceUpdate, portMAX_DELAY);
}
//}
}
if (doc.containsKey("bitcoin"))
{
if (currentPrice != doc["bitcoin"].as<long>())
{
processNewPrice(doc["bitcoin"].as<long>(), CURRENCY_USD);
}
}
}
uint getPrice() { return currentPrice; }
void processNewPrice(uint newPrice, char currency)
{
uint minSecPriceUpd = preferences.getUInt(
"minSecPriceUpd", DEFAULT_SECONDS_BETWEEN_PRICE_UPDATE);
uint currentTime = esp_timer_get_time() / 1000000;
void setPrice(uint newPrice) { currentPrice = newPrice; }
if (lastUpdateMap.find(currency) == lastUpdateMap.end() ||
(currentTime - lastUpdateMap[currency]) > minSecPriceUpd)
{
// const unsigned long oldPrice = currentPrice;
currencyMap[currency] = newPrice;
if (currency == CURRENCY_USD && ( lastUpdateMap[currency] == 0 ||
(currentTime - lastUpdateMap[currency]) > 120))
{
preferences.putUInt("lastPrice", currentPrice);
}
lastUpdateMap[currency] = currentTime;
// if (abs((int)(oldPrice-currentPrice)) > round(0.0015*oldPrice)) {
if (workQueue != nullptr && (ScreenHandler::getCurrentScreen() == SCREEN_BTC_TICKER ||
ScreenHandler::getCurrentScreen() == SCREEN_SATS_PER_CURRENCY ||
ScreenHandler::getCurrentScreen() == SCREEN_MARKET_CAP))
{
WorkItem priceUpdate = {TASK_PRICE_UPDATE, currency};
xQueueSend(workQueue, &priceUpdate, portMAX_DELAY);
}
//}
}
}
bool isPriceNotifyConnected() {
if (clientPrice == NULL) return false;
uint getLastPriceUpdate(char currency)
{
if (lastUpdateMap.find(currency) == lastUpdateMap.end())
{
return 0;
}
return lastUpdateMap[currency];
}
uint getPrice(char currency)
{
if (currencyMap.find(currency) == currencyMap.end())
{
return 0;
}
return currencyMap[currency];
}
void setPrice(uint newPrice, char currency)
{
currencyMap[currency] = newPrice;
}
bool isPriceNotifyConnected()
{
if (clientPrice == NULL)
return false;
return esp_websocket_client_is_connected(clientPrice);
}
void stopPriceNotify() {
bool getPriceNotifyInit()
{
return priceNotifyInit;
}
void stopPriceNotify()
{
if (clientPrice == NULL)
return;
esp_websocket_client_close(clientPrice, pdMS_TO_TICKS(5000));
esp_websocket_client_stop(clientPrice);
esp_websocket_client_destroy(clientPrice);
clientPrice = NULL;
}
void restartPriceNotify()
{
stopPriceNotify();
if (clientPrice == NULL)
{
setupPriceNotify();
return;
}
// esp_websocket_client_close(clientPrice, pdMS_TO_TICKS(5000));
// esp_websocket_client_stop(clientPrice);
// esp_websocket_client_start(clientPrice);
}

View file

@ -3,7 +3,7 @@
#include <Arduino.h>
#include <ArduinoJson.h>
#include <esp_websocket_client.h>
#include "block_notify.hpp"
#include <string>
#include "lib/screen_handler.hpp"
@ -12,10 +12,19 @@ void setupPriceNotify();
void onWebsocketPriceEvent(void *handler_args, esp_event_base_t base,
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();
void setPrice(uint newPrice);
uint getPrice(char currency);
void setPrice(uint newPrice, char currency);
//void processNewPrice(uint newPrice);
void processNewPrice(uint newPrice, char currency);
bool isPriceNotifyConnected();
void stopPriceNotify();
void stopPriceNotify();
void restartPriceNotify();
bool getPriceNotifyInit();
uint getLastPriceUpdate(char currency);

View file

@ -1,289 +1,340 @@
#include "screen_handler.hpp"
// TaskHandle_t priceUpdateTaskHandle;
// TaskHandle_t blockUpdateTaskHandle;
// TaskHandle_t timeUpdateTaskHandle;
TaskHandle_t taskScreenRotateTaskHandle;
TaskHandle_t workerTaskHandle;
esp_timer_handle_t screenRotateTimer;
esp_timer_handle_t minuteTimer;
std::array<std::string, NUM_SCREENS> taskEpdContent = {"", "", "", "",
"", "", ""};
std::string priceString;
#define WORK_QUEUE_SIZE 10
QueueHandle_t workQueue = NULL;
uint currentScreen;
// Initialize static members
uint ScreenHandler::currentScreen = SCREEN_BLOCK_HEIGHT;
uint ScreenHandler::currentCurrency = CURRENCY_USD;
void workerTask(void *pvParameters) {
WorkItem receivedItem;
std::array<std::string, NUM_SCREENS> taskEpdContent = {};
while (1) {
// Wait for a work item to be available in the queue
if (xQueueReceive(workQueue, &receivedItem, portMAX_DELAY)) {
uint firstIndex = 0;
// Process the work item based on its type
switch (receivedItem.type) {
case TASK_PRICE_UPDATE: {
uint price = getPrice();
char priceSymbol = '$';
if (preferences.getBool("fetchEurPrice", false)) {
priceSymbol = '[';
}
if (getCurrentScreen() == SCREEN_BTC_TICKER) {
taskEpdContent = parsePriceData(price, priceSymbol);
} else if (getCurrentScreen() == SCREEN_MSCW_TIME) {
taskEpdContent = parseSatsPerCurrency(price, priceSymbol);
} else {
taskEpdContent =
parseMarketCap(getBlockHeight(), price, priceSymbol,
preferences.getBool("mcapBigChar", true));
}
setEpdContent(taskEpdContent);
break;
}
case TASK_BLOCK_UPDATE: {
if (getCurrentScreen() != SCREEN_HALVING_COUNTDOWN) {
taskEpdContent = parseBlockHeight(getBlockHeight());
} else {
taskEpdContent = parseHalvingCountdown(getBlockHeight());
}
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
}
// Convert existing functions to static member functions
void ScreenHandler::setCurrentScreen(uint newScreen) {
if (newScreen != SCREEN_CUSTOM) {
preferences.putUInt("currentScreen", newScreen);
}
currentScreen = newScreen;
switch (currentScreen) {
case SCREEN_TIME: {
WorkItem timeUpdate = {TASK_TIME_UPDATE, 0};
xQueueSend(workQueue, &timeUpdate, portMAX_DELAY);
break;
}
case SCREEN_HALVING_COUNTDOWN:
case SCREEN_BLOCK_HEIGHT: {
WorkItem blockUpdate = {TASK_BLOCK_UPDATE, 0};
xQueueSend(workQueue, &blockUpdate, portMAX_DELAY);
break;
}
case SCREEN_MARKET_CAP:
case SCREEN_SATS_PER_CURRENCY:
case SCREEN_BTC_TICKER: {
WorkItem priceUpdate = {TASK_PRICE_UPDATE, 0};
xQueueSend(workQueue, &priceUpdate, portMAX_DELAY);
break;
}
case SCREEN_BLOCK_FEE_RATE: {
WorkItem blockUpdate = {TASK_FEE_UPDATE, 0};
xQueueSend(workQueue, &blockUpdate, portMAX_DELAY);
break;
}
case SCREEN_BITAXE_BESTDIFF:
case SCREEN_BITAXE_HASHRATE: {
if (preferences.getBool("bitaxeEnabled", DEFAULT_BITAXE_ENABLED)) {
WorkItem bitaxeUpdate = {TASK_BITAXE_UPDATE, 0};
xQueueSend(workQueue, &bitaxeUpdate, portMAX_DELAY);
} else {
setCurrentScreen(SCREEN_BLOCK_HEIGHT);
return;
}
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);
}
void ScreenHandler::setCurrentCurrency(char currency) {
currentCurrency = currency;
preferences.putUChar("lastCurrency", currency);
}
bool ScreenHandler::isCurrencySpecific(uint screen) {
switch (screen) {
case SCREEN_BTC_TICKER:
case SCREEN_SATS_PER_CURRENCY:
case SCREEN_MARKET_CAP:
return true;
default:
return false;
}
}
bool ScreenHandler::handleCurrencyRotation(bool forward) {
if ((getDataSource() == BTCLOCK_SOURCE || getDataSource() == CUSTOM_SOURCE) && isCurrencySpecific(getCurrentScreen())) {
std::vector<std::string> ac = getActiveCurrencies();
if (ac.empty()) return false;
std::string curCode = getCurrencyCode(getCurrentCurrency());
auto it = std::find(ac.begin(), ac.end(), curCode);
if (it == ac.end()) {
// Current currency not found in active currencies - initialize based on direction
setCurrentCurrency(getCurrencyChar(forward ? ac.front() : ac.back()));
setCurrentScreen(getCurrentScreen());
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()));
}
}
setCurrentScreen(nextScreen);
}
void ScreenHandler::previousScreen() {
if (handleCurrencyRotation(false)) return;
int currentIndex = findScreenIndexByValue(getCurrentScreen());
int prevScreen = findNextVisibleScreen(currentIndex, false);
// 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();
if (!ac.empty()) {
setCurrentCurrency(getCurrencyChar(ac.back()));
}
}
setCurrentScreen(prevScreen);
}
void ScreenHandler::showSystemStatusScreen() {
std::array<String, NUM_SCREENS> sysStatusEpdContent;
std::fill(sysStatusEpdContent.begin(), sysStatusEpdContent.end(), "");
String ipAddr = WiFi.localIP().toString();
String subNet = WiFi.subnetMask().toString();
sysStatusEpdContent[0] = "IP/Subnet";
int ipAddrPos = 0;
int subnetPos = 0;
for (int i = 0; i < 4; i++) {
sysStatusEpdContent[1 + i] = ipAddr.substring(0, ipAddr.indexOf('.')) +
"/" + subNet.substring(0, subNet.indexOf('.'));
ipAddrPos = ipAddr.indexOf('.') + 1;
subnetPos = subNet.indexOf('.') + 1;
ipAddr = ipAddr.substring(ipAddrPos);
subNet = subNet.substring(subnetPos);
}
sysStatusEpdContent[NUM_SCREENS - 2] = "RAM/Status";
sysStatusEpdContent[NUM_SCREENS - 1] =
String((int)round(ESP.getFreeHeap() / 1024)) + "/" +
(int)round(ESP.getHeapSize() / 1024);
setCurrentScreen(SCREEN_CUSTOM);
setEpdContent(sysStatusEpdContent);
}
// Keep these as free functions
void workerTask(void *pvParameters) {
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(getBitAxeHashRate()) :
parseBitaxeBestDiff(getBitaxeBestDiff());
setEpdContent(taskEpdContent);
break;
}
case TASK_MINING_POOL_STATS_UPDATE: {
if (currentScreenValue != SCREEN_MINING_POOL_STATS_HASHRATE &&
currentScreenValue != SCREEN_MINING_POOL_STATS_EARNINGS) break;
taskEpdContent = (currentScreenValue == SCREEN_MINING_POOL_STATS_HASHRATE) ?
parseMiningPoolStatsHashRate(getMiningPoolStatsHashRate(), *getMiningPool()) :
parseMiningPoolStatsDailyEarnings(getMiningPoolStatsDailyEarnings(),
getMiningPool()->getDailyEarningsLabel(), *getMiningPool());
setEpdContent(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 {
taskEpdContent =
parseMarketCap(getBlockHeight(), price, currency,
preferences.getBool("mcapBigChar", DEFAULT_MCAP_BIG_CHAR));
}
setEpdContent(taskEpdContent);
break;
}
case TASK_FEE_UPDATE: {
if (currentScreenValue == SCREEN_BLOCK_FEE_RATE) {
taskEpdContent = parseBlockFees(static_cast<std::uint16_t>(getBlockMedianFee()));
setEpdContent(taskEpdContent);
}
break;
}
case TASK_BLOCK_UPDATE: {
if (currentScreenValue != SCREEN_HALVING_COUNTDOWN) {
taskEpdContent = parseBlockHeight(getBlockHeight());
} else {
taskEpdContent = parseHalvingCountdown(getBlockHeight(), preferences.getBool("useBlkCountdown", DEFAULT_USE_BLOCK_COUNTDOWN));
}
if (currentScreenValue == SCREEN_HALVING_COUNTDOWN ||
currentScreenValue == SCREEN_BLOCK_HEIGHT) {
setEpdContent(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];
}
setEpdContent(taskEpdContent);
}
break;
}
// Add more cases for additional task types
}
}
}
}
}
void taskScreenRotate(void *pvParameters) {
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
int nextScreen = (currentScreen + 1) % SCREEN_COUNT;
String key = "screen" + String(nextScreen) + "Visible";
while (!preferences.getBool(key.c_str(), true)) {
nextScreen = (nextScreen + 1) % SCREEN_COUNT;
key = "screen" + String(nextScreen) + "Visible";
for (;;) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
ScreenHandler::nextScreen();
}
setCurrentScreen(nextScreen);
}
}
void IRAM_ATTR minuteTimerISR(void *arg) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// vTaskNotifyGiveFromISR(timeUpdateTaskHandle, &xHigherPriorityTaskWoken);
WorkItem timeUpdate = {TASK_TIME_UPDATE, 0};
xQueueSendFromISR(workQueue, &timeUpdate, &xHigherPriorityTaskWoken);
if (priceFetchTaskHandle != NULL) {
vTaskNotifyGiveFromISR(priceFetchTaskHandle, &xHigherPriorityTaskWoken);
}
if (xHigherPriorityTaskWoken == pdTRUE) {
portYIELD_FROM_ISR();
}
}
void IRAM_ATTR screenRotateTimerISR(void *arg) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(taskScreenRotateTaskHandle, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken == pdTRUE) {
portYIELD_FROM_ISR();
}
}
void setupTasks() {
workQueue = xQueueCreate(WORK_QUEUE_SIZE, sizeof(WorkItem));
workQueue = xQueueCreate(WORK_QUEUE_SIZE, sizeof(WorkItem));
xTaskCreate(workerTask, "workerTask", 4096, NULL, tskIDLE_PRIORITY,
&workerTaskHandle);
xTaskCreate(workerTask, "workerTask", 4096, NULL, tskIDLE_PRIORITY,
&workerTaskHandle);
xTaskCreate(taskScreenRotate, "rotateScreen", 2048, NULL, tskIDLE_PRIORITY,
&taskScreenRotateTaskHandle);
xTaskCreate(taskScreenRotate, "rotateScreen", 4096, NULL, tskIDLE_PRIORITY,
&taskScreenRotateTaskHandle);
waitUntilNoneBusy();
setCurrentScreen(preferences.getUInt("currentScreen", 0));
waitUntilNoneBusy();
if (findScreenIndexByValue(preferences.getUInt("currentScreen", DEFAULT_CURRENT_SCREEN)) != -1)
ScreenHandler::setCurrentScreen(preferences.getUInt("currentScreen", DEFAULT_CURRENT_SCREEN));
}
void setupTimeUpdateTimer(void *pvParameters) {
const esp_timer_create_args_t minuteTimerConfig = {
.callback = &minuteTimerISR, .name = "minute_timer"};
esp_timer_create(&minuteTimerConfig, &minuteTimer);
time_t currentTime;
struct tm timeinfo;
time(&currentTime);
localtime_r(&currentTime, &timeinfo);
uint32_t secondsUntilNextMinute = 60 - timeinfo.tm_sec;
if (secondsUntilNextMinute > 0)
vTaskDelay(pdMS_TO_TICKS((secondsUntilNextMinute * 1000)));
esp_timer_start_periodic(minuteTimer, usPerMinute);
WorkItem timeUpdate = {TASK_TIME_UPDATE, 0};
xQueueSend(workQueue, &timeUpdate, portMAX_DELAY);
// xTaskNotifyGive(timeUpdateTaskHandle);
vTaskDelete(NULL);
}
void setupScreenRotateTimer(void *pvParameters) {
const esp_timer_create_args_t screenRotateTimerConfig = {
.callback = &screenRotateTimerISR, .name = "screen_rotate_timer"};
esp_timer_create(&screenRotateTimerConfig, &screenRotateTimer);
if (preferences.getBool("timerActive", true)) {
esp_timer_start_periodic(screenRotateTimer,
getTimerSeconds() * usPerSecond);
}
vTaskDelete(NULL);
}
uint getTimerSeconds() { return preferences.getUInt("timerSeconds", 1800); }
bool isTimerActive() { return esp_timer_is_active(screenRotateTimer); }
void setTimerActive(bool status) {
if (status) {
esp_timer_start_periodic(screenRotateTimer,
getTimerSeconds() * usPerSecond);
queueLedEffect(LED_EFFECT_START_TIMER);
preferences.putBool("timerActive", true);
} else {
esp_timer_stop(screenRotateTimer);
queueLedEffect(LED_EFFECT_PAUSE_TIMER);
preferences.putBool("timerActive", false);
}
if (eventSourceTaskHandle != NULL) xTaskNotifyGive(eventSourceTaskHandle);
}
void toggleTimerActive() { setTimerActive(!isTimerActive()); }
uint getCurrentScreen() { return currentScreen; }
void setCurrentScreen(uint newScreen) {
if (newScreen != SCREEN_CUSTOM) {
preferences.putUInt("currentScreen", newScreen);
}
currentScreen = newScreen;
switch (currentScreen) {
case SCREEN_TIME: {
WorkItem timeUpdate = {TASK_TIME_UPDATE, 0};
xQueueSend(workQueue, &timeUpdate, portMAX_DELAY);
// xTaskNotifyGive(timeUpdateTaskHandle);
break;
}
case SCREEN_HALVING_COUNTDOWN:
case SCREEN_BLOCK_HEIGHT: {
WorkItem blockUpdate = {TASK_BLOCK_UPDATE, 0};
xQueueSend(workQueue, &blockUpdate, portMAX_DELAY);
// xTaskNotifyGive(blockUpdateTaskHandle);
break;
}
case SCREEN_MARKET_CAP:
case SCREEN_MSCW_TIME:
case SCREEN_BTC_TICKER: {
WorkItem priceUpdate = {TASK_PRICE_UPDATE, 0};
xQueueSend(workQueue, &priceUpdate, portMAX_DELAY);
// xTaskNotifyGive(priceUpdateTaskHandle);
break;
}
}
if (eventSourceTaskHandle != NULL) xTaskNotifyGive(eventSourceTaskHandle);
}
void nextScreen() {
int newCurrentScreen = (getCurrentScreen() + 1) % SCREEN_COUNT;
String key = "screen" + String(newCurrentScreen) + "Visible";
while (!preferences.getBool(key.c_str(), true)) {
newCurrentScreen = (newCurrentScreen + 1) % SCREEN_COUNT;
key = "screen" + String(newCurrentScreen) + "Visible";
}
setCurrentScreen(newCurrentScreen);
}
void previousScreen() {
int newCurrentScreen = modulo(getCurrentScreen() - 1, SCREEN_COUNT);
String key = "screen" + String(newCurrentScreen) + "Visible";
while (!preferences.getBool(key.c_str(), true)) {
newCurrentScreen = modulo(newCurrentScreen - 1, SCREEN_COUNT);
key = "screen" + String(newCurrentScreen) + "Visible";
}
setCurrentScreen(newCurrentScreen);
}
void showSystemStatusScreen() {
std::array<String, NUM_SCREENS> sysStatusEpdContent = {"", "", "", "",
"", "", ""};
String ipAddr = WiFi.localIP().toString();
String subNet = WiFi.subnetMask().toString();
sysStatusEpdContent[0] = "IP/Subnet";
int ipAddrPos = 0;
int subnetPos = 0;
for (int i = 0; i < 4; i++) {
sysStatusEpdContent[1 + i] = ipAddr.substring(0, ipAddr.indexOf('.')) +
"/" + subNet.substring(0, subNet.indexOf('.'));
ipAddrPos = ipAddr.indexOf('.') + 1;
subnetPos = subNet.indexOf('.') + 1;
ipAddr = ipAddr.substring(ipAddrPos);
subNet = subNet.substring(subnetPos);
}
sysStatusEpdContent[NUM_SCREENS - 2] = "RAM/Status";
sysStatusEpdContent[NUM_SCREENS - 1] =
String((int)round(ESP.getFreeHeap() / 1024)) + "/" +
(int)round(ESP.getHeapSize() / 1024);
setCurrentScreen(SCREEN_CUSTOM);
setEpdContent(sysStatusEpdContent);
void cleanup() {
vQueueDelete(workQueue);
// Add any other cleanup needed
}

View file

@ -5,26 +5,25 @@
#include <freertos/task.h>
#include <data_handler.hpp>
#include <bitaxe_handler.hpp>
#include "lib/mining_pool/mining_pool_stats_handler.hpp"
#include "lib/epd.hpp"
#include "lib/price_fetch.hpp"
#include "lib/shared.hpp"
// extern TaskHandle_t priceUpdateTaskHandle;
// extern TaskHandle_t blockUpdateTaskHandle;
// extern TaskHandle_t timeUpdateTaskHandle;
#define WORK_QUEUE_SIZE 10
extern TaskHandle_t workerTaskHandle;
extern TaskHandle_t taskScreenRotateTaskHandle;
extern esp_timer_handle_t screenRotateTimer;
extern esp_timer_handle_t minuteTimer;
extern QueueHandle_t workQueue;
typedef enum {
TASK_PRICE_UPDATE,
TASK_BLOCK_UPDATE,
TASK_TIME_UPDATE
TASK_FEE_UPDATE,
TASK_TIME_UPDATE,
TASK_BITAXE_UPDATE,
TASK_MINING_POOL_STATS_UPDATE
} TaskType;
typedef struct {
@ -32,28 +31,26 @@ typedef struct {
char data;
} 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);
uint getCurrentScreen();
void setCurrentScreen(uint newScreen);
void nextScreen();
void previousScreen();
void showSystemStatusScreen();
void setupTimeUpdateTimer(void *pvParameters);
void setupScreenRotateTimer(void *pvParameters);
void IRAM_ATTR minuteTimerISR(void *arg);
void IRAM_ATTR screenRotateTimerISR(void *arg);
// void taskPriceUpdate(void *pvParameters);
// void taskBlockUpdate(void *pvParameters);
// void taskTimeUpdate(void *pvParameters);
void taskScreenRotate(void *pvParameters);
uint getTimerSeconds();
bool isTimerActive();
void setTimerActive(bool status);
void toggleTimerActive();
void setupTasks();
void cleanup();

182
src/lib/shared.cpp Normal file
View file

@ -0,0 +1,182 @@
#include "shared.hpp"
// const char *github_root_ca =
// "-----BEGIN CERTIFICATE-----\n"
// "MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL\n"
// "MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl\n"
// "eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT\n"
// "JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx\n"
// "MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT\n"
// "Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg\n"
// "VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm\n"
// "aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo\n"
// "I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng\n"
// "o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G\n"
// "A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD\n"
// "VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB\n"
// "zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW\n"
// "RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg=\n"
// "-----END CERTIFICATE-----\n"
// "-----BEGIN CERTIFICATE-----\n"
// "MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\n"
// "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"
// "d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\n"
// "MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\n"
// "MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n"
// "b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n"
// "9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n"
// "2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n"
// "1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\n"
// "q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\n"
// "tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\n"
// "vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\n"
// "BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\n"
// "5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\n"
// "1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\n"
// "NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\n"
// "Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\n"
// "8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\n"
// "pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\n"
// "MrY=\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
uint8_t input_buffer[3 * input_buffer_pixels]; // up to depth 24
uint8_t output_row_mono_buffer[max_row_width / 8]; // buffer for at least one row of b/w bits
uint8_t output_row_color_buffer[max_row_width / 8]; // buffer for at least one row of color bits
uint8_t mono_palette_buffer[max_palette_pixels / 8]; // palette buffer for depth <= 8 b/w
uint8_t color_palette_buffer[max_palette_pixels / 8]; // palette buffer for depth <= 8 c/w
uint16_t rgb_palette_buffer[max_palette_pixels]; // palette buffer for depth <= 8 for buffered graphics, needed for 7-color display
#endif
// Function to calculate SHA-256 hash
String calculateSHA256(uint8_t *data, size_t len)
{
byte shaResult[32];
mbedtls_md_context_t ctx;
mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;
mbedtls_md_init(&ctx);
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 0);
mbedtls_md_starts(&ctx);
mbedtls_md_update(&ctx, data, len);
mbedtls_md_finish(&ctx, shaResult);
mbedtls_md_free(&ctx);
char sha256_str[65];
for (int i = 0; i < 32; i++)
{
sprintf(sha256_str + (i * 2), "%02x", shaResult[i]);
}
sha256_str[64] = 0;
return String(sha256_str);
}
String calculateSHA256(WiFiClient *stream, size_t contentLength) {
mbedtls_md_context_t ctx;
mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;
mbedtls_md_init(&ctx);
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 0);
mbedtls_md_starts(&ctx);
uint8_t buff[1024];
size_t bytesRead = 0;
while (bytesRead < contentLength) {
size_t toRead = min((size_t)(contentLength - bytesRead), sizeof(buff));
size_t readBytes = stream->readBytes(buff, toRead);
if (readBytes == 0) {
break;
}
mbedtls_md_update(&ctx, buff, readBytes);
bytesRead += readBytes;
}
byte shaResult[32];
mbedtls_md_finish(&ctx, shaResult);
mbedtls_md_free(&ctx);
String result = "";
for (int i = 0; i < sizeof(shaResult); i++) {
char str[3];
sprintf(str, "%02x", (int)shaResult[i]);
result += str;
}
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,47 +1,111 @@
#pragma once
#include <Adafruit_MCP23X17.h>
#include "MCP23017.h"
// #include <zlib_turbo.h>
#include <ArduinoJson.h>
#include <WiFiClientSecure.h>
#include <Preferences.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <GxEPD2.h>
#include <GxEPD2_BW.h>
#include <mbedtls/md.h>
#include "esp_crt_bundle.h"
#include <Update.h>
#include <HTTPClient.h>
#include <mutex>
#include <utils.hpp>
extern Adafruit_MCP23X17 mcp1;
#ifdef IS_BTCLOCK_S3
extern Adafruit_MCP23X17 mcp2;
#include "defaults.hpp"
#define USER_AGENT "BTClock/3.0"
extern MCP23017 mcp1;
#ifdef IS_BTCLOCK_V8
extern MCP23017 mcp2;
#endif
extern Preferences preferences;
extern std::mutex mcpMutex;
#ifdef VERSION_EPD_2_13
#define EPD_CLASS GxEPD2_213_B74
#endif
#ifdef VERSION_EPD_2_9
#define EPD_CLASS GxEPD2_290_T94
#endif
const PROGMEM int SCREEN_BLOCK_HEIGHT = 0;
const PROGMEM int SCREEN_MSCW_TIME = 1;
const PROGMEM int SCREEN_BTC_TICKER = 2;
const PROGMEM int SCREEN_TIME = 3;
const PROGMEM int SCREEN_HALVING_COUNTDOWN = 4;
const PROGMEM int SCREEN_MARKET_CAP = 5;
const PROGMEM int SCREEN_BLOCK_FEE_RATE = 6;
const PROGMEM int SCREEN_SATS_PER_CURRENCY = 10;
const PROGMEM int SCREEN_BTC_TICKER = 20;
const PROGMEM int SCREEN_MARKET_CAP = 30;
const PROGMEM int SCREEN_MINING_POOL_STATS_HASHRATE = 70;
const PROGMEM int SCREEN_MINING_POOL_STATS_EARNINGS = 71;
const PROGMEM int SCREEN_BITAXE_HASHRATE = 80;
const PROGMEM int SCREEN_BITAXE_BESTDIFF = 81;
const PROGMEM int SCREEN_COUNTDOWN = 98;
const PROGMEM int SCREEN_CUSTOM = 99;
const int SCREEN_COUNT = 6;
const int SCREEN_COUNT = 7;
const PROGMEM int screens[SCREEN_COUNT] = {
SCREEN_BLOCK_HEIGHT, SCREEN_MSCW_TIME, SCREEN_BTC_TICKER,
SCREEN_TIME, SCREEN_HALVING_COUNTDOWN, SCREEN_MARKET_CAP};
SCREEN_BLOCK_HEIGHT, SCREEN_SATS_PER_CURRENCY, SCREEN_BTC_TICKER,
SCREEN_TIME, SCREEN_HALVING_COUNTDOWN, SCREEN_MARKET_CAP,
SCREEN_BLOCK_FEE_RATE};
const int usPerSecond = 1000000;
const int usPerMinute = 60 * usPerSecond;
struct SpiRamAllocator {
void *allocate(size_t size) {
return heap_caps_malloc(size, MALLOC_CAP_SPIRAM);
}
// extern const char *github_root_ca;
extern const char *isrg_root_x1cert;
void deallocate(void *pointer) { heap_caps_free(pointer); }
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");
void *reallocate(void *ptr, size_t new_size) {
return heap_caps_realloc(ptr, new_size, MALLOC_CAP_SPIRAM);
}
// 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_WEBUI = U_SPIFFS;
const PROGMEM char UPDATE_ALL = 99;
struct ScreenMapping {
int value;
const char* name;
};
using SpiRamJsonDocument = BasicJsonDocument<SpiRamAllocator>;
String calculateSHA256(uint8_t* data, size_t len);
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);
}
};
}
class HttpHelper {
public:
static HTTPClient* begin(const String& url);
static void end(HTTPClient* http);
private:
static WiFiClientSecure secureClient;
static bool certBundleSet;
static WiFiClient insecureClient;
};

90
src/lib/timers.cpp Normal file
View file

@ -0,0 +1,90 @@
#include "timers.hpp"
esp_timer_handle_t screenRotateTimer;
esp_timer_handle_t minuteTimer;
void setupTimeUpdateTimer(void *pvParameters) {
const esp_timer_create_args_t minuteTimerConfig = {
.callback = &minuteTimerISR, .name = "minute_timer"};
esp_timer_create(&minuteTimerConfig, &minuteTimer);
time_t currentTime;
struct tm timeinfo;
time(&currentTime);
localtime_r(&currentTime, &timeinfo);
uint32_t secondsUntilNextMinute = 60 - timeinfo.tm_sec;
if (secondsUntilNextMinute > 0)
vTaskDelay(pdMS_TO_TICKS((secondsUntilNextMinute * 1000)));
esp_timer_start_periodic(minuteTimer, usPerMinute);
WorkItem timeUpdate = {TASK_TIME_UPDATE, 0};
xQueueSend(workQueue, &timeUpdate, portMAX_DELAY);
// xTaskNotifyGive(timeUpdateTaskHandle);
vTaskDelete(NULL);
}
void setupScreenRotateTimer(void *pvParameters) {
const esp_timer_create_args_t screenRotateTimerConfig = {
.callback = &screenRotateTimerISR, .name = "screen_rotate_timer"};
esp_timer_create(&screenRotateTimerConfig, &screenRotateTimer);
if (preferences.getBool("timerActive", DEFAULT_TIMER_ACTIVE)) {
esp_timer_start_periodic(screenRotateTimer,
getTimerSeconds() * usPerSecond);
}
vTaskDelete(NULL);
}
uint getTimerSeconds() { return preferences.getUInt("timerSeconds", DEFAULT_TIMER_SECONDS); }
bool isTimerActive() { return esp_timer_is_active(screenRotateTimer); }
void setTimerActive(bool status) {
if (status) {
esp_timer_start_periodic(screenRotateTimer,
getTimerSeconds() * usPerSecond);
queueLedEffect(LED_EFFECT_START_TIMER);
preferences.putBool("timerActive", true);
} else {
esp_timer_stop(screenRotateTimer);
queueLedEffect(LED_EFFECT_PAUSE_TIMER);
preferences.putBool("timerActive", false);
}
if (eventSourceTaskHandle != NULL) xTaskNotifyGive(eventSourceTaskHandle);
}
void toggleTimerActive() { setTimerActive(!isTimerActive()); }
void IRAM_ATTR minuteTimerISR(void *arg) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// vTaskNotifyGiveFromISR(timeUpdateTaskHandle, &xHigherPriorityTaskWoken);
WorkItem timeUpdate = {TASK_TIME_UPDATE, 0};
xQueueSendFromISR(workQueue, &timeUpdate, &xHigherPriorityTaskWoken);
if (bitaxeFetchTaskHandle != NULL) {
vTaskNotifyGiveFromISR(bitaxeFetchTaskHandle, &xHigherPriorityTaskWoken);
}
if (miningPoolStatsFetchTaskHandle != NULL) {
vTaskNotifyGiveFromISR(miningPoolStatsFetchTaskHandle, &xHigherPriorityTaskWoken);
}
if (xHigherPriorityTaskWoken == pdTRUE) {
portYIELD_FROM_ISR();
}
}
void IRAM_ATTR screenRotateTimerISR(void *arg) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(taskScreenRotateTaskHandle, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken == pdTRUE) {
portYIELD_FROM_ISR();
}
}

22
src/lib/timers.hpp Normal file
View file

@ -0,0 +1,22 @@
#pragma once
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "lib/shared.hpp"
#include "lib/screen_handler.hpp"
extern esp_timer_handle_t screenRotateTimer;
extern esp_timer_handle_t minuteTimer;
void setupTimeUpdateTimer(void *pvParameters);
void setupScreenRotateTimer(void *pvParameters);
void IRAM_ATTR minuteTimerISR(void *arg);
void IRAM_ATTR screenRotateTimerISR(void *arg);
uint getTimerSeconds();
bool isTimerActive();
void setTimerActive(bool status);
void toggleTimerActive();

175
src/lib/v2_notify.cpp Normal file
View file

@ -0,0 +1,175 @@
#include "v2_notify.hpp"
using namespace V2Notify;
namespace V2Notify
{
WebSocketsClient webSocket;
TaskHandle_t v2NotifyTaskHandle;
String currentHostname;
void setupV2Notify()
{
String hostname = "ws.btclock.dev";
if (getDataSource() == CUSTOM_SOURCE)
{
Serial.println(F("Connecting to custom source"));
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
{
Serial.println(F("Connecting to V2 source"));
webSocket.beginSSL(hostname, 443, "/api/v2/ws");
}
webSocket.onEvent(V2Notify::onWebsocketV2Event);
webSocket.setReconnectInterval(5000);
webSocket.enableHeartbeat(15000, 3000, 2);
V2Notify::setupV2NotifyTask();
currentHostname = hostname;
}
void onWebsocketV2Event(WStype_t type, uint8_t *payload, size_t length)
{
switch (type)
{
case WStype_DISCONNECTED:
Serial.print(F("[WSc] Disconnected!\n"));
break;
case WStype_CONNECTED:
{
Serial.print(F("[WSc] Connected to "));
Serial.print(currentHostname);
Serial.print(F(": "));
Serial.println((char *)payload);
JsonDocument response;
response["type"] = "subscribe";
response["eventType"] = "blockfee";
size_t responseLength = measureMsgPack(response);
uint8_t *buffer = new uint8_t[responseLength];
serializeMsgPack(response, buffer, responseLength);
webSocket.sendBIN(buffer, responseLength);
delete[] buffer;
buffer = new uint8_t[responseLength];
response["type"] = "subscribe";
response["eventType"] = "blockheight";
responseLength = measureMsgPack(response);
buffer = new uint8_t[responseLength];
serializeMsgPack(response, buffer, responseLength);
webSocket.sendBIN(buffer, responseLength);
delete[] buffer;
buffer = new uint8_t[responseLength];
response["type"] = "subscribe";
response["eventType"] = "price";
JsonArray currenciesArray = response["currencies"].to<JsonArray>();
for (const auto &str : getActiveCurrencies())
{
currenciesArray.add(str);
}
// response["currencies"] = currenciesArray;
responseLength = measureMsgPack(response);
buffer = new uint8_t[responseLength];
serializeMsgPack(response, buffer, responseLength);
webSocket.sendBIN(buffer, responseLength);
break;
}
case WStype_TEXT:
Serial.print(F("[WSc] get text: "));
Serial.println((char *)payload);
// send message to server
// webSocket.sendTXT("message here");
break;
case WStype_BIN:
{
JsonDocument doc;
DeserializationError error = deserializeMsgPack(doc, payload, length);
V2Notify::handleV2Message(doc);
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 handleV2Message(JsonDocument doc)
{
if (doc.containsKey("blockheight"))
{
uint newBlockHeight = doc["blockheight"].as<uint>();
if (newBlockHeight == getBlockHeight())
{
return;
}
processNewBlock(newBlockHeight);
}
else if (doc.containsKey("blockfee"))
{
uint medianFee = doc["blockfee"].as<uint>();
processNewBlockFee(medianFee);
}
else if (doc.containsKey("price"))
{
// Iterate through the key-value pairs of the "price" object
for (JsonPair kv : doc["price"].as<JsonObject>())
{
const char *currency = kv.key().c_str();
uint newPrice = kv.value().as<uint>();
processNewPrice(newPrice, getCurrencyChar(currency));
}
}
}
void taskV2Notify(void *pvParameters)
{
for (;;)
{
webSocket.loop();
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void setupV2NotifyTask()
{
xTaskCreate(V2Notify::taskV2Notify, "v2Notify", (6 * 1024), NULL, tskIDLE_PRIORITY,
&V2Notify::v2NotifyTaskHandle);
}
bool isV2NotifyConnected()
{
return webSocket.isConnected();
}
}

26
src/lib/v2_notify.hpp Normal file
View file

@ -0,0 +1,26 @@
#pragma once
#include <Arduino.h>
#include <ArduinoJson.h>
#include <esp_websocket_client.h>
#include "block_notify.hpp"
#include <string>
#include "lib/screen_handler.hpp"
namespace V2Notify {
extern TaskHandle_t v2NotifyTaskHandle;
void setupV2NotifyTask();
void taskV2Notify(void *pvParameters);
void setupV2Notify();
void onWebsocketV2Event(WStype_t type, uint8_t * payload, size_t length);
void handleV2Message(JsonDocument doc);
bool isV2NotifyConnected();
}
// void stopV2Notify();
// void restartV2Notify();
// bool getPriceNotifyInit();
// uint getLastPriceUpdate();

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,7 @@
#include "lib/price_notify.hpp"
#include "lib/screen_handler.hpp"
#include "webserver/OneParamRewrite.hpp"
#include "lib/mining_pool/pool_factory.hpp"
extern TaskHandle_t eventSourceTaskHandle;
@ -21,18 +22,25 @@ void stopWebServer();
void setupWebserver();
bool processEpdColorSettings(AsyncWebServerRequest *request);
void onApiStatus(AsyncWebServerRequest *request);
void onApiSystemStatus(AsyncWebServerRequest *request);
void onApiSetWifiTxPower(AsyncWebServerRequest *request);
void onApiScreenControl(AsyncWebServerRequest *request);
void onApiShowScreen(AsyncWebServerRequest *request);
void onApiShowCurrency(AsyncWebServerRequest *request);
void onApiShowText(AsyncWebServerRequest *request);
void onApiIdentify(AsyncWebServerRequest *request);
void onApiShowTextAdvanced(AsyncWebServerRequest *request, JsonVariant &json);
void onApiActionPause(AsyncWebServerRequest *request);
void onApiActionTimerRestart(AsyncWebServerRequest *request);
void onApiSettingsGet(AsyncWebServerRequest *request);
void onApiSettingsPost(AsyncWebServerRequest *request);
void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json);
void onApiFullRefresh(AsyncWebServerRequest *request);
@ -42,11 +50,28 @@ void onApiLightsSetColor(AsyncWebServerRequest *request);
void onApiLightsSetJson(AsyncWebServerRequest *request, JsonVariant &json);
void onApiRestart(AsyncWebServerRequest *request);
void onFirmwareUpdate(AsyncWebServerRequest *request);
void asyncFirmwareUpdateHandler(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);
void asyncFileUpdateHandler(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final, int command);
void asyncWebuiUpdateHandler(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);
void onAutoUpdateFirmware(AsyncWebServerRequest *request);
void onIndex(AsyncWebServerRequest *request);
void onNotFound(AsyncWebServerRequest *request);
StaticJsonDocument<512> getLedStatusObject();
StaticJsonDocument<768> getStatusObject();
JsonDocument getLedStatusObject();
JsonDocument getStatusObject();
void eventSourceUpdate();
void eventSourceTask(void *pvParameters);
void eventSourceTask(void *pvParameters);
void onApiStopDataSources(AsyncWebServerRequest *request);
void onApiRestartDataSources(AsyncWebServerRequest *request);
#ifdef HAS_FRONTLIGHT
void onApiFrontlightOn(AsyncWebServerRequest *request);
void onApiFrontlightFlash(AsyncWebServerRequest *request);
void onApiFrontlightSetBrightness(AsyncWebServerRequest *request);
void onApiFrontlightStatus(AsyncWebServerRequest *request);
void onApiFrontlightOff(AsyncWebServerRequest *request);
#endif

View file

@ -1,5 +1,5 @@
/*
* Copyright 2023 Djuri Baars
* Copyright 2023-2024 Djuri Baars
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,39 +20,141 @@
#include "lib/config.hpp"
uint wifiLostConnection;
uint priceNotifyLostConnection = 0;
uint blockNotifyLostConnection = 0;
int64_t getUptime() {
return esp_timer_get_time() / 1000000;
}
void handlePriceNotifyDisconnection() {
if (priceNotifyLostConnection == 0) {
priceNotifyLostConnection = getUptime();
Serial.println(F("Lost price notification connection, trying to reconnect..."));
}
if ((getUptime() - priceNotifyLostConnection) > 300) { // 5 minutes timeout
Serial.println(F("Price notification connection lost for 5 minutes, restarting handler..."));
restartPriceNotify();
priceNotifyLostConnection = 0;
}
}
void handleBlockNotifyDisconnection() {
if (blockNotifyLostConnection == 0) {
blockNotifyLostConnection = getUptime();
Serial.println(F("Lost block notification connection, trying to reconnect..."));
}
if ((getUptime() - blockNotifyLostConnection) > 300) { // 5 minutes timeout
Serial.println(F("Block notification connection lost for 5 minutes, restarting handler..."));
restartBlockNotify();
blockNotifyLostConnection = 0;
}
}
void handleFrontlight() {
#ifdef HAS_FRONTLIGHT
if (hasLightLevel() && preferences.getUInt("luxLightToggle", DEFAULT_LUX_LIGHT_TOGGLE) != 0) {
uint lightLevel = getLightLevel();
uint luxThreshold = preferences.getUInt("luxLightToggle", DEFAULT_LUX_LIGHT_TOGGLE);
if (lightLevel <= 1 && preferences.getBool("flOffWhenDark", DEFAULT_FL_OFF_WHEN_DARK)) {
if (frontlightIsOn()) frontlightFadeOutAll();
} else if (lightLevel < luxThreshold && !frontlightIsOn()) {
frontlightFadeInAll();
} else if (frontlightIsOn() && lightLevel > luxThreshold) {
frontlightFadeOutAll();
}
}
#endif
}
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..."));
delay(2000);
ESP.restart();
}
WiFi.begin();
} else if (wifiLostConnection) {
wifiLostConnection = 0;
Serial.println(F("Connection restored, reset timer."));
}
}
void checkMissedBlocks() {
Serial.println(F("Long time (45 min) since last block, checking if I missed anything..."));
int currentBlock = getBlockFetch();
if (currentBlock != -1) {
if (currentBlock != getBlockHeight()) {
Serial.println(F("Detected stuck block height... restarting block handler."));
restartBlockNotify();
}
setLastBlockUpdate(getUptime());
}
}
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
if (getBlockNotifyInit() && !isBlockNotifyConnected()) {
handleBlockNotifyDisconnection();
} else if (blockNotifyLostConnection > 0 && isBlockNotifyConnected()) {
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 ((getLastBlockUpdate() - getUptime()) > 45 * 60) {
checkMissedBlocks();
}
}
extern "C" void app_main() {
initArduino();
Serial.begin(115200);
setup();
bool thirdPartySource = getDataSource() == THIRD_PARTY_SOURCE;
while (true) {
// vTaskList(ptrTaskList);
// Serial.println(F("**********************************"));
// Serial.println(F("Task State Prio Stack Num"));
// Serial.println(F("**********************************"));
// Serial.print(ptrTaskList);
// Serial.println(F("**********************************"));
if (eventSourceTaskHandle != NULL)
if (eventSourceTaskHandle != NULL) {
xTaskNotifyGive(eventSourceTaskHandle);
}
if (!WiFi.isConnected()) {
if (!wifiLostConnection) {
wifiLostConnection = esp_timer_get_time() / 1000000;
Serial.println("Lost WiFi connection, trying to reconnect...");
if (!getIsOTAUpdating()) {
handleFrontlight();
checkWiFiConnection();
if (thirdPartySource) {
monitorDataConnections();
}
if (((esp_timer_get_time() / 1000000) - wifiLostConnection) > 600) {
Serial.println("Still no connection after 10 minutes, restarting...");
delay(2000);
ESP.restart();
if (getUptime() - getLastTimeSync() > 24 * 60 * 60) {
Serial.println(F("Last time update is longer than 24 hours ago, sync again"));
syncTime();
}
WiFi.begin();
} else if (wifiLostConnection) {
wifiLostConnection = 0;
Serial.println("Connection restored, reset timer.");
}
vTaskDelay(pdMS_TO_TICKS(5000));

View file

@ -1,68 +1,310 @@
#include <data_handler.hpp>
#include <unity.h>
void setUp(void) {
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) {
void tearDown(void)
{
// clean stuff up here
}
void test_CorrectSatsPerDollarConversion(void) {
std::array<std::string, NUM_SCREENS> output = parseSatsPerCurrency(37253, '$');
void test_CorrectSatsPerDollarConversion(void)
{
std::array<std::string, NUM_SCREENS> output = parseSatsPerCurrency(37253, CURRENCY_USD, false);
TEST_ASSERT_EQUAL_STRING("MSCW/TIME", output[0].c_str());
TEST_ASSERT_EQUAL_STRING("2", output[NUM_SCREENS-4].c_str());
TEST_ASSERT_EQUAL_STRING("6", output[NUM_SCREENS-3].c_str());
TEST_ASSERT_EQUAL_STRING("8", output[NUM_SCREENS-2].c_str());
TEST_ASSERT_EQUAL_STRING("4", output[NUM_SCREENS-1].c_str());
TEST_ASSERT_EQUAL_STRING("2", output[NUM_SCREENS - 4].c_str());
TEST_ASSERT_EQUAL_STRING("6", output[NUM_SCREENS - 3].c_str());
TEST_ASSERT_EQUAL_STRING("8", output[NUM_SCREENS - 2].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_SixCharacterBlockHeight(void) {
void test_CorrectSatsPerPoundConversion(void)
{
std::array<std::string, NUM_SCREENS> output = parseSatsPerCurrency(37253, CURRENCY_GBP, false);
TEST_ASSERT_EQUAL_STRING("SATS/GBP", output[0].c_str());
TEST_ASSERT_EQUAL_STRING("2", output[NUM_SCREENS - 4].c_str());
TEST_ASSERT_EQUAL_STRING("6", output[NUM_SCREENS - 3].c_str());
TEST_ASSERT_EQUAL_STRING("8", output[NUM_SCREENS - 2].c_str());
TEST_ASSERT_EQUAL_STRING("4", output[NUM_SCREENS - 1].c_str());
}
void test_SixCharacterBlockHeight(void)
{
std::array<std::string, NUM_SCREENS> output = parseBlockHeight(999999);
TEST_ASSERT_EQUAL_STRING("BLOCK/HEIGHT", output[0].c_str());
TEST_ASSERT_EQUAL_STRING("9", output[1].c_str());
}
void test_SevenCharacterBlockHeight(void) {
void test_SevenCharacterBlockHeight(void)
{
std::array<std::string, NUM_SCREENS> output = parseBlockHeight(1000000);
TEST_ASSERT_EQUAL_STRING("1", output[0].c_str());
TEST_ASSERT_EQUAL_STRING("0", output[1].c_str());
}
void test_PriceOf100kusd(void) {
void test_FeeRateDisplay(void)
{
uint testValue = 21;
std::array<std::string, NUM_SCREENS> output = parseBlockFees(static_cast<std::uint16_t>(testValue));
TEST_ASSERT_EQUAL_STRING("FEE/RATE", output[0].c_str());
TEST_ASSERT_EQUAL_STRING("2", output[NUM_SCREENS - 3].c_str());
TEST_ASSERT_EQUAL_STRING("1", output[NUM_SCREENS - 2].c_str());
TEST_ASSERT_EQUAL_STRING("sat/vB", output[NUM_SCREENS - 1].c_str());
}
void test_PriceOf100kusd(void)
{
std::array<std::string, NUM_SCREENS> output = parsePriceData(100000, '$');
TEST_ASSERT_EQUAL_STRING("$", output[0].c_str());
TEST_ASSERT_EQUAL_STRING("1", output[1].c_str());
}
void test_PriceOf1MillionUsd(void) {
void test_PriceOf1MillionUsd(void)
{
std::array<std::string, NUM_SCREENS> output = parsePriceData(1000000, '$');
TEST_ASSERT_EQUAL_STRING("BTC/USD", output[0].c_str());
for (int i = 1; i <= NUM_SCREENS-3; i++) {
TEST_ASSERT_EQUAL_STRING(" ", output[i].c_str());
}
TEST_ASSERT_EQUAL_STRING("1", output[NUM_SCREENS-2].c_str());
TEST_ASSERT_EQUAL_STRING("M", output[NUM_SCREENS-1].c_str());
TEST_ASSERT_EQUAL_STRING("1", output[NUM_SCREENS - 5].c_str());
TEST_ASSERT_EQUAL_STRING(".", output[NUM_SCREENS - 4].c_str());
TEST_ASSERT_EQUAL_STRING("0", output[NUM_SCREENS - 3].c_str());
TEST_ASSERT_EQUAL_STRING("0", output[NUM_SCREENS - 2].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)
{
std::array<std::string, NUM_SCREENS> output = parseMarketCap(810000, 26000, '$', true);
TEST_ASSERT_EQUAL_STRING("USD/MCAP", output[0].c_str());
// TEST_ASSERT_EQUAL_STRING("$", output[NUM_SCREENS-6].c_str());
TEST_ASSERT_EQUAL_STRING("$", output[NUM_SCREENS - 5].c_str());
TEST_ASSERT_EQUAL_STRING("5", output[NUM_SCREENS - 4].c_str());
TEST_ASSERT_EQUAL_STRING("0", output[NUM_SCREENS - 3].c_str());
TEST_ASSERT_EQUAL_STRING("7", output[NUM_SCREENS - 2].c_str());
TEST_ASSERT_EQUAL_STRING("B", output[NUM_SCREENS - 1].c_str());
}
void test_Mcap1TrillionUsd(void)
{
std::array<std::string, NUM_SCREENS> output = parseMarketCap(831000, 52000, '$', true);
TEST_ASSERT_EQUAL_STRING("USD/MCAP", output[0].c_str());
TEST_ASSERT_EQUAL_STRING("$", output[NUM_SCREENS - 6].c_str());
TEST_ASSERT_EQUAL_STRING("1", output[NUM_SCREENS - 5].c_str());
TEST_ASSERT_EQUAL_STRING(".", output[NUM_SCREENS - 4].c_str());
TEST_ASSERT_EQUAL_STRING("0", output[NUM_SCREENS - 3].c_str());
TEST_ASSERT_EQUAL_STRING("2", output[NUM_SCREENS - 2].c_str());
TEST_ASSERT_EQUAL_STRING("T", output[NUM_SCREENS - 1].c_str());
}
void test_Mcap1TrillionUsdSmallChars(void)
{
std::array<std::string, NUM_SCREENS> output = parseMarketCap(831000, 52000, '$', false);
TEST_ASSERT_EQUAL_STRING("USD/MCAP", output[0].c_str());
std::string joined = joinArrayWithBrackets(output);
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("020", output[NUM_SCREENS - 4].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("825", output[NUM_SCREENS - 3].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("000", output[NUM_SCREENS - 2].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("000", output[NUM_SCREENS - 1].c_str(), joined.c_str());
}
void test_Mcap1TrillionEur(void)
{
std::array<std::string, NUM_SCREENS> output = parseMarketCap(831000, 52000, CURRENCY_EUR, true);
TEST_ASSERT_EQUAL_STRING("EUR/MCAP", output[0].c_str());
TEST_ASSERT_TRUE(CURRENCY_EUR == output[NUM_SCREENS - 6].c_str()[0]);
TEST_ASSERT_EQUAL_STRING("1", output[NUM_SCREENS - 5].c_str());
TEST_ASSERT_EQUAL_STRING(".", output[NUM_SCREENS - 4].c_str());
TEST_ASSERT_EQUAL_STRING("0", output[NUM_SCREENS - 3].c_str());
TEST_ASSERT_EQUAL_STRING("2", output[NUM_SCREENS - 2].c_str());
TEST_ASSERT_EQUAL_STRING("T", output[NUM_SCREENS - 1].c_str());
}
void test_Mcap1TrillionEurSmallChars(void)
{
std::array<std::string, NUM_SCREENS> output = parseMarketCap(831000, 52000, CURRENCY_EUR, false);
TEST_ASSERT_EQUAL_STRING("EUR/MCAP", output[0].c_str());
std::string joined = joinArrayWithBrackets(output);
char result[4];
snprintf(result, sizeof(result), " %c ", CURRENCY_EUR);
TEST_ASSERT_EQUAL_STRING(result, output[NUM_SCREENS - 6].c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE(" 1", output[NUM_SCREENS - 5].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("020", output[NUM_SCREENS - 4].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("825", output[NUM_SCREENS - 3].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("000", output[NUM_SCREENS - 2].c_str(), joined.c_str());
TEST_ASSERT_EQUAL_STRING_MESSAGE("000", output[NUM_SCREENS - 1].c_str(), joined.c_str());
}
void test_Mcap1TrillionJpy(void)
{
std::array<std::string, NUM_SCREENS> output = parseMarketCap(831000, 52000, CURRENCY_JPY, true);
TEST_ASSERT_EQUAL_STRING("JPY/MCAP", output[0].c_str());
TEST_ASSERT_TRUE(CURRENCY_JPY == output[NUM_SCREENS - 6].c_str()[0]);
TEST_ASSERT_EQUAL_STRING("1", output[NUM_SCREENS - 5].c_str());
TEST_ASSERT_EQUAL_STRING(".", output[NUM_SCREENS - 4].c_str());
TEST_ASSERT_EQUAL_STRING("0", output[NUM_SCREENS - 3].c_str());
TEST_ASSERT_EQUAL_STRING("2", output[NUM_SCREENS - 2].c_str());
TEST_ASSERT_EQUAL_STRING("T", output[NUM_SCREENS - 1].c_str());
}
void test_Mcap1TrillionJpySmallChars(void)
{
std::array<std::string, NUM_SCREENS> output = parseMarketCap(831000, 52000, CURRENCY_JPY, false);
TEST_ASSERT_EQUAL_STRING("JPY/MCAP", output[0].c_str());
char result[4];
snprintf(result, sizeof(result), " %c ", CURRENCY_JPY);
TEST_ASSERT_EQUAL_STRING(result, output[NUM_SCREENS - 6].c_str());
TEST_ASSERT_EQUAL_STRING(" 1", output[NUM_SCREENS - 5].c_str());
TEST_ASSERT_EQUAL_STRING("020", output[NUM_SCREENS - 4].c_str());
TEST_ASSERT_EQUAL_STRING("825", output[NUM_SCREENS - 3].c_str());
TEST_ASSERT_EQUAL_STRING("000", output[NUM_SCREENS - 2].c_str());
TEST_ASSERT_EQUAL_STRING("000", output[NUM_SCREENS - 1].c_str());
}
// not needed when using generate_test_runner.rb
int runUnityTests(void) {
int runUnityTests(void)
{
UNITY_BEGIN();
RUN_TEST(test_CorrectSatsPerDollarConversion);
RUN_TEST(test_CorrectSatsPerPoundConversion);
RUN_TEST(test_SatsPerDollarAfter1B);
RUN_TEST(test_SixCharacterBlockHeight);
RUN_TEST(test_SevenCharacterBlockHeight);
RUN_TEST(test_FeeRateDisplay);
RUN_TEST(test_PriceOf100kusd);
RUN_TEST(test_PriceOf1MillionUsd);
RUN_TEST(test_McapLowerUsd);
RUN_TEST(test_Mcap1TrillionUsd);
RUN_TEST(test_Mcap1TrillionUsdSmallChars);
RUN_TEST(test_Mcap1TrillionEur);
RUN_TEST(test_Mcap1TrillionEurSmallChars);
RUN_TEST(test_Mcap1TrillionJpy);
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();
}
int main(void) {
return runUnityTests();
int main(void)
{
return runUnityTests();
}
extern "C" void app_main() {
runUnityTests();
extern "C" void app_main()
{
runUnityTests();
}

Some files were not shown because too many files have changed in this diff Show more