Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@

Post-v1 features, intentionally deferred.

## Experimental model work (Inspire XH1 pilot)
## Protocol capture tooling (in-app Capture Studio)

- [ ] **Verify XH1 BLE UUIDs** — candidate UUIDs from APK analysis (`0000ae0x` family); confirm with nRF Connect on real hardware
- [ ] **Verify XH1 ANC wire format** — opcode 0x68 candidate from APK; confirm mode bytes (0x08/0x09/0x0A for Commute/Outdoor/Indoor) via Frida hook
- [ ] **Implement XH1 ANC SET** — once wire format confirmed, wire `execute_command` for adaptive modes
- [ ] **XH1 HomeTab** — show single headphone battery card instead of L/R/case grid
- [ ] **Promote XH1 from Experimental → Verified** — after ANC + battery confirmed by an owner
The APK-derived Inspire XH1/XP1/XC1 models were removed (never hardware-verified). The
replacement is tooling that lets real owners capture and contribute *verified* support:

## Other Baseus models
- [ ] **In-app Capture Studio** — a dev mode in the desktop app: scan any BLE device, auto-detect notify/write chars, live hex log, guided "toggle ANC now → captured N frames", export a shareable capture bundle. (Owns its own design spec.)
- [ ] **Declarative model format** — define models as data (UUIDs, opcode→event map, frame layout) read by a generic decoder, so adding a device is data + a golden test, not a Rust module. The Capture Studio can auto-draft it.
- [ ] Re-add community-contributed models through the above, each verified on real hardware.

- [ ] Add more models using `tools/extract_apk_model.py` — the workflow is now established; any contributor can add a model as a draft PR
## BP1 Pro ANC remaining work

## BP1 Pro ANC remaining work

Expand Down
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Changelog

