JK BMS Web Gateway

Ein privates Projekt (LTSolutions), das JK-BMS-Steuergeräte für LiFePO4-Solarbatterien zugänglich macht — ohne die Handy-BLE-App oder das PC-Tool des Herstellers. Ein ESP32-Gateway liest die Batterie über RS485 aus und hört das CAN-Signal des Wechselrichters mit, und stellt das als JSON-REST-API und Web-Dashboard direkt in Ihrem WLAN bereit.

Hersteller der BMS-Einheiten: jkbms.com / jk-bms.com. Dieses Projekt steht in keiner Verbindung zum Hersteller.

Benötigte Hardware

Das Gateway benötigt ein ESP32-Board, einen RS485-Wandler und (optional) einen CAN-Wandler, verkabelt nach den Tabellen unten. Die genauen Pin-Nummern sind in der Firmware unter src/main.cpp festgelegt — ändern Sie sie dort, falls Sie anders verkabeln.

Was Sie brauchen

Optional: eine fertige KiCad-Trägerplatine, die das ESP32-DevKit, beide Wandler und zwei RJ45-Buchsen auf einer Platine vereint — Quelldateien auf GitHub.

Verkabelung: MAX485 (RS485) → ESP32

MAX485-PinESP32-PinHinweis
VCC3.3VBeide Wandler sind native 3.3V-Bauteile — betreiben Sie sie nicht mit 5V, das könnte den RX-Pin des ESP32 überlasten.
GNDGND
DI (driver in)GPIO17 (TX2)
RO (receiver out)GPIO16 (RX2)
DE + RE (zusammengeführt)GPIO4HIGH = Senden, LOW = Empfangen
ABMS RS485-A (RJ45 pin 2/7)
BBMS RS485-B (RJ45 pin 1/8)

Verkabelung: SN65HVD230 (CAN) → ESP32

SN65HVD230-PinESP32-PinHinweis
3 VCC3.3VBeide Wandler sind native 3.3V-Bauteile — betreiben Sie sie nicht mit 5V, das könnte den RX-Pin des ESP32 überlasten.
2 GNDGND
1 D (TXD)GPIO25CAN_TX_PIN
4 R (RXD)GPIO26CAN_RX_PIN
8 Rs10 kΩ → GND
5 Vref
7 CANH / 6 CANLBMS CAN-H / CAN-L

Kombinierter BMS-Port 485/CAN (RJ45)

Der JK-PB2A16S20P hat einen kombinierten "485/CAN"-RJ45-Port, der beide Busse gleichzeitig führt:

RJ45-PinSignal
1, 8RS485-B
2, 7RS485-A
3NC
4CAN-H
5CAN-L
6GND

Wichtige Hinweise

Kontaktformular

JK BMS Updater einrichten

⚠️ Wir übernehmen keine Haftung für Schäden durch unsachgemäßen Gebrauch!

Angebotene Version: R1-V1.00e

Ihr Browser unterstützt die für das direkte Flashen hier benötigte Web-Serial-API nicht. Das funktioniert in Chrome, Edge oder Opera am Desktop. Laden Sie die Binärdateien herunter und flashen Sie sie manuell mit esptool.py.
Diese Seite muss über eine gesicherte HTTPS-Verbindung bereitgestellt werden, damit das Flashen im Browser funktioniert.

Flashen Sie die unten stehenden Dateien mit:

esptool.py --chip esp32 --port PORT write_flash \
  0x1000  bootloader.bin \
  0x8000  partitions.bin \
  0xe000  boot_app0.bin \
  0x10000 firmware.bin \
  0x290000 littlefs.bin

bootloader.bin · partitions.bin · boot_app0.bin · firmware.bin · littlefs.bin

REST API

Alle Antworten sind JSON. Beispiel: curl http://jkbms.local/api/realtime

