962 lines
37 KiB
C
962 lines
37 KiB
C
#include "app_manager.h"
|
||
#include "rgb.h"
|
||
#include "event.h"
|
||
#include "database.h"
|
||
#include "ui.h"
|
||
#include "system.h"
|
||
#include "../Drivers/rf.h"
|
||
#include "../Drivers/lcd.h"
|
||
|
||
// 定义单个应用轮询 onRun 的最大允许 CPU 占用 Tick 数 (5 毫秒)
|
||
#define APP_ONRUN_MAX_TICKS 5
|
||
|
||
// 定义局部静态应用实例注册表,大小为 5 (即 APP_ID_MAX)
|
||
static WristbandApp app_registry[APP_ID_MAX];
|
||
// 指向当前活跃的前台应用程序的指针
|
||
static WristbandApp *active_app = NULL;
|
||
// 默认启动加载的应用:待机时钟页
|
||
#define DEFAULT_APP_ID APP_ID_CLOCK
|
||
|
||
// 二级传感器对码菜单中,各类传感器的法语显示名称定义
|
||
char *code menu_items[5] = {
|
||
"DETEC. PORTE", // 0: 门磁探测器 (法语)
|
||
"DETEC. PIR", // 1: 红外探测器
|
||
"DETEC. FUMEE", // 2: 烟雾传感器
|
||
"DETEC. GAZ", // 3: 燃气探测器
|
||
"DETEC. EAU" // 4: 水浸探测器
|
||
};
|
||
|
||
/* ====== 内部应用程序回调函数的前置声明 ====== */
|
||
// ClockApp (待机时钟应用) 生命周期函数
|
||
void ClockApp_OnStart(void);
|
||
void ClockApp_onRun(void);
|
||
void ClockApp_onClose(void);
|
||
EventResult ClockApp_onEvent(SystemEvent *evt);
|
||
|
||
// MenuApp (菜单选择应用) 生命周期函数
|
||
void MenuApp_OnStart(void);
|
||
void MenuApp_onRun(void);
|
||
void MenuApp_onClose(void);
|
||
EventResult MenuApp_onEvent(SystemEvent *evt);
|
||
|
||
// PairApp (无线传感器对码学习应用) 生命周期函数
|
||
void PairApp_OnStart(void);
|
||
void PairApp_onRun(void);
|
||
void PairApp_onClose(void);
|
||
EventResult PairApp_onEvent(SystemEvent *evt);
|
||
|
||
// AlarmApp (传感器报警显示与振动反馈应用) 生命周期函数
|
||
void AlarmApp_OnStart(void);
|
||
void AlarmApp_onRun(void);
|
||
void AlarmApp_onClose(void);
|
||
EventResult AlarmApp_onEvent(SystemEvent *evt);
|
||
|
||
// SOSApp (SOS 紧急求救无线发射与警灯应用) 生命周期函数
|
||
void SOSApp_OnStart(void);
|
||
void SOSApp_onRun(void);
|
||
void SOSApp_onClose(void);
|
||
EventResult SOSApp_onEvent(SystemEvent *evt);
|
||
|
||
|
||
/* ====== AppManager 核心接口函数实现 ====== */
|
||
|
||
/**
|
||
* @brief 初始化应用管理器并注册所有系统支持的应用
|
||
*/
|
||
void AppManager_Init(void)
|
||
{
|
||
u8 i;
|
||
/* 1. 清空初始化注册表 */
|
||
for (i = 0; i < APP_ID_MAX; i++) {
|
||
app_registry[i].app_id = APP_ID_MAX;
|
||
app_registry[i].app_type = APP_TYPE_FOREGROUND;
|
||
app_registry[i].app_name = "";
|
||
app_registry[i].OnStart = NULL;
|
||
app_registry[i].onRun = NULL;
|
||
app_registry[i].onClose = NULL;
|
||
app_registry[i].onEvent = NULL;
|
||
app_registry[i].is_active = 0;
|
||
app_registry[i].is_running = 0;
|
||
}
|
||
|
||
/* 2. 注册待机时钟应用 ClockApp */
|
||
app_registry[APP_ID_CLOCK].app_id = APP_ID_CLOCK;
|
||
app_registry[APP_ID_CLOCK].app_type = APP_TYPE_FOREGROUND;
|
||
app_registry[APP_ID_CLOCK].app_name = "Clock";
|
||
app_registry[APP_ID_CLOCK].OnStart = ClockApp_OnStart;
|
||
app_registry[APP_ID_CLOCK].onRun = ClockApp_onRun;
|
||
app_registry[APP_ID_CLOCK].onClose = ClockApp_onClose;
|
||
app_registry[APP_ID_CLOCK].onEvent = ClockApp_onEvent;
|
||
|
||
/* 3. 注册系统设置主菜单应用 MenuApp */
|
||
app_registry[APP_ID_MENU].app_id = APP_ID_MENU;
|
||
app_registry[APP_ID_MENU].app_type = APP_TYPE_FOREGROUND;
|
||
app_registry[APP_ID_MENU].app_name = "Menu";
|
||
app_registry[APP_ID_MENU].OnStart = MenuApp_OnStart;
|
||
app_registry[APP_ID_MENU].onRun = MenuApp_onRun;
|
||
app_registry[APP_ID_MENU].onClose = MenuApp_onClose;
|
||
app_registry[APP_ID_MENU].onEvent = MenuApp_onEvent;
|
||
|
||
/* 4. 注册无线对码配对应用 PairApp */
|
||
app_registry[APP_ID_PAIR].app_id = APP_ID_PAIR;
|
||
app_registry[APP_ID_PAIR].app_type = APP_TYPE_FOREGROUND;
|
||
app_registry[APP_ID_PAIR].app_name = "Pair";
|
||
app_registry[APP_ID_PAIR].OnStart = PairApp_OnStart;
|
||
app_registry[APP_ID_PAIR].onRun = PairApp_onRun;
|
||
app_registry[APP_ID_PAIR].onClose = PairApp_onClose;
|
||
app_registry[APP_ID_PAIR].onEvent = PairApp_onEvent;
|
||
|
||
/* 5. 注册防区入侵警报应用 AlarmApp */
|
||
app_registry[APP_ID_ALARM].app_id = APP_ID_ALARM;
|
||
app_registry[APP_ID_ALARM].app_type = APP_TYPE_FOREGROUND;
|
||
app_registry[APP_ID_ALARM].app_name = "Alarm";
|
||
app_registry[APP_ID_ALARM].OnStart = AlarmApp_OnStart;
|
||
app_registry[APP_ID_ALARM].onRun = AlarmApp_onRun;
|
||
app_registry[APP_ID_ALARM].onClose = AlarmApp_onClose;
|
||
app_registry[APP_ID_ALARM].onEvent = AlarmApp_onEvent;
|
||
|
||
/* 6. 注册 SOS 紧急求救应用 SOSApp */
|
||
app_registry[APP_ID_SOS].app_id = APP_ID_SOS;
|
||
app_registry[APP_ID_SOS].app_type = APP_TYPE_FOREGROUND;
|
||
app_registry[APP_ID_SOS].app_name = "SOS";
|
||
app_registry[APP_ID_SOS].OnStart = SOSApp_OnStart;
|
||
app_registry[APP_ID_SOS].onRun = SOSApp_onRun;
|
||
app_registry[APP_ID_SOS].onClose = SOSApp_onClose;
|
||
app_registry[APP_ID_SOS].onEvent = SOSApp_onEvent;
|
||
|
||
/* 默认运行启动待机时钟应用 */
|
||
AppManager_StartApp(DEFAULT_APP_ID);
|
||
}
|
||
|
||
/**
|
||
* @brief 切换并启动目标前台应用程序 (切应用生命周期)
|
||
* @param app_id 目标加载的应用 ID
|
||
*/
|
||
void AppManager_StartApp(AppID app_id)
|
||
{
|
||
WristbandApp *target;
|
||
if (app_id >= APP_ID_MAX) return; // 越界防护
|
||
target = &app_registry[app_id];
|
||
if (target->OnStart == NULL) return; // 空指针防护
|
||
if (active_app == target) return; // 避免重复启动同一个应用造成页面重画
|
||
|
||
Uart_SendString("[APP] StartApp: ");
|
||
Uart_SendString(target->app_name);
|
||
Uart_SendString("\r\n");
|
||
|
||
/* 1. 关闭清除当前活跃运行的应用,释放其外设和标志 */
|
||
if (active_app != NULL && active_app->onClose != NULL) {
|
||
active_app->is_active = 0; // 标明不再处于前台活跃状态
|
||
active_app->onClose(); // 调用该应用的退出现场清理回调
|
||
}
|
||
|
||
/* 2. 切换当前前台活跃应用指针,启动新加载的应用 */
|
||
active_app = target;
|
||
if (active_app->OnStart != NULL) {
|
||
active_app->is_active = 1; // 标明进入活跃状态
|
||
active_app->is_running = 1; // 标记处于运行状态,开启 onRun 调用
|
||
active_app->OnStart(); // 调用启动回调,初始化显示 UI 与外设
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 将捕获的系统事件分发给当前活跃的前台应用
|
||
* @param evt 待分发的系统事件
|
||
*/
|
||
void AppManager_DispatchEvent(SystemEvent evt)
|
||
{
|
||
EventResult result = EVENT_IGNORED;
|
||
if (active_app != NULL && active_app->is_active && active_app->onEvent != NULL) {
|
||
// 调用活跃应用的事件处理函数
|
||
result = active_app->onEvent(&evt);
|
||
if (result == EVENT_HANDLED) return; // 被前台消耗掉,结束分发
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 在主循环中持续轮询当前活跃应用的 onRun
|
||
* @details 包含 CPU 占用时间检测算法,避免坏设计的 onRun 回调死循环卡死系统。
|
||
*/
|
||
void AppManager_RunActiveApp(void)
|
||
{
|
||
u16 start_tick;
|
||
u16 elapsed;
|
||
if (active_app == NULL || !active_app->is_active) return;
|
||
|
||
// CPU 挂起恢复机制:如果在上一次循环中因超时被强行挂起 (is_running=0)
|
||
// 则在本次循环中将 is_running 标志重新置 1 恢复,但直接返回,跳过本次执行,把 CPU 使用权让给其他底层服务
|
||
if (!active_app->is_running) {
|
||
active_app->is_running = 1;
|
||
return;
|
||
}
|
||
|
||
if (active_app->onRun == NULL) return;
|
||
|
||
start_tick = ms_tick; // 记录开始前的毫秒 Tick 数 (0~999 循环)
|
||
active_app->onRun(); // 执行应用的轮询代码
|
||
|
||
// 计算本次 onRun 调用的真实耗时 (考虑 ms_tick 模 1000 归零的情况)
|
||
if (ms_tick >= start_tick) {
|
||
elapsed = ms_tick - start_tick;
|
||
} else {
|
||
elapsed = (1000 + ms_tick) - start_tick;
|
||
}
|
||
|
||
// 超时检测:如果单次 onRun 运行时间超过了 APP_ONRUN_MAX_TICKS (5毫秒)
|
||
if (elapsed > APP_ONRUN_MAX_TICKS) {
|
||
active_app->is_running = 0; // 强行挂起,下次循环跳过该应用 onRun 以释放 CPU
|
||
} else {
|
||
active_app->is_running = 1; // 正常,继续运行
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 获取当前正在活跃的前台应用 ID
|
||
*/
|
||
AppID AppManager_GetActiveAppID(void)
|
||
{
|
||
if (active_app == NULL) return APP_ID_MAX;
|
||
return active_app->app_id;
|
||
}
|
||
|
||
|
||
/* =========================================================================
|
||
* 1. ClockApp (待机时钟应用) 具体生命周期回调实现
|
||
* ========================================================================= */
|
||
|
||
// 编辑时间参数所用到的私有局部变量
|
||
static u8 time_edit_active = 0; // 标识当前是否正在编辑修改时间中 (1:是, 0:否)
|
||
static u8 time_edit_field = 1; // 标识当前聚焦在哪个参数上进行微调 (1:修改小时, 2:修改分钟)
|
||
static u8 time_edit_blink_on = 1; // 时钟修改时光标闪烁周期状态位
|
||
|
||
/**
|
||
* @brief 触发启动时间设置状态机,准备修改时钟
|
||
*/
|
||
void Start_Time_Edit(void)
|
||
{
|
||
time_edit_active = 1;
|
||
time_edit_field = 1;
|
||
time_edit_blink_on = 1;
|
||
current_state = STATE_SET_TIME; // 系统切入时间设置状态
|
||
|
||
// FCOB 幻彩灯条双灯珠均显示蓝色常亮 (R=0, G=0, B=180),进行模式视觉区分
|
||
RGB_Send(0, 0, 180, 0, 0, 180);
|
||
|
||
// 振动马达单次短震 50 毫秒,提供触觉反馈
|
||
MOTOR = 1;
|
||
Delay_ms(50);
|
||
MOTOR = 0;
|
||
|
||
// 渲染显示时间编辑界面,默认高亮编辑小时字段,消隐位开启
|
||
UI_ShowSetTimePage(current_hour, current_min, time_edit_field, 1);
|
||
}
|
||
|
||
/**
|
||
* @brief ClockApp 启动回调
|
||
*/
|
||
void ClockApp_OnStart(void)
|
||
{
|
||
time_edit_active = 0;
|
||
current_state = STATE_NORMAL; // 系统切入正常待机状态
|
||
RF_SetMode(1); // 开启天线并置为接收模式,监听无线探测器
|
||
UI_ShowClockPage(current_state, current_hour, current_min); // 渲染时钟主页
|
||
|
||
// 关断/熄灭 FCOB 双灯条
|
||
RGB_Send(0, 0, 0, 0, 0, 0);
|
||
|
||
Uart_SendString("[ClockApp] Started\r\n");
|
||
}
|
||
|
||
/**
|
||
* @brief ClockApp 轮询主回调
|
||
* @details 负责处理两个非阻塞式周期性事务:
|
||
* 1. 若正在时间修改编辑模式中,基于 ms_tick 提供 500ms 周期的被改参数隐现闪烁效果。
|
||
* 2. 在待机主页常态下,非阻塞轮询射频解调接收缓冲区,识别到无线信号后在数据库中匹配,并实现防区布撤防规则过滤。
|
||
*/
|
||
void ClockApp_onRun(void)
|
||
{
|
||
u32 rx_addr;
|
||
u8 rx_data;
|
||
|
||
if (time_edit_active)
|
||
{
|
||
// ====== 时间设置闪烁动画 (周期 500ms) ======
|
||
u8 blink = (ms_tick / 500) % 2;
|
||
if (blink != time_edit_blink_on)
|
||
{
|
||
time_edit_blink_on = blink;
|
||
// 每次闪烁状态发生变化,重新刷写屏幕数值
|
||
UI_ShowSetTimePage(current_hour, current_min, time_edit_field, time_edit_blink_on);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// ====== 常态待机下的射频检测与触发过滤逻辑 ======
|
||
|
||
// 软时钟发生分钟改变,需要通知屏幕重画时钟主页
|
||
if (clock_updated) {
|
||
clock_updated = 0;
|
||
UI_ShowClockPage(current_state, current_hour, current_min);
|
||
}
|
||
|
||
// 轮询无线 EV1527 硬件解调器是否收到一个完整的 24 位无线码
|
||
if (EV1527_Decode(&rx_addr, &rx_data)) {
|
||
u8 slot;
|
||
// 比对收到的 24 位射频源地址是否合法注册在手环本地配置数据库中
|
||
if (Check_Sensor_ID(rx_addr, &slot)) {
|
||
u8 sensor_type = sensor_list[slot].type; // 获取注册的设备类型
|
||
if (sensor_type < 5) {
|
||
// 【防区分类过滤与布撤防过滤逻辑】:
|
||
// - 0号防区,或者烟感(2)、气感(3)、水浸(4) 为 24小时紧急防区,不受当前布防状态影响,一律强制触发警报;
|
||
// - 门磁(0)和红外(1)为普通边界入侵防区,只有当系统处于 STATE_ARMED (布防) 状态时,才触发入侵警报。
|
||
if (sensor_list[slot].zone == 0 ||
|
||
sensor_type == 2 ||
|
||
sensor_type == 3 ||
|
||
sensor_type == 4 ||
|
||
current_state == STATE_ARMED)
|
||
{
|
||
SystemEvent rf_evt;
|
||
rf_evt.key_event = KEY_EVENT_NONE;
|
||
rf_evt.priority = EVENT_PRIORITY_HIGH; // 将事件定为 HIGH (高优先级)
|
||
rf_evt.extra_data = rx_addr; // 携带报警源地址
|
||
rf_evt.extra_size = 4;
|
||
|
||
// 高优先级事件,强行插队到队首优先执行以提高响应速率
|
||
EventQueue_InsertFront(rf_evt);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief ClockApp 退出清理回调
|
||
*/
|
||
void ClockApp_onClose(void)
|
||
{
|
||
time_edit_active = 0;
|
||
RGB_Send(0, 0, 0, 0, 0, 0); // 关闭灯条
|
||
Uart_SendString("[ClockApp] Closed\r\n");
|
||
}
|
||
|
||
/**
|
||
* @brief ClockApp 接收事件处理回调 (按键导航与布撤防交互)
|
||
*/
|
||
EventResult ClockApp_onEvent(SystemEvent *evt)
|
||
{
|
||
if (time_edit_active)
|
||
{
|
||
// ====== 处于时间设置状态下的按键拦截处理机 ======
|
||
if (evt->key_event == KEY_EVENT_UP_CLICK)
|
||
{
|
||
// 短按 ▲ 键增加数值 (小时/分钟循环相加)
|
||
if (time_edit_field == 1) {
|
||
current_hour = (current_hour + 1) % 24;
|
||
} else {
|
||
current_min = (current_min + 1) % 60;
|
||
}
|
||
UI_ShowSetTimePage(current_hour, current_min, time_edit_field, 1);
|
||
return EVENT_HANDLED; // 吞掉并消化事件
|
||
}
|
||
if (evt->key_event == KEY_EVENT_DOWN_CLICK)
|
||
{
|
||
// 短按 ▼ 键减小数值
|
||
if (time_edit_field == 1) {
|
||
current_hour = (current_hour == 0) ? 23 : (current_hour - 1);
|
||
} else {
|
||
current_min = (current_min == 0) ? 59 : (current_min - 1);
|
||
}
|
||
UI_ShowSetTimePage(current_hour, current_min, time_edit_field, 1);
|
||
return EVENT_HANDLED;
|
||
}
|
||
if (evt->key_event == KEY_EVENT_CONFIRM_LONG)
|
||
{
|
||
// 长按 ■ 确认键切换调节字段 (在修改小时与修改分钟间跳转)
|
||
if (time_edit_field == 1) {
|
||
time_edit_field = 2;
|
||
} else {
|
||
time_edit_field = 1;
|
||
}
|
||
UI_ShowSetTimePage(current_hour, current_min, time_edit_field, 1);
|
||
return EVENT_HANDLED;
|
||
}
|
||
if (evt->key_event == KEY_EVENT_CONFIRM_CLICK)
|
||
{
|
||
// 短按 ■ 确认键保存最新时钟配置并退出编辑模式
|
||
time_edit_active = 0;
|
||
current_state = STATE_NORMAL;
|
||
RGB_Send(0, 0, 0, 0, 0, 0); // 熄灭蓝色指示灯
|
||
|
||
// 退出短振反馈
|
||
MOTOR = 1; Delay_ms(50); MOTOR = 0;
|
||
|
||
UI_ShowClockPage(current_state, current_hour, current_min);
|
||
return EVENT_HANDLED;
|
||
}
|
||
return EVENT_HANDLED; // 在时间设置模式下屏蔽拦截其他一切无关按键事件
|
||
}
|
||
|
||
// ====== 正常时钟对待机状态下的按键拦截处理 ======
|
||
|
||
// 1. 长按 ■ 确认键,打开设置主菜单 MenuApp
|
||
if (evt->key_event == KEY_EVENT_CONFIRM_LONG) {
|
||
AppManager_StartApp(APP_ID_MENU);
|
||
return EVENT_HANDLED;
|
||
}
|
||
|
||
// 2. 长按 ▲ 键,一键切换系统的 布防 (ARMED) 和 撤防 (DISARMED) 状态
|
||
if (evt->key_event == KEY_EVENT_UP_LONG) {
|
||
if (current_state == STATE_ARMED) {
|
||
current_state = STATE_DISARMED;
|
||
} else {
|
||
current_state = STATE_ARMED;
|
||
}
|
||
// 布撤防状态切换成功后进行 100ms 短震确认,并重新刷写时钟待机页
|
||
MOTOR = 1; Delay_ms(100); MOTOR = 0;
|
||
UI_ShowClockPage(current_state, current_hour, current_min);
|
||
return EVENT_HANDLED;
|
||
}
|
||
|
||
// 3. 遇到 SOS 物理求救按键(无论短按还是长按),直接进入 SOSAPP
|
||
if (evt->key_event == KEY_EVENT_SOS_CLICK || evt->key_event == KEY_EVENT_SOS_LONG) {
|
||
AppManager_StartApp(APP_ID_SOS);
|
||
return EVENT_HANDLED;
|
||
}
|
||
|
||
return EVENT_IGNORED; // 其他未被关心的事件交由后台应用或丢弃
|
||
}
|
||
|
||
|
||
/* =========================================================================
|
||
* 2. MenuApp (菜单选择应用) 具体生命周期回调实现
|
||
* ========================================================================= */
|
||
|
||
// menu_level: 0-主菜单, 1-选择传感器二级对码菜单, 2-网络摄像机(IPC)一键绑定发射中
|
||
static u8 menu_level = 0;
|
||
// 记录上一次进行 IPC 绑定无线射频发射的时间 Tick 数,实现周期性发射
|
||
static u16 last_bind_tx_ms = 0;
|
||
|
||
/**
|
||
* @brief MenuApp 启动回调
|
||
*/
|
||
void MenuApp_OnStart(void)
|
||
{
|
||
menu_level = 0;
|
||
menu_select = 0;
|
||
current_state = STATE_PAIR_MENU; // 系统运行状态设为对码配置菜单
|
||
RF_SetMode(0); // 关闭射频接收芯片,防止接收杂波干扰屏幕绘制
|
||
UI_ShowMainMenu(menu_select); // 初始化绘制一级主菜单
|
||
RGB_Send(0, 0, 0, 0, 0, 0); // 熄灭所有灯条
|
||
Uart_SendString("[MenuApp] Started (Main Menu)\r\n");
|
||
}
|
||
|
||
/**
|
||
* @brief MenuApp 轮询主回调
|
||
* @details 当且仅当进入 IPC 绑定状态下 (menu_level == 2) 时运行。
|
||
* - 在 FCOB 灯条上产生青色 (R=0, G=val, B=val) 的非阻塞呼吸效果以暗示绑定进程。
|
||
* - 控制无线发射引脚,每隔 500ms 向外广播一次克隆的 EV1527 24位出厂物理 ID 的绑定对码帧。
|
||
*/
|
||
void MenuApp_onRun(void)
|
||
{
|
||
if (menu_level == 2)
|
||
{
|
||
// ====== 控制青色双灯珠同步呼吸效果 (呼吸周期 1000ms) ======
|
||
u16 breath = ms_tick % 1000;
|
||
u8 val = (breath < 500) ? (breath * 2 / 5) : ((1000 - breath) * 2 / 5); // 将 0~500 范围比例映射为 0~200 亮度
|
||
RGB_Send(0, val, val, 0, val, val); // 渲染青色 (G+B)
|
||
|
||
// ====== 控制无线发射,每 500ms 广播一次配对数据 ======
|
||
if (ms_tick - last_bind_tx_ms >= 500 || ms_tick < last_bind_tx_ms)
|
||
{
|
||
last_bind_tx_ms = ms_tick; // 备份当前 Tick 时间
|
||
RF_SetMode(2); // 暂时切换射频芯片为发射调制模式
|
||
EV1527_Transmit(CLONED_ADDR, 0x01); // 调制发射手环的 24 位物理 ID + 绑定特定数据码 (0x01)
|
||
RF_SetMode(0); // 切回关闭/高阻状态,避免噪声产生
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief MenuApp 退出清理回调
|
||
*/
|
||
void MenuApp_onClose(void)
|
||
{
|
||
menu_level = 0;
|
||
RGB_Send(0, 0, 0, 0, 0, 0); // 熄灭 RGB
|
||
RF_SetMode(1); // 重新开启无线接收模式
|
||
Uart_SendString("[MenuApp] Closed\r\n");
|
||
}
|
||
|
||
/**
|
||
* @brief MenuApp 接收按键与控制事件回调
|
||
*/
|
||
EventResult MenuApp_onEvent(SystemEvent *evt)
|
||
{
|
||
if (menu_level == 0)
|
||
{
|
||
// ====== 处于一级主菜单层 (0-HORLOGE, 1-APPAIRAGE, 2-LIAISON) ======
|
||
if (evt->key_event == KEY_EVENT_UP_CLICK) {
|
||
// 短按 ▲ 向上移动选中焦点
|
||
if (menu_select > 0) {
|
||
menu_select--;
|
||
UI_ShowMainMenu(menu_select); // 重新画屏
|
||
}
|
||
return EVENT_HANDLED;
|
||
}
|
||
if (evt->key_event == KEY_EVENT_DOWN_CLICK) {
|
||
// 短按 ▼ 向下移动选中焦点
|
||
if (menu_select < 2) {
|
||
menu_select++;
|
||
UI_ShowMainMenu(menu_select);
|
||
}
|
||
return EVENT_HANDLED;
|
||
}
|
||
if (evt->key_event == KEY_EVENT_CONFIRM_CLICK) {
|
||
// 短按 ■ 确认键,进入选定功能的设置页面或二级菜单
|
||
if (menu_select == 0) {
|
||
// 功能 1:设置时间。启动待机时钟应用,并调用微调编辑入口
|
||
AppManager_StartApp(APP_ID_CLOCK);
|
||
Start_Time_Edit();
|
||
} else if (menu_select == 1) {
|
||
// 功能 2:传感器学习。进入二级传感器类别选择菜单
|
||
menu_level = 1;
|
||
menu_select = 0;
|
||
UI_ShowPairMenuPage(menu_select); // 绘制二级设备分类页
|
||
} else if (menu_select == 2) {
|
||
// 功能 3:摄像机配对。进入摄像机绑定状态
|
||
menu_level = 2;
|
||
last_bind_tx_ms = ms_tick; // 记录初始发射时间
|
||
current_state = STATE_IPC_BIND; // 系统设为 IPC 绑定状态
|
||
UI_ShowBindingPage(CLONED_ADDR); // 绘制摄像机绑定信息页
|
||
}
|
||
return EVENT_HANDLED;
|
||
}
|
||
if (evt->key_event == KEY_EVENT_CONFIRM_LONG) {
|
||
// 长按 ■ 确认键,退出设置菜单直接返还待机时钟
|
||
AppManager_StartApp(APP_ID_CLOCK);
|
||
return EVENT_HANDLED;
|
||
}
|
||
}
|
||
else if (menu_level == 1)
|
||
{
|
||
// ====== 处于二级传感器对码类型菜单层 (0-门磁, 1-PIR, 2-烟感, 3-气感, 4-水浸) ======
|
||
if (evt->key_event == KEY_EVENT_UP_CLICK) {
|
||
// 上移焦点
|
||
if (menu_select > 0) {
|
||
menu_select--;
|
||
UI_ShowPairMenuPage(menu_select);
|
||
}
|
||
return EVENT_HANDLED;
|
||
}
|
||
if (evt->key_event == KEY_EVENT_DOWN_CLICK) {
|
||
// 下移焦点
|
||
if (menu_select < 4) {
|
||
menu_select++;
|
||
UI_ShowPairMenuPage(menu_select);
|
||
}
|
||
return EVENT_HANDLED;
|
||
}
|
||
if (evt->key_event == KEY_EVENT_CONFIRM_CLICK) {
|
||
// 确认选择:缓存用户选好的待绑定硬件类型,短震 100ms 反馈,并加载对码学习应用 (PairApp)
|
||
captured_type = menu_select;
|
||
MOTOR = 1;
|
||
Delay_ms(100);
|
||
MOTOR = 0;
|
||
AppManager_StartApp(APP_ID_PAIR); // 跳转加载 PairApp
|
||
return EVENT_HANDLED;
|
||
}
|
||
if (evt->key_event == KEY_EVENT_DOWN_LONG) {
|
||
// 长按 ▼ 键,从二级菜单退回到一级主菜单
|
||
menu_level = 0;
|
||
menu_select = 1; // 回到 APPAIRAGE 选项上
|
||
UI_ShowMainMenu(menu_select);
|
||
return EVENT_HANDLED;
|
||
}
|
||
}
|
||
else if (menu_level == 2)
|
||
{
|
||
// ====== 处于摄像机配对一键绑定发送层 ======
|
||
// 长按 ■ 确认键 (3秒) 或 长按 ▼ 键 (2秒) 均表示取消绑定,退出发射并返回一级主菜单
|
||
if (evt->key_event == KEY_EVENT_CONFIRM_LONG || evt->key_event == KEY_EVENT_DOWN_LONG) {
|
||
menu_level = 0;
|
||
menu_select = 2; // 回到 LIAISON 选项上
|
||
current_state = STATE_PAIR_MENU;
|
||
RGB_Send(0, 0, 0, 0, 0, 0); // 关闭呼吸灯
|
||
UI_ShowMainMenu(menu_select);
|
||
return EVENT_HANDLED;
|
||
}
|
||
}
|
||
return EVENT_IGNORED;
|
||
}
|
||
|
||
|
||
/* =========================================================================
|
||
* 3. PairApp (无线传感器对码学习应用) 具体生命周期回调实现
|
||
* ========================================================================= */
|
||
|
||
// PairApp 私有状态状态机机定义
|
||
enum {
|
||
PAIR_STATE_WAIT, // 等待信号输入 (雷达搜寻画面)
|
||
PAIR_STATE_CONFIRM, // 已捕获,防区选择与微调确认画面
|
||
PAIR_STATE_SUCCESS, // 绑定成功,显示 SUCCESS 对勾页
|
||
PAIR_STATE_FAIL // 超时或失败,显示 FAIL 叉号页
|
||
};
|
||
|
||
static u8 pair_state; // 当前的对码阶段状态
|
||
static u8 last_radar_sec; // 上一次雷达页面帧索引备份
|
||
static u8 captured_zone = 1; // 当前配置准备分配给该传感器的防区号 (1~99)
|
||
static u8 success_start_sec = 0; // 记录对码成功时的开始秒数,以便延迟退出
|
||
static u8 fail_start_sec = 0; // 记录对码失败时的开始秒数
|
||
|
||
/**
|
||
* @brief PairApp 启动回调
|
||
*/
|
||
void PairApp_OnStart(void)
|
||
{
|
||
pair_state = PAIR_STATE_WAIT; // 初始化设为等待无线探测器状态
|
||
last_radar_sec = 99; // 强制刷写
|
||
current_state = STATE_PAIR_WAIT;
|
||
pair_timeout_ms = 0; // 对码超时毫秒计时清零
|
||
captured_zone = 1; // 默认防区分配号为 1
|
||
RF_SetMode(1); // 开启无线接收
|
||
|
||
// 初始化对码搜索页面绘制
|
||
UI_ShowPairWaitPage(0);
|
||
|
||
// 开启白色 RGB 幻彩双灯珠常亮 (R=150, G=150, B=150),作为对码标志
|
||
RGB_Send(150, 150, 150, 150, 150, 150);
|
||
|
||
Uart_SendString("[PairApp] Started (WAIT)\r\n");
|
||
}
|
||
|
||
/**
|
||
* @brief PairApp 轮询主回调
|
||
* @details 非阻塞式控制以下配对时序:
|
||
* 1. 处于 PAIR_STATE_WAIT 状态:控制双灯进行白色 3Hz 高频闪烁,控制雷达动画绘制,并进行 30 秒超时判定。
|
||
* 同时监测射频接收,收到无线码后,排除 SOS (0x08) 码,将系统切入防区确认状态。
|
||
* 2. 处于 PAIR_STATE_CONFIRM 状态:控制灯光橙色慢闪。
|
||
* 3. 处于 SUCCESS / FAIL 状态:控制延迟 2秒 或 5秒 自动退出,重新回到二级对码菜单页。
|
||
*/
|
||
void PairApp_onRun(void)
|
||
{
|
||
u32 rx_addr;
|
||
u8 rx_data;
|
||
u8 flash;
|
||
u8 frame;
|
||
|
||
if (pair_state == PAIR_STATE_WAIT) {
|
||
// ====== 等待状态:白色高频快闪 (3Hz,周期 333ms) ======
|
||
flash = (ms_tick / 166) % 2;
|
||
if (flash) RGB_Send(150, 150, 150, 150, 150, 150);
|
||
else RGB_Send(0, 0, 0, 0, 0, 0);
|
||
|
||
// 每隔 250ms,滚动绘制雷达搜索的 3 帧图片,构成扩散环形雷达图
|
||
frame = (u8)((ms_tick / 250) % 3);
|
||
if (frame != last_radar_sec) {
|
||
last_radar_sec = frame;
|
||
UI_ShowPairWaitPage(frame);
|
||
}
|
||
|
||
// ====== 30 秒超时判定 ======
|
||
if (pair_timeout_ms >= 30000UL) {
|
||
pair_timeout_ms = 0;
|
||
pair_state = PAIR_STATE_FAIL; // 切入失败
|
||
current_state = STATE_PAIR_FAIL;
|
||
fail_start_sec = current_sec; // 记录失败时间
|
||
UI_ShowPairFailPage(1); // 绘制超时失败大红叉页
|
||
|
||
// 红色双灯常亮警告
|
||
RGB_Send(200, 0, 0, 200, 0, 0);
|
||
|
||
Uart_SendString("[PairApp] Timeout\r\n");
|
||
return;
|
||
}
|
||
|
||
// ====== 射频包监听接收 ======
|
||
if (EV1527_Decode(&rx_addr, &rx_data)) {
|
||
// 【保护机制】:排除 SOS 数据码 (0x08)。SOS 主动求救的数据格式不能作为普通防区进行学习绑定。
|
||
if (rx_data != 0x08)
|
||
{
|
||
captured_addr = rx_addr; // 锁定射频源地址
|
||
pair_state = PAIR_STATE_CONFIRM; // 进入防区微调确认模式
|
||
current_state = STATE_PAIR_CONFIRM;
|
||
RF_SetMode(0); // 关闭射频接收,防止再次覆盖
|
||
captured_zone = 1; // 默认初始防区为 1
|
||
UI_ShowPairConfirmPage(captured_type, captured_zone); // 刷新界面
|
||
Uart_SendString("[PairApp] Sensor captured\r\n");
|
||
}
|
||
}
|
||
} else if (pair_state == PAIR_STATE_CONFIRM) {
|
||
// ====== 确认状态:灯光橙色慢闪 (1.5Hz,周期 666ms) ======
|
||
u8 flash = (ms_tick / 333) % 2;
|
||
if (flash) RGB_Send(100, 180, 0, 100, 180, 0); // 橙色 (G=180, R=100)
|
||
else RGB_Send(0, 0, 0, 0, 0, 0);
|
||
} else if (pair_state == PAIR_STATE_SUCCESS) {
|
||
// ====== 成功状态:延时 2 秒自动返回二级设置菜单 ======
|
||
u8 elapsed = (current_sec >= success_start_sec) ? (current_sec - success_start_sec) : (60 + current_sec - success_start_sec);
|
||
if (elapsed >= 2)
|
||
{
|
||
AppManager_StartApp(APP_ID_MENU);
|
||
}
|
||
} else if (pair_state == PAIR_STATE_FAIL) {
|
||
// ====== 失败状态:延时 5 秒自动返回二级设置菜单 ======
|
||
u8 elapsed = (current_sec >= fail_start_sec) ? (current_sec - fail_start_sec) : (60 + current_sec - fail_start_sec);
|
||
if (elapsed >= 5)
|
||
{
|
||
AppManager_StartApp(APP_ID_MENU);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief PairApp 退出清理
|
||
*/
|
||
void PairApp_onClose(void)
|
||
{
|
||
RGB_Send(0, 0, 0, 0, 0, 0); // 关灯
|
||
MOTOR = 0; // 停震
|
||
RF_SetMode(1); // 重开射频接收
|
||
Uart_SendString("[PairApp] Closed\r\n");
|
||
}
|
||
|
||
/**
|
||
* @brief PairApp 接收事件处理 (防区微调与对码保存)
|
||
*/
|
||
EventResult PairApp_onEvent(SystemEvent *evt)
|
||
{
|
||
if (pair_state == PAIR_STATE_CONFIRM) {
|
||
if (evt->key_event == KEY_EVENT_UP_CLICK) {
|
||
// 短按 ▲ 键增加目标防区号 (最大 99)
|
||
if (captured_zone < 99) {
|
||
captured_zone++;
|
||
UI_ShowPairConfirmPage(captured_type, captured_zone);
|
||
}
|
||
return EVENT_HANDLED;
|
||
}
|
||
if (evt->key_event == KEY_EVENT_DOWN_CLICK) {
|
||
// 短按 ▼ 键减小目标防区号 (最小 1)
|
||
if (captured_zone > 1) {
|
||
captured_zone--;
|
||
UI_ShowPairConfirmPage(captured_type, captured_zone);
|
||
}
|
||
return EVENT_HANDLED;
|
||
}
|
||
if (evt->key_event == KEY_EVENT_CONFIRM_CLICK) {
|
||
// 短按 ■ 确认键,写入配置数据库并启动保存
|
||
Add_Sensor_With_Zone(captured_addr, captured_type, captured_zone);
|
||
|
||
pair_state = PAIR_STATE_SUCCESS; // 切入成功反馈状态
|
||
current_state = STATE_PAIR_SUCCESS;
|
||
success_start_sec = current_sec; // 记录开始秒数
|
||
UI_ShowPairSuccessPage(); // 绘制成功画面
|
||
|
||
// 绿色双灯常亮,并开启振动马达长震 400ms,暗示配对保存圆满完成
|
||
RGB_Send(0, 200, 0, 0, 200, 0);
|
||
MOTOR = 1;
|
||
Delay_ms(400);
|
||
MOTOR = 0;
|
||
return EVENT_HANDLED;
|
||
}
|
||
if (evt->key_event == KEY_EVENT_CONFIRM_LONG) {
|
||
// 长按 ■ 确认键,放弃本次捕获直接取消配对,回到主菜单
|
||
RGB_Send(0, 0, 0, 0, 0, 0);
|
||
AppManager_StartApp(APP_ID_MENU);
|
||
return EVENT_HANDLED;
|
||
}
|
||
}
|
||
|
||
// 在搜寻期或确认期,长按 ▼ 键皆可强制放弃并退回二级菜单
|
||
if (evt->key_event == KEY_EVENT_DOWN_LONG) {
|
||
RGB_Send(0, 0, 0, 0, 0, 0);
|
||
AppManager_StartApp(APP_ID_MENU);
|
||
return EVENT_HANDLED;
|
||
}
|
||
return EVENT_IGNORED;
|
||
}
|
||
|
||
|
||
/* =========================================================================
|
||
* 4. AlarmApp (传感器报警显示与振动反馈应用) 具体生命周期回调实现
|
||
* ========================================================================= */
|
||
|
||
/**
|
||
* @brief AlarmApp 启动回调
|
||
* @details 判定触发警报的传感器信息,初始化报警 UI,输出对应的声光颜色。
|
||
*/
|
||
void AlarmApp_OnStart(void)
|
||
{
|
||
u8 slot;
|
||
u8 type = 0;
|
||
u8 zone = 1;
|
||
|
||
current_state = STATE_ALARM; // 系统切入报警状态
|
||
alarm_timer_ms = 0; // 振动波形状态机定时器清零
|
||
alarm_flash_flag = 0;
|
||
|
||
// 比对查出当前触发高优先级警报的传感器在数据库中的属性
|
||
if (Check_Sensor_ID(captured_addr, &slot)) {
|
||
type = sensor_list[slot].type;
|
||
zone = sensor_list[slot].zone;
|
||
alarm_sensor_slot = slot;
|
||
}
|
||
|
||
// 渲染显示报警图文界面
|
||
UI_ShowAlarmPage(type, zone);
|
||
|
||
// 首次亮起防区特定的警示色彩,起到区分报警硬件的目的:
|
||
if (type == 0) RGB_Send(0, 180, 0, 0, 180, 0); // 门磁: 红色 (GRB中R=180,G=0)
|
||
else if (type == 1) RGB_Send(100, 180, 0, 100, 180, 0); // 红外PIR: 黄色 (G=100, R=180)
|
||
else if (type == 2) RGB_Send(180, 0, 0, 180, 0, 0); // 烟感: 绿色 (G=180)
|
||
else if (type == 3) RGB_Send(0, 180, 180, 0, 180, 180); // 气感: 紫色 (R=180, B=180)
|
||
else RGB_Send(0, 0, 180, 0, 0, 180); // 水浸: 蓝色 (B=180)
|
||
|
||
Uart_SendString("[AlarmApp] Started\r\n");
|
||
}
|
||
|
||
/**
|
||
* @brief AlarmApp 轮询主回调
|
||
* @details 负责非阻塞式同步控制 RGB 爆闪,并向 OLED 屏幕的四周边框绘制或擦除红边框,形成像素爆闪动效。
|
||
* (马达具体的报警振动波形由于对毫秒时序要求极高,已交由 Timer1_Isr 进行高精度的实时管理控制)。
|
||
*/
|
||
void AlarmApp_onRun(void)
|
||
{
|
||
u8 type = sensor_list[alarm_sensor_slot].type;
|
||
u8 flash = (ms_tick / 250) % 2; // 以 250ms 为周期周期亮灭
|
||
|
||
// 1. 同步控制 RGB 双幻彩灯进行交替爆闪
|
||
if (flash)
|
||
{
|
||
if (type == 0) RGB_Send(0, 180, 0, 0, 180, 0);
|
||
else if (type == 1) RGB_Send(100, 180, 0, 100, 180, 0);
|
||
else if (type == 2) RGB_Send(180, 0, 0, 180, 0, 0);
|
||
else if (type == 3) RGB_Send(0, 180, 180, 0, 180, 180);
|
||
else RGB_Send(0, 0, 180, 0, 0, 180);
|
||
}
|
||
else
|
||
{
|
||
RGB_Send(0, 0, 0, 0, 0, 0); // 灭灯
|
||
}
|
||
|
||
// 2. 控制 Truly AMOLED 屏幕的最外围红色警告框闪烁效果
|
||
if (alarm_flash_flag) {
|
||
LCD_DrawRectBorder(4, 4, 112, 232, COLOR_RED);
|
||
LCD_DrawRectBorder(6, 6, 108, 228, COLOR_RED);
|
||
} else {
|
||
LCD_DrawRectBorder(4, 4, 112, 232, COLOR_BLACK);
|
||
LCD_DrawRectBorder(6, 6, 108, 228, COLOR_BLACK);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief AlarmApp 退出清理
|
||
*/
|
||
void AlarmApp_onClose(void)
|
||
{
|
||
RGB_Send(0, 0, 0, 0, 0, 0); // 关闭灯条
|
||
MOTOR = 0; // 停震
|
||
current_state = STATE_NORMAL;
|
||
Uart_SendString("[AlarmApp] Closed\r\n");
|
||
}
|
||
|
||
/**
|
||
* @brief AlarmApp 接收事件处理回调
|
||
* @details 报警发生时,用户按下手环任意键,均代表已阅读入侵警报,应用将结束报警并退回待机时钟。
|
||
*/
|
||
EventResult AlarmApp_onEvent(SystemEvent *evt)
|
||
{
|
||
if (evt->key_event != KEY_EVENT_NONE) {
|
||
AppManager_StartApp(APP_ID_CLOCK); // 清除报警并切回 Clock
|
||
return EVENT_HANDLED;
|
||
}
|
||
return EVENT_IGNORED;
|
||
}
|
||
|
||
|
||
/* =========================================================================
|
||
* 5. SOSApp (SOS 紧急求救无线发射与警灯应用) 具体生命周期回调实现
|
||
* ========================================================================= */
|
||
|
||
// 私有标志,用作记录 SOS 的发射半周期状态
|
||
static u8 sos_last_phase;
|
||
|
||
/**
|
||
* @brief SOSApp 启动回调
|
||
*/
|
||
void SOSApp_OnStart(void)
|
||
{
|
||
current_state = STATE_SOS_EMITTED; // 进入 SOS 紧急求救状态
|
||
sos_last_phase = 99;
|
||
alarm_timer_ms = 0; // 马达震动波形计数器清零
|
||
UI_ShowSosPage(); // 初始化渲染 SOS 警示主页
|
||
|
||
// 立即点亮左侧灯为红色,右侧熄灭,准备开启双色红灭交替爆闪
|
||
RGB_Send(0, 200, 0, 0, 0, 0);
|
||
|
||
Uart_SendString("[SOSApp] Started\r\n");
|
||
}
|
||
|
||
/**
|
||
* @brief SOSApp 轮询主回调
|
||
* @details 负责控制两个非阻塞的时序任务:
|
||
* 1. 控制无线发射芯片:每 500ms 开启发射模式,广播发射手环的 24 位出厂物理 Factory ID + SOS特定数据码 (0x08)。
|
||
* 2. 控制警示灯效:以 50ms 的超快频率让灯条上的两个灯珠进行左右交替红色爆闪,达到强烈警告的视觉效果。
|
||
* 同时同步闪烁屏幕边框。
|
||
*/
|
||
void SOSApp_onRun(void)
|
||
{
|
||
// ====== 1. 控制无线射频,每 500ms 发射一次 SOS 无线数据帧 (0x08) ======
|
||
u8 sec_phase = (u8)((ms_tick / 500) % 2);
|
||
if (sec_phase != sos_last_phase) {
|
||
sos_last_phase = sec_phase;
|
||
RF_SetMode(2); // 切换发射
|
||
EV1527_Transmit(CLONED_ADDR, 0x08); // 发射克隆 ID 及其 SOS (0x08) 命令字
|
||
RF_SetMode(0); // 释放接收
|
||
}
|
||
|
||
// ====== 2. 控制 RGB 双灯珠以 50ms 频率进行高频左右交替红色爆闪 ======
|
||
{
|
||
u8 flash = (ms_tick / 50) % 2;
|
||
if (flash) {
|
||
RGB_Send(0, 200, 0, 0, 0, 0); // 灯1亮红 (G=200,GRB中R排在第二),灯2灭
|
||
} else {
|
||
RGB_Send(0, 0, 0, 0, 200, 0); // 灯1灭,灯2亮红
|
||
}
|
||
}
|
||
|
||
// ====== 3. 控制屏幕绘制多层爆闪红白框 ======
|
||
if (alarm_flash_flag) {
|
||
LCD_DrawRectBorder(4, 4, 112, 232, COLOR_RED);
|
||
LCD_DrawRectBorder(5, 5, 110, 230, COLOR_RED);
|
||
LCD_DrawRectBorder(8, 8, 104, 224, COLOR_WHITE);
|
||
} else {
|
||
LCD_DrawRectBorder(4, 4, 112, 232, COLOR_BLACK);
|
||
LCD_DrawRectBorder(5, 5, 110, 230, COLOR_BLACK);
|
||
LCD_DrawRectBorder(8, 8, 104, 224, COLOR_BLACK);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief SOSApp 退出清理
|
||
*/
|
||
void SOSApp_onClose(void)
|
||
{
|
||
RGB_Send(0, 0, 0, 0, 0, 0); // 关闭 RGB
|
||
MOTOR = 0; // 停震
|
||
current_state = STATE_NORMAL;
|
||
Uart_SendString("[SOSApp] Closed\r\n");
|
||
}
|
||
|
||
/**
|
||
* @brief SOSApp 接收事件处理
|
||
* @details 紧急呼救状态下,短按或长按物理按键,代表解除报警,直接切回待机时钟 ClockApp。
|
||
*/
|
||
EventResult SOSApp_onEvent(SystemEvent *evt)
|
||
{
|
||
if (evt->key_event != KEY_EVENT_NONE) {
|
||
AppManager_StartApp(APP_ID_CLOCK); // 退出 SOS,回到时钟
|
||
return EVENT_HANDLED;
|
||
}
|
||
return EVENT_IGNORED;
|
||
}
|