diff -Nru pulseaudio-16.1+dfsg1/debian/changelog pulseaudio-16.1+dfsg1/debian/changelog --- pulseaudio-16.1+dfsg1/debian/changelog 2024-01-08 16:52:43.000000000 +0800 +++ pulseaudio-16.1+dfsg1/debian/changelog 2024-02-01 13:09:27.000000000 +0800 @@ -1,3 +1,10 @@ +pulseaudio (1:16.1+dfsg1-2ubuntu7) noble; urgency=medium + + * d/p/0001-backend-native-Handle-multi-AT-commands-in-a-buffer.patch + (LP: #2051895) + + -- Hui Wang Thu, 01 Feb 2024 13:09:27 +0800 + pulseaudio (1:16.1+dfsg1-2ubuntu6) noble; urgency=medium * Cherry-pick change from 16.1+dfsg1-3 diff -Nru pulseaudio-16.1+dfsg1/debian/patches/0001-backend-native-Handle-multi-AT-commands-in-a-buffer.patch pulseaudio-16.1+dfsg1/debian/patches/0001-backend-native-Handle-multi-AT-commands-in-a-buffer.patch --- pulseaudio-16.1+dfsg1/debian/patches/0001-backend-native-Handle-multi-AT-commands-in-a-buffer.patch 1970-01-01 08:00:00.000000000 +0800 +++ pulseaudio-16.1+dfsg1/debian/patches/0001-backend-native-Handle-multi-AT-commands-in-a-buffer.patch 2024-02-01 13:09:10.000000000 +0800 @@ -0,0 +1,234 @@ +From d7dc04e8f5c404b1fa16409f69dcde7c56312f02 Mon Sep 17 00:00:00 2001 +From: Hui Wang +Date: Mon, 25 Dec 2023 19:42:07 +0800 +Subject: [PATCH] backend-native: Handle multi AT commands in a buffer + +When we connect Lenovo XT99 bt headset in the Ubuntu 22.04, this +headset could only work in A2DP profile, couldn't work in HFP profile +with a high chance. + +This headset supports mSBC, after pulseaudio replies "+BCS:2" to +headset, we expect to receive a "AT+BCS=2\r" from the headset, but +with a high chance, it will receive 2 AT commands in a buffer like +this "AT+CHLD=?\rAT+BCS=2\r", and we also observed other 2 AT commands +in a buffer like this "AT+NREC=0\rAT+CGMI?\r". + +Here we don't suppose there is only one AT command in a buffer, we +will find each command by the delimiter "\r" and handle each command +by sequence. + +Signed-off-by: Hui Wang +Part-of: +--- + src/modules/bluetooth/backend-native.c | 176 +++++++++++++------------ + 1 file changed, 95 insertions(+), 81 deletions(-) + +diff --git a/src/modules/bluetooth/backend-native.c b/src/modules/bluetooth/backend-native.c +index 829d7bf82..263736ad4 100644 +--- a/src/modules/bluetooth/backend-native.c ++++ b/src/modules/bluetooth/backend-native.c +@@ -889,106 +889,120 @@ static void rfcomm_io_callback(pa_mainloop_api *io, pa_io_event *e, int fd, pa_i + } + + if (events & PA_IO_EVENT_INPUT) { +- char buf[512]; ++ char rbuf[512]; + ssize_t len; + int gain, dummy; + bool do_reply = false; + int vendor, product, version, features; ++ char *buf = rbuf; + int num; + +- len = pa_read(fd, buf, 511, NULL); ++ len = pa_read(fd, rbuf, 511, NULL); + if (len < 0) { + pa_log_error("RFCOMM read error: %s", pa_cstrerror(errno)); + goto fail; + } +- buf[len] = 0; +- pa_log_debug("RFCOMM << %s", buf); +- +- /* There are only four HSP AT commands: +- * AT+VGS=value: value between 0 and 15, sent by the HS to AG to set the speaker gain. +- * +VGS=value is sent by AG to HS as a response to an AT+VGS command or when the gain +- * is changed on the AG side. +- * AT+VGM=value: value between 0 and 15, sent by the HS to AG to set the microphone gain. +- * +VGM=value is sent by AG to HS as a response to an AT+VGM command or when the gain +- * is changed on the AG side. +- * AT+CKPD=200: Sent by HS when headset button is pressed. +- * RING: Sent by AG to HS to notify of an incoming call. It can safely be ignored because +- * it does not expect a reply. */ +- if (sscanf(buf, "AT+VGS=%d", &gain) == 1 || sscanf(buf, "\r\n+VGM%*[=:]%d\r\n", &gain) == 1) { +- if (!t->set_sink_volume) { +- pa_log_debug("HS/HF peer supports speaker gain control"); +- t->set_sink_volume = set_sink_volume; +- } ++ rbuf[len] = 0; ++ pa_log_debug("RFCOMM << %s", rbuf); ++ ++ while (buf[0]) { ++ /* There are only four HSP AT commands: ++ * AT+VGS=value: value between 0 and 15, sent by the HS to AG to set the speaker gain. ++ * +VGS=value is sent by AG to HS as a response to an AT+VGS command or when the gain ++ * is changed on the AG side. ++ * AT+VGM=value: value between 0 and 15, sent by the HS to AG to set the microphone gain. ++ * +VGM=value is sent by AG to HS as a response to an AT+VGM command or when the gain ++ * is changed on the AG side. ++ * AT+CKPD=200: Sent by HS when headset button is pressed. ++ * RING: Sent by AG to HS to notify of an incoming call. It can safely be ignored because ++ * it does not expect a reply. */ ++ if (sscanf(buf, "AT+VGS=%d", &gain) == 1 || sscanf(buf, "\r\n+VGM%*[=:]%d\r\n", &gain) == 1) { ++ if (!t->set_sink_volume) { ++ pa_log_debug("HS/HF peer supports speaker gain control"); ++ t->set_sink_volume = set_sink_volume; ++ } + +- t->sink_volume = hsp_gain_to_volume(gain); +- pa_hook_fire(pa_bluetooth_discovery_hook(t->device->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_SINK_VOLUME_CHANGED), t); +- do_reply = true; ++ t->sink_volume = hsp_gain_to_volume(gain); ++ pa_hook_fire(pa_bluetooth_discovery_hook(t->device->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_SINK_VOLUME_CHANGED), t); ++ do_reply = true; + +- } else if (sscanf(buf, "AT+VGM=%d", &gain) == 1 || sscanf(buf, "\r\n+VGS%*[=:]%d\r\n", &gain) == 1) { +- if (!t->set_source_volume) { +- pa_log_debug("HS/HF peer supports microphone gain control"); +- t->set_source_volume = set_source_volume; +- } +- +- t->source_volume = hsp_gain_to_volume(gain); +- pa_hook_fire(pa_bluetooth_discovery_hook(t->device->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_SOURCE_VOLUME_CHANGED), t); +- do_reply = true; +- } else if (sscanf(buf, "AT+CKPD=%d", &dummy) == 1) { +- do_reply = true; +- } else if (sscanf(buf, "AT+XAPL=%04x-%04x-%04x,%d", &vendor, &product, &version, &features) == 4) { +- if (features & 0x2) +- /* claim, that we support battery status reports */ +- rfcomm_write_response(fd, "+XAPL=iPhone,6"); +- do_reply = true; +- } else if (sscanf(buf, "AT+IPHONEACCEV=%d", &num) == 1) { +- char *substr = buf, *keystr; +- int key, val, i; +- +- do_reply = true; +- +- for (i = 0; i < num; ++i) { +- keystr = strchr(substr, ','); +- if (!keystr) { +- pa_log_warn("%s misses key for argument #%d", buf, i); +- do_reply = false; +- break; +- } +- keystr++; +- substr = strchr(keystr, ','); +- if (!substr) { +- pa_log_warn("%s misses value for argument #%d", buf, i); +- do_reply = false; +- break; ++ } else if (sscanf(buf, "AT+VGM=%d", &gain) == 1 || sscanf(buf, "\r\n+VGS%*[=:]%d\r\n", &gain) == 1) { ++ if (!t->set_source_volume) { ++ pa_log_debug("HS/HF peer supports microphone gain control"); ++ t->set_source_volume = set_source_volume; + } +- substr++; + +- key = atoi(keystr); +- val = atoi(substr); +- +- switch (key) { +- case 1: +- pa_log_notice("Battery Level: %d0%%", val + 1); +- pa_bluetooth_device_report_battery_level(t->device, (val + 1) * 10, "Apple accessory indication"); +- break; +- case 2: +- pa_log_notice("Dock Status: %s", val ? "docked" : "undocked"); ++ t->source_volume = hsp_gain_to_volume(gain); ++ pa_hook_fire(pa_bluetooth_discovery_hook(t->device->discovery, PA_BLUETOOTH_HOOK_TRANSPORT_SOURCE_VOLUME_CHANGED), t); ++ do_reply = true; ++ } else if (sscanf(buf, "AT+CKPD=%d", &dummy) == 1) { ++ do_reply = true; ++ } else if (sscanf(buf, "AT+XAPL=%04x-%04x-%04x,%d", &vendor, &product, &version, &features) == 4) { ++ if (features & 0x2) ++ /* claim, that we support battery status reports */ ++ rfcomm_write_response(fd, "+XAPL=iPhone,6"); ++ do_reply = true; ++ } else if (sscanf(buf, "AT+IPHONEACCEV=%d", &num) == 1) { ++ char *substr = buf, *keystr; ++ int key, val, i; ++ ++ do_reply = true; ++ ++ for (i = 0; i < num; ++i) { ++ keystr = strchr(substr, ','); ++ if (!keystr) { ++ pa_log_warn("%s misses key for argument #%d", buf, i); ++ do_reply = false; + break; +- default: +- pa_log_debug("Unexpected IPHONEACCEV key %#x", key); ++ } ++ keystr++; ++ substr = strchr(keystr, ','); ++ if (!substr) { ++ pa_log_warn("%s misses value for argument #%d", buf, i); ++ do_reply = false; + break; ++ } ++ substr++; ++ ++ key = atoi(keystr); ++ val = atoi(substr); ++ ++ switch (key) { ++ case 1: ++ pa_log_notice("Battery Level: %d0%%", val + 1); ++ pa_bluetooth_device_report_battery_level(t->device, (val + 1) * 10, "Apple accessory indication"); ++ break; ++ case 2: ++ pa_log_notice("Dock Status: %s", val ? "docked" : "undocked"); ++ break; ++ default: ++ pa_log_debug("Unexpected IPHONEACCEV key %#x", key); ++ break; ++ } + } +- } +- if (!do_reply) ++ if (!do_reply) ++ rfcomm_write_response(fd, "ERROR"); ++ } else if (t->config) { /* t->config is only non-null for hfp profile */ ++ do_reply = hfp_rfcomm_handle(fd, t, buf); ++ } else { + rfcomm_write_response(fd, "ERROR"); +- } else if (t->config) { /* t->config is only non-null for hfp profile */ +- do_reply = hfp_rfcomm_handle(fd, t, buf); +- } else { +- rfcomm_write_response(fd, "ERROR"); +- do_reply = false; +- } ++ do_reply = false; ++ } + +- if (do_reply) +- rfcomm_write_response(fd, "OK"); ++ if (do_reply) ++ rfcomm_write_response(fd, "OK"); ++ ++ if (buf[0] == '\r') /* in case it is the command with format \r\nCOMMAND\r\n, skip the starting \r */ ++ buf = buf + 1; ++ ++ buf = strstr(buf, "\r"); /* try to find the next AT command in the buf */ ++ if (!buf) ++ break; ++ else if (buf[1] == '\n') ++ buf = buf + 2; /* skip \r\n */ ++ else ++ buf = buf + 1; /* skip \r */ ++ } + } + + return; +-- +2.34.1 + diff -Nru pulseaudio-16.1+dfsg1/debian/patches/series pulseaudio-16.1+dfsg1/debian/patches/series --- pulseaudio-16.1+dfsg1/debian/patches/series 2023-02-15 09:44:55.000000000 +0800 +++ pulseaudio-16.1+dfsg1/debian/patches/series 2024-02-01 13:09:23.000000000 +0800 @@ -17,3 +17,4 @@ 0016-check_off_profile_when_switching_profile.patch 0001-card-restore-setting-preferred-ports-in-entry_from_c.patch 0015-bluetooth-Amend-writeout-to-send-more-initial-frames.patch +0001-backend-native-Handle-multi-AT-commands-in-a-buffer.patch