Jak kopać Malmar Coin?

Kompletny przewodnik kopania MLM na ESP32

⚡ NOWOŚĆ

Flash przez przeglądarkę — bez Arduino IDE

Podłącz ESP32 kablem USB, kliknij „Flashuj", wpisz dane WiFi — gotowe. Zero kompilowania, zero bibliotek, 2 minuty.

Wolisz starą drogę? Pełna instrukcja Arduino IDE poniżej — działa, ale trzeba zainstalować środowisko i biblioteki.

Co potrzebujesz?

ESP32

Płytka ESP32 (WROOM, DevKit, NodeMCU)

WiFi

Połączenie z internetem

Arduino IDE

Do wgrania kodu na ESP32

Portfel

Adres MLM do odbierania nagród

Krok po kroku

1

Utwórz portfel

Najpierw potrzebujesz adresu portfela, na który będą wpływać wykopane monety.

Utwórz portfel
2

Zainstaluj Arduino IDE

Pobierz i zainstaluj Arduino IDE ze strony oficjalnej.

Pobierz Arduino IDE
Dodaj obsługę ESP32:
  1. Otwórz Arduino IDE
  2. Przejdź do File → Preferences
  3. W polu "Additional Board Manager URLs" wklej: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  4. Przejdź do Tools → Board → Boards Manager
  5. Wyszukaj "ESP32" i zainstaluj
3

Zainstaluj bibliotekę ArduinoJson

Miner wymaga biblioteki ArduinoJson do komunikacji z poolem.

  1. W Arduino IDE przejdź do Sketch → Include Library → Manage Libraries
  2. Wyszukaj "ArduinoJson"
  3. Zainstaluj najnowszą wersję (autor: Benoit Blanchon)
4

Skopiuj kod minera

Skopiuj poniższy kod i wklej do Arduino IDE.

Ważne! Zmień dane WiFi i adres portfela w kodzie!
/*
 * MalmarCoin ESP32 Dual-Core Miner
 * Wersja: 3.3.1
 *
 * Wymagane biblioteki (Menedzer bibliotek Arduino):
 *   - ArduinoJson  (Benoit Blanchon)
 *   - WiFiClientSecure (wbudowana)
 *
 * Plytka: ESP32 Dev Module
 */

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include "mbedtls/sha256.h"

// ============================================
// KONFIGURACJA — ZMIEN NA SWOJE DANE
// ============================================

const char* WIFI_SSID      = "TWOJA_SIEC_WIFI";
const char* WIFI_PASSWORD  = "TWOJE_HASLO_WIFI";

const char* HOST           = "malmar.portaleai.pl";
const char* WALLET_ADDRESS = "TWOJ_ADRES_PORTFELA_MLM";
const char* DEVICE_NAME    = "ESP32-Miner-1";
const char* FIRMWARE_VER   = "3.3.1";

const int MAX_HASHRATE_PER_CORE  = 0;
const unsigned long TELEMETRY_INTERVAL = 30000;

#define LED_PIN 2

// ============================================
// ZMIENNE GLOBALNE
// ============================================

String deviceId;
SemaphoreHandle_t xMutex;

volatile bool          miningActive  = false;
volatile bool          solutionFound = false;
volatile unsigned long foundNonce    = 0;

char jobId[64]          = {0};
char blockTemplate[128] = {0};
int  shareDifficulty    = 3;

volatile unsigned long hashCount[2]      = {0, 0};
volatile unsigned long statsHashCount[2] = {0, 0};

unsigned long statsWindowStart  = 0;
unsigned long lastJobTime       = 0;
unsigned long lastStatsTime     = 0;
unsigned long lastBlinkTime     = 0;
unsigned long lastTelemetryTime = 0;
bool          ledState          = false;

TaskHandle_t miningTask0;
TaskHandle_t miningTask1;

// ============================================
// TEMPERATURA
// ============================================

float readCpuTempC() {
    float raw = temperatureRead();
    if (raw > 120.0f) return (raw - 32.0f) * 5.0f / 9.0f;
    return raw;
}

// ============================================
// CPU LOAD — pomiar przez licznik idle taska
// Osobny task niskiego priorytetu zlicza ile razy
// zdąży się wykonać — im mniej, tym większe obciążenie
// ============================================

volatile unsigned long idleCounter    = 0;
volatile float         cpuLoadPercent = 0.0f;
unsigned long          idleBaseline   = 0;  // wartość przy 0% obciążeniu

void cpuMeasureTask(void* param) {
    // Kalibracja — zmierz baseline przez 1s przy bezczynności
    unsigned long start = millis();
    unsigned long cnt   = 0;
    while (millis() - start < 1000) { cnt++; taskYIELD(); }
    idleBaseline = cnt > 0 ? cnt : 1;

    while (true) {
        unsigned long t0 = millis();
        unsigned long c0 = 0;
        while (millis() - t0 < 1000) { c0++; taskYIELD(); }
        float ratio = (float)c0 / (float)idleBaseline;
        cpuLoadPercent = max(0.0f, min(100.0f, (1.0f - ratio) * 100.0f));
        idleCounter++;
    }
}

// ============================================
// SHA256 — każdy task ma WŁASNY kontekst (FIX!)
// Nie używamy globalnego sha_ctx — race condition!
// ============================================

void computeHash(mbedtls_sha256_context* ctx,
                 const char* tpl, unsigned long nonce, char* outHex) {
    char input[200];
    int len = snprintf(input, sizeof(input), "%s%lu", tpl, nonce);
    unsigned char hashBytes[32];
    mbedtls_sha256_starts(ctx, 0);
    mbedtls_sha256_update(ctx, (const unsigned char*)input, len);
    mbedtls_sha256_finish(ctx, hashBytes);
    for (int i = 0; i < 32; i++) sprintf(outHex + (i*2), "%02x", hashBytes[i]);
    outHex[64] = '\0';
}

bool meetsDifficulty(const char* hexHash, int diff) {
    for (int i = 0; i < diff; i++) if (hexHash[i] != '0') return false;
    return true;
}

// ============================================
// MINING TASK (dual-core)
// Każdy task ma lokalny sha_ctx — thread safe!
// ============================================

