diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-11 00:54:56 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-11 00:54:56 -0500 |
| commit | 70d2ebcad0fbc43b1b80d4bfff3d8eddeda4a66b (patch) | |
| tree | ef67e0ccd1ba8e1cb66e97b65bbc3f905a83bb7d /docs/specs/2026-07-11-bt-doctor-expansion-spec.org | |
| parent | 7ac0c363c830840090ad81b5e9f07f350c6f6e57 (diff) | |
| download | archsetup-70d2ebcad0fbc43b1b80d4bfff3d8eddeda4a66b.tar.gz archsetup-70d2ebcad0fbc43b1b80d4bfff3d8eddeda4a66b.zip | |
docs: catalogue net + bt failure modes and draft two doctor expansion specs
I ran the audio doctor's design arc for the net and bluetooth doctors: a blind by-layer research catalogue of failure modes, a symptom-cluster triage, then a spec per doctor.
The taxonomy holds ~74 network and ~55 bluetooth distinct root causes, each sourced to a forum or issue-tracker report, sorted into eight network and five bluetooth symptom clusters. The clusters are keyed to each doctor's existing probe ladder, so the fix-versus-guide boundary falls along the tiers the doctor already walks. Every entry carries a remedy class (auto, privileged, reboot-tail, or guide), reusing the audio doctor's four-class run-time privilege model.
The two specs are expansions, not rewrites. The net doctor is already the most mature of the three: its classifier reaches six of the eight clusters today, so its spec adds only the control-plane cluster (a rival network manager, a masked NetworkManager, a bad keyfile) and sharper auth naming. The bt doctor's chain is structurally right but blind at both ends, so its spec adds a probe that names the firmware blob the kernel already logged and a probe that catches an adapter that power-on won't keep on across reboots. Both stay DRAFT with three open decisions each, headed for spec-review.
Diffstat (limited to 'docs/specs/2026-07-11-bt-doctor-expansion-spec.org')
| -rw-r--r-- | docs/specs/2026-07-11-bt-doctor-expansion-spec.org | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/docs/specs/2026-07-11-bt-doctor-expansion-spec.org b/docs/specs/2026-07-11-bt-doctor-expansion-spec.org new file mode 100644 index 0000000..0775a18 --- /dev/null +++ b/docs/specs/2026-07-11-bt-doctor-expansion-spec.org @@ -0,0 +1,184 @@ +#+TITLE: Bt Doctor Expansion — name the blob, catch the boot-disable +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-11 +#+TODO: TODO | DONE +#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED + +* DRAFT Bt Doctor Expansion +:PROPERTIES: +:ID: 3d4d61c4-e5df-44e9-b8e0-40b31452c3f7 +:END: +- [2026-07-11 Sat @ 00:08:41 -0500] DRAFT — drafted. Extends the existing bt doctor (=~/.dotfiles/bluetooth/=, shipped) using the bluetooth half of the failure taxonomy ([[file:../design/2026-07-10-net-bt-failure-taxonomy.org][2026-07-10-net-bt-failure-taxonomy.org]]). Grounded in a read of the live engine, not memory. + +* Metadata + +| Field | Value | +|----------+-----------------------------------------------------------------------------------| +| Status | draft | +|----------+-----------------------------------------------------------------------------------| +| Owner | Craig Jennings | +|----------+-----------------------------------------------------------------------------------| +| Reviewer | Craig Jennings | +|----------+-----------------------------------------------------------------------------------| +| Related | [[file:../design/2026-07-10-net-bt-failure-taxonomy.org][net/bt failure taxonomy]] ; the cross-panel run-time-privilege and copy+close tasks | +|----------+-----------------------------------------------------------------------------------| + +* Summary + +The bt doctor walks a clean chain — adapter → rfkill → bluetooth.service → powered → per-device → audio profile — and applies four safe repairs (unblock, power-on, service-restart, a2dp). The failure taxonomy shows the chain is structurally right but blind in two specific spots the user hits most: when there is no adapter it says "no adapter found" without naming *why* (a missing firmware blob the dmesg log already names), and it never checks whether the adapter is configured to power on at boot, so an =AutoEnable=false= laptop that is dead every login reads as a healthy powered-off adapter. This spec adds those two probes and adopts the cross-panel privilege model. It is an expansion of a working doctor. + +* Problem / Context + +The taxonomy sorted ~55 real bluetooth failure modes into five clusters keyed to the doctor's chain. Read against the live engine (=~/.dotfiles/bluetooth/src/bt/doctor.py=), the doctor already handles cluster 2 well (unblock, service-restart, power-on are its exact auto-fix tiers) and cluster 5 partially (the =a2dp= repair forces the A2DP profile for a card stuck in HSP/HFP or with no sink). Clusters 3 (pairing) and 4 (connection stability) are deliberately light — the doctor treats connecting and pairing as user intents, not health repairs, and that is a correct design choice, not a gap. The two real gaps are at the ends of the chain. + +** "No adapter found" doesn't say why (cluster 1) + +=_adapter_step= reports a missing adapter as "no Bluetooth adapter found (hardware/driver)" and stops. But the taxonomy's cluster 1 is almost entirely *firmware* faults, and the kernel already logged the specific cause: "Direct firmware load for intel/ibt-…sfi failed", "mediatek/BT_RAM_CODE_MT7961… failed", "BCM: Patch …hcd not found", "Reading QCA version information failed". The doctor has that log a =journalctl -k= read away and doesn't consult it. A user who sees "no adapter — hardware/driver" is left to search; a user who sees "the Intel firmware blob ibt-20-1-4.sfi failed to load — update linux-firmware and reboot" has the fix. This is the direct parallel to the audio doctor's cluster-1 dmesg-hint proposal, and the same read-only, bounded shape. + +** A boot-disabled adapter reads as merely powered-off (cluster 2) + +=_powered_step= checks whether the adapter is powered *now* and offers =power-on=. But =power-on= via bluetoothctl doesn't persist: on an =AutoEnable=false= machine (bluez's static default when main.conf is absent), or a TLP laptop that disables bluetooth on startup, the adapter is dead again next boot. The doctor fixes the symptom every session and never names the cause. The taxonomy's cluster 2 has a whole sub-family here — AutoEnable off, service-not-enabled-at-boot, TLP-disables-on-startup, systemd-rfkill-restores-a-stale-block — that all present as "powered off / blocked" and all need a *persistent* fix the doctor doesn't distinguish from a one-shot power-on. + +** Pairing and connection clusters stay light on purpose (clusters 3, 4) + +The doctor doesn't auto-pair or auto-connect, and the destructive re-pair is always user-confirmed. That is right. The one addition worth considering is a *stale-bond signature* — a device that fails to connect with a bond present, repeatedly — so the doctor can *offer* the re-pair with confidence instead of the user guessing. That is a candidate, held as a decision, not a v1 commitment. The rest of clusters 3/4 (physical range, USB3 noise, coexistence, connection parameters) are Guide-only and mostly out of a health doctor's reach. + +* Goals and Non-Goals + +** Goals + +- When there is no adapter, name the specific firmware/driver cause by reading the kernel log, and print the matching fix — instead of a generic "hardware/driver." +- Distinguish a *persistently* disabled adapter (AutoEnable off, service not enabled at boot, TLP-disabled) from a merely powered-off one, and offer the persistent fix rather than a one-shot power-on that dies next boot. +- Adopt the cross-panel run-time privilege model for the new root-needing repairs (editing main.conf's AutoEnable, =systemctl enable=, editing tlp.conf), under the Confirm/Arm-default floor. + +** Non-Goals + +- Auto-pairing or auto-connecting devices. Connecting and pairing are user intents; the doctor's existing stance stands. +- Fixing the firmware itself. The dmesg-hint probe *names* the missing blob and prints the update/reboot instruction; it does not install firmware or rebuild initramfs inside a diagnose run (that is a Reboot-tail Guide the user runs). +- The physical/coexistence connection-stability faults (USB3 noise, range, 2.4GHz coexistence, connection parameters). These are Guide-only and stay out of the health chain. +- Silent privileged action. Editing main.conf, enabling the service, or touching tlp.conf all default to Confirm/Arm, never silent Auto. + +** Scope tiers + +- *v1:* the dmesg firmware-hint probe for the no-adapter case (cluster 1); the boot-enablement probe distinguishing AutoEnable-off / service-not-enabled / TLP-disabled from a plain power-off (cluster 2), with the persistent fix; adoption of the run-time privilege model. +- *Out of scope:* auto-pair/auto-connect; firmware installation inside a run; the physical/coexistence connection faults. +- *vNext:* the stale-bond re-pair-offer signature (cluster 3, pending the decision below); a connection-parameter/coexistence hint tail for a "keeps dropping" verdict (cluster 4); the bluetooth-specific audio-profile expansion beyond the current a2dp repair (codec fallback, default-sink, absolute-volume — several overlap the audio taxonomy and want coordination with the audio doctor). All logged to =todo.org=. + +* Design + +** For the user + +Two verdicts that today under-inform start telling the user the actual cause. + +When there is no adapter, the wall stops at "no adapter" no longer. It names the blob and the fix: "the MediaTek BT firmware (BT_RAM_CODE_MT7961) failed to load — update linux-firmware and reboot," pulled straight from the kernel log the doctor now reads. When the log shows a clean absence (no firmware error, genuinely no controller), it says that instead — the same fail-honest distinction the audio doctor draws between "the server lost a device" and "nothing is attached." + +When the adapter is powered off, the doctor asks a second question: is it *supposed* to be on at boot? If AutoEnable is off, or bluetooth.service isn't enabled, or TLP is disabling it on startup, the verdict names that — "your adapter powers off every boot because AutoEnable is false" — and FIX offers the persistent fix (set AutoEnable, enable the service) rather than a power-on that will be undone by the next reboot. A plain "it's off right now" still gets the quick power-on. + +** For the implementer + +*** The dmesg firmware-hint probe + +When =_adapter_step= finds no adapter, run a bounded =journalctl -k -b --no-pager= (or =dmesg=) read, scanned for the known firmware-load-failure signatures per vendor (Intel ibt-*.sfi, MediaTek BT_RAM_CODE, Realtek rtl_bt, Broadcom BCM .hcd, Qualcomm QCA version-read). A match yields a specific =no-adapter-firmware= verdict naming the blob and the Reboot-tail Guide (update linux-firmware / symlink the blob / reboot). No match yields the existing generic =no-adapter=. The read is bounded and read-only; it runs only in the no-adapter branch, so it costs nothing on a healthy adapter. + +*** The boot-enablement probe + +When =_powered_step= finds the adapter powered off (or =_rfkill_step= finds a soft-block), consult three persistence signals: bluez =AutoEnable= (parse =/etc/bluetooth/main.conf= [Policy], defaulting to the bluez static default when absent), =systemctl is-enabled bluetooth=, and whether TLP's =DEVICES_TO_DISABLE_ON_STARTUP= lists bluetooth. A powered-off adapter with a persistence fault gets a =powered-off-persistent= verdict distinct from the plain =powered-off=; its fix is the persistent one (set AutoEnable / enable the service / edit tlp.conf), Privileged/Confirm. The existing =power-on= stays for the plain case. + +*** The privilege model + +The new repairs — editing main.conf, =systemctl enable bluetooth=, editing tlp.conf — are root-needing and adopt the cross-panel run-time model: run where passwordless sudo exists, prompt on a tty, default to Confirm/Arm, never silent Auto. Same shared implementation as the net and audio doctors. The existing four auto-fix tiers (unblock, power-on, service-restart, a2dp) are unchanged — they are already user-scope or already the doctor's safe tier. + +* Alternatives Considered + +** Print a generic "check firmware" hint without reading the log + +- Good, because it needs no journal read. +- Bad, because "check your firmware" is exactly the uselessly-generic verdict this fixes; the whole value is naming the specific blob the kernel already identified. +- Rejected. The log has the answer; read it. + +** Make power-on always persistent (set AutoEnable on every power-on) + +- Good, because the adapter would then stay on across boots without a separate verdict. +- Bad, because it conflates a deliberate boot-disable (a user or TLP choosing bluetooth-off-by-default for battery) with a fault, and silently overrides a policy the user may have set on purpose. Persistence is a distinct decision that deserves a distinct, confirmed action. +- Rejected in favor of a separate verdict the user confirms. + +** Build the stale-bond re-pair signature into v1 + +- Good, because it would let the doctor offer re-pair with confidence. +- Bad, because re-pair is destructive (removes the bond) and the signature ("fails to connect with a bond present, repeatedly") needs care to not fire on a merely-out-of-range device; getting it wrong offers a destructive fix for a transient condition. +- Held as an open decision, not a v1 commitment. + +* Decisions [0/3] + +** TODO The dmesg firmware-hint probe +Context: cluster 1 is almost all firmware faults the kernel already logs, and the doctor reports a generic "hardware/driver" instead of the named blob. +Decision (proposed): we will read the kernel log in the no-adapter branch, match the known per-vendor firmware-load-failure signatures, and emit a =no-adapter-firmware= verdict naming the blob and its Reboot-tail Guide; no match falls back to the generic verdict. +Consequences: the most common no-adapter cause becomes self-explaining; harder — a per-vendor signature table to maintain, and the read must stay bounded and only run in the no-adapter branch so it never slows a healthy run. +Owner: Craig. By: before Phase 1 lands. + +** TODO The boot-enablement probe and the persistent-vs-transient split +Context: =power-on= doesn't persist, so an AutoEnable-off / not-enabled / TLP-disabled adapter is dead every boot and the doctor fixes only the symptom. +Decision (proposed): we will add a persistence probe (AutoEnable, service-enabled, TLP) and a =powered-off-persistent= verdict distinct from =powered-off=, whose fix is the persistent one under the Confirm floor; the plain power-on stays for the transient case. +Consequences: the boot-disable cause is named and fixed once instead of every session; harder — the doctor must not override a *deliberate* boot-disable, so the persistent fix is always confirmed and the verdict names the specific persistence cause rather than blanket-enabling. +Owner: Craig. By: before Phase 2 lands. + +** TODO Whether the stale-bond re-pair-offer lands in v1 or vNext +Context: a device failing to connect with a bond present, repeatedly, is a re-pair candidate — but re-pair is destructive and the signature can misfire on an out-of-range device. +Decision (proposed): defer to vNext. v1 keeps re-pair strictly user-initiated; the signature is designed and validated before the doctor ever *offers* it. +Consequences: no risk of the doctor offering a destructive fix for a transient condition in v1; harder — the "keeps failing to connect" case stays unnamed for now, which is the one cluster-3 gap a user might reasonably expect the doctor to catch. +Owner: Craig. By: before Phase 1 (scope confirmation). + +* Implementation phases + +Each phase leaves the tree green and independently useful, as the existing bt phases did. + +** TODO Phase 0 — the two read-only probes +Pure engine, no repair changes. The dmesg firmware-hint reader (no-adapter branch) and the boot-enablement reader (AutoEnable / service-enabled / TLP), both reporting into the diagnose report. =bt diag --json= (or the equivalent) shows the new signals. Fakes: a canned journal buffer with each vendor's signature, and injected main.conf / =systemctl is-enabled= / tlp.conf states. + +** TODO Phase 1 — the firmware-hint verdict + privilege model +=diagnose= emits =no-adapter-firmware= (naming the blob, Reboot-tail Guide) when the signature matches. The run-time privilege resolution lands (shared cross-panel code). No auto-fix — this verdict is a Guide. + +** TODO Phase 2 — the persistent-power verdict and its fix +=powered-off-persistent= distinct from =powered-off=; the persistent fix (set AutoEnable / enable service / tlp) registers Privileged/Confirm. =bt doctor --fix= applies it under the Confirm floor; the plain power-on path is unchanged. + +** TODO Phase 3 — flip this spec to IMPLEMENTED +And log the vNext items (stale-bond signature, connection-parameter hints, bt-audio-profile expansion) to =todo.org=. + +* Acceptance criteria + +- [ ] With no adapter and a MediaTek/Intel/Realtek/Broadcom/Qualcomm firmware-load error in the kernel log, the doctor names the specific blob and prints the update-and-reboot Guide. +- [ ] With no adapter and no firmware error in the log, the doctor reports the generic "no controller attached" without inventing a firmware cause. +- [ ] An adapter that is powered off with AutoEnable=false reports =powered-off-persistent= and FIX offers the persistent fix, not just a one-shot power-on. +- [ ] An adapter powered off with AutoEnable on (a transient off) still gets the quick power-on. +- [ ] Every new root-needing repair defaults to Confirm/Arm and never runs silently as Auto. +- [ ] The existing chain (rfkill/service/powered/device/audio) and its four auto-fix tiers classify and repair exactly as today (regression). + +* Readiness dimensions + +- *Data model & ownership* — the diagnose report gains a firmware-hint field (generated from the kernel log) and boot-persistence signals (generated from main.conf / systemctl / tlp.conf). The doctor writes main.conf / tlp.conf / the service enablement only under the Confirm floor. +- *Errors, empty states & failure* — an unreadable journal or main.conf yields "unknown," never a false firmware or persistence verdict. A no-match firmware scan falls back to the existing generic verdict (the safe direction). +- *Security & privacy* — device MACs are already redacted by =redact.py=; the new probes read the kernel log and config files, no secrets. The firmware-signature scan must not surface unrelated journal content. +- *Observability* — the wall names the blob and the persistence cause. The =--json= output carries the new signals. +- *Performance & scale* — the journal read runs only in the no-adapter branch; the persistence read only when powered-off/blocked. Neither touches a healthy run. +- *Reuse & lost opportunities* — reuses the existing chain, =btctl=, =sysio=, =priv.py=, and the shared cross-panel privilege model rather than a bt-local one. =diagnose= stays the single report authority. +- *Architecture fit* — both probes are additive inside existing chain branches (=_adapter_step=, =_powered_step=), not new chain links. Weak point: the firmware-signature table is vendor-specific and will drift as new hardware appears; it degrades to the generic verdict rather than mis-naming, which bounds the risk. +- *Config surface* — none new for the doctor; it *reads* bluez/tlp config and *writes* it only as a confirmed repair. N/A for its own knobs. +- *Documentation plan* — module docstrings, as the package does today. The wall is the user documentation. +- *Dev tooling* — =make test= and the bt panel smoke cover it; the new probes need a canned journal fixture and injected config states, fixture shapes the package can adopt. +- *Rollout, compatibility & rollback* — additive; the existing chain is untouched. The persistent fixes change bluez/tlp config and service enablement, so all are Confirm-tier and reversible by the user. +- *External APIs & deps* — =journalctl -k=/=dmesg=, =/etc/bluetooth/main.conf=, =systemctl is-enabled=, and =/etc/tlp.conf= layouts are verified against the live system before Phase 0. The per-vendor firmware signatures come from the taxonomy's cluster-1 sources; no new packages. + +* Risks, rabbit holes, and drawbacks + +The firmware-signature table is the maintenance rabbit hole: each vendor logs its failure differently and new hardware adds new strings. The mitigation is that a miss degrades to the existing generic verdict — the doctor is never *worse* than today, only sometimes not-better — so the table can grow incrementally without a correctness cliff. + +The persistent-power fix must not fight a deliberate choice. A user (or TLP on a battery-conscious laptop) may want bluetooth off by default; blanket-enabling AutoEnable would override that silently. The verdict names the specific persistence cause and the fix is always confirmed, so the user sees exactly what would change before it does. + +The bt-audio-profile expansion (vNext) overlaps the audio taxonomy's Bluetooth-mic cluster. When it is picked up, it needs coordination with the audio doctor so the two panels don't both claim the same A2DP/HFP diagnosis with divergent verdicts. Named here so the seam is known. + +* Review and iteration history + +** 2026-07-11 Sat @ 00:08:41 -0500 — Craig Jennings — Author +- What: drafted the bt doctor expansion from the bluetooth half of the failure taxonomy. v1 adds the dmesg firmware-hint probe (cluster 1) and the boot-enablement probe (cluster 2); the pairing/connection/audio expansions are staged to vNext. +- Why: the taxonomy showed the doctor's chain is structurally right but blind at its ends — a no-adapter verdict that won't name the firmware blob the kernel already logged, and a power-on that doesn't persist across boots. +- Artifacts: [[file:../design/2026-07-10-net-bt-failure-taxonomy.org][the net/bt failure taxonomy]]; code read across =~/.dotfiles/bluetooth/src/bt/= (=doctor.py=, =repair.py=, =btctl.py=, =sysio.py=, =audio.py=, =priv.py=). |
