From c30c145a4a6c4f3ec722483af53de198a01aa14b Mon Sep 17 00:00:00 2001 From: kerms Date: Sat, 6 Jul 2024 15:47:08 +0800 Subject: [PATCH] feat(ssdp) new method to be discovered in a network --- components/SSDP/CMakeLists.txt | 8 + components/SSDP/ssdp.c | 372 ++++++++++++++++++ components/SSDP/ssdp.h | 15 + components/memory_pool/memory_pool.c | 2 +- project_components/web_server/CMakeLists.txt | 2 +- .../web_server/uri_modules/uri_html_base.c | 13 +- project_components/web_server/web_server.c | 1 + .../wifi_manager/CMakeLists.txt | 2 +- .../wifi_manager/wifi_event_handler.c | 12 +- .../wifi_manager/wifi_manager.c | 5 + 10 files changed, 426 insertions(+), 6 deletions(-) create mode 100644 components/SSDP/CMakeLists.txt create mode 100644 components/SSDP/ssdp.c create mode 100644 components/SSDP/ssdp.h diff --git a/components/SSDP/CMakeLists.txt b/components/SSDP/CMakeLists.txt new file mode 100644 index 0000000..7b4812e --- /dev/null +++ b/components/SSDP/CMakeLists.txt @@ -0,0 +1,8 @@ +file(GLOB SOURCES ssdp.c + ) + +idf_component_register( + SRCS ${SOURCES} + INCLUDE_DIRS "." + PRIV_REQUIRES esp_netif memory_pool wt_storage +) diff --git a/components/SSDP/ssdp.c b/components/SSDP/ssdp.c new file mode 100644 index 0000000..ef819b4 --- /dev/null +++ b/components/SSDP/ssdp.c @@ -0,0 +1,372 @@ +#include "ssdp.h" + +#include + +#include "esp_log.h" +#include "esp_mac.h" +#include "esp_netif.h" +#include "lwip/err.h" +#include "lwip/sockets.h" +#include "memory_pool.h" +#include "wt_nvs.h" + +#define TAG "SSDP" + +#define SSDP_PORT 1900 +#define SSDP_MULTICAST_TTL 4 +#define SSDP_UUID_BASE "9079d7e9-92c6-45b3-aa28-77cdcc" +#define SSDP_MULTICAST_ADDR "239.255.255.250" +#define HEADER_SEARCH "M-SEARCH" +#define SSDP_DEVICE_TYPE "urn:schemas-upnp-org:device:Basic:1" +#define SSDP_MODEL_NAME "wireless-proxy" +#define SSDP_DEFAULT_FRIENDLY_NAME "允斯调试器" +#define SSDP_MANUFACTURER "允斯工作室" + +#ifndef SSDP_MODEL_URL +#define SSDP_MODEL_URL "https://yunsi.studio/" +#endif + +static struct ssdp_ctx_t { + TaskHandle_t ssdp_service_task; + ip4_addr_t ip; + ip4_addr_t gw; + uint8_t uuid_end[3]; /* actually use MAC[3:5] */ + char friendly_name[32]; +} ssdp_ctx; + +static volatile uint8_t ssdp_running = false; +static int ssdp_socket = -1; + +static void ssdp_service_task(void *pvParameters); +static int ssdp_handle_data(int sock, in_addr_t remote_addr, uint16_t remote_port, + char *buf, int len); +static int ssdp_get_friendly_name(); + +static int create_ssdp_socket(int *sock) +{ + int err; + + *sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_IP); + if (*sock < 0) { + ESP_LOGE(TAG, "Failed to create socket. Error %d", errno); + return -1; + } + + int opt = 1; + if (setsockopt(*sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) != 0) { + ESP_LOGE(TAG, "setsockopt SO_REUSEADDR failed %d\n", errno); + goto err; + } + + struct sockaddr_in addr = { + .sin_family = AF_INET, + .sin_port = htons(SSDP_PORT), + .sin_addr.s_addr = htonl(INADDR_ANY) + }; + err = bind(*sock, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)); + if (err < 0) { + ESP_LOGE(TAG, "Failed to bind socket. Error %d", errno); + goto err; + } + + /* Multicast TTL (independent to normal interface TTL) + * should be set to small value like 2~4 */ + opt = SSDP_MULTICAST_TTL; + if (setsockopt(*sock, IPPROTO_IP, IP_MULTICAST_TTL, &opt, sizeof(opt)) < 0) { + ESP_LOGE(TAG, "Failed to set IP_MULTICAST_TTL. Error %d", errno); + err = errno; + goto err; + } + + /* Receive multicast packet sent from this device, + * useful when other service need to be aware of multicast notification */ + opt = 0; + if (setsockopt(*sock, IPPROTO_IP, IP_MULTICAST_LOOP, &opt, sizeof(opt)) < 0) { + ESP_LOGE(TAG, "Failed to unset IP_MULTICAST_LOOP. Error %d", errno); + err = errno; + goto err; + } + + struct ip_mreq mreq; + mreq.imr_multiaddr.s_addr = inet_addr(SSDP_MULTICAST_ADDR); + mreq.imr_interface.s_addr = htonl(INADDR_ANY); + if (setsockopt(*sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) { + perror("Adding to multicast group failed"); + goto err; + } + + return 0; + +err: + close(*sock); + return -1; +} + +static int ssdp_send_notify(int sock, char *buf, int buf_len) +{ + int msg_len = snprintf( + buf, buf_len, + "NOTIFY * HTTP/1.1\r\n" + "HOST: 239.255.255.250:1900\r\n" + "NTS: ssdp:alive\r\n" + "NT: upnp:rootdevice\r\n" + "CACHE-CONTROL: max-age=3600\r\n" + "SERVER: FreeRTOS/v10 UPnP/1.1 product/version\r\n" + "USN: uuid:"SSDP_UUID_BASE"%02x%02x%02x::upnp:rootdevice\r\n" + "LOCATION: http://%s:%u/SSDP.xml\r\n", + ssdp_ctx.uuid_end[0], ssdp_ctx.uuid_end[1], ssdp_ctx.uuid_end[2], + ip4addr_ntoa((const ip4_addr_t *)&ssdp_ctx.ip), 80 + ); + buf[msg_len] = '\0'; + struct sockaddr_in dest_addr; + dest_addr.sin_family = AF_INET; + dest_addr.sin_port = htons(SSDP_PORT); + dest_addr.sin_addr.s_addr = inet_addr(SSDP_MULTICAST_ADDR); + + int err = sendto(sock, buf, msg_len, 0, + (struct sockaddr *)&dest_addr, sizeof(dest_addr)); + if (err < 0) { + ESP_LOGE(TAG, "IPV4 sendto failed. errno: %d", errno); + return errno; + } + + return 0; +} + +static int ssdp_send_response(int sock, in_addr_t remote_addr, uint16_t remote_port, char *buf, int buf_len) +{ + int msg_len = snprintf( + buf, buf_len, + "HTTP/1.1 200 OK\r\n" + "EXT:\r\n" + "ST: ""upnp:rootdevice""\r\n" + "CACHE-CONTROL: max-age=3600\r\n" + "SERVER: FreeRTOS/v10 UPnP/1.1 product/version\r\n" + "USN: uuid:"SSDP_UUID_BASE"%02x%02x%02x" /* MAC=UUID */ + "::""upnp:rootdevice""\r\n" + "LOCATION: http://%s:%u/SSDP.xml\r\n" /* ip:port */ + "\r\n", + ssdp_ctx.uuid_end[0], ssdp_ctx.uuid_end[1], ssdp_ctx.uuid_end[2], /* MAC=UUID */ + ip4addr_ntoa((const ip4_addr_t *)&ssdp_ctx.ip), 80 /* ip:port */ + ); + + buf[msg_len] = '\0'; + + struct sockaddr_in dest_addr; + dest_addr.sin_family = AF_INET; + dest_addr.sin_port = remote_port; + dest_addr.sin_addr.s_addr = remote_addr; + + int err = sendto(sock, buf, msg_len, 0, + (struct sockaddr *)&dest_addr, sizeof(dest_addr)); + if (err < 0) { + ESP_LOGE(TAG, "IPV4 sendto failed. errno: %d", errno); + return errno; + } + + return 0; +} + +static int ssdp_handle_data(int sock, in_addr_t remote_addr, uint16_t remote_port, + char *buf, int len) +{ + /* on M-SEARCH: send RESPONSE anyway (even if this device doesn't match search type) + * on NOTIFY : ignore + * on RESPONSE: HTTP/1.1 200 OK : ignore + * */ + if (memcmp(buf, HEADER_SEARCH, strlen(HEADER_SEARCH)) != 0) { + return 0; + } + + return ssdp_send_response(sock, remote_addr, remote_port, buf, len); +} + +static void ssdp_service_handle_socket() +{ + char *buf = memory_pool_get(portMAX_DELAY); + + while (1) { + struct sockaddr_storage remote_addr; + socklen_t socklen = sizeof(remote_addr); + int len = recvfrom(ssdp_socket, buf, memory_pool_get_buf_size(), 0, + (struct sockaddr *)&remote_addr, &socklen); + if (len < 0) { + ESP_LOGE(TAG, "multicast recvfrom failed: errno %d", errno); + break; + } else if (len == 0) { + continue; + } + buf[len] = '\0'; + + uint16_t remote_port = ((struct sockaddr_in *)&remote_addr)->sin_port; + in_addr_t remote_ip = ((struct sockaddr_in *)&remote_addr)->sin_addr.s_addr; + ssdp_handle_data(ssdp_socket, remote_ip, remote_port, + buf, memory_pool_get_buf_size()); + } + + memory_pool_put(buf); +} + +static void ssdp_service_task(void *pvParameters) +{ + ESP_LOGD(TAG, "ssdp_service_task()"); + ssdp_running = true; + + int err; + while (ssdp_running) { + err = create_ssdp_socket(&ssdp_socket); + if (err) { + ssdp_socket = -1; + ESP_LOGE(TAG, "Failed to create multicast socket"); + vTaskDelay(pdMS_TO_TICKS(1000)); + continue; + } + + ssdp_service_handle_socket(); + + ESP_LOGE(TAG, "Shutting down socket and restarting..."); + shutdown(ssdp_socket, 0); + close(ssdp_socket); + ssdp_socket = -1; + } + + vTaskDelete(NULL); +} + +/* + * Global Functions + */ +esp_err_t ssdp_init() +{ + ssdp_ctx.ssdp_service_task = NULL; + ssdp_socket = -1; + ssdp_running = false; + ssdp_ctx.ip.addr = 0; + ssdp_ctx.gw.addr = 0; + + uint8_t mac[6]; + esp_base_mac_addr_get(mac); + for (int i = 0; i < 3; ++i) { + /* use MAC last 3 bytes, as the first 3 bytes do not change */ + ssdp_ctx.uuid_end[i] = mac[i + 3]; + } + + /* get name from nvs, if failed: use default */ + if (ssdp_get_friendly_name()) + ssdp_set_friendly_name(SSDP_DEFAULT_FRIENDLY_NAME); + return ESP_OK; +} + +esp_err_t ssdp_start() +{ + if (ssdp_socket != -1 || ssdp_ctx.ssdp_service_task) { + ESP_LOGE(TAG, "SSDP already started"); + return ESP_ERR_INVALID_STATE; + } + + /* get a default ip from available netif */ + esp_err_t err; + esp_netif_ip_info_t ip_info = {0}; + esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + if (netif == NULL) { + netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"); + } + if (netif == NULL) { + netif = esp_netif_get_handle_from_ifkey("ETH_DEF"); + } + + err = esp_netif_get_ip_info(netif, &ip_info); + if (err != ESP_OK) { + /* fallback to 0.0.0.0 default IP */ + ssdp_ctx.ip.addr = 0; + ssdp_ctx.gw.addr = 0; + } else { + ssdp_ctx.ip.addr = ip_info.ip.addr; + ssdp_ctx.gw.addr = ip_info.gw.addr; + } + + BaseType_t res = xTaskCreatePinnedToCore( + ssdp_service_task, "ssdp task", 4096, NULL, + tskIDLE_PRIORITY + 1, &ssdp_ctx.ssdp_service_task, + 0); + if (res != pdPASS || ssdp_ctx.ssdp_service_task == NULL) { + ESP_LOGE(TAG, "Failed to create task"); + err = ESP_FAIL; + } + return err; +} + +void ssdp_stop() +{ + ESP_LOGD(TAG, "Stopping SSDP"); + if (ssdp_socket != -1) { + shutdown(ssdp_socket, 0); + close(ssdp_socket); + ssdp_socket = -1; + } + ssdp_running = false; + ssdp_ctx.ssdp_service_task = NULL; +} + + +#define WT_NVS_KEY_SSDP_FRIENDLY_NAME 0x80 +#define WT_NVS_NS_NETWORK "net" + +int ssdp_get_friendly_name() +{ + return wt_nvs_get_once(WT_NVS_NS_NETWORK, WT_NVS_KEY_SSDP_FRIENDLY_NAME, + ssdp_ctx.friendly_name, sizeof(ssdp_ctx.friendly_name)); +} + +int ssdp_set_friendly_name(const char *name) +{ + snprintf(ssdp_ctx.friendly_name, sizeof(ssdp_ctx.friendly_name), + "%s", name); + ssdp_ctx.friendly_name[sizeof(ssdp_ctx.friendly_name) - 1] = '\0'; + return 0; +} + +int ssdp_set_ip_gw(const uint32_t *ip, const uint32_t *gw) +{ + ssdp_ctx.ip.addr = *ip; + ssdp_ctx.gw.addr = *gw; + + char *buf = memory_pool_get(portMAX_DELAY); + + /* send a ssdp notification to the new connected network */ + ssdp_send_notify(ssdp_socket, buf, memory_pool_get_buf_size()); + memory_pool_put(buf); + return 0; +} + +int ssdp_get_schema_str(char *buf, uint32_t buf_len) +{ + int write_size = snprintf( + buf, buf_len - 1, + "" + "" + "" + "1" + "0" + "" + "http://%s:%u" /* ip, port */ + "" + "%s" /* friendly_name */ + ""SSDP_DEVICE_TYPE"" + "/" + "0000" + ""SSDP_MODEL_NAME"" + "0000" + ""SSDP_MODEL_URL"" + ""SSDP_MANUFACTURER"" + "https://yunsi.studio" + "" + "\r\n" + "\r\n", + ip4addr_ntoa((const ip4_addr_t *)&ssdp_ctx.ip), 80, /* ip, port */ + ssdp_ctx.friendly_name /* friendly_name */ + ); + + buf[write_size] = '\0'; + return write_size < buf_len - 1; +} \ No newline at end of file diff --git a/components/SSDP/ssdp.h b/components/SSDP/ssdp.h new file mode 100644 index 0000000..832fb32 --- /dev/null +++ b/components/SSDP/ssdp.h @@ -0,0 +1,15 @@ +#ifndef SSDP_HEADER_GUARD +#define SSDP_HEADER_GUARD +#include +#include + +esp_err_t ssdp_init(); +esp_err_t ssdp_start(); +void ssdp_stop(); + +int ssdp_set_friendly_name(const char *name); +int ssdp_set_ip_gw(const uint32_t *ip, const uint32_t *gw); +int ssdp_get_schema_str(char *buf, uint32_t buf_len); + + +#endif /* SSDP_HEADER_GUARD */ \ No newline at end of file diff --git a/components/memory_pool/memory_pool.c b/components/memory_pool/memory_pool.c index fe8b9a9..cc3c56d 100644 --- a/components/memory_pool/memory_pool.c +++ b/components/memory_pool/memory_pool.c @@ -8,7 +8,7 @@ #include #include -#define BUFFER_NR 8 +#define BUFFER_NR 7 #define BUFFER_SZ 2048 static uint8_t buf[BUFFER_NR][BUFFER_SZ]; diff --git a/project_components/web_server/CMakeLists.txt b/project_components/web_server/CMakeLists.txt index 925ace4..e25b288 100644 --- a/project_components/web_server/CMakeLists.txt +++ b/project_components/web_server/CMakeLists.txt @@ -10,7 +10,7 @@ idf_component_register( SRCS ${SOURCES} INCLUDE_DIRS "." REQUIRES esp_http_server - PRIV_REQUIRES request_runner api_router json memory_pool utils html + PRIV_REQUIRES request_runner api_router json memory_pool utils html SSDP ) idf_component_set_property(${COMPONENT_NAME} WHOLE_ARCHIVE ON) diff --git a/project_components/web_server/uri_modules/uri_html_base.c b/project_components/web_server/uri_modules/uri_html_base.c index e744295..959fa53 100644 --- a/project_components/web_server/uri_modules/uri_html_base.c +++ b/project_components/web_server/uri_modules/uri_html_base.c @@ -1,12 +1,16 @@ #include "web_uri_module.h" +#include "ssdp.h" #include #include +#include "memory_pool.h" #define TAG __FILE_NAME__ -#define URI_ROOT 0x0000002F /* / */ -#define URI_WS_DOT 0x2E73772F /* /ws. */ +#define MAKE_U32(b0, b1, b2, b3) ((b0) | (b1) << 8 | (b2) << 16 | (b3) << 24) +#define URI_ROOT MAKE_U32 ('/', '\0', '\0', '\0')/* / */ +#define URI_WS_DOT MAKE_U32('/', 'w', 's', '.') /* /ws. */ +#define URI_SSDP MAKE_U32 ('/', 'S', 'S', 'D') /* /SSD */ #define HTML_INDEX "index_html_gz" #define JS_WS_SHARED "ws_sharedworker_js_gz" @@ -29,6 +33,11 @@ static esp_err_t html_base_get_handler(httpd_req_t *req) httpd_resp_set_type(req, "text/javascript"); httpd_resp_set_hdr(req, "Content-encoding", "gzip"); SEND_FILE(req, JS_WS_SHARED); + } else if (*URI_HASH == URI_SSDP) { + httpd_resp_set_type(req, "text/xml"); + char *buf = memory_pool_get(portMAX_DELAY); + ssdp_get_schema_str(buf, memory_pool_get_buf_size()); + httpd_resp_send(req, buf, strlen(buf)); } else { httpd_resp_set_type(req, "text/html"); httpd_resp_set_hdr(req, "Content-encoding", "gzip"); diff --git a/project_components/web_server/web_server.c b/project_components/web_server/web_server.c index a94afea..6106219 100644 --- a/project_components/web_server/web_server.c +++ b/project_components/web_server/web_server.c @@ -1,5 +1,6 @@ #include "web_server.h" #include "web_uri_module.h" +#include "ssdp.h" #include #include diff --git a/project_components/wifi_manager/CMakeLists.txt b/project_components/wifi_manager/CMakeLists.txt index 4898152..5636202 100644 --- a/project_components/wifi_manager/CMakeLists.txt +++ b/project_components/wifi_manager/CMakeLists.txt @@ -11,7 +11,7 @@ file(GLOB SOURCES idf_component_register( SRCS ${SOURCES} INCLUDE_DIRS "." - PRIV_REQUIRES mdns esp_wifi esp_event api_router wt_storage driver + PRIV_REQUIRES mdns esp_wifi esp_event api_router wt_storage driver SSDP ) idf_component_set_property(${COMPONENT_NAME} WHOLE_ARCHIVE ON) diff --git a/project_components/wifi_manager/wifi_event_handler.c b/project_components/wifi_manager/wifi_event_handler.c index 1ec152a..4821765 100644 --- a/project_components/wifi_manager/wifi_event_handler.c +++ b/project_components/wifi_manager/wifi_event_handler.c @@ -7,6 +7,9 @@ #include #include +#include "ssdp.h" +#include "wifi_configuration.h" + #define TAG __FILE_NAME__ void event_on_connected(ip_event_got_ip_t *event); @@ -22,11 +25,18 @@ void ip_event_handler(void *handler_arg __attribute__((unused)), printf("STA GOT IP : %s\n", ip4addr_ntoa((const ip4_addr_t *) &event->ip_info.ip)); event_on_connected(event); + ssdp_set_ip_gw(&event->ip_info.ip.addr, &event->ip_info.gw.addr); break; } - case IP_EVENT_STA_LOST_IP: + case IP_EVENT_STA_LOST_IP: { printf("sta lost ip\n"); + ip4_addr_t ip; + ip4_addr_t gw; + IP4_ADDR_EXPAND(&ip, WIFI_DEFAULT_AP_IP); + IP4_ADDR_EXPAND(&gw, WIFI_DEFAULT_AP_GATEWAY); + ssdp_set_ip_gw(&ip.addr, &gw.addr); break; + } case IP_EVENT_AP_STAIPASSIGNED: printf("event STAIPASSIGNED\n"); break; diff --git a/project_components/wifi_manager/wifi_manager.c b/project_components/wifi_manager/wifi_manager.c index 1ae7001..c650b8f 100644 --- a/project_components/wifi_manager/wifi_manager.c +++ b/project_components/wifi_manager/wifi_manager.c @@ -19,6 +19,8 @@ #include #include +#include "ssdp.h" + #define TAG __FILENAME__ typedef struct wifi_ctx_t { @@ -131,6 +133,9 @@ void wifi_manager_init(void) if (do_connect) { disconn_handler(); } + + ssdp_init(); + ssdp_start(); } void wifi_led_init()