diff --git a/components/mdns/include/mdns.h b/components/mdns/include/mdns.h index 4c89dde736..85233f0f23 100644 --- a/components/mdns/include/mdns.h +++ b/components/mdns/include/mdns.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -757,6 +757,47 @@ void mdns_query_results_free(mdns_result_t *results); */ esp_err_t mdns_query_ptr(const char *service_type, const char *proto, uint32_t timeout, size_t max_results, mdns_result_t **results); +/** + * @brief Query mDNS for service subtype (Selective Instance Enumeration per RFC 6763 Section 7.1) + * + * Sends a PTR query for `_subtype._sub._service_type._proto.local` to discover + * only the instances of a service that have the given subtype. + * + * @param service_type service type (_http, _arduino, _ftp etc.) + * @param proto service protocol (_tcp, _udp, etc.) + * @param subtype the subtype to query for (e.g. "_printer") + * @param timeout time in milliseconds to wait for answer. + * @param max_results maximum results to be collected + * @param results pointer to the results of the query + * + * @return + * - ESP_OK success + * - ESP_ERR_INVALID_STATE mDNS is not running + * - ESP_ERR_NO_MEM memory error + * - ESP_ERR_INVALID_ARG parameter error + */ +esp_err_t mdns_query_ptr_subtype(const char *service_type, const char *proto, const char *subtype, + uint32_t timeout, size_t max_results, mdns_result_t **results); + +/** + * @brief Query mDNS for service subtype asynchronously (Selective Instance Enumeration per RFC 6763 Section 7.1) + * + * Sends a PTR query for `_subtype._sub._service_type._proto.local`. + * The search object must be checked for progress and deleted manually. + * + * @param service_type service type (_http, _arduino, _ftp etc.) + * @param proto service protocol (_tcp, _udp, etc.) + * @param subtype the subtype to query for (e.g. "_printer") + * @param timeout time in milliseconds during which mDNS query is active + * @param max_results maximum results to be collected + * @param notifier Notification function to be called when the result is ready, can be NULL + * + * @return mdns_search_once_s pointer to new search object if query initiated successfully. + * NULL otherwise. + */ +mdns_search_once_t *mdns_query_async_new_subtype(const char *service_type, const char *proto, const char *subtype, + uint32_t timeout, size_t max_results, mdns_query_notify_t notifier); + /** * @brief Query mDNS for SRV record * @@ -919,6 +960,20 @@ esp_err_t mdns_netif_action(esp_netif_t *esp_netif, mdns_event_actions_t event_a */ mdns_browse_t *mdns_browse_new(const char *service, const char *proto, mdns_browse_notify_t notifier); +/** + * @brief Browse mDNS for a service subtype `_subtype._sub._service._proto` + * (Selective Instance Enumeration per RFC 6763 Section 7.1). + * + * @param service Pointer to the `_service` which will be browsed. + * @param proto Pointer to the `_proto` which will be browsed. + * @param subtype Pointer to the `_subtype` to restrict browsing to. Can be NULL for standard browse. + * @param notifier The callback which will be called when the browsing service changed. + * @return mdns_browse_t pointer to new browse object if initiated successfully. + * NULL otherwise. + */ +mdns_browse_t *mdns_browse_new_subtype(const char *service, const char *proto, const char *subtype, + mdns_browse_notify_t notifier); + /** * @brief Stop the `_service._proto` browse. * @param service Pointer to the `_service` which will be browsed. @@ -930,6 +985,18 @@ mdns_browse_t *mdns_browse_new(const char *service, const char *proto, mdns_brow */ esp_err_t mdns_browse_delete(const char *service, const char *proto); +/** + * @brief Stop the `_subtype._sub._service._proto` browse. + * @param service Pointer to the `_service` which will be browsed. + * @param proto Pointer to the `_proto` which will be browsed. + * @param subtype Pointer to the `_subtype`. Can be NULL to match a browse without subtype. + * @return + * - ESP_OK success. + * - ESP_ERR_FAIL mDNS is not running or the browsing was never started. + * - ESP_ERR_NO_MEM memory error. + */ +esp_err_t mdns_browse_delete_subtype(const char *service, const char *proto, const char *subtype); + #ifdef __cplusplus } #endif diff --git a/components/mdns/mdns_browser.c b/components/mdns/mdns_browser.c index c74a256bba..ffe6656f51 100644 --- a/components/mdns/mdns_browser.c +++ b/components/mdns/mdns_browser.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -50,6 +50,7 @@ static void browse_item_free(mdns_browse_t *browse) { mdns_mem_free(browse->service); mdns_mem_free(browse->proto); + mdns_mem_free(browse->subtype); if (browse->result) { mdns_priv_query_results_free(browse->result); } @@ -79,12 +80,12 @@ static void browse_sync(mdns_browse_sync_t *browse_sync) */ static void browse_send(mdns_browse_t *browse, mdns_if_t interface) { - // Using search once for sending the PTR query mdns_search_once_t search = {0}; search.instance = NULL; search.service = browse->service; search.proto = browse->proto; + search.subtype = browse->subtype; search.type = MDNS_TYPE_PTR; search.unicast = false; search.result = NULL; @@ -113,6 +114,23 @@ void mdns_priv_browse_free(void) } } +static bool browse_match(const mdns_browse_t *a, const mdns_browse_t *b) +{ + if (strlen(a->service) != strlen(b->service) || memcmp(a->service, b->service, strlen(a->service)) != 0) { + return false; + } + if (strlen(a->proto) != strlen(b->proto) || memcmp(a->proto, b->proto, strlen(a->proto)) != 0) { + return false; + } + if (a->subtype == NULL && b->subtype == NULL) { + return true; + } + if (a->subtype == NULL || b->subtype == NULL) { + return false; + } + return strlen(a->subtype) == strlen(b->subtype) && memcmp(a->subtype, b->subtype, strlen(a->subtype)) == 0; +} + /** * @brief Mark browse as finished, remove and free it from browse chain */ @@ -122,8 +140,7 @@ static void browse_finish(mdns_browse_t *browse) mdns_browse_t *b = s_browse; mdns_browse_t *target_free = NULL; while (b) { - if (strlen(b->service) == strlen(browse->service) && memcmp(b->service, browse->service, strlen(b->service)) == 0 && - strlen(b->proto) == strlen(browse->proto) && memcmp(b->proto, browse->proto, strlen(b->proto)) == 0) { + if (browse_match(b, browse)) { target_free = b; b = b->next; queueDetach(mdns_browse_t, s_browse, target_free); @@ -138,7 +155,8 @@ static void browse_finish(mdns_browse_t *browse) /** * @brief Allocate new browse structure */ -static mdns_browse_t *browse_init(const char *service, const char *proto, mdns_browse_notify_t notifier) +static mdns_browse_t *browse_init(const char *service, const char *proto, const char *subtype, + mdns_browse_notify_t notifier) { mdns_browse_t *browse = (mdns_browse_t *)mdns_mem_malloc(sizeof(mdns_browse_t)); @@ -166,6 +184,14 @@ static mdns_browse_t *browse_init(const char *service, const char *proto, mdns_b } } + if (!mdns_utils_str_null_or_empty(subtype)) { + browse->subtype = mdns_mem_strndup(subtype, MDNS_NAME_BUF_LEN - 1); + if (!browse->subtype) { + browse_item_free(browse); + return NULL; + } + } + browse->notifier = notifier; return browse; } @@ -178,10 +204,8 @@ static void browse_add(mdns_browse_t *browse) browse->state = BROWSE_RUNNING; mdns_browse_t *queue = s_browse; bool found = false; - // looking for this browse in active browses while (queue) { - if (strlen(queue->service) == strlen(browse->service) && memcmp(queue->service, browse->service, strlen(queue->service)) == 0 && - strlen(queue->proto) == strlen(browse->proto) && memcmp(queue->proto, browse->proto, strlen(queue->proto)) == 0) { + if (browse_match(queue, browse)) { found = true; break; } @@ -615,6 +639,12 @@ esp_err_t mdns_priv_browse_sync(mdns_browse_sync_t *browse_sync) * @defgroup MDNS_PUBCLIC_API */ mdns_browse_t *mdns_browse_new(const char *service, const char *proto, mdns_browse_notify_t notifier) +{ + return mdns_browse_new_subtype(service, proto, NULL, notifier); +} + +mdns_browse_t *mdns_browse_new_subtype(const char *service, const char *proto, const char *subtype, + mdns_browse_notify_t notifier) { mdns_browse_t *browse = NULL; @@ -622,7 +652,7 @@ mdns_browse_t *mdns_browse_new(const char *service, const char *proto, mdns_brow return NULL; } - browse = browse_init(service, proto, notifier); + browse = browse_init(service, proto, subtype, notifier); if (!browse) { return NULL; } @@ -636,6 +666,11 @@ mdns_browse_t *mdns_browse_new(const char *service, const char *proto, mdns_brow } esp_err_t mdns_browse_delete(const char *service, const char *proto) +{ + return mdns_browse_delete_subtype(service, proto, NULL); +} + +esp_err_t mdns_browse_delete_subtype(const char *service, const char *proto, const char *subtype) { mdns_browse_t *browse = NULL; @@ -643,7 +678,7 @@ esp_err_t mdns_browse_delete(const char *service, const char *proto) return ESP_FAIL; } - browse = browse_init(service, proto, NULL); + browse = browse_init(service, proto, subtype, NULL); if (!browse) { return ESP_ERR_NO_MEM; } diff --git a/components/mdns/mdns_querier.c b/components/mdns/mdns_querier.c index 09e81fa253..b714b2e64a 100644 --- a/components/mdns/mdns_querier.c +++ b/components/mdns/mdns_querier.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -21,6 +21,9 @@ static mdns_search_once_t *s_search_once; static esp_err_t send_search_action(mdns_action_type_t type, mdns_search_once_t *search); static void search_free(mdns_search_once_t *search); +static mdns_search_once_t *search_init_with_subtype(const char *name, const char *service, const char *proto, + const char *subtype, uint16_t type, bool unicast, + uint32_t timeout, uint8_t max_results, mdns_query_notify_t notifier); void mdns_priv_query_results_free(mdns_result_t *results) { @@ -167,6 +170,7 @@ void mdns_priv_query_free(void) mdns_mem_free(h->instance); mdns_mem_free(h->service); mdns_mem_free(h->proto); + mdns_mem_free(h->subtype); vSemaphoreDelete(h->done_semaphore); if (h->result) { mdns_priv_query_results_free(h->result); @@ -249,7 +253,15 @@ mdns_search_once_t *mdns_priv_query_find_from(mdns_search_once_t *s, mdns_name_t } if (type == MDNS_TYPE_PTR && type == s->type && !strcasecmp(name->service, s->service) && !strcasecmp(name->proto, s->proto)) { - return s; + if (name->sub) { + if (s->subtype && !strcasecmp(name->host, s->subtype)) { + return s; + } + } else if (!s->subtype) { + return s; + } + s = s->next; + continue; } s = s->next; @@ -287,6 +299,7 @@ static mdns_tx_packet_t *create_search_packet(mdns_search_once_t *search, mdns_i q->service = search->service; q->proto = search->proto; q->domain = MDNS_UTILS_DEFAULT_DOMAIN; + q->subtype = search->subtype; q->own_dynamic_memory = false; queueToEnd(mdns_out_question_t, packet->questions, q); @@ -345,6 +358,7 @@ static void search_free(mdns_search_once_t *search) mdns_mem_free(search->instance); mdns_mem_free(search->service); mdns_mem_free(search->proto); + mdns_mem_free(search->subtype); vSemaphoreDelete(search->done_semaphore); mdns_mem_free(search); } @@ -354,6 +368,13 @@ static void search_free(mdns_search_once_t *search) */ static mdns_search_once_t *search_init(const char *name, const char *service, const char *proto, uint16_t type, bool unicast, uint32_t timeout, uint8_t max_results, mdns_query_notify_t notifier) +{ + return search_init_with_subtype(name, service, proto, NULL, type, unicast, timeout, max_results, notifier); +} + +static mdns_search_once_t *search_init_with_subtype(const char *name, const char *service, const char *proto, + const char *subtype, uint16_t type, bool unicast, + uint32_t timeout, uint8_t max_results, mdns_query_notify_t notifier) { mdns_search_once_t *search = (mdns_search_once_t *)mdns_mem_malloc(sizeof(mdns_search_once_t)); if (!search) { @@ -392,6 +413,14 @@ static mdns_search_once_t *search_init(const char *name, const char *service, co } } + if (!mdns_utils_str_null_or_empty(subtype)) { + search->subtype = mdns_mem_strndup(subtype, MDNS_NAME_BUF_LEN - 1); + if (!search->subtype) { + search_free(search); + return NULL; + } + } + search->type = type; search->unicast = unicast; search->timeout = timeout; @@ -775,6 +804,72 @@ esp_err_t mdns_query_ptr(const char *service, const char *proto, uint32_t timeou return mdns_query(NULL, service, proto, MDNS_TYPE_PTR, timeout, max_results, results); } +esp_err_t mdns_query_ptr_subtype(const char *service_type, const char *proto, const char *subtype, + uint32_t timeout, size_t max_results, mdns_result_t **results) +{ + mdns_search_once_t *search = NULL; + + if (mdns_utils_str_null_or_empty(service_type) || mdns_utils_str_null_or_empty(proto) || + mdns_utils_str_null_or_empty(subtype)) { + return ESP_ERR_INVALID_ARG; + } + + if (!results) { + return ESP_ERR_INVALID_ARG; + } + + *results = NULL; + + if (!mdns_priv_is_server_init()) { + return ESP_ERR_INVALID_STATE; + } + + if (!timeout) { + return ESP_ERR_INVALID_ARG; + } + + search = search_init_with_subtype(NULL, service_type, proto, subtype, MDNS_TYPE_PTR, false, timeout, + max_results, NULL); + if (!search) { + return ESP_ERR_NO_MEM; + } + + if (send_search_action(ACTION_SEARCH_ADD, search)) { + search_free(search); + return ESP_ERR_NO_MEM; + } + xSemaphoreTake(search->done_semaphore, portMAX_DELAY); + + *results = search->result; + search_free(search); + + return ESP_OK; +} + +mdns_search_once_t *mdns_query_async_new_subtype(const char *service_type, const char *proto, const char *subtype, + uint32_t timeout, size_t max_results, mdns_query_notify_t notifier) +{ + mdns_search_once_t *search = NULL; + + if (!mdns_priv_is_server_init() || !timeout || mdns_utils_str_null_or_empty(service_type) || + mdns_utils_str_null_or_empty(proto) || mdns_utils_str_null_or_empty(subtype)) { + return NULL; + } + + search = search_init_with_subtype(NULL, service_type, proto, subtype, MDNS_TYPE_PTR, false, timeout, + max_results, notifier); + if (!search) { + return NULL; + } + + if (send_search_action(ACTION_SEARCH_ADD, search)) { + search_free(search); + return NULL; + } + + return search; +} + esp_err_t mdns_query_srv(const char *instance, const char *service, const char *proto, uint32_t timeout, mdns_result_t **result) { if (mdns_utils_str_null_or_empty(instance) || mdns_utils_str_null_or_empty(service) || mdns_utils_str_null_or_empty(proto)) { diff --git a/components/mdns/mdns_send.c b/components/mdns/mdns_send.c index 0ea8185a39..a40cfdc5cc 100644 --- a/components/mdns/mdns_send.c +++ b/components/mdns/mdns_send.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -413,6 +413,7 @@ static bool append_host_question(mdns_out_question_t **questions, const char *ho q->service = NULL; q->proto = NULL; q->domain = MDNS_UTILS_DEFAULT_DOMAIN; + q->subtype = NULL; q->own_dynamic_memory = false; if (question_exists(q, *questions)) { mdns_mem_free(q); @@ -636,6 +637,7 @@ void mdns_priv_create_answer_from_parsed_packet(mdns_parsed_packet_t *parsed_pac q->proto = NULL; out_question->domain = q->domain; q->domain = NULL; + out_question->subtype = NULL; out_question->next = NULL; out_question->own_dynamic_memory = true; queueToEnd(mdns_out_question_t, packet->questions, out_question); @@ -754,9 +756,12 @@ static uint16_t append_question(uint8_t *packet, uint16_t *index, mdns_out_quest } else #endif /* CONFIG_MDNS_RESPOND_REVERSE_QUERIES */ { - const char *str[4]; + const char *str[6]; uint8_t str_index = 0; - if (q->host) { + if (q->subtype) { + str[str_index++] = q->subtype; + str[str_index++] = MDNS_SUB_STR; + } else if (q->host) { str[str_index++] = q->host; } if (q->service) { @@ -1413,6 +1418,7 @@ mdns_tx_packet_t *mdns_priv_create_probe_packet(mdns_if_t tcpip_if, mdns_ip_prot q->service = services[i]->service->service; q->proto = services[i]->service->proto; q->domain = MDNS_UTILS_DEFAULT_DOMAIN; + q->subtype = NULL; q->own_dynamic_memory = false; if (!q->host || question_exists(q, packet->questions)) { mdns_mem_free(q); diff --git a/components/mdns/private_include/mdns_private.h b/components/mdns/private_include/mdns_private.h index e48c85f80b..98cac93029 100644 --- a/components/mdns/private_include/mdns_private.h +++ b/components/mdns/private_include/mdns_private.h @@ -280,6 +280,7 @@ typedef struct mdns_out_question_s { const char *service; const char *proto; const char *domain; + const char *subtype; bool own_dynamic_memory; } mdns_out_question_t; @@ -348,6 +349,7 @@ typedef struct mdns_search_once_s { char *instance; char *service; char *proto; + char *subtype; mdns_result_t *result; } mdns_search_once_t; @@ -359,6 +361,7 @@ typedef struct mdns_browse_s { char *service; char *proto; + char *subtype; mdns_result_t *result; } mdns_browse_t;