// Copyright 2019 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include #include #include #include #include #include #include #include #include #include #include #include #include "wifi_provisioning_priv.h" #define WIFI_PROV_MGR_VERSION "v1.0" static const char *TAG = "wifi_prov_mgr"; typedef enum { WIFI_PROV_STATE_IDLE, WIFI_PROV_STATE_STARTING, WIFI_PROV_STATE_STARTED, WIFI_PROV_STATE_CRED_RECV, WIFI_PROV_STATE_FAIL, WIFI_PROV_STATE_SUCCESS, WIFI_PROV_STATE_STOPPING } wifi_prov_mgr_state_t; /** * @brief Structure for storing capabilities supported by * the provisioning service */ struct wifi_prov_capabilities { /* Proof of Possession is not required for establishing session */ bool no_pop; /* Provisioning doesn't stop on it's own after receiving Wi-Fi credentials * instead application has to explicitly call wifi_prov_mgr_stop_provisioning() */ bool no_auto_stop; }; /** * @brief Structure for storing miscellaneous information about * provisioning service that will be conveyed to clients */ struct wifi_prov_info { const char *version; struct wifi_prov_capabilities capabilities; }; /** * @brief Context data for provisioning manager */ struct wifi_prov_mgr_ctx { /* Provisioning manager configuration */ wifi_prov_mgr_config_t mgr_config; /* State of the provisioning service */ wifi_prov_mgr_state_t prov_state; /* Provisioning scheme configuration */ void *prov_scheme_config; /* Protocomm handle */ protocomm_t *pc; /* Type of security to use with protocomm */ int security; /* Pointer to proof of possession */ protocomm_security_pop_t pop; /* Handle to timer */ esp_timer_handle_t timer; /* State of Wi-Fi Station */ wifi_prov_sta_state_t wifi_state; /* Code for Wi-Fi station disconnection (if disconnected) */ wifi_prov_sta_fail_reason_t wifi_disconnect_reason; /* Protocomm handlers for Wi-Fi configuration endpoint */ wifi_prov_config_handlers_t *wifi_prov_handlers; /* Count of used endpoint UUIDs */ unsigned int endpoint_uuid_used; /* Provisioning service information */ struct wifi_prov_info mgr_info; /* Application related information in JSON format */ cJSON *app_info_json; /* Delay after which resources will be cleaned up asynchronously * upon execution of wifi_prov_mgr_stop_provisioning() */ uint32_t cleanup_delay; }; /* Mutex to lock/unlock access to provisioning singleton * context data. This is allocated only once on first init * and never deleted as wifi_prov_mgr is a singleton */ static SemaphoreHandle_t prov_ctx_lock = NULL; /* Pointer to provisioning context data */ static struct wifi_prov_mgr_ctx *prov_ctx; /* This executes registered app_event_callback for a particular event * * NOTE : By the time this fucntion returns, it is possible that * the manager got de-initialized due to a call to wifi_prov_mgr_deinit() * either inside the event callbacks or from another thread. Therefore * post execution of execute_event_cb(), the validity of prov_ctx must * always be checked. A cleaner way, to avoid this pitfall safely, would * be to limit the usage of this function to only public APIs, and that * too at the very end, just before returning. * * NOTE: This function should be called only after ensuring that the * context is valid and the control mutex is locked. */ static void execute_event_cb(wifi_prov_cb_event_t event_id, void *event_data) { ESP_LOGD(TAG, "execute_event_cb : %d", event_id); if (prov_ctx) { wifi_prov_cb_func_t app_cb = prov_ctx->mgr_config.app_event_handler.event_cb; void *app_data = prov_ctx->mgr_config.app_event_handler.user_data; wifi_prov_cb_func_t scheme_cb = prov_ctx->mgr_config.scheme_event_handler.event_cb; void *scheme_data = prov_ctx->mgr_config.scheme_event_handler.user_data; /* Release the mutex before executing the callbacks. This is done so that * wifi_prov_mgr_event_handler() doesn't stay blocked for the duration */ xSemaphoreGiveRecursive(prov_ctx_lock); if (scheme_cb) { /* Call scheme specific event handler */ scheme_cb(scheme_data, event_id, event_data); } if (app_cb) { /* Call application specific event handler */ app_cb(app_data, event_id, event_data); } xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); } } esp_err_t wifi_prov_mgr_set_app_info(const char *label, const char *version, const char**capabilities, size_t total_capabilities) { if (!label || !version || !capabilities) { return ESP_ERR_INVALID_ARG; } if (!prov_ctx_lock) { ESP_LOGE(TAG, "Provisioning manager not initialized"); return ESP_ERR_INVALID_STATE; } esp_err_t ret = ESP_FAIL; xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); if (prov_ctx && prov_ctx->prov_state == WIFI_PROV_STATE_IDLE) { if (!prov_ctx->app_info_json) { prov_ctx->app_info_json = cJSON_CreateObject(); } cJSON *new_entry_json = cJSON_CreateObject(); cJSON *capabilities_json = cJSON_CreateArray(); cJSON_AddItemToObject(prov_ctx->app_info_json, label, new_entry_json); /* Version ("ver") */ cJSON_AddStringToObject(new_entry_json, "ver", version); /* List of capabilities ("cap") */ cJSON_AddItemToObject(new_entry_json, "cap", capabilities_json); for (unsigned int i = 0; i < total_capabilities; i++) { if (capabilities[i]) { cJSON_AddItemToArray(capabilities_json, cJSON_CreateString(capabilities[i])); } } } else { ret = ESP_ERR_INVALID_STATE; } xSemaphoreGiveRecursive(prov_ctx_lock); return ret; } static cJSON* wifi_prov_get_info_json(void) { cJSON *full_info_json = prov_ctx->app_info_json ? cJSON_Duplicate(prov_ctx->app_info_json, 1) : cJSON_CreateObject(); cJSON *prov_info_json = cJSON_CreateObject(); cJSON *prov_capabilities = cJSON_CreateArray(); /* Use label "prov" to indicate provisioning related information */ cJSON_AddItemToObject(full_info_json, "prov", prov_info_json); /* Version field */ cJSON_AddStringToObject(prov_info_json, "ver", prov_ctx->mgr_info.version); /* Capabilities field */ cJSON_AddItemToObject(prov_info_json, "cap", prov_capabilities); /* If Proof of Possession is not used, indicate in capabilities */ if (prov_ctx->mgr_info.capabilities.no_pop) { cJSON_AddItemToArray(prov_capabilities, cJSON_CreateString("no_pop")); } return full_info_json; } static esp_err_t wifi_prov_mgr_start_service(const char *service_name, const char *service_key) { const wifi_prov_scheme_t *scheme = &prov_ctx->mgr_config.scheme; esp_err_t ret; /* Create new protocomm instance */ prov_ctx->pc = protocomm_new(); if (prov_ctx->pc == NULL) { ESP_LOGE(TAG, "Failed to create new protocomm instance"); return ESP_FAIL; } ret = scheme->set_config_service(prov_ctx->prov_scheme_config, service_name, service_key); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to configure service"); protocomm_delete(prov_ctx->pc); return ret; } /* Start provisioning */ ret = scheme->prov_start(prov_ctx->pc, prov_ctx->prov_scheme_config); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to start service"); protocomm_delete(prov_ctx->pc); return ret; } /* Set version information / capabilities of provisioning service and application */ cJSON *version_json = wifi_prov_get_info_json(); ret = protocomm_set_version(prov_ctx->pc, "proto-ver", cJSON_Print(version_json)); cJSON_Delete(version_json); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to set version endpoint"); scheme->prov_stop(prov_ctx->pc); protocomm_delete(prov_ctx->pc); return ret; } /* Set protocomm security type for endpoint */ if (prov_ctx->security == 0) { ret = protocomm_set_security(prov_ctx->pc, "prov-session", &protocomm_security0, NULL); } else if (prov_ctx->security == 1) { ret = protocomm_set_security(prov_ctx->pc, "prov-session", &protocomm_security1, &prov_ctx->pop); } else { ESP_LOGE(TAG, "Unsupported protocomm security version %d", prov_ctx->security); ret = ESP_ERR_INVALID_ARG; } if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to set security endpoint"); scheme->prov_stop(prov_ctx->pc); protocomm_delete(prov_ctx->pc); return ret; } prov_ctx->wifi_prov_handlers = malloc(sizeof(wifi_prov_config_handlers_t)); if (!prov_ctx->wifi_prov_handlers) { ESP_LOGD(TAG, "Failed to allocate memory for provisioning handlers"); scheme->prov_stop(prov_ctx->pc); protocomm_delete(prov_ctx->pc); } *prov_ctx->wifi_prov_handlers = get_wifi_prov_handlers(); /* Add protocomm endpoint for Wi-Fi station configuration */ ret = protocomm_add_endpoint(prov_ctx->pc, "prov-config", wifi_prov_config_data_handler, prov_ctx->wifi_prov_handlers); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to set provisioning endpoint"); free(prov_ctx->wifi_prov_handlers); scheme->prov_stop(prov_ctx->pc); protocomm_delete(prov_ctx->pc); return ret; } ESP_LOGI(TAG, "Provisioning started with service name : %s ", service_name ? service_name : ""); return ESP_OK; } esp_err_t wifi_prov_mgr_endpoint_create(const char *ep_name) { if (!prov_ctx_lock) { ESP_LOGE(TAG, "Provisioning manager not initialized"); return ESP_ERR_INVALID_STATE; } esp_err_t err = ESP_FAIL; xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); if (prov_ctx && prov_ctx->prov_state == WIFI_PROV_STATE_IDLE) { err = prov_ctx->mgr_config.scheme.set_config_endpoint( prov_ctx->prov_scheme_config, ep_name, prov_ctx->endpoint_uuid_used + 1); } if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to create additional endpoint"); } else { prov_ctx->endpoint_uuid_used++; } xSemaphoreGiveRecursive(prov_ctx_lock); return err; } esp_err_t wifi_prov_mgr_endpoint_register(const char *ep_name, protocomm_req_handler_t handler, void *user_ctx) { if (!prov_ctx_lock) { ESP_LOGE(TAG, "Provisioning manager not initialized"); return ESP_ERR_INVALID_STATE; } esp_err_t err = ESP_FAIL; xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); if (prov_ctx && prov_ctx->prov_state > WIFI_PROV_STATE_STARTING && prov_ctx->prov_state < WIFI_PROV_STATE_STOPPING) { err = protocomm_add_endpoint(prov_ctx->pc, ep_name, handler, user_ctx); } xSemaphoreGiveRecursive(prov_ctx_lock); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to register handler for endpoint"); } return err; } void wifi_prov_mgr_endpoint_unregister(const char *ep_name) { if (!prov_ctx_lock) { ESP_LOGE(TAG, "Provisioning manager not initialized"); return; } xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); if (prov_ctx && prov_ctx->prov_state > WIFI_PROV_STATE_STARTING && prov_ctx->prov_state < WIFI_PROV_STATE_STOPPING) { protocomm_remove_endpoint(prov_ctx->pc, ep_name); } xSemaphoreGiveRecursive(prov_ctx_lock); } static void prov_stop_task(void *arg) { bool is_this_a_task = (bool) arg; wifi_prov_cb_func_t app_cb = prov_ctx->mgr_config.app_event_handler.event_cb; void *app_data = prov_ctx->mgr_config.app_event_handler.user_data; wifi_prov_cb_func_t scheme_cb = prov_ctx->mgr_config.scheme_event_handler.event_cb; void *scheme_data = prov_ctx->mgr_config.scheme_event_handler.user_data; /* This delay is so that the client side app is notified first * and then the provisioning is stopped. Generally 1000ms is enough. */ uint32_t cleanup_delay = prov_ctx->cleanup_delay > 100 ? prov_ctx->cleanup_delay : 100; vTaskDelay(cleanup_delay / portTICK_PERIOD_MS); /* All the extra application added endpoints are also * removed automatically when prov_stop is called */ prov_ctx->mgr_config.scheme.prov_stop(prov_ctx->pc); /* Delete protocomm instance */ protocomm_delete(prov_ctx->pc); prov_ctx->pc = NULL; /* Free provisioning handlers */ free(prov_ctx->wifi_prov_handlers->ctx); free(prov_ctx->wifi_prov_handlers); prov_ctx->wifi_prov_handlers = NULL; /* Switch device to Wi-Fi STA mode irrespective of * whether provisioning was completed or not */ esp_wifi_set_mode(WIFI_MODE_STA); ESP_LOGI(TAG, "Provisioning stopped"); if (is_this_a_task) { xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); prov_ctx->prov_state = WIFI_PROV_STATE_IDLE; xSemaphoreGiveRecursive(prov_ctx_lock); ESP_LOGD(TAG, "execute_event_cb : %d", WIFI_PROV_END); if (scheme_cb) { scheme_cb(scheme_data, WIFI_PROV_END, NULL); } if (app_cb) { app_cb(app_data, WIFI_PROV_END, NULL); } vTaskDelete(NULL); } } /* This will do one of these: * 1) if blocking is false, start a task for stopping the provisioning service (returns true) * 2) if blocking is true, stop provisioning service immediately (returns true) * 3) if service was already in the process of termination, in blocking mode this will * wait till the service is stopped (returns false) * 4) if service was not running, this will return immediately (returns false) * * NOTE: This function should be called only after ensuring that the context * is valid and the control mutex is locked * * NOTE: When blocking mode is selected, the event callbacks are not executed. * This help with de-initialization. */ static bool wifi_prov_mgr_stop_service(bool blocking) { if (blocking) { /* Wait for any ongoing calls to wifi_prov_mgr_start_service() or * wifi_prov_mgr_stop_service() from another thread to finish */ while (prov_ctx && ( prov_ctx->prov_state == WIFI_PROV_STATE_STARTING || prov_ctx->prov_state == WIFI_PROV_STATE_STOPPING)) { xSemaphoreGiveRecursive(prov_ctx_lock); vTaskDelay(100 / portTICK_PERIOD_MS); xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); } } else { /* Wait for any ongoing call to wifi_prov_mgr_start_service() * from another thread to finish */ while (prov_ctx && prov_ctx->prov_state == WIFI_PROV_STATE_STARTING) { xSemaphoreGiveRecursive(prov_ctx_lock); vTaskDelay(100 / portTICK_PERIOD_MS); xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); } if (prov_ctx && prov_ctx->prov_state == WIFI_PROV_STATE_STOPPING) { ESP_LOGD(TAG, "Provisioning is already stopping"); return false; } } if (!prov_ctx || prov_ctx->prov_state == WIFI_PROV_STATE_IDLE) { ESP_LOGD(TAG, "Provisioning not running"); return false; } /* Timer not needed anymore */ if (prov_ctx->timer) { esp_timer_stop(prov_ctx->timer); esp_timer_delete(prov_ctx->timer); prov_ctx->timer = NULL; } ESP_LOGD(TAG, "Stopping provisioning"); prov_ctx->prov_state = WIFI_PROV_STATE_STOPPING; /* Free proof of possession */ if (prov_ctx->pop.data) { free((void *)prov_ctx->pop.data); prov_ctx->pop.data = NULL; } if (blocking) { /* Run the cleanup without launching a separate task. Also the * WIFI_PROV_END event is not emitted in this case */ xSemaphoreGiveRecursive(prov_ctx_lock); prov_stop_task((void *)0); xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); prov_ctx->prov_state = WIFI_PROV_STATE_IDLE; } else { /* Launch cleanup task to perform the cleanup asynchronously. * It is important to do this asynchronously because, there are * situations in which the transport level resources have to be * released - some duration after - returning from a call to * wifi_prov_mgr_stop_provisioning(), like when it is called * inside a protocomm handler */ assert(xTaskCreate(prov_stop_task, "prov_stop_task", 4096, (void *)1, tskIDLE_PRIORITY, NULL) == pdPASS); ESP_LOGD(TAG, "Provisioning scheduled for stopping"); } return true; } /* Task spawned by timer callback */ static void stop_prov_timer_cb(void *arg) { wifi_prov_mgr_stop_provisioning(); } esp_err_t wifi_prov_mgr_disable_auto_stop(uint32_t cleanup_delay) { if (!prov_ctx_lock) { ESP_LOGE(TAG, "Provisioning manager not initialized"); return ESP_ERR_INVALID_STATE; } esp_err_t ret = ESP_FAIL; xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); if (prov_ctx && prov_ctx->prov_state == WIFI_PROV_STATE_IDLE) { prov_ctx->mgr_info.capabilities.no_auto_stop = true; prov_ctx->cleanup_delay = cleanup_delay; } else { ret = ESP_ERR_INVALID_STATE; } xSemaphoreGiveRecursive(prov_ctx_lock); return ret; } /* Call this if provisioning is completed before the timeout occurs */ esp_err_t wifi_prov_mgr_done(void) { if (!prov_ctx_lock) { ESP_LOGE(TAG, "Provisioning manager not initialized"); return ESP_ERR_INVALID_STATE; } xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); /* Stop provisioning if auto stop is not disabled */ if (prov_ctx && !prov_ctx->mgr_info.capabilities.no_auto_stop) { wifi_prov_mgr_stop_provisioning(); } xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_OK; } /* Event handler for starting/stopping provisioning. * To be called from within the context of the main * event handler */ esp_err_t wifi_prov_mgr_event_handler(void *ctx, system_event_t *event) { /* For accessing reason codes in case of disconnection */ system_event_info_t *info = &event->event_info; if (!prov_ctx_lock) { ESP_LOGE(TAG, "Provisioning manager not initialized"); return ESP_ERR_INVALID_STATE; } xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); /* If pointer to provisioning application data is NULL * then provisioning manager is not running, therefore * return with error to allow the global handler to act */ if (!prov_ctx) { xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_ERR_INVALID_STATE; } /* Only handle events when credential is received and * Wi-Fi STA is yet to complete trying the connection */ if (prov_ctx->prov_state != WIFI_PROV_STATE_CRED_RECV) { xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_OK; } esp_err_t ret = ESP_OK; switch (event->event_id) { case SYSTEM_EVENT_STA_START: ESP_LOGD(TAG, "STA Start"); /* Once configuration is received through protocomm, * device is started as station. Once station starts, * wait for connection to establish with configured * host SSID and password */ prov_ctx->wifi_state = WIFI_PROV_STA_CONNECTING; break; case SYSTEM_EVENT_STA_GOT_IP: ESP_LOGD(TAG, "STA Got IP"); /* Station got IP. That means configuration is successful. */ prov_ctx->wifi_state = WIFI_PROV_STA_CONNECTED; prov_ctx->prov_state = WIFI_PROV_STATE_SUCCESS; /* If auto stop is enabled (default), schedule timer to * stop provisioning app after 30 seconds. */ if (!prov_ctx->mgr_info.capabilities.no_auto_stop) { ESP_LOGD(TAG, "Starting 30s timer for stop_prov_timer_cb()"); esp_timer_start_once(prov_ctx->timer, 30000 * 1000U); } /* Execute user registered callback handler */ execute_event_cb(WIFI_PROV_CRED_SUCCESS, NULL); break; case SYSTEM_EVENT_STA_DISCONNECTED: ESP_LOGD(TAG, "STA Disconnected"); /* Station couldn't connect to configured host SSID */ prov_ctx->wifi_state = WIFI_PROV_STA_DISCONNECTED; ESP_LOGD(TAG, "Disconnect reason : %d", info->disconnected.reason); /* Set code corresponding to the reason for disconnection */ switch (info->disconnected.reason) { case WIFI_REASON_AUTH_EXPIRE: case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT: case WIFI_REASON_BEACON_TIMEOUT: case WIFI_REASON_AUTH_FAIL: case WIFI_REASON_ASSOC_FAIL: case WIFI_REASON_HANDSHAKE_TIMEOUT: ESP_LOGD(TAG, "STA Auth Error"); prov_ctx->wifi_disconnect_reason = WIFI_PROV_STA_AUTH_ERROR; break; case WIFI_REASON_NO_AP_FOUND: ESP_LOGD(TAG, "STA AP Not found"); prov_ctx->wifi_disconnect_reason = WIFI_PROV_STA_AP_NOT_FOUND; break; default: /* If none of the expected reasons, * retry connecting to host SSID */ prov_ctx->wifi_state = WIFI_PROV_STA_CONNECTING; esp_wifi_connect(); } /* In case of disconnection, update state of service and * run the event handler with disconnection reason as data */ if (prov_ctx->wifi_state == WIFI_PROV_STA_DISCONNECTED) { prov_ctx->prov_state = WIFI_PROV_STATE_FAIL; wifi_prov_sta_fail_reason_t reason = prov_ctx->wifi_disconnect_reason; /* Execute user registered callback handler */ execute_event_cb(WIFI_PROV_CRED_FAIL, (void *)&reason); } break; default: /* This event is not intended to be handled by this handler. * Return ESP_FAIL to signal global event handler to take * control */ ret = ESP_FAIL; break; } xSemaphoreGiveRecursive(prov_ctx_lock); return ret; } esp_err_t wifi_prov_mgr_get_wifi_state(wifi_prov_sta_state_t *state) { if (!prov_ctx_lock) { ESP_LOGE(TAG, "Provisioning manager not initialized"); return ESP_ERR_INVALID_STATE; } xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); if (prov_ctx == NULL || state == NULL) { xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_FAIL; } *state = prov_ctx->wifi_state; xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_OK; } esp_err_t wifi_prov_mgr_get_wifi_disconnect_reason(wifi_prov_sta_fail_reason_t *reason) { if (!prov_ctx_lock) { ESP_LOGE(TAG, "Provisioning manager not initialized"); return ESP_ERR_INVALID_STATE; } xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); if (prov_ctx == NULL || reason == NULL) { xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_FAIL; } if (prov_ctx->wifi_state != WIFI_PROV_STA_DISCONNECTED) { xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_FAIL; } *reason = prov_ctx->wifi_disconnect_reason; xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_OK; } static void debug_print_wifi_credentials(wifi_sta_config_t sta, const char* pretext) { size_t passlen = strlen((const char*) sta.password); ESP_LOGD(TAG, "%s Wi-Fi SSID : %.*s", pretext, strnlen((const char *) sta.ssid, sizeof(sta.ssid)), (const char *) sta.ssid); if (passlen) { /* Mask password partially if longer than 3, else mask it completely */ memset(sta.password + (passlen > 3), '*', passlen - 2*(passlen > 3)); ESP_LOGD(TAG, "%s Wi-Fi Password : %s", pretext, (const char *) sta.password); } } esp_err_t wifi_prov_mgr_is_provisioned(bool *provisioned) { if (!provisioned) { return ESP_ERR_INVALID_ARG; } *provisioned = false; if (!prov_ctx_lock) { ESP_LOGE(TAG, "Provisioning manager not initialized"); return ESP_ERR_INVALID_STATE; } /* Get Wi-Fi Station configuration */ wifi_config_t wifi_cfg; if (esp_wifi_get_config(ESP_IF_WIFI_STA, &wifi_cfg) != ESP_OK) { return ESP_FAIL; } if (strlen((const char *) wifi_cfg.sta.ssid)) { *provisioned = true; debug_print_wifi_credentials(wifi_cfg.sta, "Found"); } return ESP_OK; } esp_err_t wifi_prov_mgr_configure_sta(wifi_config_t *wifi_cfg) { if (!prov_ctx_lock) { ESP_LOGE(TAG, "Provisioning manager not initialized"); return ESP_ERR_INVALID_STATE; } xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); if (!prov_ctx) { ESP_LOGE(TAG, "Invalid state of Provisioning app"); xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_FAIL; } if (prov_ctx->prov_state >= WIFI_PROV_STATE_CRED_RECV) { ESP_LOGE(TAG, "Wi-Fi credentials already received by provisioning app"); xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_FAIL; } debug_print_wifi_credentials(wifi_cfg->sta, "Received"); /* Configure Wi-Fi as both AP and/or Station */ if (esp_wifi_set_mode(prov_ctx->mgr_config.scheme.wifi_mode) != ESP_OK) { ESP_LOGE(TAG, "Failed to set Wi-Fi mode"); xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_FAIL; } /* Don't release mutex yet as it is possible that right after * esp_wifi_connect() is called below, the related Wi-Fi event * happens even before manager state is updated in the next * few lines causing the internal event handler to miss */ /* Set Wi-Fi storage again to flash to keep the newly * provided credentials on NVS */ if (esp_wifi_set_storage(WIFI_STORAGE_FLASH) != ESP_OK) { ESP_LOGE(TAG, "Failed to set storage Wi-Fi"); xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_FAIL; } /* Configure Wi-Fi station with host credentials * provided during provisioning */ if (esp_wifi_set_config(ESP_IF_WIFI_STA, wifi_cfg) != ESP_OK) { ESP_LOGE(TAG, "Failed to set Wi-Fi configuration"); xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_FAIL; } /* (Re)Start Wi-Fi */ if (esp_wifi_start() != ESP_OK) { ESP_LOGE(TAG, "Failed to set Wi-Fi configuration"); xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_FAIL; } /* Connect to AP */ if (esp_wifi_connect() != ESP_OK) { ESP_LOGE(TAG, "Failed to connect Wi-Fi"); xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_FAIL; } /* This delay allows channel change to complete */ vTaskDelay(1000 / portTICK_PERIOD_MS); /* Reset Wi-Fi station state for provisioning app */ prov_ctx->wifi_state = WIFI_PROV_STA_CONNECTING; prov_ctx->prov_state = WIFI_PROV_STATE_CRED_RECV; /* Execute user registered callback handler */ execute_event_cb(WIFI_PROV_CRED_RECV, (void *)&wifi_cfg->sta); xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_OK; } esp_err_t wifi_prov_mgr_init(wifi_prov_mgr_config_t config) { if (!prov_ctx_lock) { /* Create mutex if this is the first time init is being called. * This is created only once and never deleted because if some * other thread is trying to take this mutex while it is being * deleted from another thread then the reference may become * invalid and cause exception */ prov_ctx_lock = xSemaphoreCreateRecursiveMutex(); if (!prov_ctx_lock) { ESP_LOGE(TAG, "Failed to create mutex"); return ESP_ERR_NO_MEM; } } void *fn_ptrs[] = { config.scheme.prov_stop, config.scheme.prov_start, config.scheme.new_config, config.scheme.delete_config, config.scheme.set_config_service, config.scheme.set_config_endpoint }; /* All function pointers in the scheme structure must be non-null */ for (int i = 0; i < sizeof(fn_ptrs)/sizeof(fn_ptrs[0]); i++) { if (!fn_ptrs[i]) { return ESP_ERR_INVALID_ARG; } } xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); if (prov_ctx) { ESP_LOGE(TAG, "Provisioning manager already initialized"); xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_ERR_INVALID_STATE; } /* Allocate memory for provisioning app data */ prov_ctx = (struct wifi_prov_mgr_ctx *) calloc(1, sizeof(struct wifi_prov_mgr_ctx)); if (!prov_ctx) { ESP_LOGE(TAG, "Error allocating memory for singleton instance"); xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_ERR_NO_MEM; } prov_ctx->mgr_config = config; prov_ctx->prov_state = WIFI_PROV_STATE_IDLE; prov_ctx->mgr_info.version = WIFI_PROV_MGR_VERSION; /* Allocate memory for provisioning scheme configuration */ const wifi_prov_scheme_t *scheme = &prov_ctx->mgr_config.scheme; esp_err_t ret = ESP_OK; prov_ctx->prov_scheme_config = scheme->new_config(); if (!prov_ctx->prov_scheme_config) { ESP_LOGE(TAG, "failed to allocate provisioning scheme configuration"); ret = ESP_ERR_NO_MEM; goto exit; } ret = scheme->set_config_endpoint(prov_ctx->prov_scheme_config, "prov-session", 0xFF51); if (ret != ESP_OK) { ESP_LOGE(TAG, "failed to configure security endpoint"); goto exit; } ret = scheme->set_config_endpoint(prov_ctx->prov_scheme_config, "prov-config", 0xFF52); if (ret != ESP_OK) { ESP_LOGE(TAG, "failed to configure Wi-Fi configuration endpoint"); goto exit; } ret = scheme->set_config_endpoint(prov_ctx->prov_scheme_config, "proto-ver", 0xFF53); if (ret != ESP_OK) { ESP_LOGE(TAG, "failed to configure version endpoint"); goto exit; } /* Application specific custom endpoints will be assigned * incremental UUIDs starting after this value */ prov_ctx->endpoint_uuid_used = 0xFF53; /* This delay is so that the client side app is notified first * and then the provisioning is stopped. Default is 1000ms. */ prov_ctx->cleanup_delay = 1000; exit: if (ret != ESP_OK) { if (prov_ctx->prov_scheme_config) { config.scheme.delete_config(prov_ctx->prov_scheme_config); } free(prov_ctx); } else { execute_event_cb(WIFI_PROV_INIT, NULL); } xSemaphoreGiveRecursive(prov_ctx_lock); return ret; } void wifi_prov_mgr_wait(void) { if (!prov_ctx_lock) { ESP_LOGE(TAG, "Provisioning manager not initialized"); return; } while (1) { xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); if (prov_ctx && prov_ctx->prov_state != WIFI_PROV_STATE_IDLE) { xSemaphoreGiveRecursive(prov_ctx_lock); vTaskDelay(1000 / portTICK_PERIOD_MS); continue; } break; } xSemaphoreGiveRecursive(prov_ctx_lock); } void wifi_prov_mgr_deinit(void) { if (!prov_ctx_lock) { ESP_LOGE(TAG, "Provisioning manager not initialized"); return; } xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); /* This will do one of these: * 1) if found running, stop the provisioning service (returns true) * 2) if service was already in the process of termination, this will * wait till the service is stopped (returns false) * 3) if service was not running, this will return immediately (returns false) */ bool service_was_running = wifi_prov_mgr_stop_service(1); /* If service was not running, its also possible that manager * was not even initialized */ if (!service_was_running && !prov_ctx) { ESP_LOGD(TAG, "Manager already de-initialized"); xSemaphoreGiveRecursive(prov_ctx_lock); return; } if (prov_ctx->app_info_json) { cJSON_Delete(prov_ctx->app_info_json); } if (prov_ctx->prov_scheme_config) { prov_ctx->mgr_config.scheme.delete_config(prov_ctx->prov_scheme_config); } /* Extract the callbacks to be called post deinit */ wifi_prov_cb_func_t app_cb = prov_ctx->mgr_config.app_event_handler.event_cb; void *app_data = prov_ctx->mgr_config.app_event_handler.user_data; wifi_prov_cb_func_t scheme_cb = prov_ctx->mgr_config.scheme_event_handler.event_cb; void *scheme_data = prov_ctx->mgr_config.scheme_event_handler.user_data; /* Free manager context */ free(prov_ctx); prov_ctx = NULL; xSemaphoreGiveRecursive(prov_ctx_lock); /* If a running service was also stopped during de-initialization * then WIFI_PROV_END event also needs to be emitted before deinit */ if (service_was_running) { ESP_LOGD(TAG, "execute_event_cb : %d", WIFI_PROV_END); if (scheme_cb) { scheme_cb(scheme_data, WIFI_PROV_END, NULL); } if (app_cb) { app_cb(app_data, WIFI_PROV_END, NULL); } } ESP_LOGD(TAG, "execute_event_cb : %d", WIFI_PROV_DEINIT); /* Execute deinit event callbacks */ if (scheme_cb) { scheme_cb(scheme_data, WIFI_PROV_DEINIT, NULL); } if (app_cb) { app_cb(app_data, WIFI_PROV_DEINIT, NULL); } } esp_err_t wifi_prov_mgr_start_provisioning(wifi_prov_security_t security, const char *pop, const char *service_name, const char *service_key) { if (!prov_ctx_lock) { ESP_LOGE(TAG, "Provisioning manager not initialized"); return ESP_ERR_INVALID_STATE; } xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); if (!prov_ctx) { ESP_LOGE(TAG, "Provisioning manager not initialized"); xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_ERR_INVALID_STATE; } if (prov_ctx->prov_state != WIFI_PROV_STATE_IDLE) { ESP_LOGE(TAG, "Provisioning service already started"); xSemaphoreGiveRecursive(prov_ctx_lock); return ESP_ERR_INVALID_STATE; } esp_err_t ret = ESP_OK; /* Update state so that parallel call to wifi_prov_mgr_start_provisioning() * or wifi_prov_mgr_stop_provisioning() or wifi_prov_mgr_deinit() from another * thread doesn't interfere with this process */ prov_ctx->prov_state = WIFI_PROV_STATE_STARTING; /* Change Wi-Fi storage to RAM temporarily and erase any old * credentials (i.e. without erasing the copy on NVS). Also * call disconnect to make sure device doesn't remain connected * to the AP whose credentials were present earlier */ wifi_config_t wifi_cfg_empty, wifi_cfg_old; memset(&wifi_cfg_empty, 0, sizeof(wifi_config_t)); esp_wifi_get_config(ESP_IF_WIFI_STA, &wifi_cfg_old); esp_wifi_set_storage(WIFI_STORAGE_RAM); esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_cfg_empty); esp_wifi_disconnect(); /* Initialize app data */ if (pop) { prov_ctx->pop.len = strlen(pop); prov_ctx->pop.data = malloc(prov_ctx->pop.len); if (!prov_ctx->pop.data) { ESP_LOGE(TAG, "Unable to allocate PoP data"); ret = ESP_ERR_NO_MEM; goto err; } memcpy((void *)prov_ctx->pop.data, pop, prov_ctx->pop.len); } else { prov_ctx->mgr_info.capabilities.no_pop = true; } prov_ctx->security = security; /* If auto stop on completion is enabled (default) create the stopping timer */ if (!prov_ctx->mgr_info.capabilities.no_auto_stop) { /* Create timer object as a member of app data */ esp_timer_create_args_t timer_conf = { .callback = stop_prov_timer_cb, .arg = NULL, .dispatch_method = ESP_TIMER_TASK, .name = "wifi_prov_mgr_tm" }; ret = esp_timer_create(&timer_conf, &prov_ctx->timer); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to create timer"); free((void *)prov_ctx->pop.data); goto err; } } /* System APIs for BLE / Wi-Fi will be called inside wifi_prov_mgr_start_service(), * which may trigger system level events. Hence, releasing the context lock will * ensure that wifi_prov_mgr_event_handler() doesn't block the global event_loop * handler when system events need to be handled */ xSemaphoreGiveRecursive(prov_ctx_lock); /* Start provisioning service */ ret = wifi_prov_mgr_start_service(service_name, service_key); if (ret != ESP_OK) { esp_timer_delete(prov_ctx->timer); free((void *)prov_ctx->pop.data); } xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); if (ret == ESP_OK) { prov_ctx->prov_state = WIFI_PROV_STATE_STARTED; /* Execute user registered callback handler */ execute_event_cb(WIFI_PROV_START, NULL); goto exit; } err: prov_ctx->prov_state = WIFI_PROV_STATE_IDLE; esp_wifi_set_storage(WIFI_STORAGE_FLASH); esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_cfg_old); exit: xSemaphoreGiveRecursive(prov_ctx_lock); return ret; } void wifi_prov_mgr_stop_provisioning(void) { if (!prov_ctx_lock) { ESP_LOGE(TAG, "Provisioning manager not initialized"); return; } xSemaphoreTakeRecursive(prov_ctx_lock, portMAX_DELAY); /* Launches task for stopping the provisioning service. This will do one of these: * 1) start a task for stopping the provisioning service (returns true) * 2) if service was already in the process of termination, this will * wait till the service is stopped (returns false) * 3) if service was not running, this will return immediately (returns false) */ wifi_prov_mgr_stop_service(0); xSemaphoreGiveRecursive(prov_ctx_lock); }