Compare commits

..

1 commit

Author SHA1 Message Date
fa705e45e8 add mqtt module 2024-12-20 12:00:18 +01:00
40 changed files with 1893 additions and 1234 deletions

View file

@ -16,21 +16,21 @@ Biggest differences with v2 are:
New features: New features:
- BitAxe integration - BitAxe integration
- Nostr Zap notifier - Zap notifier
- Multiple mining pool stats integrations - Braiins Pool and Ocean mining stats integration
"Steal focus on new block" means that when a new block is mined, the display will switch to the block height screen if it's not on it already. "Steal focus on new block" means that when a new block is mined, the display will switch to the block height screen if it's not on it already.
See the [docs](https://git.btclock.dev/btclock/docs) repo for more information and building instructions. Most [information](https://github.com/btclock/btclock_v2/wiki) about BTClock v2 is still valid for this version.
**NOTE**: The software assumes that the hardware is run in a controlled private network. ~~The Web UI and the OTA update mechanism are not password protected and accessible to anyone in the network. Also, since the device only fetches numbers through WebSockets it will skip server certificate verification to save resources.~~ Since 3.2.0 the WebUI is password protectable and all certificates are verified. OTA update mechanism is not password-protected. **NOTE**: The software assumes that the hardware is run in a controlled private network. ~~The Web UI and the OTA update mechanism are not password protected and accessible to anyone in the network. Also, since the device only fetches numbers through WebSockets it will skip server certificate verification to save resources.~~ Since 3.2.0 the WebUI is password protectable and all certificates are verified. OTA update mechanism is not password-protected.
## Building ## Building
Use PlatformIO to build it yourself. Make sure you fetch the [WebUI](https://git.btclock.dev/btclock/webui) submodule. Use PlatformIO to build it yourself. Make sure you fetch the [WebUI](https://github.com/btclock/webui) submodule.
## Mining pool stats ## Braiins Pool and Ocean integration
Enable mining pool stats by accessing your btclock's web UI (point a web browser at the device's IP address). 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. Under Settings -> Extra Features: toggle Enable Mining Pool Stats.
@ -41,8 +41,6 @@ The Mining Pool Earnings screen displays:
* Braiins: Today's mining reward thus far * Braiins: Today's mining reward thus far
* Ocean: Your estimated earnings if the pool were to find a block right now * 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 ### Braiins Pool integration
Create an API key based on the steps [here](https://academy.braiins.com/en/braiins-pool/monitoring/#api-configuration). Create an API key based on the steps [here](https://academy.braiins.com/en/braiins-pool/monitoring/#api-configuration).

2
data

@ -1 +1 @@
Subproject commit 924be8fc2eb02fe384a20b53da1a9fa3d8db8a05 Subproject commit fd328d4f05345eaa73cf27d05bb542eaa6915cdb

View file

@ -164,82 +164,3 @@ int64_t getAmountInSatoshis(std::string bolt11) {
return satoshis; return satoshis;
} }
void parseHashrateString(const std::string& hashrate, std::string& label, std::string& output, unsigned int maxCharacters) {
// Handle empty string or "0" cases
if (hashrate.empty() || hashrate == "0") {
label = "H/S";
output = "0";
return;
}
size_t suffixLength = 0;
if (hashrate.length() > 21) {
label = "ZH/S";
suffixLength = 21;
} else if (hashrate.length() > 18) {
label = "EH/S";
suffixLength = 18;
} else if (hashrate.length() > 15) {
label = "PH/S";
suffixLength = 15;
} else if (hashrate.length() > 12) {
label = "TH/S";
suffixLength = 12;
} else if (hashrate.length() > 9) {
label = "GH/S";
suffixLength = 9;
} else if (hashrate.length() > 6) {
label = "MH/S";
suffixLength = 6;
} else if (hashrate.length() > 3) {
label = "KH/S";
suffixLength = 3;
} else {
label = "H/S";
suffixLength = 0;
}
double value = std::stod(hashrate) / std::pow(10, suffixLength);
// Calculate integer part length
int integerPartLength = std::to_string(static_cast<int>(value)).length();
// Calculate remaining space for decimals
int remainingSpace = maxCharacters - integerPartLength;
char buffer[32];
if (remainingSpace <= 0)
{
// No space for decimals, just round to integer
snprintf(buffer, sizeof(buffer), "%.0f", value);
}
else
{
// Space for decimal point and some decimals
snprintf(buffer, sizeof(buffer), "%.*f", remainingSpace - 1, value);
}
// Remove trailing zeros and decimal point if necessary
output = buffer;
if (output.find('.') != std::string::npos)
{
output = output.substr(0, output.find_last_not_of('0') + 1);
if (output.back() == '.')
{
output.pop_back();
}
}
}
int getHashrateMultiplier(char unit) {
if (unit == '0')
return 0;
static const std::unordered_map<char, int> multipliers = {
{'Z', 21}, {'E', 18}, {'P', 15}, {'T', 12},
{'G', 9}, {'M', 6}, {'K', 3}
};
return multipliers.at(unit);
}

View file

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

View file

@ -7,13 +7,13 @@
; ;
; Please visit documentation for the other options and examples ; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html ; https://docs.platformio.org/page/projectconf.html
[platformio] [platformio]
data_dir = data/build_gz data_dir = data/build_gz
default_envs = lolin_s3_mini_213epd, lolin_s3_mini_29epd, btclock_rev_b_213epd, btclock_v8_213epd default_envs = lolin_s3_mini_213epd, lolin_s3_mini_29epd, btclock_rev_b_213epd, btclock_v8_213epd
[env] [env]
[btclock_base] [btclock_base]
platform = espressif32 @ ^6.9.0 platform = espressif32 @ ^6.9.0
framework = arduino, espidf framework = arduino, espidf
@ -21,8 +21,6 @@ monitor_speed = 115200
monitor_filters = esp32_exception_decoder, colorize monitor_filters = esp32_exception_decoder, colorize
board_build.filesystem = littlefs board_build.filesystem = littlefs
extra_scripts = pre:scripts/pre_script.py, post:scripts/extra_script.py extra_scripts = pre:scripts/pre_script.py, post:scripts/extra_script.py
platform_packages =
earlephilhower/tool-mklittlefs-rp2040-earlephilhower
board_build.embed_files = board_build.embed_files =
x509_crt_bundle x509_crt_bundle
build_flags = build_flags =
@ -44,6 +42,7 @@ lib_deps =
https://github.com/dsbaars/GxEPD2#universal_pin https://github.com/dsbaars/GxEPD2#universal_pin
https://github.com/tzapu/WiFiManager.git#v2.0.17 https://github.com/tzapu/WiFiManager.git#v2.0.17
rblb/Nostrduino@1.2.8 rblb/Nostrduino@1.2.8
elims/PsychicMqttClient@^0.2.0
[env:lolin_s3_mini] [env:lolin_s3_mini]
extends = btclock_base extends = btclock_base
@ -61,9 +60,7 @@ build_flags =
-D IS_HW_REV_A -D IS_HW_REV_A
build_unflags = build_unflags =
${btclock_base.build_unflags} ${btclock_base.build_unflags}
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
[env:btclock_rev_b] [env:btclock_rev_b]
extends = btclock_base extends = btclock_base
@ -87,9 +84,6 @@ lib_deps =
claws/BH1750@^1.3.0 claws/BH1750@^1.3.0
build_unflags = build_unflags =
${btclock_base.build_unflags} ${btclock_base.build_unflags}
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
[env:lolin_s3_mini_213epd] [env:lolin_s3_mini_213epd]
extends = env:lolin_s3_mini extends = env:lolin_s3_mini
@ -99,9 +93,7 @@ build_flags =
-D USE_QR -D USE_QR
-D VERSION_EPD_2_13 -D VERSION_EPD_2_13
-D HW_REV=\"REV_A_EPD_2_13\" -D HW_REV=\"REV_A_EPD_2_13\"
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
[env:btclock_rev_b_213epd] [env:btclock_rev_b_213epd]
extends = env:btclock_rev_b extends = env:btclock_rev_b
@ -111,9 +103,6 @@ build_flags =
-D USE_QR -D USE_QR
-D VERSION_EPD_2_13 -D VERSION_EPD_2_13
-D HW_REV=\"REV_B_EPD_2_13\" -D HW_REV=\"REV_B_EPD_2_13\"
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
[env:lolin_s3_mini_29epd] [env:lolin_s3_mini_29epd]
extends = env:lolin_s3_mini extends = env:lolin_s3_mini
@ -123,9 +112,6 @@ build_flags =
-D USE_QR -D USE_QR
-D VERSION_EPD_2_9 -D VERSION_EPD_2_9
-D HW_REV=\"REV_A_EPD_2_9\" -D HW_REV=\"REV_A_EPD_2_9\"
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
[env:btclock_rev_b_29epd] [env:btclock_rev_b_29epd]
extends = env:btclock_rev_b extends = env:btclock_rev_b
@ -135,9 +121,6 @@ build_flags =
-D USE_QR -D USE_QR
-D VERSION_EPD_2_9 -D VERSION_EPD_2_9
-D HW_REV=\"REV_B_EPD_2_9\" -D HW_REV=\"REV_B_EPD_2_9\"
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
[env:btclock_v8] [env:btclock_v8]
extends = btclock_base extends = btclock_base
@ -164,9 +147,6 @@ build_flags =
-D MCP2_A2_PIN=14 -D MCP2_A2_PIN=14
build_unflags = build_unflags =
${btclock_base.build_unflags} ${btclock_base.build_unflags}
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
[env:btclock_v8_213epd] [env:btclock_v8_213epd]
extends = env:btclock_v8 extends = env:btclock_v8
@ -176,9 +156,6 @@ build_flags =
-D USE_QR -D USE_QR
-D VERSION_EPD_2_13 -D VERSION_EPD_2_13
-D HW_REV=\"REV_V8_EPD_2_13\" -D HW_REV=\"REV_V8_EPD_2_13\"
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216
[env:native_test_only] [env:native_test_only]
platform = native platform = native
@ -189,8 +166,3 @@ build_flags =
-D NEOPIXEL_PIN=34 -D NEOPIXEL_PIN=34
-D NEOPIXEL_COUNT=4 -D NEOPIXEL_COUNT=4
-D NUM_SCREENS=7 -D NUM_SCREENS=7
-D UNITY_TEST
-std=gnu++17
platform_packages =
platformio/tool-mklittlefs@^1.203.210628
earlephilhower/tool-mklittlefs-rp2040-earlephilhower@^5.100300.230216

View file

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

File diff suppressed because it is too large Load diff

View file

@ -54,7 +54,7 @@ void taskBitaxeFetch(void *pvParameters)
void setupBitaxeFetchTask() void setupBitaxeFetchTask()
{ {
xTaskCreate(taskBitaxeFetch, "bitaxeFetch", (3 * 1024), NULL, tskIDLE_PRIORITY, xTaskCreate(taskBitaxeFetch, "bitaxeFetch", (6 * 1024), NULL, tskIDLE_PRIORITY,
&bitaxeFetchTaskHandle); &bitaxeFetchTaskHandle);
xTaskNotifyGive(bitaxeFetchTaskHandle); xTaskNotifyGive(bitaxeFetchTaskHandle);

View file

@ -296,26 +296,44 @@ void restartBlockNotify()
} }
int getBlockFetch() { int getBlockFetch()
{
try { try {
String mempoolInstance = preferences.getString("mempoolInstance", DEFAULT_MEMPOOL_INSTANCE); WiFiClientSecure client;
if (preferences.getBool("mempoolSecure", DEFAULT_MEMPOOL_SECURE)) {
client.setCACertBundle(rootca_crt_bundle_start);
}
String mempoolInstance =
preferences.getString("mempoolInstance", DEFAULT_MEMPOOL_INSTANCE);
// Get current block height through regular API
HTTPClient http;
const String protocol = preferences.getBool("mempoolSecure", DEFAULT_MEMPOOL_SECURE) ? "https" : "http"; const String protocol = preferences.getBool("mempoolSecure", DEFAULT_MEMPOOL_SECURE) ? "https" : "http";
String url = protocol + "://" + mempoolInstance + "/api/blocks/tip/height";
HTTPClient* http = HttpHelper::begin(url); if (preferences.getBool("mempoolSecure", DEFAULT_MEMPOOL_SECURE))
Serial.println("Fetching block height from " + url); http.begin(client, protocol + "://" + mempoolInstance + "/api/blocks/tip/height");
int httpCode = http->GET(); else
http.begin(protocol + "://" + mempoolInstance + "/api/blocks/tip/height");
if (httpCode > 0 && httpCode == HTTP_CODE_OK) { Serial.println("Fetching block height from " + protocol + "://" + mempoolInstance + "/api/blocks/tip/height");
String blockHeightStr = http->getString(); int httpCode = http.GET();
HttpHelper::end(http);
if (httpCode > 0 && httpCode == HTTP_CODE_OK)
{
String blockHeightStr = http.getString();
return blockHeightStr.toInt(); return blockHeightStr.toInt();
} } else {
HttpHelper::end(http);
Serial.println("HTTP code" + String(httpCode)); Serial.println("HTTP code" + String(httpCode));
} catch (...) { return 0;
Serial.println(F("An exception occurred while trying to get the latest block"));
} }
}
catch (...) {
Serial.println(F("An exception occured while trying to get the latest block"));
}
return 2203; // B-T-C return 2203; // B-T-C
} }

View file

@ -19,50 +19,39 @@ TickType_t lastDebounceTime = 0;
void buttonTask(void *parameter) { void buttonTask(void *parameter) {
while (1) { while (1) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
std::lock_guard<std::mutex> lock(mcpMutex);
TickType_t currentTime = xTaskGetTickCount(); TickType_t currentTime = xTaskGetTickCount();
if ((currentTime - lastDebounceTime) >= debounceDelay) { if ((currentTime - lastDebounceTime) >= debounceDelay) {
lastDebounceTime = currentTime; lastDebounceTime = currentTime;
std::lock_guard<std::mutex> lock(mcpMutex);
if (!digitalRead(MCP_INT_PIN)) { if (!digitalRead(MCP_INT_PIN)) {
uint16_t intFlags = mcp1.getInterruptFlagRegister(); uint pin = mcp1.getInterruptFlagRegister();
uint16_t intCap = mcp1.getInterruptCaptureRegister();
// Check each button individually switch (pin) {
if (intFlags & BTN_1) handleButton1(); case BTN_1:
if (intFlags & BTN_2) handleButton2();
if (intFlags & BTN_3) handleButton3();
if (intFlags & BTN_4) handleButton4();
}
}
// Clear interrupt state
while (!digitalRead(MCP_INT_PIN)) {
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(); toggleTimerActive();
} break;
case BTN_2:
void handleButton2() {
nextScreen(); nextScreen();
} break;
case BTN_3:
void handleButton3() {
previousScreen(); previousScreen();
} break;
case BTN_4:
void handleButton4() {
showSystemStatusScreen(); showSystemStatusScreen();
break;
}
}
mcp1.getInterruptCaptureRegister();
} else {
}
// Very ugly, but for some reason this is necessary
while (!digitalRead(MCP_INT_PIN)) {
mcp1.getInterruptCaptureRegister();
}
}
} }
void IRAM_ATTR handleButtonInterrupt() { void IRAM_ATTR handleButtonInterrupt() {

View file

@ -8,13 +8,6 @@
extern TaskHandle_t buttonTaskHandle; extern TaskHandle_t buttonTaskHandle;
// Task and setup functions
void buttonTask(void *pvParameters); void buttonTask(void *pvParameters);
void IRAM_ATTR handleButtonInterrupt(); void IRAM_ATTR handleButtonInterrupt();
void setupButtonTask(); void setupButtonTask();
// Individual button handlers
void handleButton1();
void handleButton2();
void handleButton3();
void handleButton4();

View file

@ -100,6 +100,12 @@ void setup()
setupMiningPoolStatsFetchTask(); setupMiningPoolStatsFetchTask();
} }
if (preferences.getBool("mqttEnabled", DEFAULT_MQTT_ENABLED))
{
if (setupMqtt())
setupMqttTask();
}
setupButtonTask(); setupButtonTask();
setupOTA(); setupOTA();
@ -359,6 +365,68 @@ String replaceAmbiguousChars(String input)
return input; return input;
} }
// void addCurrencyMappings(const std::vector<std::string>& currencies)
// {
// for (const auto& currency : currencies)
// {
// int satsPerCurrencyScreen;
// int btcTickerScreen;
// int marketCapScreen;
// // Determine the corresponding screen IDs based on the currency code
// if (currency == "USD")
// {
// satsPerCurrencyScreen = SCREEN_SATS_PER_CURRENCY_USD;
// btcTickerScreen = SCREEN_BTC_TICKER_USD;
// marketCapScreen = SCREEN_MARKET_CAP_USD;
// }
// else if (currency == "EUR")
// {
// satsPerCurrencyScreen = SCREEN_SATS_PER_CURRENCY_EUR;
// btcTickerScreen = SCREEN_BTC_TICKER_EUR;
// marketCapScreen = SCREEN_MARKET_CAP_EUR;
// }
// else if (currency == "GBP")
// {
// satsPerCurrencyScreen = SCREEN_SATS_PER_CURRENCY_GBP;
// btcTickerScreen = SCREEN_BTC_TICKER_GBP;
// marketCapScreen = SCREEN_MARKET_CAP_GBP;
// }
// else if (currency == "JPY")
// {
// satsPerCurrencyScreen = SCREEN_SATS_PER_CURRENCY_JPY;
// btcTickerScreen = SCREEN_BTC_TICKER_JPY;
// marketCapScreen = SCREEN_MARKET_CAP_JPY;
// }
// else if (currency == "AUD")
// {
// satsPerCurrencyScreen = SCREEN_SATS_PER_CURRENCY_AUD;
// btcTickerScreen = SCREEN_BTC_TICKER_AUD;
// marketCapScreen = SCREEN_MARKET_CAP_AUD;
// }
// else if (currency == "CAD")
// {
// satsPerCurrencyScreen = SCREEN_SATS_PER_CURRENCY_CAD;
// btcTickerScreen = SCREEN_BTC_TICKER_CAD;
// marketCapScreen = SCREEN_MARKET_CAP_CAD;
// }
// else
// {
// continue; // Unknown currency, skip it
// }
// // Create the string locally to ensure it persists
// std::string satsPerCurrencyString = "Sats per " + currency;
// std::string btcTickerString = "Ticker " + currency;
// std::string marketCapString = "Market Cap " + currency;
// // Pass the c_str() to the function
// addScreenMapping(satsPerCurrencyScreen, satsPerCurrencyString.c_str());
// addScreenMapping(btcTickerScreen, btcTickerString.c_str());
// addScreenMapping(marketCapScreen, marketCapString.c_str());
// }
// }
void setupWebsocketClients(void *pvParameters) void setupWebsocketClients(void *pvParameters)
{ {
if (preferences.getBool("ownDataSource", DEFAULT_OWN_DATA_SOURCE)) if (preferences.getBool("ownDataSource", DEFAULT_OWN_DATA_SOURCE))
@ -439,19 +507,15 @@ void setupHardware()
Serial.println(F("Error loading WebUI")); Serial.println(F("Error loading WebUI"));
} }
// if (!LittleFS.exists("/qr.txt"))
// { // {
// File f = LittleFS.open("/qr.txt", "w"); // File f = LittleFS.open("/qr.txt", "w");
// if(f) { // if(f) {
// if (f.print("Hello")) {
// Serial.println(F("Written QR to FS"));
// Serial.printf("\nLittleFS free: %zu\n", LittleFS.totalBytes() - LittleFS.usedBytes());
// }
// } else { // } else {
// Serial.println(F("Can't write QR to FS")); // Serial.println(F("Can't write QR to FS"));
// } // }
// f.close();
// } // }
setupLeds(); setupLeds();
@ -466,34 +530,32 @@ void setupHardware()
Wire.begin(I2C_SDA_PIN, I2C_SCK_PIN, 400000); Wire.begin(I2C_SDA_PIN, I2C_SCK_PIN, 400000);
if (!mcp1.begin()) { if (!mcp1.begin())
{
Serial.println(F("Error MCP23017 1")); Serial.println(F("Error MCP23017 1"));
} else {
// while (1)
// ;
}
else
{
pinMode(MCP_INT_PIN, INPUT_PULLUP); pinMode(MCP_INT_PIN, INPUT_PULLUP);
// mcp1.setupInterrupts(false, false, LOW);
mcp1.enableControlRegister(MCP23x17_IOCR_ODR);
// Enable mirrored interrupts (both INTA and INTB pins signal any interrupt) mcp1.mirrorInterrupts(true);
if (!mcp1.mirrorInterrupts(true)) {
Serial.println(F("Error setting up mirrored interrupts"));
}
// Configure all 4 button pins as inputs with pullups and interrupts for (int i = 0; i < 4; i++)
for (int i = 0; i < 4; i++) { {
if (!mcp1.pinMode1(i, INPUT_PULLUP)) { mcp1.pinMode1(i, INPUT_PULLUP);
Serial.printf("Error setting pin %d to input pull up\n", i); mcp1.enableInterrupt(i, LOW);
} }
// Enable interrupt on CHANGE for each pin #ifndef IS_BTCLOCK_V8
if (!mcp1.enableInterrupt(i, CHANGE)) { for (int i = 8; i <= 14; i++)
Serial.printf("Error enabling interrupt for pin %d\n", i); {
mcp1.pinMode1(i, OUTPUT);
} }
} #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 #ifdef IS_HW_REV_B
@ -767,3 +829,19 @@ const char* getWebUiFilename() {
return "littlefs_4MB.bin"; return "littlefs_4MB.bin";
} }
} }
// void loadIcons() {
// size_t ocean_logo_size = 886;
// int iUncompSize = zt.gzip_info((uint8_t *)epd_compress_bitaxe, ocean_logo_size);
// Serial.printf("uncompressed size = %d\n", iUncompSize);
// uint8_t *pUncompressed;
// pUncompressed = (uint8_t *)malloc(iUncompSize+4);
// int rc = zt.gunzip((uint8_t *)epd_compress_bitaxe, ocean_logo_size, pUncompressed);
// if (rc == ZT_SUCCESS) {
// Serial.println("Decode success");
// }
// }

