Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/meow_launcherd.c
T
MeowOS dafe31784d 通知中心 ⑧⑨: 一键清除(保留活动通知) + 面板跟手下拉半程可回(agent-133)
- ⑧ 一键清除: mnp_clearable(ongoing/media/progress) 红线保留活动通知; 先 meow_notif 转储(notifications.log+10MB滚动) 再清空显示; 勿扰左半+清除右半(NCP_DND_SPLIT_X=fb_xres/2)对称, 命中互斥
- ⑨ 面板跟手下拉/半程可回: reveal 连续映射 1:1 跟手(mnp_drag_progress/reveal), 松手 mnp_drag_anchor(fix,40%)=1→补到1024/0→回0收回; 与⑦行左滑/列表滚动/meow_edge_back 互斥; 复用快照+局部刷(panel帧/新露出条)
- meow_ui.a 零改动(d249c6e6); launcher=b140e9e5; 宿主189项ok; 新增0warn
- NOTIFICATION_SYSTEM §9 增 ⑧⑨; MEOW_APP_SDK §5.7 收合动画落点标注
2026-08-30 17:45:15 +08:00

9573 lines
518 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* meow_launcherd.c — R3 Pro II Plan B zero-patch app launcher daemon.
*
* M2.53 亮度接管修正(开机/切到 MeowOS 亮度被重置为 25/16):
* - 根因:HibyOS 播放器运行/恢复画面会把它自己的背光下限(16/25)写进 sysfs,
* MeowOS 主屏接管时又以 read_brightness() 读到的该值当「当前亮度」
* (saved_brightness / meow_saved_brightness),从不重应用用户 display.brightness,
* 故用户设的低亮度(<25,最低到 1)被覆盖。
* - 修法:run_menu 主屏接管(had_player==0,播放器已死=真接管)时调
* apply_user_brightness():读 settings display.brightness(经新导出的
* meow_settings_brightness_clamped(),clamp 下限 brightness_min 默认 1、上限 100)
* → write_brightness 强写回,覆盖播放器残留的 16/25。
* - 边界:播放器会话期间的覆盖菜单(had_player==1)不压回——播电影时亮度归播放器管;
* ensure_backlight/restore_backlight 仍用 saved_brightness(覆盖会话=播放器真实值),
* 不改为用户值,避免菜单关闭瞬间抢播放器背光。
*
* M2.46-2 状态栏 5 问题(2026-08-29):
* ① 亮度图标主题色(on_surface_variant 灰 → on_surface 主色)+ 拖动亮度滑条时状态栏百分比实时跟:
* 根因 s_commit brightness 分支只 write_brightness 未同步 g_sys.brightness/未重绘;补
* g_sys.brightness=v + paint_sb_right()(与随后 meow_sys_refresh 从 sysfs 回读一致不打架)。
* ② 右簇音/电顺序互换:最右由「音量」改「电量」(绘制顺序先电量后音量,右→左:电→音→蓝牙→WiFi→亮度)。
* ③ 图标视觉对齐:sb_blit_icon 按 ink 边界居中 + 主图标等比等大(SB_ICON_INK=16),音量不再「小一圈/
* 下边距」;ink 垂直中心对齐状态栏文字中心(g_sb_icon_cy=SB_ICON_CY=14,见 sb_text_y),图文同中线。
* ④ WiFi 图标随信号分档:新增 wifi_signal_level() 读 /proc/net/wireless wlan0 level(dBm),
* ≥-60 强 / [-60,-75) 中 / <-75 弱,选 SB_ICON_WIFI_STRONG/MEDIUM/WEAK(新位图追加进 sb_icons.h);
* 强弱档保留自然 ink(少一格即语义),满格/强档走等大;wifi_on 为假不画(保持现状)。
* (⑤ 数字随亮度刷新已并入①。)
*
* M2a 通知存储 + 原生 IPC(2026-08-29,阶段 D 链开端):
* - 新增 meow_notif.h/.c(NOTIFICATION_SYSTEM.md §4/§7/§8):通知结构对齐 freedesktop
* + MeowOS 扩展(target_activity/progress);内存配额(每 App ≤ notify.categories 类型、
* 每类型活跃 ≤ notify.category_quota、媒体每 App ≤ notify.media_quota,系统事件作
* app_id="system" 同走配额);TTL 10s 转储钩子(§8,TF 落盘 M2c 后置,预留 notif_dump_cb);
* 满丢该类型最旧条(ring 语义);ONGOING 常驻(转储后仍保留,服务死随清)。
* - meow_settings 默认表新增 notify.categories/category_quota/media_quota(B2a 后端)。
* - IPC:ipc_dispatch 新增 MSG_NOTIFY→MSG_NOTIFY_ACK、MSG_NOTIFY_DISMISS→MSG_ACK、
* MSG_NOTIFY_PROGRESS→MSG_ACK(实时进度仅改内存活跃槽,不落盘);
* MSG_NOTIFY_LIST 为守护→App 下行(M5 接入)。meow_ipc 测试钩子新增 notify/dismiss 子命令。
* - meow_notif_before_poweroff() 挂入 sys_do_reboot():正常电源事件前先转储未上卡通知再清板载。
* - 通知模型补全(2026-08-30,用户定案):SDK→IPC→存储→dump 全链路补 urgency(0-3)/
* icon(空→按 app_id 回退该 app 默认图标);progress 实时仅改内存(MSG_NOTIFY_PROGRESS),
* TF 快照只在创建/结束(100% 或 dismiss)(meow_notif_dump_tf 写当时值)。
*
* B2a 统一系统设置后端(2026-08-29):
* - 新增 meow_settings.h/.c(SYSTEM_SETTINGS.md §3/MEOWOS_API §2):全部系统设置
* 抽象为「名称/类型/默认/取值域」扁平表,单点存板载 /usr/data/meow/settings.conf;
* clamp 集中 set,变更即广播 listener,提供 get/set/枚举/list/导入导出。
* - settings_load()(手写解析 settings.conf)→ meow_settings_init() + 缓存同步;
* settings_save_volume() → meow_settings_save();settings_volume 等全局仅作缓存,
* 由 meow_settings 的 set 回调/加载后同步,DAC/状态栏/息屏/刷新率调度照旧引用。
* 音量渐变(1% 爬)仍只在渐变结束落盘一次,不逐 1% 写盘(UBIFS 低频)。
* - 本批只建后端 + 迁移 audio/display 已有设置;实体设置页(B2b)/Web 面板(C)后复用。
*
* M2.39 设置页第二批 7 问题(2026-08-29,版本 0.7.34):
* ① 亮度滑条 1/4 卡 25:设备 settings.conf 残留旧 brightness_min=25(覆盖默认 1)致
* clamp 回 25。默认表维持 1;加载时把恰好为旧默认 25 的 brightness_min 迁移为 1 并落盘,
* 亮度滑条可调到「极暗但可见」的 1(防黑屏下限仍由 brightness_min 控)。滑条上限 101→100
* (101 闪屏)。③ 子菜单值刷新核验:set 后 after_modal 整帧重绘,s_display_value 用 get_enum/
* get_str 重读,值即时更新(已核验,无需额外改动)。
* ④ 设置页音量键不再当光标移动(用户约束)——改调系统音量(vol_ramp_begin/tick 线性渐变写
* DAC + 刷状态栏 VOL);仅 run_settings_page 移除,电菜单/应用列表的系统级音量导航不动;
* 选择器子模态内部仍保留音量键=移动选项高亮(属「选值」另一语义)。
* ⑤ 新增「网络」分组:WiFi 开关(network.wifi_enabled)+已连接 SSID+已保存网络(下拉 connect
* wpa_cli select_network)+附近设备(WiFi scan/scan_results)+蓝牙开关(network.bluetooth_enabled)
* +已配对设备;读取全部经带超时 popen(wpa_cli/bluetoothctl),扫描异步触发,绝不阻塞主循环;
* ensure_wifi() 尊重 wifi_enabled(关=0 不再自动连网)。⑥ 设置页支持左缘右滑返回(复用共享
* 判定 meow_edge_back(),与 SDK 全局左缘返回同一语义)。⑦ 底部「返回」改用像素左向箭头+「返回」,
* 点阵回退字库缺 '‹' 字形不再渲染方框。
*
* M2.40 设置页网络/子菜单 5 问题(2026-08-29,版本沿 0.7.34):
* ① 子菜单点值无反应(严重):s_dd_modal 触摸解析用裸 ddx/ddy 在按下边沿锁原点,踩 goodix
* 「BTN_TOUCH 先于 ABS」时序 → 锁到 -1/上一手势残留 → 点选项被判「点卡片外=取消」→ 菜单
* 关闭无变化。改为抬手用抬手时刻最新 abs 坐标判定(与 meow_touch mt_up 同源),点值真正生效。
* ② WiFi 扫描把 MAC 当独立设备:net_wifi_scan 按 BSSID 去重(同 BSSID 只留一条,优先具名,
* 后到且有 SSID 覆盖先到空 SSID 行),隐藏网络显示「(隐藏网络)」,不再与具名条目重复。
* ③ 已保存/已连接中文 SSID 乱码:wpa_cli list_networks/status 对非 ASCII 输出 \xNN 转义文字,
* 新增 wpa_decode_ssid() 还原成真实 UTF-8 字节存入 name。
* ④ 网络分组命名/顺序:SMENU「附近设备」→「附近WiFi」(其顺序本就正确);蓝牙「蓝牙设备」改
* 读附近扫描发现(已配对+未配对),新增 net_bt_scan_trigger()/net_bt_scan():后台有界驱动
* bluetoothctl(scan on→sleep 4→scan off→devices→quit)写 /tmp 缓存,system() 尾部 & 立即
* 返回,绝不死循环;扫描中显示「扫描中...」,无缓存退回已配对列表。
* ⑤ WiFi 扫描触发本就立即返回(wpa_cli scan),net_wifi_scan 读超时 1800→2500ms(留扫描出结果余量)。
*
* M2.41 B2b 主题三方式(手动深色/手动浅色/时段自动,2026-08-29,版本 0.7.38):
* - 新增设置键 display.theme_mode(enum manual/auto,默认 manual)+ 深色时段两键
* display.theme_dark_start/end(int 分钟 0-1439,默认 0);display.theme 保留为手动深浅。
* - s_ui_theme:manual=直读 display.theme(原行为不变);auto=读时段两键+本地分钟,按
* [start,end) 判定(start>end 跨午夜;start==end 视为未配置 → 回退 manual 读 display.theme)。
* - 设置页「主题」行改绑 display.theme_mode(手动/时段自动),新增「主题深浅」(display.theme)
* 与「深色时段开始/结束」两行(SC_SLIDER 0-1439,step 15,显示 HH:MM;start==end 显示「未设置」)。
*
* M2.29 电源菜单加「升级模式」按钮(2026-08-29,版本 0.7.24):
* - 电源菜单 PM_NUM 4→5,新增「升级模式」按钮(index 3):动作 =
* system("/usr/bin/bootmode.sh Recovery")(写 mtd5=ota:kernel2)+ sys_do_reboot(RESTART),
* uboot 读 mtd5=ota:kernel2 引导 kernel2 升级模式(自动刷 TF 卡 r3proii.upt)。
* - 布局 2×2 → 2 列×3 行(行 y 300/420/540),取消仍默认选中(index 4,右下=安全项)。
* - MEOWOS_VERSION 0.7.23 → 0.7.24。
*
* M2.25 Launcher 统一触摸接口 meow_touch(2026-08-29,版本 0.7.20):
* - 新增 meow_touch.h/.c:轻量、不依赖 SDK 的触摸手势解析(goodix 时序免疫:BTN_TOUCH=1
* 不锁原点,等按下后首个有效 ABS 才锁;抬手位移 >= MEOW_TOUCH_SWIPE_THRESH 判滑动)。
* - 电源菜单 show_power_menu / 确认框 confirm_reboot_player 触摸解析迁到 meow_touch:
* 修「确认框/电源菜单点两下」—— 原来两者各自独立解析,按下边沿用当时 t_last(可能
* 残留 -1/旧坐标)锁原点 → 抬手命中失败;现统一原点锁定,点一下即确认。
* - 黑屏修复:电源菜单关闭(尤其触摸点「取消」)后,kd_* 防抖状态因阻塞式
* show_power_menu() 冻结,返回后 stable 与 raw 错配 → 误判电源「单击」→ meow_toggle_panel
* 关掉面板(黑屏且难唤醒)。菜单关闭后 memset 重播 kd_* + 以当前 raw 对齐 m_prev_*。
* - 应用运行期电源菜单(M2.24 watchdog 路径)关闭后重播 save_pow/save_keys 防误判 key_edge。
* - 应用列表(run_menu 内联解析,M2.23 已修)保留内联(含 cooldown/滑动日志/行带判定),
* 语义与 meow_touch 一致。
* - 版本 0.7.19 → 0.7.20。
* M2.24 全面按键接管(2026-08-29,版本 0.7.19):
* - launcher 应用运行期(waitpid 轮询)消费 watchdog 的电源长按菜单请求
* (/tmp/meow_power_menu):SIGSTOP app → 写 /tmp/meow_power_menu_active(watchdog 让位)
* → 弹统一电源菜单 show_power_menu(复用,一处绘制)→ 关菜单 → SIGCONT app。
* 电源单击=息屏/亮屏、音量=调音量、播放/上下曲=抓消费由 watchdog 在应用期统一处理;
* launcher 的 run_menu 菜单期语义不变(KEY_CONTROL_DESIGN §3 统一监听点)。
* - launch_app 末尾 settings_load():应用期 watchdog 可能改音量(settings.conf 同源),
* 返回菜单前 re-load,保证菜单音量与真实 DAC 一致。
* - 版本 0.7.18 → 0.7.19。
* M2.23 菜单触摸判定修复(2026-08-29,版本 0.7.18):
* - 手势原点锁定:BTN_TOUCH=1 只标「已按下」,t_down_x/y 延迟到本次按下后首个有效坐标才锁,
* 杜绝上一手势残留(-1/旧坐标)当手势起点(修「按下坐标=-1」)。
* - 滑动不启动:抬手位移 >= MENU_SWIPE_THRESH(65) 判为滑动,不 launch、不做行判定
* (修「位移 85 被当 tap 命中行」;为将来左右滑切屏预留,本轮只判滑动)。
* - 行判定收紧:仅当 tap(位移<阈值)且 ty 落进行带才 launch_app;行带外 tap(空白)不触发。
* - 版本 0.7.17 → 0.7.18。
* M2.16 无闪烁渲染 + 刷新率调度 + 按键防抖/单击长按(2026-08-29,版本 0.7.11):
* - 无闪烁渲染地基:RAM 离屏后备缓冲(现由 SDK 提供),所有绘制
* (put_px/meow_font/dim_screen/draw_menu/statusbar/电源菜单/确认框)一律画进 SDK back_buf;
* 呈现统一走 SDK meow_ui_flush() = FBIO_WAITFORVSYNC 等垂直同步 + 一次 memcpy(dirty 区或全帧)
* 到 fb0(buffer0, yoffset=0)。fb 从不被渐进绘制、绝无中间态/无撕裂。空闲无 dirty → 不画不拷。
* - 脏区追踪 meow_dirty_add(x,y,w,h)(桥进 SDK 脏追踪):矩形合并集(相交吸收为 bbox,
* 超限转全屏);状态栏时钟/右侧信息格只在"那个格"变化时标脏(paint_sb_clock/paint_sb_right)。
* - 刷新率调度:display.refresh_rate(60/30/15,默认 60,屏幕 62Hz 锁 60 不逼瓶颈) +
* display.refresh_always_max(1=恒 max 不降档)。A-1/A-2 起该帧率门控+自适应降档 governor
* 归口 SDK present(meow_ui_set_refresh_rate/set_refresh_always_max),launcher 不再自带一套。
* - A-1/A-2(0.7.23):launcher 与 app 统一渲染栈(SDK back_buf + meow_ui_flush present + 帧率门控),
* launcher 不再自建 back_buf/meow_present/refresh_governor;播放器 fb 双缓冲交互(fb_backup_player/
* fb_norm_buf0/FBIOPAN/yoffset)保留——与上屏 present 分属不同层。
* - 按键防抖 + 单击/长按:每键值须稳定 KEY_DEBOUNCE_MS(40ms) 才认(抗 ADC 抖动"点一下跳5-6下");
* 按压 <CLICK_MS(400ms)=单击一次,≥=长按(连续;如音量长按连续渐变)。音量渐变只在稳定边沿触发。
* - 本批次不做:通知中心 UI / MPRIS / 服务保活(后续里程碑)。播放器不动。
*
* 阶段 D · M0+M1:IPC 骨架 + 路由表(FRAMEWORK_DESIGN §1/§2/§9,版本 0.7.10):
* - M0 IPC 骨架:AF_UNIX /usr/data/meow_ipc.sock(unlink+bind+chmod0666+listen4),
* IPC 线程只读帧+解析+入队(SPSC 无锁环,__sync 原子),eventfd 唤醒主循环;
* 帧解析 = 16B 头(magic=0x4D4F5701/version=1/type/req_id/len,LE)→ 校验 → 读负载。
* MSG_HELLO→ACK / 未知→MSG_ERR;主循环 meow_ipc_process() 每 10ms 排空(零阻塞,
* 事件fd 仅为低延迟;即使 eventfd 失败,10ms 轮询仍保证排空)。日志埋点。
* - M1 路由表 + 解析:app.json 扁平行 route.<id>=path / framework=true /
* service.<svc>=mode:restart:max:window_ms / icon;scan_apps 解析入 g_accts 注册表;
* MSG_ACT_DECLARE(运行时并集覆盖)/ACT_QUERY/ACT_RESULT;resolve_chain(app,activity)
* 返回 [root..target] 链路与深度(route 缺失→深度1兜底;activity 未注册→MEOW_ERR_NOENT)。
* - 本阶段不做:通知存储(M2)、导航栈/launch_activity(M3)、UI(阶段 B)。播放器不动。
* 测试钩子:meow_ipc(patch_v2/meow_launcherd/meow_ipc.c,与 evtool 同定位)+
* launcher --ipc-only N --ipc-sock <path>(qemu sim 免 fb/输入)。
*
* M2.14 音量渐变批次(2026-08-29,版本 0.7.9):
* - audio.volume_step 默认 2 → **1**(SYSTEM_SETTINGS.md:默认 1,可配 1-5,clamp 上限 5)。
* - 线性渐变:settings_volume_adj(±step) 不再一次跳 N%,改起 vol_ramp_begin/tick 状态机——
* 每主循环 tick 按 **1%** 递增爬向目标(VOL_RAMP_MS=15ms 墙钟节流,不阻塞主循环),
* 每 1% 写一次 meow_dac_vol_write(真实 DAC);step>1 时不突兀、step=1 一步即达。
* - 长按持续渐变:到目标若按键仍按住则再延一段(vol_ramp_target += dir*step)直到松开/到上限;
* 连按=每次边沿叠加继续。落盘只在渐变结束一次。状态栏 VOL 随渐变刷新(显示爬升值/最终值)。
* MEOWOS_VERSION 0.7.8 → 0.7.9。
*
* M2.13 真实 DAC 音量批次(2026-08-28,版本 0.7.8):
* - 软件占位音量 → 真实 CS43198 DAC 音量:settings_volume_set/adj 每次变更即经
* amixer cset(SNDRV_CTL_IOCTL_ELEM_WRITE)直写 /dev/snd/controlC0 的
* "Left/Right Playback Volume"(numid 1/2,0-255 映射 0-100)到双片 CS43198。
* 新增 meow_dac_vol_write()/meow_vol_get()/meow_vol_set();进 MeowOS 时
* meow_sys_refresh 同步 DAC 至当前档。尊重上限:audio.volume_max(默认 100)clamp;
* 跳步更细 audio.volume_step(默认 2,可配回 5)。
* - 读回局限(§18.39):codec 驱动音量 get 为 dummy 恒 0(amixer cget 死镜像成因),
* /dev/i2c-3 被驱动占用(i2cget busy),故 meow_vol_get() 返回我方写入跟踪值。
* MEOWOS_VERSION 0.7.7 → 0.7.8。
*
* M2.12 迁移批次(2026-08-28,版本 0.7.7):
* - 外围 CLI(curl/socat/tmux/jq/rsync/nano/htop/ncdu/file/xxd)自 rootfs /usr/bin 迁至
* TF 卡 MeowOS/opt/bin/(Tools 皆静态 MIPS,无 .so 依赖);MeowOS 启动时卡在则 PATH
* 前插 /usr/data/mnt/sd_0/MeowOS/opt/bin(卡不在跳过)。固件 rootfs 瘦身(目标 ≤42MB)。
* - MeowAPPs 数据目录(exFAT 无符号链接 → 用环境变量,不做 symlink):launch_app 启动前
* 调 meow_app_prepare(exec) 建 MeowData/<pkg>/{data,cache}(如缺),并向子进程注入
* MEOW_DATA_DIR/MEOW_CACHE_DIR(与 MEOW_LOG_DIR 同族)。新增 .mpk 安装/导出接口
* meow_pkg_install/export + CLI 钩子 --pkg-install/--pkg-export(本期接口,UI 后期)。
* - 零卡依赖审查:settings 在 /data、字库/应用/日志/opt 均可缺省回退;拔卡不死、插卡自建。
* MEOWOS_VERSION 0.7.6 → 0.7.7。
*
* M2.11 修复批次(2026-08-28,版本 0.7.6):
* - A 系统状态全量重抓:新增 meow_sys_state + meow_sys_refresh(),每次进 MeowOS
* 重抓 电量/WiFi/蓝牙/音量/亮度/存储/系统;主循环 10ms 热键轮询绝不读系统态;
* 全量进系统一次 + 1s 轻刷时钟/电量/WiFi;状态栏统一读快照(不再 amixer 死镜像)。
* - B 电源菜单「按电源键」触发关机:电源=按当前项确认;默认光标改「取消」(安全项,
* 避免未导航按电源即关机);取消→只关菜单绝不关机;关机/重启/回HibyOS→按对应项才执行;
* 菜单里按电源键本身绝不触发 poweroff。菜单返回后清 pwr_hold_at/pwr_menu_state,
* 杜绝「按住电源开菜单→确认→仍按住→run_menu 8s 强关」残留覆盖。
* - C TF 卡热插拔最佳努力:拔出检测(/proc/mounts + /dev/mmcblk0 消失)→ 标记无卡、
* 停止扫描、优雅中止不崩;插入检测 → 自动挂载 + 重扫 MeowAPPs + 重载字库 + 日志迁回;
* 写日志/挂载前确认,失败回退内存;扫描遍历安全终止防死循环。
* - D 运行中应用卡拔:前台 MeowAPP 二进制/资源在卡上,卡被拔 → 启动器主动 SIGTERM
* 应用(干净退出,避免等 SIGBUS 崩),显示「卡已移除,应用已结束」,回菜单。
* - E:音量键调软件音量(settings audio.volume 0-100 步进5 clamp,持久化);
* 蓝牙读实际 powered(关=灰/不显示);WiFi 读实际(operstate);唤醒偶发加日志。
* MEOWOS_VERSION 0.7.5 → 0.7.6。
*
* M2.10 修复批次(2026-08-28):
* - Bug:电源菜单「回到 HibyOS」点一下就关机。根因:电源菜单 8s 强关(按住电源≥8s 绕过)与
* 用户点选按钮竞争 —— 8s 检查在触摸处理之前,用户按住电源开菜单后(仍按着)点「回到HibyOS/
* 重启」,8s 一到先触发强关(poweroff)而非执行所选动作。修法:用户一旦与菜单互动(音量导航/
* 触摸/播放/电源确认)置 pm_interacted=1,8s 强关取消;8s 强关移到输入处理之后。另复核
* 「回到HibyOS」动作即 set_boot_default_hiby()+sys_do_reboot(RESTART)(RESTART 已真机确认会
* 重启,非关机)。版本 0.7.5。
*
* M2.9 修复批次(2026-08-28):
* - Bug 2:电源菜单 POWER MENU 标题 y=300 与 2×2 按钮(pm_y0 顶缘 320)重叠被遮,
* 标题上移到 y=150(标题在上、按钮在下,不重叠)。
* - Bug 4:「返回播放器」确认框是/否按钮主循环内每 10ms 全刷 → 频闪;改为 dirty 绘制
* (仅 sel 变化才重画两钮),提示行移出循环只画一次。
* - 复核电源菜单动作直连 syscall(SYS_reboot)(M2.7 已改,本批确认路径一致)。
* 版本 0.7.4。
*
* M2.8 修复批次(2026-08-28):Bug 3 残留 —— MeowOS 主屏用电源键息屏后再按电源,
* 只有背光无画面(M2.7 加了 fb_norm_buf0 仍黑)。根因:fb_norm_buf0 在缓存
* fb_active_yoffset==0 时短路不再重发 FBIOPAN_DISPLAY,而 FBIOBLANK(4)->(0) 后 LCDC
* 扫描地址可能已复位到非 buffer0/失效位置,缓存仍是 0 导致真正的 re-pan 从未发出;
* 且唤醒不是 draw 事件,buffer0 内容可能为空。修法:新增 fb_pan_buf0_force(无条件
* 重发 pan)+ meow_repaint_surface(唤醒时把当前界面整体重绘到 buffer0 再强制 pan),
* 电源菜单/确认框也在「息屏态打开」时强制 re-pan。版本 0.7.3。
*
* M2.2 修复批次(2026-08-28):
* - MeowOS 常驻主界面:启动即自绘应用列表主界面(buffer0+pan0,不等热键),
* 绝不遗留播放器死帧;播放器已死时不备份/不冻结/不 SIGCONT、不恢复死帧。
* - BACK TO PLAYER(播放器已死)→ 显示「重启后返回播放器」提示并留在主界面。
* - MeowOS 系统拔营:无播放器时由 launcher 接管 TF 卡挂载(/dev/mmcblk0 ->
* /usr/data/mnt/sd_0, exfat)与 WiFi 联网(/usr/bin/wifi_up.sh),后台周期重试。
*
* Design (see ANALYSIS.md §18):
* - PASSIVE multi-reader: never EVIOCGRABs, never injects events.
* Physical key events multicast to all evdev readers on this kernel,
* but injected events (write()) update device state yet are NEVER
* delivered to any client (vendor input-subsystem quirk, verified).
* => keys are read via EVIOCGKEY state polling (immune to any grab,
* exact mechanism the player itself uses for the power key), touch is
* read via normal event reads on event1 (not grabbed by the player).
* - Hotkey (v6.5): HOLD power, then a SINGLE VOL_DOWN tap 60-2500ms after
* the power-down edge opens the menu. The player sees the power press (its
* screen-off flow starts — accepted side effect; the menu open undoes it
* via the verified unblank + bring-backlight-only-if-dark logic) and the
* VOL_DOWN tap (volume -1 — accepted). Legendary sequence mode
* (N quick taps of one key, default VOL_UP x3) remains available via
* meow.conf hotkey=seq; the seq taps raise volume, snapshotted and
* restored on trigger. A two-key chord on event2 is impossible (its four
* keys share ONE ADC channel), and the PREVIOUS key's GPIO (PC28) never
* fires on this unit (§18.12). Configurable via MeowAPPs/meow.conf:
* hotkey=power_voldown|seq / hotkey_seq=volup,volup,volup /
* hotkey_seq_gap=600. Inside an app, VOL_DOWN x3 quick taps force-kill
* the app (volume restored too).
* - Menu: SIGSTOP hiby_player, bring the backlight back ONLY if the panel
* is dark (brightness 0 / blanked — the player's own screen timeout or
* standby); a normal brightness is left untouched (Bug A: the old
* force_backlight() yanked the brightness to 101 on every menu open),
* draw app list on fb0 (scan /usr/data/mnt/sd_0/MeowAPPs/<id>/app.json,
* 5 edge cases), navigate with touch or keys, launch app via
* fork/execve/waitpid, spawn meow_watchdog <pid> for the virtual back
* key, then SIGCONT.
* - Double-buffer (v6.6): fb0 is double-buffered — FBIOGET_VSCREENINFO
* reports yres_virtual=1440 (2x720), smem_len=1382400 (exactly 2
* frames), ypanstep=1 (verified on device), and the DISPLAYED buffer is
* selected by fb_var.yoffset via FBIOPAN_DISPLAY (0x4606, verified to
* flip 0<->720). The player's power/screen-off redraw leaves yoffset on
* a back buffer, so the menu drawn ONLY at mmap offset 0 (buffer0) can
* land on the buffer the panel is NOT showing -> menu invisible, screen
* frozen on the player's last frame (the v6.5 bug). Fix: at menu open
* snapshot the player's displayed frame (the buffer at the current
* yoffset) into a heap backup, pan the display to buffer0 (what this
* code AND every app draw to — apps mmap exactly one frame at offset 0
* and never pan), draw the menu/app-list to buffer0, and on session end
* copy the backup back to the player's buffer + pan to the original
* yoffset so a SIGSTOPped player that does not redraw (event-driven,
* §18.15) still shows its own frame. fb is mapped over the FULL virtual
* framebuffer (both frames), not just one.
* - Backlight discipline (2026-08-28 hotfix): the launcher NEVER touches
* FBIOBLANK outside the menu. The player owns the panel blank state:
* the bkl_time ("背光时间") screen timeout writes brightness 0 +
* FBIOBLANK(4) and the player stops drawing while it believes the panel
* is blanked. The old backlight keeper restored brightness +
* FBIOBLANK(0) behind its back, which wedged the player's display state
* machine (UI frozen, keys dead — device log: keeper fired at 40.9s
* post-boot, player wedged). Do NOT reintroduce unblanking outside the
* menu. Inside the menu: unblank_panel() runs AFTER stop_player() so
* the frozen player cannot re-blank behind the menu (v6.2 hotfix —
* unblanking while the player was still running let it re-blank, so the
* menu was drawn on a black panel); on session end restore_backlight()
* re-blanks first (if the menu opened while blanked) and only then the
* player resumes, keeping its state machine consistent. ensure_backlight
* / restore_backlight only ever write brightness (harmless; ignored
* while the panel is blanked) and bl_power.
* - Player watchdog: SIGSTOP does not exit the process, so the
* hiby_player.sh "exit -> reboot" watchdog never triggers.
* - UI: pure black background by default (configurable via
* MeowAPPs/meow.conf bg_color=RRGGBB), slim 20px status bar with
* clock / battery / wifi.
* - Logging: unified to the TF card MeowOS/logs/meow_<bootts>.log
* (boot timestamp from wall-clock minus uptime); when the card is absent
* (S90 starts before the card mounts) logs go to a bounded /tmp boot
* buffer (/tmp/meow_boot.log, recoverable over SSH, survives daemon
* restarts) and are moved into the card log automatically once the card
* mounts (periodic retry in the main loop — no menu needed). NEVER write
* onboard storage, which is nearly full. Watchdog gets
* MEOW_LOG_FILE, apps get MEOW_LOG_DIR via the environment.
*
* Static MIPS32r2 musl build:
* ~/Project/R3ProII/.tools/zig-mipsel-cc -Os -static meow_launcherd.c -o meow_launcherd
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <dirent.h>
#include <stdint.h>
#include <time.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <poll.h> /* 0-warning:musl 把 <sys/poll.h> 重定向到 <poll.h>,用后者消除 -W#warnings */
#include "meow_touch.h" /* M2.25:Launcher 统一触摸接口(电源菜单/确认框复用) */
#include "pm_icons.h" /* M2.32:Material Symbols rounded 图标(电源菜单圆钮,alpha 位图+主题色) */
#include "sb_icons.h" /* MD3 状态栏/通知中心图标(Material Symbols rounded,32x32 单通道 alpha) */
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/syscall.h> /* Bug 2:直连 reboot(2) syscall 用 */
#include <pthread.h> /* M0:IPC 线程 */
#include <sys/eventfd.h> /* M0:IPC→主循环唤醒(EFD_NONBLOCK) */
#include <sys/socket.h> /* M0:AF_UNIX IPC */
#include <sys/un.h> /* M0:sockaddr_un */
#include <linux/input.h>
#include "meow_font.h" /* M2.6: 预渲染位图字体(agent-28 交付;对象已打包进 meow_ui.a,launcher 不再单独链 meow_font.a) */
#include "meow_ipc.h" /* M0+M1:IPC 协议帧/消息/负载定(与 meow_ipc 工具同源) */
#include "meow_settings.h" /* B2a:统一系统设置后端(SYSTEM_SETTINGS.md §3/MEOWOS_API §2) */
#include "meow_notif.h" /* M2a:通知存储 + 分发(NOTIFICATION_SYSTEM.md §4/§7/§8) */
#include "meow_ui.h" /* A-1/A-2:launcher 用 SDK 渲染栈(meow_ui_fill_rect/text/flush/theme) */
#include "meow_ui_internal.h" /* A-1/A-2:launcher 与 SDK 共用 back_buf + 手动脏模型桥接(mark_full/back_buf) */
#include "meow_notif_panel.h" /* 阶段B:通知中心面板纯逻辑(header-only,可单测) */
/* ---- paths ---- */
#define DEV_POWER "/dev/input/event0" /* md-gpio-keys: KEY_POWER, KEY_PREVIOUS */
#define DEV_KEYS "/dev/input/event2" /* jz adc keyboard: VOL, PLAY, NEXT */
#define DEV_TOUCH "/dev/input/event1" /* goodix-ts (not grabbed, shareable) */
#define MEOWAPPS "/usr/data/mnt/sd_0/MeowAPPs"
#define LOG_DIR_CARD "/usr/data/mnt/sd_0/MeowOS/logs" /* TF card logs(系统约定:系统文件放 MeowOS/ 下) */
#define PID_PATH "/usr/data/meow_launcherd.pid"
#define WATCHDOG1 "/usr/data/meow_watchdog"
#define WATCHDOG2 "/usr/bin/meow_watchdog"
/* M2.24 全面按键接管:watchdog(应用运行期)检测到电源长按≥3s 写此标记,launcher 在
* waitpid 轮询中消费 → SIGSTOP app + 弹统一电源菜单;菜单期 launcher 写 powmenu_active
* 让 watchdog 让位,菜单关后清除再 SIGCONT app。 */
#define POWMENU_MARKER "/tmp/meow_power_menu"
#define POWMENU_ACTIVE "/tmp/meow_power_menu_active"
#define BACKLIGHT "/sys/class/backlight/backlight_pwm0/brightness"
#define BACKLIGHT_POWER "/sys/class/backlight/backlight_pwm0/bl_power"
#define FB_DEV "/dev/fb0"
#define MOUNTS "/proc/mounts"
/* ---- key codes ---- */
#ifndef KEY_POWER
#define KEY_POWER 116
#endif
#ifndef KEY_PREVIOUSSONG
#define KEY_PREVIOUSSONG 165
#endif
#ifndef KEY_VOLUMEDOWN
#define KEY_VOLUMEDOWN 114
#endif
#ifndef KEY_VOLUMEUP
#define KEY_VOLUMEUP 115
#endif
#ifndef KEY_PLAYPAUSE
#define KEY_PLAYPAUSE 164
#endif
#ifndef KEY_NEXT
#define KEY_NEXT 163
#endif
/* ---- tuning ---- */
#define SEQ_DEFAULT_TAPS 3 /* default hotkey: VOL_UP x3 quick taps */
#define POLL_MS 10
/* Single source of truth for the MeowOS build version. The on-screen label
* ("MeowOS v<MEOWOS_VERSION> dev", bottom-right of the app list) and the
* etc/meowos_version file that build_fw_apps.sh greps from this line must both
* agree — change the version HERE only, never in two places. */
#define MEOWOS_VERSION "dev" /* M2.50:版本号唯一来源 = /etc/meowos_version(运行时 load_meowos_version 读);本宏仅作文件缺失时占位,不再当实际版本,升级只改 /etc/meowos_version */
#define MAX_APPS 8
#define TOAST_MS 1500
#define LOG_SIZE_LIMIT (1u << 30) /* 1 GB total on the TF card */
/* 阶段 D · M1:Activity 路由注册表(g_accts)容量。route=app.json/声明的路由行,
* svc=service.<svc> 行(M1 仅解析存储,保活 M4 实现)。 */
#define MEOW_MAX_ACTS 16
#define MEOW_MAX_ROUTES 8
#define MEOW_MAX_SVCS 8
/* ---- event codes / ioctls ---- */
#define BTN_TOUCH 0x14a
#define ABS_X 0x00
#define ABS_Y 0x01
#define ABS_MT_POSITION_X 0x35
#define ABS_MT_POSITION_Y 0x36
#ifndef EV_KEY
#define EV_KEY 1
#endif
#ifndef EV_ABS
#define EV_ABS 3
#endif
#ifndef EVIOCGKEY
#define EVIOCGKEY(len) _IOC(_IOC_READ, 'E', 0x18, len)
#endif
#define KEYBUF_WORDS 32 /* 1024 bits, covers KEY_MAX 767 */
/* ================================================================== */
/* unified logging: TF card MeowOS/logs/meow_<bootts>.log */
/* - no TF card at startup (S90 runs before the card mounts) -> */
/* bounded /tmp boot buffer (/tmp/meow_boot.log): recoverable */
/* over SSH, survives daemon restarts within the boot session */
/* - log_poll_card() (called periodically in the main loop) opens */
/* the card log as soon as the card mounts and moves the /tmp */
/* buffer into it — no menu needed */
/* - NEVER write onboard storage (nearly full) */
/* ================================================================== */
#define BOOT_BUF_TMP "/tmp/meow_boot.log"
#define BOOT_BUF_MAX (256 * 1024)
static int log_fd = -1; /* file log fd on the TF card */
static char log_dir[256] = ""; /* "MeowOS/logs" on card, or "" */
static char log_file[256] = ""; /* full path of the boot log file */
static int card_available(void)
{
/* TF card mounted? */
FILE *f = fopen(MOUNTS, "r");
if (!f) return 0;
char line[256];
int found = 0;
while (fgets(line, sizeof line, f)) {
if (strstr(line, " /usr/data/mnt/sd_0 ")) { found = 1; break; }
}
fclose(f);
if (!found) return 0;
/* writable logs dir? */
mkdir(LOG_DIR_CARD, 0755);
return access(LOG_DIR_CARD, W_OK) == 0;
}
static void boot_timestamp(char *out, size_t n)
{
/* reuse the timestamp cached for this boot session (/tmp is cleared on
* reboot) so repeated launcher starts in one boot share one log file */
int fd = open("/tmp/meow_bootts", O_RDONLY);
if (fd >= 0) {
int r = read(fd, out, (int)n - 1);
close(fd);
if (r > 0) { out[r] = 0; return; }
}
time_t now = time(NULL);
time_t up = 0;
fd = open("/proc/uptime", O_RDONLY);
if (fd >= 0) {
char buf[64];
int r = read(fd, buf, sizeof buf - 1);
close(fd);
if (r > 0) {
buf[r] = 0;
char *p = strchr(buf, ' ');
if (p) *p = 0;
up = (time_t)atof(buf);
}
}
time_t boot = (time_t)((double)now - (double)up + 0.5);
struct tm tm;
localtime_r(&boot, &tm);
strftime(out, n, "meow_%Y%m%d_%H%M%S", &tm);
/* cache for the rest of this boot session */
fd = open("/tmp/meow_bootts", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) {
write(fd, out, strlen(out));
close(fd);
}
}
static unsigned long long now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (unsigned long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
/* M2.18 Bug 1: 无符号回绕保护的「空闲时长」。
* launch_app()/confirm_reboot_player() 是阻塞调用,主循环 for(;;) 顶部的 t=now_ms()
* 在 app 运行期间被卡住而停在启动前旧值;app 退出后 last_input_ms 被置为 app 退出
* 时刻(较大值)。若该圈 any_input==0(如触摸释放单独落在下一圈、音量/播放边沿发生在
* 更早圈),line「if (any_input) last_input_ms = t」不执行 → 检查处 t < last_input_ms,
* (unsigned long long)(t - last_input_ms) 无符号相减回绕成 2^64-Δ 的极大值,恒 >= 30s
* → 误判「空闲超时」→ 立即 meow_panel_off()。r3proii 日志铁证:auto screen-off after
* 30s idle (t=141578 last=145688),仅 122ms 即被误判为已空闲 30s。凡判定空闲一律经此
* 函数取差:t < last(t 过期)时返回 0,绝不回绕。 */
static unsigned long long idle_ms_since(unsigned long long t, unsigned long long last)
{
return (t >= last) ? (t - last) : 0;
}
/* remove oldest log files until total size <= limit (called at boot) */
static void log_cleanup_old(void)
{
struct dirent *de;
DIR *d = opendir(LOG_DIR_CARD);
if (!d) return;
/* collect meow_*.log with mtime + size */
struct logfile { char name[64]; long long size; long long mtime; } files[64];
int n = 0;
while ((de = readdir(d)) && n < 64) {
if (strncmp(de->d_name, "meow_", 5)) continue;
char path[320];
snprintf(path, sizeof path, "%s/%s", LOG_DIR_CARD, de->d_name);
struct stat st;
if (stat(path, &st) || !S_ISREG(st.st_mode)) continue;
files[n].mtime = (long long)st.st_mtime;
files[n].size = (long long)st.st_size;
snprintf(files[n].name, sizeof files[n].name, "%s", de->d_name);
n++;
}
closedir(d);
long long total = 0;
for (int i = 0; i < n; i++) total += files[i].size;
/* simple selection sort by mtime ascending (oldest first) */
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (files[j].mtime < files[i].mtime) {
struct logfile t = files[i];
files[i] = files[j]; files[j] = t;
}
int deleted = 0;
for (int i = 0; i < n && total > (long long)LOG_SIZE_LIMIT; i++) {
char path[320];
snprintf(path, sizeof path, "%s/%s", LOG_DIR_CARD, files[i].name);
if (unlink(path) == 0) { total -= files[i].size; deleted++; }
}
if (deleted) fprintf(stderr, "log_cleanup: deleted %d old log(s), total now %lld\n",
deleted, total);
}
/* try to open the card log file; returns 0 on success */
static int log_open_card(void)
{
if (log_fd >= 0) return 0;
if (!card_available()) return -1;
char ts[64];
boot_timestamp(ts, sizeof ts);
snprintf(log_dir, sizeof log_dir, "%s", LOG_DIR_CARD);
snprintf(log_file, sizeof log_file, "%s/%s.log", LOG_DIR_CARD, ts);
int fd = open(log_file, O_WRONLY | O_APPEND | O_CREAT, 0644);
if (fd < 0) return -1;
log_fd = fd;
return 0;
}
static void logmsg(const char *fmt, ...);
/* flush the /tmp boot buffer into the file log (once the card mounts) */
static void log_poll_card(void)
{
if (log_fd >= 0) return;
if (log_open_card() != 0) return;
/* move the /tmp boot buffer into the card log, then drop it */
int fd = open(BOOT_BUF_TMP, O_RDONLY);
if (fd >= 0) {
char chunk[1024];
ssize_t n;
while ((n = read(fd, chunk, sizeof chunk)) > 0) {
if (write(log_fd, chunk, n) < 0) { close(fd); log_fd = -1; return; }
}
close(fd);
unlink(BOOT_BUF_TMP);
logmsg("boot buffer moved into %s", log_file);
} else {
logmsg("card log open: %s", log_file);
}
}
/* bounded append to the /tmp boot buffer (no card log open yet) */
static void boot_buf_put(const char *line, int len)
{
struct stat st;
if (stat(BOOT_BUF_TMP, &st) == 0 && st.st_size > BOOT_BUF_MAX)
unlink(BOOT_BUF_TMP); /* roll the buffer (boot-stage only) */
int fd = open(BOOT_BUF_TMP, O_WRONLY | O_APPEND | O_CREAT, 0644);
if (fd < 0) return;
if (write(fd, line, len) < 0) { /* best effort */ }
close(fd);
}
static void logmsg(const char *fmt, ...)
{
char buf[512];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof buf, fmt, ap);
va_end(ap);
char line[576];
int l = snprintf(line, sizeof line, "%llu %s\n", now_ms(), buf);
if (log_fd >= 0) {
if (write(log_fd, line, l) < 0) log_fd = -1; /* card gone -> buffer */
}
if (log_fd < 0) boot_buf_put(line, l);
fprintf(stderr, "%llu %s\n", now_ms(), buf);
}
/* ================================================================== */
/* fb0 helpers (RGB565 / RGBA8888 auto) */
/* ================================================================== */
/* NOTE: on this device FBIOGET_VSCREENINFO=0x4600, FSCREENINFO=0x4602
* (device-verified legacy numbers; do NOT swap them). */
#define FBIOGET_VSCREENINFO 0x4600
#define FBIOGET_FSCREENINFO 0x4602
#define FBIOPAN_DISPLAY 0x4606
#define FBIOBLANK 0x4611
#define FBIO_WAITFORVSYNC 0x4620
static unsigned char *fb; /* 绘制目标指针 = SDK 的 back_buf(A-1/A-2:launcher 与 app 统一渲染栈) */
static unsigned char *fb_mem = NULL; /* 真实 mmap 帧缓冲(播放器帧备份/恢复源 + app 帧读回 + 双缓冲 yoffset 用) */
static int fb_xres, fb_yres, fb_line, fb_bpp;
static int fb_fd = -1;
static unsigned int fb_yres_virtual = 0; /* virtual height (double-buffer = 2*yres) */
static unsigned int fb_active_yoffset = 0; /* scanline of the displayed buffer (0 or yres) */
static int fb_orig_yoffset = -1; /* player's yoffset at menu open (saved) */
static unsigned char *fb_player_backup = NULL;
static size_t fb_player_backup_len = 0;
/* ---- A-1/A-2:launcher 用 SDK 渲染栈 ----
* fb 指向 SDK 的 back_buf(meow_ui_back_buf()),所有绘制(put_px/meow_font/dim_screen/
* draw_menu/statusbar/电源菜单/确认框)画进 SDK 的离屏;呈现统一走 meow_ui_flush()(= SDK present,
* 内含 vsync + 帧率门控 + 原子 blit 脏区)。launcher 不再自建 back_buf/meow_present/帧率门控。
* fb_mem 仍保留 launcher 自有的全虚拟 mmap:仅用于播放器帧备份/恢复/双缓冲 yoffset 切换
* (fb_backup_player/fb_norm_buf0/fb_restore_player/fb_pan_buf0_force)与 app 帧读回
* (pm_capture_app_frame)——这些是「显示 buffer/页码切换」,与「present 上屏」不同层,不动。 */
static int back_buf_ok = 0; /* fb 是否指向 SDK back_buf(1=可经 SDK present 上屏) */
/* ---- M2.16 按键防抖 + 单击/长按界限 ---- */
#define KEY_DEBOUNCE_MS 40 /* 键值须稳定此毫秒数才认可(抗 ADC 抖动) */
#define CLICK_MS 400 /* 按压 <此毫秒 = 单击一次;≥ = 长按(连续) */
struct kd_state { int raw, stable, down; int longpressed; unsigned long long since, down_at; };
/* ---- M2.17 超时自动息屏(display.screen_off_timeout / screen_off_mode)----
* timeout 单位秒,0=常亮(不自动息屏);mode:0=panel(面板下电+背光0+FBIOBLANK(4),挂起触摸
* 仅电源/任意键唤醒)、1=black(背光0全黑,触摸可唤醒)。见 SYSTEM_SETTINGS.md。 */
#define SCREEN_OFF_TIMEOUT_DEFAULT 30
#define SCREEN_OFF_MODE_DEFAULT 0 /* 0=panel, 1=black */
static int screen_off_timeout = SCREEN_OFF_TIMEOUT_DEFAULT;
static int screen_off_mode = SCREEN_OFF_MODE_DEFAULT;
/* ---- M2.37 电源长按语义缓存(power.longpress_menu_ms / power.forceoff_ms)----
* 设置页可配;默认 3000/8000 与注释「3s 电源菜单 / 8s 强关」一致。缓存供 run_menu
* 电源按住判定使用(原为硬编码 2000/8000/3000,现接线读取以让设置生效)。 */
#define PWR_MENU_MS_DEFAULT 3000
#define PWR_FORCE_MS_DEFAULT 8000
static int power_longpress_menu_ms = PWR_MENU_MS_DEFAULT;
static int power_forceoff_ms = PWR_FORCE_MS_DEFAULT;
/* ---- M2.16 刷新率调度配置(display.refresh_rate / refresh_always_max)----
* A-1/A-2:档位上限/恒满速开关由 launcher 配置(settings.conf),经
* meow_ui_set_refresh_rate()/meow_ui_set_refresh_always_max() 交给 SDK 的 present
* 帧率门控/governor 执行(归口 PERF_SCHEDULER 显示刷新率子域)。当前有效档 eff_rate
* 与帧计数已在 SDK 侧,launcher 不再各自维护一套。 */
#define REFRESH_RATE_CFG_DEFAULT 60
static int refresh_rate_cfg = REFRESH_RATE_CFG_DEFAULT; /* 目标档 60/30/15(上限,default 60) */
static int refresh_always_max = 0; /* 1=恒 max 不降档 */
/* M2.16 渲染/防抖前向声明(定义在下方;fb_open 等先引用) */
static void meow_dirty_full(void);
static void meow_dirty_add(int x, int y, int w, int h);
static void meow_dirty_reset(void);
static inline void put_px(int x, int y, uint32_t color)
{
if (x < 0 || x >= fb_xres || y < 0 || y >= fb_yres) return;
unsigned char *p = fb + y * fb_line + x * (fb_bpp / 8);
if (fb_bpp == 16) {
uint16_t c = ((color >> 8) & 0xF800) | ((color >> 5) & 0x07E0) |
((color >> 3) & 0x001F);
*(uint16_t *)p = c;
} else {
*(uint32_t *)p = (color << 8) | 0xFF;
}
}
static void fill_rect(int x0, int y0, int w, int h, uint32_t color)
{
for (int y = y0; y < y0 + h; y++)
for (int x = x0; x < x0 + w; x++)
put_px(x, y, color);
}
/* 5x7 font (same table as hello.c) */
static const unsigned char font[][8] = {
{0x0E,0x11,0x13,0x15,0x19,0x11,0x0E,0x00},
{0x04,0x0C,0x04,0x04,0x04,0x04,0x0E,0x00},
{0x0E,0x11,0x01,0x02,0x04,0x08,0x1F,0x00},
{0x1F,0x02,0x04,0x02,0x01,0x11,0x0E,0x00},
{0x02,0x06,0x0A,0x12,0x1F,0x02,0x02,0x00},
{0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E,0x00},
{0x06,0x08,0x10,0x1E,0x11,0x11,0x0E,0x00},
{0x1F,0x01,0x02,0x04,0x08,0x08,0x08,0x00},
{0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E,0x00},
{0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C,0x00},
{0x0E,0x11,0x11,0x1F,0x11,0x11,0x11,0x00},
{0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E,0x00},
{0x0E,0x11,0x10,0x10,0x10,0x11,0x0E,0x00},
{0x1C,0x12,0x11,0x11,0x11,0x12,0x1C,0x00},
{0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F,0x00},
{0x1F,0x10,0x10,0x1E,0x10,0x10,0x10,0x00},
{0x0E,0x11,0x10,0x17,0x11,0x11,0x0F,0x00},
{0x11,0x11,0x11,0x1F,0x11,0x11,0x11,0x00},
{0x0E,0x04,0x04,0x04,0x04,0x04,0x0E,0x00},
{0x07,0x02,0x02,0x02,0x02,0x12,0x0C,0x00},
{0x11,0x12,0x14,0x18,0x14,0x12,0x11,0x00},
{0x10,0x10,0x10,0x10,0x10,0x10,0x1F,0x00},
{0x11,0x1B,0x15,0x15,0x11,0x11,0x11,0x00},
{0x11,0x19,0x15,0x13,0x11,0x11,0x11,0x00},
{0x0E,0x11,0x11,0x11,0x11,0x11,0x0E,0x00},
{0x1E,0x11,0x11,0x1E,0x10,0x10,0x10,0x00},
{0x0E,0x11,0x11,0x11,0x15,0x12,0x0D,0x00},
{0x1E,0x11,0x11,0x1E,0x14,0x12,0x11,0x00},
{0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E,0x00},
{0x1F,0x04,0x04,0x04,0x04,0x04,0x04,0x00},
{0x11,0x11,0x11,0x11,0x11,0x11,0x0E,0x00},
{0x11,0x11,0x11,0x11,0x11,0x0A,0x04,0x00},
{0x11,0x11,0x11,0x15,0x15,0x1B,0x11,0x00},
{0x11,0x11,0x0A,0x04,0x0A,0x11,0x11,0x00},
{0x11,0x11,0x11,0x0A,0x04,0x04,0x04,0x00},
{0x1F,0x01,0x02,0x04,0x08,0x10,0x1F,0x00},
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00},
{0x04,0x04,0x04,0x04,0x04,0x00,0x04,0x00},
{0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00},
};
static int glyph_index(char c)
{
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'A' && c <= 'Z') return 10 + (c - 'A');
if (c >= 'a' && c <= 'z') return 10 + (c - 'a');
if (c == ' ') return 36;
if (c == '.') return 37;
if (c == '!') return 38;
if (c == '-' || c == '_') return 39;
return 36;
}
static void draw_glyph(int x, int y, char c, int scale, uint32_t color)
{
const unsigned char *g = font[glyph_index(c)];
for (int row = 0; row < 7; row++) {
unsigned char bits = g[row];
for (int col = 0; col < 5; col++) {
if (bits & (0x10 >> col)) {
for (int dy = 0; dy < scale; dy++)
for (int dx = 0; dx < scale; dx++)
put_px(x + col * scale + dx, y + row * scale + dy, color);
}
}
}
}
static void draw_text(int x, int y, const char *s, int scale, uint32_t color)
{
while (*s) {
draw_glyph(x, y, *s, scale, color);
x += 6 * scale;
s++;
}
}
/* ================================================================== */
/* M2.6 meow_font 集成(agent-28 字体子系统,font/meow_font.a + libz.a)*/
/* M2.6 MeowOS 字重结构:字库放 TF 卡 MeowOS/fonts/default/regular/font.png+font.idx
*(或固件 /usr/share/meowos/font/);MeowOS 启动 ensure_card_mounted 后再 load,卡晚 */
/* 挂载时轮询重试。没加载成功时 ui_draw 回退 5x7 点阵(ASCII)。 */
/* ================================================================== */
#define UI_FONT_PNG_CARD "/usr/data/mnt/sd_0/MeowOS/fonts/default/regular/font.png"
#define UI_FONT_IDX_CARD "/usr/data/mnt/sd_0/MeowOS/fonts/default/regular/font.idx"
#define UI_FONT_PNG_SYS "/usr/share/meowos/font/font.png"
#define UI_FONT_IDX_SYS "/usr/share/meowos/font/font.idx"
/* M2.5x 多字体库 + M2.6 字重:字体目录 = <卡根>/MeowOS/fonts/<family>/<weight>/(font.png+font.idx+font.json)。
* <卡根> 默认 /usr/data/mnt/sd_0(card_present() 判定;--fontscan 可指定宿主根以便单测)。 */
#define MEOW_FONTS_CFG "MeowOS/fonts"
#define FONT_PRESETS_MAX 17 /* 1 默认 + ≤16 卡内字体(防爆) */
/* M2.6 字重结构:卡内字体 = <卡根>/MeowOS/fonts/<family>/<weight>/(三件套)。
* 每个 preset = 一套 <family>/<weight>(如 misans/regular)。值 = 卡根相对路径
* MeowOS/fonts/<family>/<weight>/font.png;family/weight 单列供设置页「family→字重」子菜单
* 分组与 meow_font_effective_paths_tiered 按 family+weight 选库。 */
static const char *font_presets[FONT_PRESETS_MAX]; /* SC_PRESET 值 = 卡根相对路径 */
static const char *font_preset_labels[FONT_PRESETS_MAX]; /* SC_PRESET 中文标签(含字重+档位大小标记) */
/* M2.5x 字体档位:每套的 charset 档("common"/"gbk"/"full",取自 font.json,缺省 "common")
* 与 glyph_count(信息用)。供档位分层装载(meow_font_effective_paths_tiered)与设置页标注。 */
static const char *font_preset_tiers[FONT_PRESETS_MAX];
static int font_preset_glyphs[FONT_PRESETS_MAX];
static const char *font_preset_fams[FONT_PRESETS_MAX]; /* family slug(目录一级) */
static const char *font_preset_wts[FONT_PRESETS_MAX]; /* weight slug(目录二级) */
static const char *font_preset_names[FONT_PRESETS_MAX]; /* font.json name(family 显示名) */
static char font_preset_val_buf[FONT_PRESETS_MAX][320]; /* 值持久存储(指向上面指针数组) */
static char font_preset_label_buf[FONT_PRESETS_MAX][96]; /* 标签持久存储(含字重+档位标记) */
static char font_preset_tier_buf[FONT_PRESETS_MAX][8]; /* charset 持久存储 */
static char font_preset_fam_buf[FONT_PRESETS_MAX][64]; /* family 持久存储 */
static char font_preset_wt_buf[FONT_PRESETS_MAX][32]; /* weight 持久存储 */
static char font_preset_name_buf[FONT_PRESETS_MAX][64]; /* name 持久存储 */
static int font_presets_n = 0; /* 运行时动态计数(默认=1,仅"默认字体") */
static int mfont_ready = 0;
static int mfont_forced = 0; /* font.file 变更→强制真实(un)load,跳过 SDK 复用捷径 */
/* 24-bit RGB → RGB565(与 put_px/meow_font 一致) */
static uint16_t rgb565(uint32_t c)
{
return (uint16_t)(((c >> 8) & 0xF800) | ((c >> 5) & 0x07E0) | ((c >> 3) & 0x001F));
}
/* fb_open 后调用:设置字库绘制目标为当前双缓冲(buffer0 起点 = fb 偏移 0) */
static void meow_font_bind_fb(void)
{
meow_font_set_fb(fb_xres, fb_yres, fb_line);
}
/* 把 font.file 存的「卡根相对/绝对」路径解析为设备绝对路径。
* 相对路径以 /usr/data/mnt/sd_0/ 为卡根(与 MEOWAPPS 同源);绝对路径原样。
* 返回 0 成功,-1 参数无效。 */
static int meow_font_resolve_path(const char *ref, char *out, int olen)
{
if (!ref || !ref[0]) return -1;
if (ref[0] == '/') { snprintf(out, olen, "%s", ref); return 0; }
snprintf(out, olen, "/usr/data/mnt/sd_0/%s", ref);
return 0;
}
/* 前向声明:meow_font_ensure 在下方 font 区中调用(定义在 meow_font_effective_paths_tiered 处)。 */
static int meow_font_effective_tier(int app_tier);
static int meow_font_effective_paths_tiered(int tier, char *out_png, int plen,
char *out_idx, int ilen, char *out_family, int flen);
/* 解析 font.file 指向的 png + 派生同目录 idx(font.png → font.idx)。
* 仅当指向的 png 存在(access R_OK)返回 0;否则 -1(交由回退链)。
* 返回 0:out_png/out_idx 已填设备绝对路径。 */
static int meow_font_resolve_selected(char *out_png, int png_len, char *out_idx, int idx_len)
{
char ref[MEOW_SET_MAX_LEN];
if (meow_settings_get_str("font.file", ref, sizeof ref) != 0) return -1;
if (meow_font_resolve_path(ref, out_png, png_len) != 0) return -1;
if (access(out_png, R_OK) != 0) return -1; /* 指向不存在 → 回退卡默认/固件 */
snprintf(out_idx, idx_len, "%s", out_png); /* 同目录 font.idx(标准三件套布局) */
size_t n = strlen(out_idx);
if (n > 4 && !strcmp(out_idx + n - 4, ".png")) {
out_idx[n-4] = '.'; out_idx[n-3] = 'i'; out_idx[n-2] = 'd'; out_idx[n-1] = 'x'; out_idx[n] = 0;
}
return 0;
}
/* 尝试加载字库;成功返回 1。优先级(M2.5x,font.load_full 已接入):
* 0) 生效档 = font.load_full ? gbk : common(launcher 自身 UI 跟随系统大字库开关);
* 1) 生效字体(tier 解析,tier0 即 font.file 所选;相对路径按卡根解析);
* 2) 卡 MeowOS/fonts/default/regular/font.png(默认字库);
* 3) 固件 /usr/share/meowos/font/font.png。
* A-1/A-2:SDK meow_ui_init() 已用同一 meow_font.o 加载过(可能成功也可能未成功)。
* 当字体指向「默认卡字库」或未设置时复用 SDK 已加载状态,避免重复 unload+reload;
* 当字体指向自定义字库时必须显式 load(SDK 只加载默认,不读 font.file)。
* mfont_forced=1(font.file/font.load_full 变更触发)时无条件真实 reload,跳过上述捷径。
* 全失败 → mfont_ready=0,ui_draw 回退 5x7 点阵(系统不死)。 */
static int meow_font_ensure(void)
{
if (mfont_ready && !mfont_forced) return 1;
mfont_forced = 0;
char fpng[400], fidx[400], fam[96];
int tier = meow_font_effective_tier(MEOW_FONT_TIER_COMMON); /* launcher 自身 UI 跟随 load_full */
int have_sel = (meow_font_effective_paths_tiered(tier, fpng, sizeof fpng,
fidx, sizeof fidx, fam, sizeof fam) == 0);
/* SDK 已加载且所选字库为「默认卡字库」(或未设置)→ 复用,避免重复解码。 */
if (meow_gfx_mfont_ready() && (!have_sel || !strcmp(fpng, UI_FONT_PNG_CARD))) {
mfont_ready = 1;
logmsg("meow_font: reuse sdk-loaded font");
return 1;
}
/* 生效字体优先(tier=common 时即 font.file;load_full → 大字库 gbk 档)。 */
if (have_sel && meow_font_load(fpng, fidx) == 0) {
mfont_ready = 1;
logmsg("meow_font: loaded %s (%d/%d sizes), tier=%d", fpng,
meow_font_has_size(28), meow_font_has_size(20), tier);
return 1;
}
if (have_sel)
logmsg("meow_font: selected font %s missing, fallback default", fpng); /* 系统日志 */
/* 卡默认 → 固件。 */
if (meow_font_load(UI_FONT_PNG_CARD, UI_FONT_IDX_CARD) == 0 ||
meow_font_load(UI_FONT_PNG_SYS, UI_FONT_IDX_SYS) == 0) {
mfont_ready = 1;
logmsg("meow_font: loaded fallback (%d/%d sizes)",
meow_font_has_size(28), meow_font_has_size(20));
return 1;
}
logmsg("meow_font: all font sources missing, using 5x7 bitmap"); /* WARN:字库不可用 */
return 0;
}
/* 从 MeowOS/fonts/<family>/<weight>/font.png 绝对路径提 family(第一级目录名)。
* 如 .../misans/regular/font.png → "misans"、.../default/regular/font.png → "default";
* 结构异常/找不到前缀 → 回退 "default"。 */
static void font_family_from_png(const char *png, char *out, int olen)
{
const char *prefix = "/MeowOS/fonts/";
const char *p = png ? strstr(png, prefix) : NULL;
if (!p) { snprintf(out, olen, "default"); return; }
const char *q = p + strlen(prefix);
const char *slash = strchr(q, '/');
int n = slash ? (int)(slash - q) : (int)strlen(q);
if (n <= 0 || n >= olen) { snprintf(out, olen, "default"); return; }
memcpy(out, q, (size_t)n); out[n] = 0;
}
/* 同路径同时提取 family + weight(两级目录名)。weight 缺省 "regular"。 */
static void font_family_weight_from_png(const char *png, char *fam, int flen, char *wt, int wlen)
{
const char *prefix = "/MeowOS/fonts/";
const char *p = png ? strstr(png, prefix) : NULL;
if (!p) { snprintf(fam, flen, "default"); snprintf(wt, wlen, "regular"); return; }
const char *q = p + strlen(prefix);
const char *s1 = strchr(q, '/');
if (!s1) { snprintf(fam, flen, "%s", q); snprintf(wt, wlen, "regular"); return; }
int nf = (int)(s1 - q);
if (nf > 0 && nf < flen) { memcpy(fam, q, (size_t)nf); fam[nf] = 0; }
else snprintf(fam, flen, "default");
const char *s2 = strchr(s1 + 1, '/');
int nw = s2 ? (int)(s2 - (s1 + 1)) : (int)strlen(s1 + 1);
if (nw > 0 && nw < wlen) { memcpy(wt, s1 + 1, (size_t)nw); wt[nw] = 0; }
else snprintf(wt, wlen, "regular");
}
/* 求「当前生效字体」的 png/idx 绝对路径 + family,供 launch_app 注入 MEOW_FONT_PNG/IDX/APP。
* 优先级(与 meow_font_ensure 一致):font.file(resolve_selected)→ 卡默认 MeowOS/fonts/default。
* family 取自 MeowOS/fonts/<family>/<weight>/font.png 的第一级目录名;无法判定则 "default"。
* 返回 0 成功(out_png/out_idx/out_family 已填),-1 无卡/无可用字体(调用方跳过注入,
* SDK 自身会回退卡默认→固件→点阵)。 */
static int meow_font_effective_paths(char *out_png, int plen, char *out_idx, int ilen,
char *out_family, int flen)
{
char fpng[400], fidx[400];
if (meow_font_resolve_selected(fpng, sizeof fpng, fidx, sizeof fidx) == 0) {
snprintf(out_png, plen, "%s", fpng);
snprintf(out_idx, ilen, "%s", fidx);
font_family_from_png(fpng, out_family, flen);
return 0;
}
/* 回退卡默认 MeowOS/fonts/default */
if (access(UI_FONT_PNG_CARD, R_OK) == 0) {
snprintf(out_png, plen, "%s", UI_FONT_PNG_CARD);
snprintf(out_idx, ilen, "%s", UI_FONT_IDX_CARD);
snprintf(out_family, flen, "default");
return 0;
}
return -1;
}
/* 档位 → font.json charset 字符串名(common/gbk/full)。 */
static const char *font_tier_charset(int tier)
{
if (tier >= MEOW_FONT_TIER_FULL) return "full";
if (tier >= MEOW_FONT_TIER_GBK) return "gbk";
return "common";
}
/* 生效档:app 声明档(0/1/2,越界 clamp)合并「加载大字库」开关(font.load_full)。
* 开 → 强制 ≥GBK(gbk≈20k 档大字库);关(默认)→ 保持 app 声明(缺省 common≈4k)。
* 断言:0<=返回<=2。 */
static int meow_font_effective_tier(int app_tier)
{
if (app_tier < MEOW_FONT_TIER_COMMON) app_tier = MEOW_FONT_TIER_COMMON;
if (app_tier > MEOW_FONT_TIER_FULL) app_tier = MEOW_FONT_TIER_FULL;
int lf = 0;
if (meow_settings_get_switch("font.load_full", &lf) == 0 && lf)
if (app_tier < MEOW_FONT_TIER_GBK) app_tier = MEOW_FONT_TIER_GBK;
return app_tier;
}
/* 【tier 选择入口,M2.5x 已实现】按字体档位取「当前生效字体」png/idx,供 launch_app 注入
* MEOW_FONT_PNG/IDX/APP(配合 MEOW_FONT_TIER)。口径(MEOWOS_SPEC_REF §5.2/§5.7.8):
* - 生效档 = max(app 声明档, font.load_full ? GBK : COMMON):系统「加载大字库」开 →
* 强制 ≥gbk;
* - tier<=common → 走既有 meow_font_effective_paths(font.file/卡默认/固件);
* - tier>0 → 在已扫描的 MeowOS/fonts/<family>/<weight>/ 里找 charset 匹配(gbk 请求接受
* gbk/full;full 请求接受 full)的那套,以其 font.png/idx 绝对路径返回(family = 一级目录名);
* - M2.6 字重:优先选「与用户当前 font.file 同 family(其次同 weight)」的大字库,避免
* 大字库跳到另一家字体;找不到匹配 → 回退 meow_font_effective_paths(避免点到固件小库)。
* 返回 0 成功(out_png/out_idx/out_family 已填),-1 无可用。 */
static int meow_font_effective_paths_tiered(int tier, char *out_png, int plen,
char *out_idx, int ilen, char *out_family, int flen)
{
int eff = meow_font_effective_tier(tier); /* 合并 font.load_full:开 → 强制 ≥GBK */
if (eff <= MEOW_FONT_TIER_COMMON)
return meow_font_effective_paths(out_png, plen, out_idx, ilen, out_family, flen);
/* 大字库:从扫描结果选 charset 匹配的一套。
* 打分(档位主导 ×100):gbk 请求 → gbk=200 > full=100;full 请求 → full=200 > gbk=100;
* + 同 family +30(其次同 weight +10,M2.6)仅作同档位内 tie-break:大字库沿用用户所选
* family,但绝不因「同 family」而把 common 小库越档选成大字库。 */
const char *want = font_tier_charset(eff);
char curfam[64] = "", curwt[32] = "";
{ /* 用户当前 font.file 所选 family/weight(无法判定 → 空,不参与加成) */
char fpng[400], fidx[400];
if (meow_font_resolve_selected(fpng, sizeof fpng, fidx, sizeof fidx) == 0)
font_family_weight_from_png(fpng, curfam, sizeof curfam, curwt, sizeof curwt);
}
char ref[320];
int found = -1, best_score = 0;
for (int i = 0; i < font_presets_n; i++) {
const char *cs = font_preset_tiers[i] ? font_preset_tiers[i] : "common";
/* 档位优先(×100 主导):gbk 请求 → gbk=200 > full=100;full 请求 → full=200 > gbk=100。
* M2.6:同 family +30、同 weight +10 仅作同档位内 tie-break(不越档选小库)。 */
int score = 0;
if (!strcmp(cs, want)) score = 200;
else if (!strcmp(cs, "full") || !strcmp(cs, "gbk")) score = 100; /* 降级 */
if (curfam[0] && font_preset_fams[i] && !strcmp(font_preset_fams[i], curfam)) score += 30;
if (curfam[0] && curwt[0] && font_preset_wts[i] && !strcmp(font_preset_wts[i], curwt)) score += 10;
if (score > best_score) { best_score = score; found = i; }
}
if (found >= 0)
snprintf(ref, sizeof ref, "%s", font_presets[found]);
if (found >= 0) {
/* 相对路径按卡根解析为设备绝对路径(与 meow_font_effective_paths 同源)。 */
if (meow_font_resolve_path(ref, out_png, plen) != 0) return -1;
if (access(out_png, R_OK) != 0) goto fallback;
snprintf(out_idx, ilen, "%s", out_png);
size_t n = strlen(out_idx);
if (n > 4 && !strcmp(out_idx + n - 4, ".png")) {
out_idx[n-4] = '.'; out_idx[n-3] = 'i'; out_idx[n-2] = 'd';
out_idx[n-1] = 'x'; out_idx[n] = 0;
}
font_family_from_png(out_png, out_family, flen);
return 0;
}
fallback:
/* 无匹配大字库/不可读 → 回退全局(font.file 所选;其可能是大字库或默认)。 */
return meow_font_effective_paths(out_png, plen, out_idx, ilen, out_family, flen);
}
/* M2.5x 多字体:从 settings 读 display.multi_font + 6 类 font.class_*,把各自相对路径按卡根
* 解析为绝对路径,去重装载到 meow_font 槽位(同 family 去重 & ≤4 套上限由
* meow_font_config_multifont 统一处理)。仅当全局字库已装载(mfont_ready / slot0)时生效;
* 关或未装载 → 单字体(on=0),零额外开销。调用时机:启动 meow_font_ensure() 之后,以及
* 设置页变更 display.multi_font / font.class_* / font.file。 */
static void meow_font_reload_multifont(void)
{
static const char *cls_keys[MEOW_FCLS_COUNT] = {
"font.class_cjk", "font.class_cjk_sym", "font.class_num",
"font.class_latin", "font.class_latin_sym", "font.class_other" };
int on = 0;
meow_settings_get_switch("display.multi_font", &on);
char gpng[400], gidx[400], gfam[96];
/* 全局跟生效档(合并 font.load_full → 大字库),与 meow_font_ensure 同源,避免槽0实际装载与
* 此处理解不一致(多字体去重以全局 png 为基准)。 */
if (meow_font_effective_paths_tiered(meow_font_effective_tier(MEOW_FONT_TIER_COMMON),
gpng, sizeof gpng, gidx, sizeof gidx, gfam, sizeof gfam) != 0)
return; /* 无全局字体:无法多字体 */
/* 各类候选(静态缓冲保活;config_multifont 同步读取,不持指针) */
static char cpng[MEOW_FCLS_COUNT][400], cidx[MEOW_FCLS_COUNT][400];
meow_font_class_cfg cfg[MEOW_FCLS_COUNT];
memset(cfg, 0, sizeof cfg);
for (int c = 0; c < MEOW_FCLS_COUNT; c++) {
char ref[MEOW_SET_MAX_LEN];
if (meow_settings_get_str(cls_keys[c], ref, sizeof ref) == 0 && ref[0]) {
char abs[400];
if (meow_font_resolve_path(ref, abs, sizeof abs) == 0 && access(abs, R_OK) == 0) {
snprintf(cpng[c], sizeof cpng[c], "%s", abs);
snprintf(cidx[c], sizeof cidx[c], "%s", abs);
size_t n = strlen(cidx[c]);
if (n > 4 && !strcmp(cidx[c] + n - 4, ".png")) {
cidx[c][n-4]='.'; cidx[c][n-3]='i'; cidx[c][n-2]='d'; cidx[c][n-1]='x'; cidx[c][n]=0;
}
cfg[c].png = cpng[c]; cfg[c].idx = cidx[c];
} else {
/* M2.58:类别字体配置了但缺失 → 该类别回退全局,并记系统 WARN。 */
logmsg("multi_font: class %s font %s missing, fallback global", cls_keys[c], ref);
}
}
}
int distinct = 0, truncated = 0;
if (meow_font_config_multifont(on, gpng, gidx, cfg, &distinct, &truncated) != 0)
logmsg("multi_font: config failed -> single-font");
else if (truncated)
logmsg("multi_font: WARN >%d 套不同字体(会增内存),已按上限截断", distinct);
else if (on)
logmsg("multi_font: enabled, %d distinct fonts (global=%s)", distinct, gfam);
}
/* 统一文本绘制:字库就绪用 meow_font(真字体/中文),否则回退 5x7 点阵。
* size 为字库字号(28=状态栏/大标题,20=正文/按钮);回退时按 scale 近似。 */
static void ui_draw(int x, int y, const char *s, uint32_t color, int size)
{
if (mfont_ready) {
meow_font_drawtext((uint16_t *)fb, x, y, s, rgb565(color), size);
} else {
draw_text(x, y, s, (size >= 16) ? 2 : 1, color);
}
}
/* 文本宽度(字库 or 回退估算),用于右对齐/居中 */
static int ui_width(const char *s, int size)
{
if (mfont_ready) return meow_font_width_text(s, size);
return (int)strlen(s) * 6 * ((size >= 16) ? 2 : 1);
}
/* 文本高度(字库行高 or 回退 7×scale 估算),用于垂直居中 / 行高预算。 */
static int ui_text_height(int size)
{
if (mfont_ready) return meow_font_line_height(size);
return 7 * ((size >= 16) ? 2 : 1);
}
static int fb_open(void)
{
int fd = open(FB_DEV, O_RDWR);
if (fd < 0) { logmsg("fb open failed: %s", strerror(errno)); return -1; }
unsigned char vinfo[160], finfo[68];
memset(vinfo, 0, sizeof vinfo);
memset(finfo, 0, sizeof finfo);
if (ioctl(fd, FBIOGET_VSCREENINFO, vinfo) < 0 ||
ioctl(fd, FBIOGET_FSCREENINFO, finfo) < 0) {
logmsg("fb ioctl failed"); close(fd); return -1;
}
fb_xres = *(uint32_t *)(vinfo + 0);
fb_yres = *(uint32_t *)(vinfo + 4);
fb_yres_virtual = *(uint32_t *)(vinfo + 12);
fb_active_yoffset = *(uint32_t *)(vinfo + 20);
fb_bpp = *(uint32_t *)(vinfo + 24);
fb_line = *(uint32_t *)(finfo + 44);
if (!fb_line) fb_line = fb_xres * (fb_bpp / 8);
/* v6.6: fb0 is DOUBLE-BUFFERED (verified yres_virtual=1440=2*720,
* smem_len=2 frames, ypanstep=1). The displayed buffer is selected by
* yoffset (FBIOPAN_DISPLAY 0x4606 verified to flip it). Map the FULL
* virtual framebuffer so we can read/write both buffers for the
* backup/restore below; mmapping only one frame would let the menu
* land on the buffer the panel is NOT showing. */
if (fb_yres_virtual < fb_yres) fb_yres_virtual = fb_yres; /* single buffer */
fb_mem = mmap(NULL, (size_t)fb_line * fb_yres_virtual,
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (fb_mem == MAP_FAILED) {
logmsg("fb mmap failed: %s (xres=%d yres=%d bpp=%d line=%d size=%d)",
strerror(errno), fb_xres, fb_yres, fb_bpp, fb_line,
fb_line * fb_yres_virtual);
close(fd);
return -1;
}
fb_fd = fd;
/* A-1/A-2:launcher 用 SDK 渲染栈 —— fb 指向 SDK 的 back_buf(统一离屏),present 统一走
* meow_ui_flush()(SDK present:vsync + 帧率门控 + 原子 blit 脏区)。不再自建 back_buf。
* fb_mem(本函数 mmap 的全虚拟 fb)仅保留给播放器帧备份/恢复 + yoffset 切换 + app 帧读回。 */
fb = meow_ui_back_buf();
if (fb) {
back_buf_ok = 1;
meow_dirty_full();
logmsg("fb: %dx%d bpp=%d line=%d vres=%u yoffset=%u (A-1/A-2 SDK back_buf, meow_ui_flush)",
fb_xres, fb_yres, fb_bpp, fb_line, fb_yres_virtual, fb_active_yoffset);
} else {
fb = fb_mem; back_buf_ok = 0;
logmsg("fb: no SDK back_buf -> direct fb write (present disabled)");
}
/* NOTE: the panel is NOT unblanked here anymore — the player may still
* be running at this point (stop_player() comes after fb_open) and its
* screen-off state machine would re-blank the panel behind the menu.
* unblank_panel() runs after stop_player() (v6.2 hotfix) so the frozen
* player cannot re-blank and the menu stays visible. */
return fd;
}
/* ---- double-buffer handling (v6.6) --------------------------------
* fb0 is double-buffered and the DISPLAYED buffer is chosen by fb_var's
* yoffset (FBIOPAN_DISPLAY flips it). The player page-flips via yoffset (its
* power/screen-off redraw leaves yoffset on the back buffer), so the menu
* MUST be drawn to the buffer the panel actually shows — otherwise the menu
* is invisible and the screen freezes on the player's last frame (the v6.5
* bug). Strategy: snapshot the player's displayed frame into a heap backup,
* pan the display to buffer0 (what the launcher and all apps draw to — apps
* like hello.c mmap exactly one frame at offset 0 and never pan), draw the
* menu/app-list to buffer0, and on session end copy the backup back to the
* player's buffer and pan to the original yoffset so a SIGSTOPped player that
* does NOT redraw (event-driven, ANALYSIS §18.15) still shows its own frame. */
/* Snapshot the player's currently-displayed frame into a heap backup. */
static void fb_backup_player(void)
{
if (fb_yres_virtual <= fb_yres) { fb_orig_yoffset = -1; return; } /* single buffer */
if (fb_active_yoffset >= fb_yres_virtual) fb_active_yoffset = 0;
fb_orig_yoffset = (int)fb_active_yoffset;
fb_player_backup_len = (size_t)fb_line * fb_yres;
fb_player_backup = malloc(fb_player_backup_len);
if (!fb_player_backup) {
logmsg("fb backup malloc failed (len=%zu)", fb_player_backup_len);
fb_orig_yoffset = -1;
fb_player_backup_len = 0;
return;
}
memcpy(fb_player_backup, fb_mem + (size_t)fb_active_yoffset * fb_line,
fb_player_backup_len);
logmsg("fb backup: player frame @ yoffset=%u (buffer %u) -> heap",
fb_active_yoffset,
fb_yres ? fb_active_yoffset / fb_yres : 0);
}
/* Pan the display to buffer0 (yoffset=0). buffer0 is what this launcher and
* every app draw to (apps mmap one frame at offset 0 and never pan), so the
* panel must show buffer0 for the menu and apps to be visible. */
static void fb_norm_buf0(void)
{
if (fb_fd < 0 || fb_yres_virtual <= fb_yres) { fb_active_yoffset = 0; return; }
if (fb_active_yoffset == 0) return; /* already showing buffer0 */
unsigned char v[160];
memset(v, 0, sizeof v);
if (ioctl(fb_fd, FBIOGET_VSCREENINFO, v) < 0) return;
*(uint32_t *)(v + 16) = 0; /* xoffset */
*(uint32_t *)(v + 20) = 0; /* yoffset -> buffer0 */
ioctl(fb_fd, FBIOPAN_DISPLAY, v);
fb_active_yoffset = 0;
logmsg("fb pan -> buffer0 (yoffset=0)");
}
/* M2.8 Bug 3-residual 根因:fb_norm_buf0 在缓存 fb_active_yoffset==0 时短路提前返回、
* 不再重发 FBIOPAN_DISPLAY。但 FBIOBLANK(4) 下电 -> FBIOBLANK(0) 上电后,LCDC 的扫描
* 地址可能已复位到非 buffer0 / 失效位置,而缓存仍是 0 —— 于是真正的 re-pan 从未发出,
* 唤醒后仍是黑屏/旧帧。本函数无条件重发 pan 到 buffer0(同 yoffset 的重复 pan 是硬件
* no-op,无频闪),供唤醒/重绘路径调用,确保重画的界面一定落在被显示的缓冲。 */
static void fb_pan_buf0_force(void)
{
if (fb_fd < 0 || fb_yres_virtual <= fb_yres) { fb_active_yoffset = 0; return; }
unsigned char v[160];
memset(v, 0, sizeof v);
if (ioctl(fb_fd, FBIOGET_VSCREENINFO, v) < 0) return; /* read current vinfo */
*(uint32_t *)(v + 16) = 0; /* xoffset */
*(uint32_t *)(v + 20) = 0; /* yoffset -> buffer0 */
ioctl(fb_fd, FBIOPAN_DISPLAY, v); /* force-issue, even if cached 0 */
fb_active_yoffset = 0;
logmsg("fb force-pan -> buffer0 (yoffset=0)");
}
/* Restore the player's frame to its buffer and pan back to the original
* yoffset. Called at menu/session end (BACK TO PLAYER / SIGTERM) so a frozen
* player that never redraws shows its own frame again. Idempotent. */
static void fb_restore_player(void)
{
int restore_y = fb_orig_yoffset;
if (fb_fd >= 0 && restore_y >= 0 && fb_player_backup) {
memcpy(fb_mem + (size_t)restore_y * fb_line, fb_player_backup,
fb_player_backup_len);
unsigned char v[160];
memset(v, 0, sizeof v);
if (ioctl(fb_fd, FBIOGET_VSCREENINFO, v) == 0) {
*(uint32_t *)(v + 16) = 0;
*(uint32_t *)(v + 20) = (uint32_t)restore_y;
if (ioctl(fb_fd, FBIOPAN_DISPLAY, v) < 0)
logmsg("fb restore pan failed: %s", strerror(errno));
}
logmsg("fb restore: player frame -> yoffset=%d (buffer %d)",
restore_y, fb_yres ? restore_y / fb_yres : 0);
}
if (fb_player_backup) { free(fb_player_backup); fb_player_backup = NULL; }
fb_orig_yoffset = -1;
fb_player_backup_len = 0;
}
/* ================================================================== */
/* A-1/A-2 渲染桥:launcher 手动脏模型 → SDK 脏追踪,呈现走 meow_ui_flush() */
/* launcher 的绘制(put_px/fill_rect/meow_font/dim_screen)直接把像素画进 */
/* SDK 的 back_buf(fb),在此仅把「哪些区域变了」桥进 SDK 的脏追踪; */
/* present 不再由 launcher 做,统一 meow_ui_flush() = SDK present(内含 */
/* vsync + 帧率门控 + 原子 blit 脏区)。launcher 不再自带 meow_present/ */
/* refresh_governor —— 帧率调度已归口 SDK(PERF_SCHEDULER 显示子域)。 */
/* ================================================================== */
/* 标记全屏脏:桥进 SDK 的整帧脏,下次 meow_ui_flush() 整帧 blit。 */
static void meow_dirty_full(void) { meow_gfx_mark_full(); }
/* 清空脏集:SDK present 上屏后自清;launcher 侧无需再清(保留接口兼容)。 */
static void meow_dirty_reset(void) { }
/* 标记一个矩形脏:桥进 SDK 脏追踪(相交合并/超限转全屏由 SDK dirty_add 处理)。 */
static void meow_dirty_add(int x, int y, int w, int h) { meow_gfx_mark_dirty(x, y, w, h); }
/* ---- M2.16 按键防抖:键值须稳定 KEY_DEBOUNCE_MS 才认可,防 ADC 抖动("点一下跳 5-6 下")。
* 返回值:0=无事件;KD_DOWN=稳定按下边沿;KD_UP=稳定松开边沿(下沿后计数)。
* s->down=稳定按住;s->longpressed=按住已 ≥CLICK_MS(长按)。 */
enum { KD_NONE = 0, KD_DOWN = 1, KD_UP = 2 };
static int kd_tick(struct kd_state *s, int raw)
{
unsigned long long now = now_ms();
int ev = KD_NONE;
if (raw != s->raw) {
if (s->raw == 0 && raw != 0) { /* 任何跳变都重记稳定窗口(含原始 raw 变化) */ }
s->raw = raw; s->since = now;
}
int want = s->raw;
if (want != s->stable) {
if (now - s->since >= KEY_DEBOUNCE_MS) {
s->stable = want;
if (s->stable && !s->down) { s->down = 1; s->down_at = now; s->longpressed = 0; ev = KD_DOWN; }
else if (!s->stable && s->down) { s->down = 0; ev = KD_UP; }
}
}
if (s->down && !s->longpressed && now - s->down_at >= CLICK_MS) s->longpressed = 1;
return ev;
}
/* Unblank the panel AFTER the player is frozen: with the player SIGSTOPped
* it can no longer re-blank, so the menu (drawn on fb0) stays visible even
* when the menu opened while the player's screen timeout had blanked the
* panel. The blank state still belongs to the player's state machine — on
* session end restore_backlight() re-blanks when menu_opened_blank, so the
* player resumes into a consistent (blanked) state. */
static void unblank_panel(void)
{
int fbfd = open(FB_DEV, O_RDWR);
if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 0); close(fbfd); }
logmsg("panel unblanked (player frozen)");
}
/* Backlight handling (defined below): the menu only brings the panel back
* when it is actually dark (brightness 0 / panel blanked) and otherwise
* leaves the player's brightness untouched — the old force_backlight()
* yanked the brightness to 101 on every menu open (Bug A). */
static int read_brightness(void);
static void write_brightness(int val);
static void ensure_backlight(void);
/* ---- MeowOS 面板自持(M2.6:播放器已死,MeowOS 拥有面板/背光,无拔河)----
* 电源键按时长三分:单击(短按松开)= 息屏/亮屏;3s=电源菜单;8s=强制关机。
* 息屏 = 亮度0 + FBIOBLANK(4)(面板下电,同时挂起 goodix 触摸,无误触);
* 亮屏 = FBIOBLANK(0) + 恢复亮度。 */
static int meow_screen_on = 1;
static int meow_saved_brightness = 60;
static int meow_auto_slept = 0; /* M2.17:当前是否「超时自动息屏」置位(手动电源息屏不置位) */
static void meow_panel_off(void);
static void meow_panel_on(void);
static void meow_panel_off(void)
{
meow_saved_brightness = read_brightness();
if (meow_saved_brightness <= 0) meow_saved_brightness = 60;
write_brightness(0);
if (screen_off_mode == 0) {
/* panel:面板下电 + FBIOPBLANK(4)(同时挂起 goodix 触摸,无误触) */
int f = open(FB_DEV, O_RDWR);
if (f >= 0) { ioctl(f, FBIOBLANK, 4); close(f); }
} else {
/* black:仅背光 0 全黑(面板仍上电,触摸可唤醒) */
}
meow_screen_on = 0;
logmsg("meow panel OFF (mode=%s brightness 0%s)",
screen_off_mode ? "black" : "panel",
screen_off_mode ? "" : " + blank 4");
}
static void meow_panel_on(void)
{
int f = open(FB_DEV, O_RDWR);
if (f >= 0) { ioctl(f, FBIOBLANK, 0); close(f); }
write_brightness(meow_saved_brightness > 0 ? meow_saved_brightness : 60);
meow_screen_on = 1;
/* M2.8 Bug 3-residual:亮屏必须用 fb_pan_buf0_force 无条件重发 pan。fb_norm_buf0 在
* 缓存 fb_active_yoffset==0 时提前返回,而 FBIOBLANK(4)->(0) 后 LCDC 扫描地址可能已复位
* 到非 buffer0 / 失效位置,缓存仍是 0 导致真正的 re-pan 从未发出 —— M2.7 只加 fb_norm_buf0
* 仍黑屏的原因。主界面都画在 buffer0(mmap 偏移 0),此处强制 re-pan 保证唤醒后落在 buffer0。
* 当前界面内容的重绘由调用方(run_menu 醒屏分支)经 meow_repaint_surface 完成。 */
fb_pan_buf0_force();
logmsg("meow panel ON (blank 0 + brightness %d)", meow_saved_brightness);
}
static void meow_toggle_panel(void) { if (meow_screen_on) meow_panel_off(); else meow_panel_on(); }
/* ================================================================== */
/* key state polling (EVIOCGKEY) */
/* ================================================================== */
struct keypoll {
int fd;
unsigned long cur[KEYBUF_WORDS];
unsigned long prev[KEYBUF_WORDS];
int ok;
};
static int kp_open(struct keypoll *kp, const char *dev)
{
memset(kp, 0, sizeof *kp);
kp->fd = open(dev, O_RDONLY | O_NONBLOCK);
if (kp->fd < 0) { logmsg("open %s: %s", dev, strerror(errno)); return -1; }
if (ioctl(kp->fd, EVIOCGKEY(sizeof kp->cur), kp->cur) < 0) {
logmsg("EVIOCGKEY %s: %s", dev, strerror(errno));
close(kp->fd); return -1;
}
memcpy(kp->prev, kp->cur, sizeof kp->cur);
kp->ok = 1;
logmsg("keypoll %s ready", dev);
return 0;
}
/* 关闭 keypoll(设置页/下拉框等自开设备用;幂等,ok=0 时不动) */
static void kp_close(struct keypoll *kp)
{
if (kp->ok && kp->fd >= 0) close(kp->fd);
kp->ok = 0;
}
/* Poll device key state; call cb(code, is_down) for each edge.
* Returns 1 if any edge. */
static int kp_poll(struct keypoll *kp, int (*cb)(int code, int down, void *ctx),
void *ctx)
{
if (!kp->ok) return 0;
if (ioctl(kp->fd, EVIOCGKEY(sizeof kp->cur), kp->cur) < 0) return 0;
int changed = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp->cur[w] == kp->prev[w]) continue;
for (int b = 0; b < 32; b++) {
int c = (kp->cur[w] >> b) & 1;
int p = (kp->prev[w] >> b) & 1;
if (c != p) {
int code = w * 32 + b;
changed = 1;
if (cb) cb(code, c, ctx);
}
}
kp->prev[w] = kp->cur[w];
}
return changed;
}
static int kp_is_down(const struct keypoll *k, int code)
{
return (k->cur[code / 32] >> (code % 32)) & 1;
}
/* ================================================================== */
/* app scanning (MeowAPPs spec) */
/* ================================================================== */
/* 路由行:route.<act>=<chain>。act=route 键后缀(activity_id 的 app 内部分),
* chain=根屏→目标屏的 activity_id 序列(';' 分隔),链长=逻辑深度。 */
struct meow_route {
char act[16]; /* route 键后缀,如 "nowplaying" */
char chain[MEOW_CHAIN_MAX]; /* 如 "home;search;nowplaying" */
int keep_awake; /* M2.17:该 activity 常亮(app.json awake.<id>=1 / route.<id>.keep_awake=1) */
int fullscreen; /* 活动级全屏声名(app.json route.<id>.fullscreen=1;M3 活动切换时消费) */
};
/* 服务行:service.<svc>=mode:restart:max:window_ms(M1 仅解析存储,M4 保活) */
struct meow_svc {
char name[16];
int mode; /* 0=keepalive 1=ondemand(预留) */
int restart; /* 1=退避重启 */
int max; /* max_restart 熔断阈值 */
int window_ms; /* 重启窗口 */
};
struct app {
char id[MEOW_APP_ID_MAX]; /* M2.52:app_id=app.json 的 package(权威,缺省回退卡上目录名 de->d_name),供 MEOW_LOG_APP 注入 */
char package[MEOW_APP_ID_MAX]; /* 安卓化:app.json 的 package 字段(反域名权威标识;目录名=package),scan_apps 据此定 app_id */
char name[17]; /* ASCII display name */
char exec[129]; /* absolute path */
int ok; /* exec exists and X_OK */
/* ---- 阶段 D · M1:app.json 路由/框架/服务/图标(scan_apps 解析)---- */
char framework; /* '1'=framework=true(看门狗返回发 SIGUSR1) */
char immersive; /* '1'=immersive=true(隐藏虚拟返回键,返回手势归 SDK) */
char fullscreen; /* '1'=app.json fullscreen=true(注入 MEOW_FULLSCREEN=1,SDK 预先设全屏) */
char fonts; /* 字库档:0=默认4K常用 1=大字库GBK 2=Full(app.json "fonts";注入 MEOW_FONT_TIER) */
char icon[129]; /* icon= 路径 */
int nroute; /* route.<id> 行数 */
struct meow_route routes[MEOW_MAX_ROUTES];
int nsvc;
struct meow_svc svcs[MEOW_MAX_SVCS];
};
/* Activity 路由注册表项:app_id 全局唯一的运行时路由层,由 scan_apps(静态 app.json)
* 与 MSG_ACT_DECLARE/HELLO(运行时)并集填充;单一写者=主循环,无需锁。 */
struct meow_acct {
char app_id[MEOW_APP_ID_MAX];
char name[17];
char exec[129];
int ok;
char framework;
char icon[129];
int nroute;
struct meow_route routes[MEOW_MAX_ROUTES];
int nsvc;
struct meow_svc svcs[MEOW_MAX_SVCS];
int declared; /* 是否经 HELLO/ACT_DECLARE 运行时声明过 */
};
static struct meow_acct g_accts[MEOW_MAX_ACTS]; /* 主循环单写者 */
static int g_naccts = 0;
/* 阶段 D · M0:主循环排空 IPC 队列(定义在下方 IPC 骨架区;run_menu/main 调用) */
static void meow_ipc_process(void);
/* ---- 阶段 D · M1:路由注册表辅助(主循环单写者,无锁)---- */
static struct meow_acct *meow_acct_find(const char *app_id)
{
if (!app_id) return NULL;
for (int i = 0; i < g_naccts; i++)
if (!strcmp(g_accts[i].app_id, app_id)) return &g_accts[i];
return NULL;
}
/* 取(或在末尾新建)app_id 对应的注册表项。返回 NULL 当满。 */
static struct meow_acct *meow_acct_upsert(const char *app_id)
{
if (!app_id || !app_id[0]) return NULL;
struct meow_acct *a = meow_acct_find(app_id);
if (a) return a;
if (g_naccts >= MEOW_MAX_ACTS) { logmsg("act: registry full, drop %s", app_id); return NULL; }
a = &g_accts[g_naccts++];
memset(a, 0, sizeof *a);
strncpy(a->app_id, app_id, MEOW_APP_ID_MAX - 1);
return a;
}
/* 由 scan_apps 调用:把 app.json 静态路由整体同步进注册表(扫描即权威,替换)。 */
static void meow_acct_sync(const char *app_id, const struct app *ap)
{
if (!ap) return;
struct meow_acct *a = meow_acct_upsert(app_id);
if (!a) return;
a->framework = ap->framework;
strncpy(a->icon, ap->icon, 128);
strncpy(a->name, ap->name, 16);
strncpy(a->exec, ap->exec, 128);
a->ok = ap->ok;
a->nroute = ap->nroute > MEOW_MAX_ROUTES ? MEOW_MAX_ROUTES : ap->nroute;
for (int i = 0; i < a->nroute; i++) a->routes[i] = ap->routes[i];
a->nsvc = ap->nsvc > MEOW_MAX_SVCS ? MEOW_MAX_SVCS : ap->nsvc;
for (int i = 0; i < a->nsvc; i++) a->svcs[i] = ap->svcs[i];
}
/* 运行时 HELLO:声明 app 存在(补入注册表),记录声明源。 */
static void meow_acct_hello(const char *app_id, uint32_t version, uint32_t flags)
{
struct meow_acct *a = meow_acct_upsert(app_id);
if (a) { a->declared = 1; (void)version; (void)flags; }
logmsg("act: hello app=%s ver=%u flags=%u", app_id, version, flags);
}
/* 运行时 ACT_DECLARE:并集覆盖静态路由。entries=count 个 NUL 结尾 "id=chain" 串。 */
static void meow_acct_declare(const char *app_id, const char *entries, int count)
{
struct meow_acct *a = meow_acct_upsert(app_id);
if (!a || !entries || count <= 0) return;
a->declared = 1;
const char *p = entries;
for (int k = 0; k < count && *p; k++) {
const char *eq = strchr(p, '=');
if (eq && eq > p) {
int idlen = (int)(eq - p);
if (idlen >= 16) idlen = 15;
char id[16];
memcpy(id, p, idlen); id[idlen] = 0;
const char *chain = eq + 1;
int ri = -1;
for (int i = 0; i < a->nroute; i++)
if (!strcmp(a->routes[i].act, id)) { ri = i; break; }
if (ri < 0) {
if (a->nroute >= MEOW_MAX_ROUTES) { p += strlen(p) + 1; continue; }
ri = a->nroute++;
}
strncpy(a->routes[ri].act, id, 15);
strncpy(a->routes[ri].chain, chain, MEOW_CHAIN_MAX - 1);
}
p += strlen(p) + 1;
}
}
/* 链路深度 = ';' 分隔段数(根屏→目标屏的逻辑链长)。 */
static int chain_depth(const char *c)
{
if (!c || !*c) return 0;
int d = 1;
for (; *c; c++) if (*c == ';') d++;
return d;
}
/* 解析 activity 链路:填 out_chain(root..target)与 depth。
* 返回 0=成功;MEOW_ERR_NOENT=app 未注册 / activity 未注册。
* 规则(FRAMEWORK_DESIGN §2):route 缺失(app 未声明任何路由)→ chain=activity、深度 1 兜底;
* 声明了路由但 activity 非 route 键 → MEOW_ERR_NOENT(activity 未注册)。 */
static int resolve_chain(const char *app_id, const char *activity, char *out, int *depth)
{
if (!out || !depth) return MEOW_ERR_INVAL;
struct meow_acct *a = meow_acct_find(app_id);
if (!a) return MEOW_ERR_NOENT;
if (a->nroute == 0) {
strncpy(out, activity, MEOW_CHAIN_MAX - 1);
*depth = 1;
return MEOW_OK;
}
for (int i = 0; i < a->nroute; i++) {
if (!strcmp(a->routes[i].act, activity)) {
strncpy(out, a->routes[i].chain, MEOW_CHAIN_MAX - 1);
*depth = chain_depth(a->routes[i].chain);
return MEOW_OK;
}
}
return MEOW_ERR_NOENT;
}
/* ---- M2.17 keep_awake:前台当前顶层 activity 追踪 ----
* 前台应用经 IPC MSG_ACT_FOCUS/BACK 上报其当前顶层 activity;fg_keep_awake() 依注册表判断该
* activity 是否声明 keep_awake(app.json awake.<id>=1 / route.<id>.keep_awake=1;运行时同类)。
* 主界面(无上报/空)= 非 keep_awake(主页空闲照常超时息屏)。单一写者=主循环(IPC 排空处)。 */
static char g_fg_app[MEOW_APP_ID_MAX] = "";
static char g_fg_act[MEOW_ACT_MAX] = "";
static void meow_fg_act_set(const char *app_id, const char *act)
{
if (!app_id) return;
strncpy(g_fg_app, app_id, MEOW_APP_ID_MAX - 1);
g_fg_app[MEOW_APP_ID_MAX - 1] = 0;
if (act) { strncpy(g_fg_act, act, MEOW_ACT_MAX - 1); g_fg_act[MEOW_ACT_MAX - 1] = 0; }
else g_fg_act[0] = 0;
logmsg("keep_awake: fg_act app=%s act=%s", g_fg_app, g_fg_act);
}
static void meow_fg_act_back(void)
{
g_fg_app[0] = 0;
g_fg_act[0] = 0;
logmsg("keep_awake: fg_act back to home (cleared)");
}
/* 前台当前 activity 是否 keep_awake(无上报/未注册/未声明=0,即照常超时息屏)。 */
static int fg_keep_awake(void)
{
if (!g_fg_app[0] || !g_fg_act[0]) return 0;
struct meow_acct *a = meow_acct_find(g_fg_app);
if (!a) return 0;
for (int i = 0; i < a->nroute; i++)
if (!strcmp(a->routes[i].act, g_fg_act)) return a->routes[i].keep_awake;
return 0;
}
/* 标识符字符(含 '.',用于防止 json_str 误命中更长键的一部分) */
static int ident_ch(char c)
{
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '_' || c == '.';
}
/* 极简 JSON/扁平值提取:在一行里找 key,读取其值(跳过 ':'/'=' 与可选引号)。
* 支持 JSON `"name": "Hello"` 与扁平 `framework=true`。返回拷贝到 out 的字符数。
* 返回 0=未命中/空。 */
static int json_str(const char *buf, const char *key, char *out, int olen)
{
out[0] = 0;
if (!buf || !key || olen <= 0) return 0;
const char *p = strstr(buf, key);
if (!p) return 0;
if (p > buf && ident_ch((unsigned char)p[-1])) return 0; /* 键前缀需独立 */
p += strlen(key);
if (*p == '"') p++; /* JSON 键自身右引号:跳到 ':'/'=' */
while (*p && (*p == ':' || *p == '=')) p++;
while (*p == ' ' || *p == '\t') p++;
if (*p == '"') p++; /* 值左引号(JSON 字符串值) */
int i = 0;
while (*p && *p != '"' && *p != ',' && *p != '\n' && *p != '\r' && i < olen - 1)
out[i++] = *p++;
out[i] = 0;
while (i > 0 && (out[i - 1] == ' ' || out[i - 1] == '\t')) out[--i] = 0;
return i;
}
/* state: 0 ok / 1 no storage card / 2 no usable apps */
static int card_present(void)
{
FILE *f = fopen(MOUNTS, "r");
if (!f) return 0;
char line[256];
int found = 0;
while (fgets(line, sizeof line, f)) {
if (strstr(line, " /usr/data/mnt/sd_0 ")) { found = 1; break; }
}
fclose(f);
return found;
}
/* ================================================================== */
/* MeowOS 系统拔营(M2.2 批次):TF 卡挂载 + WiFi 联网 */
/* 官方环境由 hiby_player / sys_server 负责这两件事;MeowOS 作为主系统 */
/* 时无播放器,必须由 launcher 主动接管。失败不阻塞主界面,后台周期重试。 */
/* ================================================================== */
static int sd_mounted(void)
{
return card_present();
}
/* TF 卡设备节点是否出现(任一常见节点即视为在卡) */
static int sd_device_present(void)
{
return access("/dev/mmcblk0", F_OK) == 0 || access("/dev/mmcblk0p1", F_OK) == 0;
}
/* 主动挂载 TF 卡到 /usr/data/mnt/sd_0。返回 1=已挂载,0=尚未(调用方轮询重试)。
* 依次尝试 /dev/mmcblk0、/dev/mmcblk0p1,先 -t exfat 再自动探测;卡未插入返回 0。 */
static int ensure_card_mounted(void)
{
if (sd_mounted()) return 1;
if (!sd_device_present()) return 0;
const char *nodes[2] = { "/dev/mmcblk0", "/dev/mmcblk0p1" };
for (int i = 0; i < 2; i++) {
if (access(nodes[i], F_OK) != 0) continue;
char cmd[160];
snprintf(cmd, sizeof cmd, "mkdir -p /usr/data/mnt/sd_0; "
"PATH=/bin:/sbin:/usr/bin:/usr/sbin mount -t exfat %s /usr/data/mnt/sd_0 2>/dev/null",
nodes[i]);
if (system(cmd) == 0 && sd_mounted()) { logmsg("sd_0 mounted: %s", nodes[i]); return 1; }
snprintf(cmd, sizeof cmd, "mkdir -p /usr/data/mnt/sd_0; "
"PATH=/bin:/sbin:/usr/bin:/usr/sbin mount %s /usr/data/mnt/sd_0 2>/dev/null",
nodes[i]);
if (system(cmd) == 0 && sd_mounted()) { logmsg("sd_0 mounted (auto): %s", nodes[i]); return 1; }
}
return 0;
}
/* ================================================================== */
/* M2.5x 多字体库扫描 + M2.6 字重结构 / preset 动态化 */
/* 字体目录 = <卡根>/MeowOS/fonts/<family>/<weight>/,每套含三件套: */
/* font.png(预渲染灰度 PNG,bit depth 4|8)+ font.idx(MFNI v2 索引) */
/* + font.json(元数据 + 完整性校验)。缺任一件 → 该套不进列表 + 日志。 */
/* font.json schema(定版): */
/* { */
/* "family": "家族标识(同目录名/family)", // 信息用,不参与校验 */
/* "name": "下拉显示名", // 缺则回退目录名 */
/* "weight": "regular", // 字重 slug(缺省 regular) */
/* "glyph_count": 10000, // 信息用 */
/* "charset": "common|gbk|full", // 档位(M2.5x,驱动按需加载) */
/* "size_list": [28,20], // 信息用 */
/* "bit_depth": 4, // 信息用(4|8) */
/* "sha256_png": "<64hex>", // font.png 校验 */
/* "sha256_idx": "<64hex>", // font.idx 校验 */
/* "source": "...", // 可选:来源 */
/* "license": "..." // 可选:许可 */
/* } */
/* 校验:sha256 不匹配仅 WARN 不硬失败(仍可选);缺三件套或 font.json */
/* 解析不出 name → 跳过 + 日志。仅启动/进设置页扫描一次(勿放热路径)。 */
/* ================================================================== */
/* 计算文件 sha256(走 busybox/宿主 sha256sum;输出小写 64hex)。返回 0 成功。 */
static int font_sha256_file(const char *path, char *out, int olen)
{
out[0] = 0;
/* 单引号转义防 shell 注入(路径含 ' 时安全;转义后超长则截断 → sha256sum 失败走 WARN) */
char esc[1024]; int j = 0;
for (const char *p = path; *p && j < (int)sizeof esc - 4; ) {
if (*p == '\'') { memcpy(esc + j, "'\\''", 4); j += 4; p++; }
else esc[j++] = *p++;
}
esc[j] = 0;
char cmd[1080];
snprintf(cmd, sizeof cmd, "sha256sum '%s' 2>/dev/null", esc);
FILE *f = popen(cmd, "r");
if (!f) return -1;
char line[160];
int ok = -1;
if (fgets(line, sizeof line, f)) {
int i = 0, k = 0;
while (line[i] == ' ' || line[i] == '\t') i++; /* 跳前导空白 */
while (line[i] && k < 64) {
char c = line[i++];
if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))
out[k++] = c;
else break;
}
out[k] = 0;
ok = (k == 64) ? 0 : -1;
}
pclose(f);
return ok;
}
/* 比较两个 sha256 十六进制串(大小写不敏感)。均为 64hex 才比对,否则视为不匹配。 */
static int font_sha_eq(const char *a, const char *b)
{
if (!a || !b || strlen(a) < 64 || strlen(b) < 64) return 0;
for (int i = 0; i < 64; i++) {
char ca = a[i], cb = b[i];
if (ca >= 'A' && ca <= 'F') ca += 32;
if (cb >= 'A' && cb <= 'F') cb += 32;
if (ca != cb) return 0;
}
return 1;
}
/* 解析 font.json:提取 name / sha256_png / sha256_idx / weight(M2.6,缺省 "regular")。
* 文件缺失或解析不出则留空(name 空 → 调用方回退目录名;weight 空 → 回退 "regular")。 */
static void meow_font_json_probe(const char *json_path, char *name, int nlen,
char *sha_png, int slen, char *sha_idx, int xlen,
char *charset, int clen, int *glyph_count,
char *weight, int wlen)
{
name[0] = 0; sha_png[0] = 0; sha_idx[0] = 0;
if (charset) charset[0] = 0;
if (glyph_count) *glyph_count = 0;
if (weight) weight[0] = 0;
FILE *f = fopen(json_path, "r");
if (!f) return;
char buf[256];
while (fgets(buf, sizeof buf, f)) {
if (!name[0]) json_str(buf, "name", name, nlen);
if (!sha_png[0]) json_str(buf, "sha256_png", sha_png, slen);
if (!sha_idx[0]) json_str(buf, "sha256_idx", sha_idx, xlen);
/* M2.5x 字体档位:charset(common|gbk|full)+ glyph_count(信息用,驱动档位分层/标注)。 */
if (charset && !charset[0]) {
char v[8] = "";
if (json_str(buf, "charset", v, sizeof v) > 0 && v[0])
snprintf(charset, clen, "%s", v);
}
if (glyph_count && *glyph_count == 0) {
char v[16] = "";
if (json_str(buf, "glyph_count", v, sizeof v) > 0 && v[0])
*glyph_count = atoi(v);
}
/* M2.6 字重:weight(regular|bold|italic|…),缺省 "regular"。 */
if (weight && !weight[0]) {
char v[32] = "";
if (json_str(buf, "weight", v, sizeof v) > 0 && v[0])
snprintf(weight, wlen, "%s", v);
}
}
fclose(f);
}
/* 字重 slug → 显示名(首字母大写、连字符/下划线转空格:regular→Regular、bold→Bold、italic→Italic)。 */
static void font_weight_display(const char *weight, char *out, int olen)
{
const char *w = (weight && weight[0]) ? weight : "regular";
out[0] = 0;
int o = 0, cap = 1;
for (const char *p = w; *p && o < olen - 1; p++) {
char c = *p;
if (c == '_' || c == '-') { if (o < olen - 1) out[o++] = ' '; cap = 1; continue; }
out[o++] = cap ? (char)((c >= 'a' && c <= 'z') ? c - 32 : c) : c;
cap = 0;
}
out[o] = 0;
}
/* 档位大小标记(设置页字体选项标注):common≈4k / gbk≈20k / full。
* 由 font.json 的 charset + glyph_count 派生;缺 charset 回退 common。 */
static void font_tier_marker(const char *charset, int glyph_count, char *out, int olen)
{
const char *tier = (charset && charset[0]) ? charset : "common";
if (!strcmp(tier, "full")) { snprintf(out, olen, "full"); return; }
if (glyph_count > 0)
snprintf(out, olen, "%s≈%dk", tier, (glyph_count + 500) / 1000);
else
snprintf(out, olen, "%s", tier);
}
/* 探测一个 <卡根>/MeowOS/fonts/<family>/<weight> 套件是否合法:三件套齐全 → 解析 label+档位+字重。
* 返回 1=进列表,0=跳过(缺组件/json 不可读)。sha 失败仅 WARN。
* 输出:label=显示名 `<name> <Weight>(common≈4k)`(如 `MiSans Regular(common≈4k)`);
* charset/glyph_count=档位信息;wtout=weight slug(缺省 "regular")。 */
static int meow_font_probe_weight(const char *dirpath, const char *family, const char *weight,
char *label, int llen,
char *charset, int clen, int *glyph_count,
char *wtout, int wtlen, char *nameout, int namelen)
{
char ppath[400], ipath[400], jpath[400];
snprintf(ppath, sizeof ppath, "%s/font.png", dirpath);
snprintf(ipath, sizeof ipath, "%s/font.idx", dirpath);
snprintf(jpath, sizeof jpath, "%s/font.json", dirpath);
if (access(ppath, R_OK) != 0 || access(ipath, R_OK) != 0 || access(jpath, R_OK) != 0) {
logmsg("fonts: skip '%s/%s' (缺 font.png/font.idx/font.json)", family, weight);
return 0;
}
char name[64] = "", jsha_png[80] = "", jsha_idx[80] = "";
char cs[8] = ""; int gc = 0; char wts[32] = "";
meow_font_json_probe(jpath, name, sizeof name, jsha_png, sizeof jsha_png,
jsha_idx, sizeof jsha_idx, cs, sizeof cs, &gc, wts, sizeof wts);
const char *wt = wts[0] ? wts : (weight ? weight : "regular");
if (wtout) snprintf(wtout, wtlen, "%s", wt);
if (nameout) snprintf(nameout, namelen, "%s", name[0] ? name : family);
/* 显示名 = `<name> <Weight>(common≈4k)`(字重缺省 Regular) */
char wd[24]; font_weight_display(wt, wd, sizeof wd);
char marker[24];
font_tier_marker(cs, gc, marker, sizeof marker);
snprintf(label, llen, "%s %s(%s)", name[0] ? name : family, wd, marker);
if (charset) snprintf(charset, clen, "%s", cs[0] ? cs : "common");
if (glyph_count) *glyph_count = gc;
/* 完整性校验:仅 WARN,不硬失败;该套仍标记可选。 */
char apng[80] = "", aidx[80] = "";
if (font_sha256_file(ppath, apng, sizeof apng) != 0)
logmsg("fonts: '%s/%s' sha256(png) 不可用,跳过校验", family, wt);
else if (jsha_png[0] && !font_sha_eq(apng, jsha_png))
logmsg("fonts: '%s/%s' WARN sha256_png 不匹配(仍可选)", family, wt);
if (font_sha256_file(ipath, aidx, sizeof aidx) != 0)
logmsg("fonts: '%s/%s' sha256(idx) 不可用,跳过校验", family, wt);
else if (jsha_idx[0] && !font_sha_eq(aidx, jsha_idx))
logmsg("fonts: '%s/%s' WARN sha256_idx 不匹配(仍可选)", family, wt);
logmsg("fonts: add '%s/%s' (%s) charset=%s glyph_count=%d", family, wt, label,
cs[0]?cs:"common", gc);
return 1;
}
/* M2.58 字体索引:CSV 字段写入(始终双引号包裹,内嵌双引号加倍)。返回写入长度。 */
static int font_index_csvq(char *dst, int olen, const char *s)
{
int o = 0;
if (olen <= 0) return 0;
if (o < olen) dst[o++] = '"';
for (; *s && o < olen - 3; s++) {
if (*s == '"') dst[o++] = '"';
if (o < olen - 2) dst[o++] = *s;
}
if (o < olen) dst[o++] = '"';
dst[o] = 0;
return o;
}
/* 把当前 font_presets* 扫描结果落盘为 <card_root>/MeowOS/config/fonts/index(覆盖)。
* 每套一条 `"family","weight","path","label","tier",glyph_count`;path=卡根相对 font.png。
* 目录不存在则 mkdir -p;无卡(mkdir/fopen 失败)→ 静默放弃(仅内存默认项)。
* 该文件是 SDK meow_font_get_available_count()/enumerate() 的持久数据源(规格 §5.7)。 */
static void meow_font_write_index(const char *card_root)
{
if (!card_root || !card_root[0]) return;
char t1[360], t2[380], base[400], path[420];
snprintf(t1, sizeof t1, "%s/MeowOS", card_root);
snprintf(t2, sizeof t2, "%s/MeowOS/config", card_root);
snprintf(base, sizeof base, "%s/MeowOS/config/fonts", card_root);
/* 无卡(card_root 父目录不存在 → ENOENT)→ 静默放弃:仅内存默认项,不刷系统日志。 */
if (mkdir(t1, 0755) != 0 && errno != EEXIST) {
if (errno != ENOENT) logmsg("fonts: index mkdir fail %s", t1);
return;
}
if (mkdir(t2, 0755) != 0 && errno != EEXIST) { logmsg("fonts: index mkdir fail %s", t2); return; }
if (mkdir(base, 0755) != 0 && errno != EEXIST) { logmsg("fonts: index mkdir fail %s", base); return; }
snprintf(path, sizeof path, "%s/index", base);
FILE *f = fopen(path, "w");
if (!f) { logmsg("fonts: index fopen fail %s", path); return; }
char line[1024];
for (int i = 0; i < font_presets_n; i++) {
const char *fam = font_preset_fams[i] ? font_preset_fams[i] : "default";
const char *wt = font_preset_wts[i] ? font_preset_wts[i] : "regular";
const char *val = font_presets[i] ? font_presets[i] : "MeowOS/fonts/default/regular/font.png";
const char *lbl = font_preset_labels[i] ? font_preset_labels[i] : fam;
const char *tier = font_preset_tiers[i] ? font_preset_tiers[i] : "common";
int o = 0;
o = font_index_csvq(line, sizeof line, fam);
line[o++] = ','; line[o] = 0;
o += font_index_csvq(line + o, sizeof line - (size_t)o, wt);
line[o++] = ','; line[o] = 0;
o += font_index_csvq(line + o, sizeof line - (size_t)o, val);
line[o++] = ','; line[o] = 0;
o += font_index_csvq(line + o, sizeof line - (size_t)o, lbl);
line[o++] = ','; line[o] = 0;
o += font_index_csvq(line + o, sizeof line - (size_t)o, tier);
line[o++] = ','; line[o] = 0;
snprintf(line + o, sizeof line - (size_t)o, "%d\n", font_preset_glyphs[i]);
fputs(line, f);
}
fclose(f);
logmsg("fonts: index written (%d entries) -> %s", font_presets_n, path);
}
/* 扫描 <card_root>/MeowOS/fonts/ 下两级 <family>/<weight>/,重建字体 preset 列表。
* 每个 (family,weight) 组合一套(如 misans/regular、misans/bold);family 级 'default' 由
* 索引 0(默认字体)单独处理,不进循环,避免重复。上限 FONT_PRESETS_MAX
* (1 默认 + ≤16 用户字体)。无卡/无目录时仅默认。
*
* M2.58 释放语义(规格 §5.7 需求 4「扫→写 index→释放(或按需读 index)」结论):
* - 持久来源 = <card_root>/MeowOS/config/fonts/index(扫描后由 meow_font_write_index 落盘)。
* SDK 只读该文件,从不依赖本内存数组。
* - font_presets* / *_buf 均为「静态、有界(FONT_PRESETS_MAX=17)、非堆」数组:无内存泄漏,
* 故无运行期 free;「释放」含义 = 两个扫描点(启动 / 进设置页)重建 + 每套固定上限防膨胀。
* - 设置字体列表(font.file / font.class_* 下拉、family→字重子菜单)在本视图内显示时用
* 该数组;离开字体列表视图(run_settings_page 退出)无需显式 free(静态数组),下次
* 进设置/重扫自动重建。
* - ⚠️ 未做「启动后即清空数组」:meow_font_effective_paths_tiered(launch_app 大字库档位
* 选库)与 meow_font_reload_multifont 在运行期持续读 font_presets_n,清空会造成大字库
* 档位退化(red line「保持档位/多字体行为」)。如需「零驻留」,须把这几个核心路径改为
* 直接读 index(较大重构,超出本批边界,留待后续)。 */
static void meow_font_scan_presets_root(const char *card_root)
{
font_presets_n = 0;
font_presets[0] = "MeowOS/fonts/default/regular/font.png"; /* 默认字库 = MeowOS/fonts/default/regular */
font_preset_labels[0] = font_preset_label_buf[0]; /* 指向缓冲(运行时派生含字重+大小标记) */
font_preset_fams[0] = font_preset_fam_buf[0];
font_preset_wts[0] = font_preset_wt_buf[0];
font_preset_names[0] = font_preset_name_buf[0];
/* 默认套也读 font.json 档位+字重(存在则跟真实档,缺则 common/regular);供分层装载/标注。 */
{
char jp[400], cs[8] = ""; int gc = 0;
char nm0[64] = "", sp0[80] = "", si0[80] = "", wt0[32] = "";
snprintf(jp, sizeof jp, "%s/%s/default/regular/font.json", card_root, MEOW_FONTS_CFG);
meow_font_json_probe(jp, nm0, sizeof nm0, sp0, sizeof sp0, si0, sizeof si0,
cs, sizeof cs, &gc, wt0, sizeof wt0);
char mk[24]; font_tier_marker(cs, gc, mk, sizeof mk);
char wd[24]; font_weight_display(wt0[0] ? wt0 : "regular", wd, sizeof wd);
snprintf(font_preset_label_buf[0], sizeof font_preset_label_buf[0], "默认字体 %s(%s)", wd, mk);
snprintf(font_preset_tier_buf[0], sizeof font_preset_tier_buf[0], "%s", cs[0]?cs:"common");
snprintf(font_preset_fam_buf[0], sizeof font_preset_fam_buf[0], "default");
snprintf(font_preset_wt_buf[0], sizeof font_preset_wt_buf[0], "%s", wt0[0]?wt0:"regular");
snprintf(font_preset_name_buf[0], sizeof font_preset_name_buf[0], "默认字体");
font_preset_tiers[0] = font_preset_tier_buf[0];
font_preset_glyphs[0] = gc;
}
font_presets_n = 1;
char root[340];
snprintf(root, sizeof root, "%s/%s", card_root, MEOW_FONTS_CFG);
DIR *d = opendir(root);
if (d) { /* 无卡/无字库目录 → 跳过扫描,仅默认 */
struct dirent *de;
while ((de = readdir(d)) && font_presets_n < FONT_PRESETS_MAX) {
if (de->d_name[0] == '.') continue;
if (!strcmp(de->d_name, "default")) continue; /* 默认字体由索引 0 单独处理 */
char fampath[400];
snprintf(fampath, sizeof fampath, "%s/%s", root, de->d_name);
DIR *w = opendir(fampath); /* 一级:<family>/ */
if (!w) continue;
struct dirent *we;
while ((we = readdir(w)) && font_presets_n < FONT_PRESETS_MAX) {
if (we->d_name[0] == '.') continue;
char dirpath[400], label[96], cs[8] = ""; int gc = 0; char wt[32] = "", nm[64] = "";
snprintf(dirpath, sizeof dirpath, "%s/%s", fampath, we->d_name);
if (meow_font_probe_weight(dirpath, de->d_name, we->d_name, label, sizeof label,
cs, sizeof cs, &gc, wt, sizeof wt, nm, sizeof nm) == 0)
continue; /* 缺组件/损坏 → 跳过该套 */
int i = font_presets_n;
snprintf(font_preset_val_buf[i], sizeof font_preset_val_buf[i],
"%s/%s/%s/font.png", MEOW_FONTS_CFG, de->d_name, we->d_name);
snprintf(font_preset_label_buf[i], sizeof font_preset_label_buf[i], "%s", label);
snprintf(font_preset_tier_buf[i], sizeof font_preset_tier_buf[i], "%s", cs[0]?cs:"common");
snprintf(font_preset_fam_buf[i], sizeof font_preset_fam_buf[i], "%s", de->d_name);
snprintf(font_preset_wt_buf[i], sizeof font_preset_wt_buf[i], "%s", wt);
snprintf(font_preset_name_buf[i], sizeof font_preset_name_buf[i], "%s", nm);
font_presets[i] = font_preset_val_buf[i];
font_preset_labels[i] = font_preset_label_buf[i];
font_preset_tiers[i] = font_preset_tier_buf[i];
font_preset_fams[i] = font_preset_fam_buf[i];
font_preset_wts[i] = font_preset_wt_buf[i];
font_preset_names[i] = font_preset_name_buf[i];
font_preset_glyphs[i] = gc;
font_presets_n++;
}
closedir(w);
}
closedir(d);
}
/* M2.58:扫描结果落盘为 <card_root>/MeowOS/config/fonts/index(含默认行 index0),
* 作为 SDK meow_font_get_available_count()/enumerate() 的持久数据源;无卡时仅默认 1 行,
* 由 meow_font_write_index 内部 mkdir-fail 静默放弃(此路径不刷屏)。 */
meow_font_write_index(card_root);
}
/* 设备默认卡根扫描(等价现状:无卡时 opendir 失败 → 仅默认字体)。 */
static void meow_font_scan_presets(void)
{
meow_font_scan_presets_root("/usr/data/mnt/sd_0");
}
/* wlan0 是否已联网(链路 up,与状态栏 WIFI 指示一致) */
static int wlan_connected(void)
{
int fd = open("/sys/class/net/wlan0/operstate", O_RDONLY);
if (fd < 0) return 0;
char x[8];
int n = read(fd, x, sizeof x - 1);
close(fd);
if (n <= 0) return 0;
x[n] = 0;
return !strncmp(x, "up", 2);
}
/* wlan0 网卡是否 up(管理态 IFF_UP,非载波):读 /sys/class/net/wlan0/flags(十六进制如 "0x1003")。
* M2.48:用于区分「网卡 up 但未关联 AP」(→显示 WiFi 开未连接)与「已关联」(wlan_connected 才是关联态)。
* 接口不存在返回 0。IFF_UP=0x1。不依赖 read_sys_str(其定义在本函数之后)。 */
static int wlan_radio_up(void)
{
if (access("/sys/class/net/wlan0", F_OK)) return 0;
int fd = open("/sys/class/net/wlan0/flags", O_RDONLY);
if (fd < 0) return 0;
char b[16];
ssize_t r = read(fd, b, sizeof b - 1);
close(fd);
if (r <= 0) return 0;
b[r] = 0;
return (int)(strtoul(b, NULL, 16) & 0x1UL);
}
/* 是否有 comm==name 的进程在运行(避免重复拉起 wifi 进程) */
static int proc_by_comm(const char *target)
{
DIR *d = opendir("/proc");
if (!d) return 0;
struct dirent *de;
int found = 0;
while ((de = readdir(d))) {
if (de->d_name[0] < '0' || de->d_name[0] > '9') continue;
char path[64], buf[256];
snprintf(path, sizeof path, "/proc/%s/comm", de->d_name);
int fd = open(path, O_RDONLY);
if (fd < 0) continue;
ssize_t n = read(fd, buf, sizeof buf - 1);
close(fd);
if (n <= 0) continue;
buf[n] = 0;
if (!strncmp(buf, target, strlen(target))) { found = 1; break; }
}
closedir(d);
return found;
}
/* 确保联网:wlan0 未 up 且尚无 wpa_supplicant 在跑 → 调用 /usr/bin/wifi_up.sh 连保存网络。
* 幂等:连上 / 已有 wpa_supplicant 则不动;每轮询重试,不阻塞主界面。
* M2.39:尊重 network.wifi_enabled —— 用户在设置页关 WiFi(=0)则不自动拉起(否则与
* 手动关冲突);取值失败(未 init)保默认 1=允许自动连网。 */
static void ensure_wifi(void)
{
int enabled = 1;
if (meow_settings_get_bool("network.wifi_enabled", &enabled) == 0 && !enabled) return;
if (wlan_connected()) return;
if (proc_by_comm("wpa_supplicant")) return;
logmsg("wifi not up -> /usr/bin/wifi_up.sh");
system("PATH=/bin:/sbin:/usr/bin:/usr/sbin /usr/bin/wifi_up.sh");
}
/* 解析一个 app.json 流 → 填充 struct app 的 name/exec/icon/framework/routes/svcs。
* 极简:不做 JSON 校验,仅匹配 key 与路由/服务扁平行(JSON 或扁平皆可)。
* 不设 name/exec(由调用方从返回的 app 再复制),成功判定由调用方做。 */
static void meow_parse_app_json(FILE *f, struct app *a)
{
char buf[256];
char name[17] = "", exec[129] = "", icon[129] = "";
char package[MEOW_APP_ID_MAX] = "";
int fram = 0, immersive = 0;
int fullscreen = 0; /* app.json fullscreen=<true|1>(注入 MEOW_FULLSCREEN env,SDK 预先设全屏) */
int fonts = 0, fonts_set = 0; /* app.json "fonts":<档>(0=默认4K常用 1=大字库GBK 2=Full;缺省 0) */
struct app tmp;
memset(&tmp, 0, sizeof tmp);
while (fgets(buf, sizeof buf, f)) {
if (!name[0]) json_str(buf, "name", name, sizeof name);
if (!exec[0]) json_str(buf, "exec", exec, sizeof exec);
if (!icon[0]) json_str(buf, "icon", icon, sizeof icon);
if (!package[0]) json_str(buf, "package", package, sizeof package); /* 安卓化权威 app_id */
/* route.<id>=<chain>(activity 路由,M1) */
char *rp = strstr(buf, "route.");
if (rp && tmp.nroute < MEOW_MAX_ROUTES) {
rp += 6; /* 跳过 "route." */
int idl = 0; char id[32] = ""; /* 读满整键(识别 ".fullscreen" 后缀,cap 31) */
while (*rp && *rp != '"' && *rp != ':' && *rp != '=' && idl < 31)
id[idl++] = *rp++;
id[idl] = 0;
if (idl > 0) {
char *vp = rp;
while (*vp && *vp != ':' && *vp != '=') vp++;
if (*vp == ':' || *vp == '=') vp++;
while (*vp == ' ' || *vp == '\t') vp++;
if (*vp == '"') vp++;
/* route.<id>.fullscreen=<on>:活动级全屏声明(M1 顺带;M3 活动切换时消费)。
* 命中既有 route.<id> 置 fullscreen;不新建 route(避免 id 被截断误建 "…fulls")。 */
if (idl >= 11 && !strcmp(id + idl - 11, ".fullscreen")) {
id[idl - 11] = 0; /* 去掉 ".fullscreen" 还原 act */
char v[8] = ""; int k = 0;
while (*vp && *vp != '"' && *vp != ',' && *vp != '\n' &&
*vp != '\r' && k < 7) v[k++] = *vp++;
v[k] = 0;
int on = (!strcmp(v, "1") || !strcmp(v, "true") || !strcmp(v, "yes"));
for (int i = 0; i < tmp.nroute; i++)
if (!strcmp(tmp.routes[i].act, id)) { tmp.routes[i].fullscreen = on; break; }
} else {
int vl = 0; char chain[MEOW_CHAIN_MAX] = "";
while (*vp && *vp != '"' && *vp != ',' && *vp != '\n' &&
*vp != '\r' && vl < MEOW_CHAIN_MAX - 1)
chain[vl++] = *vp++;
chain[vl] = 0;
while (vl > 0 && (chain[vl - 1] == ' ' || chain[vl - 1] == '\t'))
chain[--vl] = 0;
strncpy(tmp.routes[tmp.nroute].act, id, 15);
tmp.routes[tmp.nroute].act[15] = 0; /* 显式收尾(原依赖 id 读长 ≤15) */
strncpy(tmp.routes[tmp.nroute].chain, chain, MEOW_CHAIN_MAX - 1);
tmp.nroute++;
}
}
}
/* awake.<id>=1(M2.17 keep_awake:该 activity 常亮,超时不自动息屏)。
* 命中的既有 route.<id> 置 keep_awake;无 route 则新建单 activity 路由。 */
char *ap = strstr(buf, "awake.");
if (ap && tmp.nroute < MEOW_MAX_ROUTES) {
ap += 6; /* 跳过 "awake." */
int al = 0; char aid[16] = "";
while (*ap && *ap != '"' && *ap != ':' && *ap != '=' && al < 15)
aid[al++] = *ap++;
aid[al] = 0;
if (al > 0) {
char *vp = ap;
while (*vp && *vp != ':' && *vp != '=') vp++;
if (*vp == ':' || *vp == '=') vp++;
while (*vp == ' ' || *vp == '\t') vp++;
if (*vp == '"') vp++;
int on = atoi(vp);
int ri = -1;
for (int i = 0; i < tmp.nroute; i++)
if (!strcmp(tmp.routes[i].act, aid)) { ri = i; break; }
if (ri < 0) {
ri = tmp.nroute++;
strncpy(tmp.routes[ri].act, aid, 15);
strncpy(tmp.routes[ri].chain, aid, MEOW_CHAIN_MAX - 1); /* 单 activity 兜底链路 */
}
tmp.routes[ri].keep_awake = (on != 0);
}
}
/* framework=true(看门狗返回发 SIGUSR1,M1 解析存储) */
if (!fram && strstr(buf, "framework")) {
char v[8] = "";
if (json_str(buf, "framework", v, sizeof v) > 0) {
if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes"))
fram = 1;
}
}
/* immersive=true(隐藏虚拟返回键、返回手势移交 SDK;写 /tmp/meowapp_state) */
if (!immersive && strstr(buf, "immersive")) {
char v[8] = "";
if (json_str(buf, "immersive", v, sizeof v) > 0) {
if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes"))
immersive = 1;
}
}
/* fullscreen=true(app 级:注入 MEOW_FULLSCREEN=1,SDK 端 meow_ui_init 预先设全屏)。
* 与 immersive 是两回事:fullscreen 隐藏状态栏/沉浸,immersive 关虚拟返回键。 */
if (!fullscreen && strstr(buf, "fullscreen")) {
char v[8] = "";
if (json_str(buf, "fullscreen", v, sizeof v) > 0) {
if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes"))
fullscreen = 1;
}
}
/* "fonts":<档>(字库档:0=默认4K常用 1=大字库GBK 2=Full;可选,缺省 0)。
* launcher 注入 MEOW_FONT_TIER;大字库装载由后续 font 代理实现。 */
if (!fonts_set && strstr(buf, "fonts")) {
char v[8] = "";
if (json_str(buf, "fonts", v, sizeof v) > 0) {
if (!strcmp(v, "common") || !strcmp(v, "0")) fonts = 0;
else if (!strcmp(v, "gbk") || !strcmp(v, "1")) fonts = 1;
else if (!strcmp(v, "full") || !strcmp(v, "2")) fonts = 2;
else fonts = atoi(v); /* 数字兜底(越界收敛到 [0,2]) */
if (fonts < 0) fonts = 0;
if (fonts > 2) fonts = 2;
fonts_set = 1;
}
}
/* service.<svc>=mode:restart:max:window_ms(M1 解析存储,保活 M4) */
char *sp = strstr(buf, "service.");
if (sp && tmp.nsvc < MEOW_MAX_SVCS) {
sp += 8; /* 跳过 "service." */
int sl = 0; char nm[16] = "";
while (*sp && *sp != '"' && *sp != ':' && *sp != '=' && sl < 15)
nm[sl++] = *sp++;
nm[sl] = 0;
if (sl > 0) {
char *vp = sp;
while (*vp && *vp != ':' && *vp != '=') vp++;
if (*vp == ':' || *vp == '=') vp++;
while (*vp == ' ' || *vp == '\t') vp++;
if (*vp == '"') vp++;
int m = 0, r = 0, mx = 0, w = 0;
sscanf(vp, "%d:%d:%d:%d", &m, &r, &mx, &w);
strncpy(tmp.svcs[tmp.nsvc].name, nm, 15);
tmp.svcs[tmp.nsvc].mode = m;
tmp.svcs[tmp.nsvc].restart = r;
tmp.svcs[tmp.nsvc].max = mx;
tmp.svcs[tmp.nsvc].window_ms = w;
tmp.nsvc++;
}
}
}
strncpy(a->name, name, 16);
strncpy(a->exec, exec, 128);
strncpy(a->package, package, sizeof a->package - 1);
a->framework = fram ? '1' : 0;
a->immersive = immersive ? '1' : 0;
a->fullscreen = fullscreen ? '1' : 0; /* 注入 MEOW_FULLSCREEN env */
a->fonts = (char)(fonts < 0 ? 0 : (fonts > 2 ? 2 : fonts)); /* 注入 MEOW_FONT_TIER env */
strncpy(a->icon, icon, 128);
a->nroute = tmp.nroute;
if (tmp.nroute) memcpy(a->routes, tmp.routes, sizeof(a->routes[0]) * (size_t)tmp.nroute);
a->nsvc = tmp.nsvc;
if (tmp.nsvc) memcpy(a->svcs, tmp.svcs, sizeof(a->svcs[0]) * (size_t)tmp.nsvc);
}
static int scan_apps(struct app apps[MAX_APPS])
{
memset(apps, 0, sizeof(struct app) * MAX_APPS);
if (!card_present()) return -1; /* state 1 */
DIR *d = opendir(MEOWAPPS);
if (!d) return -2; /* state 2 */
int n = 0;
struct dirent *de;
while ((de = readdir(d)) && n < MAX_APPS) {
if (de->d_name[0] == '.') continue;
char jpath[192];
snprintf(jpath, sizeof jpath, "%s/%s/app.json", MEOWAPPS, de->d_name);
FILE *f = fopen(jpath, "r");
if (!f) continue;
struct app a0;
memset(&a0, 0, sizeof a0);
meow_parse_app_json(f, &a0);
fclose(f);
if (!a0.name[0] || !a0.exec[0]) continue; /* corrupt -> skip */
struct app *a = &apps[n];
memset(a, 0, sizeof *a);
memcpy(a, &a0, sizeof a0);
a->ok = (access(a->exec, X_OK) == 0);
/* 安卓化:app_id=app.json 的 package(权威),缺省回退目录名 de->d_name;
* 两者都存在且不同则 WARN(正常情况二者相等)。launch_app 注入 MEOW_LOG_APP 用 */
const char *aid = a0.package[0] ? a0.package : de->d_name;
strncpy(a->id, aid, sizeof a->id - 1);
if (a0.package[0] && strcmp(a0.package, de->d_name) != 0)
logmsg("WARN: app '%s' package '%s' != dir '%s'", a0.name, a0.package, de->d_name);
/* M1 路由注册表同步(app_id = a->id,即 package 优先) */
meow_acct_sync(a->id, a);
n++;
}
closedir(d);
return n; /* 0 -> state 2 */
}
/* ================================================================== */
/* hiby_player management */
/* ================================================================== */
static int find_hiby_pid(void)
{
/* scan /proc/N/exe for /usr/bin/hiby_player */
DIR *d = opendir("/proc");
if (!d) return 0;
struct dirent *de;
int pid = 0;
while ((de = readdir(d))) {
if (de->d_name[0] < '0' || de->d_name[0] > '9') continue;
char path[64], exe[256];
snprintf(path, sizeof path, "/proc/%s/exe", de->d_name);
ssize_t n = readlink(path, exe, sizeof exe - 1);
if (n <= 0) continue;
exe[n] = 0;
if (!strcmp(exe, "/usr/bin/hiby_player")) { pid = atoi(de->d_name); break; }
}
closedir(d);
return pid;
}
/* ---- MeowOS 常驻主界面判定(M2.2 修复批次)----
* Launcher 被 MeowOS 拉起(bootmenu MeowOS 分支 exec,或热键切 MeowOS 后 exec)时,
* 播放器已死(find_hiby_pid()==0)。此时 launcher == MeowOS 主界面常驻:必须自绘
* fb0(画 buffer0 + FBIOPAN yoffset=0),绝不走旧「备份播放器帧/冻结/SIGCONT」路径,
* 绝不遗留/恢复播放器死帧。仅当存在存活播放器(会话覆盖场景)才保留旧菜单覆盖语义。 */
static int meow_mode = 0;
static int is_meow_mode(void)
{
return find_hiby_pid() <= 0;
}
static int player_stopped = 0;
static int player_pid = 0;
static int proc_is_stopped(int pid)
{
char path[64], line[512];
snprintf(path, sizeof path, "/proc/%d/stat", pid);
int fd = open(path, O_RDONLY);
if (fd < 0) return 0;
ssize_t n = read(fd, line, sizeof line - 1);
close(fd);
if (n <= 0) return 0;
line[n] = 0;
/* comm may contain spaces; find last ')' */
char *p = strrchr(line, ')');
if (!p || p + 2 >= line + n) return 0;
return p[2] == 'T';
}
static int stop_player(void)
{
player_pid = find_hiby_pid();
if (!player_pid) { logmsg("hiby_player not found, menu without stop"); return 0; }
if (kill(player_pid, SIGSTOP) < 0) {
logmsg("SIGSTOP %d failed: %s", player_pid, strerror(errno));
return -1;
}
/* wait for state T */
for (int i = 0; i < 100; i++) {
if (proc_is_stopped(player_pid)) break;
usleep(10000);
}
player_stopped = proc_is_stopped(player_pid);
logmsg("hiby_player pid=%d stopped=%d", player_pid, player_stopped);
return player_stopped ? 0 : -1;
}
static void cont_player(void)
{
if (player_stopped && player_pid > 0) {
kill(player_pid, SIGCONT);
logmsg("SIGCONT %d", player_pid);
}
player_stopped = 0;
}
/* ================================================================== */
/* menu */
/* ================================================================== */
#define ROW_H 52
#define ROW_TOP 96
#define EXIT_ROW 640
#define TOAST_Y 590
/* brightness save/restore (defined below, used by run_menu) */
static int saved_brightness = -1;
/* set by run_menu: the player's panel was blanked (screen timeout) when the
* menu opened. fb_open() unblanks it so the menu is visible; on session end
* restore_backlight() re-blanks (brightness 0 + FBIOBLANK(4)) to put the
* physical panel back into the state the player's display state machine
* assumes — otherwise the player resumes believing the panel is blanked
* while the panel is actually lit (frozen frame). Blanking is idempotent
* and matches what the player itself did, so it cannot wedge the player. */
static int menu_opened_blank = 0;
/* ---- config (MeowAPPs/meow.conf): pure black background by default ---- */
static uint32_t menu_bg = 0x000000;
/* Hotkey scheme (v6.5):
* hotkey=power_voldown (default) — HOLD power, then a single VOL_DOWN tap
* 60-2500ms after the power-down edge opens the menu. The player sees
* the power press (its screen-off flow starts; accepted side effect)
* and the VOL_DOWN tap (volume -1; accepted). Menu open runs the
* verified unblank + bring-backlight-only-if-dark logic (§18.15).
* hotkey=seq (legacy) — same key quick-tapped N times
* (default VOL_UP x3 within seq_gap_ms each). The PREVIOUS key combo
* was retired (its GPIO never fires on this unit, §18.12) — a multi-tap
* of ONE key is ADC-friendly and physical-safe.
* meow.conf keys:
* hotkey=power_voldown | seq
* hotkey_seq=volup,volup,volup (same key name repeated = N taps, seq mode)
* hotkey_seq_gap=600 (ms between taps, total window ~2x gap)
*/
#define HOTKEY_MODE_PWRVOL 0
#define HOTKEY_MODE_SEQ 1
#define HK_PWRWIN_MIN 60 /* min power-hold time before VOL_DOWN counts */
/* (2026-08-28: 300->60; 仅留防抖/去抖动, */
/* natural ~220ms 电源+音量减 实测被 300 拦下) */
#define HK_PWRWIN_MAX 2500 /* max power-hold window; beyond this no trig */
static int hotkey_mode = HOTKEY_MODE_PWRVOL;
#define MAX_SEQ_LEN 6
static int seq_codes[MAX_SEQ_LEN];
static int seq_len = 0;
static int seq_gap_ms = 600;
static int key_name_code(const char *n)
{
if (!strcmp(n, "volup")) return KEY_VOLUMEUP;
if (!strcmp(n, "voldown")) return KEY_VOLUMEDOWN;
if (!strcmp(n, "play")) return KEY_PLAYPAUSE;
if (!strcmp(n, "next")) return KEY_NEXT;
if (!strcmp(n, "previous")) return KEY_PREVIOUSSONG;
if (!strcmp(n, "power")) return KEY_POWER;
return -1;
}
static void load_config(void)
{
menu_bg = 0x000000;
hotkey_mode = HOTKEY_MODE_PWRVOL; /* default: hold power + VOL_DOWN tap */
seq_len = 0;
seq_gap_ms = 600;
char path[256];
snprintf(path, sizeof path, "%s/meow.conf", MEOWAPPS);
FILE *f = fopen(path, "r");
if (f) {
char buf[128];
while (fgets(buf, sizeof buf, f)) {
if (!strncmp(buf, "bg_color=", 9)) {
unsigned int v;
if (sscanf(buf + 9, "%x", &v) == 1) menu_bg = v & 0xFFFFFF;
} else if (!strncmp(buf, "hotkey=", 7)) {
char *p = buf + 7;
while (*p == ' ' || *p == '\t') p++;
if (!strncmp(p, "seq", 3)) hotkey_mode = HOTKEY_MODE_SEQ;
else hotkey_mode = HOTKEY_MODE_PWRVOL;
} else if (!strncmp(buf, "hotkey_seq=", 11)) {
char *p = buf + 11;
seq_len = 0;
/* BUGFIX: the outer loop must consume the EOL — with
* *p staying at '\n' the empty-name branch never advanced p
* and the daemon spun forever (found on-device: a hotkey_seq
* line wedged load_config in a busy loop). */
while (*p && *p != '\n' && *p != '\r' && seq_len < MAX_SEQ_LEN) {
char name[16];
int k = 0;
while (*p && *p != ',' && *p != '\n' && *p != '\r' && k < 15)
name[k++] = *p++;
name[k] = 0;
if (k >= 15) /* malformed long name: skip to separator/EOL */
while (*p && *p != ',' && *p != '\n' && *p != '\r') p++;
if (*p == ',') p++;
int c = key_name_code(name);
if (c > 0) seq_codes[seq_len++] = c;
}
} else if (!strncmp(buf, "hotkey_seq_gap=", 15)) {
int g = atoi(buf + 15);
if (g >= 200 && g <= 2000) seq_gap_ms = g;
}
}
fclose(f);
}
if (seq_len == 0) { seq_codes[0] = KEY_VOLUMEUP; seq_len = 3; } /* seq fallback volup x3 */
if (hotkey_mode == HOTKEY_MODE_PWRVOL)
logmsg("config: bg_color=%06x hotkey=power_voldown (window %d-%dms)",
menu_bg, HK_PWRWIN_MIN, HK_PWRWIN_MAX);
else
logmsg("config: bg_color=%06x hotkey=seq (%d taps of code %d gap=%dms)",
menu_bg, seq_len, seq_codes[0], seq_gap_ms);
}
/* ---- volume save/restore ---- */
/* The player sets volume via ioctl on /dev/snd/controlC0 writing the
* CS43198 "Left/Right Playback Volume" controls (§11.4). We snapshot the
* same controls through amixer so a hotkey sequence's volume side effects
* can be restored silently (the player itself never reads these; next key
* press it writes its own value again — acceptable). */
struct vol_snap { int l, r; };
static int amixer_vol(int numid)
{
char cmd[96];
snprintf(cmd, sizeof cmd, "amixer -c 0 cget numid=%d 2>/dev/null", numid);
FILE *p = popen(cmd, "r");
if (!p) return -1;
char buf[128];
int v = -1;
while (fgets(buf, sizeof buf, p)) {
/* ": values=N" line — the other "values=1" on the type line is the
* element's value COUNT, not the actual volume */
char *s = strstr(buf, ": values=");
if (s) { v = atoi(s + 9); break; }
}
pclose(p);
return v;
}
static void amixer_set(int numid, int v)
{
if (v < 0) return;
char cmd[96];
snprintf(cmd, sizeof cmd, "amixer -c 0 cset numid=%d %d >/dev/null 2>&1",
numid, v);
system(cmd);
}
static struct vol_snap vol_snapshot(void)
{
struct vol_snap s = { amixer_vol(1), amixer_vol(2) };
return s;
}
static void vol_restore(struct vol_snap s)
{
logmsg("volume restore -> L=%d R=%d", s.l, s.r);
amixer_set(1, s.l);
amixer_set(2, s.r);
}
static int read_file_int(const char *path)
{
int fd = open(path, O_RDONLY);
if (fd < 0) return -1;
char buf[16];
int n = read(fd, buf, sizeof buf - 1);
close(fd);
if (n <= 0) return -1;
buf[n] = 0;
return atoi(buf);
}
/* ================================================================== */
/* 系统设置(阶段 B2a 统一后端:settings_volume 等仅作缓存) */
/* 统一后端(SYSTEM_SETTINGS.md §3/§7):扁平 key=value 存板载 */
/* /usr/data/meow/settings.conf(KB 级小文件)。单点数据源,clamp 集 */
/* 中在本模块。此处的全局变量(settings_volume/screen_off_timeout/ */
/* refresh_rate_cfg 等)仅作缓存,由 meow_settings 的 set 回调/加载 */
/* 后同步,供既有大量引用这些变量的代码(DAC/状态栏/息屏/刷新率调度)*/
/* 使用,零侵入、不破坏编译与功能。 */
/* ================================================================== */
#define MEOW_VOL_MAX 100 /* 硬上限 0-100 */
#define MEOW_VOL_DEFAULT 60 /* 默认音量 */
#define MEOW_VOL_STEP_DEFAULT 1 /* 默认跳步 1%(线性渐变;可配 audio.volume_step 1-5) */
#define MEOW_VOL_STEP_MAX 5 /* 跳步上限(SYSTEM_SETTINGS.md:最大 5) */
#define MEOW_VOL_MAX_DEFAULT 100 /* 默认最大音量上限(尊重上限;可配 audio.volume_max) */
static int settings_volume = MEOW_VOL_DEFAULT; /* 当前音量 0-100(= 写入 DAC 的映射值) */
static int settings_volume_max = MEOW_VOL_MAX_DEFAULT; /* 最大音量上限 clamp */
static int settings_volume_step = MEOW_VOL_STEP_DEFAULT; /* 跳步 */
/* M2.17/M2.16:把 meow_settings 当前值同步到本模块的缓存全局。读取方
* (DAC/状态栏/息屏/刷新率调度)统一从缓存取,避免散读 backend。 */
static void settings_sync_cache_from_backend(void)
{
int v;
if (meow_settings_get_int("audio.volume", &v) == 0) settings_volume = v;
if (meow_settings_get_int("audio.volume_max", &v) == 0) settings_volume_max = v;
if (meow_settings_get_int("audio.volume_step", &v) == 0) settings_volume_step = v;
if (meow_settings_get_enum("display.refresh_rate", &v) == 0) /* names "60,30,15" */
refresh_rate_cfg = (v == 1) ? 30 : (v == 2) ? 15 : 60;
if (meow_settings_get_bool("display.refresh_always_max", &v) == 0) refresh_always_max = v;
if (meow_settings_get_int("display.screen_off_timeout", &v) == 0) screen_off_timeout = v;
if (meow_settings_get_enum("display.screen_off_mode", &v) == 0) screen_off_mode = v;
if (meow_settings_get_int("power.longpress_menu_ms", &v) == 0) power_longpress_menu_ms = v;
if (meow_settings_get_int("power.forceoff_ms", &v) == 0) power_forceoff_ms = v;
}
/* meow_settings 变更回调:set_*(如音量键)后同步缓存全局,保证既有引用
* settings_volume 等变量的代码与 backend 单点一致(SYSTEM_SETTINGS §3)。 */
static void settings_sync_cache_listener(const char *key)
{
if (!strcmp(key, "audio.volume")) { int v; if (meow_settings_get_int("audio.volume", &v) == 0) settings_volume = v; }
else if (!strcmp(key, "audio.volume_max")) { int v; if (meow_settings_get_int("audio.volume_max", &v) == 0) settings_volume_max = v; }
else if (!strcmp(key, "audio.volume_step")) { int v; if (meow_settings_get_int("audio.volume_step", &v) == 0) settings_volume_step = v; }
else if (!strcmp(key, "display.refresh_rate")) { int v; if (meow_settings_get_enum("display.refresh_rate", &v) == 0) refresh_rate_cfg = (v == 1) ? 30 : (v == 2) ? 15 : 60; }
else if (!strcmp(key, "display.refresh_always_max")) { int v; if (meow_settings_get_bool("display.refresh_always_max", &v) == 0) refresh_always_max = v; }
else if (!strcmp(key, "display.screen_off_timeout")) { int v; if (meow_settings_get_int("display.screen_off_timeout", &v) == 0) screen_off_timeout = v; }
else if (!strcmp(key, "display.screen_off_mode")) { int v; if (meow_settings_get_enum("display.screen_off_mode", &v) == 0) screen_off_mode = v; }
else if (!strcmp(key, "power.longpress_menu_ms")) { int v; if (meow_settings_get_int("power.longpress_menu_ms", &v) == 0) power_longpress_menu_ms = v; }
else if (!strcmp(key, "power.forceoff_ms")) { int v; if (meow_settings_get_int("power.forceoff_ms", &v) == 0) power_forceoff_ms = v; }
}
/* ---- 通知 dump 回调(通知#6 转储 TF 卡;做成真)----
* meow_notif 在 TTL 到点(meow_notif_tick,§8 10s)调用此回调归档一条通知,随后释放槽。
* 本回调把 meow_notif 结构体转成一行 CSV 追加写入 <卡>/MeowOS/notifications/notifications.log
* (含 mkdir -p 与按总大小滚动截断、保留最新 10MB),落盘实现在 meow_notif.c 的
* meow_notif_dump_tf()。无卡/不可写仅打点降级(NOTIFICATION_SYSTEM.md §5/§8 + MEWSPEC §5.3)。 */
static void notif_dump_tf_placeholder(const meow_notif_t *n)
{
meow_notif_dump_tf(n);
}
/* ---- 通知图标回退回调(用户定案:icon 为空 → 用该 app 默认图标)----
* post 的 app_icon 为空时调用(meow_notif.c 的 notif_resolve_icon),按 app_id 在 app
* 注册表(scan_apps 静态 app.json 的 icon)里找该 app 的图标(name/path 或空)。 */
static int notif_app_icon_fallback(const char *app_id, char *out, size_t outsz)
{
if (!app_id || !app_id[0] || !out || outsz == 0) return 0;
struct meow_acct *a = meow_acct_find(app_id);
if (a && a->icon[0]) { snprintf(out, outsz, "%s", a->icon); return 1; }
return 0; /* 无注册/无图标 → 保持空(通知中心 UI 再回退) */
}
/* 启动加载(M2.11/M2.24):统一由 meow_settings 后端读 settings.conf;
* 此处仅注册监听(一次性)+ 把 backend 当前值同步到缓存全局。重载(应用期
* watchdog 可能改音量)等价于 meow_settings_init() + 手动同步。 */
static int settings_cache_registered = 0; /* M2.24:监听只注册一次 */
static void settings_load(void)
{
meow_settings_init(); /* 幂等:注册默认 + 读 settings.conf */
meow_notif_init(); /* M2a:通知存储初始化(幂等,读 notify.* 配额) */
meow_notif_set_dump_cb(notif_dump_tf_placeholder); /* 通知#6:挂 dump 回调 = TF 转储(meow_notif_dump_tf:一行 CSV 追加 + 10MB 滚动截断) */
meow_notif_set_icon_cb(notif_app_icon_fallback); /* icon 空 → 按 app_id 回退该 app 默认图标(app.json icon) */
if (!settings_cache_registered) {
meow_settings_add_listener(settings_sync_cache_listener);
settings_cache_registered = 1;
}
settings_sync_cache_from_backend();
logmsg("settings: vol=%d(max=%d,step=%d) refresh=%d(always_max=%d) off=%ds mode=%s",
settings_volume, settings_volume_max, settings_volume_step,
refresh_rate_cfg, refresh_always_max, screen_off_timeout,
screen_off_mode ? "black" : "panel");
}
/* ---- MeowOS 真实 DAC 音量(CS43198,ANALYSIS §18.39)----
* HibyOS 播放器经 ioctl 直写 /dev/snd/controlC0 的 "Left/Right Playback
* Volume"(numid 1/2,0-255)→ codec 驱动 cs43198_vol_*_put → I2C 直写 CS43198
* 寄存器(0x9000-0x9002/0x7000-0x7001,双片 L/R)。我们复刻同路径(amixer
* cset = SNDRV_CTL_IOCTL_ELEM_WRITE),0-100 → 0-255 映射直写两声道。
* 读回不可用(驱动 get 为 dummy 恒 0;/dev/i2c-3 被驱动占用),故 meow_vol_get()
* 返回我们写入的跟踪值(= 真实 DAC 映射值)。 */
static void meow_dac_vol_write(int pct)
{
int dac = pct * 255 / 100;
if (dac < 0) dac = 0;
if (dac > 255) dac = 255;
char cmd[160];
snprintf(cmd, sizeof cmd,
"amixer -c 0 cset numid=1 %d >/dev/null 2>&1; amixer -c 0 cset numid=2 %d >/dev/null 2>&1",
dac, dac);
system(cmd);
logmsg("DAC vol -> L/R=%d (pct %d)", dac, pct);
}
/* ---- M2.38 音量滑条拖动节流:meow_dac_vol_write 每次 system() fork+exec /bin/sh + 2 个
* amixer,嵌入式 MIPS 上极慢;滑条拖动逐帧 set → s_commit → system() 导致卡死十几秒。
* 修法:墙钟节流(两次真实 DAC 写入 ≥ VOL_DRAG_DAC_MS),拖动中只更新缓存 + 刷状态栏,
* 抬手/结束时经 meow_dac_vol_flush() 无条件直写一次,保证最终音量必然落 DAC。 ---- */
#define VOL_DRAG_DAC_MS 60 /* 滑条拖动期间两次真实 DAC 写入的最短墙钟间隔 */
static unsigned long long vol_dac_last_ms = 0;
static void meow_dac_vol_write_throttled(void)
{
unsigned long long now = now_ms();
if (now - vol_dac_last_ms < VOL_DRAG_DAC_MS) return; /* 节流窗口内跳过,等抬手统一写 */
vol_dac_last_ms = now;
meow_dac_vol_write(settings_volume);
}
/* 抬手/点击提交连 save 时调用:无条件直写一次真实 DAC(绕过节流,保证最终音量落 DAC/落盘)。 */
static void meow_dac_vol_flush(void)
{
vol_dac_last_ms = now_ms();
meow_dac_vol_write(settings_volume);
}
/* clamp 集中:0..MEOW_VOL_MAX,再 clamp 到上限 settings_volume_max。 */
static int vol_clamp(int v)
{
if (v < 0) v = 0;
if (v > MEOW_VOL_MAX) v = MEOW_VOL_MAX;
if (v > settings_volume_max) v = settings_volume_max;
return v;
}
static void settings_volume_set(int v)
{
v = vol_clamp(v); /* 0..MEOW_VOL_MAX,再 clamp 到上限 */
meow_settings_set_int("audio.volume", v); /* clamp 集中在 backend;listener 同步缓存 */
meow_dac_vol_write(settings_volume); /* 每次变更即直写真实 DAC(CS43198) */
meow_settings_save(); /* 单点变更即落盘 */
}
static int settings_volume_get(void) { return settings_volume; }
/* ---- M2.14 线性渐变:step>1 时按 1% 递增爬到目标,避免一次跳 N% 突兀 ----
* 每主循环 tick 最多迈 1%(VOL_RAMP_MS 墙钟节流,不阻塞主循环);到目标若
* 键仍按住则继续延段(长按=连续渐变到松开/到上限);step=1 时一步即达(天然平滑)。
* 落盘只在渐变结束时一次(meow_settings_save),不逐 1% 写盘。 */
#define VOL_RAMP_MS 15 /* 每 1% 步进的最短墙钟间隔 */
static int vol_ramp_dir = 0; /* +1 上 / -1 下 / 0 停 */
static int vol_ramp_target = -1; /* 目标值;-1=无 */
static unsigned long long vol_ramp_next_at = 0;
static unsigned long long vol_ramp_press_at = 0; /* M2.18 Bug 4:本方向键按下时刻(长按判据) */
static void vol_ramp_begin(int delta)
{
int target = vol_clamp(settings_volume + delta);
if (target == settings_volume) return;
vol_ramp_dir = (target > settings_volume) ? 1 : -1;
vol_ramp_target = target;
vol_ramp_press_at = now_ms(); /* M2.18 Bug 4:记录该方向键按下时刻,供 CLICK_MS 长按判定 */
vol_ramp_next_at = 0; /* 立即迈第一步,给即时反馈 */
}
/* 每主循环 tick 调;key_held=该方向按键是否仍按住(长按续爬)。返回 1=仍在渐变。
* M2.18 Bug 4 语义:单击(<CLICK_MS=400ms) 到 target 即停(只跳 step 格);长按(≥400ms) 才
* 续段连续爬。普通按压力 50-150ms 远大于 VOL_RAMP_MS(15ms),旧逻辑在到 target 后只要
* key_held=1 就续段 → 单击被当成长按,连爬 6~7 格(「点一下跳好几格」)。CLICK_MS 此前只
* 被 kd_tick 状态机使用,vol_ramp 路径从未接上。 */
static int vol_ramp_tick(int key_held)
{
if (vol_ramp_dir == 0) return 0;
unsigned long long now = now_ms();
if (now < vol_ramp_next_at) return 1;
vol_ramp_next_at = now + VOL_RAMP_MS;
/* 先看是否已到「当前目标值」(settings_volume 等于 target,而非候选 v)——
* 因为一旦在 target 再取 v=settings_volume+dir 便越过 target,导致 hold_wait 失效继续爬。 */
if (settings_volume == vol_ramp_target) {
if (!key_held) { /* 松开 → 一段完成,结束(单击到 target 即停,只跳 step 格) */
vol_ramp_dir = 0; vol_ramp_target = -1;
meow_settings_save(); return 0;
}
if (now - vol_ramp_press_at < CLICK_MS) {
/* hold_wait:已到 target 但未到长按阈值 → 停在 target 不爬(保持待命不清 dir,
* 等长按激活或松开);若直接续段,长按第一次到 target 就断了。 */
return 1;
}
/* 长按激活:续段(在 target 基础上再 +step) */
vol_ramp_target = vol_clamp(vol_ramp_target + vol_ramp_dir * settings_volume_step);
if (vol_ramp_target == settings_volume) { /* 续段到上限无进展 → 结束 */
vol_ramp_dir = 0; vol_ramp_target = -1;
meow_settings_save(); return 0;
}
}
int v = vol_clamp(settings_volume + vol_ramp_dir);
if (v == settings_volume) { /* 已达上下限 */
vol_ramp_dir = 0; vol_ramp_target = -1;
meow_settings_save(); return 0;
}
meow_settings_set_int("audio.volume", v); /* 更新 backend 内存 + 同步缓存(不逐 1% 落盘) */
meow_dac_vol_write(settings_volume); /* 每 1% 写一次真实 DAC(settings_volume 已同步) */
return 1;
}
/* 步进调节(±settings_volume_step)→ 起线性渐变(1% 爬,见上)。 */
static void settings_volume_adj(int delta) { vol_ramp_begin(delta); }
/* MeowOS 音量 API:get 返回当前(= 写入 DAC 的映射值);set 写真实 DAC + 持久化。 */
static void meow_vol_set(int pct) { settings_volume_set(pct); }
static int meow_vol_get(void) { return settings_volume; }
/* ================================================================== */
/* 系统状态统一快照(A:进 MeowOS 全量重抓 + 每进必刷) */
/* 主循环 10ms 热键轮询绝不读系统态;全量 meow_sys_refresh() 仅在进 */
/* MeowOS 时调一次;1s 轻刷 meow_sys_light() 只更新时钟/电量/WiFi, */
/* 并约 5s 节流探测一次蓝牙角色(M2.48)。 */
/* 状态栏/后续设置/面板统一读此快照,单点一致,避免重复读 amixer/sysfs。*/
/* ================================================================== */
typedef struct meow_sys_state {
int batt; /* 容量 % 0-100,-1 未知 */
int charging; /* 1 充电 0 放电 */
int batt_mv; /* 电压 mV */
int wifi_on; /* M2.48:1 网卡 up(射频开)0 down(关则不显示图标) */
int wifi_connected;/* M2.48:1 已关联 AP(operstate up)0 未连(区分「网卡up未连」→WIFI_NO) */
char wifi_ssid[33];
char wifi_ip[16];
int bt_on; /* 1 蓝牙上电 0 关 */
int bt_seen; /* hci 节点存在 1/0(无模块不放图标) */
int bt_role; /* M2.48:蓝牙角色(bt_role() 判定缓存):BT_ROLE_* */
int vol; /* 系统软件音量 0-100(settings) */
int brightness; /* sysfs */
long data_free_kb; /* /data 剩余 KB */
int sd_mounted; /* TF 卡已挂载 */
long sd_free_kb; long sd_total_kb;
int mode; /* 1 meow 0 hiby(调用方) */
int napps; /* 应用数(调用方) */
char ver[16];
} meow_sys_state;
static meow_sys_state g_sys;
/* M2.48 蓝牙角色(状态栏多态 + 设置页"蓝牙模式"行共用;bt_role() 判定,缓存进 g_sys.bt_role) */
#define BT_ROLE_OFF 0 /* 关 */
#define BT_ROLE_ON_NO_CONN 1 /* 开未连接 */
#define BT_ROLE_MASTER 2 /* 主模式:连了外围(如耳机) */
#define BT_ROLE_SLAVE 3 /* 从模式:被远端接入,设备自身当耳机 */
#define BT_ROLE_UNKNOWN (-1) /* 判定失败(调用方按实际态兜底) */
static char g_bt_mode[48] = "开启未连接"; /* 设置页"蓝牙模式"行展示(bt_role_refresh 刷新) */
static int bt_role(char *desc, int len); /* 前向声明;实现在网络段(用 popen 带超时) */
static void bt_role_refresh(int force); /* 节流重探(force=1 立即;进系统/设置页/网络动作用) */
/* M2.47 版本号单一数据源:/etc/meowos_version(固件内独立文件,打包脚本写)。
* launcher 启动时读它,右下角显示/SSH 识别同一来源;不再维护 MEOWOS_VERSION 宏与文件两处。
* 读取失败/缺失时保留内置默认(最后一次编译时版本)。 */
#define MEOWOS_VERSION_FILE "/etc/meowos_version"
#define MEOWOS_VERSION_DEF MEOWOS_VERSION /* 兜底默认 = 编译期宏;运行时读 /etc/meowos_version 覆盖 */
static char g_meowos_version[16] = MEOWOS_VERSION_DEF;
static void load_meowos_version(void)
{
FILE *f = fopen(MEOWOS_VERSION_FILE, "r");
if (!f) { logmsg("meowos_version: %s missing, use default %s", MEOWOS_VERSION_FILE, g_meowos_version); return; }
char buf[32];
if (fgets(buf, sizeof buf, f)) {
char *e = buf + strlen(buf);
while (e > buf && (e[-1] == '\n' || e[-1] == '\r' || e[-1] == ' ')) e--;
*e = 0;
if (buf[0]) snprintf(g_meowos_version, sizeof g_meowos_version, "%s", buf);
}
fclose(f);
logmsg("meowos_version: read %s = '%s'", MEOWOS_VERSION_FILE, g_meowos_version);
}
/* 读一份 sysfs 字符串(剥尾随换行);失败回退 "" */
static void read_sys_str(const char *path, char *buf, size_t n)
{
buf[0] = 0;
int fd = open(path, O_RDONLY);
if (fd < 0) return;
ssize_t r = read(fd, buf, n - 1);
close(fd);
if (r <= 0) { buf[0] = 0; return; }
buf[r] = 0;
while (r > 0 && (buf[r-1] == '\n' || buf[r-1] == '\r')) buf[--r] = 0;
}
/* 蓝牙实际上电状态(A/E:不只看 hci0 存在——节点常在但控制器可能 DOWN)。
* 本设备 /sys/class/bluetooth/hci0/powered 不存在(旧内核/BlueZ 5.54),
* 权威来源为 hciconfig -a hci0 的 UP/DOWN 行。费用较高(spawn),故缓存,
* 由 meow_sys_refresh()(进系统一次)/meow_sys_light()(轻刷)节流更新;
* 状态栏只读 g_sys.bt_on。 */
static int hci_powered(void)
{
FILE *p = popen("hciconfig -a hci0 2>/dev/null", "r");
if (!p) return 0;
int up = 0;
char buf[256];
while (fgets(buf, sizeof buf, p)) {
/* 状态行含 "UP RUNNING" 或 "DOWN"。匹配单词 UP(避免误中 UART 等)。 */
char *s = buf, *tok;
while ((tok = strsep(&s, " \t\r\n")) != NULL) {
if (!strcmp(tok, "UP")) { up = 1; break; }
}
}
pclose(p);
return up;
}
/* 全量重抓(进 MeowOS 时调用一次;阻塞可接受)。绝不放进 10ms 主循环。 */
static void meow_sys_refresh(void)
{
load_meowos_version(); /* M2.47:读 /etc/meowos_version(单一数据源,每次刷新对齐) */
int b = read_file_int("/sys/class/power_supply/battery/capacity");
g_sys.batt = (b >= 0 && b <= 100) ? b : -1;
char st[16];
read_sys_str("/sys/class/power_supply/battery/status", st, sizeof st);
g_sys.charging = (strstr(st, "Charg") || strstr(st, "Full")) ? 1 : 0;
g_sys.batt_mv = read_file_int("/sys/class/power_supply/battery/voltage_now") / 1000;
/* WiFi:wifi_on=网卡管理态 up(射频开);wifi_connected=operstate up(已关联 AP)。
* M2.48:二者分工,状态栏才能区分「开未连接(WIFI_NO)」与「已连接(信号分档)」。 */
char op[16];
read_sys_str("/sys/class/net/wlan0/operstate", op, sizeof op);
g_sys.wifi_connected = !strcmp(op, "up") ? 1 : 0;
g_sys.wifi_on = wlan_radio_up();
g_sys.wifi_ip[0] = 0; g_sys.wifi_ssid[0] = 0;
if (g_sys.wifi_connected) {
/* 行形如 "2: wlan0 inet <device_ip>/24 brd ...",取 inet 后第一个斜杠前的 token。
* 用大缓冲读整行,再从局部 token 拷贝到 16B 小字段(避免 OOB)。 */
char big[256];
FILE *p = popen("ip -o addr show wlan0 2>/dev/null | grep 'inet '", "r");
if (p) {
if (fgets(big, sizeof big, p)) {
char *s = strstr(big, "inet ");
if (s) { s += 5; while (*s == ' ') s++; char *e = strchr(s, '/'); if (e) *e = 0;
snprintf(g_sys.wifi_ip, sizeof g_sys.wifi_ip, "%s", s); }
}
pclose(p);
}
/* SSID 尽力读(wpa_cli 若在;失败留空不外溢)。注意:wpa_cli 可能因 wpa_supplicant
* 未跑而挂起——用后台 + 短等待避免阻塞 UI;超时即放弃(SSID 仅面板用,状态栏不读)。 */
}
/* 蓝牙:hci0 存在性 + 上电状态 + M2.48 角色/连接(进系统全量重探一次) */
g_sys.bt_seen = access("/sys/class/bluetooth/hci0", F_OK) == 0;
g_sys.bt_on = g_sys.bt_seen ? hci_powered() : 0;
bt_role_refresh(1); /* 立即探测角色/连接态,供状态栏与设置页"蓝牙模式"行 */
/* 音量(settings;进 MeowOS 全量时把真实 DAC 同步到当前档)/ 亮度 */
g_sys.vol = settings_volume_get();
meow_dac_vol_write(settings_volume); /* M2.13:进 MeowOS 即把 CS43198 DAC 置为当前音量 */
int br = read_brightness();
g_sys.brightness = (br >= 0) ? br : 0;
/* 存储:/data 剩余 + TF 卡挂载容量 */
g_sys.data_free_kb = 0;
FILE *df = popen("df -k /data 2>/dev/null | awk 'NR==2{print $4}'", "r");
if (df) { char x[24]; if (fgets(x, sizeof x, df)) g_sys.data_free_kb = atol(x); pclose(df); }
g_sys.sd_mounted = sd_mounted();
g_sys.sd_free_kb = g_sys.sd_total_kb = 0;
if (g_sys.sd_mounted) {
FILE *df2 = popen("df -k /usr/data/mnt/sd_0 2>/dev/null | awk 'NR==2{print $4, $2}'", "r");
if (df2) { char x[48]; if (fgets(x, sizeof x, df2)) { sscanf(x, "%ld %ld", &g_sys.sd_free_kb, &g_sys.sd_total_kb); } pclose(df2); }
}
snprintf(g_sys.ver, sizeof g_sys.ver, "%s", g_meowos_version);
logmsg("sys snapshot: batt=%d%% chg=%d wifi=%d ssid=%s ip=%s bt=%d/%d vol=%d bri=%d "
"data_free=%ldKB sd=%d(%ld/%ldKB)",
g_sys.batt, g_sys.charging, g_sys.wifi_on, g_sys.wifi_ssid[0]?g_sys.wifi_ssid:"-",
g_sys.wifi_ip[0]?g_sys.wifi_ip:"-", g_sys.bt_on, g_sys.bt_seen,
g_sys.vol, g_sys.brightness, g_sys.data_free_kb, g_sys.sd_mounted,
g_sys.sd_free_kb, g_sys.sd_total_kb);
}
/* 轻刷(1s 定时,只更新时钟/电量/WiFi 信号 + 节流探测蓝牙角色——便宜,绝不碰负载/温度) */
static void meow_sys_light(void)
{
int b = read_file_int("/sys/class/power_supply/battery/capacity");
if (b >= 0 && b <= 100) g_sys.batt = b;
char op[16];
read_sys_str("/sys/class/net/wlan0/operstate", op, sizeof op);
g_sys.wifi_connected = !strcmp(op, "up") ? 1 : 0;
g_sys.wifi_on = wlan_radio_up();
g_sys.vol = settings_volume_get(); /* 每进 MeowOS 重抓;此处同步内存值 */
bt_role_refresh(0); /* M2.48:节流探测蓝牙角色(约 5s 一次,不阻塞主循环) */
}
/* 40px status bar (Bug 1, 2026-08-28): the old 20px bar is doubled so the
* app list can also show voltage + bluetooth alongside time / battery / wifi.
* line 1: time (left) battery% WIFI BT (right)
* line 2: VOL nn% (left)
* M2.11:值全部取自 meow_sys_state 快照(g_sys),不再每帧读 amixer/sysfs;
* 音量 = settings audio.volume(非 CS43198 死镜像);蓝牙 = 实际 powered;
* WiFi = operstate 实际 up。纯黑背景 = menu_bg;分隔线 y=28(栏高 40→28)。*/
/* ---- 状态栏固定完整格重绘(M2.17:修「整个状态栏部件脏区残留」)----
* 根因:M2.16 各部件/数字更新只把「当前内容包围盒」标为脏区并用它清底——旧内容超出新包围盒的
* 部分(时钟数字 '0' 底弧落于新 '1' 包围盒外、电量%/WiFi/BT 出现或消失、音量数字)未被重绘 →
* 残留;按音量键全屏刷新才清。修法:状态栏(0,0,480×28)用一条固定完整格:字段更新时整条清底 +
* 重绘时钟/右侧 + 整条标脏(SDK back_buf 做最终态 → meow_ui_flush 原子 blit 整条,无中间态)。
* 任何旧内容残留被整条覆盖。480×28≈27KB 极小,每秒一 blit 无碍;分隔线 y=28 单独绘制不在此列。
* (逐字段固定格在 480px 宽 + 「蓝牙」CJK 全字段同现时与时钟格重叠,故取整条退化——§18.46。) */
#define SB_BAR_Y 0
#define SB_BAR_H 28 /* rows 0..27;分隔线在 y=28 不在此(40→28,用户定) */
#define SB_FONT 20 /* 状态栏字体 28→20(28px 栏放不下 28 号;垂直居中锚点见 sb_text_y) */
static int g_sb_clock_x, g_sb_clock_y, g_sb_clock_w, g_sb_clock_h;
static int g_sb_right_x, g_sb_right_y, g_sb_right_w, g_sb_right_h;
/* ---- 主题(MD3 color roles,dark/light 两套)----
* M2.44:上移到本处(状态栏绘制之前),使状态栏/设置页共用同一套主题色板。 */
typedef struct meow_ui_theme {
uint32_t surface; /* 全局背景 */
uint32_t surface_variant; /* 卡片/列表底 */
uint32_t surface_container; /* 控件容器 */
uint32_t primary; /* 高亮/选中/accent */
uint32_t on_surface; /* 主文字 */
uint32_t on_surface_variant; /* 次文字 */
uint32_t outline; /* 描边/分隔线 */
uint32_t error; /* 错误提示 */
uint32_t surface_tint; /* 选中态叠色 */
} meow_ui_theme_t;
/* MD3 tonal palette(565 参考值见 MD3_UI_SPEC §1;此处存 24-bit RGB,#hex) */
static const meow_ui_theme_t meow_ui_theme_dark = {
0x141824, 0x1E2436, 0x263038, 0x4A6A9A, 0xFFFFFF,
0x9AA4C8, 0x2A2E38, 0xE57373, 0x4A6A9A
};
static const meow_ui_theme_t meow_ui_theme_light = {
0xE8EAF0, 0xD5D9E4, 0xCFD4E0, 0x2A5A8A, 0x1A1A2A,
0x4A4F5A, 0x9AA4C8, 0xB3261E, 0x2A5A8A
};
/* B2b 主题三方式(manual 深色 / manual 浅色 / 时段自动)。
* display.theme_mode(enum "manual,auto"):manual=读 display.theme(dark/light);
* auto=按深色时段。display.theme 保留为手动深浅的当前值(manual 下 s_ui_theme 直读它)。 */
#define THEME_MODE_MANUAL 0 /* display.theme_mode 枚举索引 0 = 手动 */
#define THEME_MODE_AUTO 1 /* display.theme_mode 枚举索引 1 = 时段自动 */
/* 获取当前本地时刻的「当天第几分钟」(tm_hour*60+tm_min);供时段判定用 */
static int s_now_min_of_day(void)
{
time_t t = time(NULL);
struct tm tm;
localtime_r(&t, &tm);
return tm.tm_hour * 60 + tm.tm_min;
}
/* 深色时段判定(纯逻辑,便于单测):now_min ∈ [start, end) → 深色。
* - start==end 视为未配置,返回 -1(由调用方决定回退策略)。
* - start<end:非跨天,如 720→1080 = 12:00→18:00,now ∈ [720,1080) 为深色。
* - start>end:跨午夜,如 1140→420 = 19:00→次日07:00,now∈[1140,1440)∪[0,420) 为深色。
* - 边界:end 半开(恰好 end 分回到浅色),start 闭(恰好 start 分进入深色)。 */
static int theme_should_dark(int now_min, int start, int end)
{
if (start == end) return -1; /* 未配置 */
if (start < end)
return (now_min >= start && now_min < end) ? 1 : 0;
return (now_min >= start || now_min < end) ? 1 : 0; /* 跨午夜 */
}
/* 当前主题(dark/light 两套;接 display.theme_mode + display.theme)。
* manual(mode==THEME_MODE_MANUAL):直读 display.theme(dark/light),保持原有行为。
* auto(mode==THEME_MODE_AUTO):读深色时段两键 + 当前本地分钟;start==end 未配置 →
* 回退 manual(读 display.theme,避免误判全天深色)。
* 读取失败一律回退 dark。 */
static const meow_ui_theme_t *s_ui_theme(void)
{
int mode = THEME_MODE_MANUAL;
if (meow_settings_get_enum("display.theme_mode", &mode) != 0) mode = THEME_MODE_MANUAL;
int v = 0; /* display.theme(manual)当前索引 */
int have_manual = (meow_settings_get_enum("display.theme", &v) == 0);
if (mode == THEME_MODE_AUTO) {
int start = 0, end = 0;
meow_settings_get_int("display.theme_dark_start", &start);
meow_settings_get_int("display.theme_dark_end", &end);
int d = theme_should_dark(s_now_min_of_day(), start, end);
if (d < 0) { /* 未配置:回退 manual(display.theme) */
if (have_manual && v == 1) return &meow_ui_theme_light;
return &meow_ui_theme_dark;
}
return d ? &meow_ui_theme_dark : &meow_ui_theme_light;
}
/* manual:照旧读 display.theme */
if (have_manual && v == 1) return &meow_ui_theme_light;
return &meow_ui_theme_dark;
}
/* ---- 状态栏右簇图标化(MD3)---- 布局常量 */
#define SB_TGAP 6 /* 图标右缘 ↔ 文字间隙 */
#define SB_XGAP 10 /* 相邻两项(右→左)间隙 */
#define SB_ICON_INK 16 /* 主图标目标 ink 高(px):24→16(28px 栏等比缩放,留上下边距) */
#define SB_CLK_MARGIN 14 /* 右簇左缘相对时钟右缘须留出的安全间距 */
/* 图标 ink 垂直中心线 = 状态栏文字垂直中心(28px 栏文字按 (28-行高)/2 锚定后中心≈14,见 sb_text_y)。
* 图标 ink 对齐此线 → 与数字/文字同中线(修「图标比文字低」);如需微调只改这一个常量。 */
#define SB_ICON_CY 14
static int g_sb_icon_cy = SB_ICON_CY;
/* 状态栏文字垂直锚 y(行首升线顶):(栏高-行高)/2 把文字垂直居中于 28px 栏;
* 下限 1 防极端行高(>栏高)时算出负 y 溢出顶端(不越界优先)。 */
static int sb_text_y(void)
{
int y = (SB_BAR_H - ui_text_height(SB_FONT)) / 2;
return (y < 1) ? 1 : y;
}
/* 状态栏 MD3 图标 blit:把 sb_icons.h 的 32x32 单通道 alpha 位图画进状态栏(fb = SDK back_buf)。
* x = 图标槽左缘(槽宽 SB_ICON_SIZE);fg = 24-bit RGB 前景色(主题 on_surface / on_surface_variant)。
* M2.46-2 视觉对齐(音量图标小一圈/下边距、图标与文字垂直中心):
* - 先求该位图 ink 边界(阈值 32,忽略近透明抗锯齿);按 ink 边界在槽内水平居中,并把 ink 垂直
* 中心对齐到全局 g_sb_icon_cy(==状态栏文字中心),→ 所有图标共用同一光学中线,不再「偏下」。
* - ink_h>0 时对 ink 做等比缩放(绘制时最近邻采样,源位图不变)到该 ink 高:主图标统一用
* SB_ICON_INK,音量(volume_up ink 较矮)随之放大到与电量/亮度一致 → 不显「小一圈」。
* - ink_h==0 保持位图自然 ink 尺寸(仅居中):用于 WiFi 强弱档——较短 ink(少一格)本身即
* 「信号弱」语义,不可被等比拉大。
* 像素 = 前景与背景按 alpha 软边混合(a<8 跳过透明;a>=240 直写前景;其余按 a>>4 在 565 空间
* 插值),与 blit_pm_icon 同构——暗/亮主题下图标随状态栏底色一起换色。 */
static void sb_blit_icon(int icon_idx, int x, uint32_t fg, int ink_h)
{
const unsigned char *src = sb_icons[icon_idx].data;
uint16_t col = rgb565(fg);
/* 1) ink 边界(含空图兜底) */
int minx = SB_ICON_SIZE, miny = SB_ICON_SIZE, maxx = -1, maxy = -1;
for (int i = 0; i < SB_ICON_SIZE * SB_ICON_SIZE; i++) {
if (src[i] >= 32) {
int xx = i % SB_ICON_SIZE, yy = i / SB_ICON_SIZE;
if (xx < minx) minx = xx; if (xx > maxx) maxx = xx;
if (yy < miny) miny = yy; if (yy > maxy) maxy = yy;
}
}
if (maxx < minx || maxy < miny) { minx = miny = 0; maxx = maxy = SB_ICON_SIZE - 1; }
int ink_w = maxx - minx + 1, ink_h_n = maxy - miny + 1;
int t_h = (ink_h > 0) ? ink_h : ink_h_n; /* 目标 ink 高 */
int t_w = ink_w * t_h / ink_h_n; if (t_w < 1) t_w = 1;
if (t_w > SB_ICON_SIZE * 2) t_w = SB_ICON_SIZE * 2; /* 防御:等比极端上限 */
/* 2) 目标框左上:水平居中于 32px 槽;垂直中心 = g_sb_icon_cy */
int x0 = x + (SB_ICON_SIZE - t_w) / 2;
int y0 = g_sb_icon_cy - t_h / 2;
/* 3) 最近邻采样映射:目标 (dx,dy) ← 源 ink (minx+dx*ink_w/t_w, miny+dy*ink_h_n/t_h) */
for (int dy = 0; dy < t_h; dy++) {
int ty = y0 + dy;
if (ty < 0 || ty >= fb_yres) continue;
int sy = miny + (dy * ink_h_n) / t_h;
uint16_t *row = (uint16_t *)fb + ty * (fb_line / 2);
for (int dx = 0; dx < t_w; dx++) {
int tx = x0 + dx;
if (tx < 0 || tx >= fb_xres) continue;
int sx = minx + (dx * ink_w) / t_w;
int a = src[sy * SB_ICON_SIZE + sx];
if (a < 8) continue; /* 透明像素跳过 */
uint16_t bg = row[tx];
if (a >= 240) { row[tx] = col; continue; }
int a4 = a >> 4; /* 0..15 */
int r = ((col >> 11) * a4 + ((bg >> 11) & 0x1F) * (16 - a4)) >> 4;
int g = (((col >> 5) & 0x3F) * a4 + ((bg >> 5) & 0x3F) * (16 - a4)) >> 4;
int b = ((col & 0x1F) * a4 + (bg & 0x1F) * (16 - a4)) >> 4;
row[tx] = (uint16_t)((r << 11) | (g << 5) | b);
}
}
}
/* 画一个右对齐状态栏项:右边界 = *xr,画完向左推进 *xr(含 SB_XGAP 项间隙)。
* icon>=0 画图标(可配右侧文字 txt;txt==NULL 则仅图标)。文字/图标同用主题色 col。
* ink_h 传给 sb_blit_icon(主图标用 SB_ICON_INK 等大;WiFi 强弱档传 0 保持自然 ink)。
* 返回该项左边缘(供 g_sb_right_x 包围盒)。 */
static int sb_r_place(int *xr, int icon, const char *txt, uint32_t col, int ink_h)
{
int iconw = (icon >= 0) ? SB_ICON_SIZE : 0;
int gap = (icon >= 0 && txt) ? SB_TGAP : 0;
int tw = txt ? ui_width(txt, SB_FONT) : 0;
int x = *xr - (iconw + gap + tw);
if (icon >= 0) { sb_blit_icon(icon, x, col, ink_h); x += iconw + gap; }
if (txt) ui_draw(x, sb_text_y(), txt, col, SB_FONT);
*xr -= (iconw + gap + tw) + SB_XGAP;
return *xr + SB_XGAP;
}
#define WIFI_LVL_UNKNOWN (-1000) /* wifi_signal_level 读取失败哨兵(低于任何真实 dBm) */
/* 读 /proc/net/wireless 的 wlan0 level(dBm,如 -46);失败(文件不存在/无 wlan0 行/解析失败)返回
* WIFI_LVL_UNKNOWN。行格式(Linux wext)形如 " wlan0: 0000 64. -46. -256 0 ...";
* 接口名后第 3 个空白分隔数值 = level(注意末尾带 '.',需剥离)。 */
static int wifi_signal_level(void)
{
FILE *f = fopen("/proc/net/wireless", "r");
if (!f) return WIFI_LVL_UNKNOWN;
char line[256];
int lvl = WIFI_LVL_UNKNOWN;
while (fgets(line, sizeof line, f)) {
char *p = strstr(line, "wlan0:");
if (!p) continue;
p += 6; /* 跳过 "wlan0:" */
int tok = 0;
while (*p) {
while (*p == ' ' || *p == '\t') p++;
if (!*p) break;
char *t = p;
while (*p && *p != ' ' && *p != '\t' && *p != '\n') p++;
char save = *p; *p = 0; /* 临时截断 token */
if (tok == 2) { /* 第 3 个数值(0 起) = level */
size_t n = strlen(t);
if (n && t[n - 1] == '.') t[n - 1] = 0; /* 去尾部 '.'(-46. → -46) */
lvl = atoi(t);
*p = save;
break;
}
*p = save;
tok++;
}
break; /* 只需 wlan0 行 */
}
fclose(f);
return lvl;
}
/* 整条状态栏:清底→重绘时钟+右侧信息→整条标脏。调用点:时钟每秒 / 电量·WiFi·音量事件。 */
static void sb_redraw(void)
{
const meow_ui_theme_t *th = s_ui_theme(); /* M2.44 状态栏接主题(浅色主题浅底深字,不再固定深色) */
uint32_t sb_bg = th->surface, sb_fg = th->on_surface;
fill_rect(0, SB_BAR_Y, fb_xres, SB_BAR_H, sb_bg); /* 整条清底→无旧内容残留 */
char timebuf[16];
time_t now = time(NULL);
struct tm tm;
localtime_r(&now, &tm);
snprintf(timebuf, sizeof timebuf, "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec);
ui_draw(8, sb_text_y(), timebuf, sb_fg, SB_FONT);
/* 图标 ink 垂直中心 = 状态栏文字中心(SB_ICON_CY=14,28px 栏文字中心):同中线。 */
g_sb_icon_cy = SB_ICON_CY;
/* 右簇 MD3 图标化(从右端 fb_xres-8 往左):[电量图标+%] [音量图标+%] [蓝牙图标] [WiFi图标] [亮度图标+%]。
* M2.46-2:音量/电量顺序互换(最右由音量改为电量);亮度由 on_surface_variant 改为主色 on_surface;
* WiFi 按信号强度分档(/proc/net/wireless level:≥-60 强 / -60..-75 中 / <-75 弱)。
* 主项色 on_surface;次项(蓝牙)用 on_surface_variant 稍淡。亮度「可选」:仅在右簇左缘
* 与时钟右缘相距足够(SB_CLK_MARGIN)时才画,否则整体让位,避免与时钟叠字。 */
uint32_t sb_fg2 = th->on_surface_variant;
char vol[16], batt[16], bright[16];
int xr = fb_xres - 8;
if (g_sys.batt >= 0) { /* 电量:图标+%(未知时不画)——最右项 */
snprintf(batt, sizeof batt, "%d%%", g_sys.batt);
sb_r_place(&xr, SB_I_BATT, batt, sb_fg, SB_ICON_INK);
}
snprintf(vol, sizeof vol, "%d%%", g_sys.vol);
sb_r_place(&xr, SB_I_VOL, vol, sb_fg, SB_ICON_INK); /* 音量:图标+% */
/* M2.48 蓝牙多态:关(bt_on=0 不画,见上行门);开未连接→SB_I_BT_NO;主模式/从模式→SB_I_BT_CONN。
* 主/从区分用颜色:主模式走次色 sb_fg2(与外设相连),从模式(被连当耳机)走主色 sb_fg 稍醒目。 */
if (g_sys.bt_seen && g_sys.bt_on) {
int bco = SB_I_BT_NO; /* 默认:开未连接 */
uint32_t bcol = sb_fg2;
if (g_sys.bt_role == BT_ROLE_MASTER) { bco = SB_I_BT_CONN; bcol = sb_fg2; }
else if (g_sys.bt_role == BT_ROLE_SLAVE) { bco = SB_I_BT_CONN; bcol = sb_fg; }
sb_r_place(&xr, bco, NULL, bcol, SB_ICON_INK);
}
/* M2.48 WiFi 多态:网卡关(wifi_on=0 不画);开未连接→SB_I_WIFI_NO;已连接→按信号分档。 */
if (g_sys.wifi_on) { /* WiFi:仅图标(按连接态+信号分档) */
int wco = SB_I_WIFI_NO; /* 默认:网卡开但未连 */
if (g_sys.wifi_connected) {
wco = SB_I_WIFI_STRONG; /* 已连接;读取失败/未知 → 满格 */
int wl = wifi_signal_level();
if (wl != WIFI_LVL_UNKNOWN) {
if (wl >= -60) wco = SB_I_WIFI_STRONG;
else if (wl >= -75) wco = SB_I_WIFI_MEDIUM;
else wco = SB_I_WIFI_WEAK;
}
}
/* 强弱档(MEDIUM/WEAK)保留自然 ink(少一格本身即语义);SRONG/满格/开未连接走等大。 */
sb_r_place(&xr, wco, NULL, sb_fg,
(wco == SB_I_WIFI_MEDIUM || wco == SB_I_WIFI_WEAK) ? 0 : SB_ICON_INK);
}
if (g_sys.brightness > 0) {
int bl = (g_sys.brightness > 100) ? 100 : g_sys.brightness;
snprintf(bright, sizeof bright, "%d%%", bl);
int bw = SB_ICON_SIZE + SB_TGAP + ui_width(bright, SB_FONT) + SB_XGAP;
if (xr - bw > 8 + ui_width(timebuf, SB_FONT) + SB_CLK_MARGIN)
sb_r_place(&xr, SB_I_BRIGHT, bright, sb_fg, SB_ICON_INK); /* 亮度:图标+%(主色,与时钟不重叠才画) */
}
g_sb_clock_x = 4; g_sb_clock_y = sb_text_y();
g_sb_clock_w = 12 + ui_width(timebuf, SB_FONT); g_sb_clock_h = SB_BAR_H;
g_sb_right_x = xr + SB_XGAP;
g_sb_right_y = sb_text_y(); g_sb_right_w = (fb_xres - 8) - g_sb_right_x + 4;
g_sb_right_h = SB_BAR_H;
meow_dirty_add(0, SB_BAR_Y, fb_xres, SB_BAR_H); /* 整条标脏 */
}
/* 只重画状态栏整条到 SDK back_buf 并标整条脏 —— 每秒(时钟)/ 事件(电量/WiFi/音量)触发,绝不动列表。
* (取代 M2.16 的「只标当前内容包围盒」路径——它造成残留。) */
static void paint_sb_clock(void) { sb_redraw(); }
static void paint_sb_right(void) { sb_redraw(); }
static void draw_statusbar(void)
{
sb_redraw();
const meow_ui_theme_t *th = s_ui_theme(); /* M2.44 分隔线随主题 outline */
fill_rect(0, SB_BAR_H, fb_xres, 1, th->outline); /* separator(整条之外,单独绘制;栏底=28) */
}
/* returns 0 on success (app ran and exited), -1 on launch failure */
static int launch_app(struct app *a);
/* M2.36 B2b:实体系统设置页(定义在下方 write_brightness 之后;run_menu 触摸命中
* 「系统设置」行时调用,返回后由调用方重置按键防抖基线防面板误切换)。 */
static void run_settings_page(void);
/* 阶段B:通知中心面板(定义在下方 run_settings_page 之后;run_menu 顶部热区下滑调用)。
* *want_launch 初始为 -1;若点击通知命中可启动 app,返回时置为 apps[] 下标(run_menu 据此
* 走标准启动链路)。返回后由调用方重置按键防抖基线防面板误切换。 */
static void run_notif_center(struct app *apps, int napps, int *want_launch,
int live_open, int lox, int loy);
static int touch_fd = -1;
static int t_last_x = -1, t_last_y = -1;
static int t_down = 0;
static int t_down_x = 0, t_down_y = 0;
/* M2.23 手势原点锁定:BTN_TOUCH=1 只标「已按下」,坐标(goodix)不保证先于按下边沿,
* 故 t_down_x/y 延迟到「本次按下之后」的首个有效 ABS 才锁定,避免把上一手势残留
* (-1/旧坐标) 当本次原点。t_down_valid=0 表示本次尚未锁到有效坐标。 */
static int t_down_valid = 0;
/* ⑨ 顶部热区「跟手下拉」进入通知中心的 per-gesture latch:进入后置 1,本次手势不再二次进入;
* 由下次 touch-down 复位。防止「拖动中切入 run_notif_center 后返回」把同次手势再判成打开/滑动。 */
static int menu_live_open_pending = 0;
#define MENU_SWIPE_THRESH 65 /* px:抬手位移 >= 阈值判为滑动,不启动行(同 SDK SWIPE_THRESH) */
/* ⑨ 面板跟手下拉、半程可回(progress=手指位移映射;复用 mnp_anim_off/快照/ncp_anim_rebuild)。
* NCP_DRAG_ENTER_SLOP = 顶部热区下拉进入 run_notif_center(run_menu 切入时机)的位移阈值(px);
* NCP_DRAG_THR_PCT = 松手锚定阈值(progress >= fix*40/100≈410 → 补到展开;否则收回=半程取消)。
* 拖满=面板高 PH(g_ncp_ph),实现「面板顶缘 1:1 跟手」:off = dy - PH。 */
#define NCP_DRAG_ENTER_SLOP 16
#define NCP_DRAG_THR_PCT 40
/* Bug 2/4 (2026-08-28): after an app exits we return to the menu, but touch
* events that were generated DURING the app run (the goodix ghost touch the
* v6.4 watchdog guards against, plus the back-tap/press that actually exited
* the app) are still queued on the launcher's touch fd and get replayed as
* fresh menu taps. A replayed tap on an app row relaunches the app (symptom:
* BYE then the app's main screen — Bug 2); a replayed tap on the "BACK TO
* PLAYER" row closes the menu and SIGCONTs the player (symptom: whole screen
* flashes back to the player — Bug 4). We drain the queue immediately after
* the app exits and ignore touches for a short cooldown that covers the
* measured 1.2-2.25s ghost window (matches the watchdog's COOLDOWN_MS). */
static unsigned long long menu_touch_cooldown_until = 0;
/* M2.6 幽灵触摸根除:会话边界(app退出/电源菜单关闭/启动/重启确认)统一
* drain 触摸队列 + 冷却(TAP_ARM_DELAY),避免触发开合的那次触摸/残留被
* 重放成「点击第 0 行 hello」。
* M2.22(D):700ms 偏激进,把正常点击也当「启动残留触摸」丢弃(点一下没反应要点几下)。
* 用户要求「点一下就能进」,同时仍防 ghost 重放 → 调窄到 300ms。 */
#define TAP_ARM_DELAY_MS 300
static void menu_drain_touch(void)
{
if (touch_fd >= 0) {
unsigned char ev[16];
while (read(touch_fd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ }
}
}
static void menu_arm_touch(long ms)
{
menu_drain_touch();
menu_touch_cooldown_until = now_ms() + ms;
}
/* Drain the touch queue + arm the post-app cooldown (call right after the
* app exits, before the menu resumes polling touch). The drain always runs so
* touches queued by the launch-attempt tail are cleared; the cooldown only
* arms when an app actually ran (app_ran) so a failed launch doesn't lock the
* menu out for 2.5s. */
static void menu_after_app_exit(int app_ran)
{
menu_drain_touch();
/* M2.22(D):2500ms 冷却把正常的「app 退出后立即点下一个应用」也吞掉(点几下才进)。
* 用户要求点一下就能进 → 调窄到 1200ms,仍覆盖 goodix ghost 窗口(1.2–2.25s 实测略短,
* 与 watchdog COOLDOWN_MS 同阶),防启动重放。只调窄,保留 cooldown 本身。 */
if (app_ran) menu_touch_cooldown_until = now_ms() + 1200;
}
/* Header block (status bar + MEOWAPPS title + thin divider) covering the
* region y=0..58, kept separate from the app-list body so the ~1s clock tick
* (draw_clock) never redraws or tears the list. draw_menu fills the whole
* background then calls draw_header_content. The menu is pre-drawn before
* fb_norm_buf0 so the initial pan shows a complete frame. */
static void draw_header_content(void)
{
draw_statusbar();
/* M2.6 标题真字体(28px;回退点阵3x) */
if (mfont_ready) {
meow_font_drawtext((uint16_t *)fb, 24, 48, "MeowOS 应用", rgb565(0xFFE066), 28);
fill_rect(0, 48 + meow_font_line_height(28) + 2, fb_xres, 2, 0x3A3A5C);
} else {
draw_text(24, 28, "MEOWAPPS", 3, 0xFFE066);
fill_rect(0, 56, fb_xres, 2, 0x3A3A5C);
}
}
/* Refresh ONLY the clock digits (top-left, HH:MM:SS) once per second — the one
* status-bar value that changes every second. This is what makes the "clock
* updates every second" work WITHOUT redrawing (or tearing) the app list, and
* WITHOUT the slow amixer popen that a full draw_statusbar() would incur on
* each tick. Everything else (battery/wifi/bluetooth/volume) changes only on
* user input, which triggers a full draw_menu via the dirty flag. Clears a
* small box then redraws the time, so old digits do not ghost. */
static void draw_clock(void)
{
paint_sb_clock(); /* M2.16:本地重画时钟格(每秒),只动这个矩形 */
}
/* M2.36 右向小箭头(进入/下拉提示)。点阵后备字库(5x7)缺 '>' 字形(glyph_index 落到空
* 格),故用像素画一个 4x7 的 '>';顶点在 (x,y) 指向右。设置页选择器/设置项行复用。 */
static void s_draw_chevron(int x, int y, uint32_t color)
{
for (int i = 0; i < 4; i++) {
put_px(x - i, y - i, color);
put_px(x - i, y + i, color);
}
}
/* M2.39 左向小箭头(返回提示)。点阵后备字库同样缺 '<' 字形('‹' 会渲染成方框),
* 故用像素画一个指向左的 4x7 '<';顶点在 (x,y)。设置页底部「返回」/任何左向提示复用。 */
static void s_draw_chevron_left(int x, int y, uint32_t color)
{
for (int i = 0; i < 4; i++) {
put_px(x + i, y - i, color);
put_px(x + i, y + i, color);
}
}
static void draw_menu(struct app apps[MAX_APPS], int napps, int state,
int cursor, int toast_ms, const char *toast)
{
fill_rect(0, 0, fb_xres, fb_yres, menu_bg);
draw_header_content();
char line[64];
int y = ROW_TOP;
if (state == 1) {
if (mfont_ready) { ui_draw(30, 200, "没有存储卡", 0xFFFFFF, 20); ui_draw(30, 234, "请插入TF卡后重开菜单", 0x9FB8D9, 20); }
else { draw_text(30, 200, "NO STORAGE CARD", 2, 0xFFFFFF); draw_text(30, 240, "INSERT TF CARD", 2, 0x9FB8D9); draw_text(30, 272, "THEN RE-OPEN MENU", 2, 0x9FB8D9); }
} else if (state == 2 || napps == 0) {
if (mfont_ready) { ui_draw(30, 200, "没有找到应用", 0xFFFFFF, 20); ui_draw(30, 234, "请放入应用到", 0x9FB8D9, 20); ui_draw(30, 268, "MeowAPPs/<id>/app.json", 0x9FB8D9, 20); }
else { draw_text(30, 200, "NO APPS FOUND", 2, 0xFFFFFF); draw_text(30, 240, "PUT APP IN", 2, 0x9FB8D9); draw_text(30, 272, "MeowAPPs/<id>/app.json", 2, 0x9FB8D9); }
} else {
for (int i = 0; i < napps; i++) {
int sel = (i == cursor);
if (sel) {
fill_rect(8, y - 4, fb_xres - 16, ROW_H - 8, 0x2A3A6A);
snprintf(line, sizeof line, "%d %s", i + 1, apps[i].name);
ui_draw(24, y, line, 0xFFFFFF, 20);
if (!apps[i].ok) ui_draw(24, y + ROW_H - 24, "[缺失]", 0xFF6B6B, 20);
} else {
snprintf(line, sizeof line, "%d %s", i + 1, apps[i].name);
ui_draw(24, y, line, 0xC8D0E8, 20);
if (!apps[i].ok) ui_draw(24, y + ROW_H - 24, "[缺失]", 0x8A4A4A, 20);
}
y += ROW_H;
}
}
/* M2.36 B2b:系统设置 builtin 行(过渡入口)。位于应用项之后、Exit 行之前;
* 不依赖 .mpk(图标/文案就地绘制)。音量导航可把光标移到此处(cursor==napps),
* 触摸点击该行进入设置页(run_menu 分发到 run_settings_page)。 */
{
int sy = EXIT_ROW - ROW_H; /* 固定在 Exit 行上方 588 */
int selset = (cursor == napps); /* 光标已移到设置行 */
fill_rect(8, sy - 4, fb_xres - 16, ROW_H - 8, selset ? 0x2A3A6A : 0x12142A);
if (mfont_ready) ui_draw(24, sy, "系统设置", selset ? 0xFFFFFF : 0xC8D0E8, 20);
else draw_text(24, sy, "SETTINGS", 2, selset ? 0xFFFFFF : 0xC8D0E8);
s_draw_chevron(fb_xres - 22, sy + ROW_H / 2, 0x9FB8D9); /* 右向进入提示 */
}
/* exit row */
fill_rect(8, EXIT_ROW - 4, fb_xres - 16, ROW_H - 8, 0x1A1A3A);
/* M2.2/M2.6:MeowOS 常驻主界面(播放器已死)时改「重启返回播放器」。 */
if (mfont_ready) ui_draw(24, EXIT_ROW, meow_mode ? "重启返回播放器" : "返回播放器", 0x8FBFD9, 20);
else draw_text(24, EXIT_ROW, meow_mode ? "REBOOT TO PLAYER" : "BACK TO PLAYER", 2, 0x8FBFD9);
/* M2.18 Bug 3: 底部对齐,弃用硬编码 y=700。屏幕高 fb_yres=720,字号 20 真实行高
* meow_font_line_height(20)=27(ascent 21 + descent 6);原 y=700 使字底 = 700+27 = 727
* > 720,溢出 7px ≈ 1/5 字符高被裁。改为 y = fb_yres - 行高 - 底部留白。非 mfont 回退
* 分支(5x7 点阵,字高=7*scale)同样底部对齐,两分支都处理。 */
#define FOOTY_PAD 8
/* dev version, bottom-right: single source MEOWOS_VERSION. */
{
char ver[32];
snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version);
int foot_y = fb_yres - meow_font_line_height(20) - FOOTY_PAD;
ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, 0x666688, 20);
}
if (toast_ms > 0 && toast && toast[0]) {
fill_rect(20, TOAST_Y - 6, fb_xres - 40, 40, 0x2A0A0A);
ui_draw(30, TOAST_Y, toast, 0xFF8080, 20);
}
}
/* ================================================================== */
/* M2.3A 电源菜单(Android 11 样式):长按电源 3s 弹出;8s 强制关机 */
/* 半透明深色遮罩 + 居中 5 个大按钮(2 列×3 行,ASCII 点阵,不耦合字体) */
/* 与「单击=息屏」「电源+音量减=切MeowOS」按电源按住时长区分。 */
/* ================================================================== */
/* M2.3A sim 钩子(电源菜单验证,物理留用户):
* --sim-power-hold N —— 主循环 N ms 后强制 pwr=1(模拟按住电源),驱动
* 3s 弹电源菜单 / 8s 强制关机两条路径。
* --sim-power-pick N —— 电源菜单打开 2s 后自动选中按钮 N(0..4;M2.29 起 5 钮)。
* --sim-no-exec —— 免执行:电源动作(关机/重启/回HibyOS)/8s强关仅打 log,
* 不真正 poweroff/reboot(安全 sim 用)。 */
static long sim_power_hold_ms = 0;
static int sim_power_pick = -1;
static int sim_no_exec = 0;
#define PM_NUM 5
/* M2.31 电源菜单 Android12(MD3) 圆钮:正中间圆角卡片 + 每行 2 个大圆钮 + 换行居中。
* 5 钮 → 2 列×3 行(行3=取消,单钮居中,默认选中安全项)。
* PM_R 半径50(直径100),行距 PM_GAPY=140、圆心距 PM_GAPX=130 保证圆下 label 不被下一行圆压住;
* 卡片 40..440 / 115..620 居中放置。 */
#define PM_R 50 /* 圆钮半径(直径 100) */
#define PM_GAPX 130 /* 两列圆心 x 间距(175->305,居中 240) */
#define PM_GAPY 140 /* 相邻行圆心 y 间距 */
#define PM_CARD_X0 40 /* 圆角卡片左 */
#define PM_CARD_W 400 /* 圆角卡片宽(40..440,居中) */
#define PM_CARD_CX (PM_CARD_X0 + PM_CARD_W / 2) /* 卡片水平中心 240 */
#define PM_CARD_Y0 115 /* 圆角卡片顶 */
#define PM_CARD_H 505 /* 圆角卡片高(115..620) */
#define PM_CARD_R 26 /* 圆角卡片圆角 */
static const int pm_cx[PM_NUM] = { PM_CARD_CX - PM_GAPX / 2, PM_CARD_CX + PM_GAPX / 2,
PM_CARD_CX - PM_GAPX / 2, PM_CARD_CX + PM_GAPX / 2, PM_CARD_CX };
static const int pm_cy[PM_NUM] = { 250, 250, 390, 390, 530 }; /* 圆心 y(行距 140) */
static const char *pm_labels_cn[PM_NUM] = { "关机", "重启", "回HiBy", "升级", "取消" };
static const char *pm_labels_en[PM_NUM] = { "POWER OFF", "RESTART", "HIBYOS", "UPGRADE", "CANCEL" };
static void fill_circle(int cx, int cy, int r, uint32_t color)
{
for (int dy = -r; dy <= r; dy++)
for (int dx = -r; dx <= r; dx++)
if (dx * dx + dy * dy <= r * r)
put_px(cx + dx, cy + dy, color);
}
/* 圆角矩形 */
static void fill_round_rect(int x0, int y0, int w, int h, int r, uint32_t color)
{
fill_rect(x0 + r, y0, w - 2 * r, h, color);
fill_rect(x0, y0 + r, w, h - 2 * r, color);
fill_circle(x0 + r, y0 + r, r, color);
fill_circle(x0 + w - r, y0 + r, r, color);
fill_circle(x0 + r, y0 + h - r, r, color);
fill_circle(x0 + w - r, y0 + h - r, r, color);
}
/* 电源菜单圆钮图标:从 pm_icons.h 的 56×56 alpha 位图 blit 到 fb(以圆钮中心 cx,cy 为锚居中)。
* alpha 按 col(RGB565)映射:图标前景色随选中/未选中主题色变化(日夜间由 pm_theme_* 提供)。
* dst_px 为 fb16bpp 写入:新像素 = col 前景与背景(btn bg)按 alpha 混合。 */
static void blit_pm_icon(int idx, int cx, int cy, uint16_t col)
{
const unsigned char *src = pm_icons[idx].data;
int S = PM_ICON_SIZE;
int x0 = cx - S / 2, y0 = cy - S / 2;
for (int y = 0; y < S; y++) {
int yy = y0 + y;
if (yy < 0 || yy >= fb_yres) continue;
uint16_t *row = (uint16_t *)fb + yy * (fb_line / 2);
for (int x = 0; x < S; x++) {
int a = src[y * S + x];
if (a < 8) continue; /* 透明像素跳过 */
int xx = x0 + x;
if (xx < 0 || xx >= fb_xres) continue;
/* 前景 col 已含所需亮度;此处仅做软边 alpha 混合到背景,避免锯齿 */
uint16_t bg = row[xx];
if (a >= 240) { row[xx] = col; continue; }
int a4 = a >> 4; /* 0..15 */
int r = ((col >> 11) * a4 + ((bg >> 11) & 0x1F) * (16 - a4)) >> 4;
int g = (((col >> 5) & 0x3F) * a4 + ((bg >> 5) & 0x3F) * (16 - a4)) >> 4;
int b = ((col & 0x1F) * a4 + (bg & 0x1F) * (16 - a4)) >> 4;
row[xx] = (uint16_t)((r << 11) | (g << 5) | b);
}
}
}
/* 绘制电源菜单圆钮 i(MD3 圆形:外描边+内填充+圆内 Material 图标;选中高亮)。
* 圆内放 Material Symbols 位图,文字标签放圆下(Android 12 式:icon 上、label 下)。 */
static void draw_pm_button(int i, int selected)
{
int cx = pm_cx[i], cy = pm_cy[i];
/* 主题(日夜间由 pm_theme_* 决定;默认取 dark,后续接设置) */
const pm_theme_t *th = &pm_theme_dark;
/* 先铺一块比任何选中圈都大的卡片底色圆,盖掉上一态残留的选中描边(问题3:
* 焦点移走后 PM_R+2..PM_R+6 之间残留亮圈。以 0x1C2438 卡片色清底)。 */
fill_circle(cx, cy, PM_R + 10, 0x1C2438);
/* MD3 无边框高亮:选中=未选中底色稍亮填充(无描边圈),仅底色区分焦点;避免圆形描边残留。 */
if (selected)
fill_circle(cx, cy, PM_R, th->btn_sel_bg);
else
fill_circle(cx, cy, PM_R, th->btn_nor_bg);
/* 圆内 Material 图标(alpha 位图 + 主题前景色) */
blit_pm_icon(i, cx, cy, selected ? th->icon_sel : th->icon_nor);
uint32_t col = selected ? 0xFFFFFF : 0xC8D0E8;
/* 圆下文字标签(真字体/中文 或 点阵 ASCII 回退) */
int ly = cy + PM_R + 8;
if (mfont_ready) {
const char *lab = pm_labels_cn[i];
int lw = meow_font_width_text(lab, 18);
meow_font_drawtext((uint16_t *)fb, cx - lw / 2, ly, lab, rgb565(col), 18);
} else {
const char *lab = pm_labels_en[i];
int lw = (int)strlen(lab) * 5;
draw_text(cx - lw / 2, ly + 1, lab, 1, col);
}
}
/* 把当前显示 buffer(buffer0)逐像素压暗(半透明遮罩模拟) */
static void dim_screen(void)
{
for (int y = 0; y < fb_yres; y++) {
unsigned char *row = fb + (size_t)y * fb_line;
for (int x = 0; x < fb_xres; x++) {
unsigned char *p = row + x * (fb_bpp / 8);
if (fb_bpp == 16) {
uint16_t c = *(uint16_t *)p;
int r = (((c >> 11) & 0x1F) * 3) / 5;
int g = (((c >> 5) & 0x3F) * 3) / 5;
int b = ((c & 0x1F) * 3) / 5;
*(uint16_t *)p = (uint16_t)((r << 11) | (g << 5) | b);
}
}
}
}
/* A-0:电源菜单背景 = app 当前帧。从 fb0 buffer0(yoffset=0,app 永不 pan)读回 app
* SIGSTOP 后的最后一帧 → back_buf,再经 dim_screen 压暗作菜单背景,替代旧的「压暗
* launcher 应用列表底」(Bug:应用内长按电源,背景错显示成应用列表)。
* 参考 fb_backup_player 的读回思路,但目标是 app 帧而非播放器帧(播放器帧在其自身
* yoffset,app 帧固定 buffer0)。MeowOS 主界面(无 app 运行)时 buffer0 = launcher
* 列表帧,读回即现有「压暗列表底」效果,无回归。 */
static void pm_capture_app_frame(void)
{
if (!fb_mem || !back_buf_ok || !fb) return; /* 无真实 fb / 直写模式:维持现状 */
memcpy(fb, fb_mem, (size_t)fb_line * (size_t)fb_yres); /* fb = SDK back_buf */
meow_dirty_full(); /* 新背景整帧呈现 */
logmsg("pm: menu bg = app frame (fb0 buffer0)");
}
/* Bug 2 修复:直连 reboot(2) syscall。M2.6 用 system("... reboot"/"poweroff") 经 BusyBox
* 不生效(设备实测:动作日志打了但不起机/不关机——BusyBox reboot 非 PID1 时依赖 init 上的
* 信号/teinit 握手,本设备不完成;且 system() fork+sh 有竞态)。改为直接 syscall 绕过
* /bin/sh 与 init:sync() 后调用 reboot(2),内核立即重启/关机。 */
#define LINUX_REBOOT_MAGIC1 0xfee1dead
#define LINUX_REBOOT_MAGIC2 672274793 /* 0x28121969 */
#define LINUX_REBOOT_CMD_RESTART 0x01234567
#define LINUX_REBOOT_CMD_POWER_OFF 0x4321fedc
static void sys_do_reboot(int cmd)
{
/* M2a:正常电源事件(关机/重启)触发清除板载通知数据之前,先转储所有未上卡通知
* (NOTIFICATION_SYSTEM.md §8)。meow_notif_before_poweroff 幂等安全(未 init 直接返回)。 */
meow_notif_before_poweroff();
sync(); /* 先落盘,避免数据丢失 */
logmsg("sys_do_reboot cmd=0x%x", cmd);
syscall(SYS_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, cmd, NULL);
logmsg("sys_do_reboot returned: %s", strerror(errno)); /* 正常不会返回 */
for (;;) usleep(1000); /* 防御:永不返回 */
}
/* 回HibyOS:写板载 /data/meow_boot_default=1(开机默认权威标记,O_TRUNC 单值、不写 TF 卡),
* 再 reboot 回选单并默认 HibyOS。meow.conf 不再写/承载 boot_default(bootmenu 以板载标记为准)。 */
static void set_boot_default_hiby(void)
{
int fd = open("/data/meow_boot_default", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) { write(fd, "1\n", 2); close(fd); logmsg("pm: /data/meow_boot_default=1"); }
}
/* 电源菜单(M2.6):2×2 网格,dirty 绘制消频闪;输入修复:
* - 触摸按下捕获坐标、抬起命中按钮即确认(同 run_menu MT 防抖);
* - 音量上/下导航;播放=确认;电源新按(松开长按后)=确认当前钮(非取消);
* - 取消=选「取消」钮;8s 持续按住=强制关机(绕过)。
* 返回 1=采取动作,0=取消。 */
static int show_power_menu(void)
{
int fbfd = open(FB_DEV, O_RDWR);
if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 0); close(fbfd); } /* 亮起面板 */
pm_capture_app_frame(); /* A-0:菜单背景 = app 当前帧(fb0 buffer0),再压暗绘制其上 */
dim_screen();
/* M2.8 Bug 3-residual:若电源菜单是在「息屏」态打开(按住 3s),此处 FBIOBLANK(0)
* 刚上电,LCDC 扫描地址可能非 buffer0;强制 re-pan 让压暗的应用列表 + 菜单落在被显示的
* 缓冲,否则菜单画在非显示缓冲上不可见。 */
fb_pan_buf0_force();
meow_screen_on = 1; /* 菜单打开即视为亮屏 */
/* M2.11 B:默认光标 = 「取消」(安全项,index 4)。用户打开菜单后未导航就按电源/播放,
* 只会取消关菜单,绝不因「按电源键」本身关机/重启。要真关机必须先导航到「关机」再确认。 */
int sel = PM_NUM - 1; /* 默认取消(安全) */
/* M2.30 MD3:居中圆角卡片背景 + 标题居中 + 圆钮网格。卡片白色柔和描边/深色底。 */
fill_round_rect(PM_CARD_X0, PM_CARD_Y0, PM_CARD_W, PM_CARD_H, PM_CARD_R, 0x141824);
fill_round_rect(PM_CARD_X0 + 2, PM_CARD_Y0 + 2, PM_CARD_W - 4, PM_CARD_H - 4, PM_CARD_R - 2, 0x1C2438);
/* 卡片顶部标题,水平居中 */
char title[32];
snprintf(title, sizeof title, "POWER MENU");
int title_y = PM_CARD_Y0 + 26; /* 标题在卡片内顶部(卡片顶 115,标题基线 ~141) */
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, (fb_xres - meow_font_width_text(title, 20)) / 2, title_y, title, rgb565(0xFFFFFF), 20);
else draw_text((fb_xres - (int)strlen(title) * 6) / 2, title_y, title, 2, 0xFFFFFF);
/* 底部提示行(卡片下方,非圆钮所需) */
int hint_y = PM_CARD_Y0 + PM_CARD_H + 10;
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, (fb_xres - meow_font_width_text("音量导航 电源确认", 18)) / 2, hint_y, "音量导航 电源确认", rgb565(0x777799), 18);
else draw_text((fb_xres - (int)strlen("VOLUME NAV POWER OK") * 5) / 2, hint_y + 1, "VOLUME NAV POWER OK", 1, 0x777799);
meow_dirty_full(); /* M2.16:把压暗列表+卡片+标题+(下方圆钮)作为一整帧呈现 */
/* 触摸状态(按下→抬起):统一 meow_touch 接口(goodix 时序免疫,原点由首个有效 ABS 锁) */
meow_touch mt;
mt_reset(&mt);
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER);
kp_open(&kp_keys, DEV_KEYS);
int prev_vold = 0, prev_volu = 0, prev_play = 0, prev_pwr = 0;
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
prev_pwr = kp_is_down(&kp_power, KEY_POWER) ? 1 : 0;
prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN) ? 1 : 0;
prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP) ? 1 : 0;
prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE) ? 1 : 0;
int act = -1;
unsigned long long pm_t0 = now_ms();
unsigned long long pm_last_interact = pm_t0; /* M2.33:菜单空闲自动关闭基准(任何互动刷新) */
int pm_init = 0, last_sel = -1;
/* M2.10 Bug:电源菜单 8s 强关(按住电源≥8s=绕过强关)会覆盖用户的菜单操作 —— 用户按住
* 电源打开菜单后(尤其仍按着电源)点选「回到HibyOS/重启」等按钮,8s 一到先触发强关 → 关机
* 而非执行所选动作。修法:一旦用户与菜单互动(音量导航/触摸/播放/电源确认),置 pm_interacted=1
* 取消 8s 强关,让所选动作(如「回到HibyOS」→ reboot RESTART)生效。仅当全程未互动才保留「按住
* 8s=强关」。*/
int pm_interacted = 0;
for (;;) {
/* dirty 绘制:首次全画;此后仅选中变化才重画受影响两钮(消频闪)→ 标脏 + present。 */
if (!pm_init) {
for (int i = 0; i < PM_NUM; i++) draw_pm_button(i, i == sel);
pm_init = 1; last_sel = sel;
meow_ui_flush(); /* A-1/A-2:SDK present(vsync+帧率门控+blit 脏=全帧,含压暗列表+标题+按钮) */
} else if (sel != last_sel) {
draw_pm_button(last_sel, 0);
draw_pm_button(sel, 1);
/* M2.30 圆形:dirty 区 = 圆(圆心±半径)+ 圆下文字(半径+偏移+行高),四周留 10px */
int dr = (PM_R + 6) + 10, dtxt = PM_R + 26 + 10;
meow_dirty_add(pm_cx[last_sel] - dr, pm_cy[last_sel] - dr, dr * 2, dr + dtxt);
meow_dirty_add(pm_cx[sel] - dr, pm_cy[sel] - dr, dr * 2, dr + dtxt);
last_sel = sel;
meow_ui_flush(); /* A-1/A-2:SDK present,blit 两钮区 */
}
if (sim_power_pick >= 0 && now_ms() - pm_t0 >= 2000) act = sim_power_pick;
int vold = 0, volu = 0, play = 0, pwr = 0;
if (kp_keys.ok) { kp_poll(&kp_keys, NULL, NULL);
vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); }
if (kp_power.ok) { kp_poll(&kp_power, NULL, NULL); pwr = kp_is_down(&kp_power, KEY_POWER); }
if (vold && !prev_vold) { sel = (sel + 1) % PM_NUM; pm_interacted = 1; pm_last_interact = now_ms(); }
if (volu && !prev_volu) { sel = (sel - 1 + PM_NUM) % PM_NUM; pm_interacted = 1; pm_last_interact = now_ms(); }
/* M2.19:播放键不再作确认(让位给全局媒体控制)。电源菜单的物理确认
* 已由电源键负责(下行),播放键删除,重复确认移除。 */
(void)play;
if (pwr && !prev_pwr) { act = sel; pm_interacted = 1; pm_last_interact = now_ms(); } /* 电源新按=确认(非取消) */
prev_vold = vold; prev_volu = volu; prev_play = play; prev_pwr = pwr;
/* 触摸:按下(meow_touch 原点锁定,goodix 时序免疫),抬起按按钮命中确认 */
if (tfd >= 0) {
struct pollfd pfd = { tfd, POLLIN, 0 };
if (poll(&pfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == (ssize_t)sizeof ev) {
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) mt_abs(&mt, val, mt.last_y);
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) mt_abs(&mt, mt.last_x, val);
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val && !mt.down) {
mt_down(&mt);
pm_interacted = 1;
pm_last_interact = now_ms();
} else if (!val && mt.down) {
pm_interacted = 1;
pm_last_interact = now_ms();
int dx, dy;
/* M2.25:抬手位移 >= MEOW_TOUCH_SWIPE_THRESH 判滑动(返回0),
* 不做确认;否则有效 tap,按下原点命中的按钮即确认。 */
if (mt_up(&mt, &dx, &dy)) {
/* M2.30 圆形命中:按下原点距圆心 <= PM_R+6 即命中 */
for (int i = 0; i < PM_NUM; i++) {
int ddx = mt.down_x - pm_cx[i], ddy = mt.down_y - pm_cy[i];
int rr = (PM_R + 6) * (PM_R + 6);
if (ddx * ddx + ddy * ddy <= rr) { sel = i; act = i; break; }
}
}
}
}
}
}
}
/* 8s 强关(绕过):仅当电源持续按住、且用户全程未与菜单互动才触发 —— 放在触摸/按键处理
* 之后,避免「用户已点选按钮」的同一轮被强关抢先(M2.10 Bug)。 */
if (pwr && !pm_interacted && now_ms() - pm_t0 >= 5000) {
if (sim_no_exec) { logmsg("pm: [sim-no-exec] POWER HOLD 8s -> would force poweroff"); }
else { logmsg("pm: POWER HOLD 8s -> force poweroff"); sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); }
return 1;
}
/* M2.33 菜单空闲超时自动关闭:菜单受 MeowOS screen_off_timeout 管理(系统界面不豁免)。
* 超过超时无任何互动 → 关闭菜单(视为取消)并自动息屏。目的是:息屏后单击电源亮屏时,
* 电源按下信号不会命中菜单按钮(菜单已关),杜绝误触关机/执行菜单动作。 */
if (screen_off_timeout > 0 &&
(unsigned long long)(now_ms() - pm_last_interact) >= (unsigned long long)screen_off_timeout * 1000) {
logmsg("pm: idle %ds -> close menu + auto screen-off (act=cancel)", screen_off_timeout);
meow_panel_off();
meow_auto_slept = 1;
act = 4; /* 取消(关菜单) */
break;
}
if (act >= 0) break;
usleep(POLL_MS * 1000);
}
if (tfd >= 0) close(tfd);
/* M2.11 B:电源键在菜单 = 按当前项确认。act 与 syscall 类型逐项核对:
* act==4(取消) -> 只关菜单,绝不关机(return 0,永不到 reboot);
* act==0(关机) -> 才 poweroff;act==1(重启)/act==2(回HibyOS)/act==3(升级模式) -> 才 reboot。
* 只有 act 为 0/1/2/3 才可能执行 sys_do_reboot;power 键本身绝不触发关机。 */
logmsg("pm: act=%d -> %s", act,
act == 0 ? "关机" : act == 1 ? "重启" : act == 2 ? "回HibyOS" : act == 3 ? "升级模式" : "取消");
if (act == 4) { logmsg("pm: cancel (close menu only)"); return 0; }
if (act == 0) {
logmsg("pm: POWER OFF");
if (!sim_no_exec) {
/* 先显关机 logo(cmd_jpeg_display 是独立二进制,能正常显图),再直连 poweroff */
system("PATH=/bin:/sbin:/usr/bin:/usr/sbin cmd_jpeg_display /etc/shutdown_logo.jpeg >/dev/null 2>&1");
sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF);
}
} else if (act == 1) {
logmsg("pm: REBOOT");
if (!sim_no_exec) sys_do_reboot(LINUX_REBOOT_CMD_RESTART);
} else if (act == 2) {
logmsg("pm: RETURN TO HIBYOS");
set_boot_default_hiby();
if (!sim_no_exec) sys_do_reboot(LINUX_REBOOT_CMD_RESTART);
} else if (act == 3) {
/* M2.29 升级模式:写 mtd5=ota:kernel2(bootmode.sh Recovery = flash_erase + nandwrite)
* 再 reboot —— uboot 读 mtd5=ota:kernel2 引导 kernel2(升级模式,自动刷 TF 卡
* r3proii.upt,双进度条,跑完重启进新固件)。已验证此链路可靠。 */
logmsg("pm: ENTER UPGRADE");
if (!sim_no_exec) {
system("PATH=/bin:/sbin:/usr/bin:/usr/sbin /usr/bin/bootmode.sh Recovery >/dev/null 2>&1");
sys_do_reboot(LINUX_REBOOT_CMD_RESTART);
}
}
return 1;
}
/* M2.6 G「Reboot to player」:MeowOS 常驻模式点「返回播放器」时,播放器已死,弹确认框
* 「返回播放器需重启,现在重启?[是/否]」。是=写选单默认 HibyOS + reboot;否=留在 MeowOS。
* VOL=是/否 切换;PLAY=确认;POWER=取消;触摸点 是/否 确认。风格同电源菜单。
* 返回 1=是(已 reboot,通常不回),0=否/取消(调用方重绘应用列表)。 */
/* 「返回播放器」确认框「是/否」按钮(左右),选中高亮。
* M2.9 Bug 4:改为 dirty 绘制 —— 仅 sel 变化时重画两钮(旧钮取消高亮 + 新钮高亮),
* 不随主循环每 10ms 全刷(原实现每帧重画同一内容,与 LCD 刷新竞争产生频闪)。 */
static void draw_cr_button(int sel, int i)
{
int x0 = (i == 0) ? 80 : 280, y0 = 420, w = 120, h = 60;
fill_round_rect(x0, y0, w, h, 12, (i == sel) ? 0x2A3A6A : 0x1A1A2A);
const char *lbl = (i == 0) ? "是" : "否";
if (mfont_ready) {
int lw = meow_font_width_text(lbl, 20);
meow_font_drawtext((uint16_t *)fb, x0 + w / 2 - lw / 2, y0 + 30 - meow_font_line_height(20) / 2,
lbl, rgb565((i == sel) ? 0xFFFFFF : 0xC8D0E8), 20);
} else {
draw_text(x0 + w / 2 - 6, y0 + 24, (i == 0) ? "YES" : "NO", 2, (i == sel) ? 0xFFFFFF : 0xC8D0E8);
}
}
static int confirm_reboot_player(void)
{
dim_screen();
/* M2.8 Bug 3-residual:确认框同电源菜单 —— 若打开前刚经历息屏/blank,LCDC 扫描地址可能
* 非 buffer0;强制 re-pan 让「压暗列表 + 是/否」落在被显示的缓冲。 */
fb_pan_buf0_force();
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 320, "返回播放器需重启,现在重启?", rgb565(0xFFFFFF), 20);
else draw_text(20, 322, "REBOOT TO RETURN TO PLAYER?", 2, 0xFFFFFF);
meow_touch mt;
mt_reset(&mt);
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER);
kp_open(&kp_keys, DEV_KEYS);
int prev_vold = 0, prev_volu = 0, prev_play = 0, prev_pwr = 0;
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
prev_pwr = kp_is_down(&kp_power, KEY_POWER) ? 1 : 0;
prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN) ? 1 : 0;
prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP) ? 1 : 0;
prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE) ? 1 : 0;
int sel = 1; /* 默认「否」:不误重启 */
int act = -1;
int cr_init = 0, cr_last_sel = -1; /* M2.9 Bug 4:dirty——仅 sel 变化才重画两钮 */
/* 标题/按键提示:打开时画一次(原实现把它们放主循环每 10ms 全刷,与 LCD 刷新竞争 →
* 「是/否」钮频闪;改为只在打开/状态变化时绘制)。标题在前面已画一次,这里画提示行。 */
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 560, "音量切换 电源取消", rgb565(0x666688), 20);
else draw_text(20, 562, "VOL NAV POWER EXIT", 1, 0x666688);
meow_dirty_full(); /* M2.16:压暗列表+标题+提示作为一整帧呈现 */
for (;;) {
/* dirty 绘制:首次全画两钮;此后仅 sel 变化才重画(消频闪)→ 标脏 + present。 */
if (!cr_init) {
for (int i = 0; i < 2; i++) draw_cr_button(sel, i);
cr_init = 1; cr_last_sel = sel;
meow_ui_flush();
} else if (sel != cr_last_sel) {
draw_cr_button(sel, cr_last_sel); /* 旧选中:取消高亮 */
draw_cr_button(sel, sel); /* 新选中:高亮 */
meow_dirty_add((cr_last_sel == 0 ? 80 : 280) - 10, 412, 140, 76);
meow_dirty_add((sel == 0 ? 80 : 280) - 10, 412, 140, 76);
cr_last_sel = sel;
meow_ui_flush();
}
int vold = 0, volu = 0, play = 0, pwr = 0;
if (kp_keys.ok) { kp_poll(&kp_keys, NULL, NULL);
vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); }
if (kp_power.ok) { kp_poll(&kp_power, NULL, NULL); pwr = kp_is_down(&kp_power, KEY_POWER); }
if (vold && !prev_vold) sel = (sel + 1) % 2;
if (volu && !prev_volu) sel = (sel - 1 + 2) % 2;
/* M2.19:播放键不再作「是/确认」(让位给全局媒体控制)。确认框的确认
* 走触摸点选(下方 tfd 命中),电源键=取消(下行);播放键删除。 */
(void)play;
if (pwr && !prev_pwr) act = 3; /* 电源=取消 */
prev_vold = vold; prev_volu = volu; prev_play = play; prev_pwr = pwr;
if (tfd >= 0) {
struct pollfd pfd = { tfd, POLLIN, 0 };
if (poll(&pfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == (ssize_t)sizeof ev) {
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) mt_abs(&mt, val, mt.last_y);
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) mt_abs(&mt, mt.last_x, val);
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val && !mt.down) {
mt_down(&mt);
} else if (!val && mt.down) {
int dx, dy;
if (mt_up(&mt, &dx, &dy)) {
for (int i = 0; i < 2; i++) {
int x0 = (i == 0) ? 80 : 280;
/* 命中「是/否」钮:x∈[x0-8, x0+128], y∈[412, 488](含端点) */
if (mt_in(&mt, x0 - 8, 412, 137, 77))
{ sel = i; act = i; break; }
}
}
}
}
}
}
}
if (act >= 0) break;
usleep(POLL_MS * 1000);
}
if (tfd >= 0) close(tfd);
if (act == 0) {
logmsg("reboot-to-player: YES -> default HibyOS + reboot");
set_boot_default_hiby();
if (!sim_no_exec) sys_do_reboot(LINUX_REBOOT_CMD_RESTART);
return 1;
}
logmsg("reboot-to-player: cancel");
return 0;
}
/* returns: 0 = stay in menu, 1 = exit menu (BACK TO PLAYER / sim-exit),
* 2 = launch request */
/* M2.8 Bug 3-residual「统一重绘」:把当前 MeowOS 界面内容整体重绘到 buffer0 并强制 pan 到
* buffer0。息屏唤醒(电源单击亮屏)时调用 —— 唤醒不是 draw 事件:FBIOBLANK(4) 下电 ->
* FBIOBLANK(0) 上电后,LCDC 指向的内容可能已失效/为空,必须把当前真实界面(应用列表主界面,
* 含「没有存储卡/没有找到应用」态)重新画一遍再强制 re-pan,而不是仅翻页到陈旧/空缓冲。
* signature 传入当前界面状态(由 run_menu 持有),不把应用列表局部状态全局化。 */
static void meow_repaint_surface(struct app apps[MAX_APPS], int napps, int state, int cursor)
{
/* M2.11 E:记录唤醒上下文(时刻/亮度/当前 screen_on/待 yoffset),便于定位偶发「唤醒后黑屏
* 又自愈」的竞态。 */
int pre_bri = read_brightness();
int pre_screen = meow_screen_on;
logmsg("wake repaint: pre bri=%d screen_on=%d (state=%d napps=%d cursor=%d)",
pre_bri, pre_screen, state, napps, cursor);
draw_menu(apps, napps, state, cursor, 0, ""); /* 完整重绘当前视图到 back_buf */
fb_pan_buf0_force(); /* 确保落在被显示的缓冲(buffer0) */
meow_dirty_full(); meow_ui_flush(); /* A-1/A-2:SDK present(vsync + 单次全帧 blit) */
int post_bri = read_brightness();
logmsg("wake repaint done: post bri=%d (after force-pan buffer0)", post_bri);
}
/* set by on_sigterm; run_menu breaks on it so the menu closes cleanly and
* the main loop restores the player (must be declared before run_menu) */
static volatile sig_atomic_t sigterm_flag = 0;
/* M2.2 sim 钩子:--sim-exit-tap N —— 菜单打开 N ms 后模拟「点 BACK TO PLAYER 行」,
* 用于验证 MeowOS 模式下 BACK TO PLAYER 显示需重启提示(而非恢复死帧)。 */
static long sim_exit_tap_ms = 0;
static int sim_exit_tap_done = 0;
/* M2.53:MeowOS 主屏接管时把亮度强制压回用户设置值(display.brightness,经
* meow_settings_brightness_clamped() —— 下限 brightness_min 默认 1、上限 100),
* 覆盖 HibyOS 播放器写入 sysfs 的背光下限(16/25)。仅应在「MeowOS 真正接管画面」
* (播放器已死、主屏常驻,即 run_menu 的 had_player==0)时调用;播放器会话期间的
* 覆盖菜单(had_player==1)不去抢亮度——尊重播放器背光(§红线)。 */
static void apply_user_brightness(void)
{
int v = meow_settings_brightness_clamped();
if (v <= 0) v = 60; /* 防御:clamp 后不可能 ≤0,仅兜底 */
write_brightness(v);
g_sys.brightness = v;
logmsg("apply user brightness (MeowOS takeover) -> %d", v);
}
/* real menu loop */
static void run_menu(struct app apps[MAX_APPS], int napps, int state,
long sim_tap_ms, long sim_exit_ms)
{
int cursor = 0;
unsigned long long t0 = now_ms();
int toast_ms = 0;
char toast[64] = "";
int want_exit = 0;
/* M2.17 超时自动息屏:任何输入重置 last_input;空闲 > screen_off_timeout → meow_panel_off()。
* meow_auto_slept 记录「自动息屏」态(手动电源息屏不置位),任意输入唤醒 + meow_repaint_surface。 */
unsigned long long last_input_ms = now_ms();
int any_input = 0;
int fbfd = -1;
int had_player = 0; /* M2.2:菜单打开时是否存在存活播放器 */
long sim_tap_done = 0, sim_exit_done = 0;
long meow_last_poll = 0; /* M2.2:MeowOS 后台拔营(卡/网)轮询时间戳 */
load_config(); /* meow.conf on the card: bg_color etc. */
/* A-1/A-2:launcher 用 SDK 渲染栈 —— 初始化 SDK(自建 back_buf/读几何/载字库),
* 并把刷新率档位/恒满速开关交给 SDK present 帧率门控(去中心化:launcher 不再自带一套)。
* set_statusbar(0):launcher 用自己的状态栏(draw_statusbar),关闭 SDK 全局状态栏避免
* 同区双画(SDK 全局状态栏/左缘返回对 APP 生效;launcher 系统界面暂保留自有状态栏)。 */
meow_ui_init(NULL);
meow_ui_set_statusbar(0);
meow_ui_set_refresh_rate(refresh_rate_cfg);
meow_ui_set_refresh_always_max(refresh_always_max);
/* record the brightness BEFORE we touch it, so any menu exit can
* restore the player's pre-menu value (0 = player standby -> default 60) */
int raw_brightness = read_brightness();
saved_brightness = (raw_brightness > 0) ? raw_brightness : 60;
logmsg("menu saved brightness = %d", saved_brightness);
/* remember whether the player had blanked the panel: fb_open() below
* unblanks so the menu is visible, and restore_backlight() re-blanks
* on exit to keep the player's display state machine consistent */
menu_opened_blank = (raw_brightness <= 0);
fbfd = fb_open();
meow_font_bind_fb(); /* M2.6 字库绑定当前 fb 几何(宽/高/pitch) */
/* M2.6 加载字库(卡上 MeowOS/fonts/default/,卡片已挂载则该步成功;未挂载由下方轮询补载) */
meow_font_ensure();
meow_font_reload_multifont(); /* M2.5x 多字体(读 settings display.multi_font + 6 类;关=单字体) */
/* M2.2:先判播放器是否存活。MeowOS 常驻主界面(播放器已死)→ 不 SIGSTOP、不备份
* 播放器帧,全程只自绘 fb0(buffer0 + pan0);仅当存在存活播放器(会话覆盖场景)才
* 冻结 + 备份,退出时恢复其帧。 */
had_player = (find_hiby_pid() > 0);
meow_mode = had_player ? 0 : 1; /* 权威判定:每次按播放器是否存活重置 */
/* M2.11 A:每次进 MeowOS(热键切换/开机选 MeowOS/覆盖菜单)都全量重抓系统状态快照,
* HibyOS 里改过(蓝牙/WiFi/音量/亮度等)也同步。主循环 10ms 绝不读系统态。 */
meow_sys_refresh();
g_sys.mode = meow_mode;
g_sys.napps = napps;
if (had_player) {
stop_player(); /* freeze the player so the menu owns fb0 */
} else {
logmsg("meow mode: hiby_player dead, self-drawn home (no stop/backup)");
}
ensure_backlight(); /* only if the panel is dark, never yank brightness */
unblank_panel(); /* player frozen: it can no longer re-blank */
/* M2.53:MeowOS 主屏接管(播放器已死,had_player==0)时强制把亮度压回用户
* display.brightness 设定值。HibyOS 播放器运行/恢复画面会把它自己的背光下限
* (16/25)写进 sysfs;若以它为「当前亮度」,用户设的低亮度(<25)会被覆盖。
* 覆盖菜单(had_player==1,播放器会话中)不压回——播电影时亮度归播放器管。 */
if (!had_player) apply_user_brightness();
if (had_player) fb_backup_player(); /* only snapshot when a live player is overlaid */
draw_menu(apps, napps, state, 0, 0, ""); /* full menu into back_buf */
fb_norm_buf0(); /* ensure LCDC shows buffer0 (yoffset=0) */
meow_dirty_full(); meow_ui_flush(); /* A-1/A-2:SDK present(vsync + 单次全帧 blit 到 fb0) */
/* touch: event reads (event1 is not grabbed) */
touch_fd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
if (touch_fd < 0) logmsg("touch open failed: %s", strerror(errno));
/* M2.6 幽灵触摸防护:菜单激活(启动)即 drain + 冷却 */
menu_arm_touch(TAP_ARM_DELAY_MS);
/* key polling */
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER);
kp_open(&kp_keys, DEV_KEYS);
/* M2.16 按键防抖状态(每键独立);kd_tick 40ms 稳定后才认,抗 ADC 抖动 */
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
logmsg("menu open: napps=%d state=%d cursor=%d",
napps, state, cursor);
/* key edge baselines: sample the initial state so keys already held
* when the menu opens (e.g. the hotkey release tail) do not fire
* edges; kp.prev cannot be used for this because kp_poll() syncs it
* to cur before we compare */
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
}
/* M2.3A 电源按住计时:≥8s 强制关机;≥3s 弹电源菜单;与单击息屏/热键按时长区分 */
unsigned long long pwr_hold_at = 0;
int pwr_menu_state = 0;
/* M2.18 Bug 2: 一次性标志——本次电源按下已触发「自动息屏唤醒」时置位,其释放
* 只唤醒不 toggle(防同一次按键被唤醒路径与单单击 toggle 各消费一次 → 闪黑)。 */
int pwr_woke = 0;
/* M2.34: 一次性标志——电源菜单刚关闭(无论确认/取消/超时)。返回主循环后,本次
* 电源按下-松开不得被 3130「松开→单击」路径误判为「电源单击」→ toggle 息屏,否则:
* ①菜单点关机/重启:关机/重启画面被息屏覆盖;②焦点在取消键单击电源:取消同时熄屏。
* 清除时机:电源松开(3130 松开分支末尾)或下一次电源按下边沿。 */
int pwr_suppress_click = 0;
/* The menu was already drawn into buffer0 above (pre-drawn before the pan
* to buffer0), so a redraw is only needed when something actually changes:
* cursor movement (input), app exit/enter, toast start/end, or a future
* notification-badge change. NEVER on a periodic timer — see the big
* comment on the redraw block below. */
int dirty = 0; /* 结构变化(app 进出/state/唤醒/热键…)→ 全帧重画 */
int need_menu_paint = 0; /* M2.16:需 draw_menu 全画 back_buf(光标/toast/test 变化) */
int last_cursor = 0; /* pre-drawn frame showed cursor 0 */
int toast_active = 0; /* tracks toast_ms>0 for start/end redraw edges */
long last_status_tick = 0; /* ~1s status-bar-only refresh (clock) */
for (;;) {
unsigned long long t = now_ms();
long el = (long)(t - t0);
any_input = 0; /* M2.17:本圈是否有输入(重置 auto screen-off 计时) */
/* SIGTERM during the menu: close it cleanly (restore + resume happen
* in the hotkey block / main loop after we return) */
if (sigterm_flag) { logmsg("menu: sigterm, closing"); want_exit = 1; break; }
/* 阶段 D · M0:菜单/主屏循环内排空 IPC(非阻塞;应用可随时查询/声明路由) */
meow_ipc_process();
meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/
/* sim hooks */
if (sim_tap_ms > 0 && !sim_tap_done && el >= sim_tap_ms) {
logmsg("SIM TAP row 0");
sim_tap_done = 1;
if (napps > 0 && state == 0) {
/* fall through: select row 0 */
int ret = launch_app(&apps[0]);
/* app exited: RETURN TO THE MENU (v6.3) — closing the menu
* here would leave the app's last frame (e.g. BYE) on fb0,
* and the player only redraws on input events (none arrive,
* injection is not delivered), so the screen would stay
* dark until a key press. Staying in the menu keeps the
* menu visible; BACK TO PLAYER then resumes the player.
* Bug 2/3/4: a queued touch from the app run must NOT be
* replayed as a fresh menu tap (relaunch / flash-to-player),
* and the "CANNOT START" toast only fires on a real launch
* failure (ret != 0), never after a clean app exit. */
menu_after_app_exit(ret == 0);
last_input_ms = now_ms(); /* M2.17:回主界面即视为输入,重置自动息屏计时 */
dirty = 1; /* app wrote buffer0 during its run -> redraw menu */
if (ret != 0) {
snprintf(toast, sizeof toast, "CANNOT START: %s", apps[0].name);
toast_ms = TOAST_MS;
}
} else {
snprintf(toast, sizeof toast, "NO APPS");
toast_ms = TOAST_MS;
}
}
if (sim_exit_ms > 0 && !sim_exit_done && el >= sim_exit_ms) {
logmsg("SIM EXIT");
sim_exit_done = 1;
want_exit = 1;
break;
}
/* M2.6 sim:点 BACK TO PLAYER 行 → MeowOS 模式弹「需重启」确认框并留下;
* 会话覆盖模式则正常退出。 */
if (sim_exit_tap_ms > 0 && !sim_exit_tap_done && el >= sim_exit_tap_ms) {
logmsg("SIM TAP exit row");
sim_exit_tap_done = 1;
if (meow_mode) {
int rb = confirm_reboot_player();
menu_arm_touch(TAP_ARM_DELAY_MS);
dirty = 1;
logmsg("meow mode: BACK TO PLAYER -> confirm reboot (rb=%d)", rb);
} else {
want_exit = 1;
break;
}
}
/* M2.2 MeowOS 系统拔营:无播放器时由 launcher 接管官方交给
* hiby_player/sys_server 的两件事 —— TF 卡挂载 + WiFi 联网。
* 后台每 2s 重试,不阻塞主界面;卡片就绪后重扫应用列表并就地刷新。
* 仅 meow 常驻模式需要(HibyOS 覆盖模式播放器仍在,/或其管理)。
* M2.11 C:TF 卡热插拔最佳努力 —— 拔出检测(节点消失→无卡)、
* 插入检测(节点出现→自动挂载+重扫+重载字库)。不阻塞、不崩。 */
if (meow_mode && (long)(t - t0) - meow_last_poll >= 2000) {
meow_last_poll = (long)(t - t0);
ensure_wifi();
if (!sd_device_present()) {
/* 卡被拔出:设备节点消失 → 无卡态。停止扫描;留 state=1(NO STORAGE CARD)。
* 若此前是正常态,切到无卡并重绘。不杀正在跑的应用(其由 D 的监测处理)。 */
if (state == 0 || state == 2) { state = 1; dirty = 1; logmsg("hotplug: card removed -> state=1"); }
g_sys.sd_mounted = 0;
log_poll_card(); /* 卡日志不可用时回退 /tmp 缓冲 */
} else if (ensure_card_mounted()) {
/* 卡在且已挂载(刚插入/重挂成功):补字库 + 重扫 · 仅真正变化才重绘 */
int was_nocard = (state == 1);
if (!mfont_ready && meow_font_ensure()) { dirty = 1; logmsg("hotplug: font loaded late"); }
if (mfont_ready) meow_font_reload_multifont(); /* M2.5x:卡挂载后重配多字体(此前无全局可配) */
if (was_nocard || state != 0 || napps == 0) {
struct app fresh[MAX_APPS];
int fn = scan_apps(fresh);
int fstate = (fn < 0) ? (fn == -1 ? 1 : 2) : (fn == 0 ? 2 : 0);
if (fstate == 0 && fn > 0) {
for (int i = 0; i < fn; i++) apps[i] = fresh[i];
napps = fn; state = 0;
logmsg("hotplug: card ready -> %d apps loaded", napps);
dirty = 1;
} else if (fstate != state) {
state = fstate;
logmsg("hotplug: state -> %d", state);
dirty = 1;
}
}
g_sys.sd_mounted = 1;
log_poll_card();
} else if (!sd_mounted()) {
/* 卡在但未挂载(正挂载/失败):保持当前显示不误判;不崩。 */
g_sys.sd_mounted = 0;
}
}
/* keys: poll and handle edges. Power is NOT handled here anymore
* (v6.5): inside the menu the power key is a pure screen-off/on
* toggle handled by sys_server/kernel (the player being SIGSTOPped
* does not affect it) — the launcher must NOT touch brightness/panel
* or exit the menu on it. Exit is only via the BACK TO PLAYER touch
* row (below) or sim-exit. */
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
int vold = 0, volu = 0, play = 0, pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
}
if (sim_power_hold_ms > 0 && el >= sim_power_hold_ms) pwr = 1; /* sim:按住电源 */
/* 原始(未防抖)键值:长按连续(vol_ramp held)用,避免防抖延迟让单击仍连爬多档。 */
int rvold = vold, rvolu = volu, rplay = play, rpwr = pwr; /* 原始值:长按连续用 rvolu/rvold */
(void)rplay; (void)rpwr; /* play/pwr 只用稳定边沿,不需原始 held */
/* M2.16 防抖:每键值须稳定 KEY_DEBOUNCE_MS(40ms) 才认可,抗 ADC 抖动("点一下跳5-6下")。
* 稳定态 vold/volu/play/pwr 与 m_prev_*(上一稳定值)比较得稳定边沿;长按(连续)用 .down。 */
(void)kd_tick(&kd_vold, vold);
(void)kd_tick(&kd_volu, volu);
(void)kd_tick(&kd_play, play);
(void)kd_tick(&kd_pwr, pwr);
vold = kd_vold.stable; volu = kd_volu.stable;
play = kd_play.stable; pwr = kd_pwr.stable;
/* M2.6 电源语义(MeowOS 自持面板,无播放器拔河):
* 单击(短按松开 <2s)= 息屏/亮屏;2s = 电源菜单;8s = 强制关机。 */
if (meow_mode) {
if (pwr && !m_prev_pwr) {
pwr_hold_at = t; pwr_menu_state = 0; any_input = 1;
pwr_suppress_click = 0; /* M2.34:新一次电源按下清除旧的抑制标志 */
/* M2.18 Bug 2: 若这次按下正好落在「自动息屏」态(auto_slept 且屏已关),
* 本次电源键只做唤醒(下方通用醒屏分支 meow_panel_on()),其释放不再走
* 单单击 toggle —— 否则同一次按下先被唤醒点亮、又在释放时被 toggle 关掉
* (症状:自动息屏后点电源「闪一下黑、再点才亮」)。 */
if (meow_auto_slept && !meow_screen_on) pwr_woke = 1;
}
if (pwr && pwr_hold_at) {
/* M2.10/M2.11 B:8s 强关只在该「按住」从未打开过电源菜单时才触发
* (!pwr_menu_state)。一旦打开过菜单(用户已互动/取消),run_menu 的
* 8s 强关永远不在此 hold 上再触发 —— 由菜单自身的「按当前项确认」/取消
* 决定动作。避免「按住电源开菜单->确认取消->仍按住->run_menu 8s 强关」残留。
* M2.37:阈值接 power.forceoff_ms(默认 8000),强关保持 > 弹菜单上限。 */
if (t - pwr_hold_at >= power_forceoff_ms && !pwr_menu_state) {
if (sim_no_exec) { logmsg("pm: [sim-no-exec] POWER HOLD %dms -> would force poweroff", power_forceoff_ms); }
else { logmsg("pm: POWER HOLD %dms -> force poweroff", power_forceoff_ms); sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); }
m_prev_pwr = pwr; continue;
} else if (t - pwr_hold_at >= power_longpress_menu_ms && !pwr_menu_state) {
pwr_menu_state = 1;
logmsg("pm: POWER HOLD %dms -> show power menu", power_longpress_menu_ms);
show_power_menu(); /* 动作在函数内 system(); 取消则续循环 */
/* M2.31:菜单打开期间主循环阻塞,last_input_ms 不会刷新;若停留超过自动息屏
* 超时,取消返回后会被误判「空闲」而立即熄屏(用户点取消瞬间黑屏)。
* 菜单返回即视为「刚有输入」,重置自动息屏计时。 */
last_input_ms = now_ms();
/* M2.11 B:菜单已关闭(取消)。清空按住计时与菜单态:仍按住/随后松开的电源
* 按「新按下」处理,绝不把已互动的 hold 再累计到 8s 强关。 */
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击熄屏 */
dirty = 1; /* 取消后重绘应用列表 */
menu_arm_touch(800); /* M2.6 幽灵触摸防护:菜单关闭后忽略触摸 */
/* M2.25 黑屏修复:阻塞式 show_power_menu() 期间 kd_* 防抖状态冻结。
* 用户常「松开电源 → 用触摸点取消」,返回后 kd_pwr.stable 仍是旧的「按住=1」,
* 而 m_prev_pwr 已按 raw 重播为 0 → 二者错配误判「新按下」刷新 pwr_hold_at,
* 随后稳定值转 0 时又被误判「单击」→ meow_toggle_panel 把面板关掉(黑屏且
* 难唤醒:launcher 以为屏开着,电源唤醒逻辑不触发)。电源键确认取消时 raw
* 仍=按住=1,无此错配,所以只有触摸路径黑屏。此处重播 kd_* 并以当前 raw
* 对齐 m_prev_*,杜绝该假边沿(真实新按键仍正常防抖/单击)。 */
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
continue;
}
}
if (!pwr && m_prev_pwr) {
/* 松开:短按(<3s 且菜单未开)→ 单击 = 息屏/亮屏切换 */
if (pwr_suppress_click) {
/* M2.34:本次电源按下来自「电源菜单内确认/取消」,其松开不得 toggle 息屏
* (否则关机/重启画面被息屏覆盖,或焦点在取消键单击电源导致熄屏)。 */
logmsg("pm: power release after power-menu -> no toggle (suppress)");
} else if (pwr_hold_at && (t - pwr_hold_at < power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) {
logmsg("pm: power single-click -> toggle panel (was %s)", meow_screen_on ? "on" : "off");
meow_toggle_panel();
if (meow_screen_on) {
/* M2.8 Bug 3-residual:唤醒不是 draw 事件,LCDC 在 FBIOBLANK(4)->(0)
* 后指向的内容可能已失效/为空;必须把当前界面(应用列表)整体重绘到
* buffer0 并强制 re-pan,而非仅翻页/仅 dirty 重绘(M2.7 仍黑屏根因)。 */
meow_auto_slept = 0; /* M2.17:手动/自动息屏后电源单击唤醒均清自动态 */
last_input_ms = now_ms(); /* M2.18 Bug 1:用真实墙钟(防 t 过期回绕) */
meow_repaint_surface(apps, napps, state, cursor);
} else {
dirty = 1; /* 息屏:面板已黑,留 dirty 供下次亮屏重绘 */
last_input_ms = now_ms(); /* M2.18 Bug 1:用真实墙钟 */
}
} else if (pwr_woke) {
/* M2.18 Bug 2:本次电源按下已由自动息屏唤醒分支消费,释放只记录、不 toggle。 */
logmsg("pm: power release after auto screen-off wake -> no toggle (wake only)");
}
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
pwr_suppress_click = 0; /* M2.34:电源已松开,抑制标志一次性用完即清 */
}
m_prev_pwr = pwr;
}
/* volume/play edges against the launcher-maintained baselines
* (previously dead code: prev was re-seeded from cur each pass, so
* no edge could ever fire and the menu keys could not navigate or
* launch).
* M2.11 E:MeowOS 下音量键 = 调系统软件音量(settings audio.volume,步进5 clamp),
* 状态栏即时显示、持久化。有播放器的覆盖态才保留音量键=菜单光标导航。 */
if (meow_mode) {
if (vold && !m_prev_vold) {
any_input = 1;
settings_volume_adj(-settings_volume_step); /* 起线性渐变(1% 爬) */
logmsg("volume DOWN <- ramp step=%d", settings_volume_step);
} else if (volu && !m_prev_volu) {
any_input = 1;
settings_volume_adj(+settings_volume_step);
logmsg("volume UP <- ramp step=%d", settings_volume_step);
}
} else {
/* M2.36 B2b:光标范围扩展为 [0..napps](napps=「系统设置」行,位于 Exit 行上方)。
* 音量下移越过最后应用 → 设置行;设置行再下移 → 回 0。应用行仍 0..napps-1。 */
if (vold && !m_prev_vold && napps > 0 && state == 0) {
any_input = 1;
cursor++;
if (cursor > napps) cursor = 0; /* napps=设置行 */
logmsg("menu cursor -> %d", cursor);
}
if (volu && !m_prev_volu && napps > 0 && state == 0) {
any_input = 1;
cursor--;
if (cursor < 0) cursor = napps; /* 回绕到设置行 */
logmsg("menu cursor -> %d", cursor);
}
}
/* M2.14 线性渐变:每 tick 推进 1% 并刷新状态栏(显示爬升中的值或最终值) */
if (meow_mode && vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0) ? rvolu : rvold; /* 长按=该方向键仍按住(用原始值:松开立即停止) */
int going = vol_ramp_tick(held);
g_sys.vol = settings_volume_get();
paint_sb_right(); /* M2.16:只重画右侧信息格(VOL 变化),不动列表/时钟 */
if (!going) logmsg("volume ramp end -> %d", g_sys.vol);
}
/* M2.19:播放键不再是「应用列表的确认/打开」——它让位给全局媒体控制
* (播放/暂停,路由到当前活跃媒体会话)。菜单/列表的确认只走触摸点选
* (见下方 touch 段),物理确认交给电源键(power 菜单/确认框),播放键
* 在 MeowOS 主循环里不再触发 launch_app。媒体预留:后续媒体会话接管,
* 此处仅作 any_input(any_input 仍用于重置自动息屏计时)。(原播放键=打开
* 已删;如需物理启动应用,可后续加「长按播放键」,本轮不做。) */
if (play && !m_prev_play) {
any_input = 1; /* 仍视为输入(重置息屏计时);不再 launch_app */
logmsg("play key: media-reserved (no menu confirm)");
}
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play;
/* touch (event reads) */
if (touch_fd >= 0 && meow_screen_on) { /* 息屏时不处理触摸(goodix 已被 FBIOBLANK(4) 挂起) */
struct pollfd pfd = { touch_fd, POLLIN, 0 };
if (poll(&pfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(touch_fd, ev, sizeof ev) == 16) {
uint16_t type = *(uint16_t *)(ev + 8);
uint16_t code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) t_last_x = val;
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) t_last_y = val;
/* M2.23 原点锁定:goodix 事件时序不保证 ABS 与 BTN_TOUCH 先后,
* 按下边沿只标 t_down,原点延迟到「本次按下之后」的首个有效坐标才锁定,
* 杜绝把上一手势残留(-1/旧坐标)当本次手势起点。 */
if (t_down && !t_down_valid && t_last_x >= 0 && t_last_y >= 0) {
t_down_x = t_last_x; t_down_y = t_last_y; t_down_valid = 1;
}
/* ⑨ 顶部热区「跟手下拉」打开通知中心(拖动中即进,真跟手):
* 原点锁定 + 起点在顶部热区(y<SB_BAR_H) + 下拉达 NCP_DRAG_ENTER_SLOP →
* 立即切入 run_notif_center(此时手指仍按下拖动),由面板内拖动路径把 progress
* 映射为手指位移(跟手 1:1),松手由面板内锚定判定补到展开/收回(半程可回)。
* 原「抬手边沿 mnp_top_swipe_down」打开改为「拖动中即开」(该抬手边沿保留为防御)。
* 先在本次 ABS 内触发一次(menu_live_open_pending latch);
* 进入前 t_down=0 使返回后同次手势的剩余事件被忽略(防 double-open/残留)。 */
if (t_down && t_down_valid && !menu_live_open_pending &&
t_down_y >= 0 && t_down_y < SB_BAR_H &&
(t_last_y - t_down_y) >= NCP_DRAG_ENTER_SLOP) {
menu_live_open_pending = 1; /* 本次手势仅触发一次 */
int loy = t_down_y;
logmsg("menu: top-edge live drag-down -> notification center (dy=%d)",
t_last_y - loy);
t_down = 0; t_down_valid = 0; /* 消费本次手势:返回后不再判 tap/滑动/重开 */
menu_arm_touch(TAP_ARM_DELAY_MS);
int want_launch = -1;
run_notif_center(apps, napps, &want_launch, 1, t_down_x, loy);
/* 重播按键防抖基线(同原 top-swipe 路径),防面板内电源/音量边沿被 run_menu 误判 */
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
last_input_ms = now_ms();
menu_arm_touch(800);
dirty = 1;
if (want_launch >= 0 && want_launch < napps) {
logmsg("menu: notification launch app idx=%d", want_launch);
int ret = launch_app(&apps[want_launch]);
menu_after_app_exit(ret == 0);
last_input_ms = now_ms();
dirty = 1;
if (ret != 0) {
snprintf(toast, sizeof toast, "CANNOT START: %s",
apps[want_launch].name);
toast_ms = TOAST_MS;
}
}
continue; /* 本圈不再按 tap/其余处理(剩余事件因 t_down=0 成 no-op) */
}
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val && !t_down) {
/* Bug 2/4: ignore touches inside the post-app
* cooldown (goodix ghost + back-tap replay). */
if (now_ms() < menu_touch_cooldown_until) {
/* M2.18 排查乱跳:记录被冷却丢弃的按下(坐标 + 剩余冷却),
* 便于定位「概率进 Hello」是否 ghost/重放越过冷却。 */
logmsg("menu touch dropped by cooldown (at %d,%d, rem %llu ms)",
t_last_x, t_last_y,
menu_touch_cooldown_until - now_ms());
} else {
any_input = 1; /* M2.17:触摸重置自动息屏计时 */
t_down = 1;
t_down_valid = 0; /* 原点待首个有效坐标锁定(防残留 -1/旧值) */
t_last_x = -1; t_last_y = -1; /* 本次以「无坐标」起步(防跨手势残留) */
menu_live_open_pending = 0; /* ⑨ 新触摸:复位 live-open latch */
}
}
else if (!val && t_down) {
t_down = 0;
if (!t_down_valid) {
/* 本次按下从未锁到有效坐标(坐标滞后/ghost 空按下):丢本次。 */
logmsg("menu tap dropped: no valid origin (last %d,%d)",
t_last_x, t_last_y);
t_down_x = t_down_y = -1;
} else {
int tx = t_down_x, ty = t_down_y;
int dx = t_last_x - tx, dy = t_last_y - ty;
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
t_down_valid = 0;
logmsg("menu tap at %d,%d (up %d,%d, d=(%d,%d))", tx, ty,
t_last_x, t_last_y, dx, dy);
/* 阶段B:顶部热区下滑打开通知中心。判定在「滑动」分支内先于左缘返回——
* 左缘(down_x<MEOW_EDGE_W)起点交给 meow_edge_back 优先消费;
* 本函数只在顶部热区(down_y<SB_BAR_H)+向下主导时返回 1,与左缘互斥。 */
if (mnp_top_swipe_down(tx, ty, dx, dy, SB_BAR_H, MENU_SWIPE_THRESH)) {
logmsg("menu: top-edge swipe-down -> notification center (release edge, fallback)");
menu_arm_touch(TAP_ARM_DELAY_MS);
int want_launch = -1;
run_notif_center(apps, napps, &want_launch, 0, 0, 0); /* ⑨ live_open=0 → 触发式自动展开(防御) */
/* 重播按键防抖基线,防面板内电源/音量边沿被 run_menu 误判(M2.25 同款)。 */
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
last_input_ms = now_ms();
menu_arm_touch(800);
dirty = 1;
/* 点击通知命中可启动 app → 走标准启动链路(与触摸行同路径)。 */
if (want_launch >= 0 && want_launch < napps) {
logmsg("menu: notification launch app idx=%d", want_launch);
int ret = launch_app(&apps[want_launch]);
menu_after_app_exit(ret == 0);
last_input_ms = now_ms();
dirty = 1;
if (ret != 0) {
snprintf(toast, sizeof toast, "CANNOT START: %s",
apps[want_launch].name);
toast_ms = TOAST_MS;
}
}
continue; /* 本圈不再按 tap/其余处理 */
}
/* M2.23 滑动不启动:抬手位移 >= 阈值 → 判为滑动,不 launch、
* 不进行行判定(为将来左右滑切屏预留;本轮只判滑动、不打事件)。 */
if (adx >= MENU_SWIPE_THRESH || ady >= MENU_SWIPE_THRESH) {
logmsg("menu swipe (dx=%d,dy=%d) — no launch", dx, dy);
} else if (tx >= 0 && ty >= 0) {
if (ty >= EXIT_ROW - 4 && ty < EXIT_ROW + ROW_H - 8) {
logmsg("menu: touch exit row");
if (meow_mode) {
/* M2.6 G:播放器已死,弹出「返回播放器需重启」确认框。
* 是=写选单默认HibyOS + reboot;否=留在 MeowOS。
* 绝不恢复到播放器死帧、绝不对死进程 SIGCONT。 */
logmsg("meow mode: BACK TO PLAYER -> confirm reboot");
int rb = confirm_reboot_player();
menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护 */
dirty = 1;
if (rb) { logmsg("reboot-to-player: yes, rebooting"); }
/* rb==1 时 reboot 已发起,通常不回;rb==0 留在菜单 */
} else {
want_exit = 1;
break;
}
}
/* M2.36 B2b:系统设置行(位于 Exit 行上方,y ∈ [588-4, 588+52-8))。
* 命中 → 进设置页。阻塞式 run_settings_page 返回后,重播 kd_* 并以
* 当前 raw 对齐 m_prev_*(M2.25 黑屏修复同款),杜绝「设置页内按电源
* 返回」的抬起边沿在 run_menu 被误判为面板单击而熄屏。 */
else if (ty >= EXIT_ROW - ROW_H - 4 && ty < EXIT_ROW - ROW_H + ROW_H - 8) {
logmsg("menu: touch settings row -> settings page");
menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护:吃掉打开此行的那次触摸 */
run_settings_page();
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
last_input_ms = now_ms(); /* 回主界面即视为输入 */
menu_arm_touch(800); /* 设置页返回后再忽略近期触摸 */
dirty = 1; /* 全帧重画应用列表 */
}
/* M2.18 排查乱跳:改为 else if —— 原为两个独立 if,退出行与
* 应用行动作非互斥。应用很多(napps*ROW_H 上探到 EXIT 带)时,
* 一个 tap 会同时命中退出行(<~684)与应用行带,触发确认重启又启动
* 应用(或直接 break 走人)→ 概率乱跳。退出行优先,命中即不再
* 落到应用行。普通小列表两区间不重叠,行为不变。 */
else if (ty >= ROW_TOP && ty < ROW_TOP + napps * ROW_H && napps > 0 && state == 0) {
int idx = (ty - ROW_TOP) / ROW_H;
if (idx < napps) {
logmsg("menu: touch row %d", idx);
int ret = launch_app(&apps[idx]);
/* app exited: return to menu (v6.3).
* menu_after_app_exit() drains queued
* app-run touches so they are not
* replayed as row/tap gestures. */
menu_after_app_exit(ret == 0);
last_input_ms = now_ms(); /* M2.17:回主界面即视为输入,重置自动息屏计时 */
dirty = 1; /* app wrote buffer0 -> redraw menu */
if (ret != 0) {
snprintf(toast, sizeof toast, "CANNOT START: %s", apps[idx].name);
toast_ms = TOAST_MS;
}
}
}
}
t_down_x = t_down_y = -1; /* 复位,下一手势从零开始 */
}
}
}
}
if (want_exit) break;
}
}
/* ---- M2.17 超时自动息屏(主界面;主循环 10ms 低频检查,不阻塞)----
* 任何输入重置 last_input;空闲 > screen_off_timeout(0=常亮)且未在 keep_awake 页 → 息屏。
* 自动息屏后任意输入唤醒(电源单击路径已在 meow_mode 块处理;这里处理其他输入/触摸)。
* black 模式:面板仍上电、触摸未挂起,故在屏下额外探测一次 BTN_TOUCH 按下 → 触发唤醒
* (不处理点击;唤醒后下一圈正常触摸处理)。panel 模式触摸已被 FBIOBLANK(4) 挂起,仅按键唤醒。 */
if (meow_auto_slept && !meow_screen_on && screen_off_mode == 1 && touch_fd >= 0) {
struct pollfd spfd = { touch_fd, POLLIN, 0 };
if (poll(&spfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(touch_fd, ev, sizeof ev) == 16) {
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_KEY && code == BTN_TOUCH && val) any_input = 1;
}
}
}
/* M2.18 Bug 1: 用 now_ms() 而非本圈顶部捕获的 t —— launch_app() 阻塞期间 t 已过期
* (停在 app 启动前小数),若仍写 t 会把 last_input 倒拨到 app 启动时刻;写 now_ms()
* 才是「输入发生」的真实墙钟。配合下方 idle_ms_since 的回绕保护,杜绝误判空闲。 */
if (any_input) last_input_ms = now_ms();
if (meow_auto_slept && any_input && !meow_screen_on) {
logmsg("auto screen-off: input wake (meow_mode=%d keep_awake=%d)", meow_mode, fg_keep_awake());
meow_panel_on();
meow_auto_slept = 0;
last_input_ms = now_ms();
meow_repaint_surface(apps, napps, state, cursor); /* M2.8 唤醒重绘 */
}
if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept && !fg_keep_awake()) {
/* M2.18 Bug 1: 用 idle_ms_since 取差,杜绝 t<last_input 无符号回绕误判「空闲30s」。 */
if (idle_ms_since(t, last_input_ms) >= (unsigned long long)screen_off_timeout * 1000) {
meow_panel_off();
meow_auto_slept = 1;
logmsg("auto screen-off after %ds idle (mode=%s keep_awake=%d, t=%llu last=%llu)",
screen_off_timeout, screen_off_mode ? "black" : "panel",
fg_keep_awake(), t, last_input_ms);
}
}
/* Redraw only when something actually changed: cursor movement (an
* input event), app exit/enter (which rewrote buffer0), toast
* start/end, or a future notification-badge change. NEVER on a
* periodic timer.
*
* v6.8 BUG (the ~3fps list flicker): the v6.8 "500ms safety redraw"
* rewrote the ENTIRE menu into the live-displayed buffer0 every 500ms
* (the v6.8 fix made last_redraw=el so the timer finally fired), plus
* draw_statusbar()'s amixer popen on every redraw. An in-place full
* rewrite of the buffer the LCDC is scanning produces visible tearing/
* pulsing on the app list, and the slow amixer popen stalls the loop.
* Full-redraw in place is only correct right after a REAL change; the
* steady-state list must not be touched. */
/* M2.16:只在真变化时标脏(精确到格子),绝无"动一下就全屏重绘"。
* 光标移动 → 旧/新两行;toast 起止 → toast 区;其余结构变化 → dirty=1(全帧)。
* 音量渐变/时钟只经 paint_sb_right/draw_clock 标右侧格/时钟格。 */
if (cursor != last_cursor) {
/* M2.36 B2b:光标可能在应用行(0..napps-1)或「系统设置」行(napps,位于 Exit 行上方)。
* 分别标脏对应行 rect(设置行固定 y=EXIT_ROW-ROW_H,与 napps 无关)。 */
if (cursor == napps || last_cursor == napps)
meow_dirty_add(8, EXIT_ROW - ROW_H - 4, fb_xres - 16, ROW_H - 8);
if (state == 0 && napps > 0) {
if (last_cursor >= 0 && last_cursor < napps)
meow_dirty_add(8, ROW_TOP + last_cursor * ROW_H - 4, fb_xres - 16, ROW_H - 8);
if (cursor >= 0 && cursor < napps)
meow_dirty_add(8, ROW_TOP + cursor * ROW_H - 4, fb_xres - 16, ROW_H - 8);
}
last_cursor = cursor;
need_menu_paint = 1;
}
if (toast_active != (toast_ms > 0)) { /* toast start/end */
toast_active = (toast_ms > 0);
meow_dirty_add(20, TOAST_Y - 6, fb_xres - 40, 40);
need_menu_paint = 1;
}
if (dirty) { /* 结构变化 → 全帧重画 + 全屏 blit */
meow_dirty_full();
need_menu_paint = 1;
dirty = 0;
}
if (need_menu_paint) {
draw_menu(apps, napps, state, cursor, toast_ms, toast); /* 全画 SDK back_buf(RAM,快) */
need_menu_paint = 0;
}
/* A-1/A-2:呈现统一走 SDK meow_ui_flush()(内含帧率门控;无脏返回,门控未达期保留脏
* 待下一轮)。fb_norm_buf0 确保 LCDC 显示 buffer0(yoffset 非 0 才 pan,短路无开销)。
* 不再用 launcher 的 g_ndirty/g_dirty_full 做呈现门——SDK present 自会 no-op 于空脏。 */
fb_norm_buf0(); /* Bug 3:确保 LCDC 显示 buffer0 */
meow_ui_flush(); /* M2.16 无闪烁 + M2.16 帧率门控归口 SDK */
if (toast_ms > 0) {
toast_ms -= POLL_MS;
if (toast_ms < 0) toast_ms = 0;
}
/* Clock refresh ~1s: redraw ONLY the clock digits (and the right info
* cell if battery/wifi changed), never the list. */
if (el - last_status_tick >= 1000) {
last_status_tick = el;
meow_sys_light(); /* 同步电量/WiFi/音量进 g_sys */
draw_clock(); /* M2.17:sb_redraw() 重画整条(时钟+电量/WiFi/音量)并标整条脏 */
fb_norm_buf0();
meow_ui_flush(); /* blit 状态栏整条,不动列表 */
}
usleep(POLL_MS * 1000);
}
if (touch_fd >= 0) close(touch_fd);
/* v6.6: restore the player's frame + pan back BEFORE releasing fb (the
* player is event-driven and won't redraw on its own — see §18.15).
* M2.2:仅当存在存活播放器才恢复其帧;MeowOS 常驻主界面(无播放器)绝不
* 恢复死帧——屏幕保持自绘的应用列表(buffer0),不遗留播放器残帧。 */
if (had_player) fb_restore_player();
if (fbfd >= 0) { if (fb_fd == fbfd) fb_fd = -1; close(fbfd); }
logmsg("menu close (exit=%d meow_mode=%d had_player=%d)", want_exit, meow_mode, had_player);
}
/* ================================================================== */
/* app launch */
/* ================================================================== */
static void on_sigterm(int s) { (void)s; sigterm_flag = 1; }
/* Drain pending events from all input devices (non-blocking read to EAGAIN).
* Called right before launching an app so residual events (the tap/key that
* selected the row, the hotkey combo's release tail) are consumed before the
* app opens its own fds. Note: events generated AFTER the drain still reach
* the app (multicast); the app therefore also drains itself at startup and
* only exits on a fresh press edge (see hello.c). */
static void drain_input(void)
{
for (int i = 0; i < 4; i++) {
char dev[32];
snprintf(dev, sizeof dev, "/dev/input/event%d", i);
int fd = open(dev, O_RDONLY | O_NONBLOCK);
if (fd < 0) continue;
unsigned char ev[16];
while (read(fd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ }
close(fd);
}
}
/* ---- backlight save/restore ---- */
static int read_brightness(void)
{
int fd = open(BACKLIGHT, O_RDONLY);
if (fd < 0) return -1;
char buf[16];
int n = read(fd, buf, sizeof buf - 1);
close(fd);
if (n <= 0) return -1;
buf[n] = 0;
return atoi(buf);
}
static void write_file(const char *path, const char *val)
{
int fd = open(path, O_WRONLY);
if (fd < 0) return;
int l = strlen(val);
if (write(fd, val, l) < 0)
logmsg("write %s <- %s: %s", path, val, strerror(errno));
close(fd);
}
static void write_brightness(int val)
{
char buf[8];
int l = snprintf(buf, sizeof buf, "%d\n", val);
int fd = open(BACKLIGHT, O_WRONLY);
if (fd < 0) { logmsg("backlight open fail: %s", strerror(errno)); return; }
if (write(fd, buf, l) < 0) logmsg("backlight write %d: %s", val, strerror(errno));
close(fd);
}
/* ================================================================== */
/* M2.36 阶段 B2b:实体系统设置页(MD3 风格) */
/* - 入口:应用列表「系统设置」builtin 行(draw_menu 中位于 Exit 行上 */
/* 方;触摸命中 run_menu 分发到此 run_settings_page)。 */
/* - 结构:分组列表(显示/电源/开机/输入/字体主题/通知/日志/音频), */
/* 全屏单张 surface_variant 圆角卡片,组标题 on_surface_variant 小字。*/
/* - 控件:开关(SC_SWITCH)/滑条(SC_SLIDER)/选择器(SC_ENUM)/预设路径 */
/* (SC_PRESET)。所有读写仅走 meow_settings_getXXX / setXXX 接口; */
/* meow_settings_save(),绝不直接改文件(SYSTEM_SETTINGS §4)。 */
/* ================================================================== */
/* M2.36 阶段 B2b:实体系统设置页(MD3 风格) */
/* - 入口:应用列表「系统设置」builtin 行(draw_menu 中位于 Exit 行上 */
/* 方;触摸命中 run_menu 分发到此 run_settings_page)。 */
/* - 结构:分组列表(显示/电源/开机/输入/字体主题/通知/日志/音频), */
/* 全屏单张 surface_variant 圆角卡片,组标题 on_surface_variant 小字。*/
/* - 控件:开关(SC_SWITCH)/滑条(SC_SLIDER)/选择器(SC_ENUM)/预设路径 */
/* (SC_PRESET)。所有读写仅走 meow_settings_getXXX / setXXX 接口; */
/* meow_settings_save(),绝不直接改文件(SYSTEM_SETTINGS §4)。 */
/* - 主题:MD3 color roles(dark/light 两套,接 display.theme),集中 */
/* 下表,禁止散落硬编码色值(MD3_UI_SPEC §1)。 */
/* - 交互:音量键=调系统音量 / 电源键=系统级语义(单击=息屏/亮屏、长按=电源菜单、
8s=强关;不再返回)/ 播放键=抓住(不返回)/ 退出仅靠触摸(底部返回 + 左缘左滑)。*/
/* 选中行 tonal 高亮(meow_ui_fill_rect_alpha),滑条拖动实时 set_*。 */
/* 静态展示不主动刷新,仅交互时经 meow_dirty 区标脏 + blit 局部。 */
/* ================================================================== */
/* 主题(MD3 color roles)定义已上移到本文件前方状态栏处(M2.44)。 */
/* ---- 控件类型 ---- */
typedef enum { SC_HEADER, SC_SWITCH, SC_SLIDER, SC_ENUM, SC_PRESET, SC_INFO, SC_LIST } sctrl_t;
/* ---- 一行描述(label+key+控件);key=NULL 表示分组标题行 ---- */
typedef struct {
const char *label;
const char *key;
sctrl_t ctrl;
int min, max, step; /* SC_SLIDER 取值域/步进 */
const char *const *presets; /* SC_PRESET 选项名 */
const char *const *preset_labels; /* SC_PRESET 选项中文标签 */
int preset_n;
int net_list; /* SC_INFO/SC_LIST 网络信息类别(NET_LIST_*),0=无 */
} srow_t;
/* 网络信息类别(SC_LIST 打开下拉/详情;对应 SMENU「网络」组行) */
#define NET_LIST_WIFI_SSID 1 /* WiFi 当前连接(只读信息;点开显示详情) */
#define NET_LIST_WIFI_SAVED 2 /* WiFi 已保存网络(下拉,可 connect) */
#define NET_LIST_WIFI_SCAN 3 /* 附近WiFi:WiFi 扫描结果(按 BSSID 去重,只读展示) */
#define NET_LIST_BT_NEARBY 4 /* 蓝牙附近设备(已配对 + 扫描发现;下拉,只读展示) */
#define NET_LIST_BT_MODE 5 /* 蓝牙当前连接模式(只读信息;对应"蓝牙模式"行,s_net_get 显示) */
#define NET_LIST_ABOUT 6 /* 系统设置「关于本机」:只读设备信息弹窗(复用 s_dd_modal) */
#define S_NET_MAX 8 /* 网络下拉一次最多展示项(屏幕高度限制) */
/* SC_PRESET 预设表(路径/枚举字符串的"预设项,自定义文件选择留后期") */
static const char *const bg_presets[] = { "black", "blue", "green" };
static const char *const bg_preset_labels[] = { "黑色", "深蓝", "深绿" };
#define BG_PRESETS_N 3
static const char *const hk_presets[] = { "power_voldown", "seq" };
static const char *const hk_preset_labels[] = { "电源+音量减", "连按序列" };
#define HK_PRESETS_N 2
static const char *const seq_presets[] = { "volup,volup,volup", "voldown,voldown,voldown" };
static const char *const seq_preset_labels[] = { "音量上×3", "音量下×3" };
#define SEQ_PRESETS_N 2
/* font.file 行(字体文件)不使用编译期静态表:其 presets/preset_labels 指向字体区
* (font_presets/font_preset_labels, 见本文件字体区) 的运行时动态数组,计数由
* s_row_presets() 动态返回(默认 1,进设置页扫描 TF 卡后更新)。见 SMENU 行。 */
static const char *const out_presets[] = { "hw:0,0" };
static const char *const out_preset_labels[] = { "CS43198 (hw:0,0)" };
#define OUT_PRESETS_N 1
/* M2.37:息屏时间滑条改为「离散档位」而非线性 0~86400 秒(线性步进 1 根本调不到 30s,
* 上限 24h 过于抽象)。档位 0=常亮;从 5s 起步的细粒度段可精调「30s 左右」,随后按
* 45s/分/刻/时 跳档,最高 2 时。滑条值 = 档位索引(0..S_TIME_TIERS_N-1,步进 1),
* 落盘仍写「秒」(display.screen_off_timeout);索引<->秒双向映射见 s_time_tier_*。 */
static const int s_time_tiers[] = {
0, /* 常亮 */
5, 10, 15, 20, /* 细粒度段:可精确到 30s 附近 */
30, 45, /* 半分钟/三刻钟 */
60, 90, 120, 180, /* 1 分 / 1.5 分 / 2 分 / 3 分 */
300, 600, 900, /* 5 分 / 10 分 / 15 分 */
1800, /* 30 分 */
3600, 7200 /* 1 时 / 2 时 */
};
#define S_TIME_TIERS_N ((int)(sizeof s_time_tiers / sizeof s_time_tiers[0]))
/* 设置页扁平化菜单(分组=组标题行,其余=设置项)。顺序即显示顺序。 */
/* 分组/键/类型/默认与 SYSTEM_SETTINGS.md §1 一致(新建项四端同步)。 */
static const srow_t SMENU[] = {
/* 显示 */
{ "显示", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
{ "亮度", "display.brightness", SC_SLIDER, 1,100,1, NULL,NULL,0 },
{ "息屏模式", "display.screen_off_mode", SC_ENUM, 0,0,0, NULL,NULL,0 },
{ "息屏时间", "display.screen_off_timeout", SC_SLIDER, 0,(S_TIME_TIERS_N-1),1, NULL,NULL,0 },
{ "刷新率", "display.refresh_rate", SC_ENUM, 0,0,0, NULL,NULL,0 },
{ "最高刷新率", "display.refresh_always_max", SC_SWITCH, 0,0,0, NULL,NULL,0 },
/* B2b 主题三方式:标题行「主题」改选 display.theme_mode(手动/时段自动),
* 手动深浅由下方「主题深浅」(display.theme) 控制;时段两键为分钟滑条(STEP=15)。 */
{ "主题", "display.theme_mode", SC_ENUM, 0,0,0, NULL,NULL,0 },
{ "主题深浅", "display.theme", SC_ENUM, 0,0,0, NULL,NULL,0 },
{ "深色时段开始", "display.theme_dark_start", SC_SLIDER, 0,1439,15, NULL,NULL,0 },
{ "深色时段结束", "display.theme_dark_end", SC_SLIDER, 0,1439,15, NULL,NULL,0 },
{ "背景", "display.background", SC_PRESET, 0,0,0, bg_presets, bg_preset_labels, BG_PRESETS_N },
/* 网络 */
{ "网络", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
{ "WiFi", "network.wifi_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 },
{ "WiFi连接", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_SSID },
{ "已保存网络", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_SAVED },
{ "附近WiFi", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_SCAN },
{ "蓝牙", "network.bluetooth_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 },
{ "蓝牙模式", NULL, SC_INFO, 0,0,0, NULL,NULL,0, NET_LIST_BT_MODE },
{ "蓝牙设备", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_BT_NEARBY },
/* 电源 */
{ "电源", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
{ "长按弹菜单", "power.longpress_menu_ms", SC_SLIDER, 500,5000,500, NULL,NULL,0 },
{ "长按强关", "power.forceoff_ms", SC_SLIDER, 5500,15000,500, NULL,NULL,0 },
{ "退出热键", "power.defeat_hotkey", SC_PRESET, 0,0,0, hk_presets, hk_preset_labels, HK_PRESETS_N },
/* 开机 */
{ "开机", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
{ "默认系统", "boot.default", SC_ENUM, 0,0,0, NULL,NULL,0 },
{ "显示选单", "boot.menu_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 },
{ "选单超时", "boot.timeout_ms", SC_SLIDER, 0,30000,500, NULL,NULL,0 },
/* 输入 */
{ "输入", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
{ "热键序列", "input.hotkey_seq", SC_PRESET, 0,0,0, seq_presets, seq_preset_labels, SEQ_PRESETS_N },
{ "热键窗口", "input.hotkey_window_ms", SC_SLIDER, 0,5000,100, NULL,NULL,0 },
{ "触摸布防", "input.touch_arm_delay_ms", SC_SLIDER, 0,2000,50, NULL,NULL,0 },
{ "滑动方向", "input.swipe_invert", SC_SWITCH, 0,0,0, NULL,NULL,0 },
/* 字体主题 */
{ "字体主题", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
{ "字体文件", "font.file", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
/* M2.5x 字体档位分层/按需大字库:「加载大字库」= font.load_full(int 0/1 默认 0)。
* 开 → 强制全系统用大字库 gbk≈20k 档(覆盖 app 未声明/覆盖默认 font.file);
* 关(默认)→ 小字库 common≈4k,仅 app 声明 fonts:"full" 才大字库。 */
{ "加载大字库", "font.load_full", SC_SWITCH, 0,0,0, NULL,NULL,0 },
/* M2.5x 多字体:开关 + 6 个字符类别字体(preset=可用 MeowOS/fonts/<family>/<weight>/ 列表;
* 值空 = 跟随全局 font.file)。仅开关开启时 6 行可见(s_row_visible)。 */
{ "多字体", "display.multi_font", SC_SWITCH, 0,0,0, NULL,NULL,0 },
{ "中文(CJK)", "font.class_cjk", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
{ "中文标点", "font.class_cjk_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
{ "数字", "font.class_num", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
{ "拉丁字母", "font.class_latin", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
{ "半角标点", "font.class_latin_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
{ "其他字符", "font.class_other", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
{ "标题字号", "font.size.title", SC_SLIDER, 16,40,1, NULL,NULL,0 },
{ "正文字号", "font.size.body", SC_SLIDER, 12,28,1, NULL,NULL,0 },
{ "UI风格", "ui.style", SC_ENUM, 0,0,0, NULL,NULL,0 },
/* 通知 */
{ "通知", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
{ "通知中心", "notify.center_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 },
{ "应用配额", "notify.app_quota", SC_SLIDER, 1,64,1, NULL,NULL,0 },
/* 日志 */
{ "日志", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
{ "日志级别", "log.level", SC_ENUM, 0,0,0, NULL,NULL,0 },
{ "写入TF卡", "log.to_card", SC_SWITCH, 0,0,0, NULL,NULL,0 },
/* 音频 */
{ "音频", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
{ "音量", "audio.volume", SC_SLIDER, 0,100,1, NULL,NULL,0 },
{ "音量上限", "audio.volume_max", SC_SLIDER, 0,100,1, NULL,NULL,0 },
{ "音量跳步", "audio.volume_step", SC_SLIDER, 1,5,1, NULL,NULL,0 },
{ "输出设备", "audio.output", SC_PRESET, 0,0,0, out_presets, out_preset_labels, OUT_PRESETS_N },
/* 关于(设置列表最底;「关于本机」点开 s_about_open 弹只读设备信息框) */
{ "关于", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
{ "关于本机", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_ABOUT },
};
#define SMENU_N ((int)(sizeof SMENU / sizeof SMENU[0]))
/* 设置页布局(单卡片,行高统一便于整行式滚动对位与裁剪)。
* SET_CONTENT_H 取 SROW_H 的整数倍(520=10 行),使 scroll 恒为整行步进,
* 视口内行首行/末行都完整落位,避免部分可见行被画到卡片外(泄漏)。 */
#define SROW_H 58 /* M2.37:行高 52→58,便于触摸点选 */
#define SET_CARD_X 8
#define SET_CARD_W (fb_xres - 16)
#define SET_CARD_Y 92
#define SET_CARD_H 536 /* 卡片底 628;视口高 = 536-16 = 520 = 10×52 */
#define SET_CONTENT_X (SET_CARD_X + 12)
#define SET_CONTENT_W (SET_CARD_W - 24)
#define SET_CONTENT_Y (SET_CARD_Y + 8)
#define SET_CONTENT_H (SET_CARD_H - 16)
#define SET_CONTENT_BOT (SET_CONTENT_Y + SET_CONTENT_H)
#define S_SCROLL_SLOP 40 /* px:触摸纵向位移超此值 → 判为列表拖动滚动(非 tap;tap 判定 ady<60) */
/* M2.39 左缘返回手势已抽为共享函数 meow_edge_back()(meow_touch.h MEOW_EDGE_W/SWIPE 由
* 其定义),设置页运行期调用;此处不再定义独立 S_EDGE_W/S_SWIPE_THRESH。 */
/* 当前选中行若是滑条:记录其 track 纵向位置,供触摸拖动命中判(x/w 为布局常量,
* 所有滑条行相同,可直接算;仅 y 随行变,故独立记录)。 */
static int s_sel_track_y = -1;
/* 滑条 track 布局常量(所有滑条行一致;track 纵向 = 行顶 + 40,由 s_sel_track_y 记录) */
#define S_SLIDER_X (SET_CARD_X + 16)
#define S_SLIDER_W (SET_CONTENT_W)
/* ---- 行高/位置/导航辅助 ---- */
/* M2.5x 多字体:6 个「字体文件」行仅在 display.multi_font=1 时可见(开时显示)。
* 可见性与分组的标题行无关(标题恒显示)。 */
static int s_row_visible(int i)
{
const srow_t *r = &SMENU[i];
if (r->key && !strncmp(r->key, "font.class_", 11)) {
int on = 0; meow_settings_get_switch("display.multi_font", &on);
return on;
}
return 1;
}
/* 可见行数(= 压缩后占用的行数;标题行计入,隐藏的 class 行不计) */
static int s_vis_count(void)
{
int n = 0;
for (int i = 0; i < SMENU_N; i++) if (s_row_visible(i)) n++;
return n;
}
/* SMENU 下标 → 压缩位置(第几个可见行);不可见返回 -1 */
static int s_vis_of(int i)
{
if (i < 0 || i >= SMENU_N || !s_row_visible(i)) return -1;
int p = 0;
for (int k = 0; k < i; k++) if (s_row_visible(k)) p++;
return p;
}
/* 压缩位置 → SMENU 下标 */
static int s_vis_at(int p)
{
int c = 0;
for (int i = 0; i < SMENU_N; i++) if (s_row_visible(i)) { if (c == p) return i; c++; }
return SMENU_N - 1;
}
static int s_setting_idx(int i) { return i >= 0 && i < SMENU_N && SMENU[i].ctrl != SC_HEADER && s_row_visible(i); }
static int s_first_setting(void)
{
for (int i = 0; i < SMENU_N; i++) if (s_setting_idx(i)) return i;
return 0;
}
static int s_next_setting(int i)
{
for (int k = i + 1; k < SMENU_N; k++) if (s_setting_idx(k)) return k;
return s_first_setting(); /* 回绕 */
}
/* 字体行(font.file + font.class_*)走「family→字重」两级子菜单(M2.6 字重结构) */
static int s_row_is_font(const srow_t *r)
{
return r && r->key && (!strcmp(r->key, "font.file") || !strncmp(r->key, "font.class_", 11));
}
/* SC_PRESET 行取「选项表 + 数量」:字体行(font.file + font.class_*)用运行时动态数组
* (TF 卡 MeowOS/fonts/<family>/<weight>/ 扫描结果,见字体区 font_presets/font_presets_n),
* 其余行用编译期静态表(r->presets / r->preset_n)。 */
static const char *const *s_row_presets(const srow_t *r, int *n)
{
if (r && r->key && (!strcmp(r->key, "font.file") || !strncmp(r->key, "font.class_", 11))) {
if (n) *n = font_presets_n;
return font_presets;
}
if (n) *n = r->preset_n;
return r->presets;
}
/* 滚动:纯范围钳制到 [0, 内容高度-视口高](列表拖动滚动用;不强制选中行可见) */
static int s_clamp_scroll_range(int scroll)
{
int total = s_vis_count() * SROW_H;
if (total <= SET_CONTENT_H) return 0;
int max_scroll = total - SET_CONTENT_H;
if (scroll < 0) scroll = 0;
if (scroll > max_scroll) scroll = max_scroll;
return scroll;
}
/* 滚动:保证当前选中行在 [SET_CONTENT_Y, SET_CONTENT_BOT] 内 */
static int s_clamp_scroll(int sel, int scroll)
{
scroll = s_clamp_scroll_range(scroll);
int pos = s_vis_of(sel);
if (pos < 0) pos = s_vis_of(s_first_setting());
int top = pos * SROW_H;
int bot = top + SROW_H;
if (top < scroll) scroll = top; /* 选中行滑出顶部 → 上滚 */
if (bot > scroll + SET_CONTENT_H) scroll = bot - SET_CONTENT_H; /* 滑出底部 → 下滚 */
return s_clamp_scroll_range(scroll);
}
/* 行 y 落在 content 坐标 cy 时对应的行号(压缩位置 → SMENU 下标);越界返回 -1 */
static int s_row_at_content_y(int cy)
{
if (cy < 0) return -1;
int pos = cy / SROW_H;
if (pos >= s_vis_count()) return -1;
return s_vis_at(pos);
}
/* ---- 值格式化/展示 ---- */
static void s_fmt_time(int sec, char *buf, int len)
{
if (sec <= 0) { snprintf(buf, len, "常亮"); return; }
if (sec < 60) { snprintf(buf, len, "%d 秒", sec); return; }
if (sec < 3600) { snprintf(buf, len, "%d 分", sec / 60); return; }
snprintf(buf, len, "%d 时", sec / 3600);
}
/* B2b 主题时段:分钟(0-1439) → "HH:MM"(如 1140→"19:00")。注意与 s_fmt_time 的秒语义区分。 */
static void s_fmt_minutes(int mins, char *buf, int len)
{
if (mins < 0) mins = 0;
if (mins > 1439) mins = 1439;
snprintf(buf, len, "%02d:%02d", mins / 60, mins % 60);
}
/* 息屏时间:秒 → 最近档位索引(显示滑动位置用;等值优先,跨档取较近档,平手取高档) */
static int s_time_tier_idx(int sec)
{
if (sec <= s_time_tiers[0]) return 0;
for (int i = 1; i < S_TIME_TIERS_N; i++) {
if (sec == s_time_tiers[i]) return i;
if (sec < s_time_tiers[i]) {
int lo = s_time_tiers[i - 1], hi = s_time_tiers[i];
return (sec - lo < hi - sec) ? i - 1 : i;
}
}
return S_TIME_TIERS_N - 1;
}
/* 息屏时间:档位索引 → 秒(落盘写给 display.screen_off_timeout 的实值) */
static int s_time_tier_val(int idx)
{
if (idx < 0) idx = 0;
if (idx >= S_TIME_TIERS_N) idx = S_TIME_TIERS_N - 1;
return s_time_tiers[idx];
}
static void s_fmt_value(const srow_t *r, char *buf, int len)
{
int v = 0;
if (meow_settings_get_int(r->key, &v) != 0) { snprintf(buf, len, "-"); return; }
if (!strcmp(r->key, "display.screen_off_timeout")) { s_fmt_time(v, buf, len); return; }
/* B2b 深色时段(分钟 0-1439 → "HH:MM");start==end 视为未配置,显示「未设置」 */
if (!strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) {
int s2 = 0, e2 = 0;
meow_settings_get_int("display.theme_dark_start", &s2);
meow_settings_get_int("display.theme_dark_end", &e2);
if (s2 == e2) { snprintf(buf, len, "未设置"); return; }
s_fmt_minutes(v, buf, len);
return;
}
/* 长按弹菜单/强关:毫秒换算成秒(0.5 步进,如 "2.5 秒") */
if (!strcmp(r->key, "power.longpress_menu_ms") || !strcmp(r->key, "power.forceoff_ms")) {
if (v % 1000 == 0) snprintf(buf, len, "%d 秒", v / 1000);
else snprintf(buf, len, "%.1f 秒", v / 1000.0);
return;
}
snprintf(buf, len, "%d", v);
}
static void s_display_value(const srow_t *r, char *buf, int len)
{
if (r->ctrl == SC_ENUM) {
int idx = 0;
if (meow_settings_get_enum(r->key, &idx) != 0) idx = 0;
if (meow_settings_enum_label(r->key, idx, buf, len) != 0) snprintf(buf, len, "-");
return;
}
/* SC_PRESET */
char cur[MEOW_SET_MAX_LEN];
if (meow_settings_get_str(r->key, cur, sizeof cur) != 0) { snprintf(buf, len, "-"); return; }
/* M2.5x 多字体:class 行空值 = 跟随全局 font.file */
if (r->key && !strncmp(r->key, "font.class_", 11) && !cur[0]) {
snprintf(buf, len, "跟随全局");
return;
}
int pn = 0;
const char *const *pps = s_row_presets(r, &pn);
for (int i = 0; i < pn; i++) {
if (!strcmp(pps[i], cur)) {
snprintf(buf, len, "%s", (r->preset_labels && r->preset_labels[i][0]) ? r->preset_labels[i] : cur);
return;
}
}
snprintf(buf, len, "%s", cur);
}
/* ===== M2.39/M2.40 网络/WiFi/蓝牙(设置页「网络」分组)=====
* 背景:launcher 已读 wlan0 operstate(wlan_connected)/ hci_powered(hciconfig UP)。
* 本组在设置页内做 WiFi/蓝牙开关 + 当前连接 SSID + 已保存网络 + 附近WiFi(WiFi 扫描,
* 按 BSSID 去重)+ 蓝牙设备(已配对 + 附近扫描发现)。读取全部经带超时 popen(wpa_cli/
* bluetoothctl 在 supplicant/BlueZ 未跑时可能挂起),绝不阻塞主循环;WiFi/蓝牙扫描均异步
* (先触发、后读缓存/结果),超时余量。开关/计数/列表只读展示走 meow_settings 新键
* network.wifi_enabled/network.bluetooth_enabled。
* M2.40:wpa_cli 对非 ASCII(中文)SSID 输出转义文字 \xNN,新增 wpa_decode_ssid() 还原成
* 真实 UTF-8 字节;net_wifi_scan 按 BSSID 去重(同 BSSID 只留一条,优先具名);蓝牙附近
* 扫描经后台有界 bluetoothctl 写 /tmp 缓存(system() 尾部 & 立即返回,不阻塞主循环)。
* 设计:绘制路径只读缓存(g_net_*,s_net_refresh 刷新一次),下拉打开时才重新 popen 解析,
* 避免绘制热点反复 spawn。 */
/* 带超时跑一条命令并读 stdout(防 wpa_supplicant/BlueZ 未跑时命令挂起)。
* 返回 malloc 字符串(调用方 free);超时/失败返回 NULL;子进程超时被 SIGKILL。 */
static char *popen_read_tmo(int tmo_ms, const char *cmd)
{
int fds[2];
if (pipe(fds) != 0) return NULL;
pid_t pid = fork();
if (pid < 0) { close(fds[0]); close(fds[1]); return NULL; }
if (pid == 0) {
close(fds[0]);
dup2(fds[1], 1);
dup2(fds[1], 2);
close(fds[1]);
execl("/bin/sh", "sh", "-c", cmd, (char *)NULL);
_exit(127);
}
close(fds[1]);
int fd = fds[0];
int fl = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
char *out = NULL; size_t cap = 0, len = 0;
char tmp[256];
int done = 0;
unsigned long long deadline = now_ms() + tmo_ms;
while (!done) {
struct pollfd pfd = { fd, POLLIN, 0 };
int pr = poll(&pfd, 1, 25);
if (pr < 0) break;
if (pr == 0) { if (now_ms() > deadline) break; continue; }
ssize_t r = read(fd, tmp, sizeof tmp);
if (r > 0) {
if (len + (size_t)r + 1 > cap) {
size_t nc = cap ? cap * 2 : 512;
char *nn = realloc(out, nc);
if (!nn) { free(out); out = NULL; break; }
out = nn; cap = nc;
}
memcpy(out + len, tmp, (size_t)r); len += (size_t)r; out[len] = 0;
} else if (r == 0) {
done = 1;
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
done = 1;
}
}
/* 子进程收尸:已退出则不杀;仍在跑(读到 EOF 前超时)则 SIGKILL,防僵尸。 */
int status = 0;
pid_t rp = waitpid(pid, &status, WNOHANG);
if (rp == 0) { kill(pid, SIGKILL); waitpid(pid, &status, 0); }
close(fd);
return out;
}
/* 已保存网络 / 扫描结果 / 蓝牙设备名称列表(用于下拉展示与连接动作) */
typedef struct { int id; char name[48]; } meow_net_t;
/* 十六进制字符值('0'-'9'/'a'-'f'/'A'-'F');非法返回 -1。避免依赖 ctype.h(musl/内存受限)。
* M2.40:wpa_cli 对非 ASCII SSID 输出 "\xNN" 转义文字(`\`+x+两位hex 的纯文字,非真字节),
* 需先还原成真实 UTF-8 字节才能进界面文本。 */
static int hex_val(char c)
{
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
/* wpa_decode_ssid:把 wpa_cli 的 "\xNN" 转义文字还原成真实字节(\xe5\xa4\xa9… = "天…")。
* 遇 `\x`+两位合法 hex → 拼一字节;普通字符原样拷贝;dlen 为 dst 容量(含 NUL)。
* 无 \x 的纯 ASCII 名(如 "<ssid>")原样通过。 */
static void wpa_decode_ssid(const char *src, char *dst, int dlen)
{
int o = 0;
while (*src && o < dlen - 1) {
if (src[0] == '\\' && src[1] == 'x' &&
hex_val(src[2]) >= 0 && hex_val(src[3]) >= 0) {
dst[o++] = (char)((hex_val(src[2]) << 4) | hex_val(src[3]));
src += 4;
} else {
dst[o++] = *src++;
}
}
dst[o] = 0;
}
/* 通用选择器下拉(定义在 s_net_open 之后);网络列表复用同款模态 */
static int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur);
/* 取 WiFi 当前连接 SSID(wpa_cli -i wlan0 status 的 ssid= 行);未连接返回空串 */
static void net_wifi_ssid(char *buf, int len)
{
buf[0] = 0;
if (!wlan_connected()) return;
char *o = popen_read_tmo(1200, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 status 2>/dev/null");
if (!o) return;
char *s = strstr(o, "ssid=");
if (s) {
s += 5;
char *e = s; while (*e && *e != '\n' && *e != '\r') e++;
int l = (int)(e - s);
/* M2.40:status 的 ssid= 同样可能是中文 \xNN 转义文字,还原成真实 UTF-8 字节 */
char raw[128];
if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
memcpy(raw, s, (size_t)l); raw[l] = 0;
wpa_decode_ssid(raw, buf, len);
}
free(o);
}
/* 当前 WiFi 连接详情(点开「WiFi连接」行展示):解析 wpa_cli status + 路由网关。
* 填充 info[][48](调用方提供的二维缓冲),每行 "标签: 值",返回行数(≤max)。
* 当前SSID / AP MAC(bssid) / 本机MAC(address) / AP IP(默认网关) / 本机IP(ip_address) / 频道freq。
* 未连接返回 0(调用方显示"未连接")。绝不阻塞:popen 带超时。 */
static int net_wifi_info(char (*info)[48], int max)
{
if (!wlan_connected()) return 0;
char *o = popen_read_tmo(1500,
"PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 status 2>/dev/null; ip route 2>/dev/null | grep default");
if (!o) return 0;
char ssid[64] = "", bssid[24] = "", addr[24] = "", ip[32] = "", freq[16] = "", gw[32] = "";
char *p = o;
while (p && *p) {
char *e = strchr(p, '\n'); if (e) *e = 0;
if (!strncmp(p, "ssid=", 5)) { char raw[128]; snprintf(raw, sizeof raw, "%s", p + 5); wpa_decode_ssid(raw, ssid, sizeof ssid); }
else if (!strncmp(p, "bssid=", 6)) snprintf(bssid, sizeof bssid, "%s", p + 6);
else if (!strncmp(p, "address=", 8)) snprintf(addr, sizeof addr, "%s", p + 8);
else if (!strncmp(p, "ip_address=", 11)) snprintf(ip, sizeof ip, "%s", p + 11);
else if (!strncmp(p, "freq=", 5)) snprintf(freq, sizeof freq, "%s", p + 5);
else if (!strncmp(p, "default via ", 11)) { char *s = p + 11; char *g = s; while (*g && *g != ' ') g++; snprintf(gw, sizeof gw, "%s", s); }
p = e ? e + 1 : NULL;
}
free(o);
int n = 0;
if (n < max && ssid[0]) snprintf(info[n++], 48, "SSID: %s", ssid);
if (n < max && bssid[0]) snprintf(info[n++], 48, "AP MAC: %s", bssid);
if (n < max && addr[0]) snprintf(info[n++], 48, "本机MAC: %s", addr);
if (n < max && gw[0]) snprintf(info[n++], 48, "AP IP: %s", gw);
if (n < max && ip[0]) snprintf(info[n++], 48, "本机IP: %s", ip);
if (n < max && freq[0]) snprintf(info[n++], 48, "频道: %s MHz", freq);
return n;
}
/* 已保存网络(wpa_cli -i wlan0 list_networks):首行为表头,其余每行 "<id>\t<ssid>\t<bssid>\t<flag>"。
* 填 out[i].id/.name(SSID,中文经 wpa_decode_ssid 还原 \xNN),返回项数(≤max)。 */
static int net_wifi_saved(meow_net_t *out, int max)
{
int n = 0;
char *o = popen_read_tmo(1500, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 list_networks 2>/dev/null");
if (!o) return 0;
char *line = strchr(o, '\n');
if (line) line++; /* 跳表头 */
while (line && *line && n < max) {
char *eol = strchr(line, '\n');
if (eol) *eol = 0;
char *p = line; while (*p == ' ' || *p == '\t') p++;
if (*p && *p != '#') {
int id = atoi(p);
char *sp = p; while (*sp && *sp != ' ' && *sp != '\t') sp++;
while (*sp == ' ' || *sp == '\t') sp++;
char *se = sp; while (*se && *se != ' ' && *se != '\t' && *se != '\n' && *se != '\r') se++;
if (sp != se) {
/* M2.40:list_networks 对中文 SSID 输出 \xNN 转义文字(如 \xe5\xa4\xa9… = "天…"),
* 先拷进临时缓冲再还原成真实 UTF-8 字节存入 name,否则界面显示超长乱码。 */
char raw[160];
int l = (int)(se - sp);
if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
memcpy(raw, sp, (size_t)l); raw[l] = 0;
out[n].id = id;
wpa_decode_ssid(raw, out[n].name, sizeof out[n].name);
n++;
}
}
line = eol ? eol + 1 : NULL;
}
free(o);
return n;
}
/* WiFi 扫描触发(异步:wpa_cli scan 只是通知 supplicant 发起扫描,立即返回)。
* 结果稍后经 net_wifi_scan() 读。 */
static void net_wifi_scan_trigger(void)
{
if (!wlan_connected()) return;
system("PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 scan 2>/dev/null");
}
/* 读 WiFi 扫描结果(wpa_cli -i wlan0 scan_results):首行列头,数据行形如
* <bssid> <freq> <signal> [flags..] [ssid...]
* flags 恒以 '[' 起;SSID 为 flags 后的整段余量(可含空格;为空=隐藏网络)。
* M2.40:同一 AP 常出现多条(不同频段/空 SSID/重名),按 BSSID 去重——每个 BSSID 只留一条,
* 优先采用含完整 SSID 的行(后到且有 SSID 则覆盖先到的空 SSID 行),隐藏网络显示 "(隐藏网络)"。
* 填 out[i].id=-1;name 存 SSID(或隐藏标记)。返回项数(≤max,且 ≤S_NET_MAX)。 */
static int net_wifi_scan(meow_net_t *out, int max)
{
int n = 0;
if (max > S_NET_MAX) max = S_NET_MAX;
char *o = popen_read_tmo(2500, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 scan_results 2>/dev/null");
if (!o) return 0;
char seen_bssid[S_NET_MAX][24];
int named[S_NET_MAX] = {0};
char *line = strchr(o, '\n');
if (line) line++;
while (line && *line && n < max) {
char *eol = strchr(line, '\n');
if (eol) *eol = 0;
char *p = line; while (*p == ' ' || *p == '\t') p++;
if (*p) {
/* 首 token = bssid */
char bssid[24] = "";
{ char *b = p; while (*p && *p != ' ' && *p != '\t') p++;
int l = (int)(p - b); if (l > 22) l = 22; memcpy(bssid, b, (size_t)l); bssid[l] = 0; }
/* 找 flags(首个以 '[' 起始的 token),之后(trim 掉前导空白)才是 SSID */
const char *ssid = "";
char *br = strchr(p, '[');
if (br) {
char *q = br;
while (*q) {
while (*q == ' ' || *q == '\t') q++;
if (*q == '[') { while (*q && *q != ' ' && *q != '\t') q++; }
else break;
}
ssid = q;
while (*ssid == ' ' || *ssid == '\t') ssid++;
}
/* SSID 去尾部空白/换行 */
char *se = (char *)ssid;
while (*se && *se != '\n' && *se != '\r') se++;
while (se > ssid && (se[-1] == ' ' || se[-1] == '\t')) se--;
int has = (se > ssid && *ssid);
/* 去重:同一 BSSID 只留一条,优先具名 SSID */
int idx = -1;
for (int j = 0; j < n; j++) if (!strcmp(seen_bssid[j], bssid)) { idx = j; break; }
if (idx < 0) {
if (n >= max) { line = eol ? eol + 1 : NULL; continue; }
idx = n++;
strncpy(seen_bssid[idx], bssid, sizeof seen_bssid[idx] - 1);
seen_bssid[idx][sizeof seen_bssid[idx] - 1] = 0;
named[idx] = has;
out[idx].id = -1;
if (has) {
/* M2.41:扫描结果 SSID 也可能是 wpa_cli 的 \xNN 转义(中文),还原成真 UTF-8 */
char raw[96];
int l = (int)(se - ssid); if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
memcpy(raw, ssid, (size_t)l); raw[l] = 0;
wpa_decode_ssid(raw, out[idx].name, sizeof out[idx].name);
} else {
snprintf(out[idx].name, sizeof out[idx].name, "(隐藏网络)");
}
} else if (has && !named[idx]) {
/* 同 BSSID 先出现无 SSID 行、后有具名行 → 用后者 SSID 覆盖(解码 \xNN) */
char raw[96];
int l = (int)(se - ssid); if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
memcpy(raw, ssid, (size_t)l); raw[l] = 0;
wpa_decode_ssid(raw, out[idx].name, sizeof out[idx].name);
named[idx] = 1;
}
}
line = eol ? eol + 1 : NULL;
}
free(o);
return n;
}
/* 蓝牙已配对设备(bluetoothctl paired-devices):每行 "Device <MAC> <Name>"。
* hci 未上电则返回 0(避免 spawn bluetoothctl 白跑)。 */
static int net_bt_paired(meow_net_t *out, int max)
{
if (!hci_powered()) return 0;
int n = 0;
char *o = popen_read_tmo(1800, "bluetoothctl paired-devices 2>/dev/null");
if (!o) return 0;
char *line = o;
while (line && *line && n < max) {
char *eol = strchr(line, '\n');
if (eol) *eol = 0;
char *p = strstr(line, "Device ");
if (p) {
p += 7;
char *mac = p; while (*mac && *mac != ' ' && *mac != '\t') mac++;
char *nm = mac; while (*nm == ' ' || *nm == '\t') nm++;
/* 设备名每行即名称 token;名称可含空格,这里取整行剩余部分(含 MAC 后的名字) */
if (*nm)
snprintf(out[n].name, sizeof out[n].name, "%s", nm);
else {
int ml = (int)(mac - p); if (ml > 47) ml = 47;
memcpy(out[n].name, p, (size_t)ml); out[n].name[ml] = 0;
}
out[n].id = -1;
n++;
}
line = eol ? eol + 1 : NULL;
}
free(o);
return n;
}
/* ---- 蓝牙附近设备(未配对扫描发现;异步,不阻塞主循环)----
* M2.40:用户要的「附近的设备」= 蓝牙扫描发现(含未配对),而不只是已配对。扫描较慢(2-6s),
* 故后台有界驱动 bluetoothctl:scan on → sleep 4 → scan off → devices → quit,输出写 /tmp 缓存;
* system() 因尾部 & 立即返回(约 5s 后自灭),绝不卡死主循环。读取端 net_bt_scan 只解析缓存
* (bluetoothctl `devices` 行 "Device <MAC> <Name>",含已配对+未配对发现,去重)。 */
static long g_bt_scan_ms = 0; /* 最近一次发起扫描的 now_ms();0=从未 */
static void net_bt_scan_trigger(void)
{
if (!hci_powered()) return;
long t = (long)now_ms();
if (g_bt_scan_ms && t - g_bt_scan_ms < 7000) return; /* 7s 内不重复触发,防堆积 */
g_bt_scan_ms = t;
system("(sleep 1; echo 'scan on'; sleep 4; echo 'scan off'; echo 'devices'; echo 'quit') | bluetoothctl > /tmp/meow_bt_scan.txt 2>&1 &");
}
/* 读蓝牙附近设备缓存:解析 bluetoothctl `devices` 输出("Device <MAC> <Name>",去重;含已配对
* 与未配对但已发现的设备)。缓存不存在/为空则退回已配对列表(保证至少展示已知设备)。
* 返回项数(≤max,且 ≤S_NET_MAX)。 */
static int net_bt_scan(meow_net_t *out, int max)
{
if (!hci_powered()) return 0;
if (max > S_NET_MAX) max = S_NET_MAX;
int n = 0;
FILE *f = fopen("/tmp/meow_bt_scan.txt", "r");
if (f) {
char line[256];
char seen[S_NET_MAX][24];
while (fgets(line, sizeof line, f)) {
char *p = line; while (*p == ' ' || *p == '\t') p++;
/* 认两种行:`devices` 输出的 "Device <MAC> <Name>";发现会话里的 "[NEW] Device <MAC> <Name>"。
* 跳过 [CHG]/[DEL] 等属性/移除行(无稳定名称)。 */
char *dv = NULL;
if (!strncmp(p, "Device ", 7)) dv = p + 7;
else if (!strncmp(p, "[NEW] Device ", 13)) dv = p + 13;
if (!dv) continue;
char *mac = dv; while (*mac && *mac != ' ' && *mac != '\t' && *mac != '\n' && *mac != '\r') mac++;
int ml = (int)(mac - dv);
if (ml < 6 || !memchr(dv, ':', (size_t)ml)) continue; /* 需像 MAC(含冒号);过滤属性行 */
/* 去重(同 MAC 只留一条,取首个出现) */
int dup = 0;
for (int j = 0; j < n; j++) if (!strncmp(seen[j], dv, (size_t)ml)) { dup = 1; break; }
if (dup) continue;
if (n >= max) break;
char *nm = mac; while (*nm == ' ' || *nm == '\t') nm++;
char *e = nm; while (*e && *e != '\n' && *e != '\r') e++;
*e = 0; /* 去行尾换行 */
strncpy(seen[n], dv, (size_t)ml); seen[n][ml] = 0;
if (*nm)
snprintf(out[n].name, sizeof out[n].name, "%s", nm);
else {
int l = ml; if (l > 47) l = 47;
memcpy(out[n].name, dv, (size_t)l); out[n].name[l] = 0;
}
out[n].id = -1;
n++;
}
fclose(f);
if (n > 0) return n;
}
return net_bt_paired(out, max); /* 无扫描缓存 → 退回已配对列表 */
}
/* ---- 蓝牙角色判定(M2.48:状态栏多态 + 设置页"蓝牙模式"行)----
* 返回 BT_ROLE_*;desc 非空则写中文描述(供设置页只读行展示)。全部 popen 带超时,绝不阻塞。
* 判定(best-effort,蓝牙栈=BlueZ 5.54):
* 关 :hci0 不存在或 DOWN(hci_powered)。
* 开未连接:hciconfig UP 但 hcitool con 无 ACL 连接。
* 主模式 :有 ACL 连接(连了外围设备如耳机);设备侧 A2DP sink 命中 → 主。
* 从模式 :有 ACL 连接且设备侧 A2DP source 命中(远端接入,设备被当耳机用)。
* 注:master/slave 依赖 bluealsa-cli a2dp list 端点角色,其输出随固件格式不同;解析不到时,
* 有 ACL 一律按「主模式」兜底(仍可靠区分 关/开未连接/已连接三态)。若真机 a2dp 角色
* 与注释相反,只需交换 a2dp_side() 的 1/2 返回值即可。 */
static void copy_utf8_trunc(char *dst, int dlen, const char *src)
{
int o = 0;
while (*src && o < dlen - 1) {
unsigned char c = (unsigned char)*src;
int nb = 1;
if ((c & 0xE0) == 0xC0) nb = 2;
else if ((c & 0xF0) == 0xE0) nb = 3;
else if ((c & 0xF8) == 0xF0) nb = 4;
if (o + nb > dlen - 1) break; /* 放不下整个字符就停(不截半字符) */
for (int k = 0; k < nb; k++) { if (!src[k]) break; dst[o++] = src[k]; }
src += nb;
}
dst[o] = 0;
}
/* A2DP 段端点角色:1=设备侧 sink(主模式),2=设备侧 source(从模式),-1=无法判定。
* 宽松匹配:同段内出现 connected/running 且出现对应角色词。优先认 BlueALSA 对象路径
* a2dpsnk/a2dpsrc(跨版本稳定),再退到 source/sink/source 单词。 */
static int a2dp_side(const char *sec)
{
if (!sec) return -1;
if (!strstr(sec, "connected") && !strstr(sec, "running")) return -1;
int src = (strstr(sec, "a2dpsrc") || strstr(sec, "source") || strstr(sec, "Source")) ? 1 : 0;
int snk = (strstr(sec, "a2dpsnk") || strstr(sec, "sink") ||
strstr(sec, "Sink") || strstr(sec, "target")) ? 1 : 0;
if (src && !snk) return 2; /* 设备侧 source → 被当耳机(从) */
if (snk && !src) return 1; /* 设备侧 sink → 送流给外围(主) */
return -1;
}
/* 蓝牙角色:见上方注释。一次 popen 拿 ACL + A2DP + 已连设备名三类信息,避免反复 spawn。 */
static int bt_role(char *desc, int len)
{
if (desc && len > 0) desc[0] = 0;
if (!g_sys.bt_seen || !hci_powered()) {
if (desc && len > 0) snprintf(desc, len, "关闭");
return BT_ROLE_OFF;
}
/* 快的放前(HCI 连接、A2DP 端点),bluetoothctl 最慢放最后;整体超时防挂起(超时 SIGKILL)。 */
char *o = popen_read_tmo(1800,
"PATH=/bin:/sbin:/usr/bin:/usr/sbin hcitool con 2>/dev/null; "
"echo '===A2DP==='; bluealsa-cli a2dp list 2>/dev/null; "
"echo '===NAMES==='; printf 'devices Connected\\nquit\\n' | bluetoothctl 2>/dev/null");
if (!o) { /* 探测失败:保守按「开未连接」 */
if (desc && len > 0) snprintf(desc, len, "开启未连接");
return BT_ROLE_ON_NO_CONN;
}
/* 按标记分节(把标记首字符置 0 切断) */
char *s_acl = o;
char *s_a2dp = NULL, *s_names = NULL;
char *p = strstr(o, "===A2DP===");
if (p) { *p = 0; s_a2dp = p + 10; } /* "===A2DP===" 长 10 */
if (s_a2dp) { char *q = strstr(s_a2dp, "===NAMES==="); if (q) { *q = 0; s_names = q + 11; } }
else { char *q = strstr(o, "===NAMES==="); if (q) { *q = 0; s_names = q + 11; } }
/* ACL:hcitool con 的连接行含 "ACL <MAC>"(如 " < ACL 00:1A:.. handle.. state.. lm MASTER") */
int has_acl = 0;
{
char *line = s_acl;
while (line && *line) {
char *eol = strchr(line, '\n'); if (eol) *eol = 0;
if (strstr(line, "ACL") && strchr(line, ':')) { has_acl = 1; break; }
line = eol ? eol + 1 : NULL;
}
}
/* 已连设备名(bluetoothctl `devices Connected`:"Device <MAC> <Name>",取首个)。
* 定长 22:配合「从模式·被连(耳机…」前缀(UTF-8 约 25B)仍落在展示缓冲 48B 内。
* 同时也作「已连接」的第二证据(hcitool 缺失时 bluetoothctl 兜底)。 */
char dev[22] = "";
int has_conn = has_acl;
if (s_names) {
char *line = s_names;
while (line && *line) {
char *eol = strchr(line, '\n'); if (eol) *eol = 0;
char *q = line; while (*q == ' ' || *q == '\t') q++;
if (!strncmp(q, "Device ", 7)) {
has_conn = 1; /* bluetoothctl 也确认有设备已连 */
if (!dev[0]) {
q += 7;
char *mac = q; while (*mac && *mac != ' ' && *mac != '\t' &&
*mac != '\n' && *mac != '\r') mac++;
char *nm = mac; while (*nm == ' ' || *nm == '\t') nm++;
char *e = nm; while (*e && *e != '\n' && *e != '\r') e++;
*e = 0;
if (*nm) copy_utf8_trunc(dev, sizeof dev, nm);
else copy_utf8_trunc(dev, sizeof dev, q);
}
}
line = eol ? eol + 1 : NULL;
}
}
int side = a2dp_side(s_a2dp);
int role;
if (!has_conn) role = BT_ROLE_ON_NO_CONN;
else if (side == 2) role = BT_ROLE_SLAVE; /* 设备侧 source → 被当耳机 */
else role = BT_ROLE_MASTER; /* sink 或 a2dp 不明但有连接 → 主 */
if (desc && len > 0) {
if (role == BT_ROLE_ON_NO_CONN) snprintf(desc, len, "开启未连接");
else if (role == BT_ROLE_SLAVE) snprintf(desc, len, "从模式·被连(耳机%s%s)",
dev[0] ? " " : "", dev);
else snprintf(desc, len, "主模式·已连%s%s",
dev[0] ? " " : "", dev[0] ? dev : "设备");
}
free(o);
return role;
}
/* 节流重探蓝牙角色(约 5s 一次;force=1 立即)。结果写 g_sys.bt_role + g_bt_mode,
* 并同步 g_sys.bt_on/bt_seen,保证状态栏与本行一致。蓝牙关时免费(只 spawn hciconfig)。 */
static long g_bt_role_ms = 0; /* 最近一次 bt_role 探测的 now_ms();0=从未 */
static void bt_role_refresh(int force)
{
if (!force && g_bt_role_ms && (now_ms() - g_bt_role_ms) < 5000) return;
g_bt_role_ms = now_ms();
char d[48];
g_sys.bt_role = bt_role(d, sizeof d);
snprintf(g_bt_mode, sizeof g_bt_mode, "%s", d);
g_sys.bt_seen = access("/sys/class/bluetooth/hci0", F_OK) == 0;
g_sys.bt_on = (g_sys.bt_role != BT_ROLE_OFF) ? 1 : 0;
}
/* 网络信息缓存(绘制路径只读,s_net_refresh 刷新;下拉打开时才重新解析列表) */
static char g_net_ssid[64] = "未连接";
static int g_net_saved_n = 0;
static int g_net_scan_n = 0;
static int g_net_bt_n = 0;
/* 刷新网络信息缓存(进入设置页 / 网络动作后调用;含一次 WiFi 扫描触发) */
static void s_net_refresh(void)
{
if (wlan_connected()) {
char ss[64]; net_wifi_ssid(ss, sizeof ss);
if (ss[0]) snprintf(g_net_ssid, sizeof g_net_ssid, "已连接: %s", ss);
else snprintf(g_net_ssid, sizeof g_net_ssid, "已连接");
} else {
snprintf(g_net_ssid, sizeof g_net_ssid, "未连接");
}
net_wifi_scan_trigger(); /* 异步发起扫描,稍后附近/计数更接近真实 */
net_bt_scan_trigger(); /* M2.40:异步发起蓝牙附近扫描(写 /tmp 缓存,不阻塞) */
bt_role_refresh(1); /* M2.48:进入设置/网络动作时立即重探蓝牙角色,本行/状态栏最新 */
meow_net_t tmp[S_NET_MAX];
g_net_saved_n = net_wifi_saved(tmp, S_NET_MAX);
g_net_scan_n = net_wifi_scan(tmp, S_NET_MAX);
g_net_bt_n = net_bt_scan(tmp, S_NET_MAX); /* 附近(已配对+扫描发现) */
}
/* SC_INFO/SC_LIST 行的即时展示文本(读缓存;draw 路径不 popen) */
static int s_net_get(const srow_t *r, char *buf, int len)
{
switch (r->net_list) {
case NET_LIST_WIFI_SSID: snprintf(buf, len, "%s", g_net_ssid); return 0;
case NET_LIST_WIFI_SAVED: snprintf(buf, len, "%d 个网络", g_net_saved_n); return 0;
case NET_LIST_WIFI_SCAN: snprintf(buf, len, "%d 个附近WiFi", g_net_scan_n); return 0;
case NET_LIST_BT_NEARBY: snprintf(buf, len, "%d 台设备", g_net_bt_n); return 0;
case NET_LIST_BT_MODE: snprintf(buf, len, "%s", g_bt_mode); return 0;
case NET_LIST_ABOUT: snprintf(buf, len, "MeowOS v%s", g_meowos_version); return 0;
}
snprintf(buf, len, "-");
return 0;
}
/* 网络动作开关:wifi/bluetooth 开/关(写 wlan0/hci0;立即生效,异步刷新缓存)。
* 走 system(wifi_up.sh 自身后台化子进程、ip link/hciconfig 均为快 ioctl)。 */
static void s_net_toggle_wifi(int on)
{
if (on) system("PATH=/bin:/sbin:/usr/bin:/usr/sbin /usr/bin/wifi_up.sh");
else system("PATH=/bin:/sbin:/usr/bin:/usr/sbin ip link set wlan0 down 2>/dev/null");
}
static void s_net_toggle_bt(int on)
{
if (on) system("hciconfig hci0 up 2>/dev/null");
else system("hciconfig hci0 down 2>/dev/null");
}
/* 进入设置页时把 network.wifi_enabled/bluetooth_enabled 同步为真实硬件态,
* 让开关行反映 reality(仅同步内存 + 刷新缓存;不落盘 —— s_commit 的 save 才真正写)。
* settings 未 init/取值失败则不动(保默认)。 */
static void s_net_sync_from_sys(void)
{
int cur = 0;
if (meow_settings_get_bool("network.wifi_enabled", &cur) == 0) {
int real = wlan_connected() ? 1 : 0;
if (real != cur) meow_settings_set_bool("network.wifi_enabled", real);
}
if (meow_settings_get_bool("network.bluetooth_enabled", &cur) == 0) {
int real = hci_powered() ? 1 : 0;
if (real != cur) meow_settings_set_bool("network.bluetooth_enabled", real);
}
s_net_refresh();
}
/* M2.5x 系统设置「关于本机」:复用 s_dd_modal 弹只读设备信息(定义见下,供 s_net_open 分发)。
* 前置声明:s_net_open 在其定义之前调用它。 */
static void s_about_open(void);
/* 打开 SC_LIST 网络下拉(重新解析最新列表):已保存=可 select_network 连接;
* 附近WiFi(扫描)/蓝牙设备(附近扫描+已配对)=只读展示(选择即关闭;带密码/配对仍留后期)。
* 返回后刷新缓存。 */
static void s_net_open(const srow_t *r)
{
meow_net_t items[S_NET_MAX];
const char *txt[S_NET_MAX];
char back[S_NET_MAX][48];
int n = 0;
switch (r->net_list) {
case NET_LIST_WIFI_SSID: /* 点开「WiFi连接」→ 显示当前连接详情(只读) */
n = net_wifi_info(back, S_NET_MAX);
if (n == 0) { snprintf(back[0], sizeof back[0], "%s", wlan_connected() ? "已连接(无详情)" : "未连接"); n = 1; }
for (int i = 0; i < n; i++) txt[i] = back[i];
s_dd_modal(r->label, txt, n, -1); /* 只读展示 */
break;
case NET_LIST_WIFI_SAVED:
n = net_wifi_saved(items, S_NET_MAX);
if (n == 0) { snprintf(back[0], sizeof back[0], "无已保存网络"); txt[0] = back[0]; n = 1; items[0].id = -1; }
else for (int i = 0; i < n; i++) {
snprintf(back[i], sizeof back[i], "%s", items[i].name[0] ? items[i].name : "<未命名>");
txt[i] = back[i];
}
{
int pick = s_dd_modal(r->label, txt, n, -1); /* cur=-1:不预勾选 */
if (pick >= 0 && pick < n && items[pick].id >= 0) {
char cmd[160];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 select_network %d 2>/dev/null",
items[pick].id);
system(cmd);
}
}
break;
case NET_LIST_WIFI_SCAN:
net_wifi_scan_trigger();
n = net_wifi_scan(items, S_NET_MAX);
if (n == 0) { snprintf(back[0], sizeof back[0], "未扫描到附近网络"); txt[0] = back[0]; n = 1; }
else for (int i = 0; i < n; i++) { snprintf(back[i], sizeof back[i], "%s", items[i].name); txt[i] = back[i]; }
s_dd_modal(r->label, txt, n, -1); /* 只读展示 */
break;
case NET_LIST_BT_NEARBY:
net_bt_scan_trigger(); /* M2.40:异步发起/刷新附近扫描(读缓存;绝不阻塞主循环) */
n = net_bt_scan(items, S_NET_MAX);
if (n == 0) {
long t = (long)now_ms();
if (g_bt_scan_ms && t - g_bt_scan_ms < 6000)
snprintf(back[0], sizeof back[0], "扫描中...");
else
snprintf(back[0], sizeof back[0], "未发现附近设备");
txt[0] = back[0]; n = 1; items[0].id = -1;
}
else for (int i = 0; i < n; i++) { snprintf(back[i], sizeof back[i], "%s", items[i].name); txt[i] = back[i]; }
s_dd_modal(r->label, txt, n, -1); /* 只读展示 */
break;
case NET_LIST_ABOUT:
s_about_open(); /* M2.5x 系统设置「关于本机」:只读设备信息弹窗 */
return; /* 不上网/蓝牙缓存刷新(信息已现场读取) */
default:
return;
}
s_net_refresh();
}
/* M2.5x 系统设置「关于本机」:复用 s_dd_modal(与「WiFi连接状态」弹窗同款)列只读设备信息,
* 点卡片外/返回即关。信息源:settings system.hostname / g_meowos_version(=/etc/meowos_version) /
* uname -r / /proc/cpuinfo / /proc/meminfo / df / wpa_cli status / hciconfig / fb 分辨率+状态栏高。
* 只展示安全信息(不含任何密钥/凭证);实时读,短超时不阻塞主循环;行数 ≤12 适配下拉视口。
* 说明:launcher/bootmenu/watchdog/meow_v 未单独暴露版本号,统一以 /etc/meowos_version 的
* MeowOS 版本表示(与固件打包同源);无独立固件版本文件。 */
static void s_about_open(void)
{
char info[12][96];
const char *item[12];
int n = 0;
char host[64];
char *o;
/* 设备名(settings system.hostname;缺省 meowos) */
if (meow_settings_get_str("system.hostname", host, sizeof host) != 0 || !host[0])
snprintf(host, sizeof host, "meowos");
snprintf(info[n++], sizeof info[0], "设备名: %s", host);
/* MeowOS / 固件版本(/etc/meowos_version;与固件打包同源) */
snprintf(info[n++], sizeof info[0], "MeowOS: v%s", g_meowos_version);
/* 内核版本 */
o = popen_read_tmo(400, "PATH=/bin:/sbin:/usr/bin:/usr/sbin uname -r 2>/dev/null");
if (o) { for (char *p = o; *p; p++) if (*p == '\n' || *p == '\r') { *p = 0; break; }
snprintf(info[n++], sizeof info[0], "内核: %s", o[0] ? o : "-"); free(o); }
else snprintf(info[n++], sizeof info[0], "内核: -");
/* CPU(/proc/cpuinfo:Processor/CPU model/Hardware/system type 任取首非空行) */
o = popen_read_tmo(500,
"PATH=/bin:/sbin:/usr/bin:/usr/sbin awk -F: '/(model name|cpu model|system type|machine|hardware)/{print $2; exit}' /proc/cpuinfo 2>/dev/null");
if (o) { char *q = o; for (char *p = o; *p; p++) if (*p == '\n' || *p == '\r') { *p = 0; break; }
while (*q == ' ' || *q == '\t') q++;
snprintf(info[n++], sizeof info[0], "CPU: %s", q[0] ? q : "-"); free(o); }
else snprintf(info[n++], sizeof info[0], "CPU: -");
/* 内存(/proc/meminfo MemTotal / MemAvailable,KB→MB) */
long mtot = 0, mavail = 0;
o = popen_read_tmo(500, "PATH=/bin:/sbin:/usr/bin:/usr/sbin awk '/^MemTotal:/{print $2}/^MemAvailable:/{print $2}' /proc/meminfo 2>/dev/null");
if (o) { if (sscanf(o, "%ld%ld", &mtot, &mavail) < 2) mavail = 0; free(o); }
if (mtot > 0) snprintf(info[n++], sizeof info[0], "内存: %ld/%ld MB", mavail / 1024, mtot / 1024);
else snprintf(info[n++], sizeof info[0], "内存: -");
/* 板载 /usr/data:df -k → 可用/总量(MB) */
long dfree = 0, dtot = 0;
o = popen_read_tmo(600, "PATH=/bin:/sbin:/usr/bin:/usr/sbin df -k /usr/data 2>/dev/null | awk 'NR==2{print $4, $2}'");
if (o) { if (sscanf(o, "%ld%ld", &dfree, &dtot) < 2) dtot = 0; free(o); }
if (dtot > 0) snprintf(info[n++], sizeof info[0], "板载: %ld/%ld MB", dfree / 1024, dtot / 1024);
else snprintf(info[n++], sizeof info[0], "板载: -");
/* TF 卡 /usr/data/mnt/sd_0:卡在(/proc/mounts 含该挂载点)则 df,否则「无卡」 */
if (card_present()) {
long sfree = 0, stot = 0;
o = popen_read_tmo(600, "PATH=/bin:/sbin:/usr/bin:/usr/sbin df -k /usr/data/mnt/sd_0 2>/dev/null | awk 'NR==2{print $4, $2}'");
if (o) { if (sscanf(o, "%ld%ld", &sfree, &stot) < 2) stot = 0; free(o); }
if (stot > 0) snprintf(info[n++], sizeof info[0], "TF卡: %ld/%ld MB", sfree / 1024, stot / 1024);
else snprintf(info[n++], sizeof info[0], "TF卡: -");
} else {
snprintf(info[n++], sizeof info[0], "TF卡: 无卡");
}
/* 当前屏幕分辨率 + 状态栏高 */
snprintf(info[n++], sizeof info[0], "屏幕: %dx%d 状态栏%dpx", fb_xres, fb_yres, (int)SB_BAR_H);
/* WiFi:连上则 SSID(decode)+ 本机IP + 本机MAC;未连接显示「未连接」 */
if (wlan_connected()) {
char ss[64]; net_wifi_ssid(ss, sizeof ss);
snprintf(info[n++], sizeof info[0], "WiFi: %s", ss[0] ? ss : "已连接");
o = popen_read_tmo(1200, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 status 2>/dev/null");
char ip[32] = "", mac[24] = "";
if (o) {
char *p = o;
while (p && *p) {
char *e = strchr(p, '\n'); if (e) *e = 0;
if (!strncmp(p, "ip_address=", 11)) snprintf(ip, sizeof ip, "%s", p + 11);
else if (!strncmp(p, "address=", 8)) snprintf(mac, sizeof mac, "%s", p + 8);
p = e ? e + 1 : NULL;
}
free(o);
}
if (ip[0]) snprintf(info[n++], sizeof info[0], "本机IP: %s", ip);
if (mac[0]) snprintf(info[n++], sizeof info[0], "本机MAC: %s", mac);
} else {
snprintf(info[n++], sizeof info[0], "WiFi: 未连接");
}
/* 蓝牙:hci_powered 决定开/关 */
snprintf(info[n++], sizeof info[0], "蓝牙: %s", hci_powered() ? "开" : "关");
for (int i = 0; i < n && i < 12; i++) item[i] = info[i];
s_dd_modal("关于本机", item, n, -1); /* cur=-1:只读展示,无预勾选 */
}
/* ---- 控件绘制(MD3 形态;全部用主题角色色,无散落硬编码)---- */
static void s_draw_switch(int x, int y, int on, const meow_ui_theme_t *th)
{
int w = 48, h = 28;
meow_gfx_fill_round(x, y, w, h, h / 2, on ? th->primary : th->surface_container);
int knob_r = h / 2 - 3;
int kx = on ? x + w - knob_r * 2 - 3 : x + 3;
meow_gfx_fill_round(kx, y + 3, knob_r * 2, knob_r * 2, knob_r,
on ? 0xFFFFFF : th->on_surface_variant);
}
static void s_draw_slider(int x, int y, int w, int val, int mn, int mx,
const meow_ui_theme_t *th)
{
int h = 6, ty = y;
fill_rect(x, ty, w, h, th->surface_container); /* 未达段 */
int range = mx - mn;
int frac = range ? (val - mn) * w / range : 0;
if (frac > 0) fill_rect(x, ty, frac, h, th->primary); /* 已达段 */
int tx = x + frac, cy = ty + h / 2;
meow_gfx_fill_round(tx - 12, cy - 12, 24, 24, 12, th->primary); /* thumb */
meow_gfx_fill_round(tx - 5, cy - 5, 10, 10, 5, 0xFFFFFF); /* thumb 中点 */
}
/* ---- set 后统一侧效应(DAC/背光/刷新率)+ 可选落盘 ---- */
static void s_commit(const srow_t *r, int save_now)
{
if (!r || !r->key) return;
if (!strcmp(r->key, "audio.volume")) {
/* M2.38 Bug1/2:音量变更——拖动中节流写 DAC(防 system() 高频卡死),抬手才无条件直写;
* 同时把 g_sys.vol 同步为当前值并刷整条状态栏(顶层 VOL 数字随之更新,不整屏重绘)。 */
g_sys.vol = settings_volume_get();
paint_sb_right();
if (save_now) meow_dac_vol_flush(); /* 抬手/点击落盘=无条件直写真实 DAC */
else meow_dac_vol_write_throttled();/* 拖动中=墙钟节流写 DAC */
}
else if (!strcmp(r->key, "display.brightness")) {
/* M2.46-2 ①:亮度滑条拖动时状态栏百分比数字实时跟。根因:旧代码只 write_brightness(v)
* 写背光,未同步 g_sys.brightness(状态栏亮度数字取它)也未重绘 → 数字卡住。
* 修法:写背光后同步 g_sys.brightness 并 paint_sb_right()(≡sb_redraw 整条标脏)。
* 一致性:其后 meow_sys_refresh 从 sysfs 读回的也是刚写入的 v,二者一致不打架。 */
int v; if (meow_settings_get_int("display.brightness", &v) == 0) {
write_brightness(v);
g_sys.brightness = v;
paint_sb_right();
}
}
else if (!strcmp(r->key, "display.refresh_rate"))
meow_ui_set_refresh_rate(refresh_rate_cfg);
else if (!strcmp(r->key, "display.refresh_always_max"))
meow_ui_set_refresh_always_max(refresh_always_max);
else if (!strcmp(r->key, "network.wifi_enabled")) {
int on = 0; meow_settings_get_bool(r->key, &on);
s_net_toggle_wifi(on);
s_net_refresh();
}
else if (!strcmp(r->key, "network.bluetooth_enabled")) {
int on = 0; meow_settings_get_bool(r->key, &on);
s_net_toggle_bt(on);
s_net_refresh();
}
/* M2.44 背景选项真正生效:display.background(black/blue/green) → menu_bg(主界面背景)。
* 此前 SC_PRESET 只写 meow_settings,未应用 menu_bg → 黑/绿点了没用。 */
else if (!strcmp(r->key, "display.background")) {
char bg[16]; meow_settings_get_str(r->key, bg, sizeof bg);
if (!strcmp(bg, "blue")) menu_bg = 0x0A1428; /* 深蓝 */
else if (!strcmp(bg, "green")) menu_bg = 0x0A2814; /* 深绿 */
else menu_bg = 0x000000; /* black(默认) */
/* 状态栏(menu_bg)与主界面背景都会用到 menu_bg;退出设置页回主界面时重绘即可。 */
}
/* M2.44 主题/mode 变更:状态栏文字/底色已接 s_ui_theme(),此处仅确保回主界面重绘。 */
else if (!strcmp(r->key, "display.theme") || !strcmp(r->key, "display.theme_mode") ||
!strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) {
paint_sb_right(); /* 状态栏文字/底色随主题刷新(整条标脏) */
}
/* M2.5x:字体文件(font.file)变更 → 立即 unload + reload(若已加载)。
* 置 mfont_forced 跳过 meow_font_ensure 的 SDK 复用捷径,强制走
* font.file → 卡默认 → 固件 链;全失败则回退 5x7 点阵(系统不死)。 */
else if (!strcmp(r->key, "font.file")) {
mfont_ready = 0;
mfont_forced = 1;
if (!meow_font_ensure())
logmsg("font.file: reload failed -> dotmatrix fallback");
else
logmsg("font.file: reloaded");
meow_font_reload_multifont(); /* M2.5x:全局字体变更 → 重配多字体(各类跟随全局者随之更新) */
meow_dirty_full(); /* 字体变化可能影响所有文本渲染,整帧重绘 */
}
/* M2.5x「加载大字库」开关(font.load_full)变更 → 强制重载 launcher 自身字体(小/大字库切换),
* 并重配多字体;大字库 atlas 大,meow_font_ensure 先 unload 旧再 load 新(防双驻留)。 */
else if (!strcmp(r->key, "font.load_full")) {
mfont_ready = 0;
mfont_forced = 1; /* 跳过 SDK 复用捷径,强制按生效档(gbk/common)真实 reload */
if (!meow_font_ensure())
logmsg("font.load_full: reload failed -> dotmatrix fallback");
else {
int lf = 0; meow_settings_get_switch("font.load_full", &lf);
logmsg("font.load_full: reloaded (load_full=%d)", lf);
}
meow_font_reload_multifont();
meow_dirty_full();
}
/* M2.5x 多字体:开关或任一类别字体变更 → 重新去重装载类别字库并重绘(多字体可能影响文本渲染)。 */
else if (!strcmp(r->key, "display.multi_font") || !strncmp(r->key, "font.class_", 11)) {
meow_font_reload_multifont();
meow_dirty_full();
}
if (save_now) meow_settings_save();
}
static void s_set_int_value(const srow_t *r, int v, int save_now)
{
if (!strcmp(r->key, "audio.volume")) v = vol_clamp(v); /* 尊重 audio.volume_max */
meow_settings_set_int(r->key, v);
s_commit(r, save_now);
}
/* 滑条拖动/点击落值:对「档位型」息屏时间做 索引→秒 换算,其余行原样写盘。
* sl_v 为滑条轨迹值(s_slider_val_from_x 返回值;息屏时间=档位索引)。 */
static void s_slider_set_from_track(const srow_t *r, int sl_v, int save_now)
{
if (!strcmp(r->key, "display.screen_off_timeout")) sl_v = s_time_tier_val(sl_v);
/* B2b 深色时段滑条:分钟按 15 分钟步进吸附(0-1439,整 15 分钟),避免拖出任意分钟 */
if (!strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) {
sl_v = ((sl_v + 7) / 15) * 15;
if (sl_v < 0) sl_v = 0;
if (sl_v > 1439) sl_v = 1439;
}
s_set_int_value(r, sl_v, save_now);
}
/* 滑条拖动值域映射:x(屏幕坐标)→ value,基于布局常量 S_SLIDER_X/W。
* 所有滑条行共用同一 track 几何,故任意滑条行都能正确映射。 */
static int s_slider_val_from_x(const srow_t *r, int px)
{
int range = r->max - r->min;
int v = r->min + (range ? (px - S_SLIDER_X) * range / S_SLIDER_W : 0);
if (v < r->min) v = r->min;
if (v > r->max) v = r->max;
return v;
}
/* ---- 绘制设置页(整页画进 SDK back_buf;调用方再 meow_dirty_add 局部 + flush)---- */
static void draw_settings_page(int sel, int scroll_off)
{
const meow_ui_theme_t *th = s_ui_theme();
int W = fb_xres, H = fb_yres;
fill_rect(0, 0, W, H, th->surface);
draw_statusbar(); /* 复用状态栏骨架 */
if (mfont_ready) ui_draw(24, 50, "系统设置", th->on_surface, 28);
else draw_text(24, 30, "SETTINGS", 3, th->on_surface);
fill_rect(0, 50 + (mfont_ready ? meow_font_line_height(28) : 24) + 2, W, 2, th->outline);
/* M2.38 Bug5:整屏背景替代带圆角的单张卡片(用户反馈圆角非全屏底很怪)。
* 内容区从 SET_CARD_Y 起整屏 fill surface_variant、去圆角、满宽直到屏幕底;
* 状态栏(0-28)与标题区(28-92)仍为 surface,标题/分隔线/分组 outline/选中高亮/滚动条布局不变。 */
fill_rect(0, SET_CARD_Y, W, H - SET_CARD_Y, th->surface_variant);
int clip_top = SET_CONTENT_Y, clip_bot = SET_CONTENT_BOT;
s_sel_track_y = -1;
for (int i = 0; i < SMENU_N; i++) {
int pos = s_vis_of(i);
if (pos < 0) continue; /* 隐藏行(多字体关闭时 6 个 class 行) */
int sy = SET_CONTENT_Y + pos * SROW_H - scroll_off;
if (sy + SROW_H < clip_top || sy >= clip_bot) continue; /* 视野外行跳过(配滚动) */
const srow_t *r = &SMENU[i];
int selected = (i == sel);
if (selected)
meow_ui_fill_rect_alpha(SET_CARD_X + 4, sy, SET_CARD_W - 8, SROW_H, th->primary, 34);
meow_ui_fill_rect(SET_CARD_X + 12, sy + SROW_H - 1, SET_CONTENT_W, 1, th->outline);
if (r->ctrl == SC_HEADER) {
ui_draw(SET_CARD_X + 16, sy + 14, r->label, th->on_surface_variant, 18);
} else if (r->ctrl == SC_SWITCH) {
ui_draw(SET_CARD_X + 16, sy + (SROW_H - meow_font_line_height(22)) / 2, r->label,
th->on_surface, 22);
int on = 0; meow_settings_get_switch(r->key, &on);
s_draw_switch(SET_CARD_X + SET_CARD_W - 16 - 48, sy + (SROW_H - 28) / 2, on, th);
} else if (r->ctrl == SC_SLIDER) {
int ly = sy + (SROW_H - meow_font_line_height(22)) / 2 - 6; /* 标题略上移,给下方滑条留位 */
ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22);
char vb[32]; s_fmt_value(r, vb, sizeof vb);
ui_draw(SET_CARD_X + SET_CARD_W - 16 - ui_width(vb, 22), ly, vb, th->on_surface_variant, 22);
int v = 0; meow_settings_get_int(r->key, &v);
/* 息屏时间为档位索引型:把秒换算成档位索引再画(各档等距,便于触摸对位);
* 其余滑条防御性 clamp 到 [min,max],防止后端暂存值越界(如 brightness 旧值 101
* 在画时被 clamp 到 100,thumb 不冲出 track)。 */
if (!strcmp(r->key, "display.screen_off_timeout")) v = s_time_tier_idx(v);
else {
if (v < r->min) v = r->min;
if (v > r->max) v = r->max;
}
int tyy = sy + 40;
s_draw_slider(S_SLIDER_X, tyy, S_SLIDER_W, v, r->min, r->max, th);
if (selected) s_sel_track_y = tyy;
} else if (r->ctrl == SC_INFO) { /* M2.39 只读动态信息(如 WiFi 连接 SSID) */
int ly = sy + (SROW_H - meow_font_line_height(22)) / 2;
ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22);
char vb[48]; s_net_get(r, vb, sizeof vb);
int vx = SET_CARD_X + SET_CARD_W - 16 - ui_width(vb, 22);
ui_draw(vx, ly, vb, th->on_surface_variant, 22);
} else if (r->ctrl == SC_LIST) { /* M2.39 网络列表行:摘要 + 右向展开提示 */
int ly = sy + (SROW_H - meow_font_line_height(22)) / 2;
ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22);
char vb[48]; s_net_get(r, vb, sizeof vb);
int vx = SET_CARD_X + SET_CARD_W - 16 - 12 - ui_width(vb, 22); /* 给右向 chevron 留位 */
ui_draw(vx, ly, vb, th->on_surface_variant, 22);
s_draw_chevron(SET_CARD_X + SET_CARD_W - 16, sy + SROW_H / 2, th->on_surface_variant);
} else { /* SC_ENUM / SC_PRESET */
int ly = sy + (SROW_H - meow_font_line_height(22)) / 2;
ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22);
char vb[40]; s_display_value(r, vb, sizeof vb);
int vx = SET_CARD_X + SET_CARD_W - 16 - ui_width(vb, 22);
ui_draw(vx, ly, vb, th->on_surface_variant, 22);
s_draw_chevron(vx - 8, sy + SROW_H / 2, th->on_surface_variant);
}
}
/* 滚动条(内容超出视口时显示 thumb;on_surface_variant 弱化) */
int vis_total = s_vis_count() * SROW_H;
if (vis_total > SET_CONTENT_H) {
int total = vis_total;
int thumb_h = SET_CONTENT_H * SET_CONTENT_H / total;
if (thumb_h < 24) thumb_h = 24;
int th_y = SET_CONTENT_Y + (SET_CONTENT_H - thumb_h) * scroll_off / (total - SET_CONTENT_H);
meow_gfx_fill_round(fb_xres - 12, SET_CONTENT_Y, 5, SET_CONTENT_H, 3, th->surface_container);
meow_gfx_fill_round(fb_xres - 12, th_y, 5, thumb_h, 3, th->on_surface_variant);
}
/* 底部:返回提示(左)+ 版本号(右),复用应用列表底部对齐逻辑。
* M2.39:返回按钮改用像素左向箭头 + 「返回」——点阵回退字库缺少 '‹' 字形,
* 原 「‹ 返回」在 !mfont_ready 时 '‹' 渲染成方框。 */
{
char ver[32];
snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version);
int foot_y = fb_yres - meow_font_line_height(20) - 8;
s_draw_chevron_left(SET_CARD_X + 12, foot_y + meow_font_line_height(20) / 2, th->on_surface_variant);
ui_draw(SET_CARD_X + 24, foot_y, "返回", th->on_surface_variant, 20);
ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, th->outline, 20);
}
}
/* M2.5x 多字体:class 行下拉的「跟随全局」选项(值=空串)在 write 路径经此映射回真实存储值。 */
static const char *s_preset_value(const srow_t *r, int pick)
{
int is_cls = r->key && !strncmp(r->key, "font.class_", 11);
if (is_cls) {
if (pick == 0) return ""; /* 跟随全局 font.file */
pick--;
}
int pn = 0;
const char *const *pps = s_row_presets(r, &pn);
if (pick < 0 || pick >= pn) return is_cls ? "" : (pps && pn > 0 ? pps[0] : "");
return pps[pick];
}
/* ---- 选择器子对话框(MD3 §3.3:居中圆角卡片,列选项;中文标签)----
* ENUM 用 meow_settings_enum_names/enum_label;PRESET 用本地预设表。
* 返回选中索引;取消返回 -1。阻塞(自开输入设备)。
* M2.39:modal 核心抽到 s_dd_modal(网络列表复用),本函数只负责组装选项数组。 */
static int s_dropdown(const srow_t *r)
{
int n = 0, cur = 0;
const char *names[8];
char labels[8][48];
const char *item[S_NET_MAX];
if (r->ctrl == SC_ENUM) {
int kn = 0;
meow_settings_enum_names(r->key, names, &kn);
n = kn;
if (n > S_NET_MAX) n = S_NET_MAX;
for (int i = 0; i < n; i++) {
meow_settings_enum_label(r->key, i, labels[i], sizeof labels[i]);
item[i] = labels[i];
}
int ci = 0; meow_settings_get_enum(r->key, &ci); cur = ci;
} else {
char cstr[MEOW_SET_MAX_LEN];
meow_settings_get_str(r->key, cstr, sizeof cstr);
int pn = 0;
const char *const *pps = s_row_presets(r, &pn);
int is_cls = r->key && !strncmp(r->key, "font.class_", 11);
int base = is_cls ? 1 : 0; /* class 行 0 号留给「跟随全局」 */
int cnt = pn; if (cnt > S_NET_MAX - base) cnt = S_NET_MAX - base;
n = base + cnt;
if (is_cls) {
names[0] = "";
snprintf(labels[0], sizeof labels[0], "跟随全局字体");
item[0] = labels[0];
if (!cstr[0]) cur = 0;
}
for (int i = 0; i < cnt; i++) {
names[base + i] = pps[i];
snprintf(labels[base + i], sizeof labels[base + i], "%s",
(r->preset_labels && r->preset_labels[i][0]) ? r->preset_labels[i] : pps[i]);
item[base + i] = labels[base + i];
if (!strcmp(pps[i], cstr)) cur = base + i;
}
}
if (n <= 0) return -1;
if (cur < 0 || cur >= n) cur = 0;
return s_dd_modal(r->label, item, n, cur);
}
/* ---- 通用选择器下拉模态(MD3 §3.3:居中圆角卡片,列选项)----
* title=标题;item[i]=第 i 项显示文本;n=项数(≤S_NET_MAX);cur=当前值下标(画勾选)。
* 返回选中下标;取消/点卡片外返回 -1。阻塞(自开输入设备)。
* s_dropdown(设置项 enum/preset)与 s_net_open(网络列表)共用。
* M2.40:触摸命中判定改在抬手用抬手时刻最新 abs 坐标(见循环内注释),修「点选项无反应」。
* 说明:模态内部仍保留音量键=移动选项高亮(这是「选值」不是「设置页列表光标」,属另一语义,
* 与 M2.39 仅移除 run_settings_page 列表光标移动的约束不冲突)。 */
static int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur)
{
int sel = cur;
const meow_ui_theme_t *th = s_ui_theme();
int W = fb_xres, H = fb_yres;
int opt_row = 48, pad = 20, title_h = 40;
int d_h = title_h + n * opt_row + pad;
int d_w = 348, d_x = (W - d_w) / 2, d_y = (H - d_h) / 2;
if (d_y < SB_BAR_H) d_y = SB_BAR_H; /* 下拉不压状态栏(栏高 40→28,随常量) */
if (H - d_y - d_h < 20) d_y = H - d_h - 20;
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
struct kd_state kd_vold, kd_volu, kd_pwr;
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_pwr, 0, sizeof kd_pwr);
int m_prev_vold = 0, m_prev_volu = 0, m_prev_pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
}
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
if (tfd >= 0) {
/* M2.37:彻底清空「打开下拉」这次触摸的残留边沿。goodix 的抬起序列(ABS 坐标/
* BTN_TOUCH=0/MT_TRACKING=-1)在开 fd 后仍可能陆续送达,若只清空一次,第一轮
* 下拉循环会读到这些残边 → 误判抬起/选项错位/点卡片外穿透。故 排空→短延时→再排空。 */
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == 16) {}
usleep(40000); /* M2.38 Bug3:排空→40ms→再排空,吸收 goodix 抬起序列长尾 */
while (read(tfd, ev, sizeof ev) == 16) {}
}
int pwr_pending = 0;
int result = -1;
int dlx = -1, dly = -1, dtd = 0; /* 下拉触摸解析(dlx/dly=最新有效坐标;dtd=已按下) */
/* M2.40:不再锁按下时刻的 ddx/ddy —— goodix 时序 BTN_TOUCH 先于 ABS,按下边沿时坐标可能
* 还是 -1/上一手势残留,锁原点会把点击判成「点卡片外=取消」。命中改在抬手时用抬手时刻的
* dlx/dly(抬起时必已更新到本次最新有效坐标),与 meow_touch mt_up 语义同源。 */
/* 初始绘制:全屏压暗覆盖层 + 居中圆角卡片(fill 后画空心圆角描边) */
meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150);
meow_gfx_fill_round(d_x, d_y, d_w, d_h, 16, th->surface_container);
meow_ui_draw_rect_round(d_x, d_y, d_w, d_h, 16, th->outline);
if (mfont_ready) ui_draw(d_x + 18, d_y + 8, title, th->on_surface, 20);
else draw_text(d_x + 18, d_y + 8, title, 2, th->on_surface);
for (int i = 0; i < n; i++) {
int ry = d_y + title_h + i * opt_row;
int cur_sel = (i == sel);
if (cur_sel) meow_ui_fill_rect_alpha(d_x + 4, ry, d_w - 8, opt_row, th->primary, 36);
ui_draw(d_x + 18, ry + (opt_row - meow_font_line_height(20)) / 2, item[i] ? item[i] : "",
cur_sel ? 0xFFFFFF : th->on_surface, 20);
if (i == cur) meow_ui_fill_rect(d_x + d_w - 28, ry + (opt_row - 12) / 2, 12, 12, th->primary);
}
meow_dirty_full(); meow_ui_flush();
for (;;) {
if (sigterm_flag) { result = -1; break; }
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int vold = 0, volu = 0, pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
}
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu); (void)kd_tick(&kd_pwr, pwr);
vold = kd_vold.stable; volu = kd_volu.stable; pwr = kd_pwr.stable;
int need = 0;
if (vold && !m_prev_vold && sel < n - 1) { sel++; need = 1; }
if (volu && !m_prev_volu && sel > 0) { sel--; need = 1; }
if (pwr && !m_prev_pwr) pwr_pending = 1; /* 按下:待抬起确认 */
if (!pwr && m_prev_pwr && pwr_pending) { pwr_pending = 0; result = sel; break; } /* 抬起=确认 */
m_prev_vold = vold; m_prev_volu = volu; m_prev_pwr = pwr;
/* 触摸:抬起命中选项=确认,点卡片外=取消 */
if (tfd >= 0) {
struct pollfd pfd = { tfd, POLLIN, 0 };
if (poll(&pfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == 16) {
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) dlx = val;
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) dly = val;
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val) {
/* 按下边沿:只标「已按下」,绝不锁原点(goodix 时序 BTN_TOUCH 先于
* ABS,此刻 dlx/dly 可能还是 -1 或上一手势残留)。 */
dtd = 1;
} else if (dtd) {
dtd = 0;
/* 抬手判定:此刻 dlx/dly 必为本次最新有效坐标(抬起时 ABS 已先到)。
* 卡片内命中 → 返回选项下标;卡片外 → 取消(-1);从未见有效坐标 → 丢弃。 */
if (dlx >= 0 && dly >= 0) {
if (dlx >= d_x && dlx < d_x + d_w && dly >= d_y && dly < d_y + d_h) {
int i = (dly - d_y - title_h) / opt_row;
if (i >= 0 && i < n) { sel = i; result = sel; goto ddone; }
/* 卡片内但落在标题/内边距:保持弹出态,等待下一次点选 */
} else {
result = -1; goto ddone; /* 点卡片外=取消 */
}
}
/* dlx/dly 为 -1:ghost/残留抬手,丢弃不动作 */
}
}
}
}
}
if (need) {
/* 重绘覆盖层+选项(仅 sel 高亮变化) */
meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150);
meow_gfx_fill_round(d_x, d_y, d_w, d_h, 16, th->surface_container);
meow_ui_draw_rect_round(d_x, d_y, d_w, d_h, 16, th->outline);
if (mfont_ready) ui_draw(d_x + 18, d_y + 8, title, th->on_surface, 20);
for (int i = 0; i < n; i++) {
int ry = d_y + title_h + i * opt_row;
int cur_sel = (i == sel);
if (cur_sel) meow_ui_fill_rect_alpha(d_x + 4, ry, d_w - 8, opt_row, th->primary, 36);
ui_draw(d_x + 18, ry + (opt_row - meow_font_line_height(20)) / 2, item[i] ? item[i] : "",
cur_sel ? 0xFFFFFF : th->on_surface, 20);
if (i == cur) meow_ui_fill_rect(d_x + d_w - 28, ry + (opt_row - 12) / 2, 12, 12, th->primary);
}
meow_dirty_full(); meow_ui_flush();
}
usleep(POLL_MS * 1000);
}
ddone:
if (tfd >= 0) close(tfd);
kp_close(&kp_power); kp_close(&kp_keys);
return result;
}
/* ---- 字体行「family→字重」两级子菜单(M2.6 字重结构)----
* 一级:列出扫描到的去重 family(class 行首项「跟随全局」=空值);选 family 后自动进入
* 二级:该 family 的可用字重(regular/bold/italic…)。选中写 out_val = 卡根相对路径
* MeowOS/fonts/<family>/<weight>/font.png(或 class 行「跟随全局」=空串)。
* 复用 s_dd_modal(与 WiFi/关于 同款模态)。返回 1=已选中(out_val 已填),0=取消。 */
static int s_font_dropdown(const srow_t *r, char *out_val, int vlen)
{
if (!r) return 0;
int is_cls = r->key && !strncmp(r->key, "font.class_", 11);
char cur[MEOW_SET_MAX_LEN];
meow_settings_get_str(r->key, cur, sizeof cur);
char curfam[64] = "", curwt[32] = "";
if (cur[0])
font_family_weight_from_png(cur, curfam, sizeof curfam, curwt, sizeof curwt);
/* ---- 一级:去重 family 列表(class 行首项「跟随全局」)---- */
char fam_slug[S_NET_MAX][64], fam_disp[S_NET_MAX][64];
int famn = 0;
if (is_cls) {
if (famn < S_NET_MAX) { snprintf(fam_slug[famn], sizeof fam_slug[0], "");
snprintf(fam_disp[famn], sizeof fam_disp[0], "跟随全局字体"); famn++; }
}
for (int i = 0; i < font_presets_n && famn < S_NET_MAX; i++) {
const char *f = font_preset_fams[i] ? font_preset_fams[i] : "";
int dup = 0;
for (int k = 0; k < famn; k++) if (!strcmp(fam_slug[k], f)) { dup = 1; break; }
if (dup) continue;
snprintf(fam_slug[famn], sizeof fam_slug[0], "%s", f);
snprintf(fam_disp[famn], sizeof fam_disp[0], "%s",
(font_preset_names[i] && font_preset_names[i][0]) ? font_preset_names[i] : f);
famn++;
}
if (famn <= 0) return 0;
const char *fitem[S_NET_MAX];
char ftext[S_NET_MAX][64];
for (int i = 0; i < famn; i++) { snprintf(ftext[i], sizeof ftext[i], "%s", fam_disp[i]); fitem[i] = ftext[i]; }
int cur_fam = is_cls ? 0 : -1;
for (int i = is_cls ? 1 : 0; i < famn; i++)
if (curfam[0] && !strcmp(fam_slug[i], curfam)) cur_fam = i;
int pick = s_dd_modal(r->label, fitem, famn, cur_fam);
if (pick < 0) return 0;
if (is_cls && pick == 0) { snprintf(out_val, vlen, ""); return 1; } /* 跟随全局 */
const char *fam = fam_slug[pick];
/* ---- 二级:该 family 的字重列表(值=完整卡根相对路径,显示=label)---- */
char wt_val[S_NET_MAX][MEOW_SET_MAX_LEN];
char wt_text[S_NET_MAX][96];
int wn = 0;
for (int i = 0; i < font_presets_n && wn < S_NET_MAX; i++) {
if (!font_preset_fams[i] || strcmp(font_preset_fams[i], fam)) continue;
snprintf(wt_val[wn], sizeof wt_val[0], "%s", font_presets[i]);
snprintf(wt_text[wn], sizeof wt_text[0], "%s", font_preset_labels[i]);
wn++;
}
if (wn <= 0) return 0; /* 该 family 无字重(理论不出现:能进 family 必已探到字重) */
const char *witem[S_NET_MAX];
for (int i = 0; i < wn; i++) witem[i] = wt_text[i];
int cur_wt = -1;
for (int i = 0; i < wn; i++) if (cur[0] && !strcmp(wt_val[i], cur)) cur_wt = i;
int wp = s_dd_modal(r->label, witem, wn, cur_wt);
if (wp < 0) return 0;
snprintf(out_val, vlen, "%s", wt_val[wp]);
return 1;
}
/* ---- 运行设置页(独立子循环;run_menu 触摸命中设置行后调用)---- */
static void run_settings_page(void)
{
int sel = s_first_setting();
int scroll_off = 0;
int H = fb_yres;
/* M2.6:进设置页扫描一次 TF 卡 MeowOS/fonts/<family>/<weight>/,刷新「字体文件」下拉。
* 进设置时卡通常在位;此步只枚举/校验,不加载(加载由 meow_font_ensure 按 font.file 决定)。 */
meow_font_scan_presets();
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
}
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
unsigned long long touch_cooldown_until = now_ms() + 300;
unsigned long long last_tick = now_ms();
unsigned long long last_act = now_ms(); /* 最近一次交互时间(自动息屏用) */
int after_modal = 0; /* 选择器子对话框刚关闭:其全屏 dim 覆盖过状态栏,需整帧重绘还原 */
/* 触摸拖动滑条状态 */
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
int drag = -1; /* 被拖动的行索引;-1=无 */
/* M2.37 列表拖动滚动状态 */
int scroll_drag = 0; /* 是否处于列表拖动滚动手势 */
int scroll_base = 0; /* 进入滚动时对应的 scroll_off 基准(连续跟手) */
int scroll_y0 = 0; /* 按下原点纵向坐标 */
int scroll_invert = 0; /* M2.38 Bug4:滑动方向切换(input.swipe_invert;0=标准方向) */
int sel_changed = 0; /* 本轮是否改变了选中行(仅此场景才强制选中行可见) */
int wake_suppress = 0; /* 自动息屏唤醒后:吸收唤醒键边沿,待全部抬起再接管 */
/* M2.x(KEY_CONTROL_DESIGN §2)电源键状态:设置页电源键 = 系统级语义(非返回),
* 对照主界面 run_menu 3130-3137 的同名变量。单击=息屏/亮屏、长按=电源菜单、8s=强关。 */
unsigned long long pwr_hold_at = 0; /* 本次电源按下计时起点 */
int pwr_menu_state = 0; /* 是否已因此次 hold 弹过电源菜单 */
int pwr_woke = 0; /* 本次按下=自动息屏唤醒(释放只唤醒不 toggle) */
int pwr_suppress_click = 0; /* 菜单刚关闭:松开不得误判单击熄屏(M2.34 同款) */
/* 首次全帧绘制 */
s_net_sync_from_sys(); /* M2.39:WiFi/蓝牙开关行同步真实硬件态 + 刷新网络信息缓存 */
scroll_off = s_clamp_scroll(sel, scroll_off);
draw_settings_page(sel, scroll_off);
meow_dirty_full(); meow_ui_flush();
for (;;) {
unsigned long long now = now_ms();
if (sigterm_flag) break;
/* 重绘触发:值/选中/滚动变化 */
int need_paint = 0;
/* ---- M2.37 自动息屏态:留在设置页,等待任意输入唤醒,恢复后重绘(不丢上下文)----
* 对照 run_menu 主循环 meow_auto_slept 的唤醒重绘(M2.17/2.18)。休眠期间仅探测
* 输入(按键原始按下 / black 模式触摸按下);一旦唤醒:亮屏、清自动态并整帧重绘。 */
if (meow_auto_slept && !meow_screen_on) {
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int wake = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) wake = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) wake = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) wake = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) wake = 1;
}
if (!wake && screen_off_mode == 1 && tfd >= 0) { /* black 模式:面板仍上电,触摸可唤醒 */
struct pollfd spfd = { tfd, POLLIN, 0 };
if (poll(&spfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == 16) {
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_KEY && code == BTN_TOUCH && val) wake = 1;
}
}
}
if (wake) {
meow_panel_on();
meow_auto_slept = 0;
wake_suppress = 1;
last_act = now_ms();
/* 重置按键防抖与 m_prev_*(含唤醒键原始态对齐),并丢唤醒的这次触摸 */
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
touch_cooldown_until = now_ms() + 300;
td = 0; td_ok = 0; drag = -1; scroll_drag = 0; tlx = tly = -1;
logmsg("settings: auto screen-off -> input wake, stayed in settings page");
scroll_off = s_clamp_scroll_range(scroll_off);
draw_settings_page(sel, scroll_off);
meow_dirty_full(); meow_ui_flush();
}
usleep(POLL_MS * 1000);
continue;
}
/* ---- 按键 ---- */
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int vold = 0, volu = 0, play = 0, pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
}
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu);
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
vold = kd_vold.stable; volu = kd_volu.stable;
play = kd_play.stable; pwr = kd_pwr.stable;
if (wake_suppress) {
/* 刚被唤醒:吸收唤醒键的边沿,等所有按键完全抬起后再接管动作,
* 避免「电源唤醒→抬起→被当成电源单击返回」或音量唤醒→误导航。 */
if (!pwr && !vold && !volu && !play) wake_suppress = 0;
} else {
/* M2.39:设置页音量键 = 调系统音量(写 DAC),不做光标移动(用户约束:
* 设置页仅触摸导航;音量键在电菜单/应用列表的系统级导航保持不动)。
* 按下边沿即起线性渐变(vol_ramp_begin),下方 vol_ramp_tick 逐 1% 爬 + 刷状态栏。 */
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
/* M2.x(KEY_CONTROL_DESIGN §2/§3)播放键在设置页同样被系统「抓住」:
* 捕获+日志+消费,不再映射为「返回/确认」(原 play 边沿=break 已删,播放键不再退出设置页)。
* 仍视为输入(重置自动息屏计时,与主界面 play 的媒体预留/any_input 一致)。 */
if (play && !m_prev_play) {
logmsg("settings: play key caught (media-reserved, no exit)");
last_act = now;
}
/* M2.x(KEY_CONTROL_DESIGN §2)电源键 = 系统级语义,对齐主界面 run_menu 3284-3366:
* 单击(按<长按阈值后松开)= 息屏/亮屏;长按>=longpress_menu_ms = 电源菜单;
* 按住>=forceoff_ms(且未开过菜单)= 强制关机;自动息屏按下只唤醒。
* ※ 电源键在设置页不再映射为「返回」——退出只能靠触摸(底部返回按钮 + 左缘左滑)。 */
if (pwr && !m_prev_pwr) {
pwr_hold_at = now; pwr_menu_state = 0;
pwr_suppress_click = 0; /* M2.34:新一次电源按下清除旧的抑制标志 */
last_act = now; /* 视为输入,防按住期间误触发自动息屏 */
/* M2.18 Bug 2 同款:按下正落在自动息屏态(auto_slept 且屏已关)→ 本次只做唤醒 */
if (meow_auto_slept && !meow_screen_on) pwr_woke = 1;
}
if (pwr && pwr_hold_at) {
/* M2.10/M2.11 B/M2.37 同款:8s 强关只在该「按住」从未开过菜单(!pwr_menu_state)
* 时触发;开过菜单则交给菜单内确认/取消,杜绝「开菜单->取消->仍按住->强关」残留。 */
if (now - pwr_hold_at >= power_forceoff_ms && !pwr_menu_state) {
if (sim_no_exec) { logmsg("settings pm: [sim-no-exec] POWER HOLD %dms -> would force poweroff", power_forceoff_ms); }
else { logmsg("settings pm: POWER HOLD %dms -> force poweroff", power_forceoff_ms); sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); }
/* 关机一般不复返;若 reboot 失败返回仍按按住处理(m_prev 由下方统一复写) */
} else if (now - pwr_hold_at >= power_longpress_menu_ms && !pwr_menu_state) {
pwr_menu_state = 1;
logmsg("settings pm: POWER HOLD %dms -> show power menu", power_longpress_menu_ms);
show_power_menu(); /* 动作在函数内 system();取消则续循环 */
last_act = now_ms(); /* 菜单返回即视为「刚有输入」,重置自动息屏计时 */
/* M2.11 B 同款:菜单已关闭。清空按住计时与菜单态,绝不把已互动的 hold 再累计到 8s 强关。 */
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击息屏 */
/* M2.25 同款:阻塞式 show_power_menu() 期间 kd_* 冻结,m_prev_* 与稳定值可能错配,
* 重播 kd_* 并以当前 raw 对齐 m_prev_*,杜绝「菜单内触摸点取消->返回后被误判单击息屏」。 */
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
after_modal = 1; /* 全屏 dim 覆盖过状态栏,需整帧重绘还原 */
need_paint = 1;
/* 已按 raw 对齐 m_prev_*,跳过下方基线复写,防把 raw 值覆盖回旧稳定值 → 假单边沿 */
goto settings_keys_seeded;
}
}
if (!pwr && m_prev_pwr) {
/* 松开:短按(<长按阈值且菜单未开)→ 单击 = 息屏/亮屏切换 */
if (pwr_suppress_click) {
logmsg("settings pm: power release after power-menu -> no toggle (suppress)");
} else if (pwr_hold_at && (now - pwr_hold_at < power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) {
logmsg("settings pm: power single-click -> toggle panel (was %s)", meow_screen_on ? "on" : "off");
meow_toggle_panel();
meow_auto_slept = 0; /* M2.17:手动/自动息屏后电源单击唤醒均清自动态 */
last_act = now_ms();
need_paint = 1; /* 亮屏重绘 / 息屏留屏(面板已黑,下次亮屏重绘) */
} else if (pwr_woke) {
logmsg("settings pm: power release after auto screen-off wake -> no toggle (wake only)");
}
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
pwr_suppress_click = 0; /* M2.34:电源已松开,抑制标志一次性用完即清 */
}
}
/* M2.39:驱动音量线性渐变(长按续爬/松开即止)+ 刷新状态栏 VOL;「音量」行随 need_paint 更新 */
if (vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0) ? volu : vold;
int going = vol_ramp_tick(held);
g_sys.vol = settings_volume_get();
paint_sb_right();
need_paint = 1;
if (!going) logmsg("settings vol ramp end -> %d", g_sys.vol);
}
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr;
settings_keys_seeded:
;
/* ---- 触摸 ---- */
if (tfd >= 0 && meow_screen_on) { /* M2.x:手动电源息屏后不处理触摸(与主界面一致,且防屏黑时误退出/误改值) */
struct pollfd pfd = { tfd, POLLIN, 0 };
if (poll(&pfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == 16) {
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) tlx = val;
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val;
if (td && !td_ok && tlx >= 0 && tly >= 0) {
tdx = tlx; tdy = tly; td_ok = 1;
scroll_y0 = tdy; /* 列表拖动基准:按下原点(触发时才锁 scroll_base) */
/* 按下原点落在选中滑条 track 带内 → 开始拖动 */
if (SMENU[sel].ctrl == SC_SLIDER && s_sel_track_y >= 0 &&
tlx >= S_SLIDER_X - 12 && tlx <= S_SLIDER_X + S_SLIDER_W + 12 &&
tly >= s_sel_track_y - 14 && tly <= s_sel_track_y + 20) {
drag = sel;
const srow_t *r = &SMENU[sel];
int v = s_slider_val_from_x(r, tlx);
s_slider_set_from_track(r, v, 0);
need_paint = 1;
}
}
/* M2.37 列表拖动滚动:非滑条行按下后纵向位移超阈值 → 进入列表滚动。
* scroll_base 在触发瞬间反推,保证进入滚动那一刻不跳变(连续跟手)。
* M2.38 Bug4:默认标准滑动(手指上滑 → scroll_off 增大 = 内容往下翻看后面项);
* 方向可经 input.swipe_invert 切换(1=反向:手指上滑 → 内容上移)。 */
if (td_ok && drag < 0 && !scroll_drag) {
int ddy = tly - scroll_y0;
if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) {
scroll_drag = 1;
meow_settings_get_bool("input.swipe_invert", &scroll_invert);
scroll_base = scroll_off + (scroll_invert ? -ddy : ddy);
}
}
if (scroll_drag && td_ok) {
int dydt = tly - scroll_y0;
scroll_off = s_clamp_scroll_range(scroll_base + (scroll_invert ? dydt : -dydt));
need_paint = 1;
}
if (drag >= 0 && td_ok) {
const srow_t *r = &SMENU[drag];
int v = s_slider_val_from_x(r, tlx);
s_slider_set_from_track(r, v, 0); /* 拖动实时 set_*,抬起才落盘 */
need_paint = 1;
}
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val && !td) {
if (now_ms() < touch_cooldown_until) {
tlx = tly = -1; /* 冷却丢弃 */
} else {
td = 1; td_ok = 0; tlx = -1; tly = -1; drag = -1; scroll_drag = 0;
}
} else if (!val && td) {
td = 0;
if (drag >= 0) { /* 滑条拖动结束 → 落盘 */
const srow_t *r = &SMENU[drag];
s_commit(r, 1);
drag = -1;
need_paint = 1;
} else if (scroll_drag) {
scroll_drag = 0; /* 列表滚动结束(已实时重绘),不再当 tap */
scroll_base = 0; scroll_y0 = 0;
need_paint = 1;
} else if (td_ok) {
int dx = tlx - tdx, dy = tly - tdy;
/* M2.39 左缘右滑返回:复用全局左缘返回判定 meow_edge_back()
* (与 SDK 全局左缘返回 A-0 同语义:左缘起点 + 右滑≥MEOW_EDGE_SWIPE
* 且横向主导),避免设置页自造一套重复判定。 */
if (meow_edge_back(tdx, dx, dy)) {
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
goto done;
}
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
if (adx < 60 && ady < 60) { /* 视为 tap */
int tx = tdx, ty = tdy;
/* 底部返回按钮 */
if (ty >= H - 52 && tx >= SET_CARD_X && tx <= SET_CARD_X + 120) {
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
goto done;
}
int cy = ty - SET_CONTENT_Y + scroll_off;
int idx = s_row_at_content_y(cy);
if (idx >= 0 && !s_setting_idx(idx)) {
/* 点分组标题行 → 仅选中下一设置行 */
int ns = s_next_setting(idx);
sel = ns; need_paint = 1; sel_changed = 1;
} else if (idx >= 0) {
const srow_t *r = &SMENU[idx];
sel = idx; need_paint = 1; sel_changed = 1;
if (r->ctrl == SC_SWITCH) {
int on = 0; meow_settings_get_switch(r->key, &on);
meow_settings_set_switch(r->key, !on);
s_commit(r, 1);
} else if (r->ctrl == SC_LIST) {
/* M2.39 网络列表行:开下拉(重新解析)+ 重绘 + 排空本页 tfd
* (复刻 s_dropdown 后的穿透防护,见下方 ENUM/PRESET 分支)。 */
after_modal = 1;
s_net_open(r);
if (tfd >= 0) {
unsigned char e[16];
while (read(tfd, e, sizeof e) == 16) {}
usleep(40000);
while (read(tfd, e, sizeof e) == 16) {}
}
td = 0; td_ok = 0; drag = -1; scroll_drag = 0;
scroll_base = 0; scroll_y0 = 0;
tdx = tdy = tlx = tly = -1;
touch_cooldown_until = now_ms() + 300;
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
need_paint = 1;
} else if (r->ctrl == SC_ENUM || r->ctrl == SC_PRESET) {
/* 单次点击即开选择器子对话框 */
after_modal = 1; /* 关闭后整帧重绘(还原被 dim 的状态栏) */
if (s_row_is_font(r)) {
/* M2.6 字重:字体行走「family→字重」两级子菜单 */
char fval[MEOW_SET_MAX_LEN];
if (s_font_dropdown(r, fval, sizeof fval)) {
meow_settings_set_str(r->key, fval);
s_commit(r, 1);
}
} else {
int pick = s_dropdown(r);
if (pick >= 0) {
if (r->ctrl == SC_ENUM) meow_settings_set_enum(r->key, pick);
else meow_settings_set_str(r->key, s_preset_value(r, pick));
s_commit(r, 1);
}
}
/* M2.38 Bug3:s_dropdown 返回后彻底排空本页 tfd +
* 重置触摸基线。根因:input 子系统把事件广播到所有
* 打开的 fd —— s_dropdown 用独立 fd 消费选项 tap,但
* 本页 tfd 同样收到了整套下拉会话事件;若不清空,
* 外层 while(read) 会把它们当新 tap → 穿透到下层设置项。
* 故 排空→40ms→再排空 + 重置触摸状态 + 冷却窗口吸收长尾。 */
if (tfd >= 0) {
unsigned char e[16];
while (read(tfd, e, sizeof e) == 16) {}
usleep(40000);
while (read(tfd, e, sizeof e) == 16) {}
}
td = 0; td_ok = 0; drag = -1; scroll_drag = 0;
scroll_base = 0; scroll_y0 = 0;
tdx = tdy = tlx = tly = -1;
touch_cooldown_until = now_ms() + 300;
/* 重置设置页按键防抖基线,防残边 */
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
need_paint = 1;
} else if (r->ctrl == SC_SLIDER && tlx >= 0) {
int v = s_slider_val_from_x(r, tlx);
s_slider_set_from_track(r, v, 1);
}
}
}
}
td_ok = 0;
}
}
}
}
}
/* ---- 滚动 + 重绘 ---- */
if (need_paint) {
last_act = now;
/* 仅选中行变化时强制其可见;列表拖动滚动时保留用户滚到的位置,不把选中拉回视野 */
if (sel_changed) { scroll_off = s_clamp_scroll(sel, scroll_off); sel_changed = 0; }
draw_settings_page(sel, scroll_off);
/* 局部标脏:标题+卡片+滚动条+底部(y=42 以下全部,覆盖主题/选中/值变化),
* 状态栏(0-28)由 draw_statusbar/draw_clock 各自维护,不在此列。
* 选择器关闭后(after_modal)其全屏 dim 把状态栏也压暗了,须整帧重绘还原。 */
if (after_modal) { meow_dirty_full(); after_modal = 0; }
else meow_dirty_add(0, 42, fb_xres, fb_yres - 42);
meow_ui_flush();
}
/* M2.37 超时自动息屏(与主界面一致):空闲 > screen_off_timeout → 关屏,但留在设置页:
* 置 meow_auto_slept 进入休眠态,由顶部睡眠门等待任意输入唤醒并重绘设置页、不丢上下文
* (对照 run_menu 外层 meow_auto_slept 的唤醒重绘,M2.17/2.18 同款语义)。 */
if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept &&
(unsigned long long)(now - last_act) >= (unsigned long long)screen_off_timeout * 1000) {
meow_panel_off();
meow_auto_slept = 1;
wake_suppress = 1; /* 唤醒输入边沿待完全抬起后再接管,避免误动作 */
logmsg("settings: idle %ds -> auto screen-off (stay in settings)", screen_off_timeout);
}
/* ~1s 状态栏时钟刷新 */
if (now - last_tick >= 1000) {
last_tick = now;
draw_clock();
fb_norm_buf0();
meow_ui_flush();
}
usleep(POLL_MS * 1000);
}
done:
if (tfd >= 0) close(tfd);
kp_close(&kp_power); kp_close(&kp_keys);
logmsg("settings page closed");
}
/* ================================================================== */
/* 阶段B:通知中心面板(状态栏下滑,系统级叠加层,§6.1 ③ 仅框架内部) */
/* - 入口:run_menu 顶部热区下滑 → run_notif_center 阻塞子循环。 */
/* - 结构:顶部把手/标题 + 快捷开关行(WiFi/蓝牙/主题/亮度/音量) + */
/* 亮度/音量滑条 + 通知列表(app 图标+标题+内容+时间+进度)。 */
/* - 只读写 meow_settings_* / meow_notif 存储,零旁路;MD3 接 s_ui_theme()。*/
/* - 手势:面板内上滑/点空白/左缘左滑关闭(左缘复用 meow_edge_back)。 */
/* - 物理键:系统语义不变(电源=息屏/亮屏/菜单/8s强关、音量=调音量、 */
/* 播放=媒体预留),绝不映射为返回/确认/exit(APPS §3.4.1)。 */
/* - 性能:打开/交互只标面板体区域脏(状态栏 0-27 由 draw_statusbar */
/* 单独维护),绝不整屏重绘;滑条拖动实时 set_*(后端 clamp),抬手落盘。*/
/* ================================================================== */
/* ---- 面板布局常量 ---- */
#define NCP_TOP SB_BAR_H /* 28:面板顶缘(状态栏下) */
#define NCP_GRAB_Y 32
#define NCP_TITLE_Y 46
#define NCP_CHIP_Y 86
#define NCP_CHIP_H 44
#define NCP_CHIP_W 70
#define NCP_CHIP_GAP 8
#define NCP_CHIP_N 5 /* ⑥:快捷 chip 扩充为 5 个(+电源菜单) */
#define NCP_SLIDER_X 20
#define NCP_SLIDER_W (fb_xres - 40)
#define NCP_SL1_Y 162 /* 亮度滑条 track 顶(label 在 -26 =136) */
#define NCP_SL2_Y 214 /* 音量滑条 track 顶(label 在 -26 =188) */
#define NCP_DIV_Y 258
#define NCP_DND_Y 264 /* ⑥:勿扰面板内开关带顶缘(分隔线下方) */
#define NCP_DND_H 40 /* 勿扰开关带高(label+状态+s_draw_switch 48×28) */
#define NCP_LIST_TOP 308 /* 通知列表顶缘(原 266 → 下移 42 给勿扰带) */
#define NCP_LIST_BOT 694
#define NCP_ROW_H 92
#define NCP_ROW_X 12
#define NCP_ROW_W (fb_xres - 24)
#define NCP_ICON_X 26
#define NCP_TEXT_X 72
#define NCP_TEXT_W (NCP_ROW_W - (NCP_TEXT_X - NCP_ROW_X) - 10)
#define NCP_FOOT_Y 700
#define NCP_MAX_ROWS 24 /* 面板一次缓冲的通知行上限(防御) */
#define NCP_SCROLL_REBOUND_TOL 48 /* px:列表拖到顶/底仍继续拖过此容差 → 判回弹(关闭语义),
避免在边界小幅抖动即误关(> S_SCROLL_SLOP=40) */
/* ---- ⑦ 单条通知行左滑删除(跟手 + 半程可回 + 回弹)---- */
#define NCP_SWIPE_SLOP 24 /* px:行内水平位移达此值且横向主导+左移 → 进行左滑(>阈值) */
#define NCP_SWIPE_RATE 3 /* px/ms:行左滑落位动画线性速率(回弹/滑出) */
#define NCP_SWIPE_DELETE_FRAC 40 /* %:行宽百分比阈值(│off│>=row_w*40/100 → 删) */
#define NCP_SWIPE_DELETE_MIN 110 /* px:绝对像素阈值(│off│>=110 → 删;OR 语义,窄行由 frac 起作用) */
/* 勿扰过滤阈值:dnd=1 只显示 urgency>=2(HIGH=2/CRITICAL=3) */
#define NCP_DND_MIN_URGENCY 2
/* ⑧ 一键清除(勿扰对称处):与勿扰带同排(NCP_DND_Y/H),x 对半(中线=fb_xres/2)。
* 勿扰=左半段 [NCP_ROW_X, fb_xres/2);清除=右半段 [fb_xres/2, fb_xres-NCP_ROW_X)。 */
#define NCP_DND_SPLIT_X (fb_xres / 2) /* 勿扰/清除带分割中线(左右对称) */
/* ---- 开关平移动画(§9:状态时间轴 + 快照 blit 平移,不整屏重绘)---- */
#define NCP_ANIM_FIX 1024 /* 进度 p 的定点分母(1/1024 精度) */
#define NCP_ANIM_MS 180 /* 单程打开/关闭时长(ms,真机可调) */
/* chip 索引(⑥:第 5 个 = 电源菜单,点击唤起既有系统电源菜单 show_power_menu) */
enum { NCP_CHIP_WIFI=0, NCP_CHIP_BT=1, NCP_CHIP_THEME=2, NCP_CHIP_BRIGHT=3,
NCP_CHIP_POWER=4, NCP_CHIP_COUNT=5 };
/* 通知行快照(面板读取字段 + 缓存的 icon 句柄)。 */
typedef struct {
uint32_t id;
char app_id[MEOW_NOTIF_APP_ID_MAX];
char summary[MEOW_NOTIF_SUMMARY_MAX];
char body[MEOW_NOTIF_BODY_MAX];
char icon[MEOW_NOTIF_ICON_MAX];
char target[MEOW_NOTIF_ACTIVITY_MAX];
uint64_t created_ms;
int progress;
uint8_t urgency;
meow_img *img; /* 缓存图标(meow_img_load_file 结果;NULL=回退首字符圆点) */
} ncp_row_t;
static ncp_row_t g_rows[NCP_MAX_ROWS];
static int g_nrows = 0;
static struct app *g_ncp_apps = NULL; /* run_notif_center 注入(通知点击找 app 用) */
static int g_ncp_napps = 0;
/* ⑥ 勿扰面板内开关:当前 dnd 态(开面板/切换时从 meow_settings notify.dnd 读/写,零旁路)。
* ncp_collect_cb / ncp_active_count 按它过滤行(only urgency>=NCP_DND_MIN_URGENCY when on)。 */
static int g_ncp_dnd = 0;
/* ⑦ 行左滑删除的绘制态(供 ncp_draw_list / ncp_repaint_row 画该行位移)。
* g_ncp_swipe_row = 正在左滑的行下标(-1=无);g_ncp_swipe_off = 该行左移量(px,<=0)。
* run_notif_center 的手势状态机(row_swipe_row/row_off/row_settle/row_off_target/row_anim_at/row_paint)
* 与其同步,绘制只读这组全局,避免给 draw_notif_center/ncp_draw_list 加参数。 */
static int g_ncp_swipe_row = -1;
static int g_ncp_swipe_off = 0;
/* 短串拷贝(同 meow_notif.c 语义:保证 NUL + 截断) */
static void ncp_strncpy(char *dst, const char *src, size_t len)
{
if (!dst || len == 0) return;
if (!src) { dst[0] = 0; return; }
strncpy(dst, src, len - 1);
dst[len - 1] = 0;
}
/* app_id → 显示名(apps[] 匹配则用 name,否则原样 app_id;无则空串) */
static const char *ncp_app_name(const char *app_id)
{
if (app_id && g_ncp_apps)
for (int i = 0; i < g_ncp_napps; i++)
if (!strcmp(g_ncp_apps[i].id, app_id)) return g_ncp_apps[i].name;
return app_id ? app_id : "";
}
/* 通知图标加载回调:把 app_icon(相对卡根/绝对)解成绝对路径,加载 meow_img。 */
static void ncp_load_icon(ncp_row_t *r)
{
if (!r) return;
r->img = NULL;
if (!r->icon[0]) return;
char path[MEOW_NOTIF_ICON_MAX + 80];
if (r->icon[0] == '/') snprintf(path, sizeof path, "%s", r->icon);
else snprintf(path, sizeof path, "/usr/data/mnt/sd_0/%s", r->icon);
r->img = meow_img_load_file(path);
}
static int ncp_collect_cb(const meow_notif_t *n, void *ctx)
{
(void)ctx;
if (g_nrows >= NCP_MAX_ROWS) return 1; /* 满:中止遍历 */
/* 勿扰(⑥):dnd 开时只收集 urgency>=NCP_DND_MIN_URGENCY(HIGH/CRITICAL)。 */
if (!mnp_dnd_should_show(n->urgency, g_ncp_dnd)) return 0;
ncp_row_t *r = &g_rows[g_nrows++];
memset(r, 0, sizeof *r);
r->id = n->id;
ncp_strncpy(r->app_id, n->app_id, sizeof r->app_id);
ncp_strncpy(r->summary, n->summary, sizeof r->summary);
ncp_strncpy(r->body, n->body, sizeof r->body);
ncp_strncpy(r->icon, n->app_icon, sizeof r->icon);
ncp_strncpy(r->target, n->target_activity, sizeof r->target);
r->created_ms = n->created_ms;
r->progress = n->progress;
r->urgency = n->urgency;
ncp_load_icon(r);
return 0;
}
/* 释放全部缓存图标 + 清行数(面板关闭/列表刷新前用)。 */
static void ncp_rows_free(void)
{
for (int i = 0; i < g_nrows; i++)
if (g_rows[i].img) { meow_img_free(g_rows[i].img); g_rows[i].img = NULL; }
g_nrows = 0;
}
/* 重新采集通知列表(比 meow_notif_count 变化时调用)。 */
static int ncp_refresh_rows(void)
{
ncp_rows_free();
meow_notif_list(NULL, ncp_collect_cb, NULL);
return g_nrows;
}
/* 当前应显示的活跃通知数(按 g_ncp_dnd 过滤,与 g_rows 同口径)——供变化检测/重采集用。 */
static int ncp_count_cb(const meow_notif_t *n, void *ctx)
{
if (!mnp_dnd_should_show(n->urgency, g_ncp_dnd)) return 0;
(*(int *)ctx)++;
return 0;
}
static int ncp_active_count(void)
{
int c = 0;
meow_notif_list(NULL, ncp_count_cb, &c);
if (c > NCP_MAX_ROWS) c = NCP_MAX_ROWS;
return c;
}
/* 通知列表是否变化(按过滤后活跃数比;新条/撤下/TTL 释放都反映在 count,含 dnd 过滤)。 */
static int ncp_rows_changed(void)
{
return ncp_active_count() != g_nrows;
}
/* ---- 开关平移动画辅助(§9:快照 blit + 平移;只在 run_notif_center 内分配/释放)----
* g_ncp_ph = 面板高 PH = fb_yres - NCP_TOP;两块快照各 PH × fb_line 字节:
* bg_snap = 进入面板时(绘制面板前)从 back_buf 行 [NCP_TOP, NCP_TOP+PH) 拍的背景(应用列表);
* panel_snap= 最终位置整面板渲染(draw_notif_center 后拍),稳态重绘后刷新。
* 快照不可用(分配失败/无 fb)时动画降级为瞬时显示/消失(fallback)。 */
static unsigned char *g_ncp_bg_snap = NULL;
static unsigned char *g_ncp_panel_snap = NULL;
static int g_ncp_ph = 0;
static void ncp_snap_init(void)
{
int ph = fb_yres - NCP_TOP;
if (ph <= 0 || fb_line <= 0) { g_ncp_ph = 0; return; }
size_t bsz = (size_t)ph * (size_t)fb_line;
g_ncp_bg_snap = malloc(bsz);
g_ncp_panel_snap = malloc(bsz);
if (!g_ncp_bg_snap || !g_ncp_panel_snap) {
free(g_ncp_bg_snap); g_ncp_bg_snap = NULL;
free(g_ncp_panel_snap); g_ncp_panel_snap = NULL;
g_ncp_ph = 0;
return;
}
g_ncp_ph = ph;
}
static void ncp_snap_free(void)
{
free(g_ncp_bg_snap); g_ncp_bg_snap = NULL;
free(g_ncp_panel_snap); g_ncp_panel_snap = NULL;
g_ncp_ph = 0;
}
/* 从 back_buf 行 [NCP_TOP, NCP_TOP+PH) 拷贝到 dst(行宽 fb_line)。 */
static void ncp_snap_copy_rows(unsigned char *dst, const unsigned char *base)
{
if (!dst || g_ncp_ph <= 0) return;
size_t row = (size_t)fb_line;
for (int i = 0; i < g_ncp_ph; i++)
memcpy(dst + (size_t)i * row, base + (size_t)i * row, row);
}
/* 拍背景(应用列表):进入面板时、面板绘制前调用。 */
static void ncp_snap_bg(void)
{
ncp_snap_copy_rows(g_ncp_bg_snap, fb + (size_t)NCP_TOP * fb_line);
}
/* 拍最终整面板:draw_notif_center 画完后 / 每次稳态重绘后调用。 */
static void ncp_snap_panel(void)
{
ncp_snap_copy_rows(g_ncp_panel_snap, fb + (size_t)NCP_TOP * fb_line);
}
/* 重建动画帧:把 back_buf 面板区 [NCP_TOP..) 还原为「背景 + 平移后的面板」。
* off∈[-PH,0];prev_bot=上一帧覆盖底缘(exclusive),用于关(收窄)时恢复新露出背景条。
* 返回本帧覆盖底缘(exclusive,供调用方算脏区与传给下次)。 */
static int ncp_anim_rebuild(int off, int prev_bot)
{
if (!g_ncp_bg_snap || !g_ncp_panel_snap || g_ncp_ph <= 0) return NCP_TOP;
int top = NCP_TOP;
int ph = g_ncp_ph;
int cur_bot = mnp_anim_covered_bot(top, ph, off); /* ∈[top, top+ph](§9.3 裁切) */
if (cur_bot < top) cur_bot = top;
if (cur_bot > top + ph) cur_bot = top + ph;
size_t row = (size_t)fb_line;
/* 1) 面板收窄(收回中):[cur_bot, prev_bot) 新露出 → 恢复背景(应用列表)。 */
if (prev_bot > cur_bot) {
int b0 = prev_bot < top + ph ? prev_bot : top + ph;
for (int sy = cur_bot; sy < b0; sy++)
memcpy(fb + (size_t)sy * row,
g_ncp_bg_snap + (size_t)(sy - top) * row, row);
}
/* 2) 贴平移面板:[top, cur_bot) 从 panel_snap 行 yy = sy-(top+off) 拷回。 */
for (int sy = top; sy < cur_bot; sy++) {
int yy = sy - (top + off); /* ∈[0, ph)(见 §9.3 裁切论证) */
if (yy < 0) yy = 0;
if (yy >= ph) yy = ph - 1;
memcpy(fb + (size_t)sy * row,
g_ncp_panel_snap + (size_t)yy * row, row);
}
return cur_bot;
}
/* ⑨ 应用一帧面板动画(开关时间轴 §9 与跟手拖动 ⑨ 共用):把 back_buf 面板区重建为
* 「背景 + 平移后的面板」并标脏/呈现。p=当前进度;prev_bot=上一帧覆盖底缘(exclusive,
* 回填本帧);ok=快照可用(否则 no-op,动画降级为瞬时)。只刷面板矩形 + 新露出条,
* 顶= NCP_TOP(不盖状态栏)、不含 [0,NCP_TOP) 带;不重排面板控件(panel_snap 固定)。 */
static void ncp_anim_frame(int p, int *prev_bot, int ok)
{
if (!ok || g_ncp_ph <= 0 || !prev_bot) return;
int off = mnp_anim_off(p, NCP_ANIM_FIX, g_ncp_ph);
int cur_bot = ncp_anim_rebuild(off, *prev_bot);
int maxb = (*prev_bot > cur_bot) ? *prev_bot : cur_bot;
*prev_bot = cur_bot;
meow_dirty_add(0, NCP_TOP, fb_xres, maxb - NCP_TOP);
meow_ui_flush();
}
/* 把文本按宽度截断(UTF-8 安全:不切开多字节码),超出加 "..."。 */
static void ncp_text(int x, int y, const char *s, int size, uint32_t color, int maxw)
{
if (!s) s = "";
if (ui_width(s, size) <= maxw) { ui_draw(x, y, s, color, size); return; }
int ell = ui_width("...", size);
int limit = maxw - ell;
int len = 0;
char buf[128];
while (s[len] && len < (int)sizeof buf - 8) {
/* 取当前字节对应的 UTF-8 码长(1-4),避免切断多字节字符 */
unsigned char c0 = (unsigned char)s[len];
int cp = 1;
if ((c0 & 0xE0) == 0xC0) cp = 2;
else if ((c0 & 0xF0) == 0xE0) cp = 3;
else if ((c0 & 0xF8) == 0xF0) cp = 4;
if (len + cp >= (int)sizeof buf - 1) break;
for (int k = 0; k < cp; k++) buf[len + k] = s[len + k];
buf[len + cp] = 0;
if (ui_width(buf, size) > limit) { buf[len] = 0; break; }
len += cp;
}
if (len > 0) {
if (len + 3 < (int)sizeof buf) { buf[len] = '.'; buf[len+1] = '.'; buf[len+2] = '.'; buf[len+3] = 0; }
ui_draw(x, y, buf, color, size);
} else {
ui_draw(x, y, "...", color, size);
}
}
/* 画一个小圆点(近似圆 = 圆角方 r=radius;主题 chip 用)。 */
static void ncp_dot(int cx, int cy, int r, uint32_t col)
{
meow_gfx_fill_round(cx - r, cy - r, r * 2, r * 2, r, col);
}
/* 画 32×32 图标(复用 sb_blit_icon;ink 垂直中心对齐 cy)。 */
static void ncp_sb_icon(int icon, int slot_x, int cy, uint32_t col, int ink_h)
{
int saved = g_sb_icon_cy;
g_sb_icon_cy = cy;
sb_blit_icon(icon, slot_x, col, ink_h);
g_sb_icon_cy = saved;
}
/* chip 位置(4 个,居中排布)。返回下标对应 chip 的 x 左缘。 */
static int ncp_chip_x(int chip)
{
int total = NCP_CHIP_N * NCP_CHIP_W + (NCP_CHIP_N - 1) * NCP_CHIP_GAP;
int x0 = (fb_xres - total) / 2;
return x0 + chip * (NCP_CHIP_W + NCP_CHIP_GAP);
}
static int ncp_chip_hit(int chip, int tx, int ty)
{
int x = ncp_chip_x(chip);
return (tx >= x && tx < x + NCP_CHIP_W && ty >= NCP_CHIP_Y && ty < NCP_CHIP_Y + NCP_CHIP_H);
}
/* 面板当前 4 个 chip 的状态(① 能否写入/取值在 meow_settings_*;② 显示用)。 */
static void ncp_chip_state(int chip, int *on, char *val, int vlen)
{
*on = 0;
if (val && vlen > 0) val[0] = 0;
if (chip == NCP_CHIP_WIFI) {
int v = 0; meow_settings_get_switch("network.wifi_enabled", &v);
*on = v;
} else if (chip == NCP_CHIP_BT) {
int v = 0; meow_settings_get_switch("network.bluetooth_enabled", &v);
*on = v;
} else if (chip == NCP_CHIP_THEME) {
int v = 0; meow_settings_get_enum("display.theme", &v); /* dark=0, light=1 */
*on = (v == 1);
if (val && vlen > 0) snprintf(val, vlen, "%s", *on ? "浅色" : "深色");
} else if (chip == NCP_CHIP_BRIGHT) {
int v = 0; meow_settings_get_int("display.brightness", &v);
int vmin = 1; meow_settings_get_int("display.brightness_min", &vmin);
int vc = mnp_brightness_clamp(v, vmin);
if (val && vlen > 0) snprintf(val, vlen, "%d%%", vc);
*on = (vc >= 50); /* 过半视为"亮"态 */
} else if (chip == NCP_CHIP_POWER) {
/* 电源菜单 chip:非开关(无 on/off 态),恒示中性;点击由 ncp_chip_apply 唤起电源菜单。 */
*on = 0;
}
}
/* chip 快捷动作(写 meow_settings_* + 侧效应;零手写文件)。
* 返回 1=主题变化(需重刷状态栏);2=电源菜单已弹出(调用方置 after_modal=1 整帧重绘还原);
* 0=其它。电源菜单 chip 复用既有系统级 show_power_menu(含 dim/圆钮/音量导航/电源确认/8s强关/
* 空闲自动息屏),真弹对话框,动作在函数内 system();取消则返回续 panel —— 与电源键长按同款。 */
static int ncp_chip_apply(int chip)
{
if (chip == NCP_CHIP_WIFI) {
int v = 0; meow_settings_get_switch("network.wifi_enabled", &v);
meow_settings_set_switch("network.wifi_enabled", !v);
s_net_toggle_wifi(!v);
s_net_refresh();
} else if (chip == NCP_CHIP_BT) {
int v = 0; meow_settings_get_switch("network.bluetooth_enabled", &v);
meow_settings_set_switch("network.bluetooth_enabled", !v);
s_net_toggle_bt(!v);
s_net_refresh();
} else if (chip == NCP_CHIP_THEME) {
/* 快捷主题:切到 manual(确保生效),再翻转 dark/light(display.theme)。 */
int mode = THEME_MODE_MANUAL;
meow_settings_get_enum("display.theme_mode", &mode);
if (mode != THEME_MODE_MANUAL) meow_settings_set_enum("display.theme_mode", THEME_MODE_MANUAL);
int v = 0; meow_settings_get_enum("display.theme", &v);
meow_settings_set_enum("display.theme", v ? 0 : 1);
paint_sb_right(); /* 状态栏文字/底色随主题刷新 */
meow_settings_save();
return 1; /* 主题变化 → 重刷状态栏 */
} else if (chip == NCP_CHIP_BRIGHT) {
/* 快捷亮度:在「极暗(下限)」与「100」之间切换。 */
int cur = 0; meow_settings_get_int("display.brightness", &cur);
int vmin = 1; meow_settings_get_int("display.brightness_min", &vmin);
if (mnp_brightness_clamp(cur, vmin) < 100) {
meow_settings_set_int("display.brightness", 100);
write_brightness(100);
g_sys.brightness = 100;
} else {
meow_settings_set_int("display.brightness", vmin);
write_brightness(vmin);
g_sys.brightness = vmin;
}
paint_sb_right();
} else if (chip == NCP_CHIP_POWER) {
/* 电源菜单 chip:唤起既有系统级电源菜单(真弹对话框,动作在函数内 system();取消则返回)。
* 不涉及任何设置项(电源菜单不走 meow_settings,零旁路)。返回 2 → 调用方整帧重绘还原面板/状态栏。 */
logmsg("notif-center: power chip -> show power menu");
show_power_menu();
return 2;
}
meow_settings_save();
return 0;
}
/* ⑦ 画删除底:行右缘被空出的条(宽=-off)铺 error 红底 + 白「删除」字(条宽≥44 才显字,否则纯红渐进)。
* off<=0(负得多=卡滑出多、露出右条多);off=0 不画。 */
static void ncp_draw_delete_bg(int ry, int off, const meow_ui_theme_t *th)
{
if (off >= 0) return;
int bw = -off;
if (bw <= 0) return;
int bx = NCP_ROW_X + NCP_ROW_W + off; /* 被空出的右条左缘 */
fill_rect(bx, ry, bw, NCP_ROW_H - 6, th->error);
if (bw >= 44) {
int cx = bx + bw / 2, cy = ry + (NCP_ROW_H - 6) / 2;
ui_draw(cx - ui_width("删除", 16) / 2, cy - meow_font_line_height(16) / 2,
"删除", 0xFFFFFF, 16);
}
}
/* 画一行内容(行卡 + 图标/APP名/时间/标题/正文/进度),整体随 off 左移(⑦ 行左滑跟手)。
* i=g_rows 下标;ry=行顶(已含 scroll);off=行左移量(px,<=0)。 */
static void ncp_draw_row(int i, int ry, int off, const meow_ui_theme_t *th)
{
const ncp_row_t *r = &g_rows[i];
meow_gfx_fill_round(NCP_ROW_X + off, ry, NCP_ROW_W, NCP_ROW_H - 6, 12, th->surface_container);
/* 图标 */
int icx = NCP_ICON_X + off, icy = ry + (NCP_ROW_H - 32) / 2;
if (r->img) meow_ui_img_cover(icx, icy, 32, r->img);
else {
/* 回退:主题色圆点 + 首字符 */
const char *nm = ncp_app_name(r->app_id);
ncp_dot(icx + 16, icy + 16, 16, th->primary);
char ch = nm[0] ? nm[0] : '?';
char cbuf[4] = { ch, 0, 0, 0 };
/* 首字符若是多字节(中文),取整码 */
if ((unsigned char)ch >= 0x80 && nm[1]) { cbuf[1] = nm[1]; cbuf[2] = 0;
if ((unsigned char)ch >= 0xE0 && nm[2]) { cbuf[2] = nm[2]; cbuf[3] = 0; } }
int cw = ui_width(cbuf, 20);
ui_draw(icx + 16 - cw / 2, icy + 16 - meow_font_line_height(20) / 2, cbuf, 0xFFFFFF, 20);
}
/* app 名 + 时间 */
ui_draw(NCP_TEXT_X + off, ry + 10, ncp_app_name(r->app_id), th->on_surface_variant, 16);
char tbuf[32]; mnp_time_ago(r->created_ms, (uint64_t)time(NULL) * 1000u, tbuf, sizeof tbuf);
ui_draw(NCP_ROW_X + NCP_ROW_W - 8 - ui_width(tbuf, 16) + off, ry + 12, tbuf, th->on_surface_variant, 16);
/* 标题 + 内容 */
ncp_text(NCP_TEXT_X + off, ry + 30, r->summary, 20, th->on_surface, NCP_TEXT_W);
ncp_text(NCP_TEXT_X + off, ry + 56, r->body, 18, th->on_surface_variant, NCP_TEXT_W);
/* 进度(progress>=0):细条 */
if (r->progress >= 0) {
int px = NCP_TEXT_X + off, pw = NCP_TEXT_W, py = ry + NCP_ROW_H - 14;
fill_rect(px, py, pw, 4, th->surface_variant);
int fw = r->progress * pw / 100;
if (fw > 0) fill_rect(px, py, fw, 4, th->primary);
}
}
/* 绘制通知列表可见区(scroll=像素偏移)。每行:图标 + app名 + 标题 + 内容 + 时间 + 进度。
* ⑦:左滑中的行(g_ncp_swipe_row)按 g_ncp_swipe_off 左移,其下先画删除底(ncp_draw_delete_bg)。 */
static void ncp_draw_list(int scroll)
{
const meow_ui_theme_t *th = s_ui_theme();
if (g_nrows == 0) {
ui_draw(NCP_ROW_X + 8, NCP_LIST_TOP + 40, "暂无通知", th->on_surface_variant, 22);
return;
}
int total = g_nrows * NCP_ROW_H;
scroll = mnp_scroll_clamp(scroll, total, NCP_LIST_BOT - NCP_LIST_TOP);
for (int i = 0; i < g_nrows; i++) {
int ry = NCP_LIST_TOP + i * NCP_ROW_H - scroll;
if (ry + NCP_ROW_H < NCP_LIST_TOP || ry >= NCP_LIST_BOT) continue; /* 视口外整行跳过 */
int off = (i == g_ncp_swipe_row) ? g_ncp_swipe_off : 0; /* 左滑中的行位移 */
if (off < 0) ncp_draw_delete_bg(ry, off, th);
ncp_draw_row(i, ry, off, th);
}
/* 滚动条 */
if (total > NCP_LIST_BOT - NCP_LIST_TOP) {
int view = NCP_LIST_BOT - NCP_LIST_TOP;
int thumb_h = view * view / total; if (thumb_h < 24) thumb_h = 24;
int ty = NCP_LIST_TOP + (view - thumb_h) * scroll / (total - view);
meow_gfx_fill_round(fb_xres - 8, NCP_LIST_TOP, 5, view, 3, th->surface_container);
meow_gfx_fill_round(fb_xres - 8, ty, 5, thumb_h, 3, th->on_surface_variant);
}
}
/* 仅重绘列表可见区(滚动专用;不整面板、不刷状态栏)。先清列表底(面板 surface_variant),
* 再画可见行 + 滚动条;只标列表带脏([NCP_LIST_TOP, NCP_LIST_BOT)),面板体其它控件 / 状态栏带
* [0,NCP_TOP) 均不触碰 —— 满足「滚动只重绘列表可视区,绝不整屏」。 */
static void ncp_repaint_list(int scroll)
{
const meow_ui_theme_t *th = s_ui_theme();
fill_rect(0, NCP_LIST_TOP, fb_xres, NCP_LIST_BOT - NCP_LIST_TOP, th->surface_variant);
ncp_draw_list(scroll);
meow_dirty_add(0, NCP_LIST_TOP, fb_xres, NCP_LIST_BOT - NCP_LIST_TOP);
meow_ui_flush();
}
/* ⑦ 仅重绘一行(行左滑跟手/回弹/滑出专用;只刷该行矩形,不整面板、不刷状态栏)。
* 清该行带(含被滑出的右条)→ 画删除底(若 off<0)+ 该行内容位移 → 只标该行带脏。 */
static void ncp_repaint_row(int scroll, int row, int off)
{
const meow_ui_theme_t *th = s_ui_theme();
if (row < 0 || row >= g_nrows) return;
int ry = NCP_LIST_TOP + row * NCP_ROW_H - scroll;
if (ry + NCP_ROW_H < NCP_LIST_TOP || ry >= NCP_LIST_BOT) return; /* 视口外:无需重绘 */
/* 清该行带全宽(含左缘 [0,NCP_ROW_X),因深滑时卡可滑到左缘外),再画删除底 + 位移行卡。 */
fill_rect(0, ry, NCP_ROW_X + NCP_ROW_W, NCP_ROW_H, th->surface_variant);
if (off < 0) ncp_draw_delete_bg(ry, off, th);
ncp_draw_row(row, ry, off, th);
meow_dirty_add(0, ry, fb_xres, NCP_ROW_H);
meow_ui_flush();
}
/* ⑥/⑧ 勿扰开关带命中(面板内小 toggle,⑧ 收窄为左半段):
* 落在 [NCP_DND_Y, NCP_DND_Y+NCP_DND_H) 且 x ∈ [0, NCP_DND_SPLIT_X) → 勿扰命中。
* 右半段归「一键清除」(ncp_clear_hit,互斥);不与 5 chip(y≤130)、滑条带(y≤238)、
* 通知列表(y≥NCP_LIST_TOP=308)、状态栏(y<28)重叠。 */
static int ncp_dnd_hit(int tx, int ty)
{
return (ty >= NCP_DND_Y && ty < NCP_DND_Y + NCP_DND_H &&
tx >= 0 && tx < NCP_DND_SPLIT_X);
}
/* ⑧ 一键清除带命中(勿扰对称处的右半段):
* 落在 [NCP_DND_Y, NCP_DND_Y+NCP_DND_H) 且 x ∈ [NCP_DND_SPLIT_X, fb_xres) → 清除命中。 */
static int ncp_clear_hit(int tx, int ty)
{
return (ty >= NCP_DND_Y && ty < NCP_DND_Y + NCP_DND_H &&
tx >= NCP_DND_SPLIT_X && tx < fb_xres);
}
/* ⑥ 绘勿扰带(⑧ 收窄为左半段):label「勿扰」+ 铃铛图标 + 状态「开/关」 + 左半段右侧 s_draw_switch(48×28)。
* 右半段由 ncp_draw_clear 画「一键清除」按钮(对称)。 */
static void ncp_draw_dnd(const meow_ui_theme_t *th)
{
int y = NCP_DND_Y;
int dnd_on = g_ncp_dnd;
int cy = y + NCP_DND_H / 2;
/* 铃铛(简化:圆顶 + 顶蒂 + 底部小圆点)。 */
int bx = NCP_ROW_X + 4, bcy = cy - 2;
meow_gfx_fill_round(bx, bcy - 7, 16, 13, 7, th->on_surface_variant); /* 铃身圆顶 */
meow_gfx_fill_round(bx + 6, bcy - 11, 4, 4, 2, th->on_surface_variant);/* 顶蒂 */
ncp_dot(bx + 8, bcy + 6, 2, th->on_surface_variant); /* 底部小圆点 */
/* label + 状态 */
ui_draw(bx + 22, cy - meow_font_line_height(18) / 2 + 2, "勿扰", th->on_surface, 18);
char st[16];
snprintf(st, sizeof st, "%s", dnd_on ? "开" : "关");
int lw = ui_width("勿扰", 18);
ui_draw(bx + 22 + lw + 12, cy - meow_font_line_height(18) / 2 + 2, st,
dnd_on ? th->primary : th->on_surface_variant, 18);
/* 左半段右侧开关(⑧:从原 fb_xres-24-48 左移到分割线左侧,48×28 垂直居中) */
s_draw_switch(NCP_DND_SPLIT_X - 6 - 48, cy - 14, dnd_on, th);
}
/* ⑧ 绘一键清除按钮(勿扰对称处的右半段):圆角按钮底 + 垃圾桶字形 + 「清除」文字。 */
static void ncp_draw_clear(const meow_ui_theme_t *th)
{
int y = NCP_DND_Y;
int cy = y + NCP_DND_H / 2;
int cx0 = NCP_DND_SPLIT_X + 8; /* 右半段按钮左缘 */
int cw = (fb_xres - NCP_ROW_X) - cx0; /* 右半段按钮宽 */
if (cw <= 0) return;
meow_gfx_fill_round(cx0, y + 4, cw, NCP_DND_H - 8, 12, th->surface_container);
/* 垃圾桶字形(简化:顶盖横条 + 桶身圆角 + 侧提手)+「清除」文字,整体在按钮内居中(美观对称)。 */
const char *lbl = "清除";
int lw = ui_width(lbl, 18);
int unit = 22; /* 图标占位宽 + 与文字的间距 */
int total = unit + lw;
int gx = cx0 + (cw - total) / 2; /* 图标左缘(居中) */
int icy = cy;
fill_rect(gx, icy - 7, 14, 2, th->on_surface_variant); /* 顶盖 */
meow_gfx_fill_round(gx + 1, icy - 3, 12, 12, 2, th->on_surface_variant);/* 桶身 */
meow_gfx_fill_round(gx - 2, icy - 6, 4, 3, 1, th->on_surface_variant);/* 侧提手左 */
meow_gfx_fill_round(gx + 14, icy - 6, 4, 3, 1, th->on_surface_variant);/* 侧提手右 */
ui_draw(gx + unit, cy - meow_font_line_height(18) / 2 + 2, lbl, th->on_surface, 18);
}
/* ---- ⑧ 一键清除(批量):先转储留档 → 再清空显示(红线:保留 ongoing/media/progress)---- */
typedef struct { uint32_t ids[NCP_MAX_ROWS]; int n; } ncp_clear_ctx_t;
static int ncp_clear_collect_cb(const meow_notif_t *n, void *ctx)
{
ncp_clear_ctx_t *c = ctx;
if (!mnp_clearable(n->ongoing, n->media, n->progress)) return 0; /* 保留红线项 */
if (c->n >= NCP_MAX_ROWS) return 1; /* 满:中止遍历 */
c->ids[c->n++] = n->id;
return 0;
}
/* 收集可清除通知 id 后逐条 dismiss(dismiss = archive_one(dump→TF) + 释放槽 = 转储留档 + 清出显示)。
* 返回清除条数。 */
static int ncp_clear_all(void)
{
ncp_clear_ctx_t c;
memset(&c, 0, sizeof c);
meow_notif_list(NULL, ncp_clear_collect_cb, &c);
for (int i = 0; i < c.n; i++) meow_notif_dismiss(c.ids[i]);
return c.n;
}
/* 绘制整个通知中心面板(画进 SDK back_buf;redraw_sb=1 时另绘状态栏在最顶)。
* 性能:面板体单独;(0..27) 状态栏只在打开/主题变化时 redraw_sb=1 重绘,交互时跳过,
* 使脏区仅限面板体,绝不整屏重绘。 */
static void draw_notif_center(int scroll, int redraw_sb)
{
const meow_ui_theme_t *th = s_ui_theme();
int W = fb_xres, H = fb_yres;
/* 状态栏带 [0,NCP_TOP) 仅当 redraw_sb=1(打开/主题变化)时清底,随后由下方 draw_statusbar()
* 覆盖重绘;redraw_sb=0(交互重绘,如拖滑条)时**绝不触碰**——保留 back_buf 中 paint_sb_right()
* 已绘好的状态栏,避免被清成 surface 底仍被 blit → 状态栏数值闪烁/消失(真机①修复)。
* 面板体(状态栏之下 surface_variant)态恒整片铺。 */
if (redraw_sb) fill_rect(0, 0, W, NCP_TOP, th->surface);
fill_rect(0, NCP_TOP, W, H - NCP_TOP, th->surface_variant);
/* 把手 */
int grab_w = 44, grab_x = (W - grab_w) / 2;
meow_gfx_fill_round(grab_x, NCP_GRAB_Y, grab_w, 6, 3, th->outline);
/* 标题 + 「上滑/点空白关闭」 提示 */
ui_draw(24, NCP_TITLE_Y, "通知中心", th->on_surface, 22);
ui_draw(W - 24 - ui_width("点空白关闭", 16), NCP_TITLE_Y + 2, "点空白关闭", th->on_surface_variant, 16);
/* 快捷开关行(⑥:5 个 = WiFi/蓝牙/主题/亮度/电源菜单,居中自动随 NCP_CHIP_N) */
static const char *chip_lbl[NCP_CHIP_COUNT] = { "WiFi", "蓝牙", "主题", "亮度", "电源菜单" };
int total_chip = NCP_CHIP_N * NCP_CHIP_W + (NCP_CHIP_N - 1) * NCP_CHIP_GAP;
int chip_x0 = (W - total_chip) / 2;
for (int c = 0; c < NCP_CHIP_N; c++) {
int cx = chip_x0 + c * (NCP_CHIP_W + NCP_CHIP_GAP);
int on = 0; char val[16] = ""; ncp_chip_state(c, &on, val, sizeof val);
uint32_t bg = on ? th->primary : th->surface_container;
uint32_t fg = on ? 0xFFFFFF : th->on_surface_variant;
meow_gfx_fill_round(cx, NCP_CHIP_Y, NCP_CHIP_W, NCP_CHIP_H, 12, bg);
int icon_cx = cx + (NCP_CHIP_W - SB_ICON_SIZE) / 2;
int icon_cy = NCP_CHIP_Y + 14;
if (c == NCP_CHIP_WIFI) ncp_sb_icon(on ? SB_I_WIFI : SB_I_WIFI_OFF, icon_cx, icon_cy, fg, 22);
else if (c == NCP_CHIP_BT) ncp_sb_icon(on ? SB_I_BT : SB_I_BT_DIS, icon_cx, icon_cy, fg, 22);
else if (c == NCP_CHIP_THEME) {
if (on) ncp_dot(icon_cx + 16, icon_cy, 11, fg);
else { ncp_dot(icon_cx + 16, icon_cy, 11, fg); ncp_dot(icon_cx + 16, icon_cy, 6, bg); }
}
else if (c == NCP_CHIP_BRIGHT) ncp_sb_icon(SB_I_BRIGHT, icon_cx, icon_cy, fg, 22);
else if (c == NCP_CHIP_POWER) {
/* 电源符号:圆环 + 顶部短柄,近似 ⏻(用现有 meow_gfx_fill_round 近似圆)。 */
int pc = icon_cx + SB_ICON_SIZE / 2;
meow_gfx_fill_round(pc - 8, icon_cy - 8, 16, 16, 8, fg); /* 外环 */
meow_gfx_fill_round(pc - 4, icon_cy - 4, 8, 8, 4, bg); /* 内孔(显环厚) */
meow_gfx_fill_round(pc - 1, icon_cy - 16, 3, 9, 1, fg); /* 顶部短柄 */
}
char label[24];
snprintf(label, sizeof label, "%s", chip_lbl[c]);
if (val[0] && c == NCP_CHIP_BRIGHT)
snprintf(label, sizeof label, "%.9s", val); /* chip 下示值 */
ui_draw(cx + (NCP_CHIP_W - ui_width(label, 14)) / 2, NCP_CHIP_Y + NCP_CHIP_H - 18,
label, fg, 14);
}
/* 亮度滑条 */
int bmin = 1; meow_settings_get_int("display.brightness_min", &bmin);
int bval = 0; meow_settings_get_int("display.brightness", &bval);
bval = mnp_brightness_clamp(bval, bmin);
ui_draw(NCP_SLIDER_X, NCP_SL1_Y - 26, "亮度", th->on_surface, 18);
char vbb[16]; snprintf(vbb, sizeof vbb, "%d%%", bval);
ui_draw(NCP_SLIDER_X + NCP_SLIDER_W - ui_width(vbb, 18), NCP_SL1_Y - 26, vbb,
th->on_surface_variant, 18);
s_draw_slider(NCP_SLIDER_X, NCP_SL1_Y, NCP_SLIDER_W, bval, bmin, 100, th);
/* 音量滑条 */
int vval = 0; meow_settings_get_int("audio.volume", &vval);
vval = vol_clamp(vval);
ui_draw(NCP_SLIDER_X, NCP_SL2_Y - 26, "音量", th->on_surface, 18);
char vbl[16]; snprintf(vbl, sizeof vbl, "%d%%", vval);
ui_draw(NCP_SLIDER_X + NCP_SLIDER_W - ui_width(vbl, 18), NCP_SL2_Y - 26, vbl,
th->on_surface_variant, 18);
s_draw_slider(NCP_SLIDER_X, NCP_SL2_Y, NCP_SLIDER_W, vval, 0, vol_clamp(100), th);
/* 分隔线 */
fill_rect(12, NCP_DIV_Y, W - 24, 1, th->outline);
/* ⑥ 勿扰面板内开关带(list 上方,左半段) */
ncp_draw_dnd(th);
/* ⑧ 一键清除按钮(勿扰对称处的右半段) */
ncp_draw_clear(th);
/* 通知列表 */
ncp_draw_list(scroll);
/* 底部提示 */
ui_draw((W - ui_width("上滑或点空白关闭", 16)) / 2, NCP_FOOT_Y, "上滑或点空白关闭",
th->on_surface_variant, 16);
/* 状态栏画在面板体之上(0-27 单独由 draw_statusbar 维护/标脏,不落面板体脏区)。
* 仅 redraw_sb=1(打开/主题变化)才重绘,交互时跳过以保持脏区只限面板体。 */
if (redraw_sb) draw_statusbar();
}
/* 触摸滑条带命中:origin 落在 (slugX, slugY) 的滑条行带内。返回 1=命中。 */
static int ncp_slider_band(int tx, int ty, int sy)
{
return (tx >= NCP_SLIDER_X - 14 && tx <= NCP_SLIDER_X + NCP_SLIDER_W + 14 &&
ty >= sy - 30 && ty <= sy + 24);
}
/* 滑条值应用(拖动实时 set_*,save_now=抬手落盘)。
* 复用设置页方案:亮度 clamp [bmin,100];音量 vol_clamp 上限 + 跳步吸附 + DAC 节流去抖。
* 返回 1=值实际变更(需重绘面板/刷状态栏);0=吸附+钳制后与当前已提交值相同(懒跳过:
* 不写 DAC/背光、不刷状态栏、不标脏)→ 拖动去抖,消除逐事件刷状态栏/DAC 的卡顿(真机②修复)。
* 亮度写背光仅 sysfs 不引额外节流;音量写 DAC 节流(meow_dac_vol_write_throttled/flush 同 s_commit)。 */
static int ncp_slider_apply(int which, int tx, int save_now)
{
if (which == 1) {
int bmin = 1; meow_settings_get_int("display.brightness_min", &bmin);
int v = mnp_slider_val_from_x(tx, NCP_SLIDER_X, NCP_SLIDER_W, bmin, 100);
v = mnp_brightness_clamp(v, bmin); /* 下限 brightness_min(≥1)、上限 100 */
int cur = 0; meow_settings_get_int("display.brightness", &cur);
if (!save_now && v == mnp_brightness_clamp(cur, bmin)) return 0; /* 去抖:值未变(含 clamp 后) */
meow_settings_set_int("display.brightness", v);
write_brightness(v);
g_sys.brightness = v; /* 状态栏数值源=最近写入值(含 clamp 后) */
paint_sb_right();
if (save_now) meow_settings_save();
return 1;
} else {
int vmax = vol_clamp(settings_volume_max); /* 音量上限(尊重 audio.volume_max) */
int v = mnp_slider_val_from_x(tx, NCP_SLIDER_X, NCP_SLIDER_W, 0, vmax);
v = mnp_step_round(v, settings_volume_step); /* 跳步吸附(默认 step=1 → 连续同设置页) */
v = vol_clamp(v); /* 0..MEOW_VOL_MAX + 上限 clamp */
if (!save_now && v == settings_volume_get()) return 0; /* 去抖:值未变跳过写 DAC/刷状态栏 */
meow_settings_set_int("audio.volume", v);
g_sys.vol = settings_volume_get(); /* 状态栏数值源=最近写入值(含 clamp 后) */
paint_sb_right();
if (save_now) { meow_dac_vol_flush(); meow_settings_save(); } /* 抬手=无条件直写 DAC */
else meow_dac_vol_write_throttled();/* 拖动中=墙钟节流写 DAC */
return 1;
}
}
/* 通知点击:dismiss(标记阅读)+ 若 app 有 entry 记录则请求启动。返回 1=应关闭面板。 */
static int ncp_row_tap(int tx, int ty, int scroll, int *want_launch)
{
int eff = ty - NCP_LIST_TOP + scroll;
int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H);
if (row < 0 || row >= g_nrows) return 0;
const ncp_row_t *r = &g_rows[row];
meow_notif_dismiss(r->id); /* 点击 = 阅读,撤下 */
/* 跳转:app entry 记录(apps[] 内且 ok)→ 记 want_launch(run_menu 走标准启动)。
* target_activity 为后续 M3 导航预留下的跳转目标(activity 级路由后置)。 */
if (want_launch) *want_launch = -1;
if (g_ncp_apps)
for (int i = 0; i < g_ncp_napps; i++)
if (!strcmp(g_ncp_apps[i].id, r->app_id) && g_ncp_apps[i].ok) {
if (want_launch) *want_launch = i;
break;
}
return 1; /* 点击即关(启动由 run_menu 执行;无记录则仅关闭) */
}
/* ---- 通知中心面板主循环(阻塞;参照 run_settings_page 交互惯用法)---- */
static void run_notif_center(struct app *apps, int napps, int *want_launch,
int live_open, int lox, int loy)
{
if (want_launch) *want_launch = -1;
int list_scroll = 0;
unsigned char evbuf16[16];
g_ncp_apps = apps; g_ncp_napps = napps;
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
}
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
unsigned long long touch_cooldown_until = now_ms() + 300;
unsigned long long last_tick = now_ms();
unsigned long long last_act = now_ms();
unsigned long long last_listchk = now_ms();
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
int drag_slider = 0; /* 0=无, 1=亮度, 2=音量 */
/* 通知列表拖动滚动(方向遵循 input.swipe_invert,与设置页列表同式;关闭手势区分见 §11.3) */
int scroll_drag = 0; /* 是否处于列表区拖动滚动手势 */
int scroll_base = 0; /* 进入滚动时锁定的 list_scroll 基准(连续跟手,防触发瞬间跳变) */
int scroll_y0 = 0; /* 按下原点纵向坐标 */
int scroll_invert = 0; /* input.swipe_invert(0=标准方向) */
int scroll_rebound = 0; /* 拖到顶/底仍继续拖(回弹)→ 抬手按关闭语义收起 */
int list_paint = 0; /* 仅列表区重绘(滚动专用,不整面板/不刷状态栏) */
/* ⑦ 单条通知行左滑删除(跟手+半程可回+回弹;绘制态见 g_ncp_swipe_row/off):
* row_swipe_row = 正在左滑的行下标(-1=无);row_off = 行左移量(px,<=0,跟手);
* row_settle = 0 跟手 / 1 松手后向 row_off_target 回弹或滑出;row_anim_at = 落位动画计时。
* row_paint = 仅重绘该行带(跟手局部 blit/位移,不整面板/不刷状态栏)。 */
int row_swipe_row = -1;
int row_off = 0;
int row_settle = 0;
int row_off_target = 0;
unsigned long long row_anim_at = 0;
int row_paint = 0; /* 仅重绘该行带(行左滑专用) */
/* ⑨ 面板跟手下拉、半程可回:
* panel_drag = 1 面板跟手拖动中(anim_p 直接由手指位移映射,状态时间轴暂停);
* drag_seed_reveal = 进入拖动时锁定的面板「露出高度」(= ph + off,防触发瞬间跳变);
* drag_y0 = 进入拖动时锁定手指 y(此后 reveal = seed + (tly - drag_y0),连续跟手/半程可回);
* drag_paint = 仅用动画重建路径重绘一帧(复用 §9 快照 blit,不整屏/不重排控件)。 */
int panel_drag = 0;
int drag_seed_reveal = 0;
int drag_y0 = 0;
int drag_paint = 0;
int closed = 0; /* 非动画路径的兜底关面板(快照不可用时瞬时退出) */
int after_modal = 0; /* 电源菜单返回:整帧重绘还原被 dim 的状态栏 */
int wake_suppress = 0;
unsigned long long pwr_hold_at = 0;
int pwr_menu_state = 0, pwr_woke = 0, pwr_suppress_click = 0;
int need_sb = 1; /* 打开/主题变化时重绘状态栏;其余交互跳过(脏区只限面板体) */
/* 开关平移动画状态(§9):anim_p∈[0,NCP_ANIM_FIX],anim_target 为意图(FIX=开、0=关)。
* 仅当快照可用(anim_ok)才做平滑动画;否则降级为瞬时显示/消失(closed 兜底)。 */
int anim_p = 0, anim_target = NCP_ANIM_FIX;
int anim_last_target = NCP_ANIM_FIX; /* 记录上一步 anim_target(用于检测「意图切换」重置计时) */
unsigned long long anim_last = 0;
int anim_prev_bot = NCP_TOP; /* 上一帧面板覆盖底缘(exclusive;初始=NCP_TOP=未覆盖) */
int anim_ok = 0;
/* ⑨ live_open 入口(run_menu 热区拖动中切入):手指此刻已按下且正拖动。
* - 触摸:直接以传入原点作「已按下已锁原点」(evdev 广播,本 fd 只接后续 move/release)。
* - 进度:面板从隐藏(anim_p=0)起步,target 未定(=0),由拖动路径实时映射 anim_p;
* 松手按阈值锚定 target。默认(非 live_open)仍为 trigger 式自动展开(防御路径)。 */
if (live_open) {
td = 1; td_ok = 1; tdx = lox; tdy = loy; scroll_y0 = tdy;
anim_target = 0; anim_last_target = 0; /* 待拖动/松手锚定 */
}
/* ⑥ 读勿扰面板内开关态(开面板一次;切换在下面 tap 分支写回)。 */
{
int d = 0; meow_settings_get_bool("notify.dnd", &d);
g_ncp_dnd = d;
}
/* 首次绘制:先拍背景(应用列表)→ 渲染最终位置整面板 → 拍面板快照 → 还原背景到 p=0
* 起点(面板隐藏、先露应用列表),随后进入循环由动画推进 p:0→FIX 滑入。 */
ncp_refresh_rows();
list_scroll = mnp_scroll_clamp(0, g_nrows * NCP_ROW_H, NCP_LIST_BOT - NCP_LIST_TOP);
ncp_snap_init();
if (g_ncp_ph > 0) {
anim_ok = 1;
ncp_snap_bg(); /* 背景(应用列表)在面板绘制前拍 */
draw_notif_center(list_scroll, 1); /* 渲染最终位置整面板 → back_buf */
ncp_snap_panel(); /* 拍面板快照 */
/* 起点 p=0:按「上一帧为全覆盖(NCP_TOP+PH)」重建 → 整段还原为背景(面板隐藏) */
anim_prev_bot = ncp_anim_rebuild(mnp_anim_off(0, NCP_ANIM_FIX, g_ncp_ph),
NCP_TOP + g_ncp_ph);
meow_dirty_add(0, NCP_TOP, fb_xres, fb_yres - NCP_TOP);
meow_ui_flush();
} else {
draw_notif_center(list_scroll, 1);
meow_dirty_full(); meow_ui_flush();
}
need_sb = 0;
anim_last = now_ms();
int need_paint = 0; /* 持久:跨 tick 保持(动画进行中延迟到稳态再画) */
for (;;) {
unsigned long long now = now_ms();
if (sigterm_flag) break;
int animating = anim_ok && (anim_p != anim_target);
/* 自动息屏(同设置页语义) */
if (meow_auto_slept && !meow_screen_on) {
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int wak = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) wak = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) wak = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) wak = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) wak = 1;
}
if (!wak && screen_off_mode == 1 && tfd >= 0) {
struct pollfd spfd = { tfd, POLLIN, 0 };
if (poll(&spfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == 16) {
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_KEY && code == BTN_TOUCH && val) wak = 1;
}
}
}
if (wak) {
meow_panel_on(); meow_auto_slept = 0; wake_suppress = 1; last_act = now_ms();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
touch_cooldown_until = now_ms() + 300;
td = 0; td_ok = 0; drag_slider = 0; tlx = tly = -1;
scroll_drag = 0; scroll_base = 0; scroll_y0 = 0;
scroll_invert = 0; scroll_rebound = 0; list_paint = 0;
row_swipe_row = -1; row_off = 0; row_settle = 0;
row_off_target = 0; row_anim_at = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
/* 唤醒:面板直接就位(不重播滑入动画,面板早已打开);重拍快照防内容已变 */
anim_p = anim_target = anim_last_target = NCP_ANIM_FIX;
if (anim_ok) anim_prev_bot = NCP_TOP + g_ncp_ph;
draw_notif_center(list_scroll, 1);
if (anim_ok) ncp_snap_panel();
meow_dirty_full(); meow_ui_flush();
anim_last = now_ms();
need_sb = 0;
}
usleep(POLL_MS * 1000);
continue;
}
/* 按键:系统语义(APP §3.4.1 红线——不映射返回/确认) */
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int vold = 0, volu = 0, play = 0, pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
}
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu);
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
vold = kd_vold.stable; volu = kd_volu.stable;
play = kd_play.stable; pwr = kd_pwr.stable;
if (wake_suppress) {
if (!pwr && !vold && !volu && !play) wake_suppress = 0;
} else {
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
if (play && !m_prev_play) {
logmsg("notif-center: play key caught (media-reserved, no exit)");
last_act = now;
}
if (pwr && !m_prev_pwr) {
pwr_hold_at = now; pwr_menu_state = 0; pwr_suppress_click = 0; last_act = now;
if (meow_auto_slept && !meow_screen_on) pwr_woke = 1;
}
if (pwr && pwr_hold_at) {
if (now - pwr_hold_at >= power_forceoff_ms && !pwr_menu_state) {
if (sim_no_exec) logmsg("notif-center pm: [sim-no-exec] hold %dms -> would forceoff", power_forceoff_ms);
else { logmsg("notif-center pm: hold %dms -> force poweroff", power_forceoff_ms); sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); }
} else if (now - pwr_hold_at >= power_longpress_menu_ms && !pwr_menu_state) {
pwr_menu_state = 1;
logmsg("notif-center pm: hold %dms -> power menu", power_longpress_menu_ms);
show_power_menu();
last_act = now_ms();
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 1;
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
after_modal = 1; need_paint = 1;
goto ncp_keys_seeded;
}
}
if (!pwr && m_prev_pwr) {
if (pwr_suppress_click)
logmsg("notif-center pm: release after menu -> no toggle");
else if (pwr_hold_at && (now - pwr_hold_at < power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) {
logmsg("notif-center pm: single-click -> toggle panel");
meow_toggle_panel(); meow_auto_slept = 0; last_act = now_ms(); need_paint = 1;
} else if (pwr_woke) {
logmsg("notif-center pm: release after wake -> no toggle");
}
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 0;
}
}
if (vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0) ? volu : vold;
int going = vol_ramp_tick(held);
g_sys.vol = settings_volume_get();
paint_sb_right();
need_paint = 1;
if (!going) logmsg("notif-center vol ramp end -> %d", g_sys.vol);
}
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr;
ncp_keys_seeded:
;
/* 触摸 */
if (tfd >= 0 && meow_screen_on) {
/* 面板是否就位(稳态开):就位才允许控件交互;动画中抑制 chip/滑条/通知行命中
* (面板在滑,控件坐标已定但只部分可见,命中会误触——见 §9.5)。 */
int open_settled = anim_ok ? (anim_target >= NCP_ANIM_FIX && anim_p >= NCP_ANIM_FIX) : 1;
struct pollfd pfd = { tfd, POLLIN, 0 };
if (poll(&pfd, 1, 0) > 0) {
while (read(tfd, evbuf16, sizeof evbuf16) == 16) {
uint16_t type = *(uint16_t *)(evbuf16 + 8), code = *(uint16_t *)(evbuf16 + 10);
int32_t val = *(int32_t *)(evbuf16 + 12);
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) tlx = val;
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val;
if (td && !td_ok && tlx >= 0 && tly >= 0) {
tdx = tlx; tdy = tly; td_ok = 1;
scroll_y0 = tdy; /* 列表拖动基准:按下原点(进滚动时才锁 scroll_base) */
/* 按下原点落在滑条带 → 开始拖动(仅面板就位) */
if (open_settled && drag_slider == 0) {
if (ncp_slider_band(tlx, tly, NCP_SL1_Y)) { drag_slider = 1; if (ncp_slider_apply(1, tlx, 0)) need_paint = 1; }
else if (ncp_slider_band(tlx, tly, NCP_SL2_Y)) { drag_slider = 2; if (ncp_slider_apply(2, tlx, 0)) need_paint = 1; }
}
}
if (drag_slider == 1 && td_ok) { if (ncp_slider_apply(1, tlx, 0)) need_paint = 1; }
else if (drag_slider == 2 && td_ok) { if (ncp_slider_apply(2, tlx, 0)) need_paint = 1; }
/* ⑦ 行左滑(进入):面板就位(open_settled)+ 未锁其它手势 + 位移达 slop,
* 判定横向主导(adx>ady)且左移(ddx<0)且起点落在可见行 → 进行左滑(只该行)。
* 与竖向滚动互斥:本分支锁 row_swipe_row 后下方 scroll 不再进入(gate 含
* row_swipe_row<0);竖向主导(ady>adx)或不左移则回退既有滚动(改变不大:
* 以前「横主导左移」也滚,现改为行左滑,符合「横向主导→行左滑」)。 */
if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && row_swipe_row < 0) {
int ddx = tlx - tdx, ddy = tly - tdy;
int adx = ddx < 0 ? -ddx : ddx, ady = ddy < 0 ? -ddy : ddy;
if ((adx >= NCP_SWIPE_SLOP || ady >= S_SCROLL_SLOP) && adx > ady && ddx < 0) {
int eff = tdy - NCP_LIST_TOP + list_scroll;
int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H);
/* 起点必须在列表区(DND 带/底部提示等不在列表区,防 scroll 导致误匹行) */
if (tdy >= NCP_LIST_TOP && tdy < NCP_LIST_BOT &&
row >= 0 && row < g_nrows) {
row_swipe_row = row;
row_off = mnp_swipe_clamp(ddx, NCP_ROW_W);
row_settle = 0; row_off_target = 0;
row_anim_at = now_ms();
g_ncp_swipe_row = row; g_ncp_swipe_off = row_off;
row_paint = 1;
}
}
}
/* ⑦ 行左滑跟手:已进则更新 off(反向可回 → 回落至 0);只该行生效。 */
if (row_swipe_row >= 0 && td_ok && !row_settle) {
row_off = mnp_swipe_clamp(tlx - tdx, NCP_ROW_W);
g_ncp_swipe_off = row_off;
row_paint = 1;
}
/* ⑨ 面板跟手拖动(进入):竖向位移达 slop、起点不在列表溢出滚动区(归 §11)、
* 未锁行左滑/滚动/滑条 → 进入拖动。锁定当前面板露出高度 + 当前手指 y,
* 此后 reveal = seed + (tly - drag_y0),方向无关、连续可反向(半程可回)。 */
if (td_ok && !drag_slider && !scroll_drag && row_swipe_row < 0 && !panel_drag) {
int ady2 = (tly - tdy) < 0 ? (tdy - tly) : (tly - tdy);
if (ady2 >= S_SCROLL_SLOP) {
int av = g_nrows * NCP_ROW_H;
int vw = NCP_LIST_BOT - NCP_LIST_TOP;
int in_over = (tdy >= NCP_LIST_TOP && tdy < NCP_LIST_BOT &&
mnp_scroll_max(av, vw) > 0);
if (!in_over) {
int cur_off = mnp_anim_off(anim_p, NCP_ANIM_FIX, g_ncp_ph);
drag_seed_reveal = (g_ncp_ph > 0) ? (g_ncp_ph + cur_off) : 0;
drag_y0 = tly;
panel_drag = 1;
}
}
}
/* ⑨ 跟手:panel_drag 时 reveal=seed+位移 → progress 更新 anim_p;拖动期时间轴暂停。 */
if (panel_drag && td_ok) {
int reveal = mnp_drag_reveal(drag_seed_reveal, tly - drag_y0, g_ncp_ph);
int np = mnp_drag_progress(reveal, NCP_ANIM_FIX, g_ncp_ph);
if (np != anim_p) { anim_p = np; drag_paint = 1; }
}
/* 列表滚动(§11):面板就位(open_settled)+ 起点在列表区 + 内容溢出 +
* 纵向位移达 slop → 进滚动。方向遵循 input.swipe_invert(与设置页列表同式);
* 拖到顶/底继续拖 → 回弹标记。动画进行中(面板在滑,未就位)不启动滚动。 */
if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && row_swipe_row < 0) {
int ddy = tly - scroll_y0;
if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) {
int total = g_nrows * NCP_ROW_H;
int view = NCP_LIST_BOT - NCP_LIST_TOP;
if (mnp_scroll_max(total, view) > 0 &&
tdy >= NCP_LIST_TOP && tdy < NCP_LIST_BOT) {
scroll_drag = 1;
meow_settings_get_bool("input.swipe_invert", &scroll_invert);
scroll_base = mnp_scroll_base(list_scroll, ddy, scroll_invert);
scroll_rebound = 0;
}
}
}
if (scroll_drag && td_ok) {
int total = g_nrows * NCP_ROW_H;
int view = NCP_LIST_BOT - NCP_LIST_TOP;
int target = mnp_scroll_target(scroll_base, tly - scroll_y0, scroll_invert);
list_scroll = mnp_scroll_clamp(target, total, view);
scroll_rebound = mnp_scroll_rebound(target, mnp_scroll_max(total, view),
NCP_SCROLL_REBOUND_TOL);
list_paint = 1;
}
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val && !td) {
if (now_ms() < touch_cooldown_until) { tlx = tly = -1; }
else {
td = 1; td_ok = 0; tlx = -1; tly = -1; drag_slider = 0;
scroll_drag = 0; scroll_base = 0; scroll_y0 = 0;
scroll_invert = 0; scroll_rebound = 0;
/* ⑦ 新触摸:清行左滑(防上一手势残留) */
row_swipe_row = -1; row_off = 0; row_settle = 0;
row_off_target = 0; row_anim_at = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
/* ⑨ 新触摸:清面板跟手(防上一拖动残留) */
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
}
} else if (!val && td) {
td = 0;
if (drag_slider) { /* 滑条拖动结束 → 落盘 */
ncp_slider_apply(drag_slider, tlx >= 0 ? tlx : tdx, 1);
drag_slider = 0; need_paint = 1;
} else if (td_ok) {
int dx = tlx - tdx, dy = tly - tdy;
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
/* 左缘左滑 = 关闭(复用全局左缘返回语义;动画中同样可打断)——
* 优先于列表滚动落定,保「左缘返回关」语义不受滚动影响 */
if (meow_edge_back(tdx, dx, dy)) {
if (anim_ok) anim_target = 0; else closed = 1;
break;
}
if (row_swipe_row >= 0) {
/* ⑦ 行左滑松手:过阈值 → 滑出删除;未过/反向 → 回弹复原(半程取消)。
* 只该行生效;本手势锁定后不再走 tap/上滑关闭/点空白,不关面板。 */
/* 用抬手最终位移兜底刷新 off(可能最后有位移但无 ABS 事件跟进)。 */
row_off = mnp_swipe_clamp(dx, NCP_ROW_W);
g_ncp_swipe_off = row_off;
if (mnp_swipe_delete(row_off, NCP_ROW_W,
NCP_SWIPE_DELETE_FRAC, NCP_SWIPE_DELETE_MIN)) {
row_off_target = -NCP_ROW_W; /* 滑出(删除)目标:整行移出 */
} else {
row_off_target = 0; /* 回弹复原 */
}
row_settle = 1; row_anim_at = now_ms();
} else if (scroll_drag) {
/* 列表滚动落定:最终 clamp 到边界(拖动中已实时跟手,此处确认终值);
* 若回弹(拖到顶/底继续拖)→ 按「上滑/下滑关闭」收起(§11.3)。
* 绝不把「滑列表」误判成「上滑关闭」——scroll_drag 一旦置位即跳过
* 下方 mnp_vertical_up 关闭判定。 */
int total = g_nrows * NCP_ROW_H;
int view = NCP_LIST_BOT - NCP_LIST_TOP;
int target = mnp_scroll_target(scroll_base, tly - scroll_y0, scroll_invert);
list_scroll = mnp_scroll_clamp(target, total, view);
if (scroll_rebound) {
if (anim_ok) anim_target = 0; else closed = 1;
break;
}
list_paint = 1;
scroll_base = 0; scroll_y0 = 0; scroll_rebound = 0; scroll_invert = 0;
} else if (panel_drag) {
/* ⑨ 面板跟手拖动松手:按阈值锚定 target(≥40% → 补到展开;
* <40% 或反向拖回 → 回 0 收回 = 半程取消/关闭过阈值)。时间轴接管
* 把 anim_p 补到位;不误关面板、不走 tap/上滑/点空白(本手势锁定)。 */
int go_open = mnp_drag_anchor(anim_p, NCP_ANIM_FIX, NCP_DRAG_THR_PCT);
if (anim_ok) anim_target = go_open ? NCP_ANIM_FIX : 0;
else if (!go_open) closed = 1; /* 无快照降级:收回 → 直接退出 */
panel_drag = 0; drag_paint = 0; drag_seed_reveal = 0; drag_y0 = 0;
} else if (adx < 60 && ady < 60) { /* tap */
if (animating) break; /* 动画进行中:tap 忽略(控件命中会误触,见 §9.5) */
int tx = tdx, ty = tdy;
int hit = 0;
for (int c = 0; c < NCP_CHIP_COUNT; c++)
if (ncp_chip_hit(c, tx, ty)) {
int ar = ncp_chip_apply(c);
if (ar == 1) need_sb = 1; /* 主题变化 → 重刷状态栏 */
else if (ar == 2) {
after_modal = 1; /* 电源菜单已弹 → 整帧重绘还原 */
/* 电源菜单返回:菜单期间用户可能按电源确认/取消,键边沿可能错配,
* 同电源键长按路径(line 7480-7488)重 poll + 重采 m_prev_*,防误判「单击」→ 误切 panel。 */
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
/* 菜单期间用户的触摸经 evdev 广播也会进本 panel 的 tfd → 立即排空,
* 防返回后把「菜单里的点击」误判为 panel 内 tap/滑动。 */
if (tfd >= 0) {
unsigned char ev2[16];
while (read(tfd, ev2, sizeof ev2) == 16) {}
}
}
hit = 1; need_paint = 1; break;
}
if (!hit && ncp_slider_band(tx, ty, NCP_SL1_Y)) { hit = 1; if (ncp_slider_apply(1, tx, 1)) need_paint = 1; }
else if (!hit && ncp_slider_band(tx, ty, NCP_SL2_Y)) { hit = 1; if (ncp_slider_apply(2, tx, 1)) need_paint = 1; }
/* ⑥ 勿扰面板内开关(列表上方小 toggle):tap 即切换(整带命中) */
if (!hit && ncp_dnd_hit(tx, ty)) {
int dv = 0; meow_settings_get_bool("notify.dnd", &dv);
meow_settings_set_bool("notify.dnd", dv ? 0 : 1);
meow_settings_save();
g_ncp_dnd = dv ? 0 : 1;
ncp_refresh_rows(); /* 按 dnd 过滤重采集 */
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
NCP_LIST_BOT - NCP_LIST_TOP);
hit = 1; need_paint = 1;
}
/* ⑧ 一键清除(勿扰对称处右半段):先转储留档 → 再清空显示(留 ongoing/media/progress)。 */
if (!hit && ncp_clear_hit(tx, ty)) {
int n = ncp_clear_all();
logmsg("notif-center: clear %d notif(s) (dumped first, kept ongo/media/progress)", n);
/* 清除后取消行左滑(下标会变),再重采集 + 重钳 + 整面板重画。 */
row_swipe_row = -1; row_off = 0; row_settle = 0;
row_off_target = 0; row_anim_at = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
ncp_refresh_rows();
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
NCP_LIST_BOT - NCP_LIST_TOP);
hit = 1; need_paint = 1;
}
if (!hit && ty >= NCP_LIST_TOP && ty < NCP_LIST_BOT) {
if (ncp_row_tap(tx, ty, list_scroll, want_launch)) {
hit = 1;
if (anim_ok) anim_target = 0; /* 点击通知:dismiss + 记录启动 → 滑出收回 */
else closed = 1;
break;
}
}
if (!hit) { /* 点空白(标题/把手/提示/分隔线/列表外)→ 关闭 */
if (anim_ok) anim_target = 0; else closed = 1;
break;
}
} else if (mnp_vertical_up(dx, dy, MENU_SWIPE_THRESH)) {
if (anim_ok) anim_target = 0; else closed = 1; /* 面板内上滑 → 关闭 */
break;
} else if (anim_ok && anim_target == 0 &&
mnp_top_swipe_down(tdx, tdy, dx, dy, SB_BAR_H, MENU_SWIPE_THRESH)) {
anim_target = NCP_ANIM_FIX; /* 收回途中顶部热区下滑 → 重新打开(平滑转向) */
}
/* 其余横滑/下滑忽略(不关闭不动作) */
}
td_ok = 0;
}
}
}
}
}
if (closed) break; /* 非动画兜底:上滑 / 点空白 / 左缘左滑 / 通知点击 → 关面板返回 run_menu */
/* 列表变化检测:~500ms 对比 count;变化重采集 + 重绘列表区 */
if (now - last_listchk >= 500) {
last_listchk = now;
meow_notif_tick();
if (ncp_rows_changed()) {
/* ⑦ 列表已变(新条/撤下/TTL):取消当前行左滑,避免行下标错位。⑨ 同时取消面板跟手。 */
row_swipe_row = -1; row_off = 0; row_settle = 0;
row_off_target = 0; row_anim_at = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
ncp_refresh_rows();
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
NCP_LIST_BOT - NCP_LIST_TOP);
need_paint = 1;
}
}
/* 开关动画推进(§9:状态时间轴 + 快照 blit 平移;present 尊重 SDK 帧率门控,动画不更高频)。
* 每 tick 依壁钟差推进 p;改 anim_target 即平滑转向(可打断,不跳变)。 */
if (anim_ok && anim_p != anim_target && !panel_drag) {
unsigned long long anow = now_ms();
int dt;
if (anim_target != anim_last_target) {
/* 意图刚切换(打开↔关闭):重置计时起点,本 tick 不计时,
* 避免稳态闲置后 anim_last 陈旧导致首帧就跳到目标(不产生动画)。 */
anim_last_target = anim_target;
anim_last = anow;
dt = 0;
} else {
dt = (int)((anow > anim_last) ? (anow - anim_last) : 0);
anim_last = anow;
}
int np = mnp_anim_step(anim_p, anim_target, NCP_ANIM_FIX,
(unsigned long long)dt, NCP_ANIM_MS);
if (np != anim_p) {
anim_p = np;
ncp_anim_frame(anim_p, &anim_prev_bot, anim_ok); /* §9 快照 blit 平移(只刷面板矩形) */
}
}
if (anim_ok && !panel_drag && !td && anim_target == 0 && anim_p == 0) break; /* 滑出到 0 且无按下触摸 → 真正退出子循环 */
/* ⑦ 行左滑落位动画:row_settle=1 时按壁钟差向 row_off_target 线性步进(回弹→0 / 滑出→-row_w)。
* 到达目标:0=回弹完成(行复原,无需重排);-row_w=删除 → dismiss + 重采集 + 重排(need_paint)。
* 只该行生效;与面板开关动画(上块)互不干扰(行左滑仅在面板稳态 open_settled 下进行)。 */
if (row_swipe_row >= 0 && row_settle) {
unsigned long long rnow = now_ms();
int rdt = (int)((rnow > row_anim_at) ? (rnow - row_anim_at) : 0);
row_anim_at = rnow;
int noff = mnp_swipe_step(row_off, row_off_target, (unsigned long long)rdt, NCP_SWIPE_RATE);
if (noff != row_off) {
row_off = noff;
g_ncp_swipe_off = noff;
row_paint = 1;
}
if (noff == row_off_target) {
if (row_off_target == 0) {
/* 回弹复原完成:先把行重画回 0 位(清残留偏移),再清滑动态(无需重排)。 */
ncp_repaint_row(list_scroll, row_swipe_row, 0);
row_swipe_row = -1; row_settle = 0; row_off = 0;
row_off_target = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
if (anim_ok) ncp_snap_panel(); /* 行已复原:刷新面板快照(供滑出动画用) */
} else {
/* 滑出删除完成:撤下该通知 + 重采集 + 重排列表(整面板重画)。 */
if (row_swipe_row >= 0 && row_swipe_row < g_nrows)
meow_notif_dismiss(g_rows[row_swipe_row].id);
row_swipe_row = -1; row_settle = 0; row_off = 0;
row_off_target = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
ncp_refresh_rows();
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
NCP_LIST_BOT - NCP_LIST_TOP);
need_paint = 1; /* 行已移除/重排:整面板(列表区)重画 */
}
}
}
/* 重绘(面板体区域;状态栏 0-27 由 draw_statusbar 单独维护,不落入此脏区)。
* need_sb=1(打开/主题变化)时 draw_notif_center 内另绘状态栏并标其脏——脏区仍
* 只含「面板体 + 28px 状态栏」两带,绝不整屏重绘。
* 动画进行中:不画稳态面板(会覆盖正在滑动的帧),need_paint 保持到到位后再补画。 */
if (need_paint) {
last_act = now;
if (!(anim_ok && anim_p != anim_target)) {
draw_notif_center(list_scroll, need_sb);
need_sb = 0;
if (anim_ok) ncp_snap_panel(); /* 面板内容已变:刷新快照(供滑出动画用) */
if (after_modal) { meow_dirty_full(); after_modal = 0; }
else meow_dirty_add(0, NCP_TOP, fb_xres, fb_yres - NCP_TOP);
meow_ui_flush();
need_paint = 0;
list_paint = 0; /* 整面板已含列表:滚动重绘标记随之清除 */
}
/* 否则动画进行中:保留 need_paint,到位后(下一稳态 tick)补画最终面板 */
}
/* 仅列表区重绘(滚动专用:只刷列表带,不整面板/不刷状态栏;同受动画门控,
* 动画进行中不画稳态,list_paint 保持到到位后再补画一次列表终值)。 */
if (list_paint) {
last_act = now;
if (!(anim_ok && anim_p != anim_target)) {
ncp_repaint_list(list_scroll);
if (anim_ok) ncp_snap_panel(); /* 列表已变:刷新面板快照(供滑出动画用) */
list_paint = 0;
}
}
/* ⑨ 面板跟手拖动重绘:只刷面板矩形 + 新露出条(§9 快照 blit 平移;不整屏/不重排控件)。
* 拖动期 anim_p 直接由手指映射,故不受下方「animating」门控(非时间轴推进),直接呈现。 */
if (drag_paint) {
last_act = now;
ncp_anim_frame(anim_p, &anim_prev_bot, anim_ok);
drag_paint = 0;
}
/* 仅重绘该行带(行左滑跟手/回弹/滑出专用:只刷该行矩形,不整面板/不刷状态栏;同受动画门控)。
* row_swipe_row<0(行已回弹/删除/被取消)则跳过并清标记,避免对已移除行重绘。 */
if (row_paint) {
last_act = now;
if (row_swipe_row >= 0 && !(anim_ok && anim_p != anim_target))
ncp_repaint_row(list_scroll, row_swipe_row, row_off);
row_paint = 0;
}
/* ~1s 状态栏时钟 */
if (now - last_tick >= 1000) {
last_tick = now;
draw_clock();
fb_norm_buf0();
meow_ui_flush();
}
usleep(POLL_MS * 1000);
}
if (tfd >= 0) close(tfd);
kp_close(&kp_power); kp_close(&kp_keys);
ncp_snap_free(); /* 释放动画快照(退出即回收,不驻留) */
ncp_rows_free();
logmsg("notification center closed");
}
/* Bring the panel up for the menu, but ONLY when it is actually dark:
* brightness 0 (the player's own screen-off/standby writes brightness 0
* then FBIOBLANK(4); on this device bl_power stays 0, so brightness==0 is
* the reliable indicator — keep the bl_power check as a belt-and-suspenders
* for other states). A normal brightness is left completely untouched so
* the menu never yanks the brightness up (Bug A).
*
* CRITICAL (2026-08-28 hotfix): we NEVER touch FBIOBLANK here — the panel
* blank state belongs to the player's display state machine. fb_open()
* already unblanks for the menu (the menu must be visible); forcibly
* unblanking while the player thinks the screen is blanked leaves the
* player's state machine inconsistent (it stops drawing, UI freezes —
* this is the Bug B keeper deadlock, do not reintroduce). */
static void ensure_backlight(void)
{
int cur = read_brightness();
int blp = read_file_int(BACKLIGHT_POWER);
if (cur > 0 && blp <= 0) {
logmsg("backlight: screen on (%d), leave untouched", cur);
return;
}
int target = (saved_brightness > 0) ? saved_brightness : 60;
logmsg("backlight off (cur=%d bl_power=%d) -> %d", cur, blp, target);
if (cur <= 0) write_brightness(target);
if (blp > 0) write_file(BACKLIGHT_POWER, "0\n");
}
/* Restore the backlight after the player resumes (app exit / SIGTERM).
* Minimal duty after the 2026-08-28 hotfix:
* - if the menu opened while the player had blanked the panel (screen
* timeout), re-blank it (brightness 0 + FBIOBLANK(4)) so the physical
* panel matches the player's display state machine — no frozen frame,
* and the player still owns the blank state;
* - otherwise only write brightness back when it reads 0, and never
* unblank (unblanking behind the player's back wedges its display state
* machine — see ensure_backlight). A normal brightness is left
* completely untouched (Bug A: no forced brightness raise). */
static void restore_backlight(void)
{
if (menu_opened_blank) {
logmsg("restore: panel was blanked at menu open, re-blank (0 + FBIOBLANK 4)");
write_brightness(0);
int fbfd = open(FB_DEV, O_RDWR);
if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 4); close(fbfd); }
menu_opened_blank = 0;
return;
}
int cur = read_brightness();
int blp = read_file_int(BACKLIGHT_POWER);
if (cur <= 0 || blp > 0) {
int target = (saved_brightness > 0) ? saved_brightness : 60;
logmsg("restore backlight (cur=%d bl_power=%d) -> %d (saved=%d)",
cur, blp, target, saved_brightness);
if (cur <= 0) write_brightness(target);
if (blp > 0) write_file(BACKLIGHT_POWER, "0\n");
}
}
/* ================================================================== */
/* M2.12 MeowAPPs 包数据目录 + .mpk 接口 */
/* exFAT 无符号链接 → 数据目录用环境变量(MEOW_DATA_DIR/CACHE_DIR) */
/* 传址给应用,不做 symlink(APPS.md 附录 v2 修订)。 */
/* ================================================================== */
/* 由 exec 绝对路径取包名:/usr/data/mnt/sd_0/MeowAPPs/<pkg>/<binary> → <pkg> */
static int meow_pkg_name(const char *exec, char *out, int n)
{
if (!exec || strncmp(exec, MEOWAPPS, strlen(MEOWAPPS)) != 0) { if (n>0) out[0]=0; return -1; }
const char *p = exec + strlen(MEOWAPPS);
if (*p != '/') { if (n>0) out[0]=0; return -1; }
p++;
const char *end = strchr(p, '/');
int len = end ? (int)(end - p) : (int)strlen(p);
if (len <= 0 || len >= n) { if (n>0) out[0]=0; return -1; }
memcpy(out, p, len); out[len] = 0;
return 0;
}
/* 启动应用前准备数据目录:建 MeowData/<pkg>/{data,cache}(如缺)。幂等。
* 失败只 log(MeowData 所在卡不存在/只读),绝不崩 —— 零卡依赖:应用需按
* APPS.md 规范容忍无数据目录。返回 0=就绪,-1=pkg 不在卡上/不能判名。 */
static int meow_app_prepare(const char *exec)
{
char pkg[64];
if (meow_pkg_name(exec, pkg, sizeof pkg) != 0) return -1;
if (!sd_device_present()) return -1; /* 卡不在,跳过(应用仍可跑非卡资源) */
char cmd[320];
snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' "
"'/usr/data/mnt/sd_0/MeowData/%s/cache' 2>/dev/null", pkg, pkg);
int rc = system(cmd);
logmsg("app_prepare: pkg=%s rc=%d", pkg, rc);
return rc == 0 ? 0 : -1;
}
/* .mpk 安装(本期接口/CLI + 文档;UI 后期):解压 .mpk → MeowAPPs/<pkg>/。
* 约定 .mpk = tar.gz,顶层即包目录(含 app.json);包名 = 文件名茎 <pkg>.mpk。
* 同名先备份;解压后校验 app.json;随后数据 init(MeowData/<pkg>/{data,cache})。
* 返回 0 ok / 负=错误。 */
static int meow_pkg_install(const char *mpk_path)
{
if (!mpk_path || access(mpk_path, R_OK) != 0) { logmsg("pkg_install: bad mpk %s", mpk_path?mpk_path:"(null)"); return -1; }
if (!sd_device_present()) { logmsg("pkg_install: no card"); return -1; }
if (strpbrk(mpk_path, " ;`$'\"&|<>") != NULL) { logmsg("pkg_install: unsafe path"); return -2; }
/* 包名 = 文件名茎(去掉 .mpk) */
const char *bn = strrchr(mpk_path, '/');
bn = bn ? bn + 1 : mpk_path;
const char *dot = strrchr(bn, '.');
char pkg[64];
if (!dot || strcmp(dot, ".mpk") != 0) { logmsg("pkg_install: need <pkg>.mpk"); return -1; }
int len = (int)(dot - bn);
if (len <= 0 || len >= (int)sizeof pkg) { logmsg("pkg_install: bad pkg name"); return -1; }
memcpy(pkg, bn, len); pkg[len] = 0;
/* 目标 MeowAPPs/<pkg>/;同名先备份(.bak_<pid>),再解压,再校验 app.json */
char dst[256], bak[256], cmd[700];
snprintf(dst, sizeof dst, "/usr/data/mnt/sd_0/MeowAPPs/%s", pkg);
snprintf(bak, sizeof bak, "%s.bak_%d", dst, (int)getpid());
/* BusyBox tar 无 -z:用 gzip -dc | tar -x 处理 tar.gz。 */
snprintf(cmd, sizeof cmd,
"rm -rf '%s' 2>/dev/null; "
"[ -d '%s' ] && mv '%s' '%s' 2>/dev/null; "
"mkdir -p '%s' && gzip -dc '%s' | tar -x -C '%s' 2>/dev/null && "
"[ -f '%s/app.json' ]", bak, dst, dst, bak, dst, mpk_path, dst, dst);
if (system(cmd) != 0) { logmsg("pkg_install: extract/validate fail for %s", pkg); return -1; }
/* 数据 init:MeowData/<pkg>/{data,cache} */
snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' "
"'/usr/data/mnt/sd_0/MeowData/%s/cache' 2>/dev/null", pkg, pkg);
system(cmd);
logmsg("pkg_install: %s installed (old backed up %s)", pkg, bak);
return 0;
}
/* .mpk 导出:打包 MeowAPPs/<pkg>/(数据目录本就由 env 指向 MeowData,MeowAPPs/<pkg> 下无
* data/cache 实体,直接全打包)→ <pkg>.mpk。返回 0 ok。 */
static int meow_pkg_export(const char *pkg, const char *out_path)
{
if (!pkg || !out_path || strpbrk(pkg, " ;`$'\"&|<>/" ) || strpbrk(out_path, " ;`$'\"&|<>")) {
logmsg("pkg_export: unsafe path"); return -2;
}
if (!sd_mounted()) { logmsg("pkg_export: card not mounted"); return -1; }
char src[256], dst[256];
snprintf(src, sizeof src, "/usr/data/mnt/sd_0/MeowAPPs/%s", pkg);
if (access(src, R_OK) != 0) { logmsg("pkg_export: no such pkg"); return -1; }
snprintf(dst, sizeof dst, "%s", out_path);
/* BusyBox tar 无 -z:tar -c | gzip -c > out(先写 .tmp 再 mv,防半成品)。 */
char cmd[512];
snprintf(cmd, sizeof cmd, "tar -c -C '%s' . | gzip -c > '%s.tmp' 2>/dev/null && "
"mv '%s.tmp' '%s'", src, dst, dst, dst);
if (system(cmd) != 0) { logmsg("pkg_export: tar/gzip fail"); return -1; }
logmsg("pkg_export: %s -> %s", pkg, dst);
return 0;
}
/* ================================================================== */
/* 阶段 D · M0:AF_UNIX IPC 骨架 */
/* IPC 线程只读帧+解析+入队(SPSC 无锁环 + eventfd 唤醒主循环); */
/* 主循环 meow_ipc_process() 单写者改状态并回 ACK/ERR/RESULT。 */
/* 遵守 FRAMEWORK_DESIGN §1/§6:主循环 10ms 热键轮询绝不被 socket IO 阻塞。 */
/* ================================================================== */
#define MEOW_IPC_RING 16 /* 无锁环深度(单生产者=IPC线程,单消费者=主循环) */
#define MEOW_IPC_CONNS 10 /* 同时接入连接上限(backlog 4 + 余量) */
#define MEOW_IPC_BACKLOG 4
/* 入队消息:类型 + 请求 id + 客户端 fd(回包用)+ 负载拷贝(≤CARGO) */
struct meow_ipc_msg {
uint16_t type;
uint16_t plen;
uint32_t req_id;
int fd;
uint8_t payload[MEOW_IPC_CARGO];
};
static struct meow_ipc_msg ipc_ring[MEOW_IPC_RING];
static volatile uint32_t ipc_head = 0; /* 消费端(主循环)推进 */
static volatile uint32_t ipc_tail = 0; /* 生产端(IPC线程)推进 */
static void ipc_dispatch(struct meow_ipc_msg *m); /* 定义在下方,主循环排空用 */
static int ipc_event_fd = -1;
static volatile int ipc_shutdown = 0;
static volatile int ipc_thread_ok = 0; /* bind+listen 成功 */
static char ipc_sock_path[MEOW_IPC_SOCK_LEN] = MEOW_IPC_PATH;
static pthread_t ipc_pthread;
/* 每连接字节流帧组装状态(TCP 流语义下逐字节缓冲,跨 poll 保留) */
struct meow_ipc_conn {
int fd;
uint8_t hdr[MEOW_IPC_HDR_LEN];
int hdr_n;
uint8_t pay[MEOW_IPC_MAX];
uint32_t pay_tot;
uint32_t pay_n;
};
static struct meow_ipc_conn ipc_conns[MEOW_IPC_CONNS];
/* 写一帧(16B 头 + 负载)。返回写入字节;<=0 视为失败。 */
static size_t ipc_send(int fd, uint16_t type, uint32_t req_id,
const void *payload, uint32_t len)
{
if (fd < 0) return 0;
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC;
uint16_t ver = MEOW_IPC_VERSION;
memcpy(hdr + 0, &magic, 4);
memcpy(hdr + 4, &ver, 2);
memcpy(hdr + 6, &type, 2);
memcpy(hdr + 8, &req_id, 4);
memcpy(hdr + 12, &len, 4);
size_t off = 0;
while (off < sizeof hdr) {
ssize_t w = write(fd, hdr + off, sizeof hdr - off);
if (w <= 0) return off;
off += (size_t)w;
}
if (len && payload) {
size_t p = 0;
while (p < len) {
ssize_t w = write(fd, (const uint8_t *)payload + p, len - p);
if (w <= 0) break;
p += (size_t)w;
}
off += p;
}
return off;
}
/* 组装完的帧入队(SPSC 无锁,先写槽再推进 tail,__sync 发布)。 */
static void ipc_enqueue_frame(struct meow_ipc_conn *c)
{
uint16_t type;
uint32_t req_id, len;
memcpy(&type, c->hdr + 6, 2);
memcpy(&req_id, c->hdr + 8, 4);
len = c->pay_tot;
if ((ipc_tail - ipc_head) >= MEOW_IPC_RING) {
logmsg("ipc: ring full, drop frame type=0x%04x", type);
return;
}
uint32_t slot = ipc_tail % MEOW_IPC_RING;
struct meow_ipc_msg *m = &ipc_ring[slot];
m->type = type;
m->req_id = req_id;
m->fd = c->fd;
uint32_t copy = len < MEOW_IPC_CARGO ? len : MEOW_IPC_CARGO;
m->plen = (uint16_t)copy;
memcpy(m->payload, c->pay, copy);
__sync_synchronize();
ipc_tail++;
if (ipc_event_fd >= 0) { uint64_t one = 1; (void)write(ipc_event_fd, &one, sizeof one); }
}
/* 读一个连接的可用字节并组装成帧(>=1 完整帧即入队)。返回 0=ok(可能已入队),
* -1=连接关闭/坏帧(调用方应关闭并回收)。 */
static int ipc_conn_read(struct meow_ipc_conn *c)
{
for (;;) {
if (c->hdr_n < MEOW_IPC_HDR_LEN) {
ssize_t r = read(c->fd, c->hdr + c->hdr_n, MEOW_IPC_HDR_LEN - c->hdr_n);
if (r < 0) { if (errno == EAGAIN) return 0; return -1; }
if (r == 0) return -1;
c->hdr_n += (int)r;
if (c->hdr_n < MEOW_IPC_HDR_LEN) return 0;
uint32_t magic, len;
uint16_t ver;
memcpy(&magic, c->hdr, 4);
memcpy(&ver, c->hdr + 4, 2);
memcpy(&len, c->hdr + 12, 4);
if (magic != MEOW_IPC_MAGIC) { logmsg("ipc: bad magic %08x", magic); return -1; }
if (ver != MEOW_IPC_VERSION) { logmsg("ipc: bad version %u", ver); return -1; }
if (len > MEOW_IPC_MAX) { logmsg("ipc: frame len %u too long", len); return -1; }
c->pay_tot = len;
c->pay_n = 0;
if (len == 0) {
ipc_enqueue_frame(c);
c->hdr_n = 0;
continue;
}
}
if (c->pay_n < c->pay_tot) {
ssize_t r = read(c->fd, c->pay + c->pay_n, c->pay_tot - c->pay_n);
if (r < 0) { if (errno == EAGAIN) return 0; return -1; }
if (r == 0) return -1;
c->pay_n += (uint32_t)r;
if (c->pay_n < c->pay_tot) return 0;
}
ipc_enqueue_frame(c);
c->hdr_n = 0;
c->pay_n = 0;
}
}
static void *ipc_thread(void *arg)
{
(void)arg;
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (lfd < 0) { logmsg("ipc: socket() fail %s", strerror(errno)); return NULL; }
unlink(ipc_sock_path);
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, ipc_sock_path, sizeof a.sun_path - 1);
if (bind(lfd, (struct sockaddr *)&a, sizeof a) < 0) {
logmsg("ipc: bind %s fail %s", ipc_sock_path, strerror(errno));
close(lfd);
return NULL;
}
chmod(ipc_sock_path, 0666);
if (listen(lfd, MEOW_IPC_BACKLOG) < 0) {
logmsg("ipc: listen fail %s", strerror(errno));
close(lfd);
return NULL;
}
fcntl(lfd, F_SETFL, O_NONBLOCK);
ipc_thread_ok = 1;
logmsg("ipc: listening on %s (backlog %d)", ipc_sock_path, MEOW_IPC_BACKLOG);
struct pollfd fds[MEOW_IPC_CONNS + 1];
int nfds = 1;
fds[0].fd = lfd; fds[0].events = POLLIN; fds[0].revents = 0;
while (!ipc_shutdown) {
int r = poll(fds, nfds, 400);
if (r < 0) { if (errno == EINTR) continue; break; }
if (r == 0) continue;
if (fds[0].revents & POLLIN) {
for (;;) {
int c = accept(lfd, NULL, NULL);
if (c < 0) break;
fcntl(c, F_SETFL, O_NONBLOCK);
if (nfds <= MEOW_IPC_CONNS) {
ipc_conns[nfds - 1].fd = c;
ipc_conns[nfds - 1].hdr_n = 0;
ipc_conns[nfds - 1].pay_n = 0;
ipc_conns[nfds - 1].pay_tot = 0;
fds[nfds].fd = c;
fds[nfds].events = POLLIN;
fds[nfds].revents = 0;
nfds++;
logmsg("ipc: conn %d accept (nfds=%d)", c, nfds - 1);
} else {
close(c);
logmsg("ipc: conn %d rejected (full)", c);
}
}
}
int i = 1;
while (i < nfds) {
int ev = fds[i].revents;
if (ev & (POLLIN | POLLHUP | POLLERR | POLLNVAL)) {
struct meow_ipc_conn *cc = &ipc_conns[i - 1];
if (ipc_conn_read(cc) < 0) {
logmsg("ipc: conn %d closed", cc->fd);
close(cc->fd);
for (int j = i; j < nfds - 1; j++) {
fds[j] = fds[j + 1];
ipc_conns[j - 1] = ipc_conns[j];
}
nfds--;
continue;
}
}
i++;
}
for (int j = 0; j < nfds; j++) fds[j].revents = 0;
}
for (int i = 1; i < nfds; i++) close(fds[i].fd);
close(lfd);
unlink(ipc_sock_path);
logmsg("ipc: thread exit");
return NULL;
}
/* 启动 IPC 线程(主循环任意时刻调一次)。返回 0=就绪,-1=线程创建失败。
* (bind/listen 失败记 ipc_thread_ok=0,随后 meow_ipc_process 仍安全轮询空队列。) */
static int meow_ipc_init(void)
{
ipc_event_fd = eventfd(0, EFD_NONBLOCK);
if (ipc_event_fd < 0)
logmsg("ipc: eventfd fail %s (10ms poll drains anyway)", strerror(errno));
if (pthread_create(&ipc_pthread, NULL, ipc_thread, NULL) != 0) {
logmsg("ipc: pthread_create fail");
return -1;
}
usleep(60000); /* 给 bind+listen 一点时间,log 就绪状态 */
if (ipc_thread_ok) logmsg("ipc: ready (eventfd=%d)", ipc_event_fd);
else logmsg("ipc: NOT ready (bind/listen failed, thread exiting)");
return ipc_thread_ok ? 0 : -1;
}
/* 主循环:非阻塞排空 eventfd + 处理入队帧(单写者改状态并回包)。
* 事件fd 仅为低延迟优化;若为 -1,10ms 轮询仍保证每圈排空。 */
static void meow_ipc_process(void)
{
if (ipc_event_fd >= 0) {
uint64_t v;
while (read(ipc_event_fd, &v, sizeof v) == (ssize_t)sizeof v) {}
}
while (ipc_head != ipc_tail) {
struct meow_ipc_msg *m = &ipc_ring[ipc_head % MEOW_IPC_RING];
ipc_dispatch(m);
ipc_head++;
}
__sync_synchronize();
}
/* 处理一条入队帧:HELLO→ACK、ACT_QUERY→ACT_RESULT/ERR、ACT_DECLARE→ACK、其他→ERR。 */
static void ipc_dispatch(struct meow_ipc_msg *m)
{
switch (m->type) {
case MSG_HELLO: {
m_hello_t h;
memset(&h, 0, sizeof h);
memcpy(&h, m->payload, m->plen < (uint16_t)sizeof h ? m->plen : (uint16_t)sizeof h);
h.app_id[MEOW_APP_ID_MAX - 1] = 0;
meow_acct_hello(h.app_id, h.version, h.flags);
m_ack_t a;
a.code = MEOW_OK;
a.status = (uint32_t)g_naccts;
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
logmsg("ipc: HELLO app=%s ver=%u -> ACK naccts=%d", h.app_id, h.version, g_naccts);
break;
}
case MSG_ACT_DECLARE: {
m_act_declare_t d;
memset(&d, 0, sizeof d);
memcpy(&d, m->payload, m->plen < (uint16_t)sizeof d ? m->plen : (uint16_t)sizeof d);
d.app_id[MEOW_APP_ID_MAX - 1] = 0;
meow_acct_declare(d.app_id, d.entries, d.count);
m_ack_t a;
a.code = MEOW_OK;
a.status = (uint32_t)g_naccts;
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
logmsg("ipc: ACT_DECLARE app=%s n=%u -> ACK", d.app_id, d.count);
break;
}
case MSG_ACT_QUERY: {
m_act_query_t q;
memset(&q, 0, sizeof q);
memcpy(&q, m->payload, m->plen < (uint16_t)sizeof q ? m->plen : (uint16_t)sizeof q);
q.app_id[MEOW_APP_ID_MAX - 1] = 0;
q.activity[MEOW_ACT_MAX - 1] = 0;
char chain[MEOW_CHAIN_MAX] = "";
int depth = 0;
int code = resolve_chain(q.app_id, q.activity, chain, &depth);
if (code == MEOW_OK) {
m_act_result_t r;
memset(&r, 0, sizeof r);
r.depth = (uint32_t)depth;
r.code = MEOW_OK;
size_t cl = strlen(chain);
if (cl >= MEOW_CHAIN_MAX) cl = MEOW_CHAIN_MAX - 1;
memcpy(r.chain, chain, cl);
r.chain_len = (uint16_t)cl;
ipc_send(m->fd, MSG_ACT_RESULT, m->req_id, &r, sizeof r);
logmsg("ipc: ACT_QUERY app=%s act=%s -> depth=%d chain=%s",
q.app_id, q.activity, depth, chain);
} else {
m_err_t e;
memset(&e, 0, sizeof e);
e.code = (uint32_t)code;
const char *msg = (code == MEOW_ERR_NOENT) ? "unregistered app/activity"
: "resolve failed";
e.msg_len = (uint16_t)strlen(msg);
memcpy(e.msg, msg, e.msg_len);
ipc_send(m->fd, MSG_ERR, m->req_id, &e, sizeof e);
logmsg("ipc: ACT_QUERY app=%s act=%s -> ERR(0x%x)",
q.app_id, q.activity, code);
}
break;
}
case MSG_ACT_FOCUS: {
m_act_focus_t f;
memset(&f, 0, sizeof f);
memcpy(&f, m->payload, m->plen < (uint16_t)sizeof f ? m->plen : (uint16_t)sizeof f);
f.app_id[MEOW_APP_ID_MAX - 1] = 0;
f.activity[MEOW_ACT_MAX - 1] = 0;
meow_fg_act_set(f.app_id, f.activity); /* M2.17:当前顶层 activity(keep_awake 判定) */
m_ack_t a;
a.code = MEOW_OK;
a.status = (uint32_t)g_naccts;
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
logmsg("ipc: ACT_FOCUS app=%s act=%s -> ACK", f.app_id, f.activity);
break;
}
case MSG_ACT_BACK: {
meow_fg_act_back(); /* 回到根/无 activity:清 keep_awake */
m_ack_t a;
a.code = MEOW_OK;
a.status = (uint32_t)g_naccts;
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
logmsg("ipc: ACT_BACK -> ACK");
break;
}
/* M2a:通知存储(NOTIFICATION_SYSTEM.md §4/§7/§8)。IPC 线程只入队,此处在主循环
* 单写者落存储 + 回 ACK(与 HELLO/QUERY 同一线程模型,绝不在 socket IO 阻塞)。 */
case MSG_NOTIFY: {
m_notify_t mn;
memset(&mn, 0, sizeof mn);
memcpy(&mn, m->payload, m->plen < (uint16_t)sizeof mn ? m->plen : (uint16_t)sizeof mn);
mn.app_id[MEOW_APP_ID_MAX - 1] = 0;
mn.category[23] = 0;
mn.summary[63] = 0;
mn.body[95] = 0;
mn.app_icon[47] = 0;
mn.target_activity[MEOW_ACT_MAX - 1] = 0;
/* 映射到存储结构(freedesktop + MeowOS 扩展;actions/hints v1 空) */
meow_notif_t n;
memset(&n, 0, sizeof n);
n.id = mn.replaces_id; /* replaces_id==0 => 新建;>0 => 替换(不存在则新建) */
snprintf(n.app_id, sizeof n.app_id, "%s", mn.app_id);
snprintf(n.category, sizeof n.category, "%s", mn.category);
snprintf(n.summary, sizeof n.summary, "%s", mn.summary);
snprintf(n.body, sizeof n.body, "%s", mn.body);
snprintf(n.app_icon, sizeof n.app_icon, "%s", mn.app_icon);
snprintf(n.target_activity, sizeof n.target_activity, "%s", mn.target_activity);
n.expire_timeout = mn.expire_timeout;
n.progress = mn.progress;
n.urgency = mn.urgency; /* 0-3(低/普通/高/紧急);旧调用=0 → LOW(静默),新调用缺省 NORMAL */
n.ongoing = mn.ongoing;
n.media = mn.media;
uint32_t nid = 0;
int rc = meow_notif_post(&n, &nid);
m_ack_t a;
a.code = (rc == NOTIF_OK) ? MEOW_OK : (uint32_t)rc;
a.status = nid; /* 返回分配/更新的通知 id */
ipc_send(m->fd, MSG_NOTIFY_ACK, m->req_id, &a, sizeof a);
logmsg("ipc: NOTIFY app=%s cat=%s id=%u rc=%d -> ACK", mn.app_id, mn.category, nid, rc);
break;
}
case MSG_NOTIFY_DISMISS: {
uint32_t id = 0;
if (m->plen >= 4) memcpy(&id, m->payload, 4);
int rc = meow_notif_dismiss(id);
m_ack_t a;
a.code = (rc == NOTIF_OK) ? MEOW_OK : (uint32_t)rc;
a.status = 0;
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
logmsg("ipc: NOTIFY_DISMISS id=%u rc=%d -> ACK", id, rc);
break;
}
case MSG_NOTIFY_PROGRESS: {
m_notify_progress_t p;
memset(&p, 0, sizeof p);
memcpy(&p, m->payload, m->plen < (uint16_t)sizeof p ? m->plen : (uint16_t)sizeof p);
/* 实时进度:仅改 launcher 内存活跃槽(不落盘);progress>=100 视为结束 → 里程碑落盘 */
int rc = meow_notif_update_progress(p.nid, p.progress);
m_ack_t a;
a.code = (rc == NOTIF_OK) ? MEOW_OK : (uint32_t)rc;
a.status = 0;
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
logmsg("ipc: NOTIFY_PROGRESS nid=%u p=%d rc=%d -> ACK", p.nid, p.progress, rc);
break;
}
case MSG_NOTIFY_LIST:
/* ↓ 守护→App 下发通知清单(重同步)。本帧不来自客户端(无请求类型),故不在此应答;
* 需用时由守护主动经 meow_notif_serialize_list() + ipc_send 下发(M5 通知中心接入)。 */
logmsg("ipc: NOTIFY_LIST (downlink) not dispatched here");
break;
default:
logmsg("ipc: unsupported frame type 0x%04x (req %u)", m->type, m->req_id);
{
m_err_t e;
memset(&e, 0, sizeof e);
e.code = MEOW_ERR_INVAL;
char msg[40];
snprintf(msg, sizeof msg, "unsupported type 0x%04x", m->type);
e.msg_len = (uint16_t)strlen(msg);
memcpy(e.msg, msg, e.msg_len);
ipc_send(m->fd, MSG_ERR, m->req_id, &e, sizeof e);
}
break;
}
}
/* returns 0 on success (app ran and exited), -1 on launch failure */
/* GESTURE_BACK §2.4:把 app.json 的 immersive 声明写进 /tmp/meowapp_state,
* watchdog 据此决定是否画虚拟返回键 / 走 immersive 手势(watchdog 每 100ms 读一次)。
* immersive=1→"1";0→"0";<0→清空(应用退出,恢复默认非沉浸)。 */
static void meow_write_app_state(int immersive)
{
if (immersive < 0) { unlink("/tmp/meowapp_state"); return; }
int fd = open("/tmp/meowapp_state", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) return;
if (write(fd, immersive ? "1" : "0", 1) < 0) { /* ignore write error */ }
close(fd);
}
static int launch_app(struct app *a);
/* (definition follows) */
static int launch_app(struct app *a)
{
logmsg("launch: %s (%s)", a->name, a->exec);
if (!a->ok) { logmsg("launch blocked: exec not ok"); return -1; }
/* M2.12:启动前准备 MeowData/<pkg>/{data,cache}(幂等;卡不在/失败只 log 不崩)。
* 应用经环境变量 MEOW_DATA_DIR/MEOW_CACHE_DIR 定位数据(exFAT 无 symlink)。 */
meow_app_prepare(a->exec);
/* save the brightness the player had before the menu/app session, then
* drain input queues so the app starts clean */
saved_brightness = read_brightness();
if (saved_brightness <= 0) saved_brightness = 60; /* standby/unknown */
logmsg("saved brightness = %d", saved_brightness);
drain_input();
/* Bug 3: distinguish a REAL exec failure from a clean app exit. The child
* holds the write end of a CLOEXEC pipe; if execve() fails it writes the
* errno and _exit(127), if it succeeds the fd closes on exec so the parent
* read() returns EOF. Parent blocks on read() until the child either execs
* (success) or reports the error (failure); only then do we spawn the
* watchdog and waitpid — so an exec failure never leaves an orphan watchdog
* and never masquerades as "app ran and exited". */
int execpipe[2];
if (pipe(execpipe) < 0) { logmsg("exec pipe failed: %s", strerror(errno)); return -1; }
fcntl(execpipe[1], F_SETFD, FD_CLOEXEC);
pid_t pid = fork();
if (pid < 0) {
close(execpipe[0]); close(execpipe[1]);
logmsg("fork failed: %s", strerror(errno)); return -1;
}
if (pid == 0) {
/* child: exec the app. M2.12 注入 MEOW_LOG_DIR + MEOW_DATA_DIR/MEOW_CACHE_DIR
* (exFAT 无 symlink,数据目录用 env 传给应用)。PATH 继承 launcher(含卡上 opt/bin
* 若在),卡不在则回退普通 PATH。M2.52 追加注入 MEOW_LOG_APP(app_id)+MEOW_LOG_LEVEL
* (读全局 log.level),使 SDK meow_log() 按 app 分离写 apps/<app_id>.log 并做级别过滤。 */
close(execpipe[0]);
char *argv[] = { a->exec, NULL };
char env_path[320], env_log[320], env_data[320], env_cache[320];
char env_app[MEOW_APP_ID_MAX + 16], env_lvl[48];
char env_fpng[400], env_fidx[400], env_fapp[96];
char env_mf[24];
char env_tier[24];
char env_fs[20];
char env_cls[MEOW_FCLS_COUNT][2][400];
char *envp[40]; /* PATH,HOME,log_dir,log_app,log_lvl,font 全局(3),多字体(1),tier(1),class(≤12),fullscreen(1),data,cache */
int ne = 0;
const char *cp = getenv("PATH");
snprintf(env_path, sizeof env_path, "PATH=%s", cp ? cp : "/usr/bin:/bin");
envp[ne++] = env_path;
envp[ne++] = "HOME=/root";
if (log_dir[0]) { snprintf(env_log, sizeof env_log, "MEOW_LOG_DIR=%s", log_dir); envp[ne++] = env_log; }
/* M2.52:MEOW_LOG_APP=app_id(=app.json.package,缺省回退目录名;与注册表一致;无 id 则跳过,SDK 兜底 "app") */
if (a->id[0]) { snprintf(env_app, sizeof env_app, "MEOW_LOG_APP=%s", a->id); envp[ne++] = env_app; }
/* M2.52:MEOW_LOG_LEVEL 读全局 log.level(error/warn/info/debug→索引0/1/2/3),
* SDK 只认大写字符串名(ERROR/WARN/INFO/DEBUG),故转大写注入;缺省兜底 info。 */
{
static const char *lvl_name[] = { "ERROR", "WARN", "INFO", "DEBUG" };
int lv = 2; /* 默认 info */
meow_settings_get_enum("log.level", &lv);
if (lv < 0 || lv > 3) lv = 2;
snprintf(env_lvl, sizeof env_lvl, "MEOW_LOG_LEVEL=%s", lvl_name[lv]);
envp[ne++] = env_lvl;
}
/* M2.x:注入「当前生效字体」= 系统 font.file(launcher 已 load 的那套),
* SDK meow_ui_gfx.load_font 优先读 MEOW_FONT_PNG/IDX,app 未自带字库(§5.7 load_handle
* 未实现)时跟随系统字体,不再硬编码 MeowAPPs/fonts。无卡/无可用字体则跳过注入,
* SDK 自行回退卡默认→固件→点阵。 */
{
static const char *cls_env_png[MEOW_FCLS_COUNT] = {
"MEOW_FONT_CLS_CJK_PNG", "MEOW_FONT_CLS_CJK_SYM_PNG", "MEOW_FONT_CLS_NUM_PNG",
"MEOW_FONT_CLS_LATIN_PNG", "MEOW_FONT_CLS_LATIN_SYM_PNG", "MEOW_FONT_CLS_OTHER_PNG" };
static const char *cls_env_idx[MEOW_FCLS_COUNT] = {
"MEOW_FONT_CLS_CJK_IDX", "MEOW_FONT_CLS_CJK_SYM_IDX", "MEOW_FONT_CLS_NUM_IDX",
"MEOW_FONT_CLS_LATIN_IDX", "MEOW_FONT_CLS_LATIN_SYM_IDX", "MEOW_FONT_CLS_OTHER_IDX" };
static const char *cls_keys[MEOW_FCLS_COUNT] = {
"font.class_cjk", "font.class_cjk_sym", "font.class_num",
"font.class_latin", "font.class_latin_sym", "font.class_other" };
char fpng[400], fidx[400], fam[96];
int have_global;
int mf_on = 0;
meow_settings_get_switch("display.multi_font", &mf_on);
snprintf(env_mf, sizeof env_mf, "MEOW_MULTI_FONT=%d", mf_on ? 1 : 0);
envp[ne++] = env_mf;
/* M2.5x:字库档 MEOW_FONT_TIER=<0|1|2> —— 按 app.json "fonts" 声明注入,并合并
* 「加载大字库」开关(font.load_full → 强制 ≥GBK)得**生效档**;0=默认4K常用,
* 1=大字库GBK,2=Full。SDK meow_ui_gfx 读入存档并按档装载(见
* meow_font_effective_paths_tiered;真正装载由 meow_font_reload_tier 实现)。 */
int eff_tier = meow_font_effective_tier(a->fonts);
snprintf(env_tier, sizeof env_tier, "MEOW_FONT_TIER=%d", eff_tier);
envp[ne++] = env_tier;
have_global = (meow_font_effective_paths_tiered(eff_tier, fpng, sizeof fpng,
fidx, sizeof fidx, fam, sizeof fam) == 0);
if (have_global) {
snprintf(env_fpng, sizeof env_fpng, "MEOW_FONT_PNG=%s", fpng);
snprintf(env_fidx, sizeof env_fidx, "MEOW_FONT_IDX=%s", fidx);
snprintf(env_fapp, sizeof env_fapp, "MEOW_FONT_APP=%s", fam);
envp[ne++] = env_fpng;
envp[ne++] = env_fidx;
envp[ne++] = env_fapp;
logmsg("launch: font env %s/%s (family=%s)", fpng, fidx, fam);
}
/* M2.5x 多字体:每类注入 png/idx(命中则覆盖全局;类值为空=跟随全局,不注入)。
* SDK 读这些 env 构建多字体映射;同 family 去重由 SDK 侧 meow_font_config_multifont 处理。 */
if (mf_on)
for (int c = 0; c < MEOW_FCLS_COUNT; c++) {
char ref[MEOW_SET_MAX_LEN], abs[400], idxabs[400];
if (meow_settings_get_str(cls_keys[c], ref, sizeof ref) != 0 || !ref[0]) continue;
if (meow_font_resolve_path(ref, abs, sizeof abs) != 0 || access(abs, R_OK) != 0) continue;
snprintf(idxabs, sizeof idxabs, "%s", abs);
size_t n4 = strlen(idxabs);
if (n4 > 4 && !strcmp(idxabs + n4 - 4, ".png")) {
idxabs[n4-4]='.'; idxabs[n4-3]='i'; idxabs[n4-2]='d'; idxabs[n4-1]='x'; idxabs[n4]=0;
}
snprintf(env_cls[c][0], sizeof env_cls[c][0], "%s=%s", cls_env_png[c], abs);
snprintf(env_cls[c][1], sizeof env_cls[c][1], "%s=%s", cls_env_idx[c], idxabs);
envp[ne++] = env_cls[c][0];
envp[ne++] = env_cls[c][1];
}
}
/* 全屏声明:app.json "fullscreen"(app 级)→ 注入 MEOW_FULLSCREEN=1,SDK 端
* meow_ui_init 据此预先设全屏。基座只接 app 级;活动级 route.<id>.fullscreen
* (meow_route.fullscreen)在 M3 活动切换时由后续代理消费(此处暂不下发)。
* 未声明则不注入(SDK 默认非全屏)。 */
if (a->fullscreen) {
snprintf(env_fs, sizeof env_fs, "MEOW_FULLSCREEN=1");
envp[ne++] = env_fs;
}
char pkg[64];
if (meow_pkg_name(a->exec, pkg, sizeof pkg) == 0) {
snprintf(env_data, sizeof env_data, "MEOW_DATA_DIR=/usr/data/mnt/sd_0/MeowData/%s/data", pkg);
snprintf(env_cache, sizeof env_cache, "MEOW_CACHE_DIR=/usr/data/mnt/sd_0/MeowData/%s/cache", pkg);
envp[ne++] = env_data;
envp[ne++] = env_cache;
}
envp[ne] = NULL;
execve(a->exec, argv, envp);
int e = errno;
(void)write(execpipe[1], &e, sizeof e); /* reached only on exec failure */
_exit(127);
}
close(execpipe[1]);
int exe_err = 0;
ssize_t er = read(execpipe[0], &exe_err, sizeof exe_err);
close(execpipe[0]);
if (er > 0) {
logmsg("launch FAILED execve(%s): %s (errno %d)", a->exec,
strerror(exe_err), exe_err);
int st = 0;
waitpid(pid, &st, 0); /* reap the failed child */
return -1;
}
/* GESTURE_BACK §2.4:应用启动即写 immersive 状态,watchdog 依此隐藏/保留虚拟返回键 */
meow_write_app_state(a->immersive == '1' ? 1 : 0);
/* watchdog for virtual back key / gestures */
char wd1[192], wd2[192], pidstr[16];
pid_t wpid = 0;
snprintf(pidstr, sizeof pidstr, "%d", pid);
if (access(WATCHDOG1, X_OK) == 0)
snprintf(wd1, sizeof wd1, "%s", WATCHDOG1);
else if (access(WATCHDOG2, X_OK) == 0)
snprintf(wd1, sizeof wd1, "%s", WATCHDOG2);
else
wd1[0] = 0;
if (wd1[0]) {
wpid = fork();
if (wpid == 0) {
/* pass the same boot log file + dir so all MeowAPPs components
* log to one place */
char *argv[] = { wd1, pidstr, NULL };
char *envp[] = { "PATH=/usr/bin:/bin", NULL, NULL, NULL };
char file_env[320], dir_env[256];
if (log_file[0]) {
snprintf(file_env, sizeof file_env, "MEOW_LOG_FILE=%s", log_file);
envp[1] = file_env;
}
if (log_dir[0]) {
snprintf(dir_env, sizeof dir_env, "MEOW_LOG_DIR=%s", log_dir);
envp[2] = dir_env;
}
execve(wd1, argv, envp);
_exit(0);
}
if (wpid > 0) logmsg("watchdog %s pid=%d", wd1, wpid);
}
/* wait for app; in-app kill = VOL_DOWN x3 quick taps (the old
* PREVIOUS+VOL_UP hold combo is retired — the PREVIOUS key's GPIO
* never fires on this unit, §18.12). Volume is snapped on the first
* tap and restored when the sequence completes. */
int status = 0;
int app_prev_vold = 0;
int app_seq_hits = 0;
unsigned long long app_seq_last = 0;
struct vol_snap app_vol0 = { -1, -1 };
/* M2.11 D:应用二进制/资源若在卡上,卡被拔可能 SIGBUS 崩。启动前记录,运行期监测卡移除。
* (拷贝到内存/板载 staging 的方案先不做,仅记录 APPS.md 规范。) */
int app_on_card = (strncmp(a->exec, MEOWAPPS, strlen(MEOWAPPS)) == 0);
if (app_on_card) logmsg("app on card (%s): monitoring card removal during run", a->exec);
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER);
kp_open(&kp_keys, DEV_KEYS);
/* M2.17 前台应用会话:系统仍统一控制息屏(应用不自管)。launcher 无法看到应用内触摸,
* 故以「可物理键」为 idle 信号;前台当前 activity(经 IPC MSG_ACT_FOCUS 上报)若 keep_awake
* 则不解屏(fg_keep_awake)。唤醒时应用随其输入重绘,launcher 仅 fb_pan_buf0_force 兜底
* (M2.8 唤醒重绘路径的首步)。见 §18.46。 */
unsigned long long app_last_input = now_ms();
unsigned long save_pow[KEYBUF_WORDS], save_keys[KEYBUF_WORDS];
memcpy(save_pow, kp_power.cur, sizeof save_pow);
memcpy(save_keys, kp_keys.cur, sizeof save_keys);
/* M2.24:清上一次会话可能残留的电源菜单标记,只认本次新 watchdog 生成的请求。 */
unlink(POWMENU_MARKER);
unlink(POWMENU_ACTIVE);
for (;;) {
int r = waitpid(pid, &status, WNOHANG);
if (r == pid) break;
if (r < 0 && errno != EINTR) break;
if (sigterm_flag) { logmsg("sigterm during app"); kill(pid, SIGTERM); break; }
/* M2.11 D:卡被拔(设备节点消失)且前台应用在卡上 → 主动 SIGTERM 干净退出,
* 避免应用等 SIGBUS 崩。回菜单走正常应用退出路径。 */
if (app_on_card && !sd_device_present()) {
logmsg("hotplug: card removed during app run -> SIGTERM app ");
kill(pid, SIGTERM);
usleep(300000);
if (waitpid(pid, &status, WNOHANG) != pid) { kill(pid, SIGKILL); waitpid(pid, &status, 0); }
break;
}
/* in-app kill: VOL_DOWN x3 quick taps */
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
int vold = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
}
/* M2.17 自动息屏(app 会话):任意物理键边沿重置计时;空闲>timeout 且非 keep_awake → 息屏;
* 息屏后再按键 → 唤醒(fb_pan_buf0_force 兜底 LCDC 落 buffer0,内容随应用输入重绘)。 */
{
int key_edge = 0;
for (int w = 0; w < KEYBUF_WORDS; w++)
if (kp_power.cur[w] != save_pow[w] || kp_keys.cur[w] != save_keys[w]) { key_edge = 1; break; }
if (key_edge) {
memcpy(save_pow, kp_power.cur, sizeof save_pow);
memcpy(save_keys, kp_keys.cur, sizeof save_keys);
app_last_input = now_ms();
if (meow_auto_slept) {
logmsg("auto screen-off (app): key wake");
meow_panel_on();
meow_auto_slept = 0;
fb_pan_buf0_force();
}
}
if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept && !fg_keep_awake()) {
if (now_ms() - app_last_input >= (unsigned long long)screen_off_timeout * 1000) {
meow_panel_off();
meow_auto_slept = 1;
logmsg("auto screen-off (app %s) after %ds idle (mode=%s keep_awake=%d)",
a->name, screen_off_timeout, screen_off_mode ? "black" : "panel",
fg_keep_awake());
}
}
}
if (vold && !app_prev_vold) {
unsigned long long nw = now_ms();
if (app_seq_hits == 0) app_vol0 = vol_snapshot();
else if (nw - app_seq_last > (unsigned)seq_gap_ms) {
app_seq_hits = 0;
app_vol0 = vol_snapshot();
}
app_seq_hits++;
app_seq_last = nw;
if (app_seq_hits >= 3) {
logmsg("kill seq: VOL_DOWN x3 -> SIGTERM app");
vol_restore(app_vol0);
kill(pid, SIGTERM);
usleep(500000);
kill(pid, SIGKILL);
waitpid(pid, &status, 0);
break;
}
}
app_prev_vold = vold;
if (app_seq_hits > 0 && now_ms() - app_seq_last > (unsigned)seq_gap_ms * 2)
app_seq_hits = 0;
/* M2.24 全面按键接管:watchdog 检测到电源长按≥3s 写 /tmp/meow_power_menu → 此处消费:
* SIGSTOP 应用冻结 → 写 powmenu_active 让 watchdog 让位 → 统一电源菜单(launcher 一处绘制,
* 复用 show_power_menu,含触摸/音量导航/电源确认/8s 强关/pm_interacted 保护)→ 菜单关 →
* 清 active → SIGCONT 应用续跑。 */
if (access(POWMENU_MARKER, F_OK) == 0) {
unlink(POWMENU_MARKER);
logmsg("watchdog: power menu request (app %s) -> SIGSTOP + menu", a->name);
kill(pid, SIGSTOP);
int mfd = open(POWMENU_ACTIVE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (mfd >= 0) { write(mfd, "1", 1); close(mfd); }
int pm_ret = show_power_menu();
unlink(POWMENU_ACTIVE);
kill(pid, SIGCONT);
logmsg("watchdog: power menu closed (ret=%d) -> SIGCONT app", pm_ret);
/* M2.25:菜单期间按键可能已变化,重播 save_pow/save_keys 防误判 key_edge
* (避免菜单关闭瞬间误触发「键唤醒」或刷新自动息屏计时)。 */
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
memcpy(save_pow, kp_power.cur, sizeof save_pow);
memcpy(save_keys, kp_keys.cur, sizeof save_keys);
app_last_input = now_ms(); /* M2.17:菜单交互即输入,重置应用会话自动息屏计时 */
if (pm_ret != 0) break; /* 关机/重启/回HibyOS:系统通常已 reboot,防御性退出 */
}
usleep(100000);
}
if (wpid > 0) {
/* reap the watchdog: SIGTERM then wait up to 2s (it exits when the
* app dies; bounded so we never hang if it is stuck) */
kill(wpid, SIGTERM);
unsigned long long wstart = now_ms();
while (now_ms() - wstart < 2000) {
if (waitpid(wpid, NULL, WNOHANG) == wpid) break;
usleep(50000);
}
}
meow_write_app_state(-1); /* GESTURE_BACK §2.4:应用退出,清 immersive 状态 */
/* M2.24:应用运行期音量由 watchdog 写(settings.conf 同源)。返回菜单前 re-load,
* 让菜单音量/状态栏与真实 DAC 一致(watchdog 可能已改)。 */
settings_load();
logmsg("app %s exited status=%d", a->name, WEXITSTATUS(status));
return 0;
}
/* ================================================================== */
/* main */
/* ================================================================== */
static void usage(void)
{
fprintf(stderr,
"usage: meow_launcherd [--sim-hotkey N] [--sim-tap N] [--sim-exit N]\n"
" [--sim-exit-tap N] [--sim-power-hold N]\n"
" [--sim-power-pick N] [--sim-no-exec]\n"
" [--dumpkeys N] [--scan] [--sysdump]\n"
" [--pkg-install <pkg.mpk>] [--pkg-export <pkg> <out.mpk>]\n"
" [--ipc-only N] [--ipc-sock <path>] [--appinfo <app.json>]\n"
" [--fontscan <card_root>]\n");
}
int main(int argc, char **argv)
{
long sim_hotkey = 0, sim_tap = 0, sim_exit = 0, dumpkeys = 0;
int do_scan = 0, do_sysdump = 0, sim_vol = 0;
long ipc_only_ms = 0;
const char *appinfo_path = NULL;
const char *fontscan_root = NULL; /* --fontscan <root>:宿主/qemu 测试字形扫描 */
const char *pkg_install_mpk = NULL, *pkg_export_name = NULL, *pkg_export_out = NULL;
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--sim-hotkey") && i + 1 < argc) sim_hotkey = atol(argv[++i]);
else if (!strcmp(argv[i], "--sim-tap") && i + 1 < argc) sim_tap = atol(argv[++i]);
else if (!strcmp(argv[i], "--sim-exit") && i + 1 < argc) sim_exit = atol(argv[++i]);
else if (!strcmp(argv[i], "--sim-exit-tap") && i + 1 < argc) sim_exit_tap_ms = atol(argv[++i]);
else if (!strcmp(argv[i], "--sim-power-hold") && i + 1 < argc) sim_power_hold_ms = atol(argv[++i]);
else if (!strcmp(argv[i], "--sim-power-pick") && i + 1 < argc) sim_power_pick = atoi(argv[++i]);
else if (!strcmp(argv[i], "--sim-no-exec")) sim_no_exec = 1;
else if (!strcmp(argv[i], "--dumpkeys") && i + 1 < argc) dumpkeys = atol(argv[++i]);
else if (!strcmp(argv[i], "--scan")) do_scan = 1;
else if (!strcmp(argv[i], "--sysdump")) do_sysdump = 1;
else if (!strcmp(argv[i], "--sim-vol") && i + 1 < argc) sim_vol = atoi(argv[++i]);
else if (!strcmp(argv[i], "--ipc-only") && i + 1 < argc) ipc_only_ms = atol(argv[++i]);
else if (!strcmp(argv[i], "--ipc-sock") && i + 1 < argc) {
snprintf(ipc_sock_path, sizeof ipc_sock_path, "%s", argv[++i]);
}
else if (!strcmp(argv[i], "--appinfo") && i + 1 < argc) appinfo_path = argv[++i];
else if (!strcmp(argv[i], "--fontscan") && i + 1 < argc) fontscan_root = argv[++i];
else if (!strcmp(argv[i], "--pkg-install") && i + 1 < argc) pkg_install_mpk = argv[++i];
else if (!strcmp(argv[i], "--pkg-export") && i + 2 < argc) { pkg_export_name = argv[++i]; pkg_export_out = argv[++i]; }
else { usage(); return 1; }
}
/* unified log: TF card (or /tmp boot buffer when the card is absent) */
log_cleanup_old();
if (log_open_card() == 0)
fprintf(stderr, "log file: %s\n", log_file);
else
fprintf(stderr, "log: no TF card, using /tmp boot buffer (%s)\n",
BOOT_BUF_TMP);
logmsg("=== meow_launcherd start (sim_hotkey=%ld sim_tap=%ld sim_exit=%ld dumpkeys=%ld scan=%d) ===",
sim_hotkey, sim_tap, sim_exit, dumpkeys, do_scan);
load_config(); /* hotkey_seq / hotkey_seq_gap / bg_color (also run per
* menu open so edits apply without a daemon restart) */
settings_load(); /* M2.11:读系统设置(audio.volume 等),供音量/状态栏使用 */
/* M2.6:启动扫描一次 TF 卡 MeowOS/fonts/<family>/<weight>/,填充「字体文件」下拉 preset。
* 无卡/无字库目录 → 仅默认字体(等价现状)。进设置页时 run_settings_page 会再扫一次。 */
meow_font_scan_presets();
if (fontscan_root) {
/* M2.6 测试钩子:扫描指定卡根下的 MeowOS/fonts/<family>/<weight>/ 并打印 preset(qemu/宿主可测)。
* 用于验证 font.json 解析(含 weight)/三件套/缺件跳过/sha 校验 + preset 动态化逻辑。 */
meow_font_scan_presets_root(fontscan_root);
printf("fonts=%d\n", font_presets_n);
for (int i = 0; i < font_presets_n; i++)
printf(" %d %s -> %s (fam=%s wt=%s)\n", i, font_preset_labels[i], font_presets[i],
font_preset_fams[i], font_preset_wts[i] ? font_preset_wts[i] : "regular");
return 0;
}
if (sim_vol > 0) {
/* M2.14 sim:验证线性渐变——step=1/2/5 下设定音量按 1% 逐级爬升/回落,
* 每 1% 经 meow_dac_vol_write 写一次真实 DAC,打点进 log(stderr)。 */
settings_volume = 60; /* 固定起点,便于对比 */
settings_volume_step = (sim_vol > 5) ? 5 : (sim_vol < 1 ? 1 : sim_vol);
int base = settings_volume;
/* UP */
settings_volume_adj(+settings_volume_step);
for (int i = 0; i < 300 && vol_ramp_dir != 0; i++) { vol_ramp_tick(0); usleep(20000); }
int up = settings_volume;
/* DOWN */
settings_volume_adj(-settings_volume_step);
for (int i = 0; i < 300 && vol_ramp_dir != 0; i++) { vol_ramp_tick(0); usleep(20000); }
int down = settings_volume;
printf("sim-vol: step=%d base=%d up=%d down=%d (each 1%% logged 'DAC vol -> L/R=')\n",
settings_volume_step, base, up, down);
/* 长按:held=1 持续延段爬升 → 连续渐变到上限 clamp */
settings_volume = 60;
settings_volume_adj(+settings_volume_step);
for (int i = 0; i < 300 && vol_ramp_dir != 0; i++) { vol_ramp_tick(1); usleep(20000); }
printf("sim-vol-held: step=%d long-press top=%d (expect clamp 100)\n",
settings_volume_step, settings_volume);
return 0;
}
if (pkg_install_mpk) {
/* M2.12 .mpk 安装接口(CLI 钩子;UI 后期)。安全、离开主循环。 */
int r = meow_pkg_install(pkg_install_mpk);
printf("pkg_install %s -> %d\n", pkg_install_mpk, r);
return r;
}
if (pkg_export_name && pkg_export_out) {
/* M2.12 .mpk 导出接口(CLI 钩子;UI 后期)。 */
int r = meow_pkg_export(pkg_export_name, pkg_export_out);
printf("pkg_export %s -> %s (%d)\n", pkg_export_name, pkg_export_out, r);
return r;
}
if (do_sysdump) {
/* M2.11 测试钩子:安全打印系统状态快照(不碰 fb/输入设备,可在常驻 launcher
* 旁运行验证 A/E 的 BT/WiFi/电量/音量/亮度抓取),然后退出。 */
meow_sys_refresh();
printf("batt=%d%% chg=%d mv=%d\n", g_sys.batt, g_sys.charging, g_sys.batt_mv);
printf("wifi_on=%d ip=%s ssid=%s\n", g_sys.wifi_on, g_sys.wifi_ip[0]?g_sys.wifi_ip:"-",
g_sys.wifi_ssid[0]?g_sys.wifi_ssid:"-");
printf("bt_seen=%d bt_on=%d\n", g_sys.bt_seen, g_sys.bt_on);
printf("vol=%d brightness=%d\n", g_sys.vol, g_sys.brightness);
printf("data_free_kb=%ld sd_mounted=%d sd_free=%ld sd_total=%ld\n",
g_sys.data_free_kb, g_sys.sd_mounted, g_sys.sd_free_kb, g_sys.sd_total_kb);
printf("mode=%d napps=%d ver=%s\n", g_sys.mode, g_sys.napps, g_sys.ver);
printf("render: eff_rate=%d cfg=%d always_max=%d frames=%llu\n",
meow_ui_effective_rate(), refresh_rate_cfg, refresh_always_max,
meow_ui_frames_presented());
return 0;
}
if (do_scan) {
struct app apps[MAX_APPS];
int n = scan_apps(apps);
printf("state=%d napps=%d\n", n < 0 ? (n == -1 ? 1 : 2) : (n == 0 ? 2 : 0), n < 0 ? 0 : n);
for (int i = 0; i < n && n > 0; i++)
printf(" %d %s -> %s ok=%d\n", i, apps[i].name, apps[i].exec, apps[i].ok);
return 0;
}
if (appinfo_path) {
/* 阶段 D · M1 debug:解析单个 app.json 并打印路由/框架/服务/图标(qemu 可测静态路由) */
FILE *f = fopen(appinfo_path, "r");
if (!f) { printf("appinfo: cannot open %s\n", appinfo_path); return 1; }
struct app a;
memset(&a, 0, sizeof a);
meow_parse_app_json(f, &a);
fclose(f);
printf("name=%s\n", a.name[0] ? a.name : "(missing)");
printf("exec=%s\n", a.exec[0] ? a.exec : "(missing)");
printf("ok=%d\n", (a.exec[0] && access(a.exec, X_OK) == 0) ? 1 : 0);
printf("framework=%d\n", a.framework ? 1 : 0);
printf("icon=%s\n", a.icon[0] ? a.icon : "(none)");
printf("routes=%d\n", a.nroute);
for (int i = 0; i < a.nroute; i++)
printf(" route.%s -> %s (depth=%d keep_awake=%d)\n", a.routes[i].act, a.routes[i].chain,
chain_depth(a.routes[i].chain), a.routes[i].keep_awake);
printf("services=%d\n", a.nsvc);
for (int i = 0; i < a.nsvc; i++)
printf(" service.%s -> mode:%d restart:%d max:%d window_ms:%d\n",
a.svcs[i].name, a.svcs[i].mode, a.svcs[i].restart,
a.svcs[i].max, a.svcs[i].window_ms);
return 0;
}
if (ipc_only_ms > 0) {
/* 阶段 D · M0/M1 sim:仅跑 IPC 骨架(免 fb/输入设备),供 qemu 下用 meow_ipc
* 验证 HELLO→ACK、ACT_QUERY→链路。无卡时 /tmp 缓冲承载日志。 */
fprintf(stderr, "ipc-only: socket=%s\n", ipc_sock_path);
int rc = meow_ipc_init();
if (rc != 0) { fprintf(stderr, "ipc-only: IPC init failed, exiting\n"); return 1; }
unsigned long long s0 = now_ms();
while (!sigterm_flag && now_ms() - s0 < (unsigned long long)ipc_only_ms) {
meow_ipc_process();
meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/
usleep(POLL_MS * 1000);
}
ipc_shutdown = 1;
unlink(ipc_sock_path);
logmsg("=== meow_launcherd ipc-only exit ===");
return 0;
}
/* pid file */
int pfd = open(PID_PATH, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (pfd >= 0) {
char pbuf[16];
int l = snprintf(pbuf, sizeof pbuf, "%d\n", getpid());
write(pfd, pbuf, l);
close(pfd);
}
signal(SIGTERM, on_sigterm);
signal(SIGINT, on_sigterm);
signal(SIGHUP, SIG_IGN);
/* 阶段 D · M0:启动 IPC 线程(AF_UNIX /usr/data/meow_ipc.sock)。幂等;失败只 log,
* 主循环仍跑(10ms 热键轮询不受影响)。线程创建晚于进程但有独立生命周期。 */
meow_ipc_init();
struct keypoll kp_power, kp_keys;
/* retry opening the input devices (they may not exist yet at boot) */
for (int tries = 0; tries < 30; tries++) {
if (kp_open(&kp_power, DEV_POWER) == 0 &&
kp_open(&kp_keys, DEV_KEYS) == 0)
break;
logmsg("input devices not ready, retry %d/30 in 1s", tries + 1);
log_poll_card(); /* flush boot buffer even if input devices lag */
sleep(1);
}
if (!kp_power.ok || !kp_keys.ok)
logmsg("WARNING: input devices unavailable, launcher blind");
/* ===== MeowOS 常驻主界面(M2.2 修复批次)=====
* 播放器已死(find_hiby_pid()==0:bootmenu MeowOS 分支 exec、或热键切 MeowOS 后 exec,
* 二者皆此语境)时,launcher == MeowOS 主界面常驻:
* - 启动即扫描并自绘应用列目录(run_menu 画 buffer0 + FBIOPAN yoffset=0),
* 绝不等到首次热键才画 —— 修复「切到 MeowOS 后屏幕停留在播放器最后一帧」;
* - 无存活播放器 → run_menu 内不 SIGSTOP / 不备份 / 不 SIGCONT 死播放器、不恢复死帧;
* - BACK TO PLAYER → 显示「需重启」提示并留在主界面(不渲染死帧);
* - 仅 SIGTERM 退出(rcS/SSH 关停),退出时屏幕保持应用列表主界面(无残帧)。
* 若此时竟有存活播放器(不应用于 MeowOS,防御性),回落到下面的旧覆盖路径。 */
meow_mode = is_meow_mode();
/* M2.12:卡在则把 /usr/data/mnt/sd_0/MeowOS/opt/bin 前插 PATH(外围 CLI 已迁卡,
* 按需可用;卡不在跳过,绝不崩)。SSH 会话另由 rootfs /etc/profile.d/meow_card_path.sh 处理。 */
if (ensure_card_mounted() || sd_mounted()) {
setenv("PATH", "/usr/data/mnt/sd_0/MeowOS/opt/bin:/usr/bin:/bin:/usr/sbin:/sbin", 1);
logmsg("PATH: card MeowOS/opt/bin prepended");
} else {
logmsg("PATH: no card, MeowOS/opt/bin skipped");
}
if (meow_mode) {
/* M2.2 拔营:启动即尝试挂 TF 卡 + 联网(非阻塞);失败由 run_menu 后台周期重试。 */
ensure_card_mounted();
ensure_wifi();
struct app apps[MAX_APPS];
int n = scan_apps(apps);
int state = (n < 0) ? (n == -1 ? 1 : 2) : (n == 0 ? 2 : 0);
int napps = n < 0 ? 0 : n;
logmsg("MeowOS mode: hiby_player NOT running -> resident app-list home "
"(state=%d napps=%d), drain-to-home immediately", state, napps);
run_menu(apps, napps, state, sim_tap, sim_exit);
logmsg("=== meow_launcherd exit (MeowOS mode) ===");
if (log_fd >= 0) { close(log_fd); log_fd = -1; }
ipc_shutdown = 1; unlink(ipc_sock_path);
unlink(PID_PATH);
return 0;
}
/* hotkey state: power_voldown (default) + legacy seq */
int prev_pwr = 0, prev_prev = 0;
int prev_vold = 0, prev_volu = 0, prev_play = 0, prev_nxt = 0;
int pwr_hold_armed = 0; /* power held, within trigger window */
unsigned long long pwr_down_at = 0;
int seq_key_prev = 0; /* the configured seq key, edge-tracked */
int seq_hits = 0;
unsigned long long seq_last_at = 0;
struct vol_snap seq_vol0 = { -1, -1 };
long sim_hk_done = 0;
unsigned long long t0 = now_ms();
int log_tick = 0; /* periodic card-log retry (~1s) while no card log */
if (hotkey_mode == HOTKEY_MODE_PWRVOL)
logmsg("ready, hotkey = power_voldown (hold power, tap VOL_DOWN)");
else
logmsg("ready, hotkey = %d taps of key code %d (gap %dms)",
seq_len, seq_codes[0], seq_gap_ms);
while (!sigterm_flag) {
unsigned long long t = now_ms();
long el = (long)(t - t0);
/* 阶段 D · M0:排空 IPC 队列(非阻塞,零延迟;热键轮询 10ms 不受 socket 阻塞) */
meow_ipc_process();
meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/
/* sample current key states */
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
int pwr = 0, prev = 0, vold = 0, volu = 0, play = 0, nxt = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
if (kp_power.cur[w] & (1UL << (KEY_PREVIOUSSONG % 32))) prev = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
if (kp_keys.cur[w] & (1UL << (KEY_NEXT % 32))) nxt = 1;
}
/* sim hotkey: emulate the configured hotkey.
* power_voldown (default): HOLD power from N, VOL_DOWN tap at N+800
* (within the 60-2500ms window) — this drives pwr/vold so the
* real detection below sees the same DOWN edges a physical press
* would, and fires the menu at ~N+800.
* seq: 3 quick VOL_UP taps at N, N+300, N+600. */
if (sim_hotkey > 0 && !sim_hk_done) {
if (hotkey_mode == HOTKEY_MODE_PWRVOL) {
if (el >= sim_hotkey && el < sim_hotkey + 3000) pwr = 1;
if (el >= sim_hotkey + 800 && el < sim_hotkey + 950) vold = 1;
} else {
if (el >= sim_hotkey && el < sim_hotkey + 150) volu = 1;
if (el >= sim_hotkey + 300 && el < sim_hotkey + 450) volu = 1;
if (el >= sim_hotkey + 600 && el < sim_hotkey + 750) volu = 1;
if (el >= sim_hotkey + 750) sim_hk_done = 1;
}
}
if (vold != prev_vold) logmsg("key VOL_DOWN %s", vold ? "DOWN" : "UP");
if (volu != prev_volu) logmsg("key VOL_UP %s", volu ? "DOWN" : "UP");
if (play != prev_play) logmsg("key PLAY %s", play ? "DOWN" : "UP");
if (nxt != prev_nxt) logmsg("key NEXT %s", nxt ? "DOWN" : "UP");
if (pwr != prev_pwr) logmsg("key POWER %s", pwr ? "DOWN" : "UP");
if (prev != prev_prev) logmsg("key PREVIOUS %s", prev ? "DOWN" : "UP");
/* hotkey trigger (v6.5).
* DEFAULT — power_voldown: the launcher arms on the power DOWN edge
* and fires when a VOL_DOWN DOWN edge lands while power is still
* held, 60-2500ms after the power press. The player sees the power
* press (its screen-off flow starts — accepted side effect) and the
* VOL_DOWN tap (volume -1 — accepted). The menu open runs the
* verified unblank + bring-backlight-only-if-dark logic, so a
* power-initiated screen-off is undone for the menu. Power release
* or window expiry disarms; a VOL_DOWN before 60ms or after 2500ms
* is ignored (a plain volume press while holding power).
* LEGACY — seq: N quick taps of one key within seq_gap_ms opens the
* menu (single/double presses stay ordinary volume keys); the taps
* raise volume, snapshotted and restored on trigger. */
if (pwr && !prev_pwr && hotkey_mode == HOTKEY_MODE_PWRVOL) {
pwr_hold_armed = 1;
pwr_down_at = t;
logmsg("hotkey: power DOWN, arm hold-window");
}
if (!pwr && prev_pwr) pwr_hold_armed = 0; /* release: disarm */
int hotkey_fired = 0;
if (hotkey_mode == HOTKEY_MODE_PWRVOL) {
/* disarm once the hold window has passed (power still held) */
if (pwr_hold_armed && pwr && t - pwr_down_at > HK_PWRWIN_MAX)
pwr_hold_armed = 0;
if (pwr_hold_armed && pwr && vold && !prev_vold) {
long since = (long)(t - pwr_down_at);
if (since >= HK_PWRWIN_MIN && since <= HK_PWRWIN_MAX) {
logmsg("HOTKEY: power held + VOL_DOWN tap (%ldms) -> menu",
since);
hotkey_fired = 1;
} else {
logmsg("hotkey: POWER+VOL_DOWN ignored (%ldms outside window)",
since);
}
}
} else {
int seq_key = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_power.cur[w] & (1UL << (seq_codes[0] % 32))) seq_key = 1;
if (kp_keys.cur[w] & (1UL << (seq_codes[0] % 32))) seq_key = 1;
}
if (seq_key && !seq_key_prev) { /* DOWN edge */
if (seq_hits == 0) seq_vol0 = vol_snapshot();
else if (t - seq_last_at > (unsigned)seq_gap_ms) {
seq_hits = 0; /* gap too long: restart */
seq_vol0 = vol_snapshot();
}
seq_hits++;
seq_last_at = t;
logmsg("seq tap %d/%d", seq_hits, seq_len);
if (seq_hits >= seq_len) {
logmsg("SEQ HOTKEY: %d taps of key %d -> menu",
seq_len, seq_codes[0]);
vol_restore(seq_vol0);
seq_hits = 0;
hotkey_fired = 1;
}
} else if (!seq_key && seq_key_prev) { /* UP edge: nothing */
}
seq_key_prev = seq_key;
if (seq_hits > 0 && t - seq_last_at > (unsigned)seq_gap_ms * 2)
seq_hits = 0;
}
if (hotkey_fired) {
struct app apps[MAX_APPS];
int n = scan_apps(apps);
int state = (n < 0) ? (n == -1 ? 1 : 2) : (n == 0 ? 2 : 0);
int napps = n < 0 ? 0 : n;
run_menu(apps, napps, state, sim_tap, sim_exit);
/* restore the player's panel state FIRST (re-blank if the menu
* opened while blanked), THEN let it resume — so the player never
* sees a panel that contradicts its display state machine
* (v6.2 hotfix) */
restore_backlight();
cont_player();
sim_hk_done = 1;
pwr_hold_armed = 0;
/* absorb current state as baseline for the menu keys */
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
prev_vold = prev_volu = prev_play = prev_nxt = 0;
prev_pwr = prev_prev = 0;
seq_key_prev = 0;
seq_hits = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) prev_vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) prev_volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) prev_play = 1;
if (kp_keys.cur[w] & (1UL << (KEY_NEXT % 32))) prev_nxt = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) prev_pwr = 1;
if (kp_power.cur[w] & (1UL << (KEY_PREVIOUSSONG % 32))) prev_prev = 1;
}
/* the menu just proved the card is present: flush */
log_poll_card();
continue;
}
prev_vold = vold; prev_volu = volu; prev_play = play; prev_nxt = nxt;
prev_pwr = pwr; prev_prev = prev;
if (dumpkeys > 0 && el >= dumpkeys) {
logmsg("dumpkeys duration reached, exit");
break;
}
/* card may have mounted after S90: open the card log and move the
* /tmp boot buffer into it (no menu needed to see boot logs) */
if (log_fd < 0 && (++log_tick % 100) == 0) log_poll_card();
/* NOTE: the Bug B backlight keeper was REMOVED (2026-08-28 hotfix).
* It restored brightness + FBIOBLANK(0) after the player's bkl_time
* screen timeout, which unblanked the panel behind the player's back
* and froze its display state machine (UI gone, keys dead — device
* log: keeper at 40.9s, player wedged). The player owns the panel
* blank state; the launcher never touches fb0/backlight outside the
* menu/app sessions anymore. The player's own "背光时间" timeout is
* expected stock behaviour (set it to 常亮 in the player settings UI
* if unwanted). */
usleep(POLL_MS * 1000);
}
/* If the player is still stopped we never made it through the menu path's
* restore+resume (e.g. SIGKILL-free SIGTERM race): restore the panel state
* before resuming. When the player is already resumed the menu path did it,
* and re-running restore_backlight here would write brightness onto a panel
* the player still believes is blanked — leave it alone. */
if (player_stopped) { fb_restore_player(); restore_backlight(); }
cont_player();
logmsg("=== meow_launcherd exit ===");
if (log_fd >= 0) { close(log_fd); log_fd = -1; }
ipc_shutdown = 1; unlink(ipc_sock_path);
unlink(PID_PATH);
return 0;
}