MethodePfadBeschreibung
GET /api/info Gateway-Status (IP, Laufzeit, letzte Abfrage)
GET /api/realtime[?addr=N] Zellspannungen, Pack V/I/P, SOC, Temperaturen, Alarme
GET /api/settings[?addr=N] Aktuelle Werte der Schutzparameter
POST /api/settings[?addr=N] Body: {"feldname": wert, ...} — schreibt geänderte Felder
GET /api/scan Fragt die Adressen 0–15 ab und meldet, welche antworten
GET /api/can Dekodierte CAN-Wechselrichter-Übertragung (SOC, Pack V/I/T, Lade-/Entladegrenzwerte, Flags, Hersteller)
GET /api/can/raw[?n=20] Zuletzt empfangene rohe CAN-Rahmen (id, dlc, data[], Alter in ms)
GET / POST /api/can/config CAN-Einstellungen — Bitrate, Autoscan, Profil (in NVS gespeichert)
GET /api/debug/read?reg=0xHEX&count=N[&addr=N] Rohes Lesen von Modbus-Haltregistern
POST /api/debug/verifywrite?reg=0xHEX[&addr=N] Liest ein Register, schreibt denselben Wert zurück, liest erneut — belegt, dass der Schreibpfad das Register erreicht, ohne etwas zu ändern

Beispiel-C/C++-Konsolenclient

Ein minimaler Client — ein HTTP-GET, ein JSON-Parse, gibt Spannung/Strom/SOC des Packs aus. jkbms_client_example.cpp herunterladen

// JK BMS Web Gateway — minimal REST API console client.
//
// Fetches GET /api/realtime from the gateway and prints pack voltage,
// current, and state of charge. Demonstrates the smallest useful client:
// one HTTP GET, one JSON parse. See the full endpoint list and JSON field
// reference on the site's #api section.
//
// Build (Linux/macOS, needs libcurl + nlohmann/json):
//   g++ -std=c++17 jkbms_client_example.cpp -lcurl -o jkbms_client_example
// Run:
//   ./jkbms_client_example http://jkbms.local

#include <curl/curl.h>
#include <nlohmann/json.hpp>
#include <cstdio>
#include <cstdlib>
#include <string>

using nlohmann::json;

// libcurl calls this once per received chunk of the HTTP response body;
// appending to a std::string is the standard way to buffer a small response.
static size_t appendToBuffer(char *data, size_t size, size_t count, void *userData) {
    auto *buffer = static_cast<std::string *>(userData);
    buffer->append(data, size * count);
    return size * count;
}

// Performs one blocking HTTP GET and returns the response body.
// Throws std::runtime_error if the transfer itself fails (not on HTTP
// error status — the caller is expected to check the JSON's own "error" field).
static std::string httpGet(const std::string &url) {
    CURL *curl = curl_easy_init();
    if (!curl) {
        throw std::runtime_error("curl_easy_init failed");
    }

    std::string body;
    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, appendToBuffer);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);

    CURLcode result = curl_easy_perform(curl);
    curl_easy_cleanup(curl);

    if (result != CURLE_OK) {
        throw std::runtime_error(curl_easy_strerror(result));
    }
    return body;
}

int main(int argc, char *argv[]) {
    // Default to the gateway's mDNS name; override with a bare IP if mDNS
    // isn't reachable on your network (e.g. "http://192.168.1.50").
    std::string baseUrl = (argc > 1) ? argv[1] : "http://jkbms.local";

    std::string body;
    try {
        body = httpGet(baseUrl + "/api/realtime");
    } catch (const std::exception &ex) {
        std::fprintf(stderr, "request failed: %s\n", ex.what());
        return 1;
    }

    json realtime = json::parse(body, /*cb*/ nullptr, /*allow_exceptions*/ false);
    if (realtime.is_discarded()) {
        std::fprintf(stderr, "invalid JSON response: %s\n", body.c_str());
        return 1;
    }
    if (realtime.contains("error")) {
        std::fprintf(stderr, "gateway error: %s\n", realtime["error"].get<std::string>().c_str());
        return 1;
    }

    std::printf("Pack voltage: %.2f V\n", realtime.value("totalVoltage", 0.0));
    std::printf("Current:      %.2f A\n", realtime.value("current", 0.0));
    std::printf("SOC:          %d %%\n", realtime.value("soc", 0));
    return 0;
}