Monitor two JK-BMS units with one ESP32-C3
When I added a second battery, I wanted to monitor both BMS units through the ESP32-C3 bridge I already had. One board now maintains two independent Bluetooth connections and exposes each battery's live readings over Wi-Fi. I can select both BMS units and read their telemetry in a browser.
This extends my original configurable JK-BMS bridge. Wi-Fi provisioning and browser-based BMS selection are still part of the firmware. The new work adds a second saved selection and separate voltage, current, cell and diagnostic readings for each battery.
JK-BMS 1 -- BLE --\
ESP32-C3 -- Wi-Fi -- browser
JK-BMS 2 -- BLE --/ \-- ESPHome native API
Each BMS has its own online state, charge percentage, voltage, current, cells and diagnostics. The bridge reads telemetry without changing BMS protection settings.
The hardware I tested
The bridge is an ESP32-C3 board with 4 MB flash and native USB Serial/JTAG, configured as esp32-c3-devkitm-1. Both BMS units are JK-PB1A16S10P models with hardware version 19A. Each monitors a 16-cell, 105 Ah LiFePO4 pack; their software versions are 19.31 and 19.34.
I used ESPHome 2026.6.5, ESP-IDF 5.5.4 and the JK02_32S protocol. The external syssi JK-BMS component is pinned to the commit used in the working build. Its multiple-device example shows the same basic arrangement: one BLE client and one JK parser instance per BMS.
The board needs normal 5 V USB power, coverage from the home 2.4 GHz Wi-Fi network and Bluetooth range to both batteries. A power bank can supply it if the bank keeps its output enabled at the board's load. A USB charger is another option. The computer is used to build and flash; it is not part of the monitoring path.
Two BLE clients and two saved slots
The central change is a second client and parser. ESPHome's BLE component sets the shared connection limit; the BLE Client component provides each connection. Here is the relevant excerpt from the complete downloadable YAML:
esp32_ble:
max_connections: 2
ble_client:
- mac_address: "00:00:00:00:00:01"
id: client0
- mac_address: "00:00:00:00:00:02"
id: client1
jk_bms_selector:
id: bms_selector
ble_client_id: client0
ble_client_2_id: client1
jk_bms_ble:
- ble_client_id: client0
protocol_version: JK02_32S
throttle: 5s
id: bms0
- ble_client_id: client1
protocol_version: JK02_32S
throttle: 5s
id: bms1
The two placeholder addresses are replaced by the selections stored in flash. The local jk_bms_selector component now has two slots. It stores an address and BLE address type for each, rejects assigning the same device twice and applies changes on ESPHome's main loop.
I kept the original slot's NVS namespace and keys. That lets an application OTA update retain the first battery's selection. The second slot has separate keys. A temporarily offline BMS keeps its saved address for reconnection; clearing a slot is a deliberate setup action.
The first battery keeps its existing sensor names. New metrics have a BMS 2 prefix, so each connection publishes its own voltage, current, cells and diagnostics. Adding the second unit does not rename the first unit's readings.
Select and identify both batteries
The bridge first tries the Wi-Fi details in secrets.yaml. If that network is unavailable, it creates the temporary open JK-BMS-C3 Setup access point. Connect to it and open http://192.168.4.1/wifi to configure the local 2.4 GHz network.
After the bridge joins Wi-Fi, open http://jk-bms-c3.local/bms. If mDNS is unavailable, find the bridge in the router's client list and use its assigned address.
- Choose the original unit as BMS 1.
- Choose the additional unit as BMS 2.
- Check the published device information, pack voltage and active cells for each slot.
- Confirm that both online states are true and that both readings continue to update.
Use the known BLE address and then confirm the identity from telemetry. A familiar name or service advertisement is only a discovery hint. Some JK units advertise a serial-style name, and a phone app may show a different suffix from the bridge's scan. Close an app that is holding the BMS connection if the bridge cannot connect.
My original pack was temporarily disconnected from the inverter, but its BMS was still powered and reporting. It correctly showed zero current. A BMS that is actually powered off cannot supply live readings; check its online flag rather than treating the last displayed voltage as current data.
Read both batteries in the browser
Open http://jk-bms-c3.local/ for live telemetry. The web page groups summary values, cell readings and detailed diagnostics separately for BMS 1 and BMS 2. Each unit has its own online status, so a disconnected battery is visible without confusing it with the other pack.
For my 16-cell packs, I check each battery's state of charge, total voltage, current, temperatures, cell minimum and maximum, and all 16 active cell voltages. The YAML also declares channels for larger packs; use only the channels the connected model actually provides.
The metrics page uses ESPHome Web Server v3, which normally loads its JavaScript from the internet. The standalone /bms selection page does not need that JavaScript. Once powered and connected to Wi-Fi, the board runs independently of the computer used to build and flash it.
What I verified on the hardware
A 30-second capture received seven pack-voltage updates from the original BMS and six from the new one. Both were online, with no BMS errors or warning logs. The original pack reported 100%, 53.424 V and 0 A; the new pack reported 73%, 53.701 V and a positive charging current of 12.789 A. These are commissioning samples, not charging targets.
Each pack voltage agreed with the sum of its own 16 cells within 0.01 V. The two native serial readings were different. Together, these checks gave me much better evidence of two separate data streams than two similar voltage readings alone.
After the OTA transfer, the bridge did not immediately return to the network. It came back after normal USB power was restored, without holding BOOT. I did not capture boot logs, so I cannot identify the earlier startup cause. This is an observation from this installation, not evidence of a general ESP32-C3 fault.
The first BMS selection survived that physical power cycle. I saved the second slot afterward and tested the two-slot restore logic with host tests. I have not performed a second physical restart after saving both addresses. On a new installation, checking that both selections return after a restart is still part of commissioning.
Two details that mattered in the implementation
BLE disconnection is asynchronous. The selector now waits for the old client to reach IDLE before changing its remote address. Changing the address during CONNECTING can prevent events from the original peer from matching, leaving the shared scanner unable to continue normally.
The tested ESPHome IDF web adapter also needs its native redirect helper for a successful form submission. A generic 303 response in this version falls through to an error status. The selector uses the helper and checks NVS errors before reporting a saved selection.
The scan cache keeps up to 40 advertisements, but the page renders at most 12 fresh results, with likely JK devices first. This bounds the HTML allocation while both BLE links and the native API use the C3's memory.
Download and build
Download the complete English source package. It includes the firmware, runtime selector, host tests and setup instructions. It contains no real Wi-Fi credentials, device addresses or serial numbers, and no private prebuilt firmware.
Source ZIP SHA-256:
63b6680c70a009f3fdcc13709133881fd63c4ddeda19fddcad7c830d81c3efbc
unzip jk-bms-c3-dual-source.zip
cp source/secrets.example.yaml source/secrets.yaml
# Edit source/secrets.yaml with your own 2.4 GHz Wi-Fi details.
docker run --rm -v "$PWD/source":/config \
ghcr.io/esphome/esphome:2026.6.5 \
config /config/jk-bms-c3.yaml
docker run --rm -v "$PWD/source":/config \
ghcr.io/esphome/esphome:2026.6.5 \
compile /config/jk-bms-c3.yaml
The included README explains the layout, configuration and tests. The YAML pins the JK component to the tested commit. If you use another ESPHome release or JK hardware revision, validate it again rather than assuming that current upstream examples match this build.
The download translates the setup interface into English. It passed the host and schema tests, ESPHome configuration validation and C++ source generation. I did not separately flash that translation; the hardware results above describe the deployed build.
For a compatible bridge that is already running, prefer an ordinary OTA application update and keep a known working source and image. A full erase removes Wi-Fi provisioning and both selections. For a first serial installation, identify the exact chip and port, then save and verify its original flash before writing. For a confirmed 4 MB C3, the backup step can be:
python -m esptool --chip esp32c3 --port /dev/ttyACM0 \
read-flash 0x0 0x400000 esp32-c3-before.bin
python -m esptool --chip esp32c3 --port /dev/ttyACM0 \
verify-flash 0x0 esp32-c3-before.bin
sha256sum esp32-c3-before.bin
Use your actual port and flash size, confirm the file is complete, and keep the backup private: it may contain network credentials. Espressif documents both reading flash and verifying it against a local file. The generated factory image goes at 0x0; it is different from an OTA application image.
Keep the operating limits clear
Both radios must be within range of the bridge. Adding a second slot does not extend Bluetooth coverage or make an unpowered BMS available. Unsupported cell or temperature channels are not zero readings; use the channels the actual BMS provides.
ESPHome documents Wi-Fi connection issues on single-core chips such as ESP32-C3. Its API-gated scanning workaround changes when discovery runs. Consider that trade-off in a standalone bridge: browser access does not establish a native API connection.
The fallback setup AP in this version is temporary and open. The sample API, HTTP and OTA interfaces are for a trusted local network; do not expose them directly to the internet. Battery protection settings, fuses and inverter limits remain separate from this monitoring project.