Skip to content

feat(ble): add presence gate for anonymous BLE adverts#111

Merged
chenchaoyi merged 2 commits into
mainfrom
feat/ble-presence-gate
May 21, 2026
Merged

feat(ble): add presence gate for anonymous BLE adverts#111
chenchaoyi merged 2 commits into
mainfrom
feat/ble-presence-gate

Conversation

@chenchaoyi

@chenchaoyi chenchaoyi commented May 21, 2026

Copy link
Copy Markdown
Owner

Summary

Two folded-together changes, both driven by the 2026-05-21 audit + the user's events-modal observation that anonymous Apple BLE adverts kept popping up at 0s even when nothing was moving nearby.

1. BLE presence gate (the main change)

Adds a configurable presence gate on BLEPoller. Anonymous BLE adverts (vendor + RSSI only, no name) now enter a PENDING state on first observation and only fire BLEDeviceSeenEvent after being observed for at least presence_gate_s seconds. If they age out via TTL before the gate matures, no event fires at all — neither seen nor left. Eliminates the single-packet seen_for=0s ghost flicker while preserving legitimate walk-bys (5-30 s contact, multi-packet, still register).

Named devices (helper-given name like Magic Keyboard, ccy iPhone 15 Pro Max, Z-GM0YXG5J) and connected peripherals (retrieveConnectedPeripherals) bypass the gate. The events panel must not lag the BLE list for paired peripherals — these are high-confidence by construction.

Default presence_gate_s = 5.0. CLI flag --ble-presence-gate DURATION (5s / 30s / 2m, or 0 to disable) + env var DITING_BLE_PRESENCE_GATE. CLI wins over env, blank env falls to default. 0 restores the A1 "record everything" contract for users with use cases that need it (security research, AirTag-spotting, brief-advertiser debugging).

State machine per identifier per session:

INIT ──(named OR _connected OR gate=0)──> PRESENT
INIT ──(anonymous advert, gate>0)──> PENDING
PENDING ──(gate elapsed, still in _devices)──> PRESENT  [emit seen]
PENDING ──(TTL eviction before gate)──> INIT  [silent drop, no events]
PRESENT ──(TTL eviction)──> DEPARTED  [emit left]
DEPARTED ──(re-arrival OR re-eviction)──> DEPARTED  (silent, per #107)

2. diting --help restructure (folded in per user request)

The previous --help flatlist mixed subcommands (once / watch / monitor / calibrate / analyze) with global flags (--lang / --log / --version / --help) under the same 2-space indent, hard to scan. New layout splits them into clearly-titled Subcommands and Global options sections.

Other cleanups:

  • --notify promoted from an inline mention under (no args) to a top-level Global options entry.
  • analyze gets a flags: --since DUR --for-llm [DIR] --anonymize line so the post-A2 / Track-B options are discoverable from --help alone, not just README.
  • Per-flag descriptions trimmed: 2-line summaries instead of 5-line paragraphs. Long-form documentation stays in README.

Both EN and ZH catalogs updated.

What's in this PR

  • openspec/changes/add-ble-presence-gate/ — proposal / design / tasks / bluetooth-scanning spec delta
  • src/diting/ble.pyBLEPoller(presence_gate_s=5.0), new _pending_seen map, re-shaped _detect_transitions
  • src/diting/cli.py_extract_ble_presence_gate_arg + _resolve_ble_presence_gate + restructured help text
  • src/diting/i18n.py — three new keys (gate help, env-var error messages) + restructured EN+ZH help catalog
  • src/diting/tui.pyDitingApp plumbs ble_presence_gate_s through to BLEPoller
  • tests/test_ble.py — 6 new gate tests
  • tests/test_cli.py — 10 new flag/env tests
  • tests/TESTING.md + docs/zh/TESTING.md — extended BLE row + new CLI-flag row, EN ↔ ZH parity
  • CHANGELOG.md + docs/zh/CHANGELOG.md — two ### Changed bullets (gate + help restructure), EN ↔ ZH parity
  • README.md + docs/zh/README.md — BLE bullet extended

Test plan

  • uv run pytest — 784 passed
  • uv run python scripts/tui_snapshot.py --mode regression — green
  • openspec validate --specs --strict — 21/21 passed
  • openspec validate add-ble-presence-gate --strict — valid
  • diting --help and DITING_LANG=zh diting --help both render the new layout
  • Real-environment smoke: launch a fresh session in dense BLE, confirm events panel no longer scrolls with (匿名) 0s flicker

Out of scope

RPA rotation dedup. A static nearby Apple device's Continuity address rotates every ~15 min; each rotation lasts long enough to graduate any reasonable gate, so the gate alone does not suppress these. Separate follow-up — anonymous-vendor coalescing by (vendor, ±10 dBm RSSI window) for the events stream, mirroring merge_for_display's logic.

Audit context

Findings file: /private/tmp/wfs-tui-audit-20260521-172708/findings.md. The user's specific complaint that motivated the gate: "没有人路过,但还是有 0s 的匿名 apple 设备不断出现." The 0s seen_for_seconds on the paired BLEDeviceLeftEvent is the giveaway — single-packet detection from edge-of-range / transient sources. A 5 s gate crushes those without rejecting real-but-brief walk-bys.

Generated with Claude Code

chenchaoyi and others added 2 commits May 21, 2026 19:53
Adds a configurable presence gate on BLEPoller. Anonymous BLE
adverts (vendor + RSSI only, no `name`) enter PENDING on first
observation; they only fire BLEDeviceSeenEvent after being
observed for at least `presence_gate_s` seconds. If they age
out via TTL before the gate matures, no event fires at all --
no seen, no left. This eliminates the single-packet
`seen_for=0s` ghost flicker the user observed in dense RF
environments without giving up on legitimate walk-bys (5-30s
contact, multi-packet, still register).

Named devices (helper-given `name` like "Magic Keyboard",
"ccy iPhone 15 Pro Max", "Z-GM0YXG5J") and connected
peripherals (CBPeripheral.retrieveConnectedPeripherals) bypass
the gate -- they're high-confidence by construction and the
events panel must not lag the BLE list for paired peripherals.

Default `presence_gate_s = 5.0`. CLI flag
`--ble-presence-gate DURATION` (5s / 30s / 2m, or `0` to
disable) + env var `DITING_BLE_PRESENCE_GATE` mirror the
existing DITING_LANG / DITING_LOG resolution pattern (CLI wins
over env, blank env falls to default). `0` restores the A1
"record everything" contract for users with use cases that
need it (security research, AirTag-spotting, brief-advertiser
debugging).

State machine per identifier per session:

  INIT --(named OR _connected OR gate=0)--> PRESENT
  INIT --(anonymous advert, gate>0)--> PENDING
  PENDING --(gate elapsed, still in _devices)--> PRESENT [emit seen]
  PENDING --(TTL eviction before gate)--> INIT [silent drop]
  PRESENT --(TTL eviction)--> DEPARTED [emit left]
  DEPARTED --(re-arrival OR re-eviction)--> DEPARTED (silent)
    [previously handled by fix-ble-left-dedup #107]

Spec: bluetooth-scanning MODIFIED requirement: the
"No debounce SHALL be applied" sentence is replaced with the
gated semantics + bypass rules; new "Anonymous advert below
presence-gate is silent" / "graduates after gate elapses" /
"gate=0 restores no-debounce" scenarios. 21/21 specs validate.

Tests: 6 new BLE-poller tests + 10 new CLI tests covering
parse forms, env-var fallback, blank-env-as-default, invalid
input. 784 pytest pass. Snapshot regression green.

Out of scope: RPA rotation dedup (a static Apple device's
Continuity address rotates every ~15min; each rotation lasts
long enough to graduate any reasonable gate, so the gate
alone doesn't suppress these). Separate proposal in the
audit follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Folded into #111 per user request. The previous --help flatlist
mixed subcommands (once / watch / monitor / calibrate / analyze)
with global flags (--lang / --log / --version / --help) under
the same 2-space indent, hard to scan. New layout splits them
into two clearly-titled sections.

Other cleanups:
- --notify promoted from an inline mention under (no args) to a
  top-level Global options entry.
- analyze gets a "flags: --since DUR  --for-llm [DIR]
  --anonymize" line so the post-A2 / Track-B options are
  discoverable from the help text alone, not just README.
- Per-flag descriptions trimmed: 2-line summaries instead of
  5-line paragraphs. The long-form documentation stays in
  README, where it belongs.

Both EN catalog key and ZH value updated; rendered both via
`diting --help` and `DITING_LANG=zh diting --help` to confirm.

784 pytest pass, snapshot regression clean, openspec validate
green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chenchaoyi chenchaoyi merged commit 4f3f63a into main May 21, 2026
5 checks passed
chenchaoyi added a commit that referenced this pull request May 21, 2026
Applies the MODIFIED transition-events requirement from
add-ble-presence-gate (#111) into canonical
openspec/specs/bluetooth-scanning/spec.md. The "No debounce
SHALL be applied" sentence is replaced with the PENDING /
PRESENT / DEPARTED gated semantics + named-bypass +
connected-bypass rules; three new scenarios cover the gate
paths (anonymous-below-gate-is-silent, graduates-after-gate,
gate-zero-restores-no-debounce). Moves the change dir to
openspec/changes/archive/2026-05-21-add-ble-presence-gate/.

All artifacts done, all tasks complete, validate --specs
--strict green (21/21).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chenchaoyi chenchaoyi mentioned this pull request May 22, 2026
6 tasks
@chenchaoyi chenchaoyi deleted the feat/ble-presence-gate branch June 5, 2026 10:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant