mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-06-17 13:26:31 +03:00
Merge remote-tracking branch 'emsesp/core3' into core3
This commit is contained in:
@@ -119,9 +119,8 @@ void AnalogSensor::reload(bool get_nvs) {
|
||||
#if defined(EMSESP_STANDALONE)
|
||||
analog_enabled_ = true; // for local offline testing
|
||||
#endif
|
||||
for (auto sensor : sensors_) {
|
||||
for (const auto & sensor : sensors_) {
|
||||
remove_ha_topic(sensor.type(), sensor.gpio());
|
||||
sensor.ha_registered = false;
|
||||
#ifndef EMSESP_STANDALONE
|
||||
if ((sensor.type() >= AnalogType::CNT_0 && sensor.type() <= AnalogType::CNT_2)
|
||||
|| (sensor.type() >= AnalogType::FREQ_0 && sensor.type() <= AnalogType::FREQ_2)) {
|
||||
@@ -675,7 +674,7 @@ void AnalogSensor::publish_values(const bool force) {
|
||||
}
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
JsonDocument doc(PSRAM_DOC);
|
||||
JsonObject obj = doc.to<JsonObject>();
|
||||
bool ha_dev_created = false;
|
||||
|
||||
@@ -704,7 +703,7 @@ void AnalogSensor::publish_values(const bool force) {
|
||||
if (Mqtt::ha_enabled() && (!sensor.ha_registered || force)) {
|
||||
LOG_DEBUG("Recreating HA config for analog sensor GPIO %02d", sensor.gpio());
|
||||
|
||||
JsonDocument config;
|
||||
JsonDocument config(PSRAM_DOC);
|
||||
config["~"] = Mqtt::base();
|
||||
|
||||
char stat_t[50];
|
||||
|
||||
@@ -146,7 +146,7 @@ class AnalogSensor {
|
||||
bool updated_values();
|
||||
|
||||
// return back reference to the sensor list, used by other classes
|
||||
std::vector<Sensor, AllocatorPSRAM<Sensor>> sensors() const {
|
||||
const std::vector<Sensor, AllocatorPSRAM<Sensor>> & sensors() const {
|
||||
return sensors_;
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ class AnalogSensor {
|
||||
bool get_value_info(JsonObject output, const char * cmd, const int8_t id = -1);
|
||||
void store_counters();
|
||||
std::string get_metrics_prometheus();
|
||||
static std::vector<uint8_t> exclude_types() {
|
||||
static const std::vector<uint8_t> & exclude_types() {
|
||||
return exclude_types_;
|
||||
}
|
||||
|
||||
|
||||
@@ -527,8 +527,9 @@ Command::CmdFunction * Command::find_command(const uint8_t device_type, const ui
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const std::string cmd_lower = Helpers::toLower(cmd);
|
||||
for (auto & cf : cmdfunctions_) {
|
||||
if (Helpers::toLower(cmd) == Helpers::toLower(cf.cmd_) && (cf.device_type_ == device_type) && (!device_id || cf.device_id_ == device_id)
|
||||
if (cmd_lower == Helpers::toLower(cf.cmd_) && (cf.device_type_ == device_type) && (!device_id || cf.device_id_ == device_id)
|
||||
&& (cf.device_type_ < EMSdevice::DeviceType::BOILER || flag == CommandFlag::CMD_FLAG_DEFAULT || (flag & 0x3F) == (cf.flags_ & 0x3F))) {
|
||||
return &cf;
|
||||
}
|
||||
@@ -554,9 +555,10 @@ void Command::erase_command(const uint8_t device_type, const char * cmd, uint8_t
|
||||
if ((cmd == nullptr) || (strlen(cmd) == 0) || (cmdfunctions_.empty())) {
|
||||
return;
|
||||
}
|
||||
auto it = cmdfunctions_.begin();
|
||||
const std::string cmd_lower = Helpers::toLower(cmd);
|
||||
auto it = cmdfunctions_.begin();
|
||||
for (auto const & cf : cmdfunctions_) {
|
||||
if (Helpers::toLower(cmd) == Helpers::toLower(cf.cmd_) && (cf.device_type_ == device_type) && ((flag & 0x3F) == (cf.flags_ & 0x3F))) {
|
||||
if (cmd_lower == Helpers::toLower(cf.cmd_) && (cf.device_type_ == device_type) && ((flag & 0x3F) == (cf.flags_ & 0x3F))) {
|
||||
cmdfunctions_.erase(it);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ class Command {
|
||||
}
|
||||
};
|
||||
|
||||
static std::vector<CmdFunction, AllocatorPSRAM<CmdFunction>> commands() {
|
||||
static const std::vector<CmdFunction, AllocatorPSRAM<CmdFunction>> & commands() {
|
||||
return cmdfunctions_;
|
||||
}
|
||||
|
||||
|
||||
@@ -187,8 +187,9 @@
|
||||
{ 74, DeviceType::ALERT, "EM10", DeviceFlags::EMS_DEVICE_FLAG_NONE},
|
||||
|
||||
// Gateways - 0x48
|
||||
{17, DeviceType::GATEWAY, "MX400", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x48 and 0x4B
|
||||
{189, DeviceType::GATEWAY, "KM200, MB LAN 2", DeviceFlags::EMS_DEVICE_FLAG_NONE},
|
||||
{17, DeviceType::GATEWAY, "MX400", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x48, 0x4B, or 0x50 as wireless base
|
||||
{189, DeviceType::GATEWAY, "KM200, MB LAN 2", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x48
|
||||
{222, DeviceType::GATEWAY, "KM300,", DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x4A
|
||||
{252, DeviceType::GATEWAY, "K30RF, MX300", DeviceFlags::EMS_DEVICE_FLAG_NONE},
|
||||
|
||||
// Generic - 0x40 or other with no product-id and no version
|
||||
@@ -207,4 +208,4 @@
|
||||
// {157, DeviceType::THERMOSTAT, "RC120", DeviceFlags::EMS_DEVICE_FLAG_CR120}
|
||||
#endif
|
||||
|
||||
// clang-format on
|
||||
// clang-format on
|
||||
@@ -105,8 +105,12 @@ const char * EMSdevice::uom_to_string(uint8_t uom) {
|
||||
}
|
||||
|
||||
std::string EMSdevice::brand_to_char() {
|
||||
return std::string{brand_to_cstr()};
|
||||
}
|
||||
|
||||
const char * EMSdevice::brand_to_cstr() const {
|
||||
if (!custom_brand().empty()) {
|
||||
return custom_brand();
|
||||
return custom_brand().c_str();
|
||||
}
|
||||
switch (brand_) {
|
||||
case EMSdevice::Brand::BOSCH:
|
||||
@@ -2160,7 +2164,7 @@ void EMSdevice::mqtt_ha_entity_config_create() {
|
||||
if (!dv.has_state(DeviceValueState::DV_HA_CONFIG_CREATED) && dv.has_state(DeviceValueState::DV_ACTIVE)
|
||||
&& !dv.has_state(DeviceValueState::DV_API_MQTT_EXCLUDE)) {
|
||||
// create_device_config is only done once for the EMS device. It can added to any entity, so we take the first
|
||||
if (Mqtt::publish_ha_sensor_config_dv(dv, name().c_str(), brand_to_char().c_str(), to_string_version().c_str(), false, create_device_config)) {
|
||||
if (Mqtt::publish_ha_sensor_config_dv(dv, name().c_str(), brand_to_cstr(), to_string_version().c_str(), false, create_device_config)) {
|
||||
dv.add_state(DeviceValueState::DV_HA_CONFIG_CREATED);
|
||||
create_device_config = false; // only create the main config once
|
||||
count++;
|
||||
@@ -2224,7 +2228,7 @@ bool EMSdevice::has_telegram_id(uint16_t id) const {
|
||||
}
|
||||
|
||||
// return the name of the telegram type
|
||||
const char * EMSdevice::telegram_type_name(std::shared_ptr<const Telegram> telegram) {
|
||||
const char * EMSdevice::telegram_type_name(const std::shared_ptr<const Telegram> & telegram) {
|
||||
// see if it's one of the common ones, like Version
|
||||
if (telegram->type_id == EMS_TYPE_VERSION) {
|
||||
return "Version";
|
||||
@@ -2243,12 +2247,12 @@ const char * EMSdevice::telegram_type_name(std::shared_ptr<const Telegram> teleg
|
||||
|
||||
// take a telegram_type_id and call the matching handler
|
||||
// return true if match found
|
||||
bool EMSdevice::handle_telegram(std::shared_ptr<const Telegram> telegram) {
|
||||
bool EMSdevice::handle_telegram(const std::shared_ptr<const Telegram> & telegram) {
|
||||
for (auto & tf : telegram_functions_) {
|
||||
if (tf.telegram_type_id_ == telegram->type_id) {
|
||||
// for telegram destination only read telegram
|
||||
if (telegram->dest == device_id_ && telegram->message_length > 0) {
|
||||
tf.process_function_(telegram);
|
||||
tf.process_function_(this, telegram);
|
||||
return true;
|
||||
}
|
||||
// if the data block is empty and we have not received data before, assume that this telegram
|
||||
@@ -2266,7 +2270,7 @@ bool EMSdevice::handle_telegram(std::shared_ptr<const Telegram> telegram) {
|
||||
}
|
||||
if (telegram->message_length > 0) {
|
||||
tf.received_ = true;
|
||||
tf.process_function_(telegram);
|
||||
tf.process_function_(this, telegram);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "emsdevicevalue.h"
|
||||
|
||||
#include <esp32-psram.h>
|
||||
#include <initializer_list>
|
||||
#include <map>
|
||||
|
||||
namespace emsesp {
|
||||
@@ -34,7 +35,15 @@ class EMSdevice {
|
||||
public:
|
||||
virtual ~EMSdevice() = default; // destructor of base class must always be virtual because it's a polymorphic class
|
||||
|
||||
using process_function_p = std::function<void(std::shared_ptr<const Telegram>)>;
|
||||
// Raw function pointer + EMSdevice* context, instead of std::function.
|
||||
// Each std::function<void(...)> typically heap-allocates its capture (a few
|
||||
// bytes for the [&] closure) on libstdc++ ESP32 builds. With hundreds of
|
||||
// registered telegram handlers across devices, that's tens of KB of
|
||||
// long-lived heap. The MAKE_PF_CB macro produces a non-capturing trampoline
|
||||
// that decays to this raw pointer (zero heap, zero indirection beyond the
|
||||
// call itself). The first parameter receives `this` of the dispatching
|
||||
// EMSdevice instance; the trampoline downcasts to the actual derived type.
|
||||
using process_function_p = void (*)(EMSdevice * dev, const std::shared_ptr<const Telegram> & t);
|
||||
|
||||
// device_type defines which derived class to use, e.g. BOILER, THERMOSTAT etc..
|
||||
EMSdevice(uint8_t device_type, uint8_t device_id, uint8_t product_id, const char * version, const char * default_name, uint8_t flags, uint8_t brand)
|
||||
@@ -64,6 +73,7 @@ class EMSdevice {
|
||||
bool has_tags(const int8_t tag) const;
|
||||
bool has_cmd(const char * cmd, const int8_t id) const;
|
||||
std::string brand_to_char();
|
||||
const char * brand_to_cstr() const;
|
||||
std::string to_string();
|
||||
std::string to_string_short();
|
||||
std::string to_string_version();
|
||||
@@ -125,7 +135,7 @@ class EMSdevice {
|
||||
custom_name_ = custom_name;
|
||||
}
|
||||
|
||||
std::string custom_name() const {
|
||||
const std::string & custom_name() const {
|
||||
return custom_name_;
|
||||
}
|
||||
|
||||
@@ -134,7 +144,7 @@ class EMSdevice {
|
||||
custom_brand_ = custom_brand;
|
||||
}
|
||||
|
||||
std::string custom_brand() const {
|
||||
const std::string & custom_brand() const {
|
||||
return custom_brand_;
|
||||
}
|
||||
// set device model
|
||||
@@ -142,7 +152,7 @@ class EMSdevice {
|
||||
model_ = model;
|
||||
}
|
||||
|
||||
std::string model() const {
|
||||
const std::string & model() const {
|
||||
return model_;
|
||||
}
|
||||
|
||||
@@ -207,29 +217,36 @@ class EMSdevice {
|
||||
}
|
||||
}
|
||||
|
||||
void has_enumupdate(std::shared_ptr<const Telegram> telegram, uint8_t & value, const uint8_t index, int8_t s = 0) {
|
||||
void has_enumupdate(const std::shared_ptr<const Telegram> & telegram, uint8_t & value, const uint8_t index, int8_t s = 0) {
|
||||
if (telegram->read_enumvalue(value, index, s)) {
|
||||
has_update_ = true;
|
||||
publish_value((void *)&value);
|
||||
}
|
||||
}
|
||||
|
||||
void has_enumupdate(std::shared_ptr<const Telegram> telegram, uint8_t & value, const uint8_t index, const std::vector<uint8_t> & maskIn) {
|
||||
uint8_t val = value < maskIn.size() ? maskIn[value] : EMS_VALUE_UINT8_NOTSET;
|
||||
// maskIn is taken as a std::initializer_list so brace-list call sites
|
||||
// like has_enumupdate(t, v, idx, {0,5,1,2,4}) avoid the per-call
|
||||
// heap allocation of a temporary std::vector<uint8_t>. The backing
|
||||
// array of an initializer_list of integral constants is placed in
|
||||
// static storage or on the stack — never on the heap.
|
||||
void has_enumupdate(const std::shared_ptr<const Telegram> & telegram, uint8_t & value, const uint8_t index, std::initializer_list<uint8_t> maskIn) {
|
||||
uint8_t val = value < maskIn.size() ? *(maskIn.begin() + value) : EMS_VALUE_UINT8_NOTSET;
|
||||
if (telegram->read_value(val, index)) {
|
||||
for (uint8_t i = 0; i < maskIn.size(); i++) {
|
||||
if (val == maskIn[i]) {
|
||||
uint8_t i = 0;
|
||||
for (auto m : maskIn) {
|
||||
if (val == m) {
|
||||
value = i;
|
||||
has_update_ = true;
|
||||
publish_value((void *)&value);
|
||||
return;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Value>
|
||||
void has_update(std::shared_ptr<const Telegram> telegram, Value & value, const uint8_t index, uint8_t s = 0) {
|
||||
void has_update(const std::shared_ptr<const Telegram> & telegram, Value & value, const uint8_t index, uint8_t s = 0) {
|
||||
if (telegram->read_value(value, index, s)) {
|
||||
has_update_ = true;
|
||||
publish_value((void *)&value);
|
||||
@@ -237,7 +254,7 @@ class EMSdevice {
|
||||
}
|
||||
|
||||
template <typename BitValue>
|
||||
void has_bitupdate(std::shared_ptr<const Telegram> telegram, BitValue & value, const uint8_t index, uint8_t b) {
|
||||
void has_bitupdate(const std::shared_ptr<const Telegram> & telegram, BitValue & value, const uint8_t index, uint8_t b) {
|
||||
if (telegram->read_bitvalue(value, index, b)) {
|
||||
has_update_ = true;
|
||||
publish_value((void *)&value);
|
||||
@@ -260,7 +277,7 @@ class EMSdevice {
|
||||
void getCustomizationEntities(std::vector<std::string> & entity_ids);
|
||||
|
||||
void register_telegram_type(const uint16_t telegram_type_id, const char * telegram_type_name, bool fetch, const process_function_p cb, uint8_t length = 0);
|
||||
bool handle_telegram(std::shared_ptr<const Telegram> telegram);
|
||||
bool handle_telegram(const std::shared_ptr<const Telegram> & telegram);
|
||||
|
||||
std::string get_value_uom(const std::string & shortname) const;
|
||||
bool get_value_info(JsonObject root, const char * cmd, const int8_t id);
|
||||
@@ -359,7 +376,7 @@ class EMSdevice {
|
||||
void publish_value(void * value_p) const;
|
||||
void publish_all_values();
|
||||
void mqtt_ha_entity_config_create();
|
||||
const char * telegram_type_name(std::shared_ptr<const Telegram> telegram);
|
||||
const char * telegram_type_name(const std::shared_ptr<const Telegram> & telegram);
|
||||
void fetch_values();
|
||||
void toggle_fetch(uint16_t telegram_id, bool toggle);
|
||||
bool is_fetch(uint16_t telegram_id, uint8_t len = 0) const;
|
||||
@@ -518,13 +535,17 @@ class EMSdevice {
|
||||
uint8_t count_entities_fav();
|
||||
bool has_entities() const;
|
||||
|
||||
// void reserve_device_values(uint8_t elements) {
|
||||
// devicevalues_.reserve(elements);
|
||||
// }
|
||||
// Pre-allocate vector capacity to avoid realloc storms during device
|
||||
// construction. Realloc here is especially expensive because each entry
|
||||
// contains a std::function (heap-allocated functor) and DeviceValue
|
||||
// (with std::string member), so growing copies a lot.
|
||||
void reserve_device_values(uint16_t elements) {
|
||||
devicevalues_.reserve(elements);
|
||||
}
|
||||
|
||||
// void reserve_telegram_functions(uint8_t elements) {
|
||||
// telegram_functions_.reserve(elements);
|
||||
// }
|
||||
void reserve_telegram_functions(uint8_t elements) {
|
||||
telegram_functions_.reserve(elements);
|
||||
}
|
||||
|
||||
#if defined(EMSESP_STANDALONE)
|
||||
struct TelegramFunctionDump {
|
||||
|
||||
@@ -38,21 +38,23 @@ DeviceValue::DeviceValue(uint8_t device_type,
|
||||
int16_t min,
|
||||
uint32_t max,
|
||||
uint8_t state)
|
||||
: device_type(device_type)
|
||||
, tag(tag)
|
||||
, value_p(value_p)
|
||||
, type(type)
|
||||
, options(options)
|
||||
, options_single(options_single)
|
||||
, numeric_operator(numeric_operator)
|
||||
// Initializer list ordered to match the reordered field declarations in
|
||||
// emsdevicevalue.h (pointers first, then 1-byte block, then 2/4-byte, then std::string)
|
||||
: value_p(value_p)
|
||||
, short_name(short_name)
|
||||
, fullname(fullname)
|
||||
, custom_fullname(custom_fullname)
|
||||
, options(options)
|
||||
, options_single(options_single)
|
||||
, device_type(device_type)
|
||||
, tag(tag)
|
||||
, type(type)
|
||||
, state(state)
|
||||
, numeric_operator(numeric_operator)
|
||||
, uom(uom)
|
||||
, has_cmd(has_cmd)
|
||||
, min(min)
|
||||
, max(max)
|
||||
, state(state) {
|
||||
, custom_fullname(custom_fullname) {
|
||||
// calculate #options in options list
|
||||
if (options_single) {
|
||||
options_size = 1;
|
||||
|
||||
@@ -167,23 +167,27 @@ class DeviceValue {
|
||||
DV_NUMOP_MUL50 = -50
|
||||
};
|
||||
|
||||
uint8_t device_type; // EMSdevice::DeviceType
|
||||
int8_t tag; // DeviceValueTAG::*
|
||||
// Layout chosen for compact packing AND cache locality on 32-bit ESP32.
|
||||
// pointers — 5 × 4 bytes, all naturally aligned
|
||||
void * value_p; // pointer to variable of any type
|
||||
uint8_t type; // DeviceValueType::*
|
||||
const char * const short_name; // used in MQTT and API
|
||||
const char * const * fullname; // used in Web and Console, is translated
|
||||
const char * const ** options; // options as a flash char array
|
||||
const char * const * options_single; // options are not translated
|
||||
int8_t numeric_operator;
|
||||
const char * const short_name; // used in MQTT and API
|
||||
const char * const * fullname; // used in Web and Console, is translated
|
||||
std::string custom_fullname; // optional, from customization
|
||||
uint8_t uom; // DeviceValueUOM::*
|
||||
bool has_cmd; // true if there is a Console/MQTT command which matches the short_name
|
||||
int16_t min; // min range
|
||||
uint32_t max; // max range
|
||||
uint8_t state; // DeviceValueState::*
|
||||
|
||||
uint8_t options_size; // number of options in the char array, calculated at class initialization
|
||||
// single-byte fields packed together — hot fields, share cache line 0 with the pointers above
|
||||
uint8_t device_type; // EMSdevice::DeviceType
|
||||
int8_t tag; // DeviceValueTAG::*
|
||||
uint8_t type; // DeviceValueType::*
|
||||
uint8_t state; // DeviceValueState::*
|
||||
int8_t numeric_operator; // DeviceValueNumOp::*
|
||||
uint8_t uom; // DeviceValueUOM::*
|
||||
bool has_cmd; // true if there is a Console/MQTT command which matches the short_name
|
||||
uint8_t options_size; // number of options in the char array, calculated at class initialization
|
||||
// wider numeric range fields
|
||||
int16_t min; // min range
|
||||
uint32_t max; // max range
|
||||
// largest member last (cold path: only read during customization save/load and web display)
|
||||
std::string custom_fullname; // optional, from customization
|
||||
|
||||
DeviceValue(uint8_t device_type, // EMSdevice::DeviceType
|
||||
int8_t tag, // DeviceValueTAG::*
|
||||
|
||||
@@ -436,7 +436,7 @@ void EMSESP::show_device_values(uuid::console::Shell & shell) {
|
||||
// print header, with device type translated
|
||||
shell.printfln("%s: %s (%d)", emsdevice->device_type_2_device_name_translated(), emsdevice->to_string().c_str(), emsdevice->count_entities());
|
||||
|
||||
JsonDocument doc;
|
||||
JsonDocument doc(PSRAM_DOC);
|
||||
JsonObject json = doc.to<JsonObject>();
|
||||
|
||||
emsdevice->generate_values(json, DeviceValueTAG::TAG_NONE, true, EMSdevice::OUTPUT_TARGET::CONSOLE);
|
||||
@@ -460,7 +460,7 @@ void EMSESP::show_device_values(uuid::console::Shell & shell) {
|
||||
// show any custom entities
|
||||
if (webCustomEntityService.count_entities() > 0) {
|
||||
shell.printfln("Custom Entities:");
|
||||
JsonDocument custom_doc; // use max size
|
||||
JsonDocument custom_doc(PSRAM_DOC); // use max size
|
||||
JsonObject custom_output = custom_doc.to<JsonObject>();
|
||||
webCustomEntityService.show_values(custom_output);
|
||||
for (JsonPair p : custom_output) {
|
||||
@@ -625,7 +625,7 @@ void EMSESP::reset_mqtt_ha() {
|
||||
// this will also create the HA /config topic for each device value
|
||||
// generate_values_json is called to build the device value (dv) object array
|
||||
void EMSESP::publish_device_values(uint8_t device_type) {
|
||||
JsonDocument doc;
|
||||
JsonDocument doc(PSRAM_DOC);
|
||||
JsonObject json = doc.to<JsonObject>();
|
||||
bool need_publish = false;
|
||||
bool nested = (Mqtt::is_nested());
|
||||
@@ -701,7 +701,7 @@ void EMSESP::publish_sensor_values(const bool time, const bool force) {
|
||||
}
|
||||
|
||||
// MQTT publish a telegram as raw data to the topic 'response'
|
||||
void EMSESP::publish_response(std::shared_ptr<const Telegram> telegram) {
|
||||
void EMSESP::publish_response(const std::shared_ptr<const Telegram> & telegram) {
|
||||
static char * buffer = nullptr;
|
||||
static uint8_t offset = 0;
|
||||
static uint16_t type = 0;
|
||||
@@ -815,7 +815,7 @@ std::string EMSESP::device_tostring(const uint8_t device_id) {
|
||||
|
||||
// create a pretty print telegram as a text string
|
||||
// e.g. Boiler(0x08) -> Me(0x0B), Version(0x02), data: 7B 06 01 00 00 00 00 00 00 04 (offset 1)
|
||||
std::string EMSESP::pretty_telegram(std::shared_ptr<const Telegram> telegram) {
|
||||
std::string EMSESP::pretty_telegram(const std::shared_ptr<const Telegram> & telegram) {
|
||||
uint8_t src = telegram->src & 0x7F;
|
||||
uint8_t dest = telegram->dest & 0x7F;
|
||||
uint8_t offset = telegram->offset;
|
||||
@@ -975,7 +975,7 @@ std::string EMSESP::pretty_telegram(std::shared_ptr<const Telegram> telegram) {
|
||||
* e.g. in example above 1st byte = x0B = b1011 so we have deviceIDs 0x08, 0x09, 0x011
|
||||
* and 2nd byte = x80 = b1000 b0000 = deviceID 0x17
|
||||
*/
|
||||
void EMSESP::process_UBADevices(std::shared_ptr<const Telegram> telegram) {
|
||||
void EMSESP::process_UBADevices(const std::shared_ptr<const Telegram> & telegram) {
|
||||
// exit it length is incorrect (must be 13 or 15 bytes long)
|
||||
if (telegram->message_length > 15) {
|
||||
return;
|
||||
@@ -1001,7 +1001,7 @@ void EMSESP::process_UBADevices(std::shared_ptr<const Telegram> telegram) {
|
||||
}
|
||||
|
||||
// read deviceName from telegram 0x01 offset 27 and set it to custom name
|
||||
void EMSESP::process_deviceName(std::shared_ptr<const Telegram> telegram) {
|
||||
void EMSESP::process_deviceName(const std::shared_ptr<const Telegram> & telegram) {
|
||||
// exit if only part of name fields
|
||||
if (telegram->offset > 27 || (telegram->offset + telegram->message_length) < 29) {
|
||||
return;
|
||||
@@ -1029,7 +1029,7 @@ void EMSESP::process_deviceName(std::shared_ptr<const Telegram> telegram) {
|
||||
|
||||
// process the Version telegram (type 0x02), which is a common type
|
||||
// e.g. 09 0B 02 00 PP V1 V2
|
||||
void EMSESP::process_version(std::shared_ptr<const Telegram> telegram) {
|
||||
void EMSESP::process_version(const std::shared_ptr<const Telegram> & telegram) {
|
||||
// check for valid telegram, just in case
|
||||
if (telegram->offset != 0) {
|
||||
return;
|
||||
@@ -1087,7 +1087,7 @@ void EMSESP::process_version(std::shared_ptr<const Telegram> telegram) {
|
||||
// but only process if the telegram is sent to us or it's a broadcast (dest=0x00=all)
|
||||
// We also check for common telegram types, like the Version(0x02)
|
||||
// returns false if there are none found
|
||||
bool EMSESP::process_telegram(std::shared_ptr<const Telegram> telegram) {
|
||||
bool EMSESP::process_telegram(const std::shared_ptr<const Telegram> & telegram) {
|
||||
// if watching or reading...
|
||||
if ((telegram->type_id == read_id_ || telegram->type_id == response_id_) && (telegram->dest == EMSbus::ems_bus_id())) {
|
||||
if (telegram->type_id == response_id_) {
|
||||
@@ -1868,7 +1868,7 @@ void EMSESP::loop() {
|
||||
// start an upload from a URL, assuming the URL exists and set from a previous pass
|
||||
// Note this next call is synchronous and blocking.
|
||||
if (!system_.uploadFirmwareURL()) {
|
||||
// upload failed, send a "reset" to return back to normal
|
||||
// upload failed, send a "reset" to reset the OTA URL
|
||||
Shell::loop_all(); // flush log buffers so latest error message are shown in console
|
||||
system_.uploadFirmwareURL("reset");
|
||||
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
|
||||
|
||||
@@ -56,6 +56,8 @@
|
||||
#include "../web/WebCustomEntityService.h"
|
||||
#include "../web/WebModulesService.h"
|
||||
|
||||
#include "psram_json_allocator.h"
|
||||
#include "psram_async_json_response.h"
|
||||
#include "emsdevicevalue.h"
|
||||
#include "emsdevice.h"
|
||||
#include "emsfactory.h"
|
||||
@@ -82,7 +84,19 @@ class Module {}; // forward declaration
|
||||
#define WATCH_ID_NONE 0 // no watch id set
|
||||
|
||||
// helpers for callback functions
|
||||
#define MAKE_PF_CB(__f) [&](std::shared_ptr<const Telegram> t) { __f(t); } // for Process Function callbacks to EMSDevice::process_function_p
|
||||
//
|
||||
// MAKE_PF_CB(member) produces a non-capturing trampoline that decays to a
|
||||
// plain function pointer (EMSdevice::process_function_p). The outer IILE
|
||||
// (immediately-invoked lambda expression) captures `this` purely to deduce
|
||||
// the derived-class type via decltype; the inner lambda is non-capturing and
|
||||
// therefore convertible to a function pointer via the unary `+` operator.
|
||||
// Result: zero heap (no std::function control block) and direct dispatch.
|
||||
#define MAKE_PF_CB(__f) \
|
||||
([this]() { \
|
||||
using SelfT = std::remove_pointer_t<decltype(this)>; \
|
||||
return +[](emsesp::EMSdevice * dev, const std::shared_ptr<const Telegram> & t) { static_cast<SelfT *>(dev)->__f(t); }; \
|
||||
}())
|
||||
|
||||
#define MAKE_CF_CB(__f) [&](const char * value, const int8_t id) { return __f(value, id); } // for Command Function callbacks Command::cmd_function_p
|
||||
|
||||
namespace emsesp {
|
||||
@@ -121,8 +135,8 @@ class EMSESP {
|
||||
static void uart_telegram(const std::vector<uint8_t> & rx_data);
|
||||
#endif
|
||||
|
||||
static bool process_telegram(std::shared_ptr<const Telegram> telegram);
|
||||
static std::string pretty_telegram(std::shared_ptr<const Telegram> telegram);
|
||||
static bool process_telegram(const std::shared_ptr<const Telegram> & telegram);
|
||||
static std::string pretty_telegram(const std::shared_ptr<const Telegram> & telegram);
|
||||
|
||||
static void send_read_request(const uint16_t type_id, const uint8_t dest, const uint8_t offset = 0, const uint8_t length = 0, const bool front = false);
|
||||
static void send_write_request(const uint16_t type_id,
|
||||
@@ -251,10 +265,10 @@ class EMSESP {
|
||||
|
||||
private:
|
||||
static std::string device_tostring(const uint8_t device_id);
|
||||
static void process_UBADevices(std::shared_ptr<const Telegram> telegram);
|
||||
static void process_deviceName(std::shared_ptr<const Telegram> telegram);
|
||||
static void process_version(std::shared_ptr<const Telegram> telegram);
|
||||
static void publish_response(std::shared_ptr<const Telegram> telegram);
|
||||
static void process_UBADevices(const std::shared_ptr<const Telegram> & telegram);
|
||||
static void process_deviceName(const std::shared_ptr<const Telegram> & telegram);
|
||||
static void process_version(const std::shared_ptr<const Telegram> & telegram);
|
||||
static void publish_response(const std::shared_ptr<const Telegram> & telegram);
|
||||
static void publish_all_loop();
|
||||
|
||||
void shell_prompt();
|
||||
|
||||
@@ -23,17 +23,21 @@
|
||||
|
||||
using uuid::log::Level;
|
||||
|
||||
// Log macros gate on logger_.enabled(level) so that expensive argument
|
||||
// expressions (e.g. pretty_telegram(...).c_str()) are not evaluated when
|
||||
// the level is filtered out. Without this, every LOG_TRACE on the RX path
|
||||
// allocates a std::string even when no handler is interested.
|
||||
#if defined(EMSESP_DEBUG)
|
||||
#define LOG_DEBUG(...) logger_.debug(__VA_ARGS__)
|
||||
#define LOG_DEBUG(...) (logger_.enabled(uuid::log::Level::DEBUG) ? logger_.debug(__VA_ARGS__) : (void)0)
|
||||
#else
|
||||
#define LOG_DEBUG(...)
|
||||
#define LOG_DEBUG(...) ((void)0)
|
||||
#endif
|
||||
|
||||
#define LOG_INFO(...) logger_.info(__VA_ARGS__)
|
||||
#define LOG_TRACE(...) logger_.trace(__VA_ARGS__)
|
||||
#define LOG_NOTICE(...) logger_.notice(__VA_ARGS__)
|
||||
#define LOG_WARNING(...) logger_.warning(__VA_ARGS__)
|
||||
#define LOG_ERROR(...) logger_.err(__VA_ARGS__)
|
||||
#define LOG_INFO(...) (logger_.enabled(uuid::log::Level::INFO) ? logger_.info(__VA_ARGS__) : (void)0)
|
||||
#define LOG_TRACE(...) (logger_.enabled(uuid::log::Level::TRACE) ? logger_.trace(__VA_ARGS__) : (void)0)
|
||||
#define LOG_NOTICE(...) (logger_.enabled(uuid::log::Level::NOTICE) ? logger_.notice(__VA_ARGS__) : (void)0)
|
||||
#define LOG_WARNING(...) (logger_.enabled(uuid::log::Level::WARNING) ? logger_.warning(__VA_ARGS__) : (void)0)
|
||||
#define LOG_ERROR(...) (logger_.enabled(uuid::log::Level::ERR) ? logger_.err(__VA_ARGS__) : (void)0)
|
||||
|
||||
// flash strings
|
||||
using uuid::string_vector;
|
||||
|
||||
@@ -293,7 +293,7 @@ MAKE_WORD_TRANSLATION(curve, "heatingcurve", "Heizkurve", "stookkromme", "värme
|
||||
MAKE_WORD_TRANSLATION(radiator, "radiator", "Heizkörper", "radiator", "Radiator", "grzejniki", "radiator", "radiateur", "radyatör", "radiatore", "radiátor", "radiátor")
|
||||
MAKE_WORD_TRANSLATION(convector, "convector", "Konvektor", "convector", "Konvektor", "konwektory", "konvektor", "convecteur", "convector", "convettore", "konvektor", "konvektor")
|
||||
MAKE_WORD_TRANSLATION(floor, "floor", "Fussboden", "vloer", "Golv", "podłoga", "gulv", "sol", "yer", "pavimento", "podlaha", "podlaha")
|
||||
MAKE_WORD_TRANSLATION(roomflow, "roomflow", "Raum Fluß", "kamer doorstroming", "Rumsflöde", "przepływ w pomieszczeniu", "romstrøm", "flux de la pièce", "oda akışı", "flusso della stanza", "prúdenie miestnosti", "průtok mistnosti")
|
||||
MAKE_WORD_TRANSLATION(roomflow, "roomflow", "Raum Fluss", "kamer doorstroming", "Rumsflöde", "przepływ w pomieszczeniu", "romstrøm", "flux de la pièce", "oda akışı", "flusso della stanza", "prúdenie miestnosti", "průtok mistnosti")
|
||||
MAKE_WORD_TRANSLATION(roomload, "roomload", "Raum Bedarf", "kamer behoefte", "Rumsbehov", "zapotrzebowanie pomieszczenia", "rombelastning", "charge de la pièce", "oda yükü", "carico della stanza", "izbová zaťaž", "zatížení místnosti")
|
||||
MAKE_WORD_TRANSLATION(summer, "summer", "Sommer", "zomer", "Sommar", "lato", "sommer", "été", "yaz", "estate", "leto", "léto")
|
||||
MAKE_WORD_TRANSLATION(winter, "winter", "Winter", "winter", "Vinter", "zima", "vinter", "hiver", "kış", "inverno", "zima", "zima")
|
||||
@@ -827,8 +827,8 @@ MAKE_TRANSLATION(vacations7, "vacations7", "vacation dates 7", "Urlaubstage 7",
|
||||
MAKE_TRANSLATION(vacations8, "vacations8", "vacation dates 8", "Urlaubstage 8", "Vakantiedagen 8", "Semesterdatum 8", "urlop 8", "feriedager 8", "dates vacances 8", "izin günleri 8", "date vacanze 8", "termíny dovolenky 8", "data prázdnin 8")
|
||||
MAKE_TRANSLATION(absent, "absent", "absent", "Abwesend", "", "Frånvarande", "", "", "", "", "", "chýbajúci", "chybějící")
|
||||
MAKE_TRANSLATION(redthreshold, "redthreshold", "reduction threshold", "Absenkschwelle", "", "Tröskel för sänkning", "", "", "", "", "", "zníženie tresholdu", "práh snížení")
|
||||
MAKE_TRANSLATION(solarinfl, "solarinfl", "solar influence", "Solareinfluß", "", "", "", "", "", "", "", "slnečný vplyv", "sluneční vliv")
|
||||
MAKE_TRANSLATION(currsolarinfl, "currsolarinfl", "current solar influence", "akt. Solareinfluß", "", "", "", "", "", "", "", "aktuálny slnečný vplyv", "aktuální sluneční vliv")
|
||||
MAKE_TRANSLATION(solarinfl, "solarinfl", "solar influence", "Solareinfluss", "", "", "", "", "", "", "", "slnečný vplyv", "sluneční vliv")
|
||||
MAKE_TRANSLATION(currsolarinfl, "currsolarinfl", "current solar influence", "akt. Solareinfluss", "", "", "", "", "", "", "", "aktuálny slnečný vplyv", "aktuální sluneční vliv")
|
||||
MAKE_TRANSLATION(hpmode, "hpmode", "HP Mode", "WP-Modus", "Modus warmtepomp", "Värmepumpsläge", "tryb pracy pompy ciepła", "", "", "yüksek güç modu", "Modalità Termopompa", "Režim TČ", "režim tepelného čerpadla")
|
||||
MAKE_TRANSLATION(dewoffset, "dewoffset", "dew point offset", "Taupunktdifferenz", "Offset dauwpunt", "Daggpunktsförskjutning", "przesunięcie punktu rosy", "", "", "çiğ noktası göreli", "differenza del punto di rugiada", "posun rosného bodu", "offset rosného bodu")
|
||||
MAKE_TRANSLATION(roomtempdiff, "roomtempdiff", "room temp difference", "Raumtemperaturdifferenz", "Verschiltemperatuur kamertemp", "Rumstemperaturskillnad", "różnica temp. pomieszczenia", "", "", "oda sıcaklığı farkı", "differenza temperatura ambiente", "rozdiel izbovej teploty", "rozdíl teploty místnosti")
|
||||
|
||||
@@ -519,7 +519,7 @@ void Mqtt::on_connect() {
|
||||
// e.g. homeassistant/sensor/ems-esp/status/config
|
||||
// all the values from the heartbeat payload will be added as attributes to the entity state
|
||||
void Mqtt::ha_status() {
|
||||
JsonDocument doc;
|
||||
JsonDocument doc(PSRAM_DOC);
|
||||
|
||||
char uniq[70];
|
||||
if (Mqtt::entity_format() == entityFormat::MULTI_SHORT) {
|
||||
@@ -981,7 +981,7 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev
|
||||
}
|
||||
|
||||
// build the full topic's payload
|
||||
JsonDocument doc;
|
||||
JsonDocument doc(PSRAM_DOC);
|
||||
doc["~"] = Mqtt::base();
|
||||
doc["uniq_id"] = uniq_id;
|
||||
|
||||
@@ -1406,7 +1406,7 @@ bool Mqtt::publish_ha_climate_config(const DeviceValue & dv, const bool has_room
|
||||
snprintf(temp_cmd_s, sizeof(temp_cmd_s), "~/%s/%s%d/seltemp", devicename, tagname, hc_num);
|
||||
snprintf(mode_cmd_s, sizeof(mode_cmd_s), "~/%s/%s%d/mode", devicename, tagname, hc_num);
|
||||
|
||||
JsonDocument doc;
|
||||
JsonDocument doc(PSRAM_DOC);
|
||||
|
||||
doc["~"] = Mqtt::base();
|
||||
doc["uniq_id"] = uniq_id_s;
|
||||
|
||||
@@ -146,11 +146,11 @@ class Mqtt {
|
||||
mqtt_enabled_ = mqtt_enabled;
|
||||
}
|
||||
|
||||
static std::string base() {
|
||||
static const std::string & base() {
|
||||
return mqtt_base_;
|
||||
}
|
||||
|
||||
static std::string basename() {
|
||||
static const std::string & basename() {
|
||||
return mqtt_basename_;
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ class Mqtt {
|
||||
ha_enabled_ = ha_enabled;
|
||||
}
|
||||
|
||||
static std::string get_response() {
|
||||
static const std::string & get_response() {
|
||||
return lastresponse_;
|
||||
}
|
||||
|
||||
|
||||
@@ -501,8 +501,6 @@ void Network::startWIFI() {
|
||||
|
||||
wifi_connect_pending_ = true;
|
||||
|
||||
LOG_DEBUG("WiFi connection with %s and %s", ssid_.c_str(), password_.c_str());
|
||||
|
||||
// attempt to connect to the wifi network
|
||||
// the event handlers handle error handling and retries
|
||||
uint8_t bssid[6];
|
||||
|
||||
143
src/core/psram_async_json_response.h
Normal file
143
src/core/psram_async_json_response.h
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* EMS-ESP - https://github.com/emsesp/EMS-ESP
|
||||
* Copyright 2020-2025 emsesp.org
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef EMSESP_PSRAM_ASYNC_JSON_RESPONSE_H
|
||||
#define EMSESP_PSRAM_ASYNC_JSON_RESPONSE_H
|
||||
|
||||
#include "psram_json_allocator.h"
|
||||
|
||||
#ifndef EMSESP_STANDALONE
|
||||
#include <AsyncJson.h>
|
||||
#include <ChunkPrint.h>
|
||||
#else
|
||||
#include <AsyncJson.h>
|
||||
#endif
|
||||
|
||||
namespace emsesp {
|
||||
|
||||
// AsyncJsonResponse subclass whose JsonDocument lives in PSRAM instead of
|
||||
// internal SRAM.
|
||||
//
|
||||
// Why: every web API response goes through AsyncJsonResponse. The library's
|
||||
// base class declares `JsonDocument _jsonBuffer;` with the *default*
|
||||
// allocator, which on ESP32 means malloc() → internal heap. For large
|
||||
// payloads (Dashboard, /rest/coreData, /rest/sensorData, full settings,
|
||||
// customizations, etc.) this transiently consumes many KB of the same
|
||||
// internal heap that LwIP / AsyncTCP / mbedTLS also need. Each concurrent
|
||||
// browser tab compounds the cost.
|
||||
//
|
||||
// We can't change the base class's _jsonBuffer allocator (the upstream
|
||||
// constructor doesn't take one), but we can route around it: keep our own
|
||||
// PSRAM-backed document, override the virtual setLength()/_fillBuffer() so
|
||||
// the framework serialises *our* document, and name-hide getRoot() so
|
||||
// callers populate *our* document. The base's _jsonBuffer stays empty
|
||||
// (just one root slot, <~32 bytes).
|
||||
//
|
||||
// Callers must use the derived type (or `auto`) when calling getRoot(),
|
||||
// because getRoot() is non-virtual in the base. `request->send(response)`
|
||||
// works as-is because setLength()/_fillBuffer() ARE virtual in the
|
||||
// AsyncAbstractResponse grandparent.
|
||||
//
|
||||
// On standalone the lib_standalone AsyncJsonResponse stub never actually
|
||||
// serves responses, so this whole class still compiles and behaves
|
||||
// identically (allocator falls back to malloc anyway).
|
||||
class PsramAsyncJsonResponse : public ::AsyncJsonResponse {
|
||||
public:
|
||||
explicit PsramAsyncJsonResponse(bool isArray = false)
|
||||
: ::AsyncJsonResponse(isArray)
|
||||
, psram_doc_(PsramJsonAllocator::instance()) {
|
||||
if (isArray) {
|
||||
psram_root_ = psram_doc_.add<JsonArray>();
|
||||
} else {
|
||||
psram_root_ = psram_doc_.add<JsonObject>();
|
||||
}
|
||||
}
|
||||
|
||||
// Hides AsyncJsonResponse::getRoot(). Must be called through a
|
||||
// derived-type pointer/reference (the framework's base pointer keeps
|
||||
// pointing at the empty base _jsonBuffer, which is intentional).
|
||||
JsonVariant getRoot() {
|
||||
return psram_root_;
|
||||
}
|
||||
|
||||
#ifndef EMSESP_STANDALONE
|
||||
size_t setLength() override {
|
||||
_contentLength = measureJson(psram_root_);
|
||||
if (_contentLength) {
|
||||
_isValid = true;
|
||||
}
|
||||
return _contentLength;
|
||||
}
|
||||
|
||||
size_t _fillBuffer(uint8_t * data, size_t len) override {
|
||||
ChunkPrint dest(data, _sentLength, len);
|
||||
serializeJson(psram_root_, dest);
|
||||
return dest.written();
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
JsonDocument psram_doc_;
|
||||
JsonVariant psram_root_;
|
||||
};
|
||||
|
||||
#if !defined(EMSESP_STANDALONE) && defined(ASYNC_MSG_PACK_SUPPORT) && ASYNC_MSG_PACK_SUPPORT == 1
|
||||
// MessagePack equivalent — same routing trick but serialises with MsgPack.
|
||||
class PsramAsyncMessagePackResponse : public ::AsyncMessagePackResponse {
|
||||
public:
|
||||
explicit PsramAsyncMessagePackResponse(bool isArray = false)
|
||||
: ::AsyncMessagePackResponse(isArray)
|
||||
, psram_doc_(PsramJsonAllocator::instance()) {
|
||||
if (isArray) {
|
||||
psram_root_ = psram_doc_.add<JsonArray>();
|
||||
} else {
|
||||
psram_root_ = psram_doc_.add<JsonObject>();
|
||||
}
|
||||
}
|
||||
|
||||
JsonVariant getRoot() {
|
||||
return psram_root_;
|
||||
}
|
||||
|
||||
size_t setLength() override {
|
||||
_contentLength = measureMsgPack(psram_root_);
|
||||
if (_contentLength) {
|
||||
_isValid = true;
|
||||
}
|
||||
return _contentLength;
|
||||
}
|
||||
|
||||
size_t _fillBuffer(uint8_t * data, size_t len) override {
|
||||
ChunkPrint dest(data, _sentLength, len);
|
||||
serializeMsgPack(psram_root_, dest);
|
||||
return dest.written();
|
||||
}
|
||||
|
||||
private:
|
||||
JsonDocument psram_doc_;
|
||||
JsonVariant psram_root_;
|
||||
};
|
||||
#else
|
||||
// Standalone or no msgpack support: alias to plain JSON response so the
|
||||
// codebase compiles unchanged.
|
||||
using PsramAsyncMessagePackResponse = PsramAsyncJsonResponse;
|
||||
#endif
|
||||
|
||||
} // namespace emsesp
|
||||
|
||||
#endif
|
||||
106
src/core/psram_json_allocator.h
Normal file
106
src/core/psram_json_allocator.h
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* EMS-ESP - https://github.com/emsesp/EMS-ESP
|
||||
* Copyright 2020-2025 emsesp.org
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef EMSESP_PSRAM_JSON_ALLOCATOR_H
|
||||
#define EMSESP_PSRAM_JSON_ALLOCATOR_H
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
#ifndef EMSESP_STANDALONE
|
||||
#include <esp_heap_caps.h>
|
||||
#endif
|
||||
|
||||
namespace emsesp {
|
||||
|
||||
// PSRAM-backed ArduinoJson allocator with internal-heap fallback.
|
||||
//
|
||||
// Rationale: by default ArduinoJson allocates with malloc(), which on the
|
||||
// ESP32 lands in internal DRAM. Large transient JsonDocuments (full MQTT
|
||||
// payload, HA discovery configs, Web API responses, settings load/save)
|
||||
// were eating multiple KB of the same internal heap that LwIP, mbedTLS and
|
||||
// AsyncTCP also need. Routing them through SPIRAM via heap_caps_malloc
|
||||
// keeps internal heap available for the network stack, at the cost of a
|
||||
// small latency penalty on PSRAM reads/writes (a few cycles per access,
|
||||
// negligible for JSON build-up which is dominated by string formatting).
|
||||
//
|
||||
// On the standalone (Linux) build, PSRAM doesn't exist; the allocator
|
||||
// silently falls through to plain malloc/free/realloc.
|
||||
//
|
||||
// Usage:
|
||||
// JsonDocument doc(emsesp::PsramJsonAllocator::instance());
|
||||
// or with the convenience macro:
|
||||
// JsonDocument doc(PSRAM_DOC);
|
||||
class PsramJsonAllocator : public ArduinoJson::Allocator {
|
||||
public:
|
||||
void * allocate(size_t size) override {
|
||||
#ifdef EMSESP_STANDALONE
|
||||
return malloc(size);
|
||||
#else
|
||||
// Try SPIRAM first; fall back to internal heap so we never fail
|
||||
// on boards without PSRAM or when PSRAM is full.
|
||||
void * p = heap_caps_malloc(size, MALLOC_CAP_SPIRAM);
|
||||
if (p == nullptr) {
|
||||
p = malloc(size);
|
||||
}
|
||||
return p;
|
||||
#endif
|
||||
}
|
||||
|
||||
void deallocate(void * ptr) override {
|
||||
#ifdef EMSESP_STANDALONE
|
||||
free(ptr);
|
||||
#else
|
||||
// heap_caps_free handles both PSRAM- and internal-heap pointers.
|
||||
heap_caps_free(ptr);
|
||||
#endif
|
||||
}
|
||||
|
||||
void * reallocate(void * ptr, size_t new_size) override {
|
||||
#ifdef EMSESP_STANDALONE
|
||||
return realloc(ptr, new_size);
|
||||
#else
|
||||
// Prefer keeping the block in PSRAM; heap_caps_realloc will move
|
||||
// the data if the original region can't be grown in-place.
|
||||
void * p = heap_caps_realloc(ptr, new_size, MALLOC_CAP_SPIRAM);
|
||||
if (p == nullptr) {
|
||||
p = realloc(ptr, new_size);
|
||||
}
|
||||
return p;
|
||||
#endif
|
||||
}
|
||||
|
||||
static ArduinoJson::Allocator * instance() {
|
||||
static PsramJsonAllocator inst;
|
||||
return &inst;
|
||||
}
|
||||
|
||||
private:
|
||||
PsramJsonAllocator() = default;
|
||||
~PsramJsonAllocator() = default;
|
||||
};
|
||||
|
||||
} // namespace emsesp
|
||||
|
||||
// Convenience shorthand. Use only for *large* or *transient* JsonDocuments
|
||||
// (MQTT publish payloads, HA discovery, full API responses, big settings
|
||||
// load/save). For small hot-path docs (single-command output, parse of a
|
||||
// short HTTP body), keep the default allocator: PSRAM has higher access
|
||||
// latency than internal SRAM, so tiny docs are faster on the regular heap.
|
||||
#define PSRAM_DOC emsesp::PsramJsonAllocator::instance()
|
||||
|
||||
#endif
|
||||
@@ -87,6 +87,7 @@ PButton System::myPButton_;
|
||||
bool System::test_set_all_active_ = false;
|
||||
uint32_t System::max_alloc_mem_;
|
||||
uint32_t System::heap_mem_;
|
||||
uint32_t System::min_free_mem_;
|
||||
|
||||
// GPIOs
|
||||
std::vector<uint8_t, AllocatorPSRAM<uint8_t>> System::valid_system_gpios_;
|
||||
@@ -173,7 +174,7 @@ bool System::command_sendmail(const char * value, const int8_t id) {
|
||||
delete basic_client;
|
||||
return false;
|
||||
}
|
||||
JsonDocument doc;
|
||||
JsonDocument doc(PSRAM_DOC);
|
||||
String body = value;
|
||||
if (body.length()) {
|
||||
auto error = deserializeJson(doc, (const char *)value);
|
||||
@@ -922,6 +923,11 @@ void System::heartbeat_json(JsonObject output) {
|
||||
#ifndef EMSESP_STANDALONE
|
||||
output["freemem"] = getHeapMem();
|
||||
output["max_alloc"] = getMaxAllocMem();
|
||||
// All-time low watermark of free internal heap (KB). Unlike freemem
|
||||
// (sampled now), this captures the worst transient dip since boot —
|
||||
// the actual metric to watch when measuring the effect of transient
|
||||
// allocation optimisations (e.g. JsonDocument on PSRAM).
|
||||
output["min_free"] = getMinFreeMem();
|
||||
#endif
|
||||
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2
|
||||
output["temperature"] = (int)temperature_;
|
||||
@@ -1076,13 +1082,33 @@ void System::show_system(uuid::console::Shell & shell) {
|
||||
#if CONFIG_IDF_TARGET_ESP32S3 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S2
|
||||
shell.printfln(" CPU temperature: %d °C", (int)temperature());
|
||||
#endif
|
||||
shell.printfln(" Free heap/Max alloc: %lu KB / %lu KB", getHeapMem(), getMaxAllocMem());
|
||||
// Free heap = current; Min free = all-time low watermark (lowest free
|
||||
// heap has ever been since boot). Min free is the actual metric that
|
||||
// reflects optimisations targeting transient peaks (publishes, /api/system,
|
||||
// TLS handshakes). If transient peaks are reduced, min_free goes up.
|
||||
shell.printfln(" Free heap/Max alloc/Min free: %lu KB / %lu KB / %lu KB", getHeapMem(), getMaxAllocMem(), getMinFreeMem());
|
||||
#ifndef EMSESP_STANDALONE
|
||||
// Largest contiguous free block of *internal* SRAM. Network stack
|
||||
// (LwIP/mbedTLS/AsyncTCP) and JSON output allocations need this to be
|
||||
// healthy — total free heap can look fine while this collapses due to
|
||||
// fragmentation. Compare before and after a big API call or MQTT publish.
|
||||
shell.printfln(" Internal heap free/largest block: %u KB / %u KB",
|
||||
heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) / 1024,
|
||||
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) / 1024);
|
||||
#endif
|
||||
shell.printfln(" App used/free: %lu KB / %lu KB", appUsed(), appFree());
|
||||
uint32_t FSused = LittleFS.usedBytes() / 1024;
|
||||
shell.printfln(" FS used/free: %lu KB / %lu KB", FSused, FStotal() - FSused);
|
||||
shell.printfln(" Flash size: %lu KB", ESP.getFlashChipSize() / 1024);
|
||||
if (PSram()) {
|
||||
#ifndef EMSESP_STANDALONE
|
||||
shell.printfln(" PSRAM size/free/largest block: %lu KB / %lu KB / %u KB",
|
||||
PSram(),
|
||||
ESP.getFreePsram() / 1024,
|
||||
heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM) / 1024);
|
||||
#else
|
||||
shell.printfln(" PSRAM size/free: %lu KB / %lu KB", PSram(), ESP.getFreePsram() / 1024);
|
||||
#endif
|
||||
} else {
|
||||
shell.printfln(" PSRAM: not available");
|
||||
}
|
||||
@@ -1227,7 +1253,7 @@ bool System::check_restore() {
|
||||
#ifndef EMSESP_STANDALONE
|
||||
File new_file = LittleFS.open(TEMP_FILENAME_PATH);
|
||||
if (new_file) {
|
||||
JsonDocument jsonDocument;
|
||||
JsonDocument jsonDocument(PSRAM_DOC);
|
||||
DeserializationError error = deserializeJson(jsonDocument, new_file);
|
||||
if (error == DeserializationError::Ok && jsonDocument.is<JsonObject>()) {
|
||||
JsonObject input = jsonDocument.as<JsonObject>();
|
||||
@@ -1591,7 +1617,7 @@ void System::exportSettings(const std::string & type, const char * filename, Jso
|
||||
File settingsFile = LittleFS.open(filename);
|
||||
if (settingsFile) {
|
||||
{
|
||||
JsonDocument jsonDocument;
|
||||
JsonDocument jsonDocument(PSRAM_DOC);
|
||||
DeserializationError error = deserializeJson(jsonDocument, settingsFile);
|
||||
settingsFile.close(); // close early, we no longer need the file
|
||||
if (error || !jsonDocument.is<JsonObject>()) {
|
||||
@@ -1650,7 +1676,7 @@ void System::exportSystemBackup(JsonObject output) {
|
||||
// special case for custom support
|
||||
File file = LittleFS.open(EMSESP_CUSTOMSUPPORT_FILE, "r");
|
||||
if (file) {
|
||||
JsonDocument jsonDocument;
|
||||
JsonDocument jsonDocument(PSRAM_DOC);
|
||||
DeserializationError error = deserializeJson(jsonDocument, file);
|
||||
file.close(); // close early, we no longer need the file
|
||||
if (!error && jsonDocument.is<JsonObject>()) {
|
||||
@@ -1859,7 +1885,7 @@ bool System::get_value_info(JsonObject output, const char * cmd) {
|
||||
}
|
||||
|
||||
// fetch all the data from the system in a different json
|
||||
JsonDocument doc;
|
||||
JsonDocument doc(PSRAM_DOC);
|
||||
JsonObject root = doc.to<JsonObject>();
|
||||
(void)command_info("", 0, root);
|
||||
|
||||
@@ -1954,7 +1980,7 @@ std::string System::get_metrics_prometheus() {
|
||||
result.reserve(16000);
|
||||
|
||||
// get system data
|
||||
JsonDocument doc;
|
||||
JsonDocument doc(PSRAM_DOC);
|
||||
JsonObject root = doc.to<JsonObject>();
|
||||
(void)command_info("", 0, root);
|
||||
|
||||
@@ -2233,6 +2259,7 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output
|
||||
node["sdk"] = ESP.getSdkVersion();
|
||||
node["freeMem"] = getHeapMem();
|
||||
node["maxAlloc"] = getMaxAllocMem();
|
||||
node["minFree"] = getMinFreeMem(); // all-time low watermark of internal heap
|
||||
node["freeCaps"] = heap_caps_get_free_size(MALLOC_CAP_8BIT) / 1024; // includes heap and psram
|
||||
node["usedApp"] = EMSESP::system_.appUsed(); // kilobytes
|
||||
node["freeApp"] = EMSESP::system_.appFree(); // kilobytes
|
||||
|
||||
@@ -299,10 +299,20 @@ class System {
|
||||
static uint32_t getHeapMem() {
|
||||
return heap_mem_;
|
||||
}
|
||||
// All-time low watermark of free internal heap (KB).
|
||||
// Unlike getHeapMem() (sampled now), this captures the *lowest* free heap
|
||||
// has ever been since boot — i.e. the worst transient dip during MQTT
|
||||
// publishes, HA discovery, /api/system calls, TLS handshakes, etc.
|
||||
// This is the number that actually reflects optimisations targeting
|
||||
// transient JSON / buffer peaks (e.g. Phase C PSRAM JsonDocuments).
|
||||
static uint32_t getMinFreeMem() {
|
||||
return min_free_mem_;
|
||||
}
|
||||
static void refreshHeapMem() {
|
||||
#ifndef EMSESP_STANDALONE
|
||||
max_alloc_mem_ = ESP.getMaxAllocHeap() / 1024;
|
||||
heap_mem_ = ESP.getFreeHeap() / 1024;
|
||||
min_free_mem_ = ESP.getMinFreeHeap() / 1024;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -346,6 +356,7 @@ class System {
|
||||
static bool test_set_all_active_; // force all entities in a device to have a value
|
||||
static uint32_t max_alloc_mem_;
|
||||
static uint32_t heap_mem_;
|
||||
static uint32_t min_free_mem_;
|
||||
|
||||
uint8_t systemStatus_; // uses SYSTEM_STATUS enum
|
||||
|
||||
|
||||
@@ -572,11 +572,11 @@ bool TxService::send_raw(const char * telegram_data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// since the telegram data is a const, make a copy. add 1 to grab the \0 EOS
|
||||
// since the telegram data is a const, make a copy
|
||||
char * telegram = strdup(telegram_data);
|
||||
|
||||
uint8_t count = 0;
|
||||
uint8_t data[256];
|
||||
uint8_t data[256]; // max raw telegram length
|
||||
|
||||
// get values
|
||||
char * p = strtok(telegram, " ,"); // delimiter
|
||||
@@ -700,4 +700,4 @@ uint16_t TxService::post_send_query() {
|
||||
return post_typeid;
|
||||
}
|
||||
|
||||
} // namespace emsesp
|
||||
} // namespace emsesp
|
||||
@@ -494,7 +494,7 @@ void TemperatureSensor::publish_values(const bool force) {
|
||||
}
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
JsonDocument doc(PSRAM_DOC);
|
||||
bool ha_dev_created = false;
|
||||
|
||||
for (auto & sensor : sensors_) {
|
||||
@@ -519,7 +519,7 @@ void TemperatureSensor::publish_values(const bool force) {
|
||||
} else if (!sensor.ha_registered || force) {
|
||||
LOG_DEBUG("Recreating HA config for sensor ID %s", sensor.id());
|
||||
|
||||
JsonDocument config;
|
||||
JsonDocument config(PSRAM_DOC);
|
||||
config["~"] = Mqtt::base();
|
||||
config["dev_cla"] = "temperature";
|
||||
config["stat_cla"] = "measurement";
|
||||
|
||||
@@ -99,7 +99,7 @@ class TemperatureSensor {
|
||||
std::string get_metrics_prometheus();
|
||||
|
||||
// return back reference to the sensor list, used by other classes
|
||||
std::vector<Sensor, AllocatorPSRAM<Sensor>> sensors() const {
|
||||
const std::vector<Sensor, AllocatorPSRAM<Sensor>> & sensors() const {
|
||||
return sensors_;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user