JK BMS - Arduino

JK BMS - Arduino
/* JK-BMS Web + JSON (16 cells, signed power by current)
 * Sprzęt: Mega2560 + RS485 (RE&DE -> D7, RO->19 RX1, DI->18 TX1) + Ethernet W5100/W5500
 * UART: 115200 8N1
 *
 * HTTP:
 *   GET /      -> strona WWW (odświeża /json co 2 s)
 *   GET /json  -> JSON z polami: uptime, voltage, current, power (ZE ZNAKIEM wg prądu),
 *                 temp{t1,t2}, soc, soh, cells[16], error (0/1), error_msg
 *
 * Modbus: FC=0x03 z automatycznym fallbackiem na FC=0x04.
 * Polling: co ~1 s jedno zapytanie (stabilnie dla JK).
 */

#include <SPI.h>
#include <Ethernet.h>
#include <Arduino.h>
#include <math.h>   // fabsf, isnan

// ======== KONFIG ========
#define RS485_DIR_PIN       7
#define SLAVE_ID            0x01
#define BMS_BAUD            115200
#define N_CELLS             16     // liczba cel

// Timingi Modbus
#define RX_WINDOW_MS        2000
#define IB_GAP_MS             60
#define DIR_SETTLE_US        600
#define STEP_INTERVAL_MS    1000    // 1 zapytanie / sek.

// Ethernet
#define USE_DHCP            0
byte mac[] = {0xDE,0xAD,0xBE,0xEF,0xFE,0xED};
// Statyczne IP (gdy DHCP zawiedzie lub USE_DHCP==0)
IPAddress ip(192,168,1,34), dns(192,168,1,1), gw(192,168,1,1), mask(255,255,255,0);
EthernetServer server(80);

// ======== POMOCNICZE: uptime w sekundach (bez problemu przepełnienia w praktyce) ========
static inline uint32_t uptime_seconds() {
  return millis() / 1000UL;
}

// ======== CRC16 MODBUS ========
static uint16_t crc16(const uint8_t* d, size_t n){
  uint16_t c=0xFFFF;
  for(size_t i=0;i<n;i++){ c^=d[i]; for(uint8_t b=0;b<8;b++) c=(c&1)?(c>>1)^0xA001:(c>>1); }
  return c;
}
static inline void rs485_tx(bool tx){ digitalWrite(RS485_DIR_PIN, tx?HIGH:LOW); delayMicroseconds(DIR_SETTLE_US); }

// TX/RX jednej ramki
static bool xchg(const uint8_t* tx, size_t ntx, uint8_t* rx, size_t cap, size_t &n){
  while(Serial1.available()) Serial1.read();
  rs485_tx(true);  Serial1.write(tx,ntx); Serial1.flush(); rs485_tx(false);
  n=0; uint32_t t0=millis(), last=millis();
  while(millis()-t0 < RX_WINDOW_MS){
    bool any=false;
    while(Serial1.available()){
      if(n<cap) rx[n++] = (uint8_t)Serial1.read();
      any=true; last=millis();
    }
    if(!any && (millis()-last) >= IB_GAP_MS) break;
    delay(1);
  }
  return n>0;
}

// Czytaj FC=fn (0x03/0x04). True jeśli ramka OK (len+CRC).
static bool read_fn(uint8_t fn, uint16_t addr, uint16_t regs, uint8_t* rx, size_t& n, String* why=nullptr){
  uint8_t req[8] = { SLAVE_ID, fn, (uint8_t)(addr>>8), (uint8_t)addr, (uint8_t)(regs>>8), (uint8_t)regs, 0, 0 };
  uint16_t c=crc16(req,6); req[6]=(uint8_t)c; req[7]=(uint8_t)(c>>8);

  if(!xchg(req,sizeof req,rx,256,n)){ if(why) *why="timeout"; return false; }
  if(n<5){ if(why) *why="short"; return false; }
  if(rx[0]!=SLAVE_ID){ if(why) *why="slave_id"; return false; }
  if(rx[1]==(fn|0x80)){ if(why){ *why="exception "; *why+=String(rx[2],DEC);} return false; }
  if(rx[1]!=fn){ if(why) *why="wrong_func"; return false; }
  { uint8_t bc=rx[2]; uint16_t need=3+bc+2; if(n!=need){ if(why){*why="len "; *why+=String(need);} return false; } }
  { uint16_t rcrc=(uint16_t)rx[n-2] | ((uint16_t)rx[n-1]<<8); if(rcrc!=crc16(rx,n-2)){ if(why) *why="crc"; return false; } }
  return true;
}

// Auto: spróbuj 0x03, jak nie › 0x04
static bool read_auto(uint16_t addr, uint16_t regs, uint8_t* rx, size_t& n, String& why, uint8_t& fn_used){
  if(read_fn(0x03, addr, regs, rx, n, &why)){ fn_used=0x03; return true; }
  if(why=="timeout" || why=="short" || why=="crc" || why=="wrong_func"){
    if(read_fn(0x04, addr, regs, rx, n, &why)){ fn_used=0x04; return true; }
  }
  return false;
}

// ======== Dekodery typów ========
static uint32_t u32_LEw(const uint8_t* p){ uint16_t lw=((uint16_t)p[0]<<8)|p[1]; uint16_t hw=((uint16_t)p[2]<<8)|p[3]; return ((uint32_t)lw)|((uint32_t)hw<<16); }
static int32_t  i32_LEw(const uint8_t* p){ return (int32_t)u32_LEw(p); }
static uint16_t u16_BE (const uint8_t* p){ return ((uint16_t)p[0]<<8)|p[1]; }

// ======== Pomocnik: znak mocy wg prądu ========
static float signed_power_by_current(float p, float i){
  if (isnan(p) || isnan(i)) return p;
  return (i < 0.0f) ? -fabsf(p) : fabsf(p);
}

// ======== Globalne dane ========
struct Data {
  float V=NAN, I=NAN, P=NAN, T1=NAN, T2=NAN;
  int   SOC=-1, SOH=-1;
  uint16_t cells[N_CELLS] = {0};
  uint32_t last_update_s = 0;   // sekundy od startu
  uint8_t  error = 1;           // 0 OK, 1 błąd
  String   error_msg = "not ready";
} g;

// Flagi kompletności jednej rundy
static bool fV=false,fI=false,fP=false,fT=false,fSOC=false,fSOH=false,fC=false;
static bool hadErr=false; String lastErr="";

// ======== Poller (1 krok / sek.) ========
enum Step { S_V, S_P, S_I, S_T, S_SOC, S_SOH, S_CELLS, S__N };
static Step step = S_V;
static uint32_t t_next = 0;

static void poll_one_step(){
  uint8_t rx[260]; size_t n=0; String why; uint8_t fn=0;

  switch(step){
    case S_V:   // 0x1290 U32 mV
      if(read_auto(0x1290,2,rx,n,why,fn)){ g.V = u32_LEw(&rx[3])/1000.0f; fV=true; }
      else { hadErr=true; lastErr="0x1290 "+why; }
      break;

    case S_P:   // 0x1294 U32 mW -> W, znak wg prądu (jeśli już znany)
      if(read_auto(0x1294,2,rx,n,why,fn)){
        g.P = u32_LEw(&rx[3]) / 1000.0f;
        if (fI) g.P = signed_power_by_current(g.P, g.I);
        fP=true;
      } else { hadErr=true; lastErr="0x1294 "+why; }
      break;

    case S_I:   // 0x1298 S32 mA -> A
      if(read_auto(0x1298,2,rx,n,why,fn)){
        g.I = i32_LEw(&rx[3]) / 1000.0f;
        if (fP) g.P = signed_power_by_current(g.P, g.I);
        fI=true;
      } else { hadErr=true; lastErr="0x1298 "+why; }
      break;

    case S_T:   // 0x129C/0x129E INT16 0.1°C
      if(read_auto(0x129C,2,rx,n,why,fn)){
        g.T1 = ((int16_t)u16_BE(&rx[3]))/10.0f;
        g.T2 = ((int16_t)u16_BE(&rx[5]))/10.0f;
        fT=true;
      } else { hadErr=true; lastErr="0x129C "+why; }
      break;

    case S_SOC: // 0x12A6, SOC w HI
      if(read_auto(0x12A6,1,rx,n,why,fn)){ g.SOC = (int)rx[3]; fSOC=true; }
      else { hadErr=true; lastErr="0x12A6 "+why; }
      break;

    case S_SOH: // 0x12B8, SOH w LO
      if(read_auto(0x12B8,1,rx,n,why,fn)){ g.SOH = (int)rx[4]; fSOH=true; }
      else { hadErr=true; lastErr="0x12B8 "+why; }
      break;

    case S_CELLS: // 0x1200.. N_CELLS × UINT16 mV
      if(read_auto(0x1200, N_CELLS, rx, n, why, fn)){
        for(int i=0;i<N_CELLS;i++) g.cells[i] = u16_BE(&rx[3+2*i]);
        fC=true;
      } else { hadErr=true; lastErr="0x1200 "+why; }
      break;
    default: break;
  }

  // Koniec rundy
  if(step == S_CELLS){
    if(fV && fP && fI && fT && fSOC && fSOH && fC){
      g.P = signed_power_by_current(g.P, g.I);
      g.error = 0; g.error_msg = "";
    } else { g.error = 1; g.error_msg = lastErr; }
    g.last_update_s = uptime_seconds();   // zapis w sekundach

    // reset flag
    fV=fP=fI=fT=fSOC=fSOH=fC=false; hadErr=false; lastErr="";
  }

  step = (Step)((step + 1) % S__N);
}

// ======== HTTP: /json i / ========
static void send_http_json(EthernetClient &cl){
  cl.println(F("HTTP/1.1 200 OK"));
  cl.println(F("Content-Type: application/json; charset=utf-8"));
  cl.println(F("Connection: close"));
  cl.println();

  // JSON (streaming bez dużych Stringów)
  cl.print(F("{\"uptime\":"));           cl.print(uptime_seconds());   // sekundy zamiast millis
  cl.print(F(",\"voltage\":"));          cl.print(g.V,3);
  cl.print(F(",\"current\":"));          cl.print(g.I,3);
  float p_signed = signed_power_by_current(g.P, g.I);
  cl.print(F(",\"power\":"));            cl.print(p_signed,3);
  cl.print(F(",\"temp\":{\"t1\":"));     cl.print(g.T1,1); cl.print(F(",\"t2\":")); cl.print(g.T2,1); cl.print('}');
  cl.print(F(",\"soc\":"));              cl.print(g.SOC);
  cl.print(F(",\"soh\":"));              cl.print(g.SOH);
  cl.print(F(",\"cells\":["));
  for(int i=0;i<N_CELLS;i++){ cl.print(g.cells[i]); if(i<N_CELLS-1) cl.print(','); }
  cl.print(']');
  cl.print(F(",\"error\":"));            cl.print((int)g.error);
  cl.print(F(",\"error_msg\":\""));
  for(size_t i=0;i<g.error_msg.length();i++){ char c=g.error_msg[i]; if(c=='\"') cl.print('\\'); cl.print(c); }
  cl.print(F("\"}"));
}

static void send_http_index(EthernetClient &cl){
  cl.println(F("HTTP/1.1 200 OK"));
  cl.println(F("Content-Type: text/html; charset=utf-8"));
  cl.println(F("Connection: close"));
  cl.println();

  cl.println(F(
"<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>JK-BMS Live (16S)</title>"
"<style>"
"body{font-family:system-ui,Segoe UI,Arial;margin:20px;background:#0b1220;color:#eaeef3}"
".card{background:#111a2b;padding:16px;border-radius:12px;box-shadow:0 4px 16px rgba(0,0,0,.3);max-width:980px}"
"h1{margin:0 0 12px;font-size:22px}"
"table{width:100%;border-collapse:collapse}td{padding:6px 8px;border-bottom:1px solid #1b2942}"
".ok{color:#62d26f}.err{color:#ff6b6b}.mono{font-family:ui-monospace,Consolas,monospace;word-break:break-word}"
".cells{display:grid;grid-template-columns:repeat(auto-fit,minmax(90px,1fr));gap:6px;margin-top:6px}"
".pill{background:#0e203b;border:1px solid #1c335a;border-radius:8px;padding:6px 8px}"
"</style></head><body><div class='card'><h1>JK-BMS Live (16S)</h1>"
"<div id='status'>wczytywanie…</div>"
"<table><tbody>"
"<tr><td>Napięcie [V]</td><td class='mono' id='v'>-</td></tr>"
"<tr><td>Prąd [A]</td><td class='mono' id='i'>-</td></tr>"
"<tr><td>Moc [W]</td><td class='mono' id='p'>-</td></tr>"
"<tr><td>Temp 1/2 [°C]</td><td class='mono' id='t12'>-</td></tr>"
"<tr><td>SOC / SOH [%]</td><td class='mono' id='socsoh'>-</td></tr>"
"</tbody></table>"
"<div class='mono cells' id='cells'></div>"
"<p style='margin-top:10px'>JSON: <a href='/json' style='color:#9bd'>/json</a></p>"
"</div><script>"
"function el(id){return document.getElementById(id)};"
"function renderCells(arr){const c=el('cells');c.innerHTML='';(arr||[]).forEach((v,i)=>{const d=document.createElement('div');d.className='cell';d.textContent='C'+(i+1)+': '+v+' mV';c.appendChild(d);});}"
"function fmtTime(sec){sec=Math.floor(sec);const h=Math.floor(sec/3600);const m=Math.floor((sec%3600)/60);const s=sec%60;return String(h).padStart(2,'0')+':'+String(m).padStart(2,'0')+':'+String(s).padStart(2,'0');}"
"async function tick(){try{const r=await fetch('/json',{cache:'no-store'});const j=await r.json();"
"el('v').textContent=(j.voltage??NaN).toFixed(3);"
"el('i').textContent=(j.current??NaN).toFixed(3);"
"el('p').textContent=(j.power??NaN).toFixed(3);"
"el('t12').textContent=(j.temp?.t1??NaN).toFixed(1)+' / '+(j.temp?.t2??NaN).toFixed(1);"
"el('socsoh').textContent=(j.soc??-1)+' / '+(j.soh??-1);"
"renderCells(j.cells);"
"const s=el('status');if(j.error){s.innerHTML='Status: <span class=\"err\">BŁĄD</span> – '+(j.error_msg||'');}"
"else{s.innerHTML='Status: <span class=\"ok\">OK</span> (uptime='+fmtTime(j.uptime)+')';}}catch(e){el('status').innerHTML='Status: <span class=\"err\">BŁĄD fetch</span> – '+e;}}"
"tick(); setInterval(tick,2000);"
"</script></body></html>"
  ));
}

