Added sthome canbus

This commit is contained in:
Chris Stuurman 2025-09-03 22:38:18 +02:00
parent 03045de7c2
commit ea865dfa94
13 changed files with 361 additions and 163 deletions

View File

@ -2,9 +2,21 @@
// limit the amount of logging from this class as it can be called very frequently. Too much logging can lead to instability and no connectivity.
namespace solar
{
const cb_frame emptyframe = cb_frame(); // to return a reference to when no valid frame is found
cb_frame::cb_frame()
{
this->msg_id = 0;
this->publish = false;
this->rtr = false;
this->can_id = 0;
this->frame.clear();
// ESP_LOGI(tag("frame CTOR0").c_str(), "%-20s %s", "Created frame", this->to_string().c_str());
}
cb_frame::cb_frame(int msg_id, uint32_t can_id)
{
this->msg_id = msg_id;
this->publish = false;
this->rtr = false;
this->can_id = can_id;
this->frame.clear();
@ -13,6 +25,7 @@ namespace solar
cb_frame::cb_frame(int msg_id, uint32_t can_id, const byte_vector& frame, bool rtr)
{
this->msg_id = msg_id;
this->publish = false;
this->rtr = rtr;
this->can_id = can_id;
this->frame = frame;
@ -21,6 +34,7 @@ namespace solar
void cb_frame::clear()
{
this->msg_id = 0;
this->publish = false;
this->rtr = false;
this->can_id = 0;
this->frame.clear();
@ -28,6 +42,7 @@ namespace solar
void cb_frame::swap(cb_frame &s)
{
std::swap(this->msg_id, s.msg_id);
std::swap(this->publish, s.publish);
std::swap(this->rtr, s.rtr);
std::swap(this->can_id, s.can_id);
this->frame.swap(s.frame);
@ -35,6 +50,34 @@ namespace solar
bool cb_frame::is_valid() const
{
return this->can_id != 0;
}
void cb_frame::setpublish(bool do_publish) const
{
this->publish = do_publish;
}
bool cb_frame::getpublish() const
{
return this->publish;
}
bool cb_frame::send_frame(esphome::canbus::Canbus *canbus, bool extended_id, bool remote_transmission_request) const
{
if(canbus == nullptr) {
ESP_LOGE(tag("cb_frame::send_data").c_str(), "CAN bus interface is null, cannot send data");
return false;
}
if(!this->is_valid()) {
ESP_LOGE(tag("cb_frame::send_data").c_str(), "Frame is not valid, cannot send data: %s", this->to_string().c_str());
return false;
}
return cb_frame::send_frame(canbus, this->can_id, this->frame, extended_id, remote_transmission_request);
}
bool cb_frame::send_frame(esphome::canbus::Canbus *canbus, uint32_t can_id, const byte_vector& frame, bool extended_id, bool remote_transmission_request)
{
if(canbus == nullptr) {
return false;
}
canbus->send_data(can_id, extended_id, remote_transmission_request, frame);
return true;
}
int cb_frame::compare(const cb_frame& b, const int *comparecolumns) const
{
@ -132,13 +175,13 @@ namespace solar
}
return result;
}
std::string cb_frame::tag(const std::string& prefix) const
std::string cb_frame::tag(const std::string& prefix, int prefixlen) const
{
char tag[48];
if(prefix.length() == 0)
snprintf(tag, sizeof(tag), "%04d 0x%03X ", msg_id, can_id);
else
snprintf(tag, sizeof(tag), "%-18s %04d 0x%03X ", prefix.c_str(), msg_id, can_id);
snprintf(tag, sizeof(tag), "%-*s %04d 0x%03X ", prefixlen, prefix.c_str(), msg_id, can_id);
return std::string(tag);
}
std::string cb_frame::to_string() const
@ -147,22 +190,105 @@ namespace solar
// Incorrect placement / misaligned arguments can lead to undefined behavior and processor crashes.
std::string text = "";
char hex[10];
snprintf(hex, sizeof(hex), " %s", rtr ? "RTR" : " ");
std::string line(hex);
if (this->frame.empty()) {
line += " <empty>";
return line;
std::string line;
int n = 0;
if(rtr) {
line = "<remote request> ";
text = " ";
n = 6; // to align text output
}
for (const auto& byte : frame) {
snprintf(hex, sizeof(hex), "%02X ", byte);
line += hex;
if(byte > 31 && byte < 127) {
text += (char) byte;
else {
if (this->frame.empty()) {
return "<empty>";
}
else {
text += ".";
}
for (const auto& byte : frame) {
n++;
snprintf(hex, sizeof(hex), "%02X ", byte);
line += hex;
if(byte > 31 && byte < 127) {
text += (char) byte;
}
else {
text += ".";
}
}
}
for (int i = n; i < 8; i++) {
line += " ";
text += " ";
}
return line + " " + text;
}
// helper function to extract a scaled double value from two bytes
double cb_frame::get_double(uint8_t lsb, uint8_t msb, int inv_scale, bool is_signed)
{
int value = 0;
if ((lsb == 0xFF && msb == 0xFF) || inv_scale == 0) {
return std::numeric_limits<double>::quiet_NaN();
}
if (is_signed) {
value = static_cast<int16_t>((msb << 8) + lsb);
} else {
value = static_cast<uint16_t>((msb << 8) + lsb);
}
//ESP_LOGI("cb_frame::get_double", "0x%02X%02X unsigned %d, scale %d", msb, lsb, value, scale);
return static_cast<double>(value) / inv_scale;
}
// Helper function to convert four scaled values into a byte stream
// set scale to negative for signed values
std::vector<uint8_t> cb_frame::get_byte_stream(double value1, int scale1, double value2, int scale2, double value3, int scale3, double value4, int scale4)
{
//ESP_LOGI("cb_frame::get_byte_stream", "1: %.3f 2: %.3f 3: %.3f 4: %.3f", value1, value2, value3, value4);
std::vector<uint8_t> byte_stream(8, 0);
int value;
if(scale1 != 0) {
value = (scale1 < 0) ? static_cast<int16_t>(value1 * -scale1) : static_cast<uint16_t>(value1 * scale1);
byte_stream[0] = value % 256;
byte_stream[1] = (value >> 8) % 256;
}
if(scale2 != 0) {
value = (scale2 < 0) ? static_cast<int16_t>(value2 * -scale2) : static_cast<uint16_t>(value2 * scale2);
byte_stream[2] = value % 256;
byte_stream[3] = (value >> 8) % 256;
}
if(scale3 != 0) {
value = (scale3 < 0) ? static_cast<int16_t>(value3 * -scale3) : static_cast<uint16_t>(value3 * scale3);
byte_stream[4] = value % 256;
byte_stream[5] = (value >> 8) % 256;
}
if(scale4 != 0) {
value = (scale4 < 0) ? static_cast<int16_t>(value4 * -scale4) : static_cast<uint16_t>(value4 * scale4);
byte_stream[6] = value % 256;
byte_stream[7] = (value >> 8) % 256;
}
return byte_stream;
}
// Helper function to convert four scaled values into a byte stream
// set scale to negative for signed values
// Overloaded function to handle double scale for the fourth value
std::vector<uint8_t> cb_frame::get_byte_stream(double value1, int scale1, double value2, int scale2, double value3, int scale3, double value4, double scale4)
{
std::vector<uint8_t> byte_stream(8, 0);
int value;
if(scale1 != 0) {
value = (scale1 < 0) ? static_cast<int16_t>(value1 * -scale1) : static_cast<uint16_t>(value1 * scale1);
byte_stream[0] = value % 256;
byte_stream[1] = (value >> 8) % 256;
}
if(scale2 != 0) {
value = (scale2 < 0) ? static_cast<int16_t>(value2 * -scale2) : static_cast<uint16_t>(value2 * scale2);
byte_stream[2] = value % 256;
byte_stream[3] = (value >> 8) % 256;
}
if(scale3 != 0) {
value = (scale3 < 0) ? static_cast<int16_t>(value3 * -scale3) : static_cast<uint16_t>(value3 * scale3);
byte_stream[4] = value % 256;
byte_stream[5] = (value >> 8) % 256;
}
value = (scale4 < 0) ? static_cast<int16_t>(round(value4 * -scale4)) : static_cast<uint16_t>(round(value4 * scale4));
byte_stream[6] = value % 256;
byte_stream[7] = (value >> 8) % 256;
return byte_stream;
}
} // namespace solar

View File

@ -9,25 +9,27 @@ namespace solar
{
class cbf_store;
class cb_frame {
private:
mutable bool publish; // whether this frame should be published
public:
enum cbf_store_sortcolumns : int
enum cbf_sortcolumns : int
{
sisortNULL,
sisortcanid,
sisortframe,
sisortrtr,
sisortmsgid,
sisortEND,
sisortcase = SORTCASE, // case sensitive
sisortreverse = SORTREVERSE, // reverse order
sisortwild = WILDNULL, // an empty, or null entry equates to wildcard
sistopcomparecol = STOPCOMPARE // stop compare after this column if both items to be compared are valid and a compare was possible
sisortNULL,
sisortcanid,
sisortrtr,
sisortframe,
sisortmsgid,
sisortEND,
sisortcase = SORTCASE, // case sensitive
sisortreverse = SORTREVERSE, // reverse order
sisortwild = WILDNULL, // an empty, or null entry equates to wildcard
sistopcomparecol = STOPCOMPARE // stop compare after this column if both items to be compared are valid and a compare was possible
};
int msg_id;
bool rtr;
bool rtr;
uint32_t can_id;
byte_vector frame;
cb_frame() = default;
cb_frame();
cb_frame(int msg_id, uint32_t can_id);
cb_frame(int msg_id, uint32_t can_id, const byte_vector& _frame, bool _rtr);
void clear();
@ -36,11 +38,26 @@ namespace solar
virtual int compare(const cb_frame& b, const int *comparecolumns) const;
virtual int compare(const cb_frame &b, int cmpflag, bool *stopcomparesignal, bool validnextcol) const;
int compare_frame(const byte_vector& _frame) const;
virtual std::string tag(const std::string& prefix = "") const;
void setpublish(bool do_publish = true) const; // we promise only to modify the publish flag
bool getpublish() const;
virtual std::string tag(const std::string& prefix = "", int prefixlen = 18) const;
virtual std::string to_string() const;
virtual ~cb_frame() = default;
// using default (compiler auto generated) copy and move constructors and assignment operators
/// Helper functions to convert four scaled values into a byte stream
/// set scale to negative for signed values
static std::vector<uint8_t> get_byte_stream(double value1, int scale1, double value2 = 0, int scale2 = 0, double value3 = 0, int scale3 = 0, double value4 = 0, int scale4 = 0);
/// Overloaded function to handle double scale for the fourth value
static std::vector<uint8_t> get_byte_stream(double value1, int scale1, double value2, int scale2, double value3, int scale3, double value4, double scale4);
/// helper function to extract a scaled double value from two bytes
static double get_double(uint8_t lsb, uint8_t msb, int inv_scale, bool is_signed = false); //inv_scale is the reciprocal of scale factor
bool send_frame(esphome::canbus::Canbus *canbus, bool extended_id = false, bool remote_transmission_request = false) const;
static bool send_frame(esphome::canbus::Canbus *canbus, uint32_t can_id, const byte_vector& frame, bool extended_id = false, bool remote_transmission_request = false); // static version to send arbitrary frame
};
extern const cb_frame emptyframe; // to return a reference to when no valid frame is found
} // namespace solar
#endif // __SOLAR_CB_FRAME

View File

@ -32,15 +32,16 @@ namespace solar
}
auto& kvp = *ret.first;
auto& item = kvp.second;
bool publish = false;
if(item.update(storeitem, publish, comparecolumns, 1)) {
auto updateresult = item.update(storeitem, comparecolumns, 1);
if(updateresult & cbf_store::cbf_updateresult::stu_DUPLICATE) {
// ESP_LOGI(item.store0.tag("== ST1 DUP == ").c_str(), item.store0.to_string().c_str());
return publish;
return updateresult & cbf_store::cbf_updateresult::stu_PUBLISH;
}
// try next store to see if it has a duplicate of new item
if(item.update(storeitem, publish, comparecolumns, 2)) {
updateresult = item.update(storeitem, comparecolumns, 2);
if(updateresult & cbf_store::cbf_updateresult::stu_DUPLICATE) {
// ESP_LOGI(item.store1.tag("== ST2 DUP == ").c_str(), item.store1.to_string().c_str());
return publish;
return updateresult & cbf_store::cbf_updateresult::stu_PUBLISH;
}
item.update(storeitem);
//cache_map.erase(kvp.first);
@ -49,6 +50,34 @@ namespace solar
// ESP_LOGE(item.store0.tag("== ST1 ERR == ").c_str(), "Error re-inserting item into cache_map");
// ESP_LOGE(item.store1.tag("== ST2 ERR == ").c_str(), "Error re-inserting item into cache_map");
//}
return publish;
return false;
}
const cb_frame& cbf_cache::get_frame(uint32_t can_id) const
{
auto it = cache_map.find(can_id);
if(it == cache_map.end())
return emptyframe; // return reference to static empty frame if not found
const auto& item = it->second;
return item.get_frame();
}
bool cbf_cache::send_frame(esphome::canbus::Canbus *canbus, uint32_t can_id, bool extended_id, bool remote_transmission_request)
{
if(!this->hasitem(can_id))
return false;
const auto& cbf = get_frame(can_id);
if(cbf.getpublish()) {
return cbf.send_frame(canbus, extended_id, remote_transmission_request);
}
return false;
}
// static version to send arbitrary frame
bool cbf_cache::send_frame(esphome::canbus::Canbus *canbus, uint32_t can_id, const byte_vector& frame, bool extended_id, bool remote_transmission_request)
{
return cb_frame::send_frame(canbus, can_id, frame, extended_id, remote_transmission_request);
}
bool cbf_cache::send_request(esphome::canbus::Canbus *canbus, uint32_t can_id, bool extended_id)
{
return cb_frame::send_frame(canbus, can_id, {}, extended_id, true);
}
} // namespace solar

View File

@ -12,9 +12,8 @@ using namespace esphome;
namespace solar
{
class cbf_cache {
private:
std::map<uint32_t, cbf_cache_item> cache_map; // map of CAN IDs to cache items
public:
std::map<uint32_t, cbf_cache_item> cache_map; // map of CAN IDs to cache items
cbf_cache() = default;
void clear();
int size() const;
@ -23,7 +22,13 @@ namespace solar
const cbf_cache_item& getitem(uint32_t can_id) const;
// Add a new item to the cache or update an existing one
bool additem(const cbf_store& item);
const cb_frame& get_frame(uint32_t can_id) const;
virtual ~cbf_cache() = default; // virtual destructor for base class
bool send_frame(esphome::canbus::Canbus *canbus, uint32_t can_id, bool extended_id = false, bool remote_transmission_request = false);
static bool send_frame(esphome::canbus::Canbus *canbus, uint32_t can_id, const byte_vector& frame, bool extended_id = false, bool remote_transmission_request = false); // static version to send arbitrary frame
static bool send_request(esphome::canbus::Canbus *canbus, uint32_t can_id, bool extended_id = false); // static version to send remote transmission request frame
// using default (compiler auto generated) copy and move constructors and assignment operators
};
} // namespace solar

View File

@ -15,10 +15,7 @@ namespace solar
this->store0 = store0.clone();
this->store1 = store0.clone();
this->store0->id = 1;
//this->store1->clear();
this->store1->id = 2;
//ESP_LOGI( this->store0->tag("cache_item CTOR1A ").c_str(), "%-20s %s", "Created cache item", this->store0->to_string().c_str());
//ESP_LOGI( this->store1->tag("cache_item CTOR1B ").c_str(), "%-20s %s", "Created cache item", this->store1->to_string().c_str());
}
void cbf_cache_item::clear()
{
@ -32,60 +29,69 @@ namespace solar
}
bool cbf_cache_item::update(const cbf_store& newitem)
{
//std::string posttag0 = std::string("cache_item UPD ST1");
//std::string posttag1 = std::string("cache_item UPD ST2");
// ESP_LOGI(store0->tag(posttag0).c_str(), "VALID: %s %s", store0->is_valid() ? "Y" : "N", store0->to_string().c_str());
// ESP_LOGI(store1->tag(posttag1).c_str(), "VALID: %s %s", store1->is_valid() ? "Y" : "N", store1->to_string().c_str());
if(!this->store0->is_valid()) {
store0 = newitem.clone();
store0->id = 1;
// ESP_LOGI(store0->tag("== ST1 INV NEW ==").c_str(), store0->to_string().c_str());
return true;
}
if(!this->store1->is_valid()) {
store1 = newitem.clone();
store1->id = 2;
// ESP_LOGI(store1->tag("== ST2 INV NEW ==").c_str(), store1->to_string().c_str());
return true;
}
bool result = store1->last_timestamp > store0->last_timestamp;
if(result) {
store0 = newitem.clone();
this->store0->id = 1;
// ESP_LOGI(store0->tag("== ST1 OLD NEW ==").c_str(), store0->to_string().c_str());
}
else {
store1 = newitem.clone();
this->store1->id = 2;
// ESP_LOGI(store1->tag("== OLD ST2 NEW ==").c_str(), store1->to_string().c_str());
}
return result;
}
bool cbf_cache_item::update(const cbf_store& newitem, bool& publish, const int *comparecolumns, int store_selector)
cbf_store::cbf_updateresult cbf_cache_item::update(const cbf_store& newitem, const int *comparecolumns, int store_selector)
{
bool result = false;
cbf_store::cbf_updateresult result = cbf_store::cbf_updateresult::stu_NONE;
switch(store_selector) {
case 2:
result = this->store1->update(newitem, publish, comparecolumns);
result = this->store1->update(newitem, comparecolumns);
break;
default:
result = this->store0->update(newitem, publish, comparecolumns);
result = this->store0->update(newitem, comparecolumns);
break;
}
//std::string posttag = std::string("cache_item ") + (result ? "UPD" : "NUP") + " ST" + std::to_string(store_selector) + " ";
//if(store_selector == 1 ) {
// ESP_LOGI(store1->tag(posttag).c_str(), store1->to_string().c_str());
//} else {
// ESP_LOGI(store0->tag(posttag).c_str(), store0->to_string().c_str());
//}
return result;
}
const cb_frame& cbf_cache_item::get_frame() const
{
return get_store(); // return most recent valid store's frame
}
const cbf_store& cbf_cache_item::get_store() const
{
if(this->store0->is_valid() && !this->store1->is_valid()) {
return *store0;
}
if(!this->store0->is_valid() && this->store1->is_valid()) {
return *store1;
}
if(this->store0->is_valid() && this->store1->is_valid()) {
if(this->store0->last_timestamp >= this->store1->last_timestamp) {
return *this->store0;
} else {
return *this->store1;
}
}
return *this->store0; // no valid stores, but return store0 which is always present
}
std::string cbf_cache_item::tag(const std::string& prefix) const
{
char tag[24];
snprintf(tag, sizeof(tag), "%-18s ", prefix.c_str());
return std::string(tag);
}
return get_store().tag(prefix, prefix.length());
}
std::string cbf_cache_item::to_string() const
{
return get_store().to_string();
}
std::string cbf_cache_item::tag0(const std::string& prefix) const
{
return store0->tag(prefix);
@ -94,15 +100,16 @@ namespace solar
{
return store1->tag(prefix);
}
std::string cbf_cache_item::to_string() const
std::string cbf_cache_item::st0_tostring() const
{
// Be extra careful with the placement of printf format specifiers and their corresponding arguments.
// Incorrect placement / misaligned arguments can lead to undefined behavior and processor crashes.
char buffer[180];
// trim
auto trimmedtag0 = trim(tag0());
auto trimmedtag1 = trim(tag1());
snprintf(buffer, sizeof(buffer), "ST1: [%s] %s | ST2: [%s] %s", trimmedtag0.c_str(), store0->to_string().c_str(), trimmedtag1.c_str(), store1->to_string().c_str());
char buffer[150];
snprintf(buffer, sizeof(buffer), "ST1: [%s] %s", trim(tag0()).c_str(), store0->to_string().c_str());
return std::string(buffer);
}
std::string cbf_cache_item::st1_tostring() const
{
char buffer[150];
snprintf(buffer, sizeof(buffer), "ST2: [%s] %s", trim(tag1()).c_str(), store1->to_string().c_str());
return std::string(buffer);
}

View File

@ -20,12 +20,15 @@ namespace solar
void clear();
void swap(cbf_cache_item &s);
bool update(const cbf_store& newitem);
bool update(const cbf_store& newitem, bool& publish, const int *comparecolumns, int store_selector);
cbf_store::cbf_updateresult update(const cbf_store& newitem, const int *comparecolumns, int store_selector);
const cb_frame& get_frame() const; // returns the most recent valid frame
const cbf_store& get_store() const; // returns the most recent valid store
virtual std::string tag0(const std::string& prefix = "") const;
virtual std::string tag1(const std::string& prefix = "") const;
virtual std::string tag(const std::string& prefix = "") const;
virtual std::string to_string() const;
virtual std::string st0_tostring() const; // string of store 0
virtual std::string st1_tostring() const; // string of store 1
// the default copy and move constructors and assignment operators are fine
// because shared_ptr takes care of the underlying memory management
cbf_cache_item(const cbf_cache_item& b) = default;

View File

@ -4,14 +4,15 @@ namespace solar
{
// see common.h for definition of publish_spec_t
// publish_spec_t(int on_count, int interval, int timeout)
const publish_spec_t cbf_pylon::publish_spec = publish_spec_t(3, 10, 30); // default publish spec for Pylontech battery messages
const publish_spec_t cbf_pylon::publish_spec = publish_spec_t(3, 15, 30); // default publish spec for Pylontech battery messages
const publish_spec_t cbf_pylon::rtr_publish_spec = publish_spec_t(2, 10, 15); // for remote transmission requests
cbf_pylon& cbf_pylon::operator=(cbf_pylon&& src)
{
if (this != &src) {
cb_frame::operator=(std::move(src));
src.clear();
ESP_LOGI(tag("pylon MOASS").c_str(), "%-20s %s", "Assigned pylon", this->to_string().c_str());
//ESP_LOGI(tag("pylon MOASS").c_str(), "%-20s %s", "Assigned pylon", this->to_string().c_str());
}
return *this;
}
@ -19,6 +20,13 @@ namespace solar
{
cb_frame::clear();
}
// float cbf_pylon::_get_battery_charge_voltage_limit() const
// {
// const auto& x = this->frame;
// return 0.1 * (float)((x[1] << 8) + x[0]);
// }
// Function to build a message from the pylon canbus frame
std::string cbf_pylon::to_string() const
{
@ -27,6 +35,9 @@ namespace solar
{
case CB_BATTERY_LIMITS:
{
if(rtr) {
return "Request for BATTERY LIMITS info";
}
if (this->frame.size() < 6) {
return "Invalid frame size for CB_BATTERY_LIMITS";
}
@ -40,6 +51,9 @@ namespace solar
break;
case CB_BATTERY_STATE:
{
if(rtr) {
return "Request for BATTERY STATE info";
}
if (this->frame.size() < 4) {
return "Invalid frame size for CB_BATTERY_STATE";
}
@ -52,6 +66,9 @@ namespace solar
break;
case CB_BATTERY_STATUS:
{
if(rtr) {
return "Request for BATTERY STATUS info";
}
if (this->frame.size() < 6) {
return "Invalid frame size for CB_BATTERY_STATUS";
}
@ -65,6 +82,9 @@ namespace solar
break;
case CB_BATTERY_FAULT:
{
if(rtr) {
return "Request for BATTERY FAULT info";
}
if (this->frame.size() < 8) {
return "Invalid frame size for CB_BATTERY_FAULT";
}
@ -96,6 +116,9 @@ namespace solar
break;
case CB_BATTERY_REQUEST_FLAGS:
{
if(rtr) {
return "Request for BATTERY REQUEST FLAGS info";
}
if (this->frame.size() < 1) {
return "Invalid frame size for CB_BATTERY_REQUEST_FLAGS";
}
@ -112,18 +135,24 @@ namespace solar
break;
case CB_BATTERY_MANUFACTURER:
{
if (this->frame.size() < 8) {
return "Invalid frame size for CB_BATTERY_MANUFACTURER";
}
const auto& x = this->frame;
// Manufacturer name is in the first 8 bytes, padded with spaces
// Convert to string and trim trailing spaces
std::string manufacturer(reinterpret_cast<const char*>(x.data()), 8);
manufacturer.erase(std::find_if(manufacturer.rbegin(), manufacturer.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), manufacturer.end());
return "BATTERY OEM: " + manufacturer;
if(rtr) {
return "Request for BATTERY MANUFACTURER info";
}
if (this->frame.size() < 8) {
return "Invalid frame size for CB_BATTERY_MANUFACTURER";
}
const auto& x = this->frame;
// Manufacturer name is in the first 8 bytes, padded with spaces
// Convert to string and trim trailing spaces
std::string manufacturer(reinterpret_cast<const char*>(x.data()), 8);
manufacturer.erase(std::find_if(manufacturer.rbegin(), manufacturer.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), manufacturer.end());
return "BATTERY OEM: " + manufacturer;
}
break;
}
if(rtr) {
return "Request for unknown CAN ID info";
}
return "Unknown CAN ID";
}
} // namespace solar

View File

@ -8,70 +8,30 @@ using namespace esphome;
namespace solar
{
class cbf_pylon : virtual public cb_frame {
public:
// Pylontech publish spec
// This is used to determine when to publish the battery data from the Pylontech battery
static const publish_spec_t publish_spec; // default publish spec for Pylontech battery messages
static const publish_spec_t rtr_publish_spec; // for remote transmission requests
// Pylontech battery CAN IDs
// All unverified IDs are taken from the Pylontech CAN protocol documentation
// https://domosimple.eu/en/documentation/manuels/pylontech-can-bus-protocole
// VERIFIED IDs have been confirmed by capturing actual CAN bus traffic from a Pylontech battery
static const int CB_BATTERY_LIMITS = 0x351; // VERIFIED: Pylontech battery max charging/discharging values message
static const int CB_BATTERY_SETTING2 = 0x352; // used for Pylontech batteries
static const int CB_BATTERY_SETTING3 = 0x353; // used for Pylontech batteries
static const int CB_BATTERY_SETTING4 = 0x354; // used for Pylontech batteries
static const int CB_BATTERY_STATE = 0x355; // VERIFIED: Pylontech battery state message
static const int CB_BATTERY_STATUS = 0x356; // VERIFIED: Pylontech battery status message
static const int CB_BATTERY_ERROR = 0x357;
static const int CB_BATTERY_WARNING = 0x358;
static const int CB_BATTERY_FAULT = 0x359; // VERIFIED: Pylontech battery fault message
static const int CB_BATTERY_REQUEST = 0x35A;
static const int CB_BATTERY_RESPONSE = 0x35B;
static const int CB_BATTERY_REQUEST_FLAGS = 0x35C; // VERIFIED: Pylontech battery request flag message
static const int CB_BATTERY_MANUFACTURER_ID = 0x35D;
static const int CB_BATTERY_MODEL_ID = 0x35E;
static const int CB_BATTERY_MANUFACTURER = 0x35E; // VERIFIED: Pylontech battery manufacturer message
static const int CB_BATTERY_MODEL = 0x35D;
static const int CB_BATTERY_SERIAL_NUMBER = 0x35F;
static const int CB_BATTERY_VERSION = 0x360;
static const int CB_BATTERY_CAPACITY = 0x361;
static const int CB_BATTERY_TEMPERATURE = 0x362;
static const int CB_BATTERY_VOLTAGE = 0x363;
static const int CB_BATTERY_CURRENT = 0x364;
static const int CB_BATTERY_SOC = 0x365;
static const int CB_BATTERY_SOH = 0x366;
static const int CB_BATTERY_CHARGE_POWER = 0x367;
static const int CB_BATTERY_DISCHARGE_POWER = 0x368;
static const int CB_BATTERY_CHARGE_ENERGY = 0x369;
static const int CB_BATTERY_DISCHARGE_ENERGY = 0x36A;
static const int CB_BATTERY_CHARGE_CYCLES = 0x36B;
static const int CB_BATTERY_CHARGE_LIMIT = 0x36C;
static const int CB_BATTERY_DISCHARGE_LIMIT = 0x36D;
static const int CB_BATTERY_CHARGE_MODE = 0x36E;
static const int CB_BATTERY_DISCHARGE_MODE = 0x36F;
static const int CB_BATTERY_CHARGE_STATUS = 0x370;
static const int CB_BATTERY_DISCHARGE_STATUS = 0x371;
static const int CB_BATTERY_CHARGE_CYCLE_COUNT = 0x372;
static const int CB_BATTERY_CHARGE_CYCLE_LIMIT = 0x373;
static const int CB_BATTERY_CHARGE_CYCLE_STATUS = 0x374;
static const int CB_BATTERY_CHARGE_CYCLE_MODE = 0x375;
static const int CB_BATTERY_CHARGE_CYCLE_LIMIT_STATUS = 0x376;
static const int CB_BATTERY_CHARGE_CYCLE_MODE_STATUS = 0x377;
static const int CB_BATTERY_CHARGE_CYCLE_LIMIT_MODE = 0x378;
static const int CB_BATTERY_CHARGE_CYCLE_LIMIT_MODE_STATUS = 0x379;
static const int CB_BATTERY_CHARGE_CYCLE_LIMIT_MODE_STATUS2 = 0x37A;
static const int CB_BATTERY_CHARGE_CYCLE_LIMIT_MODE_STATUS3 = 0x37B;
static const int CB_BATTERY_CHARGE_CYCLE_LIMIT_MODE_STATUS4 = 0x37C;
static const int CB_BATTERY_CHARGE_CYCLE_LIMIT_MODE_STATUS5 = 0x37D;
static const int CB_BATTERY_CHARGE_CYCLE_LIMIT_MODE_STATUS6 = 0x37E;
static const int CB_BATTERY_CHARGE_CYCLE_LIMIT_MODE_STATUS7 = 0x37F;
// Pylontech battery publish spec
// This is used to determine when to publish the battery data
static const publish_spec_t publish_spec;
//Property<int, cbf_pylon> battery_charge_voltage_limit {this, &cbf_pylon::_set_battery_charge_voltage_limit, &cbf_pylon::_get_battery_charge_voltage_limit };
//Property<float, cbf_pylon> battery_charge_voltage_limit {this, &cbf_pylon::_get_battery_charge_voltage_limit};
cbf_pylon() = default;
cbf_pylon(int msg_id, uint32_t can_id, const byte_vector& frame, bool rtr);
void clear();
std::string to_string() const override;
virtual std::string to_string() const override;
cbf_pylon& operator=(cbf_pylon&& src); // required due to double inheritance in cbf_store_pylon
// using default (compiler auto generated) copy and move constructors and assignment operators
};

View File

@ -64,24 +64,29 @@ namespace solar
{
return (this->last_timestamp == 0) || ((currenttime - this->last_timestamp) >= timeout_interval);
}
bool cbf_store::update(const cbf_store& newitem, bool& publish, const int *comparecolumns)
cbf_store::cbf_updateresult cbf_store::update(const cbf_store& newitem, const int *comparecolumns)
{
ESP_LOGW(this->tag("store UPDATE").c_str(), "%-20s %s", "Default update call", this->to_string().c_str()); // this should happen as all updating should be called from derived classes, i.e. providing publish_spec as a parameter
return update(newitem, publish_spec_t{1, 5, 10}, publish, comparecolumns); // we are forced to instantiate this class, so we provide a default implementation
ESP_LOGW(this->tag("store UPDATE").c_str(), "%-20s %s", "Default update call", this->to_string().c_str()); // this should happen as all updating should be called from derived classes, i.e. providing the publish_specs as parameters
return update(newitem, publish_spec_t{1, 5, 10}, publish_spec_t{1, 5, 10}, comparecolumns); // we are forced to instantiate this class, so we provide a default implementation
}
bool cbf_store::update(const cbf_store& newitem, publish_spec_t publish_spec, bool& publish, const int *comparecolumns)
cbf_store::cbf_updateresult cbf_store::update(const cbf_store& newitem, publish_spec_t publish_spec, publish_spec_t rtr_publish_spec, const int *comparecolumns)
{
if(!is_valid()) {
return false;
return cbf_updateresult::stu_NONE; // cannot update an invalid store
}
auto pbspec = rtr ? rtr_publish_spec : publish_spec;
time_t newtime = newitem.last_timestamp;
bool publish_expired = this->is_publish_expired(newtime, publish_spec.interval);
bool validity_expired = this->is_validity_expired(newtime, publish_spec.timeout);
bool publish_expired = this->is_publish_expired(newtime, pbspec.interval);
bool validity_expired = this->is_validity_expired(newtime, pbspec.timeout);
bool isduplicate = this->compare(newitem, comparecolumns) == 0;
time_t reset_timer = this->first_timestamp + publish_spec.interval - newtime;
auto ntstime = ESPTime::from_epoch_local(newtime).strftime("%H:%M:%S");
time_t reset_timer = this->first_timestamp + pbspec.interval - newtime;
int timediff = is_valid() ? (int)(newtime - this->last_timestamp) : -1;
//ESP_LOGI(this->tag("store UPDATE Bef").c_str(), "%s Td:%2d To:%s Ex:%s Rt:%2d Nts: %s %s", isduplicate ? "DUP" : "NEW", timediff, validity_expired ? "Y" : "N", publish_expired ? "Y" : "N", static_cast<int>(reset_timer), ntstime.c_str(), this->to_string().c_str());
auto result = isduplicate ? cbf_updateresult::stu_DUPLICATE : cbf_updateresult::stu_NONE;
// auto ftstime = ESPTime::from_epoch_local(this->first_timestamp).strftime("%H:%M:%S");
// auto ltstime = ESPTime::from_epoch_local(this->last_timestamp).strftime("%H:%M:%S");
// auto ntstime = ESPTime::from_epoch_local(newtime).strftime("%H:%M:%S");
// if(rtr)
// ESP_LOGI(this->tag("store UPDATE").c_str(), "A: %s PBS %d,%d,%d PE:%s VE:%s DUP:%s FTS %s LTS %s NTS %s RES[D%s P%s]", rtr ? "RTR" : "NML", pbspec.on_count, pbspec// .interval, pbspec.timeout, publish_expired ? "Y" : "N", validity_expired ? "Y" : "N", isduplicate ? "Y" : "N", ftstime.c_str(), ltstime.c_str(), ntstime.c_str(),// result & cbf_updateresult::stu_DUPLICATE ? "Y" : "N", result & cbf_updateresult::stu_PUBLISH ? "Y" : "N");
if(validity_expired) {
*this = newitem;
if(!isduplicate) {
@ -91,15 +96,22 @@ namespace solar
}
if(isduplicate || publish_expired) {
this->count++;
publish = (this->count == publish_spec.on_count);
if(this->count == pbspec.on_count)
this->setpublish(); // must be reset by caller after publish
if(this->getpublish())
result = static_cast<cbf_updateresult>(result | cbf_updateresult::stu_PUBLISH);
this->last_timestamp = newtime;
if(reset_timer <= 0) {
this->first_timestamp = newtime;
this->count = 1;
}
}
//ESP_LOGI(this->tag("store UPDATE Aft").c_str(), "%s Td:%2d To:%s Ex:%s Rt:%2d Pu:%s Nts: %s %s", isduplicate ? "DUP" : "NEW", timediff, validity_expired ? "Y" : "N", publish_expired ? "Y" : "N", static_cast<int>(reset_timer), publish ? "Y" : "N", ntstime.c_str(), this->to_string().c_str());
return isduplicate;
// ftstime = ESPTime::from_epoch_local(this->first_timestamp).strftime("%H:%M:%S");
// ltstime = ESPTime::from_epoch_local(this->last_timestamp).strftime("%H:%M:%S");
// if(rtr)
// ESP_LOGI(this->tag("store UPDATE").c_str(), "B: %s PBS %d,%d,%d PE:%s VE:%s DUP:%s FTS %s LTS %s NTS %s RES[D%s P%s]", rtr ? "RTR" : "NML", pbspec.on_count, pbspec.interval, pbspec.timeout, publish_expired ? "Y" : "N", validity_expired ? "Y" : "N", isduplicate ? "Y" : "N", ftstime.c_str(), ltstime.c_str(), ntstime.c_str(), result & cbf_updateresult::stu_DUPLICATE ? "Y" : "N", result & cbf_updateresult::stu_PUBLISH ? "Y" : "N");
return result;
}
int cbf_store::compare(const cbf_store& b, const int *comparecolumns) const
{
@ -199,15 +211,15 @@ namespace solar
result = -result;
return result;
}
std::string cbf_store::tag(const std::string& prefix) const
std::string cbf_store::tag(const std::string& prefix, int prefixlen) const
{
// Be extra careful with the placement of printf format specifiers and their corresponding arguments.
// Incorrect placement / misaligned arguments can lead to undefined behavior and processor crashes.
char tag[48];
if(prefix.length() == 0)
snprintf(tag, sizeof(tag), "%04d id%d 0x%03X ", msg_id, id, can_id);
else
snprintf(tag, sizeof(tag), "%-18s %04d id%d 0x%03X ", prefix.c_str(), msg_id, id, can_id);
snprintf(tag, sizeof(tag), "#%04d s%d 0x%03X %s", msg_id, id, can_id, getpublish() ? "P" : " ");
else
snprintf(tag, sizeof(tag), "%-*s #%04d s%d 0x%03X %s", prefixlen, prefix.c_str(), msg_id, id, can_id, getpublish() ? "P" : " ");
return std::string(tag);
}
std::string cbf_store::to_string() const

View File

@ -16,12 +16,12 @@ namespace solar
int count; // how many times a duplicate of this frame was received; used to signal publish
time_t first_timestamp; // used for checking whether updateinterval has expired, i.e. to force a publish
time_t last_timestamp; // used for choosing which store to overwrite with new (different) frame data
enum cbf_store_sortcolumns : int
enum cbf_sortcolumns : int
{
sisortNULL,
sisortcanid = cb_frame::sisortcanid,
sisortframe = cb_frame::sisortframe,
sisortrtr = cb_frame::sisortrtr,
sisortframe = cb_frame::sisortframe,
sisortmsgid = cb_frame::sisortmsgid,
sisortcount,
sisortfirst_timestamp,
@ -32,6 +32,14 @@ namespace solar
sisortwild = WILDNULL, // where applicable, an empty or null entry equates to wildcard
sistopcomparecol = STOPCOMPARE // stop compare after this column if both items to be compared are valid and a compare was possible
};
enum cbf_updateresult : int
{
stu_NONE = 0, // no update
stu_DUPLICATE = 0b01, // updated with new timestamps no publish
stu_PUBLISH = 0b10, // can publish data
stu_DUPLICATE_PUBLISH = 0b11, // updated with new timestamps; can publish data
};
cbf_store() = delete; // default constructor not allowed
cbf_store(int msg_id, uint32_t can_id, int id);
cbf_store(int msg_id, uint32_t can_id, int id, time_t _first_timestamp, time_t _last_timestamp);
@ -43,12 +51,12 @@ namespace solar
std::shared_ptr<cbf_store> clone() const;
int compare(const cbf_store& b, const int *comparecolumns) const;
std::string tag(const std::string& prefix = "") const override;
std::string to_string() const override;
virtual std::string tag(const std::string& prefix = "", int prefixlen = 18) const override;
virtual std::string to_string() const override;
bool is_publish_expired(time_t currenttime, int update_interval) const;
bool is_validity_expired(time_t currenttime, int timeout_interval) const;
bool update(const cbf_store& newitem, publish_spec_t publish_spec, bool& publish, const int *comparecolumns);
virtual bool update(const cbf_store& newitem, bool& publish, const int *comparecolumns); // should be pure virtual function, but we need to instantiate this class
cbf_updateresult update(const cbf_store& newitem, publish_spec_t publish_spec, publish_spec_t rtr_publish_spec, const int *comparecolumns);
virtual cbf_updateresult update(const cbf_store& newitem, const int *comparecolumns); // should be pure virtual function, but we need to instantiate this class
private:
int compare(const cbf_store &b, int cmpflag, bool *stopcomparesignal, bool validnextcol) const;

View File

@ -2,8 +2,8 @@
namespace solar
{
cbf_store_pylon::cbf_store_pylon(int msg_id, uint32_t can_id, const byte_vector& _frame, bool _rtr, time_t timestamp)
: cb_frame(msg_id, can_id, _frame, _rtr), cbf_store(msg_id, can_id, 0, timestamp, timestamp)
cbf_store_pylon::cbf_store_pylon(int msg_id, uint32_t can_id, const byte_vector& frame, bool rtr, time_t timestamp)
: cb_frame(msg_id, can_id, frame, rtr), cbf_store(msg_id, can_id, 0, timestamp, timestamp) //, cbf_pylon(msg_id, can_id, frame, rtr)
{
this->id = 0;
this->count = 0;
@ -58,14 +58,14 @@ namespace solar
cb_frame::swap(s);
cbf_store::swap(s);
}
bool cbf_store_pylon::update(const cbf_store& newitem, bool& publish, const int *comparecolumns)
cbf_store::cbf_updateresult cbf_store_pylon::update(const cbf_store& newitem, const int *comparecolumns)
{
//ESP_LOGI(tag("store_pylon UPDATE").c_str(), "%-20s %s", "Updating store_pylon", this->to_string().c_str());
return cbf_store::update(newitem, publish_spec, publish, comparecolumns);
return cbf_store::update(newitem, publish_spec, rtr_publish_spec, comparecolumns);
}
std::string cbf_store_pylon::to_string() const
{
return cbf_store::to_string() + " " + cbf_pylon::to_string();
return cb_frame::to_string() + " " + cbf_pylon::to_string();
}
} // namespace solar

View File

@ -18,8 +18,8 @@ namespace solar
std::shared_ptr<cbf_store_pylon> clone() const;
void clear();
void swap(cbf_store_pylon &s);
virtual bool update(const cbf_store& newitem, bool& publish, const int *comparecolumns) override;
std::string to_string() const override;
virtual cbf_store::cbf_updateresult update(const cbf_store& newitem, const int *comparecolumns) override;
virtual std::string to_string() const override;
cbf_store_pylon(const cbf_store_pylon& b);
cbf_store_pylon& operator=(const cbf_store_pylon& b);

View File

@ -12,6 +12,8 @@ namespace solar
#define SEARCHWILD 0x0100000 // do wildcard comparison (only searchkey should contain wildcards)
#define num_compare(a, b) ((a) > (b)) ? +1 : ((a) < (b)) ? -1 : 0
#define bool_compare(a, b) ((!(a) && !(b)) || ((a) && (b))) ? 0 : (a) ? +1 : -1
#define max(a, b) ((a) > (b)) ? (a) : (b)
#define min(a, b) ((a) < (b)) ? (a) : (b)
typedef unsigned int uint;
typedef std::vector<uint8_t> byte_vector;