feat: add ESP32 embedded agent + namespace VRF reference docs + update android-agent submodule

- apps/device-agent-embedded/: ESP32 WireGuard agent (C, ESP-IDF, mbedTLS)
- reference/namespace-vrf/: Linux namespace/VRF network isolation docs
- apps/android-agent: VPN permission check, StartResult sealed class
This commit is contained in:
datadunia
2026-09-13 14:39:24 +07:00
parent 88fa60901e
commit e523398fa8
31 changed files with 6766 additions and 1 deletions
+264
View File
@@ -0,0 +1,264 @@
/**
* @file config.c
* @brief NVS Configuration Management
*/
#include <string.h>
#include "config.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_log.h"
static const char *TAG = "nexus_config";
/* NVS namespace */
static const char *NVS_NAMESPACE = "nexusguard";
/* NVS keys (all < 15 chars per NVS limit) */
static const char *KEY_DEVICE_ID = "device_id";
static const char *KEY_PRIVATE_KEY = "private_key";
static const char *KEY_PRESHARED_KEY = "preshared_key";
static const char *KEY_INTERNAL_IP = "internal_ip";
static const char *KEY_SERVER_PUB = "server_pub";
static const char *KEY_ENDPOINT = "endpoint";
static const char *KEY_DNS = "dns";
static const char *KEY_ALLOWED_IPS = "allowed_ips";
static const char *KEY_SERVER_WG_IP = "server_wgip";
static const char *KEY_CONFIG_HASH = "config_hash";
/* NVS handle (open once, reuse) */
static nvs_handle_t s_nvs_handle = 0;
static bool s_nvs_open = false;
/**
* @brief Helper: write string to NVS
*/
static esp_err_t nvs_write_str(const char *key, const char *value)
{
if (!s_nvs_open) {
return ESP_ERR_INVALID_STATE;
}
esp_err_t err = nvs_set_str(s_nvs_handle, key, value);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write NVS key '%s': %s", key, esp_err_to_name(err));
}
return err;
}
/**
* @brief Helper: read string from NVS into buffer
*/
static esp_err_t nvs_read_str(const char *key, char *buf, size_t buf_len)
{
if (!s_nvs_open) {
return ESP_ERR_INVALID_STATE;
}
size_t required_size = 0;
esp_err_t err = nvs_get_str(s_nvs_handle, key, NULL, &required_size);
if (err != ESP_OK) {
return err;
}
if (required_size > buf_len) {
ESP_LOGE(TAG, "NVS key '%s' value too large (%zu > %zu)", key, required_size, buf_len);
return ESP_ERR_NVS_INVALID_LENGTH;
}
err = nvs_get_str(s_nvs_handle, key, buf, &buf_len);
return err;
}
esp_err_t config_init(void)
{
ESP_LOGI(TAG, "Initializing NVS config module...");
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &s_nvs_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to open NVS namespace '%s': %s", NVS_NAMESPACE, esp_err_to_name(err));
return err;
}
s_nvs_open = true;
ESP_LOGI(TAG, "NVS config module initialized");
return ESP_OK;
}
esp_err_t config_save(const wg_config_t *config)
{
if (config == NULL) {
return ESP_ERR_INVALID_ARG;
}
ESP_LOGI(TAG, "Saving config for device: %s", config->device_id);
esp_err_t err;
esp_err_t final_err = ESP_OK;
err = nvs_write_str(KEY_DEVICE_ID, config->device_id);
if (err != ESP_OK) final_err = err;
err = nvs_write_str(KEY_PRIVATE_KEY, config->private_key);
if (err != ESP_OK) final_err = err;
err = nvs_write_str(KEY_PRESHARED_KEY, config->preshared_key);
if (err != ESP_OK) final_err = err;
err = nvs_write_str(KEY_INTERNAL_IP, config->internal_ip);
if (err != ESP_OK) final_err = err;
err = nvs_write_str(KEY_SERVER_PUB, config->server_pub);
if (err != ESP_OK) final_err = err;
err = nvs_write_str(KEY_ENDPOINT, config->endpoint);
if (err != ESP_OK) final_err = err;
err = nvs_write_str(KEY_DNS, config->dns);
if (err != ESP_OK) final_err = err;
err = nvs_write_str(KEY_ALLOWED_IPS, config->allowed_ips);
if (err != ESP_OK) final_err = err;
err = nvs_write_str(KEY_SERVER_WG_IP, config->server_wg_ip);
if (err != ESP_OK) final_err = err;
err = nvs_write_str(KEY_CONFIG_HASH, config->config_hash);
if (err != ESP_OK) final_err = err;
if (final_err != ESP_OK) {
ESP_LOGE(TAG, "Failed to save config");
return final_err;
}
/* Commit to flash */
err = nvs_commit(s_nvs_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to commit NVS: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(TAG, "Config saved successfully");
return ESP_OK;
}
esp_err_t config_load(wg_config_t *config)
{
if (config == NULL) {
return ESP_ERR_INVALID_ARG;
}
/* Zero out the struct first */
memset(config, 0, sizeof(wg_config_t));
ESP_LOGI(TAG, "Loading config from NVS...");
esp_err_t err;
esp_err_t final_err = ESP_OK;
err = nvs_read_str(KEY_DEVICE_ID, config->device_id, sizeof(config->device_id));
if (err != ESP_OK) {
ESP_LOGW(TAG, "No device_id in NVS (first boot?)");
return err;
}
err = nvs_read_str(KEY_PRIVATE_KEY, config->private_key, sizeof(config->private_key));
if (err != ESP_OK) final_err = err;
err = nvs_read_str(KEY_PRESHARED_KEY, config->preshared_key, sizeof(config->preshared_key));
if (err != ESP_OK) final_err = err;
err = nvs_read_str(KEY_INTERNAL_IP, config->internal_ip, sizeof(config->internal_ip));
if (err != ESP_OK) final_err = err;
err = nvs_read_str(KEY_SERVER_PUB, config->server_pub, sizeof(config->server_pub));
if (err != ESP_OK) final_err = err;
err = nvs_read_str(KEY_ENDPOINT, config->endpoint, sizeof(config->endpoint));
if (err != ESP_OK) final_err = err;
err = nvs_read_str(KEY_DNS, config->dns, sizeof(config->dns));
if (err != ESP_OK) final_err = err;
err = nvs_read_str(KEY_ALLOWED_IPS, config->allowed_ips, sizeof(config->allowed_ips));
if (err != ESP_OK) final_err = err;
err = nvs_read_str(KEY_SERVER_WG_IP, config->server_wg_ip, sizeof(config->server_wg_ip));
if (err != ESP_OK) final_err = err;
err = nvs_read_str(KEY_CONFIG_HASH, config->config_hash, sizeof(config->config_hash));
if (err != ESP_OK) final_err = err;
if (final_err != ESP_OK) {
ESP_LOGW(TAG, "Config loaded with errors");
return final_err;
}
ESP_LOGI(TAG, "Config loaded successfully for device: %s", config->device_id);
return ESP_OK;
}
esp_err_t config_clear(void)
{
ESP_LOGI(TAG, "Clearing all NexusGuard NVS keys...");
if (!s_nvs_open) {
return ESP_ERR_INVALID_STATE;
}
esp_err_t err;
esp_err_t final_err = ESP_OK;
err = nvs_erase_key(s_nvs_handle, KEY_DEVICE_ID);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
err = nvs_erase_key(s_nvs_handle, KEY_PRIVATE_KEY);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
err = nvs_erase_key(s_nvs_handle, KEY_PRESHARED_KEY);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
err = nvs_erase_key(s_nvs_handle, KEY_INTERNAL_IP);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
err = nvs_erase_key(s_nvs_handle, KEY_SERVER_PUB);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
err = nvs_erase_key(s_nvs_handle, KEY_ENDPOINT);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
err = nvs_erase_key(s_nvs_handle, KEY_DNS);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
err = nvs_erase_key(s_nvs_handle, KEY_ALLOWED_IPS);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
err = nvs_erase_key(s_nvs_handle, KEY_SERVER_WG_IP);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
err = nvs_erase_key(s_nvs_handle, KEY_CONFIG_HASH);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) final_err = err;
/* Commit erases to flash */
err = nvs_commit(s_nvs_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to commit NVS erase: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(TAG, "Config cleared");
return final_err;
}
bool config_is_valid(const wg_config_t *config)
{
if (config == NULL) {
return false;
}
/* device_id and private_key must be non-empty */
if (strlen(config->device_id) == 0) {
return false;
}
if (strlen(config->private_key) == 0) {
return false;
}
return true;
}
+63
View File
@@ -0,0 +1,63 @@
/**
* @file config.h
* @brief NVS Configuration Management
*/
#ifndef CONFIG_H
#define CONFIG_H
#include <esp_err.h>
/* WireGuard configuration structure */
typedef struct {
char device_id[37]; /* UUID */
char private_key[64]; /* WG private key hex */
char preshared_key[64]; /* PSK hex */
char internal_ip[18]; /* e.g. "10.172.21.2" */
char server_pub[64]; /* Server WG public key hex */
char endpoint[128]; /* e.g. "italy-twenty.gl.at.ply.gg:59750" */
char dns[64]; /* DNS server */
char allowed_ips[128]; /* e.g. "0.0.0.0/0" */
char server_wg_ip[18]; /* e.g. "10.172.21.1" */
char config_hash[64]; /* Last config hash */
} wg_config_t;
/* Device state */
typedef enum {
STATE_BOOT,
STATE_WIFI_INIT,
STATE_WIFI_CONNECTED,
STATE_PROVISIONING,
STATE_PROVISIONED,
STATE_TUNNEL_UP,
STATE_TUNNEL_DOWN,
STATE_HEARTBEAT,
STATE_ERROR
} device_state_t;
/**
* @brief Initialize configuration module
*/
esp_err_t config_init(void);
/**
* @brief Save configuration to NVS
*/
esp_err_t config_save(const wg_config_t *config);
/**
* @brief Load configuration from NVS
*/
esp_err_t config_load(wg_config_t *config);
/**
* @brief Clear configuration from NVS
*/
esp_err_t config_clear(void);
/**
* @brief Check if configuration is valid
*/
bool config_is_valid(const wg_config_t *config);
#endif /* CONFIG_H */
+136
View File
@@ -0,0 +1,136 @@
/**
* @file crypto.c
* @brief AES-256-GCM Crypto Operations (mbedtls)
*/
#include <string.h>
#include <stdlib.h>
#include "crypto.h"
#include "esp_log.h"
#include "mbedtls/sha256.h"
#include "mbedtls/cipher.h"
#include "mbedtls/error.h"
static const char *TAG = "nexus_crypto";
/* AES-256-GCM constants */
#define GCM_NONCE_SIZE 12
#define GCM_TAG_SIZE 16
#define KEY_SIZE 32
esp_err_t crypto_init(void)
{
/* mbedtls auto-initializes on first use, nothing needed */
ESP_LOGI(TAG, "Crypto module initialized (mbedtls)");
return ESP_OK;
}
esp_err_t crypto_derive_key(const char *hwid, const char *salt, uint8_t key[32])
{
if (hwid == NULL || salt == NULL || key == NULL) {
return ESP_ERR_INVALID_ARG;
}
/* Build input: hwid + salt (concatenation, NOT HMAC) */
size_t hwid_len = strlen(hwid);
size_t salt_len = strlen(salt);
size_t input_len = hwid_len + salt_len;
char *input = malloc(input_len);
if (input == NULL) {
ESP_LOGE(TAG, "Failed to allocate key derivation input");
return ESP_ERR_NO_MEM;
}
memcpy(input, hwid, hwid_len);
memcpy(input + hwid_len, salt, salt_len);
/* SHA-256 hash */
int ret = mbedtls_sha256((const unsigned char *)input, input_len, key, 0);
free(input);
if (ret != 0) {
ESP_LOGE(TAG, "SHA-256 failed: -0x%04x", -ret);
return ESP_FAIL;
}
ESP_LOGI(TAG, "Key derived from HWID+salt");
return ESP_OK;
}
esp_err_t crypto_decrypt(const uint8_t *ciphertext, size_t len,
const uint8_t key[32], uint8_t **plaintext,
size_t *plaintext_len)
{
if (ciphertext == NULL || key == NULL || plaintext == NULL || plaintext_len == NULL) {
return ESP_ERR_INVALID_ARG;
}
/* Minimum: nonce(12) + tag(16) = 28 bytes minimum */
if (len < GCM_NONCE_SIZE + GCM_TAG_SIZE) {
ESP_LOGE(TAG, "Ciphertext too short: %zu bytes (min %d)", len, GCM_NONCE_SIZE + GCM_TAG_SIZE);
return ESP_ERR_INVALID_SIZE;
}
/* Split: nonce(12) + encrypted_data + tag(16) */
const uint8_t *nonce = ciphertext;
const uint8_t *encrypted = ciphertext + GCM_NONCE_SIZE;
size_t encrypted_len = len - GCM_NONCE_SIZE - GCM_TAG_SIZE;
const uint8_t *tag = ciphertext + len - GCM_TAG_SIZE;
/* Allocate output buffer */
uint8_t *out = malloc(encrypted_len + 1); /* +1 for null terminator (JSON payload) */
if (out == NULL) {
ESP_LOGE(TAG, "Failed to allocate plaintext buffer");
return ESP_ERR_NO_MEM;
}
/* Initialize mbedtls cipher context */
mbedtls_cipher_context_t ctx;
mbedtls_cipher_init(&ctx);
ret = mbedtls_cipher_setup(&ctx, mbedtls_cipher_info_from_type(MBEDTLS_CIPHER_AES_256_GCM));
if (ret != 0) {
ESP_LOGE(TAG, "Cipher setup failed: -0x%04x", -ret);
mbedtls_cipher_free(&ctx);
free(out);
return ESP_FAIL;
}
ret = mbedtls_cipher_setkey(&ctx, key, KEY_SIZE * 8, MBEDTLS_DECRYPT);
if (ret != 0) {
ESP_LOGE(TAG, "Cipher setkey failed: -0x%04x", -ret);
mbedtls_cipher_free(&ctx);
free(out);
return ESP_FAIL;
}
ret = mbedtls_cipher_set_iv(&ctx, nonce, GCM_NONCE_SIZE);
if (ret != 0) {
ESP_LOGE(TAG, "Cipher setiv failed: -0x%04x", -ret);
mbedtls_cipher_free(&ctx);
free(out);
return ESP_FAIL;
}
/* Start decrypt */
ret = mbedtls_cipher_auth_decrypt(&ctx, tag, GCM_TAG_SIZE,
NULL, 0,
encrypted, encrypted_len,
out, plaintext_len);
mbedtls_cipher_free(&ctx);
if (ret != 0) {
ESP_LOGE(TAG, "AES-256-GCM decrypt failed: -0x%04x", -ret);
free(out);
return ESP_ERR_AUTH_CRYPTO;
}
/* Null-terminate for JSON parsing */
out[*plaintext_len] = '\0';
*plaintext = out;
ESP_LOGI(TAG, "Decrypted %zu bytes", *plaintext_len);
return ESP_OK;
}
+41
View File
@@ -0,0 +1,41 @@
/**
* @file crypto.h
* @brief AES-256-GCM Crypto Operations
*/
#ifndef CRYPTO_H
#define CRYPTO_H
#include <esp_err.h>
#include <stdint.h>
/**
* @brief Initialize crypto module
*/
esp_err_t crypto_init(void);
/**
* @brief Derive key from HWID + salt
*
* @param hwid Hardware ID
* @param salt Salt string
* @param key Output 32-byte key
* @return esp_err_t ESP_OK on success
*/
esp_err_t crypto_derive_key(const char *hwid, const char *salt, uint8_t key[32]);
/**
* @brief Decrypt AES-256-GCM ciphertext
*
* @param ciphertext Input ciphertext (nonce + encrypted)
* @param len Ciphertext length
* @param key 32-byte key
* @param plaintext Output plaintext (caller must free)
* @param plaintext_len Output plaintext length
* @return esp_err_t ESP_OK on success
*/
esp_err_t crypto_decrypt(const uint8_t *ciphertext, size_t len,
const uint8_t key[32], uint8_t **plaintext,
size_t *plaintext_len);
#endif /* CRYPTO_H */
+207
View File
@@ -0,0 +1,207 @@
/**
* @file heartbeat.c
* @brief HTTP Heartbeat Loop
*/
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include "heartbeat.h"
#include "config.h"
#include "esp_log.h"
#include "esp_http_client.h"
#include "cJSON.h"
static const char *TAG = "nexus_heartbeat";
/* Retry config */
#define MAX_RETRIES 3
#define CONNECT_TIMEOUT_MS 10000
#define READ_TIMEOUT_MS 30000
/**
* @brief Convert epoch seconds to ISO8601 string
* @param epoch Unix timestamp
* @param buf Output buffer (at least 25 bytes)
* @param buf_len Buffer length
*/
static void epoch_to_iso8601(int64_t epoch, char *buf, size_t buf_len)
{
time_t t = (time_t)epoch;
struct tm tm_info;
gmtime_r(&t, &tm_info);
strftime(buf, buf_len, "%Y-%m-%dT%H:%M:%SZ", &tm_info);
}
/**
* @brief HTTP event handler for heartbeat
*/
static esp_err_t heartbeat_event_handler(esp_http_client_event_t *evt)
{
switch (evt->event_id) {
case HTTP_EVENT_ERROR:
ESP_LOGD(TAG, "HTTP_EVENT_ERROR");
break;
case HTTP_EVENT_ON_CONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_CONNECTED");
break;
case HTTP_EVENT_ON_HEADER:
break;
case HTTP_EVENT_ON_DATA:
ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
break;
case HTTP_EVENT_ON_FINISH:
ESP_LOGD(TAG, "HTTP_EVENT_FINISHED");
break;
case HTTP_EVENT_DISCONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_DISCONNECTED");
break;
default:
break;
}
return ESP_OK;
}
/**
* @brief Helper: copy cJSON string to char buffer
*/
static void copy_json_str(const cJSON *json, const char *key, char *buf, size_t buf_len)
{
cJSON *item = cJSON_GetObjectItemCaseSensitive(json, key);
if (cJSON_IsString(item) && item->valuestring != NULL) {
strncpy(buf, item->valuestring, buf_len - 1);
buf[buf_len - 1] = '\0';
}
}
esp_err_t heartbeat_send(const wg_config_t *config, const char *status,
bool tunnel_up, int64_t last_handshake)
{
if (config == NULL || status == NULL) {
return ESP_ERR_INVALID_ARG;
}
/* Convert last_handshake to ISO8601 */
char handshake_str[25] = "1970-01-01T00:00:00Z";
if (last_handshake > 0) {
epoch_to_iso8601(last_handshake, handshake_str, sizeof(handshake_str));
}
/* Build request JSON */
cJSON *req_json = cJSON_CreateObject();
cJSON_AddStringToObject(req_json, "device_id", config->device_id);
cJSON_AddStringToObject(req_json, "status", status);
cJSON_AddBoolToObject(req_json, "tunnel_up", tunnel_up);
cJSON_AddStringToObject(req_json, "last_handshake", handshake_str);
char *req_body = cJSON_PrintUnformatted(req_json);
cJSON_Delete(req_json);
if (req_body == NULL) {
ESP_LOGE(TAG, "Failed to create heartbeat JSON");
return ESP_ERR_NO_MEM;
}
/* Build URL — use server_wg_ip if available, else we need external URL
* For simplicity, we store server_url elsewhere or use the configured endpoint.
* Since we don't have server_url in wg_config_t, we construct from endpoint.
* TODO: store server_url in config or pass as param */
char url[192];
/* Use server_wg_ip to build the heartbeat URL */
if (strlen(config->server_wg_ip) > 0) {
snprintf(url, sizeof(url), "http://%s:8080/api/v1/heartbeat", config->server_wg_ip);
} else {
/* Fallback — shouldn't happen in normal operation */
ESP_LOGE(TAG, "No server_wg_ip configured");
free(req_body);
return ESP_ERR_INVALID_STATE;
}
esp_err_t final_err = ESP_FAIL;
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
ESP_LOGD(TAG, "Heartbeat attempt %d/%d...", attempt, MAX_RETRIES);
esp_http_client_config_t http_config = {
.url = url,
.event_handler = heartbeat_event_handler,
.timeout_ms = CONNECT_TIMEOUT_MS,
};
esp_http_client_handle_t client = esp_http_client_init(&http_config);
if (client == NULL) {
ESP_LOGE(TAG, "Failed to init HTTP client");
continue;
}
esp_http_client_set_method(client, HTTP_METHOD_POST);
esp_http_client_set_header(client, "Content-Type", "application/json");
esp_http_client_set_post_field(client, req_body, strlen(req_body));
esp_err_t err = esp_http_client_open(client, strlen(req_body));
if (err != ESP_OK) {
ESP_LOGE(TAG, "HTTP open failed: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
int written = esp_http_client_write(client, req_body, strlen(req_body));
if (written < 0) {
ESP_LOGE(TAG, "HTTP write failed");
esp_http_client_close(client);
esp_http_client_cleanup(client);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
int content_length = esp_http_client_fetch_headers(client);
int status_code = esp_http_client_get_status_code(client);
if (status_code != 200) {
ESP_LOGW(TAG, "Heartbeat failed, HTTP %d", status_code);
esp_http_client_close(client);
esp_http_client_cleanup(client);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
/* Read response body for config sync */
if (content_length > 0) {
char *response_buf = malloc(content_length + 1);
if (response_buf != NULL) {
int read_len = esp_http_client_read(client, response_buf, content_length);
response_buf[read_len] = '\0';
/* TODO: Parse config sync fields if config_changed is detected
* For now, log the response size */
ESP_LOGD(TAG, "Heartbeat response: %d bytes", read_len);
free(response_buf);
}
}
esp_http_client_close(client);
esp_http_client_cleanup(client);
final_err = ESP_OK;
break;
}
free(req_body);
return final_err;
}
bool heartbeat_config_changed(const char *old_hash, const char *new_hash)
{
if (old_hash == NULL || new_hash == NULL) {
/* If either is NULL, treat as changed */
return true;
}
/* Empty new hash means no hash provided — treat as changed */
if (strlen(new_hash) == 0) {
return true;
}
/* Simple string comparison */
return (strcmp(old_hash, new_hash) != 0);
}
@@ -0,0 +1,35 @@
/**
* @file heartbeat.h
* @brief HTTP Heartbeat Loop
*/
#ifndef HEARTBEAT_H
#define HEARTBEAT_H
#include <esp_err.h>
#include <stdint.h>
#include <stdbool.h>
#include "config.h"
/**
* @brief Send heartbeat to server
*
* @param config Device configuration
* @param status Device status ("connected", "recovering", "stopped")
* @param tunnel_up WireGuard tunnel status
* @param last_handshake Last handshake timestamp (epoch seconds)
* @return esp_err_t ESP_OK on success
*/
esp_err_t heartbeat_send(const wg_config_t *config, const char *status,
bool tunnel_up, int64_t last_handshake);
/**
* @brief Check if config changed based on hash comparison
*
* @param old_hash Previous config hash
* @param new_hash New config hash
* @return true if config changed, false otherwise
*/
bool heartbeat_config_changed(const char *old_hash, const char *new_hash);
#endif /* HEARTBEAT_H */
+248
View File
@@ -0,0 +1,248 @@
/**
* @file main.c
* @brief NexusGuard Embedded Agent - Main Entry Point
*
* ESP32-based WireGuard agent for IoT/edge devices.
* Connects to NexusGuard Server Core via HTTP provisioning + heartbeat.
*
* @version 1.0.0
* @date 2026-06-26
*/
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "nvs_flash.h"
/* Agent modules */
#include "config.h"
#include "wifi.h"
#include "provision.h"
#include "heartbeat.h"
#include "tunnel.h"
#include "crypto.h"
static const char *TAG = "nexusguard";
/* Event bits */
#define WIFI_CONNECTED_BIT BIT0
#define PROVISIONED_BIT BIT1
#define TUNNEL_UP_BIT BIT2
#define HEARTBEAT_OK_BIT BIT3
static EventGroupHandle_t s_event_group;
/* Agent configuration */
static wg_config_t s_config;
static device_state_t s_state = STATE_BOOT;
/**
* @brief Get hardware ID from ESP32 eFuse MAC
*/
static void get_hardware_id(char *hwid, size_t len)
{
uint8_t mac[6];
esp_efuse_mac_get_default(mac);
snprintf(hwid, len, "%02x%02x%02x%02x%02x%02x",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
/**
* @brief WiFi 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) {
ESP_LOGW(TAG, "WiFi disconnected, reconnecting...");
esp_wifi_connect();
xEventGroupClearBits(s_event_group, WIFI_CONNECTED_BIT);
} 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));
xEventGroupSetBits(s_event_group, WIFI_CONNECTED_BIT);
}
}
/**
* @brief Initialize WiFi
*/
static esp_err_t init_wifi(const char *ssid, const char *password)
{
ESP_LOGI(TAG, "Initializing WiFi...");
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
esp_event_handler_instance_t instance_any_id;
esp_event_handler_instance_t instance_got_ip;
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
&wifi_event_handler, NULL, &instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
&wifi_event_handler, NULL, &instance_got_ip));
wifi_config_t wifi_config = {
.sta = {
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
},
};
strncpy((char *)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid) - 1);
strncpy((char *)wifi_config.sta.password, 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_LOGI(TAG, "WiFi initialized, connecting to %s...", ssid);
return ESP_OK;
}
/**
* @brief Main entry point
*/
void app_main(void)
{
ESP_LOGI(TAG, "=== NexusGuard Embedded Agent v%s ===", CONFIG_NEXUS_AGENT_VERSION);
ESP_LOGI(TAG, "Hardware: %s", CONFIG_IDF_TARGET);
ESP_LOGI(TAG, "IDF Version: %s", esp_get_idf_version());
ESP_LOGI(TAG, "Server: %s", CONFIG_NEXUS_SERVER_URL);
/* Initialize NVS */
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);
/* Create event group */
s_event_group = xEventGroupCreate();
/* Get hardware ID */
char hwid[32];
get_hardware_id(hwid, sizeof(hwid));
ESP_LOGI(TAG, "Hardware ID: %s", hwid);
/* Initialize modules */
ESP_ERROR_CHECK(config_init());
ESP_ERROR_CHECK(crypto_init());
/* Load saved config */
ret = config_load(&s_config);
if (ret == ESP_OK && config_is_valid(&s_config)) {
ESP_LOGI(TAG, "Loaded saved config for device: %s", s_config.device_id);
xEventGroupSetBits(s_event_group, PROVISIONED_BIT);
} else {
ESP_LOGW(TAG, "No valid config found, will provision...");
}
/* Initialize WiFi */
const char *ssid = CONFIG_NEXUS_WIFI_SSID;
const char *password = CONFIG_NEXUS_WIFI_PASSWORD;
if (strlen(ssid) == 0) {
ESP_LOGE(TAG, "WiFi SSID not configured! Set via menuconfig or NVS.");
ESP_LOGE(TAG, "Run: idf.py menuconfig → Component config → NexusGuard Agent");
return;
}
init_wifi(ssid, password);
/* Wait for WiFi connection */
ESP_LOGI(TAG, "Waiting for WiFi connection...");
xEventGroupWaitBits(s_event_group, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE,
pdMS_TO_TICKS(30000));
if (!(xEventGroupGetBits(s_event_group) & WIFI_CONNECTED_BIT)) {
ESP_LOGE(TAG, "WiFi connection failed, restarting...");
esp_restart();
}
ESP_LOGI(TAG, "WiFi connected!");
/* Provision if needed */
if (!(xEventGroupGetBits(s_event_group) & PROVISIONED_BIT)) {
ESP_LOGI(TAG, "Provisioning device...");
s_state = STATE_PROVISIONING;
const char *server_url = CONFIG_NEXUS_SERVER_URL;
const char *token = CONFIG_NEXUS_REG_TOKEN;
if (strlen(server_url) == 0 || strlen(token) == 0) {
ESP_LOGE(TAG, "Server URL or registration token not configured!");
ESP_LOGE(TAG, "Run: idf.py menuconfig → Component config → NexusGuard Agent");
return;
}
ret = provision_device(server_url, token, hwid, &s_config);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Provisioning failed: %s", esp_err_to_name(ret));
ESP_LOGE(TAG, "Restarting in 5 seconds...");
vTaskDelay(pdMS_TO_TICKS(5000));
esp_restart();
}
ESP_LOGI(TAG, "Provisioning successful!");
xEventGroupSetBits(s_event_group, PROVISIONED_BIT);
}
/* Initialize WireGuard tunnel */
ESP_LOGI(TAG, "Initializing WireGuard tunnel...");
s_state = STATE_TUNNEL_UP;
ret = tunnel_init(&s_config);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Tunnel initialization failed: %s", esp_err_to_name(ret));
ESP_LOGE(TAG, "Restarting in 5 seconds...");
vTaskDelay(pdMS_TO_TICKS(5000));
esp_restart();
}
ESP_LOGI(TAG, "WireGuard tunnel initialized!");
xEventGroupSetBits(s_event_group, TUNNEL_UP_BIT);
/* Main heartbeat loop */
ESP_LOGI(TAG, "Starting heartbeat loop...");
s_state = STATE_HEARTBEAT;
while (1) {
/* Wait for 30 seconds */
vTaskDelay(pdMS_TO_TICKS(30000));
/* Check WiFi */
if (!(xEventGroupGetBits(s_event_group) & WIFI_CONNECTED_BIT)) {
ESP_LOGW(TAG, "WiFi not connected, skipping heartbeat");
continue;
}
/* Send heartbeat */
bool tunnel_up = tunnel_is_up();
int64_t last_hs = tunnel_get_last_handshake();
ret = heartbeat_send(&s_config, "connected", tunnel_up, last_hs);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Heartbeat failed: %s", esp_err_to_name(ret));
continue;
}
ESP_LOGI(TAG, "Heartbeat sent (tunnel=%s)", tunnel_up ? "up" : "down");
/* Check for config changes */
// TODO: Compare config hash and rebuild tunnel if needed
}
}
+279
View File
@@ -0,0 +1,279 @@
/**
* @file provision.c
* @brief HTTP Provisioning
*/
#include <string.h>
#include <stdlib.h>
#include "provision.h"
#include "config.h"
#include "crypto.h"
#include "esp_log.h"
#include "esp_http_client.h"
#include "cJSON.h"
#include "mbedtls/base64.h"
static const char *TAG = "nexus_provision";
/* Retry config */
#define MAX_RETRIES 3
#define CONNECT_TIMEOUT_MS 10000
#define READ_TIMEOUT_MS 30000
/* Config key name for decryption (must match server) */
#define CONFIG_NEXUS_SALT CONFIG_NEXUS_SERVER_SALT
/**
* @brief Helper: copy cJSON string to char buffer with bounds check
*/
static void copy_json_str(const cJSON *json, const char *key, char *buf, size_t buf_len)
{
cJSON *item = cJSON_GetObjectItemCaseSensitive(json, key);
if (cJSON_IsString(item) && item->valuestring != NULL) {
strncpy(buf, item->valuestring, buf_len - 1);
buf[buf_len - 1] = '\0';
}
}
/**
* @brief HTTP event handler for provisioning
*/
static esp_err_t provision_event_handler(esp_http_client_event_t *evt)
{
switch (evt->event_id) {
case HTTP_EVENT_ERROR:
ESP_LOGD(TAG, "HTTP_EVENT_ERROR");
break;
case HTTP_EVENT_ON_CONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_CONNECTED");
break;
case HTTP_EVENT_ON_HEADER:
break;
case HTTP_EVENT_ON_DATA:
ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
break;
case HTTP_EVENT_ON_FINISH:
ESP_LOGD(TAG, "HTTP_EVENT_FINISHED");
break;
case HTTP_EVENT_DISCONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_DISCONNECTED");
break;
default:
break;
}
return ESP_OK;
}
esp_err_t provision_device(const char *server_url, const char *token,
const char *hwid, wg_config_t *config)
{
if (server_url == NULL || token == NULL || hwid == NULL || config == NULL) {
return ESP_ERR_INVALID_ARG;
}
ESP_LOGI(TAG, "Starting provisioning...");
/* Build request JSON */
cJSON *req_json = cJSON_CreateObject();
cJSON_AddStringToObject(req_json, "token", token);
cJSON_AddStringToObject(req_json, "hwid", hwid);
char *req_body = cJSON_PrintUnformatted(req_json);
cJSON_Delete(req_json);
if (req_body == NULL) {
ESP_LOGE(TAG, "Failed to create request JSON");
return ESP_ERR_NO_MEM;
}
/* Build URL */
char url[192];
snprintf(url, sizeof(url), "%s/api/v1/provision", server_url);
esp_err_t final_err = ESP_FAIL;
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
ESP_LOGI(TAG, "Provisioning attempt %d/%d...", attempt, MAX_RETRIES);
/* Configure HTTP client */
esp_http_client_config_t http_config = {
.url = url,
.event_handler = provision_event_handler,
.timeout_ms = CONNECT_TIMEOUT_MS,
.transport_type = HTTP_TRANSPORT_OVER_SSL,
.skip_cert_common_name_check = true, /* TODO: use proper CA cert */
};
esp_http_client_handle_t client = esp_http_client_init(&http_config);
if (client == NULL) {
ESP_LOGE(TAG, "Failed to init HTTP client");
continue;
}
/* Set POST data */
esp_http_client_set_method(client, HTTP_METHOD_POST);
esp_http_client_set_header(client, "Content-Type", "application/json");
esp_http_client_set_post_field(client, req_body, strlen(req_body));
/* Execute request */
esp_err_t err = esp_http_client_open(client, strlen(req_body));
if (err != ESP_OK) {
ESP_LOGE(TAG, "HTTP open failed: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
int written = esp_http_client_write(client, req_body, strlen(req_body));
if (written < 0) {
ESP_LOGE(TAG, "HTTP write failed");
esp_http_client_close(client);
esp_http_client_cleanup(client);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
int content_length = esp_http_client_fetch_headers(client);
int status_code = esp_http_client_get_status_code(client);
if (status_code != 200) {
ESP_LOGE(TAG, "Provisioning failed, HTTP %d", status_code);
esp_http_client_close(client);
esp_http_client_cleanup(client);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
/* Read response body */
char *response_buf = malloc(content_length + 1);
if (response_buf == NULL) {
ESP_LOGE(TAG, "Failed to allocate response buffer");
esp_http_client_close(client);
esp_http_client_cleanup(client);
final_err = ESP_ERR_NO_MEM;
break;
}
int read_len = esp_http_client_read(client, response_buf, content_length);
esp_http_client_close(client);
esp_http_client_cleanup(client);
if (read_len < 0) {
ESP_LOGE(TAG, "HTTP read failed");
free(response_buf);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
response_buf[read_len] = '\0';
/* Parse response JSON */
cJSON *resp_json = cJSON_Parse(response_buf);
free(response_buf);
if (resp_json == NULL) {
ESP_LOGE(TAG, "Failed to parse response JSON");
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
/* Extract encrypted_config (base64) */
cJSON *encrypted_item = cJSON_GetObjectItemCaseSensitive(resp_json, "encrypted_config");
if (!cJSON_IsString(encrypted_item) || encrypted_item->valuestring == NULL) {
ESP_LOGE(TAG, "Response missing 'encrypted_config'");
cJSON_Delete(resp_json);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
const char *b64_data = encrypted_item->valuestring;
size_t b64_len = strlen(b64_data);
/* Base64 decode */
size_t decoded_max_len = (b64_len * 3) / 4 + 4;
uint8_t *decoded_buf = malloc(decoded_max_len);
if (decoded_buf == NULL) {
ESP_LOGE(TAG, "Failed to allocate base64 decode buffer");
cJSON_Delete(resp_json);
final_err = ESP_ERR_NO_MEM;
break;
}
size_t decoded_len = 0;
int b64_ret = mbedtls_base64_decode(decoded_buf, decoded_max_len, &decoded_len,
(const unsigned char *)b64_data, b64_len);
if (b64_ret != 0) {
ESP_LOGE(TAG, "Base64 decode failed: -0x%04x", -b64_ret);
free(decoded_buf);
cJSON_Delete(resp_json);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
/* Derive decryption key: SHA256(hwid + salt) */
uint8_t key[32];
err = crypto_derive_key(hwid, CONFIG_NEXUS_SALT, key);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Key derivation failed");
explicit_bzero(key, sizeof(key));
free(decoded_buf);
cJSON_Delete(resp_json);
final_err = err;
break;
}
/* Decrypt config */
uint8_t *plaintext = NULL;
size_t plaintext_len = 0;
err = crypto_decrypt(decoded_buf, decoded_len, key, &plaintext, &plaintext_len);
explicit_bzero(key, sizeof(key));
free(decoded_buf);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Decryption failed");
cJSON_Delete(resp_json);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
/* Parse decrypted JSON payload */
cJSON *config_json = cJSON_Parse((char *)plaintext);
free(plaintext);
if (config_json == NULL) {
ESP_LOGE(TAG, "Failed to parse decrypted config JSON");
cJSON_Delete(resp_json);
vTaskDelay(pdMS_TO_TICKS(1000 * attempt));
continue;
}
/* Extract config fields */
copy_json_str(config_json, "device_id", config->device_id, sizeof(config->device_id));
copy_json_str(config_json, "private_key", config->private_key, sizeof(config->private_key));
copy_json_str(config_json, "preshared_key", config->preshared_key, sizeof(config->preshared_key));
copy_json_str(config_json, "internal_ip", config->internal_ip, sizeof(config->internal_ip));
copy_json_str(config_json, "server_pub", config->server_pub, sizeof(config->server_pub));
copy_json_str(config_json, "endpoint", config->endpoint, sizeof(config->endpoint));
copy_json_str(config_json, "dns", config->dns, sizeof(config->dns));
copy_json_str(config_json, "allowed_ips", config->allowed_ips, sizeof(config->allowed_ips));
copy_json_str(config_json, "server_wg_ip", config->server_wg_ip, sizeof(config->server_wg_ip));
copy_json_str(config_json, "config_hash", config->config_hash, sizeof(config->config_hash));
cJSON_Delete(config_json);
cJSON_Delete(resp_json);
/* Save config to NVS */
err = config_save(config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to save config: %s", esp_err_to_name(err));
final_err = err;
break;
}
ESP_LOGI(TAG, "Provisioning complete, device: %s", config->device_id);
final_err = ESP_OK;
break;
}
free(req_body);
return final_err;
}
@@ -0,0 +1,24 @@
/**
* @file provision.h
* @brief HTTP Provisioning
*/
#ifndef PROVISION_H
#define PROVISION_H
#include <esp_err.h>
#include "config.h"
/**
* @brief Provision device with server
*
* @param server_url Server URL (e.g., "https://api-nexus.datadunia.com")
* @param token Registration token
* @param hwid Hardware ID
* @param config Configuration to populate
* @return esp_err_t ESP_OK on success
*/
esp_err_t provision_device(const char *server_url, const char *token,
const char *hwid, wg_config_t *config);
#endif /* PROVISION_H */
+106
View File
@@ -0,0 +1,106 @@
/**
* @file tunnel.c
* @brief WireGuard Tunnel Management (Stub)
*
* TODO: Integrate wireguard-esp32 component for actual WireGuard tunnel.
* These are stubs that log and maintain state for the heartbeat loop.
*/
#include <string.h>
#include <time.h>
#include "tunnel.h"
#include "config.h"
#include "esp_log.h"
static const char *TAG = "nexus_tunnel";
/* Tunnel state */
static bool s_tunnel_up = false;
static int64_t s_last_handshake = 0;
esp_err_t tunnel_init(const wg_config_t *config)
{
if (config == NULL) {
return ESP_ERR_INVALID_ARG;
}
ESP_LOGI(TAG, "Initializing WireGuard tunnel...");
ESP_LOGI(TAG, " Device: %s", config->device_id);
ESP_LOGI(TAG, " Endpoint: %s", config->endpoint);
ESP_LOGI(TAG, " Internal IP: %s", config->internal_ip);
ESP_LOGI(TAG, " Allowed IPs: %s", config->allowed_ips);
/* TODO: integrate wireguard-esp32 component */
/* Steps when integrated:
* 1. Parse private_key hex to binary
* 2. Parse server_pub hex to binary
* 3. Parse preshared_key hex to binary
* 4. Create WireGuard device with private_key
* 5. Add peer with server_pub, preshared_key, endpoint
* 6. Set peer allowed_ips
* 7. Start WireGuard tunnel
*/
/* Stub: mark tunnel as up and record handshake time */
s_tunnel_up = true;
s_last_handshake = (int64_t)time(NULL);
ESP_LOGI(TAG, "WireGuard tunnel initialized (stub)");
return ESP_OK;
}
esp_err_t tunnel_apply_config(const wg_config_t *config)
{
if (config == NULL) {
return ESP_ERR_INVALID_ARG;
}
ESP_LOGI(TAG, "Applying WireGuard config...");
ESP_LOGI(TAG, " Endpoint: %s", config->endpoint);
/* TODO: integrate wireguard-esp32 component */
/* Steps when integrated:
* 1. Stop existing tunnel
* 2. Reconfigure device with new keys
* 3. Reconfigure peer with new endpoint/allowed_ips
* 4. Restart tunnel
*/
s_tunnel_up = true;
s_last_handshake = (int64_t)time(NULL);
ESP_LOGI(TAG, "WireGuard config applied (stub)");
return ESP_OK;
}
esp_err_t tunnel_rebuild(const wg_config_t *config)
{
if (config == NULL) {
return ESP_ERR_INVALID_ARG;
}
ESP_LOGI(TAG, "Rebuilding WireGuard tunnel...");
/* Mark tunnel down during rebuild */
s_tunnel_up = false;
/* Apply new config */
esp_err_t err = tunnel_apply_config(config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Tunnel rebuild failed: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(TAG, "WireGuard tunnel rebuilt successfully");
return ESP_OK;
}
int64_t tunnel_get_last_handshake(void)
{
return s_last_handshake;
}
bool tunnel_is_up(void)
{
return s_tunnel_up;
}
+39
View File
@@ -0,0 +1,39 @@
/**
* @file tunnel.h
* @brief WireGuard Tunnel Management
*/
#ifndef TUNNEL_H
#define TUNNEL_H
#include <esp_err.h>
#include <stdint.h>
#include <stdbool.h>
#include "config.h"
/**
* @brief Initialize WireGuard tunnel
*/
esp_err_t tunnel_init(const wg_config_t *config);
/**
* @brief Apply configuration to tunnel
*/
esp_err_t tunnel_apply_config(const wg_config_t *config);
/**
* @brief Rebuild tunnel with new configuration
*/
esp_err_t tunnel_rebuild(const wg_config_t *config);
/**
* @brief Get last handshake timestamp (epoch seconds)
*/
int64_t tunnel_get_last_handshake(void);
/**
* @brief Check if tunnel is up
*/
bool tunnel_is_up(void);
#endif /* TUNNEL_H */
+147
View File
@@ -0,0 +1,147 @@
/**
* @file wifi.c
* @brief WiFi STA Management
*/
#include <string.h>
#include "wifi.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/task.h"
static const char *TAG = "nexus_wifi";
/* Event group bits */
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
/* Static state */
static EventGroupHandle_t s_wifi_event_group = NULL;
static esp_netif_t *s_sta_netif = NULL;
static esp_event_handler_instance_t s_any_id_instance = NULL;
static esp_event_handler_instance_t s_got_ip_instance = NULL;
static bool s_initialized = false;
/**
* @brief WiFi 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) {
switch (event_id) {
case WIFI_EVENT_STA_START:
ESP_LOGI(TAG, "WiFi STA started, connecting...");
esp_wifi_connect();
break;
case WIFI_EVENT_STA_DISCONNECTED:
ESP_LOGW(TAG, "WiFi disconnected");
xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
/* Auto-reconnect is handled by esp_wifi_connect() below */
esp_wifi_connect();
break;
default:
break;
}
} 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));
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}
esp_err_t wifi_init_sta(const char *ssid, const char *password)
{
if (ssid == NULL || password == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (s_initialized) {
ESP_LOGW(TAG, "WiFi already initialized");
return ESP_OK;
}
ESP_LOGI(TAG, "Initializing WiFi STA...");
/* Create event group */
s_wifi_event_group = xEventGroupCreate();
/* Initialize network interface */
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
s_sta_netif = esp_netif_create_default_wifi_sta();
/* Initialize WiFi */
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
/* Register event handlers */
ESP_ERROR_CHECK(esp_event_handler_instance_register(
WIFI_EVENT, ESP_EVENT_ANY_ID,
&wifi_event_handler, NULL, &s_any_id_instance));
ESP_ERROR_CHECK(esp_event_handler_instance_register(
IP_EVENT, IP_EVENT_STA_GOT_IP,
&wifi_event_handler, NULL, &s_got_ip_instance));
/* Configure WiFi */
wifi_config_t wifi_config = {
.sta = {
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
.pmf_cfg = {
.capable = true,
.required = false,
},
},
};
strncpy((char *)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid) - 1);
strncpy((char *)wifi_config.sta.password, 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());
s_initialized = true;
ESP_LOGI(TAG, "WiFi STA initialized, will connect to: %s", ssid);
return ESP_OK;
}
esp_err_t wifi_connect(void)
{
if (!s_initialized) {
ESP_LOGE(TAG, "WiFi not initialized, call wifi_init_sta first");
return ESP_ERR_INVALID_STATE;
}
ESP_LOGI(TAG, "Connecting to WiFi...");
esp_err_t err = esp_wifi_connect();
if (err != ESP_OK) {
ESP_LOGE(TAG, "WiFi connect failed: %s", esp_err_to_name(err));
}
return err;
}
esp_err_t wifi_disconnect(void)
{
if (!s_initialized) {
return ESP_ERR_INVALID_STATE;
}
ESP_LOGI(TAG, "Disconnecting from WiFi...");
return esp_wifi_disconnect();
}
bool wifi_is_connected(void)
{
if (s_wifi_event_group == NULL) {
return false;
}
return (xEventGroupGetBits(s_wifi_event_group) & WIFI_CONNECTED_BIT) != 0;
}
+31
View File
@@ -0,0 +1,31 @@
/**
* @file wifi.h
* @brief WiFi STA Management
*/
#ifndef WIFI_H
#define WIFI_H
#include <esp_err.h>
/**
* @brief Initialize WiFi in STA mode
*/
esp_err_t wifi_init_sta(const char *ssid, const char *password);
/**
* @brief Connect to WiFi
*/
esp_err_t wifi_connect(void);
/**
* @brief Disconnect from WiFi
*/
esp_err_t wifi_disconnect(void);
/**
* @brief Check if WiFi is connected
*/
bool wifi_is_connected(void);
#endif /* WIFI_H */