mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-10-28 11:14:58 +00:00
possible fix for #3201
This commit is contained in:
@@ -16,6 +16,7 @@ This release is based on the latest Espressif/Arduino core version 3. It brings
|
|||||||
- cleaner MD5 and firmware uploading in WebUI [#3222](https://github.com/emsesp/EMS-ESP32/issues/3222)
|
- cleaner MD5 and firmware uploading in WebUI [#3222](https://github.com/emsesp/EMS-ESP32/issues/3222)
|
||||||
- pc0Flow in l/min [#3224](https://github.com/emsesp/EMS-ESP32/issues/3224)
|
- pc0Flow in l/min [#3224](https://github.com/emsesp/EMS-ESP32/issues/3224)
|
||||||
- remove HA config for excluded entities [#3229](https://github.com/emsesp/EMS-ESP32/issues/3229)
|
- remove HA config for excluded entities [#3229](https://github.com/emsesp/EMS-ESP32/issues/3229)
|
||||||
|
- `show system` reports the Ethernet status on boards with a PHY, so a failed initialisation or a link that never came up is visible instead of Ethernet being omitted completely. Also shows the loop task's lowest free stack, to help diagnose reboots
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
@@ -28,6 +29,9 @@ This release is based on the latest Espressif/Arduino core version 3. It brings
|
|||||||
- HA Discovery warning on Uptime after EMS-ESP boot due to NTP not ready
|
- HA Discovery warning on Uptime after EMS-ESP boot due to NTP not ready
|
||||||
- hc/control setting for UI800 thermostats [#3181](https://github.com/emsesp/EMS-ESP32/discussions/3181)
|
- hc/control setting for UI800 thermostats [#3181](https://github.com/emsesp/EMS-ESP32/discussions/3181)
|
||||||
- solar module switches [#3223](https://github.com/emsesp/EMS-ESP32/issues/3223)
|
- solar module switches [#3223](https://github.com/emsesp/EMS-ESP32/issues/3223)
|
||||||
|
- possible crash when the WebUI asked for version information while the hourly versions.json refresh was rewriting the cache from the main loop task
|
||||||
|
- HTTP client could write past a fixed 64 byte buffer when a URL had a hostname of 64 characters or more, could busy-spin for the whole read budget if the socket reported bytes it wouldn't return, and buffered responses without any size limit
|
||||||
|
- the "Ethernet clock mode (GPIO16/17) conflicts with PSRAM" error was never logged because Ethernet had already been skipped by the time the check ran
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
@@ -42,4 +46,6 @@ This release is based on the latest Espressif/Arduino core version 3. It brings
|
|||||||
- set `None` for undefined values in HA `val_tpl`
|
- set `None` for undefined values in HA `val_tpl`
|
||||||
- changes in WebUI - remove multiple calls systemInfo endpoint, Vewrsion pages checks for internet connection [#3195](https://github.com/emsesp/EMS-ESP32/pull/3195)
|
- changes in WebUI - remove multiple calls systemInfo endpoint, Vewrsion pages checks for internet connection [#3195](https://github.com/emsesp/EMS-ESP32/pull/3195)
|
||||||
- multiple optimization in network, power down WiFi when Ethernet and WiFi nosleep is instant when selected [#3213](https://github.com/emsesp/EMS-ESP32/pull/3213)
|
- multiple optimization in network, power down WiFi when Ethernet and WiFi nosleep is instant when selected [#3213](https://github.com/emsesp/EMS-ESP32/pull/3213)
|
||||||
|
- TLS buffers (~17KB per HTTPS request) are taken from PSRAM on boards that have it, instead of fragmenting the internal heap
|
||||||
|
- failed versions.json fetches back off from 5 up to 160 minutes instead of retrying every 5 minutes, as each attempt blocks the main loop for as long as the DNS, connect and read timeouts allow
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ build_flags =
|
|||||||
; ESPAsyncWebServer
|
; ESPAsyncWebServer
|
||||||
; -D WS_MAX_QUEUED_MESSAGES=0 ; not used, default 8
|
; -D WS_MAX_QUEUED_MESSAGES=0 ; not used, default 8
|
||||||
; -D SSE_MAX_QUEUED_MESSAGES=1 ; for log messages, default 32
|
; -D SSE_MAX_QUEUED_MESSAGES=1 ; for log messages, default 32
|
||||||
|
; ESP_SSLClient
|
||||||
|
-D ENABLE_PSRAM ; take the ~17KB of TLS buffers from PSRAM instead of the internal heap. The library
|
||||||
|
; needs BOARD_HAS_PSRAM too, so boards without PSRAM keep using the internal heap
|
||||||
-D EMSESP_MQTT_RUNNING_CORE=1 ; default 1
|
-D EMSESP_MQTT_RUNNING_CORE=1 ; default 1
|
||||||
; -D EMSESP_MQTT_STACKSIZE=5120 ; default
|
; -D EMSESP_MQTT_STACKSIZE=5120 ; default
|
||||||
-D EMSESP_UART_RUNNING_CORE=1 ; default any core
|
-D EMSESP_UART_RUNNING_CORE=1 ; default any core
|
||||||
|
|||||||
+37
-10
@@ -35,6 +35,31 @@ int HttpClient::request(std::string url, const std::string & method, const std::
|
|||||||
return 0; // unsupported scheme
|
return 0; // unsupported scheme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// split the host from the path before anything is allocated, so a bad URL costs nothing
|
||||||
|
url.replace(0, is_https ? 8 : 7, "");
|
||||||
|
std::string host = url;
|
||||||
|
auto index = url.find_first_of('/');
|
||||||
|
if (index != std::string::npos) {
|
||||||
|
host = url.substr(0, index);
|
||||||
|
url.replace(0, index, "");
|
||||||
|
} else {
|
||||||
|
url = "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.empty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ESP_SSLClient keeps the host in a fixed char[64] and strcpy()s into it without a length
|
||||||
|
// check, so a longer name would corrupt whatever follows it and crash somewhere unrelated
|
||||||
|
if (host.length() > MAX_HOSTNAME_LENGTH) {
|
||||||
|
EMSESP::logger().warning("%s hostname is too long (%u characters, max is %u)",
|
||||||
|
is_https ? "HTTPS" : "HTTP",
|
||||||
|
(unsigned)host.length(),
|
||||||
|
(unsigned)MAX_HOSTNAME_LENGTH);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
WiFiClient * basic_client = new WiFiClient;
|
WiFiClient * basic_client = new WiFiClient;
|
||||||
ESP_SSLClient * ssl_client = new ESP_SSLClient;
|
ESP_SSLClient * ssl_client = new ESP_SSLClient;
|
||||||
if (is_https) {
|
if (is_https) {
|
||||||
@@ -50,16 +75,6 @@ int HttpClient::request(std::string url, const std::string & method, const std::
|
|||||||
ssl_client->setTimeout(5); // seconds, drives BearSSL only - unused on the plain HTTP path
|
ssl_client->setTimeout(5); // seconds, drives BearSSL only - unused on the plain HTTP path
|
||||||
ssl_client->setClient(basic_client, is_https); // enableSSL = false for plain HTTP
|
ssl_client->setClient(basic_client, is_https); // enableSSL = false for plain HTTP
|
||||||
|
|
||||||
url.replace(0, is_https ? 8 : 7, "");
|
|
||||||
std::string host = url;
|
|
||||||
auto index = url.find_first_of('/');
|
|
||||||
if (index != std::string::npos) {
|
|
||||||
host = url.substr(0, index);
|
|
||||||
url.replace(0, index, "");
|
|
||||||
} else {
|
|
||||||
url = "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint16_t port = is_https ? 443 : 80;
|
const uint16_t port = is_https ? 443 : 80;
|
||||||
if (ssl_client->connect(host.c_str(), port)) {
|
if (ssl_client->connect(host.c_str(), port)) {
|
||||||
bool content_set = false;
|
bool content_set = false;
|
||||||
@@ -97,16 +112,25 @@ int HttpClient::request(std::string url, const std::string & method, const std::
|
|||||||
// the stream goes idle, or the overall budget runs out
|
// the stream goes idle, or the overall budget runs out
|
||||||
const uint32_t started = millis();
|
const uint32_t started = millis();
|
||||||
uint32_t last_data = started;
|
uint32_t last_data = started;
|
||||||
|
bool truncated = false;
|
||||||
while (millis() - started < TOTAL_TIMEOUT_MS) {
|
while (millis() - started < TOTAL_TIMEOUT_MS) {
|
||||||
const int avail = ssl_client->available();
|
const int avail = ssl_client->available();
|
||||||
if (avail > 0) {
|
if (avail > 0) {
|
||||||
|
if (result.length() >= MAX_RESPONSE_BYTES) {
|
||||||
|
truncated = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
uint8_t buf[128];
|
uint8_t buf[128];
|
||||||
const size_t want = (avail < (int)sizeof(buf)) ? (size_t)avail : sizeof(buf);
|
const size_t want = (avail < (int)sizeof(buf)) ? (size_t)avail : sizeof(buf);
|
||||||
const int len = ssl_client->read(buf, want);
|
const int len = ssl_client->read(buf, want);
|
||||||
if (len > 0) {
|
if (len > 0) {
|
||||||
result.append(reinterpret_cast<const char *>(buf), len);
|
result.append(reinterpret_cast<const char *>(buf), len);
|
||||||
last_data = millis();
|
last_data = millis();
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
// available() reported bytes the client won't hand over. Yielding here keeps a
|
||||||
|
// stale count from busy-spinning the whole read budget on this task's core
|
||||||
|
delay(1);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!ssl_client->connected()) {
|
if (!ssl_client->connected()) {
|
||||||
@@ -121,6 +145,9 @@ int HttpClient::request(std::string url, const std::string & method, const std::
|
|||||||
ssl_client->stop();
|
ssl_client->stop();
|
||||||
|
|
||||||
const auto received = result.length();
|
const auto received = result.length();
|
||||||
|
if (truncated) {
|
||||||
|
EMSESP::logger().warning("%s response from %s truncated at %u bytes", is_https ? "HTTPS" : "HTTP", host.c_str(), (unsigned)received);
|
||||||
|
}
|
||||||
|
|
||||||
// parse the status line "HTTP/1.x <code> <reason>". stoi() would abort rather than throw on
|
// parse the status line "HTTP/1.x <code> <reason>". stoi() would abort rather than throw on
|
||||||
// a malformed response, since the firmware is built with -fno-exceptions
|
// a malformed response, since the firmware is built with -fno-exceptions
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ class HttpClient {
|
|||||||
static constexpr uint32_t FIRST_BYTE_TIMEOUT_MS = 8000; // how long the server may take to start replying
|
static constexpr uint32_t FIRST_BYTE_TIMEOUT_MS = 8000; // how long the server may take to start replying
|
||||||
static constexpr uint32_t IDLE_TIMEOUT_MS = 500; // gap in the stream that marks the end of a response
|
static constexpr uint32_t IDLE_TIMEOUT_MS = 500; // gap in the stream that marks the end of a response
|
||||||
static constexpr uint32_t TOTAL_TIMEOUT_MS = 10000; // ceiling for the whole read
|
static constexpr uint32_t TOTAL_TIMEOUT_MS = 10000; // ceiling for the whole read
|
||||||
|
|
||||||
|
// ceiling on the response we'll buffer. Without it a chatty or hijacked endpoint can grow the
|
||||||
|
// result string until the heap runs out, which aborts the firmware since it's built -fno-exceptions
|
||||||
|
static constexpr size_t MAX_RESPONSE_BYTES = 32 * 1024;
|
||||||
|
|
||||||
|
static constexpr size_t MAX_HOSTNAME_LENGTH = 63; // ESP_SSLClient stores the host in a char[64]
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace emsesp
|
} // namespace emsesp
|
||||||
|
|||||||
@@ -1126,6 +1126,12 @@ void System::show_system(uuid::console::Shell & shell) {
|
|||||||
shell.printfln(" Internal heap free/largest block: %u KB / %u KB",
|
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_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) / 1024,
|
||||||
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) / 1024);
|
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) / 1024);
|
||||||
|
// Lowest the loop task's stack has ever been since boot. EMSESP::loop() runs the blocking HTTP
|
||||||
|
// client and all the JSON work on this task, and a TLS handshake alone can want several KB
|
||||||
|
TaskHandle_t loop_task = xTaskGetHandle("loopTask");
|
||||||
|
if (loop_task != nullptr) {
|
||||||
|
shell.printfln(" Loop task stack min free/total: %u / %u bytes", (unsigned)uxTaskGetStackHighWaterMark(loop_task), (unsigned)getArduinoLoopTaskStackSize());
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
shell.printfln(" App used/free: %lu KB / %lu KB", appUsed(), appFree());
|
shell.printfln(" App used/free: %lu KB / %lu KB", appUsed(), appFree());
|
||||||
uint32_t FSused = LittleFS.usedBytes() / 1024;
|
uint32_t FSused = LittleFS.usedBytes() / 1024;
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
#define EMSESP_APP_VERSION "3.9.0-dev.9"
|
#define EMSESP_APP_VERSION "3.9.0-dev.10"
|
||||||
|
|||||||
@@ -348,7 +348,18 @@ void WebStatusService::getVersions(JsonObject root) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!versions_cache_valid_) {
|
// take a snapshot, since the loop task can be rewriting the cache while we run on AsyncTCP
|
||||||
|
VersionInfo stable;
|
||||||
|
VersionInfo dev;
|
||||||
|
bool cache_valid;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock{versions_mutex_};
|
||||||
|
cache_valid = versions_cache_valid_;
|
||||||
|
stable = versions_stable_;
|
||||||
|
dev = versions_dev_;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cache_valid) {
|
||||||
// no successful fetch yet (no network, fetch pending, or parse error)
|
// no successful fetch yet (no network, fetch pending, or parse error)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -364,8 +375,8 @@ void WebStatusService::getVersions(JsonObject root) {
|
|||||||
out["upgradeable"] = info.upgradeable;
|
out["upgradeable"] = info.upgradeable;
|
||||||
};
|
};
|
||||||
|
|
||||||
add_section("stable", versions_stable_);
|
add_section("stable", stable);
|
||||||
add_section("dev", versions_dev_);
|
add_section("dev", dev);
|
||||||
#else
|
#else
|
||||||
// standalone/test build: provide deterministic dummy data
|
// standalone/test build: provide deterministic dummy data
|
||||||
JsonObject stable_out = root["stable"].to<JsonObject>();
|
JsonObject stable_out = root["stable"].to<JsonObject>();
|
||||||
@@ -383,7 +394,8 @@ void WebStatusService::getVersions(JsonObject root) {
|
|||||||
// schedule the next versions.json fetch a few seconds out so the network stack has time to settle
|
// schedule the next versions.json fetch a few seconds out so the network stack has time to settle
|
||||||
// (DHCP completion, default-netif assignment and DNS server propagation through lwip)
|
// (DHCP completion, default-netif assignment and DNS server propagation through lwip)
|
||||||
void WebStatusService::schedule_versions_refresh() {
|
void WebStatusService::schedule_versions_refresh() {
|
||||||
uint32_t next = uuid::get_uptime() + VERSIONS_INITIAL_FETCH_DELAY_MS;
|
versions_failures_ = 0; // a fresh link is a fresh chance, so start again from the short retry delay
|
||||||
|
uint32_t next = uuid::get_uptime() + VERSIONS_INITIAL_FETCH_DELAY_MS;
|
||||||
if (next == 0) {
|
if (next == 0) {
|
||||||
next = 1; // 0 is the "idle" sentinel — never let the wrap land there
|
next = 1; // 0 is the "idle" sentinel — never let the wrap land there
|
||||||
}
|
}
|
||||||
@@ -409,8 +421,18 @@ void WebStatusService::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ok = refresh_versions_cache();
|
uint32_t interval;
|
||||||
uint32_t next = uuid::get_uptime() + (ok ? VERSIONS_REFRESH_INTERVAL_MS : VERSIONS_RETRY_INTERVAL_MS);
|
if (refresh_versions_cache()) {
|
||||||
|
versions_failures_ = 0;
|
||||||
|
interval = VERSIONS_REFRESH_INTERVAL_MS;
|
||||||
|
} else {
|
||||||
|
if (versions_failures_ < VERSIONS_MAX_BACKOFF_STEPS) {
|
||||||
|
versions_failures_++;
|
||||||
|
}
|
||||||
|
interval = VERSIONS_RETRY_INTERVAL_MS << (versions_failures_ - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t next = uuid::get_uptime() + interval;
|
||||||
if (next == 0) {
|
if (next == 0) {
|
||||||
next = 1;
|
next = 1;
|
||||||
}
|
}
|
||||||
@@ -451,16 +473,21 @@ bool WebStatusService::refresh_versions_cache() {
|
|||||||
out.upgradeable = !out.version.empty() && FirmwareVersion(out.version) > current_version;
|
out.upgradeable = !out.version.empty() && FirmwareVersion(out.version) > current_version;
|
||||||
};
|
};
|
||||||
|
|
||||||
read_section("stable", versions_stable_);
|
// parse into locals first, then publish in one go so the AsyncTCP task never sees a half-written cache
|
||||||
read_section("dev", versions_dev_);
|
VersionInfo stable;
|
||||||
|
VersionInfo dev;
|
||||||
|
read_section("stable", stable);
|
||||||
|
read_section("dev", dev);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock{versions_mutex_};
|
||||||
|
versions_stable_ = stable;
|
||||||
|
versions_dev_ = dev;
|
||||||
|
versions_cache_valid_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
versions_cache_valid_ = true;
|
|
||||||
#if defined(EMSESP_DEBUG)
|
#if defined(EMSESP_DEBUG)
|
||||||
EMSESP::logger().debug("Fetched from %s: stable=%s, dev=%s, current=%s",
|
EMSESP::logger().debug("Fetched from %s: stable=%s, dev=%s, current=%s", VERSIONS_URL, stable.version.c_str(), dev.version.c_str(), current_version_s.c_str());
|
||||||
VERSIONS_URL,
|
|
||||||
versions_stable_.version.c_str(),
|
|
||||||
versions_dev_.version.c_str(),
|
|
||||||
current_version_s.c_str());
|
|
||||||
#endif
|
#endif
|
||||||
return true;
|
return true;
|
||||||
#endif
|
#endif
|
||||||
@@ -468,11 +495,12 @@ bool WebStatusService::refresh_versions_cache() {
|
|||||||
|
|
||||||
// returns if current dev/stable is upgradeable
|
// returns if current dev/stable is upgradeable
|
||||||
bool WebStatusService::current_upgradeable() const {
|
bool WebStatusService::current_upgradeable() const {
|
||||||
|
FirmwareVersion current_version(current_version_s);
|
||||||
|
bool is_dev = current_version.prerelease().find("dev") != std::string::npos;
|
||||||
|
std::lock_guard<std::mutex> lock{versions_mutex_};
|
||||||
if (!versions_cache_valid_) {
|
if (!versions_cache_valid_) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
FirmwareVersion current_version(current_version_s);
|
|
||||||
bool is_dev = current_version.prerelease().find("dev") != std::string::npos;
|
|
||||||
return is_dev ? versions_dev_.upgradeable : versions_stable_.upgradeable;
|
return is_dev ? versions_dev_.upgradeable : versions_stable_.upgradeable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
#include "../core/firmwareVersion.h"
|
#include "../core/firmwareVersion.h"
|
||||||
#include "../emsesp_version.h"
|
#include "../emsesp_version.h"
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
namespace emsesp {
|
namespace emsesp {
|
||||||
|
|
||||||
class WebStatusService {
|
class WebStatusService {
|
||||||
@@ -67,14 +69,24 @@ class WebStatusService {
|
|||||||
VersionInfo versions_dev_;
|
VersionInfo versions_dev_;
|
||||||
bool versions_cache_valid_ = false; // true once we've had at least one successful fetch
|
bool versions_cache_valid_ = false; // true once we've had at least one successful fetch
|
||||||
uint32_t versions_next_fetch_ms_ = 0; // uuid::get_uptime() of the next attempt; 0 = idle
|
uint32_t versions_next_fetch_ms_ = 0; // uuid::get_uptime() of the next attempt; 0 = idle
|
||||||
|
uint8_t versions_failures_ = 0; // consecutive failed fetches, drives the retry backoff
|
||||||
|
|
||||||
|
// the cache is written by the loop task in refresh_versions_cache() and read by the AsyncTCP
|
||||||
|
// task in getVersions()/current_upgradeable(), so the std::strings need guarding - without it a
|
||||||
|
// request landing mid-update reads a buffer that has just been freed
|
||||||
|
mutable std::mutex versions_mutex_;
|
||||||
|
|
||||||
bool refresh_versions_cache(); // does the actual HTTPS fetch + parse, returns true on success
|
bool refresh_versions_cache(); // does the actual HTTPS fetch + parse, returns true on success
|
||||||
|
|
||||||
static constexpr const char * VERSIONS_URL = "http://emsesp.org/versions.json";
|
static constexpr const char * VERSIONS_URL = "http://emsesp.org/versions.json";
|
||||||
|
|
||||||
static constexpr uint32_t VERSIONS_REFRESH_INTERVAL_MS = 60UL * 60UL * 1000UL; // 1 hour on success
|
static constexpr uint32_t VERSIONS_REFRESH_INTERVAL_MS = 60UL * 60UL * 1000UL; // 1 hour on success
|
||||||
static constexpr uint32_t VERSIONS_RETRY_INTERVAL_MS = 5UL * 60UL * 1000UL; // 5 min after failure
|
static constexpr uint32_t VERSIONS_RETRY_INTERVAL_MS = 5UL * 60UL * 1000UL; // 5 min after the first failure
|
||||||
static constexpr uint32_t VERSIONS_INITIAL_FETCH_DELAY_MS = 5UL * 1000UL; // 5 s after a link comes up
|
static constexpr uint32_t VERSIONS_INITIAL_FETCH_DELAY_MS = 5UL * 1000UL; // 5 s after a link comes up
|
||||||
|
|
||||||
|
// each attempt blocks the loop task for as long as the DNS lookup, connect and read timeouts
|
||||||
|
// allow, so double the retry delay per consecutive failure: 5, 10, 20, 40, 80, 160 minutes
|
||||||
|
static constexpr uint8_t VERSIONS_MAX_BACKOFF_STEPS = 6;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace emsesp
|
} // namespace emsesp
|
||||||
|
|||||||
Reference in New Issue
Block a user