intial test version

This commit is contained in:
2026-06-04 21:35:07 +10:00
commit 4ea1e12ee9
24 changed files with 1877 additions and 0 deletions
+52
View File
@@ -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
+21
View File
@@ -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.
+92
View File
@@ -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.
+199
View File
@@ -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).
+19
View File
@@ -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
+76
View File
@@ -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();
}
+19
View File
@@ -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
+54
View File
@@ -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();
}
}
+19
View File
@@ -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
+69
View File
@@ -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();
}
+41
View File
@@ -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
+52
View File
@@ -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"
]
}
}
+11
View File
@@ -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
+436
View File
@@ -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); }
+214
View File
@@ -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
+43
View File
@@ -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
+115
View File
@@ -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
+60
View File
@@ -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
+61
View File
@@ -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
+36
View File
@@ -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
+41
View File
@@ -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
+36
View File
@@ -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
+78
View File
@@ -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
+33
View File
@@ -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