diff --git a/claw/core/gateway.c b/claw/core/gateway.c index ba450ff..4457e19 100644 --- a/claw/core/gateway.c +++ b/claw/core/gateway.c @@ -38,6 +38,10 @@ int gateway_register_service(const char *name, uint8_t type_mask, return CLAW_OK; } +/* --- Statistics --- */ + +static struct gateway_stats s_stats; + /* --- Message dispatch --- */ static void dispatch_msg(struct gateway_msg *msg) @@ -45,12 +49,18 @@ static void dispatch_msg(struct gateway_msg *msg) uint8_t bit = (uint8_t)(1 << msg->type); int delivered = 0; + s_stats.total++; + if (msg->type < GW_MSG_TYPE_MAX) { + s_stats.per_type[msg->type]++; + } + for (int i = 0; i < s_service_count; i++) { if (s_services[i].type_mask & bit) { if (claw_mq_send(s_services[i].inbox, msg, sizeof(*msg), CLAW_NO_WAIT) == CLAW_OK) { delivered++; } else { + s_stats.dropped++; CLAW_LOGW(TAG, "drop msg type=%d -> %s (queue full)", msg->type, s_services[i].name); } @@ -58,6 +68,7 @@ static void dispatch_msg(struct gateway_msg *msg) } if (delivered == 0) { + s_stats.no_consumer++; CLAW_LOGD(TAG, "msg type=%d len=%d (no consumer)", msg->type, msg->len); } @@ -85,6 +96,7 @@ static void gateway_thread_entry(void *param) int gateway_init(void) { memset(s_services, 0, sizeof(s_services)); + memset(&s_stats, 0, sizeof(s_stats)); s_service_count = 0; gw_mq = claw_mq_create("gw_mq", sizeof(struct gateway_msg), @@ -114,3 +126,10 @@ int gateway_send(struct gateway_msg *msg) { return claw_mq_send(gw_mq, msg, sizeof(*msg), CLAW_NO_WAIT); } + +void gateway_get_stats(struct gateway_stats *out) +{ + if (out) { + memcpy(out, &s_stats, sizeof(s_stats)); + } +} diff --git a/claw/core/heartbeat.c b/claw/core/heartbeat.c index e60c63f..9fcb690 100644 --- a/claw/core/heartbeat.c +++ b/claw/core/heartbeat.c @@ -20,6 +20,12 @@ #include #include +#ifdef CLAW_PLATFORM_ESP_IDF +#include "esp_heap_caps.h" +#elif defined(CLAW_PLATFORM_RTTHREAD) +#include +#endif + #define TAG "heartbeat" /* LLM connectivity state: -1 = unknown, 0 = offline, 1 = online */ @@ -129,10 +135,53 @@ static void heartbeat_ai_thread(void *arg) claw_mutex_unlock(s_lock); } +/* + * Device health check — collect anomalies and post events. + * Runs before the LLM ping so anomalies are included in the + * next heartbeat AI summary. + */ +static void check_device_health(void) +{ + uint32_t free_kb = 0; + uint32_t total_kb = 0; + +#ifdef CLAW_PLATFORM_ESP_IDF + size_t free_sz = heap_caps_get_free_size(MALLOC_CAP_DEFAULT); + size_t total_sz = heap_caps_get_total_size(MALLOC_CAP_DEFAULT); + free_kb = (uint32_t)(free_sz / 1024); + total_kb = (uint32_t)(total_sz / 1024); +#elif defined(CLAW_PLATFORM_RTTHREAD) + rt_size_t total_rt = 0, used_rt = 0, max_rt = 0; + rt_memory_info(&total_rt, &used_rt, &max_rt); + free_kb = (uint32_t)((total_rt - used_rt) / 1024); + total_kb = (uint32_t)(total_rt / 1024); +#endif + + if (total_kb == 0) { + return; + } + + uint32_t used_pct = 100 - (free_kb * 100 / total_kb); + + /* Alert when heap usage exceeds 80% */ + if (used_pct > 80) { + char msg[80]; + snprintf(msg, sizeof(msg), + "heap %lu%% used (%luKB free / %luKB total)", + (unsigned long)used_pct, + (unsigned long)free_kb, + (unsigned long)total_kb); + heartbeat_post("memory", msg); + } +} + /* LLM connectivity probe — runs in its own thread */ static void ping_thread(void *arg) { (void)arg; + + check_device_health(); + int ok = (ai_ping() == CLAW_OK) ? 1 : 0; int prev = s_llm_state; s_llm_state = ok; diff --git a/claw/shell/shell_commands.c b/claw/shell/shell_commands.c index 295a2d4..564e2b7 100644 --- a/claw/shell/shell_commands.c +++ b/claw/shell/shell_commands.c @@ -92,6 +92,17 @@ void shell_nvs_config_load(void) /* ---- Common command handlers ---- */ +static const char *log_level_name(int level) +{ + switch (level) { + case CLAW_LOG_ERROR: return "error"; + case CLAW_LOG_WARN: return "warn"; + case CLAW_LOG_INFO: return "info"; + case CLAW_LOG_DEBUG: return "debug"; + default: return "unknown"; + } +} + static void cmd_log(int argc, char **argv) { if (argc < 2) { @@ -100,8 +111,31 @@ static void cmd_log(int argc, char **argv) claw_log_set_enabled(1); } else if (strcmp(argv[1], "off") == 0) { claw_log_set_enabled(0); + } else if (strcmp(argv[1], "level") == 0) { + if (argc < 3) { + printf("Log level: %s\n", + log_level_name(claw_log_get_level())); + return; + } + int lv = -1; + if (strcmp(argv[2], "error") == 0) { + lv = CLAW_LOG_ERROR; + } else if (strcmp(argv[2], "warn") == 0) { + lv = CLAW_LOG_WARN; + } else if (strcmp(argv[2], "info") == 0) { + lv = CLAW_LOG_INFO; + } else if (strcmp(argv[2], "debug") == 0) { + lv = CLAW_LOG_DEBUG; + } + if (lv < 0) { + printf("Usage: /log level \n"); + return; + } + claw_log_set_level(lv); + printf("Log level: %s\n", log_level_name(lv)); + return; } else { - printf("Usage: /log [on|off]\n"); + printf("Usage: /log [on|off|level ]\n"); return; } printf("Log output: %s\n", @@ -305,7 +339,7 @@ static void cmd_nodes(int argc, char **argv) /* ---- Common command table ---- */ const shell_cmd_t shell_common_commands[] = { - SHELL_CMD("/log", cmd_log, "Toggle log [on|off]"), + SHELL_CMD("/log", cmd_log, "Log [on|off|level ]"), SHELL_CMD("/history", cmd_history, "Show conversation count"), SHELL_CMD("/clear", cmd_clear, "Clear conversation memory"), SHELL_CMD("/ai_set", cmd_ai_set, "Set AI config (NVS)"), diff --git a/claw/tools/tool_system.c b/claw/tools/tool_system.c index b492d88..42d2008 100644 --- a/claw/tools/tool_system.c +++ b/claw/tools/tool_system.c @@ -177,14 +177,22 @@ static int tool_memory_info(const cJSON *params, cJSON *result) size_t total = heap_caps_get_total_size(MALLOC_CAP_DEFAULT); size_t free_sz = heap_caps_get_free_size(MALLOC_CAP_DEFAULT); size_t min_free = heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT); + size_t largest = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT); cJSON_AddStringToObject(result, "status", "ok"); cJSON_AddNumberToObject(result, "heap_total_bytes", total); cJSON_AddNumberToObject(result, "heap_free_bytes", free_sz); cJSON_AddNumberToObject(result, "heap_min_free_bytes", min_free); + cJSON_AddNumberToObject(result, "heap_largest_block", largest); cJSON_AddNumberToObject(result, "heap_used_percent", (double)(total - free_sz) / total * 100.0); + /* fragmentation: 100% = all fragments, 0% = single block */ + if (free_sz > 0) { + double frag = (1.0 - (double)largest / free_sz) * 100.0; + cJSON_AddNumberToObject(result, "fragmentation_percent", frag); + } + return CLAW_OK; } @@ -231,8 +239,8 @@ void claw_tools_register_system(void) 0, CLAW_TOOL_LOCAL_ONLY); claw_tool_register("memory_info", - "Get heap memory status: total, free, minimum-ever-free bytes, " - "and usage percentage.", + "Get heap memory status: total, free, minimum-ever-free, " + "largest contiguous block, usage and fragmentation percent.", schema_empty, tool_memory_info, 0, CLAW_TOOL_LOCAL_ONLY); diff --git a/docs/en/architecture.md b/docs/en/architecture.md index 6ff4f6a..2f6586f 100644 --- a/docs/en/architecture.md +++ b/docs/en/architecture.md @@ -65,7 +65,8 @@ Message routing hub with service registry and type-based dispatch. Services register with a type_mask bitmap and their own message queue; the gateway delivers incoming messages to all matching consumers. Message types: DATA, CMD, EVENT, SWARM, AI_REQ. Queue: 16 messages x 256 bytes. Dedicated thread -at priority 15. +at priority 15. Built-in dispatch statistics track total/per-type/dropped/ +no-consumer message counts via `gateway_get_stats()`. ### Scheduler (`claw/core/scheduler.c`) @@ -78,9 +79,12 @@ executed in turn, preventing task starvation. ### Heartbeat (`claw/core/heartbeat.c`) -Optional periodic AI check-in every 5 minutes. Sends a heartbeat prompt -to the AI engine so the assistant can perform background monitoring. -Depends on the scheduler service for timing. +Optional periodic AI check-in every 5 minutes. Three-layer tick logic: +(1) events pending -- AI summary via `ai_chat_raw()`; +(2) no events -- device health check (heap > 80% triggers auto-alert) + followed by lightweight LLM connectivity ping (`ai_ping()`, + max_tokens=1); (3) state change (online/offline) -- notification +delivered to IM or console. Depends on the scheduler service for timing. ### AI Engine (`claw/services/ai/`) diff --git a/docs/en/usage.md b/docs/en/usage.md index f58ef92..f4de92d 100644 --- a/docs/en/usage.md +++ b/docs/en/usage.md @@ -28,6 +28,7 @@ Commands start with `/`. | `/skill [args]` | Execute skill | | `/nodes` | Show swarm nodes | | `/log [on\|off]` | Toggle log output | +| `/log level ` | Set log level (error/warn/info/debug) | | `/history` | Show message count | | `/clear` | Clear conversation | | `/help` | List commands | @@ -73,7 +74,7 @@ is always enabled). | Tool | Parameters | Description | |------|------------|-------------| | `system_info` | — | Show system information | -| `memory_info` | — | Show heap usage | +| `memory_info` | — | Show heap usage, largest block, fragmentation | | `clear_history` | — | Clear conversation history | | `system_restart` | — | Reboot the device | | `save_memory` | `key`, `value` | Save to long-term memory | diff --git a/docs/zh/architecture.md b/docs/zh/architecture.md index f16a307..c82dcf8 100644 --- a/docs/zh/architecture.md +++ b/docs/zh/architecture.md @@ -65,6 +65,7 @@ claw/*.c ---> #include "osal/claw_os.h" 和自有消息队列注册;网关将传入消息投递到所有匹配的消费者。 消息类型:DATA、CMD、EVENT、SWARM、AI_REQ。 队列容量:16 条消息 x 256 字节。专用线程优先级为 15。 +内置消息统计:通过 `gateway_get_stats()` 查询总计/按类型/丢弃/无消费者计数。 ### 调度器(`claw/core/scheduler.c`) @@ -75,8 +76,12 @@ claw/*.c ---> #include "osal/claw_os.h" ### 心跳(`claw/core/heartbeat.c`) -可选的周期性 AI 签到功能,每 5 分钟执行一次。向 AI 引擎发送心跳提示, -使助手能够执行后台监控。依赖调度器服务进行计时。 +可选的周期性 AI 签到功能,每 5 分钟执行一次。三层 tick 逻辑: +(1)有待处理事件——通过 `ai_chat_raw()` 进行 AI 总结; +(2)无事件——设备健康检查(堆使用 > 80% 触发自动告警), +随后进行轻量级 LLM 连通性探测(`ai_ping()`,max_tokens=1); +(3)状态变化(在线/离线)——通知投递到 IM 或控制台。 +依赖调度器服务进行计时。 ### AI 引擎(`claw/services/ai/`) diff --git a/docs/zh/usage.md b/docs/zh/usage.md index bbbbd31..b095219 100644 --- a/docs/zh/usage.md +++ b/docs/zh/usage.md @@ -28,6 +28,7 @@ | `/skill [args]` | 执行技能 | | `/nodes` | 显示集群节点 | | `/log [on\|off]` | 切换日志输出 | +| `/log level ` | 设置日志级别(error/warn/info/debug) | | `/history` | 显示消息数量 | | `/clear` | 清空对话 | | `/help` | 列出命令 | @@ -72,7 +73,7 @@ Kconfig 选项控制(Audio 除外,始终启用)。 | 工具 | 参数 | 说明 | |------|------|------| | `system_info` | — | 显示系统信息 | -| `memory_info` | — | 显示堆内存使用情况 | +| `memory_info` | — | 显示堆内存使用、最大连续块、碎片率 | | `clear_history` | — | 清空对话历史 | | `system_restart` | — | 重启设备 | | `save_memory` | `key`, `value` | 保存到长期记忆 | diff --git a/include/claw/core/gateway.h b/include/claw/core/gateway.h index c6341b6..ff0eb0a 100644 --- a/include/claw/core/gateway.h +++ b/include/claw/core/gateway.h @@ -49,8 +49,17 @@ struct gw_service_entry { claw_mq_t inbox; /* service's own message queue */ }; +/* Message statistics */ +struct gateway_stats { + uint32_t total; /* total dispatched */ + uint32_t per_type[GW_MSG_TYPE_MAX]; /* per message type */ + uint32_t dropped; /* queue-full drops */ + uint32_t no_consumer; /* msgs with no match */ +}; + int gateway_init(void); int gateway_send(struct gateway_msg *msg); +void gateway_get_stats(struct gateway_stats *out); /** * Register a service to receive messages matching type_mask. diff --git a/include/osal/claw_os.h b/include/osal/claw_os.h index b9e4c39..5da5d0d 100644 --- a/include/osal/claw_os.h +++ b/include/osal/claw_os.h @@ -104,6 +104,8 @@ void claw_log(int level, const char *tag, const char *fmt, ...); void claw_log_raw(const char *fmt, ...); void claw_log_set_enabled(int enabled); int claw_log_get_enabled(void); +void claw_log_set_level(int level); +int claw_log_get_level(void); /* Convenience macros */ #define CLAW_LOGE(tag, fmt, ...) claw_log(CLAW_LOG_ERROR, tag, fmt, ##__VA_ARGS__) diff --git a/osal/freertos/claw_os_freertos.c b/osal/freertos/claw_os_freertos.c index da4ee7e..65f1596 100644 --- a/osal/freertos/claw_os_freertos.c +++ b/osal/freertos/claw_os_freertos.c @@ -267,6 +267,7 @@ void claw_free(void *ptr) /* ---------- Log ---------- */ static int s_log_enabled = 1; +static int s_log_level = CLAW_LOG_DEBUG; /* show all by default */ void claw_log_set_enabled(int enabled) { @@ -285,11 +286,33 @@ int claw_log_get_enabled(void) return s_log_enabled; } +void claw_log_set_level(int level) +{ + if (level < CLAW_LOG_ERROR) { + level = CLAW_LOG_ERROR; + } + if (level > CLAW_LOG_DEBUG) { + level = CLAW_LOG_DEBUG; + } + s_log_level = level; +#ifdef CLAW_PLATFORM_ESP_IDF + static const esp_log_level_t map[] = { + ESP_LOG_ERROR, ESP_LOG_WARN, ESP_LOG_INFO, ESP_LOG_DEBUG + }; + esp_log_level_set("*", map[level]); +#endif +} + +int claw_log_get_level(void) +{ + return s_log_level; +} + #ifdef CLAW_PLATFORM_ESP_IDF void claw_log(int level, const char *tag, const char *fmt, ...) { - if (!s_log_enabled) { + if (!s_log_enabled || level > s_log_level) { return; } @@ -346,7 +369,7 @@ void claw_log(int level, const char *tag, const char *fmt, ...) { va_list ap; - if (!s_log_enabled) { + if (!s_log_enabled || level > s_log_level) { return; } if (level < 0) { diff --git a/osal/rtthread/claw_os_rtthread.c b/osal/rtthread/claw_os_rtthread.c index b5acf57..e404e30 100644 --- a/osal/rtthread/claw_os_rtthread.c +++ b/osal/rtthread/claw_os_rtthread.c @@ -203,6 +203,7 @@ void claw_free(void *ptr) /* ---------- Log ---------- */ static int s_log_enabled = 1; +static int s_log_level = CLAW_LOG_DEBUG; /* show all by default */ static const char *level_str[] = { "E", "W", "I", "D" }; void claw_log_set_enabled(int enabled) @@ -215,11 +216,27 @@ int claw_log_get_enabled(void) return s_log_enabled; } +void claw_log_set_level(int level) +{ + if (level < CLAW_LOG_ERROR) { + level = CLAW_LOG_ERROR; + } + if (level > CLAW_LOG_DEBUG) { + level = CLAW_LOG_DEBUG; + } + s_log_level = level; +} + +int claw_log_get_level(void) +{ + return s_log_level; +} + void claw_log(int level, const char *tag, const char *fmt, ...) { va_list ap; - if (!s_log_enabled) { + if (!s_log_enabled || level > s_log_level) { return; } if (level < 0) {