diff --git a/lib/btclock/data_handler.cpp b/lib/btclock/data_handler.cpp index eaf4538..437f435 100644 --- a/lib/btclock/data_handler.cpp +++ b/lib/btclock/data_handler.cpp @@ -46,8 +46,11 @@ std::string getCurrencyCode(char input) case CURRENCY_CAD: return CURRENCY_CODE_CAD; break; - default: + case CURRENCY_USD: return CURRENCY_CODE_USD; + break; + default: + return CURRENCY_CODE_UNKNOWN; } } @@ -63,126 +66,172 @@ char getCurrencyChar(const std::string& input) return CURRENCY_AUD; else if (input == "CAD") return CURRENCY_CAD; - else + else if (input == "USD") return CURRENCY_USD; // Assuming USD is the default for unknown inputs + else + return ' '; } -std::array parsePriceData(std::uint32_t price, char currencySymbol, bool useSuffixFormat, bool mowMode, bool shareDot) -{ - std::array ret; - std::string priceString; - if (std::to_string(price).length() >= NUM_SCREENS || useSuffixFormat) +// Private helper methods +namespace { + std::array formatPriceData(std::uint32_t price, char currencySymbol, const std::string& currencyCode, bool useSuffixFormat, bool mowMode, bool shareDot, bool useSymbol) { - int numScreens = shareDot || mowMode ? NUM_SCREENS - 1 : NUM_SCREENS - 2; - priceString = getCurrencySymbol(currencySymbol) + formatNumberWithSuffix(price, numScreens, mowMode); - } - else - { - priceString = getCurrencySymbol(currencySymbol) + std::to_string(price); - } - std::uint32_t firstIndex = 0; - if ((shareDot && priceString.length() <= (NUM_SCREENS)) || priceString.length() < (NUM_SCREENS)) - { - priceString.insert(priceString.begin(), NUM_SCREENS - priceString.length(), ' '); - - if (mowMode) + std::array ret; + std::string priceString; + // If useSymbol is true, we're using the char version - use symbol as requested + // If useSymbol is false, we're using the string version - only use symbol if it's a recognized currency + bool shouldUseSymbol = useSymbol || (!useSymbol && currencySymbol != ' '); + + if (std::to_string(price).length() >= NUM_SCREENS || useSuffixFormat) { - ret[0] = "MOW/UNITS"; + int numScreens = shareDot || mowMode ? NUM_SCREENS - 1 : NUM_SCREENS - 2; + if (shouldUseSymbol) { + priceString = getCurrencySymbol(currencySymbol) + formatNumberWithSuffix(price, numScreens, mowMode); + } else { + priceString = formatNumberWithSuffix(price, numScreens, mowMode); + } } else { - ret[0] = "BTC/" + getCurrencyCode(currencySymbol); - } - - - firstIndex = 1; - } - - size_t dotPosition = priceString.find('.'); - - if (shareDot && dotPosition != std::string::npos && dotPosition > 0) - { - std::vector tempArray; - if (dotPosition != std::string::npos && dotPosition > 0) - { - for (size_t i = 0; i < priceString.length(); ++i) - { - if (i == dotPosition - 1) - { - tempArray.push_back(std::string(1, priceString[i]) + "."); - ++i; // Skip the dot in the next iteration - } - else - { - tempArray.push_back(std::string(1, priceString[i])); - } - } - - // Copy from tempArray to ret - for (std::uint32_t i = firstIndex; i < NUM_SCREENS && i - firstIndex < tempArray.size(); ++i) - { - ret[i] = tempArray[i - firstIndex]; + if (shouldUseSymbol) { + priceString = getCurrencySymbol(currencySymbol) + std::to_string(price); + } else { + priceString = std::to_string(price); } } - } - else - { - for (std::uint32_t i = firstIndex; i < NUM_SCREENS; i++) - { - ret[i] = std::string(1, priceString[i]); - } - } - - - return ret; -} - -std::array parseSatsPerCurrency(std::uint32_t price,char currencySymbol, bool withSatsSymbol) -{ - std::array ret; - std::string priceString = std::to_string(int(round(1 / float(price) * 10e7))); - std::uint32_t firstIndex = 0; - std::uint8_t insertSatSymbol = NUM_SCREENS - priceString.length() - 1; - - if (priceString.length() < (NUM_SCREENS)) - { - // Check if price is greater than 1 billion - if (price >= 100000000) - { - double satsPerCurrency = (1.0 / static_cast(price)) * 1e8; // Calculate satoshis - std::ostringstream oss; - oss << std::fixed << std::setprecision(3) << satsPerCurrency; // Format with 3 decimal places - priceString = oss.str(); - } - else - { - priceString = std::to_string(static_cast(round(1.0 / static_cast(price) * 1e8))); // Default formatting - } - - // Pad the string with spaces if necessary - if (priceString.length() < NUM_SCREENS) + std::uint32_t firstIndex = 0; + if ((shareDot && priceString.length() <= (NUM_SCREENS)) || priceString.length() < (NUM_SCREENS)) { priceString.insert(priceString.begin(), NUM_SCREENS - priceString.length(), ' '); + + if (mowMode) + { + ret[0] = "MOW/UNITS"; + } + else + { + ret[0] = "BTC/" + currencyCode; + } + + + firstIndex = 1; } + + size_t dotPosition = priceString.find('.'); - if (currencySymbol != CURRENCY_USD || price >= 100000000) // no time anymore when earlier than 1 - ret[0] = "SATS/" + getCurrencyCode(currencySymbol); - else - ret[0] = "MSCW/TIME"; - - firstIndex = 1; - - for (std::uint32_t i = firstIndex; i < NUM_SCREENS; i++) + if (shareDot && dotPosition != std::string::npos && dotPosition > 0) { - ret[i] = priceString[i]; + std::vector tempArray; + if (dotPosition != std::string::npos && dotPosition > 0) + { + for (size_t i = 0; i < priceString.length(); ++i) + { + if (i == dotPosition - 1) + { + tempArray.push_back(std::string(1, priceString[i]) + "."); + ++i; // Skip the dot in the next iteration + } + else + { + tempArray.push_back(std::string(1, priceString[i])); + } + } + + // Copy from tempArray to ret + for (std::uint32_t i = firstIndex; i < NUM_SCREENS && i - firstIndex < tempArray.size(); ++i) + { + ret[i] = tempArray[i - firstIndex]; + } + } + } + else + { + for (std::uint32_t i = firstIndex; i < NUM_SCREENS; i++) + { + ret[i] = std::string(1, priceString[i]); + } } - if (withSatsSymbol) - { - ret[insertSatSymbol] = "STS"; - } + return ret; } - return ret; + + std::array formatSatsPerCurrency(std::uint32_t price, char currencySymbol, const std::string& currencyCode, bool withSatsSymbol, bool alwaysShowSats) + { + std::array ret; + double satsPerCurrency = (1.0 / static_cast(price)) * 1e8; + std::string priceString; + + // Handle values below 1 sat per currency with 3 decimal places + if (satsPerCurrency < 1.0) { + std::ostringstream oss; + oss << std::fixed << std::setprecision(3) << satsPerCurrency; + priceString = oss.str(); + } + // // Check if price is greater than 1 billion (for displaying with 3 decimal places) + // else if (price >= 100000000) { + // std::ostringstream oss; + // oss << std::fixed << std::setprecision(3) << satsPerCurrency; + // priceString = oss.str(); + // } + else { + // Default formatting for integer values + priceString = std::to_string(static_cast(round(satsPerCurrency))); + } + + std::uint32_t firstIndex = 0; + std::uint8_t insertSatSymbol = NUM_SCREENS - priceString.length() - 1; + + if (priceString.length() < (NUM_SCREENS)) + { + // Pad the string with spaces if necessary + if (priceString.length() < NUM_SCREENS) + { + priceString.insert(priceString.begin(), NUM_SCREENS - priceString.length(), ' '); + } + + if (alwaysShowSats || currencySymbol != CURRENCY_USD || price >= 100000000) // no time anymore when earlier than 1 + ret[0] = "SATS/" + currencyCode; + else + ret[0] = "MSCW/TIME"; + + firstIndex = 1; + + for (std::uint32_t i = firstIndex; i < NUM_SCREENS; i++) + { + ret[i] = priceString[i]; + } + + if (withSatsSymbol) + { + ret[insertSatSymbol] = "STS"; + } + } + return ret; + } +} + +// Updated public methods to use the helper methods +std::array parsePriceData(std::uint32_t price, char currencySymbol, bool useSuffixFormat, bool mowMode, bool shareDot) +{ + // For char version, always use the currency symbol + return formatPriceData(price, currencySymbol, getCurrencyCode(currencySymbol), useSuffixFormat, mowMode, shareDot, true); +} + +std::array parsePriceData(std::uint32_t price, const std::string& currencyCode, bool useSuffixFormat, bool mowMode, bool shareDot) +{ + // For string version, let formatPriceData decide whether to use symbol based on if it's a recognized currency + char currencyChar = getCurrencyChar(currencyCode); + return formatPriceData(price, currencyChar, currencyCode, useSuffixFormat, mowMode, shareDot, false); +} + +std::array parseSatsPerCurrency(std::uint32_t price, char currencySymbol, bool withSatsSymbol) +{ + return formatSatsPerCurrency(price, currencySymbol, getCurrencyCode(currencySymbol), withSatsSymbol, false); +} + +std::array parseSatsPerCurrency(std::uint32_t price, const std::string& currencyCode, bool withSatsSymbol) +{ + return formatSatsPerCurrency(price, getCurrencyChar(currencyCode), currencyCode, withSatsSymbol, true); } std::array parseBlockHeight(std::uint32_t blockHeight) @@ -350,7 +399,12 @@ emscripten::val parseBlockHeightArray(std::uint32_t blockHeight) emscripten::val parsePriceDataArray(std::uint32_t price, const std::string ¤cySymbol, bool useSuffixFormat = false, bool mowMode = false, bool shareDot = false) { - return arrayToStringArray(parsePriceData(price, currencySymbol[0], useSuffixFormat, mowMode, shareDot)); + // Handle both single character and three-character currency codes + if (currencySymbol.length() == 1) { + return arrayToStringArray(parsePriceData(price, currencySymbol[0], useSuffixFormat, mowMode, shareDot)); + } else { + return arrayToStringArray(parsePriceData(price, currencySymbol, useSuffixFormat, mowMode, shareDot)); + } } emscripten::val parseHalvingCountdownArray(std::uint32_t blockHeight, bool asBlocks) @@ -370,7 +424,12 @@ emscripten::val parseBlockFeesArray(std::uint16_t blockFees) emscripten::val parseSatsPerCurrencyArray(std::uint32_t price, const std::string ¤cySymbol, bool withSatsSymbol) { - return arrayToStringArray(parseSatsPerCurrency(price, currencySymbol[0], withSatsSymbol)); + // Handle both single character and three-character currency codes + if (currencySymbol.length() == 1) { + return arrayToStringArray(parseSatsPerCurrency(price, currencySymbol[0], withSatsSymbol)); + } else { + return arrayToStringArray(parseSatsPerCurrency(price, currencySymbol, withSatsSymbol)); + } } EMSCRIPTEN_BINDINGS(my_module) diff --git a/lib/btclock/data_handler.hpp b/lib/btclock/data_handler.hpp index 4dda86b..d1dabc5 100644 --- a/lib/btclock/data_handler.hpp +++ b/lib/btclock/data_handler.hpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include "utils.hpp" @@ -19,9 +21,13 @@ const std::string CURRENCY_CODE_GBP = "GBP"; const std::string CURRENCY_CODE_JPY = "JPY"; const std::string CURRENCY_CODE_AUD = "AUD"; const std::string CURRENCY_CODE_CAD = "CAD"; +const std::string CURRENCY_CODE_UNKNOWN = " "; +// Public methods std::array parsePriceData(std::uint32_t price, char currency, bool useSuffixFormat = false, bool mowMode = false, bool shareDot = false); +std::array parsePriceData(std::uint32_t price, const std::string& currencyCode, bool useSuffixFormat = false, bool mowMode = false, bool shareDot = false); std::array parseSatsPerCurrency(std::uint32_t price, char currencySymbol, bool withSatsSymbol); +std::array parseSatsPerCurrency(std::uint32_t price, const std::string& currencyCode, bool withSatsSymbol); std::array parseBlockHeight(std::uint32_t blockHeight); std::array parseHalvingCountdown(std::uint32_t blockHeight, bool asBlocks); std::array parseMarketCap(std::uint32_t blockHeight, std::uint32_t price, char currencySymbol, bool bigChars); @@ -29,4 +35,10 @@ std::array parseBlockFees(std::uint16_t blockFees); char getCurrencySymbol(char input); std::string getCurrencyCode(char input); -char getCurrencyChar(const std::string& input); \ No newline at end of file +char getCurrencyChar(const std::string& input); + +// Private helper methods +namespace { + std::array formatPriceData(std::uint32_t price, char currencySymbol, const std::string& currencyCode, bool useSuffixFormat, bool mowMode, bool shareDot, bool useSymbol = true); + std::array formatSatsPerCurrency(std::uint32_t price, char currencySymbol, const std::string& currencyCode, bool withSatsSymbol, bool alwaysShowSats = false); +} \ No newline at end of file diff --git a/src/lib/config.cpp b/src/lib/config.cpp index 908a5a6..c0f9305 100644 --- a/src/lib/config.cpp +++ b/src/lib/config.cpp @@ -1,5 +1,6 @@ #include "config.hpp" #include "led_handler.hpp" +#include "lnbits.hpp" #define MAX_ATTEMPTS_WIFI_CONNECTION 20 @@ -392,6 +393,10 @@ String replaceAmbiguousChars(String input) void setupWebsocketClients(void *pvParameters) { DataSourceType dataSource = getDataSource(); + + if (preferences.getBool("lnbitsEnabled", DEFAULT_LNBITS_ENABLED)) { + setupLnbits(); + } if (dataSource == BTCLOCK_SOURCE || dataSource == CUSTOM_SOURCE) { diff --git a/src/lib/defaults.hpp b/src/lib/defaults.hpp index 00d8bc0..f7d8b51 100644 --- a/src/lib/defaults.hpp +++ b/src/lib/defaults.hpp @@ -99,4 +99,8 @@ enum DataSourceType { #ifndef DEFAULT_BOOT_TEXT #define DEFAULT_BOOT_TEXT "BTCLOCK" -#endif \ No newline at end of file +#endif + +#define DEFAULT_LNBITS_INSTANCE "demo.lnbits.com" +#define DEFAULT_LNBITS_HTTPS true +#define DEFAULT_LNBITS_ENABLED false diff --git a/src/lib/lnbits.cpp b/src/lib/lnbits.cpp new file mode 100644 index 0000000..a170744 --- /dev/null +++ b/src/lib/lnbits.cpp @@ -0,0 +1,151 @@ +#include "lnbits.hpp" +#include "lib/shared.hpp" + +LNBitsFetch* LNBitsFetch::instance = nullptr; + +LNBitsFetch::LNBitsFetch() : initialized(false), taskHandle(nullptr) {} + +LNBitsFetch& LNBitsFetch::getInstance() { + if (instance == nullptr) { + instance = new LNBitsFetch(); + } + return *instance; +} + +void LNBitsFetch::setup() { + if (initialized) return; + + currentCurrency = getActiveCurrencies().front(); + + xTaskCreate(taskLNBits, "lnbitsTask", 6 * 1024, NULL, tskIDLE_PRIORITY, &taskHandle); + initialized = true; + + // Fetch rates immediately when setup + fetchRates(); +} + +String LNBitsFetch::buildApiUrl(const std::string& currency) { + String instance = preferences.getString("lnbitsInstance", DEFAULT_LNBITS_INSTANCE); + bool useHttps = preferences.getBool("lnbitsHttps", DEFAULT_LNBITS_HTTPS); + + String protocol = useHttps ? "https" : "http"; + return protocol + "://" + instance + "/api/v1/rate/" + String(currency.c_str()); +} + +void LNBitsFetch::fetchRates() { + if (!initialized) return; + + std::vector currencies = getActiveCurrencies(); + HTTPClient http; + + for (const std::string& currency : currencies) { + String apiUrl = buildApiUrl(currency); + + if (debugLogEnabled()) { + Serial.printf("Fetching LNBits rate for %s from %s\n", currency.c_str(), apiUrl.c_str()); + } + + http.begin(apiUrl); + int httpCode = http.GET(); + + if (httpCode == HTTP_CODE_OK) { + String payload = http.getString(); + JsonDocument doc; + DeserializationError error = deserializeJson(doc, payload); + + if (!error) { + if (doc.containsKey("price")) { + uint price = doc["price"].as(); + + if (price > 0) { + priceMap[currency] = price; + unsigned long int currentTime = esp_timer_get_time() / 1000000; + lastUpdateMap[currency] = currentTime; + + if (debugLogEnabled()) { + Serial.printf("Updated LNBits price for %s: %d\n", currency.c_str(), price); + } + + // Update the screen if it's currently displaying price-related information + if (workQueue != nullptr && ( + ScreenHandler::getCurrentScreen() == SCREEN_BTC_TICKER || + ScreenHandler::getCurrentScreen() == SCREEN_SATS_PER_CURRENCY || + ScreenHandler::getCurrentScreen() == SCREEN_MARKET_CAP)) { + WorkItem rateUpdate = {TASK_PRICE_UPDATE, currency[0]}; + xQueueSend(workQueue, &rateUpdate, portMAX_DELAY); + } + } + } + } else if (debugLogEnabled()) { + Serial.printf("JSON parse error for %s: %s\n", currency.c_str(), error.c_str()); + } + } else if (debugLogEnabled()) { + Serial.printf("HTTP error for %s: %d\n", currency.c_str(), httpCode); + } + + http.end(); + } +} + +void LNBitsFetch::stop() { + if (taskHandle != nullptr) { + vTaskDelete(taskHandle); + taskHandle = nullptr; + } + initialized = false; +} + +void LNBitsFetch::restart() { + stop(); + setup(); +} + +bool LNBitsFetch::isInitialized() const { + return initialized; +} + +TaskHandle_t LNBitsFetch::getTaskHandle() const { + return taskHandle; +} + +uint LNBitsFetch::getPrice(const std::string& currencyCode) { + if (priceMap.find(currencyCode) == priceMap.end()) { + return 0; + } + return priceMap[currencyCode]; +} + +void LNBitsFetch::setPrice(uint newPrice, const std::string& currencyCode) { + priceMap[currencyCode] = newPrice; +} + +unsigned long int LNBitsFetch::getLastPriceUpdate(const std::string& currencyCode) { + if (lastUpdateMap.find(currencyCode) == lastUpdateMap.end()) { + return 0; + } + return lastUpdateMap[currencyCode]; +} + +void setupLnbits() { + if (preferences.getBool("lnbitsEnabled", DEFAULT_LNBITS_ENABLED)) { + LNBitsFetch::getInstance().setup(); + } +} + +void taskLNBits(void* pvParameters) { + for (;;) { + // Wait for notification from timer + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + // Fetch rates + LNBitsFetch::getInstance().fetchRates(); + } +} + +void LNBitsFetch::setCurrentCurrency(const std::string& currency) { + currentCurrency = currency; +} + +std::string LNBitsFetch::getCurrentCurrency() const { + return currentCurrency; +} \ No newline at end of file diff --git a/src/lib/lnbits.hpp b/src/lib/lnbits.hpp new file mode 100644 index 0000000..3f74acc --- /dev/null +++ b/src/lib/lnbits.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "lib/screen_handler.hpp" + +class LNBitsFetch { +private: + LNBitsFetch(); + static LNBitsFetch* instance; + + bool initialized; + TaskHandle_t taskHandle; + std::map priceMap; + std::map lastUpdateMap; + std::string currentCurrency; + String buildApiUrl(const std::string& currency); + +public: + static LNBitsFetch& getInstance(); + + void setup(); + void fetchRates(); + void stop(); + void restart(); + void setCurrentCurrency(const std::string& currency); + std::string getCurrentCurrency() const; + + bool isInitialized() const; + TaskHandle_t getTaskHandle() const; + + uint getPrice(const std::string& currencyCode); + void setPrice(uint newPrice, const std::string& currencyCode); + unsigned long int getLastPriceUpdate(const std::string& currencyCode); +}; + +void setupLnbits(); +void taskLNBits(void* pvParameters); \ No newline at end of file diff --git a/src/lib/screen_handler.cpp b/src/lib/screen_handler.cpp index 75e59aa..2372aa5 100644 --- a/src/lib/screen_handler.cpp +++ b/src/lib/screen_handler.cpp @@ -89,24 +89,47 @@ bool ScreenHandler::handleCurrencyRotation(bool forward) { std::vector ac = getActiveCurrencies(); if (ac.empty()) return false; - std::string curCode = getCurrencyCode(getCurrentCurrency()); - auto it = std::find(ac.begin(), ac.end(), curCode); - - if (it == ac.end()) { - // Current currency not found in active currencies - initialize based on direction - setCurrentCurrency(getCurrencyChar(forward ? ac.front() : ac.back())); - setCurrentScreen(getCurrentScreen()); - return true; - } else if (forward && curCode != ac.back()) { - // Moving forward and not at last currency - setCurrentCurrency(getCurrencyChar(ac.at(std::distance(ac.begin(), it) + 1))); - setCurrentScreen(getCurrentScreen()); - return true; - } else if (!forward && curCode != ac.front()) { - // Moving backward and not at first currency - setCurrentCurrency(getCurrencyChar(ac.at(std::distance(ac.begin(), it) - 1))); - setCurrentScreen(getCurrentScreen()); - return true; + if (preferences.getBool("lnbitsEnabled", DEFAULT_LNBITS_ENABLED)) { + std::string curCode = LNBitsFetch::getInstance().getCurrentCurrency(); + auto it = std::find(ac.begin(), ac.end(), curCode); + + if (it == ac.end()) { + // Current currency not found in active currencies - initialize based on direction + LNBitsFetch::getInstance().setCurrentCurrency(forward ? ac.front() : ac.back()); + setCurrentScreen(getCurrentScreen()); + return true; + } else if (forward && curCode != ac.back()) { + // Moving forward and not at last currency + LNBitsFetch::getInstance().setCurrentCurrency(ac.at(std::distance(ac.begin(), it) + 1)); + setCurrentScreen(getCurrentScreen()); + return true; + } else if (!forward && curCode != ac.front()) { + // Moving backward and not at first currency + LNBitsFetch::getInstance().setCurrentCurrency(ac.at(std::distance(ac.begin(), it) - 1)); + setCurrentScreen(getCurrentScreen()); + return true; + } + } else { + + std::string curCode = getCurrencyCode(getCurrentCurrency()); + auto it = std::find(ac.begin(), ac.end(), curCode); + + if (it == ac.end()) { + // Current currency not found in active currencies - initialize based on direction + setCurrentCurrency(getCurrencyChar(forward ? ac.front() : ac.back())); + setCurrentScreen(getCurrentScreen()); + return true; + } else if (forward && curCode != ac.back()) { + // Moving forward and not at last currency + setCurrentCurrency(getCurrencyChar(ac.at(std::distance(ac.begin(), it) + 1))); + setCurrentScreen(getCurrentScreen()); + return true; + } else if (!forward && curCode != ac.front()) { + // Moving backward and not at first currency + setCurrentCurrency(getCurrencyChar(ac.at(std::distance(ac.begin(), it) - 1))); + setCurrentScreen(getCurrentScreen()); + return true; + } } // If we're at the last/first currency of current screen, let nextScreen/previousScreen handle it return false; @@ -241,15 +264,32 @@ void workerTask(void *pvParameters) { case TASK_PRICE_UPDATE: { uint currency = ScreenHandler::getCurrentCurrency(); - uint price = getPrice(currency); + + uint price; + if (preferences.getBool("lnbitsEnabled", DEFAULT_LNBITS_ENABLED)) { + price = LNBitsFetch::getInstance().getPrice(LNBitsFetch::getInstance().getCurrentCurrency()); + } else { + price = getPrice(currency); + } if (currentScreenValue == SCREEN_BTC_TICKER) { - taskEpdContent = parsePriceData(price, currency, preferences.getBool("suffixPrice", DEFAULT_SUFFIX_PRICE), - preferences.getBool("mowMode", DEFAULT_MOW_MODE), - preferences.getBool("suffixShareDot", DEFAULT_SUFFIX_SHARE_DOT) - ); + if (preferences.getBool("lnbitsEnabled", DEFAULT_LNBITS_ENABLED)) { + taskEpdContent = parsePriceData(price, LNBitsFetch::getInstance().getCurrentCurrency(), preferences.getBool("suffixPrice", DEFAULT_SUFFIX_PRICE), + preferences.getBool("mowMode", DEFAULT_MOW_MODE), + preferences.getBool("suffixShareDot", DEFAULT_SUFFIX_SHARE_DOT) + ); + } else { + taskEpdContent = parsePriceData(price, currency, preferences.getBool("suffixPrice", DEFAULT_SUFFIX_PRICE), + preferences.getBool("mowMode", DEFAULT_MOW_MODE), + preferences.getBool("suffixShareDot", DEFAULT_SUFFIX_SHARE_DOT) + ); + } } else if (currentScreenValue == SCREEN_SATS_PER_CURRENCY) { - taskEpdContent = parseSatsPerCurrency(price, currency, preferences.getBool("useSatsSymbol", DEFAULT_USE_SATS_SYMBOL)); + if (preferences.getBool("lnbitsEnabled", DEFAULT_LNBITS_ENABLED)) { + taskEpdContent = parseSatsPerCurrency(price, LNBitsFetch::getInstance().getCurrentCurrency(), preferences.getBool("useSatsSymbol", DEFAULT_USE_SATS_SYMBOL)); + } else { + taskEpdContent = parseSatsPerCurrency(price, currency, preferences.getBool("useSatsSymbol", DEFAULT_USE_SATS_SYMBOL)); + } } else { auto& blockNotify = BlockNotify::getInstance(); taskEpdContent = parseMarketCap(blockNotify.getBlockHeight(), price, currency, preferences.getBool("mcapBigChar", DEFAULT_MCAP_BIG_CHAR)); diff --git a/src/lib/timers.cpp b/src/lib/timers.cpp index 850ead3..a8b7e49 100644 --- a/src/lib/timers.cpp +++ b/src/lib/timers.cpp @@ -1,5 +1,6 @@ #include "timers.hpp" #include "led_handler.hpp" +#include "lnbits.hpp" esp_timer_handle_t screenRotateTimer; esp_timer_handle_t minuteTimer; @@ -78,6 +79,11 @@ void IRAM_ATTR minuteTimerISR(void *arg) { if (miningPoolHandle != NULL) { vTaskNotifyGiveFromISR(miningPoolHandle, &xHigherPriorityTaskWoken); } + + TaskHandle_t lnbitsHandle = LNBitsFetch::getInstance().getTaskHandle(); + if (lnbitsHandle != NULL) { + vTaskNotifyGiveFromISR(lnbitsHandle, &xHigherPriorityTaskWoken); + } if (xHigherPriorityTaskWoken == pdTRUE) { portYIELD_FROM_ISR(); diff --git a/src/lib/v2_notify.cpp b/src/lib/v2_notify.cpp index b915518..fa01165 100644 --- a/src/lib/v2_notify.cpp +++ b/src/lib/v2_notify.cpp @@ -77,21 +77,22 @@ namespace V2Notify buffer = new uint8_t[responseLength]; - response["type"] = "subscribe"; - response["eventType"] = "price"; + if (!preferences.getBool("lnbitsEnabled", DEFAULT_LNBITS_ENABLED)) { + response["type"] = "subscribe"; + response["eventType"] = "price"; - JsonArray currenciesArray = response["currencies"].to(); + JsonArray currenciesArray = response["currencies"].to(); - for (const auto &str : getActiveCurrencies()) - { - currenciesArray.add(str); + for (const auto &str : getActiveCurrencies()) + { + currenciesArray.add(str); + } + + responseLength = measureMsgPack(response); + buffer = new uint8_t[responseLength]; + serializeMsgPack(response, buffer, responseLength); + webSocket.sendBIN(buffer, responseLength); } - - // response["currencies"] = currenciesArray; - responseLength = measureMsgPack(response); - buffer = new uint8_t[responseLength]; - serializeMsgPack(response, buffer, responseLength); - webSocket.sendBIN(buffer, responseLength); break; } case WStype_TEXT: diff --git a/src/lib/webserver.cpp b/src/lib/webserver.cpp index 8313b76..a25d4d7 100644 --- a/src/lib/webserver.cpp +++ b/src/lib/webserver.cpp @@ -5,7 +5,7 @@ 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", "ceEndpoint", "fontName", "localPoolEndpoint", "tzString"}; + "hostnamePrefix", "mempoolInstance", "nostrPubKey", "nostrRelay", "bitaxeHostname", "miningPoolName", "miningPoolUser", "nostrZapPubkey", "httpAuthUser", "httpAuthPass", "gitReleaseUrl", "poolLogosUrl", "ceEndpoint", "fontName", "localPoolEndpoint", "tzString", "lnbitsInstance"}; static const char *const PROGMEM uintSettings[] = {"minSecPriceUpd", "fullRefreshMin", "ledBrightness", "flMaxBrightness", "flEffectDelay", "luxLightToggle", "wpTimeout"}; @@ -18,7 +18,7 @@ static const char *const PROGMEM boolSettings[] = {"ledTestOnPower", "ledFlashOn "mempoolSecure", "bitaxeEnabled", "miningPoolStats", "verticalDesc", "nostrZapNotify", "httpAuthEnabled", - "enableDebugLog", "ceDisableSSL", "dndEnabled", "dndTimeBasedEnabled"}; + "enableDebugLog", "ceDisableSSL", "dndEnabled", "dndTimeBasedEnabled", "lnbitsEnabled", "lnbitsHttps"}; AsyncWebServer server(80); AsyncEventSource events("/events"); @@ -252,9 +252,15 @@ JsonDocument getStatusObject() conStatus["blocks"] = blockNotify.isConnected(); conStatus["V2"] = V2Notify::isV2NotifyConnected(); conStatus["nostr"] = nostrConnected(); + conStatus["lnbits"] = LNBitsFetch::getInstance().isInitialized(); root["rssi"] = WiFi.RSSI(); - root["currency"] = getCurrencyCode(ScreenHandler::getCurrentCurrency()); + + if (preferences.getBool("lnbitsEnabled", DEFAULT_LNBITS_ENABLED)) { + root["currency"] = LNBitsFetch::getInstance().getCurrentCurrency(); + } else { + root["currency"] = getCurrencyCode(ScreenHandler::getCurrentCurrency()); + } #ifdef HAS_FRONTLIGHT std::vector statuses = ledHandler.frontlightGetStatus(); @@ -713,6 +719,12 @@ void onApiSettingsGet(AsyncWebServerRequest *request) root["ledFlashOnZap"] = preferences.getBool("ledFlashOnZap", DEFAULT_LED_FLASH_ON_ZAP); root["fontName"] = preferences.getString("fontName", DEFAULT_FONT_NAME); root["availableFonts"] = FontNames::getAvailableFonts(); + + // LNBits settings + root["lnbitsEnabled"] = preferences.getBool("lnbitsEnabled", DEFAULT_LNBITS_ENABLED); + root["lnbitsInstance"] = preferences.getString("lnbitsInstance", DEFAULT_LNBITS_INSTANCE); + root["lnbitsHttps"] = preferences.getBool("lnbitsHttps", DEFAULT_LNBITS_HTTPS); + // Custom endpoint settings (only used for CUSTOM_SOURCE) root["customEndpoint"] = preferences.getString("customEndpoint", DEFAULT_CUSTOM_ENDPOINT); root["customEndpointDisableSSL"] = preferences.getBool("customEndpointDisableSSL", DEFAULT_CUSTOM_ENDPOINT_DISABLE_SSL); @@ -917,6 +929,7 @@ void onApiStopDataSources(AsyncWebServerRequest *request) stopPriceNotify(); BlockNotify::getInstance().stop(); + LNBitsFetch::getInstance().stop(); request->send(response); } @@ -928,6 +941,9 @@ void onApiRestartDataSources(AsyncWebServerRequest *request) restartPriceNotify(); BlockNotify::getInstance().restart(); + if (preferences.getBool("lnbitsEnabled", DEFAULT_LNBITS_ENABLED)) { + LNBitsFetch::getInstance().restart(); + } request->send(response); } @@ -1077,10 +1093,17 @@ void onApiShowCurrency(AsyncWebServerRequest *request) return; } - char curChar = getCurrencyChar(currency); + // LNbits: set currency using a string + if (preferences.getBool("lnbitsEnabled", DEFAULT_LNBITS_ENABLED)) { + LNBitsFetch::getInstance().setCurrentCurrency(currency); + ScreenHandler::setCurrentScreen(ScreenHandler::getCurrentScreen()); - ScreenHandler::setCurrentCurrency(curChar); - ScreenHandler::setCurrentScreen(ScreenHandler::getCurrentScreen()); + } else { + // Normal situation: set currency using a char + char curChar = getCurrencyChar(currency); + ScreenHandler::setCurrentCurrency(curChar); + ScreenHandler::setCurrentScreen(ScreenHandler::getCurrentScreen()); + } request->send(HTTP_OK); return; diff --git a/src/lib/webserver.hpp b/src/lib/webserver.hpp index ddd6b73..fb4a348 100644 --- a/src/lib/webserver.hpp +++ b/src/lib/webserver.hpp @@ -8,6 +8,11 @@ #include #include "AsyncJson.h" #include +#include +#include +#include +#include +#include #include "lib/block_notify.hpp" #include "lib/led_handler.hpp" @@ -15,6 +20,11 @@ #include "lib/screen_handler.hpp" #include "webserver/OneParamRewrite.hpp" #include "lib/mining_pool/pool_factory.hpp" +#include "lib/lnbits.hpp" +#include "lib/v2_notify.hpp" +#include "lib/ota.hpp" +#include "lib/shared.hpp" + extern TaskHandle_t eventSourceTaskHandle; diff --git a/test/test_datahandler/test_main.cpp b/test/test_datahandler/test_main.cpp index 24118e4..9a127ac 100644 --- a/test/test_datahandler/test_main.cpp +++ b/test/test_datahandler/test_main.cpp @@ -36,12 +36,15 @@ void test_CorrectSatsPerDollarConversion(void) void test_SatsPerDollarAfter1B(void) { std::array output = parseSatsPerCurrency(120000000, CURRENCY_USD, false); - TEST_ASSERT_EQUAL_STRING("SATS/USD", output[0].c_str()); - TEST_ASSERT_EQUAL_STRING("0", output[NUM_SCREENS - 5].c_str()); - TEST_ASSERT_EQUAL_STRING(".", output[NUM_SCREENS - 4].c_str()); - TEST_ASSERT_EQUAL_STRING("8", output[NUM_SCREENS - 3].c_str()); - TEST_ASSERT_EQUAL_STRING("3", output[NUM_SCREENS - 2].c_str()); - TEST_ASSERT_EQUAL_STRING("3", output[NUM_SCREENS - 1].c_str()); + + std::string joined = joinArrayWithBrackets(output); + + TEST_ASSERT_EQUAL_STRING_MESSAGE("SATS/USD", output[0].c_str(), joined.c_str()); + TEST_ASSERT_EQUAL_STRING_MESSAGE("0", output[NUM_SCREENS - 5].c_str(), joined.c_str()); + TEST_ASSERT_EQUAL_STRING_MESSAGE(".", output[NUM_SCREENS - 4].c_str(), joined.c_str()); + TEST_ASSERT_EQUAL_STRING_MESSAGE("8", output[NUM_SCREENS - 3].c_str(), joined.c_str()); + TEST_ASSERT_EQUAL_STRING_MESSAGE("3", output[NUM_SCREENS - 2].c_str(), joined.c_str()); + TEST_ASSERT_EQUAL_STRING_MESSAGE("3", output[NUM_SCREENS - 1].c_str(), joined.c_str()); } void test_CorrectSatsPerPoundConversion(void) @@ -272,6 +275,18 @@ void test_Mcap1TrillionJpySmallChars(void) TEST_ASSERT_EQUAL_STRING("000", output[NUM_SCREENS - 1].c_str()); } +void test_PriceDataWithCurrencyCode(void) +{ + std::array output = parsePriceData(100000, "PYG", false, false, false); + TEST_ASSERT_EQUAL_STRING("BTC/PYG", output[0].c_str()); +} + +void test_SatsPerCurrencyWithCurrencyCode(void) +{ + std::array output = parseSatsPerCurrency(100000, "ZAR", false); + TEST_ASSERT_EQUAL_STRING("SATS/ZAR", output[0].c_str()); +} + // not needed when using generate_test_runner.rb int runUnityTests(void) { @@ -295,6 +310,8 @@ int runUnityTests(void) RUN_TEST(test_PriceSuffixModeCompact2); RUN_TEST(test_PriceSuffixModeMow); RUN_TEST(test_PriceSuffixModeMowCompact); + RUN_TEST(test_PriceDataWithCurrencyCode); + RUN_TEST(test_SatsPerCurrencyWithCurrencyCode); return UNITY_END(); }