forked from btclock/btclock_v3
Compare commits
34 commits
bitaxe_log
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
957a947bc5 | ||
|
c44626cb42 | ||
|
4fdd6b6b4f | ||
|
8a818c66a0 | ||
|
9889e983ec | ||
|
03dbb8add6 | ||
|
753838b122 | ||
|
46c0f3a22b | ||
|
fb70d435a9 | ||
|
7bcb24bab0 | ||
|
e8a7b221cb | ||
|
aeee5238b3 | ||
|
f613c7e9a1 | ||
c7ea2f3e4d | |||
|
814cd234a9 | ||
f9aa593f0b | |||
01ef6daf9f | |||
8f9307d1e4 | |||
e758659a4a | |||
be224d1f91 | |||
72e5ee6580 | |||
c3af0b4d36 | |||
fabc6c1d28 | |||
e175b5f2f5 | |||
2bc5984f6f | |||
1bd465b33a | |||
|
ae2e6656df | ||
|
c989169ff4 | ||
|
1a4bc9b711 | ||
|
0dcde59fb4 | ||
|
de8fe2e26e | ||
|
af4c466659 | ||
|
83d293c58e | ||
|
da25c7de90 |
53 changed files with 2167 additions and 1874 deletions
|
@ -1,4 +1,4 @@
|
|||
name: 'BTClock CI'
|
||||
name: "BTClock CI"
|
||||
|
||||
on:
|
||||
push:
|
||||
|
@ -22,7 +22,7 @@ jobs:
|
|||
with:
|
||||
node-version: lts/*
|
||||
cache: yarn
|
||||
cache-dependency-path: '**/yarn.lock'
|
||||
cache-dependency-path: "**/yarn.lock"
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
|
@ -34,8 +34,8 @@ jobs:
|
|||
key: ${{ runner.os }}-pio
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.9'
|
||||
cache: 'pip'
|
||||
python-version: "3.9"
|
||||
cache: "pip"
|
||||
- name: Get current date
|
||||
id: dateAndTime
|
||||
shell: bash
|
||||
|
@ -81,9 +81,9 @@ jobs:
|
|||
version: esp32s3
|
||||
epd_variant: [213epd, 29epd]
|
||||
exclude:
|
||||
- chip: {name: btclock_rev_b, version: esp32s3}
|
||||
- chip: { name: btclock_rev_b, version: esp32s3 }
|
||||
epd_variant: 29epd
|
||||
- chip: {name: btclock_v8, version: esp32s3}
|
||||
- chip: { name: btclock_v8, version: esp32s3 }
|
||||
epd_variant: 29epd
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/forgejo/download-artifact@v4
|
||||
|
@ -93,9 +93,10 @@ jobs:
|
|||
- 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
|
||||
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 \
|
||||
|
@ -105,10 +106,19 @@ jobs:
|
|||
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;
|
||||
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
|
||||
# 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 \
|
||||
|
@ -116,19 +126,26 @@ jobs:
|
|||
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 \
|
||||
0x370000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/littlefs.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
|
||||
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
|
||||
|
||||
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 }}
|
||||
|
||||
|
@ -161,11 +178,11 @@ jobs:
|
|||
- name: Create release
|
||||
uses: https://code.forgejo.org/actions/forgejo-release@v2.4.0
|
||||
with:
|
||||
url: 'https://git.btclock.dev'
|
||||
repo: '${{ github.repository }}'
|
||||
url: "https://git.btclock.dev"
|
||||
repo: "${{ github.repository }}"
|
||||
direction: upload
|
||||
tag: '${{ github.ref_name }}'
|
||||
sha: '${{ github.sha }}'
|
||||
tag: "${{ github.ref_name }}"
|
||||
sha: "${{ github.sha }}"
|
||||
release-dir: release
|
||||
token: ${{ secrets.TOKEN }}
|
||||
override: ${{ github.ref_type != 'tag' && github.ref_name != 'main' }}
|
||||
|
|
37
README.md
37
README.md
|
@ -16,15 +16,44 @@ Biggest differences with v2 are:
|
|||
|
||||
New features:
|
||||
- BitAxe integration
|
||||
- Zap notifier
|
||||
-
|
||||
- Nostr Zap notifier
|
||||
- Multiple mining pool stats integrations
|
||||
|
||||
"Steal focus on new block" means that when a new block is mined, the display will switch to the block height screen if it's not on it already.
|
||||
|
||||
Most [information](https://github.com/btclock/btclock_v2/wiki) about BTClock v2 is still valid for this version.
|
||||
See the [docs](https://git.btclock.dev/btclock/docs) repo for more information and building instructions.
|
||||
|
||||
**NOTE**: The software assumes that the hardware is run in a controlled private network. ~~The Web UI and the OTA update mechanism are not password protected and accessible to anyone in the network. Also, since the device only fetches numbers through WebSockets it will skip server certificate verification to save resources.~~ Since 3.2.0 the WebUI is password protectable and all certificates are verified. OTA update mechanism is not password-protected.
|
||||
|
||||
## 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.
|
||||
|
|
2
data
2
data
|
@ -1 +1 @@
|
|||
Subproject commit 266a99be96189bea92e0ef593f930bb92d3b5b20
|
||||
Subproject commit 924be8fc2eb02fe384a20b53da1a9fa3d8db8a05
|
|
@ -164,3 +164,82 @@ int64_t getAmountInSatoshis(std::string bolt11) {
|
|||
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
#include <cstdint>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <unordered_map>
|
||||
|
||||
|
||||
int modulo(int x,int N);
|
||||
|
||||
|
@ -13,3 +15,5 @@ double getSupplyAtBlock(std::uint32_t blockNr);
|
|||
std::string formatNumberWithSuffix(std::uint64_t num, int numCharacters = 4);
|
||||
std::string formatNumberWithSuffix(std::uint64_t num, int numCharacters, 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);
|
|
@ -3,5 +3,5 @@ nvs, data, nvs, 0x9000, 0x5000,
|
|||
otadata, data, ota, 0xe000, 0x2000,
|
||||
app0, app, ota_0, 0x10000, 0x1b8000,
|
||||
app1, app, ota_1, , 0x1b8000,
|
||||
spiffs, data, spiffs, , 0x66800,
|
||||
spiffs, data, spiffs, , 0x66C00,
|
||||
coredump, data, coredump,, 0x10000,
|
|
|
@ -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, , 410K,
|
||||
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,7 +1,7 @@
|
|||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 36K, 20K,
|
||||
otadata, data, ota, 56K, 8K,
|
||||
app0, app, ota_0, 64K, 1760K,
|
||||
app1, app, ota_1, , 1760K,
|
||||
spiffs, data, spiffs, , 410K,
|
||||
coredump, data, coredump,, 64K,
|
||||
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,
|
|
211
platformio.ini
211
platformio.ini
|
@ -7,119 +7,137 @@
|
|||
;
|
||||
; 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 = espressif32 @ ^6.9.0
|
||||
framework = arduino, espidf
|
||||
framework = arduino, espidf
|
||||
monitor_speed = 115200
|
||||
monitor_filters = esp32_exception_decoder, colorize
|
||||
board_build.filesystem = littlefs
|
||||
extra_scripts = post:scripts/extra_script.py
|
||||
board_build.embed_files = x509_crt_bundle
|
||||
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
|
||||
!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 =
|
||||
https://github.com/joltwallet/esp_littlefs.git
|
||||
bblanchon/ArduinoJson@^7.2.1
|
||||
mathieucarbou/ESPAsyncWebServer @ 3.3.23
|
||||
adafruit/Adafruit BusIO@^1.16.2
|
||||
adafruit/Adafruit MCP23017 Arduino Library@^2.3.2
|
||||
adafruit/Adafruit NeoPixel@^1.12.3
|
||||
https://github.com/dsbaars/universal_pin
|
||||
https://github.com/dsbaars/GxEPD2#universal_pin
|
||||
https://github.com/tzapu/WiFiManager.git#v2.0.17
|
||||
rblb/Nostrduino@1.2.8
|
||||
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
|
||||
rblb/Nostrduino@1.2.8
|
||||
|
||||
[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
|
||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||
-D IS_HW_REV_A
|
||||
${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:btclock_rev_b]
|
||||
extends = btclock_base
|
||||
board = btclock_rev_b
|
||||
board_build.partitions = partition.csv
|
||||
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
|
||||
${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
|
||||
${btclock_base.lib_deps}
|
||||
robtillaart/PCA9685@^0.7.1
|
||||
claws/BH1750@^1.3.0
|
||||
build_unflags =
|
||||
${btclock_base.build_unflags}
|
||||
${btclock_base.build_unflags}
|
||||
platform_packages =
|
||||
platformio/tool-mklittlefs@^1.203.210628
|
||||
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
|
||||
|
||||
[env:lolin_s3_mini_213epd]
|
||||
extends = env:lolin_s3_mini
|
||||
test_framework = unity
|
||||
build_flags =
|
||||
${env:lolin_s3_mini.build_flags}
|
||||
-D USE_QR
|
||||
-D VERSION_EPD_2_13
|
||||
-D HW_REV=\"REV_A_EPD_2_13\"
|
||||
|
||||
${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_rev_b_213epd]
|
||||
extends = env:btclock_rev_b
|
||||
test_framework = unity
|
||||
build_flags =
|
||||
${env:btclock_rev_b.build_flags}
|
||||
-D USE_QR
|
||||
-D VERSION_EPD_2_13
|
||||
-D HW_REV=\"REV_B_EPD_2_13\"
|
||||
${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\"
|
||||
${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\"
|
||||
${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
|
||||
|
@ -128,40 +146,51 @@ 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
|
||||
${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\"
|
||||
${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
|
||||
${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
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
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()
|
||||
|
@ -26,7 +29,7 @@ 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)
|
||||
|
@ -38,10 +41,85 @@ def process_directory(input_dir, output_dir):
|
|||
|
||||
# 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
|
7
scripts/pre_script.py
Normal file
7
scripts/pre_script.py
Normal 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)
|
||||
|
1419
src/icons/icons.cpp
1419
src/icons/icons.cpp
File diff suppressed because it is too large
Load diff
|
@ -54,7 +54,7 @@ void taskBitaxeFetch(void *pvParameters)
|
|||
|
||||
void setupBitaxeFetchTask()
|
||||
{
|
||||
xTaskCreate(taskBitaxeFetch, "bitaxeFetch", (6 * 1024), NULL, tskIDLE_PRIORITY,
|
||||
xTaskCreate(taskBitaxeFetch, "bitaxeFetch", (3 * 1024), NULL, tskIDLE_PRIORITY,
|
||||
&bitaxeFetchTaskHandle);
|
||||
|
||||
xTaskNotifyGive(bitaxeFetchTaskHandle);
|
||||
|
|
|
@ -296,45 +296,27 @@ void restartBlockNotify()
|
|||
}
|
||||
|
||||
|
||||
int getBlockFetch()
|
||||
{
|
||||
try {
|
||||
WiFiClientSecure client;
|
||||
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";
|
||||
|
||||
if (preferences.getBool("mempoolSecure", DEFAULT_MEMPOOL_SECURE)) {
|
||||
client.setCACertBundle(rootca_crt_bundle_start);
|
||||
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"));
|
||||
}
|
||||
|
||||
String mempoolInstance =
|
||||
preferences.getString("mempoolInstance", DEFAULT_MEMPOOL_INSTANCE);
|
||||
|
||||
// Get current block height through regular API
|
||||
HTTPClient http;
|
||||
|
||||
const String protocol = preferences.getBool("mempoolSecure", DEFAULT_MEMPOOL_SECURE) ? "https" : "http";
|
||||
|
||||
if (preferences.getBool("mempoolSecure", DEFAULT_MEMPOOL_SECURE))
|
||||
http.begin(client, protocol + "://" + mempoolInstance + "/api/blocks/tip/height");
|
||||
else
|
||||
http.begin(protocol + "://" + mempoolInstance + "/api/blocks/tip/height");
|
||||
|
||||
Serial.println("Fetching block height from " + protocol + "://" + mempoolInstance + "/api/blocks/tip/height");
|
||||
int httpCode = http.GET();
|
||||
|
||||
if (httpCode > 0 && httpCode == HTTP_CODE_OK)
|
||||
{
|
||||
String blockHeightStr = http.getString();
|
||||
return blockHeightStr.toInt();
|
||||
} else {
|
||||
Serial.println("HTTP code" + String(httpCode));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
catch (...) {
|
||||
Serial.println(F("An exception occured while trying to get the latest block"));
|
||||
}
|
||||
|
||||
return 2203; // B-T-C
|
||||
return 2203; // B-T-C
|
||||
}
|
||||
|
||||
uint getLastBlockUpdate()
|
||||
|
|
|
@ -5,54 +5,66 @@ const TickType_t debounceDelay = pdMS_TO_TICKS(50);
|
|||
TickType_t lastDebounceTime = 0;
|
||||
|
||||
#ifdef IS_BTCLOCK_V8
|
||||
#define BTN_1 0
|
||||
#define BTN_2 1
|
||||
#define BTN_3 2
|
||||
#define BTN_4 3
|
||||
#define BTN_1 256
|
||||
#define BTN_2 512
|
||||
#define BTN_3 1024
|
||||
#define BTN_4 2048
|
||||
#else
|
||||
#define BTN_1 3
|
||||
#define BTN_2 2
|
||||
#define BTN_3 1
|
||||
#define BTN_4 0
|
||||
#define BTN_1 2048
|
||||
#define BTN_2 1024
|
||||
#define BTN_3 512
|
||||
#define BTN_4 256
|
||||
#endif
|
||||
|
||||
void buttonTask(void *parameter) {
|
||||
while (1) {
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
std::lock_guard<std::mutex> lock(mcpMutex);
|
||||
|
||||
TickType_t currentTime = xTaskGetTickCount();
|
||||
|
||||
if ((currentTime - lastDebounceTime) >= debounceDelay) {
|
||||
lastDebounceTime = currentTime;
|
||||
|
||||
if (!digitalRead(MCP_INT_PIN)) {
|
||||
uint pin = mcp1.getLastInterruptPin();
|
||||
std::lock_guard<std::mutex> lock(mcpMutex);
|
||||
|
||||
switch (pin) {
|
||||
case BTN_1:
|
||||
toggleTimerActive();
|
||||
break;
|
||||
case BTN_2:
|
||||
nextScreen();
|
||||
break;
|
||||
case BTN_3:
|
||||
previousScreen();
|
||||
break;
|
||||
case BTN_4:
|
||||
showSystemStatusScreen();
|
||||
break;
|
||||
}
|
||||
if (!digitalRead(MCP_INT_PIN)) {
|
||||
uint16_t intFlags = mcp1.getInterruptFlagRegister();
|
||||
uint16_t intCap = mcp1.getInterruptCaptureRegister();
|
||||
|
||||
// Check each button individually
|
||||
if (intFlags & BTN_1) handleButton1();
|
||||
if (intFlags & BTN_2) handleButton2();
|
||||
if (intFlags & BTN_3) handleButton3();
|
||||
if (intFlags & BTN_4) handleButton4();
|
||||
}
|
||||
mcp1.clearInterrupts();
|
||||
} else {
|
||||
}
|
||||
// Very ugly, but for some reason this is necessary
|
||||
|
||||
// Clear interrupt state
|
||||
while (!digitalRead(MCP_INT_PIN)) {
|
||||
mcp1.clearInterrupts();
|
||||
std::lock_guard<std::mutex> lock(mcpMutex);
|
||||
mcp1.getInterruptCaptureRegister();
|
||||
delay(1); // Small delay to prevent tight loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions to handle each button
|
||||
void handleButton1() {
|
||||
toggleTimerActive();
|
||||
}
|
||||
|
||||
void handleButton2() {
|
||||
nextScreen();
|
||||
}
|
||||
|
||||
void handleButton3() {
|
||||
previousScreen();
|
||||
}
|
||||
|
||||
void handleButton4() {
|
||||
showSystemStatusScreen();
|
||||
}
|
||||
|
||||
void IRAM_ATTR handleButtonInterrupt() {
|
||||
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
||||
xTaskNotifyFromISR(buttonTaskHandle, 0, eNoAction, &xHigherPriorityTaskWoken);
|
||||
|
|
|
@ -8,6 +8,13 @@
|
|||
|
||||
extern TaskHandle_t buttonTaskHandle;
|
||||
|
||||
// Task and setup functions
|
||||
void buttonTask(void *pvParameters);
|
||||
void IRAM_ATTR handleButtonInterrupt();
|
||||
void setupButtonTask();
|
||||
|
||||
// Individual button handlers
|
||||
void handleButton1();
|
||||
void handleButton2();
|
||||
void handleButton3();
|
||||
void handleButton4();
|
||||
|
|
|
@ -2,10 +2,12 @@
|
|||
|
||||
#define MAX_ATTEMPTS_WIFI_CONNECTION 20
|
||||
|
||||
// zlib_turbo zt;
|
||||
|
||||
Preferences preferences;
|
||||
Adafruit_MCP23X17 mcp1;
|
||||
MCP23017 mcp1(0x20);
|
||||
#ifdef IS_BTCLOCK_V8
|
||||
Adafruit_MCP23X17 mcp2;
|
||||
MCP23017 mcp2(0x21);
|
||||
#endif
|
||||
|
||||
#ifdef HAS_FRONTLIGHT
|
||||
|
@ -35,7 +37,7 @@ void setup()
|
|||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lockMcp(mcpMutex);
|
||||
if (mcp1.digitalRead(3) == LOW)
|
||||
if (mcp1.read1(3) == LOW)
|
||||
{
|
||||
preferences.putBool("wifiConfigured", false);
|
||||
preferences.remove("txPower");
|
||||
|
@ -46,7 +48,7 @@ void setup()
|
|||
}
|
||||
|
||||
{
|
||||
if (mcp1.digitalRead(0) == LOW)
|
||||
if (mcp1.read1(0) == LOW)
|
||||
{
|
||||
// Then loop forever to prevent anything else from writing to the screen
|
||||
while (true)
|
||||
|
@ -54,7 +56,7 @@ void setup()
|
|||
delay(1000);
|
||||
}
|
||||
}
|
||||
else if (mcp1.digitalRead(1) == LOW)
|
||||
else if (mcp1.read1(1) == LOW)
|
||||
{
|
||||
preferences.clear();
|
||||
queueLedEffect(LED_EFFECT_WIFI_ERASE_SETTINGS);
|
||||
|
@ -66,6 +68,7 @@ void setup()
|
|||
}
|
||||
|
||||
setupWifi();
|
||||
// loadIcons();
|
||||
|
||||
setupWebserver();
|
||||
|
||||
|
@ -92,6 +95,11 @@ void setup()
|
|||
setupBitaxeFetchTask();
|
||||
}
|
||||
|
||||
if (preferences.getBool("miningPoolStats", DEFAULT_MINING_POOL_STATS_ENABLED))
|
||||
{
|
||||
setupMiningPoolStatsFetchTask();
|
||||
}
|
||||
|
||||
setupButtonTask();
|
||||
setupOTA();
|
||||
|
||||
|
@ -106,6 +114,7 @@ void setup()
|
|||
#endif
|
||||
|
||||
forceFullRefresh();
|
||||
|
||||
}
|
||||
|
||||
void setupWifi()
|
||||
|
@ -132,7 +141,7 @@ void setupWifi()
|
|||
bool buttonPress = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lockMcp(mcpMutex);
|
||||
buttonPress = (mcp1.digitalRead(2) == LOW);
|
||||
buttonPress = (mcp1.read1(2) == LOW);
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -327,6 +336,14 @@ void setupPreferences()
|
|||
addScreenMapping(SCREEN_BITAXE_HASHRATE, "BitAxe Hashrate");
|
||||
addScreenMapping(SCREEN_BITAXE_BESTDIFF, "BitAxe Best Difficulty");
|
||||
}
|
||||
|
||||
if (preferences.getBool("miningPoolStats", DEFAULT_MINING_POOL_STATS_ENABLED))
|
||||
{
|
||||
addScreenMapping(SCREEN_MINING_POOL_STATS_HASHRATE, "Mining Pool Hashrate");
|
||||
if (getMiningPool()->supportsDailyEarnings()) {
|
||||
addScreenMapping(SCREEN_MINING_POOL_STATS_EARNINGS, "Mining Pool Earnings");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String replaceAmbiguousChars(String input)
|
||||
|
@ -342,68 +359,6 @@ String replaceAmbiguousChars(String input)
|
|||
return input;
|
||||
}
|
||||
|
||||
// void addCurrencyMappings(const std::vector<std::string>& currencies)
|
||||
// {
|
||||
// for (const auto& currency : currencies)
|
||||
// {
|
||||
// int satsPerCurrencyScreen;
|
||||
// int btcTickerScreen;
|
||||
// int marketCapScreen;
|
||||
|
||||
// // Determine the corresponding screen IDs based on the currency code
|
||||
// if (currency == "USD")
|
||||
// {
|
||||
// satsPerCurrencyScreen = SCREEN_SATS_PER_CURRENCY_USD;
|
||||
// btcTickerScreen = SCREEN_BTC_TICKER_USD;
|
||||
// marketCapScreen = SCREEN_MARKET_CAP_USD;
|
||||
// }
|
||||
// else if (currency == "EUR")
|
||||
// {
|
||||
// satsPerCurrencyScreen = SCREEN_SATS_PER_CURRENCY_EUR;
|
||||
// btcTickerScreen = SCREEN_BTC_TICKER_EUR;
|
||||
// marketCapScreen = SCREEN_MARKET_CAP_EUR;
|
||||
// }
|
||||
// else if (currency == "GBP")
|
||||
// {
|
||||
// satsPerCurrencyScreen = SCREEN_SATS_PER_CURRENCY_GBP;
|
||||
// btcTickerScreen = SCREEN_BTC_TICKER_GBP;
|
||||
// marketCapScreen = SCREEN_MARKET_CAP_GBP;
|
||||
// }
|
||||
// else if (currency == "JPY")
|
||||
// {
|
||||
// satsPerCurrencyScreen = SCREEN_SATS_PER_CURRENCY_JPY;
|
||||
// btcTickerScreen = SCREEN_BTC_TICKER_JPY;
|
||||
// marketCapScreen = SCREEN_MARKET_CAP_JPY;
|
||||
// }
|
||||
// else if (currency == "AUD")
|
||||
// {
|
||||
// satsPerCurrencyScreen = SCREEN_SATS_PER_CURRENCY_AUD;
|
||||
// btcTickerScreen = SCREEN_BTC_TICKER_AUD;
|
||||
// marketCapScreen = SCREEN_MARKET_CAP_AUD;
|
||||
// }
|
||||
// else if (currency == "CAD")
|
||||
// {
|
||||
// satsPerCurrencyScreen = SCREEN_SATS_PER_CURRENCY_CAD;
|
||||
// btcTickerScreen = SCREEN_BTC_TICKER_CAD;
|
||||
// marketCapScreen = SCREEN_MARKET_CAP_CAD;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// continue; // Unknown currency, skip it
|
||||
// }
|
||||
|
||||
// // Create the string locally to ensure it persists
|
||||
// std::string satsPerCurrencyString = "Sats per " + currency;
|
||||
// std::string btcTickerString = "Ticker " + currency;
|
||||
// std::string marketCapString = "Market Cap " + currency;
|
||||
|
||||
// // Pass the c_str() to the function
|
||||
// addScreenMapping(satsPerCurrencyScreen, satsPerCurrencyString.c_str());
|
||||
// addScreenMapping(btcTickerScreen, btcTickerString.c_str());
|
||||
// addScreenMapping(marketCapScreen, marketCapString.c_str());
|
||||
// }
|
||||
// }
|
||||
|
||||
void setupWebsocketClients(void *pvParameters)
|
||||
{
|
||||
if (preferences.getBool("ownDataSource", DEFAULT_OWN_DATA_SOURCE))
|
||||
|
@ -484,15 +439,19 @@ void setupHardware()
|
|||
Serial.println(F("Error loading WebUI"));
|
||||
}
|
||||
|
||||
// if (!LittleFS.exists("/qr.txt"))
|
||||
// {
|
||||
// File f = LittleFS.open("/qr.txt", "w");
|
||||
|
||||
// if(f) {
|
||||
|
||||
// if (f.print("Hello")) {
|
||||
// Serial.println(F("Written QR to FS"));
|
||||
// Serial.printf("\nLittleFS free: %zu\n", LittleFS.totalBytes() - LittleFS.usedBytes());
|
||||
// }
|
||||
// } else {
|
||||
// Serial.println(F("Can't write QR to FS"));
|
||||
// }
|
||||
|
||||
// f.close();
|
||||
// }
|
||||
|
||||
setupLeds();
|
||||
|
@ -507,29 +466,34 @@ void setupHardware()
|
|||
|
||||
Wire.begin(I2C_SDA_PIN, I2C_SCK_PIN, 400000);
|
||||
|
||||
if (!mcp1.begin_I2C(0x20))
|
||||
{
|
||||
if (!mcp1.begin()) {
|
||||
Serial.println(F("Error MCP23017 1"));
|
||||
|
||||
// while (1)
|
||||
// ;
|
||||
}
|
||||
else
|
||||
{
|
||||
} else {
|
||||
pinMode(MCP_INT_PIN, INPUT_PULLUP);
|
||||
mcp1.setupInterrupts(false, false, LOW);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
mcp1.pinMode(i, INPUT_PULLUP);
|
||||
mcp1.setupInterruptPin(i, LOW);
|
||||
// Enable mirrored interrupts (both INTA and INTB pins signal any interrupt)
|
||||
if (!mcp1.mirrorInterrupts(true)) {
|
||||
Serial.println(F("Error setting up mirrored interrupts"));
|
||||
}
|
||||
#ifndef IS_BTCLOCK_V8
|
||||
for (int i = 8; i <= 14; i++)
|
||||
{
|
||||
mcp1.pinMode(i, OUTPUT);
|
||||
|
||||
// Configure all 4 button pins as inputs with pullups and interrupts
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (!mcp1.pinMode1(i, INPUT_PULLUP)) {
|
||||
Serial.printf("Error setting pin %d to input pull up\n", i);
|
||||
}
|
||||
// Enable interrupt on CHANGE for each pin
|
||||
if (!mcp1.enableInterrupt(i, CHANGE)) {
|
||||
Serial.printf("Error enabling interrupt for pin %d\n", i);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Set interrupt pins as open drain with active-low polarity
|
||||
if (!mcp1.setInterruptPolarity(2)) { // 2 = Open drain
|
||||
Serial.println(F("Error setting interrupt polarity"));
|
||||
}
|
||||
|
||||
// Clear any pending interrupts
|
||||
mcp1.getInterruptCaptureRegister();
|
||||
}
|
||||
|
||||
#ifdef IS_HW_REV_B
|
||||
|
@ -538,7 +502,7 @@ void setupHardware()
|
|||
#endif
|
||||
|
||||
#ifdef IS_BTCLOCK_V8
|
||||
if (!mcp2.begin_I2C(0x21))
|
||||
if (!mcp2.begin())
|
||||
{
|
||||
Serial.println(F("Error MCP23017 2"));
|
||||
|
||||
|
@ -791,3 +755,15 @@ const char* getFirmwareFilename() {
|
|||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
const char* getWebUiFilename() {
|
||||
if (HW_REV == "REV_B_EPD_2_13") {
|
||||
return "littlefs_8MB.bin";
|
||||
} else if (HW_REV == "REV_A_EPD_2_13") {
|
||||
return "littlefs_4MB.bin";
|
||||
} else if (HW_REV == "REV_A_EPD_2_9") {
|
||||
return "littlefs_4MB.bin";
|
||||
} else {
|
||||
return "littlefs_4MB.bin";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <Adafruit_MCP23X17.h>
|
||||
#include <MCP23017.h>
|
||||
#include <Arduino.h>
|
||||
#include <Preferences.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
|
@ -18,6 +18,7 @@
|
|||
#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"
|
||||
|
||||
|
@ -32,7 +33,6 @@
|
|||
|
||||
#define NTP_SERVER "pool.ntp.org"
|
||||
#define DEFAULT_TIME_OFFSET_SECONDS 3600
|
||||
#define USER_AGENT "BTClock/3.0"
|
||||
#ifndef MCP_DEV_ADDR
|
||||
#define MCP_DEV_ADDR 0x20
|
||||
#endif
|
||||
|
@ -84,3 +84,5 @@ void addScreenMapping(int value, const char* name);
|
|||
int findScreenIndexByValue(int value);
|
||||
String replaceAmbiguousChars(String input);
|
||||
const char* getFirmwareFilename();
|
||||
const char* getWebUiFilename();
|
||||
// void loadIcons();
|
|
@ -58,6 +58,10 @@
|
|||
#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
|
||||
|
@ -71,3 +75,5 @@
|
|||
|
||||
#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"
|
||||
|
|
198
src/lib/epd.cpp
198
src/lib/epd.cpp
|
@ -138,6 +138,9 @@ uint8_t qrcode[800];
|
|||
#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++)
|
||||
|
@ -146,25 +149,6 @@ void forceFullRefresh()
|
|||
}
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
std::lock_guard<std::mutex> lockMcp(mcpMutex);
|
||||
|
@ -176,7 +160,7 @@ void setupDisplays()
|
|||
|
||||
updateQueue = xQueueCreate(UPDATE_QUEUE_SIZE, sizeof(UpdateDisplayTaskItem));
|
||||
|
||||
xTaskCreate(prepareDisplayUpdateTask, "PrepareUpd", EPD_TASK_STACK_SIZE, NULL, 11, NULL);
|
||||
xTaskCreate(prepareDisplayUpdateTask, "PrepareUpd", EPD_TASK_STACK_SIZE*2, NULL, 11, NULL);
|
||||
|
||||
for (uint i = 0; i < NUM_SCREENS; i++)
|
||||
{
|
||||
|
@ -191,7 +175,7 @@ void setupDisplays()
|
|||
}
|
||||
|
||||
// Hold lower button to enable "storage mode" (prevents burn-in of ePaper displays)
|
||||
if (mcp1.digitalRead(0) == LOW)
|
||||
if (mcp1.read1(0) == LOW)
|
||||
{
|
||||
setFgColor(GxEPD_BLACK);
|
||||
setBgColor(GxEPD_WHITE);
|
||||
|
@ -275,7 +259,10 @@ void prepareDisplayUpdateTask(void *pvParameters)
|
|||
}
|
||||
else if (epdContent[epdIndex].startsWith(F("mdi")))
|
||||
{
|
||||
renderIcon(epdIndex, epdContent[epdIndex], updatePartial);
|
||||
bool updated = renderIcon(epdIndex, epdContent[epdIndex], updatePartial);
|
||||
if (!updated) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (epdContent[epdIndex].length() > 5)
|
||||
{
|
||||
|
@ -414,89 +401,36 @@ void splitText(const uint dispNum, const String &top, const String &bottom,
|
|||
displays[dispNum].print(bottom);
|
||||
}
|
||||
|
||||
// void showChars(const uint dispNum, const String &chars, bool partial,
|
||||
// const GFXfont *font)
|
||||
// {
|
||||
// displays[dispNum].setRotation(2);
|
||||
// displays[dispNum].setFont(font);
|
||||
// displays[dispNum].setTextColor(getFgColor());
|
||||
// int16_t tbx, tby;
|
||||
// uint16_t tbw, tbh;
|
||||
// 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());
|
||||
}
|
||||
|
||||
// displays[dispNum].getTextBounds(chars, 0, 0, &tbx, &tby, &tbw, &tbh);
|
||||
void showDigit(const uint dispNum, char chr, bool partial, const GFXfont *font) {
|
||||
String str(chr);
|
||||
if (chr == '.') {
|
||||
str = "!";
|
||||
}
|
||||
|
||||
// // 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;
|
||||
setupDisplay(dispNum, font);
|
||||
|
||||
// displays[dispNum].fillScreen(getBgColor());
|
||||
int16_t tbx, tby;
|
||||
uint16_t tbw, tbh;
|
||||
displays[dispNum].getTextBounds(str, 0, 0, &tbx, &tby, &tbw, &tbh);
|
||||
|
||||
// displays[dispNum].setCursor(x, y);
|
||||
// displays[dispNum].print(chars);
|
||||
uint16_t x = ((displays[dispNum].width() - tbw) / 2) - tbx;
|
||||
uint16_t y = ((displays[dispNum].height() - tbh) / 2) - tby;
|
||||
|
||||
// // displays[dispNum].setCursor(10, 3);
|
||||
// // displays[dispNum].setFont(&FONT_SMALL);
|
||||
// // displays[dispNum].setTextColor(getFgColor());
|
||||
// // displays[dispNum].println("Y = " + y);
|
||||
// }
|
||||
displays[dispNum].setCursor(x, y);
|
||||
displays[dispNum].print(str);
|
||||
|
||||
void showDigit(const uint dispNum, char chr, bool partial,
|
||||
const GFXfont *font)
|
||||
{
|
||||
String str(chr);
|
||||
|
||||
if (chr == '.')
|
||||
{
|
||||
str = "!";
|
||||
}
|
||||
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;
|
||||
|
||||
// if (str.equals("."))
|
||||
// {
|
||||
// // int16_t yAdvance = font->yAdvance;
|
||||
// // uint8_t charIndex = 46 - font->first;
|
||||
// // GFXglyph *glyph = (&font->glyph)[charIndex];
|
||||
// int16_t tbx2, tby2;
|
||||
// uint16_t tbw2, tbh2;
|
||||
// displays[dispNum].getTextBounds(".!", 0, 0, &tbx2, &tby2, &tbw2, &tbh2);
|
||||
|
||||
// y = ((displays[dispNum].height() - tbh2) / 2) - tby2;
|
||||
// // Serial.print("yAdvance");
|
||||
// // Serial.println(yAdvance);
|
||||
// // if (glyph != nullptr) {
|
||||
// // Serial.print("height");
|
||||
// // Serial.println(glyph->height);
|
||||
// // Serial.print("yOffset");
|
||||
// // Serial.println(glyph->yOffset);
|
||||
// // }
|
||||
|
||||
// // y = 250-99+18+19;
|
||||
// }
|
||||
|
||||
displays[dispNum].fillScreen(getBgColor());
|
||||
|
||||
displays[dispNum].setCursor(x, y);
|
||||
displays[dispNum].print(str);
|
||||
|
||||
if (chr == '.')
|
||||
{
|
||||
displays[dispNum].fillRect(x, y, displays[dispNum].width(), round(displays[dispNum].height() * 0.9), getBgColor());
|
||||
}
|
||||
|
||||
// displays[dispNum].setCursor(10, 3);
|
||||
// displays[dispNum].setFont(&FONT_SMALL);
|
||||
// displays[dispNum].setTextColor(getFgColor());
|
||||
// displays[dispNum].println("Y = " + y);
|
||||
if (chr == '.') {
|
||||
displays[dispNum].fillRect(x, y, displays[dispNum].width(),
|
||||
round(displays[dispNum].height() * 0.9), getBgColor());
|
||||
}
|
||||
}
|
||||
|
||||
int16_t calculateDescent(const GFXfont *font) {
|
||||
|
@ -514,21 +448,16 @@ int16_t calculateDescent(const GFXfont *font) {
|
|||
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());
|
||||
setupDisplay(dispNum, font);
|
||||
|
||||
|
||||
int16_t tbx, tby;
|
||||
uint16_t tbw, tbh;
|
||||
displays[dispNum].getTextBounds(chars, 0, 0, &tbx, &tby, &tbw, &tbh);
|
||||
|
||||
int16_t descent = calculateDescent(font);
|
||||
|
||||
// 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];
|
||||
|
@ -594,27 +523,55 @@ void renderText(const uint dispNum, const String &text, bool partial)
|
|||
}
|
||||
}
|
||||
|
||||
void renderIcon(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());
|
||||
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 = 3;
|
||||
iconIndex = 2;
|
||||
}
|
||||
else if (text.endsWith("bitaxe")) {
|
||||
iconIndex = 4;
|
||||
width = 88;
|
||||
height = 220;
|
||||
iconIndex = 3;
|
||||
}
|
||||
else if (text.endsWith("miningpool")) {
|
||||
LogoData logo = getMiningPoolLogo();
|
||||
|
||||
if (logo.size == 0) {
|
||||
Serial.println("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;
|
||||
}
|
||||
|
||||
displays[dispNum].drawInvertedBitmap(0,0, epd_icons_allArray[iconIndex], 122, 250, getFgColor());
|
||||
|
||||
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());
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -661,15 +618,12 @@ void waitUntilNoneBusy()
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include <Fonts/FreeSansBold9pt7b.h>
|
||||
#include <GxEPD2_BW.h>
|
||||
|
||||
|
||||
#include <mcp23x17_pin.hpp>
|
||||
#include <mutex>
|
||||
#include <native_pin.hpp>
|
||||
|
@ -13,6 +14,7 @@
|
|||
#include "lib/config.hpp"
|
||||
#include "lib/shared.hpp"
|
||||
#include "icons/icons.h"
|
||||
#include "mining_pool_stats_fetch.hpp"
|
||||
|
||||
#ifdef USE_QR
|
||||
#include "qrcodegen.h"
|
||||
|
@ -24,7 +26,6 @@ typedef struct {
|
|||
} UpdateDisplayTaskItem;
|
||||
|
||||
void forceFullRefresh();
|
||||
void refreshFromMemory();
|
||||
void setupDisplays();
|
||||
|
||||
void splitText(const uint dispNum, const String &top, const String &bottom,
|
||||
|
@ -43,7 +44,7 @@ int getFgColor();
|
|||
void setBgColor(int color);
|
||||
void setFgColor(int color);
|
||||
|
||||
void renderIcon(const uint dispNum, const String &text, bool partial);
|
||||
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);
|
||||
|
||||
|
|
32
src/lib/mining_pool/braiins/brains_pool.cpp
Normal file
32
src/lib/mining_pool/braiins/brains_pool.cpp
Normal file
|
@ -0,0 +1,32 @@
|
|||
#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
|
||||
{
|
||||
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)};
|
||||
}
|
||||
|
33
src/lib/mining_pool/braiins/brains_pool.hpp
Normal file
33
src/lib/mining_pool/braiins/brains_pool.hpp
Normal 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;
|
||||
}
|
||||
};
|
6
src/lib/mining_pool/gobrrr_pool/gobrrr_pool.cpp
Normal file
6
src/lib/mining_pool/gobrrr_pool/gobrrr_pool.cpp
Normal 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;
|
||||
}
|
30
src/lib/mining_pool/gobrrr_pool/gobrrr_pool.hpp
Normal file
30
src/lib/mining_pool/gobrrr_pool/gobrrr_pool.hpp
Normal 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;
|
||||
}
|
||||
};
|
11
src/lib/mining_pool/logo_data.hpp
Normal file
11
src/lib/mining_pool/logo_data.hpp
Normal 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;
|
||||
};
|
18
src/lib/mining_pool/mining_pool_interface.cpp
Normal file
18
src/lib/mining_pool/mining_pool_interface.cpp
Normal 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);
|
||||
}
|
35
src/lib/mining_pool/mining_pool_interface.hpp
Normal file
35
src/lib/mining_pool/mining_pool_interface.hpp
Normal 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;
|
||||
};
|
95
src/lib/mining_pool/mining_pool_stats_handler.cpp
Normal file
95
src/lib/mining_pool/mining_pool_stats_handler.cpp
Normal 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;
|
||||
}
|
11
src/lib/mining_pool/mining_pool_stats_handler.hpp
Normal file
11
src/lib/mining_pool/mining_pool_stats_handler.hpp
Normal 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);
|
27
src/lib/mining_pool/noderunners/noderunners_pool.cpp
Normal file
27
src/lib/mining_pool/noderunners/noderunners_pool.cpp
Normal file
|
@ -0,0 +1,27 @@
|
|||
// 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 {
|
||||
std::string hashrateStr = doc["hashrate1m"].as<std::string>();
|
||||
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
|
||||
};
|
||||
}
|
33
src/lib/mining_pool/noderunners/noderunners_pool.hpp
Normal file
33
src/lib/mining_pool/noderunners/noderunners_pool.hpp
Normal 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;
|
||||
}
|
||||
};
|
18
src/lib/mining_pool/ocean/ocean_pool.cpp
Normal file
18
src/lib/mining_pool/ocean/ocean_pool.cpp
Normal file
|
@ -0,0 +1,18 @@
|
|||
#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 {
|
||||
return PoolStats{
|
||||
.hashrate = doc["result"]["hashrate_300s"].as<std::string>(),
|
||||
.dailyEarnings = static_cast<int64_t>(
|
||||
doc["result"]["estimated_earn_next_block"].as<float>() * 100000000
|
||||
)
|
||||
};
|
||||
}
|
31
src/lib/mining_pool/ocean/ocean_pool.hpp
Normal file
31
src/lib/mining_pool/ocean/ocean_pool.hpp
Normal 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;
|
||||
}
|
||||
};
|
134
src/lib/mining_pool/pool_factory.cpp
Normal file
134
src/lib/mining_pool/pool_factory.cpp
Normal file
|
@ -0,0 +1,134 @@
|
|||
#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::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>(); }}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
56
src/lib/mining_pool/pool_factory.hpp
Normal file
56
src/lib/mining_pool/pool_factory.hpp
Normal file
|
@ -0,0 +1,56 @@
|
|||
#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 <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
|
||||
};
|
||||
}
|
||||
|
||||
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* LOGOS_DIR;
|
||||
};
|
10
src/lib/mining_pool/pool_stats.hpp
Normal file
10
src/lib/mining_pool/pool_stats.hpp
Normal file
|
@ -0,0 +1,10 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <optional>
|
||||
|
||||
struct PoolStats {
|
||||
std::string hashrate;
|
||||
std::optional<int64_t> dailyEarnings;
|
||||
};
|
21
src/lib/mining_pool/public_pool/public_pool.cpp
Normal file
21
src/lib/mining_pool/public_pool/public_pool.cpp
Normal file
|
@ -0,0 +1,21 @@
|
|||
// 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;
|
||||
|
||||
for (JsonVariantConst worker : doc["workers"].as<JsonArrayConst>()) {
|
||||
totalHashrate += static_cast<uint64_t>(std::llround(worker["hashRate"].as<double>()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
return PoolStats{
|
||||
.hashrate = std::to_string(totalHashrate),
|
||||
.dailyEarnings = std::nullopt // Public Pool doesn't support daily earnings
|
||||
};
|
||||
}
|
15
src/lib/mining_pool/public_pool/public_pool.hpp
Normal file
15
src/lib/mining_pool/public_pool/public_pool.hpp
Normal 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;
|
||||
};
|
6
src/lib/mining_pool/satoshi_radio/satoshi_radio_pool.cpp
Normal file
6
src/lib/mining_pool/satoshi_radio/satoshi_radio_pool.cpp
Normal file
|
@ -0,0 +1,6 @@
|
|||
// src/noderunners/noderunners_pool.cpp
|
||||
#include "satoshi_radio_pool.hpp"
|
||||
|
||||
std::string SatoshiRadioPool::getApiUrl() const {
|
||||
return "https://pool.satoshiradio.nl/api/v1/users/" + poolUser;
|
||||
}
|
14
src/lib/mining_pool/satoshi_radio/satoshi_radio_pool.hpp
Normal file
14
src/lib/mining_pool/satoshi_radio/satoshi_radio_pool.hpp
Normal 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
|
||||
};
|
123
src/lib/mining_pool_stats_fetch.cpp
Normal file
123
src/lib/mining_pool_stats_fetch.cpp
Normal file
|
@ -0,0 +1,123 @@
|
|||
#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());
|
||||
poolInterface->prepareRequest(http);
|
||||
int httpCode = http.GET();
|
||||
if (httpCode == 200)
|
||||
{
|
||||
String payload = http.getString();
|
||||
JsonDocument doc;
|
||||
deserializeJson(doc, payload);
|
||||
|
||||
PoolStats stats = poolInterface->parseResponse(doc);
|
||||
|
||||
miningPoolStatsHashrate = stats.hashrate;
|
||||
|
||||
if (stats.dailyEarnings)
|
||||
{
|
||||
miningPoolStatsDailyEarnings = *stats.dailyEarnings;
|
||||
}
|
||||
else
|
||||
{
|
||||
miningPoolStatsDailyEarnings = 0; // or any other default value
|
||||
}
|
||||
|
||||
if (workQueue != nullptr && (getCurrentScreen() == SCREEN_MINING_POOL_STATS_HASHRATE || 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 (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;
|
||||
}
|
19
src/lib/mining_pool_stats_fetch.hpp
Normal file
19
src/lib/mining_pool_stats_fetch.hpp
Normal 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();
|
|
@ -171,14 +171,13 @@ int downloadUpdateHandler(char updateType)
|
|||
break;
|
||||
case UPDATE_WEBUI:
|
||||
{
|
||||
latestRelease = getLatestRelease("littlefs.bin");
|
||||
latestRelease = getLatestRelease(getWebUiFilename());
|
||||
// updateWebUi(latestRelease.fileUrl, U_SPIFFS);
|
||||
// return 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// First, download the expected SHA256
|
||||
String expectedSHA256 = downloadSHA256(latestRelease.checksumUrl);
|
||||
if (expectedSHA256.isEmpty())
|
||||
|
|
|
@ -33,9 +33,19 @@ void workerTask(void *pvParameters) {
|
|||
parseBitaxeBestDiff(getBitaxeBestDiff());
|
||||
}
|
||||
setEpdContent(taskEpdContent);
|
||||
|
||||
break;
|
||||
}
|
||||
case TASK_MINING_POOL_STATS_UPDATE: {
|
||||
if (getCurrentScreen() == SCREEN_MINING_POOL_STATS_HASHRATE) {
|
||||
taskEpdContent =
|
||||
parseMiningPoolStatsHashRate(getMiningPoolStatsHashRate(), *getMiningPool());
|
||||
} else if (getCurrentScreen() == SCREEN_MINING_POOL_STATS_EARNINGS) {
|
||||
taskEpdContent =
|
||||
parseMiningPoolStatsDailyEarnings(getMiningPoolStatsDailyEarnings(), getMiningPool()->getDailyEarningsLabel(), *getMiningPool());
|
||||
}
|
||||
setEpdContent(taskEpdContent);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case TASK_PRICE_UPDATE: {
|
||||
uint currency = getCurrentCurrency();
|
||||
uint price = getPrice(currency);
|
||||
|
@ -179,6 +189,17 @@ void setCurrentScreen(uint newScreen) {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#include <data_handler.hpp>
|
||||
#include <bitaxe_handler.hpp>
|
||||
#include "lib/mining_pool/mining_pool_stats_handler.hpp"
|
||||
|
||||
#include "lib/epd.hpp"
|
||||
#include "lib/shared.hpp"
|
||||
|
@ -23,7 +24,8 @@ typedef enum {
|
|||
TASK_BLOCK_UPDATE,
|
||||
TASK_FEE_UPDATE,
|
||||
TASK_TIME_UPDATE,
|
||||
TASK_BITAXE_UPDATE
|
||||
TASK_BITAXE_UPDATE,
|
||||
TASK_MINING_POOL_STATS_UPDATE
|
||||
} TaskType;
|
||||
|
||||
typedef struct {
|
||||
|
|
|
@ -144,3 +144,39 @@ String calculateSHA256(WiFiClient *stream, size_t contentLength) {
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <Adafruit_MCP23X17.h>
|
||||
#include "MCP23017.h"
|
||||
// #include <zlib_turbo.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
#include <Preferences.h>
|
||||
|
@ -11,15 +12,18 @@
|
|||
#include <mbedtls/md.h>
|
||||
#include "esp_crt_bundle.h"
|
||||
#include <Update.h>
|
||||
#include <HTTPClient.h>
|
||||
|
||||
#include <mutex>
|
||||
#include <utils.hpp>
|
||||
|
||||
#include "defaults.hpp"
|
||||
|
||||
extern Adafruit_MCP23X17 mcp1;
|
||||
#define USER_AGENT "BTClock/3.0"
|
||||
|
||||
extern MCP23017 mcp1;
|
||||
#ifdef IS_BTCLOCK_V8
|
||||
extern Adafruit_MCP23X17 mcp2;
|
||||
extern MCP23017 mcp2;
|
||||
#endif
|
||||
extern Preferences preferences;
|
||||
extern std::mutex mcpMutex;
|
||||
|
@ -41,24 +45,16 @@ 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_BTC_TICKER_USD = 20;
|
||||
// const PROGMEM int SCREEN_BTC_TICKER_EUR = 21;
|
||||
// const PROGMEM int SCREEN_BTC_TICKER_GBP = 22;
|
||||
// const PROGMEM int SCREEN_BTC_TICKER_JPY = 23;
|
||||
// const PROGMEM int SCREEN_BTC_TICKER_AUD = 24;
|
||||
// const PROGMEM int SCREEN_BTC_TICKER_CAD = 25;
|
||||
|
||||
const PROGMEM int SCREEN_MARKET_CAP = 30;
|
||||
// const PROGMEM int SCREEN_MARKET_CAP_USD = 30;
|
||||
// const PROGMEM int SCREEN_MARKET_CAP_EUR = 31;
|
||||
// const PROGMEM int SCREEN_MARKET_CAP_GBP = 32;
|
||||
// const PROGMEM int SCREEN_MARKET_CAP_JPY = 33;
|
||||
// const PROGMEM int SCREEN_MARKET_CAP_AUD = 34;
|
||||
// const PROGMEM int SCREEN_MARKET_CAP_CAD = 35;
|
||||
|
||||
const PROGMEM int SCREEN_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 = 7;
|
||||
|
@ -73,7 +69,12 @@ const int usPerMinute = 60 * usPerSecond;
|
|||
extern const char *isrg_root_x1cert;
|
||||
|
||||
extern const uint8_t rootca_crt_bundle_start[] asm("_binary_x509_crt_bundle_start");
|
||||
// extern const uint8_t ocean_logo_comp[] asm("_binary_ocean_gz_start");
|
||||
// extern const uint8_t ocean_logo_comp_end[] asm("_binary_ocean_gz_end");
|
||||
|
||||
// uint8_t* getOceanIcon();
|
||||
|
||||
// const size_t ocean_logo_size = ocean_logo_comp_end - ocean_logo_comp;
|
||||
|
||||
const PROGMEM char UPDATE_FIRMWARE = U_FLASH;
|
||||
const PROGMEM char UPDATE_WEBUI = U_SPIFFS;
|
||||
|
@ -86,3 +87,25 @@ struct ScreenMapping {
|
|||
|
||||
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;
|
||||
};
|
|
@ -72,6 +72,10 @@ void IRAM_ATTR minuteTimerISR(void *arg) {
|
|||
vTaskNotifyGiveFromISR(bitaxeFetchTaskHandle, &xHigherPriorityTaskWoken);
|
||||
}
|
||||
|
||||
if (miningPoolStatsFetchTaskHandle != NULL) {
|
||||
vTaskNotifyGiveFromISR(miningPoolStatsFetchTaskHandle, &xHigherPriorityTaskWoken);
|
||||
}
|
||||
|
||||
if (xHigherPriorityTaskWoken == pdTRUE) {
|
||||
portYIELD_FROM_ISR();
|
||||
}
|
||||
|
|
|
@ -1,26 +1,37 @@
|
|||
#include "webserver.hpp"
|
||||
|
||||
static const char* JSON_CONTENT = "application/json";
|
||||
|
||||
static const char *const PROGMEM strSettings[] = {
|
||||
"hostnamePrefix", "mempoolInstance", "nostrPubKey", "nostrRelay", "bitaxeHostname", "miningPoolName", "miningPoolUser", "nostrZapPubkey", "httpAuthUser", "httpAuthPass", "gitReleaseUrl", "poolLogosUrl"};
|
||||
|
||||
static const char *const PROGMEM uintSettings[] = {"minSecPriceUpd", "fullRefreshMin", "ledBrightness", "flMaxBrightness", "flEffectDelay", "luxLightToggle", "wpTimeout", "srcV2Currency"};
|
||||
|
||||
static const char *const PROGMEM boolSettings[] = {"fetchEurPrice", "ledTestOnPower", "ledFlashOnUpd",
|
||||
"mdnsEnabled", "otaEnabled", "stealFocus",
|
||||
"mcapBigChar", "useSatsSymbol", "useBlkCountdown",
|
||||
"suffixPrice", "disableLeds", "ownDataSource",
|
||||
"mowMode", "suffixShareDot", "flOffWhenDark",
|
||||
"flAlwaysOn", "flDisable", "flFlashOnUpd",
|
||||
"mempoolSecure", "useNostr", "bitaxeEnabled",
|
||||
"miningPoolStats", "verticalDesc",
|
||||
"nostrZapNotify", "stagingSource", "httpAuthEnabled"};
|
||||
|
||||
AsyncWebServer server(80);
|
||||
AsyncEventSource events("/events");
|
||||
TaskHandle_t eventSourceTaskHandle;
|
||||
|
||||
#define HTTP_OK 200
|
||||
#define HTTP_BAD_REQUEST 400
|
||||
|
||||
void setupWebserver()
|
||||
{
|
||||
events.onConnect([](AsyncEventSourceClient *client)
|
||||
{ client->send("welcome", NULL, millis(), 1000); });
|
||||
server.addHandler(&events);
|
||||
|
||||
// server.ad.
|
||||
// server.serveStatic("/css", LittleFS, "/css/");
|
||||
// server.serveStatic("/fonts", LittleFS, "/fonts/");
|
||||
// server.serveStatic("/build", LittleFS, "/build");
|
||||
// server.serveStatic("/swagger.json", LittleFS, "/swagger.json");
|
||||
// server.serveStatic("/api.html", LittleFS, "/api.html");
|
||||
// server.serveStatic("/fs_hash.txt", LittleFS, "/fs_hash.txt");
|
||||
|
||||
AsyncStaticWebHandler &staticHandler = server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
|
||||
|
||||
|
||||
server.rewrite("/convert", "/");
|
||||
server.rewrite("/api", "/");
|
||||
|
||||
|
@ -31,7 +42,6 @@ void setupWebserver()
|
|||
preferences.getString("httpAuthPass", DEFAULT_HTTP_AUTH_PASSWORD));
|
||||
}
|
||||
// server.on("/", HTTP_GET, onIndex);
|
||||
|
||||
server.on("/api/status", HTTP_GET, onApiStatus);
|
||||
server.on("/api/system_status", HTTP_GET, onApiSystemStatus);
|
||||
server.on("/api/wifi_set_tx_power", HTTP_GET, onApiSetWifiTxPower);
|
||||
|
@ -51,8 +61,8 @@ void setupWebserver()
|
|||
|
||||
server.on("/api/show/text", HTTP_GET, onApiShowText);
|
||||
|
||||
server.on("/api/screen/next", HTTP_GET, onApiScreenNext);
|
||||
server.on("/api/screen/previous", HTTP_GET, onApiScreenPrevious);
|
||||
server.on("/api/screen/next", HTTP_GET, onApiScreenControl);
|
||||
server.on("/api/screen/previous", HTTP_GET, onApiScreenControl);
|
||||
|
||||
AsyncCallbackJsonWebHandler *settingsPatchHandler =
|
||||
new AsyncCallbackJsonWebHandler("/api/json/settings", onApiSettingsPatch);
|
||||
|
@ -212,36 +222,6 @@ void asyncFileUpdateHandler(AsyncWebServerRequest *request, String filename, siz
|
|||
void asyncFirmwareUpdateHandler(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
|
||||
{
|
||||
asyncFileUpdateHandler(request, filename, index, data, len, final, U_FLASH);
|
||||
|
||||
// if (!index)
|
||||
// {
|
||||
// Serial.printf("Update Start: %s\n", filename.c_str());
|
||||
|
||||
// // Update.runAsync(true);
|
||||
// if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000))
|
||||
// {
|
||||
// Update.printError(Serial);
|
||||
// }
|
||||
// }
|
||||
// if (!Update.hasError())
|
||||
// {
|
||||
// if (Update.write(data, len) != len)
|
||||
// {
|
||||
// Update.printError(Serial);
|
||||
// }
|
||||
// }
|
||||
// if (final)
|
||||
// {
|
||||
// if (Update.end(true))
|
||||
// {
|
||||
// Serial.printf("Update Success: %uB\n", index + len);
|
||||
// onApiRestart(request);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Update.printError(Serial);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
JsonDocument getStatusObject()
|
||||
|
@ -295,7 +275,6 @@ JsonDocument getLedStatusObject()
|
|||
for (uint i = 0; i < pixels.numPixels(); i++)
|
||||
{
|
||||
uint32_t pixColor = pixels.getPixelColor(pixels.numPixels() - i - 1);
|
||||
uint alpha = (pixColor >> 24) & 0xFF;
|
||||
uint red = (pixColor >> 16) & 0xFF;
|
||||
uint green = (pixColor >> 8) & 0xFF;
|
||||
uint blue = pixColor & 0xFF;
|
||||
|
@ -313,25 +292,26 @@ JsonDocument getLedStatusObject()
|
|||
return root;
|
||||
}
|
||||
|
||||
void eventSourceUpdate()
|
||||
{
|
||||
if (!events.count())
|
||||
return;
|
||||
JsonDocument root = getStatusObject();
|
||||
JsonArray data = root["data"].to<JsonArray>();
|
||||
void eventSourceUpdate() {
|
||||
if (!events.count()) return;
|
||||
|
||||
root["leds"] = getLedStatusObject()["data"];
|
||||
JsonDocument doc = getStatusObject();
|
||||
doc["leds"] = getLedStatusObject()["data"];
|
||||
|
||||
String epdContent[NUM_SCREENS];
|
||||
std::array<String, NUM_SCREENS> retEpdContent = getCurrentEpdContent();
|
||||
std::copy(std::begin(retEpdContent), std::end(retEpdContent), epdContent);
|
||||
// Get current EPD content directly as array
|
||||
std::array<String, NUM_SCREENS> epdContent = getCurrentEpdContent();
|
||||
|
||||
copyArray(epdContent, data);
|
||||
// Add EPD content arrays
|
||||
JsonArray data = doc["data"].to<JsonArray>();
|
||||
|
||||
String bufString;
|
||||
serializeJson(root, bufString);
|
||||
// Copy array elements directly
|
||||
for(const auto& content : epdContent) {
|
||||
data.add(content);
|
||||
}
|
||||
|
||||
events.send(bufString.c_str(), "status");
|
||||
String buffer;
|
||||
serializeJson(doc, buffer);
|
||||
events.send(buffer.c_str(), "status");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -341,21 +321,22 @@ void eventSourceUpdate()
|
|||
void onApiStatus(AsyncWebServerRequest *request)
|
||||
{
|
||||
AsyncResponseStream *response =
|
||||
request->beginResponseStream("application/json");
|
||||
request->beginResponseStream(JSON_CONTENT);
|
||||
|
||||
JsonDocument root = getStatusObject();
|
||||
|
||||
// Get current EPD content directly as array
|
||||
std::array<String, NUM_SCREENS> epdContent = getCurrentEpdContent();
|
||||
|
||||
// Add EPD content arrays
|
||||
JsonArray data = root["data"].to<JsonArray>();
|
||||
JsonArray rendered = root["rendered"].to<JsonArray>();
|
||||
String epdContent[NUM_SCREENS];
|
||||
|
||||
// Copy array elements directly
|
||||
for(const auto& content : epdContent) {
|
||||
data.add(content);
|
||||
}
|
||||
|
||||
root["leds"] = getLedStatusObject()["data"];
|
||||
|
||||
std::array<String, NUM_SCREENS> retEpdContent = getCurrentEpdContent();
|
||||
|
||||
std::copy(std::begin(retEpdContent), std::end(retEpdContent), epdContent);
|
||||
|
||||
copyArray(epdContent, data);
|
||||
copyArray(epdContent, rendered);
|
||||
serializeJson(root, *response);
|
||||
|
||||
request->send(response);
|
||||
|
@ -368,7 +349,7 @@ void onApiStatus(AsyncWebServerRequest *request)
|
|||
void onApiActionPause(AsyncWebServerRequest *request)
|
||||
{
|
||||
setTimerActive(false);
|
||||
request->send(200);
|
||||
request->send(HTTP_OK);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -378,7 +359,7 @@ void onApiActionPause(AsyncWebServerRequest *request)
|
|||
void onApiActionTimerRestart(AsyncWebServerRequest *request)
|
||||
{
|
||||
setTimerActive(true);
|
||||
request->send(200);
|
||||
request->send(HTTP_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -392,7 +373,7 @@ void onApiFullRefresh(AsyncWebServerRequest *request)
|
|||
|
||||
setEpdContent(newEpdContent, true);
|
||||
|
||||
request->send(200);
|
||||
request->send(HTTP_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -407,28 +388,21 @@ void onApiShowScreen(AsyncWebServerRequest *request)
|
|||
uint currentScreen = p->value().toInt();
|
||||
setCurrentScreen(currentScreen);
|
||||
}
|
||||
request->send(200);
|
||||
request->send(HTTP_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Api
|
||||
* @Path("/api/screen/next")
|
||||
*/
|
||||
void onApiScreenNext(AsyncWebServerRequest *request)
|
||||
{
|
||||
nextScreen();
|
||||
request->send(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Api
|
||||
* @Path("/api/screen/previous")
|
||||
*/
|
||||
void onApiScreenPrevious(AsyncWebServerRequest *request)
|
||||
{
|
||||
previousScreen();
|
||||
|
||||
request->send(200);
|
||||
void onApiScreenControl(AsyncWebServerRequest *request) {
|
||||
const String& action = request->url();
|
||||
if (action.endsWith("/next")) {
|
||||
nextScreen();
|
||||
} else if (action.endsWith("/previous")) {
|
||||
previousScreen();
|
||||
}
|
||||
request->send(HTTP_OK);
|
||||
}
|
||||
|
||||
void onApiShowText(AsyncWebServerRequest *request)
|
||||
|
@ -448,7 +422,7 @@ void onApiShowText(AsyncWebServerRequest *request)
|
|||
setEpdContent(textEpdContent);
|
||||
}
|
||||
setCurrentScreen(SCREEN_CUSTOM);
|
||||
request->send(200);
|
||||
request->send(HTTP_OK);
|
||||
}
|
||||
|
||||
void onApiShowTextAdvanced(AsyncWebServerRequest *request, JsonVariant &json)
|
||||
|
@ -466,7 +440,7 @@ void onApiShowTextAdvanced(AsyncWebServerRequest *request, JsonVariant &json)
|
|||
setEpdContent(epdContent);
|
||||
|
||||
setCurrentScreen(SCREEN_CUSTOM);
|
||||
request->send(200);
|
||||
request->send(HTTP_OK);
|
||||
}
|
||||
|
||||
void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json)
|
||||
|
@ -484,49 +458,49 @@ void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json)
|
|||
|
||||
bool settingsChanged = true;
|
||||
|
||||
if (settings.containsKey("fgColor"))
|
||||
if (settings["fgColor"].is<String>())
|
||||
{
|
||||
String fgColor = settings["fgColor"].as<String>();
|
||||
preferences.putUInt("fgColor", strtol(fgColor.c_str(), NULL, 16));
|
||||
setFgColor(int(strtol(fgColor.c_str(), NULL, 16)));
|
||||
uint32_t color = strtol(fgColor.c_str(), NULL, 16);
|
||||
preferences.putUInt("fgColor", color);
|
||||
setFgColor(color);
|
||||
Serial.print(F("Setting foreground color to "));
|
||||
Serial.println(strtol(fgColor.c_str(), NULL, 16));
|
||||
Serial.println(color);
|
||||
settingsChanged = true;
|
||||
}
|
||||
if (settings.containsKey("bgColor"))
|
||||
if (settings["bgColor"].is<String>())
|
||||
{
|
||||
String bgColor = settings["bgColor"].as<String>();
|
||||
|
||||
preferences.putUInt("bgColor", strtol(bgColor.c_str(), NULL, 16));
|
||||
setBgColor(int(strtol(bgColor.c_str(), NULL, 16)));
|
||||
uint32_t color = strtol(bgColor.c_str(), NULL, 16);
|
||||
preferences.putUInt("bgColor", color);
|
||||
setBgColor(color);
|
||||
Serial.print(F("Setting background color to "));
|
||||
Serial.println(bgColor.c_str());
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
if (settings.containsKey("timePerScreen"))
|
||||
if (settings["timePerScreen"].is<uint>())
|
||||
{
|
||||
preferences.putUInt("timerSeconds",
|
||||
settings["timePerScreen"].as<uint>() * 60);
|
||||
}
|
||||
|
||||
String strSettings[] = {"hostnamePrefix", "mempoolInstance", "nostrPubKey", "nostrRelay", "bitaxeHostname", "nostrZapPubkey", "httpAuthUser", "httpAuthPass", "gitReleaseUrl"};
|
||||
|
||||
for (String setting : strSettings)
|
||||
{
|
||||
if (settings.containsKey(setting))
|
||||
if (settings[setting].is<String>())
|
||||
{
|
||||
preferences.putString(setting.c_str(), settings[setting].as<String>());
|
||||
Serial.printf("Setting %s to %s\r\n", setting.c_str(),
|
||||
settings[setting].as<String>());
|
||||
settings[setting].as<String>().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
String uintSettings[] = {"minSecPriceUpd", "fullRefreshMin", "ledBrightness", "flMaxBrightness", "flEffectDelay", "luxLightToggle", "wpTimeout", "srcV2Currency"};
|
||||
|
||||
|
||||
for (String setting : uintSettings)
|
||||
{
|
||||
if (settings.containsKey(setting))
|
||||
if (settings[setting].is<uint>())
|
||||
{
|
||||
preferences.putUInt(setting.c_str(), settings[setting].as<uint>());
|
||||
Serial.printf("Setting %s to %d\r\n", setting.c_str(),
|
||||
|
@ -534,7 +508,7 @@ void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json)
|
|||
}
|
||||
}
|
||||
|
||||
if (settings.containsKey("tzOffset"))
|
||||
if (settings["tzOffset"].is<int>())
|
||||
{
|
||||
int gmtOffset = settings["tzOffset"].as<int>() * 60;
|
||||
size_t written = preferences.putInt("gmtOffset", gmtOffset);
|
||||
|
@ -542,27 +516,17 @@ void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json)
|
|||
gmtOffset, settings["tzOffset"].as<int>(), written);
|
||||
}
|
||||
|
||||
String boolSettings[] = {"fetchEurPrice", "ledTestOnPower", "ledFlashOnUpd",
|
||||
"mdnsEnabled", "otaEnabled", "stealFocus",
|
||||
"mcapBigChar", "useSatsSymbol", "useBlkCountdown",
|
||||
"suffixPrice", "disableLeds", "ownDataSource",
|
||||
"mowMode", "suffixShareDot", "flOffWhenDark",
|
||||
"flAlwaysOn", "flDisable", "flFlashOnUpd",
|
||||
"mempoolSecure", "useNostr", "bitaxeEnabled",
|
||||
"verticalDesc",
|
||||
"nostrZapNotify", "stagingSource", "httpAuthEnabled"};
|
||||
|
||||
for (String setting : boolSettings)
|
||||
{
|
||||
if (settings.containsKey(setting))
|
||||
if (settings[setting].is<bool>())
|
||||
{
|
||||
preferences.putBool(setting.c_str(), settings[setting].as<boolean>());
|
||||
preferences.putBool(setting.c_str(), settings[setting].as<bool>());
|
||||
Serial.printf("Setting %s to %d\r\n", setting.c_str(),
|
||||
settings[setting].as<boolean>());
|
||||
settings[setting].as<bool>());
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.containsKey("screens"))
|
||||
if (settings["screens"].is<JsonArray>())
|
||||
{
|
||||
for (JsonVariant screen : settings["screens"].as<JsonArray>())
|
||||
{
|
||||
|
@ -570,12 +534,12 @@ void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json)
|
|||
uint id = s["id"].as<uint>();
|
||||
String key = "screen[" + String(id) + "]";
|
||||
String prefKey = "screen" + String(id) + "Visible";
|
||||
bool visible = s["enabled"].as<boolean>();
|
||||
bool visible = s["enabled"].as<bool>();
|
||||
preferences.putBool(prefKey.c_str(), visible);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.containsKey("actCurrencies"))
|
||||
if (settings["actCurrencies"].is<JsonArray>())
|
||||
{
|
||||
String actCurrencies;
|
||||
|
||||
|
@ -589,10 +553,10 @@ void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json)
|
|||
}
|
||||
|
||||
preferences.putString("actCurrencies", actCurrencies.c_str());
|
||||
Serial.printf("Set actCurrencies: %s\n", actCurrencies);
|
||||
Serial.printf("Set actCurrencies: %s\n", actCurrencies.c_str());
|
||||
}
|
||||
|
||||
if (settings.containsKey("txPower"))
|
||||
if (settings["txPower"].is<int>())
|
||||
{
|
||||
int txPower = settings["txPower"].as<int>();
|
||||
|
||||
|
@ -619,7 +583,7 @@ void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json)
|
|||
}
|
||||
}
|
||||
|
||||
request->send(200);
|
||||
request->send(HTTP_OK);
|
||||
if (settingsChanged)
|
||||
{
|
||||
queueLedEffect(LED_FLASH_SUCCESS);
|
||||
|
@ -628,7 +592,7 @@ void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json)
|
|||
|
||||
void onApiRestart(AsyncWebServerRequest *request)
|
||||
{
|
||||
request->send(200);
|
||||
request->send(HTTP_OK);
|
||||
|
||||
if (events.count())
|
||||
events.send("closing");
|
||||
|
@ -642,7 +606,7 @@ void onApiIdentify(AsyncWebServerRequest *request)
|
|||
{
|
||||
queueLedEffect(LED_FLASH_IDENTIFY);
|
||||
|
||||
request->send(200);
|
||||
request->send(HTTP_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -714,10 +678,13 @@ void onApiSettingsGet(AsyncWebServerRequest *request)
|
|||
root["bitaxeEnabled"] = preferences.getBool("bitaxeEnabled", DEFAULT_BITAXE_ENABLED);
|
||||
root["bitaxeHostname"] = preferences.getString("bitaxeHostname", DEFAULT_BITAXE_HOSTNAME);
|
||||
|
||||
root["miningPoolStats"] = preferences.getBool("miningPoolStats", DEFAULT_MINING_POOL_STATS_ENABLED);
|
||||
root["miningPoolName"] = preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME);
|
||||
root["miningPoolUser"] = preferences.getString("miningPoolUser", DEFAULT_MINING_POOL_USER);
|
||||
root["availablePools"] = PoolFactory::getAvailablePools();
|
||||
root["httpAuthEnabled"] = preferences.getBool("httpAuthEnabled", DEFAULT_HTTP_AUTH_ENABLED);
|
||||
root["httpAuthUser"] = preferences.getString("httpAuthUser", DEFAULT_HTTP_AUTH_USERNAME);
|
||||
root["httpAuthPass"] = preferences.getString("httpAuthPass", DEFAULT_HTTP_AUTH_PASSWORD);
|
||||
|
||||
#ifdef HAS_FRONTLIGHT
|
||||
root["hasFrontlight"] = true;
|
||||
root["flDisable"] = preferences.getBool("flDisable");
|
||||
|
@ -751,17 +718,8 @@ void onApiSettingsGet(AsyncWebServerRequest *request)
|
|||
#endif
|
||||
JsonArray screens = root["screens"].to<JsonArray>();
|
||||
|
||||
JsonArray actCurrencies = root["actCurrencies"].to<JsonArray>();
|
||||
for (const auto &str : getActiveCurrencies())
|
||||
{
|
||||
actCurrencies.add(str);
|
||||
}
|
||||
|
||||
JsonArray availableCurrencies = root["availableCurrencies"].to<JsonArray>();
|
||||
for (const auto &str : getAvailableCurrencies())
|
||||
{
|
||||
availableCurrencies.add(str);
|
||||
}
|
||||
root["actCurrencies"] = getActiveCurrencies();
|
||||
root["availableCurrencies"] = getAvailableCurrencies();
|
||||
|
||||
std::vector<ScreenMapping> screenNameMap = getScreenNameMap();
|
||||
|
||||
|
@ -774,8 +732,10 @@ void onApiSettingsGet(AsyncWebServerRequest *request)
|
|||
o["enabled"] = preferences.getBool(key.c_str(), true);
|
||||
}
|
||||
|
||||
root["poolLogosUrl"] = preferences.getString("poolLogosUrl", DEFAULT_MINING_POOL_LOGOS_URL);
|
||||
|
||||
AsyncResponseStream *response =
|
||||
request->beginResponseStream("application/json");
|
||||
request->beginResponseStream(JSON_CONTENT);
|
||||
serializeJson(root, *response);
|
||||
|
||||
request->send(response);
|
||||
|
@ -787,8 +747,9 @@ bool processEpdColorSettings(AsyncWebServerRequest *request)
|
|||
if (request->hasParam("fgColor", true))
|
||||
{
|
||||
const AsyncWebParameter *fgColor = request->getParam("fgColor", true);
|
||||
preferences.putUInt("fgColor", strtol(fgColor->value().c_str(), NULL, 16));
|
||||
setFgColor(int(strtol(fgColor->value().c_str(), NULL, 16)));
|
||||
uint32_t color = strtol(fgColor->value().c_str(), NULL, 16);
|
||||
preferences.putUInt("fgColor", color);
|
||||
setFgColor(color);
|
||||
// Serial.print(F("Setting foreground color to "));
|
||||
// Serial.println(fgColor->value().c_str());
|
||||
settingsChanged = true;
|
||||
|
@ -797,8 +758,9 @@ bool processEpdColorSettings(AsyncWebServerRequest *request)
|
|||
{
|
||||
const AsyncWebParameter *bgColor = request->getParam("bgColor", true);
|
||||
|
||||
preferences.putUInt("bgColor", strtol(bgColor->value().c_str(), NULL, 16));
|
||||
setBgColor(int(strtol(bgColor->value().c_str(), NULL, 16)));
|
||||
uint32_t color = strtol(bgColor->value().c_str(), NULL, 16);
|
||||
preferences.putUInt("bgColor", color);
|
||||
setBgColor(color);
|
||||
// Serial.print(F("Setting background color to "));
|
||||
// Serial.println(bgColor->value().c_str());
|
||||
settingsChanged = true;
|
||||
|
@ -810,7 +772,7 @@ bool processEpdColorSettings(AsyncWebServerRequest *request)
|
|||
void onApiSystemStatus(AsyncWebServerRequest *request)
|
||||
{
|
||||
AsyncResponseStream *response =
|
||||
request->beginResponseStream("application/json");
|
||||
request->beginResponseStream(JSON_CONTENT);
|
||||
|
||||
JsonDocument root;
|
||||
|
||||
|
@ -818,6 +780,9 @@ void onApiSystemStatus(AsyncWebServerRequest *request)
|
|||
root["espHeapSize"] = ESP.getHeapSize();
|
||||
root["espFreePsram"] = ESP.getFreePsram();
|
||||
root["espPsramSize"] = ESP.getPsramSize();
|
||||
root["fsUsedBytes"] = LittleFS.usedBytes();
|
||||
root["fsTotalBytes"] = LittleFS.totalBytes();
|
||||
|
||||
root["rssi"] = WiFi.RSSI();
|
||||
root["txPower"] = WiFi.getTxPower();
|
||||
|
||||
|
@ -849,19 +814,19 @@ void onApiSetWifiTxPower(AsyncWebServerRequest *request)
|
|||
if (WiFi.setTxPower(static_cast<wifi_power_t>(txPower)))
|
||||
{
|
||||
preferences.putInt("txPower", txPower);
|
||||
request->send(200, "application/json", "{\"setTxPower\": \"ok\"}");
|
||||
request->send(HTTP_OK, "application/json", "{\"setTxPower\": \"ok\"}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return request->send(400);
|
||||
return request->send(HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
void onApiLightsStatus(AsyncWebServerRequest *request)
|
||||
{
|
||||
AsyncResponseStream *response =
|
||||
request->beginResponseStream("application/json");
|
||||
request->beginResponseStream(JSON_CONTENT);
|
||||
|
||||
serializeJson(getLedStatusObject()["data"], *response);
|
||||
|
||||
|
@ -871,7 +836,7 @@ void onApiLightsStatus(AsyncWebServerRequest *request)
|
|||
void onApiStopDataSources(AsyncWebServerRequest *request)
|
||||
{
|
||||
AsyncResponseStream *response =
|
||||
request->beginResponseStream("application/json");
|
||||
request->beginResponseStream(JSON_CONTENT);
|
||||
|
||||
stopPriceNotify();
|
||||
stopBlockNotify();
|
||||
|
@ -882,7 +847,7 @@ void onApiStopDataSources(AsyncWebServerRequest *request)
|
|||
void onApiRestartDataSources(AsyncWebServerRequest *request)
|
||||
{
|
||||
AsyncResponseStream *response =
|
||||
request->beginResponseStream("application/json");
|
||||
request->beginResponseStream(JSON_CONTENT);
|
||||
|
||||
restartPriceNotify();
|
||||
restartBlockNotify();
|
||||
|
@ -895,7 +860,7 @@ void onApiRestartDataSources(AsyncWebServerRequest *request)
|
|||
void onApiLightsOff(AsyncWebServerRequest *request)
|
||||
{
|
||||
setLights(0, 0, 0);
|
||||
request->send(200);
|
||||
request->send(HTTP_OK);
|
||||
}
|
||||
|
||||
void onApiLightsSetColor(AsyncWebServerRequest *request)
|
||||
|
@ -903,7 +868,7 @@ void onApiLightsSetColor(AsyncWebServerRequest *request)
|
|||
if (request->hasParam("c"))
|
||||
{
|
||||
AsyncResponseStream *response =
|
||||
request->beginResponseStream("application/json");
|
||||
request->beginResponseStream(JSON_CONTENT);
|
||||
|
||||
String rgbColor = request->getParam("c")->value();
|
||||
|
||||
|
@ -927,7 +892,7 @@ void onApiLightsSetColor(AsyncWebServerRequest *request)
|
|||
}
|
||||
else
|
||||
{
|
||||
request->send(400);
|
||||
request->send(HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -944,7 +909,7 @@ void onApiLightsSetJson(AsyncWebServerRequest *request, JsonVariant &json)
|
|||
}
|
||||
|
||||
Serial.printf("Invalid values for LED set %d\n", lights.size());
|
||||
request->send(400);
|
||||
request->send(HTTP_BAD_REQUEST);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -952,27 +917,27 @@ void onApiLightsSetJson(AsyncWebServerRequest *request, JsonVariant &json)
|
|||
{
|
||||
unsigned int red, green, blue;
|
||||
|
||||
if (lights[i].containsKey("red") && lights[i].containsKey("green") &&
|
||||
lights[i].containsKey("blue"))
|
||||
if (lights[i]["red"].is<uint>() && lights[i]["green"].is<uint>() &&
|
||||
lights[i]["blue"].is<uint>())
|
||||
{
|
||||
red = lights[i]["red"].as<uint>();
|
||||
green = lights[i]["green"].as<uint>();
|
||||
blue = lights[i]["blue"].as<uint>();
|
||||
}
|
||||
else if (lights[i].containsKey("hex"))
|
||||
else if (lights[i]["hex"].is<const char*>())
|
||||
{
|
||||
if (!sscanf(lights[i]["hex"].as<String>().c_str(), "#%02X%02X%02X", &red,
|
||||
&green, &blue) == 3)
|
||||
{
|
||||
Serial.printf("Invalid hex for LED %d\n", i);
|
||||
request->send(400);
|
||||
request->send(HTTP_BAD_REQUEST);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Serial.printf("No valid color for LED %d\n", i);
|
||||
request->send(400);
|
||||
request->send(HTTP_BAD_REQUEST);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -983,7 +948,7 @@ void onApiLightsSetJson(AsyncWebServerRequest *request, JsonVariant &json)
|
|||
pixels.show();
|
||||
saveLedState();
|
||||
|
||||
request->send(200);
|
||||
request->send(HTTP_OK);
|
||||
}
|
||||
|
||||
void onIndex(AsyncWebServerRequest *request)
|
||||
|
@ -993,48 +958,14 @@ void onIndex(AsyncWebServerRequest *request)
|
|||
|
||||
void onNotFound(AsyncWebServerRequest *request)
|
||||
{
|
||||
// Serial.printf("NotFound, URL[%s]\n", request->url());
|
||||
|
||||
// Serial.printf("NotFound, METHOD[%s]\n", request->methodToString());
|
||||
|
||||
// int headers = request->headers();
|
||||
// int i;
|
||||
// for (i = 0; i < headers; i++)
|
||||
// {
|
||||
// AsyncWebHeader *h = request->getHeader(i);
|
||||
// Serial.printf("NotFound HEADER[%s]: %s\n", h->name().c_str(),
|
||||
// h->value().c_str());
|
||||
// }
|
||||
|
||||
// int params = request->params();
|
||||
// for (int i = 0; i < params; i++)
|
||||
// {
|
||||
// const AsyncWebParameter *p = request->getParam(i);
|
||||
// if (p->isFile())
|
||||
// { // p->isPost() is also true
|
||||
// Serial.printf("NotFound FILE[%s]: %s, size: %u\n",
|
||||
// p->name().c_str(), p->value().c_str(), p->size());
|
||||
// }
|
||||
// else if (p->isPost())
|
||||
// {
|
||||
// Serial.printf("NotFound POST[%s]: %s\n", p->name().c_str(),
|
||||
// p->value().c_str());
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Serial.printf("NotFound GET[%s]: %s\n", p->name().c_str(),
|
||||
// p->value().c_str());
|
||||
// }
|
||||
// }
|
||||
|
||||
// Access-Control-Request-Method == POST might be better
|
||||
// Access-Control-Request-Method == POST might be better
|
||||
|
||||
if (request->method() == HTTP_OPTIONS ||
|
||||
request->hasHeader("Sec-Fetch-Mode"))
|
||||
{
|
||||
// Serial.printf("NotFound, Return[%d]\n", 200);
|
||||
|
||||
request->send(200);
|
||||
request->send(HTTP_OK);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1070,7 +1001,7 @@ void onApiShowCurrency(AsyncWebServerRequest *request)
|
|||
setCurrentCurrency(curChar);
|
||||
setCurrentScreen(getCurrentScreen());
|
||||
|
||||
request->send(200);
|
||||
request->send(HTTP_OK);
|
||||
return;
|
||||
}
|
||||
request->send(404);
|
||||
|
@ -1081,13 +1012,13 @@ void onApiFrontlightOn(AsyncWebServerRequest *request)
|
|||
{
|
||||
frontlightFadeInAll();
|
||||
|
||||
request->send(200);
|
||||
request->send(HTTP_OK);
|
||||
}
|
||||
|
||||
void onApiFrontlightStatus(AsyncWebServerRequest *request)
|
||||
{
|
||||
AsyncResponseStream *response =
|
||||
request->beginResponseStream("application/json");
|
||||
request->beginResponseStream(JSON_CONTENT);
|
||||
|
||||
JsonDocument root;
|
||||
|
||||
|
@ -1106,7 +1037,7 @@ void onApiFrontlightFlash(AsyncWebServerRequest *request)
|
|||
{
|
||||
frontlightFlash(preferences.getUInt("flEffectDelay"));
|
||||
|
||||
request->send(200);
|
||||
request->send(HTTP_OK);
|
||||
}
|
||||
|
||||
void onApiFrontlightSetBrightness(AsyncWebServerRequest *request)
|
||||
|
@ -1114,11 +1045,11 @@ void onApiFrontlightSetBrightness(AsyncWebServerRequest *request)
|
|||
if (request->hasParam("b"))
|
||||
{
|
||||
frontlightSetBrightness(request->getParam("b")->value().toInt());
|
||||
request->send(200);
|
||||
request->send(HTTP_OK);
|
||||
}
|
||||
else
|
||||
{
|
||||
request->send(400);
|
||||
request->send(HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1126,6 +1057,6 @@ void onApiFrontlightOff(AsyncWebServerRequest *request)
|
|||
{
|
||||
frontlightFadeOutAll();
|
||||
|
||||
request->send(200);
|
||||
request->send(HTTP_OK);
|
||||
}
|
||||
#endif
|
|
@ -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;
|
||||
|
||||
|
@ -27,8 +28,7 @@ void onApiStatus(AsyncWebServerRequest *request);
|
|||
void onApiSystemStatus(AsyncWebServerRequest *request);
|
||||
void onApiSetWifiTxPower(AsyncWebServerRequest *request);
|
||||
|
||||
void onApiScreenNext(AsyncWebServerRequest *request);
|
||||
void onApiScreenPrevious(AsyncWebServerRequest *request);
|
||||
void onApiScreenControl(AsyncWebServerRequest *request);
|
||||
|
||||
void onApiShowScreen(AsyncWebServerRequest *request);
|
||||
void onApiShowCurrency(AsyncWebServerRequest *request);
|
||||
|
|
252
src/main.cpp
252
src/main.cpp
|
@ -22,152 +22,132 @@
|
|||
uint wifiLostConnection;
|
||||
uint priceNotifyLostConnection = 0;
|
||||
uint blockNotifyLostConnection = 0;
|
||||
// char ptrTaskList[1500];
|
||||
|
||||
extern "C" void app_main()
|
||||
{
|
||||
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();
|
||||
|
||||
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)
|
||||
while (true) {
|
||||
if (eventSourceTaskHandle != NULL) {
|
||||
xTaskNotifyGive(eventSourceTaskHandle);
|
||||
}
|
||||
|
||||
int64_t currentUptime = esp_timer_get_time() / 1000000;
|
||||
;
|
||||
if (!getIsOTAUpdating()) {
|
||||
handleFrontlight();
|
||||
checkWiFiConnection();
|
||||
monitorDataConnections();
|
||||
|
||||
if (!getIsOTAUpdating())
|
||||
{
|
||||
#ifdef HAS_FRONTLIGHT
|
||||
if (hasLightLevel()) {
|
||||
if (preferences.getUInt("luxLightToggle", DEFAULT_LUX_LIGHT_TOGGLE) != 0)
|
||||
{
|
||||
if (hasLightLevel() && getLightLevel() <= 1 && preferences.getBool("flOffWhenDark", DEFAULT_FL_OFF_WHEN_DARK))
|
||||
{
|
||||
if (frontlightIsOn()) {
|
||||
frontlightFadeOutAll();
|
||||
}
|
||||
}
|
||||
else if (hasLightLevel() && getLightLevel() < preferences.getUInt("luxLightToggle", DEFAULT_LUX_LIGHT_TOGGLE) && !frontlightIsOn())
|
||||
{
|
||||
frontlightFadeInAll();
|
||||
}
|
||||
else if (frontlightIsOn() && getLightLevel() > preferences.getUInt("luxLightToggle", DEFAULT_LUX_LIGHT_TOGGLE))
|
||||
{
|
||||
frontlightFadeOutAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!WiFi.isConnected())
|
||||
{
|
||||
if (!wifiLostConnection)
|
||||
{
|
||||
wifiLostConnection = currentUptime;
|
||||
Serial.println(F("Lost WiFi connection, trying to reconnect..."));
|
||||
}
|
||||
|
||||
if ((currentUptime - 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."));
|
||||
}
|
||||
|
||||
if (getPriceNotifyInit() && !preferences.getBool("fetchEurPrice", DEFAULT_FETCH_EUR_PRICE) && !isPriceNotifyConnected())
|
||||
{
|
||||
priceNotifyLostConnection++;
|
||||
Serial.println(F("Lost price data connection..."));
|
||||
queueLedEffect(LED_DATA_PRICE_ERROR);
|
||||
|
||||
// if price WS connection does not come back after 6*5 seconds, destroy and recreate
|
||||
if (priceNotifyLostConnection > 6)
|
||||
{
|
||||
Serial.println(F("Restarting price handler..."));
|
||||
|
||||
restartPriceNotify();
|
||||
// setupPriceNotify();
|
||||
priceNotifyLostConnection = 0;
|
||||
}
|
||||
}
|
||||
else if (priceNotifyLostConnection > 0 && isPriceNotifyConnected())
|
||||
{
|
||||
priceNotifyLostConnection = 0;
|
||||
}
|
||||
|
||||
if (getBlockNotifyInit() && !isBlockNotifyConnected())
|
||||
{
|
||||
blockNotifyLostConnection++;
|
||||
Serial.println(F("Lost block data connection..."));
|
||||
queueLedEffect(LED_DATA_BLOCK_ERROR);
|
||||
// if mempool WS connection does not come back after 6*5 seconds, destroy and recreate
|
||||
if (blockNotifyLostConnection > 6)
|
||||
{
|
||||
Serial.println(F("Restarting block handler..."));
|
||||
|
||||
restartBlockNotify();
|
||||
// setupBlockNotify();
|
||||
blockNotifyLostConnection = 0;
|
||||
}
|
||||
}
|
||||
else if (blockNotifyLostConnection > 0 && isBlockNotifyConnected())
|
||||
{
|
||||
blockNotifyLostConnection = 0;
|
||||
}
|
||||
|
||||
// if more than 5 price updates are missed, there is probably something wrong, reconnect
|
||||
if ((getLastPriceUpdate(CURRENCY_USD) - currentUptime) > (preferences.getUInt("minSecPriceUpd", DEFAULT_SECONDS_BETWEEN_PRICE_UPDATE) * 5))
|
||||
{
|
||||
Serial.println(F("Detected 5 missed price updates... restarting price handler."));
|
||||
|
||||
restartPriceNotify();
|
||||
// setupPriceNotify();
|
||||
|
||||
priceNotifyLostConnection = 0;
|
||||
}
|
||||
|
||||
// If after 45 minutes no mempool blocks, check the rest API
|
||||
if ((getLastBlockUpdate() - currentUptime) > 45 * 60)
|
||||
{
|
||||
Serial.println(F("Long time (45 min) since last block, checking if I missed anything..."));
|
||||
int currentBlock = getBlockFetch();
|
||||
if (currentBlock != -1)
|
||||
{
|
||||
if (currentBlock != getBlockHeight())
|
||||
{
|
||||
Serial.println(F("Detected stuck block height... restarting block handler."));
|
||||
// Mempool source stuck, restart
|
||||
restartBlockNotify();
|
||||
// setupBlockNotify();
|
||||
}
|
||||
// set last block update so it doesn't fetch for 45 minutes
|
||||
setLastBlockUpdate(currentUptime);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentUptime - getLastTimeSync() > 24 * 60 * 60)
|
||||
{
|
||||
if (getUptime() - getLastTimeSync() > 24 * 60 * 60) {
|
||||
Serial.println(F("Last time update is longer than 24 hours ago, sync again"));
|
||||
syncTime();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
|
|
75
test/test_mining_pool/test_main.cpp
Normal file
75
test/test_mining_pool/test_main.cpp
Normal file
|
@ -0,0 +1,75 @@
|
|||
#include <utils.hpp>
|
||||
#include <unity.h>
|
||||
|
||||
void test_parseMiningPoolStatsHashRate1dot34TH(void)
|
||||
{
|
||||
std::string hashrate;
|
||||
std::string label;
|
||||
std::string output;
|
||||
|
||||
parseHashrateString("1340000000000", label, output, 4);
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING_MESSAGE("TH/S", label.c_str(), label.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING_MESSAGE("1.34", output.c_str(), output.c_str());
|
||||
}
|
||||
|
||||
void test_parseMiningPoolStatsHashRate645GH(void)
|
||||
{
|
||||
std::string hashrate = "645000000000";
|
||||
std::string label;
|
||||
std::string output;
|
||||
|
||||
parseHashrateString(hashrate, label, output, 4);
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING_MESSAGE("GH/S", label.c_str(), label.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING_MESSAGE("645", output.c_str(), output.c_str());
|
||||
}
|
||||
|
||||
void test_parseMiningPoolStatsHashRateEmpty(void)
|
||||
{
|
||||
std::string hashrate = "";
|
||||
std::string label;
|
||||
std::string output;
|
||||
|
||||
parseHashrateString(hashrate, label, output, 4);
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING_MESSAGE("H/S", label.c_str(), label.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING_MESSAGE("0", output.c_str(), output.c_str());
|
||||
}
|
||||
|
||||
void test_parseMiningPoolStatsHashRateZero(void)
|
||||
{
|
||||
std::string hashrate = "0";
|
||||
std::string label;
|
||||
std::string output;
|
||||
|
||||
parseHashrateString(hashrate, label, output, 4);
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING_MESSAGE("H/S", label.c_str(), label.c_str());
|
||||
TEST_ASSERT_EQUAL_STRING_MESSAGE("0", output.c_str(), output.c_str());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// not needed when using generate_test_runner.rb
|
||||
int runUnityTests(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_parseMiningPoolStatsHashRate1dot34TH);
|
||||
RUN_TEST(test_parseMiningPoolStatsHashRate645GH);
|
||||
RUN_TEST(test_parseMiningPoolStatsHashRateZero);
|
||||
RUN_TEST(test_parseMiningPoolStatsHashRateEmpty);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
return runUnityTests();
|
||||
}
|
||||
|
||||
extern "C" void app_main()
|
||||
{
|
||||
runUnityTests();
|
||||
}
|
Loading…
Reference in a new issue