/* 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_LIST 为守护→App 下行(M5 接入)。meow_ipc 测试钩子新增 notify/dismiss 子命令。 * - meow_notif_before_poweroff() 挂入 sys_do_reboot():正常电源事件前先转储未上卡通知再清板载。 * * 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下"); * 按压 =path / framework=true / * service.=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 (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//{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//app.json, * 5 edge cases), navigate with touch or keys, launch app via * fork/execve/waitpid, spawn meow_watchdog 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_.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 #include #include #include #include #include #include #include #include #include #include #include #include #include #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 #include #include /* Bug 2:直连 reboot(2) syscall 用 */ #include /* M0:IPC 线程 */ #include /* M0:IPC→主循环唤醒(EFD_NONBLOCK) */ #include /* M0:AF_UNIX IPC */ #include /* M0:sockaddr_un */ #include #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) */ /* ---- 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 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. 行(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_.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)*/ /* 字库放 TF 卡 MeowOS/fonts/default/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/font.png" #define UI_FONT_IDX_CARD "/usr/data/mnt/sd_0/MeowOS/fonts/default/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:TF 卡多字体库 —— 字体目录 = <卡根>/MeowOS/fonts//(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 卡内字体(防爆) */ static const char *font_presets[FONT_PRESETS_MAX]; /* SC_PRESET 值 = 卡根相对路径 */ static const char *font_preset_labels[FONT_PRESETS_MAX]; /* SC_PRESET 中文标签 */ static char font_preset_val_buf[FONT_PRESETS_MAX][320]; /* 值持久存储(指向上面指针数组) */ static char font_preset_label_buf[FONT_PRESETS_MAX][64]; /* 标签持久存储 */ 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; } /* 解析 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): * 1) font.file(设置键,真正接管加载;相对路径按卡根解析)→ 存在且能 load 即用; * 2) 卡 MeowOS/fonts/default/font.png(默认字库); * 3) 固件 /usr/share/meowos/font/font.png。 * A-1/A-2:SDK meow_ui_init() 已用同一 meow_font.o 加载过(可能成功也可能未成功)。 * 当 font.file 指向「默认卡字库」或未设置时复用 SDK 已加载状态,避免重复 unload+reload; * 当 font.file 指向自定义字库时必须显式 load(SDK 只加载默认,不读 font.file)。 * mfont_forced=1(font.file 变更触发)时无条件真实 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[320], fidx[320]; int have_sel = (meow_font_resolve_selected(fpng, sizeof fpng, fidx, sizeof fidx) == 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; } /* font.file 优先。 */ if (have_sel && meow_font_load(fpng, fidx) == 0) { mfont_ready = 1; logmsg("meow_font: loaded font.file %s (%d/%d sizes)", fpng, meow_font_has_size(28), meow_font_has_size(20)); return 1; } if (have_sel) logmsg("meow_font: font.file %s load failed, fallback", 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; } return 0; } /* 求「当前生效字体」的 png/idx 绝对路径 + family,供 launch_app 注入 MEOW_FONT_PNG/IDX/APP。 * 优先级(与 meow_font_ensure 一致):font.file(resolve_selected)→ 卡默认 MeowOS/fonts/default。 * family 取自 MeowOS/fonts//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); const char *prefix = "/MeowOS/fonts/"; const char *p = strstr(fpng, prefix); if (p) { const char *q = p + strlen(prefix); const char *end = strrchr(q, '/'); int rest = end ? (int)(end - q) : 0; if (rest > 0 && rest < flen) { memcpy(out_family, q, rest); out_family[rest] = 0; } else snprintf(out_family, flen, "default"); } else snprintf(out_family, flen, "default"); 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; } /* 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_bool("display.multi_font", &on); char gpng[400], gidx[400], gfam[96]; if (meow_font_effective_paths(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]; } } } 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=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.=1 / route..keep_awake=1) */ }; /* 服务行:service.=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 icon[129]; /* icon= 路径 */ int nroute; /* route. 行数 */ 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.=1 / route..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:TF 卡多字体库扫描 + preset 动态化 */ /* 字体目录 = <卡根>/MeowOS/fonts//,每套含三件套: */ /* font.png(预渲染灰度 PNG,bit depth 4|8)+ font.idx(MFNI v2 索引) */ /* + font.json(元数据 + 完整性校验)。缺任一件 → 该套不进列表 + 日志。 */ /* font.json schema(定版): */ /* { */ /* "family": "家族标识(同目录名)", // 信息用,不参与校验 */ /* "name": "下拉显示名", // 缺则回退目录名 */ /* "glyph_count": 10000, // 信息用 */ /* "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(文件缺失或解析不出则留空)。 */ static void meow_font_json_probe(const char *json_path, char *name, int nlen, char *sha_png, int slen, char *sha_idx, int xlen) { name[0] = 0; sha_png[0] = 0; sha_idx[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); } fclose(f); } /* 探测一个 / 套件是否合法:三件套齐全 → 解析 label + 校验 sha。 * 返回 1=进列表,0=跳过(缺组件/json 不可读)。sha 失败仅 WARN。 */ static int meow_font_probe_family(const char *dirpath, const char *family, char *label, int llen) { 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' (缺 font.png/font.idx/font.json)", family); return 0; } char name[64] = "", jsha_png[80] = "", jsha_idx[80] = ""; meow_font_json_probe(jpath, name, sizeof name, jsha_png, sizeof jsha_png, jsha_idx, sizeof jsha_idx); snprintf(label, llen, "%s", name[0] ? name : family); /* 显示名:name 或回退目录名 */ /* 完整性校验:仅 WARN,不硬失败;该套仍标记可选。 */ char apng[80] = "", aidx[80] = ""; if (font_sha256_file(ppath, apng, sizeof apng) != 0) logmsg("fonts: '%s' sha256(png) 不可用,跳过校验", family); else if (jsha_png[0] && !font_sha_eq(apng, jsha_png)) logmsg("fonts: '%s' WARN sha256_png 不匹配(仍可选)", family); if (font_sha256_file(ipath, aidx, sizeof aidx) != 0) logmsg("fonts: '%s' sha256(idx) 不可用,跳过校验", family); else if (jsha_idx[0] && !font_sha_eq(aidx, jsha_idx)) logmsg("fonts: '%s' WARN sha256_idx 不匹配(仍可选)", family); logmsg("fonts: add '%s' (%s)", label, family); return 1; } /* 扫描 /MeowOS/fonts/ 下每个家族子目录,重建字体 preset 列表(默认 + 卡内合法套件)。 * 上限 FONT_PRESETS_MAX(1 默认 + ≤16 用户字体)。无卡/无目录时仅默认。 */ static void meow_font_scan_presets_root(const char *card_root) { font_presets_n = 0; font_presets[0] = "MeowOS/fonts/default/font.png"; /* 默认字库 = MeowOS/fonts/default */ font_preset_labels[0] = "默认字体"; font_presets_n = 1; char root[340]; snprintf(root, sizeof root, "%s/%s", card_root, MEOW_FONTS_CFG); DIR *d = opendir(root); if (!d) return; /* 无卡/无字库目录:仅默认 */ struct dirent *de; while ((de = readdir(d)) && font_presets_n < FONT_PRESETS_MAX) { if (de->d_name[0] == '.') continue; char dirpath[400], label[64]; snprintf(dirpath, sizeof dirpath, "%s/%s", root, de->d_name); if (meow_font_probe_family(dirpath, de->d_name, label, sizeof label) == 0) continue; /* 缺组件/损坏 → 跳过该套 */ int i = font_presets_n; snprintf(font_preset_val_buf[i], sizeof font_preset_val_buf[i], "%s/%s/font.png", MEOW_FONTS_CFG, de->d_name); snprintf(font_preset_label_buf[i], sizeof font_preset_label_buf[i], "%s", label); font_presets[i] = font_preset_val_buf[i]; font_preset_labels[i] = font_preset_label_buf[i]; font_presets_n++; } closedir(d); } /* 设备默认卡根扫描(等价现状:无卡时 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; 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.=(activity 路由,M1) */ char *rp = strstr(buf, "route."); if (rp && tmp.nroute < MEOW_MAX_ROUTES) { rp += 6; /* 跳过 "route." */ int idl = 0; char id[16] = ""; while (*rp && *rp != '"' && *rp != ':' && *rp != '=' && idl < 15) 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++; 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); strncpy(tmp.routes[tmp.nroute].chain, chain, MEOW_CHAIN_MAX - 1); tmp.nroute++; } } /* awake.=1(M2.17 keep_awake:该 activity 常亮,超时不自动息屏)。 * 命中的既有 route. 置 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; } } /* service.=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; 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; } } /* 启动加载(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.* 配额) */ 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 语义:单击( 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 /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(由调用方决定回退策略)。 * - startend:跨午夜,如 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); 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; #define MENU_SWIPE_THRESH 65 /* px:抬手位移 >= 阈值判为滑动,不启动行(同 SDK SWIPE_THRESH) */ /* 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//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//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; } } 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; /* 本次以「无坐标」起步(防跨手势残留) */ } } 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); /* 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= (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 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 多字体:开关 + 6 个字符类别字体(preset=可用 MeowOS/fonts// 列表; * 值空 = 跟随全局 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 }, }; #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_bool("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(); /* 回绕 */ } /* SC_PRESET 行取「选项表 + 数量」:字体行(font.file + font.class_*)用运行时动态数组 * (TF 卡 MeowOS/fonts// 扫描结果,见字体区 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 名(如 "")原样通过。 */ 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):首行为表头,其余每行 "\t\t\t"。 * 填 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):首行列头,数据行形如 * [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 "。 * 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 ",含已配对+未配对发现,去重)。 */ 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 ",去重;含已配对 * 与未配对但已发现的设备)。缓存不存在/为空则退回已配对列表(保证至少展示已知设备)。 * 返回项数(≤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 ";发现会话里的 "[NEW] Device "。 * 跳过 [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 "(如 " < 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 ",取首个)。 * 定长 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; } 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(); } /* 打开 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; default: return; } s_net_refresh(); } /* ---- 控件绘制(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 多字体:开关或任一类别字体变更 → 重新去重装载类别字库并重绘(多字体可能影响文本渲染)。 */ 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_bool(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; } /* ---- 运行设置页(独立子循环;run_menu 触摸命中设置行后调用)---- */ static void run_settings_page(void) { int sel = s_first_setting(); int scroll_off = 0; int H = fb_yres; /* M2.5x:进设置页扫描一次 TF 卡 MeowOS/fonts//,刷新「字体文件」下拉。 * 进设置时卡通常在位;此步只枚举/校验,不加载(加载由 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_bool(r->key, &on); meow_settings_set_bool(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 的状态栏) */ 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"); } /* 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// → */ 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//{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//。 * 约定 .mpk = tar.gz,顶层即包目录(含 app.json);包名 = 文件名茎 .mpk。 * 同名先备份;解压后校验 app.json;随后数据 init(MeowData//{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 .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//;同名先备份(.bak_),再解压,再校验 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//{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//(数据目录本就由 env 指向 MeowData,MeowAPPs/ 下无 * data/cache 实体,直接全打包)→ .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.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_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//{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/.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_cls[MEOW_FCLS_COUNT][2][400]; char *envp[32]; /* PATH,HOME,log_dir,log_app,log_lvl,font 全局(3),多字体(1),class(≤12),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_bool("display.multi_font", &mf_on); snprintf(env_mf, sizeof env_mf, "MEOW_MULTI_FONT=%d", mf_on ? 1 : 0); envp[ne++] = env_mf; have_global = (meow_font_effective_paths(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]; } } 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-export ]\n" " [--ipc-only N] [--ipc-sock ] [--appinfo ]\n" " [--fontscan ]\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 :宿主/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.5x:启动扫描一次 TF 卡 MeowOS/fonts//,填充「字体文件」下拉 preset。 * 无卡/无字库目录 → 仅默认字体(等价现状)。进设置页时 run_settings_page 会再扫一次。 */ meow_font_scan_presets(); if (fontscan_root) { /* M2.5x 测试钩子:扫描指定卡根下的 MeowOS/fonts// 并打印 preset(qemu/宿主可测)。 * 用于验证 font.json 解析/三件套/缺件跳过/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\n", i, font_preset_labels[i], font_presets[i]); 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; }