void miningTaskFunction(void* parameter) {
    int coreId = (int)parameter;
    char hashBuf[65];

    // Lokalny kontekst SHA — NIE współdzielony (FIX race condition)
    mbedtls_sha256_context localCtx;
    mbedtls_sha256_init(&localCtx);

    unsigned long rateWindowStart = millis();
    unsigned long rateCount       = 0;
    unsigned long hashIter        = 0;

    while (true) {
        if (!miningActive || solutionFound || blockTemplate[0] == '\0') {
            vTaskDelay(pdMS_TO_TICKS(50));
            rateWindowStart = millis();
            rateCount = 0;
            hashIter  = 0;
            continue;
        }

        char tpl[128];
        strncpy(tpl, blockTemplate, sizeof(tpl) - 1);
        tpl[sizeof(tpl) - 1] = '\0';
        int diff = shareDifficulty;
        unsigned long n = (unsigned long)coreId; // Core0: 0,2,4... Core1: 1,3,5...

        while (miningActive && !solutionFound) {
            computeHash(&localCtx, tpl, n, hashBuf);
            hashCount[coreId]++;
            statsHashCount[coreId]++;
            rateCount++;
            hashIter++;

            if (meetsDifficulty(hashBuf, diff)) {
                if (xSemaphoreTake(xMutex, portMAX_DELAY)) {
                    if (!solutionFound) {
                        solutionFound = true;
                        foundNonce    = n;
                        Serial.printf("[Core %d] FOUND! Nonce:%lu Hash:%.16s...\n",
                                      coreId, n, hashBuf);
                    }
                    xSemaphoreGive(xMutex);
                }
                break;
            }

            n += 2;

            // Yield co 10000 hashów — nie blokuj schedulera
            if (hashIter % 10000 == 0) {
                vTaskDelay(1);
                hashIter = 0;
            }

            if (MAX_HASHRATE_PER_CORE > 0 &&
                rateCount >= (unsigned long)MAX_HASHRATE_PER_CORE) {
                unsigned long elapsed = millis() - rateWindowStart;
                if (elapsed < 1000) vTaskDelay(pdMS_TO_TICKS(1000 - elapsed));
                rateWindowStart = millis();
                rateCount = 0;
            }
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

// ============================================
// HTTPS
// ============================================

bool httpsGet(const String& path, String& response) {
    WiFiClientSecure client;
    client.setInsecure();
    client.setTimeout(15);
    HTTPClient http;
    http.setReuse(false);
    http.begin(client, HOST, 443, path, true);
    http.setTimeout(15000);
    int code = http.GET();
    bool ok  = false;
    if (code == 200) {
        response = http.getString();
        ok = true;
    } else {
        String body = http.getString();
        Serial.printf("GET -> HTTP %d\n", code);
        if (body.length() > 0) Serial.printf("Serwer: %.200s\n", body.c_str());
    }
    http.end();
    client.stop();
    return ok;
}

bool httpsPost(const String& path, const String& body, String& response) {
    WiFiClientSecure client;
    client.setInsecure();
    client.setTimeout(15);
    HTTPClient http;
    http.setReuse(false);
    http.begin(client, HOST, 443, path, true);
    http.addHeader("Content-Type", "application/json");
    http.setTimeout(15000);
    int code = http.POST(body);
    bool ok  = false;
    if (code == 200) {
        response = http.getString();
        ok = true;
    } else {
        Serial.printf("POST %s -> %d\n", path.c_str(), code);
    }
    http.end();
    client.stop();
    return ok;
}

// ============================================
// GET JOB
// ============================================

bool getJob() {
    if (WiFi.status() != WL_CONNECTED) return false;

    float tempC = readCpuTempC();

    String path = String("/api.php?action=get_job")
                + "&wallet="      + String(WALLET_ADDRESS)
                + "&device_id="   + deviceId
                + "&device_name=" + String(DEVICE_NAME)
                + "&model="       + String(ESP.getChipModel())
                + "&rev="         + String(ESP.getChipRevision())
                + "&cores="       + String(ESP.getChipCores())
                + "&fw="          + String(FIRMWARE_VER)
                + "&flash="       + String(ESP.getFlashChipSize() / 1024)
                + "&psram="       + String(ESP.getPsramSize() / 1024)
                + "&temp="        + String(tempC, 1)
                + "&load="        + String((int)cpuLoadPercent)
                + "&heap="        + String(ESP.getFreeHeap())
                + "&rssi="        + String(WiFi.RSSI())
                + "&uptime="      + String(millis() / 1000)
                + "&mac="         + WiFi.macAddress();

    String response;
    if (!httpsGet(path, response)) return false;

    StaticJsonDocument<1536> doc;
    DeserializationError jerr = deserializeJson(doc, response);
    if (jerr != DeserializationError::Ok) {
        Serial.printf("JSON error (get_job): %s\n", jerr.c_str());
        Serial.printf("Response: %.120s\n", response.c_str());
        return false;
    }
    if (!doc["success"]) {
        Serial.printf("Blad: %s\n", doc["error"].as<const char*>());
        return false;
    }

    strncpy(jobId,         doc["job_id"].as<const char*>(),         sizeof(jobId) - 1);
    strncpy(blockTemplate, doc["block_template"].as<const char*>(), sizeof(blockTemplate) - 1);
    jobId[sizeof(jobId) - 1]                 = '\0';
    blockTemplate[sizeof(blockTemplate) - 1] = '\0';
    shareDifficulty = doc["difficulty"].as<int>();

    Serial.printf("Job: %.8s...  diff=%d\n", jobId, shareDifficulty);
    return true;
}

// ============================================
// TELEMETRIA
// ============================================

void sendTelemetry() {
    if (WiFi.status() != WL_CONNECTED) return;
    float tempC = readCpuTempC();

    StaticJsonDocument<384> doc;
    doc["device_id"] = deviceId;
    doc["wallet"]    = WALLET_ADDRESS;
    doc["model"]     = ESP.getChipModel();
    doc["rev"]       = ESP.getChipRevision();
    doc["cores"]     = ESP.getChipCores();
    doc["mac"]       = WiFi.macAddress();
    doc["fw"]        = FIRMWARE_VER;
    doc["flash"]     = ESP.getFlashChipSize() / 1024;
    doc["psram"]     = ESP.getPsramSize() / 1024;
    doc["temp"]      = round(tempC * 10.0f) / 10.0f;
    doc["load"]      = round(cpuLoadPercent * 10.0f) / 10.0f;
    doc["heap"]      = (int)ESP.getFreeHeap();
    doc["rssi"]      = (int)WiFi.RSSI();
    doc["uptime"]    = millis() / 1000;

    String body, response;
    serializeJson(doc, body);
    if (httpsPost("/api.php?action=telemetry", body, response)) {
        Serial.printf("[Tele] temp=%.1fC  heap=%d  rssi=%d\n",
                      tempC, (int)doc["heap"], (int)doc["rssi"]);
    }
}

// ============================================
// SUBMIT SHARE
// ============================================

bool submitShare(unsigned long nonce, float hashrate) {
    if (WiFi.status() != WL_CONNECTED) return false;

    // Lokalny kontekst SHA do weryfikacji
    char hashBuf[65];
    mbedtls_sha256_context verifyCtx;
    mbedtls_sha256_init(&verifyCtx);
    computeHash(&verifyCtx, blockTemplate, nonce, hashBuf);
    mbedtls_sha256_free(&verifyCtx);

    StaticJsonDocument<512> doc;
    doc["job_id"]    = jobId;
    doc["nonce"]     = (uint32_t)nonce;
    doc["hash"]      = hashBuf;
    doc["device_id"] = deviceId;
    doc["wallet"]    = WALLET_ADDRESS;
    doc["hashrate"]  = (int)hashrate;

    String body, response;
    serializeJson(doc, body);
    if (!httpsPost("/api.php?action=submit_share", body, response)) return false;

    StaticJsonDocument<256> res;
    if (deserializeJson(res, response) != DeserializationError::Ok) {
        Serial.println("JSON error (submit)");
        return false;
    }

    if (res["accepted"]) {
        if (res["waiting"]) {
            int waitSec = res["wait_seconds"] | 10;
            Serial.printf(">>> Za szybko! Czekam maks 3s...\n");
            vTaskDelay(pdMS_TO_TICKS(min((long)waitSec * 1000L, 3000L)));
            return true;
        }
        Serial.println(">>> Share zaakceptowany!");
        for (int i = 0; i < 3; i++) {
            digitalWrite(LED_PIN, HIGH); delay(60);
            digitalWrite(LED_PIN, LOW);  delay(60);
        }
        if (res["block_found"]) {
            Serial.printf("*** BLOK! +%.2f MLM ***\n", res["reward"].as<float>());
            digitalWrite(LED_PIN, HIGH); delay(1500); digitalWrite(LED_PIN, LOW);
        }
        return true;
    }

    Serial.printf("Share odrzucony: %s\n", res["error"] | "");
    return false;
}

// ============================================
// SETUP
// ============================================

void setup() {
    Serial.begin(115200);
    delay(1500);
    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, HIGH);

    Serial.println("\n================================");
    Serial.println("  MalmarCoin ESP32 Miner v3.3.1");
    Serial.println("================================");

    uint64_t mac = ESP.getEfuseMac();
    char macStr[17];
    sprintf(macStr, "%016llX", mac);
    deviceId = String(macStr);

    Serial.printf("Chip:   %s rev%d  %d rdzenie\n",
                  ESP.getChipModel(), ESP.getChipRevision(), ESP.getChipCores());
    Serial.printf("Heap:   %d B free\n", ESP.getFreeHeap());
    Serial.printf("Device: %s\n", DEVICE_NAME);
    Serial.printf("Wallet: %.16s...\n", WALLET_ADDRESS);

    Serial.printf("\nLaczenie WiFi: %s", WIFI_SSID);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    int att = 0;
    while (WiFi.status() != WL_CONNECTED && att++ < 40) {
        delay(500); Serial.print(".");
    }
    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("\nBrak WiFi! Restart...");
        delay(3000);
        ESP.restart();
    }
    Serial.printf("\nWiFi OK! IP: %s  RSSI: %d dBm\n\n",
                  WiFi.localIP().toString().c_str(), WiFi.RSSI());

    xMutex = xSemaphoreCreateMutex();

    // Pomiar CPU load (niski priorytet, Core 0)
    xTaskCreatePinnedToCore(cpuMeasureTask, "CpuMsr", 2048, NULL, 0, NULL, 0);

    xTaskCreatePinnedToCore(miningTaskFunction, "Mine0", 16000, (void*)0, 1, &miningTask0, 0);
    xTaskCreatePinnedToCore(miningTaskFunction, "Mine1", 16000, (void*)1, 1, &miningTask1, 1);

    digitalWrite(LED_PIN, LOW);
    statsWindowStart  = millis();
    lastStatsTime     = millis();
    lastTelemetryTime = millis();

    Serial.println("Koparka uruchomiona! (v3.3.1)");
}

// ============================================
// LOOP
// ============================================

void loop() {
    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("WiFi zerwane...");
        miningActive = false;
        WiFi.reconnect();
        delay(5000);
        return;
    }

    if (solutionFound || !miningActive || millis() - lastJobTime > 60000) {
        if (solutionFound) {
            float elapsed  = (millis() - lastJobTime) / 1000.0f;
            float hashrate = elapsed > 0 ? (hashCount[0] + hashCount[1]) / elapsed : 0;
            miningActive = false;
            bool ok = false;
            for (int i = 1; i <= 5 && !ok; i++) {
                ok = submitShare(foundNonce, hashrate);
                if (!ok) { Serial.printf("Retry %d/5\n", i); delay(2000); }
            }
            solutionFound    = false;
            foundNonce       = 0;
            hashCount[0]     = 0;
            hashCount[1]     = 0;
        }
        if (getJob()) { miningActive = true; lastJobTime = millis(); }
        else delay(5000);
    }

    if (millis() - lastTelemetryTime > TELEMETRY_INTERVAL) {
        sendTelemetry();
        lastTelemetryTime = millis();
    }

    if (millis() - lastStatsTime > 10000) {
        unsigned long elapsed = millis() - statsWindowStart;
        if (elapsed > 0) {
            float hr   = (statsHashCount[0] + statsHashCount[1]) * 1000.0f / elapsed;
            int heap   = (int)ESP.getFreeHeap();
            Serial.printf("[Stats] %.0f H/s  Temp:%.1fC  CPU:%.0f%%  Heap:%d  RSSI:%d\n",
                          hr, readCpuTempC(), cpuLoadPercent, heap, (int)WiFi.RSSI());
            if (heap < 30000) {
                Serial.printf("[WARN] Malo pamieci: %d B! Restart\n", heap);
                delay(500); ESP.restart();
            }
            statsHashCount[0] = 0;
            statsHashCount[1] = 0;
            statsWindowStart  = millis();
        }
        lastStatsTime = millis();
    }

    unsigned long blinkMs = miningActive ? 150 : 1000;
    if (millis() - lastBlinkTime >= blinkMs) {
        ledState = !ledState;
        digitalWrite(LED_PIN, ledState);
        lastBlinkTime = millis();
    }

    delay(100);
}
5

Skonfiguruj minera

Zmień następujące wartości w kodzie:

WIFI_SSID Nazwa Twojej sieci WiFi
WIFI_PASSWORD Hasło do WiFi
WALLET_ADDRESS Twój adres portfela MLM
POOL_URL http://malmar.portaleai.pl
DEVICE_NAME Nazwa urządzenia (opcjonalnie)
6

Wgraj kod na ESP32

  1. Podłącz ESP32 kablem USB do komputera
  2. W Arduino IDE wybierz:
    • Tools → Board → ESP32 Dev Module
    • Tools → Port → (wybierz port COM ESP32)
  3. Kliknij przycisk Upload (strzałka w prawo)
  4. Poczekaj na zakończenie wgrywania
7

Sprawdź działanie

Otwórz Serial Monitor (Tools → Serial Monitor) i ustaw prędkość na 115200 baud.

Powinieneś zobaczyć:

================================
MalmarCoin ESP32 Miner v1.0
================================
Device ID: A1B2C3D4E5F6...
Connecting to WiFi: TwojaSSiec...
WiFi connected!
IP: 192.168.1.100
Mining tasks created on both cores!
New job: abc123..., difficulty: 3
Hashrate: 25.50 H/s | Core0: 12800 | Core1: 12750

Twój miner działa! Sprawdź statystyki na:

Informacje o Pool

Pool URL: http://malmar.portaleai.pl
Algorytm: SHA-256
Nagroda za blok: 5 MLM
Trudność share: 3
Minimalna wypłata: 1 MLM
Prowizja: 0%

Często zadawane pytania

ESP32 osiąga około 20-30 H/s przy kopaniu SHA-256. Wykorzystując oba rdzenie (dual-core) możesz zwiększyć wydajność o około 80-90% w porównaniu do single-core.

ESP32 podczas intensywnego kopania zużywa około 150-200 mA przy 5V, co daje około 0.75-1W. Miesięczny koszt prądu to kilka groszy.

Tak! Możesz podłączyć dowolną liczbę ESP32 do tego samego portfela. Każde urządzenie będzie widoczne osobno w panelu Pool. Wystarczy zmienić DEVICE_NAME dla każdego urządzenia.

  • Sprawdź siłę sygnału WiFi
  • Upewnij się, że serwer jest dostępny
  • Sprawdź czy adres portfela jest poprawny
  • Zrestartuj ESP32
  • Sprawdź Serial Monitor dla błędów

Monety są przyznawane natychmiast po znalezieniu bloku. Nagroda za blok wynosi 5 MLM. Możesz śledzić swoje saldo w Portfelu.

ESP8266 ma tylko jeden rdzeń i mniej RAM, więc będzie znacznie wolniejszy (około 5-10 H/s). Zalecamy używanie ESP32 dla lepszej wydajności. Kod wymaga modyfikacji dla ESP8266.

Rozwiązywanie problemów

"Failed to connect to WiFi"
  • Sprawdź nazwę sieci WiFi (SSID) - wielkość liter ma znaczenie
  • Sprawdź hasło WiFi
  • Upewnij się, że sieć działa na 2.4 GHz (ESP32 nie obsługuje 5 GHz)
"Failed to get job"
  • Sprawdź adres POOL_URL
  • Upewnij się, że serwer jest dostępny
  • Sprawdź poprawność adresu portfela
"Share rejected"
  • To normalne - niektóre share mogą być odrzucone
  • Sprawdź czy job nie wygasł (timeout)
  • Upewnij się, że zegar ESP32 jest zsynchronizowany
Błąd kompilacji w Arduino IDE
  • Upewnij się, że zainstalowałeś ESP32 board
  • Zainstaluj bibliotekę ArduinoJson
  • Wybierz odpowiednią płytkę (ESP32 Dev Module)

Gotowy do kopania?

Dołącz do społeczności minerów Malmar Coin!