Linux bluetooth development
 help / color / mirror / Atom feed
From: Luiz Augusto von Dentz <luiz.dentz@gmail.com>
To: linux-bluetooth@vger.kernel.org
Subject: [PATCH BlueZ vRFC 5/7] monitor: Add audio quality statistics for SCO data
Date: Fri,  2 Oct 2026 15:27:57 -0400	[thread overview]
Message-ID: <20261002192759.3071262-5-luiz.dentz@gmail.com> (raw)
In-Reply-To: <20261002192759.3071262-1-luiz.dentz@gmail.com>

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


  parent reply	other threads:[~2026-10-02 19:28 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 ` Luiz Augusto von Dentz [this message]
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

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261002192759.3071262-5-luiz.dentz@gmail.com \
    --to=luiz.dentz@gmail.com \
    --cc=linux-bluetooth@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox