diff --git a/README.md b/README.md index 5bf116b..e727a9a 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/cpp/src/devices.h b/cpp/src/devices.h index b9f23eb..cd767a0 100644 --- a/cpp/src/devices.h +++ b/cpp/src/devices.h @@ -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}; diff --git a/cpp/tests/test_connection.cpp b/cpp/tests/test_connection.cpp index 80e30b5..bd166e2 100644 --- a/cpp/tests/test_connection.cpp +++ b/cpp/tests/test_connection.cpp @@ -100,6 +100,12 @@ TEST(qc35_no_eq) { ASSERT_FALSE(dev.has_feature("mode_config")); } +TEST(prince_has_eq) { + auto t = std::make_unique(); + 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')); } diff --git a/docs/architecture.md b/docs/architecture.md index cf94ea3..21f15bd 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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] | diff --git a/docs/quietcomfort-headphones-prince.md b/docs/quietcomfort-headphones-prince.md index 3581e10..3356343 100644 --- a/docs/quietcomfort-headphones-prince.md +++ b/docs/quietcomfort-headphones-prince.md @@ -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: @@ -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`. diff --git a/python/pybmap/devices/qc_prince.py b/python/pybmap/devices/qc_prince.py index 2e8fe91..1831407 100644 --- a/python/pybmap/devices/qc_prince.py +++ b/python/pybmap/devices/qc_prince.py @@ -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), }, diff --git a/python/tests/test_qc_prince.py b/python/tests/test_qc_prince.py new file mode 100644 index 0000000..3f43402 --- /dev/null +++ b/python/tests/test_qc_prince.py @@ -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] diff --git a/rust/src/devices.rs b/rust/src/devices.rs index e36ac21..8e00b1b 100644 --- a/rust/src/devices.rs +++ b/rust/src/devices.rs @@ -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, @@ -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]);