What differs between the backends
The hci backend stops scanning before it issues a connection and does not resume until the
attempt has settled. processConnectionQueue (lib/hci-socket/bindings.js:147-163):
if (this._isScanning) {
this.stopScanning();
return;
}
The dbus backend has no equivalent. _connect (lib/dbus/bindings.js:600-626) calls
iface.Connect() with discovery left running, and BlueZ resumes its own scan as soon as the
LL connection is reported complete — which is before the connection is actually usable.
From a user capture on a Raspberry Pi 5 (btsnoop, matterjs-server 1.4.0, NOBLE_BINDINGS=dbus):
40.035974 Sent LE Create Connection
40.244794 Rcvd LE Meta (LE Connection Complete) interval 48.75 ms, supervision 420 ms
40.248855 Sent LE Set Random Address
40.249210 Sent LE Set Scan Parameters Active, interval 11.25 ms, window 11.25 ms
40.249525 Sent LE Set Scan Enable ← 100% duty cycle, 5 ms into setup
40.249889 Sent LE Read Remote Features
40.584371 Rcvd LE Read Remote Features Complete Status: 0x3e
40.584776 Rcvd Disconnect Complete Reason: 0x3e
No ACL packet is ever exchanged. The link dies 334 ms after Connection Complete, which is
about six connection intervals — the Link Layer connection establishment window from Core
Spec Vol 6 Part B 4.5. BlueZ pauses discovery for the connect request on its own, but
resumes it the moment the controller reports Connection Complete, because the client never
asked it to stop.
What I am not claiming
I first assumed the resumed scan was the cause of the 0x3e. It is not, or at least not on
its own: a capture from a different user on the hci backend hits the same 0x3e with
scanning explicitly disabled.
577: LE Set Scan Enable -> Scanning: Disabled (0x00)
581: LE Create Connection
599: LE Connection Complete Status: Success (0x00)
617: Status: Connection Failed to be Established (0x3e)
625: LE Create Connection ← retry, succeeds
So this peripheral intermittently fails to complete establishment regardless of scan state,
and what rescues the hci user is the retry, not the quiet radio. Resuming a 100%-duty active
scan inside the establishment window on a single-radio controller plausibly raises the
failure rate, but I have no capture isolating it, and I would rather file this as a
behavioural inconsistency between the two backends than as a diagnosed root cause.
The retry asymmetry itself is a consumer-side issue, not noble's: a failed attempt correctly
emits connect(id, err) and no disconnect on both backends, and the consumer here only
retries on disconnect. That is being fixed on the matter.js side.
Proposal
Pause BlueZ discovery for the duration of a connect attempt in the dbus backend, so both
backends serialise scanning against connecting the same way.
Three constraints I would want any patch to respect:
- Reference counting. The dbus backend has no connection queue, and callers do connect
to several peripherals in parallel. Discovery should stop when the first attempt starts
and resume only when the last one finishes, and only if it was running to begin with.
- Restore on every exit path. A rejected or timed-out
Connect() must not leave
discovery switched off.
- Do not reuse
_stopScanning/_startScanning as they are. Those flip _isScanning,
run _unwatchAllDeviceProps(), emit scanStop/scanStart and re-run
GetManagedObjects(). Firing that around every connect would look like a full scan
restart to consumers and re-surface every cached device — one user on this path is already
reporting a flood of re-discovery events. The pause needs to be quiet: call BlueZ
StopDiscovery/StartDiscovery only, leaving noble's own scan state and watchers intact.
Two adjacent levers I checked and ruled out: SetDiscoveryFilter already sets
Transport: 'le' (lib/dbus/bindings.js:302-305), so there is no BR/EDR inquiry
interleaving to remove; and scan interval and window cannot be influenced at all, since
BlueZ owns them and setScanParameters is a deliberate no-op
(lib/dbus/bindings.js:279-282).
Happy to open a PR for this if you want it — it is small, and I would keep it to the pause
plus tests. Equally happy to leave it if you would rather not add moving parts to the dbus
connect path on the strength of a correlation.
Context: matter-js/matterjs-server#929.
What differs between the backends
The hci backend stops scanning before it issues a connection and does not resume until the
attempt has settled.
processConnectionQueue(lib/hci-socket/bindings.js:147-163):The dbus backend has no equivalent.
_connect(lib/dbus/bindings.js:600-626) callsiface.Connect()with discovery left running, and BlueZ resumes its own scan as soon as theLL connection is reported complete — which is before the connection is actually usable.
From a user capture on a Raspberry Pi 5 (btsnoop, matterjs-server 1.4.0,
NOBLE_BINDINGS=dbus):No ACL packet is ever exchanged. The link dies 334 ms after Connection Complete, which is
about six connection intervals — the Link Layer connection establishment window from Core
Spec Vol 6 Part B 4.5. BlueZ pauses discovery for the connect request on its own, but
resumes it the moment the controller reports Connection Complete, because the client never
asked it to stop.
What I am not claiming
I first assumed the resumed scan was the cause of the
0x3e. It is not, or at least not onits own: a capture from a different user on the hci backend hits the same
0x3ewithscanning explicitly disabled.
So this peripheral intermittently fails to complete establishment regardless of scan state,
and what rescues the hci user is the retry, not the quiet radio. Resuming a 100%-duty active
scan inside the establishment window on a single-radio controller plausibly raises the
failure rate, but I have no capture isolating it, and I would rather file this as a
behavioural inconsistency between the two backends than as a diagnosed root cause.
The retry asymmetry itself is a consumer-side issue, not noble's: a failed attempt correctly
emits
connect(id, err)and nodisconnecton both backends, and the consumer here onlyretries on
disconnect. That is being fixed on the matter.js side.Proposal
Pause BlueZ discovery for the duration of a connect attempt in the dbus backend, so both
backends serialise scanning against connecting the same way.
Three constraints I would want any patch to respect:
to several peripherals in parallel. Discovery should stop when the first attempt starts
and resume only when the last one finishes, and only if it was running to begin with.
Connect()must not leavediscovery switched off.
_stopScanning/_startScanningas they are. Those flip_isScanning,run
_unwatchAllDeviceProps(), emitscanStop/scanStartand re-runGetManagedObjects(). Firing that around every connect would look like a full scanrestart to consumers and re-surface every cached device — one user on this path is already
reporting a flood of re-discovery events. The pause needs to be quiet: call BlueZ
StopDiscovery/StartDiscoveryonly, leaving noble's own scan state and watchers intact.Two adjacent levers I checked and ruled out:
SetDiscoveryFilteralready setsTransport: 'le'(lib/dbus/bindings.js:302-305), so there is no BR/EDR inquiryinterleaving to remove; and scan interval and window cannot be influenced at all, since
BlueZ owns them and
setScanParametersis a deliberate no-op(
lib/dbus/bindings.js:279-282).Happy to open a PR for this if you want it — it is small, and I would keep it to the pause
plus tests. Equally happy to leave it if you would rather not add moving parts to the dbus
connect path on the strength of a correlation.
Context: matter-js/matterjs-server#929.