feat(ble): add presence gate for anonymous BLE adverts#111
Merged
Conversation
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>
2 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, noname) now enter a PENDING state on first observation and only fireBLEDeviceSeenEventafter being observed for at leastpresence_gate_sseconds. If they age out via TTL before the gate matures, no event fires at all — neitherseennorleft. Eliminates the single-packetseen_for=0sghost flicker while preserving legitimate walk-bys (5-30 s contact, multi-packet, still register).Named devices (helper-given
namelikeMagic 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, or0to disable) + env varDITING_BLE_PRESENCE_GATE. CLI wins over env, blank env falls to default.0restores 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:
2.
diting --helprestructure (folded in per user request)The previous
--helpflatlist 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:
--notifypromoted from an inline mention under(no args)to a top-level Global options entry.analyzegets aflags: --since DUR --for-llm [DIR] --anonymizeline so the post-A2 / Track-B options are discoverable from--helpalone, not just README.Both EN and ZH catalogs updated.
What's in this PR
openspec/changes/add-ble-presence-gate/— proposal / design / tasks /bluetooth-scanningspec deltasrc/diting/ble.py—BLEPoller(presence_gate_s=5.0), new_pending_seenmap, re-shaped_detect_transitionssrc/diting/cli.py—_extract_ble_presence_gate_arg+_resolve_ble_presence_gate+ restructured help textsrc/diting/i18n.py— three new keys (gate help, env-var error messages) + restructured EN+ZH help catalogsrc/diting/tui.py—DitingAppplumbsble_presence_gate_sthrough toBLEPollertests/test_ble.py— 6 new gate teststests/test_cli.py— 10 new flag/env teststests/TESTING.md+docs/zh/TESTING.md— extended BLE row + new CLI-flag row, EN ↔ ZH parityCHANGELOG.md+docs/zh/CHANGELOG.md— two### Changedbullets (gate + help restructure), EN ↔ ZH parityREADME.md+docs/zh/README.md— BLE bullet extendedTest plan
uv run pytest— 784 passeduv run python scripts/tui_snapshot.py --mode regression— greenopenspec validate --specs --strict— 21/21 passedopenspec validate add-ble-presence-gate --strict— validditing --helpandDITING_LANG=zh diting --helpboth render the new layout(匿名) 0sflickerOut 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, mirroringmerge_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 0sseen_for_secondson the pairedBLEDeviceLeftEventis 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