intial test version
This commit is contained in:
+52
@@ -0,0 +1,52 @@
|
|||||||
|
# PlatformIO
|
||||||
|
.pio
|
||||||
|
.pioenvs
|
||||||
|
.piolibdeps
|
||||||
|
.clang_complete
|
||||||
|
.gcc-flags.json
|
||||||
|
.ccls
|
||||||
|
compile_commands.json
|
||||||
|
|
||||||
|
# VSCode
|
||||||
|
.vscode/
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.hex
|
||||||
|
*.bin
|
||||||
|
*.elf
|
||||||
|
*.eep
|
||||||
|
*.lss
|
||||||
|
*.map
|
||||||
|
*.sym
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
*~
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.idea/
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
|
.atom/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Arduino
|
||||||
|
*.ino.cpp
|
||||||
|
|
||||||
|
*.tar.gz
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Scott Penrose
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# BluettiBLE — Quick Start
|
||||||
|
|
||||||
|
Get a Bluetti power station reporting to an ESP32 in a few minutes.
|
||||||
|
|
||||||
|
## 1. Find your device's BLE name
|
||||||
|
|
||||||
|
Install a BLE scanner on your phone (nRF Connect or LightBlue) and scan. Your
|
||||||
|
Bluetti advertises a name that starts with the model, for example:
|
||||||
|
|
||||||
|
- `AC3001234567890`
|
||||||
|
- `AC200M1234567890`
|
||||||
|
- `EB3A1234567890`
|
||||||
|
|
||||||
|
Copy the **full** advertised name — that string is how the library finds the unit
|
||||||
|
(there is no pairing and no encryption key to enter).
|
||||||
|
|
||||||
|
## 2. Install the library
|
||||||
|
|
||||||
|
PlatformIO (`platformio.ini`):
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[env:esp32dev]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32dev
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
lib_deps =
|
||||||
|
https://gitea.sh3d.com.au/Sh3d/BluettiBLE.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Or copy this folder into `lib/` (PlatformIO) or `libraries/` (Arduino IDE).
|
||||||
|
|
||||||
|
## 3. Minimal sketch
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "BluettiBLE.h"
|
||||||
|
|
||||||
|
BluettiBLE bluetti;
|
||||||
|
|
||||||
|
void onData(const BluettiDevice* dev) {
|
||||||
|
Serial.printf("%s SoC %u%% AC out %u W DC out %u W\n",
|
||||||
|
dev->data.model, dev->data.totalBatteryPercent,
|
||||||
|
dev->data.acOutputPower, dev->data.dcOutputPower);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
bluetti.begin();
|
||||||
|
bluetti.setCallback(onData);
|
||||||
|
bluetti.addDevice("My Bluetti", "AC3001234567890", BLUETTI_AC300); // <- your name + model
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
bluetti.loop();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the model to match your unit: `BLUETTI_AC300`, `BLUETTI_AC200M`,
|
||||||
|
`BLUETTI_EB3A`, `BLUETTI_EP500P`, `BLUETTI_AC500`, `BLUETTI_EP500`,
|
||||||
|
`BLUETTI_EP600`.
|
||||||
|
|
||||||
|
## 4. Upload and watch
|
||||||
|
|
||||||
|
Open the serial monitor at 115200. You should see the library scan, connect, and
|
||||||
|
then print a line every poll cycle (default every 3 seconds). Turn on debug for
|
||||||
|
the raw command/connection trace:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bluetti.setDebug(true);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Control output (optional)
|
||||||
|
|
||||||
|
Once connected you can toggle the outputs:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
if (bluetti.isConnected()) {
|
||||||
|
bluetti.setACOutput(true); // turn AC output on
|
||||||
|
bluetti.setDCOutput(false); // turn DC output off
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Never connects** — double-check the BLE name is exact (case sensitive), and
|
||||||
|
that the Bluetti app on your phone is closed (a Bluetti accepts one BLE client
|
||||||
|
at a time).
|
||||||
|
- **Connects but no data** — make sure the model enum matches your hardware; the
|
||||||
|
register map is model-specific.
|
||||||
|
- **Drops out** — keep `bluetti.loop()` running every iteration; the library
|
||||||
|
reconnects automatically after a disconnect.
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
# BluettiBLE
|
||||||
|
|
||||||
|
ESP32 library for reading and controlling **Bluetti** power stations over
|
||||||
|
Bluetooth Low Energy (BLE).
|
||||||
|
|
||||||
|
It is a companion to [VictronBLE](https://gitea.sh3d.com.au/Sh3d/VictronBLE) and
|
||||||
|
shares its interface philosophy — a single global object, device registration,
|
||||||
|
one callback, a non-blocking `loop()`, and a flat parsed data struct. The key
|
||||||
|
difference is the transport: Victron broadcasts data in BLE *advertisements*
|
||||||
|
(passive, connectionless), whereas Bluetti requires an active **GATT connection**
|
||||||
|
and a polled, Modbus-style request/response protocol. BluettiBLE therefore runs a
|
||||||
|
`scan → connect → poll → parse` state machine under the hood.
|
||||||
|
|
||||||
|
The protocol is ported from the excellent
|
||||||
|
[Bluetti_ESP32_Bridge](https://github.com/) project.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Connects to a Bluetti power station by its BLE advertised name — **no pairing
|
||||||
|
or key required**.
|
||||||
|
- Polls the device on a configurable interval and delivers a parsed snapshot
|
||||||
|
through a single callback.
|
||||||
|
- Reads state of charge, AC/DC input & output power, voltages, frequencies,
|
||||||
|
battery pack and per-cell voltages, model and serial number.
|
||||||
|
- Optional control: toggle AC and DC output.
|
||||||
|
- Supports up to `BLUETTI_MAX_DEVICES` registered devices, connecting to one at a
|
||||||
|
time in round-robin.
|
||||||
|
- Zero external dependencies — uses the stock ESP32 Arduino BLE stack.
|
||||||
|
|
||||||
|
## Supported devices
|
||||||
|
|
||||||
|
| Model | Enum | Status (from reference project) |
|
||||||
|
|----------|------------------|---------------------------------|
|
||||||
|
| AC300 | `BLUETTI_AC300` | Tested |
|
||||||
|
| AC200M | `BLUETTI_AC200M` | Tested (per-cell voltages) |
|
||||||
|
| EB3A | `BLUETTI_EB3A` | Tested |
|
||||||
|
| EP500P | `BLUETTI_EP500P` | Tested (per-cell voltages) |
|
||||||
|
| AC500 | `BLUETTI_AC500` | Minimal / untested |
|
||||||
|
| EP500 | `BLUETTI_EP500` | Untested |
|
||||||
|
| EP600 | `BLUETTI_EP600` | Partial (different register map)|
|
||||||
|
|
||||||
|
## Hardware requirements
|
||||||
|
|
||||||
|
- Any ESP32 (ESP32, ESP32-S3, ESP32-C3 all work) with Bluetooth.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### PlatformIO
|
||||||
|
|
||||||
|
```ini
|
||||||
|
lib_deps =
|
||||||
|
https://gitea.sh3d.com.au/Sh3d/BluettiBLE.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Or drop the folder into your project's `lib/` directory.
|
||||||
|
|
||||||
|
### Arduino IDE
|
||||||
|
|
||||||
|
Copy this folder into your Arduino `libraries/` directory and restart the IDE.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "BluettiBLE.h"
|
||||||
|
|
||||||
|
BluettiBLE bluetti;
|
||||||
|
|
||||||
|
void onBluettiData(const BluettiDevice* dev) {
|
||||||
|
const BluettiData& d = dev->data;
|
||||||
|
Serial.printf("\n=== %s (%s) ===\n", dev->name, d.model);
|
||||||
|
Serial.printf("SoC: %u%%\n", d.totalBatteryPercent);
|
||||||
|
Serial.printf("AC out: %u W DC out: %u W\n", d.acOutputPower, d.dcOutputPower);
|
||||||
|
Serial.printf("AC in: %u W DC in: %u W\n", d.acInputPower, d.dcInputPower);
|
||||||
|
Serial.printf("AC output: %s DC output: %s\n",
|
||||||
|
d.acOutputOn ? "ON" : "off", d.dcOutputOn ? "ON" : "off");
|
||||||
|
Serial.printf("RSSI: %d dBm\n", dev->rssi);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
bluetti.begin();
|
||||||
|
bluetti.setDebug(false);
|
||||||
|
bluetti.setCallback(onBluettiData);
|
||||||
|
|
||||||
|
// The BLE name is what the unit advertises, e.g. "AC3001234567890".
|
||||||
|
bluetti.addDevice("Shed AC300", "AC3001234567890", BLUETTI_AC300);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
bluetti.loop(); // non-blocking state machine
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Finding your device's BLE name
|
||||||
|
|
||||||
|
Use any BLE scanner app (nRF Connect, LightBlue) and look for a device whose name
|
||||||
|
begins with your model (e.g. `AC300…`, `AC200M…`, `EB3A…`). That full advertised
|
||||||
|
name is the second argument to `addDevice()`.
|
||||||
|
|
||||||
|
## API reference
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
BluettiBLE();
|
||||||
|
|
||||||
|
bool begin();
|
||||||
|
bool addDevice(const char* name, const char* bleName, BluettiModel model);
|
||||||
|
void setCallback(BluettiCallback cb);
|
||||||
|
void setDebug(bool enable);
|
||||||
|
void setPollInterval(uint32_t ms); // default 3000
|
||||||
|
bool isConnected() const;
|
||||||
|
size_t getDeviceCount() const;
|
||||||
|
void loop(); // call every loop iteration
|
||||||
|
|
||||||
|
// Control (acts on the currently connected device)
|
||||||
|
bool setACOutput(bool on);
|
||||||
|
bool setDCOutput(bool on);
|
||||||
|
```
|
||||||
|
|
||||||
|
The callback is a plain function pointer:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
typedef void (*BluettiCallback)(const BluettiDevice* device);
|
||||||
|
```
|
||||||
|
|
||||||
|
It fires once per completed poll cycle for the connected device.
|
||||||
|
|
||||||
|
## Data structures
|
||||||
|
|
||||||
|
`BluettiDevice` describes the device and carries the latest snapshot:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct BluettiDevice {
|
||||||
|
char name[32]; // your label
|
||||||
|
char bleName[32]; // advertised name matched on
|
||||||
|
BluettiModel model;
|
||||||
|
int8_t rssi;
|
||||||
|
uint32_t lastUpdate; // millis() of last update
|
||||||
|
bool connected;
|
||||||
|
bool dataValid;
|
||||||
|
BluettiData data;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`BluettiData` is a flat superset of values across models. Fields a given model
|
||||||
|
does not report stay at `0`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct BluettiData {
|
||||||
|
char model[16];
|
||||||
|
uint64_t serialNumber;
|
||||||
|
float armVersion, dspVersion;
|
||||||
|
uint8_t totalBatteryPercent; // %
|
||||||
|
uint16_t dcInputPower, acInputPower; // W
|
||||||
|
uint16_t acOutputPower, dcOutputPower; // W
|
||||||
|
float powerGeneration; // kWh total
|
||||||
|
bool acOutputOn, dcOutputOn;
|
||||||
|
float acInputVoltage, acInputFrequency;
|
||||||
|
float internalAcVoltage, internalAcFrequency;
|
||||||
|
float internalDcInputVoltage, internalDcInputCurrent;
|
||||||
|
float packVoltage;
|
||||||
|
uint8_t packNum, packNumMax, packBatteryPercent;
|
||||||
|
float cellVoltage[16]; // V, 0 if unsupported
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. **Scan** — active BLE scan for a device advertising the Bluetti service UUID
|
||||||
|
`0000ff00-…` whose advertised name matches a registered device.
|
||||||
|
2. **Connect** — open a GATT client, negotiate MTU 517, resolve the write
|
||||||
|
(`0000ff02-…`) and notify (`0000ff01-…`) characteristics, subscribe to notify.
|
||||||
|
3. **Poll** — every `pollInterval`, send an 8-byte read command
|
||||||
|
(`prefix, cmd=0x03, page, offset, count, CRC-16/MODBUS`) for the next register
|
||||||
|
range in the model's poll table.
|
||||||
|
4. **Parse** — notification responses are decoded against the model's register
|
||||||
|
map (big-endian uint16, decimals, version, serial, strings) into a rolling
|
||||||
|
snapshot; a completed cycle fires the callback.
|
||||||
|
|
||||||
|
Control writes use the same frame with `cmd=0x06` and the value placed
|
||||||
|
big-endian in the length field.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
- **BasicRead** — connect to one device and print all values.
|
||||||
|
- **Control** — read, then toggle AC/DC output on a schedule.
|
||||||
|
- **Logger** — print only when values change (snapshot/change-detection pattern).
|
||||||
|
|
||||||
|
## Adding a new model
|
||||||
|
|
||||||
|
Copy a header in `src/devices/`, port the register/poll/command tables from the
|
||||||
|
Bluetti_ESP32_Bridge project's `Device_*.h`, add a `BluettiModel` enum value, and
|
||||||
|
register it in `getModelTables()` in `BluettiBLE.cpp`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — see [LICENSE](LICENSE).
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[env]
|
||||||
|
lib_extra_dirs = ../..
|
||||||
|
|
||||||
|
[env:esp32dev]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32dev
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_filters = esp32_exception_decoder
|
||||||
|
|
||||||
|
[env:esp32-s3]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32-s3-devkitc-1
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_filters = esp32_exception_decoder
|
||||||
|
build_flags =
|
||||||
|
-D ARDUINO_USB_MODE=1
|
||||||
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* BluettiBLE Basic Read Example
|
||||||
|
*
|
||||||
|
* Connects to a single Bluetti power station and prints its values every poll
|
||||||
|
* cycle.
|
||||||
|
*
|
||||||
|
* Setup:
|
||||||
|
* 1. Find your device's BLE advertised name with a BLE scanner app
|
||||||
|
* (e.g. "AC3001234567890").
|
||||||
|
* 2. Set the name and model below.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "BluettiBLE.h"
|
||||||
|
|
||||||
|
BluettiBLE bluetti;
|
||||||
|
|
||||||
|
void onBluettiData(const BluettiDevice* dev) {
|
||||||
|
const BluettiData& d = dev->data;
|
||||||
|
|
||||||
|
Serial.printf("\n=== %s (%s) ===\n", dev->name, d.model[0] ? d.model : "?");
|
||||||
|
Serial.printf("Serial: %llu\n", d.serialNumber);
|
||||||
|
Serial.printf("Firmware: ARM %.2f / DSP %.2f\n", d.armVersion, d.dspVersion);
|
||||||
|
Serial.printf("State of charge: %u %%\n", d.totalBatteryPercent);
|
||||||
|
Serial.printf("AC output: %u W (%s)\n", d.acOutputPower, d.acOutputOn ? "ON" : "off");
|
||||||
|
Serial.printf("DC output: %u W (%s)\n", d.dcOutputPower, d.dcOutputOn ? "ON" : "off");
|
||||||
|
Serial.printf("AC input: %u W DC input: %u W\n", d.acInputPower, d.dcInputPower);
|
||||||
|
Serial.printf("Generation total: %.1f kWh\n", d.powerGeneration);
|
||||||
|
|
||||||
|
if (d.acInputVoltage > 0)
|
||||||
|
Serial.printf("AC in: %.1f V @ %.2f Hz\n", d.acInputVoltage, d.acInputFrequency);
|
||||||
|
if (d.internalDcInputVoltage > 0)
|
||||||
|
Serial.printf("DC in: %.1f V %.1f A\n", d.internalDcInputVoltage, d.internalDcInputCurrent);
|
||||||
|
if (d.packNumMax > 0)
|
||||||
|
Serial.printf("Battery packs: %u (pack #%u %.1f V %u%%)\n",
|
||||||
|
d.packNumMax, d.packNum, d.packVoltage, d.packBatteryPercent);
|
||||||
|
|
||||||
|
bool anyCell = false;
|
||||||
|
for (int i = 0; i < BLUETTI_CELLS; i++) if (d.cellVoltage[i] > 0) anyCell = true;
|
||||||
|
if (anyCell) {
|
||||||
|
Serial.print("Cells: ");
|
||||||
|
for (int i = 0; i < BLUETTI_CELLS; i++)
|
||||||
|
if (d.cellVoltage[i] > 0) Serial.printf("%.2f ", d.cellVoltage[i]);
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf("RSSI: %d dBm (updated %lus ago)\n",
|
||||||
|
dev->rssi, (millis() - dev->lastUpdate) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
Serial.println("\n\n=============================");
|
||||||
|
Serial.println("BluettiBLE Basic Read Example");
|
||||||
|
Serial.println("=============================\n");
|
||||||
|
|
||||||
|
if (!bluetti.begin()) {
|
||||||
|
Serial.println("ERROR: failed to initialise BluettiBLE!");
|
||||||
|
while (1) delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
bluetti.setDebug(true);
|
||||||
|
bluetti.setCallback(onBluettiData);
|
||||||
|
|
||||||
|
// CHANGE THESE: your device's advertised BLE name and model.
|
||||||
|
// bluetti.addDevice("My Bluetti", "AC3001234567890", BLUETTI_AC300);
|
||||||
|
bluetti.addDevice("My Bluetti", "EL3002546110146262", BLUETTI_AC300);
|
||||||
|
|
||||||
|
Serial.printf("Configured %d device(s). Scanning...\n", (int)bluetti.getDeviceCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
bluetti.loop();
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[env]
|
||||||
|
lib_extra_dirs = ../..
|
||||||
|
|
||||||
|
[env:esp32dev]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32dev
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_filters = esp32_exception_decoder
|
||||||
|
|
||||||
|
[env:esp32-s3]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32-s3-devkitc-1
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_filters = esp32_exception_decoder
|
||||||
|
build_flags =
|
||||||
|
-D ARDUINO_USB_MODE=1
|
||||||
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* BluettiBLE Control Example
|
||||||
|
*
|
||||||
|
* Reads the device and toggles the AC output on/off every 30 seconds to show how
|
||||||
|
* to send control commands. DC output toggling works the same way via
|
||||||
|
* setDCOutput().
|
||||||
|
*
|
||||||
|
* WARNING: this actively switches your inverter output. Only run it when it is
|
||||||
|
* safe to power-cycle whatever is plugged in.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "BluettiBLE.h"
|
||||||
|
|
||||||
|
BluettiBLE bluetti;
|
||||||
|
|
||||||
|
static uint32_t lastToggle = 0;
|
||||||
|
static bool desiredAC = true;
|
||||||
|
|
||||||
|
void onBluettiData(const BluettiDevice* dev) {
|
||||||
|
const BluettiData& d = dev->data;
|
||||||
|
Serial.printf("[%s] SoC %u%% AC out %u W (%s) DC out %u W (%s)\n",
|
||||||
|
d.model, d.totalBatteryPercent,
|
||||||
|
d.acOutputPower, d.acOutputOn ? "ON" : "off",
|
||||||
|
d.dcOutputPower, d.dcOutputOn ? "ON" : "off");
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
Serial.println("\nBluettiBLE Control Example\n");
|
||||||
|
|
||||||
|
bluetti.begin();
|
||||||
|
bluetti.setDebug(true);
|
||||||
|
bluetti.setCallback(onBluettiData);
|
||||||
|
|
||||||
|
// CHANGE THESE: your device's advertised BLE name and model.
|
||||||
|
bluetti.addDevice("My Bluetti", "AC3001234567890", BLUETTI_AC300);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
bluetti.loop();
|
||||||
|
|
||||||
|
if (bluetti.isConnected() && millis() - lastToggle > 30000) {
|
||||||
|
Serial.printf(">> setting AC output %s\n", desiredAC ? "ON" : "OFF");
|
||||||
|
if (bluetti.setACOutput(desiredAC))
|
||||||
|
Serial.println(" command sent");
|
||||||
|
else
|
||||||
|
Serial.println(" command not supported / not connected");
|
||||||
|
desiredAC = !desiredAC;
|
||||||
|
lastToggle = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[env]
|
||||||
|
lib_extra_dirs = ../..
|
||||||
|
|
||||||
|
[env:esp32dev]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32dev
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_filters = time, esp32_exception_decoder
|
||||||
|
|
||||||
|
[env:esp32-s3]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32-s3-devkitc-1
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
monitor_filters = time, esp32_exception_decoder
|
||||||
|
build_flags =
|
||||||
|
-D ARDUINO_USB_MODE=1
|
||||||
|
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* BluettiBLE Logger Example
|
||||||
|
*
|
||||||
|
* Prints a line only when a value of interest changes, rather than every poll
|
||||||
|
* cycle. Demonstrates the snapshot / change-detection pattern.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "BluettiBLE.h"
|
||||||
|
|
||||||
|
BluettiBLE bluetti;
|
||||||
|
|
||||||
|
// Last-seen snapshot of the fields we care about.
|
||||||
|
struct Snapshot {
|
||||||
|
bool valid = false;
|
||||||
|
uint8_t soc = 0;
|
||||||
|
uint16_t acOut = 0;
|
||||||
|
uint16_t dcOut = 0;
|
||||||
|
bool acOn = false;
|
||||||
|
bool dcOn = false;
|
||||||
|
uint32_t packetsSinceLog = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
static Snapshot prev;
|
||||||
|
|
||||||
|
void onBluettiData(const BluettiDevice* dev) {
|
||||||
|
const BluettiData& d = dev->data;
|
||||||
|
prev.packetsSinceLog++;
|
||||||
|
|
||||||
|
bool changed = !prev.valid ||
|
||||||
|
prev.soc != d.totalBatteryPercent ||
|
||||||
|
prev.acOut != d.acOutputPower ||
|
||||||
|
prev.dcOut != d.dcOutputPower ||
|
||||||
|
prev.acOn != d.acOutputOn ||
|
||||||
|
prev.dcOn != d.dcOutputOn;
|
||||||
|
|
||||||
|
if (!changed) return;
|
||||||
|
|
||||||
|
Serial.printf("CHANGE (%lu polls): SoC %u%% AC %u W (%s) DC %u W (%s)\n",
|
||||||
|
prev.packetsSinceLog,
|
||||||
|
d.totalBatteryPercent,
|
||||||
|
d.acOutputPower, d.acOutputOn ? "ON" : "off",
|
||||||
|
d.dcOutputPower, d.dcOutputOn ? "ON" : "off");
|
||||||
|
|
||||||
|
prev.valid = true;
|
||||||
|
prev.soc = d.totalBatteryPercent;
|
||||||
|
prev.acOut = d.acOutputPower;
|
||||||
|
prev.dcOut = d.dcOutputPower;
|
||||||
|
prev.acOn = d.acOutputOn;
|
||||||
|
prev.dcOn = d.dcOutputOn;
|
||||||
|
prev.packetsSinceLog = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
Serial.println("\nBluettiBLE Logger Example\n");
|
||||||
|
|
||||||
|
bluetti.begin();
|
||||||
|
bluetti.setCallback(onBluettiData);
|
||||||
|
|
||||||
|
// CHANGE THESE: your device's advertised BLE name and model.
|
||||||
|
bluetti.addDevice("My Bluetti", "AC3001234567890", BLUETTI_AC300);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
bluetti.loop();
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#######################################
|
||||||
|
# Syntax Coloring Map For BluettiBLE
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Datatypes (KEYWORD1)
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
BluettiBLE KEYWORD1
|
||||||
|
BluettiDevice KEYWORD1
|
||||||
|
BluettiData KEYWORD1
|
||||||
|
BluettiCallback KEYWORD1
|
||||||
|
bluettiCommand KEYWORD1
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Methods and Functions (KEYWORD2)
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
begin KEYWORD2
|
||||||
|
addDevice KEYWORD2
|
||||||
|
setCallback KEYWORD2
|
||||||
|
setDebug KEYWORD2
|
||||||
|
setPollInterval KEYWORD2
|
||||||
|
isConnected KEYWORD2
|
||||||
|
getDeviceCount KEYWORD2
|
||||||
|
loop KEYWORD2
|
||||||
|
setACOutput KEYWORD2
|
||||||
|
setDCOutput KEYWORD2
|
||||||
|
|
||||||
|
#######################################
|
||||||
|
# Constants (LITERAL1)
|
||||||
|
#######################################
|
||||||
|
|
||||||
|
BLUETTI_UNKNOWN LITERAL1
|
||||||
|
BLUETTI_AC300 LITERAL1
|
||||||
|
BLUETTI_AC200M LITERAL1
|
||||||
|
BLUETTI_EB3A LITERAL1
|
||||||
|
BLUETTI_EP500P LITERAL1
|
||||||
|
BLUETTI_AC500 LITERAL1
|
||||||
|
BLUETTI_EP500 LITERAL1
|
||||||
|
BLUETTI_EP600 LITERAL1
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"name": "bluettible",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "ESP32 library for reading and controlling Bluetti power stations via Bluetooth Low Energy (BLE). Connects over GATT and polls the Bluetti Modbus-style protocol. Supports AC300, AC200M, EB3A, EP500P and other models.",
|
||||||
|
"keywords": "bluetti, ble, bluetooth, solar, power station, battery, inverter, ac300, ac200m, eb3a, ep500p, esp32, iot, energy, monitoring",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://gitea.sh3d.com.au/Sh3d/BluettiBLE.git"
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Scott Penrose",
|
||||||
|
"email": "scottp@dd.com.au",
|
||||||
|
"url": "https://gitea.sh3d.com.au/Sh3d",
|
||||||
|
"maintainer": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://gitea.sh3d.com.au/Sh3d/BluettiBLE",
|
||||||
|
"frameworks": ["arduino", "espidf"],
|
||||||
|
"platforms": ["espressif32"],
|
||||||
|
"headers": ["BluettiBLE.h"],
|
||||||
|
"dependencies": [],
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"name": "BasicRead",
|
||||||
|
"base": "examples/BasicRead",
|
||||||
|
"files": ["src/main.cpp"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Control",
|
||||||
|
"base": "examples/Control",
|
||||||
|
"files": ["src/main.cpp"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Logger",
|
||||||
|
"base": "examples/Logger",
|
||||||
|
"files": ["src/main.cpp"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"export": {
|
||||||
|
"exclude": [
|
||||||
|
"examples/*/.pio",
|
||||||
|
"examples/*/.vscode",
|
||||||
|
"examples/*/test",
|
||||||
|
"test",
|
||||||
|
"tests",
|
||||||
|
".git",
|
||||||
|
".gitignore"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
name=BluettiBLE
|
||||||
|
version=0.1.0
|
||||||
|
author=Scott Penrose
|
||||||
|
maintainer=Scott Penrose <scottp@dd.com.au>
|
||||||
|
sentence=ESP32 library for reading and controlling Bluetti power stations via BLE
|
||||||
|
paragraph=Connect to Bluetti power stations (AC300, AC200M, EB3A, EP500P and more) over Bluetooth Low Energy. Polls the Bluetti Modbus-style protocol and delivers parsed data through a single callback. Supports toggling AC and DC output. No pairing required.
|
||||||
|
category=Communication
|
||||||
|
url=https://gitea.sh3d.com.au/Sh3d/BluettiBLE
|
||||||
|
architectures=esp32
|
||||||
|
depends=
|
||||||
|
includes=BluettiBLE.h
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
/**
|
||||||
|
* BluettiBLE implementation — see BluettiBLE.h for the public interface.
|
||||||
|
*
|
||||||
|
* Protocol ported from the Bluetti_ESP32_Bridge project.
|
||||||
|
* Copyright (c) 2026 Scott Penrose — License: MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "BluettiBLE.h"
|
||||||
|
#include "BluettiCRC.h"
|
||||||
|
#include "devices/BluettiFields.h"
|
||||||
|
|
||||||
|
// Per-model register tables (each header defines static const arrays).
|
||||||
|
#include "devices/Device_AC300.h"
|
||||||
|
#include "devices/Device_AC200M.h"
|
||||||
|
#include "devices/Device_EB3A.h"
|
||||||
|
#include "devices/Device_EP500P.h"
|
||||||
|
#include "devices/Device_AC500.h"
|
||||||
|
#include "devices/Device_EP500.h"
|
||||||
|
#include "devices/Device_EP600.h"
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
// --- BLE UUIDs (ported from BTooth.h) ---
|
||||||
|
static BLEUUID BLUETTI_SERVICE_UUID("0000ff00-0000-1000-8000-00805f9b34fb");
|
||||||
|
static BLEUUID BLUETTI_WRITE_UUID ("0000ff02-0000-1000-8000-00805f9b34fb");
|
||||||
|
static BLEUUID BLUETTI_NOTIFY_UUID ("0000ff01-0000-1000-8000-00805f9b34fb");
|
||||||
|
|
||||||
|
static constexpr int HEADER_SIZE = 4; // matches PayloadParser.h
|
||||||
|
|
||||||
|
// Single active instance — the ESP32 BLE notify/scan callbacks are plain C
|
||||||
|
// function pointers with no user-data argument, so we route through this.
|
||||||
|
static BluettiBLE* s_instance = nullptr;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Model registry
|
||||||
|
// ============================================================
|
||||||
|
struct ModelTables {
|
||||||
|
const bluetti_field_t* state;
|
||||||
|
size_t stateCount;
|
||||||
|
const bluetti_poll_t* poll;
|
||||||
|
size_t pollCount;
|
||||||
|
const bluetti_field_t* command;
|
||||||
|
size_t commandCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
#define TABLES(P) { P##_state, sizeof(P##_state)/sizeof(bluetti_field_t), \
|
||||||
|
P##_poll, sizeof(P##_poll)/sizeof(bluetti_poll_t), \
|
||||||
|
P##_command, sizeof(P##_command)/sizeof(bluetti_field_t) }
|
||||||
|
|
||||||
|
static bool getModelTables(BluettiModel m, ModelTables& out) {
|
||||||
|
switch (m) {
|
||||||
|
case BLUETTI_AC300: out = (ModelTables)TABLES(AC300); return true;
|
||||||
|
case BLUETTI_AC200M: out = (ModelTables)TABLES(AC200M); return true;
|
||||||
|
case BLUETTI_EB3A: out = (ModelTables)TABLES(EB3A); return true;
|
||||||
|
case BLUETTI_EP500P: out = (ModelTables)TABLES(EP500P); return true;
|
||||||
|
case BLUETTI_AC500: out = (ModelTables)TABLES(AC500); return true;
|
||||||
|
case BLUETTI_EP500: out = (ModelTables)TABLES(EP500); return true;
|
||||||
|
case BLUETTI_EP600:
|
||||||
|
out = { EP600_state, sizeof(EP600_state)/sizeof(bluetti_field_t),
|
||||||
|
EP600_poll, sizeof(EP600_poll)/sizeof(bluetti_poll_t),
|
||||||
|
nullptr, 0 };
|
||||||
|
return true;
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Construction / setup
|
||||||
|
// ============================================================
|
||||||
|
BluettiBLE::BluettiBLE()
|
||||||
|
: deviceCount(0), pBLEScan(nullptr), pClient(nullptr),
|
||||||
|
pWriteChar(nullptr), pNotifyChar(nullptr), foundDevice(nullptr),
|
||||||
|
scanCallbackObj(nullptr), clientCallbackObj(nullptr),
|
||||||
|
callback(nullptr), debugEnabled(false), pollIntervalMs(3000),
|
||||||
|
initialized(false), state(STATE_IDLE), activeIndex(-1), pollTick(0),
|
||||||
|
scanComplete(false), lastPollTime(0), pendingPage(0), pendingOffset(0) {
|
||||||
|
memset(devices, 0, sizeof(devices));
|
||||||
|
memset(&working, 0, sizeof(working));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BluettiBLE::begin() {
|
||||||
|
if (initialized) return true;
|
||||||
|
s_instance = this;
|
||||||
|
|
||||||
|
BLEDevice::init("BluettiBLE");
|
||||||
|
BLEDevice::setMTU(517);
|
||||||
|
|
||||||
|
pBLEScan = BLEDevice::getScan();
|
||||||
|
scanCallbackObj = new BluettiBLEAdvertisedDeviceCallbacks(this);
|
||||||
|
clientCallbackObj = new BluettiBLEClientCallbacks(this);
|
||||||
|
pBLEScan->setAdvertisedDeviceCallbacks(scanCallbackObj, true);
|
||||||
|
pBLEScan->setActiveScan(true);
|
||||||
|
pBLEScan->setInterval(1349);
|
||||||
|
pBLEScan->setWindow(449);
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
|
state = STATE_IDLE;
|
||||||
|
if (debugEnabled) Serial.println("[BluettiBLE] initialised");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BluettiBLE::addDevice(const char* name, const char* bleName, BluettiModel model) {
|
||||||
|
if (deviceCount >= BLUETTI_MAX_DEVICES) return false;
|
||||||
|
if (!bleName || strlen(bleName) == 0) return false;
|
||||||
|
ModelTables t;
|
||||||
|
if (!getModelTables(model, t)) return false;
|
||||||
|
|
||||||
|
DeviceEntry* e = &devices[deviceCount];
|
||||||
|
memset(e, 0, sizeof(DeviceEntry));
|
||||||
|
e->active = true;
|
||||||
|
strncpy(e->device.name, name ? name : bleName, BLUETTI_NAME_LEN - 1);
|
||||||
|
strncpy(e->device.bleName, bleName, BLUETTI_NAME_LEN - 1);
|
||||||
|
e->device.model = model;
|
||||||
|
e->device.rssi = -100;
|
||||||
|
deviceCount++;
|
||||||
|
|
||||||
|
if (debugEnabled) Serial.printf("[BluettiBLE] Added: %s (%s)\n",
|
||||||
|
e->device.name, e->device.bleName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BluettiBLE::isConnected() const {
|
||||||
|
return state == STATE_CONNECTED && pClient && pClient->isConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Static trampolines
|
||||||
|
// ============================================================
|
||||||
|
void BluettiBLE::scanDoneTrampoline(BLEScanResults results) {
|
||||||
|
if (s_instance) s_instance->scanComplete = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluettiBLE::notifyTrampoline(BLERemoteCharacteristic* c, uint8_t* data,
|
||||||
|
size_t length, bool isNotify) {
|
||||||
|
if (s_instance) s_instance->onNotify(data, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluettiBLEAdvertisedDeviceCallbacks::onResult(BLEAdvertisedDevice advertisedDevice) {
|
||||||
|
if (owner) owner->onScanResult(advertisedDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluettiBLEClientCallbacks::onDisconnect(BLEClient* c) {
|
||||||
|
if (owner) owner->onDisconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// State machine
|
||||||
|
// ============================================================
|
||||||
|
void BluettiBLE::loop() {
|
||||||
|
if (!initialized || deviceCount == 0) return;
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case STATE_IDLE:
|
||||||
|
startScan();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case STATE_SCANNING:
|
||||||
|
if (scanComplete) {
|
||||||
|
pBLEScan->stop();
|
||||||
|
if (foundDevice) {
|
||||||
|
state = STATE_CONNECTING;
|
||||||
|
} else {
|
||||||
|
// No match this round — rotate to the next device and retry.
|
||||||
|
activeIndex = (activeIndex + 1) % (int)deviceCount;
|
||||||
|
state = STATE_IDLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case STATE_CONNECTING:
|
||||||
|
connectActive();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case STATE_CONNECTED:
|
||||||
|
if (!isConnected()) {
|
||||||
|
onDisconnect();
|
||||||
|
} else if (millis() - lastPollTime >= pollIntervalMs) {
|
||||||
|
sendPoll();
|
||||||
|
lastPollTime = millis();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluettiBLE::startScan() {
|
||||||
|
// Choose the next active device to look for.
|
||||||
|
if (activeIndex < 0) activeIndex = 0;
|
||||||
|
while (activeIndex < (int)deviceCount && !devices[activeIndex].active)
|
||||||
|
activeIndex = (activeIndex + 1) % (int)deviceCount;
|
||||||
|
|
||||||
|
if (foundDevice) { delete foundDevice; foundDevice = nullptr; }
|
||||||
|
scanComplete = false;
|
||||||
|
pBLEScan->clearResults();
|
||||||
|
if (debugEnabled)
|
||||||
|
Serial.printf("[BluettiBLE] scanning for %s\n", devices[activeIndex].device.bleName);
|
||||||
|
if (pBLEScan->start(5, scanDoneTrampoline, false))
|
||||||
|
state = STATE_SCANNING;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluettiBLE::onScanResult(BLEAdvertisedDevice advertisedDevice) {
|
||||||
|
if (activeIndex < 0 || activeIndex >= (int)deviceCount) return;
|
||||||
|
const char* want = devices[activeIndex].device.bleName;
|
||||||
|
|
||||||
|
bool hasBluettiService = advertisedDevice.haveServiceUUID() &&
|
||||||
|
advertisedDevice.isAdvertisingService(BLUETTI_SERVICE_UUID);
|
||||||
|
|
||||||
|
if (debugEnabled) {
|
||||||
|
const char* nm = advertisedDevice.getName().c_str();
|
||||||
|
Serial.printf("[BluettiBLE] seen: name='%s' rssi=%d bluetti=%s%s\n",
|
||||||
|
(nm && nm[0]) ? nm : "(no name)",
|
||||||
|
advertisedDevice.getRSSI(),
|
||||||
|
hasBluettiService ? "yes" : "no",
|
||||||
|
(strcmp(nm, want) == 0) ? " <-- MATCHES" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasBluettiService &&
|
||||||
|
strcmp(advertisedDevice.getName().c_str(), want) == 0) {
|
||||||
|
if (foundDevice) delete foundDevice;
|
||||||
|
foundDevice = new BLEAdvertisedDevice(advertisedDevice);
|
||||||
|
devices[activeIndex].device.rssi = advertisedDevice.getRSSI();
|
||||||
|
pBLEScan->stop();
|
||||||
|
scanComplete = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluettiBLE::connectActive() {
|
||||||
|
if (!foundDevice) { state = STATE_IDLE; return; }
|
||||||
|
|
||||||
|
if (!pClient) {
|
||||||
|
pClient = BLEDevice::createClient();
|
||||||
|
pClient->setClientCallbacks(clientCallbackObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debugEnabled) Serial.println("[BluettiBLE] connecting...");
|
||||||
|
if (!pClient->connect(foundDevice)) {
|
||||||
|
if (debugEnabled) Serial.println("[BluettiBLE] connect failed");
|
||||||
|
onDisconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BLERemoteService* svc = pClient->getService(BLUETTI_SERVICE_UUID);
|
||||||
|
if (!svc) { if (debugEnabled) Serial.println("[BluettiBLE] no service"); pClient->disconnect(); onDisconnect(); return; }
|
||||||
|
|
||||||
|
pWriteChar = svc->getCharacteristic(BLUETTI_WRITE_UUID);
|
||||||
|
pNotifyChar = svc->getCharacteristic(BLUETTI_NOTIFY_UUID);
|
||||||
|
if (!pWriteChar || !pNotifyChar) {
|
||||||
|
if (debugEnabled) Serial.println("[BluettiBLE] missing characteristics");
|
||||||
|
pClient->disconnect();
|
||||||
|
onDisconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pNotifyChar->canNotify())
|
||||||
|
pNotifyChar->registerForNotify(notifyTrampoline);
|
||||||
|
|
||||||
|
devices[activeIndex].device.connected = true;
|
||||||
|
memset(&working, 0, sizeof(working));
|
||||||
|
pollTick = 0;
|
||||||
|
lastPollTime = millis() - pollIntervalMs; // poll immediately
|
||||||
|
state = STATE_CONNECTED;
|
||||||
|
if (debugEnabled) Serial.println("[BluettiBLE] connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluettiBLE::onDisconnect() {
|
||||||
|
if (activeIndex >= 0 && activeIndex < (int)deviceCount)
|
||||||
|
devices[activeIndex].device.connected = false;
|
||||||
|
pWriteChar = nullptr;
|
||||||
|
pNotifyChar = nullptr;
|
||||||
|
// Rotate to the next device so multiple registrations get a turn.
|
||||||
|
if (deviceCount > 0) activeIndex = (activeIndex + 1) % (int)deviceCount;
|
||||||
|
state = STATE_IDLE;
|
||||||
|
if (debugEnabled) Serial.println("[BluettiBLE] disconnected");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Polling
|
||||||
|
// ============================================================
|
||||||
|
void BluettiBLE::sendPoll() {
|
||||||
|
ModelTables t;
|
||||||
|
if (!getModelTables(devices[activeIndex].device.model, t)) return;
|
||||||
|
if (t.pollCount == 0) return;
|
||||||
|
if (pollTick >= t.pollCount) pollTick = 0;
|
||||||
|
|
||||||
|
const bluetti_poll_t& p = t.poll[pollTick];
|
||||||
|
bluettiCommand cmd;
|
||||||
|
cmd.prefix = 0x01;
|
||||||
|
cmd.cmd = 0x03; // read
|
||||||
|
cmd.page = p.page;
|
||||||
|
cmd.offset = p.offset;
|
||||||
|
cmd.len = (uint16_t)p.count << 8; // big-endian register count
|
||||||
|
cmd.checkSum = bluettiModbusCrc((uint8_t*)&cmd, 6);
|
||||||
|
|
||||||
|
pendingPage = p.page;
|
||||||
|
pendingOffset = p.offset;
|
||||||
|
sendCommand(cmd);
|
||||||
|
|
||||||
|
pollTick++;
|
||||||
|
if (pollTick >= t.pollCount) {
|
||||||
|
// Completed a full cycle: publish the rolling snapshot.
|
||||||
|
pollTick = 0;
|
||||||
|
BluettiDevice& d = devices[activeIndex].device;
|
||||||
|
memcpy(&d.data, &working, sizeof(BluettiData));
|
||||||
|
d.dataValid = true;
|
||||||
|
d.lastUpdate = millis();
|
||||||
|
if (pClient) d.rssi = pClient->getRssi();
|
||||||
|
if (callback) callback(&d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BluettiBLE::sendCommand(const bluettiCommand& cmd) {
|
||||||
|
if (!pWriteChar) return false;
|
||||||
|
if (debugEnabled) {
|
||||||
|
Serial.print("[BluettiBLE] >> ");
|
||||||
|
const uint8_t* b = (const uint8_t*)&cmd;
|
||||||
|
for (int i = 0; i < 8; i++) Serial.printf("%02x", b[i]);
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
pWriteChar->writeValue((uint8_t*)&cmd, sizeof(cmd), true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Response parsing (arithmetic ported from PayloadParser.cpp)
|
||||||
|
// ============================================================
|
||||||
|
void BluettiBLE::onNotify(uint8_t* pData, size_t length) {
|
||||||
|
if (length < (size_t)HEADER_SIZE) return;
|
||||||
|
parseResponse(pendingPage, pendingOffset, pData, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluettiBLE::parseResponse(uint8_t reqPage, uint8_t reqOffset,
|
||||||
|
uint8_t* pData, size_t length) {
|
||||||
|
if (pData[1] != 0x03) return; // only handle range-read responses
|
||||||
|
|
||||||
|
ModelTables t;
|
||||||
|
if (!getModelTables(devices[activeIndex].device.model, t)) return;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < t.stateCount; i++) {
|
||||||
|
const bluetti_field_t& f = t.state[i];
|
||||||
|
if (f.page != reqPage) continue;
|
||||||
|
if (f.offset < reqOffset) continue;
|
||||||
|
|
||||||
|
int delta = (int)f.offset - (int)reqOffset;
|
||||||
|
if ((2 * delta) + HEADER_SIZE > (int)length) continue;
|
||||||
|
if ((2 * (delta + f.size)) + HEADER_SIZE > (int)length) continue;
|
||||||
|
|
||||||
|
// 1-based indexing quirk from the reference: data begins at pData[2*delta+3]
|
||||||
|
int dataStart = (2 * delta) + HEADER_SIZE;
|
||||||
|
const uint8_t* bytes = &pData[dataStart - 1];
|
||||||
|
applyField(f.name, f.scale, bytes, f.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluettiBLE::applyField(int fieldName, uint8_t scale,
|
||||||
|
const uint8_t* b, int sizeRegs) {
|
||||||
|
auto u16 = [&](int i) -> uint16_t {
|
||||||
|
return ((uint16_t)b[i] << 8) | (uint16_t)b[i + 1];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cell voltages occupy a contiguous enum range.
|
||||||
|
if (fieldName >= BF_CELL01_VOLTAGE && fieldName <= BF_CELL16_VOLTAGE) {
|
||||||
|
working.cellVoltage[fieldName - BF_CELL01_VOLTAGE] = u16(0) / powf(10, scale);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (fieldName) {
|
||||||
|
case BF_DEVICE_TYPE: {
|
||||||
|
int n = 2 * sizeRegs;
|
||||||
|
if (n > BLUETTI_MODEL_LEN - 1) n = BLUETTI_MODEL_LEN - 1;
|
||||||
|
memcpy(working.model, b, n);
|
||||||
|
working.model[n] = '\0';
|
||||||
|
// trim trailing spaces/nulls
|
||||||
|
for (int i = n - 1; i >= 0 && (working.model[i] == ' ' || working.model[i] == 0); i--)
|
||||||
|
working.model[i] = '\0';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case BF_SERIAL_NUMBER: {
|
||||||
|
uint16_t v1 = u16(0), v2 = u16(2), v3 = u16(4), v4 = u16(6);
|
||||||
|
working.serialNumber = ((uint64_t)v1) | ((uint64_t)v2 << 16) |
|
||||||
|
((uint64_t)v3 << 32) | ((uint64_t)v4 << 48);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case BF_ARM_VERSION:
|
||||||
|
case BF_DSP_VERSION: {
|
||||||
|
uint16_t low = u16(0), high = u16(2);
|
||||||
|
long val = (long)low | ((long)high << 16);
|
||||||
|
float ver = (float)val / 100.0f;
|
||||||
|
if (fieldName == BF_ARM_VERSION) working.armVersion = ver;
|
||||||
|
else working.dspVersion = ver;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case BF_DC_INPUT_POWER: working.dcInputPower = u16(0); break;
|
||||||
|
case BF_AC_INPUT_POWER: working.acInputPower = u16(0); break;
|
||||||
|
case BF_AC_OUTPUT_POWER: working.acOutputPower = u16(0); break;
|
||||||
|
case BF_DC_OUTPUT_POWER: working.dcOutputPower = u16(0); break;
|
||||||
|
case BF_POWER_GENERATION: working.powerGeneration = u16(0) / powf(10, scale); break;
|
||||||
|
case BF_TOTAL_BATTERY_PERCENT: working.totalBatteryPercent = (uint8_t)u16(0); break;
|
||||||
|
case BF_AC_OUTPUT_ON: working.acOutputOn = (b[1] == 1); break;
|
||||||
|
case BF_DC_OUTPUT_ON: working.dcOutputOn = (b[1] == 1); break;
|
||||||
|
case BF_AC_INPUT_VOLTAGE: working.acInputVoltage = u16(0) / powf(10, scale); break;
|
||||||
|
case BF_AC_INPUT_FREQUENCY: working.acInputFrequency = u16(0) / powf(10, scale); break;
|
||||||
|
case BF_INTERNAL_AC_VOLTAGE: working.internalAcVoltage = u16(0) / powf(10, scale); break;
|
||||||
|
case BF_INTERNAL_AC_FREQUENCY: working.internalAcFrequency = u16(0) / powf(10, scale); break;
|
||||||
|
case BF_INTERNAL_DC_INPUT_VOLTAGE: working.internalDcInputVoltage = u16(0) / powf(10, scale); break;
|
||||||
|
case BF_INTERNAL_DC_INPUT_CURRENT: working.internalDcInputCurrent = u16(0) / powf(10, scale); break;
|
||||||
|
case BF_INTERNAL_PACK_VOLTAGE: working.packVoltage = u16(0) / powf(10, scale); break;
|
||||||
|
case BF_PACK_NUM_MAX: working.packNumMax = (uint8_t)u16(0); break;
|
||||||
|
case BF_PACK_NUM: working.packNum = (uint8_t)u16(0); break;
|
||||||
|
case BF_PACK_BATTERY_PERCENT: working.packBatteryPercent = (uint8_t)u16(0); break;
|
||||||
|
default: break; // fields without a struct member are ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Control
|
||||||
|
// ============================================================
|
||||||
|
bool BluettiBLE::writeControl(int fieldName, uint16_t value) {
|
||||||
|
if (!isConnected()) return false;
|
||||||
|
ModelTables t;
|
||||||
|
if (!getModelTables(devices[activeIndex].device.model, t)) return false;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < t.commandCount; i++) {
|
||||||
|
if ((int)t.command[i].name != fieldName) continue;
|
||||||
|
bluettiCommand cmd;
|
||||||
|
cmd.prefix = 0x01;
|
||||||
|
cmd.cmd = 0x06; // write
|
||||||
|
cmd.page = t.command[i].page;
|
||||||
|
cmd.offset = t.command[i].offset;
|
||||||
|
cmd.len = bluettiSwapBytes(value); // value big-endian on the wire
|
||||||
|
cmd.checkSum = bluettiModbusCrc((uint8_t*)&cmd, 6);
|
||||||
|
return sendCommand(cmd);
|
||||||
|
}
|
||||||
|
return false; // model has no such writable control
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BluettiBLE::setACOutput(bool on) { return writeControl(BF_AC_OUTPUT_ON, on ? 1 : 0); }
|
||||||
|
bool BluettiBLE::setDCOutput(bool on) { return writeControl(BF_DC_OUTPUT_ON, on ? 1 : 0); }
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
/**
|
||||||
|
* BluettiBLE - ESP32 library for Bluetti power stations via Bluetooth Low Energy
|
||||||
|
*
|
||||||
|
* Connects to a Bluetti power station over BLE GATT, polls its registers with the
|
||||||
|
* Bluetti Modbus-style protocol, and delivers parsed data through a single
|
||||||
|
* callback. Modelled on the VictronBLE library's interface, but Bluetti requires
|
||||||
|
* an active connection (not passive advertisements), so the library runs a
|
||||||
|
* connect -> poll -> parse state machine instead of a passive scanner.
|
||||||
|
*
|
||||||
|
* Protocol ported from the Bluetti_ESP32_Bridge project.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Scott Penrose
|
||||||
|
* License: MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BLUETTI_BLE_H
|
||||||
|
#define BLUETTI_BLE_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <BLEDevice.h>
|
||||||
|
#include <BLEAdvertisedDevice.h>
|
||||||
|
#include <BLEScan.h>
|
||||||
|
|
||||||
|
// --- Constants ---
|
||||||
|
static constexpr int BLUETTI_MAX_DEVICES = 4;
|
||||||
|
static constexpr int BLUETTI_NAME_LEN = 32; // user label / advertised name
|
||||||
|
static constexpr int BLUETTI_MODEL_LEN = 16; // DEVICE_TYPE string
|
||||||
|
static constexpr int BLUETTI_CELLS = 16;
|
||||||
|
|
||||||
|
// --- Models (selects the register table) ---
|
||||||
|
enum BluettiModel {
|
||||||
|
BLUETTI_UNKNOWN = 0,
|
||||||
|
BLUETTI_AC300,
|
||||||
|
BLUETTI_AC200M,
|
||||||
|
BLUETTI_EB3A,
|
||||||
|
BLUETTI_EP500P,
|
||||||
|
BLUETTI_AC500,
|
||||||
|
BLUETTI_EP500,
|
||||||
|
BLUETTI_EP600
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Wire-format command frame (8 bytes) — ported from BTooth.h
|
||||||
|
// ============================================================
|
||||||
|
// prefix : always 0x01
|
||||||
|
// cmd : 0x03 = read/poll, 0x06 = write
|
||||||
|
// page : register page (0x00, 0x07, 0x08, 0x0B, ...)
|
||||||
|
// offset : starting register offset
|
||||||
|
// len : read = registerCount << 8; write = swapBytes(value)
|
||||||
|
// checkSum : CRC-16/MODBUS over the first 6 bytes
|
||||||
|
struct bluettiCommand {
|
||||||
|
uint8_t prefix;
|
||||||
|
uint8_t cmd;
|
||||||
|
uint8_t page;
|
||||||
|
uint8_t offset;
|
||||||
|
uint16_t len;
|
||||||
|
uint16_t checkSum;
|
||||||
|
} __attribute__((packed));
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Parsed data (flat struct, superset across models)
|
||||||
|
// Fields a given model does not report stay at 0.
|
||||||
|
// ============================================================
|
||||||
|
struct BluettiData {
|
||||||
|
char model[BLUETTI_MODEL_LEN]; // e.g. "AC300"
|
||||||
|
uint64_t serialNumber;
|
||||||
|
float armVersion;
|
||||||
|
float dspVersion;
|
||||||
|
|
||||||
|
uint8_t totalBatteryPercent; // state of charge %
|
||||||
|
uint16_t dcInputPower; // W
|
||||||
|
uint16_t acInputPower; // W
|
||||||
|
uint16_t acOutputPower; // W
|
||||||
|
uint16_t dcOutputPower; // W
|
||||||
|
float powerGeneration; // kWh total
|
||||||
|
bool acOutputOn;
|
||||||
|
bool dcOutputOn;
|
||||||
|
|
||||||
|
float acInputVoltage; // V
|
||||||
|
float acInputFrequency; // Hz
|
||||||
|
float internalAcVoltage; // V
|
||||||
|
float internalAcFrequency; // Hz
|
||||||
|
float internalDcInputVoltage; // V
|
||||||
|
float internalDcInputCurrent; // A
|
||||||
|
|
||||||
|
float packVoltage; // V (selected pack)
|
||||||
|
uint8_t packNum; // currently reported pack number
|
||||||
|
uint8_t packNumMax; // number of packs
|
||||||
|
uint8_t packBatteryPercent; // %
|
||||||
|
float cellVoltage[BLUETTI_CELLS];// V, 0 if unsupported
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Device descriptor handed to the callback
|
||||||
|
// ============================================================
|
||||||
|
struct BluettiDevice {
|
||||||
|
char name[BLUETTI_NAME_LEN]; // user label
|
||||||
|
char bleName[BLUETTI_NAME_LEN]; // advertised name to match
|
||||||
|
BluettiModel model;
|
||||||
|
int8_t rssi;
|
||||||
|
uint32_t lastUpdate; // millis() of last full update
|
||||||
|
bool connected;
|
||||||
|
bool dataValid;
|
||||||
|
BluettiData data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Callback — simple function pointer (no virtual class)
|
||||||
|
// ============================================================
|
||||||
|
typedef void (*BluettiCallback)(const BluettiDevice* device);
|
||||||
|
|
||||||
|
// Forward declarations for the BLE scan/client glue.
|
||||||
|
class BluettiBLEAdvertisedDeviceCallbacks;
|
||||||
|
class BluettiBLEClientCallbacks;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Main BluettiBLE class
|
||||||
|
// ============================================================
|
||||||
|
class BluettiBLE {
|
||||||
|
public:
|
||||||
|
BluettiBLE();
|
||||||
|
|
||||||
|
bool begin();
|
||||||
|
bool addDevice(const char* name, const char* bleName, BluettiModel model);
|
||||||
|
void setCallback(BluettiCallback cb) { callback = cb; }
|
||||||
|
void setDebug(bool enable) { debugEnabled = enable; }
|
||||||
|
void setPollInterval(uint32_t ms) { pollIntervalMs = ms; }
|
||||||
|
bool isConnected() const;
|
||||||
|
size_t getDeviceCount() const { return deviceCount; }
|
||||||
|
void loop();
|
||||||
|
|
||||||
|
// Control: act on the currently connected device.
|
||||||
|
bool setACOutput(bool on);
|
||||||
|
bool setDCOutput(bool on);
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class BluettiBLEAdvertisedDeviceCallbacks;
|
||||||
|
friend class BluettiBLEClientCallbacks;
|
||||||
|
|
||||||
|
enum State { STATE_IDLE, STATE_SCANNING, STATE_CONNECTING, STATE_CONNECTED };
|
||||||
|
|
||||||
|
struct DeviceEntry {
|
||||||
|
BluettiDevice device;
|
||||||
|
bool active;
|
||||||
|
};
|
||||||
|
|
||||||
|
DeviceEntry devices[BLUETTI_MAX_DEVICES];
|
||||||
|
size_t deviceCount;
|
||||||
|
|
||||||
|
BLEScan* pBLEScan;
|
||||||
|
BLEClient* pClient;
|
||||||
|
BLERemoteCharacteristic* pWriteChar;
|
||||||
|
BLERemoteCharacteristic* pNotifyChar;
|
||||||
|
BLEAdvertisedDevice* foundDevice; // matched during scan
|
||||||
|
BluettiBLEAdvertisedDeviceCallbacks* scanCallbackObj;
|
||||||
|
BluettiBLEClientCallbacks* clientCallbackObj;
|
||||||
|
|
||||||
|
BluettiCallback callback;
|
||||||
|
bool debugEnabled;
|
||||||
|
uint32_t pollIntervalMs;
|
||||||
|
bool initialized;
|
||||||
|
|
||||||
|
State state;
|
||||||
|
int activeIndex; // device currently connected / being connected
|
||||||
|
size_t pollTick; // index into the model's poll table
|
||||||
|
bool scanComplete; // set by scan-done callback
|
||||||
|
uint32_t lastPollTime;
|
||||||
|
uint8_t pendingPage; // page/offset of the in-flight poll request
|
||||||
|
uint8_t pendingOffset;
|
||||||
|
BluettiData working; // accumulates one poll cycle before publishing
|
||||||
|
|
||||||
|
// BLE callback hooks
|
||||||
|
void onScanResult(BLEAdvertisedDevice dev);
|
||||||
|
void onDisconnect();
|
||||||
|
void onNotify(uint8_t* pData, size_t length);
|
||||||
|
|
||||||
|
// state machine helpers
|
||||||
|
void startScan();
|
||||||
|
void connectActive();
|
||||||
|
void sendPoll();
|
||||||
|
bool sendCommand(const bluettiCommand& cmd);
|
||||||
|
|
||||||
|
// parsing
|
||||||
|
void parseResponse(uint8_t reqPage, uint8_t reqOffset, uint8_t* pData, size_t length);
|
||||||
|
void applyField(int fieldName, uint8_t scale, const uint8_t* bytes, int sizeRegs);
|
||||||
|
|
||||||
|
// control helper: look up a writable field for the active model and write it
|
||||||
|
bool writeControl(int fieldName, uint16_t value);
|
||||||
|
|
||||||
|
static void scanDoneTrampoline(BLEScanResults results);
|
||||||
|
static void notifyTrampoline(BLERemoteCharacteristic* c, uint8_t* data,
|
||||||
|
size_t length, bool isNotify);
|
||||||
|
};
|
||||||
|
|
||||||
|
// BLE scan callback (required by the ESP32 BLE API).
|
||||||
|
class BluettiBLEAdvertisedDeviceCallbacks : public BLEAdvertisedDeviceCallbacks {
|
||||||
|
public:
|
||||||
|
BluettiBLEAdvertisedDeviceCallbacks(BluettiBLE* parent) : owner(parent) {}
|
||||||
|
void onResult(BLEAdvertisedDevice advertisedDevice) override;
|
||||||
|
private:
|
||||||
|
BluettiBLE* owner;
|
||||||
|
};
|
||||||
|
|
||||||
|
// BLE client callback for connect/disconnect events.
|
||||||
|
class BluettiBLEClientCallbacks : public BLEClientCallbacks {
|
||||||
|
public:
|
||||||
|
BluettiBLEClientCallbacks(BluettiBLE* parent) : owner(parent) {}
|
||||||
|
void onConnect(BLEClient* c) override {}
|
||||||
|
void onDisconnect(BLEClient* c) override;
|
||||||
|
private:
|
||||||
|
BluettiBLE* owner;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BLUETTI_BLE_H
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* BluettiCRC - CRC-16/MODBUS helpers for the Bluetti BLE protocol
|
||||||
|
*
|
||||||
|
* Ported from the Bluetti_ESP32_Bridge project (crc16.h / utils.cpp).
|
||||||
|
* Polynomial 0xA001 (reversed 0x8005), initial value 0xFFFF.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Scott Penrose
|
||||||
|
* License: MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BLUETTI_CRC_H
|
||||||
|
#define BLUETTI_CRC_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
// Update a running CRC-16/MODBUS with one byte.
|
||||||
|
static inline uint16_t bluettiCrc16Update(uint16_t crc, uint8_t a) {
|
||||||
|
crc ^= a;
|
||||||
|
for (int i = 0; i < 8; ++i) {
|
||||||
|
if (crc & 1)
|
||||||
|
crc = (crc >> 1) ^ 0xA001;
|
||||||
|
else
|
||||||
|
crc = (crc >> 1);
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRC-16/MODBUS over a buffer (init 0xFFFF).
|
||||||
|
static inline uint16_t bluettiModbusCrc(const uint8_t* buf, size_t len) {
|
||||||
|
uint16_t crc = 0xFFFF;
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
crc = bluettiCrc16Update(crc, buf[i]);
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap the two bytes of a 16-bit value (used to place write values big-endian).
|
||||||
|
static inline uint16_t bluettiSwapBytes(uint16_t v) {
|
||||||
|
return (uint16_t)((v << 8) | (v >> 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // BLUETTI_CRC_H
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/**
|
||||||
|
* BluettiFields - field and register-table definitions for Bluetti devices
|
||||||
|
*
|
||||||
|
* The shape mirrors the Bluetti_ESP32_Bridge `device_field_data_t` table so the
|
||||||
|
* per-model register maps port across cleanly. Each model header (Device_*.h)
|
||||||
|
* provides three tables built from these types:
|
||||||
|
* - a state table (bluetti_field_t[]) : fields to extract from poll responses
|
||||||
|
* - a poll table (bluetti_poll_t[]) : register ranges to request
|
||||||
|
* - a command table (bluetti_field_t[]) : writable controls
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 Scott Penrose
|
||||||
|
* License: MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef BLUETTI_FIELDS_H
|
||||||
|
#define BLUETTI_FIELDS_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// How a raw register value is decoded.
|
||||||
|
enum BluettiFieldType {
|
||||||
|
B_UINT, // big-endian uint16
|
||||||
|
B_BOOL, // second byte == 1
|
||||||
|
B_ENUM, // enum value (decoded as uint16)
|
||||||
|
B_STRING, // raw ASCII
|
||||||
|
B_DECIMAL, // uint16 / 10^scale
|
||||||
|
B_VERSION, // 4 bytes -> uint32 / 100
|
||||||
|
B_SN, // 8 bytes -> uint64
|
||||||
|
B_TYPE_UNDEFINED
|
||||||
|
};
|
||||||
|
|
||||||
|
// Logical field identity. Used to map a parsed value onto a BluettiData member.
|
||||||
|
enum BluettiFieldName {
|
||||||
|
BF_DEVICE_TYPE,
|
||||||
|
BF_SERIAL_NUMBER,
|
||||||
|
BF_ARM_VERSION,
|
||||||
|
BF_DSP_VERSION,
|
||||||
|
BF_DC_INPUT_POWER,
|
||||||
|
BF_AC_INPUT_POWER,
|
||||||
|
BF_AC_OUTPUT_POWER,
|
||||||
|
BF_DC_OUTPUT_POWER,
|
||||||
|
BF_POWER_GENERATION,
|
||||||
|
BF_TOTAL_BATTERY_PERCENT,
|
||||||
|
BF_AC_OUTPUT_ON,
|
||||||
|
BF_DC_OUTPUT_ON,
|
||||||
|
BF_AC_OUTPUT_MODE,
|
||||||
|
BF_INTERNAL_AC_VOLTAGE,
|
||||||
|
BF_INTERNAL_AC_FREQUENCY,
|
||||||
|
BF_INTERNAL_CURRENT_ONE,
|
||||||
|
BF_INTERNAL_POWER_ONE,
|
||||||
|
BF_INTERNAL_CURRENT_TWO,
|
||||||
|
BF_INTERNAL_POWER_TWO,
|
||||||
|
BF_INTERNAL_CURRENT_THREE,
|
||||||
|
BF_INTERNAL_POWER_THREE,
|
||||||
|
BF_AC_INPUT_VOLTAGE,
|
||||||
|
BF_AC_INPUT_FREQUENCY,
|
||||||
|
BF_INTERNAL_DC_INPUT_VOLTAGE,
|
||||||
|
BF_INTERNAL_DC_INPUT_POWER,
|
||||||
|
BF_INTERNAL_DC_INPUT_CURRENT,
|
||||||
|
BF_INTERNAL_PACK_VOLTAGE,
|
||||||
|
BF_PACK_NUM_MAX,
|
||||||
|
BF_PACK_NUM,
|
||||||
|
BF_PACK_BATTERY_PERCENT,
|
||||||
|
BF_UPS_MODE,
|
||||||
|
BF_GRID_CHARGE_ON,
|
||||||
|
BF_AUTO_SLEEP_MODE,
|
||||||
|
BF_POWER_OFF,
|
||||||
|
BF_CELL01_VOLTAGE, // 16 consecutive cell-voltage fields must stay in order
|
||||||
|
BF_CELL02_VOLTAGE,
|
||||||
|
BF_CELL03_VOLTAGE,
|
||||||
|
BF_CELL04_VOLTAGE,
|
||||||
|
BF_CELL05_VOLTAGE,
|
||||||
|
BF_CELL06_VOLTAGE,
|
||||||
|
BF_CELL07_VOLTAGE,
|
||||||
|
BF_CELL08_VOLTAGE,
|
||||||
|
BF_CELL09_VOLTAGE,
|
||||||
|
BF_CELL10_VOLTAGE,
|
||||||
|
BF_CELL11_VOLTAGE,
|
||||||
|
BF_CELL12_VOLTAGE,
|
||||||
|
BF_CELL13_VOLTAGE,
|
||||||
|
BF_CELL14_VOLTAGE,
|
||||||
|
BF_CELL15_VOLTAGE,
|
||||||
|
BF_CELL16_VOLTAGE,
|
||||||
|
BF_BATTERY_MIN_PERCENTAGE,
|
||||||
|
BF_AC_CHARGE_MAX_PERCENTAGE,
|
||||||
|
BF_AC_INPUT_POWER_MAX,
|
||||||
|
BF_AC_INPUT_CURRENT_MAX,
|
||||||
|
BF_AC_OUTPUT_POWER_MAX,
|
||||||
|
BF_AC_OUTPUT_CURRENT_MAX,
|
||||||
|
BF_UNDEFINED
|
||||||
|
};
|
||||||
|
|
||||||
|
// One field within a register page.
|
||||||
|
// page : protocol page (0x00, 0x07, 0x08, 0x0B, ...)
|
||||||
|
// offset : starting register offset within the page
|
||||||
|
// size : number of 16-bit registers
|
||||||
|
// scale : decimal scale (value / 10^scale) for B_DECIMAL
|
||||||
|
struct bluetti_field_t {
|
||||||
|
BluettiFieldName name;
|
||||||
|
uint8_t page;
|
||||||
|
uint8_t offset;
|
||||||
|
int8_t size;
|
||||||
|
int8_t scale;
|
||||||
|
int8_t fenum;
|
||||||
|
BluettiFieldType type;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A register range to request in a single read poll.
|
||||||
|
struct bluetti_poll_t {
|
||||||
|
uint8_t page;
|
||||||
|
uint8_t offset;
|
||||||
|
uint8_t count; // number of 16-bit registers
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BLUETTI_FIELDS_H
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Bluetti AC200M register map.
|
||||||
|
* Ported from Bluetti_ESP32_Bridge/Device_AC200M.h (tested model).
|
||||||
|
*/
|
||||||
|
#ifndef BLUETTI_DEVICE_AC200M_H
|
||||||
|
#define BLUETTI_DEVICE_AC200M_H
|
||||||
|
|
||||||
|
#include "BluettiFields.h"
|
||||||
|
|
||||||
|
static const bluetti_field_t AC200M_state[] = {
|
||||||
|
{BF_DEVICE_TYPE, 0x00, 0x0A, 7, 0, 0, B_STRING},
|
||||||
|
{BF_SERIAL_NUMBER, 0x00, 0x11, 4, 0, 0, B_SN},
|
||||||
|
{BF_ARM_VERSION, 0x00, 0x17, 2, 0, 0, B_VERSION},
|
||||||
|
{BF_DSP_VERSION, 0x00, 0x19, 2, 0, 0, B_VERSION},
|
||||||
|
{BF_DC_INPUT_POWER, 0x00, 0x24, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_INPUT_POWER, 0x00, 0x25, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_OUTPUT_POWER, 0x00, 0x26, 1, 0, 0, B_UINT},
|
||||||
|
{BF_DC_OUTPUT_POWER, 0x00, 0x27, 1, 0, 0, B_UINT},
|
||||||
|
{BF_POWER_GENERATION, 0x00, 0x29, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_TOTAL_BATTERY_PERCENT, 0x00, 0x2B, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_OUTPUT_ON, 0x00, 0x30, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_DC_OUTPUT_ON, 0x00, 0x31, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_INTERNAL_AC_VOLTAGE, 0x00, 0x47, 1, 0, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_AC_FREQUENCY, 0x00, 0x4A, 2, 1, 0, B_DECIMAL},
|
||||||
|
{BF_AC_INPUT_VOLTAGE, 0x00, 0x4D, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_DC_INPUT_VOLTAGE, 0x00, 0x56, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_PACK_NUM_MAX, 0x00, 0x5B, 1, 0, 0, B_UINT},
|
||||||
|
{BF_INTERNAL_PACK_VOLTAGE, 0x00, 0x5C, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL01_VOLTAGE, 0x00, 0x69, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL02_VOLTAGE, 0x00, 0x6A, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL03_VOLTAGE, 0x00, 0x6B, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL04_VOLTAGE, 0x00, 0x6C, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL05_VOLTAGE, 0x00, 0x6D, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL06_VOLTAGE, 0x00, 0x6E, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL07_VOLTAGE, 0x00, 0x6F, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL08_VOLTAGE, 0x00, 0x70, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL09_VOLTAGE, 0x00, 0x71, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL10_VOLTAGE, 0x00, 0x72, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL11_VOLTAGE, 0x00, 0x73, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL12_VOLTAGE, 0x00, 0x74, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL13_VOLTAGE, 0x00, 0x75, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL14_VOLTAGE, 0x00, 0x76, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL15_VOLTAGE, 0x00, 0x77, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL16_VOLTAGE, 0x00, 0x78, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_AUTO_SLEEP_MODE, 0x0B, 0xF5, 1, 0, 0, B_UINT},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const bluetti_poll_t AC200M_poll[] = {
|
||||||
|
{0x00, 0x0A, 0x7F},
|
||||||
|
{0x0B, 0xB9, 0x3F},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const bluetti_field_t AC200M_command[] = {
|
||||||
|
{BF_DC_OUTPUT_ON, 0x0B, 0xC0, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_AC_OUTPUT_ON, 0x0B, 0xBF, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_AUTO_SLEEP_MODE, 0x0B, 0xF5, 1, 0, 0, B_UINT},
|
||||||
|
{BF_POWER_OFF, 0x0B, 0xF4, 1, 0, 0, B_BOOL},
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BLUETTI_DEVICE_AC200M_H
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Bluetti AC300 register map.
|
||||||
|
* Ported from Bluetti_ESP32_Bridge/Device_AC300.h (tested model).
|
||||||
|
*/
|
||||||
|
#ifndef BLUETTI_DEVICE_AC300_H
|
||||||
|
#define BLUETTI_DEVICE_AC300_H
|
||||||
|
|
||||||
|
#include "BluettiFields.h"
|
||||||
|
|
||||||
|
static const bluetti_field_t AC300_state[] = {
|
||||||
|
// Page 0x00 core
|
||||||
|
{BF_DEVICE_TYPE, 0x00, 0x0A, 7, 0, 0, B_STRING},
|
||||||
|
{BF_SERIAL_NUMBER, 0x00, 0x11, 4, 0, 0, B_SN},
|
||||||
|
{BF_ARM_VERSION, 0x00, 0x17, 2, 0, 0, B_VERSION},
|
||||||
|
{BF_DSP_VERSION, 0x00, 0x19, 2, 0, 0, B_VERSION},
|
||||||
|
{BF_DC_INPUT_POWER, 0x00, 0x24, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_INPUT_POWER, 0x00, 0x25, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_OUTPUT_POWER, 0x00, 0x26, 1, 0, 0, B_UINT},
|
||||||
|
{BF_DC_OUTPUT_POWER, 0x00, 0x27, 1, 0, 0, B_UINT},
|
||||||
|
{BF_POWER_GENERATION, 0x00, 0x29, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_TOTAL_BATTERY_PERCENT, 0x00, 0x2B, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_OUTPUT_ON, 0x00, 0x30, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_DC_OUTPUT_ON, 0x00, 0x31, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_AC_OUTPUT_MODE, 0x00, 0x46, 1, 0, 0, B_UINT},
|
||||||
|
{BF_INTERNAL_AC_VOLTAGE, 0x00, 0x47, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_CURRENT_ONE, 0x00, 0x48, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_POWER_ONE, 0x00, 0x49, 1, 0, 0, B_UINT},
|
||||||
|
{BF_INTERNAL_AC_FREQUENCY, 0x00, 0x4A, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_CURRENT_TWO, 0x00, 0x4B, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_POWER_TWO, 0x00, 0x4C, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_INPUT_VOLTAGE, 0x00, 0x4D, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_CURRENT_THREE, 0x00, 0x4E, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_POWER_THREE, 0x00, 0x4F, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_INPUT_FREQUENCY, 0x00, 0x50, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_DC_INPUT_VOLTAGE, 0x00, 0x56, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_DC_INPUT_POWER, 0x00, 0x57, 1, 0, 0, B_UINT},
|
||||||
|
{BF_INTERNAL_DC_INPUT_CURRENT, 0x00, 0x58, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_PACK_NUM_MAX, 0x00, 0x5B, 1, 0, 0, B_UINT},
|
||||||
|
{BF_INTERNAL_PACK_VOLTAGE, 0x00, 0x5C, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_PACK_NUM, 0x00, 0x60, 1, 0, 0, B_UINT},
|
||||||
|
{BF_PACK_BATTERY_PERCENT, 0x00, 0x63, 1, 0, 0, B_UINT},
|
||||||
|
// Page 0x0B controls
|
||||||
|
{BF_UPS_MODE, 0x0B, 0xB9, 1, 0, 0, B_UINT},
|
||||||
|
{BF_GRID_CHARGE_ON, 0x0B, 0xC3, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_AUTO_SLEEP_MODE, 0x0B, 0xF5, 1, 0, 0, B_UINT},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const bluetti_poll_t AC300_poll[] = {
|
||||||
|
{0x00, 0x0A, 0x28},
|
||||||
|
{0x00, 0x46, 0x15},
|
||||||
|
{0x0B, 0xDA, 0x01},
|
||||||
|
{0x0B, 0xF5, 0x07},
|
||||||
|
{0x00, 0x5B, 0x25},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const bluetti_field_t AC300_command[] = {
|
||||||
|
{BF_AC_OUTPUT_ON, 0x0B, 0xBF, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_DC_OUTPUT_ON, 0x0B, 0xC0, 1, 0, 0, B_BOOL},
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BLUETTI_DEVICE_AC300_H
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Bluetti AC500 register map.
|
||||||
|
* Ported from Bluetti_ESP32_Bridge/Device_AC500.h (minimal/untested support).
|
||||||
|
*/
|
||||||
|
#ifndef BLUETTI_DEVICE_AC500_H
|
||||||
|
#define BLUETTI_DEVICE_AC500_H
|
||||||
|
|
||||||
|
#include "BluettiFields.h"
|
||||||
|
|
||||||
|
static const bluetti_field_t AC500_state[] = {
|
||||||
|
{BF_DEVICE_TYPE, 0x00, 0x0A, 7, 0, 0, B_STRING},
|
||||||
|
{BF_SERIAL_NUMBER, 0x00, 0x11, 4, 0, 0, B_SN},
|
||||||
|
{BF_ARM_VERSION, 0x00, 0x17, 2, 0, 0, B_VERSION},
|
||||||
|
{BF_DSP_VERSION, 0x00, 0x19, 2, 0, 0, B_VERSION},
|
||||||
|
{BF_DC_INPUT_POWER, 0x00, 0x24, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_INPUT_POWER, 0x00, 0x25, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_OUTPUT_POWER, 0x00, 0x26, 1, 0, 0, B_UINT},
|
||||||
|
{BF_DC_OUTPUT_POWER, 0x00, 0x27, 1, 0, 0, B_UINT},
|
||||||
|
{BF_POWER_GENERATION, 0x00, 0x29, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_TOTAL_BATTERY_PERCENT, 0x00, 0x2B, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_OUTPUT_ON, 0x00, 0x30, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_DC_OUTPUT_ON, 0x00, 0x31, 1, 0, 0, B_BOOL},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const bluetti_poll_t AC500_poll[] = {
|
||||||
|
{0x00, 0x0A, 0x28},
|
||||||
|
{0x00, 0x46, 0x15},
|
||||||
|
{0x0B, 0xB9, 0x3D},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const bluetti_field_t AC500_command[] = {
|
||||||
|
{BF_AC_OUTPUT_ON, 0x0B, 0xBF, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_DC_OUTPUT_ON, 0x0B, 0xC0, 1, 0, 0, B_BOOL},
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BLUETTI_DEVICE_AC500_H
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Bluetti EB3A register map.
|
||||||
|
* Ported from Bluetti_ESP32_Bridge/DEVICE_EB3A.h (tested model).
|
||||||
|
*/
|
||||||
|
#ifndef BLUETTI_DEVICE_EB3A_H
|
||||||
|
#define BLUETTI_DEVICE_EB3A_H
|
||||||
|
|
||||||
|
#include "BluettiFields.h"
|
||||||
|
|
||||||
|
static const bluetti_field_t EB3A_state[] = {
|
||||||
|
{BF_DEVICE_TYPE, 0x00, 0x0A, 7, 0, 0, B_STRING},
|
||||||
|
{BF_SERIAL_NUMBER, 0x00, 0x11, 4, 0, 0, B_SN},
|
||||||
|
{BF_ARM_VERSION, 0x00, 0x17, 2, 0, 0, B_VERSION},
|
||||||
|
{BF_DSP_VERSION, 0x00, 0x19, 2, 0, 0, B_VERSION},
|
||||||
|
{BF_DC_INPUT_POWER, 0x00, 0x24, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_INPUT_POWER, 0x00, 0x25, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_OUTPUT_POWER, 0x00, 0x26, 1, 0, 0, B_UINT},
|
||||||
|
{BF_DC_OUTPUT_POWER, 0x00, 0x27, 1, 0, 0, B_UINT},
|
||||||
|
{BF_POWER_GENERATION, 0x00, 0x29, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_TOTAL_BATTERY_PERCENT, 0x00, 0x2B, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_OUTPUT_ON, 0x00, 0x30, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_DC_OUTPUT_ON, 0x00, 0x31, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_AC_INPUT_VOLTAGE, 0x00, 0x4D, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_DC_INPUT_VOLTAGE, 0x00, 0x56, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_PACK_NUM_MAX, 0x00, 0x5B, 1, 0, 0, B_UINT},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const bluetti_poll_t EB3A_poll[] = {
|
||||||
|
{0x00, 0x0A, 0x28},
|
||||||
|
{0x00, 0x46, 0x15},
|
||||||
|
{0x0B, 0xDA, 0x01},
|
||||||
|
{0x0B, 0xF4, 0x07},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const bluetti_field_t EB3A_command[] = {
|
||||||
|
{BF_AC_OUTPUT_ON, 0x0B, 0xBF, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_DC_OUTPUT_ON, 0x0B, 0xC0, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_POWER_OFF, 0x0B, 0xF4, 1, 0, 0, B_BOOL},
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BLUETTI_DEVICE_EB3A_H
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Bluetti EP500 register map.
|
||||||
|
* Ported from Bluetti_ESP32_Bridge/Device_EP500.h (untested in the reference).
|
||||||
|
*/
|
||||||
|
#ifndef BLUETTI_DEVICE_EP500_H
|
||||||
|
#define BLUETTI_DEVICE_EP500_H
|
||||||
|
|
||||||
|
#include "BluettiFields.h"
|
||||||
|
|
||||||
|
static const bluetti_field_t EP500_state[] = {
|
||||||
|
{BF_DEVICE_TYPE, 0x00, 0x0A, 7, 0, 0, B_STRING},
|
||||||
|
{BF_SERIAL_NUMBER, 0x00, 0x11, 4, 0, 0, B_SN},
|
||||||
|
{BF_ARM_VERSION, 0x00, 0x17, 2, 0, 0, B_VERSION},
|
||||||
|
{BF_DSP_VERSION, 0x00, 0x19, 2, 0, 0, B_VERSION},
|
||||||
|
{BF_DC_INPUT_POWER, 0x00, 0x24, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_INPUT_POWER, 0x00, 0x25, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_OUTPUT_POWER, 0x00, 0x26, 1, 0, 0, B_UINT},
|
||||||
|
{BF_DC_OUTPUT_POWER, 0x00, 0x27, 1, 0, 0, B_UINT},
|
||||||
|
{BF_POWER_GENERATION, 0x00, 0x29, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_TOTAL_BATTERY_PERCENT, 0x00, 0x2B, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_OUTPUT_ON, 0x00, 0x30, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_DC_OUTPUT_ON, 0x00, 0x31, 1, 0, 0, B_BOOL},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const bluetti_poll_t EP500_poll[] = {
|
||||||
|
{0x00, 0x0A, 0x28},
|
||||||
|
{0x00, 0x46, 0x15},
|
||||||
|
{0x0B, 0xB9, 0x3D},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const bluetti_field_t EP500_command[] = {
|
||||||
|
{BF_AC_OUTPUT_ON, 0x0B, 0xBF, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_DC_OUTPUT_ON, 0x0B, 0xC0, 1, 0, 0, B_BOOL},
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BLUETTI_DEVICE_EP500_H
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Bluetti EP500P register map.
|
||||||
|
* Ported from Bluetti_ESP32_Bridge/DEVICE_EP500P.h (tested model).
|
||||||
|
*/
|
||||||
|
#ifndef BLUETTI_DEVICE_EP500P_H
|
||||||
|
#define BLUETTI_DEVICE_EP500P_H
|
||||||
|
|
||||||
|
#include "BluettiFields.h"
|
||||||
|
|
||||||
|
static const bluetti_field_t EP500P_state[] = {
|
||||||
|
{BF_DEVICE_TYPE, 0x00, 0x0A, 7, 0, 0, B_STRING},
|
||||||
|
{BF_SERIAL_NUMBER, 0x00, 0x11, 4, 0, 0, B_SN},
|
||||||
|
{BF_ARM_VERSION, 0x00, 0x17, 2, 0, 0, B_VERSION},
|
||||||
|
{BF_DSP_VERSION, 0x00, 0x19, 2, 0, 0, B_VERSION},
|
||||||
|
{BF_DC_INPUT_POWER, 0x00, 0x24, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_INPUT_POWER, 0x00, 0x25, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_OUTPUT_POWER, 0x00, 0x26, 1, 0, 0, B_UINT},
|
||||||
|
{BF_DC_OUTPUT_POWER, 0x00, 0x27, 1, 0, 0, B_UINT},
|
||||||
|
{BF_POWER_GENERATION, 0x00, 0x29, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_TOTAL_BATTERY_PERCENT, 0x00, 0x2B, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_OUTPUT_ON, 0x00, 0x30, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_DC_OUTPUT_ON, 0x00, 0x31, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_AC_OUTPUT_MODE, 0x00, 0x46, 1, 0, 0, B_UINT},
|
||||||
|
{BF_INTERNAL_AC_VOLTAGE, 0x00, 0x47, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_CURRENT_ONE, 0x00, 0x48, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_POWER_ONE, 0x00, 0x49, 1, 0, 0, B_UINT},
|
||||||
|
{BF_INTERNAL_AC_FREQUENCY, 0x00, 0x4A, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_CURRENT_TWO, 0x00, 0x4B, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_POWER_TWO, 0x00, 0x4C, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_INPUT_VOLTAGE, 0x00, 0x4D, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_CURRENT_THREE, 0x00, 0x4E, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_POWER_THREE, 0x00, 0x4F, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_INPUT_FREQUENCY, 0x00, 0x50, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_DC_INPUT_VOLTAGE, 0x00, 0x56, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_INTERNAL_DC_INPUT_POWER, 0x00, 0x57, 1, 0, 0, B_UINT},
|
||||||
|
{BF_INTERNAL_DC_INPUT_CURRENT, 0x00, 0x58, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_PACK_NUM_MAX, 0x00, 0x5B, 1, 0, 0, B_UINT},
|
||||||
|
{BF_INTERNAL_PACK_VOLTAGE, 0x00, 0x5C, 1, 1, 0, B_DECIMAL},
|
||||||
|
{BF_PACK_BATTERY_PERCENT, 0x00, 0x5E, 1, 0, 0, B_UINT},
|
||||||
|
{BF_PACK_NUM, 0x00, 0x60, 1, 0, 0, B_UINT},
|
||||||
|
{BF_CELL01_VOLTAGE, 0x00, 0x69, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL02_VOLTAGE, 0x00, 0x6A, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL03_VOLTAGE, 0x00, 0x6B, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL04_VOLTAGE, 0x00, 0x6C, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL05_VOLTAGE, 0x00, 0x6D, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL06_VOLTAGE, 0x00, 0x6E, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL07_VOLTAGE, 0x00, 0x6F, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL08_VOLTAGE, 0x00, 0x70, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL09_VOLTAGE, 0x00, 0x71, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL10_VOLTAGE, 0x00, 0x72, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL11_VOLTAGE, 0x00, 0x73, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL12_VOLTAGE, 0x00, 0x74, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL13_VOLTAGE, 0x00, 0x75, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL14_VOLTAGE, 0x00, 0x76, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL15_VOLTAGE, 0x00, 0x77, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_CELL16_VOLTAGE, 0x00, 0x78, 1, 2, 0, B_DECIMAL},
|
||||||
|
{BF_UPS_MODE, 0x0B, 0xB9, 1, 0, 0, B_UINT},
|
||||||
|
{BF_GRID_CHARGE_ON, 0x0B, 0xC3, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_AUTO_SLEEP_MODE, 0x0B, 0xF5, 1, 0, 0, B_UINT},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const bluetti_poll_t EP500P_poll[] = {
|
||||||
|
{0x00, 0x0A, 0x28},
|
||||||
|
{0x00, 0x46, 0x15},
|
||||||
|
{0x0B, 0xDA, 0x01},
|
||||||
|
{0x0B, 0xF5, 0x07},
|
||||||
|
{0x00, 0x5B, 0x25},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const bluetti_field_t EP500P_command[] = {
|
||||||
|
{BF_AC_OUTPUT_ON, 0x0B, 0xBF, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_DC_OUTPUT_ON, 0x0B, 0xC0, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_GRID_CHARGE_ON, 0x0B, 0xC3, 1, 0, 0, B_BOOL},
|
||||||
|
{BF_UPS_MODE, 0x0B, 0xB9, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AUTO_SLEEP_MODE,0x0B, 0xF5, 1, 0, 0, B_UINT},
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // BLUETTI_DEVICE_EP500P_H
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Bluetti EP600 register map (different page/offset scheme to the older models).
|
||||||
|
* Ported from Bluetti_ESP32_Bridge/Device_EP600.h (partial support).
|
||||||
|
* Refs: doc.chromedshark.com/bluetti/ep600.html, github.com/warhammerkid/bluetti_mqtt
|
||||||
|
*/
|
||||||
|
#ifndef BLUETTI_DEVICE_EP600_H
|
||||||
|
#define BLUETTI_DEVICE_EP600_H
|
||||||
|
|
||||||
|
#include "BluettiFields.h"
|
||||||
|
|
||||||
|
static const bluetti_field_t EP600_state[] = {
|
||||||
|
{BF_TOTAL_BATTERY_PERCENT, 0x00, 0x66, 1, 0, 0, B_UINT},
|
||||||
|
{BF_DEVICE_TYPE, 0x00, 0x6E, 6, 0, 0, B_STRING},
|
||||||
|
{BF_SERIAL_NUMBER, 0x00, 0x74, 4, 0, 0, B_SN},
|
||||||
|
{BF_POWER_GENERATION, 0x00, 0x90, 1, 3, 0, B_DECIMAL},
|
||||||
|
{BF_BATTERY_MIN_PERCENTAGE, 0x07, 0xE6, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_CHARGE_MAX_PERCENTAGE,0x07, 0xE7, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_INPUT_POWER_MAX, 0x08, 0xA5, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_INPUT_CURRENT_MAX, 0x08, 0xA6, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_OUTPUT_POWER_MAX, 0x08, 0xA7, 1, 0, 0, B_UINT},
|
||||||
|
{BF_AC_OUTPUT_CURRENT_MAX, 0x08, 0xA8, 1, 0, 0, B_UINT},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const bluetti_poll_t EP600_poll[] = {
|
||||||
|
{0x00, 0x64, 0x3E},
|
||||||
|
{0x07, 0xD0, 0x30},
|
||||||
|
{0x08, 0x00, 0x29},
|
||||||
|
};
|
||||||
|
|
||||||
|
// No verified writable controls for the EP600 in the reference project; the
|
||||||
|
// model registry registers a null command table with a count of zero.
|
||||||
|
|
||||||
|
#endif // BLUETTI_DEVICE_EP600_H
|
||||||
Reference in New Issue
Block a user