BTMON(1)
๐ธ BTMON(1) ๐ธ
okay so bestie, welcome to btmon(1) โ the Linux System Administration era of the Bluetooth world. this is the Bluetooth monitor, and she is that girl. โจ
๐ NAME ๐
btmon โ Bluetooth monitor. that's it. that's the name. iconic, short, does what it says. ๐ซถ
๐ฆ SYNOPSIS ๐ฆ
btmon [OPTIONS ...]
literally just the command plus whatever options you wanna throw at her. ๐
๐ DESCRIPTION ๐
so the btmon(1) command gives you access to the Bluetooth subsystem monitor infrastructure, and its whole personality is reading HCI traces. that's the vibe. that's the job. she reads traces and she reads them well. ๐
โญ OPTIONS โญ
okay this is the part where we go through every single flag, no skipping, no cap. ๐ง
-r FILE,--read FILEโ reads traces in btsnoop format from FILE. simple. slay.-w FILE,--write FILEโ saves traces in btsnoop format to FILE. the opposite of read. balance. ๐-a FILE,--analyze FILEโ analyzes traces in btsnoop format from FILE. it displays the devices found in the FILE with its packets by type. and if gnuplot is installed on the system, it also attempts to plot a packet latency graph. fancy behavior honestly. ๐-s SOCKET,--server SOCKETโ starts the monitor server socket. server mode activated bestie. ๐-p PRIORITY,--priority PRIORITYโ shows only priority or lower for the user log. here's the priority lore:3โ Error4โ Warning6โ Information (Default)7โ Debug.debugcan be used.
-i NUM,--index NUMโ shows only the specified controller.hciNUMis also acceptable. this one's useful to capture the traces from the specific controller when the multiple controllers are presented. multi-controller girlies rise. ๐ฅ-d TTY,--tty TTYโ reads data from TTY.-B SPEED,--rate SPEEDโ sets the TTY speed. the default SPEED is115300.-V COMPID,--vendor COMPIDโ sets the default company identifier. the COMPID is a unique number assigned by the Bluetooth SIG to a member company and can be found/searched from the Bluetooth SIG webpage. for example, Intel is2and Realtek is93. iykyk. ๐ข-M,--mgmtโ opens channel for mgmt events.-K,--kernelโ opens kmsg for kernel messages. kernel era. ๐-t,--timeโ shows a time instead of time offset.-T,--dateโ shows a time and date information instead of time offset. we're getting the full context now.-N,--no-timeโ suppresses the time offset display entirely. no time, no problem. ๐-S,--scoโ dumps SCO traffic in raw hex format.-A,--a2dpโ dumps A2DP stream traffic in a raw hex format.-I,--isoโ dumps ISO stream traffic in raw hex format. required to see LE Audio isochronous data in the output. like, don't forget her or you'll be confused. ๐ง-E IP,--ellisys IPโ sends Ellisys HCI Injection.-P,--no-pagerโ disables pager usage while reading the log file.-J OPTIONS,--jlink OPTIONSโ reads data from RTT. each options are comma(,) separated without spaces. the options are:DEVICEโ Required. Set the target device.SERIALNOโ (Optional) Set the USB serial number. Default is0.INTERFACEโ (Optional) Target interface. Default isswd.SPEEDโ (Optional) Set target interface speed in kHz. Default is1000.
-R OPTIONS,--rtt OPTIONSโ RTT control block parameters. each options are comma(,) separated without spaces. the options are:ADDRESSโ (Optional) Address of RTT buffer. Default is0x00AREAโ (Optional) Size of range to search in RTT buffer. Default is0NAMEโ (Optional) Buffer name. Default isbtmonitor
-C WIDTH,--columns WIDTHโ output width if not a terminal.-c MODE,--color MODEโ sets output color. the possible MODE values are:auto|always|never. default value isauto. we love a little color coordination. ๐จ-v,--versionโ shows version.-h,--helpโ shows help options.
โจ READING THE OUTPUT โจ
so btmon output is organized as a stream of frames, and each frame represents a single event in the Bluetooth subsystem. understanding the output format is essential for debugging Bluetooth issues โ like, don't skip this part bestie, it's the whole point. ๐
๐ธ Line Prefixes ๐ธ
every frame begins with a single-character prefix that identifies its source and type. here's the cheat sheet:
<โ HCI Command / Data TX โ sent from host to controller (outgoing). HCI commands, ACL/SCO/ISO data transmitted to the controller.>โ HCI Event / Data RX โ received from controller to host (incoming). HCI events, ACL/SCO/ISO data received from the controller.@โ Management traffic โ management interface (MGMT) commands and events betweenbluetoothdand the kernel management layer.=โ System notes โ system-level annotations: kernel information, index changes, process log messages, and D-Bus signals.
๐ฆ HCI Traffic (< and >) ๐ฆ
HCI frames represent the actual communication between the host software and the Bluetooth controller hardware. this is where the real main character energy lives. ๐
Anatomy of an HCI command line:
< HCI Command: Reset (0x03|0x0003) plen 0 #5 [hci0] 12:35:01.843185
โ โ โ โ โ โ โ โ
โ โ โ โ โ โ โ โโ Timestamp
โ โ โ โ โ โ โโ Controller
โ โ โ โ โ โโ Frame number
โ โ โ โ โโ Parameter length (bytes)
โ โ โ โโ Full opcode (16-bit)
โ โ โโ OGF|OCF (Opcode Group / Command Field)
โ โโ Command name (human-readable)
โโ Direction: < = Host to Controller (outgoing)
Anatomy of an HCI event line:
OK so this is where it gets good โ let's talk about what all these little arrows and pipes actually mean ๐
Here's a fully annotated HCI event so you can see every field:
> HCI Event: Command Complete (0x0e) plen 4 #6 [hci0] 12:35:01.864922
โ โ โ โ โ โ
โ โ โ โ โ โโ Timestamp
โ โ โ โ โโ Controller
โ โ โ โโ Frame number
โ โ โโ Parameter length
โ โโ Event code
โ
โโ Direction: > = Controller to Host (incoming)
The < direction means the host is sending to the controller (commands and data). The > direction means the controller is sending to the host (events and data). Think of it from the controller's perspective: < is input going into the controller, > is output coming from it. told you it was from the controller's POV ๐ฆ
HCI commands with parameters are followed by indented detail lines, like this:
< HCI Command: LE Set Extende.. (0x08|0x0039) plen 2 #1 [hci0] 12:35:01.738352
Extended advertising: Disabled (0x00)
Number of sets: Disable all sets (0x00)
And HCI event responses reference the command they complete:
> HCI Event: Command Complete (0x0e) plen 4 #6 [hci0] 12:35:01.864922
Reset (0x03|0x0003) ncmd 2
Status: Success (0x00)
Here ncmd 2 indicates the controller can accept 2 more commands (HCI flow control). The indented body shows the command this event completes and the result status. flow control bestie, it's literally a vibe check for the controller โจ
LE Meta Events contain a subevent type:
> HCI Event: LE Meta Event (0x3e) plen 31 #487 [hci0] 12:36:18.974201
LE Enhanced Connection Complete (0x0a)
Status: Success (0x00)
Handle: 2048
Role: Peripheral (0x01)
Peer address type: Public (0x00)
Peer address: AA:BB:CC:DD:EE:FF (OUI Company)
Connection interval: 60.00 msec (0x0030)
Connection latency: 0 (0x0000)
Supervision timeout: 9600 msec (0x03c0)
ACL Data shows data plane traffic with handle and protocol decoding:
< LE-ACL: Handle 2048 [66:B0:26:F1:D3:BC] [1/6] flags 0x00 dlen 16 #493 [hci0] 12:36:18.977915
โ โ โ โ โ โ โ โ โ โ
โ โ โ โ โ โ โ โ โ โโ Timestamp
โ โ โ โ โ โ โ โ โโ Controller
โ โ โ โ โ โ โ โโ Frame number
โ โ โ โ โ โ โโ Data length
โ โ โ โ โ โโ flags
โ โ โ โ โโ Buffer tracking (optional)
โ โ โ โโ Peer address (optional)
โ โ โโ Handle number
โ โโ Connection-type-aware label (e.g. BR-ACL, LE-ACL, BR-SCO, LE-ISO)
โโ Direction marker: '<' = host->controller (TX), '>' = controller->host (RX)
ACL data is automatically decoded into higher-layer protocols:
< LE-ACL: Handle 2048 [2/6] flags 0x00 dlen 7 #494 [hci0] 12:36:18.978488
ATT: Exchange MTU Request (0x02) len 2
Client RX MTU: 517
> LE-ACL: Handle 2048 flags 0x02 dlen 11 #497 [hci0] 12:36:19.000048
SMP: Pairing Request (0x01) len 6
IO capability: NoInputNoOutput (0x03)
OOB data: Authentication data not present (0x00)
Authentication requirement: Bonding, MITM, SC, No Keypresses, CT2 (0x2d)
Max encryption key size: 16
๐
Management Traffic (@)
Lines starting with @ show management interface traffic โ the structured command/event protocol between bluetoothd and the kernel (see doc/mgmt-protocol.rst). different layer, different drama ๐
Anatomy of a management line:
@ MGMT Command: Set Powered (0x0005) plen 1 {0x0001} [hci0] 12:35:04.033564
โ โ โ โ โ โ
โ โ โ โ โ โโ Timestamp
โ โ โ โ โโ Controller
โ โ โ โโ MGMT socket ID
โ โ โโ Parameter length
โ โโ MGMT opcode
โโ @ = Management channel
The {0x0001} is the management socket identifier โ it distinguishes between multiple management clients (e.g. bluetoothd and btmgmt running simultaneously). so you can tell WHO is talking, iconic ๐
MGMT Open/Close track when processes connect to the management channel:
@ MGMT Open: bluetoothd (privileged) version 1.23 {0x0001} 12:34:49.881936
@ MGMT Close: bluetoothd {0x0001} 12:35:01.866256
These show the process name, privilege level, and protocol version. ๐ซถ
MGMT commands with parameters:
@ MGMT Command: Set Powered (0x0005) plen 1 {0x0001} [hci0] 12:35:04.033564
Powered: Enabled (0x01)
MGMT events (responses and notifications):
@ MGMT Event: Command Complete (0x0001) plen 7 {0x0001} [hci0] 12:35:04.114789
Set Powered (0x0005) plen 4
Status: Success (0x00)
Current settings: 0x004e0ac1
Powered
Secure Simple Pairing
MGMT without controller index (global operations):
@ MGMT Command: Read Management Ver.. (0x0001) plen 0 {0x0001} 12:35:04.027771
@ MGMT Event: Command Complete (0x0001) plen 6 {0x0001} 12:35:04.027776
Notice there is no [hci0] โ these operate at the system level, not on a specific controller. no controller, no ๐๏ธ, it's giving head office energy ๐ข
๐ธ System Notes (=)
Lines starting with = are system-level annotations injected by the kernel or by processes via the monitor channel. They are not HCI or MGMT protocol traffic. they're the narrator of the story, fr fr ๐
Kernel information (shown at startup):
= Note: Linux version 6.16.0-rc6-0903 (x86_64) 12:34:49.881926
= Note: Bluetooth subsystem version 2.22 12:34:49.881930
Index lifecycle (controller added/removed/opened/closed):
= New Index: 00:11:22:33:44:55 (Primary,USB,hci0) [hci0] 12:34:49.881932
= Open Index: 00:11:22:33:44:55 [hci0] 12:34:49.881933
= Index Info: 00:11:22:33:44:55 (OUI Company) [hci0] 12:34:49.881934
= Close Index: 00:11:22:33:44:55 [hci0] 12:35:01.865125
- New Index โ a controller was registered with the kernel
- Open Index โ a controller was activated
- Index Info โ controller vendor information
- Close Index โ a controller was deactivated
Process log messages (debug output from bluetoothd and other daemons):
= bluetoothd: src/adapter.c:connected_callback() hci0 devic.. 12:36:18.975307
โ โ โ
โ โ โโ Timestamp
โ โโ Source file, function, and message (may be truncated)
โโ Process name
These appear when bluetoothd is running with debug enabled (-d) or when a process writes to the kernel logging channel. They show the source file path, function name, and a log message โ invaluable for correlating daemon-internal decisions with the HCI traffic around them. ๐ง
D-Bus activity (signals and method calls):
= bluetoothd: [:1.21220:method_call] > org.freedesktop.DBus.. 12:34:53.912508
= bluetoothd: [:1.21220:method_return] < [#5] 12:34:53.912546
= bluetoothd: [signal] org.freedesktop.DBus.ObjectManager.I.. 12:36:18.975691
The format is [bus_name:message_type] followed by > (outgoing) or < (incoming). Note that > and < within D-Bus system notes indicate D-Bus message direction, not HCI direction. โ ๏ธ do NOT mix these up bestie, the arrows mean something different here ๐
๐ฆ Right-Side Metadata
Every line has metadata right-aligned at the end. The exact fields depend on the line type:
โโ Main content (left-aligned, variable width)
โ โโ Frame # โโ โControllerโ โโ Timestamp โโ
โ โ โ โ โ โ โ
< HCI Command: Reset (0x03|0x0003) plen 0 #5 [hci0] 12:35:01.843185
> HCI Event: Command Complete (0x0e) plen 4 #6 [hci0] 12:35:01.864922
@ MGMT Command: Set Powered (0x0005) plen 1 {0x0001} [hci0] 12:35:04.033564
= Note: Linux version 6.16.0-rc6-0903 (x86_64) 12:34:49.881926
= Open Index: 00:11:22:33:44:55 [hci0] 12:34:49.881933
Frame number (#N): Sequential counter for HCI frames only. Useful for identifying specific packets in a trace. Only HCI traffic (< and >) gets frame numbers โ MGMT (@) and system notes (=) do not. sorry not sorry, them's the rules โจ
Controller ([hciN]): Identifies which Bluetooth controller the frame belongs to. Absent for global operations (kernel notes, MGMT commands without a controller index).
MGMT socket ID ({0xNNNN}): Shown on @ lines instead of frame numbers. Identifies which management socket (process) sent the command.
Timestamp: Always the rightmost field. The format depends on the command-line option used:
โโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโ
โ Option โ Format โ Example โ
โโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโค
โ (default) โ Seconds since trace โ 0.881932 โ
โ โ start โ โ
โโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโค
โ -t โ Time of day โ 12:35:01.843185 โ
โ โ (HH:MM:SS.usec) โ โ
โโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโค
โ -T โ Full date and time โ 2026-01-13 โ
โ โ โ 12:34:49.881926 โ
โโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโ
๐ Indented Detail Lines
Lines indented below a frame header contain the decoded payload of that frame. The indentation level indicates the protocol layer: so the more indented it is, the deeper into the stack you're looking ๐ ๐
- Level two (8 spaces) โ the decoded fields living inside that payload ๐
- Level three (10+ spaces) โ nested protocol data, like L2CAP chillin' inside ACL, or ATT inside L2CAP ๐ฆ
Here's what protocol layering actually looks like in ACL data, bestie:
> ACL: Handle 2048 flags 0x02 dlen 11 #497 [hci0] 12:36:19.000048
SMP: Pairing Request (0x01) len 6 โ L2CAP/SMP layer
IO capability: NoInputNoOutput (0x03) โ SMP fields
OOB data: Authentication data not present (0x00)
Authentication requirement: Bonding, MITM, SC (0x2d)
Max encryption key size: 16
๐ธ Timestamp Notes ๐ธ
When you're reading btsnoop files with -t or -T, the timestamps show the wall-clock time that was recorded in the btsnoop file. How precise they are depends on where they came from:
- Live capture (btmon monitor channel) โ microsecond precision, straight from the kernel โจ
- btsnoop files โ the btsnoop format stores timestamps as microseconds since epoch, so full microsecond precision is preserved. Those trailing zeros in the display (like
14:38:46.589000) mean the original capture source only had millisecond granularity ๐
The default timestamp mode shows seconds elapsed since the first packet in the trace, which is lowkey perfect for measuring intervals between events without needing to know the absolute time ๐ซถ
๐ฆ Frame Numbers vs Line Numbers ๐ฆ
btmon assigns sequential frame numbers (#N) to HCI packets. These are stable identifiers for specific packets no matter how you format the output. BUT โ when you're processing btmon text output with tools like grep or sed, the relevant unit is line numbers in the output file. The two are literally unrelated:
- A single frame may produce many output lines (header + decoded fields) ๐ญ
- Frame numbers only apply to HCI traffic (
<and>). MGMT (@) and system notes (=) do not have frame numbers ๐ - When referencing specific packets, prefer frame numbers (
#487) over line numbers, because frame numbers stay stable across different terminal widths and formatting options ๐
๐ Practical Reading Guide ๐
Your classic command-response pair:
< HCI Command: Read BD ADDR (0x04|0x0009) plen 0 #13 [hci0] 12:35:04.057866
> HCI Event: Command Complete (0x0e) plen 10 #14 [hci0] 12:35:04.058750
Read BD ADDR (0x04|0x0009) ncmd 1
Status: Success (0x00)
Address: 00:11:22:33:44:55 (OUI Company)
Read this as: Frame #13, the host asked the controller for its Bluetooth address. Frame #14, the controller replied with success and the address 00:11:22:33:44:55. The response arrived ~0.9ms later โจ
Typical MGMT flow, showing how it relates to HCI:
@ MGMT Command: Set Powered (0x0005) plen 1 {0x0001} [hci0] 12:35:04.033564
Powered: Enabled (0x01)
< HCI Command: Reset (0x03|0x0003) plen 0 #7 [hci0] 12:35:04.033907
> HCI Event: Command Complete (0x0e) plen 4 #8 [hci0] 12:35:04.055753
Reset (0x03|0x0003) ncmd 2
Status: Success (0x00)
... (more HCI commands to configure the controller) ...
@ MGMT Event: Command Complete (0x0001) plen 7 {0x0001} [hci0] 12:35:04.114789
Set Powered (0x0005) plen 4
Status: Success (0x00)
Read this as: bluetoothd sent Set Powered via MGMT. The kernel translated this into a whole sequence of HCI commands (Reset, then configuration). After all those HCI commands completed, the kernel sent the MGMT Command Complete event back to bluetoothd ๐
Connection establishment flow, and this one's iconic:
> HCI Event: LE Meta Event (0x3e) plen 31 #487 [hci0] 12:36:18.974201
LE Enhanced Connection Complete (0x0a)
Status: Success (0x00)
Handle: 2048
Role: Peripheral (0x01)
Peer address: AA:BB:CC:DD:EE:FF (OUI Company)
@ MGMT Event: Device Connec.. (0x000b) plen 13 {0x0001} [hci0] 12:36:18.974319
= bluetoothd: src/adapter.c:connected_callback() hci0 devic.. 12:36:18.975307
< ACL: Handle 2048 [1.. flags 0x00 dlen 16 #493 [hci0] 12:36:18.977915
LE L2CAP: Connection Parameter Update Request (0x12) ident 1 len 8
< ACL: Handle 2048 [2/6] flags 0x00 dlen 7 #494 [hci0] 12:36:18.978488
ATT: Exchange MTU Request (0x02) len 2
Client RX MTU: 517
Read this as: The controller reported a new LE connection (HCI event). The kernel forwarded this as a MGMT Device Connected event. Then bluetoothd logged its connected_callback(). After that, data exchange began โ an L2CAP parameter update and ATT MTU negotiation over the new ACL connection ๐ฅ
๐ฅ ANALYZE MODE ๐ฅ
The -a (--analyze) option reads a btsnoop file and produces a statistical summary instead of the full decoded trace. It's giving data-science-girlie ๐
โจ Usage โจ
$ btmon -a hcidump.log
๐ Output Contents ๐
Analyze mode reports, for each controller found in the trace:
- Packet counts โ total HCI packets broken down by type (commands, events, ACL, SCO, ISO, vendor diagnostics, system notes, user logs, control messages) ๐ฆ
- Per-connection statistics โ for each connection handle found:
- Connection type (
BR-ACL,LE-ACL,BR-SCO,BR-ESCO,LE-ISO) - Device address
- TX and RX packet counts and completion counts
- Latency statistics (min, max, moving average and standard deviation) in milliseconds
- Packet size statistics (min, max, average) in octets
- Throughput estimate in Kb/s
- Packet loss statistics, for isochronous connections
- Connection type (
- Per-channel statistics โ for each L2CAP channel within a connection, the same packet/latency/size statistics ๐ธ
- Latency plots โ if
gnuplotis installed, ASCII-art latency distribution plots are rendered in the terminal โญ
๐ Latency Standard Deviation ๐
Latency gets reported like this:
TX Latency: 5-300 msec (~91 msec +/- 104 msec)
The range is the minimum and maximum observed latency, ~ is a moving average, and +/- is the standard deviation over all samples ๐ซถ
The standard deviation is literally the thing that separates a link that's merely slow from one that's unstable. High average with low deviation = consistent latency, which is usually a scheduling or interval configuration issue. Low average with high deviation = most packets are fast but some are heavily delayed, which typically points at interference, retransmissions or controller buffer stalls. Comparing the maximum against average + deviation shows whether the worst case is representative or just a one-off outlier ๐ญ
๐ Packet Loss ๐
Packet loss is tracked for isochronous (CIS/BIS) connections, where the ISO data packet header carries an SDU sequence number and a packet status flag. It's reported like this:
RX loss: 13/21 (61.90%) dropped 1 invalid 1
Here's what the counters actually mean:
- loss โ SDUs missing from the received sequence, derived from gaps in the sequence number. The denominator is the number of SDUs that should have been received, meaning the ones actually seen plus the ones detected as missing ๐
- dropped โ SDUs the controller delivered with a packet status flag of
2(lost data), meaning the controller knows the payload did not arrive - invalid โ SDUs delivered with a packet status flag of
1(possibly invalid), meaning the payload arrived but may be corrupt ๐
Duplicate and reordered sequence numbers are not counted as loss. Important, no cap.
Loss counters also show up during live decoding, on the ISO data packet where a discontinuity is detected. The line is only emitted once a loss has occurred, so a clean stream produces no extra output โ slay ๐
๐ PROTOCOL ERROR CODES ๐
btmon automatically decodes error codes from multiple protocol layers. This section is your reference for interpreting errors seen across the ATT, SMP, and L2CAP layers ๐ฆ
๐ ATT Error Codes
Okay bestie, so ATT errors show up in Error Response (0x01) PDUs. Beyond the GATT discovery context โ where Attribute Not Found is literally just business as usual, no drama โ these errors mean something is actually wrong and you should pay attention ๐ซถ
0x01โ Invalid Handle โ Client used a handle that does not exist. Like, ma'am, that handle is not real ๐0x02โ Read Not Permitted โ Characteristic does not allow reads0x03โ Write Not Permitted โ Characteristic does not allow writes0x05โ Authentication Insufficient โ Operation requires an authenticated (MITM-protected) bond. Triggers SMP pairing if not yet bonded. So it's not being rude, it's just asking you to bond first โจ0x06โ Request Not Supported โ Server does not support this ATT operation0x07โ Invalid Offset โ Read/write blob offset exceeds attribute length0x08โ Authorization Insufficient โ Server requires additional authorization0x09โ Prepare Queue Full โ Too many prepared writes queued0x0aโ Attribute Not Found โ No attributes in requested range. Normal termination for GATT discovery procedures. Genuinely the good ending, no cap ๐ธ0x0bโ Attribute Not Long โ Attribute cannot be read with Read Blob0x0cโ Insufficient Encryption Key Size โ Encryption key is too short0x0dโ Invalid Attribute Value Length โ Write value length is incorrect for this attribute0x0eโ Unlikely Error โ Generic unlikely error. The vibes are off but nobody knows why ๐0x0fโ Insufficient Encryption โ Link is not encrypted. Triggers encryption setup.0x10โ Unsupported Group Type โ Attribute type is not a valid grouping type0x11โ Insufficient Resources โ Server out of resources0x12โ Value Not Allowed โ Value is not within permitted range0x80โ0x9fโ Application Error โ Application-specific error; meaning depends on the profile/service. ASCS uses these for ASE-specific errors. iykyk ๐0xfcโ Write Request Rejected โ Write rejected (CSIP, ASCS)0xfdโ CCC Descriptor Improperly Configured โ CCC must be enabled before certain operations0xfeโ Procedure Already in Progress โ Another procedure is already running. One thing at a time, bestie โญ0xffโ Out of Range โ Value is outside valid range
๐ L2CAP Connection Response Results
okay so both L2CAP Connection Response and LE Connection Response carry a result code, and the code is basically the remote's way of texting back "hii" or "no ๐". Here's the whole roster, bestie:
0x0000โ Connection successful โ Channel established normally โจ (the slay outcome)0x0001โ Connection pending โ Connection in progress (this one'sBR/EDRonly, iykyk)0x0002โ Connection refused - PSM not supported โ Remote does not have a server for this protocol ๐0x0003โ Connection refused - security block โ Security requirements not met ๐0x0004โ Connection refused - no resources โ Remote ran out of channel resources0x0005โ Connection refused - invalid Source CID โ Source CID is invalid or already in use0x0006โ Connection refused - Source CID already allocated โ CID collision, fr fr0x0007โ Connection refused - unacceptable parameters โ LE credit-based: MTU, MPS, or credits unacceptable0x0008โ Connection refused - invalid parameters โ Parameter values are invalid0x0009โ Connection refused - insufficient authentication โ Not authenticated0x000aโ Connection refused - insufficient authorization โ Not authorized0x000bโ Connection refused - insufficient encryption key size โ Encryption key too short0x000cโ Connection refused - insufficient encryption โ Link not encrypted
๐ Automating Error Detection
bestie, you are NOT going to sit there scrolling a trace with your own two eyeballs. let the shell do it ๐
Find all ATT errors (excluding normal discovery termination):
grep -n "Error Response" output.txt
Then check whether each error is Attribute Not Found (0x0a) within a discovery sequence (that one's normal, don't panic) or a different error code (that's the problem, go look ๐).
Find all authentication/encryption related errors:
grep -n "Authentication Insufficient\|Insufficient Encryption\|Insufficient Security\|security block" output.txt
These indicate the link needs pairing or encryption. Check whether SMP pairing follows.
Find all L2CAP channel rejections:
grep -n "Connection refused" output.txt
Cross-layer error correlation:
Errors often cascade across layers โ one thing falls over and drags its friends down with it ๐ญ. Common patterns:
- ATT Insufficient Encryption (
0x0f) โ triggers HCI LE Start Encryption โ Encryption Change success โ ATT operation retried - ATT Authentication Insufficient (
0x05) โ triggers SMP Pairing Request โ pairing completes โ ATT operation retried - SMP Pairing Failed โ Disconnect Complete with reason Authentication Failure (
0x05) - L2CAP Connection refused - security block โ triggers SMP pairing
๐ฆ PROTOCOL FLOWS ๐ฆ
๐ธ HCI INITIALIZATION SEQUENCE ๐ธ
Every btsnoop trace that captures controller startup begins with a dense block of HCI commands and events. That's just the kernel's Bluetooth subsystem initializing the controller through a multi-stage sequence defined in net/bluetooth/hci_sync.c โ main character behavior, honestly. Understanding this sequence helps you tell normal initialization traffic apart from actual application-level issues. ๐
Overview
The kernel initializes a Bluetooth controller in four stages after opening the HCI device. Each stage sends a batch of HCI commands and waits for their completion before proceeding to the next. The full call chain is:
hci_power_on_sync
โโ hci_dev_open_sync
โโ hci_dev_init_sync
โโ hci_dev_setup_sync (driver setup + quirks)
โโ hci_init_sync
โโ Stage 1: Reset + identity
โโ Stage 2: Capabilities + buffer sizes
โโ Stage 3: Event masks + policy
โโ Stage 4: Final configuration
After all four stages complete, a post-init phase (hci_powered_update_sync) configures runtime parameters like SSP, advertising, and scan settings. โจ
For unconfigured devices (e.g. controllers that need firmware or a BD address programmed), only a minimal Stage 0 runs โ just enough to identify the hardware.
๐ Stage 0: Reset and Basic Identity (Unconfigured Only)
This stage runs only for unconfigured controllers that need setup before full initialization.
Commands sent:
HCI_Resetโ Reset the controller (skipped if theRESET_ON_CLOSEquirk is set)HCI_Read_Local_Version_Informationโ Read hardware/firmware versionHCI_Read_BD_ADDRโ Read the controller's Bluetooth address
โญ Stage 1: Reset and Read Local Features
Resets the controller and reads core identity and capability information. ๐
ok so here's the literal receipt of every command the tool fires off during this stage โ no notes, just vibes and a very specific checklist ๐
HCI_Resetโ resets the controller. clean slate energy, we love a fresh start โจHCI_Read_Local_Supported_Featuresโ reads the LMP feature bitmask (that's your BR/EDR, LE, SSP, etc. situation). it's giving "let me see what you're working with" ๐HCI_Read_Local_Version_Informationโ reads the HCI version, the LMP version, and the manufacturer. the whole origin story, fr fr ๐HCI_Read_BD_ADDRโ reads the public Bluetooth address. yes bestie, we're getting the actual address ๐ญ
๐ธ Stage 2: Read Capabilities and Setup ๐ธ
so after the reset-and-read-everything opener, stage two is where it actually digs in. it reads the detailed capabilities, enables the core features, and reads the buffer sizes. main character behaviour tbh ๐ซถ
highkey important detail: this stage comes in three phases โ the common commands, the BR/EDR-specific commands, and the LE-specific commands. three phases, three vibes, one extremely thorough controller ๐
Common Commands
okay so these are the baseline commands every controller gets, no cap ๐
HCI_Read_Local_Supported_Commandsโ reads the supported command bitmask (HCI 1.2+) โจHCI_Write_Simple_Pairing_Mode (enable)โ enables SSP if it's supported and configured ๐ซถHCI_Write_Extended_Inquiry_Response (clear)โ clears EIR data when SSP is disabled ๐งนHCI_Write_Inquiry_Modeโ sets inquiry mode (RSSI or Extended, based on features) ๐HCI_Read_Inquiry_Response_Transmit_Power_Levelโ reads inquiry TX power if supported ๐กHCI_Read_Local_Extended_Features (page 1)โ reads extended feature page 1 (SSP host, LE host, etc.) ๐HCI_Write_Authentication_Enableโ syncs authentication state with theLINK_SECURITYflag ๐
BR/EDR Commands (if BR/EDR capable)
if your controller speaks BR/EDR, this lil squad shows up too ๐
HCI_Read_Buffer_Sizeโ reads ACL/SCO buffer sizes and count ๐ฆHCI_Read_Class_of_Deviceโ reads the current device class ๐HCI_Read_Local_Nameโ reads the stored local name ๐ท๏ธHCI_Read_Voice_Settingโ reads SCO voice setting (if supported) ๐๏ธHCI_Read_Number_of_Supported_IACโ reads the number of supported inquiry access codes ๐ขHCI_Read_Current_IAC_LAPโ reads current IAC LAP values ๐HCI_Set_Event_Filter (clear all)โ clears any stored event filters ๐งผHCI_Write_Connection_Accept_Timeoutโ sets connection accept timeout (~20 seconds) โณHCI_Write_Synchronous_Flow_Control_Enableโ enables SCO flow control if supported ๐
LE Commands (if LE capable)
and if LE is in the chat? bestie, we got a whole other set ๐ฆ
LE_Read_Local_Supported_Featuresโ reads the LE feature bitmask โจLE_Read_All_Local_Supported_Featuresโ reads extended LE features (if supported) ๐ซLE_Read_Buffer_Size [v2] or [v1]โ reads LE ACL (and ISO) buffer sizes; v2 is used when ISO capable ๐LE_Read_Supported_Statesโ reads the LE state combination table ๐๏ธ
Stage 3: Event Masks, Link Policy, and Features
this stage is literally the longest one: it configures which events the controller should report, sets link policy, and reads extended feature pages. main character energy, but make it event masks ๐๐
Event Masks and Link Policy
HCI_Set_Event_Maskโ configures the main event mask based on controller capabilities ๐๏ธHCI_Read_Stored_Link_Keyโ reads all stored link keys ๐HCI_Write_Default_Link_Policy_Settingsโ sets up role switch, hold, sniff, and park modes based on what the LMP features actually support ๐HCI_Read_Page_Scan_Activityโ reads back the page scan interval and windowHCI_Read_Default_Erroneous_Data_Reportingโ reads the error data reporting state (this is the wideband speech one, iykyk)HCI_Read_Page_Scan_Typeโ reads the page scan type, either standard or interlacedHCI_Read_Local_Extended_Features (pages 2..N)โ reads every remaining extended feature page, not just the first one, we go all the way ๐ซถ
โจ Event mask details, bestie, listen up โจ For dual-mode controllers the kernel enables events for inquiry results (RSSI and extended), SSP (IO capability, user confirmation, passkey), synchronous connections, sniff subrating, encryption refresh, link supervision, and LE meta-events. For LE-only controllers a minimal mask covers only command completion, hardware errors, disconnection, and encryption changes.
๐ธ LE Event Mask and Capabilities ๐ธ
Okay so here's the whole LE-side shopping list, each command and what it's actually for:
LE_Set_Event_Maskโ configure which LE sub-events get reportedLE_Read_Advertising_Channel_Tx_Powerโ read advertising TX power (legacy advertising only, note that!)LE_Read_Transmit_Powerโ read the min/max transmit power rangeLE_Read_Accept_List_Sizeโ read the filter accept list capacityLE_Clear_Accept_Listโ clear the filter accept listLE_Read_Resolving_List_Sizeโ read the resolving list capacity (LL Privacy, we love her)LE_Clear_Resolving_Listโ clear the resolving listLE_Set_Resolvable_Private_Address_Timeoutโ set the RPA rotation timeoutLE_Read_Maximum_Data_Lengthโ read max TX/RX octets and time (Data Length Extension)LE_Read_Suggested_Default_Data_Lengthโ read the current default data lengthLE_Read_Number_of_Supported_Advertising_Setsโ read extended advertising set capacityHCI_Write_LE_Host_Supportedโ notify the controller that the host supports LE (dual-mode only)LE_Set_Host_Featureโ enable CIS Central (bit 32) and/or Channel Sounding (bit 47)
๐ฆ LE event mask details ๐ฆ The kernel enables LE sub-events based on features: connection complete (enhanced if available), advertising reports (extended if available), long term key request, connection parameter request, data length change, PHY update, channel selection algorithm, periodic advertising events, CIS established/request (if CIS capable), BIG create/sync/info (if BIS capable), and channel sounding events (if CS capable).
๐ Stage 4: Final Configuration ๐
Last stage, the glow-up finish โจ It performs final setup: deletes stale keys, sets event mask page 2, reads codec information, enables Secure Connections, and configures LE data length and PHY defaults. That's the whole final pass โ tidy, correct, and absolutely serving ๐
๐ Keys, Codecs, and Secure Connections ๐
Okay bestie, this chunk is literally the controllerโs โwho am I and what can I doโ era, but make it keys, codecs, and security. Hereโs the command tea, no cap:
HCI_Delete_Stored_Link_Key(all) โ deletes all stored link keys from the controller. All of them. Gone. Bye ๐HCI_Set_Event_Mask_Page_2โ enables page 2 events (authenticated payload timeout, etc.).HCI_Read_Local_Supported_Codecs[v2]or[v1]โ reads supported codec IDs;v2includes transport type info.HCI_Read_Local_Pairing_Optionsโ reads default pairing options (max encryption key size).HCI_Get_MWS_Transport_Layer_Configurationโ reads MWS coexistence config if supported.HCI_Read_Synchronization_Train_Parametersโ reads sync train params (Connectionless Peripheral Broadcast).HCI_Write_Secure_Connections_Support(enable) โ enables Secure Connections if SSP is active.HCI_Write_Default_Erroneous_Data_Reportingโ enables/disables based on the wideband speech setting.
๐ฆ LE Data Length and PHY Defaults ๐ฆ
LE_Write_Suggested_Default_Data_Lengthโ sets the default TX octets/time for new connections.LE_Set_Default_PHYโ sets the preferred PHY (1M always; 2M and Coded if supported).
๐ธ Post-Initialization ๐ธ
After the four stages complete, hci_powered_update_sync runs to apply runtime configuration:
HCI_Write_Simple_Pairing_Modeโ re-enables SSP + Secure Connections if configured.HCI_Write_LE_Host_Supportedโ syncs LE host support state.- LE advertising setup โ configures advertising parameters and data.
HCI_Write_Authentication_Enableโ syncs authentication enable state.- Scan/class/name/EIR updates โ configures page scan, device class, local name, EIR data.
LE_Set_Random_Addressโ sets a static random address if thereโs no public address.
๐ฆ Reading the Init Sequence in a Trace ๐ฆ
When examining a btsnoop trace, the initialization block is the first thing after the controller is opened. A typical dual-mode controller trace starts with:
< HCI Command: Reset
> HCI Event: Command Complete (Reset)
< HCI Command: Read Local Supported Features
> HCI Event: Command Complete (Read Local Supported Features)
< HCI Command: Read Local Version Information
> HCI Event: Command Complete (Read Local Version Information)
< HCI Command: Read BD ADDR
> HCI Event: Command Complete (Read BD ADDR)
... [Stage 2-4 commands follow]
Key things to look for:
- Missing commands: if expected commands are absent, the controller may not support the corresponding feature. For example, no
LE_Read_Buffer_Sizemeans the controller is BR/EDR only. - Command failures: a Status other than
0x00in a Command Complete event during init usually indicates a broken controller or unsupported feature. The kernel handles most gracefully, but persistent errors may prevent the adapter from functioning. - Buffer sizes: the values returned by
Read_Buffer_SizeandLE_Read_Buffer_Sizedetermine how many in-flight packets the controller can hold. Small buffer counts can cause throughput issues. - Feature bits: the
Read_Local_Supported_Featuresresponse reveals what the controller supports (LE, SSP, eSCO, etc.). Cross reference with the commands that follow โ the kernel only sends commands for features the controller reports supporting. - Event mask: the
Set_Event_Maskcommand shows exactly which events the host wants to receive. If an expected event never appears in the trace, check whether it was enabled in the mask. - LE-only controllers: these skip all BR/EDR commands (
Read_Buffer_Size,Read_Local_Name, link policy, etc.) and use a minimal event mask. The trace will be noticeably shorter. - Vendor commands: some controllers (Intel, Broadcom, Qualcomm, Realtek, MediaTek) insert vendor-specific HCI commands between stages for firmware download, configuration, or patch application. These appear as opcode groups
0x3F(vendor) and are driver-specific.
๐ CONNECTION TRACKING ๐
okay so HCI is out here using connection handles โ little 16-bit integers โ to keep track of every single connection, like little name tags at a party ๐ and bestie, if you wanna actually read traces you gotta understand how those handles map to actual devices. it's giving "learn the seating chart or be lost forever" energy.
โจ Handle Types
so different connection types live in different handle ranges, BUT โ and this is the important bit โ those ranges are controller-specific and not standardized. so you can't just look at a number and know. instead, you figure out the connection type by peeking at the event that created the handle. main character detective work ๐
- BR/EDR ACL โ created by
Connection Completeโ Classic Bluetooth data connection - LE ACL โ created by
LE (Enhanced) Connection Completeโ Low Energy data connection - CIS โ created by
LE CIS Establishedโ Connected Isochronous Stream (LE Audio) - BIS โ created by
LE BIG Completeโ Broadcast Isochronous Stream (LE Audio) - SCO/eSCO โ created by
Synchronous Connection Completeโ Voice/audio synchronous connection (classic)
and yes, a single device can absolutely be holding multiple handles at the same time, no cap. like an LE Audio device will have an LE ACL handle for all the control traffic plus one or more CIS handles for the audio streams โ multitasking icon behavior ๐งโจ and hear this: the LE CIS Established event actually includes the ACL connection handle that the CIS is associated with, so you can link 'em up. iykyk.
๐ฆ Controller Buffer Tracking
buffer tracking might throw a lil indicator at you in square brackets, like this:
< ACL: Handle 2048 [1/6] flags 0x00 dlen 16
so that [1/6]? that means you're looking at buffer slot 1 out of 6 available controller ACL buffers ๐ it's reflecting the host-side HCI flow control โ the host is keeping tabs on how many buffers the controller has available and showing you the current usage. and when the controller sends Number of Completed Packets events, buffers get freed and that count goes down. literally just bookkeeping, but make it cute ๐
๐ HCI ERROR AND DISCONNECT REASON CODES ๐
fun fact bestie: HCI status codes and disconnect reason codes share the same code space. they show up in the Status: and Reason: fields all over your trace, and btmon decodes them for you automatically โ but the hex values are still super useful for searching and filtering, so don't sleep on 'em ๐ฅ
๐ธ Common Disconnect Reasons
0x05โ Authentication Failure โ Pairing or encryption setup failed. Key may be stale or devices have mismatched security databases.0x08โ Connection Timeout โ The supervision timer expired. The remote device moved out of range or stopped responding. This is an RF link loss.0x13โ Remote User Terminated Connection โ The remote device intentionally disconnected. This is the normal graceful disconnect.0x14โ Remote Device Terminated due to Low Resources โ The remote device ran out of resources (memory, connection slots).0x15โ Remote Device Terminated due to Power Off โ The remote device is powering down.0x16โ Connection Terminated By Local Host โ The local BlueZ stack intentionally disconnected. Normal whenbluetoothdinitiates disconnect.0x1fโ Unspecified Error โ Generic error. Often indicates a firmware issue.0x22โ LMP/LL Response Timeout โ Link layer procedure timed out. The remote device stopped responding to LL control PDUs.0x28โ Instant Passed โ A timing-critical operation missed its deadline. Often seen with connection parameter updates.0x2fโ Insufficient Security โ The required security level (encryption, MITM protection) was not met.0x3bโ Unacceptable Connection Parameters โ The remote rejected a connection parameter update.0x3dโ Connection Terminated due to MIC Failure โ Encryption integrity check failed. Possible key mismatch or corruption.0x3eโ Connection Failed to be Established โ Connection attempt failed entirely (e.g., the remote device did not respond to connection requests).0x3fโ MAC Connection Failed โ MAC-level connection failure.0x44โ Operation Cancelled by Host โ The host cancelled the operation before it completed.
โญ Full Error Code Table
the complete set of HCI error codes (0x00-0x45) is defined in the Bluetooth Core Specification, Volume 1, Part F ๐ and btmon decodes every single one of them automatically in the Status: and Reason: fields โ no manual lookup era, we love to see it ๐ the source mapping lives in monitor/packet.c (error2str_table) if you ever wanna go stare at the receipts yourself ๐ซถ
๐ง RECONSTRUCTING A GATT DATABASE FROM SNOOP TRACES ๐ง
okay so bestie, here's the iconic part: a btsnoop trace literally contains the whole ATT protocol exchange that GATT clients and servers use to discover each other's services. By reading the discovery requests and responses, you can reconstruct the full GATT database of a remote device โ even if you never touch the device itself. no cap, that's kind of magic โจ
This section explains the GATT discovery procedure and how each ATT operation shows up in btmon output. iykyk ๐
๐ฆ Overview of GATT Discovery ๐ฆ
GATT discovery is a multi-phase process where a client queries the server's attribute database using ATT protocol operations. The phases are:
- Primary Service Discovery -- Find all primary services and their handle ranges.
- Secondary Service Discovery -- Find any secondary (included-only) services.
- Included Service Discovery -- Find which services include other services.
- Characteristic Discovery -- Find all characteristics within each service.
- Descriptor Discovery -- Find all descriptors for each characteristic.
- Characteristic Value Reading -- Read the values of readable characteristics.
Each phase uses a specific ATT operation and produces a request/response pattern in the trace. The client repeats each request with advancing handle ranges until the server responds with Attribute Not Found, indicating the end of that phase. periodt. ๐ซ
๐ธ Phase 1: Primary Service Discovery (Read By Group Type) ๐ธ
The client discovers primary services using Read By Group Type Request with the Primary Service UUID (0x2800) as the group type. this is the main character phase tbh ๐
Request:
< ACL Data TX: Handle 2048 flags 0x00 dlen 11 #516 [hci0] 0.124726
ATT: Read By Group Type Request (0x10) len 6
Handle range: 0x0001-0xffff
Attribute group type: Primary Service (0x2800)
The first request always starts at handle 0x0001 and searches through 0xffff (the entire handle space). literally the whole map. ๐บ๏ธ
Response:
> ACL Data RX: Handle 2048 flags 0x02 dlen 42 #523 [hci0] 0.240151
ATT: Read By Group Type Response (0x11) len 37
Attribute data length: 6
Attribute group list: 6 entries
Handle range: 0x0001-0x0009
UUID: Generic Access Profile (0x1800)
Handle range: 0x000a-0x0011
UUID: Generic Attribute Profile (0x1801)
Handle range: 0x0012-0x0014
UUID: Device Information (0x180a)
Handle range: 0x0015-0x0039
UUID: Generic Telephony Bearer (0x184c)
Handle range: 0x003a-0x0059
UUID: Generic Media Control (0x1849)
Handle range: 0x005a-0x005c
UUID: Telephony and Media Audio (0x1855)
Each entry provides:
- Handle range -- The start and end handle of the service. All attributes belonging to this service (characteristics, descriptors) have handles within this range.
- UUID -- The service UUID. Standard 16-bit UUIDs are shown with their name (e.g.,
Generic Access Profile). 128-bit vendor-specific UUIDs appear as full UUID strings.
The client continues by sending another request starting after the last handle in the response:
< ACL Data TX: Handle 2048 flags 0x00 dlen 11 #525 [hci0] 0.240641
ATT: Read By Group Type Request (0x10) len 6
Handle range: 0x005d-0xffff
Attribute group type: Primary Service (0x2800)
This continues until the server responds with Attribute Not Found:
> ACL Data RX: Handle 2048 flags 0x02 dlen 9 #532 [hci0] 0.360069
ATT: Error Response (0x01) len 4
Read By Group Type Request (0x10)
Handle: 0x005d
Error: Attribute Not Found (0x0a)
This error indicates that no more primary services exist beyond handle 0x005d. The client now has the complete list of primary services. slay, phase one ate. โ
NOTE: TheAttribute data lengthfield indicates the size of each entry in the response. A value of6means 16-bit UUIDs (2 bytes start handle + 2 bytes end handle + 2 bytes UUID). A value of20means 128-bit UUIDs (2 + 2 + 16). If the server has both 16-bit and 128-bit service UUIDs, they are returned in separate responses because all entries in a single response must have the same length. important little detail, don't skip it bestie ๐ซถ
๐ Phase 2: Secondary Service Discovery ๐
After primary services, the client may discover secondary services using the same Read By Group Type Request but with the Secondary Service UUID (0x2801):
< ACL Data TX: Handle 2048 flags 0x00 dlen 11 #534 [hci0] 0.360752
ATT: Read By Group Type Request (0x10) len 6
Handle range: 0x0001-0xffff
Attribute group type: Secondary Service (0x2801)
If no secondary services exist, the server responds with Attribute Not Found. Secondary services are not directly accessible to clients -- they are only reachable via include references from primary services. lowkey mysterious side-character energy. ๐ฆ
๐ Phase 3: Included Service Discovery (Read By Type) ๐
To discover which services include other services, the client uses Read By Type Request with the Include UUID (0x2802):
< ACL Data TX: Handle 2048 flags 0x00 dlen 11 #540 [hci0] 0.480731
ATT: Read By Type Request (0x08) len 6
Handle range: 0x0001-0x005c
Attribute type: Include (0x2802)
The handle range typically spans the entire discovered database. Each include declaration in the response identifies a service that is included by the service containing that handle. it's giving "who invited who" energy. ๐
๐ฅ Phase 4: Characteristic Discovery (Read By Type) ๐ฅ
For each service, the client discovers its characteristics using Read By Type Request with the Characteristic UUID (0x2803). The handle range is limited to the service's handle range. per-service snooping, we love to see it. ๐
Request:
> ACL Data RX: Handle 2048 flags 0x02 dlen 11 #531 [hci0] 0.360063
ATT: Read By Type Request (0x08) len 6
Handle range: 0x0008-0x0011
Attribute type: Characteristic (0x2803)
Response:
< ACL Data TX: Handle 2048 flags 0x00 dlen 27 #533 [hci0] 0.360714
ATT: Read By Type Response (0x09) len 22
Attribute data length: 7
Attribute data list: 3 entries
Handle: 0x0009
Value[5]: 200a00052a
Properties: 0x20
Indicate (0x20)
Value Handle: 0x000a
Value UUID: Service Changed (0x2a05)
Handle: 0x000c
Value[5]: 0a0d00292b
Properties: 0x0a
Read (0x02)
Write (0x08)
Value Handle: 0x000d
Value UUID: Client Supported Features (0x2b29)
Handle: 0x000e
Value[5]: 020f002a2b
Properties: 0x02
Read (0x02)
Value Handle: 0x000f
Value UUID: Database Hash (0x2b2a)
Each characteristic entry provides:
- Handle -- The handle of the characteristic declaration attribute.
Properties โ a bitmask saying which operations are supported, bestie:
0x01โ Broadcast โ Can be broadcast in advertising data0x02โ Read โ Can be read0x04โ Write Without Response โ Can be written without acknowledgment0x08โ Write โ Can be written with acknowledgment0x10โ Notify โ Server can send notifications0x20โ Indicate โ Server can send indications0x40โ Authenticated Signed Writes โ Supports signed write commands0x80โ Extended Properties โ Has extended properties descriptor- Value Handle โ The handle where the characteristic's value is stored (always declaration handle + 1).
- Value UUID โ The UUID identifying the characteristic type.
The client continues with advancing handle ranges until it receives Attribute Not Found:
> ACL Data RX: Handle 2048 flags 0x02 dlen 9 #572 [hci0] 1.200228
ATT: Error Response (0x01) len 4
Read By Type Request (0x08)
Handle: 0x005c
Error: Attribute Not Found (0x0a)
Phase 5: Descriptor Discovery (Find Information) ๐ต๏ธ
Descriptors occupy the handles between a characteristic's value handle and the next characteristic declaration (or end of service). The client discovers them using Find Information Request.
Request:
> ACL Data RX: Handle 2048 flags 0x02 dlen 9 #556 [hci0] 0.959965
ATT: Find Information Request (0x04) len 4
Handle range: 0x000b-0x000b
The handle range covers the gap between the characteristic value handle and the next characteristic declaration handle.
Response:
< ACL Data TX: Handle 2048 flags 0x00 dlen 10 #561 [hci0] 0.961049
ATT: Find Information Response (0x05) len 5
Format: UUID-16 (0x01)
Handle: 0x000b
UUID: Client Characteristic Configuration (0x2902)
Common descriptor UUIDs:
0x2900โ Characteristic Extended Properties โ Additional property bits0x2901โ Characteristic User Description โ Human-readable description string0x2902โ Client Characteristic Configuration (CCC) โ Enable/disable notifications or indications0x2903โ Server Characteristic Configuration โ Server-side broadcast configuration0x2904โ Characteristic Presentation Format โ Data format, exponent, unit
Phase 6: Reading Characteristic Values ๐
After discovery, the client may read characteristic values using Read Request:
> ACL Data RX: Handle 2048 flags 0x02 dlen 7 #577 [hci0] 1.380203
ATT: Read Request (0x0a) len 2
Handle: 0x000f
< ACL Data TX: Handle 2048 flags 0x00 dlen 21 #579 [hci0] 1.380774
ATT: Read Response (0x0b) len 16
Value[16]: a470d508da8751a2a50b79da0250bfda
The Handle in the request corresponds to a characteristic value handle from the discovery phase. btmon shows the raw value bytes; the interpretation depends on the characteristic UUID.
Find By Type Value (Targeted Service Search) ๐ฏ
In addition to discovering all services, a client can search for a specific service UUID using Find By Type Value Request:
> ACL Data RX: Handle 2048 flags 0x02 dlen 13 #513 [hci0] 0.124195
ATT: Find By Type Value Request (0x06) len 8
Handle range: 0x0001-0xffff
Attribute type: Primary Service (0x2800)
UUID: Generic Attribute Profile (0x1801)
< ACL Data TX: Handle 2048 flags 0x00 dlen 9 #515 [hci0] 0.124684
ATT: Find By Type Value Response (0x07) len 4
Handle range: 0x0008-0x0011
This returns only the handle range for the matching service, without iterating through all services. If the service is not found:
< ACL Data TX: Handle 2048 flags 0x00 dlen 9 #524 [hci0] 0.240607
ATT: Error Response (0x01) len 4
Find By Type Value Request (0x06)
Handle: 0x0012
Error: Attribute Not Found (0x0a)
Bidirectional Discovery ๐
Both devices in a connection can act as GATT client and server simultaneously. In a btsnoop trace, you may see interleaved discovery in both directions:
TX (<)requests +RX (>)responses โ The local device (whose trace this is) is acting as a GATT client, discovering the remote device's services.RX (>)requests +TX (<)responses โ The remote device is acting as a GATT client, discovering the local device's services.
For example, the local server responding to the remote's discovery:
> ACL Data RX: Handle 2048 flags 0x02 dlen 11 #584 [hci0] 1.512006
ATT: Read By Group Type Request (0x10) len 6
Handle range: 0x0001-0xffff
Attribute group type: Primary Service (0x2800)
< ACL Data TX: Handle 2048 flags 0x00 dlen 66 #586 [hci0] 1.518778
ATT: Read By Group Type Response (0x11) len 61
Attribute data length: 6
Attribute group list: 10 entries
Handle range: 0x0001-0x0007
UUID: Generic Access Profile (0x1800)
Handle range: 0x0008-0x0011
UUID: Generic Attribute Profile (0x1801)
Handle range: 0x0012-0x0014
UUID: Device Information (0x180a)
Handle range: 0x0015-0x001e
UUID: Coordinated Set Identification (0x1846)
Handle range: 0x001f-0x0020
UUID: Common Audio (0x1853)
Handle range: 0x0021-0x0024
UUID: Microphone Control (0x184d)
Handle range: 0x0041-0x004b
UUID: Volume Control (0x1844)
Handle range: 0x006b-0x0073
UUID: Broadcast Audio Scan (0x184f)
Handle range: 0x0074-0x0086
UUID: Published Audio Capabilities (0x1850)
Handle range: 0x0087-0x0096
UUID: Audio Stream Control (0x184e)
Okay so this trace is literally showing the local device's own GATT database as the remote sees it. If you're trying to reconstruct the remote device's database, lock in on the TX requests and RX responses โ that's the local device acting as client. ๐
๐ ๏ธ Building the Attribute Table
To reconstruct the GATT database, extract the discovery responses and organize them into a table. Using the trace above as an example, the remote device at address 00:11:22:33:44:55 has:
Services (from Read By Group Type Response) โ columns are Handle Range / UUID / Service Name:
0x0001-0x0009โ0x1800โ Generic Access Profile0x000a-0x0011โ0x1801โ Generic Attribute Profile0x0012-0x0014โ0x180aโ Device Information0x0015-0x0039โ0x184cโ Generic Telephony Bearer0x003a-0x0059โ0x1849โ Generic Media Control0x005a-0x005cโ0x1855โ Telephony and Media Audio
Characteristics (from Read By Type Response, within GAP 0x0001-0x0009) โ columns are Handle / Value Handle / Properties / UUID / Name:
- handle
0x0002, value handle0x0003, properties Read, UUID0x2a00, name Device Name - handle
0x0004, value handle0x0005, properties Read, UUID0x2a01, name Appearance - handle
0x0006, value handle0x0007, properties Read, UUID0x2a04, name Peripheral Preferred Conn Params - handle
0x0008, value handle0x0009, properties Read, UUID0x2aa6, name Central Address Resolution
Characteristics (within GATT 0x000a-0x0011) โ columns are Handle / Value Handle / Properties / UUID / Name:
- handle
0x000b, value handle0x000c, properties Indicate, UUID0x2a05, name Service Changed - handle
0x000e, value handle0x000f, properties Read, Write, UUID0x2b29, name Client Supported Features - handle
0x0010, value handle0x0011, properties Read, UUID0x2b2a, name Database Hash
Descriptors (from Find Information Response) โ columns are Handle / UUID / Name:
- handle
0x000d, UUID0x2902, name Client Characteristic Configuration
The CCC descriptor at handle 0x000d belongs to the Service Changed characteristic (0x000c), because it falls between that value handle and the next characteristic declaration at 0x000e. ๐
๐ SMP PAIRING FLOW ๐
The Security Manager Protocol (SMP) handles pairing, key generation, and key distribution between Bluetooth devices. SMP traffic appears inside L2CAP on fixed CID 0x0006 (LE) or CID 0x0007 (BR/EDR). btmon decodes all SMP operations automatically. ๐ซถ
๐ฆ Pairing Phases
SMP pairing proceeds in three phases. Each phase produces a distinct pattern in the btmon output. No cap.
๐ธ Phase 1: Feature Exchange
Pairing begins when one device sends a Security Request (peripheral) or the host initiates pairing directly. The initiator sends a Pairing Request and the responder replies with a Pairing Response:
> ACL Data RX: Handle 2048 flags 0x02 dlen 11 #497 [hci0] 0.026107
SMP: Pairing Request (0x01) len 6
IO capability: NoInputNoOutput (0x03)
OOB data: Authentication data not present (0x00)
Authentication requirement: Bonding, MITM, SC, CT2 (0x2d)
Max encryption key size: 16
Initiator key distribution: IdKey Sign (0x06)
Responder key distribution: IdKey Sign (0x06)
< ACL Data TX: Handle 2048 flags 0x00 dlen 11 #499 [hci0] 0.026894
SMP: Pairing Response (0x02) len 6
IO capability: KeyboardDisplay (0x04)
OOB data: Authentication data not present (0x00)
Authentication requirement: Bonding, SC, CT2 (0x29)
Max encryption key size: 16
Initiator key distribution: IdKey (0x02)
Responder key distribution: IdKey (0x02)
Key fields to check:
Authentication requirementโ the SC flag indicates Secure Connections. Its absence means Legacy Pairing.IO capabilityโ determines the association model (Just Works, Passkey Entry, Numeric Comparison, OOB).Key distributionโ which keys each side will send after encryption is established.IdKey= Identity Resolving Key (IRK),EncKey= Long Term Key (legacy only),Sign= CSRK.
๐ฅ Phase 2: Authentication (Secure Connections)
For Secure Connections pairing (SC flag set), both devices exchange public keys, then perform the confirm/random value exchange:
> ACL Data RX: Handle 2048 flags 0x02 dlen 69 #501 [hci0] 0.098224
SMP: Pairing Public Key (0x0c) len 64
X: 1a2b3c4d...
Y: 5e6f7a8b...
< ACL Data TX: Handle 2048 flags 0x00 dlen 69 #503 [hci0] 0.148556
SMP: Pairing Public Key (0x0c) len 64
X: 9c8d7e6f...
Y: 0a1b2c3d...
< ACL Data TX: Handle 2048 flags 0x00 dlen 21 #505 [hci0] 0.149003
SMP: Pairing Confirm (0x03) len 16
Confirm value: a1b2c3d4e5f6...
> ACL Data RX: Handle 2048 flags 0x02 dlen 21 #507 [hci0] 0.212884
SMP: Pairing Random (0x04) len 16
Random value: 1122334455...
< ACL Data TX: Handle 2048 flags 0x00 dlen 21 #509 [hci0] 0.213100
SMP: Pairing Random (0x04) len 16
Random value: 6677889900...
> ACL Data RX: Handle 2048 flags 0x02 dlen 21 #511 [hci0] 0.278003
SMP: Pairing DHKey Check (0x0d) len 16
E: aabbccddee...
< ACL Data TX: Handle 2048 flags 0x00 dlen 21 #513 [hci0] 0.278450
SMP: Pairing DHKey Check (0x0d) len 16
E: ffeeddccbb...
After DHKey Check, the initiator starts encryption at the HCI level:
< HCI Command: LE Start Encryption (0x08|0x0019) plen 28 #515 [hci0] 0.279002
> HCI Event: Encryption Change (0x08) plen 4 #517 [hci0] 0.342556
Status: Success (0x00)
Handle: 2048
Encryption: Enabled with AES-CCM (0x01)
๐ Phase 2: Authentication (Legacy Pairing)
Legacy pairing (no SC flag) skips the Public Key and DHKey Check exchanges. Only Confirm and Random values are exchanged:
< ACL Data TX: Handle 2048 flags 0x00 dlen 21 #501 [hci0] 0.098224
SMP: Pairing Confirm (0x03) len 16
Confirm value: ...
> ACL Data RX: Handle 2048 flags 0x02 dlen 21 #503 [hci0] 0.162556
SMP: Pairing Confirm (0x03) len 16
Confirm value: ...
< ACL Data TX: Handle 2048 flags 0x00 dlen 21 #505 [hci0] 0.163003
SMP: Pairing Random (0x04) len 16
Random value: ...
> ACL Data RX: Handle 2048 flags 0x02 dlen 21 #507 [hci0] 0.228884
SMP: Pairing Random (0x04) len 16
Random value: ...
๐ Phase 3: Key Distribution
After encryption is established, each device distributes keys as negotiated in Phase 1:
> ACL Data RX: Handle 2048 flags 0x02 dlen 21 #519 [hci0] 0.343002
SMP: Identity Information (0x08) len 16
Identity resolving key: 00112233445566778899aabbccddeeff
> ACL Data RX: Handle 2048 flags 0x02 dlen 12 #521 [hci0] 0.343556
SMP: Identity Address Information (0x09) len 7
Address type: Public (0x00)
Address: 00:11:22:33:44:55
The Identity Address Information reveals the device's true public or static random address (as opposed to a Resolvable Private Address used during connection).
For Legacy Pairing, LTK distribution also appears:
> ACL Data RX: Handle 2048 flags 0x02 dlen 21 #519 [hci0] 0.343002
SMP: Encryption Information (0x06) len 16
Long term key: 00112233...
> ACL Data RX: Handle 2048 flags 0x02 dlen 15 #521 [hci0] 0.343556
SMP: Central Identification (0x07) len 10
EDIV: 0x1234
Rand: 0x0123456789abcdef
๐ Pairing Failure
When pairing fails, one device sends a Pairing Failed PDU:
> ACL Data RX: Handle 2048 flags 0x02 dlen 6 #505 [hci0] 0.213002
SMP: Pairing Failed (0x05) len 1
Reason: Authentication requirements (0x03)
๐ SMP failure reasons
When SMP goes sideways, the reason code tells you exactly which flavour of sideways it went. Match it up here, bestie:
0x01โ Passkey Entry Failed โ user cancelled or typed the wrong passkey ๐0x02โ OOB Not Available โ OOB data was expected but nobody provided it0x03โ Authentication Requirements โ the devices cannot agree on a security level (like, one of them requires MITM but the IO caps only allow Just Works)0x04โ Confirm Value Failed โ the cryptographic check failed; possible MITM attack ๐ฅ (yes, for real, pay attention to this one)0x05โ Pairing Not Supported โ remote doesn't support pairing, that's it, that's the tweet0x06โ Encryption Key Size โ cannot agree on a key size0x07โ Command Not Supported โ received an unrecognized SMP command0x08โ Unspecified Reason โ generic failure, the "it's giving nothing" of error codes ๐0x09โ Repeated Attempts โ pairing is rate-limited; wait before retrying0x0aโ Invalid Parameters โ invalid fields in the SMP command0x0bโ DHKey Check Failed โ ECDH key agreement failed (SC only)0x0cโ Numeric Comparison Failed โ user rejected the numeric comparison ๐ซถ0x0dโ BR/EDR Pairing In Progress โ classic pairing is already active0x0eโ Cross-Transport Key Derivation Not Allowed โ CTKD rejected by policy
๐ Automating Pairing Analysis
Okay so you don't wanna hand-scroll a million lines, right? Here's how to let grep do the heavy lifting, bestie.
First up, find every pairing attempt:
grep -n "Pairing Request\|Pairing Response\|Pairing Failed\|Pairing Public Key\|DHKey Check" output.txt
Now check which pairing method got used โ Secure Connections or Legacy:
- If
Pairing Public Keyappears between Request/Response and Confirm, it's Secure Connections โจ - If only Confirm/Random follow Request/Response, that's Legacy Pairing
- Also check the Authentication requirement line for the SC flag, iykyk
Wanna catch the fails? Easy:
grep -n "Pairing Failed" output.txt
Next: correlate pairing with encryption ๐ After a successful pairing you should expect an Encryption Change with Status: Success. Search for it like:
grep -n "Encryption Change\|Encryption:" output.txt
And to spot re-pairing on reconnect ๐ โ reconnections to a bonded device should show Encryption Change without any SMP traffic (it's using the stored keys, slay). If an SMP Pairing Request shows up on reconnection, that bond was lost on one side. Rude.
Here's the full pairing diagnosis pattern, step by step:
- Find
Pairing Requestโ note the handle, IO capabilities, auth requirements - Find
Pairing Responseโ compare IO capabilities to determine the association model - Check for
Pairing Failedโ if it's there, the reason code identifies the failure - Check for
Encryption ChangewithStatus: Successโ confirms pairing completed - Check for
Identity Address Informationโ reveals the device's true address ๐ต๏ธ
๐ L2CAP CHANNEL TRACKING
So L2CAP (that's Logical Link Control and Adaptation Protocol, say it with me) multiplexes multiple logical channels over a single ACL connection. btmon decodes the L2CAP signaling for you automatically and routes data to the higher-layer protocol decoders based on the channel. Literally does it for you. No cap. ๐
โจ Fixed Channels โจ
Okay so fixed channels are the iconic ones โ they already have a pre-assigned Channel Identifier (CID) and literally do not need signaling to set themselves up. No handshake, no "hey can we talk", they just exist. Main character energy fr fr ๐
0x0001โ L2CAP Signaling (BR/EDR) โ channel management for classic connections0x0002โ Connectionless Reception โ connectionless L2CAP data0x0003โ AMP Manager โ AMP (Alternate MAC/PHY) control0x0004โ ATT โ Attribute Protocol (GATT operations)0x0005โ L2CAP Signaling (LE) โ channel management for LE connections0x0006โ SMP (LE) โ Security Manager Protocol0x0007โ SMP (BR/EDR) โ Security Manager over classic transport
In btmon output, fixed channel traffic is decoded directly โ no L2CAP signaling preamble at all, it just shows up decoded. For example, ATT on CID 0x0004 appears as:
< ACL Data TX: Handle 2048 flags 0x00 dlen 7 #494 [hci0] 0.004488
ATT: Exchange MTU Request (0x02) len 2
Client RX MTU: 517
๐ธ Dynamic Channels (BR/EDR) ๐ธ
Classic Bluetooth uses L2CAP signaling on CID 0x0001 to establish dynamic channels, and each channel is identified by a PSM (Protocol/Service Multiplexer) โ that's the thing that decides which protocol gets to run on it. Think of the PSM as the vibe check that determines who's allowed in ๐
Channel establishment looks like this:
> ACL Data RX: Handle 256 flags 0x02 dlen 16 #142 [hci0] 2.034556
L2CAP: Connection Request (0x02) ident 3 len 4
PSM: 25 (0x0019)
Source CID: 0x0040
< ACL Data TX: Handle 256 flags 0x00 dlen 20 #144 [hci0] 2.035002
L2CAP: Connection Response (0x03) ident 3 len 8
Destination CID: 0x0041
Source CID: 0x0040
Result: Connection successful (0x0000)
Status: No further information available (0x0000)
And after the connection is up, configuration gets exchanged:
> ACL Data RX: Handle 256 flags 0x02 dlen 20 #146 [hci0] 2.035556
L2CAP: Configure Request (0x04) ident 4 len 8
Destination CID: 0x0041
Flags: 0x0000
Option: MTU (0x01) [2]
MTU: 1024
< ACL Data TX: Handle 256 flags 0x00 dlen 18 #148 [hci0] 2.036003
L2CAP: Configure Response (0x05) ident 4 len 6
Source CID: 0x0040
Flags: 0x0000
Result: Success (0x0000)
๐ฆ Common PSM-to-protocol mappings ๐ฆ
These are the ones you'll keep seeing over and over, so memorize the squad:
0x0001โ SDP โ Service Discovery Protocol0x0003โ RFCOMM โ serial port emulation (SPP, HFP, etc.)0x000fโ BNEP โ Bluetooth Network Encapsulation Protocol0x0017โ AVCTP โ Audio/Video Control Transport (AVRCP)0x0019โ AVDTP โ Audio/Video Distribution Transport (A2DP)0x001bโ AVCTP Browsing โ AVRCP browsing channel0x001fโ ATT (BR/EDR) โ Attribute Protocol over classic transport0x0027โ EATT โ Enhanced Attribute Protocol
๐ LE Credit-Based Channels ๐
LE connections do their dynamic channels over L2CAP signaling on CID 0x0005, and the LE Credit Based Connection mechanism is what gives you flow control โ it's giving very much "you may have exactly this many credits, bestie" ๐ญ
< ACL Data TX: Handle 2048 flags 0x00 dlen 18 #600 [hci0] 1.824003
LE L2CAP: LE Connection Request (0x14) ident 1 len 10
PSM: 39 (0x0027)
Source CID: 0x0040
MTU: 517
MPS: 251
Credits: 10
> ACL Data RX: Handle 2048 flags 0x02 dlen 18 #602 [hci0] 1.886556
LE L2CAP: LE Connection Response (0x15) ident 1 len 10
Destination CID: 0x0041
MTU: 517
MPS: 251
Credits: 10
Result: Connection successful (0x0000)
And EATT (Enhanced ATT) uses PSM 0x0027 over LE Credit-Based channels so it can provide multiple parallel ATT bearers โ like, several conversations at once, she's multitasking ๐ฅ
โญ Connection Parameter Updates โญ
LE peripherals are always requesting connection parameter changes over L2CAP signaling โ it's basically their hobby at this point:
< ACL Data TX: Handle 2048 flags 0x00 dlen 16 #493 [hci0] 0.003915
LE L2CAP: Connection Parameter Update Request (0x12) ident 1 len 8
Min interval: 24
Max interval: 40
Peripheral latency: 0
Timeout multiplier: 256
> ACL Data RX: Handle 2048 flags 0x02 dlen 10 #495 [hci0] 0.066003
LE L2CAP: Connection Parameter Update Response (0x13) ident 1 len 2
Result: Connection Parameters accepted (0x0000)
A result of Connection Parameters rejected (0x0001) means the central straight-up denied the request. no vote, no appeal, request denied bestie ๐
๐ ๏ธ Automating L2CAP Analysis
Find all L2CAP channel establishments:
grep -n "Connection Request\|Connection Response\|LE Connection Request\|LE Connection Response" output.txt
Track PSM usage (identifies which protocols are active):
grep -n "PSM:" output.txt
Find connection parameter update issues:
grep -n "Parameter Update Request\|Parameter Update Response\|Parameters rejected" output.txt
Find EATT channel setup:
grep -n "PSM: 39\|Enhanced Credit" output.txt
Trace a specific L2CAP channel: to follow traffic on a dynamic channel, note the Source CID and Destination CID from the Connection Request/Response pair. Then search for those CIDs in subsequent data frames. that's the whole trick, lowkey iconic ๐
๐ Throughput Estimation
btmon's --analyze (-a) mode computes per-channel statistics including throughput. For each L2CAP channel it reports:
- Speed โ computed as
bytes * 8 / latency_sum_mswherelatency_sum_msis the sum of inter-packet deltas (not wall-clock duration). This means idle gaps are excluded, so the figure represents the active sending rate rather than application-level throughput. โจ - Min/Avg/Max latency โ per-packet inter-arrival time range.
Example output:
Found TX L2CAP channel with CID 64
PSM 128 (0x0080)
Mode: LE Credit
MTU: 672
MPS: 490
TX packets: 29120/29114
TX Latency: 1-79 msec (~29 msec)
TX size: 494-494 octets (~494 octets)
TX speed: ~571 Kb/s
Channel details shown in analyze mode:
- Fixed channels (
CID <= 7) display their protocol name (e.g.,ATT,L2CAP Signaling (LE)). ๐ PSMis shown in both decimal and hexadecimal.Modeis decoded from Configure Request options (BR/EDR) or LE signaling (Basic,ERTM,LE Credit,Enhanced Credit, etc.).MTUandMPSare extracted from signaling exchanges.
Throughput caveats โ read these before you @ me about the numbers bestie:
- The speed value uses inter-packet latency sums as the denominator, not wall-clock elapsed time. If the sender pauses (e.g., waiting for credits), the idle period is not counted, which inflates the reported speed relative to overall throughput. ๐
TXlatency is measured from command submission to completion event.RXlatency is the inter-arrival time between consecutive packets. These measure different things, so TX and RX speeds for the same channel are not directly comparable. ๐ซถ- For windowed throughput (min/avg/max),
btsnoop-analyzeruses 1-second sampling windows over wall-clock time, which provides a more realistic view of application-level bandwidth variation.
Automated throughput extraction from btmon analyze output:
btmon -a trace.btsnoop 2>/dev/null | grep -E "speed:|Mode:|MTU:|MPS:|PSM"
๐ง LE AUDIO PROTOCOL FLOW
LE Audio uses a multi-layer protocol stack visible in btmon traces. The setup sequence involves ATT operations on specific GATT characteristics (PACS, ASCS) followed by HCI-level CIS/BIG management. btmon fully decodes all layers. every single one. she ate ๐ฅ
PACS: Published Audio Capabilities
Before audio streaming begins, devices exchange codec capabilities via the Published Audio Capabilities Service (PACS). The client reads PACS characteristics to learn what the remote device supports.
Sink PAC read (remote device's receive capabilities):
< ACL Data TX: Handle 2048 flags 0x00 dlen 7 #550 [hci0] 0.824003
ATT: Read Request (0x0a) len 2
Handle: 0x0075
> ACL Data RX: Handle 2048 flags 0x02 dlen 30 #552 [hci0] 0.886556
ATT: Read Response (0x0b) len 25
Handle: 0x0075
Number of PAC(s): 1
Codec: LC3 (0x06)
Codec Specific Capabilities: #0
Sampling Frequency: 8000 Hz 16000 Hz 24000 Hz 32000 Hz 48000 Hz
Frame Duration: 7.5 ms 10 ms
Audio Channel Counts: 1
Frame Length: 26 - 240
The PAC record shows codec capabilities using LTV (Length-Type-Value) encoding. Key fields:
- Codec โ
LC3(0x06) is the mandatory LE Audio codec ๐ - Sampling Frequency โ supported sample rates (bitmask)
- Frame Duration โ supported frame durations (
7.5 msand/or10 ms) - Audio Channel Counts โ supported channel counts
- Frame Length โ min and max octets per codec frame
Audio Locations (channel assignment):
> ACL Data RX: Handle 2048 flags 0x02 dlen 9 #554 [hci0] 0.948003
ATT: Read Response (0x0b) len 4
Handle: 0x0077
Location: Front Left
Available Audio Contexts (current use cases):
> ACL Data RX: Handle 2048 flags 0x02 dlen 9 #558 [hci0] 1.012556
ATT: Read Response (0x0b) len 4
Handle: 0x007b
Sink Context: Media Conversational
Source Context: Unspecified
ASCS: Audio Stream Control
The Audio Stream Control Service (ASCS) manages the ASE (Audio Stream Endpoint) state machine. Each ASE transitions through a defined set of states as streaming is set up and torn down. it's giving choreography honestly ๐
ASE State Machine:
Idle โโโบ Codec Configured โโโบ QoS Configured โโโบ Enabling โโโบ Streaming
โ
Idle โโโ Releasing โโโโโโโโโ Disabling โโโโโโโโโโโโโโโโโโโโโโโโโโโ
ASE Status notification (state change):
> ACL Data RX: Handle 2048 flags 0x02 dlen 20 #580 [hci0] 1.456003
ATT: Handle Value Notification (0x1b) len 15
Handle: 0x0088
ASE ID: 0x01
State: Codec Configured (0x01)
Framing: Unframed PDUs supported (0x00)
PHY: 0x02
LE 2M PHY (0x02)
RTN: 2
Max Transport Latency: 10
Presentation Delay Min: 20000 us
Presentation Delay Max: 40000 us
Preferred Presentation Delay Min: 20000 us
Preferred Presentation Delay Max: 40000 us
Codec: LC3 (0x06)
Sampling Frequency: 48000 Hz
Frame Duration: 10 ms
Audio Channel Allocation: Front Left
Frame Length: 120
ASE Control Point operations drive state transitions. The client writes to the ASE Control Point characteristic to issue commands:
< ACL Data TX: Handle 2048 flags 0x00 dlen 25 #582 [hci0] 1.518003
ATT: Write Request (0x12) len 20
Handle: 0x008b
ASE Control Point: Config Codec (0x01)
ASE ID: 0x01
Target Latency: Low Latency (0x01)
PHY: LE 2M PHY
Codec: LC3 (0x06)
Sampling Frequency: 48000 Hz
Frame Duration: 10 ms
Audio Channel Allocation: Front Left
Frame Length: 120
ASE Control Point commands: โ okay bestie, this is the whole state machine in one lil' cheat sheet, and yes you do need to know it ๐
0x01Config Codec โ picks the codec and its parameters, and moves the ASE from Idle โ Codec Configured.0x02Config QoS โ sets the CIG/CIS IDs and the QoS params, taking you Codec Configured โ QoS Configured.0x03Enable โ starts the ASE with metadata, QoS Configured โ Enabling.0x04Receiver Start Ready โ signals receiver readiness, Enabling โ Streaming, and this one's server-side.0x05Disable โ stops streaming, Streaming โ Disabling.0x06Receiver Stop Ready โ signals that the receiver has stopped.0x07Update Metadata โ changes the metadata mid-stream, the way that you can just swap the vibe while it's already playing.0x08Release โ tears the ASE all the way down, any โ Releasing โ Idle.
Bidirectional Streams (Source + Sink)
A device may expose multiple ASEs with different directions, and honestly this is the moment where LE Audio gets iconic. In the conversational (telephony) use case, the remote device typically has at least one Sink ASE (receives audio, e.g., speaker) and one Source ASE (sends audio, e.g., microphone). Both ASEs share the same CIG and often the same CIS, using the bidirectional capability of Connected Isochronous Streams. Efficiency queen behavior fr fr.
Direction indicators in btmon traces โ the way to tell who's who ๐
- The
Receiver Start Readycommand is only sent for Source ASEs (ASEs that send audio toward the local device). The server issues this command to indicate it is ready to transmit. - The
Receiver Stop Readycommand likewise applies to Source ASEs. - Sink ASEs transition directly from Enabling to Streaming without
Receiver Start Ready.
Typical bidirectional setup sequence (two ASEs on one CIS):
Config Codec ASE ID=1 (Sink) โ Codec Configured
Config Codec ASE ID=3 (Source) โ Codec Configured
Config QoS ASE ID=1 โ QoS Configured (CIG=X, CIS=Y)
Config QoS ASE ID=3 โ QoS Configured (CIG=X, CIS=Y)
Enable ASE ID=1 โ Enabling โ Streaming (immediate)
Enable ASE ID=3 โ Enabling
CIS Established (Success)
Setup ISO Data Path Input (HostโController, for Sink)
Setup ISO Data Path Output (ControllerโHost, for Source)
Receiver Start Ready ASE ID=3 โ Streaming
Both ASEs may reach Streaming at different times. The Sink ASE can start receiving audio as soon as CIS is established, while the Source ASE waits for the Receiver Start Ready handshake. No drama, they're just on different schedules ๐ซถ
NOTE: This is normal behavior. Seeing one Source ASE and one Sink ASE on the same connection is the standard bidirectional (conversational) configuration. It is not an error or misconfiguration. Both directions sharing a single CIS is efficient and expected. Do not come for it, it's slaying.
Multiple ASEs per direction are also valid. For example, a stereo headset may expose two Sink ASEs (left and right channels) and one Source ASE (mono microphone), each with its own GATT handle for ASE state notifications. Very "each of my personalities has her own handle" energy ๐
CIS Setup and Teardown
After ASE QoS Configuration, the host creates Connected Isochronous Streams (CIS) at the HCI level. This is where the real plumbing kicks in, bestie ๐ธ
CIG Parameters (configure the CIS group):
< HCI Command: LE Set CIG Parameters (0x08|0x0062) plen 26 #590 [hci0] 1.624003
CIG ID: 0x00
Central to Peripheral SDU Interval: 10000 us
Peripheral to Central SDU Interval: 10000 us
SCA: 0x00
Packing: Sequential (0x00)
Framing: Unframed (0x00)
Central to Peripheral Max Latency: 10 ms
Peripheral to Central Max Latency: 10 ms
Number of CIS: 1
CIS ID: 0x00
Central to Peripheral Max SDU: 120
Peripheral to Central Max SDU: 0
Central to Peripheral PHY: LE 2M PHY
Peripheral to Central PHY: LE 2M PHY
Central to Peripheral RTN: 2
Peripheral to Central RTN: 2
> HCI Event: Command Complete (0x0e) plen 8 #592 [hci0] 1.624556
LE Set CIG Parameters (0x08|0x0062) ncmd 1
Status: Success (0x00)
CIG ID: 0x00
Number of Handles: 1
Connection Handle: 2064
CIS Creation:
< HCI Command: LE Create CIS (0x08|0x0064) plen 9 #594 [hci0] 1.688003
Number of CIS: 1
CIS Handle: 2064
ACL Handle: 2048
> HCI Event: LE Meta Event (0x3e) plen 29 #596 [hci0] 1.756556
LE CIS Established (0x19)
Status: Success (0x00)
Connection Handle: 2064
CIG Sync Delay: 5000 us
CIS Sync Delay: 5000 us
Central to Peripheral Latency: 10000 us
Peripheral to Central Latency: 10000 us
Central to Peripheral PHY: LE 2M PHY
Peripheral to Central PHY: LE 2M PHY
NSE: 3
Central to Peripheral BN: 1
Peripheral to Central BN: 0
Central to Peripheral FT: 2
Peripheral to Central FT: 2
Max PDU C to P: 120
Max PDU P to C: 0
ISO Interval: 10.00 msec (0x0008)
Note that the CIS Handle (2064) is different from the ACL Handle (2048). CIS data packets use the CIS handle. Do not mix those up, that's the classic blunder and I will clock it ๐
ISO Data Path Setup:
< HCI Command: LE Setup ISO Data Path (0x08|0x006e) plen 13 #598 [hci0] 1.820003
Handle: 2064
Data Path Direction: Input (Host to Controller) (0x00)
Data Path ID: HCI (0x00)
Coding Format: LC3 (0x06)
Company ID: 0x0000
Vendor Codec ID: 0x0000
Controller Delay: 0 us
After this, ISO data packets flow on the CIS handle โ and look at that, actual audio data showing up to the party ๐
< ISO Data TS: Handle 2064 flags 0x02 dlen 124 #600 [hci0] 1.884003
Broadcast Audio (BIG)
Broadcast Isochronous Streams use BIG (Broadcast Isochronous Group) instead of CIS. The setup involves periodic advertising with BASE (Broadcast Audio Source Endpoint) announcements. Yes, it's a whole different flavor of the same delicious thing, and yes, the acronyms are unhinged. We move โญ
BASE announcement (in periodic advertising data):
> HCI Event: LE Meta Event (0x3e) plen 80 #200 [hci0] 0.500003
LE Periodic Advertising Report (0x0f)
...
Service Data: Basic Audio Announcement (0x1851)
Presentation Delay: 40000 us
Number of Subgroups: 1
Number of BIS: 2
Codec: LC3 (0x06)
Sampling Frequency: 48000 Hz
Frame Duration: 10 ms
Frame Length: 120
BIS #1
Audio Channel Allocation: Front Left
BIS #2
Audio Channel Allocation: Front Right
Front Left and Front Right having their own BIS entries? It's giving stereo, it's giving separation, we love to see it ๐
BIG creation (source side):
< HCI Command: LE Create BIG (0x08|0x0068) plen 31 #210 [hci0] 0.600003
BIG Handle: 0x00
Advertising Handle: 0x01
Number of BIS: 2
SDU Interval: 10000 us
Max SDU: 120
Max Latency: 10 ms
RTN: 2
PHY: LE 2M PHY
Packing: Sequential (0x00)
Framing: Unframed (0x00)
Encryption: Unencrypted (0x00)
BIG sync (receiver side): and that's exactly where we're picking up next, so don't go anywhere ๐ฆ
๐ซถ BIG Sync Receiver Flow ๐ซถ
Okay bestie, so a receiver has to do a whole little ritual before it can hear any broadcast audio. Like, it's a chain โ every link depends on the one before it. Here's the sacred order:
- Synchronize to periodic advertising โ the receiver must first discover and sync to the broadcaster's periodic advertising train.
- Receive PA Reports with BASE โ the periodic advertising data contains the BASE (Broadcast Audio Source Endpoint) structure describing the broadcast's codec configuration.
- Receive BIG Info Advertising Report โ this event tells the receiver that a BIG exists on the periodic advertising train and provides its parameters (number of BIS, encryption, SDU interval, etc.). This is the critical gate: without receiving BIG Info, the receiver cannot issue LE BIG Create Sync.
- Issue LE BIG Create Sync โ using the sync handle from step 1 and parameters from step 3.
- Receive BIG Sync Established โ the controller confirms sync with BIS connection handles.
- Setup ISO Data Path โ configure the data path for each BIS.
- ISO Data flows โ broadcast audio packets arrive.
If any step fails or is missing, the subsequent steps cannot proceed. The most common failure pattern is that BIG Info is never received (e.g., the periodic advertising data does not contain a BIG, or PA sync was lost before BIG Info arrived), which means LE BIG Create Sync is never sent. ๐ญ Like the whole thing just stalls out and you're sitting there staring at your trace going "hello??"
๐ Without PAST (Direct PA Sync)
When the receiver scans for and syncs to periodic advertising directly (without assistance from a Broadcast Assistant):
Step 1 โ Create PA sync:
< HCI Command: LE Periodic Advertising Create Sync (0x08|0x0044) plen 14 #100 [hci0] 0.100003
Options: 0x0000
SID: 0x01
Adv Address Type: Public (0x00)
Adv Address: XX:XX:XX:XX:XX:XX
Skip: 0x0000
Sync Timeout: 2000 msec (0x00c8)
Sync CTE Type: 0x0000
Step 2 โ PA Sync Established (sync handle assigned):
> HCI Event: LE Meta Event (0x3e) plen 16 #105 [hci0] 0.150003
LE Periodic Advertising Sync Established (0x0e)
Status: Success (0x00)
Sync Handle: 0x0001
Advertising SID: 0x01
Advertiser Address Type: Public (0x00)
Advertiser Address: XX:XX:XX:XX:XX:XX
Advertiser PHY: LE 2M PHY (0x02)
Periodic Advertising Interval: 10.000 msec (0x0008)
Advertiser Clock Accuracy: 0x05
Step 3 โ PA Reports (contain BASE data):
> HCI Event: LE Meta Event (0x3e) plen 80 #110 [hci0] 0.200003
LE Periodic Advertising Report (0x0f)
Sync Handle: 0x0001
...
Service Data: Basic Audio Announcement (0x1851)
Step 4 โ BIG Info Advertising Report (critical gate):
> HCI Event: LE Meta Event (0x3e) plen 24 #120 [hci0] 0.300003
LE BIG Info Advertising Report (0x22)
Sync Handle: 0x0001
Number BIS: 2
NSE: 4
ISO Interval: 10.000 msec (0x0008)
BN: 2
PTO: 1
IRC: 2
Maximum PDU: 120
SDU Interval: 10000 us
Maximum SDU: 120
PHY: LE 2M PHY (0x02)
Framing: Unframed (0x00)
Encryption: 0x00
This event is generated by the controller each time it receives a periodic advertising packet that contains BIG Info. It provides all parameters the receiver needs to decide whether to sync to the BIG. Key fields, bestie:
- Number BIS โ how many BIS streams are available.
- SDU Interval and Maximum SDU โ audio frame timing and size.
- Encryption โ whether a Broadcast Code is required (
0x01) or not (0x00). If encrypted, the receiver must supply the correct Broadcast Code in LE BIG Create Sync. - Sync Handle โ must match a currently active PA sync.
Step 5 โ BIG Create Sync (using sync handle + BIG Info):
< HCI Command: LE BIG Create Sync (0x08|0x006b) plen 15 #130 [hci0] 0.400003
BIG Handle: 0x00
BIG Sync Handle: 0x0001
Encryption: Unencrypted (0x00)
Broadcast Code: 00000000000000000000000000000000
Maximum Number Subevents: 0x00
Timeout: 2000 ms (0x00c8)
Number of BIS: 2
BIS: 0x01
BIS: 0x02
Step 6 โ BIG Sync Established:
> HCI Event: LE Meta Event (0x3e) plen 20 #135 [hci0] 0.450003
LE BIG Sync Established (0x1d)
Status: Success (0x00)
BIG Handle: 0x00
Transport Latency: 10000 us
NSE: 4
BN: 2
PTO: 1
IRC: 2
Maximum PDU: 120
ISO Interval: 10.000 msec (0x0008)
Connection Handle: 0x0010
Connection Handle: 0x0011
On success, the controller assigns BIS connection handles (0x0010, 0x0011 above). A non-zero Status indicates failure โ common errors:
0x3e(Connection Failed to be Established) โ BIG parameters do not match or the BIG is no longer present.0x3f(Limit Reached) โ controller resources exhausted.
Step 7 โ Setup ISO Data Path (for each BIS):
< HCI Command: LE Setup ISO Data Path (0x08|0x006e) plen 13 #140 [hci0] 0.500003
Connection Handle: 0x0010
Data Path Direction: Output (Controller to Host) (0x01)
Data Path ID: HCI (0x00)
< HCI Command: LE Setup ISO Data Path (0x08|0x006e) plen 13 #145 [hci0] 0.550003
Connection Handle: 0x0011
Data Path Direction: Output (Controller to Host) (0x01)
Data Path ID: HCI (0x00)
Step 8 โ ISO Data flows on BIS handles:
> ISO Data: Handle 0x0010 flags 0x02 dlen 124 #150 [hci0] 0.600003
> ISO Data: Handle 0x0011 flags 0x02 dlen 124 #151 [hci0] 0.600003
๐ฆ With PAST (Periodic Advertising Sync Transfer)
So when a Broadcast Assistant (think: a phone) helps a Scan Delegator (think: a hearing aid) sync to a broadcast, it can transfer the PA sync via PAST over an existing ACL connection. This avoids the delegator having to scan for and sync to the PA train itself โ honestly? iconic. โจ
The BASS (Broadcast Audio Scan Service) protocol coordinates this:
- The assistant writes an Add Source operation to the delegator's BASS control point with PA Sync set to
0x01(Synchronize via PAST). - The delegator prepares to receive the transfer by setting PAST parameters.
- The assistant transfers its PA sync to the delegator.
- The delegator receives the PAST event with a sync handle.
- From here, the flow continues as in the non-PAST case (PA Reports โ BIG Info โ BIG Create Sync โ etc.).
BASS Add Source (assistant writes to delegator, seen in ATT):
< ACL Data TX: Handle 64 flags 0x00 dlen 27 #300 [hci0] 1.000003
ATT: Write Command (0x52) len 22
Handle: 0x0025
Data: 04...
Opcode: Add Source (0x04)
Advertiser Address Type: Public (0x00)
Advertiser Address: XX:XX:XX:XX:XX:XX
Advertising SID: 0x01
PA Sync: Synchronize to PA - PAST (0x01)
PA Interval: 0x0008
Number of Subgroups: 1
BIS Sync: 0x00000003
Metadata Length: 0
PA Sync values in the Add Source operation:
0x00โ Do not synchronize to PA0x01โ Synchronize to PA, PAST available0x02โ Synchronize to PA, PAST not available (delegator must scan and sync directly)
PAST Parameters (delegator prepares to receive transfer):
< HCI Command: LE Periodic Advertising Sync Transfer Parameters (0x08|0x005c) plen 8 #310 [hci0] 1.100003
Connection handle: 64
Mode: Enabled with report events enabled (0x02)
Skip: 0x00
Sync timeout: 2000 msec (0x00c8)
Sync CTE Type: 0x0000
PAST Transfer (assistant sends its PA sync):
< HCI Command: LE Periodic Advertising Sync Transfer (0x08|0x005a) plen 6 #320 [hci0] 1.200003
Connection handle: 64
Service data: 0x0001
Sync handle: 1
PAST Received (delegator gets the sync handle):
> HCI Event: LE Meta Event (0x3e) plen 19 #325 [hci0] 1.250003
LE Periodic Advertising Sync Transfer Received (0x18)
Status: Success (0x00)
Handle: 64
Connection handle: 64
Service data: 0x0001
Sync handle: 1
SID: 0x01
Address type: Public (0x00)
Address: XX:XX:XX:XX:XX:XX
PHY: LE 2M PHY (0x02)
Periodic advertising Interval: 10.000
Clock Accuracy: 0x05
On success, the delegator now has a PA sync (Sync handle: 1) and will begin receiving PA Reports and BIG Info events, continuing from step 3 of the non-PAST flow above. Slay, the baton has been passed. ๐โโ๏ธ๐จ
NOTE: Race condition, bestie โ beware: The PAST Parameters command must be sent before the assistant sends the PAST Transfer. In BlueZ, the PA sync state in BASS is set after probing completes to avoid the remote sending PAST before the kernel has enabled PAST Parameters on the HCI level. It's giving "who goes first" energy and if you get it backwards it's a bad time. ๐
๐ BIG Sync Teardown
Receiver-initiated teardown โ the receiver terminates its BIG sync:
< HCI Command: LE BIG Terminate Sync (0x08|0x006c) plen 1 #500 [hci0] 5.000003
BIG Handle: 0x00
> HCI Event: Command Complete (0x0e) plen 5 #501 [hci0] 5.001003
LE BIG Terminate Sync (0x08|0x006c) ncmd 1
Status: Success (0x00)
BIG Handle: 0x00
Broadcaster-initiated teardown โ the broadcaster terminates its BIG, and the receiver gets a BIG Sync Lost event:
> HCI Event: LE Meta Event (0x3e) plen 2 #510 [hci0] 6.000003
LE BIG Sync Lost (0x1e)
BIG Handle: 0x00
Reason: Connection Terminated By Local Host (0x16)
The Reason field indicates why sync was lost:
0x08(Connection Timeout) โ BIG packets not received within the sync timeout.0x13(Remote User Terminated Connection) โ broadcaster stopped the BIG intentionally.0x16(Connection Terminated By Local Host) โ local controller terminated.0x3e(Connection Failed to be Established) โ could not establish sync initially.
Source-side BIG termination (broadcaster tears down):
< HCI Command: LE Terminate BIG (0x08|0x006a) plen 2 #520 [hci0] 7.000003
BIG Handle: 0x00
Reason: Connection Terminated By Local Host (0x16)
> HCI Event: LE Meta Event (0x3e) plen 2 #521 [hci0] 7.001003
LE BIG Terminate (0x1c)
BIG Handle: 0x00
Reason: Connection Terminated By Local Host (0x16)
BIG Sync Failure Diagnosis ๐
okay bestie, so your BIG sync is failing and you're like why. don't spiral. run through this checklist in order, fr fr, because each step depends on the one before it:
- Is PA sync established? โ Look for
LE Periodic Advertising Sync EstablishedwithStatus: Success. If it's missing, the receiver never synced to the PA train, periodt. - Are PA Reports arriving? โ Look for
LE Periodic Advertising Reportevents. If they're absent after PA sync, the PA train may have been lost. ๐ซ - Is BIG Info received? โ Look for
LE BIG Info Advertising Report. If this event never appears, the BIG does not exist on this PA train, or the broadcaster has not yet started it. Without BIG Info,LE BIG Create Synccannot be sent. (this one is the big gate, no cap) - Is BIG Create Sync sent? โ If BIG Info was received but
LE BIG Create Syncwas never sent, the host-side logic failed to act on the BIG Info (e.g., mismatched codec, encryption mismatch, application-level issue). - Does BIG Sync Established succeed? โ Check the
Statusfield. A non-zero status means the controller could not sync to the BIG. - Is ISO Data Path set up? โ Look for
LE Setup ISO Data Pathfor each BIS handle from BIG Sync Established. - Is ISO Data flowing? โ Look for ISO Data packets on the BIS handles. ๐
Automating LE Audio Analysis ๐ค
First up, just identify that LE Audio is even happening:
grep -n "ASE Control Point\|ASE ID\|State:.*Codec Configured\|State:.*QoS Configured\|State:.*Enabling\|State:.*Streaming\|State:.*Releasing" output.txt
Track ASE state transitions for one specific ASE:
grep -n "ASE ID:" output.txt
Then examine the State: line following each ASE ID match. ๐ต๏ธโโ๏ธ
Check the codec configuration:
grep -n "Codec: LC3\|Sampling Frequency:\|Frame Duration:\|Frame Length:\|Audio Channel" output.txt
Verify CIS establishment:
grep -n "Set CIG Parameters\|Create CIS\|CIS Established\|Setup ISO Data Path" output.txt
Detect CIS failures โ check the Status field after CIS Established:
grep -n "CIS Established" output.txt
Then examine the following line for Status:. ๐ค
Detect broadcast audio:
grep -n "Basic Audio Announcement\|Create BIG\|BIG Complete\|BIG Create Sync\|BIG Sync\|BIG Info\|BIG Terminate\|BIG Sync Lost" output.txt
Trace the BIG Sync receiver flow โ verify each prerequisite step:
grep -n "Periodic Advertising Create Sync\|Periodic Advertising Sync Established\|BIG Info Advertising Report\|BIG Create Sync\|BIG Sync Established\|BIG Sync Lost\|BIG Terminate" output.txt
Detect PAST-based sync โ check for Periodic Advertising Sync Transfer:
grep -n "Sync Transfer Parameters\|Sync Transfer (0x08\|PAST Received\|PA Sync:.*PAST\|Add Source" output.txt
Check BIG Info arrival โ the critical gate for BIG sync. If this is absent, the receiver has no BIG to sync to:
grep -n "BIG Info Advertising Report" output.txt
Full LE Audio diagnosis pattern, bestie:
Unicast (CIS) flow:
- Find PACS reads โ verify codec compatibility between devices
- Find ASE Control Point writes โ trace the Config Codec โ Config QoS โ Enable sequence
- Find ASE state notifications โ verify each transition succeeds
- Find CIG Parameters and CIS creation โ verify HCI-level setup
- Find CIS Established โ check
Statusfor success - Find Setup ISO Data Path โ verify data path configuration
- Find ISO Data packets โ confirm audio is flowing
- On failure, check ASE Control Point notification responses for error codes (
Response CodeandResponse Reasonfields)
Broadcast (BIG) receiver flow:
- Find Periodic Advertising Create Sync or PAST Received โ how did PA sync start?
- Find Periodic Advertising Sync Established or PAST Received with
Status: Successโ is PA synced? - Find Periodic Advertising Report with Basic Audio Announcement โ is BASE data arriving?
- Find BIG Info Advertising Report โ critical: does the BIG exist? If missing, the BIG cannot be synced.
- Find BIG Create Sync โ did the host request BIG sync?
- Find BIG Sync Established โ check
Statusfor success. - Find Setup ISO Data Path for each BIS handle.
- Find ISO Data on BIS handles โ confirm audio is flowing.
- On failure, check for BIG Sync Lost and examine
Reason. ๐ญ
๐ก CHANNEL SOUNDING PROTOCOL FLOW ๐ก
Channel Sounding (CS) enables precise distance measurement between two Bluetooth LE devices. It uses dedicated HCI commands and events to configure and execute measurement procedures that exchange tones and timing packets on multiple channels. btmon fully decodes all CS operations including step-level result data. step-level result data, bestie!! โจ
CS uses two roles: the Initiator sends CS packets first in each step, and the Reflector responds. Both devices must support CS (feature bits 46--48 in the LE feature set). ๐ซถ
Capability Exchange ๐
Before any CS activity, both devices must learn each other's CS capabilities. The local controller's capabilities are read once, and the remote device's capabilities are fetched over the air.
Read local CS capabilities:
< HCI Command: LE CS Read Local Supported Capabilities (0x08|0x0089) plen 0 #100 [hci0]
> HCI Event: Command Complete (0x0e) plen 42 #101 [hci0]
LE CS Read Local Supported Capabilities (0x08|0x0089) ncmd 1
Status: Success (0x00)
Num Config Supported: 4
Max Consecutive Procedures Supported: 255
Num Antennas Supported: 2
Max Antenna Paths Supported: 4
Roles Supported: 0x03
Initiator
Reflector
Modes Supported: 0x03
RTT Capability: 0x03
RTT AA Only N: 10
RTT Sounding N: 10
RTT Random Payload N: 10
CS Sync PHYs Supported: 0x02
LE 2M
T_IP1 Times Supported: 0x0005
10 us
30 us
T_IP2 Times Supported: 0x0005
10 us
30 us
T_FCS Times Supported: 0x0009
15 us
50 us
T_PM Times Supported: 0x0003
10 us
20 us
Key capability fields, let's go through them ๐
- Roles Supported โ Bitmask: bit 0 = Initiator, bit 1 = Reflector. A device must support at least one role.
- Modes Supported โ Bitmask of CS measurement modes the controller can perform (Mode 1 = RTT, Mode 2 = PBR tone, Mode 3 = both).
- CS Sync PHYs โ Physical layers available for CS sync exchange. LE 2M is the most common.
- Num Antennas / Max Antenna Paths โ Determines multi-antenna Phase-Based Ranging capability.
- T_IP / T_FCS / T_PM times โ Timing parameters the controller supports for inter-step and tone durations.
Read remote CS capabilities (over the air via LL):
< HCI Command: LE CS Read Remote Supported Capabilities (0x08|0x008a) plen 2 #110 [hci0]
Handle: 2048
> HCI Event: Command Status (0x0f) plen 4 #111 [hci0]
LE CS Read Remote Supported Capabilities (0x08|0x008a) ncmd 1
Status: Success (0x00)
> HCI Event: LE Meta Event (0x3e) plen 42 #115 [hci0]
LE CS Read Remote Supported Capabilities Complete (0x2c)
Status: Success (0x00)
Handle: 2048
Num Config Supported: 4
Max Consecutive Procedures Supported: 128
Num Antennas Supported: 1
Max Antenna Paths Supported: 1
Roles Supported: 0x03
Initiator
Reflector
Modes Supported: 0x03
...
This is an asynchronous command โ the controller sends Command Status immediately, then the completion event arrives after the LL exchange. If this fails with a non-zero status, the remote device may not support CS or the connection may have dropped. ๐ฌ
Cached capabilities can be written directly instead of reading over the air, using LE CS Write Cached Remote Supported Capabilities. handy fr.
Security Enable ๐
CS security must be enabled on the connection before creating configurations. This performs a CS security start procedure over the LL to exchange nonces:
< HCI Command: LE CS Security Enable (0x08|0x008c) plen 2 #120 [hci0]
Handle: 2048
> HCI Event: Command Status (0x0f) plen 4 #121 [hci0]
LE CS Security Enable (0x08|0x008c) ncmd 1
Status: Success (0x00)
> HCI Event: LE Meta Event (0x3e) plen 3 #125 [hci0]
LE CS Security Enable Complete (0x2e)
Status: Success (0x00)
Handle: 2048
Security Enable must complete successfully before configuration or procedure commands. If this fails, the link may lack encryption or the remote does not support CS security. ๐ฉ
Default Settings โ๏ธ
Default settings configure which roles the local device is willing to accept and the antenna/power preferences for this connection:
< HCI Command: LE CS Set Default Settings (0x08|0x008d) plen 5 #130 [hci0]
Handle: 2048
Role Enable: 0x03
Initiator
Reflector
CS Sync Antenna Selection: 0x01
Max TX Power: 20
> HCI Event: Command Complete (0x0e) plen 5 #131 [hci0]
LE CS Set Default Settings (0x08|0x008d) ncmd 1
Status: Success (0x00)
Handle: 2048
- Role Enable โ Bitmask of roles the device is willing to perform. Both sides typically enable both roles for flexibility.
- CS Sync Antenna Selection โ Preferred antenna for CS sync exchange.
- Max TX Power โ Upper bound on transmit power for CS procedures (dBm, signed).
FAE Table Exchange ๐
The Frequency Actuation Error (FAE) table contains per-channel calibration data that improves distance accuracy. Like capabilities, it can be read from the remote or written from a cache. ๐
Read remote FAE table:
< HCI Command: LE CS Read Remote FAE Table (0x08|0x008e) plen 2 #135 [hci0]
Handle: 2048
> HCI Event: Command Status (0x0f) plen 4 #136 [hci0]
LE CS Read Remote FAE Table (0x08|0x008e) ncmd 1
Status: Success (0x00)
> HCI Event: LE Meta Event (0x3e) plen 75 #140 [hci0]
LE CS Read Remote FAE Table Complete (0x2d)
Status: Success (0x00)
Handle: 2048
Packet NADMโ the Normalized Attack Detector Metric, aka the vibe check on whether someone's been messing with your RTT measurement ๐ญ๐ it tells you how likely it is that the whole thing has been tampered with:0x00= attack is extremely unlikely โ we're chilling, bestie ๐ซถ
lowkey the NADM field is the moment of truth fr โ it's the difference between "this distance reading is real" and "someone is lying to you about where they are" ๐ stay tuned for the other values, they're coming in the next bit of this section ๐
โฆand the attack-likelihood scale wraps up like this, bestie:
0x01= attack very unlikely0x02= attack unlikely0x03= attack possible0x04= attack likely0x05= attack very likely0x06= attack extremely likely0xff= unknown- ToA/ToD โ Time-of-Arrival minus Time-of-Departure difference (signed 16-bit).
0x8000= measurement not available. like, no data, girl, it's not there ๐ - PCT1/PCT2 โ Phase Correction Terms (I/Q, 12 bits each). Present only with certain RTT types. so don't go looking for them on every single packet, k?
Mode 2 โ Phase-Based Ranging (PBR / Tone Exchange) ๐ต
Mode 2 is the tone-exchange era โจ it swaps tones back and forth for that high-precision phase measurement, and multiple antenna paths can be measured simultaneously โ multitasking queen behavior, honestly ๐
Fields reported:
- Antenna Permutation Index โ Identifies the antenna switching pattern used. iykyk which antenna went where ๐
- PCT (per antenna path) โ Phase Correction Term with I component (bits
0โ11) and Q component (bits12โ23). yes bestie, one PCT per path, she's thorough ๐ธ - Tone Quality Indicator โ Low nibble:
0x00= high quality0x01= medium quality0x02= low quality0x03= not available
High nibble (tone extension slot):
0x00= not a tone extension slot0x01= tone extension slot, tone not expected0x02= tone extension slot, tone expected
Mode 3 โ Combined RTT + Tone ๐ฅ
Mode 3 is the collab nobody asked for but everybody needed: it combines Mode 1 and Mode 2 in a single step ๐ซถ The initial fields match Mode 1 (quality, NADM, RSSI, ToA/ToD, antenna), followed by Mode 2 tone data. literally the best of both worlds, periodt.
๐ฆ Result Status Fields
Every subevent result event carries status fields telling you whether the procedure and the subevent actually completed normally. vibe check for your measurement, basically โจ
Procedure Done Status:
0x00โ All results complete for this CS procedure ๐0x01โ Partial results, more events will follow (she's not done yet)0x0fโ All subsequent procedures aborted ๐ญ
Subevent Done Status:
0x00โ All results complete for this subevent0x01โ Partial results, continuation event will follow0x0fโ Current subevent aborted
Abort Reason (packed byte): โ yeah, both reasons live in one byte, low nibble and high nibble, they're roommates ๐
Low nibble (procedure abort reason):
0x00โ No abort0x01โ Aborted by local Host or remote request0x02โ Channel map has fewer than 15 channels0x03โ Channel map update instant has passed0x0fโ Unspecified reason
High nibble (subevent abort reason):
0x00โ No abort0x01โ Aborted by local Host or remote request0x02โ NoCS_SYNC(Mode 0) received0x03โ Scheduling conflict or limited resources0x0fโ Unspecified reason
๐ CS State Machine
Okay so this is the part where it gets good ๐ a CS configuration transitions through these states on each device. And here's the tea: the state is not reported explicitly in a single field โ you gotta infer it from the sequence of HCI commands and events. detective mode, activated ๐
โโโโโโโโโโโโ
โ IDLE โ
โโโโโโฌโโโโโโ
โ LE CS Read Local/Remote Supported Capabilities
โ LE CS Security Enable Complete
โ LE CS Set Default Settings
โผ
โโโโโโโโโโโโโโโโโโโโ
โ CAPABILITIES โ Both devices' CS parameters are known.
โ EXCHANGED โ Security is established.
โโโโโโฌโโโโโโโโโโโโโโ
โ LE CS Create Config
โ โโโบ LE CS Config Complete (status=0x00)
โผ
โโโโโโโโโโโโโโโโโโโโ
โ CONFIGURED โ Config ID N exists with negotiated params.
โ (config_id=N) โ Can create up to 4 configs simultaneously.
โโโโโโฌโโโโโโโโโโโโโโ
โ LE CS Set Procedure Parameters
โผ
โโโโโโโโโโโโโโโโโโโโ
โ PARAMETERS SET โ Scheduling and antenna params are locked.
โ (config_id=N) โ
โโโโโโฌโโโโโโโโโโโโโโ
โ LE CS Procedure Enable (enable=1)
โ โโโบ LE CS Procedure Enable Complete (state=1)
โผ
โโโโโโโโโโโโโโโโโโโโ
โ PROCEDURE โ Controller is actively measuring.
โ RUNNING โ Subevent Result events stream in.
โ (config_id=N) โ
โโโโโโฌโโโโโโโโโโโโโโ
โ LE CS Procedure Enable (enable=0)
โ โโโบ LE CS Procedure Enable Complete (state=0)
โผ
โโโโโโโโโโโโโโโโโโโโ
โ CONFIGURED โ Config still exists, can re-enable.
โ (config_id=N) โ
โโโโโโฌโโโโโโโโโโโโโโ
โ LE CS Remove Config
โผ
โโโโโโโโโโโโ
โ IDLE โ
โโโโโโโโโโโโ
Multiple configurations can coexist, bestie โ removing one config does not affect the others. she's independent, she's thriving ๐
๐ฌ Typical CS Setup Sequence
A complete HCI-only CS distance measurement session follows this flow. Left column = the Host โโโบ Controller commands, right column = the resulting state. it's giving choreography ๐
Host โโโบ Controller State
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
LE CS Read Local Supported Capabilities IDLE
โโโ Command Complete (capabilities)
โ
LE CS Read Remote Supported Capabilities โ
โโโ Command Status โ
โโโ LE CS Read Remote Supp. Cap. Complete โ
โ
LE CS Security Enable โ
โโโ Command Status โ
โโโ LE CS Security Enable Complete โ
โ
LE CS Set Default Settings โ
โโโ Command Complete โผ
CAPABILITIES EXCHANGED
LE CS Read Remote FAE Table (optional) โ
โโโ LE CS Read Remote FAE Complete โ
โ
LE CS Set Channel Classification (optional) โ
โโโ Command Complete โ
โ
LE CS Create Config (config_id=0) โ
โโโ Command Status โ
โโโ LE CS Config Complete โผ
CONFIGURED (id=0)
LE CS Set Procedure Parameters (config_id=0) โ
โโโ Command Complete โผ
PARAMETERS SET (id=0)
LE CS Procedure Enable (id=0, enable=1) โ
โโโ Command Status โ
โโโ LE CS Procedure Enable Complete (state=1)โผ
PROCEDURE RUNNING (id=0)
โโโ LE CS Subevent Result โ โ
โโโ LE CS Subevent Result Cont. โ(repeated) โ
โโโ LE CS Subevent Result โ โ
โ
LE CS Procedure Enable (id=0, enable=0) โ
โโโ Command Status โ
โโโ LE CS Procedure Enable Complete (state=0)โผ
CONFIGURED (id=0)
LE CS Remove Config (id=0) (optional) โ
โโโ Command Status โผ
IDLE
Steps above the first LE CS Create Config are one-time setup per connection ๐ซถ The config/params/enable/disable cycle can be repeated, so you're not locked in โ re-run the whole little routine as many times as you want โจ
๐งช CS Test Mode
CS Test mode lets the controller run CS procedures without a remote device โ for manufacturing and validation purposes. solo mode, no plus-one needed ๐
< HCI Command: LE CS Test (0x08|0x0095) plen 34 #400 [hci0]
Main Mode Type: 0x01
Sub Mode Type: 0xff
Main Mode Repetition: 0
Mode 0 Steps: 3
Role: Initiator (0x00)
RTT Type: 0x01
CS Sync PHY: LE 2M (0x01)
CS Sync Antenna Selection: 0x01
Subevent Len: 5000 us
Subevent Interval: 0
Max Num Subevents: 1
Transmit Power Level: 10
T_IP1 Time: 30 us
T_IP2 Time: 30 us
T_FCS Time: 50 us
T_PM Time: 10 us
T_SW Time: 0 us
Tone Antenna Config Selection: 0x01
SNR Control Initiator: 0x00
SNR Control Reflector: 0x00
DRBG Nonce: 0x0000
Channel Map Repetition: 1
Override Config: 0x0000
> HCI Event: Command Complete (0x0e) plen 5 #401 [hci0]
LE CS Test (0x08|0x0095) ncmd 1
Status: Success (0x00)
Results arrive as normal LE CS Subevent Result events ๐ The test is terminated with:
< HCI Command: LE CS Test End (0x08|0x0096) plen 0 #450 [hci0]
> HCI Event: LE Meta Event (0x3e) plen 1 #451 [hci0]
LE CS Test End Complete (0x33)
Status: Success (0x00)
โ ๏ธ Common Issues
the "why is it not working" corner, we've all been there ๐ญ
Security Enable fails
The connection must be encrypted before CS Security Enable. Check that SMP pairing and LE Encrypt have completed. Also verify both devices advertise CS support in their LE feature set. no encryption, no measurement, that's the rule ๐ซ
Config Complete with error
The controller may reject a configuration if the requested parameters are incompatible with the intersection of both devices' capabilities. Check that main mode, PHY, and timing parameters fall within both devices' supported ranges. it's giving "your vibes don't match", but for PHYs ๐ธ
Channel map too small
CS requires at least 15 channels. If channel classification or interference eliminates too many channels, the procedure aborts with abort reason 0x02 (low nibble). count your channels bestie ๐ข
No Mode 0 sync received
Subevent abort reason 0x02 (high nibble) means the Initiator's Mode 0 sync packet was not received by the Reflector (or vice versa). This indicates an RF issue, scheduling miss, or device out of range. something got lost in the mail ๐๐ญ
Procedure aborted by host
Abort reason 0x01 in either nibble means the local host or remote device requested termination, typically via Procedure Enable with enable=0. soโฆ somebody hung up on purpose, no drama, it's fine ๐
ToA/ToD not available (0x8000)โ okay so this one is lowkey a โsorry bestie, no time for youโ situation: the controller could not compute a valid time measurement for this step. Itโs common with weak signals, or when the CS Access Address was not detected (packet quality = 0x02). Fr fr, not a vibe. ๐ญ
๐ RANGING SERVICE (RAS) / RANGING PROFILE (RAP) ๐
Okay so boom: the Ranging Service (RAS) is a GATT service that transfers CS measurement data between devices at the application layer. It complements the HCI-level CS procedure by giving everyone a standardized way to exchange, acknowledge, and retrieve ranging results over ATT. The Ranging Profile (RAP) defines how a device discovers and interacts with RAS on a remote device. Itโs giving โwe have a whole protocol for this, bestie.โ ๐
And yes โ btmon decodes all RAS characteristics automatically when it sees ATT operations on the RAS UUIDs. Obsessed. โจ
RAS Service UUID: 0x185B
๐ RAS Characteristics ๐
RAS Featuresโ UUID0x2C14; properties: Read; description: bitmask of supported RAS capabilities. ๐RAS Real-time Ranging Dataโ UUID0x2C15; properties: Notify, Indicate; description: streaming ranging results as they are produced. ๐ธRAS On-demand Ranging Dataโ UUID0x2C16; properties: Notify, Indicate; description: ranging results retrieved on request. ๐ฆRAS Control Pointโ UUID0x2C17; properties: Write Without Response, Indicate; description: command interface for data retrieval and filtering. ๐๏ธRAS Ranging Data Readyโ UUID0x2C18; properties: Read, Notify, Indicate; description: signals that a new ranging dataset is available. โญRAS Ranging Data Overwrittenโ UUID0x2C19; properties: Read, Notify, Indicate; description: signals that a stored dataset was overwritten. ๐
All characteristics except RAS Features use Client Characteristic Configuration (CCC) descriptors to enable notifications/indications. The typical GATT attribute layout uses 18 handles total. Not me knowing the handle budget like itโs my rent money. ๐ธ
โญ RAS Features
The RAS Features characteristic is a 4-byte bitmask read by the client to discover the serverโs capabilities. Main character energy tbh. ๐
> ACL Data RX: Handle 2048 flags 0x02 dlen 11 #200 [hci0]
ATT: Read Response (0x0b) len 4
Handle: 0x0003
Features: 0x0000000f
Real-time Ranging Data (0x00000001)
Retrieve Lost Ranging Data Segments (0x00000002)
Abort Operation (0x00000004)
Filter Ranging Data (0x00000008)
Feature bits, decoded and cute:
- Bit 0 โ Real-time Ranging Data: the server can stream results via the Real-time Ranging Data characteristic as CS procedures run. ๐ฅ
- Bit 1 โ Retrieve Lost Ranging Data Segments: the client can request retransmission of missing data segments. ๐ซถ
- Bit 2 โ Abort Operation: the client can cancel an in-progress data transfer. โ
- Bit 3 โ Filter Ranging Data: the client can configure which fields are included in ranging data notifications. ๐
๐ฆ RAS Ranging Data Ready
When a CS procedure completes and results are stored, the server notifies the client that data is available for retrieval. Itโs giving โyour order is ready for pickup.โ ๐
> ACL Data RX: Handle 2048 flags 0x02 dlen 9 #250 [hci0]
ATT: Handle Value Notification (0x1b) len 2
Handle: 0x000e
Counter: 42
The counter value identifies the specific ranging dataset. The client uses this counter in subsequent Control Point commands to request or acknowledge the data. Iykyk. ๐ซ
๐ RAS Ranging Data Overwritten
If the serverโs buffer is full and older results are discarded, this characteristic notifies the client:
> ACL Data RX: Handle 2048 flags 0x02 dlen 9 #260 [hci0]
ATT: Handle Value Notification (0x1b) len 2
Handle: 0x0011
Overwritten Count: 38
This tells the client that ranging data with counter value 38 (and possibly earlier) has been overwritten and can no longer be retrieved. Rip to that data, honestly. ๐ชฆ
๐๏ธ RAS Control Point
The RAS Control Point is the command interface for managing ranging data transfers. The client writes commands, and the server responds via indications on the same characteristic. Itโs a whole conversation, bestie. ๐ฌ
Opcodes:
0x00โGet Ranging Dataโ request on-demand transfer of a specific dataset. ๐ฅ0x01โACK Ranging Dataโ acknowledge receipt of a dataset. โ0x02โRetrieve Lost Ranging Data Segmentsโ re-request specific missing segments. ๐0x03โAbort Operationโ cancel an in-progress data transfer. ๐0x04โSet Filterโ configure which data fields are included. ๐๏ธ
Get Ranging Data โ requests transfer of a stored dataset:
< ACL Data TX: Handle 2048 flags 0x00 dlen 10 #270 [hci0]
ATT: Write Command (0x52) len 3
Handle: 0x000b
Opcode: Get Ranging Data (0x00)
Ranging Counter: 0x002a
After this command, the server sends the data as a sequence of notifications on the On-demand Ranging Data characteristic (0x2C16). Slay, data delivery. ๐
ACK Ranging Dataโ tells the server "bestie I got the whole thing ๐" and frees that buffer:
< ACL Data TX: Handle 2048 flags 0x00 dlen 10 #290 [hci0]
ATT: Write Command (0x52) len 3
Handle: 0x000b
Opcode: ACK Ranging Data (0x01)
Ranging Counter: 0x002a
Retrieve Lost Ranging Data Segmentsโ "hey king, some of those segments got dropped, can you resend them ๐" for segments that were missed or corrupted:
< ACL Data TX: Handle 2048 flags 0x00 dlen 12 #295 [hci0]
ATT: Write Command (0x52) len 5
Handle: 0x000b
Opcode: Retrieve Lost Ranging Data Segments (0x02)
Ranging Counter: 0x002a
First Segment Index: 3
Last Segment Index: 5
A Last Segment Index of 0xFF literally means "all remaining segments from the first index onward," no cap. โจ
Abort Operationโ cancels any in-progress data transfer, mid-flight, no notes:
< ACL Data TX: Handle 2048 flags 0x00 dlen 8 #298 [hci0]
ATT: Write Command (0x52) len 1
Handle: 0x000b
Opcode: Abort Operation (0x03)
Set Filterโ configures which fields get included in ranging data notifications:
< ACL Data TX: Handle 2048 flags 0x00 dlen 10 #265 [hci0]
ATT: Write Command (0x52) len 3
Handle: 0x000b
Opcode: Set Filter (0x04)
Filter Configuration: 0x0001
Mode: 1
Filter Bit Mask: 0x0000
The filter configuration is a 16-bit value, and it's splitting duties: bits [1:0] select the filter mode, and bits [15:2] are the field bitmask. Two jobs, one little value, multitasking icon ๐
โจ Ranging Data Format โจ
Both Real-time (0x2C15) and On-demand (0x2C16) Ranging Data characteristics use the same segmented data format, bestie โ think of it as one outfit, two occasions ๐
Large datasets get split across multiple ATT notifications, so you gotta reassemble them like a group chat that got sent in 12 separate messages.
Segmentation Header (1 byte, present in every notification):
> ACL Data RX: Handle 2048 flags 0x02 dlen 30 #275 [hci0]
ATT: Handle Value Notification (0x1b) len 25
Handle: 0x0005
Segmentation Header: 0x01
First Segment: True
Last Segment: False
Segment Index: 0
Ranging Data Body:
Ranging Counter: 0x02a
Configuration ID: 0
Selected TX Power: 12 dBm
Antenna Paths Mask: 0x03
Antenna Path 1 (0x01)
Antenna Path 2 (0x02)
Subevent #0:
Start ACL Connection Event: 1200
Frequency Compensation: 150 (0.01 ppm)
Ranging Done Status: Partial results (0x1)
Subevent Done Status: All results complete (0x0)
Ranging Abort Reason: No abort (0x0)
Subevent Abort Reason: No abort (0x0)
Reference Power Level: -20 dBm
Number of Steps Reported: 8
Remaining Ranging Data Segment: ...
๐ซถ Segmentation header fields ๐ซถ
- First Segment (bit 0) โ
Truefor the first notification of a dataset. Only the first segment contains the Ranging Header, so likeโฆ don't go looking for it in the others. - Last Segment (bit 1) โ
Truefor the final notification. When both bits are set, the entire dataset fits in one notification โ one-and-done, ate and left no crumbs ๐ฅ - Segment Index (bits
[7:2]) โ sequential index for reassembly. Count them up, keep them in order.
๐ธ Ranging Header (4 bytes, only in the first segment) ๐ธ
- Ranging Counter (bits
[11:0]) โ matches the counter from the Data Ready notification; identifies the CS procedure instance. This is the vibe check that ties everything together. - Configuration ID (bits
[15:12]) โ the CS configuration ID (0--3) used for this measurement. - Selected TX Power (1 byte, signed) โ actual transmit power used, in dBm.
- Antenna Paths Mask (1 byte) โ bitmask saying which antenna paths have data: bit 0 = path 1, bit 1 = path 2, etc. Gotta know which antenna was the main character ๐ฆ
๐ Subevent Header (per subevent within the ranging data) ๐
- Start ACL Connection Event โ the ACL connection event counter at which this CS subevent started.
- Frequency Compensation โ measured frequency offset in
0.01 ppmunits (signed 16-bit). - Ranging Done Status โ same codes as HCI subevent results:
0x0= complete,0x1= partial,0xF= aborted. - Subevent Done Status โ
0x0= complete,0xF= aborted. - Ranging/Subevent Abort Reason โ same abort reason codes as HCI:
0x0= no abort,0x1= host/remote request,0x2= channel map / no sync,0x3= scheduling,0xF= unspecified. - Reference Power Level โ RSSI reference in dBm (signed).
- Number of Steps Reported โ count of CS step results that follow. Spoiler: they're coming right up.
The step data following the subevent header uses the same mode-specific format as HCI CS Subevent Result events (Mode 0--3), described in the CS Step Modes section above. No new format, no surprises, same lore ๐ง
Continuation segments contain only the Segmentation Header followed by raw ranging data bytes that get reassembled with the previous segments. That's it. Just vibes and bytes.
โจ RAS Data Transfer State Machine โจ
The RAS data transfer for a single ranging dataset is giving very much "chosen path" energy โ it transitions through these states:
โโโโโโโโโโโโโโโโโ
โ IDLE โ No active data transfer.
โโโโโโโฌโโโโโโโโโโ
โ Server: CS procedure completes (HCI level)
โ Server: notifies Ranging Data Ready (counter=N)
โผ
โโโโโโโโโโโโโโโโโ
โ DATA READY โ Dataset N is buffered on server.
โ (counter=N) โ Client has been notified.
โโโโโโโฌโโโโโโโโโโ
โ Client writes: Get Ranging Data (counter=N)
โ โโ OR โโ (real-time mode: automatic push)
โผ
โโโโโโโโโโโโโโโโโ
โ TRANSFERRING โ Server sends segmented notifications.
โ (counter=N) โ Segment Index increments: 0, 1, 2, ...
โโโโโโโฌโโโโโโโฌโโโ
โ โ Segment lost? Client writes:
โ โ Retrieve Lost Segments (counter=N, first, last)
โ โ Server re-sends missing segments.
โ โ (loops back to TRANSFERRING)
โ โ
โ โ Client writes: Abort Operation
โ โโโโโโโโโโโโโโโโโโโโโโโโ
โ Last Segment received โ
โผ โผ
โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ
โ COMPLETE โ โ ABORTED โ
โ (counter=N) โ โโโโโโโโโฌโโโโโโโโ
โโโโโโโฌโโโโโโโโโโ โ
โ Client writes: โ
โ ACK Ranging Data โ
โ (counter=N) โ
โผ โผ
โโโโโโโโโโโโโโโโโ
โ IDLE โ Server may free buffer for counter=N.
โโโโโโโโโโโโโโโโโ
fr fr though: if the server's buffer fills before the client retrieves data, a Ranging Data Overwritten notification gets sent and the dataset transitions directly from DATA READY to overwritten (lost). Gone. Not coming back. ๐ญ That's not a vibe, that's just the protocol.
โจ Typical RAS Data Flow โจ
A complete RAS session for on-demand data retrieval, step by step, no skips:
- Client discovers RAS (UUID
0x185B) via GATT primary service discovery - Client reads RAS Features (
0x2C14) to learn capabilities - Client enables CCC notifications on:
- RAS Ranging Data Ready (
0x2C18) - RAS Ranging Data Overwritten (
0x2C19) - RAS On-demand Ranging Data (
0x2C16) or RAS Real-time Ranging Data (0x2C15)
- RAS Ranging Data Ready (
- CS procedure runs (HCI level)
- Server notifies Ranging Data Ready with counter=N
- Client writes Control Point: Get Ranging Data (counter=N)
- Server sends On-demand Ranging Data notifications (segmented)
- Client writes Control Point: ACK Ranging Data (counter=N)
For real-time streaming, step 6 is skipped โ the server pushes data on the Real-time Ranging Data characteristic (0x2C15) as each CS subevent completes, using the same segmented format. No asking required, it just shows up. She's proactive like that ๐
And if segments get lost (say, due to ATT MTU limitations or missed notifications), the client uses Retrieve Lost Ranging Data Segments to request retransmission of specific segment indices. That's the recovery arc, and honestly? Iconic.
โจ Combined HCI + GATT Flow โจ
Okay bestie, this next chart is the whole interleaved timeline of HCI CS operations and GATT RAS operations as they appear in a btmon trace. The left column is HCI traffic (< HCI / > HCI), the center shows GATT ATT traffic (< ACL / > ACL), and the right column shows the combined state:
HCI (CS) GATT (RAS) State
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
--- One-time connection setup ---
< LE CS Read Local Supp. Cap. IDLE
> Command Complete โ
< LE CS Read Remote Supp. Cap. โ
> Command Status โ
> LE CS Read Remote Supp. โ
Cap. Complete โ
< LE CS Security Enable โ
> Command Status โ
> LE CS Security Enable โ
Complete โ
< LE CS Set Default Settings โ
> Command Complete โผ
CAPS EXCHANGED
< ATT: Find By Type Value โ
(RAS UUID 0x185B) โ
> ATT: Find By Type Value โ
Response โ
< ATT: Read Request โ
(RAS Features 0x2C14) โ
> ATT: Read Response โ
Features: 0x0000000f โ
< ATT: Write Request โ
(CCC: Ready 0x2C18) โ
> ATT: Write Response โ
< ATT: Write Request โ
(CCC: On-demand 0x2C16) โ
> ATT: Write Response โผ
RAS READY
--- Per-measurement cycle (repeatable) ---
< LE CS Create Config (id=0) โ
> Command Status โ
> LE CS Config Complete โผ
CONFIGURED (id=0)
< LE CS Set Proc. Params (id=0) โ
> Command Complete โผ
PARAMS SET (id=0)
< LE CS Proc. Enable โ
(id=0, enable=1) โ
> Command Status โ
> LE CS Proc. Enable โ
Complete (state=1) โผ
PROCEDURE RUNNING
> LE CS Subevent Result โโ โ
> LE CS Subevent Result โ (measurement data โ
Continue โ streams from โ
> LE CS Subevent Result โ controller) โ
> LE CS Subevent Result โโ โ
โ
> LE CS Subevent Result โ
(Procedure Done=0x00) โผ
PROCEDURE COMPLETE
> ATT: Notification โ
(Data Ready 0x2C18) โ
Counter: N โผ
DATA READY (N)
< ATT: Write Command โ
(Control Point 0x2C17) โ
Get Ranging Data โ
Counter: N โผ
TRANSFERRING (N)
> ATT: Notification โ
(On-demand 0x2C16) โ
Seg[0]: First=T Last=F โ
Ranging Header + data โ
> ATT: Notification โ
(On-demand 0x2C16) โ
Seg[1]: First=F Last=F โ
> ATT: Notification โ
(On-demand 0x2C16) โ
Seg[2]: First=F Last=T โผ
TRANSFER COMPLETE (N)
< ATT: Write Command โ
(Control Point 0x2C17) โ
ACK Ranging Data โ
Counter: N โผ
IDLE (buffer freed)
--- Cleanup (optional) ---
< LE CS Remove Config (id=0)
> Command Status IDLE
๐ซ Real-time Streaming Flow ๐ซ
When the server supports real-time ranging data (Features bit 0) โ yup, the good bit โ results get pushed immediately as CS subevents complete, without waiting for a Get Ranging Data request. It's giving instant gratification, bestie:
HCI (CS) GATT (RAS) State
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
< LE CS Proc. Enable PARAMS SET
(id=0, enable=1)
> LE CS Proc. Enable Complete โผ
PROCEDURE RUNNING
> LE CS Subevent Result โโโ
โ > ATT: Notification
โ (Real-time 0x2C15)
โ Seg[0]: First=T Last=T
โ Ranging Header +
โ subevent data
> LE CS Subevent Result โโโค
โ > ATT: Notification
โ (Real-time 0x2C15)
โ Seg[0]: First=T Last=F
โ > ATT: Notification
โ (Real-time 0x2C15)
โ Seg[1]: First=F Last=T
> LE CS Subevent Result โโโ
> ATT: Notification
(Real-time 0x2C15)
...
> LE CS Subevent Result โ
(Procedure Done=0x00) โผ
PROCEDURE COMPLETE
< ATT: Write Command
ACK Ranging Data
Counter: N โผ
IDLE
In real-time mode, each HCI Subevent Result triggers one or more GATT notifications immediately immediately. The Ranging Counter in the data header increments with each procedure. And if a notification segment gets lost? The client can use Retrieve Lost Segments after the procedure completes โ but only if Features bit 1 is set. iykyk ๐
๐ซถ Lost Segment Recovery Flow ๐ซถ
When segments are missed during transfer, the client requests retransmission:
GATT (RAS) State
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
> ATT: Notification (On-demand 0x2C16) TRANSFERRING
Seg[0]: First=T Last=F โโ received
> ATT: Notification (On-demand 0x2C16)
Seg[1]: First=F Last=F โโ received
โ
Seg[2] โโ LOST (not received) โ
โ
> ATT: Notification (On-demand 0x2C16) โ
Seg[3]: First=F Last=T โโ received โ
โ
(Client detects gap: Seg[2] missing) โผ
INCOMPLETE
< ATT: Write Command (Control Point 0x2C17)
Retrieve Lost Ranging Data Segments
Counter: N
First Segment Index: 2
Last Segment Index: 2 โผ
RECOVERING
> ATT: Notification (On-demand 0x2C16)
Seg[2]: First=F Last=F โโ re-sent โผ
TRANSFER COMPLETE
< ATT: Write Command (Control Point 0x2C17)
ACK Ranging Data
Counter: N โผ
IDLE
The missing Seg[2] gets re-sent, then ACKed, and the state lands back at IDLE โ gap closed, no cap. ๐ฆ
๐ธ RAS Common Issues ๐ธ
- Features read returns
0x00000000โ the server does not support any optional RAS features. Like, none. Only basic on-demand data retrieval via Control Point is available. - Ranging Data Ready not received โ check that CCC notifications are enabled on handle
0x2C18. Also verify the CS procedure actually completed โ because if the procedure was aborted, no Data Ready notification is sent. periodt. - Segmentation reassembly errors โ verify that Segment Index values are sequential and that the First Segment flag is set on the initial notification. A missing segment triggers the Retrieve Lost Segments mechanism if the server supports it (
Features bit 1). - Data Overwritten before retrieval โ the server has limited buffer space, bestie. If the client does not retrieve and ACK data promptly, older datasets are overwritten. The Data Overwritten notification indicates the counter value of the lost data. ๐ญ
- Control Point write rejected โ the connection must be encrypted. RAS Control Point requires write permission with encryption (
WRITE_ENCRYPT). Verify that SMP pairing has completed. ๐
๐ CLASSIC AUDIO PROTOCOL FLOW ๐
okay bestie, so Classic Bluetooth audio is lowkey a whole two-profile situation and you need to know both ๐
- A2DP โ for the high-quality stereo streaming, the good stuff ๐ง
- HFP โ for voice calls, the "can you hear me now" vibes ๐
Both of them run over BR/EDR ACL connections, but they use totally different transport mechanisms for the audio data, and this is the part people get confused about:
- A2DP rides on L2CAP-based AVDTP media channels
- HFP uses SCO/eSCO synchronous connections
โจ A2DP: Advanced Audio Distribution โจ
So A2DP uses AVDTP (Audio/Video Distribution Transport Protocol) sitting on top of L2CAP to negotiate codec parameters and stream that high-quality audio. And btmon? She fully decodes AVDTP signaling, codec capabilities, and AVRCP remote control messages. not a single byte left behind, periodt ๐ฅ
๐ AVDTP Signaling Channel ๐
A2DP kicks off with an L2CAP connection on PSM 25 (0x0019). and here's the iconic little detail โ the first L2CAP connection on this PSM carries AVDTP signaling, and a second connection on that same PSM carries the media transport data. two connections, one PSM, no cap ๐ฆ
< ACL Data TX: Handle 1 flags 0x00 dlen 12
L2CAP: Connection Request (0x02) ident 1 len 4
PSM: 25 (0x0019)
Source CID: 64
> ACL Data RX: Handle 1 flags 0x02 dlen 16
L2CAP: Connection Response (0x03) ident 1 len 8
Destination CID: 64
Source CID: 64
Result: Connection successful (0x0000)
Status: No further information available (0x0000)
Once the L2CAP configuration is done, AVDTP signaling begins on this channel. slay ๐
๐ธ Stream Endpoint Discovery ๐ธ
The initiator goes looking for available Stream Endpoints (SEPs) on the remote device:
< ACL Data TX: Handle 1 flags 0x00 dlen 6
Channel: 64 len 2 [PSM 25 mode Basic (0x00)] {chan 0}
AVDTP: Discover (0x01) Command (0x00) type 0x00 label 0 nosp 0
> ACL Data RX: Handle 1 flags 0x02 dlen 10
Channel: 64 len 6 [PSM 25 mode Basic (0x00)] {chan 0}
AVDTP: Discover (0x01) Response Accept (0x02) type 0x00 label 0 nosp 0
ACP SEID: 1
Media Type: Audio (0x00)
SEP Type: SNK (0x01)
In use: No
Every single SEP has a SEID (Stream Endpoint Identifier), a media type, and a role (Source or Sink). And In use: Yes means the endpoint is already streaming โ she's busy, don't @ her ๐
๐ Codec Capability Discovery ๐
Once the SEIDs are discovered, the initiator goes and queries each endpoint's capabilities:
< ACL Data TX: Handle 1 flags 0x00 dlen 7
Channel: 64 len 3 [PSM 25 mode Basic (0x00)] {chan 0}
AVDTP: Get All Capabilities (0x0c) Command (0x00) type 0x00 label 1 nosp 0
ACP SEID: 1
> ACL Data RX: Handle 1 flags 0x02 dlen 30
Channel: 64 len 26 [PSM 25 mode Basic (0x00)] {chan 0}
AVDTP: Get All Capabilities (0x0c) Response Accept (0x02) type 0x00 label 1 nosp 0
Service Category: Media Transport (0x01)
Service Category: Media Codec (0x07)
Media Type: Audio (0x00)
Media Codec: SBC (0x00)
Frequency: 0xf0
16000
32000
44100
48000
Channel Mode: 0x0f
Mono
Dual Channel
Stereo
Joint Stereo
Block Length: 0xf0
4
8
12
16
Subbands: 0x0c
4
8
Allocation Method: 0x03
SNR
Loudness
Minimum Bitpool: 2
Maximum Bitpool: 53
Service Category: Content Protection (0x04)
Content Protection Type: SCMS-T (0x0002)
Service Category: Delay Reporting (0x08)
Capability responses list all the supported values as bitmasks โ like a menu, not an order. The key service categories, bestie:
- Media Transport (
0x01) โ always present, it means the endpoint supports a media transport channel ๐ซ - Media Codec (
0x07) โ the codec type plus all its supported parameters - Content Protection (
0x04) โ copy protection, either SCMS-T or DTCP - Delay Reporting (
0x08) โ this endpoint supports delay reporting
๐ Supported Codecs ๐
btmon decodes several A2DP codecs, and each one has its own parameter format. let's go through them ๐
SBC (the mandatory A2DP codec, the one everybody has to support):
Media Codec: SBC (0x00)
Frequency: 44100 (0x20)
Channel Mode: Joint Stereo (0x01)
Block Length: 16 (0x10)
Subbands: 8 (0x04)
Allocation Method: Loudness (0x01)
Minimum Bitpool: 2
Maximum Bitpool: 53
AAC (MPEG-2,4):
Media Codec: MPEG-2,4 AAC (0x02)
Object Type: MPEG-4 AAC LC (0x40)
Frequency: 44100 (0x0100)
Channels: 2 (0x04)
Bitrate: 256000bps
VBR: No
aptX (Qualcomm, vendor-specific โ the plot thickens):
Media Codec: Non-A2DP (0xff)
Vendor ID: Qualcomm Technologies International, Ltd. (APT) (0x0000004f)
Vendor Specific Codec ID: aptX (0x0001)
Frequency: 44100 (0x20)
Channel Mode: Stereo (0x02)
aptX HD (Qualcomm again, vendor-specific again):
Media Codec: Non-A2DP (0xff)
Vendor ID: Qualcomm Technologies, Inc. (0x000000d7)
Vendor Specific Codec ID: aptX HD (0x0024)
Frequency: 44100 (0x20)
Channel Mode: Stereo (0x02)
LDAC (Sony, vendor-specific, the audiophile icon):
Media Codec: Non-A2DP (0xff)
Vendor ID: Sony Corporation (0x0000012d)
Vendor Specific Codec ID: LDAC (0x00aa)
Vendor codecs show up as Non-A2DP (0xff) with the decoded Vendor ID and Codec ID. And remember the golden rule: capability responses list all supported values as bitmasks, while configuration responses show the single selected value. menu vs. order, iykyk ๐
๐ฆ Stream Configuration ๐ฆ
Once a codec and its parameters are chosen, the initiator sends Set Configuration:
< ACL Data TX: Handle 1 flags 0x00 dlen 20
Channel: 64 len 16 [PSM 25 mode Basic (0x00)] {chan 0}
AVDTP: Set Configuration (0x03) Command (0x00) type 0x00 label 2 nosp 0
ACP SEID: 1
INT SEID: 1
Service Category: Media Transport (0x01)
Service Category: Media Codec (0x07)
Media Type: Audio (0x00)
Media Codec: SBC (0x00)
Frequency: 44100 (0x20)
Channel Mode: Joint Stereo (0x01)
Block Length: 16 (0x10)
Subbands: 8 (0x04)
Allocation Method: Loudness (0x01)
Minimum Bitpool: 2
Maximum Bitpool: 53
> ACL Data RX: Handle 1 flags 0x02 dlen 6
Channel: 64 len 2 [PSM 25 mode Basic (0x00)] {chan 0}
AVDTP: Set Configuration (0x03) Response Accept (0x02) type 0x00 label 2 nosp 0
Configuration responses show single selected values instead of bitmask lists (told you โจ). Quick key so you don't get lost: ACP SEID is the remote endpoint, INT SEID is the local endpoint. ๐ซถ
And if the configuration gets rejected? it looks like this:
> ACL Data RX: Handle 1 flags 0x02 dlen 8
Channel: 64 len 4 [PSM 25 mode Basic (0x00)] {chan 0}
AVDTP: Set Configuration (0x03) Response Reject (0x03) type 0x00 label 2 nosp 0
Service Category: Media Codec (0x07)
Error code: Unsupported Configuration (0x29)
The common AVDTP error codes, all laid out ๐ญ:
0x01โ Bad Header Format โ malformed AVDTP header0x11โ Bad ACP SEID โ unknown endpoint ID0x12โ SEP In Use โ endpoint already streaming0x13โ SEP Not In Use โ endpoint not configured0x29โ Unsupported Configuration โ requested parameters not supported0x31โ Bad State โ command not valid in current state
โญ Open and Start โญ
After configuration, the stream gets opened and then started:
< ACL Data TX: Handle 1 flags 0x00 dlen 7
Channel: 64 len 3 [PSM 25 mode Basic (0x00)] {chan 0}
AVDTP: Open (0x06) Command (0x00) type 0x00 label 3 nosp 0
ACP SEID: 1
> ACL Data RX: Handle 1 flags 0x02 dlen 6
Channel: 64 len 2 [PSM 25 mode Basic (0x00)] {chan 0}
AVDTP: Open (0x06) Response Accept (0x02) type 0x00 label 3 nosp 0
Once Open succeeds, a second L2CAP connection on PSM 25 gets established for the media transport channel (there's that second connection we mentioned ๐). Then Start actually kicks off the streaming:
< ACL Data TX: Handle 1 flags 0x00 dlen 7
Channel: 64 len 3 [PSM 25 mode Basic (0x00)] {chan 0}
AVDTP: Start (0x07) Command (0x00) type 0x00 label 4 nosp 0
ACP SEID: 1
> ACL Data RX: Handle 1 flags 0x02 dlen 6
Channel: 64 len 2 [PSM 25 mode Basic (0x00)] {chan 0}
AVDTP: Start (0x07) Response Accept (0x02) type 0x00 label 4 nosp 0
๐ Media Data ๐
After Start, the encoded audio flows on the media transport channel โ that's the second L2CAP connection on PSM 25, bestie, keep track ๐
heads up:btmondoes not decode media packet contents by default โ media data is suppressed. You only see the L2CAP channel header when the--show-a2dp-streamfilter is enabled, and even then the payload is shown as a raw hex dump. no pretty decoding for the actual audio, sorry ๐ญ
okay so the AVDTP state machine is literally the main character of any A2DP trace, and here's the vibe check on every state it can be in:
Idleโ triggered by Initial / Abort response. No stream configured, just vibing with nothing set up.Configuredโ triggered by Set Configuration accept. Codec and parameters selected, we're getting somewhere.Openโ triggered by Open accept / Suspend accept. Transport channel ready, not streaming yet. It's giving "almost there."Streamingโ triggered by Start accept. Audio data flowing, the moment we've all been waiting for ๐ฅClosingโ triggered by Close command sent. Tearing down stream, say bye.Abortingโ triggered by Abort command sent. Forced teardown, no cap, it's over.
โจ Suspend, Close, and Abort โจ
Suspend pauses streaming without tearing down the transport โ think "brb" not "goodbye":
AVDTP: Suspend (0x09) Command (0x00) type 0x00 label 5 nosp 0
ACP SEID: 1
AVDTP: Suspend (0x09) Response Accept (0x02) type 0x00 label 5 nosp 0
Close tears down the stream (returns to Idle), like logging off for real:
AVDTP: Close (0x08) Command (0x00) type 0x00 label 6 nosp 0
ACP SEID: 1
AVDTP: Close (0x08) Response Accept (0x02) type 0x00 label 6 nosp 0
Abort forces immediate teardown โ the dramatic exit:
AVDTP: Abort (0x0a) Command (0x00) type 0x00 label 7 nosp 0
ACP SEID: 1
AVDTP: Abort (0x0a) Response Accept (0x02) type 0x00 label 7 nosp 0
๐ Delay Reporting ๐
When the sink supports delay reporting, it sends a Delay Report to inform the source of its rendering delay. It's literally the sink saying "hey bestie, I need a sec":
AVDTP: Delay Report (0x0d) Command (0x00) type 0x00 label 8 nosp 0
ACP SEID: 1
Delay: 15.0ms
AVDTP: Delay Report (0x0d) Response Accept (0x02) type 0x00 label 8 nosp 0
๐ AVRCP Remote Control ๐
AVRCP (Audio/Video Remote Control Profile) uses L2CAP PSM 23 (control) and PSM 27 (browsing). btmon decodes AVCTP framing and AVRCP PDUs โ it's got your back.
Volume control, the moment you turn it up:
< ACL Data TX: Handle 1 flags 0x00 dlen 17
Channel: 66 len 13 [PSM 23 mode Basic (0x00)] {chan 2}
AVCTP Control: Response: type 0x00 label 0 PID 0x110e
AV/C: Accepted: address 0x48 opcode 0x00
Subunit: Panel
Opcode: Vendor Dependent
Company ID: 0x001958
AVRCP: SetAbsoluteVolume pt Single len 0x0001
Volume: 50.39% (64/127)
Playback status, so you know what's playing:
AVRCP: GetPlayStatus pt Single len 0x0009
SongLength: 0x00038270 (230000 milliseconds)
SongPosition: 0x00000000 (0 milliseconds)
PlayStatus: 0x01 (PLAYING)
Track metadata (GetElementAttributes response) โ the song and artist reveal:
AVRCP: GetElementAttributes pt Single len 0x0050
AttributeCount: 0x02
Attribute: 0x00000001 (Title)
CharsetID: 0x006a (UTF-8)
AttributeValueLength: 0x000c
AttributeValue: My Song Name
Attribute: 0x00000002 (Artist)
CharsetID: 0x006a (UTF-8)
AttributeValueLength: 0x000b
AttributeValue: The Artist
Passthrough commands (play, pause, skip) โ the actual button presses:
AVCTP Control: Command: type 0x00 label 1 PID 0x110e
AV/C: Control: address 0x48 opcode 0x7c
Subunit: Panel
Opcode: Passthrough
Operation: 0x44 (PLAY Pressed)
Length: 0x00
Event notifications (volume change, track change) โ the "notify me when this happens" energy:
AVRCP: RegisterNotification pt Single len 0x0005
EventID: 0x0d (EVENT_VOLUME_CHANGED)
Volume: 50.39% (64/127)
๐ฆ Automating A2DP Analysis ๐ฆ
Identify A2DP activity:
grep -n "AVDTP:\|Media Codec:" output.txt
Track AVDTP state transitions:
grep -n "AVDTP:.*Command\|AVDTP:.*Response" output.txt
Check codec negotiation (what was selected):
grep -n "Set Configuration\|Media Codec:" output.txt
Verify stream is flowing โ look for Start accept followed by media data:
grep -n "AVDTP: Start\|PSM 25.*chan 1" output.txt
Detect codec mismatch โ look for Set Configuration reject:
grep -n "Response Reject\|Error code:" output.txt
Track AVRCP volume and playback:
grep -n "SetAbsoluteVolume\|Volume:\|GetPlayStatus\|PlayStatus:" output.txt
Full A2DP diagnosis pattern โ follow the steps, bestie:
- Find L2CAP Connection Request for PSM 25 โ confirms AVDTP channel setup
- Find Discover response โ shows available endpoints and whether any are already in use
- Find Get All Capabilities response โ verify codec support overlap
- Find Set Configuration โ verify agreed codec and parameters
- Find Open and Start โ confirm stream setup succeeded
- Find second L2CAP connection on PSM 25 โ media transport channel
- On failure, check for Response Reject with error code
- If audio cuts out, look for Suspend or Close, or check for ACL disconnection
๐ธ HFP: Hands-Free Profile ๐ธ
HFP carries voice audio over SCO/eSCO connections with call control over RFCOMM. btmon decodes RFCOMM framing and SCO/eSCO connection setup. AT commands (the HFP control protocol) appear as raw hex dumps within RFCOMM data frames โ btmon does not parse AT command syntax. So you'll see the hex, but no fancy AT decoding. That's just how it is, no cap.
๐ SDP Discovery ๐
HFP uses SDP to discover the remote device's Hands-Free or Audio Gateway service record and its RFCOMM channel number:
< ACL Data TX: Handle 1 flags 0x00 dlen 25
Channel: 64 len 21 [PSM 1 mode Basic (0x00)] {chan 0}
SDP: Service Search Attribute Request (0x06) tid 1 len 16
Search pattern: [len 5]
Sequence (6) with 3 byte(s) [8 extra bits] len 5
UUID (3) with 2 byte(s) [0 extra bits] len 3
Handsfree Audio Gateway (0x111f)
Max record count: 65535
Attribute list: [len 5]
Sequence (6) with 3 byte(s) [8 extra bits] len 5
Unsigned Integer (1) with 4 byte(s) [0 extra bits] len 5
0x0000ffff
Continuation state: 0
The response contains the service record with the RFCOMM channel โ this is the good part:
> ACL Data RX: Handle 1 flags 0x02 dlen 89
Channel: 64 len 85 [PSM 1 mode Basic (0x00)] {chan 0}
SDP: Service Search Attribute Response (0x07) tid 1 len 80
Attribute bytes: 77
Attribute list: [len 75] {position 0}
Attribute: Service Class ID List (0x0001) [len 2]
Handsfree Audio Gateway (0x111f)
Attribute: Protocol Descriptor List (0x0004) [len 2]
L2CAP (0x0100)
RFCOMM (0x0003)
Channel: 1
Attribute: Bluetooth Profile Descriptor List (0x0009) [len 2]
Handsfree (0x111e)
Version: 0x0108
Continuation state: 0
Key fields to extract โ write these down, bestie:
- Service Class โ Handsfree (
0x111e) for HF role, Handsfree Audio Gateway (0x111f) for AG role - RFCOMM Channel โ The channel number under RFCOMM (
0x0003) in the Protocol Descriptor List (e.g., channel 1) - Profile Version โ Under the Bluetooth Profile Descriptor List (e.g.,
0x0108= HFP 1.8,0x0109= HFP 1.9)
๐ง RFCOMM Connection Setup ๐ง
RFCOMM runs over L2CAP PSM 3. The connection proceeds in stages: multiplexer session on DLCI 0, parameter negotiation, then the data channel on the target DLCI. It's a whole process, but we love a structured queen.
L2CAP connection for RFCOMM:
< ACL Data TX: Handle 1 flags 0x00 dlen 12
L2CAP: Connection Request (0x02) ident 2 len 4
PSM: 3 (0x0003)
Source CID: 65
> ACL Data RX: Handle 1 flags 0x02 dlen 16
L2CAP: Connection Response (0x03) ident 2 len 8
Destination CID: 65
Source CID: 65
Result: Connection successful (0x0000)
Status: No further information available (0x0000)
SABM/UA on DLCI 0 (multiplexer session) โ the handshake:
< ACL Data TX: Handle 1 flags 0x00 dlen 12
Channel: 65 len 4 [PSM 3 mode Basic (0x00)] {chan 1}
RFCOMM: Set Async Balance Mode (SABM) (0x2f)
Address: 0x03 cr 1 dlci 0x00
Control: 0x3f poll/final 1
Length: 0
FCS: 0x1c
> ACL Data RX: Handle 1 flags 0x02 dlen 12
Channel: 65 len 4 [PSM 3 mode Basic (0x00)] {chan 1}
RFCOMM: Unnumbered Ack (UA) (0x63)
Address: 0x03 cr 1 dlci 0x00
Control: 0x73 poll/final 1
Length: 0
FCS: 0xd7
Parameter Negotiation (MCC on DLCI 0):
Channel: 65 len 14 [PSM 3 mode Basic (0x00)] {chan 1}
RFCOMM: Unnumbered Info with Header Check (UIH) (0xef)
Address: 0x03 cr 1 dlci 0x00
Control: 0xef poll/final 0
Length: 10
FCS: 0x70
MCC Message type: DLC Parameter Negotiation CMD (0x20)
Length: 8
dlci 2 frame_type 0 credit_flow 15 pri 7
ack_timer 0 frame_size 127 max_retrans 0 credits 7
The DLCI in the PN command identifies the target channel. For RFCOMM channel N, DLCI = N * 2 (or N * 2 + 1 depending on the initiator role). Math, but make it fashion.
SABM/UA on target DLCI (data channel) โ the final handshake before data flows:
Channel: 65 len 4 [PSM 3 mode Basic (0x00)] {chan 1}
RFCOMM: Set Async Balance Mode (SABM) (0x2f)
Address: 0x0b cr 1 dlci 0x02
Control: 0x3f poll/final 1
Length: 0
FCS: 0x59
okay so the next thing you'll clock in the RFCOMM parade is the Modem Status Command, and honestly it's giving "just checking you're still alive" energy โ it's the frame that signals readiness ๐ซถ
Modem Status Command (signals readiness) ๐
Channel: 65 len 8 [PSM 3 mode Basic (0x00)] {chan 1}
RFCOMM: Unnumbered Info with Header Check (UIH) (0xef)
Address: 0x03 cr 1 dlci 0x00
Control: 0xef poll/final 0
Length: 4
FCS: 0x70
MCC Message type: Modem Status Command CMD (0x38)
Length: 2
dlci 2
fc 0 rtc 1 rtr 1 ic 0 dv 1
like, fc 0 rtc 1 rtr 1 ic 0 dv 1 โ that's the whole vibe check in one line, no notes ๐
AT Command Exchange (SLC Setup) ๐
HFP uses AT commands over RFCOMM for call control and feature negotiation โ yes bestie, your headset is literally having a text conversation with your phone ๐ญ btmon shows these as RFCOMM UIH frames with the AT command text visible right there in the hex dump.
AT command chillin' inside an RFCOMM UIH frame:
Channel: 65 len 21 [PSM 3 mode Basic (0x00)] {chan 1}
RFCOMM: Unnumbered Info with Header Check (UIH) (0xef)
Address: 0x09 cr 0 dlci 0x02
Control: 0xff poll/final 1
Length: 14
FCS: 0x86
Credits: 1
41 54 2b 42 52 53 46 3d 32 30 35 35 0d AT+BRSF=2055.
The AT command text is readable in the ASCII column of the hex dump โ that's the right side, bestie, it's literally right there being all legible ๐
The Service Level Connection (SLC) setup sequence is where the two sides exchange their features and capabilities, and it goes a lil something like this:
AT+BRSF=<features>/+BRSF:<features>โ the supported features bitmask exchange โจAT+BAC=1,2โ available codecs (only if codec negotiation is supported). Codec IDs:1= CVSD,2= mSBC,3= LC3-SWBAT+CIND=?/+CIND:(...)โ indicator mapping query ๐AT+CIND?/+CIND:valuesโ current indicator valuesAT+CMER=3,0,0,1โ turn on indicator status reporting ๐ขAT+CHLD=?/+CHLD:(0,1,2,3,4)โ three-way calling support
Key HFP feature bits (from AT+BRSF) โ memorise these, they're iconic:
- Bit 0 โ HF: EC/NR ยท AG: Three-way calling
- Bit 1 โ HF: Three-way calling ยท AG: EC/NR
- Bit 2 โ HF: CLI presentation ยท AG: Voice recognition
- Bit 3 โ HF: Voice recognition ยท AG: In-band ring tone
- Bit 4 โ HF: Remote volume ยท AG: Voice tag
- Bit 5 โ HF: Enhanced call status ยท AG: Reject call
- Bit 6 โ HF: Enhanced call control ยท AG: Enhanced call status
- Bit 7 โ HF: Codec negotiation ยท AG: Enhanced call control
- Bit 8 โ HF: HF indicators ยท AG: Extended error codes
- Bit 9 โ HF: eSCO S4 (T2) ยท AG: Codec negotiation
- Bit 11 โ HF: (nothing listed) ยท AG: eSCO S4 (T2)
Codec Connection Setup ๐ฆ
When both sides support codec negotiation โ that's feature bit 7 on the HF and bit 9 on the AG โ the AG picks a codec before establishing the audio link. Fancy, we know ๐
This shows up as AT commands inside RFCOMM UIH frames:
AG -> HF: +BCS:2 (select mSBC)
HF -> AG: AT+BCS=2 (confirm mSBC)
AG -> HF: OK
HFP codec IDs (the ones used in AT+BAC and AT+BCS):
1โCVSDโ Narrow band (8 kHz), mandatory (so it's not optional, it's the baseline, periodt)2โmSBCโ Wide band speech (16 kHz)3โLC3-SWBโ Super wide band (32 kHz), HFP 1.9+
โ ๏ธ These HFP-level codec IDs differ from HCI codec IDs โ do not mix them up or you will be sad ๐ญ
Voice Setting ๐ธ
Before SCO/eSCO setup, the host configures the voice setting. For CVSD, the air coding format is CVSD; for mSBC or LC3-SWB, it must be set to Transparent Data โ that part is not optional, bestie, it's the rule:
< HCI Command: Write Voice Setting (0x0c|0x0026) plen 2
Setting: 0x0063
Input Coding: Linear
Input Data Format: 2's complement
Input Sample Size: 16-bit
# of bits padding at MSB: 0
Air Coding Format: Transparent Data
And for CVSD, it looks like this instead:
< HCI Command: Write Voice Setting (0x0c|0x0026) plen 2
Setting: 0x0060
Input Coding: Linear
Input Data Format: 2's complement
Input Sample Size: 16-bit
# of bits padding at MSB: 0
Air Coding Format: CVSD
so like, 0x0063 vs 0x0060 โ one nibble, whole different codec, iykyk ๐
SCO/eSCO Connection Setup ๐ฅ
After codec negotiation wraps up, the host establishes a synchronous connection for the voice audio. That's the moment the actual talking happens ๐ค
Setup Synchronous Connection (basic version):
< HCI Command: Setup Synchronous Connection (0x01|0x0028) plen 17
Handle: 1
Transmit bandwidth: 8000
Receive bandwidth: 8000
Max latency: 13
Setting: 0x0063
Input Coding: Linear
Input Data Format: 2's complement
Input Sample Size: 16-bit
# of bits padding at MSB: 0
Air Coding Format: Transparent Data
Retransmission effort: Optimize for link quality (0x02)
Packet type: 0x0008
EV3 may be used
Enhanced Setup Synchronous Connection (this one's codec-aware, main character energy ๐):
< HCI Command: Enhanced Setup Synchronous Connection (0x01|0x003d) plen 59
Handle: 1
Transmit bandwidth: 8000
Receive bandwidth: 8000
Transmit Coding Format:
Codec: mSBC (0x05)
Receive Coding Format:
Codec: mSBC (0x05)
Transmit Codec Frame Size: 60
Receive Codec Frame Size: 60
Input Coding Format:
Codec: mSBC (0x05)
Output Coding Format:
Codec: mSBC (0x05)
Input Coded Data Size: 16
Output Coded Data Size: 16
Input PCM Data Format: 2's complement
Output PCM Data Format: 2's complement
Input PCM Sample Payload MSB Position: 0
Output PCM Sample Payload MSB Position: 0
Input Data Path: HCI
Output Data Path: HCI
Input Transport Unit Size: 60
Output Transport Unit Size: 60
Max latency: 13
Packet type: 0x0008
EV3 may be used
Retransmission effort: Optimize for link quality (0x02)
like observe the plen 17 vs plen 59 โ the enhanced one brought receipts and a whole suitcase of codec info ๐งณโจ and we're still not done, so stay tuned bestie ๐
Okay so HCI codec IDs โ here's the lil cheat sheet of what btmon actually prints and what each one is for:
0x02โ CVSD โ narrow band voice0x03โ Transparent โ transparent data mode0x04โ Linear PCM โ uncompressed PCM input/output0x05โ mSBC โ wide band speech0x06โ LC3 โ super wide band (LC3-SWB)
Synchronous Connection Complete (result), bestie โ this is what the payoff looks like:
> HCI Event: Synchronous Connection Complete (0x2c) plen 17
Status: Success (0x00)
Handle: 257
Address: 11:22:33:44:55:66 (OUI 11-22-33)
Link type: eSCO (0x02)
Transmission interval: 0x0c
Retransmission window: 0x06
RX packet length: 60
TX packet length: 60
Air mode: Transparent (0x03)
Key fields, the ones you actually gotta clock:
- Link type โ
SCO(0x00) for legacy,eSCO(0x02) for enhanced (the one mSBC and LC3-SWB ride on) - Air mode โ
CVSD(0x02) for narrow band,Transparent(0x03) for mSBC or LC3-SWB - RX/TX packet length โ 60 bytes, typical for mSBC T2 settings
SCO Data Packets
Once the synchronous connection is up, the voice data flows as SCO/eSCO data packets, like so:
> BR-ESCO: Handle 257 flags 0x00 dlen 60
< BR-ESCO: Handle 257 flags 0x00 dlen 60
btmon labels these bad boys based on whatever connection type got established in Synchronous Connection Complete:
- BR-SCO โ legacy SCO connection
- BR-ESCO โ enhanced SCO connection (mSBC, LC3-SWB, or eSCO CVSD)
Heads up: SCO data payload is not displayed by default. You need the --show-sco-data filter to actually see the hex dump of that sweet, sweet voice data.
Codec-Specific Connection Summary
CVSD (narrow band), the OG:
- HFP codec ID:
1 - HCI codec: CVSD (
0x02) - Voice setting:
0x0060(Air Coding Format: CVSD) - Air mode: CVSD (
0x02) - Link type: SCO or eSCO
- Typical packet size: 48 bytes (HV3) or 60 bytes (EV3)
mSBC (wide band speech), the glow-up:
- HFP codec ID:
2 - HCI codec: mSBC (
0x05) - Voice setting:
0x0063(Air Coding Format: Transparent Data) - Air mode: Transparent (
0x03) - Link type: eSCO
- Typical packet size: 60 bytes (EV3, T2 settings)
LC3-SWB (super wide band), the main character:
- HFP codec ID:
3 - HCI codec: LC3 (
0x06) - Voice setting:
0x0063(Air Coding Format: Transparent Data) - Air mode: Transparent (
0x03) - Link type: eSCO
- Note:
btmondisplaysLC3, notLC3-SWB
Automating HFP Analysis
Identify HFP activity โ you're looking for RFCOMM on PSM 3:
grep -n "PSM: 3\|RFCOMM:" output.txt
Read the AT commands โ search the hex dump ASCII for AT command patterns:
grep -n "AT+B\|AT+C\|+BRSF\|+CIND\|+CHLD\|+BCS" output.txt
Check codec negotiation โ look for BCS (Bluetooth Codec Selection):
grep -n "+BCS\|AT+BAC\|AT+BCS" output.txt
Verify the SCO/eSCO setup:
grep -n "Setup Synchronous\|Enhanced Setup Synchronous\|Synchronous Connection Complete\|Write Voice Setting" output.txt
Check the voice codec โ confirm air mode and coding format:
grep -n "Air mode:\|Air Coding Format:\|Codec:" output.txt
Detect SCO failures โ check the Synchronous Connection Complete status:
grep -n "Synchronous Connection Complete" output.txt
Then peek at the next line for Status:. Common failures, so you know what you're looking at:
- Connection Rejected due to Limited Resources (
0x0d) โ the controller can't allocate bandwidth - SCO Offset Rejected (
0x2b) โ timing parameters got rejected - SCO Interval Rejected (
0x2c) โ interval parameters got rejected
Track call state โ look for CIEV indicator updates:
grep -n "+CIEV\|AT+CHUP\|ATD\|ATA\|AT+CLCC\|RING" output.txt
Full HFP diagnosis pattern, the whole ritual, in order:
- Find the SDP query for UUID
0x111e/0x111fโ that confirms HFP discovery - Find the L2CAP Connection Request for PSM 3 โ RFCOMM channel setup
- Find RFCOMM SABM/UA on DLCI 0 then the target DLCI โ that's the multiplexer and the data channel
- Find
AT+BRSFin the hex dumps โ feature exchange, check that codec negotiation bit - Find
AT+BACin the hex dumps โ available codecs reported - Find
+BCS/AT+BCSin the hex dumps โ the codec selected for audio - Find Write Voice Setting โ verify the air coding format matches the codec
- Find Setup Synchronous Connection or the Enhanced variant โ SCO setup
- Find Synchronous Connection Complete โ check Status, Link type, Air mode
- Find BR-SCO or BR-ESCO packets โ voice data flowing ๐
๐ธ ADVERTISING AND SCANNING ๐ธ
okay so this is the part where btmon is out here doing the most and decoding advertising data structures automatically, no cap ๐
. Advertising and scan response data shows up in HCI LE advertising report events and also in advertising command parameters โ that's the vibe. Let's get into it ๐
๐ Advertising Reports
When the controller reports received advertisements, it looks like this:
> HCI Event: LE Meta Event (0x3e) plen 43 #120 [hci0] 0.500003
LE Extended Advertising Report (0x0d)
Event type: 0x0013
Props: 0x0013
Connectable
Scannable
Complete
Address type: Random (0x01)
Address: 00:11:22:33:44:55
Primary PHY: LE 1M
Secondary PHY: LE 2M
SID: 0x01
TX power: 0 dBm
RSSI: -55 dBm (0xc9)
Data length: 18
The advertising data (AD) structures tucked inside the report get decoded as typed fields, bestie โจ.
Common AD types btmon decodes โ the whole lineup:
0x01โ Flags โ shows up likeFlags: 0x06 withdecoded bits (LE General Discoverable, BR/EDR Not Supported)0x02/0x03โ Incomplete/Complete 16-bit UUIDs โ shows up like16-bit Service UUIDs (complete): 2 entriesfollowed by the UUID list0x06/0x07โ Incomplete/Complete 128-bit UUIDs โ shows up like128-bit Service UUIDs (complete): 1 entry0x08/0x09โ Shortened/Complete Local Name โ shows up likeName (complete): MyDevice0x0aโ TX Power Level โ shows up likeTX power: 4 dBm0x16โ Service Data (16-bit UUID) โ shows up likeService Data (UUID 0x184e): ...with protocol-specific decoding0xffโ Manufacturer Specific Data โ shows up likeCompany: Apple, Inc. (76)followed by hex data
A typical advertising report, serving looks ๐ซถ:
> HCI Event: LE Meta Event (0x3e) plen 38 #120 [hci0] 0.500003
LE Extended Advertising Report (0x0d)
Address: 00:11:22:33:44:55
RSSI: -62 dBm (0xc2)
Flags: 0x06
LE General Discoverable Mode
BR/EDR Not Supported
Name (complete): LE-Audio-Left
16-bit Service UUIDs (complete): 3 entries
Published Audio Capabilities (0x1850)
Audio Stream Control (0x184e)
Common Audio (0x1853)
Service Data (UUID 0x1852): 01a2b3
Appearance: Earbud (0x0941)
๐ฆ Extended Advertising
Modern controllers use extended advertising commands and events โ that's just the era we're in, periodt. The setup sequence in btmon goes:
< HCI Command: LE Set Extended Adv Parameters (0x08|0x0036) plen 25 #50 [hci0] 0.100003
Handle: 0x01
Properties: 0x0000
Min advertising interval: 160.000 msec (0x0100)
Max advertising interval: 160.000 msec (0x0100)
Channel map: 37, 38, 39 (0x07)
Own address type: Random (0x01)
Peer address type: Public (0x00)
PHY: LE 1M, LE 2M
SID: 0x01
TX power: 7 dBm
< HCI Command: LE Set Extended Adv Data (0x08|0x0037) plen 35 #52 [hci0] 0.101003
Handle: 0x01
Operation: Complete extended advertising data (0x01)
Fragment preference: No fragmentation (0x01)
๐ Periodic Advertising (LE Audio)
LE Audio broadcast sources use periodic advertising to transmit BASE announcements containing codec configuration โ iconic behavior ngl ๐
> HCI Event: LE Meta Event (0x3e) plen 80 #200 [hci0] 0.500003
LE Periodic Advertising Report (0x0f)
Sync handle: 0x0001
TX power: 0 dBm
RSSI: -45 dBm
CTE Type: No CTE (0xff)
Data status: Complete (0x00)
Data length: 60
Service Data: Basic Audio Announcement (0x1851)
Presentation Delay: 40000 us
Number of Subgroups: 1
Codec: LC3 (0x06)
Sampling Frequency: 48000 Hz
Frame Duration: 10 ms
Frame Length: 120
โญ Automating Advertising Analysis
Find all advertising reports (devices seen) โ the "who's out there" check:
grep -n "Advertising Report\|Address:.*RSSI:" output.txt
Extract device names โ get those name tags ๐ท๏ธ:
grep -n "Name (complete):\|Name (short):" output.txt
Find LE Audio devices (by service UUIDs in advertising):
grep -n "Audio Stream Control\|Published Audio Capabilities\|Common Audio\|Basic Audio Announcement\|Broadcast Audio" output.txt
Track advertising setup (local device configuring advertising):
grep -n "Set Extended Adv\|Set Advertising\|Set Scan Response\|Adv Enable" output.txt
Find periodic advertising (broadcast audio):
grep -n "Periodic Advertising\|PA Sync\|PA Report\|Basic Audio Announcement\|Broadcast.*Announcement" output.txt
Identify devices by appearance โ the aesthetic check ๐ :
grep -n "Appearance:" output.txt
๐ฅ MGMT PROTOCOL FLOWS ๐ฅ
The Management Interface (MGMT) is the structured command/event protocol hanging out between userspace (typically bluetoothd) and the kernel Bluetooth subsystem, fr fr. In btmon output, MGMT traffic is prefixed with @ โ so cute, so identifiable โจ โ and it gives you insight into adapter configuration, device discovery, pairing, and connection management at a higher level than raw HCI. This section covers common MGMT protocol flows as seen through btmon.
For MGMT output format basics (line anatomy, Open/Close, parameters, events), see the "Management Traffic" subsection in READING THE OUTPUT above. ๐
๐ Initialization and Version Handshake
When bluetoothd starts, it opens a management socket and queries the kernel for protocol version and supported features. This handshake must succeed before any controller operations โ non-negotiable, bestie:
@ MGMT Open: bluetoothd (privileged) version 1.23 {0x0001} 12:34:49.881936
@ MGMT Command: Read Management Ver.. (0x0001) plen 0 {0x0001} 12:34:49.882003
@ MGMT Event: Command Complete (0x0001) plen 6 {0x0001} 12:34:49.882010
Read Management Version Information (0x0001) plen 3
Status: Success (0x00)
Version: 1.23
@ MGMT Command: Read Management Sup.. (0x0002) plen 0 {0x0001} 12:34:49.882050
@ MGMT Event: Command Complete (0x0001) plen 58 {0x0001} 12:34:49.882055
Read Supported Commands (0x0002) plen 55
Status: Success (0x00)
Num of commands: 120
Num of events: 38
After the version check, bluetoothd reads the controller list and queries each controller's information:
@ MGMT Command: Read Controller Index List (0x0003) plen 0 {0x0001} 12:34:49.882100
@ MGMT Event: Command Complete (0x0001) plen 7 {0x0001} 12:34:49.882105
Read Controller Index List (0x0003) plen 4
Status: Success (0x00)
Num controllers: 1
Controller: hci0
@ MGMT Command: Read Controller Inf.. (0x0004) plen 0 {0x0001} [hci0] 12:34:49.882200
@ MGMT Event: Command Complete (0x0001) plen 283 {0x0001} [hci0] 12:34:49.882210
Read Controller Information (0x0004) plen 280
Status: Success (0x00)
Address: 00:11:22:33:44:55
Bluetooth version: 5.4
Manufacturer: Intel (2)
Supported settings: 0x003effff
Current settings: 0x00000080
Key things to check in the initialization flow โ the checklist, bestie ๐:
- MGMT Open โ Confirms
bluetoothdconnected. If it's missing, the daemon did not start or crashed before reaching MGMT setup. ๐ญ - Version mismatch โ If
bluetoothdexpects a newer MGMT version than the kernel provides, some features may be unavailable. - Num controllers: 0 โ No Bluetooth hardware detected. Check
dmesgfor driver issues. - Current settings โ The bitmask shows what is currently enabled on the controller (Powered, LE, BR/EDR, SSP, etc.).
๐ Adapter Configuration
After reading controller info, bluetoothd configures the adapter with a series of MGMT commands before powering it on. The exact sequence depends on main.conf settings and adapter capabilities โ iykyk, it's not one-size-fits-all ๐
.
Loading stored keys:
@ MGMT Command: Load Link Keys (0x0012) plen 3 {0x0001} [hci0] 12:34:50.001200
Debug keys: Disabled (0x00)
Key count: 0
@ MGMT Event: Command Complete (0x0001) plen 4 {0x0001} [hci0] 12:34:50.001220
Load Link Keys (0x0012) plen 1
Status: Success (0x00)
okay so the key-loading bits look like this:
@ MGMT Command: Load Long Term Keys (0x0013) plen 2 {0x0001} [hci0] 12:34:50.001300
Key count: 0
@ MGMT Event: Command Complete (0x0001) plen 4 {0x0001} [hci0] 12:34:50.001315
Load Long Term Keys (0x0013) plen 1
Status: Success (0x00)
@ MGMT Command: Load Identity Resolving Keys (0x0030) plen 2 {0x0001} [hci0] 12:34:50.001400
Key count: 0
@ MGMT Event: Command Complete (0x0001) plen 4 {0x0001} [hci0] 12:34:50.001415
Load Identity Resolving Keys (0x0030) plen 1
Status: Success (0x00)
When bonded devices exist, the Key count will be non-zero and btmon will list each key entry. A large number of keys can indicate many paired devices; this is normal. Itโs giving โpopular adapter,โ not โsomethingโs wrong.โ ๐
Setting adapter properties:
@ MGMT Command: Set Secure Connections (0x002d) plen 1 {0x0001} [hci0] 12:34:50.002100
Secure connections: Enabled (0x01)
@ MGMT Event: Command Complete (0x0001) plen 7 {0x0001} [hci0] 12:34:50.002120
Set Secure Connections (0x002d) plen 4
Status: Success (0x00)
Current settings: 0x004e0a81
@ MGMT Command: Set Bondable (0x0009) plen 1 {0x0001} [hci0] 12:34:50.002200
Bondable: Enabled (0x01)
@ MGMT Event: Command Complete (0x0001) plen 7 {0x0001} [hci0] 12:34:50.002220
Set Bondable (0x0009) plen 4
Status: Success (0x00)
Current settings: 0x004e0a91
Powering on:
@ MGMT Command: Set Powered (0x0005) plen 1 {0x0001} [hci0] 12:35:04.033564
Powered: Enabled (0x01)
@ MGMT Event: Command Complete (0x0001) plen 7 {0x0001} [hci0] 12:35:04.114789
Set Powered (0x0005) plen 4
Status: Success (0x00)
Current settings: 0x004e0ac1
Between the Set Powered command and its response, btmon will show the HCI commands the kernel sends to initialize the radio (see HCI INITIALIZATION SEQUENCE). Thatโs the glow-up montage before the adapter is officially awake. โจ
๐ Discovery
Device discovery is initiated through MGMT rather than raw HCI commands. bluetoothd (or btmgmt) sends a Start Discovery command specifying which transport types to scan.
Starting discovery:
@ MGMT Command: Start Discovery (0x0023) plen 1 {0x0001} [hci0] 12:36:00.100200
Address type: 0x07
BR/EDR
LE Public
LE Random
@ MGMT Event: Command Complete (0x0001) plen 5 {0x0001} [hci0] 12:36:00.100500
Start Discovery (0x0023) plen 2
Status: Success (0x00)
Address type: 0x07
After this, btmon shows the HCI-level scan commands the kernel issues (LE Set Scan Parameters, LE Set Scan Enable, and/or Inquiry for BR/EDR). Discovered devices appear as MGMT events:
@ MGMT Event: Device Found (0x0012) plen 38 {0x0001} [hci0] 12:36:00.250003
LE Address: AA:BB:CC:DD:EE:FF (Random)
RSSI: -62
Flags: 0x0000
EIR Data:
Name (complete): My Device
TX power: 0
Service discovery uses a filtered variant:
@ MGMT Command: Start Service Discovery (0x003a) plen 19 {0x0001} [hci0] 12:36:10.100200
Address type: 0x06
LE Public
LE Random
RSSI threshold: -127
UUIDs: 1
UUID: Heart Rate (0x180d)
@ MGMT Event: Command Complete (0x0001) plen 5 {0x0001} [hci0] 12:36:10.100500
Start Service Discovery (0x003a) plen 2
Status: Success (0x00)
Stopping discovery:
@ MGMT Command: Stop Discovery (0x0024) plen 1 {0x0001} [hci0] 12:36:15.200100
Address type: 0x07
@ MGMT Event: Command Complete (0x0001) plen 5 {0x0001} [hci0] 12:36:15.200400
Stop Discovery (0x0024) plen 2
Status: Success (0x00)
@ MGMT Event: Discovering (0x0013) plen 2 {0x0001} [hci0] 12:36:15.200500
Address type: 0x07
Discovery: Disabled (0x00)
Discovery problems to look for โ the red flags bestie, do not ignore them ๐ฉ:
Status: Busy (0x0a)onStart Discoveryโ Another discovery session is already active.Status: Not Powered (0x0f)โ The adapter is not powered on.Status: RFKilled (0x12)โ The radio is blocked byrfkill.- No
Device Foundevents โ The remote device may not be advertising, or address type filtering may be wrong.
๐ Pairing and Bonding via MGMT
MGMT provides a high-level pairing interface. The host sends a Pair Device command; the kernel coordinates SMP or SSP as appropriate and reports the result. Itโs literally the matchmaker layer, iykyk. ๐ซถ
Initiating pairing:
@ MGMT Command: Pair Device (0x0019) plen 8 {0x0001} [hci0] 12:37:00.500200
LE Address: AA:BB:CC:DD:EE:FF (Random)
Capability: KeyboardDisplay (0x04)
After this command, btmon shows the underlying SMP or SSP exchange (see SMP PAIRING FLOW and HCI INITIALIZATION SEQUENCE for details). User interaction events may appear:
@ MGMT Event: User Confirmation Request (0x000f) plen 12 {0x0001} [hci0] 12:37:01.200100
LE Address: AA:BB:CC:DD:EE:FF (Random)
Value: 123456
@ MGMT Command: User Confirmation Reply (0x001e) plen 6 {0x0001} [hci0] 12:37:03.800200
LE Address: AA:BB:CC:DD:EE:FF (Random)
@ MGMT Event: Command Complete (0x0001) plen 10 {0x0001} [hci0] 12:37:03.800350
User Confirmation Reply (0x001e) plen 7
Status: Success (0x00)
Or for passkey entry:
@ MGMT Event: User Passkey Request (0x0010) plen 6 {0x0001} [hci0] 12:37:01.200100
LE Address: AA:BB:CC:DD:EE:FF (Random)
@ MGMT Command: User Passkey Reply (0x0020) plen 10 {0x0001} [hci0] 12:37:05.100200
LE Address: AA:BB:CC:DD:EE:FF (Random)
Passkey: 123456
Pairing success โ we love a happy ending โจ:
@ MGMT Event: New Long Term Key (0x000a) plen 37 {0x0001} [hci0] 12:37:06.100200
Store hint: Yes (0x01)
LE Address: AA:BB:CC:DD:EE:FF (Random)
Key type: Authenticated P-256 (0x03)
Central: 0x00
Encryption size: 16
@ MGMT Event: New Identity Resolving Key (0x0018) plen 30 {0x0001} [hci0] 12:37:06.100300
Store hint: Yes (0x01)
Random address: AA:BB:CC:DD:EE:FF
LE Address: 11:22:33:44:55:66 (Public)
Key: 00112233445566778899aabbccddeeff
@ MGMT Event: Command Complete (0x0001) plen 10 {0x0001} [hci0] 12:37:06.100500
Pair Device (0x0019) plen 7
Status: Success (0x00)
LE Address: AA:BB:CC:DD:EE:FF (Random)
Key events to check after pairing โ bestie, these are the receipts ๐งพ:
New Long Term KeywithStore hint: Yesโ The kernel is telling userspace to persist this key for future reconnections.New Identity Resolving Keyโ Reveals the deviceโs real address behind anRPA. This is essential for address resolution on reconnect.Key typeโAuthenticated P-256indicates Secure Connections with MITM protection.Unauthenticated P-256means Just Works SC.Authenticated(noP-256) means Legacy with MITM.
Pairing failure โ oop, the vibe check failed ๐ญ:
@ MGMT Event: Command Complete (0x0001) plen 10 {0x0001} [hci0] 12:37:06.100500
Pair Device (0x0019) plen 7
Status: Authentication Failed (0x05)
LE Address: AA:BB:CC:DD:EE:FF (Random)
MGMT pairing error codes: ๐
ok so these are the MGMT-specific pairing fails, and bestie, they are highkey giving โcheck the layer belowโ energy:
0x03โ Failed โ Generic failure; check SMP or HCI errors preceding this.0x04โ Connect Failed โ Could not establish connection before pairing.0x05โ Authentication Failed โ SMP or SSP authentication did not succeed; check SMP Pairing Failed reason or HCI Authentication Failure event.0x08โ Timeout โ Pairing timed out; remote device may have gone out of range.0x0bโ Rejected โ Remote device rejected the pairing request.0x0dโ Invalid Parameters โ Bad address type or capability value in the Pair Device command.
Device Connection and Disconnection ๐
MGMT reports connection lifecycle events that complement the HCI-level events shown elsewhere in the trace. It is literally the userspace โwe are so back / itโs overโ duo.
Device connected:
@ MGMT Event: Device Connected (0x000b) plen 13 {0x0001} [hci0] 12:36:18.974319
LE Address: AA:BB:CC:DD:EE:FF (Random)
Flags: 0x0000
EIR Data Length: 0
This event follows the HCI LE (Enhanced) Connection Complete event. It tells userspace that a new connection is ready.
Device disconnected:
@ MGMT Event: Device Disconnected (0x000c) plen 8 {0x0001} [hci0] 12:38:20.500200
LE Address: AA:BB:CC:DD:EE:FF (Random)
Reason: Connection timeout (0x01)
Disconnect reasons reported by MGMT:
0x00โ Unspecified โ No reason provided; typically a local disconnect.0x01โ Connection timeout โ Link supervision timeout expired; device may be out of range.0x02โ Connection terminated by local host โ Local side initiated the disconnection.0x03โ Connection terminated by remote host โ Remote device initiated the disconnection.
Unpair Device removes stored keys and optionally disconnects:
@ MGMT Command: Unpair Device (0x001a) plen 7 {0x0001} [hci0] 12:39:00.100200
LE Address: AA:BB:CC:DD:EE:FF (Random)
Disconnect: Enabled (0x01)
@ MGMT Event: Command Complete (0x0001) plen 10 {0x0001} [hci0] 12:39:00.100500
Unpair Device (0x001a) plen 7
Status: Success (0x00)
Advertising via MGMT ๐ฃ
Modern BlueZ configures advertising through MGMT commands rather than sending HCI LE advertising commands directly. This provides a multi-client advertising infrastructure.
Adding an advertisement:
@ MGMT Command: Add Advertising (0x003e) plen 22 {0x0001} [hci0] 12:40:00.100200
Instance: 1
Flags: 0x0006
The connectable flag will be managed
The limited discoverable flag will be managed
Duration: 0
Timeout: 0
Advertising data length: 6
Scan response length: 0
@ MGMT Event: Command Complete (0x0001) plen 5 {0x0001} [hci0] 12:40:00.100500
Add Advertising (0x003e) plen 2
Status: Success (0x00)
Instance: 1
Removing an advertisement:
@ MGMT Command: Remove Advertising (0x003f) plen 1 {0x0001} [hci0] 12:41:00.100200
Instance: 1
@ MGMT Event: Command Complete (0x0001) plen 5 {0x0001} [hci0] 12:41:00.100500
Remove Advertising (0x003f) plen 2
Status: Success (0x00)
Instance: 1
For the HCI-level advertising commands that result from these MGMT operations, see ADVERTISING AND SCANNING.
Error Diagnosis ๐ต๏ธโโ๏ธ
MGMT Command Status and Command Complete events carry error codes that are distinct from HCI error codes. When diagnosing failures, check both layers. Literally look at both, bestie.
MGMT error in Command Complete:
@ MGMT Event: Command Complete (0x0001) plen 4 {0x0001} [hci0] 12:42:00.100200
Set Powered (0x0005) plen 1
Status: RFKilled (0x12)
MGMT Command Status (asynchronous command accepted or rejected):
@ MGMT Event: Command Status (0x0002) plen 3 {0x0001} [hci0] 12:42:01.100200
Start Discovery (0x0023) plen 0
Status: Busy (0x0a)
๐ MGMT error codes ๐
ok so here's the master list of every MGMT status code the kernel can throw back at you โ memorize the vibe, not the numbers, and you're golden:
0x00Success โ command completed successfully โจ literally the only one you want to see.0x01Unknown Command โ the kernel does not recognize the command; MGMT version may be too old ๐ญ your kernel's giving "who dis".0x02Not Connected โ this operation requires an active connection that does not exist. no connection, no service, bestie.0x03Failed โ generic failure; check the HCI events for the root cause ๐ it's giving "something happened" with zero elaboration.0x04Connect Failed โ the connection attempt failed at the HCI level.0x05Authentication Failed โ the pairing or authentication procedure failed.0x06Not Paired โ operation requires an existing bond; the device is not paired. no bond = no entry, periodt.0x07No Resources โ the kernel ran out of resources (memory, handles, etc.) ๐ญ the poor thing is tapped out.0x08Timeout โ operation timed out. it simply gave up waiting.0x09Already Connected โ a connection already exists to the target device. you're already there, girl.0x0aBusy โ another operation is in progress (e.g., discovery running). one thing at a time, main character.0x0bRejected โ operation rejected by the remote device or by policy. not you getting ghosted ๐0x0cNot Supported โ feature not supported by this controller or kernel version.0x0dInvalid Parameters โ bad parameters in the command. check your args, bestie.0x0eDisconnected โ the connection was lost during the operation.0x0fNot Powered โ the controller is not powered on; call Set Powered first โก like, turn it on before you ask it stuff.0x10Cancelled โ operation was cancelled by the user or by another command.0x11Invalid Index โ controller index does not exist. thathcinumber? not real.0x12RFKilled โ the radio is disabled by rfkill; unblock withrfkill unblock bluetooth๐ classic.0x13Already Paired โ the device is already paired; unpair first if re-pairing is needed.0x14Permission Denied โ the process lacks required privileges (CAP_NET_ADMIN). elevate responsibly ๐
Correlating MGMT and HCI errors: when MGMT reports Failed (0x03) or Authentication Failed (0x05), look at the HCI events immediately preceding the MGMT response. The HCI-level error (e.g., Authentication Failure (0x05), Connection Timeout (0x08)) provides the specific hardware-level cause ๐ต๏ธโโ๏ธ the tea is always in the event right before.
๐ Automating MGMT Analysis ๐
ngl this is the part where grep becomes your bestie forever. here's the whole toolbox:
List all MGMT commands and events:
grep -n "@ MGMT" output.txt
Find MGMT errors (non-zero status):
grep -n "@ MGMT" output.txt | grep -v "Status: Success"
Track adapter power state changes:
grep -n "Set Powered\|RFKilled\|Not Powered" output.txt
Find all discovery activity:
grep -n "Start Discovery\|Stop Discovery\|Device Found\|Discovering" output.txt
Find pairing activity via MGMT:
grep -n "Pair Device\|User Confirmation\|User Passkey\|New Long Term Key\|New Identity Resolving Key\|Authentication Failed" output.txt
Find connection lifecycle events:
grep -n "Device Connected\|Device Disconnected\|Unpair Device" output.txt
Identify which MGMT clients are active:
grep -n "@ MGMT Open\|@ MGMT Close" output.txt
๐ Full MGMT diagnosis pattern ๐
follow these in order and the trace will basically explain itself, iykyk:
- Find MGMT Open โ confirm bluetoothd connected and note the socket ID.
- Check Read Controller Information โ verify address, version, and current settings.
- Find Set Powered โ confirm the adapter was powered on successfully.
- Search for non-Success status codes โ these indicate failures ๐ฉ
- For pairing issues, find Pair Device and trace forward to its Command Complete response.
- For connection issues, find Device Connected / Device Disconnected and check the disconnect reason.
โจ EXAMPLES โจ
๐ Capture the traces from hci0 to hcidump.log file ๐
$ btmon -i hci0 -w hcidump.log
๐ฆ Open the trace file ๐ฆ
$ btmon -r hcidump.log
๐ธ Open the trace file with wall-clock timestamps ๐ธ
$ btmon -t -r hcidump.log
๐ Open the trace file with full date and time ๐
$ btmon -T -r hcidump.log
๐ค AUTOMATED TRACE ANALYSIS ๐ค
Okay bestie, so this whole section is the glow-up part โ it's the guidance for analyzing btmon traces programmatically, or with a little help from your AI bestie. Every topic in here points back at the detailed protocol sections earlier in this doc, so if you're like "wait what does that mean" โ it's giving "read the earlier chapters," and honestly, do it ๐
๐ธ Recommended Workflow ๐ธ
Follow these steps in order, queen. This is the whole itinerary:
- Get an overview โ start with
btmon -a <file>to see packet counts, connection handles, device addresses, and traffic volumes. Vibes check first, always ๐ - Decode with timestamps โ use
btmon -t -r <file> > output.txtto produce a text file with wall-clock timestamps for analysis. Timestamps are lowkey essential, don't skip โจ - Identify connections โ search for connection establishment events to build a handle-to-address mapping:
grep -n "Connection Complete\|Enhanced Connection Complete\|CIS Established" output.txt
- Track disconnections โ search for disconnect events and their reasons:
grep -n "Disconnect Complete" output.txt
Then peek at the lines following each match for the Reason: field. See HCI ERROR AND DISCONNECT REASON CODES for how to interpret it ๐
- Check pairing/security โ search for SMP activity (see SMP PAIRING FLOW):
grep -n "Pairing Request\|Pairing Response\|Pairing Failed\|Encryption Change" output.txt
- Identify LE Audio โ search for ASCS and CIS activity (see LE AUDIO PROTOCOL FLOW):
grep -n "ASE Control Point\|CIG Parameters\|Create Connected Isochronous\|CIS Established\|Setup ISO Data Path" output.txt
- Check for errors โ search across all protocol layers (see PROTOCOL ERROR CODES):
# HCI-level errors
grep -n "Status:" output.txt | grep -v "Success"
# ATT-level errors
grep -n "Error Response" output.txt
# SMP failures
grep -n "Pairing Failed" output.txt
# L2CAP rejections
grep -n "Connection refused" output.txt
- Extract GATT discovery โ filter the GATT service/characteristic discovery traffic (see RECONSTRUCTING A GATT DATABASE FROM SNOOP TRACES):
# Find all service discovery responses
grep -n "Read By Group Type Response\|Attribute group list\|Handle range.*UUID" output.txt
# Find all characteristic discovery responses
grep -n "Read By Type Response\|Properties:\|Value Handle:\|Value UUID:" output.txt
# Find all descriptor discovery responses
grep -n "Find Information Response\|Format:\|Handle:.*UUID:" output.txt
# Find targeted service searches
grep -n "Find By Type Value" output.txt
- Check L2CAP channels โ identify protocol usage and channel issues (see L2CAP CHANNEL TRACKING):
grep -n "PSM:\|Connection Request\|Connection Response\|Parameter Update" output.txt
- Check advertising โ see what devices are visible and what they're advertising (see ADVERTISING AND SCANNING):
grep -n "Advertising Report\|Name (complete):\|Appearance:" output.txt
๐ฆ Key Patterns for Connection Lifecycle ๐ฆ
A complete connection lifecycle for an LE ACL connection follows this pattern in the trace โ memorize it, it's the main character arc of every trace fr fr:
- LE Enhanced Connection Complete โ connection established, note the
HandleandPeeraddress ๐ - LE Connection Update Complete โ connection parameters changed (may occur zero or more times, so don't panic if it's absent)
- Encryption Change โ link encrypted (may show encryption algorithm). See SMP PAIRING FLOW for the SMP exchange that precedes this ๐
- ACL Data with ATT/SMP/L2CAP โ service discovery and data exchange. See RECONSTRUCTING A GATT DATABASE FROM SNOOP TRACES for GATT, L2CAP CHANNEL TRACKING for channel setup, and PROTOCOL ERROR CODES for error interpretation.
- Disconnect Complete โ connection ended, check the
Reasonfield. See HCI ERROR AND DISCONNECT REASON CODES for reason codes. And that's the season finale ๐ญ
For LE Audio connections, additional steps appear between 3 and 5 (see LE AUDIO PROTOCOL FLOW for full details):
- ATT operations on PACS/ASCS characteristics (codec negotiation) ๐ง
- LE Set CIG Parameters command and response
- LE Create CIS command
- LE CIS Established event (note the CIS handle)
- LE Setup ISO Data Path command
- ISO Data TX/RX (audio streaming)
- Disconnect Complete on CIS handle (stream ended)
- LE Remove CIG (group removed)
๐ Common Debugging Scenarios ๐
Pairing failure diagnosis
- Find Pairing Request โ note IO capabilities and auth requirements
- Find Pairing Response โ compare to determine association model
- If Pairing Failed appears, the reason code identifies the failure (see SMP PAIRING FLOW)
- If Encryption Change shows
Status: Success, pairing succeeded ๐ - If no SMP traffic on reconnect but Encryption Change fails, the bond was lost on one side ๐ฌ
Audio streaming failure diagnosis (see LE AUDIO PROTOCOL FLOW)
- Check PACS reads โ do both devices support compatible codecs?
- Check ASE Control Point Config Codec โ was it accepted?
- Check ASE state notifications โ did the ASE reach
Streaming? - Check CIS Established โ was
StatusSuccess? - Check Setup ISO Data Path โ was it configured?
- Check for ISO Data packets โ is audio actually flowing?
GATT operation rejected (see PROTOCOL ERROR CODES)
- Find Error Response โ note the error code
- Insufficient Encryption (
0x0f) โ expect LE Start Encryption to follow - Authentication Insufficient (
0x05) โ expect SMP pairing to follow - After security is established, the ATT operation should be retried ๐
Connection parameter negotiation failure
- Find Connection Parameter Update Request (L2CAP level)
- Check the Response โ rejected means the central denied it
- Alternatively, find LE Connection Update Complete (HCI level)
- Check
Statusโ non-zero means the controller rejected the update
๐ง Vendor-Specific Events ๐ง
Vendor-specific HCI events (event code 0xFF) contain controller-manufacturer diagnostic data. btmon decodes some vendor events for known manufacturers (Intel, Broadcom, etc.) but many sub-events show as Unknown with raw hex data. These are expected and generally not actionable without vendor documentation โ so don't @ me, it's the vendor's fault ๐
Intel controllers emit extended telemetry events (subevent 0x8780) that include connection quality metrics, error counters, and firmware state. Partial decoding is available in monitor/intel.c.
๐ RESOURCES ๐
http://www.bluez.org โ go touch grass on the official site, bestie ๐ฑ
๐ญ REPORTING BUGS ๐ญ
linux-bluetooth@vger.kernel.org โ found a bug? Tell the mailing list, they'll handle it ๐
โญ SEE ALSO โญ
๐ AUTHOR ๐
Marcel Holtmann <marcel@holtmann.org>, Tedd Ho-Jeong An <tedd.an@intel.com>
๐ COPYRIGHT ๐
Free use of this software is granted under the terms of the GNU Lesser General Public Licenses (LGPL).
And that's a wrap on the whole page, bestie!! ๐ซถ This lil legend is from BlueZ, dated April 2021, and it's the BTMON(1) manual โ go forth and decode those traces. Periodt. ๐
โจ
๐ฆ where this page comes from
- centos-stream 9: bluez 5.87+2.git8750129efca8-1.el9 ยท GPLv2+
- ๐ girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms