Price notify refactor

This commit is contained in:
Djuri 2025-01-05 23:50:26 +01:00
parent e19cad05bc
commit e987e9afd5
Signed by: djuri
GPG key ID: 61B9B2DDE5AA3AC1
18 changed files with 628 additions and 297 deletions

View file

@ -1,7 +1,7 @@
#include "config.hpp" #include "config.hpp"
#include "led_handler.hpp"
#define MAX_ATTEMPTS_WIFI_CONNECTION 20 // Global instance definitions
PriceNotify::PriceNotifyManager priceManager;
// zlib_turbo zt; // zlib_turbo zt;
@ -266,7 +266,7 @@ void setupPreferences()
EPDManager::getInstance().setForegroundColor(preferences.getUInt("fgColor", DEFAULT_FG_COLOR)); EPDManager::getInstance().setForegroundColor(preferences.getUInt("fgColor", DEFAULT_FG_COLOR));
EPDManager::getInstance().setBackgroundColor(preferences.getUInt("bgColor", DEFAULT_BG_COLOR)); EPDManager::getInstance().setBackgroundColor(preferences.getUInt("bgColor", DEFAULT_BG_COLOR));
setBlockHeight(preferences.getUInt("blockHeight", INITIAL_BLOCK_HEIGHT)); 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")) { if (!preferences.isKey("enableDebugLog")) {
preferences.putBool("enableDebugLog", DEFAULT_ENABLE_DEBUG_LOG); preferences.putBool("enableDebugLog", DEFAULT_ENABLE_DEBUG_LOG);
@ -375,6 +375,14 @@ void setupWebsocketClients(void *pvParameters)
{ {
setupBlockNotify(); setupBlockNotify();
setupPriceNotify(); 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); vTaskDelete(NULL);
@ -753,3 +761,17 @@ DataSourceType getDataSource() {
void setDataSource(DataSourceType source) { void setDataSource(DataSourceType source) {
preferences.putUChar("dataSource", static_cast<uint8_t>(source)); preferences.putUChar("dataSource", static_cast<uint8_t>(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<char>(currency)};
xQueueSend(workQueue, &priceUpdate, portMAX_DELAY);
}
});
priceManager.connect();
}

View file

@ -1,107 +1,88 @@
#pragma once #pragma once
#include <MCP23017.h>
#include <Arduino.h> #include <Arduino.h>
#include <ArduinoJson.h>
#include <Preferences.h> #include <Preferences.h>
#include <WiFiClientSecure.h> #include <WiFi.h>
#include <WiFiManager.h>
#include <base64.h>
#include <esp_task_wdt.h> #include <esp_task_wdt.h>
#include <WiFiClientSecure.h>
#include <nvs_flash.h> #include <nvs_flash.h>
#include <map> #include <WiFiManager.h>
#include <ESPmDNS.h>
#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 "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 "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" #ifdef HAS_FRONTLIGHT
#define DEFAULT_TIME_OFFSET_SECONDS 3600 #include <BH1750.h>
#ifndef MCP_DEV_ADDR #include <PCA9685.h>
#define MCP_DEV_ADDR 0x20
#endif #endif
extern Preferences preferences;
extern QueueHandle_t workQueue;
extern PriceNotify::PriceNotifyManager priceManager;
void setup(); void setupConfig();
void syncTime();
uint getLastTimeSync();
void setupPreferences();
void setupWebsocketClients(void *pvParameters);
void setupHardware();
void setupWifi(); void setupWifi();
void setupOTA();
void setupMDNS();
void setupDataSources();
void stopDataSources();
void restartDataSources();
void setupTasks();
void setupTimers(); 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 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 #ifdef HAS_FRONTLIGHT
extern BH1750 bh1750;
extern bool hasLuxSensor;
float getLightLevel(); float getLightLevel();
bool hasLightLevel(); bool hasLightLevel();
extern PCA9685 flArray;
#endif #endif
String getMyHostname();
std::vector<ScreenMapping> getScreenNameMap();
std::vector<std::string> getLocalUrl();
// bool improv_connectWifi(std::string ssid, std::string password);
// void improvGetAvailableWifiNetworks();
// bool onImprovCommandCallback(improv::ImprovCommand cmd);
// void onImprovErrorCallback(improv::Error err);
// void improv_set_state(improv::State state);
// void improv_send_response(std::vector<uint8_t> &response);
// void improv_set_error(improv::Error error);
//void addCurrencyMappings(const std::vector<std::string>& currencies);
std::vector<std::string> getActiveCurrencies();
std::vector<std::string> getAvailableCurrencies();
bool isActiveCurrency(std::string &currency);
void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info);
String getHwRev(); String getHwRev();
bool isWhiteVersion(); bool isWhiteVersion();
String getFsRev(); String getFsRev();
bool debugLogEnabled(); #define NTP_SERVER "pool.ntp.org"
#define DEFAULT_TIME_OFFSET_SECONDS 3600
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);

6
src/lib/globals.hpp Normal file
View file

@ -0,0 +1,6 @@
#pragma once
#include "price_notify/price_notify.hpp"
// Global instances
extern PriceNotify::PriceNotifyManager priceManager;

View file

@ -1,5 +1,6 @@
#include "nostr_notify.hpp" #include "nostr_notify.hpp"
#include "led_handler.hpp" #include "led_handler.hpp"
#include "globals.hpp"
std::vector<nostr::NostrPool *> pools; std::vector<nostr::NostrPool *> pools;
nostr::Transport *transport; nostr::Transport *transport;
@ -171,7 +172,7 @@ void handleNostrEventCallback(const String &subId, nostr::SignedNostrEvent *even
// Process the data // Process the data
if (!typeValue.isEmpty()) { if (!typeValue.isEmpty()) {
if (typeValue == "priceUsd") { if (typeValue == "priceUsd") {
processNewPrice(obj["content"].as<uint>(), CURRENCY_USD); priceManager.processNewPrice(obj["content"].as<uint>(), static_cast<PriceNotify::Currency>(CURRENCY_USD));
} }
else if (typeValue == "blockHeight") { else if (typeValue == "blockHeight") {
processNewBlock(obj["content"].as<uint>()); processNewBlock(obj["content"].as<uint>());

View file

@ -10,8 +10,8 @@
#include "NostrEvent.h" #include "NostrEvent.h"
#include "NostrPool.h" #include "NostrPool.h"
#include "price_notify.hpp"
#include "block_notify.hpp" #include "block_notify.hpp"
#include "price_notify/price_notify.hpp"
#include "lib/timers.hpp" #include "lib/timers.hpp"
void setupNostrNotify(bool asDatasource, bool zapNotify); void setupNostrNotify(bool asDatasource, bool zapNotify);

View file

@ -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<char, std::uint64_t> currencyMap;
std::map<char, unsigned long int> 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<JsonObject>())
{
if (currentPrice != doc["bitcoin"].as<long>())
{
processNewPrice(doc["bitcoin"].as<long>(), 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<std::string> currencies = getAvailableCurrencies();
for (const std::string &currency : 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);
}

View file

@ -1,29 +0,0 @@
#pragma once
#include <Arduino.h>
#include <ArduinoJson.h>
#include <WebSocketsClient.h>
#include <string>
#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();

View file

@ -0,0 +1,53 @@
#pragma once
#include <Arduino.h>
#include <WebSocketsClient.h>
#include <functional>
#include <map>
#include <string>
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<void(Currency, uint64_t)> callback) = 0;
// Process websocket loop - should be called regularly
virtual void loop() = 0;
protected:
function<void(Currency, uint64_t)> priceUpdateCallback;
};
} // namespace PriceNotify

View file

@ -0,0 +1,115 @@
#include "price_notify.hpp"
#include <esp_timer.h>
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<void(Currency, uint64_t)> 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<CoinCapSource>();
break;
case PriceSource::KRAKEN:
priceSource = make_unique<KrakenSource>();
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

View file

@ -0,0 +1,68 @@
#pragma once
#include "interfaces/price_source.hpp"
#include "sources/coincap_source.hpp"
#include "sources/kraken_source.hpp"
#include <memory>
#include <map>
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<void(Currency, uint64_t)> 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<IPriceSource> priceSource;
PriceSource currentSource;
function<void(Currency, uint64_t)> userCallback;
// For storing prices when no explicit source is set
map<Currency, uint64_t> prices;
map<Currency, uint32_t> lastUpdates;
};
} // namespace PriceNotify

View file

@ -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<void(Currency, uint64_t)> 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<JsonObject>()) {
uint64_t price = doc["bitcoin"].as<uint64_t>();
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

View file

@ -0,0 +1,36 @@
#pragma once
#include "../interfaces/price_source.hpp"
#include <ArduinoJson.h>
#include <esp_timer.h>
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<void(Currency, uint64_t)> 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<Currency, uint64_t> prices;
map<Currency, uint32_t> lastUpdates;
bool connected;
static CoinCapSource* instance; // For callback handling
};
} // namespace PriceNotify

View file

@ -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<void(Currency, uint64_t)> 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<JsonArray>() && doc.size() >= 4) {
JsonArray arr = doc.as<JsonArray>();
if (arr[2] == "ticker" && arr[3] == "XBT/USD") {
JsonObject tickerData = arr[1].as<JsonObject>();
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<float>() * 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

View file

@ -0,0 +1,37 @@
#pragma once
#include "../interfaces/price_source.hpp"
#include <ArduinoJson.h>
#include <esp_timer.h>
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<void(Currency, uint64_t)> 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<Currency, uint64_t> prices;
map<Currency, uint32_t> lastUpdates;
bool connected;
static KrakenSource* instance; // For callback handling
};
} // namespace PriceNotify

View file

@ -241,7 +241,7 @@ void workerTask(void *pvParameters) {
case TASK_PRICE_UPDATE: { case TASK_PRICE_UPDATE: {
uint currency = ScreenHandler::getCurrentCurrency(); uint currency = ScreenHandler::getCurrentCurrency();
uint price = getPrice(currency); uint price = priceManager.getPrice(static_cast<PriceNotify::Currency>(currency));
if (currentScreenValue == SCREEN_BTC_TICKER) { if (currentScreenValue == SCREEN_BTC_TICKER) {
taskEpdContent = parsePriceData(price, currency, preferences.getBool("suffixPrice", DEFAULT_SUFFIX_PRICE), taskEpdContent = parsePriceData(price, currency, preferences.getBool("suffixPrice", DEFAULT_SUFFIX_PRICE),

View file

@ -1,4 +1,5 @@
#include "v2_notify.hpp" #include "v2_notify.hpp"
#include "globals.hpp"
using namespace V2Notify; using namespace V2Notify;
@ -146,14 +147,15 @@ namespace V2Notify
} }
else if (doc["price"].is<JsonObject>()) else if (doc["price"].is<JsonObject>())
{ {
// Iterate through the key-value pairs of the "price" object // Iterate through the key-value pairs of the "price" object
for (JsonPair kv : doc["price"].as<JsonObject>()) for (JsonPair kv : doc["price"].as<JsonObject>())
{ {
const char *currency = kv.key().c_str(); const char *currency = kv.key().c_str();
uint newPrice = kv.value().as<uint>(); uint newPrice = kv.value().as<uint>();
processNewPrice(newPrice, getCurrencyChar(currency)); // Convert currency string to PriceNotify::Currency using data_handler's conversion
char currencyChar = getCurrencyChar(currency);
priceManager.processNewPrice(newPrice, static_cast<PriceNotify::Currency>(currencyChar));
} }
} }
} }

View file

@ -246,7 +246,7 @@ JsonDocument getStatusObject()
JsonObject conStatus = root["connectionStatus"].to<JsonObject>(); JsonObject conStatus = root["connectionStatus"].to<JsonObject>();
conStatus["price"] = isPriceNotifyConnected(); conStatus["price"] = priceManager.isConnected();
conStatus["blocks"] = isBlockNotifyConnected(); conStatus["blocks"] = isBlockNotifyConnected();
conStatus["V2"] = V2Notify::isV2NotifyConnected(); conStatus["V2"] = V2Notify::isV2NotifyConnected();
conStatus["nostr"] = nostrConnected(); conStatus["nostr"] = nostrConnected();

View file

@ -11,7 +11,7 @@
#include "lib/block_notify.hpp" #include "lib/block_notify.hpp"
#include "lib/led_handler.hpp" #include "lib/led_handler.hpp"
#include "lib/price_notify.hpp"
#include "lib/screen_handler.hpp" #include "lib/screen_handler.hpp"
#include "webserver/OneParamRewrite.hpp" #include "webserver/OneParamRewrite.hpp"
#include "lib/mining_pool/pool_factory.hpp" #include "lib/mining_pool/pool_factory.hpp"