[Draft] Cross-platform: bring the linux-rust app to Windows natively (one crate, two OSes) - #710
Draft
arctumn wants to merge 244 commits into
Draft
[Draft] Cross-platform: bring the linux-rust app to Windows natively (one crate, two OSes)#710arctumn wants to merge 244 commits into
arctumn wants to merge 244 commits into
Conversation
Begin the cross-platform (Windows + Linux) refactor by adding a `src/platform/` module whose backend is selected at compile time via `#[cfg(target_os)]`. The first OS seam to move behind it is filesystem paths: the XDG logic from `utils.rs` becomes the `AppPaths` trait's Linux impl (`platform/linux/paths.rs`), with a `directories`-based Windows stub. Call sites keep the same `get_*_path` free-function names, so only their `use` lines change. No behaviour change on Linux; `cargo check` is green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace direct use of `bluer::Address` across the crate with `platform::DeviceId`. On Linux `DeviceId` is a type alias for `bluer::Address`, so every `Display`/`FromStr`/`Hash`/`Serialize` impl carries over and behaviour is unchanged. The Windows backend gets a real `[u8; 6]` MAC newtype with matching `XX:XX:...` formatting and `u64` (`BluetoothAddress`) conversions, so `devices.json` MAC keys stay compatible across platforms. Only type references and `use` lines change; `cargo check` is green on Linux with no new warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Open-source Windows kernel driver that opens the Apple Accessory Protocol
L2CAP channel (PSM 0x1001) to AirPods — which user-mode Winsock cannot — and
bridges it to user space via DeviceIoControl, so LibrePods can drive ANC,
battery, ear detection, etc. on Windows.
Binds as the function driver for the AAP SDP service the AirPods advertise
(INF matches BTHENUM\{74ec2172-0bad-4d01-8f77-997b2be0722a}), so its parent
I/O target is the Bluetooth stack and it can submit BRB_L2CA_OPEN_CHANNEL
outbound — no filter driver needed. Organised across Driver/Device/L2cap/Ioctl
plus a shared header. Compiles + links + passes inf2cat against WDK 28000.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User-mode Rust tool (cross-compiles to windows-gnu) that drives the LibrePodsAAP driver over its IOCTL interface for end-to-end testing against real AirPods: `find`, `status`, `connect`, `battery` (connect + AAP handshake + decode battery), `anc <mode>`, and raw `send`/`recv`. Organised into driver.rs (DeviceIoControl wrapper) / aap.rs (protocol) / bt.rs (find AirPods MAC) / main.rs (CLI). Builds clean; `find` correctly reports the driver as not-yet-installed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The two BSODs (bugcheck 0x3B in BTHport!IOTracing::TraceStart, reading BRB->Hdr.Type at a NULL BRB) were caused by submitting the BRB via a WDF memory descriptor, which left Parameters.Others.Argument1 NULL so bthport dereferenced a null BRB. Fix: format the IRP directly and set Parameters.Others.Argument1 = Brb, then WdfRequestSend synchronously. Also: install.ps1 now signs with /sm (machine store) and auto-removes any previously installed LibrePodsAAP package before re-adding. VALIDATED on real AirPods Pro (H2): connect + AAP handshake + battery read (L/R/Case %) and ANC control (listening-mode command) all work over the driver — no BSOD. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce platform::L2capTransport (async send/recv) and platform-selected l2cap_connect(). Linux backend wraps bluer's SeqPacket (moving the connect + peer-CID wait out of aacp.rs/att.rs); Windows backend bridges to the LibrePodsAAP kernel driver via DeviceIoControl (mirrors lp-driver-test). aacp.rs/att.rs now hold Arc<dyn L2capTransport> instead of Arc<SeqPacket> — the AAP/ATT protocol logic is now transport-agnostic. cargo check green on Linux; no new warnings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move find_connected_airpods / find_other_managed_devices behind platform::,
returning a neutral DiscoveredDevice { id: DeviceId, name } instead of
bluer::Device. The Linux impl (platform/linux/discovery.rs) creates its own
bluer session; main.rs's discovery no longer needs an adapter param and drops
the bluer::ErrorKind matching (Option/Vec instead of Result). Removes
bluetooth/discovery.rs. cargo check green on Linux.
Remaining for Phase D: the org.bluez D-Bus connection watcher + adapter
power-on in main.rs (the WinRT DeviceWatcher equivalent on Windows).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A tiny system-tray app (tray-icon + Win32 message loop, cross-compiles to windows-gnu) that talks to the LibrePodsAAP driver: opens the AAP session, shows Left/Right/Case battery and the current noise mode in the tray menu, and switches Off/ANC/Transparency/Adaptive on click. Reuses the proven driver IOCTL bridge + AAP parsing. windows-app/README.md is an end-to-end guide (install driver in Test Mode + run the app + how it works). Runs and shows battery/ANC against real AirPods. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Tooltip/title show the Bluetooth device name (e.g. "AirPods Pro de Pedro"), stripping the Windows "- Find My" suffix. - `connected` stays true while the link is up; it only flips false when a send actually fails, so the UI no longer flickers Disconnected<->value on idle reads. A periodic REQUEST_NOTIFS nudges fresh battery + checks liveness. - Battery components reporting status 0x04 (not present, e.g. case out of range) now show "—" instead of a bogus 0%. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 8s REQUEST_NOTIFS poll made Windows re-negotiate the audio profile, cutting/switching the output (ANC "wind", device bounce). Go passive: handshake once, then only listen for pushed packets. Liveness is checked via GET_STATUS (reads a driver variable, no L2CAP I/O) instead of a send, so it never disturbs the audio. Keeping the channel open still kills the HFP/A2DP "static" — the point is to hold it gently. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add Core Audio (WASAPI IAudioEndpointVolume) volume to the tray menu: a live "Volume: N%" line plus Volume +/- and Mute, controlling the default output (the AirPods when active). Shown in the tooltip too. Uses the `windows` crate for clean COM. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…part 2)
main.rs no longer touches bluer/dbus directly. The org.bluez PropertiesChanged
watcher moves to platform/linux/watcher.rs, which runs the blocking D-Bus loop
on its own thread and forwards platform::BtConnectionEvent (Connected {id,
name, uuids} / Disconnected {id}) over an async channel; adapter power-on
becomes platform::power_on_adapter(). async_main consumes the event stream and
keeps the device-init logic. The core event loop is now platform-agnostic for
Bluetooth (WinRT DeviceWatcher can back the same API on Windows). Phase D
complete. cargo check green on Linux.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A modern eframe/egui window: device name + connection status, Left/Right/Case battery as progress bars, ANC as a segmented button row, and a draggable volume slider (Core Audio). Reuses the driver/aap/bt/volume modules and the passive AAP session. Cross-compiles to windows-gnu (glow backend). COM is initialized STA (winit's OleInitialize needs it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add windows-driver/LibrePodsAAP/prebuilt/ with the compiled .sys + .inf + .cat so users can test-sign + install via install.ps1 without Visual Studio / WDK. The app binaries are pure Rust and never needed C++ either. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the Rust app (linux-rust -> crossplatform/crossplatform-rust) and the
Windows components (windows-driver, windows-app) under a single crossplatform/
umbrella. Update flake.nix + the CI workflow paths (renamed to
ci-crossplatform-rust.yml). cargo check green from the new path.
crossplatform/
crossplatform-rust/ (shared Rust app, was linux-rust)
windows-driver/ (LibrePodsAAP kernel driver + prebuilt)
windows-app/ (librepods-tray, librepods-ui)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The AAP L2CAP channel leaked when the app exited: the driver kept it open, so Windows reported "Can't disconnect" for the AirPods until a manual disconnect/reboot. Register a WDF_FILEOBJECT_CONFIG with EvtFileClose that calls LpDisconnect when the app closes its handle (clean exit or crash). - Driver.c: WDF_FILEOBJECT_CONFIG_INIT + WdfDeviceInitSetFileObjectConfig; AutoForwardCleanupClose = WdfFalse (our device-interface CREATE/CLOSE IRPs are completed here, not forwarded down to bthport). - Ioctl.c: LpEvtFileClose -> idempotent LpDisconnect at PASSIVE_LEVEL. - LibrePodsAAP.h: declare the callback. - prebuilt/: refresh .sys/.cat (built clean on the Windows host + inf2cat OK). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per-user, no admin: copies the exe to %LOCALAPPDATA%\LibrePods and drops a Startup-folder shortcut (WScript.Shell). `-Remove` undoes it. Stable location so it survives a WSL build-target clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the OS-specific LE advertisement source out of bluetooth/le.rs behind the
platform layer, so only the RPA/IRK match + AES decode + battery parse remain
platform-agnostic (Linux stays green; Windows gets a WinRT watcher in Phase I/J).
- platform: add LeAdvertisement{address, apple_data} and
watch_le_advertisements() -> channel of adverts, plus
connect_device(&DeviceId) for the auto-connect path.
- platform/linux/le_scan.rs: the old bluer::monitor Apple-0x004C pattern +
per-device dev.events() ManufacturerData source, and the bluetoothctl-connect
shell-out.
- bluetooth/le.rs: consume the platform stream; extract maybe_auto_connect
(dedup by observed RPA, connect by stored identity MAC). start_le_monitor no
longer returns bluer::Result. No more direct bluer imports here.
- main.rs: update the caller.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pause media when both AirPods leave your ears and resume when one goes back in (the Apple/MagicPods behaviour) — without touching the A2DP profile, which Windows manages itself. The one-time REQUEST_NOTIFS already sent at handshake is what makes the buds push ear-detection (opcode 0x06) events; we just parse them. - aap.rs: EarStatus + parse_ear_detection (opcode 0x06, primary/secondary at bytes 6/7). - media.rs: WinRT SMTC (GlobalSystemMediaTransportControlsSession) pause/play/ is_playing; COM MTA init on the receiver thread. - main.rs: run_receiver state machine — pause only if something was playing, and only resume media that WE paused (never fight a user-initiated pause). - Cargo.toml: windows crate Foundation + Media_Control features. Compiles + deployed; hardware validation pending. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Confirmed on real AirPods Pro 3: removing both buds pauses media, putting one back resumes it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move all PulseAudio (libpulse) code out of media_controller.rs behind the new platform::AudioRouter trait. The MediaController keeps its orchestration and now delegates the A2DP card-profile management + sink-volume ducking to the platform. - platform: AudioRouter trait (activate_a2dp / deactivate_a2dp / get_volume / set_volume, mac-addressed, 0..=100 volume) + platform::audio_router(). - platform/linux/audio.rs: LinuxAudioRouter owns the device-index + cached-profile state and all the libpulse helpers (card/sink introspection, set_card_profile, transition_sink_volume, wireplumber restart). - media_controller.rs: drop libpulse; hold Arc<dyn AudioRouter>; activate/ deactivate + conversational-awareness volume now go through it. (The as_f64 on Volume::NORMAL.0 came from dbus RefArg being in scope; use a plain cast.) Linux cargo check green. MPRIS/media control still in-file (Phase G next). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move all MPRIS/D-Bus media control out of media_controller.rs behind the new platform::MediaControl trait. MediaController keeps orchestration and delegates is_playing / pause / pause_all / resume / next / previous to the platform. - platform: MediaControl trait (sync/blocking) + platform::media_control(). - platform/linux/media.rs: LinuxMediaControl = the MPRIS enumeration + play/ pause/skip logic (list_mpris_services, kdeconnect filter, PlaybackStatus). - media_controller.rs: hold Arc<dyn MediaControl>; the pause/resume/skip methods now wrap the trait in spawn_blocking. Zero direct dbus/libpulse remain here. Linux cargo check green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Isolate the ksni/StatusNotifier tray so nothing outside the Linux backend depends on it. MyTray stays the platform-neutral view-model; its ksni::Tray impl (menu + battery-icon rendering) and the ksni/ab_glyph imports are now cfg(target_os = "linux"). - platform: TrayHandle type (Linux = ksni::Handle<MyTray>) + platform::spawn_tray. - platform/linux/tray.rs: ksni spawn (graceful — logs instead of unwrap-panic). - ui/tray.rs: gate the ksni::Tray impl + generate_icon + get_app_settings_path. - airpods.rs / le.rs / main.rs: use platform::TrayHandle; drop direct ksni imports. ksni is now referenced only from Linux-gated code. Linux cargo check green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gate the Linux-only system-library deps and provide Windows stub backends for
every platform trait, so `cargo check --target x86_64-pc-windows-gnu` passes
(0 errors) while Linux stays green. This is the milestone where the whole
librepods app builds for Windows — real WinRT/WASAPI/SMTC/tray-icon backends
replace the stubs in Phase J.
- Cargo.toml: move bluer/dbus/libpulse-binding/ksni under
[target.'cfg(target_os="linux")'.dependencies].
- bluetooth/aacp.rs, att.rs: drop bluer::{Error,Result} for std::io::{Error,
Result} (io::Error::from(io::Error) is identity; the protocol is now
platform-neutral).
- devices/airpods.rs: local adapter MAC via platform::local_adapter_address()
instead of a direct bluer::Session (added Linux impl + Windows stub).
- platform/windows/{watcher,discovery,le_scan,audio,media,tray}.rs: stub
backends implementing the AudioRouter/MediaControl traits + adapter/discovery/
LE/tray plumbing (idle channels, no-op A2DP, WindowsTrayHandle).
- platform: export the Windows backends + TrayHandle alias.
- windows/transport.rs: CreateFileW hTemplateFile must be a HANDLE null_mut(),
not 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…for Windows Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ackends Replace the media/audio stubs with working backends, reusing the approach proven in librepods-tray. - platform/windows/media.rs: MediaControl via System Media Transport Controls (GlobalSystemMediaTransportControlsSessionManager) — is_playing / pause_playing (tracks paused sessions by AppUserModelId) / pause_all / resume / Next+Previous. COM (MTA) ensured per call since these run on spawn_blocking threads. - platform/windows/audio.rs: AudioRouter volume via Core Audio (IAudioEndpointVolume on the default render endpoint); A2DP stays no-op (Windows manages it). - Cargo.toml: windows crate (Media_Control + Foundation + Win32_Media_Audio) for the Windows target. cargo check --target x86_64-pc-windows-gnu green; Linux green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ss (WinRT)
- platform/windows/le_scan.rs: BluetoothLEAdvertisementWatcher (active scan) with
a Received handler that pulls Apple (0x004C) manufacturer data out of each
advert and forwards LeAdvertisement{address, apple_data} to the shared
bluetooth/le.rs decoder — same RPA/battery/ear-detection pipeline as Linux.
Address u64 -> DeviceId via the existing From<u64>. Watcher leaked to stay
alive for the app lifetime.
- platform/windows/watcher.rs: local_adapter_address via WinRT
BluetoothAdapter.BluetoothAddress (was a None stub).
- Cargo.toml: add Devices_Bluetooth, Devices_Bluetooth_Advertisement,
Storage_Streams windows features.
cargo check --target x86_64-pc-windows-gnu green; Linux green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
added 9 commits
August 8, 2026 18:17
…pp version - OverlayText also translates interpolated daemon strings (Connected · Left/Right/ Case, battery-low, case opened/closed, renamed) — not just the fixed ones. - Device info: model + firmware always visible; only the serial sits behind the eye + blur (the serial is the sensitive field, per the Linux app). - Show the app version in the About card (for bug reports).
Full translations of all 90 UI strings; the Strings\**\*.resw glob picks them up and the OS display language auto-selects them (en-US stays the fallback).
…Android Available for the noise-control card / island / tray. Wiring them into the UI is a visual pass (icon appearance + RadioButton content) done next with the app open.
Documents Windows support alongside Linux/Android, incl. the hi-res two-way audio (native mic) that is Windows-only, localized UI, and the experimental HR status.
daemon/ipc: Command::Disconnect releases the AAP session (recv loop checks connect_requested and bails, drops the driver, no auto-reconnect); Command::SetName sends the 0x1A rename (aap::build_rename). winui: DeviceHeader gets a pencil that swaps the name for a text box + submit/cancel (Enter/Esc), and a Disconnect button shown while connected. Localized in en/pt/fr/es. Also drop the localized-UI row from the README feature matrix.
….Content XamlParseException on startup: HeaderDisconnectBtn was defined as .Content but its x:Uid sits on a TextBlock (which has no Content property). Renamed the key to .Text in all four languages. Also switched AccentButtonStyle to StaticResource (the correct reference for a non-theme style).
…w name Rename reached the AirPods but the app reverted the name: the daemon kept pushing snapshots with the old dev_name. Make Ctx.dev_name an Arc<Mutex<String>> and set it on SetName (+ push), so the new name persists in the UI (the OS's cached BT name may still need a re-pair, but the app shows what the user typed).
…imestamps Disconnect now also toggles the AirPods' A2DP/HFP services OFF on the local radio (a true Windows disconnect, not just releasing the AAP session); Connect toggles them ON. Both run off-thread. Also prefix every daemon.log line with a UTC HH:MM:SS.mmm timestamp for correlation.
…_descriptors Match the working Android client (upstream PR librepods-org#702): the heart-rate enable now sends control 0x30 (HRM_STATE=on) between the AACP 1.3 session init and the stream start, and drops the request_all_descriptors frames (an iOS-capture guess that never helped and Android does not send). A btvs + Wireshark capture (rtbuddy/aacp dissector) confirms the AirPods ACK service 19 every cycle but emit zero data frames on the A3063 test unit even with 0x30 on the wire — so 0x30 is necessary to match Android but not sufficient standalone here; the remaining gap is non-protocol (fit / another Apple host holding the service / firmware). Kept because it mirrors the working client.
Mirror the working Android HeartRateMonitor loop, which the PR librepods-org#702 author confirmed: the enable is complete (no keepalive, no HealthKit), at least one bud must be in-ear, and a full disconnect/reconnect is worth trying. - Only succeed on a decoded reading; ignore the bare service-19 ACKs (the failure mode is ACK-but-no-data), and re-send the full enable every ~8 s while HR is on instead of stopping after one attempt. - After 4 ACK-only attempts, ask run_receiver to rebuild the L2CAP channel (drop + reopen in-process, so no driver code-38) and re-arm HR on it — the Android requestTransportRecovery. - Cap that at 3 rebuilds, then give up ("Heart rate unavailable") so we don't reconnect forever; reset the budget when HR is re-toggled or on a new connect. - Stop head tracking up front (beforeFirstStart), matching the author's order. Tested (bud in-ear, iPhone off): the AirPods ACK service 19 every cycle but never stream a data frame across several rebuilds — type19 stays 0. Firmware is the leading suspect vs the author's working unit.
The WinUI resource step loads Microsoft.Build.Packaging.Pri.Tasks.dll, a .NET Framework assembly that `dotnet build` (MSBuild hosted on .NET Core) cannot load → MSB4062. Build the WinUI project with Visual Studio's msbuild.exe (microsoft/setup-msbuild), which is .NET Framework and loads it; keep the .NET 10 SDK for the net10.0 target resolver. Also make artifact collection locate librepods-winui.exe instead of hardcoding the TFM/RID path.
The upstream PR librepods-org#702 author flagged our incrementing sequence id as the one remaining byte-level difference from his HEART_RATE_START_1S. Pin it to his exact value (0x2363 = varint e3 46) so our start frame is byte-for-byte identical, and so retries re-send the same constant like his client does. Tested: no change (still ACK-only), but it removes the last byte difference.
Tested whether AirPods stream heart-rate only over an ERTM channel (Android/ Fluoride opens AAP sockets in ERTM; ours was Basic). Opened the channel with BRB_L2CA_OPEN_ENHANCED_CHANNEL + CM_RETRANSMISSION_AND_FLOW across two builds (timeouts 0, then 2000/12000 with a zeroed config and MPS>0). Result: the Windows bthport stack never serializes the ERTM option to the wire for a client profile driver (the Configure Request always went out Basic — MTU+FlushTO only), and the AirPods counter-propose Basic anyway. So ERTM is not the HR differentiator, and requesting it just breaks the connect. Reverted to plain BRB_L2CA_OPEN_CHANNEL. install.ps1: regenerate the catalog over the *signed* .sys (signing changes the file, so the pre-generated .cat hash would mismatch). DriverVer bumped so a rebuilt package supersedes the installed one.
The BLE watcher fires on every advertisement (several/sec), so the 20 s time debounce still re-asked "connect?" forever while the AirPods sat nearby, and it ignored a dismissed prompt. Prompt once when they appear, then stay quiet until they've been absent (no advertisement for >45 s — cased or out of range) and return — the natural "ask again on the next case-open" behaviour.
… gated)
AirPods Pro 3 restrict the heart-rate stream to Apple hosts: on Windows the buds
accept and ACK the enable but never send readings — an intentional Apple-
ecosystem restriction, not a LibrePods bug (ERTM / encryption / Device-ID spoof
all ruled out; see docs/windows/heart-rate.md). So the card no longer ships on by
default.
- AppSettings: persist an EnableHeartRate opt-in (off by default).
- SettingsPage: new "Experimental" card — a warning InfoBar ("very likely won't
work on Windows") + a toggle; raises HeartRateVisibilityChanged.
- DevicePage: the HeartRateCard is collapsed unless opted in; RefreshHeartRate-
Visibility() flips it live. MainWindow wires the two together.
- Localized the new strings (en-US / pt-PT / es-ES / fr-FR).
- docs/windows/heart-rate.md: why it doesn't work + what was ruled out.
The Lua dissectors at github.com/pabloaul/apple-wireshark name most AACP message types, and ship rtbuddy.proto -- the SensorDataWX protobuf schema we had been reconstructing by hand. Validated here against three captures: the RTBuddy length field matched the real payload on 100% of 4073 frames. Replaces the "Undocumented Opcodes Observed" table with names for all but one of the 23 entries. Notable: 0x22 / 0x23 Case Info Request / Case Info 0x59 Dynamic End Of Charge 0x1B Timestamp (ISO 8601 local date-time) 0x54 Set Band Edges 0x29 Set Country Code 0x2B Stream State Info 0x4F Unified Accessory Restore Protocol (its own dissector) Correction: 0x44 is **Send Smart Routing 2.0 Info**, audio routing. An earlier revision called it sensor subscription because it appeared shortly before stream changes. The name fits the observations better than the sensor reading ever did -- it shows up at session and audio state changes, and its payload never correlated with any stream starting. Two leads recorded for the unresolved 0x01 vs 0x05 charging question: 0x22/0x23 give case state directly rather than inferring it from the battery packet, and 0x59 "Dynamic End Of Charge" is present in the capture where 0x05 appeared and absent from the hands-off captures where it did not. Flagged as a correlation to test, not a conclusion. (cherry picked from commit 7dde1fc)
…icon A pass over the WinUI client's visuals + tray, driven by dogfooding. Device artwork (Services/DeviceArt.cs — model number → asset family): - DeviceHeader shows the model's render instead of the generic airpods.png. - BatteryCard shows per-component bud/case artwork above each gauge. - The connect island uses the model render, with the last-seen model cached (AppSettings.LastModel) so it's right before the 0x1D metadata arrives. Noise control: the vertical RadioButtons become a segmented row of icon buttons (Off / NC / Transparency / Adaptive) with the active mode named below. Island: the message mode shows the mode icon for a noise-control change (not the device), and the whole popup now follows the app theme. Nav pane: the device item gains a model icon + a compact battery %, shown in the expanded pane (handy once multiple devices are listed). Tray: renamed TrayIcon -> TrayController; the WinUI MenuFlyout (which clipped to ~1 char in H.NotifyIcon's SecondWindow host) and a native Win32 menu (no per-item icons, no theme) are both unusable here, so the context menu is now a custom themed window (Popup/TrayMenuWindow + TrayMenuView) with an icon per row, the model image, battery, ANC checkmarks, mute/open/quit — dismissed on focus loss. The icon badge is a large battery number; the tooltip gains the ANC mode. Fixes: the "Connect?" prompt no longer shows while already connected; the one-column device layout drops the phantom right gap (collapsed Col1 kept its ColumnSpacing). New app/tray icon (transparent background). Settings: a UI-language selector (persisted, applied at startup). NOTE: runtime language switching is limited on the unpackaged build — PrimaryLanguageOverride does not re-resolve x:Uid there; a code-based localization pass is the follow-up.
The raster ANC art (anc_*.png) is fixed-colour — white washed out on the light acrylic, black vanished on dark. Show a Segoe Fluent FontIcon (follows the theme foreground) instead, matching the tray menu's mode glyphs.
The anc_*.png icons had inconsistent padding/weight so they framed unevenly next to the (glyph) Off button. Use uniform Segoe Fluent FontIcons — consistent framing, theme-following (incl. on the accent-selected button), matching the island + tray mode glyphs.
x:Uid + PrimaryLanguageOverride don't re-resolve on the unpackaged build, so the
language selector needed a full restart (and didn't even work). Replace it with an
Angular-style translate service:
- Services/Loc.cs — a singleton INotifyPropertyChanged that parses the embedded
Strings/<culture>/Resources.resw into a culture -> key -> value map. Indexer
Loc["Key"] returns the current-culture string; SetCulture() raises the indexer
PropertyChanged so every binding re-fetches → the whole UI switches LIVE.
- All UI text now binds to {Binding [Key_Prop], Source={StaticResource Loc}}
instead of x:Uid (keys are the resw names with '.' → '_' for the indexer path).
- Localize.Get (code-picked strings) delegates to Loc.
- The language selector calls Loc.SetCulture — no more restart hint.
- The .resw are embedded (LogicalName loc.<culture>) alongside the existing PRI
resources, so translations aren't duplicated.
Works on the unpackaged build (no MRT / x:Uid / PrimaryLanguageOverride).
…otify
WinUI (unlike WPF) doesn't refresh {Binding [key]} on an "Item[]" notification,
so switching language updated nothing outside the active page. Raise the empty-
string "all properties changed" notification so every binding re-fetches. Also
make the embedded-resw lookup scan the manifest names (not a hard-coded
LogicalName) so a prefixed/mangled name can't silently yield an empty map.
Confirmed: the whole app switches language live now.
The built-in NavigationView Settings item is OS-localized; re-label it from Loc (live) so it follows the in-app language. Update the language hint to say it applies immediately (no restart).
App.xaml instantiated a SECOND Loc via <services:Loc x:Key=Loc>, distinct from the static Loc.Instance used by code — so SetCulture changed one while the other stayed, leaving code strings in one language and XAML bindings in another. Inject the single Loc.Instance into Application.Resources instead.
A daemon overlay (e.g. "Connected · battery") is a one-shot string resolved when it arrives, so one shown before a language switch lingered in the old language. Close it on a culture change; the next overlay renders in the new language.
…sources write Mutating Application.Resources in the App ctor threw a COMException. Revert to declaring the one <services:Loc x:Key=Loc/> in App.xaml (which works), but drop the static new() so it's the ONLY instance; its ctor publishes Loc.Instance for code. No second instance → no culture split, and no runtime resource mutation.
…itch - ToggleSwitch On/Off content was OS-localized (system language); bind OnContent/ OffContent to Loc (Toggle_On/Toggle_Off keys, 4 languages) so it follows the app. - Code-picked strings (header status, mic state, ANC mode name) are only set when a snapshot arrives, so they lingered in the old language after a switch. Re-render the last snapshot on a culture change.
The BLE proximity watcher prompted "Connect?" and waited for a click. The buds being audio-connected in Windows doesn't open our AAP L2CAP session (separate channels), so the app sat on "Disconnected" until a manual Connect. Instead auto-connect on the proximity edge-trigger: set connect_requested (run_receiver opens the session) and nudge the OS audio up. Still edge-triggered per visit, so no reconnect spam.
winui: model artwork, richer island/nav, custom tray menu, new icon
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.
This is a work-in-progress port that makes the
linux-rustapp (librepods) build and run natively on Windows while keeping Linux green from the same codebase — no Bumble, no re-pairing, no WSL. Opening as a draft for visibility and feedback; it is developed and hardware-validated on real AirPods Pro on a Windows machine.Addresses #433 (native Windows support — no WSL).
What it does
All OS-specific integration lives behind a
platform/abstraction selected at compile time (#[cfg(target_os)]). The protocol/UI code (aacp,att,le,media_controller, the iced GUI) is identical on both platforms.bluerSeqPacketLibrePodsAAPkernel driver via IOCTLbluer::monitorBluetoothLEAdvertisementWatcherLibrePodsMicvirtual audio driverksnitray-icon%APPDATA%Why two kernel drivers
Microsoft exposes no user-mode API for what they do (verified against the driver docs + a Winsock
AF_BTHspike that returnedWSAENETDOWN):0x1001); Windows only allows an L2CAP client connection from a kernel profile driver (BRB_L2CA_OPEN_CHANNEL). So a signed KMDF driver bridges it to user space.Highlights validated on hardware
librepodsd) owns the exclusive driver + AAP session and serves thin UI clients (tray + the iced app) over named-pipe IPC — so audio stays clean and the UIs coexist.Scope / not asking to merge yet
#[cfg(windows)]-gated; Linux paths are unchanged and stay green at every step (cargo buildboth targets).AAP Definitions.mdwas missing battery status0x05(charging in case) and the0xFF-absent rule — added here.Feedback welcome on the
platform/abstraction shape and whether upstream wants to host the Windows drivers in-tree or as a companion.How this compares to MagicPods
The closest Windows counterpart is MagicPods — a mature, polished app worth acknowledging. This is a fair-as-I-can feature comparison (correct me where I'm wrong); the point isn't to knock it, it's to show what a fully-FOSS, cross-platform alternative brings.
Where MagicPods is ahead (multi-vendor breadth, low-latency mode, Store polish) is noted honestly; where this project is ahead is being 100% free and open — drivers included — with a hi-res mic, host-side CA ducking, experimental heart-rate, and a single codebase that keeps Linux and Windows in lockstep.
Native WinUI 3 client (optional front-end)
Alongside the cross-platform iced app, this branch adds an optional native C# / WinUI 3 (Windows App SDK) front-end (
librepods-winui.exe) — another thin IPC client of the samelibrepodsd. It is the default UI on Windows (switchable back to iced from Settings), fully async, self-contained/unpackaged on .NET 10, with native tray, toasts and an iOS-style connection island. All hardware-validated on real AirPods Pro.