chore: migrate project into clean repository

This commit is contained in:
yuuux
2026-08-13 16:50:52 +08:00
commit d1d25a09e7
27405 changed files with 9422808 additions and 0 deletions

14
src/CMakeLists.txt Normal file
View File

@@ -0,0 +1,14 @@
# =============================================================================
# Voice Assistant Application Build Configuration
# =============================================================================
listenai_library_named(app)
# Core application source files
add_subdirectory(category)
add_subdirectory(middleware)
add_subdirectory(server)
add_subdirectory(shell)
add_subdirectory(utils)
add_subdirectory(system)
add_subdirectory(framework)

3
src/Kconfig Normal file
View File

@@ -0,0 +1,3 @@
rsource "category/Kconfig"
rsource "server/Kconfig"
rsource "middleware/Kconfig"

View File

@@ -0,0 +1,11 @@
add_subdirectory(comm)
if (CONFIG_CATEGORY_YUBA)
add_subdirectory(yuba)
elseif (CONFIG_CATEGORY_EVB)
add_subdirectory(evb)
elseif (CONFIG_CATEGORY_MINI)
add_subdirectory(mini)
elseif (CONFIG_CATEGORY_ROBOT)
add_subdirectory(robot)
endif()

24
src/category/Kconfig Normal file
View File

@@ -0,0 +1,24 @@
menu "Application cateory"
choice APPLICATION_CATEORY
prompt "Select Application cateory"
default CATEGORY_YUBA
config CATEGORY_YUBA
bool "Yuba"
config CATEGORY_EVB
bool "evb board"
config CATEGORY_MINI
bool "mini board"
endchoice
orsource "comm/Kconfig"
orsource "evb/Kconfig"
orsource "mini/Kconfig"
orsource "yuba/Kconfig"
endmenu

View File

@@ -0,0 +1,7 @@
listenai_include_directories(./)
listenai_library_sources(
app_datas.c
)
add_subdirectory(msgs)

11
src/category/comm/Kconfig Normal file
View File

@@ -0,0 +1,11 @@
config ROMFS_IMAGE_ADDR
hex "Address of romfs iamge"
default 0x30100000
config ROMFS_IMAGE_SIZE
hex "Size of romfs iamge"
default 0x700000
rsource "Kconfig.rcmd"
rsource "Kconfig.cloud"

View File

@@ -0,0 +1,48 @@
menu "Cloud Service Configuration"
config CLOUD_PRODUCT_ID_DEFAULT
string "Cloud Product ID"
default "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
help
The Product ID assigned by the cloud service for this device.
config CLOUD_SECRET_ID_DEFAULT
string "Cloud Secret ID"
default "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
help
The Secret ID assigned by the cloud service for this device.
config CLOUD_FULL_DUPLEX_ENABLE
bool "Enable Full Duplex Mode"
default n
help
Enable or disable full duplex communication with the cloud service.
When enabled, the device can send and receive audio data simultaneously.
if CLOUD_FULL_DUPLEX_ENABLE
config CLOUD_FULL_DUPLEX_TIMEOUT_MS
int "Full Duplex Timeout (ms)"
default 90000
help
Timeout value in milliseconds for full duplex communication.
If no activity is detected within this timeout period, the full duplex
session will be terminated.
endif
config CLOUD_IDLE_EXIT_TIMEOUT_MS
int "Idle Exit Timeout (ms)"
default 10000
help
Local business timeout in milliseconds.
After TTS playback ends, if no valid IAT text is received within this
timeout, the device will actively exit the current session.
config CLOUD_STAGING_MODE_ENABLE
bool "Enable Staging Mode"
default n
help
Enable or disable staging mode for testing purposes.
When enabled, the device will connect to the staging environment
of the cloud service instead of the production environment.
endmenu

View File

@@ -0,0 +1,47 @@
menu "Recognize Command Router Configuration"
choice RCMD_ROUTER_STRATEGY
prompt "Command router strategy"
default RCMD_ROUTER_STRATEGY_ONLINE_FIRST
help
Select the routing strategy for voice commands:
- ONLINE_FIRST: Prefer online recognition, fall back to offline when device is offline
- OFFLINE_ONLY: Only process offline recognition results
- TIMEOUT_FALLBACK: Cache offline command and wait for online result with timeout
config RCMD_ROUTER_STRATEGY_ONLINE_FIRST
bool "Online first"
help
Process online results with priority. When device is online, ignore offline
results and wait for online recognition. When device is offline, process
offline results immediately.
config RCMD_ROUTER_STRATEGY_OFFLINE_ONLY
bool "Offline only"
help
Only process offline voice recognition results. Online results are ignored.
This mode is useful for fully offline scenarios.
config RCMD_ROUTER_STRATEGY_TIMEOUT_FALLBACK
bool "Timeout fallback"
help
Cache offline command and wait for online result. If online result arrives
before timeout, process online result and invalidate cached offline command.
If timeout occurs, process the cached offline command.
endchoice
if RCMD_ROUTER_STRATEGY_TIMEOUT_FALLBACK
config RCMD_ROUTER_ONLINE_TIMEOUT_MS
int "Online command timeout (ms)"
default 3000
range 100 10000
depends on RCMD_ROUTER_STRATEGY_TIMEOUT_FALLBACK
help
Timeout value in milliseconds for waiting online command result.
Only applicable when TIMEOUT_FALLBACK strategy is selected.
If no online result arrives within this timeout, the cached offline
command will be executed.
endif
endmenu

View File

@@ -0,0 +1,346 @@
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#define TAG "app.datas"
#include "sys_init.h"
#include "app_datas.h"
#include "lisa_log.h"
#include "lisa_mem.h"
#include "kv.h"
#include "lisa_kv.h"
#include "FreeRTOS.h"
#include "task.h"
#include "romfs.h"
#include "ini.h"
#define DEFAULT_PRODUCT_ID CONFIG_CLOUD_PRODUCT_ID_DEFAULT
#define DEFAULT_SECRET_ID CONFIG_CLOUD_SECRET_ID_DEFAULT
#if CONFIG_CLOUD_FULL_DUPLEX_ENABLE
#define DEFAULT_FULL_DUPLEX (1)
#define DEFAULT_FULL_DUPLEX_TIMEOUT_MS (CONFIG_CLOUD_FULL_DUPLEX_TIMEOUT_MS)
#else
#define DEFAULT_FULL_DUPLEX (0)
#define DEFAULT_FULL_DUPLEX_TIMEOUT_MS (0)
#endif
#if CONFIG_CLOUD_STAGING_MODE_ENABLE
#define DEFAULT_DEVICE_MODE (DEVICE_MODE_STAGING)
#else
#define DEFAULT_DEVICE_MODE (DEVICE_MODE_PROD)
#endif
static struct app_datas *g_app_datas = NULL;
static TaskHandle_t g_app_datas_init_task = NULL;
static struct romfs *romfs = NULL;
#define STR_EMPTY(s) (s == NULL || strlen(s) == 0)
typedef struct {
struct app_datas *app_data;
} ini_parse_context_t;
#define MAX_WAKEUP_KEYWORDS_NUM 10
static char *wakeup_keywords[MAX_WAKEUP_KEYWORDS_NUM] = {
"xiao ao tong xue",
"ni hao xiao ao",
"xiao ling xiao ling",
};
static int ini_config_handler(void* user, const char* section, const char* name, const char* value)
{
ini_parse_context_t *ctx = (ini_parse_context_t *)user;
struct app_datas *app_data = ctx->app_data;
if (strcmp(section, "cloud") == 0) {
if (strcmp(name, "ws-host") == 0) {
strncpy(app_data->host, value, sizeof(app_data->host) - 1);
app_data->host[sizeof(app_data->host) - 1] = '\0';
} else if (strcmp(name, "ws-host-staging") == 0) {
strncpy(app_data->host_staging, value, sizeof(app_data->host_staging) - 1);
app_data->host_staging[sizeof(app_data->host_staging) - 1] = '\0';
} else if (strcmp(name, "auth-url") == 0) {
strncpy(app_data->token_url, value, sizeof(app_data->token_url) - 1);
app_data->token_url[sizeof(app_data->token_url) - 1] = '\0';
} else if (strcmp(name, "auth-url-staging") == 0) {
strncpy(app_data->token_url_staging, value, sizeof(app_data->token_url_staging) - 1);
app_data->token_url_staging[sizeof(app_data->token_url_staging) - 1] = '\0';
} else if (strcmp(name, "ws-host-integration") == 0) {
strncpy(app_data->host_integration, value, sizeof(app_data->host_integration) - 1);
app_data->host_integration[sizeof(app_data->host_integration) - 1] = '\0';
} else if (strcmp(name, "auth-url-integration") == 0) {
strncpy(app_data->token_url_integration, value, sizeof(app_data->token_url_integration) - 1);
app_data->token_url_integration[sizeof(app_data->token_url_integration) - 1] = '\0';
} else if (strcmp(name, "pid") == 0) {
strncpy(app_data->pid, value, sizeof(app_data->pid) - 1);
app_data->pid[sizeof(app_data->pid) - 1] = '\0';
} else if (strcmp(name, "sid") == 0) {
strncpy(app_data->sid, value, sizeof(app_data->sid) - 1);
app_data->sid[sizeof(app_data->sid) - 1] = '\0';
} else if (strcmp(name, "one-shot") == 0) {
if (strcmp(value, "false") == 0) {
app_data->oneshot = 0;
}
}
} else if (strcmp(section, "wakeup") == 0) {
if (strcmp(name, "keyword") == 0) {
static uint8_t keyword_num = 0;
if (keyword_num < MAX_WAKEUP_KEYWORDS_NUM) {
wakeup_keywords[keyword_num] = psram_malloc(strlen(value) + 1);
strcpy(wakeup_keywords[keyword_num], value);
keyword_num++;
}
} else if (strcmp(name, "prompt") == 0) {
strncpy(app_data->wakeup_prompt, value, sizeof(app_data->wakeup_prompt) - 1);
app_data->wakeup_prompt[sizeof(app_data->wakeup_prompt) - 1] = '\0';
}
}
return 1;
}
struct app_datas *get_app_datas(void)
{
assert(g_app_datas_init_task);
if (g_app_datas_init_task != xTaskGetCurrentTaskHandle()) {
LOGW("Attempting to access app data in a dangerous thread. Caller: %p, current thread:%s, safety thread: %s",
__builtin_return_address(0), pcTaskGetName(xTaskGetCurrentTaskHandle()),
pcTaskGetName(g_app_datas_init_task));
}
return g_app_datas;
}
static const char *device_id_str_get(void)
{
static char id_buffer_str[17] = {0};
uint8_t id_buffer[8] = {0};
uint32_t *id_1 = (uint32_t *)0x48600208;
uint32_t *id_2 = (uint32_t *)0x4860020c;
char *device_id = NULL;
int r;
if (id_buffer_str[0] != '\0') {
return id_buffer_str;
}
if (*id_1 == 0 && *id_2 == 0) {
id_buffer_str[0] = '\0';
} else {
memcpy(id_buffer, id_1, sizeof(uint32_t));
memcpy(id_buffer + 4, id_2, sizeof(uint32_t));
sprintf(id_buffer_str, "%02x%02x%02x%02x%02x%02x%02x%02x", id_buffer[0], id_buffer[1], id_buffer[2],
id_buffer[3], id_buffer[4], id_buffer[5], id_buffer[6], id_buffer[7]);
}
return id_buffer_str;
}
static void app_datas_load_from_romfs(void)
{
int r;
r = romfs_init(&romfs, CONFIG_ROMFS_IMAGE_ADDR, CONFIG_ROMFS_IMAGE_SIZE);
if (r) {
LOGW("romfs init failed");
return;
}
const char *locale = "zh-CN";
char *temp_buf = NULL;
char config_path[64] = {0};
r = lisa_kv_get_string("user.locale", &temp_buf);
if (r == 0 && temp_buf) {
if (strcmp(temp_buf, "en-GB") == 0) {
locale = "en-GB";
}
lisa_kv_free(temp_buf);
}
snprintf(config_path, sizeof(config_path), "%s/config.ini", locale);
uint32_t size;
uint8_t *data;
r = romfs_info_get(romfs, config_path, &data, &size);
if (r) {
LOGW("failed to load config from romfs: %s", config_path);
return;
}
ini_parse_context_t ctx = {
.app_data = g_app_datas,
};
r = ini_parse_string_length((const char *)data, size, ini_config_handler, &ctx);
if (r != 0) {
LOGW("ini parse failed: %d", r);
return;
}
LOGI("loaded default config from romfs: %s", config_path);
}
static void app_datas_load_from_lisa_kv(void)
{
int r;
char *temp_buf = NULL;
r = lisa_kv_get_string(KV_KEY_USER_PID, &temp_buf);
if (r == 0) {
strcpy(g_app_datas->pid, temp_buf);
lisa_kv_free(temp_buf);
LOGI("use pid from kv system");
}
r = lisa_kv_get_string(KV_KEY_USER_SID, &temp_buf);
if (r == 0) {
strcpy(g_app_datas->sid, temp_buf);
lisa_kv_free(temp_buf);
LOGI("use sid from kv system");
}
r = lisa_kv_get_string(KV_KEY_USER_DEVICE_ID, &temp_buf);
if (r == 0) {
strcpy(g_app_datas->did, temp_buf);
lisa_kv_free(temp_buf);
} else {
strcpy(g_app_datas->did, device_id_str_get());
}
r = lisa_kv_get_bool(KV_KEY_FULL_DUPLEX, (bool *)&g_app_datas->full_duplex);
if (r != 0) {
g_app_datas->full_duplex = DEFAULT_FULL_DUPLEX;
}
int timeout_ms = 0;
r = lisa_kv_get_int(KV_KEY_FULL_DUPLEX_TIMEOUT_MS, &timeout_ms);
if (r != 0) {
timeout_ms = DEFAULT_FULL_DUPLEX_TIMEOUT_MS;
}
g_app_datas->full_duplex_timeout_ms = timeout_ms;
int device_mode = DEFAULT_DEVICE_MODE;
r = lisa_kv_get_int(KV_KEY_DEVICE_MODE, &device_mode);
if (r != 0) {
device_mode = DEFAULT_DEVICE_MODE;
}
if (device_mode < DEVICE_MODE_PROD || device_mode > DEVICE_MODE_INTEGRATION) {
device_mode = DEFAULT_DEVICE_MODE;
}
g_app_datas->device_mode = (uint8_t)device_mode;
int work_mode = -1;
r = lisa_kv_get_int(KV_KEY_WAKEUP_MODE, &work_mode);
if (r) {
work_mode = VOICE_WORK_MODE_VOICE_WAKEUP;
}
g_app_datas->voice_work_mode = work_mode;
}
static void app_datas_load_from_default(void)
{
if (STR_EMPTY(g_app_datas->pid)) {
LOGI("use pid from hard code");
strcpy(g_app_datas->pid, DEFAULT_PRODUCT_ID);
}
if (STR_EMPTY(g_app_datas->sid)) {
LOGI("use sid from hard code");
strcpy(g_app_datas->sid, DEFAULT_SECRET_ID);
}
}
int app_datas_init(void)
{
g_app_datas_init_task = xTaskGetCurrentTaskHandle();
assert(g_app_datas_init_task != NULL);
g_app_datas = (struct app_datas *)lisa_mem_alloc(sizeof(struct app_datas));
assert(g_app_datas != NULL);
memset(g_app_datas, 0, sizeof(struct app_datas));
g_app_datas->oneshot = 1;
#ifdef CONFIG_OTA
strcpy(g_app_datas->wakeup_prompt, "请通过\"#唤醒词#\"唤醒我");
#else
strcpy(g_app_datas->wakeup_prompt, "请通过\"小马采宝\"唤醒我");
#endif
app_datas_load_from_romfs();
app_datas_load_from_lisa_kv();
app_datas_load_from_default();
assert(strlen(g_app_datas->pid) > 0);
assert(strlen(g_app_datas->sid) > 0);
assert(strlen(g_app_datas->did) > 0);
int pid_len = strlen(g_app_datas->pid);
int sid_len = strlen(g_app_datas->sid);
if (pid_len > 8) {
LOGI("PID: %.4s****%.4s", g_app_datas->pid, g_app_datas->pid + pid_len - 4);
} else {
LOGI("PID: %s", g_app_datas->pid);
}
if (sid_len > 8) {
LOGI("SID: %.4s****%.4s", g_app_datas->sid, g_app_datas->sid + sid_len - 4);
} else {
LOGI("SID: %s", g_app_datas->sid);
}
g_app_datas->can_wakeup = 1;
LOGI("did: %s", g_app_datas->did);
LOGI("full_duplex: %d", g_app_datas->full_duplex);
LOGI("full_duplex_timeout_ms: %d", g_app_datas->full_duplex_timeout_ms);
LOGI("device_mode: %d", g_app_datas->device_mode);
LOGI("voice_work_mode: 0x%02x", g_app_datas->voice_work_mode);
LOGI("oneshot: %d", g_app_datas->oneshot);
LOGI("host: %s", g_app_datas->host);
LOGI("host_staging: %s", g_app_datas->host_staging);
LOGI("host_integration: %s", g_app_datas->host_integration);
LOGI("token_url: %s", g_app_datas->token_url);
LOGI("token_url_staging: %s", g_app_datas->token_url_staging);
LOGI("token_url_integration: %s", g_app_datas->token_url_integration);
LOGI("music_active_url: %s", g_app_datas->music_active_url);
LOGI("music_tranlink_url: %s", g_app_datas->music_tranlink_url);
LOGI("wakeup_prompt: %s", g_app_datas->wakeup_prompt);
LOGI("port: %s", g_app_datas->port);
LOGI("scheme: %s", g_app_datas->scheme);
for (int i = 0; i < MAX_WAKEUP_KEYWORDS_NUM; i++) {
LOGI("wakeup keyword[%d]: %s", i, wakeup_keywords[i] ? wakeup_keywords[i] : "null");
}
return 0;
}
bool is_wakeup_keyword(char *keyword)
{
#ifdef CONFIG_BOARD_ARCS_MINI
return true;
#else
for (int i = 0; i < MAX_WAKEUP_KEYWORDS_NUM; i++) {
if (wakeup_keywords[i] == NULL) {
break;
}
if (strcmp(wakeup_keywords[i], keyword) == 0) {
return true;
}
}
return false;
#endif
}

View File

@@ -0,0 +1,45 @@
#ifndef __VOICE_APP_DATAS_H__
#define __VOICE_APP_DATAS_H__
enum {
VOICE_WORK_MODE_VOICE_WAKEUP = (1 << 0),
VOICE_WORK_MODE_BUTTON_WAKEUP = (1 << 1),
};
enum {
DEVICE_MODE_PROD = 0,
DEVICE_MODE_STAGING = 1,
DEVICE_MODE_INTEGRATION = 2,
};
struct app_datas {
char pid[64]; /* 产品ID */
char sid[64]; /* 产品密钥 */
char did[64]; /* 设备ID */
uint8_t wifi_connected; /* 是否已经连接到WiFi */
uint8_t network_connected; /* 是否已经连接到网络 */
uint8_t voice_cloud_connected; /* 是否已经连接到云端 */
uint8_t auth_failed;
uint8_t full_duplex; /* 是否开启全双工 */
uint32_t full_duplex_timeout_ms; /* 全双工超时时间 */
uint8_t device_mode; /* 使用 正式/测试/研发 环境 */
uint8_t can_wakeup;
uint8_t voice_work_mode; /* BIT0: 支持语音唤醒, BIT1: 支持按鍵喚醒 */
uint8_t oneshot;
char host[128];
char host_staging[128];
char host_integration[128];
char token_url[128];
char token_url_staging[128];
char token_url_integration[128];
char music_active_url[128];
char music_tranlink_url[128];
char wakeup_prompt[64];
char port[16];
char scheme[16];
} __attribute__((packed));
struct app_datas *get_app_datas(void);
int app_datas_init(void);
#endif

View File

@@ -0,0 +1,12 @@
listenai_library_named(events)
listenai_library_sources(
voice_wifi_msg.c
voice_cloud_msg.c
voice_wakeup_msg.c
voice_livekit_poll_msg.c
voice_platform.c
voice_rcmd_msg.c
)
listenai_include_directories(${CMAKE_CURRENT_SOURCE_DIR})

View File

@@ -0,0 +1,244 @@
#include "stdint.h"
#include "stddef.h"
#include "stdbool.h"
#include <string.h>
#include "FreeRTOS.h"
#include "timers.h"
#include "lisa_log.h"
#include "voice_msg.h"
#include "app_datas.h"
#include "voice_cloud.h"
#include "player_mgr.h"
#include "tone.h"
#include "kv.h"
#include "lisa_kv.h"
#include "sys_init.h"
#define TAG "voice.app.cloud"
#define VOICE_IDLE_EXIT_TIMEOUT_MS_DEFAULT ((uint32_t)CONFIG_CLOUD_IDLE_EXIT_TIMEOUT_MS)
static bool s_voice_cloud_session_running = false;
static TimerHandle_t s_voice_idle_exit_timer = NULL;
static uint32_t s_voice_idle_exit_timeout_ms = VOICE_IDLE_EXIT_TIMEOUT_MS_DEFAULT;
static void voice_idle_exit_timeout_refresh_from_kv(void)
{
int timeout_ms = 0;
int ret = lisa_kv_get_int(KV_KEY_IDLE_EXIT_TIMEOUT_MS, &timeout_ms);
if (ret == 0 && timeout_ms > 0) {
s_voice_idle_exit_timeout_ms = (uint32_t)timeout_ms;
LOGI("voice idle exit timeout use kv: %u ms", (unsigned)s_voice_idle_exit_timeout_ms);
return;
}
s_voice_idle_exit_timeout_ms = VOICE_IDLE_EXIT_TIMEOUT_MS_DEFAULT;
LOGI("voice idle exit timeout use default: %u ms", (unsigned)s_voice_idle_exit_timeout_ms);
}
static void voice_idle_exit_timer_cb(TimerHandle_t xTimer)
{
(void)xTimer;
if (!s_voice_cloud_session_running) {
return;
}
LOGI("voice idle exit timeout reached, trigger mcp chat exit");
voice_cloud_chat_stop();
voice_msg_pub(VOICE_MSG_CLOUD_SESSION_FINISHED, NULL, 0);
player_mgr_play(LOCAL, app_tone_get_url(TONE_ID_72), 0);
}
static void voice_idle_exit_timer_init(void)
{
voice_idle_exit_timeout_refresh_from_kv();
if (s_voice_idle_exit_timer == NULL) {
s_voice_idle_exit_timer =
xTimerCreate("voice.idle.exit", pdMS_TO_TICKS(s_voice_idle_exit_timeout_ms), pdFALSE, NULL,
voice_idle_exit_timer_cb);
assert(s_voice_idle_exit_timer != NULL);
}
}
static void voice_idle_exit_timer_stop(void)
{
if (s_voice_idle_exit_timer == NULL) {
return;
}
if (xTimerIsTimerActive(s_voice_idle_exit_timer) == pdFALSE) {
return;
}
if (xTimerStop(s_voice_idle_exit_timer, 0) != pdPASS) {
LOGW("voice_idle_exit_timer_stop failed");
}
}
static void voice_idle_exit_timer_start(void)
{
if (!s_voice_cloud_session_running) {
return;
}
if (s_voice_idle_exit_timer == NULL) {
return;
}
voice_idle_exit_timeout_refresh_from_kv();
if (xTimerIsTimerActive(s_voice_idle_exit_timer) != pdFALSE) {
if (xTimerStop(s_voice_idle_exit_timer, 0) != pdPASS) {
LOGW("voice_idle_exit_timer_stop before start failed");
}
}
if (xTimerChangePeriod(s_voice_idle_exit_timer, pdMS_TO_TICKS(s_voice_idle_exit_timeout_ms), 0) != pdPASS) {
LOGW("voice_idle_exit_timer_change failed");
}
}
static void voice_cloud_connected(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
LOGI("voice_cloud_connected");
struct app_datas *app_datas = get_app_datas();
assert(app_datas != NULL);
app_datas->voice_cloud_connected = 1;
lsc_music_active();
}
static void voice_cloud_disconnected(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
LOGI("voice_cloud_disconnected");
struct app_datas *app_datas = get_app_datas();
assert(app_datas != NULL);
app_datas->voice_cloud_connected = 0;
s_voice_cloud_session_running = false;
voice_idle_exit_timer_stop();
}
static void voice_cloud_session_starting(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
LOGI("voice_cloud_session_starting");
s_voice_cloud_session_running = true;
voice_idle_exit_timer_start();
}
static void voice_cloud_tts_txt(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
if (msg_id == VOICE_MSG_CLOUD_TTS_TEXT_START) {
LOGI("voice_cloud_tts_txt, start");
} else if (msg_id == VOICE_MSG_CLOUD_TTS_TEXT_UPDATE) {
LOGI("voice_cloud_tts_txt, update: %s", (char *)data);
} else if (msg_id == VOICE_MSG_CLOUD_TTS_TEXT_END) {
LOGI("voice_cloud_tts_txt, end");
}
}
static void voice_cloud_iat_txt(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
if (msg_id == VOICE_MSG_CLOUD_IAT_START) {
LOGI("voice_cloud_iat_txt, start");
} else if (msg_id == VOICE_MSG_CLOUD_IAT_UPDATE) {
LOGI("voice_cloud_iat_txt, update: %s", (char *)data);
if (data && len > 0 && ((char *)data)[0] != '\0') {
voice_idle_exit_timer_stop();
service_image_waiting_cancel();
}
} else if (msg_id == VOICE_MSG_CLOUD_IAT_END) {
LOGI("voice_cloud_iat_txt, end");
}
}
static void voice_cloud_tts_stoped(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
(void)unused;
(void)msg_id;
(void)data;
(void)len;
(void)user_data;
LOGI("tts stopped, start idle exit timer");
voice_idle_exit_timer_start();
}
static void voice_cloud_session_finished(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
LOGI("voice_cloud_session_finished");
s_voice_cloud_session_running = false;
voice_idle_exit_timer_stop();
// player_mgr_focus_release(AIP);
}
static void voice_cloud_mcp_chat_exit(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
LOGI("voice_cloud_mcp_chat_exit");
s_voice_cloud_session_running = false;
voice_idle_exit_timer_stop();
voice_cloud_chat_stop();
voice_msg_pub(VOICE_MSG_CLOUD_SESSION_FINISHED, NULL, 0);
}
static void voice_cloud_audio_url_play(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
(void)unused;
(void)msg_id;
(void)user_data;
if (data == NULL || len == 0) {
LOGW("voice_cloud_audio_url_play: invalid payload");
return;
}
char audio_url[512] = {0};
uint32_t copy_len = len > sizeof(audio_url) - 1 ? sizeof(audio_url) - 1 : len;
memcpy(audio_url, data, copy_len);
if (audio_url[0] == '\0') {
LOGW("voice_cloud_audio_url_play: empty url");
return;
}
LOGI("voice_cloud_audio_url_play: %s", audio_url);
player_mgr_play(TTS, audio_url, 0);
}
int voice_cloud_evt_init(void)
{
#ifndef CONFIG_VOICE_LINK_LSCHAT
LOGI("LSChat link inactive, skip cloud event subscriptions");
return 0;
#endif
voice_idle_exit_timer_init();
voice_msg_sub(VOICE_MSG_CLOUD_CONNECTED, voice_cloud_connected, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_DISCONNECTED, voice_cloud_disconnected, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_SESSION_STARTING, voice_cloud_session_starting, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_SESSION_FINISHED, voice_cloud_session_finished, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_TTS_TEXT_START, voice_cloud_tts_txt, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_TTS_TEXT_UPDATE, voice_cloud_tts_txt, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_TTS_TEXT_END, voice_cloud_tts_txt, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_IAT_START, voice_cloud_iat_txt, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_IAT_UPDATE, voice_cloud_iat_txt, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_IAT_END, voice_cloud_iat_txt, NULL);
voice_msg_sub(VOICE_MSG_PLAYER_TTS_STOPED, voice_cloud_tts_stoped, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_MCP_CHAT_EXIT, voice_cloud_mcp_chat_exit, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_AUDIO_URL, voice_cloud_audio_url_play, NULL);
return 0;
}
SYS_INIT(voice_cloud_evt_init, SYS_INIT_LEVEL_PRE_APPLICATION, 50);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,189 @@
#include "stdint.h"
#define TAG "voice.app.platform"
#include "voice_msg.h"
#include "lisa_log.h"
#include "sys_init.h"
#include "voice_cloud.h"
#ifdef CONFIG_VOICE_LINK_LINGXIN
#include "lingxin_server.h"
#include "lingxin_trace.h"
#include "lisa_thread.h"
#include "lisa_typedef.h"
#endif
#ifdef CONFIG_OTA
#include "ota_manager.h"
#endif
#include "app_datas.h"
static void voice_platform_ready(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
LOGI("network probe ready");
app_datas_init();
ls_wifi_mgr_init();
}
static void do_voice_cloud_connect(void)
{
struct app_datas *app_datas = get_app_datas();
assert(app_datas != NULL);
app_datas->network_connected = 1;
if (!app_datas->voice_cloud_connected) {
struct voice_cloud_connect_config connect_config = {
.pid = app_datas->pid,
.sid = app_datas->sid,
.did = app_datas->did,
.auto_reconn = true,
.reconn_interval_ms = 3000,
.device_mode = app_datas->device_mode,
.host = app_datas->host[0] == 0 ? NULL : app_datas->host,
.host_staging = app_datas->host_staging[0] == 0 ? NULL : app_datas->host_staging,
.host_integration = app_datas->host_integration[0] == 0 ? NULL : app_datas->host_integration,
.token_url = app_datas->token_url[0] == 0 ? NULL : app_datas->token_url,
.token_url_staging = app_datas->token_url_staging[0] == 0 ? NULL : app_datas->token_url_staging,
.token_url_integration = app_datas->token_url_integration[0] == 0 ? NULL : app_datas->token_url_integration,
.music_active_url = app_datas->music_active_url[0] == 0 ? NULL : app_datas->music_active_url,
.music_tranlink_url = app_datas->music_tranlink_url[0] == 0 ? NULL : app_datas->music_tranlink_url,
.port = app_datas->port[0] == 0 ? NULL : app_datas->port,
.scheme = app_datas->scheme[0] == 0 ? NULL : app_datas->scheme,
};
voice_cloud_connect(&connect_config);
}
}
#ifdef CONFIG_VOICE_LINK_LINGXIN
static volatile bool s_lingxin_auto_chat_starting;
static bool s_lingxin_auto_start_chat = false;
#define LINGXIN_AUTO_CHAT_DELAY_MS 12000
static void lingxin_auto_chat_thread(void *arg)
{
(void)arg;
LOGI("[Lingxin] auto chat thread begin");
lingxin_trace_set("auto:chat_delay");
lisa_thread_mdelay(LINGXIN_AUTO_CHAT_DELAY_MS);
lingxin_trace_set("auto:chat_start_call");
int ret = lingxin_server_chat_start();
lingxin_trace_set(ret == 0 ? "auto:chat_start_ret_ok" : "auto:chat_start_ret_fail");
LOGI("[Lingxin] auto chat thread end, ret=%d", ret);
s_lingxin_auto_chat_starting = false;
}
static void schedule_lingxin_auto_chat(void)
{
if (!s_lingxin_auto_start_chat) {
lingxin_trace_set("auto:chat_start_disabled");
LOGW("[Lingxin] auto chat start disabled for freeze bisection");
return;
}
if (s_lingxin_auto_chat_starting) {
LOGI("[Lingxin] auto chat skipped: already starting");
return;
}
s_lingxin_auto_chat_starting = true;
lisa_thread_attr_t attr = {
.name = (uint8_t *)"lingxin.auto",
.stack_size = 12 * 1024,
.priority = LISA_OS_PRIORITY_LOW,
};
LOGI("[Lingxin] schedule auto chat start");
lisa_thread_t *thread = lisa_thread_create(&attr, lingxin_auto_chat_thread, NULL);
if (thread == NULL) {
s_lingxin_auto_chat_starting = false;
LOGE("[Lingxin] auto chat thread create failed");
}
}
static void do_lingxin_cloud_connect(void)
{
struct app_datas *app_datas = get_app_datas();
assert(app_datas != NULL);
app_datas->network_connected = 1;
if (app_datas->voice_cloud_connected) {
LOGI("[Lingxin] cloud connect skipped: already marked connected");
return;
}
LOGI("[Lingxin] cloud connect begin");
if (lingxin_server_connect() == 0) {
app_datas->voice_cloud_connected = 1;
voice_msg_pub(VOICE_MSG_CLOUD_CONNECTED, NULL, 0);
LOGI("[Lingxin] cloud connect ready");
schedule_lingxin_auto_chat();
} else {
app_datas->voice_cloud_connected = 0;
voice_msg_pub(VOICE_MSG_CLOUD_DISCONNECTED, NULL, 0);
LOGE("[Lingxin] cloud connect failed");
}
}
#endif
static void voice_system_network_probe_success(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
LOGI("network probe success");
struct app_datas *app_datas = get_app_datas();
assert(app_datas != NULL);
app_datas->network_connected = 1;
#ifdef CONFIG_VOICE_LINK_LSCHAT
#ifdef CONFIG_OTA
ota_manager_check_all();
#else
do_voice_cloud_connect();
#endif
#elif defined(CONFIG_VOICE_LINK_LINGXIN)
do_lingxin_cloud_connect();
#else
LOGI("LSChat link inactive, skip voice cloud connect");
#endif
}
static void voice_system_network_probe_fail(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
LOGI("network probe fail");
struct app_datas *app_datas = get_app_datas();
assert(app_datas != NULL);
app_datas->network_connected = 0;
}
#ifdef CONFIG_OTA
static void voice_ota_up_to_date(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
#ifdef CONFIG_VOICE_LINK_LSCHAT
do_voice_cloud_connect();
#elif defined(CONFIG_VOICE_LINK_LINGXIN)
do_lingxin_cloud_connect();
#else
LOGI("LSChat link inactive, skip OTA-triggered voice cloud connect");
#endif
}
#endif
int voice_platform_evt_init(void)
{
LOGI("voice_platform_evt_init");
voice_msg_sub(VOICE_MSG_PLATFORM_READY, voice_platform_ready, NULL);
voice_msg_sub(VOICE_MSG_SYSTEM_NETWORK_PROBE_SUCCESS, voice_system_network_probe_success, NULL);
voice_msg_sub(VOICE_MSG_SYSTEM_NETWORK_PROBE_FAIL, voice_system_network_probe_fail, NULL);
#ifdef CONFIG_OTA
voice_msg_sub(VOICE_MSG_OTA_UP_TO_DATE, voice_ota_up_to_date, NULL);
#endif
return 0;
}
SYS_INIT(voice_platform_evt_init, SYS_INIT_LEVEL_PRE_APPLICATION, 50);

View File

@@ -0,0 +1,135 @@
#include "stdint.h"
#include <stdio.h>
#include <string.h>
#include "rcmd_router.h"
#include "voice_msg.h"
#include "sys_init.h"
#include "voice_cloud.h"
#include "app_datas.h"
#define TAG "recognize.cmd.msg"
#include "lisa_log.h"
static void recognize_cmd_msg_combine_pub(voice_msg_cloud_recognized_command_t *cmd, rcmd_router_cmd_type_e cmd_type)
{
LISA_LOGI(TAG, "recognize_cmd_msg_combine_pub: %s, context id: %s, from type: %s",
cmd->command, cmd->context.id,
cmd_type == RCMD_ROUTER_CMD_TYPE_OFFLINE? "OFFLINE":"ONLINE");
if(cmd_type == RCMD_ROUTER_CMD_TYPE_OFFLINE){
voice_msg_pub(VOICE_MSG_RECOGNIZED_OFFLINE_COMMAND, cmd, sizeof(voice_msg_cloud_recognized_command_t));
}
else{
voice_msg_pub(VOICE_MSG_RECOGNIZED_ONLINE_COMMAND, cmd, sizeof(voice_msg_cloud_recognized_command_t));
}
}
static void recognize_status_msg_combine_pub(rcmd_router_status_e status, rcmd_router_cmd_type_e cmd_type)
{
LISA_LOGI(TAG, "recognize_status_msg_combine_pub status: %d, from type: %s", status,
cmd_type == RCMD_ROUTER_CMD_TYPE_OFFLINE? "OFFLINE":"ONLINE");
/*只有离线命令才需要终端处理finish,在线状态云端直接播报tts*/
if((status == RCMD_ROUTER_STATUS_SESSION_FINISH)&&(cmd_type == RCMD_ROUTER_CMD_TYPE_OFFLINE)){
voice_msg_pub(VOICE_MSG_RECOGNIZED_SESSION_FINISH, NULL, 0);
}
}
/*离线命令处理*/
static void voice_offline_command_msg(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
struct app_datas *app_datas = get_app_datas();
voice_msg_cloud_recognized_command_t cmd_data;
memset(&cmd_data, 0, sizeof(cmd_data));
snprintf(cmd_data.command, sizeof(cmd_data.command), "%s", (char *)data);
// 离线命令没有 context.id保持为空
rcmd_router_offline_cmd_proc(&cmd_data, app_datas->voice_cloud_connected);
LISA_LOGI(TAG, "voice_offline_command_msg: %s", (char *)data);
}
static void voice_offline_status_finish_msg(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
static int32_t prev_mode = -1;
struct app_datas *app_datas = get_app_datas();
if (app_datas == NULL) {
LOGW("Invalid app_datas");
return;
}
rcmd_router_offline_status_proc(RCMD_ROUTER_STATUS_SESSION_FINISH,app_datas->voice_cloud_connected);
}
static void voice_online_status_finish_msg(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
static int32_t prev_mode = -1;
struct app_datas *app_datas = get_app_datas();
if (app_datas == NULL) {
LOGW("Invalid app_datas");
return;
}
rcmd_router_online_status_proc(RCMD_ROUTER_STATUS_SESSION_FINISH);
}
/*在线命令处理*/
static void voice_online_command_msg(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
voice_msg_cloud_recognized_command_t *msg_cmd = (voice_msg_cloud_recognized_command_t *)data;
struct app_datas *app_datas = get_app_datas();
rcmd_router_online_cmd_proc(msg_cmd);
LISA_LOGI(TAG, "voice_online_command_msg: %s, context id: %s", msg_cmd->command, msg_cmd->context.id);
}
/*初始化*/
int voice_rcmd_msg_process_init(void)
{
#ifndef CONFIG_VOICE_LINK_LSCHAT
LOGI("LSChat link inactive, skip command router subscriptions");
return 0;
#endif
int ret;
rcmd_router_config_t config = {
#ifdef CONFIG_RCMD_ROUTER_STRATEGY_ONLINE_FIRST
.strategy = CMD_ROUTER_STRATEGY_ONLINE_FIRST,
#elif defined(CONFIG_RCMD_ROUTER_STRATEGY_OFFLINE_ONLY)
.strategy = CMD_ROUTER_STRATEGY_OFFLINE_ONLY,
#elif defined(CONFIG_RCMD_ROUTER_STRATEGY_TIMEOUT_FALLBACK)
.strategy = CMD_ROUTER_STRATEGY_TIMEOUT_FALLBACK,
#else
.strategy = CMD_ROUTER_STRATEGY_ONLINE_FIRST,
#endif
#ifdef CONFIG_RCMD_ROUTER_ONLINE_TIMEOUT_MS
.online_timeout_ms = CONFIG_RCMD_ROUTER_ONLINE_TIMEOUT_MS,
#else
.online_timeout_ms = 3000,
#endif
.action = recognize_cmd_msg_combine_pub,
.status_action = recognize_status_msg_combine_pub,
};
LOGI("recognize_cmd_msg_process_init");
ret = rcmd_router_init(&config);
if (ret != 0) {
LOGE("rcmd_router_init failed:%d", ret);
return ret;
}
voice_msg_sub(VOICE_MSG_WAKEUP_COMMAND, voice_offline_command_msg, NULL);
voice_msg_sub(VOICE_MSG_WAKEUP_RECOGNIZED_FINISH, voice_offline_status_finish_msg, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_RECOGNIZED_COMMAND, voice_online_command_msg, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_SESSION_FINISHED, voice_online_status_finish_msg, NULL);
return 0;
}
SYS_INIT(voice_rcmd_msg_process_init, SYS_INIT_LEVEL_PRE_APPLICATION, 90);

View File

@@ -0,0 +1,274 @@
#include "stdint.h"
#include "stddef.h"
#define TAG "voice.app.wakeup"
#include "voice_msg.h"
#include "app_datas.h"
#include "sys_init.h"
#include "voice_cloud.h"
#ifdef CONFIG_VOICE_LINK_LINGXIN
#include "lingxin_server.h"
#include "lingxin_trace.h"
#include "lisa_thread.h"
#include "lisa_typedef.h"
#endif
#include "lisa_log.h"
static bool is_valid_keyword(char *keyword)
{
if (keyword == NULL) {
return false;
}
return true;
}
static bool start_voice_cloud(struct app_datas *app_datas)
{
if (app_datas == NULL) {
LOGW("Invalid app_datas");
return false;
}
if (app_datas->wifi_connected == 0) {
LOGW("WiFi not connected");
return false;
}
if (app_datas->voice_cloud_connected == 0) {
LOGW("Voice cloud not connected");
return false;
}
if (app_datas->can_wakeup == 0) {
LOGI("ignore wakeup msg");
return false;
}
const char *keywords[] = {"小马采宝"};
struct voice_cloud_chat_config chat_config = {
.full_duplex = app_datas->full_duplex,
.timeout_ms = app_datas->full_duplex_timeout_ms,
.oneshot = app_datas->oneshot,
.words = (char **)keywords,
.words_cnt = 1,
};
return voice_cloud_chat_start(&chat_config) == 0;
}
#ifdef CONFIG_VOICE_LINK_LINGXIN
static volatile bool s_lingxin_starting;
static bool start_lingxin_chat(struct app_datas *app_datas)
{
if (app_datas == NULL) {
LOGW("[Lingxin] start chat blocked: invalid app_datas");
return false;
}
if (app_datas->wifi_connected == 0 || app_datas->network_connected == 0) {
LOGW("[Lingxin] start chat blocked: network not ready, wifi=%d, network=%d",
app_datas->wifi_connected,
app_datas->network_connected);
return false;
}
if (app_datas->can_wakeup == 0) {
LOGI("[Lingxin] start chat blocked: can_wakeup=0");
return false;
}
LOGI("[Lingxin] start chat checks passed");
int ret = lingxin_server_chat_start();
if (ret != 0) {
LOGE("[Lingxin] start chat failed: %d", ret);
return false;
}
LOGI("[Lingxin] start chat ok");
return true;
}
static void lingxin_start_thread(void *arg)
{
(void)arg;
lingxin_trace_set("wakeup:start_thread_enter");
LOGI("[Lingxin] async start thread begin");
lisa_thread_mdelay(1000);
lingxin_trace_set("wakeup:start_after_delay");
struct app_datas *app_datas = get_app_datas();
bool started = start_lingxin_chat(app_datas);
lingxin_trace_set(started ? "wakeup:start_ret_ok" : "wakeup:start_ret_fail");
LOGI("[Lingxin] async start thread end, started=%d", started ? 1 : 0);
s_lingxin_starting = false;
}
static void start_lingxin_chat_async(const char *reason)
{
if (s_lingxin_starting) {
LOGW("[Lingxin] start ignored: already running, reason=%s", reason ? reason : "unknown");
return;
}
s_lingxin_starting = true;
lisa_thread_attr_t attr = {
.name = (uint8_t *)"lingxin.start",
.stack_size = 12 * 1024,
.priority = LISA_OS_PRIORITY_LOW,
};
lingxin_trace_clear();
lingxin_trace_set("wakeup:start_async_enter");
LOGI("[Lingxin] create async start thread, reason=%s", reason ? reason : "unknown");
lisa_thread_t *thread = lisa_thread_create(&attr, lingxin_start_thread, NULL);
if (thread == NULL) {
s_lingxin_starting = false;
lingxin_trace_set("wakeup:start_thread_create_fail");
LOGE("[Lingxin] create async start thread failed");
} else {
lingxin_trace_set("wakeup:start_thread_created");
}
}
#endif
static void voice_wakeup_keyword(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
LOGI("voice_wakeup_keyword, keyword: %s", (char *)data);
struct app_datas *app_datas = get_app_datas();
if (app_datas == NULL) {
return;
}
if (!is_valid_keyword((char *)data)) {
LOGW("Invalid keyword: %s", (char *)data);
return;
}
if ((app_datas->voice_work_mode & VOICE_WORK_MODE_VOICE_WAKEUP) == 0) {
LOGW("ignore wakeup evt, voice work mode: %d", app_datas->voice_work_mode);
return;
}
#ifdef CONFIG_VOICE_LINK_LSCHAT
start_voice_cloud(app_datas);
#elif defined(CONFIG_VOICE_LINK_LINGXIN)
LOGI("[Lingxin] keyword wakeup accepted, keyword=%s", (char *)data);
start_lingxin_chat_async("keyword");
#endif
}
static void voice_wakeup_button_event(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
(void)unused;
(void)msg_id;
(void)len;
(void)user_data;
if (data == NULL || len < sizeof(voice_msg_button_evt_t)) {
LOGW("Invalid button evt payload");
return;
}
voice_msg_button_evt_t *evt = (voice_msg_button_evt_t *)data;
LOGI("button evt id=%d action=%d", evt->button_id, evt->action);
/* only k1 (button0) is configured for PTT */
if (evt->button_id != 0) {
LOGI("ignore button_id=%d", evt->button_id);
return;
}
#ifdef CONFIG_VOICE_LINK_LINGXIN
/*
* Lingxin uses click-to-start plus local endpointing. Do not treat raw
* press/release as PTT, because release would stop the whole chat session.
*/
return;
#endif
if (evt->action == VOICE_MSG_BUTTON_ACTION_PRESS_DOWN) {
voice_msg_pub(VOICE_MSG_WAKEUP_BUTTON_START, NULL, 0);
} else if (evt->action == VOICE_MSG_BUTTON_ACTION_SHORT_UP
|| evt->action == VOICE_MSG_BUTTON_ACTION_LONG_UP
|| evt->action == VOICE_MSG_BUTTON_ACTION_LONG_HOLD_UP) {
voice_msg_pub(VOICE_MSG_WAKEUP_BUTTON_STOP, NULL, 0);
}
}
static void voice_msg_btn_wakeup_start(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
struct app_datas *app_datas = get_app_datas();
if (app_datas == NULL) {
return;
}
if ((app_datas->voice_work_mode & VOICE_WORK_MODE_BUTTON_WAKEUP) == 0) {
LOGW("ignore button evt, voice work mode: %d", app_datas->voice_work_mode);
return;
}
if (!app_datas->can_wakeup) {
return;
}
if (!app_datas->voice_cloud_connected) {
return;
}
#ifdef CONFIG_VOICE_LINK_LSCHAT
voice_cloud_audio_recognition_start();
#elif defined(CONFIG_VOICE_LINK_LINGXIN)
LOGI("[Lingxin] button start accepted");
start_lingxin_chat_async("button");
#endif
}
static void voice_msg_btn_wakeup_stop(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
struct app_datas *app_datas = get_app_datas();
if (app_datas == NULL) {
return;
}
if ((app_datas->voice_work_mode & VOICE_WORK_MODE_BUTTON_WAKEUP) == 0) {
LOGW("ignore button evt, voice work mode: %d", app_datas->voice_work_mode);
return;
}
if (!app_datas->can_wakeup) {
return;
}
if (!app_datas->voice_cloud_connected) {
return;
}
#ifdef CONFIG_VOICE_LINK_LSCHAT
voice_cloud_audio_recognition_stop();
#elif defined(CONFIG_VOICE_LINK_LINGXIN)
LOGI("[Lingxin] button stop accepted: end record");
lingxin_server_chat_end_record();
#endif
}
int voice_wakeup_evt_init(void)
{
#if !defined(CONFIG_VOICE_LINK_LSCHAT) && !defined(CONFIG_VOICE_LINK_LINGXIN)
LOGI("No voice link active, skip wakeup event subscriptions");
return 0;
#endif
voice_msg_sub(VOICE_MSG_WAKEUP_KEYWORD, voice_wakeup_keyword, NULL);
voice_msg_sub(VOICE_MSG_BUTTON_CHANGE, voice_wakeup_button_event, NULL);
voice_msg_sub(VOICE_MSG_WAKEUP_BUTTON_START, voice_msg_btn_wakeup_start, NULL);
voice_msg_sub(VOICE_MSG_WAKEUP_BUTTON_STOP, voice_msg_btn_wakeup_stop, NULL);
return 0;
}
SYS_INIT(voice_wakeup_evt_init, SYS_INIT_LEVEL_PRE_APPLICATION, 50);

View File

@@ -0,0 +1,43 @@
#include "stdint.h"
#include "stddef.h"
#include "lisa_log.h"
#include "voice_msg.h"
#include "sys_init.h"
#include "app_datas.h"
#define TAG "voice.app.wifi"
static void voice_wifi_ip_got(void *unused, uint32_t evt, void *data, uint32_t len, void *user_data)
{
LOGI("voice_wifi_ip_got");
struct app_datas *app_datas = get_app_datas();
assert(app_datas != NULL);
app_datas->wifi_connected = 1;
network_probe_start();
}
static void voice_wifi_disconnected(void *unused, uint32_t evt, void *data, uint32_t len, void *user_data)
{
LOGI("voice_wifi_disconnected");
struct app_datas *app_datas = get_app_datas();
assert(app_datas != NULL);
app_datas->wifi_connected = 0;
app_datas->network_connected = 0;
}
int voice_wifi_evt_init(void)
{
LOGI("voice_wifi_evt_init");
voice_msg_sub(VOICE_MSG_WIFI_IP_GOT, voice_wifi_ip_got, NULL);
voice_msg_sub(VOICE_MSG_WIFI_DISCONNECTED, voice_wifi_disconnected, NULL);
return 0;
}
SYS_INIT(voice_wifi_evt_init, SYS_INIT_LEVEL_PRE_APPLICATION, 50);

View File

@@ -0,0 +1,2 @@
add_subdirectory(mcp-tools)

27
src/category/mini/Kconfig Normal file
View File

@@ -0,0 +1,27 @@
menu "category mini"
config MCP_TOOL_EMOJI
bool "mcp tool emoji"
default y
help
Enable mcp tool emoji
config MCP_TOOL_MODEL_PERSONA_UPDATE
bool "mcp tool model persona update"
default y
help
Enable mcp tool model persona update
config MCP_TOOL_SHOW_QRCODE
bool "mcp tool show qrcode"
default y
help
Enable mcp tool show qrcode
config MCP_TOOL_LOADING
bool "mcp tool loading"
default y
help
Enable mcp tool loading
endmenu

View File

@@ -0,0 +1,12 @@
listenai_library_named(category_mini_mcp_tools)
listenai_library_sources(empty.c)
listenai_library_sources_ifdef(CONFIG_MCP_TOOL_EMOJI mcp_tool_emoji.c)
listenai_library_sources_ifdef(CONFIG_MCP_TOOL_MODEL_PERSONA_UPDATE mcp_tool_model_persona_update.c)
listenai_library_sources_ifdef(CONFIG_MCP_TOOL_SHOW_QRCODE mcp_tool_show_qrcode.c)
listenai_library_sources_ifdef(CONFIG_MCP_TOOL_LOADING mcp_tool_loading.c)
listenai_include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
)

View File

View File

@@ -0,0 +1,114 @@
#include <stdbool.h>
#include <string.h>
#include <stdio.h>
#include "lisa_log.h"
#include "voice_msg.h"
#include "sysheap.h"
#include "mcp.h"
#include "cJSON.h"
#define EMOJI_DESCRIPTION_MAX 1024
#define EMOJI_PROPERTY_MAX 1536
#define EMOJI_PROP_FINAL_MAX 1700
extern int emoji_anim_get_loaded_count(void);
extern const char *emoji_anim_get_loaded_name(int index);
extern int emoji_anim_is_alias(int index);
static cJSON *set_emotion_list(const char *name)
{
char *description = psram_malloc(EMOJI_DESCRIPTION_MAX);
char *property_desc = psram_malloc(EMOJI_PROPERTY_MAX);
char *final_prop_desc = psram_malloc(EMOJI_PROP_FINAL_MAX);
cJSON *tool = NULL;
if (!description || !property_desc || !final_prop_desc) {
LOGE("Failed to allocate memory for emotion list");
goto cleanup;
}
memset(description, 0, EMOJI_DESCRIPTION_MAX);
memset(property_desc, 0, EMOJI_PROPERTY_MAX);
int desc_len = 0;
int prop_len = 0;
int first = 1;
for (int i = 0; i < emoji_anim_get_loaded_count(); i++) {
const char *name = emoji_anim_get_loaded_name(i);
if (name == NULL || name[0] == '\0') {
continue;
}
if (emoji_anim_is_alias(i)) {
continue;
}
if (!first) {
desc_len += snprintf(description + desc_len, EMOJI_DESCRIPTION_MAX - desc_len, ", ");
prop_len += snprintf(property_desc + prop_len, EMOJI_PROPERTY_MAX - prop_len, "");
}
first = 0;
desc_len += snprintf(description + desc_len, EMOJI_DESCRIPTION_MAX - desc_len, "%s", name);
prop_len += snprintf(property_desc + prop_len, EMOJI_PROPERTY_MAX - prop_len, "'%s'", name);
}
snprintf(final_prop_desc, EMOJI_PROP_FINAL_MAX, "表情类型,可以是 %s", property_desc);
tool = mcp_tool_list_info_create_default(name, description);
if (!tool) {
goto cleanup;
}
cJSON *properties = mcp_tool_info_properties_get(tool);
if (!properties) {
cJSON_Delete(tool);
tool = NULL;
goto cleanup;
}
mcp_tool_info_add_property(tool, "emotion", final_prop_desc, "string", true);
cleanup:
if (description) {
psram_free(description);
}
if (property_desc) {
psram_free(property_desc);
}
if (final_prop_desc) {
psram_free(final_prop_desc);
}
return tool;
}
static cJSON *set_emotion_call(const char *id, const char *name, cJSON *args)
{
const cJSON *emotion = mcp_tool_call_args_get(args, "emotion");
if (!emotion || emotion->valuestring == NULL) {
LOGE("mcp tool call args get emotion failed");
return NULL;
}
LOGI("mcp emoji received, name: %s", emotion->valuestring);
cJSON *result = mcp_tool_call_result_create(name);
if (!result) {
return NULL;
}
cJSON *content_array = cJSON_CreateArray();
cJSON *content_item = cJSON_CreateObject();
cJSON_AddStringToObject(content_item, "type", "text");
cJSON_AddStringToObject(content_item, "text", "已完成操作");
cJSON_AddItemToArray(content_array, content_item);
cJSON_AddItemToObject(result, "content", content_array);
cJSON_AddBoolToObject(result, "isError", false);
voice_msg_pub(VOICE_MSG_CLOUD_MCP_EMOJI, emotion->valuestring, strlen(emotion->valuestring) + 1);
return result;
}
MCP_TOOL_DEFINE(ls.built_in.set_emotion, set_emotion_list, set_emotion_call);

View File

@@ -0,0 +1,59 @@
#include <stdbool.h>
#include <string.h>
#include "lisa_log.h"
#include "voice_msg.h"
#include "service_image.h"
#include "mcp.h"
#include "cJSON.h"
static cJSON *show_loading_list(const char *name)
{
cJSON *tool = mcp_tool_list_info_create_default(name, "显示等待状态");
if (!tool) {
return NULL;
}
return tool;
}
static cJSON *show_loading_call(const char *id, const char *name, cJSON *args)
{
(void)id;
const char *loading_text = "请稍等...";
if (args && cJSON_IsObject(args)) {
cJSON *text_item = cJSON_GetObjectItem(args, "text");
if (text_item && cJSON_IsString(text_item) && text_item->valuestring) {
loading_text = text_item->valuestring;
}
}
if (loading_text) {
LOGI("loading_text=%s", loading_text);
} else {
LOGW("default loading_text");
}
voice_msg_pub(VOICE_MSG_CLOUD_MCP_LOADING, (void *)loading_text, strlen(loading_text) + 1);
service_image_waiting_start();
cJSON *result = mcp_tool_call_result_create(name);
if (!result) {
return NULL;
}
cJSON *content_array = cJSON_CreateArray();
cJSON *content_item = cJSON_CreateObject();
cJSON_AddStringToObject(content_item, "type", "text");
cJSON_AddStringToObject(content_item, "text", "已完成操作");
cJSON_AddItemToArray(content_array, content_item);
cJSON_AddItemToObject(result, "content", content_array);
cJSON_AddBoolToObject(result, "isError", false);
return result;
}
MCP_TOOL_DEFINE(ls.built_in.show_loading, show_loading_list, show_loading_call);

View File

@@ -0,0 +1,84 @@
#include <stdbool.h>
#include <string.h>
#include "cJSON.h"
#include "lisa_log.h"
#include "mcp.h"
#include "voice_msg.h"
#define TAG "mcp_model_persona_update"
static cJSON *model_persona_update_list(const char *name)
{
cJSON *tool = cJSON_CreateObject();
if (!tool) {
LOGE("Failed to create tool JSON object");
return NULL;
}
cJSON_AddStringToObject(tool, "name", name);
cJSON_AddStringToObject(tool, "description", "修改大模型设置或者更新助手人设配置");
cJSON *inputSchema = cJSON_CreateObject();
if (!inputSchema) {
cJSON_Delete(tool);
return NULL;
}
cJSON_AddStringToObject(inputSchema, "type", "object");
cJSON *properties = cJSON_CreateObject();
if (!properties) {
cJSON_Delete(inputSchema);
cJSON_Delete(tool);
return NULL;
}
cJSON_AddItemToObject(inputSchema, "properties", properties);
cJSON *required = cJSON_CreateArray();
if (!required) {
cJSON_Delete(tool);
return NULL;
}
cJSON_AddItemToObject(inputSchema, "required", required);
cJSON_AddItemToObject(tool, "inputSchema", inputSchema);
return tool;
}
static cJSON *model_persona_update_call(const char *id, const char *name, cJSON *args)
{
(void)args;
LOGI("Tool call received, tool=%s, id=%s", name ? name : "NULL", id ? id : "NULL");
voice_msg_pub(VOICE_MSG_CLOUD_OPEN_INFO, NULL, 0);
LOGI("VOICE_MSG_CLOUD_OPEN_INFO published");
cJSON *result = mcp_tool_call_result_create(name);
if (!result) {
return NULL;
}
cJSON *content_array = cJSON_CreateArray();
cJSON *content_item = cJSON_CreateObject();
if (!content_array || !content_item) {
if (content_array) {
cJSON_Delete(content_array);
}
cJSON_Delete(result);
return NULL;
}
cJSON_AddStringToObject(content_item, "type", "text");
cJSON_AddStringToObject(content_item, "text", "已完成操作");
cJSON_AddItemToArray(content_array, content_item);
cJSON_AddItemToObject(result, "content", content_array);
cJSON_AddBoolToObject(result, "isError", false);
LOGI("Tool call handled");
return result;
}
MCP_TOOL_DEFINE(ls.built_in.model_persona_update, model_persona_update_list, model_persona_update_call);

View File

@@ -0,0 +1,107 @@
#include <stdbool.h>
#include <string.h>
#include <stdio.h>
#include "lisa_log.h"
#include "voice_msg.h"
#include "sysheap.h"
#include "mcp.h"
#include "cJSON.h"
#define TAG "mcp_show_qrcode"
static cJSON *show_qrcode_list(const char *name)
{
cJSON *tool = cJSON_CreateObject();
if (!tool) {
LOGE("Failed to create tool JSON object");
return NULL;
}
cJSON_AddStringToObject(tool, "name", name);
cJSON_AddStringToObject(tool, "description", "Display QR code");
cJSON *inputSchema = cJSON_CreateObject();
cJSON_AddStringToObject(inputSchema, "type", "object");
cJSON *properties = cJSON_CreateObject();
cJSON *url_prop = cJSON_CreateObject();
cJSON_AddStringToObject(url_prop, "type", "string");
cJSON_AddStringToObject(url_prop, "description", "QR code URL");
cJSON_AddItemToObject(properties, "url", url_prop);
cJSON *message_prop = cJSON_CreateObject();
cJSON_AddStringToObject(message_prop, "type", "string");
cJSON_AddStringToObject(message_prop, "description", "Text message");
cJSON_AddItemToObject(properties, "message", message_prop);
cJSON *err_code_prop = cJSON_CreateObject();
cJSON_AddStringToObject(err_code_prop, "type", "string");
cJSON_AddStringToObject(err_code_prop, "description", "Error code");
cJSON_AddItemToObject(properties, "err_code", err_code_prop);
cJSON_AddItemToObject(inputSchema, "properties", properties);
cJSON *required = cJSON_CreateArray();
cJSON_AddItemToArray(required, cJSON_CreateString("url"));
cJSON_AddItemToObject(inputSchema, "required", required);
cJSON_AddItemToObject(tool, "inputSchema", inputSchema);
return tool;
}
static cJSON *show_qrcode_call(const char *id, const char *name, cJSON *args)
{
const cJSON *url_json = mcp_tool_call_args_get(args, "url");
const cJSON *message_json = mcp_tool_call_args_get(args, "message");
const cJSON *err_code_json = mcp_tool_call_args_get(args, "err_code");
if (!url_json || !url_json->valuestring) {
LOGE("MCP tool call args get url failed");
return NULL;
}
const char *url = url_json->valuestring;
const char *message = (message_json && message_json->valuestring) ? message_json->valuestring : "";
const char *err_code = (err_code_json && err_code_json->valuestring) ? err_code_json->valuestring : "";
LOGI("MCP show_qrcode received - url: %s, message: %s, err_code: %s", url, message, err_code);
cJSON *payload = cJSON_CreateObject();
if (!payload) {
LOGE("Failed to create payload JSON");
return NULL;
}
cJSON_AddStringToObject(payload, "url", url);
cJSON_AddStringToObject(payload, "message", message);
cJSON_AddStringToObject(payload, "err_code", err_code);
char *payload_str = cJSON_PrintUnformatted(payload);
if (payload_str) {
voice_msg_pub(VOICE_MSG_CLOUD_SHOW_QRCODE, payload_str, strlen(payload_str) + 1);
cJSON_free(payload_str);
}
cJSON_Delete(payload);
cJSON *result = mcp_tool_call_result_create(name);
if (!result) {
return NULL;
}
cJSON *content_array = cJSON_CreateArray();
cJSON *content_item = cJSON_CreateObject();
cJSON_AddStringToObject(content_item, "type", "text");
cJSON_AddStringToObject(content_item, "text", "Operation completed");
cJSON_AddItemToArray(content_array, content_item);
cJSON_AddItemToObject(result, "content", content_array);
cJSON_AddBoolToObject(result, "isError", false);
return result;
}
MCP_TOOL_DEFINE(ls.built_in.show_qrcode, show_qrcode_list, show_qrcode_call);

View File

@@ -0,0 +1,8 @@
listenai_library_named(voice_framework)
listenai_library_sources(
voice_msg.c
async_task.c
)
listenai_include_directories(.)

211
src/framework/async_task.c Normal file
View File

@@ -0,0 +1,211 @@
#include "async_task.h"
#include "lisa_log.h"
#include "lisa_mem.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include <string.h>
#define DEFAULT_TASK_NAME "task.async"
struct async_task {
char *name;
async_task_func_t func;
void *user_data;
async_task_complete_callback_t callback;
uint32_t stack_size;
uint32_t priority;
TaskHandle_t task_handle;
SemaphoreHandle_t mutex;
volatile bool should_stop;
volatile bool is_running;
};
static void async_task_thread(void *param)
{
async_task_t *task = (async_task_t *)param;
if (!task || !task->func) {
LOGE("Invalid task");
vTaskDelete(NULL);
return;
}
xSemaphoreTake(task->mutex, portMAX_DELAY);
task->is_running = true;
task->should_stop = false;
xSemaphoreGive(task->mutex);
LOGI("Async task started");
task->func(task->user_data, (bool *)&task->should_stop);
xSemaphoreTake(task->mutex, portMAX_DELAY);
bool was_stopped = task->should_stop;
task->is_running = false;
task->task_handle = NULL;
xSemaphoreGive(task->mutex);
LOGI("Async task completed, interrupted: %d", was_stopped);
if (task->callback) {
task->callback(task->user_data, !was_stopped, was_stopped);
}
async_task_destroy(task);
vTaskDelete(NULL);
}
async_task_t *async_task_create(const char *name, uint32_t stack_size, uint32_t priority, async_task_func_t func,
async_task_complete_callback_t callback, void *user_data)
{
if (!func) {
LOGE("Invalid function pointer");
return NULL;
}
async_task_t *task = lisa_mem_calloc(1, sizeof(async_task_t));
if (!task) {
LOGE("Failed to allocate task");
return NULL;
}
task->mutex = xSemaphoreCreateMutex();
if (!task->mutex) {
LOGE("Failed to create mutex");
lisa_mem_free(task);
return NULL;
}
const char *task_name = name ? name : DEFAULT_TASK_NAME;
task->name = lisa_mem_calloc(1, strlen(task_name) + 1);
if (!task->name) {
LOGE("Failed to allocate task name");
vSemaphoreDelete(task->mutex);
lisa_mem_free(task);
return NULL;
}
strcpy(task->name, task_name);
task->func = func;
task->user_data = user_data;
task->callback = callback;
task->stack_size = stack_size > 0 ? stack_size : 2048;
task->priority = priority;
task->task_handle = NULL;
task->should_stop = false;
task->is_running = false;
LOGI("Async task created: %s", task->name);
return task;
}
int async_task_start(async_task_t *task)
{
if (!task) {
LOGE("Invalid task");
return -1;
}
xSemaphoreTake(task->mutex, portMAX_DELAY);
if (task->is_running) {
LOGW("Task is already running");
xSemaphoreGive(task->mutex);
return -1;
}
BaseType_t ret =
xTaskCreate(async_task_thread, task->name, task->stack_size, task, task->priority, &task->task_handle);
xSemaphoreGive(task->mutex);
if (ret != pdPASS) {
LOGE("Failed to create task thread");
return -1;
}
LOGI("Async task started");
return 0;
}
int async_task_stop(async_task_t *task)
{
if (!task) {
LOGE("Invalid task");
return -1;
}
xSemaphoreTake(task->mutex, portMAX_DELAY);
if (!task->is_running) {
LOGW("Task is not running");
xSemaphoreGive(task->mutex);
return -1;
}
task->should_stop = true;
LOGI("Stop signal sent to task");
xSemaphoreGive(task->mutex);
return 0;
}
bool async_task_is_running(async_task_t *task)
{
if (!task) {
return false;
}
xSemaphoreTake(task->mutex, portMAX_DELAY);
bool running = task->is_running;
xSemaphoreGive(task->mutex);
return running;
}
void async_task_destroy(async_task_t *task)
{
if (!task) {
return;
}
if (task->is_running) {
LOGW("Destroying running task, stopping first");
async_task_stop(task);
TickType_t start_tick = xTaskGetTickCount();
const uint32_t timeout_ms = 5000;
bool timeout_logged = false;
while (task->is_running) {
vTaskDelay(pdMS_TO_TICKS(10));
if (!timeout_logged) {
TickType_t current_tick = xTaskGetTickCount();
uint32_t elapsed_ms = (current_tick - start_tick) * portTICK_PERIOD_MS;
if (elapsed_ms >= timeout_ms) {
LOGE("Task %s (func: %p) failed to stop after %u ms, still waiting...",
task->name ? task->name : "unknown", task->func, timeout_ms);
timeout_logged = true;
}
}
}
}
if (task->mutex) {
vSemaphoreDelete(task->mutex);
}
if (task->name) {
lisa_mem_free(task->name);
}
lisa_mem_free(task);
LOGI("Async task destroyed");
}

View File

@@ -0,0 +1,27 @@
#ifndef ASYNC_TASK_H
#define ASYNC_TASK_H
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct async_task async_task_t;
typedef void (*async_task_func_t)(void *user_data, bool *should_stop);
typedef void (*async_task_complete_callback_t)(void *user_data, bool completed, bool interrupted);
async_task_t *async_task_create(const char *name, uint32_t stack_size, uint32_t priority, async_task_func_t func,
async_task_complete_callback_t callback, void *user_data);
int async_task_start(async_task_t *task);
int async_task_stop(async_task_t *task);
bool async_task_is_running(async_task_t *task);
void async_task_destroy(async_task_t *task);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,67 @@
#ifndef IMAGE_URL_UTILS_H
#define IMAGE_URL_UTILS_H
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#ifdef __cplusplus
extern "C" {
#endif
#define IMAGE_OSS_DEFAULT_WIDTH (148)
static inline int build_image_display_url_with_width(const char *input_url,
char *output_url,
size_t output_size,
unsigned int width)
{
char oss_process[128] = {0};
if (!input_url || !output_url || output_size == 0 || width == 0) {
return -1;
}
int p = snprintf(oss_process,
sizeof(oss_process),
"x-oss-process=image/resize,m_pad,w_%u,limit_0,color_000000/quality,q_100/format,jpg",
width);
if (p <= 0 || (size_t)p >= sizeof(oss_process)) {
return -1;
}
const char *q = strchr(input_url, '?');
if (!q) {
int n = snprintf(output_url, output_size, "%s?%s", input_url, oss_process);
return (n > 0 && (size_t)n < output_size) ? 0 : -1;
}
if (strstr(q + 1, "x-oss-process=") != NULL) {
size_t base_len = (size_t)(q - input_url);
if (base_len + 1 >= output_size) {
return -1;
}
memcpy(output_url, input_url, base_len);
output_url[base_len] = '\0';
int n = snprintf(output_url + base_len, output_size - base_len, "?%s", oss_process);
return (n > 0 && (size_t)n < (output_size - base_len)) ? 0 : -1;
}
int n = snprintf(output_url, output_size, "%s&%s", input_url, oss_process);
return (n > 0 && (size_t)n < output_size) ? 0 : -1;
}
static inline int build_image_display_url(const char *input_url, char *output_url, size_t output_size)
{
return build_image_display_url_with_width(input_url,
output_url,
output_size,
IMAGE_OSS_DEFAULT_WIDTH);
}
#ifdef __cplusplus
}
#endif
#endif

225
src/framework/voice_msg.c Normal file
View File

@@ -0,0 +1,225 @@
#include <assert.h>
#include <stdint.h>
#include <string.h>
#include <stddef.h>
#include <stdio.h>
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "sys_init.h"
#include "lisa_log.h"
#include "lisa_typedef.h"
#include "ebus/ebus.h"
#include "voice_msg.h"
#include "sysheap.h"
#define TAG "voice.msg"
static ebus_chn_t *voice_default_chn = NULL;
struct voice_invoke {
voice_invoke_worker_t worker;
uint32_t data_len;
uint8_t data[0];
};
struct voice_invoke_sync {
voice_invoke_sync_worker_t worker;
struct voice_invoke_rsp *rsp;
uint32_t data_len;
uint8_t data[0];
};
static SemaphoreHandle_t voice_invoke_sync_resp_sem = NULL;
static SemaphoreHandle_t voice_invoke_sync_lock = NULL;
static void voice_invoke_sumbit_msg_received(void *unused, uint32_t evt, void *data, uint32_t len, void *user_data)
{
struct voice_invoke *invoke = (struct voice_invoke *)data;
if (invoke == NULL) {
return;
}
LOGI("voice invoke, worker: %p, data: %p, len: %d", invoke->worker, invoke->data, invoke->data_len);
if (invoke->worker) {
invoke->worker(invoke->data, invoke->data_len);
}
}
static void voice_invoke_sync_sumbit_msg_received(void *unused, uint32_t evt, void *data, uint32_t len, void *user_data)
{
struct voice_invoke_sync *invoke = (struct voice_invoke_sync *)data;
if (invoke == NULL) {
return;
}
LOGI("voice invoke, worker: %p, data: %p, len: %d", invoke->worker, invoke->data, invoke->data_len);
if (invoke->worker) {
invoke->worker(invoke->data, invoke->data_len, invoke->rsp);
}
xSemaphoreGive(voice_invoke_sync_resp_sem);
}
int voice_invoke(voice_invoke_worker_t worker, void *data, uint32_t len)
{
if (worker == NULL) {
return -1;
}
struct voice_invoke *invoke = psram_malloc(sizeof(*invoke) + len);
if (invoke == NULL) {
return -1;
}
invoke->worker = worker;
invoke->data_len = len;
if (len) {
memcpy(invoke->data, data, len);
}
if (voice_msg_pub(VOICE_MSG_INVOKE_SUBMIT, invoke, sizeof(*invoke) + len) != 0) {
psram_free(invoke);
return -1;
}
return 0;
}
int voice_invoke_sync(voice_invoke_sync_worker_t worker, void *data, uint32_t len, struct voice_invoke_rsp *rsp,
uint32_t timeout)
{
if (worker == NULL || rsp == NULL) {
return -1;
}
struct voice_invoke_sync *invoke_sync = psram_malloc(sizeof(*invoke_sync) + len);
if (invoke_sync == NULL) {
return -1;
}
invoke_sync->worker = worker;
invoke_sync->data_len = len;
invoke_sync->rsp = rsp;
if (len) {
memcpy(invoke_sync->data, data, len);
}
BaseType_t r = xSemaphoreTake(voice_invoke_sync_lock, timeout);
if (r != pdPASS) {
psram_free(invoke_sync);
return -1;
}
xSemaphoreTake(voice_invoke_sync_resp_sem, 0);
voice_msg_pub(VOICE_MSG_INVOKE_SYNC_SUBMIT, invoke_sync, sizeof(*invoke_sync) + len);
r = xSemaphoreTake(voice_invoke_sync_resp_sem, timeout);
xSemaphoreGive(voice_invoke_sync_lock);
return r == pdPASS ? 0 : -1;
}
int voice_msg_init(void)
{
int r;
ebus_handle_t *bus;
ebus_chn_t *chn;
r = ebus_init();
LOGI("voice_msg_init, ebus_init ret:%d", r);
assert(r == 0);
bus = ebus_create_async("voice.ebus", 128, 32 * 1024, 6);
LOGI("voice_msg_init, ebus_create ret:%p", bus);
assert(bus);
voice_default_chn = ebus_chn_create_attach(bus, "voice.ebus.default");
LOGI("voice_msg_init, ebus_chn_create_attach ret:%p", voice_default_chn);
assert(voice_default_chn);
voice_invoke_sync_lock = xSemaphoreCreateMutex();
assert(voice_invoke_sync_lock);
voice_invoke_sync_resp_sem = xSemaphoreCreateBinary();
assert(voice_invoke_sync_resp_sem);
voice_msg_sub(VOICE_MSG_INVOKE_SUBMIT, voice_invoke_sumbit_msg_received, NULL);
voice_msg_sub(VOICE_MSG_INVOKE_SYNC_SUBMIT, voice_invoke_sync_sumbit_msg_received, NULL);
return 0;
}
int voice_msg_pub(uint32_t evt, void *data, uint32_t len)
{
return voice_msg_pub_timeout(evt, data, len, LISA_OS_WAIT_FOREVER);
}
int voice_msg_pub_timeout(uint32_t evt, void *data, uint32_t len, int32_t timeout_ms)
{
ebus_chn_t *chn;
int r;
chn = voice_default_chn;
if (chn == NULL) {
LOGE("voice msg chn is null, evt:%d", evt);
return -1;
}
r = ebus_message_pub_async_timeout(chn, evt, data, len, timeout_ms);
if (r != 0) {
LOGE("voice msg pub failed, evt:%d", evt);
return -1;
}
return 0;
}
int voice_msg_sub(uint32_t evt, voice_msg_cb_t cb, void *user_data)
{
ebus_chn_t *chn;
int r;
chn = voice_default_chn;
if (chn == NULL) {
LOGE("voice msg chn is null, evt:%d", evt);
return -1;
}
r = ebus_message_subscribe(chn, EBUS_SUBSCRIBER_TYPE_ASYNC, evt, (ebus_chn_cb_t)cb, user_data);
if (r != 0) {
LOGE("voice msg sub failed, evt:%d", evt);
return -1;
}
return 0;
}
int voice_msg_unsub(uint32_t evt, voice_msg_cb_t cb)
{
ebus_chn_t *chn;
int r;
chn = voice_default_chn;
if (chn == NULL) {
LOGE("voice msg chn is null, evt:%d", evt);
return -1;
}
r = ebus_message_unsubscribe(chn, (ebus_chn_cb_t)cb);
if (r != 0) {
LOGE("voice msg unsub failed, evt:%d", evt);
return -1;
}
return 0;
}
SYS_INIT(voice_msg_init, SYS_INIT_LEVEL_PRE_APPLICATION, 30);

244
src/framework/voice_msg.h Normal file
View File

@@ -0,0 +1,244 @@
#ifndef __VOICE_EVT_H__
#define __VOICE_EVT_H__
#include <stdint.h>
#include "voice_msg_structure.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Domain */
enum {
VOICE_DOMAIN_APPLICATION = 0, /* 由应用程序自行定义 */
VOICE_DOMAIN_SYSTEM,
VOICE_DOMAIN_INVOKE,
VOICE_DOMAIN_POWER,
VOICE_DOMAIN_PLATFORM,
VOICE_DOMAIN_WIFI,
VOICE_DOMAIN_BLE,
VOICE_DOMAIN_BUTTON,
VOICE_DOMAIN_WAKEUP,
VOICE_DOMAIN_CLOUD,
VOICE_DOMAIN_PLAYER,
VOICE_DOMAIN_RECORD,
VOICE_DOMAIN_ALARM,
VOICE_DOMAIN_RECOGNIZED,
VOICE_DOMAIN_PLAY_CONTROL,
#ifdef CONFIG_OTA
VOICE_DOMAIN_OTA,
#endif
VOICE_DOMAIN_MAX,
};
#define VOICE_MSG_ID(domain, evt) (((domain) << 16) | (evt))
#define VOICE_MSG_DOMAIN(msg) ((msg) >> 16)
#define VOICE_MSG_EVT(msg) ((msg) & 0xffff)
typedef enum {
VOICE_MSG_BUTTON_ACTION_PRESS_DOWN = 0,
VOICE_MSG_BUTTON_ACTION_CLICK,
VOICE_MSG_BUTTON_ACTION_DOUBLE_CLICK,
VOICE_MSG_BUTTON_ACTION_TRIPLE_CLICK,
VOICE_MSG_BUTTON_ACTION_QUADRUPLE_CLICK,
VOICE_MSG_BUTTON_ACTION_QUINTUPLE_CLICK,
VOICE_MSG_BUTTON_ACTION_SEXTUPLE_CLICK,
VOICE_MSG_BUTTON_ACTION_SEPTUPLE_CLICK,
VOICE_MSG_BUTTON_ACTION_REPEAT_CLICK,
VOICE_MSG_BUTTON_ACTION_SHORT_START,
VOICE_MSG_BUTTON_ACTION_SHORT_UP,
VOICE_MSG_BUTTON_ACTION_LONG_START,
VOICE_MSG_BUTTON_ACTION_LONG_UP,
VOICE_MSG_BUTTON_ACTION_LONG_HOLD,
VOICE_MSG_BUTTON_ACTION_LONG_HOLD_UP,
VOICE_MSG_BUTTON_ACTION_UNKNOWN,
} voice_msg_button_action_t;
typedef struct {
uint8_t button_id;
voice_msg_button_action_t action;
} voice_msg_button_evt_t;
struct voice_msg_audio_item {
uint8_t playable;
char id[32];
char name[64];
} __attribute__((packed));
struct voice_msg_audio_items {
uint32_t cnt;
struct voice_msg_audio_item items[0];
} __attribute__((packed));
struct voice_invoke_rsp {
int err;
uint8_t data[0];
};
typedef void (*voice_invoke_worker_t)(void *, uint32_t);
typedef void (*voice_invoke_sync_worker_t)(void *, uint32_t, struct voice_invoke_rsp *);
enum {
/* System事件 */
VOICE_MSG_SYSTEM_START = VOICE_MSG_ID(VOICE_DOMAIN_SYSTEM, 0),
VOICE_MSG_SYSTEM_NETWORK_PROBE_SUCCESS,
VOICE_MSG_SYSTEM_NETWORK_PROBE_FAIL,
VOICE_MSG_SYSTEM_MAX,
/* WORKQ */
VOICE_MSG_INVOKE_START = VOICE_MSG_ID(VOICE_DOMAIN_INVOKE, 0),
/*
* 注意: 应用层不要监听INVOKE相关
* 如需要代理执行, 请调用voice_invoke接口
*/
VOICE_MSG_INVOKE_SUBMIT,
VOICE_MSG_INVOKE_SYNC_SUBMIT,
/* Power事件 */
VOICE_MSG_POWER_START = VOICE_MSG_ID(VOICE_DOMAIN_POWER, 0),
VOICE_MSG_POWER_BATTERY_UPDATE,
VOICE_MSG_POWER_MAX,
/* Platform事件 */
VOICE_MSG_PLATFORM_START = VOICE_MSG_ID(VOICE_DOMAIN_PLATFORM, 0),
VOICE_MSG_PLATFORM_READY = VOICE_MSG_PLATFORM_START,
VOICE_MSG_PLATFORM_MAX,
#ifdef CONFIG_OTA
/* OTA事件 */
VOICE_MSG_OTA_START = VOICE_MSG_ID(VOICE_DOMAIN_OTA, 0),
VOICE_MSG_OTA_CHECKING,
VOICE_MSG_OTA_UPDATING,
VOICE_MSG_OTA_SUCCESSED,
VOICE_MSG_OTA_FAILED,
VOICE_MSG_OTA_UP_TO_DATE,
VOICE_MSG_OTA_MAX,
#endif
/* WIFI事件 */
VOICE_MSG_WIFI_START = VOICE_MSG_ID(VOICE_DOMAIN_WIFI, 0),
VOICE_MSG_WIFI_CONNECTED = VOICE_MSG_WIFI_START,
VOICE_MSG_WIFI_DISCONNECTED,
VOICE_MSG_WIFI_SCAN_DONE,
VOICE_MSG_WIFI_IP_GOT,
VOICE_MSG_WIFI_MAX,
/* BLE事件 */
VOICE_MSG_BLE_START = VOICE_MSG_ID(VOICE_DOMAIN_BLE, 0),
VOICE_MSG_BLE_CONNECT_DONE = VOICE_MSG_BLE_START,
VOICE_MSG_BLE_DISCONNECT_DONE,
VOICE_MSG_BLE_SCAN_DONE,
VOICE_MSG_BLE_MAX,
VOICE_MSG_BUTTON_START = VOICE_MSG_ID(VOICE_DOMAIN_BUTTON, 0),
VOICE_MSG_BUTTON_CHANGE = VOICE_MSG_BUTTON_START,
VOICE_MSG_BUTTON_IMAGE_RECOGNITION,
VOICE_MSG_BUTTON_MAX,
/* WAKEUP事件 */
VOICE_MSG_WAKEUP_START = VOICE_MSG_ID(VOICE_DOMAIN_WAKEUP, 0),
VOICE_MSG_WAKEUP_KEYWORD = VOICE_MSG_WAKEUP_START,
VOICE_MSG_WAKEUP_COMMAND,
VOICE_MSG_WAKEUP_RECOGNIZED_START,
VOICE_MSG_WAKEUP_RECOGNIZED_FINISH,
VOICE_MSG_WAKEUP_BUTTON,
VOICE_MSG_WAKEUP_BUTTON_START,
VOICE_MSG_WAKEUP_BUTTON_STOP,
VOICE_MSG_WAKEUP_MAX,
/* LSChat事件 */
VOICE_MSG_CLOUD_START = VOICE_MSG_ID(VOICE_DOMAIN_CLOUD, 0),
VOICE_MSG_CLOUD_CONNECTING = VOICE_MSG_CLOUD_START,
VOICE_MSG_CLOUD_CONNECTED,
VOICE_MSG_CLOUD_DISCONNECTED,
VOICE_MSG_CLOUD_RECONNECTING,
VOICE_MSG_CLOUD_ERROR,
VOICE_MSG_CLOUD_CLOUD_AUTH_FAILED,
VOICE_MSG_CLOUD_CLOUD_AUTH_SUCCESS,
VOICE_MSG_CLOUD_SESSION_STARTING,
VOICE_MSG_CLOUD_SESSION_STARTED,
VOICE_MSG_CLOUD_SESSION_FINISHED,
VOICE_MSG_CLOUD_TTS_URL,
VOICE_MSG_CLOUD_IAT_START,
VOICE_MSG_CLOUD_IAT_UPDATE,
VOICE_MSG_CLOUD_IAT_END,
VOICE_MSG_CLOUD_TTS_TEXT_START,
VOICE_MSG_CLOUD_TTS_TEXT_UPDATE,
VOICE_MSG_CLOUD_TTS_TEXT_END,
VOICE_MSG_CLOUD_MCP,
VOICE_MSG_CLOUD_MCP_CALL_RESP, /* char * */
VOICE_MSG_CLOUD_AUDIO_URL, /* char * */
VOICE_MSG_CLOUD_AUDIO_ITEM, /* struct voice_msg_audio_items * */
VOICE_MSG_CLOUD_RECOGNIZED_COMMAND,
VOICE_MSG_CLOUD_RECOGNIZED_COMMAND_WITH_TIMER,/*定时控制命令*/
VOICE_MSG_CLOUD_VAD,
VOICE_MSG_CLOUD_EMOJI,
VOICE_MSG_CLOUD_MCP_EMOJI,
VOICE_MSG_CLOUD_MCP_LOADING,
VOICE_MSG_CLOUD_MCP_CHAT_EXIT,
VOICE_MSG_CLOUD_MCP_IMAGE_RECOGNITION, /* char * */
VOICE_MSG_CLOUD_MCP_IMAGE_URL, /* char * */
VOICE_MSG_CLOUD_ROLE_SETTING_QRCODE,
VOICE_MSG_CLOUD_VPR_INFO,
VOICE_MSG_CLOUD_VPR_FEATURE,
VOICE_MSG_CLOUD_STANDBY_TEXTS, /* char * banner json */
VOICE_MSG_CLOUD_SHOW_QRCODE, /* char * json with url, message, err_code */
VOICE_MSG_CLOUD_OPEN_INFO,
VOICE_MSG_CLOUD_MAX,
/* Player事件 */
VOICE_MSG_PLAYER_START = VOICE_MSG_ID(VOICE_DOMAIN_PLAYER, 0),
VOICE_MSG_PLAYER_TTS_PLAYING,
VOICE_MSG_PLAYER_TTS_PAUSED,
VOICE_MSG_PLAYER_TTS_STOPED,
VOICE_MSG_PLAYER_MAX,
/* Record事件 */
VOICE_MSG_RECORD_START = VOICE_MSG_ID(VOICE_DOMAIN_RECORD, 0),
VOICE_MSG_RECORD_MAX,
/* Alarm事件 */
VOICE_MSG_ALARM_START = VOICE_MSG_ID(VOICE_DOMAIN_ALARM, 0),
VOICE_MSG_ALARM_TRIGGER,
VOICE_MSG_ALARM_CREATE,
VOICE_MSG_ALARM_DELETE,
VOICE_MSG_ALARM_QUERY,
VOICE_MSG_ALARM_MAX,
/*命令词处理事件*/
VOICE_MSG_RECOGNIZED_START = VOICE_MSG_ID(VOICE_DOMAIN_RECOGNIZED, 0),
VOICE_MSG_RECOGNIZED_ONLINE_COMMAND = VOICE_MSG_RECOGNIZED_START,
VOICE_MSG_RECOGNIZED_OFFLINE_COMMAND,
VOICE_MSG_RECOGNIZED_SESSION_FINISH,
VOICE_MSG_RECOGNIZED_MAX,
/* PlayControl事件 */
VOICE_MSG_PLAY_CONTROL_START = VOICE_MSG_ID(VOICE_DOMAIN_PLAY_CONTROL, 0),
VOICE_MSG_PLAY_CONTROL_PLAY,
VOICE_MSG_PLAY_CONTROL_PAUSE,
VOICE_MSG_PLAY_CONTROL_NEXT,
VOICE_MSG_PLAY_CONTROL_PREVIOUS,
VOICE_MSG_PLAY_CONTROL_REPLAY,
VOICE_MSG_PLAY_CONTROL_MAX,
};
struct voice_msg_wakeup {
char keyword[64];
};
typedef void (*voice_msg_cb_t)(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data);
int voice_msg_init(void);
int voice_msg_pub(uint32_t msg_id, void *data, uint32_t len);
int voice_msg_pub_timeout(uint32_t msg_id, void *data, uint32_t len, int32_t timeout_ms);
int voice_msg_sub(uint32_t msg_id, voice_msg_cb_t cb, void *user_data);
int voice_msg_unsub(uint32_t msg_id, voice_msg_cb_t cb);
int voice_invoke(voice_invoke_worker_t worker, void *data, uint32_t len);
int voice_invoke_sync(voice_invoke_sync_worker_t worker, void *data, uint32_t len, struct voice_invoke_rsp *rsp,
uint32_t timeout);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,42 @@
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct{
char id[40];
char name[64];
}voice_msg_cloud_mcp_context_t;
typedef struct{
voice_msg_cloud_mcp_context_t context;
char command[32];
}voice_msg_cloud_recognized_command_t;
typedef struct{
voice_msg_cloud_mcp_context_t context;
char cmd[64];
char time[32];
}voice_msg_cloud_recognition_cmd_timer_t;
typedef enum {
VOICE_MSG_BATTERY_STATUS_NO_BATTERY = 0,
VOICE_MSG_BATTERY_STATUS_NOT_CONNECT,
VOICE_MSG_BATTERY_STATUS_CHARGING,
VOICE_MSG_BATTERY_STATUS_CHARGE_DONE,
VOICE_MSG_BATTERY_STATUS_UNKNOWN,
} voice_msg_battery_status_t;
typedef struct {
uint8_t level; /* 0-100 */
uint8_t status; /* voice_msg_battery_status_t */
} voice_msg_battery_info_t;
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,35 @@
# =============================================================================
# Voice Assistant Application Build Configuration
# =============================================================================
listenai_include_directories(./)
add_subdirectory(alarm)
add_subdirectory(audio)
add_subdirectory(ble)
add_subdirectory(libc)
add_subdirectory(battery)
add_subdirectory(config)
add_subdirectory(system)
add_subdirectory(display)
# add_subdirectory(led)
add_subdirectory(button)
# add_subdirectory(fs)
add_subdirectory(kv)
# add_subdirectory(flash)
# add_subdirectory(video)
if(CONFIG_MIDDLEWARE_TONE)
add_subdirectory(tone)
endif()
add_subdirectory(player)
add_subdirectory(rcmd_router)
add_subdirectory(usb)
add_subdirectory(wifi)
add_subdirectory(workqueue)
add_subdirectory(romfs)
add_subdirectory(inih)
add_subdirectory(vendor_storage)
add_subdirectory(power)
# add_subdirectory(sdemmc)
add_subdirectory(ota)

12
src/middleware/Kconfig Normal file
View File

@@ -0,0 +1,12 @@
menu "application middleware"
rsource "audio/Kconfig"
rsource "alarm/Kconfig"
rsource "battery/Kconfig"
rsource "button/Kconfig"
rsource "power/Kconfig"
rsource "tone/Kconfig"
rsource "usb/Kconfig"
rsource "vendor_storage/Kconfig"
rsource "ota/Kconfig"
endmenu

View File

@@ -0,0 +1,19 @@
listenai_library_named(aiui_alarm)
listenai_library_sources(
alarm_aiui.c
alarm.c
alarm_ring.c
alarm_store.c
alarm_next.c
)
listenai_include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
)
target_include_directories(
${LISTENAI_CURRENT_LIBRARY}
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/private
)

View File

@@ -0,0 +1,9 @@
menu "alarm"
config ALARM_RING_DURATION_MS
int "Alarm ring stop timeout (ms)"
default 60000
help
Maximum duration for alarm ringing before auto-stop.
endmenu

View File

@@ -0,0 +1,448 @@
#define TAG "alarm"
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include "stdint.h"
#include "stddef.h"
#include "stdbool.h"
#include "string.h"
#include "utlist.h"
#include "time.h"
#include "math.h"
#include "lisa_typedef.h"
#include "lisa_mem.h"
#include "lisa_timer.h"
#include "lisa_log.h"
#include "lisa_mutex.h"
#include "lisa_kv.h"
#include "alarm_next.h"
#include "alarm.h"
#include "listen_system.h"
#include "sys/time.h"
#include "alarm_store.h"
#include "voice_msg.h"
static struct ls_alarm *g_alarm_head = NULL;
static lisa_timer_t *g_alarm_timer = NULL;
static lisa_mutex_t *g_alarm_mutex = NULL;
static ls_alarm_user_callback_t g_alarm_user_callback = NULL;
#define ALARM_TIMER_MAX_MS (24U * 60U * 60U * 1000U)
static void ls_alarm_timer_callback_handle(struct lisa_timer *timer);
static int ls_alarm_delete(struct ls_alarm *alarm);
/* ==================== 工具函数 ==================== */
struct ls_alarm *ls_alarm_get(void)
{
return g_alarm_head;
}
int ls_alarm_count_get(void)
{
int cnt = 0;
struct ls_alarm *node;
DL_COUNT(g_alarm_head, node, cnt);
return cnt;
}
static int ls_alarm_cmp(struct ls_alarm *a1, struct ls_alarm *a2)
{
return a1->timestamp - a2->timestamp;
}
static void ls_alarm_print_timestamp(uint64_t timestamp)
{
struct tm _tm = {0};
struct tm *ret_tm;
ret_tm = localtime_r(&timestamp, &_tm);
LISA_LOGI(TAG, "timestamp:%lld, local time:%04d年%02d月%02d日%02d时%02d分%02d秒", timestamp,
ret_tm->tm_year + 1970, ret_tm->tm_mon + 1, ret_tm->tm_mday, ret_tm->tm_hour, ret_tm->tm_min, ret_tm->tm_sec);
}
static int get_network_time(struct tm *network_time, time_t *network_timestamp)
{
struct timeval tv;
/* Prefer SNTP-synced calendar; fallback to gettimeofday */
if (ls_sys_get_time(&tv) != 0) {
if (gettimeofday(&tv, NULL) < 0) {
return -1;
}
}
/* Use the same conversion helpers as listen_system to avoid platform mismatches */
long int ts = (long int)tv.tv_sec;
if (network_timestamp) {
*network_timestamp = (time_t)ts;
}
if (network_time) {
ls_sys_get_tmtime(&ts, network_time);
}
return 0;
}
static bool ls_alarm_timestamp_is_valid(uint64_t timestamp)
{
struct timeval tm;
gettimeofday(&tm, NULL);
bool valid = timestamp > tm.tv_sec;
if (!valid) {
LISA_LOGI(TAG,
"ls alarm timestamp is invalid, current timestamp:%lld, alarm timestamp:%lld",
timestamp, tm.tv_sec);
}
return valid;
}
/* ==================== 删除闹钟 ==================== */
static int ls_alarm_delete(struct ls_alarm *alarm)
{
int err = 0;
if (alarm == NULL) {
return -1;
}
DL_DELETE(g_alarm_head, alarm);
lisa_mem_free(alarm);
return err;
}
int ls_alarm_delete_by_timestamp(uint64_t timestamp)
{
struct ls_alarm *node;
struct ls_alarm *temp;
int err = -1;
temp = lisa_mem_alloc(sizeof(struct ls_alarm));
if (temp == NULL) {
return -1;
}
temp->timestamp = timestamp;
lisa_mutex_lock(g_alarm_mutex, LISA_OS_WAIT_FOREVER);
DL_SEARCH(g_alarm_head, node, temp, ls_alarm_cmp);
if (node) {
LISA_LOGI(TAG, "ls alarm delete, timestamp:%lld", timestamp);
err = ls_alarm_delete(node);
} else {
LISA_LOGI(TAG, "ls alarm delete not found, timestamp:%lld", timestamp);
}
lisa_mutex_unlock(g_alarm_mutex);
lisa_mem_free(temp);
return err;
}
int ls_alarm_clear_all(void)
{
lisa_mutex_lock(g_alarm_mutex, LISA_OS_WAIT_FOREVER);
struct ls_alarm *node, *tmp;
DL_FOREACH_SAFE(g_alarm_head, node, tmp) {
LISA_LOGI(TAG, "Clearing alarm with timestamp:%lld", node->timestamp);
alarm_store_delete_obj_by_timestamp(node->timestamp);
DL_DELETE(g_alarm_head, node);
lisa_mem_free(node);
}
g_alarm_head = NULL;
if (g_alarm_timer != NULL) {
lisa_timer_stop(g_alarm_timer);
}
lisa_mutex_unlock(g_alarm_mutex);
LISA_LOGI(TAG, "All alarms cleared");
return 0;
}
/* ==================== 闹钟触发回调 ==================== */
static void ls_alarm_timer_start(uint64_t timeout_s)
{
LISA_LOGI(TAG, "ls_alarm_timer_start, timeout_s:%llu s\r\n", (unsigned long long)timeout_s);
timeout_s = timeout_s < 1 ? 1 : timeout_s;
uint64_t timeout_ms64 = (uint64_t)timeout_s * 1000U;
uint32_t timeout_ms = timeout_ms64 > ALARM_TIMER_MAX_MS ? ALARM_TIMER_MAX_MS : (uint32_t)timeout_ms64;
if (timeout_ms64 > ALARM_TIMER_MAX_MS) {
LISA_LOGI(TAG, "alarm timer capped: requested=%llus capped=%ums",
(unsigned long long)timeout_s, timeout_ms);
}
if (g_alarm_timer == NULL) {
g_alarm_timer = lisa_timer_create(timeout_ms, ls_alarm_timer_callback_handle, NULL);
}
// 如果有更短时长的闹钟则更新更短时长
if (!lisa_timer_isactive(g_alarm_timer)) {
timeout_ms = timeout_ms < 2000 ? 2000 : timeout_ms;
lisa_timer_change_period(g_alarm_timer, timeout_ms);
lisa_timer_start(g_alarm_timer);
LISA_LOGI(TAG, "start new alarm timer, timeout:%dms\r\n", timeout_ms);
} else {
uint32_t remain = lisa_timer_remain_time(g_alarm_timer);
if (remain > timeout_ms) {
lisa_timer_change_period(g_alarm_timer, timeout_ms);
lisa_timer_start(g_alarm_timer);
LISA_LOGI(TAG, "alarm timer remain:%dms, new period:%dms\r\n", remain, timeout_ms);
}
}
}
static void ls_alarm_timer_callback_handle(struct lisa_timer *timer)
{
struct ls_alarm *node, *temp;
struct tm now;
time_t now_ts;
get_network_time(&now, &now_ts);
LISA_LOGI(TAG, "ls_alarm_timer_callback_handle, current time: %lld", now_ts);
node = ls_alarm_get();
// 基于闹钟类型(单次/循环)处理闹钟对象
while (node != NULL) {
if (now_ts >= node->timestamp) {
uint64_t fired_ts = node->timestamp;
if (node->cb) {
node->cb(node, NULL);
}
// 1. 通过 alarm_store_find_by_id 查询闹钟对象
const alarm_object_t *alarm_obj = alarm_store_find_by_id(node->timestamp);
if (alarm_obj) {
LISA_LOGI(TAG, "alarm fired, id %llu type=%u cal=%u",
(unsigned long long)alarm_obj->alarm_id,
(unsigned)alarm_obj->trigger.type,
(unsigned)alarm_obj->calendar);
if (alarm_obj->trigger.type == ALARM_TRIG_ONCE) {
// 单次闹钟,直接删除
alarm_store_delete_obj_by_timestamp(node->timestamp);
if (ls_alarm_delete(node) == 0) {
voice_msg_pub(VOICE_MSG_ALARM_DELETE, &fired_ts, sizeof(fired_ts));
}
} else {
// 循环闹钟,计算下次触发时间
alarm_object_t next_alarm = *alarm_obj;
uint64_t next_ts = alarm_calc_next_trigger(&next_alarm, now_ts);
LISA_LOGI(TAG, "alarm next_ts=%llu (now=%llu)",
(unsigned long long)next_ts, (unsigned long long)now_ts);
if (next_ts > now_ts) {
next_alarm.alarm_id = next_ts;
int create_ret = alarm_store_create_obj(&next_alarm, NULL, 0);
ls_alarm_insert_by_timestamp(next_alarm.alarm_id, next_alarm.text);
LISA_LOGI(TAG, "alarm create_ret=%d next_id=%llu",
create_ret, (unsigned long long)next_alarm.alarm_id);
if (create_ret != 0) {
LISA_LOGE(TAG, "failed to create next alarm, next_ts=%llu",
(unsigned long long)next_ts);
}
// 3. 删除旧实例
alarm_store_delete_obj_by_timestamp(node->timestamp);
ls_alarm_delete(node);
} else {
// 没有下次触发,直接删除
alarm_store_delete_obj_by_timestamp(node->timestamp);
if (ls_alarm_delete(node) == 0) {
voice_msg_pub(VOICE_MSG_ALARM_DELETE, &fired_ts, sizeof(fired_ts));
}
}
}
} else {
// 查不到对象,直接删
if (ls_alarm_delete(node) == 0) {
voice_msg_pub(VOICE_MSG_ALARM_DELETE, &fired_ts, sizeof(fired_ts));
}
}
} else {
break;
}
node = ls_alarm_get();
}
node = ls_alarm_get();
if (node) {
LISA_LOGI(TAG, "ls_alarm_timer_callback_handle, node->timestamp:%lld", node->timestamp);
if (node->timestamp > now_ts) {
uint64_t next_timer_period = (uint64_t)node->timestamp - (uint64_t)now_ts;
ls_alarm_timer_start(next_timer_period);
} else {
LISA_LOGW(TAG, "ls_alarm_timer_callback_handle, node->timestamp invalid");
}
} else {
LISA_LOGI(TAG, "ls_alarm_timer_callback_handle, node is null");
}
}
/* ==================== 新增闹钟 ==================== */
static int ls_alarm_update(struct ls_alarm *alarm, struct ls_alarm *new)
{
if (alarm == NULL || new == NULL) {
return -1;
}
memset(alarm->text, 0, sizeof(alarm->text));
strcat(alarm->text, new->text);
int ret = alarm_store_update_obj_by_timestamp(new->timestamp, new->text);
return ret;
}
static int ls_alarm_insert(struct ls_alarm *alarm)
{
struct ls_alarm *node;
struct ls_alarm *temp;
if (alarm == NULL) {
return -1;
}
temp = lisa_mem_alloc(sizeof(struct ls_alarm));
if (temp == NULL) {
return -1;
}
temp->timestamp = alarm->timestamp;
lisa_mutex_lock(g_alarm_mutex, LISA_OS_WAIT_FOREVER);
// 检查新增闹钟是否已存在
DL_SEARCH(g_alarm_head, node, temp, ls_alarm_cmp);
if (node) {
LISA_LOGI(TAG, "alarm is already exist, timestamp:%lld, old text:%s new text:%s\r\n",
node->timestamp, node->text, alarm->text);
// 新增闹钟有提示文本时,再更新闹钟文本
int err = 0;
if (alarm->text[0] != 0) {
err = ls_alarm_update(node, alarm);
}
lisa_mutex_unlock(g_alarm_mutex);
lisa_mem_free(temp);
return err;
}
lisa_mem_free(temp);
LISA_LOGI(TAG, "insert new alarm, timestamp:%lld, text:%s\r\n", alarm->timestamp, alarm->text);
ls_alarm_print_timestamp(alarm->timestamp);
// 插入新闹钟
DL_INSERT_INORDER(g_alarm_head, alarm, ls_alarm_cmp);
// 创建定时器(基于当前时间)并打印剩余触发时间
struct tm network_time = {0};
time_t network_time_ts = 0;
get_network_time(&network_time, &network_time_ts);
time_t current_timestamp = network_time_ts;
/* 使用最早的闹钟(链表头)启动定时器 */
node = ls_alarm_get();
if (node) {
uint64_t remain_s = (node->timestamp > current_timestamp) ? (node->timestamp - current_timestamp) : 0;
ls_alarm_timer_start(remain_s);
long long hh = (long long)(remain_s / 3600);
long long mm = (long long)((remain_s % 3600) / 60);
long long ss = (long long)(remain_s % 60);
LISA_LOGI(TAG, "ls alarm insert, next:%lld, now:%lld, remain:%llds (%02lld:%02lld:%02lld)",
node->timestamp, (long long)current_timestamp, (long long)remain_s, hh, mm, ss);
} else {
LISA_LOGW(TAG, "ls alarm insert, node is null after insert");
}
LISA_LOGI(TAG, "ls alarm insert, alarm->timestamp:%lld", alarm->timestamp);
lisa_mutex_unlock(g_alarm_mutex);
int alarm_cnt = ls_alarm_count_get();
LISA_LOGI(TAG, "current alarm cnt:%d", alarm_cnt);
return 0;
}
static int ls_alarm_insert_inner_by_timestamp(uint64_t timestamp, const uint8_t *text, ls_alarm_callbacks_t cb)
{
// 检查时间戳是否过期
if (!ls_alarm_timestamp_is_valid(timestamp)) {
return -1;
}
// 构造 ls_alarm 对象
struct ls_alarm *alarm = lisa_mem_alloc(sizeof(struct ls_alarm));
if (alarm == NULL) {
return -1;
}
memset(alarm, 0, sizeof(struct ls_alarm));
alarm->cb = cb;
alarm->timestamp = timestamp;
if (text != NULL) {
int cpy_len = strlen(text);
if (cpy_len > (sizeof(alarm->text) - 1)) {
cpy_len = sizeof(alarm->text) - 1;
}
memcpy(alarm->text, text, cpy_len);
}
// 插入闹钟链表
if (ls_alarm_insert(alarm) != 0) {
lisa_mem_free(alarm);
return -1;
}
return 0;
}
static void ls_alarm_test_callback(struct ls_alarm *alarm, void *data)
{
if (g_alarm_user_callback) {
g_alarm_user_callback(alarm->timestamp, alarm->text);
}
}
int ls_alarm_insert_by_timestamp(uint64_t timestamp, const uint8_t *text)
{
return ls_alarm_insert_inner_by_timestamp(timestamp, text, ls_alarm_test_callback);
}
/* ==================== 初始化 ==================== */
void ls_alarm_init(ls_alarm_user_callback_t cb)
{
if (g_alarm_mutex == NULL) {
g_alarm_mutex = lisa_mutex_create();
}
g_alarm_user_callback = cb;
alarm_store_init();
int alarm_cnt = ls_alarm_count_get();
LISA_LOGI(TAG, "alarm init complete, count:%d", alarm_cnt);
}

View File

@@ -0,0 +1,34 @@
#ifndef __ALARM_H__
#define __ALARM_H__
#include "stdint.h"
#define LS_ALARM_MAX_COUNT (20)
struct ls_alarm;
typedef void (*ls_alarm_callbacks_t)(struct ls_alarm *alarm, void *data);
typedef void (*ls_alarm_user_callback_t)(uint64_t timestamp, const uint8_t *text);
#define LS_ALARM_TEXT_MAX_LEN 128
struct ls_alarm {
struct ls_alarm *prev;
struct ls_alarm *next;
uint64_t timestamp;
ls_alarm_callbacks_t cb;
uint8_t text[LS_ALARM_TEXT_MAX_LEN];
};
struct ls_alarm_nvs {
uint64_t timestamp;
uint32_t text_len;
uint8_t text[LS_ALARM_TEXT_MAX_LEN];
};
void ls_alarm_init(ls_alarm_user_callback_t cb);
int ls_alarm_insert_by_timestamp(uint64_t timestamp, const uint8_t *text);
int ls_alarm_delete_by_timestamp(uint64_t timestamp);
struct ls_alarm *ls_alarm_get(void);
int ls_alarm_count_get(void);
#endif

View File

@@ -0,0 +1,300 @@
#include "string.h"
#include "stdint.h"
#include "cJSON.h"
#include <time.h>
#include "alarm.h"
#include "alarm_aiui.h"
#include <stdbool.h>
#include "lisa_mem.h"
#include "lisa_log.h"
#include "sys_init.h"
#include "sys/time.h"
#define TAG "alarm_aiui"
enum {
AIUI_ALARM_INTENT_UNKNOWN = 0,
AIUI_ALARM_INTENT_CREATE,
AIUI_ALARM_INTENT_CANCEL,
AIUI_ALARM_INTENT_VIEW,
AIUI_ALARM_INTENT_MAX,
};
enum {
AIUI_ALARM_SLOTS_ITEM_TYPE_ERROR = -1,
AIUI_ALARM_SLOTS_ITEM_TYPE_UNKNOWN = 0,
AIUI_ALARM_SLOTS_ITEM_TYPE_DATETIME,
AIUI_ALARM_SLOTS_ITEM_TYPE_CONTENT,
AIUI_ALARM_SLOTS_ITEM_TYPE_REPEAT,
AIUI_ALARM_SLOTS_ITEM_TYPE_ALL,
};
typedef void (*alarm_aiui_intent_handle_t)(uint64_t timestamp, const uint8_t *text);
static void alarm_aiui_create_intent_handle(uint64_t timestamp, const uint8_t *text);
static void alarm_aiui_cancel_intent_handle(uint64_t timestamp, const uint8_t *text);
static void alarm_aiui_unknown_intent_handle(uint64_t timestamp, const uint8_t *text);
extern void listen_client_tts(const char *text);
const uint8_t *aiui_alarm_intent_string[AIUI_ALARM_INTENT_MAX] = {
[AIUI_ALARM_INTENT_CREATE] = "CREATE",
[AIUI_ALARM_INTENT_CANCEL] = "CANCEL",
};
const alarm_aiui_intent_handle_t alarm_aiui_intent_handles[AIUI_ALARM_INTENT_MAX] = {
[AIUI_ALARM_INTENT_UNKNOWN] = alarm_aiui_unknown_intent_handle,
[AIUI_ALARM_INTENT_CREATE] = alarm_aiui_create_intent_handle,
[AIUI_ALARM_INTENT_CANCEL] = alarm_aiui_cancel_intent_handle,
};
static void alarm_aiui_unknown_intent_handle(uint64_t timestamp, const uint8_t *text)
{
LISA_LOGD(TAG, "unsupported intent");
}
static void alarm_aiui_create_intent_handle(uint64_t timestamp, const uint8_t *text)
{
struct timeval tm;
gettimeofday(&tm, NULL);
int cnt = ls_alarm_count_get();
LISA_LOGD(TAG, "alarm_aiui_create_intent_handle, curr time:%lld, timestamp:%lld,cnt:%d", tm.tv_sec,
timestamp, cnt);
if (timestamp < tm.tv_sec) {
LISA_LOGD(TAG, "invalid timestamp");
return;
}
ls_alarm_insert_by_timestamp(timestamp, text);
// assist_controller_trigger_event(CONTROLLER_EVENT_STATE_ALARM_ADD_ITEM, &timestamp, sizeof(timestamp));
}
static void alarm_aiui_cancel_intent_handle(uint64_t timestamp, const uint8_t *text)
{
int err = ls_alarm_delete_by_timestamp(timestamp);
if (err) {
LISA_LOGD(TAG, "delete alarm err:%d", err);
}
else{
// assist_controller_trigger_event(CONTROLLER_EVENT_STATE_ALARM_DELETE_ITEM, &timestamp, sizeof(timestamp));
}
}
static void alarm_aiui_intent_handle_without_precise_time(int intent, int type, uint64_t timestamp)
{
LISA_LOGD(TAG, "alarm aiui intent:%d type:%d without precise time", intent, type);
}
static void alarm_aiui_intent_handle_dispatch(
int intent, int type, const char *datetime_string, const uint8_t *text)
{
uint64_t timestamp;
char *p;
bool precise_time = true;
LISA_LOGD(TAG, "aiui alarm intent dispatch, intent:%d, type:%d, datetime:%s, content:%s",
intent, type, datetime_string, text);
if (intent >= AIUI_ALARM_INTENT_MAX) {
return;
}
if (intent == AIUI_ALARM_INTENT_UNKNOWN) {
alarm_aiui_unknown_intent_handle(0, NULL);
return;
}
/* 2023-12-28T03:00:00, 2023-12-28 */
p = strstr(datetime_string, "T");
precise_time &= p != NULL;
/* 2023-12-28T03:00:00/2023-12-28T05:00:00 */
p = strstr(datetime_string, "/");
precise_time &= p == NULL;
struct tm tm = {0};
extern char *strptime (const char *__restrict __s,
const char *__restrict __fmt, struct tm *__tp);
strptime(datetime_string, "%Y-%m-%dT%H:%M:%S", &tm);
timestamp = mktime(&tm);
timestamp -= 60 * 60 * 8;
LISA_LOGD(TAG, "precise time:%d", precise_time);
if (!precise_time) {
/* no precise time */
alarm_aiui_intent_handle_without_precise_time(intent, type, timestamp);
return;
}
alarm_aiui_intent_handle_t handle = alarm_aiui_intent_handles[intent];
if (handle) {
handle(timestamp, text);
}
}
static int alarm_aiui_intent_convert(const uint8_t *intent_string)
{
int intent = AIUI_ALARM_INTENT_UNKNOWN;
int i;
for (i = 0; i < AIUI_ALARM_INTENT_MAX; i++) {
if (aiui_alarm_intent_string[i] != NULL
&& strcmp(aiui_alarm_intent_string[i], intent_string) == 0) {
return i;
}
}
return AIUI_ALARM_INTENT_UNKNOWN;
}
static int alarm_aiui_slots_item_parse(cJSON *slots_item, char **out)
{
*out = NULL;
if (slots_item == NULL) {
return AIUI_ALARM_SLOTS_ITEM_TYPE_ERROR;
}
cJSON *slots_item_name = cJSON_GetObjectItem(slots_item, "name");
if (slots_item_name == NULL) {
return AIUI_ALARM_SLOTS_ITEM_TYPE_ERROR;
}
LISA_LOGD(TAG, "slots item name:%s", slots_item_name->valuestring);
if (strcmp(slots_item_name->valuestring, "datetime") == 0) {
cJSON *normValue = cJSON_GetObjectItem(slots_item, "normValue");
if (normValue == NULL) {
return AIUI_ALARM_SLOTS_ITEM_TYPE_ERROR;
}
cJSON *normValueJson = cJSON_Parse(normValue->valuestring);
if (normValueJson == NULL) {
return AIUI_ALARM_SLOTS_ITEM_TYPE_ERROR;
}
cJSON *suggestDatetime = cJSON_GetObjectItem(normValueJson, "suggestDatetime");
if (suggestDatetime == NULL) {
cJSON_Delete(normValueJson);
return AIUI_ALARM_SLOTS_ITEM_TYPE_ERROR;
}
/* cpy value */
int tmp_len = strlen(suggestDatetime->valuestring) + 1;
char *tmp = lisa_mem_alloc(tmp_len);
if (tmp) {
memset(tmp, 0, tmp_len);
strcat(tmp, suggestDatetime->valuestring);
*out = tmp;
}
cJSON_Delete(normValueJson);
return AIUI_ALARM_SLOTS_ITEM_TYPE_DATETIME;
} else if (strcmp(slots_item_name->valuestring, "content") == 0) {
cJSON *value = cJSON_GetObjectItem(slots_item, "value");
int tmp_len = strlen(value->valuestring) + 1;
char *tmp = lisa_mem_alloc(tmp_len);
if (tmp) {
memset(tmp, 0, tmp_len);
strcat(tmp, value->valuestring);
*out = tmp;
}
return AIUI_ALARM_SLOTS_ITEM_TYPE_CONTENT;
} else if (strcmp(slots_item_name->valuestring, "property") == 0) {
cJSON *value = cJSON_GetObjectItem(slots_item, "value");
if (strcmp(value->valuestring, "all") == 0) {
return AIUI_ALARM_SLOTS_ITEM_TYPE_ALL;
}
} else if (strcmp(slots_item_name->valuestring, "repeat") == 0) {
return AIUI_ALARM_SLOTS_ITEM_TYPE_REPEAT;
} else if (strstr(slots_item_name->valuestring, "repeat") != NULL) {
return AIUI_ALARM_SLOTS_ITEM_TYPE_REPEAT;
}
return AIUI_ALARM_SLOTS_ITEM_TYPE_UNKNOWN;
}
int alarm_aiui_intent_process(cJSON *intent_root)
{
int alarm_intent;
cJSON *semantic = cJSON_GetObjectItem(intent_root, "semantic");
if (semantic == NULL) {
return -1;
}
int size = cJSON_GetArraySize(semantic);
if (size <= 0) {
return -1;
}
cJSON *sema_item = cJSON_GetArrayItem(semantic, 0);
if (sema_item == NULL) {
return -1;
};
cJSON *intent = cJSON_GetObjectItem(sema_item, "intent");
if (intent == NULL) {
return -1;
}
alarm_intent = alarm_aiui_intent_convert(intent->valuestring);
cJSON *slots = cJSON_GetObjectItem(sema_item, "slots");
if (slots == NULL) {
return -1;
}
size = cJSON_GetArraySize(slots);
if (size <= 0) {
return -1;
}
char *datetime_string = NULL;
char *content_string = NULL;
int alarm_type = AIUI_ALARM_SLOTS_ITEM_TYPE_UNKNOWN;
for (int i = 0; i < size; i++) {
LISA_LOGD(TAG, "item size:%d, curr:%d", size, i);
cJSON *slots_item = cJSON_GetArrayItem(slots, i);
char *temp = NULL;
alarm_type = alarm_aiui_slots_item_parse(slots_item, &temp);
if (alarm_type == AIUI_ALARM_SLOTS_ITEM_TYPE_DATETIME) {
datetime_string = temp;
} else if (alarm_type == AIUI_ALARM_SLOTS_ITEM_TYPE_CONTENT) {
content_string = temp;
} else if (alarm_type == AIUI_ALARM_SLOTS_ITEM_TYPE_REPEAT) {
/* not support */
alarm_type = AIUI_ALARM_SLOTS_ITEM_TYPE_UNKNOWN;
LISA_LOGD(TAG, "slots break iteration, curr index:%d", i);
break;
}
}
alarm_aiui_intent_handle_dispatch(alarm_intent, alarm_type, datetime_string, content_string);
lisa_mem_free(datetime_string);
lisa_mem_free(content_string);
return 0;
}
int alarm_aiui_init(alarm_aiui_user_callback_t cb)
{
ls_alarm_init((ls_alarm_user_callback_t)cb);
LISA_LOGI(TAG, "Alarm module initialized (test alarms will be created later)");
return 0;
}
/* 闹钟模块自动初始化包装函数 */
static int alarm_aiui_auto_init(void)
{
LISA_LOGI(TAG, "Auto initializing alarm module with test alarms");
return alarm_aiui_init(NULL);
}
/* 闹钟模块自动初始化优先级80在应用层之前 */
// SYS_INIT(alarm_aiui_auto_init, SYS_INIT_LEVEL_PRE_APPLICATION, 90);

View File

@@ -0,0 +1,13 @@
#ifndef __ALARM_AIUI_H__
#define __ALARM_AIUI_H__
#include "stdint.h"
#include "cJSON.h"
int alarm_aiui_intent_process(cJSON *intent_root);
typedef void (*alarm_aiui_user_callback_t)(uint64_t timestamp, const uint8_t *text);
int alarm_aiui_init(alarm_aiui_user_callback_t cb);
int ls_alarm_count_get(void);
#endif

View File

@@ -0,0 +1,528 @@
#include "alarm_next.h"
#include <string.h>
#include <stdio.h>
#include <stdbool.h>
#include "alarm_store.h"
#include "lisa_log.h"
#include "lisa_http.h"
#include "lisa_kv.h"
#include "lisa_mem.h"
#include "cJSON.h"
#include "kv_user.h"
#include "lsc.h"
#define TAG "alarm_next"
static int alarm_update_trigger_from_timestamp(alarm_object_t *alarm, uint64_t timestamp)
{
if (!alarm || timestamp == 0) {
return -1;
}
time_t ts = (time_t)timestamp;
struct tm tmv;
memset(&tmv, 0, sizeof(tmv));
if (!localtime_r(&ts, &tmv)) {
return -1;
}
alarm->trigger.year = (uint16_t)(tmv.tm_year + 1970);
alarm->trigger.month = (uint8_t)(tmv.tm_mon + 1);
alarm->trigger.day = (uint8_t)tmv.tm_mday;
alarm->trigger.hour = (uint8_t)tmv.tm_hour;
alarm->trigger.minute = (uint8_t)tmv.tm_min;
alarm->trigger.second = (uint8_t)tmv.tm_sec;
return 0;
}
static char *s_listenai_date_headers = NULL;
static void *listenai_date_headers_cb(void)
{
return (void *)s_listenai_date_headers;
}
static void listenai_date_json_on_data(lisa_http_data_t *data)
{
cJSON **json = (cJSON **)data->user;
if (!json || !data || !data->buf) {
return;
}
if (*json) {
cJSON_Delete(*json);
*json = NULL;
}
*json = cJSON_ParseWithLength((const char *)data->buf, data->len);
}
static cJSON *listenai_date_transition_json(const char *type, const char *date_name, int *out_err)
{
if (out_err) *out_err = -1;
if (!type || !date_name) return NULL;
// 请求地址
char url[128] = {0};
const char *host_suffix = "";
int device_mode = 0;
if (lisa_kv_get_int(KV_KEY_DEVICE_MODE, &device_mode) != 0) {
device_mode = 0;
}
snprintf(url, sizeof(url), "http://39.108.110.91:4080/v1/date/transition");
// 请求头
const char *header_fmt = "Content-Type: application/json\r\nAuthorization: Bearer %s";
const char *token = get_lsc_jwt_token();
if (token == NULL) {
if (out_err) *out_err = -2;
return NULL;
}
size_t header_len = strlen(header_fmt) + strlen(token) + 1;
if (s_listenai_date_headers) {
lisa_mem_free(s_listenai_date_headers);
s_listenai_date_headers = NULL;
}
s_listenai_date_headers = lisa_mem_calloc(1, header_len);
if (!s_listenai_date_headers) {
if (out_err) *out_err = -3;
return NULL;
}
snprintf(s_listenai_date_headers, header_len, header_fmt, token);
// 请求体
char body[128];
int body_len = snprintf(body, sizeof(body), "{\"date_name\":\"%s\",\"type\":\"%s\"}", date_name, type);
if (body_len < 0 || body_len >= (int)sizeof(body)) {
lisa_mem_free(s_listenai_date_headers);
s_listenai_date_headers = NULL;
if (out_err) *out_err = -4;
return NULL;
}
cJSON *json = NULL;
lisa_http_request_t req = {
.method = LISA_HTTP_POST,
.url = (uint8_t *)url,
.timeout = 10,
.body = body,
.body_len = body_len,
.headers = (uint8_t *)listenai_date_headers_cb,
.on_data = listenai_date_json_on_data,
.user = &json,
};
lisa_http_t *http = lisa_http_init(&req);
if (!http) {
lisa_mem_free(s_listenai_date_headers);
s_listenai_date_headers = NULL;
if (out_err) *out_err = -9;
return NULL;
}
lisa_http_err_e err = lisa_http_perform(http);
lisa_http_cleanup(http);
if (s_listenai_date_headers) {
lisa_mem_free(s_listenai_date_headers);
s_listenai_date_headers = NULL;
}
if (err != LISA_HTTP_OK) {
if (json) cJSON_Delete(json);
if (out_err) *out_err = -10;
return NULL;
}
if (!json && out_err) {
*out_err = -5;
}
return json;
}
int listenai_date_transition(const char *type, const char *date_name, char *out_date, size_t out_len)
{
if (!type || !date_name) return -1;
if (out_date == NULL) {
if (out_len != 0) return -1;
} else if (out_len < 11) {
return -1;
}
int err = 0;
cJSON *json = listenai_date_transition_json(type, date_name, &err);
if (!json) {
return err;
}
char *json_str = cJSON_PrintUnformatted(json);
if (json_str) {
LISA_LOGI(TAG, "date transition response: type=%s name=%s resp=%s", type, date_name, json_str);
cJSON_free(json_str);
}
cJSON *code = cJSON_GetObjectItem(json, "code");
if (cJSON_IsNumber(code) && code->valueint != 0) {
cJSON_Delete(json);
return -6;
}
cJSON *date = cJSON_GetObjectItem(json, "date");
if (!cJSON_IsString(date) || date->valuestring == NULL) {
cJSON_Delete(json);
return -7;
}
if (out_date && out_len > 0) {
strncpy(out_date, date->valuestring, out_len - 1);
out_date[out_len - 1] = '\0';
}
cJSON_Delete(json);
return 0;
}
int listenai_date_is_workday(const char *date_name, bool *out_is_work_day)
{
if (!date_name || !out_is_work_day) return -1;
int err = 0;
cJSON *json = listenai_date_transition_json("workday", date_name, &err);
if (!json) {
return err;
}
char *json_str = cJSON_PrintUnformatted(json);
if (json_str) {
LISA_LOGI(TAG, "date workday response: name=%s resp=%s", date_name, json_str);
cJSON_free(json_str);
}
cJSON *code = cJSON_GetObjectItem(json, "code");
if (cJSON_IsNumber(code) && code->valueint != 0) {
cJSON_Delete(json);
return -6;
}
cJSON *is_work_day = cJSON_GetObjectItem(json, "is_work_day");
if (cJSON_IsBool(is_work_day)) {
*out_is_work_day = cJSON_IsTrue(is_work_day);
} else if (cJSON_IsNumber(is_work_day)) {
*out_is_work_day = is_work_day->valueint != 0;
} else {
cJSON_Delete(json);
return -7;
}
cJSON_Delete(json);
return 0;
}
static int days_in_month(int year, int month)
{
static const uint8_t days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (month < 1 || month > 12) {
return 30;
}
if (month == 2) {
bool leap = ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
return leap ? 29 : 28;
}
return days[month - 1];
}
static uint64_t alarm_calc_next_trigger_lunar(alarm_object_t *alarm, time_t now_ts, uint64_t base)
{
if (!alarm) {
return 0;
}
switch (alarm->trigger.type) {
case ALARM_TRIG_DAILY:
{
uint64_t next = base;
while (next <= (uint64_t)now_ts) {
next += 24 * 60 * 60;
}
alarm_update_trigger_from_timestamp(alarm, next);
return next;
}
case ALARM_TRIG_WEEKLY:
{
uint64_t next = base;
while (next <= (uint64_t)now_ts) {
next += 7 * 24 * 60 * 60;
}
alarm_update_trigger_from_timestamp(alarm, next);
return next;
}
case ALARM_TRIG_MONTHLY:
{
if (alarm->trigger.lunar_month == 0 || alarm->trigger.lunar_day == 0) {
LISA_LOGE(TAG, "lunar monthly missing lunar month/day");
return 0;
}
uint8_t next_lunar_month = alarm->trigger.lunar_month;
alarm_object_t temp = *alarm;
for (int i = 0; i < 24; i++) {
next_lunar_month += 1;
if (next_lunar_month > 12) {
next_lunar_month = 1;
}
char lunar_date[16] = {0};
snprintf(lunar_date, sizeof(lunar_date), "%02u-%02u",
next_lunar_month, alarm->trigger.lunar_day);
char gregorian_date[16] = {0};
if (listenai_date_transition("lunar", lunar_date, gregorian_date, sizeof(gregorian_date)) != 0) {
LISA_LOGE(TAG, "lunar transition failed, lunar=%s", lunar_date);
return 0;
}
unsigned int year = 0, month = 0, day = 0;
if (sscanf(gregorian_date, "%u-%u-%u", &year, &month, &day) != 3) {
LISA_LOGE(TAG, "lunar transition parse failed, date=%s", gregorian_date);
return 0;
}
temp.trigger.year = (uint16_t)year;
temp.trigger.month = (uint8_t)month;
temp.trigger.day = (uint8_t)day;
temp.trigger.lunar_month = next_lunar_month;
temp.trigger.lunar_day = alarm->trigger.lunar_day;
uint64_t next = alarm_obj_to_timestamp(&temp);
if (next > (uint64_t)now_ts) {
*alarm = temp;
alarm_update_trigger_from_timestamp(alarm, next);
return next;
}
}
LISA_LOGW(TAG, "lunar monthly next not in future, now=%lld", (long long)now_ts);
return 0;
}
case ALARM_TRIG_YEARLY:
{
if (alarm->trigger.lunar_month == 0 || alarm->trigger.lunar_day == 0) {
LISA_LOGE(TAG, "lunar yearly missing lunar month/day");
return 0;
}
char lunar_date[16] = {0};
snprintf(lunar_date, sizeof(lunar_date), "%02u-%02u",
alarm->trigger.lunar_month, alarm->trigger.lunar_day);
char gregorian_date[16] = {0};
if (listenai_date_transition("lunar", lunar_date, gregorian_date, sizeof(gregorian_date)) != 0) {
LISA_LOGE(TAG, "lunar transition failed, lunar=%s", lunar_date);
return 0;
}
unsigned int year = 0, month = 0, day = 0;
if (sscanf(gregorian_date, "%u-%u-%u", &year, &month, &day) != 3) {
LISA_LOGE(TAG, "lunar transition parse failed, date=%s", gregorian_date);
return 0;
}
alarm->trigger.year = (uint16_t)year;
alarm->trigger.month = (uint8_t)month;
alarm->trigger.day = (uint8_t)day;
uint64_t next = alarm_obj_to_timestamp(alarm);
if (next <= (uint64_t)now_ts) {
LISA_LOGW(TAG, "lunar yearly next not in future, date=%s now=%lld",
gregorian_date, (long long)now_ts);
return 0;
}
alarm_update_trigger_from_timestamp(alarm, next);
return next;
}
default:
LISA_LOGW(TAG, "lunar trigger type not supported: %d", alarm->trigger.type);
return 0;
}
}
uint64_t alarm_calc_next_trigger(alarm_object_t *alarm, time_t now_ts)
{
if (!alarm) {
return 0;
}
uint64_t base = alarm_obj_to_timestamp(alarm);
if (base == 0) {
return 0;
}
if (alarm->calendar == ALARM_CAL_LUNAR) {
return alarm_calc_next_trigger_lunar(alarm, now_ts, base);
}
switch (alarm->trigger.type) {
case ALARM_TRIG_ONCE:
return 0;
case ALARM_TRIG_DAILY:
{
uint64_t next = base;
while (next <= (uint64_t)now_ts) {
next += 24 * 60 * 60;
}
alarm_update_trigger_from_timestamp(alarm, next);
return next;
}
case ALARM_TRIG_WEEKLY:
{
uint64_t next = base;
while (next <= (uint64_t)now_ts) {
next += 7 * 24 * 60 * 60;
}
alarm_update_trigger_from_timestamp(alarm, next);
return next;
}
case ALARM_TRIG_WORKDAY:
{
uint64_t next = base;
while (1) {
next += 24 * 60 * 60;
alarm->trigger.day += 1;
char date_str[16] = {0};
snprintf(date_str, sizeof(date_str), "%02d-%02d", alarm->trigger.month, alarm->trigger.day);
bool is_work_day = true;
if (listenai_date_is_workday(date_str, &is_work_day) != 0) {
LISA_LOGE(TAG, "workday transition failed, date=%s", date_str);
return 0;
}
if (is_work_day) {
break;
}
}
alarm_update_trigger_from_timestamp(alarm, next);
return next;
}
case ALARM_TRIG_WEEKEND:
{
uint64_t next = base;
struct tm tmv;
const uint64_t day_sec = 24 * 60 * 60;
while (1) {
time_t ts = (time_t)next;
if (!localtime_r(&ts, &tmv)) {
next += day_sec;
continue;
}
int wd = tmv.tm_wday; // 0=Sun .. 6=Sat
bool is_weekend = (wd == 0 || wd == 6);
if (next > (uint64_t)now_ts && is_weekend) {
break;
}
int step_days = 1;
if (wd == 6) {
step_days = 1; // Sat -> Sun
} else if (wd == 0) {
step_days = 6; // Sun -> next Sat
} else {
step_days = 6 - wd; // Mon..Fri -> Sat
}
next += (uint64_t)step_days * day_sec;
}
alarm_update_trigger_from_timestamp(alarm, next);
return next;
}
case ALARM_TRIG_MONTHLY:
{
uint8_t desired_day = alarm->trigger.day;
if (alarm->trigger.day_of_month > 0) {
desired_day = alarm->trigger.day_of_month;
}
alarm_object_t temp = *alarm;
uint64_t next = base;
while (next <= (uint64_t)now_ts) {
temp.trigger.month += 1;
if (temp.trigger.month > 12) {
temp.trigger.month = 1;
temp.trigger.year += 1;
}
int max_day = days_in_month(temp.trigger.year, temp.trigger.month);
if (desired_day > (uint8_t)max_day) {
temp.trigger.day = (uint8_t)max_day;
} else {
temp.trigger.day = desired_day;
}
next = alarm_obj_to_timestamp(&temp);
}
*alarm = temp;
alarm_update_trigger_from_timestamp(alarm, next);
return next;
}
case ALARM_TRIG_YEARLY:
{
uint8_t desired_day = alarm->trigger.day;
if (alarm->trigger.day_of_month > 0) {
desired_day = alarm->trigger.day_of_month;
}
if (alarm->calendar == ALARM_CAL_LUNAR) {
char lunar_date[16] = {0};
snprintf(lunar_date, sizeof(lunar_date), "%02u-%02u",
alarm->trigger.lunar_month, alarm->trigger.lunar_day);
char gregorian_date[16] = {0};
if (listenai_date_transition("lunar", lunar_date, gregorian_date, sizeof(gregorian_date)) != 0) {
LISA_LOGE(TAG, "lunar transition failed, lunar=%s", lunar_date);
return 0;
}
unsigned int year = 0, month = 0, day = 0;
if (sscanf(gregorian_date, "%u-%u-%u", &year, &month, &day) != 3) {
LISA_LOGE(TAG, "lunar transition parse failed, date=%s", gregorian_date);
return 0;
}
alarm->trigger.year = (uint16_t)year;
alarm->trigger.month = (uint8_t)month;
alarm->trigger.day = (uint8_t)day;
uint64_t next = alarm_obj_to_timestamp(alarm);
if (next <= (uint64_t)now_ts) {
LISA_LOGW(TAG, "lunar next not in future, date=%s now=%lld",
gregorian_date, (long long)now_ts);
return 0;
}
alarm_update_trigger_from_timestamp(alarm, next);
return next;
}
alarm_object_t temp = *alarm;
uint64_t next = base;
while (next <= (uint64_t)now_ts) {
temp.trigger.year += 1;
int max_day = days_in_month(temp.trigger.year, temp.trigger.month);
if (desired_day > (uint8_t)max_day) {
temp.trigger.day = (uint8_t)max_day;
} else {
temp.trigger.day = desired_day;
}
next = alarm_obj_to_timestamp(&temp);
}
*alarm = temp;
alarm_update_trigger_from_timestamp(alarm, next);
return next;
}
case ALARM_TRIG_HOLIDAY:
{
char gregorian_date[16] = {0};
if (listenai_date_transition("holiday", alarm->trigger.holiday_name,
gregorian_date, sizeof(gregorian_date)) != 0) {
LISA_LOGE(TAG, "holiday transition failed, name=%s", alarm->trigger.holiday_name);
return 0;
}
unsigned int year = 0, month = 0, day = 0;
if (sscanf(gregorian_date, "%u-%u-%u", &year, &month, &day) != 3) {
LISA_LOGE(TAG, "holiday transition parse failed, date=%s", gregorian_date);
return 0;
}
alarm->trigger.year = (uint16_t)year;
alarm->trigger.month = (uint8_t)month;
alarm->trigger.day = (uint8_t)day;
uint64_t next = alarm_obj_to_timestamp(alarm);
if (next <= (uint64_t)now_ts) {
LISA_LOGW(TAG, "holiday next not in future, date=%s now=%lld",
gregorian_date, (long long)now_ts);
return 0;
}
alarm_update_trigger_from_timestamp(alarm, next);
return next;
}
default:
return 0;
}
}

View File

@@ -0,0 +1,37 @@
#ifndef __ALARM_NEXT_H__
#define __ALARM_NEXT_H__
#include <stdint.h>
#include <time.h>
#include "alarm_store.h"
/**
* @brief 计算循环闹钟的下一次触发时间,并更新 alarm 的年月日/时分秒
*
* @param alarm 闹钟对象(会被更新触发日期)
* @param now_ts 当前时间戳(秒)
* @return 下一次触发时间戳,失败/无下次返回 0
*/
uint64_t alarm_calc_next_trigger(alarm_object_t *alarm, time_t now_ts);
/**
* @brief 调用 ListenAI 日期转换服务,将节日名/农历日期转换为公历日期
*
* @param type 类型:"holiday" 或 "lunar"
* @param date_name 节日名或农历日期(如 "圣诞节" / "01-01"
* @param out_date 输出缓冲区,返回格式 "YYYY-MM-DD"
* @param out_len 输出缓冲区长度(需至少 11 字节含结尾 \0
* @return 0 成功;非 0 失败
*/
int listenai_date_transition(const char *type, const char *date_name, char *out_date, size_t out_len);
/**
* @brief 查询指定日期是否工作日
*
* @param date_name 日期(格式 "MM-DD"
* @param out_is_work_day 输出是否工作日
* @return 0 成功;非 0 失败
*/
int listenai_date_is_workday(const char *date_name, bool *out_is_work_day);
#endif

View File

@@ -0,0 +1,155 @@
#define TAG "alarm_ring"
#include "alarm_ring.h"
#include <string.h>
#include "lisa_timer.h"
#include "lisa_log.h"
#include "voice_msg.h"
#include "service_alarm.h"
#ifndef CONFIG_ALARM_RING_DURATION_MS
#define CONFIG_ALARM_RING_DURATION_MS 60000
#endif
#define ALARM_RING_DURATION_MS CONFIG_ALARM_RING_DURATION_MS
typedef enum {
ALARM_RING_STATE_IDLE = 0,
ALARM_RING_STATE_RINGING,
} alarm_ring_state_t;
typedef struct {
alarm_ring_state_t state;
char text[128];
lisa_timer_t *timeout_timer;
alarm_ring_play_once_cb_t play_cb;
alarm_ring_force_stop_cb_t stop_cb;
bool inited;
} alarm_ring_ctx_t;
static alarm_ring_ctx_t s_alarm_ring_ctx = {0};
static void alarm_ring_timeout_cb(struct lisa_timer *timer)
{
(void)timer;
LISA_LOGI(TAG, "alarm ring timeout 60s");
alarm_ring_stop();
}
static void alarm_ring_on_wakeup(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
(void)unused;
(void)msg_id;
(void)data;
(void)len;
(void)user_data;
alarm_ring_stop();
}
static void alarm_ring_on_button_change(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
(void)unused;
(void)msg_id;
(void)data;
(void)len;
(void)user_data;
alarm_ring_stop();
}
static void alarm_ring_on_trigger(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
(void)unused;
(void)msg_id;
(void)len;
(void)user_data;
const struct service_alarm *alarm = (const struct service_alarm *)data;
if (!alarm) {
return;
}
/* Stop current voice session first to avoid ASR/TTS racing with alarm ringing. */
voice_msg_pub(VOICE_MSG_CLOUD_MCP_CHAT_EXIT, NULL, 0);
alarm_ring_start((const char *)alarm->text);
}
int alarm_ring_init(alarm_ring_play_once_cb_t play_cb, alarm_ring_force_stop_cb_t stop_cb)
{
s_alarm_ring_ctx.play_cb = play_cb;
s_alarm_ring_ctx.stop_cb = stop_cb;
if (s_alarm_ring_ctx.timeout_timer == NULL) {
s_alarm_ring_ctx.timeout_timer = lisa_timer_create(ALARM_RING_DURATION_MS, alarm_ring_timeout_cb, NULL);
}
if (!s_alarm_ring_ctx.inited) {
voice_msg_sub(VOICE_MSG_ALARM_TRIGGER, alarm_ring_on_trigger, NULL);
voice_msg_sub(VOICE_MSG_WAKEUP_KEYWORD, alarm_ring_on_wakeup, NULL);
voice_msg_sub(VOICE_MSG_WAKEUP_COMMAND, alarm_ring_on_wakeup, NULL);
voice_msg_sub(VOICE_MSG_BUTTON_CHANGE, alarm_ring_on_button_change, NULL);
s_alarm_ring_ctx.inited = true;
}
return 0;
}
int alarm_ring_start(const char *text)
{
alarm_ring_stop();
s_alarm_ring_ctx.state = ALARM_RING_STATE_RINGING;
memset(s_alarm_ring_ctx.text, 0, sizeof(s_alarm_ring_ctx.text));
if (text) {
strncpy(s_alarm_ring_ctx.text, text, sizeof(s_alarm_ring_ctx.text) - 1);
}
if (s_alarm_ring_ctx.timeout_timer) {
lisa_timer_change_period(s_alarm_ring_ctx.timeout_timer, ALARM_RING_DURATION_MS);
lisa_timer_start(s_alarm_ring_ctx.timeout_timer);
}
if (s_alarm_ring_ctx.play_cb) {
s_alarm_ring_ctx.play_cb(s_alarm_ring_ctx.text);
}
return 0;
}
void alarm_ring_stop(void)
{
if (s_alarm_ring_ctx.state != ALARM_RING_STATE_RINGING) {
return;
}
s_alarm_ring_ctx.state = ALARM_RING_STATE_IDLE;
if (s_alarm_ring_ctx.timeout_timer) {
lisa_timer_stop(s_alarm_ring_ctx.timeout_timer);
}
if (s_alarm_ring_ctx.stop_cb) {
s_alarm_ring_ctx.stop_cb();
}
}
void alarm_ring_notify_playback_complete(void)
{
if (s_alarm_ring_ctx.state != ALARM_RING_STATE_RINGING) {
return;
}
if (s_alarm_ring_ctx.play_cb) {
s_alarm_ring_ctx.play_cb(s_alarm_ring_ctx.text);
}
}
bool alarm_ring_is_active(void)
{
return s_alarm_ring_ctx.state == ALARM_RING_STATE_RINGING;
}

View File

@@ -0,0 +1,23 @@
#ifndef __ALARM_RING_H__
#define __ALARM_RING_H__
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef void (*alarm_ring_play_once_cb_t)(const char *text);
typedef void (*alarm_ring_force_stop_cb_t)(void);
int alarm_ring_init(alarm_ring_play_once_cb_t play_cb, alarm_ring_force_stop_cb_t stop_cb);
int alarm_ring_start(const char *text);
void alarm_ring_stop(void);
void alarm_ring_notify_playback_complete(void);
bool alarm_ring_is_active(void);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,879 @@
#define TAG "alarm_store"
#include "alarm_store.h"
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include "lisa_log.h"
#include "lisa_kv.h"
#include "lisa_mem.h"
#include "lisa_mutex.h"
#include "sysutils.h"
#include "alarm.h"
#include "alarm_next.h"
static alarm_store_t s_alarm_store __psram_bss__;
static lisa_mutex_t *s_alarm_store_mutex = NULL;
/* ==================== 工具函数 ==================== */
static void alarm_store_cache_add(const alarm_object_t *alarm)
{
if (!alarm) {
return;
}
lisa_mutex_lock(s_alarm_store_mutex, LISA_OS_WAIT_FOREVER);
for (uint32_t i = 0; i < s_alarm_store.count; ++i) {
if (s_alarm_store.items[i].alarm_id == alarm->alarm_id) {
s_alarm_store.items[i] = *alarm;
lisa_mutex_unlock(s_alarm_store_mutex);
return;
}
}
if (s_alarm_store.count < LS_ALARM_MAX_COUNT) {
s_alarm_store.items[s_alarm_store.count++] = *alarm;
} else {
LISA_LOGW(TAG, "alarm store full, skip cache add id=%llu",
(unsigned long long)alarm->alarm_id);
}
lisa_mutex_unlock(s_alarm_store_mutex);
}
static void alarm_store_cache_remove(uint64_t alarm_id)
{
lisa_mutex_lock(s_alarm_store_mutex, LISA_OS_WAIT_FOREVER);
for (uint32_t i = 0; i < s_alarm_store.count; ++i) {
if (s_alarm_store.items[i].alarm_id == alarm_id) {
for (uint32_t j = i + 1; j < s_alarm_store.count; ++j) {
s_alarm_store.items[j - 1] = s_alarm_store.items[j];
}
s_alarm_store.count--;
break;
}
}
lisa_mutex_unlock(s_alarm_store_mutex);
}
static int get_network_time(struct tm *network_time, time_t *network_timestamp)
{
struct timeval tv;
if (gettimeofday(&tv, NULL) < 0) {
return -1;
}
if (network_timestamp) {
*network_timestamp = (time_t)tv.tv_sec;
}
if (network_time) {
localtime_r(&tv.tv_sec, network_time);
}
return 0;
}
static const char *alarm_calendar_name(alarm_calendar_t calendar)
{
switch (calendar) {
case ALARM_CAL_GREGORIAN:
return "GREGORIAN";
case ALARM_CAL_LUNAR:
return "LUNAR";
default:
return "UNKNOWN";
}
}
static const char *alarm_trigger_type_name(alarm_trigger_type_t type)
{
switch (type) {
case ALARM_TRIG_ONCE:
return "ONCE";
case ALARM_TRIG_DAILY:
return "DAILY";
case ALARM_TRIG_WEEKLY:
return "WEEKLY";
case ALARM_TRIG_WORKDAY:
return "WORKDAY";
case ALARM_TRIG_WEEKEND:
return "WEEKEND";
case ALARM_TRIG_MONTHLY:
return "MONTHLY";
case ALARM_TRIG_YEARLY:
return "YEARLY";
case ALARM_TRIG_HOLIDAY:
return "HOLIDAY";
default:
return "UNKNOWN";
}
}
void alarm_obj_print(const alarm_object_t *alarm)
{
if (!alarm) {
LISA_LOGW(TAG, "alarm_obj_log: null alarm");
return;
}
char keywords_buf[ALARM_KEYWORD_MAX * ALARM_KEYWORD_LEN] = {0};
size_t off = 0;
for (uint8_t i = 0; i < alarm->keyword_count && i < ALARM_KEYWORD_MAX; i++) {
if (alarm->keywords[i][0] == '\0') {
continue;
}
int n = snprintf(keywords_buf + off, sizeof(keywords_buf) - off,
"%s%s", (off > 0) ? "," : "", alarm->keywords[i]);
if (n < 0 || (size_t)n >= sizeof(keywords_buf) - off) {
break;
}
off += (size_t)n;
}
char buf[512];
size_t used = 0;
// helper macro to append with bounds check
#define APPEND_FIELD(_fmt, ...) \
do { \
if (used < sizeof(buf)) { \
int _n = snprintf(buf + used, sizeof(buf) - used, _fmt, __VA_ARGS__); \
if (_n > 0) { \
size_t _added = (size_t)_n; \
used += (_added < (sizeof(buf) - used)) ? _added : (sizeof(buf) - used - 1); \
} \
} \
} while (0)
APPEND_FIELD("%s", "alarm_obj:");
if (alarm->cloud_id != 0) {
APPEND_FIELD(" cloud_id=%llu", (unsigned long long)alarm->cloud_id);
}
if (alarm->alarm_id != 0) {
APPEND_FIELD(" id=%llu", (unsigned long long)alarm->alarm_id);
}
APPEND_FIELD(" cal=%s", alarm_calendar_name(alarm->calendar));
APPEND_FIELD(" type=%s", alarm_trigger_type_name(alarm->trigger.type));
if (alarm->trigger.hour || alarm->trigger.minute || alarm->trigger.second) {
APPEND_FIELD(" time=%02u:%02u:%02u", (unsigned)alarm->trigger.hour, (unsigned)alarm->trigger.minute, (unsigned)alarm->trigger.second);
}
if (alarm->trigger.year || alarm->trigger.month || alarm->trigger.day) {
APPEND_FIELD(" date=%04u-%02u-%02u", (unsigned)alarm->trigger.year, (unsigned)alarm->trigger.month, (unsigned)alarm->trigger.day);
}
if (alarm->trigger.day_of_week) {
APPEND_FIELD(" day_of_week=%u", (unsigned)alarm->trigger.day_of_week);
}
if (alarm->trigger.day_of_month) {
APPEND_FIELD(" day_of_month=%u", (unsigned)alarm->trigger.day_of_month);
}
if (alarm->trigger.lunar_month || alarm->trigger.lunar_day) {
APPEND_FIELD(" lunar=%02u-%02u", (unsigned)alarm->trigger.lunar_month, (unsigned)alarm->trigger.lunar_day);
}
if (alarm->trigger.holiday_name[0]) {
APPEND_FIELD(" holiday=%s", alarm->trigger.holiday_name);
}
if (alarm->text[0]) {
APPEND_FIELD(" text=%s", (const char *)alarm->text);
}
if (keywords_buf[0]) {
APPEND_FIELD(" keywords=%s", keywords_buf);
}
shellPrint(shellGetCurrent(), "%s", buf);
#undef APPEND_FIELD
}
#define BEIJING_TIME_OFFSET_SEC (8 * 60 * 60)
uint64_t alarm_obj_to_timestamp(const alarm_object_t *alarm)
{
if (!alarm) {
return 0;
}
if (alarm->trigger.year < 1970 || alarm->trigger.month < 1 || alarm->trigger.day < 1) {
LISA_LOGE(TAG, "invalid date for timestamp conversion: %u-%u-%u",
alarm->trigger.year, alarm->trigger.month, alarm->trigger.day);
return 0;
}
struct tm tmv;
memset(&tmv, 0, sizeof(tmv));
tmv.tm_year = (int)alarm->trigger.year - 1900;
tmv.tm_mon = (int)alarm->trigger.month - 1;
tmv.tm_mday = (int)alarm->trigger.day;
tmv.tm_hour = (int)alarm->trigger.hour;
tmv.tm_min = (int)alarm->trigger.minute;
tmv.tm_sec = (int)alarm->trigger.second;
time_t ts = mktime(&tmv);
if (ts < 0) {
LISA_LOGE(TAG, "mktime failed for %u-%u-%u %u:%u:%u",
alarm->trigger.year, alarm->trigger.month, alarm->trigger.day,
alarm->trigger.hour, alarm->trigger.minute, alarm->trigger.second);
return 0;
}
ts -= BEIJING_TIME_OFFSET_SEC;
return (uint64_t)ts;
}
static int alarm_obj_from_timestamp(uint64_t timestamp, const uint8_t *text, alarm_object_t *out_alarm)
{
if (!out_alarm || timestamp == 0) {
return -1;
}
memset(out_alarm, 0, sizeof(*out_alarm));
out_alarm->alarm_id = timestamp;
out_alarm->calendar = ALARM_CAL_GREGORIAN;
out_alarm->trigger.type = ALARM_TRIG_ONCE;
time_t ts = (time_t)timestamp;
struct tm tmv;
memset(&tmv, 0, sizeof(tmv));
if (!localtime_r(&ts, &tmv)) {
return -1;
}
out_alarm->trigger.year = (uint16_t)(tmv.tm_year + 1970);
out_alarm->trigger.month = (uint8_t)(tmv.tm_mon + 1);
out_alarm->trigger.day = (uint8_t)tmv.tm_mday;
out_alarm->trigger.hour = (uint8_t)tmv.tm_hour;
out_alarm->trigger.minute = (uint8_t)tmv.tm_min;
out_alarm->trigger.second = (uint8_t)tmv.tm_sec;
if (text) {
size_t copy_len = strnlen((const char *)text, sizeof(out_alarm->text) - 1);
memcpy(out_alarm->text, text, copy_len);
out_alarm->text[copy_len] = '\0';
}
return 0;
}
/* ==================== Alarm_list NVS 管理工具 ==================== */
static int alarm_list_load(uint64_t **ids, uint32_t *count)
{
int len = 0;
uint8_t *data = NULL;
if (ids) {
*ids = NULL;
}
if (count) {
*count = 0;
}
int ret = lisa_kv_get_blob(ALARM_LIST_KEY, &data, &len);
if (ret != 0 || !data || len <= 0) {
if (data) {
lisa_mem_free(data);
}
return 0;
}
if (len % (int)sizeof(uint64_t) != 0) {
lisa_mem_free(data);
return -1;
}
if (ids) {
*ids = (uint64_t *)data;
} else {
lisa_mem_free(data);
return -1;
}
if (count) {
*count = (uint32_t)(len / sizeof(uint64_t));
}
return 0;
}
static bool alarm_list_contains(const uint64_t *ids, uint32_t count, uint64_t alarm_id)
{
for (uint32_t i = 0; i < count; i++) {
if (ids[i] == alarm_id) {
return true;
}
}
return false;
}
static int alarm_list_save(const uint64_t *ids, uint32_t count)
{
if (!ids || count == 0) {
return lisa_kv_set_blob(ALARM_LIST_KEY, (uint8_t *)ids, 0);
}
return lisa_kv_set_blob(ALARM_LIST_KEY, (uint8_t *)ids, count * sizeof(uint64_t));
}
// 添加一个id到闹钟ID列表
static int alarm_list_add(uint64_t id)
{
uint64_t *ids = NULL;
uint32_t count = 0;
int ret = alarm_list_load(&ids, &count);
if (ret != 0) {
if (ids) lisa_mem_free(ids);
return -1;
}
// 检查是否已存在
for (uint32_t i = 0; i < count; ++i) {
if (ids[i] == id) {
if (ids) lisa_mem_free(ids);
return 0; // 已存在无需添加
}
}
uint64_t *new_ids = lisa_mem_alloc((count + 1) * sizeof(uint64_t));
if (!new_ids) {
if (ids) lisa_mem_free(ids);
return -1;
}
if (count > 0 && ids) {
memcpy(new_ids, ids, count * sizeof(uint64_t));
}
new_ids[count] = id;
ret = alarm_list_save(new_ids, count + 1);
lisa_mem_free(new_ids);
if (ids) lisa_mem_free(ids);
return ret;
}
// 从闹钟ID列表中删除一个id
static int alarm_list_remove(uint64_t id)
{
uint64_t *ids = NULL;
uint32_t count = 0;
int ret = alarm_list_load(&ids, &count);
if (ret != 0 || count == 0) {
if (ids) lisa_mem_free(ids);
return -1;
}
uint32_t new_count = 0;
uint64_t *new_ids = lisa_mem_alloc(count * sizeof(uint64_t));
if (!new_ids) {
if (ids) lisa_mem_free(ids);
return -1;
}
for (uint32_t i = 0; i < count; ++i) {
if (ids[i] != id) {
new_ids[new_count++] = ids[i];
}
}
if (new_count == 0) {
ret = lisa_kv_del(ALARM_LIST_KEY);
} else {
ret = lisa_kv_set_blob(ALARM_LIST_KEY, (uint8_t *)new_ids, new_count * sizeof(uint64_t));
}
lisa_mem_free(new_ids);
if (ids) lisa_mem_free(ids);
return ret;
}
/* ==================== Alarm_obj NVS 管理工具 ==================== */
static size_t alarm_text_len(const char *text)
{
if (!text) {
return 0;
}
return strnlen(text, LS_ALARM_TEXT_MAX_LEN - 1);
}
// alarm_object_t <-> alarm_obj_nvs_t 转换
void alarm_object_to_nvs(const alarm_object_t *alarm, alarm_obj_nvs_t *hdr, size_t *text_len_out) {
if (!alarm || !hdr) return;
memset(hdr, 0, sizeof(*hdr));
hdr->cloud_id = alarm->cloud_id;
hdr->calendar = (uint8_t)alarm->calendar;
hdr->trigger_type = (uint8_t)alarm->trigger.type;
hdr->hour = alarm->trigger.hour;
hdr->minute = alarm->trigger.minute;
hdr->second = alarm->trigger.second;
hdr->year = alarm->trigger.year;
hdr->month = alarm->trigger.month;
hdr->day = alarm->trigger.day;
hdr->day_of_week = alarm->trigger.day_of_week;
hdr->day_of_month = alarm->trigger.day_of_month;
hdr->lunar_month = alarm->trigger.lunar_month;
hdr->lunar_day = alarm->trigger.lunar_day;
strncpy(hdr->holiday_name, alarm->trigger.holiday_name, sizeof(hdr->holiday_name) - 1);
hdr->holiday_name[sizeof(hdr->holiday_name) - 1] = '\0';
hdr->keyword_count = alarm->keyword_count;
for (uint8_t i = 0; i < ALARM_KEYWORD_MAX; i++) {
strncpy(hdr->keywords[i], alarm->keywords[i], ALARM_KEYWORD_LEN - 1);
hdr->keywords[i][ALARM_KEYWORD_LEN - 1] = '\0';
}
size_t text_len = alarm_text_len((const char *)alarm->text);
hdr->text_len = (uint16_t)text_len;
if (text_len_out) *text_len_out = text_len;
}
void alarm_object_from_nvs(const alarm_obj_nvs_t *hdr, const uint8_t *text, size_t text_len, uint64_t alarm_id, alarm_object_t *out_alarm) {
if (!hdr || !out_alarm) return;
memset(out_alarm, 0, sizeof(*out_alarm));
out_alarm->alarm_id = alarm_id;
out_alarm->cloud_id = hdr->cloud_id;
out_alarm->calendar = (alarm_calendar_t)hdr->calendar;
out_alarm->trigger.type = (alarm_trigger_type_t)hdr->trigger_type;
out_alarm->trigger.hour = hdr->hour;
out_alarm->trigger.minute = hdr->minute;
out_alarm->trigger.second = hdr->second;
out_alarm->trigger.year = hdr->year;
out_alarm->trigger.month = hdr->month;
out_alarm->trigger.day = hdr->day;
out_alarm->trigger.day_of_week = hdr->day_of_week;
out_alarm->trigger.day_of_month = hdr->day_of_month;
out_alarm->trigger.lunar_month = hdr->lunar_month;
out_alarm->trigger.lunar_day = hdr->lunar_day;
strncpy(out_alarm->trigger.holiday_name, hdr->holiday_name, sizeof(out_alarm->trigger.holiday_name) - 1);
out_alarm->trigger.holiday_name[sizeof(out_alarm->trigger.holiday_name) - 1] = '\0';
out_alarm->keyword_count = hdr->keyword_count;
for (uint8_t i = 0; i < ALARM_KEYWORD_MAX; i++) {
strncpy(out_alarm->keywords[i], hdr->keywords[i], ALARM_KEYWORD_LEN - 1);
out_alarm->keywords[i][ALARM_KEYWORD_LEN - 1] = '\0';
}
if (text && text_len > 0) {
size_t max_copy = sizeof(out_alarm->text) - 1;
size_t copy_len = text_len > max_copy ? max_copy : text_len;
memcpy(out_alarm->text, text, copy_len);
out_alarm->text[copy_len] = '\0';
}
}
static int alarm_obj_add_to_nvs(const alarm_object_t *alarm)
{
if (!alarm) {
return -1;
}
alarm_obj_nvs_t hdr;
size_t text_len = 0;
alarm_object_to_nvs(alarm, &hdr, &text_len);
size_t total_len = sizeof(hdr) + text_len;
uint8_t *buf = lisa_mem_alloc(total_len);
if (!buf) {
return -1;
}
memcpy(buf, &hdr, sizeof(hdr));
if (text_len > 0) {
memcpy(buf + sizeof(hdr), alarm->text, text_len);
}
char key[32];
snprintf(key, sizeof(key), "%llu", (unsigned long long)alarm->alarm_id);
int ret = lisa_kv_set_blob(key, buf, total_len);
lisa_mem_free(buf);
return ret;
}
static int alarm_obj_load_from_nvs(uint64_t alarm_id, alarm_object_t *out_alarm)
{
if (!out_alarm) {
return -1;
}
char key[32];
snprintf(key, sizeof(key), "%llu", (unsigned long long)alarm_id);
uint8_t *data = NULL;
int len = 0;
int ret = lisa_kv_get_blob(key, &data, &len);
if (ret != 0 || !data || len < (int)sizeof(alarm_obj_nvs_t)) {
if (data) {
lisa_mem_free(data);
}
return -1;
}
if ((size_t)len < sizeof(alarm_obj_nvs_t)) {
lisa_mem_free(data);
return -1;
}
alarm_obj_nvs_t hdr;
memcpy(&hdr, data, sizeof(hdr));
size_t text_len = hdr.text_len;
if ((size_t)len < sizeof(hdr) + text_len) {
lisa_mem_free(data);
return -1;
}
alarm_object_from_nvs(&hdr, data + sizeof(hdr), text_len, alarm_id, out_alarm);
lisa_mem_free(data);
return 0;
}
static int alarm_obj_delete_from_nvs(uint64_t alarm_id)
{
char key[32];
snprintf(key, sizeof(key), "%llu", (unsigned long long)alarm_id);
return lisa_kv_del(key);
}
/* ==================== 闹钟管理工具 ==================== */
int alarm_store_create_obj(const alarm_object_t *alarm, char *err_msg, size_t err_len)
{
if (!alarm) {
snprintf(err_msg, err_len, "闹钟参数无效");
return -1;
}
// 时间戳作为闹钟ID
uint64_t alarm_id = alarm_obj_to_timestamp(alarm);
if (alarm_id == 0) {
LISA_LOGE(TAG, "invalid alarm datetime, cannot create alarm");
snprintf(err_msg, err_len, "闹钟时间无效");
return -1;
}
// 检查闹钟时间是否已过
time_t now_ts = 0;
if (get_network_time(NULL, &now_ts) != 0) {
LISA_LOGE(TAG, "failed to get current time");
snprintf(err_msg, err_len, "无法获取当前时间");
return -1;
}
if ((time_t)alarm_id <= now_ts) {
LISA_LOGE(TAG, "alarm time already passed, id=%llu , now = %llu", (unsigned long long)alarm_id, now_ts);
snprintf(err_msg, err_len, "闹钟时间已过期");
return -1;
}
// 确保ID唯一
uint64_t *ids = NULL;
uint32_t count = 0;
if (alarm_list_load(&ids, &count) != 0) {
if (ids) {
lisa_mem_free(ids);
}
LISA_LOGE(TAG, "failed to load alarm list");
snprintf(err_msg, err_len, "闹钟列表读取失败");
return -1;
}
while (alarm_list_contains(ids, count, alarm_id)) {
LISA_LOGE(TAG, "alarm id %llu already exists",(unsigned long long)alarm_id);
snprintf(err_msg, err_len, "闹钟已存在");
return -1;
}
// 保存闹钟
alarm_object_t temp = *alarm;
// 添加到缓存
alarm_store_cache_add(&temp);
// 保存闹钟对象到nvs
temp.alarm_id = alarm_id;
LISA_LOGI(TAG, "alarm_store_create_obj called, alarm id=%llu", (unsigned long long)alarm_id);
int ret = alarm_obj_add_to_nvs(&temp);
if (ret == 0) {
ret = alarm_list_add(alarm_id);
}
if (ids) {
lisa_mem_free(ids);
}
return ret;
}
int alarm_store_create_obj_by_timestamp(const uint64_t timestamp, const uint8_t *text)
{
alarm_object_t temp;
if (alarm_obj_from_timestamp(timestamp, text, &temp) != 0) {
LISA_LOGE(TAG, "failed to build alarm object from timestamp: %llu",
(unsigned long long)timestamp);
return -1;
}
return alarm_store_create_obj(&temp, NULL, 0);
}
int alarm_store_delete_obj(const alarm_object_t *alarm)
{
if (alarm == NULL) {
return -1;
}
uint64_t target_id = alarm_obj_to_timestamp(alarm);
if (target_id == 0) {
LISA_LOGE(TAG, "invalid alarm datetime, cannot update alarm");
return -1;
}
uint64_t *ids = NULL;
uint32_t count = 0;
if (alarm_list_load(&ids, &count) != 0) {
if (ids) {
lisa_mem_free(ids);
}
return -1;
}
if (!alarm_list_contains(ids, count, target_id)) {
if (ids) {
lisa_mem_free(ids);
}
LISA_LOGE(TAG, "alarm id %llu not found", (unsigned long long)target_id);
return -1;
}
int ret = alarm_obj_delete_from_nvs(target_id);
if (ret == 0) {
lisa_mutex_lock(s_alarm_store_mutex, LISA_OS_WAIT_FOREVER);
ret = alarm_list_remove(target_id);
lisa_mutex_unlock(s_alarm_store_mutex);
}
if (ret == 0) {
alarm_store_cache_remove(target_id);
}
if (ids) {
lisa_mem_free(ids);
}
return ret;
}
int alarm_store_delete_obj_by_timestamp(const uint64_t timestamp)
{
alarm_object_t temp;
if (alarm_obj_from_timestamp(timestamp, (const uint8_t*)"", &temp) != 0) {
LISA_LOGE(TAG, "failed to build alarm object from timestamp: %llu",
(unsigned long long)timestamp);
return -1;
}
return alarm_store_delete_obj(&temp);
}
int alarm_store_update_obj(const alarm_object_t *alarm)
{
if (!alarm) {
return -1;
}
uint64_t target_id = alarm_obj_to_timestamp(alarm);
if (target_id == 0) {
LISA_LOGE(TAG, "invalid alarm datetime, cannot update alarm");
return -1;
}
alarm_object_t existing;
if (alarm_obj_load_from_nvs(target_id, &existing) != 0) {
LISA_LOGE(TAG, "alarm id %llu not found", (unsigned long long)target_id);
return -1;
}
alarm_object_t temp = *alarm;
temp.alarm_id = target_id;
LISA_LOGI(TAG, "alarm_store_update_obj called, alarm id=%llu", (unsigned long long)target_id);
int ret = alarm_obj_add_to_nvs(&temp);
if (ret == 0) {
alarm_store_cache_add(&temp);
}
return ret;
}
int alarm_store_update_obj_by_timestamp(const uint64_t timestamp, const uint8_t *text)
{
alarm_object_t temp;
if (alarm_obj_load_from_nvs(timestamp, &temp) != 0) {
LISA_LOGE(TAG, "failed to load alarm object: %llu",
(unsigned long long)timestamp);
return -1;
}
if (text && text[0] != '\0') {
strncpy(temp.text, (const char *)text, sizeof(temp.text) - 1);
temp.text[sizeof(temp.text) - 1] = '\0';
}
return alarm_store_update_obj(&temp);
}
// 查询指定闹钟对象是否存在存在则返回text不存在返回错误
// text_out: 输出参数指向缓冲区长度为buf_len
// 返回值: 0=找到,-1=未找到或出错
int alarm_store_query_obj(const alarm_object_t *alarm, char *text_out, size_t buf_len)
{
if (!alarm || !text_out || buf_len == 0) {
return -1;
}
uint64_t alarm_id = alarm_obj_to_timestamp(alarm);
if (alarm_id == 0) {
return -1;
}
alarm_object_t found;
if (alarm_obj_load_from_nvs(alarm_id, &found) == 0) {
// 拷贝text到输出缓冲区
strncpy(text_out, (const char *)found.text, buf_len - 1);
text_out[buf_len - 1] = '\0';
return 0;
}
return -1;
}
// 查询所有闹钟对象
const alarm_store_t *alarm_store_get_all(void) {
return &s_alarm_store;
}
// 查询指定id的闹钟对象
const alarm_object_t *alarm_store_find_by_id(uint64_t alarm_id) {
for (uint32_t i = 0; i < s_alarm_store.count; ++i) {
if (s_alarm_store.items[i].alarm_id == alarm_id) {
return &s_alarm_store.items[i];
}
}
return NULL;
}
const alarm_object_t *alarm_store_find_by_cloud_id(uint64_t cloud_id) {
if (cloud_id == 0) {
return NULL;
}
const alarm_object_t *found = NULL;
lisa_mutex_lock(s_alarm_store_mutex, LISA_OS_WAIT_FOREVER);
for (uint32_t i = 0; i < s_alarm_store.count; ++i) {
if (s_alarm_store.items[i].cloud_id == cloud_id) {
found = &s_alarm_store.items[i];
break;
}
}
lisa_mutex_unlock(s_alarm_store_mutex);
return found;
}
/* ==================== 初始化 ==================== */
static int alarm_store_load(alarm_store_t *store)
{
if (!store) {
return -1;
}
memset(store, 0, sizeof(*store));
store->count = 0;
// 读取闹钟ID列表
uint64_t *ids = NULL;
uint32_t count = 0;
int ret = alarm_list_load(&ids, &count);
for (uint32_t i = 0; i < count; ++i) {
LISA_LOGI(TAG, "alarm_store_load: id[%u] = %llu", i, (unsigned long long)ids[i]);
}
if (ret != 0 || !ids || count == 0) {
if (ids) {
lisa_mem_free(ids);
}
return 0;
}
// 加载 alarm_obj 到 alarm_store
uint32_t loaded = 0;
for (uint32_t i = 0; i < count && loaded < LS_ALARM_MAX_COUNT; ++i) {
alarm_object_t obj;
if (alarm_obj_load_from_nvs(ids[i], &obj) == 0) {
store->items[loaded] = obj;
loaded++;
} else {
// id没有对应的闹钟对象从列表中删除
lisa_mutex_lock(s_alarm_store_mutex, LISA_OS_WAIT_FOREVER);
alarm_list_remove(ids[i]);
lisa_mutex_unlock(s_alarm_store_mutex);
}
}
store->count = loaded;
lisa_mem_free(ids);
return 0;
}
int alarm_store_init(void)
{
if (s_alarm_store_mutex == NULL) {
s_alarm_store_mutex = lisa_mutex_create();
}
int ret;
ret = alarm_store_load(&s_alarm_store);
// 过期闹钟检查与重建
time_t now_ts = 0;
bool have_now = (get_network_time(NULL, &now_ts) == 0);
if (have_now) {
uint32_t new_count = 0;
for (uint32_t i = 0; i < s_alarm_store.count; ++i) {
alarm_object_t obj = s_alarm_store.items[i];
if (obj.alarm_id <= (uint64_t)now_ts) {
if (obj.trigger.type == ALARM_TRIG_ONCE) { // 单次闹钟过期直接清除
lisa_mutex_lock(s_alarm_store_mutex, LISA_OS_WAIT_FOREVER);
alarm_list_remove(obj.alarm_id);
lisa_mutex_unlock(s_alarm_store_mutex);
alarm_obj_delete_from_nvs(obj.alarm_id);
continue;
}
// 循环闹钟计算下次触发时间并更新nvs以及store
alarm_object_t temp = obj;
uint64_t next_ts = alarm_calc_next_trigger(&temp, now_ts);
if (next_ts > (uint64_t)now_ts) {
temp.alarm_id = next_ts;
alarm_obj_delete_from_nvs(obj.alarm_id);
lisa_mutex_lock(s_alarm_store_mutex, LISA_OS_WAIT_FOREVER);
alarm_list_remove(obj.alarm_id);
alarm_list_add(next_ts);
lisa_mutex_unlock(s_alarm_store_mutex);
alarm_obj_add_to_nvs(&temp);
s_alarm_store.items[new_count++] = temp;
} else {
lisa_mutex_lock(s_alarm_store_mutex, LISA_OS_WAIT_FOREVER);
alarm_list_remove(obj.alarm_id);
lisa_mutex_unlock(s_alarm_store_mutex);
alarm_obj_delete_from_nvs(obj.alarm_id);
}
} else {
s_alarm_store.items[new_count++] = obj;
}
}
s_alarm_store.count = new_count;
}
// 遍历所有已加载的闹钟对象,创建定时器
for (uint32_t i = 0; i < s_alarm_store.count; ++i) {
alarm_object_t *alarm = &s_alarm_store.items[i];
const uint8_t *text = (alarm->text[0] != '\0') ? (const uint8_t *)alarm->text : (const uint8_t *)"";
ls_alarm_insert_by_timestamp(alarm->alarm_id, text);
}
return 0;
}

View File

@@ -0,0 +1,103 @@
#ifndef __ALARM_STORE_H__
#define __ALARM_STORE_H__
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include "alarm.h"
typedef enum {
ALARM_CAL_GREGORIAN = 0,
ALARM_CAL_LUNAR = 1,
} alarm_calendar_t;
typedef enum {
ALARM_TRIG_ONCE = 0,
ALARM_TRIG_DAILY,
ALARM_TRIG_WEEKLY,
ALARM_TRIG_WORKDAY,
ALARM_TRIG_WEEKEND,
ALARM_TRIG_MONTHLY,
ALARM_TRIG_YEARLY,
ALARM_TRIG_HOLIDAY,
} alarm_trigger_type_t;
#define ALARM_KEYWORD_MAX 3
#define ALARM_KEYWORD_LEN 16
typedef struct {
alarm_trigger_type_t type;
uint8_t hour;
uint8_t minute;
uint8_t second;
uint16_t year; // ONCE
uint8_t month; // ONCE/YEARLY
uint8_t day; // ONCE/YEARLY/MONTHLY
uint8_t day_of_week; // WEEKLY
uint8_t day_of_month; // MONTHLY
uint8_t lunar_month; // LUNAR (MM)
uint8_t lunar_day; // LUNAR (DD)
char holiday_name[24]; // HOLIDAY
} alarm_trigger_t;
typedef struct {
uint64_t alarm_id; // timestamp as ID
uint64_t cloud_id; // cloud provided ID
alarm_calendar_t calendar;
alarm_trigger_t trigger;
char text[LS_ALARM_TEXT_MAX_LEN];
uint8_t keyword_count;
char keywords[ALARM_KEYWORD_MAX][ALARM_KEYWORD_LEN];
} alarm_object_t;
typedef struct {
uint32_t count;
alarm_object_t items[LS_ALARM_MAX_COUNT];
} alarm_store_t;
typedef struct {
uint64_t cloud_id;
uint8_t calendar;
uint8_t trigger_type;
uint8_t hour;
uint8_t minute;
uint8_t second;
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t day_of_week;
uint8_t day_of_month;
uint8_t lunar_month;
uint8_t lunar_day;
char holiday_name[24];
uint16_t text_len;
uint8_t keyword_count;
char keywords[ALARM_KEYWORD_MAX][ALARM_KEYWORD_LEN];
} alarm_obj_nvs_t;
#define ALARM_LIST_KEY "user.alarm_list"
int alarm_store_init(void);
int alarm_store_create_obj(const alarm_object_t *alarm, char *err_msg, size_t err_len);
int alarm_store_create_obj_by_timestamp(const uint64_t timestamp, const uint8_t *text);
int alarm_store_update_obj(const alarm_object_t *alarm);
int alarm_store_update_obj_by_timestamp(const uint64_t timestamp, const uint8_t *text);
int alarm_store_delete_obj(const alarm_object_t *alarm);
int alarm_store_query_obj(const alarm_object_t *alarm, char *text_out, size_t buf_len);
void alarm_obj_print(const alarm_object_t *alarm);
uint64_t alarm_obj_to_timestamp(const alarm_object_t *alarm);
const alarm_store_t *alarm_store_get_all(void);
const alarm_object_t *alarm_store_find_by_id(uint64_t alarm_id);
const alarm_object_t *alarm_store_find_by_cloud_id(uint64_t cloud_id);
#ifdef __cplusplus
}
#endif
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
listenai_library_named(audio)
listenai_library_sources(
app_player.c
audio_play.c
pa_manager.c
)
listenai_include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
)

View File

@@ -0,0 +1,17 @@
menu "Audio PA Manager"
config PA_PORT
int "PA GPIO PORT NUM"
default 1
range 0 1
config PA_PIN
int "PA GPIO PIN"
default 8
range 0 31
config PA_FUNC
int "PA PIN IOMUX FUNCTION NUM"
default 1
endmenu

View File

@@ -0,0 +1,708 @@
#include <stdio.h>
#include <string.h>
#include "lisa_mem.h"
#include "lisa_log.h"
#include "lisa_player.h"
#include "app_player.h"
#include "pa_manager.h"
#include "lisa_mutex.h"
#include "lisa_typedef.h"
#include "lisa_semaphore.h"
#define TAG "APP_PLAYER"
#define TONE_ID_TAG "mem://"
#define APP_PLAYER_VOL_RANGE_MIN (1)
#define APP_PLAYER_VOL_RANGE_MAX (100)
#define APP_PLAYER_USE_AUDIO_PLAYER (1)
typedef struct player_vol_range {
uint8_t min;
uint8_t max;
} player_vol_range;
typedef struct status_cb_map {
lisa_mutex_t *lock;
player_status_cb pre_cb;
uint32_t pre_id;
player_status_cb cur_cb;
uint32_t cur_id;
} status_cb_map_t;
typedef struct app_player_item_s {
/** 播放器句柄 */
PLAYER_HANDLE hld;
/** 播放ID */
uint32_t playid;
/** 播放器回调 */
status_cb_map_t cb_map;
/** 播放器 Prepareing Flag */
bool is_preparing;
/** 播放器 Wait Prepare Intercepte Flag */
bool wait_prepare_intercepted;
/** 播放器 Force Pause when Preparing */
bool pause_preparing;
/** 适配 Preparing 等待 */
lisa_semaphore_t *preparing_sem;
/** Volume Range */
player_vol_range vol_range;
/** Player Evt */
PlayerEvt evt;
} app_player_item_t;
typedef struct app_player_s {
/** 提示音播放器 */
app_player_item_t tone_player;
/** 音乐播放器 */
app_player_item_t audio_player;
} app_player_t;
static app_player_t *s_app_player = NULL;
#if 1
#define APPLICATION_PLAY_RB_LEFT_SHIFT_BIT (12)
#define APPLICATION_PLAY_ONE_FRAME_SIZE (512)
#define APPLICATION_PLAY_SAMPLERATE (16000)
#define APPLICATION_PLAY_SAMPLERATE_ENUM (1) // for samplerate_16000
int lisaplayer_track_samplerate_hook(unsigned char *samplerate_enum, uint16_t *one_frame_size, uint16_t *ringbuffer_shift_bit)
{
LISA_LOGI(TAG, "Application samplerate: %d, index: %d, one frame size: %d, left shift: %d",
APPLICATION_PLAY_SAMPLERATE,
APPLICATION_PLAY_SAMPLERATE_ENUM,
APPLICATION_PLAY_ONE_FRAME_SIZE,
APPLICATION_PLAY_RB_LEFT_SHIFT_BIT);
*samplerate_enum = APPLICATION_PLAY_SAMPLERATE_ENUM;
*one_frame_size = APPLICATION_PLAY_ONE_FRAME_SIZE;
*ringbuffer_shift_bit = APPLICATION_PLAY_RB_LEFT_SHIFT_BIT;
return APPLICATION_PLAY_SAMPLERATE;
}
#endif
static void __player_cache_callback(app_player_item_t *player_item, player_status_cb cb, uint32_t id)
{
if (!player_item) return;
lisa_mutex_lock(player_item->cb_map.lock, LISA_OS_WAIT_FOREVER);
if (player_item->cb_map.pre_cb == NULL || player_item->cb_map.pre_id == 0)
{
LISA_LOGV(TAG, "cache callback to pre by %d %p", id, cb);
player_item->cb_map.pre_cb = cb;
player_item->cb_map.pre_id = id;
}
else
{
LISA_LOGV(TAG, "cache callback to cur by %d %p", id, cb);
player_item->cb_map.cur_cb = cb;
player_item->cb_map.cur_id = id;
}
lisa_mutex_unlock(player_item->cb_map.lock);
}
static void __player_broadcast_status(app_player_item_t *player_item, PlayerEvt st)
{
player_status_cb cb = NULL;
uint32_t cb_id = 0;
bool locked = false;
if (!player_item) return;
if (lisa_mutex_lock(player_item->cb_map.lock, LISA_NO_WAIT) == 0) {
locked = true;
} else {
LISA_LOGE(TAG, "callback lock busy, fallback broadcast evt=%d", st);
}
if (player_item->cb_map.pre_cb != NULL)
{
cb = player_item->cb_map.pre_cb;
cb_id = player_item->cb_map.pre_id;
if (locked && (st == PLAYER_EVT_STOPED || st == PLAYER_EVT_PLAYBACK_COMPLETE || st == PLAYER_EVT_ERROR))
{
LISA_LOGV(TAG, "move cur: %d %p to pre: %d %p",
player_item->cb_map.cur_id,
player_item->cb_map.cur_cb,
player_item->cb_map.pre_id,
player_item->cb_map.pre_cb);
player_item->cb_map.pre_cb = player_item->cb_map.cur_cb;
player_item->cb_map.pre_id = player_item->cb_map.cur_id;
player_item->cb_map.cur_cb = NULL;
player_item->cb_map.cur_id = 0;
}
}
else
{
LISA_LOGE(TAG, "pre callback null");
}
if (locked) {
lisa_mutex_unlock(player_item->cb_map.lock);
}
if (cb != NULL)
{
LISA_LOGV(TAG, "pre callback with %d %p", cb_id, cb);
cb(st);
}
}
static void __player_clear_prepare_state(app_player_item_t *player_item)
{
if (!player_item) return;
player_item->is_preparing = false;
player_item->wait_prepare_intercepted = false;
player_item->pause_preparing = false;
}
/**
* @brief 提示音播放器状态回调
* @param evt 播放器事件
* @param arg1 参数1
* @param arg2 参数2
* @param id
* @return
*/
static int _tone_player_callback(PlayerEvt evt, int arg1, int arg2, int id)
{
app_player_item_t *player = &(s_app_player->tone_player);
bool ignore_play = false;
player->is_preparing = false;
if (player->wait_prepare_intercepted) {
ignore_play = true;
lisa_semaphore_give(player->preparing_sem);
}
LISA_LOGI(TAG, "tone player evt %d", evt);
switch (evt) {
case PLAYER_EVT_PREPARED: {
if (!ignore_play) {
if (!player->pause_preparing) {
if (lisa_player_play(player->hld) == PLAYER_OP_FAIL) {
__player_broadcast_status(player, PLAYER_EVT_ERROR);
}
} else {
LISA_LOGD(TAG, "tone player has pause when preparing");
__player_broadcast_status(player, PLAYER_EVT_PAUSED);
}
}
return 0;
} break;
case PLAYER_EVT_PAUSED:
case PLAYER_EVT_STOPED:
case PLAYER_EVT_PLAYBACK_COMPLETE:
case PLAYER_EVT_ERROR:
if (evt == PLAYER_EVT_STOPED || evt == PLAYER_EVT_PLAYBACK_COMPLETE || evt == PLAYER_EVT_ERROR) {
player->wait_prepare_intercepted = false;
player->pause_preparing = false;
}
__player_broadcast_status(player, evt);
if (player->evt == PLAYER_EVT_STOPED && evt == PLAYER_EVT_ERROR) {
// 打断时(Stop), 先来Stop情况下, 再来Error, 不需要关闭PA
} else {
pa_manager_refresh(PA_MGR_OFF, LS_PA_BASE_TIME, "tone_player_end");
}
if (evt == PLAYER_EVT_ERROR) {
lisa_player_reset(player->hld);
}
break;
case PLAYER_EVT_PLAYING: {
__player_broadcast_status(player, evt);
} break;
default:
break;
}
player->evt = evt;
return 0;
}
#if APP_PLAYER_USE_AUDIO_PLAYER
/**
* @brief 音乐播放器状态回调
* @param evt 播放器事件
* @param arg1 参数1
* @param arg2 参数2
* @param id
* @return
*/
static int _audio_player_callback(PlayerEvt evt, int arg1, int arg2, int id)
{
app_player_item_t *player = &(s_app_player->audio_player);
bool ignore_play = false;
player->is_preparing = false;
if (player->wait_prepare_intercepted) {
ignore_play = true;
lisa_semaphore_give(player->preparing_sem);
}
LISA_LOGI(TAG, "audio player evt %d", evt);
switch (evt) {
case PLAYER_EVT_PREPARED: {
if (!ignore_play) {
if (!player->pause_preparing) {
lisa_player_play(player->hld);
} else {
LISA_LOGD(TAG, "audio player has pause when preparing");
__player_broadcast_status(player, PLAYER_EVT_PAUSED);
}
}
return 0;
} break;
case PLAYER_EVT_PAUSED:
case PLAYER_EVT_STOPED:
case PLAYER_EVT_PLAYBACK_COMPLETE:
case PLAYER_EVT_ERROR:
if (evt == PLAYER_EVT_STOPED || evt == PLAYER_EVT_PLAYBACK_COMPLETE || evt == PLAYER_EVT_ERROR) {
player->wait_prepare_intercepted = false;
player->pause_preparing = false;
}
__player_broadcast_status(player, evt);
pa_manager_refresh(PA_MGR_OFF, LS_PA_BASE_TIME, "audio_player_end");
if (evt == PLAYER_EVT_ERROR) {
lisa_player_reset(player->hld);
}
break;
case PLAYER_EVT_PLAYING: {
__player_broadcast_status(player, evt);
} break;
default:
break;
}
player->evt = evt;
return 0;
}
#endif
void app_player_init()
{
if (s_app_player) return;
LISA_LOGD(TAG, "App Player init [in]");
s_app_player = (app_player_t *)lisa_mem_calloc(1, sizeof(app_player_t));
LISA_LOGI(TAG, "Lisa Player Version %s", lisa_player_get_version());
// 创建提示音播放器
s_app_player->tone_player.hld = lisa_player_create("toneplayer", 0);
s_app_player->tone_player.cb_map.lock = lisa_mutex_create();
s_app_player->tone_player.cb_map.pre_cb = NULL;
s_app_player->tone_player.cb_map.pre_id = 0;
s_app_player->tone_player.cb_map.cur_cb = NULL;
s_app_player->tone_player.cb_map.cur_id = 0;
s_app_player->tone_player.is_preparing = false;
s_app_player->tone_player.wait_prepare_intercepted = false;
s_app_player->tone_player.pause_preparing = false;
s_app_player->tone_player.preparing_sem = lisa_semaphore_create(1);
s_app_player->tone_player.vol_range.min = APP_PLAYER_VOL_RANGE_MIN;
s_app_player->tone_player.vol_range.max = APP_PLAYER_VOL_RANGE_MAX;
s_app_player->tone_player.evt = PLAYER_EVT_ERROR;
// 设置提示音播放器回调函数
lisa_player_set_callback(s_app_player->tone_player.hld, _tone_player_callback);
#if APP_PLAYER_USE_AUDIO_PLAYER
// 创建音乐播放器
s_app_player->audio_player.hld = lisa_player_create("audioplayer", 0);
s_app_player->audio_player.cb_map.lock = lisa_mutex_create();
s_app_player->audio_player.cb_map.pre_cb = NULL;
s_app_player->audio_player.cb_map.pre_id = 0;
s_app_player->audio_player.cb_map.cur_cb = NULL;
s_app_player->audio_player.cb_map.cur_id = 0;
s_app_player->audio_player.is_preparing = false;
s_app_player->audio_player.wait_prepare_intercepted = false;
s_app_player->audio_player.pause_preparing = false;
s_app_player->audio_player.preparing_sem = lisa_semaphore_create(1);
s_app_player->audio_player.vol_range.min = APP_PLAYER_VOL_RANGE_MIN;
s_app_player->audio_player.vol_range.max = APP_PLAYER_VOL_RANGE_MAX;
s_app_player->audio_player.evt = PLAYER_EVT_ERROR;
// 设置音乐播放器回调函数
lisa_player_set_callback(s_app_player->audio_player.hld, _audio_player_callback);
#endif
LISA_LOGD(TAG, "App Player init [out]");
}
/**
* @brief 检查 Preparing 状态
* @param player_item Player Pointer
* @param name Player Name
* @return true Continue
* @return false Intercepte
*/
static bool __player_prepare_check(app_player_item_t *player_item, const char *tips)
{
if (player_item->is_preparing) {
// is_preparing 判断完后打印后, 可能引起时间片由player回调线程调度
LISA_LOGD(TAG, "wait %s prepare complete...", tips);
lisa_player_pre_close(player_item->hld);
// 回调线程执行后, is_preparing 会被置为false
// 因此此处信号量可能会一直等待
if (player_item->is_preparing) {
player_item->wait_prepare_intercepted = true;
lisa_semaphore_take(player_item->preparing_sem, LISA_OS_WAIT_FOREVER);
player_item->wait_prepare_intercepted = false;
}
LISA_LOGD(TAG, "pre %s prepare complete", tips);
lisa_player_stop_sync(player_item->hld);
lisa_player_reset(player_item->hld);
__player_clear_prepare_state(player_item);
LISA_LOGD(TAG, "pre %s status check end", tips);
return false;
}
return true;
}
void app_player_play(player_t type, char *url, player_status_cb cb)
{
app_player_play_by_throw(type, url, 0, cb);
}
void app_player_play_by_throw(player_t type, char *url, int throw_time_ms, player_status_cb cb)
{
if (!s_app_player) return;
if (!(s_app_player->tone_player.hld) || (APP_PLAYER_USE_AUDIO_PLAYER && !(s_app_player->audio_player.hld))) return;
pa_manager_refresh(PA_MGR_ON, LS_PA_FOREVER, "player play");
PlayerErr ret = PLAYER_OK;
if (type == PLAYER_T_TONE) {
app_player_item_t *player = &s_app_player->tone_player;
if(__player_prepare_check(player, "tone_play")) {
// Reset Tone Player
app_player_reset(PLAYER_T_TONE);
}
__player_clear_prepare_state(player);
player->playid++;
__player_cache_callback(player, cb, player->playid);
if (cb) cb(APP_PLAYER_PREPARING);
player->pause_preparing = false;
lisa_player_throw_low_energy(player->hld, throw_time_ms);
player->is_preparing = true;
ret = lisa_player_seturl(player->hld, url);
if (ret != PLAYER_OK) {
__player_clear_prepare_state(player);
}
} else if (type == PLAYER_T_CLOUD) {
app_player_item_t *player = &s_app_player->audio_player;
if(__player_prepare_check(player, "audio_play")) {
// Reset Cloud Player
app_player_reset(PLAYER_T_CLOUD);
}
__player_clear_prepare_state(player);
player->playid++;
__player_cache_callback(player, cb, player->playid);
if (cb) cb(APP_PLAYER_PREPARING);
player->pause_preparing = false;
if (strncmp("http", url, 4) == 0 || strncmp("mem://", url, 6) == 0) {
lisa_player_throw_low_energy(player->hld, throw_time_ms);
player->is_preparing = true;
ret = lisa_player_seturl(player->hld, url);
if (ret != PLAYER_OK) {
LISA_LOGE(TAG, "audio seturl failed: %d, url=%s", ret, url);
__player_clear_prepare_state(player);
}
} else if (strncmp("stream", url, 6) == 0) {
player->is_preparing = true;
ret = lisa_player_seturl(player->hld, url);
if (ret != PLAYER_OK) {
LISA_LOGE(TAG, "stream seturl failed: %d, url=%s", ret, url);
__player_clear_prepare_state(player);
}
}
}
}
int app_player_put_stream_data(player_t type, uint8_t *data, uint32_t size, uint32_t wait_ms)
{
if (!s_app_player) return -1;
if (!(s_app_player->tone_player.hld) || (APP_PLAYER_USE_AUDIO_PLAYER && !(s_app_player->audio_player.hld))) return -1;
app_player_item_t *player = NULL;
if (type == PLAYER_T_TONE) {
player = &s_app_player->tone_player;
} else if (type == PLAYER_T_CLOUD) {
player = &s_app_player->audio_player;
}
if (player)
{
return lisa_player_put_stream_data(player->hld, data, size, wait_ms);
}
return -1;
}
void app_player_pause(player_t type)
{
if (!s_app_player) return;
if (!(s_app_player->tone_player.hld) || (APP_PLAYER_USE_AUDIO_PLAYER && !(s_app_player->audio_player.hld))) return;
pa_manager_refresh(PA_MGR_OFF, LS_PA_BASE_TIME, "player pause");
app_player_item_t *player = NULL;
if (type == PLAYER_T_TONE) {
player = &s_app_player->tone_player;
} else if (type == PLAYER_T_CLOUD) {
player = &s_app_player->audio_player;
}
if (player)
{
if (player->is_preparing) {
player->pause_preparing = true;
} else {
lisa_player_pause(player->hld);
}
}
}
void app_player_resume(player_t type)
{
if (!s_app_player) return;
if (!(s_app_player->tone_player.hld) || (APP_PLAYER_USE_AUDIO_PLAYER && !(s_app_player->audio_player.hld))) return;
pa_manager_refresh(PA_MGR_ON, LS_PA_FOREVER, "player resume");
app_player_item_t *player = NULL;
if (type == PLAYER_T_TONE) {
player = &s_app_player->tone_player;
} else if (type == PLAYER_T_CLOUD) {
player = &s_app_player->audio_player;
}
if (player)
{
if (player->pause_preparing) {
player->pause_preparing = false;
lisa_player_play(player->hld);
} else {
lisa_player_resume(player->hld);
}
}
}
void app_player_resume_sync(player_t type)
{
if (!s_app_player) return;
if (!(s_app_player->tone_player.hld) || (APP_PLAYER_USE_AUDIO_PLAYER && !(s_app_player->audio_player.hld))) return;
pa_manager_refresh(PA_MGR_ON, LS_PA_FOREVER, "player resume sync");
app_player_item_t *player = NULL;
if (type == PLAYER_T_TONE) {
player = &s_app_player->tone_player;
} else if (type == PLAYER_T_CLOUD) {
player = &s_app_player->audio_player;
}
if (player)
{
if (player->pause_preparing) {
player->pause_preparing = false;
lisa_player_play(player->hld);
} else {
lisa_player_resume_sync(player->hld);
}
}
}
void app_player_stop(player_t type)
{
if (!s_app_player) return;
if (!(s_app_player->tone_player.hld) || (APP_PLAYER_USE_AUDIO_PLAYER && !(s_app_player->audio_player.hld))) return;
app_player_item_t *player = NULL;
char *tips = "none";
if (type == PLAYER_T_TONE) {
player = &s_app_player->tone_player;
tips = "tone_stop";
} else if (type == PLAYER_T_CLOUD) {
player = &s_app_player->audio_player;
tips = "audio_stop";
}
pa_manager_refresh(PA_MGR_OFF, LS_PA_BASE_TIME, tips);
if (player)
{
if (__player_prepare_check(player, tips)) {
lisa_player_stop(player->hld);
}
__player_clear_prepare_state(player);
}
}
void app_player_stop_sync(player_t type)
{
if (!s_app_player) return;
if (!(s_app_player->tone_player.hld) || (APP_PLAYER_USE_AUDIO_PLAYER && !(s_app_player->audio_player.hld))) return;
app_player_item_t *player = NULL;
char *tips = "none";
if (type == PLAYER_T_TONE) {
player = &s_app_player->tone_player;
tips = "tone_stop_sync";
} else if (type == PLAYER_T_CLOUD) {
player = &s_app_player->audio_player;
tips = "audio_stop_sync";
}
pa_manager_refresh(PA_MGR_OFF, LS_PA_BASE_TIME, tips);
if (__player_prepare_check(player, tips)) {
int ret = lisa_player_stop_sync(player->hld);
if (ret != PLAYER_OK)
{
__player_broadcast_status(player, PLAYER_EVT_STOPED);
}
} else {
__player_broadcast_status(player, PLAYER_EVT_STOPED);
}
__player_clear_prepare_state(player);
}
void app_player_seek(player_t type, uint32_t seek_ms)
{
if (!s_app_player) return;
if (!(s_app_player->tone_player.hld) || (APP_PLAYER_USE_AUDIO_PLAYER && !(s_app_player->audio_player.hld))) return;
if (type == PLAYER_T_TONE) {
lisa_player_seek(s_app_player->tone_player.hld, seek_ms);
} else if (type == PLAYER_T_CLOUD) {
lisa_player_seek(s_app_player->audio_player.hld, seek_ms);
}
}
uint32_t app_player_position(player_t type)
{
if (!s_app_player) return 0;
if (!(s_app_player->tone_player.hld) || (APP_PLAYER_USE_AUDIO_PLAYER && !(s_app_player->audio_player.hld))) return 0;
if (type == PLAYER_T_TONE) {
return lisa_player_get_pos(s_app_player->tone_player.hld);
} else if (type == PLAYER_T_CLOUD) {
return lisa_player_get_pos(s_app_player->audio_player.hld);
}
return 0;
}
uint32_t app_player_duration(player_t type)
{
if (!s_app_player) return 0;
if (!(s_app_player->tone_player.hld) || (APP_PLAYER_USE_AUDIO_PLAYER && !(s_app_player->audio_player.hld))) return 0;
if (type == PLAYER_T_TONE) {
return lisa_player_get_duration(s_app_player->tone_player.hld);
} else if (type == PLAYER_T_CLOUD) {
return lisa_player_get_duration(s_app_player->audio_player.hld);
}
return 0;
}
void app_player_volume(player_t type, uint8_t volume)
{
if (!s_app_player) return;
if (!(s_app_player->tone_player.hld) || (APP_PLAYER_USE_AUDIO_PLAYER && !(s_app_player->audio_player.hld))) return;
app_player_item_t *player = NULL;
char *tips = "none";
if (type == PLAYER_T_TONE) {
player = &s_app_player->tone_player;
tips = "tone";
} else if (type == PLAYER_T_CLOUD) {
player = &s_app_player->audio_player;
tips = "cloud";
}
if (player)
{
int standard_range = APP_PLAYER_VOL_RANGE_MAX - APP_PLAYER_VOL_RANGE_MIN + 1;
int player_range = player->vol_range.max - player->vol_range.min;
int regular_vol = ((volume * player_range) / standard_range) + player->vol_range.min;
if (regular_vol < player->vol_range.min) {
regular_vol = player->vol_range.min;
}
if (regular_vol > player->vol_range.max) {
regular_vol = player->vol_range.max;
}
LISA_LOGD(TAG, "%s vol %d -> %d with [%d, %d]",
tips,
volume,
regular_vol,
player->vol_range.min,
player->vol_range.max);
lisa_player_set_vol(player->hld, regular_vol);
}
}
void app_player_set_vol_range(player_t type, uint8_t min, uint8_t max)
{
if (!s_app_player) return;
if (!(s_app_player->tone_player.hld) || (APP_PLAYER_USE_AUDIO_PLAYER && !(s_app_player->audio_player.hld))) return;
app_player_item_t *player = NULL;
if (type == PLAYER_T_TONE) {
player = &s_app_player->tone_player;
} else if (type == PLAYER_T_CLOUD) {
player = &s_app_player->audio_player;
}
if (player)
{
player->vol_range.min = min;
player->vol_range.max = max;
}
}
void app_player_reset(player_t type)
{
if (!s_app_player) return;
if (!(s_app_player->tone_player.hld) || (APP_PLAYER_USE_AUDIO_PLAYER && !(s_app_player->audio_player.hld))) return;
app_player_item_t *player = NULL;
if (type == PLAYER_T_TONE) {
player = &s_app_player->tone_player;
lisa_player_reset(s_app_player->tone_player.hld);
} else if (type == PLAYER_T_CLOUD) {
player = &s_app_player->audio_player;
lisa_player_reset(s_app_player->audio_player.hld);
}
__player_clear_prepare_state(player);
}
void app_player_close(player_t type)
{
if (!s_app_player) return;
if (!(s_app_player->tone_player.hld) || (APP_PLAYER_USE_AUDIO_PLAYER && !(s_app_player->audio_player.hld))) return;
if (type == PLAYER_T_TONE) {
lisa_player_pre_close(s_app_player->tone_player.hld);
} else if (type == PLAYER_T_CLOUD) {
lisa_player_pre_close(s_app_player->audio_player.hld);
}
}
#if 0
void app_player_set_data_hook(player_t type, app_player_data_hook hook, int flag)
{
if (!s_app_player) return;
if (!(s_app_player->tone_player.hld) || (APP_PLAYER_USE_AUDIO_PLAYER && !(s_app_player->audio_player.hld))) return;
extern PlayerErr lisa_player_hook_data(PLAYER_HANDLE h, app_player_data_hook hook, int type);
if (type == PLAYER_T_TONE) {
lisa_player_hook_data(s_app_player->tone_player.hld, hook, flag);
} else if (type == PLAYER_T_CLOUD) {
lisa_player_hook_data(s_app_player->audio_player.hld, hook, flag);
}
}
#endif

View File

@@ -0,0 +1,64 @@
/**
* @brief 播放器管理
* @version 0.1
* @date 2022-08-24
* @author mokee
*
* Copyright (C) 2022 ANHUI LISTENAI Co., LTD All Rights Reserved
*/
#ifndef __LISTENAI_APP_PLAYER_H__
#define __LISTENAI_APP_PLAYER_H__
#include <stdint.h>
#include "lisa_player.h"
typedef uint8_t player_t;
#define PLAYER_T_CLOUD (1)
#define PLAYER_T_TONE (2)
#define APP_PLAYER_PREPARING (0xEF)
typedef int (*player_status_cb)(uint16_t st);
typedef void (*app_player_data_hook)(const char *const data, uint32_t size);
/**
* @brief 应用层播放器初始化
*/
void app_player_init();
void app_player_play(player_t type, char *url, player_status_cb cb);
void app_player_play_by_throw(player_t type, char *url, int throw_time_ms, player_status_cb cb);
int app_player_put_stream_data(player_t type, uint8_t *data, uint32_t size, uint32_t wait_ms);
void app_player_pause(player_t type);
void app_player_resume(player_t type);
void app_player_resume_sync(player_t type);
void app_player_stop(player_t type);
void app_player_stop_sync(player_t type);
void app_player_seek(player_t type, uint32_t seek_ms);
uint32_t app_player_position(player_t type);
uint32_t app_player_duration(player_t type);
void app_player_volume(player_t type, uint8_t volume);
void app_player_set_vol_range(player_t type, uint8_t min, uint8_t max);
void app_player_reset(player_t type);
void app_player_close(player_t type);
void app_player_set_data_hook(player_t type, app_player_data_hook hook, int flag);
#endif

View File

@@ -0,0 +1,14 @@
/**
* Audio Common Header File
*/
#ifndef __LISTENAI_AUDIO_COMM_H__
#define __LISTENAI_AUDIO_COMM_H__
typedef enum
{
ap2cp_play_stream_id = 0,
ap2cp_record_stream_id,
} ic_stream_id_e;
#endif

View File

@@ -0,0 +1,17 @@
#include <stdint.h>
#include "lisa_device.h"
#include "lisa_audio.h"
#define TAG "audio_play"
#include "lisa_log.h"
void audio_play_send_pcm(char *data, int size)
{
static lisa_device_t *audio_dev = NULL;
if(audio_dev == NULL){
audio_dev = lisa_device_get("audio0");
}
int ret = lisa_audio_play_write(audio_dev, (int16_t *)data,size / 2);
}

View File

@@ -0,0 +1,200 @@
#define TAG "PA_MGR"
#include "systick.h"
#include "lisa_log.h"
#include "lisa_mem.h"
#include "evs_utils.h"
#include "lisa_mutex.h"
#include "pa_manager.h"
#include "lisa_timer.h"
#include "Driver_GPIO.h"
#include "IOMuxManager.h"
#include "lisa_typedef.h"
typedef struct pa_manager_s {
PA_MGR_STATE m_state;
uint32_t m_timer_period;
lisa_timer_t *m_change_timer;
lisa_mutex_t *m_lock;
} pa_manager_t;
static pa_manager_t *s_pa_hdl = NULL;
#ifdef CONFIG_BOARD_ARCS_MINI
#include "pinmux.h"
#define PA_CONTROL_IO_PAD CSK_IOMUX_PAD_A
#define PA_CONTROL_IO_NUM PA_EN_PIN
#else // CONFIG_BOARD_ARCS_MINI
#define PA_CONTROL_IO_PAD CONFIG_PA_PORT
#define PA_CONTROL_IO_NUM CONFIG_PA_PIN
#define PA_CONTROL_IO_FUNC CONFIG_PA_FUNC
#endif // CONFIG_BOARD_ARCS_MINI
#define PA_CONTROL_IO_POS (1 << PA_CONTROL_IO_NUM)
#define PA_OUT_ON (1)
#define PA_OUT_OFF (0)
static void *PA_DRV_HANDLE = NULL;
void pa_manager_pre_init()
{
LISA_LOGI(TAG, "pa manager pre init");
PA_DRV_HANDLE = (PA_CONTROL_IO_PAD == CSK_IOMUX_PAD_A) ? GPIOA() : GPIOB();
#ifndef CONFIG_BOARD_ARCS_MINI
IOMuxManager_PinConfigure(PA_CONTROL_IO_PAD, PA_CONTROL_IO_NUM, PA_CONTROL_IO_FUNC);
#endif // CONFIG_BOARD_ARCS_MINI
GPIO_Control(PA_DRV_HANDLE, CSK_GPIO_DEBOUNCE_DISABLE, PA_CONTROL_IO_POS);
GPIO_SetDir(PA_DRV_HANDLE, PA_CONTROL_IO_POS, CSK_GPIO_DIR_OUTPUT);
GPIO_PinWrite(PA_DRV_HANDLE, PA_CONTROL_IO_POS, PA_OUT_OFF);
}
int pa_manager_onoff(int onoff)
{
if (PA_DRV_HANDLE != NULL) {
if (onoff) {
LISA_LOGI(TAG, "PA ON");
for(volatile int i = 0; i < 7; i++) {
GPIO_PinWrite(PA_DRV_HANDLE, PA_CONTROL_IO_POS, PA_OUT_OFF);
SysTick_Delay_Us(50);
GPIO_PinWrite(PA_DRV_HANDLE, PA_CONTROL_IO_POS, PA_OUT_ON);
SysTick_Delay_Us(50);
}
} else {
LISA_LOGI(TAG, "PA OFF");
GPIO_PinWrite(PA_DRV_HANDLE, PA_CONTROL_IO_POS, PA_OUT_OFF);
SysTick_Delay_Us(50);
}
return 0;
}
return -1;
}
/**
* @brief Switch PA State
* @param next_state Next PA State
*/
static void __pa_switch_state(PA_MGR_STATE next_state)
{
if (s_pa_hdl) {
lisa_mutex_lock(s_pa_hdl->m_lock, LISA_OS_WAIT_FOREVER);
switch (next_state) {
case PA_MGR_OFF: {
if (s_pa_hdl->m_state != PA_MGR_OFF) {
// pa off
if (pa_manager_onoff(0) == 0) {
// set state
s_pa_hdl->m_state = PA_MGR_OFF;
}
}
} break;
case PA_MGR_ON: {
if (s_pa_hdl->m_state != PA_MGR_ON) {
// pa on
if (pa_manager_onoff(1) == 0) {
// set state
s_pa_hdl->m_state = PA_MGR_ON;
}
}
} break;
case PA_MGR_NONE: {
s_pa_hdl->m_state = PA_MGR_NONE;
};
}
lisa_mutex_unlock(s_pa_hdl->m_lock);
}
}
static int __handle_pa_off_runnable(void *arg)
{
pa_manager_t *hdl = (pa_manager_t *)arg;
LISA_LOGI(TAG, "PA off, by timeout");
__pa_switch_state(PA_MGR_OFF);
return 0;
}
/**
* @brief PA OFF Timer Callback
* @param timer Timer Handle
*/
static void __pa_off_timeout(lisa_timer_t *timer)
{
if (timer && timer->arg) {
evs_handler_post_runnable(__handle_pa_off_runnable, timer->arg);
}
}
void pa_manager_init(PA_MGR_STATE init_state)
{
s_pa_hdl = (pa_manager_t *)lisa_mem_alloc(sizeof(pa_manager_t));
LISA_ASSERT(s_pa_hdl, "pa handle null");
s_pa_hdl->m_change_timer = lisa_timer_create(LS_PA_BASE_TIME, __pa_off_timeout, s_pa_hdl);
LISA_ASSERT(s_pa_hdl->m_change_timer, "pa change timer null");
s_pa_hdl->m_lock = lisa_mutex_create();
LISA_ASSERT(s_pa_hdl->m_lock, "pa lock null");
s_pa_hdl->m_timer_period = LS_PA_BASE_TIME;
s_pa_hdl->m_state = PA_MGR_NONE;
if (init_state != PA_MGR_NONE)
{
pa_manager_refresh(init_state, LS_PA_BASE_TIME, "pa_mgr_init");
}
}
void pa_manager_refresh(PA_MGR_STATE next_state, uint32_t duration, const char *const by_which)
{
if (!s_pa_hdl) return;
LISA_LOGD(TAG, "Refresh PA to %s, timeout %u, by \"%s\"",
PA_PRINT_STATE(next_state), duration, by_which);
if (next_state == PA_MGR_ON) {
__pa_switch_state(next_state);
} else if (next_state == PA_MGR_OFF) {
// delay change PA state to off
if (duration == 0) {
__pa_switch_state(next_state);
// stop change timer
lisa_timer_stop(s_pa_hdl->m_change_timer);
}
}
if (duration == LS_PA_FOREVER) {
lisa_timer_stop(s_pa_hdl->m_change_timer);
} else {
// launch change to off timer
if (duration != s_pa_hdl->m_timer_period) {
lisa_timer_change_period(s_pa_hdl->m_change_timer, duration);
s_pa_hdl->m_timer_period = duration;
// Restart Timer
lisa_timer_reset(s_pa_hdl->m_change_timer);
} else {
lisa_timer_stop(s_pa_hdl->m_change_timer);
lisa_timer_start(s_pa_hdl->m_change_timer);
}
}
LISA_LOGD(TAG, "Refresh PA end by \"%s\"", by_which);
}
void pa_manager_reset_state(const char *const by_which)
{
LISA_LOGD(TAG, "Reset PA to %s, by \"%s\"", PA_PRINT_STATE(PA_MGR_NONE), by_which);
__pa_switch_state(PA_MGR_NONE);
}
PA_MGR_STATE pa_manager_get_state()
{
if (s_pa_hdl) {
return s_pa_hdl->m_state;
}
return PA_MGR_NONE;
}

View File

@@ -0,0 +1,72 @@
/**
* @brief PA Manager
* @version 0.1
* @date 2022-11-04
* @author mokee
*
* Copyright (C) 2022 ANHUI LISTENAI Co., LTD All Rights Reserved
*/
#ifndef __LISTENAI_PA_MANAGER_H__
#define __LISTENAI_PA_MANAGER_H__
#include <stdint.h>
// PA delay off time
#define LS_PA_BASE_TIME (30 * 1000)
// Forever
#define LS_PA_FOREVER (0xffffffffUL)
typedef enum PA_MGR_STATE {
// PA OFF
PA_MGR_OFF = 0,
// PA ON
PA_MGR_ON = 1,
PA_MGR_NONE = 0xFF,
} PA_MGR_STATE;
#define PA_PRINT_STATE(s) \
(s == PA_MGR_ON)? "ON": \
(s == PA_MGR_OFF)? "OFF": \
(s == PA_MGR_NONE)? "NONE":"UNKNOW"
/**
* @brief PA GPIO Init
*/
void pa_manager_pre_init();
/**
* @brief PA OnOFF
* @param onoff 0: OFF, other: ON
*/
int pa_manager_onoff(int onoff);
/**
* @brief Init PA Manager
* @param init_state Init State
*/
void pa_manager_init(PA_MGR_STATE init_state);
/**
* @brief Refresh PA state
* @param next_state Next state of PA
* @param duration Time(ms) of switching to off after change PA state
* default: LS_PA_BASE_TIME
*
* @param by_which Caller
*/
void pa_manager_refresh(PA_MGR_STATE next_state, uint32_t duration, const char *const by_which);
/**
* @brief 重置 PA state
* @param by_which Caller
*/
void pa_manager_reset_state(const char *const by_which);
/**
* @brief 获取 PA state
*/
PA_MGR_STATE pa_manager_get_state();
#endif

View File

@@ -0,0 +1,13 @@
if(CONFIG_BATTERY_COLLECTION)
listenai_library_named(battery)
listenai_library_sources(
battery.c
)
listenai_include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
)
endif()

View File

@@ -0,0 +1,21 @@
config BATTERY_COLLECTION
bool "Enable battery data collection"
default n
depends on POWER_MANAGER
if BATTERY_COLLECTION
config BATTERY_COLLECTION_ADC_PAD
string "Battery Voltage ADC Pad (\"gpioa\" or \"gpiob\")"
default "gpioa"
config BATTERY_COLLECTION_ADC_CHANNEL
int "Battery Voltage ADC Channel Number"
default 3
range 0 7
config BATTERY_COLLECTION_CHARGE_DETECT_PAD
string "Battery Charge Detect GPIO Pad (\"gpioa\" or \"gpiob\")"
default "gpiob"
endif

View File

@@ -0,0 +1,332 @@
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h> // 添加abs函数需要的头文件
#include "arcs_ap.h"
#include "lisa_log.h"
#include "lisa_mutex.h"
#include "battery.h"
#include "power/power_manager.h"
#include "lisa_timer.h"
#include "board.h"
#include "lisa_gpio.h"
#include "lisa_adc.h"
#include "voice_msg.h"
#define TAG "battery"
#define BAT_ADC_PAD CONFIG_BATTERY_COLLECTION_ADC_PAD
#define BAT_ADC_CH CONFIG_BATTERY_COLLECTION_ADC_CHANNEL
#define CHARGE_DET_PAD CONFIG_BATTERY_COLLECTION_CHARGE_DETECT_PAD
#define VBAT_MAX_VOLTAGE (4350) /* 锂电池理论最大电压 */
#define VBAT_MIN_VOLTAGE (3500) /* 锂电池理论最小电压 */
#define VBAT_PARTIAL_VOLTAGE_PERCENTAGE (40) /* 硬件电池分压2/5, 百分比为40 */
#define VBAT_SAMPLE_PRIOD (1000)
/* 低于该电压认为未接入电池单位mV */
#define VBAT_PRESENT_THRESHOLD (2000)
// 电压滤波相关定义
#define VOLTAGE_FILTER_WINDOW_SIZE 5
#define PERCENTAGE_CHANGE_THRESHOLD 10 // 电量百分比变化阈值
static lisa_device_t *bat_adc_dev = NULL;
static lisa_device_t *charge_det_dev = NULL;
lisa_timer_t *battery_timer = NULL;
// 移动平均滤波器变量
static uint16_t voltage_history[VOLTAGE_FILTER_WINDOW_SIZE] = {0};
static uint8_t voltage_index = 0;
static bool voltage_buffer_full = false;
static uint16_t last_vbat_voltage_mv = 0;
static uint16_t last_vbat_detect_mv = 0;
// 电池电压百分比查找表 (按10%步进从0%到100%)
// 请根据实际电池特性填充对应的电压值 (单位: mV)
//
// 建议测量方法:
// 1. 将电池充满到100%,记录电压值
// 2. 以10%为步长,逐步放电并记录对应电压值
// 3. 确保测量时电池处于静置状态(非充放电状态)
// 4. 多次测量取平均值以提高准确性
//
static const uint16_t battery_voltage_table_discharge[11] = {
3300, // 0% - 最低工作电压,系统关机电压
3783, // 10%
3843, // 20%
3870, // 30%
3885, // 40%
3919, // 50%
3973, // 60%
4024, // 70%
4085, // 80%
4179, // 90%
4199 // 100%
};
// 充电电压百分比查找表 (按10%步进从0%到100%)
// 由于充电时端电压会被抬高,建议使用单独的充电曲线。
// 以下为基于实测的初始值,可根据实际电池/充电IC进一步校准。
static const uint16_t battery_voltage_table_charge[11] = {
3700, // 0%
4000, // 10%
4040, // 20%
4070, // 30%
4090, // 40%
4110, // 50%
4125, // 60%
4138, // 70%
4145, // 80%
4185, // 90%
4210 // 100%
};
/**
* @brief 移动平均滤波器 - 对电压进行平滑滤波
* @param new_voltage 新的电压值 (mV)
* @return 滤波后的电压值 (mV)
*/
static uint16_t voltage_moving_average_filter(uint16_t new_voltage)
{
// 将新电压值存入循环缓冲区
voltage_history[voltage_index] = new_voltage;
voltage_index = (voltage_index + 1) % VOLTAGE_FILTER_WINDOW_SIZE;
// 检查缓冲区是否已满
if (!voltage_buffer_full && voltage_index == 0) {
voltage_buffer_full = true;
}
// 计算平均值
uint32_t sum = 0;
uint8_t count = voltage_buffer_full ? VOLTAGE_FILTER_WINDOW_SIZE : voltage_index;
for (uint8_t i = 0; i < count; i++) {
sum += voltage_history[i];
}
return (uint16_t)(sum / count);
}
static void voltage_filter_reset(uint16_t voltage)
{
for (uint8_t i = 0; i < VOLTAGE_FILTER_WINDOW_SIZE; i++) {
voltage_history[i] = voltage;
}
voltage_index = 0;
voltage_buffer_full = true;
}
/**
* @brief 基于查找表的电池百分比计算
* @param voltage 当前电池电压 (mV)
* @param table 电压-百分比查找表
* @return 电池电量百分比 (0-100)
*/
static uint8_t voltage_to_percentage_by_table(uint16_t voltage, const uint16_t *table)
{
// 边界处理
if (voltage <= table[0]) {
return 0;
}
if (voltage >= table[10]) {
return 100;
}
// 在查找表中寻找合适的区间
for (uint8_t i = 0; i < 10; i++) {
if (voltage >= table[i] && voltage <= table[i + 1]) {
// 线性插值计算精确百分比
uint16_t voltage_diff = table[i + 1] - table[i];
uint16_t current_diff = voltage - table[i];
// 避免除零错误
if (voltage_diff == 0) {
return i * 10;
}
// 计算插值百分比
uint8_t base_percentage = i * 10;
uint8_t interpolated_percentage = (uint8_t)((current_diff * 10) / voltage_diff);
return base_percentage + interpolated_percentage;
}
}
// 如果没有找到合适区间,使用线性计算作为备选
return (uint8_t)((voltage - VBAT_MIN_VOLTAGE) * 100 / (VBAT_MAX_VOLTAGE - VBAT_MIN_VOLTAGE));
}
static void battery_sample_pin_init(void)
{
// Vbat voltage adc sample
bat_adc_dev = lisa_device_get("adc0");
lisa_adc_channel_config_t adc_ch_cfg = {
.reference = LISA_ADC_REF_VDD_3V6,
.resolution = LISA_ADC_RESOLUTION_10BIT,
};
lisa_adc_channel_setup(bat_adc_dev, BAT_ADC_CH, &adc_ch_cfg);
// charge status
charge_det_dev = lisa_device_get(CHARGE_DET_PAD);
lisa_gpio_configure(charge_det_dev, CHARGE_DET_PIN, LISA_GPIO_INPUT);
}
static voice_msg_battery_status_t battery_status_to_msg(battery_status_t status)
{
switch (status) {
case BATTERY_STATUS_NO_BATTERY:
return VOICE_MSG_BATTERY_STATUS_NO_BATTERY;
case BATTERY_STATUS_NOT_CONNECT:
return VOICE_MSG_BATTERY_STATUS_NOT_CONNECT;
case BATTERY_STATUS_CHARGING:
return VOICE_MSG_BATTERY_STATUS_CHARGING;
case BATTERY_STATUS_CHARGE_DONE:
return VOICE_MSG_BATTERY_STATUS_CHARGE_DONE;
case BATTERY_STATUS_UNKNOWN:
default:
return VOICE_MSG_BATTERY_STATUS_UNKNOWN;
}
}
static void battery_voltage_sample_cb(struct lisa_timer *timer)
{
uint8_t raw_percentage = battery_get_pct();
static uint8_t last_percentage = 0xFF;
battery_status_t status = battery_get_status();
static uint8_t last_status = 0xFF;
voice_msg_battery_info_t msg = {
.level = raw_percentage,
.status = battery_status_to_msg(status),
};
if (raw_percentage != last_percentage || status != last_status) {
voice_msg_pub(VOICE_MSG_POWER_BATTERY_UPDATE, &msg, sizeof(msg));
last_percentage = raw_percentage;
last_status = status;
}
LISA_LOGD(TAG, "Battery: raw=%d%%, status=%d", raw_percentage, status);
lisa_timer_start(battery_timer);
}
void battery_init(void)
{
static bool init_flag = false;
if (init_flag) {
return;
}
battery_sample_pin_init();
battery_timer = lisa_timer_create(VBAT_SAMPLE_PRIOD, battery_voltage_sample_cb, NULL);
if (battery_timer) {
lisa_timer_start(battery_timer);
} else {
LISA_LOGE(TAG, "Failed to create battery sample timer");
}
init_flag = true;
return;
}
static uint16_t battery_get_voltage(void)
{
static bool last_usb_plugged = false;
bool usb_plugged;
uint16_t adc_raw_value;
uint16_t adc_real_voltage;
uint16_t vbat_real_voltage;
uint16_t filtered_voltage;
lisa_adc_read(bat_adc_dev, BAT_ADC_CH, &adc_raw_value);
adc_real_voltage = LISA_ADC_RAW_TO_MV(adc_raw_value, 3600, LISA_ADC_RESOLUTION_10BIT);
/* remove Hardware voltage division */
vbat_real_voltage = (uint16_t)(adc_real_voltage * 100 / VBAT_PARTIAL_VOLTAGE_PERCENTAGE);
usb_plugged = power_is_usb_plugged();
if (usb_plugged != last_usb_plugged) {
// 充电状态变化时,重置滤波,避免电压跳变被均值拖尾
voltage_filter_reset(vbat_real_voltage);
filtered_voltage = vbat_real_voltage;
last_usb_plugged = usb_plugged;
} else {
// 应用移动平均滤波
filtered_voltage = voltage_moving_average_filter(vbat_real_voltage);
}
LISA_LOGD(TAG, "adc_raw: %d, vbat_raw: %d, vbat_filtered: %d", adc_real_voltage, vbat_real_voltage,
filtered_voltage);
return filtered_voltage;
}
uint8_t battery_get_pct(void)
{
uint16_t filtered_voltage;
uint8_t vbat_voltage_percentage;
filtered_voltage = battery_get_voltage();
// 电压范围限制
if (filtered_voltage < VBAT_MIN_VOLTAGE) {
filtered_voltage = VBAT_MIN_VOLTAGE;
if (!power_is_usb_plugged()) {
power_shutdown();
}
} else if (filtered_voltage > VBAT_MAX_VOLTAGE) {
filtered_voltage = VBAT_MAX_VOLTAGE;
}
/* 使用查找表获取电池电压百分比 */
if (power_is_usb_plugged()){
vbat_voltage_percentage = voltage_to_percentage_by_table(filtered_voltage, battery_voltage_table_charge);
} else {
vbat_voltage_percentage = voltage_to_percentage_by_table(filtered_voltage, battery_voltage_table_discharge);
}
return vbat_voltage_percentage;
}
battery_status_t battery_get_status(void)
{
static uint8_t s_discharge_static_cnt = 0; // 解决电脑供电时,充电状态不稳定的问题
battery_status_t ret = 0;
uint16_t filtered_voltage;
filtered_voltage = battery_get_voltage();
if (filtered_voltage < VBAT_PRESENT_THRESHOLD) {
s_discharge_static_cnt = 0;
ret = BATTERY_STATUS_NO_BATTERY;
return ret;
} else {
ret = BATTERY_STATUS_NOT_CONNECT;
}
if (power_is_usb_plugged()) {
if (s_discharge_static_cnt < 3) {
s_discharge_static_cnt++;
}else{
ret = BATTERY_STATUS_CHARGING;
}
} else {
s_discharge_static_cnt = 0;
}
const uint16_t *table = power_is_usb_plugged() ? battery_voltage_table_discharge : battery_voltage_table_charge;
if (voltage_to_percentage_by_table(filtered_voltage, table) >= 98) {
ret = BATTERY_STATUS_CHARGE_DONE;
}
return ret;
}

View File

@@ -0,0 +1,49 @@
/*
* @file battery.h
* @brief
* @version 0.1
* @date 2025-04-16
*
* @copyright Copyright (C) 2025 ANHUI LISTENAI Co., Ltd. All Rights Reserved.
*/
#ifndef __BATTERY_H__
#define __BATTERY_H__
#if defined(__cplusplus)
extern "C" {
#endif
#include <stdint.h>
/**
* @brief 电池状态
*/
typedef enum {
BATTERY_STATUS_NO_BATTERY, // 无电池
BATTERY_STATUS_NOT_CONNECT, // 未连接
BATTERY_STATUS_CHARGING, // 充电中
BATTERY_STATUS_CHARGE_DONE, // 充电完成
BATTERY_STATUS_UNKNOWN, // 未知状态
} battery_status_t;
void battery_init(void);
/**
* @brief 获取电池电量(百分比)
*
*/
uint8_t battery_get_pct(void);
/**
* @brief 获取电池状态
*
* @return battery_status_t
*/
battery_status_t battery_get_status(void);
#if defined(__cplusplus)
}
#endif
#endif

View File

@@ -0,0 +1,10 @@
listenai_library_named(app-ble)
listenai_include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
)
listenai_library_sources(
app_ble_common.c
app_net_cfg.c
)

View File

@@ -0,0 +1,120 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include "lisa_bluetooth.h"
#include "bt_app_if.h"
#include "bt_stack_if.h"
#include "netcfg_bles.h"
#include "diss.h"
#define LOG_TAG "app_ble_common"
#include <lisa_log.h>
uint8_t manufacturer_data[18] = {
0xab, 0x0a, 0xa1, 0xdc, 0xa8, 0x76, 0x83, 0x65, 0x73, 0x83, 0x72, 0x65, 0x82, 0x67, 0x83, 0x68, 0x00, 0x78,
};
ble_gap_cfg_t user_bt_stack_dev_cfg = {
.addr = {{0x44, 0x55, 0x66, 0x03, 0x23, 0x20}, 0},
.name_len = sizeof(DEVICE_NAME),
.name = DEVICE_NAME,
.appearance = GAP_APP_GENERIC_MEDIA_PLAYER, // hid_keyboard
.iocap = GAP_IO_CAP_NO_INPUT_NO_OUTPUT,
.auth = GAP_SEC_NOT_ENC,
.pairing_mode = GAPM_PAIRING_LEGACY,
};
#if (ADV_USER_DATA)
uint8_t lisa_ble_gen_user_adv_data(uint8_t *p_data)
{
uint8_t nb_uuid = 1;
uint16_t uuids[1] = {HID_UUID};
// Remaining Length
uint8_t rem_len = LEGA_ADV_DATA_LEN - 3;
uint8_t *p_buf = p_data;
uint8_t length = 0;
/// add Manufacturer specific
*p_buf++ = sizeof(manufacturer_data) + 1;
*p_buf++ = 0xff; // GAP_AD_TYPE_MANU_SPECIFIC_DATA;
memcpy(p_buf, manufacturer_data, 18);
p_buf += sizeof(manufacturer_data);
length += (sizeof(manufacturer_data) + 2);
// Sanity check
assert(rem_len >= LEGA_ADV_DATA_LEN - 3);
// Get remaining space in the Advertising Data - 2 bytes are used for name length/flag
rem_len -= length;
// Check if additional data can be added to the Advertising data - 2 bytes needed for type and length
if (rem_len > 2) {
uint8_t dev_name_length = MIN(user_bt_stack_dev_cfg.name_len, (rem_len - 2));
// Device name length
*p_buf = dev_name_length + 1;
// Device name flag (check if device name is complete or not)
*(p_buf + 1) = (dev_name_length == user_bt_stack_dev_cfg.name_len)
? 0x09
: 0x08; // GAP_AD_TYPE_COMPLETE_NAME : GAP_AD_TYPE_SHORTENED_NAME;
// Copy device name
memcpy(p_buf + 2, user_bt_stack_dev_cfg.name, dev_name_length);
// Update advertising data length
length += (dev_name_length + 2);
}
return length;
}
#endif
uint8_t adv_user_data[LEGA_ADV_DATA_LEN];
const uint8_t *lisa_bt_get_adv_data(uint8_t *len)
{
*len = lisa_ble_gen_user_adv_data(adv_user_data);
return adv_user_data;
}
extern uint16_t netcfg_bles_profile_set_cb(uint8_t conidx, uint8_t att_idx, uint16_t op, uint8_t *p_value);
/// Message callback handle from APP
static const netcfg_bles_cb_t netcfg_app_cb = {
.cb_value_set = netcfg_bles_profile_set_cb,
};
const diss_cb_t user_bt_stack_ble_diss_msg_cb = {
.cb_value_get = NULL, // dis_profile_get_cb,
};
/**
* @brief 初始化自定义服务
*
* 注册 GATT 用户回调并添加服务到数据库。
* 此函数应在协议栈初始化完成后调用。
*/
void app_ble_init_cmp(void)
{
// enable bass service
ble_bass_init();
ble_bass_enable(0, bt_stack_vbat_percent_get());
// enable net config.
ble_netcfg_bles_init((netcfg_bles_cb_t *)&netcfg_app_cb);
// enable diss.
ble_diss_init((diss_cb_t *)&user_bt_stack_ble_diss_msg_cb);
#if BLE_VOICE_SIMULATOR
// enable hid service
uint8_t svc_features = HOGPD_CFG_KEYBOARD | HOGPD_CFG_MOUSE | HOGPD_CFG_PROTO_MODE | HOGPD_CFG_REPORT_NTF_EN;
uint8_t report_char_cfg = HOGPD_CFG_REPORT_IN;
hogpd_report_map_t report_map = {sizeof(hid_report_map), 0, (uint8_t *)hid_report_map};
ble_hogpd_init(svc_features, report_char_cfg, (hogpd_cb_t *)&bt_stack_ble_hogpd_msg_cb, &report_map);
ble_hogpd_enable(0);
#endif
}

View File

@@ -0,0 +1,295 @@
/**
****************************************************************************************
*
* @file netcfg.c
*
* @brief net config
*
* Copyright (C) ListenAI 2020-2099
*
*
****************************************************************************************
*/
/*
* MACROS
****************************************************************************************
*/
#if defined(CONFIG_CLOUD_PRODUCT_ID_DEFAULT)
#define PRODUCT_ID CONFIG_CLOUD_PRODUCT_ID_DEFAULT
#else
#define PRODUCT_ID "cf75e7a9-66b6-41e3-918a-141800aebb5b"
#endif
#if defined(CONFIG_CLOUD_SECRET_ID_DEFAULT)
#define SECRET_ID CONFIG_CLOUD_SECRET_ID_DEFAULT
#else
#define SECRET_ID "e354f1f0-34e5-488f-8ef2-f6db2a44c815"
#endif
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <stdio.h>
#include "netcfg_ble.h"
#include "netcfg_bles.h"
#include "ble_gap.h"
#include "ble_gatt.h"
#include "ble_prf.h"
#include "ls_wifi_type.h"
#include "nvds_tag_def.h"
#include "wifi_api.h"
#include "btos_al.h"
#include "lisa_log.h"
#include "lisa_kv.h"
#include "../kv/kv_user.h"
// #include "../cloud/config/aiui_cfg.h"
// #include "assistant_controller.h"
#include "wifi_manager/wifi_manager.h"
#include "lisa_thread.h"
#include "lisa_mem.h"
#include "tone.h"
#include "player_mgr.h"
static char *TAG = "netcfg_ble";
extern void HAL_PMU_Chip_Software_Reset_Enable(void);
void netcfg_bles_send_connect_status_dummy(uint32_t milli_seconds);
uint16_t netcfg_ble_notify_wifi(struct netcfg_ble_data *data);
uint8_t app_ble_netcfg_bles_send_notify(uint8_t conidx, uint16_t op, uint16_t status, uint16_t len, uint8_t *data);
void app_ble_adv_stop(uint8_t reason);
typedef struct {
uint8_t conidx;
uint16_t op;
struct netcfg_ble_data *p_value;
} ble_notify_param_t;
static void ble_notify_thread(void *param)
{
ble_notify_param_t *notify_param = (ble_notify_param_t *)param;
uint16_t status = NETCFG_BLE_ERR;
uint8_t notify_ret = 0;
if (notify_param != NULL) {
status = netcfg_ble_notify_wifi(notify_param->p_value);
notify_ret = app_ble_netcfg_bles_send_notify(notify_param->conidx, notify_param->op, status, 0, NULL);
LOGI("netcfg_bles_profile_set_cb notify status:0x%04X, ret:%d", status, notify_ret);
if (status == NETCFG_BLE_SUCCESS) {
#ifdef CONFIG_BOARD_ARCS_MINI
LOGI("BLE config success, playing tone 72");
player_mgr_play(LOCAL, app_tone_get_url(TONE_ID_72), 0);
#else // !CONFIG_BOARD_ARCS_MINI
LOGI("BLE config success, playing tone 10");
player_mgr_play(LOCAL, app_tone_get_url(TONE_ID_10), 0);
#endif // CONFIG_BOARD_ARCS_MINI
}
lisa_mem_free(notify_param);
}
vTaskDelete(NULL);
}
static int netcfg_ble_wifi_connect(const int8_t *ssid, const int8_t *pwd)
{
int result = -1;
if (!ssid || !pwd) {
return result;
}
#if CONFIG_WIFI_MANAGER
wifi_mgr_sta_config_t sta_config = { 0 };
strcpy(sta_config.ssid, (const char *)ssid);
strcpy(sta_config.pwd, (const char *)pwd);
LOGI("netcfg_ble_wifi_connect wifi_mgr_sta_connect ssid:%s, pwd:%s", ssid, pwd);
result = wifi_mgr_sta_connect(&sta_config, false);
#else
wifi_connect_cfg_t sta_config = {
.dhcp_mode = DHCP_CLIENT,
};
strcpy(sta_config.ssid, (const char *)ssid);
strcpy(sta_config.key, (const char *)pwd);
result = wifi_sta_connect(&sta_config);
#endif
LISA_LOGI(TAG, "netcfg_ble_wifi_connect ssid:%s, pwd:%s", ssid, pwd);
LISA_LOGI(TAG, "netcfg_ble_wifi_connect result:%d", result);
return result;
}
uint16_t netcfg_ble_notify_wifi(struct netcfg_ble_data *data)
{
int status = 0;
#if NETCFG_BLE_DBG
NETCFG_BLE_LOGD("[%s]: Get ssid = %s, pwd = %s\n", __func__, data->ssid, data->pwd);
#endif
LISA_LOGI(TAG, "Get ssid = %s, pwd = %s\n", data->ssid, data->pwd);
/*
* Fix me: Instead of directly call wifi connect, we need to create a task handling
* the messages and wifi/ble status exchange here.
* Will implement this later.
*/
status = netcfg_ble_wifi_connect((const int8_t *)data->ssid, (const int8_t *)data->pwd);
if (status != 0) {
return NETCFG_BLE_ERR;
}
return NETCFG_BLE_SUCCESS;
}
/**
* @brief Get product ID from KV storage or default macro
*
* @param product_id Buffer to store product ID
* @param max_len Maximum buffer length
* @return int 0 on success, -1 on failure
*/
static int get_current_product_id(char *product_id, int max_len)
{
if (!product_id || max_len <= 0) {
return -1;
}
char *pid = NULL;
int ret = lisa_kv_get_string(KV_KEY_USER_PID, &pid);
if (ret != 0 || pid == NULL) {
if (strlen(PRODUCT_ID) >= max_len) {
return -1;
}
strcpy(product_id, PRODUCT_ID);
return 0;
} else {
if (strlen(pid) >= max_len) {
lisa_kv_free(pid);
return -1;
}
strcpy(product_id, pid);
lisa_kv_free(pid);
return 0;
}
}
/**
* @brief Get device ID from KV storage or chip hardware ID
*
* @param device_id Buffer to store device ID
* @param max_len Maximum buffer length
* @return int 0 on success, -1 on failure
*/
int get_current_device_id(char *device_id, int max_len)
{
if (!device_id || max_len <= 0) {
return -1;
}
char *kv_device_id = NULL;
int ret = lisa_kv_get_string(KV_KEY_USER_DEVICE_ID, &kv_device_id);
if (ret == 0 && kv_device_id != NULL && strlen(kv_device_id) > 0) {
if (strlen(kv_device_id) >= max_len) {
lisa_kv_free(kv_device_id);
return -1;
}
strcpy(device_id, kv_device_id);
lisa_kv_free(kv_device_id);
return 0;
} else {
// KV中没有设备ID从芯片读取硬件ID
uint32_t *id_1 = (uint32_t *)0x48600208;
uint32_t *id_2 = (uint32_t *)0x4860020c;
uint8_t id_buffer[8];
if (*id_1 == 0 && *id_2 == 0) {
// 芯片ID也是全零返回错误
return -1;
}
if (max_len < 17) { // 16个字符 + 结束符
return -1;
}
memcpy(id_buffer, id_1, sizeof(uint32_t));
memcpy(id_buffer + 4, id_2, sizeof(uint32_t));
snprintf(device_id, max_len, "%02x%02x%02x%02x%02x%02x%02x%02x",
id_buffer[0], id_buffer[1], id_buffer[2], id_buffer[3],
id_buffer[4], id_buffer[5], id_buffer[6], id_buffer[7]);
return 0;
}
}
uint16_t netcfg_bles_profile_set_cb(uint8_t conidx, uint8_t att_idx, uint16_t op, uint8_t *p_value)
{
uint16_t status = NETCFG_BLE_ERR;
LISA_LOGI(TAG, "netcfg_bles_profile_set_cb op: 0x%04X", op);
switch (op) {
case NETCFG_BLE_OP_SSID: {
} break;
case NETCFG_BLE_OP_PWD: {
} break;
case NETCFG_BLE_OP_DONE: {
ble_notify_param_t *notify_param = lisa_mem_alloc(sizeof(ble_notify_param_t));
if (notify_param != NULL) {
notify_param->conidx = conidx;
notify_param->op = op;
notify_param->p_value = (struct netcfg_ble_data *)p_value;
lisa_thread_attr_t thread_attr;
thread_attr.name = (uint8_t *)"ble_notify_thread";
thread_attr.stack_size = 4 * 1024;
thread_attr.priority = LISA_OS_PRIORITY_ABOVE_NORMAL;
lisa_thread_create(&thread_attr, ble_notify_thread, notify_param);
status = NETCFG_BLE_SUCCESS;
} else {
LISA_LOGE(TAG, "Failed to allocate memory for ble notify param");
status = NETCFG_BLE_ERR;
}
} break;
case NETCFG_BLE_OP_REBOOT: {
ble_gap_disconnect(conidx, 0x13);
} break;
case NETCFG_BLE_AUTH_INFO:{
// 0xA012
LISA_LOGI(TAG, "netcfg_bles_profile_set_cb receive auth info request");
char product_id[64] = {0};
char device_id[32] = {0};
if (get_current_product_id(product_id, sizeof(product_id)) != 0) {
LISA_LOGW(TAG, "Failed to get product ID, using default");
strcpy(product_id, "00000000-0000-0000-0000-000000000000");
}
if (get_current_device_id(device_id, sizeof(device_id)) != 0) {
LISA_LOGW(TAG, "Failed to get device ID, using default");
strcpy(device_id, "0000000000000000");
}
char auth_info[256] = {0};
snprintf(auth_info, sizeof(auth_info),
"{\"code\":0,\"product_id\":\"%s\",\"device_id\":\"%s\"}",
product_id, device_id);
LISA_LOGI(TAG, "auth info: %s", auth_info);
ble_netcfg_bles_send_notify_custom_data(conidx, strlen(auth_info), (uint8_t *)auth_info);
LISA_LOGI(TAG, "netcfg_bles_profile_set_cb TEST done status: 0x%04X", status);
// assist_controller_trigger_event(CONTROLLER_EVENT_OPT_EXIT_BLE_CONFIG, NULL, 0);
app_ble_adv_stop(0);
status = NETCFG_BLE_SUCCESS;
break;
}
default:
break;
}
return status;
}

View File

@@ -0,0 +1,14 @@
#ifndef __APP_NET_CFG_H__
#define __APP_NET_CFG_H__
#ifdef __cplusplus
extern "C" {
#endif
int get_current_device_id(char *device_id, int max_len);
#ifdef __cplusplus
}
#endif
#endif /* __APP_NET_CFG_H__ */

View File

@@ -0,0 +1,116 @@
#ifndef __BT_CONFIG_H__
#define __BT_CONFIG_H__
//#
//# bt ip config
//#
//# Top level product configuration
//# controller
#define BT_EMB_PRESENT 0
#define BLE_EMB_PRESENT 1
#define BLE_ISO_PRESENT 0
#define BT_DUAL_MODE (BT_EMB_PRESENT && BLE_EMB_PRESENT)
//# run ble only on dual mode set 1, run dual mode set 0
#define SINGLE_RUN_ON_DUAL 1
//# host
#define BLE_HOST_PRESENT 1
#define BT_STACK_PRESENT 0
//# classic profile
#define BT_MUSIC_PRESENT 0
#define BT_CALL_PRESENT 0
//# ble profile
#define BLE_GAF_PRESENT 0
#define SMP_PRESENT 1
#define LEA_PRESENT 0
#define MESH_PRESENT 0
//# transport
#define HCIT_UART_PRESENT 0
#define HCIT_USB_PRESENT 0
//#hci audio access
#define HCIT_AUD_PRESENT 0
#define HCI_PRESENT 1
#define AHI_PRESENT 0
#define BLE_APP_PRESENT 1
#define TWS_PRESENT 0
#define LISTENAI_TWS_SUPPORT TWS_PRESENT
//# classic bt config
//# Maximum number of ACL links
#define MAX_NB_ACTIVE_ACL 4
//# Maximum number of Synchronous connections (0 to 2)
#define MAX_NB_SYNC 2
//# FPGA RF board
#define RF_MAX2830_SUPPORT 0
#define RF_ARCS_B0_SUPPORT 0
//# DEBUG SETUP
#define LS_DEBUG 1
#define LS_DEBUG_MEM 1
#define LS_DEBUG_FLASH 0
#define LS_DEBUG_STACK_PROF 0
#define DBG_LOG_PRESENT 0
//# for fpga two node direct connect test
#define LS_DEBUG_FPGA_DIRECT_MODE 0
//#for bis aes generate gsk in interrupt,for testcase iso_bis_p2p@31
#define LS_BIS_GEN_GSK_INT 1
//# TRACER SETUP
#define TRACER_PRESENT 0
//# TRACE MASK, see dbg_trc_cfg_fields
#define TRACE_CFG_MASK 0xffffffff
//#/// Support HL Message API
#define BLE_HL_MSG_API 1
//#/// Support GATT Client
#define BLE_GATT_CLI 1
//#/// Number of L2CAP COC channel that can be created per connection
#define L2CAP_COC_CHAN_PER_CON_NB (10)
//#/// Total Number of L2CAP channel and GATT bearer that can be allocated in environment heap
#define L2CAP_CHAN_IN_ENV_NB (10)
//#/// Maximal authorized MTU / MPS value - Depends on memory size available
#define GAP_LE_MTU_MAX (2048)
#define GAP_LE_MPS_MAX (2048)
//#/// Maximum attribute value length
#define GATT_MAX_VALUE (2048)
//#/// Maximum number of devices in RAL
#define BLE_RAL_MAX (3)
//#/// Maximum number of simultaneous BLE activities (scan, connection, advertising, initiating)
#define BLE_ACTIVITY_MAX (5)
//#/// Maximum number of simultaneous connections
#define BLE_CONNECTION_MAX (3)
//#/// LE Power Control
#define BLE_PWR_CTRL (1)
//# Maximum number of advertising BLE activities
#define BLE_ACTIVITY_ADV_MAX (1)
//# Maximum number of scan BLE activities
#define BLE_ACTIVITY_SCAN_MAX (1)
//# Maximum number of connection BLE activities
#define BLE_ACTIVITY_CON_MAX (1)
//# Maximum number of initiating BLE activities
#define BLE_ACTIVITY_INIT_MAX (1)
//# ISO configure
//#// Connected Isochronous Stream
#define BLE_CIS 1
//#// Broadcast Isochronous Stream
#define BLE_BIS 1
//#/// Maximum number of ISO channel / streams
#define BLE_ISO_CON 4
//#/// Proprietary ISO over HCI
#define BLE_ISOOHCI 1
//#/// Internal ISO generator for validation purpose
#define BLE_ISOGEN 1
//#notify
#define BT_RTOS_NOTIFY_SUPPORT 1
#endif // _BT_CONFIG_H_

View File

@@ -0,0 +1,16 @@
if(CONFIG_FLEXIBLE_BUTTON)
listenai_library_named(btn)
listenai_library_sources(
lisa_btn.c
FlexibleButton/flexible_button.c
)
listenai_include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/FlexibleButton
)
endif()

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2018-2019 MurphyZhao <d2014zjt@163.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,325 @@
# FlexibleButton
FlexibleButton 是一个基于标准 C 语言的小巧灵活的按键处理库,支持单击、连击、短按、长按、自动消抖,可以自由设置组合按键,可用于中断和低功耗场景。
该按键库解耦了具体的按键硬件结构理论上支持轻触按键与自锁按键并可以无限扩展按键数量。另外FlexibleButton 使用扫描的方式一次性读取所有所有的按键状态,然后通过事件回调机制上报按键事件。核心的按键扫描代码仅有三行,没错,就是经典的 **三行按键扫描算法**。使用 C 语言标准库 API 编写,也使得该按键库可以无缝兼容任意的处理器平台,并且支持任意 OS 和 non-OS裸机编程
## 获取
### Git 方式
```SHELL
git clone https://github.com/murphyzhao/FlexibleButton.git
```
### RT-Thread menuconfig 方式
```
RT-Thread online packages --->
miscellaneous packages --->
[*] FlexibleButton: Small and flexible button driver --->
[*] Enable flexible button demo
version (latest) --->
```
配置完成后,输入 `pkgs --update` 下载软件包。
## 资源统计
ARMCC -O0 优化的情况下FlexibleButton 资源占用如下:
- CODE798 字节
- RO DATA0
- RW DATA13 字节
- ZI DATA0
## 快速体验
FlexibleButton 库中提供了一个测试例程 [`./examples/demo_rtt_iotboard.c`](./examples/demo_rtt_iotboard.c),该例程基于 RT-Thread OS 进行测试,硬件平台选择了 *RT-Thread IoT Board Pandora v2.51* 开发板。当然你可以选择使用其他的 OS或者使用裸机测试只需要移除 OS 相关的特性即可。
如果你使用自己的硬件平台,只需要将 FlexibleButton 库源码和例程加入你既有的工程下即可。
## DEMO 程序说明
该示例程序可以直接在 RT-Thread [`stm32l475-atk-pandora`](https://github.com/RT-Thread/rt-thread/tree/master/bsp/stm32/stm32l475-atk-pandora) BSP 中运行,可以在该 BSP 目录下,使用 menuconfig 获取本软件包。
### 确定用户按键
```C
typedef enum
{
USER_BUTTON_0 = 0, // 对应 IoT Board 开发板的 PIN_KEY0
USER_BUTTON_1, // 对应 IoT Board 开发板的 PIN_KEY1
USER_BUTTON_2, // 对应 IoT Board 开发板的 PIN_KEY2
USER_BUTTON_3, // 对应 IoT Board 开发板的 PIN_WK_UP
USER_BUTTON_MAX
} user_button_t;
static flex_button_t user_button[USER_BUTTON_MAX];
```
上述代码定义了 4 个按键,数据结构存储在 `user_button` 数组中。
### 程序入口
```C
int flex_button_main(void)
{
rt_thread_t tid = RT_NULL;
user_button_init();
/* 创建按键扫描线程 flex_btn线程栈 1024 byte优先级 10 */
tid = rt_thread_create("flex_btn", button_scan, RT_NULL, 1024, 10, 10);
if(tid != RT_NULL)
{
rt_thread_startup(tid);
}
return 0;
}
/* 使用 RT-Thread 的自动初始化 */
INIT_APP_EXPORT(flex_button_main);
```
如上代码所示,首先使用 `user_button_init();` 初始化用户按键硬件,该步骤将用户按键绑定到 FlexibleButton 库。然后,使用 RT-Thread 的 `INIT_APP_EXPORT` 接口导出为上电自动初始化,创建了一个 “flex_btn” 名字的按键扫描线程,线程里扫描检查按键事件。
### 按键初始化代码
`user_button_init();` 初始化代码如下所示:
```C
static void user_button_init(void)
{
int i;
/* 初始化按键数据结构 */
rt_memset(&user_button[0], 0x0, sizeof(user_button));
/* 初始化 IoT Board 按键引脚,使用 rt-thread PIN 设备框架 */
rt_pin_mode(PIN_KEY0, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */
rt_pin_mode(PIN_KEY1, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */
rt_pin_mode(PIN_KEY2, PIN_MODE_INPUT_PULLUP); /* 设置 GPIO 为上拉输入模式 */
rt_pin_mode(PIN_WK_UP, PIN_MODE_INPUT_PULLDOWN); /* 设置 GPIO 为下拉输入模式 */
for (i = 0; i < USER_BUTTON_MAX; i ++)
{
user_button[i].id = i;
user_button[i].usr_button_read = common_btn_read;
user_button[i].cb = common_btn_evt_cb;
user_button[i].pressed_logic_level = 0;
user_button[i].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500);
user_button[i].long_press_start_tick = FLEX_MS_TO_SCAN_CNT(3000);
user_button[i].long_hold_start_tick = FLEX_MS_TO_SCAN_CNT(4500);
if (i == USER_BUTTON_3)
{
user_button[USER_BUTTON_3].pressed_logic_level = 1;
}
flex_button_register(&user_button[i]);
}
}
```
核心的配置如下:
|配置项|说明|
| :---- | :----|
| id | 按键编号 |
| usr_button_read | 设置按键读值回调函数 |
| cb | 设置按键事件回调函数 |
| pressed_logic_level | 设置按键按下时的逻辑电平 |
| short_press_start_tick | 短按起始 tick使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数 |
| long_press_start_tick | 长按起始 tick使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数 |
| long_hold_start_tick | 超长按起始 tick使用 FLEX_MS_TO_SCAN_CNT 宏转化为扫描次数 |
注意short_press_start_tick、long_press_start_tick 和 long_hold_start_tick 必须使用 `FLEX_MS_TO_SCAN_CNT` 将毫秒时间转化为扫描次数。
`user_button[i].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500);` 表示按键按下开始计时1500 ms 后按键依旧是按下状态的话,就断定为短按开始。
### 事件处理代码
```C
static void common_btn_evt_cb(void *arg)
{
flex_button_t *btn = (flex_button_t *)arg;
rt_kprintf("id: [%d - %s] event: [%d - %30s] repeat: %d\n",
btn->id, enum_btn_id_string[btn->id],
btn->event, enum_event_string[btn->event],
btn->click_cnt);
if (flex_button_event_read(&user_button[USER_BUTTON_0]) == flex_button_event_read(&user_button[USER_BUTTON_1]) == FLEX_BTN_PRESS_CLICK)
{
rt_kprintf("[combination]: button 0 and button 1\n");
}
}
```
示例代码中,将所有的按键事件回调均绑定到 `common_btn_evt_cb` 函数,在该函数中打印了按键 ID 和按键事件,以及按键连击次数,并演示了如何使用组合按键。
## FlexibleButton 代码说明
### 按键事件定义
按键事件的定义并没有使用 Windows 驱动上的定义,主要是方便嵌入式设备中的应用场景(也可能是我理解的偏差),按键事件定义如下:
```C
typedef enum
{
FLEX_BTN_PRESS_DOWN = 0, // 按下事件
FLEX_BTN_PRESS_CLICK, // 单击事件
FLEX_BTN_PRESS_DOUBLE_CLICK, // 双击事件
FLEX_BTN_PRESS_REPEAT_CLICK, // 连击事件,使用 flex_button_t 中的 click_cnt 断定连击次数
FLEX_BTN_PRESS_SHORT_START, // 短按开始事件
FLEX_BTN_PRESS_SHORT_UP, // 短按抬起事件
FLEX_BTN_PRESS_LONG_START, // 长按开始事件
FLEX_BTN_PRESS_LONG_UP, // 长按抬起事件
FLEX_BTN_PRESS_LONG_HOLD, // 长按保持事件
FLEX_BTN_PRESS_LONG_HOLD_UP, // 长按保持的抬起事件
FLEX_BTN_PRESS_MAX,
FLEX_BTN_PRESS_NONE,
} flex_button_event_t;
```
其中 `FLEX_BTN_PRESS_LONG_HOLD` 事件可以用来实现长按累加的应用场景。
### 按键数据结构
```C
typedef struct flex_button
{
struct flex_button* next;
uint8_t (*usr_button_read)(void *);
flex_button_response_callback cb;
uint16_t scan_cnt;
uint16_t click_cnt;
uint16_t max_multiple_clicks_interval;
uint16_t debounce_tick;
uint16_t short_press_start_tick;
uint16_t long_press_start_tick;
uint16_t long_hold_start_tick;
uint8_t id;
uint8_t pressed_logic_level : 1;
uint8_t event : 4;
uint8_t status : 3;
} flex_button_t;
```
| 序号 | 数据成员 | 是否需要用户初始化 | 说明 |
| :----: | :---- | :----: | :---- |
| 1 | next | 否 | 按键库使用单向链表串起所有的按键 |
| 2 | usr_button_read | 是 | 用户设备的按键引脚电平读取函数,**重要** |
| 3 | cb | 是 | 设置按键事件回调,用于应用层对按键事件的分类处理 |
| 4 | scan_cnt | 否 | 用于记录扫描次数,按键按下是开始从零计数 |
| 5 | click_cnt | 否 | 记录单击次数,用于判定单击、连击 |
| 6 | max_multiple_clicks_interval | 是 | 连击间隙,用于判定是否结束连击计数,有默认值 `MAX_MULTIPLE_CLICKS_INTERVAL` |
| 7 | debounce_tick | 否 | 消抖时间,暂未使用,依靠扫描间隙进行消抖 |
| 8 | short_press_start_tick | 是 | 设置短按事件触发的起始 tick |
| 9 | long_press_start_tick | 是 | 设置长按事件触发的起始 tick |
| 10 | long_hold_start_tick | 是 | 设置长按保持事件触发的起始 tick |
| 11 | id | 是 | 当多个按键使用同一个回调函数时,用于断定属于哪个按键 |
| 12 | pressed_logic_level | 是 | 设置按键按下的逻辑电平。1标识按键按下的时候为高电平0标识按键按下的时候未低电平**重要** |
| 13 | event | 否 | 用于记录当前按键事件 |
| 14 | status | 否 | 用于记录当前按键的状态,用于内部状态机 |
注意,在使用 `max_multiple_clicks_interval``debounce_tick``short_press_start_tick``long_press_start_tick``long_hold_start_tick` 的时候,注意需要使用宏 `**FLEX_MS_TO_SCAN_CNT(ms)**` 将毫秒值转换为扫描次数。因为按键库基于扫描次数运转。示例如下:
```
user_button[1].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(1500); // 1500 毫秒
```
上述代码表示表示按键按下后开始计时1500ms 的时候,按键依旧按下,则断定为短按开始,并上报 `FLEX_BTN_PRESS_SHORT_START` 事件。
### 按键注册接口
使用该接口注册一个用户按键,入参为一个 flex_button_t 结构体实例的地址。
```C
int8_t flex_button_register(flex_button_t *button);
```
### 按键事件读取接口
使用该接口获取指定按键的事件。
```C
flex_button_event_t flex_button_event_read(flex_button_t* button);
````
### 按键扫描接口
20
```C
void flex_button_scan(void);
```
## 注意事项
- 阻塞问题
因为按键事件回调函数以及按键键值读取函数是在按键扫描的过程中执行的,因此请不要在这类函数中使用阻塞接口,不要进行延时操作。
- 按键扫描函数栈需求
按键扫描函数本身对栈的需求小于 300 字节,但是按键事件回调函数和按键键值读取函数都是在按键扫描函数的上下文中执行的,请格外关心按键事件回调函数与按键键值读取函数对栈空间的需求。
## 其它
### 关于低功耗
本按键库是通过不间断扫描的方式来检查按键状态,因此会一直占用 CPU 资源这对低功耗应用场景是不友好的。为了降低正常工作模式下的功耗建议合理配置扫描周期5ms - 20ms扫描间隙里 CPU 可以进入轻度睡眠。
该按键库不在底层实现低功耗处理,应用层可以根据自己的功耗模式灵活处理,通常会有以下两种方式:
1. 进入低功耗前,挂起按键扫描线程;退出低功耗后,唤醒按键扫描。
2. 增加按键中断模式,所有的按键中断来,就触发一次按键扫描,以确认所有的按键状态。
> 低功耗相关的探讨参考 [issue 1](https://github.com/murphyzhao/FlexibleButton/issues/1) 中的讨论。
### 关于按键中断模式
由于该按键库一次扫描可以确定所有的按键状态,因此可以将所有的按键中断通过 “**或**” 的方式转化为一个中断,然后在中断处理函数中执行一次按键扫描。
中断 “**或**” 的方式可以通过硬件来完成,也可以通过软件来完成。
硬件方式,需要使用一个 **或门** 芯片,多个输入条件转化为一个输出条件,然后通过一个外部中断即可完成所有按键的中断方式检测。
软件方式,需要为每一个按键配置为中断触发模式,然后在每一个按键中断的中断处理函数中执行按键扫描。
为了在降低中断处理函数中执行按键扫描带来的时延,可以通过信号量的方式来异步处理,仅在中断处理函数中释放一个按键扫描的信号量,然后在按键扫描线程中监测该信号量。
### 关于组合按键
该按键库仅做了底层的按键扫描处理,一次扫描可以确定所有的按键状态,并上报对应的按键事件,如果需要支持组合按键,请再封一层,根据按键库返回的事件封装需要的组合按键。[示例程序](./examples/demo_rtt_iotboard.c)提供了简单的实现。
### 关于矩阵键盘
不管你的矩阵键盘是通过什么通信方式获取按键状态的,只要你将读取按键状态的函数对接到 Flexible_button 数据结构中的 `uint8_t (*usr_button_read)(void*);` 函数上即可。
> 参考 [issue 2](https://github.com/murphyzhao/FlexibleButton/issues/2) 中的讨论。
## 问题和建议
如果有什么问题或者建议欢迎提交 [Issue](https://github.com/murphyzhao/FlexibleButton/issues) 进行讨论。
## 维护
- [MurphyZhao](https://github.com/murphyzhao)
## 感谢
感谢所有一起探讨的朋友,感谢所有使用 flexible_button 的朋友,感谢你们的 Star 和 Fork谢谢你们的支持。
- 感谢 [BOBBOM](https://github.com/BOBBOM) 发现 flex_button_register 函数中的逻辑问题
- 感谢 [BOBBOM](https://github.com/BOBBOM) 解除 flexible_button 中对按键数量的限制
- 感谢 [**rt-thread**](https://mp.weixin.qq.com/s/HJEcSXhykBq1T5Hx0TdjMw) 的支持
- 感谢 [**电子发烧友**](https://mp.weixin.qq.com/s/mQFyrPAvz_TSktQLrSqQfA) 的支持
- 感谢 [**威驰电子**](https://mp.weixin.qq.com/s/oAwFXPostMFBtb2EGxTdig) 的支持
## 友情链接
- RT-Thread [IoT Board](https://github.com/RT-Thread/IoT_Board) 开发板

View File

@@ -0,0 +1,326 @@
/**
* @File: flexible_button.c
* @Author: MurphyZhao
* @Date: 2018-09-29
*
* Copyright (c) 2018-2019 MurphyZhao <d2014zjt@163.com>
* https://github.com/murphyzhao
* All rights reserved.
* License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Change logs:
* Date Author Notes
* 2018-09-29 MurphyZhao First add
* 2019-08-02 MurphyZhao Migrate code to github.com/murphyzhao account
* 2019-12-26 MurphyZhao Refactor code and implement multiple clicks
*
*/
#include "flexible_button.h"
#ifndef NULL
#define NULL 0
#endif
#define EVENT_SET_AND_EXEC_CB(btn, evt) \
do \
{ \
btn->event = evt; \
if(btn->cb) \
btn->cb((flex_button_t*)btn); \
} while(0)
/**
* BTN_IS_PRESSED
*
* 1: is pressed
* 0: is not pressed
*/
#define BTN_IS_PRESSED(i) (g_btn_status_reg & (1 << i))
enum FLEX_BTN_STAGE
{
FLEX_BTN_STAGE_DEFAULT = 0,
FLEX_BTN_STAGE_DOWN = 1,
FLEX_BTN_STAGE_MULTIPLE_CLICK = 2
};
typedef uint32_t btn_type_t;
static flex_button_t *btn_head = NULL;
/**
* g_logic_level
*
* The logic level of the button pressed,
* Each bit represents a button.
*
* First registered button, the logic level of the button pressed is
* at the low bit of g_logic_level.
*/
btn_type_t g_logic_level = (btn_type_t)0;
/**
* g_btn_status_reg
*
* The status register of all button, each bit records the pressing state of a button.
*
* First registered button, the pressing state of the button is
* at the low bit of g_btn_status_reg.
*/
btn_type_t g_btn_status_reg = (btn_type_t)0;
static uint8_t button_cnt = 0;
/**
* @brief Register a user button
*
* @param button: button structure instance
* @return Number of keys that have been registered, or -1 when error
*/
int32_t flex_button_register(flex_button_t *button)
{
flex_button_t *curr = btn_head;
if (!button || (button_cnt > sizeof(btn_type_t) * 8))
{
return -1;
}
while (curr)
{
if(curr == button)
{
return -1; /* already exist. */
}
curr = curr->next;
}
/**
* First registered button is at the end of the 'linked list'.
* btn_head points to the head of the 'linked list'.
*/
button->next = btn_head;
button->status = FLEX_BTN_STAGE_DEFAULT;
button->event = FLEX_BTN_PRESS_NONE;
button->scan_cnt = 0;
button->click_cnt = 0;
button->max_multiple_clicks_interval = MAX_MULTIPLE_CLICKS_INTERVAL;
btn_head = button;
/**
* First registered button, the logic level of the button pressed is
* at the low bit of g_logic_level.
*/
g_logic_level |= (button->pressed_logic_level << button_cnt);
button_cnt ++;
return button_cnt;
}
/**
* @brief Read all key values in one scan cycle
*
* @param void
* @return none
*/
static void flex_button_read(void)
{
uint8_t i;
flex_button_t* target;
/* The button that was registered first, the button value is in the low position of raw_data */
btn_type_t raw_data = 0;
for(target = btn_head, i = button_cnt - 1;
(target != NULL) && (target->usr_button_read != NULL);
target = target->next, i--)
{
raw_data = raw_data | ((target->usr_button_read)(target) << i);
}
g_btn_status_reg = (~raw_data) ^ g_logic_level;
}
/**
* @brief Handle all key events in one scan cycle.
* Must be used after 'flex_button_read' API
*
* @param void
* @return Activated button count
*/
static uint8_t flex_button_process(void)
{
uint8_t i;
uint8_t active_btn_cnt = 0;
flex_button_t* target;
for (target = btn_head, i = button_cnt - 1; target != NULL; target = target->next, i--)
{
if (target->status > FLEX_BTN_STAGE_DEFAULT)
{
target->scan_cnt ++;
if (target->scan_cnt >= ((1 << (sizeof(target->scan_cnt) * 8)) - 1))
{
target->scan_cnt = target->long_hold_start_tick;
}
}
switch (target->status)
{
case FLEX_BTN_STAGE_DEFAULT: /* stage: default(button up) */
if (BTN_IS_PRESSED(i)) /* is pressed */
{
target->scan_cnt = 0;
target->click_cnt = 0;
EVENT_SET_AND_EXEC_CB(target, FLEX_BTN_PRESS_DOWN);
/* swtich to button down stage */
target->status = FLEX_BTN_STAGE_DOWN;
}
else
{
target->event = FLEX_BTN_PRESS_NONE;
}
break;
case FLEX_BTN_STAGE_DOWN: /* stage: button down */
if (BTN_IS_PRESSED(i)) /* is pressed */
{
if (target->click_cnt > 0) /* multiple click */
{
if (target->scan_cnt > target->max_multiple_clicks_interval)
{
EVENT_SET_AND_EXEC_CB(target,
target->click_cnt <= FLEX_BTN_PRESS_SEPTUPLE_CLICK ?
target->click_cnt :
FLEX_BTN_PRESS_REPEAT_CLICK);
/* swtich to button down stage */
target->status = FLEX_BTN_STAGE_DOWN;
target->scan_cnt = 0;
target->click_cnt = 0;
}
}
else if (target->scan_cnt >= target->long_hold_start_tick)
{
if (target->event != FLEX_BTN_PRESS_LONG_HOLD)
{
EVENT_SET_AND_EXEC_CB(target, FLEX_BTN_PRESS_LONG_HOLD);
}
}
else if (target->scan_cnt >= target->long_press_start_tick)
{
if (target->event != FLEX_BTN_PRESS_LONG_START)
{
EVENT_SET_AND_EXEC_CB(target, FLEX_BTN_PRESS_LONG_START);
}
}
else if (target->scan_cnt >= target->short_press_start_tick)
{
if (target->event != FLEX_BTN_PRESS_SHORT_START)
{
EVENT_SET_AND_EXEC_CB(target, FLEX_BTN_PRESS_SHORT_START);
}
}
}
else /* button up */
{
if (target->scan_cnt >= target->long_hold_start_tick)
{
EVENT_SET_AND_EXEC_CB(target, FLEX_BTN_PRESS_LONG_HOLD_UP);
target->status = FLEX_BTN_STAGE_DEFAULT;
}
else if (target->scan_cnt >= target->long_press_start_tick)
{
EVENT_SET_AND_EXEC_CB(target, FLEX_BTN_PRESS_LONG_UP);
target->status = FLEX_BTN_STAGE_DEFAULT;
}
else if (target->scan_cnt >= target->short_press_start_tick)
{
EVENT_SET_AND_EXEC_CB(target, FLEX_BTN_PRESS_SHORT_UP);
target->status = FLEX_BTN_STAGE_DEFAULT;
}
else
{
EVENT_SET_AND_EXEC_CB(target, FLEX_BTN_PRESS_SHORT_UP);
/* swtich to multiple click stage */
target->status = FLEX_BTN_STAGE_MULTIPLE_CLICK;
target->click_cnt ++;
}
}
break;
case FLEX_BTN_STAGE_MULTIPLE_CLICK: /* stage: multiple click */
if (BTN_IS_PRESSED(i)) /* is pressed */
{
/* swtich to button down stage */
target->status = FLEX_BTN_STAGE_DOWN;
target->scan_cnt = 0;
}
else
{
if (target->scan_cnt > target->max_multiple_clicks_interval)
{
EVENT_SET_AND_EXEC_CB(target,
target->click_cnt <= FLEX_BTN_PRESS_SEPTUPLE_CLICK ?
target->click_cnt :
FLEX_BTN_PRESS_REPEAT_CLICK);
/* swtich to default stage */
target->status = FLEX_BTN_STAGE_DEFAULT;
}
}
break;
}
if (target->status > FLEX_BTN_STAGE_DEFAULT)
{
active_btn_cnt ++;
}
}
return active_btn_cnt;
}
/**
* flex_button_event_read
*
* @brief Get the button event of the specified button.
*
* @param button: button structure instance
* @return button event
*/
flex_button_event_t flex_button_event_read(flex_button_t* button)
{
return (flex_button_event_t)(button->event);
}
/**
* flex_button_scan
*
* @brief Start key scan.
* Need to be called cyclically within the specified period.
* Sample cycle: 5 - 20ms
*
* @param void
* @return Activated button count
*/
uint8_t flex_button_scan(void)
{
flex_button_read();
return flex_button_process();
}

View File

@@ -0,0 +1,160 @@
/**
* @File: flexible_button.h
* @Author: MurphyZhao
* @Date: 2018-09-29
*
* Copyright (c) 2018-2019 MurphyZhao <d2014zjt@163.com>
* https://github.com/murphyzhao
* All rights reserved.
* License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Change logs:
* Date Author Notes
* 2018-09-29 MurphyZhao First add
* 2019-08-02 MurphyZhao Migrate code to github.com/murphyzhao account
* 2019-12-26 MurphyZhao Refactor code and implement multiple clicks
*
*/
#ifndef __FLEXIBLE_BUTTON_H__
#define __FLEXIBLE_BUTTON_H__
#include "stdint.h"
#define FLEX_BTN_SCAN_FREQ_HZ 50 // How often flex_button_scan () is called
#define FLEX_MS_TO_SCAN_CNT(ms) (ms / (1000 / FLEX_BTN_SCAN_FREQ_HZ))
/* Multiple clicks interval, default 300ms */
#define MAX_MULTIPLE_CLICKS_INTERVAL (FLEX_MS_TO_SCAN_CNT(300))
typedef void (*flex_button_response_callback)(void*);
typedef enum
{
FLEX_BTN_PRESS_DOWN = 0,
FLEX_BTN_PRESS_CLICK,
FLEX_BTN_PRESS_DOUBLE_CLICK,
FLEX_BTN_PRESS_TRIPLE_CLICK,
FLEX_BTN_PRESS_QUADRUPLE_CLICK,
FLEX_BTN_PRESS_QUINTUPLE_CLICK,
FLEX_BTN_PRESS_SEXTUPLE_CLICK,
FLEX_BTN_PRESS_SEPTUPLE_CLICK,
FLEX_BTN_PRESS_REPEAT_CLICK,
FLEX_BTN_PRESS_SHORT_START,
FLEX_BTN_PRESS_SHORT_UP,
FLEX_BTN_PRESS_LONG_START,
FLEX_BTN_PRESS_LONG_UP,
FLEX_BTN_PRESS_LONG_HOLD,
FLEX_BTN_PRESS_LONG_HOLD_UP,
FLEX_BTN_PRESS_MAX,
FLEX_BTN_PRESS_NONE,
} flex_button_event_t;
/**
* flex_button_t
*
* @brief Button data structure
* Below are members that need to user init before scan.
*
* @member next
* Internal use.
* One-way linked list, pointing to the next button.
*
* @member usr_button_read
* User function is used to read button vaule.
*
* @member cb
* Button event callback function.
*
* @member scan_cnt
* Internal use, user read-only.
* Number of scans, counted when the button is pressed, plus one per scan cycle.
*
* @member click_cnt
* Internal use, user read-only.
* Number of button clicks
*
* @member max_multiple_clicks_interval
* Multiple click interval. Default 'MAX_MULTIPLE_CLICKS_INTERVAL'.
* Need to use FLEX_MS_TO_SCAN_CNT to convert milliseconds into scan cnts.
*
* @member debounce_tick
* Debounce. Not used yet.
* Need to use FLEX_MS_TO_SCAN_CNT to convert milliseconds into scan cnts.
*
* @member short_press_start_tick
* Short press start time. Requires user configuration.
* Need to use FLEX_MS_TO_SCAN_CNT to convert milliseconds into scan cnts.
*
* @member long_press_start_tick
* Long press start time. Requires user configuration.
* Need to use FLEX_MS_TO_SCAN_CNT to convert milliseconds into scan cnts.
*
* @member long_hold_start_tick
* Long hold press start time. Requires user configuration.
*
* @member id
* Button id. Requires user configuration.
* When multiple buttons use the same button callback function,
* they are used to distinguish the buttons.
* Each button id must be unique.
*
* @member pressed_logic_level
* Requires user configuration.
* The logic level of the button pressed, each bit represents a button.
*
* @member event
* Internal use, users can call 'flex_button_event_read' to get current button event.
* Used to record the current button event.
*
* @member status
* Internal use, user unavailable.
* Used to record the current state of buttons.
*
*/
typedef struct flex_button
{
struct flex_button* next;
uint8_t (*usr_button_read)(void *);
flex_button_response_callback cb;
uint16_t scan_cnt;
uint16_t click_cnt;
uint16_t max_multiple_clicks_interval;
uint16_t debounce_tick;
uint16_t short_press_start_tick;
uint16_t long_press_start_tick;
uint16_t long_hold_start_tick;
uint8_t id;
uint8_t pressed_logic_level : 1;
uint8_t event : 5;
uint8_t status : 2;
} flex_button_t;
#ifdef __cplusplus
extern "C" {
#endif
int32_t flex_button_register(flex_button_t *button);
flex_button_event_t flex_button_event_read(flex_button_t* button);
uint8_t flex_button_scan(void);
#ifdef __cplusplus
}
#endif
#endif /* __FLEXIBLE_BUTTON_H__ */

View File

@@ -0,0 +1,6 @@
config FLEXIBLE_BUTTON
bool "Enable flexible button support"
default n
help
Enable flexible button library for ADC button support.
This provides multi-button detection using ADC voltage levels.

View File

@@ -0,0 +1,444 @@
/**
* @file lisa_btn.c
* @brief Button driver - supports both ADC and GPIO buttons
*/
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include "lisa_log.h"
#include "lisa_thread.h"
#include "flexible_button.h"
#include "lisa_btn.h"
#include "lisa_adc.h"
#include "lisa_device.h"
#ifdef CONFIG_BOARD_ARCS_MINI
#include "lisa_gpio.h"
#else
#include "Driver_GPIO.h"
#endif
#define TAG "btn"
#define BTN_DEBUG_ENABLE (0)
#define BTN_MAX_ADC_COUNT (8)
#define BTN_MAX_GPIO_COUNT (8)
/* ADC按键上下文 */
struct adc_btn_ctx {
bool is_init;
lisa_btn_adc_mode_t mode;
uint8_t button_count;
flex_button_t btns[BTN_MAX_ADC_COUNT];
lisa_device_t *adc_dev;
uint16_t ref_voltage;
uint8_t resolution;
struct {
uint8_t adc_channel;
const lisa_btn_adc_range_t *ranges;
} one_to_many;
struct {
uint8_t *channels;
lisa_btn_adc_range_t *ranges;
} one_to_one;
lisa_btn_cb_t callback;
void *user_data;
lisa_btn_time_config_t time_config;
};
/* GPIO按键上下文 */
struct gpio_btn_ctx {
bool is_init;
uint8_t button_count;
flex_button_t btns[BTN_MAX_GPIO_COUNT];
#ifdef CONFIG_BOARD_ARCS_MINI
lisa_device_t *gpio_dev;
#else
void *gpio_res;
#endif
const lisa_btn_gpio_item_t *buttons;
lisa_btn_cb_t callback;
void *user_data;
lisa_btn_time_config_t time_config;
};
static struct adc_btn_ctx g_adc_ctx = {0};
static struct gpio_btn_ctx g_gpio_ctx = {0};
static bool g_scan_task_created = false;
static uint16_t g_scan_period = 5;
/* ===== ADC按键读取 ===== */
static uint16_t adc_read_voltage(uint32_t channel)
{
uint16_t adc_value = 0;
int ret;
if (!g_adc_ctx.adc_dev) {
return 0xFFFF;
}
ret = lisa_adc_read(g_adc_ctx.adc_dev, channel, &adc_value);
if (ret != 0) {
return 0xFFFF;
}
return LISA_ADC_RAW_TO_MV(adc_value, g_adc_ctx.ref_voltage, g_adc_ctx.resolution);
}
static uint8_t adc_btn_read_one_to_many(uint8_t btn_id)
{
uint16_t voltage = adc_read_voltage(g_adc_ctx.one_to_many.adc_channel);
if (voltage == 0xFFFF) {
return 0;
}
if (btn_id < g_adc_ctx.button_count) {
const lisa_btn_adc_range_t *range = &g_adc_ctx.one_to_many.ranges[btn_id];
if (voltage >= range->voltage_min && voltage <= range->voltage_max) {
#if BTN_DEBUG_ENABLE
LISA_LOGI(TAG, "ADC Btn%d pressed: %dmV", btn_id, voltage);
#endif
return 1;
}
}
return 0;
}
static uint8_t adc_btn_read_one_to_one(uint8_t btn_id)
{
if (btn_id >= g_adc_ctx.button_count || !g_adc_ctx.one_to_one.channels) {
return 0;
}
uint8_t channel = g_adc_ctx.one_to_one.channels[btn_id];
uint16_t voltage = adc_read_voltage(channel);
if (voltage == 0xFFFF) {
return 0;
}
const lisa_btn_adc_range_t *range = &g_adc_ctx.one_to_one.ranges[btn_id];
if (voltage >= range->voltage_min && voltage <= range->voltage_max) {
return 1;
}
return 0;
}
static uint8_t adc_flex_btn_read_cb(void *arg)
{
flex_button_t *btn = (flex_button_t *)arg;
uint8_t btn_id = btn->id;
uint8_t pressed = 0;
if (g_adc_ctx.mode == LISA_BTN_ADC_MODE_ONE_TO_MANY) {
pressed = adc_btn_read_one_to_many(btn_id);
} else {
pressed = adc_btn_read_one_to_one(btn_id);
}
return pressed ? 0 : 1;
}
static void adc_flex_btn_event_cb(void *arg)
{
flex_button_t *btn = (flex_button_t *)arg;
LISA_LOGI(TAG, "[ADC Btn%d] event=%d", btn->id, btn->event);
if (g_adc_ctx.callback) {
g_adc_ctx.callback((lisa_btn_event_t)btn->event, btn->id, g_adc_ctx.user_data);
}
}
/* ===== GPIO按键读取 ===== */
static uint8_t gpio_flex_btn_read_cb(void *arg)
{
flex_button_t *btn = (flex_button_t *)arg;
uint8_t btn_id = btn->id;
if (btn_id >= g_gpio_ctx.button_count || !g_gpio_ctx.buttons ||
#ifdef CONFIG_BOARD_ARCS_MINI
!g_gpio_ctx.gpio_dev
#else
!g_gpio_ctx.gpio_res
#endif
) {
return 1; /* 释放 */
}
const lisa_btn_gpio_item_t *item = &g_gpio_ctx.buttons[btn_id];
#ifdef CONFIG_BOARD_ARCS_MINI
int32_t value = lisa_gpio_read_pin(g_gpio_ctx.gpio_dev, item->pin_num);
if (value < 0) {
return 1; /* 读取失败,返回释放 */
}
uint8_t pin_level = (value == LISA_GPIO_HIGH) ? 1 : 0;
#else
uint32_t pin_mask = (1UL << item->pin_num);
int32_t value = GPIO_PinRead(g_gpio_ctx.gpio_res, pin_mask);
if (value < 0) {
return 1; /* 读取失败,返回释放 */
}
/* 检查是否按下 */
uint8_t pin_level = (value & pin_mask) ? 1 : 0;
#endif
uint8_t pressed = (pin_level == item->active_level) ? 1 : 0;
#if BTN_DEBUG_ENABLE
if (pressed) {
LISA_LOGI(TAG, "GPIO Btn%d pressed: pin=%d, value=%d", btn_id, item->pin_num, pin_level);
}
#endif
return pressed ? 0 : 1;
}
static void gpio_flex_btn_event_cb(void *arg)
{
flex_button_t *btn = (flex_button_t *)arg;
LISA_LOGI(TAG, "[GPIO Btn%d] event=%d", btn->id, btn->event);
if (g_gpio_ctx.callback) {
g_gpio_ctx.callback((lisa_btn_event_t)btn->event, btn->id, g_gpio_ctx.user_data);
}
}
/* ===== 扫描任务 ===== */
static void btn_scan_task(void *arg)
{
LISA_LOGI(TAG, "Button scan task started");
lisa_thread_mdelay(2000);
while (1) {
flex_button_scan();
lisa_thread_mdelay(g_scan_period);
}
}
static int create_scan_task_if_needed(uint16_t scan_period)
{
if (g_scan_task_created) {
return 0;
}
g_scan_period = scan_period;
lisa_thread_attr_t attr = {
.name = "btn",
.stack_size = 2048,
.priority = 6,
};
lisa_thread_t *td = lisa_thread_create(&attr, btn_scan_task, NULL);
if (!td) {
LISA_LOGE(TAG, "Failed to create scan task");
return -1;
}
g_scan_task_created = true;
return 0;
}
/* ===== 公共接口 ===== */
int lisa_btn_adc_init(const lisa_btn_adc_config_t *config)
{
int ret;
uint8_t i;
if (!config || !config->callback) {
LISA_LOGE(TAG, "Invalid ADC config");
return -1;
}
if (g_adc_ctx.is_init) {
LISA_LOGW(TAG, "ADC buttons already initialized");
return 0;
}
memset(&g_adc_ctx, 0, sizeof(g_adc_ctx));
g_adc_ctx.mode = config->mode;
g_adc_ctx.ref_voltage = config->ref_voltage;
g_adc_ctx.resolution = config->resolution;
g_adc_ctx.callback = config->callback;
g_adc_ctx.user_data = config->user_data;
g_adc_ctx.time_config = config->time_config;
g_adc_ctx.adc_dev = lisa_device_get(config->adc_dev_name);
if (!g_adc_ctx.adc_dev) {
LISA_LOGE(TAG, "Failed to get ADC device: %s", config->adc_dev_name);
return -2;
}
LISA_LOGI(TAG, "ADC device: %s", config->adc_dev_name);
if (config->mode == LISA_BTN_ADC_MODE_ONE_TO_MANY) {
g_adc_ctx.button_count = config->one_to_many.button_count;
g_adc_ctx.one_to_many.adc_channel = config->one_to_many.adc_channel;
g_adc_ctx.one_to_many.ranges = config->one_to_many.ranges;
lisa_adc_channel_config_t adc_cfg = {
.reference = LISA_ADC_REF_VDD_3V6,
.resolution = LISA_ADC_RESOLUTION_10BIT,
};
ret = lisa_adc_channel_setup(g_adc_ctx.adc_dev, config->one_to_many.adc_channel, &adc_cfg);
if (ret != 0) {
LISA_LOGE(TAG, "ADC channel %d setup failed: %d", config->one_to_many.adc_channel, ret);
return -3;
}
LISA_LOGI(TAG, "ADC channel %d configured", config->one_to_many.adc_channel);
} else {
g_adc_ctx.button_count = config->one_to_one.button_count;
LISA_LOGW(TAG, "One-to-one mode not fully implemented");
}
if (g_adc_ctx.button_count > BTN_MAX_ADC_COUNT) {
g_adc_ctx.button_count = BTN_MAX_ADC_COUNT;
}
for (i = 0; i < g_adc_ctx.button_count; i++) {
g_adc_ctx.btns[i].id = i;
g_adc_ctx.btns[i].usr_button_read = adc_flex_btn_read_cb;
g_adc_ctx.btns[i].cb = adc_flex_btn_event_cb;
g_adc_ctx.btns[i].pressed_logic_level = 0;
g_adc_ctx.btns[i].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(config->time_config.short_press_time);
g_adc_ctx.btns[i].long_press_start_tick = FLEX_MS_TO_SCAN_CNT(config->time_config.long_press_time);
g_adc_ctx.btns[i].long_hold_start_tick = FLEX_MS_TO_SCAN_CNT(config->time_config.long_hold_time);
flex_button_register(&g_adc_ctx.btns[i]);
LISA_LOGI(TAG, "ADC Button %d registered", i);
}
ret = create_scan_task_if_needed(config->time_config.scan_period);
if (ret != 0) {
return -4;
}
g_adc_ctx.is_init = true;
LISA_LOGI(TAG, "ADC button init done: %d buttons, mode=%d", g_adc_ctx.button_count, g_adc_ctx.mode);
return 0;
}
int lisa_btn_gpio_init(const lisa_btn_gpio_config_t *config)
{
int ret;
uint8_t i;
if (!config || !config->callback || !config->buttons) {
LISA_LOGE(TAG, "Invalid GPIO config");
return -1;
}
if (g_gpio_ctx.is_init) {
LISA_LOGW(TAG, "GPIO buttons already initialized");
return 0;
}
memset(&g_gpio_ctx, 0, sizeof(g_gpio_ctx));
g_gpio_ctx.button_count = config->button_count;
g_gpio_ctx.buttons = config->buttons;
g_gpio_ctx.callback = config->callback;
g_gpio_ctx.user_data = config->user_data;
g_gpio_ctx.time_config = config->time_config;
/* 获取GPIO资源 (GPIOA或GPIOB) */
#ifdef CONFIG_BOARD_ARCS_MINI
g_gpio_ctx.gpio_dev = lisa_device_get(config->gpio_dev_name);
if (!g_gpio_ctx.gpio_dev) {
LISA_LOGE(TAG, "Failed to get GPIO device: %s", config->gpio_dev_name);
return -2;
}
#else
g_gpio_ctx.gpio_res = GPIOA();
if (!g_gpio_ctx.gpio_res) {
LISA_LOGE(TAG, "Failed to get GPIO resource");
return -2;
}
/* 初始化GPIO */
ret = GPIO_Initialize(g_gpio_ctx.gpio_res, NULL, NULL);
if (ret != 0) {
LISA_LOGE(TAG, "GPIO initialize failed: %d", ret);
return -3;
}
ret = GPIO_PowerControl(g_gpio_ctx.gpio_res, CSK_POWER_FULL);
if (ret != 0) {
LISA_LOGE(TAG, "GPIO power control failed: %d", ret);
return -3;
}
#endif
LISA_LOGI(TAG, "GPIO initialized");
if (g_gpio_ctx.button_count > BTN_MAX_GPIO_COUNT) {
g_gpio_ctx.button_count = BTN_MAX_GPIO_COUNT;
}
/* 配置GPIO引脚 */
for (i = 0; i < g_gpio_ctx.button_count; i++) {
const lisa_btn_gpio_item_t *item = &config->buttons[i];
#ifdef CONFIG_BOARD_ARCS_MINI
lisa_gpio_configure(g_gpio_ctx.gpio_dev, item->pin_num,
LISA_GPIO_INPUT |
(item->pull_enable ? (item->pull_up ? LISA_GPIO_PULL_UP : LISA_GPIO_PULL_DOWN) : 0));
#else
uint32_t pin_mask = (1UL << item->pin_num);
/* 设置为输入 */
ret = GPIO_SetDir(g_gpio_ctx.gpio_res, pin_mask, CSK_GPIO_DIR_INPUT);
if (ret != 0) {
LISA_LOGE(TAG, "GPIO pin %d set dir failed: %d", item->pin_num, ret);
return -4;
}
/* 设置上下拉 */
uint32_t pull_mode = CSK_GPIO_MODE_PULL_NONE;
if (item->pull_enable) {
pull_mode = item->pull_up ? CSK_GPIO_MODE_PULL_UP : CSK_GPIO_MODE_PULL_DOWN;
}
ret = GPIO_Control(g_gpio_ctx.gpio_res, pull_mode, pin_mask);
if (ret != 0) {
LISA_LOGW(TAG, "GPIO pin %d set pull mode failed: %d", item->pin_num, ret);
}
#endif
g_gpio_ctx.btns[i].id = i;
g_gpio_ctx.btns[i].usr_button_read = gpio_flex_btn_read_cb;
g_gpio_ctx.btns[i].cb = gpio_flex_btn_event_cb;
g_gpio_ctx.btns[i].pressed_logic_level = 0;
g_gpio_ctx.btns[i].short_press_start_tick = FLEX_MS_TO_SCAN_CNT(config->time_config.short_press_time);
g_gpio_ctx.btns[i].long_press_start_tick = FLEX_MS_TO_SCAN_CNT(config->time_config.long_press_time);
g_gpio_ctx.btns[i].long_hold_start_tick = FLEX_MS_TO_SCAN_CNT(config->time_config.long_hold_time);
flex_button_register(&g_gpio_ctx.btns[i]);
LISA_LOGI(TAG, "GPIO Button %d registered (pin=%d)", i, item->pin_num);
}
ret = create_scan_task_if_needed(config->time_config.scan_period);
if (ret != 0) {
return -5;
}
g_gpio_ctx.is_init = true;
LISA_LOGI(TAG, "GPIO button init done: %d buttons", g_gpio_ctx.button_count);
return 0;
}
void lisa_btn_deinit(void)
{
g_adc_ctx.is_init = false;
g_gpio_ctx.is_init = false;
}

View File

@@ -0,0 +1,151 @@
/**
* @file lisa_btn.h
* @brief 通用按键驱动接口
* @version 2.0
* @date 2025-12-11
*
* @copyright Copyright (C) 2025 ANHUI LISTENAI Co., Ltd. All Rights Reserved.
*/
#ifndef __LISA_BTN_H__
#define __LISA_BTN_H__
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
/* ===== 按键事件类型 ===== */
typedef enum {
LISA_BTN_PRESS_DOWN = 0,
LISA_BTN_PRESS_CLICK,
LISA_BTN_PRESS_DOUBLE_CLICK,
LISA_BTN_PRESS_TRIPLE_CLICK,
LISA_BTN_PRESS_QUADRUPLE_CLICK,
LISA_BTN_PRESS_QUINTUPLE_CLICK,
LISA_BTN_PRESS_SEXTUPLE_CLICK,
LISA_BTN_PRESS_SEPTUPLE_CLICK,
LISA_BTN_PRESS_REPEAT_CLICK,
LISA_BTN_PRESS_SHORT_START,
LISA_BTN_PRESS_SHORT_UP,
LISA_BTN_PRESS_LONG_START,
LISA_BTN_PRESS_LONG_UP,
LISA_BTN_PRESS_LONG_HOLD,
LISA_BTN_PRESS_LONG_HOLD_UP,
LISA_BTN_PRESS_MAX,
LISA_BTN_PRESS_NONE,
} lisa_btn_event_t;
/* ===== 按键类型 ===== */
typedef enum {
LISA_BTN_TYPE_ADC = 0, /* ADC按键 */
LISA_BTN_TYPE_GPIO, /* GPIO按键 */
} lisa_btn_type_t;
/* ===== 按键事件回调 ===== */
typedef void (*lisa_btn_cb_t)(lisa_btn_event_t evt, uint8_t btn_id, void *user);
/* ===== ADC按键模式 ===== */
typedef enum {
LISA_BTN_ADC_MODE_ONE_TO_ONE = 0, /* 一对一一个ADC通道对应一个按键 */
LISA_BTN_ADC_MODE_ONE_TO_MANY, /* 一对多一个ADC通道对应多个按键电阻分压 */
} lisa_btn_adc_mode_t;
/* ===== ADC按键电压范围 ===== */
typedef struct {
uint16_t voltage_min; /* 电压最小值 (mV) */
uint16_t voltage_max; /* 电压最大值 (mV) */
} lisa_btn_adc_range_t;
/* ===== 按键时间配置 ===== */
typedef struct {
uint16_t short_press_time; /* 短按时间 (ms) */
uint16_t long_press_time; /* 长按时间 (ms) */
uint16_t long_hold_time; /* 长按保持时间 (ms) */
uint16_t scan_period; /* 扫描周期 (ms) */
} lisa_btn_time_config_t;
/* ===== ADC按键引脚配置 ===== */
typedef struct {
uint8_t pin_num; /* 引脚号 (如PB6的6) */
uint8_t iomux_func; /* IOMUX复用功能 */
} lisa_btn_adc_pin_t;
/* ===== ADC按键配置 ===== */
typedef struct {
const char *adc_dev_name; /* ADC设备名称 (如"adc0") */
uint16_t ref_voltage; /* 参考电压 (mV) */
uint8_t resolution; /* 分辨率 (bit) */
lisa_btn_adc_mode_t mode; /* ADC按键模式 */
/* 一对多模式配置 */
struct {
uint8_t adc_channel; /* ADC通道号 (0-5) */
lisa_btn_adc_pin_t pin; /* 引脚配置 */
uint8_t button_count; /* 按键数量 */
const lisa_btn_adc_range_t *ranges; /* 按键电压范围数组 */
} one_to_many;
/* 一对一模式配置 */
struct {
uint8_t button_count; /* 按键数量 */
struct {
uint8_t adc_channel; /* ADC通道号 */
lisa_btn_adc_pin_t pin; /* 引脚配置 */
lisa_btn_adc_range_t range; /* 电压范围 */
} *buttons; /* 按键配置数组 */
} one_to_one;
lisa_btn_time_config_t time_config; /* 时间配置 */
lisa_btn_cb_t callback; /* 事件回调函数 */
void *user_data; /* 用户数据 */
} lisa_btn_adc_config_t;
/* ===== GPIO按键配置 ===== */
typedef struct {
uint8_t pin_num; /* 引脚号 */
uint8_t iomux_func; /* IOMUX复用功能 */
uint8_t active_level; /* 有效电平 (0=低电平, 1=高电平) */
bool pull_enable; /* 是否使能上下拉 */
bool pull_up; /* true=上拉, false=下拉 */
} lisa_btn_gpio_item_t;
typedef struct {
const char *gpio_dev_name; /* GPIO设备名称 (默认"gpio0") */
uint8_t button_count; /* 按键数量 */
const lisa_btn_gpio_item_t *buttons; /* GPIO按键配置数组 */
lisa_btn_time_config_t time_config; /* 时间配置 */
lisa_btn_cb_t callback; /* 事件回调函数 */
void *user_data; /* 用户数据 */
} lisa_btn_gpio_config_t;
/* ===== 接口函数 ===== */
/**
* @brief 初始化ADC按键驱动
* @param config ADC按键配置
* @return 0=成功, 其他=失败
*/
int lisa_btn_adc_init(const lisa_btn_adc_config_t *config);
/**
* @brief 初始化GPIO按键驱动
* @param config GPIO按键配置
* @return 0=成功, 其他=失败
*/
int lisa_btn_gpio_init(const lisa_btn_gpio_config_t *config);
/**
* @brief 反初始化按键驱动
*/
void lisa_btn_deinit(void);
#ifdef __cplusplus
}
#endif
#endif /* __LISA_BTN_H__ */

View File

@@ -0,0 +1,3 @@
listenai_library_named(config)
listenai_library_sources(config_parser.c)
listenai_include_directories(${CMAKE_CURRENT_SOURCE_DIR})

View File

@@ -0,0 +1,621 @@
#include "config_parser.h"
#include "cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "sysheap.h"
// Helper function to duplicate a string
static char *strdup_safe(const char *str)
{
if (!str) {
return NULL;
}
char *new_str = psram_malloc(strlen(str) + 1);
if (new_str) {
strcpy(new_str, str);
}
return new_str;
}
// Helper function to parse a string array
static StringArray parse_string_array(const cJSON *array_json)
{
StringArray result = {0};
if (!cJSON_IsArray(array_json)) {
return result;
}
size_t count = cJSON_GetArraySize(array_json);
if (count == 0) {
return result;
}
char **items = psram_malloc(count * sizeof(char *));
if (!items) {
return result;
}
size_t valid_count = 0;
for (size_t i = 0; i < count; i++) {
const cJSON *item = cJSON_GetArrayItem(array_json, i);
if (cJSON_IsString(item)) {
items[valid_count] = strdup_safe(item->valuestring);
if (items[valid_count]) {
valid_count++;
}
}
}
result.items = items;
result.count = valid_count;
return result;
}
// Helper function to free a string array
static void free_string_array(StringArray *array)
{
if (!array || !array->items) {
return;
}
for (size_t i = 0; i < array->count; i++) {
if (array->items[i]) {
psram_free(array->items[i]);
}
}
psram_free(array->items);
array->items = NULL;
array->count = 0;
}
// Parse a single resource configuration
static ResourceConfig parse_resource_config(const cJSON *resource_json)
{
ResourceConfig resource = {0};
if (!resource_json) {
return resource;
}
const cJSON *name = cJSON_GetObjectItemCaseSensitive(resource_json, "name");
const cJSON *check = cJSON_GetObjectItemCaseSensitive(resource_json, "check");
const cJSON *address = cJSON_GetObjectItemCaseSensitive(resource_json, "address");
const cJSON *size = cJSON_GetObjectItemCaseSensitive(resource_json, "size");
const cJSON *crc32 = cJSON_GetObjectItemCaseSensitive(resource_json, "crc32");
if (cJSON_IsString(name)) {
resource.name = strdup_safe(name->valuestring);
}
if (cJSON_IsBool(check)) {
resource.check = cJSON_IsTrue(check);
} else {
// Default to false if not specified
resource.check = false;
}
if (cJSON_IsNumber(address)) {
resource.address = (uint32_t)address->valuedouble;
}
if (cJSON_IsNumber(size)) {
resource.size = (uint32_t)size->valuedouble;
}
if (cJSON_IsNumber(crc32)) {
resource.crc32 = (uint32_t)crc32->valuedouble;
}
return resource;
}
// Parse resources array
static ResourceArray parse_resources(const cJSON *resources_json)
{
ResourceArray result = {0};
if (!cJSON_IsArray(resources_json)) {
return result;
}
size_t count = cJSON_GetArraySize(resources_json);
if (count == 0) {
return result;
}
ResourceConfig *items = psram_malloc(count * sizeof(ResourceConfig));
if (!items) {
return result;
}
size_t valid_count = 0;
for (size_t i = 0; i < count; i++) {
const cJSON *item = cJSON_GetArrayItem(resources_json, i);
if (item) {
items[valid_count] = parse_resource_config(item);
valid_count++;
}
}
result.items = items;
result.count = valid_count;
return result;
}
// Parse network configuration
static NetworkConfig parse_network_config(const cJSON *network_json)
{
NetworkConfig config = {0};
if (!network_json) {
return config;
}
// Parse DNS configuration
const cJSON *dns_json = cJSON_GetObjectItemCaseSensitive(network_json, "dns");
if (dns_json) {
const cJSON *servers = cJSON_GetObjectItemCaseSensitive(dns_json, "servers");
if (servers) {
config.dns.servers = parse_string_array(servers);
}
const cJSON *timeout = cJSON_GetObjectItemCaseSensitive(dns_json, "timeout-ms");
if (cJSON_IsNumber(timeout)) {
config.dns.timeout_ms = timeout->valueint;
}
const cJSON *retry_interval = cJSON_GetObjectItemCaseSensitive(dns_json, "retry-interval-ms");
if (cJSON_IsNumber(retry_interval)) {
config.dns.retry_interval_ms = retry_interval->valueint;
}
const cJSON *retry_times = cJSON_GetObjectItemCaseSensitive(dns_json, "retry-times");
if (cJSON_IsNumber(retry_times)) {
config.dns.retry_times = retry_times->valueint;
}
}
// Parse SNTP configuration
const cJSON *sntp_json = cJSON_GetObjectItemCaseSensitive(network_json, "sntp");
if (sntp_json) {
const cJSON *servers = cJSON_GetObjectItemCaseSensitive(sntp_json, "servers");
if (servers) {
config.sntp.servers = parse_string_array(servers);
}
const cJSON *timezone = cJSON_GetObjectItemCaseSensitive(sntp_json, "timezone");
if (cJSON_IsString(timezone)) {
config.sntp.timezone = strdup_safe(timezone->valuestring);
}
const cJSON *timeout = cJSON_GetObjectItemCaseSensitive(sntp_json, "timeout-ms");
if (cJSON_IsNumber(timeout)) {
config.sntp.timeout_ms = timeout->valueint;
}
const cJSON *retry_interval = cJSON_GetObjectItemCaseSensitive(sntp_json, "retry-interval-ms");
if (cJSON_IsNumber(retry_interval)) {
config.sntp.retry_interval_ms = retry_interval->valueint;
}
const cJSON *retry_times = cJSON_GetObjectItemCaseSensitive(sntp_json, "retry-times");
if (cJSON_IsNumber(retry_times)) {
config.sntp.retry_times = retry_times->valueint;
}
}
return config;
}
// Parse wake-up configuration
static WakeUpConfig parse_wake_up_config(const cJSON *wake_up_json)
{
WakeUpConfig config = {0};
if (!wake_up_json) {
return config;
}
// Parse voice wake-up configuration
const cJSON *voice_json = cJSON_GetObjectItemCaseSensitive(wake_up_json, "voice");
if (voice_json) {
const cJSON *enable = cJSON_GetObjectItemCaseSensitive(voice_json, "enable");
if (cJSON_IsBool(enable)) {
config.voice.enable = cJSON_IsTrue(enable);
}
const cJSON *keywords_filter = cJSON_GetObjectItemCaseSensitive(voice_json, "keywords_filter");
if (cJSON_IsBool(keywords_filter)) {
config.voice.keywords_filter = cJSON_IsTrue(keywords_filter);
}
const cJSON *keywords = cJSON_GetObjectItemCaseSensitive(voice_json, "keywords");
if (keywords) {
config.voice.keywords = parse_string_array(keywords);
}
}
// Parse button wake-up configuration
const cJSON *button_json = cJSON_GetObjectItemCaseSensitive(wake_up_json, "button");
if (button_json) {
const cJSON *enable = cJSON_GetObjectItemCaseSensitive(button_json, "enable");
if (cJSON_IsBool(enable)) {
config.button.enable = cJSON_IsTrue(enable);
}
const cJSON *mode = cJSON_GetObjectItemCaseSensitive(button_json, "mode");
if (cJSON_IsString(mode)) {
config.button.mode = strdup_safe(mode->valuestring);
}
}
return config;
}
// Parse role configuration
static RoleConfig parse_role_config(const cJSON *role_json)
{
RoleConfig config = {0};
if (!role_json) {
return config;
}
const cJSON *name = cJSON_GetObjectItemCaseSensitive(role_json, "name");
if (cJSON_IsString(name)) {
config.name = strdup_safe(name->valuestring);
}
const cJSON *prompt = cJSON_GetObjectItemCaseSensitive(role_json, "prompt");
if (cJSON_IsString(prompt)) {
config.prompt = strdup_safe(prompt->valuestring);
}
const cJSON *hello_json = cJSON_GetObjectItemCaseSensitive(role_json, "hello");
if (hello_json) {
const cJSON *text = cJSON_GetObjectItemCaseSensitive(hello_json, "text");
if (cJSON_IsString(text)) {
config.hello.text = strdup_safe(text->valuestring);
}
const cJSON *tone_id = cJSON_GetObjectItemCaseSensitive(hello_json, "tone-id");
if (cJSON_IsNumber(tone_id)) {
config.hello.tone_id = tone_id->valueint;
}
}
return config;
}
// Parse cloud configuration
static void parse_cloud_config(CloudConfig *config, const cJSON *cloud_json)
{
if (!config || !cloud_json) {
return;
}
const cJSON *name = cJSON_GetObjectItemCaseSensitive(cloud_json, "name");
if (cJSON_IsString(name)) {
config->name = strdup_safe(name->valuestring);
}
const cJSON *config_json = cJSON_GetObjectItemCaseSensitive(cloud_json, "config");
if (config_json) {
const cJSON *pid = cJSON_GetObjectItemCaseSensitive(config_json, "pid");
if (cJSON_IsString(pid)) {
config->pid = strdup_safe(pid->valuestring);
}
const cJSON *sid = cJSON_GetObjectItemCaseSensitive(config_json, "sid");
if (cJSON_IsString(sid)) {
config->sid = strdup_safe(sid->valuestring);
}
const cJSON *app_id = cJSON_GetObjectItemCaseSensitive(config_json, "app-id");
if (cJSON_IsString(app_id)) {
config->app_id = strdup_safe(app_id->valuestring);
}
const cJSON *app_key = cJSON_GetObjectItemCaseSensitive(config_json, "app-key");
if (cJSON_IsString(app_key)) {
config->app_key = strdup_safe(app_key->valuestring);
}
}
// Parse auth configuration
const cJSON *auth_json = cJSON_GetObjectItemCaseSensitive(cloud_json, "auth");
if (auth_json) {
const cJSON *url = cJSON_GetObjectItemCaseSensitive(auth_json, "url");
if (cJSON_IsString(url)) {
config->auth.url = strdup_safe(url->valuestring);
}
}
// Parse WebSocket configuration
const cJSON *ws_json = cJSON_GetObjectItemCaseSensitive(cloud_json, "websocket");
if (ws_json) {
const cJSON *host = cJSON_GetObjectItemCaseSensitive(ws_json, "host");
if (cJSON_IsString(host)) {
config->websocket.host = strdup_safe(host->valuestring);
}
const cJSON *port = cJSON_GetObjectItemCaseSensitive(ws_json, "port");
if (cJSON_IsString(port)) {
config->websocket.port = strdup_safe(port->valuestring);
}
const cJSON *path = cJSON_GetObjectItemCaseSensitive(ws_json, "path");
if (cJSON_IsString(path)) {
config->websocket.path = strdup_safe(path->valuestring);
}
const cJSON *scheme = cJSON_GetObjectItemCaseSensitive(ws_json, "scheme");
if (cJSON_IsString(scheme)) {
config->websocket.scheme = strdup_safe(scheme->valuestring);
}
}
}
// Main configuration parsing function
const Config *config_parse(const char *json_str)
{
if (!json_str) {
return NULL;
}
cJSON *json = cJSON_Parse(json_str);
if (!json) {
const char *error_ptr = cJSON_GetErrorPtr();
if (error_ptr) {
printf("JSON parse error before: %s\n", error_ptr);
}
return NULL;
}
Config *config = psram_calloc(1, sizeof(Config));
if (!config) {
cJSON_Delete(json);
return NULL;
}
// Parse application info
const cJSON *app = cJSON_GetObjectItemCaseSensitive(json, "application");
if (!app) {
config_free(config);
cJSON_Delete(json);
return NULL;
}
// Parse basic application info
const cJSON *name = cJSON_GetObjectItemCaseSensitive(app, "name");
if (cJSON_IsString(name)) {
config->name = strdup_safe(name->valuestring);
}
const cJSON *version = cJSON_GetObjectItemCaseSensitive(app, "version");
if (cJSON_IsString(version)) {
config->version = strdup_safe(version->valuestring);
}
// Parse abilities
const cJSON *abilities = cJSON_GetObjectItemCaseSensitive(app, "abilities");
if (abilities) {
const cJSON *chat = cJSON_GetObjectItemCaseSensitive(abilities, "chat");
if (chat) {
const cJSON *cloud = cJSON_GetObjectItemCaseSensitive(chat, "cloud");
if (cloud) {
parse_cloud_config(&config->abilities.chat.cloud, cloud);
}
}
}
// Parse config section
const cJSON *config_json = cJSON_GetObjectItemCaseSensitive(app, "config");
if (config_json) {
// Parse wake-up configuration
const cJSON *wake_up_json = cJSON_GetObjectItemCaseSensitive(config_json, "wake-up");
if (wake_up_json) {
config->wake_up = parse_wake_up_config(wake_up_json);
}
// Parse role configuration
const cJSON *role_json = cJSON_GetObjectItemCaseSensitive(config_json, "role");
if (role_json) {
config->role = parse_role_config(role_json);
}
// Parse network configuration
const cJSON *network_json = cJSON_GetObjectItemCaseSensitive(config_json, "network");
if (network_json) {
config->network = parse_network_config(network_json);
}
// Parse resources
const cJSON *resources_json = cJSON_GetObjectItemCaseSensitive(config_json, "resources");
if (resources_json) {
config->resources = parse_resources(resources_json);
}
}
cJSON_Delete(json);
return config;
}
// Free configuration memory
void config_free(Config *config)
{
if (!config) {
return;
}
// Free basic application info
if (config->name) {
psram_free(config->name);
}
if (config->version) {
psram_free(config->version);
}
// Free cloud configuration
if (config->abilities.chat.cloud.name) {
psram_free(config->abilities.chat.cloud.name);
}
if (config->abilities.chat.cloud.pid) {
psram_free(config->abilities.chat.cloud.pid);
}
if (config->abilities.chat.cloud.sid) {
psram_free(config->abilities.chat.cloud.sid);
}
if (config->abilities.chat.cloud.app_id) {
psram_free(config->abilities.chat.cloud.app_id);
}
if (config->abilities.chat.cloud.app_key) {
psram_free(config->abilities.chat.cloud.app_key);
}
if (config->abilities.chat.cloud.auth.url) {
psram_free(config->abilities.chat.cloud.auth.url);
}
if (config->abilities.chat.cloud.websocket.host) {
psram_free(config->abilities.chat.cloud.websocket.host);
}
if (config->abilities.chat.cloud.websocket.port) {
psram_free(config->abilities.chat.cloud.websocket.port);
}
if (config->abilities.chat.cloud.websocket.path) {
psram_free(config->abilities.chat.cloud.websocket.path);
}
if (config->abilities.chat.cloud.websocket.scheme) {
psram_free(config->abilities.chat.cloud.websocket.scheme);
}
// Free wake-up configuration
free_string_array(&config->wake_up.voice.keywords);
if (config->wake_up.button.mode) {
psram_free(config->wake_up.button.mode);
}
// Free role configuration
if (config->role.name) {
psram_free(config->role.name);
}
if (config->role.prompt) {
psram_free(config->role.prompt);
}
if (config->role.hello.text) {
psram_free(config->role.hello.text);
}
// Free network configuration
free_string_array(&config->network.dns.servers);
free_string_array(&config->network.sntp.servers);
if (config->network.sntp.timezone) {
psram_free(config->network.sntp.timezone);
}
// Free resources
if (config->resources.items) {
for (size_t i = 0; i < config->resources.count; i++) {
if (config->resources.items[i].name) {
psram_free(config->resources.items[i].name);
}
}
psram_free(config->resources.items);
}
psram_free(config);
}
// Print configuration for debugging
void config_print(const Config *config)
{
if (!config) {
printf("Configuration is NULL\n");
return;
}
printf("=== Application Configuration ===\n");
printf("Name: %s\n", config->name ? config->name : "");
printf("Version: %s\n\n", config->version ? config->version : "");
printf("=== Cloud Configuration ===\n");
printf("Name: %s\n", config->abilities.chat.cloud.name ? config->abilities.chat.cloud.name : "");
printf("PID: %s\n", config->abilities.chat.cloud.pid ? config->abilities.chat.cloud.pid : "");
printf("SID: %s\n", config->abilities.chat.cloud.sid ? config->abilities.chat.cloud.sid : "");
printf("App ID: %s\n", config->abilities.chat.cloud.app_id ? config->abilities.chat.cloud.app_id : "");
printf("App Key: %s\n", config->abilities.chat.cloud.app_key ? "[HIDDEN]" : "");
printf("Auth URL: %s\n", config->abilities.chat.cloud.auth.url ? config->abilities.chat.cloud.auth.url : "");
printf("WebSocket: %s://%s:%s%s\n\n",
config->abilities.chat.cloud.websocket.scheme ? config->abilities.chat.cloud.websocket.scheme : "",
config->abilities.chat.cloud.websocket.host ? config->abilities.chat.cloud.websocket.host : "",
config->abilities.chat.cloud.websocket.port ? config->abilities.chat.cloud.websocket.port : "",
config->abilities.chat.cloud.websocket.path ? config->abilities.chat.cloud.websocket.path : "");
printf("=== Wake-up Configuration ===\n");
printf("Voice Wake-up: %s\n", config->wake_up.voice.enable ? "Enabled" : "Disabled");
printf("Wake-up Keywords:\n");
for (size_t i = 0; i < config->wake_up.voice.keywords.count; i++) {
printf(" - %s\n", config->wake_up.voice.keywords.items[i]);
}
printf("Button Wake-up: %s\n", config->wake_up.button.enable ? "Enabled" : "Disabled");
printf("Button Mode: %s\n\n", config->wake_up.button.mode ? config->wake_up.button.mode : "");
printf("=== Role Configuration ===\n");
printf("Name: %s\n", config->role.name ? config->role.name : "");
printf("Prompt: %s\n", config->role.prompt ? config->role.prompt : "");
printf("Hello Text: %s\n", config->role.hello.text ? config->role.hello.text : "");
printf("Hello Tone ID: %d\n\n", config->role.hello.tone_id);
printf("=== Network Configuration ===\n");
printf("DNS Servers:\n");
for (size_t i = 0; i < config->network.dns.servers.count; i++) {
printf(" - %s\n", config->network.dns.servers.items[i]);
}
printf("DNS Timeout: %ums, Retry: %dx every %ums\n", config->network.dns.timeout_ms,
config->network.dns.retry_times, config->network.dns.retry_interval_ms);
printf("\nSNTP Servers:\n");
for (size_t i = 0; i < config->network.sntp.servers.count; i++) {
printf(" - %s\n", config->network.sntp.servers.items[i]);
}
printf("SNTP Timezone: %s\n", config->network.sntp.timezone ? config->network.sntp.timezone : "");
printf("SNTP Timeout: %ums, Retry: %dx every %ums\n", config->network.sntp.timeout_ms,
config->network.sntp.retry_times, config->network.sntp.retry_interval_ms);
printf("\n=== Resources ===\n");
for (size_t i = 0; i < config->resources.count; i++) {
const ResourceConfig *res = &config->resources.items[i];
printf("Resource %zu:\n", i + 1);
printf(" Name: %s\n", res->name ? res->name : "");
printf(" Check: %s\n", res->check ? "Yes" : "No");
printf(" Address: 0x%08X\n", res->address);
printf(" Size: %u bytes\n", res->size);
printf(" CRC32: 0x%08X\n", res->crc32);
printf("\n");
}
}
// Global configuration instance
static const Config *g_config = NULL;
// Get the global configuration instance
const Config *config_get(void)
{
return g_config;
}
// Initialize the global configuration
const Config *config_init(const char *config_json_str)
{
if (g_config) {
return g_config;
}
g_config = config_parse(config_json_str);
return g_config;
}
const ResourceConfig *config_get_resource_by_name(const char *name)
{
for (size_t i = 0; i < g_config->resources.count; i++) {
if (strcmp(g_config->resources.items[i].name, name) == 0) {
return &g_config->resources.items[i];
}
}
return NULL;
}

View File

@@ -0,0 +1,135 @@
#ifndef CONFIG_PARSER_H
#define CONFIG_PARSER_H
#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
char** items;
size_t count;
} StringArray;
typedef struct {
char* name;
char* url;
} AuthConfig;
typedef struct {
char* host;
char* port;
char* path;
char* scheme;
} WebSocketConfig;
typedef struct {
char* name;
char* pid;
char* sid;
char* app_id;
char* app_key;
AuthConfig auth;
WebSocketConfig websocket;
} CloudConfig;
typedef struct {
bool enable;
bool keywords_filter;
StringArray keywords;
} VoiceWakeUpConfig;
typedef struct {
bool enable;
char* mode;
} ButtonWakeUpConfig;
typedef struct {
VoiceWakeUpConfig voice;
ButtonWakeUpConfig button;
} WakeUpConfig;
typedef struct {
char* text;
int tone_id;
} HelloConfig;
typedef struct {
char* name;
char* prompt;
HelloConfig hello;
} RoleConfig;
typedef struct {
StringArray servers;
uint32_t timeout_ms;
uint32_t retry_interval_ms;
uint32_t retry_times;
} DnsConfig;
typedef struct {
StringArray servers;
char* timezone;
uint32_t timeout_ms;
uint32_t retry_interval_ms;
uint32_t retry_times;
} SntpConfig;
typedef struct {
DnsConfig dns;
SntpConfig sntp;
} NetworkConfig;
typedef struct {
char* name;
bool check;
uint32_t address;
uint32_t size;
uint32_t crc32;
} ResourceConfig;
typedef struct {
ResourceConfig* items;
size_t count;
} ResourceArray;
typedef struct {
struct {
CloudConfig cloud;
} chat;
} AbilitiesConfig;
typedef struct {
char* name;
char* version;
AbilitiesConfig abilities;
WakeUpConfig wake_up;
RoleConfig role;
NetworkConfig network;
ResourceArray resources;
} Config;
// Parse JSON string into Config structure
const Config* config_parse(const char* json_str);
// Free memory allocated by config_parse
void config_free(Config* config);
// Print configuration for debugging
void config_print(const Config* config);
// Get global configuration instance
const Config* config_get(void);
// Initialize global configuration
const Config* config_init(const char* config_json_str);
const ResourceConfig *config_get_resource_by_name(const char *name);
#ifdef __cplusplus
}
#endif
#endif // CONFIG_PARSER_H

View File

@@ -0,0 +1,11 @@
listenai_library_named(app_dis)
listenai_library_sources(
app_display.c
lv_fs_net.c
lv_img_net_loader.c
)
listenai_include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
)

View File

@@ -0,0 +1,49 @@
#include "stdint.h"
#include "lisa_display.h"
#include "kv.h"
#include "lisa_kv.h"
#define APP_DISPLAY_BRIGHTNESS_DEFAULT 70
#define APP_DISPLAY_BRIGHTNESS_MIN 10
#define APP_DISPLAY_BRIGHTNESS_MAX 100
#define APP_DISPLAY_BRIGHTNESS_REVERT 0
int app_display_set_brightness(uint8_t val)
{
if (val > APP_DISPLAY_BRIGHTNESS_MAX) {
val = APP_DISPLAY_BRIGHTNESS_MAX;
}
if (val < APP_DISPLAY_BRIGHTNESS_MIN) {
val = APP_DISPLAY_BRIGHTNESS_MIN;
}
uint8_t saved = val;
#if APP_DISPLAY_BRIGHTNESS_REVERT
val = APP_DISPLAY_BRIGHTNESS_MAX - val;
#endif
lisa_display_set_brightness(lisa_display_get(), val);
lisa_kv_set_int(KV_KEY_USER_BRIGHTNESS, saved);
return 0;
}
uint8_t app_display_get_brightness(void)
{
int val = APP_DISPLAY_BRIGHTNESS_DEFAULT;
int r = lisa_kv_get_int(KV_KEY_USER_BRIGHTNESS, &val);
return val;
}
int app_display_brightness_init(void)
{
uint8_t val = app_display_get_brightness();
app_display_set_brightness(val);
return 0;
}

View File

@@ -0,0 +1,10 @@
#ifndef __APP_DISPLAY_H__
#define __APP_DISPLAY_H__
#include "stdint.h"
int app_display_brightness_init(void);
uint8_t app_display_get_brightness(void);
int app_display_set_brightness(uint8_t val);
#endif

View File

@@ -0,0 +1,622 @@
#define TAG "LV_FS_NET"
#include <string.h>
#include <stdint.h>
#include <stdio.h>
#include "HTTPCUsr_api.h"
#include "lv_fs_net.h"
#include "lisa_log.h"
#include "lvgl.h"
#define LV_FS_NET_ENABLE_CACHE_MODE (1)
#define NET_FS_HTTP_TIMEOUT_SEC (10)
#ifdef LV_FS_NET_ENABLE_CACHE_MODE
#define NET_FS_CACHE_CHUNK_SIZE (4096)
#define NET_FS_GLOBAL_CACHE_COUNT (2)
#endif
typedef struct {
char url[HTTP_CLIENT_MAX_URL_LENGTH];
HTTPParameters *http_params;
HTTP_CLIENT http_info;
uint32_t position;
uint32_t bytes_read;
bool connection_active;
bool eof_reached;
#ifdef LV_FS_NET_ENABLE_CACHE_MODE
uint8_t *cache_buffer;
uint32_t cache_size;
uint32_t cache_position;
bool owns_cache;
#endif
} net_file_t;
#ifdef LV_FS_NET_ENABLE_CACHE_MODE
typedef struct {
char url[HTTP_CLIENT_MAX_URL_LENGTH];
uint8_t *cache_buffer;
uint32_t cache_size;
uint32_t last_access_time;
bool in_use;
} global_cache_entry_t;
static global_cache_entry_t g_cache_pool[NET_FS_GLOBAL_CACHE_COUNT];
static uint32_t g_cache_access_counter = 0;
#endif
static bool is_valid_url(const char *url);
static bool fs_ready_cb(lv_fs_drv_t *drv);
static void *fs_open_cb(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode);
static lv_fs_res_t fs_close_cb(lv_fs_drv_t *drv, void *file_p);
static lv_fs_res_t fs_read_cb(lv_fs_drv_t *drv, void *file_p, void *buf, uint32_t btr, uint32_t *br);
static lv_fs_res_t fs_seek_cb(lv_fs_drv_t *drv, void *file_p, uint32_t pos, lv_fs_whence_t whence);
static lv_fs_res_t fs_tell_cb(lv_fs_drv_t *drv, void *file_p, uint32_t *pos_p);
static int http_connect(net_file_t *net_file, uint32_t start_position);
static void http_disconnect(net_file_t *net_file);
static int http_reconnect_at_position(net_file_t *net_file, uint32_t position);
#ifdef LV_FS_NET_ENABLE_CACHE_MODE
static int download_entire_file(net_file_t *net_file);
static global_cache_entry_t *find_cache_entry(const char *url);
static global_cache_entry_t *find_lru_cache_entry(void);
static void store_to_global_cache(const char *url, uint8_t *buffer, uint32_t size);
static void init_global_cache(void);
#endif
static lv_fs_drv_t fs_drv;
void lv_fs_net_init(void)
{
lv_fs_drv_init(&fs_drv);
fs_drv.letter = 'N';
fs_drv.cache_size = 0;
fs_drv.ready_cb = fs_ready_cb;
fs_drv.open_cb = fs_open_cb;
fs_drv.close_cb = fs_close_cb;
fs_drv.read_cb = fs_read_cb;
fs_drv.seek_cb = fs_seek_cb;
fs_drv.tell_cb = fs_tell_cb;
fs_drv.dir_open_cb = NULL;
fs_drv.dir_read_cb = NULL;
fs_drv.dir_close_cb = NULL;
lv_fs_drv_register(&fs_drv);
#ifdef LV_FS_NET_ENABLE_CACHE_MODE
init_global_cache();
#endif
LOGI("Network file system driver registered with letter 'N'");
}
static bool is_valid_url(const char *url)
{
if (url == NULL || strlen(url) == 0) {
return false;
}
if (strncmp(url, "http://", 7) != 0 && strncmp(url, "https://", 8) != 0) {
LOGE("URL must start with http:// or https://");
return false;
}
size_t url_len = strlen(url);
if (url_len < 10 || url_len > 2048) {
LOGE("URL length invalid: %zu", url_len);
return false;
}
return true;
}
static bool fs_ready_cb(lv_fs_drv_t *drv)
{
return true;
}
static int http_connect(net_file_t *net_file, uint32_t start_position)
{
int ret;
if (net_file->connection_active) {
LOGW("Connection already active");
return 0;
}
if (net_file->http_params == NULL) {
net_file->http_params = (HTTPParameters *)lv_mem_alloc(sizeof(HTTPParameters));
if (net_file->http_params == NULL) {
LOGE("Failed to allocate HTTP parameters");
return -1;
}
memset(net_file->http_params, 0, sizeof(HTTPParameters));
}
strncpy(net_file->http_params->Uri, net_file->url, HTTP_CLIENT_MAX_URL_LENGTH - 1);
net_file->http_params->HttpVerb = VerbGet;
net_file->http_params->nTimeout = NET_FS_HTTP_TIMEOUT_SEC;
ret = HTTPC_open(net_file->http_params);
if (ret != 0) {
LOGE("HTTPC_open failed: %d", ret);
return -1;
}
char *range_header = NULL;
if (start_position > 0) {
range_header = (char *)lv_mem_alloc(128);
if (range_header != NULL) {
snprintf(range_header, 128, "Range: bytes=%u-\r\n", start_position);
LOGD("Using Range header: %s", range_header);
}
}
ret = HTTPC_request(net_file->http_params, (HTTP_CLIENT_GET_HEADER)range_header);
if (range_header != NULL) {
lv_mem_free(range_header);
}
if (ret != 0) {
LOGE("HTTPC_request failed: %d", ret);
HTTPC_close(net_file->http_params);
return -1;
}
ret = HTTPC_get_request_info(net_file->http_params, &net_file->http_info);
if (ret != 0) {
LOGE("HTTPC_get_request_info failed: %d", ret);
HTTPC_close(net_file->http_params);
return -1;
}
net_file->connection_active = true;
net_file->position = start_position;
net_file->bytes_read = 0;
LOGI("HTTP connected: file_size=%u, status=%u, start_pos=%u", net_file->http_info.TotalResponseBodyLength,
net_file->http_info.HTTPStatusCode, start_position);
return 0;
}
static void http_disconnect(net_file_t *net_file)
{
if (net_file->connection_active && net_file->http_params != NULL) {
HTTPC_close(net_file->http_params);
net_file->connection_active = false;
LOGD("HTTP disconnected");
}
}
static int http_reconnect_at_position(net_file_t *net_file, uint32_t position)
{
LOGD("Reconnecting at position %u", position);
http_disconnect(net_file);
return http_connect(net_file, position);
}
#ifdef LV_FS_NET_ENABLE_CACHE_MODE
static void init_global_cache(void)
{
memset(g_cache_pool, 0, sizeof(g_cache_pool));
g_cache_access_counter = 0;
LOGI("Global cache initialized: %d slots", NET_FS_GLOBAL_CACHE_COUNT);
}
static global_cache_entry_t *find_cache_entry(const char *url)
{
for (int i = 0; i < NET_FS_GLOBAL_CACHE_COUNT; i++) {
if (g_cache_pool[i].cache_buffer != NULL && strcmp(g_cache_pool[i].url, url) == 0) {
g_cache_pool[i].last_access_time = ++g_cache_access_counter;
LOGI("Cache HIT: %s (slot %d, size=%u)", url, i, g_cache_pool[i].cache_size);
return &g_cache_pool[i];
}
}
LOGD("Cache MISS: %s", url);
return NULL;
}
static global_cache_entry_t *find_lru_cache_entry(void)
{
global_cache_entry_t *lru_entry = &g_cache_pool[0];
uint32_t min_access_time = g_cache_pool[0].last_access_time;
for (int i = 1; i < NET_FS_GLOBAL_CACHE_COUNT; i++) {
if (g_cache_pool[i].cache_buffer == NULL) {
LOGD("Found empty cache slot %d", i);
return &g_cache_pool[i];
}
if (g_cache_pool[i].in_use) {
continue;
}
if (g_cache_pool[i].last_access_time < min_access_time) {
min_access_time = g_cache_pool[i].last_access_time;
lru_entry = &g_cache_pool[i];
}
}
if (lru_entry->cache_buffer != NULL) {
LOGI("Evicting cache: %s (size=%u)", lru_entry->url, lru_entry->cache_size);
lv_mem_free(lru_entry->cache_buffer);
lru_entry->cache_buffer = NULL;
}
return lru_entry;
}
static void store_to_global_cache(const char *url, uint8_t *buffer, uint32_t size)
{
global_cache_entry_t *entry = find_lru_cache_entry();
strncpy(entry->url, url, HTTP_CLIENT_MAX_URL_LENGTH - 1);
entry->url[HTTP_CLIENT_MAX_URL_LENGTH - 1] = '\0';
entry->cache_buffer = buffer;
entry->cache_size = size;
entry->last_access_time = ++g_cache_access_counter;
entry->in_use = true;
LOGI("Stored to global cache: %s (size=%u, slot=%ld)", url, size, entry - g_cache_pool);
}
static int download_entire_file(net_file_t *net_file)
{
if (net_file == NULL || !net_file->connection_active) {
LOGE("Invalid net_file or connection not active");
return -1;
}
uint32_t file_size = net_file->http_info.TotalResponseBodyLength;
if (file_size == 0) {
LOGE("File size is 0, cannot download");
return -1;
}
LOGI("Starting full file download: %u bytes from %s", file_size, net_file->url);
net_file->cache_buffer = (uint8_t *)lv_mem_alloc(file_size);
if (net_file->cache_buffer == NULL) {
LOGE("Failed to allocate %u bytes for cache buffer", file_size);
return -1;
}
uint32_t total_downloaded = 0;
uint8_t *write_ptr = net_file->cache_buffer;
while (total_downloaded < file_size) {
uint32_t bytes_to_read = (file_size - total_downloaded > NET_FS_CACHE_CHUNK_SIZE)
? NET_FS_CACHE_CHUNK_SIZE
: (file_size - total_downloaded);
UINT32 received = 0;
int ret = HTTPC_read(net_file->http_params, write_ptr, bytes_to_read, &received);
if (ret != 0 && ret != HTTP_CLIENT_EOS) {
LOGE("HTTPC_read failed during cache download: %d (downloaded %u/%u bytes)", ret, total_downloaded,
file_size);
lv_mem_free(net_file->cache_buffer);
net_file->cache_buffer = NULL;
return -1;
}
if (received == 0) {
if (total_downloaded < file_size) {
LOGW("Download incomplete: got %u/%u bytes", total_downloaded, file_size);
}
break;
}
write_ptr += received;
total_downloaded += received;
static uint32_t last_progress = 0;
uint32_t progress = (total_downloaded * 100) / file_size;
if (progress >= last_progress + 10 || total_downloaded == file_size) {
LOGI("Download progress: %u%% (%u/%u bytes)", progress, total_downloaded, file_size);
last_progress = progress;
}
if (ret == HTTP_CLIENT_EOS) {
LOGD("Reached end of stream");
break;
}
}
net_file->cache_size = total_downloaded;
net_file->cache_position = 0;
LOGH("net_file->cache_buffer", net_file->cache_buffer, 10);
LOGI("File download completed: %u/%u bytes cached", total_downloaded, file_size);
http_disconnect(net_file);
return (total_downloaded == file_size) ? 0 : -1;
}
#endif // LV_FS_NET_ENABLE_CACHE_MODE
static void *fs_open_cb(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode)
{
if (!(mode & LV_FS_MODE_RD)) {
LOGE("Network FS only supports read mode");
return NULL;
}
if (!is_valid_url(path)) {
LOGE("Invalid URL: %s", path);
return NULL;
}
#ifdef LV_FS_NET_ENABLE_CACHE_MODE
LOGI("Opening network file: %s (cache_mode=enabled)", path);
#else
LOGI("Opening network file: %s (cache_mode=disabled)", path);
#endif
net_file_t *net_file = (net_file_t *)lv_mem_alloc(sizeof(net_file_t));
if (net_file == NULL) {
LOGE("Failed to allocate net_file_t");
return NULL;
}
memset(net_file, 0, sizeof(net_file_t));
strncpy(net_file->url, path, HTTP_CLIENT_MAX_URL_LENGTH - 1);
net_file->url[HTTP_CLIENT_MAX_URL_LENGTH - 1] = '\0';
net_file->http_params = NULL;
net_file->position = 0;
net_file->bytes_read = 0;
net_file->connection_active = false;
net_file->eof_reached = false;
#ifdef LV_FS_NET_ENABLE_CACHE_MODE
net_file->cache_buffer = NULL;
net_file->cache_size = 0;
net_file->cache_position = 0;
net_file->owns_cache = false;
global_cache_entry_t *cached = find_cache_entry(path);
if (cached != NULL) {
net_file->cache_buffer = cached->cache_buffer;
net_file->cache_size = cached->cache_size;
net_file->cache_position = 0;
net_file->owns_cache = false;
LOGI("Using cached file (size=%u bytes)", net_file->cache_size);
return (void *)net_file;
}
#endif
if (http_connect(net_file, 0) != 0) {
LOGE("Failed to connect to HTTP server");
lv_mem_free(net_file);
return NULL;
}
LOGI("Opened network file (size=%u bytes)", net_file->http_info.TotalResponseBodyLength);
#ifdef LV_FS_NET_ENABLE_CACHE_MODE
LOGI("Cache mode enabled, downloading entire file...");
if (download_entire_file(net_file) != 0) {
LOGE("Failed to download file to cache");
http_disconnect(net_file);
if (net_file->http_params != NULL) {
lv_mem_free(net_file->http_params);
}
lv_mem_free(net_file);
return NULL;
}
net_file->owns_cache = false;
store_to_global_cache(path, net_file->cache_buffer, net_file->cache_size);
LOGI("File successfully cached, total %u bytes", net_file->cache_size);
#endif
return (void *)net_file;
}
static lv_fs_res_t fs_close_cb(lv_fs_drv_t *drv, void *file_p)
{
if (file_p == NULL) {
LOGE("Attempted to close NULL file pointer");
return LV_FS_RES_INV_PARAM;
}
net_file_t *net_file = (net_file_t *)file_p;
LOGD("Closing network file: %s", net_file->url);
http_disconnect(net_file);
if (net_file->http_params != NULL) {
lv_mem_free(net_file->http_params);
}
#ifdef LV_FS_NET_ENABLE_CACHE_MODE
global_cache_entry_t *cached = find_cache_entry(net_file->url);
if (cached != NULL) {
cached->in_use = false;
LOGD("Released cache entry (keeping data): %s", net_file->url);
}
#endif
lv_mem_free(net_file);
LOGD("Network file closed successfully");
return LV_FS_RES_OK;
}
static lv_fs_res_t fs_read_cb(lv_fs_drv_t *drv, void *file_p, void *buf, uint32_t btr, uint32_t *br)
{
if (file_p == NULL || buf == NULL) {
LOGE("Invalid parameters: file_p=%p, buf=%p", file_p, buf);
if (br != NULL) {
*br = 0;
}
return LV_FS_RES_INV_PARAM;
}
net_file_t *net_file = (net_file_t *)file_p;
#ifdef LV_FS_NET_ENABLE_CACHE_MODE
if (net_file->cache_position >= net_file->cache_size) {
if (br != NULL) {
*br = 0;
}
LOGD("EOF reached in cache (position: %u/%u)", net_file->cache_position, net_file->cache_size);
return LV_FS_RES_OK;
}
uint32_t available = net_file->cache_size - net_file->cache_position;
uint32_t to_read = (btr < available) ? btr : available;
memcpy(buf, net_file->cache_buffer + net_file->cache_position, to_read);
net_file->cache_position += to_read;
if (br != NULL) {
*br = to_read;
}
LOGD("Read from cache: %u/%u bytes (position: %u/%u)", to_read, btr, net_file->cache_position,
net_file->cache_size);
return LV_FS_RES_OK;
#else
if (net_file->eof_reached) {
if (br != NULL) {
*br = 0;
}
LOGD("EOF already reached");
return LV_FS_RES_OK;
}
if (!net_file->connection_active) {
LOGE("HTTP connection not active");
if (br != NULL) {
*br = 0;
}
return LV_FS_RES_UNKNOWN;
}
UINT32 received = 0;
int ret = HTTPC_read(net_file->http_params, buf, btr, &received);
if (ret != 0) {
if (ret == HTTP_CLIENT_EOS || received == 0) {
LOGD("End of stream reached");
net_file->eof_reached = true;
} else {
LOGE("HTTPC_read failed: %d", ret);
}
}
net_file->position += received;
net_file->bytes_read += received;
if (br != NULL) {
*br = received;
}
LOGD("Read from stream: %u/%u bytes (position: %u, total_read: %u/%u)", received, btr, net_file->position,
net_file->bytes_read, net_file->http_info.TotalResponseBodyLength);
return LV_FS_RES_OK;
#endif
}
static lv_fs_res_t fs_seek_cb(lv_fs_drv_t *drv, void *file_p, uint32_t pos, lv_fs_whence_t whence)
{
if (file_p == NULL) {
LOGE("Invalid file pointer");
return LV_FS_RES_INV_PARAM;
}
net_file_t *net_file = (net_file_t *)file_p;
uint32_t new_position;
uint32_t old_position;
uint32_t file_size;
#ifdef LV_FS_NET_ENABLE_CACHE_MODE
old_position = net_file->cache_position;
file_size = net_file->cache_size;
#else
old_position = net_file->position;
file_size = net_file->http_info.TotalResponseBodyLength;
#endif
switch (whence) {
case LV_FS_SEEK_SET:
new_position = pos;
break;
case LV_FS_SEEK_CUR:
new_position = old_position + pos;
break;
case LV_FS_SEEK_END:
if (pos > file_size) {
LOGE("Seek offset exceeds file size: %u > %u", pos, file_size);
return LV_FS_RES_INV_PARAM;
}
new_position = file_size - pos;
break;
default:
LOGE("Invalid whence parameter: %d", whence);
return LV_FS_RES_INV_PARAM;
}
if (new_position > file_size) {
LOGW("Seek position beyond file size, clamping");
new_position = file_size;
}
#ifdef LV_FS_NET_ENABLE_CACHE_MODE
net_file->cache_position = new_position;
LOGD("Cache seek from %u to %u (whence=%d, offset=%u)", old_position, new_position, whence, pos);
return LV_FS_RES_OK;
#else
if (new_position != net_file->position) {
LOGD("Stream seek requires reconnect: %u -> %u", old_position, new_position);
if (http_reconnect_at_position(net_file, new_position) != 0) {
LOGE("Failed to reconnect at position %u", new_position);
return LV_FS_RES_UNKNOWN;
}
net_file->eof_reached = false;
}
LOGD("Stream seek from %u to %u (whence=%d, offset=%u)", old_position, new_position, whence, pos);
return LV_FS_RES_OK;
#endif
}
static lv_fs_res_t fs_tell_cb(lv_fs_drv_t *drv, void *file_p, uint32_t *pos_p)
{
if (file_p == NULL || pos_p == NULL) {
return LV_FS_RES_INV_PARAM;
}
net_file_t *net_file = (net_file_t *)file_p;
#ifdef LV_FS_NET_ENABLE_CACHE_MODE
*pos_p = net_file->cache_position;
#else
*pos_p = net_file->position;
#endif
return LV_FS_RES_OK;
}

View File

@@ -0,0 +1,14 @@
#ifndef LV_FS_NET_H
#define LV_FS_NET_H
#ifdef __cplusplus
extern "C" {
#endif
void lv_fs_net_init(void);
#ifdef __cplusplus
} /*extern "C"*/
#endif
#endif /*LV_FS_NET_H*/

View File

@@ -0,0 +1,348 @@
#define TAG "LV_IMG_NET_LOADER"
#include "lv_img_net_loader.h"
#include "HTTPCUsr_api.h"
#include "lisa_log.h"
#include <string.h>
#include <stdio.h>
#define NET_IMG_TIMEOUT_SEC (15)
#define NET_IMG_MAX_SIZE (512 * 1024) // 512KB max image size
typedef struct {
lv_img_dsc_t img_dsc;
uint8_t* data;
char* url;
} net_img_cache_t;
// Simple cache for loaded images
#define NET_IMG_CACHE_SIZE 5
static net_img_cache_t img_cache[NET_IMG_CACHE_SIZE];
static int cache_initialized = 0;
static bool is_valid_image_url(const char* url);
static int download_image_data(const char* url, uint8_t** data, uint32_t* size);
static int find_cache_slot(const char* url);
static int find_empty_cache_slot(void);
static int find_lru_cache_slot(void);
static void clear_cache_slot(int slot);
static bool is_valid_image_url(const char* url)
{
if (!url || strlen(url) == 0) {
return false;
}
// Remove N: prefix if present
if (strncmp(url, "N:", 2) == 0) {
url += 2;
}
if (strncmp(url, "http://", 7) != 0 && strncmp(url, "https://", 8) != 0) {
LOGE("URL must start with http:// or https://");
return false;
}
size_t url_len = strlen(url);
if (url_len < 10 || url_len > 1024) {
LOGE("URL length invalid: %zu", url_len);
return false;
}
return true;
}
static int download_image_data(const char* url, uint8_t** data, uint32_t* size)
{
HTTPParameters* http_params = NULL;
HTTP_CLIENT http_info = {0};
int ret = -1;
uint8_t* buffer = NULL;
uint32_t total_downloaded = 0;
// Remove N: prefix if present
if (strncmp(url, "N:", 2) == 0) {
url += 2;
}
LOGI("Downloading image from: %s", url);
// Allocate HTTP parameters
http_params = (HTTPParameters*)lv_mem_alloc(sizeof(HTTPParameters));
if (!http_params) {
LOGE("Failed to allocate HTTP parameters");
return -1;
}
memset(http_params, 0, sizeof(HTTPParameters));
// Setup HTTP request
strncpy(http_params->Uri, url, HTTP_CLIENT_MAX_URL_LENGTH - 1);
http_params->HttpVerb = VerbGet;
http_params->nTimeout = NET_IMG_TIMEOUT_SEC;
// Open connection
ret = HTTPC_open(http_params);
if (ret != 0) {
LOGE("HTTPC_open failed: %d", ret);
goto cleanup;
}
// Send request
ret = HTTPC_request(http_params, NULL);
if (ret != 0) {
LOGE("HTTPC_request failed: %d", ret);
goto cleanup;
}
// Get response info
ret = HTTPC_get_request_info(http_params, &http_info);
if (ret != 0) {
LOGE("HTTPC_get_request_info failed: %d", ret);
goto cleanup;
}
uint32_t content_length = http_info.TotalResponseBodyLength;
LOGI("Image size: %u bytes, status: %u", content_length, http_info.HTTPStatusCode);
if (http_info.HTTPStatusCode != 200) {
LOGE("HTTP error: %u", http_info.HTTPStatusCode);
ret = -1;
goto cleanup;
}
if (content_length == 0 || content_length > NET_IMG_MAX_SIZE) {
LOGE("Invalid image size: %u", content_length);
ret = -1;
goto cleanup;
}
// Allocate buffer for image data
buffer = (uint8_t*)lv_mem_alloc(content_length);
if (!buffer) {
LOGE("Failed to allocate %u bytes for image data", content_length);
ret = -1;
goto cleanup;
}
// Download image data
uint8_t* write_ptr = buffer;
uint32_t remaining = content_length;
while (remaining > 0) {
UINT32 received = 0;
uint32_t to_read = (remaining > 4096) ? 4096 : remaining;
ret = HTTPC_read(http_params, write_ptr, to_read, &received);
if (ret != 0 && ret != HTTP_CLIENT_EOS) {
LOGE("HTTPC_read failed: %d (downloaded %u/%u)", ret, total_downloaded, content_length);
break;
}
if (received == 0) {
LOGW("Received 0 bytes, stopping download");
break;
}
write_ptr += received;
total_downloaded += received;
remaining -= received;
// Log progress for large images
if (content_length > 50000) {
uint32_t progress = (total_downloaded * 100) / content_length;
static uint32_t last_progress = 0;
if (progress >= last_progress + 20) {
LOGI("Download progress: %u%% (%u/%u bytes)", progress, total_downloaded, content_length);
last_progress = progress;
}
}
if (ret == HTTP_CLIENT_EOS) {
break;
}
}
if (total_downloaded == content_length) {
*data = buffer;
*size = total_downloaded;
buffer = NULL; // Don't free in cleanup
ret = 0;
LOGI("Image download completed: %u bytes", total_downloaded);
} else {
LOGE("Download incomplete: %u/%u bytes", total_downloaded, content_length);
ret = -1;
}
cleanup:
if (http_params) {
HTTPC_close(http_params);
lv_mem_free(http_params);
}
if (buffer) {
lv_mem_free(buffer);
}
return ret;
}
static int find_cache_slot(const char* url)
{
if (!cache_initialized) return -1;
for (int i = 0; i < NET_IMG_CACHE_SIZE; i++) {
if (img_cache[i].url && strcmp(img_cache[i].url, url) == 0) {
return i;
}
}
return -1;
}
static int find_empty_cache_slot(void)
{
for (int i = 0; i < NET_IMG_CACHE_SIZE; i++) {
if (img_cache[i].url == NULL) {
return i;
}
}
return -1;
}
static int find_lru_cache_slot(void)
{
// For simplicity, just use slot 0 as LRU
return 0;
}
static void clear_cache_slot(int slot)
{
if (slot < 0 || slot >= NET_IMG_CACHE_SIZE) return;
if (img_cache[slot].data) {
lv_mem_free(img_cache[slot].data);
img_cache[slot].data = NULL;
}
if (img_cache[slot].url) {
lv_mem_free(img_cache[slot].url);
img_cache[slot].url = NULL;
}
memset(&img_cache[slot].img_dsc, 0, sizeof(lv_img_dsc_t));
}
void lv_img_net_loader_init(void)
{
if (cache_initialized) return;
memset(img_cache, 0, sizeof(img_cache));
cache_initialized = 1;
LOGI("Network image loader initialized");
}
lv_img_dsc_t* lv_img_net_load(const char* url)
{
if (!url || !is_valid_image_url(url)) {
LOGE("Invalid URL: %s", url ? url : "NULL");
return NULL;
}
if (!cache_initialized) {
lv_img_net_loader_init();
}
// Check cache first
int cache_slot = find_cache_slot(url);
if (cache_slot >= 0) {
LOGI("Using cached image: %s", url);
return &img_cache[cache_slot].img_dsc;
}
// Download image
uint8_t* img_data = NULL;
uint32_t img_size = 0;
if (download_image_data(url, &img_data, &img_size) != 0) {
LOGE("Failed to download image: %s", url);
return NULL;
}
// Find cache slot
cache_slot = find_empty_cache_slot();
if (cache_slot < 0) {
cache_slot = find_lru_cache_slot();
clear_cache_slot(cache_slot);
}
// Store in cache
img_cache[cache_slot].data = img_data;
img_cache[cache_slot].url = (char*)lv_mem_alloc(strlen(url) + 1);
if (img_cache[cache_slot].url) {
strcpy(img_cache[cache_slot].url, url);
}
// Setup image descriptor
img_cache[cache_slot].img_dsc.header.always_zero = 0;
img_cache[cache_slot].img_dsc.header.w = 0; // Will be determined by LVGL
img_cache[cache_slot].img_dsc.header.h = 0; // Will be determined by LVGL
img_cache[cache_slot].img_dsc.data_size = img_size;
img_cache[cache_slot].img_dsc.data = img_data;
// Try to determine image format from data
if (img_size >= 8) {
// PNG signature
if (memcmp(img_data, "\x89PNG\r\n\x1a\n", 8) == 0) {
img_cache[cache_slot].img_dsc.header.cf = LV_IMG_CF_RAW;
}
// JPEG signature
else if (img_size >= 2 && img_data[0] == 0xFF && img_data[1] == 0xD8) {
img_cache[cache_slot].img_dsc.header.cf = LV_IMG_CF_RAW;
}
// Default
else {
img_cache[cache_slot].img_dsc.header.cf = LV_IMG_CF_RAW;
}
} else {
img_cache[cache_slot].img_dsc.header.cf = LV_IMG_CF_RAW;
}
LOGI("Cached image: %s (%u bytes, slot %d)", url, img_size, cache_slot);
return &img_cache[cache_slot].img_dsc;
}
void lv_img_net_free(lv_img_dsc_t* img_dsc)
{
if (!img_dsc) return;
// Find the cache slot
for (int i = 0; i < NET_IMG_CACHE_SIZE; i++) {
if (&img_cache[i].img_dsc == img_dsc) {
LOGI("Freeing cached image: %s", img_cache[i].url ? img_cache[i].url : "unknown");
clear_cache_slot(i);
return;
}
}
LOGW("Image descriptor not found in cache");
}
lv_res_t lv_img_set_src_net(lv_obj_t* img, const char* url)
{
if (!img || !url) {
LOGE("Invalid parameters: img=%p, url=%s", img, url ? url : "NULL");
return LV_RES_INV;
}
lv_img_dsc_t* img_dsc = lv_img_net_load(url);
if (!img_dsc) {
LOGE("Failed to load network image: %s", url);
return LV_RES_INV;
}
lv_img_set_src(img, img_dsc);
LOGI("Set image source: %s", url);
return LV_RES_OK;
}

View File

@@ -0,0 +1,41 @@
#ifndef LV_IMG_NET_LOADER_H
#define LV_IMG_NET_LOADER_H
#ifdef __cplusplus
extern "C" {
#endif
#include "lvgl.h"
/**
* @brief Initialize the network image loader
*/
void lv_img_net_loader_init(void);
/**
* @brief Load an image from network URL and return image descriptor
* @param url The network URL (without N: prefix)
* @return Pointer to image descriptor, or NULL if failed
* @note The returned pointer should be freed with lv_img_net_free() when done
*/
lv_img_dsc_t* lv_img_net_load(const char* url);
/**
* @brief Free a network-loaded image
* @param img_dsc Image descriptor returned by lv_img_net_load()
*/
void lv_img_net_free(lv_img_dsc_t* img_dsc);
/**
* @brief Set image source from network URL
* @param img LVGL image object
* @param url Network URL (with or without N: prefix)
* @return LV_RES_OK on success, error code on failure
*/
lv_res_t lv_img_set_src_net(lv_obj_t* img, const char* url);
#ifdef __cplusplus
}
#endif
#endif /* LV_IMG_NET_LOADER_H */

View File

@@ -0,0 +1,9 @@
listenai_library_named(flash)
listenai_library_sources(
listen_flash.c
)
listenai_include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
)

View File

@@ -0,0 +1,24 @@
#include "lisa_log.h"
#include "listen_flash.h"
#include "arcs_ap_base.h"
FLASH_DEV s_app_flash_dev = {
.base_addr = CMN_FLASHC_BASE,
.d_width = 4,
.sclk_div = 0xFF, //divider is 1
.run_mod = RUN_WITHOUT_INT,
.timeout = 2000000,
.addr_bytes = 3,
.addr_auto = 0,
};
void listen_flash_init(void)
{
int err = flash_init(&s_app_flash_dev, 0, 0);
LISA_ASSERT((err == 0), "Flash Driver Initialize err %d ", err);
}
FLASH_DEV *listen_flash_get_dev(void)
{
return &s_app_flash_dev;
}

View File

@@ -0,0 +1,10 @@
#ifndef __LISTENAI_FLASH_H__
#define __LISTENAI_FLASH_H__
#include "spiflash.h"
void listen_flash_init(void);
FLASH_DEV * listen_flash_get_dev(void);
#endif

View File

@@ -0,0 +1,5 @@
listenai_library_named(app_fs)
listenai_library_sources(
app_fs.c
)

View File

@@ -0,0 +1,99 @@
#include <string.h>
#include "lsfs.h"
#include "disk/disk_access.h"
#include "arcs_ap.h"
#include "log_print.h"
#include "fs.h"
#if CONFIG_LVFS_POSIX_API
#include "lvfs.h"
#endif
// #define FLASHDISK_DEVICE "NAND:"
#define DISK_DEVICE "NAND:"
#define DISK_MOUNT_POINT "/" DISK_DEVICE
static struct lsfs_mount_t flash_lsfs_mnt = {
.type = LSFS_FATFS,
.mnt_point = DISK_MOUNT_POINT,
.fs_data = NULL,
};
static int fs_mount(struct lsfs_mount_t *mp)
{
int ret;
ret = lsfs_mount(mp);
if (ret != 0) {
CLOG("Failed to mount filesystem: %d", ret);
return ret;
}
CLOG("%s mounted successfully", mp->mnt_point);
}
static int list_dir(const char *path)
{
struct lsfs_dir_t dir;
struct lsfs_dirent entry;
int ret;
int nfile = 0, ndir = 0;
/* 初始化目录对象 */
lsfs_dir_t_init(&dir);
ret = lsfs_opendir(&dir, path);
CLOG("%s opendir: %d", path, ret);
if (ret == 0)
{
while (1)
{
ret = lsfs_readdir(&dir, &entry);
if (ret != 0 || entry.name[0] == 0)
break; /* Error or end of dir */
if (entry.type == LSFS_DIR_ENTRY_DIR)
{
/* Directory */
CLOG(" <DIR> %s", entry.name);
ndir++;
}
else
{
/* File */
CLOG("%10lu %s", entry.size, entry.name);
nfile++;
}
}
lsfs_closedir(&dir);
CLOG("%d dirs, %d files.", ndir, nfile);
}
else
{
CLOG("Failed to open \"%s\". (%u)", path, ret);
}
return ret;
}
int user_fs_init(void)
{
int ret = 0;
disk_init(NULL);
#if CONFIG_LVFS_POSIX_API
lvfs_init();
#endif
lsfs_init();
if (0 != fs_mount(&flash_lsfs_mnt)) {
ret = lsfs_mkfs(flash_lsfs_mnt.type, &flash_lsfs_mnt.mnt_point[1], NULL, 0);
if (ret != 0) {
CLOG("Failed to create filesystem: %d", ret);
return ret;
}
if (0 != fs_mount(&flash_lsfs_mnt)) {
CLOG("Failed to mount filesystem: %d", ret);
return ret;
}
}
return 0;
}

View File

@@ -0,0 +1,9 @@
listenai_library_named(ini)
listenai_library_sources(
ini.c
)
listenai_include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
)

View File

@@ -0,0 +1,27 @@
The "inih" library is distributed under the New BSD license:
Copyright (c) 2009, Ben Hoyt
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Ben Hoyt nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY BEN HOYT ''AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL BEN HOYT BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

328
src/middleware/inih/ini.c Normal file
View File

@@ -0,0 +1,328 @@
/* inih -- simple .INI file parser
SPDX-License-Identifier: BSD-3-Clause
Copyright (C) 2009-2025, Ben Hoyt
inih is released under the New BSD license (see LICENSE.txt). Go to the project
home page for more info:
https://github.com/benhoyt/inih
*/
#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS)
#define _CRT_SECURE_NO_WARNINGS
#endif
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include "ini.h"
#if !INI_USE_STACK
#if INI_CUSTOM_ALLOCATOR
#include <stddef.h>
void* ini_malloc(size_t size);
void ini_free(void* ptr);
void* ini_realloc(void* ptr, size_t size);
#else
#include <stdlib.h>
#define ini_malloc malloc
#define ini_free free
#define ini_realloc realloc
#endif
#endif
#define MAX_SECTION 50
#define MAX_NAME 50
/* Used by ini_parse_string() to keep track of string parsing state. */
typedef struct {
const char* ptr;
size_t num_left;
} ini_parse_string_ctx;
/* Strip whitespace chars off end of given string, in place. end must be a
pointer to the NUL terminator at the end of the string. Return s. */
static char* ini_rstrip(char* s, char* end)
{
while (end > s && isspace((unsigned char)(*--end)))
*end = '\0';
return s;
}
/* Return pointer to first non-whitespace char in given string. */
static char* ini_lskip(const char* s)
{
while (*s && isspace((unsigned char)(*s)))
s++;
return (char*)s;
}
/* Return pointer to first char (of chars) or inline comment in given string,
or pointer to NUL at end of string if neither found. Inline comment must
be prefixed by a whitespace character to register as a comment. */
static char* ini_find_chars_or_comment(const char* s, const char* chars)
{
#if INI_ALLOW_INLINE_COMMENTS
int was_space = 0;
while (*s && (!chars || !strchr(chars, *s)) &&
!(was_space && strchr(INI_INLINE_COMMENT_PREFIXES, *s))) {
was_space = isspace((unsigned char)(*s));
s++;
}
#else
while (*s && (!chars || !strchr(chars, *s))) {
s++;
}
#endif
return (char*)s;
}
/* Similar to strncpy, but ensures dest (size bytes) is
NUL-terminated, and doesn't pad with NULs. */
static char* ini_strncpy0(char* dest, const char* src, size_t size)
{
/* Could use strncpy internally, but it causes gcc warnings (see issue #91) */
size_t i;
for (i = 0; i < size - 1 && src[i]; i++)
dest[i] = src[i];
dest[i] = '\0';
return dest;
}
/* See documentation in header file. */
int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
void* user)
{
/* Uses a fair bit of stack (use heap instead if you need to) */
#if INI_USE_STACK
char line[INI_MAX_LINE];
size_t max_line = INI_MAX_LINE;
#else
char* line;
size_t max_line = INI_INITIAL_ALLOC;
#endif
#if INI_ALLOW_REALLOC && !INI_USE_STACK
char* new_line;
#endif
char section[MAX_SECTION] = "";
#if INI_ALLOW_MULTILINE
char prev_name[MAX_NAME] = "";
#endif
size_t offset;
char* start;
char* end;
char* name;
char* value;
int lineno = 0;
int error = 0;
char abyss[16]; /* Used to consume input when a line is too long. */
size_t abyss_len;
#if !INI_USE_STACK
line = (char*)ini_malloc(INI_INITIAL_ALLOC);
if (!line) {
return -2;
}
#endif
#if INI_HANDLER_LINENO
#define HANDLER(u, s, n, v) handler(u, s, n, v, lineno)
#else
#define HANDLER(u, s, n, v) handler(u, s, n, v)
#endif
/* Scan through stream line by line */
while (reader(line, (int)max_line, stream) != NULL) {
offset = strlen(line);
#if INI_ALLOW_REALLOC && !INI_USE_STACK
while (max_line < INI_MAX_LINE &&
offset == max_line - 1 && line[offset - 1] != '\n') {
max_line *= 2;
if (max_line > INI_MAX_LINE)
max_line = INI_MAX_LINE;
new_line = ini_realloc(line, max_line);
if (!new_line) {
ini_free(line);
return -2;
}
line = new_line;
if (reader(line + offset, (int)(max_line - offset), stream) == NULL)
break;
offset += strlen(line + offset);
}
#endif
lineno++;
/* If line exceeded INI_MAX_LINE bytes, discard till end of line. */
if (offset == max_line - 1 && line[offset - 1] != '\n') {
while (reader(abyss, sizeof(abyss), stream) != NULL) {
if (!error)
error = lineno;
abyss_len = strlen(abyss);
if (abyss_len > 0 && abyss[abyss_len - 1] == '\n')
break;
}
}
start = line;
#if INI_ALLOW_BOM
if (lineno == 1 && (unsigned char)start[0] == 0xEF &&
(unsigned char)start[1] == 0xBB &&
(unsigned char)start[2] == 0xBF) {
start += 3;
}
#endif
start = ini_rstrip(ini_lskip(start), line + offset);
if (strchr(INI_START_COMMENT_PREFIXES, *start)) {
/* Start-of-line comment */
}
#if INI_ALLOW_MULTILINE
else if (*prev_name && *start && start > line) {
#if INI_ALLOW_INLINE_COMMENTS
end = ini_find_chars_or_comment(start, NULL);
*end = '\0';
ini_rstrip(start, end);
#endif
/* Non-blank line with leading whitespace, treat as continuation
of previous name's value (as per Python configparser). */
if (!HANDLER(user, section, prev_name, start) && !error)
error = lineno;
}
#endif
else if (*start == '[') {
/* A "[section]" line */
end = ini_find_chars_or_comment(start + 1, "]");
if (*end == ']') {
*end = '\0';
ini_strncpy0(section, start + 1, sizeof(section));
#if INI_ALLOW_MULTILINE
*prev_name = '\0';
#endif
#if INI_CALL_HANDLER_ON_NEW_SECTION
if (!HANDLER(user, section, NULL, NULL) && !error)
error = lineno;
#endif
}
else if (!error) {
/* No ']' found on section line */
error = lineno;
}
}
else if (*start) {
/* Not a comment, must be a name[=:]value pair */
end = ini_find_chars_or_comment(start, "=:");
if (*end == '=' || *end == ':') {
*end = '\0';
name = ini_rstrip(start, end);
value = end + 1;
#if INI_ALLOW_INLINE_COMMENTS
end = ini_find_chars_or_comment(value, NULL);
*end = '\0';
#endif
value = ini_lskip(value);
ini_rstrip(value, end);
#if INI_ALLOW_MULTILINE
ini_strncpy0(prev_name, name, sizeof(prev_name));
#endif
/* Valid name[=:]value pair found, call handler */
if (!HANDLER(user, section, name, value) && !error)
error = lineno;
}
else {
/* No '=' or ':' found on name[=:]value line */
#if INI_ALLOW_NO_VALUE
*end = '\0';
name = ini_rstrip(start, end);
if (!HANDLER(user, section, name, NULL) && !error)
error = lineno;
#else
if (!error)
error = lineno;
#endif
}
}
#if INI_STOP_ON_FIRST_ERROR
if (error)
break;
#endif
}
#if !INI_USE_STACK
ini_free(line);
#endif
return error;
}
/* See documentation in header file. */
int ini_parse_file(FILE* file, ini_handler handler, void* user)
{
return ini_parse_stream((ini_reader)fgets, file, handler, user);
}
/* See documentation in header file. */
int ini_parse(const char* filename, ini_handler handler, void* user)
{
FILE* file;
int error;
file = fopen(filename, "r");
if (!file)
return -1;
error = ini_parse_file(file, handler, user);
fclose(file);
return error;
}
/* An ini_reader function to read the next line from a string buffer. This
is the fgets() equivalent used by ini_parse_string(). */
static char* ini_reader_string(char* str, int num, void* stream) {
ini_parse_string_ctx* ctx = (ini_parse_string_ctx*)stream;
const char* ctx_ptr = ctx->ptr;
size_t ctx_num_left = ctx->num_left;
char* strp = str;
char c;
if (ctx_num_left == 0 || num < 2)
return NULL;
while (num > 1 && ctx_num_left != 0) {
c = *ctx_ptr++;
ctx_num_left--;
*strp++ = c;
if (c == '\n')
break;
num--;
}
*strp = '\0';
ctx->ptr = ctx_ptr;
ctx->num_left = ctx_num_left;
return str;
}
/* See documentation in header file. */
int ini_parse_string(const char* string, ini_handler handler, void* user) {
return ini_parse_string_length(string, strlen(string), handler, user);
}
/* See documentation in header file. */
int ini_parse_string_length(const char* string, size_t length,
ini_handler handler, void* user) {
ini_parse_string_ctx ctx;
ctx.ptr = string;
ctx.num_left = length;
return ini_parse_stream((ini_reader)ini_reader_string, &ctx, handler,
user);
}

189
src/middleware/inih/ini.h Normal file
View File

@@ -0,0 +1,189 @@
/* inih -- simple .INI file parser
SPDX-License-Identifier: BSD-3-Clause
Copyright (C) 2009-2025, Ben Hoyt
inih is released under the New BSD license (see LICENSE.txt). Go to the project
home page for more info:
https://github.com/benhoyt/inih
*/
#ifndef INI_H
#define INI_H
/* Make this header file easier to include in C++ code */
#ifdef __cplusplus
extern "C" {
#endif
#include <stdio.h>
/* Nonzero if ini_handler callback should accept lineno parameter. */
#ifndef INI_HANDLER_LINENO
#define INI_HANDLER_LINENO 0
#endif
/* Visibility symbols, required for Windows DLLs */
#ifndef INI_API
#if defined _WIN32 || defined __CYGWIN__
# ifdef INI_SHARED_LIB
# ifdef INI_SHARED_LIB_BUILDING
# define INI_API __declspec(dllexport)
# else
# define INI_API __declspec(dllimport)
# endif
# else
# define INI_API
# endif
#else
# if defined(__GNUC__) && __GNUC__ >= 4
# define INI_API __attribute__ ((visibility ("default")))
# else
# define INI_API
# endif
#endif
#endif
/* Typedef for prototype of handler function.
Note that even though the value parameter has type "const char*", the user
may cast to "char*" and modify its content, as the value is not used again
after the call to ini_handler. This is not true of section and name --
those must not be modified.
*/
#if INI_HANDLER_LINENO
typedef int (*ini_handler)(void* user, const char* section,
const char* name, const char* value,
int lineno);
#else
typedef int (*ini_handler)(void* user, const char* section,
const char* name, const char* value);
#endif
/* Typedef for prototype of fgets-style reader function. */
typedef char* (*ini_reader)(char* str, int num, void* stream);
/* Parse given INI-style file. May have [section]s, name=value pairs
(whitespace stripped), and comments starting with ';' (semicolon). Section
is "" if name=value pair parsed before any section heading. name:value
pairs are also supported as a concession to Python's configparser.
For each name=value pair parsed, call handler function with given user
pointer as well as section, name, and value (data only valid for duration
of handler call). Handler should return nonzero on success, zero on error.
Returns 0 on success, line number of first error on parse error (doesn't
stop on first error), -1 on file open error, or -2 on memory allocation
error (only when INI_USE_STACK is zero).
*/
INI_API int ini_parse(const char* filename, ini_handler handler, void* user);
/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't
close the file when it's finished -- the caller must do that. */
INI_API int ini_parse_file(FILE* file, ini_handler handler, void* user);
/* Same as ini_parse(), but takes an ini_reader function pointer instead of
filename. Used for implementing custom or string-based I/O (see also
ini_parse_string). */
INI_API int ini_parse_stream(ini_reader reader, void* stream, ini_handler handler,
void* user);
/* Same as ini_parse(), but takes a zero-terminated string with the INI data
instead of a file. Useful for parsing INI data from a network socket or
which is already in memory. */
INI_API int ini_parse_string(const char* string, ini_handler handler, void* user);
/* Same as ini_parse_string(), but takes a string and its length, avoiding
strlen(). Useful for parsing INI data from a network socket or which is
already in memory, or interfacing with C++ std::string_view. */
INI_API int ini_parse_string_length(const char* string, size_t length, ini_handler handler, void* user);
/* Nonzero to allow multi-line value parsing, in the style of Python's
configparser. If allowed, ini_parse() will call the handler with the same
name for each subsequent line parsed. */
#ifndef INI_ALLOW_MULTILINE
#define INI_ALLOW_MULTILINE 1
#endif
/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of
the file. See https://github.com/benhoyt/inih/issues/21 */
#ifndef INI_ALLOW_BOM
#define INI_ALLOW_BOM 1
#endif
/* Chars that begin a start-of-line comment. Per Python configparser, allow
both ; and # comments at the start of a line by default. */
#ifndef INI_START_COMMENT_PREFIXES
#define INI_START_COMMENT_PREFIXES ";#"
#endif
/* Nonzero to allow inline comments (with valid inline comment characters
specified by INI_INLINE_COMMENT_PREFIXES). Set to 0 to turn off and match
Python 3.2+ configparser behaviour. */
#ifndef INI_ALLOW_INLINE_COMMENTS
#define INI_ALLOW_INLINE_COMMENTS 1
#endif
#ifndef INI_INLINE_COMMENT_PREFIXES
#define INI_INLINE_COMMENT_PREFIXES ";"
#endif
/* Nonzero to use stack for line buffer, zero to use heap (malloc/free). */
#ifndef INI_USE_STACK
#define INI_USE_STACK 1
#endif
/* Maximum line length for any line in INI file (stack or heap). Note that
this must be 3 more than the longest line (due to '\r', '\n', and '\0'). */
#ifndef INI_MAX_LINE
#define INI_MAX_LINE 200
#endif
/* Nonzero to allow heap line buffer to grow via realloc(), zero for a
fixed-size buffer of INI_MAX_LINE bytes. Only applies if INI_USE_STACK is
zero. */
#ifndef INI_ALLOW_REALLOC
#define INI_ALLOW_REALLOC 0
#endif
/* Initial size in bytes for heap line buffer. Only applies if INI_USE_STACK
is zero. */
#ifndef INI_INITIAL_ALLOC
#define INI_INITIAL_ALLOC 200
#endif
/* Stop parsing on first error (default is to keep parsing). */
#ifndef INI_STOP_ON_FIRST_ERROR
#define INI_STOP_ON_FIRST_ERROR 0
#endif
/* Nonzero to call the handler at the start of each new section (with
name and value NULL). Default is to only call the handler on
each name=value pair. */
#ifndef INI_CALL_HANDLER_ON_NEW_SECTION
#define INI_CALL_HANDLER_ON_NEW_SECTION 0
#endif
/* Nonzero to allow a name without a value (no '=' or ':' on the line) and
call the handler with value NULL in this case. Default is to treat
no-value lines as an error. */
#ifndef INI_ALLOW_NO_VALUE
#define INI_ALLOW_NO_VALUE 0
#endif
/* Nonzero to use custom ini_malloc, ini_free, and ini_realloc memory
allocation functions (INI_USE_STACK must also be 0). These functions must
have the same signatures as malloc/free/realloc and behave in a similar
way. ini_realloc is only needed if INI_ALLOW_REALLOC is set. */
#ifndef INI_CUSTOM_ALLOCATOR
#define INI_CUSTOM_ALLOCATOR 0
#endif
#ifdef __cplusplus
}
#endif
#endif /* INI_H */

View File

@@ -0,0 +1,3 @@
listenai_include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
)

6
src/middleware/kv/kv.h Normal file
View File

@@ -0,0 +1,6 @@
#ifndef __KV_H__
#define __KV_H__
#include "kv_user.h"
#endif

View File

@@ -0,0 +1,6 @@
#ifndef __KV_SYS_H__
#define __KV_SYS_H__
#define KV_KEY_SYS_WAKEWORD "sys.wakeword"
#endif

View File

@@ -0,0 +1,28 @@
#ifndef __KV_USER_H__
#define __KV_USER_H__
#define KV_KEY_USER_PID "user.pid"
#define KV_KEY_USER_SID "user.sid"
#define KV_KEY_APPID "user.appid"
#define KV_KEY_APPKEY "user.appkey"
#define KV_KEY_TOKEN "user.token"
#define KV_KEY_DEVICE_MODE "user.device_mode"
#define KV_KEY_USER_VOLUME "user.volume"
#define KV_KEY_USER_BRIGHTNESS "user.brightness"
#ifdef CONFIG_BOARD_ARCS_MINI
#define KV_KEY_USER_MIC_GAIN_DB "user.mic_gain_db"
#define KV_KEY_USER_AEC_GAIN_DB "user.aec_gain_db"
#define KV_KEY_USER_SPK_GAIN_DB "user.spk_gain_db"
#else // !CONFIG_BOARD_ARCS_MINI
#define KV_KEY_USER_MIC_GAIN "user.mic_gain"
#endif // CONFIG_BOARD_ARCS_MINI
#define KV_KEY_WAKEUP_MODE "user.wakeup_mode"
#define KV_KEY_FULL_DUPLEX "user.full_duplex"
#define KV_KEY_FULL_DUPLEX_TIMEOUT_MS "user.full_duplex_timeout_ms"
#define KV_KEY_IDLE_EXIT_TIMEOUT_MS "user.idle_exit_timeout_ms"
#define KV_KEY_USER_DISABLE_WAKEWORD_UPDATE "user.disable_wakeword_update"
#define KV_KEY_USER_DISABLE_TONE_UPDATE "user.disable_tone_update"
#define KV_KEY_USER_DISABLE_EMOJI_UPDATE "user.disable_emoji_update"
#define KV_KEY_USER_DEVICE_ID "user.device.id"
#endif

View File

@@ -0,0 +1,10 @@
listenai_library_named(led)
listenai_library_sources(
led.c
)
listenai_include_directories(
${CMAKE_CURRENT_SOURCE_DIR}
)

190
src/middleware/led/led.c Normal file
View File

@@ -0,0 +1,190 @@
#include "shell.h"
#include "stdint.h"
#include "stdbool.h"
#include "string.h"
/* LED service implementation */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "led.h"
#include "lisa_log.h"
#include "lisa_thread.h"
#include "Driver_GPIO.h"
#include "IOMuxManager.h"
#define TAG "led"
typedef enum {
LED_CMD_ON = 0,
LED_CMD_OFF,
LED_CMD_BLINK,
LED_CMD_STOP,
} led_cmd_e;
typedef struct {
led_cmd_e cmd;
uint32_t on_ms;
uint32_t off_ms;
} led_msg_t;
static lisa_thread_t *s_led_task = NULL;
static QueueHandle_t s_led_queue = NULL;
/**
* @brief Control LED
* @param state true: LED on, false: LED off
*/
void led_hw_control(bool state)
{
uint32_t pin_mask = 0x01 << 1;
uint32_t pin_value = !state; // LED is active low
GPIO_PinWrite(GPIOB(), pin_mask, pin_value);
}
/* Concrete hardware hooks using RGB helpers */
void led_hw_init(void)
{
/* Initialize GPIOB module */
GPIO_Initialize(GPIOB(), NULL, NULL);
/* Configure IO Mux for LED pins */
IOMuxManager_PinConfigure(CSK_IOMUX_PAD_B, 1, CSK_IOMUX_FUNC_DEFAULT); /* Green LED - Pin1 */
/* Configure Green LED - GPIOB Pin1 */
uint32_t green_pin_mask = 0x01 << 1;
GPIO_SetDir(GPIOB(), green_pin_mask, CSK_GPIO_DIR_OUTPUT);
GPIO_PinWrite(GPIOB(), green_pin_mask, 1); /* LED off (high level) */
}
void led_hw_on(void)
{
led_hw_control(true);
}
void led_hw_off(void)
{
led_hw_control(false);
}
static void led_task(void *param)
{
(void)param;
led_msg_t msg;
uint32_t on_ms = 0;
uint32_t off_ms = 0;
int blinking = 0;
for (;;) {
if (!blinking) {
if (xQueueReceive(s_led_queue, &msg, portMAX_DELAY) != pdPASS) {
continue;
}
} else {
/* While blinking, poll queue with timeout to allow preemption */
if (xQueueReceive(s_led_queue, &msg, 0) == pdPASS) {
/* Got a new command: fall through to handle */
} else {
/* Perform one blink cycle */
led_hw_on();
vTaskDelay(pdMS_TO_TICKS(on_ms));
led_hw_off();
vTaskDelay(pdMS_TO_TICKS(off_ms));
continue;
}
}
switch (msg.cmd) {
case LED_CMD_ON:
blinking = 0;
led_hw_on();
break;
case LED_CMD_OFF:
blinking = 0;
led_hw_off();
break;
case LED_CMD_BLINK:
if (msg.on_ms == 0 && msg.off_ms == 0) {
/* Treat as off */
blinking = 0;
led_hw_off();
} else {
on_ms = msg.on_ms ? msg.on_ms : 100;
off_ms = msg.off_ms ? msg.off_ms : 100;
blinking = 1;
/* Don't start the first cycle here, let the main loop handle it */
}
break;
case LED_CMD_STOP:
default:
blinking = 0;
led_hw_off();
break;
}
}
}
void app_led_init(void)
{
if (s_led_queue != NULL) {
return;
}
led_hw_init();
s_led_queue = xQueueCreate(4, sizeof(led_msg_t));
if (s_led_queue == NULL) {
LISA_LOGE(TAG, "create queue failed");
return;
}
lisa_thread_attr_t attr = {
.name = (uint8_t *)"led_task",
.stack_size = 2048,
.priority = LISA_OS_PRIORITY_LOW
};
s_led_task = lisa_thread_create(&attr, led_task, NULL);
if (s_led_task == NULL) {
LISA_LOGE(TAG, "create led task failed");
vQueueDelete(s_led_queue);
s_led_queue = NULL;
return;
}
LISA_LOGI(TAG, "LED service initialized");
}
static inline void led_send_cmd(led_cmd_e cmd, uint32_t on_ms, uint32_t off_ms)
{
if (!s_led_queue) {
LISA_LOGW(TAG, "LED queue not ready");
return;
}
led_msg_t msg = { .cmd = cmd, .on_ms = on_ms, .off_ms = off_ms };
BaseType_t ret = xQueueSend(s_led_queue, &msg, pdMS_TO_TICKS(100));
if (ret != pdPASS) {
LISA_LOGW(TAG, "LED queue send failed");
}
}
void app_led_on(void)
{
led_send_cmd(LED_CMD_ON, 0, 0);
}
void app_led_off(void)
{
led_send_cmd(LED_CMD_OFF, 0, 0);
}
void app_led_blink(uint32_t on_ms, uint32_t off_ms)
{
led_send_cmd(LED_CMD_BLINK, on_ms, off_ms);
}
void app_led_stop(void)
{
led_send_cmd(LED_CMD_STOP, 0, 0);
}

26
src/middleware/led/led.h Normal file
View File

@@ -0,0 +1,26 @@
#ifndef __LED_H__
#define __LED_H__
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Public APIs */
void app_led_init(void);
void app_led_on(void);
void app_led_off(void);
void app_led_blink(uint32_t on_ms, uint32_t off_ms);
void app_led_stop(void);
/* Platform hooks (weak) - implement in BSP if needed */
void led_hw_init(void);
void led_hw_on(void);
void led_hw_off(void);
#ifdef __cplusplus
}
#endif
#endif /* __LED_H__ */

View File

@@ -0,0 +1,5 @@
listenai_library_named(libc)
listenai_library_sources(
wrap_time.c
)

View File

@@ -0,0 +1,37 @@
/**
* @version 0.1
* @date 2022-12-19
* @author mokee
*
* Copyright (C) 2022 ANHUI LISTENAI Co., LTD All Rights Reserved
*/
#include <time.h>
#include <stdio.h>
#include <sys/time.h>
#include "listen_system.h"
int __wrap_gettimeofday(struct timeval *tv, struct timezone *tz)
{
if (!tv) return -1;
return ls_sys_get_time(tv);
}
long int __wrap_time(long int *_timer)
{
struct timeval tv;
ls_sys_get_time(&tv);
return (tv.tv_sec * 1000);
}
void __wrap_settimeofday(const struct timeval *tv, const struct timezone *tz)
{
ls_sys_set_timeval((struct timeval *)tv);
}
struct tm *__wrap_gmtime_r(const long int *tv_sec, struct tm *__tm)
{
return ls_sys_get_tmtime(tv_sec, __tm);
}

Some files were not shown because too many files have changed in this diff Show More