// ======== SETUP / LOOP ========
void setup(){
  pinMode(RS485_DIR_PIN, OUTPUT); rs485_tx(false);
  Serial.begin(115200); while(!Serial){}
  Serial.println(F("\n== JK-BMS Web + JSON (16 cells, signed power)=="));

  Serial1.begin(BMS_BAUD, SERIAL_8N1);

#if USE_DHCP
  if(Ethernet.begin(mac)==0){
    Serial.println(F("DHCP FAIL, używam statycznego IP"));
    Ethernet.begin(mac, ip, dns, gw, mask);
  }
#else
  Ethernet.begin(mac, ip, dns, gw, mask);
#endif
  delay(200);
  Serial.print(F("IP: ")); Serial.println(Ethernet.localIP());
  server.begin();

  t_next = millis();   // harmonogram w ms (bezpieczny dzięki różnicom)
}

void loop(){
  // Poller (1 krok / s)
  if((int32_t)(millis()-t_next) >= 0){
    poll_one_step();
    t_next += STEP_INTERVAL_MS;
  }

  // HTTP
  EthernetClient client = server.available();
  if(client){
    // proste parsowanie 1. linii
    String line = client.readStringUntil('\n');
    bool wantJson = line.startsWith("GET /json");
    // zjedz nagłówki
    while(client.connected()){
      String l = client.readStringUntil('\n');
      if(l=="\r" || l.length()==0) break;
    }
    if(wantJson) send_http_json(client);
    else         send_http_index(client);
    client.stop();
  }
}
skopiuj kod   |   pobierz kod

Lokalny monitoring magazynu energii bez chmury

W tym projekcie zrealizowaliśmy własny system monitorowania Battery Management System (BMS) oparty o Arduino Mega 2560, który komunikuje się bezpośrednio z JK-BMS i udostępnia dane lokalnie w sieci LAN w postaci strony WWW oraz API JSON.

To rozwiązanie powstało z realnej potrzeby: chcieliśmy mieć pełny, stabilny i niezależny dostęp do danych magazynu energii, bez korzystania z aplikacji producenta i bez wysyłania danych do chmury.

Nasz przypadek - co dokładnie zrobiliśmy

W praktyce Arduino pełni tutaj rolę samodzielnego serwera BMS:
  • cyklicznie odczytuje dane z JK-BMS przez RS-485 (Modbus RTU),
  • przetwarza je lokalnie,
  • udostępnia aktualne wartości przez przeglądarkę internetową oraz JSON API.
Całość działa autonomicznie, bez Raspberry Pi, bez dodatkowego oprogramowania i bez Internetu (poza siecią lokalną).

Zastosowany sprzęt

W naszej instalacji wykorzystaliśmy:
  • Arduino Mega 2560 – jako główny kontroler,
  • JK-BMS (komunikacja Modbus RTU),
  • konwerter RS-485 ↔ UART,
  • Ethernet Shield W5100 / W5500,
  • standardową sieć LAN.
Arduino Mega zostało wybrane celowo – zapewnia sprzętowy port UART, co daje stabilną komunikację Modbus nawet przy wyższych prędkościach.

Jak jest to podłączone

Komunikacja z BMS odbywa się przez RS-485:
  • linie A/B z BMS → konwerter RS-485,
  • konwerter → port Serial1 Arduino,
  • sterowanie kierunkiem transmisji realizowane jest jednym pinem cyfrowym.
Do sieci lokalnej Arduino podłączone jest kablem Ethernet, co gwarantuje stabilność i brak opóźnień typowych dla Wi-Fi.

Adres IP i dostęp w sieci

Arduino pracuje z ustawionym na stałe adresem IP, dzięki czemu zawsze jest dostępne pod tym samym adresem, np.:
http://192.168.1.34

Po wejściu w przeglądarce otrzymujemy prostą stronę WWW z aktualnymi danymi BMS, odświeżanymi automatycznie co kilka sekund.
Dodatkowo dostępne jest API JSON pod osobnym endpointem, przeznaczone do integracji z innymi systemami.
iHome
Wersja systemu: 2.10.44, aktualiacja: 2026-04-16
Polityka prywatności
Copyright net2me.pl 2018
stats pixel