* [PATCH BlueZ vRFC 1/7] monitor: Add audio quality statistics for ISO streams
@ 2026-10-02 19:27 Luiz Augusto von Dentz
2026-10-02 19:27 ` [PATCH BlueZ vRFC 2/7] monitor/analyze: Fix a new connection for each BIS or eSCO packet Luiz Augusto von Dentz
` (6 more replies)
0 siblings, 7 replies; 8+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-02 19:27 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Track the bursts of erased samples, the Gilbert model burst ratio and the
RFC 3550 interarrival jitter of received ISO data, using the controller
timestamp when present or the SN paced at ISO Interval over BN otherwise.
Live decoding now prints the loss only for the sample revealing it, along
with late samples:
> LE-CIS: Handle 2304 SN... flags 0x02 dlen 4 sflags 2 #302 [hci0] 83.728300
Lost: 0/34 (0.00%) dropped 1 invalid 0 burst 1
Late: +7.50 msec (interval 7.50 msec)
The new --quality option prints a periodic summary per stream:
= Quality: LE-CIS 2304 [hci0] 55.004307
RX loss: 5/101 (4.95%) burst max 4
RX jitter: 1.10 msec late 2
The analyze mode reports the bursts and the jitter per connection:
Found LE-CIS connection with handle 2304
...
RX loss: 2896/30733 (9.42%) dropped 0 invalid 0
RX bursts: 1304 (max 7) [1: 799, 2: 52, 3-5: 326, 6-10: 127, >10: 0]
RX burstiness: p 0.0468 q 0.4503 BurstR 2.01
RX jitter: 6.66 msec (max 13.06 msec, late 4275)
RX interval: 10.00 msec (0-91 msec, ~8 msec +/- 10 msec)
A reordered sample is no longer accounted as lost.
Assisted-by: OpenCode:claude-opus-5.5
---
doc/btmon.rst | 6 +
monitor/analyze.c | 42 +++--
monitor/main.c | 12 +-
monitor/packet.c | 387 ++++++++++++++++++++++++++++++++++++++++++++--
monitor/packet.h | 46 ++++++
5 files changed, 465 insertions(+), 28 deletions(-)
diff --git a/doc/btmon.rst b/doc/btmon.rst
index 1129ceb2e616..fd19b73b22b3 100644
--- a/doc/btmon.rst
+++ b/doc/btmon.rst
@@ -93,6 +93,12 @@ OPTIONS
-I, --iso Dump ISO stream traffic in raw hex format. Required
to see LE Audio isochronous data in the output.
+-Q MSEC, --quality MSEC Print a summary of the received audio stream
+ quality every MSEC milliseconds: erasures, the
+ longest burst of erasures, jitter and late
+ samples. Only LE Audio isochronous streams are
+ currently supported.
+
-E IP, --ellisys IP Send Ellisys HCI Injection.
-P, --no-pager Disable pager usage while reading the log file.
diff --git a/monitor/analyze.c b/monitor/analyze.c
index 5d54ad61b7b6..13d470ff566d 100644
--- a/monitor/analyze.c
+++ b/monitor/analyze.c
@@ -107,6 +107,7 @@ struct hci_conn {
struct hci_stats rx;
struct hci_stats tx;
struct packet_loss rx_loss;
+ struct packet_jitter rx_jitter;
};
struct hci_conn_tx {
@@ -380,14 +381,8 @@ static void conn_destroy(void *data)
print_stats(&conn->rx, "RX");
print_stats(&conn->tx, "TX");
- if (conn->rx_loss.total)
- print_field("RX loss: %zu/%zu (%zu.%02zu%%) "
- "dropped %zu invalid %zu",
- conn->rx_loss.lost, conn->rx_loss.total,
- conn->rx_loss.lost * 100 / conn->rx_loss.total,
- (conn->rx_loss.lost * 10000 /
- conn->rx_loss.total) % 100,
- conn->rx_loss.dropped, conn->rx_loss.invalid);
+ packet_loss_print(&conn->rx_loss, "RX");
+ packet_jitter_print(&conn->rx_jitter, "RX");
if (conn->setup_seen) {
print_field("Connected: #%lu", conn->frame_connected);
@@ -1060,6 +1055,15 @@ static void evt_sync_conn_complete(struct hci_dev *dev, struct timeval *tv,
conn->setup_seen = true;
}
+/* Nominal SDU interval of an unframed stream: ISO Interval over BN */
+static void set_iso_interval(struct hci_conn *conn, uint16_t interval,
+ uint8_t bn)
+{
+ if (bn)
+ conn->rx_jitter.interval = (uint32_t)le16_to_cpu(interval) *
+ 1250 / bn;
+}
+
static void evt_le_cis_established(struct hci_dev *dev, struct timeval *tv,
unsigned long frame,
struct iovec *iov)
@@ -1082,6 +1086,9 @@ static void evt_le_cis_established(struct hci_dev *dev, struct timeval *tv,
link = link_lookup(dev, conn->handle);
if (link)
memcpy(conn->bdaddr, link->bdaddr, 6);
+
+ /* Only the Peripheral gets a CIS Request for the handle */
+ set_iso_interval(conn, evt->interval, link ? evt->c_bn : evt->p_bn);
}
static void evt_le_cis_req(struct hci_dev *dev, struct timeval *tv,
@@ -1123,6 +1130,7 @@ static void evt_le_big_complete(struct hci_dev *dev, struct timeval *tv,
if (conn) {
conn->setup_seen = true;
conn->frame_connected = frame;
+ set_iso_interval(conn, evt->interval, evt->bn);
}
}
}
@@ -1149,6 +1157,7 @@ static void evt_le_big_sync_established(struct hci_dev *dev, struct timeval *tv,
if (conn) {
conn->setup_seen = true;
conn->frame_connected = frame;
+ set_iso_interval(conn, evt->interval, evt->bn);
}
}
}
@@ -1459,6 +1468,7 @@ static void iso_pkt(struct timeval *tv, uint16_t index, bool out,
{
const struct bt_hci_iso_hdr *hdr = data;
struct iovec iov = { .iov_base = (void *)data, .iov_len = size };
+ const void *ts = NULL;
struct hci_conn *conn;
struct hci_dev *dev;
uint16_t handle;
@@ -1492,12 +1502,22 @@ static void iso_pkt(struct timeval *tv, uint16_t index, bool out,
/* Skip the timestamp when present */
if (ISO_FLAGS_TS(flags))
- util_iov_pull_mem(&iov, sizeof(uint32_t));
+ ts = util_iov_pull_mem(&iov, sizeof(uint32_t));
start = util_iov_pull_mem(&iov, sizeof(*start));
- if (start)
- packet_loss_add(&conn->rx_loss, le16_to_cpu(start->sn),
+ if (start) {
+ uint16_t sn = le16_to_cpu(start->sn);
+
+ packet_loss_add(&conn->rx_loss, sn,
ISO_DATA_FLAGS(le16_to_cpu(start->slen)));
+
+ /* Prefer the controller timestamp over the SN */
+ if (ts)
+ packet_jitter_add_ts(&conn->rx_jitter, tv,
+ get_le32(ts));
+ else
+ packet_jitter_add_sn(&conn->rx_jitter, tv, sn);
+ }
}
if (out) {
diff --git a/monitor/main.c b/monitor/main.c
index 1a7a25713de1..c285832612b8 100644
--- a/monitor/main.c
+++ b/monitor/main.c
@@ -68,6 +68,8 @@ static void usage(void)
"\t-S, --sco Dump SCO traffic\n"
"\t-A, --a2dp Dump A2DP stream traffic\n"
"\t-I, --iso Dump ISO traffic\n"
+ "\t-Q, --quality <msec> Print a periodic quality summary\n"
+ "\t of the audio streams\n"
"\t-E, --ellisys [ip] Send Ellisys HCI Injection\n"
"\t-P, --no-pager Disable pager usage\n"
"\t-0, --print0 Separate frames with a NUL, for a\n"
@@ -99,6 +101,7 @@ static const struct option main_options[] = {
{ "sco", no_argument, NULL, 'S' },
{ "a2dp", no_argument, NULL, 'A' },
{ "iso", no_argument, NULL, 'I' },
+ { "quality", required_argument, NULL, 'Q' },
{ "ellisys", required_argument, NULL, 'E' },
{ "no-pager", no_argument, NULL, 'P' },
{ "print0", no_argument, NULL, '0' },
@@ -137,7 +140,7 @@ int main(int argc, char *argv[])
struct sockaddr_un addr;
opt = getopt_long(argc, argv,
- "r:w:a:s:p:i:d:B:V:MKNtTSAIE:P0J:R:C:c:vh",
+ "r:w:a:s:p:i:d:B:V:MKNtTSAIQ:E:P0J:R:C:c:vh",
main_options, NULL);
if (opt < 0)
break;
@@ -214,6 +217,13 @@ int main(int argc, char *argv[])
case 'I':
filter_mask |= PACKET_FILTER_SHOW_ISO_DATA;
break;
+ case 'Q':
+ if (atoi(optarg) <= 0) {
+ usage();
+ return EXIT_FAILURE;
+ }
+ packet_set_quality_period(atoi(optarg));
+ break;
case 'E':
ellisys_server = optarg;
ellisys_port = 24352;
diff --git a/monitor/packet.c b/monitor/packet.c
index 8450347fc56b..09603c19e05e 100644
--- a/monitor/packet.c
+++ b/monitor/packet.c
@@ -114,6 +114,7 @@
static time_t time_offset = ((time_t) -1);
static int priority_level = BTSNOOP_PRIORITY_DEBUG;
static unsigned long filter_mask = 0;
+static unsigned int quality_period;
static bool index_filter = false;
static uint16_t index_current = 0;
static uint16_t fallback_manufacturer = COMPANY_ID_UNKNOWN;
@@ -11974,8 +11975,62 @@ long long packet_latency_stddev(const struct packet_latency *latency)
return (long long)sqrt(var);
}
+static void loss_burst_end(struct packet_loss *loss)
+{
+ size_t len = loss->burst;
+
+ if (!len)
+ return;
+
+ loss->bursts++;
+ loss->burst_last = len;
+
+ if (len == 1)
+ loss->burst_hist[0]++;
+ else if (len == 2)
+ loss->burst_hist[1]++;
+ else if (len <= 5)
+ loss->burst_hist[2]++;
+ else if (len <= 10)
+ loss->burst_hist[3]++;
+ else
+ loss->burst_hist[4]++;
+
+ loss->burst = 0;
+}
+
+static void loss_sample(struct packet_loss *loss, bool erased, size_t count)
+{
+ if (!count)
+ return;
+
+ if (loss->have_prev && loss->prev_erased != erased) {
+ if (erased)
+ loss->good_lost++;
+ else
+ loss->lost_good++;
+ }
+
+ if (erased) {
+ loss->erased += count;
+ loss->burst += count;
+ if (loss->burst > loss->burst_max)
+ loss->burst_max = loss->burst;
+ } else {
+ loss_burst_end(loss);
+ }
+
+ loss->prev_erased = erased;
+ loss->have_prev = true;
+}
+
+/* How far back a sequence number may go to be considered a reorder */
+#define LOSS_REORDER_MAX 16
+
void packet_loss_add(struct packet_loss *loss, uint16_t sn, uint8_t sflags)
{
+ bool advanced = true;
+
loss->total++;
switch (sflags) {
@@ -11998,13 +12053,188 @@ void packet_loss_add(struct packet_loss *loss, uint16_t sn, uint8_t sflags)
if (sn != loss->last_sn && gap < 0x8000) {
loss->lost += gap;
loss->total += gap;
+ loss_sample(loss, true, gap);
+ } else if ((uint16_t)(loss->last_sn - sn) < LOSS_REORDER_MAX) {
+ advanced = false;
+
+ /* A late arrival of a sample already counted as lost */
+ if (sn != loss->last_sn && loss->lost) {
+ loss->lost--;
+ loss->total--;
+ }
}
}
+ /* Duplicates and reorders do not take part in the bursts */
+ if (!advanced)
+ return;
+
+ loss_sample(loss, sflags == 0x02, 1);
+
+ /* Anything further back than a reorder restarts the sequence */
loss->last_sn = sn;
loss->have_sn = true;
}
+size_t packet_loss_burst_max(const struct packet_loss *loss)
+{
+ return loss->burst_max;
+}
+
+/*
+ * Two state Gilbert model of the erasures: p is the probability of going
+ * from received to erased and q the one of going back, the burst ratio
+ * being how much burstier the erasures are than random ones.
+ */
+bool packet_loss_burst_ratio(const struct packet_loss *loss, double *p,
+ double *q, double *ratio)
+{
+ size_t good = loss->total - loss->erased;
+
+ if (!loss->erased || !good)
+ return false;
+
+ *p = (double)loss->good_lost / good;
+ *q = (double)loss->lost_good / loss->erased;
+
+ if (*p + *q <= 0)
+ return false;
+
+ *ratio = 1 / (*p + *q);
+
+ return true;
+}
+
+void packet_loss_print(const struct packet_loss *loss, const char *label)
+{
+ struct packet_loss tmp = *loss;
+ double p, q, ratio;
+
+ if (!loss->total)
+ return;
+
+ print_field("%s loss: %zu/%zu (%zu.%02zu%%) dropped %zu invalid %zu",
+ label, loss->lost, loss->total,
+ loss->lost * 100 / loss->total,
+ (loss->lost * 10000 / loss->total) % 100,
+ loss->dropped, loss->invalid);
+
+ /* Account for the burst still in progress */
+ loss_burst_end(&tmp);
+
+ if (!tmp.bursts)
+ return;
+
+ print_field("%s bursts: %zu (max %zu) [1: %zu, 2: %zu, 3-5: %zu, "
+ "6-10: %zu, >10: %zu]", label, tmp.bursts,
+ tmp.burst_max, tmp.burst_hist[0], tmp.burst_hist[1],
+ tmp.burst_hist[2], tmp.burst_hist[3],
+ tmp.burst_hist[4]);
+
+ if (packet_loss_burst_ratio(loss, &p, &q, &ratio))
+ print_field("%s burstiness: p %.4f q %.4f BurstR %.2f", label,
+ p, q, ratio);
+}
+
+static void jitter_add(struct packet_jitter *jitter, struct timeval *tv,
+ int64_t expected)
+{
+ struct timeval delta;
+ int64_t arrival, d;
+
+ timersub(tv, &jitter->prev_tv, &delta);
+ jitter->prev_tv = *tv;
+
+ packet_latency_add(&jitter->delta, &delta);
+
+ /*
+ * RFC 3550 interarrival jitter: the difference of the transit times
+ * of two consecutive samples, smoothed with a gain of 1/16.
+ */
+ arrival = (int64_t)delta.tv_sec * 1000000 + delta.tv_usec;
+ d = arrival - expected;
+ jitter->last_dev = d;
+
+ /* A sample arriving an interval or more later than expected */
+ if (jitter->interval && d >= jitter->interval)
+ jitter->late++;
+
+ if (d < 0)
+ d = -d;
+
+ if ((uint64_t)d > jitter->max_dev)
+ jitter->max_dev = d;
+
+ jitter->jitter16 += (uint64_t)d - ((jitter->jitter16 + 8) >> 4);
+}
+
+void packet_jitter_add_sn(struct packet_jitter *jitter, struct timeval *tv,
+ uint16_t sn)
+{
+ uint16_t diff = sn - jitter->prev_sn;
+
+ if (!jitter->interval)
+ return;
+
+ if (!jitter->have_prev) {
+ jitter->prev_tv = *tv;
+ jitter->prev_sn = sn;
+ jitter->have_prev = true;
+ return;
+ }
+
+ /* Leave the duplicates and the reorders out */
+ if (!diff || diff >= 0x8000)
+ return;
+
+ jitter->prev_sn = sn;
+ jitter_add(jitter, tv, (int64_t)diff * jitter->interval);
+}
+
+void packet_jitter_add_ts(struct packet_jitter *jitter, struct timeval *tv,
+ uint32_t ts)
+{
+ uint32_t diff = ts - jitter->prev_ts;
+
+ if (!jitter->have_prev) {
+ jitter->prev_tv = *tv;
+ jitter->prev_ts = ts;
+ jitter->have_prev = true;
+ return;
+ }
+
+ if (!diff || diff >= 0x80000000)
+ return;
+
+ jitter->prev_ts = ts;
+ jitter_add(jitter, tv, diff);
+}
+
+void packet_jitter_print(const struct packet_jitter *jitter,
+ const char *label)
+{
+ uint64_t j = jitter->jitter16 >> 4;
+
+ if (!jitter->delta.count)
+ return;
+
+ print_field("%s jitter: %" PRIu64 ".%02" PRIu64 " msec (max %"
+ PRIu64 ".%02" PRIu64 " msec, late %zu)", label,
+ j / 1000, (j % 1000) / 10,
+ jitter->max_dev / 1000, (jitter->max_dev % 1000) / 10,
+ jitter->late);
+
+ if (jitter->interval)
+ print_field("%s interval: %u.%02u msec (%lld-%lld msec, "
+ "~%lld msec +/- %lld msec)", label,
+ jitter->interval / 1000,
+ (jitter->interval % 1000) / 10,
+ TV_MSEC(jitter->delta.min),
+ TV_MSEC(jitter->delta.max),
+ TV_MSEC(jitter->delta.med),
+ packet_latency_stddev(&jitter->delta));
+}
+
/*
* Timestamp and frame number of the packet being decoded. btmon decodes one
* packet at a time, so this lets the upper layers reference the frame that
@@ -13338,6 +13568,17 @@ static void le_past_received_evt(struct timeval *tv, uint16_t index,
print_clock_accuracy(evt->clock_accuracy);
}
+/* Nominal SDU interval of an unframed stream: ISO Interval over BN */
+static void set_iso_interval(uint16_t handle, uint16_t interval, uint8_t bn)
+{
+ struct packet_conn_data *conn = packet_get_conn_data(handle);
+
+ if (!conn || !bn)
+ return;
+
+ conn->rx_jitter.interval = (uint32_t)le16_to_cpu(interval) * 1250 / bn;
+}
+
static void le_cis_established_evt(struct timeval *tv, uint16_t index,
const void *data, uint8_t size)
{
@@ -13360,10 +13601,16 @@ static void le_cis_established_evt(struct timeval *tv, uint16_t index,
print_field("Peripheral to Central MTU: %u", le16_to_cpu(evt->p_mtu));
print_slot_125("ISO Interval", evt->interval);
- if (!evt->status)
- assign_handle(index, le16_to_cpu(evt->conn_handle),
- BTMON_CONN_CIS,
- NULL, BDADDR_LE_PUBLIC);
+ if (!evt->status) {
+ uint16_t handle = le16_to_cpu(evt->conn_handle);
+
+ assign_handle(index, handle, BTMON_CONN_CIS, NULL,
+ BDADDR_LE_PUBLIC);
+
+ /* Only the Peripheral gets a CIS Request for the handle */
+ set_iso_interval(handle, evt->interval,
+ lookup_parent(handle) ? evt->c_bn : evt->p_bn);
+ }
}
static void le_req_cis_evt(struct timeval *tv, uint16_t index,
@@ -13411,10 +13658,13 @@ static void le_big_complete_evt(struct timeval *tv, uint16_t index,
if (!evt->status) {
int i;
- for (i = 0; i < evt->num_bis; i++)
+ for (i = 0; i < evt->num_bis; i++) {
assign_handle(index, le16_to_cpu(evt->bis_handle[i]),
BTMON_CONN_BIS, NULL,
BDADDR_LE_PUBLIC);
+ set_iso_interval(le16_to_cpu(evt->bis_handle[i]),
+ evt->interval, evt->bn);
+ }
}
}
@@ -13447,9 +13697,12 @@ static void le_big_sync_estabilished_evt(struct timeval *tv, uint16_t index,
if (!evt->status) {
int i;
- for (i = 0; i < evt->num_bis; i++)
+ for (i = 0; i < evt->num_bis; i++) {
assign_handle(index, le16_to_cpu(evt->bis[i]),
BTMON_CONN_BIS, NULL, BDADDR_LE_PUBLIC);
+ set_iso_interval(le16_to_cpu(evt->bis[i]),
+ evt->interval, evt->bn);
+ }
}
}
@@ -15149,6 +15402,95 @@ void packet_hci_scodata(struct timeval *tv, struct ucred *cred, uint16_t index,
packet_hexdump(data, size);
}
+static void print_rx_quality(struct packet_conn_data *conn, size_t erased,
+ size_t late, uint8_t sflags, size_t burst)
+{
+ struct packet_loss *loss = &conn->rx_loss;
+ struct packet_jitter *jitter = &conn->rx_jitter;
+
+ /* Only report the samples that revealed a loss or arrived late */
+ if (loss->erased != erased || sflags)
+ print_field("Lost: %zu/%zu (%zu.%02zu%%) dropped %zu "
+ "invalid %zu burst %zu",
+ loss->lost, loss->total,
+ loss->lost * 100 / loss->total,
+ (loss->lost * 10000 / loss->total) % 100,
+ loss->dropped, loss->invalid, burst);
+
+ if (jitter->late != late)
+ print_field("Late: +%" PRId64 ".%02" PRId64 " msec "
+ "(interval %u.%02u msec)",
+ jitter->last_dev / 1000,
+ (jitter->last_dev % 1000) / 10,
+ jitter->interval / 1000,
+ (jitter->interval % 1000) / 10);
+}
+
+void packet_set_quality_period(unsigned int msec)
+{
+ quality_period = msec;
+}
+
+static void quality_summary(struct timeval *tv, struct ucred *cred,
+ uint16_t index, struct packet_conn_data *conn,
+ size_t burst)
+{
+ struct packet_quality *q = &conn->rx_quality;
+ struct packet_loss *loss = &conn->rx_loss;
+ struct packet_jitter *jitter = &conn->rx_jitter;
+ char str[32];
+ struct timeval delta;
+ size_t erased, total;
+ uint64_t j;
+
+ if (!quality_period || !tv)
+ return;
+
+ if (burst > q->burst_max)
+ q->burst_max = burst;
+
+ if (!timerisset(&q->start)) {
+ q->start = *tv;
+ q->erased = loss->erased;
+ q->total = loss->total;
+ q->late = jitter->late;
+ return;
+ }
+
+ timersub(tv, &q->start, &delta);
+ if (TV_MSEC(delta) < quality_period)
+ return;
+
+ erased = loss->erased - q->erased;
+ total = loss->total - q->total;
+ j = jitter->jitter16 >> 4;
+
+ snprintf(str, sizeof(str), "%s %u", conn_type_str(conn->type),
+ conn->handle);
+ print_packet(tv, cred, '=', index, NULL, COLOR_SYSTEM_NOTE,
+ "Quality", str, NULL);
+
+ print_field("RX loss: %zu/%zu (%zu.%02zu%%) burst max %zu", erased,
+ total, total ? erased * 100 / total : 0,
+ total ? (erased * 10000 / total) % 100 : 0,
+ q->burst_max);
+
+ if (jitter->delta.count)
+ print_field("RX jitter: %" PRIu64 ".%02" PRIu64 " msec "
+ "late %zu", j / 1000, (j % 1000) / 10,
+ jitter->late - q->late);
+
+ if (conn->tx_l.count)
+ print_field("TX latency: ~%lld msec",
+ TV_MSEC(conn->tx_l.med));
+
+ q->start = *tv;
+ q->erased = loss->erased;
+ q->total = loss->total;
+ q->late = jitter->late;
+ q->burst_max = loss->burst;
+}
+
void packet_hci_isodata(struct timeval *tv, struct ucred *cred, uint16_t index,
bool in, const void *data, uint16_t size)
{
@@ -15164,6 +15506,8 @@ void packet_hci_isodata(struct timeval *tv, struct ucred *cred, uint16_t index,
bool have_hdr;
uint16_t sn = 0;
uint8_t sflags = 0;
+ uint32_t ts = 0;
+ size_t erased = 0, late = 0;
if (index >= MAX_INDEX) {
print_field("Invalid index (%d).", index);
@@ -15194,7 +15538,8 @@ void packet_hci_isodata(struct timeval *tv, struct ucred *cred, uint16_t index,
if (size < ts_size || !have_hdr)
goto malformed;
- snprintf(ts_str, sizeof(ts_str), " ts %u", get_le32(data));
+ ts = get_le32(data);
+ snprintf(ts_str, sizeof(ts_str), " ts %u", ts);
data += ts_size;
size -= ts_size;
@@ -15223,9 +15568,19 @@ void packet_hci_isodata(struct timeval *tv, struct ucred *cred, uint16_t index,
conn = packet_get_conn_data(handle);
- if (in && have_hdr && conn)
+ if (in && have_hdr && conn) {
+ erased = conn->rx_loss.erased;
+ late = conn->rx_jitter.late;
+
packet_loss_add(&conn->rx_loss, sn, sflags);
+ /* Prefer the controller timestamp over the SN pacing */
+ if (tv && ts_size)
+ packet_jitter_add_ts(&conn->rx_jitter, tv, ts);
+ else if (tv)
+ packet_jitter_add_sn(&conn->rx_jitter, tv, sn);
+ }
+
if (!in && pool->total)
sprintf(handle_str, "Handle %d [%u/%u]%s",
acl_handle(handle), ++pool->tx, pool->total, sn_str);
@@ -15245,16 +15600,16 @@ void packet_hci_isodata(struct timeval *tv, struct ucred *cred, uint16_t index,
print_packet(tv, cred, in ? '>' : '<', index, NULL, COLOR_HCI_ISODATA,
label, handle_str, extra_str);
- if (in && conn) {
+ if (in && have_hdr && conn) {
struct packet_loss *loss = &conn->rx_loss;
+ size_t burst = loss->burst;
- if (loss->lost || loss->dropped || loss->invalid)
- print_field("Lost: %zu/%zu (%zu.%02zu%%) "
- "dropped %zu invalid %zu",
- loss->lost, loss->total,
- loss->lost * 100 / loss->total,
- (loss->lost * 10000 / loss->total) % 100,
- loss->dropped, loss->invalid);
+ /* A burst that this sample has just ended */
+ if (!burst && loss->erased != erased)
+ burst = loss->burst_last;
+
+ print_rx_quality(conn, erased, late, sflags, burst);
+ quality_summary(tv, cred, index, conn, burst);
}
if (!in)
diff --git a/monitor/packet.h b/monitor/packet.h
index 3559f52719cb..c3e3608bdede 100644
--- a/monitor/packet.h
+++ b/monitor/packet.h
@@ -43,6 +43,39 @@ struct packet_loss {
size_t invalid; /* Samples flagged possibly invalid */
size_t dropped; /* Samples flagged as lost data */
size_t total; /* Samples seen, including the lost ones */
+ /* Bursts of erased samples, either missing or flagged as lost */
+ size_t erased; /* Erased samples */
+ size_t burst; /* Length of the burst in progress */
+ size_t burst_max; /* Longest burst */
+ size_t burst_last; /* Length of the last burst ended */
+ size_t bursts; /* Bursts ended so far */
+ size_t burst_hist[5]; /* Burst lengths: 1, 2, 3-5, 6-10, >10 */
+ size_t good_lost; /* Transitions from received to erased */
+ size_t lost_good; /* Transitions from erased to received */
+ bool have_prev;
+ bool prev_erased;
+};
+
+struct packet_jitter {
+ uint32_t interval; /* Nominal interval in usec, 0 if unknown */
+ bool have_prev;
+ struct timeval prev_tv;
+ uint32_t prev_ts;
+ uint16_t prev_sn;
+ uint64_t jitter16; /* RFC 3550 jitter in usec, scaled by 16 */
+ int64_t last_dev; /* Deviation of the last sample in usec */
+ uint64_t max_dev; /* Largest deviation in usec */
+ size_t late; /* Samples deviating more than an interval */
+ struct packet_latency delta; /* Arrival intervals */
+};
+
+/* Window of the periodic quality summary */
+struct packet_quality {
+ struct timeval start;
+ size_t erased;
+ size_t total;
+ size_t burst_max;
+ size_t late;
};
/* Protocols tracked for request and response matching */
@@ -83,6 +116,8 @@ struct packet_conn_data {
struct queue *chan_q;
struct packet_latency tx_l;
struct packet_loss rx_loss;
+ struct packet_jitter rx_jitter;
+ struct packet_quality rx_quality;
struct queue *req_q;
void *data;
void (*destroy)(struct packet_conn_data *conn, void *data);
@@ -92,6 +127,17 @@ struct packet_conn_data *packet_get_conn_data(uint16_t handle);
void packet_latency_add(struct packet_latency *latency, struct timeval *delta);
long long packet_latency_stddev(const struct packet_latency *latency);
void packet_loss_add(struct packet_loss *loss, uint16_t sn, uint8_t sflags);
+size_t packet_loss_burst_max(const struct packet_loss *loss);
+bool packet_loss_burst_ratio(const struct packet_loss *loss, double *p,
+ double *q, double *ratio);
+void packet_loss_print(const struct packet_loss *loss, const char *label);
+void packet_jitter_add_sn(struct packet_jitter *jitter, struct timeval *tv,
+ uint16_t sn);
+void packet_jitter_add_ts(struct packet_jitter *jitter, struct timeval *tv,
+ uint32_t ts);
+void packet_jitter_print(const struct packet_jitter *jitter,
+ const char *label);
+void packet_set_quality_period(unsigned int msec);
void packet_get_context(struct timeval *tv, size_t *num);
void packet_req_add(uint16_t handle, uint16_t cid, uint8_t proto, uint16_t id,
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH BlueZ vRFC 2/7] monitor/analyze: Fix a new connection for each BIS or eSCO packet
2026-10-02 19:27 [PATCH BlueZ vRFC 1/7] monitor: Add audio quality statistics for ISO streams Luiz Augusto von Dentz
@ 2026-10-02 19:27 ` Luiz Augusto von Dentz
2026-10-02 19:27 ` [PATCH BlueZ vRFC 3/7] monitor/analyze: Fix the size of SCO packets Luiz Augusto von Dentz
` (5 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-02 19:27 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
The lookup of a CIS, and of a SCO, allocates a connection when the handle
belongs to another type, so the fallback to BIS, and to eSCO, was never
reached and each packet ended up on a connection of its own.
Fixes: 8157e2fde5d3 ("monitor: Print connection type in Disconnect Complete")
Assisted-by: OpenCode:claude-opus-5.5
---
monitor/analyze.c | 37 +++++++++++++++++++++----------------
1 file changed, 21 insertions(+), 16 deletions(-)
diff --git a/monitor/analyze.c b/monitor/analyze.c
index 13d470ff566d..168efdb4b882 100644
--- a/monitor/analyze.c
+++ b/monitor/analyze.c
@@ -463,6 +463,23 @@ static struct hci_conn *conn_lookup_type(struct hci_dev *dev, uint16_t handle,
return conn;
}
+/*
+ * Look up a connection of either type, only allocating one of the first type
+ * when there is none, so the packets of the second type do not end up on a
+ * new connection each.
+ */
+static struct hci_conn *conn_lookup_types(struct hci_dev *dev,
+ uint16_t handle, uint8_t type1,
+ uint8_t type2)
+{
+ struct hci_conn *conn = conn_lookup(dev, handle);
+
+ if (conn && (conn->type == type1 || conn->type == type2))
+ return conn;
+
+ return conn_lookup_type(dev, handle, type1);
+}
+
static void dev_destroy(void *data)
{
struct hci_dev *dev = data;
@@ -1386,14 +1403,8 @@ static void sco_pkt(struct timeval *tv, uint16_t index, bool out,
dev->num_hci++;
dev->num_sco++;
- conn = conn_lookup_type(dev, le16_to_cpu(hdr->handle) & 0x0fff,
- BTMON_CONN_SCO);
- if (!conn) {
- conn = conn_lookup_type(dev, le16_to_cpu(hdr->handle) & 0x0fff,
- BTMON_CONN_ESCO);
- if (!conn)
- return;
- }
+ conn = conn_lookup_types(dev, le16_to_cpu(hdr->handle) & 0x0fff,
+ BTMON_CONN_SCO, BTMON_CONN_ESCO);
if (out) {
conn_pkt_tx(conn, tv, size - sizeof(*hdr), NULL);
@@ -1481,14 +1492,8 @@ static void iso_pkt(struct timeval *tv, uint16_t index, bool out,
dev->num_hci++;
dev->num_iso++;
- conn = conn_lookup_type(dev, le16_to_cpu(hdr->handle) & 0x0fff,
- BTMON_CONN_CIS);
- if (!conn) {
- conn = conn_lookup_type(dev, le16_to_cpu(hdr->handle) & 0x0fff,
- BTMON_CONN_BIS);
- if (!conn)
- return;
- }
+ conn = conn_lookup_types(dev, le16_to_cpu(hdr->handle) & 0x0fff,
+ BTMON_CONN_CIS, BTMON_CONN_BIS);
handle = le16_to_cpu(hdr->handle);
flags = ISO_FLAGS(handle);
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH BlueZ vRFC 3/7] monitor/analyze: Fix the size of SCO packets
2026-10-02 19:27 [PATCH BlueZ vRFC 1/7] monitor: Add audio quality statistics for ISO streams Luiz Augusto von Dentz
2026-10-02 19:27 ` [PATCH BlueZ vRFC 2/7] monitor/analyze: Fix a new connection for each BIS or eSCO packet Luiz Augusto von Dentz
@ 2026-10-02 19:27 ` Luiz Augusto von Dentz
2026-10-02 19:27 ` [PATCH BlueZ vRFC 4/7] monitor/analyze: Fix the type of synchronous connections Luiz Augusto von Dentz
` (4 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-02 19:27 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
The SCO header is one octet shorter than the ACL one used to strip it, so
the packets were accounted one octet short.
Fixes: dce3971d227d ("monitor/analyze: Fix not calculating latency for ISO/SCO packets")
Assisted-by: OpenCode:claude-opus-5.5
---
monitor/analyze.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/monitor/analyze.c b/monitor/analyze.c
index 168efdb4b882..b132643154f2 100644
--- a/monitor/analyze.c
+++ b/monitor/analyze.c
@@ -1392,7 +1392,7 @@ static void acl_pkt(struct timeval *tv, uint16_t index, bool out,
static void sco_pkt(struct timeval *tv, uint16_t index, bool out,
const void *data, uint16_t size)
{
- const struct bt_hci_acl_hdr *hdr = data;
+ const struct bt_hci_sco_hdr *hdr = data;
struct hci_dev *dev;
struct hci_conn *conn;
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH BlueZ vRFC 4/7] monitor/analyze: Fix the type of synchronous connections
2026-10-02 19:27 [PATCH BlueZ vRFC 1/7] monitor: Add audio quality statistics for ISO streams Luiz Augusto von Dentz
2026-10-02 19:27 ` [PATCH BlueZ vRFC 2/7] monitor/analyze: Fix a new connection for each BIS or eSCO packet Luiz Augusto von Dentz
2026-10-02 19:27 ` [PATCH BlueZ vRFC 3/7] monitor/analyze: Fix the size of SCO packets Luiz Augusto von Dentz
@ 2026-10-02 19:27 ` Luiz Augusto von Dentz
2026-10-02 19:27 ` [PATCH BlueZ vRFC 5/7] monitor: Add audio quality statistics for SCO data Luiz Augusto von Dentz
` (3 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-02 19:27 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
The Link_Type of the Synchronous Connection Complete event was used as
the connection type, so an eSCO connection was tracked as SCO and a SCO
one as of any type.
Fixes: 1f6e606c8504 ("monitor: Add connection tracking for SCO/ISO with -a/--analyze")
Assisted-by: OpenCode:claude-opus-5.5
---
monitor/analyze.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/monitor/analyze.c b/monitor/analyze.c
index b132643154f2..c5bf88216408 100644
--- a/monitor/analyze.c
+++ b/monitor/analyze.c
@@ -1063,7 +1063,9 @@ static void evt_sync_conn_complete(struct hci_dev *dev, struct timeval *tv,
if (evt->status)
return;
- conn = conn_lookup_type(dev, le16_to_cpu(evt->handle), evt->link_type);
+ conn = conn_lookup_type(dev, le16_to_cpu(evt->handle),
+ evt->link_type ? BTMON_CONN_ESCO :
+ BTMON_CONN_SCO);
if (!conn)
return;
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH BlueZ vRFC 5/7] monitor: Add audio quality statistics for SCO data
2026-10-02 19:27 [PATCH BlueZ vRFC 1/7] monitor: Add audio quality statistics for ISO streams Luiz Augusto von Dentz
` (2 preceding siblings ...)
2026-10-02 19:27 ` [PATCH BlueZ vRFC 4/7] monitor/analyze: Fix the type of synchronous connections Luiz Augusto von Dentz
@ 2026-10-02 19:27 ` Luiz Augusto von Dentz
2026-10-02 19:27 ` [PATCH BlueZ vRFC 6/7] monitor: Add ISO link details to the analyze mode Luiz Augusto von Dentz
` (2 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-02 19:27 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Account the Packet_Status_Flag of received SCO data, reported with
Erroneous Data Reporting, as erasures, and the jitter against the time the
data of a packet takes to play, derived from the air mode:
> BR-ESCO: Handle 257 [90:09.. flags 0x01 dlen 120 #1455 [hci0] 19979.296105
Status: Possibly invalid (0x01)
Lost: 0/1 (0.00%) missing 0 dropped 0 invalid 1 partial 0 burst 0
Found BR-ESCO connection with handle 257
...
RX loss: 0/8 (0.00%) missing 0 dropped 0 invalid 1 partial 0
RX jitter: 0.11 msec (max 0.48 msec, late 0)
RX interval: 7.50 msec (7-7 msec, ~7 msec +/- 0 msec)
The loss now reports all the erasures, with the missing samples apart.
Assisted-by: OpenCode:claude-opus-5.5
---
monitor/analyze.c | 15 +++
monitor/packet.c | 287 ++++++++++++++++++++++++++++++++--------------
monitor/packet.h | 6 +
3 files changed, 220 insertions(+), 88 deletions(-)
diff --git a/monitor/analyze.c b/monitor/analyze.c
index c5bf88216408..3a11590a0061 100644
--- a/monitor/analyze.c
+++ b/monitor/analyze.c
@@ -108,6 +108,7 @@ struct hci_conn {
struct hci_stats tx;
struct packet_loss rx_loss;
struct packet_jitter rx_jitter;
+ uint8_t sco_rate;
};
struct hci_conn_tx {
@@ -1072,6 +1073,7 @@ static void evt_sync_conn_complete(struct hci_dev *dev, struct timeval *tv,
memcpy(conn->bdaddr, evt->bdaddr, 6);
conn->frame_connected = frame;
conn->setup_seen = true;
+ conn->sco_rate = packet_sco_rate(evt->air_mode);
}
/* Nominal SDU interval of an unframed stream: ISO Interval over BN */
@@ -1408,6 +1410,19 @@ static void sco_pkt(struct timeval *tv, uint16_t index, bool out,
conn = conn_lookup_types(dev, le16_to_cpu(hdr->handle) & 0x0fff,
BTMON_CONN_SCO, BTMON_CONN_ESCO);
+ if (!out && size > sizeof(*hdr)) {
+ uint8_t status = (le16_to_cpu(hdr->handle) >> 12) & 0x03;
+
+ packet_loss_add_status(&conn->rx_loss, status);
+
+ if (conn->sco_rate) {
+ conn->rx_jitter.interval = hdr->dlen * 1000 /
+ conn->sco_rate;
+ packet_jitter_add(&conn->rx_jitter, tv,
+ conn->rx_jitter.interval);
+ }
+ }
+
if (out) {
conn_pkt_tx(conn, tv, size - sizeof(*hdr), NULL);
} else {
diff --git a/monitor/packet.c b/monitor/packet.c
index 09603c19e05e..1e177d0abd3d 100644
--- a/monitor/packet.c
+++ b/monitor/packet.c
@@ -12076,6 +12076,26 @@ void packet_loss_add(struct packet_loss *loss, uint16_t sn, uint8_t sflags)
loss->have_sn = true;
}
+/* Packet_Status_Flag of SCO data, reported with Erroneous Data Reporting */
+void packet_loss_add_status(struct packet_loss *loss, uint8_t status)
+{
+ loss->total++;
+
+ switch (status) {
+ case 0x01:
+ loss->invalid++;
+ break;
+ case 0x02:
+ loss->dropped++;
+ break;
+ case 0x03:
+ loss->partial++;
+ break;
+ }
+
+ loss_sample(loss, status == 0x02 || status == 0x03, 1);
+}
+
size_t packet_loss_burst_max(const struct packet_loss *loss)
{
return loss->burst_max;
@@ -12113,11 +12133,12 @@ void packet_loss_print(const struct packet_loss *loss, const char *label)
if (!loss->total)
return;
- print_field("%s loss: %zu/%zu (%zu.%02zu%%) dropped %zu invalid %zu",
- label, loss->lost, loss->total,
- loss->lost * 100 / loss->total,
- (loss->lost * 10000 / loss->total) % 100,
- loss->dropped, loss->invalid);
+ print_field("%s loss: %zu/%zu (%zu.%02zu%%) missing %zu dropped %zu "
+ "invalid %zu partial %zu", label, loss->erased,
+ loss->total, loss->erased * 100 / loss->total,
+ (loss->erased * 10000 / loss->total) % 100,
+ loss->lost, loss->dropped, loss->invalid,
+ loss->partial);
/* Account for the burst still in progress */
loss_burst_end(&tmp);
@@ -12168,6 +12189,18 @@ static void jitter_add(struct packet_jitter *jitter, struct timeval *tv,
jitter->jitter16 += (uint64_t)d - ((jitter->jitter16 + 8) >> 4);
}
+void packet_jitter_add(struct packet_jitter *jitter, struct timeval *tv,
+ uint32_t expected)
+{
+ if (!jitter->have_prev) {
+ jitter->prev_tv = *tv;
+ jitter->have_prev = true;
+ return;
+ }
+
+ jitter_add(jitter, tv, expected);
+}
+
void packet_jitter_add_sn(struct packet_jitter *jitter, struct timeval *tv,
uint16_t sn)
{
@@ -12615,11 +12648,18 @@ static void sync_conn_complete_evt(struct timeval *tv, uint16_t index,
print_field("TX packet length: %d", le16_to_cpu(evt->tx_pkt_len));
print_air_mode(evt->air_mode);
- if (evt->status == 0x00)
+ if (evt->status == 0x00) {
+ struct packet_conn_data *conn;
+
assign_handle(index, le16_to_cpu(evt->handle),
evt->link_type ? BTMON_CONN_ESCO :
BTMON_CONN_SCO,
(void *)evt->bdaddr, BDADDR_BREDR);
+
+ conn = packet_get_conn_data(le16_to_cpu(evt->handle));
+ if (conn)
+ conn->sco_rate = packet_sco_rate(evt->air_mode);
+ }
}
static void sync_conn_changed_evt(struct timeval *tv, uint16_t index,
@@ -15337,71 +15377,6 @@ void packet_hci_acldata(struct timeval *tv, struct ucred *cred, uint16_t index,
packet_set_context(NULL, 0);
}
-void packet_hci_scodata(struct timeval *tv, struct ucred *cred, uint16_t index,
- bool in, const void *data, uint16_t size)
-{
- const hci_sco_hdr *hdr = data;
- uint16_t handle = le16_to_cpu(hdr->handle);
- uint8_t flags = acl_flags(handle);
- char label[8];
- char handle_str[42], extra_str[32];
- struct packet_conn_data *conn;
-
- if (index >= MAX_INDEX) {
- print_field("Invalid index (%d).", index);
- return;
- }
-
- index_list[index].frame++;
-
- if (size < HCI_SCO_HDR_SIZE) {
- if (in)
- print_packet(tv, cred, '*', index, NULL, COLOR_ERROR,
- "Malformed SCO Data RX packet", NULL, NULL);
- else
- print_packet(tv, cred, '*', index, NULL, COLOR_ERROR,
- "Malformed SCO Data TX packet", NULL, NULL);
- packet_hexdump(data, size);
- return;
- }
-
- data += HCI_SCO_HDR_SIZE;
- size -= HCI_SCO_HDR_SIZE;
- conn = packet_get_conn_data(handle);
-
- if (index_list[index].sco.total && !in)
- sprintf(handle_str, "Handle %d [%u/%u]", acl_handle(handle),
- index_list[index].sco.total, index_list[index].sco.tx);
- else
- sprintf(handle_str, "Handle %d", acl_handle(handle));
-
- handle_str_append_addr(handle_str, conn);
-
- sprintf(extra_str, "flags 0x%2.2x dlen %d", flags, hdr->dlen);
-
- if (conn)
- sprintf(label, "%s", conn_type_str(conn->type));
- else
- sprintf(label, "SCO");
-
- print_packet(tv, cred, in ? '>' : '<', index, NULL, COLOR_HCI_SCODATA,
- label, handle_str, extra_str);
-
- if (!in)
- packet_enqueue_tx(tv, acl_handle(handle),
- index_list[index].frame, hdr->dlen);
-
- if (size != hdr->dlen) {
- print_text(COLOR_ERROR, "invalid packet size (%d != %d)",
- size, hdr->dlen);
- packet_hexdump(data, size);
- return;
- }
-
- if (filter_mask & PACKET_FILTER_SHOW_SCO_DATA)
- packet_hexdump(data, size);
-}
-
static void print_rx_quality(struct packet_conn_data *conn, size_t erased,
size_t late, uint8_t sflags, size_t burst)
{
@@ -15410,12 +15385,13 @@ static void print_rx_quality(struct packet_conn_data *conn, size_t erased,
/* Only report the samples that revealed a loss or arrived late */
if (loss->erased != erased || sflags)
- print_field("Lost: %zu/%zu (%zu.%02zu%%) dropped %zu "
- "invalid %zu burst %zu",
- loss->lost, loss->total,
- loss->lost * 100 / loss->total,
- (loss->lost * 10000 / loss->total) % 100,
- loss->dropped, loss->invalid, burst);
+ print_field("Lost: %zu/%zu (%zu.%02zu%%) missing %zu "
+ "dropped %zu invalid %zu partial %zu burst %zu",
+ loss->erased, loss->total,
+ loss->erased * 100 / loss->total,
+ (loss->erased * 10000 / loss->total) % 100,
+ loss->lost, loss->dropped, loss->invalid,
+ loss->partial, burst);
if (jitter->late != late)
print_field("Late: +%" PRId64 ".%02" PRId64 " msec "
@@ -15491,6 +15467,150 @@ static void quality_summary(struct timeval *tv, struct ucred *cred,
q->burst_max = loss->burst;
}
+static void rx_quality(struct timeval *tv, struct ucred *cred,
+ uint16_t index, struct packet_conn_data *conn,
+ size_t erased, size_t late, uint8_t status)
+{
+ struct packet_loss *loss = &conn->rx_loss;
+ size_t burst = loss->burst;
+
+ /* A burst that this sample has just ended */
+ if (!burst && loss->erased != erased)
+ burst = loss->burst_last;
+
+ print_rx_quality(conn, erased, late, status, burst);
+ quality_summary(tv, cred, index, conn, burst);
+}
+
+static void sco_rx_quality(struct timeval *tv, struct ucred *cred,
+ uint16_t index, struct packet_conn_data *conn,
+ uint8_t status, uint8_t dlen);
+
+void packet_hci_scodata(struct timeval *tv, struct ucred *cred, uint16_t index,
+ bool in, const void *data, uint16_t size)
+{
+ const hci_sco_hdr *hdr = data;
+ uint16_t handle = le16_to_cpu(hdr->handle);
+ uint8_t flags = acl_flags(handle);
+ char label[8];
+ char handle_str[42], extra_str[32];
+ struct packet_conn_data *conn;
+
+ if (index >= MAX_INDEX) {
+ print_field("Invalid index (%d).", index);
+ return;
+ }
+
+ index_list[index].frame++;
+
+ if (size < HCI_SCO_HDR_SIZE) {
+ if (in)
+ print_packet(tv, cred, '*', index, NULL, COLOR_ERROR,
+ "Malformed SCO Data RX packet", NULL, NULL);
+ else
+ print_packet(tv, cred, '*', index, NULL, COLOR_ERROR,
+ "Malformed SCO Data TX packet", NULL, NULL);
+ packet_hexdump(data, size);
+ return;
+ }
+
+ data += HCI_SCO_HDR_SIZE;
+ size -= HCI_SCO_HDR_SIZE;
+ conn = packet_get_conn_data(handle);
+
+ if (index_list[index].sco.total && !in)
+ sprintf(handle_str, "Handle %d [%u/%u]", acl_handle(handle),
+ index_list[index].sco.total, index_list[index].sco.tx);
+ else
+ sprintf(handle_str, "Handle %d", acl_handle(handle));
+
+ handle_str_append_addr(handle_str, conn);
+
+ sprintf(extra_str, "flags 0x%2.2x dlen %d", flags, hdr->dlen);
+
+ if (conn)
+ sprintf(label, "%s", conn_type_str(conn->type));
+ else
+ sprintf(label, "SCO");
+
+ print_packet(tv, cred, in ? '>' : '<', index, NULL, COLOR_HCI_SCODATA,
+ label, handle_str, extra_str);
+
+ if (!in)
+ packet_enqueue_tx(tv, acl_handle(handle),
+ index_list[index].frame, hdr->dlen);
+
+ if (size != hdr->dlen) {
+ print_text(COLOR_ERROR, "invalid packet size (%d != %d)",
+ size, hdr->dlen);
+ packet_hexdump(data, size);
+ return;
+ }
+
+ if (in && conn)
+ sco_rx_quality(tv, cred, index, conn, flags & 0x03,
+ hdr->dlen);
+
+ if (filter_mask & PACKET_FILTER_SHOW_SCO_DATA)
+ packet_hexdump(data, size);
+}
+
+static const char *sco_status_str(uint8_t status)
+{
+ switch (status) {
+ case 0x01:
+ return "Possibly invalid";
+ case 0x02:
+ return "No data";
+ case 0x03:
+ return "Partially lost";
+ }
+
+ return "Correct";
+}
+
+/* Octets per msec of the host data, as configured by the air mode */
+uint8_t packet_sco_rate(uint8_t air_mode)
+{
+ switch (air_mode) {
+ case 0x00:
+ case 0x01:
+ /* 8 kHz 8-bit logarithmic */
+ return 8;
+ case 0x02:
+ /* 8 kHz 16-bit linear */
+ return 16;
+ case 0x03:
+ /* 64 kb/s transparent: mSBC, LC3-SWB */
+ return 8;
+ }
+
+ return 0;
+}
+
+static void sco_rx_quality(struct timeval *tv, struct ucred *cred,
+ uint16_t index, struct packet_conn_data *conn,
+ uint8_t status, uint8_t dlen)
+{
+ size_t erased = conn->rx_loss.erased;
+ size_t late = conn->rx_jitter.late;
+
+ if (status)
+ print_field("Status: %s (0x%2.2x)", sco_status_str(status),
+ status);
+
+ packet_loss_add_status(&conn->rx_loss, status);
+
+ if (tv && conn->sco_rate) {
+ /* The data of a packet takes that long to play */
+ conn->rx_jitter.interval = dlen * 1000 / conn->sco_rate;
+ packet_jitter_add(&conn->rx_jitter, tv,
+ conn->rx_jitter.interval);
+ }
+
+ rx_quality(tv, cred, index, conn, erased, late, status);
+}
+
void packet_hci_isodata(struct timeval *tv, struct ucred *cred, uint16_t index,
bool in, const void *data, uint16_t size)
{
@@ -15600,17 +15720,8 @@ void packet_hci_isodata(struct timeval *tv, struct ucred *cred, uint16_t index,
print_packet(tv, cred, in ? '>' : '<', index, NULL, COLOR_HCI_ISODATA,
label, handle_str, extra_str);
- if (in && have_hdr && conn) {
- struct packet_loss *loss = &conn->rx_loss;
- size_t burst = loss->burst;
-
- /* A burst that this sample has just ended */
- if (!burst && loss->erased != erased)
- burst = loss->burst_last;
-
- print_rx_quality(conn, erased, late, sflags, burst);
- quality_summary(tv, cred, index, conn, burst);
- }
+ if (in && have_hdr && conn)
+ rx_quality(tv, cred, index, conn, erased, late, sflags);
if (!in)
packet_enqueue_tx(tv, acl_handle(handle),
diff --git a/monitor/packet.h b/monitor/packet.h
index c3e3608bdede..85b7f2ff762c 100644
--- a/monitor/packet.h
+++ b/monitor/packet.h
@@ -42,6 +42,7 @@ struct packet_loss {
size_t lost; /* Samples missing from the SN sequence */
size_t invalid; /* Samples flagged possibly invalid */
size_t dropped; /* Samples flagged as lost data */
+ size_t partial; /* Samples flagged as partially lost */
size_t total; /* Samples seen, including the lost ones */
/* Bursts of erased samples, either missing or flagged as lost */
size_t erased; /* Erased samples */
@@ -118,6 +119,7 @@ struct packet_conn_data {
struct packet_loss rx_loss;
struct packet_jitter rx_jitter;
struct packet_quality rx_quality;
+ uint8_t sco_rate; /* SCO octets per msec, 0 if unknown */
struct queue *req_q;
void *data;
void (*destroy)(struct packet_conn_data *conn, void *data);
@@ -131,6 +133,10 @@ size_t packet_loss_burst_max(const struct packet_loss *loss);
bool packet_loss_burst_ratio(const struct packet_loss *loss, double *p,
double *q, double *ratio);
void packet_loss_print(const struct packet_loss *loss, const char *label);
+void packet_loss_add_status(struct packet_loss *loss, uint8_t status);
+void packet_jitter_add(struct packet_jitter *jitter, struct timeval *tv,
+ uint32_t expected);
+uint8_t packet_sco_rate(uint8_t air_mode);
void packet_jitter_add_sn(struct packet_jitter *jitter, struct timeval *tv,
uint16_t sn);
void packet_jitter_add_ts(struct packet_jitter *jitter, struct timeval *tv,
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH BlueZ vRFC 6/7] monitor: Add ISO link details to the analyze mode
2026-10-02 19:27 [PATCH BlueZ vRFC 1/7] monitor: Add audio quality statistics for ISO streams Luiz Augusto von Dentz
` (3 preceding siblings ...)
2026-10-02 19:27 ` [PATCH BlueZ vRFC 5/7] monitor: Add audio quality statistics for SCO data Luiz Augusto von Dentz
@ 2026-10-02 19:27 ` Luiz Augusto von Dentz
2026-10-02 19:27 ` [PATCH BlueZ vRFC 7/7] monitor: Add audio quality statistics for A2DP streams Luiz Augusto von Dentz
2026-10-02 22:07 ` [BlueZ,vRFC,1/7] monitor: Add audio quality statistics for ISO streams bluez.test.bot
6 siblings, 0 replies; 8+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-02 19:27 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Print the link parameters of CIS and BIS connections along with the last
LE Read ISO Link Quality counters:
Found LE-BIS connection with handle 2560
...
Link: ISO interval 30.00 msec NSE 12 BN 3 PTO 0 IRC 4 latency 28880 usec
...
Link quality: 1 reads
TX unacked 140 flushed 9 last subevent 31 retransmitted 210
RX CRC errors 402 unreceived 12 duplicated 3
With a burst number above one the SDUs of an ISO interval are delivered
together, so pace them by the interval they belong to rather than by the
SDU interval, which reported most of them as jitter. On the BIS above:
- RX jitter: 13.05 msec (max 45.73 msec, late 2556)
+ RX jitter: 0.68 msec (max 30.00 msec, late 0)
Assisted-by: OpenCode:claude-opus-5.5
---
monitor/analyze.c | 73 +++++++++++++++++++++++++++++++++++++++++++++--
monitor/packet.c | 20 +++++++++++--
monitor/packet.h | 2 ++
3 files changed, 89 insertions(+), 6 deletions(-)
diff --git a/monitor/analyze.c b/monitor/analyze.c
index 3a11590a0061..2b5e3087197c 100644
--- a/monitor/analyze.c
+++ b/monitor/analyze.c
@@ -109,6 +109,9 @@ struct hci_conn {
struct packet_loss rx_loss;
struct packet_jitter rx_jitter;
uint8_t sco_rate;
+ size_t lq_num;
+ struct bt_hci_rsp_le_read_iso_link_quality lq;
+ char iso_link[96];
};
struct hci_conn_tx {
@@ -382,9 +385,26 @@ static void conn_destroy(void *data)
print_stats(&conn->rx, "RX");
print_stats(&conn->tx, "TX");
+ if (conn->iso_link[0])
+ print_field("Link: %s", conn->iso_link);
+
packet_loss_print(&conn->rx_loss, "RX");
packet_jitter_print(&conn->rx_jitter, "RX");
+ if (conn->lq_num) {
+ print_field("Link quality: %zu reads", conn->lq_num);
+ print_field(" TX unacked %u flushed %u last subevent %u "
+ "retransmitted %u",
+ le32_to_cpu(conn->lq.tx_unacked_packets),
+ le32_to_cpu(conn->lq.tx_flushed_packets),
+ le32_to_cpu(conn->lq.tx_last_subevent_packets),
+ le32_to_cpu(conn->lq.retransmitted_packets));
+ print_field(" RX CRC errors %u unreceived %u duplicated %u",
+ le32_to_cpu(conn->lq.crc_error_packets),
+ le32_to_cpu(conn->lq.rx_unreceived_packets),
+ le32_to_cpu(conn->lq.duplicated_packets));
+ }
+
if (conn->setup_seen) {
print_field("Connected: #%lu", conn->frame_connected);
if (conn->terminated) {
@@ -902,6 +922,24 @@ static void rsp_read_bd_addr(struct hci_dev *dev, struct timeval *tv,
memcpy(dev->bdaddr, rsp->bdaddr, 6);
}
+static void rsp_le_read_iso_link_quality(struct hci_dev *dev,
+ const void *data, uint16_t size)
+{
+ const struct bt_hci_rsp_le_read_iso_link_quality *rsp = data;
+ struct hci_conn *conn;
+
+ if (size < sizeof(*rsp) || rsp->status)
+ return;
+
+ conn = conn_lookup(dev, le16_to_cpu(rsp->handle));
+ if (!conn)
+ return;
+
+ /* The counters are cumulative so the last read has them all */
+ conn->lq = *rsp;
+ conn->lq_num++;
+}
+
static void evt_cmd_complete(struct hci_dev *dev, struct timeval *tv,
const void *data, uint16_t size)
{
@@ -919,6 +957,9 @@ static void evt_cmd_complete(struct hci_dev *dev, struct timeval *tv,
case BT_HCI_CMD_READ_BD_ADDR:
rsp_read_bd_addr(dev, tv, data, size);
break;
+ case BT_HCI_CMD_LE_READ_ISO_LINK_QUALITY:
+ rsp_le_read_iso_link_quality(dev, data, size);
+ break;
}
}
@@ -1080,9 +1121,22 @@ static void evt_sync_conn_complete(struct hci_dev *dev, struct timeval *tv,
static void set_iso_interval(struct hci_conn *conn, uint16_t interval,
uint8_t bn)
{
- if (bn)
- conn->rx_jitter.interval = (uint32_t)le16_to_cpu(interval) *
- 1250 / bn;
+ if (!bn)
+ return;
+
+ conn->rx_jitter.interval = (uint32_t)le16_to_cpu(interval) * 1250 / bn;
+ conn->rx_jitter.bn = bn;
+}
+
+static void set_bis_link(struct hci_conn *conn, uint16_t interval,
+ uint8_t nse, uint8_t bn, uint8_t pto,
+ uint8_t irc, const uint8_t *latency)
+{
+ snprintf(conn->iso_link, sizeof(conn->iso_link), "ISO interval "
+ "%u.%02u msec NSE %u BN %u PTO %u IRC %u latency %u "
+ "usec", le16_to_cpu(interval) * 125 / 100,
+ le16_to_cpu(interval) * 125 % 100, nse, bn, pto, irc,
+ get_le24(latency));
}
static void evt_le_cis_established(struct hci_dev *dev, struct timeval *tv,
@@ -1110,6 +1164,15 @@ static void evt_le_cis_established(struct hci_dev *dev, struct timeval *tv,
/* Only the Peripheral gets a CIS Request for the handle */
set_iso_interval(conn, evt->interval, link ? evt->c_bn : evt->p_bn);
+
+ snprintf(conn->iso_link, sizeof(conn->iso_link), "ISO interval "
+ "%u.%02u msec NSE %u BN %u/%u FT %u/%u latency %u/%u "
+ "usec (C/P)",
+ le16_to_cpu(evt->interval) * 125 / 100,
+ le16_to_cpu(evt->interval) * 125 % 100, evt->nse,
+ evt->c_bn, evt->p_bn, evt->c_ft, evt->p_ft,
+ get_le24(evt->c_latency),
+ get_le24(evt->p_latency));
}
static void evt_le_cis_req(struct hci_dev *dev, struct timeval *tv,
@@ -1152,6 +1215,8 @@ static void evt_le_big_complete(struct hci_dev *dev, struct timeval *tv,
conn->setup_seen = true;
conn->frame_connected = frame;
set_iso_interval(conn, evt->interval, evt->bn);
+ set_bis_link(conn, evt->interval, evt->nse, evt->bn,
+ evt->pto, evt->irc, evt->latency);
}
}
}
@@ -1179,6 +1244,8 @@ static void evt_le_big_sync_established(struct hci_dev *dev, struct timeval *tv,
conn->setup_seen = true;
conn->frame_connected = frame;
set_iso_interval(conn, evt->interval, evt->bn);
+ set_bis_link(conn, evt->interval, evt->nse, evt->bn,
+ evt->pto, evt->irc, evt->latency);
}
}
}
diff --git a/monitor/packet.c b/monitor/packet.c
index 1e177d0abd3d..c21c79f75971 100644
--- a/monitor/packet.c
+++ b/monitor/packet.c
@@ -12161,7 +12161,7 @@ static void jitter_add(struct packet_jitter *jitter, struct timeval *tv,
int64_t expected)
{
struct timeval delta;
- int64_t arrival, d;
+ int64_t arrival, d, period;
timersub(tv, &jitter->prev_tv, &delta);
jitter->prev_tv = *tv;
@@ -12177,7 +12177,8 @@ static void jitter_add(struct packet_jitter *jitter, struct timeval *tv,
jitter->last_dev = d;
/* A sample arriving an interval or more later than expected */
- if (jitter->interval && d >= jitter->interval)
+ period = (int64_t)jitter->interval * (jitter->bn > 1 ? jitter->bn : 1);
+ if (jitter->interval && d >= period)
jitter->late++;
if (d < 0)
@@ -12212,6 +12213,7 @@ void packet_jitter_add_sn(struct packet_jitter *jitter, struct timeval *tv,
if (!jitter->have_prev) {
jitter->prev_tv = *tv;
jitter->prev_sn = sn;
+ jitter->ext_sn = sn;
jitter->have_prev = true;
return;
}
@@ -12221,7 +12223,18 @@ void packet_jitter_add_sn(struct packet_jitter *jitter, struct timeval *tv,
return;
jitter->prev_sn = sn;
- jitter_add(jitter, tv, (int64_t)diff * jitter->interval);
+ jitter->ext_sn += diff;
+
+ /*
+ * With a burst number above one the samples of an ISO interval are
+ * delivered together, so pace them by the interval they belong to.
+ */
+ if (jitter->bn > 1)
+ jitter_add(jitter, tv, (int64_t)(jitter->ext_sn / jitter->bn -
+ (jitter->ext_sn - diff) / jitter->bn) *
+ jitter->interval * jitter->bn);
+ else
+ jitter_add(jitter, tv, (int64_t)diff * jitter->interval);
}
void packet_jitter_add_ts(struct packet_jitter *jitter, struct timeval *tv,
@@ -13617,6 +13630,7 @@ static void set_iso_interval(uint16_t handle, uint16_t interval, uint8_t bn)
return;
conn->rx_jitter.interval = (uint32_t)le16_to_cpu(interval) * 1250 / bn;
+ conn->rx_jitter.bn = bn;
}
static void le_cis_established_evt(struct timeval *tv, uint16_t index,
diff --git a/monitor/packet.h b/monitor/packet.h
index 85b7f2ff762c..77921af39263 100644
--- a/monitor/packet.h
+++ b/monitor/packet.h
@@ -59,6 +59,8 @@ struct packet_loss {
struct packet_jitter {
uint32_t interval; /* Nominal interval in usec, 0 if unknown */
+ uint8_t bn; /* Samples delivered together, 0 if 1 */
+ uint64_t ext_sn; /* Extended sequence number */
bool have_prev;
struct timeval prev_tv;
uint32_t prev_ts;
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH BlueZ vRFC 7/7] monitor: Add audio quality statistics for A2DP streams
2026-10-02 19:27 [PATCH BlueZ vRFC 1/7] monitor: Add audio quality statistics for ISO streams Luiz Augusto von Dentz
` (4 preceding siblings ...)
2026-10-02 19:27 ` [PATCH BlueZ vRFC 6/7] monitor: Add ISO link details to the analyze mode Luiz Augusto von Dentz
@ 2026-10-02 19:27 ` Luiz Augusto von Dentz
2026-10-02 22:07 ` [BlueZ,vRFC,1/7] monitor: Add audio quality statistics for ISO streams bluez.test.bot
6 siblings, 0 replies; 8+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-02 19:27 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Track the RTP sequence numbers and timestamps of AVDTP media channels, in
both directions, with the clock rate derived from the arrival times. A
pause in the arrivals of a second or more, such as a suspended stream,
restarts the pacing instead of reporting a late packet:
< BR-ACL: Handle 256 [E8:EE:C.. flags 0x02 dlen 602 #15682 [hci0] 141.214796
Channel: 41024 len 598 [PSM 25 mode Basic (0x00)] {chan 3}
Late: +374.81 msec (interval 14.51 msec)
= Quality: A2DP 256 CID 0xa040 [hci0] 36.379012
TX loss: 0/345 (0.00%) burst max 0
TX jitter: 0.12 msec late 0
Found TX L2CAP channel with CID 41024
...
TX RTP: payload type 96 clock 44100 Hz gaps 1
TX loss: 0/14786 (0.00%) missing 0 dropped 0 invalid 0 partial 0
TX jitter: 0.06 msec (max 915.34 msec, late 2)
TX interval: 14.51 msec (0-929 msec, ~14 msec +/- 8 msec)
Assisted-by: OpenCode:claude-opus-5.5
---
doc/btmon.rst | 5 +-
monitor/analyze.c | 13 +++
monitor/avdtp.c | 59 ++++++++++++
monitor/packet.c | 229 +++++++++++++++++++++++++++++++++++++---------
monitor/packet.h | 20 ++++
5 files changed, 283 insertions(+), 43 deletions(-)
diff --git a/doc/btmon.rst b/doc/btmon.rst
index fd19b73b22b3..9383fe74941b 100644
--- a/doc/btmon.rst
+++ b/doc/btmon.rst
@@ -96,8 +96,9 @@ OPTIONS
-Q MSEC, --quality MSEC Print a summary of the received audio stream
quality every MSEC milliseconds: erasures, the
longest burst of erasures, jitter and late
- samples. Only LE Audio isochronous streams are
- currently supported.
+ samples. Covers LE Audio isochronous streams,
+ SCO/eSCO data over HCI and the RTP of A2DP media
+ channels.
-E IP, --ellisys IP Send Ellisys HCI Injection.
diff --git a/monitor/analyze.c b/monitor/analyze.c
index 2b5e3087197c..7e19b61978f9 100644
--- a/monitor/analyze.c
+++ b/monitor/analyze.c
@@ -132,6 +132,7 @@ struct l2cap_chan {
uint8_t mode;
bool out;
struct timeval last_rx;
+ struct packet_rtp rtp;
struct hci_stats rx;
struct hci_stats tx;
};
@@ -301,6 +302,7 @@ static void chan_destroy(void *data)
print_stats(&chan->rx, "RX");
print_stats(&chan->tx, "TX");
+ packet_rtp_print(&chan->rtp, chan->out ? "TX" : "RX");
done:
queue_destroy(chan->rx.plot, free);
@@ -1444,12 +1446,23 @@ static void acl_pkt(struct timeval *tv, uint16_t index, bool out,
switch (le16_to_cpu(hdr->handle) >> 12) {
case 0x00:
case 0x02:
+ /* The start of a frame carries the L2CAP header */
+ if (size < 4)
+ break;
+
cid = get_le16(data + 2);
chan = chan_lookup(conn, cid, out);
if (cid == 1)
l2cap_sig(conn, out, data + 4, size - 4);
else if (cid == 5)
l2cap_le_sig(conn, out, data + 4, size - 4);
+ else if (chan && cid >= 0x0040 &&
+ (chan->psm == 0x0019 || !chan->psm))
+ /*
+ * AVDTP media transport, also attempted on channels
+ * whose setup was not captured.
+ */
+ packet_rtp_add(&chan->rtp, tv, data + 4, size - 4);
break;
}
diff --git a/monitor/avdtp.c b/monitor/avdtp.c
index 9d0016d6bc56..4f08edf5ca95 100644
--- a/monitor/avdtp.c
+++ b/monitor/avdtp.c
@@ -768,6 +768,64 @@ static bool avdtp_signalling_packet(struct avdtp_frame *avdtp_frame)
return true;
}
+#define MAX_STREAM 8
+
+/* Media transport channels, tracked for their RTP quality */
+static struct avdtp_stream {
+ bool used;
+ uint16_t index;
+ uint16_t handle;
+ uint16_t cid;
+ bool in;
+ struct packet_rtp rtp;
+ struct packet_quality q;
+} stream_list[MAX_STREAM];
+
+static struct avdtp_stream *stream_lookup(const struct l2cap_frame *frame)
+{
+ struct avdtp_stream *stream;
+ static unsigned int next;
+ int i;
+
+ for (i = 0; i < MAX_STREAM; i++) {
+ stream = &stream_list[i];
+
+ if (stream->used && stream->index == frame->index &&
+ stream->handle == frame->handle &&
+ stream->cid == frame->cid &&
+ stream->in == frame->in)
+ return stream;
+ }
+
+ /* Take over the oldest stream when all are used */
+ stream = &stream_list[next++ % MAX_STREAM];
+ memset(stream, 0, sizeof(*stream));
+ stream->used = true;
+ stream->index = frame->index;
+ stream->handle = frame->handle;
+ stream->cid = frame->cid;
+ stream->in = frame->in;
+
+ return stream;
+}
+
+static void avdtp_media_packet(const struct l2cap_frame *frame)
+{
+ struct avdtp_stream *stream = stream_lookup(frame);
+ struct timeval tv = frame->tv;
+ size_t erased = stream->rtp.loss.erased;
+ size_t late = stream->rtp.jitter.late;
+
+ if (!packet_rtp_add(&stream->rtp, timerisset(&tv) ? &tv : NULL,
+ frame->data, frame->size))
+ return;
+
+ packet_rtp_quality(timerisset(&tv) ? &tv : NULL, frame->index,
+ frame->handle, frame->cid,
+ frame->in, &stream->rtp, &stream->q, erased,
+ late);
+}
+
void avdtp_packet(const struct l2cap_frame *frame)
{
struct avdtp_frame avdtp_frame;
@@ -780,6 +838,7 @@ void avdtp_packet(const struct l2cap_frame *frame)
ret = avdtp_signalling_packet(&avdtp_frame);
break;
default:
+ avdtp_media_packet(frame);
if (packet_has_filter(PACKET_FILTER_SHOW_A2DP_STREAM))
packet_hexdump(frame->data, frame->size);
return;
diff --git a/monitor/packet.c b/monitor/packet.c
index c21c79f75971..def24c1c47d3 100644
--- a/monitor/packet.c
+++ b/monitor/packet.c
@@ -12256,6 +12256,125 @@ void packet_jitter_add_ts(struct packet_jitter *jitter, struct timeval *tv,
jitter_add(jitter, tv, diff);
}
+static const uint32_t rtp_rates[] = {
+ 8000, 16000, 22050, 24000, 32000, 44100, 48000, 88200, 90000, 96000,
+};
+
+/* Number of samples to derive the RTP clock rate from */
+#define RTP_PROBE 100
+
+/* Pause in the arrivals that restarts the pacing, in seconds */
+#define RTP_GAP_SEC 1
+
+static uint32_t rtp_rate(uint64_t ts, uint64_t usec)
+{
+ uint32_t best = 0;
+ uint64_t rate, best_diff = UINT64_MAX;
+ size_t i;
+
+ if (!usec)
+ return 0;
+
+ rate = ts * 1000000 / usec;
+
+ for (i = 0; i < ARRAY_SIZE(rtp_rates); i++) {
+ uint64_t diff = rate > rtp_rates[i] ? rate - rtp_rates[i] :
+ rtp_rates[i] - rate;
+
+ if (diff < best_diff) {
+ best_diff = diff;
+ best = rtp_rates[i];
+ }
+ }
+
+ return best;
+}
+
+/*
+ * Account a RTP packet, which is recognized by its version and a dynamic
+ * payload type, the latter never matching an AVDTP signal identifier.
+ */
+bool packet_rtp_add(struct packet_rtp *rtp, struct timeval *tv,
+ const void *data, uint16_t size)
+{
+ const uint8_t *hdr = data;
+ uint16_t seq;
+ uint32_t ts, diff;
+ struct timeval delta;
+
+ if (size < 12 || (hdr[0] & 0xc0) != 0x80 || (hdr[1] & 0x7f) < 96)
+ return false;
+
+ seq = get_be16(hdr + 2);
+ ts = get_be32(hdr + 4);
+
+ packet_loss_add(&rtp->loss, seq, 0);
+ rtp->pt = hdr[1] & 0x7f;
+
+ if (!tv)
+ return true;
+
+ if (!rtp->probe++) {
+ rtp->first_tv = *tv;
+ rtp->first_ts = ts;
+ rtp->prev_ts = ts;
+ return true;
+ }
+
+ if (!rtp->rate) {
+ if (rtp->probe < RTP_PROBE)
+ return true;
+
+ timersub(tv, &rtp->first_tv, &delta);
+ rtp->rate = rtp_rate(ts - rtp->first_ts,
+ (uint64_t)delta.tv_sec * 1000000 +
+ delta.tv_usec);
+ if (!rtp->rate)
+ return true;
+
+ rtp->jitter.prev_tv = *tv;
+ rtp->jitter.have_prev = true;
+ rtp->prev_ts = ts;
+ return true;
+ }
+
+ diff = ts - rtp->prev_ts;
+
+ /* Leave the duplicates and the reorders out */
+ if (!diff || diff >= 0x80000000)
+ return true;
+
+ rtp->prev_ts = ts;
+
+ /*
+ * A pause, such as the stream being suspended and started again,
+ * restarts the pacing instead of accounting the packet as late.
+ */
+ timersub(tv, &rtp->jitter.prev_tv, &delta);
+ if (delta.tv_sec >= RTP_GAP_SEC) {
+ rtp->jitter.prev_tv = *tv;
+ rtp->gaps++;
+ return true;
+ }
+
+ /* A packet is late once it misses the time its data takes to play */
+ rtp->jitter.interval = (uint64_t)diff * 1000000 / rtp->rate;
+ jitter_add(&rtp->jitter, tv, rtp->jitter.interval);
+
+ return true;
+}
+
+void packet_rtp_print(const struct packet_rtp *rtp, const char *label)
+{
+ if (!rtp->loss.total)
+ return;
+
+ print_field("%s RTP: payload type %u clock %u Hz gaps %zu", label,
+ rtp->pt, rtp->rate, rtp->gaps);
+ packet_loss_print(&rtp->loss, label);
+ packet_jitter_print(&rtp->jitter, label);
+}
+
void packet_jitter_print(const struct packet_jitter *jitter,
const char *label)
{
@@ -15391,14 +15510,12 @@ void packet_hci_acldata(struct timeval *tv, struct ucred *cred, uint16_t index,
packet_set_context(NULL, 0);
}
-static void print_rx_quality(struct packet_conn_data *conn, size_t erased,
- size_t late, uint8_t sflags, size_t burst)
+static void print_quality(struct packet_loss *loss,
+ struct packet_jitter *jitter, size_t erased,
+ size_t late, uint8_t status, size_t burst)
{
- struct packet_loss *loss = &conn->rx_loss;
- struct packet_jitter *jitter = &conn->rx_jitter;
-
/* Only report the samples that revealed a loss or arrived late */
- if (loss->erased != erased || sflags)
+ if (loss->erased != erased || status)
print_field("Lost: %zu/%zu (%zu.%02zu%%) missing %zu "
"dropped %zu invalid %zu partial %zu burst %zu",
loss->erased, loss->total,
@@ -15421,14 +15538,24 @@ void packet_set_quality_period(unsigned int msec)
quality_period = msec;
}
-static void quality_summary(struct timeval *tv, struct ucred *cred,
- uint16_t index, struct packet_conn_data *conn,
- size_t burst)
+static void quality_window(struct packet_quality *q, struct timeval *tv,
+ struct packet_loss *loss,
+ struct packet_jitter *jitter)
+{
+ q->start = *tv;
+ q->erased = loss->erased;
+ q->total = loss->total;
+ q->late = jitter->late;
+ q->burst_max = loss->burst;
+}
+
+static void quality_summary(struct timeval *tv, uint16_t index,
+ const char *name, const char *dir,
+ struct packet_quality *q,
+ struct packet_loss *loss,
+ struct packet_jitter *jitter,
+ struct packet_latency *tx_l, size_t burst)
{
- struct packet_quality *q = &conn->rx_quality;
- struct packet_loss *loss = &conn->rx_loss;
- struct packet_jitter *jitter = &conn->rx_jitter;
- char str[32];
struct timeval delta;
size_t erased, total;
uint64_t j;
@@ -15440,10 +15567,7 @@ static void quality_summary(struct timeval *tv, struct ucred *cred,
q->burst_max = burst;
if (!timerisset(&q->start)) {
- q->start = *tv;
- q->erased = loss->erased;
- q->total = loss->total;
- q->late = jitter->late;
+ quality_window(q, tv, loss, jitter);
return;
}
@@ -15455,45 +15579,68 @@ static void quality_summary(struct timeval *tv, struct ucred *cred,
total = loss->total - q->total;
j = jitter->jitter16 >> 4;
- snprintf(str, sizeof(str), "%s %u", conn_type_str(conn->type),
- conn->handle);
- print_packet(tv, cred, '=', index, NULL, COLOR_SYSTEM_NOTE,
- "Quality", str, NULL);
+ print_packet(tv, NULL, '=', index, NULL, COLOR_SYSTEM_NOTE,
+ "Quality", name, NULL);
- print_field("RX loss: %zu/%zu (%zu.%02zu%%) burst max %zu", erased,
- total, total ? erased * 100 / total : 0,
+ print_field("%s loss: %zu/%zu (%zu.%02zu%%) burst max %zu", dir,
+ erased, total, total ? erased * 100 / total : 0,
total ? (erased * 10000 / total) % 100 : 0,
q->burst_max);
if (jitter->delta.count)
- print_field("RX jitter: %" PRIu64 ".%02" PRIu64 " msec "
- "late %zu", j / 1000, (j % 1000) / 10,
+ print_field("%s jitter: %" PRIu64 ".%02" PRIu64 " msec "
+ "late %zu", dir, j / 1000, (j % 1000) / 10,
jitter->late - q->late);
- if (conn->tx_l.count)
- print_field("TX latency: ~%lld msec",
- TV_MSEC(conn->tx_l.med));
+ if (tx_l && tx_l->count)
+ print_field("TX latency: ~%lld msec", TV_MSEC(tx_l->med));
- q->start = *tv;
- q->erased = loss->erased;
- q->total = loss->total;
- q->late = jitter->late;
- q->burst_max = loss->burst;
+ quality_window(q, tv, loss, jitter);
}
-static void rx_quality(struct timeval *tv, struct ucred *cred,
- uint16_t index, struct packet_conn_data *conn,
- size_t erased, size_t late, uint8_t status)
+static void stream_quality(struct timeval *tv, uint16_t index,
+ const char *name, const char *dir,
+ struct packet_quality *q,
+ struct packet_loss *loss,
+ struct packet_jitter *jitter,
+ struct packet_latency *tx_l, size_t erased,
+ size_t late, uint8_t status)
{
- struct packet_loss *loss = &conn->rx_loss;
size_t burst = loss->burst;
/* A burst that this sample has just ended */
if (!burst && loss->erased != erased)
burst = loss->burst_last;
- print_rx_quality(conn, erased, late, status, burst);
- quality_summary(tv, cred, index, conn, burst);
+ print_quality(loss, jitter, erased, late, status, burst);
+ quality_summary(tv, index, name, dir, q, loss, jitter, tx_l, burst);
+}
+
+static void rx_quality(struct timeval *tv, uint16_t index,
+ struct packet_conn_data *conn, size_t erased,
+ size_t late, uint8_t status)
+{
+ char name[32];
+
+ snprintf(name, sizeof(name), "%s %u", conn_type_str(conn->type),
+ conn->handle);
+
+ stream_quality(tv, index, name, "RX", &conn->rx_quality,
+ &conn->rx_loss, &conn->rx_jitter, &conn->tx_l,
+ erased, late, status);
+}
+
+void packet_rtp_quality(struct timeval *tv, uint16_t index, uint16_t handle,
+ uint16_t cid, bool in, struct packet_rtp *rtp,
+ struct packet_quality *q, size_t erased,
+ size_t late)
+{
+ char name[32];
+
+ snprintf(name, sizeof(name), "A2DP %u CID 0x%4.4x", handle, cid);
+
+ stream_quality(tv, index, name, in ? "RX" : "TX", q, &rtp->loss,
+ &rtp->jitter, NULL, erased, late, 0);
}
static void sco_rx_quality(struct timeval *tv, struct ucred *cred,
@@ -15622,7 +15769,7 @@ static void sco_rx_quality(struct timeval *tv, struct ucred *cred,
conn->rx_jitter.interval);
}
- rx_quality(tv, cred, index, conn, erased, late, status);
+ rx_quality(tv, index, conn, erased, late, status);
}
void packet_hci_isodata(struct timeval *tv, struct ucred *cred, uint16_t index,
@@ -15735,7 +15882,7 @@ void packet_hci_isodata(struct timeval *tv, struct ucred *cred, uint16_t index,
label, handle_str, extra_str);
if (in && have_hdr && conn)
- rx_quality(tv, cred, index, conn, erased, late, sflags);
+ rx_quality(tv, index, conn, erased, late, sflags);
if (!in)
packet_enqueue_tx(tv, acl_handle(handle),
diff --git a/monitor/packet.h b/monitor/packet.h
index 77921af39263..3c57fa3f8e34 100644
--- a/monitor/packet.h
+++ b/monitor/packet.h
@@ -72,6 +72,19 @@ struct packet_jitter {
struct packet_latency delta; /* Arrival intervals */
};
+/* RTP media stream, the clock rate is derived from the arrival times */
+struct packet_rtp {
+ struct packet_loss loss;
+ struct packet_jitter jitter;
+ uint32_t rate; /* Clock rate in Hz, 0 until known */
+ size_t probe; /* Samples taken to derive the rate */
+ struct timeval first_tv;
+ uint32_t first_ts;
+ uint32_t prev_ts;
+ uint8_t pt; /* Payload type */
+ size_t gaps; /* Pauses, such as a suspended stream */
+};
+
/* Window of the periodic quality summary */
struct packet_quality {
struct timeval start;
@@ -145,6 +158,13 @@ void packet_jitter_add_ts(struct packet_jitter *jitter, struct timeval *tv,
uint32_t ts);
void packet_jitter_print(const struct packet_jitter *jitter,
const char *label);
+bool packet_rtp_add(struct packet_rtp *rtp, struct timeval *tv,
+ const void *data, uint16_t size);
+void packet_rtp_print(const struct packet_rtp *rtp, const char *label);
+void packet_rtp_quality(struct timeval *tv, uint16_t index, uint16_t handle,
+ uint16_t cid, bool in, struct packet_rtp *rtp,
+ struct packet_quality *q, size_t erased,
+ size_t late);
void packet_set_quality_period(unsigned int msec);
void packet_get_context(struct timeval *tv, size_t *num);
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* RE: [BlueZ,vRFC,1/7] monitor: Add audio quality statistics for ISO streams
2026-10-02 19:27 [PATCH BlueZ vRFC 1/7] monitor: Add audio quality statistics for ISO streams Luiz Augusto von Dentz
` (5 preceding siblings ...)
2026-10-02 19:27 ` [PATCH BlueZ vRFC 7/7] monitor: Add audio quality statistics for A2DP streams Luiz Augusto von Dentz
@ 2026-10-02 22:07 ` bluez.test.bot
6 siblings, 0 replies; 8+ messages in thread
From: bluez.test.bot @ 2026-10-02 22:07 UTC (permalink / raw)
To: linux-bluetooth, luiz.dentz
[-- Attachment #1: Type: text/plain, Size: 5963 bytes --]
This is automated email and please do not reply to this email!
Dear submitter,
Thank you for submitting the patches to the linux bluetooth mailing list.
This is a CI test results with your patch series:
PW Link:https://patchwork.kernel.org/series/1178559/
---Test result---
Test Summary:
CheckPatch FAIL 4.07 seconds
GitLint FAIL 1.59 seconds
BuildEll PASS 19.30 seconds
BluezMake PASS 372.71 seconds
MakeCheck PASS 14.45 seconds
MakeDistcheck PASS 139.51 seconds
CheckValgrind PASS 228.69 seconds
CheckSmatch WARNING 288.24 seconds
bluezmakeextell PASS 92.37 seconds
TestFunctional PASS 1112.13 seconds
IncrementalBuild PASS 424.40 seconds
ScanBuild PASS 1110.12 seconds
Details
##############################
Test: CheckPatch - FAIL
Desc: Run checkpatch.pl script
Output:
[BlueZ,vRFC,1/7] monitor: Add audio quality statistics for ISO streams
WARNING:COMMIT_LOG_LONG_LINE: Prefer a maximum 75 chars per line (possible unwrapped commit description?)
#105:
> LE-CIS: Handle 2304 SN... flags 0x02 dlen 4 sflags 2 #302 [hci0] 83.728300
/home/runner/work/bluez/bluez/src/patch/14864145.patch total: 0 errors, 1 warnings, 682 lines checked
NOTE: For some of the reported defects, checkpatch may be able to
mechanically convert to the typical style using --fix or --fix-inplace.
/home/runner/work/bluez/bluez/src/patch/14864145.patch has style problems, please review.
NOTE: Ignored message types: COMMIT_MESSAGE COMPLEX_MACRO CONST_STRUCT FILE_PATH_CHANGES MISSING_SIGN_OFF PREFER_PACKED SPDX_LICENSE_TAG SPLIT_STRING SSCANF_TO_KSTRTO
NOTE: If any of the errors are false positives, please report
them to the maintainer, see CHECKPATCH in MAINTAINERS.
[BlueZ,vRFC,5/7] monitor: Add audio quality statistics for SCO data
WARNING:COMMIT_LOG_LONG_LINE: Prefer a maximum 75 chars per line (possible unwrapped commit description?)
#106:
> BR-ESCO: Handle 257 [90:09.. flags 0x01 dlen 120 #1455 [hci0] 19979.296105
/home/runner/work/bluez/bluez/src/patch/14864148.patch total: 0 errors, 1 warnings, 396 lines checked
NOTE: For some of the reported defects, checkpatch may be able to
mechanically convert to the typical style using --fix or --fix-inplace.
/home/runner/work/bluez/bluez/src/patch/14864148.patch has style problems, please review.
NOTE: Ignored message types: COMMIT_MESSAGE COMPLEX_MACRO CONST_STRUCT FILE_PATH_CHANGES MISSING_SIGN_OFF PREFER_PACKED SPDX_LICENSE_TAG SPLIT_STRING SSCANF_TO_KSTRTO
NOTE: If any of the errors are false positives, please report
them to the maintainer, see CHECKPATCH in MAINTAINERS.
[BlueZ,vRFC,6/7] monitor: Add ISO link details to the analyze mode
WARNING:COMMIT_LOG_WRONG_LINK: 'Link:' should be followed by a public http(s) link
#107:
Link: ISO interval 30.00 msec NSE 12 BN 3 PTO 0 IRC 4 latency 28880 usec
/home/runner/work/bluez/bluez/src/patch/14864149.patch total: 0 errors, 1 warnings, 182 lines checked
NOTE: For some of the reported defects, checkpatch may be able to
mechanically convert to the typical style using --fix or --fix-inplace.
/home/runner/work/bluez/bluez/src/patch/14864149.patch has style problems, please review.
NOTE: Ignored message types: COMMIT_MESSAGE COMPLEX_MACRO CONST_STRUCT FILE_PATH_CHANGES MISSING_SIGN_OFF PREFER_PACKED SPDX_LICENSE_TAG SPLIT_STRING SSCANF_TO_KSTRTO
NOTE: If any of the errors are false positives, please report
them to the maintainer, see CHECKPATCH in MAINTAINERS.
[BlueZ,vRFC,7/7] monitor: Add audio quality statistics for A2DP streams
WARNING:COMMIT_LOG_LONG_LINE: Prefer a maximum 75 chars per line (possible unwrapped commit description?)
#107:
< BR-ACL: Handle 256 [E8:EE:C.. flags 0x02 dlen 602 #15682 [hci0] 141.214796
/home/runner/work/bluez/bluez/src/patch/14864150.patch total: 0 errors, 1 warnings, 442 lines checked
NOTE: For some of the reported defects, checkpatch may be able to
mechanically convert to the typical style using --fix or --fix-inplace.
/home/runner/work/bluez/bluez/src/patch/14864150.patch has style problems, please review.
NOTE: Ignored message types: COMMIT_MESSAGE COMPLEX_MACRO CONST_STRUCT FILE_PATH_CHANGES MISSING_SIGN_OFF PREFER_PACKED SPDX_LICENSE_TAG SPLIT_STRING SSCANF_TO_KSTRTO
NOTE: If any of the errors are false positives, please report
them to the maintainer, see CHECKPATCH in MAINTAINERS.
##############################
Test: GitLint - FAIL
Desc: Run gitlint
Output:
[BlueZ,vRFC,2/7] monitor/analyze: Fix a new connection for each BIS or eSCO packet
1: T1 Title exceeds max length (82>80): "[BlueZ,vRFC,2/7] monitor/analyze: Fix a new connection for each BIS or eSCO packet"
##############################
Test: CheckSmatch - WARNING
Desc: Run smatch tool with source
Output:
monitor/packet.c:2290:26: warning: Variable length array is used.monitor/packet.c: note: in included file:monitor/bt.h:3924:52: warning: array of flexible structuresmonitor/bt.h:3912:40: warning: array of flexible structuresmonitor/packet.c:2290:26: warning: Variable length array is used.monitor/packet.c: note: in included file:monitor/bt.h:3924:52: warning: array of flexible structuresmonitor/bt.h:3912:40: warning: array of flexible structuresmonitor/packet.c:2290:26: warning: Variable length array is used.monitor/packet.c: note: in included file:monitor/bt.h:3924:52: warning: array of flexible structuresmonitor/bt.h:3912:40: warning: array of flexible structuresmonitor/packet.c:2290:26: warning: Variable length array is used.monitor/packet.c: note: in included file:monitor/bt.h:3924:52: warning: array of flexible structuresmonitor/bt.h:3912:40: warning: array of flexible structures
https://github.com/bluez/bluez/pull/2623
---
Regards,
Linux Bluetooth
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-10-02 22:07 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-02 19:27 [PATCH BlueZ vRFC 1/7] monitor: Add audio quality statistics for ISO streams Luiz Augusto von Dentz
2026-10-02 19:27 ` [PATCH BlueZ vRFC 2/7] monitor/analyze: Fix a new connection for each BIS or eSCO packet Luiz Augusto von Dentz
2026-10-02 19:27 ` [PATCH BlueZ vRFC 3/7] monitor/analyze: Fix the size of SCO packets Luiz Augusto von Dentz
2026-10-02 19:27 ` [PATCH BlueZ vRFC 4/7] monitor/analyze: Fix the type of synchronous connections Luiz Augusto von Dentz
2026-10-02 19:27 ` [PATCH BlueZ vRFC 5/7] monitor: Add audio quality statistics for SCO data Luiz Augusto von Dentz
2026-10-02 19:27 ` [PATCH BlueZ vRFC 6/7] monitor: Add ISO link details to the analyze mode Luiz Augusto von Dentz
2026-10-02 19:27 ` [PATCH BlueZ vRFC 7/7] monitor: Add audio quality statistics for A2DP streams Luiz Augusto von Dentz
2026-10-02 22:07 ` [BlueZ,vRFC,1/7] monitor: Add audio quality statistics for ISO streams bluez.test.bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox