Compare commits

..

10 Commits

Author SHA1 Message Date
edisondeng
0ef5991d33 feat(config): add USE_PCF8563 build switch to run without the RTC chip
Some PCB variants may not populate the PCF8563. Guards RTC init/read
in HomeApp_OnStart(), the WriteDateTime() save in SetClockApp, and the
I2C/RTC diagnostic scans in Self_Test() behind #if USE_PCF8563. When
disabled, the firmware runs entirely on the Timer1-driven software
clock; date/time just won't survive a power cycle without the chip.
2026-07-31 19:54:18 +08:00
edisondeng
6022f0a409 feat(ui): extend set_clock_app to edit day/month/year, not just time
time_edit_field now cycles through 5 fields (hour/min/day/month/year)
via long-press CONFIRM. Up/down adjusts whichever field is focused,
with day clamped to Days_In_Month() when month/year changes, and
Clock_RecalcWeekday() called after every date edit so the weekday
label stays in sync. UI_UpdateSetTimeDigit() now also refreshes the
weekday abbreviation when editing day/month/year, since the computed
weekday can change independently of which digit was actually touched.
Save writes the full date+weekday via PCF8563_WriteDateTime(). Also
removes PCF8563_ReadTime()/WriteTime(), now unused since every caller
was updated to the DateTime variants directly.
2026-07-31 19:53:56 +08:00
edisondeng
6baf93c550 feat(ui): show real weekday/day/month on HOME screen instead of placeholder
UI_ShowClockPage()/UI_ShowSetTimePage() now take weekday/day/month(/year)
and build the date bar dynamically via Build_DateString() instead of
the fixed placeholder string. app_home.c reads the full date from RTC
via PCF8563_ReadDateTime() and recalculates weekday since the RTC's
stored date may differ from the software clock's. config.h exposes
current_day/month/year/weekday alongside the existing time fields so
main.c's debug commands don't need a separate include.
2026-07-31 19:43:43 +08:00
edisondeng
81ebf7e302 feat(i18n): add weekday abbreviation strings, remove static date text
Replaces the fixed placeholder STR_CLOCK_DATE ("VEN 10-07"/"FRI 10-07")
with 7 weekday abbreviation strings (FR/EN), ordered Sunday-Saturday to
align with current_weekday's 0-6 convention for direct indexing
(STR_WEEKDAY_SUN + current_weekday).
2026-07-31 19:35:21 +08:00
edisondeng
cb37b1deea feat(clock): add day/month/year rollover and Zeller weekday calc
Extends Clock_IncMS()'s existing sec->min->hour cascade to also roll
day->month->year (with leap-year Feb 29 via Days_In_Month()), and adds
Clock_RecalcWeekday() computing current_weekday via Zeller's congruence
whenever the date changes, rather than trusting PCF8563's own weekday
register (which would drift if the date is edited manually). Verified
the formula against 2026-07-31, a known Friday.
2026-07-31 19:32:44 +08:00
edisondeng
1146bade70 feat(rtc): add PCF8563 full date read/write (day/month/year/weekday)
PCF8563's second/minute/hour/day/weekday/month/year registers are
contiguous (0x02-0x08), so PCF8563_ReadDateTime() bursts all 7 bytes
in one transaction. PCF8563_ReadTime() becomes a thin wrapper that
discards the date fields. WriteTime() is kept as its own 3-byte-only
implementation (writing only sec/min/hour) rather than routed through
the new WriteDateTime(), since forcing it through a 7-byte write would
require synthesizing date values and risk corrupting the stored date;
WriteDateTime() is added as a separate full 7-byte write for callers
that actually have real date+weekday values to persist.
2026-07-31 19:30:02 +08:00
edisondeng
4a55c1f9eb docs: add wo-07 to add real year/month/day/weekday support 2026-07-31 19:26:55 +08:00
edisondeng
983b0928dd docs: mark wo-06 code-complete, pending hardware verification 2026-07-31 19:14:43 +08:00
edisondeng
dac2ea4335 fix(ui): partial-refresh time-set digit blink instead of full screen
UI_ShowSetTimePage() cleared and redrew the entire screen on every
500ms blink toggle and on every up/down value change, causing a
full-screen flash instead of just the intended hour/minute digit
blinking. Split out UI_UpdateSetTimeDigit() which only repaints the
32x32px digit region for the field being edited; the full page draw
is now only used on entry. Field-switch (long-press CONFIRM) refreshes
both digit regions to clear any stale blanked-out state.
2026-07-31 19:14:14 +08:00
edisondeng
d4b89c9336 docs: add wo-06 for time-set screen full-screen flicker fix 2026-07-31 19:10:31 +08:00
14 changed files with 460 additions and 96 deletions

View File

@@ -44,16 +44,21 @@ static void HomeApp_OnStart(void)
current_state = STATE_NORMAL; // 系统切入正常待机状态 current_state = STATE_NORMAL; // 系统切入正常待机状态
RF_SetMode(1); // 开启天线并置为接收模式,监听无线探测器 RF_SetMode(1); // 开启天线并置为接收模式,监听无线探测器
// 尝试从 PCF8563 RTC 芯片中读取硬件时间 (具备非阻塞防卡死保护) #if USE_PCF8563
// 尝试从 PCF8563 RTC 芯片中读取硬件时间与日期 (具备非阻塞防卡死保护)
if (PCF8563_Init()) { if (PCF8563_Init()) {
PCF8563_ReadTime(&current_hour, &current_min, &current_sec); PCF8563_ReadDateTime(&current_hour, &current_min, &current_sec, &current_day, &current_month, &current_year);
Clock_RecalcWeekday(); // 读回的日期可能与软时钟不同,重新算一次星期
XTELL_LOG("[HomeApp] RTC Hardware Synced OK!\r\n"); XTELL_LOG("[HomeApp] RTC Hardware Synced OK!\r\n");
} else { } else {
XTELL_LOG("[HomeApp] RTC Hardware Offline, using system clock.\r\n"); XTELL_LOG("[HomeApp] RTC Hardware Offline, using system clock.\r\n");
} }
#else
XTELL_LOG("[HomeApp] USE_PCF8563=0, running on software clock only.\r\n");
#endif
// 绘制并渲染待机时钟主页面画面 // 绘制并渲染待机时钟主页面画面
UI_ShowClockPage(current_state, current_hour, current_min); UI_ShowClockPage(current_state, current_hour, current_min, current_weekday, current_day, current_month);
// 关断/熄灭 FCOB 双灯条 // 关断/熄灭 FCOB 双灯条
RGB_Send(0, 0, 0, 0, 0, 0); RGB_Send(0, 0, 0, 0, 0, 0);
@@ -69,7 +74,7 @@ static void HomeApp_onRun(void)
// 软时钟发生分钟改变,需要通知屏幕重画时钟主页 // 软时钟发生分钟改变,需要通知屏幕重画时钟主页
if (clock_updated) { if (clock_updated) {
clock_updated = 0; clock_updated = 0;
UI_ShowClockPage(current_state, current_hour, current_min); UI_ShowClockPage(current_state, current_hour, current_min, current_weekday, current_day, current_month);
} }
} }
@@ -117,7 +122,7 @@ static EventResult HomeApp_onEvent(SystemEvent *evt)
} }
// 布撤防状态切换成功后进行 100ms 短震确认,并重新刷写时钟待机页 // 布撤防状态切换成功后进行 100ms 短震确认,并重新刷写时钟待机页
MOTOR = 1; Delay_ms(100); MOTOR = 0; MOTOR = 1; Delay_ms(100); MOTOR = 0;
UI_ShowClockPage(current_state, current_hour, current_min); UI_ShowClockPage(current_state, current_hour, current_min, current_weekday, current_day, current_month);
return EVENT_HANDLED; return EVENT_HANDLED;
} }

View File

@@ -12,7 +12,7 @@ extern SystemState current_state;
extern volatile u16 ms_tick; extern volatile u16 ms_tick;
// 本应用私有的时间编辑状态 // 本应用私有的时间编辑状态
static u8 time_edit_field = 1; // 标识当前聚焦在哪一个参数上进行微调 (1:修改小时, 2:修改分钟) static u8 time_edit_field = 1; // 当前聚焦调节的字段 (1:小时, 2:分钟, 3:日, 4:月, 5:年)
static u8 time_edit_blink_on = 1; // 时钟修改时光标闪烁周期状态位 static u8 time_edit_blink_on = 1; // 时钟修改时光标闪烁周期状态位
// 声明本应用的生命周期回调函数 // 声明本应用的生命周期回调函数
@@ -49,7 +49,7 @@ static void SetClockApp_OnStart(void)
MOTOR = 0; MOTOR = 0;
// 渲染显示时间编辑界面,默认高亮编辑小时字段,消隐位开启 // 渲染显示时间编辑界面,默认高亮编辑小时字段,消隐位开启
UI_ShowSetTimePage(current_hour, current_min, time_edit_field, 1); UI_ShowSetTimePage(current_hour, current_min, current_day, current_month, current_year, current_weekday, time_edit_field, 1);
XTELL_LOG("[SetClockApp] Started\r\n"); XTELL_LOG("[SetClockApp] Started\r\n");
} }
@@ -63,8 +63,8 @@ static void SetClockApp_onRun(void)
if (blink != time_edit_blink_on) if (blink != time_edit_blink_on)
{ {
time_edit_blink_on = blink; time_edit_blink_on = blink;
// 每次闪烁状态发生变化,重新刷写屏幕数值 // 每次闪烁状态发生变化,只局部刷新正在调节的那个字段,不整屏重绘
UI_ShowSetTimePage(current_hour, current_min, time_edit_field, time_edit_blink_on); UI_UpdateSetTimeDigit(current_hour, current_min, current_day, current_month, current_year, current_weekday, time_edit_field, time_edit_blink_on);
} }
} }
@@ -84,44 +84,85 @@ static EventResult SetClockApp_onEvent(SystemEvent *evt)
{ {
if (evt->key_event == KEY_EVENT_UP_CLICK) if (evt->key_event == KEY_EVENT_UP_CLICK)
{ {
// 短按 ▲ 键增加数值 (小时/分钟循环相加) // 短按 ▲ 键增加当前聚焦字段的数值 (各字段独立循环)
if (time_edit_field == 1) { switch (time_edit_field) {
current_hour = (current_hour + 1) % 24; case 1: current_hour = (current_hour + 1) % 24; break;
} else { case 2: current_min = (current_min + 1) % 60; break;
current_min = (current_min + 1) % 60; case 3:
current_day = (current_day >= Days_In_Month(current_year, current_month)) ? 1 : (current_day + 1);
Clock_RecalcWeekday();
break;
case 4:
current_month = (current_month >= 12) ? 1 : (current_month + 1);
// 月份变化后,若当前日超出新月份的最大天数,截断到最大天数
if (current_day > Days_In_Month(current_year, current_month)) {
current_day = Days_In_Month(current_year, current_month);
}
Clock_RecalcWeekday();
break;
default: // 5: 年
current_year = (current_year >= 99) ? 0 : (current_year + 1);
if (current_day > Days_In_Month(current_year, current_month)) {
current_day = Days_In_Month(current_year, current_month);
}
Clock_RecalcWeekday();
break;
} }
UI_ShowSetTimePage(current_hour, current_min, time_edit_field, 1); time_edit_blink_on = 1; // 调值后强制显示 (不处于消隐周期)
UI_UpdateSetTimeDigit(current_hour, current_min, current_day, current_month, current_year, current_weekday, time_edit_field, 1);
return EVENT_HANDLED; return EVENT_HANDLED;
} }
if (evt->key_event == KEY_EVENT_DOWN_CLICK) if (evt->key_event == KEY_EVENT_DOWN_CLICK)
{ {
// 短按 ▼ 键减小数值 // 短按 ▼ 键减小当前聚焦字段的数值 (各字段独立循环)
if (time_edit_field == 1) { switch (time_edit_field) {
current_hour = (current_hour == 0) ? 23 : (current_hour - 1); case 1: current_hour = (current_hour == 0) ? 23 : (current_hour - 1); break;
} else { case 2: current_min = (current_min == 0) ? 59 : (current_min - 1); break;
current_min = (current_min == 0) ? 59 : (current_min - 1); case 3:
current_day = (current_day <= 1) ? Days_In_Month(current_year, current_month) : (current_day - 1);
Clock_RecalcWeekday();
break;
case 4:
current_month = (current_month <= 1) ? 12 : (current_month - 1);
if (current_day > Days_In_Month(current_year, current_month)) {
current_day = Days_In_Month(current_year, current_month);
}
Clock_RecalcWeekday();
break;
default: // 5: 年
current_year = (current_year == 0) ? 99 : (current_year - 1);
if (current_day > Days_In_Month(current_year, current_month)) {
current_day = Days_In_Month(current_year, current_month);
}
Clock_RecalcWeekday();
break;
} }
UI_ShowSetTimePage(current_hour, current_min, time_edit_field, 1); time_edit_blink_on = 1; // 调值后强制显示 (不处于消隐周期)
UI_UpdateSetTimeDigit(current_hour, current_min, current_day, current_month, current_year, current_weekday, time_edit_field, 1);
return EVENT_HANDLED; return EVENT_HANDLED;
} }
if (evt->key_event == KEY_EVENT_CONFIRM_LONG) if (evt->key_event == KEY_EVENT_CONFIRM_LONG)
{ {
// 长按 ■ 确认键切换调节字段 (在修改小时与修改分钟间跳转) // 长按 ■ 确认键切换调节字段 (小时 -> 分钟 -> 日 -> 月 -> 年 -> 循环回小时)
if (time_edit_field == 1) { time_edit_field = (time_edit_field >= 5) ? 1 : (time_edit_field + 1);
time_edit_field = 2; time_edit_blink_on = 1; // 切换焦点后所有字段都应处于可见状态
} else { // 全部字段各局部刷新一次,避免切换前正处于消隐周期的那个字段停留在涂黑状态
time_edit_field = 1; UI_UpdateSetTimeDigit(current_hour, current_min, current_day, current_month, current_year, current_weekday, 1, 1);
} UI_UpdateSetTimeDigit(current_hour, current_min, current_day, current_month, current_year, current_weekday, 2, 1);
UI_ShowSetTimePage(current_hour, current_min, time_edit_field, 1); UI_UpdateSetTimeDigit(current_hour, current_min, current_day, current_month, current_year, current_weekday, 3, 1);
UI_UpdateSetTimeDigit(current_hour, current_min, current_day, current_month, current_year, current_weekday, 4, 1);
UI_UpdateSetTimeDigit(current_hour, current_min, current_day, current_month, current_year, current_weekday, 5, 1);
return EVENT_HANDLED; return EVENT_HANDLED;
} }
if (evt->key_event == KEY_EVENT_CONFIRM_CLICK) if (evt->key_event == KEY_EVENT_CONFIRM_CLICK)
{ {
// 短按 ■ 确认键保存最新时配置,退出到主菜单 (父 App) // 短按 ■ 确认键保存最新时间与日期配置,退出到主菜单 (父 App)
current_state = STATE_PAIR_MENU; current_state = STATE_PAIR_MENU;
// 同步将最新时间 (hour, min, 0秒) 写入 PCF8563 芯片寄存器 #if USE_PCF8563
PCF8563_WriteTime(current_hour, current_min, 0); // 同步将最新时间与日期 (含算好的星期) 一次性写入 PCF8563 芯片寄存器
PCF8563_WriteDateTime(current_hour, current_min, 0, current_day, current_month, current_year, current_weekday);
#endif
// 退出短振反馈 // 退出短振反馈
MOTOR = 1; Delay_ms(50); MOTOR = 0; MOTOR = 1; Delay_ms(50); MOTOR = 0;

View File

@@ -5,15 +5,62 @@
u8 current_hour = 10; // 系统软时钟:小时 (默认初始化为 10 点) u8 current_hour = 10; // 系统软时钟:小时 (默认初始化为 10 点)
u8 current_min = 35; // 系统软时钟:分钟 (默认初始化为 35 分) u8 current_min = 35; // 系统软时钟:分钟 (默认初始化为 35 分)
u8 current_sec = 0; // 系统软时钟:秒 u8 current_sec = 0; // 系统软时钟:秒
u8 current_day = 1; // 系统软时钟:日 (默认初始化为 1 号)
u8 current_month = 1; // 系统软时钟:月 (默认初始化为 1 月)
u8 current_year = 25; // 系统软时钟:年 (默认初始化为 2025 年)
u8 current_weekday = 0; // 系统软时钟:星期 (0=周日, 由 Clock_RecalcWeekday 现算)
volatile u8 clock_updated = 0; // 分钟跳变标志位,置位后用于驱动 UI 刷新时间显示 volatile u8 clock_updated = 0; // 分钟跳变标志位,置位后用于驱动 UI 刷新时间显示
volatile bit second_tick_flag = 0; // 整秒滴答标志位,每次秒进位置 1由主循环转换为 KEY_EVENT_SECOND_TICK 事件 volatile bit second_tick_flag = 0; // 整秒滴答标志位,每次秒进位置 1由主循环转换为 KEY_EVENT_SECOND_TICK 事件
// 毫秒计数器 (Timer1 每 1ms 累加) // 毫秒计数器 (Timer1 每 1ms 累加)
static u16 ms_cnt = 0; static u16 ms_cnt = 0;
// 每月天数表 (2 月固定按 28 天,闰年在 Days_In_Month 里单独加 1 天处理)
static code u8 days_in_month_table[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
u8 Days_In_Month(u8 year, u8 month)
{
if (month < 1 || month > 12) {
return 31; // 防御性兜底,不应该发生
}
if (month == 2) {
// 闰年判断2000~2099 区间内,能被 4 整除即为闰年 (期间没有能被 100 整除的年份需要排除)
if ((year % 4) == 0) {
return 29;
}
}
return days_in_month_table[month - 1];
}
void Clock_RecalcWeekday(void)
{
// Zeller 公式h = (q + [13(m+1)/5] + K + [K/4] + [J/4] - 2J) mod 7
// h: 0=周六,1=周日,2=周一,...,6=周五;用 "+5J" 代替 "-2J" 避免无符号数下溢 (-2 ≡ 5 (mod 7))
// 1、2 月按上一年的 13、14 月处理
u8 m = current_month;
u8 y = current_year;
u8 j = 20; // 世纪数 (2000~2099 固定为 20)
u16 h;
if (m < 3) {
m += 12;
if (y == 0) {
y = 99; // 2000 年 1/2 月,按 1999 年处理的边界情况
j = 19;
} else {
y -= 1;
}
}
h = (current_day + (13UL * (m + 1)) / 5 + y + y / 4 + j / 4 + 5UL * j) % 7;
// 把 Zeller 原始约定 (0=周六) 转换成常用的 0=周日, 1=周一, ..., 6=周六
current_weekday = (u8)((h + 6) % 7);
}
/** /**
* @brief 系统软时钟毫秒递增更新 (在 1ms 中断中调用) * @brief 系统软时钟毫秒递增更新 (在 1ms 中断中调用)
* @details 负责由定时器 1ms 驱动递增软时钟,并在分钟发生跳变时将 clock_updated 置位 * @details 负责由定时器 1ms 驱动递增软时钟,并在分钟/日期发生跳变时做相应处理
*/ */
void Clock_IncMS(void) void Clock_IncMS(void)
{ {
@@ -29,7 +76,20 @@ void Clock_IncMS(void)
current_min = 0; // 1小时时间到 current_min = 0; // 1小时时间到
current_hour++; current_hour++;
if (current_hour >= 24) { if (current_hour >= 24) {
current_hour = 0; // 小时循环 current_hour = 0; // 小时循环,日期进位
current_day++;
if (current_day > Days_In_Month(current_year, current_month)) {
current_day = 1; // 月份进位
current_month++;
if (current_month > 12) {
current_month = 1; // 年份进位 (不处理世纪)
current_year++;
if (current_year > 99) {
current_year = 0;
}
}
}
Clock_RecalcWeekday(); // 日期变化,重新计算星期
} }
} }
clock_updated = 1; // 分钟跳变,标志位置位,前台待机 App 会捕获以刷新 UI clock_updated = 1; // 分钟跳变,标志位置位,前台待机 App 会捕获以刷新 UI

View File

@@ -7,6 +7,10 @@
extern u8 current_hour; extern u8 current_hour;
extern u8 current_min; extern u8 current_min;
extern u8 current_sec; extern u8 current_sec;
extern u8 current_day; // 日 (1~31)
extern u8 current_month; // 月 (1~12)
extern u8 current_year; // 年 (0~99代表 2000~2099不处理世纪)
extern u8 current_weekday; // 星期 (0=周日, 1=周一, ..., 6=周六)
extern volatile u8 clock_updated; extern volatile u8 clock_updated;
extern volatile bit second_tick_flag; // 整秒滴答标志位,每次 current_sec 递增时置 1供主循环转换为事件 extern volatile bit second_tick_flag; // 整秒滴答标志位,每次 current_sec 递增时置 1供主循环转换为事件
@@ -15,4 +19,18 @@ extern volatile bit second_tick_flag; // 整秒滴答标志位,每次 current_
*/ */
void Clock_IncMS(void); void Clock_IncMS(void);
/**
* @brief 返回指定年月的天数上限 (处理闰年 2 月)
* @param year 年 (0~99代表 2000~2099)
* @param month 月 (1~12)
*/
u8 Days_In_Month(u8 year, u8 month);
/**
* @brief 根据年月日重新计算星期,并写回 current_weekday
* @details 采用 Zeller 公式现算,不依赖 RTC 自身的星期寄存器,
* 避免手动改日期后星期跟着错乱;日期发生变化(自然进位或手动调节)时都要调用一次。
*/
void Clock_RecalcWeekday(void);
#endif // __CLOCK_H__ #endif // __CLOCK_H__

View File

@@ -141,6 +141,10 @@ extern SystemState current_state; // 系统当前整体运行状态
extern u8 current_hour; // 当前系统时间:小时 extern u8 current_hour; // 当前系统时间:小时
extern u8 current_min; // 当前系统时间:分钟 extern u8 current_min; // 当前系统时间:分钟
extern u8 current_sec; // 当前系统时间:秒 extern u8 current_sec; // 当前系统时间:秒
extern u8 current_day; // 当前系统日期:日
extern u8 current_month; // 当前系统日期:月
extern u8 current_year; // 当前系统日期:年 (0~99代表 2000~2099)
extern u8 current_weekday; // 当前系统日期:星期 (0=周日, ..., 6=周六)
extern volatile u8 clock_updated; // 时钟分钟更新标志 (1表示需要刷新待机页时间) extern volatile u8 clock_updated; // 时钟分钟更新标志 (1表示需要刷新待机页时间)
extern volatile bit second_tick_flag; // 整秒滴答标志位,每秒置 1由主循环转换为 KEY_EVENT_SECOND_TICK 事件 extern volatile bit second_tick_flag; // 整秒滴答标志位,每秒置 1由主循环转换为 KEY_EVENT_SECOND_TICK 事件
@@ -170,4 +174,9 @@ extern volatile u16 comb_hold; // 组合键持续按下计时器
#define MAIN_Fosc 24000000UL // STC32G 主频工作频率定义 (24.0 MHz) #define MAIN_Fosc 24000000UL // STC32G 主频工作频率定义 (24.0 MHz)
#define CLONED_ADDR 0x37A86UL // 手环自身克隆发射的默认 24 位射频出厂 ID 地址 #define CLONED_ADDR 0x37A86UL // 手环自身克隆发射的默认 24 位射频出厂 ID 地址
// 硬件模块使能开关:某些 PCB 版本可能不焊接 PCF8563 RTC 芯片,置 0 后完全跳过
// I2C 探测/读写与开机诊断扫描,只依赖软件时钟 (Timer1 驱动的 current_hour/min/sec 等)
// 不写入/不写回 RTC断电后时间不保留但整机功能不受影响。
#define USE_PCF8563 1
#endif // __CONFIG_H__ #endif // __CONFIG_H__

View File

@@ -186,21 +186,21 @@ void Debug_ProcessCommand(char cmd)
// 模拟切换至 正常撤防 (NORMAL) 状态并刷新时钟 // 模拟切换至 正常撤防 (NORMAL) 状态并刷新时钟
if (AppManager_GetActiveAppID() == APP_ID_HOME) { if (AppManager_GetActiveAppID() == APP_ID_HOME) {
current_state = STATE_NORMAL; current_state = STATE_NORMAL;
UI_ShowClockPage(current_state, current_hour, current_min); UI_ShowClockPage(current_state, current_hour, current_min, current_weekday, current_day, current_month);
} }
Uart_SendString("[UART] State: NORMAL\r\n"); Uart_SendString("[UART] State: NORMAL\r\n");
} else if (cmd == '2') { } else if (cmd == '2') {
// 模拟切换至 已布防 (ARMED) 状态,普通门磁可触发警报 // 模拟切换至 已布防 (ARMED) 状态,普通门磁可触发警报
if (AppManager_GetActiveAppID() == APP_ID_HOME) { if (AppManager_GetActiveAppID() == APP_ID_HOME) {
current_state = STATE_ARMED; current_state = STATE_ARMED;
UI_ShowClockPage(current_state, current_hour, current_min); UI_ShowClockPage(current_state, current_hour, current_min, current_weekday, current_day, current_month);
} }
Uart_SendString("[UART] State: ARMED\r\n"); Uart_SendString("[UART] State: ARMED\r\n");
} else if (cmd == '3') { } else if (cmd == '3') {
// 模拟切换至 已撤防 (DISARMED) 状态,忽略普通报警 // 模拟切换至 已撤防 (DISARMED) 状态,忽略普通报警
if (AppManager_GetActiveAppID() == APP_ID_HOME) { if (AppManager_GetActiveAppID() == APP_ID_HOME) {
current_state = STATE_DISARMED; current_state = STATE_DISARMED;
UI_ShowClockPage(current_state, current_hour, current_min); UI_ShowClockPage(current_state, current_hour, current_min, current_weekday, current_day, current_month);
} }
Uart_SendString("[UART] State: DISARMED\r\n"); Uart_SendString("[UART] State: DISARMED\r\n");
} else if (cmd == 'z' || cmd == 'Z') { } else if (cmd == 'z' || cmd == 'Z') {

View File

@@ -56,7 +56,14 @@
DEFINE_STRING(BIND_SENDING, "ENVOI EN COURS", "SENDING...") \ DEFINE_STRING(BIND_SENDING, "ENVOI EN COURS", "SENDING...") \
/* ---- 时钟页公共元素 ---- */ \ /* ---- 时钟页公共元素 ---- */ \
DEFINE_STRING(CLOCK_AMPM, "AM", "AM") \ DEFINE_STRING(CLOCK_AMPM, "AM", "AM") \
DEFINE_STRING(CLOCK_DATE, "VEN 10-07", "FRI 10-07") /* ---- 星期缩写 (顺序必须是周日~周六,与 current_weekday 的 0~6 对齐,取用时按 STR_WEEKDAY_SUN + current_weekday 直接索引,顺序不能打乱) ---- */ \
DEFINE_STRING(WEEKDAY_SUN, "DIM", "SUN") \
DEFINE_STRING(WEEKDAY_MON, "LUN", "MON") \
DEFINE_STRING(WEEKDAY_TUE, "MAR", "TUE") \
DEFINE_STRING(WEEKDAY_WED, "MER", "WED") \
DEFINE_STRING(WEEKDAY_THU, "JEU", "THU") \
DEFINE_STRING(WEEKDAY_FRI, "VEN", "FRI") \
DEFINE_STRING(WEEKDAY_SAT, "SAM", "SAT")
typedef enum { typedef enum {
WRISTBAND_STRING_LIST WRISTBAND_STRING_LIST

View File

@@ -168,6 +168,7 @@ void Self_Test(void)
Uart_SendString("\r\n========================================\r\n"); Uart_SendString("\r\n========================================\r\n");
Uart_SendString("[SELF-TEST] System Power-On Self Test...\r\n"); Uart_SendString("[SELF-TEST] System Power-On Self Test...\r\n");
#if USE_PCF8563
// 执行 I2C 总线器件扫描,查找 RTC 是否物理响应 // 执行 I2C 总线器件扫描,查找 RTC 是否物理响应
PCF8563_ScanDiagnostic(); PCF8563_ScanDiagnostic();
@@ -198,6 +199,9 @@ void Self_Test(void)
// 0.2 执行 RTC 底层单独试读测试 // 0.2 执行 RTC 底层单独试读测试
PCF8563_TryReadDiagnostic(); PCF8563_TryReadDiagnostic();
#else
Uart_SendString("[SELF-TEST] USE_PCF8563=0, skipping RTC/I2C diagnostics.\r\n");
#endif
// 1. 验证 PWR_HOLD 锁存状态 // 1. 验证 PWR_HOLD 锁存状态
if (PWR_HOLD == 1) { if (PWR_HOLD == 1) {

177
App/ui.c
View File

@@ -7,19 +7,77 @@
// 引入外部主菜单对码项名称的字符串 ID 表 (例如门磁/PIR/烟感等防区类型) // 引入外部主菜单对码项名称的字符串 ID 表 (例如门磁/PIR/烟感等防区类型)
extern const StringID device_menu_ids[5]; extern const StringID device_menu_ids[5];
// 底部日期栏固定用 8x16 小字体、LCD_ShowStringCentered 居中绘制在 y=195
// 编辑日期时对 日/月/年 做局部刷新需要精确知道每个字段的像素 X 坐标,这里按
// LCD_ShowStringCentered 的居中公式 (LCD_WIDTH - 字符数*8) / 2 手工算好,
// 前提是字符串格式固定为 "WWW DD-MM-YY" (12 个字符),不能随意改动格式。
#define DATE_BAR_Y 195
#define DATE_BAR_START_X 19 // (135 - 12*8) / 2 = 19
#define DATE_BAR_DAY_X (DATE_BAR_START_X + 4 * 8) // 第 4 个字符起 = 51
#define DATE_BAR_MONTH_X (DATE_BAR_START_X + 7 * 8) // 第 7 个字符起 = 75
#define DATE_BAR_YEAR_X (DATE_BAR_START_X + 10 * 8) // 第 10 个字符起 = 99
#define DATE_BAR_FIELD_W 16 // 2 位数字8px/字符
/**
* @brief 拼出"星期缩写 日-月"格式的日期字符串 (如 "VEN 10-07"),供 HOME 页时钟显示
* @param out 输出缓冲区,调用方需保证至少 10 字节 (3字母+空格+2位+'-'+2位+'\0')
*/
static void Build_DateString(char *out, u8 weekday, u8 day, u8 month)
{
char *wd = Lang_Get((StringID)(STR_WEEKDAY_SUN + weekday));
u8 i = 0;
while (*wd) {
out[i++] = *wd++;
}
out[i++] = ' ';
out[i++] = '0' + (day / 10);
out[i++] = '0' + (day % 10);
out[i++] = '-';
out[i++] = '0' + (month / 10);
out[i++] = '0' + (month % 10);
out[i] = '\0';
}
/**
* @brief 拼出"星期缩写 日-月-年"格式的日期字符串 (如 "VEN 10-07-25"),供时间设置页编辑日期时显示
* @param out 输出缓冲区,调用方需保证至少 13 字节 (3字母+空格+2位+'-'+2位+'-'+2位+'\0')
*/
static void Build_DateStringWithYear(char *out, u8 weekday, u8 day, u8 month, u8 year)
{
char *wd = Lang_Get((StringID)(STR_WEEKDAY_SUN + weekday));
u8 i = 0;
while (*wd) {
out[i++] = *wd++;
}
out[i++] = ' ';
out[i++] = '0' + (day / 10);
out[i++] = '0' + (day % 10);
out[i++] = '-';
out[i++] = '0' + (month / 10);
out[i++] = '0' + (month % 10);
out[i++] = '-';
out[i++] = '0' + (year / 10);
out[i++] = '0' + (year % 10);
out[i] = '\0';
}
/** /**
* @brief 渲染并显示系统待机时钟主页面 * @brief 渲染并显示系统待机时钟主页面
* @param state 当前的系统布防状态 (STATE_ARMED: 已布防, 否则为正常或撤防) * @param state 当前的系统布防状态 (STATE_ARMED: 已布防, 否则为正常或撤防)
* @param hour 当前待显示的小时数 (0~23) * @param hour 当前待显示的小时数 (0~23)
* @param min 当前待显示的分钟数 (0~59) * @param min 当前待显示的分钟数 (0~59)
* @param weekday 当前星期 (0=周日, ..., 6=周六)
* @param day 当前日 (1~31)
* @param month 当前月 (1~12)
* @details 该页面被划分为三大功能视觉区: * @details 该页面被划分为三大功能视觉区:
* 1. 顶部状态栏:左侧绘制布撤防指示锁,右侧显示真实剩余电量百分比以及电池单色位图。 * 1. 顶部状态栏:左侧绘制布撤防指示锁,右侧显示真实剩余电量百分比以及电池单色位图。
* 2. 中部核心栏:垂直居中以 16x32 大号粗字体绘制格式化时间 "HH:MM",在其下方标注 AM/PM 标识。 * 2. 中部核心栏:垂直居中以 16x32 大号粗字体绘制格式化时间 "HH:MM",在其下方标注 AM/PM 标识。
* 3. 底部信息栏:使用灰色小字模绘制法语星期日期 "VEN 10-07" (星期五 7月10日)。 * 3. 底部信息栏:使用灰色小字模绘制真实星期日期 "VEN 10-07" (星期五 7月10日)。
*/ */
void UI_ShowClockPage(SystemState state, u8 hour, u8 min) void UI_ShowClockPage(SystemState state, u8 hour, u8 min, u8 weekday, u8 day, u8 month)
{ {
char time_str[6]; char time_str[6];
char date_str[10];
char batt_str[7]; char batt_str[7];
u8 batt; u8 batt;
@@ -74,8 +132,9 @@ void UI_ShowClockPage(SystemState state, u8 hour, u8 min)
// ====== 绘制 AM/PM 状态 ====== // ====== 绘制 AM/PM 状态 ======
LCD_ShowStringCentered(150, Lang_Get(STR_CLOCK_AMPM), COLOR_WHITE, COLOR_BLACK); LCD_ShowStringCentered(150, Lang_Get(STR_CLOCK_AMPM), COLOR_WHITE, COLOR_BLACK);
// ====== 绘制日期星期栏 ====== // ====== 绘制日期星期栏 (真实日期,不再是写死的占位文案) ======
LCD_ShowStringCentered(195, Lang_Get(STR_CLOCK_DATE), COLOR_GRAY, COLOR_BLACK); Build_DateString(date_str, weekday, day, month);
LCD_ShowStringCentered(DATE_BAR_Y, date_str, COLOR_GRAY, COLOR_BLACK);
} }
/** /**
@@ -364,61 +423,105 @@ void UI_ShowMainMenu(u8 selected_index)
} }
/** /**
* @brief 渲染修改时间闪烁调节页面 (调节小时或分钟) * @brief 渲染修改时间/日期闪烁调节页面
* @param hour 修改状态中的小时 * @param hour 修改状态中的小时
* @param min 修改状态中的分钟 * @param min 修改状态中的分钟
* @param selection 当前正在编辑聚焦的属性位 (1: 修改小时, 2: 修改分钟) * @param day 修改状态中的日
* @param month 修改状态中的月
* @param year 修改状态中的年 (0~99代表 2000~2099)
* @param weekday 根据当前日期算出的星期 (0=周日, ..., 6=周六)
* @param selection 当前正在编辑聚焦的属性位 (1:小时, 2:分钟, 3:日, 4:月, 5:年)
* @param blink_on 当前 500ms 控制隐现的闪烁周期标志位 (1-绘制正常字0-以空格消隐该数值字符) * @param blink_on 当前 500ms 控制隐现的闪烁周期标志位 (1-绘制正常字0-以空格消隐该数值字符)
*/ */
void UI_ShowSetTimePage(u8 hour, u8 min, u8 selection, u8 blink_on) // 小时/分钟数字在屏幕上的固定坐标 (16x32 字体,两位数字宽度为 32px)
#define SET_TIME_DIGIT_Y 100
#define SET_TIME_DIGIT_W 32
#define SET_TIME_DIGIT_H 32
#define SET_TIME_HOUR_X 23
#define SET_TIME_MIN_X 75
void UI_ShowSetTimePage(u8 hour, u8 min, u8 day, u8 month, u8 year, u8 weekday, u8 selection, u8 blink_on)
{ {
char time_str[6]; char time_str[6];
char date_str[13];
LCD_Clear(COLOR_BLACK); LCD_Clear(COLOR_BLACK);
// 绘制绿锁头等背景静态信息 // 绘制绿锁头等背景静态信息
LCD_DrawRectBorder(10, 23, 20, 15, COLOR_GREEN); LCD_DrawRectBorder(10, 23, 20, 15, COLOR_GREEN);
LCD_FillRect(15, 15, 2, 8, COLOR_GREEN); LCD_FillRect(15, 15, 2, 8, COLOR_GREEN);
LCD_FillRect(17, 15, 8, 2, COLOR_GREEN); LCD_FillRect(17, 15, 8, 2, COLOR_GREEN);
LCD_FillRect(25, 17, 2, 6, COLOR_GREEN); LCD_FillRect(25, 17, 2, 6, COLOR_GREEN);
// 移动电池百分比和图标到 135px 右上角 // 移动电池百分比和图标到 135px 右上角
LCD_ShowString(77, 24, "85%", COLOR_GRAY, COLOR_BLACK); LCD_ShowString(77, 24, "85%", COLOR_GRAY, COLOR_BLACK);
LCD_DrawMonoBitmap(107, 27, 20, 10, bmp_battery_20x10, COLOR_GRAY, COLOR_BLACK); LCD_DrawMonoBitmap(107, 27, 20, 10, bmp_battery_20x10, COLOR_GRAY, COLOR_BLACK);
// ====== 处理调节项一:小时的消隐/显示逻辑 ====== // 整页绘制时,各数字一律完整显示 (消隐闪烁效果交给 UI_UpdateSetTimeDigit 局部刷新处理)
if (selection == 1 && !blink_on) time_str[0] = '0' + (hour / 10);
{ time_str[1] = '0' + (hour % 10);
// 聚焦在小时且处于灭周期,用空格隐藏数值,形成数值跳变闪烁调节感
time_str[0] = ' ';
time_str[1] = ' ';
}
else
{
time_str[0] = '0' + (hour / 10);
time_str[1] = '0' + (hour % 10);
}
time_str[2] = '\0'; time_str[2] = '\0';
LCD_ShowString16x32(23, 100, time_str, COLOR_CYAN, COLOR_BLACK); // 采用青色特大字描绘小时 (在 135px 下整体居中 X = 23) LCD_ShowString16x32(SET_TIME_HOUR_X, SET_TIME_DIGIT_Y, time_str, COLOR_CYAN, COLOR_BLACK); // 采用青色特大字描绘小时 (在 135px 下整体居中 X = 23)
// 绘制中间冒号 (在 135px 下整体居中 X = 59) // 绘制中间冒号 (在 135px 下整体居中 X = 59)
LCD_ShowString16x32(59, 98, ":", COLOR_WHITE, COLOR_BLACK); LCD_ShowString16x32(59, 98, ":", COLOR_WHITE, COLOR_BLACK);
// ====== 处理调节项二:分钟的消隐/显示逻辑 ====== time_str[0] = '0' + (min / 10);
if (selection == 2 && !blink_on) time_str[1] = '0' + (min % 10);
{
// 聚焦在分钟且处于灭周期,用空格隐藏数值
time_str[0] = ' ';
time_str[1] = ' ';
}
else
{
time_str[0] = '0' + (min / 10);
time_str[1] = '0' + (min % 10);
}
time_str[2] = '\0'; time_str[2] = '\0';
LCD_ShowString16x32(75, 100, time_str, COLOR_CYAN, COLOR_BLACK); // 采用青色特大字描绘分钟 (在 135px 下整体居中 X = 75) LCD_ShowString16x32(SET_TIME_MIN_X, SET_TIME_DIGIT_Y, time_str, COLOR_CYAN, COLOR_BLACK); // 采用青色特大字描绘分钟 (在 135px 下整体居中 X = 75)
// 补齐底部的固定辅助元素 // 补齐底部的固定辅助元素
LCD_ShowStringCentered(150, Lang_Get(STR_CLOCK_AMPM), COLOR_WHITE, COLOR_BLACK); LCD_ShowStringCentered(150, Lang_Get(STR_CLOCK_AMPM), COLOR_WHITE, COLOR_BLACK);
LCD_ShowStringCentered(195, Lang_Get(STR_CLOCK_DATE), COLOR_GRAY, COLOR_BLACK);
// 编辑日期时,日期栏额外带上年份 ("WWW DD-MM-YY"),方便看清正在改的年份数值
Build_DateStringWithYear(date_str, weekday, day, month, year);
LCD_ShowStringCentered(DATE_BAR_Y, date_str, COLOR_GRAY, COLOR_BLACK);
}
/**
* @brief 局部刷新时间设置界面里"正在调节的那一个"字段 (小时/分钟/日/月/年)
* @param weekday 根据当前日期算出的星期 (0=周日, ..., 6=周六),调节日/月/年时顺带刷新星期缩写
* @param selection 1:小时, 2:分钟, 3:日, 4:月, 5:年
* @param blink_on 1:显示数值, 0:消隐(涂黑),用于 500ms 周期闪烁效果
* @details 小时/分钟用 32x32px 大字体区域;日/月/年用日期栏里对应的 16x16px 小字体区域,
* 都只重画自己那一小块,不触碰锁头/电池/冒号/AM-PM/星期文字等其它内容,
* 避免每次闪烁或调值都整屏 LCD_Clear() 造成全屏闪烁。
*/
void UI_UpdateSetTimeDigit(u8 hour, u8 min, u8 day, u8 month, u8 year, u8 weekday, u8 selection, u8 blink_on)
{
char buf[3];
u16 x;
u16 y;
u16 w;
u16 h;
u8 value;
switch (selection) {
case 1: x = SET_TIME_HOUR_X; y = SET_TIME_DIGIT_Y; w = SET_TIME_DIGIT_W; h = SET_TIME_DIGIT_H; value = hour; break;
case 2: x = SET_TIME_MIN_X; y = SET_TIME_DIGIT_Y; w = SET_TIME_DIGIT_W; h = SET_TIME_DIGIT_H; value = min; break;
case 3: x = DATE_BAR_DAY_X; y = DATE_BAR_Y; w = DATE_BAR_FIELD_W; h = 16; value = day; break;
case 4: x = DATE_BAR_MONTH_X; y = DATE_BAR_Y; w = DATE_BAR_FIELD_W; h = 16; value = month; break;
default: x = DATE_BAR_YEAR_X; y = DATE_BAR_Y; w = DATE_BAR_FIELD_W; h = 16; value = year; break;
}
// 调节日/月/年时,星期缩写可能因为日期变化而改变,顺带刷新一下 (不参与闪烁,始终显示)
if (selection >= 3) {
LCD_FillRect(DATE_BAR_START_X, DATE_BAR_Y, 24, 16, COLOR_BLACK);
LCD_ShowString(DATE_BAR_START_X, DATE_BAR_Y, Lang_Get((StringID)(STR_WEEKDAY_SUN + weekday)), COLOR_GRAY, COLOR_BLACK);
}
if (!blink_on) {
// 灭周期:直接把这一小块涂黑,形成消隐效果
LCD_FillRect(x, y, w, h, COLOR_BLACK);
return;
}
buf[0] = '0' + (value / 10);
buf[1] = '0' + (value % 10);
buf[2] = '\0';
if (selection <= 2) {
LCD_ShowString16x32(x, y, buf, COLOR_CYAN, COLOR_BLACK);
} else {
LCD_ShowString(x, y, buf, COLOR_GRAY, COLOR_BLACK);
}
} }

View File

@@ -10,10 +10,13 @@
* @param state 当前的系统运行状态 (STATE_NORMAL/STATE_ARMED/STATE_DISARMED) * @param state 当前的系统运行状态 (STATE_NORMAL/STATE_ARMED/STATE_DISARMED)
* @param hour 当前待显示的小时 * @param hour 当前待显示的小时
* @param min 当前待显示的分钟 * @param min 当前待显示的分钟
* @param weekday 当前星期 (0=周日, ..., 6=周六)
* @param day 当前日 (1~31)
* @param month 当前月 (1~12)
* @details 负责在 120x240 大小的 AMOLED 屏幕上居中绘制巨大的时钟字样,并在顶部 * @details 负责在 120x240 大小的 AMOLED 屏幕上居中绘制巨大的时钟字样,并在顶部
* 绘制对应的状态图标 (如一把打开的锁代表已撤防、闭合的锁代表已布防等)。 * 绘制对应的状态图标 (如一把打开的锁代表已撤防、闭合的锁代表已布防等)。
*/ */
void UI_ShowClockPage(SystemState state, u8 hour, u8 min); void UI_ShowClockPage(SystemState state, u8 hour, u8 min, u8 weekday, u8 day, u8 month);
/** /**
* @brief 渲染显示传感器对码的二级设备选择菜单页面 * @brief 渲染显示传感器对码的二级设备选择菜单页面
@@ -74,12 +77,24 @@ void UI_ShowBindingPage(u32 addr);
void UI_ShowMainMenu(u8 selected_index); void UI_ShowMainMenu(u8 selected_index);
/** /**
* @brief 渲染显示修改时间页面 (时钟设置编辑状态) * @brief 渲染显示修改时间/日期页面 (时钟设置编辑状态)
* @param hour 待修改的小时 * @param hour 待修改的小时
* @param min 待修改的分钟 * @param min 待修改的分钟
* @param selection 当前选中的修改项 (1:修改小时, 2:修改分钟) * @param day 待修改的日
* @param month 待修改的月
* @param year 待修改的年 (0~99代表 2000~2099)
* @param weekday 根据当前日期算出的星期 (0=周日, ..., 6=周六)
* @param selection 当前选中的修改项 (1:小时, 2:分钟, 3:日, 4:月, 5:年)
* @param blink_on 控制被选中数值以 500ms 周期进行隐现闪烁的周期标志位 (1-显示, 0-消隐) * @param blink_on 控制被选中数值以 500ms 周期进行隐现闪烁的周期标志位 (1-显示, 0-消隐)
*/ */
void UI_ShowSetTimePage(u8 hour, u8 min, u8 selection, u8 blink_on); void UI_ShowSetTimePage(u8 hour, u8 min, u8 day, u8 month, u8 year, u8 weekday, u8 selection, u8 blink_on);
/**
* @brief 局部刷新时间设置界面里正在调节的那一个字段 (小时/分钟/日/月/年),用于闪烁/调值时避免整屏重绘
* @param weekday 根据当前日期算出的星期 (0=周日, ..., 6=周六),调节日/月/年时顺带刷新星期缩写
* @param selection 1:小时, 2:分钟, 3:日, 4:月, 5:年
* @param blink_on 1:显示数值, 0:消隐(涂黑)
*/
void UI_UpdateSetTimeDigit(u8 hour, u8 min, u8 day, u8 month, u8 year, u8 weekday, u8 selection, u8 blink_on);
#endif // __UI_H__ #endif // __UI_H__

View File

@@ -256,11 +256,11 @@ void PCF8563_ScanDiagnostic(void)
} }
/** /**
* @brief 从 PCF8563 读取当前时间 * @brief 从 PCF8563 连续读取时间与日期 (秒/分/时/日/星期/月/年,寄存器 0x02~0x08 地址连续)
*/ */
bit PCF8563_ReadTime(u8 *hour, u8 *min, u8 *sec) bit PCF8563_ReadDateTime(u8 *hour, u8 *min, u8 *sec, u8 *day, u8 *month, u8 *year)
{ {
u8 raw_sec, raw_min, raw_hour; u8 raw_sec, raw_min, raw_hour, raw_day, raw_weekday, raw_month, raw_year;
I2C_Start(); I2C_Start();
I2C_SendByte(PCF8563_ADDR_WRITE); I2C_SendByte(PCF8563_ADDR_WRITE);
@@ -276,49 +276,73 @@ bit PCF8563_ReadTime(u8 *hour, u8 *min, u8 *sec)
if (!I2C_WaitAck()) return 0; if (!I2C_WaitAck()) return 0;
raw_sec = I2C_ReadByte(); raw_sec = I2C_ReadByte();
I2C_SendAck(1); // 发送 ACK I2C_SendAck(1);
raw_min = I2C_ReadByte(); raw_min = I2C_ReadByte();
I2C_SendAck(1); // 发送 ACK I2C_SendAck(1);
raw_hour = I2C_ReadByte(); raw_hour = I2C_ReadByte();
I2C_SendAck(1);
raw_day = I2C_ReadByte();
I2C_SendAck(1);
raw_weekday = I2C_ReadByte(); // 星期寄存器不使用,芯片自身的星期在改日期后可能与实际不符
I2C_SendAck(1);
(void)raw_weekday;
raw_month = I2C_ReadByte();
I2C_SendAck(1);
raw_year = I2C_ReadByte();
I2C_SendAck(0); // 发送 NACK 结束连续读取 I2C_SendAck(0); // 发送 NACK 结束连续读取
I2C_Stop(); I2C_Stop();
// 解析 BCD 数据 (掩码屏蔽无效位) // 解析 BCD 数据 (掩码屏蔽无效位)
*sec = BCD2DEC(raw_sec & 0x7F); *sec = BCD2DEC(raw_sec & 0x7F);
*min = BCD2DEC(raw_min & 0x7F); *min = BCD2DEC(raw_min & 0x7F);
*hour = BCD2DEC(raw_hour & 0x3F); *hour = BCD2DEC(raw_hour & 0x3F);
*day = BCD2DEC(raw_day & 0x3F);
*month = BCD2DEC(raw_month & 0x1F); // 忽略 bit7 世纪位,固定按 2000~2099 处理
*year = BCD2DEC(raw_year);
return 1; return 1;
} }
/** /**
* @brief 将时间精准写入 PCF8563 RTC 芯片 * @brief 将时间与日期一次性连续写入 PCF8563 RTC 芯片 (寄存器 0x02~0x08 共 7 字节连续写入)
*/ */
bit PCF8563_WriteTime(u8 hour, u8 min, u8 sec) bit PCF8563_WriteDateTime(u8 hour, u8 min, u8 sec, u8 day, u8 month, u8 year, u8 weekday)
{ {
I2C_Start(); I2C_Start();
I2C_SendByte(PCF8563_ADDR_WRITE); I2C_SendByte(PCF8563_ADDR_WRITE);
if (!I2C_WaitAck()) return 0; if (!I2C_WaitAck()) return 0;
// 定位写入起始寄存器 0x02
I2C_SendByte(REG_VL_SECONDS); I2C_SendByte(REG_VL_SECONDS);
if (!I2C_WaitAck()) return 0; if (!I2C_WaitAck()) return 0;
// 写入秒
I2C_SendByte(DEC2BCD(sec & 0x7F)); I2C_SendByte(DEC2BCD(sec & 0x7F));
if (!I2C_WaitAck()) return 0; if (!I2C_WaitAck()) return 0;
// 写入分
I2C_SendByte(DEC2BCD(min & 0x7F)); I2C_SendByte(DEC2BCD(min & 0x7F));
if (!I2C_WaitAck()) return 0; if (!I2C_WaitAck()) return 0;
// 写入时
I2C_SendByte(DEC2BCD(hour & 0x3F)); I2C_SendByte(DEC2BCD(hour & 0x3F));
if (!I2C_WaitAck()) return 0; if (!I2C_WaitAck()) return 0;
I2C_SendByte(DEC2BCD(day & 0x3F));
if (!I2C_WaitAck()) return 0;
I2C_SendByte(weekday & 0x07); // 星期不是 BCD直接写 0~6
if (!I2C_WaitAck()) return 0;
I2C_SendByte(DEC2BCD(month & 0x1F)); // bit7 世纪位固定写 0不处理跨世纪
if (!I2C_WaitAck()) return 0;
I2C_SendByte(DEC2BCD(year));
if (!I2C_WaitAck()) return 0;
I2C_Stop(); I2C_Stop();
return 1; return 1;

View File

@@ -25,22 +25,29 @@
bit PCF8563_Init(void); bit PCF8563_Init(void);
/** /**
* @brief 从 PCF8563 芯片读取当前时间 (时、分、秒) * @brief 从 PCF8563 芯片连续读取时间与日期 (时/分/秒/日/月/年)
* @param hour 小时指针 (0~23) * @param hour 小时指针 (0~23)
* @param min 分钟指针 (0~59) * @param min 分钟指针 (0~59)
* @param sec 秒钟指针 (0~59) * @param sec 秒钟指针 (0~59)
* @param day 日指针 (1~31)
* @param month 月指针 (1~12)
* @param year 年指针 (0~99代表 2000~2099不处理世纪位)
* @return bit 1: 读取成功, 0: 通讯失败 * @return bit 1: 读取成功, 0: 通讯失败
*/ */
bit PCF8563_ReadTime(u8 *hour, u8 *min, u8 *sec); bit PCF8563_ReadDateTime(u8 *hour, u8 *min, u8 *sec, u8 *day, u8 *month, u8 *year);
/** /**
* @brief 将指定的时间 (时、分、秒) 写入 PCF8563 芯片 * @brief 将指定的时间与日期一次性连续写入 PCF8563 芯片
* @param hour 小时 (0~23) * @param hour 小时 (0~23)
* @param min 分钟 (0~59) * @param min 分钟 (0~59)
* @param sec 秒钟 (0~59) * @param sec 秒钟 (0~59)
* @param day 日 (1~31)
* @param month 月 (1~12)
* @param year 年 (0~99代表 2000~2099)
* @param weekday 星期 (0~6需与软件侧约定的星期数值一致芯片本身不做校验)
* @return bit 1: 写入成功, 0: 通讯失败 * @return bit 1: 写入成功, 0: 通讯失败
*/ */
bit PCF8563_WriteTime(u8 hour, u8 min, u8 sec); bit PCF8563_WriteDateTime(u8 hour, u8 min, u8 sec, u8 day, u8 month, u8 year, u8 weekday);
/** /**
* @brief I2C 总线器件扫描诊断函数 * @brief I2C 总线器件扫描诊断函数

21
work-order/wo-06 Normal file
View File

@@ -0,0 +1,21 @@
# 工单 WO-06时间设置界面整屏闪烁改成局部刷新
**优先级**:中
**状态**:代码已完成,待实机验证只有数字局部闪烁、其它元素不受影响
---
## 问题列表
1. 时间设置界面(`set_clock_app`)调整小时/分钟时的闪烁效果,现在是每次闪烁状态翻转就整屏 `LCD_Clear()` 再全部重画,视觉上是整屏黑一下亮一下,而不是只有正在调节的那个数字在闪。
---
## 实现思路
- 根因:`App/ui.c` 的 `UI_ShowSetTimePage()` 函数开头无条件 `LCD_Clear(COLOR_BLACK)`然后重画锁头图标、电池、小时、冒号、分钟、AM/PM、日期全部元素而 `App/app_set_clock.c` 的 `SetClockApp_onRun()` 每 500ms 闪烁状态翻转一次就整个调用这个函数。
- 改法:拆成两个函数——
- 一个"整页绘制"函数,只在真正进入时间设置界面(`SetClockApp_OnStart()`)或调节字段切换(长按 CONFIRM 切换 小时/分钟 焦点)时调用一次负责画锁头、电池、冒号、AM/PM、日期这些不变的静态内容以及当前的小时、分钟数字。
- 一个"局部刷新"函数,只负责重画正在调节的那个数字所在的矩形区域(先 `LCD_FillRect` 成黑色,再按闪烁状态决定是否写回数字)`SetClockApp_onRun()` 里每次闪烁翻转只调用这个局部刷新函数,不再动其它区域。
- 需要确定小时/分钟数字在屏幕上的准确坐标和尺寸(参照现有 `LCD_ShowString16x32(23, 100, ...)` / `LCD_ShowString16x32(75, 100, ...)` 调用16x32 字体两位数字宽度为 32px),局部刷新时精确擦除这一块,不多不少。
- 涉及文件:`App/ui.c`(拆分 `UI_ShowSetTimePage`)、`App/ui.h`(新增局部刷新函数声明)、`App/app_set_clock.c`(`OnStart`/字段切换调用整页绘制,`onRun` 调用局部刷新)。

50
work-order/wo-07 Normal file
View File

@@ -0,0 +1,50 @@
# 工单 WO-07完善日期功能 (年/月/日/星期)
**优先级**:中
**状态**:待处理
---
## 问题列表
1. 软时钟(`App/clock.c`)只有时/分/秒,没有年、月、日、星期。
2. `PCF8563_ReadTime`/`PCF8563_WriteTime` 只读写时分秒三个寄存器,芯片本身支持的日/星期/月/年寄存器(0x05~0x08)从没被用过。
3. `set_clock_app` 只能调小时和分钟,没有日期可调。
4. HOME APP 和时间设置界面上显示的"星期 日-月"是写死的占位字符串 `"VEN 10-07"`/`"FRI 10-07"`,不是真实日期。
---
## 实现思路
### 1. 驱动层:`Drivers/pcf8563.c`/`.h`
PCF8563 的秒/分/时/日/星期/月/年寄存器地址是连续的(0x02~0x08共 7 字节),可以一次 I2C 连续读写全部拿到/写入,不用分开多次收发:
- 新增 `PCF8563_ReadDateTime(u8 *hour, u8 *min, u8 *sec, u8 *day, u8 *month, u8 *year)`:从 0x02 开始连续读 7 字节,`day` 掩码 `0x3F`(BCD)`month` 掩码 `0x1F`(BCD忽略 bit7 世纪位)`year` 直接 BCD 转十进制(0~99代表 2000~2099)。
- 新增 `PCF8563_WriteDateTime(u8 hour, u8 min, u8 sec, u8 day, u8 month, u8 year, u8 weekday)`:从 0x02 开始连续写 7 字节,包含星期(0x06取值 0~6不需要 BCD 转换,直接写数值)。
- 旧的 `PCF8563_ReadTime`/`PCF8563_WriteTime` 保留,内部改成调用新函数并丢弃日期部分,避免改动现有调用点(`app_home.c`/`app_set_clock.c`)。
- 世纪位(月寄存器 bit7)不处理,固定按 2000~2099 年处理,这个产品的使用年限内不用考虑跨世纪。
### 2. 软时钟层:`App/clock.c`/`.h`
- 新增全局变量 `u8 current_day`、`u8 current_month`、`u8 current_year`(0~99代表 2000~2099)、`u8 current_weekday`(0~6周日=0 或周一=0需要定一个约定建议跟 Zeller 公式的定义保持一致)。
- `Clock_IncMS()` 现有的时/分/小时进位链条(秒→分→时)后面继续接上日/月/年进位:小时满 24 归零时,`current_day++`;根据"每月天数表"(`{31, 28或29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}`2 月按闰年规则(能被 4 整除且(不能被 100 整除或能被 400 整除)2000~2099 区间内简化成"能被 4 整除"即可)判断是否要月进位、年进位(99 → 0不处理世纪)。
- 日期发生变化(不管是自然进位还是手动调节)时,用 Zeller 公式或类似算法重新计算 `current_weekday`,不依赖 PCF8563 自己的星期寄存器自增(避免手动改日期后星期跟着错乱);写入 RTC 时把算出来的星期一起写进去,保持芯片内部数据一致。
- `HomeApp_OnStart()`/相关读取 RTC 的地方,从 `PCF8563_ReadTime` 换成 `PCF8563_ReadDateTime`,一并读回日期。
### 3. 界面层:`App/ui.c`/`.h`
- 新增星期缩写字符串表(法语/英语),参照现有占位文案风格:`LUN/MAR/MER/JEU/VEN/SAM/DIM` 对应 `MON/TUE/WED/THU/FRI/SAT/SUN`,放进 `App/strings.h` 的 `WRISTBAND_STRING_LIST`。
- `UI_ShowClockPage()`/`UI_ShowSetTimePage()` 里原来直接 `Lang_Get(STR_CLOCK_DATE)` 显示死文案的地方,改成拼接"星期缩写 + 日-月两位数字"的动态字符串,用真实的 `current_weekday`/`current_day`/`current_month` 现算现拼。
### 4. 时间设置 App`App/app_set_clock.c`
- `time_edit_field` 从 1~2(时/分)扩展到 1~5(时/分/日/月/年),长按 CONFIRM 依次循环切换。
- 上下键调节日/月/年时要做合理的范围限制和循环(日 1~当月最大天数、月 1~12、年 0~99),月份变化时如果当前日超过新月份的最大天数要做截断处理(比如从 31 号切到 2 月要变成 28/29 号)。
- 局部刷新沿用 WO-06 刚做的模式,不要整屏重绘:日/月/年每个字段各自对应屏幕上一小块数字区域,只刷新当前正在调节的那一块。
- 保存(短按 CONFIRM)时用新的 `PCF8563_WriteDateTime()`,同时把算好的 `current_weekday` 一起写入。
### 5. 需要确认的点
- 星期数值约定(0=周日 还是 0=周一)以及对应的 Zeller 公式实现需要仔细算一遍避免算错,建议实现后用几个已知日期(比如今天)手动核对星期是否正确。
- 日期显示格式沿用现有"星期 日-月"(如 "MON 03-08")这种顺序,不显示年份(屏幕位置有限);如果需要年份显示,要另外找位置或者做成可选切换。