View file

@ -30,9 +30,11 @@
#include "PCA9685.h" #include "PCA9685.h"
#include "BH1750.h" #include "BH1750.h"
#endif #endif
#include "lib/mqtt.hpp"
#define NTP_SERVER "pool.ntp.org" #define NTP_SERVER "pool.ntp.org"
#define DEFAULT_TIME_OFFSET_SECONDS 3600 #define DEFAULT_TIME_OFFSET_SECONDS 3600
#define USER_AGENT "BTClock/3.0"
#ifndef MCP_DEV_ADDR #ifndef MCP_DEV_ADDR
#define MCP_DEV_ADDR 0x20 #define MCP_DEV_ADDR 0x20
#endif #endif

View file

@ -62,6 +62,10 @@
#define DEFAULT_MINING_POOL_NAME "ocean" #define DEFAULT_MINING_POOL_NAME "ocean"
#define DEFAULT_MINING_POOL_USER "38Qkkei3SuF1Eo45BaYmRHUneRD54yyTFy" // Random actual Ocean hasher #define DEFAULT_MINING_POOL_USER "38Qkkei3SuF1Eo45BaYmRHUneRD54yyTFy" // Random actual Ocean hasher
#define DEFAULT_MQTT_ENABLED false
#define DEFAULT_MQTT_URL ""
#define DEFAULT_MQTT_ROOTTOPIC "home/"
#define DEFAULT_ZAP_NOTIFY_ENABLED false #define DEFAULT_ZAP_NOTIFY_ENABLED false
#define DEFAULT_ZAP_NOTIFY_PUBKEY "b5127a08cf33616274800a4387881a9f98e04b9c37116e92de5250498635c422" #define DEFAULT_ZAP_NOTIFY_PUBKEY "b5127a08cf33616274800a4387881a9f98e04b9c37116e92de5250498635c422"
#define DEFAULT_LED_FLASH_ON_ZAP true #define DEFAULT_LED_FLASH_ON_ZAP true
@ -75,5 +79,3 @@
#define DEFAULT_GIT_RELEASE_URL "https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/releases/latest" #define DEFAULT_GIT_RELEASE_URL "https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/releases/latest"
#define DEFAULT_VERTICAL_DESC true #define DEFAULT_VERTICAL_DESC true
#define DEFAULT_MINING_POOL_LOGOS_URL "https://git.btclock.dev/btclock/mining-pool-logos/raw/branch/main"

