Skip to content

← Firmware Downloads

Post-install setup · Reborn (Y2Linux) · Y2

Bluetooth reimplementation — plan

Debian-based Linux with a fast clickwheel interface for the Innioasis Y2.

Instructions published by the project, shown here word for word.

Bluetooth reimplementation — plan

Status: plan, 2026-11-03. Nothing built, flashed or run. Owner request: "not a refactor anymore, but a complete new reimplementation built on reliability, so that Bluetooth always works, without losing the codecs, bitrates and the fancy stuff". This changes the architecture of a hardware subsystem, so per AGENTS.md the roadmap/gap audit is repeated before implementation starts. This document is not implementation or hardware authorization.

Sources: Y2Linux main b8e78d3f (9.2.16), Y2Reborn 6fd259c, GitHub issues Y2Reborn #87 #92 #78 #22, field reports 0400eb78 (583b7c23 58e1456e a74ed186, 10-31 diag), 5692dd73 (962aac6c 46fcbe24), 3a1f5afc d833b3da f92f0093 aa96e00a e1db3cb0, the ledger UserReports/REVIEWED.md (F8 F14 F15 F20 F21 F22 F25 F28 F43 F51 F58 F61 F63, new F64 F65).

1. What the evidence says

What is not broken

What is broken (ranked)

  1. **The library is emptied by a Bluetooth failure (#92, F64, proven, two field

    occurrences).** abandon_pending_reconfiguration (Y2Reborn/app/reborn/src/runtime/audio.rs:152) restores the pre-action snapshot, which with_model_action took with the tracks, shelf, chapters and platform data moved out (:18-26). It does not move them back, unlike restore_failed_reconfiguration. Any peer loss or transport change while a sink start is pending empties the library, the Audiobooks row disappears, and the next checkpoint writes an empty state/books.json. That is why the progress is lost. This is not an architecture problem. It is a few-line fix and ships first (H1).

  2. Wake reconnect race (#92 "pairing froze", F65, strong). Every full sleep

    powers Bluetooth off (the kernel refuses suspend while a function is held). On wake, four actors start at once: - the headphones reconnect on their own; - y2-bt-reconnect pages; - Reborn sends SelectCodec; - the auto-resume opens the sink.

962aac6c shows the result in order: AVDTP No pending request ×2, then SelectCodec refused, then ALSA −5, then the PCM disappears, then a local disconnect. After that, Connect answered InProgress every 2 s for 1 min 45 s, through two Bluetooth power cycles. This is architectural: it comes back under any patch while several owners act on one link.

  1. Air-time shortfall (#87 crackle, strong). The XM4 install shows the encoder

    queue backing up at LDAC 606k and at AAC 192k: - LDAC ABR made 5 then 15 adjustments, and AAC fell to 144k; - two Bluetooth xruns; - zero link errors.

The shared antenna, Wi-Fi in CAM (no power save), and a weak access point (−77/−83 dBm) all cost air time. The Wi-Fi ceiling does not cap LDAC or aptX. Lower rates only mitigate this, because the radio is shared. Policy has to treat air time as the scarce resource.

  1. AVDTP Suspend+Start on every pause (F43 → F61 pattern, moderate). On

    Bluetooth, a pause closes the PCM and Play reopens it. Some headphones answer Start slowly; BlueZ then times out and drops the link. The 10-31 drop (three pause→play cycles, lost 2 s after the last) fits this.

  2. Disconnect after ~1 h on AAC with Wi-Fi off (#87): no report covers it yet.

    We need a 9.2.16+ report taken right after it happens; the reason is logged now.

Why patches keep failing: the structure

There are five owners of Bluetooth state:

They coordinate through advisory flock files (bt-operation.lock, bt-pcm.lock) and an ini "intent" file with 30 s deadlines. No single component knows the truth, so:

Git history: 58 Y2Linux and 69 Y2Reborn commits are about Bluetooth. Most of them add a rule to one owner to work around another owner.

2. Decision: what gets reimplemented, what is kept

LayerDecisionWhy
Kernel y2_connectivity (BTIF/STP/WMT/HCI)keep, add counters + one control for the daemonno field failure starts here; rewriting it risks the radio
Firmware, WMT_SOC.cfg, BT.defaultskeepowner-supplied, hash-pinned
BlueZ 5.87keep (config only), now supervisedthe A2DP/AVDTP/AVRCP protocol is not where we fail
BlueALSA 5.0.0 + patches 0001-0003keep, add --keep-alive handling, an LDAC/aptX ceiling, and a runtime rate control instead of restartthis is the codec and bitrate machinery the owner wants to keep
y2-bt-reconnect (C), bt_control.rs, intent ini, bt-operation.lock, bt-pcm.lock, bt-codec-uncertain.json, the sleep helper's radio steps for BTreplacethe five-owner structure
Reborn reborn-platform/src/bluetooth.rs + bluetooth/worker.rs (≈3800 lines), outputs.rs AutoOutput/CodecReconnect/LostPeer, AVRCP owner pollingreplace with a thin clientten copies of the truth, blocking worker
Reborn sink/playback for Bluetooth (pause by close, kept sink)rewrite the Bluetooth part around a lease + stream generationSuspend/Start drops
UI pages, settings, codec names, history, AVRCP projection (MPRIS)keep (re-pointed at the new client)users like them; they are not the problem

Rejected: replacing BlueZ+BlueALSA with PipeWire or our own A2DP stack. PipeWire adds a session manager, more RAM and a new set of unknowns on a 948 MB device. It would not fix any traced failure, because none of them is inside BlueALSA. An own A2DP stack would rewrite exactly the part that works.

3. The new architecture: one owner

 Reborn (UI, playback)                y2-radiod (platform, Rust, new)                system
 ───────────────────                  ─────────────────────────────────                ──────
 BtClient ── desired state ─────────▶ Desired { powered, preferred peer,               bluetoothd (supervised)
   (one socket, never blocks)          codec pref, quality, ... }                        bluealsad  (supervised)
 BtClient ◀── BtSnapshot{gen} ─────── Reconciler ── one action at a time ──────────────▶ D-Bus
 sink opens only when                  Link state machine per peer                       kernel y2_connectivity
   snapshot says Ready(gen)            Recovery ladder + budget                           (status, recover)
                                       Stream lease (who plays, keep-alive)
 sleep/wake ─────────────────────────▶ Power handoff (quiesce / restore)
 Wi-Fi daemon ── air-time state ─────▶ Rate policy (codec ceiling per Wi-Fi state)

3.1 Principles (each one answers a failure class above)

  1. One owner, one state machine. y2-radiod is the only process that

    powers, pages, connects, disconnects, pairs, forgets, selects codecs, restarts BlueZ/BlueALSA or touches the CONSYS recovery. Everything else (Reborn, sleep, Wi-Fi, CLI) only states what it wants. (Fixes F14 F28 F58 F65; removes all locks and intents.)

  2. Desired state, not commands. The daemon reconciles continuously toward a

    desired state. A user tap replaces the desired state, and the daemon cancels whatever conflicts with it. There is never "operation in progress, try again": the busy-retry loops disappear because the request is the state.

  3. One truth with a generation number. The daemon publishes a single

    BtSnapshot: - adapter; - each peer's link state; - codec and rate; - stream readiness; - last failure with a typed reason; - gen.

Reborn keeps exactly one copy. Every UI view, output choice and volume rule is derived from it. "Unknown" is a state of its own, never mistaken for "disconnected". (Fixes the empty-Status peer loss and F20.)

  1. Playback only on a promise. Reborn opens the BlueALSA PCM only when the

    snapshot says Ready{peer, codec, gen}. It opens it off the main thread and tags it with gen. A changed gen closes the stream cleanly; there is no rollback of the whole model. (Fixes the #92 class structurally, beside the H1 hotfix.)

  2. Nothing latches without telling the user and offering a way out. Every

    recovery path ends either back at the desired state or in one visible state: "Bluetooth needs a restart", with a button that runs the next ladder rung. No hidden marker files, no "inhibited until an intent".

  3. Never block the owner. The daemon's loop is event-driven. Each D-Bus call is

    async with a deadline sized from measurement. Volume runs on its own fast path and never waits behind a Connect.

  4. Fewer AVDTP state changes. Every Suspend/Start is a chance for a slow

    headphone to drop. The stream stays started across pause, track change and station change, within limits (§3.4).

  5. Air time is a budget. The bitrate is chosen from the link's real capacity

    (queue depth, learned per peer, Wi-Fi state), not from a static preference.

Unpaired ─pair─▶ Paired ─▶ Idle ─(desired=connected)─▶ Paging ─▶ Connecting ─▶ ServicesResolved
                                                                                   │
            ┌────────────── Disconnected{reason: remote|timeout|local|sleep|user} ◀┤
            ▼                                                                       ▼
        Backoff ──(budget left)──▶ Paging                                 CodecSettle (only if wrong)
            │                                                                       ▼
            └─(budget gone)─▶ Parked (visible, retried on headphones' own connect, wake, tap)  Ready{gen}
                                                                                   ▼
                                                                    Streaming ⇄ Held (paused, kept)

3.3 Recovery ladder (one budget, owned by the daemon)

RungActionWhen
R0retry the step after its deadlinesingle timeout
R1cancel: Device1.Disconnect + wait for idlestuck page/connect (the 1 min 45 s InProgress)
R2adapter power cycle (Powered off/on)R1 failed or the HCI is unresponsive
R3restart bluealsad (Reborn sees a new gen)PCM missing/broken, encoder errors
R4restart bluetoothdD-Bus API dead, R2 failed
R5CONSYS recover (both radios: Wi-Fi is told first)HCI dead, R4 failed, kernel error counters

3.4 Stream path (keeps every codec and bitrate)

3.5 Sleep and wake

The sleep helper no longer powers Bluetooth itself.

Research item (not a dependency): whether Bluetooth can stay powered across a sleep. That would need the kernel's held-function refusal lifted, plus wake-on-BT, plus a drain measurement. If yes, the wake race disappears entirely.

3.6 Observability

4. Phases

Each phase ends in a test build and a device check on the owner's player with at least two headsets (AirPods-class AAC, an LDAC headset). It ships only when its acceptance numbers are met.

H — Hotfixes now (independent of the rewrite, next point release 9.2.17)

P0 — Measure before designing numbers (2–3 sessions, device)

P1 — Contract and specification

P2 — Daemon on the host

Every field failure F8–F65 becomes a scripted scenario that must pass.

P3 — Daemon on the device, behind a switch

P4 — Reborn client

P5 — Rate policy and Wi-Fi coordination

P6 — Beta, then delete v1

5. Owner decisions needed

  1. Owner of the stack: the platform (y2-radiod, recommended: it must

    supervise processes and coordinate sleep and Wi-Fi) or Reborn. The 2026-10-30 platform decisions allow the Reborn API to change.

  2. Wi-Fi while Bluetooth streams:
    • power-save only (recommended default);
    • off unless leased (most reliable, but radio/Last.fm/update would switch Wi-Fi

      on on demand);

    • user choice.
  3. Default quality: Auto that steps down for air time (recommended), with High

    and Fixed for people who never had problems (#87 MickEventide's concern).

  4. Pause hold window: how long a paused stream stays started (drain vs

    reliability); P0 gives the numbers.

  5. Whether to pursue Bluetooth on through sleep (§3.5), which is a kernel and

    power project of its own.

6. Risks

7. Wi-Fi and the shared radio core (added 2026-11-03)

Owner question: Wi-Fi and Bluetooth share the MT6582 CONSYS core and one 2.4 GHz antenna. Is Wi-Fi broken the same way, and does it need its own worker or a shared one?

Wi-Fi has the same structure

Wi-Fi state is acted on by eight parties:

ActorWhat it does to Wi-Fi
wpa_supplicantassociates and reconnects on its own (autoscan exponential:2:120)
Reborn reborn-platform/src/wifi.rs (1842 lines)its own thread; talks to the supplicant control socket directly (scan, join, add/remove networks, disconnect); writes rfkill soft itself; persists /data/network/enabled
Reborn runtime/idle.rs wifi_idle_tickpowers Wi-Fi down after 5 idle minutes and back up at wake/use
platform connectivity/wifi/recovery.rsF52 reselects a saved network after 30 s DISCONNECTED; F60 cycles the same rfkill (block, wait for wlan0 to go, unblock) after 4 blind scans
platform connectivity/daemon/supervisor.rsrestarts the supplicant; F53 removes and re-adds wlan0 when it is INTERFACE_DISABLED; sets autoscan
platform connectivity/radio_policy.rs (service daemon, every 30 s)power-save policy (iw)
sleep helper suspend/helper.rs:200, 405y2-radio wifi off-runtime before every sleep, on-runtime after
y2-radio (shell) + kernel core.crfkill per device; writes recover to the shared core; a Bluetooth HCI error's automatic recovery unbinds and rebinds the Wi-Fi driver too (recover_locked, device_release_driver(&c->wifi->dev))

As with Bluetooth, nothing owns the whole state. Two components switch the same rfkill. Two decide when to (re)select a network. The ledger shows the same patch chain:

Each fix added a self-heal to one actor. F60 is still open. After a wake the saved access point is seen but rejected for minutes. The first episode happened while the headphones reconnected and A2DP started (fa30bf96).

It is less acute, but it shares the weak point

No component today weighs one radio's recovery against the other's.

Recommendation: one radio daemon, two independent controllers

y2-radiod
├── Core arbiter      (shared, small)
│     power/function lifecycle of CONSYS, sleep quiesce/restore handshake,
│     wake ordering, kernel recover (R5) with both radios informed,
│     air-time policy: Wi-Fi state → BT codec ceiling, BT streaming → Wi-Fi power-save/lease
├── Bluetooth controller   (its own task: BlueZ/BlueALSA, §3)
└── Wi-Fi controller       (its own task: wpa_supplicant D-Bus/ctrl, DHCP, DNS readiness)

Today these are spread over the sleep helper, y2-radio, the service daemon's coexistence file and the kernel. Only an arbiter that sees both radios can stagger the wake, for example:

  1. Bluetooth first while music was playing, with its adoption window;
    1. Wi-Fi association after Ready or after the window;
    2. Wi-Fi first when nothing plays.

Order

  1. Bluetooth stays first. It has the user-visible failures: dropouts, a stuck

    reconnect, the library loss.

  2. The core arbiter is built in P2/P3 from the start, even before the Wi-Fi

    controller exists, because the sleep handshake, wake ordering and recover decision belong to it, not to the Bluetooth controller. At first it drives the existing Wi-Fi path (y2-radio, Reborn's worker) through a narrow adapter.

  3. The Wi-Fi controller follows as P7, after the Bluetooth beta (P6), with the

    same pattern: - host simulation of a fake supplicant (F13/F52/F53/F60 as scripted scenarios); - a v1/v2 switch; - a soak of 200 wake cycles with the saved network in range, with connect time and failures per cycle.

  4. P0 adds wake measurements: Wi-Fi association time and reject codes at wake

    with and without Bluetooth reconnecting in parallel. That tests directly whether staggering fixes F60, before any rewrite.

Decision 1 in §5 becomes: radio daemon in the platform (recommended), with shared arbiter + two controllers.

8. As built (2026-11-03, owner: "do the full implementation in one step, build and install")

The owner asked for everything at once after reading §1–§7. What was built, and what deliberately was not:

PlanBuiltWhere
H1 #92 library lossrolled-back model keeps the whole live library and platform factsReborn runtime/audio.rs keep_live_parts, test a_rolled_back_output_change_keeps_the_library_and_book_progress
H2 empty status ≠ peer lostbt_lost and the transport epoch ignore a status without the adapterReborn main_loop.rs
H3 sleep reasona disconnect during the daemon's quiesce is sleepy2-radiod bt/links.rs ended
H4 LDAC ceilingBlueALSA patch 0004: LDAC ≤ MQ (330 kbit/s) while Wi-Fi is onbuildroot/patches/bluez-alsa/0004-y2-ldac-codec-ceiling.patch
One ownery2-radiod (new crate platform/crates/y2-radiod): arbiter + Bluetooth controller; y2-bt-reconnect, bluetooth-power, intents, bt-operation.lock, bt-pcm.lock, bt-codec-uncertain.json and Reborn's D-Bus worker are gone§3
Desired state, no "busy"requests change a wish and are answered at once; Connect replaces a pagebt/requests.rs
One truthy2p_contract::radiod::State over /run/y2/radiod.sock; Reborn's Status is built only from it; Device::settling gates playbackReborn reborn-platform/src/bluetooth.rs
Adoption window, episodes3 s for headsets to return on their own; 3 tries (power-on, wake, tap, timeout/local loss), 1 after a remote end, none after the owner's disconnect or a sleepbt/links.rs
Recovery laddercancel page → power cycle → BlueALSA restart → BlueZ restart, 3 each per 10 min, then Failed until "Restart Bluetooth"bt/ladder.rs, bt/reconcile.rs
Supervisionbluetoothd (its init script) and bluealsad restarted when they end (5 per 10 min)bt/supervise.rs
Codec at connect, never mid-streampolicy moved from Reborn unchanged; SelectCodec only when it differs, only while the stream is not running; no answer leaves the headset usablebt/codec.rs, bt/settle.rs
Pause ends the streambluealsad --keep-alive=0 (9.3.12): a pause suspends at once as a phone's does, Play starts a new stream; builds 210–219 kept it 900 s (one Suspend in three unanswered when sent 10 s into an idle stream, 2026-11-03), but headphones left in a started stream without packets went silent after every gap longer than their buffer (AirPods, 2026-11-04, as F43 in Beta 6)bt/supervise.rs
Wake orderWi-Fi waits up to 12 s for the headset connected before the sleeparbiter.rs
Wi-Fi powery2-radio, Reborn's switch and the F60 restart go through the daemon (direct only when it does not run)§7
Journal/data/system/platform/bluetooth-journal.jsonl, /run/y2/radiod-state.json, both in error reportsjournal.rs

Not built in this step (stays open): the P0 measurements (no device was reachable while building); a learned per-headset rate level and a runtime rate control socket in BlueALSA (the existing ABR plus the Wi-Fi ceiling, now including LDAC, remain the rate policy); the Wi-Fi association controller (P7); the per-device auto-connect switch in Reborn's UI (the daemon has AutoConnect, Reborn does not send it yet, #78); no v1/v2 switch on the player (rollback is the installer's kept previous package).

Tests: y2-radiod unit tests (episodes, reasons, ladder, codec ranking and history, generations, owner files, core status, ctl words) and an end-to-end test against a fake BlueZ/BlueALSA on a private bus (power-on, adoption, a stuck InProgress cancelled and retried, Ready with a generation, sleep); Reborn's client tests; platform gate and host tests.

Measured on the owner's player (2026-11-03, builds 208/209, AirPods Pro, Wi-Fi on)

WhatMeasuredConstant it informs
Adapter power-on (warm, after boot)0.41 s (request → adapter on)POWER_ON_DEADLINE 100 s is only for the first boot's calibration
Bluetooth on → AirPods Ready5.6 s (3 s adoption, page → connected 2.2 s)AirPods never came back on their own within the 3 s ADOPTION: for them the window is pure delay; kept at 3 s until other headsets are measured
Wake (sleep helper's radio steps) → AirPods Ready≈6 s; Wi-Fi held, released when the headset was back (not by the 12 s limit)WAKE_HOLD 12 s
Bluetooth power-down at a sleep0.17 s, chip functions 0quiesce deadline 15 s
bluealsad killed → Ready again7.7 s (BlueZ ends the link local, episode Lost(Local), page after 2 s)supervision spacing 5 s
Reconnect where BlueALSA refused the configuration (A2DP.InvalidChannels)connected without services, cancelled after STUCK 12 s, then ReadySTUCK 12 s (shorter would cut real slow set-ups)
Pause with a 10 s keep-aliveone AVDTP Suspend in three unanswered ("No reply to Suspend request"), link dropped 3 s laterkeep-alive raised to 900 s (build 210)
Pause and resume inside a started stream (keep-alive 900, 9.3.7–9.3.11)AirPods Pro silent after every pause/resume gap of 0.4–0.6 s and after track changes that followed one; packets ran on at 47/s with no gap at the skips themselves (btmon 21:10 2026-11-04), so the sink, not the sender, had stopped; a new stream (AVDTP Start) always playedkeep-alive back to 0 (9.3.12): Suspend at the pause, Start at Play, as 9.2.16 ran; a kept sink keeps its audio at a track change
Daemon CPU while settled0 ticks in 30 s—
Codec settings change with the headset connected (9.3.3 → 9.3.4)34 s (half set-up link) → 7–9 s (clean disconnect, restart, reconnect)HALF_CONNECTED 8 s
Wake with AirPods reconnecting on their own (9.3.4)≈15 s: BlueALSA refuses their AAC configuration (A2DP.InvalidChannels), cancelled after 8 s, the daemon's own page works. btmon: their SetConfiguration sends AAC octet 3 = 0x07 (stereo plus the two bits A2DP 1.3 reserves and 1.4 reads as 5.1/7.1); ours sends 0x84BlueALSA patch 0006 reads stereo plus those bits as stereo (9.3.5, build 213)
Wake with AirPods reconnecting on their own (9.3.5)configuration accepted, stream open after 0.2 s, yet cancelled after 8 s as half connected; wake 21 s once in five (others 5.8–7.8 s). BlueZ 5.87 resolves no services for a known headset that connects to us (ServicesResolved false for the whole connection)set up = resolved services or a BlueALSA stream (9.3.6, build 214)
Wake with AirPods reconnecting on their own (9.3.6)Ready 2.1 s and 2.6 s (paged wakes 5.2–7.6 s, 0 cancels in 19 wakes), audio plays; but the codec decision saw no mutually usable codec: BlueALSA listed the headset's codecs only after BlueZ asked them, ≈20 ms after the streamwait up to 3 s for a codec list that contains the running codec (9.3.7, build 215)
Wake with AirPods reconnecting on their own (9.3.7)four of four came back by themselves, Ready 2.5–4.9 s, AAC mutually usable and Settled every time, no page, no refusal—
A pause longer than ≈22 s (9.3.7, Wi-Fi on or off, paged or self-reconnected link)AirPods stop answering at the baseband ≈2 s after the pause (last packet from them at the pause, no sniff or role change), the link ends with a supervision timeout (HCI 0x08) 20 s later, AirPods reconnect within 0.4–3 s. A resume inside those 20 s plays into a dead linkopen: what silences them (headset idle behaviour or our keep-alive stream without packets); link supervision timeout is the controller default 20 s. btmon on this platform shows no A2DP media packets (also on healthy links), so audio flow is not visible in captures

After the owner's second round (2026-11-03): Bluetooth off and on is the owner's restart (no separate button); stream lease from Reborn; ReloadCodecs (no player restart for codec settings); Wi-Fi power save while streaming; learned start rates (patch 0005); AirPods' own AAC configuration accepted (patch 0006); status asks the daemon; #78 Connect Automatically in Reborn; six fake-BlueZ scenarios. Remaining from §8's list: the Wi-Fi association controller (P7), other headsets, an overnight.

Published by the schulzcode/y2linux project [1] [3]