diff --git a/src/lib/config.cpp b/src/lib/config.cpp index 6354a7a..ab891ca 100644 --- a/src/lib/config.cpp +++ b/src/lib/config.cpp @@ -1,7 +1,7 @@ #include "config.hpp" -#include "led_handler.hpp" -#define MAX_ATTEMPTS_WIFI_CONNECTION 20 +// Global instance definitions +PriceNotify::PriceNotifyManager priceManager; // zlib_turbo zt; @@ -266,7 +266,7 @@ void setupPreferences() EPDManager::getInstance().setForegroundColor(preferences.getUInt("fgColor", DEFAULT_FG_COLOR)); EPDManager::getInstance().setBackgroundColor(preferences.getUInt("bgColor", DEFAULT_BG_COLOR)); setBlockHeight(preferences.getUInt("blockHeight", INITIAL_BLOCK_HEIGHT)); - setPrice(preferences.getUInt("lastPrice", INITIAL_LAST_PRICE), CURRENCY_USD); + priceManager.processNewPrice(preferences.getUInt("lastPrice", INITIAL_LAST_PRICE), CURRENCY_USD); if (!preferences.isKey("enableDebugLog")) { preferences.putBool("enableDebugLog", DEFAULT_ENABLE_DEBUG_LOG); @@ -375,6 +375,14 @@ void setupWebsocketClients(void *pvParameters) { setupBlockNotify(); setupPriceNotify(); + + // Create task for price manager loop + xTaskCreate([](void* param) { + for (;;) { + priceManager.loop(); + vTaskDelay(10 / portTICK_PERIOD_MS); + } + }, "priceManagerLoop", (6 * 1024), NULL, tskIDLE_PRIORITY, NULL); } vTaskDelete(NULL); @@ -753,3 +761,17 @@ DataSourceType getDataSource() { void setDataSource(DataSourceType source) { preferences.putUChar("dataSource", static_cast(source)); } + +void setupPriceNotify() { + priceManager.init(PriceNotify::PriceSource::COINCAP); + priceManager.onPriceUpdate([](PriceNotify::Currency currency, uint64_t price) { + if (workQueue != nullptr && (ScreenHandler::getCurrentScreen() == SCREEN_BTC_TICKER || + ScreenHandler::getCurrentScreen() == SCREEN_SATS_PER_CURRENCY || + ScreenHandler::getCurrentScreen() == SCREEN_MARKET_CAP)) + { + WorkItem priceUpdate = {TASK_PRICE_UPDATE, static_cast(currency)}; + xQueueSend(workQueue, &priceUpdate, portMAX_DELAY); + } + }); + priceManager.connect(); +} diff --git a/src/lib/config.hpp b/src/lib/config.hpp index 95c877d..8cfc741 100644 --- a/src/lib/config.hpp +++ b/src/lib/config.hpp @@ -1,107 +1,88 @@ #pragma once -#include #include +#include #include -#include -#include -#include +#include #include +#include + #include -#include - -#include "lib/block_notify.hpp" -#include "lib/button_handler.hpp" -#include "lib/epd.hpp" -// #include "lib/improv.hpp" -#include "lib/led_handler.hpp" -#include "lib/ota.hpp" -#include "lib/nostr_notify.hpp" -#include "lib/bitaxe_fetch.hpp" -#include "lib/mining_pool_stats_fetch.hpp" - -#include "lib/v2_notify.hpp" - -#include "lib/price_notify.hpp" -#include "lib/screen_handler.hpp" -#include "lib/shared.hpp" -#include "lib/webserver.hpp" -#ifdef HAS_FRONTLIGHT -#include "PCA9685.h" -#include "BH1750.h" -#endif +#include +#include +#include "block_notify.hpp" +#include "led_handler.hpp" +#include "nostr_notify.hpp" +#include "price_notify/price_notify.hpp" +#include "screen_handler.hpp" #include "shared.hpp" -#include "defaults.hpp" +#include "timers.hpp" +#include "v2_notify.hpp" +#include "webserver.hpp" +#include "button_handler.hpp" +#include "bitaxe_fetch.hpp" +#include "mining_pool_stats_fetch.hpp" +#include "epd.hpp" -#define NTP_SERVER "pool.ntp.org" -#define DEFAULT_TIME_OFFSET_SECONDS 3600 -#ifndef MCP_DEV_ADDR -#define MCP_DEV_ADDR 0x20 +#ifdef HAS_FRONTLIGHT +#include +#include #endif +extern Preferences preferences; +extern QueueHandle_t workQueue; +extern PriceNotify::PriceNotifyManager priceManager; -void setup(); -void syncTime(); -uint getLastTimeSync(); -void setupPreferences(); -void setupWebsocketClients(void *pvParameters); -void setupHardware(); +void setupConfig(); void setupWifi(); +void setupOTA(); +void setupMDNS(); +void setupDataSources(); +void stopDataSources(); +void restartDataSources(); +void setupTasks(); void setupTimers(); +void setupLittleFS(); +void setupPreferences(); +void setupWDT(); +void setupWorkQueue(); +void setupLedHandler(); +void setupScreenHandler(); +void setupWebserver(); +void setupNostrNotify(); +void setupBlockNotify(); +void setupV2Notify(); +void setupPriceNotify(); +void setupHardware(); void finishSetup(); -void setupMcp(); +void syncTime(); + +void handleOTA(); +void handleWifi(); +void handleWDT(); +void handleWorkQueue(); + +void setWifiTxPower(int8_t power); +void onWifiEvent(WiFiEvent_t event); +void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info); + +DataSourceType getDataSource(); +void setDataSource(DataSourceType source); +String getMyHostname(); +uint getLastTimeSync(); +bool debugLogEnabled(); + #ifdef HAS_FRONTLIGHT -extern BH1750 bh1750; -extern bool hasLuxSensor; float getLightLevel(); bool hasLightLevel(); + +extern PCA9685 flArray; #endif -String getMyHostname(); -std::vector getScreenNameMap(); - -std::vector getLocalUrl(); -// bool improv_connectWifi(std::string ssid, std::string password); -// void improvGetAvailableWifiNetworks(); -// bool onImprovCommandCallback(improv::ImprovCommand cmd); -// void onImprovErrorCallback(improv::Error err); -// void improv_set_state(improv::State state); -// void improv_send_response(std::vector &response); -// void improv_set_error(improv::Error error); -//void addCurrencyMappings(const std::vector& currencies); -std::vector getActiveCurrencies(); -std::vector getAvailableCurrencies(); - -bool isActiveCurrency(std::string ¤cy); - -void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info); String getHwRev(); bool isWhiteVersion(); String getFsRev(); -bool debugLogEnabled(); - -void addScreenMapping(int value, const char* name); -// void addScreenMapping(int value, const String& name); -// void addScreenMapping(int value, const std::string& name); - -int findScreenIndexByValue(int value); -String replaceAmbiguousChars(String input); -const char* getFirmwareFilename(); -const char* getWebUiFilename(); -// void loadIcons(); - -extern Preferences preferences; -extern MCP23017 mcp1; -#ifdef IS_BTCLOCK_V8 -extern MCP23017 mcp2; -#endif - -#ifdef HAS_FRONTLIGHT -extern PCA9685 flArray; -#endif - -// Expose DataSourceType enum -extern DataSourceType getDataSource(); -extern void setDataSource(DataSourceType source); \ No newline at end of file +#define NTP_SERVER "pool.ntp.org" +#define DEFAULT_TIME_OFFSET_SECONDS 3600 \ No newline at end of file diff --git a/src/lib/globals.hpp b/src/lib/globals.hpp new file mode 100644 index 0000000..dbe52f6 --- /dev/null +++ b/src/lib/globals.hpp @@ -0,0 +1,6 @@ +#pragma once + +#include "price_notify/price_notify.hpp" + +// Global instances +extern PriceNotify::PriceNotifyManager priceManager; \ No newline at end of file diff --git a/src/lib/nostr_notify.cpp b/src/lib/nostr_notify.cpp index 6d9cce9..154b4a2 100644 --- a/src/lib/nostr_notify.cpp +++ b/src/lib/nostr_notify.cpp @@ -1,5 +1,6 @@ #include "nostr_notify.hpp" #include "led_handler.hpp" +#include "globals.hpp" std::vector pools; nostr::Transport *transport; @@ -171,7 +172,7 @@ void handleNostrEventCallback(const String &subId, nostr::SignedNostrEvent *even // Process the data if (!typeValue.isEmpty()) { if (typeValue == "priceUsd") { - processNewPrice(obj["content"].as(), CURRENCY_USD); + priceManager.processNewPrice(obj["content"].as(), static_cast(CURRENCY_USD)); } else if (typeValue == "blockHeight") { processNewBlock(obj["content"].as()); diff --git a/src/lib/nostr_notify.hpp b/src/lib/nostr_notify.hpp index ebbe2cb..035e02b 100644 --- a/src/lib/nostr_notify.hpp +++ b/src/lib/nostr_notify.hpp @@ -10,8 +10,8 @@ #include "NostrEvent.h" #include "NostrPool.h" -#include "price_notify.hpp" #include "block_notify.hpp" +#include "price_notify/price_notify.hpp" #include "lib/timers.hpp" void setupNostrNotify(bool asDatasource, bool zapNotify); diff --git a/src/lib/price_notify.cpp b/src/lib/price_notify.cpp deleted file mode 100644 index 6ac2f3b..0000000 --- a/src/lib/price_notify.cpp +++ /dev/null @@ -1,175 +0,0 @@ -#include "price_notify.hpp" - -const char *wsServerPrice = "wss://ws.coincap.io/prices?assets=bitcoin"; - -WebSocketsClient webSocket; -uint currentPrice = 90000; -unsigned long int lastPriceUpdate; -bool priceNotifyInit = false; -std::map currencyMap; -std::map lastUpdateMap; -TaskHandle_t priceNotifyTaskHandle; - -void onWebsocketPriceEvent(WStype_t type, uint8_t * payload, size_t length); - -void setupPriceNotify() -{ - webSocket.beginSSL("ws.coincap.io", 443, "/prices?assets=bitcoin"); - webSocket.onEvent([](WStype_t type, uint8_t * payload, size_t length) { - onWebsocketPriceEvent(type, payload, length); - }); - webSocket.setReconnectInterval(5000); - webSocket.enableHeartbeat(15000, 3000, 2); - - setupPriceNotifyTask(); -} - -void onWebsocketPriceEvent(WStype_t type, uint8_t * payload, size_t length) { - switch(type) { - case WStype_DISCONNECTED: - Serial.println(F("Price WS Connection Closed")); - break; - case WStype_CONNECTED: - { - Serial.println("Connected to " + String(wsServerPrice)); - priceNotifyInit = true; - break; - } - case WStype_TEXT: - { - JsonDocument doc; - deserializeJson(doc, (char *)payload); - - if (doc["bitcoin"].is()) - { - if (currentPrice != doc["bitcoin"].as()) - { - processNewPrice(doc["bitcoin"].as(), CURRENCY_USD); - } - } - break; - } - case WStype_BIN: - break; - case WStype_ERROR: - case WStype_FRAGMENT_TEXT_START: - case WStype_FRAGMENT_BIN_START: - case WStype_FRAGMENT: - case WStype_PING: - case WStype_PONG: - case WStype_FRAGMENT_FIN: - break; - } -} - -void processNewPrice(uint newPrice, char currency) -{ - uint minSecPriceUpd = preferences.getUInt( - "minSecPriceUpd", DEFAULT_SECONDS_BETWEEN_PRICE_UPDATE); - uint currentTime = esp_timer_get_time() / 1000000; - - if (lastUpdateMap.find(currency) == lastUpdateMap.end() || - (currentTime - lastUpdateMap[currency]) > minSecPriceUpd) - { - currencyMap[currency] = newPrice; - - // Store price in preferences if enough time has passed - if (lastUpdateMap[currency] == 0 || (currentTime - lastUpdateMap[currency]) > 120) - { - String prefKey = String("lastPrice_") + getCurrencyCode(currency).c_str(); - preferences.putUInt(prefKey.c_str(), newPrice); - } - - lastUpdateMap[currency] = currentTime; - - if (workQueue != nullptr && (ScreenHandler::getCurrentScreen() == SCREEN_BTC_TICKER || - ScreenHandler::getCurrentScreen() == SCREEN_SATS_PER_CURRENCY || - ScreenHandler::getCurrentScreen() == SCREEN_MARKET_CAP)) - { - WorkItem priceUpdate = {TASK_PRICE_UPDATE, currency}; - xQueueSend(workQueue, &priceUpdate, portMAX_DELAY); - } - } -} - -void loadStoredPrices() -{ - // Load prices for all supported currencies - std::vector currencies = getAvailableCurrencies(); - - for (const std::string ¤cy : currencies) { - // Get first character as the currency identifier - String prefKey = String("lastPrice_") + currency.c_str(); - uint storedPrice = preferences.getUInt(prefKey.c_str(), 0); - - if (storedPrice > 0) { - currencyMap[getCurrencyChar(currency)] = storedPrice; - // Initialize lastUpdateMap to 0 so next update will store immediately - lastUpdateMap[getCurrencyChar(currency)] = 0; - } - } -} - -uint getLastPriceUpdate(char currency) -{ - if (lastUpdateMap.find(currency) == lastUpdateMap.end()) - { - return 0; - } - - return lastUpdateMap[currency]; -} - -uint getPrice(char currency) -{ - if (currencyMap.find(currency) == currencyMap.end()) - { - return 0; - } - return currencyMap[currency]; -} - -void setPrice(uint newPrice, char currency) -{ - currencyMap[currency] = newPrice; -} - -bool isPriceNotifyConnected() -{ - return webSocket.isConnected(); -} - -bool getPriceNotifyInit() -{ - return priceNotifyInit; -} - -void stopPriceNotify() -{ - webSocket.disconnect(); - if (priceNotifyTaskHandle != NULL) { - vTaskDelete(priceNotifyTaskHandle); - priceNotifyTaskHandle = NULL; - } -} - -void restartPriceNotify() -{ - stopPriceNotify(); - setupPriceNotify(); -} - -void taskPriceNotify(void *pvParameters) -{ - for (;;) - { - webSocket.loop(); - vTaskDelay(10 / portTICK_PERIOD_MS); - } -} - -void setupPriceNotifyTask() -{ - xTaskCreate(taskPriceNotify, "priceNotify", (6 * 1024), NULL, tskIDLE_PRIORITY, - &priceNotifyTaskHandle); -} \ No newline at end of file diff --git a/src/lib/price_notify.hpp b/src/lib/price_notify.hpp deleted file mode 100644 index 6c8c6df..0000000 --- a/src/lib/price_notify.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "lib/screen_handler.hpp" - -extern TaskHandle_t priceNotifyTaskHandle; - -void setupPriceNotify(); -void setupPriceNotifyTask(); -void taskPriceNotify(void *pvParameters); - -void onWebsocketPriceEvent(WStype_t type, uint8_t * payload, size_t length); - -uint getPrice(char currency); -void setPrice(uint newPrice, char currency); - -void processNewPrice(uint newPrice, char currency); - -bool isPriceNotifyConnected(); -void stopPriceNotify(); -void restartPriceNotify(); - -bool getPriceNotifyInit(); -uint getLastPriceUpdate(char currency); -void loadStoredPrices(); \ No newline at end of file diff --git a/src/lib/price_notify/interfaces/price_source.hpp b/src/lib/price_notify/interfaces/price_source.hpp new file mode 100644 index 0000000..7762737 --- /dev/null +++ b/src/lib/price_notify/interfaces/price_source.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace PriceNotify { + +using std::function; +using std::map; +using std::string; + +// Convert char-based currency to enum for type safety +enum class Currency : char { + USD = '$', + EUR = '[', + GBP = ']', + JPY = '^', + AUD = '_', + CAD = '`' +}; + +class IPriceSource { +public: + virtual ~IPriceSource() = default; + + // Initialize and connect to the websocket + virtual void connect() = 0; + + // Disconnect and cleanup + virtual void disconnect() = 0; + + // Check connection status + virtual bool isConnected() const = 0; + + // Get the last known price for a currency + virtual uint64_t getPrice(Currency currency) const = 0; + + // Get the last update timestamp for a currency + virtual uint32_t getLastUpdate(Currency currency) const = 0; + + // Set callback for price updates + virtual void onPriceUpdate(function callback) = 0; + + // Process websocket loop - should be called regularly + virtual void loop() = 0; + +protected: + function priceUpdateCallback; +}; +} // namespace PriceNotify \ No newline at end of file diff --git a/src/lib/price_notify/price_notify.cpp b/src/lib/price_notify/price_notify.cpp new file mode 100644 index 0000000..7381e00 --- /dev/null +++ b/src/lib/price_notify/price_notify.cpp @@ -0,0 +1,115 @@ +#include "price_notify.hpp" +#include + +using std::make_unique; + +namespace PriceNotify { + +PriceNotifyManager::PriceNotifyManager() : currentSource(PriceSource::NONE) {} + +PriceNotifyManager::~PriceNotifyManager() { + disconnect(); +} + +void PriceNotifyManager::init(PriceSource source) { + currentSource = source; + setPriceSource(source); +} + +void PriceNotifyManager::connect() { + if (priceSource) { + priceSource->connect(); + } +} + +void PriceNotifyManager::disconnect() { + if (priceSource) { + priceSource->disconnect(); + } +} + +bool PriceNotifyManager::isConnected() const { + return priceSource ? priceSource->isConnected() : false; +} + +uint64_t PriceNotifyManager::getPrice(Currency currency) const { + if (priceSource) { + return priceSource->getPrice(currency); + } + // If no price source, return from internal storage + auto it = prices.find(currency); + return (it != prices.end()) ? it->second : 0; +} + +uint32_t PriceNotifyManager::getLastUpdate(Currency currency) const { + if (priceSource) { + return priceSource->getLastUpdate(currency); + } + // If no price source, return from internal storage + auto it = lastUpdates.find(currency); + return (it != lastUpdates.end()) ? it->second : 0; +} + +void PriceNotifyManager::onPriceUpdate(function callback) { + userCallback = callback; + if (priceSource) { + priceSource->onPriceUpdate([this](Currency currency, uint64_t price) { + if (userCallback) { + userCallback(currency, price); + } + }); + } +} + +void PriceNotifyManager::loop() { + if (priceSource) { + priceSource->loop(); + } +} + +void PriceNotifyManager::setPriceSource(PriceSource source) { + // Store the callback before destroying the old source + auto oldCallback = userCallback; + + // Disconnect and destroy old source + if (priceSource) { + priceSource->disconnect(); + } + + // Create new source + switch (source) { + case PriceSource::COINCAP: + priceSource = make_unique(); + break; + case PriceSource::KRAKEN: + priceSource = make_unique(); + break; + case PriceSource::NONE: + priceSource.reset(); + break; + } + + currentSource = source; + + // Restore callback if it exists + if (oldCallback) { + onPriceUpdate(oldCallback); + } +} + +PriceSource PriceNotifyManager::getCurrentSource() const { + return currentSource; +} + +void PriceNotifyManager::processNewPrice(uint64_t price, Currency currency) { + // Store the price and timestamp + prices[currency] = price; + lastUpdates[currency] = esp_timer_get_time() / 1000000; // Current time in seconds + + // If we have a callback, notify about the price update + if (userCallback) { + userCallback(currency, price); + } +} + +} // namespace PriceNotify \ No newline at end of file diff --git a/src/lib/price_notify/price_notify.hpp b/src/lib/price_notify/price_notify.hpp new file mode 100644 index 0000000..0959a4c --- /dev/null +++ b/src/lib/price_notify/price_notify.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include "interfaces/price_source.hpp" +#include "sources/coincap_source.hpp" +#include "sources/kraken_source.hpp" +#include +#include + +namespace PriceNotify { + +using std::unique_ptr; +using std::function; +using std::map; + +enum class PriceSource { + NONE, // Added for when no explicit source is set + COINCAP, + KRAKEN +}; + +class PriceNotifyManager { +public: + PriceNotifyManager(); + ~PriceNotifyManager(); + + // Initialize with a specific price source + void init(PriceSource source); + + // Connect to the price source + void connect(); + + // Disconnect from the price source + void disconnect(); + + // Check if connected to price source + bool isConnected() const; + + // Get the last known price for a currency + uint64_t getPrice(Currency currency) const; + + // Get the last update timestamp for a currency + uint32_t getLastUpdate(Currency currency) const; + + // Set callback for price updates + void onPriceUpdate(function callback); + + // Process websocket loop - should be called regularly + void loop(); + + // Change the price source + void setPriceSource(PriceSource source); + + // Get current price source + PriceSource getCurrentSource() const; + + // Process new price from external source + void processNewPrice(uint64_t price, Currency currency); + +private: + unique_ptr priceSource; + PriceSource currentSource; + function userCallback; + + // For storing prices when no explicit source is set + map prices; + map lastUpdates; +}; +} // namespace PriceNotify \ No newline at end of file diff --git a/src/lib/price_notify/sources/coincap_source.cpp b/src/lib/price_notify/sources/coincap_source.cpp new file mode 100644 index 0000000..63691a9 --- /dev/null +++ b/src/lib/price_notify/sources/coincap_source.cpp @@ -0,0 +1,96 @@ +#include "coincap_source.hpp" + +namespace PriceNotify { + +CoinCapSource* CoinCapSource::instance = nullptr; + +CoinCapSource::CoinCapSource() : connected(false) { + instance = this; +} + +CoinCapSource::~CoinCapSource() { + disconnect(); + instance = nullptr; +} + +void CoinCapSource::connect() { + webSocket.beginSSL("ws.coincap.io", 443, "/prices?assets=bitcoin"); + webSocket.onEvent([](WStype_t type, uint8_t* payload, size_t length) { + if (instance) { + instance->handleWebSocketEvent(type, payload, length); + } + }); + webSocket.setReconnectInterval(5000); + webSocket.enableHeartbeat(15000, 3000, 2); +} + +void CoinCapSource::disconnect() { + webSocket.disconnect(); + connected = false; +} + +bool CoinCapSource::isConnected() const { + return connected && webSocket.isConnected(); +} + +uint64_t CoinCapSource::getPrice(Currency currency) const { + auto it = prices.find(currency); + return (it != prices.end()) ? it->second : 0; +} + +uint32_t CoinCapSource::getLastUpdate(Currency currency) const { + auto it = lastUpdates.find(currency); + return (it != lastUpdates.end()) ? it->second : 0; +} + +void CoinCapSource::onPriceUpdate(function callback) { + priceUpdateCallback = callback; +} + +void CoinCapSource::loop() { + webSocket.loop(); +} + +void CoinCapSource::handleWebSocketEvent(WStype_t type, uint8_t* payload, size_t length) { + switch(type) { + case WStype_DISCONNECTED: + Serial.println(F("CoinCap WS Disconnected")); + connected = false; + break; + + case WStype_CONNECTED: + Serial.println(F("CoinCap WS Connected")); + connected = true; + break; + + case WStype_TEXT: + processMessage((char*)payload); + break; + + default: + break; + } +} + +void CoinCapSource::processMessage(const char* payload) { + StaticJsonDocument<512> doc; + DeserializationError error = deserializeJson(doc, payload); + + if (error) { + Serial.println(F("Failed to parse CoinCap message")); + return; + } + + if (doc["bitcoin"].is()) { + uint64_t price = doc["bitcoin"].as(); + uint32_t timestamp = esp_timer_get_time() / 1000000; // Current time in seconds + + prices[Currency::USD] = price; + lastUpdates[Currency::USD] = timestamp; + + if (priceUpdateCallback) { + priceUpdateCallback(Currency::USD, price); + } + } +} +} // namespace PriceNotify \ No newline at end of file diff --git a/src/lib/price_notify/sources/coincap_source.hpp b/src/lib/price_notify/sources/coincap_source.hpp new file mode 100644 index 0000000..3aa1b8c --- /dev/null +++ b/src/lib/price_notify/sources/coincap_source.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "../interfaces/price_source.hpp" +#include +#include + +namespace PriceNotify { + +using std::map; + +class CoinCapSource : public IPriceSource { +public: + CoinCapSource(); + ~CoinCapSource() override; + + void connect() override; + void disconnect() override; + bool isConnected() const override; + uint64_t getPrice(Currency currency) const override; + uint32_t getLastUpdate(Currency currency) const override; + void onPriceUpdate(function callback) override; + void loop() override; + +private: + static void onWebSocketEvent(WStype_t type, uint8_t* payload, size_t length); + void handleWebSocketEvent(WStype_t type, uint8_t* payload, size_t length); + void processMessage(const char* payload); + + WebSocketsClient webSocket; + map prices; + map lastUpdates; + bool connected; + + static CoinCapSource* instance; // For callback handling +}; +} // namespace PriceNotify \ No newline at end of file diff --git a/src/lib/price_notify/sources/kraken_source.cpp b/src/lib/price_notify/sources/kraken_source.cpp new file mode 100644 index 0000000..69ca9b6 --- /dev/null +++ b/src/lib/price_notify/sources/kraken_source.cpp @@ -0,0 +1,118 @@ +#include "kraken_source.hpp" + +namespace PriceNotify { + +KrakenSource* KrakenSource::instance = nullptr; + +KrakenSource::KrakenSource() : connected(false) { + instance = this; +} + +KrakenSource::~KrakenSource() { + disconnect(); + instance = nullptr; +} + +void KrakenSource::connect() { + webSocket.beginSSL("ws.kraken.com", 443, "/ws"); + webSocket.onEvent([](WStype_t type, uint8_t* payload, size_t length) { + if (instance) { + instance->handleWebSocketEvent(type, payload, length); + } + }); + webSocket.setReconnectInterval(5000); + webSocket.enableHeartbeat(15000, 3000, 2); +} + +void KrakenSource::disconnect() { + webSocket.disconnect(); + connected = false; +} + +bool KrakenSource::isConnected() const { + return connected && webSocket.isConnected(); +} + +uint64_t KrakenSource::getPrice(Currency currency) const { + auto it = prices.find(currency); + return (it != prices.end()) ? it->second : 0; +} + +uint32_t KrakenSource::getLastUpdate(Currency currency) const { + auto it = lastUpdates.find(currency); + return (it != lastUpdates.end()) ? it->second : 0; +} + +void KrakenSource::onPriceUpdate(function callback) { + priceUpdateCallback = callback; +} + +void KrakenSource::loop() { + webSocket.loop(); +} + +void KrakenSource::handleWebSocketEvent(WStype_t type, uint8_t* payload, size_t length) { + switch(type) { + case WStype_DISCONNECTED: + Serial.println(F("Kraken WS Disconnected")); + connected = false; + break; + + case WStype_CONNECTED: + Serial.println(F("Kraken WS Connected")); + connected = true; + subscribe(); + break; + + case WStype_TEXT: + processMessage((char*)payload); + break; + + default: + break; + } +} + +void KrakenSource::subscribe() { + // Subscribe to XBT/USD ticker + StaticJsonDocument<256> doc; + doc["event"] = "subscribe"; + JsonArray pair = doc.createNestedArray("pair"); + pair.add("XBT/USD"); + doc["subscription"]["name"] = "ticker"; + + String message; + serializeJson(doc, message); + webSocket.sendTXT(message); +} + +void KrakenSource::processMessage(const char* payload) { + StaticJsonDocument<512> doc; + DeserializationError error = deserializeJson(doc, payload); + + if (error) { + Serial.println(F("Failed to parse Kraken message")); + return; + } + + // Check if it's a ticker update (array format) + if (doc.is() && doc.size() >= 4) { + JsonArray arr = doc.as(); + if (arr[2] == "ticker" && arr[3] == "XBT/USD") { + JsonObject tickerData = arr[1].as(); + if (tickerData.containsKey("c")) { + // Get the first value from the "c" array which is the last trade price + uint64_t price = tickerData["c"][0].as() * 100; // Convert to cents + uint32_t timestamp = esp_timer_get_time() / 1000000; // Current time in seconds + + prices[Currency::USD] = price; + lastUpdates[Currency::USD] = timestamp; + + if (priceUpdateCallback) { + priceUpdateCallback(Currency::USD, price); + } + } + } + } +} +} // namespace PriceNotify \ No newline at end of file diff --git a/src/lib/price_notify/sources/kraken_source.hpp b/src/lib/price_notify/sources/kraken_source.hpp new file mode 100644 index 0000000..89701c0 --- /dev/null +++ b/src/lib/price_notify/sources/kraken_source.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "../interfaces/price_source.hpp" +#include +#include + +namespace PriceNotify { + +using std::map; + +class KrakenSource : public IPriceSource { +public: + KrakenSource(); + ~KrakenSource() override; + + void connect() override; + void disconnect() override; + bool isConnected() const override; + uint64_t getPrice(Currency currency) const override; + uint32_t getLastUpdate(Currency currency) const override; + void onPriceUpdate(function callback) override; + void loop() override; + +private: + static void onWebSocketEvent(WStype_t type, uint8_t* payload, size_t length); + void handleWebSocketEvent(WStype_t type, uint8_t* payload, size_t length); + void processMessage(const char* payload); + void subscribe(); + + WebSocketsClient webSocket; + map prices; + map lastUpdates; + bool connected; + + static KrakenSource* instance; // For callback handling +}; +} // namespace PriceNotify \ No newline at end of file diff --git a/src/lib/screen_handler.cpp b/src/lib/screen_handler.cpp index 6b3241e..07cc626 100644 --- a/src/lib/screen_handler.cpp +++ b/src/lib/screen_handler.cpp @@ -241,7 +241,7 @@ void workerTask(void *pvParameters) { case TASK_PRICE_UPDATE: { uint currency = ScreenHandler::getCurrentCurrency(); - uint price = getPrice(currency); + uint price = priceManager.getPrice(static_cast(currency)); if (currentScreenValue == SCREEN_BTC_TICKER) { taskEpdContent = parsePriceData(price, currency, preferences.getBool("suffixPrice", DEFAULT_SUFFIX_PRICE), diff --git a/src/lib/v2_notify.cpp b/src/lib/v2_notify.cpp index 560b39d..85d70b3 100644 --- a/src/lib/v2_notify.cpp +++ b/src/lib/v2_notify.cpp @@ -1,4 +1,5 @@ #include "v2_notify.hpp" +#include "globals.hpp" using namespace V2Notify; @@ -146,14 +147,15 @@ namespace V2Notify } else if (doc["price"].is()) { - // Iterate through the key-value pairs of the "price" object for (JsonPair kv : doc["price"].as()) { const char *currency = kv.key().c_str(); uint newPrice = kv.value().as(); - processNewPrice(newPrice, getCurrencyChar(currency)); + // Convert currency string to PriceNotify::Currency using data_handler's conversion + char currencyChar = getCurrencyChar(currency); + priceManager.processNewPrice(newPrice, static_cast(currencyChar)); } } } diff --git a/src/lib/webserver.cpp b/src/lib/webserver.cpp index 1d6a878..74d7165 100644 --- a/src/lib/webserver.cpp +++ b/src/lib/webserver.cpp @@ -246,7 +246,7 @@ JsonDocument getStatusObject() JsonObject conStatus = root["connectionStatus"].to(); - conStatus["price"] = isPriceNotifyConnected(); + conStatus["price"] = priceManager.isConnected(); conStatus["blocks"] = isBlockNotifyConnected(); conStatus["V2"] = V2Notify::isV2NotifyConnected(); conStatus["nostr"] = nostrConnected(); diff --git a/src/lib/webserver.hpp b/src/lib/webserver.hpp index ddd6b73..39e66d1 100644 --- a/src/lib/webserver.hpp +++ b/src/lib/webserver.hpp @@ -11,7 +11,7 @@ #include "lib/block_notify.hpp" #include "lib/led_handler.hpp" -#include "lib/price_notify.hpp" + #include "lib/screen_handler.hpp" #include "webserver/OneParamRewrite.hpp" #include "lib/mining_pool/pool_factory.hpp"