View file

@ -138,9 +138,6 @@ uint8_t qrcode[800];
#define EPD_TASK_STACK_SIZE 2048 #define EPD_TASK_STACK_SIZE 2048
#endif #endif
#define BUSY_TIMEOUT_COUNT 200
#define BUSY_RETRY_DELAY pdMS_TO_TICKS(10)
void forceFullRefresh() void forceFullRefresh()
{ {
for (uint i = 0; i < NUM_SCREENS; i++) for (uint i = 0; i < NUM_SCREENS; i++)
@ -149,6 +146,25 @@ 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() void setupDisplays()
{ {
std::lock_guard<std::mutex> lockMcp(mcpMutex); std::lock_guard<std::mutex> lockMcp(mcpMutex);
@ -160,7 +176,7 @@ void setupDisplays()
updateQueue = xQueueCreate(UPDATE_QUEUE_SIZE, sizeof(UpdateDisplayTaskItem)); updateQueue = xQueueCreate(UPDATE_QUEUE_SIZE, sizeof(UpdateDisplayTaskItem));
xTaskCreate(prepareDisplayUpdateTask, "PrepareUpd", EPD_TASK_STACK_SIZE*2, NULL, 11, NULL); xTaskCreate(prepareDisplayUpdateTask, "PrepareUpd", EPD_TASK_STACK_SIZE, NULL, 11, NULL);
for (uint i = 0; i < NUM_SCREENS; i++) for (uint i = 0; i < NUM_SCREENS; i++)
{ {
@ -259,10 +275,7 @@ void prepareDisplayUpdateTask(void *pvParameters)
} }
else if (epdContent[epdIndex].startsWith(F("mdi"))) else if (epdContent[epdIndex].startsWith(F("mdi")))
{ {
bool updated = renderIcon(epdIndex, epdContent[epdIndex], updatePartial); renderIcon(epdIndex, epdContent[epdIndex], updatePartial);
if (!updated) {
continue;
}
} }
else if (epdContent[epdIndex].length() > 5) else if (epdContent[epdIndex].length() > 5)
{ {
@ -401,36 +414,89 @@ void splitText(const uint dispNum, const String &top, const String &bottom,
displays[dispNum].print(bottom); displays[dispNum].print(bottom);
} }
// Consolidate common display setup code into a helper function // void showChars(const uint dispNum, const String &chars, bool partial,
void setupDisplay(const uint dispNum, const GFXfont *font) { // const GFXfont *font)
// {
// displays[dispNum].setRotation(2);
// displays[dispNum].setFont(font);
// displays[dispNum].setTextColor(getFgColor());
// int16_t tbx, tby;
// uint16_t tbw, tbh;
// displays[dispNum].getTextBounds(chars, 0, 0, &tbx, &tby, &tbw, &tbh);
// // center the bounding box by transposition of the origin:
// uint16_t x = ((displays[dispNum].width() - tbw) / 2) - tbx;
// uint16_t y = ((displays[dispNum].height() - tbh) / 2) - tby;
// displays[dispNum].fillScreen(getBgColor());
// displays[dispNum].setCursor(x, y);
// displays[dispNum].print(chars);
// // displays[dispNum].setCursor(10, 3);
// // displays[dispNum].setFont(&FONT_SMALL);
// // displays[dispNum].setTextColor(getFgColor());
// // displays[dispNum].println("Y = " + y);
// }
void showDigit(const uint dispNum, char chr, bool partial,
const GFXfont *font)
{
String str(chr);
if (chr == '.')
{
str = "!";
}
displays[dispNum].setRotation(2); displays[dispNum].setRotation(2);
displays[dispNum].setFont(font); displays[dispNum].setFont(font);
displays[dispNum].setTextColor(getFgColor()); displays[dispNum].setTextColor(getFgColor());
displays[dispNum].fillScreen(getBgColor());
}
void showDigit(const uint dispNum, char chr, bool partial, const GFXfont *font) {
String str(chr);
if (chr == '.') {
str = "!";
}
setupDisplay(dispNum, font);
int16_t tbx, tby; int16_t tbx, tby;
uint16_t tbw, tbh; uint16_t tbw, tbh;
displays[dispNum].getTextBounds(str, 0, 0, &tbx, &tby, &tbw, &tbh); displays[dispNum].getTextBounds(str, 0, 0, &tbx, &tby, &tbw, &tbh);
// center the bounding box by transposition of the origin:
uint16_t x = ((displays[dispNum].width() - tbw) / 2) - tbx; uint16_t x = ((displays[dispNum].width() - tbw) / 2) - tbx;
uint16_t y = ((displays[dispNum].height() - tbh) / 2) - tby; uint16_t y = ((displays[dispNum].height() - tbh) / 2) - tby;
// if (str.equals("."))
// {
// // int16_t yAdvance = font->yAdvance;
// // uint8_t charIndex = 46 - font->first;
// // GFXglyph *glyph = (&font->glyph)[charIndex];
// int16_t tbx2, tby2;
// uint16_t tbw2, tbh2;
// displays[dispNum].getTextBounds(".!", 0, 0, &tbx2, &tby2, &tbw2, &tbh2);
// y = ((displays[dispNum].height() - tbh2) / 2) - tby2;
// // Serial.print("yAdvance");
// // Serial.println(yAdvance);
// // if (glyph != nullptr) {
// // Serial.print("height");
// // Serial.println(glyph->height);
// // Serial.print("yOffset");
// // Serial.println(glyph->yOffset);
// // }
// // y = 250-99+18+19;
// }
displays[dispNum].fillScreen(getBgColor());
displays[dispNum].setCursor(x, y); displays[dispNum].setCursor(x, y);
displays[dispNum].print(str); displays[dispNum].print(str);
if (chr == '.') { if (chr == '.')
displays[dispNum].fillRect(x, y, displays[dispNum].width(), {
round(displays[dispNum].height() * 0.9), getBgColor()); 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);
} }
int16_t calculateDescent(const GFXfont *font) { int16_t calculateDescent(const GFXfont *font) {
@ -448,16 +514,21 @@ int16_t calculateDescent(const GFXfont *font) {
void showChars(const uint dispNum, const String &chars, bool partial, void showChars(const uint dispNum, const String &chars, bool partial,
const GFXfont *font) const GFXfont *font)
{ {
setupDisplay(dispNum, font); displays[dispNum].setRotation(2);
displays[dispNum].setFont(font);
displays[dispNum].setTextColor(getFgColor());
int16_t tbx, tby; int16_t tbx, tby;
uint16_t tbw, tbh; uint16_t tbw, tbh;
displays[dispNum].getTextBounds(chars, 0, 0, &tbx, &tby, &tbw, &tbh); displays[dispNum].getTextBounds(chars, 0, 0, &tbx, &tby, &tbw, &tbh);
int16_t descent = calculateDescent(font);
// center the bounding box by transposition of the origin: // center the bounding box by transposition of the origin:
uint16_t x = ((displays[dispNum].width() - tbw) / 2) - tbx; uint16_t x = ((displays[dispNum].width() - tbw) / 2) - tbx;
uint16_t y = ((displays[dispNum].height() - tbh) / 2) - tby; uint16_t y = ((displays[dispNum].height() - tbh) / 2) - tby;
displays[dispNum].fillScreen(getBgColor());
// displays[dispNum].setCursor(x, y);
// displays[dispNum].print(chars);
for (int i = 0; i < chars.length(); i++) { for (int i = 0; i < chars.length(); i++) {
char c = chars[i]; char c = chars[i];
@ -523,7 +594,7 @@ void renderText(const uint dispNum, const String &text, bool partial)
} }
} }
bool renderIcon(const uint dispNum, const String &text, bool partial) void renderIcon(const uint dispNum, const String &text, bool partial)
{ {
displays[dispNum].setRotation(2); displays[dispNum].setRotation(2);
@ -542,24 +613,19 @@ bool renderIcon(const uint dispNum, const String &text, bool partial)
iconIndex = 2; iconIndex = 2;
} }
else if (text.endsWith("bitaxe")) { else if (text.endsWith("bitaxe")) {
width = 88; width = 122;
height = 220; height = 250;
iconIndex = 3; iconIndex = 3;
} }
else if (text.endsWith("miningpool")) { else if (text.endsWith("miningpool")) {
LogoData logo = getMiningPoolLogo(); LogoData logo = getMiningPoolLogo();
if (logo.size == 0) {
Serial.println("No logo found");
return false;
}
int x_offset = (displays[dispNum].width() - logo.width) / 2; int x_offset = (displays[dispNum].width() - logo.width) / 2;
int y_offset = (displays[dispNum].height() - logo.height) / 2; int y_offset = (displays[dispNum].height() - logo.height) / 2;
// Close the file // Close the file
displays[dispNum].drawInvertedBitmap(x_offset,y_offset, logo.data, logo.width, logo.height, getFgColor()); displays[dispNum].drawInvertedBitmap(x_offset,y_offset, logo.data, logo.width, logo.height, getFgColor());
return true; return;
} }
@ -569,12 +635,15 @@ bool renderIcon(const uint dispNum, const String &text, bool partial)
displays[dispNum].drawInvertedBitmap(x_offset,y_offset, epd_icons_allArray[iconIndex], width, height, getFgColor()); 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()); // displays[dispNum].drawInvertedBitmap(0,0, getOceanIcon(), 122, 250, getFgColor());
} }
void renderQr(const uint dispNum, const String &text, bool partial) void renderQr(const uint dispNum, const String &text, bool partial)
{ {
#ifdef USE_QR #ifdef USE_QR
@ -618,12 +687,15 @@ void waitUntilNoneBusy()
while (EPD_BUSY[i].digitalRead()) while (EPD_BUSY[i].digitalRead())
{ {
count++; count++;
vTaskDelay(BUSY_RETRY_DELAY); vTaskDelay(10);
if (count == 200)
if (count == BUSY_TIMEOUT_COUNT) { {
vTaskDelay(pdMS_TO_TICKS(100)); // displays[i].init(0, false);
} else if (count > BUSY_TIMEOUT_COUNT + 5) { vTaskDelay(100);
log_e("Display %d busy timeout", i); }
else if (count > 205)
{
Serial.printf("Busy timeout %d", i);
break; break;
} }
} }

View file

@ -26,6 +26,7 @@ typedef struct {
} UpdateDisplayTaskItem; } UpdateDisplayTaskItem;
void forceFullRefresh(); void forceFullRefresh();
void refreshFromMemory();
void setupDisplays(); void setupDisplays();
void splitText(const uint dispNum, const String &top, const String &bottom, void splitText(const uint dispNum, const String &top, const String &bottom,
@ -44,7 +45,7 @@ int getFgColor();
void setBgColor(int color); void setBgColor(int color);
void setFgColor(int color); void setFgColor(int color);
bool renderIcon(const uint dispNum, const String &text, bool partial); void renderIcon(const uint dispNum, const String &text, bool partial);
void renderText(const uint dispNum, const String &text, bool partial); void renderText(const uint dispNum, const String &text, bool partial);
void renderQr(const uint dispNum, const String &text, bool partial); void renderQr(const uint dispNum, const String &text, bool partial);

View file

@ -10,13 +10,6 @@ std::string BraiinsPool::getApiUrl() const {
PoolStats BraiinsPool::parseResponse(const JsonDocument &doc) const 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>(); std::string unit = doc["btc"]["hash_rate_unit"].as<std::string>();
static const std::unordered_map<std::string, int> multipliers = { static const std::unordered_map<std::string, int> multipliers = {
@ -30,3 +23,10 @@ PoolStats BraiinsPool::parseResponse(const JsonDocument &doc) const
.dailyEarnings = static_cast<int64_t>(doc["btc"]["today_reward"].as<float>() * 100000000)}; .dailyEarnings = static_cast<int64_t>(doc["btc"]["today_reward"].as<float>() * 100000000)};
} }
LogoData BraiinsPool::getLogo() const {
return LogoData{
.data = epd_icons_allArray[5],
.width = 122,
.height = 250
};
}

View file

@ -2,7 +2,6 @@
#include "lib/mining_pool/mining_pool_interface.hpp" #include "lib/mining_pool/mining_pool_interface.hpp"
#include <icons/icons.h> #include <icons/icons.h>
#include <utils.hpp>
class BraiinsPool : public MiningPoolInterface class BraiinsPool : public MiningPoolInterface
{ {
@ -11,23 +10,11 @@ public:
void prepareRequest(HTTPClient &http) const override; void prepareRequest(HTTPClient &http) const override;
std::string getApiUrl() const override; std::string getApiUrl() const override;
PoolStats parseResponse(const JsonDocument &doc) const override; PoolStats parseResponse(const JsonDocument &doc) const override;
LogoData getLogo() const override;
bool supportsDailyEarnings() const override { return true; } bool supportsDailyEarnings() const override { return true; }
bool hasLogo() const override { return true; } bool hasLogo() const override { return true; }
std::string getDisplayLabel() const override { return "BRAIINS/POOL"; } // Fallback if needed std::string getDisplayLabel() const override { return "BRAIINS/POOL"; } // Fallback if needed
std::string getDailyEarningsLabel() const override { return "sats/earned"; } std::string getDailyEarningsLabel() const override { return "sats/earned"; }
std::string getLogoFilename() const override { private:
return "braiins.bin"; static int getHashrateMultiplier(const std::string &unit);
}
std::string getPoolName() const override {
return "braiins";
}
int getLogoWidth() const override {
return 37;
}
int getLogoHeight() const override {
return 230;
}
}; };

View file

@ -4,3 +4,11 @@
std::string GoBrrrPool::getApiUrl() const { std::string GoBrrrPool::getApiUrl() const {
return "https://pool.gobrrr.me/api/client/" + poolUser; return "https://pool.gobrrr.me/api/client/" + poolUser;
} }
LogoData GoBrrrPool::getLogo() const {
return LogoData {
.data = epd_icons_allArray[7],
.width = 122,
.height = 122
};
}

View file

@ -11,20 +11,5 @@ public:
std::string getApiUrl() const override; std::string getApiUrl() const override;
bool hasLogo() const override { return true; } bool hasLogo() const override { return true; }
std::string getDisplayLabel() const override { return "GOBRRR/POOL"; } std::string getDisplayLabel() const override { return "GOBRRR/POOL"; }
LogoData getLogo() const override;
std::string getLogoFilename() const override {
return "gobrrr.bin";
}
std::string getPoolName() const override {
return "gobrrr_pool";
}
int getLogoWidth() const override {
return 122;
}
int getLogoHeight() const override {
return 122;
}
}; };

View file

@ -7,5 +7,4 @@ struct LogoData {
const uint8_t* data; const uint8_t* data;
size_t width; size_t width;
size_t height; size_t height;
size_t size;
}; };

View file

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

View file

@ -4,7 +4,6 @@
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include "pool_stats.hpp" #include "pool_stats.hpp"
#include "logo_data.hpp" #include "logo_data.hpp"
#include "lib/shared.hpp"
class MiningPoolInterface { class MiningPoolInterface {
public: public:
@ -14,21 +13,10 @@ public:
virtual std::string getApiUrl() const = 0; virtual std::string getApiUrl() const = 0;
virtual PoolStats parseResponse(const JsonDocument& doc) const = 0; virtual PoolStats parseResponse(const JsonDocument& doc) const = 0;
virtual bool hasLogo() const = 0; virtual bool hasLogo() const = 0;
virtual LogoData getLogo() const; virtual LogoData getLogo() const = 0;
virtual std::string getDisplayLabel() const = 0; virtual std::string getDisplayLabel() const = 0;
virtual bool supportsDailyEarnings() const = 0; virtual bool supportsDailyEarnings() const = 0;
virtual std::string getDailyEarningsLabel() 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: protected:
std::string poolUser; std::string poolUser;

View file

@ -1,15 +1,42 @@
#include "mining_pool_stats_handler.hpp" #include "mining_pool_stats_handler.hpp"
#include <iostream>
std::array<std::string, NUM_SCREENS> parseMiningPoolStatsHashRate(const std::string& hashrate, const MiningPoolInterface& pool) std::array<std::string, NUM_SCREENS> parseMiningPoolStatsHashRate(std::string text, const MiningPoolInterface& pool)
{ {
std::array<std::string, NUM_SCREENS> ret; std::array<std::string, NUM_SCREENS> ret;
ret.fill(""); // Initialize all elements to empty strings ret.fill(""); // Initialize all elements to empty strings
std::string hashrate;
std::string label; std::string label;
std::string output;
parseHashrateString(hashrate, label, output, 4); if (text.length() > 21) {
// We are massively future-proof!!
label = "ZH/S";
hashrate = text.substr(0, text.length() - 21);
} else if (text.length() > 18) {
label = "EH/S";
hashrate = text.substr(0, text.length() - 18);
} else if (text.length() > 15) {
label = "PH/S";
hashrate = text.substr(0, text.length() - 15);
} else if (text.length() > 12) {
label = "TH/S";
hashrate = text.substr(0, text.length() - 12);
} else if (text.length() > 9) {
label = "GH/S";
hashrate = text.substr(0, text.length() - 9);
} else if (text.length() > 6) {
label = "MH/S";
hashrate = text.substr(0, text.length() - 6);
} else if (text.length() > 3) {
label = "KH/S";
hashrate = text.substr(0, text.length() - 3);
} else {
label = "H/S";
hashrate = text;
}
std::size_t textLength = hashrate.length();
std::size_t textLength = output.length();
// Calculate the position where the digits should start // Calculate the position where the digits should start
// Account for the position of the mining pool logo and the hashrate label // Account for the position of the mining pool logo and the hashrate label
std::size_t startIndex = NUM_SCREENS - 1 - textLength; std::size_t startIndex = NUM_SCREENS - 1 - textLength;
@ -23,7 +50,7 @@ std::array<std::string, NUM_SCREENS> parseMiningPoolStatsHashRate(const std::str
// Place the digits // Place the digits
for (std::size_t i = 0; i < textLength; ++i) for (std::size_t i = 0; i < textLength; ++i)
{ {
ret[startIndex + i] = output.substr(i, 1); ret[startIndex + i] = hashrate.substr(i, 1);
} }
ret[NUM_SCREENS - 1] = label; ret[NUM_SCREENS - 1] = label;

View file

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

View file

@ -15,13 +15,28 @@ PoolStats NoderunnersPool::parseResponse(const JsonDocument& doc) const {
std::string value = hashrateStr.substr(0, hashrateStr.size() - 1); std::string value = hashrateStr.substr(0, hashrateStr.size() - 1);
int multiplier = getHashrateMultiplier(unit); int multiplier = getHashrateMultiplier(unit);
double hashrate = std::stod(value) * std::pow(10, multiplier);
char buffer[32];
snprintf(buffer, sizeof(buffer), "%.0f", hashrate);
return PoolStats{ return PoolStats{
.hashrate = buffer, .hashrate = value + std::string(multiplier, '0'),
.dailyEarnings = std::nullopt .dailyEarnings = std::nullopt
}; };
} }
LogoData NoderunnersPool::getLogo() const {
return LogoData {
.data = epd_icons_allArray[6],
.width = 122,
.height = 122
};
}
int NoderunnersPool::getHashrateMultiplier(char unit) {
if (unit == '0')
return 0;
static const std::unordered_map<char, int> multipliers = {
{'Z', 21}, {'E', 18}, {'P', 15}, {'T', 12},
{'G', 9}, {'M', 6}, {'K', 3}
};
return multipliers.at(unit);
}

View file

@ -1,8 +1,8 @@
#pragma once #pragma once
#include "lib/mining_pool/mining_pool_interface.hpp" #include "lib/mining_pool/mining_pool_interface.hpp"
#include <icons/icons.h> #include <icons/icons.h>
#include <utils.hpp>
class NoderunnersPool : public MiningPoolInterface { class NoderunnersPool : public MiningPoolInterface {
public: public:
@ -11,23 +11,12 @@ public:
void prepareRequest(HTTPClient& http) const override; void prepareRequest(HTTPClient& http) const override;
std::string getApiUrl() const override; std::string getApiUrl() const override;
PoolStats parseResponse(const JsonDocument& doc) const override; PoolStats parseResponse(const JsonDocument& doc) const override;
LogoData getLogo() const override;
bool supportsDailyEarnings() const override { return false; } bool supportsDailyEarnings() const override { return false; }
std::string getDailyEarningsLabel() const override { return ""; } std::string getDailyEarningsLabel() const override { return ""; }
bool hasLogo() const override { return true; } bool hasLogo() const override { return true; }
std::string getDisplayLabel() const override { return "NODE/RUNNERS"; } // Fallback if needed std::string getDisplayLabel() const override { return "NODE/RUNNERS"; } // Fallback if needed
std::string getLogoFilename() const override {
return "noderunners.bin";
}
std::string getPoolName() const override { protected:
return "noderunners"; static int getHashrateMultiplier(char unit);
}
int getLogoWidth() const override {
return 122;
}
int getLogoHeight() const override {
return 122;
}
}; };

View file

@ -16,3 +16,11 @@ PoolStats OceanPool::parseResponse(const JsonDocument& doc) const {
) )
}; };
} }
LogoData OceanPool::getLogo() const {
return LogoData{
.data = epd_icons_allArray[4],
.width = 122,
.height = 122
};
}

View file

@ -9,23 +9,10 @@ public:
void prepareRequest(HTTPClient& http) const override; void prepareRequest(HTTPClient& http) const override;
std::string getApiUrl() const override; std::string getApiUrl() const override;
PoolStats parseResponse(const JsonDocument& doc) const override; PoolStats parseResponse(const JsonDocument& doc) const override;
LogoData getLogo() const override;
bool hasLogo() const override { return true; } bool hasLogo() const override { return true; }
std::string getDisplayLabel() const override { return "OCEAN/POOL"; } // Fallback if needed std::string getDisplayLabel() const override { return "OCEAN/POOL"; } // Fallback if needed
bool supportsDailyEarnings() const override { return true; } bool supportsDailyEarnings() const override { return true; }
std::string getDailyEarningsLabel() const override { return "sats/block"; } std::string getDailyEarningsLabel() const override { return "sats/block"; }
std::string getLogoFilename() const override {
return "ocean.bin";
}
std::string getPoolName() const override {
return "ocean";
}
int getLogoWidth() const override {
return 122;
}
int getLogoHeight() const override {
return 122;
}
}; };

View file

@ -6,7 +6,6 @@ const char* PoolFactory::MINING_POOL_NAME_BRAIINS = "braiins";
const char* PoolFactory::MINING_POOL_NAME_SATOSHI_RADIO = "satoshi_radio"; 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_PUBLIC_POOL = "public_pool";
const char* PoolFactory::MINING_POOL_NAME_GOBRRR_POOL = "gobrrr_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) { std::unique_ptr<MiningPoolInterface> PoolFactory::createPool(const std::string& poolName) {
static const std::unordered_map<std::string, std::function<std::unique_ptr<MiningPoolInterface>()>> poolFactories = { static const std::unordered_map<std::string, std::function<std::unique_ptr<MiningPoolInterface>()>> poolFactories = {
@ -24,111 +23,3 @@ std::unique_ptr<MiningPoolInterface> PoolFactory::createPool(const std::string&
} }
return it->second(); return it->second();
} }
void PoolFactory::downloadPoolLogo(const std::string& poolName, const MiningPoolInterface* poolInterface)
{
const int MAX_RETRIES = 5;
const int RETRY_DELAY_MS = 1000; // 1 second between retries
if (!poolInterface || !poolInterface->hasLogo()) {
Serial.println(F("No pool interface or logo"));
return;
}
// Ensure logos directory exists
if (!LittleFS.exists(LOGOS_DIR)) {
LittleFS.mkdir(LOGOS_DIR);
}
String logoPath = String(LOGOS_DIR) + "/" + String(poolName.c_str()) + "_logo.bin";
// Only download if the logo doesn't exist
if (!LittleFS.exists(logoPath)) {
// Clean up logos directory first
File root = LittleFS.open(LOGOS_DIR, "r");
if (root) {
File file = root.openNextFile();
while (file) {
String path = file.path();
file.close();
LittleFS.remove(path);
file = root.openNextFile();
}
root.close();
}
// Download new logo with retries
std::string logoUrl = poolInterface->getLogoUrl();
if (!logoUrl.empty()) {
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
Serial.printf("Downloading pool logo (attempt %d of %d)...\n", attempt, MAX_RETRIES);
HTTPClient http;
http.setUserAgent(USER_AGENT);
http.begin(logoUrl.c_str());
int httpCode = http.GET();
if (httpCode == 200) {
File file = LittleFS.open(logoPath, "w");
if (file) {
http.writeToStream(&file);
file.close();
Serial.println(F("Logo downloaded successfully"));
http.end();
return; // Success!
}
}
http.end();
if (attempt < MAX_RETRIES) {
Serial.printf("Failed to download logo, HTTP code: %d. Retrying...\n", httpCode);
vTaskDelay(pdMS_TO_TICKS(RETRY_DELAY_MS));
} else {
Serial.printf("Failed to download logo after %d attempts\n", MAX_RETRIES);
}
}
}
} else {
Serial.println(F("Logo already exists"));
}
}
LogoData PoolFactory::loadLogoFromFS(const std::string& poolName, const MiningPoolInterface* poolInterface)
{
// Initialize with dimensions from the pool interface
LogoData logo = {nullptr,
0,
0,
0};
String logoPath = String(LOGOS_DIR) + "/" + String(poolName.c_str()) + "_logo.bin";
if (!LittleFS.exists(logoPath)) {
return logo;
}
// Only set dimensions if file exists
logo.width = static_cast<size_t>(poolInterface->getLogoWidth());
logo.height = static_cast<size_t>(poolInterface->getLogoHeight());
File file = LittleFS.open(logoPath, "r");
if (!file) {
return logo;
}
size_t size = file.size();
uint8_t* buffer = new uint8_t[size];
if (file.read(buffer, size) == size) {
logo.data = buffer;
logo.size = size;
} else {
delete[] buffer;
logo.data = nullptr;
logo.size = 0;
}
file.close();
return logo;
}

