Skip to content

Commit 15552c5

Browse files
authored
feat(#72): runtime capability gating wiring (ESP32 + AmebaZ2) (#101)
* feat(#72): ESP32 runtime capability gating wiring [BENCH-PENDING, do not merge yet] Wires the shared gate predicates (matter_aircon_map.h, PR #100) into the ESP32 features callback so a per-unit A/C hides the surfaces it lacks: power_save -> eco ep, fan_mute -> quiet ep, power_display -> display ep via esp_matter::endpoint::enable/disable (the CHIP ember emberAfEndpointEnable- Disable is declared but NOT linked in esp-matter's data model -- confirmed at link time -- so the esp_matter-native per-endpoint API is used instead); cool_heat -> Thermostat FeatureMap (35 Heat+Cool+Auto vs 2 Cool-only) via attribute::update of the global FeatureMap attribute. Strategy: retroactive-correct. Endpoints created at boot (stable IDs, contiguous; never skip endpoint::create) then hidden from on_features once the 0x66 reply lands. Runs in bus-task context under the CHIP stack lock; ordering is safe (bus task runs after esp_matter::start()/enable_all()). Diff-guarded off the known boot layout so an all-present unit (every unit we own) makes ZERO ember/attribute calls. Compiles + links clean (debug, 15% flash free). Bumps PROJECT_VER 1.1.8 -> 1.1.9. NOT FOR MERGE until validated on a bench (user rule + #64): needs a flag-ABSENT unit or a spoofed 0x66 reply to exercise the hide path (all our units report every capability present), AND matter-server confirmation that HA drops the entity / re-derives HVAC modes on a live PartsList/FeatureMap change vs. a manual re-interview. Compare the exposed model against firmware/../72-baseline.json. AmebaZ2 wiring (matter_drivers.cpp, emberAfEndpointEnableDisable + FeatureMap::Set) still to write. Assisted-by: AI * chore(#72): bump esp32 to 1.1.10 (clean build deployed to node 35 after forced-gate validation) Node 35 validated the gating (endpoint::disable removes ep3 from PartsList, matter- server picks it up) then was reverted to this clean gating build (eco re-enabled, matches baseline). Still BENCH-PENDING / not for merge. Assisted-by: AI * feat(#72): AmebaZ2 runtime capability gating wiring Mirrors the ESP32 gating on the AmebaZ2 path (matter_drivers.cpp features callback), using the same shared predicates (matter_aircon_map.h): power_save->eco / fan_mute->quiet / power_display->display via emberAfEndpointEnable- Disable -- which LINKS on AmebaZ2 (ember static .zap data model, attribute-storage.cpp), unlike esp-matter where it is declared-but-unlinked. It flips the isEnabled bit on the existing endpoint slot: no removal, no renumber, so the {0..10} contiguity boot-fault rule is untouched. cool_heat -> Thermostat FeatureMap (35 vs 2) via ThermAttr::FeatureMap::Set (ep1 is a composite endpoint, cannot be endpoint-disabled). Diff-guarded off the boot layout (all-present unit = zero calls); bus-task context so it takes the CHIP stack lock. Builds + links clean (--debug, serial 11426). Bumps version.txt 1.3.25 -> 1.3.26. Same retroactive-correct strategy + caveats as the ESP32 side (validated on node 35): endpoint disable propagates to matter-server but HA leaves a runtime-hidden entity lingering; the clean path is gating at commissioning (persist-at-boot follow-up). Assisted-by: AI
1 parent 5631b86 commit 15552c5

5 files changed

Lines changed: 96 additions & 6 deletions

File tree

firmware/esp32-matter/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# . ~/esp/esp-idf-v5.5.4/export.sh && . $ESP_MATTER_PATH/export.sh
33
# idf.py set-target esp32 && idf.py build flash monitor
44
cmake_minimum_required(VERSION 3.16)
5-
set(PROJECT_VER "1.1.8")
5+
set(PROJECT_VER "1.1.10")
66

77
# Unified versioning (issue #77): the Matter softwareVersion INT is DERIVED from PROJECT_VER --
88
# MAJOR*10000+MINOR*100+PATCH -> a readable, strictly-monotonic uint32. This keeps the human

firmware/esp32-matter/main/app_main.cpp

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -692,12 +692,67 @@ static void on_status(const HisenseState *st)
692692
// lk (ScopedChipStackLock) releases the CHIP stack lock here at scope exit.
693693
}
694694

695-
// 0x66/40 ProductType feature-flags (bus-task context) -> log. Bit positions RE'd from the
696-
// stock firmware; decoded in the shared driver, not surfaced to HA (capability flags are static).
695+
/* #72 runtime capability gating. Hide the Matter surfaces a per-unit A/C does not support, decided
696+
* by the SHARED predicates (matter_aircon_map.h) so this path and the AmebaZ2 one cannot drift:
697+
* power_save -> eco ep, fan_mute -> quiet ep, power_display -> display ep (endpoint enable/disable);
698+
* cool_heat -> Thermostat FeatureMap (35 Heat+Cool+Auto vs 2 Cool-only).
699+
*
700+
* STRATEGY (first cut): retroactive-correct. Endpoints are created at boot (stable IDs, contiguous
701+
* table -- we NEVER skip endpoint::create, a renumber would hard-fault the AmebaZ2 boot table) and
702+
* default-enabled; this hides the absent ones once the 0x66 features reply lands. Runs in bus-task
703+
* context, so it takes the CHIP stack lock (like on_link). Ordering is safe: esp_matter::start()
704+
* calls endpoint::enable_all() which would clobber a pre-start disable, but the bus task only runs
705+
* AFTER start(), so on_features always fires post-start.
706+
*
707+
* The guards are seeded to the KNOWN boot layout (all enabled; FeatureMap default 35), so an
708+
* all-present unit -- every unit we own -- makes ZERO ember/attribute calls and never churns
709+
* Descriptor/FeatureMap change reports on the ~60s features refresh.
710+
*
711+
* BENCH-PENDING, DO NOT SHIP UNVALIDATED (#64 lesson): needs (a) a flag-ABSENT unit or a spoofed
712+
* 0x66 reply to exercise the hide path (all our units report every capability present), and
713+
* (b) matter-server validation that HA actually drops the entity / re-derives HVAC modes on a live
714+
* PartsList/FeatureMap change vs. requiring a manual re-interview. KNOWN CAVEAT: a unit commissioned
715+
* DURING the boot window (before the first 0x66 reply) captures the full layout into HA's interview
716+
* snapshot and needs a manual re-interview to reflect a later correction; persist-and-gate-at-boot
717+
* is the follow-up if that matters. Compare the exposed PartsList/FeatureMap against 72-baseline.json. */
718+
static void apply_capability_gates(const HisenseFeatures *f)
719+
{
720+
static int8_t g_eco = 1, g_quiet = 1, g_display = 1;
721+
static uint32_t g_fm = MATTER_THERMOSTAT_FEATUREMAP_FULL;
722+
723+
const int8_t eco = matter_gate_eco(f), quiet = matter_gate_quiet(f), display = matter_gate_display(f);
724+
const uint32_t fm = matter_thermostat_featuremap(f);
725+
if (eco == g_eco && quiet == g_quiet && display == g_display && fm == g_fm) return; // common case: no-op
726+
727+
lock::ScopedChipStackLock lk(portMAX_DELAY);
728+
// esp_matter's native per-endpoint enable/disable (the CHIP ember emberAfEndpointEnableDisable
729+
// is declared but NOT linked in esp-matter's data model). endpoint::get(id) resolves the handle.
730+
auto gate_ep = [](uint16_t id, int8_t want, int8_t *cur, const char *name) {
731+
if (want == *cur) return;
732+
endpoint_t *ep = endpoint::get(id);
733+
if (!ep) return;
734+
(want ? endpoint::enable(ep) : endpoint::disable(ep));
735+
*cur = want;
736+
ESP_LOGW(TAG, "#72 gate: %s ep%u %s", name, id, want ? "shown" : "HIDDEN");
737+
};
738+
gate_ep(s_ep_eco, eco, &g_eco, "eco");
739+
gate_ep(s_ep_mute, quiet, &g_quiet, "quiet");
740+
gate_ep(s_ep_display, display, &g_display, "display");
741+
if (fm != g_fm) {
742+
esp_matter_attr_val_t v = esp_matter_bitmap32(fm);
743+
attribute::update(s_ep_id, Thermostat::Id, chip::app::Clusters::Globals::Attributes::FeatureMap::Id, &v);
744+
g_fm = fm;
745+
ESP_LOGW(TAG, "#72 gate: thermostat FeatureMap -> %u (%s)", (unsigned) fm,
746+
fm == MATTER_THERMOSTAT_FEATUREMAP_FULL ? "Heat+Cool+Auto" : "Cool-only");
747+
}
748+
}
749+
750+
// 0x66/40 ProductType feature-flags (bus-task context). Log, then gate the per-unit capabilities (#72).
697751
static void on_features(const HisenseFeatures *f)
698752
{
699753
ESP_LOGI(TAG, "A/C features (0x66/40): ai=%d display=%d swing8=%d eco=%d mute=%d purify=%d",
700754
f->ai, f->power_display, f->swing_dir_8, f->power_save, f->fan_mute, f->purify);
755+
apply_capability_gates(f);
701756
}
702757

703758
// Bus link lost/restored (#56). On loss, null every liveness attribute (LocalTemperature +

firmware/esp32-matter/sdkconfig.defaults

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ CONFIG_CUSTOM_DEVICE_INFO_PROVIDER=y
4646
# so it (and the fallback NUMBER) MUST stay equal to PROJECT_VER or the device reports a stale
4747
# softwareVersionString. Edit PROJECT_VER in CMakeLists.txt, then update both lines below.
4848
# esp32-lint.sh enforces this equality (fails the commit/CI if they drift).
49-
CONFIG_DEVICE_SOFTWARE_VERSION_NUMBER=10108
50-
CONFIG_DEVICE_SOFTWARE_VERSION_STRING="1.1.8"
49+
CONFIG_DEVICE_SOFTWARE_VERSION_NUMBER=10110
50+
CONFIG_DEVICE_SOFTWARE_VERSION_STRING="1.1.10"
5151
# --- OTA hardening (faster + reliable on marginal Wi-Fi) ---
5252
# Delta OTA: ship a diff (tens of KB) instead of the full ~1.5MB image over BDX.
5353
CONFIG_ENABLE_DELTA_OTA=y

firmware/src/sdk-edits/matter_drivers.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ static void hisense_breakglass_start(void);
3737
#include <app-common/zap-generated/ids/Attributes.h>
3838
#include <app-common/zap-generated/ids/Clusters.h>
3939
#include <app/util/attribute-table.h>
40+
#include <app/util/endpoint-config-api.h> // #72 emberAfEndpointEnableDisable (runtime endpoint gating)
4041
#include <protocols/interaction_model/StatusCode.h>
4142
#include <app/server/Server.h> // Server / fabric table / commissioning window (F1 "77")
4243
#include <app/server/CommissioningWindowManager.h>
@@ -480,6 +481,39 @@ static void matter_driver_on_recommission(uint8_t reason)
480481
// Driver feature-flags callback (bus-task context): log the A/C's 0x66/40 ProductType
481482
// capability/state flags each time they're parsed. Not surfaced to HA (capability flags
482483
// are static per model); available in-driver via hisense_get_features().
484+
/* #72 runtime capability gating (mirrors the ESP32 path; shared decision predicates in
485+
* matter_aircon_map.h). AmebaZ2 uses the ember STATIC .zap data model, so emberAfEndpointEnable-
486+
* Disable -- which LINKS here (attribute-storage.cpp), unlike esp-matter -- flips the isEnabled bit
487+
* on the existing endpoint slot: no removal, no renumber, so the {0..10} contiguity boot-fault rule
488+
* is untouched. cool_heat gates the composite ep1's Thermostat FeatureMap (35 Heat+Cool+Auto vs 2
489+
* Cool-only) via the generated Set accessor (ep1 cannot be endpoint-disabled). Diff-guarded off the
490+
* boot layout so an all-present unit makes ZERO calls. Bus-task context -> take the CHIP stack lock.
491+
*
492+
* BENCH note (validated on the ESP32 side, #72): the endpoint disable propagates to matter-server,
493+
* but HA leaves a runtime-hidden entity lingering (unavailable) until a reload; the clean case is a
494+
* unit gated at commissioning. The FeatureMap runtime change's HA re-derivation is still unproven. */
495+
static void apply_capability_gates(const HisenseFeatures *f)
496+
{
497+
static int8_t g_eco = 1, g_quiet = 1, g_display = 1;
498+
static uint32_t g_fm = MATTER_THERMOSTAT_FEATUREMAP_FULL;
499+
500+
const int8_t eco = matter_gate_eco(f), quiet = matter_gate_quiet(f), display = matter_gate_display(f);
501+
const uint32_t fm = matter_thermostat_featuremap(f);
502+
if (eco == g_eco && quiet == g_quiet && display == g_display && fm == g_fm) return; // common case: no-op
503+
504+
chip::DeviceLayer::PlatformMgr().LockChipStack();
505+
if (eco != g_eco) { emberAfEndpointEnableDisable(kEcoEp, eco); g_eco = eco;
506+
ChipLogProgress(DeviceLayer, "#72 gate: eco ep%d %s", kEcoEp, eco ? "shown" : "HIDDEN"); }
507+
if (quiet != g_quiet) { emberAfEndpointEnableDisable(kMuteEp, quiet); g_quiet = quiet;
508+
ChipLogProgress(DeviceLayer, "#72 gate: quiet ep%d %s", kMuteEp, quiet ? "shown" : "HIDDEN"); }
509+
if (display != g_display){ emberAfEndpointEnableDisable(kDisplayEp, display); g_display = display;
510+
ChipLogProgress(DeviceLayer, "#72 gate: display ep%d %s", kDisplayEp, display ? "shown" : "HIDDEN"); }
511+
if (fm != g_fm) { ThermAttr::FeatureMap::Set(kAirconEp, fm); g_fm = fm;
512+
ChipLogProgress(DeviceLayer, "#72 gate: thermostat FeatureMap -> %u (%s)", (unsigned) fm,
513+
fm == MATTER_THERMOSTAT_FEATUREMAP_FULL ? "Heat+Cool+Auto" : "Cool-only"); }
514+
chip::DeviceLayer::PlatformMgr().UnlockChipStack();
515+
}
516+
483517
static void matter_driver_on_features(const HisenseFeatures *f)
484518
{
485519
ChipLogProgress(DeviceLayer,
@@ -495,6 +529,7 @@ static void matter_driver_on_features(const HisenseFeatures *f)
495529
ChipLogProgress(DeviceLayer,
496530
"A/C features (ext): unknown -- reply %uB, need >39B", (unsigned)f->reply_len);
497531
}
532+
apply_capability_gates(f); // #72: hide the per-unit unsupported surfaces
498533
}
499534

500535
// Driver link-health callback (#56, bus-task context) -> latch the state and post one

firmware/src/version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.3.25
1+
1.3.26

0 commit comments

Comments
 (0)