Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 83 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,67 @@ USB 数据线 D+_OUT/D-_OUT 经 USBLC6-2SC6 ESD 后直连 ESP32-C3 原生 USB (I

---

## 状态机架构

自 v1.0-34 起,设备运行逻辑由 **5 个正交 (orthogonal) 有限状态机** 管理。每个 FSM 维护自己的状态,通过事件路由器解耦。

```
┌─────────────────────────────────────────────────────────┐
│ main loop (1Hz tick) │
│ button bits / wifi queue / audio queue / EVT_TICK_1HZ │
│ │ │
│ route_event() │
│ (event_router) │
│ ┌────────┬───────┼───────┬──────────┐ │
│ ▼ ▼ ▼ ▼ ▼ │
│ wake_fsm sys_fsm net_fsm audio_fsm display_fsm │
│ (6状态) (3状态) (5状态) (6状态) (3状态) │
│ │ │ │ │ │ │
│ └────────┴───────┼───────┴──────────┘ │
│ ▼ │
│ apply_actions() │
│ (executor) │
│ audio_init / audio_play / wifi_connect / ... │
└─────────────────────────────────────────────────────────┘
```

### 5 个 FSM 职责

| FSM | 状态 | 典型转换 |
|-----|------|---------|
| **wake_fsm** | DORMANT / FROM_BTN / FROM_RTC / FROM_SYS / ALARM_RINGING / GOTO_SLEEP | 冷启动检测唤醒源;RTC 唤醒强制 auto-play;用户关闹钟退回 FROM_BTN |
| **sys_fsm** | BOOT / NORMAL / SLEEPING | BOOT→NORMAL(主循环就绪);BTN_SLEEP_PRESS→SLEEPING+deep sleep;工厂复位 |
| **net_fsm** | OFFLINE / PROVISIONING / CONNECTING / CONNECTED / FAILED | 无凭据→PROVISIONING;IP_GOT→CONNECTED;断开→CONNECTING;30s超时→FAILED |
| **audio_fsm** | IDLE / PENDING / INIT / PLAYING / STOPPING / ERROR | 左键短按→INIT→PLAYING;待WiFi→PENDING;停止→STOPPING→IDLE;自然播完→IDLE(自动切歌) |
| **display_fsm** | DAY / NIGHT_AUTO / NIGHT_FORCED | 时间穿越夜间时段→NIGHT_AUTO;长按右键→强制切换 |

### 事件路由器

22 种原始事件 (`app_event_t`) 通过 `route_event()` 分发到 5 个区域:
- **1-1 映射**:大部分按钮事件只到单一区域 (如 `AUDIO_TOGGLE` → `audio_fsm`)
- **fan-out**:`EVT_TICK_1HZ` 到全部 5 个区域,`EVT_IP_GOT` 同时到 `net_fsm`+`audio_fsm` (PENDING→INIT)
- **条件 fan-out**:`EVT_BOOT_DONE` 只在 `wake=FROM_RTC` 时才给 `audio_fsm` 发 `AUTO_PLAY_REQUEST`

### 执行器

FSM step 返回的动作列表 (`fsm_actions_t`) 由 `apply_actions()` 串行执行,顺序固定:**wake → sys → net → audio → display**。

### 测试

每个区域 FSM 的主机端单元测试位于 `tests/test_*_fsm.c`,直接链接生产代码 (`main/regions/*_fsm.c`),不做镜像复制。运行方式:

```bash
make -C tests test # 8/8 测试套件,109 个用例
```

相关文件:
- `main/app_fsm.h` — 共享类型 (`app_event_t`, `app_input_t`, `fsm_action_t`)
- `main/event_router.h/.c` — 事件路由
- `main/regions/{wake,sys,net,audio,display}_fsm.h/.c` — 5 个 FSM 实现
- `main/main.c` — executor + 主 FSM 循环 (~line 1148)

---

## 程序启动流程

```mermaid
Expand All @@ -203,58 +264,31 @@ flowchart TD
C -->|右键| E[wake=btn]
C -->|冷启动| F[wake=sys]

D --> G[GPIO 早期初始化]
D --> G[GPIO + SSD1322 + 双键 + RTC + LVGL]
E --> G
F --> G

G --> H[ssd1322_init<br/>显示保持关闭]
H --> I[双键初始化<br/>右键: 短=睡眠 长=日夜 三击=重置<br/>左键: 短=播放 长=下一首]
I --> J[wifi_set_timezone<br/>CST-8]
J --> K[PCF85063 RTC 初始化<br/>读取时间恢复到系统]
K --> L[lvgl_adapter_init<br/>L8→I4 DMA]
L --> M{首次启动无 WiFi 凭据?}
M -->|是| MA[config_screen<br/>QR 码配网页面]
M -->|否| MB[clock_screen_create<br/>首帧渲染]
MA --> MC[ssd1322_display_on]
MB --> MC

MC --> N{夜间模式?}

N -->|是| O[跳过 WiFi/天气/音频<br/>极暗数码管显示]
N -->|否| P{唤醒源 = RTC?}

P -->|否 (按键唤醒)| NA[无网络模式<br/>显示室内温湿度 + 闹钟时间<br/>底部提示: 按左键播放]
P -->|是 (RTC 闹钟)| Q[WiFi STA 连接]
Q --> R[SNTP 时间同步]
R --> RC[PCF85063 时间回写]
RC --> S{Agent 已启用?}
S -->|否| SA[clock-only 模式<br/>跳过 /api/esp]
S -->|是| T[audio_fetch_api<br/>单次 HTTP GET /api/esp]
T --> U[解析天气 + 电台URL]
U --> V{有电台URL?}
V -->|是| W[audio_play_url<br/>HTTP 流 MP3 解码]
V -->|否| WA[跳过音频]
T --> TB[arm_pcf85063_alarm<br/>写入下次闹钟]

O --> X[主循环 1800s]
NA --> X
SA --> X
W --> X
WA --> X

X --> Y{事件驱动 1s tick}
Y -->|右键短按| Z[深度睡眠]
Y -->|右键三击| ZA[恢复出厂 → 重启]
Y -->|右键长按| ZB[强制切换日夜模式]
Y -->|左键短按| ZC[播放/停止<br/>首次自动连 WiFi]
Y -->|左键长按| ZD[下一首]
Y -->|歌曲结束/卡住| ZE[auto_advance<br/>请求下一首]
Y -->|超时 1800s| Z
Y -->|每10秒| ZF[刷新 SHTC3 传感器]
ZE --> T
ZC --> T
ZD --> T
ZF --> X
G --> H{首次启动无 WiFi 凭据?}
H -->|是| HA[配网页面]
H -->|否| HB[clock_screen_create]

HA --> I[ssd1322_display_on]
HB --> I

I --> J{夜间模式?}
J -->|是| K[极暗数码管显示]
J -->|否| L{唤醒源 = RTC?}
L -->|否| M[无网络模式<br/>室内温湿度 + 闹钟时间]
L -->|是| N[WiFi + /api/esp + auto-play]

K --> O[FSM 启动注入 EVT_BOOT_DONE]
M --> O
N --> O

O --> P[FSM 驱动主循环 1800s]
P -->|事件| Q[route_event → 5 region step → executor]
Q -->|SYS_SLEEPING| R[深度睡眠]
R --> A
```

---
Expand Down
8 changes: 7 additions & 1 deletion main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,11 @@ idf_component_register(SRCS "config_screen.c" "wifi_provisioning.c" "main.c"
"ui/ui.c"
"ui/screens.c"
"audio_player_wrapper.c"
INCLUDE_DIRS "."
"event_router.c"
"regions/wake_fsm.c"
"regions/sys_fsm.c"
"regions/net_fsm.c"
"regions/audio_fsm.c"
"regions/display_fsm.c"
INCLUDE_DIRS "." "regions"
REQUIRES lvgl__lvgl driver esp_netif esp_wifi esp_http_server lwip nvs_flash esp_http_client json mbedtls esp-audio-player esp_driver_i2s espressif__button esp-libhelix-mp3 shtc3 i2c_bus pcf85063 esp_timer)
208 changes: 208 additions & 0 deletions main/app_fsm.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*
* app_fsm.h — 公共类型:5 个正交 region FSM 共享的事件 / 输入 / 动作类型。
*
* 设计原则:
* - 每个 region 的状态 + 事件枚举在自己的 regions/<name>_fsm.h 里定义
* - app_fsm.h 通过前向声明 typedef 把 region 状态类型嵌入 app_state_t
* - region step 函数签名也在各自的 regions/<name>_fsm.h 中声明
* - 此文件不依赖 ESP-IDF / LVGL / audio_player 等,可在主机上被直接 #include
*/

#ifndef APP_FSM_H
#define APP_FSM_H

#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>

#ifdef __cplusplus
extern "C" {
#endif

/* ── region 状态类型 ──────────────────────────────────────────────────────
* 完整定义在 regions/<name>_fsm.h(通过下方 _state_t 字段使用它们)。
* C 不允许 incomplete enum 作为 struct 字段,所以 app_state_t 实际使用 int
* 字段;赋值时靠编译器 int↔enum 隐式转换。region step() 函数仍用强类型。 */

/* ── 唤醒来源 ─────────────────────────────────────────────────────────
* 与 main.c 的 wake_kind_t 共用同一枚举 (WAKE_BTN / WAKE_RTC / WAKE_SYS)。
* 这里是定义来源;main.c 不要重复 typedef。 */
typedef enum {
WAKE_NONE = 0, /* 未检测 (boot 前) */
WAKE_BTN = 1, /* 用户按右键 (CONFIG_WAKEUP_GPIO) */
WAKE_RTC = 2, /* PCF85063 闹钟 (CONFIG_PCF85063_INT_GPIO) */
WAKE_SYS = 3, /* 冷启动 / 异常 */
} wake_kind_t;

/* ── 音频播放器回调事件 (来自 esp-audio-player, 简化版,避免头依赖) ── */
typedef enum {
APP_AUDIO_PLAYER_EVT_NONE = 0,
APP_AUDIO_PLAYER_EVT_PLAYING,
APP_AUDIO_PLAYER_EVT_IDLE,
APP_AUDIO_PLAYER_EVT_NEXT,
APP_AUDIO_PLAYER_EVT_PAUSE,
APP_AUDIO_PLAYER_EVT_SHUTDOWN,
APP_AUDIO_PLAYER_EVT_UNKNOWN_FILE,
APP_AUDIO_PLAYER_EVT_ERROR,
} app_audio_player_evt_t;

/* ── 应用层动作种类 ──────────────────────────────────────────────────────── */
/* 一个动作最多带一个 payload(union)。动作执行顺序固定:
* wake → sys → net → audio → display
* 每个 apply_actions() 内部:stop_* 在 init_* 之前,deinit 在 init 之前。 */
typedef enum {
ACT_NONE = 0,
/* 唤醒相关 */
ACT_DISPLAY_FADE_IN,
ACT_DISPLAY_FADE_OUT,
ACT_VOLUME_MAX, /* 闹钟唤醒强制最大音量 */
ACT_VOLUME_RESTORE, /* 闹钟完成后恢复 */
ACT_ARM_RTC_FOR_TOMORROW, /* arm 第二天同一时间的闹铃 */
/* 显示 */
ACT_DISPLAY_OFF,
ACT_DISPLAY_BRIGHT,
ACT_DISPLAY_STATION, /* payload: const char * */
ACT_DISPLAY_AUDIO_INDICATOR, /* payload: bool on */
ACT_DISPLAY_INDOOR_FULL, /* payload: float temp_c, float humidity */
ACT_DISPLAY_ALARM_TIME, /* payload: int hour, int minute */
ACT_DISPLAY_ALARM_OFF,
ACT_DISPLAY_BUTTON_HINT,
ACT_DISPLAY_BUTTON_HINT_AGENT_OFF,
ACT_SET_NIGHT_MODE, /* payload: bool on */
ACT_SET_NIGHT_OVERRIDE, /* payload: int8_t override */
ACT_DRAW_MINIMAL_CLOCK,
ACT_DRAW_WEATHER,
/* 网络 */
ACT_RUN_PROVISIONING, /* 阻塞, 完成后回 PROV_OK/PROV_FAIL */
ACT_WIFI_ENSURE_NETIF,
ACT_WIFI_INIT_STA,
ACT_WIFI_STA_ENSURE,
ACT_WIFI_RECONNECT,
ACT_NET_AUTO_CONNECT, /* 闹钟唤醒专用的强制 wifi 起步 */
ACT_NVS_ERASE_OLD_CREDS, /* 自愈:擦掉错密码 */
ACT_NTP_START,
ACT_NTP_BLOCK_SYNC, /* 阻塞 3s 等 SNTP 第一个响应 */
/* 音频 */
ACT_AUDIO_INIT,
ACT_AUDIO_DEINIT,
ACT_AUDIO_PLAY_URL,
ACT_AUDIO_STOP,
ACT_AUDIO_AUTO_PLAY, /* 闹钟唤醒专用:跳过 agent 标志 */
ACT_FETCH_API, /* audio_fetch_api() */
/* 系统 */
ACT_GPIO_HOLD, /* 持 pin 状态进入 deep sleep */
ACT_TIMER_SET, /* esp_sleep_enable_timer_wakeup */
ACT_DEEP_SLEEP, /* esp_deep_sleep_start() */
ACT_NVS_ERASE, /* factory reset */
ACT_FACTORY_RESET, /* NVS_ERASE + reboot */
/* 维护 */
ACT_LOG_HEAP,
ACT_REFRESH_DISPLAY, /* lvgl_adapter_refr_now */
ACT_INDOOR_READ, /* shtc3_read */
ACT_SYNC_PCF_FROM_SYSTEM, /* pcf85063_sync_from_system */
ACT_APPLY_WEATHER, /* screens_set_weather_data_ptr */
} app_action_kind_t;

/* ── 动作 + payload ──────────────────────────────────────────────────────── */
typedef struct {
app_action_kind_t kind;
union {
struct { const char *name; } station;
struct { bool on; } indicator;
struct { int hour, minute; } alarm_time;
struct { float temp_c, humidity; } indoor;
struct { int8_t override; } night_override;
struct { bool on; } night;
struct { bool reconnect; } audio_start;
} u;
} fsm_action_t;

#define FSM_ACTIONS_MAX 8

typedef struct {
fsm_action_t items[FSM_ACTIONS_MAX];
uint8_t count;
} fsm_actions_t;

/* ── 输入上下文 (executor 每 tick 组装后传给所有 region step) ──────────── */
typedef struct {
/* 静态:boot 时一次性填入 */
bool has_creds;
bool agent_enabled;
bool weekend_skip;
wake_kind_t wake_kind; /* 仅 boot 第一次使用,后续由 wake 状态覆盖 */

/* 动态:executor 每 tick 从各 region 状态 / 硬件读出 */
bool net_connected;
bool audio_url_set;
bool alarm_valid;
bool alarm_disabled;
bool night_now; /* clock_screen_is_night_time() 由 executor 读 */
app_audio_player_evt_t last_audio_event; /* 由 callback 线程原子写入 */
uint32_t pending_ticks;
uint32_t net_connect_ticks; /* net_fsm CONNECTING 状态下的秒数 */
uint8_t stall_ticks;
bool first_advance_synced;
uint8_t alarm_ring_minutes; /* 自 ALARM_RINGING 起累计分钟 */

/* 当周是否周末 (Sat/Sun),仅 boot 一次 */
bool is_saturday;
bool is_sunday;
} app_input_t;

/* ── 复合应用状态:5 个 region 状态正交组合 ────────────────────────────────
* 字段类型为 int(不是强 enum),因为 C 不允许 incomplete enum 作为结构体
* 字段;region .h 中的强类型 enum 在赋值时通过隐式 int 转换互通。 */
typedef struct {
int wake; /* wake_state_t : WAKE_DORMANT / FROM_BTN / FROM_RTC / FROM_SYS / ALARM_RINGING / GOTO_SLEEP */
int sys; /* sys_state_t : SYS_BOOT / SYS_NORMAL / SYS_SLEEPING */
int net; /* net_state_t : NET_OFFLINE / PROVISIONING / CONNECTING / CONNECTED / FAILED */
int audio; /* audio_state_t : AUDIO_IDLE / PENDING / INIT / PLAYING / STOPPING / ERROR */
int display; /* display_state_t : DISP_DAY / NIGHT_AUTO / NIGHT_FORCED */
} app_state_t;

/* ── 应用层原始事件 (由主循环 drain 后喂给 route_event) ───────────────── */
/* 注意:这些是 router 的输入,各 region 收到的子事件是它自己的 * 开头枚举。 */
typedef enum {
EVT_NONE = 0,
/* 唤醒检测 */
EVT_BOOT_DONE, /* main.c 第一次进入主循环时发出 */
EVT_WAKE_DETECT, /* 一次性,触发 wake_fsm 决定从哪种来源唤醒 */
/* 定时 */
EVT_TICK_1HZ,
EVT_TICK_60S,
/* 按钮(位图,可在同一 tick 内 fan-out) */
EVT_BTN_SLEEP_PRESS, /* 右键短按 */
EVT_BTN_NIGHT_TOGGLE, /* 右键长按 */
EVT_BTN_PROVISION_REQUEST, /* 右键三击 */
EVT_BTN_AUDIO_TOGGLE, /* 左键短按 */
EVT_BTN_NTP_SYNC, /* 左键短按 (agent-off) */
EVT_BTN_NEXT_TRACK, /* 左键长按 */
EVT_BTN_LEFT_TOGGLE, /* 闹钟响铃时关掉 */
/* WiFi */
EVT_WIFI_STA_CONNECTED,
EVT_WIFI_IP_GOT,
EVT_WIFI_DISCONNECTED,
EVT_WIFI_TIMEOUT,
/* 配网 */
EVT_PROVISION_OK,
EVT_PROVISION_FAIL,
/* 音频播放器回调 */
EVT_AUDIO_PLAYER_PLAYING,
EVT_AUDIO_PLAYER_IDLE,
EVT_AUDIO_PLAYER_NEXT,
EVT_AUDIO_PLAYER_ERROR,
/* 唤醒完成 (ALARM_COMPLETE) */
EVT_ALARM_COMPLETE,
/* 系统 */
EVT_DEEP_SLEEP_TICK, /* wake_fsm 通告 sys_fsm 进入 deep sleep */
} app_event_t;

/* Convenience: 给 region step 返回的"该 region 不关心这个原始事件"语义。 */
#define REGION_EVT_NONE_FOR(cur, none_evt) (*(cur) == *(cur) ? (none_evt) : (none_evt))

#ifdef __cplusplus
}
#endif

#endif /* APP_FSM_H */
Loading