Big refactor of mining pool support, optimization of existing icons

This commit is contained in:
Djuri Baars 2024-12-20 01:08:03 +01:00
parent f9aa593f0b
commit 814cd234a9
23 changed files with 772 additions and 1190 deletions

View file

@ -1,7 +1,7 @@
#include "mining_pool_stats_handler.hpp"
#include <iostream>
std::array<std::string, NUM_SCREENS> parseMiningPoolStatsHashRate(std::string miningPoolName, std::string text)
std::array<std::string, NUM_SCREENS> parseMiningPoolStatsHashRate(std::string text)
{
std::array<std::string, NUM_SCREENS> ret;
ret.fill(""); // Initialize all elements to empty strings
@ -55,31 +55,19 @@ std::array<std::string, NUM_SCREENS> parseMiningPoolStatsHashRate(std::string mi
ret[NUM_SCREENS - 1] = label;
// Set the mining pool logo
if (miningPoolName == "ocean") {
ret[0] = "mdi:ocean_logo";
} else if (miningPoolName == "braiins") {
ret[0] = "mdi:braiins_logo";
}
ret[0] = "mdi:miningpool";
return ret;
}
std::array<std::string, NUM_SCREENS> parseMiningPoolStatsDailyEarnings(std::string miningPoolName, int sats)
std::array<std::string, NUM_SCREENS> parseMiningPoolStatsDailyEarnings(int sats, std::string label)
{
std::array<std::string, NUM_SCREENS> ret;
ret.fill(""); // Initialize all elements to empty strings
std::string satsDisplay = std::to_string(sats);
std::string label;
if (miningPoolName == "braiins") {
// fpps guarantees payout; report current daily earnings
label = "sats/earned";
} else {
// TIDES can only estimate earnings on the next block Ocean finds
label = "sats/block";
}
if (sats >= 100000000) {
// A whale mining 1+ BTC per day! No decimal points; whales scoff at such things.
@ -121,12 +109,7 @@ std::array<std::string, NUM_SCREENS> parseMiningPoolStatsDailyEarnings(std::stri
ret[NUM_SCREENS - 1] = label;
// Set the mining pool logo
if (miningPoolName == "ocean") {
ret[0] = "mdi:ocean_logo";
} else if (miningPoolName == "braiins") {
ret[0] = "mdi:braiins_logo";
}
ret[0] = "mdi:miningpool";
return ret;
}

View file

@ -1,5 +1,5 @@
#include <array>
#include <string>
std::array<std::string, NUM_SCREENS> parseMiningPoolStatsHashRate(std::string miningPoolName, std::string text);
std::array<std::string, NUM_SCREENS> parseMiningPoolStatsDailyEarnings(std::string miningPoolName, int sats);
std::array<std::string, NUM_SCREENS> parseMiningPoolStatsHashRate(std::string text);
std::array<std::string, NUM_SCREENS> parseMiningPoolStatsDailyEarnings(int sats, std::string label);

File diff suppressed because it is too large Load diff

View file

@ -340,7 +340,9 @@ void setupPreferences()
if (preferences.getBool("miningPoolStats", DEFAULT_MINING_POOL_STATS_ENABLED))
{
addScreenMapping(SCREEN_MINING_POOL_STATS_HASHRATE, "Mining Pool Hashrate");
addScreenMapping(SCREEN_MINING_POOL_STATS_EARNINGS, "Mining Pool Earnings");
if (getMiningPool()->supportsDailyEarnings()) {
addScreenMapping(SCREEN_MINING_POOL_STATS_EARNINGS, "Mining Pool Earnings");
}
}
}

View file

@ -604,26 +604,36 @@ void renderIcon(const uint dispNum, const String &text, bool partial)
displays[dispNum].setTextColor(getFgColor());
uint iconIndex = 0;
uint width = 122;
uint height = 122;
if (text.endsWith("rocket")) {
iconIndex = 1;
}
else if (text.endsWith("lnbolt")) {
iconIndex = 3;
iconIndex = 2;
}
else if (text.endsWith("bitaxe")) {
iconIndex = 4;
width = 122;
height = 250;
iconIndex = 3;
}
else if (text.endsWith("ocean_logo")) {
iconIndex = 5;
}
else if (text.endsWith("braiins_logo")) {
iconIndex = 6;
else if (text.endsWith("miningpool")) {
LogoData logo = getMiningPoolLogo();
int x_offset = (displays[dispNum].width() - logo.width) / 2;
int y_offset = (displays[dispNum].height() - logo.height) / 2;
// Close the file
displays[dispNum].drawInvertedBitmap(x_offset,y_offset, logo.data, logo.width, logo.height, getFgColor());
return;
}
int x_offset = (displays[dispNum].width() - width) / 2;
int y_offset = (displays[dispNum].height() - height) / 2;
displays[dispNum].drawInvertedBitmap(0,0, epd_icons_allArray[iconIndex], 122, 250, getFgColor());
displays[dispNum].drawInvertedBitmap(x_offset,y_offset, epd_icons_allArray[iconIndex], width, height, getFgColor());
// displays[dispNum].drawInvertedBitmap(0,0, getOceanIcon(), 122, 250, getFgColor());
@ -633,6 +643,7 @@ void renderIcon(const uint dispNum, const String &text, bool partial)
void renderQr(const uint dispNum, const String &text, bool partial)
{
#ifdef USE_QR

View file

@ -14,6 +14,7 @@
#include "lib/config.hpp"
#include "lib/shared.hpp"
#include "icons/icons.h"
#include "mining_pool_stats_fetch.hpp"
#ifdef USE_QR
#include "qrcodegen.h"

View file

@ -0,0 +1,32 @@
#include "brains_pool.hpp"
void BraiinsPool::prepareRequest(HTTPClient& http) const {
http.addHeader("Pool-Auth-Token", poolUser.c_str());
}
std::string BraiinsPool::getApiUrl() const {
return "https://pool.braiins.com/accounts/profile/json/btc/";
}
PoolStats BraiinsPool::parseResponse(const JsonDocument &doc) const
{
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)};
}
LogoData BraiinsPool::getLogo() const {
return LogoData{
.data = epd_icons_allArray[5],
.width = 122,
.height = 250
};
}

View file

@ -0,0 +1,18 @@
#pragma once
#include "lib/mining_pool/mining_pool_interface.hpp"
#include <icons/icons.h>
class BraiinsPool : public MiningPoolInterface
{
public:
void setPoolUser(const std::string &user) override { poolUser = user; }
void prepareRequest(HTTPClient &http) const override;
std::string getApiUrl() const override;
PoolStats parseResponse(const JsonDocument &doc) const override;
LogoData getLogo() const override;
bool supportsDailyEarnings() const override { return true; }
std::string getDailyEarningsLabel() const override { return "sats/earned"; }
private:
static int getHashrateMultiplier(const std::string &unit);
};

View file

@ -0,0 +1,10 @@
#pragma once
#include <cstdint>
#include <stddef.h>
struct LogoData {
const uint8_t* data;
size_t width;
size_t height;
};

View file

@ -0,0 +1,21 @@
#pragma once
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include "pool_stats.hpp"
#include "logo_data.hpp"
class MiningPoolInterface {
public:
virtual ~MiningPoolInterface() = default;
virtual void setPoolUser(const std::string& user) = 0;
virtual void prepareRequest(HTTPClient& http) const = 0;
virtual std::string getApiUrl() const = 0;
virtual PoolStats parseResponse(const JsonDocument& doc) const = 0;
virtual LogoData getLogo() const = 0;
virtual bool supportsDailyEarnings() const = 0;
virtual std::string getDailyEarningsLabel() const = 0;
protected:
std::string poolUser;
};

View file

@ -0,0 +1,39 @@
// src/noderunners/noderunners_pool.cpp
#include "noderunners_pool.hpp"
void NodeRunnersPool::prepareRequest(HTTPClient& http) const {
// Empty as NodeRunners doesn't need special headers
}
std::string NodeRunnersPool::getApiUrl() const {
return "https://pool.noderunners.network/api/v1/users/" + poolUser;
}
PoolStats NodeRunnersPool::parseResponse(const JsonDocument& doc) const {
std::string hashrateStr = doc["hashrate1m"].as<std::string>();
char unit = hashrateStr.back();
std::string value = hashrateStr.substr(0, hashrateStr.size() - 1);
int multiplier = getHashrateMultiplier(unit);
return PoolStats{
.hashrate = value + std::string(multiplier, '0'),
.dailyEarnings = std::nullopt
};
}
LogoData NodeRunnersPool::getLogo() const {
return LogoData {
.data = epd_icons_allArray[6],
.width = 122,
.height = 122
};
}
int NodeRunnersPool::getHashrateMultiplier(char unit) {
static const std::unordered_map<char, int> multipliers = {
{'Z', 21}, {'E', 18}, {'P', 15}, {'T', 12},
{'G', 9}, {'M', 6}, {'K', 3}
};
return multipliers.at(unit);
}

View file

@ -0,0 +1,20 @@
#pragma once
#include "lib/mining_pool/mining_pool_interface.hpp"
#include <icons/icons.h>
class NodeRunnersPool : public MiningPoolInterface {
public:
void setPoolUser(const std::string& user) override { poolUser = user; }
void prepareRequest(HTTPClient& http) const override;
std::string getApiUrl() const override;
PoolStats parseResponse(const JsonDocument& doc) const override;
LogoData getLogo() const override;
bool supportsDailyEarnings() const override { return false; }
std::string getDailyEarningsLabel() const override { return ""; }
private:
static int getHashrateMultiplier(char unit);
};

View file

@ -0,0 +1,26 @@
#include "ocean_pool.hpp"
void OceanPool::prepareRequest(HTTPClient& http) const {
// Empty as Ocean doesn't need special headers
}
std::string OceanPool::getApiUrl() const {
return "https://api.ocean.xyz/v1/statsnap/" + poolUser;
}
PoolStats OceanPool::parseResponse(const JsonDocument& doc) const {
return PoolStats{
.hashrate = doc["result"]["hashrate_300s"].as<std::string>(),
.dailyEarnings = static_cast<int64_t>(
doc["result"]["estimated_earn_next_block"].as<float>() * 100000000
)
};
}
LogoData OceanPool::getLogo() const {
return LogoData{
.data = epd_icons_allArray[4],
.width = 122,
.height = 122
};
}

View file

@ -0,0 +1,15 @@
#pragma once
#include "lib/mining_pool/mining_pool_interface.hpp"
#include <icons/icons.h>
class OceanPool : public MiningPoolInterface {
public:
void setPoolUser(const std::string& user) override { poolUser = user; }
void prepareRequest(HTTPClient& http) const override;
std::string getApiUrl() const override;
PoolStats parseResponse(const JsonDocument& doc) const override;
LogoData getLogo() const override;
bool supportsDailyEarnings() const override { return true; }
std::string getDailyEarningsLabel() const override { return "sats/block"; }
};

View file

@ -0,0 +1,20 @@
#include "pool_factory.hpp"
const char* PoolFactory::MINING_POOL_NAME_OCEAN = "ocean";
const char* PoolFactory::MINING_POOL_NAME_NODERUNNERS = "noderunners";
const char* PoolFactory::MINING_POOL_NAME_BRAIINS = "braiins";
std::unique_ptr<MiningPoolInterface> PoolFactory::createPool(const std::string& poolName) {
static const std::unordered_map<std::string, std::function<std::unique_ptr<MiningPoolInterface>()>> poolFactories = {
{MINING_POOL_NAME_OCEAN, []() { return std::make_unique<OceanPool>(); }},
{MINING_POOL_NAME_NODERUNNERS, []() { return std::make_unique<NodeRunnersPool>(); }},
{MINING_POOL_NAME_BRAIINS, []() { return std::make_unique<BraiinsPool>(); }}
};
auto it = poolFactories.find(poolName);
if (it == poolFactories.end()) {
return nullptr;
}
return it->second();
}

View file

@ -0,0 +1,35 @@
#pragma once
#include "mining_pool_interface.hpp"
#include <memory>
#include <string>
#include "noderunners/noderunners_pool.hpp"
#include "braiins/brains_pool.hpp"
#include "ocean/ocean_pool.hpp"
class PoolFactory {
public:
static std::unique_ptr<MiningPoolInterface> createPool(const std::string& poolName);
static std::vector<std::string> getAvailablePools() {
return {
MINING_POOL_NAME_OCEAN,
MINING_POOL_NAME_NODERUNNERS,
MINING_POOL_NAME_BRAIINS
};
}
static std::string getAvailablePoolsAsString() {
const auto pools = getAvailablePools();
std::string result;
for (size_t i = 0; i < pools.size(); ++i) {
result += pools[i];
if (i < pools.size() - 1) {
result += ", ";
}
}
return result;
}
private:
static const char* MINING_POOL_NAME_OCEAN;
static const char* MINING_POOL_NAME_NODERUNNERS;
static const char* MINING_POOL_NAME_BRAIINS;
};

View file

@ -0,0 +1,10 @@
#pragma once
#include <string>
#include <optional>
struct PoolStats {
std::string hashrate;
std::optional<int64_t> dailyEarnings;
};

View file

@ -24,68 +24,38 @@ void taskMiningPoolStatsFetch(void *pvParameters)
HTTPClient http;
http.setUserAgent(USER_AGENT);
String miningPoolStatsApiUrl;
miningPoolName = preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME).c_str();
std::string poolName = preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME).c_str();
std::string poolUser = preferences.getString("miningPoolUser", DEFAULT_MINING_POOL_USER).c_str();
std::string httpHeaderKey = "";
std::string httpHeaderValue;
if (miningPoolName == "ocean") {
miningPoolStatsApiUrl = "https://api.ocean.xyz/v1/statsnap/" + preferences.getString("miningPoolUser", DEFAULT_MINING_POOL_USER);
}
else if (miningPoolName == "braiins") {
miningPoolStatsApiUrl = "https://pool.braiins.com/accounts/profile/json/btc/";
httpHeaderKey = "Pool-Auth-Token";
httpHeaderValue = preferences.getString("miningPoolUser", DEFAULT_MINING_POOL_USER).c_str();
}
else
auto poolInterface = PoolFactory::createPool(poolName);
if (!poolInterface)
{
// Unknown mining pool / missing implementation
Serial.println("Unknown mining pool: \"" + String(poolName.c_str()) + "\"");
continue;
}
http.begin(miningPoolStatsApiUrl.c_str());
if (httpHeaderKey.length() > 0) {
http.addHeader(httpHeaderKey.c_str(), httpHeaderValue.c_str());
}
poolInterface->setPoolUser(poolUser);
std::string apiUrl = poolInterface->getApiUrl();
http.begin(apiUrl.c_str());
poolInterface->prepareRequest(http);
int httpCode = http.GET();
if (httpCode == 200)
{
String payload = http.getString();
JsonDocument doc;
deserializeJson(doc, payload);
if (miningPoolName == "ocean") {
miningPoolStatsHashrate = doc["result"]["hashrate_300s"].as<std::string>();
miningPoolStatsDailyEarnings = int(doc["result"]["estimated_earn_next_block"].as<float>() * 100000000);
PoolStats stats = poolInterface->parseResponse(doc);
miningPoolStatsHashrate = stats.hashrate;
if (stats.dailyEarnings)
{
miningPoolStatsDailyEarnings = *stats.dailyEarnings;
}
else if (miningPoolName == "braiins") {
// Reports hashrate in specific hashrate units (e.g. Gh/s); we want raw total hashes per second.
std::string hashrateUnit = doc["btc"]["hash_rate_unit"].as<std::string>();
int multiplier = 0;
if (hashrateUnit == "Zh/s") {
multiplier = 21;
} else if (hashrateUnit == "Eh/s") {
multiplier = 18;
} else if (hashrateUnit == "Ph/s") {
multiplier = 15;
} else if (hashrateUnit == "Th/s") {
multiplier = 12;
} else if (hashrateUnit == "Gh/s") {
multiplier = 9;
} else if (hashrateUnit == "Mh/s") {
multiplier = 6;
} else if (hashrateUnit == "Kh/s") {
multiplier = 3;
}
// Add zeroes to pad to a full h/s output
miningPoolStatsHashrate = std::to_string(static_cast<int>(std::round(doc["btc"]["hash_rate_5m"].as<float>()))) + std::string(multiplier, '0');
miningPoolStatsDailyEarnings = int(doc["btc"]["today_reward"].as<float>() * 100000000);
else
{
miningPoolStatsDailyEarnings = 0; // or any other default value
}
if (workQueue != nullptr && (getCurrentScreen() == SCREEN_MINING_POOL_STATS_HASHRATE || getCurrentScreen() == SCREEN_MINING_POOL_STATS_EARNINGS))
@ -99,7 +69,6 @@ void taskMiningPoolStatsFetch(void *pvParameters)
Serial.print(
F("Error retrieving mining pool data. HTTP status code: "));
Serial.println(httpCode);
Serial.println(miningPoolStatsApiUrl);
}
}
}
@ -110,4 +79,21 @@ void setupMiningPoolStatsFetchTask()
&miningPoolStatsFetchTaskHandle);
xTaskNotifyGive(miningPoolStatsFetchTaskHandle);
}
}
std::unique_ptr<MiningPoolInterface>& getMiningPool()
{
static std::unique_ptr<MiningPoolInterface> currentMiningPool;
if (!currentMiningPool) {
std::string poolName = preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME).c_str();
currentMiningPool = PoolFactory::createPool(poolName);
}
return currentMiningPool;
}
LogoData getMiningPoolLogo()
{
return getMiningPool()->getLogo();
}

View file

@ -2,6 +2,7 @@
#include <Arduino.h>
#include <HTTPClient.h>
#include "mining_pool/pool_factory.hpp"
#include "lib/config.hpp"
#include "lib/shared.hpp"
@ -13,3 +14,6 @@ void taskMiningPoolStatsFetch(void *pvParameters);
std::string getMiningPoolStatsHashRate();
int getMiningPoolStatsDailyEarnings();
std::unique_ptr<MiningPoolInterface>& getMiningPool();
LogoData getMiningPoolLogo();

View file

@ -38,10 +38,10 @@ void workerTask(void *pvParameters) {
case TASK_MINING_POOL_STATS_UPDATE: {
if (getCurrentScreen() == SCREEN_MINING_POOL_STATS_HASHRATE) {
taskEpdContent =
parseMiningPoolStatsHashRate(preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME).c_str(), getMiningPoolStatsHashRate());
parseMiningPoolStatsHashRate(getMiningPoolStatsHashRate());
} else if (getCurrentScreen() == SCREEN_MINING_POOL_STATS_EARNINGS) {
taskEpdContent =
parseMiningPoolStatsDailyEarnings(preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME).c_str(), getMiningPoolStatsDailyEarnings());
parseMiningPoolStatsDailyEarnings(getMiningPoolStatsDailyEarnings(), getMiningPool()->getDailyEarningsLabel());
}
setEpdContent(taskEpdContent);
break;

View file

@ -42,26 +42,15 @@ const PROGMEM int SCREEN_BLOCK_FEE_RATE = 6;
const PROGMEM int SCREEN_SATS_PER_CURRENCY = 10;
const PROGMEM int SCREEN_BTC_TICKER = 20;
// const PROGMEM int SCREEN_BTC_TICKER_USD = 20;
// const PROGMEM int SCREEN_BTC_TICKER_EUR = 21;
// const PROGMEM int SCREEN_BTC_TICKER_GBP = 22;
// const PROGMEM int SCREEN_BTC_TICKER_JPY = 23;
// const PROGMEM int SCREEN_BTC_TICKER_AUD = 24;
// const PROGMEM int SCREEN_BTC_TICKER_CAD = 25;
const PROGMEM int SCREEN_MARKET_CAP = 30;
// const PROGMEM int SCREEN_MARKET_CAP_USD = 30;
// const PROGMEM int SCREEN_MARKET_CAP_EUR = 31;
// const PROGMEM int SCREEN_MARKET_CAP_GBP = 32;
// const PROGMEM int SCREEN_MARKET_CAP_JPY = 33;
// const PROGMEM int SCREEN_MARKET_CAP_AUD = 34;
// const PROGMEM int SCREEN_MARKET_CAP_CAD = 35;
const PROGMEM int SCREEN_MINING_POOL_STATS_HASHRATE = 70;
const PROGMEM int SCREEN_MINING_POOL_STATS_EARNINGS = 71;
const PROGMEM int SCREEN_BITAXE_HASHRATE = 80;
const PROGMEM int SCREEN_BITAXE_BESTDIFF = 81;
const PROGMEM int SCREEN_MINING_POOL_STATS_HASHRATE = 70;
const PROGMEM int SCREEN_MINING_POOL_STATS_EARNINGS = 71;
const PROGMEM int SCREEN_COUNTDOWN = 98;
const PROGMEM int SCREEN_CUSTOM = 99;
@ -94,4 +83,15 @@ struct ScreenMapping {
};
String calculateSHA256(uint8_t* data, size_t len);
String calculateSHA256(WiFiClient *stream, size_t contentLength);
String calculateSHA256(WiFiClient *stream, size_t contentLength);
namespace ArduinoJson {
template <typename T>
struct Converter<std::vector<T>> {
static void toJson(const std::vector<T>& src, JsonVariant dst) {
JsonArray array = dst.to<JsonArray>();
for (T item : src)
array.add(item);
}
};
}

View file

@ -717,11 +717,10 @@ void onApiSettingsGet(AsyncWebServerRequest *request)
root["miningPoolStats"] = preferences.getBool("miningPoolStats", DEFAULT_MINING_POOL_STATS_ENABLED);
root["miningPoolName"] = preferences.getString("miningPoolName", DEFAULT_MINING_POOL_NAME);
root["miningPoolUser"] = preferences.getString("miningPoolUser", DEFAULT_MINING_POOL_USER);
root["availablePools"] = PoolFactory::getAvailablePools();
root["httpAuthEnabled"] = preferences.getBool("httpAuthEnabled", DEFAULT_HTTP_AUTH_ENABLED);
root["httpAuthUser"] = preferences.getString("httpAuthUser", DEFAULT_HTTP_AUTH_USERNAME);
root["httpAuthPass"] = preferences.getString("httpAuthPass", DEFAULT_HTTP_AUTH_PASSWORD);
#ifdef HAS_FRONTLIGHT
root["hasFrontlight"] = true;
root["flDisable"] = preferences.getBool("flDisable");
@ -755,17 +754,8 @@ void onApiSettingsGet(AsyncWebServerRequest *request)
#endif
JsonArray screens = root["screens"].to<JsonArray>();
JsonArray actCurrencies = root["actCurrencies"].to<JsonArray>();
for (const auto &str : getActiveCurrencies())
{
actCurrencies.add(str);
}
JsonArray availableCurrencies = root["availableCurrencies"].to<JsonArray>();
for (const auto &str : getAvailableCurrencies())
{
availableCurrencies.add(str);
}
root["actCurrencies"] = getActiveCurrencies();
root["availableCurrencies"] = getAvailableCurrencies();
std::vector<ScreenMapping> screenNameMap = getScreenNameMap();

View file

@ -14,6 +14,7 @@
#include "lib/price_notify.hpp"
#include "lib/screen_handler.hpp"
#include "webserver/OneParamRewrite.hpp"
#include "lib/mining_pool/pool_factory.hpp"
extern TaskHandle_t eventSourceTaskHandle;