feat: implement ESP32 power outage monitor with MQTT and OTA support

- WiFi manager with captive portal for configuration
- MQTTS client with TLS support using EMQX Cloud
- NVS storage for persistent configuration
- Power monitoring with 30-second heartbeat messages
- OTA firmware updates via MQTT
- NTP time synchronization (Kenya timezone EAT-3)
- Automatic reconnection handling
- Rollback protection for OTA updates

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>
This commit is contained in:
Rodney Osodo
2025-12-19 15:37:30 +03:00
parent ef68a2a94d
commit 25006d2a1f
25 changed files with 4821 additions and 110 deletions
+3
View File
@@ -3,3 +3,6 @@
# ESP-IDF default build directory name
build/
# ESP-IDF default directory for project dependencies
managed_components/
+1 -5
View File
@@ -1,8 +1,4 @@
# The following lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
idf_build_set_property(MINIMAL_BUILD ON)
project(hello_world)
project(power_outage_monitor)
+97 -40
View File
@@ -1,58 +1,115 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | Linux |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | ----- |
# ESP32 Power Outage Monitor
# Hello World Example
An ESP-IDF based IoT application for ESP32 that monitors power outages by sending periodic heartbeat messages to an MQTT broker.
Starts a FreeRTOS task to print "Hello World".
## Hardware Requirements
(See the README.md file in the upper level 'examples' directory for more information about examples.)
- ESP32-C3 (or compatible ESP32 variant)
- USB cable for power and programming
- Internet connectivity via Wi-Fi
## How to use example
## Architecture
Follow detailed instructions provided specifically for this example.
### Data Model
Select the instructions depending on Espressif chip installed on your development board:
Messages published to MQTT follow this JSON schema:
- [ESP32 Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/stable/get-started/index.html)
- [ESP32-S2 Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/get-started/index.html)
## Example folder contents
The project **hello_world** contains one source file in C language [hello_world_main.c](main/hello_world_main.c). The file is located in folder [main](main).
ESP-IDF projects are built using CMake. The project build configuration is contained in `CMakeLists.txt` files that provide set of directives and instructions describing the project's source files and targets (executable, library, or both).
Below is short explanation of remaining files in the project folder.
```
├── CMakeLists.txt
├── pytest_hello_world.py Python script used for automated testing
├── main
│ ├── CMakeLists.txt
│ └── hello_world_main.c
└── README.md This is the file you are currently reading
```json
{
"device_id": "ESP32-AABBCCDDEEFF",
"power_status": true,
"latitude": 40.7128,
"longitude": -74.006,
"timestamp": "2025-12-17T10:30:45Z",
"metadata": {
"firmware_version": "1.0.0"
}
}
```
For more information on structure and contents of ESP-IDF projects, please refer to Section [Build System](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/build-system.html) of the ESP-IDF Programming Guide.
## Building and Flashing
## Troubleshooting
### Prerequisites
- Program upload failure
- Hardware connection is not correct: run `idf.py -p PORT monitor`, and reboot your board to see if there are any output logs.
- The baud rate for downloading is too high: lower your baud rate in the `menuconfig` menu, and try again.
1. **Install ESP-IDF v5.5.1 or later**:
## Technical support and feedback
Follow the official guide: <https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/>
Please use the following feedback channels:
2. **Set up ESP-IDF environment** (required before every build):
- For technical queries, go to the [esp32.com](https://esp32.com/) forum
- For a feature request or bug report, create a [GitHub issue](https://github.com/espressif/esp-idf/issues)
```bash
. $HOME/esp/esp-idf/export.sh
```
We will get back to you as soon as possible.
## Getting Started
### Quick Build (Recommended)
```bash
idf.py set-target esp32
idf.py menuconfig
idf.py build
```
### Manual Build
If you prefer manual steps:
1. **Set target**:
```bash
idf.py set-target esp32c3
```
2. **Build**:
```bash
idf.py build
```
The `sdkconfig.defaults` file automatically configures:
- Custom partition table (partitions.csv)
- 2MB flash size
- OTA rollback support
- Optimized settings
### Flash
```bash
idf.py -p /dev/ttyUSB0 flash monitor
```
Replace `/dev/ttyUSB0` with your serial port:
- Linux: `/dev/ttyUSB0` or `/dev/ttyACM0`
- macOS: `/dev/cu.usbserial-*`
- Windows: `COM3`, `COM4`, etc.
## Initial Setup
### First Boot
When the device boots for the first time (no Wi-Fi credentials stored):
1. The device starts in **Access Point mode**
2. Connect to Wi-Fi network: `PowerMonitor-Setup`
3. Password: `12345678`
4. Open browser and navigate to: `http://192.168.4.1`
5. Fill in the configuration form:
- **Wi-Fi SSID**: Your Wi-Fi network name
- **Wi-Fi Password**: Your Wi-Fi password
- **MQTT Broker URL**: e.g., `mqtt://broker.hivemq.com`
- **MQTT Port**: Default `1883`
- **MQTT Topic**: e.g., `power/monitor`
- **MQTT Username/Password**: Optional
- **Device ID**: Unique identifier for this device
- **Latitude/Longitude**: GPS coordinates
6. Click **Save and Restart**
The device will restart and connect to your Wi-Fi network.
### Subsequent Boots
On every boot after initial configuration:
1. Device loads credentials from NVS
2. Connects to Wi-Fi network
3. Connects to MQTT broker
4. Sends "power restored" event
5. Starts sending periodic heartbeats (every 30 seconds)
+21
View File
@@ -0,0 +1,21 @@
dependencies:
espressif/mqtt:
component_hash: ffdad5659706b4dc14bc63f8eb73ef765efa015bf7e9adf71c813d52a2dc9342
dependencies:
- name: idf
require: private
version: '>=5.3'
source:
registry_url: https://components.espressif.com/
type: service
version: 1.0.0
idf:
source:
type: idf
version: 5.5.1
direct_dependencies:
- espressif/mqtt
- idf
manifest_hash: d11c86bf1e65d8286be5092ea284c0f288cf083564f6cbe51df18abb3bc2cf71
target: esp32c3
version: 2.0.0
+16 -3
View File
@@ -1,3 +1,16 @@
idf_component_register(SRCS "hello_world_main.c"
PRIV_REQUIRES spi_flash
INCLUDE_DIRS "")
idf_component_register(SRCS "power_outage_main.c"
"nvs_storage.c"
"wifi_manager.c"
"mqtt_client.c"
"power_monitor.c"
"ota_manager.c"
INCLUDE_DIRS "."
PRIV_REQUIRES nvs_flash
esp_wifi
esp_netif
esp_http_server
esp_event
mqtt
json
app_update
esp_https_ota)
+55
View File
@@ -0,0 +1,55 @@
#ifndef CONFIG_H
#define CONFIG_H
#define FIRMWARE_VERSION "1.2.2"
#define WIFI_AP_SSID "PowerMonitor-Setup"
#define WIFI_AP_PASSWORD "12345678"
#define WIFI_AP_CHANNEL 1
#define WIFI_AP_MAX_CONNECTIONS 4
#define WIFI_RECONNECT_DELAY_MS 5000
#define MQTT_DEFAULT_BROKER "mqtts://qdadf0a9.ala.eu-central-1.emqxsl.com"
#define MQTT_DEFAULT_PORT 8883
#define MQTT_DEFAULT_TOPIC "tujuepawa/power/monitor"
#define MQTT_USERNAME "clad-citric-criteria"
#define MQTT_PASSWORD "uhLFkhbj4ge5wrnQQ2MA"
#define MQTT_QOS 2
#define MQTT_RETAIN 1
#define MQTT_USE_TLS 1
#define HEARTBEAT_INTERVAL_MS 10000
#define POWER_CHECK_INTERVAL_MS 1000
#define TIMEZONE "EAT-3"
#define DEFAULT_LATITUDE 0.0
#define DEFAULT_LONGITUDE 0.0
#define NVS_NAMESPACE "power_monitor"
#define NVS_WIFI_SSID "wifi_ssid"
#define NVS_WIFI_PASSWORD "wifi_pass"
#define NVS_MQTT_BROKER "mqtt_broker"
#define NVS_MQTT_PORT "mqtt_port"
#define NVS_MQTT_TOPIC "mqtt_topic"
#define NVS_MQTT_USERNAME "mqtt_user"
#define NVS_MQTT_PASSWORD "mqtt_pass"
#define NVS_DEVICE_ID "device_id"
#define NVS_LATITUDE "latitude"
#define NVS_LONGITUDE "longitude"
#define OTA_RECV_TIMEOUT_MS 5000
#define OTA_TOPIC "power/monitor/ota"
#define MAX_SSID_LEN 32
#define MAX_PASSWORD_LEN 64
#define MAX_BROKER_LEN 128
#define MAX_TOPIC_LEN 64
#define MAX_DEVICE_ID_LEN 32
#define MAX_JSON_LEN 512
#define HTTP_SERVER_PORT 80
#define HTTP_MAX_URI_LEN 256
#define HTTP_MAX_RESP_LEN 8192
#endif // CONFIG_H
+38 -36
View File
@@ -4,49 +4,51 @@
* SPDX-License-Identifier: CC0-1.0
*/
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#include <inttypes.h>
#include <stdio.h>
void app_main(void)
{
printf("Hello world!\n");
void app_main(void) {
printf("Hello world!\n");
/* Print chip information */
esp_chip_info_t chip_info;
uint32_t flash_size;
esp_chip_info(&chip_info);
printf("This is %s chip with %d CPU core(s), %s%s%s%s, ",
CONFIG_IDF_TARGET,
chip_info.cores,
(chip_info.features & CHIP_FEATURE_WIFI_BGN) ? "WiFi/" : "",
(chip_info.features & CHIP_FEATURE_BT) ? "BT" : "",
(chip_info.features & CHIP_FEATURE_BLE) ? "BLE" : "",
(chip_info.features & CHIP_FEATURE_IEEE802154) ? ", 802.15.4 (Zigbee/Thread)" : "");
/* Print chip information */
esp_chip_info_t chip_info;
uint32_t flash_size;
esp_chip_info(&chip_info);
printf("This is %s chip with %d CPU core(s), %s%s%s%s, ", CONFIG_IDF_TARGET,
chip_info.cores,
(chip_info.features & CHIP_FEATURE_WIFI_BGN) ? "WiFi/" : "",
(chip_info.features & CHIP_FEATURE_BT) ? "BT" : "",
(chip_info.features & CHIP_FEATURE_BLE) ? "BLE" : "",
(chip_info.features & CHIP_FEATURE_IEEE802154)
? ", 802.15.4 (Zigbee/Thread)"
: "");
unsigned major_rev = chip_info.revision / 100;
unsigned minor_rev = chip_info.revision % 100;
printf("silicon revision v%d.%d, ", major_rev, minor_rev);
if(esp_flash_get_size(NULL, &flash_size) != ESP_OK) {
printf("Get flash size failed");
return;
}
unsigned major_rev = chip_info.revision / 100;
unsigned minor_rev = chip_info.revision % 100;
printf("silicon revision v%d.%d, ", major_rev, minor_rev);
if (esp_flash_get_size(NULL, &flash_size) != ESP_OK) {
printf("Get flash size failed");
return;
}
printf("%" PRIu32 "MB %s flash\n", flash_size / (uint32_t)(1024 * 1024),
(chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external");
printf("%" PRIu32 "MB %s flash\n", flash_size / (uint32_t)(1024 * 1024),
(chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded"
: "external");
printf("Minimum free heap size: %" PRIu32 " bytes\n", esp_get_minimum_free_heap_size());
printf("Minimum free heap size: %" PRIu32 " bytes\n",
esp_get_minimum_free_heap_size());
for (int i = 10; i >= 0; i--) {
printf("Restarting in %d seconds...\n", i);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
printf("Restarting now.\n");
fflush(stdout);
esp_restart();
for (int i = 10; i >= 0; i--) {
printf("Restarting in %d seconds...\n", i);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
printf("Restarting now.\n");
fflush(stdout);
esp_restart();
}
+17
View File
@@ -0,0 +1,17 @@
## IDF Component Manager Manifest File
dependencies:
## Required IDF version
idf:
version: ">=4.1.0"
# # Put list of dependencies here
# # For components maintained by Espressif:
# component: "~1.0.0"
# # For 3rd party components:
# username/component: ">=1.0.0,<2.0.0"
# username2/component2:
# version: "~1.0.0"
# # For transient dependencies `public` flag can be set.
# # `public` flag doesn't have an effect dependencies of the `main` component.
# # All dependencies of `main` are public by default.
# public: true
espressif/mqtt: "*"
+34
View File
@@ -0,0 +1,34 @@
/*
* MQTT TLS/SSL Certificates
* EMQX Cloud CA Certificate (DigiCert Global Root G2)
*/
#ifndef MQTT_CERTS_H
#define MQTT_CERTS_H
/* DigiCert Global Root G2 - Used by EMQX Cloud */
static const char *mqtt_server_ca_cert =
"-----BEGIN CERTIFICATE-----\n"
"MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\n"
"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n"
"d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\n"
"MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\n"
"MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n"
"b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n"
"9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n"
"2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n"
"1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\n"
"q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\n"
"tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\n"
"vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\n"
"BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\n"
"5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\n"
"1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\n"
"NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\n"
"Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\n"
"8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\n"
"pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\n"
"MrY=\n"
"-----END CERTIFICATE-----\n";
#endif // MQTT_CERTS_H
+217
View File
@@ -0,0 +1,217 @@
#include "mqtt_client.h"
#include "cJSON.h"
#include "config.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mqtt_certs.h"
#include "nvs_storage.h"
#include "ota_manager.h"
#include <string.h>
#include <sys/time.h>
#include <time.h>
static const char *TAG = "MQTT_CLIENT";
static esp_mqtt_client_handle_t client = NULL;
static bool is_connected = false;
static char mqtt_topic[MAX_TOPIC_LEN] = {0};
static void ota_update_task(void *pvParameter) {
char *url = (char *)pvParameter;
ESP_LOGI(TAG, "Starting OTA update from: %s", url);
esp_err_t ret = ota_start_update(url);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "OTA update failed");
}
free(url);
vTaskDelete(NULL);
}
static void mqtt_event_handler(void *handler_args, esp_event_base_t base,
int32_t event_id, void *event_data) {
esp_mqtt_event_handle_t event = (esp_mqtt_event_handle_t)event_data;
switch ((esp_mqtt_event_id_t)event_id) {
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "MQTT connected");
is_connected = true;
char device_id[MAX_DEVICE_ID_LEN] = {0};
if (nvs_load_device_id(device_id, sizeof(device_id)) != ESP_OK) {
ESP_LOGE(TAG, "Failed to load device ID, cannot subscribe to topics");
break;
}
char topic[128];
snprintf(topic, sizeof(topic), "power/monitor/%s/#", device_id);
esp_mqtt_client_subscribe(client, topic, 0);
char ota_topic[128];
snprintf(ota_topic, sizeof(ota_topic), "power/monitor/%s/ota", device_id);
esp_mqtt_client_subscribe(client, ota_topic, 1);
ESP_LOGI(TAG, "Subscribed to OTA updates: %s", ota_topic);
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGW(TAG, "MQTT disconnected");
is_connected = false;
break;
case MQTT_EVENT_ERROR:
ESP_LOGE(TAG, "MQTT error");
break;
case MQTT_EVENT_DATA:
ESP_LOGI(TAG, "MQTT message received");
ESP_LOGI(TAG, "TOPIC=%.*s", event->topic_len, event->topic);
ESP_LOGI(TAG, "DATA=%.*s", event->data_len, event->data);
// Create null-terminated topic buffer for safe string comparison
if (event->topic_len > 0 && event->topic_len < 128) {
char topic_buf[128];
memcpy(topic_buf, event->topic, event->topic_len);
topic_buf[event->topic_len] = '\0';
if (strstr(topic_buf, "/ota") != NULL) {
char url[256] = {0};
int len = event->data_len < 255 ? event->data_len : 255;
memcpy(url, event->data, len);
url[len] = '\0';
ESP_LOGI(TAG, "OTA update requested: %s", url);
char *url_copy = strdup(url);
if (url_copy != NULL) {
BaseType_t task_result = xTaskCreate(ota_update_task, "ota_update",
8192, url_copy, 5, NULL);
if (task_result != pdPASS) {
ESP_LOGE(TAG, "Failed to create OTA task");
free(url_copy);
}
} else {
ESP_LOGE(TAG, "Failed to allocate memory for OTA URL");
}
}
}
break;
default:
break;
}
}
esp_err_t mqtt_client_init(void) {
char broker[MAX_BROKER_LEN] = {0};
uint16_t port = MQTT_DEFAULT_PORT;
// Use hardcoded broker, port, topic, and credentials from config.h
strcpy(broker, MQTT_DEFAULT_BROKER);
port = MQTT_DEFAULT_PORT;
strcpy(mqtt_topic, MQTT_DEFAULT_TOPIC);
ESP_LOGI(TAG, "Connecting to MQTT broker: %s:%d, topic: %s", broker, port,
mqtt_topic);
esp_mqtt_client_config_t mqtt_cfg = {
.broker.address.uri = broker,
.broker.address.port = port,
.network.disable_auto_reconnect = false,
.credentials.set_null_client_id = false,
};
if (port == 8883 || strstr(broker, "mqtts://") != NULL) {
ESP_LOGI(TAG, "Enabling MQTTS with TLS/SSL");
mqtt_cfg.broker.verification.certificate = mqtt_server_ca_cert;
mqtt_cfg.broker.verification.skip_cert_common_name_check = false;
}
// Use hardcoded credentials from config.h
mqtt_cfg.credentials.username = MQTT_USERNAME;
mqtt_cfg.credentials.authentication.password = MQTT_PASSWORD;
ESP_LOGI(TAG, "Using MQTT authentication with username: %s", MQTT_USERNAME);
client = esp_mqtt_client_init(&mqtt_cfg);
if (client == NULL) {
ESP_LOGE(TAG, "Failed to initialize MQTT client");
return ESP_FAIL;
}
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler,
NULL);
return ESP_OK;
}
esp_err_t mqtt_client_start(void) {
if (client == NULL) {
return ESP_FAIL;
}
esp_err_t ret = esp_mqtt_client_start(client);
return ret;
}
esp_err_t mqtt_client_stop(void) {
if (client == NULL) {
return ESP_OK;
}
esp_mqtt_client_stop(client);
return ESP_OK;
}
esp_err_t mqtt_publish_power_status(bool power_on, const char *device_id,
float latitude, float longitude) {
if (!is_connected) {
ESP_LOGW(TAG, "MQTT not connected, cannot publish");
return ESP_FAIL;
}
time_t now;
struct tm timeinfo;
time(&now);
gmtime_r(&now, &timeinfo);
char timestamp[32];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", &timeinfo);
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "device_id", device_id);
cJSON_AddBoolToObject(root, "power_status", power_on);
cJSON_AddNumberToObject(root, "latitude", latitude);
cJSON_AddNumberToObject(root, "longitude", longitude);
cJSON_AddStringToObject(root, "timestamp", timestamp);
cJSON *metadata = cJSON_CreateObject();
cJSON_AddStringToObject(metadata, "firmware_version", FIRMWARE_VERSION);
cJSON_AddItemToObject(root, "metadata", metadata);
char *json_str = cJSON_PrintUnformatted(root);
if (json_str == NULL) {
cJSON_Delete(root);
ESP_LOGE(TAG, "Failed to create JSON");
return ESP_FAIL;
}
ESP_LOGI(TAG, "Publishing: %s", json_str);
int msg_id = esp_mqtt_client_publish(client, mqtt_topic, json_str,
strlen(json_str), MQTT_QOS, MQTT_RETAIN);
cJSON_free(json_str);
cJSON_Delete(root);
if (msg_id < 0) {
ESP_LOGE(TAG, "Failed to publish message");
return ESP_FAIL;
}
return ESP_OK;
}
bool mqtt_is_connected(void) { return is_connected; }
+23
View File
@@ -0,0 +1,23 @@
#ifndef MQTT_LIB_H
#define MQTT_LIB_H
#include "esp_err.h"
#include <stdbool.h>
/* Initialize MQTT client */
esp_err_t mqtt_client_init(void);
/* Start MQTT client */
esp_err_t mqtt_client_start(void);
/* Stop MQTT client */
esp_err_t mqtt_client_stop(void);
/* Publish power status message */
esp_err_t mqtt_publish_power_status(bool power_on, const char *device_id,
float latitude, float longitude);
/* Check if MQTT is connected */
bool mqtt_is_connected(void);
#endif // MQTT_LIB_H
+359
View File
@@ -0,0 +1,359 @@
#include "nvs_storage.h"
#include "config.h"
#include "esp_log.h"
#include "nvs.h"
#include "nvs_flash.h"
#include <string.h>
static const char *TAG = "NVS_STORAGE";
esp_err_t nvs_storage_init(void) {
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_LOGW(TAG, "Erasing NVS flash and reinitializing");
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
return ret;
}
esp_err_t nvs_save_wifi_credentials(const char *ssid, const char *password) {
nvs_handle_t handle;
esp_err_t ret = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to open NVS handle: %s", esp_err_to_name(ret));
return ret;
}
ret = nvs_set_str(handle, NVS_WIFI_SSID, ssid);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to save SSID: %s", esp_err_to_name(ret));
nvs_close(handle);
return ret;
}
ret = nvs_set_str(handle, NVS_WIFI_PASSWORD, password);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to save password: %s", esp_err_to_name(ret));
nvs_close(handle);
return ret;
}
ret = nvs_commit(handle);
nvs_close(handle);
return ret;
}
esp_err_t nvs_load_wifi_credentials(char *ssid, size_t ssid_len, char *password,
size_t pass_len) {
nvs_handle_t handle;
esp_err_t ret = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
if (ret != ESP_OK) {
return ret;
}
size_t required_size = ssid_len;
ret = nvs_get_str(handle, NVS_WIFI_SSID, ssid, &required_size);
if (ret != ESP_OK) {
if (ret == ESP_ERR_NVS_INVALID_LENGTH) {
ESP_LOGE(TAG, "SSID buffer too small, required: %zu, provided: %zu",
required_size, ssid_len);
}
nvs_close(handle);
return ret;
}
required_size = pass_len;
ret = nvs_get_str(handle, NVS_WIFI_PASSWORD, password, &required_size);
if (ret != ESP_OK) {
if (ret == ESP_ERR_NVS_INVALID_LENGTH) {
ESP_LOGE(TAG, "Password buffer too small, required: %zu, provided: %zu",
required_size, pass_len);
}
}
nvs_close(handle);
return ret;
}
bool nvs_has_wifi_credentials(void) {
nvs_handle_t handle;
esp_err_t ret = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
if (ret != ESP_OK) {
return false;
}
size_t len = 0;
ret = nvs_get_str(handle, NVS_WIFI_SSID, NULL, &len);
nvs_close(handle);
return (ret == ESP_OK && len > 0);
}
esp_err_t nvs_save_mqtt_config(const char *broker, uint16_t port,
const char *topic, const char *username,
const char *password) {
nvs_handle_t handle;
esp_err_t ret = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to open NVS handle: %s", esp_err_to_name(ret));
return ret;
}
ret = nvs_set_str(handle, NVS_MQTT_BROKER, broker);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to save MQTT broker: %s", esp_err_to_name(ret));
nvs_close(handle);
return ret;
}
ret = nvs_set_u16(handle, NVS_MQTT_PORT, port);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to save MQTT port: %s", esp_err_to_name(ret));
nvs_close(handle);
return ret;
}
ret = nvs_set_str(handle, NVS_MQTT_TOPIC, topic);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to save MQTT topic: %s", esp_err_to_name(ret));
nvs_close(handle);
return ret;
}
if (username && strlen(username) > 0) {
ret = nvs_set_str(handle, NVS_MQTT_USERNAME, username);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to save MQTT username: %s", esp_err_to_name(ret));
nvs_close(handle);
return ret;
}
}
if (password && strlen(password) > 0) {
ret = nvs_set_str(handle, NVS_MQTT_PASSWORD, password);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to save MQTT password: %s", esp_err_to_name(ret));
nvs_close(handle);
return ret;
}
}
ret = nvs_commit(handle);
nvs_close(handle);
return ret;
}
esp_err_t nvs_load_mqtt_broker(char *broker, size_t len) {
nvs_handle_t handle;
esp_err_t ret = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
if (ret != ESP_OK) {
return ret;
}
size_t required_size = len;
ret = nvs_get_str(handle, NVS_MQTT_BROKER, broker, &required_size);
if (ret != ESP_OK) {
if (ret == ESP_ERR_NVS_INVALID_LENGTH) {
ESP_LOGE(TAG, "Broker buffer too small, required: %zu, provided: %zu",
required_size, len);
}
}
nvs_close(handle);
return ret;
}
esp_err_t nvs_load_mqtt_port(uint16_t *port) {
nvs_handle_t handle;
esp_err_t ret = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
if (ret != ESP_OK) {
return ret;
}
ret = nvs_get_u16(handle, NVS_MQTT_PORT, port);
nvs_close(handle);
return ret;
}
esp_err_t nvs_load_mqtt_topic(char *topic, size_t len) {
nvs_handle_t handle;
esp_err_t ret = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
if (ret != ESP_OK) {
return ret;
}
size_t required_size = len;
ret = nvs_get_str(handle, NVS_MQTT_TOPIC, topic, &required_size);
if (ret != ESP_OK) {
if (ret == ESP_ERR_NVS_INVALID_LENGTH) {
ESP_LOGE(TAG, "Topic buffer too small, required: %zu, provided: %zu",
required_size, len);
}
}
nvs_close(handle);
return ret;
}
esp_err_t nvs_load_mqtt_username(char *username, size_t len) {
nvs_handle_t handle;
esp_err_t ret = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
if (ret != ESP_OK) {
return ret;
}
size_t required_size = len;
ret = nvs_get_str(handle, NVS_MQTT_USERNAME, username, &required_size);
if (ret != ESP_OK) {
if (ret == ESP_ERR_NVS_INVALID_LENGTH) {
ESP_LOGE(TAG, "Username buffer too small, required: %zu, provided: %zu",
required_size, len);
}
}
nvs_close(handle);
return ret;
}
esp_err_t nvs_load_mqtt_password(char *password, size_t len) {
nvs_handle_t handle;
esp_err_t ret = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
if (ret != ESP_OK) {
return ret;
}
size_t required_size = len;
ret = nvs_get_str(handle, NVS_MQTT_PASSWORD, password, &required_size);
if (ret != ESP_OK) {
if (ret == ESP_ERR_NVS_INVALID_LENGTH) {
ESP_LOGE(TAG, "Password buffer too small, required: %zu, provided: %zu",
required_size, len);
}
}
nvs_close(handle);
return ret;
}
esp_err_t nvs_save_device_id(const char *device_id) {
nvs_handle_t handle;
esp_err_t ret = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to open NVS handle: %s", esp_err_to_name(ret));
return ret;
}
ret = nvs_set_str(handle, NVS_DEVICE_ID, device_id);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to save device ID: %s", esp_err_to_name(ret));
nvs_close(handle);
return ret;
}
ret = nvs_commit(handle);
nvs_close(handle);
return ret;
}
esp_err_t nvs_load_device_id(char *device_id, size_t len) {
nvs_handle_t handle;
esp_err_t ret = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
if (ret != ESP_OK) {
return ret;
}
size_t required_size = len;
ret = nvs_get_str(handle, NVS_DEVICE_ID, device_id, &required_size);
if (ret != ESP_OK) {
if (ret == ESP_ERR_NVS_INVALID_LENGTH) {
ESP_LOGE(TAG, "Device ID buffer too small, required: %zu, provided: %zu",
required_size, len);
}
}
nvs_close(handle);
return ret;
}
esp_err_t nvs_save_location(float latitude, float longitude) {
nvs_handle_t handle;
esp_err_t ret = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to open NVS handle: %s", esp_err_to_name(ret));
return ret;
}
ret = nvs_set_blob(handle, NVS_LATITUDE, &latitude, sizeof(float));
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to save latitude: %s", esp_err_to_name(ret));
nvs_close(handle);
return ret;
}
ret = nvs_set_blob(handle, NVS_LONGITUDE, &longitude, sizeof(float));
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to save longitude: %s", esp_err_to_name(ret));
nvs_close(handle);
return ret;
}
ret = nvs_commit(handle);
nvs_close(handle);
return ret;
}
esp_err_t nvs_load_location(float *latitude, float *longitude) {
nvs_handle_t handle;
esp_err_t ret = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
if (ret != ESP_OK) {
return ret;
}
size_t size = sizeof(float);
ret = nvs_get_blob(handle, NVS_LATITUDE, latitude, &size);
if (ret == ESP_OK) {
size = sizeof(float);
ret = nvs_get_blob(handle, NVS_LONGITUDE, longitude, &size);
}
nvs_close(handle);
return ret;
}
esp_err_t nvs_erase(void) {
nvs_handle_t handle;
esp_err_t ret = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle);
if (ret != ESP_OK) {
return ret;
}
ret = nvs_erase_all(handle);
nvs_commit(handle);
nvs_close(handle);
return ret;
}
+35
View File
@@ -0,0 +1,35 @@
#ifndef NVS_STORAGE_H
#define NVS_STORAGE_H
#include "esp_err.h"
#include <stdbool.h>
/* Initialize NVS */
esp_err_t nvs_storage_init(void);
/* Wi-Fi Credentials */
esp_err_t nvs_save_wifi_credentials(const char *ssid, const char *password);
esp_err_t nvs_load_wifi_credentials(char *ssid, size_t ssid_len, char *password,
size_t pass_len);
bool nvs_has_wifi_credentials(void);
/* MQTT Configuration */
esp_err_t nvs_save_mqtt_config(const char *broker, uint16_t port,
const char *topic, const char *username,
const char *password);
esp_err_t nvs_load_mqtt_broker(char *broker, size_t len);
esp_err_t nvs_load_mqtt_port(uint16_t *port);
esp_err_t nvs_load_mqtt_topic(char *topic, size_t len);
esp_err_t nvs_load_mqtt_username(char *username, size_t len);
esp_err_t nvs_load_mqtt_password(char *password, size_t len);
/* Device Configuration */
esp_err_t nvs_save_device_id(const char *device_id);
esp_err_t nvs_load_device_id(char *device_id, size_t len);
esp_err_t nvs_save_location(float latitude, float longitude);
esp_err_t nvs_load_location(float *latitude, float *longitude);
/* Erase all settings */
esp_err_t nvs_erase(void);
#endif // NVS_STORAGE_H
+70
View File
@@ -0,0 +1,70 @@
/*
* OTA Server Certificate for gist.rodneyosodo.com
*/
#ifndef OTA_CERTS_H
#define OTA_CERTS_H
static const char ota_server_cert_pem[] =
"-----BEGIN CERTIFICATE-----\n"
"MIIDsDCCA1WgAwIBAgIQVJFJzsk2fvYO6u4tfCnjNDAKBggqhkjOPQQDAjA7MQsw\n"
"CQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMQwwCgYD\n"
"VQQDEwNXRTEwHhcNMjUxMTAxMDYyMjM3WhcNMjYwMTMwMDcyMDU1WjAaMRgwFgYD\n"
"VQQDEw9yb2RuZXlvc29kby5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASn\n"
"jiG/+LOnHpIYT78fvoELyqtyJqffXHkchyl5J0TEw/0clPlnSgaPxGBE2z9+xKC2\n"
"kz9cM3slRlBdWMkCK3Ego4ICWjCCAlYwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM\n"
"MAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFAZu/CV+AbCUx0Ej\n"
"lSopJ+RsayXAMB8GA1UdIwQYMBaAFJB3kjVnxP+ozKnme9mAeXvMk/k4MF4GCCsG\n"
"AQUFBwEBBFIwUDAnBggrBgEFBQcwAYYbaHR0cDovL28ucGtpLmdvb2cvcy93ZTEv\n"
"VkpFMCUGCCsGAQUFBzAChhlodHRwOi8vaS5wa2kuZ29vZy93ZTEuY3J0MC0GA1Ud\n"
"EQQmMCSCD3JvZG5leW9zb2RvLmNvbYIRKi5yb2RuZXlvc29kby5jb20wEwYDVR0g\n"
"BAwwCjAIBgZngQwBAgEwNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2MucGtpLmdv\n"
"b2cvd2UxL2J0dmQ2Nlo5dVFZLmNybDCCAQMGCisGAQQB1nkCBAIEgfQEgfEA7wB2\n"
"AA5XlLzzrqk+MxssmQez95Dfm8I9cTIl3SGpJaxhxU4hAAABmj5L+gsAAAQDAEcw\n"
"RQIhAOKqqnExy3qGupBgBsuFwdMG+m3CpV20nI8bshlQITASAiBEy+DB052tglIx\n"
"80Z7IVd2jWaocNGXt7VcinavsGo8NAB1ANFuqaVoB35mNaA/N6XdvAOlPEESFNSI\n"
"GPXpMbMjy5UEAAABmj5L+xEAAAQDAEYwRAIgL9Tmpw2dyD+BaP3N0wicEzPkYK2q\n"
"15zfv+cx9h5rVCkCIAdL9OLitGYoUhLJrWDDdQwwB1AbnRHgOvfxQtrzyDXgMAoG\n"
"CCqGSM49BAMCA0kAMEYCIQCKISJTQ/mwjgTzzRST1821dl9UJBSob/MZmqICXYm+\n"
"DwIhAKB7PAG6aZpUrTXnxDCiIhiErcZr7F7Xuq8BiGEDwRKf\n"
"-----END CERTIFICATE-----\n"
"-----BEGIN CERTIFICATE-----\n"
"MIICnzCCAiWgAwIBAgIQf/MZd5csIkp2FV0TttaF4zAKBggqhkjOPQQDAzBHMQsw\n"
"CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU\n"
"MBIGA1UEAxMLR1RTIFJvb3QgUjQwHhcNMjMxMjEzMDkwMDAwWhcNMjkwMjIwMTQw\n"
"MDAwWjA7MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZp\n"
"Y2VzMQwwCgYDVQQDEwNXRTEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARvzTr+\n"
"Z1dHTCEDhUDCR127WEcPQMFcF4XGGTfn1XzthkubgdnXGhOlCgP4mMTG6J7/EFmP\n"
"LCaY9eYmJbsPAvpWo4H+MIH7MA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggr\n"
"BgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU\n"
"kHeSNWfE/6jMqeZ72YB5e8yT+TgwHwYDVR0jBBgwFoAUgEzW63T/STaj1dj8tT7F\n"
"avCUHYwwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzAChhhodHRwOi8vaS5wa2ku\n"
"Z29vZy9yNC5jcnQwKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL2MucGtpLmdvb2cv\n"
"ci9yNC5jcmwwEwYDVR0gBAwwCjAIBgZngQwBAgEwCgYIKoZIzj0EAwMDaAAwZQIx\n"
"AOcCq1HW90OVznX+0RGU1cxAQXomvtgM8zItPZCuFQ8jSBJSjz5keROv9aYsAm5V\n"
"sQIwJonMaAFi54mrfhfoFNZEfuNMSQ6/bIBiNLiyoX46FohQvKeIoJ99cx7sUkFN\n"
"7uJW\n"
"-----END CERTIFICATE-----\n"
"-----BEGIN CERTIFICATE-----\n"
"MIIDejCCAmKgAwIBAgIQf+UwvzMTQ77dghYQST2KGzANBgkqhkiG9w0BAQsFADBX\n"
"MQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UE\n"
"CxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIzMTEx\n"
"NTAzNDMyMVoXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoT\n"
"GUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFI0\n"
"MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE83Rzp2iLYK5DuDXFgTB7S0md+8Fhzube\n"
"Rr1r1WEYNa5A3XP3iZEwWus87oV8okB2O6nGuEfYKueSkWpz6bFyOZ8pn6KY019e\n"
"WIZlD6GEZQbR3IvJx3PIjGov5cSr0R2Ko4H/MIH8MA4GA1UdDwEB/wQEAwIBhjAd\n"
"BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zAd\n"
"BgNVHQ4EFgQUgEzW63T/STaj1dj8tT7FavCUHYwwHwYDVR0jBBgwFoAUYHtmGkUN\n"
"l8qJUC99BM00qP/8/UswNgYIKwYBBQUHAQEEKjAoMCYGCCsGAQUFBzAChhpodHRw\n"
"Oi8vaS5wa2kuZ29vZy9nc3IxLmNydDAtBgNVHR8EJjAkMCKgIKAehhxodHRwOi8v\n"
"Yy5wa2kuZ29vZy9yL2dzcjEuY3JsMBMGA1UdIAQMMAowCAYGZ4EMAQIBMA0GCSqG\n"
"SIb3DQEBCwUAA4IBAQAYQrsPBtYDh5bjP2OBDwmkoWhIDDkic574y04tfzHpn+cJ\n"
"odI2D4SseesQ6bDrarZ7C30ddLibZatoKiws3UL9xnELz4ct92vID24FfVbiI1hY\n"
"+SW6FoVHkNeWIP0GCbaM4C6uVdF5dTUsMVs/ZbzNnIdCp5Gxmx5ejvEau8otR/Cs\n"
"kGN+hr/W5GvT1tMBjgWKZ1i4//emhA1JG1BbPzoLJQvyEotc03lXjTaCzv8mEbep\n"
"8RqZ7a2CPsgRbuvTPBwcOMBBmuFeU88+FSBX6+7iP0il8b4Z0QFqIwwMHfs/L6K1\n"
"vepuoxtGzi4CZ68zJpiq1UvSqTbFJjtbD4seiMHl\n"
"-----END CERTIFICATE-----\n";
#endif
+76
View File
@@ -0,0 +1,76 @@
#include "ota_manager.h"
#include "config.h"
#include "esp_http_client.h"
#include "esp_https_ota.h"
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "ota_certs.h"
#include <string.h>
static const char *TAG = "OTA_MANAGER";
esp_err_t ota_manager_init(void) {
const esp_partition_t *running = esp_ota_get_running_partition();
esp_app_desc_t running_app_info;
if (esp_ota_get_partition_description(running, &running_app_info) == ESP_OK) {
ESP_LOGI(TAG, "Current firmware version: %s", running_app_info.version);
ESP_LOGI(TAG, "Running partition: %s", running->label);
}
esp_ota_img_states_t ota_state;
if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) {
if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) {
ESP_LOGI(TAG, "OTA update successful, marking as valid");
esp_ota_mark_app_valid_cancel_rollback();
}
}
return ESP_OK;
}
esp_err_t ota_start_update(const char *url) {
ESP_LOGI(TAG, "Starting OTA update from: %s", url);
esp_http_client_config_t config = {
.url = url,
.timeout_ms = OTA_RECV_TIMEOUT_MS,
.keep_alive_enable = true,
};
if (strncmp(url, "https://", 8) == 0) {
if (strstr(url, "gist.rodneyosodo.com") != NULL ||
strstr(url, "rodneyosodo.com") != NULL) {
ESP_LOGI(TAG, "Using rodneyosodo.com certificate");
config.cert_pem = ota_server_cert_pem;
config.skip_cert_common_name_check = false;
} else {
ESP_LOGE(
TAG,
"HTTPS URL from unknown domain - OTA update rejected for security");
return ESP_ERR_NOT_SUPPORTED;
}
} else if (strncmp(url, "http://", 7) == 0) {
ESP_LOGW(TAG, "Using HTTP (non-encrypted) for OTA update");
// HTTP is allowed but not recommended
} else {
ESP_LOGE(TAG, "Invalid OTA URL scheme");
return ESP_ERR_INVALID_ARG;
}
esp_https_ota_config_t ota_config = {
.http_config = &config,
};
esp_err_t ret = esp_https_ota(&ota_config);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "OTA update successful, restarting...");
vTaskDelay(pdMS_TO_TICKS(2000));
esp_restart();
} else {
ESP_LOGE(TAG, "OTA update failed: %s", esp_err_to_name(ret));
}
return ret;
}
+12
View File
@@ -0,0 +1,12 @@
#ifndef OTA_MANAGER_H
#define OTA_MANAGER_H
#include "esp_err.h"
/* Initialize OTA manager */
esp_err_t ota_manager_init(void);
/* Start OTA update from URL */
esp_err_t ota_start_update(const char *url);
#endif // OTA_MANAGER_H
+128
View File
@@ -0,0 +1,128 @@
#include "power_monitor.h"
#include "config.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mqtt_client.h"
#include "mqtt_lib.h"
#include "nvs_storage.h"
#include <string.h>
static const char *TAG = "POWER_MONITOR";
static TaskHandle_t monitor_task_handle = NULL;
static bool is_running = false;
static char device_id[MAX_DEVICE_ID_LEN] = {0};
static float latitude = DEFAULT_LATITUDE;
static float longitude = DEFAULT_LONGITUDE;
static void power_monitor_task(void *pvParameters) {
ESP_LOGI(TAG, "Power monitor task started");
while (is_running) {
// Send heartbeat (power ON status)
if (mqtt_is_connected()) {
ESP_LOGI(TAG, "Sending heartbeat");
mqtt_publish_power_status(true, device_id, latitude, longitude);
} else {
ESP_LOGW(TAG, "MQTT not connected, skipping heartbeat");
}
// Wait for next heartbeat interval
vTaskDelay(pdMS_TO_TICKS(HEARTBEAT_INTERVAL_MS));
}
ESP_LOGI(TAG, "Power monitor task stopped");
monitor_task_handle = NULL;
vTaskDelete(NULL);
}
esp_err_t power_monitor_init(void) {
if (nvs_load_device_id(device_id, sizeof(device_id)) != ESP_OK) {
ESP_LOGW(TAG, "No device ID found, using MAC address");
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
snprintf(device_id, sizeof(device_id), "ESP32-%02X%02X%02X%02X%02X%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
nvs_save_device_id(device_id);
}
if (nvs_load_location(&latitude, &longitude) != ESP_OK) {
ESP_LOGW(TAG, "No location found, using defaults");
latitude = DEFAULT_LATITUDE;
longitude = DEFAULT_LONGITUDE;
}
ESP_LOGI(TAG, "Power monitor initialized - Device: %s, Location: %.6f, %.6f",
device_id, latitude, longitude);
return ESP_OK;
}
esp_err_t power_monitor_start(void) {
if (is_running) {
ESP_LOGW(TAG, "Power monitor already running");
return ESP_OK;
}
is_running = true;
xTaskCreate(power_monitor_task, "power_monitor", 4096, NULL, 5,
&monitor_task_handle);
return ESP_OK;
}
esp_err_t power_monitor_stop(void) {
is_running = false;
if (monitor_task_handle != NULL) {
// Wait for task to finish
vTaskDelay(pdMS_TO_TICKS(100));
}
return ESP_OK;
}
esp_err_t power_monitor_reload_config(void) {
// Reload device ID
if (nvs_load_device_id(device_id, sizeof(device_id)) != ESP_OK) {
ESP_LOGW(TAG, "No device ID found, using MAC address");
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
snprintf(device_id, sizeof(device_id), "ESP32-%02X%02X%02X%02X%02X%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
nvs_save_device_id(device_id);
}
// Reload location
if (nvs_load_location(&latitude, &longitude) != ESP_OK) {
ESP_LOGW(TAG, "No location found, using defaults");
latitude = DEFAULT_LATITUDE;
longitude = DEFAULT_LONGITUDE;
}
ESP_LOGI(TAG, "Configuration reloaded - Device: %s, Location: %.6f, %.6f",
device_id, latitude, longitude);
return ESP_OK;
}
esp_err_t power_monitor_send_power_restored(void) {
ESP_LOGI(TAG, "Sending power restored event");
// Wait a bit for MQTT to connect
int retries = 0;
while (!mqtt_is_connected() && retries < 50) {
vTaskDelay(pdMS_TO_TICKS(100));
retries++;
}
if (mqtt_is_connected()) {
return mqtt_publish_power_status(true, device_id, latitude, longitude);
} else {
ESP_LOGW(TAG, "MQTT not connected, cannot send power restored event");
return ESP_FAIL;
}
}
+21
View File
@@ -0,0 +1,21 @@
#ifndef POWER_MONITOR_H
#define POWER_MONITOR_H
#include "esp_err.h"
/* Initialize power monitor */
esp_err_t power_monitor_init(void);
/* Start monitoring and heartbeat */
esp_err_t power_monitor_start(void);
/* Stop monitoring */
esp_err_t power_monitor_stop(void);
/* Reload configuration from NVS */
esp_err_t power_monitor_reload_config(void);
/* Send power restored event */
esp_err_t power_monitor_send_power_restored(void);
#endif // POWER_MONITOR_H
+143
View File
@@ -0,0 +1,143 @@
#include "config.h"
#include "esp_log.h"
#include "esp_sntp.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "mqtt_client.h"
#include "mqtt_lib.h"
#include "nvs_storage.h"
#include "ota_manager.h"
#include "power_monitor.h"
#include "wifi_manager.h"
#include <stdio.h>
#include <time.h>
static const char *TAG = "MAIN";
static void time_sync_notification_cb(struct timeval *tv) {
ESP_LOGI(TAG, "Time synchronized with NTP server");
}
static void initialize_sntp(void) {
// Check if SNTP is already initialized
if (esp_sntp_enabled()) {
ESP_LOGI(TAG, "SNTP already initialized, skipping");
return;
}
ESP_LOGI(TAG, "Initializing SNTP");
setenv("TZ", TIMEZONE, 1);
tzset();
esp_sntp_setoperatingmode(SNTP_OPMODE_POLL);
esp_sntp_setservername(0, "pool.ntp.org");
esp_sntp_setservername(1, "time.google.com");
esp_sntp_set_time_sync_notification_cb(time_sync_notification_cb);
esp_sntp_init();
time_t now = 0;
struct tm timeinfo = {0};
int retry = 0;
const int retry_count = 15;
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(2000 / portTICK_PERIOD_MS);
}
time(&now);
localtime_r(&now, &timeinfo);
if (timeinfo.tm_year < (2024 - 1900)) {
ESP_LOGW(TAG, "Time not set yet, continuing anyway");
} else {
char strftime_buf[64];
strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo);
ESP_LOGI(TAG, "Current time: %s", strftime_buf);
}
}
static void wifi_event_callback(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data) {
if (event_base == WIFI_MANAGER_EVENT) {
switch (event_id) {
case WIFI_MANAGER_EVENT_STA_CONNECTED:
ESP_LOGI(TAG, "Connected to WiFi, syncing time...");
initialize_sntp();
ESP_LOGI(TAG, "Starting MQTT");
mqtt_client_start();
vTaskDelay(pdMS_TO_TICKS(2000));
power_monitor_send_power_restored();
power_monitor_start();
break;
case WIFI_MANAGER_EVENT_STA_DISCONNECTED:
ESP_LOGW(TAG, "WiFi disconnected");
power_monitor_stop();
mqtt_client_stop();
break;
case WIFI_MANAGER_EVENT_AP_STARTED:
ESP_LOGI(TAG, "Access Point started for configuration");
break;
default:
break;
}
}
}
void app_main(void) {
ESP_ERROR_CHECK(nvs_storage_init());
ESP_ERROR_CHECK(wifi_manager_init());
ESP_ERROR_CHECK(mqtt_client_init());
ESP_ERROR_CHECK(power_monitor_init());
ESP_ERROR_CHECK(ota_manager_init());
ESP_ERROR_CHECK(esp_event_handler_register(
WIFI_MANAGER_EVENT, ESP_EVENT_ANY_ID, &wifi_event_callback, NULL));
if (nvs_has_wifi_credentials()) {
ESP_LOGI(TAG, "WiFi credentials found, connecting to network");
ESP_ERROR_CHECK(wifi_manager_connect_sta());
} else {
ESP_LOGI(TAG,
"No WiFi credentials, starting Access Point for configuration");
ESP_LOGI(TAG, "Connect to '%s' with password '%s'", WIFI_AP_SSID,
WIFI_AP_PASSWORD);
ESP_LOGI(TAG, "Then open http://192.168.4.1 in your browser");
ESP_ERROR_CHECK(wifi_manager_start_ap());
}
ESP_LOGI(TAG, "System initialized and running");
while (1) {
vTaskDelay(pdMS_TO_TICKS(5000));
if (wifi_manager_config_updated()) {
ESP_LOGI(TAG, "Configuration updated, reconnecting to WiFi...");
wifi_manager_clear_config_flag();
mqtt_client_stop();
power_monitor_stop();
// Reload configuration (device ID, location, etc.)
power_monitor_reload_config();
if (nvs_has_wifi_credentials()) {
ESP_LOGI(TAG, "Connecting to WiFi with updated credentials");
wifi_manager_connect_sta();
}
}
}
}
+449
View File
@@ -0,0 +1,449 @@
#include "wifi_manager.h"
#include "config.h"
#include "esp_http_server.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_wifi.h"
#include "lwip/dns.h"
#include "nvs_storage.h"
#include "ota_manager.h"
#include <stdlib.h>
#include <string.h>
static const char *TAG = "WIFI_MANAGER";
ESP_EVENT_DEFINE_BASE(WIFI_MANAGER_EVENT);
static esp_netif_t *sta_netif = NULL;
static esp_netif_t *ap_netif = NULL;
static httpd_handle_t server = NULL;
static bool is_connected = false;
static int retry_count = 0;
static const int MAX_RETRY = 5;
static bool config_updated = false;
// Simplified HTML template without external dependencies for offline AP mode
// operation
static const char *config_page_template =
"<!DOCTYPE html>"
"<html><head><meta name='viewport' "
"content='width=device-width,initial-scale=1'>"
"<meta charset='UTF-8'>"
"<title>Power Monitor Setup</title>"
"<link rel='stylesheet' "
"href='https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'/>"
"<style>"
"body{font-family:Arial,sans-serif;background:#f0f0f0;margin:0;padding:"
"20px}"
".container{max-width:600px;margin:0 "
"auto;background:white;padding:30px;border-radius:10px;box-shadow:0 2px "
"10px rgba(0,0,0,0.1)}"
"h1{color:#333;text-align:center;margin-bottom:10px}"
".status{text-align:center;color:#666;margin-bottom:20px;font-size:14px}"
"h3{color:#555;border-bottom:2px solid "
"#4CAF50;padding-bottom:5px;margin-top:20px}"
"input,select{width:100%%;padding:12px;margin:8px 0;border:1px solid "
"#ddd;border-radius:5px;box-sizing:border-box;font-size:14px}"
"input:focus{border-color:#4CAF50;outline:none}"
"button{width:100%%;padding:14px;background:#4CAF50;color:white;border:"
"none;border-radius:5px;cursor:pointer;font-size:16px;margin-top:15px}"
"button:hover{background:#45a049}"
".info{background:#e3f2fd;padding:10px;border-radius:5px;margin:10px "
"0;font-size:13px}"
".current-value{color:#666;font-size:12px;margin-top:3px}"
".coord-group{display:flex;gap:10px;margin-top:10px}"
".coord-group>div{flex:1}"
"#map{height:300px;width:100%%;border-radius:5px;margin:15px 0;border:1px "
"solid #ddd}"
".location-hint{color:#666;font-size:12px;margin-top:10px;text-align:"
"center;background:#f8f9fa;padding:8px;border-radius:5px}"
"label{display:block;font-size:12px;color:#666;margin-bottom:4px}"
"</style></head><body>"
"<div class='container'>"
"<h1>Power Monitor</h1>"
"<div class='status'>Configuration Portal</div>"
"<div class='info'>Configure your device settings below. Changes are saved "
"immediately.</div>"
"<form action='/save' method='POST' id='configForm'>"
"<h3>WiFi Configuration</h3>"
"<input type='text' name='ssid' id='ssid' placeholder='WiFi SSID' "
"value='%s' required>"
"<input type='password' name='pass' placeholder='WiFi Password (leave "
"blank to keep current)' value=''>"
"<h3>Device Configuration</h3>"
"<input type='text' name='device_id' placeholder='Device ID' value='%s' "
"required>"
"<h3>Location</h3>"
"<div class='current-value'>Click on the map to set location, or enter "
"coordinates manually</div>"
"<div id='map'></div>"
"<div class='coord-group'>"
"<div>"
"<label>Latitude (-90 to 90)</label>"
"<input type='number' step='0.000001' name='latitude' id='latitude' "
"placeholder='0.000000' value='%.6f' min='-90' max='90'>"
"</div>"
"<div>"
"<label>Longitude (-180 to 180)</label>"
"<input type='number' step='0.000001' name='longitude' id='longitude' "
"placeholder='0.000000' value='%.6f' min='-180' max='180'>"
"</div>"
"</div>"
"<div class='location-hint'>Current: <span id='coordDisplay'>%.6f, "
"%.6f</span></div>"
"<button type='submit'>Save Configuration</button>"
"</form>"
"<div id='message' "
"style='margin-top:15px;padding:10px;border-radius:5px;display:none'></div>"
"</div>"
"<script src='https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'></script>"
"<script>"
"var lat=parseFloat(document.getElementById('latitude').value)||0;"
"var lng=parseFloat(document.getElementById('longitude').value)||0;"
"var map=L.map('map').setView([lat,lng],lat===0&&lng===0?2:13);"
"L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/"
"{y}.png',{attribution:'&copy; OpenStreetMap'}).addTo(map);"
"var marker=L.marker([lat,lng],{draggable:true}).addTo(map);"
"function updateDisplay(){"
"var lat=parseFloat(document.getElementById('latitude').value)||0;"
"var lng=parseFloat(document.getElementById('longitude').value)||0;"
"document.getElementById('coordDisplay').textContent=lat.toFixed(6)+', "
"'+lng.toFixed(6);"
"}"
"function updateCoords(latlng){"
"document.getElementById('latitude').value=latlng.lat.toFixed(6);"
"document.getElementById('longitude').value=latlng.lng.toFixed(6);"
"updateDisplay();"
"}"
"map.on('click',function(e){marker.setLatLng(e.latlng);updateCoords(e."
"latlng);});"
"marker.on('dragend',function(e){updateCoords(e.target.getLatLng());});"
"document.getElementById('latitude').addEventListener('input',function(){"
"var newLat=parseFloat(this.value)||0;"
"var newLng=parseFloat(document.getElementById('longitude').value)||0;"
"marker.setLatLng([newLat,newLng]);map.setView([newLat,newLng]);"
"updateDisplay();"
"});"
"document.getElementById('longitude').addEventListener('input',function(){"
"var newLat=parseFloat(document.getElementById('latitude').value)||0;"
"var newLng=parseFloat(this.value)||0;"
"marker.setLatLng([newLat,newLng]);map.setView([newLat,newLng]);"
"updateDisplay();"
"});"
"document.getElementById('configForm').onsubmit=function(e){"
"e.preventDefault();"
"var formData=new FormData(this);"
"var xhr=new XMLHttpRequest();"
"xhr.open('POST','/save',true);"
"xhr.onload=function(){"
"var msg=document.getElementById('message');"
"if(xhr.status===200){"
"msg.style.background='#d4edda';msg.style.color='#155724';"
"msg.innerHTML='Configuration saved successfully! Connecting to WiFi...';"
"msg.style.display='block';"
"setTimeout(function(){location.reload()},2000);"
"}else{"
"msg.style.background='#f8d7da';msg.style.color='#721c24';"
"msg.innerHTML='Failed to save configuration. Please try again.';"
"msg.style.display='block';"
"}"
"};"
"xhr.send(new URLSearchParams(formData));"
"};"
"</script></body></html>";
static void parse_form_data(char *data, char *ssid, char *pass, char *device_id,
float *latitude, float *longitude) {
char *token = strtok(data, "&");
while (token != NULL) {
char *key = token;
char *value = strchr(token, '=');
if (value != NULL) {
*value = '\0';
value++;
char decoded[256] = {0};
int j = 0;
for (int i = 0; value[i] && j < 255; i++, j++) {
if (value[i] == '+') {
decoded[j] = ' ';
} else if (value[i] == '%' && value[i + 1] && value[i + 2]) {
char hex[3] = {value[i + 1], value[i + 2], '\0'};
decoded[j] = (char)strtol(hex, NULL, 16);
i += 2;
} else {
decoded[j] = value[i];
}
}
if (strcmp(key, "ssid") == 0)
strcpy(ssid, decoded);
else if (strcmp(key, "pass") == 0)
strcpy(pass, decoded);
else if (strcmp(key, "device_id") == 0)
strcpy(device_id, decoded);
else if (strcmp(key, "latitude") == 0)
*latitude = atof(decoded);
else if (strcmp(key, "longitude") == 0)
*longitude = atof(decoded);
}
token = strtok(NULL, "&");
}
}
static esp_err_t config_get_handler(httpd_req_t *req) {
char *response = malloc(HTTP_MAX_RESP_LEN);
if (response == NULL) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory");
return ESP_FAIL;
}
char ssid[MAX_SSID_LEN] = {0};
char password[MAX_PASSWORD_LEN] = {0};
char device_id[MAX_DEVICE_ID_LEN] = {0};
float latitude = 0.0;
float longitude = 0.0;
nvs_load_wifi_credentials(ssid, sizeof(ssid), password, sizeof(password));
nvs_load_device_id(device_id, sizeof(device_id));
nvs_load_location(&latitude, &longitude);
snprintf(response, HTTP_MAX_RESP_LEN, config_page_template, ssid, device_id,
latitude, longitude, latitude, longitude);
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, response, HTTPD_RESP_USE_STRLEN);
free(response);
return ESP_OK;
}
static esp_err_t config_post_handler(httpd_req_t *req) {
char buf[1024];
int ret, remaining = req->content_len;
if (remaining >= sizeof(buf)) {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Content too long");
return ESP_FAIL;
}
ret = httpd_req_recv(req, buf, remaining);
if (ret <= 0) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Failed to receive data");
return ESP_FAIL;
}
buf[ret] = '\0';
char ssid[MAX_SSID_LEN] = {0};
char password[MAX_PASSWORD_LEN] = {0};
char device_id[MAX_DEVICE_ID_LEN] = {0};
float latitude = DEFAULT_LATITUDE;
float longitude = DEFAULT_LONGITUDE;
parse_form_data(buf, ssid, password, device_id, &latitude, &longitude);
ESP_LOGI(TAG, "Saving configuration - SSID: %s, Device ID: %s", ssid,
device_id);
if (strlen(ssid) > 0) {
if (strlen(password) > 0) {
nvs_save_wifi_credentials(ssid, password);
} else {
char old_password[MAX_PASSWORD_LEN] = {0};
nvs_load_wifi_credentials(ssid, sizeof(ssid), old_password,
sizeof(old_password));
nvs_save_wifi_credentials(ssid, old_password);
}
}
nvs_save_device_id(device_id);
nvs_save_location(latitude, longitude);
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req,
"{\"status\":\"success\",\"message\":\"Configuration saved, "
"connecting to WiFi...\"}",
HTTPD_RESP_USE_STRLEN);
config_updated = true;
return ESP_OK;
}
// Single consolidated webserver initialization function
// Uses direct initialization - no need for separate task as httpd library
// handles threading internally
static esp_err_t start_webserver(void) {
if (server != NULL) {
ESP_LOGI(TAG, "Web server already running");
return ESP_OK;
}
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = HTTP_SERVER_PORT;
config.max_uri_handlers = 8;
config.stack_size = 8192; // Increase stack size for larger HTML responses
if (httpd_start(&server, &config) == ESP_OK) {
httpd_uri_t config_get = {
.uri = "/", .method = HTTP_GET, .handler = config_get_handler};
httpd_uri_t config_post = {
.uri = "/save", .method = HTTP_POST, .handler = config_post_handler};
httpd_register_uri_handler(server, &config_get);
httpd_register_uri_handler(server, &config_post);
ESP_LOGI(TAG, "Web server started successfully");
return ESP_OK;
}
ESP_LOGE(TAG, "Failed to start web server");
return ESP_FAIL;
}
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, "Station started, connecting...");
esp_wifi_connect();
break;
case WIFI_EVENT_STA_DISCONNECTED:
ESP_LOGW(TAG, "Disconnected from AP");
is_connected = false;
if (retry_count < MAX_RETRY) {
ESP_LOGI(TAG, "Retrying connection... (%d/%d)", retry_count + 1,
MAX_RETRY);
vTaskDelay(pdMS_TO_TICKS(WIFI_RECONNECT_DELAY_MS));
esp_wifi_connect();
retry_count++;
} else {
ESP_LOGE(TAG, "Max retries reached");
}
esp_event_post(WIFI_MANAGER_EVENT, WIFI_MANAGER_EVENT_STA_DISCONNECTED,
NULL, 0, portMAX_DELAY);
break;
case WIFI_EVENT_AP_START:
ESP_LOGI(TAG, "Access Point started");
esp_event_post(WIFI_MANAGER_EVENT, WIFI_MANAGER_EVENT_AP_STARTED, NULL, 0,
portMAX_DELAY);
break;
default:
break;
}
} else if (event_base == IP_EVENT) {
if (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));
ESP_LOGI(TAG, "Configuration portal accessible at: http://" IPSTR,
IP2STR(&event->ip_info.ip));
retry_count = 0;
is_connected = true;
if (server == NULL) {
start_webserver();
}
esp_event_post(WIFI_MANAGER_EVENT, WIFI_MANAGER_EVENT_STA_CONNECTED, NULL,
0, portMAX_DELAY);
}
}
}
esp_err_t wifi_manager_init(void) {
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
sta_netif = esp_netif_create_default_wifi_sta();
ap_netif = esp_netif_create_default_wifi_ap();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
&wifi_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
&wifi_event_handler, NULL));
return ESP_OK;
}
esp_err_t wifi_manager_start_ap(void) {
wifi_config_t wifi_config = {
.ap = {.ssid = WIFI_AP_SSID,
.password = WIFI_AP_PASSWORD,
.ssid_len = strlen(WIFI_AP_SSID),
.channel = WIFI_AP_CHANNEL,
.max_connection = WIFI_AP_MAX_CONNECTIONS,
.authmode = WIFI_AUTH_WPA2_PSK},
};
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());
start_webserver();
ESP_LOGI(TAG, "AP started - SSID: %s", WIFI_AP_SSID);
return ESP_OK;
}
esp_err_t wifi_manager_connect_sta(void) {
char ssid[MAX_SSID_LEN] = {0};
char password[MAX_PASSWORD_LEN] = {0};
esp_err_t ret =
nvs_load_wifi_credentials(ssid, sizeof(ssid), password, sizeof(password));
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to load WiFi credentials");
return ret;
}
return wifi_manager_connect(ssid, password);
}
esp_err_t wifi_manager_connect(const char *ssid, const char *password) {
wifi_config_t wifi_config = {0};
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_wifi_stop();
vTaskDelay(pdMS_TO_TICKS(100));
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, "Connecting to SSID: %s", ssid);
return ESP_OK;
}
bool wifi_manager_is_connected(void) { return is_connected; }
bool wifi_manager_config_updated(void) { return config_updated; }
void wifi_manager_clear_config_flag(void) { config_updated = false; }
esp_err_t wifi_manager_stop(void) {
if (server) {
httpd_stop(server);
server = NULL;
}
esp_wifi_stop();
return ESP_OK;
}
+41
View File
@@ -0,0 +1,41 @@
#ifndef WIFI_MANAGER_H
#define WIFI_MANAGER_H
#include "esp_err.h"
#include "esp_event.h"
#include <stdbool.h>
/* WiFi events */
ESP_EVENT_DECLARE_BASE(WIFI_MANAGER_EVENT);
typedef enum {
WIFI_MANAGER_EVENT_STA_CONNECTED,
WIFI_MANAGER_EVENT_STA_DISCONNECTED,
WIFI_MANAGER_EVENT_AP_STARTED,
} wifi_manager_event_t;
/* Initialize WiFi manager */
esp_err_t wifi_manager_init(void);
/* Start AP mode for configuration */
esp_err_t wifi_manager_start_ap(void);
/* Connect to saved WiFi credentials */
esp_err_t wifi_manager_connect_sta(void);
/* Connect to specific WiFi network */
esp_err_t wifi_manager_connect(const char *ssid, const char *password);
/* Check if connected */
bool wifi_manager_is_connected(void);
/* Check if configuration was updated */
bool wifi_manager_config_updated(void);
/* Clear config updated flag */
void wifi_manager_clear_config_flag(void);
/* Stop WiFi */
esp_err_t wifi_manager_stop(void);
#endif // WIFI_MANAGER_H
+6
View File
@@ -0,0 +1,6 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000,
ota_0, app, ota_0, 0x10000, 0xF0000,
ota_1, app, ota_1, 0x100000, 0xF0000,
1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x4000
3 otadata data ota 0xd000 0x2000
4 phy_init data phy 0xf000 0x1000
5 ota_0 app ota_0 0x10000 0xF0000
6 ota_1 app ota_1 0x100000 0xF0000
+913 -26
View File
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
# Partition Table Configuration
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_OFFSET=0x8000
# Flash size
CONFIG_ESPTOOLPY_FLASHSIZE_2MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="2MB"
# OTA Configuration
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
CONFIG_BOOTLOADER_ROLLBACK_ENABLE=y
# Application Manager
CONFIG_APP_COMPILE_TIME_DATE=y
CONFIG_APP_RETRIEVE_LEN_ELF_SHA=16
# Log levels
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
CONFIG_LOG_MAXIMUM_LEVEL_INFO=y
# FreeRTOS
CONFIG_FREERTOS_HZ=100
CONFIG_FREERTOS_USE_TRACE_FACILITY=n
CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=n
# ESP32C3 specific
CONFIG_ESP32C3_DEFAULT_CPU_FREQ_160=y
# Wi-Fi
CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=10
CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=32
CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=32
# LWIP
CONFIG_LWIP_MAX_SOCKETS=10
CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=4096
# System Event Task
CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096
# HTTP Server
CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024
CONFIG_HTTPD_MAX_URI_LEN=512
CONFIG_HTTPD_STACK_SIZE=6144
# MQTT with TLS/SSL Support
CONFIG_MQTT_PROTOCOL_311=y
CONFIG_MQTT_TRANSPORT_SSL=y
CONFIG_MQTT_TRANSPORT_WEBSOCKET=n
CONFIG_MQTT_USE_CUSTOM_CONFIG=n
# mbedTLS for MQTT TLS
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y
CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y
CONFIG_MBEDTLS_KEY_EXCHANGE_RSA=y
CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_RSA=y
CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA=y
+1985
View File
File diff suppressed because it is too large Load Diff