UltraWideLock v0.4.0

Approach Direction#

How the Apple Home app's "Approach Direction" control is exposed by an Aliro lock, and every trap hit making it appear on the ESP32-S3 port. Validated on silicon 2026-07-21: the control renders and its Left/Front/Right selection round-trips to the device.

note

Convention: VERIFIED = confirmed on silicon or against SDK source; INFERRED = consistent with observation but not directly proven.

---

1. It is not a standard Matter attribute#

VERIFIED. No attribute of the standard Door Lock cluster (0x0101) carries approach direction through spec revision 1.6, neither in the Aliro attribute block (0x0080-0x0088) nor anywhere else. That emptiness is not evidence the feature does not exist.

The control is driven by a manufacturer-specific cluster using a Matter MEI: vendor ID in the upper 16 bits, cluster ID in the lower 16. Manufacturer clusters live in a separate ID space a search of the standard data model cannot reach.

Vendor 0x1349 is Apple (CHIPVendorIdentifiers.hpp:46), so the cluster is 0x1349FC03.

2. The descriptor#

VERIFIED on silicon with exactly these values, and against the type tables in chip-types.xml and attribute-metadata.h.

Cluster 0x1349FC03, mask 0x40 (server), on the door lock endpoint, the same endpoint as the Door Lock cluster, not endpoint 0. Three attributes, 7 bytes:

AttributeSizeTypeMaskDefault
0x0000 direction10x18 bitmap80x03 writable\nonvolatile7
0xFFFC FeatureMap40x1B bitmap3200
0xFFFD ClusterRevision20x21 int16u01

Type codes per chip-types.xml lines 28/30/33; mask bits per attribute-metadata.h:110,112 (MATTER_ATTRIBUTE_FLAG_WRITABLE = 0x01, ..._NONVOLATILE = 0x02).

Two easy mistakes:

  • The direction attribute is a bitmap8, not int8u: type code 0x18, not 0x20.
  • The default 7 is 0b111, all three directions permitted, matching Home's "unlock when you approach from any direction". Which single bit means Left versus Right is still unknown, and nothing in the port depends on it.

3. Missing global attributes fail silently, then loudly#

VERIFIED, cost three pairing cycles.

FeatureMap and ClusterRevision are mandatory on every Matter cluster, and esp_matter's cluster::create() emits neither; every generated cluster in the SDK adds them explicitly (any file under data_model/generated/clusters/).

Declaring the cluster with only the direction attribute produces one that cannot answer ClusterRevision:

  1. Home runs a complete commissioning (PASE, attestation, CSR request, AddNOC, CommissioningComplete) on two fabrics
  2. Roughly 0.5 s later it sends RemoveFabric for its own fabric
  3. The UI says "Unable to Add Accessory"

Nothing in the device log errors. The read Home makes immediately before bailing succeeds, so the rejection rests on a value it received, not an error status.

How to recognise it: OpCreds: Received a RemoveFabric Command in the log after a successful Commissioning completed successfully. Accepted then rejected is a data-model problem, not connectivity or crypto.

Fix:

c
cluster_t *c = cluster::create(endpoint, kApproachDirectionClusterId, CLUSTER_FLAG_SERVER);
attribute::create(c, 0x0000, ATTRIBUTE_FLAG_WRITABLE | ATTRIBUTE_FLAG_NONVOLATILE,
                  esp_matter_bitmap8(7));
cluster::global::attribute::create_feature_map(c, 0);
cluster::global::attribute::create_cluster_revision(c, 1);

create_feature_map / create_cluster_revision are declared in generated/esp_matter_data_model_utils.h:62, already pulled in by esp_matter.h:25.

4. The endpoint descriptor is cached at commissioning#

VERIFIED. A controller reads the endpoint's cluster list once, at commissioning, and caches it. Adding a cluster to an already-paired device changes nothing, however many times the app is restarted. Any newly declared cluster requires removing the accessory and adding it again.

5. Removal and re-pairing traps#

Three ways to end up unable to re-add a device, all seen on the bench:

5.1 Removing while the device is unreachable. Removing an accessory sends RemoveFabric to the device. If it is offline, the controller removes it locally and the device never hears: it still boots with Fabric already commissioned. Disabling BLE advertisement and cannot be discovered.

5.2 Multiple fabrics. A device commissioned into Apple Home typically holds two fabrics. Removing the accessory in the app clears one; the device stays commissioned on the other, so it still will not advertise.

On ESP32 the fallback is a device-side factory reset, which clears every fabric at once: matter> factoryreset. If the console is unresponsive, erase only the nvs partition rather than the whole flash, so esp_secure_cert and fctry survive. The DWM3001CDK stack supports authenticated, durable, per-fabric removal; use a surviving controller's Manage fabrics action there and reserve SW2 factory reset for a node no administrator can reach.

5.3 Trust-store exhaustion. Tangential, but it bites during repeated pairing cycles. A Matter factory reset does not touch the reader's own provisioning namespace, and nothing evicts superseded credentials, so each re-pair burns a slot. Once the store is at ULTRAWIDELOCK_TRUST_MAX, the reader verifies the phone's signature and then rejects the credential:

device signature OK
credential key NOT trusted (not in trust store); rejecting

ultrawidelock trust reports FAILED (trust store full or NVS error), which covers three causes and does not say which. Run ultrawidelock prov for the true count and ultrawidelock clear to empty the store.

6. Red herring: cluster 0x1349FC00#

VERIFIED as a dead end. During commissioning, Home reads clusterId 0x1349FC00, attributeId 0x0001 and the device answers err = 5c3.

This port renders the Approach Direction control while still answering UNSUPPORTED_CLUSTER for 0x1349FC00, so that cluster does not gate the control. Do not spend build cycles guessing at its datatype.

7. Error codes in these logs#

Interaction Model status codes appear in device logs as err = 0x500 + status:

Log valueStatusMeaning
5c30xc3UNSUPPORTED_CLUSTER
5860x86UNSUPPORTED_ATTRIBUTE

Per src/protocols/interaction_model/StatusCodeList.h. UNSUPPORTED_CLUSTER on a manufacturer cluster during commissioning is normal probing, not a problem: the device answers the same way for optional standard clusters it does not implement, such as ICD Management (0x0046).

8. The control is cosmetic on single-antenna hardware#

VERIFIED. Nothing gates an unlock on the stored value. Measuring approach direction requires angle of arrival, which needs a dual-antenna UWB part; the DW3110 on the DWM3000EVB has a single antenna and cannot produce it.

The attribute is stored, reported, and round-trips to the app. The behaviour behind it is not implemented, and on this hardware cannot be.

9. Port coverage#

PortStatus
apps/esp32-matter-lockimplemented: runtime cluster::create() in app_main.cpp
nRF5340 door lock appimplemented: static tables in integrations/nrfconnect-door-lock/matter-aliro-door-lock-app/src/matter/zap_uwb/

The ESP32 port constructs endpoints at runtime, so the cluster is a few lines of C++. The nRF app's data model is static tables compiled into the image.

9.1 The nRF data model is not regenerated at build time#

VERIFIED. The .zap file is not the source of truth on this build. ncs_configure_data_model passes BYPASS_IDL to chip_configure_data_model, which takes the branch that skips the chip_zapgen target and instead adds the checked-in zap-generated/ directory as an include path. endpoint_config.h appears nowhere in build.ninja as a build rule. Editing lock.zap alone changes nothing in the image.

The variant in use is selected by src/matter/CMakeLists.txt: with CONFIG_NCS_SAMPLE_MATTER_ZAP_FILE_PATH unset and CONFIG_DOOR_LOCK_BLE_UWB=y, that is zap_uwb/. Read it from a configured build's .config, not from Kconfig defaults.

So the cluster is added by patching the generated endpoint_config.h: three attribute records, one cluster record, and the counters that describe them.

9.2 The counters must stay consistent with the tables#

The static tables are index-addressed, and four #defines must agree with them: GENERATED_ATTRIBUTE_COUNT, GENERATED_CLUSTER_COUNT, ZAP_FIXED_ENDPOINT_DATA_VERSION_COUNT and ATTRIBUTE_MAX_SIZE, plus the per-endpoint GENERATED_ENDPOINT_TYPES row { cluster index, cluster count, endpoint size }.

Two structural rules make the edit an append rather than a renumbering:

  • A cluster's attributes must be contiguous in the attribute table, and an endpoint's clusters contiguous in the cluster table. The door lock endpoint's clusters are already last, so a new cluster appends at the end of both tables without moving anything.
  • clusterSize is the sum of the sizes of that cluster's attributes that are not EXTERNAL_STORAGE, and an endpoint's size is the sum of its clusters' sizes. All three new attributes are RAM-backed, so the cluster adds 1 + 4 + 2 = 7 bytes, and both the endpoint row and ATTRIBUTE_MAX_SIZE grow by 7.

Every counter can be re-derived from the tables in the same file, which catches an arithmetic slip before a build.

This path gets one thing free: FeatureMap and ClusterRevision are written out per cluster like any other attribute, so §3 cannot occur here as an omission by a helper.

9.3 It used to collide with the opt-in Home Assistant data model#

The Home Assistant variant adds a third endpoint and bumps the same counter lines. While both were patches into the fetched application, two could not both change GENERATED_ATTRIBUTE_COUNT 205 in either order, so the stack had to be cumulative: Approach Direction applied first, the HA patches cut against a tree that already had it, and tests/tooling/patch_drift_check.sh held the ordering.

Neither is a patch now. Both data models are complete files in integrations/nrfconnect-door-lock/matter-aliro-door-lock-app/src/matter/, zap_uwb and zap_uwb_ha, and CONFIG_ULTRAWIDELOCK_HA picks one. Each carries its own counters, so there is no composition left to break; both must be regenerated when the data model changes.

That HA=1 patch stack is unrelated to Matter multi-admin sharing on the DWM3001CDK. The DWM image needs no Home Assistant build variant.