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
+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