* [PATCH BlueZ v2 1/4] shared/hfp: Add HF SLC connection function
@ 2025-08-20 13:33 Frédéric Danis
2025-08-20 13:33 ` [PATCH BlueZ v2 2/4] unit/test-hfp: Add SLC connection test Frédéric Danis
` (4 more replies)
0 siblings, 5 replies; 6+ messages in thread
From: Frédéric Danis @ 2025-08-20 13:33 UTC (permalink / raw)
To: linux-bluetooth
This implements the minimal SLC connection exchange, i.e. AT+BRSF,
AT+CIND=?, AT+CIND? and AT+CMER=3,0,0,1 requested to complete the
Service Level Connection Establishment.
---
src/shared/hfp.c | 508 +++++++++++++++++++++++++++++++++++++++++++++++
src/shared/hfp.h | 69 +++++++
2 files changed, 577 insertions(+)
diff --git a/src/shared/hfp.c b/src/shared/hfp.c
index df6eab35d..c1bcb61cf 100644
--- a/src/shared/hfp.c
+++ b/src/shared/hfp.c
@@ -25,6 +25,12 @@
#include "src/shared/io.h"
#include "src/shared/hfp.h"
+#define DBG(_hfp, fmt, arg...) \
+ hfp_debug(_hfp->debug_callback, _hfp->debug_data, "%s:%s() " fmt, \
+ __FILE__, __func__, ## arg)
+
+#define HFP_HF_FEATURES (HFP_HF_FEAT_ESCO_S4_T2)
+
struct hfp_gw {
int ref_count;
int fd;
@@ -50,6 +56,16 @@ struct hfp_gw {
bool destroyed;
};
+typedef void (*ciev_func_t)(uint8_t val, void *user_data);
+
+struct indicator {
+ uint8_t index;
+ uint32_t min;
+ uint32_t max;
+ uint32_t val;
+ ciev_func_t cb;
+};
+
struct hfp_hf {
int ref_count;
int fd;
@@ -73,6 +89,17 @@ struct hfp_hf {
bool in_disconnect;
bool destroyed;
+
+ struct hfp_hf_callbacks *callbacks;
+ void *callbacks_data;
+
+ uint32_t features;
+ struct indicator ag_ind[HFP_INDICATOR_LAST];
+ bool service;
+ uint8_t signal;
+ bool roaming;
+ uint8_t battchg;
+
};
struct cmd_handler {
@@ -101,6 +128,19 @@ struct event_handler {
hfp_hf_result_func_t callback;
};
+static void hfp_debug(hfp_debug_func_t debug_func, void *debug_data,
+ const char *format, ...)
+{
+ va_list ap;
+
+ if (!debug_func || !format)
+ return;
+
+ va_start(ap, format);
+ util_debug_va(debug_func, debug_data, format, ap);
+ va_end(ap);
+}
+
static void destroy_cmd_handler(void *data)
{
struct cmd_handler *handler = data;
@@ -1527,3 +1567,471 @@ bool hfp_hf_disconnect(struct hfp_hf *hfp)
return io_shutdown(hfp->io);
}
+
+static void ciev_service_cb(uint8_t val, void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+
+ DBG(hfp, "%u", val);
+
+ if (val < hfp->ag_ind[HFP_INDICATOR_SERVICE].min ||
+ val > hfp->ag_ind[HFP_INDICATOR_SERVICE].max) {
+ DBG(hfp, "hf: Incorrect state %u:", val);
+ return;
+ }
+
+ hfp->service = val;
+ if (hfp->callbacks && hfp->callbacks->update_indicator)
+ hfp->callbacks->update_indicator(HFP_INDICATOR_SERVICE, val,
+ hfp->callbacks_data);
+}
+
+static void ciev_call_cb(uint8_t val, void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+
+ DBG(hfp, "%u", val);
+
+ if (val < hfp->ag_ind[HFP_INDICATOR_CALL].min ||
+ val > hfp->ag_ind[HFP_INDICATOR_CALL].max) {
+ DBG(hfp, "hf: Incorrect call state %u:", val);
+ return;
+ }
+}
+
+static void ciev_callsetup_cb(uint8_t val, void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+
+ DBG(hfp, "%u", val);
+
+ if (val < hfp->ag_ind[HFP_INDICATOR_CALLSETUP].min ||
+ val > hfp->ag_ind[HFP_INDICATOR_CALLSETUP].max) {
+ DBG(hfp, "hf: Incorrect call setup state %u:", val);
+ return;
+ }
+}
+
+static void ciev_callheld_cb(uint8_t val, void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+
+ DBG(hfp, "%u", val);
+
+ if (val < hfp->ag_ind[HFP_INDICATOR_CALLHELD].min ||
+ val > hfp->ag_ind[HFP_INDICATOR_CALLHELD].max) {
+ DBG(hfp, "hf: Incorrect call held state %u:", val);
+ return;
+ }
+}
+
+static void ciev_signal_cb(uint8_t val, void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+
+ DBG(hfp, "%u", val);
+
+ if (val < hfp->ag_ind[HFP_INDICATOR_SIGNAL].min ||
+ val > hfp->ag_ind[HFP_INDICATOR_SIGNAL].max) {
+ DBG(hfp, "hf: Incorrect signal value %u:", val);
+ return;
+ }
+
+ hfp->signal = val;
+ if (hfp->callbacks && hfp->callbacks->update_indicator)
+ hfp->callbacks->update_indicator(HFP_INDICATOR_SIGNAL, val,
+ hfp->callbacks_data);
+}
+
+static void ciev_roam_cb(uint8_t val, void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+
+ DBG(hfp, "%u", val);
+
+ if (val < hfp->ag_ind[HFP_INDICATOR_ROAM].min ||
+ val > hfp->ag_ind[HFP_INDICATOR_ROAM].max) {
+ DBG(hfp, "hf: Incorrect roaming state %u:", val);
+ return;
+ }
+
+ hfp->roaming = val;
+ if (hfp->callbacks && hfp->callbacks->update_indicator)
+ hfp->callbacks->update_indicator(HFP_INDICATOR_ROAM, val,
+ hfp->callbacks_data);
+}
+
+static void ciev_battchg_cb(uint8_t val, void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+
+ DBG(hfp, "%u", val);
+
+ if (val < hfp->ag_ind[HFP_INDICATOR_BATTCHG].min ||
+ val > hfp->ag_ind[HFP_INDICATOR_BATTCHG].max) {
+ DBG(hfp, "hf: Incorrect battery charge value %u:", val);
+ return;
+ }
+
+ hfp->battchg = val;
+ if (hfp->callbacks && hfp->callbacks->update_indicator)
+ hfp->callbacks->update_indicator(HFP_INDICATOR_BATTCHG, val,
+ hfp->callbacks_data);
+}
+
+static void set_indicator_value(uint8_t index, unsigned int val,
+ struct indicator *ag_ind, struct hfp_hf *hfp)
+{
+ int i;
+
+ for (i = 0; i < HFP_INDICATOR_LAST; i++) {
+ if (index != ag_ind[i].index)
+ continue;
+
+ ag_ind[i].val = val;
+ ag_ind[i].cb(val, hfp);
+ return;
+ }
+}
+
+static void slc_cmer_resp(enum hfp_result result, enum hfp_error cme_err,
+ void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+
+ DBG(hfp, "");
+
+ if (result != HFP_RESULT_OK) {
+ DBG(hfp, "hf: CMER error: %d", result);
+ goto failed;
+ }
+
+ if (hfp->callbacks->session_ready)
+ hfp->callbacks->session_ready(HFP_RESULT_OK, 0,
+ hfp->callbacks_data);
+ return;
+
+failed:
+ if (hfp->callbacks->session_ready)
+ hfp->callbacks->session_ready(result, cme_err,
+ hfp->callbacks_data);
+}
+
+static void slc_cind_status_cb(struct hfp_context *context,
+ void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+ uint8_t index = 1;
+
+ while (hfp_context_has_next(context)) {
+ uint32_t val;
+
+ if (!hfp_context_get_number(context, &val)) {
+ DBG(hfp, "hf: Error on CIND status response");
+ return;
+ }
+
+ set_indicator_value(index++, val, hfp->ag_ind, hfp);
+ }
+}
+
+static void slc_cind_status_resp(enum hfp_result result,
+ enum hfp_error cme_err,
+ void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+
+ DBG(hfp, "");
+
+ hfp_hf_unregister(hfp, "+CIND");
+
+ if (result != HFP_RESULT_OK) {
+ DBG(hfp, "hf: CIND error: %d", result);
+ goto failed;
+ }
+
+ /* Continue with SLC creation */
+ if (!hfp_hf_send_command(hfp, slc_cmer_resp, hfp,
+ "AT+CMER=3,0,0,1")) {
+ DBG(hfp, "hf: Could not send AT+CMER");
+ result = HFP_RESULT_ERROR;
+ goto failed;
+ }
+
+ return;
+
+failed:
+ if (hfp->callbacks->session_ready)
+ hfp->callbacks->session_ready(result, cme_err,
+ hfp->callbacks_data);
+}
+
+static void set_indicator_parameters(struct hfp_hf *hfp, uint8_t index,
+ const char *indicator,
+ unsigned int min,
+ unsigned int max)
+{
+ struct indicator *ag_ind = hfp->ag_ind;
+
+ DBG(hfp, "%s, %i", indicator, index);
+
+ if (strcmp("service", indicator) == 0) {
+ if (min != 0 || max != 1) {
+ DBG(hfp, "hf: Invalid min/max values for service,"
+ " expected (0,1) got (%u,%u)", min, max);
+ return;
+ }
+ ag_ind[HFP_INDICATOR_SERVICE].index = index;
+ ag_ind[HFP_INDICATOR_SERVICE].min = min;
+ ag_ind[HFP_INDICATOR_SERVICE].max = max;
+ ag_ind[HFP_INDICATOR_SERVICE].cb = ciev_service_cb;
+ return;
+ }
+
+ if (strcmp("call", indicator) == 0) {
+ if (min != 0 || max != 1) {
+ DBG(hfp, "hf: Invalid min/max values for call,"
+ " expected (0,1) got (%u,%u)", min, max);
+ return;
+ }
+ ag_ind[HFP_INDICATOR_CALL].index = index;
+ ag_ind[HFP_INDICATOR_CALL].min = min;
+ ag_ind[HFP_INDICATOR_CALL].max = max;
+ ag_ind[HFP_INDICATOR_CALL].cb = ciev_call_cb;
+ return;
+ }
+
+ if (strcmp("callsetup", indicator) == 0) {
+ if (min != 0 || max != 3) {
+ DBG(hfp, "hf: Invalid min/max values for callsetup,"
+ " expected (0,3) got (%u,%u)", min, max);
+ return;
+ }
+ ag_ind[HFP_INDICATOR_CALLSETUP].index = index;
+ ag_ind[HFP_INDICATOR_CALLSETUP].min = min;
+ ag_ind[HFP_INDICATOR_CALLSETUP].max = max;
+ ag_ind[HFP_INDICATOR_CALLSETUP].cb = ciev_callsetup_cb;
+ return;
+ }
+
+ if (strcmp("callheld", indicator) == 0) {
+ if (min != 0 || max != 2) {
+ DBG(hfp, "hf: Invalid min/max values for callheld,"
+ " expected (0,2) got (%u,%u)", min, max);
+ return;
+ }
+ ag_ind[HFP_INDICATOR_CALLHELD].index = index;
+ ag_ind[HFP_INDICATOR_CALLHELD].min = min;
+ ag_ind[HFP_INDICATOR_CALLHELD].max = max;
+ ag_ind[HFP_INDICATOR_CALLHELD].cb = ciev_callheld_cb;
+ return;
+ }
+
+ if (strcmp("signal", indicator) == 0) {
+ if (min != 0 || max != 5) {
+ DBG(hfp, "hf: Invalid min/max values for signal,"
+ " expected (0,5) got (%u,%u)", min, max);
+ return;
+ }
+ ag_ind[HFP_INDICATOR_SIGNAL].index = index;
+ ag_ind[HFP_INDICATOR_SIGNAL].min = min;
+ ag_ind[HFP_INDICATOR_SIGNAL].max = max;
+ ag_ind[HFP_INDICATOR_SIGNAL].cb = ciev_signal_cb;
+ return;
+ }
+
+ if (strcmp("roam", indicator) == 0) {
+ if (min != 0 || max != 1) {
+ DBG(hfp, "hf: Invalid min/max values for roam,"
+ " expected (0,1) got (%u,%u)", min, max);
+ return;
+ }
+ ag_ind[HFP_INDICATOR_ROAM].index = index;
+ ag_ind[HFP_INDICATOR_ROAM].min = min;
+ ag_ind[HFP_INDICATOR_ROAM].max = max;
+ ag_ind[HFP_INDICATOR_ROAM].cb = ciev_roam_cb;
+ return;
+ }
+
+ if (strcmp("battchg", indicator) == 0) {
+ if (min != 0 || max != 5) {
+ DBG(hfp, "hf: Invalid min/max values for battchg,"
+ " expected (0,5) got (%u,%u)", min, max);
+ return;
+ }
+ ag_ind[HFP_INDICATOR_BATTCHG].index = index;
+ ag_ind[HFP_INDICATOR_BATTCHG].min = min;
+ ag_ind[HFP_INDICATOR_BATTCHG].max = max;
+ ag_ind[HFP_INDICATOR_BATTCHG].cb = ciev_battchg_cb;
+ return;
+ }
+
+ DBG(hfp, "hf: Unknown indicator: %s", indicator);
+}
+
+static void slc_cind_cb(struct hfp_context *context, void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+ int index = 1;
+
+ DBG(hfp, "");
+
+ while (hfp_context_has_next(context)) {
+ char name[255];
+ unsigned int min, max;
+
+ /* e.g ("callsetup",(0-3)) */
+ if (!hfp_context_open_container(context))
+ break;
+
+ if (!hfp_context_get_string(context, name, sizeof(name))) {
+ DBG(hfp, "hf: Could not get string");
+ goto failed;
+ }
+
+ if (!hfp_context_open_container(context)) {
+ DBG(hfp, "hf: Could not open container");
+ goto failed;
+ }
+
+ if (!hfp_context_get_range(context, &min, &max)) {
+ if (!hfp_context_get_number(context, &min)) {
+ DBG(hfp, "hf: Could not get number");
+ goto failed;
+ }
+
+ if (!hfp_context_get_number(context, &max)) {
+ DBG(hfp, "hf: Could not get number");
+ goto failed;
+ }
+ }
+
+ if (!hfp_context_close_container(context)) {
+ DBG(hfp, "hf: Could not close container");
+ goto failed;
+ }
+
+ if (!hfp_context_close_container(context)) {
+ DBG(hfp, "hf: Could not close container");
+ goto failed;
+ }
+
+ set_indicator_parameters(hfp, index, name, min, max);
+ index++;
+ }
+
+ return;
+
+failed:
+ DBG(hfp, "hf: Error on CIND response");
+}
+
+static void slc_cind_resp(enum hfp_result result, enum hfp_error cme_err,
+ void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+
+ DBG(hfp, "");
+
+ hfp_hf_unregister(hfp, "+CIND");
+
+ if (result != HFP_RESULT_OK) {
+ DBG(hfp, "hf: CIND error: %d", result);
+ goto failed;
+ }
+
+ /* Continue with SLC creation */
+ if (!hfp_hf_register(hfp, slc_cind_status_cb, "+CIND", hfp,
+ NULL)) {
+ DBG(hfp, "hf: Could not register +CIND");
+ result = HFP_RESULT_ERROR;
+ goto failed;
+ }
+
+ if (!hfp_hf_send_command(hfp, slc_cind_status_resp, hfp,
+ "AT+CIND?")) {
+ DBG(hfp, "hf: Could not send AT+CIND?");
+ result = HFP_RESULT_ERROR;
+ goto failed;
+ }
+
+ return;
+
+failed:
+ if (hfp->callbacks->session_ready)
+ hfp->callbacks->session_ready(result, cme_err,
+ hfp->callbacks_data);
+}
+
+static void slc_brsf_cb(struct hfp_context *context, void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+ unsigned int feat;
+
+ DBG(hfp, "");
+
+ if (hfp_context_get_number(context, &feat))
+ hfp->features = feat;
+}
+
+static void slc_brsf_resp(enum hfp_result result, enum hfp_error cme_err,
+ void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+
+ DBG(hfp, "");
+
+ hfp_hf_unregister(hfp, "+BRSF");
+
+ if (result != HFP_RESULT_OK) {
+ DBG(hfp, "BRSF error: %d", result);
+ goto failed;
+ }
+
+ /* Continue with SLC creation */
+ if (!hfp_hf_register(hfp, slc_cind_cb, "+CIND", hfp, NULL)) {
+ DBG(hfp, "hf: Could not register for +CIND");
+ result = HFP_RESULT_ERROR;
+ goto failed;
+ }
+
+ if (!hfp_hf_send_command(hfp, slc_cind_resp, hfp, "AT+CIND=?")) {
+ DBG(hfp, "hf: Could not send AT+CIND command");
+ result = HFP_RESULT_ERROR;
+ goto failed;
+ }
+
+ return;
+
+failed:
+ if (hfp->callbacks->session_ready)
+ hfp->callbacks->session_ready(result, cme_err,
+ hfp->callbacks_data);
+}
+
+bool hfp_hf_session_register(struct hfp_hf *hfp,
+ struct hfp_hf_callbacks *callbacks,
+ void *callbacks_data)
+{
+ if (!hfp)
+ return false;
+
+ hfp->callbacks = callbacks;
+ hfp->callbacks_data = callbacks_data;
+
+ return true;
+}
+
+bool hfp_hf_session(struct hfp_hf *hfp)
+{
+ DBG(hfp, "");
+
+ if (!hfp)
+ return false;
+
+ if (!hfp_hf_register(hfp, slc_brsf_cb, "+BRSF", hfp, NULL))
+ return false;
+
+ return hfp_hf_send_command(hfp, slc_brsf_resp, hfp,
+ "AT+BRSF=%u", HFP_HF_FEATURES);
+}
diff --git a/src/shared/hfp.h b/src/shared/hfp.h
index 600d084a7..f54b86a92 100644
--- a/src/shared/hfp.h
+++ b/src/shared/hfp.h
@@ -10,6 +10,34 @@
#include <stdbool.h>
+#define HFP_HF_FEAT_ECNR 0x00000001
+#define HFP_HF_FEAT_3WAY 0x00000002
+#define HFP_HF_FEAT_CLIP 0x00000004
+#define HFP_HF_FEAT_VOICE_RECOGNITION 0x00000008
+#define HFP_HF_FEAT_REMOTE_VOLUME_CONTROL 0x00000010
+#define HFP_HF_FEAT_ENHANCED_CALL_STATUS 0x00000020
+#define HFP_HF_FEAT_ENHANCED_CALL_CONTROL 0x00000040
+#define HFP_HF_FEAT_CODEC_NEGOTIATION 0x00000080
+#define HFP_HF_FEAT_HF_INDICATORS 0x00000100
+#define HFP_HF_FEAT_ESCO_S4_T2 0x00000200
+#define HFP_HF_FEAT_ENHANCED_VOICE_RECOGNITION_STATUS 0x00000400
+#define HFP_HF_FEAT_VOICE_RECOGNITION_TEXT 0x00000800
+
+#define HFP_AG_FEAT_3WAY 0x00000001
+#define HFP_AG_FEAT_ECNR 0x00000002
+#define HFP_AG_FEAT_VOICE_RECOGNITION 0x00000004
+#define HFP_AG_FEAT_IN_BAND_RING_TONE 0x00000008
+#define HFP_AG_FEAT_ATTACH_VOICE_TAG 0x00000010
+#define HFP_AG_FEAT_REJECT_CALL 0x00000020
+#define HFP_AG_FEAT_ENHANCED_CALL_STATUS 0x00000040
+#define HFP_AG_FEAT_ENHANCED_CALL_CONTROL 0x00000080
+#define HFP_AG_FEAT_EXTENDED_RES_CODE 0x00000100
+#define HFP_AG_FEAT_CODEC_NEGOTIATION 0x00000200
+#define HFP_AG_FEAT_HF_INDICATORS 0x00000400
+#define HFP_AG_FEAT_ESCO_S4_T2 0x00000800
+#define HFP_AG_FEAT_ENHANCED_VOICE_RECOGNITION_STATUS 0x00001000
+#define HFP_AG_FEAT_VOICE_RECOGNITION_TEXT 0x00001000
+
enum hfp_result {
HFP_RESULT_OK = 0,
HFP_RESULT_CONNECT = 1,
@@ -57,6 +85,35 @@ enum hfp_gw_cmd_type {
HFP_GW_CMD_TYPE_COMMAND
};
+enum hfp_indicator {
+ HFP_INDICATOR_SERVICE = 0,
+ HFP_INDICATOR_CALL,
+ HFP_INDICATOR_CALLSETUP,
+ HFP_INDICATOR_CALLHELD,
+ HFP_INDICATOR_SIGNAL,
+ HFP_INDICATOR_ROAM,
+ HFP_INDICATOR_BATTCHG,
+ HFP_INDICATOR_LAST
+};
+
+enum hfp_call {
+ CIND_CALL_NONE = 0,
+ CIND_CALL_IN_PROGRESS
+};
+
+enum hfp_call_setup {
+ CIND_CALLSETUP_NONE = 0,
+ CIND_CALLSETUP_INCOMING,
+ CIND_CALLSETUP_DIALING,
+ CIND_CALLSETUP_ALERTING
+};
+
+enum hfp_call_held {
+ CIND_CALLHELD_NONE = 0,
+ CIND_CALLHELD_HOLD_AND_ACTIVE,
+ CIND_CALLHELD_HOLD
+};
+
struct hfp_context;
typedef void (*hfp_result_func_t)(struct hfp_context *context,
@@ -128,6 +185,13 @@ typedef void (*hfp_response_func_t)(enum hfp_result result,
struct hfp_hf;
+struct hfp_hf_callbacks {
+ void (*session_ready)(enum hfp_result result, enum hfp_error cme_err,
+ void *user_data);
+ void (*update_indicator)(enum hfp_indicator indicator, uint32_t val,
+ void *user_data);
+};
+
struct hfp_hf *hfp_hf_new(int fd);
struct hfp_hf *hfp_hf_ref(struct hfp_hf *hfp);
@@ -146,3 +210,8 @@ bool hfp_hf_register(struct hfp_hf *hfp, hfp_hf_result_func_t callback,
bool hfp_hf_unregister(struct hfp_hf *hfp, const char *prefix);
bool hfp_hf_send_command(struct hfp_hf *hfp, hfp_response_func_t resp_cb,
void *user_data, const char *format, ...);
+
+bool hfp_hf_session_register(struct hfp_hf *hfp,
+ struct hfp_hf_callbacks *callbacks,
+ void *callbacks_data);
+bool hfp_hf_session(struct hfp_hf *hfp);
--
2.43.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH BlueZ v2 2/4] unit/test-hfp: Add SLC connection test
2025-08-20 13:33 [PATCH BlueZ v2 1/4] shared/hfp: Add HF SLC connection function Frédéric Danis
@ 2025-08-20 13:33 ` Frédéric Danis
2025-08-20 13:33 ` [PATCH BlueZ v2 3/4] shared/hfp: Add +CIEV event support Frédéric Danis
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Frédéric Danis @ 2025-08-20 13:33 UTC (permalink / raw)
To: linux-bluetooth
This adds minimal packet exchange to test the SLC establishment.
---
v1->v2: Fix ERROR:unit/test-hfp.c:734:hf_update_indicator: assertion failed (val == 1): (0 == 1)
unit/test-hfp.c | 117 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 117 insertions(+)
diff --git a/unit/test-hfp.c b/unit/test-hfp.c
index b4af99d53..b79034642 100644
--- a/unit/test-hfp.c
+++ b/unit/test-hfp.c
@@ -104,6 +104,14 @@ struct test_data {
data.test_handler = test_hf_handler; \
} while (0)
+static void print_debug(const char *str, void *user_data)
+{
+ const char *prefix = user_data;
+
+ if (tester_use_debug())
+ tester_debug("%s%s", prefix, str);
+}
+
static void test_free(gconstpointer user_data)
{
const struct test_data *data = user_data;
@@ -680,6 +688,110 @@ static void test_hf_robustness(gconstpointer data)
context_quit(context);
}
+#define MINIMAL_SLC_SESSION \
+ raw_pdu('\r', '\n', '+', 'B', 'R', 'S', 'F', ':', \
+ ' ', '0', '\r', '\n'), \
+ frg_pdu('\r', '\n', 'O', 'K', '\r', '\n'), \
+ raw_pdu('\r', '\n', '+', 'C', 'I', 'N', 'D', ':', ' '), \
+ frg_pdu('(', '\"', 's', 'e', 'r', 'v', 'i', 'c', 'e'), \
+ frg_pdu('\"', '(', '0', ',', '1', ')', ')', ','), \
+ frg_pdu('(', '\"', 'c', 'a', 'l', 'l', '\"'), \
+ frg_pdu('(', '0', ',', '1', ')', ')', ','), \
+ frg_pdu('(', '\"', 'c', 'a', 'l', 'l', 's', 'e', 't'), \
+ frg_pdu('u', 'p', '\"', ',', '(', '0', '-', '3', ')'), \
+ frg_pdu(')', ','), \
+ frg_pdu('(', '\"', 'c', 'a', 'l', 'l', 'h', 'e', 'l'), \
+ frg_pdu('d', '\"', ',', '(', '0', '-', '2', ')', ')'), \
+ frg_pdu(',', '(', '\"', 's', 'i', 'g', 'n', 'a', 'l'), \
+ frg_pdu('\"', '(', '0', '-', '5', ')', ')', ','), \
+ frg_pdu('(', '\"', 'r', 'o', 'a', 'm', '\"', ',', '('), \
+ frg_pdu('0', ',', '1', ')', ')', ','), \
+ frg_pdu('(', '\"', 'b', 'a', 't', 't', 'c', 'h', 'g'), \
+ frg_pdu('\"', '(', '0', '-', '5', ')', ')', ','), \
+ frg_pdu('\r', '\n'), \
+ frg_pdu('\r', '\n', 'O', 'K', '\r', '\n'), \
+ raw_pdu('\r', '\n', '+', 'C', 'I', 'N', 'D', ':', ' '), \
+ frg_pdu('0', ',', '0', ',', '0', ',', '0', ',', '5'), \
+ frg_pdu(',', '0', ',', '5', '\r', '\n'), \
+ frg_pdu('\r', '\n', 'O', 'K', '\r', '\n'), \
+ raw_pdu('\r', '\n', 'O', 'K', '\r', '\n')
+
+static void hf_session_ready_cb(enum hfp_result res, enum hfp_error cme_err,
+ void *user_data)
+{
+ struct context *context = user_data;
+
+ g_assert_cmpint(res, ==, HFP_RESULT_OK);
+
+ context->data->response_func(res, cme_err, context);
+}
+
+static void hf_update_indicator(enum hfp_indicator indicator, uint32_t val,
+ void *user_data)
+{
+ switch (indicator) {
+ case HFP_INDICATOR_SERVICE:
+ g_assert_cmpint(val, ==, 0);
+ break;
+ case HFP_INDICATOR_CALL:
+ g_assert_cmpint(val, ==, 0);
+ break;
+ case HFP_INDICATOR_CALLSETUP:
+ g_assert_cmpint(val, ==, 0);
+ break;
+ case HFP_INDICATOR_CALLHELD:
+ g_assert_cmpint(val, ==, 0);
+ break;
+ case HFP_INDICATOR_SIGNAL:
+ g_assert_cmpint(val, ==, 5);
+ break;
+ case HFP_INDICATOR_ROAM:
+ g_assert_cmpint(val, ==, 0);
+ break;
+ case HFP_INDICATOR_BATTCHG:
+ g_assert_cmpint(val, ==, 5);
+ break;
+ case HFP_INDICATOR_LAST:
+ default:
+ tester_test_failed();
+ }
+}
+
+static struct hfp_hf_callbacks hf_session_callbacks = {
+ .session_ready = hf_session_ready_cb,
+ .update_indicator = hf_update_indicator,
+};
+
+static void test_hf_session_done(enum hfp_result res, enum hfp_error cme_err,
+ void *user_data)
+{
+ struct context *context = user_data;
+
+ hfp_hf_disconnect(context->hfp_hf);
+}
+
+static void test_hf_session(gconstpointer data)
+{
+ struct context *context = create_context(data);
+ bool ret;
+
+ context->hfp_hf = hfp_hf_new(context->fd_client);
+ g_assert(context->hfp_hf);
+
+ ret = hfp_hf_set_debug(context->hfp_hf, print_debug, "hfp-hf:", NULL);
+ g_assert(ret);
+
+ ret = hfp_hf_set_close_on_unref(context->hfp_hf, true);
+ g_assert(ret);
+
+ ret = hfp_hf_session_register(context->hfp_hf, &hf_session_callbacks,
+ context);
+ g_assert(ret);
+
+ ret = hfp_hf_session(context->hfp_hf);
+ g_assert(ret);
+}
+
int main(int argc, char *argv[])
{
tester_init(&argc, &argv);
@@ -850,5 +962,10 @@ int main(int argc, char *argv[])
frg_pdu('1', ',', '2', 'x', '\r', '\n'),
data_end());
+ define_hf_test("/hfp_hf/test_session_minimal", test_hf_session,
+ NULL, test_hf_session_done,
+ MINIMAL_SLC_SESSION,
+ data_end());
+
return tester_run();
}
--
2.43.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH BlueZ v2 3/4] shared/hfp: Add +CIEV event support
2025-08-20 13:33 [PATCH BlueZ v2 1/4] shared/hfp: Add HF SLC connection function Frédéric Danis
2025-08-20 13:33 ` [PATCH BlueZ v2 2/4] unit/test-hfp: Add SLC connection test Frédéric Danis
@ 2025-08-20 13:33 ` Frédéric Danis
2025-08-20 13:33 ` [PATCH BlueZ v2 4/4] unit/test-hfp: Add indicators tests for HF Frédéric Danis
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Frédéric Danis @ 2025-08-20 13:33 UTC (permalink / raw)
To: linux-bluetooth
Register +CIEV handler on SLC completion to call the update_indicator
call back on unsolicited events.
---
src/shared/hfp.c | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/src/shared/hfp.c b/src/shared/hfp.c
index c1bcb61cf..71f193f83 100644
--- a/src/shared/hfp.c
+++ b/src/shared/hfp.c
@@ -1694,6 +1694,22 @@ static void set_indicator_value(uint8_t index, unsigned int val,
}
}
+static void ciev_cb(struct hfp_context *context, void *user_data)
+{
+ struct hfp_hf *hfp = user_data;
+ unsigned int index, val;
+
+ DBG(hfp, "");
+
+ if (!hfp_context_get_number(context, &index))
+ return;
+
+ if (!hfp_context_get_number(context, &val))
+ return;
+
+ set_indicator_value(index, val, hfp->ag_ind, hfp);
+}
+
static void slc_cmer_resp(enum hfp_result result, enum hfp_error cme_err,
void *user_data)
{
@@ -1709,6 +1725,10 @@ static void slc_cmer_resp(enum hfp_result result, enum hfp_error cme_err,
if (hfp->callbacks->session_ready)
hfp->callbacks->session_ready(HFP_RESULT_OK, 0,
hfp->callbacks_data);
+
+ /* Register unsolicited results handlers */
+ hfp_hf_register(hfp, ciev_cb, "+CIEV", hfp, NULL);
+
return;
failed:
--
2.43.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH BlueZ v2 4/4] unit/test-hfp: Add indicators tests for HF
2025-08-20 13:33 [PATCH BlueZ v2 1/4] shared/hfp: Add HF SLC connection function Frédéric Danis
2025-08-20 13:33 ` [PATCH BlueZ v2 2/4] unit/test-hfp: Add SLC connection test Frédéric Danis
2025-08-20 13:33 ` [PATCH BlueZ v2 3/4] shared/hfp: Add +CIEV event support Frédéric Danis
@ 2025-08-20 13:33 ` Frédéric Danis
2025-08-20 15:00 ` [PATCH BlueZ v2 1/4] shared/hfp: Add HF SLC connection function patchwork-bot+bluetooth
2025-08-20 15:01 ` [BlueZ,v2,1/4] " bluez.test.bot
4 siblings, 0 replies; 6+ messages in thread
From: Frédéric Danis @ 2025-08-20 13:33 UTC (permalink / raw)
To: linux-bluetooth
This adds the following tests:
- /HFP/HF/TRS/BV-01-C
Verify that the HF accepts the registration status indication.
- /HFP/HF/PSI/BV-01-C
Verify that the HF successfully receives the signal strength status of
the AG.
- /HFP/HF/PSI/BV-02-C
Verify that the HF successfully receives the roaming status of the AG.
- /HFP/HF/PSI/BV-03-C
Verify that the HF successfully receives the battery level status of
the AG.
---
unit/test-hfp.c | 131 ++++++++++++++++++++++++++++++++++++++----------
1 file changed, 105 insertions(+), 26 deletions(-)
diff --git a/unit/test-hfp.c b/unit/test-hfp.c
index b79034642..9be1b05ae 100644
--- a/unit/test-hfp.c
+++ b/unit/test-hfp.c
@@ -19,6 +19,11 @@
#include "src/shared/tester.h"
#include "src/shared/util.h"
+struct session {
+ bool completed;
+ guint step;
+};
+
struct context {
guint watch_id;
int fd_server;
@@ -27,6 +32,7 @@ struct context {
struct hfp_hf *hfp_hf;
const struct test_data *data;
unsigned int pdu_offset;
+ struct session session;
};
struct test_pdu {
@@ -720,40 +726,75 @@ static void hf_session_ready_cb(enum hfp_result res, enum hfp_error cme_err,
void *user_data)
{
struct context *context = user_data;
+ const char *test_name = context->data->test_name;
g_assert_cmpint(res, ==, HFP_RESULT_OK);
+ context->session.completed = true;
- context->data->response_func(res, cme_err, context);
+ if (g_str_equal(test_name, "/hfp_hf/test_session_minimal"))
+ context->data->response_func(res, cme_err, context);
}
static void hf_update_indicator(enum hfp_indicator indicator, uint32_t val,
void *user_data)
{
- switch (indicator) {
- case HFP_INDICATOR_SERVICE:
- g_assert_cmpint(val, ==, 0);
- break;
- case HFP_INDICATOR_CALL:
- g_assert_cmpint(val, ==, 0);
- break;
- case HFP_INDICATOR_CALLSETUP:
- g_assert_cmpint(val, ==, 0);
- break;
- case HFP_INDICATOR_CALLHELD:
- g_assert_cmpint(val, ==, 0);
- break;
- case HFP_INDICATOR_SIGNAL:
- g_assert_cmpint(val, ==, 5);
- break;
- case HFP_INDICATOR_ROAM:
- g_assert_cmpint(val, ==, 0);
- break;
- case HFP_INDICATOR_BATTCHG:
- g_assert_cmpint(val, ==, 5);
- break;
- case HFP_INDICATOR_LAST:
- default:
- tester_test_failed();
+ struct context *context = user_data;
+ const char *test_name = context->data->test_name;
+
+ if (!context->session.completed) {
+ switch (indicator) {
+ case HFP_INDICATOR_SERVICE:
+ g_assert_cmpint(val, ==, 0);
+ break;
+ case HFP_INDICATOR_CALL:
+ g_assert_cmpint(val, ==, 0);
+ break;
+ case HFP_INDICATOR_CALLSETUP:
+ g_assert_cmpint(val, ==, 0);
+ break;
+ case HFP_INDICATOR_CALLHELD:
+ g_assert_cmpint(val, ==, 0);
+ break;
+ case HFP_INDICATOR_SIGNAL:
+ g_assert_cmpint(val, ==, 5);
+ break;
+ case HFP_INDICATOR_ROAM:
+ g_assert_cmpint(val, ==, 0);
+ break;
+ case HFP_INDICATOR_BATTCHG:
+ g_assert_cmpint(val, ==, 5);
+ break;
+ case HFP_INDICATOR_LAST:
+ default:
+ tester_test_failed();
+ }
+ return;
+ }
+
+ if (g_str_equal(test_name, "/HFP/HF/TRS/BV-01-C")) {
+ context->session.step++;
+ g_assert_cmpint(indicator, ==, HFP_INDICATOR_SERVICE);
+ g_assert_cmpint(val, ==, context->session.step % 2);
+
+ if (context->session.step == 3)
+ context->data->response_func(HFP_RESULT_OK, 0,
+ context);
+ } else if (g_str_equal(test_name, "/HFP/HF/PSI/BV-01-C")) {
+ g_assert_cmpint(indicator, ==, HFP_INDICATOR_SIGNAL);
+ g_assert_cmpint(val, ==, 3);
+ context->data->response_func(HFP_RESULT_OK, 0, context);
+ } else if (g_str_equal(test_name, "/HFP/HF/PSI/BV-02-C")) {
+ context->session.step++;
+ g_assert_cmpint(indicator, ==, HFP_INDICATOR_ROAM);
+ g_assert_cmpint(val, ==, context->session.step % 2);
+
+ if (context->session.step == 2)
+ context->data->response_func(HFP_RESULT_OK, 0,
+ context);
+ } else if (g_str_equal(test_name, "/HFP/HF/PSI/BV-03-C")) {
+ g_assert_cmpint(indicator, ==, HFP_INDICATOR_BATTCHG);
+ g_assert_cmpint(val, ==, 3);
+ context->data->response_func(HFP_RESULT_OK, 0, context);
}
}
@@ -967,5 +1008,43 @@ int main(int argc, char *argv[])
MINIMAL_SLC_SESSION,
data_end());
+ /* Transfer Registration Status - HF */
+ define_hf_test("/HFP/HF/TRS/BV-01-C", test_hf_session,
+ NULL, test_hf_session_done,
+ MINIMAL_SLC_SESSION,
+ frg_pdu('\r', '\n', '+', 'C', 'I', 'E', 'V', ':'),
+ frg_pdu(' ', '1', ',', '1', '\r', '\n'),
+ frg_pdu('\r', '\n', '+', 'C', 'I', 'E', 'V', ':'),
+ frg_pdu(' ', '1', ',', '0', '\r', '\n'),
+ frg_pdu('\r', '\n', '+', 'C', 'I', 'E', 'V', ':'),
+ frg_pdu(' ', '1', ',', '1', '\r', '\n'),
+ data_end());
+
+ /* Transfer Signal Strength Indication - HF */
+ define_hf_test("/HFP/HF/PSI/BV-01-C", test_hf_session,
+ NULL, test_hf_session_done,
+ MINIMAL_SLC_SESSION,
+ frg_pdu('\r', '\n', '+', 'C', 'I', 'E', 'V', ':'),
+ frg_pdu(' ', '5', ',', '3', '\r', '\n'),
+ data_end());
+
+ /* Transfer Roaming Status Indication - HF */
+ define_hf_test("/HFP/HF/PSI/BV-02-C", test_hf_session,
+ NULL, test_hf_session_done,
+ MINIMAL_SLC_SESSION,
+ frg_pdu('\r', '\n', '+', 'C', 'I', 'E', 'V', ':'),
+ frg_pdu(' ', '6', ',', '1', '\r', '\n'),
+ frg_pdu('\r', '\n', '+', 'C', 'I', 'E', 'V', ':'),
+ frg_pdu(' ', '6', ',', '0', '\r', '\n'),
+ data_end());
+
+ /* Transfer Battery Level Indication - HF */
+ define_hf_test("/HFP/HF/PSI/BV-03-C", test_hf_session,
+ NULL, test_hf_session_done,
+ MINIMAL_SLC_SESSION,
+ frg_pdu('\r', '\n', '+', 'C', 'I', 'E', 'V', ':'),
+ frg_pdu(' ', '7', ',', '3', '\r', '\n'),
+ data_end());
+
return tester_run();
}
--
2.43.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* Re: [PATCH BlueZ v2 1/4] shared/hfp: Add HF SLC connection function
2025-08-20 13:33 [PATCH BlueZ v2 1/4] shared/hfp: Add HF SLC connection function Frédéric Danis
` (2 preceding siblings ...)
2025-08-20 13:33 ` [PATCH BlueZ v2 4/4] unit/test-hfp: Add indicators tests for HF Frédéric Danis
@ 2025-08-20 15:00 ` patchwork-bot+bluetooth
2025-08-20 15:01 ` [BlueZ,v2,1/4] " bluez.test.bot
4 siblings, 0 replies; 6+ messages in thread
From: patchwork-bot+bluetooth @ 2025-08-20 15:00 UTC (permalink / raw)
To: =?utf-8?b?RnLDqWTDqXJpYyBEYW5pcyA8ZnJlZGVyaWMuZGFuaXNAY29sbGFib3JhLmNvbT4=?=
Cc: linux-bluetooth
Hello:
This series was applied to bluetooth/bluez.git (master)
by Luiz Augusto von Dentz <luiz.von.dentz@intel.com>:
On Wed, 20 Aug 2025 15:33:35 +0200 you wrote:
> This implements the minimal SLC connection exchange, i.e. AT+BRSF,
> AT+CIND=?, AT+CIND? and AT+CMER=3,0,0,1 requested to complete the
> Service Level Connection Establishment.
> ---
> src/shared/hfp.c | 508 +++++++++++++++++++++++++++++++++++++++++++++++
> src/shared/hfp.h | 69 +++++++
> 2 files changed, 577 insertions(+)
Here is the summary with links:
- [BlueZ,v2,1/4] shared/hfp: Add HF SLC connection function
https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=56b483c7f869
- [BlueZ,v2,2/4] unit/test-hfp: Add SLC connection test
https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=fb9b5c04fc3c
- [BlueZ,v2,3/4] shared/hfp: Add +CIEV event support
https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=bcd94e3e327d
- [BlueZ,v2,4/4] unit/test-hfp: Add indicators tests for HF
https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=ed2da1a3786d
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply [flat|nested] 6+ messages in thread
* RE: [BlueZ,v2,1/4] shared/hfp: Add HF SLC connection function
2025-08-20 13:33 [PATCH BlueZ v2 1/4] shared/hfp: Add HF SLC connection function Frédéric Danis
` (3 preceding siblings ...)
2025-08-20 15:00 ` [PATCH BlueZ v2 1/4] shared/hfp: Add HF SLC connection function patchwork-bot+bluetooth
@ 2025-08-20 15:01 ` bluez.test.bot
4 siblings, 0 replies; 6+ messages in thread
From: bluez.test.bot @ 2025-08-20 15:01 UTC (permalink / raw)
To: linux-bluetooth, frederic.danis
[-- Attachment #1: Type: text/plain, Size: 1261 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/project/bluetooth/list/?series=993518
---Test result---
Test Summary:
CheckPatch PENDING 0.40 seconds
GitLint PENDING 0.43 seconds
BuildEll PASS 20.04 seconds
BluezMake PASS 2541.26 seconds
MakeCheck PASS 20.25 seconds
MakeDistcheck PASS 184.16 seconds
CheckValgrind PASS 236.60 seconds
CheckSmatch PASS 307.95 seconds
bluezmakeextell PASS 127.03 seconds
IncrementalBuild PENDING 0.29 seconds
ScanBuild PASS 909.09 seconds
Details
##############################
Test: CheckPatch - PENDING
Desc: Run checkpatch.pl script
Output:
##############################
Test: GitLint - PENDING
Desc: Run gitlint
Output:
##############################
Test: IncrementalBuild - PENDING
Desc: Incremental build with the patches in the series
Output:
---
Regards,
Linux Bluetooth
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2025-08-20 15:01 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-08-20 13:33 [PATCH BlueZ v2 1/4] shared/hfp: Add HF SLC connection function Frédéric Danis
2025-08-20 13:33 ` [PATCH BlueZ v2 2/4] unit/test-hfp: Add SLC connection test Frédéric Danis
2025-08-20 13:33 ` [PATCH BlueZ v2 3/4] shared/hfp: Add +CIEV event support Frédéric Danis
2025-08-20 13:33 ` [PATCH BlueZ v2 4/4] unit/test-hfp: Add indicators tests for HF Frédéric Danis
2025-08-20 15:00 ` [PATCH BlueZ v2 1/4] shared/hfp: Add HF SLC connection function patchwork-bot+bluetooth
2025-08-20 15:01 ` [BlueZ,v2,1/4] " 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;
as well as URLs for NNTP newsgroup(s).