Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion firmware/esp32-matter/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# . ~/esp/esp-idf-v5.5.4/export.sh && . $ESP_MATTER_PATH/export.sh
# idf.py set-target esp32 && idf.py build flash monitor
cmake_minimum_required(VERSION 3.16)
set(PROJECT_VER "1.1.4")
set(PROJECT_VER "1.1.5")

# Unified versioning (issue #77): the Matter softwareVersion INT is DERIVED from PROJECT_VER --
# MAJOR*10000+MINOR*100+PATCH -> a readable, strictly-monotonic uint32. This keeps the human
Expand Down
52 changes: 52 additions & 0 deletions firmware/esp32-matter/main/app_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

#include <esp_matter.h>
#include <esp_matter_endpoint.h>
#include <esp_matter_ota.h> // BDX-OTA TX-power throttle (#12): custom OTA requestor driver

#include <app/server/Server.h> // Server / FabricTable / commissioning window (F1 "77")
#include <app/server/CommissioningWindowManager.h>
Expand Down Expand Up @@ -932,6 +933,44 @@ static void on_recommission_cancel(void)
#define HISENSE_OTA_TX_POWER_QDBM 40 /* 10 dBm, quarter-dBm units. Plenty for a LAN hop. */
#define HISENSE_OTA_CHUNK_YIELD_MS 8 /* breathing room between flash writes */

/* #12 brownout mitigation, Matter (BDX) OTA path. The stock esp-matter OTA requestor runs the
* whole download+apply at the full 20 dBm TX ceiling, so on this marginal A/C rail the concurrent
* flash-write (300-500 mA) + Wi-Fi TX peak is exactly the combination that hung a node mid-OTA.
* The HTTP break-glass path (https_ota_task, below) already drops TX for its duration; this brings
* the SAME throttle to the PRIMARY Matter path by swapping in a driver that lowers TX on idle-exit
* (an update is starting) and restores it on idle-enter (back to idle on ANY path: done / abort /
* timeout). Capture-once + restore-on-every-idle-enter mirrors the save/restore-on-every-branch
* discipline of the HTTP path, so the node is never left stuck at 10 dBm. Not a substitute for the
* bulk cap; it only lowers the probability of a brownout during the OTA window. */
class HisenseOTARequestorDriver : public chip::DeviceLayer::ExtendedOTARequestorDriver {
public:
void HandleIdleStateExit() override
{
if (!m_tx_saved) { // capture the live ceiling ONCE, on the first exit-from-idle
m_tx_saved = (esp_wifi_get_max_tx_power(&m_saved_tx) == ESP_OK);
if (m_tx_saved) {
esp_wifi_set_max_tx_power(HISENSE_OTA_TX_POWER_QDBM);
ESP_LOGW(TAG, "Matter OTA: Wi-Fi TX power %d -> %d (quarter-dBm) to cut the current peak",
(int) m_saved_tx, HISENSE_OTA_TX_POWER_QDBM);
}
}
chip::DeviceLayer::ExtendedOTARequestorDriver::HandleIdleStateExit();
}
void HandleIdleStateEnter(chip::IdleStateReason reason) override
{
chip::DeviceLayer::ExtendedOTARequestorDriver::HandleIdleStateEnter(reason);
if (m_tx_saved) { // restore on ANY return to idle (success / abort / timeout)
esp_wifi_set_max_tx_power(m_saved_tx);
ESP_LOGW(TAG, "Matter OTA: Wi-Fi TX power restored to %d (quarter-dBm)", (int) m_saved_tx);
m_tx_saved = false;
}
}
private:
int8_t m_saved_tx = 0;
bool m_tx_saved = false;
};
static HisenseOTARequestorDriver s_hisense_ota_driver;

static void https_ota_task(void *arg)
{
ESP_LOGW(TAG, "HTTPS-OTA: fetching %s", HISENSE_OTA_URL);
Expand Down Expand Up @@ -1452,6 +1491,19 @@ extern "C" void app_main()

esp_matter::start(NULL); // brings up Wi-Fi/Matter + commissioning

// #12 brownout mitigation: swap in the TX-throttling OTA requestor driver for the Matter (BDX)
// path. Must run AFTER esp_matter::start() (esp_matter_ota_requestor_init() has executed) and
// BEFORE the async kDnssdInitialized event fires esp_matter_ota_requestor_start(), which is what
// consumes this driver. Timeout fields left 0 -> set_config keeps the esp-matter defaults (it
// guards each with `if`). Only the driver pointer is captured, so the config structs are locals.
{
esp_matter_ota_requestor_impl_t ota_impl = {};
ota_impl.driver = &s_hisense_ota_driver;
esp_matter_ota_config_t ota_cfg = {};
ota_cfg.impl = &ota_impl;
esp_matter_ota_requestor_set_config(ota_cfg);
}

// HA entity labels (UserLabel key "ha_entitylabel") -> distinguishable same-type entities.
// start() has returned (Server::Init done, endpoints exist); take the stack lock since we
// run on a different task than the Matter event loop. Read live by the UserLabel cluster.
Expand Down
4 changes: 2 additions & 2 deletions firmware/esp32-matter/sdkconfig.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ CONFIG_CUSTOM_DEVICE_INFO_PROVIDER=y
# so it (and the fallback NUMBER) MUST stay equal to PROJECT_VER or the device reports a stale
# softwareVersionString. Edit PROJECT_VER in CMakeLists.txt, then update both lines below.
# esp32-lint.sh enforces this equality (fails the commit/CI if they drift).
CONFIG_DEVICE_SOFTWARE_VERSION_NUMBER=10104
CONFIG_DEVICE_SOFTWARE_VERSION_STRING="1.1.4"
CONFIG_DEVICE_SOFTWARE_VERSION_NUMBER=10105
CONFIG_DEVICE_SOFTWARE_VERSION_STRING="1.1.5"
# --- OTA hardening (faster + reliable on marginal Wi-Fi) ---
# Delta OTA: ship a diff (tens of KB) instead of the full ~1.5MB image over BDX.
CONFIG_ENABLE_DELTA_OTA=y
Expand Down
27 changes: 20 additions & 7 deletions firmware/scripts/esp32-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -256,14 +256,27 @@ check_subscription_log() {
say " PI_HOST/PI_SSH_KEY unset -- cannot read the matter-server log; node availability stands as the subscription assertion (#64)"
return 0
fi
local line
if ! line="$(ssh -o BatchMode=yes -o ConnectTimeout=10 -i "$PI_SSH_KEY" "$PI_HOST" \
"docker logs --since 15m matter-server 2>&1 | grep -m1 '<Node:$node> Subscription succeeded' || true" 2>/dev/null)"; then
say " could not read the matter-server log on $PI_HOST -- node availability stands as the subscription assertion (#64)"
return 0
fi
# After an OTA the device REBOOTS, so the healthy post-flash signal is usually a
# '<Node:N> Re-Subscription succeeded' logged a few seconds after the re-interview, NOT the
# plain 'Subscription succeeded' (that one is the PRE-reboot subscription, often already >15m
# old). matter-server also colourises its logs, so an ESC[..m reset sits between '<Node:N>'
# and the message -- strip ANSI first or the anchored pattern never matches. Both gaps
# false-alarmed an otherwise-healthy node 35 flash on 2026-07-22 (version confirmed, node
# available + subscribed, yet the old single-shot grep found nothing). So: strip ANSI, match
# BOTH forms, take the most RECENT (tail -1), and poll, since the resubscribe can land a few
# seconds after we start looking.
local line=""
for _ in 1 2 3 4 5 6; do
if ! line="$(ssh -o BatchMode=yes -o ConnectTimeout=10 -i "$PI_SSH_KEY" "$PI_HOST" \
"docker logs --since 15m matter-server 2>&1 | sed -E 's/\x1b\[[0-9;]*m//g' | grep -E '<Node:$node> (Re-)?Subscription succeeded' | tail -1 || true" 2>/dev/null)"; then
say " could not read the matter-server log on $PI_HOST -- node availability stands as the subscription assertion (#64)"
return 0
fi
[ -n "$line" ] && break
sleep 10
done
[ -n "$line" ] \
|| die "no 'Subscription succeeded' for node $node in the last 15m of the matter-server log -- subscription is broken (#64, docs/10 §16)"
|| die "no '(Re-)Subscription succeeded' for node $node in the last 15m of the matter-server log -- subscription is broken (#64, docs/10 §16)"
say " matter-server log confirms: ${line:0:120}"
}
flash() {
Expand Down
2 changes: 1 addition & 1 deletion firmware/scripts/ota-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ check_subscription_log() {
local line=""
for _ in 1 2 3 4 5 6; do
if ! line="$(ssh -o BatchMode=yes -o ConnectTimeout=10 -i "$PI_SSH_KEY" "$PI_HOST" \
"docker logs --since 15m matter-server 2>&1 | grep -E '<Node:$node> (Re-)?Subscription succeeded' | tail -1 || true" 2>/dev/null)"; then
"docker logs --since 15m matter-server 2>&1 | sed -E 's/\x1b\[[0-9;]*m//g' | grep -E '<Node:$node> (Re-)?Subscription succeeded' | tail -1 || true" 2>/dev/null)"; then
say " could not read the matter-server log on $PI_HOST -- node availability stands as the subscription assertion (#64)"
return 0
fi
Expand Down