All notable changes to this project are documented here. Format loosely follows
[Keep a Changelog](https://keepachangelog.com/); versions match the git tags.

## [0.3.0] — 2026-07-03

### Added
- **Game / low-latency mode** for the BP1 Pro — a toggle that sends `BA 24 01/00`
and tracks state from the device's `AA 23` confirmation. Community-verified over
both SPP and BLE in [#3](https://github.com/elaxptr/baseus-desktop/issues/3).
- **UX motion pass** — an animated game-mode switch (sliding knob + accent halo),
battery rings that sweep and count up, a gliding sidebar indicator, spring tab
transitions, and hover/press/ripple micro-interactions. All motion respects
`prefers-reduced-motion`.

### Changed
- ANC ack handling is now firmware-tolerant: some units ack every ANC command with
a flat `AA 34 01` (including Off), so state is resolved against the last commanded
mode rather than trusting the ack payload as a mode value ([#3]).
- Numeric readouts (battery %, ANC strength, session timer) use a monospace face.

### Removed
- **Inspire XH1 / XP1 / XC1 support.** These were extracted from the Baseus Android
APK and never confirmed on real hardware. Rather than ship unverified promises,
they've been removed. The model registry is kept (BP1-only) as the extension point
for future owner-contributed, hardware-verified models — see `BACKLOG.md` for the
planned in-app Capture Studio and declarative model format.

[0.3.0]: https://github.com/elaxptr/baseus-desktop/releases/tag/v0.3.0

## [0.2.1] and earlier

See the git history and release tags (`v0.2.1`, `v0.2.0`, `v0.1.0`). This changelog
was introduced in 0.3.0.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,24 @@ the official Baseus Android app.

## Supported hardware

| Model | Status | Battery | ANC | EQ |
|---|---|---|---|---|
| Bass BP1 Pro ANC | ✅ Verified | ✅ L/R/case | ✅ 3-mode + strength | ✅ 4 presets |
| Inspire XH1 | 🧪 Experimental | ⚠ Untested | ⚠ 5-mode adaptive (untested) | — |
| Inspire XP1 | 🧪 Experimental | ⚠ Untested | ⚠ 3-mode (untested) | ⚠ Untested |
| Inspire XC1 | 🧪 Experimental | ⚠ Untested | ⚠ 3-mode (untested) | ⚠ Untested |

**Experimental** means the protocol was extracted from the Baseus Android APK via static analysis —
no physical device has been tested. If you own any of these, install the app and
[report what works](https://github.com/elaxptr/baseus-desktop/issues). See
[docs/protocol/inspire-xh1.md](docs/protocol/inspire-xh1.md) for the full protocol draft.
| Model | Status | Battery | ANC | EQ | Game mode |
|---|---|---|---|---|---|
| Bass BP1 Pro ANC | ✅ Verified | ✅ L/R/case | ✅ 3-mode + strength | ✅ 4 presets | ✅ Low-latency toggle |

Only hardware-verified models are supported. Earlier drafts included Inspire XH1/XP1/XC1
support extracted from the Baseus Android APK, but since none was ever confirmed on a real
device it has been removed rather than ship promises we can't back.

**Own a different Baseus model?** Adding it is the goal — see
[docs/re-methodology.md](docs/re-methodology.md) for how to capture your device's protocol
and contribute it back. Protocol capture tooling to make this much easier is on the roadmap.

## Features

- Live L / R / case battery with charge state indicators
- Session timer (time since buds connected)
- ANC mode switching (Off / Active Noise Cancellation / Transparency) with strength slider
- Game / low-latency mode toggle
- EQ preset selection (Balanced / Bass Boost / Voice / Clear)
- Find-my-buds (plays a tone on one earbud)
- Low-battery desktop notifications
Expand Down
2 changes: 1 addition & 1 deletion apps/baseus-app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "baseus-app",
"version": "0.1.0",
"version": "0.3.0",
"description": "",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion apps/baseus-app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "baseus-app"
version = "0.2.1"
version = "0.3.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
Expand Down
6 changes: 0 additions & 6 deletions apps/baseus-app/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@ pub fn set_anc_mode(
"off" => AncMode::Off,
"anc" => AncMode::Anc,
"transparency" => AncMode::Transparency,
// XH1 adaptive modes — command is accepted but will no-op in execute_command
// (wire format unverified). Logged server-side for debugging.
"adaptive_self" => AncMode::AdaptiveSelf,
"adaptive_indoor" => AncMode::AdaptiveIndoor,
"adaptive_outdoor" => AncMode::AdaptiveOutdoor,
"adaptive_commute" => AncMode::AdaptiveCommute,
other => return Err(format!("unknown mode: {other}")),
};
let byte = level.unwrap_or(0x68);
Expand Down
97 changes: 21 additions & 76 deletions apps/baseus-app/src-tauri/src/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use std::time::Duration;

use baseus_protocol::{
framing::Frame,
models::{bp1_pro_anc::Bp1ProAnc, inspire_xh1::InspireXh1},
types::{AncMode, BaseusModel, DeviceEvent, EqPreset, ModelStatus},
models::bp1_pro_anc::Bp1ProAnc,
types::{AncMode, BaseusModel, DeviceEvent, EqPreset},
};
use baseus_transport::win::ble::GattTransport;
use serde::Serialize;
Expand Down Expand Up @@ -45,7 +45,6 @@ pub fn command_channel() -> (CommandSender, CommandReceiver) {
#[derive(Debug, Clone, Serialize)]
pub struct ModelInfo {
pub name: &'static str,
pub status: ModelStatus,
}

pub async fn run_loop(app: AppHandle, mut cmd_rx: CommandReceiver) {
Expand Down Expand Up @@ -87,7 +86,6 @@ pub async fn run_loop(app: AppHandle, mut cmd_rx: CommandReceiver) {
"model-info",
&ModelInfo {
name: connected_model.display_name(),
status: connected_model.status(),
},
);

Expand Down Expand Up @@ -130,11 +128,7 @@ async fn notification_loop(
tracing::debug!("raw notification: {}", hex(&data));
if let Ok(frame) = Frame::decode(&data) {
let event = match model {
// XP1/XC1 share the same earbud protocol as BP1 (same 0x0C ANC
// family, same battery framing) — APK-extracted, unverified.
BaseusModel::Bp1ProAnc
| BaseusModel::InspireXp1
| BaseusModel::InspireXc1 => {
BaseusModel::Bp1ProAnc => {
if frame.cmd == 0x34 {
// Ack payload semantics vary by firmware (issue #3):
// some units send a flat AA 34 01 for every ANC command,
Expand All @@ -153,15 +147,6 @@ async fn notification_loop(
}
}
}
BaseusModel::InspireXh1 => {
match InspireXh1::decode_frame(&frame) {
Ok(ev) => Some(ev),
Err(e) => {
tracing::debug!("unhandled frame cmd={:#04x}: {e}", frame.cmd);
None
}
}
}
};
if let Some(ev) = event {
maybe_alert_battery(app, &ev, &mut thresholds);
Expand Down Expand Up @@ -236,56 +221,28 @@ async fn execute_command(
model: BaseusModel,
) -> Result<(), String> {
let bytes: Vec<u8> = match (cmd, model) {
// BP1 / XP1 / XC1 — verified for BP1; XP1/XC1 assumed same protocol (APK-extracted).
(
DeviceCommand::SetAncMode(AncMode::Off, _),
BaseusModel::Bp1ProAnc | BaseusModel::InspireXp1 | BaseusModel::InspireXc1,
) => vec![0xBA, 0x34, 0x00, 0xFF],
(
DeviceCommand::SetAncMode(AncMode::Anc, level),
BaseusModel::Bp1ProAnc | BaseusModel::InspireXp1 | BaseusModel::InspireXc1,
) => vec![0xBA, 0x34, 0x01, *level],
(
DeviceCommand::SetAncMode(AncMode::Transparency, level),
BaseusModel::Bp1ProAnc | BaseusModel::InspireXp1 | BaseusModel::InspireXc1,
) => vec![0xBA, 0x34, 0x02, *level],
(
DeviceCommand::SetEqPreset(preset),
BaseusModel::Bp1ProAnc | BaseusModel::InspireXp1 | BaseusModel::InspireXc1,
) => vec![0xBA, 0x43, preset.to_byte()],
// Game/low-latency mode — verified for BP1 over both SPP and BLE (issue #3).
(
DeviceCommand::SetGameMode(on),
BaseusModel::Bp1ProAnc | BaseusModel::InspireXp1 | BaseusModel::InspireXc1,
) => vec![0xBA, 0x24, u8::from(*on)],
(
DeviceCommand::FindEarbud(Side::Left),
BaseusModel::Bp1ProAnc | BaseusModel::InspireXp1 | BaseusModel::InspireXc1,
) => vec![0xBA, 0x10, 0x00, 0x01],
(
DeviceCommand::FindEarbud(Side::Right),
BaseusModel::Bp1ProAnc | BaseusModel::InspireXp1 | BaseusModel::InspireXc1,
) => vec![0xBA, 0x10, 0x01, 0x01],
// Inspire XH1 — setting ANC/EQ not yet supported (wire format unverified).
(DeviceCommand::SetAncMode(mode, _), BaseusModel::InspireXh1) => {
tracing::info!("XH1 ANC set for {mode:?} not yet supported — wire format unverified");
return Ok(());
// BP1 Pro ANC — verified on hardware.
(DeviceCommand::SetAncMode(AncMode::Off, _), BaseusModel::Bp1ProAnc) => {
vec![0xBA, 0x34, 0x00, 0xFF]
}
(DeviceCommand::SetEqPreset(_), BaseusModel::InspireXh1) => {
tracing::info!("XH1 EQ preset not yet supported — wire format unverified");
return Ok(());
(DeviceCommand::SetAncMode(AncMode::Anc, level), BaseusModel::Bp1ProAnc) => {
vec![0xBA, 0x34, 0x01, *level]
}
(DeviceCommand::SetGameMode(_), BaseusModel::InspireXh1) => {
tracing::info!("XH1 game mode not yet supported — wire format unverified");
return Ok(());
(DeviceCommand::SetAncMode(AncMode::Transparency, level), BaseusModel::Bp1ProAnc) => {
vec![0xBA, 0x34, 0x02, *level]
}
(DeviceCommand::SetEqPreset(preset), BaseusModel::Bp1ProAnc) => {
vec![0xBA, 0x43, preset.to_byte()]
}
// Game/low-latency mode — verified for BP1 over both SPP and BLE (issue #3).
(DeviceCommand::SetGameMode(on), BaseusModel::Bp1ProAnc) => {
vec![0xBA, 0x24, u8::from(*on)]
}
(DeviceCommand::FindEarbud(_), BaseusModel::InspireXh1) => {
tracing::info!("XH1 find not yet supported");
return Ok(());
(DeviceCommand::FindEarbud(Side::Left), BaseusModel::Bp1ProAnc) => {
vec![0xBA, 0x10, 0x00, 0x01]
}
// Adaptive ANC modes — only used by XH1; unreachable for earbud models.
(DeviceCommand::SetAncMode(_, _), _) => {
return Err("ANC mode not supported for this model".to_string());
(DeviceCommand::FindEarbud(Side::Right), BaseusModel::Bp1ProAnc) => {
vec![0xBA, 0x10, 0x01, 0x01]
}
};
transport.send(&bytes).await.map_err(|e| e.to_string())
Expand Down Expand Up @@ -325,18 +282,6 @@ fn maybe_alert_battery(app: &AppHandle, event: &DeviceEvent, thresholds: &mut Ba
thresholds.left_was_ok = left_now_ok;
thresholds.right_was_ok = right_now_ok;
}
DeviceEvent::HeadphoneBatteryUpdate(h) => {
let now_ok = h.pct >= LOW || h.pct == 0;
if thresholds.left_was_ok && !now_ok {
let _ = app
.notification()
.builder()
.title("Baseus — Headphone low")
.body(format!("{}% remaining", h.pct))
.show();
}
thresholds.left_was_ok = now_ok;
}
DeviceEvent::CaseUpdate(c) => {
let case_now_ok = c.case_pct >= LOW || c.case_pct == 0;
if thresholds.case_was_ok && !case_now_ok {
Expand Down
2 changes: 1 addition & 1 deletion apps/baseus-app/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "baseus-app",
"version": "0.2.1",
"version": "0.3.0",
"identifier": "com.baseus.desktop",
"build": {
"beforeDevCommand": "pnpm dev",
Expand Down
Loading
Loading