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

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.clangd
build/
.vscode/
.cache/
build*/
tools/tone_tool/output
tools/tone_tool/tts
*.lpk
listenai-dev-tools/

33
Dockerfile Normal file
View File

@@ -0,0 +1,33 @@
FROM docker.m.daocloud.io/library/ubuntu:24.04
RUN <<EOF
set -eux
apt-get update
apt-get install -y --no-install-recommends \
bzip2 \
ca-certificates \
wget \
python3 \
python3-pip \
python3-pillow \
libpython3-dev \
build-essential
rm -rf /var/lib/apt/lists/*
mkdir -p /opt/arcs
cd /opt/arcs
wget http://listenai-firmware-delivery.oss-cn-beijing.aliyuncs.com/ARCS/tools/dev-tools/linux-amd64/v0.0.1/listenai-tools.tar.gz \
-O /tmp/listenai-tools.tar.gz
tar xf /tmp/listenai-tools.tar.gz
rm /tmp/listenai-tools.tar.gz
wget https://listenai-firmware-delivery.oss-cn-beijing.aliyuncs.com/ARCS/tools/toolchain/linux-amd64/nuclei_riscv_newlibc_prebuilt_linux64_2025.02.tar.bz2 \
-O /tmp/nuclei_riscv_newlibc_prebuilt_linux64_2025.02.tar.bz2
tar xf /tmp/nuclei_riscv_newlibc_prebuilt_linux64_2025.02.tar.bz2
rm /tmp/nuclei_riscv_newlibc_prebuilt_linux64_2025.02.tar.bz2
EOF
ENV LISTENAI_TOOLS_PATH=/opt/arcs/listenai-tools \
NUCLEI_TOOLCHAIN_PATH=/opt/arcs/gcc

710
LIVEKIT_CALL_LINK.md Normal file
View File

@@ -0,0 +1,710 @@
# LiveKit 通话链路说明
本文档记录 `arcs_mini` 硬件端新增的通话链路:设备通过语音意图触发呼叫,连接 Node LiveKit 桥接服务,完成设备与设备、设备与 App/Web 之间的实时语音通话。
## 背景
原有主链路主要负责唤醒、语音对话、MCP 工具调用、TTS 播放等能力。新增通话能力后,硬件端多了一条独立的通话链路:
1. 设备联网后主动连接 LiveKit Node 桥接服务的 WebSocket。
2. 用户通过主链路语音对话说“给某人/某设备打电话”。
3. 主链路意图识别命中通话工具,调用硬件端 MCP 工具 `ls.built_in.call_device`
4. MCP 工具把目标设备号 `toId` 交给硬件端 LiveKit 桥接模块。
5. 桥接模块通过 WebSocket 给 Node 发送 `device_call`
6. Node 创建 invite、加入 LiveKit 房间,并把双方音频转成硬件可消费的 PCM 帧。
7. 硬件端收下行 PCM 播放,同时采集本机麦克风 PCM 上行给 Node。
这条链路和主对话链路是并行关系:主对话链路负责“识别用户想打电话”和“调用工具”,通话链路负责“真正建立通话和传输音频”。
## 核心链路
```mermaid
sequenceDiagram
participant User as 用户
participant Device as 硬件端 arcs_mini
participant MainCloud as 主链路云服务
participant MCP as MCP 工具 call_device
participant Bridge as 硬件 LiveKit Bridge
participant Node as Node LiveKit 桥接服务
participant Peer as 被叫端/对端
Device->>Node: WiFi IP ready 后连接 /ws/device?deviceId=<did>
User->>Device: 单击唤醒后说“给小红打电话”
Device->>MainCloud: 主链路语音对话/意图识别
MainCloud->>Device: MCP tool call: ls.built_in.call_device(toId)
Device->>MCP: 执行 MCP 工具
MCP->>Bridge: voice_livekit_bridge_request_call(toId)
Bridge->>Node: {"type":"device_call","toId":"..."}
Node->>Peer: incoming_call / invite
Peer->>Node: accept
Node->>Device: {"type":"call_start","sampleRate":16000,"channels":1}
Node-->>Device: 二进制 PCM 下行帧
Device-->>Node: 二进制 PCM 上行帧
Device->>Node: 来电中三击 {"type":"device_reject","inviteId":"..."}
Device->>Node: {"type":"device_hangup"} 或收到 call_end
```
## 新增/修改代码清单
### 1. LiveKit WebSocket 与音频桥接
文件:`src/category/comm/msgs/voice_livekit_poll_msg.c`
这是硬件端通话链路的核心文件,职责包括:
- 设备联网后连接 Node 桥接服务。
- 维护通话状态机。
- 处理 Node 下发的 JSON 控制消息。
- 处理 Node 下发的二进制 PCM 音频帧。
- 从本机麦克风采集 PCM上行发送给 Node。
- 处理来电响铃、双击接听、三击拒接、双击挂断。
- 通话结束后恢复唤醒和主云链路。
当前关键配置在文件顶部:
```c
#define LK_BRIDGE_WS_SCHEME "ws"
#define LK_BRIDGE_WS_HOST "39.108.110.91"
#define LK_BRIDGE_WS_PORT "3000"
#define LK_BRIDGE_WS_PATH_PREFIX "/ws/device?deviceId="
#define LK_BRIDGE_FALLBACK_DEVICE_ID "B"
#define LK_BRIDGE_DEFAULT_CALL_TARGET "f17c0a1a322ed2ad"
```
这些配置决定硬件端连接哪个 Node LiveKit 桥接服务。当前仍是编译期宏,部署到线上时需要改成目标服务器地址,或后续再改造成配置文件/云端下发。
#### 通话状态机
```c
typedef enum {
LK_CALL_IDLE = 0,
LK_CALL_RINGING,
LK_CALL_PREPARING,
LK_CALL_ACTIVE,
} lk_call_state_e;
```
含义:
- `LK_CALL_IDLE`:空闲状态,可以发起呼叫或接收来电。
- `LK_CALL_RINGING`:收到来电,设备播放提示音并显示表情,等待双击接听。
- `LK_CALL_PREPARING`:预留状态,用于播放器准备阶段。
- `LK_CALL_ACTIVE`:通话中,正在收发 PCM 音频。
#### WebSocket 连接
初始化入口:
```c
SYS_INIT(voice_livekit_poll_evt_init, SYS_INIT_LEVEL_PRE_APPLICATION, 50);
```
模块初始化时订阅:
```c
voice_msg_sub(VOICE_MSG_WIFI_IP_GOT, bridge_wifi_ip_got, NULL);
voice_msg_sub(VOICE_MSG_BUTTON_CHANGE, bridge_button_event, NULL);
```
当 WiFi 获取 IP 后,创建 `lk.bridge` 线程,线程会循环连接 Node
```c
bridge_connect();
```
WebSocket 路径由设备 ID 拼出:
```c
/ws/device?deviceId=<device_did>
```
设备 ID 优先来自:
```c
get_app_datas()->did
```
如果设备 ID 为空,则使用 `LK_BRIDGE_FALLBACK_DEVICE_ID`
#### JSON 控制消息
`handle_text_frame()` 负责处理 Node 发来的文本 JSON。
目前支持的消息:
- `hello`:连接确认。
- `play_url`:调试用,播放 URL。
- `stop`:停止播放。
- `incoming_call`:来电通知,进入响铃状态。
- `call_start`:通话开始,初始化音频播放和录音。
- `call_end`:通话结束,停止音频链路。
- `device_call_ack`:发起呼叫确认。
- `device_accept_ack`:接听确认。
- `device_reject_ack`:拒接确认。
- `device_hangup_ack`:挂断确认。
发给 Node 的消息:
```json
{"type":"device_call","toId":"目标设备号"}
{"type":"device_accept","inviteId":"来电 inviteId"}
{"type":"device_reject","inviteId":"来电 inviteId"}
{"type":"device_hangup"}
```
#### 二进制音频协议
二进制帧格式:
```c
#define LK_BRIDGE_MAGIC 0xA5
#define LK_BRIDGE_VERSION 0x01
#define LK_BRIDGE_HEADER_SIZE 20
#define LK_BRIDGE_SAMPLE_RATE 16000
#define LK_BRIDGE_CHANNELS 1
#define LK_BRIDGE_BITS 16
```
每个音频包:
- 前 20 字节:自定义 header。
- 后面 payloadPCM16 little-endian。
- 下行方向Node -> 硬件,`flags = 0x00`
- 上行方向:硬件 -> Node`flags = 0x01`
header 结构:
```c
typedef struct {
uint8_t magic;
uint8_t version;
uint8_t flags;
uint8_t channels;
uint32_t sample_rate;
uint32_t samples_per_channel;
uint32_t seq;
uint32_t timestamp;
} lk_bridge_header_t;
```
#### 下行播放
Node 下发的 PCM 二进制帧进入:
```c
handle_binary_frame()
```
处理流程:
1. 校验 magic/version/sampleRate/channels。
2. 确认当前处于 `LK_CALL_ACTIVE`
3. 把 PCM payload 放入 `g_pcm_queue`
4. `lk.audio.writer` 线程从队列取帧。
5. 调用 `lisa_audio_play_write()` 写入音频设备。
为降低断续感,播放端做了预填充:
```c
#define LK_BRIDGE_AUDIO_START_PREFILL 4
```
队列满时丢弃最旧帧,优先保持实时性,避免旧音频追播。
#### 上行录音
通话开始时,`start_stream_playback()` 会配置 `audio0`
```c
lisa_audio_record_config()
lisa_audio_register_callback()
lisa_audio_record_start()
```
录音回调:
```c
livekit_audio_record_cb()
```
处理流程:
1. 音频设备回调 PCM。
2. `uplink_accumulate_pcm()` 聚合为 640 字节一帧。
3. 放入 `g_uplink_queue`
4. `lk.audio.uplink` 线程组装 20 字节 header。
5. 调用 `lisa_ws_send_binary()` 发给 Node。
当前上行帧大小:
```c
#define LK_BRIDGE_UPLINK_FRAME_BYTES 640
```
对应 16kHz/16bit/mono 下约 20ms 音频。
#### 音频参数
当前通话链路使用:
```c
#define LK_BRIDGE_RECORD_ANALOG_GAIN 20
#define LK_BRIDGE_RECORD_DIGITAL_GAIN 0
#define LK_BRIDGE_PLAY_DIGITAL_GAIN (-18)
```
含义:
- `RECORD_ANALOG_GAIN`:设备麦克风采集增益。
- `RECORD_DIGITAL_GAIN`:设备麦克风数字增益。
- `PLAY_DIGITAL_GAIN`:设备播放通话音频的数字增益。
之前调试噪音时,主要就是围绕这几个参数和 Node 端音频增益做过调整。当前版本里,硬件端播放数字增益为 `-18`,上行录音模拟增益为 `20`
#### 来电响铃与表情
收到 `incoming_call` 后进入 `LK_CALL_RINGING`
```c
bridge_start_ringing()
```
行为:
- 保存 `inviteId``fromId`
- 显示表情:
```c
#define LK_BRIDGE_RING_EMOJI "wait"
```
- 循环播放本地提示音:
```c
player_mgr_play(LOCAL, app_tone_get_url(TONE_ID_94), 0);
```
- 每 2500ms 重复一次:
```c
#define LK_BRIDGE_RING_REPEAT_MS 2500
```
通话接通或结束后显示:
```c
#define LK_BRIDGE_IDLE_EMOJI "neutral"
```
#### 按键交互
`bridge_button_event()` 中处理 button 0 的双击和三击:
```c
if (evt->action == VOICE_MSG_BUTTON_ACTION_DOUBLE_CLICK) {
if (g_call_state == LK_CALL_RINGING) {
bridge_request_accept();
} else if (g_call_state == LK_CALL_IDLE) {
// 当前不再用双击主动发起呼叫
} else {
bridge_request_hangup();
}
} else if (evt->action == VOICE_MSG_BUTTON_ACTION_TRIPLE_CLICK) {
if (g_call_state == LK_CALL_RINGING) {
bridge_request_reject();
}
}
```
当前设计:
- 来电中:双击接听。
- 来电中:三击拒接。
- 通话中:双击挂断。
- 空闲中:双击不主动发起呼叫,发起呼叫改由语音意图/MCP 工具触发。
- 空闲中:三击不再打开二维码/信息页,避免和来电拒接冲突。
### 2. MCP 通话工具
文件:`apps/arcs-mini/mcp-tools/mcp_tool_call_device.c`
这个文件新增 MCP 工具:
```c
MCP_TOOL_DEFINE(ls.built_in.call_device, call_device_list, call_device_call);
```
工具名:
```text
ls.built_in.call_device
```
入参:
```json
{
"toId": "目标设备号"
}
```
执行逻辑:
```c
voice_livekit_bridge_request_call(to_id);
```
也就是说,主链路意图识别到“打电话”后,只要返回 MCP tool call硬件端就会通过该工具把目标设备号交给 LiveKit 桥接模块。
返回结果:
```json
{
"content": "呼叫已发起",
"ret": 0
}
```
如果失败则返回:
```json
{
"content": "呼叫发起失败",
"ret": -1
}
```
### 3. MCP 工具注册
文件:`apps/arcs-mini/mcp-tools/CMakeLists.txt`
新增编译文件:
```cmake
mcp_tool_call_device.c
```
这一步确保 `ls.built_in.call_device` 会进入固件,主链路才能调用到该工具。
### 4. LiveKit 事件模块注册
文件:`src/category/comm/msgs/CMakeLists.txt`
新增编译文件:
```cmake
voice_livekit_poll_msg.c
```
这一步确保 LiveKit WebSocket/音频桥接模块会进入固件,并通过 `SYS_INIT` 自动初始化。
### 5. 主按键逻辑调整
文件:`apps/arcs-mini/main.c`
主按键处理里,双击逻辑被调整为保留给 LiveKit 通话,不再触发原先可能冲突的行为:
```c
case VOICE_MSG_BUTTON_ACTION_DOUBLE_CLICK:
{
LISA_LOGI(TAG, "power button double click, reserved for livekit call");
break;
}
```
实际通话状态下的双击接听/挂断由 `voice_livekit_poll_msg.c` 中订阅的 `bridge_button_event()` 处理。
三击逻辑也被调整为保留给 LiveKit 来电拒接,主按键处理不再打开信息/二维码页:
```c
case VOICE_MSG_BUTTON_ACTION_TRIPLE_CLICK:
{
LISA_LOGI(TAG, "power button triple click, reserved for livekit reject");
break;
}
```
这样做的目的:
- 避免双击同时触发拍照识图、唤醒或其他主链路行为。
- 避免来电中三击拒接后又打开二维码/信息页。
- 让通话状态机拥有双击接听、三击拒接、双击挂断的优先处理权。
## 发起呼叫流程
### 语音发起
1. 用户单击设备按钮唤醒。
2. 用户说“给小红打电话”。
3. 主链路云服务识别为通话意图。
4. 主链路根据联系人/设备映射得到目标 `toId`
5. 主链路返回 MCP 调用:
```json
{
"name": "ls.built_in.call_device",
"arguments": {
"toId": "目标设备号"
}
}
```
6. 硬件端执行 `mcp_tool_call_device.c`
7. 调用 `voice_livekit_bridge_request_call(toId)`
8. 硬件端通过 WebSocket 发:
```json
{"type":"device_call","toId":"目标设备号"}
```
9. Node 创建 invite通知被叫端。
### 代码入口
```text
apps/arcs-mini/mcp-tools/mcp_tool_call_device.c
-> voice_livekit_bridge_request_call(toId)
src/category/comm/msgs/voice_livekit_poll_msg.c
-> lisa_ws_send_text({"type":"device_call","toId":"..."})
```
## 接听流程
1. Node 给硬件端发送:
```json
{
"type": "incoming_call",
"inviteId": "...",
"fromId": "..."
}
```
2. 硬件端进入 `LK_CALL_RINGING`
3. 设备循环播放提示音并显示 `wait` 表情。
4. 用户双击按钮。
5. 硬件端发送:
```json
{"type":"device_accept","inviteId":"..."}
```
6. Node 完成接听,双方加入 LiveKit 房间。
7. Node 下发:
```json
{"type":"call_start","sampleRate":16000,"channels":1}
```
8. 硬件端启动录音和播放,进入 `LK_CALL_ACTIVE`
## 拒接流程
来电中三击按钮:
```json
{"type":"device_reject","inviteId":"..."}
```
Node 收到后:
1. 校验 invite 仍为 `pending`
2. 将 invite 状态更新为 `rejected`
3. 回包给设备:
```json
{"type":"device_reject_ack","inviteId":"...","status":"rejected"}
```
4. 通知主叫端 `call_end`reason 为 `rejected`
硬件端发送拒接后会立即:
- 停止本地来电提示音。
- 清空当前 pending invite。
- 状态回到 `LK_CALL_IDLE`
- 显示空闲表情。
## 挂断流程
### 设备主动挂断
通话中双击按钮:
```json
{"type":"device_hangup"}
```
Node 结束 invite并通知双方 `call_end`
### 对端挂断
Node 下发:
```json
{"type":"call_end","reason":"..."}
```
硬件端执行:
```c
stop_stream_playback();
```
然后:
- 停止播放。
- 停止录音。
- 清空上下行队列。
- 关闭 PA。
- 恢复唤醒。
- 请求主云链路重连。
对应代码:
```c
app_wakeup_start();
voice_cloud_reconnect_async();
```
## 与 Node LiveKit 桥接服务的协议约定
硬件端连接:
```text
ws://<LK_BRIDGE_WS_HOST>:<LK_BRIDGE_WS_PORT>/ws/device?deviceId=<deviceId>
```
文本消息使用 JSON。
音频消息使用二进制帧:
```text
20 字节 header + PCM16 LE payload
```
当前固定音频格式:
```text
sampleRate: 16000
channels: 1
bits: 16
frame: 20ms 左右
```
如果 Node 下发音频格式不一致,硬件端会丢帧并打印 `format mismatch`
## 与主链路的关系
这次硬件端新增的是“工具执行能力”和“实时通话音频能力”,而不是在硬件端自己做联系人识别。
联系人识别/目标设备号选择由主链路完成:
- App/后台维护联系人别名与设备号映射。
- 主链路意图识别根据用户语音得到目标 `toId`
- 硬件端只接受 `toId` 并发起呼叫。
所以硬件端不关心“小红”“爸爸”“卧室设备”这些自然语言名称,它只关心最终的设备标识。
## 调试日志关键字
设备端常用日志关键字:
```text
voice.app.lkbridge
bridge ws connect
bridge ws: connected
incoming call ringing
device call requested
device accept requested
device reject requested
device_reject_ack
device hangup requested
call_start
first pcm queued
writer stats
first uplink pcm sent
uplink stats
direct audio playback stopped
```
Node 端常用日志关键字:
```text
[bridge.ws] device=<id> connected
[bridge.call] device invite created
[bridge] incoming_call
[bridge.ws] device=<id> text: { type: 'device_reject', ... }
[bridge.call] invite=<id> ... pending->rejected by=<deviceId>
[call.accept]
[bridge] joined room
[bridge.uplink] audio stats
[bridge.ws] audio stats
```
## 当前限制与后续优化点
### 1. Node 地址仍是硬编码
当前硬件端地址在 `voice_livekit_poll_msg.c` 顶部宏里:
```c
#define LK_BRIDGE_WS_HOST "39.108.110.91"
#define LK_BRIDGE_WS_PORT "3000"
```
后续如果要更适合测试/生产切换,可以改为:
- 配置文件读取。
- BLE/云端配置下发。
- 编译参数注入。
### 2. 默认目标仍有兜底值
```c
#define LK_BRIDGE_DEFAULT_CALL_TARGET "f17c0a1a322ed2ad"
```
正常语音发起通话时,目标应该由 MCP 入参 `toId` 提供;默认目标只是防御性兜底。
### 3. 来电拒接已接入,但拒接入口固定为三击
当前来电中支持:
- 双击接听。
- 三击拒接。
- 对端取消或超时后 Node 下发结束。
为避免来电拒接后继续打开二维码页,主按键里的三击打开信息/二维码页逻辑已经移除。后续如果还需要二维码入口,建议放到设置菜单、五击/六击/七击组合,或由语音/页面触发,而不要复用三击。
### 4. 音频参数仍需要按设备环境微调
当前播放/录音参数已经经过一轮调试,但不同硬件、外壳、麦克风距离、扬声器音量下仍可能需要调整:
```c
LK_BRIDGE_RECORD_ANALOG_GAIN
LK_BRIDGE_RECORD_DIGITAL_GAIN
LK_BRIDGE_PLAY_DIGITAL_GAIN
LK_BRIDGE_AUDIO_BUFFER_COUNT
LK_BRIDGE_AUDIO_BUFFER_SAMPLES
LK_BRIDGE_AUDIO_START_PREFILL
```
### 5. 与主链路音频资源互斥
通话开始时会停止唤醒/录音/播放相关共享音频路径:
```c
app_wakeup_stop();
lisa_audio_record_stop();
lisa_audio_play_stop();
```
通话结束后再恢复:
```c
app_wakeup_start();
voice_cloud_reconnect_async();
```
这是为了避免主链路 ASR/TTS 和通话链路同时占用 `audio0` 导致噪声、卡顿或录音失败。

182
README.MD Normal file
View File

@@ -0,0 +1,182 @@
# ARCS 语音解决方案
基于 ARCS SDK 的语音交互解决方案,支持多项目的语音解决方案定制功能。
## 项目概述
本项目是一个完整的语音助手解决方案,集成了语音唤醒、语音识别、云端交互、本地播放等功能。
### 主要特性
- 语音唤醒和识别
- 云端语音服务对接
- 本地音频播放
- WiFi/BLE 网络连接
## 环境搭建
参考SDK的开发文档
- [开发文档(在线)](https://docs2.listenai.com/arcs-sdk/v0.1.0/zh/html/get_started.html)
## 工程结构
### 顶层目录
```
voiceassistant/
├── apps/ # 应用方案工程集合
├── arcs-sdk/ # ARCS SDK 核心框架
├── modules/ # 项目级第三方模块
├── res/ # 资源文件(固件、配置、音频、图片等)
├── src/ # 语音交互核心代码
├── tools/ # 开发工具和脚本
├── yaml/ # 配置文件
└── build.sh # 工程构建脚本
```
### 详细目录说明
#### [apps/](apps/) - 应用程序
包含多个应用方案项目,详细说明请参考对应工程下的README.MD如有
#### [src/](src/) - 语音交互核心代码
包含语音助手的核心业务逻辑、中间件和工具库:
```
src/
├── category/ # 品类相关代码
│ ├── comm/ # 品类通用代码
│ ├── evb/ # EVB 品类实现
│ └── yuba/ # YUBA 品类实现
├── framework/ # 核心框架
│ └── voice_msg.* # 语音消息处理
├── middleware/ # 中间件层
├── server/ # 语音服务
├── shell/ # 调试命令行
│ └── cmd/ # Shell 命令实现
├── system/ # 系统服务
│ ├── platform.c # 平台抽象层
│ └── remote_logger.*# 远程日志
└── utils/ # 工具库
```
**核心模块说明:**
- **category**: 不同产品品类的特定实现,封装品类差异
- **comm**: 品类间通用代码和共享组件
- **framework**: 核心框架,提供语音消息传递机制
- **middleware**: 提供硬件抽象和系统服务,供应用层调用
- **server**: 语音交互核心逻辑,包括云端通信和唤醒处理
- **shell**: 提供调试命令行接口,用于开发调试
- **system**: 系统级服务,包括平台抽象和远程日志
- **utils**: 通用工具库,提供事件、配置、编解码等基础功能
## 快速开始
### 编译工程
基本编译命令,这里以/apps/evb-arcs项目为例,用户根据自己的实际项目路径选择:
```bash
./build.sh -S ./apps/arcs-evb -C
```
编译成功后,固件文件位于 [build/arcs-evb.bin](build/arcs-evb.bin)
### 固件烧录
#### FLASH 分区说明
参考对应app的资源分配置表说明文件
#### 烧录方式
##### 方式一: ROM 升级模式(空板烧录)
适用于首次烧录或 boot 程序损坏的情况。
**进入 ROM 升级模式:**
1. 将开发板的 `boot` 引脚接地
2. 复位开发板
3. 开发板进入 ROM 升级模式
**烧录 boot 程序:**
```bash
# 连接串口 UART0(GPIOA2_RX0/GPIOA3_TX0)
cskburn -C arcs -s /dev/ttyACM0 -b 3000000 0x0 ./res/res_pack_a/boot.bin
```
> **注意:** boot 引脚和烧录串口引脚复用,烧录前需将 boot 引脚悬空
##### 方式二: ADB 烧录(推荐)
适用于已有 boot 程序的开发板,支持快速烧录。
**步骤 1: 进入恢复模式**
```bash
adb shell recovery
```
**步骤 2: 烧录固件和资源**
参考对应app的资源分配置表说明文件,烧录所有分区数据.
**步骤 3: 重启设备**
```bash
adb shell reboot hard
```
## 常见问题
### 编译问题
**Q: 找不到工具链**
```
A: 检查环境变量是否正确设置:
echo $NUCLEI_TOOLCHAIN_PATH
echo $LISTENAI_TOOLS_PATH
```
**Q: 编译失败**
```
A: 尝试清理后重新编译:
./build.sh -C
```
### 烧录问题
**Q: ADB 连接不上设备**
```
A: 1. 检查 USB 连接
2. 运行 adb devices 查看设备
3. 尝试重启 adb: adb kill-server && adb start-server
```
**Q: 烧录后无法启动**
```
A: 1. 检查是否烧录了所有必要分区
2. 查看串口日志定位问题
3. 尝试重新烧录 boot.bin
```
## 版本信息
- **当前分支**: master
- **主分支**: master
## 参考文档
- [ARCS SDK 文档](arcs-sdk/README.MD)
- [ebus 模块文档](modules/ebus/readme.md)
- [lschat 模块文档](modules/lschat/README.MD)
## 许可证
本项目遵循相应的开源许可证,详见各模块的 LICENSE 文件。

110
SDK服务切换说明.md Normal file
View File

@@ -0,0 +1,110 @@
# SDK 服务切换说明
本文档说明 arcs-mini 固件如何在 Lingxin SDK 和原来的 39 服务之间切换。
## 配置位置
修改文件:
```text
apps/arcs-mini/prj.conf
```
核心开关:
```conf
CONFIG_ARCS_USE_LINGXIN_SDK=y
```
## 切到 Lingxin SDK
`apps/arcs-mini/prj.conf` 中保留:
```conf
CONFIG_ARCS_USE_LINGXIN_SDK=y
```
该宏会自动启用以下配置:
```conf
# CONFIG_VOICE_LINK_LSCHAT is not set
CONFIG_VOICE_LINK_LINGXIN=y
CONFIG_LS_CHAT=y
CONFIG_SERVER_VOICE_LSCHAT=y
CONFIG_LINGXIN_ENGINE_ENABLE=y
CONFIG_LINGXIN_ENGINE_PROFILE_CHAT_CORE=y
CONFIG_LINGXIN_USER_TRACK_STUB=y
```
Lingxin 的云端地址仍然使用:
```conf
CONFIG_LINGXIN_CLOUD_HOST="eagent.edu-aliyun.com"
CONFIG_LINGXIN_CLOUD_PORT=80
```
## 切回 39 服务
`apps/arcs-mini/prj.conf` 中的:
```conf
CONFIG_ARCS_USE_LINGXIN_SDK=y
```
改成:
```conf
# CONFIG_ARCS_USE_LINGXIN_SDK is not set
```
或者直接删除这一行。
这样会回到默认的 LSChat 链路,也就是原来的 39 服务。
## 重新打包
切换配置后需要重新打包:
```bash
docker run --rm -it --init -w $PWD -v $PWD:$PWD arcs-builder ./build.sh -S ./apps/arcs-mini -C
```
## 检查是否切换成功
打包完成后查看生成的配置:
```bash
rg -n "CONFIG_ARCS_USE_LINGXIN_SDK|CONFIG_VOICE_LINK_LSCHAT|CONFIG_VOICE_LINK_LINGXIN|CONFIG_LINGXIN_ENGINE_ENABLE" build/.config
```
### Lingxin SDK 状态
如果看到:
```conf
CONFIG_ARCS_USE_LINGXIN_SDK=y
# CONFIG_VOICE_LINK_LSCHAT is not set
CONFIG_VOICE_LINK_LINGXIN=y
CONFIG_LINGXIN_ENGINE_ENABLE=y
```
说明当前是 Lingxin SDK。
### 39 服务状态
如果看到:
```conf
# CONFIG_ARCS_USE_LINGXIN_SDK is not set
CONFIG_VOICE_LINK_LSCHAT=y
# CONFIG_VOICE_LINK_LINGXIN is not set
```
说明当前是 39 服务。
## 注意事项
- 这个宏只负责切换 SDK 链路。
- 39 服务的 websocket 地址如果在其他配置或代码里已经改过,不需要因为这个宏再改一遍。
- 每次切换后都要重新打包,并烧入新的 `arcs-mini.bin`
- 如果资源也有变化,需要同时烧入完整资源 bin。

25
apps-ui/CMakeLists.txt Normal file
View File

@@ -0,0 +1,25 @@
set(LISA_UI_LIB_NAME lisa_ui_app)
set(CMAKE_C_EXTENSIONS ON)
set(CONFIG_LOG_ENABLE 1)
add_definitions(-DCONFIG_LOG_ENABLE=${CONFIG_LOG_ENABLE})
set(CONFIG_LOG_LEVEL 4)
add_definitions(-DCONFIG_LOG_LEVEL=${CONFIG_LOG_LEVEL})
add_library(${LISA_UI_LIB_NAME} STATIC)
target_sources(
${LISA_UI_LIB_NAME}
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/lisa_ui.c
${CMAKE_CURRENT_SOURCE_DIR}/lisa_ui_nav_scr.c
)
target_include_directories(${LISA_UI_LIB_NAME} PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
add_subdirectory(portable)
add_subdirectory(apps)
add_subdirectory(assets)
add_subdirectory(lv-i18n)

17
apps-ui/Kconfig Normal file
View File

@@ -0,0 +1,17 @@
menu "Application UI"
config APPLICATION_UI
bool "application ui"
default n
if APPLICATION_UI
config ALARM_RING_AUTO_RETURN_MS
int "Alarm ring UI auto return timeout (ms)"
default 60000
help
Time before alarm ring page automatically returns to Home.
endif
endmenu

29
apps-ui/README.MD Normal file
View File

@@ -0,0 +1,29 @@
# 说明
### linux模拟器开发
1. 进入到sim目录,生成assets并编译运行
首次编译或修改图片资源后需要先生成assets
```bash
cd sim
# 生成assets C文件和头文件
python3 ../assets/assets.py ../assets/embed build/generated/assets
# 配置和编译
cmake -B build && cmake --build build
# 运行模拟器
./build/lisa_ui_app_sim
```
后续修改代码(非图片资源)可以直接编译运行:
```bash
cmake --build build && ./build/lisa_ui_app_sim
```
### arcs开发
按照正常的arcs工程编译即可

324
apps-ui/UI_MIGRATION.md Normal file
View File

@@ -0,0 +1,324 @@
# UI框架迁移指南
## 新UI框架架构总结
### 核心特点
1. **完全业务分离的MVP架构**
- **View层**纯UI渲染无业务逻辑`info_view.c`, `lisa_ui_llm_primary.c`
- **Presenter层**:业务逻辑和生命周期管理(`info_presenter.c`, `home_presenter.c`
- **Model层**:数据接口(预留,暂未实现)
2. **导航系统**
- 基于`lisa_ui_nav_scr`结构的页面管理
- 标准生命周期:`open → show → pause → resume → close`
- 通过ID进行页面导航
3. **组件继承体系**
```
lv_obj (LVGL基础)
├─ lisa_ui_llm_base (基础组件:任务栏+容器)
│ ├─ lisa_ui_llm_primary (主页面)
│ └─ lisa_ui_info_view (信息/二维码页面)
└─ lisa_ui_anim / lisa_ui_anim_ext (动画组件)
```
## 老UI分析
### 已有页面(`/apps/arcs-mini/groups/llm/launcher/pages/`
1. **page_primary.c** (主页面)
- emoji分阶段动画系统
- WiFi/电量状态栏
- 内容文本轮播
- 犯困模式定时器
2. **page_info.c** (信息页面)
- 二维码显示BLE/网络)
- 顶部/底部文本提示
- 自动返回定时器
## 已完成迁移
### ✅ 完整设置系统Settings System
#### 1. 设置主页面Setting Main Page
**文件**: `apps-ui/apps/llm/views/setting_view.c/h`
**功能**:
- 列表形式的设置菜单
- 支持动态添加设置项
- 点击回调导航到子页面
**API接口**:
```c
lv_obj_t *lisa_ui_setting_view_create(lv_obj_t *parent);
void lisa_ui_setting_view_item_add(lv_obj_t *obj, const char *label, const char *icon_symbol);
void lisa_ui_setting_view_set_click_cb(lv_obj_t *obj, lisa_ui_setting_item_click_cb_t cb, void *user_data);
void lisa_ui_setting_view_clear(lv_obj_t *obj);
```
#### 2. WiFi设置页面WiFi Settings
**文件**: `apps-ui/apps/llm/views/wifi_view.c/h`
**功能**:
- WiFi开关控制
- 我的WiFi列表已连接过
- 其他WiFi列表
- 网络信号强度显示
#### 3. 音量设置页面Volume Settings
**文件**: `apps-ui/apps/llm/views/volume_view.c/h`
**功能**:
- 音量滑块控制0-100%
- 麦克风增益滑块控制
- 实时数值显示
#### 4. 关于页面About Page
**文件**: `apps-ui/apps/llm/views/about_view.c/h`
**功能**:
- 系统信息展示
- 版本号、编译日期
- 版权信息
#### 5. Presenter层
**文件**: `apps-ui/apps/llm/presenters/setting_presenter.c`
**功能**:
- 统一管理所有设置页面的导航
- setting_nav_scr - 主设置页面
- wifi_nav_scr - WiFi设置
- volume_nav_scr - 音量设置
- about_nav_scr - 关于页面
#### 6. 导航注册
- 在`lisa_ui_app.c`中注册所有设置页面
- 导航ID定义
- `LISA_UI_NAV_SCR_ID_SETTING`
- `LISA_UI_NAV_SCR_ID_SETTING_WIFI`
- `LISA_UI_NAV_SCR_ID_SETTING_VOLUME`
- `LISA_UI_NAV_SCR_ID_SETTING_ABOUT`
#### 7. 资源迁移
已拷贝所有图标和emoji资源
- **Emoji动画**: `/assets/emoji/` - angry, blink, heart, sad, sleepy, wakeup等
- **图标资源**: `/assets/lisa_ui_assets.c/h` - WiFi、电池、电量图标
### ✅ 信息/二维码页面Info Page
#### 1. View组件
**文件**: `apps-ui/apps/llm/views/info_view.c/h`
**功能**:
- 继承自`lisa_ui_llm_base`
- 包含顶部提示文本、QR图片、底部说明文本
- 垂直flex布局居中对齐
**API接口**:
```c
lv_obj_t *lisa_ui_info_view_create(lv_obj_t *parent);
void lisa_ui_info_view_set_top_text(lv_obj_t *obj, const char *text);
void lisa_ui_info_view_set_bottom_text(lv_obj_t *obj, const char *text);
void lisa_ui_info_view_set_qr_image(lv_obj_t *obj, const void *src);
lv_obj_t *lisa_ui_info_view_get_qr_image(lv_obj_t *obj);
```
#### 2. Presenter
**文件**: `apps-ui/apps/llm/presenters/info_presenter.c`
**功能**:
- 管理info页面生命周期
- 10秒自动返回主页面定时器
- 预留数据接口对接点
#### 3. 导航注册
- 在`lisa_ui_app.c`中注册`info_nav_scr`
- ID: `LISA_UI_NAV_SCR_ID_WX_QRCODE`
- 支持从主页面设置按钮跳转
#### 4. 编译配置
- 已更新`views/CMakeLists.txt`
- 已更新`presenters/CMakeLists.txt`
## 页面导航流程
```
主页面 (Home)
├─ 点击设置图标 → 设置主页面 (Setting)
│ ├─ WiFi → WiFi设置页面
│ ├─ Volume → 音量设置页面
│ └─ About → 关于页面
└─ (预留)其他导航入口 → 信息/二维码页面 (Info)
```
## 编译配置完成
### CMakeLists.txt更新
1. **views/CMakeLists.txt** - 添加所有view文件
```cmake
info_view.c
setting_view.c
wifi_view.c
volume_view.c
about_view.c
```
2. **presenters/CMakeLists.txt** - 添加presenter
```cmake
setting_presenter.c
```
3. **lisa_ui_app.c** - 注册所有导航
```c
extern const struct lisa_ui_nav_scr setting_nav_scr;
extern const struct lisa_ui_nav_scr wifi_nav_scr;
extern const struct lisa_ui_nav_scr volume_nav_scr;
extern const struct lisa_ui_nav_scr about_nav_scr;
```
## 待迁移内容
### 🔲 主页面增强Home Page Enhancement
#### 需要实现的功能
1. **Emoji动画系统**
- 分阶段动画enter → loop → exit
- 动画配置数组每种emoji的帧数、时长
- 状态管理当前emoji、阶段、循环计数
- 适配`lisa_ui_anim_ext`组件
2. **状态栏动态更新**
- WiFi连接状态
- 电量显示(老版有,新版已移除)
- 状态文本("我在听..."、"思考中..."等)
3. **内容文本轮播**
- 云端待机提示词轮换
- 定时器管理
- 文本队列
4. **特殊模式**
- 犯困模式30秒后切换到sleepy emoji
- 充电状态检测
### 🔲 设置页面Setting Page - 可选)
如需要设置页面,需创建:
- `setting_view.c/h`
- `setting_presenter.c`
- 添加`LISA_UI_NAV_SCR_ID_SETTING`
- 注册到`lisa_ui_app.c`
### 🔲 动画资源迁移
需要将老UI的emoji图片资源迁移到新框架
```
老路径: apps/arcs-mini/groups/llm/launcher/pages/private/anim_images.h
新路径: apps-ui/assets/ (需创建)
```
包含的emoji类型
- BLINK (眨眼)
- LOVE (爱心)
- SAD (难过)
- ANGRY (生气)
- HAPPY (开心)
- CUTE (可爱)
- SLEEPY (犯困)
- WAIT (等待)
- BATTERY_CHANGE (充电动画)
## 编译和测试
### Linux模拟器
```bash
cd apps-ui/sim
cmake -B build && cmake --build build && ./build/lisa_ui_app_sim
```
### 操作演示
1. 启动后显示主页面home
2. 点击左上角设置图标 → 跳转到Info页面
3. Info页面显示10秒后自动返回主页面
## 数据接口对接(待实现)
### Model层设计建议
```c
// apps-ui/apps/llm/models/ui_model.h
typedef struct {
bool wifi_connected;
uint8_t battery_level;
char status_text[32];
lisa_ui_emoji_type_e current_emoji;
char content_text[256];
const void *qr_image_src;
} ui_model_data_t;
// 数据更新回调
typedef void (*ui_model_update_cb_t)(const ui_model_data_t *data, void *user_data);
```
### Presenter调用Model
```c
// 在presenter中监听数据变化
ui_model_register_callback(on_data_update, presenter_data);
// 回调中更新view
void on_data_update(const ui_model_data_t *data, void *user_data) {
// 更新view显示
lisa_ui_llm_primary_set_status_text(view, data->status_text);
lisa_ui_llm_primary_set_emoji(view, data->current_emoji);
}
```
## 下一步工作优先级
1. **高优先级**实现emoji动画系统适配`lisa_ui_anim_ext`
2. **中优先级**迁移emoji图片资源
3. **中优先级**:实现主页面状态栏动态更新
4. **低优先级**:内容文本轮播功能
5. **低优先级**:犯困模式和特殊定时器
## 注意事项
### IDE Lint错误
当前IDE会报`lv_conf.h`找不到的错误,这是正常的:
- `lv_conf.h`在`portable/`目录下
- CMake编译时会正确处理包含路径
- 不影响实际编译和运行
### 代码风格
- 使用英文注释和日志
- View层不包含业务逻辑
- Presenter负责生命周期和业务
- 保持与现有代码风格一致
## 迁移完成标准
- [x] 所有页面View组件实现
- [x] Info/QRCode页面
- [x] Setting主页面
- [x] WiFi设置页面
- [x] Volume设置页面
- [x] About页面
- [x] 所有Presenter实现并注册
- [x] info_presenter
- [x] setting_presenter含4个子页面
- [x] 资源完整拷贝
- [x] Emoji动画资源angry, blink, heart等
- [x] 图标资源WiFi, 电池等)
- [x] 编译配置完成
- [x] CMakeLists.txt更新
- [x] 导航系统注册
- [ ] Emoji动画系统完整迁移待实现
- [ ] 模拟器测试通过
- [ ] 数据接口对接完成(业务层对接后)
- [ ] 页面导航流畅无bug
## 参考文档
- 新框架README: `apps-ui/README.MD`
- 老UI实现: `apps/arcs-mini/groups/llm/launcher/pages/`
- LVGL文档: https://docs.lvgl.io/

View File

@@ -0,0 +1 @@
add_subdirectory(llm)

View File

@@ -0,0 +1,25 @@
target_sources(
${LISA_UI_LIB_NAME}
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/lisa_ui_app.c
)
target_include_directories(
${LISA_UI_LIB_NAME}
PRIVATE
./
)
# Only link lvgl library in simulator environment
if(CONFIG_LVGL_ENV_SIMULATOR)
target_link_libraries(
${LISA_UI_LIB_NAME}
PUBLIC
lvgl
)
endif()
add_subdirectory(models)
add_subdirectory(presenters)
add_subdirectory(views)
add_subdirectory(utils)

View File

@@ -0,0 +1,66 @@
#include "lisa_ui.h"
#include "lisa_ui_log.h"
#include "lisa_ui_nav_scr.h"
#include "model_camera.h"
#include "model_common.h"
#include "model_voice.h"
#include "model_wifi.h"
#include "model_qrcode.h"
#include "model_alarm.h"
#include "model_battery.h"
extern void alarm_navigation_init(void);
extern const struct lisa_ui_nav_scr home_nav_scr;
extern const struct lisa_ui_nav_scr info_nav_scr;
extern const struct lisa_ui_nav_scr setting_nav_scr;
extern const struct lisa_ui_nav_scr setting_common_nav_scr;
extern const struct lisa_ui_nav_scr setting_wifi_nav_scr;
extern const struct lisa_ui_nav_scr setting_wakeup_nav_scr;
extern const struct lisa_ui_nav_scr setting_clock_nav_scr;
extern const struct lisa_ui_nav_scr alarm_success_nav_scr;
extern const struct lisa_ui_nav_scr alarm_ring_nav_scr;
extern const struct lisa_ui_nav_scr quota_qrcode_nav_scr;
#ifdef CONFIG_OTA
extern const struct lisa_ui_nav_scr ota_nav_scr;
#endif
static int lisa_ui_model_init(void)
{
model_wifi_init();
model_voice_init();
model_common_init();
model_alarm_init();
// model_camera_init();
model_qrcode_init();
model_battery_init();
return 0;
}
int lisa_ui_app_init(void)
{
lisa_ui_model_init();
/* 初始化闹钟导航处理器 */
alarm_navigation_init();
lisa_ui_nav_scr_add(&home_nav_scr);
lisa_ui_nav_scr_add(&info_nav_scr);
lisa_ui_nav_scr_add(&setting_nav_scr);
lisa_ui_nav_scr_add(&setting_common_nav_scr);
lisa_ui_nav_scr_add(&setting_wifi_nav_scr);
lisa_ui_nav_scr_add(&setting_wakeup_nav_scr);
lisa_ui_nav_scr_add(&setting_clock_nav_scr);
lisa_ui_nav_scr_add(&alarm_success_nav_scr);
lisa_ui_nav_scr_add(&alarm_ring_nav_scr);
lisa_ui_nav_scr_add(&quota_qrcode_nav_scr);
#ifdef CONFIG_OTA
lisa_ui_nav_scr_add(&ota_nav_scr);
#endif
/* fisrt src will be open and show now */
lisa_ui_nav_scr_default_set(&home_nav_scr);
return 0;
}

View File

@@ -0,0 +1,22 @@
#ifndef __NAV_SCR_IDS_H__
#define __NAV_SCR_IDS_H__
enum {
LISA_UI_NAV_SCR_ID_HOME = 0,
LISA_UI_NAV_SCR_ID_INFO, // 信息/二维码页面
LISA_UI_NAV_SCR_ID_SETTING,
LISA_UI_NAV_SCR_ID_SETTING_COMMON, // 基础设置(音量、亮度)
LISA_UI_NAV_SCR_ID_SETTING_WIFI, // 网络设置
LISA_UI_NAV_SCR_ID_SETTING_WAKEUP, // 唤醒交互
LISA_UI_NAV_SCR_ID_SETTING_CLOCK, // 闹钟设置(旧版)
LISA_UI_NAV_SCR_ID_ALARM_SUCCESS, // 闹钟设置成功页面
LISA_UI_NAV_SCR_ID_ALARM_RING, // 闹钟响铃页面
LISA_UI_NAV_SCR_ID_ALARM_CLOCK, // 闹钟设置页面MVP架构
LISA_UI_NAV_SCR_ID_QUOTA_QRCODE, // 额度不足二维码页面
#ifdef CONFIG_OTA
LISA_UI_NAV_SCR_ID_OTA, // OTA检查页面
#endif
};
#endif

View File

@@ -0,0 +1,21 @@
target_sources(
${LISA_UI_LIB_NAME}
PRIVATE
model_voice.c
model_common.c
model_qrcode.c
model_wifi.c
model_battery.c
model_camera.c
model_alarm.c
)
if(CONFIG_OTA)
target_sources(${LISA_UI_LIB_NAME} PRIVATE model_ota.c)
endif()
target_include_directories(
${LISA_UI_LIB_NAME}
PRIVATE
./
)

View File

@@ -0,0 +1,332 @@
/**
* @file model_alarm.c
* @brief Alarm data model implementation
*/
#include "model_alarm.h"
#include <string.h>
#include <stdio.h>
#include <stdbool.h>
#include <time.h>
#include <sys/time.h>
#include "lisa_ui.h"
#include "lisa_ui_invoke.h"
#include "lisa_ui_nav_scr_ids.h"
#ifdef LISA_UI_PLATFORM_ARCS
#include "voice_msg.h"
#include "service_alarm.h"
#include "app_datas.h"
#define ALARM_TRIGGER_NAV_DELAY_MS 200
#endif
#define TAG "model_alarm"
/**
* @brief Alarm model context
*/
typedef struct {
alarm_data_t data;
char event_text[128];
bool initialized;
bool is_ringing; /* 闹钟是否正在响铃 */
uint32_t alarm_count;
} alarm_model_ctx_t;
static alarm_model_ctx_t g_alarm_ctx = {0};
#ifdef LISA_UI_PLATFORM_ARCS
static void alarm_trigger_nav_ui_worker(void *arg, uint32_t len)
{
if (!arg || len < sizeof(struct service_alarm)) {
LISA_UI_LOGE("Model: invalid alarm trigger payload");
return;
}
const struct service_alarm *alarm = (const struct service_alarm *)arg;
LISA_UI_LOGI("Model: Alarm triggered - timestamp: %llu", alarm->timestamp);
g_alarm_ctx.data.timestamp = alarm->timestamp;
g_alarm_ctx.event_text[0] = '\0';
g_alarm_ctx.initialized = true;
g_alarm_ctx.is_ringing = true;
int top_id = lisa_ui_nav_scr_get_top_id();
if (top_id == LISA_UI_NAV_SCR_ID_ALARM_RING) {
LISA_UI_LOGI("Already on alarm ring page, skipping navigation");
return;
}
if (top_id != LISA_UI_NAV_SCR_ID_HOME) {
LISA_UI_LOGI("Model: Navigating to home before alarm ring, top_id=%d", top_id);
lisa_ui_nav_scr_nav_to(LISA_UI_NAV_SCR_ID_HOME);
}
LISA_UI_LOGI("Model: Navigating to alarm ring page");
lisa_ui_nav_scr_nav_to(LISA_UI_NAV_SCR_ID_ALARM_RING);
}
static void on_alarm_create_event(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
(void)unused;
(void)msg_id;
(void)user_data;
struct service_alarm *alarm = (struct service_alarm *)data;
LISA_UI_LOGI("Model: Alarm created - timestamp: %llu", alarm->timestamp);
g_alarm_ctx.alarm_count++;
LISA_UI_INVOKE_UI_ARG_PTR(alarm, sizeof(struct service_alarm), {
g_alarm_ctx.data.timestamp = _invoke_alarm->timestamp;
g_alarm_ctx.event_text[0] = '\0';
g_alarm_ctx.initialized = true;
if (lisa_ui_nav_scr_get_top_id() == LISA_UI_NAV_SCR_ID_ALARM_SUCCESS) {
LISA_UI_LOGI("Already on alarm success page, skipping navigation");
return;
}
lisa_ui_nav_scr_nav_to(LISA_UI_NAV_SCR_ID_ALARM_SUCCESS);
});
}
static void on_alarm_delete_event(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;
if (g_alarm_ctx.alarm_count > 0) {
g_alarm_ctx.alarm_count--;
}
}
static void on_alarm_query_event(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
(void)unused;
(void)msg_id;
(void)user_data;
if (!data || len < sizeof(uint32_t)) {
return;
}
g_alarm_ctx.alarm_count = *(uint32_t *)data;
}
static void on_alarm_trigger_event(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
(void)unused;
(void)msg_id;
(void)user_data;
struct service_alarm *alarm = (struct service_alarm *)data;
if (!alarm) {
LISA_UI_LOGE("Model: alarm trigger data is NULL");
return;
}
#ifdef LISA_UI_PLATFORM_ARCS
struct app_datas *app_datas = get_app_datas();
if (app_datas) {
LISA_UI_LOGI("Model: Before setting - can_wakeup=%d, voice_work_mode=%d",
app_datas->can_wakeup, app_datas->voice_work_mode);
app_datas->can_wakeup = true;
app_datas->voice_work_mode |= VOICE_WORK_MODE_VOICE_WAKEUP;
LISA_UI_LOGI("Model: After setting - can_wakeup=%d, voice_work_mode=%d",
app_datas->can_wakeup, app_datas->voice_work_mode);
} else {
LISA_UI_LOGE("Model: Failed to get app_datas!");
}
/* Alarm page switch can race with MCP session UI transition; stop MCP first. */
voice_msg_pub(VOICE_MSG_CLOUD_MCP_CHAT_EXIT, NULL, 0);
if (lisa_ui_invoke_ui_delayed(alarm_trigger_nav_ui_worker, alarm, sizeof(struct service_alarm),
ALARM_TRIGGER_NAV_DELAY_MS) != 0) {
LISA_UI_LOGE("Model: failed to schedule alarm ring navigation");
}
#endif
}
#endif
int model_alarm_init(void)
{
if (g_alarm_ctx.initialized) {
return 0;
}
LISA_UI_LOGI("Initializing alarm model");
#ifdef LISA_UI_PLATFORM_ARCS
voice_msg_sub(VOICE_MSG_ALARM_CREATE, on_alarm_create_event, NULL);
voice_msg_sub(VOICE_MSG_ALARM_DELETE, on_alarm_delete_event, NULL);
voice_msg_sub(VOICE_MSG_ALARM_QUERY, on_alarm_query_event, NULL);
voice_msg_sub(VOICE_MSG_ALARM_TRIGGER, on_alarm_trigger_event, NULL);
uint32_t cnt = 0;
struct service_alarm *service_alarms = service_alarm_get_all(&cnt);
if (service_alarms) {
lisa_mem_free(service_alarms);
}
g_alarm_ctx.alarm_count = cnt;
#endif
g_alarm_ctx.initialized = true;
LISA_UI_LOGI("Alarm model initialized");
return 0;
}
void model_alarm_set_data(const alarm_data_t *data)
{
if (!data) {
LISA_UI_LOGE("Invalid alarm data");
return;
}
g_alarm_ctx.data = *data;
LISA_UI_LOGI("Alarm data set - timestamp: %llu", (unsigned long long)data->timestamp);
}
int model_alarm_get_data(alarm_data_t *data)
{
if (!data) {
LISA_UI_LOGE("Invalid output parameter");
return -1;
}
if (!g_alarm_ctx.initialized) {
LISA_UI_LOGW("Alarm model not initialized");
return -1;
}
*data = g_alarm_ctx.data;
return 0;
}
const char *model_alarm_get_event_text(void)
{
return g_alarm_ctx.event_text;
}
int model_alarm_get_time_info(uint64_t timestamp, alarm_time_info_t *info)
{
if (!info) {
LISA_UI_LOGE("Invalid parameter");
return -1;
}
struct timeval tv;
time_t current_time = time(NULL);
if (gettimeofday(&tv, NULL) == 0) {
current_time = tv.tv_sec;
}
time_t alarm_time = (time_t)timestamp;
struct tm alarm_tm_struct;
struct tm *alarm_tm = localtime_r(&alarm_time, &alarm_tm_struct);
if (!alarm_tm) {
LISA_UI_LOGE("Failed to convert timestamp");
return -1;
}
info->year = alarm_tm->tm_year + 1970;
info->month = alarm_tm->tm_mon + 1;
info->day = alarm_tm->tm_mday;
info->hour = alarm_tm->tm_hour;
info->minute = alarm_tm->tm_min;
int64_t local_ts = (int64_t)timestamp + (int64_t)8 * 3600;
int64_t days_since_epoch = local_ts / 86400;
info->weekday = (int)((4 + (days_since_epoch % 7) + 7) % 7);
int64_t alarm_local_days = ((int64_t)timestamp + (int64_t)8 * 3600) / 86400;
int64_t current_local_days = ((int64_t)current_time + (int64_t)8 * 3600) / 86400;
int64_t day_diff = alarm_local_days - current_local_days;
info->is_today = (day_diff == 0);
info->is_tomorrow = (day_diff == 1);
return 0;
}
int model_alarm_delete_by_timestamp(uint64_t timestamp)
{
#ifdef LISA_UI_PLATFORM_ARCS
int r = ls_alarm_delete_by_timestamp(timestamp);
return r;
#endif
return 0;
}
void model_alarm_free(void *alarms)
{
if (alarms) {
lisa_ui_free(alarms);
}
}
uint32_t model_alarm_count_get(void)
{
return g_alarm_ctx.alarm_count;
}
alarm_data_t *model_alarm_get_all(uint32_t *cnt)
{
if (!cnt) {
LISA_UI_LOGE("Invalid parameter: cnt is NULL");
return NULL;
}
#ifdef LISA_UI_PLATFORM_ARCS
if (!service_alarm_init_done()) {
*cnt = 0;
return NULL;
}
uint32_t alarm_count = 0;
struct service_alarm *service_alarms = service_alarm_get_all(&alarm_count);
*cnt = alarm_count;
if (alarm_count == 0 || service_alarms == NULL) {
return NULL;
}
// Allocate memory for alarm_data_t array
alarm_data_t *alarms = lisa_ui_malloc(sizeof(alarm_data_t) * alarm_count);
if (alarms == NULL) {
LISA_UI_LOGE("Failed to allocate memory for alarms");
lisa_mem_free(service_alarms);
*cnt = 0;
return NULL;
}
// Convert service_alarm to alarm_data_t
for (uint32_t i = 0; i < alarm_count; i++) {
alarms[i].timestamp = service_alarms[i].timestamp;
LISA_UI_LOGI("alarm %d, timestamp:%llu", i, alarms[i].timestamp);
}
// Free the service_alarm array
lisa_mem_free(service_alarms);
LISA_UI_LOGI("Got %u alarms", alarm_count);
return alarms;
#else
*cnt = 0;
return NULL;
#endif
}

View File

@@ -0,0 +1,46 @@
/**
* @file model_alarm.h
* @brief Alarm data model header
*/
#ifndef __MODEL_ALARM_H__
#define __MODEL_ALARM_H__
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
typedef struct {
uint64_t timestamp;
} alarm_data_t;
typedef struct {
int year;
int month;
int day;
int hour;
int minute;
int weekday;
bool is_today;
bool is_tomorrow;
} alarm_time_info_t;
void model_alarm_set_data(const alarm_data_t *data);
int model_alarm_get_data(alarm_data_t *data);
int model_alarm_get_time_info(uint64_t timestamp, alarm_time_info_t *info);
const char *model_alarm_get_event_text(void);
int model_alarm_init(void);
int model_alarm_delete_by_timestamp(uint64_t timestamp);
alarm_data_t *model_alarm_get_all(uint32_t *cnt);
void model_alarm_free(void *alarms);
uint32_t model_alarm_count_get(void);
#ifdef __cplusplus
}
#endif
#endif /* __MODEL_ALARM_H__ */

View File

@@ -0,0 +1,132 @@
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#define TAG "model_battery"
#include "lisa_ui_invoke.h"
#include "lisa_ui_log.h"
#include "model_battery.h"
#ifdef LISA_UI_PLATFORM_ARCS
#include "voice_msg.h"
#endif
struct model_battery_context {
uint32_t inited: 1;
model_battery_info_t info;
model_battery_update_cb_t on_battery_status_update;
void *arg;
};
static struct model_battery_context model_battery_ctx = {
.inited = 0,
.info = {
.level = 0,
.status = MODEL_BATTERY_STATUS_UNKNOWN,
},
.on_battery_status_update = NULL,
.arg = NULL,
};
#ifdef LISA_UI_PLATFORM_ARCS
static model_battery_status_t convert_battery_status(voice_msg_battery_status_t status)
{
switch (status) {
case VOICE_MSG_BATTERY_STATUS_NO_BATTERY:
return MODEL_BATTERY_STATUS_NO_BATTERY;
case VOICE_MSG_BATTERY_STATUS_NOT_CONNECT:
return MODEL_BATTERY_STATUS_NOT_CONNECT;
case VOICE_MSG_BATTERY_STATUS_CHARGING:
return MODEL_BATTERY_STATUS_CHARGING;
case VOICE_MSG_BATTERY_STATUS_CHARGE_DONE:
return MODEL_BATTERY_STATUS_CHARGE_DONE;
case VOICE_MSG_BATTERY_STATUS_UNKNOWN:
default:
return MODEL_BATTERY_STATUS_UNKNOWN;
}
}
static void battery_update_msg_handle(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 < sizeof(voice_msg_battery_info_t)) {
return;
}
voice_msg_battery_info_t msg = *(voice_msg_battery_info_t *)data;
LISA_UI_INVOKE_UI_ARG_BASE(msg, {
model_battery_ctx.info.level = _invoke_msg.level;
model_battery_ctx.info.status = convert_battery_status((voice_msg_battery_status_t)_invoke_msg.status);
if (model_battery_ctx.on_battery_status_update) {
model_battery_ctx.on_battery_status_update(&model_battery_ctx.info, model_battery_ctx.arg);
}
});
}
#endif /* LISA_UI_PLATFORM_ARCS */
int model_battery_init(void)
{
if (model_battery_ctx.inited) {
return 0;
}
#ifdef LISA_UI_PLATFORM_ARCS
voice_msg_sub(VOICE_MSG_POWER_BATTERY_UPDATE, battery_update_msg_handle, NULL);
#endif
model_battery_ctx.inited = 1;
return 0;
}
int model_battery_deinit(void)
{
if (!model_battery_ctx.inited) {
return 0;
}
#ifdef LISA_UI_PLATFORM_ARCS
voice_msg_unsub(VOICE_MSG_POWER_BATTERY_UPDATE, battery_update_msg_handle);
#endif
model_battery_ctx.inited = 0;
model_battery_ctx.on_battery_status_update = NULL;
model_battery_ctx.arg = NULL;
return 0;
}
int model_battery_get_info(model_battery_info_t *info)
{
if (info == NULL) {
return -1;
}
*info = model_battery_ctx.info;
return 0;
}
int model_battery_cb_register(model_battery_update_cb_t cb, void *arg)
{
model_battery_ctx.on_battery_status_update = cb;
model_battery_ctx.arg = arg;
return 0;
}
int model_battery_cb_unregister(model_battery_update_cb_t cb)
{
(void)cb;
model_battery_ctx.on_battery_status_update = NULL;
model_battery_ctx.arg = NULL;
return 0;
}

View File

@@ -0,0 +1,41 @@
/**
* @file model_battery.h
* @brief Battery data model interface
*/
#ifndef __MODEL_BATTERY_H__
#define __MODEL_BATTERY_H__
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
typedef enum {
MODEL_BATTERY_STATUS_NO_BATTERY = 0,
MODEL_BATTERY_STATUS_NOT_CONNECT,
MODEL_BATTERY_STATUS_CHARGING,
MODEL_BATTERY_STATUS_CHARGE_DONE,
MODEL_BATTERY_STATUS_UNKNOWN,
} model_battery_status_t;
typedef struct {
uint8_t level; /* 0-100 */
model_battery_status_t status;
} model_battery_info_t;
typedef void (*model_battery_update_cb_t)(const model_battery_info_t *info, void *arg);
int model_battery_init(void);
int model_battery_deinit(void);
int model_battery_get_info(model_battery_info_t *info);
int model_battery_cb_register(model_battery_update_cb_t cb, void *arg);
int model_battery_cb_unregister(model_battery_update_cb_t cb);
#ifdef __cplusplus
}
#endif
#endif /* __MODEL_BATTERY_H__ */

View File

@@ -0,0 +1,163 @@
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#define TAG "model_camera"
#include "lisa_ui_invoke.h"
#include "lisa_ui_log.h"
#ifdef LISA_UI_PLATFORM_ARCS
#include "service_camera.h"
#include "voice_msg.h"
#endif
struct model_camera_context {
uint32_t inited: 1;
uint32_t running: 1;
};
static struct model_camera_context model_camera_ctx = {
.inited = 0,
.running = 0,
};
#ifdef LISA_UI_PLATFORM_ARCS
static void bn_camera_hw_init(void *data, uint32_t len, struct voice_invoke_rsp *rsp)
{
int r = service_camera_init();
rsp->err = r;
}
#endif
int model_camera_init(void)
{
if (model_camera_ctx.inited) {
LISA_UI_LOGW("Camera model already initialized");
return 0;
}
#ifdef LISA_UI_PLATFORM_ARCS
struct voice_invoke_rsp rsp = {
.err = -1,
};
int r = voice_invoke_sync(bn_camera_hw_init, NULL, 0, &rsp, 1000);
if (r) {
LISA_UI_LOGE("bn_camera_hw_init sync invoke failed, r:%d", r);
return r;
} else {
if (rsp.err) {
LISA_UI_LOGE("bn_camera_hw_init sync invoke resp err, r:%d", rsp.err);
return rsp.err;
}
}
#else
LISA_UI_LOGW("Camera not supported on this platform");
return -5;
#endif
model_camera_ctx.inited = 1;
LISA_UI_LOGD("Camera model initialized");
return 0;
}
#ifdef LISA_UI_PLATFORM_ARCS
struct service_camera_rsp {
struct voice_invoke_rsp base;
uint8_t *cap_buf;
uint32_t buf_len;
};
static void bn_camera_capture(void *data, uint32_t data_len, struct voice_invoke_rsp *rsp)
{
struct service_camera_rsp *camera_rsp = (struct service_camera_rsp *)rsp;
if (camera_rsp->cap_buf == NULL) {
LISA_UI_LOGE("cap buf is null");
camera_rsp->base.err = -1;
return;
}
int ret = service_camera_capture(camera_rsp->cap_buf, camera_rsp->buf_len);
camera_rsp->base.err = ret;
}
#endif
int model_camera_capture(uint8_t *in, uint32_t len)
{
#ifdef LISA_UI_PLATFORM_ARCS
if (!model_camera_ctx.inited) {
LISA_UI_LOGE("Camera model not initialized");
return -1;
}
if (in == NULL || len == 0) {
LISA_UI_LOGE("Invalid capture buffer");
return -2;
}
struct service_camera_rsp rsp = {
.base = {
.err = -1,
},
.cap_buf = in,
.buf_len = len,
};
int r = voice_invoke_sync(bn_camera_capture, NULL, 0, (struct voice_invoke_rsp *)&rsp, 1000);
if (r) {
LISA_UI_LOGE("camera cap invoke sync failed, r: %d", r);
return r;
} else {
if (rsp.base.err) {
LISA_UI_LOGE("camera cap invoke sync rsp err, r: %d", rsp.base.err);
return rsp.base.err;
}
}
#else
LISA_UI_LOGW("Camera not supported on this platform");
return -5;
#endif
return 0;
}
int model_camera_get_framesize(uint16_t *width, uint16_t *height)
{
if (!width || !height) {
LISA_UI_LOGE("Invalid framesize output parameters");
return -2;
}
*width = 0;
*height = 0;
#ifdef LISA_UI_PLATFORM_ARCS
if (!model_camera_ctx.inited) {
LISA_UI_LOGE("Camera model not initialized");
return -1;
}
int r = service_camera_get_framesize(width, height);
if (r != 0) {
*width = 0;
*height = 0;
}
return r;
#else
return 0;
#endif
}
bool model_camera_is_running(void)
{
return model_camera_ctx.running;
}
bool model_camera_is_inited(void)
{
return model_camera_ctx.inited;
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright (c) 2025, LISTENAI
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef __MODEL_CAMERA_H__
#define __MODEL_CAMERA_H__
#include <stdint.h>
#include <stdbool.h>
int model_camera_init(void);
int model_camera_capture(uint8_t *in, uint32_t len);
int model_camera_get_framesize(uint16_t *width, uint16_t *height);
bool model_camera_is_running(void);
bool model_camera_is_inited(void);
#endif // __MODEL_CAMERA_H__

View File

@@ -0,0 +1,109 @@
#include "model_common.h"
#include "lisa_ui.h"
#ifdef LISA_UI_PLATFORM_ARCS
#include "lisa_ui_invoke.h"
#include "service_brightness.h"
#include "service_volume.h"
#endif
#define TAG "model_common"
struct model_common_context {
uint8_t volume;
uint8_t brightness;
uint8_t inited;
};
static struct model_common_context model_common_ctx = {
.volume = 70,
.brightness = 70,
.inited = 0,
};
#ifdef LISA_UI_PLATFORM_ARCS
static void model_volume_set_invoke_bn(void *arg, uint32_t arg_len)
{
uint8_t volume = *(uint8_t *)arg;
service_volume_set(volume);
}
static void model_brightness_set_invoke_bn(void *arg, uint32_t arg_len)
{
uint8_t brightness = *(uint8_t *)arg;
service_brightness_set(brightness);
}
#endif
int model_common_init(void)
{
if (model_common_ctx.inited) {
return 0;
}
#ifdef LISA_UI_PLATFORM_ARCS
model_common_ctx.brightness = service_brightness_get();
model_common_ctx.volume = service_volume_get();
model_common_ctx.inited = 1;
#endif
LISA_UI_LOGD("Common model initialized");
return 0;
}
int model_common_sync_from_system(void)
{
#ifdef LISA_UI_PLATFORM_ARCS
model_common_ctx.volume = service_volume_get();
model_common_ctx.brightness = service_brightness_get();
return 0;
#else
return -1;
#endif
}
uint8_t model_common_volume_get(void)
{
return model_common_ctx.volume;
}
int model_common_volume_set(uint8_t volume)
{
if (volume > 100) {
volume = 100;
}
model_common_ctx.volume = volume;
#ifdef LISA_UI_PLATFORM_ARCS
LISA_UI_INVOKE_BN(model_volume_set_invoke_bn, &volume, sizeof(volume));
#endif
LISA_UI_LOGD("Volume set to: %d", volume);
return 0;
}
uint8_t model_common_brightness_get(void)
{
return model_common_ctx.brightness;
}
int model_common_brightness_set(uint8_t brightness)
{
if (brightness > 100) {
LISA_UI_LOGE("Invalid brightness: %d", brightness);
return -1;
}
model_common_ctx.brightness = brightness;
#ifdef LISA_UI_PLATFORM_ARCS
LISA_UI_INVOKE_BN(model_brightness_set_invoke_bn, &brightness, sizeof(brightness));
#endif
LISA_UI_LOGD("Brightness set to: %d", brightness);
return 0;
}

View File

@@ -0,0 +1,44 @@
#ifndef __MODEL_COMMON_H__
#define __MODEL_COMMON_H__
#include <stdint.h>
/**
* @brief Get current volume level
* @return Volume level (0-100)
*/
uint8_t model_common_volume_get(void);
/**
* @brief Set volume level
* @param volume Volume level (0-100)
* @return 0 on success, -1 on error
*/
int model_common_volume_set(uint8_t volume);
/**
* @brief Get current brightness level
* @return Brightness level (0-100)
*/
uint8_t model_common_brightness_get(void);
/**
* @brief Set brightness level
* @param brightness Brightness level (0-100)
* @return 0 on success, -1 on error
*/
int model_common_brightness_set(uint8_t brightness);
/**
* @brief Initialize common model
* @return 0 on success, -1 on error
*/
int model_common_init(void);
/**
* @brief Synchronize current state from system services
* @return 0 on success, -1 if unavailable
*/
int model_common_sync_from_system(void);
#endif

View File

@@ -0,0 +1,47 @@
#include "model_ota.h"
#include "lisa_ui.h"
#include "lisa_ui_invoke.h"
#include "lisa_ui_nav_scr_ids.h"
#include "voice_msg.h"
#include "ota_manager.h"
struct model_ota_context {
const struct model_ota_cb *cbs;
void *arg;
};
static struct model_ota_context model_ota_ctx;
static void handle_ota_state_change(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
ota_state_t *state = (ota_state_t *)data;
LISA_UI_INVOKE_UI_ARG_PTR(state, sizeof(ota_state_t), {
if (model_ota_ctx.cbs && model_ota_ctx.cbs->on_ota_state_change) {
model_ota_ctx.cbs->on_ota_state_change(_invoke_state, model_ota_ctx.arg);
}
});
}
int model_ota_init(void)
{
voice_msg_sub(VOICE_MSG_OTA_UPDATING, handle_ota_state_change, NULL);
voice_msg_sub(VOICE_MSG_OTA_SUCCESSED, handle_ota_state_change, NULL);
voice_msg_sub(VOICE_MSG_OTA_FAILED, handle_ota_state_change, NULL);
return 0;
}
int model_ota_cb_register(const struct model_ota_cb *cb, void *arg)
{
model_ota_ctx.cbs = cb;
model_ota_ctx.arg = arg;
return 0;
}
int model_ota_cb_unregister(const struct model_ota_cb *cb)
{
model_ota_ctx.cbs = NULL;
model_ota_ctx.arg = NULL;
return 0;
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include "ota_manager.h"
struct model_ota_cb {
void (*on_ota_state_change)(const ota_state_t *state, void *arg);
};
int model_ota_init(void);
int model_ota_cb_register(const struct model_ota_cb *cb, void *arg);
int model_ota_cb_unregister(const struct model_ota_cb *cb);

View File

@@ -0,0 +1,624 @@
/**
* @file model_qrcode.c
* @brief QR code data model implementation
*/
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#define TAG "model_qrcode"
#include "lisa_ui.h"
#include "lisa_ui_invoke.h"
#include "model_wifi.h"
#include "model_qrcode.h"
#ifdef LISA_UI_PLATFORM_ARCS
#include "bt_app_if.h"
#include "lisa_thread.h"
#include "HTTPCUsr_api.h"
#include "voice_msg.h"
#include "lvgl.h"
#include "cJSON.h"
#include "app_datas.h"
#include "lisa_ui_invoke.h"
#include "app_net_cfg.h"
#endif
LV_IMG_DECLARE(ble_qr);
/**
* @brief QR code model context
*/
typedef struct {
bool initialized;
qrcode_status_t status;
const char *top_text;
const char *bottom_text;
const void *qr_image;
lv_img_dsc_t dynamic_qrcode_img;
char role_setting_qrcode_url[512];
uint8_t *role_setting_qrcode_img_raw;
uint32_t role_setting_qrcode_img_raw_size;
char device_id[64];
char device_id_text[72];
char quota_qrcode_url[512];
uint8_t *quota_qrcode_img_raw;
uint32_t quota_qrcode_img_raw_size;
char quota_message[256];
char quota_err_code[64];
lv_img_dsc_t quota_qrcode_img;
} qrcode_model_ctx_t;
static qrcode_model_ctx_t g_qrcode_ctx = {0};
// Default text strings
static const char *DEFAULT_NOT_CONNECTED_TEXT = "当前网络未连接\n请先完成配网";
static const char *DEFAULT_CONNECTED_TEXT = "请使用微信扫码修改配置";
static const char *DEFAULT_OUT_OF_LIMIT_TEXT = "今日交互额度已用完";
static const char *DEFAULT_OUT_OF_LIMIT_BOTTOM_TEXT = "微信扫码开通会员\n获取更多交互额度";
#ifdef LISA_UI_PLATFORM_ARCS
static void qrcode_download_thread(void *arg)
{
char *url = (char *)arg;
int ret = 0;
uint8_t *download_buffer = NULL;
uint32_t buffer_size = 0;
unsigned int received = 0;
unsigned int total_received = 0;
HTTPParameters *http_param = (HTTPParameters *)lisa_ui_malloc(sizeof(HTTPParameters));
if (!http_param) {
LISA_UI_LOGE("Failed to allocate HTTP parameters");
return;
}
memset(http_param, 0, sizeof(HTTPParameters));
if (strncmp(url, "https", 5) == 0) {
strcpy(http_param->Uri, "http");
strcat(http_param->Uri, url + 5);
LISA_UI_LOGI("Convert HTTPS to HTTP: %s -> %s", url, http_param->Uri);
} else {
strcpy(http_param->Uri, url);
}
http_param->HttpVerb = VerbGet;
http_param->nTimeout = 30;
http_param->pData = NULL;
http_param->pLength = 0;
if ((ret = HTTPC_open(http_param)) != 0) {
LISA_UI_LOGE("Failed to open HTTP connection: %d", ret);
goto cleanup;
}
if ((ret = HTTPC_request(http_param, NULL)) != 0) {
LISA_UI_LOGE("Failed to send HTTP request: %d", ret);
goto cleanup;
}
HTTP_CLIENT http_client = {0};
if ((ret = HTTPC_get_request_info(http_param, &http_client)) != 0) {
LISA_UI_LOGE("Failed to get HTTP response info: %d", ret);
goto cleanup;
}
if (http_client.TotalResponseBodyLength == 0) {
LISA_UI_LOGE("Empty response body");
goto cleanup;
}
buffer_size = http_client.TotalResponseBodyLength;
download_buffer = (uint8_t *)lisa_ui_malloc(buffer_size);
if (!download_buffer) {
LISA_UI_LOGE("Failed to allocate download buffer: %u bytes", buffer_size);
goto cleanup;
}
do {
unsigned int remaining = buffer_size - total_received;
unsigned int to_read = remaining;
UINT32 received = 0;
ret = HTTPC_read(http_param, download_buffer + total_received, to_read, &received);
if (received > 0) {
total_received += received;
}
if (ret != 0) {
break;
}
vTaskDelay(2);
} while (total_received < buffer_size);
if (total_received == buffer_size) {
LISA_UI_LOGI("QR code download completed successfully: %u bytes, %p", total_received, download_buffer);
struct invoke_payload {
uint8_t *img;
uint32_t len;
};
struct invoke_payload payload = {
.img = download_buffer,
.len = total_received,
};
struct invoke_payload *p = &payload;
uint32_t p_len = sizeof(struct invoke_payload);
LISA_UI_INVOKE_UI_ARG_PTR(p, p_len, {
if (g_qrcode_ctx.role_setting_qrcode_img_raw) {
lisa_ui_free(g_qrcode_ctx.role_setting_qrcode_img_raw);
}
LISA_UI_LOGI("set role setting qrcode, img:%p, size: %d", _invoke_p->img, _invoke_p->len);
g_qrcode_ctx.role_setting_qrcode_img_raw = _invoke_p->img;
g_qrcode_ctx.role_setting_qrcode_img_raw_size = _invoke_p->len;
});
} else {
LISA_UI_LOGE("Download incomplete: %u/%u bytes", total_received, buffer_size);
}
cleanup:
HTTPC_close(http_param);
if (http_param) {
lisa_ui_free(http_param);
}
if (download_buffer) {
/* 不需要释放download_buffer */
}
LISA_UI_LOGD("QR code download thread exiting");
}
static int modify_qrcode_url(const char *input_url, char *output_url, size_t output_size)
{
if (!input_url || !output_url || output_size == 0) {
LISA_UI_LOGE("Invalid parameters");
return -1;
}
const char *question_mark = strchr(input_url, '?');
if (question_mark) {
size_t base_url_len = question_mark - input_url + 1;
const char *new_params = "x-oss-process=image/resize,m_pad,w_148,limit_0,color_000000/quality,q_100/format,jpg";
size_t new_params_len = strlen(new_params);
if (base_url_len + new_params_len + 1 > output_size) {
LISA_UI_LOGE("Output buffer too small");
return -1;
}
memcpy(output_url, input_url, base_url_len);
strcpy(output_url + base_url_len, new_params);
LISA_UI_LOGI("Modified URL: %s -> %s", input_url, output_url);
return 0;
} else {
const char *new_params = "x-oss-process=image/resize,m_pad,w_148,limit_0,color_000000/quality,q_100/format,jpg";
size_t input_len = strlen(input_url);
size_t new_params_len = strlen(new_params);
size_t total_len = input_len + 1 + new_params_len + 1;
if (total_len > output_size) {
LISA_UI_LOGE("Output buffer too small for modified URL");
return -1;
}
strcpy(output_url, input_url);
output_url[input_len] = '?';
strcpy(output_url + input_len + 1, new_params);
LISA_UI_LOGI("Modified URL (add params): %s -> %s", input_url, output_url);
return 0;
}
}
static void download_qrcode_start(void)
{
static uint8_t qr_download_name[] = "qr_download";
lisa_thread_attr_t thread_attr = {
.name = qr_download_name,
.stack_size = 1024 * 16,
.priority = 5,
};
lisa_thread_t *download_thread =
lisa_thread_create(&thread_attr, qrcode_download_thread, (void *)g_qrcode_ctx.role_setting_qrcode_url);
if (!download_thread) {
LISA_UI_LOGE("Failed to create QR code download thread");
} else {
LISA_UI_LOGD("QR code download thread created successfully");
}
}
static void voice_cloud_role_setting_qrcode_handle(void *unused, uint32_t msg_id, void *data, uint32_t len,
void *user_data)
{
if (!data || len == 0 || len >= sizeof(g_qrcode_ctx.role_setting_qrcode_url)) {
LISA_UI_LOGE("Invalid QR code URL data");
return;
}
if (g_qrcode_ctx.status == QR_STATUS_OUT_OF_LIMIT) {
LISA_UI_LOGI("Ignore role setting qrcode due to out-of-limit status");
return;
}
char *url = (char *)data;
LISA_UI_INVOKE_UI_ARG_PTR(url, len, {
int ret = modify_qrcode_url(_invoke_url, g_qrcode_ctx.role_setting_qrcode_url,
sizeof(g_qrcode_ctx.role_setting_qrcode_url));
if (ret == 0) {
download_qrcode_start();
} else {
LISA_UI_LOGE("Failed to modify QR code URL");
}
});
}
static void quota_qrcode_download_thread(void *arg)
{
char *url = (char *)arg;
int ret = 0;
uint8_t *download_buffer = NULL;
uint32_t buffer_size = 0;
unsigned int total_received = 0;
HTTPParameters *http_param = (HTTPParameters *)lisa_ui_malloc(sizeof(HTTPParameters));
if (!http_param) {
LISA_UI_LOGE("Failed to allocate HTTP parameters");
return;
}
memset(http_param, 0, sizeof(HTTPParameters));
if (strncmp(url, "https", 5) == 0) {
strcpy(http_param->Uri, "http");
strcat(http_param->Uri, url + 5);
LISA_UI_LOGI("Convert HTTPS to HTTP: %s -> %s", url, http_param->Uri);
} else {
strcpy(http_param->Uri, url);
}
http_param->HttpVerb = VerbGet;
http_param->nTimeout = 30;
http_param->pData = NULL;
http_param->pLength = 0;
if ((ret = HTTPC_open(http_param)) != 0) {
LISA_UI_LOGE("Failed to open HTTP connection: %d", ret);
goto cleanup;
}
if ((ret = HTTPC_request(http_param, NULL)) != 0) {
LISA_UI_LOGE("Failed to send HTTP request: %d", ret);
goto cleanup;
}
HTTP_CLIENT http_client = {0};
if ((ret = HTTPC_get_request_info(http_param, &http_client)) != 0) {
LISA_UI_LOGE("Failed to get HTTP response info: %d", ret);
goto cleanup;
}
if (http_client.TotalResponseBodyLength == 0) {
LISA_UI_LOGE("Empty response body");
goto cleanup;
}
buffer_size = http_client.TotalResponseBodyLength;
download_buffer = (uint8_t *)lisa_ui_malloc(buffer_size);
if (!download_buffer) {
LISA_UI_LOGE("Failed to allocate download buffer: %u bytes", buffer_size);
goto cleanup;
}
do {
unsigned int remaining = buffer_size - total_received;
unsigned int to_read = remaining;
UINT32 received = 0;
ret = HTTPC_read(http_param, download_buffer + total_received, to_read, &received);
if (received > 0) {
total_received += received;
}
if (ret != 0) {
break;
}
vTaskDelay(2);
} while (total_received < buffer_size);
if (total_received == buffer_size) {
LISA_UI_LOGI("Quota QR code download completed: %u bytes, %p", total_received, download_buffer);
struct invoke_payload {
uint8_t *img;
uint32_t len;
};
struct invoke_payload payload = {
.img = download_buffer,
.len = total_received,
};
struct invoke_payload *p = &payload;
uint32_t p_len = sizeof(struct invoke_payload);
LISA_UI_INVOKE_UI_ARG_PTR(p, p_len, {
uint8_t *old_img = g_qrcode_ctx.quota_qrcode_img_raw;
LISA_UI_LOGI("Set quota qrcode, img:0x%p, size: %u", _invoke_p->img, _invoke_p->len);
g_qrcode_ctx.quota_qrcode_img_raw = _invoke_p->img;
g_qrcode_ctx.quota_qrcode_img_raw_size = _invoke_p->len;
memset(&g_qrcode_ctx.quota_qrcode_img, 0, sizeof(lv_img_dsc_t));
g_qrcode_ctx.quota_qrcode_img.header.cf = LV_IMG_CF_RAW;
g_qrcode_ctx.quota_qrcode_img.header.always_zero = 0;
g_qrcode_ctx.quota_qrcode_img.header.reserved = 0;
g_qrcode_ctx.quota_qrcode_img.header.w = 0;
g_qrcode_ctx.quota_qrcode_img.header.h = 0;
g_qrcode_ctx.quota_qrcode_img.data_size = g_qrcode_ctx.quota_qrcode_img_raw_size;
g_qrcode_ctx.quota_qrcode_img.data = g_qrcode_ctx.quota_qrcode_img_raw;
if (old_img) {
LISA_UI_LOGI("Freeing old quota qrcode image: 0x%p", old_img);
lisa_ui_free(old_img);
}
});
download_buffer = NULL;
} else {
LISA_UI_LOGE("Quota QR code download incomplete: %u/%u bytes", total_received, buffer_size);
}
cleanup:
if (download_buffer) {
lisa_ui_free(download_buffer);
}
if (http_param) {
HTTPC_close(http_param);
lisa_ui_free(http_param);
}
}
static void download_quota_qrcode_start(void)
{
static uint8_t quota_qr_dl_name[] = "quota_qr_dl";
lisa_thread_attr_t attr = {
.name = quota_qr_dl_name,
.priority = 8,
.stack_size = 8192,
};
lisa_thread_create(&attr, quota_qrcode_download_thread, g_qrcode_ctx.quota_qrcode_url);
}
struct auth_failed_rsp {
int err;
uint8_t auth_failed;
};
static void get_auth_failed_worker(void *data, uint32_t len, struct voice_invoke_rsp *rsp)
{
struct auth_failed_rsp *auth_rsp = (struct auth_failed_rsp *)rsp;
struct app_datas *app_datas = get_app_datas();
if (app_datas) {
auth_rsp->err = 0;
auth_rsp->auth_failed = app_datas->auth_failed;
} else {
auth_rsp->err = -1;
auth_rsp->auth_failed = 0;
}
}
static void voice_cloud_show_qrcode_handle(void *unused, uint32_t msg_id, void *data, uint32_t len,
void *user_data)
{
if (!data || len == 0) {
LISA_UI_LOGE("Invalid show qrcode data");
return;
}
char *json_str = (char *)data;
LISA_UI_LOGI("Show qrcode received: %s", json_str);
LISA_UI_INVOKE_UI_ARG_PTR(json_str, len, {
cJSON *json = cJSON_Parse(_invoke_json_str);
if (!json) {
LISA_UI_LOGE("Failed to parse show qrcode JSON");
return;
}
cJSON *url_json = cJSON_GetObjectItem(json, "url");
cJSON *message_json = cJSON_GetObjectItem(json, "message");
cJSON *err_code_json = cJSON_GetObjectItem(json, "err_code");
char url_buf[512] = {0};
char message_buf[256] = {0};
char err_code_buf[64] = {0};
if (url_json && url_json->valuestring) {
strncpy(url_buf, url_json->valuestring, sizeof(url_buf) - 1);
}
if (message_json && message_json->valuestring) {
strncpy(message_buf, message_json->valuestring, sizeof(message_buf) - 1);
} else {
strncpy(message_buf, DEFAULT_OUT_OF_LIMIT_TEXT, sizeof(message_buf) - 1);
}
if (err_code_json && err_code_json->valuestring) {
strncpy(err_code_buf, err_code_json->valuestring, sizeof(err_code_buf) - 1);
}
cJSON_Delete(json);
if (url_buf[0] == '\0') {
LISA_UI_LOGE("No URL in show qrcode JSON");
return;
}
LISA_UI_LOGI("Parsed - url: %s, message: %s, err_code: %s", url_buf, message_buf, err_code_buf);
int ret = modify_qrcode_url(url_buf, g_qrcode_ctx.quota_qrcode_url,
sizeof(g_qrcode_ctx.quota_qrcode_url));
if (ret == 0) {
strncpy(g_qrcode_ctx.quota_message, message_buf,
sizeof(g_qrcode_ctx.quota_message) - 1);
g_qrcode_ctx.quota_message[sizeof(g_qrcode_ctx.quota_message) - 1] = '\0';
strncpy(g_qrcode_ctx.quota_err_code, err_code_buf,
sizeof(g_qrcode_ctx.quota_err_code) - 1);
g_qrcode_ctx.quota_err_code[sizeof(g_qrcode_ctx.quota_err_code) - 1] = '\0';
g_qrcode_ctx.status = QR_STATUS_OUT_OF_LIMIT;
download_quota_qrcode_start();
LISA_UI_LOGI("Quota qrcode download started");
} else {
LISA_UI_LOGE("Failed to modify quota QR code URL");
}
});
}
#endif
int model_qrcode_init(void)
{
if (g_qrcode_ctx.initialized) {
return 0;
}
g_qrcode_ctx.initialized = true;
#ifdef LISA_UI_PLATFORM_ARCS
voice_msg_sub(VOICE_MSG_CLOUD_ROLE_SETTING_QRCODE, voice_cloud_role_setting_qrcode_handle, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_SHOW_QRCODE, voice_cloud_show_qrcode_handle, NULL);
LISA_UI_LOGI("Subscribed to VOICE_MSG_CLOUD_SHOW_QRCODE");
#endif
return 0;
}
qrcode_status_t model_qrcode_get_status(void)
{
return g_qrcode_ctx.status;
}
int model_qrcode_get_quota_data(qrcode_data_t *data)
{
if (!data) {
LISA_UI_LOGE("Invalid parameter");
return -1;
}
if (!g_qrcode_ctx.initialized) {
LISA_UI_LOGW("QR code model not initialized, initializing now");
model_qrcode_init();
}
data->status = QR_STATUS_OUT_OF_LIMIT;
data->top_text = g_qrcode_ctx.quota_message[0] ? g_qrcode_ctx.quota_message : DEFAULT_OUT_OF_LIMIT_TEXT;
data->bottom_text = DEFAULT_OUT_OF_LIMIT_BOTTOM_TEXT;
data->qr_image = g_qrcode_ctx.quota_qrcode_img_raw ? &g_qrcode_ctx.quota_qrcode_img : NULL;
LISA_UI_LOGI("Returning quota qrcode data, top_text: %s, bottom_text: %s, img: %p",
data->top_text, data->bottom_text, data->qr_image);
return 0;
}
int model_qrcode_get_config_data(qrcode_data_t *data)
{
if (!data) {
LISA_UI_LOGE("Invalid parameter");
return -1;
}
if (!g_qrcode_ctx.initialized) {
LISA_UI_LOGW("QR code model not initialized, initializing now");
model_qrcode_init();
}
model_wifi_status_t wifi_sta = model_wifi_get_status();
if (wifi_sta == MODEL_WIFI_STATUS_DISCONNECTED || wifi_sta == MODEL_WIFI_STATUS_UNKNOWN) {
data->status = QR_STATUS_NOT_CONNECTED;
data->top_text = DEFAULT_NOT_CONNECTED_TEXT;
if (get_current_device_id(g_qrcode_ctx.device_id, sizeof(g_qrcode_ctx.device_id)) == 0) {
snprintf(g_qrcode_ctx.device_id_text, sizeof(g_qrcode_ctx.device_id_text),
"ID: %s", g_qrcode_ctx.device_id);
data->bottom_text = g_qrcode_ctx.device_id_text;
} else {
data->bottom_text = "";
}
data->qr_image = &ble_qr;
#ifdef LISA_UI_PLATFORM_ARCS
app_ble_adv_start(0, BLE_ADV_GEN);
#endif
return 0;
}
#ifdef LISA_UI_PLATFORM_ARCS
struct auth_failed_rsp auth_rsp = {
.err = -1,
.auth_failed = 0,
};
int r = voice_invoke_sync(get_auth_failed_worker, NULL, 0,
(struct voice_invoke_rsp *)&auth_rsp, 1000);
if (r == 0 && auth_rsp.err == 0 && auth_rsp.auth_failed) {
data->top_text = "云端鉴权失败";
data->bottom_text = "请联系技术对接人添加云端授权";
data->qr_image = NULL;
return 0;
}
#endif
data->status = QR_STATUS_CONNECTED;
if (g_qrcode_ctx.role_setting_qrcode_img_raw == NULL) {
data->top_text = "二维码加载中, 请稍后";
data->bottom_text = "";
data->qr_image = NULL;
return 0;
}
g_qrcode_ctx.dynamic_qrcode_img.header.cf = LV_IMG_CF_RAW;
g_qrcode_ctx.dynamic_qrcode_img.header.always_zero = 0;
g_qrcode_ctx.dynamic_qrcode_img.header.reserved = 0;
g_qrcode_ctx.dynamic_qrcode_img.header.w = 0;
g_qrcode_ctx.dynamic_qrcode_img.header.h = 0;
g_qrcode_ctx.dynamic_qrcode_img.data_size = g_qrcode_ctx.role_setting_qrcode_img_raw_size;
g_qrcode_ctx.dynamic_qrcode_img.data = g_qrcode_ctx.role_setting_qrcode_img_raw;
data->top_text = DEFAULT_CONNECTED_TEXT;
data->bottom_text = "";
data->qr_image = &g_qrcode_ctx.dynamic_qrcode_img;
return 0;
}
int model_qrcode_get_data(qrcode_data_t *data)
{
if (!data) {
LISA_UI_LOGE("Invalid parameter");
return -1;
}
if (!g_qrcode_ctx.initialized) {
LISA_UI_LOGW("QR code model not initialized, initializing now");
model_qrcode_init();
}
if (g_qrcode_ctx.status == QR_STATUS_OUT_OF_LIMIT) {
return model_qrcode_get_quota_data(data);
}
return model_qrcode_get_config_data(data);
}

View File

@@ -0,0 +1,95 @@
/**
* @file model_qrcode.h
* @brief QR code data model interface
*
* Provides data access for QR code display and management.
*/
#ifndef __MODEL_QRCODE_H__
#define __MODEL_QRCODE_H__
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
/**
* @brief QR code status enumeration
*/
typedef enum {
QR_STATUS_NOT_CONNECTED = 0, /**< Network not connected, show BLE QR */
QR_STATUS_CONNECTED, /**< Network connected, show cloud QR */
QR_STATUS_OUT_OF_LIMIT, /**< Usage limit exceeded */
} qrcode_status_t;
/**
* @brief QR code data structure
*/
typedef struct {
qrcode_status_t status; /**< Current QR code status */
const char *top_text; /**< Top instruction text */
const char *bottom_text; /**< Bottom information text */
const void *qr_image; /**< QR code image source (C array) */
} qrcode_data_t;
/**
* @brief Initialize QR code model
*
* @return 0 on success, negative on error
*/
int model_qrcode_init(void);
/**
* @brief Get current QR code status
*
* @return Current QR code status
*/
qrcode_status_t model_qrcode_get_status(void);
/**
* @brief Get QR code data for display
*
* @param data Pointer to receive QR code data
* @return 0 on success, negative on error
*/
int model_qrcode_get_data(qrcode_data_t *data);
int model_qrcode_get_config_data(qrcode_data_t *data);
int model_qrcode_get_quota_data(qrcode_data_t *data);
/**
* @brief Update QR code from cloud (RGB565 format)
*
* @param rgb565_data RGB565 image data
* @param width Image width
* @param height Image height
* @param data_size Data size in bytes
* @return 0 on success, negative on error
*/
int model_qrcode_update_cloud_qr(const uint16_t *rgb565_data, uint16_t width, uint16_t height, uint32_t data_size);
/**
* @brief Set QR code status
*
* @param status New status
* @return 0 on success, negative on error
*/
int model_qrcode_set_status(qrcode_status_t status);
/**
* @brief Set custom text for QR code page
*
* @param top_text Top text (NULL to use default)
* @param bottom_text Bottom text (NULL to use default)
* @return 0 on success, negative on error
*/
int model_qrcode_set_text(const char *top_text, const char *bottom_text);
#ifdef __cplusplus
}
#endif
#endif /* __MODEL_QRCODE_H__ */

View File

@@ -0,0 +1,999 @@
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#include "lisa_ui_invoke.h"
#include "lisa_ui_log.h"
#include "model_voice.h"
#ifdef LISA_UI_PLATFORM_ARCS
#include "app_datas.h"
#include "voice_msg.h"
#include "voice_cloud.h"
#include "lisa_kv.h"
#include "kv.h"
#if CONFIG_OTA
#include "kv_sys.h"
#endif
#include "img_helper.h"
#include "async_task.h"
#include "cJSON.h"
#include "lisa_ui_nav_scr_ids.h"
#include "model_alarm.h"
#endif
#define IMG_REC_MODE_LSCHAT 0
#define IMG_REC_MODE_MCP 1
#define MAX_STANDBY_TEXTS 10
#define MAX_TEXT_LENGTH 128
#define MAX_LOADING_TEXT_LENGTH 128
struct model_voice_context {
uint32_t cloud_connected: 1;
uint32_t inited: 1;
uint32_t voice_en: 1;
uint32_t running: 1;
uint32_t tts_playing: 1;
uint32_t img_rec_in_progress: 1;
const struct model_voice_cb *cbs;
model_voice_wakeup_mode_t wakeup_mode;
void *arg;
uint8_t img_rec_mode;
char mcp_id[64];
char prompt[64];
uint32_t img_rec_task_id;
// Standby text rotation
char standby_texts[MAX_STANDBY_TEXTS][MAX_TEXT_LENGTH];
uint32_t standby_text_count;
uint32_t standby_interval_ms;
bool standby_enabled;
#if CONFIG_OTA
char wake_word[20];
#endif
};
static struct model_voice_context model_voice_ctx = {
.voice_en = true,
.inited = 0,
.cloud_connected = 0,
.wakeup_mode = MODEL_VOICE_WAKEUP_MODE_VOICE_SINGLE,
};
static char s_last_iat_text[128] = {0};
static void notify_standby_texts_changed(void);
#ifdef LISA_UI_PLATFORM_ARCS
static void voice_cloud_show_qrcode_received(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data);
static void voice_cloud_show_qrcode_ui_worker(void *arg, uint32_t arg_len);
#define SHOW_QRCODE_NAV_DELAY_MS 50
#endif
#if defined(CONFIG_OTA) && defined(LISA_UI_PLATFORM_ARCS)
static void model_voice_load_wake_word_from_kv(void)
{
char *wake_word = NULL;
lisa_kv_get_string(KV_KEY_SYS_WAKEWORD, &wake_word);
if (wake_word != NULL && wake_word[0] != '\0') {
strncpy(model_voice_ctx.wake_word, wake_word, sizeof(model_voice_ctx.wake_word) - 1);
model_voice_ctx.wake_word[sizeof(model_voice_ctx.wake_word) - 1] = '\0';
}
lisa_kv_free(wake_word);
}
#endif
const char *model_voice_role_name_get(void)
{
#ifdef CONFIG_OTA
if (strnlen(model_voice_ctx.wake_word, sizeof(model_voice_ctx.wake_word)) > 0) {
return model_voice_ctx.wake_word;
} else {
return "小马采宝";
}
#else
return "小马采宝";
#endif
}
#ifdef CONFIG_OTA
static const char *make_wake_hint_text(const char *base_text)
{
static char wake_hint[MAX_TEXT_LENGTH];
const char *wake_word = model_voice_role_name_get();
if (!base_text) {
return "";
}
const char *placeholder = "#唤醒词#";
char *pos = strstr(base_text, placeholder);
if (pos) {
size_t prefix_len = pos - base_text;
size_t wake_word_len = strlen(wake_word);
size_t suffix_len = strlen(pos + strlen(placeholder));
if (prefix_len + wake_word_len + suffix_len > MAX_TEXT_LENGTH - 1) {
return base_text; // 超出长度限制,返回原文本
}
char *p = wake_hint;
strncpy(p, base_text, prefix_len);
p += prefix_len;
strncpy(p, wake_word, wake_word_len);
p += wake_word_len;
strncpy(p, pos + strlen(placeholder), suffix_len);
p += suffix_len;
*p = '\0';
} else {
strncpy(wake_hint, base_text, MAX_TEXT_LENGTH - 1);
wake_hint[MAX_TEXT_LENGTH - 1] = '\0';
}
return wake_hint;
}
#endif
const char *model_voice_role_propmt_get(void)
{
#ifdef CONFIG_OTA
return make_wake_hint_text(model_voice_ctx.prompt);
#else
return model_voice_ctx.prompt;
#endif
}
model_voice_wakeup_mode_t model_voice_wakeup_mode_get(void)
{
return model_voice_ctx.wakeup_mode;
}
int model_voice_wakeup_mode_set(model_voice_wakeup_mode_t mode)
{
if (mode >= MODEL_VOICE_WAKEUP_MODE_MAX) {
return -1;
}
LISA_UI_LOGI("voice wakeup mode set, mode: %d", mode);
model_voice_ctx.wakeup_mode = mode;
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_wakeup_mode_changed) {
model_voice_ctx.cbs->on_wakeup_mode_changed(model_voice_ctx.arg, mode);
}
#ifdef LISA_UI_PLATFORM_ARCS
LISA_UI_INVOKE_BN_ARG_BASE(mode, {
struct app_datas *app_datas = get_app_datas();
if (app_datas == NULL) {
return;
}
app_datas->voice_work_mode = 0;
if (_invoke_mode == MODEL_VOICE_WAKEUP_MODE_BUTTON) {
app_datas->full_duplex = false;
app_datas->voice_work_mode &= ~VOICE_WORK_MODE_VOICE_WAKEUP;
app_datas->voice_work_mode |= VOICE_WORK_MODE_BUTTON_WAKEUP;
} else if (_invoke_mode == MODEL_VOICE_WAKEUP_MODE_VOICE_MULTI) {
app_datas->full_duplex = true;
app_datas->voice_work_mode &= ~VOICE_WORK_MODE_BUTTON_WAKEUP;
app_datas->voice_work_mode |= VOICE_WORK_MODE_VOICE_WAKEUP;
} else if (_invoke_mode == MODEL_VOICE_WAKEUP_MODE_VOICE_SINGLE) {
app_datas->full_duplex = false;
app_datas->voice_work_mode &= ~VOICE_WORK_MODE_BUTTON_WAKEUP;
app_datas->voice_work_mode |= VOICE_WORK_MODE_VOICE_WAKEUP;
} else {
LISA_UI_LOGW("invalid mode to save, mode: %d", _invoke_mode);
}
voice_cloud_chat_stop();
lisa_kv_set_bool(KV_KEY_FULL_DUPLEX, app_datas->full_duplex);
LISA_UI_LOGI("save voice work mode: %d", (int)app_datas->voice_work_mode);
lisa_kv_set_int(KV_KEY_WAKEUP_MODE, (int)app_datas->voice_work_mode);
});
#endif
return 0;
}
const char *model_voice_wakeup_mode_name_get(model_voice_wakeup_mode_t mode)
{
switch (mode) {
case MODEL_VOICE_WAKEUP_MODE_BUTTON:
return "Button Wakeup";
case MODEL_VOICE_WAKEUP_MODE_VOICE_SINGLE:
return "Voice Wakeup (Single)";
case MODEL_VOICE_WAKEUP_MODE_VOICE_MULTI:
return "Voice Wakeup (Multi)";
default:
return "Unknown";
}
}
#ifdef LISA_UI_PLATFORM_ARCS
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) {
LISA_UI_INVOKE_UI_ARG_NONE({
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_tts_text_start) {
model_voice_ctx.cbs->on_tts_text_start(model_voice_ctx.arg);
}
});
} else if (msg_id == VOICE_MSG_CLOUD_TTS_TEXT_UPDATE) {
char *text = (char *)data;
LISA_UI_INVOKE_UI_ARG_PTR(text, len, {
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_tts_text_update) {
model_voice_ctx.cbs->on_tts_text_update(_invoke_text, model_voice_ctx.arg);
}
});
} else if (msg_id == VOICE_MSG_CLOUD_TTS_TEXT_END) {
LISA_UI_INVOKE_UI_ARG_NONE({
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_tts_text_end) {
model_voice_ctx.cbs->on_tts_text_end(model_voice_ctx.arg);
}
});
}
}
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) {
s_last_iat_text[0] = '\0';
LISA_UI_INVOKE_UI_ARG_NONE({
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_iat_text_start) {
model_voice_ctx.cbs->on_iat_text_start(model_voice_ctx.arg);
}
});
} else if (msg_id == VOICE_MSG_CLOUD_IAT_UPDATE) {
char *p = (char *)data;
if (p && len > 0 && p[0] != '\0') {
uint32_t cpy_len = len > (sizeof(s_last_iat_text) - 1) ? (sizeof(s_last_iat_text) - 1) : len;
memcpy(s_last_iat_text, p, cpy_len);
s_last_iat_text[cpy_len] = '\0';
}
LISA_UI_INVOKE_UI_ARG_PTR(p, len, {
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_iat_text_update) {
model_voice_ctx.cbs->on_iat_text_update(_invoke_p, model_voice_ctx.arg);
}
});
} else if (msg_id == VOICE_MSG_CLOUD_IAT_END) {
LISA_UI_INVOKE_UI_ARG_NONE({
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_iat_text_end) {
model_voice_ctx.cbs->on_iat_text_end(model_voice_ctx.arg);
}
});
}
}
static void voice_cloud_session_starting(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
service_image_waiting_cancel();
s_last_iat_text[0] = '\0';
LISA_UI_INVOKE_UI_ARG_NONE({
model_voice_ctx.running = 1;
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_start) {
model_voice_ctx.cbs->on_start(model_voice_ctx.arg);
}
});
}
static void voice_cloud_session_finished(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
LISA_UI_INVOKE_UI_ARG_NONE({
model_voice_ctx.running = 0;
#ifdef LISA_UI_PLATFORM_ARCS
LISA_UI_LOGI("Audio recognition stopped on session finished");
#endif
LISA_UI_LOGI("Session finished, tts_playing=%d", model_voice_ctx.tts_playing);
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_finished) {
model_voice_ctx.cbs->on_finished(model_voice_ctx.arg);
}
});
}
static void voice_cloud_tts_url_received(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
#ifdef LISA_UI_PLATFORM_ARCS
LISA_UI_LOGI("Audio recognition stopped on TTS URL received");
#endif
}
static void voice_cloud_connected(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
LISA_UI_INVOKE_UI_ARG_NONE({
model_voice_ctx.cloud_connected = 1;
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_connected) {
model_voice_ctx.cbs->on_connected(model_voice_ctx.arg);
}
});
}
static void voice_cloud_disconnected(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
LISA_UI_INVOKE_UI_ARG_NONE({
model_voice_ctx.cloud_connected = 0;
model_voice_ctx.running = 0;
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_disconnected) {
model_voice_ctx.cbs->on_disconnected(model_voice_ctx.arg);
}
});
}
static void voice_cloud_emoji_received(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
char *emoji = (char *)data;
LISA_UI_INVOKE_UI_ARG_PTR(emoji, len, {
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_emoji) {
model_voice_ctx.cbs->on_emoji(model_voice_ctx.arg, _invoke_emoji);
}
});
}
static void voice_cloud_mcp_emoji_received(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
char *emoji = (char *)data;
LISA_UI_INVOKE_UI_ARG_PTR(emoji, len, {
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_mcp_emoji) {
model_voice_ctx.cbs->on_mcp_emoji(model_voice_ctx.arg, _invoke_emoji);
}
});
}
static void voice_cloud_mcp_loading_received(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
if (!data || len == 0) {
LISA_UI_INVOKE_UI_ARG_NONE({
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_mcp_loading) {
model_voice_ctx.cbs->on_mcp_loading(model_voice_ctx.arg, false, NULL);
}
});
return;
}
char loading_text[MAX_LOADING_TEXT_LENGTH] = {0};
uint32_t copy_len = len > (sizeof(loading_text) - 1) ? (sizeof(loading_text) - 1) : len;
memcpy(loading_text, data, copy_len);
char *text_ptr = loading_text;
LISA_UI_INVOKE_UI_ARG_PTR(text_ptr, strlen(text_ptr) + 1, {
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_mcp_loading) {
model_voice_ctx.cbs->on_mcp_loading(model_voice_ctx.arg, true, _invoke_text_ptr);
}
});
}
static void voice_cloud_tts_player_playing(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
LISA_UI_INVOKE_UI_ARG_NONE({
model_voice_ctx.tts_playing = 1;
LISA_UI_LOGI("TTS playing started, tts_playing=1");
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_tts_playing) {
model_voice_ctx.cbs->on_tts_playing(model_voice_ctx.arg);
}
});
}
static void voice_cloud_tts_player_stoped(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
LISA_UI_INVOKE_UI_ARG_NONE({
model_voice_ctx.tts_playing = 0;
LISA_UI_LOGI("TTS playing stopped, tts_playing=0, running=%d", model_voice_ctx.running);
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_tts_stoped) {
model_voice_ctx.cbs->on_tts_stoped(model_voice_ctx.arg);
}
});
}
static void voice_button_changed(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
voice_msg_button_evt_t *evt = (voice_msg_button_evt_t *)data;
if (evt->button_id == 1) {
if (evt->action == VOICE_MSG_BUTTON_ACTION_PRESS_DOWN) {
LISA_UI_INVOKE_UI_ARG_NONE({
model_voice_ctx.img_rec_mode = IMG_REC_MODE_LSCHAT;
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_image_preview) {
model_voice_ctx.cbs->on_image_preview(model_voice_ctx.arg);
}
});
} 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) {
LISA_UI_INVOKE_UI_ARG_NONE({
model_voice_ctx.img_rec_mode = IMG_REC_MODE_LSCHAT;
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_image_rec) {
model_voice_ctx.cbs->on_image_rec(model_voice_ctx.arg);
}
});
}
} else if (evt->button_id == 2 && evt->action == VOICE_MSG_BUTTON_ACTION_CLICK) {
LISA_UI_INVOKE_UI_ARG_NONE({
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_info_show) {
model_voice_ctx.cbs->on_info_show(model_voice_ctx.arg);
}
});
}
}
static void voice_mcp_image_recognition_stop_worker(void *arg, uint32_t len)
{
uint32_t task_id = 0;
/* 检查任务ID是否匹配如果不匹配说明已被新任务打断 */
if (arg && len >= sizeof(uint32_t)) {
task_id = *(uint32_t *)arg;
if (task_id != model_voice_ctx.img_rec_task_id) {
LISA_UI_LOGW("Image recognition task %u canceled by newer task %u", task_id, model_voice_ctx.img_rec_task_id);
return;
}
}
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_image_rec) {
model_voice_ctx.cbs->on_image_rec(model_voice_ctx.arg);
}
/* 清除识别进行中标志 */
model_voice_ctx.img_rec_in_progress = 0;
LISA_UI_LOGI("Image recognition task %u completed, flag cleared", model_voice_ctx.img_rec_task_id);
}
static void voice_mcp_image_recognition(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
char *mcp_id = data;
LISA_UI_INVOKE_UI_ARG_PTR(mcp_id, len, {
/* 如果已有识别任务在进行递增任务ID打断上一次 */
if (model_voice_ctx.img_rec_in_progress) {
model_voice_ctx.img_rec_task_id++;
LISA_UI_LOGW("Image recognition already in progress, interrupting with new task %u", model_voice_ctx.img_rec_task_id);
} else {
/* 设置识别进行中标志 */
model_voice_ctx.img_rec_in_progress = 1;
model_voice_ctx.img_rec_task_id++;
LISA_UI_LOGI("Starting image recognition task %u", model_voice_ctx.img_rec_task_id);
}
memcpy(model_voice_ctx.mcp_id, _invoke_mcp_id,
_invoke_len > sizeof(model_voice_ctx.mcp_id) ? sizeof(model_voice_ctx.mcp_id) : _invoke_len);
model_voice_ctx.img_rec_mode = IMG_REC_MODE_MCP;
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_image_preview) {
model_voice_ctx.cbs->on_image_preview(model_voice_ctx.arg);
}
/* 启动预览后200ms停止预览并进行图像识别传递当前任务ID */
static uint32_t current_task_id;
current_task_id = model_voice_ctx.img_rec_task_id;
lisa_ui_invoke_ui_delayed(voice_mcp_image_recognition_stop_worker, &current_task_id, sizeof(current_task_id), 200);
});
}
static void voice_button_image_recognition(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
LISA_UI_INVOKE_UI_ARG_NONE({
model_voice_ctx.img_rec_mode = IMG_REC_MODE_LSCHAT;
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_image_preview) {
model_voice_ctx.cbs->on_image_preview(model_voice_ctx.arg);
}
/* 延迟触发识别,给预览/抓帧留出时间 */
lisa_ui_invoke_ui_delayed(voice_mcp_image_recognition_stop_worker, NULL, 0, 200);
});
}
static void voice_mcp_image_url_received(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
char *url = (char *)data;
if (!url || len == 0) {
return;
}
LISA_UI_INVOKE_UI_ARG_PTR(url, len, {
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_image_url) {
model_voice_ctx.cbs->on_image_url(model_voice_ctx.arg, _invoke_url);
}
});
}
static void notify_standby_texts_changed(void)
{
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_standby_texts_changed) {
model_voice_ctx.cbs->on_standby_texts_changed(model_voice_ctx.arg);
}
}
static void voice_cloud_show_qrcode_received(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
LISA_UI_LOGI("Show qrcode message received in model_voice");
lisa_ui_invoke_ui_delayed(voice_cloud_show_qrcode_ui_worker, NULL, 0, SHOW_QRCODE_NAV_DELAY_MS);
}
static void voice_cloud_show_qrcode_ui_worker(void *arg, uint32_t arg_len)
{
(void)arg;
(void)arg_len;
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_show_qrcode) {
LISA_UI_LOGI("Invoking on_show_qrcode callback");
model_voice_ctx.cbs->on_show_qrcode(model_voice_ctx.arg);
} else {
LISA_UI_LOGE("on_show_qrcode callback is NULL");
}
}
static void voice_cloud_open_info_received(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;
LISA_UI_LOGI("Open info page message received in model_voice");
LISA_UI_INVOKE_UI_ARG_NONE({
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_info_show) {
LISA_UI_LOGI("Invoking on_info_show callback");
model_voice_ctx.cbs->on_info_show(model_voice_ctx.arg);
} else {
LISA_UI_LOGE("on_info_show callback is NULL");
}
});
}
static void voice_cloud_standby_texts_received(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
char *banner_json = (char *)data;
if (!banner_json || len == 0) {
LISA_UI_LOGE("Invalid standby texts banner data");
return;
}
LISA_UI_LOGI("Standby texts banner received: %s", banner_json);
LISA_UI_INVOKE_UI_ARG_PTR(banner_json, len, {
// 解析 banner JSON 数据
cJSON *banner = cJSON_Parse(_invoke_banner_json);
if (!banner) {
LISA_UI_LOGE("Failed to parse banner JSON");
return;
}
// 获取 resources 数组
cJSON *resources = cJSON_GetObjectItem(banner, "resources");
if (!resources || !cJSON_IsArray(resources)) {
LISA_UI_LOGE("Invalid resources array");
cJSON_Delete(banner);
return;
}
// 获取 interval_ms
cJSON *interval_ms_item = cJSON_GetObjectItem(banner, "interval_ms");
uint32_t interval_ms = 3000; // 默认3秒
if (interval_ms_item && cJSON_IsNumber(interval_ms_item)) {
interval_ms = (uint32_t)interval_ms_item->valueint;
}
int resources_count = cJSON_GetArraySize(resources);
if (resources_count <= 0 || resources_count > MAX_STANDBY_TEXTS) {
LISA_UI_LOGE("Invalid resources count: %d", resources_count);
cJSON_Delete(banner);
return;
}
// 提取文本数组
int valid_count = 0;
for (int i = 0; i < resources_count && i < MAX_STANDBY_TEXTS; i++) {
cJSON *resource = cJSON_GetArrayItem(resources, i);
if (resource) {
cJSON *text_item = cJSON_GetObjectItem(resource, "text");
if (text_item && cJSON_IsString(text_item) && strlen(text_item->valuestring) > 0) {
strncpy(model_voice_ctx.standby_texts[valid_count], text_item->valuestring, MAX_TEXT_LENGTH - 1);
model_voice_ctx.standby_texts[valid_count][MAX_TEXT_LENGTH - 1] = '\0';
LISA_UI_LOGI("Found standby text %d: %s", valid_count, model_voice_ctx.standby_texts[valid_count]);
valid_count++;
}
}
}
cJSON_Delete(banner);
if (valid_count > 0) {
// 更新配置
model_voice_ctx.standby_text_count = valid_count;
model_voice_ctx.standby_interval_ms = interval_ms > 0 ? interval_ms : 3000;
model_voice_ctx.standby_enabled = true;
LISA_UI_LOGI("Successfully set %d standby texts with interval %dms", valid_count, interval_ms);
notify_standby_texts_changed();
} else {
LISA_UI_LOGE("No valid texts found");
model_voice_ctx.standby_enabled = false;
model_voice_ctx.standby_text_count = 0;
model_voice_ctx.standby_interval_ms = 0;
notify_standby_texts_changed();
}
});
}
#endif
#ifdef CONFIG_OTA
static void voice_cloud_ota_state_change(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
ota_state_t *state = (ota_state_t *)data;
LISA_UI_INVOKE_UI_ARG_PTR(state, sizeof(ota_state_t), {
if (_invoke_state->state == OTA_STATE_UP_TO_DATE) {
strncpy(model_voice_ctx.wake_word, _invoke_state->wake_word, sizeof(model_voice_ctx.wake_word) - 1);
}
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_ota_state_change) {
model_voice_ctx.cbs->on_ota_state_change(_invoke_state, model_voice_ctx.arg);
}
});
}
#endif
int model_voice_off(void)
{
model_voice_ctx.voice_en = false;
#ifdef LISA_UI_PLATFORM_ARCS
LISA_UI_INVOKE_BN_ARG_NONE({
struct app_datas *app_datas = get_app_datas();
if (app_datas == NULL) {
return;
}
alarm_data_t alarm_data;
bool alarm_is_ringing = (model_alarm_get_data(&alarm_data) == 0 && alarm_data.timestamp > 0);
if (!alarm_is_ringing) {
app_datas->can_wakeup = false;
LISA_UI_LOGI("Voice off: can_wakeup set to false");
} else {
LISA_UI_LOGI("Voice off: alarm is ringing, keeping can_wakeup=%d", app_datas->can_wakeup);
}
voice_cloud_chat_stop();
});
#endif
return 0;
}
int model_voice_on(void)
{
model_voice_ctx.voice_en = true;
#ifdef LISA_UI_PLATFORM_ARCS
LISA_UI_INVOKE_BN_ARG_NONE({
struct app_datas *app_datas = get_app_datas();
if (app_datas == NULL) {
return;
}
app_datas->can_wakeup = true;
});
#endif
return 0;
}
int model_voice_init(void)
{
if (model_voice_ctx.inited) {
return 0;
}
#ifdef LISA_UI_PLATFORM_ARCS
struct app_datas *app_datas = get_app_datas();
if (app_datas->voice_work_mode & VOICE_WORK_MODE_BUTTON_WAKEUP) {
model_voice_ctx.wakeup_mode = MODEL_VOICE_WAKEUP_MODE_BUTTON;
} else {
if (app_datas->full_duplex) {
model_voice_ctx.wakeup_mode = MODEL_VOICE_WAKEUP_MODE_VOICE_MULTI;
} else {
model_voice_ctx.wakeup_mode = MODEL_VOICE_WAKEUP_MODE_VOICE_SINGLE;
}
}
strncpy(model_voice_ctx.prompt, app_datas->wakeup_prompt, sizeof(model_voice_ctx.prompt) - 1);
model_voice_ctx.prompt[sizeof(model_voice_ctx.prompt) - 1] = 0;
#ifdef CONFIG_OTA
model_voice_load_wake_word_from_kv();
#endif
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_URL, voice_cloud_tts_url_received, 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_CLOUD_CONNECTED, voice_cloud_connected, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_DISCONNECTED, voice_cloud_disconnected, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_EMOJI, voice_cloud_emoji_received, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_MCP_EMOJI, voice_cloud_mcp_emoji_received, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_MCP_LOADING, voice_cloud_mcp_loading_received, NULL);
voice_msg_sub(VOICE_MSG_PLAYER_TTS_PLAYING, voice_cloud_tts_player_playing, NULL);
voice_msg_sub(VOICE_MSG_PLAYER_TTS_STOPED, voice_cloud_tts_player_stoped, NULL);
voice_msg_sub(VOICE_MSG_BUTTON_CHANGE, voice_button_changed, NULL);
voice_msg_sub(VOICE_MSG_BUTTON_IMAGE_RECOGNITION, voice_button_image_recognition, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_MCP_IMAGE_RECOGNITION, voice_mcp_image_recognition, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_MCP_IMAGE_URL, voice_mcp_image_url_received, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_STANDBY_TEXTS, voice_cloud_standby_texts_received, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_SHOW_QRCODE, voice_cloud_show_qrcode_received, NULL);
voice_msg_sub(VOICE_MSG_CLOUD_OPEN_INFO, voice_cloud_open_info_received, NULL);
#ifdef CONFIG_OTA
voice_msg_sub(VOICE_MSG_OTA_CHECKING, voice_cloud_ota_state_change, NULL);
voice_msg_sub(VOICE_MSG_OTA_UPDATING, voice_cloud_ota_state_change, NULL);
voice_msg_sub(VOICE_MSG_OTA_UP_TO_DATE, voice_cloud_ota_state_change, NULL);
#endif
#ifdef CONFIG_VOICE_LINK_LINGXIN
model_voice_ctx.cloud_connected = (uint32_t)(app_datas->voice_cloud_connected ? 1 : 0);
#else
model_voice_ctx.cloud_connected = (uint32_t)(voice_cloud_is_connected() ? 1 : 0);
#endif
LISA_UI_LOGI("Sync cloud state on init: connected=%d", (int)model_voice_ctx.cloud_connected);
#endif
model_voice_ctx.inited = 1;
return 0;
}
int model_voice_cb_register(const struct model_voice_cb *cb, void *arg)
{
model_voice_ctx.cbs = cb;
model_voice_ctx.arg = arg;
if (model_voice_ctx.cbs && model_voice_ctx.cbs->on_wakeup_mode_changed) {
model_voice_ctx.cbs->on_wakeup_mode_changed(model_voice_ctx.arg, model_voice_ctx.wakeup_mode);
}
return 0;
}
int model_voice_cb_unregister(const struct model_voice_cb *cb)
{
model_voice_ctx.cbs = NULL;
return 0;
}
uint8_t model_voice_cloud_is_connected(void)
{
return model_voice_ctx.cloud_connected;
}
uint8_t model_voice_cloud_is_running(void)
{
return model_voice_ctx.cloud_connected & model_voice_ctx.running;
}
uint8_t model_voice_tts_is_playing(void)
{
return (uint8_t)model_voice_ctx.tts_playing;
}
const char *model_voice_last_iat_text_get(void)
{
return s_last_iat_text;
}
uint8_t model_voice_img_rec_is_mcp(void)
{
return (uint8_t)(model_voice_ctx.img_rec_mode == IMG_REC_MODE_MCP);
}
uint32_t model_voice_standby_text_count_get(void)
{
if (!model_voice_ctx.standby_enabled) {
return 0;
}
return model_voice_ctx.standby_text_count;
}
uint32_t model_voice_standby_text_interval_ms_get(void)
{
if (!model_voice_ctx.standby_enabled) {
return 0;
}
return model_voice_ctx.standby_interval_ms;
}
const char *model_voice_standby_text_get(uint32_t index)
{
if (!model_voice_ctx.standby_enabled) {
return NULL;
}
if (index >= model_voice_ctx.standby_text_count) {
return NULL;
}
#ifdef CONFIG_OTA
return make_wake_hint_text(model_voice_ctx.standby_texts[index]);
#else
return model_voice_ctx.standby_texts[index];
#endif
}
#ifdef LISA_UI_PLATFORM_ARCS
struct voice_img_upload_msg {
uint8_t *data;
uint32_t len;
uint32_t w;
uint32_t h;
uint8_t mode;
uint8_t mcp_id[64];
};
struct voice_img_cloud_sync_msg {
uint8_t *jpg_img;
uint32_t len;
};
static void voice_img_rec(void *data, uint32_t len, struct voice_invoke_rsp *rsp)
{
struct voice_img_cloud_sync_msg *msg = (struct voice_img_cloud_sync_msg *)data;
int r = voice_cloud_image_recognition(msg->jpg_img, msg->len);
rsp->err = r;
}
static cJSON *mcp_image_result_create(const char *url)
{
cJSON *result = cJSON_CreateObject();
if (!result) {
return NULL;
}
cJSON *content_array = cJSON_CreateArray();
if (!content_array) {
cJSON_Delete(result);
return NULL;
}
cJSON *content_item = cJSON_CreateObject();
if (!content_item) {
cJSON_Delete(content_array);
cJSON_Delete(result);
return NULL;
}
cJSON_AddStringToObject(content_item, "type", "image");
if (url && url[0] != '\0') {
cJSON_AddStringToObject(content_item, "data", url);
} else {
cJSON_AddStringToObject(content_item, "data", "");
}
cJSON_AddStringToObject(content_item, "mimeType", "url");
cJSON_AddItemToArray(content_array, content_item);
cJSON_AddItemToObject(result, "content", content_array);
if (url && url[0] != '\0') {
cJSON_AddBoolToObject(result, "isError", false);
} else {
cJSON_AddBoolToObject(result, "isError", true);
}
return result;
}
static void async_task_img_upload(void *p, bool *should_stop)
{
struct voice_img_upload_msg *msg = (struct voice_img_upload_msg *)p;
uint8_t *jpeg_data = NULL;
uint32_t jpeg_len = 0;
int ret = img_helper_rgb565_to_jpeg(msg->data, msg->len, msg->w, msg->h, &jpeg_data, &jpeg_len);
if (ret != 0 || !jpeg_data) {
lisa_ui_free(msg->data);
lisa_ui_free(msg);
LISA_UI_LOGE("Failed to encode JPEG: %d", ret);
return;
}
if (msg->mode == IMG_REC_MODE_LSCHAT) {
struct voice_invoke_rsp rsp = {
.err = -1,
};
struct voice_img_cloud_sync_msg sync_msg = {
.jpg_img = jpeg_data,
.len = jpeg_len,
};
int r = voice_invoke_sync(voice_img_rec, &sync_msg, sizeof(struct voice_img_cloud_sync_msg),
(struct voice_invoke_rsp *)&rsp, 1000);
} else if (msg->mode == IMG_REC_MODE_MCP) {
char *url = NULL;
int upload_ret = voice_cloud_upload_jpeg_img(jpeg_data, jpeg_len, &url);
cJSON *result = mcp_image_result_create(url);
if (result) {
if (url && upload_ret == 0) {
LISA_UI_LOGI("jpeg upload success, url: %s", url);
} else {
LISA_UI_LOGE("jpeg upload failed");
}
mcp_tool_call_result_response(msg->mcp_id, result);
cJSON_Delete(result);
}
if (url) {
voice_cloud_jpeg_img_url_free(url);
}
}
img_helper_jpeg_free(jpeg_data);
lisa_ui_free(msg->data);
lisa_ui_free(msg);
}
#endif
int model_voice_img_recognition(uint8_t *rgb565, uint32_t len, int width, int height)
{
#ifdef LISA_UI_PLATFORM_ARCS
uint8_t *rgb565_cpy = lisa_ui_malloc(len);
if (rgb565_cpy == NULL) {
return -3;
}
memcpy(rgb565_cpy, rgb565, len);
struct voice_img_upload_msg *msg = lisa_ui_malloc(sizeof(struct voice_img_upload_msg));
if (msg == NULL) {
lisa_ui_free(rgb565_cpy);
return -1;
}
msg->data = rgb565_cpy;
msg->len = len;
msg->w = width;
msg->h = height;
msg->mode = model_voice_ctx.img_rec_mode;
memcpy(msg->mcp_id, model_voice_ctx.mcp_id, sizeof(msg->mcp_id));
async_task_t *async_task = async_task_create("img_rec", 4096, 5, async_task_img_upload, NULL, msg);
if (async_task == NULL) {
lisa_ui_free(rgb565_cpy);
return -1;
}
async_task_start(async_task);
return 0;
#else
return -5;
#endif
}

View File

@@ -0,0 +1,69 @@
#ifndef __MODEL_VOICE_H__
#define __MODEL_VOICE_H__
#include <math.h>
#include <stdbool.h>
#ifdef CONFIG_OTA
#include "ota_manager.h"
#endif
typedef enum {
MODEL_VOICE_WAKEUP_MODE_BUTTON = 0,
MODEL_VOICE_WAKEUP_MODE_VOICE_SINGLE,
MODEL_VOICE_WAKEUP_MODE_VOICE_MULTI,
MODEL_VOICE_WAKEUP_MODE_MAX
} model_voice_wakeup_mode_t;
int model_voice_off(void);
int model_voice_on(void);
struct model_voice_cb {
void (*on_tts_stoped)(void *arg);
void (*on_tts_playing)(void *arg);
void (*on_emoji)(void *arg, const char *emoji_name);
void (*on_mcp_emoji)(void *arg, const char *emoji_name);
void (*on_mcp_loading)(void *arg, bool is_loading, const char *loading_text);
void (*on_connected)(void *arg);
void (*on_disconnected)(void *arg);
void (*on_start)(void *arg);
void (*on_finished)(void *arg);
void (*on_tts_text_start)(void *arg);
void (*on_tts_text_update)(const char *text, void *arg);
void (*on_tts_text_end)(void *arg);
void (*on_iat_text_start)(void *arg);
void (*on_iat_text_update)(const char *text, void *arg);
void (*on_iat_text_end)(void *arg);
void (*on_image_rec)(void *arg);
void (*on_image_preview)(void *arg);
void (*on_image_url)(void *arg, const char *url);
void (*on_info_show)(void *arg);
void (*on_show_qrcode)(void *arg);
void (*on_standby_texts_changed)(void *arg);
void (*on_standby_text_update)(void *arg, const char *text, bool is_cloud_text);
#ifdef CONFIG_OTA
void (*on_ota_state_change)(const ota_state_t *state, void *arg);
#endif
void (*on_wakeup_mode_changed)(void *arg, model_voice_wakeup_mode_t mode);
};
int model_voice_init(void);
int model_voice_cb_register(const struct model_voice_cb *cb, void *arg);
int model_voice_cb_unregister(const struct model_voice_cb *cb);
const char *model_voice_role_name_get(void);
const char *model_voice_role_propmt_get(void);
uint8_t model_voice_cloud_is_connected(void);
uint8_t model_voice_cloud_is_running(void);
uint8_t model_voice_tts_is_playing(void);
const char *model_voice_last_iat_text_get(void);
model_voice_wakeup_mode_t model_voice_wakeup_mode_get(void);
int model_voice_wakeup_mode_set(model_voice_wakeup_mode_t mode);
const char *model_voice_wakeup_mode_name_get(model_voice_wakeup_mode_t mode);
int model_voice_img_recognition(uint8_t *rgb565, uint32_t len, int width, int height);
uint32_t model_voice_standby_text_count_get(void);
uint32_t model_voice_standby_text_interval_ms_get(void);
const char *model_voice_standby_text_get(uint32_t index);
#endif

View File

@@ -0,0 +1,436 @@
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#define TAG "model_wifi"
#include "lisa_ui_invoke.h"
#include "lisa_ui_log.h"
#include "model_wifi.h"
#ifdef LISA_UI_PLATFORM_ARCS
#include "wifi_manager/wifi_manager.h"
#include "voice_msg.h"
#endif
struct model_wifi_context {
uint32_t inited: 1;
uint32_t scanning: 1;
const struct model_wifi_cb *cbs;
void *arg;
model_wifi_status_t status;
model_wifi_scan_info_t scan_list[WIFI_MAX_SCAN_APS];
int scan_count;
};
static struct model_wifi_context model_wifi_ctx = {
.inited = 0,
.scanning = 0,
.status = MODEL_WIFI_STATUS_DISCONNECTED,
.scan_count = 0,
};
#ifdef LISA_UI_PLATFORM_ARCS
// WiFi Manager 到 Model 的加密模式转换
static model_wifi_encryption_mode_t convert_encryption_mode(wifi_mgr_wifi_encryption_mode_t mode)
{
switch (mode) {
case WIFI_MGR_WIFI_AUTH_AUTO:
return MODEL_WIFI_AUTH_AUTO;
case WIFI_MGR_WIFI_AUTH_OPEN:
return MODEL_WIFI_AUTH_OPEN;
case WIFI_MGR_WIFI_AUTH_WEP:
return MODEL_WIFI_AUTH_WEP;
case WIFI_MGR_WIFI_AUTH_WPA_PSK:
return MODEL_WIFI_AUTH_WPA_PSK;
case WIFI_MGR_WIFI_AUTH_WPA2_PSK:
return MODEL_WIFI_AUTH_WPA2_PSK;
case WIFI_MGR_WIFI_AUTH_WPA_WPA2_PSK:
return MODEL_WIFI_AUTH_WPA_WPA2_PSK;
case WIFI_MGR_WIFI_AUTH_WPA2_ENTERPRISE:
return MODEL_WIFI_AUTH_WPA2_ENTERPRISE;
case WIFI_MGR_WIFI_AUTH_WPA3_PSK:
return MODEL_WIFI_AUTH_WPA3_PSK;
case WIFI_MGR_WIFI_AUTH_WPA2_WPA3_PSK:
return MODEL_WIFI_AUTH_WPA2_WPA3_PSK;
default:
return MODEL_WIFI_AUTH_UNKNOWN;
}
}
// Model 到 WiFi Manager 的加密模式转换
static wifi_mgr_wifi_encryption_mode_t convert_to_wifi_mgr_encryption_mode(model_wifi_encryption_mode_t mode)
{
switch (mode) {
case MODEL_WIFI_AUTH_AUTO:
return WIFI_MGR_WIFI_AUTH_AUTO;
case MODEL_WIFI_AUTH_OPEN:
return WIFI_MGR_WIFI_AUTH_OPEN;
case MODEL_WIFI_AUTH_WEP:
return WIFI_MGR_WIFI_AUTH_WEP;
case MODEL_WIFI_AUTH_WPA_PSK:
return WIFI_MGR_WIFI_AUTH_WPA_PSK;
case MODEL_WIFI_AUTH_WPA2_PSK:
return WIFI_MGR_WIFI_AUTH_WPA2_PSK;
case MODEL_WIFI_AUTH_WPA_WPA2_PSK:
return WIFI_MGR_WIFI_AUTH_WPA_WPA2_PSK;
case MODEL_WIFI_AUTH_WPA2_ENTERPRISE:
return WIFI_MGR_WIFI_AUTH_WPA2_ENTERPRISE;
case MODEL_WIFI_AUTH_WPA3_PSK:
return WIFI_MGR_WIFI_AUTH_WPA3_PSK;
case MODEL_WIFI_AUTH_WPA2_WPA3_PSK:
return WIFI_MGR_WIFI_AUTH_WPA2_WPA3_PSK;
default:
return WIFI_MGR_WIFI_AUTH_UNKNOWN;
}
}
// WiFi Manager 到 Model 的状态转换
static model_wifi_status_t convert_wifi_status(wifi_mgr_connection_status_t status)
{
switch (status) {
case WIFI_MGR_STA_CONNECTED:
return MODEL_WIFI_STATUS_CONNECTED;
case WIFI_MGR_STA_CONNECTING:
return MODEL_WIFI_STATUS_CONNECTING;
case WIFI_MGR_STA_DISCONNECTED:
return MODEL_WIFI_STATUS_DISCONNECTED;
default:
return MODEL_WIFI_STATUS_UNKNOWN;
}
}
// WiFi 连接状态回调
static void wifi_connection_event_handler(wifi_mgr_connection_info_t *connection_info, void *arg)
{
if (!connection_info) {
return;
}
LISA_UI_LOGI("wifi connecting info, ssid: %s, reason: %d", connection_info->sta_info->ssid,
connection_info->reason);
wifi_mgr_connection_status_t status = connection_info->status;
int reason = connection_info->reason;
LISA_UI_INVOKE_UI_ARG_BASE(status, {
model_wifi_ctx.status = convert_wifi_status(_invoke_status);
if (model_wifi_ctx.cbs) {
switch (_invoke_status) {
case WIFI_MGR_STA_CONNECTED:
if (model_wifi_ctx.cbs->on_connected) {
model_wifi_sta_config_t sta_info;
if (model_wifi_get_connected_info(&sta_info) == 0) {
model_wifi_ctx.cbs->on_connected(&sta_info, model_wifi_ctx.arg);
}
}
break;
case WIFI_MGR_STA_CONNECTING:
if (model_wifi_ctx.cbs->on_connecting) {
model_wifi_ctx.cbs->on_connecting(model_wifi_ctx.arg);
}
break;
case WIFI_MGR_STA_DISCONNECTED:
if (model_wifi_ctx.cbs->on_disconnected) {
model_wifi_ctx.cbs->on_disconnected(0, model_wifi_ctx.arg);
}
break;
default:
break;
}
}
});
}
// WiFi 扫描完成回调
static void wifi_scan_done_handler(wifi_mgr_scan_info_t *aps_info, int ap_num, void *arg)
{
if (ap_num > 0 && aps_info) {
// 计算需要拷贝的AP数量
int copy_count = ap_num > WIFI_MAX_SCAN_APS ? WIFI_MAX_SCAN_APS : ap_num;
uint32_t data_len = sizeof(wifi_mgr_scan_info_t) * copy_count;
LISA_UI_INVOKE_UI_ARG_PTR(aps_info, data_len, {
wifi_mgr_scan_info_t *scan_result = (wifi_mgr_scan_info_t *)_invoke_aps_info;
int count = _invoke_len / sizeof(wifi_mgr_scan_info_t);
model_wifi_ctx.scanning = 0;
model_wifi_ctx.scan_count = count;
// 转换扫描结果到 model 层数据结构
for (int i = 0; i < count; i++) {
strncpy(model_wifi_ctx.scan_list[i].ssid, scan_result[i].ssid, WIFI_SSID_MAX_LEN - 1);
model_wifi_ctx.scan_list[i].ssid[WIFI_SSID_MAX_LEN - 1] = '\0';
strncpy(model_wifi_ctx.scan_list[i].bssid, scan_result[i].bssid, WIFI_BSSID_MAX_LEN - 1);
model_wifi_ctx.scan_list[i].bssid[WIFI_BSSID_MAX_LEN - 1] = '\0';
model_wifi_ctx.scan_list[i].channel = scan_result[i].channel;
model_wifi_ctx.scan_list[i].rssi = scan_result[i].rssi;
model_wifi_ctx.scan_list[i].encryption_mode = convert_encryption_mode(scan_result[i].encryption_mode);
}
if (model_wifi_ctx.cbs && model_wifi_ctx.cbs->on_scan_done) {
model_wifi_ctx.cbs->on_scan_done(model_wifi_ctx.scan_list, count, model_wifi_ctx.arg);
}
});
} else {
// 扫描失败
int num = ap_num;
LISA_UI_INVOKE_UI_ARG_BASE(num, {
model_wifi_ctx.scanning = 0;
model_wifi_ctx.scan_count = 0;
if (model_wifi_ctx.cbs && model_wifi_ctx.cbs->on_scan_failed) {
model_wifi_ctx.cbs->on_scan_failed(_invoke_num, model_wifi_ctx.arg);
}
});
}
}
static void wifi_disconnected_msg_handle(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
LISA_UI_INVOKE_UI_ARG_NONE({
model_wifi_ctx.status = MODEL_WIFI_STATUS_DISCONNECTED;
});
}
static void wifi_ip_got_msg_handle(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
LISA_UI_INVOKE_UI_ARG_NONE({
model_wifi_ctx.status = MODEL_WIFI_STATUS_CONNECTED;
});
}
#endif // LISA_UI_PLATFORM_ARCS
int model_wifi_init(void)
{
if (model_wifi_ctx.inited) {
return 0;
}
#ifdef LISA_UI_PLATFORM_ARCS
int ret;
ret = wifi_mgr_sta_add_connection_cb(wifi_connection_event_handler, NULL);
if (ret != 0) {
LISA_UI_LOGE("Failed to add connection callback: %d", ret);
return ret;
}
ret = wifi_mgr_add_scan_done_cb(wifi_scan_done_handler, NULL);
if (ret != 0) {
LISA_UI_LOGE("Failed to add scan done callback: %d", ret);
wifi_mgr_sta_remove_connection_cb(wifi_connection_event_handler);
return ret;
}
wifi_mgr_connection_status_t status = wifi_mgr_sta_get_status();
model_wifi_ctx.status = convert_wifi_status(status);
LISA_UI_LOGD("WiFi model initialized, status: %d", model_wifi_ctx.status);
voice_msg_sub(VOICE_MSG_WIFI_DISCONNECTED, wifi_disconnected_msg_handle, NULL);
voice_msg_sub(VOICE_MSG_WIFI_IP_GOT, wifi_ip_got_msg_handle, NULL);
#endif
model_wifi_ctx.inited = 1;
return 0;
}
int model_wifi_deinit(void)
{
if (!model_wifi_ctx.inited) {
return 0;
}
#ifdef LISA_UI_PLATFORM_ARCS
wifi_mgr_sta_remove_connection_cb(wifi_connection_event_handler);
wifi_mgr_remove_scan_done_cb(wifi_scan_done_handler);
voice_msg_unsub(VOICE_MSG_WIFI_DISCONNECTED, wifi_disconnected_msg_handle);
#endif
model_wifi_ctx.inited = 0;
model_wifi_ctx.cbs = NULL;
model_wifi_ctx.arg = NULL;
return 0;
}
int model_wifi_cb_register(const struct model_wifi_cb *cb, void *arg)
{
model_wifi_ctx.cbs = cb;
model_wifi_ctx.arg = arg;
return 0;
}
int model_wifi_cb_unregister(const struct model_wifi_cb *cb)
{
model_wifi_ctx.cbs = NULL;
model_wifi_ctx.arg = NULL;
return 0;
}
int model_wifi_scan_start(void)
{
if (!model_wifi_ctx.inited) {
LISA_UI_LOGE("WiFi model not initialized");
return -1;
}
// if (model_wifi_ctx.scanning) {
// LISA_UI_LOGW("WiFi scan already in progress");
// return -2;
// }
#ifdef LISA_UI_PLATFORM_ARCS
model_wifi_ctx.scanning = 1;
LISA_UI_INVOKE_BN_ARG_NONE({
wifi_mgr_scan_info_t ap_info[WIFI_MAX_SCAN_APS];
int ret = wifi_mgr_scan_ap(ap_info, WIFI_MAX_SCAN_APS, false);
// 扫描结果会通过回调返回
if (ret < 0) {
LISA_UI_LOGE("WiFi scan failed: %d", ret);
}
});
#else
LISA_UI_LOGW("WiFi scan not supported on this platform");
return -3;
#endif
return 0;
}
model_wifi_status_t model_wifi_get_status(void)
{
return model_wifi_ctx.status;
}
int model_wifi_connect(const char *ssid, const char *pwd, const char *bssid)
{
if (!model_wifi_ctx.inited) {
LISA_UI_LOGE("WiFi model not initialized");
return -1;
}
if (!ssid) {
LISA_UI_LOGE("SSID cannot be NULL");
return -2;
}
#ifdef LISA_UI_PLATFORM_ARCS
wifi_mgr_sta_config_t sta_config;
memset(&sta_config, 0, sizeof(sta_config));
strncpy(sta_config.ssid, ssid, sizeof(sta_config.ssid) - 1);
sta_config.ssid[sizeof(sta_config.ssid) - 1] = '\0';
if (pwd) {
strncpy(sta_config.pwd, pwd, sizeof(sta_config.pwd) - 1);
sta_config.pwd[sizeof(sta_config.pwd) - 1] = '\0';
}
if (bssid) {
strncpy(sta_config.bssid, bssid, sizeof(sta_config.bssid) - 1);
sta_config.bssid[sizeof(sta_config.bssid) - 1] = '\0';
}
sta_config.encryption_mode = WIFI_MGR_WIFI_AUTH_AUTO;
wifi_mgr_sta_config_t *config = &sta_config;
LISA_UI_INVOKE_BN_ARG_PTR(config, sizeof(sta_config), {
wifi_mgr_sta_config_t *cfg = (wifi_mgr_sta_config_t *)_invoke_config;
int ret = wifi_mgr_sta_connect(cfg, false);
if (ret != 0) {
LISA_UI_INVOKE_UI_ARG_NONE({
if (model_wifi_ctx.cbs && model_wifi_ctx.cbs->on_connection_failed) {
model_wifi_ctx.cbs->on_connection_failed(-1, model_wifi_ctx.arg);
}
});
}
});
#else
LISA_UI_LOGW("WiFi connect not supported on this platform");
return -3;
#endif
return 0;
}
int model_wifi_disconnect(void)
{
if (!model_wifi_ctx.inited) {
LISA_UI_LOGE("WiFi model not initialized");
return -1;
}
#ifdef LISA_UI_PLATFORM_ARCS
LISA_UI_INVOKE_BN_ARG_NONE({
int ret = wifi_mgr_sta_disconnect(false);
if (ret != 0) {
LISA_UI_LOGE("WiFi disconnect failed: %d", ret);
} else {
LISA_UI_LOGD("WiFi disconnected");
}
});
#else
LISA_UI_LOGW("WiFi disconnect not supported on this platform");
return -2;
#endif
return 0;
}
int model_wifi_get_connected_info(model_wifi_sta_config_t *sta_info)
{
if (!model_wifi_ctx.inited) {
LISA_UI_LOGE("WiFi model not initialized");
return -1;
}
if (!sta_info) {
LISA_UI_LOGE("sta_info cannot be NULL");
return -2;
}
#ifdef LISA_UI_PLATFORM_ARCS
wifi_mgr_sta_config_t wifi_sta_info;
int ret = wifi_mgr_sta_get_connected_info(&wifi_sta_info);
if (ret != 0) {
LISA_UI_LOGE("Failed to get WiFi connected info: %d", ret);
return ret;
}
// 转换数据结构
strncpy(sta_info->ssid, wifi_sta_info.ssid, WIFI_SSID_MAX_LEN - 1);
sta_info->ssid[WIFI_SSID_MAX_LEN - 1] = '\0';
strncpy(sta_info->bssid, wifi_sta_info.bssid, WIFI_BSSID_MAX_LEN - 1);
sta_info->bssid[WIFI_BSSID_MAX_LEN - 1] = '\0';
strncpy(sta_info->pwd, wifi_sta_info.pwd, WIFI_PWD_MAX_LEN - 1);
sta_info->pwd[WIFI_PWD_MAX_LEN - 1] = '\0';
sta_info->channel = wifi_sta_info.channel;
sta_info->rssi = wifi_sta_info.rssi;
sta_info->encryption_mode = convert_encryption_mode(wifi_sta_info.encryption_mode);
return 0;
#else
LISA_UI_LOGW("Get WiFi connected info not supported on this platform");
return -3;
#endif
}

View File

@@ -0,0 +1,73 @@
#ifndef __MODEL_WIFI_H__
#define __MODEL_WIFI_H__
#include <stdint.h>
#include <stdbool.h>
#define WIFI_SSID_MAX_LEN 32
#define WIFI_BSSID_MAX_LEN 18
#define WIFI_PWD_MAX_LEN 64
#define WIFI_MAX_SCAN_APS 20
typedef enum {
MODEL_WIFI_AUTH_AUTO = 0,
MODEL_WIFI_AUTH_OPEN,
MODEL_WIFI_AUTH_WEP,
MODEL_WIFI_AUTH_WPA_PSK,
MODEL_WIFI_AUTH_WPA2_PSK,
MODEL_WIFI_AUTH_WPA_WPA2_PSK,
MODEL_WIFI_AUTH_WPA2_ENTERPRISE,
MODEL_WIFI_AUTH_WPA3_PSK,
MODEL_WIFI_AUTH_WPA2_WPA3_PSK,
MODEL_WIFI_AUTH_UNKNOWN,
MODEL_WIFI_AUTH_MAX,
} model_wifi_encryption_mode_t;
typedef enum {
MODEL_WIFI_STATUS_CONNECTED = 0,
MODEL_WIFI_STATUS_CONNECTING,
MODEL_WIFI_STATUS_DISCONNECTED,
MODEL_WIFI_STATUS_UNKNOWN,
} model_wifi_status_t;
typedef struct {
char ssid[WIFI_SSID_MAX_LEN];
char bssid[WIFI_BSSID_MAX_LEN];
char pwd[WIFI_PWD_MAX_LEN];
int channel;
int rssi;
model_wifi_encryption_mode_t encryption_mode;
} model_wifi_sta_config_t;
typedef struct {
char ssid[WIFI_SSID_MAX_LEN];
char bssid[WIFI_BSSID_MAX_LEN];
int channel;
int rssi;
model_wifi_encryption_mode_t encryption_mode;
} model_wifi_scan_info_t;
struct model_wifi_cb {
void (*on_connected)(model_wifi_sta_config_t *sta_info, void *arg);
void (*on_disconnected)(int reason, void *arg);
void (*on_connecting)(void *arg);
void (*on_connection_failed)(int reason, void *arg);
void (*on_scan_done)(model_wifi_scan_info_t *aps_info, int ap_num, void *arg);
void (*on_scan_failed)(int reason, void *arg);
};
int model_wifi_init(void);
int model_wifi_deinit(void);
int model_wifi_cb_register(const struct model_wifi_cb *cb, void *arg);
int model_wifi_cb_unregister(const struct model_wifi_cb *cb);
int model_wifi_scan_start(void);
model_wifi_status_t model_wifi_get_status(void);
int model_wifi_connect(const char *ssid, const char *pwd, const char *bssid);
int model_wifi_disconnect(void);
int model_wifi_get_connected_info(model_wifi_sta_config_t *sta_info);
#endif

View File

@@ -0,0 +1,26 @@
target_sources(
${LISA_UI_LIB_NAME}
PRIVATE
home_presenter.c
info_presenter.c
setting_presenter.c
setting_common_presenter.c
setting_wifi_presenter.c
setting_wakeup_presenter.c
setting_clock_presenter.c
alarm_success_presenter.c
alarm_ring_presenter.c
quota_qrcode_presenter.c
)
if(CONFIG_OTA)
target_sources(${LISA_UI_LIB_NAME} PRIVATE ota_presenter.c)
endif()
target_include_directories(
${LISA_UI_LIB_NAME}
PRIVATE
./
../views/setting
../models
)

View File

@@ -0,0 +1,320 @@
/**
* @file alarm_ring_presenter.c
* @brief Alarm ring page presenter implementation
*/
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "lisa_ui_invoke.h"
#define TAG "alarm_ring_presenter"
#include "lisa_ui.h"
#include "lisa_ui_nav_scr.h"
#include "lisa_ui_nav_scr_ids.h"
#include "alarm_ring_view.h"
#include "model_alarm.h"
#include "model_voice.h"
#ifdef LISA_UI_PLATFORM_ARCS
#include "voice_msg.h"
#endif
#ifndef CONFIG_ALARM_RING_AUTO_RETURN_MS
#define CONFIG_ALARM_RING_AUTO_RETURN_MS 60000
#endif
#define ALARM_RING_AUTO_RETURN_MS CONFIG_ALARM_RING_AUTO_RETURN_MS
struct alarm_ring_nav_scr_data {
lv_obj_t *view;
lv_timer_t *auto_return;
bool iat_has_valid_text;
};
#ifdef LISA_UI_PLATFORM_ARCS
static void alarm_ring_auto_return_restart(struct alarm_ring_nav_scr_data *scr_data, const char *reason)
{
if (!scr_data || !scr_data->auto_return) {
return;
}
lv_timer_reset(scr_data->auto_return);
lv_timer_resume(scr_data->auto_return);
LISA_UI_LOGI("Alarm ring: auto return timer reset (%s)", reason ? reason : "unknown");
}
static bool alarm_iat_text_is_valid(const char *text, uint32_t len)
{
if (!text || len == 0) {
return false;
}
uint32_t n = strnlen(text, len);
for (uint32_t i = 0; i < n; i++) {
char c = text[i];
if (c != ' ' && c != '\t' && c != '\r' && c != '\n') {
return true;
}
}
return false;
}
static void alarm_ring_nav_home_ui(void *arg, uint32_t len)
{
(void)arg;
(void)len;
LISA_UI_LOGI("Alarm ring: nav_home_ui invoked, top_id=%d", lisa_ui_nav_scr_get_top_id());
if (lisa_ui_nav_scr_get_top_id() == LISA_UI_NAV_SCR_ID_ALARM_RING) {
lisa_ui_nav_scr_nav_to(LISA_UI_NAV_SCR_ID_HOME);
}
}
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;
LISA_UI_LOGI("Alarm ring: wakeup event, navigating to home");
lisa_ui_invoke_ui_delayed(alarm_ring_nav_home_ui, NULL, 0, 0);
}
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;
LISA_UI_LOGI("Alarm ring: button change, navigating to home");
lisa_ui_invoke_ui_delayed(alarm_ring_nav_home_ui, NULL, 0, 0);
}
static void alarm_ring_on_iat(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
(void)unused;
struct alarm_ring_nav_scr_data *scr_data = (struct alarm_ring_nav_scr_data *)user_data;
if (!scr_data) {
return;
}
if (msg_id == VOICE_MSG_CLOUD_IAT_START) {
scr_data->iat_has_valid_text = false;
return;
}
if (msg_id == VOICE_MSG_CLOUD_IAT_UPDATE) {
if (alarm_iat_text_is_valid((const char *)data, len)) {
scr_data->iat_has_valid_text = true;
LISA_UI_LOGI("Alarm ring: iat valid text, navigating to home (len=%u)", (unsigned int)len);
lisa_ui_invoke_ui_delayed(alarm_ring_nav_home_ui, NULL, 0, 0);
}
return;
}
if (msg_id == VOICE_MSG_CLOUD_IAT_END) {
scr_data->iat_has_valid_text = false;
}
}
static void alarm_ring_on_alarm_trigger(void *unused, uint32_t msg_id, void *data, uint32_t len, void *user_data)
{
(void)unused;
(void)msg_id;
(void)data;
(void)len;
struct alarm_ring_nav_scr_data *scr_data = (struct alarm_ring_nav_scr_data *)user_data;
if (!scr_data) {
return;
}
LISA_UI_INVOKE_UI_ARG_BASE(scr_data, {
alarm_ring_auto_return_restart(_invoke_scr_data, "alarm_trigger");
});
}
#endif
static void format_alarm_time(const alarm_time_info_t *info, char *time_str, size_t time_size,
char *date_str, size_t date_size)
{
snprintf(time_str, time_size, "%02d:%02d", info->hour, info->minute);
const char *weekdays[] = {"周日", "周一", "周二", "周三", "周四", "周五", "周六"};
const char *suffix = "";
if (info->is_today) {
suffix = "(今天)";
} else if (info->is_tomorrow) {
suffix = "(明天)";
}
snprintf(date_str, date_size, "%d月%d日 %s%s",
info->month, info->day, weekdays[info->weekday], suffix);
}
static void stop_btn_event_cb(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
LISA_UI_LOGI("Alarm ring: stop button clicked, navigating to home");
lisa_ui_nav_scr_nav_to(LISA_UI_NAV_SCR_ID_HOME);
}
}
static void alarm_ring_auto_return_cb(lv_timer_t *timer)
{
(void)timer;
LISA_UI_LOGI("Alarm ring: auto return timer, navigating to home");
lisa_ui_nav_scr_nav_to(LISA_UI_NAV_SCR_ID_HOME);
}
static int alarm_ring_nav_scr_open(const struct lisa_ui_nav_scr *scr, void **data)
{
LISA_UI_LOGD("Alarm ring nav scr open, id: %d", scr->unique_id);
struct alarm_ring_nav_scr_data *scr_data = lisa_ui_malloc(sizeof(struct alarm_ring_nav_scr_data));
if (!scr_data) {
LISA_UI_LOGE("Failed to allocate memory");
return -1;
}
memset(scr_data, 0, sizeof(struct alarm_ring_nav_scr_data));
scr_data->view = alarm_ring_view_create(lv_scr_act());
if (!scr_data->view) {
LISA_UI_LOGE("Failed to create alarm ring view");
lisa_ui_free(scr_data);
return -1;
}
alarm_data_t alarm_data;
if (model_alarm_get_data(&alarm_data) == 0) {
alarm_time_info_t time_info;
if (model_alarm_get_time_info(alarm_data.timestamp, &time_info) == 0) {
char time_str[16];
char date_str[32];
format_alarm_time(&time_info, time_str, sizeof(time_str), date_str, sizeof(date_str));
alarm_ring_view_set_time(scr_data->view, time_str);
alarm_ring_view_set_date(scr_data->view, date_str);
LISA_UI_LOGI("Alarm ring page - time: %s, date: %s", time_str, date_str);
} else {
LISA_UI_LOGW("Failed to get time info");
}
} else {
LISA_UI_LOGW("No alarm data available, using default");
}
alarm_ring_view_set_stop_cb(scr_data->view, stop_btn_event_cb, NULL);
scr_data->auto_return = lv_timer_create(alarm_ring_auto_return_cb, ALARM_RING_AUTO_RETURN_MS, NULL);
if (!scr_data->auto_return) {
LISA_UI_LOGE("Failed to create auto return timer");
lv_obj_del(scr_data->view);
lisa_ui_free(scr_data);
return -1;
}
lv_timer_set_repeat_count(scr_data->auto_return, 1);
#ifdef LISA_UI_PLATFORM_ARCS
voice_msg_sub(VOICE_MSG_ALARM_TRIGGER, alarm_ring_on_alarm_trigger, scr_data);
voice_msg_sub(VOICE_MSG_WAKEUP_KEYWORD, alarm_ring_on_wakeup, scr_data);
voice_msg_sub(VOICE_MSG_WAKEUP_COMMAND, alarm_ring_on_wakeup, scr_data);
voice_msg_sub(VOICE_MSG_BUTTON_CHANGE, alarm_ring_on_button_change, scr_data);
voice_msg_sub(VOICE_MSG_CLOUD_IAT_START, alarm_ring_on_iat, scr_data);
voice_msg_sub(VOICE_MSG_CLOUD_IAT_UPDATE, alarm_ring_on_iat, scr_data);
voice_msg_sub(VOICE_MSG_CLOUD_IAT_END, alarm_ring_on_iat, scr_data);
#endif
*data = scr_data;
return 0;
}
static int alarm_ring_nav_scr_show(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("Alarm ring nav scr show, id: %d", scr->unique_id);
struct alarm_ring_nav_scr_data *scr_data = (struct alarm_ring_nav_scr_data *)data;
if (!scr_data || !scr_data->view) {
LISA_UI_LOGE("Invalid alarm ring nav scr data");
return -1;
}
lv_obj_clear_flag(scr_data->view, LV_OBJ_FLAG_HIDDEN);
return 0;
}
static int alarm_ring_nav_scr_pause(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("Alarm ring nav scr pause, id: %d", scr->unique_id);
struct alarm_ring_nav_scr_data *scr_data = (struct alarm_ring_nav_scr_data *)data;
if (scr_data && scr_data->view) {
lv_obj_add_flag(scr_data->view, LV_OBJ_FLAG_HIDDEN);
}
return 0;
}
static int alarm_ring_nav_scr_resume(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("Alarm ring nav scr resume, id: %d", scr->unique_id);
struct alarm_ring_nav_scr_data *scr_data = (struct alarm_ring_nav_scr_data *)data;
if (scr_data && scr_data->view) {
lv_obj_clear_flag(scr_data->view, LV_OBJ_FLAG_HIDDEN);
}
return 0;
}
static int alarm_ring_nav_scr_close(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("Alarm ring nav scr close, id: %d", scr->unique_id);
struct alarm_ring_nav_scr_data *scr_data = (struct alarm_ring_nav_scr_data *)data;
if (scr_data) {
#ifdef LISA_UI_PLATFORM_ARCS
voice_msg_unsub(VOICE_MSG_WAKEUP_KEYWORD, alarm_ring_on_wakeup);
voice_msg_unsub(VOICE_MSG_WAKEUP_COMMAND, alarm_ring_on_wakeup);
voice_msg_unsub(VOICE_MSG_BUTTON_CHANGE, alarm_ring_on_button_change);
voice_msg_unsub(VOICE_MSG_CLOUD_IAT_START, alarm_ring_on_iat);
voice_msg_unsub(VOICE_MSG_CLOUD_IAT_UPDATE, alarm_ring_on_iat);
voice_msg_unsub(VOICE_MSG_CLOUD_IAT_END, alarm_ring_on_iat);
voice_msg_unsub(VOICE_MSG_ALARM_TRIGGER, alarm_ring_on_alarm_trigger);
#endif
if (scr_data->auto_return) {
lv_timer_del(scr_data->auto_return);
scr_data->auto_return = NULL;
}
if (scr_data->view) {
lv_obj_del(scr_data->view);
}
lisa_ui_free(scr_data);
}
return 0;
}
const struct lisa_ui_nav_scr alarm_ring_nav_scr = {
.unique_id = LISA_UI_NAV_SCR_ID_ALARM_RING,
.open = alarm_ring_nav_scr_open,
.show = alarm_ring_nav_scr_show,
.pause = alarm_ring_nav_scr_pause,
.resume = alarm_ring_nav_scr_resume,
.close = alarm_ring_nav_scr_close,
};

View File

@@ -0,0 +1,161 @@
/**
* @file alarm_success_presenter.c
* @brief Alarm success page presenter implementation
*/
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#define TAG "alarm_success_presenter"
#include "lisa_ui.h"
#include "lisa_ui_nav_scr.h"
#include "lisa_ui_nav_scr_ids.h"
#include "alarm_success_view.h"
#include "model_alarm.h"
struct alarm_success_nav_scr_data {
lv_obj_t *view;
lv_timer_t *auto_return;
};
static void format_alarm_time(const alarm_time_info_t *info, char *time_str, size_t time_size,
char *date_str, size_t date_size)
{
snprintf(time_str, time_size, "%02d:%02d", info->hour, info->minute);
const char *weekdays[] = {"周日", "周一", "周二", "周三", "周四", "周五", "周六"};
const char *suffix = "";
if (info->is_today) {
suffix = "(今天)";
} else if (info->is_tomorrow) {
suffix = "(明天)";
}
snprintf(date_str, date_size, "%d月%d日 %s%s",
info->month, info->day, weekdays[info->weekday], suffix);
}
static void auto_return_timer_cb(lv_timer_t *timer)
{
LISA_UI_LOGD("Auto return timer triggered, navigating to home");
lisa_ui_nav_scr_nav_to(LISA_UI_NAV_SCR_ID_HOME);
}
static int alarm_success_nav_scr_open(const struct lisa_ui_nav_scr *scr, void **data)
{
LISA_UI_LOGD("Alarm success nav scr open, id: %d", scr->unique_id);
struct alarm_success_nav_scr_data *scr_data = lisa_ui_malloc(sizeof(struct alarm_success_nav_scr_data));
if (!scr_data) {
LISA_UI_LOGE("Failed to allocate memory");
return -1;
}
scr_data->view = alarm_success_view_create(lv_scr_act());
if (!scr_data->view) {
LISA_UI_LOGE("Failed to create alarm success view");
lisa_ui_free(scr_data);
return -1;
}
/* 先隐藏view避免在设置文本时闪烁 */
lv_obj_add_flag(scr_data->view, LV_OBJ_FLAG_HIDDEN);
alarm_data_t alarm_data;
if (model_alarm_get_data(&alarm_data) == 0) {
alarm_time_info_t time_info;
if (model_alarm_get_time_info(alarm_data.timestamp, &time_info) == 0) {
char time_str[16];
char date_str[32];
format_alarm_time(&time_info, time_str, sizeof(time_str), date_str, sizeof(date_str));
alarm_success_view_set_time(scr_data->view, time_str);
alarm_success_view_set_date(scr_data->view, date_str);
LISA_UI_LOGI("Alarm success page - time: %s, date: %s", time_str, date_str);
} else {
LISA_UI_LOGW("Failed to get time info");
}
} else {
LISA_UI_LOGW("No alarm data available, using default");
}
/* 数据设置完成后立即显示避免在show时才显示导致闪烁 */
lv_obj_clear_flag(scr_data->view, LV_OBJ_FLAG_HIDDEN);
scr_data->auto_return = lv_timer_create(auto_return_timer_cb, 3000, NULL);
lv_timer_set_repeat_count(scr_data->auto_return, 1);
*data = scr_data;
return 0;
}
static int alarm_success_nav_scr_show(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("Alarm success nav scr show, id: %d", scr->unique_id);
struct alarm_success_nav_scr_data *scr_data = (struct alarm_success_nav_scr_data *)data;
if (!scr_data || !scr_data->view) {
LISA_UI_LOGE("Invalid alarm success nav scr data");
return -1;
}
/* view已经在open时显示这里不需要再操作 */
return 0;
}
static int alarm_success_nav_scr_pause(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("Alarm success nav scr pause, id: %d", scr->unique_id);
struct alarm_success_nav_scr_data *scr_data = (struct alarm_success_nav_scr_data *)data;
if (scr_data && scr_data->view) {
lv_obj_add_flag(scr_data->view, LV_OBJ_FLAG_HIDDEN);
}
return 0;
}
static int alarm_success_nav_scr_resume(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("Alarm success nav scr resume, id: %d", scr->unique_id);
struct alarm_success_nav_scr_data *scr_data = (struct alarm_success_nav_scr_data *)data;
if (scr_data && scr_data->view) {
lv_obj_clear_flag(scr_data->view, LV_OBJ_FLAG_HIDDEN);
}
return 0;
}
static int alarm_success_nav_scr_close(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("Alarm success nav scr close, id: %d", scr->unique_id);
struct alarm_success_nav_scr_data *scr_data = (struct alarm_success_nav_scr_data *)data;
if (scr_data) {
if (scr_data->auto_return) {
lv_timer_del(scr_data->auto_return);
scr_data->auto_return = NULL;
}
if (scr_data->view) {
lv_obj_del(scr_data->view);
}
lisa_ui_free(scr_data);
}
return 0;
}
const struct lisa_ui_nav_scr alarm_success_nav_scr = {
.unique_id = LISA_UI_NAV_SCR_ID_ALARM_SUCCESS,
.open = alarm_success_nav_scr_open,
.show = alarm_success_nav_scr_show,
.pause = alarm_success_nav_scr_pause,
.resume = alarm_success_nav_scr_resume,
.close = alarm_success_nav_scr_close,
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,206 @@
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#define TAG "info_presenter"
#include "lisa_ui.h"
#include "lisa_ui_nav_scr.h"
#include "lisa_ui_nav_scr_ids.h"
#include "info_view.h"
#include "model_qrcode.h"
#ifdef LISA_UI_PLATFORM_ARCS
#include "lisa_ui_invoke.h"
#include "voice_msg.h"
#endif
struct info_nav_scr_data {
lv_obj_t *view; /**< Info view object */
lv_timer_t *auto_return; /**< Auto return timer */
lv_timer_t *retry_qr; /**< QR data retry timer */
bool wait_wifi_connected;
};
static void auto_return_timer_cb(lv_timer_t *timer)
{
LISA_UI_LOGD("Auto return timer triggered");
lisa_ui_nav_scr_nav_back();
}
static void retry_qr_timer_cb(lv_timer_t *timer)
{
LISA_UI_LOGD("Retry QR timer triggered");
struct info_nav_scr_data *scr_data = (struct info_nav_scr_data *)timer->user_data;
if (!scr_data || !scr_data->view) {
LISA_UI_LOGE("Invalid scr_data in retry timer");
return;
}
qrcode_data_t qr_data;
if (model_qrcode_get_config_data(&qr_data) == 0) {
lisa_ui_info_view_set_top_text(scr_data->view, qr_data.top_text);
lisa_ui_info_view_set_bottom_text(scr_data->view, qr_data.bottom_text);
if (qr_data.qr_image) {
lisa_ui_info_view_set_qr_image(scr_data->view, qr_data.qr_image);
if (scr_data->retry_qr) {
lv_timer_del(scr_data->retry_qr);
scr_data->retry_qr = NULL;
}
}
} else {
LISA_UI_LOGE("Failed to get QR data on retry, will retry again");
}
}
#ifdef LISA_UI_PLATFORM_ARCS
static void wifi_ip_got_msg_handle(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;
LISA_UI_INVOKE_UI_ARG_NONE({
if (lisa_ui_nav_scr_get_top_id() == LISA_UI_NAV_SCR_ID_INFO) {
lisa_ui_nav_scr_nav_back();
}
});
}
#endif
static int info_nav_scr_open(const struct lisa_ui_nav_scr *scr, void **data)
{
LISA_UI_LOGD("Info nav scr open, id: %d", scr->unique_id);
struct info_nav_scr_data *scr_data = lisa_ui_malloc(sizeof(struct info_nav_scr_data));
if (!scr_data) {
LISA_UI_LOGE("Failed to allocate memory for info_nav_scr_data");
return -1;
}
memset(scr_data, 0, sizeof(struct info_nav_scr_data));
scr_data->view = lisa_ui_info_view_create(lv_scr_act());
if (!scr_data->view) {
LISA_UI_LOGE("Failed to create info view");
lisa_ui_free(scr_data);
return -1;
}
qrcode_data_t qr_data;
if (model_qrcode_get_config_data(&qr_data) == 0) {
lisa_ui_info_view_set_top_text(scr_data->view, qr_data.top_text);
lisa_ui_info_view_set_bottom_text(scr_data->view, qr_data.bottom_text);
scr_data->wait_wifi_connected = qr_data.status == QR_STATUS_NOT_CONNECTED;
if (qr_data.qr_image) {
lisa_ui_info_view_set_qr_image(scr_data->view, qr_data.qr_image);
} else {
LISA_UI_LOGD("QR image not ready, creating retry timer");
scr_data->retry_qr = lv_timer_create(retry_qr_timer_cb, 100, scr_data);
}
} else {
LISA_UI_LOGE("Failed to get QR data from model");
}
if (scr_data->wait_wifi_connected) {
#ifdef LISA_UI_PLATFORM_ARCS
voice_msg_sub(VOICE_MSG_WIFI_IP_GOT, wifi_ip_got_msg_handle, NULL);
#endif
} else {
#ifdef CONFIG_BOARD_ARCS_MINI
scr_data->auto_return = lv_timer_create(auto_return_timer_cb, 30000, NULL);
#else // !CONFIG_BOARD_ARCS_MINI
scr_data->auto_return = lv_timer_create(auto_return_timer_cb, 10000, NULL);
#endif
lv_timer_set_repeat_count(scr_data->auto_return, 1);
}
*data = scr_data;
return 0;
}
static int info_nav_scr_show(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("Info nav scr show, id: %d", scr->unique_id);
struct info_nav_scr_data *scr_data = (struct info_nav_scr_data *)data;
if (!scr_data || !scr_data->view) {
LISA_UI_LOGE("Invalid info nav scr data");
return -1;
}
lv_obj_clear_flag(scr_data->view, LV_OBJ_FLAG_HIDDEN);
return 0;
}
static int info_nav_scr_pause(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("Info nav scr pause, id: %d", scr->unique_id);
struct info_nav_scr_data *scr_data = (struct info_nav_scr_data *)data;
if (scr_data && scr_data->view) {
lv_obj_add_flag(scr_data->view, LV_OBJ_FLAG_HIDDEN);
}
return 0;
}
static int info_nav_scr_resume(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("Info nav scr resume, id: %d", scr->unique_id);
struct info_nav_scr_data *scr_data = (struct info_nav_scr_data *)data;
if (scr_data && scr_data->view) {
lv_obj_clear_flag(scr_data->view, LV_OBJ_FLAG_HIDDEN);
}
return 0;
}
static int info_nav_scr_close(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("Info nav scr close, id: %d", scr->unique_id);
struct info_nav_scr_data *scr_data = (struct info_nav_scr_data *)data;
if (scr_data) {
if (scr_data->wait_wifi_connected) {
#ifdef LISA_UI_PLATFORM_ARCS
voice_msg_unsub(VOICE_MSG_WIFI_IP_GOT, wifi_ip_got_msg_handle);
#endif
}
if (scr_data->auto_return) {
lv_timer_del(scr_data->auto_return);
scr_data->auto_return = NULL;
}
if (scr_data->retry_qr) {
lv_timer_del(scr_data->retry_qr);
scr_data->retry_qr = NULL;
}
if (scr_data->view) {
lv_obj_del(scr_data->view);
}
lisa_ui_free(scr_data);
}
return 0;
}
const struct lisa_ui_nav_scr info_nav_scr = {
.unique_id = LISA_UI_NAV_SCR_ID_INFO,
.open = info_nav_scr_open,
.show = info_nav_scr_show,
.pause = info_nav_scr_pause,
.resume = info_nav_scr_resume,
.close = info_nav_scr_close,
};

View File

@@ -0,0 +1,91 @@
#define LOG_TAG "ota_presenter"
#include "lisa_ui.h"
#include "lisa_ui_nav_scr.h"
#include "lisa_ui_nav_scr_ids.h"
#include "model_ota.h"
#include "ota_manager.h"
#include "ota_view.h"
struct ota_nav_scr_data {
lv_obj_t *view;
ota_state_t state;
};
static void model_ota_on_state_change(const ota_state_t *state, void *arg);
const struct model_ota_cb model_ota_cbs = {
.on_ota_state_change = model_ota_on_state_change,
};
static void model_ota_on_state_change(const ota_state_t *state, void *arg)
{
struct ota_nav_scr_data *scr_data = (struct ota_nav_scr_data *)arg;
if (!scr_data || !scr_data->view || !state) {
return;
}
memcpy(&scr_data->state, state, sizeof(ota_state_t));
lisa_ui_ota_view_update(scr_data->view, &scr_data->state);
}
static int ota_nav_scr_open(const struct lisa_ui_nav_scr *scr, void **data)
{
LISA_UI_LOGD("nav scr open, id: %d", scr->unique_id);
model_ota_init();
struct ota_nav_scr_data *scr_data = lisa_ui_malloc(sizeof(struct ota_nav_scr_data));
if (!scr_data) {
return -1;
}
memset(scr_data, 0, sizeof(struct ota_nav_scr_data));
scr_data->view = lisa_ui_ota_view_create(lv_scr_act());
*data = scr_data;
model_ota_cb_register(&model_ota_cbs, scr_data);
return 0;
}
static int ota_nav_scr_show(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("nav scr show, id: %d", scr->unique_id);
struct ota_nav_scr_data *scr_data = (struct ota_nav_scr_data *)data;
if (!scr_data || !scr_data->view) {
return -1;
}
lv_obj_clear_flag(scr_data->view, LV_OBJ_FLAG_HIDDEN);
return 0;
}
static int ota_nav_scr_close(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("nav scr close, id: %d", scr->unique_id);
model_ota_cb_unregister(&model_ota_cbs);
struct ota_nav_scr_data *scr_data = (struct ota_nav_scr_data *)data;
if (scr_data) {
if (scr_data->view) {
lv_obj_del(scr_data->view);
}
lisa_ui_free(scr_data);
}
return 0;
}
const struct lisa_ui_nav_scr ota_nav_scr = {
.unique_id = LISA_UI_NAV_SCR_ID_OTA,
.open = ota_nav_scr_open,
.show = ota_nav_scr_show,
.close = ota_nav_scr_close,
};

View File

@@ -0,0 +1,96 @@
#include "quota_qrcode_presenter.h"
#include "quota_qrcode_view.h"
#include "model_qrcode.h"
#include "lisa_ui.h"
#include "lisa_ui_nav_scr_ids.h"
#include "lvgl.h"
#define TAG "quota_qrcode_presenter"
struct quota_qrcode_nav_scr_data {
lv_obj_t *view;
lv_timer_t *retry_qr;
lv_timer_t *auto_return;
};
static void retry_qr_timer_cb(lv_timer_t *timer)
{
struct quota_qrcode_nav_scr_data *scr_data = timer->user_data;
qrcode_data_t qr_data;
if (model_qrcode_get_quota_data(&qr_data) == 0) {
lisa_ui_quota_qrcode_view_set_title(scr_data->view, qr_data.top_text);
lisa_ui_quota_qrcode_view_set_message(scr_data->view, qr_data.bottom_text);
if (qr_data.qr_image) {
lisa_ui_quota_qrcode_view_set_qr_image(scr_data->view, qr_data.qr_image);
lv_timer_del(timer);
scr_data->retry_qr = NULL;
LISA_UI_LOGI("QR code image loaded successfully");
}
}
}
static void auto_return_timer_cb(lv_timer_t *timer)
{
LISA_UI_LOGI("Auto return to home page");
lisa_ui_nav_scr_nav_back();
}
static int quota_qrcode_nav_scr_open(const struct lisa_ui_nav_scr *scr, void **data)
{
struct quota_qrcode_nav_scr_data *scr_data = lisa_ui_malloc(sizeof(struct quota_qrcode_nav_scr_data));
memset(scr_data, 0, sizeof(struct quota_qrcode_nav_scr_data));
scr_data->view = lisa_ui_quota_qrcode_view_create(lv_scr_act());
qrcode_data_t qr_data;
if (model_qrcode_get_quota_data(&qr_data) == 0) {
lisa_ui_quota_qrcode_view_set_title(scr_data->view, qr_data.top_text);
lisa_ui_quota_qrcode_view_set_message(scr_data->view, qr_data.bottom_text);
if (qr_data.qr_image) {
lisa_ui_quota_qrcode_view_set_qr_image(scr_data->view, qr_data.qr_image);
LISA_UI_LOGI("QR code image set immediately");
} else {
scr_data->retry_qr = lv_timer_create(retry_qr_timer_cb, 100, scr_data);
LISA_UI_LOGI("QR code image not ready, will retry");
}
} else {
LISA_UI_LOGE("Failed to get QR code data");
}
scr_data->auto_return = lv_timer_create(auto_return_timer_cb, 10000, NULL);
lv_timer_set_repeat_count(scr_data->auto_return, 1);
*data = scr_data;
LISA_UI_LOGI("Quota QR code page opened");
return 0;
}
static int quota_qrcode_nav_scr_close(const struct lisa_ui_nav_scr *scr, void *data)
{
struct quota_qrcode_nav_scr_data *scr_data = data;
if (scr_data->retry_qr) {
lv_timer_del(scr_data->retry_qr);
}
if (scr_data->auto_return) {
lv_timer_del(scr_data->auto_return);
}
if (scr_data->view) {
lv_obj_del(scr_data->view);
}
lisa_ui_free(scr_data);
LISA_UI_LOGI("Quota QR code page closed");
return 0;
}
const struct lisa_ui_nav_scr quota_qrcode_nav_scr = {
.unique_id = LISA_UI_NAV_SCR_ID_QUOTA_QRCODE,
.open = quota_qrcode_nav_scr_open,
.close = quota_qrcode_nav_scr_close,
};

View File

@@ -0,0 +1,8 @@
#ifndef LISA_UI_QUOTA_QRCODE_PRESENTER_H
#define LISA_UI_QUOTA_QRCODE_PRESENTER_H
#include "lisa_ui_nav_scr.h"
extern const struct lisa_ui_nav_scr quota_qrcode_nav_scr;
#endif

View File

@@ -0,0 +1,335 @@
/**
* @file setting_clock_presenter.c
* @brief Alarm/Clock settings presenter - handles data logic and navigation
*/
#include "lisa_ui_nav_scr.h"
#include "lisa_ui.h"
#include "lisa_ui_nav_scr_ids.h"
#include "lisa_ui_fonts.h"
#include "setting_clock_view.h"
#include "model_alarm.h"
#include "model_voice.h"
#include <stdbool.h>
#define TAG "setting_clock_presenter"
static bool alarm_navigation_initialized = false;
struct setting_clock_nav_scr_data {
lv_obj_t *view;
lv_obj_t *confirm_dialog;
uint64_t pending_delete_timestamp;
};
static struct setting_clock_nav_scr_data *g_current_scr_data = NULL;
static void on_delete_alarm(uint64_t timestamp);
static void create_delete_confirm_dialog(struct setting_clock_nav_scr_data *scr_data, uint64_t timestamp);
/**
* @brief 返回按钮回调 - 处理返回到设置主页面的导航
* 这是presenter层的业务逻辑
*/
static void setting_clock_back_btn_cb(void *user_data)
{
(void)user_data;
LISA_UI_LOGD("Clock settings back button clicked, returning to settings");
lisa_ui_nav_scr_nav_back();
}
static int setting_clock_nav_scr_open(const struct lisa_ui_nav_scr *scr, void **data)
{
(void)scr;
LISA_UI_LOGD("Clock/Alarm settings nav scr open");
struct setting_clock_nav_scr_data *scr_data =
(struct setting_clock_nav_scr_data *)lisa_ui_malloc(sizeof(struct setting_clock_nav_scr_data));
if (!scr_data) {
return -1;
}
memset(scr_data, 0, sizeof(struct setting_clock_nav_scr_data));
// Create clock view (UI layout handled by view)
scr_data->view = lisa_ui_setting_clock_view_create(lv_scr_act());
if (!scr_data->view) {
LISA_UI_LOGE("Failed to create clock view");
lisa_ui_free(scr_data);
return -1;
}
/* 注册返回按钮回调 */
lisa_ui_setting_clock_view_set_back_cb(scr_data->view, setting_clock_back_btn_cb, NULL);
/* 注册删除回调 */
lisa_ui_setting_clock_view_set_delete_cb(scr_data->view, on_delete_alarm);
/* 设置全局变量 */
g_current_scr_data = scr_data;
/* 初始化刷新闹钟列表 */
lisa_ui_setting_clock_view_refresh_list(scr_data->view);
*data = scr_data;
return 0;
}
extern int ls_alarm_delete_by_timestamp(uint64_t timestamp);
static void delete_confirm_btn_event_cb(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
lv_obj_t *btn = lv_event_get_target(e);
struct setting_clock_nav_scr_data *scr_data = (struct setting_clock_nav_scr_data *)lv_event_get_user_data(e);
if (!scr_data) {
return;
}
lv_obj_t *label = lv_obj_get_child(btn, 0);
const char *btn_text = lv_label_get_text(label);
if (strcmp(btn_text, "删除") == 0) {
if (scr_data->pending_delete_timestamp > 0) {
LISA_UI_LOGD("Deleting alarm with timestamp: %llu",
(unsigned long long)scr_data->pending_delete_timestamp);
int result = model_alarm_delete_by_timestamp(scr_data->pending_delete_timestamp);
if (result == 0) {
LISA_UI_LOGD("Alarm deletion successful");
if (scr_data->confirm_dialog) {
lv_obj_del_async(scr_data->confirm_dialog);
scr_data->confirm_dialog = NULL;
}
if (scr_data->view) {
/* 重新获取最新的闹钟列表数据 */
uint32_t cnt;
alarm_data_t *alarms = model_alarm_get_all(&cnt);
if (alarms && cnt > 0) {
alarm_item_t *alarm_items = lisa_ui_malloc(cnt * sizeof(alarm_item_t));
if (alarm_items) {
for (int i = 0; i < cnt; i++) {
alarm_items[i].timestamp = alarms[i].timestamp;
}
lisa_ui_setting_clock_view_set_alarms(scr_data->view, alarm_items, cnt);
lisa_ui_free(alarm_items);
}
model_alarm_free(alarms);
} else {
/* 如果没有闹钟了,设置空列表 */
lisa_ui_setting_clock_view_set_alarms(scr_data->view, NULL, 0);
}
/* 刷新UI显示 */
lisa_ui_setting_clock_view_refresh_list(scr_data->view);
}
} else {
LISA_UI_LOGE("Failed to delete alarm, result: %d", result);
if (scr_data->confirm_dialog) {
lv_obj_del_async(scr_data->confirm_dialog);
scr_data->confirm_dialog = NULL;
}
}
}
} else {
LISA_UI_LOGD("Delete cancelled by user");
if (scr_data->confirm_dialog) {
lv_obj_del_async(scr_data->confirm_dialog);
scr_data->confirm_dialog = NULL;
}
}
scr_data->pending_delete_timestamp = 0;
}
}
static void create_delete_confirm_dialog(struct setting_clock_nav_scr_data *scr_data, uint64_t timestamp)
{
if (!scr_data) {
return;
}
lv_obj_t *bg = lv_obj_create(lv_scr_act());
lv_obj_set_size(bg, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_bg_color(bg, lv_color_make(0, 0, 0), 0);
lv_obj_set_style_bg_opa(bg, LV_OPA_50, 0);
lv_obj_set_style_border_width(bg, 0, 0);
lv_obj_center(bg);
lv_obj_t *dialog = lv_obj_create(bg);
lv_obj_set_size(dialog, 280, 160);
lv_obj_set_style_bg_color(dialog, lv_color_make(0x2A, 0x2A, 0x2A), 0);
lv_obj_set_style_border_width(dialog, 0, 0);
lv_obj_set_style_radius(dialog, 12, 0);
lv_obj_center(dialog);
lv_obj_t *title_label = lv_label_create(dialog);
lv_label_set_text(title_label, "是否删除该闹钟");
lv_obj_set_style_text_color(title_label, lv_color_white(), 0);
lv_obj_set_style_text_font(title_label, &lv_font_chinese_16, 0);
lv_obj_align(title_label, LV_ALIGN_TOP_MID, 0, 30);
lv_obj_t *btn_container = lv_obj_create(dialog);
lv_obj_set_size(btn_container, LV_PCT(90), 50);
lv_obj_set_style_bg_opa(btn_container, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(btn_container, 0, 0);
lv_obj_set_style_pad_all(btn_container, 0, 0);
lv_obj_align(btn_container, LV_ALIGN_BOTTOM_MID, 0, -20);
lv_obj_set_flex_flow(btn_container, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(btn_container, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t *cancel_btn = lv_btn_create(btn_container);
lv_obj_set_size(cancel_btn, 100, 40);
lv_obj_set_style_bg_color(cancel_btn, lv_color_make(0x55, 0x55, 0x55), 0);
lv_obj_set_style_border_width(cancel_btn, 0, 0);
lv_obj_set_style_radius(cancel_btn, 8, 0);
lv_obj_t *cancel_label = lv_label_create(cancel_btn);
lv_label_set_text(cancel_label, "取消");
lv_obj_set_style_text_color(cancel_label, lv_color_white(), 0);
lv_obj_set_style_text_font(cancel_label, &lv_font_chinese_16, 0);
lv_obj_center(cancel_label);
lv_obj_t *delete_btn = lv_btn_create(btn_container);
lv_obj_set_size(delete_btn, 100, 40);
lv_obj_set_style_bg_color(delete_btn, lv_color_make(0xFF, 0x44, 0x44), 0);
lv_obj_set_style_border_width(delete_btn, 0, 0);
lv_obj_set_style_radius(delete_btn, 8, 0);
lv_obj_t *delete_label = lv_label_create(delete_btn);
lv_label_set_text(delete_label, "删除");
lv_obj_set_style_text_color(delete_label, lv_color_white(), 0);
lv_obj_set_style_text_font(delete_label, &lv_font_chinese_16, 0);
lv_obj_center(delete_label);
lv_obj_add_event_cb(cancel_btn, delete_confirm_btn_event_cb, LV_EVENT_CLICKED, scr_data);
lv_obj_add_event_cb(delete_btn, delete_confirm_btn_event_cb, LV_EVENT_CLICKED, scr_data);
scr_data->confirm_dialog = bg;
scr_data->pending_delete_timestamp = timestamp;
LISA_UI_LOGD("Created delete confirmation dialog for timestamp: %llu", (unsigned long long)timestamp);
}
static void on_delete_alarm(uint64_t timestamp)
{
if (g_current_scr_data) {
create_delete_confirm_dialog(g_current_scr_data, timestamp);
}
}
static int setting_clock_nav_scr_show(const struct lisa_ui_nav_scr *scr, void *data)
{
(void)scr;
struct setting_clock_nav_scr_data *scr_data = (struct setting_clock_nav_scr_data *)data;
if (!scr_data || !scr_data->view) {
return -1;
}
lv_obj_clear_flag(scr_data->view, LV_OBJ_FLAG_HIDDEN);
uint32_t cnt;
alarm_data_t *alarms = model_alarm_get_all(&cnt);
if (alarms && cnt) {
alarm_item_t *alarm_items = lisa_ui_malloc(cnt * sizeof(alarm_item_t));
if (alarm_items) {
for (int i = 0; i < cnt; i++) {
alarm_items[i].timestamp = alarms[i].timestamp;
LISA_UI_LOGI("alarm item: %llu", alarm_items[i].timestamp);
}
lisa_ui_setting_clock_view_set_alarms(scr_data->view, alarm_items, cnt);
lisa_ui_setting_clock_view_refresh_list(scr_data->view);
lisa_ui_free(alarm_items);
}
model_alarm_free(alarms);
}
return 0;
}
static int setting_clock_nav_scr_close(const struct lisa_ui_nav_scr *scr, void *data)
{
(void)scr;
struct setting_clock_nav_scr_data *scr_data = (struct setting_clock_nav_scr_data *)data;
if (scr_data) {
if (scr_data->confirm_dialog) {
lv_obj_del(scr_data->confirm_dialog);
scr_data->confirm_dialog = NULL;
}
if (scr_data->view) {
lv_obj_del(scr_data->view);
scr_data->view = NULL;
}
/* 清空全局变量 */
if (g_current_scr_data == scr_data) {
g_current_scr_data = NULL;
}
lisa_ui_free(scr_data);
}
return 0;
}
const struct lisa_ui_nav_scr setting_clock_nav_scr = {
.unique_id = LISA_UI_NAV_SCR_ID_SETTING_CLOCK,
.open = setting_clock_nav_scr_open,
.show = setting_clock_nav_scr_show,
.close = setting_clock_nav_scr_close,
};
/**
* @brief 启用语音唤醒Presenter订阅Model的语音控制指令
*/
static void enable_voice_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;
LISA_UI_LOGI("Presenter: Executing voice wakeup enable");
model_voice_on();
}
/**
* @brief 禁用语音唤醒Presenter订阅Model的语音控制指令
*/
static void disable_voice_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;
LISA_UI_LOGI("Presenter: Executing voice wakeup disable");
model_voice_off();
}
/**
* @brief 初始化闹钟导航处理器Presenter订阅Model的导航指令
*/
void alarm_navigation_init(void)
{
if (alarm_navigation_initialized) {
LISA_UI_LOGI("Alarm navigation handler already initialized");
return;
}
LISA_UI_LOGI("Initializing alarm navigation handler");
alarm_navigation_initialized = true;
LISA_UI_LOGI("Alarm navigation and voice control handler initialized");
}

View File

@@ -0,0 +1,119 @@
/**
* @file setting_common_presenter.c
* @brief Common settings presenter - handles data logic only
*/
#include "lisa_ui_nav_scr.h"
#include "lisa_ui.h"
#include "lisa_ui_llm_base.h"
#include "lisa_ui_nav_scr_ids.h"
#include "setting_common_view.h"
#include "model_common.h"
#define TAG "setting_common_presenter"
struct setting_common_nav_scr_data {
lv_obj_t *view;
};
/**
* @brief Back button callback - handles navigation back to settings
* This is business logic in presenter layer
*/
static void setting_common_back_btn_cb(void *user_data)
{
(void)user_data;
LISA_UI_LOGD("Common settings back button clicked, returning to settings");
lisa_ui_nav_scr_nav_back();
}
/**
* @brief Volume changed callback - handles volume setting
* This is business logic in presenter layer
*/
static void setting_common_volume_changed_cb(uint8_t volume, void *user_data)
{
(void)user_data;
LISA_UI_LOGD("Volume changed to: %d%%", volume);
model_common_volume_set(volume);
}
/**
* @brief Brightness changed callback - handles brightness setting
* This is business logic in presenter layer
*/
static void setting_common_brightness_changed_cb(uint8_t brightness, void *user_data)
{
(void)user_data;
LISA_UI_LOGD("Brightness changed to: %d%%", brightness);
model_common_brightness_set(brightness);
}
static int setting_common_nav_scr_open(const struct lisa_ui_nav_scr *scr, void **data)
{
(void)scr;
LISA_UI_LOGD("Common settings nav scr open");
struct setting_common_nav_scr_data *scr_data = lisa_ui_malloc(sizeof(struct setting_common_nav_scr_data));
if (!scr_data) {
return -1;
}
memset(scr_data, 0, sizeof(struct setting_common_nav_scr_data));
// Create view
scr_data->view = lisa_ui_setting_common_view_create(lv_scr_act());
if (!scr_data->view) {
lisa_ui_free(scr_data);
return -1;
}
// Sync with system services before showing UI
model_common_sync_from_system();
// Get current values from model and update view
uint8_t current_volume = model_common_volume_get();
uint8_t current_brightness = model_common_brightness_get();
lisa_ui_setting_common_view_set_volume(scr_data->view, current_volume);
lisa_ui_setting_common_view_set_brightness(scr_data->view, current_brightness);
// Set callbacks
lisa_ui_setting_common_view_set_back_cb(scr_data->view, setting_common_back_btn_cb, NULL);
lisa_ui_setting_common_view_set_volume_changed_cb(scr_data->view, setting_common_volume_changed_cb, NULL);
lisa_ui_setting_common_view_set_brightness_changed_cb(scr_data->view, setting_common_brightness_changed_cb, NULL);
LISA_UI_LOGD("Loaded settings - Volume: %d%%, Brightness: %d%%",
current_volume, current_brightness);
*data = scr_data;
return 0;
}
static int setting_common_nav_scr_show(const struct lisa_ui_nav_scr *scr, void *data)
{
struct setting_common_nav_scr_data *scr_data = (struct setting_common_nav_scr_data *)data;
if (!scr_data || !scr_data->view) {
return -1;
}
lv_obj_clear_flag(scr_data->view, LV_OBJ_FLAG_HIDDEN);
return 0;
}
static int setting_common_nav_scr_close(const struct lisa_ui_nav_scr *scr, void *data)
{
struct setting_common_nav_scr_data *scr_data = (struct setting_common_nav_scr_data *)data;
if (scr_data) {
if (scr_data->view) {
lv_obj_del(scr_data->view);
}
lisa_ui_free(scr_data);
}
return 0;
}
const struct lisa_ui_nav_scr setting_common_nav_scr = {
.unique_id = LISA_UI_NAV_SCR_ID_SETTING_COMMON,
.open = setting_common_nav_scr_open,
.show = setting_common_nav_scr_show,
.close = setting_common_nav_scr_close,
};

View File

@@ -0,0 +1,156 @@
/**
* @file setting_presenter.c
* @brief Setting main page presenter
*/
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#define TAG "setting_presenter"
#include "lisa_ui.h"
#include "lisa_ui_llm_base.h"
#include "lisa_ui_nav_scr.h"
#include "lisa_ui_nav_scr_ids.h"
#include "lisa_ui_assets.h"
#include "setting_view.h"
struct setting_nav_scr_data {
lv_obj_t *view;
};
/**
* @brief Setting item click callback - handles navigation to sub-pages
* This is business logic in presenter layer
*/
static void setting_item_click_cb(const char *name, void *user_data)
{
(void)user_data;
LISA_UI_LOGD("Setting item clicked: %s", name);
// Navigate to corresponding setting page based on name
if (strcmp(name, _("common setting")) == 0) {
lisa_ui_nav_scr_nav_to(LISA_UI_NAV_SCR_ID_SETTING_COMMON);
} else if (strcmp(name, _("network setting")) == 0) {
lisa_ui_nav_scr_nav_to(LISA_UI_NAV_SCR_ID_SETTING_WIFI);
} else if (strcmp(name, _("wakeup setting")) == 0) {
lisa_ui_nav_scr_nav_to(LISA_UI_NAV_SCR_ID_SETTING_WAKEUP);
} else if (strcmp(name, _("alarm setting")) == 0) {
lisa_ui_nav_scr_nav_to(LISA_UI_NAV_SCR_ID_SETTING_CLOCK);
}
}
/**
* @brief Back button click callback - handles navigation back to home
* This is business logic in presenter layer
*/
static void setting_back_btn_cb(void *user_data)
{
(void)user_data;
LISA_UI_LOGD("Setting back button clicked, returning to home");
lisa_ui_nav_scr_nav_back();
}
static int setting_nav_scr_open(const struct lisa_ui_nav_scr *scr, void **data)
{
LISA_UI_LOGD("Setting nav scr open, id: %d", scr->unique_id);
struct setting_nav_scr_data *scr_data = lisa_ui_malloc(sizeof(struct setting_nav_scr_data));
if (!scr_data) {
LISA_UI_LOGE("Failed to allocate memory for setting_nav_scr_data");
return -1;
}
memset(scr_data, 0, sizeof(struct setting_nav_scr_data));
// Create view
scr_data->view = lisa_ui_setting_view_create(lv_scr_act());
if (!scr_data->view) {
LISA_UI_LOGE("Failed to create setting view");
lisa_ui_free(scr_data);
return -1;
}
// Add setting items in 2x2 grid (col, row) with PNG icons from generated assets
// Row 0: Common Settings (left), Network Settings (right)
lisa_ui_setting_view_item_add(scr_data->view, _("common setting"), &icons_Icon_basics_png, 0, 0);
lisa_ui_setting_view_item_add(scr_data->view, _("network setting"), &icons_Icon_wifi_png, 1, 0);
// Row 1: Wakeup Interaction (left), Alarm Settings (right)
lisa_ui_setting_view_item_add(scr_data->view, _("wakeup setting"), &icons_Icon_wake_up_png, 0, 1);
lisa_ui_setting_view_item_add(scr_data->view, _("alarm setting"), &icons_Icon_alarm_png, 1, 1);
// Set click callback for setting items
lisa_ui_setting_view_set_click_cb(scr_data->view, setting_item_click_cb, NULL);
// Set back button callback
lisa_ui_setting_view_set_back_cb(scr_data->view, setting_back_btn_cb, NULL);
*data = scr_data;
return 0;
}
static int setting_nav_scr_show(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("Setting nav scr show, id: %d", scr->unique_id);
struct setting_nav_scr_data *scr_data = (struct setting_nav_scr_data *)data;
if (!scr_data || !scr_data->view) {
LISA_UI_LOGE("Invalid setting nav scr data");
return -1;
}
// View already created on current screen, just show it
lv_obj_clear_flag(scr_data->view, LV_OBJ_FLAG_HIDDEN);
return 0;
}
static int setting_nav_scr_pause(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("Setting nav scr pause, id: %d", scr->unique_id);
struct setting_nav_scr_data *scr_data = (struct setting_nav_scr_data *)data;
if (scr_data && scr_data->view) {
lv_obj_add_flag(scr_data->view, LV_OBJ_FLAG_HIDDEN);
}
return 0;
}
static int setting_nav_scr_resume(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("Setting nav scr resume, id: %d", scr->unique_id);
struct setting_nav_scr_data *scr_data = (struct setting_nav_scr_data *)data;
if (scr_data && scr_data->view) {
lv_obj_clear_flag(scr_data->view, LV_OBJ_FLAG_HIDDEN);
}
return 0;
}
static int setting_nav_scr_close(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("Setting nav scr close, id: %d", scr->unique_id);
struct setting_nav_scr_data *scr_data = (struct setting_nav_scr_data *)data;
if (scr_data) {
if (scr_data->view) {
lv_obj_del(scr_data->view);
}
lisa_ui_free(scr_data);
}
return 0;
}
const struct lisa_ui_nav_scr setting_nav_scr = {
.unique_id = LISA_UI_NAV_SCR_ID_SETTING,
.open = setting_nav_scr_open,
.show = setting_nav_scr_show,
.pause = setting_nav_scr_pause,
.resume = setting_nav_scr_resume,
.close = setting_nav_scr_close,
};

View File

@@ -0,0 +1,103 @@
/**
* @file setting_wakeup_presenter.c
* @brief Wakeup settings presenter - handles data logic only
*/
#include "lisa_ui_nav_scr.h"
#include "lisa_ui.h"
#include "lisa_ui_nav_scr_ids.h"
#include "setting_wakeup_view.h"
#include "model_voice.h"
#define TAG "setting_wakeup_presenter"
struct setting_wakeup_nav_scr_data {
lv_obj_t *view;
};
/**
* @brief 返回按钮回调 - 处理返回到设置主页面的导航
* 这是presenter层的业务逻辑
*/
static void setting_wakeup_back_btn_cb(void *user_data)
{
(void)user_data;
LISA_UI_LOGD("Wakeup settings back button clicked, returning to settings");
lisa_ui_nav_scr_nav_back();
}
static void on_wakeup_mode_changed(uint16_t mode, void *user_data)
{
(void)user_data;
LISA_UI_LOGD("Wakeup mode changed to: %d", mode);
int ret = model_voice_wakeup_mode_set((model_voice_wakeup_mode_t)mode);
if (ret == 0) {
LISA_UI_LOGD("Successfully set wakeup mode: %s",
model_voice_wakeup_mode_name_get((model_voice_wakeup_mode_t)mode));
} else {
LISA_UI_LOGE("Failed to set wakeup mode: %d", ret);
}
}
static int setting_wakeup_nav_scr_open(const struct lisa_ui_nav_scr *scr, void **data)
{
LISA_UI_LOGD("Wakeup settings nav scr open");
struct setting_wakeup_nav_scr_data *scr_data = lisa_ui_malloc(sizeof(struct setting_wakeup_nav_scr_data));
if (!scr_data) {
return -1;
}
memset(scr_data, 0, sizeof(struct setting_wakeup_nav_scr_data));
// Create wakeup view (UI layout handled by view)
scr_data->view = lisa_ui_setting_wakeup_view_create(lv_scr_act());
if (!scr_data->view) {
LISA_UI_LOGE("Failed to create wakeup view");
lisa_ui_free(scr_data);
return -1;
}
/* 注册返回按钮回调 */
lisa_ui_setting_wakeup_view_set_back_cb(scr_data->view, setting_wakeup_back_btn_cb, NULL);
lisa_ui_setting_wakeup_view_set_mode_changed_cb(scr_data->view, on_wakeup_mode_changed, NULL);
/* 初始化UI选中状态为当前模式 */
lisa_ui_setting_wakeup_view_set_mode(scr_data->view, (uint16_t)model_voice_wakeup_mode_get());
*data = scr_data;
return 0;
}
static int setting_wakeup_nav_scr_show(const struct lisa_ui_nav_scr *scr, void *data)
{
struct setting_wakeup_nav_scr_data *scr_data = (struct setting_wakeup_nav_scr_data *)data;
if (!scr_data || !scr_data->view) {
return -1;
}
/* 同步当前模式防止模型状态更新后UI未刷新 */
lisa_ui_setting_wakeup_view_set_mode(scr_data->view, (uint16_t)model_voice_wakeup_mode_get());
lv_obj_clear_flag(scr_data->view, LV_OBJ_FLAG_HIDDEN);
return 0;
}
static int setting_wakeup_nav_scr_close(const struct lisa_ui_nav_scr *scr, void *data)
{
struct setting_wakeup_nav_scr_data *scr_data = (struct setting_wakeup_nav_scr_data *)data;
if (scr_data) {
if (scr_data->view) {
lv_obj_del(scr_data->view);
}
lisa_ui_free(scr_data);
}
return 0;
}
const struct lisa_ui_nav_scr setting_wakeup_nav_scr = {
.unique_id = LISA_UI_NAV_SCR_ID_SETTING_WAKEUP,
.open = setting_wakeup_nav_scr_open,
.show = setting_wakeup_nav_scr_show,
.close = setting_wakeup_nav_scr_close,
};

View File

@@ -0,0 +1,436 @@
/**
* @file setting_wifi_presenter.c
* @brief WiFi settings presenter - handles data logic only
*/
#define TAG "setting_wifi_presenter"
#include <stdlib.h>
#include <string.h>
#include "lisa_ui_nav_scr.h"
#include "lisa_ui.h"
#include "lisa_ui_nav_scr_ids.h"
#include "setting_wifi_view.h"
#include "model_wifi.h"
#include "lisa_ui_toast.h"
struct setting_wifi_nav_scr_data {
lv_obj_t *view;
bool wifi_enabled;
bool model_inited;
lv_timer_t *scan_timer; /* WiFi扫描定时器 */
};
static struct setting_wifi_nav_scr_data *g_scr_data = NULL;
/**
* @brief WiFi 定时扫描回调
*/
static void wifi_scan_timer_cb(lv_timer_t *timer)
{
(void)timer;
if (!g_scr_data || !g_scr_data->model_inited) {
return;
}
LISA_UI_LOGD("Timer triggered WiFi scan");
int ret = model_wifi_scan_start();
if (ret != 0) {
LISA_UI_LOGE("Timer: Failed to start WiFi scan: %d", ret);
}
}
/**
* @brief WiFi 连接成功回调
*/
static void on_wifi_connected(model_wifi_sta_config_t *sta_info, void *arg)
{
(void)arg;
if (sta_info) {
LISA_UI_LOGD("WiFi connected to: %s", sta_info->ssid);
/* 重新扫描WiFi列表以更新连接状态 */
if (g_scr_data && g_scr_data->model_inited) {
int ret = model_wifi_scan_start();
if (ret != 0) {
LISA_UI_LOGE("Failed to start WiFi scan after connection: %d", ret);
}
}
}
}
/**
* @brief WiFi 断开连接回调
*/
static void on_wifi_disconnected(int reason, void *arg)
{
(void)arg;
LISA_UI_LOGD("WiFi disconnected, reason: %d", reason);
/* 重新扫描WiFi列表以更新连接状态 */
if (g_scr_data && g_scr_data->model_inited) {
int ret = model_wifi_scan_start();
if (ret != 0) {
LISA_UI_LOGE("Failed to start WiFi scan after disconnection: %d", ret);
}
}
}
/**
* @brief WiFi 正在连接回调
*/
static void on_wifi_connecting(void *arg)
{
(void)arg;
LISA_UI_LOGD("WiFi connecting...");
// TODO: 更新UI显示连接中状态
}
/**
* @brief WiFi 连接失败回调
*/
static void on_wifi_connection_failed(int reason, void *arg)
{
(void)arg;
LISA_UI_LOGE("WiFi connection failed, reason: %d", reason);
lisa_ui_toast_show("连接失败");
}
/**
* @brief RSSI 比较函数 - 用于排序
* 排序规则:
* 1. 已连接的WiFi排在最前面
* 2. 其余WiFi按信号强度从高到低排序
*/
static int rssi_compare(const void *a, const void *b)
{
const lisa_ui_wifi_ap_info_t *ap_a = (const lisa_ui_wifi_ap_info_t *)a;
const lisa_ui_wifi_ap_info_t *ap_b = (const lisa_ui_wifi_ap_info_t *)b;
/* 如果a已连接b未连接a排在前面 */
if (ap_a->is_connected && !ap_b->is_connected) {
return -1;
}
/* 如果b已连接a未连接b排在前面 */
if (!ap_a->is_connected && ap_b->is_connected) {
return 1;
}
/* 两者连接状态相同按RSSI值越大信号越强应该排在前面 */
return ap_b->rssi - ap_a->rssi;
}
/**
* @brief WiFi 扫描完成回调
*/
static void on_wifi_scan_done(model_wifi_scan_info_t *aps_info, int ap_num, void *arg)
{
(void)arg;
LISA_UI_LOGD("WiFi scan done, found %d APs", ap_num);
if (!g_scr_data || !g_scr_data->view) {
return;
}
if (ap_num <= 0) {
/* 没有扫描到 WiFi */
lisa_ui_setting_wifi_view_clear_list(g_scr_data->view);
return;
}
/* 获取当前连接的WiFi信息 */
model_wifi_sta_config_t connected_sta;
bool has_connected = false;
if (model_wifi_get_connected_info(&connected_sta) == 0) {
has_connected = true;
LISA_UI_LOGD("Currently connected to: %s", connected_sta.ssid);
}
/* 转换为 view 层的数据结构 */
lisa_ui_wifi_ap_info_t *ap_list = lisa_ui_malloc(sizeof(lisa_ui_wifi_ap_info_t) * ap_num);
if (!ap_list) {
LISA_UI_LOGE("Failed to allocate memory for AP list");
return;
}
int unique_count = 0;
for (int i = 0; i < ap_num; i++) {
/* 过滤空SSID */
if (aps_info[i].ssid[0] == '\0' || strlen(aps_info[i].ssid) == 0) {
LISA_UI_LOGD("Skipping empty SSID at index %d", i);
continue;
}
/* 检查是否已存在相同的SSID (去重) */
bool is_duplicate = false;
for (int j = 0; j < unique_count; j++) {
if (strcmp(ap_list[j].ssid, aps_info[i].ssid) == 0) {
is_duplicate = true;
/* 如果发现重复,保留信号更强的 */
if (aps_info[i].rssi > ap_list[j].rssi) {
LISA_UI_LOGD("Found duplicate SSID '%s', updating with stronger signal (%d > %d)",
aps_info[i].ssid, aps_info[i].rssi, ap_list[j].rssi);
ap_list[j].rssi = aps_info[i].rssi;
ap_list[j].channel = aps_info[i].channel;
}
break;
}
}
if (is_duplicate) {
continue;
}
/* 添加到列表 */
strncpy(ap_list[unique_count].ssid, aps_info[i].ssid, sizeof(ap_list[unique_count].ssid) - 1);
ap_list[unique_count].ssid[sizeof(ap_list[unique_count].ssid) - 1] = '\0';
ap_list[unique_count].rssi = aps_info[i].rssi;
ap_list[unique_count].channel = aps_info[i].channel;
/* 检查是否为当前连接的WiFi */
ap_list[unique_count].is_connected = has_connected &&
(strcmp(ap_list[unique_count].ssid, connected_sta.ssid) == 0);
unique_count++;
}
/* 如果过滤后没有WiFi */
if (unique_count == 0) {
LISA_UI_LOGD("No valid WiFi APs after filtering");
lisa_ui_free(ap_list);
lisa_ui_setting_wifi_view_clear_list(g_scr_data->view);
return;
}
LISA_UI_LOGD("Filtered %d -> %d unique APs", ap_num, unique_count);
/* 按照信号强度排序 (RSSI从高到低) */
qsort(ap_list, unique_count, sizeof(lisa_ui_wifi_ap_info_t), rssi_compare);
for (int i = 0; i < unique_count; i++) {
LISA_UI_LOGD("AP[%d]: SSID=%s, RSSI=%d, Channel=%d, Connected=%d",
i, ap_list[i].ssid, ap_list[i].rssi, ap_list[i].channel, ap_list[i].is_connected);
}
/* 更新 UI 显示 WiFi 列表 */
lisa_ui_setting_wifi_view_update_list(g_scr_data->view, ap_list, unique_count);
lisa_ui_free(ap_list);
}
/**
* @brief WiFi 扫描失败回调
*/
static void on_wifi_scan_failed(int reason, void *arg)
{
(void)arg;
LISA_UI_LOGE("WiFi scan failed, reason: %d", reason);
// TODO: 更新UI显示扫描失败
}
// WiFi model 回调结构
static const struct model_wifi_cb wifi_callbacks = {
.on_connected = on_wifi_connected,
.on_disconnected = on_wifi_disconnected,
.on_connecting = on_wifi_connecting,
.on_connection_failed = on_wifi_connection_failed,
.on_scan_done = on_wifi_scan_done,
.on_scan_failed = on_wifi_scan_failed,
};
/**
* @brief 密码输入确认回调
*/
static void on_password_confirm(const char *ssid, const char *password, void *user_data)
{
(void)user_data;
LISA_UI_LOGD("Password confirmed for SSID: %s", ssid);
if (!g_scr_data || !g_scr_data->view) {
return;
}
if (password == NULL || strlen(password) < 8) {
lisa_ui_toast_show("密码长度需大于8个字符");
return;
}
/* 隐藏密码输入界面 */
lisa_ui_setting_wifi_view_hide_password_input(g_scr_data->view);
/* 尝试连接WiFi */
LISA_UI_LOGI("Attempting to connect to: %s", ssid);
int ret = model_wifi_connect(ssid, password, NULL);
if (ret != 0) {
LISA_UI_LOGE("Failed to start WiFi connection: %d", ret);
}
}
/**
* @brief 密码输入取消回调
*/
static void on_password_cancel(void *user_data)
{
(void)user_data;
LISA_UI_LOGD("Password input cancelled");
}
/**
* @brief WiFi 项点击回调
*/
static void on_wifi_item_clicked(const char *ssid, void *user_data)
{
(void)user_data;
LISA_UI_LOGD("WiFi item clicked: %s", ssid);
if (!g_scr_data || !g_scr_data->view) {
return;
}
/* 检查是否为当前已连接的WiFi */
model_wifi_sta_config_t connected_sta;
if (model_wifi_get_connected_info(&connected_sta) == 0) {
if (strcmp(connected_sta.ssid, ssid) == 0) {
LISA_UI_LOGD("Clicked WiFi is already connected, ignoring");
return;
}
}
/* 显示密码输入界面 */
lisa_ui_setting_wifi_view_show_password_input(g_scr_data->view, ssid,
on_password_confirm,
on_password_cancel,
g_scr_data);
}
/**
* @brief 返回按钮回调 - 处理返回到设置主页面的导航
* 这是presenter层的业务逻辑
*/
static void setting_wifi_back_btn_cb(void *user_data)
{
(void)user_data;
LISA_UI_LOGD("WiFi settings back button clicked, returning to settings");
lisa_ui_nav_scr_nav_back();
}
static int setting_wifi_nav_scr_open(const struct lisa_ui_nav_scr *scr, void **data)
{
LISA_UI_LOGD("WiFi settings nav scr open");
struct setting_wifi_nav_scr_data *scr_data = lisa_ui_malloc(sizeof(struct setting_wifi_nav_scr_data));
if (!scr_data) {
return -1;
}
memset(scr_data, 0, sizeof(struct setting_wifi_nav_scr_data));
scr_data->wifi_enabled = true; /* 默认开启自动扫描 */
scr_data->model_inited = false;
scr_data->scan_timer = NULL;
g_scr_data = scr_data;
// 初始化 WiFi model
int ret = model_wifi_init();
if (ret != 0) {
LISA_UI_LOGE("Failed to initialize WiFi model: %d", ret);
} else {
scr_data->model_inited = true;
// 注册 WiFi 回调
ret = model_wifi_cb_register(&wifi_callbacks, scr_data);
if (ret != 0) {
LISA_UI_LOGE("Failed to register WiFi callbacks: %d", ret);
}
}
// Create WiFi view (UI layout handled by view)
scr_data->view = lisa_ui_setting_wifi_view_create(lv_scr_act());
if (!scr_data->view) {
LISA_UI_LOGE("Failed to create WiFi view");
if (scr_data->model_inited) {
model_wifi_cb_unregister(&wifi_callbacks);
}
lisa_ui_free(scr_data);
g_scr_data = NULL;
return -1;
}
// Attach data logic to auto scan switch
// lv_obj_t *wifi_switch = lisa_ui_setting_wifi_view_get_switch(scr_data->view);
// if (wifi_switch) {
// // 设置开关初始状态为开启
// // lv_obj_add_state(wifi_switch, LV_STATE_CHECKED);
// lv_obj_add_event_cb(wifi_switch, wifi_switch_cb, LV_EVENT_VALUE_CHANGED, scr_data);
// }
/* 注册返回按钮回调 */
lisa_ui_setting_wifi_view_set_back_cb(scr_data->view, setting_wifi_back_btn_cb, NULL);
/* 注册 WiFi 项点击回调 */
lisa_ui_setting_wifi_view_set_item_cb(scr_data->view, on_wifi_item_clicked, scr_data);
/* 立即扫描一次 */
model_wifi_scan_start();
scr_data->scan_timer = lv_timer_create(wifi_scan_timer_cb, 1000 * 20, NULL);
lv_timer_resume(g_scr_data->scan_timer);
if (scr_data->scan_timer) {
LISA_UI_LOGD("WiFi scan timer created (10s interval)");
} else {
LISA_UI_LOGE("Failed to create WiFi scan timer");
}
*data = scr_data;
return 0;
}
static int setting_wifi_nav_scr_show(const struct lisa_ui_nav_scr *scr, void *data)
{
LISA_UI_LOGD("WiFi settings nav scr show");
struct setting_wifi_nav_scr_data *scr_data = (struct setting_wifi_nav_scr_data *)data;
if (!scr_data || !scr_data->view) {
LISA_UI_LOGE("WiFi view is NULL");
return -1;
}
lv_obj_clear_flag(scr_data->view, LV_OBJ_FLAG_HIDDEN);
return 0;
}
static int setting_wifi_nav_scr_close(const struct lisa_ui_nav_scr *scr, void *data)
{
struct setting_wifi_nav_scr_data *scr_data = (struct setting_wifi_nav_scr_data *)data;
if (scr_data) {
/* 删除定时器 */
if (scr_data->scan_timer) {
lv_timer_del(scr_data->scan_timer);
scr_data->scan_timer = NULL;
LISA_UI_LOGD("WiFi scan timer deleted");
}
// 注销 WiFi 回调
if (scr_data->model_inited) {
model_wifi_cb_unregister(&wifi_callbacks);
}
if (scr_data->view) {
lv_obj_del(scr_data->view);
}
lisa_ui_free(scr_data);
g_scr_data = NULL;
}
return 0;
}
const struct lisa_ui_nav_scr setting_wifi_nav_scr = {
.unique_id = LISA_UI_NAV_SCR_ID_SETTING_WIFI,
.open = setting_wifi_nav_scr_open,
.show = setting_wifi_nav_scr_show,
.pause = NULL,
.resume = NULL,
.close = setting_wifi_nav_scr_close,
};

View File

@@ -0,0 +1,13 @@
target_sources(
${LISA_UI_LIB_NAME}
PRIVATE
emoji_anim.c
img_utils.c
lv_img_utils.c
)
target_include_directories(
${LISA_UI_LIB_NAME}
PRIVATE
.
)

View File

@@ -0,0 +1,654 @@
#include <string.h>
#include <stdio.h>
#include "cJSON.h"
#include "lisa_mem.h"
#include "lisa_ui_anim_ext.h"
#include "lisa_ui_assets.h"
#include "lv_img_utils.h"
#define LOG_TAG "emoji_anim"
#include "lisa_ui_log.h"
#include "romfs/romfs.h"
#define EMOJI_CONFIG_PATH "/emoji.json"
#define EMOJI_DEFAULT_DELAY 100
#define EMOJI_MAX_COUNT 64
struct emoji_phase_owner {
char *path_pattern;
int frame_begin;
int frame_end;
};
struct emoji_config {
char *name;
char *alias_name;
int alias_idx;
lisa_ui_anim_ext_config_t anim;
struct emoji_phase_owner enter_owner;
struct emoji_phase_owner loop_owner;
struct emoji_phase_owner exit_owner;
};
static struct emoji_config *s_emoji_configs;
static uint32_t s_emoji_count;
static const lisa_ui_anim_ext_config_t *s_fallback_anim;
static char *s_fallback_name;
static const lisa_ui_anim_ext_config_t s_empty_anim;
static char *emoji_strdup(const char *src)
{
uint32_t len;
char *dst;
if (src == NULL) {
return NULL;
}
len = strlen(src) + 1;
dst = lisa_mem_alloc(len);
if (dst == NULL) {
return NULL;
}
memcpy(dst, src, len);
return dst;
}
static void emoji_anim_config_free(lisa_ui_anim_config_t *cfg)
{
if (cfg == NULL) {
return;
}
if (cfg->delays != NULL) {
lisa_mem_free((void *)cfg->delays);
cfg->delays = NULL;
}
if (cfg->frames != NULL) {
lisa_mem_free((void *)cfg->frames);
cfg->frames = NULL;
}
}
static void emoji_phase_owner_free(struct emoji_phase_owner *owner)
{
if (owner == NULL) {
return;
}
lisa_mem_free(owner->path_pattern);
memset(owner, 0, sizeof(*owner));
}
static void emoji_anim_cleanup(void)
{
if (s_emoji_configs != NULL) {
for (uint32_t i = 0; i < s_emoji_count; i++) {
lisa_mem_free(s_emoji_configs[i].name);
lisa_mem_free(s_emoji_configs[i].alias_name);
emoji_anim_config_free(&s_emoji_configs[i].anim.enter);
emoji_anim_config_free(&s_emoji_configs[i].anim.loop);
emoji_anim_config_free(&s_emoji_configs[i].anim.exit);
emoji_phase_owner_free(&s_emoji_configs[i].enter_owner);
emoji_phase_owner_free(&s_emoji_configs[i].loop_owner);
emoji_phase_owner_free(&s_emoji_configs[i].exit_owner);
}
lisa_mem_free(s_emoji_configs);
}
s_emoji_configs = NULL;
s_emoji_count = 0;
s_fallback_anim = NULL;
lisa_mem_free(s_fallback_name);
s_fallback_name = NULL;
}
static int json_get_number(const cJSON *obj, const char *key, int *out)
{
const cJSON *item = cJSON_GetObjectItemCaseSensitive(obj, key);
if (!cJSON_IsNumber(item)) {
return -1;
}
*out = item->valueint;
return 0;
}
static int parse_phase_range(const cJSON *phase_json, int *frame_begin, int *frame_end)
{
const cJSON *range_json = cJSON_GetObjectItemCaseSensitive(phase_json, "range");
const cJSON *begin_json;
const cJSON *end_json;
if (!cJSON_IsArray(range_json) || cJSON_GetArraySize(range_json) != 2) {
LISA_UI_LOGE("Phase range must be [begin, end]");
return -1;
}
begin_json = cJSON_GetArrayItem(range_json, 0);
end_json = cJSON_GetArrayItem(range_json, 1);
if (!cJSON_IsNumber(begin_json) || !cJSON_IsNumber(end_json)) {
LISA_UI_LOGE("Phase range item must be number");
return -1;
}
*frame_begin = begin_json->valueint;
*frame_end = end_json->valueint;
if (*frame_begin < 0 || *frame_end < *frame_begin) {
LISA_UI_LOGE("Invalid phase range: [%d, %d]", *frame_begin, *frame_end);
return -1;
}
return 0;
}
static int parse_phase_interval_overrides(const cJSON *overrides_json, uint16_t frame_count, uint32_t default_interval,
const uint32_t **out_delays)
{
uint32_t *delays = lisa_mem_alloc(sizeof(uint32_t) * frame_count);
int count = cJSON_GetArraySize(overrides_json);
if (delays == NULL) {
LISA_UI_LOGE("Failed to allocate interval overrides array");
return -1;
}
for (uint16_t i = 0; i < frame_count; i++) {
delays[i] = default_interval;
}
for (int i = 0; i < count; i++) {
const cJSON *override_json = cJSON_GetArrayItem(overrides_json, i);
const cJSON *idx_json;
const cJSON *interval_json;
int idx;
int interval;
if (!cJSON_IsArray(override_json) || cJSON_GetArraySize(override_json) != 2) {
LISA_UI_LOGE("interval_overrides[%d] must be [frame_idx, interval_ms]", i);
lisa_mem_free(delays);
return -1;
}
idx_json = cJSON_GetArrayItem(override_json, 0);
interval_json = cJSON_GetArrayItem(override_json, 1);
if (!cJSON_IsNumber(idx_json) || !cJSON_IsNumber(interval_json)) {
LISA_UI_LOGE("interval_overrides[%d] item must be number", i);
lisa_mem_free(delays);
return -1;
}
idx = idx_json->valueint;
interval = interval_json->valueint;
if (idx < 0 || idx >= frame_count || interval < 0) {
LISA_UI_LOGE("Invalid interval override at index: %d, frame: %d, interval: %d", i, idx, interval);
lisa_mem_free(delays);
return -1;
}
delays[idx] = (uint32_t)interval;
}
*out_delays = delays;
return 0;
}
static int parse_anim_phase(const cJSON *phase_json, lisa_ui_anim_config_t *cfg, struct emoji_phase_owner *owner, int loop)
{
const cJSON *path_json;
const cJSON *overrides_json;
int frame_begin;
int frame_end;
int frame_count;
int interval_ms = EMOJI_DEFAULT_DELAY;
memset(cfg, 0, sizeof(*cfg));
memset(owner, 0, sizeof(*owner));
if (phase_json == NULL) {
return 0;
}
if (!cJSON_IsObject(phase_json)) {
LISA_UI_LOGE("Animation phase must be an object");
return -1;
}
path_json = cJSON_GetObjectItemCaseSensitive(phase_json, "path");
if (!cJSON_IsString(path_json) || path_json->valuestring == NULL) {
LISA_UI_LOGE("Missing phase path");
return -1;
}
if (parse_phase_range(phase_json, &frame_begin, &frame_end) != 0) {
return -1;
}
frame_count = frame_end - frame_begin + 1;
owner->path_pattern = emoji_strdup(path_json->valuestring);
if (owner->path_pattern == NULL) {
LISA_UI_LOGE("Failed to allocate phase path");
return -1;
}
owner->frame_begin = frame_begin;
owner->frame_end = frame_end;
cfg->frames = lisa_mem_calloc(frame_count, sizeof(lv_img_dsc_t));
if (cfg->frames == NULL) {
LISA_UI_LOGE("Failed to allocate phase frames");
return -1;
}
cfg->frame_count = (uint16_t)frame_count;
if (json_get_number(phase_json, "interval_ms", &interval_ms) == 0 && interval_ms > 0) {
cfg->default_delay = interval_ms;
} else {
cfg->default_delay = EMOJI_DEFAULT_DELAY;
}
cfg->loop = loop;
overrides_json = cJSON_GetObjectItemCaseSensitive(phase_json, "interval_overrides");
if (overrides_json != NULL) {
if (!cJSON_IsArray(overrides_json)) {
LISA_UI_LOGE("Animation interval_overrides must be an array");
return -1;
}
if (parse_phase_interval_overrides(overrides_json, cfg->frame_count, cfg->default_delay, &cfg->delays) != 0) {
return -1;
}
}
return 0;
}
static int find_emoji_index_by_name(const char *name)
{
if (name == NULL) {
return -1;
}
for (uint32_t i = 0; i < s_emoji_count; i++) {
if (strcmp(s_emoji_configs[i].name, name) == 0) {
return (int)i;
}
}
return -1;
}
static int emoji_has_valid_phase(const struct emoji_config *cfg)
{
return cfg->anim.enter.frame_count > 0 || cfg->anim.loop.frame_count > 0 || cfg->anim.exit.frame_count > 0;
}
static int emoji_alias_cycle_check(uint32_t idx, uint8_t *state)
{
const struct emoji_config *cfg = &s_emoji_configs[idx];
if (state[idx] == 2) {
return 0;
}
if (state[idx] == 1) {
LISA_UI_LOGE("Alias cycle detected at emoji: %s", cfg->name);
return -1;
}
state[idx] = 1;
if (cfg->alias_idx >= 0) {
if ((uint32_t)cfg->alias_idx >= s_emoji_count) {
LISA_UI_LOGE("Alias index out of range, emoji: %s", cfg->name);
return -1;
}
if (emoji_alias_cycle_check((uint32_t)cfg->alias_idx, state) != 0) {
return -1;
}
}
state[idx] = 2;
return 0;
}
static int resolve_animation_aliases(void)
{
uint8_t *state;
for (uint32_t i = 0; i < s_emoji_count; i++) {
struct emoji_config *cfg = &s_emoji_configs[i];
int alias_idx;
if (cfg->alias_name == NULL) {
if (!emoji_has_valid_phase(cfg)) {
LISA_UI_LOGE("animation[%s] has no valid phase", cfg->name);
return -1;
}
continue;
}
if (emoji_has_valid_phase(cfg)) {
LISA_UI_LOGE("animation[%s] should not define phase when alias is set", cfg->name);
return -1;
}
alias_idx = find_emoji_index_by_name(cfg->alias_name);
if (alias_idx < 0) {
LISA_UI_LOGE("animation[%s] alias target not found: %s", cfg->name, cfg->alias_name);
return -1;
}
if ((uint32_t)alias_idx == i) {
LISA_UI_LOGE("animation[%s] alias self is not allowed", cfg->name);
return -1;
}
cfg->alias_idx = alias_idx;
}
state = lisa_mem_calloc(s_emoji_count, sizeof(uint8_t));
if (state == NULL) {
LISA_UI_LOGE("Failed to allocate alias state");
return -1;
}
for (uint32_t i = 0; i < s_emoji_count; i++) {
if (emoji_alias_cycle_check(i, state) != 0) {
lisa_mem_free(state);
return -1;
}
}
lisa_mem_free(state);
return 0;
}
static const lisa_ui_anim_ext_config_t *emoji_anim_get_by_index(uint32_t idx, uint32_t depth)
{
const struct emoji_config *cfg;
if (idx >= s_emoji_count || depth > s_emoji_count) {
return NULL;
}
cfg = &s_emoji_configs[idx];
if (cfg->alias_idx >= 0) {
return emoji_anim_get_by_index((uint32_t)cfg->alias_idx, depth + 1);
}
return &cfg->anim;
}
static int parse_animations(const cJSON *root_json)
{
const cJSON *default_emoji_json;
const cJSON *animations_json;
uint32_t count;
if (!cJSON_IsObject(root_json)) {
LISA_UI_LOGE("emoji config root must be an object");
return -1;
}
default_emoji_json = cJSON_GetObjectItemCaseSensitive(root_json, "default_emoji");
if (!cJSON_IsString(default_emoji_json) || default_emoji_json->valuestring == NULL) {
LISA_UI_LOGE("default_emoji is required");
return -1;
}
animations_json = cJSON_GetObjectItemCaseSensitive(root_json, "animations");
if (!cJSON_IsArray(animations_json)) {
LISA_UI_LOGE("animations must be an array");
return -1;
}
count = cJSON_GetArraySize(animations_json);
if (count == 0) {
LISA_UI_LOGE("animations is empty");
return -1;
}
if (count > EMOJI_MAX_COUNT) {
LISA_UI_LOGE("emoji config too large: %u", count);
return -1;
}
s_fallback_name = emoji_strdup(default_emoji_json->valuestring);
if (s_fallback_name == NULL) {
LISA_UI_LOGE("Failed to allocate default_emoji");
return -1;
}
s_emoji_configs = lisa_mem_calloc(count, sizeof(struct emoji_config));
if (s_emoji_configs == NULL) {
LISA_UI_LOGE("Failed to allocate emoji configs");
return -1;
}
s_emoji_count = count;
for (uint32_t i = 0; i < count; i++) {
const cJSON *item = cJSON_GetArrayItem(animations_json, i);
const cJSON *name_json;
const cJSON *alias_json;
const cJSON *intro_json;
const cJSON *loop_json;
const cJSON *outro_json;
struct emoji_config *cfg = &s_emoji_configs[i];
if (!cJSON_IsObject(item)) {
LISA_UI_LOGE("animation[%u] must be an object", i);
return -1;
}
name_json = cJSON_GetObjectItemCaseSensitive(item, "name");
if (!cJSON_IsString(name_json) || name_json->valuestring == NULL) {
LISA_UI_LOGE("animation[%u] missing name", i);
return -1;
}
for (uint32_t j = 0; j < i; j++) {
if (strcmp(s_emoji_configs[j].name, name_json->valuestring) == 0) {
LISA_UI_LOGE("Duplicated animation name: %s", name_json->valuestring);
return -1;
}
}
cfg->name = emoji_strdup(name_json->valuestring);
cfg->alias_idx = -1;
if (cfg->name == NULL) {
LISA_UI_LOGE("Failed to allocate animation name");
return -1;
}
alias_json = cJSON_GetObjectItemCaseSensitive(item, "alias");
if (alias_json != NULL) {
if (!cJSON_IsString(alias_json) || alias_json->valuestring == NULL) {
LISA_UI_LOGE("animation[%s] alias must be string", cfg->name);
return -1;
}
cfg->alias_name = emoji_strdup(alias_json->valuestring);
if (cfg->alias_name == NULL) {
LISA_UI_LOGE("Failed to allocate alias for animation: %s", cfg->name);
return -1;
}
}
intro_json = cJSON_GetObjectItemCaseSensitive(item, "intro");
loop_json = cJSON_GetObjectItemCaseSensitive(item, "loop");
outro_json = cJSON_GetObjectItemCaseSensitive(item, "outro");
if (parse_anim_phase(intro_json, &cfg->anim.enter, &cfg->enter_owner, 0) != 0 ||
parse_anim_phase(loop_json, &cfg->anim.loop, &cfg->loop_owner, -1) != 0 ||
parse_anim_phase(outro_json, &cfg->anim.exit, &cfg->exit_owner, 0) != 0) {
return -1;
}
}
if (resolve_animation_aliases() != 0) {
return -1;
}
{
int fallback_idx = find_emoji_index_by_name(s_fallback_name);
if (fallback_idx < 0) {
LISA_UI_LOGE("default_emoji not found in animations: %s", s_fallback_name);
return -1;
}
s_fallback_anim = emoji_anim_get_by_index((uint32_t)fallback_idx, 0);
if (s_fallback_anim == NULL) {
LISA_UI_LOGE("default_emoji resolve failed: %s", s_fallback_name);
return -1;
}
}
return 0;
}
static int load_phase_assets(struct romfs *fs, const lisa_ui_anim_config_t *cfg, const struct emoji_phase_owner *owner)
{
char path[ROMFS_PATH_MAX];
if (cfg->frame_count == 0) {
return 0;
}
for (int frame_idx = owner->frame_begin; frame_idx <= owner->frame_end; frame_idx++) {
uint8_t *data = NULL;
uint32_t size = 0;
int path_len = snprintf(path, sizeof(path), owner->path_pattern, frame_idx);
if (path_len < 0 || path_len >= sizeof(path)) {
LISA_UI_LOGE("Invalid asset path pattern: %s", owner->path_pattern);
return -1;
}
if (romfs_info_get(fs, path, &data, &size) != 0) {
LISA_UI_LOGE("Failed to get asset from romfs, path: %s", path);
return -1;
}
lv_img_png_src_init((lv_img_dsc_t *)&cfg->frames[frame_idx - owner->frame_begin], data, size);
}
return 0;
}
static int load_emoji_assets(struct romfs *fs)
{
for (uint32_t i = 0; i < s_emoji_count; i++) {
const struct emoji_config *cfg = &s_emoji_configs[i];
if (load_phase_assets(fs, &cfg->anim.enter, &cfg->enter_owner) != 0 ||
load_phase_assets(fs, &cfg->anim.loop, &cfg->loop_owner) != 0 ||
load_phase_assets(fs, &cfg->anim.exit, &cfg->exit_owner) != 0) {
return -1;
}
LISA_UI_LOGI("Loaded emoji animation assets: %s", cfg->name);
}
return 0;
}
static int emoji_anim_config_parse(const uint8_t *json_data, uint32_t json_size)
{
cJSON *root;
root = cJSON_ParseWithLength((const char *)json_data, json_size);
if (root == NULL) {
LISA_UI_LOGE("Failed to parse emoji config JSON");
return -1;
}
if (parse_animations(root) != 0) {
cJSON_Delete(root);
return -1;
}
cJSON_Delete(root);
return 0;
}
const lisa_ui_anim_ext_config_t *emoji_anim_get_by_name(const char *name)
{
int idx;
const lisa_ui_anim_ext_config_t *anim_cfg;
if (name == NULL) {
return s_fallback_anim != NULL ? s_fallback_anim : &s_empty_anim;
}
idx = find_emoji_index_by_name(name);
if (idx >= 0) {
anim_cfg = emoji_anim_get_by_index((uint32_t)idx, 0);
if (anim_cfg != NULL) {
return anim_cfg;
}
}
LISA_UI_LOGE("Invalid emoji name: %s, use fallback instead", name);
return s_fallback_anim != NULL ? s_fallback_anim : &s_empty_anim;
}
int emoji_anim_get_loaded_count(void)
{
return (int)s_emoji_count;
}
const char *emoji_anim_get_loaded_name(int index)
{
if (index < 0 || (uint32_t)index >= s_emoji_count) {
return NULL;
}
return s_emoji_configs[index].name;
}
int emoji_anim_is_alias(int index)
{
if (index < 0 || (uint32_t)index >= s_emoji_count) {
return 0;
}
return s_emoji_configs[index].alias_idx >= 0;
}
int lisa_ui_anim_init(uint32_t flash_addr, uint32_t flash_size)
{
int ret = 0;
struct romfs *fs = NULL;
uint8_t *config_data = NULL;
uint32_t config_size = 0;
emoji_anim_cleanup();
if (romfs_init(&fs, (const void *)flash_addr, flash_size) != 0) {
LISA_UI_LOGE("Failed to init romfs");
return -1;
}
if (romfs_info_get(fs, EMOJI_CONFIG_PATH, &config_data, &config_size) != 0) {
LISA_UI_LOGE("Failed to load emoji config from romfs: %s", EMOJI_CONFIG_PATH);
ret = -1;
goto out;
}
if (emoji_anim_config_parse(config_data, config_size) != 0) {
LISA_UI_LOGE("Failed to parse emoji config");
ret = -1;
goto out;
}
if (load_emoji_assets(fs) != 0) {
LISA_UI_LOGE("Failed to load emoji assets");
ret = -1;
goto out;
}
LISA_UI_LOGI("Emoji animation loaded, animations: %u", s_emoji_count);
out:
if (ret != 0) {
emoji_anim_cleanup();
}
romfs_deinit(&fs);
return ret;
}

View File

@@ -0,0 +1,19 @@
#ifndef __EMOJI_ANIM_H__
#define __EMOJI_ANIM_H__
#include "lisa_ui_anim_ext.h"
#define EMOJI_NAME_NEUTRAL "neutral"
#define EMOJI_NAME_BATTERY "battery"
#define EMOJI_NAME_SLEEPY "sleepy"
#define EMOJI_NAME_WAIT "wait"
#define EMOJI_NAME_WAKEUP "wakeup"
const lisa_ui_anim_ext_config_t *emoji_anim_get_by_name(const char *name);
int emoji_anim_get_loaded_count(void);
const char *emoji_anim_get_loaded_name(int index);
int emoji_anim_is_alias(int index);
int lisa_ui_anim_init(uint32_t flash_addr, uint32_t flash_size);
#endif

View File

@@ -0,0 +1,168 @@
/**
* @file img_utils.c
* @author TsMax (QinYUN575@Foxmail.com)
* @brief
* @version 0.1
* @date 2024-01-02
*
* @copyright Copyright (c) 2021 - 2024 co., ltd.
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "img_utils.h"
#include <string.h>
#ifdef __ZEPHYR__
#include <zephyr/kernel.h>
#include "lvgl.h"
#elif CONFIG_LVGL_ENV_SIMULATOR
#include "lvgl/lvgl.h"
#else
#include "lvgl.h"
#endif
#define LOG_INF LV_LOG_USER
// #include <stdio.h>
// #define LOG_INF printf
#pragma pack(push, 1)
typedef struct
{
uint32_t width;
uint32_t height;
uint8_t bit_depth;
uint8_t color_type;
uint8_t compression;
uint8_t filter;
uint8_t interlace;
} IHDRChunk;
#pragma pack(pop)
#define SWAP_UINT32(x) (((x) >> 24) | \
(((x) & 0x00FF0000) >> 8) | \
(((x) & 0x0000FF00) << 8) | \
((x) << 24))
/**
* @brief 获取图片信息
*
* @param src 图片数据
* @param size 图片数据大小
* @param header 图片信息
* @return uint8_t
*/
int img_png_get_info(const uint8_t *src, int size, img_header_t *header)
{
if (size < 8)
{
return -1;
}
const uint8_t png_signature[8] = {0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A};
if (memcmp(src, png_signature, 8) != 0)
{
// LOG_INF("img_png_get_info: signature not match");
return -2; // 签名不匹配
}
// LOG_INF("img_png_get_info: signature match");
src += 8;
size -= 8;
// 检查 IHDR 块
if (size < 25)
{ // IHDR块的最小长度4(长度) + 4(类型) + 13(IHDR数据) + 4(CRC)
// LOG_INF("img_png_get_info: size too short");
return -1; // 剩余数据太短,不足以包含完整的 IHDR 块
}
// 读取块长度
uint32_t chunk_length = (src[0] << 24) | (src[1] << 16) | (src[2] << 8) | src[3];
if (chunk_length != 13)
{
// LOG_INF("img_png_get_info: chunk length not match");
return -1; // IHDR块长度不正确
}
// 确认块类型是 "IHDR"
if (src[4] != 'I' || src[5] != 'H' || src[6] != 'D' || src[7] != 'R')
{
// LOG_INF("img_png_get_info: chunk type not match");
return -2; // 块类型不是 IHDR
}
// LOG_INF("img_png_get_info: chunk type match");
src += 8;
IHDRChunk *ihdr;
ihdr = (IHDRChunk *)src;
// 大小端转换
header->width = SWAP_UINT32(ihdr->width);
header->height = SWAP_UINT32(ihdr->height);
// LOG_INF("img_png_get_info: w=%d, h=%d", ihdr->width, ihdr->height);
// LOG_INF("img_png_get_info: w=%d, h=%d", header->width, header->height);
return 0;
}
int img_gif_get_info(const uint8_t * src, int size, img_header_t * header)
{
LOG_INF("img_gif_get_info: src=%p, size=%d", src, size);
if (size < 6)
{
LOG_INF("img_gif_get_info: size too short");
return -1;
}
if (src[0] != 'G' || src[1] != 'I' || src[2] != 'F')
{
LOG_INF("img_gif_get_info: signature not match");
return -2;
}
header->width = src[6] | (src[7] << 8);
header->height = src[8] | (src[9] << 8);
LOG_INF("img_gif_get_info: w=%d, h=%d", header->width, header->height);
return 0;
}
int img_jpg_get_info(const uint8_t * src, int size, img_header_t * header)
{
LOG_INF("img_jpg_get_info: src=%p, size=%d\r\n", src, size);
if (size < 2)
{
LOG_INF("img_jpg_get_info: size too short\r\n");
return -1;
}
if (src[0] != 0xFF || src[1] != 0xD8)
{
LOG_INF("img_jpg_get_info: signature not match\r\n");
return -2;
}
int i = 2;
while (i < size)
{
if (src[i] != 0xFF)
{
i++;
continue;
}
if (src[i + 1] == 0xC0)
{
break;
}
i += 2;
int length = (src[i] << 8) | src[i + 1];
i += length;
}
if (i >= size)
{
LOG_INF("img_jpg_get_info: not found SOF0\r\n");
return -3;
}
header->height = (src[i + 5] << 8) | src[i + 6];
header->width = (src[i + 7] << 8) | src[i + 8];
LOG_INF("img_jpg_get_info: w=%d, h=%d\r\n", header->width, header->height);
return 0;
}

View File

@@ -0,0 +1,35 @@
/**
* @file img_utils.h
* @author TsMax (QinYUN575@Foxmail.com)
* @brief
* @version 0.1
* @date 2024-01-02
*
* @copyright Copyright (c) 2021 - 2024 shenzhen listenai co., ltd.
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef __IMG_UTILS_H__
#define __IMG_UTILS_H__
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
typedef struct {
uint32_t width;
uint32_t height;
uint32_t img_size;
} img_header_t;
int img_png_get_info(const uint8_t * src, int size, img_header_t * header);
int img_gif_get_info(const uint8_t * src, int size, img_header_t * header);
int img_jpg_get_info(const uint8_t * src, int size, img_header_t * header);
#ifdef __cplusplus
}
#endif
#endif // __IMG_UTILS_H__

View File

@@ -0,0 +1,319 @@
/**
* @file incbin.h
* @author Dale Weiler
* @brief Utility for including binary files
*
* Facilities for including binary files into the current translation unit and
* making use from them externally in other translation units.
*/
#ifndef INCBIN_HDR
#define INCBIN_HDR
#include <limits.h>
# define INCBIN_ALIGNMENT_INDEX 8
/* Lookup table of (1 << n) where `n' is `INCBIN_ALIGNMENT_INDEX' */
#define INCBIN_ALIGN_SHIFT_0 1
#define INCBIN_ALIGN_SHIFT_1 2
#define INCBIN_ALIGN_SHIFT_2 4
#define INCBIN_ALIGN_SHIFT_3 8
#define INCBIN_ALIGN_SHIFT_4 16
#define INCBIN_ALIGN_SHIFT_5 32
#define INCBIN_ALIGN_SHIFT_6 64
#define INCBIN_ALIGN_SHIFT_7 128
#define INCBIN_ALIGN_SHIFT_8 256
/* Actual alignment value */
#define INCBIN_ALIGNMENT \
INCBIN_CONCATENATE( \
INCBIN_CONCATENATE(INCBIN_ALIGN_SHIFT, _), \
INCBIN_ALIGNMENT_INDEX)
/* Stringize */
#define INCBIN_STR(X) \
#X
#define INCBIN_STRINGIZE(X) \
INCBIN_STR(X)
/* Concatenate */
#define INCBIN_CAT(X, Y) \
X ## Y
#define INCBIN_CONCATENATE(X, Y) \
INCBIN_CAT(X, Y)
/* Deferred macro expansion */
#define INCBIN_EVAL(X) \
X
#define INCBIN_INVOKE(N, ...) \
INCBIN_EVAL(N(__VA_ARGS__))
/* Green Hills uses a different directive for including binary data */
#if defined(__ghs__)
# define INCBIN_MACRO "\tINCBIN"
#else
# define INCBIN_MACRO ".incbin"
#endif
#ifndef _MSC_VER
# define INCBIN_ALIGN \
__attribute__((aligned(INCBIN_ALIGNMENT)))
#else
# define INCBIN_ALIGN __declspec(align(INCBIN_ALIGNMENT))
#endif
#if defined(__arm__) || /* GNU C and RealView */ \
defined(__arm) || /* Diab */ \
defined(_ARM) /* ImageCraft */
# define INCBIN_ARM
#endif
#ifdef __GNUC__
/* Utilize .balign where supported */
# define INCBIN_ALIGN_HOST ".balign " INCBIN_STRINGIZE(INCBIN_ALIGNMENT) "\n"
# define INCBIN_ALIGN_BYTE ".balign 1\n"
#elif defined(INCBIN_ARM)
/*
* On arm assemblers, the alignment value is calculated as (1 << n) where `n' is
* the shift count. This is the value passed to `.align'
*/
# define INCBIN_ALIGN_HOST ".align" INCBIN_STRINGIZE(INCBIN_ALIGNMENT_INDEX) "\n"
# define INCBIN_ALIGN_BYTE ".align 0\n"
#else
/* We assume other inline assembler's treat `.align' as `.balign' */
# define INCBIN_ALIGN_HOST ".align" INCBIN_STRINGIZE(INCBIN_ALIGNMENT) "\n"
# define INCBIN_ALIGN_BYTE ".align 1\n"
#endif
/* INCBIN_CONST is used by incbin.c generated files */
#if defined(__cplusplus)
# define INCBIN_EXTERNAL extern "C"
# define INCBIN_CONST extern const
#else
# define INCBIN_EXTERNAL extern
# define INCBIN_CONST const
#endif
#if defined(__APPLE__)
/* The directives are different for Apple branded compilers */
# define INCBIN_SECTION ".data\n"
# define INCBIN_GLOBAL(NAME) ".globl " INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME "\n"
# define INCBIN_INT ".long "
# define INCBIN_MANGLE "_"
# define INCBIN_BYTE ".byte "
# define INCBIN_TYPE(...)
#else
#if CONFIG_LISAUI_ENV_ARCS_SDK
# define INCBIN_SECTION ".section .psram.data\n"
#else
# define INCBIN_SECTION ".section .data\n"
#endif
# define INCBIN_GLOBAL(NAME) ".global " INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME "\n"
# define INCBIN_INT ".int "
# if defined(__USER_LABEL_PREFIX__)
# define INCBIN_MANGLE INCBIN_STRINGIZE(__USER_LABEL_PREFIX__)
# else
# define INCBIN_MANGLE ""
# endif
# if defined(INCBIN_ARM)
/* On arm assemblers, `@' is used as a line comment token */
# define INCBIN_TYPE(NAME) ".type " INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME ", %object\n"
# elif defined(__MINGW32__) || defined(__MINGW64__)
/* Mingw doesn't support this directive either */
# define INCBIN_TYPE(NAME)
# else
/* It's safe to use `@' on other architectures */
# define INCBIN_TYPE(NAME) ".type " INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME ", @object\n"
# endif
# define INCBIN_BYTE ".byte "
#endif
/* List of style types used for symbol names */
#define INCBIN_STYLE_CAMEL 0
#define INCBIN_STYLE_SNAKE 1
/**
* @brief Specify the prefix to use for symbol names.
*
* By default this is `g', producing symbols of the form:
* @code
* #include "incbin.h"
* INCBIN(Foo, "foo.txt");
*
* // Now you have the following symbols:
* // const unsigned char gFooData[];
* // const unsigned char gFooEnd;
* // const unsigned int gFooSize;
* @endcode
*
* If however you specify a prefix before including: e.g:
* @code
* #define INCBIN_PREFIX incbin
* #include "incbin.h"
* INCBIN(Foo, "foo.txt");
*
* // Now you have the following symbols instead:
* // const unsigned char incbinFooData[];
* // const unsigned char incbinFooEnd;
* // const unsigned int incbinFooSize;
* @endcode
*/
#if !defined(INCBIN_PREFIX)
# define INCBIN_PREFIX g
#endif
/**
* @brief Specify the style used for symbol names.
*
* Possible options are
* - INCBIN_STYLE_CAMEL "CamelCase"
* - INCBIN_STYLE_SNAKE "snake_case"
*
* Default option is *INCBIN_STYLE_CAMEL* producing symbols of the form:
* @code
* #include "incbin.h"
* INCBIN(Foo, "foo.txt");
*
* // Now you have the following symbols:
* // const unsigned char <prefix>FooData[];
* // const unsigned char <prefix>FooEnd;
* // const unsigned int <prefix>FooSize;
* @endcode
*
* If however you specify a style before including: e.g:
* @code
* #define INCBIN_STYLE INCBIN_STYLE_SNAKE
* #include "incbin.h"
* INCBIN(foo, "foo.txt");
*
* // Now you have the following symbols:
* // const unsigned char <prefix>foo_data[];
* // const unsigned char <prefix>foo_end;
* // const unsigned int <prefix>foo_size;
* @endcode
*/
#if !defined(INCBIN_STYLE)
# define INCBIN_STYLE INCBIN_STYLE_CAMEL
#endif
/* Style lookup tables */
#define INCBIN_STYLE_0_DATA Data
#define INCBIN_STYLE_0_END End
#define INCBIN_STYLE_0_SIZE Size
#define INCBIN_STYLE_1_DATA _data
#define INCBIN_STYLE_1_END _end
#define INCBIN_STYLE_1_SIZE _size
/* Style lookup: returning identifier */
#define INCBIN_STYLE_IDENT(TYPE) \
INCBIN_CONCATENATE( \
INCBIN_STYLE_, \
INCBIN_CONCATENATE( \
INCBIN_EVAL(INCBIN_STYLE), \
INCBIN_CONCATENATE(_, TYPE)))
/* Style lookup: returning string literal */
#define INCBIN_STYLE_STRING(TYPE) \
INCBIN_STRINGIZE( \
INCBIN_STYLE_IDENT(TYPE)) \
/* Generate the global labels by indirectly invoking the macro with our style
* type and concatenating the name against them. */
#define INCBIN_GLOBAL_LABELS(NAME, TYPE) \
INCBIN_INVOKE( \
INCBIN_GLOBAL, \
INCBIN_CONCATENATE( \
NAME, \
INCBIN_INVOKE( \
INCBIN_STYLE_IDENT, \
TYPE))) \
INCBIN_INVOKE( \
INCBIN_TYPE, \
INCBIN_CONCATENATE( \
NAME, \
INCBIN_INVOKE( \
INCBIN_STYLE_IDENT, \
TYPE)))
/**
* @brief Externally reference binary data included in another translation unit.
*
* Produces three external symbols that reference the binary data included in
* another translation unit.
*
* The symbol names are a concatenation of `INCBIN_PREFIX' before *NAME*; with
* "Data", as well as "End" and "Size" after. An example is provided below.
*
* @param NAME The name given for the binary data
*
* @code
* INCBIN_EXTERN(Foo);
*
* // Now you have the following symbols:
* // extern const unsigned char <prefix>FooData[];
* // extern const unsigned char <prefix>FooEnd;
* // extern const unsigned int <prefix>FooSize;
* @endcode
*/
#define INCBIN_EXTERN(NAME) \
INCBIN_EXTERNAL INCBIN_ALIGN unsigned char \
INCBIN_CONCATENATE( \
INCBIN_CONCATENATE(INCBIN_PREFIX, NAME), \
INCBIN_STYLE_IDENT(DATA))[]; \
INCBIN_EXTERNAL INCBIN_ALIGN unsigned char * \
INCBIN_CONCATENATE( \
INCBIN_CONCATENATE(INCBIN_PREFIX, NAME), \
INCBIN_STYLE_IDENT(END)); \
INCBIN_EXTERNAL unsigned int \
INCBIN_CONCATENATE( \
INCBIN_CONCATENATE(INCBIN_PREFIX, NAME), \
INCBIN_STYLE_IDENT(SIZE))
/**
* @brief Include a binary file into the current translation unit.
*
* Includes a binary file into the current translation unit, producing three symbols
* for objects that encode the data and size respectively.
*
* The symbol names are a concatenation of `INCBIN_PREFIX' before *NAME*; with
* "Data", as well as "End" and "Size" after. An example is provided below.
*
* @param NAME The name to associate with this binary data (as an identifier.)
* @param FILENAME The file to include (as a string literal.)
*
* @code
* INCBIN(Icon, "icon.png");
*
* // Now you have the following symbols:
* // const unsigned char <prefix>IconData[];
* // const unsigned char <prefix>IconEnd;
* // const unsigned int <prefix>IconSize;
* @endcode
*
* @warning This must be used in global scope
* @warning The identifiers may be different if INCBIN_STYLE is not default
*
* To externally reference the data included by this in another translation unit
* please @see INCBIN_EXTERN.
*/
#ifdef _MSC_VER
#define INCBIN(NAME, FILENAME) \
INCBIN_EXTERN(NAME)
#else
#define INCBIN(NAME, FILENAME) \
__asm__(INCBIN_SECTION \
INCBIN_GLOBAL_LABELS(NAME, DATA) \
INCBIN_ALIGN_HOST \
INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME INCBIN_STYLE_STRING(DATA) ":\n" \
INCBIN_MACRO " \"" FILENAME "\"\n" \
INCBIN_GLOBAL_LABELS(NAME, END) \
INCBIN_ALIGN_BYTE \
INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME INCBIN_STYLE_STRING(END) ":\n" \
INCBIN_BYTE "1\n" \
INCBIN_GLOBAL_LABELS(NAME, SIZE) \
INCBIN_ALIGN_HOST \
INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME INCBIN_STYLE_STRING(SIZE) ":\n" \
INCBIN_INT INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME INCBIN_STYLE_STRING(END) " - " \
INCBIN_MANGLE INCBIN_STRINGIZE(INCBIN_PREFIX) #NAME INCBIN_STYLE_STRING(DATA) "\n" \
); \
INCBIN_EXTERN(NAME)
#endif
#endif

View File

@@ -0,0 +1,107 @@
/**
* @file lv_img_utils.c
* @author TsMax (QinYUN575@Foxmail.com)
* @brief
* @version 0.1
* @date 2024-02-23
*
* @copyright Copyright (c) 2021 - 2024 shenzhen listenai co., ltd.
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "lv_img_utils.h"
#if CONFIG_LISAUI_ENV_ARCS_SDK
#include "lisa_log.h"
// #define LOG_INF CLOG
#define LOG_INF
#else
// #define LOG_INF printf
#define LOG_INF LV_LOG_USER
#endif
int lv_img_png_src_init(lv_img_dsc_t *dsc, const void *data, uint32_t size)
{
img_header_t header;
LOG_INF("lv_img_png_src_init: data=%p, size=%d\r\n", data, size);
if (img_png_get_info(data, size, &header))
{
LOG_INF("lv_img_png_src_init: get png info failed\r\n");
return -1;
}
dsc->data = data;
dsc->data_size = size;
dsc->header.w = header.width;
dsc->header.h = header.height;
dsc->header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
LOG_INF("lv_img_png_src_init: w=%d, h=%d\r\n", dsc->header.w, dsc->header.h);
return 0;
}
int lv_img_png_res_init(lisaui_ui_res_img_png_t *res_img)
{
LOG_INF("lv_img_png_res_init: res_img=%p\r\n", res_img);
if (res_img == NULL)
{
LOG_INF("lv_img_png_res_init: res_img is NULL\r\n");
return -1;
}
if (res_img->img == NULL || res_img->data == NULL || res_img->size == 0)
{
LOG_INF("lv_img_png_res_init: img or data is NULL\r\n");
return -1;
}
lv_img_dsc_t *dsc = res_img->img;
const void *data = res_img->data;
uint32_t size = res_img->size;
return lv_img_png_src_init(dsc, data, size);
}
int lv_img_gif_src_init(lv_img_dsc_t *dsc, const void *data, uint32_t size)
{
img_header_t header;
LOG_INF("lv_img_gif_src_init: data=%p, size=%d\r\n", data, size);
if (img_gif_get_info(data, size, &header))
{
LOG_INF("lv_img_gif_src_init: get gif info failed\r\n");
return -1;
}
dsc->data = data;
dsc->data_size = size;
dsc->header.w = header.width;
dsc->header.h = header.height;
dsc->header.cf = LV_IMG_CF_RAW;
LOG_INF("lv_img_gif_src_init: w=%d, h=%d\r\n", dsc->header.w, dsc->header.h);
return 0;
}
int lv_img_jpg_src_init(lv_img_dsc_t *dsc, const void *data, uint32_t size)
{
img_header_t header;
LOG_INF("lv_img_jpg_src_init: data=%p, size=%d\r\n", data, size);
if (img_jpg_get_info(data, size, &header))
{
LOG_INF("lv_img_jpg_src_init: get jpg info failed\r\n");
return -1;
}
dsc->data = data;
dsc->data_size = size;
dsc->header.w = header.width;
dsc->header.h = header.height;
dsc->header.cf = LV_IMG_CF_RAW;
LOG_INF("lv_img_jpg_src_init: w=%d, h=%d\r\n", dsc->header.w, dsc->header.h);
return 0;
}

View File

@@ -0,0 +1,51 @@
/**
* @file lv_img_utils.h
* @author TsMax (QinYUN575@Foxmail.com)
* @brief
* @version 0.1
* @date 2024-02-23
*
* @copyright Copyright (c) 2021 - 2024 shenzhen listenai co., ltd.
*
* SPDX-License-Identifier: Apache-2.0
*/
#ifndef __LV_IMG_UTILS_H__
#define __LV_IMG_UTILS_H__
#ifdef __ZEPHYR__
#include <zephyr/kernel.h>
#include "lvgl.h"
#elif CONFIG_LVGL_ENV_SIMULATOR
#include "lvgl/lvgl.h"
#else
#include "lvgl.h"
#endif
#include "img_utils.h"
#include "incbin.h"
typedef struct _lisaui_ui_res_img_png_t {
uint32_t res_id;
lv_img_dsc_t *img;
const uint8_t *data;
uint32_t size;
} lisaui_ui_res_img_png_t;
#define UI_RES_IMG_NAME(name, path) \
static lv_obj_t *lv_obj_icon_##name;\
lv_img_dsc_t lv_img_dsc_##name;\
INCBIN(file_##name, path);
#define UI_RES_IMG_PNG(name) &lv_img_dsc_##name, gfile_##name##Data, gfile_##name##Size
// #define LV_UI_RES_IMG_PNG(name) icon_##name
#define LV_OBJ_ICON(name) lv_obj_icon_##name
#define LV_IMG_DSC(name) lv_img_dsc_##name
int lv_img_png_src_init(lv_img_dsc_t *dsc, const void *data, uint32_t size);
int lv_img_png_res_init(lisaui_ui_res_img_png_t *res_img);
int lv_img_gif_src_init(lv_img_dsc_t *dsc, const void *data, uint32_t size);
int lv_img_jpg_src_init(lv_img_dsc_t *dsc, const void *data, uint32_t size);
#endif // __LV_IMG_UTILS_H__

View File

@@ -0,0 +1,25 @@
add_subdirectory(widgets)
add_subdirectory(setting)
target_sources(
${LISA_UI_LIB_NAME}
PRIVATE
info_view.c
setting_view.c
lisa_ui_llm_base.c
lisa_ui_llm_primary.c
lisa_ui_keyboard_input.c
alarm_success_view.c
alarm_ring_view.c
quota_qrcode_view.c
)
if(CONFIG_OTA)
target_sources(${LISA_UI_LIB_NAME} PRIVATE ota_view.c)
endif()
target_include_directories(
${LISA_UI_LIB_NAME}
PRIVATE
./
)

View File

@@ -0,0 +1,180 @@
/**
* @file alarm_ring_view.c
* @brief Alarm ring page view implementation
*/
#include "alarm_ring_view.h"
#include "lisa_ui.h"
#include "lisa_ui_assets.h"
#include "lisa_ui_fonts.h"
#include <stdio.h>
#define TAG "alarm_ring_view"
/* 声明闹钟PNG图标 */
LV_IMG_DECLARE(icons_Icon_alarm_png);
typedef struct {
lv_obj_t obj;
lv_obj_t *title_label;
lv_obj_t *time_label;
lv_obj_t *date_label;
lv_obj_t *ring_icon;
lv_obj_t *stop_btn;
lv_timer_t *blink_timer;
bool is_blinking;
} alarm_ring_view_t;
static void blink_timer_cb(lv_timer_t *timer)
{
lv_obj_t *obj = (lv_obj_t *)timer->user_data;
if (!obj) return;
alarm_ring_view_t *view = (alarm_ring_view_t *)obj;
if (!view->ring_icon) return;
view->is_blinking = !view->is_blinking;
if (view->is_blinking) {
lv_obj_set_style_opa(view->ring_icon, LV_OPA_30, LV_PART_MAIN);
} else {
lv_obj_set_style_opa(view->ring_icon, LV_OPA_COVER, LV_PART_MAIN);
}
}
static void alarm_ring_view_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
LV_UNUSED(class_p);
alarm_ring_view_t *view = (alarm_ring_view_t *)obj;
view->blink_timer = NULL;
view->is_blinking = false;
lv_obj_set_size(obj, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_bg_color(obj, lv_color_black(), LV_PART_MAIN);
lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN);
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
view->title_label = lv_label_create(obj);
if (!view->title_label) {
LISA_UI_LOGE("Failed to create title label");
return;
}
lv_label_set_text(view->title_label, _("Alarm Reminder"));
lv_obj_set_style_text_color(view->title_label, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_text_font(view->title_label, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_align(view->title_label, LV_ALIGN_TOP_MID, 0, 20);
view->time_label = lv_label_create(obj);
if (!view->time_label) {
LISA_UI_LOGE("Failed to create time label");
return;
}
lv_label_set_text(view->time_label, "00:00");
lv_obj_set_style_text_color(view->time_label, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_text_font(view->time_label, &lv_font_rubik_bold_64, LV_PART_MAIN);
lv_obj_align(view->time_label, LV_ALIGN_CENTER, 0, -40);
view->date_label = lv_label_create(obj);
if (!view->date_label) {
LISA_UI_LOGE("Failed to create date label");
return;
}
lv_label_set_text(view->date_label, "");
lv_obj_set_style_text_color(view->date_label, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_text_font(view->date_label, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_align_to(view->date_label, view->time_label, LV_ALIGN_OUT_BOTTOM_MID, -50, 5);
view->ring_icon = lv_img_create(obj);
if (!view->ring_icon) {
LISA_UI_LOGE("Failed to create ring icon");
return;
}
lv_img_set_src(view->ring_icon, &icons_Icon_alarm_png);
lv_obj_align(view->ring_icon, LV_ALIGN_TOP_RIGHT, -20, 20);
view->stop_btn = lv_btn_create(obj);
if (!view->stop_btn) {
LISA_UI_LOGE("Failed to create stop button");
return;
}
lv_obj_set_size(view->stop_btn, 200, 40);
lv_obj_set_style_bg_color(view->stop_btn, lv_color_hex(0xFF6B6B), LV_PART_MAIN);
lv_obj_set_style_radius(view->stop_btn, 25, LV_PART_MAIN);
lv_obj_set_style_border_width(view->stop_btn, 0, LV_PART_MAIN);
lv_obj_align(view->stop_btn, LV_ALIGN_BOTTOM_MID, 0, -10);
lv_obj_t *btn_label = lv_label_create(view->stop_btn);
if (!btn_label) {
LISA_UI_LOGE("Failed to create button label");
return;
}
lv_label_set_text(btn_label, "唤醒或按键可终止闹铃");
lv_obj_set_style_text_color(btn_label, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_text_font(btn_label, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_align(btn_label, LV_ALIGN_CENTER, 0, -3);
view->blink_timer = lv_timer_create(blink_timer_cb, 500, obj);
if (!view->blink_timer) {
LISA_UI_LOGE("Failed to create blink timer");
return;
}
view->is_blinking = false;
}
static void alarm_ring_view_destructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
LV_UNUSED(class_p);
alarm_ring_view_t *view = (alarm_ring_view_t *)obj;
if (view->blink_timer) {
lv_timer_del(view->blink_timer);
view->blink_timer = NULL;
}
}
const lv_obj_class_t alarm_ring_view_class = {
.constructor_cb = alarm_ring_view_constructor,
.destructor_cb = alarm_ring_view_destructor,
.instance_size = sizeof(alarm_ring_view_t),
.base_class = &lv_obj_class
};
lv_obj_t *alarm_ring_view_create(lv_obj_t *parent)
{
lv_obj_t *obj = lv_obj_class_create_obj(&alarm_ring_view_class, parent);
lv_obj_class_init_obj(obj);
return obj;
}
void alarm_ring_view_set_time(lv_obj_t *obj, const char *time_str)
{
if (!obj || !time_str) return;
alarm_ring_view_t *view = (alarm_ring_view_t *)obj;
if (view->time_label) {
lv_label_set_text(view->time_label, time_str);
}
}
void alarm_ring_view_set_date(lv_obj_t *obj, const char *date_str)
{
if (!obj || !date_str) return;
alarm_ring_view_t *view = (alarm_ring_view_t *)obj;
if (view->date_label) {
lv_label_set_text(view->date_label, date_str);
}
}
void alarm_ring_view_set_stop_cb(lv_obj_t *obj, lv_event_cb_t cb, void *user_data)
{
if (!obj || !cb) return;
alarm_ring_view_t *view = (alarm_ring_view_t *)obj;
if (view->stop_btn) {
lv_obj_add_event_cb(view->stop_btn, cb, LV_EVENT_CLICKED, user_data);
}
}

View File

@@ -0,0 +1,55 @@
/**
* @file alarm_ring_view.h
* @brief Alarm ring page view header
*/
#ifndef __ALARM_RING_VIEW_H__
#define __ALARM_RING_VIEW_H__
#ifdef __cplusplus
extern "C" {
#endif
#include "lvgl.h"
/** Alarm ring view class definition */
extern const lv_obj_class_t alarm_ring_view_class;
/**
* @brief Create alarm ring view object
*
* @param parent Parent object, NULL for current screen
* @return lv_obj_t* Created view object, NULL on failure
*/
lv_obj_t *alarm_ring_view_create(lv_obj_t *parent);
/**
* @brief Set alarm time text
*
* @param obj View object
* @param time_str Time string (e.g. "16:30")
*/
void alarm_ring_view_set_time(lv_obj_t *obj, const char *time_str);
/**
* @brief Set alarm date text
*
* @param obj View object
* @param date_str Date string (e.g. "12月29日 周日")
*/
void alarm_ring_view_set_date(lv_obj_t *obj, const char *date_str);
/**
* @brief Set stop button callback
*
* @param obj View object
* @param cb Callback function
* @param user_data User data passed to callback
*/
void alarm_ring_view_set_stop_cb(lv_obj_t *obj, lv_event_cb_t cb, void *user_data);
#ifdef __cplusplus
}
#endif
#endif /* __ALARM_RING_VIEW_H__ */

View File

@@ -0,0 +1,89 @@
/**
* @file alarm_success_view.c
* @brief Alarm success page view implementation
*/
#include "alarm_success_view.h"
#include "lisa_ui.h"
#include "lisa_ui_assets.h"
#include "lisa_ui_fonts.h"
#define TAG "alarm_success_view"
typedef struct {
lv_obj_t obj;
lv_obj_t *title_label;
lv_obj_t *time_label;
lv_obj_t *date_label;
} alarm_success_view_t;
static void alarm_success_view_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
LV_UNUSED(class_p);
alarm_success_view_t *view = (alarm_success_view_t *)obj;
lv_obj_set_size(obj, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_bg_color(obj, lv_color_black(), LV_PART_MAIN);
lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN);
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
view->title_label = lv_label_create(obj);
lv_label_set_text(view->title_label, _("Alarm set successfully"));
lv_obj_set_style_text_color(view->title_label, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_text_font(view->title_label, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_align(view->title_label, LV_ALIGN_TOP_MID, 0, 30);
view->time_label = lv_label_create(obj);
lv_label_set_text(view->time_label, "00:00");
lv_obj_set_style_text_color(view->time_label, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_text_font(view->time_label, &lv_font_rubik_bold_64, LV_PART_MAIN);
lv_obj_align(view->time_label, LV_ALIGN_CENTER, 0, -30);
view->date_label = lv_label_create(obj);
lv_label_set_text(view->date_label, "");
lv_obj_set_style_text_color(view->date_label, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_text_font(view->date_label, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_align_to(view->date_label, view->time_label, LV_ALIGN_OUT_BOTTOM_MID, -50, 30);
}
static void alarm_success_view_destructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
LV_UNUSED(class_p);
LV_UNUSED(obj);
}
const lv_obj_class_t alarm_success_view_class = {
.constructor_cb = alarm_success_view_constructor,
.destructor_cb = alarm_success_view_destructor,
.instance_size = sizeof(alarm_success_view_t),
.base_class = &lv_obj_class
};
lv_obj_t *alarm_success_view_create(lv_obj_t *parent)
{
lv_obj_t *obj = lv_obj_class_create_obj(&alarm_success_view_class, parent);
lv_obj_class_init_obj(obj);
return obj;
}
void alarm_success_view_set_time(lv_obj_t *obj, const char *time_str)
{
if (!obj || !time_str) return;
alarm_success_view_t *view = (alarm_success_view_t *)obj;
if (view->time_label) {
lv_label_set_text(view->time_label, time_str);
}
}
void alarm_success_view_set_date(lv_obj_t *obj, const char *date_str)
{
if (!obj || !date_str) return;
alarm_success_view_t *view = (alarm_success_view_t *)obj;
if (view->date_label) {
lv_label_set_text(view->date_label, date_str);
}
}

View File

@@ -0,0 +1,46 @@
/**
* @file alarm_success_view.h
* @brief Alarm success page view header
*/
#ifndef __ALARM_SUCCESS_VIEW_H__
#define __ALARM_SUCCESS_VIEW_H__
#ifdef __cplusplus
extern "C" {
#endif
#include "lvgl.h"
/** Alarm success view class definition */
extern const lv_obj_class_t alarm_success_view_class;
/**
* @brief Create alarm success view object
*
* @param parent Parent object, NULL for current screen
* @return lv_obj_t* Created view object, NULL on failure
*/
lv_obj_t *alarm_success_view_create(lv_obj_t *parent);
/**
* @brief Set alarm time text
*
* @param obj View object
* @param time_str Time string (e.g. "16:30")
*/
void alarm_success_view_set_time(lv_obj_t *obj, const char *time_str);
/**
* @brief Set alarm date text
*
* @param obj View object
* @param date_str Date string (e.g. "12月29日 周日")
*/
void alarm_success_view_set_date(lv_obj_t *obj, const char *date_str);
#ifdef __cplusplus
}
#endif
#endif /* __ALARM_SUCCESS_VIEW_H__ */

View File

@@ -0,0 +1,186 @@
/**
* @file info_view.c
* @brief Info/QR Code page view implementation
*
* Pure UI layer implementation for info/qrcode display page.
* This view displays QR codes (BLE or network) and informational text.
*/
#define TAG "info_view"
#include "info_view.h"
#include "lisa_ui.h"
#include "lisa_ui_llm_base.h"
#include "lisa_ui_res.h"
#include "lisa_ui_fonts.h"
#include <string.h>
/** Default text displayed when no specific info is set */
#define DEFAULT_INFO_TEXT "Please scan QR code"
/**
* @brief Info view structure
* Inherits from lisa_ui_llm_base and adds QR code display elements
*/
struct lisa_ui_info_view {
lisa_ui_llm_base_t base_obj; /**< Base object */
/* Content elements */
lv_obj_t *top_label; /**< Top instruction text */
lv_obj_t *qr_img; /**< QR code image */
lv_obj_t *bottom_label; /**< Bottom information text */
/* QR image descriptor for RGB565 data */
lv_img_dsc_t qr_img_desc; /**< Image descriptor */
};
typedef struct lisa_ui_info_view lisa_ui_info_view_t;
static void lisa_ui_info_view_class_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj);
static void lisa_ui_info_view_class_destructor(const lv_obj_class_t *class_p, lv_obj_t *obj);
const lv_obj_class_t lisa_ui_info_view_class = {
.base_class = &lisa_ui_llm_base_class,
.instance_size = sizeof(lisa_ui_info_view_t),
.constructor_cb = lisa_ui_info_view_class_constructor,
.destructor_cb = lisa_ui_info_view_class_destructor,
};
static void lisa_ui_info_view_class_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
(void)class_p;
lisa_ui_info_view_t *info_view = (lisa_ui_info_view_t *)obj;
// Hide the bar to allow content to start from the very top
lv_obj_t *bar = lisa_ui_llm_base_bar_get(obj);
if (bar) {
lv_obj_add_flag(bar, LV_OBJ_FLAG_HIDDEN);
}
lv_obj_t *container = lisa_ui_llm_base_container_get(obj);
if (NULL == container) {
LISA_UI_LOGE("Failed to get base container");
return;
}
// Create QR code image in center
info_view->qr_img = lv_img_create(container);
#ifdef CONFIG_BOARD_ARCS_MINI_DOLL_V2
lv_obj_set_size(info_view->qr_img, 160, 160);
lv_obj_align(info_view->qr_img, LV_ALIGN_CENTER, -80, 0);
#else
lv_obj_set_size(info_view->qr_img, 148, 148);
lv_obj_align(info_view->qr_img, LV_ALIGN_CENTER, 0, 5);
#endif
// Create top label above QR code
info_view->top_label = lv_label_create(container);
lv_label_set_text(info_view->top_label, "");
lv_obj_set_style_text_color(info_view->top_label, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_text_font(info_view->top_label, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_set_style_text_align(info_view->top_label, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
lv_obj_set_width(info_view->top_label, LV_PCT(90));
lv_label_set_long_mode(info_view->top_label, LV_LABEL_LONG_WRAP);
lv_obj_align_to(info_view->top_label, info_view->qr_img, LV_ALIGN_OUT_TOP_MID, 0, -25);
// Create bottom label below QR code
info_view->bottom_label = lv_label_create(container);
lv_label_set_text(info_view->bottom_label, "");
lv_obj_set_style_text_color(info_view->bottom_label, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_text_font(info_view->bottom_label, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_set_style_text_align(info_view->bottom_label, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
lv_obj_set_width(info_view->bottom_label, LV_PCT(90));
lv_label_set_long_mode(info_view->bottom_label, LV_LABEL_LONG_WRAP);
lv_obj_align_to(info_view->bottom_label, info_view->qr_img, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
}
static void lisa_ui_info_view_class_destructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
(void)class_p;
(void)obj;
// Child objects auto-deleted by LVGL
}
// ===================== Public API Implementation =====================
lv_obj_t *lisa_ui_info_view_create(lv_obj_t *parent)
{
lv_obj_t *obj = lv_obj_class_create_obj(&lisa_ui_info_view_class, parent);
lv_obj_class_init_obj(obj);
// Set page title
lisa_ui_llm_base_set_title(obj, "Info");
return obj;
}
void lisa_ui_info_view_set_top_text(lv_obj_t *obj, const char *text)
{
if (!obj || !lv_obj_has_class(obj, &lisa_ui_info_view_class)) {
return;
}
lisa_ui_info_view_t *info_view = (lisa_ui_info_view_t *)obj;
if (info_view->top_label) {
lv_label_set_text(info_view->top_label, text ? text : DEFAULT_INFO_TEXT);
}
}
void lisa_ui_info_view_set_bottom_text(lv_obj_t *obj, const char *text)
{
if (!obj || !lv_obj_has_class(obj, &lisa_ui_info_view_class)) {
return;
}
lisa_ui_info_view_t *info_view = (lisa_ui_info_view_t *)obj;
if (info_view->bottom_label) {
lv_label_set_text(info_view->bottom_label, text ? text : "");
}
}
void lisa_ui_info_view_set_qr_image(lv_obj_t *obj, const void *src)
{
if (!obj || !lv_obj_has_class(obj, &lisa_ui_info_view_class)) {
return;
}
lisa_ui_info_view_t *info_view = (lisa_ui_info_view_t *)obj;
if (info_view->qr_img && src) {
lv_img_set_src(info_view->qr_img, src);
}
}
lv_obj_t *lisa_ui_info_view_get_qr_image(lv_obj_t *obj)
{
if (!obj || !lv_obj_has_class(obj, &lisa_ui_info_view_class)) {
return NULL;
}
lisa_ui_info_view_t *info_view = (lisa_ui_info_view_t *)obj;
return info_view->qr_img;
}
void lisa_ui_info_view_set_qr_rgb565(lv_obj_t *obj, const uint16_t *data, uint16_t width, uint16_t height, uint32_t data_size)
{
if (!obj || !lv_obj_has_class(obj, &lisa_ui_info_view_class) || !data) {
return;
}
lisa_ui_info_view_t *info_view = (lisa_ui_info_view_t *)obj;
if (!info_view->qr_img) {
return;
}
// Setup image descriptor for RGB565 format
memset(&info_view->qr_img_desc, 0, sizeof(lv_img_dsc_t));
info_view->qr_img_desc.header.always_zero = 0;
info_view->qr_img_desc.header.w = width;
info_view->qr_img_desc.header.h = height;
info_view->qr_img_desc.header.cf = LV_IMG_CF_TRUE_COLOR; // RGB565 format
info_view->qr_img_desc.data_size = data_size;
info_view->qr_img_desc.data = (const uint8_t *)data;
// Set image source to descriptor
lv_img_set_src(info_view->qr_img, &info_view->qr_img_desc);
}

View File

@@ -0,0 +1,76 @@
/**
* @file info_view.h
* @brief Info/QR Code page view header
*
* Provides view interface for info/qrcode display page.
*/
#ifndef __LISA_UI_INFO_VIEW_H__
#define __LISA_UI_INFO_VIEW_H__
#ifdef __cplusplus
extern "C" {
#endif
#include "lvgl.h"
#include "lisa_ui_llm_base.h"
/** Info view class definition */
extern const lv_obj_class_t lisa_ui_info_view_class;
/**
* @brief Create info view object
*
* @param parent Parent object, NULL for current screen
* @return lv_obj_t* Created info view object, NULL on failure
*/
lv_obj_t *lisa_ui_info_view_create(lv_obj_t *parent);
/**
* @brief Set top instruction text
*
* @param obj Info view object
* @param text Text to display, NULL for default text
*/
void lisa_ui_info_view_set_top_text(lv_obj_t *obj, const char *text);
/**
* @brief Set bottom information text
*
* @param obj Info view object
* @param text Text to display, NULL to clear
*/
void lisa_ui_info_view_set_bottom_text(lv_obj_t *obj, const char *text);
/**
* @brief Set QR code image source
*
* @param obj Info view object
* @param src Image source (file path, C array, or symbol)
*/
void lisa_ui_info_view_set_qr_image(lv_obj_t *obj, const void *src);
/**
* @brief Get QR code image object
*
* @param obj Info view object
* @return lv_obj_t* QR image object for advanced operations
*/
lv_obj_t *lisa_ui_info_view_get_qr_image(lv_obj_t *obj);
/**
* @brief Set QR code image from RGB565 data
*
* @param obj Info view object
* @param data RGB565 image data pointer
* @param width Image width in pixels
* @param height Image height in pixels
* @param data_size Total data size in bytes
*/
void lisa_ui_info_view_set_qr_rgb565(lv_obj_t *obj, const uint16_t *data, uint16_t width, uint16_t height, uint32_t data_size);
#ifdef __cplusplus
}
#endif
#endif /* __LISA_UI_INFO_VIEW_H__ */

View File

@@ -0,0 +1,348 @@
/**
* @file lisa_ui_keyboard_input.c
* @brief 通用键盘输入组件实现
*/
#include <stdio.h>
#include <string.h>
#include "lisa_ui_keyboard_input.h"
#include "lisa_ui.h"
#include "lisa_ui_assets.h"
#include "lisa_ui_fonts.h"
#define TAG "keyboard_input"
typedef struct {
lv_obj_t *panel; /* 全屏面板 */
lv_obj_t *title_label; /* 标题标签 */
lv_obj_t *input_label; /* 输入框标签 (如"密码:") */
lv_obj_t *input; /* 文本输入框 */
lv_obj_t *keyboard; /* 键盘 */
lisa_ui_keyboard_input_confirm_cb_t confirm_cb; /* 确认回调 */
void *confirm_user_data; /* 确认回调用户数据 */
lisa_ui_keyboard_input_cancel_cb_t cancel_cb; /* 取消回调 */
void *cancel_user_data; /* 取消回调用户数据 */
bool switching_keyboard; /* 是否正在切换键盘 */
} lisa_ui_keyboard_input_t;
/* 键盘按钮映射 - 小写字母 */
static const char *keyboard_map_lc[] = {
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "\n",
"q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "\n",
"a", "s", "d", "f", "g", "h", "j", "k", "l", "\n",
"z", "x", "c", "v", "b", "n", "m", LV_SYMBOL_BACKSPACE, "\n",
"ABC", "#+=", " ", " ", "enter", ""
};
/* 键盘按钮映射 - 大写字母 */
static const char *keyboard_map_uc[] = {
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "\n",
"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "\n",
"A", "S", "D", "F", "G", "H", "J", "K", "L", "\n",
"Z", "X", "C", "V", "B", "N", "M", LV_SYMBOL_BACKSPACE, "\n",
"abc", "#+=", " ", " ", "enter", ""
};
/* 键盘按钮映射 - 符号 */
static const char *keyboard_map_special[] = {
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "\n",
"!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "\n",
"-", "_", "=", "+", "[", "]", "{", "}", "\n",
";", ":", "'", "\"", ",", ".", "/", LV_SYMBOL_BACKSPACE, "\n",
"abc", "ABC", " ", " ", "enter", ""
};
/* 键盘事件处理 */
static void keyboard_event_cb(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code != LV_EVENT_PRESSED) {
return;
}
lv_obj_t *keyboard = lv_event_get_target(e);
lisa_ui_keyboard_input_t *kb_input = (lisa_ui_keyboard_input_t *)lv_event_get_user_data(e);
uint16_t btn_id = lv_btnmatrix_get_selected_btn(keyboard);
if (btn_id == LV_BTNMATRIX_BTN_NONE) {
return;
}
const char *txt = lv_btnmatrix_get_btn_text(keyboard, btn_id);
if (!txt) {
return;
}
/* 处理删除键 */
if (strcmp(txt, LV_SYMBOL_BACKSPACE) == 0) {
lv_textarea_del_char(kb_input->input);
return;
}
/* 处理确定按钮 */
if (strcmp(txt, "enter") == 0) {
if (kb_input->confirm_cb) {
const char *text = lv_textarea_get_text(kb_input->input);
kb_input->confirm_cb(text, kb_input->confirm_user_data);
}
return;
}
/* 处理键盘切换 */
if (strcmp(txt, "abc") == 0) {
lv_btnmatrix_set_map(keyboard, keyboard_map_lc);
return;
}
if (strcmp(txt, "ABC") == 0) {
lv_btnmatrix_set_map(keyboard, keyboard_map_uc);
return;
}
if (strcmp(txt, "#+=") == 0) {
lv_btnmatrix_set_map(keyboard, keyboard_map_special);
return;
}
/* 处理普通字符输入 */
lv_textarea_add_text(kb_input->input, txt);
}
/* 返回按钮事件处理 */
static void back_event_cb(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code != LV_EVENT_CLICKED) {
return;
}
lisa_ui_keyboard_input_t *kb_input = (lisa_ui_keyboard_input_t *)lv_event_get_user_data(e);
/* 调用取消回调 */
if (kb_input->cancel_cb) {
kb_input->cancel_cb(kb_input->cancel_user_data);
}
/* 隐藏界面 */
lisa_ui_keyboard_input_hide((lv_obj_t *)kb_input);
}
lv_obj_t *lisa_ui_keyboard_input_create(lv_obj_t *parent)
{
if (!parent) {
parent = lv_scr_act();
}
/* 分配内存 */
lisa_ui_keyboard_input_t *kb_input = lv_mem_alloc(sizeof(lisa_ui_keyboard_input_t));
if (!kb_input) {
LISA_UI_LOGE("Failed to allocate memory for keyboard input");
return NULL;
}
memset(kb_input, 0, sizeof(lisa_ui_keyboard_input_t));
kb_input->switching_keyboard = false;
/* 创建全屏面板 */
kb_input->panel = lv_obj_create(parent);
lv_obj_set_size(kb_input->panel, LV_PCT(100), LV_PCT(100));
lv_obj_align(kb_input->panel, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_style_bg_color(kb_input->panel, lv_color_hex(0x000000), LV_PART_MAIN);
lv_obj_set_style_bg_opa(kb_input->panel, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_border_width(kb_input->panel, 0, LV_PART_MAIN);
lv_obj_set_style_pad_all(kb_input->panel, 0, LV_PART_MAIN);
lv_obj_add_flag(kb_input->panel, LV_OBJ_FLAG_HIDDEN); /* 默认隐藏 */
/* 保存结构体指针到panel的user_data */
lv_obj_set_user_data(kb_input->panel, kb_input);
/* 创建返回按钮 */
lv_obj_t *back_btn = lv_btn_create(kb_input->panel);
lv_obj_set_size(back_btn, 40, 40);
lv_obj_align(back_btn, LV_ALIGN_TOP_LEFT, 5, 5);
lv_obj_set_style_bg_opa(back_btn, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_border_width(back_btn, 0, LV_PART_MAIN);
lv_obj_set_style_shadow_width(back_btn, 0, LV_PART_MAIN);
lv_obj_t *back_icon = lv_img_create(back_btn);
lv_img_set_src(back_icon, &icons_icon_back_png);
lv_obj_center(back_icon);
lv_obj_add_event_cb(back_btn, back_event_cb, LV_EVENT_CLICKED, kb_input);
/* 创建标题标签 */
kb_input->title_label = lv_label_create(kb_input->panel);
lv_obj_set_style_text_font(kb_input->title_label, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_set_style_text_color(kb_input->title_label, lv_color_white(), LV_PART_MAIN);
lv_obj_align(kb_input->title_label, LV_ALIGN_TOP_MID, 0, 10);
lv_label_set_text(kb_input->title_label, "");
/* 创建输入框容器 */
lv_obj_t *input_container = lv_obj_create(kb_input->panel);
lv_obj_set_size(input_container, LV_PCT(90), 40);
lv_obj_align(input_container, LV_ALIGN_TOP_MID, 0, 50);
lv_obj_set_style_bg_color(input_container, lv_color_hex(0x2a2a2a), LV_PART_MAIN);
lv_obj_set_style_border_color(input_container, lv_color_hex(0x4a4a4a), LV_PART_MAIN);
lv_obj_set_style_border_width(input_container, 2, LV_PART_MAIN);
lv_obj_set_style_radius(input_container, 8, LV_PART_MAIN);
lv_obj_set_style_pad_all(input_container, 10, LV_PART_MAIN);
/* 创建输入框 */
kb_input->input = lv_textarea_create(input_container);
lv_obj_set_size(kb_input->input, LV_PCT(100), LV_PCT(100));
lv_obj_align(kb_input->input, LV_ALIGN_CENTER, 0, 0);
// lv_obj_set_style_text_font(kb_input->input, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_set_style_text_color(kb_input->input, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_bg_color(kb_input->input, lv_color_hex(0x2a2a2a), LV_PART_MAIN);
lv_obj_set_style_bg_opa(kb_input->input, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_border_width(kb_input->input, 0, LV_PART_MAIN);
lv_obj_set_style_pad_all(kb_input->input, 8, LV_PART_MAIN);
lv_textarea_set_one_line(kb_input->input, true);
lv_textarea_set_placeholder_text(kb_input->input, "please input");
/* 输入框标签(保留变量以兼容set_label函数,但设为隐藏) */
kb_input->input_label = lv_label_create(kb_input->panel);
lv_obj_add_flag(kb_input->input_label, LV_OBJ_FLAG_HIDDEN);
/* 创建键盘 */
kb_input->keyboard = lv_btnmatrix_create(kb_input->panel);
lv_obj_set_size(kb_input->keyboard, LV_PCT(100), 150); /* 降低键盘高度从180到150 */
lv_obj_align(kb_input->keyboard, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_style_bg_color(kb_input->keyboard, lv_color_hex(0x1a1a1a), LV_PART_MAIN);
lv_obj_set_style_border_width(kb_input->keyboard, 0, LV_PART_MAIN);
lv_obj_set_style_pad_all(kb_input->keyboard, 2, LV_PART_MAIN);
lv_obj_set_style_pad_gap(kb_input->keyboard, 2, LV_PART_MAIN);
// lv_obj_set_style_text_font(kb_input->keyboard, &lv_font_chinese_16, LV_PART_MAIN); /* 字体从18降到16 */
/* 按钮样式 */
lv_obj_set_style_bg_color(kb_input->keyboard, lv_color_hex(0x3a3a3a), LV_PART_ITEMS);
lv_obj_set_style_bg_color(kb_input->keyboard, lv_color_hex(0x5a5a5a), LV_PART_ITEMS | LV_STATE_PRESSED);
lv_obj_set_style_text_color(kb_input->keyboard, lv_color_white(), LV_PART_ITEMS);
lv_obj_set_style_radius(kb_input->keyboard, 4, LV_PART_ITEMS);
lv_btnmatrix_set_map(kb_input->keyboard, keyboard_map_lc);
lv_obj_add_event_cb(kb_input->keyboard, keyboard_event_cb, LV_EVENT_PRESSED, kb_input);
LISA_UI_LOGD("Keyboard input created");
return (lv_obj_t *)kb_input;
}
void lisa_ui_keyboard_input_set_title(lv_obj_t *obj, const char *title)
{
if (!obj || !title) {
return;
}
lisa_ui_keyboard_input_t *kb_input = (lisa_ui_keyboard_input_t *)obj;
lv_label_set_text(kb_input->title_label, title);
}
void lisa_ui_keyboard_input_set_placeholder(lv_obj_t *obj, const char *placeholder)
{
if (!obj || !placeholder) {
return;
}
lisa_ui_keyboard_input_t *kb_input = (lisa_ui_keyboard_input_t *)obj;
lv_textarea_set_placeholder_text(kb_input->input, placeholder);
}
void lisa_ui_keyboard_input_set_label(lv_obj_t *obj, const char *label)
{
if (!obj || !label) {
return;
}
lisa_ui_keyboard_input_t *kb_input = (lisa_ui_keyboard_input_t *)obj;
lv_label_set_text(kb_input->input_label, label);
}
void lisa_ui_keyboard_input_set_password_mode(lv_obj_t *obj, bool enable)
{
if (!obj) {
return;
}
lisa_ui_keyboard_input_t *kb_input = (lisa_ui_keyboard_input_t *)obj;
lv_textarea_set_password_mode(kb_input->input, enable);
}
void lisa_ui_keyboard_input_set_confirm_cb(lv_obj_t *obj,
lisa_ui_keyboard_input_confirm_cb_t confirm_cb,
void *user_data)
{
if (!obj) {
return;
}
lisa_ui_keyboard_input_t *kb_input = (lisa_ui_keyboard_input_t *)obj;
kb_input->confirm_cb = confirm_cb;
kb_input->confirm_user_data = user_data;
}
void lisa_ui_keyboard_input_set_cancel_cb(lv_obj_t *obj,
lisa_ui_keyboard_input_cancel_cb_t cancel_cb,
void *user_data)
{
if (!obj) {
return;
}
lisa_ui_keyboard_input_t *kb_input = (lisa_ui_keyboard_input_t *)obj;
kb_input->cancel_cb = cancel_cb;
kb_input->cancel_user_data = user_data;
}
void lisa_ui_keyboard_input_show(lv_obj_t *obj)
{
if (!obj) {
return;
}
lisa_ui_keyboard_input_t *kb_input = (lisa_ui_keyboard_input_t *)obj;
/* 清空输入框 */
lv_textarea_set_text(kb_input->input, "");
/* 重置键盘为小写模式 */
lv_btnmatrix_set_map(kb_input->keyboard, keyboard_map_lc);
/* 显示面板 */
lv_obj_clear_flag(kb_input->panel, LV_OBJ_FLAG_HIDDEN);
LISA_UI_LOGD("Keyboard input shown");
}
void lisa_ui_keyboard_input_hide(lv_obj_t *obj)
{
if (!obj) {
return;
}
lisa_ui_keyboard_input_t *kb_input = (lisa_ui_keyboard_input_t *)obj;
lv_obj_add_flag(kb_input->panel, LV_OBJ_FLAG_HIDDEN);
LISA_UI_LOGD("Keyboard input hidden");
}
const char *lisa_ui_keyboard_input_get_text(lv_obj_t *obj)
{
if (!obj) {
return "";
}
lisa_ui_keyboard_input_t *kb_input = (lisa_ui_keyboard_input_t *)obj;
return lv_textarea_get_text(kb_input->input);
}
void lisa_ui_keyboard_input_clear(lv_obj_t *obj)
{
if (!obj) {
return;
}
lisa_ui_keyboard_input_t *kb_input = (lisa_ui_keyboard_input_t *)obj;
lv_textarea_set_text(kb_input->input, "");
}

View File

@@ -0,0 +1,113 @@
/**
* @file lisa_ui_keyboard_input.h
* @brief 通用键盘输入组件
* @description 提供一个全屏的键盘输入界面,支持密码输入模式
*/
#ifndef __LISA_UI_KEYBOARD_INPUT_H__
#define __LISA_UI_KEYBOARD_INPUT_H__
#ifdef __cplusplus
extern "C" {
#endif
#include "lvgl.h"
/**
* @brief 键盘输入确认回调函数类型
* @param text 输入的文本内容
* @param user_data 用户数据
*/
typedef void (*lisa_ui_keyboard_input_confirm_cb_t)(const char *text, void *user_data);
/**
* @brief 键盘输入取消回调函数类型
* @param user_data 用户数据
*/
typedef void (*lisa_ui_keyboard_input_cancel_cb_t)(void *user_data);
/**
* @brief 创建键盘输入组件
* @param parent 父对象
* @return lv_obj_t* 创建的键盘输入对象
*/
lv_obj_t *lisa_ui_keyboard_input_create(lv_obj_t *parent);
/**
* @brief 设置标题文本
* @param obj 键盘输入对象
* @param title 标题文本
*/
void lisa_ui_keyboard_input_set_title(lv_obj_t *obj, const char *title);
/**
* @brief 设置输入框提示文本
* @param obj 键盘输入对象
* @param placeholder 提示文本
*/
void lisa_ui_keyboard_input_set_placeholder(lv_obj_t *obj, const char *placeholder);
/**
* @brief 设置输入框标签文本 (如"密码:"、"用户名:"等)
* @param obj 键盘输入对象
* @param label 标签文本
*/
void lisa_ui_keyboard_input_set_label(lv_obj_t *obj, const char *label);
/**
* @brief 设置密码模式
* @param obj 键盘输入对象
* @param enable true=密码模式, false=普通模式
*/
void lisa_ui_keyboard_input_set_password_mode(lv_obj_t *obj, bool enable);
/**
* @brief 设置确认回调
* @param obj 键盘输入对象
* @param confirm_cb 确认回调函数
* @param user_data 用户数据
*/
void lisa_ui_keyboard_input_set_confirm_cb(lv_obj_t *obj,
lisa_ui_keyboard_input_confirm_cb_t confirm_cb,
void *user_data);
/**
* @brief 设置取消回调
* @param obj 键盘输入对象
* @param cancel_cb 取消回调函数
* @param user_data 用户数据
*/
void lisa_ui_keyboard_input_set_cancel_cb(lv_obj_t *obj,
lisa_ui_keyboard_input_cancel_cb_t cancel_cb,
void *user_data);
/**
* @brief 显示键盘输入界面
* @param obj 键盘输入对象
*/
void lisa_ui_keyboard_input_show(lv_obj_t *obj);
/**
* @brief 隐藏键盘输入界面
* @param obj 键盘输入对象
*/
void lisa_ui_keyboard_input_hide(lv_obj_t *obj);
/**
* @brief 获取输入的文本
* @param obj 键盘输入对象
* @return const char* 输入的文本
*/
const char *lisa_ui_keyboard_input_get_text(lv_obj_t *obj);
/**
* @brief 清空输入框
* @param obj 键盘输入对象
*/
void lisa_ui_keyboard_input_clear(lv_obj_t *obj);
#ifdef __cplusplus
}
#endif
#endif /* __LISA_UI_KEYBOARD_INPUT_H__ */

View File

@@ -0,0 +1,175 @@
#include "lvgl.h"
#include "lisa_ui_llm_base.h"
#include <string.h>
static void lisa_ui_llm_base_class_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj);
const lv_obj_class_t lisa_ui_llm_base_class = {
.base_class = &lv_obj_class,
.width_def = LV_PCT(100),
.height_def = LV_PCT(100),
.instance_size = sizeof(lisa_ui_llm_base_t),
.constructor_cb = lisa_ui_llm_base_class_constructor,
};
static lv_style_t lisa_ui_base_style;
static uint8_t lisa_ui_base_style_inited = 0;
static void lisa_ui_base_style_init(void)
{
lv_style_init(&lisa_ui_base_style);
lv_style_set_bg_color(&lisa_ui_base_style, lv_color_hex(LISA_UI_LLM_BASE_DEFAULT_BG_COLOR));
lv_style_set_bg_opa(&lisa_ui_base_style, LV_OPA_COVER);
lv_style_set_border_width(&lisa_ui_base_style, 0);
lv_style_set_border_opa(&lisa_ui_base_style, LV_OPA_COVER);
}
static void lisa_ui_llm_base_class_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
if (!lisa_ui_base_style_inited) {
lisa_ui_base_style_init();
lisa_ui_base_style_inited = 1;
}
lv_obj_add_style(obj, &lisa_ui_base_style, LV_PART_MAIN);
lv_obj_set_size(obj, LV_PCT(100), LV_PCT(100));
// 禁用主对象的拖动/滚动功能
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_scrollbar_mode(obj, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_scroll_dir(obj, LV_DIR_NONE);
// 设置flex布局和对齐方式
lv_obj_set_layout(obj, LV_LAYOUT_FLEX);
lv_obj_set_style_flex_flow(obj, LV_FLEX_FLOW_COLUMN, LV_PART_MAIN);
lv_obj_set_style_flex_main_place(obj, LV_FLEX_ALIGN_START, LV_PART_MAIN);
lv_obj_set_style_flex_cross_place(obj, LV_FLEX_ALIGN_START, LV_PART_MAIN);
lv_obj_set_style_bg_color(obj, lv_color_hex(0x000000), LV_PART_MAIN);
lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, LV_PART_MAIN);
// 清除所有间距
lv_obj_set_style_pad_all(obj, 0, LV_PART_MAIN);
lv_obj_set_style_pad_gap(obj, 12, LV_PART_MAIN);
lisa_ui_llm_base_t *llm_base = (lisa_ui_llm_base_t *)obj;
// 创建任务栏
llm_base->bar = lv_obj_create(obj);
lv_obj_set_width(llm_base->bar, LV_PCT(100));
lv_obj_set_height(llm_base->bar, LISA_UI_LLM_BASE_DEFAULT_BAR_HEIGHT);
lv_obj_set_style_bg_color(llm_base->bar, lv_color_hex(LISA_UI_LLM_BASE_DEFAULT_BAR_COLOR), LV_PART_MAIN);
lv_obj_set_style_bg_opa(llm_base->bar, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_border_width(llm_base->bar, 0, LV_PART_MAIN);
lv_obj_set_style_pad_all(llm_base->bar, 0, LV_PART_MAIN);
lv_obj_set_style_radius(llm_base->bar, 0, LV_PART_MAIN);
// 禁用任务栏的拖动/滚动功能
lv_obj_clear_flag(llm_base->bar, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_scrollbar_mode(llm_base->bar, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_scroll_dir(llm_base->bar, LV_DIR_NONE);
// 创建容器
llm_base->container = lv_obj_create(obj);
lv_obj_set_width(llm_base->container, LV_PCT(100));
lv_obj_set_style_bg_color(llm_base->container, lv_color_hex(0x000000), LV_PART_MAIN);
lv_obj_set_style_bg_opa(llm_base->container, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_border_width(llm_base->container, 0, LV_PART_MAIN);
lv_obj_set_style_pad_all(llm_base->container, 0, LV_PART_MAIN);
lv_obj_set_style_radius(llm_base->container, 0, LV_PART_MAIN);
lv_obj_set_flex_grow(llm_base->container, 1);
// 禁用滑动功能
lv_obj_clear_flag(llm_base->container, LV_OBJ_FLAG_SCROLLABLE);
// 或者设置滑动条模式为关闭
lv_obj_set_scrollbar_mode(llm_base->container, LV_SCROLLBAR_MODE_OFF);
// 设置滑动方向为无
lv_obj_set_scroll_dir(llm_base->container, LV_DIR_NONE);
// 初始化字符串
memset(llm_base->title, 0, sizeof(llm_base->title));
}
// ===================== 公共API实现 =====================
lv_obj_t *lisa_ui_llm_base_bar_get(lv_obj_t *obj)
{
if (obj == NULL) {
// 添加错误日志
return NULL;
}
if (!LISA_UI_LLM_BASE_CLASS_CHECK(obj)) {
// 添加类型检查失败的错误日志
return NULL;
}
lisa_ui_llm_base_t *llm_base = (lisa_ui_llm_base_t *)obj;
return llm_base->bar;
}
lv_obj_t *lisa_ui_llm_base_container_get(lv_obj_t *obj)
{
if (!lisa_ui_llm_base_is_valid(obj)) {
return NULL;
}
lisa_ui_llm_base_t *llm_base = (lisa_ui_llm_base_t *)obj;
return llm_base->container;
}
void lisa_ui_llm_base_set_title(lv_obj_t *obj, const char *title)
{
if (!lisa_ui_llm_base_is_valid(obj)) {
return;
}
lisa_ui_llm_base_t *llm_base = (lisa_ui_llm_base_t *)obj;
if (title) {
strncpy(llm_base->title, title, sizeof(llm_base->title) - 1);
llm_base->title[sizeof(llm_base->title) - 1] = '\0';
} else {
memset(llm_base->title, 0, sizeof(llm_base->title));
}
}
const char *lisa_ui_llm_base_get_title(lv_obj_t *obj)
{
if (!lisa_ui_llm_base_is_valid(obj)) {
return NULL;
}
lisa_ui_llm_base_t *llm_base = (lisa_ui_llm_base_t *)obj;
return llm_base->title;
}
void lisa_ui_llm_base_set_bar_color(lv_obj_t *obj, lv_color_t color)
{
if (!lisa_ui_llm_base_is_valid(obj)) {
return;
}
lisa_ui_llm_base_t *llm_base = (lisa_ui_llm_base_t *)obj;
if (llm_base->bar) {
lv_obj_set_style_bg_color(llm_base->bar, color, LV_PART_MAIN);
}
}
void lisa_ui_llm_base_set_bar_height(lv_obj_t *obj, lv_coord_t height)
{
if (!lisa_ui_llm_base_is_valid(obj)) {
return;
}
lisa_ui_llm_base_t *llm_base = (lisa_ui_llm_base_t *)obj;
if (llm_base->bar && height > 0) {
lv_obj_set_height(llm_base->bar, height);
}
}
void lisa_ui_llm_base_clear_container(lv_obj_t *obj)
{
if (!lisa_ui_llm_base_is_valid(obj)) {
return;
}
lisa_ui_llm_base_t *llm_base = (lisa_ui_llm_base_t *)obj;
if (llm_base->container) {
lv_obj_clean(llm_base->container);
}
}

View File

@@ -0,0 +1,159 @@
/**
* @file lisa_ui_llm_base.h
* @brief LLM UI基础组件头文件
*
* 提供LLM聊天界面的基础UI组件包含任务栏和内容容器的布局管理。
* 该组件基于LVGL图形库提供了完整的创建、管理和样式设置API。
*
* @version 1.0.0
* @date 2025
* @author Lisa UI Team
*/
#ifndef __LISA_UI_LLM_BASE_H__
#define __LISA_UI_LLM_BASE_H__
#ifdef __cplusplus
extern "C" {
#endif
/*===========================================
* 包含文件
*==========================================*/
#include "lvgl.h"
#include <stdint.h>
#include <stdbool.h>
/** 默认配置参数 */
#define LISA_UI_LLM_BASE_DEFAULT_BAR_HEIGHT 36 /*!< 默认任务栏高度 */
#define LISA_UI_LLM_BASE_DEFAULT_BAR_COLOR 0x000000 /*!< 默认任务栏颜色 */
#define LISA_UI_LLM_BASE_DEFAULT_BG_COLOR 0x000000 /*!< 默认背景颜色 */
#define LISA_UI_LLM_BASE_MAX_TITLE_LENGTH 16 /*!< 最大标题长度 */
/** 类型检查宏 */
#define LISA_UI_LLM_BASE_CLASS_CHECK(obj) \
(lv_obj_has_class(obj, &lisa_ui_llm_base_class))
/**
* @brief LLM UI基础组件结构体
*
* 该结构体定义了LLM UI基础组件的数据成员包含LVGL对象、
* 任务栏、内容容器和标题信息。
*/
struct lisa_ui_llm_base {
lv_obj_t obj; /*!< LVGL基础对象 */
lv_obj_t *bar; /*!< 任务栏对象 */
lv_obj_t *container; /*!< 页面容器对象 */
char title[LISA_UI_LLM_BASE_MAX_TITLE_LENGTH]; /*!< 页面标题 */
};
/** LLM UI基础组件类型定义 */
typedef struct lisa_ui_llm_base lisa_ui_llm_base_t;
/*===========================================
* 全局变量声明
*==========================================*/
/** LLM UI基础组件类定义 */
extern const lv_obj_class_t lisa_ui_llm_base_class;
/*===========================================
* 函数声明
*==========================================*/
/**
* @brief 获取任务栏对象
*
* @param obj LLM UI基础组件对象
* @return lv_obj_t* 任务栏对象失败返回NULL
*
* @note 返回的对象可用于添加标题标签、按钮等UI元素
*/
lv_obj_t *lisa_ui_llm_base_bar_get(lv_obj_t *obj);
/**
* @brief 获取内容容器对象
*
* @param obj LLM UI基础组件对象
* @return lv_obj_t* 内容容器对象失败返回NULL
*
* @note 返回的容器对象用于放置聊天消息、输入框等内容
*/
lv_obj_t *lisa_ui_llm_base_container_get(lv_obj_t *obj);
/**
* @brief 设置页面标题
*
* @param obj LLM UI基础组件对象
* @param title 标题字符串如果为NULL则清空标题
*
* @note 标题长度限制为LISA_UI_LLM_BASE_MAX_TITLE_LENGTH-1个字符
*/
void lisa_ui_llm_base_set_title(lv_obj_t *obj, const char *title);
/**
* @brief 获取页面标题
*
* @param obj LLM UI基础组件对象
* @return const char* 标题字符串失败返回NULL
*/
const char *lisa_ui_llm_base_get_title(lv_obj_t *obj);
/**
* @brief 设置任务栏背景颜色
*
* @param obj LLM UI基础组件对象
* @param color 颜色值
*
* @code
* // 设置任务栏为绿色
* lisa_ui_llm_base_set_bar_color(llm_ui, lv_color_hex(0x4CAF50));
* @endcode
*/
void lisa_ui_llm_base_set_bar_color(lv_obj_t *obj, lv_color_t color);
/**
* @brief 设置任务栏高度
*
* @param obj LLM UI基础组件对象
* @param height 高度值(像素)
*
* @note 高度改变会影响整体布局,建议在组件创建后立即设置
*/
void lisa_ui_llm_base_set_bar_height(lv_obj_t *obj, lv_coord_t height);
/**
* @brief 清空内容容器
*
* @param obj LLM UI基础组件对象
*
* @note 该函数会删除容器中的所有子对象
*/
void lisa_ui_llm_base_clear_container(lv_obj_t *obj);
/*===========================================
* 内联函数
*==========================================*/
/**
* @brief 检查对象是否为LLM UI基础组件类型
*
* @param obj 要检查的对象
* @return true 是LLM UI基础组件类型
* @return false 不是LLM UI基础组件类型
*/
static inline bool lisa_ui_llm_base_is_valid(lv_obj_t *obj)
{
if (obj == NULL) {
return false;
}
// 检查是否为基类或其派生类
return LISA_UI_LLM_BASE_CLASS_CHECK(obj);
}
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* __LISA_UI_LLM_BASE_H__ */

View File

@@ -0,0 +1,502 @@
#include <string.h>
#include "lisa_ui.h"
#include "lisa_ui_llm_base.h"
#include "lisa_ui_llm_primary.h"
#include "lisa_ui_assets.h"
#include "lisa_ui_fonts.h"
#include "lisa_ui_anim_ext.h"
LV_IMG_DECLARE(icons_ic_status_full_duplex_png);
LV_IMG_DECLARE(icons_ic_status_alarm_png);
static void lisa_ui_llm_primary_class_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj);
const lv_obj_class_t lisa_ui_llm_primary_class = {
.base_class = &lisa_ui_llm_base_class,
.instance_size = sizeof(lisa_ui_llm_primary_t),
.constructor_cb = lisa_ui_llm_primary_class_constructor,
};
static void lisa_ui_llm_primary_class_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
(void)class_p;
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
lv_obj_t *bar = lisa_ui_llm_base_bar_get(obj);
if (NULL == bar) {
LISA_UI_LOGE("Failed to get base bar");
return;
}
lv_obj_t *container = lisa_ui_llm_base_container_get(obj);
if (NULL == container) {
LISA_UI_LOGE("Failed to get base container");
return;
}
// 设置任务栏布局
lv_obj_set_style_pad_left(bar, 8, LV_PART_MAIN);
lv_obj_set_style_pad_right(bar, 8, LV_PART_MAIN);
lv_obj_set_style_pad_top(bar, 8, LV_PART_MAIN);
lv_obj_set_style_pad_bottom(bar, 8, LV_PART_MAIN);
lv_obj_set_style_min_height(bar, 36, LV_PART_MAIN);
lv_obj_set_style_base_dir(bar, LV_BASE_DIR_LTR, LV_PART_MAIN);
#ifndef CONFIG_BOARD_ARCS_MINI
// 创建设置图标(左边)
llm_primary->settings_icon = lv_img_create(bar);
lv_obj_add_flag(llm_primary->settings_icon, LV_OBJ_FLAG_CLICKABLE);
#endif
// 创建WiFi图标右边
llm_primary->wifi_icon = lv_img_create(bar);
// 创建交互模式图标(全双工)
llm_primary->full_duplex_icon = lv_img_create(bar);
// 创建闹钟图标
llm_primary->alarm_icon = lv_img_create(bar);
// 创建电量图标
llm_primary->battery_icon = lv_img_create(bar);
// 创建状态文本标签(中间)
llm_primary->status_label = lv_label_create(bar);
// 设置主容器为垂直布局
lv_obj_set_layout(container, LV_LAYOUT_FLEX);
lv_obj_set_style_flex_flow(container, LV_FLEX_FLOW_COLUMN, LV_PART_MAIN);
lv_obj_set_style_flex_main_place(container, LV_FLEX_ALIGN_CENTER, LV_PART_MAIN);
lv_obj_set_style_flex_cross_place(container, LV_FLEX_ALIGN_CENTER, LV_PART_MAIN);
// 创建emoji动画容器上半部分
llm_primary->emoji_container = lv_obj_create(container);
// 创建emoji动画图片
llm_primary->emoji_anim = lisa_ui_anim_ext_create(llm_primary->emoji_container);
// 创建拍照图片覆盖在emoji上层默认隐藏
llm_primary->img = lv_img_create(container);
// 创建图片提示文本标签(覆盖在图片下方,默认隐藏)
llm_primary->img_hint = lv_label_create(obj);
// 创建内容文本容器(下半部分)
llm_primary->content_container = lv_obj_create(container);
// 创建内容文本标签
llm_primary->content_label = lv_textarea_create(llm_primary->content_container);
#ifdef CONFIG_BOARD_ARCS_MINI_DOLL_V2
lv_obj_add_flag(bar, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(llm_primary->content_container, LV_OBJ_FLAG_HIDDEN);
#endif
}
// ===================== 公共API实现 =====================
lv_obj_t *lisa_ui_llm_primary_create(lv_obj_t *parent)
{
lv_obj_t *obj = lv_obj_class_create_obj(&lisa_ui_llm_primary_class, parent);
lv_obj_class_init_obj(obj);
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
#ifndef CONFIG_BOARD_ARCS_MINI
// 设置图标初始化和位置(左边)
lv_img_set_src(llm_primary->settings_icon, &icons_ic_launch_setting_png);
lv_obj_align(llm_primary->settings_icon, LV_ALIGN_LEFT_MID, 0, 0);
#endif
// 设置WiFi图标位置右边
lv_img_set_src(llm_primary->wifi_icon, &icons_ic_status_wifi_no_connect_png);
lv_obj_align(llm_primary->wifi_icon, LV_ALIGN_LEFT_MID, 0, 0);
// 设置交互模式图标(默认隐藏)
lv_img_set_src(llm_primary->full_duplex_icon, &icons_ic_status_full_duplex_png);
lv_obj_align(llm_primary->full_duplex_icon, LV_ALIGN_LEFT_MID, 20, 0);
lv_obj_add_flag(llm_primary->full_duplex_icon, LV_OBJ_FLAG_HIDDEN);
// 设置闹钟图标(默认隐藏)
lv_img_set_src(llm_primary->alarm_icon, &icons_ic_status_alarm_png);
lv_obj_align(llm_primary->alarm_icon, LV_ALIGN_RIGHT_MID, -30, 0);
lv_obj_add_flag(llm_primary->alarm_icon, LV_OBJ_FLAG_HIDDEN);
// 设置电量图标(默认隐藏)
lv_img_set_src(llm_primary->battery_icon, &icons_ic_status_power0_png);
lv_obj_align(llm_primary->battery_icon, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_flag(llm_primary->battery_icon, LV_OBJ_FLAG_HIDDEN);
// 设置状态文本标签
lv_obj_set_style_text_letter_space(llm_primary->status_label, 1, LV_PART_MAIN);
lv_obj_set_style_text_color(llm_primary->status_label, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
lv_obj_set_style_text_font(llm_primary->status_label, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_set_style_text_align(llm_primary->status_label, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
lv_obj_set_style_text_line_space(llm_primary->status_label, 0, LV_PART_MAIN);
lv_obj_align(llm_primary->status_label, LV_ALIGN_CENTER, 0, 0);
// 设置emoji动画容器
lv_obj_set_style_bg_opa(llm_primary->emoji_container, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_border_width(llm_primary->emoji_container, 0, LV_PART_MAIN);
#ifdef CONFIG_BOARD_ARCS_MINI_DOLL_V2
lv_obj_set_size(llm_primary->emoji_container, LV_PCT(100), LV_PCT(100));
#else
lv_obj_set_size(llm_primary->emoji_container, LV_PCT(100), 104);
#endif
lv_obj_set_style_pad_all(llm_primary->emoji_container, 0, LV_PART_MAIN);
#ifndef CONFIG_BOARD_ARCS_MINI
lv_obj_set_style_pad_left(llm_primary->emoji_container, 55, LV_PART_MAIN); // 左边距50像素表情右移30像素
lv_obj_set_style_pad_right(llm_primary->emoji_container, 20, LV_PART_MAIN);
#endif
lv_obj_update_layout(llm_primary->emoji_container);
// 设置emoji容器为居中对齐
lv_obj_set_layout(llm_primary->emoji_container, LV_LAYOUT_FLEX);
lv_obj_set_style_flex_flow(llm_primary->emoji_container, LV_FLEX_FLOW_COLUMN, LV_PART_MAIN);
lv_obj_set_style_flex_main_place(llm_primary->emoji_container, LV_FLEX_ALIGN_CENTER, LV_PART_MAIN);
lv_obj_set_style_flex_cross_place(llm_primary->emoji_container, LV_FLEX_ALIGN_CENTER, LV_PART_MAIN);
// 设置emoji动画图片
lv_obj_set_size(llm_primary->emoji_anim, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
#ifdef CONFIG_BOARD_ARCS_MINI
lv_obj_add_flag(llm_primary->emoji_anim, LV_OBJ_FLAG_FLOATING);
lv_obj_center(llm_primary->emoji_anim);
#endif
lv_obj_set_size(llm_primary->img, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_add_flag(llm_primary->img, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(llm_primary->img, LV_OBJ_FLAG_FLOATING); /* Make img ignore flex layout from the start */
lv_label_set_text(llm_primary->img_hint, "图片可在小聆AI小程序中查看");
lv_obj_set_style_text_color(llm_primary->img_hint, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
lv_obj_set_style_text_font(llm_primary->img_hint, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_set_style_text_align(llm_primary->img_hint, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
lv_obj_add_flag(llm_primary->img_hint, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(llm_primary->img_hint, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_align(llm_primary->img_hint, LV_ALIGN_BOTTOM_MID, 0, -5);
lv_obj_move_foreground(llm_primary->img_hint);
// 设置内容文本容器
lv_obj_set_style_bg_opa(llm_primary->content_container, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_border_width(llm_primary->content_container, 0, LV_PART_MAIN);
lv_obj_set_size(llm_primary->content_container, LV_PCT(100), 85);
lv_obj_set_style_pad_all(llm_primary->content_container, 10, LV_PART_MAIN);
// 设置内容容器为居中对齐
lv_obj_set_layout(llm_primary->content_container, LV_LAYOUT_FLEX);
lv_obj_set_style_flex_flow(llm_primary->content_container, LV_FLEX_FLOW_COLUMN, LV_PART_MAIN);
lv_obj_set_style_flex_main_place(llm_primary->content_container, LV_FLEX_ALIGN_CENTER, LV_PART_MAIN);
lv_obj_set_style_flex_cross_place(llm_primary->content_container, LV_FLEX_ALIGN_CENTER, LV_PART_MAIN);
// 设置内容文本区域
lv_textarea_set_text(llm_primary->content_label, "请唤醒我");
lv_obj_set_style_text_color(llm_primary->content_label, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
lv_obj_set_style_text_font(llm_primary->content_label, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_set_style_text_align(llm_primary->content_label, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
// 隐藏边框和背景
lv_obj_set_style_border_width(llm_primary->content_label, 0, LV_PART_MAIN); // 隐藏边框
lv_obj_set_style_bg_opa(llm_primary->content_label, LV_OPA_TRANSP, LV_PART_MAIN); // 背景透明
lv_obj_set_style_outline_width(llm_primary->content_label, 0, LV_PART_MAIN); // 隐藏轮廓
// 设置滚动和交互
lv_textarea_set_cursor_click_pos(llm_primary->content_label, false); // 禁用光标点击
lv_obj_add_flag(llm_primary->content_label, LV_OBJ_FLAG_SCROLLABLE); // 启用滚动
lv_obj_set_scrollbar_mode(llm_primary->content_label, LV_SCROLLBAR_MODE_AUTO); // 自动显示滚动条
lv_obj_clear_flag(llm_primary->content_label, LV_OBJ_FLAG_CLICKABLE); // 禁用点击
// 设置尺寸和位置 - 使用更大的高度以便滚动
lv_obj_set_width(llm_primary->content_label, LV_PCT(100));
lv_obj_set_height(llm_primary->content_label, LV_PCT(100));
// 调整文本样式
lv_obj_set_style_text_line_space(llm_primary->content_label, 0, LV_PART_MAIN); // 使用字体自身line_height
lv_obj_set_style_text_letter_space(llm_primary->content_label, 1, LV_PART_MAIN); // 字符间距(字符与字符之间)
lv_obj_set_style_pad_top(llm_primary->content_label, 20, LV_PART_MAIN); // 上边距
lv_obj_set_style_pad_bottom(llm_primary->content_label, 20, LV_PART_MAIN); // 下边距
lv_obj_set_style_pad_left(llm_primary->content_label, 12, LV_PART_MAIN); // 左边距
lv_obj_set_style_pad_right(llm_primary->content_label, 12, LV_PART_MAIN); // 右边距
return obj;
}
#ifndef CONFIG_BOARD_ARCS_MINI
lv_obj_t *lisa_ui_llm_primary_settings_icon_get(lv_obj_t *obj)
{
if (!lisa_ui_llm_primary_is_valid(obj)) {
return NULL;
}
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
return llm_primary->settings_icon;
}
#endif
lv_obj_t *lisa_ui_llm_primary_wifi_icon_get(lv_obj_t *obj)
{
if (!lisa_ui_llm_primary_is_valid(obj)) {
return NULL;
}
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
return llm_primary->wifi_icon;
}
lv_obj_t *lisa_ui_llm_primary_battery_icon_get(lv_obj_t *obj)
{
if (!lisa_ui_llm_primary_is_valid(obj)) {
return NULL;
}
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
return llm_primary->battery_icon;
}
lv_obj_t *lisa_ui_llm_primary_status_label_get(lv_obj_t *obj)
{
if (!lisa_ui_llm_primary_is_valid(obj)) {
return NULL;
}
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
return llm_primary->status_label;
}
lv_obj_t *lisa_ui_llm_primary_content_label_get(lv_obj_t *obj)
{
if (!lisa_ui_llm_primary_is_valid(obj)) {
return NULL;
}
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
return llm_primary->content_label;
}
void lisa_ui_llm_primary_set_status_text(lv_obj_t *obj, const char *status)
{
if (!lisa_ui_llm_primary_is_valid(obj)) {
return;
}
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
if (llm_primary->status_label) {
lv_label_set_text(llm_primary->status_label, status);
}
}
void lisa_ui_llm_primary_set_content_text(lv_obj_t *obj, const char *content)
{
if (!lisa_ui_llm_primary_is_valid(obj)) {
return;
}
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
if (llm_primary->content_label) {
lv_textarea_set_text(llm_primary->content_label, content);
}
}
void lisa_ui_llm_primary_add_content_text(lv_obj_t *obj, const char *content)
{
if (!lisa_ui_llm_primary_is_valid(obj)) {
return;
}
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
if (llm_primary->content_label) {
lv_textarea_add_text(llm_primary->content_label, content);
}
}
void lisa_ui_llm_primary_set_wifi_img(lv_obj_t *obj, const void *img_path)
{
if (!lisa_ui_llm_primary_is_valid(obj)) {
return;
}
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
if (llm_primary->wifi_icon && img_path) {
lv_img_set_src(llm_primary->wifi_icon, img_path);
}
}
void lisa_ui_llm_primary_set_battery_img(lv_obj_t *obj, const void *img_path)
{
if (!lisa_ui_llm_primary_is_valid(obj)) {
return;
}
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
if (!llm_primary->battery_icon) {
return;
}
if (img_path) {
lv_img_set_src(llm_primary->battery_icon, img_path);
lv_obj_clear_flag(llm_primary->battery_icon, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(llm_primary->battery_icon, LV_OBJ_FLAG_HIDDEN);
}
}
void lisa_ui_llm_primary_set_full_duplex_icon_visible(lv_obj_t *obj, bool visible)
{
if (!lisa_ui_llm_primary_is_valid(obj)) {
return;
}
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
if (!llm_primary->full_duplex_icon) {
return;
}
if (visible) {
lv_obj_clear_flag(llm_primary->full_duplex_icon, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(llm_primary->full_duplex_icon, LV_OBJ_FLAG_HIDDEN);
}
}
void lisa_ui_llm_primary_set_alarm_icon_visible(lv_obj_t *obj, bool visible)
{
if (!lisa_ui_llm_primary_is_valid(obj)) {
return;
}
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
if (!llm_primary->alarm_icon) {
return;
}
if (visible) {
lv_obj_clear_flag(llm_primary->alarm_icon, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(llm_primary->alarm_icon, LV_OBJ_FLAG_HIDDEN);
}
}
#ifndef CONFIG_BOARD_ARCS_MINI
void lisa_ui_llm_primary_set_settings_icon_event_cb(lv_obj_t *obj, lv_event_cb_t event_cb, void *user_data)
{
if (!lisa_ui_llm_primary_is_valid(obj)) {
return;
}
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
if (llm_primary->settings_icon && event_cb) {
lv_obj_add_event_cb(llm_primary->settings_icon, event_cb, LV_EVENT_CLICKED, user_data);
}
}
#endif
lv_obj_t *lisa_ui_llm_primary_emoji_anim_get(lv_obj_t *obj)
{
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
return llm_primary->emoji_anim;
}
static void camera_img_anim_ready_cb(lv_anim_t *a)
{
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)a->user_data;
lv_obj_add_flag(llm_primary->img, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_style_opa(llm_primary->img, LV_OPA_COVER, 0); /* Reset opacity for next use */
lv_obj_clear_flag(llm_primary->emoji_anim, LV_OBJ_FLAG_HIDDEN);
#ifndef CONFIG_BOARD_ARCS_MINI_DOLL_V2
lv_obj_clear_flag(llm_primary->content_container, LV_OBJ_FLAG_HIDDEN);
#endif
}
static void camera_img_hide_timer_cb(lv_timer_t *timer)
{
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)timer->user_data;
lv_anim_t a;
lv_obj_add_flag(llm_primary->img, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(llm_primary->emoji_anim, LV_OBJ_FLAG_HIDDEN);
#ifndef CONFIG_BOARD_ARCS_MINI_DOLL_V2
lv_obj_clear_flag(llm_primary->content_container, LV_OBJ_FLAG_HIDDEN);
#endif
lv_timer_del(timer);
}
void lisa_ui_llm_primary_img_hide(lv_obj_t *obj)
{
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
lv_obj_add_flag(llm_primary->img, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(llm_primary->img_hint, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(llm_primary->emoji_anim, LV_OBJ_FLAG_HIDDEN);
#ifndef CONFIG_BOARD_ARCS_MINI_DOLL_V2
lv_obj_clear_flag(llm_primary->content_container, LV_OBJ_FLAG_HIDDEN);
#endif
}
void lisa_ui_llm_primary_img_show(lv_obj_t *obj, void *img)
{
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
bool enable_zoom = true;
if (llm_primary->img_hint) {
lv_obj_add_flag(llm_primary->img_hint, LV_OBJ_FLAG_HIDDEN);
}
if (llm_primary->content_label) {
lv_textarea_set_text(llm_primary->content_label, "");
}
lv_img_set_src(llm_primary->img, img);
/* URL网络图当前是LV_IMG_CF_RAW很多平台在RAW路径不支持zoom变换 */
if (lv_img_src_get_type(img) == LV_IMG_SRC_VARIABLE) {
const lv_img_dsc_t *dsc = (const lv_img_dsc_t *)img;
if (dsc && (dsc->header.cf == LV_IMG_CF_RAW ||
dsc->header.cf == LV_IMG_CF_RAW_ALPHA ||
dsc->header.cf == LV_IMG_CF_RAW_CHROMA_KEYED)) {
enable_zoom = false;
}
}
/* 128 = 256 * 0.5 (50% scale) */
lv_img_set_zoom(llm_primary->img, enable_zoom ? 128 : LV_IMG_ZOOM_NONE);
lv_obj_add_flag(llm_primary->img, LV_OBJ_FLAG_FLOATING);
lv_obj_update_layout(llm_primary->img);
lv_obj_align(llm_primary->img, LV_ALIGN_CENTER, 0, -25);
lv_obj_clear_flag(llm_primary->img, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_style_opa(llm_primary->img, LV_OPA_COVER, 0); /* Ensure fully visible */
lv_obj_add_flag(llm_primary->emoji_anim, LV_OBJ_FLAG_HIDDEN);
}
void lisa_ui_llm_primary_img_hint_show(lv_obj_t *obj, const char *text)
{
if (!lisa_ui_llm_primary_is_valid(obj)) {
return;
}
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
if (!llm_primary->img_hint) {
return;
}
lv_label_set_text(llm_primary->img_hint, text ? text : "");
lv_obj_align(llm_primary->img_hint, LV_ALIGN_BOTTOM_MID, 0, -5);
lv_obj_move_foreground(llm_primary->img_hint);
lv_obj_clear_flag(llm_primary->img_hint, LV_OBJ_FLAG_HIDDEN);
}
void lisa_ui_llm_primary_img_hint_hide(lv_obj_t *obj)
{
if (!lisa_ui_llm_primary_is_valid(obj)) {
return;
}
lisa_ui_llm_primary_t *llm_primary = (lisa_ui_llm_primary_t *)obj;
if (!llm_primary->img_hint) {
return;
}
lv_obj_add_flag(llm_primary->img_hint, LV_OBJ_FLAG_HIDDEN);
}

View File

@@ -0,0 +1,297 @@
/**
* @file lisa_ui_llm_primary.h
* @brief LLM UI主要组件头文件
*
* 继承自LLM UI基础组件提供具体的任务栏元素实现
* 包括WiFi图标、状态文本以及容器中的emoji动画和文本显示。
*
* @version 1.0.0
* @date 2024
* @author Lisa UI Team
*/
#ifndef __LISA_UI_LLM_PRIMARY_H__
#define __LISA_UI_LLM_PRIMARY_H__
#ifdef __cplusplus
extern "C" {
#endif
/*===========================================
* 包含文件
*==========================================*/
#include "lisa_ui_llm_base.h"
#include "lvgl.h"
#include <stdint.h>
#include <stdbool.h>
/*===========================================
* 宏定义
*==========================================*/
/** 状态文本最大长度 */
#define LISA_UI_LLM_PRIMARY_MAX_STATUS_LENGTH 32
/** 内容文本最大长度 */
#define LISA_UI_LLM_PRIMARY_MAX_CONTENT_LENGTH 256
/** 类型检查宏 */
#define LISA_UI_LLM_PRIMARY_CLASS_CHECK(obj) (lv_obj_has_class(obj, &lisa_ui_llm_primary_class))
/** 根据每帧时长计算总动画时长的辅助宏 */
#define LISA_UI_LLM_DURATION_BY_FRAME(frame_duration_ms, frame_count) ((frame_duration_ms) * (frame_count))
/**
* @brief LLM UI主要组件结构体
*
* 继承自LLM UI基础组件添加具体的任务栏元素和容器内容
*/
struct lisa_ui_llm_primary {
lisa_ui_llm_base_t base_obj; /*!< 基础对象 */
/* 任务栏元素 */
#ifndef CONFIG_BOARD_ARCS_MINI
lv_obj_t *settings_icon; /*!< 设置图标 */
#endif
lv_obj_t *wifi_icon; /*!< WiFi图标 */
lv_obj_t *full_duplex_icon; /*!< 交互模式图标(全双工) */
lv_obj_t *alarm_icon; /*!< 闹钟图标 */
lv_obj_t *battery_icon; /*!< 电量图标 */
lv_obj_t *status_label; /*!< 状态文本标签 */
/* 容器内容元素 */
lv_obj_t *emoji_container; /*!< emoji 动画容器 */
lv_obj_t *emoji_anim; /*!< emoji 动画图片 */
lv_obj_t *content_container; /*!< 内容文本容器 */
lv_obj_t *content_label; /*!< 内容文本标签 */
lv_obj_t *img;
lv_obj_t *img_hint;
};
/** LLM UI主要组件类型定义 */
typedef struct lisa_ui_llm_primary lisa_ui_llm_primary_t;
/*===========================================
* 全局变量声明
*==========================================*/
/** LLM UI主要组件类定义 */
extern const lv_obj_class_t lisa_ui_llm_primary_class;
/*===========================================
* 函数声明
*==========================================*/
/**
* @brief 创建LLM UI主要组件
*
* @param parent 父对象如果为NULL则使用当前活动屏幕
* @return lv_obj_t* 创建的LLM UI主要组件对象失败返回NULL
*
* @note 该函数会自动创建任务栏的所有元素和容器内容
*
* @code
* // 创建LLM UI主要组件
* lv_obj_t *llm_ui = lisa_ui_llm_primary_create(lv_scr_act());
* if (llm_ui != NULL) {
* // 组件创建成功,包含完整的任务栏和内容区域
* }
* @endcode
*/
lv_obj_t *lisa_ui_llm_primary_create(lv_obj_t *parent);
#ifndef CONFIG_BOARD_ARCS_MINI
/**
* @brief 获取设置图标对象
*
* @param obj LLM UI主要组件对象
* @return lv_obj_t* 设置图标对象失败返回NULL
*/
lv_obj_t *lisa_ui_llm_primary_settings_icon_get(lv_obj_t *obj);
#endif
/**
* @brief 获取WiFi图标对象
*
* @param obj LLM UI主要组件对象
* @return lv_obj_t* WiFi图标对象失败返回NULL
*/
lv_obj_t *lisa_ui_llm_primary_wifi_icon_get(lv_obj_t *obj);
/**
* @brief 获取状态文本标签对象
*
* @param obj LLM UI主要组件对象
* @return lv_obj_t* 状态文本标签对象失败返回NULL
*/
lv_obj_t *lisa_ui_llm_primary_status_label_get(lv_obj_t *obj);
/**
* @brief 获取电量图标对象
*
* @param obj LLM UI主要组件对象
* @return lv_obj_t* 电量图标对象失败返回NULL
*/
lv_obj_t *lisa_ui_llm_primary_battery_icon_get(lv_obj_t *obj);
/**
* @brief 获取emoji动画图片对象
*
* @param obj LLM UI主要组件对象
* @return lv_obj_t* emoji动画图片对象失败返回NULL
*/
lv_obj_t *lisa_ui_llm_primary_emoji_img_get(lv_obj_t *obj);
/**
* @brief 获取内容文本标签对象
*
* @param obj LLM UI主要组件对象
* @return lv_obj_t* 内容文本标签对象失败返回NULL
*/
lv_obj_t *lisa_ui_llm_primary_content_label_get(lv_obj_t *obj);
/**
* @brief 设置状态文本
*
* @param obj LLM UI主要组件对象
* @param status 状态文本如果为NULL则清空状态
*
*/
void lisa_ui_llm_primary_set_status_text(lv_obj_t *obj, const char *status);
/**
* @brief 设置内容文本
*
* @param obj LLM UI主要组件对象
* @param content 内容文本如果为NULL则清空内容
*
*/
void lisa_ui_llm_primary_set_content_text(lv_obj_t *obj, const char *content);
/**
* @brief 设置内容文本(追加)
*
* @param obj LLM UI主要组件对象
* @param content 内容文本如果为NULL则清空内容
*
*/
void lisa_ui_llm_primary_add_content_text(lv_obj_t *obj, const char *content);
/**
* @brief 设置WiFi图标显示状态
*
* @param obj LLM UI主要组件对象
* @param img_path 图片路径如果为NULL则不显示WiFi图标
*/
void lisa_ui_llm_primary_set_wifi_img(lv_obj_t *obj, const void *img_path);
/**
* @brief 设置交互模式图标显示状态
*
* @param obj LLM UI主要组件对象
* @param visible true显示false隐藏
*/
void lisa_ui_llm_primary_set_full_duplex_icon_visible(lv_obj_t *obj, bool visible);
/**
* @brief 设置闹钟图标显示状态
*
* @param obj LLM UI主要组件对象
* @param visible true显示false隐藏
*/
void lisa_ui_llm_primary_set_alarm_icon_visible(lv_obj_t *obj, bool visible);
/**
* @brief 设置电量图标显示状态
*
* @param obj LLM UI主要组件对象
* @param img_path 图片路径如果为NULL则隐藏图标
*/
void lisa_ui_llm_primary_set_battery_img(lv_obj_t *obj, const void *img_path);
/**
* @brief 启动emoji动画
*
* @param obj LLM UI主要组件对象
*/
void lisa_ui_llm_primary_start_emoji_animation(lv_obj_t *obj);
/**
* @brief 停止emoji动画
*
* @param obj LLM UI主要组件对象
*/
void lisa_ui_llm_primary_stop_emoji_animation(lv_obj_t *obj);
/**
* @brief 设置自定义emoji动画图片
*
* @param obj LLM UI主要组件对象
* @param images 图片数组指针
* @param images_count 图片数量
* @param duration 动画时长(毫秒)
* @param first_frame_delay 第一帧停留时间(毫秒)0表示无延迟
*/
void lisa_ui_llm_primary_set_custom_emoji_animation(lv_obj_t *obj, const void **images, uint32_t images_count,
uint32_t duration, uint32_t first_frame_delay);
/**
* @brief 设置emoji动画循环参数
*
* @param obj LLM UI主要组件对象
* @param loop_count 循环次数0表示无限循环
*/
void lisa_ui_llm_primary_set_loop_count(lv_obj_t *obj, uint32_t loop_count);
#ifndef CONFIG_BOARD_ARCS_MINI
/**
* @brief 设置设置图标事件回调
*
* @param obj LLM UI主要组件对象
* @param event_cb 事件回调函数
* @param user_data 用户数据
*/
void lisa_ui_llm_primary_set_settings_icon_event_cb(lv_obj_t *obj, lv_event_cb_t event_cb, void *user_data);
#endif
/**
* @brief 显示拍照图片到表情容器
*
* @param obj LLM UI主要组件对象
* @param rgb565_data RGB565图片数据
* @param width 图片宽度
* @param height 图片高度
*/
void lisa_ui_llm_primary_show_camera_image(lv_obj_t *obj, const uint16_t *rgb565_data, uint32_t width, uint32_t height);
/**
* @brief 隐藏拍照图片
*
* @param obj LLM UI主要组件对象
*/
void lisa_ui_llm_primary_hide_camera_image(lv_obj_t *obj);
lv_obj_t *lisa_ui_llm_primary_emoji_anim_get(lv_obj_t *obj);
/*===========================================
* 内联函数
*==========================================*/
/**
* @brief 检查对象是否为LLM UI主要组件类型
*
* @param obj 要检查的对象
* @return true 是LLM UI主要组件类型
* @return false 不是LLM UI主要组件类型
*/
static inline bool lisa_ui_llm_primary_is_valid(lv_obj_t *obj)
{
return (obj != NULL) && LISA_UI_LLM_PRIMARY_CLASS_CHECK(obj);
}
void lisa_ui_llm_primary_img_show(lv_obj_t *obj, void *img);
void lisa_ui_llm_primary_img_hide(lv_obj_t *obj);
void lisa_ui_llm_primary_img_hint_show(lv_obj_t *obj, const char *text);
void lisa_ui_llm_primary_img_hint_hide(lv_obj_t *obj);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif /* __LISA_UI_LLM_PRIMARY_H__ */

View File

@@ -0,0 +1,186 @@
/**
* @file ota_view.c
* @brief OTA status view implementation
*/
#define TAG "ota_view"
#include "ota_view.h"
#include "lisa_ui.h"
#include "lisa_ui_llm_base.h"
#include "lisa_ui_fonts.h"
#include <stdio.h>
typedef struct {
lisa_ui_llm_base_t base;
lv_obj_t *status_label;
lv_obj_t *progress_label;
lv_obj_t *eta_label;
char status_text[48];
char progress_text[32];
char eta_text[32];
} lisa_ui_ota_view_t;
static void ota_view_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj);
static void ota_view_destructor(const lv_obj_class_t *class_p, lv_obj_t *obj);
const lv_obj_class_t lisa_ui_ota_view_class = {
.base_class = &lisa_ui_llm_base_class,
.instance_size = sizeof(lisa_ui_ota_view_t),
.constructor_cb = ota_view_constructor,
.destructor_cb = ota_view_destructor,
};
static void ota_view_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
LV_UNUSED(class_p);
lisa_ui_ota_view_t *view = (lisa_ui_ota_view_t *)obj;
lv_obj_t *bar = lisa_ui_llm_base_bar_get(obj);
if (bar) {
lv_obj_add_flag(bar, LV_OBJ_FLAG_HIDDEN);
}
lv_obj_t *container = lisa_ui_llm_base_container_get(obj);
if (!container) {
return;
}
view->status_label = lv_label_create(container);
lv_label_set_text(view->status_label, "正在检查更新…");
lv_obj_set_style_text_color(view->status_label, lv_color_white(), 0);
lv_obj_set_style_text_font(view->status_label, &lv_font_chinese_16, 0);
lv_obj_set_width(view->status_label, lv_pct(100));
lv_obj_set_style_text_align(view->status_label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(view->status_label, LV_ALIGN_CENTER, 0, -16);
view->progress_label = lv_label_create(container);
lv_label_set_text(view->progress_label, "");
lv_obj_set_style_text_color(view->progress_label, lv_color_white(), 0);
lv_obj_set_style_text_font(view->progress_label, &lv_font_chinese_16, 0);
lv_obj_set_style_text_opa(view->progress_label, LV_OPA_70, 0);
lv_obj_set_width(view->progress_label, lv_pct(100));
lv_obj_set_style_text_align(view->progress_label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(view->progress_label, LV_ALIGN_CENTER, 0, 8);
view->eta_label = lv_label_create(container);
lv_label_set_text(view->eta_label, "");
lv_obj_set_style_text_color(view->eta_label, lv_color_white(), 0);
lv_obj_set_style_text_font(view->eta_label, &lv_font_chinese_16, 0);
lv_obj_set_style_text_opa(view->eta_label, LV_OPA_50, 0);
lv_obj_set_width(view->eta_label, lv_pct(100));
lv_obj_set_style_text_align(view->eta_label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(view->eta_label, LV_ALIGN_BOTTOM_MID, 0, -12);
}
static void ota_view_destructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
(void)class_p;
(void)obj;
}
lv_obj_t *lisa_ui_ota_view_create(lv_obj_t *parent)
{
lv_obj_t *obj = lv_obj_class_create_obj(&lisa_ui_ota_view_class, parent);
lv_obj_class_init_obj(obj);
return obj;
}
static const char *ota_target_name(ota_target_e target)
{
switch (target) {
case OTA_TARGET_WAKE_WORD:
return "唤醒词";
case OTA_TARGET_PROMPT_TONE:
return "提示音";
case OTA_TARGET_EMOJI:
return "表情";
default:
return "资源";
}
}
static void ota_view_format_size(char *buf, size_t buf_size, uint32_t bytes)
{
if (bytes >= 1024 * 1024) {
snprintf(buf, buf_size, "%.1fMB", (float)bytes / (1024.0f * 1024.0f));
} else {
snprintf(buf, buf_size, "%uKB", bytes / 1024);
}
}
void lisa_ui_ota_view_update(lv_obj_t *obj, const ota_state_t *state)
{
if (!obj || !lv_obj_has_class(obj, &lisa_ui_ota_view_class)) {
return;
}
lisa_ui_ota_view_t *view = (lisa_ui_ota_view_t *)obj;
switch (state->state) {
case OTA_STATE_UPDATING: {
// 第一行N/M 正在更新XXX…
if (state->update_total > 0) {
snprintf(view->status_text, sizeof(view->status_text), "%u/%u 正在更新%s…",
state->update_index, state->update_total, ota_target_name(state->target));
} else {
snprintf(view->status_text, sizeof(view->status_text), "正在更新%s…",
ota_target_name(state->target));
}
lv_label_set_text_static(view->status_label, view->status_text);
// 第二行:已下载 / 总大小
if (state->bytes_total > 0) {
char processed_str[16], total_str[16];
ota_view_format_size(processed_str, sizeof(processed_str), state->bytes_processed);
ota_view_format_size(total_str, sizeof(total_str), state->bytes_total);
snprintf(view->progress_text, sizeof(view->progress_text), "%s / %s", processed_str, total_str);
lv_label_set_text_static(view->progress_label, view->progress_text);
} else {
lv_label_set_text_static(view->progress_label, "");
}
// 第三行:预计剩余时间
if (state->bytes_processed < state->bytes_total) {
if (state->bytes_processed > 0 && state->elapsed_ms > 0) {
uint32_t remaining_bytes = state->bytes_total - state->bytes_processed;
float speed = (float)state->bytes_processed / (float)state->elapsed_ms;
uint32_t eta_sec = (uint32_t)((float)remaining_bytes / speed / 1000.0f);
uint32_t eta_min = (eta_sec + 59) / 60;
if (eta_min <= 1) {
snprintf(view->eta_text, sizeof(view->eta_text), "预计不到1分钟");
} else {
snprintf(view->eta_text, sizeof(view->eta_text), "预计%u分钟", eta_min);
}
} else {
snprintf(view->eta_text, sizeof(view->eta_text), "预计约1分钟");
}
lv_label_set_text_static(view->eta_label, view->eta_text);
} else {
lv_label_set_text_static(view->eta_label, "");
}
break;
}
case OTA_STATE_SUCCESSED:
lv_label_set_text_static(view->status_label, "更新完毕");
lv_label_set_text_static(view->progress_label, "");
if (state->reboot == OTA_REBOOT_STRATEGY_AUTO) {
lv_label_set_text_static(view->eta_label, "正在重启…");
} else {
lv_label_set_text_static(view->eta_label, "请手动重启设备");
}
break;
case OTA_STATE_FAILED:
lv_label_set_text_static(view->status_label, "更新失败");
lv_label_set_text_static(view->progress_label, "");
if (state->reboot == OTA_REBOOT_STRATEGY_AUTO) {
lv_label_set_text_static(view->eta_label, "正在重启…");
} else {
lv_label_set_text_static(view->eta_label, "请手动重启设备");
}
break;
default:
break;
}
}

View File

@@ -0,0 +1,10 @@
#ifndef __LISA_UI_OTA_VIEW_H__
#define __LISA_UI_OTA_VIEW_H__
#include "lvgl.h"
#include "ota_manager.h"
lv_obj_t *lisa_ui_ota_view_create(lv_obj_t *parent);
void lisa_ui_ota_view_update(lv_obj_t *obj, const ota_state_t *state);
#endif

View File

@@ -0,0 +1,102 @@
#include "quota_qrcode_view.h"
#include "lisa_ui_llm_base.h"
#include "lisa_ui_fonts.h"
#define TAG "quota_qrcode_view"
typedef struct {
lv_obj_t obj;
lv_obj_t *qr_img;
lv_obj_t *title_label;
lv_obj_t *message_label;
} quota_qrcode_view_t;
static void quota_qrcode_view_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj);
static void quota_qrcode_view_destructor(const lv_obj_class_t *class_p, lv_obj_t *obj);
const lv_obj_class_t lisa_ui_quota_qrcode_view_class = {
.constructor_cb = quota_qrcode_view_constructor,
.destructor_cb = quota_qrcode_view_destructor,
.instance_size = sizeof(quota_qrcode_view_t),
.base_class = &lv_obj_class
};
static void quota_qrcode_view_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
LV_UNUSED(class_p);
LV_TRACE_OBJ_CREATE("begin");
quota_qrcode_view_t *view = (quota_qrcode_view_t *)obj;
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(obj, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_bg_color(obj, lv_color_black(), 0);
lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, 0);
view->qr_img = lv_img_create(obj);
#ifdef CONFIG_BOARD_ARCS_MINI_DOLL_V2
lv_obj_set_size(view->qr_img, 160, 160);
lv_obj_align(view->qr_img, LV_ALIGN_CENTER, -80, 0);
#else
lv_obj_set_size(view->qr_img, 148, 148);
lv_obj_align(view->qr_img, LV_ALIGN_CENTER, 0, 5);
#endif
view->title_label = lv_label_create(obj);
lv_obj_set_style_text_color(view->title_label, lv_color_white(), 0);
lv_obj_set_style_text_font(view->title_label, &lv_font_chinese_16, 0);
lv_obj_set_style_text_align(view->title_label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_width(view->title_label, LV_PCT(90));
lv_label_set_long_mode(view->title_label, LV_LABEL_LONG_WRAP);
lv_label_set_text(view->title_label, "");
lv_obj_align_to(view->title_label, view->qr_img, LV_ALIGN_OUT_TOP_MID, 0, -25);
view->message_label = lv_label_create(obj);
lv_obj_set_style_text_color(view->message_label, lv_color_white(), 0);
lv_obj_set_style_text_font(view->message_label, &lv_font_chinese_16, 0);
lv_obj_set_style_text_align(view->message_label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_width(view->message_label, LV_PCT(100));
lv_label_set_long_mode(view->message_label, LV_LABEL_LONG_WRAP);
lv_label_set_text(view->message_label, "");
lv_obj_align_to(view->message_label, view->qr_img, LV_ALIGN_OUT_BOTTOM_MID, 0, 0);
LV_TRACE_OBJ_CREATE("finished");
}
static void quota_qrcode_view_destructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
LV_UNUSED(class_p);
LV_TRACE_OBJ_CREATE("begin");
LV_TRACE_OBJ_CREATE("finished");
}
lv_obj_t *lisa_ui_quota_qrcode_view_create(lv_obj_t *parent)
{
lv_obj_t *obj = lv_obj_class_create_obj(&lisa_ui_quota_qrcode_view_class, parent);
lv_obj_class_init_obj(obj);
return obj;
}
void lisa_ui_quota_qrcode_view_set_title(lv_obj_t *view, const char *title)
{
quota_qrcode_view_t *v = (quota_qrcode_view_t *)view;
if (v && v->title_label && title) {
lv_label_set_text(v->title_label, title);
}
}
void lisa_ui_quota_qrcode_view_set_message(lv_obj_t *view, const char *message)
{
quota_qrcode_view_t *v = (quota_qrcode_view_t *)view;
if (v && v->message_label && message) {
lv_label_set_text(v->message_label, message);
}
}
void lisa_ui_quota_qrcode_view_set_qr_image(lv_obj_t *view, const void *img_src)
{
quota_qrcode_view_t *v = (quota_qrcode_view_t *)view;
if (v && v->qr_img && img_src) {
lv_img_set_src(v->qr_img, img_src);
}
}

View File

@@ -0,0 +1,11 @@
#ifndef LISA_UI_QUOTA_QRCODE_VIEW_H
#define LISA_UI_QUOTA_QRCODE_VIEW_H
#include "lvgl.h"
lv_obj_t *lisa_ui_quota_qrcode_view_create(lv_obj_t *parent);
void lisa_ui_quota_qrcode_view_set_title(lv_obj_t *view, const char *title);
void lisa_ui_quota_qrcode_view_set_message(lv_obj_t *view, const char *message);
void lisa_ui_quota_qrcode_view_set_qr_image(lv_obj_t *view, const void *img_src);
#endif

View File

@@ -0,0 +1,14 @@
target_sources(
${LISA_UI_LIB_NAME}
PRIVATE
setting_common_view.c
setting_wifi_view.c
setting_wakeup_view.c
setting_clock_view.c
)
target_include_directories(
${LISA_UI_LIB_NAME}
PRIVATE
./
)

View File

@@ -0,0 +1,387 @@
/**
* @file setting_clock_view.c
* @brief Clock/Alarm settings view implementation
*/
#include "setting_clock_view.h"
#include "lisa_ui.h"
#include "lisa_ui_llm_base.h"
#include "lisa_ui_nav_scr.h"
#include "lisa_ui_nav_scr_ids.h"
#include "lisa_ui_assets.h"
#include "lisa_ui_fonts.h"
#include <time.h>
#include <stdio.h>
#define TAG "setting_clock_view"
typedef struct {
lisa_ui_llm_base_t base;
lv_obj_t *back_btn;
lv_obj_t *scroll_container;
lv_obj_t *empty_label;
lisa_ui_setting_clock_back_cb_t back_cb; /* 返回按钮回调函数 */
void *back_user_data; /* 回调函数用户数据 */
lisa_ui_setting_clock_delete_cb_t delete_cb; /* 删除闹钟回调函数 */
alarm_item_t alarm_items[20]; /* 使用统一的alarm结构体 */
int alarm_count;
} lisa_ui_setting_clock_view_t;
static void lisa_ui_setting_clock_view_class_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj);
const lv_obj_class_t lisa_ui_setting_clock_view_class = {
.base_class = &lisa_ui_llm_base_class,
.constructor_cb = lisa_ui_setting_clock_view_class_constructor,
.instance_size = sizeof(lisa_ui_setting_clock_view_t),
};
static void back_btn_event_cb(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
lisa_ui_setting_clock_view_t *view = (lisa_ui_setting_clock_view_t *)lv_event_get_user_data(e);
LISA_UI_LOGD("Clock back button clicked");
/* 调用回调函数将页面导航逻辑交给presenter层处理 */
if (view && view->back_cb) {
view->back_cb(view->back_user_data);
}
}
}
static void delete_btn_event_cb(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
lv_obj_t *btn = lv_event_get_target(e);
lv_obj_t *view_obj = lv_obj_get_parent(lv_obj_get_parent(btn));
while (view_obj && !lv_obj_check_type(view_obj, &lisa_ui_setting_clock_view_class)) {
view_obj = lv_obj_get_parent(view_obj);
}
if (!view_obj) {
return;
}
lisa_ui_setting_clock_view_t *view = (lisa_ui_setting_clock_view_t *)view_obj;
if (!view || !view->delete_cb) {
return;
}
uint64_t *timestamp = (uint64_t *)lv_event_get_user_data(e);
if (timestamp) {
view->delete_cb(*timestamp);
}
}
}
/* 手动将timestamp转换为日期时间不依赖localtime() */
static void timestamp_to_datetime(uint64_t timestamp, int *year, int *month, int *day, int *hour, int *minute,
int *second)
{
/* 时区偏移UTC+8 (28800秒) */
const int timezone_offset = 8 * 3600;
time_t t = (time_t)timestamp + timezone_offset;
/* 计算天数 */
int days = t / 86400;
int remaining = t % 86400;
/* 计算时分秒 */
*hour = remaining / 3600;
*minute = (remaining % 3600) / 60;
*second = remaining % 60;
/* 计算年月日 (从1970年1月1日开始) */
int y = 1970;
while (1) {
int days_in_year = (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) ? 366 : 365;
if (days < days_in_year) {
break;
}
days -= days_in_year;
y++;
}
*year = y;
/* 计算月份 */
int days_in_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (*year % 4 == 0 && (*year % 100 != 0 || *year % 400 == 0)) {
days_in_month[1] = 29;
}
int m = 0;
while (m < 12 && days >= days_in_month[m]) {
days -= days_in_month[m];
m++;
}
*month = m + 1;
*day = days + 1;
}
/* 使用蔡勒公式计算星期几 */
static int calculate_weekday_zeller(int year, int month, int day)
{
/* 蔡勒公式W = (d + (13*(m+1))/5 + y + y/4 - y/100 + y/400) % 7 */
/* 注意1月和2月要当作上一年的13月和14月来计算 */
if (month < 3) {
month += 12;
year -= 1;
}
int century = year / 100;
int year_of_century = year % 100;
int w = (day + (13 * (month + 1)) / 5 + year_of_century + year_of_century / 4 + century / 4 - 2 * century) % 7;
/* 蔡勒公式结果0=周六, 1=周日, 2=周一, 3=周二, 4=周三, 5=周四, 6=周五 */
/* 转换为0=周日, 1=周一, 2=周二, 3=周三, 4=周四, 5=周五, 6=周六 */
int weekday = (w + 6) % 7;
return weekday;
}
static void format_alarm_time_string(uint64_t timestamp, char *time_str, char *date_str)
{
int year, month, day, hour, minute, second;
LISA_UI_LOGI("Format alarm time - timestamp: %llu", (unsigned long long)timestamp);
/* 检查timestamp是否有效负数或过大的值都是无效的 */
if ((int64_t)timestamp < 0 || timestamp > 4102444800ULL) {
LISA_UI_LOGE(" Invalid timestamp: %lld (negative or too large)", (int64_t)timestamp);
snprintf(time_str, 16, "--:--");
snprintf(date_str, 32, _("alarm invalid"));
return;
}
/* 手动转换时间戳 */
timestamp_to_datetime(timestamp, &year, &month, &day, &hour, &minute, &second);
LISA_UI_LOGI(" Converted: year=%d, mon=%d, mday=%d, hour=%d, min=%d, sec=%d", year, month, day, hour, minute,
second);
/* 格式化时间 */
snprintf(time_str, 16, "%02d:%02d", hour, minute);
/* 使用蔡勒公式计算正确的星期几 */
int weekday = calculate_weekday_zeller(year, month, day);
LISA_UI_LOGI(" Zeller weekday: %d", weekday);
const char *weekdays[] = {"周日", "周一", "周二", "周三", "周四", "周五", "周六"};
snprintf(date_str, 32, "%d月%d日 %s", month, day, weekdays[weekday]);
LISA_UI_LOGI(" Formatted: time='%s', date='%s'", time_str, date_str);
}
static lv_obj_t *create_alarm_item(lv_obj_t *parent, const alarm_item_t *alarm_item, int y_offset)
{
lv_obj_t *alarm_container = lv_obj_create(parent);
lv_obj_set_size(alarm_container, LV_PCT(95), 80);
lv_obj_align(alarm_container, LV_ALIGN_TOP_MID, 0, y_offset);
lv_obj_set_style_bg_color(alarm_container, lv_color_make(0x4A, 0x4A, 0x4A), 0);
lv_obj_set_style_border_width(alarm_container, 0, 0);
lv_obj_set_style_radius(alarm_container, 8, 0);
lv_obj_set_style_pad_all(alarm_container, 10, 0);
char time_str[16];
char date_str[32];
format_alarm_time_string(alarm_item->timestamp, time_str, date_str);
lv_obj_t *time_label = lv_label_create(alarm_container);
lv_label_set_text(time_label, time_str);
lv_obj_set_style_text_color(time_label, lv_color_white(), 0);
lv_obj_set_style_text_font(time_label, &lv_font_chinese_16, 0);
lv_obj_align(time_label, LV_ALIGN_TOP_LEFT, 5, 8);
lv_obj_t *date_label = lv_label_create(alarm_container);
lv_label_set_text(date_label, date_str);
lv_obj_set_style_text_color(date_label, lv_color_make(0xAA, 0xAA, 0xAA), 0);
lv_obj_set_style_text_font(date_label, &lv_font_chinese_16, 0);
lv_obj_align(date_label, LV_ALIGN_BOTTOM_LEFT, 5, -8);
lv_obj_t *delete_btn = lv_btn_create(alarm_container);
lv_obj_set_size(delete_btn, 50, 30);
lv_obj_align(delete_btn, LV_ALIGN_RIGHT_MID, -5, 0);
lv_obj_set_style_bg_color(delete_btn, lv_color_make(0xFF, 0x44, 0x44), 0);
lv_obj_set_style_border_width(delete_btn, 0, 0);
lv_obj_set_style_radius(delete_btn, 4, 0);
lv_obj_t *delete_label = lv_label_create(delete_btn);
lv_label_set_text(delete_label, _("cancel"));
lv_obj_set_style_text_color(delete_label, lv_color_white(), 0);
lv_obj_set_style_text_font(delete_label, &lv_font_chinese_16, 0);
lv_obj_center(delete_label);
lv_obj_add_event_cb(delete_btn, delete_btn_event_cb, LV_EVENT_CLICKED, (void *)&alarm_item->timestamp);
return alarm_container;
}
static void lisa_ui_setting_clock_view_class_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
LV_UNUSED(class_p);
lisa_ui_setting_clock_view_t *view = (lisa_ui_setting_clock_view_t *)obj;
// Hide the bar to allow content to start from the very top
lv_obj_t *bar = lisa_ui_llm_base_bar_get(obj);
if (bar) {
lv_obj_add_flag(bar, LV_OBJ_FLAG_HIDDEN);
}
lv_obj_t *container = lisa_ui_llm_base_container_get(obj);
if (NULL == container) {
LISA_UI_LOGE("Failed to get base container");
return;
}
// Back button at the very top
view->back_btn = lv_btn_create(container);
lv_obj_set_size(view->back_btn, 30, 30);
lv_obj_align(view->back_btn, LV_ALIGN_TOP_LEFT, 5, 0);
lv_obj_set_style_bg_opa(view->back_btn, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_border_width(view->back_btn, 0, LV_PART_MAIN);
lv_obj_t *back_icon = lv_img_create(view->back_btn);
lv_img_set_src(back_icon, &icons_icon_back_png);
lv_obj_center(back_icon);
lv_obj_add_event_cb(view->back_btn, back_btn_event_cb, LV_EVENT_CLICKED, view);
/* 在返回按钮旁边创建标题文本 */
lv_obj_t *title = lv_label_create(container);
lv_label_set_text(title, _("alarm setting"));
lv_obj_set_style_text_font(title, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_set_style_text_color(title, lv_color_white(), LV_PART_MAIN);
lv_obj_align(title, LV_ALIGN_TOP_LEFT, 45, 8); /* 紧贴返回按钮右侧 */
/* 初始化列表容器为NULL等待刷新时创建 */
view->scroll_container = NULL;
view->empty_label = NULL;
view->delete_cb = NULL;
view->alarm_count = 0;
}
lv_obj_t *lisa_ui_setting_clock_view_create(lv_obj_t *parent)
{
lv_obj_t *obj = lv_obj_class_create_obj(&lisa_ui_setting_clock_view_class, parent);
lv_obj_class_init_obj(obj);
/* 初始化回调函数为NULL */
lisa_ui_setting_clock_view_t *view = (lisa_ui_setting_clock_view_t *)obj;
view->back_cb = NULL;
view->back_user_data = NULL;
view->delete_cb = NULL;
return obj;
}
void lisa_ui_setting_clock_view_set_alarms(lv_obj_t *obj, const alarm_item_t *alarms, uint32_t count)
{
if (!obj) {
return;
}
lisa_ui_setting_clock_view_t *view = (lisa_ui_setting_clock_view_t *)obj;
/* 限制最大数量 */
uint32_t copy_count = (count > 20) ? 20 : count;
/* 复制闹钟数据到view */
for (uint32_t i = 0; i < copy_count; i++) {
view->alarm_items[i].timestamp = alarms[i].timestamp;
LISA_UI_LOGI("set alarms, timestamp: %llu", view->alarm_items[i].timestamp);
}
view->alarm_count = copy_count;
LISA_UI_LOGD("Set %u alarms to view", copy_count);
}
void lisa_ui_setting_clock_view_refresh_list(lv_obj_t *obj)
{
if (!obj) {
return;
}
lisa_ui_setting_clock_view_t *view = (lisa_ui_setting_clock_view_t *)obj;
lv_obj_t *container = lisa_ui_llm_base_container_get(obj);
if (!container) {
return;
}
/* 清理旧的UI元素 */
if (view->scroll_container) {
lv_obj_del(view->scroll_container);
view->scroll_container = NULL;
}
if (view->empty_label) {
lv_obj_del(view->empty_label);
view->empty_label = NULL;
}
LISA_UI_LOGD("Refreshing alarm list, count=%d", view->alarm_count);
if (view->alarm_count == 0) {
LISA_UI_LOGD("No alarms to display, showing empty state");
view->empty_label = lv_label_create(container);
lv_label_set_text(view->empty_label, _("no alarm"));
lv_obj_set_style_text_color(view->empty_label, lv_color_make(0x88, 0x88, 0x88), 0);
lv_obj_set_style_text_font(view->empty_label, &lv_font_chinese_16, 0);
lv_obj_set_style_text_align(view->empty_label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(view->empty_label, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_width(view->empty_label, lv_pct(100));
lv_obj_set_height(view->empty_label, LV_SIZE_CONTENT);
lv_label_set_long_mode(view->empty_label, LV_LABEL_LONG_WRAP);
return;
}
/* 创建滚动容器 */
view->scroll_container = lv_obj_create(container);
lv_obj_set_size(view->scroll_container, LV_PCT(100), LV_PCT(75));
lv_obj_align(view->scroll_container, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_style_bg_color(view->scroll_container, lv_color_black(), 0);
lv_obj_set_style_border_width(view->scroll_container, 0, 0);
lv_obj_set_style_pad_all(view->scroll_container, 10, 0);
/* 创建UI元素 */
int y_offset = 10;
for (int i = 0; i < view->alarm_count; i++) {
/* 跳过无效的时间戳 */
if (view->alarm_items[i].timestamp == 0) {
LISA_UI_LOGD("Skipping alarm with timestamp=0 at index %d", i);
continue;
}
LISA_UI_LOGI("Creating alarm item %d: timestamp=%llu", i, (unsigned long long)view->alarm_items[i].timestamp);
lv_obj_t *item = create_alarm_item(view->scroll_container, &view->alarm_items[i], y_offset);
if (item) {
y_offset += 90;
}
}
}
void lisa_ui_setting_clock_view_set_back_cb(lv_obj_t *obj, lisa_ui_setting_clock_back_cb_t cb, void *user_data)
{
if (!obj) {
return;
}
/* 设置返回按钮回调函数和用户数据 */
lisa_ui_setting_clock_view_t *view = (lisa_ui_setting_clock_view_t *)obj;
view->back_cb = cb;
view->back_user_data = user_data;
}
void lisa_ui_setting_clock_view_set_delete_cb(lv_obj_t *obj, lisa_ui_setting_clock_delete_cb_t cb)
{
if (!obj) {
return;
}
lisa_ui_setting_clock_view_t *view = (lisa_ui_setting_clock_view_t *)obj;
view->delete_cb = cb;
}

View File

@@ -0,0 +1,40 @@
/**
* @file setting_clock_view.h
* @brief Clock/Alarm settings view
*/
#ifndef __SETTING_CLOCK_VIEW_H__
#define __SETTING_CLOCK_VIEW_H__
#ifdef __cplusplus
extern "C" {
#endif
#include "lvgl.h"
#include "lisa_ui_llm_base.h"
#include <stdint.h>
/**
* @brief Alarm item structure for display
*/
typedef struct {
uint64_t timestamp;
char text[128];
} alarm_item_t;
extern const lv_obj_class_t lisa_ui_setting_clock_view_class;
typedef void (*lisa_ui_setting_clock_back_cb_t)(void *user_data);
typedef void (*lisa_ui_setting_clock_delete_cb_t)(uint64_t timestamp);
lv_obj_t *lisa_ui_setting_clock_view_create(lv_obj_t *parent);
void lisa_ui_setting_clock_view_set_back_cb(lv_obj_t *obj, lisa_ui_setting_clock_back_cb_t cb, void *user_data);
void lisa_ui_setting_clock_view_set_delete_cb(lv_obj_t *obj, lisa_ui_setting_clock_delete_cb_t cb);
void lisa_ui_setting_clock_view_set_alarms(lv_obj_t *obj, const alarm_item_t *alarms, uint32_t count);
void lisa_ui_setting_clock_view_refresh_list(lv_obj_t *obj);
#ifdef __cplusplus
}
#endif
#endif /* __SETTING_CLOCK_VIEW_H__ */

View File

@@ -0,0 +1,245 @@
/**
* @file setting_common_view.c
* @brief Common settings view implementation (Volume, Brightness)
*/
#define TAG "setting_common_view"
#include "setting_common_view.h"
#include "lisa_ui.h"
#include "lisa_ui_llm_base.h"
#include "lisa_ui_nav_scr.h"
#include "lisa_ui_nav_scr_ids.h"
#include "lisa_ui_assets.h"
#include "stdio.h"
#include "lisa_ui_fonts.h"
typedef struct {
lisa_ui_llm_base_t base;
lv_obj_t *back_btn;
lv_obj_t *volume_label;
lv_obj_t *brightness_label;
lv_obj_t *volume_slider;
lv_obj_t *brightness_slider;
lisa_ui_setting_common_back_cb_t back_cb;
void *back_user_data;
lisa_ui_setting_common_volume_changed_cb_t volume_changed_cb;
void *volume_user_data;
lisa_ui_setting_common_brightness_changed_cb_t brightness_changed_cb;
void *brightness_user_data;
} lisa_ui_setting_common_view_t;
static void lisa_ui_setting_common_view_class_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj);
const lv_obj_class_t lisa_ui_setting_common_view_class = {
.base_class = &lisa_ui_llm_base_class,
.constructor_cb = lisa_ui_setting_common_view_class_constructor,
.instance_size = sizeof(lisa_ui_setting_common_view_t),
};
static void back_btn_event_cb(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
lisa_ui_setting_common_view_t *view = (lisa_ui_setting_common_view_t *)lv_event_get_user_data(e);
LISA_UI_LOGD("Back button clicked");
if (view && view->back_cb) {
view->back_cb(view->back_user_data);
}
}
}
static void volume_slider_event_cb(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_VALUE_CHANGED) {
lv_obj_t *slider = lv_event_get_target(e);
int32_t value = lv_slider_get_value(slider);
lisa_ui_setting_common_view_t *view = (lisa_ui_setting_common_view_t *)lv_event_get_user_data(e);
if (view && view->volume_label && lv_obj_is_valid(view->volume_label)) {
static char volume_text[32];
snprintf(volume_text, sizeof(volume_text), _("volume: %d%%"), (int)value);
lv_label_set_text(view->volume_label, volume_text);
}
LISA_UI_LOGD("Volume changed to: %d", (int)value);
/* 调用业务逻辑回调 */
if (view && view->volume_changed_cb) {
view->volume_changed_cb((uint8_t)value, view->volume_user_data);
}
}
}
static void brightness_slider_event_cb(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_VALUE_CHANGED) {
lv_obj_t *slider = lv_event_get_target(e);
int32_t value = lv_slider_get_value(slider);
lisa_ui_setting_common_view_t *view = (lisa_ui_setting_common_view_t *)lv_event_get_user_data(e);
if (view && view->brightness_label && lv_obj_is_valid(view->brightness_label)) {
static char brightness_text[32];
snprintf(brightness_text, sizeof(brightness_text), _("brightness: %d%%"), (int)value);
lv_label_set_text(view->brightness_label, brightness_text);
}
LISA_UI_LOGD("Brightness changed to: %d", (int)value);
/* 调用业务逻辑回调 */
if (view && view->brightness_changed_cb) {
view->brightness_changed_cb((uint8_t)value, view->brightness_user_data);
}
}
}
static void lisa_ui_setting_common_view_class_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
LV_UNUSED(class_p);
lisa_ui_setting_common_view_t *view = (lisa_ui_setting_common_view_t *)obj;
// Hide the bar to allow content to start from the very top
lv_obj_t *bar = lisa_ui_llm_base_bar_get(obj);
if (bar) {
lv_obj_add_flag(bar, LV_OBJ_FLAG_HIDDEN);
}
lv_obj_t *container = lisa_ui_llm_base_container_get(obj);
if (NULL == container) {
LISA_UI_LOGE("Failed to get base container");
return;
}
// Back button at the very top
view->back_btn = lv_btn_create(container);
lv_obj_set_size(view->back_btn, 30, 30);
lv_obj_align(view->back_btn, LV_ALIGN_TOP_LEFT, 5, 0);
lv_obj_set_style_bg_opa(view->back_btn, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_border_width(view->back_btn, 0, LV_PART_MAIN);
lv_obj_t *back_icon = lv_img_create(view->back_btn);
lv_img_set_src(back_icon, &icons_icon_back_png);
lv_obj_center(back_icon);
lv_obj_add_event_cb(view->back_btn, back_btn_event_cb, LV_EVENT_CLICKED, view);
/* 在返回按钮旁边创建标题文本 */
lv_obj_t *title = lv_label_create(container);
lv_label_set_text(title, _("common setting"));
lv_obj_set_style_text_font(title, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_set_style_text_color(title, lv_color_white(), LV_PART_MAIN);
lv_obj_align(title, LV_ALIGN_TOP_LEFT, 45, 8); /* 紧贴返回按钮右侧 */
view->volume_label = lv_label_create(container);
lv_obj_set_style_text_color(view->volume_label, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_text_font(view->volume_label, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_align(view->volume_label, LV_ALIGN_TOP_LEFT, 20, 80);
view->volume_slider = lv_slider_create(container);
lv_obj_set_width(view->volume_slider, 280);
lv_obj_align_to(view->volume_slider, view->volume_label, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 10);
lv_slider_set_range(view->volume_slider, 0, 100);
lv_slider_set_value(view->volume_slider, 50, LV_ANIM_OFF);
lv_obj_add_event_cb(view->volume_slider, volume_slider_event_cb, LV_EVENT_VALUE_CHANGED, view);
view->brightness_label = lv_label_create(container);
lv_obj_set_style_text_color(view->brightness_label, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_text_font(view->brightness_label, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_align_to(view->brightness_label, view->volume_slider, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 30);
view->brightness_slider = lv_slider_create(container);
lv_obj_set_width(view->brightness_slider, 280);
lv_obj_align_to(view->brightness_slider, view->brightness_label, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 10);
lv_slider_set_range(view->brightness_slider, 0, 100);
lv_slider_set_value(view->brightness_slider, 50, LV_ANIM_OFF);
lv_obj_add_event_cb(view->brightness_slider, brightness_slider_event_cb, LV_EVENT_VALUE_CHANGED, view);
}
lv_obj_t *lisa_ui_setting_common_view_create(lv_obj_t *parent)
{
lv_obj_t *obj = lv_obj_class_create_obj(&lisa_ui_setting_common_view_class, parent);
lv_obj_class_init_obj(obj);
lisa_ui_setting_common_view_t *view = (lisa_ui_setting_common_view_t *)obj;
view->back_cb = NULL;
view->back_user_data = NULL;
view->volume_changed_cb = NULL;
view->volume_user_data = NULL;
view->brightness_changed_cb = NULL;
view->brightness_user_data = NULL;
return obj;
}
void lisa_ui_setting_common_view_set_volume(lv_obj_t *obj, uint8_t volume)
{
if (!obj || volume > 100) {
return;
}
lisa_ui_setting_common_view_t *view = (lisa_ui_setting_common_view_t *)obj;
if (view->volume_slider && lv_obj_is_valid(view->volume_slider)) {
lv_slider_set_value(view->volume_slider, volume, LV_ANIM_OFF);
}
if (view->volume_label && lv_obj_is_valid(view->volume_label)) {
static char volume_text[32];
snprintf(volume_text, sizeof(volume_text), _("volume: %d%%"), volume);
lv_label_set_text(view->volume_label, volume_text);
}
}
void lisa_ui_setting_common_view_set_brightness(lv_obj_t *obj, uint8_t brightness)
{
if (!obj || brightness > 100) {
return;
}
lisa_ui_setting_common_view_t *view = (lisa_ui_setting_common_view_t *)obj;
if (view->brightness_slider && lv_obj_is_valid(view->brightness_slider)) {
lv_slider_set_value(view->brightness_slider, brightness, LV_ANIM_OFF);
}
if (view->brightness_label && lv_obj_is_valid(view->brightness_label)) {
static char brightness_text[32];
snprintf(brightness_text, sizeof(brightness_text), _("brightness: %d%%"), brightness);
lv_label_set_text(view->brightness_label, brightness_text);
}
}
void lisa_ui_setting_common_view_set_back_cb(lv_obj_t *obj, lisa_ui_setting_common_back_cb_t cb, void *user_data)
{
if (!obj) {
return;
}
lisa_ui_setting_common_view_t *view = (lisa_ui_setting_common_view_t *)obj;
view->back_cb = cb;
view->back_user_data = user_data;
}
void lisa_ui_setting_common_view_set_volume_changed_cb(lv_obj_t *obj, lisa_ui_setting_common_volume_changed_cb_t cb, void *user_data)
{
if (!obj) {
return;
}
lisa_ui_setting_common_view_t *view = (lisa_ui_setting_common_view_t *)obj;
view->volume_changed_cb = cb;
view->volume_user_data = user_data;
}
void lisa_ui_setting_common_view_set_brightness_changed_cb(lv_obj_t *obj, lisa_ui_setting_common_brightness_changed_cb_t cb, void *user_data)
{
if (!obj) {
return;
}
lisa_ui_setting_common_view_t *view = (lisa_ui_setting_common_view_t *)obj;
view->brightness_changed_cb = cb;
view->brightness_user_data = user_data;
}

View File

@@ -0,0 +1,88 @@
/**
* @file setting_common_view.h
* @brief Common settings view (Volume, Brightness)
*/
#ifndef __SETTING_COMMON_VIEW_H__
#define __SETTING_COMMON_VIEW_H__
#ifdef __cplusplus
extern "C" {
#endif
#include "lvgl.h"
#include "lisa_ui_llm_base.h"
/** Common settings view class definition */
extern const lv_obj_class_t lisa_ui_setting_common_view_class;
/**
* @brief Back button click callback
* @param user_data User data
*/
typedef void (*lisa_ui_setting_common_back_cb_t)(void *user_data);
/**
* @brief Volume changed callback
* @param volume New volume value (0-100)
* @param user_data User data
*/
typedef void (*lisa_ui_setting_common_volume_changed_cb_t)(uint8_t volume, void *user_data);
/**
* @brief Brightness changed callback
* @param brightness New brightness value (0-100)
* @param user_data User data
*/
typedef void (*lisa_ui_setting_common_brightness_changed_cb_t)(uint8_t brightness, void *user_data);
/**
* @brief Create common settings view (Volume/Brightness sliders)
* @param parent Parent object
* @return lv_obj_t* Created view object
*/
lv_obj_t *lisa_ui_setting_common_view_create(lv_obj_t *parent);
/**
* @brief Set volume value and update display
* @param obj View object
* @param volume Volume level (0-100)
*/
void lisa_ui_setting_common_view_set_volume(lv_obj_t *obj, uint8_t volume);
/**
* @brief Set brightness value and update display
* @param obj View object
* @param brightness Brightness level (0-100)
*/
void lisa_ui_setting_common_view_set_brightness(lv_obj_t *obj, uint8_t brightness);
/**
* @brief Set back button click callback
* @param obj View object
* @param cb Callback function
* @param user_data User data passed to callback
*/
void lisa_ui_setting_common_view_set_back_cb(lv_obj_t *obj, lisa_ui_setting_common_back_cb_t cb, void *user_data);
/**
* @brief Set volume changed callback
* @param obj View object
* @param cb Callback function
* @param user_data User data passed to callback
*/
void lisa_ui_setting_common_view_set_volume_changed_cb(lv_obj_t *obj, lisa_ui_setting_common_volume_changed_cb_t cb, void *user_data);
/**
* @brief Set brightness changed callback
* @param obj View object
* @param cb Callback function
* @param user_data User data passed to callback
*/
void lisa_ui_setting_common_view_set_brightness_changed_cb(lv_obj_t *obj, lisa_ui_setting_common_brightness_changed_cb_t cb, void *user_data);
#ifdef __cplusplus
}
#endif
#endif /* __SETTING_COMMON_VIEW_H__ */

View File

@@ -0,0 +1,281 @@
/**
* @file setting_wakeup_view.c
* @brief Wakeup settings view implementation
*/
#include "setting_wakeup_view.h"
#include "lisa_ui.h"
#include "lisa_ui_llm_base.h"
#include "lisa_ui_nav_scr.h"
#include "lisa_ui_nav_scr_ids.h"
#include "lisa_ui_assets.h"
#include "lisa_ui_fonts.h"
#define TAG "setting_wakeup_view"
#define WAKEUP_MODE_MAX 3
typedef struct {
lv_obj_t *radio_bg;
lv_obj_t *radio;
uint16_t radio_id;
lv_obj_t *tip_text;
void *view;
} radio_group_item_t;
typedef struct {
lisa_ui_llm_base_t base;
lv_obj_t *back_btn;
lv_obj_t *title_label;
lv_obj_t *radio_group;
radio_group_item_t radio_items[WAKEUP_MODE_MAX];
uint16_t current_mode;
uint16_t radio_group_index;
lisa_ui_setting_wakeup_back_cb_t back_cb; /* 返回按钮回调函数 */
void *back_user_data; /* 回调函数用户数据 */
lisa_ui_setting_wakeup_mode_changed_cb_t mode_changed_cb;
void *mode_changed_user_data;
} lisa_ui_setting_wakeup_view_t;
static void lisa_ui_setting_wakeup_view_class_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj);
const lv_obj_class_t lisa_ui_setting_wakeup_view_class = {
.base_class = &lisa_ui_llm_base_class,
.constructor_cb = lisa_ui_setting_wakeup_view_class_constructor,
.instance_size = sizeof(lisa_ui_setting_wakeup_view_t),
};
static lv_style_t style_radio;
static void back_btn_event_cb(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
lisa_ui_setting_wakeup_view_t *view = (lisa_ui_setting_wakeup_view_t *)lv_event_get_user_data(e);
LISA_UI_LOGD("Wakeup back button clicked");
/* 调用回调函数将页面导航逻辑交给presenter层处理 */
if (view && view->back_cb) {
view->back_cb(view->back_user_data);
}
}
}
static void update_radio_buttons(lisa_ui_setting_wakeup_view_t *view, uint16_t selected_mode)
{
if (!view || selected_mode >= WAKEUP_MODE_MAX) {
return;
}
for (int i = 0; i < view->radio_group_index && i < WAKEUP_MODE_MAX; i++) {
if (view->radio_items[i].radio && lv_obj_is_valid(view->radio_items[i].radio)) {
lv_obj_clear_state(view->radio_items[i].radio, LV_STATE_CHECKED);
}
}
if (selected_mode < view->radio_group_index &&
view->radio_items[selected_mode].radio &&
lv_obj_is_valid(view->radio_items[selected_mode].radio)) {
lv_obj_add_state(view->radio_items[selected_mode].radio, LV_STATE_CHECKED);
}
view->current_mode = selected_mode;
}
static void radio_group_bg_event_handler(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
radio_group_item_t *item = (radio_group_item_t *)lv_event_get_user_data(e);
if (!item || !item->view) {
return;
}
if (item->radio_id >= WAKEUP_MODE_MAX) {
return;
}
lisa_ui_setting_wakeup_view_t *view = (lisa_ui_setting_wakeup_view_t *)item->view;
update_radio_buttons(view, item->radio_id);
LISA_UI_LOGD("Wakeup mode changed to: %d", item->radio_id);
if (view->mode_changed_cb) {
view->mode_changed_cb(item->radio_id, view->mode_changed_user_data);
}
}
}
static lv_obj_t *add_radio_group_item(lv_obj_t *group, lisa_ui_setting_wakeup_view_t *view,
const char *txt, const char *tips_text, uint16_t radio_id)
{
if (!group || !txt || !tips_text || !view || radio_id >= WAKEUP_MODE_MAX) {
return NULL;
}
lv_obj_t *radio_bg = lv_obj_create(group);
if (!radio_bg) {
return NULL;
}
lv_obj_clear_flag(radio_bg, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(radio_bg, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_border_width(radio_bg, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_bg_color(radio_bg, lv_color_hex(0x24242f), LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_radius(radio_bg, 8, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_all(radio_bg, 15, LV_PART_MAIN | LV_STATE_DEFAULT);
view->radio_items[radio_id].radio_bg = radio_bg;
view->radio_items[radio_id].radio_id = radio_id;
view->radio_items[radio_id].view = view;
lv_obj_add_event_cb(radio_bg, radio_group_bg_event_handler, LV_EVENT_CLICKED,
(void *)&view->radio_items[radio_id]);
lv_obj_set_style_text_color(radio_bg, lv_color_hex(0xFFFFFF), LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_t *checkbox = lv_checkbox_create(radio_bg);
if (!checkbox) {
lv_obj_del(radio_bg);
return NULL;
}
view->radio_items[radio_id].radio = checkbox;
lv_obj_set_size(checkbox, LV_PCT(80), LV_SIZE_CONTENT);
lv_checkbox_set_text(checkbox, txt);
lv_obj_set_style_text_font(checkbox, &lv_font_chinese_16, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_add_flag(checkbox, LV_OBJ_FLAG_EVENT_BUBBLE);
lv_obj_add_style(checkbox, &style_radio, LV_PART_INDICATOR);
lv_obj_clear_flag(checkbox, LV_OBJ_FLAG_CLICKABLE);
lv_obj_t *tip_text = lv_label_create(radio_bg);
if (!tip_text) {
lv_obj_del(radio_bg);
return NULL;
}
view->radio_items[radio_id].tip_text = tip_text;
lv_obj_align_to(tip_text, checkbox, LV_ALIGN_OUT_BOTTOM_LEFT, 30, 5);
lv_obj_set_size(tip_text, LV_PCT(95), LV_SIZE_CONTENT);
lv_label_set_text(tip_text, tips_text);
lv_obj_set_style_text_font(tip_text, &lv_font_chinese_16, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_text_color(tip_text, lv_color_hex(0xBCBCBC), LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_clear_flag(tip_text, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_scrollbar_mode(tip_text, LV_SCROLLBAR_MODE_OFF);
lv_obj_set_scroll_dir(tip_text, LV_DIR_NONE);
if (view->radio_group_index < WAKEUP_MODE_MAX) {
view->radio_group_index++;
}
return checkbox;
}
static void lisa_ui_setting_wakeup_view_class_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
LV_UNUSED(class_p);
lisa_ui_setting_wakeup_view_t *view = (lisa_ui_setting_wakeup_view_t *)obj;
lv_obj_t *bar = lisa_ui_llm_base_bar_get(obj);
if (bar) {
lv_obj_add_flag(bar, LV_OBJ_FLAG_HIDDEN);
}
lv_obj_t *container = lisa_ui_llm_base_container_get(obj);
if (NULL == container) {
LISA_UI_LOGE("Failed to get base container");
return;
}
lv_obj_set_style_bg_color(container, lv_color_hex(0x000000), LV_PART_MAIN);
view->back_btn = lv_btn_create(container);
lv_obj_set_size(view->back_btn, 30, 30);
lv_obj_align(view->back_btn, LV_ALIGN_TOP_LEFT, 5, 0);
lv_obj_set_style_bg_opa(view->back_btn, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_border_width(view->back_btn, 0, LV_PART_MAIN);
lv_obj_t *back_icon = lv_img_create(view->back_btn);
lv_img_set_src(back_icon, &icons_icon_back_png);
lv_obj_center(back_icon);
lv_obj_add_event_cb(view->back_btn, back_btn_event_cb, LV_EVENT_CLICKED, view);
view->title_label = lv_label_create(container);
lv_label_set_text(view->title_label, _("wakeup setting"));
lv_obj_set_style_text_color(view->title_label, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_text_font(view->title_label, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_align(view->title_label, LV_ALIGN_TOP_LEFT, 45, 8);
lv_style_init(&style_radio);
lv_style_set_radius(&style_radio, LV_RADIUS_CIRCLE);
view->radio_group = lv_obj_create(container);
if (!view->radio_group) {
LISA_UI_LOGE("Failed to create radio_group");
return;
}
lv_obj_set_size(view->radio_group, LV_PCT(100), LV_PCT(80));
lv_obj_align(view->radio_group, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_flex_flow(view->radio_group, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(view->radio_group, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_style_border_width(view->radio_group, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_bg_color(view->radio_group, lv_color_hex(0x000000), LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_gap(view->radio_group, 10, LV_PART_MAIN | LV_STATE_DEFAULT);
view->radio_group_index = 0;
add_radio_group_item(view->radio_group, view, _("button wakeup"),
_("button wakeup description"), 0);
add_radio_group_item(view->radio_group, view, _("voice wakeup(single)"),
_("voice wakeup single description"), 1);
add_radio_group_item(view->radio_group, view, _("voice wakeup(multiple)"),
_("voice wakeup multiple description"), 2);
view->current_mode = 0;
update_radio_buttons(view, 0);
}
lv_obj_t *lisa_ui_setting_wakeup_view_create(lv_obj_t *parent)
{
lv_obj_t *obj = lv_obj_class_create_obj(&lisa_ui_setting_wakeup_view_class, parent);
lv_obj_class_init_obj(obj);
/* 初始化回调函数为NULL */
lisa_ui_setting_wakeup_view_t *view = (lisa_ui_setting_wakeup_view_t *)obj;
view->back_cb = NULL;
view->back_user_data = NULL;
view->mode_changed_cb = NULL;
view->mode_changed_user_data = NULL;
return obj;
}
void lisa_ui_setting_wakeup_view_set_back_cb(lv_obj_t *obj, lisa_ui_setting_wakeup_back_cb_t cb, void *user_data)
{
if (!obj) {
return;
}
/* 设置返回按钮回调函数和用户数据 */
lisa_ui_setting_wakeup_view_t *view = (lisa_ui_setting_wakeup_view_t *)obj;
view->back_cb = cb;
view->back_user_data = user_data;
}
void lisa_ui_setting_wakeup_view_set_mode_changed_cb(lv_obj_t *obj, lisa_ui_setting_wakeup_mode_changed_cb_t cb, void *user_data)
{
if (!obj) {
return;
}
lisa_ui_setting_wakeup_view_t *view = (lisa_ui_setting_wakeup_view_t *)obj;
view->mode_changed_cb = cb;
view->mode_changed_user_data = user_data;
}
void lisa_ui_setting_wakeup_view_set_mode(lv_obj_t *obj, uint16_t mode)
{
if (!obj) {
return;
}
lisa_ui_setting_wakeup_view_t *view = (lisa_ui_setting_wakeup_view_t *)obj;
update_radio_buttons(view, mode);
}

View File

@@ -0,0 +1,47 @@
/**
* @file setting_wakeup_view.h
* @brief Wakeup settings view
*/
#ifndef __SETTING_WAKEUP_VIEW_H__
#define __SETTING_WAKEUP_VIEW_H__
#ifdef __cplusplus
extern "C" {
#endif
#include "lvgl.h"
#include "lisa_ui_llm_base.h"
/** Wakeup settings view class definition */
extern const lv_obj_class_t lisa_ui_setting_wakeup_view_class;
/**
* @brief 返回按钮点击回调函数类型
* @param user_data 用户数据
*/
typedef void (*lisa_ui_setting_wakeup_back_cb_t)(void *user_data);
typedef void (*lisa_ui_setting_wakeup_mode_changed_cb_t)(uint16_t mode, void *user_data);
/**
* @brief Create wakeup settings view
* @param parent Parent object
* @return lv_obj_t* Created view object
*/
lv_obj_t *lisa_ui_setting_wakeup_view_create(lv_obj_t *parent);
/**
* @brief 设置返回按钮点击回调
* @param obj Wakeup view对象
* @param cb 回调函数
* @param user_data 传递给回调的用户数据
*/
void lisa_ui_setting_wakeup_view_set_back_cb(lv_obj_t *obj, lisa_ui_setting_wakeup_back_cb_t cb, void *user_data);
void lisa_ui_setting_wakeup_view_set_mode_changed_cb(lv_obj_t *obj, lisa_ui_setting_wakeup_mode_changed_cb_t cb, void *user_data);
void lisa_ui_setting_wakeup_view_set_mode(lv_obj_t *obj, uint16_t mode);
#ifdef __cplusplus
}
#endif
#endif /* __SETTING_WAKEUP_VIEW_H__ */

View File

@@ -0,0 +1,563 @@
/**
* @file setting_wifi_view.c
* @brief WiFi settings view implementation
*/
#include <stdio.h>
#include "setting_wifi_view.h"
#include "lisa_ui.h"
#include "lisa_ui_llm_base.h"
#include "lisa_ui_nav_scr.h"
#include "lisa_ui_nav_scr_ids.h"
#include "lisa_ui_assets.h"
#include "lisa_ui_fonts.h"
#include "../lisa_ui_keyboard_input.h"
#define TAG "setting_wifi_view"
#define WIFI_LIST_UPDATE_THROTTLE_MS 500 /* 列表更新限流时间(ms) */
#define WIFI_LIST_BATCH_CREATE_SIZE 5 /* 批量创建列表项数量 */
typedef struct {
lisa_ui_llm_base_t base;
lv_obj_t *back_btn;
lv_obj_t *wifi_switch;
lv_obj_t *wifi_list; /* WiFi 列表容器 */
lv_obj_t *info_label; /* 提示信息标签 */
lisa_ui_setting_wifi_back_cb_t back_cb; /* 返回按钮回调函数 */
void *back_user_data; /* 回调函数用户数据 */
lisa_ui_setting_wifi_item_cb_t item_cb; /* WiFi 项点击回调 */
void *item_user_data; /* WiFi 项回调用户数据 */
/* 密码输入界面相关 */
lv_obj_t *keyboard_input; /* 键盘输入组件 */
char current_ssid[32]; /* 当前选中的SSID */
lisa_ui_setting_wifi_password_confirm_cb_t password_confirm_cb; /* 密码确认回调 */
lisa_ui_setting_wifi_password_cancel_cb_t password_cancel_cb; /* 密码取消回调 */
void *password_user_data; /* 密码输入回调用户数据 */
/* 性能优化相关 */
lv_style_t item_style; /* WiFi项样式(复用) */
lv_style_t item_pressed_style; /* WiFi项按下样式(复用) */
uint32_t last_update_time; /* 上次更新时间戳 */
} lisa_ui_setting_wifi_view_t;
static void lisa_ui_setting_wifi_view_class_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj);
const lv_obj_class_t lisa_ui_setting_wifi_view_class = {
.base_class = &lisa_ui_llm_base_class,
.constructor_cb = lisa_ui_setting_wifi_view_class_constructor,
.instance_size = sizeof(lisa_ui_setting_wifi_view_t),
};
static void back_btn_event_cb(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
lisa_ui_setting_wifi_view_t *view = (lisa_ui_setting_wifi_view_t *)lv_event_get_user_data(e);
LISA_UI_LOGD("WiFi back button clicked");
/* 调用回调函数将页面导航逻辑交给presenter层处理 */
if (view && view->back_cb) {
view->back_cb(view->back_user_data);
}
}
}
static void lisa_ui_setting_wifi_view_class_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
LV_UNUSED(class_p);
lisa_ui_setting_wifi_view_t *view = (lisa_ui_setting_wifi_view_t *)obj;
// Hide the bar to allow content to start from the very top
lv_obj_t *bar = lisa_ui_llm_base_bar_get(obj);
if (bar) {
lv_obj_add_flag(bar, LV_OBJ_FLAG_HIDDEN);
}
lv_obj_t *container = lisa_ui_llm_base_container_get(obj);
if (NULL == container) {
LISA_UI_LOGE("Failed to get base container");
return;
}
/* 初始化可复用的样式 */
lv_style_init(&view->item_style);
lv_style_set_bg_color(&view->item_style, lv_color_hex(0x1a1a1a));
lv_style_set_border_width(&view->item_style, 0);
lv_style_set_radius(&view->item_style, 8);
lv_style_set_pad_all(&view->item_style, 12);
lv_style_set_shadow_width(&view->item_style, 4);
lv_style_set_shadow_color(&view->item_style, lv_color_black());
lv_style_set_shadow_opa(&view->item_style, LV_OPA_20);
lv_style_init(&view->item_pressed_style);
lv_style_set_bg_color(&view->item_pressed_style, lv_color_hex(0x2a2a2a));
view->last_update_time = 0;
/* ============ 标题栏区域 ============ */
// 创建标题栏容器
lv_obj_t *header = lv_obj_create(container);
lv_obj_set_size(header, LV_PCT(100), 45);
lv_obj_align(header, LV_ALIGN_TOP_MID, 0, 0);
lv_obj_set_style_bg_opa(header, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_border_width(header, 0, LV_PART_MAIN);
lv_obj_set_style_pad_all(header, 0, LV_PART_MAIN);
// 返回按钮
view->back_btn = lv_btn_create(header);
lv_obj_set_size(view->back_btn, 35, 35);
lv_obj_align(view->back_btn, LV_ALIGN_LEFT_MID, 5, 0);
lv_obj_set_style_bg_opa(view->back_btn, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_border_width(view->back_btn, 0, LV_PART_MAIN);
lv_obj_set_style_shadow_width(view->back_btn, 0, LV_PART_MAIN);
lv_obj_t *back_icon = lv_img_create(view->back_btn);
lv_img_set_src(back_icon, &icons_icon_back_png);
lv_obj_center(back_icon);
lv_obj_add_event_cb(view->back_btn, back_btn_event_cb, LV_EVENT_CLICKED, view);
// 标题文本
lv_obj_t *title = lv_label_create(header);
lv_label_set_text(title, _("wifi setting"));
lv_obj_set_style_text_font(title, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_set_style_text_color(title, lv_color_white(), LV_PART_MAIN);
lv_obj_align(title, LV_ALIGN_LEFT_MID, 50, 0);
// /* ============ WiFi 开关区域 ============ */
// // 创建 WiFi 开关容器
// lv_obj_t *switch_container = lv_obj_create(container);
// lv_obj_set_size(switch_container, LV_PCT(100), 50);
// lv_obj_align(switch_container, LV_ALIGN_TOP_MID, 0, 50);
// lv_obj_set_style_bg_color(switch_container, lv_color_hex(0x1a1a1a), LV_PART_MAIN);
// lv_obj_set_style_border_width(switch_container, 0, LV_PART_MAIN);
// lv_obj_set_style_radius(switch_container, 8, LV_PART_MAIN);
// lv_obj_set_style_pad_all(switch_container, 15, LV_PART_MAIN);
// lv_obj_clear_flag(switch_container, LV_OBJ_FLAG_SCROLLABLE);
// // WiFi 标签
// lv_obj_t *wifi_label = lv_label_create(switch_container);
// lv_label_set_text(wifi_label, _("AutoScan"));
// lv_obj_set_style_text_font(wifi_label, &lv_font_chinese_16, LV_PART_MAIN);
// lv_obj_set_style_text_color(wifi_label, lv_color_white(), LV_PART_MAIN);
// lv_obj_align(wifi_label, LV_ALIGN_LEFT_MID, 0, 0);
// // WiFi 开关
// view->wifi_switch = lv_switch_create(switch_container);
// lv_obj_set_size(view->wifi_switch, 50, 26);
// lv_obj_align(view->wifi_switch, LV_ALIGN_RIGHT_MID, 0, 0);
/* ============ 列表区域 ============ */
// WiFi 列表容器(可滚动)
view->wifi_list = lv_obj_create(container);
lv_obj_set_size(view->wifi_list, LV_PCT(100), 180);
lv_obj_align(view->wifi_list, LV_ALIGN_TOP_MID, 0, 50);
lv_obj_set_style_bg_opa(view->wifi_list, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_border_width(view->wifi_list, 0, LV_PART_MAIN);
lv_obj_set_style_pad_all(view->wifi_list, 0, LV_PART_MAIN);
lv_obj_set_style_pad_row(view->wifi_list, 8, LV_PART_MAIN);
lv_obj_set_flex_flow(view->wifi_list, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(view->wifi_list, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_scrollbar_mode(view->wifi_list, LV_SCROLLBAR_MODE_AUTO);
lv_obj_set_scroll_dir(view->wifi_list, LV_DIR_VER);
// 提示信息标签(默认隐藏)
view->info_label = lv_label_create(container);
lv_label_set_text(view->info_label, _("scaning"));
lv_obj_set_style_text_font(view->info_label, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_set_style_text_color(view->info_label, lv_color_hex(0x888888), LV_PART_MAIN);
lv_obj_align(view->info_label, LV_ALIGN_CENTER, 0, 20);
lv_obj_add_flag(view->info_label, LV_OBJ_FLAG_HIDDEN);
}
lv_obj_t *lisa_ui_setting_wifi_view_create(lv_obj_t *parent)
{
lv_obj_t *obj = lv_obj_class_create_obj(&lisa_ui_setting_wifi_view_class, parent);
lv_obj_class_init_obj(obj);
/* 初始化回调函数为NULL */
lisa_ui_setting_wifi_view_t *view = (lisa_ui_setting_wifi_view_t *)obj;
view->back_cb = NULL;
view->back_user_data = NULL;
view->item_cb = NULL;
view->item_user_data = NULL;
view->keyboard_input = NULL;
view->current_ssid[0] = '\0';
view->password_confirm_cb = NULL;
view->password_cancel_cb = NULL;
view->password_user_data = NULL;
return obj;
}
lv_obj_t *lisa_ui_setting_wifi_view_get_switch(lv_obj_t *obj)
{
if (!obj) {
return NULL;
}
lisa_ui_setting_wifi_view_t *view = (lisa_ui_setting_wifi_view_t *)obj;
return view->wifi_switch;
}
void lisa_ui_setting_wifi_view_set_back_cb(lv_obj_t *obj, lisa_ui_setting_wifi_back_cb_t cb, void *user_data)
{
if (!obj) {
return;
}
/* 设置返回按钮回调函数和用户数据 */
lisa_ui_setting_wifi_view_t *view = (lisa_ui_setting_wifi_view_t *)obj;
view->back_cb = cb;
view->back_user_data = user_data;
}
void lisa_ui_setting_wifi_view_set_item_cb(lv_obj_t *obj, lisa_ui_setting_wifi_item_cb_t cb, void *user_data)
{
if (!obj) {
return;
}
lisa_ui_setting_wifi_view_t *view = (lisa_ui_setting_wifi_view_t *)obj;
view->item_cb = cb;
view->item_user_data = user_data;
}
static void wifi_item_event_cb(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
lv_obj_t *item = lv_event_get_target(e);
lisa_ui_setting_wifi_view_t *view = (lisa_ui_setting_wifi_view_t *)lv_event_get_user_data(e);
if (view && view->item_cb) {
/* 从 item 的 user_data 获取 SSID */
const char *ssid = (const char *)lv_obj_get_user_data(item);
if (ssid) {
view->item_cb(ssid, view->item_user_data);
}
}
}
}
/* 获取信号强度图标 */
static const char *get_signal_icon(int rssi)
{
if (rssi >= -50) {
return LV_SYMBOL_WIFI; /* 强信号 */
} else if (rssi >= -70) {
return LV_SYMBOL_WIFI; /* 中等信号 */
} else {
return LV_SYMBOL_WIFI; /* 弱信号 */
}
}
/* 创建单个WiFi列表项(优化版) */
static lv_obj_t *create_wifi_item(lisa_ui_setting_wifi_view_t *view, const lisa_ui_wifi_ap_info_t *ap_info)
{
/* 创建列表项容器 */
lv_obj_t *item = lv_obj_create(view->wifi_list);
lv_obj_set_size(item, LV_PCT(100), 55);
/* 使用预定义样式,避免重复设置 */
lv_obj_add_style(item, &view->item_style, LV_PART_MAIN);
lv_obj_add_style(item, &view->item_pressed_style, LV_PART_MAIN | LV_STATE_PRESSED);
lv_obj_add_flag(item, LV_OBJ_FLAG_CLICKABLE);
/* 保存 SSID 到 item 的 user_data (需要动态分配内存) */
char *ssid_copy = lv_mem_alloc(strlen(ap_info->ssid) + 1);
if (ssid_copy) {
strcpy(ssid_copy, ap_info->ssid);
lv_obj_set_user_data(item, ssid_copy);
}
/* 连接状态打勾标记 */
if (ap_info->is_connected) {
lv_obj_t *check_icon = lv_label_create(item);
lv_label_set_text(check_icon, LV_SYMBOL_OK);
lv_obj_set_style_text_color(check_icon, lv_color_hex(0x4CAF50), LV_PART_MAIN);
lv_obj_set_style_text_font(check_icon, &lv_font_montserrat_14, LV_PART_MAIN);
lv_obj_align(check_icon, LV_ALIGN_LEFT_MID, 0, 0);
}
/* SSID 标签 - 统一左边距,保持对齐 */
lv_obj_t *ssid_label = lv_label_create(item);
lv_label_set_text(ssid_label, ap_info->ssid);
lv_obj_set_style_text_font(ssid_label, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_set_style_text_color(ssid_label, lv_color_white(), LV_PART_MAIN);
lv_label_set_long_mode(ssid_label, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_set_width(ssid_label, 125);
lv_obj_align(ssid_label, LV_ALIGN_LEFT_MID, 25, 0);
/* 信号强度显示 */
lv_obj_t *signal_label = lv_label_create(item);
char signal_buf[16];
/* 根据信号强度设置颜色 */
lv_color_t signal_color;
if (ap_info->rssi >= -50) {
signal_color = lv_color_hex(0x4CAF50); /* 强信号 - 绿色 */
} else if (ap_info->rssi >= -70) {
signal_color = lv_color_hex(0xFFC107); /* 中等信号 - 黄色 */
} else {
signal_color = lv_color_hex(0xFF5722); /* 弱信号 - 橙红色 */
}
snprintf(signal_buf, sizeof(signal_buf), "%ddBm", ap_info->rssi);
lv_label_set_text(signal_label, signal_buf);
lv_obj_set_style_text_font(signal_label, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_set_style_text_color(signal_label, signal_color, LV_PART_MAIN);
lv_obj_align(signal_label, LV_ALIGN_RIGHT_MID, 0, 0);
/* 注册点击事件 */
lv_obj_add_event_cb(item, wifi_item_event_cb, LV_EVENT_CLICKED, view);
return item;
}
/* 批量创建任务的定时器回调 */
typedef struct {
lisa_ui_setting_wifi_view_t *view;
const lisa_ui_wifi_ap_info_t *ap_list;
int total_count;
int current_index;
} wifi_list_create_data_t;
static void batch_create_timer_cb(lv_timer_t *timer)
{
wifi_list_create_data_t *data = (wifi_list_create_data_t *)timer->user_data;
if (!data || !data->view || !data->ap_list) {
lv_timer_del(timer);
if (data) {
if (data->ap_list) lv_mem_free((void *)data->ap_list);
lv_mem_free(data);
}
return;
}
/* 批量创建一部分列表项 */
int end_index = data->current_index + WIFI_LIST_BATCH_CREATE_SIZE;
if (end_index > data->total_count) {
end_index = data->total_count;
}
for (int i = data->current_index; i < end_index; i++) {
create_wifi_item(data->view, &data->ap_list[i]);
}
data->current_index = end_index;
/* 如果所有项都已创建,删除定时器并释放内存 */
if (data->current_index >= data->total_count) {
LISA_UI_LOGD("Batch creation completed: %d items", data->total_count);
lv_mem_free((void *)data->ap_list);
lv_mem_free(data);
lv_timer_del(timer);
}
}
void lisa_ui_setting_wifi_view_update_list(lv_obj_t *obj, const lisa_ui_wifi_ap_info_t *ap_list, int ap_count)
{
if (!obj || !ap_list || ap_count <= 0) {
return;
}
lisa_ui_setting_wifi_view_t *view = (lisa_ui_setting_wifi_view_t *)obj;
/* 限流检查:避免过于频繁的更新 */
uint32_t current_time = lv_tick_get();
if ((current_time - view->last_update_time) < WIFI_LIST_UPDATE_THROTTLE_MS) {
LISA_UI_LOGD("Update throttled, skipping");
return;
}
view->last_update_time = current_time;
/* 释放之前动态分配的内存 */
uint32_t child_count = lv_obj_get_child_cnt(view->wifi_list);
for (uint32_t i = 0; i < child_count; i++) {
lv_obj_t *child = lv_obj_get_child(view->wifi_list, i);
if (child) {
char *ssid_copy = (char *)lv_obj_get_user_data(child);
if (ssid_copy) {
lv_mem_free(ssid_copy);
}
}
}
/* 清空现有列表 */
lv_obj_clean(view->wifi_list);
/* 隐藏提示信息 */
lv_obj_add_flag(view->info_label, LV_OBJ_FLAG_HIDDEN);
/* 性能优化:对于大量WiFi列表,使用批量创建 */
if (ap_count > WIFI_LIST_BATCH_CREATE_SIZE * 2) {
/* 先创建一批可见项,其余延迟创建 */
int initial_count = WIFI_LIST_BATCH_CREATE_SIZE;
for (int i = 0; i < initial_count && i < ap_count; i++) {
create_wifi_item(view, &ap_list[i]);
}
/* 如果还有更多项,使用定时器批量创建 */
if (ap_count > initial_count) {
/* 复制AP列表数据 */
lisa_ui_wifi_ap_info_t *ap_copy = lv_mem_alloc(sizeof(lisa_ui_wifi_ap_info_t) * ap_count);
if (ap_copy) {
memcpy(ap_copy, ap_list, sizeof(lisa_ui_wifi_ap_info_t) * ap_count);
wifi_list_create_data_t *data = lv_mem_alloc(sizeof(wifi_list_create_data_t));
if (data) {
data->view = view;
data->ap_list = ap_copy;
data->total_count = ap_count;
data->current_index = initial_count;
/* 创建定时器,每50ms创建一批 */
lv_timer_t *timer = lv_timer_create(batch_create_timer_cb, 50, data);
if (!timer) {
lv_mem_free(data);
lv_mem_free(ap_copy);
LISA_UI_LOGE("Failed to create batch timer");
}
} else {
lv_mem_free(ap_copy);
}
}
}
LISA_UI_LOGD("Started batch creation for %d WiFi items", ap_count);
} else {
/* 对于少量WiFi,直接一次性创建 */
for (int i = 0; i < ap_count; i++) {
create_wifi_item(view, &ap_list[i]);
}
LISA_UI_LOGD("Updated WiFi list with %d items", ap_count);
}
}
void lisa_ui_setting_wifi_view_clear_list(lv_obj_t *obj)
{
if (!obj) {
return;
}
lisa_ui_setting_wifi_view_t *view = (lisa_ui_setting_wifi_view_t *)obj;
/* 释放之前动态分配的内存 */
uint32_t child_count = lv_obj_get_child_cnt(view->wifi_list);
for (uint32_t i = 0; i < child_count; i++) {
lv_obj_t *child = lv_obj_get_child(view->wifi_list, i);
if (child) {
char *ssid_copy = (char *)lv_obj_get_user_data(child);
if (ssid_copy) {
lv_mem_free(ssid_copy);
}
}
}
/* 清空列表 */
lv_obj_clean(view->wifi_list);
/* 显示提示信息 */
lv_label_set_text(view->info_label, _("no network"));
lv_obj_clear_flag(view->info_label, LV_OBJ_FLAG_HIDDEN);
LISA_UI_LOGD("Cleared WiFi list");
}
/* 密码输入确认回调的包装函数 */
static void password_confirm_wrapper(const char *text, void *user_data)
{
lisa_ui_setting_wifi_view_t *view = (lisa_ui_setting_wifi_view_t *)user_data;
if (view) {
/* 恢复当前页面的显示 */
lv_obj_clear_flag((lv_obj_t *)view, LV_OBJ_FLAG_HIDDEN);
if (view->password_confirm_cb) {
view->password_confirm_cb(view->current_ssid, text, view->password_user_data);
}
}
}
/* 密码输入取消回调的包装函数 */
static void password_cancel_wrapper(void *user_data)
{
lisa_ui_setting_wifi_view_t *view = (lisa_ui_setting_wifi_view_t *)user_data;
if (view) {
/* 恢复当前页面的显示 */
lv_obj_clear_flag((lv_obj_t *)view, LV_OBJ_FLAG_HIDDEN);
if (view->password_cancel_cb) {
view->password_cancel_cb(view->password_user_data);
}
}
}
void lisa_ui_setting_wifi_view_show_password_input(lv_obj_t *obj, const char *ssid,
lisa_ui_setting_wifi_password_confirm_cb_t confirm_cb,
lisa_ui_setting_wifi_password_cancel_cb_t cancel_cb,
void *user_data)
{
if (!obj || !ssid) {
return;
}
lisa_ui_setting_wifi_view_t *view = (lisa_ui_setting_wifi_view_t *)obj;
/* 保存回调函数和SSID */
view->password_confirm_cb = confirm_cb;
view->password_cancel_cb = cancel_cb;
view->password_user_data = user_data;
strncpy(view->current_ssid, ssid, sizeof(view->current_ssid) - 1);
view->current_ssid[sizeof(view->current_ssid) - 1] = '\0';
/* 如果键盘输入组件不存在,创建它 */
if (!view->keyboard_input) {
view->keyboard_input = lisa_ui_keyboard_input_create(lv_scr_act());
if (!view->keyboard_input) {
LISA_UI_LOGE("Failed to create keyboard input");
return;
}
lisa_ui_keyboard_input_set_placeholder(view->keyboard_input, "Please input password");
lisa_ui_keyboard_input_set_password_mode(view->keyboard_input, false);
/* 设置回调 - 使用包装函数 */
lisa_ui_keyboard_input_set_confirm_cb(view->keyboard_input, password_confirm_wrapper, view);
lisa_ui_keyboard_input_set_cancel_cb(view->keyboard_input, password_cancel_wrapper, view);
}
/* 设置标题 */
char title_text[64];
snprintf(title_text, sizeof(title_text), "%s", ssid);
lisa_ui_keyboard_input_set_title(view->keyboard_input, title_text);
/* 隐藏当前页面 */
lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN);
/* 显示键盘输入界面 */
lisa_ui_keyboard_input_show(view->keyboard_input);
LISA_UI_LOGD("Showing password input for SSID: %s", ssid);
}
void lisa_ui_setting_wifi_view_hide_password_input(lv_obj_t *obj)
{
if (!obj) {
return;
}
lisa_ui_setting_wifi_view_t *view = (lisa_ui_setting_wifi_view_t *)obj;
if (view->keyboard_input) {
lisa_ui_keyboard_input_hide(view->keyboard_input);
LISA_UI_LOGD("Password input hidden");
}
/* 恢复当前页面的显示 */
lv_obj_clear_flag(obj, LV_OBJ_FLAG_HIDDEN);
}

View File

@@ -0,0 +1,123 @@
/**
* @file setting_wifi_view.h
* @brief WiFi settings view
*/
#ifndef __SETTING_WIFI_VIEW_H__
#define __SETTING_WIFI_VIEW_H__
#ifdef __cplusplus
extern "C" {
#endif
#include "lvgl.h"
#include "lisa_ui_llm_base.h"
/** WiFi settings view class definition */
extern const lv_obj_class_t lisa_ui_setting_wifi_view_class;
/**
* @brief 返回按钮点击回调函数类型
* @param user_data 用户数据
*/
typedef void (*lisa_ui_setting_wifi_back_cb_t)(void *user_data);
/**
* @brief Create WiFi settings view
* @param parent Parent object
* @return lv_obj_t* Created view object
*/
lv_obj_t *lisa_ui_setting_wifi_view_create(lv_obj_t *parent);
/**
* @brief Get WiFi switch object
* @param obj WiFi view object
* @return lv_obj_t* WiFi switch object
*/
lv_obj_t *lisa_ui_setting_wifi_view_get_switch(lv_obj_t *obj);
/**
* @brief 设置返回按钮点击回调
* @param obj WiFi view对象
* @param cb 回调函数
* @param user_data 传递给回调的用户数据
*/
void lisa_ui_setting_wifi_view_set_back_cb(lv_obj_t *obj, lisa_ui_setting_wifi_back_cb_t cb, void *user_data);
/**
* @brief WiFi AP 信息结构
*/
typedef struct {
char ssid[32];
int rssi;
int channel;
bool is_connected; /* 是否已连接 */
} lisa_ui_wifi_ap_info_t;
/**
* @brief WiFi 项点击回调函数类型
* @param ssid WiFi SSID
* @param user_data 用户数据
*/
typedef void (*lisa_ui_setting_wifi_item_cb_t)(const char *ssid, void *user_data);
/**
* @brief WiFi 密码确认回调函数类型
* @param ssid WiFi SSID
* @param password WiFi 密码
* @param user_data 用户数据
*/
typedef void (*lisa_ui_setting_wifi_password_confirm_cb_t)(const char *ssid, const char *password, void *user_data);
/**
* @brief WiFi 密码取消回调函数类型
* @param user_data 用户数据
*/
typedef void (*lisa_ui_setting_wifi_password_cancel_cb_t)(void *user_data);
/**
* @brief 更新 WiFi 扫描列表
* @param obj WiFi view对象
* @param ap_list AP 信息数组
* @param ap_count AP 数量
*/
void lisa_ui_setting_wifi_view_update_list(lv_obj_t *obj, const lisa_ui_wifi_ap_info_t *ap_list, int ap_count);
/**
* @brief 设置 WiFi 项点击回调
* @param obj WiFi view对象
* @param cb 回调函数
* @param user_data 传递给回调的用户数据
*/
void lisa_ui_setting_wifi_view_set_item_cb(lv_obj_t *obj, lisa_ui_setting_wifi_item_cb_t cb, void *user_data);
/**
* @brief 清空 WiFi 列表
* @param obj WiFi view对象
*/
void lisa_ui_setting_wifi_view_clear_list(lv_obj_t *obj);
/**
* @brief 显示密码输入界面
* @param obj WiFi view对象
* @param ssid WiFi SSID
* @param confirm_cb 确认回调
* @param cancel_cb 取消回调
* @param user_data 用户数据
*/
void lisa_ui_setting_wifi_view_show_password_input(lv_obj_t *obj, const char *ssid,
lisa_ui_setting_wifi_password_confirm_cb_t confirm_cb,
lisa_ui_setting_wifi_password_cancel_cb_t cancel_cb,
void *user_data);
/**
* @brief 隐藏密码输入界面
* @param obj WiFi view对象
*/
void lisa_ui_setting_wifi_view_hide_password_input(lv_obj_t *obj);
#ifdef __cplusplus
}
#endif
#endif /* __SETTING_WIFI_VIEW_H__ */

View File

@@ -0,0 +1,222 @@
/**
* @file setting_view.c
* @brief Setting main page view implementation
*
* Pure UI layer for settings menu with 2x2 grid card layout.
*/
#define TAG "setting_view"
#include "setting_view.h"
#include "lisa_ui.h"
#include "lisa_ui_llm_base.h"
#include "lisa_ui_nav_scr.h"
#include "lisa_ui_nav_scr_ids.h"
#include "lisa_ui_assets.h"
#include "lisa_ui_fonts.h"
#include <string.h>
/**
* @brief Setting view structure
*/
struct lisa_ui_setting_view {
lisa_ui_llm_base_t base_obj; /**< Base object */
lv_obj_t *back_btn; /**< Back button (X icon) */
lv_obj_t *list; /**< Setting items container */
lisa_ui_setting_item_click_cb_t click_cb;
void *click_user_data;
lisa_ui_setting_back_cb_t back_cb;
void *back_user_data;
};
typedef struct lisa_ui_setting_view lisa_ui_setting_view_t;
static void lisa_ui_setting_view_class_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj);
static void lisa_ui_setting_view_class_destructor(const lv_obj_class_t *class_p, lv_obj_t *obj);
const lv_obj_class_t lisa_ui_setting_view_class = {
.base_class = &lisa_ui_llm_base_class,
.instance_size = sizeof(lisa_ui_setting_view_t),
.constructor_cb = lisa_ui_setting_view_class_constructor,
.destructor_cb = lisa_ui_setting_view_class_destructor,
};
static void list_btn_event_handler(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
lisa_ui_setting_view_t *setting = (lisa_ui_setting_view_t *)lv_event_get_user_data(e);
if (setting->click_cb) {
lv_obj_t *card = lv_event_get_target(e);
// Get text label (second child of card)
uint32_t child_cnt = lv_obj_get_child_cnt(card);
if (child_cnt >= 2) {
lv_obj_t *text_label = lv_obj_get_child(card, 1);
const char *name = lv_label_get_text(text_label);
setting->click_cb(name, setting->click_user_data);
}
}
}
}
static void back_btn_event_cb(lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
lisa_ui_setting_view_t *setting = (lisa_ui_setting_view_t *)lv_event_get_user_data(e);
LISA_UI_LOGD("Setting back button clicked");
if (setting && setting->back_cb) {
setting->back_cb(setting->back_user_data);
}
}
}
static void lisa_ui_setting_view_class_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
LV_UNUSED(class_p);
lisa_ui_setting_view_t *setting = (lisa_ui_setting_view_t *)obj;
// Hide the bar to allow content to start from the very top
lv_obj_t *bar = lisa_ui_llm_base_bar_get(obj);
if (bar) {
lv_obj_add_flag(bar, LV_OBJ_FLAG_HIDDEN);
}
lv_obj_t *container = lisa_ui_llm_base_container_get(obj);
if (NULL == container) {
LISA_UI_LOGE("Failed to get base container");
return;
}
// Create back button (X icon) in top-left corner at the very top
setting->back_btn = lv_btn_create(container);
lv_obj_set_size(setting->back_btn, 30, 30);
lv_obj_align(setting->back_btn, LV_ALIGN_TOP_LEFT, 5, 0);
lv_obj_set_style_bg_opa(setting->back_btn, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_border_width(setting->back_btn, 0, LV_PART_MAIN);
lv_obj_set_style_shadow_width(setting->back_btn, 0, LV_PART_MAIN);
lv_obj_add_flag(setting->back_btn, LV_OBJ_FLAG_CLICKABLE); // Ensure clickable
lv_obj_t *back_icon = lv_img_create(setting->back_btn);
lv_img_set_src(back_icon, &icons_icon_closs_png);
lv_obj_center(back_icon);
lv_obj_add_event_cb(setting->back_btn, back_btn_event_cb, LV_EVENT_CLICKED, setting);
LISA_UI_LOGD("Setting back button created and event added");
// Create transparent container for cards (no grid, use absolute positioning like original)
setting->list = lv_obj_create(container);
lv_obj_set_size(setting->list, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_bg_opa(setting->list, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_border_width(setting->list, 0, LV_PART_MAIN);
lv_obj_set_style_pad_all(setting->list, 0, LV_PART_MAIN);
lv_obj_clear_flag(setting->list, LV_OBJ_FLAG_CLICKABLE); // List not clickable, only cards
// Move back button to front so it's not covered by cards
lv_obj_move_foreground(setting->back_btn);
}
static void lisa_ui_setting_view_class_destructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
(void)class_p;
(void)obj;
// Child objects auto-deleted by LVGL
}
// ===================== Public API Implementation =====================
lv_obj_t *lisa_ui_setting_view_create(lv_obj_t *parent)
{
lv_obj_t *obj = lv_obj_class_create_obj(&lisa_ui_setting_view_class, parent);
lv_obj_class_init_obj(obj);
lisa_ui_setting_view_t *setting = (lisa_ui_setting_view_t *)obj;
setting->click_cb = NULL;
setting->click_user_data = NULL;
setting->back_cb = NULL;
setting->back_user_data = NULL;
// Set page title
lisa_ui_llm_base_set_title(obj, "Settings");
return obj;
}
void lisa_ui_setting_view_item_add(lv_obj_t *obj, const char *label, const void *icon_src, int col, int row)
{
if (!obj || !lv_obj_has_class(obj, &lisa_ui_setting_view_class)) {
return;
}
lisa_ui_setting_view_t *setting = (lisa_ui_setting_view_t *)obj;
// Calculate position like original UI (2x2 grid with absolute positioning)
lv_coord_t card_width = 140;
lv_coord_t card_height = 55;
lv_coord_t margin = 10;
lv_coord_t start_x = (320 - (2 * card_width + margin)) / 2; // Center horizontally
lv_coord_t start_y = 35; // Start below back button (30px height + 5px margin)
lv_coord_t x = start_x + col * (card_width + margin);
lv_coord_t y = start_y + row * (card_height + margin);
// Create card (exactly like original UI)
lv_obj_t *card = lv_obj_create(setting->list);
lv_obj_set_size(card, card_width, card_height);
lv_obj_set_pos(card, x, y);
lv_obj_set_style_bg_color(card, lv_color_hex(0x404040), LV_PART_MAIN);
lv_obj_set_style_border_width(card, 0, LV_PART_MAIN);
lv_obj_set_style_radius(card, 15, LV_PART_MAIN);
lv_obj_add_flag(card, LV_OBJ_FLAG_CLICKABLE);
// Icon on left (PNG image from assets, like original UI)
lv_obj_t *icon = lv_img_create(card);
lv_img_set_src(icon, icon_src);
lv_obj_align(icon, LV_ALIGN_LEFT_MID, 10, 0);
// Text on right (like original UI)
lv_obj_t *text = lv_label_create(card);
lv_label_set_text(text, label);
lv_obj_set_style_text_color(text, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_text_font(text, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_align(text, LV_ALIGN_RIGHT_MID, -10, 0);
// Add click event
lv_obj_add_event_cb(card, list_btn_event_handler, LV_EVENT_CLICKED, setting);
}
void lisa_ui_setting_view_set_click_cb(lv_obj_t *obj, lisa_ui_setting_item_click_cb_t cb, void *user_data)
{
if (!obj || !lv_obj_has_class(obj, &lisa_ui_setting_view_class)) {
return;
}
lisa_ui_setting_view_t *setting = (lisa_ui_setting_view_t *)obj;
setting->click_cb = cb;
setting->click_user_data = user_data;
}
void lisa_ui_setting_view_clear(lv_obj_t *obj)
{
if (!obj || !lv_obj_has_class(obj, &lisa_ui_setting_view_class)) {
return;
}
lisa_ui_setting_view_t *setting = (lisa_ui_setting_view_t *)obj;
lv_obj_clean(setting->list);
}
void lisa_ui_setting_view_set_back_cb(lv_obj_t *obj, lisa_ui_setting_back_cb_t cb, void *user_data)
{
if (!obj || !lv_obj_has_class(obj, &lisa_ui_setting_view_class)) {
return;
}
lisa_ui_setting_view_t *setting = (lisa_ui_setting_view_t *)obj;
setting->back_cb = cb;
setting->back_user_data = user_data;
}

View File

@@ -0,0 +1,75 @@
/**
* @file setting_view.h
* @brief Setting main page view header
*/
#ifndef __LISA_UI_SETTING_VIEW_H__
#define __LISA_UI_SETTING_VIEW_H__
#ifdef __cplusplus
extern "C" {
#endif
#include "lvgl.h"
#include "lisa_ui_llm_base.h"
/** Setting view class definition */
extern const lv_obj_class_t lisa_ui_setting_view_class;
/**
* @brief Setting item click callback
* @param name Item name that was clicked
* @param user_data User data
*/
typedef void (*lisa_ui_setting_item_click_cb_t)(const char *name, void *user_data);
/**
* @brief Back button click callback
* @param user_data User data
*/
typedef void (*lisa_ui_setting_back_cb_t)(void *user_data);
/**
* @brief Create setting view object
* @param parent Parent object, NULL for current screen
* @return lv_obj_t* Created setting view object, NULL on failure
*/
lv_obj_t *lisa_ui_setting_view_create(lv_obj_t *parent);
/**
* @brief Add setting item card to 2x2 grid
* @param obj Setting view object
* @param label Item label text
* @param icon_src LVGL image descriptor for icon
* @param col Grid column (0 or 1)
* @param row Grid row (0 or 1)
*/
void lisa_ui_setting_view_item_add(lv_obj_t *obj, const char *label, const void *icon_src, int col, int row);
/**
* @brief Set item click callback
* @param obj Setting view object
* @param cb Callback function
* @param user_data User data passed to callback
*/
void lisa_ui_setting_view_set_click_cb(lv_obj_t *obj, lisa_ui_setting_item_click_cb_t cb, void *user_data);
/**
* @brief Clear all setting items
* @param obj Setting view object
*/
void lisa_ui_setting_view_clear(lv_obj_t *obj);
/**
* @brief Set back button click callback
* @param obj Setting view object
* @param cb Callback function
* @param user_data User data passed to callback
*/
void lisa_ui_setting_view_set_back_cb(lv_obj_t *obj, lisa_ui_setting_back_cb_t cb, void *user_data);
#ifdef __cplusplus
}
#endif
#endif /* __LISA_UI_SETTING_VIEW_H__ */

View File

@@ -0,0 +1,13 @@
target_sources(
${LISA_UI_LIB_NAME}
PRIVATE
lisa_ui_anim_ext.c
lisa_ui_anim.c
lisa_ui_toast.c
)
target_include_directories(
${LISA_UI_LIB_NAME}
PRIVATE
./
)

View File

@@ -0,0 +1,318 @@
#define TAG "LISA_UI_ANIM"
#include "lisa_ui.h"
#include "lisa_ui_anim.h"
#include <string.h>
#define MY_CLASS &lisa_ui_anim_class
#define ANIM_DEFAULT_DELAY_MS 100
static void lisa_ui_anim_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj);
static void lisa_ui_anim_destructor(const lv_obj_class_t *class_p, lv_obj_t *obj);
static void timer_cb(lv_timer_t *timer);
static uint32_t get_frame_delay(lisa_ui_anim_t *anim, uint16_t frame_idx);
static void update_frame(lisa_ui_anim_t *anim);
const lv_obj_class_t lisa_ui_anim_class = {.constructor_cb = lisa_ui_anim_constructor,
.destructor_cb = lisa_ui_anim_destructor,
.event_cb = NULL,
.width_def = LV_SIZE_CONTENT,
.height_def = LV_SIZE_CONTENT,
.instance_size = sizeof(lisa_ui_anim_t),
.base_class = &lv_img_class};
static void lisa_ui_anim_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
LV_UNUSED(class_p);
LV_TRACE_OBJ_CREATE("begin");
lisa_ui_anim_t *anim = (lisa_ui_anim_t *)obj;
anim->frames = NULL;
anim->delays = NULL;
anim->frame_count = 0;
anim->current_frame = 0;
anim->default_delay = ANIM_DEFAULT_DELAY_MS;
anim->timer = NULL;
anim->loop = -1;
anim->playing = 0;
anim->timer = lv_timer_create(timer_cb, anim->default_delay, obj);
if (anim->timer == NULL) {
LISA_UI_LOGE("Failed to create timer");
return;
}
lv_timer_pause(anim->timer);
LISA_UI_LOGI("Animation widget created");
LV_TRACE_OBJ_CREATE("finished");
}
static void lisa_ui_anim_destructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
LV_UNUSED(class_p);
LV_TRACE_OBJ_CREATE("begin");
lisa_ui_anim_t *anim = (lisa_ui_anim_t *)obj;
if (anim->timer != NULL) {
lv_timer_del(anim->timer);
anim->timer = NULL;
}
if (anim->delays != NULL) {
lv_mem_free(anim->delays);
anim->delays = NULL;
}
LISA_UI_LOGI("Animation widget destroyed");
LV_TRACE_OBJ_CREATE("finished");
}
static void timer_cb(lv_timer_t *timer)
{
lv_obj_t *obj = (lv_obj_t *)timer->user_data;
if (obj == NULL) {
return;
}
lisa_ui_anim_t *anim = (lisa_ui_anim_t *)obj;
if (anim->frames == NULL || anim->frame_count == 0) {
return;
}
anim->current_frame++;
if (anim->current_frame >= anim->frame_count) {
if (anim->loop < 0) {
anim->current_frame = 0;
if (anim->loop_cb) {
anim->loop_cb(obj);
}
} else if (anim->loop > 0) {
anim->current_frame = 0;
anim->loop--;
if (anim->loop_cb) {
anim->loop_cb(obj);
}
} else if (anim->loop == 0) {
anim->current_frame = anim->frame_count - 1;
anim->playing = 0;
lv_timer_pause(timer);
if (anim->stop_cb) {
anim->stop_cb(obj);
}
LISA_UI_LOGD("Animation finished");
return;
}
}
update_frame(anim);
uint32_t next_delay = get_frame_delay(anim, anim->current_frame);
lv_timer_set_period(timer, next_delay);
}
static uint32_t get_frame_delay(lisa_ui_anim_t *anim, uint16_t frame_idx)
{
uint32_t delay = anim->default_delay;
if (anim->delays != NULL && frame_idx < anim->frame_count) {
delay = anim->delays[frame_idx];
}
if (delay > 10 * 1000) {
LISA_UI_LOGW("Delay too long: %d, idx=%d", delay, frame_idx);
} else if (delay == 0) {
LISA_UI_LOGW("Delay too short: %d, idx=%d", delay, frame_idx);
}
return delay;
}
static void update_frame(lisa_ui_anim_t *anim)
{
if (anim->current_frame < anim->frame_count && anim->frames != NULL) {
lv_img_set_src(&anim->img.obj, &anim->frames[anim->current_frame]);
LISA_UI_LOGD("Frame %u/%u displayed", anim->current_frame + 1, anim->frame_count);
}
}
lv_obj_t *lisa_ui_anim_create(lv_obj_t *parent)
{
LV_LOG_INFO("begin");
lv_obj_t *obj = lv_obj_class_create_obj(MY_CLASS, parent);
lv_obj_class_init_obj(obj);
return obj;
}
int lisa_ui_anim_set_config(lv_obj_t *obj, const lisa_ui_anim_config_t *config)
{
LV_ASSERT_OBJ(obj, MY_CLASS);
if (config == NULL || config->frames == NULL || config->frame_count == 0) {
LISA_UI_LOGE("Invalid parameters");
return -1;
}
lisa_ui_anim_t *anim = (lisa_ui_anim_t *)obj;
if (anim->playing) {
lisa_ui_anim_pause(obj);
}
if (anim->delays != NULL) {
lv_mem_free(anim->delays);
anim->delays = NULL;
}
anim->frames = config->frames;
anim->frame_count = config->frame_count;
anim->default_delay = config->default_delay > 0 ? config->default_delay : ANIM_DEFAULT_DELAY_MS;
anim->loop = config->loop;
anim->current_frame = 0;
if (config->delays != NULL) {
anim->delays = (uint32_t *)lv_mem_alloc(sizeof(uint32_t) * config->frame_count);
if (anim->delays != NULL) {
memcpy(anim->delays, config->delays, sizeof(uint32_t) * config->frame_count);
} else {
LISA_UI_LOGW("Failed to allocate delays array, using default delay");
}
}
update_frame(anim);
int delay = get_frame_delay(anim, 0);
if (anim->timer != NULL) {
lv_timer_set_period(anim->timer, delay);
}
LISA_UI_LOGI("Animation configured: %u frames, default_delay=%ums, next_delay=%ums, loop=%d", config->frame_count,
anim->default_delay, delay, config->loop);
return 0;
}
int lisa_ui_anim_set_frames(lv_obj_t *obj, const lv_img_dsc_t *frames, uint16_t frame_count, uint32_t delay_ms, int loop)
{
lisa_ui_anim_config_t config = {
.frames = frames, .delays = NULL, .frame_count = frame_count, .default_delay = delay_ms, .loop = loop};
return lisa_ui_anim_set_config(obj, &config);
}
void lisa_ui_anim_start(lv_obj_t *obj)
{
LV_ASSERT_OBJ(obj, MY_CLASS);
lisa_ui_anim_t *anim = (lisa_ui_anim_t *)obj;
if (anim->timer == NULL || anim->frames == NULL || anim->frame_count == 0) {
LISA_UI_LOGE("Cannot start: animation not configured");
return;
}
if (!anim->playing) {
anim->playing = 1;
lv_timer_resume(anim->timer);
LISA_UI_LOGI("Animation started");
}
if (anim->loop > 0) {
anim->loop--;
}
}
void lisa_ui_anim_pause(lv_obj_t *obj)
{
LV_ASSERT_OBJ(obj, MY_CLASS);
lisa_ui_anim_t *anim = (lisa_ui_anim_t *)obj;
if (anim->timer == NULL) {
return;
}
if (anim->playing) {
anim->playing = 0;
lv_timer_pause(anim->timer);
LISA_UI_LOGI("Animation paused at frame %u", anim->current_frame);
}
}
void lisa_ui_anim_stop(lv_obj_t *obj)
{
LV_ASSERT_OBJ(obj, MY_CLASS);
lisa_ui_anim_t *anim = (lisa_ui_anim_t *)obj;
if (anim->timer == NULL) {
return;
}
anim->playing = 0;
anim->current_frame = 0;
lv_timer_pause(anim->timer);
lv_timer_set_period(anim->timer, get_frame_delay(anim, 0));
if (anim->frames != NULL && anim->frame_count > 0) {
update_frame(anim);
}
LISA_UI_LOGI("Animation stopped and reset");
}
void lisa_ui_anim_set_frame(lv_obj_t *obj, uint16_t frame_idx)
{
LV_ASSERT_OBJ(obj, MY_CLASS);
lisa_ui_anim_t *anim = (lisa_ui_anim_t *)obj;
if (anim->frames == NULL) {
return;
}
if (frame_idx < anim->frame_count) {
anim->current_frame = frame_idx;
update_frame(anim);
if (anim->timer != NULL) {
lv_timer_set_period(anim->timer, get_frame_delay(anim, frame_idx));
}
LISA_UI_LOGD("Frame set to %u", frame_idx);
} else {
LISA_UI_LOGW("Invalid frame index: %u (max: %u)", frame_idx, anim->frame_count - 1);
}
}
uint16_t lisa_ui_anim_get_frame(lv_obj_t *obj)
{
LV_ASSERT_OBJ(obj, MY_CLASS);
lisa_ui_anim_t *anim = (lisa_ui_anim_t *)obj;
return anim->current_frame;
}
bool lisa_ui_anim_is_playing(lv_obj_t *obj)
{
LV_ASSERT_OBJ(obj, MY_CLASS);
lisa_ui_anim_t *anim = (lisa_ui_anim_t *)obj;
return anim->playing != 0;
}
void lisa_ui_anim_stop_cb_set(lv_obj_t *obj, lisa_ui_anim_stop_cb_t cb)
{
LV_ASSERT_OBJ(obj, MY_CLASS);
lisa_ui_anim_t *anim = (lisa_ui_anim_t *)obj;
anim->stop_cb = cb;
}
void lisa_ui_anim_loop_cb_set(lv_obj_t *obj, lisa_ui_anim_loop_cb_t cb)
{
LV_ASSERT_OBJ(obj, MY_CLASS);
lisa_ui_anim_t *anim = (lisa_ui_anim_t *)obj;
anim->loop_cb = cb;
}

View File

@@ -0,0 +1,56 @@
#ifndef LISA_UI_ANIM_H
#define LISA_UI_ANIM_H
#ifdef __cplusplus
extern "C" {
#endif
#include "lvgl.h"
typedef void (*lisa_ui_anim_stop_cb_t)(lv_obj_t *obj);
typedef void (*lisa_ui_anim_loop_cb_t)(lv_obj_t *obj);
typedef struct {
lv_img_t img;
lisa_ui_anim_stop_cb_t stop_cb;
lisa_ui_anim_loop_cb_t loop_cb;
const lv_img_dsc_t *frames;
uint32_t *delays;
uint16_t frame_count;
uint16_t current_frame;
uint32_t default_delay;
lv_timer_t *timer;
int32_t loop;
uint8_t playing : 1;
} lisa_ui_anim_t;
typedef struct {
const lv_img_dsc_t *frames;
const uint32_t *delays;
uint16_t frame_count;
uint32_t default_delay;
int loop;
int pause_frame_idx;
} lisa_ui_anim_config_t;
extern const lv_obj_class_t lisa_ui_anim_class;
lv_obj_t *lisa_ui_anim_create(lv_obj_t *parent);
int lisa_ui_anim_set_config(lv_obj_t *obj, const lisa_ui_anim_config_t *config);
int lisa_ui_anim_set_frames(lv_obj_t *obj, const lv_img_dsc_t *frames, uint16_t frame_count,
uint32_t delay_ms, int loop);
void lisa_ui_anim_start(lv_obj_t *obj);
void lisa_ui_anim_pause(lv_obj_t *obj);
void lisa_ui_anim_stop(lv_obj_t *obj);
void lisa_ui_anim_set_frame(lv_obj_t *obj, uint16_t frame_idx);
uint16_t lisa_ui_anim_get_frame(lv_obj_t *obj);
bool lisa_ui_anim_is_playing(lv_obj_t *obj);
void lisa_ui_anim_stop_cb_set(lv_obj_t *obj, lisa_ui_anim_stop_cb_t cb);
void lisa_ui_anim_loop_cb_set(lv_obj_t *obj, lisa_ui_anim_loop_cb_t cb);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,161 @@
#include "lisa_ui_anim_ext.h"
static void lisa_ui_anim_ext_class_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj);
static void lisa_ui_anim_ext_class_destructor(const lv_obj_class_t *class_p, lv_obj_t *obj);
const lv_obj_class_t lisa_ui_anim_ext_class = {
.constructor_cb = lisa_ui_anim_ext_class_constructor,
.destructor_cb = lisa_ui_anim_ext_class_destructor,
.event_cb = NULL,
.width_def = LV_PCT(100),
.height_def = LV_PCT(100),
.instance_size = sizeof(lisa_ui_anim_ext_t),
.base_class = &lisa_ui_anim_class,
};
#define MY_CLASS &lisa_ui_anim_ext_class
static void lisa_ui_anim_stop_cb_handle(lv_obj_t *obj)
{
lisa_ui_anim_ext_t *anim_ext = (lisa_ui_anim_ext_t *)obj;
if (anim_ext->state == LISA_UI_ANIM_STATE_ENTER) {
anim_ext->state = LISA_UI_ANIM_STATE_LOOP;
lisa_ui_anim_set_config(obj, &anim_ext->curr.loop);
lisa_ui_anim_start(obj);
} else if (anim_ext->state == LISA_UI_ANIM_STATE_LOOP) {
anim_ext->state = LISA_UI_ANIM_STATE_STOPING;
if (anim_ext->curr.exit.frame_count != 0) {
lisa_ui_anim_set_config(obj, &anim_ext->curr.exit);
lisa_ui_anim_start(obj);
}
} else if (anim_ext->state == LISA_UI_ANIM_STATE_NEXT_REQ) {
lisa_ui_anim_ext_set_config(obj, &anim_ext->next);
lisa_ui_anim_ext_start(obj);
} else {
anim_ext->state = LISA_UI_ANIM_STATE_STOPED;
}
}
static void lisa_ui_anim_loop_cb_handle(lv_obj_t *obj)
{
lisa_ui_anim_ext_t *anim_ext = (lisa_ui_anim_ext_t *)obj;
if (anim_ext->state == LISA_UI_ANIM_STATE_STOP_REQ) {
if (anim_ext->curr.exit.frame_count != 0) {
anim_ext->state = LISA_UI_ANIM_STATE_STOPING;
lisa_ui_anim_set_config(obj, &anim_ext->curr.exit);
lisa_ui_anim_start(obj);
} else {
anim_ext->state = LISA_UI_ANIM_STATE_STOPED;
lisa_ui_anim_stop(obj);
}
} else if (anim_ext->state == LISA_UI_ANIM_STATE_NEXT_REQ) {
lisa_ui_anim_ext_set_config(obj, &anim_ext->next);
lisa_ui_anim_ext_start(obj);
}
}
static void lisa_ui_anim_ext_class_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
LV_UNUSED(class_p);
LV_TRACE_OBJ_CREATE("begin");
lisa_ui_anim_ext_t *anim_ext = (lisa_ui_anim_ext_t *)obj;
memset(&anim_ext->curr, 0, sizeof(lisa_ui_anim_config_t));
memset(&anim_ext->next, 0, sizeof(lisa_ui_anim_config_t));
anim_ext->state = LISA_UI_ANIM_STATE_NONE;
lisa_ui_anim_stop_cb_set(obj, lisa_ui_anim_stop_cb_handle);
lisa_ui_anim_loop_cb_set(obj, lisa_ui_anim_loop_cb_handle);
LV_TRACE_OBJ_CREATE("finished");
}
static void lisa_ui_anim_ext_class_destructor(const lv_obj_class_t *class_p, lv_obj_t *obj)
{
LV_UNUSED(class_p);
LV_TRACE_OBJ_CREATE("begin");
LV_TRACE_OBJ_CREATE("finished");
}
lv_obj_t *lisa_ui_anim_ext_create(lv_obj_t *parent)
{
lv_obj_t *obj = lv_obj_class_create_obj(MY_CLASS, parent);
lv_obj_class_init_obj(obj);
return obj;
}
void lisa_ui_anim_ext_set_config(lv_obj_t *obj, const lisa_ui_anim_ext_config_t *config)
{
LV_ASSERT_OBJ(obj, MY_CLASS);
LV_ASSERT(config != NULL);
lisa_ui_anim_ext_t *anim_ext = (lisa_ui_anim_ext_t *)obj;
memcpy(&anim_ext->curr.enter, &config->enter, sizeof(lisa_ui_anim_config_t));
memcpy(&anim_ext->curr.loop, &config->loop, sizeof(lisa_ui_anim_config_t));
memcpy(&anim_ext->curr.exit, &config->exit, sizeof(lisa_ui_anim_config_t));
}
void lisa_ui_anim_ext_start(lv_obj_t *obj)
{
lisa_ui_anim_ext_t *anim_ext = (lisa_ui_anim_ext_t *)obj;
lisa_ui_anim_config_t *cfg = &anim_ext->curr.enter;
if (cfg->frame_count == 0) {
cfg = &anim_ext->curr.loop;
anim_ext->state = LISA_UI_ANIM_STATE_LOOP;
} else {
anim_ext->state = LISA_UI_ANIM_STATE_ENTER;
}
lisa_ui_anim_set_config(obj, cfg);
lisa_ui_anim_start(obj);
}
void lisa_ui_anim_ext_stop(lv_obj_t *obj)
{
lisa_ui_anim_ext_t *anim_ext = (lisa_ui_anim_ext_t *)obj;
if (anim_ext->state == LISA_UI_ANIM_STATE_NONE || anim_ext->state == LISA_UI_ANIM_STATE_STOPED) {
return;
}
if (anim_ext->curr.exit.frame_count == 0) {
anim_ext->state = LISA_UI_ANIM_STATE_STOPED;
lisa_ui_anim_stop(obj);
return;
}
anim_ext->state = LISA_UI_ANIM_STATE_STOP_REQ;
}
void lisa_ui_anim_ext_next(lv_obj_t *obj, const lisa_ui_anim_ext_config_t *next)
{
lisa_ui_anim_ext_t *anim_ext = (lisa_ui_anim_ext_t *)obj;
if ((anim_ext->state != LISA_UI_ANIM_STATE_STOPED) && (anim_ext->state != LISA_UI_ANIM_STATE_NONE)) {
memcpy(&anim_ext->next, next, sizeof(lisa_ui_anim_ext_config_t));
anim_ext->state = LISA_UI_ANIM_STATE_NEXT_REQ;
} else {
lisa_ui_anim_ext_set_config(obj, next);
lisa_ui_anim_ext_start(obj);
}
}
void lisa_ui_anim_ext_next_imm(lv_obj_t *obj, const lisa_ui_anim_ext_config_t *next)
{
lisa_ui_anim_ext_t *anim_ext = (lisa_ui_anim_ext_t *)obj;
lisa_ui_anim_ext_exit(obj);
lisa_ui_anim_ext_next(obj, next);
}
void lisa_ui_anim_ext_exit(lv_obj_t *obj)
{
lisa_ui_anim_ext_t *anim_ext = (lisa_ui_anim_ext_t *)obj;
anim_ext->state = LISA_UI_ANIM_STATE_STOPED;
lisa_ui_anim_stop(obj);
}

View File

@@ -0,0 +1,40 @@
#ifndef __LISA_UI_ANIM_EXT_H__
#define __LISA_UI_ANIM_EXT_H__
#include "lisa_ui_anim.h"
enum {
LISA_UI_ANIM_STATE_NONE,
LISA_UI_ANIM_STATE_ENTER,
LISA_UI_ANIM_STATE_LOOP,
LISA_UI_ANIM_STATE_STOP_REQ,
LISA_UI_ANIM_STATE_NEXT_REQ,
LISA_UI_ANIM_STATE_STOPING,
LISA_UI_ANIM_STATE_STOPED,
LISA_UI_ANIM_STATE_MAX,
};
typedef struct {
lisa_ui_anim_config_t enter;
lisa_ui_anim_config_t loop;
lisa_ui_anim_config_t exit;
} lisa_ui_anim_ext_config_t;
typedef struct {
lisa_ui_anim_t anim;
lisa_ui_anim_ext_config_t curr;
lisa_ui_anim_ext_config_t next;
uint8_t state;
} lisa_ui_anim_ext_t;
extern const lv_obj_class_t lisa_ui_anim_ext_class;
lv_obj_t *lisa_ui_anim_ext_create(lv_obj_t *parent);
void lisa_ui_anim_ext_exit(lv_obj_t *obj);
void lisa_ui_anim_ext_stop(lv_obj_t *obj);
void lisa_ui_anim_ext_start(lv_obj_t *obj);
void lisa_ui_anim_ext_next(lv_obj_t *obj, const lisa_ui_anim_ext_config_t *next);
void lisa_ui_anim_ext_next_imm(lv_obj_t *obj, const lisa_ui_anim_ext_config_t *next);
void lisa_ui_anim_ext_set_config(lv_obj_t *obj, const lisa_ui_anim_ext_config_t *config);
#endif /* __LISA_UI_ANIM_EXT_H__ */

View File

@@ -0,0 +1,48 @@
#include "lvgl.h"
#include "lisa_ui.h"
#include "lisa_ui_assets.h"
#include "lisa_ui_fonts.h"
static lv_obj_t *toast_obj = NULL;
static lv_timer_t *toast_timer = NULL;
static void toast_hide_cb(lv_timer_t *timer)
{
if (toast_obj) {
lv_obj_del(toast_obj);
toast_obj = NULL;
}
if (toast_timer) {
lv_timer_del(toast_timer);
toast_timer = NULL;
}
}
void lisa_ui_toast_show(const char *txt)
{
if (toast_obj) {
toast_hide_cb(NULL);
}
toast_obj = lv_obj_create(lv_layer_sys());
lv_obj_add_flag(toast_obj, LV_OBJ_FLAG_FLOATING);
lv_obj_set_size(toast_obj, lv_pct(80), LV_SIZE_CONTENT);
lv_obj_set_style_radius(toast_obj, 8, LV_PART_MAIN);
lv_obj_set_style_bg_color(toast_obj, lv_color_hex(0x333333), LV_PART_MAIN);
lv_obj_set_style_bg_opa(toast_obj, LV_OPA_80, LV_PART_MAIN);
lv_obj_set_style_border_width(toast_obj, 0, LV_PART_MAIN);
lv_obj_set_style_pad_all(toast_obj, 12, LV_PART_MAIN);
lv_obj_set_style_pad_gap(toast_obj, 12, LV_PART_MAIN);
lv_obj_t *label = lv_label_create(toast_obj);
lv_label_set_text(label, txt);
lv_obj_set_style_text_color(label, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
lv_obj_set_style_text_font(label, &lv_font_chinese_16, LV_PART_MAIN);
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
lv_label_set_long_mode(label, LV_LABEL_LONG_SCROLL_CIRCULAR);
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
lv_obj_align(toast_obj, LV_ALIGN_BOTTOM_MID, 0, -40);
toast_timer = lv_timer_create(toast_hide_cb, 2000, NULL);
}

View File

@@ -0,0 +1,6 @@
#ifndef __LISA_UI_TOAST_H__
#define __LISA_UI_TOAST_H__
void lisa_ui_toast_show(const char *txt);
#endif /* __LISA_UI_TOAST_H__ */

View File

@@ -0,0 +1,82 @@
add_subdirectory(fonts)
set(ASSETS_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/embed)
set(ASSETS_OUTPUT_DIR ${CMAKE_BINARY_DIR}/generated/assets)
set(ASSETS_SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/assets.py)
# Get list of input asset files
file(GLOB_RECURSE ASSET_FILES
${ASSETS_SOURCE_DIR}/*.png
${ASSETS_SOURCE_DIR}/*.jpg
${ASSETS_SOURCE_DIR}/*.jpeg
)
# Generate list of expected output C files based on input files
# Match Python script naming: path/to/file-name.png -> path_to_file_name_png.c
set(GENERATED_ASSET_SOURCES)
foreach(ASSET_FILE ${ASSET_FILES})
# Get relative path from ASSETS_SOURCE_DIR
file(RELATIVE_PATH REL_PATH ${ASSETS_SOURCE_DIR} ${ASSET_FILE})
# Convert path to C filename: match Python's sanitize_var_name function
# Replace /, \, ., - with _
string(REPLACE "/" "_" C_FILENAME "${REL_PATH}")
string(REPLACE "\\" "_" C_FILENAME "${C_FILENAME}")
string(REPLACE "." "_" C_FILENAME "${C_FILENAME}")
string(REPLACE "-" "_" C_FILENAME "${C_FILENAME}")
set(C_FILENAME "${C_FILENAME}.c")
# Add to list with absolute path
list(APPEND GENERATED_ASSET_SOURCES "${ASSETS_OUTPUT_DIR}/${C_FILENAME}")
endforeach()
list(LENGTH GENERATED_ASSET_SOURCES ASSET_COUNT)
message(STATUS "Will generate ${ASSET_COUNT} asset C files")
# Run generation during CMake configure to ensure files exist
execute_process(
COMMAND ${CMAKE_COMMAND} -E make_directory ${ASSETS_OUTPUT_DIR}
COMMAND python3 ${ASSETS_SCRIPT} ${ASSETS_SOURCE_DIR} ${ASSETS_OUTPUT_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
RESULT_VARIABLE ASSETS_RESULT
)
if(NOT ASSETS_RESULT EQUAL 0)
message(WARNING "Failed to generate assets during configuration")
endif()
# Use a stamp file to track generation for incremental builds
set(ASSETS_STAMP_FILE ${ASSETS_OUTPUT_DIR}/.assets_stamp)
# Custom command for incremental builds
add_custom_command(
OUTPUT ${ASSETS_STAMP_FILE}
COMMAND python3 ${ASSETS_SCRIPT} ${ASSETS_SOURCE_DIR} ${ASSETS_OUTPUT_DIR}
COMMAND ${CMAKE_COMMAND} -E touch ${ASSETS_STAMP_FILE}
DEPENDS ${ASSETS_SCRIPT} ${ASSET_FILES}
COMMENT "Regenerating asset C files from ${ASSETS_SOURCE_DIR}"
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
VERBATIM
)
# Create a custom target
add_custom_target(generate_assets_target ALL
DEPENDS ${ASSETS_STAMP_FILE}
)
# Mark generated files as GENERATED
set_source_files_properties(
${GENERATED_ASSET_SOURCES}
PROPERTIES GENERATED TRUE
)
# Make library depend on generation target
add_dependencies(${LISA_UI_LIB_NAME} generate_assets_target)
# Add assets source files
target_sources(${LISA_UI_LIB_NAME} PRIVATE
ble_qr.c
${GENERATED_ASSET_SOURCES}
)
target_include_directories(${LISA_UI_LIB_NAME} PUBLIC
${ASSETS_OUTPUT_DIR}
${CMAKE_CURRENT_SOURCE_DIR}
)

219
apps-ui/assets/assets.py Normal file
View File

@@ -0,0 +1,219 @@
#!/usr/bin/env python3
import os
import sys
import struct
import hashlib
import json
from pathlib import Path
try:
from PIL import Image
HAS_PIL = True
except ImportError:
HAS_PIL = False
def sanitize_var_name(path_str):
var_name = path_str.replace('/', '_').replace('\\', '_').replace('.', '_').replace('-', '_')
var_name = ''.join(c if c.isalnum() or c == '_' else '_' for c in var_name)
if var_name[0].isdigit():
var_name = '_' + var_name
return var_name
def get_file_hash(file_path):
hash_md5 = hashlib.md5()
with open(file_path, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
return hash_md5.hexdigest()
def load_cache(cache_file):
if cache_file.exists():
try:
with open(cache_file, 'r') as f:
return json.load(f)
except:
return {}
return {}
def save_cache(cache_file, cache_data):
with open(cache_file, 'w') as f:
json.dump(cache_data, f, indent=2)
def get_png_dimensions(data):
if len(data) < 24:
return 0, 0
if data[0:8] != b'\x89PNG\r\n\x1a\n':
return 0, 0
width = struct.unpack('>I', data[16:20])[0]
height = struct.unpack('>I', data[20:24])[0]
return width, height
def get_jpeg_dimensions(data):
if len(data) < 2:
return 0, 0
if data[0:2] != b'\xff\xd8':
return 0, 0
i = 2
while i < len(data) - 1:
if data[i] != 0xff:
return 0, 0
marker = data[i + 1]
i += 2
if marker == 0xd8 or marker == 0xd9 or marker == 0x01:
continue
if marker >= 0xd0 and marker <= 0xd7:
continue
if i + 2 > len(data):
return 0, 0
length = struct.unpack('>H', data[i:i+2])[0]
if marker >= 0xc0 and marker <= 0xcf and marker not in [0xc4, 0xc8, 0xcc]:
if i + 5 <= len(data):
height = struct.unpack('>H', data[i+3:i+5])[0]
width = struct.unpack('>H', data[i+5:i+7])[0]
return width, height
i += length
return 0, 0
def get_image_info(file_path, data):
ext = file_path.suffix.lower()
if ext == '.png':
width, height = get_png_dimensions(data)
return 'LV_IMG_CF_TRUE_COLOR_ALPHA', width, height
elif ext in ['.jpg', '.jpeg']:
width, height = get_jpeg_dimensions(data)
return 'LV_IMG_CF_TRUE_COLOR_ALPHA', width, height
return 'LV_IMG_CF_RAW', 0, 0
def file_to_c_file(file_path, var_name):
with open(file_path, 'rb') as f:
data = f.read()
color_format, width, height = get_image_info(file_path, data)
c_content = '#include "lvgl.h"\n\n'
c_content += f'static const unsigned char {var_name}_data[] = {{\n'
for i in range(0, len(data), 16):
chunk = data[i:i+16]
hex_values = ', '.join(f'0x{b:02x}' for b in chunk)
c_content += f' {hex_values},\n'
c_content += '};\n\n'
c_content += f'const lv_img_dsc_t {var_name} = {{\n'
c_content += ' .header = {\n'
c_content += f' .cf = {color_format},\n'
c_content += ' .always_zero = 0,\n'
c_content += ' .reserved = 0,\n'
c_content += f' .w = {width},\n'
c_content += f' .h = {height},\n'
c_content += ' },\n'
c_content += f' .data_size = {len(data)},\n'
c_content += f' .data = {var_name}_data,\n'
c_content += '};\n'
return c_content
def generate_assets(root_dir, output_dir):
root_path = Path(root_dir)
output_path = Path(output_dir)
if not root_path.exists():
print(f"Error: Directory {root_dir} does not exist")
return
output_path.mkdir(parents=True, exist_ok=True)
cache_file = output_path / '.assets_cache.json'
cache_data = load_cache(cache_file)
file_count = 0
updated_count = 0
skipped_count = 0
var_names = []
current_files = {}
for file_path in sorted(root_path.rglob('*')):
if file_path.is_file() and file_path.suffix not in ['.py', '.pyc']:
rel_path = file_path.relative_to(root_path)
rel_path_str = str(rel_path)
var_name = sanitize_var_name(rel_path_str)
output_c_file = output_path / (var_name + '.c')
file_hash = get_file_hash(file_path)
current_files[rel_path_str] = file_hash
cached_hash = cache_data.get(rel_path_str)
if cached_hash == file_hash and output_c_file.exists():
# print(f"Skipping (unchanged): {rel_path}")
skipped_count += 1
else:
# print(f"Processing: {rel_path} -> {output_c_file.name}")
c_content = file_to_c_file(file_path, var_name)
with open(output_c_file, 'w') as f:
f.write(c_content)
updated_count += 1
var_names.append(var_name)
file_count += 1
for old_file in cache_data.keys():
if old_file not in current_files:
var_name = sanitize_var_name(old_file)
old_c_file = output_path / (var_name + '.c')
if old_c_file.exists():
old_c_file.unlink()
print(f"Removed: {old_c_file.name}")
header_file = output_path / 'lisa_ui_assets.h'
h_content = '#ifndef __LISA_UI_ASSETS_H__\n'
h_content += '#define __LISA_UI_ASSETS_H__\n\n'
h_content += '#include "lvgl.h"\n\n'
for var_name in var_names:
h_content += f'LV_IMG_DECLARE({var_name});\n'
h_content += '\n#endif\n'
header_needs_update = True
if header_file.exists():
with open(header_file, 'r') as f:
old_content = f.read()
header_needs_update = (old_content != h_content)
if header_needs_update:
with open(header_file, 'w') as f:
f.write(h_content)
print(f"Updated header: {header_file.name}")
else:
print(f"Header unchanged: {header_file.name}")
save_cache(cache_file, current_files)
print(f"\nTotal: {file_count} files, Updated: {updated_count}, Skipped: {skipped_count}")
print(f"Output directory: {output_dir}")
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: python assets.py <source_directory> [output_directory]")
print("Example: python assets.py emoji ./generated")
sys.exit(1)
root_dir = sys.argv[1]
output_dir = sys.argv[2] if len(sys.argv) > 2 else './generated'
generate_assets(root_dir, output_dir)

1475
apps-ui/assets/ble_qr.c Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,67 @@
#ifdef __has_include
#if __has_include("lvgl.h")
#ifndef LV_LVGL_H_INCLUDE_SIMPLE
#define LV_LVGL_H_INCLUDE_SIMPLE
#endif
#endif
#endif
#if defined(LV_LVGL_H_INCLUDE_SIMPLE)
#include "lvgl.h"
#else
#include "lvgl/lvgl.h"
#endif
#ifndef LV_ATTRIBUTE_MEM_ALIGN
#define LV_ATTRIBUTE_MEM_ALIGN
#endif
#ifndef LV_ATTRIBUTE_IMG_IC_LAUNCH_SETTING
#define LV_ATTRIBUTE_IMG_IC_LAUNCH_SETTING
#endif
const LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST LV_ATTRIBUTE_IMG_IC_LAUNCH_SETTING uint8_t ic_launch_setting_map[] = {
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x3c, 0x3c, 0x00,
0x00, 0x7e, 0x7e, 0x00,
0x00, 0xff, 0xff, 0x00,
0x00, 0xff, 0xff, 0x00,
0x00, 0xff, 0xff, 0x00,
0x00, 0x7f, 0xfe, 0x00,
0x00, 0x78, 0x1e, 0x00,
0x01, 0xf3, 0xcf, 0x80,
0x0f, 0xe7, 0xe7, 0xf0,
0x0f, 0xef, 0xf7, 0xf0,
0x0f, 0xef, 0xf7, 0xf0,
0x0f, 0xef, 0xf7, 0xf0,
0x0f, 0xef, 0xf7, 0xf0,
0x0f, 0xe7, 0xe7, 0xf0,
0x01, 0xf3, 0xcf, 0x80,
0x00, 0x78, 0x1e, 0x00,
0x00, 0x7f, 0xfe, 0x00,
0x00, 0xff, 0xff, 0x00,
0x00, 0xff, 0xff, 0x00,
0x00, 0xff, 0xff, 0x00,
0x00, 0x7e, 0x7e, 0x00,
0x00, 0x3c, 0x3c, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
};
const lv_img_dsc_t ic_launch_setting = {
.header.cf = LV_IMG_CF_ALPHA_1BIT,
.header.always_zero = 0,
.header.reserved = 0,
.header.w = 32,
.header.h = 32,
.data_size = 128,
.data = ic_launch_setting_map,
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

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