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
- Ein beliebiges ESP32-Entwicklungsboard (z. B. ESP32-WROOM-32, 38-Pin-DevKit)
- Ein MAX485-/MAX3485-TTL-zu-RS485-Wandler (3.3-V-Version, mit DE und RE auf einem Pin zusammengeführt)
- Ein SN65HVD230-(3.3-V)-CAN-Wandler — nur nötig, wenn Sie auch das CAN-Signal des Wechselrichters lesen möchten
- Ein RJ45-Kabel zum kombinierten 485/CAN-Port des BMS
- Ein 5V-Netzteil für das ESP32-Board
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-Pin | ESP32-Pin | Hinweis |
|---|---|---|
| VCC | 3.3V | Beide Wandler sind native 3.3V-Bauteile — betreiben Sie sie nicht mit 5V, das könnte den RX-Pin des ESP32 überlasten. |
| GND | GND | |
| DI (driver in) | GPIO17 (TX2) | |
| RO (receiver out) | GPIO16 (RX2) | |
| DE + RE (zusammengeführt) | GPIO4 | HIGH = Senden, LOW = Empfangen |
| A | BMS RS485-A (RJ45 pin 2/7) | |
| B | BMS RS485-B (RJ45 pin 1/8) | |
Verkabelung: SN65HVD230 (CAN) → ESP32
| SN65HVD230-Pin | ESP32-Pin | Hinweis |
|---|---|---|
| 3 VCC | 3.3V | Beide Wandler sind native 3.3V-Bauteile — betreiben Sie sie nicht mit 5V, das könnte den RX-Pin des ESP32 überlasten. |
| 2 GND | GND | |
| 1 D (TXD) | GPIO25 | CAN_TX_PIN |
| 4 R (RXD) | GPIO26 | CAN_RX_PIN |
| 8 Rs | 10 kΩ → GND | |
| 5 Vref | — | |
| 7 CANH / 6 CANL | BMS 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-Pin | Signal |
|---|---|
| 1, 8 | RS485-B |
| 2, 7 | RS485-A |
| 3 | NC |
| 4 | CAN-H |
| 5 | CAN-L |
| 6 | GND |
Wichtige Hinweise
- Die Firmware hört den CAN-Bus nur passiv mit (listen-only) — sie sendet nie etwas, daher ist es sicher, auch an einen aktiven Bus zwischen BMS und Wechselrichter anzuschließen.
- Fügen Sie den 120-Ω-CAN-Abschlusswiderstand nur hinzu, wenn das ESP32 ein tatsächliches Busende ist. Ist es nur ein Abzweig an einem Bus, den BMS und Wechselrichter bereits terminieren, lassen Sie ihn weg.
- Die CAN-Bitrate ist standardmäßig 500 kbit/s (JK-Standard) und kann zur Laufzeit im CAN-Tab des Dashboards oder über
POST /api/can/configgeändert werden.
Kontaktformular
JK BMS Updater einrichten
Angebotene Version: R1-V1.00e
esptool.py.
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
| Methode | Pfad | Beschreibung |
|---|---|---|
| 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;
}