Code: Select all
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_spi_flash.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "lwip/err.h"
#include "lwip/sys.h"
#include "esp_netif.h"
#include "esp_http_client.h"
#include "esp_tls.h"
#include "driver/gpio.h"
#include "mdns.h" // For mDNS if you want to access the AP by name
#include "esp_sntp.h" // For time synchronization (important for HTTPS)
// ArduinoJson library (can be used directly in IDF C++ project)
#include "ArduinoJson.h"
// --- Wi-Fi Configuration ---
#define STORAGE_NAMESPACE "wifi_creds"
#define MAX_SSID_LEN 32
#define MAX_PASS_LEN 64
static char saved_ssid[MAX_SSID_LEN + 1] = {0};
static char saved_password[MAX_PASS_LEN + 1] = {0};
// --- Seam API Configuration ---
// !! IMPORTANT: Replace these with your actual Seam API Key !!
static const char *SEAM_API_KEY = "YOUR_SEAM_API_KEY";
static const char *SEAM_API_HOST = "connect.getseam.com";
static const char *LOCKS_ENDPOINT = "/locks/list";
// --- Lock Specifics ---
// !! IMPORTANT: Replace this with your actual Yale Conexis L2 Lock ID from Seam !!
static const char *TARGET_LOCK_ID = "YOUR_LOCK_ID_FROM_SEAM"; // Example: "124e9043-22c3-4f5e-b42d-b537d73674c0"
// --- LED Pin Configuration ---
// Using GPIOs 4 (Red), 5 (Green), and 7 (Yellow)
#define RED_LED_PIN GPIO_NUM_4
#define GREEN_LED_PIN GPIO_NUM_5
#define YELLOW_LED_PIN GPIO_NUM_7
// --- Polling Interval ---
#define POLLING_INTERVAL_MS 10000 // Poll every 10 seconds in milliseconds
// --- Web Server and DNS Server for AP Mode ---
#include "esp_http_server.h"
#include "lwip/dns.h"
static httpd_handle_t server = NULL;
static const char *TAG = "YALE_LOCK_APP";
// --- AP Mode Configuration ---
#define AP_SSID "YaleLockSetup"
#define AP_IP_ADDR_OCTET1 192
#define AP_IP_ADDR_OCTET2 168
#define AP_IP_ADDR_OCTET3 4
#define AP_IP_ADDR_OCTET4 1
// --- State variable to track if we are in AP mode ---
static bool in_ap_mode = false;
static esp_netif_t *ap_netif = NULL;
static esp_netif_t *sta_netif = NULL;
static wifi_mode_t s_wifi_mode = WIFI_MODE_NULL; // To store current Wi-Fi mode
// --- Function Prototypes ---
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data);
static esp_err_t http_server_start(void);
static void http_server_stop(void);
static esp_err_t root_get_handler(httpd_req_t *req);
static esp_err_t connect_post_handler(httpd_req_t *req);
static void set_led_state(bool locked, bool door_open, bool api_error);
static bool get_lock_and_door_status(bool *is_locked, bool *is_door_open);
static void wifi_init_sta(void);
static void wifi_init_ap(void);
static void polling_task(void *pvParameters);
static void obtain_time(void);
static void initialize_sntp(void);
// --- Function to set LED states based on simplified lock and door status ---
static void set_led_state(bool locked, bool door_open, bool api_error) {
gpio_set_level(GREEN_LED_PIN, 0);
gpio_set_level(RED_LED_PIN, 0);
gpio_set_level(YELLOW_LED_PIN, 0);
if (api_error) {
ESP_LOGI(TAG, "API Error: All LEDs off (cannot determine status).");
} else if (door_open) {
ESP_LOGI(TAG, "Status: DOOR OPEN. Setting Yellow LED ON.");
gpio_set_level(YELLOW_LED_PIN, 1);
} else if (!locked) {
ESP_LOGI(TAG, "Status: UNLOCKED & CLOSED. Setting Red LED ON.");
gpio_set_level(RED_LED_PIN, 1);
} else { // locked && !door_open
ESP_LOGI(TAG, "Status: LOCKED & CLOSED. Setting Green LED ON.");
gpio_set_level(GREEN_LED_PIN, 1);
}
}
// --- Function to get lock and door status from Seam API ---
static bool get_lock_and_door_status(bool *is_locked, bool *is_door_open) {
esp_http_client_config_t config = {
.host = SEAM_API_HOST,
.path = LOCKS_ENDPOINT,
.transport_type = HTTP_TRANSPORT_OVER_SSL,
.skip_cert_verify = true, // WARNING: For production, set this to false and provide CA certs!
};
esp_http_client_handle_t client = esp_http_client_init(&config);
if (!client) {
ESP_LOGE(TAG, "Failed to initialize HTTP client");
return false;
}
esp_http_client_set_method(client, HTTP_METHOD_POST);
esp_http_client_set_header(client, "Authorization", SEAM_API_KEY);
esp_http_client_set_header(client, "Content-Type", "application/json");
// Empty string for the body as parameters are optional or in JSON payload
esp_err_t err = esp_http_client_open(client, 0); // 0 for empty body
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
return false;
}
int content_length = esp_http_client_fetch_headers(client);
if (content_length < 0) {
ESP_LOGE(TAG, "HTTP client fetch headers failed");
esp_http_client_cleanup(client);
return false;
}
char *buffer = (char *)malloc(content_length + 1);
if (!buffer) {
ESP_LOGE(TAG, "Failed to allocate buffer for HTTP response");
esp_http_client_cleanup(client);
return false;
}
int read_len = esp_http_client_read(client, buffer, content_length);
buffer[read_len] = '\0'; // Null-terminate the string
ESP_LOGI(TAG, "HTTP Response code: %d", esp_http_client_get_status_code(client));
ESP_LOGI(TAG, "Response Payload:\n%s", buffer);
StaticJsonDocument<2048> doc; // Adjust size as needed
DeserializationError json_error = deserializeJson(doc, buffer);
free(buffer); // Free buffer after deserialization
if (json_error) {
ESP_LOGE(TAG, "deserializeJson() failed: %s", json_error.f_str());
esp_http_client_cleanup(client);
return false;
}
JsonObject root = doc.as<JsonObject>();
JsonArray locks = root["locks"].as<JsonArray>();
if (!locks.isNull()) {
for (JsonObject lock : locks) {
const char* device_id = lock["device_id"];
if (device_id && strcmp(device_id, TARGET_LOCK_ID) == 0) {
if (lock.containsKey("properties")) {
JsonObject properties = lock["properties"];
if (properties.containsKey("locked")) {
*is_locked = properties["locked"].as<bool>();
ESP_LOGI(TAG, "Target lock (ID: %s) status: %s", TARGET_LOCK_ID, *is_locked ? "LOCKED" : "UNLOCKED");
} else {
ESP_LOGW(TAG, "Warning: 'properties.locked' not found for target lock. Defaulting to unlocked.");
*is_locked = false;
}
if (properties.containsKey("door_open")) {
*is_door_open = properties["door_open"].as<bool>();
ESP_LOGI(TAG, "Target door (ID: %s) status: %s", TARGET_LOCK_ID, *is_door_open ? "OPEN" : "CLOSED");
} else {
ESP_LOGW(TAG, "Warning: 'properties.door_open' not found for target lock (DoorSense might not be active/available). Defaulting to closed.");
*is_door_open = false;
}
esp_http_client_cleanup(client);
return true;
} else {
ESP_LOGE(TAG, "Error: 'properties' object not found in target lock object.");
esp_http_client_cleanup(client);
return false;
}
}
}
ESP_LOGE(TAG, "Error: Target lock ID not found in the 'locks' array.");
esp_http_client_cleanup(client);
return false;
} else {
ESP_LOGE(TAG, "Error: 'locks' array not found or is invalid in the API response.");
esp_http_client_cleanup(client);
return false;
}
}
// --- Wi-Fi Event Handler ---
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
if (!in_ap_mode) { // Only retry if not intentionally in AP mode
ESP_LOGI(TAG, "Wi-Fi disconnected. Retrying connection...");
esp_wifi_connect();
set_led_state(false, false, true); // Indicate API error (all LEDs off)
}
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
ESP_LOGI(TAG, "Got IP:" IPSTR, IP2STR(&event->ip_info.ip));
in_ap_mode = false; // Successfully connected to STA, exit AP mode state
if (server) {
http_server_stop(); // Stop AP web server if running
}
ESP_LOGI(TAG, "Wi-Fi connected in STA mode. Starting polling task.");
// Start the polling task after successful connection
xTaskCreate(polling_task, "polling_task", 4096, NULL, 5, NULL);
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STACONNECTED) {
wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
ESP_LOGI(TAG, "Station " MACSTR " joined, AID=%d", MAC2STR(event->mac), event->aid);
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) {
wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
ESP_LOGI(TAG, "Station " MACSTR " left, AID=%d", MAC2STR(event->mac), event->aid);
}
}
// --- Wi-Fi Station Initialization ---
static void wifi_init_sta(void) {
sta_netif = esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, NULL));
wifi_config_t wifi_config = {
.sta = {
.ssid = "", // Will be filled from NVS
.password = "", // Will be filled from NVS
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
.sae_pwe_h2e = WPA3_SAE_PWE_BOTH, // Keep for compatibility, will default to WPA2 if not WPA3
},
};
strncpy((char*)wifi_config.sta.ssid, saved_ssid, sizeof(wifi_config.sta.ssid) - 1);
strncpy((char*)wifi_config.sta.password, saved_password, sizeof(wifi_config.sta.password) - 1);
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); // Disable power save for continuous operation
ESP_ERROR_CHECK(esp_wifi_set_max_tx_power(84)); // Set max TX power (corresponds to 19.5 dBm)
ESP_LOGI(TAG, "wifi_init_sta finished.");
}
// --- Wi-Fi AP Initialization ---
static void wifi_init_ap(void) {
ap_netif = esp_netif_create_default_wifi_ap();
wifi_config_t wifi_config = {
.ap = {
.ssid = AP_SSID,
.ssid_len = strlen(AP_SSID),
.channel = 1, // Default channel
.password = "", // No password for AP
.max_connection = 4, // Max 4 clients
.authmode = WIFI_AUTH_OPEN,
.pmf_cfg = {},
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "wifi_init_ap finished. SSID:%s IP:" IPSTR, AP_SSID, IP2STR(&ap_netif->ip_info.ip));
// For captive portal, mDNS is needed for some devices, DNS server for others
ESP_ERROR_ERROR_CHECK(mdns_init());
ESP_ERROR_CHECK(mdns_hostname_set(AP_SSID));
ESP_ERROR_CHECK(mdns_instance_name_set(AP_SSID));
// Set DNS server to AP IP (for captive portal)
ip_addr_t dnsserver_ip;
IP_ADDR4(&dnsserver_ip, AP_IP_ADDR_OCTET1, AP_IP_ADDR_OCTET2, AP_IP_ADDR_OCTET3, AP_IP_ADDR_OCTET4);
dns_setserver(0, &dnsserver_ip);
}
// --- HTTP Server Handlers ---
static const httpd_uri_t root = {
.uri = "/",
.method = HTTP_GET,
.handler = root_get_handler,
.user_ctx = NULL
};
static const httpd_uri_t connect_uri = {
.uri = "/connect",
.method = HTTP_POST,
.handler = connect_post_handler,
.user_ctx = NULL
};
static esp_err_t root_get_handler(httpd_req_t *req) {
ESP_LOGI(TAG, "Handling root request");
// Scan for networks
uint16_t number = 20; // Max 20 APs
wifi_ap_record_t ap_info[20];
uint16_t ap_count = 0;
memset(ap_info, 0, sizeof(ap_info));
esp_wifi_scan_start(NULL, true); // Blocking scan
ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&number, ap_info));
ESP_ERROR_CHECK(esp_wifi_scan_stop());
ap_count = number;
ESP_LOGI(TAG, "Total APs scanned = %u", ap_count);
// Build HTML response
char *html_response;
size_t html_len = 1500 + ap_count * 100; // Estimate size
html_response = (char*)malloc(html_len);
if (!html_response) {
ESP_LOGE(TAG, "Failed to allocate HTML buffer");
httpd_resp_send_500(req);
return ESP_FAIL;
}
snprintf(html_response, html_len, "<!DOCTYPE html><html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
"<title>Wi-Fi Setup</title>"
"<style>body{font-family:sans-serif; text-align:center; margin-top:50px;} form{margin:20px auto; padding:20px; border:1px solid #ccc; border-radius:8px; max-width:400px;} input[type=text],input[type=password],select{width:90%; padding:10px; margin:8px 0; display:inline-block; border:1px solid #ccc; border-radius:4px; box-sizing:border-box;} input[type=submit]{background-color:#4CAF50; color:white; padding:12px 20px; border:none; border-radius:4px; cursor:pointer; width:100%;} input[type=submit]:hover{background-color:#45a049;} .scan-btn{background-color:#008CBA; color:white; padding:12px 20px; border:none; border-radius:4px; cursor:pointer; width:100%;} .scan-btn:hover{background-color:#007B9E;}</style>"
"</head><body>"
"<h1>Yale Lock Wi-Fi Setup</h1>"
"<form action=\"/connect\" method=\"post\">"
"<label for=\"ssid\">Select Network:</label><br>"
"<select id=\"ssid\" name=\"ssid\">");
if (ap_count == 0) {
strcat(html_response, "<option value=\"\">No networks found</option>");
} else {
for (int i = 0; i < ap_count; i++) {
char option_buffer[256];
snprintf(option_buffer, sizeof(option_buffer), "<option value=\"%s\">%s (%d dBm)%s</option>",
(char*)ap_info[i].ssid, (char*)ap_info[i].ssid, ap_info[i].rssi,
(ap_info[i].authmode == WIFI_AUTH_OPEN) ? "" : "*");
strcat(html_response, option_buffer);
}
}
strcat(html_response, "</select><br><br>");
strcat(html_response, "<label for=\"manual_ssid\">Or Enter Manually:</label><br>");
strcat(html_response, "<input type=\"text\" id=\"manual_ssid\" name=\"manual_ssid\" placeholder=\"Enter SSID if not listed\"><br><br>");
strcat(html_response, "<label for=\"pass\">Password:</label><br>");
strcat(html_response, "<input type=\"password\" id=\"pass\" name=\"password\" placeholder=\"Enter password\"><br><br>");
strcat(html_response, "<input type=\"submit\" value=\"Connect\">");
strcat(html_response, "</form>");
strcat(html_response, "<button class=\"scan-btn\" onclick=\"window.location.reload();\">Rescan Networks</button>");
strcat(html_response, "</body></html>");
httpd_resp_send(req, html_response, HTTPD_RESP_USE_STRLEN);
free(html_response);
return ESP_OK;
}
static esp_err_t connect_post_handler(httpd_req_t *req) {
char content[req->content_len + 1];
int ret = httpd_req_recv(req, content, req->content_len);
if (ret <= 0) { // 0 for EOF, < 0 for error
if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
httpd_resp_send_408(req);
}
return ESP_FAIL;
}
content[req->content_len] = '\0'; // Null-terminate the received content
char ssid_buf[MAX_SSID_LEN + 1] = {0};
char pass_buf[MAX_PASS_LEN + 1] = {0};
// Basic URL-encoded form parsing
// Find SSID
char *ssid_start = strstr(content, "ssid=");
if (ssid_start) {
ssid_start += 5; // Move past "ssid="
char *ssid_end = strchr(ssid_start, '&');
size_t len = (ssid_end ? (size_t)(ssid_end - ssid_start) : strlen(ssid_start));
strncpy(ssid_buf, ssid_start, MIN(len, MAX_SSID_LEN));
ssid_buf[MIN(len, MAX_SSID_LEN)] = '\0';
}
// Check for manual_ssid override
char *manual_ssid_start = strstr(content, "manual_ssid=");
if (manual_ssid_start) {
manual_ssid_start += 12; // Move past "manual_ssid="
char *manual_ssid_end = strchr(manual_ssid_start, '&');
size_t len = (manual_ssid_end ? (size_t)(manual_ssid_end - manual_ssid_start) : strlen(manual_ssid_start));
if (len > 0) { // If manual SSID is not empty
strncpy(ssid_buf, manual_ssid_start, MIN(len, MAX_SSID_LEN));
ssid_buf[MIN(len, MAX_SSID_LEN)] = '\0';
}
}
// Find Password
char *pass_start = strstr(content, "password=");
if (pass_start) {
pass_start += 9; // Move past "password="
strncpy(pass_buf, pass_start, MAX_PASS_LEN); // Password is usually last param
pass_buf[MAX_PASS_LEN] = '\0';
}
// URL decode (simple for spaces, more robust needed for full URL encoding)
for (int i = 0; ssid_buf[i] != '\0'; i++) {
if (ssid_buf[i] == '+') ssid_buf[i] = ' ';
}
for (int i = 0; pass_buf[i] != '\0'; i++) {
if (pass_buf[i] == '+') pass_buf[i] = ' ';
}
ESP_LOGI(TAG, "Received SSID: %s, Password: %s", ssid_buf, pass_buf);
// Save credentials to NVS Flash
nvs_handle_t nvs_handle;
esp_err_t err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &nvs_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) opening NVS handle!", esp_err_to_name(err));
} else {
err = nvs_set_str(nvs_handle, "ssid", ssid_buf);
if (err != ESP_OK) ESP_LOGE(TAG, "Failed to write SSID to NVS (%s)", esp_err_to_name(err));
err = nvs_set_str(nvs_handle, "password", pass_buf);
if (err != ESP_OK) ESP_LOGE(TAG, "Failed to write password to NVS (%s)", esp_err_to_name(err));
nvs_commit(nvs_handle);
nvs_close(nvs_handle);
ESP_LOGI(TAG, "Wi-Fi credentials saved to NVS.");
}
const char* html_response = "<h1>Connecting to Wi-Fi...</h1>"
"<p>SSID: %s</p>"
"<p>Please wait, the device will restart and try to connect.</p>"
"<p>You can close this page now.</p>";
char response_buffer[256];
snprintf(response_buffer, sizeof(response_buffer), html_response, ssid_buf);
httpd_resp_send(req, response_buffer, HTTPD_RESP_USE_STRLEN);
// Give client time to receive response, then restart
vTaskDelay(pdMS_TO_TICKS(1000));
esp_restart();
return ESP_OK;
}
static esp_err_t http_server_start(void) {
httpd_handle_t start_server = NULL;
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.uri_wildcard_subtree = true; // Allow handling of all URIs
ESP_LOGI(TAG, "Starting HTTP server on port: '%d'", config.server_port);
if (httpd_start(&start_server, &config) == ESP_OK) {
ESP_LOGI(TAG, "Registering URI handlers");
httpd_register_uri_handler(start_server, &root);
httpd_register_uri_handler(start_server, &connect_uri);
server = start_server;
return ESP_OK;
}
ESP_LOGE(TAG, "Error starting http server!");
return ESP_FAIL;
}
static void http_server_stop(void) {
if (server) {
httpd_stop(server);
server = NULL;
ESP_LOGI(TAG, "HTTP server stopped.");
}
}
// --- Polling Task (runs after successful Wi-Fi connection) ---
static void polling_task(void *pvParameters) {
bool current_lock_status = false;
bool current_door_status = false;
bool api_success = false;
// Initialize SNTP for time synchronization (important for HTTPS certificate validation)
initialize_sntp();
obtain_time();
for (;;) {
ESP_LOGI(TAG, "--- Starting Lock Status Polling Cycle ---");
wifi_ap_record_t ap_info;
// Check if STA mode is connected. esp_wifi_sta_get_ap_info implicitly checks connection.
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK) {
ESP_LOGI(TAG, "WiFi is connected. Fetching lock status...");
api_success = get_lock_and_door_status(¤t_lock_status, ¤t_door_status);
} else {
ESP_LOGW(TAG, "WiFi is NOT connected in STA mode or disconnected. Re-initializing STA mode.");
// If Wi-Fi disconnected, stop polling task and let app_main handle re-connection
set_led_state(false, false, true); // All LEDs off to indicate problem
vTaskDelete(NULL); // Delete this task, app_main will handle re-init
}
ESP_LOGI(TAG, "Updating LED state...");
set_led_state(current_lock_status, current_door_status, !api_success);
ESP_LOGI(TAG, "LED state updated.");
ESP_LOGI(TAG, "Waiting for next polling interval (%d seconds)...", POLLING_INTERVAL_MS / 1000);
vTaskDelay(pdMS_TO_TICKS(POLLING_INTERVAL_MS));
ESP_LOGI(TAG, "--- Polling Cycle Complete ---");
}
}
// --- Time Synchronization (for HTTPS) ---
static void obtain_time(void) {
ESP_LOGI(TAG, "Initializing SNTP");
sntp_setoperatingmode(SNTP_OPMODE_POLL);
sntp_setservername(0, "pool.ntp.org");
sntp_init();
// Wait for time to be set
time_t now = 0;
struct tm timeinfo = { 0 };
int retry = 0;
const int retry_count = 10;
while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET && ++retry < retry_count) {
ESP_LOGI(TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count);
vTaskDelay(pdMS_TO_TICKS(2000));
}
time(&now);
localtime_r(&now, &timeinfo);
ESP_LOGI(TAG, "Time is: %s", asctime(&timeinfo));
}
static void initialize_sntp(void) {
esp_sntp_init();
}
// --- Main application entry point (IDF equivalent of setup()) ---
extern "C" void app_main(void) {
// Give USB CDC a moment to initialize (similar to Arduino's delay(2000))
vTaskDelay(pdMS_TO_TICKS(2000));
ESP_LOGI(TAG, "--- Starting app_main ---");
// Initialize NVS (Non-Volatile Storage)
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "NVS flash initialized.");
// Configure LED GPIOs
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << GREEN_LED_PIN) | (1ULL << RED_LED_PIN) | (1ULL << YELLOW_LED_PIN),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&io_conf);
ESP_LOGI(TAG, "LED pins configured.");
// Turn all LEDs off initially
set_led_state(false, false, true); // All off to indicate initial state
ESP_LOGI(TAG, "LEDs initialized to OFF.");
// Initialize network interfaces and event loop
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
// Read saved credentials from NVS
nvs_handle_t nvs_handle;
size_t ssid_len = MAX_SSID_LEN;
size_t pass_len = MAX_PASS_LEN;
esp_err_t err = nvs_open(STORAGE_NAMESPACE, NVS_READONLY, &nvs_handle);
if (err == ESP_OK) {
err = nvs_get_str(nvs_handle, "ssid", saved_ssid, &ssid_len);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) {
ESP_LOGE(TAG, "Error (%s) reading SSID from NVS!", esp_err_to_name(err));
}
err = nvs_get_str(nvs_handle, "password", saved_password, &pass_len);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) {
ESP_LOGE(TAG, "Error (%s) reading password from NVS!", esp_err_to_name(err));
}
nvs_close(nvs_handle);
} else {
ESP_LOGE(TAG, "Error (%s) opening NVS handle!", esp_err_to_name(err));
}
if (strlen(saved_ssid) > 0) {
ESP_LOGI(TAG, "Found saved Wi-Fi credentials. Attempting to connect to STA.");
wifi_init_sta();
} else {
ESP_LOGI(TAG, "No saved Wi-Fi credentials. Starting AP for provisioning.");
wifi_init_ap();
ESP_ERROR_CHECK(http_server_start());
in_ap_mode = true;
}
// If in AP mode, the app_main will exit, and FreeRTOS will run the HTTP server task.
// If in STA mode, the polling_task will be created by the event handler upon connection.
// The main thread can then be suspended or do other work.
// For this example, we'll just keep the main task alive.
for (;;) {
if (in_ap_mode) {
// Blink yellow LED in AP mode
gpio_set_level(YELLOW_LED_PIN, !gpio_get_level(YELLOW_LED_PIN));
vTaskDelay(pdMS_TO_TICKS(500)); // Blink every 500ms
} else {
// If not in AP mode, the polling task handles LED updates.
// This main loop just keeps app_main alive.
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
}