{"id":50509755,"url":"https://github.com/dmatking/esp32-wifi-prov","last_synced_at":"2026-06-02T19:01:30.568Z","repository":{"id":360592368,"uuid":"1234133481","full_name":"dmatking/esp32-wifi-prov","owner":"dmatking","description":"ESP-IDF WiFi provisioning component: captive-portal first-boot credential setup with NVS storage","archived":false,"fork":false,"pushed_at":"2026-05-27T03:55:36.000Z","size":32,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-27T05:13:18.667Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dmatking.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-09T19:44:32.000Z","updated_at":"2026-05-27T03:55:40.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dmatking/esp32-wifi-prov","commit_stats":null,"previous_names":["dmatking/esp32-wifi-prov"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/dmatking/esp32-wifi-prov","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmatking%2Fesp32-wifi-prov","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmatking%2Fesp32-wifi-prov/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmatking%2Fesp32-wifi-prov/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmatking%2Fesp32-wifi-prov/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dmatking","download_url":"https://codeload.github.com/dmatking/esp32-wifi-prov/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmatking%2Fesp32-wifi-prov/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33833277,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-02T02:00:07.132Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-02T19:01:29.446Z","updated_at":"2026-06-02T19:01:30.563Z","avatar_url":"https://github.com/dmatking.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"# esp32-wifi-prov\n\nESP-IDF component for first-boot WiFi provisioning via a captive portal. On first boot (or after a credential wipe) the device starts a SoftAP and serves a web form; the user connects, fills in credentials, and the device stores them in NVS and connects as a station. Subsequent boots skip the portal and connect directly.\n\nSupports all major ESP32 variants (ESP32, S2, S3, C3, C6, H2, P4). Requires ESP-IDF ≥ 5.0.\n\n## Features\n\n- SoftAP + captive portal — iOS, Android, Windows, Firefox, and Chrome all auto-open the form\n- NVS credential storage under namespace `\"wifi_prov\"`\n- Arbitrary extra form fields (text, password, number, dropdown) stored alongside WiFi creds\n- Non-secret fields pre-populate from NVS on re-entry\n- `force_portal` flag: jump straight to the portal without wiping stored data (for app-level auth recovery)\n- Boot GPIO: hold a GPIO for 3 s at boot to force re-provisioning (credentials preserved)\n- `on_portal` / `on_connect_failed` callbacks for display/UI updates\n- `device_settings`: background GPIO monitor — 3-second hold at any time fires a user callback\n\n## Repository layout\n\n```\nwifi_prov.c / wifi_prov.h          — provisioning component\ndevice_settings.c / device_settings.h — background button monitor\nCMakeLists.txt\nidf_component.yml\nnordesems__esp-captive-portal/     — captive portal sub-component (bundled, v1.3.0)\n```\n\nThe captive portal is bundled rather than pulled from the registry so the build works without network access to components.espressif.com.\n\n## Adding to a project\n\nAdd as a git submodule at `components/wifi_prov/`:\n\n```sh\ngit submodule add https://github.com/dmatking/esp32-wifi-prov.git components/wifi_prov\n```\n\nBecause ESP-IDF does not discover components nested inside another component directory, add a `file(COPY)` call to your root `CMakeLists.txt` **before** `include(project.cmake)` to expose the bundled captive portal:\n\n```cmake\nfile(COPY \"${CMAKE_SOURCE_DIR}/components/wifi_prov/nordesems__esp-captive-portal\"\n     DESTINATION \"${CMAKE_SOURCE_DIR}/components/\")\n\ninclude($ENV{IDF_PATH}/tools/cmake/project.cmake)\nproject(my_project)\n```\n\nAdd `components/nordesems__esp-captive-portal/` to `.gitignore` — it is generated at configure time and should not be tracked.\n\n## Usage\n\n```c\n#include \"wifi_prov.h\"\n\n// Optional extra fields stored in NVS alongside WiFi credentials\nstatic const wifi_prov_option_t tz_options[] = {\n    { \"US Central\", \"CST6CDT,M3.2.0,M11.1.0\" },\n    { \"US Eastern\", \"EST5EDT,M3.2.0,M11.1.0\" },\n    // ...\n};\n\nstatic const wifi_prov_field_t extra_fields[] = {\n    { .key = \"username\",  .label = \"Username\",  .placeholder = \"your name\" },\n    { .key = \"api_token\", .label = \"API Token\",  .secret = true },\n    { .key = \"count\",     .label = \"Item count\", .input_type = \"number\",\n      .input_min = \"1\",   .input_max = \"100\" },\n    { .key = \"tz\",        .label = \"Timezone\",\n      .options = tz_options, .option_count = 2 },\n};\n\nvoid app_main(void)\n{\n    wifi_prov_config_t cfg = {\n        .ap_ssid      = \"MyDevice-Setup\",\n        .ap_password  = NULL,          // open AP\n        .boot_gpio    = 0,             // hold GPIO0 for 3 s to re-provision\n        .on_portal    = my_portal_cb,  // update display when portal starts\n        .extra_fields = extra_fields,\n        .extra_count  = sizeof(extra_fields) / sizeof(extra_fields[0]),\n    };\n\n    if (!wifi_prov_start(\u0026cfg)) {\n        ESP_LOGE(\"app\", \"WiFi failed\");\n        return;\n    }\n\n    // Read credentials back from NVS after connecting\n    char username[64] = { 0 };\n    wifi_prov_get(\"username\", username, sizeof(username));\n}\n```\n\n## API\n\n### `wifi_prov_start()`\n\n```c\nbool wifi_prov_start(const wifi_prov_config_t *cfg);\n```\n\nAttempts to connect using stored NVS credentials. Falls back to the captive portal if no credentials are stored, if the connection fails, or if `force_portal` is set. Blocks until connected. Returns `true` on success.\n\nThe caller is responsible for any chip-specific WiFi hardware init before calling this (e.g. `esp_hosted_init()` on ESP32-P4).\n\n### `wifi_prov_get()`\n\n```c\nbool wifi_prov_get(const char *key, char *buf, size_t len);\n```\n\nReads a value from NVS by key. Works for `\"ssid\"`, `\"pass\"`, and any extra field key. Returns `false` if the key is not found.\n\n### `wifi_prov_reset()`\n\n```c\nvoid wifi_prov_reset(void);\n```\n\nErases all credentials from NVS, forcing the provisioning portal on the next boot.\n\n## `force_portal`\n\nSetting `cfg.force_portal = true` sends the device straight to the portal without erasing NVS. Non-secret fields are pre-populated from saved values. Useful when an app-level authentication failure (e.g. an expired API token) needs re-entry without making the user re-enter their WiFi password.\n\n## `device_settings`\n\nA background GPIO monitor that fires a callback after a 3-second button hold, at any point during normal operation. Useful for re-entering the provisioning portal, showing a settings menu, or triggering a factory reset — whatever the app needs.\n\nThe callback runs inside the monitor task and may block. After it returns the monitor waits 3 seconds before watching for another press.\n\n### Simple apps — call `wifi_prov_start()` in-place\n\nFor apps where re-entering the portal is safe to do mid-run (no conflicting network tasks):\n\n```c\n#include \"device_settings.h\"\n#include \"wifi_prov.h\"\n\nstatic wifi_prov_config_t s_cfg = { /* ... */ };\n\nstatic void enter_settings(void)\n{\n    s_cfg.force_portal = true;\n    wifi_prov_start(\u0026s_cfg);   // blocks until portal completes\n    s_cfg.force_portal = false;\n}\n\nvoid app_main(void)\n{\n    wifi_prov_start(\u0026s_cfg);\n    device_settings_start(0, enter_settings);\n    // ... rest of app\n}\n```\n\n### Apps with active network connections — restart into the portal\n\nFor apps with background tasks using the network (HTTP fetches, MQTT, etc.), calling `wifi_prov_start()` in-place can race with those tasks. The cleaner approach is to write a flag to NVS and restart:\n\n```c\nstatic void enter_settings(void)\n{\n    // Write a flag so the next boot opens the portal\n    nvs_handle_t nvs;\n    if (nvs_open(\"app\", NVS_READWRITE, \u0026nvs) == ESP_OK) {\n        nvs_set_u8(nvs, \"force_portal\", 1);\n        nvs_commit(nvs);\n        nvs_close(nvs);\n    }\n    esp_restart();\n}\n\nvoid app_main(void)\n{\n    // Read and clear the flag before wifi_prov_start()\n    nvs_flash_init();   // must be called before reading NVS\n    uint8_t force = 0;\n    nvs_handle_t nvs;\n    if (nvs_open(\"app\", NVS_READWRITE, \u0026nvs) == ESP_OK) {\n        nvs_get_u8(nvs, \"force_portal\", \u0026force);\n        if (force) { nvs_erase_key(nvs, \"force_portal\"); nvs_commit(nvs); }\n        nvs_close(nvs);\n    }\n\n    s_cfg.force_portal = (force != 0);\n    wifi_prov_start(\u0026s_cfg);\n    device_settings_start(0, enter_settings);\n    // ... rest of app\n}\n```\n\n\u003e **Note:** `nvs_flash_init()` must be called explicitly before any `nvs_open()` that happens before `wifi_prov_start()`. `wifi_prov_start()` calls it internally, but only after your early NVS reads have already run.\n\n### `device_settings_start()`\n\n```c\nvoid device_settings_start(int gpio_num, device_settings_cb_t on_hold);\n```\n\nConfigures `gpio_num` as input with internal pull-up and spawns a low-priority background task (2 KB stack). Call once after `wifi_prov_start()`.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmatking%2Fesp32-wifi-prov","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdmatking%2Fesp32-wifi-prov","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmatking%2Fesp32-wifi-prov/lists"}