mirror of
https://github.com/m5stack/ESP-Claw.git
synced 2026-05-20 11:51:49 -07:00
364 lines
12 KiB
C
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();
|
|
}
|