esp32poeLarsBatMon
An ESP32-S3 reads a LiFePO4 BMS over BLE and publishes it as MQTT over a W5500 PoE link, where Node-RED turns it into a Victron virtual battery.
0
Project description
Overview
esp32poeLarsBatMon is an open-source battery monitor for a boat named Lars. An ESP32-S3 reads cell-level data out of a LiFePO4 battery's BMS over Bluetooth LE, publishes it as MQTT over a PoE Ethernet link carried by a WIZnet W5500, and a Node-RED flow on a Victron Cerbo GX turns those messages into a virtual battery service on the Victron D-Bus — so the pack appears in VRM and on the GX display alongside the rest of the vessel's energy system.
The repository is MIT-licensed, and the author reports the chain working end to end with live readings cross-checked against both the Victron display and the battery vendor's own app.
이 프로젝트는 보트 Lars에 설치된 LiFePO4 배터리의 BMS를 Bluetooth LE로 읽어, PoE Ethernet(W5500)을 통해 MQTT로 내보내고, Victron Cerbo GX의 Node-RED가 이를 Victron D-Bus의 가상 배터리 서비스로 변환하는 오픈소스 배터리 모니터입니다. 변환된 값은 VRM과 GX 디스플레이에 실제 배터리처럼 표시됩니다.
Purpose
The goal is to bring a closed, Bluetooth-only battery into an established marine energy-monitoring platform over a wired, power-over-the-same-cable link — and to write down the protocol work so the next person does not have to repeat it.
Core Objectives
- Read voltage, current, SoC, SoH, capacity, temperature and per-cell voltages from a Redodo LiFePO4 BMS over BLE GATT
- Publish them as retained MQTT topics over PoE Ethernet, using the W5500 as the wired interface
- Map the values onto Victron D-Bus paths through Node-RED so the pack becomes a first-class device in VRM
- Document the BMS frame format, which is not the common JBD command set
목표는 Bluetooth로만 접근 가능한 폐쇄형 배터리를, 전원과 데이터를 한 가닥으로 받는 유선 링크를 통해 기존 선박용 에너지 모니터링 플랫폼에 편입시키는 것입니다. 그리고 그 과정에서 밝혀낸 프로토콜을 문서로 남겨 다음 사람이 같은 작업을 반복하지 않게 하는 것입니다.
System Architecture
Redodo LiFePO4 12.8 V / 100 Ah (RH190) | BLE GATT service 0xFFE0 - notify 0xFFE1 - write 0xFFE2 v ESP32-S3 (Waveshare ESP32-S3-ETH) | W5500 over SPI CLK 13 - MISO 12 - MOSI 11 - CS 14 - IRQ 10 - RST 9 | 802.3af PoE on the same cable v MQTT Victron Cerbo / Venus GX broker 192.168.24.213:1883 | Node-RED flow (node-red/lifepo01-victron-flow.json) v Victron D-Bus -> VRM / GX display
| Component | Description |
|---|---|
| Battery / BMS | Redodo Power LiFePO4 12.8 V / 100 Ah, model RH190, 4S pack. Exposes a UART-bridge style GATT layout at 0xFFE0 with notify on 0xFFE1 and write on 0xFFE2. |
| Gateway | Waveshare ESP32-S3-ETH (ESP32-S3R8, 16 MB flash, 8 MB octal PSRAM). Runs the BLE client and the MQTT publisher concurrently. |
| Network | WIZnet W5500 over SPI, with an optional 802.3af PoE module on the same board, so one cable carries both power and data to the node. |
| Broker | The Victron Cerbo/Venus GX's own MQTT broker at 192.168.24.213:1883, no authentication. |
| Integration | Node-RED on Venus OS Large. An importable flow feeds a victron-virtual node configured as a 100 Ah / 12 V battery device. |
| Data layer | Three retained topics: LiFePo01/status, LiFePo01/battery, LiFePo01/battery/cells. |
Role and Application of the W5500
Wired interface for a node whose radio is already busy. The ESP32-S3's Bluetooth stack is fully occupied talking to the battery. The W5500 gives the node a second, independent path for its uplink, so BLE collection and MQTT publishing do not compete for the same radio.
Power and data on one cable. The board's optional 802.3af module means the monitor needs no separate supply — relevant on a boat, where every additional run of wire is a installation cost.
How the firmware drives it. Ethernet comes up through the ESP-IDF Ethernet layer:
#include <ETH.h> #define ETH_PHY_TYPE ETH_PHY_W5500 #define ETH_PHY_ADDR 1 #define ETH_PHY_CS 14 #define ETH_PHY_IRQ 10 #define ETH_PHY_RST 9 ETH.begin(ETH_PHY_TYPE, ETH_PHY_ADDR, ETH_PHY_CS, ETH_PHY_IRQ, ETH_PHY_RST, SPI);
What the W5500 does not do here. This is the esp_eth path, so TCP/IP is handled by lwIP on the ESP32 and the W5500 is used as MAC and PHY. The chip's hardwired TCP/IP offload is not part of this design, and the project makes no claim that it is. Readers looking for an offload example should look elsewhere; readers looking for a reliable wired link on a board whose radio is committed elsewhere will find exactly that.
이 프로젝트에서 W5500은 MAC/PHY 역할을 합니다. 펌웨어가 ETH.h의 ETH_PHY_W5500 경로를 쓰기 때문에 TCP/IP 처리는 ESP32 쪽 lwIP가 담당하며, 칩의 하드웨어 TCP/IP 오프로드는 사용되지 않습니다. 대신 W5500이 제공하는 것은 이미 BLE로 점유된 무선과 독립된 유선 경로이고, 보드의 802.3af 모듈 덕분에 전원과 데이터가 한 가닥으로 들어옵니다.
Key Features
| Feature | Description |
|---|---|
| BLE BMS client | Connects to the pack's GATT service, subscribes to notifications, and issues status queries. 배터리 BMS에 GATT로 접속해 notify를 구독하고 상태를 질의합니다. |
| Reverse-engineered frame format | Implements the Redodo/LiTime/PowerQueen command set, cross-checked against two existing open-source projects. 기존 오픈소스 2종과 교차 검증한 명령 포맷을 구현합니다. |
| Fragmented response reassembly | Notifications may arrive split; the reader detects a new frame and reassembles it. 단편화되어 도착하는 응답을 재조립합니다. |
| PoE Ethernet uplink | W5500 over SPI with an optional 802.3af module — one cable for power and data. 전원과 데이터를 한 가닥으로 받습니다. |
| Retained MQTT topics | Status heartbeat, battery summary, and per-cell voltages. 상태·요약·셀 전압을 각각 retained 토픽으로 발행합니다. |
| Victron D-Bus integration | Node-RED flow maps MQTT to Dc/0/Voltage, Soc, TimeToGo and friends, creating a virtual battery service. Node-RED가 MQTT를 Victron D-Bus 경로로 매핑합니다. |
| Secrets kept out of the tree | The battery MAC lives in a gitignored include/secrets.h. 배터리 MAC은 커밋되지 않습니다. |
The part that took the work: identifying the BMS protocol
The GATT layout 0xFFE0/1/2/3 is a generic UART-bridge pattern shared by many unrelated BMS families, so having the right UUIDs was not enough. The author reports that the JBD/Xiaoxiang command set produced zero response despite a provably correct GATT connection — services and characteristics discovered, notify subscribed with peer ACK, writes ACKed.
A full GATT dump also surfaced an unrelated third-party service (f000ffc0-0451-4000-b000-000000000000, a TI CC254x-style base UUID, on what the Device Information Service reports as a Beken BLE SoC) that answers any input with a fixed handshake/error frame — a dead end.
What resolved it was identifying the battery brand from a photo of its label and searching for existing open-source reverse-engineering of that brand, rather than continuing to guess bytes. The implementation is cross-checked against:
- rubenmuehlhans/litime-ble-hacs — Home Assistant integration
- va13ak/esp_redodo_bms — ESPHome, used as a cross-check
The command frame is 8 bytes, written to 0xFFE2:
{0x00, 0x00, 0x04, 0x01, CMD, 0x55, 0xAA, CHECKSUM}
CHECKSUM = (0x04 + CMD) & 0xFF
CMD_QUERY_STATUS = 0x13The status response notifies on 0xFFE1, little-endian, and may arrive fragmented: a new frame starts when byte[2] == 0x65, and the reader reassembles until at least 104 bytes are in hand. Byte offsets and the remaining commands are listed in the header comment of src/main.cpp.
UUID만 맞춰서는 되지 않았습니다. 가장 널리 쓰이는 JBD 명령셋은 GATT 연결이 정상인데도 응답이 전혀 없었고, 결국 배터리 라벨 사진으로 제조사를 특정한 뒤 해당 브랜드의 오픈소스 분석 자료를 찾는 방식으로 해결됐습니다. 응답이 단편화되어 온다는 점과 그 재조립 규칙까지 문서에 남아 있습니다.
A trap worth carrying away: the MQTT payload that failed silently
#define MQTT_MAX_PACKET_SIZE placed before #include <PubSubClient.h> only takes effect in the one translation unit that includes the header. PubSubClient's own source is compiled separately and kept its 256-byte default buffer.
The symptom was asymmetric, which is what made it hard to see: the small LiFePo01/battery/cells payload published fine, while the larger LiFePo01/battery payload — around 600 bytes once a raw_hex copy of the BMS response was included — failed every time. publish()'s return value was not being checked, so nothing announced the failure.
The fix was to stop relying on the compile-time define and call the runtime setters in setup():
client.setBufferSize(2048); client.setKeepAlive(60);
A second robustness fix is recorded as well: a nearby device flooding BLE advertisements (Apple-continuity-style spam) overloaded the scan callback and corrupted serial output, resolved with scan->setDuplicateFilter(true).
컴파일 타임 #define은 헤더를 include한 하나의 translation unit에만 적용되므로, 별도로 컴파일되는 라이브러리 내부는 기본 256바이트 버퍼를 유지합니다. 그래서 큰 페이로드만 조용히 실패하고 작은 것은 계속 성공하는 증상이 나타났습니다. 런타임 설정 함수로 바꾸어 해결했습니다.
Cross-checks the author published
Numbers were read at three independent points and compared. From the ESP32's own MQTT payload:
{"total_voltage_v":13.291,"current_a":-4.369,"power_w":-58.07,
"soc_percent":92,"soh_percent":100,"remaining_capacity_ah":97.15,
"full_charge_capacity_ah":105,"cell_temperature_c":31,
"discharge_cycles":2}Cell voltages read [3.323, 3.323, 3.323, 3.322] — a 4S pack, 4 × 3.2 V ≈ 12.8 V nominal, matching the nameplate.
On the Victron GX battery detail page the author reports SoC 90.0 %, 13.3 V, 4.4 A discharging, 58.0 W, 31.0 °C, min/max cell voltage 3.320 V / 3.322 V, and a remaining time of 21 h 45 min — against the ESP32's own 96.6 Ah / 4.4 A ≈ 21 h 57 min. That agreement is what confirmed TimeToGo was being computed and read correctly on both sides.
The battery vendor's app served as the third reference point (SOC 96 %, 13.3 V, −4.1 A, 100.8 Ah at 2026-08-15 20:40), consistent given the time elapsed between readings.
Engineering Value and Source Limits
What the repository demonstrates is a complete path from a closed BLE battery to an established energy-monitoring platform, with the protocol work written down rather than left implicit in the code. For anyone facing a Redodo, LiTime or PowerQueen pack, the command frame and the reassembly rule alone are worth the visit.
What it does not establish, by the author's own account:
- Ethernet reconnect has not been tested under real conditions. MQTT and BLE reconnect have been; recovery after a cable or switch failure has not.
- There is no watchdog.
esp_task_wdtis listed as a possible addition in case the firmware is ever stuck in a BLE wait state. - Protection and failure flags are published as raw bitmasks only and are not decoded into Victron alarms; the known bit meanings are recorded as future work.
- The virtual battery is monitoring-only. DVCC charger control and the BMS's charge/discharge FET commands are documented as possible extensions, not implemented.
- No throughput or latency figures are published — and none are needed for a once-per-interval telemetry node.
- Part of the README documents a PlatformIO/esptool toolchain problem specific to the author's own machine rather than to the hardware.
저자가 직접 밝힌 미검증 범위입니다. MQTT와 BLE 재접속은 확인됐지만 Ethernet 재접속은 실제 조건에서 테스트되지 않았고, 워치독이 없으며, 보호/고장 플래그는 원시 비트마스크로만 발행되고 Victron 알람으로 변환되지 않습니다. 가상 배터리는 모니터링 전용이며 충전기 제어는 포함되지 않습니다.
Market & Application Value
Marine and off-grid energy systems. Victron is the de-facto platform for boats, campers and off-grid cabins, and a great many drop-in LiFePO4 packs expose their data only over Bluetooth. This project is a worked example of closing that gap on the owner's side, without waiting for the battery vendor to ship an integration.
Why wired matters here. On a vessel the monitoring node and the GX may sit in different compartments, and the ESP32's radio is already committed to the BLE link. A PoE run gives the node power and a stable uplink in one cable — the kind of installation argument that applies equally to industrial cabinets and building automation.
Reusable beyond this battery. The BLE-to-MQTT-to-D-Bus shape is generic. Swap the BMS decoder and the same structure serves any Bluetooth-only sensor that needs to appear on a wired network as a proper device.
Victron은 보트·캠핑카·오프그리드 설비에서 사실상 표준 플랫폼이고, 많은 LiFePO4 배터리가 데이터를 Bluetooth로만 노출합니다. 이 프로젝트는 제조사의 공식 연동을 기다리지 않고 사용자 측에서 그 간극을 메운 사례이며, BLE → MQTT → D-Bus 구조 자체는 다른 Bluetooth 전용 센서에도 그대로 재사용할 수 있습니다.
Source-Backed Summary
| Board | Waveshare ESP32-S3-ETH, W5500 over SPI, optional 802.3af PoE |
|---|---|
| Ethernet path | ETH.h / ETH_PHY_W5500 → esp_eth → lwIP (not hardware TCP/IP offload) |
| Battery | Redodo Power LiFePO4 12.8 V / 100 Ah, model RH190, 4S |
| BLE | GATT 0xFFE0, notify 0xFFE1, write 0xFFE2, fragmented responses reassembled |
| Uplink | MQTT (PubSubClient) to the Venus GX broker, three retained topics |
| Integration | Node-RED flow → Victron D-Bus virtual battery, visible in VRM |
| Licence | MIT |
| Status | Author reports working end to end; Ethernet reconnect untested |
FAQ
Q. Does this use the W5500's hardwired TCP/IP?
No. The firmware uses ETH.h with ETH_PHY_W5500, which is the ESP-IDF esp_eth path — lwIP on the ESP32 handles TCP/IP and the W5500 serves as MAC and PHY.
Q. Why Bluetooth and Ethernet at the same time? Isn't one enough?
They do different jobs here. BLE is the only way into the battery; Ethernet is how the result leaves the node. Both links are live simultaneously, which is also why a wired uplink is preferable to Wi-Fi on the same chip.
Q. Will this work with any LiFePO4 battery?
No. The command set is specific to the Redodo/LiTime/PowerQueen BMS family. The widely documented JBD/Xiaoxiang command set returned nothing on this pack, which is the problem the project had to solve first.
Q. Can it control the charger or trip the battery?
Not as published. The virtual battery is monitoring-only. The author documents the BMS's charge/discharge commands and Victron's DVCC paths as possible extensions, with an explicit note that FET control is safety-critical.
Q. Is it safe to leave running unattended?
The author does not claim so. There is no watchdog, and Ethernet reconnect after a cable or switch failure has not been tested — both are listed as open items.
