Bose QC Ultra 2
April 13, 2026 · View on GitHub
TL;DR
We can control the headphones from Linux without the Bose app, Bose account, or cloud.
The AudioModes function block (block 31) accepts the START operator without
authentication. This lets us switch ANC modes, read battery, read all device info,
and more — all over a direct Bluetooth RFCOMM connection.
Bose locked down SET/SETGET operators behind cloud-mediated ECDH authentication, but left the START operator on AudioModes wide open. This is the same operator the app uses to change modes in real time.
Device Info
- Product: Bose QC Ultra 2 HP (codename "wolverine")
- Platform: OTG-QCC-384 (Qualcomm QCC chipset)
- Firmware: 8.2.20+g34cf029
- BT MAC: 68:F2:1F:XX:XX:XX
- Serial: 085958TXXXXXXXXXX
- Custom name: "Fargo"
- Product ID: 0x4082, Variant: 0x01
BMAP Protocol
- Version: 1.1.0
- Transport: Bluetooth SPP over RFCOMM channel 2
- Packet format:
[fblock_id, function_id, flags, payload_length, ...payload] - Flags byte:
(device_id << 6) | (port_num << 4) | (operator & 0x0F) - Operators: SET=0, GET=1, SET_GET=2, STATUS=3, ERROR=4, START=5, RESULT=6, PROCESSING=7
What Works Without Authentication
Reading (GET operator — all blocks)
Everything can be read without auth:
- Battery level, firmware version, serial number, product name
- Current ANC mode, EQ settings, connected devices
- All device info across all function blocks
Writing — AudioModes (Block 31, START operator)
This is the breakthrough. The START operator on block 31 is unauthenticated.
Changing ANC/Audio Mode
Packet: [31, 3, 0x05, 2, MODE_INDEX, VOICE_PROMPT]
- Block 31 (AudioModes), Function 3 (CurrentMode), Operator START (5)
- MODE_INDEX: which mode to activate (see table below)
- VOICE_PROMPT: 0=silent, 1=play voice prompt
- Response: RESULT with the new mode index on success
Available Audio Modes
| Index | Name | Description | Config byte |
|---|---|---|---|
| 0 | Quiet | Full active noise cancellation | 0x01 |
| 1 | Aware | Transparency / passthrough | 0x02 |
| 2 | Immersion | Spatial audio immersive | 0x22 |
| 3 | Cinema | Spatial audio cinema | 0x24 |
| 4 | Home | Custom home profile | 0x0a |
| 5 | None | Empty/custom slot | 0x00 |
| 6 | None | Empty/custom slot | 0x00 |
| 7 | None | Empty/custom slot | 0x00 |
Other Unauthenticated START Commands (Block 31)
| Function | Name | Notes |
|---|---|---|
| [31.1] | GetAll | Returns PROCESSING — triggers full state dump |
| [31.3] | CurrentMode | Mode switching — confirmed working |
| [31.6] | ModeConfig | Returns all mode configs as STATUS messages |
| [31.9] | Reset | Accepts START (InvalidData with empty payload) |
Unauthenticated START in Other Blocks
| Function | Name | Status | Notes |
|---|---|---|---|
| [7.1] | Control.GetAll | PROCESSING | Triggers control state dump |
| [7.4] | Control.Power | RESULT | 0=power off, 1=power on |
| [4.1] | DevMgmt.Connect | InvalidData | Needs device MAC — may initiate BT connection |
| [4.8] | DevMgmt.PairingMode | RESULT | 0x01=enable, 0x00=disable |
| [4.12] | DevMgmt.Routing | RESULT | Switch active multipoint device (see below) |
| [5.1] | AudioMgmt.Source | GET only | Query active audio source (see below) |
| [5.3] | AudioMgmt.Control | InvalidData | Needs payload — play/pause/skip (see AudioControlValue) |
| [18.19] | ValidatedDeviceIdentityKeypair | PROCESSING | Accepts public key for auth flow |
AudioModes SETGET — Full Config Control (No Auth!)
BREAKTHROUGH #2: SETGET on AudioModes block 31 is completely unauthenticated. Preset modes (0-4) are firmware-locked (return Runtime error 8), but custom mode slots (5-10) accept full configuration changes via SETGET. Combined with CurrentMode START to switch to the custom mode, this gives us complete control over CNC level, spatial audio, wind block, and ANC — all without any auth.
| Function | Operator | Status | Notes |
|---|---|---|---|
| [31.6] ModeConfig | SETGET | WORKS on modes 5-10 | Full config: CNC, spatial, wind, ANC |
| [31.6] ModeConfig | SET | Error 5 (auth) | SET is auth-gated, but SETGET isn't |
| [31.8] Favorites | SETGET | WORKS | Set favorite mode indices |
| [31.8] Favorites | SET | Error 5 (auth) | |
| [31.4] DefaultMode | SETGET | Timeout (may work) | Set power-on mode |
| [31.4] DefaultMode | SET | Error 5 (auth) | |
| [31.3] CurrentMode | SETGET | Error 5 (auth) | But START works for switching |
ModeConfig SETGET Payload Format (40 bytes)
Built by FBlockAudioModesKt.createAudioModesConfigSetGetPayload():
Offset Size Field Values
0 1 modeIndex 5-10 (custom slots only)
1-2 2 voicePrompt (byte1, byte2) — see AudioModesPrompt enum
3-34 32 modeName UTF-8, null-padded to 32 bytes
35 1 cncLevel 0-10 (noise cancellation intensity)
36 1 autoCNCEnabled 0=off, 1=on
37 1 spatialAudioType 0=off, 1=fixedToRoom, 2=fixedToHead
38 1 windBlockEnabled 0=off, 1=on
39 1 ancToggleEnabled 0=off, 1=on
ModeConfig STATUS Response Format (48 bytes)
The firmware adds 3 flag bytes and extra config fields:
Offset Size Field Notes
0 1 modeIndex
1-2 2 voicePrompt
3-5 3 flags [3]=isUserEditable, [4]=isConfigured, [5]=?
6-37 32 modeName
38-39 2 ? Always 0 for custom modes
40-41 2 ? Mode-type specific (0x1d for custom modes)
42 1 cncLevel 0-10
43 1 autoCNCEnabled
44 1 spatialAudioType 0=off, 1=room, 2=head
45 1 windBlockEnabled
46 1 ?
47 1 ancToggleEnabled
Preset modes have flags[3]=0x00 (locked); custom/user modes have flags[3]=0x01 (writable).
| Mode | Name | Editable | Configured | Notes |
|---|---|---|---|---|
| 0 | Quiet | No | No | Firmware preset |
| 1 | Aware | No | No | Firmware preset |
| 2 | Immersion | No | No | Firmware preset |
| 3 | Cinema | No | No | Firmware preset |
| 4 | Home | Yes | Yes | User-created in app |
| 5-10 | None/Custom | Yes | No | Empty slots, fully configurable |
Mode 4 (Home) is editable because it was created by the user via the app. Modes 5-10 are empty slots that accept full configuration. The cloud auth the app uses is likely for syncing profiles across devices, not for writing to the headphone firmware — SETGET bypasses it entirely.
The real CNC path: [31.10] AudioModesSettingsConfig
The ModeConfig-slot approach works for configuring stored profiles, but for
live CNC/spatial/wind/ANC control, use [31.10] AudioModesSettingsConfig
SETGET directly. This is the same register the Bose app writes to, fully
unauthenticated, and applies immediately without mode switching.
# [31.10] SETGET, 5-byte payload: [cnc, autoCNC, spatial, wind, anc]
# cnc: 0-10 (INVERTED: 0=max ANC, 10=most ambient)
# autoCNC: 0 only (1 is rejected with Runtime error 8)
# spatial: 0=off, 1=room, 2=head
# wind: 0=off, 1=on
# anc: 0=off, 1=on
send(bmap_packet(31, 10, OP_SETGET, [5, 0, 0, 0, 1]))
# ↑ CNC level 5, autoCNC off, spatial off, wind block off, ANC on
Critical audibility interaction: the CNC level only produces an audible
difference when anc=on AND wind=off. Wind Block masks the CNC DSP path
(probably to prevent wind compression from fighting ANC). With wind on,
CNC 0 and CNC 10 sound identical.
Also: autoCNCEnabled=1 causes Runtime error 8 — firmware rejects it.
Only manual CNC (auto_cnc=0) is allowed.
Mode Config Details (raw data)
Mode 0 (Quiet): 000001000001 "Quiet" ...00000001
Mode 1 (Aware): 0100020000014 "Aware" ...020a0000000001
Mode 2 (Immersion): 020022000001 "Immersion" ...0002000001
Mode 3 (Cinema): 0300240000004 "Cinema" ...0001000001
Mode 4 (Home): 04000a010100 "Home" ...1d000000010001
Mode 5 (None): 0500000100004 "None" ...1d0a0000010001
Mode 6 (None): 0600000100004 "None" ...1d0a0000010001
Mode 7 (None): 0700000100004 "None" ...1d0a0000010001
Writing — Settings Block SETGET (Also Unauthenticated!)
BREAKTHROUGH #3: The SETGET operator on the Settings block [1.x] is ALSO unauthenticated! SET and START are auth-gated (error 5), but SETGET bypasses auth.
Settings SETGET Summary
All of these use [1, FUNC, 0x02, LEN, ...PAYLOAD] (block 1, operator SETGET):
| Func | Name | Payload (SETGET) | Notes |
|---|---|---|---|
| [1.2] | ProductName | UTF-8 string (no flag byte) | Free-form device name |
| [1.3] | VoicePrompts | 1 byte: (enabled<<5)|lang | See language table below |
| [1.5] | CNC | — | AUTH REQUIRED (use [31.6] instead) |
| [1.7] | EQ/Range | 2 bytes: [value, bandId] | value=-10..+10, band=0/1/2 |
| [1.9] | Buttons | 3 bytes: [btnId, evt, mode] | See button tables below |
| [1.10] | Multipoint | 1 byte: 0=off, 1=on | |
| [1.11] | Sidetone | 2 bytes: [persist, mode] | mode: 0=off,1=high,2=med,3=low |
| [1.24] | AutoPlayPause | 1 byte: 0=off, 1=on | Pause on ear removal |
| [1.27] | AutoAnswer | 1 byte: 0=off, 1=on | Auto-answer calls |
EQ Details [1.7]
SETGET payload: [VALUE, BAND_ID]
- VALUE: -10 to +10 as signed byte (0xf6 to 0x0a)
- BAND_ID: 0=Bass, 1=Mid, 2=Treble
GET response: 3× 4-byte groups [min, max, current, bandId]
- e.g. f60a0500 f60a0001 f60af702 = bass=+5, mid=0, treble=-9
Voice Prompt Languages [1.3]
| ID | Language | ID | Language |
|---|---|---|---|
| 0 | UK English | 12 | Hebrew |
| 1 | US English | 13 | Turkish |
| 2 | French | 14 | Dutch |
| 3 | Italian | 15 | Japanese |
| 4 | German | 16 | Cantonese |
| 5 | EU Spanish | 17 | Arabic |
| 6 | MX Spanish | 18 | Swedish |
| 7 | BR Portuguese | 19 | Danish |
| 8 | Mandarin | 20 | Norwegian |
| 9 | Korean | 21 | Finnish |
| 10 | Russian | 22 | Hindi |
| 11 | Polish |
Button Remapping [1.9]
GET Response Format (7+ bytes)
[0] buttonId — which physical button
[1] buttonEventType — what gesture triggers it
[2] configuredAction — current assigned action
[3:7] supportedMask — bitmask of supported ActionButtonMode values
[7:] unavailableMask — bitmask of unavailable modes (optional)
SETGET Payload (3 bytes)
[buttonId, buttonEventType, newActionMode]
Button IDs (ConfigurableButtonId)
| ID | Name |
|---|---|
| 0x00 | DistalCnc (CNC button) |
| 0x01 | Reserved |
| 0x02 | VPA (voice assistant) |
| 0x03 | RightShortcut |
| 0x04 | LeftShortcut |
| 0x80 | Shortcut (QC Ultra 2 programmable button) |
Button Event Types
| ID | Gesture |
|---|---|
| 0 | Reserved |
| 1 | Rising edge |
| 2 | Falling edge |
| 3 | Short press |
| 4 | Single press |
| 5 | Press and hold |
| 6 | Double press |
| 7 | Double press and hold |
| 8 | Triple press |
| 9 | Long press |
| 10 | Very long press |
| 11 | Very very long press |
| 12 | Very very very long press |
Action Button Modes
| ID | Action | ID | Action |
|---|---|---|---|
| 0 | NotConfigured | 11 | TrackBack |
| 1 | VPA (voice assistant) | 12 | FetchNotifications |
| 2 | ANC cycle | 13 | WindMode |
| 3 | BatteryLevel | 14 | Disabled |
| 4 | PlayPause | 15 | ClientInteraction |
| 5 | IncreaseCNC | 16 | SpotifyGo |
| 6 | DecreaseCNC | 17 | ModesCarousel |
| 7 | ToggleWakeWord | 19 | SpatialAudioMode |
| 8 | SwitchDevice | 20 | LineInSwitch |
| 9 | ConversationMode | 21 | Linking |
| 10 | TrackForward |
Current Config (QC Ultra 2)
Button 0x80 (Shortcut), long_press → Disabled
Supported actions: SwitchDevice, TrackBack, unknown(22), unknown(25)
Raw: 80090e00094002
Note: Button remapping via SETGET is confirmed to accept payloads (tested echo-back)
but has not been tested with actual mode changes yet. The supported action bitmask
indicates only a few actions are available for this button/event combo.
To remap: bose raw "01 09 02 03 80 09 08" (Shortcut, long_press → SwitchDevice)
What Requires Authentication (Blocked)
SET and START operators on most blocks return error 5 (OpNotSupp) without completing cloud-mediated ECDH authentication. However, SETGET is often unauthenticated — Bose only gated SET and START.
Confirmed auth-blocked (SET/START only):
- Settings.ProductName [1.2] — device name (SET blocked, SETGET untested)
- Settings.Multipoint [1.10] — multipoint toggle
- Settings.StandbyTimer [1.4] — auto-off timer
- AudioManagement.Control [5.3] — play/pause/skip (SET/START blocked)
Confirmed SETGET works without auth:
- Settings.EQ [1.7] — full equalizer control (-10 to +10, 3 bands)
- AudioModes.ModeConfig [31.6] — CNC, spatial, wind, ANC on custom modes
- AudioModes.Favorites [31.8] — favorite mode selection
RFCOMM Channels
| Channel | Purpose |
|---|---|
| 1 | SPP (connection refused) |
| 2 | BMAP control — primary protocol channel |
| 8 | Refused |
| 14 | Status beacon (sends ff5502... periodically) |
| 22 | Diagnostic/log stream (not BMAP, dumps data regardless of input) |
| 24 | Silent (purpose unknown) |
USB Interface
The headphones expose two USB HID interfaces when connected via USB-C:
| Interface | Class | Device | Purpose |
|---|---|---|---|
| 0 | Vendor HID | hidraw9 | BMAP control channel (bidirectional) |
| 1 | Consumer HID | hidraw10 | Media keys (play/pause/volume) |
| 2 | Audio Control | — | USB audio control |
| 3 | Audio Streaming | — | USB audio data (mic, isochronous) |
USB HID Report IDs (Interface 0, Vendor Specific 0xFF00)
| Report ID | Direction | Size | Likely Purpose |
|---|---|---|---|
| 0x09 | IN | 126 bytes | Small BMAP responses |
| 0x0a | FEATURE | 62 bytes | Feature report (config?) |
| 0x0c | OUT | 1022 bytes | BMAP commands (main) |
| 0x0d | IN | 259 bytes | BMAP responses (main) |
| 0x0e | OUT | 512 bytes | BMAP commands (alt) |
| 0x0f | IN | 512 bytes | BMAP responses (alt) |
| 0x10 | OUT | 675 bytes | BMAP commands (large) |
| 0x11 | IN | 675 bytes | BMAP responses (large) |
USB Status
Sending BMAP packets in report 0x0c gets a response on 0x0d, but always the
same 3 bytes: 04 01 05. This appears to be a "not initialized" error — the
USB BMAP channel likely requires a handshake sequence before accepting commands.
Over Bluetooth RFCOMM, BMAP is immediately active. The USB HID channel appears to be firmware update only, not general BMAP control:
- The Bose Updater (Windows, Qt/C++ app) uses a modified HIDAPI library to communicate over USB HID, downloads firmware from AWS S3, and pushes it through HID reports
- The
04 01 05response to all our BMAP attempts is likely "not in DFU mode" - The updater probably sends a special command to enter firmware update mode
- No desktop Bose app exists for general headphone control — only the updater
Conclusion: USB is for firmware updates. Use Bluetooth RFCOMM for control.
USB Device Info
Vendor: 0x05a7 (Bose Corporation)
Product: 0x4082 (Bose QC Ultra 2 HP)
Speed: Full Speed (12 Mbps)
Serial: T5333020XXXXXXXXXXXXX
Function Block Map
| Block | Name | Functions found |
|---|---|---|
| 0 | ProductInfo | 0,1,2,3,5,6,7,12,15,17,23 |
| 1 | Settings | 0,2,3,5,7,9,10,11,12,24,27 |
| 2 | Status | 0,2,5,16,21 |
| 3 | FirmwareUpdate | 0,1,4,6,7,15,16 |
| 4 | DeviceManagement | 0,1,4,8,9,14,18 |
| 5 | AudioManagement | 0,1,3,4,5,7,13,17 |
| 6 | CallManagement | 0 |
| 7 | Control | 0,1,4 |
| 8 | Debug | 7,8 |
| 9 | Notification | 0,2 |
| 18 | Authentication | 0,1,9,11,12,13,24 |
| 31 | AudioModes | 0,2,3,4,8,10,11 |
Settings Functions (Block 1)
| Func | Name | Read value | Notes |
|---|---|---|---|
| 0 | FblockInfo | "1.1.0" | |
| 2 | ProductName | "Fargo" | |
| 3 | VoicePrompts | 41000081020000 | |
| 5 | CNC | 0b0003 | [numSteps=11, step=0, flags=3] |
| 7 | RangeControl/EQ | f60a0000f60a0001f60a0002 | 3-band EQ, all at 0 |
| 9 | Buttons | 80090e00094002 | |
| 10 | Multipoint | 07 | |
| 11 | Sidetone | 01020f | |
| 12 | SetupComplete | 01 | |
| 24 | AutoPlayPause | 01 | |
| 27 | AutoAnswer | 01 |
Status Functions (Block 2)
| Func | Name | Read value | Decoded |
|---|---|---|---|
| 2 | BatteryLevel | 50ffff00 | 0x50 = 80% battery |
CNC (Noise Cancellation) Details
- Read: GET [1.5] returns
[numSteps, currentStep, flags]- numSteps: total steps (11 on this device = 0-10)
- currentStep: current level (0=min, 10=max)
- flags: bit0=isEnabled, bit1=!userEnableDisable
- Write: SetGet [1.5] with payload
[step, enabled?1:0]- Requires authentication — returns OpNotSupp error 5 without auth
EQ Details
- 3-band equalizer stored in [1.7]
- Format: 3x 4-byte groups
[f60a, VALUE, BAND_INDEX] - Values are signed bytes (f7=-9, 00=center, 0a=+10)
- Band 0=Bass, 1=Mid, 2=Treble
Authentication System (Block 18)
Overview
Cloud-mediated ECDH P-384 challenge-response. The headphones require a signature
from Bose's cloud servers (nadc.data.api.bose.io) before granting SET/SETGET
privileges. The app acts as a proxy between headphones and cloud.
Auth Capabilities (from [18.1] bitmask 0339083e07)
Supported: FblockInfo, GetAll, CondensedChallenge, OtpKeyType, ProductName, PlatformName, ValidatedDeviceIdentityKeypair, PropagateProductIrk, NoTokenChallenge, ProductToCloudChallenge, ProductToCloudChallengeVerifyResponse, CloudToProductChallenge, GoogleFeatureKeys, GoogleFeatureKeyData
Auth Device Info
- Device ECDH public key at [18.9]: P-384 PEM format
- Product name [18.12]: "wolverine"
- Platform [18.13]: "OTG-QCC-384"
- OTP key type [18.11]: 3
- Product IRK [18.24]:
3713b952XXXXXXXXXXXXXXXXXXXXXXXX
Hypothesized Auth Flow
- App generates ephemeral ECDH keypair
- [18.19] START → App sends public key → headphones return PROCESSING
- [18.27] START → Headphones generate challenge → app forwards to Bose cloud
- Bose cloud signs the challenge
- [18.28] → App sends cloud response back to headphones
- [18.29] → Cloud-to-product verification
- Headphones grant SET/SETGET privileges for this session
Cloud API
- Primary API:
nadc.data.api.bose.io(uses QUIC/HTTP3, falls back to HTTPS) - Identity:
id.api.bose.io/id-idp-mgr-core/ - Services:
services.api.bose.io(Apigee gateway) - Config:
nadc-config.data.api.bose.io - Firmware:
ota.cdn.bose.io,updates-framingham-prod.smartproducts.bose.io - API key system called "Galapagos" — key fetched from remote config
- App has certificate pinning — rejects user-installed CA certs
Why Auth Bypass Works
Bose protected SET (operator 0) and SET_GET (operator 2) behind cloud auth. But START (operator 5) on the AudioModes block was left unprotected. The app uses START to change modes in real time (it's the "instant switch" path). SET_GET is used for persistent config changes. This distinction means we can control the headphones in real time but can't change saved configuration.
Audio Source & Device Routing
AudioManagement Source [5.1] — Query Active Source
GET-only function. Response payload:
[0-1] Supported sources (bitset, 2 bytes)
[2] Active source type: 0=NONE, 1=BLUETOOTH, 2=AUXILIARY
[3+] Source-specific data (BLUETOOTH: 6 bytes MAC address)
Source types from AudioControlSourceType.java:
- NONE (0x00) — no active source
- BLUETOOTH (0x01) — BT A2DP, 6 bytes additional data (MAC)
- AUXILIARY (0x02) — 3.5mm line-in, no additional data
No SET/START on [5.1] — source switching happens implicitly (plug in aux, or route a BT device via [4.12]).
DeviceManagement Routing [4.12] — Switch Active BT Device
START operator to route audio to a specific paired BT device (multipoint switch).
Payload (7 bytes):
[0] Flags: 0x82 (bit7=UP routing direction, bit1=device slot)
[1-6] Target device MAC address (6 bytes)
Response handling (from DeviceManagementBmapPacketParser.java):
- RESULT: bytes [1-6] = MAC of now-active device (success)
- ERROR: bytes [0-1] = 16-bit error code
- STATUS: bytes [2-7] = MAC of newly routed device
ROUTING_TYPE enum: UP=1 (value << 7 = 0x80), DOWN=0.
AudioManagement Control [5.3] — Transport Controls
START operator with single-byte payload. Values from AudioControlValue.java:
0x00 STOP
0x01 PLAY
0x02 PAUSE
0x03 TRACK_FORWARD
0x04 TRACK_BACK
0x05 FAST_FORWARD_PRESS
0x06 FAST_FORWARD_RELEASE
0x07 REWIND_PRESS
0x08 REWIND_RELEASE
DeviceManagement Functions (Block 4) — Full Map
From DeviceManagementPackets.java:
| Func | Name | Notes |
|---|---|---|
| 0 | FblockInfo | |
| 1 | Connect | Needs device MAC |
| 2 | Disconnect | |
| 3 | RemoveDevice | |
| 4 | ListDevices | Returns paired device list |
| 5 | Info | Device info query |
| 7 | ClearDeviceList | |
| 8 | PairingMode | 0x01=enable, 0x00=disable |
| 9 | LocalMacAddress | |
| 10 | PrepareP2P | |
| 11 | P2PMode | |
| 12 | Routing | Switch active multipoint device |
BMAP Error Codes
| Code | Name | Description |
|---|---|---|
| 0 | Unknown | Unknown error |
| 1 | Length | Invalid length |
| 2 | Chksum | Invalid checksum |
| 3 | FblockNotSupp | Function block not supported |
| 4 | FuncNotSupp | Function not supported |
| 5 | OpNotSupp | Operator not supported (needs auth) |
| 6 | InvalidData | Data values incorrect |
| 7 | DataUnavailable | Requested data not available |
| 8 | Runtime | Temporary read/write failure |
| 9 | Timeout | Timeout |
| 10 | InvalidState | Not applicable to current state |
| 20 | InsecureTransport | Packet on insecure transport |
BMAP Protocol Class References
com.bose.bmap.messages.enums.spec.BmapFunctionBlock— block IDscom.bose.bmap.messages.enums.spec.BmapFunction— function IDscom.bose.bmap.messages.enums.spec.BmapOperator— operator IDscom.bose.bmap.messages.packets.AudioModesCurrentModeStartPacket— mode switchcom.bose.bmap.messages.packets.SettingsCncSetGetPacket— CNC controlcom.bose.bmap.service.SppConnectionManager— SPP UUID: 00001101-...com.bose.bmap.messages.models.settings.CncLevel— CNC response parsercom.bose.bmap.messages.responses.SettingsCncResponse— CNC payload formatcom.bose.bmap.utils.encryption.ECDH— secp256r1 key generationcom.bose.bmap.model.factories.AuthenticationPackets— auth error codes & capabilities bitmask
Tools
bmap-capture.py— Interactive setting change capture toolcaptures/— Captured setting toggle data (8 captures)snoop/— Network captures and bugreports
Quick Reference: Control Headphones from Linux
import socket
BOSE_MAC = "68:F2:1F:XX:XX:XX"
sock = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM)
sock.settimeout(2)
sock.connect((BOSE_MAC, 2))
# Switch to Quiet (full ANC): mode=0
sock.send(bytes([31, 3, 0x05, 2, 0, 0]))
# Switch to Aware (transparency): mode=1
sock.send(bytes([31, 3, 0x05, 2, 1, 0]))
# Switch to Immersion: mode=2
sock.send(bytes([31, 3, 0x05, 2, 2, 0]))
# Read battery level
sock.send(bytes([2, 2, 0x01, 0x00]))
resp = sock.recv(4096)
battery_pct = resp[4] # hex value, e.g. 0x50 = 80%
# Read current mode
sock.send(bytes([31, 3, 0x01, 0x00]))
resp = sock.recv(4096)
current_mode = resp[4] # 0=Quiet, 1=Aware, 2=Immersion, etc.
QC35 NoiseCancellation Block (Block 3) — Firmware 4.8.1
Block 3 is a separate NoiseCancellation fblock on the QC35, distinct from the Settings-level ANR control at [1.6]. Investigation results:
Functions
| Func | GET | SET/SETGET | START | Notes |
|---|---|---|---|---|
| 3.1 | 01 or 02 | Auth-gated (error 5) | Auth-gated | Binary NC system state |
| 3.2 | Auth-gated | Auth-gated | 14-byte payload accepted | NC state transition |
| 3.3 | Auth-gated | — | — | Unknown |
| 3.4 | 01000000020000000000 (10 bytes) | Auth-gated | — | NC config/capabilities |
| 3.5 | Auth-gated | — | — | Unknown |
| 3.6 | Empty STATUS | — | — | Unknown |
| 3.7 | Auth-gated | — | — | Unknown |
| 3.8+ | FuncNotSupp | — | — | Not implemented |
[3.2] START Payload Format (14 bytes)
Offset Size Field
0 1 currentState — must match [3.1] value
1-3 3 reserved (zeros)
4 1 targetState — must be a valid transition target
5-13 9 reserved (zeros)
Accepts exactly 14-17 bytes (Length error outside). Only valid transition found:
01 → 02 (RESULT returned, [3.1] changes). All others return error 15 or InvalidData.
[3.1] State Values
01= default state (after ANR changes, after power cycle)02= alternate state (reached via [3.2] START transition)
Key Findings
- [3.1] does not correlate with ANR mode — changing ANR via [1.6] does not affect [3.1]
- [3.4] config is static (
01 00 00 00 02 00 00 00 00 00) regardless of ANR mode - The [3.2] START is a binary state toggle, not continuous NC level control
- This is likely the low-level NC hardware enable/disable — not useful for user-facing control
- ANR at [1.6] remains the correct interface for NC mode control on QC35 firmware 4.8.1
- The continuous CNC slider (0-10) seen on older firmware (1.x) is not recoverable via block 3
Conclusion
Block 3 on QC35 firmware 4.8.1 is a low-level NC system control that was likely exposed on older firmware but is now mostly auth-gated. The only unauthenticated path ([3.2] START) is a binary state transition with no practical NC level control. The discrete ANR modes (off/high/wind/low) via [1.6] SETGET are the full extent of unauthenticated NC control on this firmware version.
Future Work
Crack ModeConfig SETGET payload formatDONE — full CNC/spatial/wind/ANC controlCrack Settings SETGETDONE — EQ, name, sidetone, multipoint, all workImplement button remapping [1.9]DONE — SETGET works, verified on QC35 and QC Ultra 2Try USB-C connectionPARTIALLY DONE — USB HID interface found, needs init handshake- Crack USB BMAP initialization — capture USB traffic from app to find handshake sequence
- Explore [31.9] AudioModes Reset — factory reset individual modes?
Reverse audio source/device routingDONE — [5.1] source query, [4.12] routing START, [5.3] transport controls- Implement [5.3] AudioManagement Control — play/pause/skip (payload format now known)
- Build a system tray widget / PipeWire integration
- Investigate firmware downgrade via bose-dfu over USB
- Map the unknown bytes [40-41] in ModeConfig STATUS (mode-type specific config?)