Reverse Engineering the Bluetti EL400 Battery

Written by Palina Kuzmina · Summer 2026

This document outlines the process used to reverse engineer the communication protocol for the Bluetti EL400 smart battery. The goal of this project was to be able to send and receive commands from our laptop/raspberry pi directly to the battery, identify the structure of its commands and responses, and determine how its cryptographic layers operate. Our more specific goal was to be able to create custom scheduling when the battery pulls power from the grid.

This is intended to be a technical record of the reverse engineering process rather than a summary of the final solution, and is part of a larger project from my work. It outlines the tools and observations that allowed us to ultimately communicate with the EL400. This goal is for future teams to be able to retrace these steps and understand the reasoning behind each stage of our investigation.

This is part of a larger team project from my Summer 2026 internship at Loisaida Inc, however the majority of the outlined work is my sole contribution.

Bluetti Official Supported Pathways

On the consumer level, Bluetti batteries communicate with an app that can set various parameters like AC/DC input ·and output, scheduling, etc. The phone connects to the battery via bluetooth and then the application sends commands.

Bluetti supports developers to research their cryptography protocol. We contacted their customer support with our intention to monitor and control the battery via MQTT. They did not fully support our use case but, after some questioning, provided development materials that helped us understand their custom handshake protocol, an internal crypto binary, and their python MQTT library for monitoring only (no controls).

Parsing Bluetooth Traffic

To begin reverse engineering this system, our first goal is to analyze the structure of the bluetooth traffic to understand how commands are exchanged between the battery and phone app. Wireshark was used to inspect the captured BLE traffic, specifically the GATT writes from the application and notifications returned by the EL400.

An android phone was connected to the battery and sent a single command setting up a grid schedule, and the logs were extracted using the android bug report feature. Wireshark allows us to isolate logs based on protocol, so looking only at ATT (a Bluetooth Low Energy protocol) we can see the Google phone device communicating with the Espressif microcontroller in the battery.

We actually captured two logs like this to compare what the payload looks like, and we could see the payload of the packets was different each time. This suggests that the application-level commands were not transmitted as readable Modbus frames but rather as encrypted binary packets.

Searching online we found the bluetooth library for the batteries which includes a binary source code for the encryption and decryption process. This confirms that the Bluetooth protocol included session-dependent cryptographic state.

Wireshark log
Wireshark log of Bluetooth traffic

Decompiling the APK with JADX

Our next step was to build a Bluetooth client in Python using Bleak and reproduce the connection process used by Bluetti’s official bluetooth library. The library contains an API called ble_crypt_link_handler which is responsible for progressing through the encrypted connection handshake.

The crypto module reports four main connection states: status 0 for initialization, statuses 1–2 while authentication is in progress, status 3 for serial-number validation, and status 4 when the encrypted link has been successfully established. When we attempted to use the library with the EL400, the connection consistently reached status 3 but never advanced to status 4.

Since the API was essentially calling a black box, we needed to understand both how the Android application was using it and what the library itself was doing internally. First we decompiled the official Bluetti Android application using a software called JADX. This allows us to take a compiled android application and inspect the Java/Kotlin code responsible for communication.Within JADX we were able to identify the application’s communication logic and identify the Modbus registers associated with specific battery functions

One important register we found was IOT_SERVER_BLE_SN = 11127. This register is used by the application to read the serial number of the battery's BLE communication module. A Modbus register can be thought of as a numbered location in the battery’s internal protocol map. Each address corresponds to a specific piece of data or control function, such as a serial number, voltage reading, or output setting. The meaning of every address is predefined by Bluetti’s firmware. A modbus command therefore specifies both an operation (like “read”) and the register address telling the battery which data the operation applies to.

Decompiling the app also led to several other interesting findings. There were two versions of battery firmware: V1 and V2, which do not have any overlapping registers. We knew that the EL400 was a V2 battery so that was the registers we needed. Additionally, some app commands like scheduling need to call write to several registers (this will be relevant later).

JADX decompilation
JADX decompilation of the Android application

Decompiling the crypto source code with Ghidra

After identifying the BLE serial-number register in JADX, we attempted to read register 11127 using the functions provided by the BLUETTI Bluetooth library. Instead of allowing the request, the crypto library returned a "Modbus rang error." To investigate this we used the Ghidra software to analyze the compiled source code.

Ghidra is a software reverse-engineering framework that is used to inspect compiled binaries when the original source code is unavailable. It will disassemble machine code into assembly, and then decompile it into C-like pseudocode making it much easier to understand the logic implemented inside a native library.

Searching for the “Modbus rang error” string in the decompiled text we followed the cross references of the function that generated it. We traced the surrounding control flow and found that the library contained an internal whitelist of permitted Modbus register values and ranges. Before processing a request, the library checked if the requested register starting address and range matched one of the entries in this list, and no valid entry was found; it was rejected and threw the error.

This was a very important finding. It explained why the read request was failing. The embedded whitelist contained V1 register ranges while the EL400 needed the V2 register map. Every whitelist entry contained a starting address and limits on the allowed range and count. This showed that the obstacle was in the library's device-specific validation path, rather than in the Bluetooth transport itself.

The remaining connection failure was similarly traced in Ghidra by locating the relevant error path and following the surrounding function calls and conditional branches. These findings made it possible to understand the secure-connection process without publishing details that could weaken the battery's protections.

Ghidra analysis
Ghidra analysis 2

Sending Commands

At this point we are able to establish connection. To run commands or read telemetry, we identified the V2 registers used by the EL400 and tested the functions that were active for this battery model. The investigation was performed in a controlled research environment; implementation details that could provide unrestricted register access are intentionally omitted.

Certain commands were straightforward to reconstruct since they only required writing to one register. In those cases we could use the existing write methods provided by the Bluetooth library. More complex commands (such as scheduling) were harder to interpret because they involved multiple registers and it was not immediately clear what each represented. To understand them we traced their implementation through the JADX decompilation where the application code revealed their mapping and then reimplemented those as functions in our own code.

At this point we can communicate with the battery and send needed reads/writes, however we are still relying on the black box crypto library. Furthermore, we want this code to run off a raspberry pi rather than our laptop which uses an ARM processor. Since this binary was compiled for x86 and we don’t have the source code, it would not be able to run.

Rewriting the library in python

To remove the dependency on the compiled library and have the freedom to modify the code and compile it for whatever architecture we want, we decided to rewrite it in Python. To do this, we ran a script in Ghidra to extract the call tree of the functions and paste it as a JSON format. This lets us trace every function being called starting from the program entry point ble_crypt_link_handler. Tracing this we identified the high level architecture of the connection process.

Without going into excessive detail, the library used ECDH key generation for the initial handshake and AES for data exchange. Following the generated function calls, we were able to mimic this functionality in Python using the cryptography library. For this process, AI was used since the Ghidra C pseudocode was difficult to parse and it significantly sped up the process of porting the code.

At the end of this process, we were able to move from observing encrypted Bluetooth traffic to communicating with the EL400 independently of the official BLUETTI application. The project required several different reverse-engineering techniques, with each tool answering a different part of the problem.

Responsible Research Note

This page describes the reverse-engineering process at a high level. Details that could weaken device protections, expose credentials, or enable unrestricted control have been intentionally omitted. Any production controller should limit its supported operations, validate battery state before making changes, and keep authentication material private.