三特性: 字体分层(gen_font.py --charset + meow_font_reload_tier + font.load_full + 尺寸标记) + 全屏机制/状态栏收合(meow_ui_sys/input) + 通知#6 TF转储(CSV+10MB+开机清板载+内存表)

This commit is contained in:
MeowOS Dev committed 2026-08-30 11:46:29 +08:00
1 parent 0b4d1d3215
commit a2673d5922
18 files changed
+819 -80

No files matched your search

+196 -1
View File
@@ -24,6 +24,11 @@
#include <strings.h> /* strcasecmp */
#include <stdarg.h>
#include <time.h>
#include <sys/stat.h> /* mkdir / stat */
#include <sys/types.h>
#include <unistd.h> /* open/close/write/pread/lseek/ftruncate */
#include <fcntl.h> /* O_* */
#include <errno.h> /* errno/strerror */
#include "meow_notif.h"
#include "meow_settings.h" /* B2a:读 notify.* 配额(MEOW_OK/MEOW_ERR_*) */
@@ -89,6 +94,192 @@ static void n_strncpy(char *dst, const char *src, size_t len)
dst[len - 1] = 0;
}
/* ================= TF 卡转储(通知#6 / M2c,NOTIFICATION_SYSTEM.md §5/§8)=================
* 每条通知一行 CSV 追加到 <卡根>/MeowOS/notifications/notifications.log,并按日志总大小
* 滚动截断(保留最新 10MB)。系统文件统一放 <卡根>/MeowOS/ 下(TF_CARD_LAYOUT.md)。 */
#define MEOW_NOTIF_TF_DIR "/usr/data/mnt/sd_0/MeowOS/notifications"
#define MEOW_NOTIF_TF_LOG MEOW_NOTIF_TF_DIR "/notifications.log"
#define MEOW_NOTIF_LOG_MAX_BYTES ((size_t)10 * 1024 * 1024) /* §8:保留最新 10MB */
/* 单行 CSV 上限:app_id(32)/category(24)/summary(64)/body(96)/target_activity(48)
* 全引号转义后仍远小于此。 */
#define MEOW_NOTIF_CSV_LINE_MAX 1024
static const char *g_tf_path = MEOW_NOTIF_TF_LOG;
static size_t g_tf_max = MEOW_NOTIF_LOG_MAX_BYTES;
/* 递归 mkdir -p:沿 path 把每一级目录建出来(EEXIST 无害;无卡/只读则静默失败) */
static void tf_mkdirs(const char *path)
{
char tmp[512];
n_strncpy(tmp, path, sizeof tmp);
for (char *p = tmp + 1; *p; p++) {
if (*p == '/') {
*p = '\0';
mkdir(tmp, 0755);
*p = '/';
}
}
}
/* 全量写入(处理短写/EINTR) */
static int tf_write_all(int fd, const char *buf, size_t len)
{
size_t off = 0;
while (off < len) {
ssize_t w = write(fd, buf + off, len - off);
if (w < 0) {
if (errno == EINTR) continue;
return -1;
}
off += (size_t)w;
}
return 0;
}
/* CSV 字段:去换行(\r\n->空格,保证单行)+ 转义(双引号包裹,内部 " 翻倍) */
static void csv_field(char *dst, size_t dstsz, const char *src)
{
if (!dst || dstsz == 0) return;
size_t o = 0;
dst[o++] = '"';
for (const char *p = src ? src : ""; *p; p++) {
if (o + 4 > dstsz) break; /* 留 2 字节给翻倍引号 + 收尾引号 + NUL */
if (*p == '\r' || *p == '\n') dst[o++] = ' '; /* 去换行 */
else if (*p == '"') { dst[o++] = '"'; dst[o++] = '"'; }
else dst[o++] = *p;
}
if (o + 1 >= dstsz) o = dstsz - 1;
dst[o++] = '"';
dst[o] = '\0';
}
/* 生成一行 CSV:
* timestamp,id,app_id,category,urgency,title,body,target_activity,ongoing,media,dismissed
* title<-summary;urgency:meow_notif 结构无该字段(保持既有 API),列值留空 ""。
* 返回写入长度(不含 NUL)。 */
static size_t tf_line(const meow_notif_t *n, char *buf, size_t bufsz)
{
char a_app [2 + 2 * MEOW_NOTIF_APP_ID_MAX + 2];
char a_cat [2 + 2 * MEOW_NOTIF_CAT_MAX + 2];
char a_sum [2 + 2 * MEOW_NOTIF_SUMMARY_MAX + 2];
char a_body[2 + 2 * MEOW_NOTIF_BODY_MAX + 2];
char a_tgt [2 + 2 * MEOW_NOTIF_ACTIVITY_MAX + 2];
csv_field(a_app, sizeof a_app, n->app_id);
csv_field(a_cat, sizeof a_cat, n->category);
csv_field(a_sum, sizeof a_sum, n->summary);
csv_field(a_body, sizeof a_body, n->body);
csv_field(a_tgt, sizeof a_tgt, n->target_activity);
int r = snprintf(buf, bufsz,
"%llu,%u,%s,%s,\"\",%s,%s,%s,%u,%u,%u\n",
(unsigned long long)n->created_ms,
(unsigned)n->id,
a_app, a_cat, a_sum, a_body, a_tgt,
n->ongoing ? 1u : 0u,
n->media ? 1u : 0u,
n->dismissed ? 1u : 0u);
if (r < 0) return 0;
return ((size_t)r < bufsz) ? (size_t)r : bufsz - 1;
}
/* 滚动截断:文件超过 g_tf_max 时删除最旧行(从尾部读窗口定位第一条换行作为保留起点),
* 把结尾 ≤max 字节的完整行重写回文件头。窗口内无换行则保留整窗(只保证 ≤max)。 */
static int tf_trim(const char *path)
{
int fd = open(path, O_RDWR);
if (fd < 0) return -1;
struct stat st;
if (fstat(fd, &st) != 0) { close(fd); return -1; }
off_t size = st.st_size;
if (size <= (off_t)g_tf_max) { close(fd); return 0; }
off_t start = size - (off_t)g_tf_max; /* 保留窗口起点 */
off_t keep = start; /* 最终保留起点(行对齐后) */
int found = 0;
char chunk[8192];
for (off_t pos = start; pos < size && !found; ) {
size_t want = (size_t)((size - pos) < (off_t)sizeof chunk ? (size - pos) : (off_t)sizeof chunk);
ssize_t rd = pread(fd, chunk, want, pos);
if (rd <= 0) break;
for (ssize_t i = 0; i < rd; i++) {
if (chunk[i] == '\n') { keep = pos + i + 1; found = 1; break; }
}
pos += rd;
if (rd < (ssize_t)want) break; /* 读到文件尾 */
}
/* 重写:把 [keep, size) 拷贝回文件头,再 ftruncate 掉尾部旧数据 */
off_t remain = size - keep;
if (lseek(fd, 0, SEEK_SET) < 0) { close(fd); return -1; }
off_t wpos = 0;
char buf[8192];
while (remain > 0) {
size_t want = (size_t)(remain < (off_t)sizeof buf ? remain : (off_t)sizeof buf);
ssize_t rd = pread(fd, buf, want, keep + wpos);
if (rd <= 0) break;
if (tf_write_all(fd, buf, (size_t)rd) < 0) { close(fd); return -1; }
wpos += rd;
remain -= rd;
}
ftruncate(fd, wpos);
close(fd);
return 0;
}
/* 把一条通知追加为一行 CSV 到 TF 日志(含 mkdir -p 与 10MB 滚动截断)。
* 返回 NOTIF_OK(0);无卡/不可写/参数非法返回负(降级:仅打点,不崩)。 */
int meow_notif_dump_tf(const meow_notif_t *n)
{
if (!n) return NOTIF_ERR_INVAL;
if (!g_tf_path || !g_tf_path[0]) return NOTIF_ERR_INVAL;
tf_mkdirs(g_tf_path); /* 目录不存在则 mkdir -p*/
char line[MEOW_NOTIF_CSV_LINE_MAX];
size_t len = tf_line(n, line, sizeof line);
if (len == 0) return NOTIF_ERR_INVAL;
int fd = open(g_tf_path, O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd < 0) {
notif_log("tf: append open fail %s (%s)", g_tf_path, strerror(errno));
return NOTIF_ERR_IO;
}
if (tf_write_all(fd, line, len) != 0) {
close(fd);
notif_log("tf: append write fail %s (%s)", g_tf_path, strerror(errno));
return NOTIF_ERR_IO;
}
long size = -1;
struct stat st;
if (fstat(fd, &st) == 0) size = (long)st.st_size;
close(fd);
if (size > (long)g_tf_max) { /* >10MB:滚动截断,保留最新 */
tf_trim(g_tf_path);
notif_log("tf: rolled to <= %zuB", g_tf_max);
}
return NOTIF_OK;
}
/* 开机清板载通知:清空转储日志文件,保留目录(通知#6 / §8:启动时清板载) */
void meow_notif_clear_tf(void)
{
if (!g_tf_path || !g_tf_path[0]) return;
tf_mkdirs(g_tf_path);
int fd = open(g_tf_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) close(fd);
else notif_log("tf: clear fail %s (%s)", g_tf_path, strerror(errno));
}
/* 覆盖转储日志路径/滚动上限(宿主单测用;NULL/0 恢复默认) */
void meow_notif_set_tf_path(const char *path)
{
g_tf_path = (path && path[0]) ? path : MEOW_NOTIF_TF_LOG;
}
void meow_notif_set_tf_max(size_t max_bytes)
{
g_tf_max = max_bytes ? max_bytes : MEOW_NOTIF_LOG_MAX_BYTES;
}
/* ---- 配额读取(B2a;键缺省/取失败用默认值)---- */
static void read_quota(void)
{
@@ -113,7 +304,11 @@ int meow_notif_init(void)
g_dumped_total = 0;
read_quota(); /* B2a:notify.* 配额 */
g_inited = 1;
notif_log("init: slots=%d default dump_cb=log-only (M2c -> TF)", MEOW_NOTIF_MAX_SLOTS);
/* 通知#6:启动清板载通知 + 内存表(内存表上面已 memset;这里清 TF 转储日志,
* 保留目录)。每会话从零开始(通知会话级),不被电源事件清(before_poweroff 只管内存)。 */
meow_notif_clear_tf();
notif_log("init: slots=%d tf_log=%s (dump_cb wired by launcher -> meow_notif_dump_tf)",
MEOW_NOTIF_MAX_SLOTS, g_tf_path);
return NOTIF_OK;
}