View file

@ -2,22 +2,15 @@
#include "mining_pool_interface.hpp" #include "mining_pool_interface.hpp"
#include <memory> #include <memory>
#include <string> #include <string>
#include "lib/shared.hpp"
#include "lib/config.hpp"
#include "noderunners/noderunners_pool.hpp" #include "noderunners/noderunners_pool.hpp"
#include "braiins/brains_pool.hpp" #include "braiins/brains_pool.hpp"
#include "ocean/ocean_pool.hpp" #include "ocean/ocean_pool.hpp"
#include "satoshi_radio/satoshi_radio_pool.hpp" #include "satoshi_radio/satoshi_radio_pool.hpp"
#include "public_pool/public_pool.hpp" #include "public_pool/public_pool.hpp"
#include "gobrrr_pool/gobrrr_pool.hpp" #include "gobrrr_pool/gobrrr_pool.hpp"
#include <LittleFS.h>
#include <HTTPClient.h>
class PoolFactory { class PoolFactory {
public: public:
static const char* getLogosDir() { return LOGOS_DIR; }
static std::unique_ptr<MiningPoolInterface> createPool(const std::string& poolName); static std::unique_ptr<MiningPoolInterface> createPool(const std::string& poolName);
static std::vector<std::string> getAvailablePools() { static std::vector<std::string> getAvailablePools() {
return { return {
@ -41,10 +34,6 @@ class PoolFactory {
} }
return result; return result;
} }
static void downloadPoolLogo(const std::string& poolName, const MiningPoolInterface* poolInterface);
static LogoData loadLogoFromFS(const std::string& poolName, const MiningPoolInterface* poolInterface);
private: private:
static const char* MINING_POOL_NAME_OCEAN; static const char* MINING_POOL_NAME_OCEAN;
static const char* MINING_POOL_NAME_NODERUNNERS; static const char* MINING_POOL_NAME_NODERUNNERS;
@ -52,5 +41,4 @@ class PoolFactory {
static const char* MINING_POOL_NAME_SATOSHI_RADIO; static const char* MINING_POOL_NAME_SATOSHI_RADIO;
static const char* MINING_POOL_NAME_PUBLIC_POOL; static const char* MINING_POOL_NAME_PUBLIC_POOL;
static const char* MINING_POOL_NAME_GOBRRR_POOL; static const char* MINING_POOL_NAME_GOBRRR_POOL;
static const char* LOGOS_DIR;
}; };

View file

@ -18,12 +18,6 @@ int getMiningPoolStatsDailyEarnings()
void taskMiningPoolStatsFetch(void *pvParameters) 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 (;;) for (;;)
{ {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
@ -31,7 +25,15 @@ void taskMiningPoolStatsFetch(void *pvParameters)
HTTPClient http; HTTPClient http;
http.setUserAgent(USER_AGENT); http.setUserAgent(USER_AGENT);
std::string poolName = preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME).c_str();
std::string poolUser = preferences.getString("miningPoolUser", DEFAULT_MINING_POOL_USER).c_str();
auto poolInterface = PoolFactory::createPool(poolName);
if (!poolInterface)
{
Serial.println("Unknown mining pool: \"" + String(poolName.c_str()) + "\"");
continue;
}
poolInterface->setPoolUser(poolUser); poolInterface->setPoolUser(poolUser);
std::string apiUrl = poolInterface->getApiUrl(); std::string apiUrl = poolInterface->getApiUrl();
@ -45,7 +47,6 @@ void taskMiningPoolStatsFetch(void *pvParameters)
deserializeJson(doc, payload); deserializeJson(doc, payload);
PoolStats stats = poolInterface->parseResponse(doc); PoolStats stats = poolInterface->parseResponse(doc);
miningPoolStatsHashrate = stats.hashrate; miningPoolStatsHashrate = stats.hashrate;
if (stats.dailyEarnings) if (stats.dailyEarnings)
@ -72,36 +73,12 @@ void taskMiningPoolStatsFetch(void *pvParameters)
} }
} }
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() void setupMiningPoolStatsFetchTask()
{ {
xTaskCreate(downloadMiningPoolLogoTask, xTaskCreate(taskMiningPoolStatsFetch, "miningPoolStatsFetch", (6 * 1024), NULL, tskIDLE_PRIORITY,
"logoDownload",
(6 * 1024),
NULL,
tskIDLE_PRIORITY,
NULL);
xTaskCreate(taskMiningPoolStatsFetch,
"miningPoolStatsFetch",
(6 * 1024),
NULL,
tskIDLE_PRIORITY,
&miningPoolStatsFetchTaskHandle); &miningPoolStatsFetchTaskHandle);
xTaskNotifyGive(miningPoolStatsFetchTaskHandle);
} }
std::unique_ptr<MiningPoolInterface>& getMiningPool() std::unique_ptr<MiningPoolInterface>& getMiningPool()
@ -118,6 +95,5 @@ std::unique_ptr<MiningPoolInterface>& getMiningPool()
LogoData getMiningPoolLogo() LogoData getMiningPoolLogo()
{ {
LogoData logo = getMiningPool()->getLogo(); return getMiningPool()->getLogo();
return logo;
} }

131
src/lib/mqtt.cpp Normal file
View file

@ -0,0 +1,131 @@
#include "mqtt.hpp"
TaskHandle_t mqttTaskHandle = NULL;
// WiFiClient wifiClient;
//PubSubClient client(wifiClient);
PsychicMqttClient mqttClient;
// avoid circular deps, just forward declare externs used here.
#ifdef HAS_FRONTLIGHT
bool hasLightLevel();
float getLightLevel();
#endif
String getMyHostname();
void onMqttCallback(char* topic, byte* payload, unsigned int length)
{
Serial.println("MQTT message arrived");
}
const String getDeviceTopic()
{
const String hostname = getMyHostname();
const String rootTopic = preferences.getString("mqttRootTopic", DEFAULT_MQTT_ROOTTOPIC);
String fullTopic = rootTopic;
if (!rootTopic.endsWith("/") && rootTopic != "")
{
fullTopic += "/";
}
fullTopic += hostname + "/";
return String(fullTopic);
}
// boolean connectMqtt()
// {
// const String willTopic = getDeviceTopic() + "status";
// mqttClient.connect()
// boolean result = client.connect("btclockClient", willTopic.c_str(), 0, true, "offline");
// if (!result)
// {
// Serial.println("[MQTT] could not connect");
// return result;
// }
// publish("status", "online", true);
// return result;
// }
boolean setupMqtt()
{
const String url = preferences.getString("mqttUrl", DEFAULT_MQTT_URL);
if (url == "")
{
Serial.println("[MQTT] url not set");
return false;
}
Serial.print("[MQTT] url: ");
Serial.println(url.c_str());
mqttClient.setClientId(getMyHostname().c_str());
mqttClient.setServer(url.c_str());
// if (url.startsWith("mqtts:") || url.startsWith("wss:"))
// mqttClient.attachArduinoCACertBundle();
mqttClient.connect();
const String willTopic = getDeviceTopic() + "status";
mqttClient.setWill(willTopic.c_str(), 0, 1, "offline");
publish("status", "online", true);
return true;
}
void mqttTask(void *pvParameters)
{
int t=0;
while (1)
{
// client.loop();
vTaskDelay(pdMS_TO_TICKS(1000));
if (t++ % 10 == 0)
{
#ifdef HAS_FRONTLIGHT
if (hasLightLevel())
{
std::string lux_s = std::to_string(static_cast<int>(std::round(getLightLevel())));
publish("sensors/lux", lux_s.c_str());
}
#endif
if (WiFi.isConnected())
{
int8_t rssi = WiFi.RSSI();
std::string rssi_s = std::to_string(static_cast<int>(rssi));
publish("wifi/rssi", rssi_s.c_str());
publish("wifi/bssid", WiFi.BSSIDstr().c_str());
}
std::string heap_free_s = std::to_string(static_cast<int>(ESP.getFreeHeap()));
publish("mem/heap_free", heap_free_s.c_str());
std::string heap_size_s = std::to_string(static_cast<int>(ESP.getHeapSize()));
publish("mem/heap_size", heap_size_s.c_str());
}
}
}
void setupMqttTask()
{
xTaskCreate(mqttTask, "mqttTask", 8192, NULL, 10, &mqttTaskHandle);
}
void publishForDevice(const char *topic, const char *payload)
{
publishForDevice(topic, payload, false);
}
void publishForDevice(const char *topic, const char *payload, boolean retain)
{
const String fullTopic = getDeviceTopic() + topic;
publish(fullTopic.c_str(), payload, retain);
}
void publish(const char *topic, const char *payload)
{
publish(topic, payload, false);
}
void publish(const char *topic, const char *payload, boolean retain)
{
if (!mqttClient.publish(topic, 0, retain, payload))
{
Serial.println("[MQTT] could not write");
}
}

13
src/lib/mqtt.hpp Normal file
View file

@ -0,0 +1,13 @@
#pragma once
#include <Arduino.h>
#include <WiFiClientSecure.h>
#include <PsychicMqttClient.h>
#include "lib/shared.hpp"
boolean setupMqtt();
void setupMqttTask();
void publishForDevice(const char *topic, const char *payload);
void publishForDevice(const char *topic, const char *payload, boolean retain);
void publish(const char *topic, const char *payload);
void publish(const char *topic, const char *payload, boolean retain);

View file

@ -152,31 +152,3 @@ String calculateSHA256(WiFiClient *stream, size_t contentLength) {
// pUncompressed = (uint8_t *)malloc(iUncompSize+4); // pUncompressed = (uint8_t *)malloc(iUncompSize+4);
// zt.gunzip((uint8_t *)ocean_logo_comp, ocean_logo_size, pUncompressed); // zt.gunzip((uint8_t *)ocean_logo_comp, ocean_logo_size, pUncompressed);
// } // }
WiFiClientSecure HttpHelper::secureClient;
WiFiClient HttpHelper::insecureClient;
bool HttpHelper::certBundleSet = false;
HTTPClient* HttpHelper::begin(const String& url) {
HTTPClient* http = new HTTPClient();
if (url.startsWith("https://")) {
if (!certBundleSet) {
secureClient.setCACertBundle(rootca_crt_bundle_start);
certBundleSet = true;
}
http->begin(secureClient, url);
} else {
http->begin(insecureClient, url);
}
http->setUserAgent(USER_AGENT);
return http;
}
void HttpHelper::end(HTTPClient* http) {
if (http) {
http->end();
delete http;
}
}

View file

@ -12,15 +12,12 @@
#include <mbedtls/md.h> #include <mbedtls/md.h>
#include "esp_crt_bundle.h" #include "esp_crt_bundle.h"
#include <Update.h> #include <Update.h>
#include <HTTPClient.h>
#include <mutex> #include <mutex>
#include <utils.hpp> #include <utils.hpp>
#include "defaults.hpp" #include "defaults.hpp"
#define USER_AGENT "BTClock/3.0"
extern MCP23017 mcp1; extern MCP23017 mcp1;
#ifdef IS_BTCLOCK_V8 #ifdef IS_BTCLOCK_V8
extern MCP23017 mcp2; extern MCP23017 mcp2;
@ -98,14 +95,3 @@ namespace ArduinoJson {
} }
}; };
} }
class HttpHelper {
public:
static HTTPClient* begin(const String& url);
static void end(HTTPClient* http);
private:
static WiFiClientSecure secureClient;
static bool certBundleSet;
static WiFiClient insecureClient;
};

View file

@ -1,37 +1,26 @@
#include "webserver.hpp" #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); AsyncWebServer server(80);
AsyncEventSource events("/events"); AsyncEventSource events("/events");
TaskHandle_t eventSourceTaskHandle; TaskHandle_t eventSourceTaskHandle;
#define HTTP_OK 200
#define HTTP_BAD_REQUEST 400
void setupWebserver() void setupWebserver()
{ {
events.onConnect([](AsyncEventSourceClient *client) events.onConnect([](AsyncEventSourceClient *client)
{ client->send("welcome", NULL, millis(), 1000); }); { client->send("welcome", NULL, millis(), 1000); });
server.addHandler(&events); 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"); AsyncStaticWebHandler &staticHandler = server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html");
server.rewrite("/convert", "/"); server.rewrite("/convert", "/");
server.rewrite("/api", "/"); server.rewrite("/api", "/");
@ -42,6 +31,7 @@ void setupWebserver()
preferences.getString("httpAuthPass", DEFAULT_HTTP_AUTH_PASSWORD)); preferences.getString("httpAuthPass", DEFAULT_HTTP_AUTH_PASSWORD));
} }
// server.on("/", HTTP_GET, onIndex); // server.on("/", HTTP_GET, onIndex);
server.on("/api/status", HTTP_GET, onApiStatus); server.on("/api/status", HTTP_GET, onApiStatus);
server.on("/api/system_status", HTTP_GET, onApiSystemStatus); server.on("/api/system_status", HTTP_GET, onApiSystemStatus);
server.on("/api/wifi_set_tx_power", HTTP_GET, onApiSetWifiTxPower); server.on("/api/wifi_set_tx_power", HTTP_GET, onApiSetWifiTxPower);
@ -61,8 +51,8 @@ void setupWebserver()
server.on("/api/show/text", HTTP_GET, onApiShowText); server.on("/api/show/text", HTTP_GET, onApiShowText);
server.on("/api/screen/next", HTTP_GET, onApiScreenControl); server.on("/api/screen/next", HTTP_GET, onApiScreenNext);
server.on("/api/screen/previous", HTTP_GET, onApiScreenControl); server.on("/api/screen/previous", HTTP_GET, onApiScreenPrevious);
AsyncCallbackJsonWebHandler *settingsPatchHandler = AsyncCallbackJsonWebHandler *settingsPatchHandler =
new AsyncCallbackJsonWebHandler("/api/json/settings", onApiSettingsPatch); new AsyncCallbackJsonWebHandler("/api/json/settings", onApiSettingsPatch);
@ -222,6 +212,36 @@ void asyncFileUpdateHandler(AsyncWebServerRequest *request, String filename, siz
void asyncFirmwareUpdateHandler(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) 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); 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() JsonDocument getStatusObject()
@ -275,6 +295,7 @@ JsonDocument getLedStatusObject()
for (uint i = 0; i < pixels.numPixels(); i++) for (uint i = 0; i < pixels.numPixels(); i++)
{ {
uint32_t pixColor = pixels.getPixelColor(pixels.numPixels() - i - 1); uint32_t pixColor = pixels.getPixelColor(pixels.numPixels() - i - 1);
uint alpha = (pixColor >> 24) & 0xFF;
uint red = (pixColor >> 16) & 0xFF; uint red = (pixColor >> 16) & 0xFF;
uint green = (pixColor >> 8) & 0xFF; uint green = (pixColor >> 8) & 0xFF;
uint blue = pixColor & 0xFF; uint blue = pixColor & 0xFF;
@ -292,26 +313,25 @@ JsonDocument getLedStatusObject()
return root; return root;
} }
void eventSourceUpdate() { void eventSourceUpdate()
if (!events.count()) return; {
if (!events.count())
return;
JsonDocument root = getStatusObject();
JsonArray data = root["data"].to<JsonArray>();
JsonDocument doc = getStatusObject(); root["leds"] = getLedStatusObject()["data"];
doc["leds"] = getLedStatusObject()["data"];
// Get current EPD content directly as array String epdContent[NUM_SCREENS];
std::array<String, NUM_SCREENS> epdContent = getCurrentEpdContent(); std::array<String, NUM_SCREENS> retEpdContent = getCurrentEpdContent();
std::copy(std::begin(retEpdContent), std::end(retEpdContent), epdContent);
// Add EPD content arrays copyArray(epdContent, data);
JsonArray data = doc["data"].to<JsonArray>();
// Copy array elements directly String bufString;
for(const auto& content : epdContent) { serializeJson(root, bufString);
data.add(content);
}
String buffer; events.send(bufString.c_str(), "status");
serializeJson(doc, buffer);
events.send(buffer.c_str(), "status");
} }
/** /**
@ -321,22 +341,21 @@ void eventSourceUpdate() {
void onApiStatus(AsyncWebServerRequest *request) void onApiStatus(AsyncWebServerRequest *request)
{ {
AsyncResponseStream *response = AsyncResponseStream *response =
request->beginResponseStream(JSON_CONTENT); request->beginResponseStream("application/json");
JsonDocument root = getStatusObject(); 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 data = root["data"].to<JsonArray>();
JsonArray rendered = root["rendered"].to<JsonArray>();
// Copy array elements directly String epdContent[NUM_SCREENS];
for(const auto& content : epdContent) {
data.add(content);
}
root["leds"] = getLedStatusObject()["data"]; 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); serializeJson(root, *response);
request->send(response); request->send(response);
@ -349,7 +368,7 @@ void onApiStatus(AsyncWebServerRequest *request)
void onApiActionPause(AsyncWebServerRequest *request) void onApiActionPause(AsyncWebServerRequest *request)
{ {
setTimerActive(false); setTimerActive(false);
request->send(HTTP_OK); request->send(200);
}; };
/** /**
@ -359,7 +378,7 @@ void onApiActionPause(AsyncWebServerRequest *request)
void onApiActionTimerRestart(AsyncWebServerRequest *request) void onApiActionTimerRestart(AsyncWebServerRequest *request)
{ {
setTimerActive(true); setTimerActive(true);
request->send(HTTP_OK); request->send(200);
} }
/** /**
@ -373,7 +392,7 @@ void onApiFullRefresh(AsyncWebServerRequest *request)
setEpdContent(newEpdContent, true); setEpdContent(newEpdContent, true);
request->send(HTTP_OK); request->send(200);
} }
/** /**
@ -388,21 +407,28 @@ void onApiShowScreen(AsyncWebServerRequest *request)
uint currentScreen = p->value().toInt(); uint currentScreen = p->value().toInt();
setCurrentScreen(currentScreen); setCurrentScreen(currentScreen);
} }
request->send(HTTP_OK); request->send(200);
} }
/** /**
* @Api * @Api
* @Path("/api/screen/next") * @Path("/api/screen/next")
*/ */
void onApiScreenControl(AsyncWebServerRequest *request) { void onApiScreenNext(AsyncWebServerRequest *request)
const String& action = request->url(); {
if (action.endsWith("/next")) {
nextScreen(); nextScreen();
} else if (action.endsWith("/previous")) { request->send(200);
previousScreen();
} }
request->send(HTTP_OK);
/**
* @Api
* @Path("/api/screen/previous")
*/
void onApiScreenPrevious(AsyncWebServerRequest *request)
{
previousScreen();
request->send(200);
} }
void onApiShowText(AsyncWebServerRequest *request) void onApiShowText(AsyncWebServerRequest *request)
@ -422,7 +448,7 @@ void onApiShowText(AsyncWebServerRequest *request)
setEpdContent(textEpdContent); setEpdContent(textEpdContent);
} }
setCurrentScreen(SCREEN_CUSTOM); setCurrentScreen(SCREEN_CUSTOM);
request->send(HTTP_OK); request->send(200);
} }
void onApiShowTextAdvanced(AsyncWebServerRequest *request, JsonVariant &json) void onApiShowTextAdvanced(AsyncWebServerRequest *request, JsonVariant &json)
@ -440,7 +466,7 @@ void onApiShowTextAdvanced(AsyncWebServerRequest *request, JsonVariant &json)
setEpdContent(epdContent); setEpdContent(epdContent);
setCurrentScreen(SCREEN_CUSTOM); setCurrentScreen(SCREEN_CUSTOM);
request->send(HTTP_OK); request->send(200);
} }
void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json) void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json)
@ -458,49 +484,52 @@ void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json)
bool settingsChanged = true; bool settingsChanged = true;
if (settings["fgColor"].is<String>()) if (settings.containsKey("fgColor"))
{ {
String fgColor = settings["fgColor"].as<String>(); String fgColor = settings["fgColor"].as<String>();
uint32_t color = strtol(fgColor.c_str(), NULL, 16); preferences.putUInt("fgColor", strtol(fgColor.c_str(), NULL, 16));
preferences.putUInt("fgColor", color); setFgColor(int(strtol(fgColor.c_str(), NULL, 16)));
setFgColor(color);
Serial.print(F("Setting foreground color to ")); Serial.print(F("Setting foreground color to "));
Serial.println(color); Serial.println(strtol(fgColor.c_str(), NULL, 16));
settingsChanged = true; settingsChanged = true;
} }
if (settings["bgColor"].is<String>()) if (settings.containsKey("bgColor"))
{ {
String bgColor = settings["bgColor"].as<String>(); String bgColor = settings["bgColor"].as<String>();
uint32_t color = strtol(bgColor.c_str(), NULL, 16); preferences.putUInt("bgColor", strtol(bgColor.c_str(), NULL, 16));
preferences.putUInt("bgColor", color); setBgColor(int(strtol(bgColor.c_str(), NULL, 16)));
setBgColor(color);
Serial.print(F("Setting background color to ")); Serial.print(F("Setting background color to "));
Serial.println(bgColor.c_str()); Serial.println(bgColor.c_str());
settingsChanged = true; settingsChanged = true;
} }
if (settings["timePerScreen"].is<uint>()) if (settings.containsKey("timePerScreen"))
{ {
preferences.putUInt("timerSeconds", preferences.putUInt("timerSeconds",
settings["timePerScreen"].as<uint>() * 60); settings["timePerScreen"].as<uint>() * 60);
} }
String strSettings[] = {"hostnamePrefix", "mempoolInstance", "nostrPubKey",
"nostrRelay", "bitaxeHostname", "nostrZapPubkey",
"httpAuthUser", "httpAuthPass", "gitReleaseUrl",
"mqttUrl", "mqttRootTopic"};
for (String setting : strSettings) for (String setting : strSettings)
{ {
if (settings[setting].is<String>()) if (settings.containsKey(setting))
{ {
preferences.putString(setting.c_str(), settings[setting].as<String>()); preferences.putString(setting.c_str(), settings[setting].as<String>());
Serial.printf("Setting %s to %s\r\n", setting.c_str(), Serial.printf("Setting %s to %s\r\n", setting.c_str(),
settings[setting].as<String>().c_str()); settings[setting].as<String>());
} }
} }
String uintSettings[] = {"minSecPriceUpd", "fullRefreshMin", "ledBrightness", "flMaxBrightness", "flEffectDelay", "luxLightToggle", "wpTimeout", "srcV2Currency"};
for (String setting : uintSettings) for (String setting : uintSettings)
{ {
if (settings[setting].is<uint>()) if (settings.containsKey(setting))
{ {
preferences.putUInt(setting.c_str(), settings[setting].as<uint>()); preferences.putUInt(setting.c_str(), settings[setting].as<uint>());
Serial.printf("Setting %s to %d\r\n", setting.c_str(), Serial.printf("Setting %s to %d\r\n", setting.c_str(),
@ -508,7 +537,7 @@ void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json)
} }
} }
if (settings["tzOffset"].is<int>()) if (settings.containsKey("tzOffset"))
{ {
int gmtOffset = settings["tzOffset"].as<int>() * 60; int gmtOffset = settings["tzOffset"].as<int>() * 60;
size_t written = preferences.putInt("gmtOffset", gmtOffset); size_t written = preferences.putInt("gmtOffset", gmtOffset);
@ -516,17 +545,27 @@ void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json)
gmtOffset, settings["tzOffset"].as<int>(), written); 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",
"miningPoolStats", "verticalDesc", "mqttEnabled",
"nostrZapNotify", "stagingSource", "httpAuthEnabled"};
for (String setting : boolSettings) for (String setting : boolSettings)
{ {
if (settings[setting].is<bool>()) if (settings.containsKey(setting))
{ {
preferences.putBool(setting.c_str(), settings[setting].as<bool>()); preferences.putBool(setting.c_str(), settings[setting].as<boolean>());
Serial.printf("Setting %s to %d\r\n", setting.c_str(), Serial.printf("Setting %s to %d\r\n", setting.c_str(),
settings[setting].as<bool>()); settings[setting].as<boolean>());
} }
} }
if (settings["screens"].is<JsonArray>()) if (settings.containsKey("screens"))
{ {
for (JsonVariant screen : settings["screens"].as<JsonArray>()) for (JsonVariant screen : settings["screens"].as<JsonArray>())
{ {
@ -534,12 +573,12 @@ void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json)
uint id = s["id"].as<uint>(); uint id = s["id"].as<uint>();
String key = "screen[" + String(id) + "]"; String key = "screen[" + String(id) + "]";
String prefKey = "screen" + String(id) + "Visible"; String prefKey = "screen" + String(id) + "Visible";
bool visible = s["enabled"].as<bool>(); bool visible = s["enabled"].as<boolean>();
preferences.putBool(prefKey.c_str(), visible); preferences.putBool(prefKey.c_str(), visible);
} }
} }
if (settings["actCurrencies"].is<JsonArray>()) if (settings.containsKey("actCurrencies"))
{ {
String actCurrencies; String actCurrencies;
@ -553,10 +592,10 @@ void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json)
} }
preferences.putString("actCurrencies", actCurrencies.c_str()); preferences.putString("actCurrencies", actCurrencies.c_str());
Serial.printf("Set actCurrencies: %s\n", actCurrencies.c_str()); Serial.printf("Set actCurrencies: %s\n", actCurrencies);
} }
if (settings["txPower"].is<int>()) if (settings.containsKey("txPower"))
{ {
int txPower = settings["txPower"].as<int>(); int txPower = settings["txPower"].as<int>();
@ -583,7 +622,7 @@ void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json)
} }
} }
request->send(HTTP_OK); request->send(200);
if (settingsChanged) if (settingsChanged)
{ {
queueLedEffect(LED_FLASH_SUCCESS); queueLedEffect(LED_FLASH_SUCCESS);
@ -592,7 +631,7 @@ void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json)
void onApiRestart(AsyncWebServerRequest *request) void onApiRestart(AsyncWebServerRequest *request)
{ {
request->send(HTTP_OK); request->send(200);
if (events.count()) if (events.count())
events.send("closing"); events.send("closing");
@ -606,7 +645,7 @@ void onApiIdentify(AsyncWebServerRequest *request)
{ {
queueLedEffect(LED_FLASH_IDENTIFY); queueLedEffect(LED_FLASH_IDENTIFY);
request->send(HTTP_OK); request->send(200);
} }
/** /**
@ -682,6 +721,11 @@ void onApiSettingsGet(AsyncWebServerRequest *request)
root["miningPoolName"] = preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME); root["miningPoolName"] = preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME);
root["miningPoolUser"] = preferences.getString("miningPoolUser", DEFAULT_MINING_POOL_USER); root["miningPoolUser"] = preferences.getString("miningPoolUser", DEFAULT_MINING_POOL_USER);
root["availablePools"] = PoolFactory::getAvailablePools(); root["availablePools"] = PoolFactory::getAvailablePools();
root["mqttEnabled"] = preferences.getBool("mqttEnabled", DEFAULT_MQTT_ENABLED);
root["mqttUrl"] = preferences.getString("mqttUrl", DEFAULT_MQTT_URL);
root["mqttRootTopic"] = preferences.getString("mqttRootTopic", DEFAULT_MQTT_ROOTTOPIC);
root["httpAuthEnabled"] = preferences.getBool("httpAuthEnabled", DEFAULT_HTTP_AUTH_ENABLED); root["httpAuthEnabled"] = preferences.getBool("httpAuthEnabled", DEFAULT_HTTP_AUTH_ENABLED);
root["httpAuthUser"] = preferences.getString("httpAuthUser", DEFAULT_HTTP_AUTH_USERNAME); root["httpAuthUser"] = preferences.getString("httpAuthUser", DEFAULT_HTTP_AUTH_USERNAME);
root["httpAuthPass"] = preferences.getString("httpAuthPass", DEFAULT_HTTP_AUTH_PASSWORD); root["httpAuthPass"] = preferences.getString("httpAuthPass", DEFAULT_HTTP_AUTH_PASSWORD);
@ -732,10 +776,8 @@ void onApiSettingsGet(AsyncWebServerRequest *request)
o["enabled"] = preferences.getBool(key.c_str(), true); o["enabled"] = preferences.getBool(key.c_str(), true);
} }
root["poolLogosUrl"] = preferences.getString("poolLogosUrl", DEFAULT_MINING_POOL_LOGOS_URL);
AsyncResponseStream *response = AsyncResponseStream *response =
request->beginResponseStream(JSON_CONTENT); request->beginResponseStream("application/json");
serializeJson(root, *response); serializeJson(root, *response);
request->send(response); request->send(response);
@ -747,9 +789,8 @@ bool processEpdColorSettings(AsyncWebServerRequest *request)
if (request->hasParam("fgColor", true)) if (request->hasParam("fgColor", true))
{ {
const AsyncWebParameter *fgColor = request->getParam("fgColor", true); const AsyncWebParameter *fgColor = request->getParam("fgColor", true);
uint32_t color = strtol(fgColor->value().c_str(), NULL, 16); preferences.putUInt("fgColor", strtol(fgColor->value().c_str(), NULL, 16));
preferences.putUInt("fgColor", color); setFgColor(int(strtol(fgColor->value().c_str(), NULL, 16)));
setFgColor(color);
// Serial.print(F("Setting foreground color to ")); // Serial.print(F("Setting foreground color to "));
// Serial.println(fgColor->value().c_str()); // Serial.println(fgColor->value().c_str());
settingsChanged = true; settingsChanged = true;
@ -758,9 +799,8 @@ bool processEpdColorSettings(AsyncWebServerRequest *request)
{ {
const AsyncWebParameter *bgColor = request->getParam("bgColor", true); const AsyncWebParameter *bgColor = request->getParam("bgColor", true);
uint32_t color = strtol(bgColor->value().c_str(), NULL, 16); preferences.putUInt("bgColor", strtol(bgColor->value().c_str(), NULL, 16));
preferences.putUInt("bgColor", color); setBgColor(int(strtol(bgColor->value().c_str(), NULL, 16)));
setBgColor(color);
// Serial.print(F("Setting background color to ")); // Serial.print(F("Setting background color to "));
// Serial.println(bgColor->value().c_str()); // Serial.println(bgColor->value().c_str());
settingsChanged = true; settingsChanged = true;
@ -772,7 +812,7 @@ bool processEpdColorSettings(AsyncWebServerRequest *request)
void onApiSystemStatus(AsyncWebServerRequest *request) void onApiSystemStatus(AsyncWebServerRequest *request)
{ {
AsyncResponseStream *response = AsyncResponseStream *response =
request->beginResponseStream(JSON_CONTENT); request->beginResponseStream("application/json");
JsonDocument root; JsonDocument root;
@ -780,9 +820,6 @@ void onApiSystemStatus(AsyncWebServerRequest *request)
root["espHeapSize"] = ESP.getHeapSize(); root["espHeapSize"] = ESP.getHeapSize();
root["espFreePsram"] = ESP.getFreePsram(); root["espFreePsram"] = ESP.getFreePsram();
root["espPsramSize"] = ESP.getPsramSize(); root["espPsramSize"] = ESP.getPsramSize();
root["fsUsedBytes"] = LittleFS.usedBytes();
root["fsTotalBytes"] = LittleFS.totalBytes();
root["rssi"] = WiFi.RSSI(); root["rssi"] = WiFi.RSSI();
root["txPower"] = WiFi.getTxPower(); root["txPower"] = WiFi.getTxPower();
@ -814,19 +851,19 @@ void onApiSetWifiTxPower(AsyncWebServerRequest *request)
if (WiFi.setTxPower(static_cast<wifi_power_t>(txPower))) if (WiFi.setTxPower(static_cast<wifi_power_t>(txPower)))
{ {
preferences.putInt("txPower", txPower); preferences.putInt("txPower", txPower);
request->send(HTTP_OK, "application/json", "{\"setTxPower\": \"ok\"}"); request->send(200, "application/json", "{\"setTxPower\": \"ok\"}");
return; return;
} }
} }
} }
return request->send(HTTP_BAD_REQUEST); return request->send(400);
} }
void onApiLightsStatus(AsyncWebServerRequest *request) void onApiLightsStatus(AsyncWebServerRequest *request)
{ {
AsyncResponseStream *response = AsyncResponseStream *response =
request->beginResponseStream(JSON_CONTENT); request->beginResponseStream("application/json");
serializeJson(getLedStatusObject()["data"], *response); serializeJson(getLedStatusObject()["data"], *response);
@ -836,7 +873,7 @@ void onApiLightsStatus(AsyncWebServerRequest *request)
void onApiStopDataSources(AsyncWebServerRequest *request) void onApiStopDataSources(AsyncWebServerRequest *request)
{ {
AsyncResponseStream *response = AsyncResponseStream *response =
request->beginResponseStream(JSON_CONTENT); request->beginResponseStream("application/json");
stopPriceNotify(); stopPriceNotify();
stopBlockNotify(); stopBlockNotify();
@ -847,7 +884,7 @@ void onApiStopDataSources(AsyncWebServerRequest *request)
void onApiRestartDataSources(AsyncWebServerRequest *request) void onApiRestartDataSources(AsyncWebServerRequest *request)
{ {
AsyncResponseStream *response = AsyncResponseStream *response =
request->beginResponseStream(JSON_CONTENT); request->beginResponseStream("application/json");
restartPriceNotify(); restartPriceNotify();
restartBlockNotify(); restartBlockNotify();
@ -860,7 +897,7 @@ void onApiRestartDataSources(AsyncWebServerRequest *request)
void onApiLightsOff(AsyncWebServerRequest *request) void onApiLightsOff(AsyncWebServerRequest *request)
{ {
setLights(0, 0, 0); setLights(0, 0, 0);
request->send(HTTP_OK); request->send(200);
} }
void onApiLightsSetColor(AsyncWebServerRequest *request) void onApiLightsSetColor(AsyncWebServerRequest *request)
@ -868,7 +905,7 @@ void onApiLightsSetColor(AsyncWebServerRequest *request)
if (request->hasParam("c")) if (request->hasParam("c"))
{ {
AsyncResponseStream *response = AsyncResponseStream *response =
request->beginResponseStream(JSON_CONTENT); request->beginResponseStream("application/json");
String rgbColor = request->getParam("c")->value(); String rgbColor = request->getParam("c")->value();
@ -892,7 +929,7 @@ void onApiLightsSetColor(AsyncWebServerRequest *request)
} }
else else
{ {
request->send(HTTP_BAD_REQUEST); request->send(400);
} }
} }
@ -909,7 +946,7 @@ void onApiLightsSetJson(AsyncWebServerRequest *request, JsonVariant &json)
} }
Serial.printf("Invalid values for LED set %d\n", lights.size()); Serial.printf("Invalid values for LED set %d\n", lights.size());
request->send(HTTP_BAD_REQUEST); request->send(400);
return; return;
} }
@ -917,27 +954,27 @@ void onApiLightsSetJson(AsyncWebServerRequest *request, JsonVariant &json)
{ {
unsigned int red, green, blue; unsigned int red, green, blue;
if (lights[i]["red"].is<uint>() && lights[i]["green"].is<uint>() && if (lights[i].containsKey("red") && lights[i].containsKey("green") &&
lights[i]["blue"].is<uint>()) lights[i].containsKey("blue"))
{ {
red = lights[i]["red"].as<uint>(); red = lights[i]["red"].as<uint>();
green = lights[i]["green"].as<uint>(); green = lights[i]["green"].as<uint>();
blue = lights[i]["blue"].as<uint>(); blue = lights[i]["blue"].as<uint>();
} }
else if (lights[i]["hex"].is<const char*>()) else if (lights[i].containsKey("hex"))
{ {
if (!sscanf(lights[i]["hex"].as<String>().c_str(), "#%02X%02X%02X", &red, if (!sscanf(lights[i]["hex"].as<String>().c_str(), "#%02X%02X%02X", &red,
&green, &blue) == 3) &green, &blue) == 3)
{ {
Serial.printf("Invalid hex for LED %d\n", i); Serial.printf("Invalid hex for LED %d\n", i);
request->send(HTTP_BAD_REQUEST); request->send(400);
return; return;
} }
} }
else else
{ {
Serial.printf("No valid color for LED %d\n", i); Serial.printf("No valid color for LED %d\n", i);
request->send(HTTP_BAD_REQUEST); request->send(400);
return; return;
} }
@ -948,7 +985,7 @@ void onApiLightsSetJson(AsyncWebServerRequest *request, JsonVariant &json)
pixels.show(); pixels.show();
saveLedState(); saveLedState();
request->send(HTTP_OK); request->send(200);
} }
void onIndex(AsyncWebServerRequest *request) void onIndex(AsyncWebServerRequest *request)
@ -958,6 +995,40 @@ void onIndex(AsyncWebServerRequest *request)
void onNotFound(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 || if (request->method() == HTTP_OPTIONS ||
@ -965,7 +1036,7 @@ void onNotFound(AsyncWebServerRequest *request)
{ {
// Serial.printf("NotFound, Return[%d]\n", 200); // Serial.printf("NotFound, Return[%d]\n", 200);
request->send(HTTP_OK); request->send(200);
} }
else else
{ {
@ -1001,7 +1072,7 @@ void onApiShowCurrency(AsyncWebServerRequest *request)
setCurrentCurrency(curChar); setCurrentCurrency(curChar);
setCurrentScreen(getCurrentScreen()); setCurrentScreen(getCurrentScreen());
request->send(HTTP_OK); request->send(200);
return; return;
} }
request->send(404); request->send(404);
@ -1012,13 +1083,13 @@ void onApiFrontlightOn(AsyncWebServerRequest *request)
{ {
frontlightFadeInAll(); frontlightFadeInAll();
request->send(HTTP_OK); request->send(200);
} }
void onApiFrontlightStatus(AsyncWebServerRequest *request) void onApiFrontlightStatus(AsyncWebServerRequest *request)
{ {
AsyncResponseStream *response = AsyncResponseStream *response =
request->beginResponseStream(JSON_CONTENT); request->beginResponseStream("application/json");
JsonDocument root; JsonDocument root;
@ -1037,7 +1108,7 @@ void onApiFrontlightFlash(AsyncWebServerRequest *request)
{ {
frontlightFlash(preferences.getUInt("flEffectDelay")); frontlightFlash(preferences.getUInt("flEffectDelay"));
request->send(HTTP_OK); request->send(200);
} }
void onApiFrontlightSetBrightness(AsyncWebServerRequest *request) void onApiFrontlightSetBrightness(AsyncWebServerRequest *request)
@ -1045,11 +1116,11 @@ void onApiFrontlightSetBrightness(AsyncWebServerRequest *request)
if (request->hasParam("b")) if (request->hasParam("b"))
{ {
frontlightSetBrightness(request->getParam("b")->value().toInt()); frontlightSetBrightness(request->getParam("b")->value().toInt());
request->send(HTTP_OK); request->send(200);
} }
else else
{ {
request->send(HTTP_BAD_REQUEST); request->send(400);
} }
} }
@ -1057,6 +1128,6 @@ void onApiFrontlightOff(AsyncWebServerRequest *request)
{ {
frontlightFadeOutAll(); frontlightFadeOutAll();
request->send(HTTP_OK); request->send(200);
} }
#endif #endif

View file

@ -28,7 +28,8 @@ void onApiStatus(AsyncWebServerRequest *request);
void onApiSystemStatus(AsyncWebServerRequest *request); void onApiSystemStatus(AsyncWebServerRequest *request);
void onApiSetWifiTxPower(AsyncWebServerRequest *request); void onApiSetWifiTxPower(AsyncWebServerRequest *request);
void onApiScreenControl(AsyncWebServerRequest *request); void onApiScreenNext(AsyncWebServerRequest *request);
void onApiScreenPrevious(AsyncWebServerRequest *request);
void onApiShowScreen(AsyncWebServerRequest *request); void onApiShowScreen(AsyncWebServerRequest *request);
void onApiShowCurrency(AsyncWebServerRequest *request); void onApiShowCurrency(AsyncWebServerRequest *request);

View file

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

View file

@ -1,75 +0,0 @@
#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();
}