From ea865dfa94dd614119722136a9b5aedfe1e54831 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 3 Sep 2025 22:38:18 +0200 Subject: [PATCH] Added sthome canbus --- cb_frame.cpp | 156 +++++++++++++++++++++++++++++++++++++++----- cb_frame.h | 45 +++++++++---- cbf_cache.cpp | 41 ++++++++++-- cbf_cache.h | 9 ++- cbf_cache_item.cpp | 73 +++++++++++---------- cbf_cache_item.h | 7 +- cbf_pylon.cpp | 51 +++++++++++---- cbf_pylon.h | 60 +++-------------- cbf_store.cpp | 46 ++++++++----- cbf_store.h | 20 ++++-- cbf_store_pylon.cpp | 10 +-- cbf_store_pylon.h | 4 +- common.h | 2 + 13 files changed, 361 insertions(+), 163 deletions(-) diff --git a/cb_frame.cpp b/cb_frame.cpp index bd3009a..59c629b 100644 --- a/cb_frame.cpp +++ b/cb_frame.cpp @@ -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 += " "; - return line; + std::string line; + int n = 0; + if(rtr) { + line = " "; + 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 ""; } - 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::quiet_NaN(); + } + if (is_signed) { + value = static_cast((msb << 8) + lsb); + } else { + value = static_cast((msb << 8) + lsb); + } + //ESP_LOGI("cb_frame::get_double", "0x%02X%02X unsigned %d, scale %d", msb, lsb, value, scale); + return static_cast(value) / inv_scale; + } + // Helper function to convert four scaled values into a byte stream + // set scale to negative for signed values + std::vector 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 byte_stream(8, 0); + int value; + if(scale1 != 0) { + value = (scale1 < 0) ? static_cast(value1 * -scale1) : static_cast(value1 * scale1); + byte_stream[0] = value % 256; + byte_stream[1] = (value >> 8) % 256; + } + if(scale2 != 0) { + value = (scale2 < 0) ? static_cast(value2 * -scale2) : static_cast(value2 * scale2); + byte_stream[2] = value % 256; + byte_stream[3] = (value >> 8) % 256; + } + if(scale3 != 0) { + value = (scale3 < 0) ? static_cast(value3 * -scale3) : static_cast(value3 * scale3); + byte_stream[4] = value % 256; + byte_stream[5] = (value >> 8) % 256; + } + if(scale4 != 0) { + value = (scale4 < 0) ? static_cast(value4 * -scale4) : static_cast(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 cb_frame::get_byte_stream(double value1, int scale1, double value2, int scale2, double value3, int scale3, double value4, double scale4) + { + std::vector byte_stream(8, 0); + int value; + if(scale1 != 0) { + value = (scale1 < 0) ? static_cast(value1 * -scale1) : static_cast(value1 * scale1); + byte_stream[0] = value % 256; + byte_stream[1] = (value >> 8) % 256; + } + if(scale2 != 0) { + value = (scale2 < 0) ? static_cast(value2 * -scale2) : static_cast(value2 * scale2); + byte_stream[2] = value % 256; + byte_stream[3] = (value >> 8) % 256; + } + if(scale3 != 0) { + value = (scale3 < 0) ? static_cast(value3 * -scale3) : static_cast(value3 * scale3); + byte_stream[4] = value % 256; + byte_stream[5] = (value >> 8) % 256; + } + value = (scale4 < 0) ? static_cast(round(value4 * -scale4)) : static_cast(round(value4 * scale4)); + byte_stream[6] = value % 256; + byte_stream[7] = (value >> 8) % 256; + return byte_stream; + } + } // namespace solar \ No newline at end of file diff --git a/cb_frame.h b/cb_frame.h index 5bd6bca..d97889b 100644 --- a/cb_frame.h +++ b/cb_frame.h @@ -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 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 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 diff --git a/cbf_cache.cpp b/cbf_cache.cpp index 442fbc1..eb974de 100644 --- a/cbf_cache.cpp +++ b/cbf_cache.cpp @@ -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 \ No newline at end of file diff --git a/cbf_cache.h b/cbf_cache.h index e887c3e..26197b9 100644 --- a/cbf_cache.h +++ b/cbf_cache.h @@ -12,9 +12,8 @@ using namespace esphome; namespace solar { class cbf_cache { - private: - std::map cache_map; // map of CAN IDs to cache items public: + std::map 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 diff --git a/cbf_cache_item.cpp b/cbf_cache_item.cpp index 4d503c9..8972d5f 100644 --- a/cbf_cache_item.cpp +++ b/cbf_cache_item.cpp @@ -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); } diff --git a/cbf_cache_item.h b/cbf_cache_item.h index 3057f09..0bf7e43 100644 --- a/cbf_cache_item.h +++ b/cbf_cache_item.h @@ -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; diff --git a/cbf_pylon.cpp b/cbf_pylon.cpp index 4e456cf..7fb38e5 100644 --- a/cbf_pylon.cpp +++ b/cbf_pylon.cpp @@ -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(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(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 \ No newline at end of file diff --git a/cbf_pylon.h b/cbf_pylon.h index 33c64b2..5b6f0ed 100644 --- a/cbf_pylon.h +++ b/cbf_pylon.h @@ -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 battery_charge_voltage_limit {this, &cbf_pylon::_set_battery_charge_voltage_limit, &cbf_pylon::_get_battery_charge_voltage_limit }; + //Property 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 }; diff --git a/cbf_store.cpp b/cbf_store.cpp index df81a9d..5d98ac0 100644 --- a/cbf_store.cpp +++ b/cbf_store.cpp @@ -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(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(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(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 diff --git a/cbf_store.h b/cbf_store.h index f817cf0..f4d2392 100644 --- a/cbf_store.h +++ b/cbf_store.h @@ -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 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; diff --git a/cbf_store_pylon.cpp b/cbf_store_pylon.cpp index 001bdfc..a32a9db 100644 --- a/cbf_store_pylon.cpp +++ b/cbf_store_pylon.cpp @@ -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 \ No newline at end of file diff --git a/cbf_store_pylon.h b/cbf_store_pylon.h index 16aa319..3a854f0 100644 --- a/cbf_store_pylon.h +++ b/cbf_store_pylon.h @@ -18,8 +18,8 @@ namespace solar std::shared_ptr 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); diff --git a/common.h b/common.h index 8cf1b05..ae88eaf 100644 --- a/common.h +++ b/common.h @@ -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 byte_vector;