Jak kopać Malmar Coin?

Kompletny przewodnik kopania MLM na ESP32

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.0 - Telemetria, dual-core mining
 *
 * 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.0.0";

// Limit hashrate per rdzen (0 = brak limitu, pelna moc)
const int MAX_HASHRATE_PER_CORE = 0;

// Interwał telemetrii (ms) — wysylaj co 30s niezaleznie od mining
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;

String jobId;
String blockTemplate;
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;

// Liczniki CPU load (FreeRTOS idle hook)
volatile unsigned long idleCount0 = 0, idleCount1 = 0;
unsigned long prevIdle0 = 0, prevIdle1 = 0;
float lastCpuLoad = 0.0f;

TaskHandle_t miningTask0;
TaskHandle_t miningTask1;

// ============================================
// CPU LOAD — FreeRTOS idle hook
// Wlacz w: menuconfig → FreeRTOS → Enable idle task hook
// ============================================

void vApplicationIdleHook() {
    if (xPortGetCoreID() == 0) idleCount0++;
    else                       idleCount1++;
}

float getCpuLoad() {
    unsigned long d0 = idleCount0 - prevIdle0;
    unsigned long d1 = idleCount1 - prevIdle1;
    prevIdle0 = idleCount0; prevIdle1 = idleCount1;
    float idle = (d0 + d1) / 2000.0f;
    lastCpuLoad = max(0.0f, min(100.0f, (1.0f - idle) * 100.0f));
    return lastCpuLoad;
}

// ============================================
// SHA256
// ============================================

void computeHash(const char* tpl, unsigned long nonce, char* outHex) {
    char input[256];
    snprintf(input, sizeof(input), "%s%lu", tpl, nonce);
    unsigned char hashBytes[32];
    mbedtls_sha256_context ctx;
    mbedtls_sha256_init(&ctx);
    mbedtls_sha256_starts(&ctx, 0);
    mbedtls_sha256_update(&ctx, (const unsigned char*)input, strlen(input));
    mbedtls_sha256_finish(&ctx, hashBytes);
    mbedtls_sha256_free(&ctx);
    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)
// ============================================

void miningTaskFunction(void* parameter) {
    int  coreId = (int)parameter;
    char hashBuf[65];
    unsigned long rateWindowStart = millis();
    unsigned long rateCount = 0;

    while (true) {
        if (!miningActive || solutionFound || blockTemplate.length() == 0) {
            vTaskDelay(pdMS_TO_TICKS(50));
            rateWindowStart = millis(); rateCount = 0;
            continue;
        }
        String tpl = blockTemplate;
        int diff   = shareDifficulty;
        unsigned long n = (unsigned long)coreId;

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

            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;
            if (hashCount[coreId] % 10000 == 0) vTaskDelay(1);
            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 HELPERS
// ============================================

bool httpsGet(const String& path, String& response) {
    WiFiClientSecure client; client.setInsecure();
    HTTPClient http;
    http.begin(client, HOST, 443, path, true);
    http.setTimeout(15000);
    int code = http.GET();
    if (code == 200) { response = http.getString(); http.end(); return true; }
    // Pokaż treść błędu (np. blokada urządzenia)
    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(); return false;
}

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

// ============================================
// GET JOB — z telemetrią w URL
// ============================================

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

    float tempC = (temperatureRead() - 32.0f) * 5.0f / 9.0f;
    float load  = getCpuLoad();

    String path = String("/api.php?action=get_job")
                + "&wallet="  + String(WALLET_ADDRESS)
                + "&device_id=" + deviceId
                + "&device_name=" + String(DEVICE_NAME)
                // ── Telemetria ──────────────────────────
                + "&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(load, 1)
                + "&heap="    + String(ESP.getFreeHeap())
                + "&rssi="    + String(WiFi.RSSI())
                + "&uptime="  + String(millis() / 1000)
                + "&mac="     + WiFi.macAddress();

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

    // Debug: pokaż odpowiedź serwera gdy błąd parsowania
    DynamicJsonDocument doc(1536);
    DeserializationError jerr = deserializeJson(doc, response);
    if (jerr != DeserializationError::Ok) {
        Serial.printf("JSON error (get_job): %s\n", jerr.c_str());
        Serial.printf("Response (%d B): %.120s\n", response.length(), response.c_str());
        return false;
    }
    if (!doc["success"]) {
        Serial.printf("Blad serwera: %s\n", doc["error"].as<const char*>()); return false;
    }
    jobId          = doc["job_id"].as<String>();
    blockTemplate  = doc["block_template"].as<String>();
    shareDifficulty = doc["difficulty"].as<int>();
    Serial.printf("Job: %.8s...  diff=%d  tpl=%.12s...\n",
                  jobId.c_str(), shareDifficulty, blockTemplate.c_str());
    return true;
}

// ============================================
// TELEMETRIA — osobny POST co TELEMETRY_INTERVAL
// ============================================

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

    float tempC = (temperatureRead() - 32.0f) * 5.0f / 9.0f;
    float load  = getCpuLoad();

    StaticJsonDocument<320> 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(load * 10.0f) / 10.0f;
    doc["heap"]      = ESP.getFreeHeap();
    doc["rssi"]      = WiFi.RSSI();
    doc["uptime"]    = millis() / 1000;

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

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

bool submitShare(unsigned long nonce, float hashrate) {
    if (WiFi.status() != WL_CONNECTED) return false;
    char hashBuf[65];
    computeHash(blockTemplate.c_str(), nonce, hashBuf);

    StaticJsonDocument<256> 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; serializeJson(doc, body);

    String response;
    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 %ds...\n", waitSec);
            for (int i = 0; i < waitSec; i++) {
                digitalWrite(LED_PIN, HIGH); vTaskDelay(pdMS_TO_TICKS(500));
                digitalWrite(LED_PIN, LOW);  vTaskDelay(pdMS_TO_TICKS(500));
            }
            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"]) {
            float reward = res["reward"];
            Serial.printf("*** BLOK ZNALEZIONY! +%.2f MLM ***\n", reward);
            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.0");
    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("Flash:  %d KB  PSRAM: %d KB\n",
                  ESP.getFlashChipSize()/1024, ESP.getPsramSize()/1024);
    Serial.printf("Device: %s (%s)\n", DEVICE_NAME, deviceId.c_str());
    Serial.printf("Wallet: %.16s...\n", WALLET_ADDRESS);
    Serial.printf("Pool:   https://%s\n", HOST);

    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  MAC: %s\n\n",
                  WiFi.localIP().toString().c_str(), WiFi.macAddress().c_str());

    xMutex = xSemaphoreCreateMutex();
    xTaskCreatePinnedToCore(miningTaskFunction, "Mine0", 12000, (void*)0, 1, &miningTask0, 0);
    xTaskCreatePinnedToCore(miningTaskFunction, "Mine1", 12000, (void*)1, 1, &miningTask1, 1);

    digitalWrite(LED_PIN, LOW);
    statsWindowStart  = millis();
    lastStatsTime     = millis();
    lastTelemetryTime = millis();
    Serial.println("Koparka uruchomiona! (v3.0 z telemetria)");
}

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

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

    // Mining
    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); vTaskDelay(pdMS_TO_TICKS(2000)); }
            }
            solutionFound = false; foundNonce = 0;
            hashCount[0] = 0; hashCount[1] = 0;
        }
        if (getJob()) { miningActive = true; lastJobTime = millis(); }
        else vTaskDelay(pdMS_TO_TICKS(5000));
    }

    // Osobna telemetria co TELEMETRY_INTERVAL
    if (millis() - lastTelemetryTime > TELEMETRY_INTERVAL) {
        sendTelemetry();
        lastTelemetryTime = millis();
    }

    // Stats w Serial co 10 s
    if (millis() - lastStatsTime > 10000) {
        unsigned long elapsed = millis() - statsWindowStart;
        if (elapsed > 0) {
            float hashrate = (statsHashCount[0] + statsHashCount[1]) * 1000.0f / elapsed;
            float tempC    = (temperatureRead() - 32.0f) * 5.0f / 9.0f;
            Serial.printf("[Stats] H/s: %.0f  Temp: %.1f°C  Load: %.1f%%  Heap: %d  RSSI: %d\n",
                          hashrate, tempC, lastCpuLoad, (int)ESP.getFreeHeap(), (int)WiFi.RSSI());
            statsHashCount[0] = statsHashCount[1] = 0;
            statsWindowStart = millis();
        }
        lastStatsTime = millis();
    }

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

    vTaskDelay(pdMS_TO_TICKS(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!