[Feature] Adds mining pool screens #5

Merged
djuri merged 14 commits from kdmukai/btclock_v3:mining_stats_v2 into main 2024-12-20 00:11:02 +00:00
Contributor

Adds current hashrate and earnings screens for Braiins Pool and Ocean.

Depends on btclock/webui#2

Adds current hashrate and earnings screens for Braiins Pool and Ocean. Depends on https://git.btclock.dev/btclock/webui/pulls/2
kdmukai added 9 commits 2024-12-19 03:37:34 +00:00
Owner

Thanks for another great addition to the functionality!
I do have some suggestions for improvement:

  • in src/lib/mining_pool_stats_fetch.cpp (from line 30) you retrieve the miningPoolName preference multiple times, it's better to retrieve the value one time and store it in a property and the reuse that in the if/else-if statements.
  • Same for src/lib/screen_handler.cpp
    Maybe it's better to just add a getMiningPoolName() getter method to src/lib/mining_pool_stats_fetch.cpp to DRY
  • To prevent high RAM usage, don't allow BitAxe and Mining Pool Stats at the same time (My assumption is users would always only need to see one of either anyways)
  • Move the mining pool #define's to shared.hpp since defaults.hpp is meant for preference defaults
  • In src/lib/shared.hpp I think it's better to set the constants in the 70 range (so 70, 71)

I've already did a quick and dirty addition of the Noderunners pool, but since it's a solo pool (ckpool based), they don't (can't) show any earnings. The quick solution I used to skip the screen now is:

  if (preferences.getBool("miningPoolStats", DEFAULT_MINING_POOL_STATS_ENABLED))
  {
    addScreenMapping(SCREEN_MINING_POOL_STATS_HASHRATE, "Mining Pool Hashrate");
    if (preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME) != MINING_POOL_NAME_NODERUNNERS) {
      addScreenMapping(SCREEN_MINING_POOL_STATS_EARNINGS, "Mining Pool Earnings");
    }
  }

But I think this can be more efficient. It might make sense to define supported mining pools with interfaces and classes, like this:


struct PoolStats {
   std::string hashrate;
   std::optional<int64_t> dailyEarnings;
};

struct LogoData {
   const uint8_t* data;
   size_t width;
   size_t height;
};

class MiningPoolInterface {
public:
   virtual ~MiningPoolInterface() = default;
   virtual void prepareRequest(HTTPClient& http) const = 0;
   virtual std::string getApiUrl(const std::string& poolUser) const = 0;
   virtual PoolStats parseResponse(const JsonDocument& doc) const = 0;
   virtual LogoData getLogo() const = 0;
};

Implementation of BraiinsPool example:

class BraiinsPool : public MiningPoolInterface {
public:
    void prepareRequest(HTTPClient& http) const override {
        http.addHeader("Pool-Auth-Token", preferences.getString("miningPoolUser", DEFAULT_MINING_POOL_USER).c_str());
    }
    
    std::string getApiUrl(const std::string& poolUser) const override {
        return "https://pool.braiins.com/accounts/profile/json/btc/";
    }
    
    PoolStats parseResponse(const JsonDocument& doc) const override {
        std::string unit = doc["btc"]["hash_rate_unit"].as<std::string>();
        
        static const std::unordered_map<std::string, int> multipliers = {
            {"Zh/s", 21}, {"Eh/s", 18}, {"Ph/s", 15}, {"Th/s", 12},
            {"Gh/s", 9},  {"Mh/s", 6},  {"Kh/s", 3}
        };
        
        int multiplier = multipliers.at(unit);
        float hashValue = doc["btc"]["hash_rate_5m"].as<float>();
        
        return PoolStats{
            .hashrate = std::to_string(static_cast<int>(std::round(hashValue))) + std::string(multiplier, '0'),
            .dailyEarnings = static_cast<int64_t>(doc["btc"]["today_reward"].as<float>() * 100000000)
        };
    }
};

You can then declutter the fetch task a lot:

void taskMiningPoolStatsFetch(void *pvParameters) {
    for (;;) {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);

        std::string poolName = preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME);
        std::string poolUser = preferences.getString("miningPoolUser", DEFAULT_MINING_POOL_USER);
        
        auto poolInterface = createPoolInterface(poolName);
        if (!poolInterface) {
            Serial.println("Unknown mining pool: \"" + String(poolName.c_str()) + "\"");
            continue;
        }

        HTTPClient http;
        http.setUserAgent(USER_AGENT);
        poolInterface->prepareRequest(http);
        
        std::string apiUrl = poolInterface->getApiUrl(poolUser);
        http.begin(apiUrl.c_str());

        int httpCode = http.GET();
        if (httpCode == 200) {
            String payload = http.getString();
            JsonDocument doc;
            deserializeJson(doc, payload);

            Serial.println(doc.as<String>());
            
            PoolStats stats = poolInterface->parseResponse(doc);
            miningPoolStatsHashrate = stats.hashrate;
            
            if (stats.dailyEarnings) {
                miningPoolStatsDailyEarnings = *stats.dailyEarnings;
            } else {
                miningPoolStatsDailyEarnings = 0;  // or any other default value
            }

            if (workQueue != nullptr && 
                (getCurrentScreen() == SCREEN_MINING_POOL_STATS_HASHRATE || 
                 getCurrentScreen() == SCREEN_MINING_POOL_STATS_EARNINGS)) {
                WorkItem priceUpdate = {TASK_MINING_POOL_STATS_UPDATE, 0};
                xQueueSend(workQueue, &priceUpdate, portMAX_DELAY);
            }
        } else {
            Serial.print(F("Error retrieving mining pool data. HTTP status code: "));
            Serial.println(httpCode);
            Serial.println(apiUrl.c_str());
        }
        
        http.end();
    }
}

I'm looking for a method to add more icons without using to much extra space. It probably helps a lot to remove the "empty" pixels above and below the logo.
Also I'm looking to load only one of them in memory depending on the setting, and maybe loading them from file system.
And if they are stored on filesystem, hopefully they could be gzipped to.

Thanks for another great addition to the functionality! I do have some suggestions for improvement: - in `src/lib/mining_pool_stats_fetch.cpp` (from line 30) you retrieve the miningPoolName preference multiple times, it's better to retrieve the value one time and store it in a property and the reuse that in the if/else-if statements. - Same for `src/lib/screen_handler.cpp` Maybe it's better to just add a getMiningPoolName() getter method to `src/lib/mining_pool_stats_fetch.cpp` to DRY - To prevent high RAM usage, don't allow BitAxe and Mining Pool Stats at the same time (My assumption is users would always only need to see one of either anyways) - Move the mining pool #define's to `shared.hpp` since `defaults.hpp` is meant for preference defaults - In `src/lib/shared.hpp` I think it's better to set the constants in the 70 range (so 70, 71) I've already did a quick and dirty addition of the Noderunners pool, but since it's a solo pool (ckpool based), they don't (can't) show any earnings. The quick solution I used to skip the screen now is: ```cpp if (preferences.getBool("miningPoolStats", DEFAULT_MINING_POOL_STATS_ENABLED)) { addScreenMapping(SCREEN_MINING_POOL_STATS_HASHRATE, "Mining Pool Hashrate"); if (preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME) != MINING_POOL_NAME_NODERUNNERS) { addScreenMapping(SCREEN_MINING_POOL_STATS_EARNINGS, "Mining Pool Earnings"); } } ``` But I think this can be more efficient. It might make sense to define supported mining pools with interfaces and classes, like this: ```cpp struct PoolStats { std::string hashrate; std::optional<int64_t> dailyEarnings; }; struct LogoData { const uint8_t* data; size_t width; size_t height; }; class MiningPoolInterface { public: virtual ~MiningPoolInterface() = default; virtual void prepareRequest(HTTPClient& http) const = 0; virtual std::string getApiUrl(const std::string& poolUser) const = 0; virtual PoolStats parseResponse(const JsonDocument& doc) const = 0; virtual LogoData getLogo() const = 0; }; ``` Implementation of BraiinsPool example: ```cpp class BraiinsPool : public MiningPoolInterface { public: void prepareRequest(HTTPClient& http) const override { http.addHeader("Pool-Auth-Token", preferences.getString("miningPoolUser", DEFAULT_MINING_POOL_USER).c_str()); } std::string getApiUrl(const std::string& poolUser) const override { return "https://pool.braiins.com/accounts/profile/json/btc/"; } PoolStats parseResponse(const JsonDocument& doc) const override { std::string unit = doc["btc"]["hash_rate_unit"].as<std::string>(); static const std::unordered_map<std::string, int> multipliers = { {"Zh/s", 21}, {"Eh/s", 18}, {"Ph/s", 15}, {"Th/s", 12}, {"Gh/s", 9}, {"Mh/s", 6}, {"Kh/s", 3} }; int multiplier = multipliers.at(unit); float hashValue = doc["btc"]["hash_rate_5m"].as<float>(); return PoolStats{ .hashrate = std::to_string(static_cast<int>(std::round(hashValue))) + std::string(multiplier, '0'), .dailyEarnings = static_cast<int64_t>(doc["btc"]["today_reward"].as<float>() * 100000000) }; } }; ``` You can then declutter the fetch task a lot: ```cpp void taskMiningPoolStatsFetch(void *pvParameters) { for (;;) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); std::string poolName = preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME); std::string poolUser = preferences.getString("miningPoolUser", DEFAULT_MINING_POOL_USER); auto poolInterface = createPoolInterface(poolName); if (!poolInterface) { Serial.println("Unknown mining pool: \"" + String(poolName.c_str()) + "\""); continue; } HTTPClient http; http.setUserAgent(USER_AGENT); poolInterface->prepareRequest(http); std::string apiUrl = poolInterface->getApiUrl(poolUser); http.begin(apiUrl.c_str()); int httpCode = http.GET(); if (httpCode == 200) { String payload = http.getString(); JsonDocument doc; deserializeJson(doc, payload); Serial.println(doc.as<String>()); PoolStats stats = poolInterface->parseResponse(doc); miningPoolStatsHashrate = stats.hashrate; if (stats.dailyEarnings) { miningPoolStatsDailyEarnings = *stats.dailyEarnings; } else { miningPoolStatsDailyEarnings = 0; // or any other default value } if (workQueue != nullptr && (getCurrentScreen() == SCREEN_MINING_POOL_STATS_HASHRATE || getCurrentScreen() == SCREEN_MINING_POOL_STATS_EARNINGS)) { WorkItem priceUpdate = {TASK_MINING_POOL_STATS_UPDATE, 0}; xQueueSend(workQueue, &priceUpdate, portMAX_DELAY); } } else { Serial.print(F("Error retrieving mining pool data. HTTP status code: ")); Serial.println(httpCode); Serial.println(apiUrl.c_str()); } http.end(); } } ``` I'm looking for a method to add more icons without using to much extra space. It probably helps a lot to remove the "empty" pixels above and below the logo. Also I'm looking to load only one of them in memory depending on the setting, and maybe loading them from file system. And if they are stored on filesystem, hopefully they could be gzipped to.
kdmukai added 2 commits 2024-12-19 19:54:50 +00:00
Author
Contributor
  • Reduced miningPoolName lookups from Preferences.
  • Got myself into some import hell trying to centralize other lookups; gave up.
  • Bitaxe + mining pool stats: I actually run two Bitaxes pointed at two different solo pools and am currently home mining to both Braiins and Ocean. It was a hard call to not wire it so that both Braiins and Ocean could be in the rotation at the same time! Anyway, if the memory constraints don't become debilitating, I'd prefer to be able to run with Bitaxe and mining pool stats active at the same time.
  • Removed the mining pool #define constants altogether as they didn't provide the wins I was originally hoping for (plus more import hell meant I couldn't figure out how to access them in other places). So now it's just "braiins" and "ocean" magic strings in all the conditionals. Not ideal, but at least the input side if fixed to the webui droplist.
  • Screen constant ids updated to 70, 71.

As I mentioned on telegram, I am in favor of the Interface refactor you suggest above. Unfortunately I need to switch gears now and based on my C++ struggles throughout, I'm not confident that even an easy refactor will go smoothly and quickly!

* Reduced `miningPoolName` lookups from `Preferences`. * Got myself into some import hell trying to centralize other lookups; gave up. * Bitaxe + mining pool stats: I actually run two Bitaxes pointed at two different solo pools and am currently home mining to both Braiins and Ocean. It was a hard call to not wire it so that both Braiins and Ocean could be in the rotation at the same time! Anyway, if the memory constraints don't become debilitating, I'd prefer to be able to run with Bitaxe and mining pool stats active at the same time. * Removed the mining pool #define constants altogether as they didn't provide the wins I was originally hoping for (plus more import hell meant I couldn't figure out how to access them in other places). So now it's just "braiins" and "ocean" magic strings in all the conditionals. Not ideal, but at least the input side if fixed to the webui droplist. * Screen constant ids updated to 70, 71. As I mentioned on telegram, I am in favor of the `Interface` refactor you suggest above. Unfortunately I need to switch gears now and based on my C++ struggles throughout, I'm not confident that even an easy refactor will go smoothly and quickly!
djuri added 1 commit 2024-12-20 00:08:19 +00:00
djuri merged commit c7ea2f3e4d into main 2024-12-20 00:11:02 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: btclock/btclock_v3#5
No description provided.