initial version
This commit is contained in:
commit
5620f73fa1
18
.atom-build.yml
Normal file
18
.atom-build.yml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
cmd: bin/build
|
||||||
|
name: "All"
|
||||||
|
errorMatch:
|
||||||
|
- (?<file>[\/0-9a-zA-Z\._]+):(?<line>\d+):(?<col>\d+):\s+(?<message>.+)
|
||||||
|
targets:
|
||||||
|
Build:
|
||||||
|
cmd: bin/build
|
||||||
|
name: "Build"
|
||||||
|
errorMatch:
|
||||||
|
- (?<file>[\/0-9a-zA-Z\._]+):(?<line>\d+):(?<col>\d+):\s+(?<message>.+)
|
||||||
|
Erase:
|
||||||
|
cmd: esptool.py erase_flash
|
||||||
|
name: "Erase"
|
||||||
|
errorMatch:
|
||||||
|
- (?<file>[\/0-9a-zA-Z\._]+):(?<line>\d+):(?<col>\d+):\s+(?<message>.+)
|
||||||
|
Upload:
|
||||||
|
cmd: bin/flash Compiled/latest
|
||||||
|
name: "Upload"
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/Compiled/*
|
||||||
|
/build/
|
||||||
|
/compile_flags.h
|
414
ESPBMS.ino
Normal file
414
ESPBMS.ino
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
#include "BLEDevice.h"
|
||||||
|
|
||||||
|
static BLEUUID serviceUUID("0000ff00-0000-1000-8000-00805f9b34fb"); //xiaoxiang bms service
|
||||||
|
static BLEUUID charUUID_rx("0000ff01-0000-1000-8000-00805f9b34fb"); //xiaoxiang bms rx id
|
||||||
|
static BLEUUID charUUID_tx("0000ff02-0000-1000-8000-00805f9b34fb"); //xiaoxiang bms tx id
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
byte start;
|
||||||
|
byte type;
|
||||||
|
byte status;
|
||||||
|
byte dataLen;
|
||||||
|
} bmsPacketHeaderStruct;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
BLEDevice::init(""); // Initialize BLE device
|
||||||
|
}
|
||||||
|
|
||||||
|
char currentName[128];
|
||||||
|
bool gotBasicInfo;
|
||||||
|
bool gotCellInfo;
|
||||||
|
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
Serial.printf("\r\n\r\n===============================\r\n\r\n");
|
||||||
|
|
||||||
|
Serial.println("Scanning...");
|
||||||
|
|
||||||
|
BLEScan* pBLEScan = BLEDevice::getScan(); // Create new scan
|
||||||
|
pBLEScan->setActiveScan(true); // Active scan uses more power, but get results faster
|
||||||
|
BLEScanResults foundDevices = pBLEScan->start(5); //seconds
|
||||||
|
|
||||||
|
Serial.println("Devices found: " + String(foundDevices.getCount()));
|
||||||
|
|
||||||
|
for (int i = 0; i < foundDevices.getCount(); i++) {
|
||||||
|
delay(1000);
|
||||||
|
Serial.printf("\r\n\r\n===============================\r\n\r\n");
|
||||||
|
|
||||||
|
BLEAdvertisedDevice advertisedDevice = foundDevices.getDevice(i);
|
||||||
|
Serial.println("\nFound Device: " + String(advertisedDevice.toString().c_str()));
|
||||||
|
|
||||||
|
std::string targetAddress = "d0:65:de:e5:89:76";
|
||||||
|
if (advertisedDevice.getAddress().toString() == targetAddress) {
|
||||||
|
Serial.println("Victron device found!");
|
||||||
|
decodeVictron(advertisedDevice);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!advertisedDevice.isAdvertisingService(serviceUUID)) {
|
||||||
|
Serial.println("Device does not advertise the specified service UUID.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
BLEClient* pClient = BLEDevice::createClient();
|
||||||
|
Serial.println("Connecting to: " + String(advertisedDevice.getName().c_str()));
|
||||||
|
|
||||||
|
int retryCount = 0;
|
||||||
|
const int maxRetries = 5; // Maximum number of retries
|
||||||
|
while(retryCount < maxRetries) {
|
||||||
|
if(pClient->connect(&advertisedDevice)) {
|
||||||
|
Serial.println("Connected successfully.");
|
||||||
|
break; // Exit the loop if the connection is successful
|
||||||
|
} else {
|
||||||
|
Serial.println("Failed to connect. Retrying...");
|
||||||
|
retryCount++; // Increment the retry counter
|
||||||
|
delay(1000); // Optional: Wait for a second before retrying
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(retryCount == maxRetries) {
|
||||||
|
Serial.println("Failed to connect after retries.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get remote service
|
||||||
|
Serial.println("Get remote service...");
|
||||||
|
BLERemoteService* pRemoteService = pClient->getService(serviceUUID);
|
||||||
|
if (pRemoteService == nullptr){
|
||||||
|
Serial.println(String("BLE: failed to find service UUID"));
|
||||||
|
Serial.print("Failed to find our service UUID: ");
|
||||||
|
Serial.println(serviceUUID.toString().c_str());
|
||||||
|
pClient->disconnect();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get BMS receive characteristic
|
||||||
|
Serial.println("Get BMS receive characteristic...");
|
||||||
|
BLERemoteCharacteristic* pRemoteCharacteristic_rx = pRemoteService->getCharacteristic(charUUID_rx);
|
||||||
|
if (pRemoteCharacteristic_rx == nullptr){
|
||||||
|
Serial.println(String("BLE: failed to find RX UUID"));
|
||||||
|
Serial.print("Failed to find rx UUID: ");
|
||||||
|
Serial.println(charUUID_rx.toString().c_str());
|
||||||
|
pClient->disconnect();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register callback for remote characteristic (receive channel)
|
||||||
|
Serial.println("Register callback for remote characteristic...");
|
||||||
|
if (pRemoteCharacteristic_rx->canNotify()){
|
||||||
|
pRemoteCharacteristic_rx->registerForNotify(MyNotifyCallback);
|
||||||
|
}else{
|
||||||
|
Serial.println(String("BLE: failed to register notification of remote characteristic"));
|
||||||
|
Serial.println("Failed to register notification of remote characteristic");
|
||||||
|
pClient->disconnect();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get BMS transmit characteristic
|
||||||
|
Serial.println("Get BMS transmit characteristic...");
|
||||||
|
BLERemoteCharacteristic* pRemoteCharacteristic_tx = pRemoteService->getCharacteristic(charUUID_tx);
|
||||||
|
if (pRemoteCharacteristic_tx == nullptr){
|
||||||
|
Serial.println(String("BLE: failed to find TX UUID"));
|
||||||
|
Serial.print("Failed to find tx UUID: ");
|
||||||
|
Serial.println(charUUID_tx.toString().c_str());
|
||||||
|
pClient->disconnect();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether tx is writeable
|
||||||
|
Serial.println("Check whether tx is writeable...");
|
||||||
|
if (!(pRemoteCharacteristic_tx->canWriteNoResponse())){
|
||||||
|
Serial.println(String("BLE: failed TX remote characteristic is not writable"));
|
||||||
|
Serial.println("Failed TX remote characteristic is not writable");
|
||||||
|
pClient->disconnect();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("Get data...");
|
||||||
|
|
||||||
|
gotBasicInfo=false;
|
||||||
|
gotCellInfo=false;
|
||||||
|
|
||||||
|
std::__cxx11::string deviceName = advertisedDevice.getName();
|
||||||
|
strncpy(currentName, deviceName.c_str(), sizeof(currentName) - 1);
|
||||||
|
currentName[sizeof(currentName) - 1] = '\0'; // Ensure null-termination
|
||||||
|
// Convert to lowercase and replace spaces with dots
|
||||||
|
for (char* p = currentName; *p; ++p) {
|
||||||
|
*p = *p == ' ' ? '.' : *p;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned long start=millis();
|
||||||
|
while( millis()-start<10000 && (gotBasicInfo==false || gotCellInfo==false) ){
|
||||||
|
// REQUEST BASIC INFO
|
||||||
|
if(gotBasicInfo==false){
|
||||||
|
Serial.println("Request Basic Info");
|
||||||
|
delay(500);
|
||||||
|
// header status command length data checksum footer
|
||||||
|
// DD A5 03 00 FF FD 77
|
||||||
|
uint8_t a_data[7] = {0xdd, 0xa5, 3, 0x0, 0xff, 0xfd, 0x77};
|
||||||
|
pRemoteCharacteristic_tx->writeValue(a_data, sizeof(a_data), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// REQUEST CELL INFO
|
||||||
|
if(gotCellInfo==false){
|
||||||
|
Serial.println("Request Cell Info");
|
||||||
|
delay(500);
|
||||||
|
// header status command length data checksum footer
|
||||||
|
// DD A5 03 00 FF FD 77
|
||||||
|
uint8_t b_data[7] = {0xdd, 0xa5, 4, 0x0, 0xff, 0xfc, 0x77};
|
||||||
|
pRemoteCharacteristic_tx->writeValue(b_data, sizeof(b_data), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pClient->disconnect();
|
||||||
|
}
|
||||||
|
Serial.println("Reboot!");
|
||||||
|
delay(100);
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void MyNotifyCallback(BLERemoteCharacteristic *pBLERemoteCharacteristic, uint8_t *pData, size_t length, bool isNotify){
|
||||||
|
//hexDump((char*)pData, length);
|
||||||
|
if(!bleCollectPacket((char *)pData, length)){
|
||||||
|
Serial.println("ERROR: packet could not be collected.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void hexDump(const char *data, uint32_t dataSize)
|
||||||
|
{
|
||||||
|
Serial.println("HEX data:");
|
||||||
|
|
||||||
|
for (int i = 0; i < dataSize; i++)
|
||||||
|
{
|
||||||
|
Serial.printf("0x%x, ", data[i]);
|
||||||
|
}
|
||||||
|
Serial.println("");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bleCollectPacket(char *data, uint32_t dataSize) // reconstruct packet, called by notifyCallback function
|
||||||
|
{
|
||||||
|
static uint8_t packetstate = 0; //0 - empty, 1 - first half of packet received, 2- second half of packet received
|
||||||
|
|
||||||
|
// packet sizes:
|
||||||
|
// (packet ID 03) = 4 (header) + 23 + 2*N_NTCs + 2 (checksum) + 1 (stop)
|
||||||
|
// (packet ID 04) = 4 (header) + 2*NUM_CELLS + 2 (checksum) + 1 (stop)
|
||||||
|
static uint8_t packetbuff[4 + 2*25 + 2 + 1] = {0x0}; // buffer size suitable for up to 25 cells
|
||||||
|
|
||||||
|
static uint32_t totalDataSize = 0;
|
||||||
|
bool retVal = false;
|
||||||
|
//hexDump(data,dataSize);
|
||||||
|
|
||||||
|
if(totalDataSize + dataSize > sizeof(packetbuff)){
|
||||||
|
Serial.printf("ERROR: datasize is overlength.");
|
||||||
|
|
||||||
|
Serial.println(
|
||||||
|
String("ERROR: datasize is overlength. ") +
|
||||||
|
String("allocated=") +
|
||||||
|
String(sizeof(packetbuff)) +
|
||||||
|
String(", size=") +
|
||||||
|
String(totalDataSize + dataSize)
|
||||||
|
);
|
||||||
|
|
||||||
|
totalDataSize = 0;
|
||||||
|
packetstate = 0;
|
||||||
|
|
||||||
|
retVal = false;
|
||||||
|
}
|
||||||
|
else if (data[0] == 0xdd && packetstate == 0) // probably got 1st half of packet
|
||||||
|
{
|
||||||
|
packetstate = 1;
|
||||||
|
for (uint8_t i = 0; i < dataSize; i++)
|
||||||
|
{
|
||||||
|
packetbuff[i] = data[i];
|
||||||
|
}
|
||||||
|
totalDataSize = dataSize;
|
||||||
|
retVal = true;
|
||||||
|
|
||||||
|
if (data[dataSize - 1] == 0x77) {
|
||||||
|
//its full packets
|
||||||
|
packetstate = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (data[dataSize - 1] == 0x77 && packetstate == 1) //probably got 2nd half of the packet
|
||||||
|
{
|
||||||
|
packetstate = 2;
|
||||||
|
for (uint8_t i = 0; i < dataSize; i++)
|
||||||
|
{
|
||||||
|
packetbuff[i + totalDataSize] = data[i];
|
||||||
|
}
|
||||||
|
totalDataSize += dataSize;
|
||||||
|
retVal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packetstate == 2) //got full packet
|
||||||
|
{
|
||||||
|
uint8_t packet[totalDataSize];
|
||||||
|
memcpy(packet, packetbuff, totalDataSize);
|
||||||
|
|
||||||
|
bmsProcessPacket(packet); //pass pointer to retrieved packet to processing function
|
||||||
|
packetstate = 0;
|
||||||
|
totalDataSize = 0;
|
||||||
|
retVal = true;
|
||||||
|
}
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bmsProcessPacket(byte *packet)
|
||||||
|
{
|
||||||
|
|
||||||
|
bool isValid = isPacketValid(packet);
|
||||||
|
|
||||||
|
if (isValid != true)
|
||||||
|
{
|
||||||
|
Serial.println("Invalid packer received");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bmsPacketHeaderStruct *pHeader = (bmsPacketHeaderStruct *)packet;
|
||||||
|
byte *data = packet + sizeof(bmsPacketHeaderStruct); // TODO Fix this ugly hack
|
||||||
|
unsigned int dataLen = pHeader->dataLen;
|
||||||
|
|
||||||
|
bool result = false;
|
||||||
|
|
||||||
|
// find packet type (basic info or cell info)
|
||||||
|
switch (pHeader->type)
|
||||||
|
{
|
||||||
|
case 3:
|
||||||
|
{
|
||||||
|
// Process basic info
|
||||||
|
result = processBasicInfo(data, dataLen);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
{
|
||||||
|
// Process cell info
|
||||||
|
result = processCellInfo(data, dataLen);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
result = false;
|
||||||
|
Serial.printf("Unsupported packet type detected. Type: %d", pHeader->type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool processBasicInfo(byte *data, unsigned int dataLen){
|
||||||
|
//// NICER!!!: https://github.com/neilsheps/overkill-xiaoxiang-jbd-bms-ble-reader/blob/main/src/main.cpp
|
||||||
|
|
||||||
|
uint16_t Volts = ((uint32_t)two_ints_into16(data[0], data[1])) * 10; // Resolution 10 mV -> convert to milivolts eg 4895 > 48950mV
|
||||||
|
int32_t Amps = ((int32_t)two_ints_into16(data[2], data[3])) * 10; // Resolution 10 mA -> convert to miliamps
|
||||||
|
|
||||||
|
int32_t Watts = Volts * Amps / 1000000; // W
|
||||||
|
|
||||||
|
//Serial.printf("Remaining Capacity: %4.2fAhr\n", ((float)(data[4] * 256 + data[5]))/100);
|
||||||
|
//Serial.printf("Nominal Capacity: %4.2fAhr\n", ((float)(data[6] * 256 + data[7]))/100);
|
||||||
|
|
||||||
|
uint32_t CapacityRemainAh = ((uint16_t)two_ints_into16(data[4], data[5])) * 10;
|
||||||
|
uint8_t CapacityRemainPercent = ((uint8_t)data[19]);
|
||||||
|
|
||||||
|
uint16_t Temp1 = (((uint16_t)two_ints_into16(data[23], data[24])) - 2731);
|
||||||
|
uint16_t Temp2 = (((uint16_t)two_ints_into16(data[25], data[26])) - 2731);
|
||||||
|
|
||||||
|
uint16_t BalanceCodeLow = (two_ints_into16(data[12], data[13]));
|
||||||
|
uint16_t BalanceCodeHigh = (two_ints_into16(data[14], data[15]));
|
||||||
|
uint8_t MosfetStatus = ((byte)data[20]);
|
||||||
|
|
||||||
|
Serial.printf(">>>RC.%s.Voltage %f\r\n",currentName, (float)Volts / 1000);
|
||||||
|
Serial.printf(">>>RC.%s.Amps %f\r\n",currentName, (float)Amps / 1000);
|
||||||
|
Serial.printf(">>>RC.%s.Watts %f\r\n",currentName, (float)Watts);
|
||||||
|
Serial.printf(">>>RC.%s.Capacity_Remain_Ah %f\r\n",currentName, (float)CapacityRemainAh / 1000);
|
||||||
|
Serial.printf(">>>RC.%s.Capacity_Remain_Wh %f\r\n",currentName, ((float)(CapacityRemainAh) / 1000) * ((float)(Volts) / 1000));
|
||||||
|
Serial.printf(">>>RC.%s.Capacity_Remain_Percent %d\r\n",currentName, CapacityRemainPercent);
|
||||||
|
Serial.printf(">>>RC.%s.Temp1 %f\r\n",currentName, (float)Temp1 / 10);
|
||||||
|
Serial.printf(">>>RC.%s.Temp2 %f\r\n",currentName, (float)Temp2 / 10);
|
||||||
|
/*
|
||||||
|
Serial.printf("%s Balance Code Low: 0x%x\r\n",currentName, BalanceCodeLow);
|
||||||
|
Serial.printf("%s Balance Code High: 0x%x\r\n",currentName, BalanceCodeHigh);
|
||||||
|
Serial.printf("%s Mosfet Status: 0x%x\r\n",currentName, MosfetStatus);
|
||||||
|
*/
|
||||||
|
gotBasicInfo=true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool processCellInfo(byte *data, unsigned int dataLen)
|
||||||
|
{
|
||||||
|
uint16_t _cellSum;
|
||||||
|
uint16_t _cellMin = 5000;
|
||||||
|
uint16_t _cellMax = 0;
|
||||||
|
uint16_t _cellAvg;
|
||||||
|
uint16_t _cellDiff;
|
||||||
|
|
||||||
|
uint8_t NumOfCells = dataLen / 2; // data contains 2 bytes per cell
|
||||||
|
|
||||||
|
//go trough individual cells
|
||||||
|
for (byte i = 0; i < dataLen / 2; i++){
|
||||||
|
int CellVolt = ((uint16_t)two_ints_into16(data[i * 2], data[i * 2 + 1])); // Resolution 1 mV
|
||||||
|
_cellSum += CellVolt;
|
||||||
|
if (CellVolt > _cellMax)
|
||||||
|
{
|
||||||
|
_cellMax = CellVolt;
|
||||||
|
}
|
||||||
|
if (CellVolt < _cellMin)
|
||||||
|
{
|
||||||
|
_cellMin = CellVolt;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf(">>>RC.%s.Cell.%d.Voltage %f\r\n",currentName, i+1,(float)CellVolt/1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf(">>>RC.%s.Max_Cell_Voltage %f\r\n",currentName, (float)_cellMax / 1000);
|
||||||
|
Serial.printf(">>>RC.%s.Min_Cell_Voltage %f\r\n",currentName, (float)_cellMin / 1000);
|
||||||
|
Serial.printf(">>>RC.%s.Difference_Cell_Voltage %f\r\n",currentName, (float)(_cellMax - _cellMin) / 1000);
|
||||||
|
Serial.printf(">>>RC.%s.Average_Cell_Voltage %f\r\n",currentName, (float)(_cellSum / NumOfCells) / 1000);
|
||||||
|
|
||||||
|
gotCellInfo=true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPacketValid(byte *packet) //check if packet is valid
|
||||||
|
{
|
||||||
|
if (packet == nullptr){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bmsPacketHeaderStruct *pHeader = (bmsPacketHeaderStruct *)packet;
|
||||||
|
int checksumPos = pHeader->dataLen + 2; // status + data len + data
|
||||||
|
|
||||||
|
int offset = 2; // header 0xDD and command type are not in data length
|
||||||
|
|
||||||
|
if (packet[0] != 0xDD){
|
||||||
|
// start bit missing
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet[offset + checksumPos + 2] != 0x77){
|
||||||
|
// stop bit missing
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte checksum = 0;
|
||||||
|
for (int i = 0; i < checksumPos; i++){
|
||||||
|
checksum += packet[offset + i];
|
||||||
|
}
|
||||||
|
checksum = ((checksum ^ 0xFF) + 1) & 0xFF;
|
||||||
|
|
||||||
|
if (checksum != packet[offset + checksumPos + 1]){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t two_ints_into16(int highbyte, int lowbyte) // turns two bytes into a single long integer
|
||||||
|
{
|
||||||
|
int16_t result = (highbyte);
|
||||||
|
result <<= 8; //Left shift 8 bits,
|
||||||
|
result = (result | lowbyte); //OR operation, merge the two
|
||||||
|
return result;
|
||||||
|
}
|
57
bin/2graphite.py
Normal file
57
bin/2graphite.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import sys
|
||||||
|
import serial_asyncio # Ensure this package is installed
|
||||||
|
|
||||||
|
def parse_to_graphite(data):
|
||||||
|
results = []
|
||||||
|
|
||||||
|
lines = data.strip().split('\n')
|
||||||
|
for line in lines:
|
||||||
|
if line.startswith('>>>'):
|
||||||
|
metric_path = line[3:].strip() # Remove the ">>>" prefix and any leading/trailing whitespace
|
||||||
|
results.append(metric_path)
|
||||||
|
return results
|
||||||
|
|
||||||
|
async def send_to_graphite(data, session, api_url):
|
||||||
|
for message in data:
|
||||||
|
#print(f"Sending data to API: {message}") # Debug message for sending data
|
||||||
|
try:
|
||||||
|
# Sending raw metric path directly as plain text
|
||||||
|
headers = {'Content-Type': 'text/plain'}
|
||||||
|
async with session.post(api_url, data=message, headers=headers) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
print(f"Failed to send data: {response.status}", await response.text())
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error sending data: {e}")
|
||||||
|
|
||||||
|
async def handle_serial(reader, api_url):
|
||||||
|
session = aiohttp.ClientSession()
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
line = await reader.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
line = line.decode('utf-8')
|
||||||
|
print(line.strip()) # echo output for received line
|
||||||
|
graphite_data = parse_to_graphite(line)
|
||||||
|
if graphite_data:
|
||||||
|
await send_to_graphite(graphite_data, session, api_url)
|
||||||
|
finally:
|
||||||
|
await session.close()
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
print("Usage: python script.py <serial_device> <api_url>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
serial_device = sys.argv[1]
|
||||||
|
api_url = sys.argv[2]
|
||||||
|
baud_rate = 115200 # You can modify this as needed
|
||||||
|
|
||||||
|
# Creating the connection to the serial port
|
||||||
|
reader, _ = await serial_asyncio.open_serial_connection(url=serial_device, baudrate=baud_rate)
|
||||||
|
await handle_serial(reader, api_url)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
BIN
bin/boot_app0.bin
Normal file
BIN
bin/boot_app0.bin
Normal file
Binary file not shown.
BIN
bin/bootloader_qio_80m.bin
Normal file
BIN
bin/bootloader_qio_80m.bin
Normal file
Binary file not shown.
17
bin/build
Executable file
17
bin/build
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
if [ ! -d "Compiled" ] ; then mkdir Compiled ; fi
|
||||||
|
|
||||||
|
if [ "${DRONE_TAG}" ] ; then
|
||||||
|
NAME=${DRONE_TAG}
|
||||||
|
elif [ "${DRONE_BRANCH}" ] ; then
|
||||||
|
NAME=${DRONE_BRANCH}-${DRONE_BUILD_NUMBER}
|
||||||
|
else
|
||||||
|
NAME=`git symbolic-ref --short HEAD`
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "#define VERSION \"${NAME}\"" >> compile_flags.h
|
||||||
|
arduino-cli compile $LIBS -e -b esp32:esp32:esp32 || exit 1
|
||||||
|
mv build/esp32.esp32.esp32/*.ino.bin Compiled/${NAME}.bin
|
||||||
|
mv build/esp32.esp32.esp32/*.ino.elf Compiled/${NAME}.elf
|
||||||
|
mv build/esp32.esp32.esp32/*.ino.partitions.bin Compiled/${NAME}.partitions.bin
|
||||||
|
bin/flash Compiled/${NAME}.bin
|
17
bin/flash
Executable file
17
bin/flash
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
BASENAME=${1%.bin};
|
||||||
|
|
||||||
|
esptool.py \
|
||||||
|
-b 921600 \
|
||||||
|
--chip esp32 \
|
||||||
|
--before default_reset \
|
||||||
|
--after hard_reset \
|
||||||
|
write_flash \
|
||||||
|
-z \
|
||||||
|
--flash_mode dio \
|
||||||
|
--flash_freq 80m \
|
||||||
|
--flash_size detect \
|
||||||
|
0x10000 $BASENAME.bin \
|
||||||
|
0x8000 $BASENAME.partitions.bin \
|
||||||
|
0xe000 bin/boot_app0.bin \
|
||||||
|
0x1000 bin/bootloader_qio_80m.bin
|
196
victron.ino
Normal file
196
victron.ino
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
// credit to: https://github.com/hoberman/Victron_BLE_Advertising_example
|
||||||
|
|
||||||
|
#include <aes/esp_aes.h> // AES library for decrypting the Victron manufacturer data.
|
||||||
|
|
||||||
|
uint8_t key[16]={0xd7,0x93,0xc8,0xb1,0x79,0x18,0x4c,0x97,0x97,0x6d,0x12,0x27,0xf2,0x23,0x48,0x6b};
|
||||||
|
|
||||||
|
int keyBits=128; // Number of bits for AES-CTR decrypt.
|
||||||
|
|
||||||
|
// Victron docs on the manufacturer data in advertisement packets can be found at:
|
||||||
|
// https://community.victronenergy.com/storage/attachments/48745-extra-manufacturer-data-2022-12-14.pdf
|
||||||
|
//
|
||||||
|
|
||||||
|
// Usage/style note: I use uint16_t in places where I need to force 16-bit unsigned integers
|
||||||
|
// instead of whatever the compiler/architecture might decide to use. I might not need to do
|
||||||
|
// the same with byte variables, but I'll do it anyway just to be at least a little consistent.
|
||||||
|
|
||||||
|
// Must use the "packed" attribute to make sure the compiler doesn't add any padding to deal with
|
||||||
|
// word alignment.
|
||||||
|
typedef struct {
|
||||||
|
uint16_t vendorID; // vendor ID
|
||||||
|
uint8_t beaconType; // Should be 0x10 (Product Advertisement) for the packets we want
|
||||||
|
uint8_t unknownData1[3]; // Unknown data
|
||||||
|
uint8_t victronRecordType; // Should be 0x01 (Solar Charger) for the packets we want
|
||||||
|
uint16_t nonceDataCounter; // Nonce
|
||||||
|
uint8_t encryptKeyMatch; // Should match pre-shared encryption key byte 0
|
||||||
|
uint8_t victronEncryptedData[21]; // (31 bytes max per BLE spec - size of previous elements)
|
||||||
|
uint8_t nullPad; // extra byte because toCharArray() adds a \0 byte.
|
||||||
|
} __attribute__((packed)) victronManufacturerData;
|
||||||
|
|
||||||
|
|
||||||
|
// Must use the "packed" attribute to make sure the compiler doesn't add any padding to deal with
|
||||||
|
// word alignment.
|
||||||
|
typedef struct {
|
||||||
|
uint8_t deviceState;
|
||||||
|
uint8_t errorCode;
|
||||||
|
int16_t batteryVoltage;
|
||||||
|
int16_t batteryCurrent;
|
||||||
|
uint16_t todayYield;
|
||||||
|
uint16_t inputPower;
|
||||||
|
uint8_t outputCurrentLo; // Low 8 bits of output current (in 0.1 Amp increments)
|
||||||
|
uint8_t outputCurrentHi; // High 1 bit of output current (must mask off unused bits)
|
||||||
|
uint8_t unused[4]; // Not currently used by Vistron, but it could be in the future.
|
||||||
|
} __attribute__((packed)) victronPanelData;
|
||||||
|
|
||||||
|
// FYI, here are Device State values. I haven't seen ones with '???' so I don't know
|
||||||
|
// if they exist or not or what they might mean:
|
||||||
|
// 0 = no charge from solar
|
||||||
|
// 1 = ???
|
||||||
|
// 2 = ???
|
||||||
|
// 3 = bulk charge
|
||||||
|
// 4 = absorption charge
|
||||||
|
// 5 = float
|
||||||
|
// 6 = ???
|
||||||
|
// 7 = equalization
|
||||||
|
// I've also seen a value '245' for about a second when my solar panel (simulated by a
|
||||||
|
// benchtop power supply) transitions from off/low voltage to on/higher voltage. There
|
||||||
|
// be others, but I haven't seen them.
|
||||||
|
|
||||||
|
void decodeVictron(BLEAdvertisedDevice advertisedDevice) {
|
||||||
|
|
||||||
|
#define manDataSizeMax 31 // BLE specs say no more than 31 bytes, but see comments below!
|
||||||
|
|
||||||
|
// See if we have manufacturer data and then look to see if it's coming from a Victron device.
|
||||||
|
if (advertisedDevice.haveManufacturerData() == true) {
|
||||||
|
|
||||||
|
// Note: This comment (and maybe some code?) needs to be adjusted so it's not so
|
||||||
|
// specific to String-vs-std:string. I'll leave it as-is for now so you at least
|
||||||
|
// understand why I have an extra byte added to the manCharBuf array.
|
||||||
|
//
|
||||||
|
// Here's the thing: BLE specs say our manufacturer data can be a max of 31 bytes.
|
||||||
|
// But: The library code puts this data into a String, which we will then copy to
|
||||||
|
// a character (i.e., byte) buffer using String.toCharArray(). Assuming we have the
|
||||||
|
// full 31 bytes of manufacturer data allowed by the BLE spec, we'll need to size our
|
||||||
|
// buffer with an extra byte for a null terminator. Our toCharArray() call will need
|
||||||
|
// to specify *32* bytes so it will copy 31 bytes of data with a null terminator
|
||||||
|
// at the end.
|
||||||
|
uint8_t manCharBuf[manDataSizeMax+1];
|
||||||
|
|
||||||
|
#ifdef USE_String
|
||||||
|
String manData = advertisedDevice.getManufacturerData(); // lib code returns String.
|
||||||
|
#else
|
||||||
|
std::string manData = advertisedDevice.getManufacturerData(); // lib code returns std::string
|
||||||
|
#endif
|
||||||
|
int manDataSize=manData.length(); // This does not count the null at the end.
|
||||||
|
|
||||||
|
// Copy the data from the String to a byte array. Must have the +1 so we
|
||||||
|
// don't lose the last character to the null terminator.
|
||||||
|
#ifdef USE_String
|
||||||
|
manData.toCharArray((char *)manCharBuf,manDataSize+1);
|
||||||
|
#else
|
||||||
|
manData.copy((char *)manCharBuf, manDataSize+1);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Now let's setup a pointer to a struct to get to the data more cleanly.
|
||||||
|
victronManufacturerData * vicData=(victronManufacturerData *)manCharBuf;
|
||||||
|
|
||||||
|
// ignore this packet if the Vendor ID isn't Victron.
|
||||||
|
if (vicData->vendorID!=0x02e1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore this packet if it isn't type 0x01 (Solar Charger).
|
||||||
|
if (vicData->victronRecordType != 0x01) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not all packets contain a device name, so if we get one we'll save it and use it from now on.
|
||||||
|
if (advertisedDevice.haveName()) {
|
||||||
|
// This works the same whether getName() returns String or std::string.
|
||||||
|
strcpy(currentName,advertisedDevice.getName().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vicData->encryptKeyMatch != key[0]) {
|
||||||
|
Serial.printf("Packet encryption key byte 0x%2.2x doesn't match configured key[0] byte 0x%2.2x\n",
|
||||||
|
vicData->encryptKeyMatch, key[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t inputData[16];
|
||||||
|
uint8_t outputData[16]={0}; // i don't really need to initialize the output.
|
||||||
|
|
||||||
|
// The number of encrypted bytes is given by the number of bytes in the manufacturer
|
||||||
|
// data as a whole minus the number of bytes (10) in the header part of the data.
|
||||||
|
int encrDataSize=manDataSize-10;
|
||||||
|
for (int i=0; i<encrDataSize; i++) {
|
||||||
|
inputData[i]=vicData->victronEncryptedData[i]; // copy for our decrypt below while I figure this out.
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_aes_context ctx;
|
||||||
|
esp_aes_init(&ctx);
|
||||||
|
|
||||||
|
auto status = esp_aes_setkey(&ctx, key, keyBits);
|
||||||
|
if (status != 0) {
|
||||||
|
Serial.printf(" Error during esp_aes_setkey operation (%i).\n",status);
|
||||||
|
esp_aes_free(&ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// construct the 16-byte nonce counter array by piecing it together byte-by-byte.
|
||||||
|
uint8_t data_counter_lsb=(vicData->nonceDataCounter) & 0xff;
|
||||||
|
uint8_t data_counter_msb=((vicData->nonceDataCounter) >> 8) & 0xff;
|
||||||
|
u_int8_t nonce_counter[16] = {data_counter_lsb, data_counter_msb, 0};
|
||||||
|
|
||||||
|
u_int8_t stream_block[16] = {0};
|
||||||
|
|
||||||
|
size_t nonce_offset=0;
|
||||||
|
status = esp_aes_crypt_ctr(&ctx, encrDataSize, &nonce_offset, nonce_counter, stream_block, inputData, outputData);
|
||||||
|
if (status != 0) {
|
||||||
|
Serial.printf("Error during esp_aes_crypt_ctr operation (%i).",status);
|
||||||
|
esp_aes_free(&ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
esp_aes_free(&ctx);
|
||||||
|
|
||||||
|
// Now do our same struct magic so we can get to the data more easily.
|
||||||
|
victronPanelData * victronData = (victronPanelData *) outputData;
|
||||||
|
|
||||||
|
// Getting to these elements is easier using the struct instead of
|
||||||
|
// hacking around with outputData[x] references.
|
||||||
|
uint8_t deviceState=victronData->deviceState;
|
||||||
|
uint8_t errorCode=victronData->errorCode;
|
||||||
|
float batteryVoltage=float(victronData->batteryVoltage)*0.01;
|
||||||
|
float batteryCurrent=float(victronData->batteryCurrent)*0.1;
|
||||||
|
float todayYield=float(victronData->todayYield)*0.01*1000;
|
||||||
|
uint16_t inputPower=victronData->inputPower; // this is in watts; no conversion needed
|
||||||
|
|
||||||
|
// Getting the output current takes some magic because of the way they have the
|
||||||
|
// 9-bit value packed into two bytes. The first byte has the low 8 bits of the count
|
||||||
|
// and the second byte has the upper (most significant) bit of the 9-bit value plus some
|
||||||
|
// There's some other junk in the remaining 7 bits - i'm not sure if it's useful for
|
||||||
|
// anything else but we can't use it here! - so we will mask them off. Then combine the
|
||||||
|
// two bye components to get an integer value in 0.1 Amp increments.
|
||||||
|
int integerOutputCurrent=((victronData->outputCurrentHi & 0x01)<<9) | victronData->outputCurrentLo;
|
||||||
|
float outputCurrent=float(integerOutputCurrent)*0.1;
|
||||||
|
|
||||||
|
// I don't know why, but every so often we'll get half-corrupted data from the Victron. As
|
||||||
|
// far as I can tell it's not a decryption issue because we (usually) get voltage data that
|
||||||
|
// agrees with non-corrupted records.
|
||||||
|
//
|
||||||
|
// Towards the goal of filtering out this noise, I've found that I've rarely (or never) seen
|
||||||
|
// corrupted data when the 'unused' bits of the outputCurrent MSB equal 0xfe. We'll use this
|
||||||
|
// as a litmus test here.
|
||||||
|
uint8_t unusedBits=victronData->outputCurrentHi & 0xfe;
|
||||||
|
if (unusedBits != 0xfe) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf(">>>RC.MPPT.1.Battery_Volts %f\r\n",batteryVoltage);
|
||||||
|
Serial.printf(">>>RC.MPPT.1.Battery_Amps %f\r\n",batteryCurrent);
|
||||||
|
Serial.printf(">>>RC.MPPT.1.Battery_Watts %f\r\n",batteryVoltage*batteryCurrent);
|
||||||
|
Serial.printf(">>>RC.MPPT.1.Solar_Watts %f\r\n",inputPower);
|
||||||
|
Serial.printf(">>>RC.MPPT.1.Output_Current %f\r\n",outputCurrent);
|
||||||
|
Serial.printf(">>>RC.MPPT.1.Yield %f\r\n",todayYield);
|
||||||
|
Serial.printf(">>>RC.MPPT.1.State %d\r\n",deviceState);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user