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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ connection to the headphones.
| Device | NC Control | EQ | Spatial | Profiles | Buttons | Status |
| --------------------------- | ------------------------------------- | ------ | -------------- | --------------------- | ---------------------- | ------------------ |
| **QC Ultra Headphones 2** | CNC 0-10 slider | 3-band | room/head | 7 custom slots | Shortcut remap | Verified |
| **QuietComfort Headphones** | CNC 0-10 + Wind Block via ModeConfig | — | field observed | 2 user slots observed | — | Verified (`prince`) |
| **QuietComfort Headphones** | CNC 0-10 + Wind Block via ModeConfig | 3-band | field observed | 2 user slots observed | — | Verified (`prince`) |
| **QuietComfort 35 / 35 II** | ANR off/high/wind/low | — | — | — | Action remap (VPA/ANC) | Verified |
| **QuietComfort Earbuds** | CNC 0-10 via direct SETGET | 3-band | — | 4 fixed modes | Remap | Verified (`lando`) |
| **QuietComfort 45** | CNC 0-10 via ModeConfig | 3-band | — | 2 user slots | Remap | Inferred (`duran`), untested on hardware |
Expand Down
1 change: 1 addition & 0 deletions cpp/src/devices.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ inline DeviceConfig qc_prince() {
c.product_name = Addr{1, 2};
c.voice_prompts = Addr{1, 3};
c.cnc = Addr{1, 5};
c.eq = Addr{1, 7};
c.pairing = Addr{4, 8};
c.get_all_modes = Addr{31, 1};
c.current_mode = Addr{31, 3};
Expand Down
6 changes: 6 additions & 0 deletions cpp/tests/test_connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ TEST(qc35_no_eq) {
ASSERT_FALSE(dev.has_feature("mode_config"));
}

TEST(prince_has_eq) {
auto t = std::make_unique<MockTransport>();
BmapConnection dev(std::move(t), qc_prince());
ASSERT_TRUE(dev.has_feature("eq"));
}

TEST(set_name_rejects_long_name) {
bool threw = false;
try { mock_qc_ultra2()->set_name(std::string(32, 'x')); }
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ Each feature entry maps a name to its protocol address and codec functions:
| RFCOMM channel | 2 | 8 | 8 |
| Init packet | None | None | GET [0.1] required |
| Noise control | CNC [1.5] / live [31.10] | CNC/wind via [31.6] ModeConfig | ANR [1.6] (off/high/wind/low) |
| EQ | 3-band [1.7] | Not verified | Not supported |
| EQ | 3-band [1.7] | 3-band [1.7] | Not supported |
| Spatial audio | [31.6] ModeConfig | Field observed in [31.6] | Not supported |
| Mode profiles | 7 editable slots (4-10) | 2 editable slots observed (2-3) | None |
| Sidetone | [1.11] | Not verified | [1.11] |
Expand Down
19 changes: 17 additions & 2 deletions docs/quietcomfort-headphones-prince.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,21 @@ wind: 0 -> 1 -> 0

These writes applied immediately when the edited slot was the current mode.

## EQ

`prince` exposes the same 3-band equalizer register as `qc45` and
`qc_ultra2`:

```
[1.7] RangeControl/EQ GET/SETGET
```

A GET returns 4-byte groups `[min, max, current, band_id]` with signed
`-10..+10` values. SETGET writes one band per packet as `[value, band_id]`
(`0` = bass, `1` = mid, `2` = treble). Verified on firmware
1.0.6-80+f5f219b: a read returned Bass +6 / Mid +5 / Treble +6, and a write
round-tripped through a subsequent read.

## Unsupported or Not Yet Verified

The tested firmware does not support QC Ultra 2's direct live settings register:
Expand All @@ -179,5 +194,5 @@ No unauthenticated true ANC-off toggle was found:
| `[1.5]` SettingsCnc SETGET with disable flag | rejected/auth gated |

The verified local controls are mode switching, editable-mode raw CNC level,
and Wind Block. EQ, buttons, multipoint, power, and true ANC off remain
unverified for `prince`.
Wind Block, and 3-band EQ. Buttons, multipoint, power, and true ANC off
remain unverified for `prince`.
7 changes: 7 additions & 0 deletions python/pybmap/devices/qc_prince.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@
"addr": (1, 5),
"parser": parsers.parse_cnc,
},
# 3-band EQ. Same Settings-block register as qc45/qc_ultra2/qc_earbuds;
# verified working on prince firmware 1.0.6-80+f5f219b (range -10..+10).
"eq": {
"addr": (1, 7),
"parser": parsers.parse_eq,
"builder": parsers.build_eq_band,
},
"pairing": {
"addr": (4, 8),
},
Expand Down
40 changes: 40 additions & 0 deletions python/tests/test_qc_prince.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Tests for QuietComfort Headphones (prince) device configuration."""

from pybmap.devices import qc_prince, parsers, DEVICES


class TestQCPrinceConfig:
def test_qc_prince_registered(self):
assert "qc_prince" in DEVICES

def test_has_device_info(self):
assert qc_prince.DEVICE_INFO["product_id"] == 0x4075
assert qc_prince.DEVICE_INFO["codename"] == "prince"

def test_has_core_features(self):
for feat in ["battery", "firmware", "product_name", "voice_prompts",
"cnc", "eq", "pairing"]:
assert feat in qc_prince.FEATURES, "Missing feature: %s" % feat

def test_rfcomm_channel_8(self):
assert qc_prince.RFCOMM_CHANNEL == 8

def test_eq_register_and_parsers(self):
eq = qc_prince.FEATURES["eq"]
assert eq["addr"] == (1, 7)
assert eq["parser"] is parsers.parse_eq
assert eq["builder"] is parsers.build_eq_band

def test_eq_builder_encodes_signed_value(self):
assert parsers.build_eq_band(-2, 0) == bytes([0xFE, 0x00])
assert parsers.build_eq_band(3, 2) == bytes([0x03, 0x02])

def test_no_audio_settings(self):
assert "audio_settings" not in qc_prince.FEATURES

def test_preset_modes(self):
assert qc_prince.PRESET_MODES["quiet"]["idx"] == 0
assert qc_prince.PRESET_MODES["aware"]["idx"] == 1

def test_editable_slots(self):
assert qc_prince.EDITABLE_SLOTS == [2, 3]
3 changes: 2 additions & 1 deletion rust/src/devices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ pub fn qc_prince() -> DeviceConfig {
product_name: Some(Addr(1, 2)),
voice_prompts: Some(Addr(1, 3)),
cnc: Some(Addr(1, 5)),
eq: None,
eq: Some(Addr(1, 7)),
buttons: None,
multipoint: None,
sidetone: None,
Expand Down Expand Up @@ -310,6 +310,7 @@ mod tests {
assert_eq!(dev.info.codename, "prince");
assert_eq!(dev.rfcomm_channel, 8);
assert!(dev.mode_config.is_some());
assert!(dev.eq.is_some());
assert!(dev.audio_settings.is_none());
assert!(!dev.supports_anc_toggle);
assert_eq!(dev.editable_slots, &[2, 3]);
Expand Down