Files
2026-04-27 16:09:03 +08:00

364 lines
12 KiB
C

/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "app_claw.h"
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include "wifi_manager.h"
#include "wear_levelling.h"
#include "time.h"
#include "nvs_flash.h"
#include "http_server.h"
#include "esp_vfs_fat.h"
#include "esp_log.h"
#include "esp_err.h"
#include "esp_board_manager_includes.h"
#include "captive_dns.h"
#if CONFIG_APP_CLAW_CAP_IM_WECHAT
#include "cap_im_wechat.h"
#endif
#include "app_config.h"
#define APP_FATFS_PARTITION_LABEL "storage"
#define APP_ENABLE_MEM_LOG (0)
static const char *TAG = "app";
static app_config_t *s_config;
static app_claw_config_t *s_claw_config;
static app_claw_storage_paths_t *s_claw_paths;
static const char *app_fatfs_base_path = "/fatfs";
static wl_handle_t s_wl_handle = WL_INVALID_HANDLE;
static esp_err_t app_allocate_runtime_state(void)
{
if (!s_config) {
s_config = calloc(1, sizeof(*s_config));
}
if (!s_claw_config) {
s_claw_config = calloc(1, sizeof(*s_claw_config));
}
if (!s_claw_paths) {
s_claw_paths = calloc(1, sizeof(*s_claw_paths));
}
if (!s_config || !s_claw_config || !s_claw_paths) {
return ESP_ERR_NO_MEM;
}
return ESP_OK;
}
static void app_free_runtime_state(void)
{
free(s_claw_paths);
s_claw_paths = NULL;
free(s_claw_config);
s_claw_config = NULL;
free(s_config);
s_config = NULL;
}
static void on_wifi_state_changed(bool connected, void *user_ctx)
{
(void)user_ctx;
wifi_manager_status_t status = {0};
wifi_manager_get_status(&status);
const char *ap_ssid = status.ap_active ? status.ap_ssid : NULL;
ESP_LOGI(TAG, "Wi-Fi state: sta_connected=%d ap_active=%d mode=%s ap_ssid=%s",
connected,
status.ap_active,
status.mode ? status.mode : "off",
ap_ssid ? ap_ssid : "(none)");
esp_err_t err = app_claw_set_network_status(connected, ap_ssid);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to update network emote: %s", esp_err_to_name(err));
}
}
static esp_err_t app_claw_init_storage_paths(app_claw_storage_paths_t *paths)
{
if (!paths || !app_fatfs_base_path || app_fatfs_base_path[0] != '/') {
return ESP_ERR_INVALID_ARG;
}
memset(paths, 0, sizeof(*paths));
if (strlcpy(paths->fatfs_base_path, app_fatfs_base_path, sizeof(paths->fatfs_base_path)) >= sizeof(paths->fatfs_base_path) ||
snprintf(paths->memory_session_root, sizeof(paths->memory_session_root), "%s/sessions", app_fatfs_base_path) >= sizeof(paths->memory_session_root) ||
snprintf(paths->memory_root_dir, sizeof(paths->memory_root_dir), "%s/memory", app_fatfs_base_path) >= sizeof(paths->memory_root_dir) ||
snprintf(paths->skills_root_dir, sizeof(paths->skills_root_dir), "%s/skills", app_fatfs_base_path) >= sizeof(paths->skills_root_dir) ||
snprintf(paths->lua_root_dir, sizeof(paths->lua_root_dir), "%s/scripts", app_fatfs_base_path) >= sizeof(paths->lua_root_dir) ||
snprintf(paths->router_rules_path, sizeof(paths->router_rules_path), "%s/router_rules/router_rules.json", app_fatfs_base_path) >= sizeof(paths->router_rules_path) ||
snprintf(paths->scheduler_rules_path, sizeof(paths->scheduler_rules_path), "%s/scheduler/schedules.json", app_fatfs_base_path) >= sizeof(paths->scheduler_rules_path) ||
snprintf(paths->im_attachment_root, sizeof(paths->im_attachment_root), "%s/inbox", app_fatfs_base_path) >= sizeof(paths->im_attachment_root)) {
return ESP_ERR_INVALID_SIZE;
}
return ESP_OK;
}
static esp_err_t main_load_config(app_config_t *config)
{
return app_config_load(config);
}
static esp_err_t main_save_config(const app_config_t *config)
{
return app_config_save(config);
}
static esp_err_t main_get_wifi_status(http_server_wifi_status_t *status)
{
if (!status) {
return ESP_ERR_INVALID_ARG;
}
wifi_manager_status_t wifi_status = {0};
wifi_manager_get_status(&wifi_status);
status->wifi_connected = wifi_status.sta_connected;
status->ip = wifi_status.sta_ip;
status->ap_active = wifi_status.ap_active;
status->ap_ssid = wifi_status.ap_ssid;
status->ap_ip = wifi_status.ap_ip;
status->wifi_mode = wifi_status.mode;
return ESP_OK;
}
#if CONFIG_APP_CLAW_CAP_IM_WECHAT
static esp_err_t main_wechat_login_start(const char *account_id, bool force)
{
return cap_im_wechat_qr_login_start(account_id, force);
}
static esp_err_t main_wechat_login_get_status(http_server_wechat_login_status_t *status)
{
if (!status) {
return ESP_ERR_INVALID_ARG;
}
cap_im_wechat_qr_login_status_t raw = {0};
esp_err_t err = cap_im_wechat_qr_login_get_status(&raw);
if (err != ESP_OK) {
return err;
}
memset(status, 0, sizeof(*status));
status->active = raw.active;
status->configured = raw.configured;
status->completed = raw.completed;
status->persisted = raw.persisted;
strlcpy(status->session_key, raw.session_key, sizeof(status->session_key));
strlcpy(status->status, raw.status, sizeof(status->status));
strlcpy(status->message, raw.message, sizeof(status->message));
strlcpy(status->qr_data_url, raw.qr_data_url, sizeof(status->qr_data_url));
strlcpy(status->account_id, raw.account_id, sizeof(status->account_id));
strlcpy(status->user_id, raw.user_id, sizeof(status->user_id));
strlcpy(status->token, raw.token, sizeof(status->token));
strlcpy(status->base_url, raw.base_url, sizeof(status->base_url));
return ESP_OK;
}
static esp_err_t main_wechat_login_cancel(void)
{
return cap_im_wechat_qr_login_cancel();
}
static esp_err_t main_wechat_login_mark_persisted(void)
{
return cap_im_wechat_qr_login_mark_persisted();
}
#endif
static esp_err_t init_nvs(void)
{
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
return err;
}
static esp_err_t init_fatfs(void)
{
esp_vfs_fat_mount_config_t mount_config = {
.format_if_mount_failed = true,
.max_files = 8,
.allocation_unit_size = 4096,
.disk_status_check_enable = false,
.use_one_fat = false,
};
uint64_t total = 0;
uint64_t free_bytes = 0;
esp_err_t err;
err = esp_vfs_fat_spiflash_mount_rw_wl(app_fatfs_base_path,
APP_FATFS_PARTITION_LABEL,
&mount_config,
&s_wl_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to mount FATFS: %s", esp_err_to_name(err));
return err;
}
err = esp_vfs_fat_info(app_fatfs_base_path, &total, &free_bytes);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to query FATFS info: %s", esp_err_to_name(err));
} else {
ESP_LOGI(TAG, "FATFS mounted total=%u used=%u",
(unsigned int)total,
(unsigned int)(total - free_bytes));
}
return ESP_OK;
}
static esp_err_t init_timezone(const char *timezone)
{
esp_err_t err = ESP_OK;
if (!timezone || timezone[0] == '\0') {
ESP_LOGE(TAG, "Timezone is empty.");
err = ESP_ERR_INVALID_ARG;
goto tz_default;
}
if (setenv("TZ", timezone, 1) != 0) {
ESP_LOGE(TAG, "Failed to set TZ env");
err = ESP_FAIL;
goto tz_default;
}
tzset();
ESP_LOGI(TAG, "Timezone set to %s", timezone);
return ESP_OK;
tz_default:
assert(setenv("TZ", "CST-8", 1) == 0);
tzset();
ESP_LOGI(TAG, "Timezone set to default: CST-8");
return err;
}
#if APP_ENABLE_MEM_LOG
static void print_task_stack_info(void)
{
#ifdef CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS
static TaskStatus_t s_task_status_snapshot[24];
UBaseType_t count = uxTaskGetSystemState(s_task_status_snapshot,
sizeof(s_task_status_snapshot) / sizeof(s_task_status_snapshot[0]),
NULL);
for (UBaseType_t i = 0; i < count; i++) {
ESP_LOGI(TAG,
"Task %s %u",
s_task_status_snapshot[i].pcTaskName,
s_task_status_snapshot[i].usStackHighWaterMark);
}
#endif
}
/* Periodic task: print internal free, minimum free, and PSRAM free every 20s */
static void memory_monitor_task(void *arg)
{
(void)arg;
while (1) {
vTaskDelay(pdMS_TO_TICKS(5000));
size_t internal_free = heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
size_t internal_min = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL);
size_t psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
ESP_LOGI(TAG, "Memory: internal_free=%u bytes, internal_min_free=%u bytes, psram_free=%u bytes",
(unsigned)internal_free, (unsigned)internal_min, (unsigned)psram_free);
print_task_stack_info();
}
}
#endif
void app_main(void)
{
esp_log_level_set("esp-x509-crt-bundle", ESP_LOG_WARN);
ESP_LOGI(TAG, "Starting app");
ESP_ERROR_CHECK(app_allocate_runtime_state());
ESP_ERROR_CHECK(init_nvs());
ESP_ERROR_CHECK(app_config_init());
ESP_ERROR_CHECK(app_config_load(s_config));
app_config_to_claw(s_config, s_claw_config);
init_timezone(app_config_get_timezone(s_config)); // no need to check error
ESP_ERROR_CHECK(esp_board_manager_init());
ESP_ERROR_CHECK(app_claw_ui_start());
ESP_ERROR_CHECK(init_fatfs());
ESP_ERROR_CHECK(wifi_manager_init());
ESP_ERROR_CHECK(http_server_init(&(http_server_config_t) {
.storage_base_path = app_fatfs_base_path,
.services = {
.load_config = main_load_config,
.save_config = main_save_config,
.get_wifi_status = main_get_wifi_status,
#if CONFIG_APP_CLAW_CAP_IM_WECHAT
.wechat_login_start = main_wechat_login_start,
.wechat_login_get_status = main_wechat_login_get_status,
.wechat_login_cancel = main_wechat_login_cancel,
.wechat_login_mark_persisted = main_wechat_login_mark_persisted,
#endif
},
}));
ESP_ERROR_CHECK(wifi_manager_register_state_callback(on_wifi_state_changed, NULL));
esp_err_t wifi_err = wifi_manager_start(&(wifi_manager_config_t) {
.sta_ssid = s_config->wifi_ssid,
.sta_password = s_config->wifi_password,
});
if (wifi_err != ESP_OK) {
ESP_LOGE(TAG, "Wi-Fi start failed: %s", esp_err_to_name(wifi_err));
} else {
ESP_ERROR_CHECK(http_server_start());
if (captive_dns_start(&(captive_dns_config_t) {
.ap_netif = wifi_manager_get_ap_netif(),
.configure_dhcp_dns = true,
}) != ESP_OK) {
ESP_LOGW(TAG, "Captive DNS could not start, portal pop-up disabled");
}
if (s_config->wifi_ssid[0] != '\0') {
if (wifi_manager_wait_connected(30000) == ESP_OK) {
wifi_manager_status_t status = {0};
wifi_manager_get_status(&status);
ESP_LOGI(TAG, "Wi-Fi STA ready: %s", status.sta_ip);
} else {
ESP_LOGW(TAG, "STA could not connect, dropped to AP fallback");
}
}
wifi_manager_status_t status = {0};
wifi_manager_get_status(&status);
ESP_LOGW(TAG,
"*** Provisioning portal: SSID=\"%s\" (open) IP=%s URL=http://%s/ ***",
status.ap_ssid,
status.ap_ip,
status.ap_ip);
}
ESP_ERROR_CHECK(app_claw_init_storage_paths(s_claw_paths));
ESP_ERROR_CHECK(app_claw_start(s_claw_config, s_claw_paths));
#if APP_ENABLE_MEM_LOG
/* Start memory monitor: print internal free, min free, PSRAM free every 20s */
xTaskCreate(memory_monitor_task, "mem_mon", 4096, NULL, 1, NULL);
#endif
app_free_runtime_state();
}