7793 lines
414 KiB
C
7793 lines
414 KiB
C
/* 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下");
|
||
* 按压 <CLICK_MS(400ms)=单击一次,≥=长按(连续;如音量长按连续渐变)。音量渐变只在稳定边沿触发。
|
||
* - 本批次不做:通知中心 UI / MPRIS / 服务保活(后续里程碑)。播放器不动。
|
||
*
|
||
* 阶段 D · M0+M1:IPC 骨架 + 路由表(FRAMEWORK_DESIGN §1/§2/§9,版本 0.7.10):
|
||
* - M0 IPC 骨架:AF_UNIX /usr/data/meow_ipc.sock(unlink+bind+chmod0666+listen4),
|
||
* IPC 线程只读帧+解析+入队(SPSC 无锁环,__sync 原子),eventfd 唤醒主循环;
|
||
* 帧解析 = 16B 头(magic=0x4D4F5701/version=1/type/req_id/len,LE)→ 校验 → 读负载。
|
||
* MSG_HELLO→ACK / 未知→MSG_ERR;主循环 meow_ipc_process() 每 10ms 排空(零阻塞,
|
||
* 事件fd 仅为低延迟;即使 eventfd 失败,10ms 轮询仍保证排空)。日志埋点。
|
||
* - M1 路由表 + 解析:app.json 扁平行 route.<id>=path / framework=true /
|
||
* service.<svc>=mode:restart:max:window_ms / icon;scan_apps 解析入 g_accts 注册表;
|
||
* MSG_ACT_DECLARE(运行时并集覆盖)/ACT_QUERY/ACT_RESULT;resolve_chain(app,activity)
|
||
* 返回 [root..target] 链路与深度(route 缺失→深度1兜底;activity 未注册→MEOW_ERR_NOENT)。
|
||
* - 本阶段不做:通知存储(M2)、导航栈/launch_activity(M3)、UI(阶段 B)。播放器不动。
|
||
* 测试钩子:meow_ipc(patch_v2/meow_launcherd/meow_ipc.c,与 evtool 同定位)+
|
||
* launcher --ipc-only N --ipc-sock <path>(qemu sim 免 fb/输入)。
|
||
*
|
||
* M2.14 音量渐变批次(2026-08-29,版本 0.7.9):
|
||
* - audio.volume_step 默认 2 → **1**(SYSTEM_SETTINGS.md:默认 1,可配 1-5,clamp 上限 5)。
|
||
* - 线性渐变:settings_volume_adj(±step) 不再一次跳 N%,改起 vol_ramp_begin/tick 状态机——
|
||
* 每主循环 tick 按 **1%** 递增爬向目标(VOL_RAMP_MS=15ms 墙钟节流,不阻塞主循环),
|
||
* 每 1% 写一次 meow_dac_vol_write(真实 DAC);step>1 时不突兀、step=1 一步即达。
|
||
* - 长按持续渐变:到目标若按键仍按住则再延一段(vol_ramp_target += dir*step)直到松开/到上限;
|
||
* 连按=每次边沿叠加继续。落盘只在渐变结束一次。状态栏 VOL 随渐变刷新(显示爬升值/最终值)。
|
||
* MEOWOS_VERSION 0.7.8 → 0.7.9。
|
||
*
|
||
* M2.13 真实 DAC 音量批次(2026-08-28,版本 0.7.8):
|
||
* - 软件占位音量 → 真实 CS43198 DAC 音量:settings_volume_set/adj 每次变更即经
|
||
* amixer cset(SNDRV_CTL_IOCTL_ELEM_WRITE)直写 /dev/snd/controlC0 的
|
||
* "Left/Right Playback Volume"(numid 1/2,0-255 映射 0-100)到双片 CS43198。
|
||
* 新增 meow_dac_vol_write()/meow_vol_get()/meow_vol_set();进 MeowOS 时
|
||
* meow_sys_refresh 同步 DAC 至当前档。尊重上限:audio.volume_max(默认 100)clamp;
|
||
* 跳步更细 audio.volume_step(默认 2,可配回 5)。
|
||
* - 读回局限(§18.39):codec 驱动音量 get 为 dummy 恒 0(amixer cget 死镜像成因),
|
||
* /dev/i2c-3 被驱动占用(i2cget busy),故 meow_vol_get() 返回我方写入跟踪值。
|
||
* MEOWOS_VERSION 0.7.7 → 0.7.8。
|
||
*
|
||
* M2.12 迁移批次(2026-08-28,版本 0.7.7):
|
||
* - 外围 CLI(curl/socat/tmux/jq/rsync/nano/htop/ncdu/file/xxd)自 rootfs /usr/bin 迁至
|
||
* TF 卡 MeowOS/opt/bin/(Tools 皆静态 MIPS,无 .so 依赖);MeowOS 启动时卡在则 PATH
|
||
* 前插 /usr/data/mnt/sd_0/MeowOS/opt/bin(卡不在跳过)。固件 rootfs 瘦身(目标 ≤42MB)。
|
||
* - MeowAPPs 数据目录(exFAT 无符号链接 → 用环境变量,不做 symlink):launch_app 启动前
|
||
* 调 meow_app_prepare(exec) 建 MeowData/<pkg>/{data,cache}(如缺),并向子进程注入
|
||
* MEOW_DATA_DIR/MEOW_CACHE_DIR(与 MEOW_LOG_DIR 同族)。新增 .mpk 安装/导出接口
|
||
* meow_pkg_install/export + CLI 钩子 --pkg-install/--pkg-export(本期接口,UI 后期)。
|
||
* - 零卡依赖审查:settings 在 /data、字库/应用/日志/opt 均可缺省回退;拔卡不死、插卡自建。
|
||
* MEOWOS_VERSION 0.7.6 → 0.7.7。
|
||
*
|
||
* M2.11 修复批次(2026-08-28,版本 0.7.6):
|
||
* - A 系统状态全量重抓:新增 meow_sys_state + meow_sys_refresh(),每次进 MeowOS
|
||
* 重抓 电量/WiFi/蓝牙/音量/亮度/存储/系统;主循环 10ms 热键轮询绝不读系统态;
|
||
* 全量进系统一次 + 1s 轻刷时钟/电量/WiFi;状态栏统一读快照(不再 amixer 死镜像)。
|
||
* - B 电源菜单「按电源键」触发关机:电源=按当前项确认;默认光标改「取消」(安全项,
|
||
* 避免未导航按电源即关机);取消→只关菜单绝不关机;关机/重启/回HibyOS→按对应项才执行;
|
||
* 菜单里按电源键本身绝不触发 poweroff。菜单返回后清 pwr_hold_at/pwr_menu_state,
|
||
* 杜绝「按住电源开菜单→确认→仍按住→run_menu 8s 强关」残留覆盖。
|
||
* - C TF 卡热插拔最佳努力:拔出检测(/proc/mounts + /dev/mmcblk0 消失)→ 标记无卡、
|
||
* 停止扫描、优雅中止不崩;插入检测 → 自动挂载 + 重扫 MeowAPPs + 重载字库 + 日志迁回;
|
||
* 写日志/挂载前确认,失败回退内存;扫描遍历安全终止防死循环。
|
||
* - D 运行中应用卡拔:前台 MeowAPP 二进制/资源在卡上,卡被拔 → 启动器主动 SIGTERM
|
||
* 应用(干净退出,避免等 SIGBUS 崩),显示「卡已移除,应用已结束」,回菜单。
|
||
* - E:音量键调软件音量(settings audio.volume 0-100 步进5 clamp,持久化);
|
||
* 蓝牙读实际 powered(关=灰/不显示);WiFi 读实际(operstate);唤醒偶发加日志。
|
||
* MEOWOS_VERSION 0.7.5 → 0.7.6。
|
||
*
|
||
* M2.10 修复批次(2026-08-28):
|
||
* - Bug:电源菜单「回到 HibyOS」点一下就关机。根因:电源菜单 8s 强关(按住电源≥8s 绕过)与
|
||
* 用户点选按钮竞争 —— 8s 检查在触摸处理之前,用户按住电源开菜单后(仍按着)点「回到HibyOS/
|
||
* 重启」,8s 一到先触发强关(poweroff)而非执行所选动作。修法:用户一旦与菜单互动(音量导航/
|
||
* 触摸/播放/电源确认)置 pm_interacted=1,8s 强关取消;8s 强关移到输入处理之后。另复核
|
||
* 「回到HibyOS」动作即 set_boot_default_hiby()+sys_do_reboot(RESTART)(RESTART 已真机确认会
|
||
* 重启,非关机)。版本 0.7.5。
|
||
*
|
||
* M2.9 修复批次(2026-08-28):
|
||
* - Bug 2:电源菜单 POWER MENU 标题 y=300 与 2×2 按钮(pm_y0 顶缘 320)重叠被遮,
|
||
* 标题上移到 y=150(标题在上、按钮在下,不重叠)。
|
||
* - Bug 4:「返回播放器」确认框是/否按钮主循环内每 10ms 全刷 → 频闪;改为 dirty 绘制
|
||
* (仅 sel 变化才重画两钮),提示行移出循环只画一次。
|
||
* - 复核电源菜单动作直连 syscall(SYS_reboot)(M2.7 已改,本批确认路径一致)。
|
||
* 版本 0.7.4。
|
||
*
|
||
* M2.8 修复批次(2026-08-28):Bug 3 残留 —— MeowOS 主屏用电源键息屏后再按电源,
|
||
* 只有背光无画面(M2.7 加了 fb_norm_buf0 仍黑)。根因:fb_norm_buf0 在缓存
|
||
* fb_active_yoffset==0 时短路不再重发 FBIOPAN_DISPLAY,而 FBIOBLANK(4)->(0) 后 LCDC
|
||
* 扫描地址可能已复位到非 buffer0/失效位置,缓存仍是 0 导致真正的 re-pan 从未发出;
|
||
* 且唤醒不是 draw 事件,buffer0 内容可能为空。修法:新增 fb_pan_buf0_force(无条件
|
||
* 重发 pan)+ meow_repaint_surface(唤醒时把当前界面整体重绘到 buffer0 再强制 pan),
|
||
* 电源菜单/确认框也在「息屏态打开」时强制 re-pan。版本 0.7.3。
|
||
*
|
||
* M2.2 修复批次(2026-08-28):
|
||
* - MeowOS 常驻主界面:启动即自绘应用列表主界面(buffer0+pan0,不等热键),
|
||
* 绝不遗留播放器死帧;播放器已死时不备份/不冻结/不 SIGCONT、不恢复死帧。
|
||
* - BACK TO PLAYER(播放器已死)→ 显示「重启后返回播放器」提示并留在主界面。
|
||
* - MeowOS 系统拔营:无播放器时由 launcher 接管 TF 卡挂载(/dev/mmcblk0 ->
|
||
* /usr/data/mnt/sd_0, exfat)与 WiFi 联网(/usr/bin/wifi_up.sh),后台周期重试。
|
||
*
|
||
* Design (see ANALYSIS.md §18):
|
||
* - PASSIVE multi-reader: never EVIOCGRABs, never injects events.
|
||
* Physical key events multicast to all evdev readers on this kernel,
|
||
* but injected events (write()) update device state yet are NEVER
|
||
* delivered to any client (vendor input-subsystem quirk, verified).
|
||
* => keys are read via EVIOCGKEY state polling (immune to any grab,
|
||
* exact mechanism the player itself uses for the power key), touch is
|
||
* read via normal event reads on event1 (not grabbed by the player).
|
||
* - Hotkey (v6.5): HOLD power, then a SINGLE VOL_DOWN tap 60-2500ms after
|
||
* the power-down edge opens the menu. The player sees the power press (its
|
||
* screen-off flow starts — accepted side effect; the menu open undoes it
|
||
* via the verified unblank + bring-backlight-only-if-dark logic) and the
|
||
* VOL_DOWN tap (volume -1 — accepted). Legendary sequence mode
|
||
* (N quick taps of one key, default VOL_UP x3) remains available via
|
||
* meow.conf hotkey=seq; the seq taps raise volume, snapshotted and
|
||
* restored on trigger. A two-key chord on event2 is impossible (its four
|
||
* keys share ONE ADC channel), and the PREVIOUS key's GPIO (PC28) never
|
||
* fires on this unit (§18.12). Configurable via MeowAPPs/meow.conf:
|
||
* hotkey=power_voldown|seq / hotkey_seq=volup,volup,volup /
|
||
* hotkey_seq_gap=600. Inside an app, VOL_DOWN x3 quick taps force-kill
|
||
* the app (volume restored too).
|
||
* - Menu: SIGSTOP hiby_player, bring the backlight back ONLY if the panel
|
||
* is dark (brightness 0 / blanked — the player's own screen timeout or
|
||
* standby); a normal brightness is left untouched (Bug A: the old
|
||
* force_backlight() yanked the brightness to 101 on every menu open),
|
||
* draw app list on fb0 (scan /usr/data/mnt/sd_0/MeowAPPs/<id>/app.json,
|
||
* 5 edge cases), navigate with touch or keys, launch app via
|
||
* fork/execve/waitpid, spawn meow_watchdog <pid> for the virtual back
|
||
* key, then SIGCONT.
|
||
* - Double-buffer (v6.6): fb0 is double-buffered — FBIOGET_VSCREENINFO
|
||
* reports yres_virtual=1440 (2x720), smem_len=1382400 (exactly 2
|
||
* frames), ypanstep=1 (verified on device), and the DISPLAYED buffer is
|
||
* selected by fb_var.yoffset via FBIOPAN_DISPLAY (0x4606, verified to
|
||
* flip 0<->720). The player's power/screen-off redraw leaves yoffset on
|
||
* a back buffer, so the menu drawn ONLY at mmap offset 0 (buffer0) can
|
||
* land on the buffer the panel is NOT showing -> menu invisible, screen
|
||
* frozen on the player's last frame (the v6.5 bug). Fix: at menu open
|
||
* snapshot the player's displayed frame (the buffer at the current
|
||
* yoffset) into a heap backup, pan the display to buffer0 (what this
|
||
* code AND every app draw to — apps mmap exactly one frame at offset 0
|
||
* and never pan), draw the menu/app-list to buffer0, and on session end
|
||
* copy the backup back to the player's buffer + pan to the original
|
||
* yoffset so a SIGSTOPped player that does not redraw (event-driven,
|
||
* §18.15) still shows its own frame. fb is mapped over the FULL virtual
|
||
* framebuffer (both frames), not just one.
|
||
* - Backlight discipline (2026-08-28 hotfix): the launcher NEVER touches
|
||
* FBIOBLANK outside the menu. The player owns the panel blank state:
|
||
* the bkl_time ("背光时间") screen timeout writes brightness 0 +
|
||
* FBIOBLANK(4) and the player stops drawing while it believes the panel
|
||
* is blanked. The old backlight keeper restored brightness +
|
||
* FBIOBLANK(0) behind its back, which wedged the player's display state
|
||
* machine (UI frozen, keys dead — device log: keeper fired at 40.9s
|
||
* post-boot, player wedged). Do NOT reintroduce unblanking outside the
|
||
* menu. Inside the menu: unblank_panel() runs AFTER stop_player() so
|
||
* the frozen player cannot re-blank behind the menu (v6.2 hotfix —
|
||
* unblanking while the player was still running let it re-blank, so the
|
||
* menu was drawn on a black panel); on session end restore_backlight()
|
||
* re-blanks first (if the menu opened while blanked) and only then the
|
||
* player resumes, keeping its state machine consistent. ensure_backlight
|
||
* / restore_backlight only ever write brightness (harmless; ignored
|
||
* while the panel is blanked) and bl_power.
|
||
* - Player watchdog: SIGSTOP does not exit the process, so the
|
||
* hiby_player.sh "exit -> reboot" watchdog never triggers.
|
||
* - UI: pure black background by default (configurable via
|
||
* MeowAPPs/meow.conf bg_color=RRGGBB), slim 20px status bar with
|
||
* clock / battery / wifi.
|
||
* - Logging: unified to the TF card MeowOS/logs/meow_<bootts>.log
|
||
* (boot timestamp from wall-clock minus uptime); when the card is absent
|
||
* (S90 starts before the card mounts) logs go to a bounded /tmp boot
|
||
* buffer (/tmp/meow_boot.log, recoverable over SSH, survives daemon
|
||
* restarts) and are moved into the card log automatically once the card
|
||
* mounts (periodic retry in the main loop — no menu needed). NEVER write
|
||
* onboard storage, which is nearly full. Watchdog gets
|
||
* MEOW_LOG_FILE, apps get MEOW_LOG_DIR via the environment.
|
||
*
|
||
* Static MIPS32r2 musl build:
|
||
* ~/Project/R3ProII/.tools/zig-mipsel-cc -Os -static meow_launcherd.c -o meow_launcherd
|
||
*/
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <stdarg.h>
|
||
#include <unistd.h>
|
||
#include <fcntl.h>
|
||
#include <errno.h>
|
||
#include <signal.h>
|
||
#include <dirent.h>
|
||
#include <stdint.h>
|
||
#include <time.h>
|
||
#include <sys/ioctl.h>
|
||
#include <sys/mman.h>
|
||
#include <sys/poll.h>
|
||
#include "meow_touch.h" /* M2.25:Launcher 统一触摸接口(电源菜单/确认框复用) */
|
||
#include "pm_icons.h" /* M2.32:Material Symbols rounded 图标(电源菜单圆钮,alpha 位图+主题色) */
|
||
#include "sb_icons.h" /* MD3 状态栏/通知中心图标(Material Symbols rounded,32x32 单通道 alpha) */
|
||
#include <sys/wait.h>
|
||
#include <sys/stat.h>
|
||
#include <sys/syscall.h> /* Bug 2:直连 reboot(2) syscall 用 */
|
||
#include <pthread.h> /* M0:IPC 线程 */
|
||
#include <sys/eventfd.h> /* M0:IPC→主循环唤醒(EFD_NONBLOCK) */
|
||
#include <sys/socket.h> /* M0:AF_UNIX IPC */
|
||
#include <sys/un.h> /* M0:sockaddr_un */
|
||
#include <linux/input.h>
|
||
#include "meow_font.h" /* M2.6: 预渲染位图字体(agent-28 交付;对象已打包进 meow_ui.a,launcher 不再单独链 meow_font.a) */
|
||
#include "meow_ipc.h" /* M0+M1:IPC 协议帧/消息/负载定(与 meow_ipc 工具同源) */
|
||
#include "meow_settings.h" /* B2a:统一系统设置后端(SYSTEM_SETTINGS.md §3/MEOWOS_API §2) */
|
||
#include "meow_notif.h" /* M2a:通知存储 + 分发(NOTIFICATION_SYSTEM.md §4/§7/§8) */
|
||
#include "meow_ui.h" /* A-1/A-2:launcher 用 SDK 渲染栈(meow_ui_fill_rect/text/flush/theme) */
|
||
#include "meow_ui_internal.h" /* A-1/A-2:launcher 与 SDK 共用 back_buf + 手动脏模型桥接(mark_full/back_buf) */
|
||
|
||
/* ---- paths ---- */
|
||
#define DEV_POWER "/dev/input/event0" /* md-gpio-keys: KEY_POWER, KEY_PREVIOUS */
|
||
#define DEV_KEYS "/dev/input/event2" /* jz adc keyboard: VOL, PLAY, NEXT */
|
||
#define DEV_TOUCH "/dev/input/event1" /* goodix-ts (not grabbed, shareable) */
|
||
#define MEOWAPPS "/usr/data/mnt/sd_0/MeowAPPs"
|
||
#define LOG_DIR_CARD "/usr/data/mnt/sd_0/MeowOS/logs" /* TF card logs(系统约定:系统文件放 MeowOS/ 下) */
|
||
#define PID_PATH "/usr/data/meow_launcherd.pid"
|
||
#define WATCHDOG1 "/usr/data/meow_watchdog"
|
||
#define WATCHDOG2 "/usr/bin/meow_watchdog"
|
||
/* M2.24 全面按键接管:watchdog(应用运行期)检测到电源长按≥3s 写此标记,launcher 在
|
||
* waitpid 轮询中消费 → SIGSTOP app + 弹统一电源菜单;菜单期 launcher 写 powmenu_active
|
||
* 让 watchdog 让位,菜单关后清除再 SIGCONT app。 */
|
||
#define POWMENU_MARKER "/tmp/meow_power_menu"
|
||
#define POWMENU_ACTIVE "/tmp/meow_power_menu_active"
|
||
#define BACKLIGHT "/sys/class/backlight/backlight_pwm0/brightness"
|
||
#define BACKLIGHT_POWER "/sys/class/backlight/backlight_pwm0/bl_power"
|
||
#define FB_DEV "/dev/fb0"
|
||
#define MOUNTS "/proc/mounts"
|
||
|
||
/* ---- key codes ---- */
|
||
#ifndef KEY_POWER
|
||
#define KEY_POWER 116
|
||
#endif
|
||
#ifndef KEY_PREVIOUSSONG
|
||
#define KEY_PREVIOUSSONG 165
|
||
#endif
|
||
#ifndef KEY_VOLUMEDOWN
|
||
#define KEY_VOLUMEDOWN 114
|
||
#endif
|
||
#ifndef KEY_VOLUMEUP
|
||
#define KEY_VOLUMEUP 115
|
||
#endif
|
||
#ifndef KEY_PLAYPAUSE
|
||
#define KEY_PLAYPAUSE 164
|
||
#endif
|
||
#ifndef KEY_NEXT
|
||
#define KEY_NEXT 163
|
||
#endif
|
||
|
||
/* ---- tuning ---- */
|
||
#define SEQ_DEFAULT_TAPS 3 /* default hotkey: VOL_UP x3 quick taps */
|
||
#define POLL_MS 10
|
||
/* Single source of truth for the MeowOS build version. The on-screen label
|
||
* ("MeowOS v<MEOWOS_VERSION> dev", bottom-right of the app list) and the
|
||
* etc/meowos_version file that build_fw_apps.sh greps from this line must both
|
||
* agree — change the version HERE only, never in two places. */
|
||
#define MEOWOS_VERSION "dev" /* M2.50:版本号唯一来源 = /etc/meowos_version(运行时 load_meowos_version 读);本宏仅作文件缺失时占位,不再当实际版本,升级只改 /etc/meowos_version */
|
||
#define MAX_APPS 8
|
||
#define TOAST_MS 1500
|
||
#define LOG_SIZE_LIMIT (1u << 30) /* 1 GB total on the TF card */
|
||
/* 阶段 D · M1:Activity 路由注册表(g_accts)容量。route=app.json/声明的路由行,
|
||
* svc=service.<svc> 行(M1 仅解析存储,保活 M4 实现)。 */
|
||
#define MEOW_MAX_ACTS 16
|
||
#define MEOW_MAX_ROUTES 8
|
||
#define MEOW_MAX_SVCS 8
|
||
|
||
/* ---- event codes / ioctls ---- */
|
||
#define BTN_TOUCH 0x14a
|
||
#define ABS_X 0x00
|
||
#define ABS_Y 0x01
|
||
#define ABS_MT_POSITION_X 0x35
|
||
#define ABS_MT_POSITION_Y 0x36
|
||
#ifndef EV_KEY
|
||
#define EV_KEY 1
|
||
#endif
|
||
#ifndef EV_ABS
|
||
#define EV_ABS 3
|
||
#endif
|
||
|
||
#ifndef EVIOCGKEY
|
||
#define EVIOCGKEY(len) _IOC(_IOC_READ, 'E', 0x18, len)
|
||
#endif
|
||
#define KEYBUF_WORDS 32 /* 1024 bits, covers KEY_MAX 767 */
|
||
|
||
/* ================================================================== */
|
||
/* unified logging: TF card MeowOS/logs/meow_<bootts>.log */
|
||
/* - no TF card at startup (S90 runs before the card mounts) -> */
|
||
/* bounded /tmp boot buffer (/tmp/meow_boot.log): recoverable */
|
||
/* over SSH, survives daemon restarts within the boot session */
|
||
/* - log_poll_card() (called periodically in the main loop) opens */
|
||
/* the card log as soon as the card mounts and moves the /tmp */
|
||
/* buffer into it — no menu needed */
|
||
/* - NEVER write onboard storage (nearly full) */
|
||
/* ================================================================== */
|
||
#define BOOT_BUF_TMP "/tmp/meow_boot.log"
|
||
#define BOOT_BUF_MAX (256 * 1024)
|
||
static int log_fd = -1; /* file log fd on the TF card */
|
||
static char log_dir[256] = ""; /* "MeowOS/logs" on card, or "" */
|
||
static char log_file[256] = ""; /* full path of the boot log file */
|
||
|
||
static int card_available(void)
|
||
{
|
||
/* TF card mounted? */
|
||
FILE *f = fopen(MOUNTS, "r");
|
||
if (!f) return 0;
|
||
char line[256];
|
||
int found = 0;
|
||
while (fgets(line, sizeof line, f)) {
|
||
if (strstr(line, " /usr/data/mnt/sd_0 ")) { found = 1; break; }
|
||
}
|
||
fclose(f);
|
||
if (!found) return 0;
|
||
/* writable logs dir? */
|
||
mkdir(LOG_DIR_CARD, 0755);
|
||
return access(LOG_DIR_CARD, W_OK) == 0;
|
||
}
|
||
|
||
static void boot_timestamp(char *out, size_t n)
|
||
{
|
||
/* reuse the timestamp cached for this boot session (/tmp is cleared on
|
||
* reboot) so repeated launcher starts in one boot share one log file */
|
||
int fd = open("/tmp/meow_bootts", O_RDONLY);
|
||
if (fd >= 0) {
|
||
int r = read(fd, out, (int)n - 1);
|
||
close(fd);
|
||
if (r > 0) { out[r] = 0; return; }
|
||
}
|
||
time_t now = time(NULL);
|
||
time_t up = 0;
|
||
fd = open("/proc/uptime", O_RDONLY);
|
||
if (fd >= 0) {
|
||
char buf[64];
|
||
int r = read(fd, buf, sizeof buf - 1);
|
||
close(fd);
|
||
if (r > 0) {
|
||
buf[r] = 0;
|
||
char *p = strchr(buf, ' ');
|
||
if (p) *p = 0;
|
||
up = (time_t)atof(buf);
|
||
}
|
||
}
|
||
time_t boot = (time_t)((double)now - (double)up + 0.5);
|
||
struct tm tm;
|
||
localtime_r(&boot, &tm);
|
||
strftime(out, n, "meow_%Y%m%d_%H%M%S", &tm);
|
||
/* cache for the rest of this boot session */
|
||
fd = open("/tmp/meow_bootts", O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||
if (fd >= 0) {
|
||
write(fd, out, strlen(out));
|
||
close(fd);
|
||
}
|
||
}
|
||
|
||
static unsigned long long now_ms(void)
|
||
{
|
||
struct timespec ts;
|
||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||
return (unsigned long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||
}
|
||
|
||
/* M2.18 Bug 1: 无符号回绕保护的「空闲时长」。
|
||
* launch_app()/confirm_reboot_player() 是阻塞调用,主循环 for(;;) 顶部的 t=now_ms()
|
||
* 在 app 运行期间被卡住而停在启动前旧值;app 退出后 last_input_ms 被置为 app 退出
|
||
* 时刻(较大值)。若该圈 any_input==0(如触摸释放单独落在下一圈、音量/播放边沿发生在
|
||
* 更早圈),line「if (any_input) last_input_ms = t」不执行 → 检查处 t < last_input_ms,
|
||
* (unsigned long long)(t - last_input_ms) 无符号相减回绕成 2^64-Δ 的极大值,恒 >= 30s
|
||
* → 误判「空闲超时」→ 立即 meow_panel_off()。r3proii 日志铁证:auto screen-off after
|
||
* 30s idle (t=141578 last=145688),仅 122ms 即被误判为已空闲 30s。凡判定空闲一律经此
|
||
* 函数取差:t < last(t 过期)时返回 0,绝不回绕。 */
|
||
static unsigned long long idle_ms_since(unsigned long long t, unsigned long long last)
|
||
{
|
||
return (t >= last) ? (t - last) : 0;
|
||
}
|
||
|
||
/* remove oldest log files until total size <= limit (called at boot) */
|
||
static void log_cleanup_old(void)
|
||
{
|
||
struct dirent *de;
|
||
DIR *d = opendir(LOG_DIR_CARD);
|
||
if (!d) return;
|
||
/* collect meow_*.log with mtime + size */
|
||
struct logfile { char name[64]; long long size; long long mtime; } files[64];
|
||
int n = 0;
|
||
while ((de = readdir(d)) && n < 64) {
|
||
if (strncmp(de->d_name, "meow_", 5)) continue;
|
||
char path[320];
|
||
snprintf(path, sizeof path, "%s/%s", LOG_DIR_CARD, de->d_name);
|
||
struct stat st;
|
||
if (stat(path, &st) || !S_ISREG(st.st_mode)) continue;
|
||
files[n].mtime = (long long)st.st_mtime;
|
||
files[n].size = (long long)st.st_size;
|
||
snprintf(files[n].name, sizeof files[n].name, "%s", de->d_name);
|
||
n++;
|
||
}
|
||
closedir(d);
|
||
long long total = 0;
|
||
for (int i = 0; i < n; i++) total += files[i].size;
|
||
/* simple selection sort by mtime ascending (oldest first) */
|
||
for (int i = 0; i < n; i++)
|
||
for (int j = i + 1; j < n; j++)
|
||
if (files[j].mtime < files[i].mtime) {
|
||
struct logfile t = files[i];
|
||
files[i] = files[j]; files[j] = t;
|
||
}
|
||
int deleted = 0;
|
||
for (int i = 0; i < n && total > (long long)LOG_SIZE_LIMIT; i++) {
|
||
char path[320];
|
||
snprintf(path, sizeof path, "%s/%s", LOG_DIR_CARD, files[i].name);
|
||
if (unlink(path) == 0) { total -= files[i].size; deleted++; }
|
||
}
|
||
if (deleted) fprintf(stderr, "log_cleanup: deleted %d old log(s), total now %lld\n",
|
||
deleted, total);
|
||
}
|
||
|
||
/* try to open the card log file; returns 0 on success */
|
||
static int log_open_card(void)
|
||
{
|
||
if (log_fd >= 0) return 0;
|
||
if (!card_available()) return -1;
|
||
char ts[64];
|
||
boot_timestamp(ts, sizeof ts);
|
||
snprintf(log_dir, sizeof log_dir, "%s", LOG_DIR_CARD);
|
||
snprintf(log_file, sizeof log_file, "%s/%s.log", LOG_DIR_CARD, ts);
|
||
int fd = open(log_file, O_WRONLY | O_APPEND | O_CREAT, 0644);
|
||
if (fd < 0) return -1;
|
||
log_fd = fd;
|
||
return 0;
|
||
}
|
||
|
||
static void logmsg(const char *fmt, ...);
|
||
|
||
/* flush the /tmp boot buffer into the file log (once the card mounts) */
|
||
static void log_poll_card(void)
|
||
{
|
||
if (log_fd >= 0) return;
|
||
if (log_open_card() != 0) return;
|
||
/* move the /tmp boot buffer into the card log, then drop it */
|
||
int fd = open(BOOT_BUF_TMP, O_RDONLY);
|
||
if (fd >= 0) {
|
||
char chunk[1024];
|
||
ssize_t n;
|
||
while ((n = read(fd, chunk, sizeof chunk)) > 0) {
|
||
if (write(log_fd, chunk, n) < 0) { close(fd); log_fd = -1; return; }
|
||
}
|
||
close(fd);
|
||
unlink(BOOT_BUF_TMP);
|
||
logmsg("boot buffer moved into %s", log_file);
|
||
} else {
|
||
logmsg("card log open: %s", log_file);
|
||
}
|
||
}
|
||
|
||
/* bounded append to the /tmp boot buffer (no card log open yet) */
|
||
static void boot_buf_put(const char *line, int len)
|
||
{
|
||
struct stat st;
|
||
if (stat(BOOT_BUF_TMP, &st) == 0 && st.st_size > BOOT_BUF_MAX)
|
||
unlink(BOOT_BUF_TMP); /* roll the buffer (boot-stage only) */
|
||
int fd = open(BOOT_BUF_TMP, O_WRONLY | O_APPEND | O_CREAT, 0644);
|
||
if (fd < 0) return;
|
||
if (write(fd, line, len) < 0) { /* best effort */ }
|
||
close(fd);
|
||
}
|
||
|
||
static void logmsg(const char *fmt, ...)
|
||
{
|
||
char buf[512];
|
||
va_list ap;
|
||
va_start(ap, fmt);
|
||
vsnprintf(buf, sizeof buf, fmt, ap);
|
||
va_end(ap);
|
||
char line[576];
|
||
int l = snprintf(line, sizeof line, "%llu %s\n", now_ms(), buf);
|
||
if (log_fd >= 0) {
|
||
if (write(log_fd, line, l) < 0) log_fd = -1; /* card gone -> buffer */
|
||
}
|
||
if (log_fd < 0) boot_buf_put(line, l);
|
||
fprintf(stderr, "%llu %s\n", now_ms(), buf);
|
||
}
|
||
|
||
/* ================================================================== */
|
||
/* fb0 helpers (RGB565 / RGBA8888 auto) */
|
||
/* ================================================================== */
|
||
/* NOTE: on this device FBIOGET_VSCREENINFO=0x4600, FSCREENINFO=0x4602
|
||
* (device-verified legacy numbers; do NOT swap them). */
|
||
#define FBIOGET_VSCREENINFO 0x4600
|
||
#define FBIOGET_FSCREENINFO 0x4602
|
||
#define FBIOPAN_DISPLAY 0x4606
|
||
#define FBIOBLANK 0x4611
|
||
#define FBIO_WAITFORVSYNC 0x4620
|
||
|
||
static unsigned char *fb; /* 绘制目标指针 = SDK 的 back_buf(A-1/A-2:launcher 与 app 统一渲染栈) */
|
||
static unsigned char *fb_mem = NULL; /* 真实 mmap 帧缓冲(播放器帧备份/恢复源 + app 帧读回 + 双缓冲 yoffset 用) */
|
||
static int fb_xres, fb_yres, fb_line, fb_bpp;
|
||
static int fb_fd = -1;
|
||
static unsigned int fb_yres_virtual = 0; /* virtual height (double-buffer = 2*yres) */
|
||
static unsigned int fb_active_yoffset = 0; /* scanline of the displayed buffer (0 or yres) */
|
||
static int fb_orig_yoffset = -1; /* player's yoffset at menu open (saved) */
|
||
static unsigned char *fb_player_backup = NULL;
|
||
static size_t fb_player_backup_len = 0;
|
||
|
||
/* ---- A-1/A-2:launcher 用 SDK 渲染栈 ----
|
||
* fb 指向 SDK 的 back_buf(meow_ui_back_buf()),所有绘制(put_px/meow_font/dim_screen/
|
||
* draw_menu/statusbar/电源菜单/确认框)画进 SDK 的离屏;呈现统一走 meow_ui_flush()(= SDK present,
|
||
* 内含 vsync + 帧率门控 + 原子 blit 脏区)。launcher 不再自建 back_buf/meow_present/帧率门控。
|
||
* fb_mem 仍保留 launcher 自有的全虚拟 mmap:仅用于播放器帧备份/恢复/双缓冲 yoffset 切换
|
||
* (fb_backup_player/fb_norm_buf0/fb_restore_player/fb_pan_buf0_force)与 app 帧读回
|
||
* (pm_capture_app_frame)——这些是「显示 buffer/页码切换」,与「present 上屏」不同层,不动。 */
|
||
static int back_buf_ok = 0; /* fb 是否指向 SDK back_buf(1=可经 SDK present 上屏) */
|
||
|
||
/* ---- M2.16 按键防抖 + 单击/长按界限 ---- */
|
||
#define KEY_DEBOUNCE_MS 40 /* 键值须稳定此毫秒数才认可(抗 ADC 抖动) */
|
||
#define CLICK_MS 400 /* 按压 <此毫秒 = 单击一次;≥ = 长按(连续) */
|
||
struct kd_state { int raw, stable, down; int longpressed; unsigned long long since, down_at; };
|
||
|
||
/* ---- M2.17 超时自动息屏(display.screen_off_timeout / screen_off_mode)----
|
||
* timeout 单位秒,0=常亮(不自动息屏);mode:0=panel(面板下电+背光0+FBIOBLANK(4),挂起触摸
|
||
* 仅电源/任意键唤醒)、1=black(背光0全黑,触摸可唤醒)。见 SYSTEM_SETTINGS.md。 */
|
||
#define SCREEN_OFF_TIMEOUT_DEFAULT 30
|
||
#define SCREEN_OFF_MODE_DEFAULT 0 /* 0=panel, 1=black */
|
||
static int screen_off_timeout = SCREEN_OFF_TIMEOUT_DEFAULT;
|
||
static int screen_off_mode = SCREEN_OFF_MODE_DEFAULT;
|
||
|
||
/* ---- M2.37 电源长按语义缓存(power.longpress_menu_ms / power.forceoff_ms)----
|
||
* 设置页可配;默认 3000/8000 与注释「3s 电源菜单 / 8s 强关」一致。缓存供 run_menu
|
||
* 电源按住判定使用(原为硬编码 2000/8000/3000,现接线读取以让设置生效)。 */
|
||
#define PWR_MENU_MS_DEFAULT 3000
|
||
#define PWR_FORCE_MS_DEFAULT 8000
|
||
static int power_longpress_menu_ms = PWR_MENU_MS_DEFAULT;
|
||
static int power_forceoff_ms = PWR_FORCE_MS_DEFAULT;
|
||
|
||
/* ---- M2.16 刷新率调度配置(display.refresh_rate / refresh_always_max)----
|
||
* A-1/A-2:档位上限/恒满速开关由 launcher 配置(settings.conf),经
|
||
* meow_ui_set_refresh_rate()/meow_ui_set_refresh_always_max() 交给 SDK 的 present
|
||
* 帧率门控/governor 执行(归口 PERF_SCHEDULER 显示刷新率子域)。当前有效档 eff_rate
|
||
* 与帧计数已在 SDK 侧,launcher 不再各自维护一套。 */
|
||
#define REFRESH_RATE_CFG_DEFAULT 60
|
||
static int refresh_rate_cfg = REFRESH_RATE_CFG_DEFAULT; /* 目标档 60/30/15(上限,default 60) */
|
||
static int refresh_always_max = 0; /* 1=恒 max 不降档 */
|
||
|
||
/* M2.16 渲染/防抖前向声明(定义在下方;fb_open 等先引用) */
|
||
static void meow_dirty_full(void);
|
||
static void meow_dirty_add(int x, int y, int w, int h);
|
||
static void meow_dirty_reset(void);
|
||
|
||
static inline void put_px(int x, int y, uint32_t color)
|
||
{
|
||
if (x < 0 || x >= fb_xres || y < 0 || y >= fb_yres) return;
|
||
unsigned char *p = fb + y * fb_line + x * (fb_bpp / 8);
|
||
if (fb_bpp == 16) {
|
||
uint16_t c = ((color >> 8) & 0xF800) | ((color >> 5) & 0x07E0) |
|
||
((color >> 3) & 0x001F);
|
||
*(uint16_t *)p = c;
|
||
} else {
|
||
*(uint32_t *)p = (color << 8) | 0xFF;
|
||
}
|
||
}
|
||
|
||
static void fill_rect(int x0, int y0, int w, int h, uint32_t color)
|
||
{
|
||
for (int y = y0; y < y0 + h; y++)
|
||
for (int x = x0; x < x0 + w; x++)
|
||
put_px(x, y, color);
|
||
}
|
||
|
||
/* 5x7 font (same table as hello.c) */
|
||
static const unsigned char font[][8] = {
|
||
{0x0E,0x11,0x13,0x15,0x19,0x11,0x0E,0x00},
|
||
{0x04,0x0C,0x04,0x04,0x04,0x04,0x0E,0x00},
|
||
{0x0E,0x11,0x01,0x02,0x04,0x08,0x1F,0x00},
|
||
{0x1F,0x02,0x04,0x02,0x01,0x11,0x0E,0x00},
|
||
{0x02,0x06,0x0A,0x12,0x1F,0x02,0x02,0x00},
|
||
{0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E,0x00},
|
||
{0x06,0x08,0x10,0x1E,0x11,0x11,0x0E,0x00},
|
||
{0x1F,0x01,0x02,0x04,0x08,0x08,0x08,0x00},
|
||
{0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E,0x00},
|
||
{0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C,0x00},
|
||
{0x0E,0x11,0x11,0x1F,0x11,0x11,0x11,0x00},
|
||
{0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E,0x00},
|
||
{0x0E,0x11,0x10,0x10,0x10,0x11,0x0E,0x00},
|
||
{0x1C,0x12,0x11,0x11,0x11,0x12,0x1C,0x00},
|
||
{0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F,0x00},
|
||
{0x1F,0x10,0x10,0x1E,0x10,0x10,0x10,0x00},
|
||
{0x0E,0x11,0x10,0x17,0x11,0x11,0x0F,0x00},
|
||
{0x11,0x11,0x11,0x1F,0x11,0x11,0x11,0x00},
|
||
{0x0E,0x04,0x04,0x04,0x04,0x04,0x0E,0x00},
|
||
{0x07,0x02,0x02,0x02,0x02,0x12,0x0C,0x00},
|
||
{0x11,0x12,0x14,0x18,0x14,0x12,0x11,0x00},
|
||
{0x10,0x10,0x10,0x10,0x10,0x10,0x1F,0x00},
|
||
{0x11,0x1B,0x15,0x15,0x11,0x11,0x11,0x00},
|
||
{0x11,0x19,0x15,0x13,0x11,0x11,0x11,0x00},
|
||
{0x0E,0x11,0x11,0x11,0x11,0x11,0x0E,0x00},
|
||
{0x1E,0x11,0x11,0x1E,0x10,0x10,0x10,0x00},
|
||
{0x0E,0x11,0x11,0x11,0x15,0x12,0x0D,0x00},
|
||
{0x1E,0x11,0x11,0x1E,0x14,0x12,0x11,0x00},
|
||
{0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E,0x00},
|
||
{0x1F,0x04,0x04,0x04,0x04,0x04,0x04,0x00},
|
||
{0x11,0x11,0x11,0x11,0x11,0x11,0x0E,0x00},
|
||
{0x11,0x11,0x11,0x11,0x11,0x0A,0x04,0x00},
|
||
{0x11,0x11,0x11,0x15,0x15,0x1B,0x11,0x00},
|
||
{0x11,0x11,0x0A,0x04,0x0A,0x11,0x11,0x00},
|
||
{0x11,0x11,0x11,0x0A,0x04,0x04,0x04,0x00},
|
||
{0x1F,0x01,0x02,0x04,0x08,0x10,0x1F,0x00},
|
||
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
|
||
{0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00},
|
||
{0x04,0x04,0x04,0x04,0x04,0x00,0x04,0x00},
|
||
{0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00},
|
||
{0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00},
|
||
};
|
||
|
||
static int glyph_index(char c)
|
||
{
|
||
if (c >= '0' && c <= '9') return c - '0';
|
||
if (c >= 'A' && c <= 'Z') return 10 + (c - 'A');
|
||
if (c >= 'a' && c <= 'z') return 10 + (c - 'a');
|
||
if (c == ' ') return 36;
|
||
if (c == '.') return 37;
|
||
if (c == '!') return 38;
|
||
if (c == '-' || c == '_') return 39;
|
||
return 36;
|
||
}
|
||
|
||
static void draw_glyph(int x, int y, char c, int scale, uint32_t color)
|
||
{
|
||
const unsigned char *g = font[glyph_index(c)];
|
||
for (int row = 0; row < 7; row++) {
|
||
unsigned char bits = g[row];
|
||
for (int col = 0; col < 5; col++) {
|
||
if (bits & (0x10 >> col)) {
|
||
for (int dy = 0; dy < scale; dy++)
|
||
for (int dx = 0; dx < scale; dx++)
|
||
put_px(x + col * scale + dx, y + row * scale + dy, color);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
static void draw_text(int x, int y, const char *s, int scale, uint32_t color)
|
||
{
|
||
while (*s) {
|
||
draw_glyph(x, y, *s, scale, color);
|
||
x += 6 * scale;
|
||
s++;
|
||
}
|
||
}
|
||
|
||
/* ================================================================== */
|
||
/* M2.6 meow_font 集成(agent-28 字体子系统,font/meow_font.a + libz.a)*/
|
||
/* 字库放 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/<family>/(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 中文标签(含档位大小标记) */
|
||
/* M2.5x 字体档位:每套的 charset 档("common"/"gbk"/"full",取自 font.json,缺省 "common")
|
||
* 与 glyph_count(信息用)。供档位分层装载(meow_font_effective_paths_tiered)与设置页标注。 */
|
||
static const char *font_preset_tiers[FONT_PRESETS_MAX];
|
||
static int font_preset_glyphs[FONT_PRESETS_MAX];
|
||
static char font_preset_val_buf[FONT_PRESETS_MAX][320]; /* 值持久存储(指向上面指针数组) */
|
||
static char font_preset_label_buf[FONT_PRESETS_MAX][80]; /* 标签持久存储 */
|
||
static char font_preset_tier_buf[FONT_PRESETS_MAX][8]; /* charset 持久存储 */
|
||
static int font_presets_n = 0; /* 运行时动态计数(默认=1,仅"默认字体") */
|
||
static int mfont_ready = 0;
|
||
static int mfont_forced = 0; /* font.file 变更→强制真实(un)load,跳过 SDK 复用捷径 */
|
||
|
||
/* 24-bit RGB → RGB565(与 put_px/meow_font 一致) */
|
||
static uint16_t rgb565(uint32_t c)
|
||
{
|
||
return (uint16_t)(((c >> 8) & 0xF800) | ((c >> 5) & 0x07E0) | ((c >> 3) & 0x001F));
|
||
}
|
||
|
||
/* fb_open 后调用:设置字库绘制目标为当前双缓冲(buffer0 起点 = fb 偏移 0) */
|
||
static void meow_font_bind_fb(void)
|
||
{
|
||
meow_font_set_fb(fb_xres, fb_yres, fb_line);
|
||
}
|
||
|
||
/* 把 font.file 存的「卡根相对/绝对」路径解析为设备绝对路径。
|
||
* 相对路径以 /usr/data/mnt/sd_0/ 为卡根(与 MEOWAPPS 同源);绝对路径原样。
|
||
* 返回 0 成功,-1 参数无效。 */
|
||
static int meow_font_resolve_path(const char *ref, char *out, int olen)
|
||
{
|
||
if (!ref || !ref[0]) return -1;
|
||
if (ref[0] == '/') { snprintf(out, olen, "%s", ref); return 0; }
|
||
snprintf(out, olen, "/usr/data/mnt/sd_0/%s", ref);
|
||
return 0;
|
||
}
|
||
|
||
/* 前向声明:meow_font_ensure 在下方 font 区中调用(定义在 meow_font_effective_paths_tiered 处)。 */
|
||
static int meow_font_effective_tier(int app_tier);
|
||
static int meow_font_effective_paths_tiered(int tier, char *out_png, int plen,
|
||
char *out_idx, int ilen, char *out_family, int flen);
|
||
|
||
/* 解析 font.file 指向的 png + 派生同目录 idx(font.png → font.idx)。
|
||
* 仅当指向的 png 存在(access R_OK)返回 0;否则 -1(交由回退链)。
|
||
* 返回 0:out_png/out_idx 已填设备绝对路径。 */
|
||
static int meow_font_resolve_selected(char *out_png, int png_len, char *out_idx, int idx_len)
|
||
{
|
||
char ref[MEOW_SET_MAX_LEN];
|
||
if (meow_settings_get_str("font.file", ref, sizeof ref) != 0) return -1;
|
||
if (meow_font_resolve_path(ref, out_png, png_len) != 0) return -1;
|
||
if (access(out_png, R_OK) != 0) return -1; /* 指向不存在 → 回退卡默认/固件 */
|
||
snprintf(out_idx, idx_len, "%s", out_png); /* 同目录 font.idx(标准三件套布局) */
|
||
size_t n = strlen(out_idx);
|
||
if (n > 4 && !strcmp(out_idx + n - 4, ".png")) {
|
||
out_idx[n-4] = '.'; out_idx[n-3] = 'i'; out_idx[n-2] = 'd'; out_idx[n-1] = 'x'; out_idx[n] = 0;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* 尝试加载字库;成功返回 1。优先级(M2.5x,font.load_full 已接入):
|
||
* 0) 生效档 = font.load_full ? gbk : common(launcher 自身 UI 跟随系统大字库开关);
|
||
* 1) 生效字体(tier 解析,tier0 即 font.file 所选;相对路径按卡根解析);
|
||
* 2) 卡 MeowOS/fonts/default/font.png(默认字库);
|
||
* 3) 固件 /usr/share/meowos/font/font.png。
|
||
* A-1/A-2:SDK meow_ui_init() 已用同一 meow_font.o 加载过(可能成功也可能未成功)。
|
||
* 当字体指向「默认卡字库」或未设置时复用 SDK 已加载状态,避免重复 unload+reload;
|
||
* 当字体指向自定义字库时必须显式 load(SDK 只加载默认,不读 font.file)。
|
||
* mfont_forced=1(font.file/font.load_full 变更触发)时无条件真实 reload,跳过上述捷径。
|
||
* 全失败 → mfont_ready=0,ui_draw 回退 5x7 点阵(系统不死)。 */
|
||
static int meow_font_ensure(void)
|
||
{
|
||
if (mfont_ready && !mfont_forced) return 1;
|
||
mfont_forced = 0;
|
||
|
||
char fpng[400], fidx[400], fam[96];
|
||
int tier = meow_font_effective_tier(MEOW_FONT_TIER_COMMON); /* launcher 自身 UI 跟随 load_full */
|
||
int have_sel = (meow_font_effective_paths_tiered(tier, fpng, sizeof fpng,
|
||
fidx, sizeof fidx, fam, sizeof fam) == 0);
|
||
|
||
/* SDK 已加载且所选字库为「默认卡字库」(或未设置)→ 复用,避免重复解码。 */
|
||
if (meow_gfx_mfont_ready() && (!have_sel || !strcmp(fpng, UI_FONT_PNG_CARD))) {
|
||
mfont_ready = 1;
|
||
logmsg("meow_font: reuse sdk-loaded font");
|
||
return 1;
|
||
}
|
||
|
||
/* 生效字体优先(tier=common 时即 font.file;load_full → 大字库 gbk 档)。 */
|
||
if (have_sel && meow_font_load(fpng, fidx) == 0) {
|
||
mfont_ready = 1;
|
||
logmsg("meow_font: loaded %s (%d/%d sizes), tier=%d", fpng,
|
||
meow_font_has_size(28), meow_font_has_size(20), tier);
|
||
return 1;
|
||
}
|
||
if (have_sel)
|
||
logmsg("meow_font: %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/<family>/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;
|
||
}
|
||
|
||
/* 档位 → font.json charset 字符串名(common/gbk/full)。 */
|
||
static const char *font_tier_charset(int tier)
|
||
{
|
||
if (tier >= MEOW_FONT_TIER_FULL) return "full";
|
||
if (tier >= MEOW_FONT_TIER_GBK) return "gbk";
|
||
return "common";
|
||
}
|
||
|
||
/* 生效档:app 声明档(0/1/2,越界 clamp)合并「加载大字库」开关(font.load_full)。
|
||
* 开 → 强制 ≥GBK(gbk≈20k 档大字库);关(默认)→ 保持 app 声明(缺省 common≈4k)。
|
||
* 断言:0<=返回<=2。 */
|
||
static int meow_font_effective_tier(int app_tier)
|
||
{
|
||
if (app_tier < MEOW_FONT_TIER_COMMON) app_tier = MEOW_FONT_TIER_COMMON;
|
||
if (app_tier > MEOW_FONT_TIER_FULL) app_tier = MEOW_FONT_TIER_FULL;
|
||
int lf = 0;
|
||
if (meow_settings_get_switch("font.load_full", &lf) == 0 && lf)
|
||
if (app_tier < MEOW_FONT_TIER_GBK) app_tier = MEOW_FONT_TIER_GBK;
|
||
return app_tier;
|
||
}
|
||
|
||
/* 【tier 选择入口,M2.5x 已实现】按字体档位取「当前生效字体」png/idx,供 launch_app 注入
|
||
* MEOW_FONT_PNG/IDX/APP(配合 MEOW_FONT_TIER)。口径(MEOWOS_SPEC_REF §5.2/§5.7.8):
|
||
* - 生效档 = max(app 声明档, font.load_full ? GBK : COMMON):系统「加载大字库」开 →
|
||
* 强制 ≥gbk;
|
||
* - tier<=common → 走既有 meow_font_effective_paths(font.file/卡默认/固件);
|
||
* - tier>0 → 在已扫描的 MeowOS/fonts/<family>/ 里找 charset 匹配(gbk 请求接受 gbk/full;
|
||
* full 请求接受 full)的那套,以其 font.png/idx 绝对路径返回(family = 目录名);
|
||
* - 找不到匹配的大字库 → 回退 meow_font_effective_paths(避免 app 直接点到固件小库)。
|
||
* 返回 0 成功(out_png/out_idx/out_family 已填),-1 无可用。 */
|
||
static int meow_font_effective_paths_tiered(int tier, char *out_png, int plen,
|
||
char *out_idx, int ilen, char *out_family, int flen)
|
||
{
|
||
int eff = meow_font_effective_tier(tier); /* 合并 font.load_full:开 → 强制 ≥GBK */
|
||
if (eff <= MEOW_FONT_TIER_COMMON)
|
||
return meow_font_effective_paths(out_png, plen, out_idx, ilen, out_family, flen);
|
||
/* 大字库:从扫描结果选 charset 匹配的一套。
|
||
* 打分(高优先):gbk 请求 → gbk=2 > full=1;full 请求 → full=2 > gbk=1(降级可接受)。 */
|
||
const char *want = font_tier_charset(eff);
|
||
char ref[320];
|
||
int found_family = -1, best_score = 0;
|
||
for (int i = 0; i < font_presets_n; i++) {
|
||
const char *cs = font_preset_tiers[i] ? font_preset_tiers[i] : "common";
|
||
int score = 0;
|
||
if (!strcmp(cs, want)) score = 2;
|
||
else if (!strcmp(cs, "full") || !strcmp(cs, "gbk")) score = 1; /* 降级 */
|
||
if (score > best_score) { best_score = score; found_family = i; }
|
||
}
|
||
if (found_family >= 0)
|
||
snprintf(ref, sizeof ref, "%s", font_presets[found_family]);
|
||
if (found_family >= 0) {
|
||
/* 相对路径按卡根解析为设备绝对路径(与 meow_font_effective_paths 同源)。 */
|
||
if (meow_font_resolve_path(ref, out_png, plen) != 0) return -1;
|
||
if (access(out_png, R_OK) != 0) goto fallback;
|
||
snprintf(out_idx, ilen, "%s", out_png);
|
||
size_t n = strlen(out_idx);
|
||
if (n > 4 && !strcmp(out_idx + n - 4, ".png")) {
|
||
out_idx[n-4] = '.'; out_idx[n-3] = 'i'; out_idx[n-2] = 'd';
|
||
out_idx[n-1] = 'x'; out_idx[n] = 0;
|
||
}
|
||
const char *prefix = "/MeowOS/fonts/";
|
||
const char *p = strstr(out_png, 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;
|
||
}
|
||
fallback:
|
||
/* 无匹配大字库/不可读 → 回退全局(font.file 所选;其可能是大字库或默认)。 */
|
||
return meow_font_effective_paths(out_png, plen, out_idx, ilen, out_family, flen);
|
||
}
|
||
|
||
/* M2.5x 多字体:从 settings 读 display.multi_font + 6 类 font.class_*,把各自相对路径按卡根
|
||
* 解析为绝对路径,去重装载到 meow_font 槽位(同 family 去重 & ≤4 套上限由
|
||
* meow_font_config_multifont 统一处理)。仅当全局字库已装载(mfont_ready / slot0)时生效;
|
||
* 关或未装载 → 单字体(on=0),零额外开销。调用时机:启动 meow_font_ensure() 之后,以及
|
||
* 设置页变更 display.multi_font / font.class_* / font.file。 */
|
||
static void meow_font_reload_multifont(void)
|
||
{
|
||
static const char *cls_keys[MEOW_FCLS_COUNT] = {
|
||
"font.class_cjk", "font.class_cjk_sym", "font.class_num",
|
||
"font.class_latin", "font.class_latin_sym", "font.class_other" };
|
||
int on = 0;
|
||
meow_settings_get_switch("display.multi_font", &on);
|
||
char gpng[400], gidx[400], gfam[96];
|
||
/* 全局跟生效档(合并 font.load_full → 大字库),与 meow_font_ensure 同源,避免槽0实际装载与
|
||
* 此处理解不一致(多字体去重以全局 png 为基准)。 */
|
||
if (meow_font_effective_paths_tiered(meow_font_effective_tier(MEOW_FONT_TIER_COMMON),
|
||
gpng, sizeof gpng, gidx, sizeof gidx, gfam, sizeof gfam) != 0)
|
||
return; /* 无全局字体:无法多字体 */
|
||
/* 各类候选(静态缓冲保活;config_multifont 同步读取,不持指针) */
|
||
static char cpng[MEOW_FCLS_COUNT][400], cidx[MEOW_FCLS_COUNT][400];
|
||
meow_font_class_cfg cfg[MEOW_FCLS_COUNT];
|
||
memset(cfg, 0, sizeof cfg);
|
||
for (int c = 0; c < MEOW_FCLS_COUNT; c++) {
|
||
char ref[MEOW_SET_MAX_LEN];
|
||
if (meow_settings_get_str(cls_keys[c], ref, sizeof ref) == 0 && ref[0]) {
|
||
char abs[400];
|
||
if (meow_font_resolve_path(ref, abs, sizeof abs) == 0 && access(abs, R_OK) == 0) {
|
||
snprintf(cpng[c], sizeof cpng[c], "%s", abs);
|
||
snprintf(cidx[c], sizeof cidx[c], "%s", abs);
|
||
size_t n = strlen(cidx[c]);
|
||
if (n > 4 && !strcmp(cidx[c] + n - 4, ".png")) {
|
||
cidx[c][n-4]='.'; cidx[c][n-3]='i'; cidx[c][n-2]='d'; cidx[c][n-1]='x'; cidx[c][n]=0;
|
||
}
|
||
cfg[c].png = cpng[c]; cfg[c].idx = cidx[c];
|
||
}
|
||
}
|
||
}
|
||
int distinct = 0, truncated = 0;
|
||
if (meow_font_config_multifont(on, gpng, gidx, cfg, &distinct, &truncated) != 0)
|
||
logmsg("multi_font: config failed -> single-font");
|
||
else if (truncated)
|
||
logmsg("multi_font: WARN >%d 套不同字体(会增内存),已按上限截断", distinct);
|
||
else if (on)
|
||
logmsg("multi_font: enabled, %d distinct fonts (global=%s)", distinct, gfam);
|
||
}
|
||
|
||
/* 统一文本绘制:字库就绪用 meow_font(真字体/中文),否则回退 5x7 点阵。
|
||
* size 为字库字号(28=状态栏/大标题,20=正文/按钮);回退时按 scale 近似。 */
|
||
static void ui_draw(int x, int y, const char *s, uint32_t color, int size)
|
||
{
|
||
if (mfont_ready) {
|
||
meow_font_drawtext((uint16_t *)fb, x, y, s, rgb565(color), size);
|
||
} else {
|
||
draw_text(x, y, s, (size >= 16) ? 2 : 1, color);
|
||
}
|
||
}
|
||
|
||
/* 文本宽度(字库 or 回退估算),用于右对齐/居中 */
|
||
static int ui_width(const char *s, int size)
|
||
{
|
||
if (mfont_ready) return meow_font_width_text(s, size);
|
||
return (int)strlen(s) * 6 * ((size >= 16) ? 2 : 1);
|
||
}
|
||
|
||
/* 文本高度(字库行高 or 回退 7×scale 估算),用于垂直居中 / 行高预算。 */
|
||
static int ui_text_height(int size)
|
||
{
|
||
if (mfont_ready) return meow_font_line_height(size);
|
||
return 7 * ((size >= 16) ? 2 : 1);
|
||
}
|
||
|
||
static int fb_open(void)
|
||
{
|
||
int fd = open(FB_DEV, O_RDWR);
|
||
if (fd < 0) { logmsg("fb open failed: %s", strerror(errno)); return -1; }
|
||
unsigned char vinfo[160], finfo[68];
|
||
memset(vinfo, 0, sizeof vinfo);
|
||
memset(finfo, 0, sizeof finfo);
|
||
if (ioctl(fd, FBIOGET_VSCREENINFO, vinfo) < 0 ||
|
||
ioctl(fd, FBIOGET_FSCREENINFO, finfo) < 0) {
|
||
logmsg("fb ioctl failed"); close(fd); return -1;
|
||
}
|
||
fb_xres = *(uint32_t *)(vinfo + 0);
|
||
fb_yres = *(uint32_t *)(vinfo + 4);
|
||
fb_yres_virtual = *(uint32_t *)(vinfo + 12);
|
||
fb_active_yoffset = *(uint32_t *)(vinfo + 20);
|
||
fb_bpp = *(uint32_t *)(vinfo + 24);
|
||
fb_line = *(uint32_t *)(finfo + 44);
|
||
if (!fb_line) fb_line = fb_xres * (fb_bpp / 8);
|
||
/* v6.6: fb0 is DOUBLE-BUFFERED (verified yres_virtual=1440=2*720,
|
||
* smem_len=2 frames, ypanstep=1). The displayed buffer is selected by
|
||
* yoffset (FBIOPAN_DISPLAY 0x4606 verified to flip it). Map the FULL
|
||
* virtual framebuffer so we can read/write both buffers for the
|
||
* backup/restore below; mmapping only one frame would let the menu
|
||
* land on the buffer the panel is NOT showing. */
|
||
if (fb_yres_virtual < fb_yres) fb_yres_virtual = fb_yres; /* single buffer */
|
||
fb_mem = mmap(NULL, (size_t)fb_line * fb_yres_virtual,
|
||
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||
if (fb_mem == MAP_FAILED) {
|
||
logmsg("fb mmap failed: %s (xres=%d yres=%d bpp=%d line=%d size=%d)",
|
||
strerror(errno), fb_xres, fb_yres, fb_bpp, fb_line,
|
||
fb_line * fb_yres_virtual);
|
||
close(fd);
|
||
return -1;
|
||
}
|
||
fb_fd = fd;
|
||
/* A-1/A-2:launcher 用 SDK 渲染栈 —— fb 指向 SDK 的 back_buf(统一离屏),present 统一走
|
||
* meow_ui_flush()(SDK present:vsync + 帧率门控 + 原子 blit 脏区)。不再自建 back_buf。
|
||
* fb_mem(本函数 mmap 的全虚拟 fb)仅保留给播放器帧备份/恢复 + yoffset 切换 + app 帧读回。 */
|
||
fb = meow_ui_back_buf();
|
||
if (fb) {
|
||
back_buf_ok = 1;
|
||
meow_dirty_full();
|
||
logmsg("fb: %dx%d bpp=%d line=%d vres=%u yoffset=%u (A-1/A-2 SDK back_buf, meow_ui_flush)",
|
||
fb_xres, fb_yres, fb_bpp, fb_line, fb_yres_virtual, fb_active_yoffset);
|
||
} else {
|
||
fb = fb_mem; back_buf_ok = 0;
|
||
logmsg("fb: no SDK back_buf -> direct fb write (present disabled)");
|
||
}
|
||
/* NOTE: the panel is NOT unblanked here anymore — the player may still
|
||
* be running at this point (stop_player() comes after fb_open) and its
|
||
* screen-off state machine would re-blank the panel behind the menu.
|
||
* unblank_panel() runs after stop_player() (v6.2 hotfix) so the frozen
|
||
* player cannot re-blank and the menu stays visible. */
|
||
return fd;
|
||
}
|
||
|
||
/* ---- double-buffer handling (v6.6) --------------------------------
|
||
* fb0 is double-buffered and the DISPLAYED buffer is chosen by fb_var's
|
||
* yoffset (FBIOPAN_DISPLAY flips it). The player page-flips via yoffset (its
|
||
* power/screen-off redraw leaves yoffset on the back buffer), so the menu
|
||
* MUST be drawn to the buffer the panel actually shows — otherwise the menu
|
||
* is invisible and the screen freezes on the player's last frame (the v6.5
|
||
* bug). Strategy: snapshot the player's displayed frame into a heap backup,
|
||
* pan the display to buffer0 (what the launcher and all apps draw to — apps
|
||
* like hello.c mmap exactly one frame at offset 0 and never pan), draw the
|
||
* menu/app-list to buffer0, and on session end copy the backup back to the
|
||
* player's buffer and pan to the original yoffset so a SIGSTOPped player that
|
||
* does NOT redraw (event-driven, ANALYSIS §18.15) still shows its own frame. */
|
||
|
||
/* Snapshot the player's currently-displayed frame into a heap backup. */
|
||
static void fb_backup_player(void)
|
||
{
|
||
if (fb_yres_virtual <= fb_yres) { fb_orig_yoffset = -1; return; } /* single buffer */
|
||
if (fb_active_yoffset >= fb_yres_virtual) fb_active_yoffset = 0;
|
||
fb_orig_yoffset = (int)fb_active_yoffset;
|
||
fb_player_backup_len = (size_t)fb_line * fb_yres;
|
||
fb_player_backup = malloc(fb_player_backup_len);
|
||
if (!fb_player_backup) {
|
||
logmsg("fb backup malloc failed (len=%zu)", fb_player_backup_len);
|
||
fb_orig_yoffset = -1;
|
||
fb_player_backup_len = 0;
|
||
return;
|
||
}
|
||
memcpy(fb_player_backup, fb_mem + (size_t)fb_active_yoffset * fb_line,
|
||
fb_player_backup_len);
|
||
logmsg("fb backup: player frame @ yoffset=%u (buffer %u) -> heap",
|
||
fb_active_yoffset,
|
||
fb_yres ? fb_active_yoffset / fb_yres : 0);
|
||
}
|
||
|
||
/* Pan the display to buffer0 (yoffset=0). buffer0 is what this launcher and
|
||
* every app draw to (apps mmap one frame at offset 0 and never pan), so the
|
||
* panel must show buffer0 for the menu and apps to be visible. */
|
||
static void fb_norm_buf0(void)
|
||
{
|
||
if (fb_fd < 0 || fb_yres_virtual <= fb_yres) { fb_active_yoffset = 0; return; }
|
||
if (fb_active_yoffset == 0) return; /* already showing buffer0 */
|
||
unsigned char v[160];
|
||
memset(v, 0, sizeof v);
|
||
if (ioctl(fb_fd, FBIOGET_VSCREENINFO, v) < 0) return;
|
||
*(uint32_t *)(v + 16) = 0; /* xoffset */
|
||
*(uint32_t *)(v + 20) = 0; /* yoffset -> buffer0 */
|
||
ioctl(fb_fd, FBIOPAN_DISPLAY, v);
|
||
fb_active_yoffset = 0;
|
||
logmsg("fb pan -> buffer0 (yoffset=0)");
|
||
}
|
||
|
||
/* M2.8 Bug 3-residual 根因:fb_norm_buf0 在缓存 fb_active_yoffset==0 时短路提前返回、
|
||
* 不再重发 FBIOPAN_DISPLAY。但 FBIOBLANK(4) 下电 -> FBIOBLANK(0) 上电后,LCDC 的扫描
|
||
* 地址可能已复位到非 buffer0 / 失效位置,而缓存仍是 0 —— 于是真正的 re-pan 从未发出,
|
||
* 唤醒后仍是黑屏/旧帧。本函数无条件重发 pan 到 buffer0(同 yoffset 的重复 pan 是硬件
|
||
* no-op,无频闪),供唤醒/重绘路径调用,确保重画的界面一定落在被显示的缓冲。 */
|
||
static void fb_pan_buf0_force(void)
|
||
{
|
||
if (fb_fd < 0 || fb_yres_virtual <= fb_yres) { fb_active_yoffset = 0; return; }
|
||
unsigned char v[160];
|
||
memset(v, 0, sizeof v);
|
||
if (ioctl(fb_fd, FBIOGET_VSCREENINFO, v) < 0) return; /* read current vinfo */
|
||
*(uint32_t *)(v + 16) = 0; /* xoffset */
|
||
*(uint32_t *)(v + 20) = 0; /* yoffset -> buffer0 */
|
||
ioctl(fb_fd, FBIOPAN_DISPLAY, v); /* force-issue, even if cached 0 */
|
||
fb_active_yoffset = 0;
|
||
logmsg("fb force-pan -> buffer0 (yoffset=0)");
|
||
}
|
||
|
||
/* Restore the player's frame to its buffer and pan back to the original
|
||
* yoffset. Called at menu/session end (BACK TO PLAYER / SIGTERM) so a frozen
|
||
* player that never redraws shows its own frame again. Idempotent. */
|
||
static void fb_restore_player(void)
|
||
{
|
||
int restore_y = fb_orig_yoffset;
|
||
if (fb_fd >= 0 && restore_y >= 0 && fb_player_backup) {
|
||
memcpy(fb_mem + (size_t)restore_y * fb_line, fb_player_backup,
|
||
fb_player_backup_len);
|
||
unsigned char v[160];
|
||
memset(v, 0, sizeof v);
|
||
if (ioctl(fb_fd, FBIOGET_VSCREENINFO, v) == 0) {
|
||
*(uint32_t *)(v + 16) = 0;
|
||
*(uint32_t *)(v + 20) = (uint32_t)restore_y;
|
||
if (ioctl(fb_fd, FBIOPAN_DISPLAY, v) < 0)
|
||
logmsg("fb restore pan failed: %s", strerror(errno));
|
||
}
|
||
logmsg("fb restore: player frame -> yoffset=%d (buffer %d)",
|
||
restore_y, fb_yres ? restore_y / fb_yres : 0);
|
||
}
|
||
if (fb_player_backup) { free(fb_player_backup); fb_player_backup = NULL; }
|
||
fb_orig_yoffset = -1;
|
||
fb_player_backup_len = 0;
|
||
}
|
||
|
||
/* ================================================================== */
|
||
/* A-1/A-2 渲染桥:launcher 手动脏模型 → SDK 脏追踪,呈现走 meow_ui_flush() */
|
||
/* launcher 的绘制(put_px/fill_rect/meow_font/dim_screen)直接把像素画进 */
|
||
/* SDK 的 back_buf(fb),在此仅把「哪些区域变了」桥进 SDK 的脏追踪; */
|
||
/* present 不再由 launcher 做,统一 meow_ui_flush() = SDK present(内含 */
|
||
/* vsync + 帧率门控 + 原子 blit 脏区)。launcher 不再自带 meow_present/ */
|
||
/* refresh_governor —— 帧率调度已归口 SDK(PERF_SCHEDULER 显示子域)。 */
|
||
/* ================================================================== */
|
||
|
||
/* 标记全屏脏:桥进 SDK 的整帧脏,下次 meow_ui_flush() 整帧 blit。 */
|
||
static void meow_dirty_full(void) { meow_gfx_mark_full(); }
|
||
|
||
/* 清空脏集:SDK present 上屏后自清;launcher 侧无需再清(保留接口兼容)。 */
|
||
static void meow_dirty_reset(void) { }
|
||
|
||
/* 标记一个矩形脏:桥进 SDK 脏追踪(相交合并/超限转全屏由 SDK dirty_add 处理)。 */
|
||
static void meow_dirty_add(int x, int y, int w, int h) { meow_gfx_mark_dirty(x, y, w, h); }
|
||
|
||
/* ---- M2.16 按键防抖:键值须稳定 KEY_DEBOUNCE_MS 才认可,防 ADC 抖动("点一下跳 5-6 下")。
|
||
* 返回值:0=无事件;KD_DOWN=稳定按下边沿;KD_UP=稳定松开边沿(下沿后计数)。
|
||
* s->down=稳定按住;s->longpressed=按住已 ≥CLICK_MS(长按)。 */
|
||
enum { KD_NONE = 0, KD_DOWN = 1, KD_UP = 2 };
|
||
static int kd_tick(struct kd_state *s, int raw)
|
||
{
|
||
unsigned long long now = now_ms();
|
||
int ev = KD_NONE;
|
||
if (raw != s->raw) {
|
||
if (s->raw == 0 && raw != 0) { /* 任何跳变都重记稳定窗口(含原始 raw 变化) */ }
|
||
s->raw = raw; s->since = now;
|
||
}
|
||
int want = s->raw;
|
||
if (want != s->stable) {
|
||
if (now - s->since >= KEY_DEBOUNCE_MS) {
|
||
s->stable = want;
|
||
if (s->stable && !s->down) { s->down = 1; s->down_at = now; s->longpressed = 0; ev = KD_DOWN; }
|
||
else if (!s->stable && s->down) { s->down = 0; ev = KD_UP; }
|
||
}
|
||
}
|
||
if (s->down && !s->longpressed && now - s->down_at >= CLICK_MS) s->longpressed = 1;
|
||
return ev;
|
||
}
|
||
|
||
/* Unblank the panel AFTER the player is frozen: with the player SIGSTOPped
|
||
* it can no longer re-blank, so the menu (drawn on fb0) stays visible even
|
||
* when the menu opened while the player's screen timeout had blanked the
|
||
* panel. The blank state still belongs to the player's state machine — on
|
||
* session end restore_backlight() re-blanks when menu_opened_blank, so the
|
||
* player resumes into a consistent (blanked) state. */
|
||
static void unblank_panel(void)
|
||
{
|
||
int fbfd = open(FB_DEV, O_RDWR);
|
||
if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 0); close(fbfd); }
|
||
logmsg("panel unblanked (player frozen)");
|
||
}
|
||
|
||
/* Backlight handling (defined below): the menu only brings the panel back
|
||
* when it is actually dark (brightness 0 / panel blanked) and otherwise
|
||
* leaves the player's brightness untouched — the old force_backlight()
|
||
* yanked the brightness to 101 on every menu open (Bug A). */
|
||
static int read_brightness(void);
|
||
static void write_brightness(int val);
|
||
static void ensure_backlight(void);
|
||
|
||
/* ---- MeowOS 面板自持(M2.6:播放器已死,MeowOS 拥有面板/背光,无拔河)----
|
||
* 电源键按时长三分:单击(短按松开)= 息屏/亮屏;3s=电源菜单;8s=强制关机。
|
||
* 息屏 = 亮度0 + FBIOBLANK(4)(面板下电,同时挂起 goodix 触摸,无误触);
|
||
* 亮屏 = FBIOBLANK(0) + 恢复亮度。 */
|
||
static int meow_screen_on = 1;
|
||
static int meow_saved_brightness = 60;
|
||
static int meow_auto_slept = 0; /* M2.17:当前是否「超时自动息屏」置位(手动电源息屏不置位) */
|
||
static void meow_panel_off(void);
|
||
static void meow_panel_on(void);
|
||
static void meow_panel_off(void)
|
||
{
|
||
meow_saved_brightness = read_brightness();
|
||
if (meow_saved_brightness <= 0) meow_saved_brightness = 60;
|
||
write_brightness(0);
|
||
if (screen_off_mode == 0) {
|
||
/* panel:面板下电 + FBIOPBLANK(4)(同时挂起 goodix 触摸,无误触) */
|
||
int f = open(FB_DEV, O_RDWR);
|
||
if (f >= 0) { ioctl(f, FBIOBLANK, 4); close(f); }
|
||
} else {
|
||
/* black:仅背光 0 全黑(面板仍上电,触摸可唤醒) */
|
||
}
|
||
meow_screen_on = 0;
|
||
logmsg("meow panel OFF (mode=%s brightness 0%s)",
|
||
screen_off_mode ? "black" : "panel",
|
||
screen_off_mode ? "" : " + blank 4");
|
||
}
|
||
static void meow_panel_on(void)
|
||
{
|
||
int f = open(FB_DEV, O_RDWR);
|
||
if (f >= 0) { ioctl(f, FBIOBLANK, 0); close(f); }
|
||
write_brightness(meow_saved_brightness > 0 ? meow_saved_brightness : 60);
|
||
meow_screen_on = 1;
|
||
/* M2.8 Bug 3-residual:亮屏必须用 fb_pan_buf0_force 无条件重发 pan。fb_norm_buf0 在
|
||
* 缓存 fb_active_yoffset==0 时提前返回,而 FBIOBLANK(4)->(0) 后 LCDC 扫描地址可能已复位
|
||
* 到非 buffer0 / 失效位置,缓存仍是 0 导致真正的 re-pan 从未发出 —— M2.7 只加 fb_norm_buf0
|
||
* 仍黑屏的原因。主界面都画在 buffer0(mmap 偏移 0),此处强制 re-pan 保证唤醒后落在 buffer0。
|
||
* 当前界面内容的重绘由调用方(run_menu 醒屏分支)经 meow_repaint_surface 完成。 */
|
||
fb_pan_buf0_force();
|
||
logmsg("meow panel ON (blank 0 + brightness %d)", meow_saved_brightness);
|
||
}
|
||
static void meow_toggle_panel(void) { if (meow_screen_on) meow_panel_off(); else meow_panel_on(); }
|
||
|
||
/* ================================================================== */
|
||
/* key state polling (EVIOCGKEY) */
|
||
/* ================================================================== */
|
||
struct keypoll {
|
||
int fd;
|
||
unsigned long cur[KEYBUF_WORDS];
|
||
unsigned long prev[KEYBUF_WORDS];
|
||
int ok;
|
||
};
|
||
|
||
static int kp_open(struct keypoll *kp, const char *dev)
|
||
{
|
||
memset(kp, 0, sizeof *kp);
|
||
kp->fd = open(dev, O_RDONLY | O_NONBLOCK);
|
||
if (kp->fd < 0) { logmsg("open %s: %s", dev, strerror(errno)); return -1; }
|
||
if (ioctl(kp->fd, EVIOCGKEY(sizeof kp->cur), kp->cur) < 0) {
|
||
logmsg("EVIOCGKEY %s: %s", dev, strerror(errno));
|
||
close(kp->fd); return -1;
|
||
}
|
||
memcpy(kp->prev, kp->cur, sizeof kp->cur);
|
||
kp->ok = 1;
|
||
logmsg("keypoll %s ready", dev);
|
||
return 0;
|
||
}
|
||
|
||
/* 关闭 keypoll(设置页/下拉框等自开设备用;幂等,ok=0 时不动) */
|
||
static void kp_close(struct keypoll *kp)
|
||
{
|
||
if (kp->ok && kp->fd >= 0) close(kp->fd);
|
||
kp->ok = 0;
|
||
}
|
||
|
||
/* Poll device key state; call cb(code, is_down) for each edge.
|
||
* Returns 1 if any edge. */
|
||
static int kp_poll(struct keypoll *kp, int (*cb)(int code, int down, void *ctx),
|
||
void *ctx)
|
||
{
|
||
if (!kp->ok) return 0;
|
||
if (ioctl(kp->fd, EVIOCGKEY(sizeof kp->cur), kp->cur) < 0) return 0;
|
||
int changed = 0;
|
||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||
if (kp->cur[w] == kp->prev[w]) continue;
|
||
for (int b = 0; b < 32; b++) {
|
||
int c = (kp->cur[w] >> b) & 1;
|
||
int p = (kp->prev[w] >> b) & 1;
|
||
if (c != p) {
|
||
int code = w * 32 + b;
|
||
changed = 1;
|
||
if (cb) cb(code, c, ctx);
|
||
}
|
||
}
|
||
kp->prev[w] = kp->cur[w];
|
||
}
|
||
return changed;
|
||
}
|
||
|
||
static int kp_is_down(const struct keypoll *k, int code)
|
||
{
|
||
return (k->cur[code / 32] >> (code % 32)) & 1;
|
||
}
|
||
|
||
/* ================================================================== */
|
||
/* app scanning (MeowAPPs spec) */
|
||
/* ================================================================== */
|
||
/* 路由行:route.<act>=<chain>。act=route 键后缀(activity_id 的 app 内部分),
|
||
* chain=根屏→目标屏的 activity_id 序列(';' 分隔),链长=逻辑深度。 */
|
||
struct meow_route {
|
||
char act[16]; /* route 键后缀,如 "nowplaying" */
|
||
char chain[MEOW_CHAIN_MAX]; /* 如 "home;search;nowplaying" */
|
||
int keep_awake; /* M2.17:该 activity 常亮(app.json awake.<id>=1 / route.<id>.keep_awake=1) */
|
||
int fullscreen; /* 活动级全屏声名(app.json route.<id>.fullscreen=1;M3 活动切换时消费) */
|
||
};
|
||
|
||
/* 服务行:service.<svc>=mode:restart:max:window_ms(M1 仅解析存储,M4 保活) */
|
||
struct meow_svc {
|
||
char name[16];
|
||
int mode; /* 0=keepalive 1=ondemand(预留) */
|
||
int restart; /* 1=退避重启 */
|
||
int max; /* max_restart 熔断阈值 */
|
||
int window_ms; /* 重启窗口 */
|
||
};
|
||
|
||
struct app {
|
||
char id[MEOW_APP_ID_MAX]; /* M2.52:app_id=app.json 的 package(权威,缺省回退卡上目录名 de->d_name),供 MEOW_LOG_APP 注入 */
|
||
char package[MEOW_APP_ID_MAX]; /* 安卓化:app.json 的 package 字段(反域名权威标识;目录名=package),scan_apps 据此定 app_id */
|
||
char name[17]; /* ASCII display name */
|
||
char exec[129]; /* absolute path */
|
||
int ok; /* exec exists and X_OK */
|
||
/* ---- 阶段 D · M1:app.json 路由/框架/服务/图标(scan_apps 解析)---- */
|
||
char framework; /* '1'=framework=true(看门狗返回发 SIGUSR1) */
|
||
char immersive; /* '1'=immersive=true(隐藏虚拟返回键,返回手势归 SDK) */
|
||
char fullscreen; /* '1'=app.json fullscreen=true(注入 MEOW_FULLSCREEN=1,SDK 预先设全屏) */
|
||
char fonts; /* 字库档:0=默认4K常用 1=大字库GBK 2=Full(app.json "fonts";注入 MEOW_FONT_TIER) */
|
||
char icon[129]; /* icon= 路径 */
|
||
int nroute; /* route.<id> 行数 */
|
||
struct meow_route routes[MEOW_MAX_ROUTES];
|
||
int nsvc;
|
||
struct meow_svc svcs[MEOW_MAX_SVCS];
|
||
};
|
||
|
||
/* Activity 路由注册表项:app_id 全局唯一的运行时路由层,由 scan_apps(静态 app.json)
|
||
* 与 MSG_ACT_DECLARE/HELLO(运行时)并集填充;单一写者=主循环,无需锁。 */
|
||
struct meow_acct {
|
||
char app_id[MEOW_APP_ID_MAX];
|
||
char name[17];
|
||
char exec[129];
|
||
int ok;
|
||
char framework;
|
||
char icon[129];
|
||
int nroute;
|
||
struct meow_route routes[MEOW_MAX_ROUTES];
|
||
int nsvc;
|
||
struct meow_svc svcs[MEOW_MAX_SVCS];
|
||
int declared; /* 是否经 HELLO/ACT_DECLARE 运行时声明过 */
|
||
};
|
||
|
||
static struct meow_acct g_accts[MEOW_MAX_ACTS]; /* 主循环单写者 */
|
||
static int g_naccts = 0;
|
||
|
||
/* 阶段 D · M0:主循环排空 IPC 队列(定义在下方 IPC 骨架区;run_menu/main 调用) */
|
||
static void meow_ipc_process(void);
|
||
|
||
/* ---- 阶段 D · M1:路由注册表辅助(主循环单写者,无锁)---- */
|
||
static struct meow_acct *meow_acct_find(const char *app_id)
|
||
{
|
||
if (!app_id) return NULL;
|
||
for (int i = 0; i < g_naccts; i++)
|
||
if (!strcmp(g_accts[i].app_id, app_id)) return &g_accts[i];
|
||
return NULL;
|
||
}
|
||
|
||
/* 取(或在末尾新建)app_id 对应的注册表项。返回 NULL 当满。 */
|
||
static struct meow_acct *meow_acct_upsert(const char *app_id)
|
||
{
|
||
if (!app_id || !app_id[0]) return NULL;
|
||
struct meow_acct *a = meow_acct_find(app_id);
|
||
if (a) return a;
|
||
if (g_naccts >= MEOW_MAX_ACTS) { logmsg("act: registry full, drop %s", app_id); return NULL; }
|
||
a = &g_accts[g_naccts++];
|
||
memset(a, 0, sizeof *a);
|
||
strncpy(a->app_id, app_id, MEOW_APP_ID_MAX - 1);
|
||
return a;
|
||
}
|
||
|
||
/* 由 scan_apps 调用:把 app.json 静态路由整体同步进注册表(扫描即权威,替换)。 */
|
||
static void meow_acct_sync(const char *app_id, const struct app *ap)
|
||
{
|
||
if (!ap) return;
|
||
struct meow_acct *a = meow_acct_upsert(app_id);
|
||
if (!a) return;
|
||
a->framework = ap->framework;
|
||
strncpy(a->icon, ap->icon, 128);
|
||
strncpy(a->name, ap->name, 16);
|
||
strncpy(a->exec, ap->exec, 128);
|
||
a->ok = ap->ok;
|
||
a->nroute = ap->nroute > MEOW_MAX_ROUTES ? MEOW_MAX_ROUTES : ap->nroute;
|
||
for (int i = 0; i < a->nroute; i++) a->routes[i] = ap->routes[i];
|
||
a->nsvc = ap->nsvc > MEOW_MAX_SVCS ? MEOW_MAX_SVCS : ap->nsvc;
|
||
for (int i = 0; i < a->nsvc; i++) a->svcs[i] = ap->svcs[i];
|
||
}
|
||
|
||
/* 运行时 HELLO:声明 app 存在(补入注册表),记录声明源。 */
|
||
static void meow_acct_hello(const char *app_id, uint32_t version, uint32_t flags)
|
||
{
|
||
struct meow_acct *a = meow_acct_upsert(app_id);
|
||
if (a) { a->declared = 1; (void)version; (void)flags; }
|
||
logmsg("act: hello app=%s ver=%u flags=%u", app_id, version, flags);
|
||
}
|
||
|
||
/* 运行时 ACT_DECLARE:并集覆盖静态路由。entries=count 个 NUL 结尾 "id=chain" 串。 */
|
||
static void meow_acct_declare(const char *app_id, const char *entries, int count)
|
||
{
|
||
struct meow_acct *a = meow_acct_upsert(app_id);
|
||
if (!a || !entries || count <= 0) return;
|
||
a->declared = 1;
|
||
const char *p = entries;
|
||
for (int k = 0; k < count && *p; k++) {
|
||
const char *eq = strchr(p, '=');
|
||
if (eq && eq > p) {
|
||
int idlen = (int)(eq - p);
|
||
if (idlen >= 16) idlen = 15;
|
||
char id[16];
|
||
memcpy(id, p, idlen); id[idlen] = 0;
|
||
const char *chain = eq + 1;
|
||
int ri = -1;
|
||
for (int i = 0; i < a->nroute; i++)
|
||
if (!strcmp(a->routes[i].act, id)) { ri = i; break; }
|
||
if (ri < 0) {
|
||
if (a->nroute >= MEOW_MAX_ROUTES) { p += strlen(p) + 1; continue; }
|
||
ri = a->nroute++;
|
||
}
|
||
strncpy(a->routes[ri].act, id, 15);
|
||
strncpy(a->routes[ri].chain, chain, MEOW_CHAIN_MAX - 1);
|
||
}
|
||
p += strlen(p) + 1;
|
||
}
|
||
}
|
||
|
||
/* 链路深度 = ';' 分隔段数(根屏→目标屏的逻辑链长)。 */
|
||
static int chain_depth(const char *c)
|
||
{
|
||
if (!c || !*c) return 0;
|
||
int d = 1;
|
||
for (; *c; c++) if (*c == ';') d++;
|
||
return d;
|
||
}
|
||
|
||
/* 解析 activity 链路:填 out_chain(root..target)与 depth。
|
||
* 返回 0=成功;MEOW_ERR_NOENT=app 未注册 / activity 未注册。
|
||
* 规则(FRAMEWORK_DESIGN §2):route 缺失(app 未声明任何路由)→ chain=activity、深度 1 兜底;
|
||
* 声明了路由但 activity 非 route 键 → MEOW_ERR_NOENT(activity 未注册)。 */
|
||
static int resolve_chain(const char *app_id, const char *activity, char *out, int *depth)
|
||
{
|
||
if (!out || !depth) return MEOW_ERR_INVAL;
|
||
struct meow_acct *a = meow_acct_find(app_id);
|
||
if (!a) return MEOW_ERR_NOENT;
|
||
if (a->nroute == 0) {
|
||
strncpy(out, activity, MEOW_CHAIN_MAX - 1);
|
||
*depth = 1;
|
||
return MEOW_OK;
|
||
}
|
||
for (int i = 0; i < a->nroute; i++) {
|
||
if (!strcmp(a->routes[i].act, activity)) {
|
||
strncpy(out, a->routes[i].chain, MEOW_CHAIN_MAX - 1);
|
||
*depth = chain_depth(a->routes[i].chain);
|
||
return MEOW_OK;
|
||
}
|
||
}
|
||
return MEOW_ERR_NOENT;
|
||
}
|
||
|
||
/* ---- M2.17 keep_awake:前台当前顶层 activity 追踪 ----
|
||
* 前台应用经 IPC MSG_ACT_FOCUS/BACK 上报其当前顶层 activity;fg_keep_awake() 依注册表判断该
|
||
* activity 是否声明 keep_awake(app.json awake.<id>=1 / route.<id>.keep_awake=1;运行时同类)。
|
||
* 主界面(无上报/空)= 非 keep_awake(主页空闲照常超时息屏)。单一写者=主循环(IPC 排空处)。 */
|
||
static char g_fg_app[MEOW_APP_ID_MAX] = "";
|
||
static char g_fg_act[MEOW_ACT_MAX] = "";
|
||
static void meow_fg_act_set(const char *app_id, const char *act)
|
||
{
|
||
if (!app_id) return;
|
||
strncpy(g_fg_app, app_id, MEOW_APP_ID_MAX - 1);
|
||
g_fg_app[MEOW_APP_ID_MAX - 1] = 0;
|
||
if (act) { strncpy(g_fg_act, act, MEOW_ACT_MAX - 1); g_fg_act[MEOW_ACT_MAX - 1] = 0; }
|
||
else g_fg_act[0] = 0;
|
||
logmsg("keep_awake: fg_act app=%s act=%s", g_fg_app, g_fg_act);
|
||
}
|
||
static void meow_fg_act_back(void)
|
||
{
|
||
g_fg_app[0] = 0;
|
||
g_fg_act[0] = 0;
|
||
logmsg("keep_awake: fg_act back to home (cleared)");
|
||
}
|
||
/* 前台当前 activity 是否 keep_awake(无上报/未注册/未声明=0,即照常超时息屏)。 */
|
||
static int fg_keep_awake(void)
|
||
{
|
||
if (!g_fg_app[0] || !g_fg_act[0]) return 0;
|
||
struct meow_acct *a = meow_acct_find(g_fg_app);
|
||
if (!a) return 0;
|
||
for (int i = 0; i < a->nroute; i++)
|
||
if (!strcmp(a->routes[i].act, g_fg_act)) return a->routes[i].keep_awake;
|
||
return 0;
|
||
}
|
||
|
||
/* 标识符字符(含 '.',用于防止 json_str 误命中更长键的一部分) */
|
||
static int ident_ch(char c)
|
||
{
|
||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||
(c >= '0' && c <= '9') || c == '_' || c == '.';
|
||
}
|
||
|
||
/* 极简 JSON/扁平值提取:在一行里找 key,读取其值(跳过 ':'/'=' 与可选引号)。
|
||
* 支持 JSON `"name": "Hello"` 与扁平 `framework=true`。返回拷贝到 out 的字符数。
|
||
* 返回 0=未命中/空。 */
|
||
static int json_str(const char *buf, const char *key, char *out, int olen)
|
||
{
|
||
out[0] = 0;
|
||
if (!buf || !key || olen <= 0) return 0;
|
||
const char *p = strstr(buf, key);
|
||
if (!p) return 0;
|
||
if (p > buf && ident_ch((unsigned char)p[-1])) return 0; /* 键前缀需独立 */
|
||
p += strlen(key);
|
||
if (*p == '"') p++; /* JSON 键自身右引号:跳到 ':'/'=' */
|
||
while (*p && (*p == ':' || *p == '=')) p++;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
if (*p == '"') p++; /* 值左引号(JSON 字符串值) */
|
||
int i = 0;
|
||
while (*p && *p != '"' && *p != ',' && *p != '\n' && *p != '\r' && i < olen - 1)
|
||
out[i++] = *p++;
|
||
out[i] = 0;
|
||
while (i > 0 && (out[i - 1] == ' ' || out[i - 1] == '\t')) out[--i] = 0;
|
||
return i;
|
||
}
|
||
|
||
/* state: 0 ok / 1 no storage card / 2 no usable apps */
|
||
static int card_present(void)
|
||
{
|
||
FILE *f = fopen(MOUNTS, "r");
|
||
if (!f) return 0;
|
||
char line[256];
|
||
int found = 0;
|
||
while (fgets(line, sizeof line, f)) {
|
||
if (strstr(line, " /usr/data/mnt/sd_0 ")) { found = 1; break; }
|
||
}
|
||
fclose(f);
|
||
return found;
|
||
}
|
||
|
||
/* ================================================================== */
|
||
/* MeowOS 系统拔营(M2.2 批次):TF 卡挂载 + WiFi 联网 */
|
||
/* 官方环境由 hiby_player / sys_server 负责这两件事;MeowOS 作为主系统 */
|
||
/* 时无播放器,必须由 launcher 主动接管。失败不阻塞主界面,后台周期重试。 */
|
||
/* ================================================================== */
|
||
static int sd_mounted(void)
|
||
{
|
||
return card_present();
|
||
}
|
||
|
||
/* TF 卡设备节点是否出现(任一常见节点即视为在卡) */
|
||
static int sd_device_present(void)
|
||
{
|
||
return access("/dev/mmcblk0", F_OK) == 0 || access("/dev/mmcblk0p1", F_OK) == 0;
|
||
}
|
||
|
||
/* 主动挂载 TF 卡到 /usr/data/mnt/sd_0。返回 1=已挂载,0=尚未(调用方轮询重试)。
|
||
* 依次尝试 /dev/mmcblk0、/dev/mmcblk0p1,先 -t exfat 再自动探测;卡未插入返回 0。 */
|
||
static int ensure_card_mounted(void)
|
||
{
|
||
if (sd_mounted()) return 1;
|
||
if (!sd_device_present()) return 0;
|
||
const char *nodes[2] = { "/dev/mmcblk0", "/dev/mmcblk0p1" };
|
||
for (int i = 0; i < 2; i++) {
|
||
if (access(nodes[i], F_OK) != 0) continue;
|
||
char cmd[160];
|
||
snprintf(cmd, sizeof cmd, "mkdir -p /usr/data/mnt/sd_0; "
|
||
"PATH=/bin:/sbin:/usr/bin:/usr/sbin mount -t exfat %s /usr/data/mnt/sd_0 2>/dev/null",
|
||
nodes[i]);
|
||
if (system(cmd) == 0 && sd_mounted()) { logmsg("sd_0 mounted: %s", nodes[i]); return 1; }
|
||
snprintf(cmd, sizeof cmd, "mkdir -p /usr/data/mnt/sd_0; "
|
||
"PATH=/bin:/sbin:/usr/bin:/usr/sbin mount %s /usr/data/mnt/sd_0 2>/dev/null",
|
||
nodes[i]);
|
||
if (system(cmd) == 0 && sd_mounted()) { logmsg("sd_0 mounted (auto): %s", nodes[i]); return 1; }
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* ================================================================== */
|
||
/* M2.5x:TF 卡多字体库扫描 + preset 动态化 */
|
||
/* 字体目录 = <卡根>/MeowOS/fonts/<family>/,每套含三件套: */
|
||
/* 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,
|
||
char *charset, int clen, int *glyph_count)
|
||
{
|
||
name[0] = 0; sha_png[0] = 0; sha_idx[0] = 0;
|
||
if (charset) charset[0] = 0;
|
||
if (glyph_count) *glyph_count = 0;
|
||
FILE *f = fopen(json_path, "r");
|
||
if (!f) return;
|
||
char buf[256];
|
||
while (fgets(buf, sizeof buf, f)) {
|
||
if (!name[0]) json_str(buf, "name", name, nlen);
|
||
if (!sha_png[0]) json_str(buf, "sha256_png", sha_png, slen);
|
||
if (!sha_idx[0]) json_str(buf, "sha256_idx", sha_idx, xlen);
|
||
/* M2.5x 字体档位:charset(common|gbk|full)+ glyph_count(信息用,驱动档位分层/标注)。 */
|
||
if (charset && !charset[0]) {
|
||
char v[8] = "";
|
||
if (json_str(buf, "charset", v, sizeof v) > 0 && v[0])
|
||
snprintf(charset, clen, "%s", v);
|
||
}
|
||
if (glyph_count && *glyph_count == 0) {
|
||
char v[16] = "";
|
||
if (json_str(buf, "glyph_count", v, sizeof v) > 0 && v[0])
|
||
*glyph_count = atoi(v);
|
||
}
|
||
}
|
||
fclose(f);
|
||
}
|
||
|
||
/* 档位大小标记(设置页字体选项标注):common≈4k / gbk≈20k / full。
|
||
* 由 font.json 的 charset + glyph_count 派生;缺 charset 回退 common。 */
|
||
static void font_tier_marker(const char *charset, int glyph_count, char *out, int olen)
|
||
{
|
||
const char *tier = (charset && charset[0]) ? charset : "common";
|
||
if (!strcmp(tier, "full")) { snprintf(out, olen, "full"); return; }
|
||
if (glyph_count > 0)
|
||
snprintf(out, olen, "%s≈%dk", tier, (glyph_count + 500) / 1000);
|
||
else
|
||
snprintf(out, olen, "%s", tier);
|
||
}
|
||
|
||
/* 探测一个 <dirpath>/<family> 套件是否合法:三件套齐全 → 解析 label + 档位 + 校验 sha。
|
||
* 返回 1=进列表,0=跳过(缺组件/json 不可读)。sha 失败仅 WARN。
|
||
* 输出:label=显示名+大小标记 `名字(common≈4k)`;charset/glyph_count=档位信息。 */
|
||
static int meow_font_probe_family(const char *dirpath, const char *family,
|
||
char *label, int llen,
|
||
char *charset, int clen, int *glyph_count)
|
||
{
|
||
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] = "";
|
||
char cs[8] = ""; int gc = 0;
|
||
meow_font_json_probe(jpath, name, sizeof name, jsha_png, sizeof jsha_png,
|
||
jsha_idx, sizeof jsha_idx, cs, sizeof cs, &gc);
|
||
/* 显示名 = 名 + 档位大小标记(如 `default(common≈4k)` / `gbk≈20k` / `full`) */
|
||
char marker[24];
|
||
font_tier_marker(cs, gc, marker, sizeof marker);
|
||
snprintf(label, llen, "%s(%s)", name[0] ? name : family, marker);
|
||
if (charset) snprintf(charset, clen, "%s", cs[0] ? cs : "common");
|
||
if (glyph_count) *glyph_count = gc;
|
||
/* 完整性校验:仅 WARN,不硬失败;该套仍标记可选。 */
|
||
char apng[80] = "", aidx[80] = "";
|
||
if (font_sha256_file(ppath, apng, sizeof apng) != 0)
|
||
logmsg("fonts: '%s' 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) charset=%s glyph_count=%d", label, family, cs[0]?cs:"common", gc);
|
||
return 1;
|
||
}
|
||
|
||
/* 扫描 <card_root>/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_preset_label_buf[0]; /* 指向缓冲(运行时派生含大小标记) */
|
||
/* 默认套也读 font.json 档位(存在则跟真实档,缺则 common);档位信息供分层装载/标注。 */
|
||
{
|
||
char jp[400], cs[8] = ""; int gc = 0;
|
||
char nm0[64] = "", sp0[80] = "", si0[80] = "";
|
||
snprintf(jp, sizeof jp, "%s/%s/default/font.json", card_root, MEOW_FONTS_CFG);
|
||
meow_font_json_probe(jp, nm0, sizeof nm0, sp0, sizeof sp0, si0, sizeof si0,
|
||
cs, sizeof cs, &gc);
|
||
char mk[24]; font_tier_marker(cs, gc, mk, sizeof mk);
|
||
snprintf(font_preset_label_buf[0], sizeof font_preset_label_buf[0], "默认字体(%s)", mk);
|
||
snprintf(font_preset_tier_buf[0], sizeof font_preset_tier_buf[0], "%s", cs[0]?cs:"common");
|
||
font_preset_tiers[0] = font_preset_tier_buf[0];
|
||
font_preset_glyphs[0] = gc;
|
||
}
|
||
font_presets_n = 1;
|
||
char root[340];
|
||
snprintf(root, sizeof root, "%s/%s", card_root, MEOW_FONTS_CFG);
|
||
DIR *d = opendir(root);
|
||
if (!d) return; /* 无卡/无字库目录:仅默认 */
|
||
struct dirent *de;
|
||
while ((de = readdir(d)) && font_presets_n < FONT_PRESETS_MAX) {
|
||
if (de->d_name[0] == '.') continue;
|
||
char dirpath[400], label[80], cs[8] = ""; int gc = 0;
|
||
snprintf(dirpath, sizeof dirpath, "%s/%s", root, de->d_name);
|
||
if (meow_font_probe_family(dirpath, de->d_name, label, sizeof label,
|
||
cs, sizeof cs, &gc) == 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);
|
||
snprintf(font_preset_tier_buf[i], sizeof font_preset_tier_buf[i], "%s", cs[0]?cs:"common");
|
||
font_presets[i] = font_preset_val_buf[i];
|
||
font_preset_labels[i] = font_preset_label_buf[i];
|
||
font_preset_tiers[i] = font_preset_tier_buf[i];
|
||
font_preset_glyphs[i] = gc;
|
||
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;
|
||
int fullscreen = 0; /* app.json fullscreen=<true|1>(注入 MEOW_FULLSCREEN env,SDK 预先设全屏) */
|
||
int fonts = 0, fonts_set = 0; /* app.json "fonts":<档>(0=默认4K常用 1=大字库GBK 2=Full;缺省 0) */
|
||
struct app tmp;
|
||
memset(&tmp, 0, sizeof tmp);
|
||
while (fgets(buf, sizeof buf, f)) {
|
||
if (!name[0]) json_str(buf, "name", name, sizeof name);
|
||
if (!exec[0]) json_str(buf, "exec", exec, sizeof exec);
|
||
if (!icon[0]) json_str(buf, "icon", icon, sizeof icon);
|
||
if (!package[0]) json_str(buf, "package", package, sizeof package); /* 安卓化权威 app_id */
|
||
/* route.<id>=<chain>(activity 路由,M1) */
|
||
char *rp = strstr(buf, "route.");
|
||
if (rp && tmp.nroute < MEOW_MAX_ROUTES) {
|
||
rp += 6; /* 跳过 "route." */
|
||
int idl = 0; char id[32] = ""; /* 读满整键(识别 ".fullscreen" 后缀,cap 31) */
|
||
while (*rp && *rp != '"' && *rp != ':' && *rp != '=' && idl < 31)
|
||
id[idl++] = *rp++;
|
||
id[idl] = 0;
|
||
if (idl > 0) {
|
||
char *vp = rp;
|
||
while (*vp && *vp != ':' && *vp != '=') vp++;
|
||
if (*vp == ':' || *vp == '=') vp++;
|
||
while (*vp == ' ' || *vp == '\t') vp++;
|
||
if (*vp == '"') vp++;
|
||
/* route.<id>.fullscreen=<on>:活动级全屏声明(M1 顺带;M3 活动切换时消费)。
|
||
* 命中既有 route.<id> 置 fullscreen;不新建 route(避免 id 被截断误建 "…fulls")。 */
|
||
if (idl >= 11 && !strcmp(id + idl - 11, ".fullscreen")) {
|
||
id[idl - 11] = 0; /* 去掉 ".fullscreen" 还原 act */
|
||
char v[8] = ""; int k = 0;
|
||
while (*vp && *vp != '"' && *vp != ',' && *vp != '\n' &&
|
||
*vp != '\r' && k < 7) v[k++] = *vp++;
|
||
v[k] = 0;
|
||
int on = (!strcmp(v, "1") || !strcmp(v, "true") || !strcmp(v, "yes"));
|
||
for (int i = 0; i < tmp.nroute; i++)
|
||
if (!strcmp(tmp.routes[i].act, id)) { tmp.routes[i].fullscreen = on; break; }
|
||
} else {
|
||
int vl = 0; char chain[MEOW_CHAIN_MAX] = "";
|
||
while (*vp && *vp != '"' && *vp != ',' && *vp != '\n' &&
|
||
*vp != '\r' && vl < MEOW_CHAIN_MAX - 1)
|
||
chain[vl++] = *vp++;
|
||
chain[vl] = 0;
|
||
while (vl > 0 && (chain[vl - 1] == ' ' || chain[vl - 1] == '\t'))
|
||
chain[--vl] = 0;
|
||
strncpy(tmp.routes[tmp.nroute].act, id, 15);
|
||
tmp.routes[tmp.nroute].act[15] = 0; /* 显式收尾(原依赖 id 读长 ≤15) */
|
||
strncpy(tmp.routes[tmp.nroute].chain, chain, MEOW_CHAIN_MAX - 1);
|
||
tmp.nroute++;
|
||
}
|
||
}
|
||
}
|
||
/* awake.<id>=1(M2.17 keep_awake:该 activity 常亮,超时不自动息屏)。
|
||
* 命中的既有 route.<id> 置 keep_awake;无 route 则新建单 activity 路由。 */
|
||
char *ap = strstr(buf, "awake.");
|
||
if (ap && tmp.nroute < MEOW_MAX_ROUTES) {
|
||
ap += 6; /* 跳过 "awake." */
|
||
int al = 0; char aid[16] = "";
|
||
while (*ap && *ap != '"' && *ap != ':' && *ap != '=' && al < 15)
|
||
aid[al++] = *ap++;
|
||
aid[al] = 0;
|
||
if (al > 0) {
|
||
char *vp = ap;
|
||
while (*vp && *vp != ':' && *vp != '=') vp++;
|
||
if (*vp == ':' || *vp == '=') vp++;
|
||
while (*vp == ' ' || *vp == '\t') vp++;
|
||
if (*vp == '"') vp++;
|
||
int on = atoi(vp);
|
||
int ri = -1;
|
||
for (int i = 0; i < tmp.nroute; i++)
|
||
if (!strcmp(tmp.routes[i].act, aid)) { ri = i; break; }
|
||
if (ri < 0) {
|
||
ri = tmp.nroute++;
|
||
strncpy(tmp.routes[ri].act, aid, 15);
|
||
strncpy(tmp.routes[ri].chain, aid, MEOW_CHAIN_MAX - 1); /* 单 activity 兜底链路 */
|
||
}
|
||
tmp.routes[ri].keep_awake = (on != 0);
|
||
}
|
||
}
|
||
/* framework=true(看门狗返回发 SIGUSR1,M1 解析存储) */
|
||
if (!fram && strstr(buf, "framework")) {
|
||
char v[8] = "";
|
||
if (json_str(buf, "framework", v, sizeof v) > 0) {
|
||
if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes"))
|
||
fram = 1;
|
||
}
|
||
}
|
||
/* immersive=true(隐藏虚拟返回键、返回手势移交 SDK;写 /tmp/meowapp_state) */
|
||
if (!immersive && strstr(buf, "immersive")) {
|
||
char v[8] = "";
|
||
if (json_str(buf, "immersive", v, sizeof v) > 0) {
|
||
if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes"))
|
||
immersive = 1;
|
||
}
|
||
}
|
||
/* fullscreen=true(app 级:注入 MEOW_FULLSCREEN=1,SDK 端 meow_ui_init 预先设全屏)。
|
||
* 与 immersive 是两回事:fullscreen 隐藏状态栏/沉浸,immersive 关虚拟返回键。 */
|
||
if (!fullscreen && strstr(buf, "fullscreen")) {
|
||
char v[8] = "";
|
||
if (json_str(buf, "fullscreen", v, sizeof v) > 0) {
|
||
if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes"))
|
||
fullscreen = 1;
|
||
}
|
||
}
|
||
/* "fonts":<档>(字库档:0=默认4K常用 1=大字库GBK 2=Full;可选,缺省 0)。
|
||
* launcher 注入 MEOW_FONT_TIER;大字库装载由后续 font 代理实现。 */
|
||
if (!fonts_set && strstr(buf, "fonts")) {
|
||
char v[8] = "";
|
||
if (json_str(buf, "fonts", v, sizeof v) > 0) {
|
||
if (!strcmp(v, "common") || !strcmp(v, "0")) fonts = 0;
|
||
else if (!strcmp(v, "gbk") || !strcmp(v, "1")) fonts = 1;
|
||
else if (!strcmp(v, "full") || !strcmp(v, "2")) fonts = 2;
|
||
else fonts = atoi(v); /* 数字兜底(越界收敛到 [0,2]) */
|
||
if (fonts < 0) fonts = 0;
|
||
if (fonts > 2) fonts = 2;
|
||
fonts_set = 1;
|
||
}
|
||
}
|
||
/* service.<svc>=mode:restart:max:window_ms(M1 解析存储,保活 M4) */
|
||
char *sp = strstr(buf, "service.");
|
||
if (sp && tmp.nsvc < MEOW_MAX_SVCS) {
|
||
sp += 8; /* 跳过 "service." */
|
||
int sl = 0; char nm[16] = "";
|
||
while (*sp && *sp != '"' && *sp != ':' && *sp != '=' && sl < 15)
|
||
nm[sl++] = *sp++;
|
||
nm[sl] = 0;
|
||
if (sl > 0) {
|
||
char *vp = sp;
|
||
while (*vp && *vp != ':' && *vp != '=') vp++;
|
||
if (*vp == ':' || *vp == '=') vp++;
|
||
while (*vp == ' ' || *vp == '\t') vp++;
|
||
if (*vp == '"') vp++;
|
||
int m = 0, r = 0, mx = 0, w = 0;
|
||
sscanf(vp, "%d:%d:%d:%d", &m, &r, &mx, &w);
|
||
strncpy(tmp.svcs[tmp.nsvc].name, nm, 15);
|
||
tmp.svcs[tmp.nsvc].mode = m;
|
||
tmp.svcs[tmp.nsvc].restart = r;
|
||
tmp.svcs[tmp.nsvc].max = mx;
|
||
tmp.svcs[tmp.nsvc].window_ms = w;
|
||
tmp.nsvc++;
|
||
}
|
||
}
|
||
}
|
||
strncpy(a->name, name, 16);
|
||
strncpy(a->exec, exec, 128);
|
||
strncpy(a->package, package, sizeof a->package - 1);
|
||
a->framework = fram ? '1' : 0;
|
||
a->immersive = immersive ? '1' : 0;
|
||
a->fullscreen = fullscreen ? '1' : 0; /* 注入 MEOW_FULLSCREEN env */
|
||
a->fonts = (char)(fonts < 0 ? 0 : (fonts > 2 ? 2 : fonts)); /* 注入 MEOW_FONT_TIER env */
|
||
strncpy(a->icon, icon, 128);
|
||
a->nroute = tmp.nroute;
|
||
if (tmp.nroute) memcpy(a->routes, tmp.routes, sizeof(a->routes[0]) * (size_t)tmp.nroute);
|
||
a->nsvc = tmp.nsvc;
|
||
if (tmp.nsvc) memcpy(a->svcs, tmp.svcs, sizeof(a->svcs[0]) * (size_t)tmp.nsvc);
|
||
}
|
||
|
||
static int scan_apps(struct app apps[MAX_APPS])
|
||
{
|
||
memset(apps, 0, sizeof(struct app) * MAX_APPS);
|
||
if (!card_present()) return -1; /* state 1 */
|
||
|
||
DIR *d = opendir(MEOWAPPS);
|
||
if (!d) return -2; /* state 2 */
|
||
int n = 0;
|
||
struct dirent *de;
|
||
while ((de = readdir(d)) && n < MAX_APPS) {
|
||
if (de->d_name[0] == '.') continue;
|
||
char jpath[192];
|
||
snprintf(jpath, sizeof jpath, "%s/%s/app.json", MEOWAPPS, de->d_name);
|
||
FILE *f = fopen(jpath, "r");
|
||
if (!f) continue;
|
||
struct app a0;
|
||
memset(&a0, 0, sizeof a0);
|
||
meow_parse_app_json(f, &a0);
|
||
fclose(f);
|
||
if (!a0.name[0] || !a0.exec[0]) continue; /* corrupt -> skip */
|
||
struct app *a = &apps[n];
|
||
memset(a, 0, sizeof *a);
|
||
memcpy(a, &a0, sizeof a0);
|
||
a->ok = (access(a->exec, X_OK) == 0);
|
||
/* 安卓化:app_id=app.json 的 package(权威),缺省回退目录名 de->d_name;
|
||
* 两者都存在且不同则 WARN(正常情况二者相等)。launch_app 注入 MEOW_LOG_APP 用 */
|
||
const char *aid = a0.package[0] ? a0.package : de->d_name;
|
||
strncpy(a->id, aid, sizeof a->id - 1);
|
||
if (a0.package[0] && strcmp(a0.package, de->d_name) != 0)
|
||
logmsg("WARN: app '%s' package '%s' != dir '%s'", a0.name, a0.package, de->d_name);
|
||
/* M1 路由注册表同步(app_id = a->id,即 package 优先) */
|
||
meow_acct_sync(a->id, a);
|
||
n++;
|
||
}
|
||
closedir(d);
|
||
return n; /* 0 -> state 2 */
|
||
}
|
||
|
||
/* ================================================================== */
|
||
/* hiby_player management */
|
||
/* ================================================================== */
|
||
static int find_hiby_pid(void)
|
||
{
|
||
/* scan /proc/N/exe for /usr/bin/hiby_player */
|
||
DIR *d = opendir("/proc");
|
||
if (!d) return 0;
|
||
struct dirent *de;
|
||
int pid = 0;
|
||
while ((de = readdir(d))) {
|
||
if (de->d_name[0] < '0' || de->d_name[0] > '9') continue;
|
||
char path[64], exe[256];
|
||
snprintf(path, sizeof path, "/proc/%s/exe", de->d_name);
|
||
ssize_t n = readlink(path, exe, sizeof exe - 1);
|
||
if (n <= 0) continue;
|
||
exe[n] = 0;
|
||
if (!strcmp(exe, "/usr/bin/hiby_player")) { pid = atoi(de->d_name); break; }
|
||
}
|
||
closedir(d);
|
||
return pid;
|
||
}
|
||
|
||
/* ---- MeowOS 常驻主界面判定(M2.2 修复批次)----
|
||
* Launcher 被 MeowOS 拉起(bootmenu MeowOS 分支 exec,或热键切 MeowOS 后 exec)时,
|
||
* 播放器已死(find_hiby_pid()==0)。此时 launcher == MeowOS 主界面常驻:必须自绘
|
||
* fb0(画 buffer0 + FBIOPAN yoffset=0),绝不走旧「备份播放器帧/冻结/SIGCONT」路径,
|
||
* 绝不遗留/恢复播放器死帧。仅当存在存活播放器(会话覆盖场景)才保留旧菜单覆盖语义。 */
|
||
static int meow_mode = 0;
|
||
|
||
static int is_meow_mode(void)
|
||
{
|
||
return find_hiby_pid() <= 0;
|
||
}
|
||
|
||
static int player_stopped = 0;
|
||
static int player_pid = 0;
|
||
|
||
static int proc_is_stopped(int pid)
|
||
{
|
||
char path[64], line[512];
|
||
snprintf(path, sizeof path, "/proc/%d/stat", pid);
|
||
int fd = open(path, O_RDONLY);
|
||
if (fd < 0) return 0;
|
||
ssize_t n = read(fd, line, sizeof line - 1);
|
||
close(fd);
|
||
if (n <= 0) return 0;
|
||
line[n] = 0;
|
||
/* comm may contain spaces; find last ')' */
|
||
char *p = strrchr(line, ')');
|
||
if (!p || p + 2 >= line + n) return 0;
|
||
return p[2] == 'T';
|
||
}
|
||
|
||
static int stop_player(void)
|
||
{
|
||
player_pid = find_hiby_pid();
|
||
if (!player_pid) { logmsg("hiby_player not found, menu without stop"); return 0; }
|
||
if (kill(player_pid, SIGSTOP) < 0) {
|
||
logmsg("SIGSTOP %d failed: %s", player_pid, strerror(errno));
|
||
return -1;
|
||
}
|
||
/* wait for state T */
|
||
for (int i = 0; i < 100; i++) {
|
||
if (proc_is_stopped(player_pid)) break;
|
||
usleep(10000);
|
||
}
|
||
player_stopped = proc_is_stopped(player_pid);
|
||
logmsg("hiby_player pid=%d stopped=%d", player_pid, player_stopped);
|
||
return player_stopped ? 0 : -1;
|
||
}
|
||
|
||
static void cont_player(void)
|
||
{
|
||
if (player_stopped && player_pid > 0) {
|
||
kill(player_pid, SIGCONT);
|
||
logmsg("SIGCONT %d", player_pid);
|
||
}
|
||
player_stopped = 0;
|
||
}
|
||
|
||
/* ================================================================== */
|
||
/* menu */
|
||
/* ================================================================== */
|
||
#define ROW_H 52
|
||
#define ROW_TOP 96
|
||
#define EXIT_ROW 640
|
||
#define TOAST_Y 590
|
||
|
||
/* brightness save/restore (defined below, used by run_menu) */
|
||
static int saved_brightness = -1;
|
||
|
||
/* set by run_menu: the player's panel was blanked (screen timeout) when the
|
||
* menu opened. fb_open() unblanks it so the menu is visible; on session end
|
||
* restore_backlight() re-blanks (brightness 0 + FBIOBLANK(4)) to put the
|
||
* physical panel back into the state the player's display state machine
|
||
* assumes — otherwise the player resumes believing the panel is blanked
|
||
* while the panel is actually lit (frozen frame). Blanking is idempotent
|
||
* and matches what the player itself did, so it cannot wedge the player. */
|
||
static int menu_opened_blank = 0;
|
||
|
||
/* ---- config (MeowAPPs/meow.conf): pure black background by default ---- */
|
||
static uint32_t menu_bg = 0x000000;
|
||
|
||
/* Hotkey scheme (v6.5):
|
||
* hotkey=power_voldown (default) — HOLD power, then a single VOL_DOWN tap
|
||
* 60-2500ms after the power-down edge opens the menu. The player sees
|
||
* the power press (its screen-off flow starts; accepted side effect)
|
||
* and the VOL_DOWN tap (volume -1; accepted). Menu open runs the
|
||
* verified unblank + bring-backlight-only-if-dark logic (§18.15).
|
||
* hotkey=seq (legacy) — same key quick-tapped N times
|
||
* (default VOL_UP x3 within seq_gap_ms each). The PREVIOUS key combo
|
||
* was retired (its GPIO never fires on this unit, §18.12) — a multi-tap
|
||
* of ONE key is ADC-friendly and physical-safe.
|
||
* meow.conf keys:
|
||
* hotkey=power_voldown | seq
|
||
* hotkey_seq=volup,volup,volup (same key name repeated = N taps, seq mode)
|
||
* hotkey_seq_gap=600 (ms between taps, total window ~2x gap)
|
||
*/
|
||
#define HOTKEY_MODE_PWRVOL 0
|
||
#define HOTKEY_MODE_SEQ 1
|
||
#define HK_PWRWIN_MIN 60 /* min power-hold time before VOL_DOWN counts */
|
||
/* (2026-08-28: 300->60; 仅留防抖/去抖动, */
|
||
/* natural ~220ms 电源+音量减 实测被 300 拦下) */
|
||
#define HK_PWRWIN_MAX 2500 /* max power-hold window; beyond this no trig */
|
||
static int hotkey_mode = HOTKEY_MODE_PWRVOL;
|
||
#define MAX_SEQ_LEN 6
|
||
static int seq_codes[MAX_SEQ_LEN];
|
||
static int seq_len = 0;
|
||
static int seq_gap_ms = 600;
|
||
|
||
static int key_name_code(const char *n)
|
||
{
|
||
if (!strcmp(n, "volup")) return KEY_VOLUMEUP;
|
||
if (!strcmp(n, "voldown")) return KEY_VOLUMEDOWN;
|
||
if (!strcmp(n, "play")) return KEY_PLAYPAUSE;
|
||
if (!strcmp(n, "next")) return KEY_NEXT;
|
||
if (!strcmp(n, "previous")) return KEY_PREVIOUSSONG;
|
||
if (!strcmp(n, "power")) return KEY_POWER;
|
||
return -1;
|
||
}
|
||
|
||
static void load_config(void)
|
||
{
|
||
menu_bg = 0x000000;
|
||
hotkey_mode = HOTKEY_MODE_PWRVOL; /* default: hold power + VOL_DOWN tap */
|
||
seq_len = 0;
|
||
seq_gap_ms = 600;
|
||
char path[256];
|
||
snprintf(path, sizeof path, "%s/meow.conf", MEOWAPPS);
|
||
FILE *f = fopen(path, "r");
|
||
if (f) {
|
||
char buf[128];
|
||
while (fgets(buf, sizeof buf, f)) {
|
||
if (!strncmp(buf, "bg_color=", 9)) {
|
||
unsigned int v;
|
||
if (sscanf(buf + 9, "%x", &v) == 1) menu_bg = v & 0xFFFFFF;
|
||
} else if (!strncmp(buf, "hotkey=", 7)) {
|
||
char *p = buf + 7;
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
if (!strncmp(p, "seq", 3)) hotkey_mode = HOTKEY_MODE_SEQ;
|
||
else hotkey_mode = HOTKEY_MODE_PWRVOL;
|
||
} else if (!strncmp(buf, "hotkey_seq=", 11)) {
|
||
char *p = buf + 11;
|
||
seq_len = 0;
|
||
/* BUGFIX: the outer loop must consume the EOL — with
|
||
* *p staying at '\n' the empty-name branch never advanced p
|
||
* and the daemon spun forever (found on-device: a hotkey_seq
|
||
* line wedged load_config in a busy loop). */
|
||
while (*p && *p != '\n' && *p != '\r' && seq_len < MAX_SEQ_LEN) {
|
||
char name[16];
|
||
int k = 0;
|
||
while (*p && *p != ',' && *p != '\n' && *p != '\r' && k < 15)
|
||
name[k++] = *p++;
|
||
name[k] = 0;
|
||
if (k >= 15) /* malformed long name: skip to separator/EOL */
|
||
while (*p && *p != ',' && *p != '\n' && *p != '\r') p++;
|
||
if (*p == ',') p++;
|
||
int c = key_name_code(name);
|
||
if (c > 0) seq_codes[seq_len++] = c;
|
||
}
|
||
} else if (!strncmp(buf, "hotkey_seq_gap=", 15)) {
|
||
int g = atoi(buf + 15);
|
||
if (g >= 200 && g <= 2000) seq_gap_ms = g;
|
||
}
|
||
}
|
||
fclose(f);
|
||
}
|
||
if (seq_len == 0) { seq_codes[0] = KEY_VOLUMEUP; seq_len = 3; } /* seq fallback volup x3 */
|
||
if (hotkey_mode == HOTKEY_MODE_PWRVOL)
|
||
logmsg("config: bg_color=%06x hotkey=power_voldown (window %d-%dms)",
|
||
menu_bg, HK_PWRWIN_MIN, HK_PWRWIN_MAX);
|
||
else
|
||
logmsg("config: bg_color=%06x hotkey=seq (%d taps of code %d gap=%dms)",
|
||
menu_bg, seq_len, seq_codes[0], seq_gap_ms);
|
||
}
|
||
|
||
/* ---- volume save/restore ---- */
|
||
/* The player sets volume via ioctl on /dev/snd/controlC0 writing the
|
||
* CS43198 "Left/Right Playback Volume" controls (§11.4). We snapshot the
|
||
* same controls through amixer so a hotkey sequence's volume side effects
|
||
* can be restored silently (the player itself never reads these; next key
|
||
* press it writes its own value again — acceptable). */
|
||
struct vol_snap { int l, r; };
|
||
|
||
static int amixer_vol(int numid)
|
||
{
|
||
char cmd[96];
|
||
snprintf(cmd, sizeof cmd, "amixer -c 0 cget numid=%d 2>/dev/null", numid);
|
||
FILE *p = popen(cmd, "r");
|
||
if (!p) return -1;
|
||
char buf[128];
|
||
int v = -1;
|
||
while (fgets(buf, sizeof buf, p)) {
|
||
/* ": values=N" line — the other "values=1" on the type line is the
|
||
* element's value COUNT, not the actual volume */
|
||
char *s = strstr(buf, ": values=");
|
||
if (s) { v = atoi(s + 9); break; }
|
||
}
|
||
pclose(p);
|
||
return v;
|
||
}
|
||
|
||
static void amixer_set(int numid, int v)
|
||
{
|
||
if (v < 0) return;
|
||
char cmd[96];
|
||
snprintf(cmd, sizeof cmd, "amixer -c 0 cset numid=%d %d >/dev/null 2>&1",
|
||
numid, v);
|
||
system(cmd);
|
||
}
|
||
|
||
static struct vol_snap vol_snapshot(void)
|
||
{
|
||
struct vol_snap s = { amixer_vol(1), amixer_vol(2) };
|
||
return s;
|
||
}
|
||
|
||
static void vol_restore(struct vol_snap s)
|
||
{
|
||
logmsg("volume restore -> L=%d R=%d", s.l, s.r);
|
||
amixer_set(1, s.l);
|
||
amixer_set(2, s.r);
|
||
}
|
||
|
||
static int read_file_int(const char *path)
|
||
{
|
||
int fd = open(path, O_RDONLY);
|
||
if (fd < 0) return -1;
|
||
char buf[16];
|
||
int n = read(fd, buf, sizeof buf - 1);
|
||
close(fd);
|
||
if (n <= 0) return -1;
|
||
buf[n] = 0;
|
||
return atoi(buf);
|
||
}
|
||
|
||
/* ================================================================== */
|
||
/* 系统设置(阶段 B2a 统一后端:settings_volume 等仅作缓存) */
|
||
/* 统一后端(SYSTEM_SETTINGS.md §3/§7):扁平 key=value 存板载 */
|
||
/* /usr/data/meow/settings.conf(KB 级小文件)。单点数据源,clamp 集 */
|
||
/* 中在本模块。此处的全局变量(settings_volume/screen_off_timeout/ */
|
||
/* refresh_rate_cfg 等)仅作缓存,由 meow_settings 的 set 回调/加载 */
|
||
/* 后同步,供既有大量引用这些变量的代码(DAC/状态栏/息屏/刷新率调度)*/
|
||
/* 使用,零侵入、不破坏编译与功能。 */
|
||
/* ================================================================== */
|
||
#define MEOW_VOL_MAX 100 /* 硬上限 0-100 */
|
||
#define MEOW_VOL_DEFAULT 60 /* 默认音量 */
|
||
#define MEOW_VOL_STEP_DEFAULT 1 /* 默认跳步 1%(线性渐变;可配 audio.volume_step 1-5) */
|
||
#define MEOW_VOL_STEP_MAX 5 /* 跳步上限(SYSTEM_SETTINGS.md:最大 5) */
|
||
#define MEOW_VOL_MAX_DEFAULT 100 /* 默认最大音量上限(尊重上限;可配 audio.volume_max) */
|
||
static int settings_volume = MEOW_VOL_DEFAULT; /* 当前音量 0-100(= 写入 DAC 的映射值) */
|
||
static int settings_volume_max = MEOW_VOL_MAX_DEFAULT; /* 最大音量上限 clamp */
|
||
static int settings_volume_step = MEOW_VOL_STEP_DEFAULT; /* 跳步 */
|
||
|
||
/* M2.17/M2.16:把 meow_settings 当前值同步到本模块的缓存全局。读取方
|
||
* (DAC/状态栏/息屏/刷新率调度)统一从缓存取,避免散读 backend。 */
|
||
static void settings_sync_cache_from_backend(void)
|
||
{
|
||
int v;
|
||
if (meow_settings_get_int("audio.volume", &v) == 0) settings_volume = v;
|
||
if (meow_settings_get_int("audio.volume_max", &v) == 0) settings_volume_max = v;
|
||
if (meow_settings_get_int("audio.volume_step", &v) == 0) settings_volume_step = v;
|
||
if (meow_settings_get_enum("display.refresh_rate", &v) == 0) /* names "60,30,15" */
|
||
refresh_rate_cfg = (v == 1) ? 30 : (v == 2) ? 15 : 60;
|
||
if (meow_settings_get_bool("display.refresh_always_max", &v) == 0) refresh_always_max = v;
|
||
if (meow_settings_get_int("display.screen_off_timeout", &v) == 0) screen_off_timeout = v;
|
||
if (meow_settings_get_enum("display.screen_off_mode", &v) == 0) screen_off_mode = v;
|
||
if (meow_settings_get_int("power.longpress_menu_ms", &v) == 0) power_longpress_menu_ms = v;
|
||
if (meow_settings_get_int("power.forceoff_ms", &v) == 0) power_forceoff_ms = v;
|
||
}
|
||
|
||
/* meow_settings 变更回调:set_*(如音量键)后同步缓存全局,保证既有引用
|
||
* settings_volume 等变量的代码与 backend 单点一致(SYSTEM_SETTINGS §3)。 */
|
||
static void settings_sync_cache_listener(const char *key)
|
||
{
|
||
if (!strcmp(key, "audio.volume")) { int v; if (meow_settings_get_int("audio.volume", &v) == 0) settings_volume = v; }
|
||
else if (!strcmp(key, "audio.volume_max")) { int v; if (meow_settings_get_int("audio.volume_max", &v) == 0) settings_volume_max = v; }
|
||
else if (!strcmp(key, "audio.volume_step")) { int v; if (meow_settings_get_int("audio.volume_step", &v) == 0) settings_volume_step = v; }
|
||
else if (!strcmp(key, "display.refresh_rate")) { int v; if (meow_settings_get_enum("display.refresh_rate", &v) == 0) refresh_rate_cfg = (v == 1) ? 30 : (v == 2) ? 15 : 60; }
|
||
else if (!strcmp(key, "display.refresh_always_max")) { int v; if (meow_settings_get_bool("display.refresh_always_max", &v) == 0) refresh_always_max = v; }
|
||
else if (!strcmp(key, "display.screen_off_timeout")) { int v; if (meow_settings_get_int("display.screen_off_timeout", &v) == 0) screen_off_timeout = v; }
|
||
else if (!strcmp(key, "display.screen_off_mode")) { int v; if (meow_settings_get_enum("display.screen_off_mode", &v) == 0) screen_off_mode = v; }
|
||
else if (!strcmp(key, "power.longpress_menu_ms")) { int v; if (meow_settings_get_int("power.longpress_menu_ms", &v) == 0) power_longpress_menu_ms = v; }
|
||
else if (!strcmp(key, "power.forceoff_ms")) { int v; if (meow_settings_get_int("power.forceoff_ms", &v) == 0) power_forceoff_ms = v; }
|
||
}
|
||
|
||
/* ---- 通知 dump 回调(通知#6 转储 TF 卡;做成真)----
|
||
* meow_notif 在 TTL 到点(meow_notif_tick,§8 10s)调用此回调归档一条通知,随后释放槽。
|
||
* 本回调把 meow_notif 结构体转成一行 CSV 追加写入 <卡>/MeowOS/notifications/notifications.log
|
||
* (含 mkdir -p 与按总大小滚动截断、保留最新 10MB),落盘实现在 meow_notif.c 的
|
||
* meow_notif_dump_tf()。无卡/不可写仅打点降级(NOTIFICATION_SYSTEM.md §5/§8 + MEWSPEC §5.3)。 */
|
||
static void notif_dump_tf_placeholder(const meow_notif_t *n)
|
||
{
|
||
meow_notif_dump_tf(n);
|
||
}
|
||
|
||
/* 启动加载(M2.11/M2.24):统一由 meow_settings 后端读 settings.conf;
|
||
* 此处仅注册监听(一次性)+ 把 backend 当前值同步到缓存全局。重载(应用期
|
||
* watchdog 可能改音量)等价于 meow_settings_init() + 手动同步。 */
|
||
static int settings_cache_registered = 0; /* M2.24:监听只注册一次 */
|
||
static void settings_load(void)
|
||
{
|
||
meow_settings_init(); /* 幂等:注册默认 + 读 settings.conf */
|
||
meow_notif_init(); /* M2a:通知存储初始化(幂等,读 notify.* 配额) */
|
||
meow_notif_set_dump_cb(notif_dump_tf_placeholder); /* 通知#6:挂 dump 回调 = TF 转储(meow_notif_dump_tf:一行 CSV 追加 + 10MB 滚动截断) */
|
||
if (!settings_cache_registered) {
|
||
meow_settings_add_listener(settings_sync_cache_listener);
|
||
settings_cache_registered = 1;
|
||
}
|
||
settings_sync_cache_from_backend();
|
||
logmsg("settings: vol=%d(max=%d,step=%d) refresh=%d(always_max=%d) off=%ds mode=%s",
|
||
settings_volume, settings_volume_max, settings_volume_step,
|
||
refresh_rate_cfg, refresh_always_max, screen_off_timeout,
|
||
screen_off_mode ? "black" : "panel");
|
||
}
|
||
|
||
/* ---- MeowOS 真实 DAC 音量(CS43198,ANALYSIS §18.39)----
|
||
* HibyOS 播放器经 ioctl 直写 /dev/snd/controlC0 的 "Left/Right Playback
|
||
* Volume"(numid 1/2,0-255)→ codec 驱动 cs43198_vol_*_put → I2C 直写 CS43198
|
||
* 寄存器(0x9000-0x9002/0x7000-0x7001,双片 L/R)。我们复刻同路径(amixer
|
||
* cset = SNDRV_CTL_IOCTL_ELEM_WRITE),0-100 → 0-255 映射直写两声道。
|
||
* 读回不可用(驱动 get 为 dummy 恒 0;/dev/i2c-3 被驱动占用),故 meow_vol_get()
|
||
* 返回我们写入的跟踪值(= 真实 DAC 映射值)。 */
|
||
static void meow_dac_vol_write(int pct)
|
||
{
|
||
int dac = pct * 255 / 100;
|
||
if (dac < 0) dac = 0;
|
||
if (dac > 255) dac = 255;
|
||
char cmd[160];
|
||
snprintf(cmd, sizeof cmd,
|
||
"amixer -c 0 cset numid=1 %d >/dev/null 2>&1; amixer -c 0 cset numid=2 %d >/dev/null 2>&1",
|
||
dac, dac);
|
||
system(cmd);
|
||
logmsg("DAC vol -> L/R=%d (pct %d)", dac, pct);
|
||
}
|
||
|
||
/* ---- M2.38 音量滑条拖动节流:meow_dac_vol_write 每次 system() fork+exec /bin/sh + 2 个
|
||
* amixer,嵌入式 MIPS 上极慢;滑条拖动逐帧 set → s_commit → system() 导致卡死十几秒。
|
||
* 修法:墙钟节流(两次真实 DAC 写入 ≥ VOL_DRAG_DAC_MS),拖动中只更新缓存 + 刷状态栏,
|
||
* 抬手/结束时经 meow_dac_vol_flush() 无条件直写一次,保证最终音量必然落 DAC。 ---- */
|
||
#define VOL_DRAG_DAC_MS 60 /* 滑条拖动期间两次真实 DAC 写入的最短墙钟间隔 */
|
||
static unsigned long long vol_dac_last_ms = 0;
|
||
|
||
static void meow_dac_vol_write_throttled(void)
|
||
{
|
||
unsigned long long now = now_ms();
|
||
if (now - vol_dac_last_ms < VOL_DRAG_DAC_MS) return; /* 节流窗口内跳过,等抬手统一写 */
|
||
vol_dac_last_ms = now;
|
||
meow_dac_vol_write(settings_volume);
|
||
}
|
||
|
||
/* 抬手/点击提交连 save 时调用:无条件直写一次真实 DAC(绕过节流,保证最终音量落 DAC/落盘)。 */
|
||
static void meow_dac_vol_flush(void)
|
||
{
|
||
vol_dac_last_ms = now_ms();
|
||
meow_dac_vol_write(settings_volume);
|
||
}
|
||
|
||
/* clamp 集中:0..MEOW_VOL_MAX,再 clamp 到上限 settings_volume_max。 */
|
||
static int vol_clamp(int v)
|
||
{
|
||
if (v < 0) v = 0;
|
||
if (v > MEOW_VOL_MAX) v = MEOW_VOL_MAX;
|
||
if (v > settings_volume_max) v = settings_volume_max;
|
||
return v;
|
||
}
|
||
static void settings_volume_set(int v)
|
||
{
|
||
v = vol_clamp(v); /* 0..MEOW_VOL_MAX,再 clamp 到上限 */
|
||
meow_settings_set_int("audio.volume", v); /* clamp 集中在 backend;listener 同步缓存 */
|
||
meow_dac_vol_write(settings_volume); /* 每次变更即直写真实 DAC(CS43198) */
|
||
meow_settings_save(); /* 单点变更即落盘 */
|
||
}
|
||
static int settings_volume_get(void) { return settings_volume; }
|
||
|
||
/* ---- M2.14 线性渐变:step>1 时按 1% 递增爬到目标,避免一次跳 N% 突兀 ----
|
||
* 每主循环 tick 最多迈 1%(VOL_RAMP_MS 墙钟节流,不阻塞主循环);到目标若
|
||
* 键仍按住则继续延段(长按=连续渐变到松开/到上限);step=1 时一步即达(天然平滑)。
|
||
* 落盘只在渐变结束时一次(meow_settings_save),不逐 1% 写盘。 */
|
||
#define VOL_RAMP_MS 15 /* 每 1% 步进的最短墙钟间隔 */
|
||
static int vol_ramp_dir = 0; /* +1 上 / -1 下 / 0 停 */
|
||
static int vol_ramp_target = -1; /* 目标值;-1=无 */
|
||
static unsigned long long vol_ramp_next_at = 0;
|
||
static unsigned long long vol_ramp_press_at = 0; /* M2.18 Bug 4:本方向键按下时刻(长按判据) */
|
||
|
||
static void vol_ramp_begin(int delta)
|
||
{
|
||
int target = vol_clamp(settings_volume + delta);
|
||
if (target == settings_volume) return;
|
||
vol_ramp_dir = (target > settings_volume) ? 1 : -1;
|
||
vol_ramp_target = target;
|
||
vol_ramp_press_at = now_ms(); /* M2.18 Bug 4:记录该方向键按下时刻,供 CLICK_MS 长按判定 */
|
||
vol_ramp_next_at = 0; /* 立即迈第一步,给即时反馈 */
|
||
}
|
||
|
||
/* 每主循环 tick 调;key_held=该方向按键是否仍按住(长按续爬)。返回 1=仍在渐变。
|
||
* M2.18 Bug 4 语义:单击(<CLICK_MS=400ms) 到 target 即停(只跳 step 格);长按(≥400ms) 才
|
||
* 续段连续爬。普通按压力 50-150ms 远大于 VOL_RAMP_MS(15ms),旧逻辑在到 target 后只要
|
||
* key_held=1 就续段 → 单击被当成长按,连爬 6~7 格(「点一下跳好几格」)。CLICK_MS 此前只
|
||
* 被 kd_tick 状态机使用,vol_ramp 路径从未接上。 */
|
||
static int vol_ramp_tick(int key_held)
|
||
{
|
||
if (vol_ramp_dir == 0) return 0;
|
||
unsigned long long now = now_ms();
|
||
if (now < vol_ramp_next_at) return 1;
|
||
vol_ramp_next_at = now + VOL_RAMP_MS;
|
||
/* 先看是否已到「当前目标值」(settings_volume 等于 target,而非候选 v)——
|
||
* 因为一旦在 target 再取 v=settings_volume+dir 便越过 target,导致 hold_wait 失效继续爬。 */
|
||
if (settings_volume == vol_ramp_target) {
|
||
if (!key_held) { /* 松开 → 一段完成,结束(单击到 target 即停,只跳 step 格) */
|
||
vol_ramp_dir = 0; vol_ramp_target = -1;
|
||
meow_settings_save(); return 0;
|
||
}
|
||
if (now - vol_ramp_press_at < CLICK_MS) {
|
||
/* hold_wait:已到 target 但未到长按阈值 → 停在 target 不爬(保持待命不清 dir,
|
||
* 等长按激活或松开);若直接续段,长按第一次到 target 就断了。 */
|
||
return 1;
|
||
}
|
||
/* 长按激活:续段(在 target 基础上再 +step) */
|
||
vol_ramp_target = vol_clamp(vol_ramp_target + vol_ramp_dir * settings_volume_step);
|
||
if (vol_ramp_target == settings_volume) { /* 续段到上限无进展 → 结束 */
|
||
vol_ramp_dir = 0; vol_ramp_target = -1;
|
||
meow_settings_save(); return 0;
|
||
}
|
||
}
|
||
int v = vol_clamp(settings_volume + vol_ramp_dir);
|
||
if (v == settings_volume) { /* 已达上下限 */
|
||
vol_ramp_dir = 0; vol_ramp_target = -1;
|
||
meow_settings_save(); return 0;
|
||
}
|
||
meow_settings_set_int("audio.volume", v); /* 更新 backend 内存 + 同步缓存(不逐 1% 落盘) */
|
||
meow_dac_vol_write(settings_volume); /* 每 1% 写一次真实 DAC(settings_volume 已同步) */
|
||
return 1;
|
||
}
|
||
|
||
/* 步进调节(±settings_volume_step)→ 起线性渐变(1% 爬,见上)。 */
|
||
static void settings_volume_adj(int delta) { vol_ramp_begin(delta); }
|
||
|
||
/* MeowOS 音量 API:get 返回当前(= 写入 DAC 的映射值);set 写真实 DAC + 持久化。 */
|
||
static void meow_vol_set(int pct) { settings_volume_set(pct); }
|
||
static int meow_vol_get(void) { return settings_volume; }
|
||
|
||
/* ================================================================== */
|
||
/* 系统状态统一快照(A:进 MeowOS 全量重抓 + 每进必刷) */
|
||
/* 主循环 10ms 热键轮询绝不读系统态;全量 meow_sys_refresh() 仅在进 */
|
||
/* MeowOS 时调一次;1s 轻刷 meow_sys_light() 只更新时钟/电量/WiFi, */
|
||
/* 并约 5s 节流探测一次蓝牙角色(M2.48)。 */
|
||
/* 状态栏/后续设置/面板统一读此快照,单点一致,避免重复读 amixer/sysfs。*/
|
||
/* ================================================================== */
|
||
typedef struct meow_sys_state {
|
||
int batt; /* 容量 % 0-100,-1 未知 */
|
||
int charging; /* 1 充电 0 放电 */
|
||
int batt_mv; /* 电压 mV */
|
||
int wifi_on; /* M2.48:1 网卡 up(射频开)0 down(关则不显示图标) */
|
||
int wifi_connected;/* M2.48:1 已关联 AP(operstate up)0 未连(区分「网卡up未连」→WIFI_NO) */
|
||
char wifi_ssid[33];
|
||
char wifi_ip[16];
|
||
int bt_on; /* 1 蓝牙上电 0 关 */
|
||
int bt_seen; /* hci 节点存在 1/0(无模块不放图标) */
|
||
int bt_role; /* M2.48:蓝牙角色(bt_role() 判定缓存):BT_ROLE_* */
|
||
int vol; /* 系统软件音量 0-100(settings) */
|
||
int brightness; /* sysfs */
|
||
long data_free_kb; /* /data 剩余 KB */
|
||
int sd_mounted; /* TF 卡已挂载 */
|
||
long sd_free_kb; long sd_total_kb;
|
||
int mode; /* 1 meow 0 hiby(调用方) */
|
||
int napps; /* 应用数(调用方) */
|
||
char ver[16];
|
||
} meow_sys_state;
|
||
static meow_sys_state g_sys;
|
||
|
||
/* M2.48 蓝牙角色(状态栏多态 + 设置页"蓝牙模式"行共用;bt_role() 判定,缓存进 g_sys.bt_role) */
|
||
#define BT_ROLE_OFF 0 /* 关 */
|
||
#define BT_ROLE_ON_NO_CONN 1 /* 开未连接 */
|
||
#define BT_ROLE_MASTER 2 /* 主模式:连了外围(如耳机) */
|
||
#define BT_ROLE_SLAVE 3 /* 从模式:被远端接入,设备自身当耳机 */
|
||
#define BT_ROLE_UNKNOWN (-1) /* 判定失败(调用方按实际态兜底) */
|
||
static char g_bt_mode[48] = "开启未连接"; /* 设置页"蓝牙模式"行展示(bt_role_refresh 刷新) */
|
||
static int bt_role(char *desc, int len); /* 前向声明;实现在网络段(用 popen 带超时) */
|
||
static void bt_role_refresh(int force); /* 节流重探(force=1 立即;进系统/设置页/网络动作用) */
|
||
|
||
/* M2.47 版本号单一数据源:/etc/meowos_version(固件内独立文件,打包脚本写)。
|
||
* launcher 启动时读它,右下角显示/SSH 识别同一来源;不再维护 MEOWOS_VERSION 宏与文件两处。
|
||
* 读取失败/缺失时保留内置默认(最后一次编译时版本)。 */
|
||
#define MEOWOS_VERSION_FILE "/etc/meowos_version"
|
||
#define MEOWOS_VERSION_DEF MEOWOS_VERSION /* 兜底默认 = 编译期宏;运行时读 /etc/meowos_version 覆盖 */
|
||
static char g_meowos_version[16] = MEOWOS_VERSION_DEF;
|
||
static void load_meowos_version(void)
|
||
{
|
||
FILE *f = fopen(MEOWOS_VERSION_FILE, "r");
|
||
if (!f) { logmsg("meowos_version: %s missing, use default %s", MEOWOS_VERSION_FILE, g_meowos_version); return; }
|
||
char buf[32];
|
||
if (fgets(buf, sizeof buf, f)) {
|
||
char *e = buf + strlen(buf);
|
||
while (e > buf && (e[-1] == '\n' || e[-1] == '\r' || e[-1] == ' ')) e--;
|
||
*e = 0;
|
||
if (buf[0]) snprintf(g_meowos_version, sizeof g_meowos_version, "%s", buf);
|
||
}
|
||
fclose(f);
|
||
logmsg("meowos_version: read %s = '%s'", MEOWOS_VERSION_FILE, g_meowos_version);
|
||
}
|
||
|
||
/* 读一份 sysfs 字符串(剥尾随换行);失败回退 "" */
|
||
static void read_sys_str(const char *path, char *buf, size_t n)
|
||
{
|
||
buf[0] = 0;
|
||
int fd = open(path, O_RDONLY);
|
||
if (fd < 0) return;
|
||
ssize_t r = read(fd, buf, n - 1);
|
||
close(fd);
|
||
if (r <= 0) { buf[0] = 0; return; }
|
||
buf[r] = 0;
|
||
while (r > 0 && (buf[r-1] == '\n' || buf[r-1] == '\r')) buf[--r] = 0;
|
||
}
|
||
|
||
/* 蓝牙实际上电状态(A/E:不只看 hci0 存在——节点常在但控制器可能 DOWN)。
|
||
* 本设备 /sys/class/bluetooth/hci0/powered 不存在(旧内核/BlueZ 5.54),
|
||
* 权威来源为 hciconfig -a hci0 的 UP/DOWN 行。费用较高(spawn),故缓存,
|
||
* 由 meow_sys_refresh()(进系统一次)/meow_sys_light()(轻刷)节流更新;
|
||
* 状态栏只读 g_sys.bt_on。 */
|
||
static int hci_powered(void)
|
||
{
|
||
FILE *p = popen("hciconfig -a hci0 2>/dev/null", "r");
|
||
if (!p) return 0;
|
||
int up = 0;
|
||
char buf[256];
|
||
while (fgets(buf, sizeof buf, p)) {
|
||
/* 状态行含 "UP RUNNING" 或 "DOWN"。匹配单词 UP(避免误中 UART 等)。 */
|
||
char *s = buf, *tok;
|
||
while ((tok = strsep(&s, " \t\r\n")) != NULL) {
|
||
if (!strcmp(tok, "UP")) { up = 1; break; }
|
||
}
|
||
}
|
||
pclose(p);
|
||
return up;
|
||
}
|
||
|
||
/* 全量重抓(进 MeowOS 时调用一次;阻塞可接受)。绝不放进 10ms 主循环。 */
|
||
static void meow_sys_refresh(void)
|
||
{
|
||
load_meowos_version(); /* M2.47:读 /etc/meowos_version(单一数据源,每次刷新对齐) */
|
||
int b = read_file_int("/sys/class/power_supply/battery/capacity");
|
||
g_sys.batt = (b >= 0 && b <= 100) ? b : -1;
|
||
char st[16];
|
||
read_sys_str("/sys/class/power_supply/battery/status", st, sizeof st);
|
||
g_sys.charging = (strstr(st, "Charg") || strstr(st, "Full")) ? 1 : 0;
|
||
g_sys.batt_mv = read_file_int("/sys/class/power_supply/battery/voltage_now") / 1000;
|
||
|
||
/* WiFi:wifi_on=网卡管理态 up(射频开);wifi_connected=operstate up(已关联 AP)。
|
||
* M2.48:二者分工,状态栏才能区分「开未连接(WIFI_NO)」与「已连接(信号分档)」。 */
|
||
char op[16];
|
||
read_sys_str("/sys/class/net/wlan0/operstate", op, sizeof op);
|
||
g_sys.wifi_connected = !strcmp(op, "up") ? 1 : 0;
|
||
g_sys.wifi_on = wlan_radio_up();
|
||
g_sys.wifi_ip[0] = 0; g_sys.wifi_ssid[0] = 0;
|
||
if (g_sys.wifi_connected) {
|
||
/* 行形如 "2: wlan0 inet <device_ip>/24 brd ...",取 inet 后第一个斜杠前的 token。
|
||
* 用大缓冲读整行,再从局部 token 拷贝到 16B 小字段(避免 OOB)。 */
|
||
char big[256];
|
||
FILE *p = popen("ip -o addr show wlan0 2>/dev/null | grep 'inet '", "r");
|
||
if (p) {
|
||
if (fgets(big, sizeof big, p)) {
|
||
char *s = strstr(big, "inet ");
|
||
if (s) { s += 5; while (*s == ' ') s++; char *e = strchr(s, '/'); if (e) *e = 0;
|
||
snprintf(g_sys.wifi_ip, sizeof g_sys.wifi_ip, "%s", s); }
|
||
}
|
||
pclose(p);
|
||
}
|
||
/* SSID 尽力读(wpa_cli 若在;失败留空不外溢)。注意:wpa_cli 可能因 wpa_supplicant
|
||
* 未跑而挂起——用后台 + 短等待避免阻塞 UI;超时即放弃(SSID 仅面板用,状态栏不读)。 */
|
||
}
|
||
|
||
/* 蓝牙:hci0 存在性 + 上电状态 + M2.48 角色/连接(进系统全量重探一次) */
|
||
g_sys.bt_seen = access("/sys/class/bluetooth/hci0", F_OK) == 0;
|
||
g_sys.bt_on = g_sys.bt_seen ? hci_powered() : 0;
|
||
bt_role_refresh(1); /* 立即探测角色/连接态,供状态栏与设置页"蓝牙模式"行 */
|
||
|
||
/* 音量(settings;进 MeowOS 全量时把真实 DAC 同步到当前档)/ 亮度 */
|
||
g_sys.vol = settings_volume_get();
|
||
meow_dac_vol_write(settings_volume); /* M2.13:进 MeowOS 即把 CS43198 DAC 置为当前音量 */
|
||
int br = read_brightness();
|
||
g_sys.brightness = (br >= 0) ? br : 0;
|
||
|
||
/* 存储:/data 剩余 + TF 卡挂载容量 */
|
||
g_sys.data_free_kb = 0;
|
||
FILE *df = popen("df -k /data 2>/dev/null | awk 'NR==2{print $4}'", "r");
|
||
if (df) { char x[24]; if (fgets(x, sizeof x, df)) g_sys.data_free_kb = atol(x); pclose(df); }
|
||
g_sys.sd_mounted = sd_mounted();
|
||
g_sys.sd_free_kb = g_sys.sd_total_kb = 0;
|
||
if (g_sys.sd_mounted) {
|
||
FILE *df2 = popen("df -k /usr/data/mnt/sd_0 2>/dev/null | awk 'NR==2{print $4, $2}'", "r");
|
||
if (df2) { char x[48]; if (fgets(x, sizeof x, df2)) { sscanf(x, "%ld %ld", &g_sys.sd_free_kb, &g_sys.sd_total_kb); } pclose(df2); }
|
||
}
|
||
|
||
snprintf(g_sys.ver, sizeof g_sys.ver, "%s", g_meowos_version);
|
||
logmsg("sys snapshot: batt=%d%% chg=%d wifi=%d ssid=%s ip=%s bt=%d/%d vol=%d bri=%d "
|
||
"data_free=%ldKB sd=%d(%ld/%ldKB)",
|
||
g_sys.batt, g_sys.charging, g_sys.wifi_on, g_sys.wifi_ssid[0]?g_sys.wifi_ssid:"-",
|
||
g_sys.wifi_ip[0]?g_sys.wifi_ip:"-", g_sys.bt_on, g_sys.bt_seen,
|
||
g_sys.vol, g_sys.brightness, g_sys.data_free_kb, g_sys.sd_mounted,
|
||
g_sys.sd_free_kb, g_sys.sd_total_kb);
|
||
}
|
||
|
||
/* 轻刷(1s 定时,只更新时钟/电量/WiFi 信号 + 节流探测蓝牙角色——便宜,绝不碰负载/温度) */
|
||
static void meow_sys_light(void)
|
||
{
|
||
int b = read_file_int("/sys/class/power_supply/battery/capacity");
|
||
if (b >= 0 && b <= 100) g_sys.batt = b;
|
||
char op[16];
|
||
read_sys_str("/sys/class/net/wlan0/operstate", op, sizeof op);
|
||
g_sys.wifi_connected = !strcmp(op, "up") ? 1 : 0;
|
||
g_sys.wifi_on = wlan_radio_up();
|
||
g_sys.vol = settings_volume_get(); /* 每进 MeowOS 重抓;此处同步内存值 */
|
||
bt_role_refresh(0); /* M2.48:节流探测蓝牙角色(约 5s 一次,不阻塞主循环) */
|
||
}
|
||
|
||
/* 40px status bar (Bug 1, 2026-08-28): the old 20px bar is doubled so the
|
||
* app list can also show voltage + bluetooth alongside time / battery / wifi.
|
||
* line 1: time (left) battery% WIFI BT (right)
|
||
* line 2: VOL nn% (left)
|
||
* M2.11:值全部取自 meow_sys_state 快照(g_sys),不再每帧读 amixer/sysfs;
|
||
* 音量 = settings audio.volume(非 CS43198 死镜像);蓝牙 = 实际 powered;
|
||
* WiFi = operstate 实际 up。纯黑背景 = menu_bg;分隔线 y=28(栏高 40→28)。*/
|
||
/* ---- 状态栏固定完整格重绘(M2.17:修「整个状态栏部件脏区残留」)----
|
||
* 根因:M2.16 各部件/数字更新只把「当前内容包围盒」标为脏区并用它清底——旧内容超出新包围盒的
|
||
* 部分(时钟数字 '0' 底弧落于新 '1' 包围盒外、电量%/WiFi/BT 出现或消失、音量数字)未被重绘 →
|
||
* 残留;按音量键全屏刷新才清。修法:状态栏(0,0,480×28)用一条固定完整格:字段更新时整条清底 +
|
||
* 重绘时钟/右侧 + 整条标脏(SDK back_buf 做最终态 → meow_ui_flush 原子 blit 整条,无中间态)。
|
||
* 任何旧内容残留被整条覆盖。480×28≈27KB 极小,每秒一 blit 无碍;分隔线 y=28 单独绘制不在此列。
|
||
* (逐字段固定格在 480px 宽 + 「蓝牙」CJK 全字段同现时与时钟格重叠,故取整条退化——§18.46。) */
|
||
#define SB_BAR_Y 0
|
||
#define SB_BAR_H 28 /* rows 0..27;分隔线在 y=28 不在此(40→28,用户定) */
|
||
#define SB_FONT 20 /* 状态栏字体 28→20(28px 栏放不下 28 号;垂直居中锚点见 sb_text_y) */
|
||
static int g_sb_clock_x, g_sb_clock_y, g_sb_clock_w, g_sb_clock_h;
|
||
static int g_sb_right_x, g_sb_right_y, g_sb_right_w, g_sb_right_h;
|
||
|
||
/* ---- 主题(MD3 color roles,dark/light 两套)----
|
||
* M2.44:上移到本处(状态栏绘制之前),使状态栏/设置页共用同一套主题色板。 */
|
||
typedef struct meow_ui_theme {
|
||
uint32_t surface; /* 全局背景 */
|
||
uint32_t surface_variant; /* 卡片/列表底 */
|
||
uint32_t surface_container; /* 控件容器 */
|
||
uint32_t primary; /* 高亮/选中/accent */
|
||
uint32_t on_surface; /* 主文字 */
|
||
uint32_t on_surface_variant; /* 次文字 */
|
||
uint32_t outline; /* 描边/分隔线 */
|
||
uint32_t error; /* 错误提示 */
|
||
uint32_t surface_tint; /* 选中态叠色 */
|
||
} meow_ui_theme_t;
|
||
|
||
/* MD3 tonal palette(565 参考值见 MD3_UI_SPEC §1;此处存 24-bit RGB,#hex) */
|
||
static const meow_ui_theme_t meow_ui_theme_dark = {
|
||
0x141824, 0x1E2436, 0x263038, 0x4A6A9A, 0xFFFFFF,
|
||
0x9AA4C8, 0x2A2E38, 0xE57373, 0x4A6A9A
|
||
};
|
||
static const meow_ui_theme_t meow_ui_theme_light = {
|
||
0xE8EAF0, 0xD5D9E4, 0xCFD4E0, 0x2A5A8A, 0x1A1A2A,
|
||
0x4A4F5A, 0x9AA4C8, 0xB3261E, 0x2A5A8A
|
||
};
|
||
|
||
/* B2b 主题三方式(manual 深色 / manual 浅色 / 时段自动)。
|
||
* display.theme_mode(enum "manual,auto"):manual=读 display.theme(dark/light);
|
||
* auto=按深色时段。display.theme 保留为手动深浅的当前值(manual 下 s_ui_theme 直读它)。 */
|
||
#define THEME_MODE_MANUAL 0 /* display.theme_mode 枚举索引 0 = 手动 */
|
||
#define THEME_MODE_AUTO 1 /* display.theme_mode 枚举索引 1 = 时段自动 */
|
||
/* 获取当前本地时刻的「当天第几分钟」(tm_hour*60+tm_min);供时段判定用 */
|
||
static int s_now_min_of_day(void)
|
||
{
|
||
time_t t = time(NULL);
|
||
struct tm tm;
|
||
localtime_r(&t, &tm);
|
||
return tm.tm_hour * 60 + tm.tm_min;
|
||
}
|
||
/* 深色时段判定(纯逻辑,便于单测):now_min ∈ [start, end) → 深色。
|
||
* - start==end 视为未配置,返回 -1(由调用方决定回退策略)。
|
||
* - start<end:非跨天,如 720→1080 = 12:00→18:00,now ∈ [720,1080) 为深色。
|
||
* - start>end:跨午夜,如 1140→420 = 19:00→次日07:00,now∈[1140,1440)∪[0,420) 为深色。
|
||
* - 边界:end 半开(恰好 end 分回到浅色),start 闭(恰好 start 分进入深色)。 */
|
||
static int theme_should_dark(int now_min, int start, int end)
|
||
{
|
||
if (start == end) return -1; /* 未配置 */
|
||
if (start < end)
|
||
return (now_min >= start && now_min < end) ? 1 : 0;
|
||
return (now_min >= start || now_min < end) ? 1 : 0; /* 跨午夜 */
|
||
}
|
||
/* 当前主题(dark/light 两套;接 display.theme_mode + display.theme)。
|
||
* manual(mode==THEME_MODE_MANUAL):直读 display.theme(dark/light),保持原有行为。
|
||
* auto(mode==THEME_MODE_AUTO):读深色时段两键 + 当前本地分钟;start==end 未配置 →
|
||
* 回退 manual(读 display.theme,避免误判全天深色)。
|
||
* 读取失败一律回退 dark。 */
|
||
static const meow_ui_theme_t *s_ui_theme(void)
|
||
{
|
||
int mode = THEME_MODE_MANUAL;
|
||
if (meow_settings_get_enum("display.theme_mode", &mode) != 0) mode = THEME_MODE_MANUAL;
|
||
int v = 0; /* display.theme(manual)当前索引 */
|
||
int have_manual = (meow_settings_get_enum("display.theme", &v) == 0);
|
||
if (mode == THEME_MODE_AUTO) {
|
||
int start = 0, end = 0;
|
||
meow_settings_get_int("display.theme_dark_start", &start);
|
||
meow_settings_get_int("display.theme_dark_end", &end);
|
||
int d = theme_should_dark(s_now_min_of_day(), start, end);
|
||
if (d < 0) { /* 未配置:回退 manual(display.theme) */
|
||
if (have_manual && v == 1) return &meow_ui_theme_light;
|
||
return &meow_ui_theme_dark;
|
||
}
|
||
return d ? &meow_ui_theme_dark : &meow_ui_theme_light;
|
||
}
|
||
/* manual:照旧读 display.theme */
|
||
if (have_manual && v == 1) return &meow_ui_theme_light;
|
||
return &meow_ui_theme_dark;
|
||
}
|
||
|
||
/* ---- 状态栏右簇图标化(MD3)---- 布局常量 */
|
||
#define SB_TGAP 6 /* 图标右缘 ↔ 文字间隙 */
|
||
#define SB_XGAP 10 /* 相邻两项(右→左)间隙 */
|
||
#define SB_ICON_INK 16 /* 主图标目标 ink 高(px):24→16(28px 栏等比缩放,留上下边距) */
|
||
#define SB_CLK_MARGIN 14 /* 右簇左缘相对时钟右缘须留出的安全间距 */
|
||
/* 图标 ink 垂直中心线 = 状态栏文字垂直中心(28px 栏文字按 (28-行高)/2 锚定后中心≈14,见 sb_text_y)。
|
||
* 图标 ink 对齐此线 → 与数字/文字同中线(修「图标比文字低」);如需微调只改这一个常量。 */
|
||
#define SB_ICON_CY 14
|
||
static int g_sb_icon_cy = SB_ICON_CY;
|
||
|
||
/* 状态栏文字垂直锚 y(行首升线顶):(栏高-行高)/2 把文字垂直居中于 28px 栏;
|
||
* 下限 1 防极端行高(>栏高)时算出负 y 溢出顶端(不越界优先)。 */
|
||
static int sb_text_y(void)
|
||
{
|
||
int y = (SB_BAR_H - ui_text_height(SB_FONT)) / 2;
|
||
return (y < 1) ? 1 : y;
|
||
}
|
||
|
||
/* 状态栏 MD3 图标 blit:把 sb_icons.h 的 32x32 单通道 alpha 位图画进状态栏(fb = SDK back_buf)。
|
||
* x = 图标槽左缘(槽宽 SB_ICON_SIZE);fg = 24-bit RGB 前景色(主题 on_surface / on_surface_variant)。
|
||
* M2.46-2 视觉对齐(音量图标小一圈/下边距、图标与文字垂直中心):
|
||
* - 先求该位图 ink 边界(阈值 32,忽略近透明抗锯齿);按 ink 边界在槽内水平居中,并把 ink 垂直
|
||
* 中心对齐到全局 g_sb_icon_cy(==状态栏文字中心),→ 所有图标共用同一光学中线,不再「偏下」。
|
||
* - ink_h>0 时对 ink 做等比缩放(绘制时最近邻采样,源位图不变)到该 ink 高:主图标统一用
|
||
* SB_ICON_INK,音量(volume_up ink 较矮)随之放大到与电量/亮度一致 → 不显「小一圈」。
|
||
* - ink_h==0 保持位图自然 ink 尺寸(仅居中):用于 WiFi 强弱档——较短 ink(少一格)本身即
|
||
* 「信号弱」语义,不可被等比拉大。
|
||
* 像素 = 前景与背景按 alpha 软边混合(a<8 跳过透明;a>=240 直写前景;其余按 a>>4 在 565 空间
|
||
* 插值),与 blit_pm_icon 同构——暗/亮主题下图标随状态栏底色一起换色。 */
|
||
static void sb_blit_icon(int icon_idx, int x, uint32_t fg, int ink_h)
|
||
{
|
||
const unsigned char *src = sb_icons[icon_idx].data;
|
||
uint16_t col = rgb565(fg);
|
||
/* 1) ink 边界(含空图兜底) */
|
||
int minx = SB_ICON_SIZE, miny = SB_ICON_SIZE, maxx = -1, maxy = -1;
|
||
for (int i = 0; i < SB_ICON_SIZE * SB_ICON_SIZE; i++) {
|
||
if (src[i] >= 32) {
|
||
int xx = i % SB_ICON_SIZE, yy = i / SB_ICON_SIZE;
|
||
if (xx < minx) minx = xx; if (xx > maxx) maxx = xx;
|
||
if (yy < miny) miny = yy; if (yy > maxy) maxy = yy;
|
||
}
|
||
}
|
||
if (maxx < minx || maxy < miny) { minx = miny = 0; maxx = maxy = SB_ICON_SIZE - 1; }
|
||
int ink_w = maxx - minx + 1, ink_h_n = maxy - miny + 1;
|
||
int t_h = (ink_h > 0) ? ink_h : ink_h_n; /* 目标 ink 高 */
|
||
int t_w = ink_w * t_h / ink_h_n; if (t_w < 1) t_w = 1;
|
||
if (t_w > SB_ICON_SIZE * 2) t_w = SB_ICON_SIZE * 2; /* 防御:等比极端上限 */
|
||
/* 2) 目标框左上:水平居中于 32px 槽;垂直中心 = g_sb_icon_cy */
|
||
int x0 = x + (SB_ICON_SIZE - t_w) / 2;
|
||
int y0 = g_sb_icon_cy - t_h / 2;
|
||
/* 3) 最近邻采样映射:目标 (dx,dy) ← 源 ink (minx+dx*ink_w/t_w, miny+dy*ink_h_n/t_h) */
|
||
for (int dy = 0; dy < t_h; dy++) {
|
||
int ty = y0 + dy;
|
||
if (ty < 0 || ty >= fb_yres) continue;
|
||
int sy = miny + (dy * ink_h_n) / t_h;
|
||
uint16_t *row = (uint16_t *)fb + ty * (fb_line / 2);
|
||
for (int dx = 0; dx < t_w; dx++) {
|
||
int tx = x0 + dx;
|
||
if (tx < 0 || tx >= fb_xres) continue;
|
||
int sx = minx + (dx * ink_w) / t_w;
|
||
int a = src[sy * SB_ICON_SIZE + sx];
|
||
if (a < 8) continue; /* 透明像素跳过 */
|
||
uint16_t bg = row[tx];
|
||
if (a >= 240) { row[tx] = col; continue; }
|
||
int a4 = a >> 4; /* 0..15 */
|
||
int r = ((col >> 11) * a4 + ((bg >> 11) & 0x1F) * (16 - a4)) >> 4;
|
||
int g = (((col >> 5) & 0x3F) * a4 + ((bg >> 5) & 0x3F) * (16 - a4)) >> 4;
|
||
int b = ((col & 0x1F) * a4 + (bg & 0x1F) * (16 - a4)) >> 4;
|
||
row[tx] = (uint16_t)((r << 11) | (g << 5) | b);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 画一个右对齐状态栏项:右边界 = *xr,画完向左推进 *xr(含 SB_XGAP 项间隙)。
|
||
* icon>=0 画图标(可配右侧文字 txt;txt==NULL 则仅图标)。文字/图标同用主题色 col。
|
||
* ink_h 传给 sb_blit_icon(主图标用 SB_ICON_INK 等大;WiFi 强弱档传 0 保持自然 ink)。
|
||
* 返回该项左边缘(供 g_sb_right_x 包围盒)。 */
|
||
static int sb_r_place(int *xr, int icon, const char *txt, uint32_t col, int ink_h)
|
||
{
|
||
int iconw = (icon >= 0) ? SB_ICON_SIZE : 0;
|
||
int gap = (icon >= 0 && txt) ? SB_TGAP : 0;
|
||
int tw = txt ? ui_width(txt, SB_FONT) : 0;
|
||
int x = *xr - (iconw + gap + tw);
|
||
if (icon >= 0) { sb_blit_icon(icon, x, col, ink_h); x += iconw + gap; }
|
||
if (txt) ui_draw(x, sb_text_y(), txt, col, SB_FONT);
|
||
*xr -= (iconw + gap + tw) + SB_XGAP;
|
||
return *xr + SB_XGAP;
|
||
}
|
||
|
||
#define WIFI_LVL_UNKNOWN (-1000) /* wifi_signal_level 读取失败哨兵(低于任何真实 dBm) */
|
||
/* 读 /proc/net/wireless 的 wlan0 level(dBm,如 -46);失败(文件不存在/无 wlan0 行/解析失败)返回
|
||
* WIFI_LVL_UNKNOWN。行格式(Linux wext)形如 " wlan0: 0000 64. -46. -256 0 ...";
|
||
* 接口名后第 3 个空白分隔数值 = level(注意末尾带 '.',需剥离)。 */
|
||
static int wifi_signal_level(void)
|
||
{
|
||
FILE *f = fopen("/proc/net/wireless", "r");
|
||
if (!f) return WIFI_LVL_UNKNOWN;
|
||
char line[256];
|
||
int lvl = WIFI_LVL_UNKNOWN;
|
||
while (fgets(line, sizeof line, f)) {
|
||
char *p = strstr(line, "wlan0:");
|
||
if (!p) continue;
|
||
p += 6; /* 跳过 "wlan0:" */
|
||
int tok = 0;
|
||
while (*p) {
|
||
while (*p == ' ' || *p == '\t') p++;
|
||
if (!*p) break;
|
||
char *t = p;
|
||
while (*p && *p != ' ' && *p != '\t' && *p != '\n') p++;
|
||
char save = *p; *p = 0; /* 临时截断 token */
|
||
if (tok == 2) { /* 第 3 个数值(0 起) = level */
|
||
size_t n = strlen(t);
|
||
if (n && t[n - 1] == '.') t[n - 1] = 0; /* 去尾部 '.'(-46. → -46) */
|
||
lvl = atoi(t);
|
||
*p = save;
|
||
break;
|
||
}
|
||
*p = save;
|
||
tok++;
|
||
}
|
||
break; /* 只需 wlan0 行 */
|
||
}
|
||
fclose(f);
|
||
return lvl;
|
||
}
|
||
|
||
/* 整条状态栏:清底→重绘时钟+右侧信息→整条标脏。调用点:时钟每秒 / 电量·WiFi·音量事件。 */
|
||
static void sb_redraw(void)
|
||
{
|
||
const meow_ui_theme_t *th = s_ui_theme(); /* M2.44 状态栏接主题(浅色主题浅底深字,不再固定深色) */
|
||
uint32_t sb_bg = th->surface, sb_fg = th->on_surface;
|
||
fill_rect(0, SB_BAR_Y, fb_xres, SB_BAR_H, sb_bg); /* 整条清底→无旧内容残留 */
|
||
char timebuf[16];
|
||
time_t now = time(NULL);
|
||
struct tm tm;
|
||
localtime_r(&now, &tm);
|
||
snprintf(timebuf, sizeof timebuf, "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec);
|
||
ui_draw(8, sb_text_y(), timebuf, sb_fg, SB_FONT);
|
||
/* 图标 ink 垂直中心 = 状态栏文字中心(SB_ICON_CY=14,28px 栏文字中心):同中线。 */
|
||
g_sb_icon_cy = SB_ICON_CY;
|
||
/* 右簇 MD3 图标化(从右端 fb_xres-8 往左):[电量图标+%] [音量图标+%] [蓝牙图标] [WiFi图标] [亮度图标+%]。
|
||
* M2.46-2:音量/电量顺序互换(最右由音量改为电量);亮度由 on_surface_variant 改为主色 on_surface;
|
||
* WiFi 按信号强度分档(/proc/net/wireless level:≥-60 强 / -60..-75 中 / <-75 弱)。
|
||
* 主项色 on_surface;次项(蓝牙)用 on_surface_variant 稍淡。亮度「可选」:仅在右簇左缘
|
||
* 与时钟右缘相距足够(SB_CLK_MARGIN)时才画,否则整体让位,避免与时钟叠字。 */
|
||
uint32_t sb_fg2 = th->on_surface_variant;
|
||
char vol[16], batt[16], bright[16];
|
||
int xr = fb_xres - 8;
|
||
if (g_sys.batt >= 0) { /* 电量:图标+%(未知时不画)——最右项 */
|
||
snprintf(batt, sizeof batt, "%d%%", g_sys.batt);
|
||
sb_r_place(&xr, SB_I_BATT, batt, sb_fg, SB_ICON_INK);
|
||
}
|
||
snprintf(vol, sizeof vol, "%d%%", g_sys.vol);
|
||
sb_r_place(&xr, SB_I_VOL, vol, sb_fg, SB_ICON_INK); /* 音量:图标+% */
|
||
/* M2.48 蓝牙多态:关(bt_on=0 不画,见上行门);开未连接→SB_I_BT_NO;主模式/从模式→SB_I_BT_CONN。
|
||
* 主/从区分用颜色:主模式走次色 sb_fg2(与外设相连),从模式(被连当耳机)走主色 sb_fg 稍醒目。 */
|
||
if (g_sys.bt_seen && g_sys.bt_on) {
|
||
int bco = SB_I_BT_NO; /* 默认:开未连接 */
|
||
uint32_t bcol = sb_fg2;
|
||
if (g_sys.bt_role == BT_ROLE_MASTER) { bco = SB_I_BT_CONN; bcol = sb_fg2; }
|
||
else if (g_sys.bt_role == BT_ROLE_SLAVE) { bco = SB_I_BT_CONN; bcol = sb_fg; }
|
||
sb_r_place(&xr, bco, NULL, bcol, SB_ICON_INK);
|
||
}
|
||
/* M2.48 WiFi 多态:网卡关(wifi_on=0 不画);开未连接→SB_I_WIFI_NO;已连接→按信号分档。 */
|
||
if (g_sys.wifi_on) { /* WiFi:仅图标(按连接态+信号分档) */
|
||
int wco = SB_I_WIFI_NO; /* 默认:网卡开但未连 */
|
||
if (g_sys.wifi_connected) {
|
||
wco = SB_I_WIFI_STRONG; /* 已连接;读取失败/未知 → 满格 */
|
||
int wl = wifi_signal_level();
|
||
if (wl != WIFI_LVL_UNKNOWN) {
|
||
if (wl >= -60) wco = SB_I_WIFI_STRONG;
|
||
else if (wl >= -75) wco = SB_I_WIFI_MEDIUM;
|
||
else wco = SB_I_WIFI_WEAK;
|
||
}
|
||
}
|
||
/* 强弱档(MEDIUM/WEAK)保留自然 ink(少一格本身即语义);SRONG/满格/开未连接走等大。 */
|
||
sb_r_place(&xr, wco, NULL, sb_fg,
|
||
(wco == SB_I_WIFI_MEDIUM || wco == SB_I_WIFI_WEAK) ? 0 : SB_ICON_INK);
|
||
}
|
||
if (g_sys.brightness > 0) {
|
||
int bl = (g_sys.brightness > 100) ? 100 : g_sys.brightness;
|
||
snprintf(bright, sizeof bright, "%d%%", bl);
|
||
int bw = SB_ICON_SIZE + SB_TGAP + ui_width(bright, SB_FONT) + SB_XGAP;
|
||
if (xr - bw > 8 + ui_width(timebuf, SB_FONT) + SB_CLK_MARGIN)
|
||
sb_r_place(&xr, SB_I_BRIGHT, bright, sb_fg, SB_ICON_INK); /* 亮度:图标+%(主色,与时钟不重叠才画) */
|
||
}
|
||
g_sb_clock_x = 4; g_sb_clock_y = sb_text_y();
|
||
g_sb_clock_w = 12 + ui_width(timebuf, SB_FONT); g_sb_clock_h = SB_BAR_H;
|
||
g_sb_right_x = xr + SB_XGAP;
|
||
g_sb_right_y = sb_text_y(); g_sb_right_w = (fb_xres - 8) - g_sb_right_x + 4;
|
||
g_sb_right_h = SB_BAR_H;
|
||
meow_dirty_add(0, SB_BAR_Y, fb_xres, SB_BAR_H); /* 整条标脏 */
|
||
}
|
||
|
||
/* 只重画状态栏整条到 SDK back_buf 并标整条脏 —— 每秒(时钟)/ 事件(电量/WiFi/音量)触发,绝不动列表。
|
||
* (取代 M2.16 的「只标当前内容包围盒」路径——它造成残留。) */
|
||
static void paint_sb_clock(void) { sb_redraw(); }
|
||
static void paint_sb_right(void) { sb_redraw(); }
|
||
|
||
static void draw_statusbar(void)
|
||
{
|
||
sb_redraw();
|
||
const meow_ui_theme_t *th = s_ui_theme(); /* M2.44 分隔线随主题 outline */
|
||
fill_rect(0, SB_BAR_H, fb_xres, 1, th->outline); /* separator(整条之外,单独绘制;栏底=28) */
|
||
}
|
||
|
||
/* returns 0 on success (app ran and exited), -1 on launch failure */
|
||
static int launch_app(struct app *a);
|
||
|
||
/* M2.36 B2b:实体系统设置页(定义在下方 write_brightness 之后;run_menu 触摸命中
|
||
* 「系统设置」行时调用,返回后由调用方重置按键防抖基线防面板误切换)。 */
|
||
static void run_settings_page(void);
|
||
|
||
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/<id>/app.json", 0x9FB8D9, 20); }
|
||
else { draw_text(30, 200, "NO APPS FOUND", 2, 0xFFFFFF); draw_text(30, 240, "PUT APP IN", 2, 0x9FB8D9); draw_text(30, 272, "MeowAPPs/<id>/app.json", 2, 0x9FB8D9); }
|
||
} else {
|
||
for (int i = 0; i < napps; i++) {
|
||
int sel = (i == cursor);
|
||
if (sel) {
|
||
fill_rect(8, y - 4, fb_xres - 16, ROW_H - 8, 0x2A3A6A);
|
||
snprintf(line, sizeof line, "%d %s", i + 1, apps[i].name);
|
||
ui_draw(24, y, line, 0xFFFFFF, 20);
|
||
if (!apps[i].ok) ui_draw(24, y + ROW_H - 24, "[缺失]", 0xFF6B6B, 20);
|
||
} else {
|
||
snprintf(line, sizeof line, "%d %s", i + 1, apps[i].name);
|
||
ui_draw(24, y, line, 0xC8D0E8, 20);
|
||
if (!apps[i].ok) ui_draw(24, y + ROW_H - 24, "[缺失]", 0x8A4A4A, 20);
|
||
}
|
||
y += ROW_H;
|
||
}
|
||
}
|
||
|
||
/* M2.36 B2b:系统设置 builtin 行(过渡入口)。位于应用项之后、Exit 行之前;
|
||
* 不依赖 .mpk(图标/文案就地绘制)。音量导航可把光标移到此处(cursor==napps),
|
||
* 触摸点击该行进入设置页(run_menu 分发到 run_settings_page)。 */
|
||
{
|
||
int sy = EXIT_ROW - ROW_H; /* 固定在 Exit 行上方 588 */
|
||
int selset = (cursor == napps); /* 光标已移到设置行 */
|
||
fill_rect(8, sy - 4, fb_xres - 16, ROW_H - 8, selset ? 0x2A3A6A : 0x12142A);
|
||
if (mfont_ready) ui_draw(24, sy, "系统设置", selset ? 0xFFFFFF : 0xC8D0E8, 20);
|
||
else draw_text(24, sy, "SETTINGS", 2, selset ? 0xFFFFFF : 0xC8D0E8);
|
||
s_draw_chevron(fb_xres - 22, sy + ROW_H / 2, 0x9FB8D9); /* 右向进入提示 */
|
||
}
|
||
|
||
/* exit row */
|
||
fill_rect(8, EXIT_ROW - 4, fb_xres - 16, ROW_H - 8, 0x1A1A3A);
|
||
/* M2.2/M2.6:MeowOS 常驻主界面(播放器已死)时改「重启返回播放器」。 */
|
||
if (mfont_ready) ui_draw(24, EXIT_ROW, meow_mode ? "重启返回播放器" : "返回播放器", 0x8FBFD9, 20);
|
||
else draw_text(24, EXIT_ROW, meow_mode ? "REBOOT TO PLAYER" : "BACK TO PLAYER", 2, 0x8FBFD9);
|
||
|
||
/* M2.18 Bug 3: 底部对齐,弃用硬编码 y=700。屏幕高 fb_yres=720,字号 20 真实行高
|
||
* meow_font_line_height(20)=27(ascent 21 + descent 6);原 y=700 使字底 = 700+27 = 727
|
||
* > 720,溢出 7px ≈ 1/5 字符高被裁。改为 y = fb_yres - 行高 - 底部留白。非 mfont 回退
|
||
* 分支(5x7 点阵,字高=7*scale)同样底部对齐,两分支都处理。 */
|
||
#define FOOTY_PAD 8
|
||
/* dev version, bottom-right: single source MEOWOS_VERSION. */
|
||
{
|
||
char ver[32];
|
||
snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version);
|
||
int foot_y = fb_yres - meow_font_line_height(20) - FOOTY_PAD;
|
||
ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, 0x666688, 20);
|
||
}
|
||
|
||
if (toast_ms > 0 && toast && toast[0]) {
|
||
fill_rect(20, TOAST_Y - 6, fb_xres - 40, 40, 0x2A0A0A);
|
||
ui_draw(30, TOAST_Y, toast, 0xFF8080, 20);
|
||
}
|
||
}
|
||
|
||
/* ================================================================== */
|
||
/* M2.3A 电源菜单(Android 11 样式):长按电源 3s 弹出;8s 强制关机 */
|
||
/* 半透明深色遮罩 + 居中 5 个大按钮(2 列×3 行,ASCII 点阵,不耦合字体) */
|
||
/* 与「单击=息屏」「电源+音量减=切MeowOS」按电源按住时长区分。 */
|
||
/* ================================================================== */
|
||
/* M2.3A sim 钩子(电源菜单验证,物理留用户):
|
||
* --sim-power-hold N —— 主循环 N ms 后强制 pwr=1(模拟按住电源),驱动
|
||
* 3s 弹电源菜单 / 8s 强制关机两条路径。
|
||
* --sim-power-pick N —— 电源菜单打开 2s 后自动选中按钮 N(0..4;M2.29 起 5 钮)。
|
||
* --sim-no-exec —— 免执行:电源动作(关机/重启/回HibyOS)/8s强关仅打 log,
|
||
* 不真正 poweroff/reboot(安全 sim 用)。 */
|
||
static long sim_power_hold_ms = 0;
|
||
static int sim_power_pick = -1;
|
||
static int sim_no_exec = 0;
|
||
|
||
#define PM_NUM 5
|
||
/* M2.31 电源菜单 Android12(MD3) 圆钮:正中间圆角卡片 + 每行 2 个大圆钮 + 换行居中。
|
||
* 5 钮 → 2 列×3 行(行3=取消,单钮居中,默认选中安全项)。
|
||
* PM_R 半径50(直径100),行距 PM_GAPY=140、圆心距 PM_GAPX=130 保证圆下 label 不被下一行圆压住;
|
||
* 卡片 40..440 / 115..620 居中放置。 */
|
||
#define PM_R 50 /* 圆钮半径(直径 100) */
|
||
#define PM_GAPX 130 /* 两列圆心 x 间距(175->305,居中 240) */
|
||
#define PM_GAPY 140 /* 相邻行圆心 y 间距 */
|
||
#define PM_CARD_X0 40 /* 圆角卡片左 */
|
||
#define PM_CARD_W 400 /* 圆角卡片宽(40..440,居中) */
|
||
#define PM_CARD_CX (PM_CARD_X0 + PM_CARD_W / 2) /* 卡片水平中心 240 */
|
||
#define PM_CARD_Y0 115 /* 圆角卡片顶 */
|
||
#define PM_CARD_H 505 /* 圆角卡片高(115..620) */
|
||
#define PM_CARD_R 26 /* 圆角卡片圆角 */
|
||
static const int pm_cx[PM_NUM] = { PM_CARD_CX - PM_GAPX / 2, PM_CARD_CX + PM_GAPX / 2,
|
||
PM_CARD_CX - PM_GAPX / 2, PM_CARD_CX + PM_GAPX / 2, PM_CARD_CX };
|
||
static const int pm_cy[PM_NUM] = { 250, 250, 390, 390, 530 }; /* 圆心 y(行距 140) */
|
||
static const char *pm_labels_cn[PM_NUM] = { "关机", "重启", "回HiBy", "升级", "取消" };
|
||
static const char *pm_labels_en[PM_NUM] = { "POWER OFF", "RESTART", "HIBYOS", "UPGRADE", "CANCEL" };
|
||
|
||
static void fill_circle(int cx, int cy, int r, uint32_t color)
|
||
{
|
||
for (int dy = -r; dy <= r; dy++)
|
||
for (int dx = -r; dx <= r; dx++)
|
||
if (dx * dx + dy * dy <= r * r)
|
||
put_px(cx + dx, cy + dy, color);
|
||
}
|
||
|
||
/* 圆角矩形 */
|
||
static void fill_round_rect(int x0, int y0, int w, int h, int r, uint32_t color)
|
||
{
|
||
fill_rect(x0 + r, y0, w - 2 * r, h, color);
|
||
fill_rect(x0, y0 + r, w, h - 2 * r, color);
|
||
fill_circle(x0 + r, y0 + r, r, color);
|
||
fill_circle(x0 + w - r, y0 + r, r, color);
|
||
fill_circle(x0 + r, y0 + h - r, r, color);
|
||
fill_circle(x0 + w - r, y0 + h - r, r, color);
|
||
}
|
||
|
||
/* 电源菜单圆钮图标:从 pm_icons.h 的 56×56 alpha 位图 blit 到 fb(以圆钮中心 cx,cy 为锚居中)。
|
||
* alpha 按 col(RGB565)映射:图标前景色随选中/未选中主题色变化(日夜间由 pm_theme_* 提供)。
|
||
* dst_px 为 fb16bpp 写入:新像素 = col 前景与背景(btn bg)按 alpha 混合。 */
|
||
static void blit_pm_icon(int idx, int cx, int cy, uint16_t col)
|
||
{
|
||
const unsigned char *src = pm_icons[idx].data;
|
||
int S = PM_ICON_SIZE;
|
||
int x0 = cx - S / 2, y0 = cy - S / 2;
|
||
for (int y = 0; y < S; y++) {
|
||
int yy = y0 + y;
|
||
if (yy < 0 || yy >= fb_yres) continue;
|
||
uint16_t *row = (uint16_t *)fb + yy * (fb_line / 2);
|
||
for (int x = 0; x < S; x++) {
|
||
int a = src[y * S + x];
|
||
if (a < 8) continue; /* 透明像素跳过 */
|
||
int xx = x0 + x;
|
||
if (xx < 0 || xx >= fb_xres) continue;
|
||
/* 前景 col 已含所需亮度;此处仅做软边 alpha 混合到背景,避免锯齿 */
|
||
uint16_t bg = row[xx];
|
||
if (a >= 240) { row[xx] = col; continue; }
|
||
int a4 = a >> 4; /* 0..15 */
|
||
int r = ((col >> 11) * a4 + ((bg >> 11) & 0x1F) * (16 - a4)) >> 4;
|
||
int g = (((col >> 5) & 0x3F) * a4 + ((bg >> 5) & 0x3F) * (16 - a4)) >> 4;
|
||
int b = ((col & 0x1F) * a4 + (bg & 0x1F) * (16 - a4)) >> 4;
|
||
row[xx] = (uint16_t)((r << 11) | (g << 5) | b);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 绘制电源菜单圆钮 i(MD3 圆形:外描边+内填充+圆内 Material 图标;选中高亮)。
|
||
* 圆内放 Material Symbols 位图,文字标签放圆下(Android 12 式:icon 上、label 下)。 */
|
||
static void draw_pm_button(int i, int selected)
|
||
{
|
||
int cx = pm_cx[i], cy = pm_cy[i];
|
||
/* 主题(日夜间由 pm_theme_* 决定;默认取 dark,后续接设置) */
|
||
const pm_theme_t *th = &pm_theme_dark;
|
||
/* 先铺一块比任何选中圈都大的卡片底色圆,盖掉上一态残留的选中描边(问题3:
|
||
* 焦点移走后 PM_R+2..PM_R+6 之间残留亮圈。以 0x1C2438 卡片色清底)。 */
|
||
fill_circle(cx, cy, PM_R + 10, 0x1C2438);
|
||
/* MD3 无边框高亮:选中=未选中底色稍亮填充(无描边圈),仅底色区分焦点;避免圆形描边残留。 */
|
||
if (selected)
|
||
fill_circle(cx, cy, PM_R, th->btn_sel_bg);
|
||
else
|
||
fill_circle(cx, cy, PM_R, th->btn_nor_bg);
|
||
/* 圆内 Material 图标(alpha 位图 + 主题前景色) */
|
||
blit_pm_icon(i, cx, cy, selected ? th->icon_sel : th->icon_nor);
|
||
uint32_t col = selected ? 0xFFFFFF : 0xC8D0E8;
|
||
/* 圆下文字标签(真字体/中文 或 点阵 ASCII 回退) */
|
||
int ly = cy + PM_R + 8;
|
||
if (mfont_ready) {
|
||
const char *lab = pm_labels_cn[i];
|
||
int lw = meow_font_width_text(lab, 18);
|
||
meow_font_drawtext((uint16_t *)fb, cx - lw / 2, ly, lab, rgb565(col), 18);
|
||
} else {
|
||
const char *lab = pm_labels_en[i];
|
||
int lw = (int)strlen(lab) * 5;
|
||
draw_text(cx - lw / 2, ly + 1, lab, 1, col);
|
||
}
|
||
}
|
||
|
||
/* 把当前显示 buffer(buffer0)逐像素压暗(半透明遮罩模拟) */
|
||
static void dim_screen(void)
|
||
{
|
||
for (int y = 0; y < fb_yres; y++) {
|
||
unsigned char *row = fb + (size_t)y * fb_line;
|
||
for (int x = 0; x < fb_xres; x++) {
|
||
unsigned char *p = row + x * (fb_bpp / 8);
|
||
if (fb_bpp == 16) {
|
||
uint16_t c = *(uint16_t *)p;
|
||
int r = (((c >> 11) & 0x1F) * 3) / 5;
|
||
int g = (((c >> 5) & 0x3F) * 3) / 5;
|
||
int b = ((c & 0x1F) * 3) / 5;
|
||
*(uint16_t *)p = (uint16_t)((r << 11) | (g << 5) | b);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* A-0:电源菜单背景 = app 当前帧。从 fb0 buffer0(yoffset=0,app 永不 pan)读回 app
|
||
* SIGSTOP 后的最后一帧 → back_buf,再经 dim_screen 压暗作菜单背景,替代旧的「压暗
|
||
* launcher 应用列表底」(Bug:应用内长按电源,背景错显示成应用列表)。
|
||
* 参考 fb_backup_player 的读回思路,但目标是 app 帧而非播放器帧(播放器帧在其自身
|
||
* yoffset,app 帧固定 buffer0)。MeowOS 主界面(无 app 运行)时 buffer0 = launcher
|
||
* 列表帧,读回即现有「压暗列表底」效果,无回归。 */
|
||
static void pm_capture_app_frame(void)
|
||
{
|
||
if (!fb_mem || !back_buf_ok || !fb) return; /* 无真实 fb / 直写模式:维持现状 */
|
||
memcpy(fb, fb_mem, (size_t)fb_line * (size_t)fb_yres); /* fb = SDK back_buf */
|
||
meow_dirty_full(); /* 新背景整帧呈现 */
|
||
logmsg("pm: menu bg = app frame (fb0 buffer0)");
|
||
}
|
||
|
||
/* Bug 2 修复:直连 reboot(2) syscall。M2.6 用 system("... reboot"/"poweroff") 经 BusyBox
|
||
* 不生效(设备实测:动作日志打了但不起机/不关机——BusyBox reboot 非 PID1 时依赖 init 上的
|
||
* 信号/teinit 握手,本设备不完成;且 system() fork+sh 有竞态)。改为直接 syscall 绕过
|
||
* /bin/sh 与 init:sync() 后调用 reboot(2),内核立即重启/关机。 */
|
||
#define LINUX_REBOOT_MAGIC1 0xfee1dead
|
||
#define LINUX_REBOOT_MAGIC2 672274793 /* 0x28121969 */
|
||
#define LINUX_REBOOT_CMD_RESTART 0x01234567
|
||
#define LINUX_REBOOT_CMD_POWER_OFF 0x4321fedc
|
||
static void sys_do_reboot(int cmd)
|
||
{
|
||
/* M2a:正常电源事件(关机/重启)触发清除板载通知数据之前,先转储所有未上卡通知
|
||
* (NOTIFICATION_SYSTEM.md §8)。meow_notif_before_poweroff 幂等安全(未 init 直接返回)。 */
|
||
meow_notif_before_poweroff();
|
||
sync(); /* 先落盘,避免数据丢失 */
|
||
logmsg("sys_do_reboot cmd=0x%x", cmd);
|
||
syscall(SYS_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, cmd, NULL);
|
||
logmsg("sys_do_reboot returned: %s", strerror(errno)); /* 正常不会返回 */
|
||
for (;;) usleep(1000); /* 防御:永不返回 */
|
||
}
|
||
|
||
/* 回HibyOS:写板载 /data/meow_boot_default=1(开机默认权威标记,O_TRUNC 单值、不写 TF 卡),
|
||
* 再 reboot 回选单并默认 HibyOS。meow.conf 不再写/承载 boot_default(bootmenu 以板载标记为准)。 */
|
||
static void set_boot_default_hiby(void)
|
||
{
|
||
int fd = open("/data/meow_boot_default", O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||
if (fd >= 0) { write(fd, "1\n", 2); close(fd); logmsg("pm: /data/meow_boot_default=1"); }
|
||
}
|
||
|
||
/* 电源菜单(M2.6):2×2 网格,dirty 绘制消频闪;输入修复:
|
||
* - 触摸按下捕获坐标、抬起命中按钮即确认(同 run_menu MT 防抖);
|
||
* - 音量上/下导航;播放=确认;电源新按(松开长按后)=确认当前钮(非取消);
|
||
* - 取消=选「取消」钮;8s 持续按住=强制关机(绕过)。
|
||
* 返回 1=采取动作,0=取消。 */
|
||
static int show_power_menu(void)
|
||
{
|
||
int fbfd = open(FB_DEV, O_RDWR);
|
||
if (fbfd >= 0) { ioctl(fbfd, FBIOBLANK, 0); close(fbfd); } /* 亮起面板 */
|
||
pm_capture_app_frame(); /* A-0:菜单背景 = app 当前帧(fb0 buffer0),再压暗绘制其上 */
|
||
dim_screen();
|
||
/* M2.8 Bug 3-residual:若电源菜单是在「息屏」态打开(按住 3s),此处 FBIOBLANK(0)
|
||
* 刚上电,LCDC 扫描地址可能非 buffer0;强制 re-pan 让压暗的应用列表 + 菜单落在被显示的
|
||
* 缓冲,否则菜单画在非显示缓冲上不可见。 */
|
||
fb_pan_buf0_force();
|
||
meow_screen_on = 1; /* 菜单打开即视为亮屏 */
|
||
|
||
/* M2.11 B:默认光标 = 「取消」(安全项,index 4)。用户打开菜单后未导航就按电源/播放,
|
||
* 只会取消关菜单,绝不因「按电源键」本身关机/重启。要真关机必须先导航到「关机」再确认。 */
|
||
int sel = PM_NUM - 1; /* 默认取消(安全) */
|
||
/* M2.30 MD3:居中圆角卡片背景 + 标题居中 + 圆钮网格。卡片白色柔和描边/深色底。 */
|
||
fill_round_rect(PM_CARD_X0, PM_CARD_Y0, PM_CARD_W, PM_CARD_H, PM_CARD_R, 0x141824);
|
||
fill_round_rect(PM_CARD_X0 + 2, PM_CARD_Y0 + 2, PM_CARD_W - 4, PM_CARD_H - 4, PM_CARD_R - 2, 0x1C2438);
|
||
/* 卡片顶部标题,水平居中 */
|
||
char title[32];
|
||
snprintf(title, sizeof title, "POWER MENU");
|
||
int title_y = PM_CARD_Y0 + 26; /* 标题在卡片内顶部(卡片顶 115,标题基线 ~141) */
|
||
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, (fb_xres - meow_font_width_text(title, 20)) / 2, title_y, title, rgb565(0xFFFFFF), 20);
|
||
else draw_text((fb_xres - (int)strlen(title) * 6) / 2, title_y, title, 2, 0xFFFFFF);
|
||
/* 底部提示行(卡片下方,非圆钮所需) */
|
||
int hint_y = PM_CARD_Y0 + PM_CARD_H + 10;
|
||
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, (fb_xres - meow_font_width_text("音量导航 电源确认", 18)) / 2, hint_y, "音量导航 电源确认", rgb565(0x777799), 18);
|
||
else draw_text((fb_xres - (int)strlen("VOLUME NAV POWER OK") * 5) / 2, hint_y + 1, "VOLUME NAV POWER OK", 1, 0x777799);
|
||
meow_dirty_full(); /* M2.16:把压暗列表+卡片+标题+(下方圆钮)作为一整帧呈现 */
|
||
|
||
/* 触摸状态(按下→抬起):统一 meow_touch 接口(goodix 时序免疫,原点由首个有效 ABS 锁) */
|
||
meow_touch mt;
|
||
mt_reset(&mt);
|
||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||
|
||
struct keypoll kp_power, kp_keys;
|
||
kp_open(&kp_power, DEV_POWER);
|
||
kp_open(&kp_keys, DEV_KEYS);
|
||
int prev_vold = 0, prev_volu = 0, prev_play = 0, prev_pwr = 0;
|
||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||
prev_pwr = kp_is_down(&kp_power, KEY_POWER) ? 1 : 0;
|
||
prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN) ? 1 : 0;
|
||
prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP) ? 1 : 0;
|
||
prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE) ? 1 : 0;
|
||
|
||
int act = -1;
|
||
unsigned long long pm_t0 = now_ms();
|
||
unsigned long long pm_last_interact = pm_t0; /* M2.33:菜单空闲自动关闭基准(任何互动刷新) */
|
||
int pm_init = 0, last_sel = -1;
|
||
/* M2.10 Bug:电源菜单 8s 强关(按住电源≥8s=绕过强关)会覆盖用户的菜单操作 —— 用户按住
|
||
* 电源打开菜单后(尤其仍按着电源)点选「回到HibyOS/重启」等按钮,8s 一到先触发强关 → 关机
|
||
* 而非执行所选动作。修法:一旦用户与菜单互动(音量导航/触摸/播放/电源确认),置 pm_interacted=1
|
||
* 取消 8s 强关,让所选动作(如「回到HibyOS」→ reboot RESTART)生效。仅当全程未互动才保留「按住
|
||
* 8s=强关」。*/
|
||
int pm_interacted = 0;
|
||
for (;;) {
|
||
/* dirty 绘制:首次全画;此后仅选中变化才重画受影响两钮(消频闪)→ 标脏 + present。 */
|
||
if (!pm_init) {
|
||
for (int i = 0; i < PM_NUM; i++) draw_pm_button(i, i == sel);
|
||
pm_init = 1; last_sel = sel;
|
||
meow_ui_flush(); /* A-1/A-2:SDK present(vsync+帧率门控+blit 脏=全帧,含压暗列表+标题+按钮) */
|
||
} else if (sel != last_sel) {
|
||
draw_pm_button(last_sel, 0);
|
||
draw_pm_button(sel, 1);
|
||
/* M2.30 圆形:dirty 区 = 圆(圆心±半径)+ 圆下文字(半径+偏移+行高),四周留 10px */
|
||
int dr = (PM_R + 6) + 10, dtxt = PM_R + 26 + 10;
|
||
meow_dirty_add(pm_cx[last_sel] - dr, pm_cy[last_sel] - dr, dr * 2, dr + dtxt);
|
||
meow_dirty_add(pm_cx[sel] - dr, pm_cy[sel] - dr, dr * 2, dr + dtxt);
|
||
last_sel = sel;
|
||
meow_ui_flush(); /* A-1/A-2:SDK present,blit 两钮区 */
|
||
}
|
||
|
||
if (sim_power_pick >= 0 && now_ms() - pm_t0 >= 2000) act = sim_power_pick;
|
||
|
||
int vold = 0, volu = 0, play = 0, pwr = 0;
|
||
if (kp_keys.ok) { kp_poll(&kp_keys, NULL, NULL);
|
||
vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||
volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||
play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); }
|
||
if (kp_power.ok) { kp_poll(&kp_power, NULL, NULL); pwr = kp_is_down(&kp_power, KEY_POWER); }
|
||
|
||
if (vold && !prev_vold) { sel = (sel + 1) % PM_NUM; pm_interacted = 1; pm_last_interact = now_ms(); }
|
||
if (volu && !prev_volu) { sel = (sel - 1 + PM_NUM) % PM_NUM; pm_interacted = 1; pm_last_interact = now_ms(); }
|
||
/* M2.19:播放键不再作确认(让位给全局媒体控制)。电源菜单的物理确认
|
||
* 已由电源键负责(下行),播放键删除,重复确认移除。 */
|
||
(void)play;
|
||
if (pwr && !prev_pwr) { act = sel; pm_interacted = 1; pm_last_interact = now_ms(); } /* 电源新按=确认(非取消) */
|
||
prev_vold = vold; prev_volu = volu; prev_play = play; prev_pwr = pwr;
|
||
|
||
/* 触摸:按下(meow_touch 原点锁定,goodix 时序免疫),抬起按按钮命中确认 */
|
||
if (tfd >= 0) {
|
||
struct pollfd pfd = { tfd, POLLIN, 0 };
|
||
if (poll(&pfd, 1, 0) > 0) {
|
||
unsigned char ev[16];
|
||
while (read(tfd, ev, sizeof ev) == (ssize_t)sizeof ev) {
|
||
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
|
||
int32_t val = *(int32_t *)(ev + 12);
|
||
if (type == EV_ABS) {
|
||
if (code == ABS_X || code == ABS_MT_POSITION_X) mt_abs(&mt, val, mt.last_y);
|
||
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) mt_abs(&mt, mt.last_x, val);
|
||
} else if (type == EV_KEY && code == BTN_TOUCH) {
|
||
if (val && !mt.down) {
|
||
mt_down(&mt);
|
||
pm_interacted = 1;
|
||
pm_last_interact = now_ms();
|
||
} else if (!val && mt.down) {
|
||
pm_interacted = 1;
|
||
pm_last_interact = now_ms();
|
||
int dx, dy;
|
||
/* M2.25:抬手位移 >= MEOW_TOUCH_SWIPE_THRESH 判滑动(返回0),
|
||
* 不做确认;否则有效 tap,按下原点命中的按钮即确认。 */
|
||
if (mt_up(&mt, &dx, &dy)) {
|
||
/* M2.30 圆形命中:按下原点距圆心 <= PM_R+6 即命中 */
|
||
for (int i = 0; i < PM_NUM; i++) {
|
||
int ddx = mt.down_x - pm_cx[i], ddy = mt.down_y - pm_cy[i];
|
||
int rr = (PM_R + 6) * (PM_R + 6);
|
||
if (ddx * ddx + ddy * ddy <= rr) { sel = i; act = i; break; }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 8s 强关(绕过):仅当电源持续按住、且用户全程未与菜单互动才触发 —— 放在触摸/按键处理
|
||
* 之后,避免「用户已点选按钮」的同一轮被强关抢先(M2.10 Bug)。 */
|
||
if (pwr && !pm_interacted && now_ms() - pm_t0 >= 5000) {
|
||
if (sim_no_exec) { logmsg("pm: [sim-no-exec] POWER HOLD 8s -> would force poweroff"); }
|
||
else { logmsg("pm: POWER HOLD 8s -> force poweroff"); sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); }
|
||
return 1;
|
||
}
|
||
|
||
/* M2.33 菜单空闲超时自动关闭:菜单受 MeowOS screen_off_timeout 管理(系统界面不豁免)。
|
||
* 超过超时无任何互动 → 关闭菜单(视为取消)并自动息屏。目的是:息屏后单击电源亮屏时,
|
||
* 电源按下信号不会命中菜单按钮(菜单已关),杜绝误触关机/执行菜单动作。 */
|
||
if (screen_off_timeout > 0 &&
|
||
(unsigned long long)(now_ms() - pm_last_interact) >= (unsigned long long)screen_off_timeout * 1000) {
|
||
logmsg("pm: idle %ds -> close menu + auto screen-off (act=cancel)", screen_off_timeout);
|
||
meow_panel_off();
|
||
meow_auto_slept = 1;
|
||
act = 4; /* 取消(关菜单) */
|
||
break;
|
||
}
|
||
|
||
if (act >= 0) break;
|
||
usleep(POLL_MS * 1000);
|
||
}
|
||
if (tfd >= 0) close(tfd);
|
||
|
||
/* M2.11 B:电源键在菜单 = 按当前项确认。act 与 syscall 类型逐项核对:
|
||
* act==4(取消) -> 只关菜单,绝不关机(return 0,永不到 reboot);
|
||
* act==0(关机) -> 才 poweroff;act==1(重启)/act==2(回HibyOS)/act==3(升级模式) -> 才 reboot。
|
||
* 只有 act 为 0/1/2/3 才可能执行 sys_do_reboot;power 键本身绝不触发关机。 */
|
||
logmsg("pm: act=%d -> %s", act,
|
||
act == 0 ? "关机" : act == 1 ? "重启" : act == 2 ? "回HibyOS" : act == 3 ? "升级模式" : "取消");
|
||
if (act == 4) { logmsg("pm: cancel (close menu only)"); return 0; }
|
||
if (act == 0) {
|
||
logmsg("pm: POWER OFF");
|
||
if (!sim_no_exec) {
|
||
/* 先显关机 logo(cmd_jpeg_display 是独立二进制,能正常显图),再直连 poweroff */
|
||
system("PATH=/bin:/sbin:/usr/bin:/usr/sbin cmd_jpeg_display /etc/shutdown_logo.jpeg >/dev/null 2>&1");
|
||
sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF);
|
||
}
|
||
} else if (act == 1) {
|
||
logmsg("pm: REBOOT");
|
||
if (!sim_no_exec) sys_do_reboot(LINUX_REBOOT_CMD_RESTART);
|
||
} else if (act == 2) {
|
||
logmsg("pm: RETURN TO HIBYOS");
|
||
set_boot_default_hiby();
|
||
if (!sim_no_exec) sys_do_reboot(LINUX_REBOOT_CMD_RESTART);
|
||
} else if (act == 3) {
|
||
/* M2.29 升级模式:写 mtd5=ota:kernel2(bootmode.sh Recovery = flash_erase + nandwrite)
|
||
* 再 reboot —— uboot 读 mtd5=ota:kernel2 引导 kernel2(升级模式,自动刷 TF 卡
|
||
* r3proii.upt,双进度条,跑完重启进新固件)。已验证此链路可靠。 */
|
||
logmsg("pm: ENTER UPGRADE");
|
||
if (!sim_no_exec) {
|
||
system("PATH=/bin:/sbin:/usr/bin:/usr/sbin /usr/bin/bootmode.sh Recovery >/dev/null 2>&1");
|
||
sys_do_reboot(LINUX_REBOOT_CMD_RESTART);
|
||
}
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
/* M2.6 G「Reboot to player」:MeowOS 常驻模式点「返回播放器」时,播放器已死,弹确认框
|
||
* 「返回播放器需重启,现在重启?[是/否]」。是=写选单默认 HibyOS + reboot;否=留在 MeowOS。
|
||
* VOL=是/否 切换;PLAY=确认;POWER=取消;触摸点 是/否 确认。风格同电源菜单。
|
||
* 返回 1=是(已 reboot,通常不回),0=否/取消(调用方重绘应用列表)。 */
|
||
/* 「返回播放器」确认框「是/否」按钮(左右),选中高亮。
|
||
* M2.9 Bug 4:改为 dirty 绘制 —— 仅 sel 变化时重画两钮(旧钮取消高亮 + 新钮高亮),
|
||
* 不随主循环每 10ms 全刷(原实现每帧重画同一内容,与 LCD 刷新竞争产生频闪)。 */
|
||
static void draw_cr_button(int sel, int i)
|
||
{
|
||
int x0 = (i == 0) ? 80 : 280, y0 = 420, w = 120, h = 60;
|
||
fill_round_rect(x0, y0, w, h, 12, (i == sel) ? 0x2A3A6A : 0x1A1A2A);
|
||
const char *lbl = (i == 0) ? "是" : "否";
|
||
if (mfont_ready) {
|
||
int lw = meow_font_width_text(lbl, 20);
|
||
meow_font_drawtext((uint16_t *)fb, x0 + w / 2 - lw / 2, y0 + 30 - meow_font_line_height(20) / 2,
|
||
lbl, rgb565((i == sel) ? 0xFFFFFF : 0xC8D0E8), 20);
|
||
} else {
|
||
draw_text(x0 + w / 2 - 6, y0 + 24, (i == 0) ? "YES" : "NO", 2, (i == sel) ? 0xFFFFFF : 0xC8D0E8);
|
||
}
|
||
}
|
||
|
||
static int confirm_reboot_player(void)
|
||
{
|
||
dim_screen();
|
||
/* M2.8 Bug 3-residual:确认框同电源菜单 —— 若打开前刚经历息屏/blank,LCDC 扫描地址可能
|
||
* 非 buffer0;强制 re-pan 让「压暗列表 + 是/否」落在被显示的缓冲。 */
|
||
fb_pan_buf0_force();
|
||
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 320, "返回播放器需重启,现在重启?", rgb565(0xFFFFFF), 20);
|
||
else draw_text(20, 322, "REBOOT TO RETURN TO PLAYER?", 2, 0xFFFFFF);
|
||
|
||
meow_touch mt;
|
||
mt_reset(&mt);
|
||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||
struct keypoll kp_power, kp_keys;
|
||
kp_open(&kp_power, DEV_POWER);
|
||
kp_open(&kp_keys, DEV_KEYS);
|
||
int prev_vold = 0, prev_volu = 0, prev_play = 0, prev_pwr = 0;
|
||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||
prev_pwr = kp_is_down(&kp_power, KEY_POWER) ? 1 : 0;
|
||
prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN) ? 1 : 0;
|
||
prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP) ? 1 : 0;
|
||
prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE) ? 1 : 0;
|
||
|
||
int sel = 1; /* 默认「否」:不误重启 */
|
||
int act = -1;
|
||
int cr_init = 0, cr_last_sel = -1; /* M2.9 Bug 4:dirty——仅 sel 变化才重画两钮 */
|
||
/* 标题/按键提示:打开时画一次(原实现把它们放主循环每 10ms 全刷,与 LCD 刷新竞争 →
|
||
* 「是/否」钮频闪;改为只在打开/状态变化时绘制)。标题在前面已画一次,这里画提示行。 */
|
||
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 560, "音量切换 电源取消", rgb565(0x666688), 20);
|
||
else draw_text(20, 562, "VOL NAV POWER EXIT", 1, 0x666688);
|
||
meow_dirty_full(); /* M2.16:压暗列表+标题+提示作为一整帧呈现 */
|
||
for (;;) {
|
||
/* dirty 绘制:首次全画两钮;此后仅 sel 变化才重画(消频闪)→ 标脏 + present。 */
|
||
if (!cr_init) {
|
||
for (int i = 0; i < 2; i++) draw_cr_button(sel, i);
|
||
cr_init = 1; cr_last_sel = sel;
|
||
meow_ui_flush();
|
||
} else if (sel != cr_last_sel) {
|
||
draw_cr_button(sel, cr_last_sel); /* 旧选中:取消高亮 */
|
||
draw_cr_button(sel, sel); /* 新选中:高亮 */
|
||
meow_dirty_add((cr_last_sel == 0 ? 80 : 280) - 10, 412, 140, 76);
|
||
meow_dirty_add((sel == 0 ? 80 : 280) - 10, 412, 140, 76);
|
||
cr_last_sel = sel;
|
||
meow_ui_flush();
|
||
}
|
||
|
||
int vold = 0, volu = 0, play = 0, pwr = 0;
|
||
if (kp_keys.ok) { kp_poll(&kp_keys, NULL, NULL);
|
||
vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||
play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); }
|
||
if (kp_power.ok) { kp_poll(&kp_power, NULL, NULL); pwr = kp_is_down(&kp_power, KEY_POWER); }
|
||
if (vold && !prev_vold) sel = (sel + 1) % 2;
|
||
if (volu && !prev_volu) sel = (sel - 1 + 2) % 2;
|
||
/* M2.19:播放键不再作「是/确认」(让位给全局媒体控制)。确认框的确认
|
||
* 走触摸点选(下方 tfd 命中),电源键=取消(下行);播放键删除。 */
|
||
(void)play;
|
||
if (pwr && !prev_pwr) act = 3; /* 电源=取消 */
|
||
prev_vold = vold; prev_volu = volu; prev_play = play; prev_pwr = pwr;
|
||
|
||
if (tfd >= 0) {
|
||
struct pollfd pfd = { tfd, POLLIN, 0 };
|
||
if (poll(&pfd, 1, 0) > 0) {
|
||
unsigned char ev[16];
|
||
while (read(tfd, ev, sizeof ev) == (ssize_t)sizeof ev) {
|
||
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
|
||
int32_t val = *(int32_t *)(ev + 12);
|
||
if (type == EV_ABS) {
|
||
if (code == ABS_X || code == ABS_MT_POSITION_X) mt_abs(&mt, val, mt.last_y);
|
||
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) mt_abs(&mt, mt.last_x, val);
|
||
} else if (type == EV_KEY && code == BTN_TOUCH) {
|
||
if (val && !mt.down) {
|
||
mt_down(&mt);
|
||
} else if (!val && mt.down) {
|
||
int dx, dy;
|
||
if (mt_up(&mt, &dx, &dy)) {
|
||
for (int i = 0; i < 2; i++) {
|
||
int x0 = (i == 0) ? 80 : 280;
|
||
/* 命中「是/否」钮:x∈[x0-8, x0+128], y∈[412, 488](含端点) */
|
||
if (mt_in(&mt, x0 - 8, 412, 137, 77))
|
||
{ sel = i; act = i; break; }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (act >= 0) break;
|
||
usleep(POLL_MS * 1000);
|
||
}
|
||
if (tfd >= 0) close(tfd);
|
||
if (act == 0) {
|
||
logmsg("reboot-to-player: YES -> default HibyOS + reboot");
|
||
set_boot_default_hiby();
|
||
if (!sim_no_exec) sys_do_reboot(LINUX_REBOOT_CMD_RESTART);
|
||
return 1;
|
||
}
|
||
logmsg("reboot-to-player: cancel");
|
||
return 0;
|
||
}
|
||
|
||
|
||
/* returns: 0 = stay in menu, 1 = exit menu (BACK TO PLAYER / sim-exit),
|
||
* 2 = launch request */
|
||
|
||
/* M2.8 Bug 3-residual「统一重绘」:把当前 MeowOS 界面内容整体重绘到 buffer0 并强制 pan 到
|
||
* buffer0。息屏唤醒(电源单击亮屏)时调用 —— 唤醒不是 draw 事件:FBIOBLANK(4) 下电 ->
|
||
* FBIOBLANK(0) 上电后,LCDC 指向的内容可能已失效/为空,必须把当前真实界面(应用列表主界面,
|
||
* 含「没有存储卡/没有找到应用」态)重新画一遍再强制 re-pan,而不是仅翻页到陈旧/空缓冲。
|
||
* signature 传入当前界面状态(由 run_menu 持有),不把应用列表局部状态全局化。 */
|
||
static void meow_repaint_surface(struct app apps[MAX_APPS], int napps, int state, int cursor)
|
||
{
|
||
/* M2.11 E:记录唤醒上下文(时刻/亮度/当前 screen_on/待 yoffset),便于定位偶发「唤醒后黑屏
|
||
* 又自愈」的竞态。 */
|
||
int pre_bri = read_brightness();
|
||
int pre_screen = meow_screen_on;
|
||
logmsg("wake repaint: pre bri=%d screen_on=%d (state=%d napps=%d cursor=%d)",
|
||
pre_bri, pre_screen, state, napps, cursor);
|
||
draw_menu(apps, napps, state, cursor, 0, ""); /* 完整重绘当前视图到 back_buf */
|
||
fb_pan_buf0_force(); /* 确保落在被显示的缓冲(buffer0) */
|
||
meow_dirty_full(); meow_ui_flush(); /* A-1/A-2:SDK present(vsync + 单次全帧 blit) */
|
||
int post_bri = read_brightness();
|
||
logmsg("wake repaint done: post bri=%d (after force-pan buffer0)", post_bri);
|
||
}
|
||
|
||
/* set by on_sigterm; run_menu breaks on it so the menu closes cleanly and
|
||
* the main loop restores the player (must be declared before run_menu) */
|
||
static volatile sig_atomic_t sigterm_flag = 0;
|
||
|
||
/* M2.2 sim 钩子:--sim-exit-tap N —— 菜单打开 N ms 后模拟「点 BACK TO PLAYER 行」,
|
||
* 用于验证 MeowOS 模式下 BACK TO PLAYER 显示需重启提示(而非恢复死帧)。 */
|
||
static long sim_exit_tap_ms = 0;
|
||
static int sim_exit_tap_done = 0;
|
||
|
||
/* M2.53:MeowOS 主屏接管时把亮度强制压回用户设置值(display.brightness,经
|
||
* meow_settings_brightness_clamped() —— 下限 brightness_min 默认 1、上限 100),
|
||
* 覆盖 HibyOS 播放器写入 sysfs 的背光下限(16/25)。仅应在「MeowOS 真正接管画面」
|
||
* (播放器已死、主屏常驻,即 run_menu 的 had_player==0)时调用;播放器会话期间的
|
||
* 覆盖菜单(had_player==1)不去抢亮度——尊重播放器背光(§红线)。 */
|
||
static void apply_user_brightness(void)
|
||
{
|
||
int v = meow_settings_brightness_clamped();
|
||
if (v <= 0) v = 60; /* 防御:clamp 后不可能 ≤0,仅兜底 */
|
||
write_brightness(v);
|
||
g_sys.brightness = v;
|
||
logmsg("apply user brightness (MeowOS takeover) -> %d", v);
|
||
}
|
||
|
||
/* real menu loop */
|
||
static void run_menu(struct app apps[MAX_APPS], int napps, int state,
|
||
long sim_tap_ms, long sim_exit_ms)
|
||
{
|
||
int cursor = 0;
|
||
unsigned long long t0 = now_ms();
|
||
int toast_ms = 0;
|
||
char toast[64] = "";
|
||
int want_exit = 0;
|
||
/* M2.17 超时自动息屏:任何输入重置 last_input;空闲 > screen_off_timeout → meow_panel_off()。
|
||
* meow_auto_slept 记录「自动息屏」态(手动电源息屏不置位),任意输入唤醒 + meow_repaint_surface。 */
|
||
unsigned long long last_input_ms = now_ms();
|
||
int any_input = 0;
|
||
int fbfd = -1;
|
||
int had_player = 0; /* M2.2:菜单打开时是否存在存活播放器 */
|
||
long sim_tap_done = 0, sim_exit_done = 0;
|
||
long meow_last_poll = 0; /* M2.2:MeowOS 后台拔营(卡/网)轮询时间戳 */
|
||
|
||
load_config(); /* meow.conf on the card: bg_color etc. */
|
||
|
||
/* A-1/A-2:launcher 用 SDK 渲染栈 —— 初始化 SDK(自建 back_buf/读几何/载字库),
|
||
* 并把刷新率档位/恒满速开关交给 SDK present 帧率门控(去中心化:launcher 不再自带一套)。
|
||
* set_statusbar(0):launcher 用自己的状态栏(draw_statusbar),关闭 SDK 全局状态栏避免
|
||
* 同区双画(SDK 全局状态栏/左缘返回对 APP 生效;launcher 系统界面暂保留自有状态栏)。 */
|
||
meow_ui_init(NULL);
|
||
meow_ui_set_statusbar(0);
|
||
meow_ui_set_refresh_rate(refresh_rate_cfg);
|
||
meow_ui_set_refresh_always_max(refresh_always_max);
|
||
|
||
/* record the brightness BEFORE we touch it, so any menu exit can
|
||
* restore the player's pre-menu value (0 = player standby -> default 60) */
|
||
int raw_brightness = read_brightness();
|
||
saved_brightness = (raw_brightness > 0) ? raw_brightness : 60;
|
||
logmsg("menu saved brightness = %d", saved_brightness);
|
||
/* remember whether the player had blanked the panel: fb_open() below
|
||
* unblanks so the menu is visible, and restore_backlight() re-blanks
|
||
* on exit to keep the player's display state machine consistent */
|
||
menu_opened_blank = (raw_brightness <= 0);
|
||
|
||
fbfd = fb_open();
|
||
meow_font_bind_fb(); /* M2.6 字库绑定当前 fb 几何(宽/高/pitch) */
|
||
/* M2.6 加载字库(卡上 MeowOS/fonts/default/,卡片已挂载则该步成功;未挂载由下方轮询补载) */
|
||
meow_font_ensure();
|
||
meow_font_reload_multifont(); /* M2.5x 多字体(读 settings display.multi_font + 6 类;关=单字体) */
|
||
/* M2.2:先判播放器是否存活。MeowOS 常驻主界面(播放器已死)→ 不 SIGSTOP、不备份
|
||
* 播放器帧,全程只自绘 fb0(buffer0 + pan0);仅当存在存活播放器(会话覆盖场景)才
|
||
* 冻结 + 备份,退出时恢复其帧。 */
|
||
had_player = (find_hiby_pid() > 0);
|
||
meow_mode = had_player ? 0 : 1; /* 权威判定:每次按播放器是否存活重置 */
|
||
/* M2.11 A:每次进 MeowOS(热键切换/开机选 MeowOS/覆盖菜单)都全量重抓系统状态快照,
|
||
* HibyOS 里改过(蓝牙/WiFi/音量/亮度等)也同步。主循环 10ms 绝不读系统态。 */
|
||
meow_sys_refresh();
|
||
g_sys.mode = meow_mode;
|
||
g_sys.napps = napps;
|
||
if (had_player) {
|
||
stop_player(); /* freeze the player so the menu owns fb0 */
|
||
} else {
|
||
logmsg("meow mode: hiby_player dead, self-drawn home (no stop/backup)");
|
||
}
|
||
ensure_backlight(); /* only if the panel is dark, never yank brightness */
|
||
unblank_panel(); /* player frozen: it can no longer re-blank */
|
||
/* M2.53:MeowOS 主屏接管(播放器已死,had_player==0)时强制把亮度压回用户
|
||
* display.brightness 设定值。HibyOS 播放器运行/恢复画面会把它自己的背光下限
|
||
* (16/25)写进 sysfs;若以它为「当前亮度」,用户设的低亮度(<25)会被覆盖。
|
||
* 覆盖菜单(had_player==1,播放器会话中)不压回——播电影时亮度归播放器管。 */
|
||
if (!had_player) apply_user_brightness();
|
||
if (had_player) fb_backup_player(); /* only snapshot when a live player is overlaid */
|
||
draw_menu(apps, napps, state, 0, 0, ""); /* full menu into back_buf */
|
||
fb_norm_buf0(); /* ensure LCDC shows buffer0 (yoffset=0) */
|
||
meow_dirty_full(); meow_ui_flush(); /* A-1/A-2:SDK present(vsync + 单次全帧 blit 到 fb0) */
|
||
|
||
/* touch: event reads (event1 is not grabbed) */
|
||
touch_fd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||
if (touch_fd < 0) logmsg("touch open failed: %s", strerror(errno));
|
||
/* M2.6 幽灵触摸防护:菜单激活(启动)即 drain + 冷却 */
|
||
menu_arm_touch(TAP_ARM_DELAY_MS);
|
||
|
||
/* key polling */
|
||
struct keypoll kp_power, kp_keys;
|
||
kp_open(&kp_power, DEV_POWER);
|
||
kp_open(&kp_keys, DEV_KEYS);
|
||
/* M2.16 按键防抖状态(每键独立);kd_tick 40ms 稳定后才认,抗 ADC 抖动 */
|
||
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
|
||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
|
||
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
|
||
|
||
logmsg("menu open: napps=%d state=%d cursor=%d",
|
||
napps, state, cursor);
|
||
|
||
/* key edge baselines: sample the initial state so keys already held
|
||
* when the menu opens (e.g. the hotkey release tail) do not fire
|
||
* edges; kp.prev cannot be used for this because kp_poll() syncs it
|
||
* to cur before we compare */
|
||
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0;
|
||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
||
}
|
||
/* M2.3A 电源按住计时:≥8s 强制关机;≥3s 弹电源菜单;与单击息屏/热键按时长区分 */
|
||
unsigned long long pwr_hold_at = 0;
|
||
int pwr_menu_state = 0;
|
||
/* M2.18 Bug 2: 一次性标志——本次电源按下已触发「自动息屏唤醒」时置位,其释放
|
||
* 只唤醒不 toggle(防同一次按键被唤醒路径与单单击 toggle 各消费一次 → 闪黑)。 */
|
||
int pwr_woke = 0;
|
||
/* M2.34: 一次性标志——电源菜单刚关闭(无论确认/取消/超时)。返回主循环后,本次
|
||
* 电源按下-松开不得被 3130「松开→单击」路径误判为「电源单击」→ toggle 息屏,否则:
|
||
* ①菜单点关机/重启:关机/重启画面被息屏覆盖;②焦点在取消键单击电源:取消同时熄屏。
|
||
* 清除时机:电源松开(3130 松开分支末尾)或下一次电源按下边沿。 */
|
||
int pwr_suppress_click = 0;
|
||
|
||
/* The menu was already drawn into buffer0 above (pre-drawn before the pan
|
||
* to buffer0), so a redraw is only needed when something actually changes:
|
||
* cursor movement (input), app exit/enter, toast start/end, or a future
|
||
* notification-badge change. NEVER on a periodic timer — see the big
|
||
* comment on the redraw block below. */
|
||
int dirty = 0; /* 结构变化(app 进出/state/唤醒/热键…)→ 全帧重画 */
|
||
int need_menu_paint = 0; /* M2.16:需 draw_menu 全画 back_buf(光标/toast/test 变化) */
|
||
int last_cursor = 0; /* pre-drawn frame showed cursor 0 */
|
||
int toast_active = 0; /* tracks toast_ms>0 for start/end redraw edges */
|
||
long last_status_tick = 0; /* ~1s status-bar-only refresh (clock) */
|
||
|
||
for (;;) {
|
||
unsigned long long t = now_ms();
|
||
long el = (long)(t - t0);
|
||
any_input = 0; /* M2.17:本圈是否有输入(重置 auto screen-off 计时) */
|
||
|
||
/* SIGTERM during the menu: close it cleanly (restore + resume happen
|
||
* in the hotkey block / main loop after we return) */
|
||
if (sigterm_flag) { logmsg("menu: sigterm, closing"); want_exit = 1; break; }
|
||
|
||
/* 阶段 D · M0:菜单/主屏循环内排空 IPC(非阻塞;应用可随时查询/声明路由) */
|
||
meow_ipc_process();
|
||
meow_notif_tick(); /* M2a:通知 TTL 10s 转储(§8)*/
|
||
|
||
/* sim hooks */
|
||
if (sim_tap_ms > 0 && !sim_tap_done && el >= sim_tap_ms) {
|
||
logmsg("SIM TAP row 0");
|
||
sim_tap_done = 1;
|
||
if (napps > 0 && state == 0) {
|
||
/* fall through: select row 0 */
|
||
int ret = launch_app(&apps[0]);
|
||
/* app exited: RETURN TO THE MENU (v6.3) — closing the menu
|
||
* here would leave the app's last frame (e.g. BYE) on fb0,
|
||
* and the player only redraws on input events (none arrive,
|
||
* injection is not delivered), so the screen would stay
|
||
* dark until a key press. Staying in the menu keeps the
|
||
* menu visible; BACK TO PLAYER then resumes the player.
|
||
* Bug 2/3/4: a queued touch from the app run must NOT be
|
||
* replayed as a fresh menu tap (relaunch / flash-to-player),
|
||
* and the "CANNOT START" toast only fires on a real launch
|
||
* failure (ret != 0), never after a clean app exit. */
|
||
menu_after_app_exit(ret == 0);
|
||
last_input_ms = now_ms(); /* M2.17:回主界面即视为输入,重置自动息屏计时 */
|
||
dirty = 1; /* app wrote buffer0 during its run -> redraw menu */
|
||
if (ret != 0) {
|
||
snprintf(toast, sizeof toast, "CANNOT START: %s", apps[0].name);
|
||
toast_ms = TOAST_MS;
|
||
}
|
||
} else {
|
||
snprintf(toast, sizeof toast, "NO APPS");
|
||
toast_ms = TOAST_MS;
|
||
}
|
||
}
|
||
if (sim_exit_ms > 0 && !sim_exit_done && el >= sim_exit_ms) {
|
||
logmsg("SIM EXIT");
|
||
sim_exit_done = 1;
|
||
want_exit = 1;
|
||
break;
|
||
}
|
||
/* M2.6 sim:点 BACK TO PLAYER 行 → MeowOS 模式弹「需重启」确认框并留下;
|
||
* 会话覆盖模式则正常退出。 */
|
||
if (sim_exit_tap_ms > 0 && !sim_exit_tap_done && el >= sim_exit_tap_ms) {
|
||
logmsg("SIM TAP exit row");
|
||
sim_exit_tap_done = 1;
|
||
if (meow_mode) {
|
||
int rb = confirm_reboot_player();
|
||
menu_arm_touch(TAP_ARM_DELAY_MS);
|
||
dirty = 1;
|
||
logmsg("meow mode: BACK TO PLAYER -> confirm reboot (rb=%d)", rb);
|
||
} else {
|
||
want_exit = 1;
|
||
break;
|
||
}
|
||
}
|
||
|
||
/* M2.2 MeowOS 系统拔营:无播放器时由 launcher 接管官方交给
|
||
* hiby_player/sys_server 的两件事 —— TF 卡挂载 + WiFi 联网。
|
||
* 后台每 2s 重试,不阻塞主界面;卡片就绪后重扫应用列表并就地刷新。
|
||
* 仅 meow 常驻模式需要(HibyOS 覆盖模式播放器仍在,/或其管理)。
|
||
* M2.11 C:TF 卡热插拔最佳努力 —— 拔出检测(节点消失→无卡)、
|
||
* 插入检测(节点出现→自动挂载+重扫+重载字库)。不阻塞、不崩。 */
|
||
if (meow_mode && (long)(t - t0) - meow_last_poll >= 2000) {
|
||
meow_last_poll = (long)(t - t0);
|
||
ensure_wifi();
|
||
if (!sd_device_present()) {
|
||
/* 卡被拔出:设备节点消失 → 无卡态。停止扫描;留 state=1(NO STORAGE CARD)。
|
||
* 若此前是正常态,切到无卡并重绘。不杀正在跑的应用(其由 D 的监测处理)。 */
|
||
if (state == 0 || state == 2) { state = 1; dirty = 1; logmsg("hotplug: card removed -> state=1"); }
|
||
g_sys.sd_mounted = 0;
|
||
log_poll_card(); /* 卡日志不可用时回退 /tmp 缓冲 */
|
||
} else if (ensure_card_mounted()) {
|
||
/* 卡在且已挂载(刚插入/重挂成功):补字库 + 重扫 · 仅真正变化才重绘 */
|
||
int was_nocard = (state == 1);
|
||
if (!mfont_ready && meow_font_ensure()) { dirty = 1; logmsg("hotplug: font loaded late"); }
|
||
if (mfont_ready) meow_font_reload_multifont(); /* M2.5x:卡挂载后重配多字体(此前无全局可配) */
|
||
if (was_nocard || state != 0 || napps == 0) {
|
||
struct app fresh[MAX_APPS];
|
||
int fn = scan_apps(fresh);
|
||
int fstate = (fn < 0) ? (fn == -1 ? 1 : 2) : (fn == 0 ? 2 : 0);
|
||
if (fstate == 0 && fn > 0) {
|
||
for (int i = 0; i < fn; i++) apps[i] = fresh[i];
|
||
napps = fn; state = 0;
|
||
logmsg("hotplug: card ready -> %d apps loaded", napps);
|
||
dirty = 1;
|
||
} else if (fstate != state) {
|
||
state = fstate;
|
||
logmsg("hotplug: state -> %d", state);
|
||
dirty = 1;
|
||
}
|
||
}
|
||
g_sys.sd_mounted = 1;
|
||
log_poll_card();
|
||
} else if (!sd_mounted()) {
|
||
/* 卡在但未挂载(正挂载/失败):保持当前显示不误判;不崩。 */
|
||
g_sys.sd_mounted = 0;
|
||
}
|
||
}
|
||
|
||
/* keys: poll and handle edges. Power is NOT handled here anymore
|
||
* (v6.5): inside the menu the power key is a pure screen-off/on
|
||
* toggle handled by sys_server/kernel (the player being SIGSTOPped
|
||
* does not affect it) — the launcher must NOT touch brightness/panel
|
||
* or exit the menu on it. Exit is only via the BACK TO PLAYER touch
|
||
* row (below) or sim-exit. */
|
||
kp_poll(&kp_power, NULL, NULL);
|
||
kp_poll(&kp_keys, NULL, NULL);
|
||
int vold = 0, volu = 0, play = 0, pwr = 0;
|
||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
|
||
}
|
||
if (sim_power_hold_ms > 0 && el >= sim_power_hold_ms) pwr = 1; /* sim:按住电源 */
|
||
/* 原始(未防抖)键值:长按连续(vol_ramp held)用,避免防抖延迟让单击仍连爬多档。 */
|
||
int rvold = vold, rvolu = volu, rplay = play, rpwr = pwr; /* 原始值:长按连续用 rvolu/rvold */
|
||
(void)rplay; (void)rpwr; /* play/pwr 只用稳定边沿,不需原始 held */
|
||
/* M2.16 防抖:每键值须稳定 KEY_DEBOUNCE_MS(40ms) 才认可,抗 ADC 抖动("点一下跳5-6下")。
|
||
* 稳定态 vold/volu/play/pwr 与 m_prev_*(上一稳定值)比较得稳定边沿;长按(连续)用 .down。 */
|
||
(void)kd_tick(&kd_vold, vold);
|
||
(void)kd_tick(&kd_volu, volu);
|
||
(void)kd_tick(&kd_play, play);
|
||
(void)kd_tick(&kd_pwr, pwr);
|
||
vold = kd_vold.stable; volu = kd_volu.stable;
|
||
play = kd_play.stable; pwr = kd_pwr.stable;
|
||
/* M2.6 电源语义(MeowOS 自持面板,无播放器拔河):
|
||
* 单击(短按松开 <2s)= 息屏/亮屏;2s = 电源菜单;8s = 强制关机。 */
|
||
if (meow_mode) {
|
||
if (pwr && !m_prev_pwr) {
|
||
pwr_hold_at = t; pwr_menu_state = 0; any_input = 1;
|
||
pwr_suppress_click = 0; /* M2.34:新一次电源按下清除旧的抑制标志 */
|
||
/* M2.18 Bug 2: 若这次按下正好落在「自动息屏」态(auto_slept 且屏已关),
|
||
* 本次电源键只做唤醒(下方通用醒屏分支 meow_panel_on()),其释放不再走
|
||
* 单单击 toggle —— 否则同一次按下先被唤醒点亮、又在释放时被 toggle 关掉
|
||
* (症状:自动息屏后点电源「闪一下黑、再点才亮」)。 */
|
||
if (meow_auto_slept && !meow_screen_on) pwr_woke = 1;
|
||
}
|
||
if (pwr && pwr_hold_at) {
|
||
/* M2.10/M2.11 B:8s 强关只在该「按住」从未打开过电源菜单时才触发
|
||
* (!pwr_menu_state)。一旦打开过菜单(用户已互动/取消),run_menu 的
|
||
* 8s 强关永远不在此 hold 上再触发 —— 由菜单自身的「按当前项确认」/取消
|
||
* 决定动作。避免「按住电源开菜单->确认取消->仍按住->run_menu 8s 强关」残留。
|
||
* M2.37:阈值接 power.forceoff_ms(默认 8000),强关保持 > 弹菜单上限。 */
|
||
if (t - pwr_hold_at >= power_forceoff_ms && !pwr_menu_state) {
|
||
if (sim_no_exec) { logmsg("pm: [sim-no-exec] POWER HOLD %dms -> would force poweroff", power_forceoff_ms); }
|
||
else { logmsg("pm: POWER HOLD %dms -> force poweroff", power_forceoff_ms); sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); }
|
||
m_prev_pwr = pwr; continue;
|
||
} else if (t - pwr_hold_at >= power_longpress_menu_ms && !pwr_menu_state) {
|
||
pwr_menu_state = 1;
|
||
logmsg("pm: POWER HOLD %dms -> show power menu", power_longpress_menu_ms);
|
||
show_power_menu(); /* 动作在函数内 system(); 取消则续循环 */
|
||
/* M2.31:菜单打开期间主循环阻塞,last_input_ms 不会刷新;若停留超过自动息屏
|
||
* 超时,取消返回后会被误判「空闲」而立即熄屏(用户点取消瞬间黑屏)。
|
||
* 菜单返回即视为「刚有输入」,重置自动息屏计时。 */
|
||
last_input_ms = now_ms();
|
||
/* M2.11 B:菜单已关闭(取消)。清空按住计时与菜单态:仍按住/随后松开的电源
|
||
* 按「新按下」处理,绝不把已互动的 hold 再累计到 8s 强关。 */
|
||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
||
pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击熄屏 */
|
||
dirty = 1; /* 取消后重绘应用列表 */
|
||
menu_arm_touch(800); /* M2.6 幽灵触摸防护:菜单关闭后忽略触摸 */
|
||
/* M2.25 黑屏修复:阻塞式 show_power_menu() 期间 kd_* 防抖状态冻结。
|
||
* 用户常「松开电源 → 用触摸点取消」,返回后 kd_pwr.stable 仍是旧的「按住=1」,
|
||
* 而 m_prev_pwr 已按 raw 重播为 0 → 二者错配误判「新按下」刷新 pwr_hold_at,
|
||
* 随后稳定值转 0 时又被误判「单击」→ meow_toggle_panel 把面板关掉(黑屏且
|
||
* 难唤醒:launcher 以为屏开着,电源唤醒逻辑不触发)。电源键确认取消时 raw
|
||
* 仍=按住=1,无此错配,所以只有触摸路径黑屏。此处重播 kd_* 并以当前 raw
|
||
* 对齐 m_prev_*,杜绝该假边沿(真实新按键仍正常防抖/单击)。 */
|
||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||
memset(&kd_vold, 0, sizeof kd_vold);
|
||
memset(&kd_volu, 0, sizeof kd_volu);
|
||
memset(&kd_play, 0, sizeof kd_play);
|
||
kp_poll(&kp_power, NULL, NULL);
|
||
kp_poll(&kp_keys, NULL, NULL);
|
||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||
continue;
|
||
}
|
||
}
|
||
if (!pwr && m_prev_pwr) {
|
||
/* 松开:短按(<3s 且菜单未开)→ 单击 = 息屏/亮屏切换 */
|
||
if (pwr_suppress_click) {
|
||
/* M2.34:本次电源按下来自「电源菜单内确认/取消」,其松开不得 toggle 息屏
|
||
* (否则关机/重启画面被息屏覆盖,或焦点在取消键单击电源导致熄屏)。 */
|
||
logmsg("pm: power release after power-menu -> no toggle (suppress)");
|
||
} else if (pwr_hold_at && (t - pwr_hold_at < power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) {
|
||
logmsg("pm: power single-click -> toggle panel (was %s)", meow_screen_on ? "on" : "off");
|
||
meow_toggle_panel();
|
||
if (meow_screen_on) {
|
||
/* M2.8 Bug 3-residual:唤醒不是 draw 事件,LCDC 在 FBIOBLANK(4)->(0)
|
||
* 后指向的内容可能已失效/为空;必须把当前界面(应用列表)整体重绘到
|
||
* buffer0 并强制 re-pan,而非仅翻页/仅 dirty 重绘(M2.7 仍黑屏根因)。 */
|
||
meow_auto_slept = 0; /* M2.17:手动/自动息屏后电源单击唤醒均清自动态 */
|
||
last_input_ms = now_ms(); /* M2.18 Bug 1:用真实墙钟(防 t 过期回绕) */
|
||
meow_repaint_surface(apps, napps, state, cursor);
|
||
} else {
|
||
dirty = 1; /* 息屏:面板已黑,留 dirty 供下次亮屏重绘 */
|
||
last_input_ms = now_ms(); /* M2.18 Bug 1:用真实墙钟 */
|
||
}
|
||
} else if (pwr_woke) {
|
||
/* M2.18 Bug 2:本次电源按下已由自动息屏唤醒分支消费,释放只记录、不 toggle。 */
|
||
logmsg("pm: power release after auto screen-off wake -> no toggle (wake only)");
|
||
}
|
||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
||
pwr_suppress_click = 0; /* M2.34:电源已松开,抑制标志一次性用完即清 */
|
||
}
|
||
m_prev_pwr = pwr;
|
||
}
|
||
/* volume/play edges against the launcher-maintained baselines
|
||
* (previously dead code: prev was re-seeded from cur each pass, so
|
||
* no edge could ever fire and the menu keys could not navigate or
|
||
* launch).
|
||
* M2.11 E:MeowOS 下音量键 = 调系统软件音量(settings audio.volume,步进5 clamp),
|
||
* 状态栏即时显示、持久化。有播放器的覆盖态才保留音量键=菜单光标导航。 */
|
||
if (meow_mode) {
|
||
if (vold && !m_prev_vold) {
|
||
any_input = 1;
|
||
settings_volume_adj(-settings_volume_step); /* 起线性渐变(1% 爬) */
|
||
logmsg("volume DOWN <- ramp step=%d", settings_volume_step);
|
||
} else if (volu && !m_prev_volu) {
|
||
any_input = 1;
|
||
settings_volume_adj(+settings_volume_step);
|
||
logmsg("volume UP <- ramp step=%d", settings_volume_step);
|
||
}
|
||
} else {
|
||
/* M2.36 B2b:光标范围扩展为 [0..napps](napps=「系统设置」行,位于 Exit 行上方)。
|
||
* 音量下移越过最后应用 → 设置行;设置行再下移 → 回 0。应用行仍 0..napps-1。 */
|
||
if (vold && !m_prev_vold && napps > 0 && state == 0) {
|
||
any_input = 1;
|
||
cursor++;
|
||
if (cursor > napps) cursor = 0; /* napps=设置行 */
|
||
logmsg("menu cursor -> %d", cursor);
|
||
}
|
||
if (volu && !m_prev_volu && napps > 0 && state == 0) {
|
||
any_input = 1;
|
||
cursor--;
|
||
if (cursor < 0) cursor = napps; /* 回绕到设置行 */
|
||
logmsg("menu cursor -> %d", cursor);
|
||
}
|
||
}
|
||
/* M2.14 线性渐变:每 tick 推进 1% 并刷新状态栏(显示爬升中的值或最终值) */
|
||
if (meow_mode && vol_ramp_dir != 0) {
|
||
int held = (vol_ramp_dir > 0) ? rvolu : rvold; /* 长按=该方向键仍按住(用原始值:松开立即停止) */
|
||
int going = vol_ramp_tick(held);
|
||
g_sys.vol = settings_volume_get();
|
||
paint_sb_right(); /* M2.16:只重画右侧信息格(VOL 变化),不动列表/时钟 */
|
||
if (!going) logmsg("volume ramp end -> %d", g_sys.vol);
|
||
}
|
||
/* M2.19:播放键不再是「应用列表的确认/打开」——它让位给全局媒体控制
|
||
* (播放/暂停,路由到当前活跃媒体会话)。菜单/列表的确认只走触摸点选
|
||
* (见下方 touch 段),物理确认交给电源键(power 菜单/确认框),播放键
|
||
* 在 MeowOS 主循环里不再触发 launch_app。媒体预留:后续媒体会话接管,
|
||
* 此处仅作 any_input(any_input 仍用于重置自动息屏计时)。(原播放键=打开
|
||
* 已删;如需物理启动应用,可后续加「长按播放键」,本轮不做。) */
|
||
if (play && !m_prev_play) {
|
||
any_input = 1; /* 仍视为输入(重置息屏计时);不再 launch_app */
|
||
logmsg("play key: media-reserved (no menu confirm)");
|
||
}
|
||
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play;
|
||
|
||
/* touch (event reads) */
|
||
if (touch_fd >= 0 && meow_screen_on) { /* 息屏时不处理触摸(goodix 已被 FBIOBLANK(4) 挂起) */
|
||
struct pollfd pfd = { touch_fd, POLLIN, 0 };
|
||
if (poll(&pfd, 1, 0) > 0) {
|
||
unsigned char ev[16];
|
||
while (read(touch_fd, ev, sizeof ev) == 16) {
|
||
uint16_t type = *(uint16_t *)(ev + 8);
|
||
uint16_t code = *(uint16_t *)(ev + 10);
|
||
int32_t val = *(int32_t *)(ev + 12);
|
||
if (type == EV_ABS) {
|
||
if (code == ABS_X || code == ABS_MT_POSITION_X) t_last_x = val;
|
||
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) t_last_y = val;
|
||
/* M2.23 原点锁定:goodix 事件时序不保证 ABS 与 BTN_TOUCH 先后,
|
||
* 按下边沿只标 t_down,原点延迟到「本次按下之后」的首个有效坐标才锁定,
|
||
* 杜绝把上一手势残留(-1/旧坐标)当本次手势起点。 */
|
||
if (t_down && !t_down_valid && t_last_x >= 0 && t_last_y >= 0) {
|
||
t_down_x = t_last_x; t_down_y = t_last_y; t_down_valid = 1;
|
||
}
|
||
} 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<last_input 无符号回绕误判「空闲30s」。 */
|
||
if (idle_ms_since(t, last_input_ms) >= (unsigned long long)screen_off_timeout * 1000) {
|
||
meow_panel_off();
|
||
meow_auto_slept = 1;
|
||
logmsg("auto screen-off after %ds idle (mode=%s keep_awake=%d, t=%llu last=%llu)",
|
||
screen_off_timeout, screen_off_mode ? "black" : "panel",
|
||
fg_keep_awake(), t, last_input_ms);
|
||
}
|
||
}
|
||
|
||
/* Redraw only when something actually changed: cursor movement (an
|
||
* input event), app exit/enter (which rewrote buffer0), toast
|
||
* start/end, or a future notification-badge change. NEVER on a
|
||
* periodic timer.
|
||
*
|
||
* v6.8 BUG (the ~3fps list flicker): the v6.8 "500ms safety redraw"
|
||
* rewrote the ENTIRE menu into the live-displayed buffer0 every 500ms
|
||
* (the v6.8 fix made last_redraw=el so the timer finally fired), plus
|
||
* draw_statusbar()'s amixer popen on every redraw. An in-place full
|
||
* rewrite of the buffer the LCDC is scanning produces visible tearing/
|
||
* pulsing on the app list, and the slow amixer popen stalls the loop.
|
||
* Full-redraw in place is only correct right after a REAL change; the
|
||
* steady-state list must not be touched. */
|
||
/* M2.16:只在真变化时标脏(精确到格子),绝无"动一下就全屏重绘"。
|
||
* 光标移动 → 旧/新两行;toast 起止 → toast 区;其余结构变化 → dirty=1(全帧)。
|
||
* 音量渐变/时钟只经 paint_sb_right/draw_clock 标右侧格/时钟格。 */
|
||
if (cursor != last_cursor) {
|
||
/* M2.36 B2b:光标可能在应用行(0..napps-1)或「系统设置」行(napps,位于 Exit 行上方)。
|
||
* 分别标脏对应行 rect(设置行固定 y=EXIT_ROW-ROW_H,与 napps 无关)。 */
|
||
if (cursor == napps || last_cursor == napps)
|
||
meow_dirty_add(8, EXIT_ROW - ROW_H - 4, fb_xres - 16, ROW_H - 8);
|
||
if (state == 0 && napps > 0) {
|
||
if (last_cursor >= 0 && last_cursor < napps)
|
||
meow_dirty_add(8, ROW_TOP + last_cursor * ROW_H - 4, fb_xres - 16, ROW_H - 8);
|
||
if (cursor >= 0 && cursor < napps)
|
||
meow_dirty_add(8, ROW_TOP + cursor * ROW_H - 4, fb_xres - 16, ROW_H - 8);
|
||
}
|
||
last_cursor = cursor;
|
||
need_menu_paint = 1;
|
||
}
|
||
if (toast_active != (toast_ms > 0)) { /* toast start/end */
|
||
toast_active = (toast_ms > 0);
|
||
meow_dirty_add(20, TOAST_Y - 6, fb_xres - 40, 40);
|
||
need_menu_paint = 1;
|
||
}
|
||
if (dirty) { /* 结构变化 → 全帧重画 + 全屏 blit */
|
||
meow_dirty_full();
|
||
need_menu_paint = 1;
|
||
dirty = 0;
|
||
}
|
||
if (need_menu_paint) {
|
||
draw_menu(apps, napps, state, cursor, toast_ms, toast); /* 全画 SDK back_buf(RAM,快) */
|
||
need_menu_paint = 0;
|
||
}
|
||
/* A-1/A-2:呈现统一走 SDK meow_ui_flush()(内含帧率门控;无脏返回,门控未达期保留脏
|
||
* 待下一轮)。fb_norm_buf0 确保 LCDC 显示 buffer0(yoffset 非 0 才 pan,短路无开销)。
|
||
* 不再用 launcher 的 g_ndirty/g_dirty_full 做呈现门——SDK present 自会 no-op 于空脏。 */
|
||
fb_norm_buf0(); /* Bug 3:确保 LCDC 显示 buffer0 */
|
||
meow_ui_flush(); /* M2.16 无闪烁 + M2.16 帧率门控归口 SDK */
|
||
if (toast_ms > 0) {
|
||
toast_ms -= POLL_MS;
|
||
if (toast_ms < 0) toast_ms = 0;
|
||
}
|
||
/* Clock refresh ~1s: redraw ONLY the clock digits (and the right info
|
||
* cell if battery/wifi changed), never the list. */
|
||
if (el - last_status_tick >= 1000) {
|
||
last_status_tick = el;
|
||
meow_sys_light(); /* 同步电量/WiFi/音量进 g_sys */
|
||
draw_clock(); /* M2.17:sb_redraw() 重画整条(时钟+电量/WiFi/音量)并标整条脏 */
|
||
fb_norm_buf0();
|
||
meow_ui_flush(); /* blit 状态栏整条,不动列表 */
|
||
}
|
||
|
||
usleep(POLL_MS * 1000);
|
||
}
|
||
|
||
if (touch_fd >= 0) close(touch_fd);
|
||
/* v6.6: restore the player's frame + pan back BEFORE releasing fb (the
|
||
* player is event-driven and won't redraw on its own — see §18.15).
|
||
* M2.2:仅当存在存活播放器才恢复其帧;MeowOS 常驻主界面(无播放器)绝不
|
||
* 恢复死帧——屏幕保持自绘的应用列表(buffer0),不遗留播放器残帧。 */
|
||
if (had_player) fb_restore_player();
|
||
if (fbfd >= 0) { if (fb_fd == fbfd) fb_fd = -1; close(fbfd); }
|
||
logmsg("menu close (exit=%d meow_mode=%d had_player=%d)", want_exit, meow_mode, had_player);
|
||
}
|
||
|
||
/* ================================================================== */
|
||
/* app launch */
|
||
/* ================================================================== */
|
||
static void on_sigterm(int s) { (void)s; sigterm_flag = 1; }
|
||
|
||
/* Drain pending events from all input devices (non-blocking read to EAGAIN).
|
||
* Called right before launching an app so residual events (the tap/key that
|
||
* selected the row, the hotkey combo's release tail) are consumed before the
|
||
* app opens its own fds. Note: events generated AFTER the drain still reach
|
||
* the app (multicast); the app therefore also drains itself at startup and
|
||
* only exits on a fresh press edge (see hello.c). */
|
||
static void drain_input(void)
|
||
{
|
||
for (int i = 0; i < 4; i++) {
|
||
char dev[32];
|
||
snprintf(dev, sizeof dev, "/dev/input/event%d", i);
|
||
int fd = open(dev, O_RDONLY | O_NONBLOCK);
|
||
if (fd < 0) continue;
|
||
unsigned char ev[16];
|
||
while (read(fd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ }
|
||
close(fd);
|
||
}
|
||
}
|
||
|
||
/* ---- backlight save/restore ---- */
|
||
static int read_brightness(void)
|
||
{
|
||
int fd = open(BACKLIGHT, O_RDONLY);
|
||
if (fd < 0) return -1;
|
||
char buf[16];
|
||
int n = read(fd, buf, sizeof buf - 1);
|
||
close(fd);
|
||
if (n <= 0) return -1;
|
||
buf[n] = 0;
|
||
return atoi(buf);
|
||
}
|
||
|
||
static void write_file(const char *path, const char *val)
|
||
{
|
||
int fd = open(path, O_WRONLY);
|
||
if (fd < 0) return;
|
||
int l = strlen(val);
|
||
if (write(fd, val, l) < 0)
|
||
logmsg("write %s <- %s: %s", path, val, strerror(errno));
|
||
close(fd);
|
||
}
|
||
|
||
static void write_brightness(int val)
|
||
{
|
||
char buf[8];
|
||
int l = snprintf(buf, sizeof buf, "%d\n", val);
|
||
int fd = open(BACKLIGHT, O_WRONLY);
|
||
if (fd < 0) { logmsg("backlight open fail: %s", strerror(errno)); return; }
|
||
if (write(fd, buf, l) < 0) logmsg("backlight write %d: %s", val, strerror(errno));
|
||
close(fd);
|
||
}
|
||
|
||
/* ================================================================== */
|
||
/* M2.36 阶段 B2b:实体系统设置页(MD3 风格) */
|
||
/* - 入口:应用列表「系统设置」builtin 行(draw_menu 中位于 Exit 行上 */
|
||
/* 方;触摸命中 run_menu 分发到此 run_settings_page)。 */
|
||
/* - 结构:分组列表(显示/电源/开机/输入/字体主题/通知/日志/音频), */
|
||
/* 全屏单张 surface_variant 圆角卡片,组标题 on_surface_variant 小字。*/
|
||
/* - 控件:开关(SC_SWITCH)/滑条(SC_SLIDER)/选择器(SC_ENUM)/预设路径 */
|
||
/* (SC_PRESET)。所有读写仅走 meow_settings_getXXX / setXXX 接口; */
|
||
/* meow_settings_save(),绝不直接改文件(SYSTEM_SETTINGS §4)。 */
|
||
/* ================================================================== */
|
||
/* M2.36 阶段 B2b:实体系统设置页(MD3 风格) */
|
||
/* - 入口:应用列表「系统设置」builtin 行(draw_menu 中位于 Exit 行上 */
|
||
/* 方;触摸命中 run_menu 分发到此 run_settings_page)。 */
|
||
/* - 结构:分组列表(显示/电源/开机/输入/字体主题/通知/日志/音频), */
|
||
/* 全屏单张 surface_variant 圆角卡片,组标题 on_surface_variant 小字。*/
|
||
/* - 控件:开关(SC_SWITCH)/滑条(SC_SLIDER)/选择器(SC_ENUM)/预设路径 */
|
||
/* (SC_PRESET)。所有读写仅走 meow_settings_getXXX / setXXX 接口; */
|
||
/* meow_settings_save(),绝不直接改文件(SYSTEM_SETTINGS §4)。 */
|
||
/* - 主题:MD3 color roles(dark/light 两套,接 display.theme),集中 */
|
||
/* 下表,禁止散落硬编码色值(MD3_UI_SPEC §1)。 */
|
||
/* - 交互:音量键=调系统音量 / 电源键=系统级语义(单击=息屏/亮屏、长按=电源菜单、
|
||
8s=强关;不再返回)/ 播放键=抓住(不返回)/ 退出仅靠触摸(底部返回 + 左缘左滑)。*/
|
||
/* 选中行 tonal 高亮(meow_ui_fill_rect_alpha),滑条拖动实时 set_*。 */
|
||
/* 静态展示不主动刷新,仅交互时经 meow_dirty 区标脏 + blit 局部。 */
|
||
/* ================================================================== */
|
||
/* 主题(MD3 color roles)定义已上移到本文件前方状态栏处(M2.44)。 */
|
||
|
||
/* ---- 控件类型 ---- */
|
||
typedef enum { SC_HEADER, SC_SWITCH, SC_SLIDER, SC_ENUM, SC_PRESET, SC_INFO, SC_LIST } sctrl_t;
|
||
/* ---- 一行描述(label+key+控件);key=NULL 表示分组标题行 ---- */
|
||
typedef struct {
|
||
const char *label;
|
||
const char *key;
|
||
sctrl_t ctrl;
|
||
int min, max, step; /* SC_SLIDER 取值域/步进 */
|
||
const char *const *presets; /* SC_PRESET 选项名 */
|
||
const char *const *preset_labels; /* SC_PRESET 选项中文标签 */
|
||
int preset_n;
|
||
int net_list; /* SC_INFO/SC_LIST 网络信息类别(NET_LIST_*),0=无 */
|
||
} srow_t;
|
||
|
||
/* 网络信息类别(SC_LIST 打开下拉/详情;对应 SMENU「网络」组行) */
|
||
#define NET_LIST_WIFI_SSID 1 /* WiFi 当前连接(只读信息;点开显示详情) */
|
||
#define NET_LIST_WIFI_SAVED 2 /* WiFi 已保存网络(下拉,可 connect) */
|
||
#define NET_LIST_WIFI_SCAN 3 /* 附近WiFi:WiFi 扫描结果(按 BSSID 去重,只读展示) */
|
||
#define NET_LIST_BT_NEARBY 4 /* 蓝牙附近设备(已配对 + 扫描发现;下拉,只读展示) */
|
||
#define NET_LIST_BT_MODE 5 /* 蓝牙当前连接模式(只读信息;对应"蓝牙模式"行,s_net_get 显示) */
|
||
#define NET_LIST_ABOUT 6 /* 系统设置「关于本机」:只读设备信息弹窗(复用 s_dd_modal) */
|
||
#define S_NET_MAX 8 /* 网络下拉一次最多展示项(屏幕高度限制) */
|
||
|
||
/* SC_PRESET 预设表(路径/枚举字符串的"预设项,自定义文件选择留后期") */
|
||
static const char *const bg_presets[] = { "black", "blue", "green" };
|
||
static const char *const bg_preset_labels[] = { "黑色", "深蓝", "深绿" };
|
||
#define BG_PRESETS_N 3
|
||
static const char *const hk_presets[] = { "power_voldown", "seq" };
|
||
static const char *const hk_preset_labels[] = { "电源+音量减", "连按序列" };
|
||
#define HK_PRESETS_N 2
|
||
static const char *const seq_presets[] = { "volup,volup,volup", "voldown,voldown,voldown" };
|
||
static const char *const seq_preset_labels[] = { "音量上×3", "音量下×3" };
|
||
#define SEQ_PRESETS_N 2
|
||
/* font.file 行(字体文件)不使用编译期静态表:其 presets/preset_labels 指向字体区
|
||
* (font_presets/font_preset_labels, 见本文件字体区) 的运行时动态数组,计数由
|
||
* s_row_presets() 动态返回(默认 1,进设置页扫描 TF 卡后更新)。见 SMENU 行。 */
|
||
static const char *const out_presets[] = { "hw:0,0" };
|
||
static const char *const out_preset_labels[] = { "CS43198 (hw:0,0)" };
|
||
#define OUT_PRESETS_N 1
|
||
|
||
/* M2.37:息屏时间滑条改为「离散档位」而非线性 0~86400 秒(线性步进 1 根本调不到 30s,
|
||
* 上限 24h 过于抽象)。档位 0=常亮;从 5s 起步的细粒度段可精调「30s 左右」,随后按
|
||
* 45s/分/刻/时 跳档,最高 2 时。滑条值 = 档位索引(0..S_TIME_TIERS_N-1,步进 1),
|
||
* 落盘仍写「秒」(display.screen_off_timeout);索引<->秒双向映射见 s_time_tier_*。 */
|
||
static const int s_time_tiers[] = {
|
||
0, /* 常亮 */
|
||
5, 10, 15, 20, /* 细粒度段:可精确到 30s 附近 */
|
||
30, 45, /* 半分钟/三刻钟 */
|
||
60, 90, 120, 180, /* 1 分 / 1.5 分 / 2 分 / 3 分 */
|
||
300, 600, 900, /* 5 分 / 10 分 / 15 分 */
|
||
1800, /* 30 分 */
|
||
3600, 7200 /* 1 时 / 2 时 */
|
||
};
|
||
#define S_TIME_TIERS_N ((int)(sizeof s_time_tiers / sizeof s_time_tiers[0]))
|
||
|
||
/* 设置页扁平化菜单(分组=组标题行,其余=设置项)。顺序即显示顺序。 */
|
||
/* 分组/键/类型/默认与 SYSTEM_SETTINGS.md §1 一致(新建项四端同步)。 */
|
||
static const srow_t SMENU[] = {
|
||
/* 显示 */
|
||
{ "显示", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
|
||
{ "亮度", "display.brightness", SC_SLIDER, 1,100,1, NULL,NULL,0 },
|
||
{ "息屏模式", "display.screen_off_mode", SC_ENUM, 0,0,0, NULL,NULL,0 },
|
||
{ "息屏时间", "display.screen_off_timeout", SC_SLIDER, 0,(S_TIME_TIERS_N-1),1, NULL,NULL,0 },
|
||
{ "刷新率", "display.refresh_rate", SC_ENUM, 0,0,0, NULL,NULL,0 },
|
||
{ "最高刷新率", "display.refresh_always_max", SC_SWITCH, 0,0,0, NULL,NULL,0 },
|
||
/* B2b 主题三方式:标题行「主题」改选 display.theme_mode(手动/时段自动),
|
||
* 手动深浅由下方「主题深浅」(display.theme) 控制;时段两键为分钟滑条(STEP=15)。 */
|
||
{ "主题", "display.theme_mode", SC_ENUM, 0,0,0, NULL,NULL,0 },
|
||
{ "主题深浅", "display.theme", SC_ENUM, 0,0,0, NULL,NULL,0 },
|
||
{ "深色时段开始", "display.theme_dark_start", SC_SLIDER, 0,1439,15, NULL,NULL,0 },
|
||
{ "深色时段结束", "display.theme_dark_end", SC_SLIDER, 0,1439,15, NULL,NULL,0 },
|
||
{ "背景", "display.background", SC_PRESET, 0,0,0, bg_presets, bg_preset_labels, BG_PRESETS_N },
|
||
/* 网络 */
|
||
{ "网络", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
|
||
{ "WiFi", "network.wifi_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 },
|
||
{ "WiFi连接", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_SSID },
|
||
{ "已保存网络", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_SAVED },
|
||
{ "附近WiFi", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_WIFI_SCAN },
|
||
{ "蓝牙", "network.bluetooth_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 },
|
||
{ "蓝牙模式", NULL, SC_INFO, 0,0,0, NULL,NULL,0, NET_LIST_BT_MODE },
|
||
{ "蓝牙设备", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_BT_NEARBY },
|
||
/* 电源 */
|
||
{ "电源", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
|
||
{ "长按弹菜单", "power.longpress_menu_ms", SC_SLIDER, 500,5000,500, NULL,NULL,0 },
|
||
{ "长按强关", "power.forceoff_ms", SC_SLIDER, 5500,15000,500, NULL,NULL,0 },
|
||
{ "退出热键", "power.defeat_hotkey", SC_PRESET, 0,0,0, hk_presets, hk_preset_labels, HK_PRESETS_N },
|
||
/* 开机 */
|
||
{ "开机", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
|
||
{ "默认系统", "boot.default", SC_ENUM, 0,0,0, NULL,NULL,0 },
|
||
{ "显示选单", "boot.menu_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 },
|
||
{ "选单超时", "boot.timeout_ms", SC_SLIDER, 0,30000,500, NULL,NULL,0 },
|
||
/* 输入 */
|
||
{ "输入", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
|
||
{ "热键序列", "input.hotkey_seq", SC_PRESET, 0,0,0, seq_presets, seq_preset_labels, SEQ_PRESETS_N },
|
||
{ "热键窗口", "input.hotkey_window_ms", SC_SLIDER, 0,5000,100, NULL,NULL,0 },
|
||
{ "触摸布防", "input.touch_arm_delay_ms", SC_SLIDER, 0,2000,50, NULL,NULL,0 },
|
||
{ "滑动方向", "input.swipe_invert", SC_SWITCH, 0,0,0, NULL,NULL,0 },
|
||
/* 字体主题 */
|
||
{ "字体主题", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
|
||
{ "字体文件", "font.file", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
|
||
/* M2.5x 字体档位分层/按需大字库:「加载大字库」= font.load_full(int 0/1 默认 0)。
|
||
* 开 → 强制全系统用大字库 gbk≈20k 档(覆盖 app 未声明/覆盖默认 font.file);
|
||
* 关(默认)→ 小字库 common≈4k,仅 app 声明 fonts:"full" 才大字库。 */
|
||
{ "加载大字库", "font.load_full", SC_SWITCH, 0,0,0, NULL,NULL,0 },
|
||
/* M2.5x 多字体:开关 + 6 个字符类别字体(preset=可用 MeowOS/fonts/<family>/ 列表;
|
||
* 值空 = 跟随全局 font.file)。仅开关开启时 6 行可见(s_row_visible)。 */
|
||
{ "多字体", "display.multi_font", SC_SWITCH, 0,0,0, NULL,NULL,0 },
|
||
{ "中文(CJK)", "font.class_cjk", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
|
||
{ "中文标点", "font.class_cjk_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
|
||
{ "数字", "font.class_num", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
|
||
{ "拉丁字母", "font.class_latin", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
|
||
{ "半角标点", "font.class_latin_sym", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
|
||
{ "其他字符", "font.class_other", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 },
|
||
{ "标题字号", "font.size.title", SC_SLIDER, 16,40,1, NULL,NULL,0 },
|
||
{ "正文字号", "font.size.body", SC_SLIDER, 12,28,1, NULL,NULL,0 },
|
||
{ "UI风格", "ui.style", SC_ENUM, 0,0,0, NULL,NULL,0 },
|
||
/* 通知 */
|
||
{ "通知", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
|
||
{ "通知中心", "notify.center_enabled", SC_SWITCH, 0,0,0, NULL,NULL,0 },
|
||
{ "应用配额", "notify.app_quota", SC_SLIDER, 1,64,1, NULL,NULL,0 },
|
||
/* 日志 */
|
||
{ "日志", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
|
||
{ "日志级别", "log.level", SC_ENUM, 0,0,0, NULL,NULL,0 },
|
||
{ "写入TF卡", "log.to_card", SC_SWITCH, 0,0,0, NULL,NULL,0 },
|
||
/* 音频 */
|
||
{ "音频", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
|
||
{ "音量", "audio.volume", SC_SLIDER, 0,100,1, NULL,NULL,0 },
|
||
{ "音量上限", "audio.volume_max", SC_SLIDER, 0,100,1, NULL,NULL,0 },
|
||
{ "音量跳步", "audio.volume_step", SC_SLIDER, 1,5,1, NULL,NULL,0 },
|
||
{ "输出设备", "audio.output", SC_PRESET, 0,0,0, out_presets, out_preset_labels, OUT_PRESETS_N },
|
||
/* 关于(设置列表最底;「关于本机」点开 s_about_open 弹只读设备信息框) */
|
||
{ "关于", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 },
|
||
{ "关于本机", NULL, SC_LIST, 0,0,0, NULL,NULL,0, NET_LIST_ABOUT },
|
||
};
|
||
#define SMENU_N ((int)(sizeof SMENU / sizeof SMENU[0]))
|
||
|
||
/* 设置页布局(单卡片,行高统一便于整行式滚动对位与裁剪)。
|
||
* SET_CONTENT_H 取 SROW_H 的整数倍(520=10 行),使 scroll 恒为整行步进,
|
||
* 视口内行首行/末行都完整落位,避免部分可见行被画到卡片外(泄漏)。 */
|
||
#define SROW_H 58 /* M2.37:行高 52→58,便于触摸点选 */
|
||
#define SET_CARD_X 8
|
||
#define SET_CARD_W (fb_xres - 16)
|
||
#define SET_CARD_Y 92
|
||
#define SET_CARD_H 536 /* 卡片底 628;视口高 = 536-16 = 520 = 10×52 */
|
||
#define SET_CONTENT_X (SET_CARD_X + 12)
|
||
#define SET_CONTENT_W (SET_CARD_W - 24)
|
||
#define SET_CONTENT_Y (SET_CARD_Y + 8)
|
||
#define SET_CONTENT_H (SET_CARD_H - 16)
|
||
#define SET_CONTENT_BOT (SET_CONTENT_Y + SET_CONTENT_H)
|
||
#define S_SCROLL_SLOP 40 /* px:触摸纵向位移超此值 → 判为列表拖动滚动(非 tap;tap 判定 ady<60) */
|
||
/* M2.39 左缘返回手势已抽为共享函数 meow_edge_back()(meow_touch.h MEOW_EDGE_W/SWIPE 由
|
||
* 其定义),设置页运行期调用;此处不再定义独立 S_EDGE_W/S_SWIPE_THRESH。 */
|
||
|
||
/* 当前选中行若是滑条:记录其 track 纵向位置,供触摸拖动命中判(x/w 为布局常量,
|
||
* 所有滑条行相同,可直接算;仅 y 随行变,故独立记录)。 */
|
||
static int s_sel_track_y = -1;
|
||
/* 滑条 track 布局常量(所有滑条行一致;track 纵向 = 行顶 + 40,由 s_sel_track_y 记录) */
|
||
#define S_SLIDER_X (SET_CARD_X + 16)
|
||
#define S_SLIDER_W (SET_CONTENT_W)
|
||
|
||
/* ---- 行高/位置/导航辅助 ---- */
|
||
/* M2.5x 多字体:6 个「字体文件」行仅在 display.multi_font=1 时可见(开时显示)。
|
||
* 可见性与分组的标题行无关(标题恒显示)。 */
|
||
static int s_row_visible(int i)
|
||
{
|
||
const srow_t *r = &SMENU[i];
|
||
if (r->key && !strncmp(r->key, "font.class_", 11)) {
|
||
int on = 0; meow_settings_get_switch("display.multi_font", &on);
|
||
return on;
|
||
}
|
||
return 1;
|
||
}
|
||
/* 可见行数(= 压缩后占用的行数;标题行计入,隐藏的 class 行不计) */
|
||
static int s_vis_count(void)
|
||
{
|
||
int n = 0;
|
||
for (int i = 0; i < SMENU_N; i++) if (s_row_visible(i)) n++;
|
||
return n;
|
||
}
|
||
/* SMENU 下标 → 压缩位置(第几个可见行);不可见返回 -1 */
|
||
static int s_vis_of(int i)
|
||
{
|
||
if (i < 0 || i >= SMENU_N || !s_row_visible(i)) return -1;
|
||
int p = 0;
|
||
for (int k = 0; k < i; k++) if (s_row_visible(k)) p++;
|
||
return p;
|
||
}
|
||
/* 压缩位置 → SMENU 下标 */
|
||
static int s_vis_at(int p)
|
||
{
|
||
int c = 0;
|
||
for (int i = 0; i < SMENU_N; i++) if (s_row_visible(i)) { if (c == p) return i; c++; }
|
||
return SMENU_N - 1;
|
||
}
|
||
|
||
static int s_setting_idx(int i) { return i >= 0 && i < SMENU_N && SMENU[i].ctrl != SC_HEADER && s_row_visible(i); }
|
||
static int s_first_setting(void)
|
||
{
|
||
for (int i = 0; i < SMENU_N; i++) if (s_setting_idx(i)) return i;
|
||
return 0;
|
||
}
|
||
static int s_next_setting(int i)
|
||
{
|
||
for (int k = i + 1; k < SMENU_N; k++) if (s_setting_idx(k)) return k;
|
||
return s_first_setting(); /* 回绕 */
|
||
}
|
||
|
||
/* SC_PRESET 行取「选项表 + 数量」:字体行(font.file + font.class_*)用运行时动态数组
|
||
* (TF 卡 MeowOS/fonts/<family>/ 扫描结果,见字体区 font_presets/font_presets_n),
|
||
* 其余行用编译期静态表(r->presets / r->preset_n)。 */
|
||
static const char *const *s_row_presets(const srow_t *r, int *n)
|
||
{
|
||
if (r && r->key && (!strcmp(r->key, "font.file") || !strncmp(r->key, "font.class_", 11))) {
|
||
if (n) *n = font_presets_n;
|
||
return font_presets;
|
||
}
|
||
if (n) *n = r->preset_n;
|
||
return r->presets;
|
||
}
|
||
|
||
/* 滚动:纯范围钳制到 [0, 内容高度-视口高](列表拖动滚动用;不强制选中行可见) */
|
||
static int s_clamp_scroll_range(int scroll)
|
||
{
|
||
int total = s_vis_count() * SROW_H;
|
||
if (total <= SET_CONTENT_H) return 0;
|
||
int max_scroll = total - SET_CONTENT_H;
|
||
if (scroll < 0) scroll = 0;
|
||
if (scroll > max_scroll) scroll = max_scroll;
|
||
return scroll;
|
||
}
|
||
|
||
/* 滚动:保证当前选中行在 [SET_CONTENT_Y, SET_CONTENT_BOT] 内 */
|
||
static int s_clamp_scroll(int sel, int scroll)
|
||
{
|
||
scroll = s_clamp_scroll_range(scroll);
|
||
int pos = s_vis_of(sel);
|
||
if (pos < 0) pos = s_vis_of(s_first_setting());
|
||
int top = pos * SROW_H;
|
||
int bot = top + SROW_H;
|
||
if (top < scroll) scroll = top; /* 选中行滑出顶部 → 上滚 */
|
||
if (bot > scroll + SET_CONTENT_H) scroll = bot - SET_CONTENT_H; /* 滑出底部 → 下滚 */
|
||
return s_clamp_scroll_range(scroll);
|
||
}
|
||
|
||
/* 行 y 落在 content 坐标 cy 时对应的行号(压缩位置 → SMENU 下标);越界返回 -1 */
|
||
static int s_row_at_content_y(int cy)
|
||
{
|
||
if (cy < 0) return -1;
|
||
int pos = cy / SROW_H;
|
||
if (pos >= s_vis_count()) return -1;
|
||
return s_vis_at(pos);
|
||
}
|
||
|
||
/* ---- 值格式化/展示 ---- */
|
||
static void s_fmt_time(int sec, char *buf, int len)
|
||
{
|
||
if (sec <= 0) { snprintf(buf, len, "常亮"); return; }
|
||
if (sec < 60) { snprintf(buf, len, "%d 秒", sec); return; }
|
||
if (sec < 3600) { snprintf(buf, len, "%d 分", sec / 60); return; }
|
||
snprintf(buf, len, "%d 时", sec / 3600);
|
||
}
|
||
/* B2b 主题时段:分钟(0-1439) → "HH:MM"(如 1140→"19:00")。注意与 s_fmt_time 的秒语义区分。 */
|
||
static void s_fmt_minutes(int mins, char *buf, int len)
|
||
{
|
||
if (mins < 0) mins = 0;
|
||
if (mins > 1439) mins = 1439;
|
||
snprintf(buf, len, "%02d:%02d", mins / 60, mins % 60);
|
||
}
|
||
/* 息屏时间:秒 → 最近档位索引(显示滑动位置用;等值优先,跨档取较近档,平手取高档) */
|
||
static int s_time_tier_idx(int sec)
|
||
{
|
||
if (sec <= s_time_tiers[0]) return 0;
|
||
for (int i = 1; i < S_TIME_TIERS_N; i++) {
|
||
if (sec == s_time_tiers[i]) return i;
|
||
if (sec < s_time_tiers[i]) {
|
||
int lo = s_time_tiers[i - 1], hi = s_time_tiers[i];
|
||
return (sec - lo < hi - sec) ? i - 1 : i;
|
||
}
|
||
}
|
||
return S_TIME_TIERS_N - 1;
|
||
}
|
||
/* 息屏时间:档位索引 → 秒(落盘写给 display.screen_off_timeout 的实值) */
|
||
static int s_time_tier_val(int idx)
|
||
{
|
||
if (idx < 0) idx = 0;
|
||
if (idx >= S_TIME_TIERS_N) idx = S_TIME_TIERS_N - 1;
|
||
return s_time_tiers[idx];
|
||
}
|
||
static void s_fmt_value(const srow_t *r, char *buf, int len)
|
||
{
|
||
int v = 0;
|
||
if (meow_settings_get_int(r->key, &v) != 0) { snprintf(buf, len, "-"); return; }
|
||
if (!strcmp(r->key, "display.screen_off_timeout")) { s_fmt_time(v, buf, len); return; }
|
||
/* B2b 深色时段(分钟 0-1439 → "HH:MM");start==end 视为未配置,显示「未设置」 */
|
||
if (!strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) {
|
||
int s2 = 0, e2 = 0;
|
||
meow_settings_get_int("display.theme_dark_start", &s2);
|
||
meow_settings_get_int("display.theme_dark_end", &e2);
|
||
if (s2 == e2) { snprintf(buf, len, "未设置"); return; }
|
||
s_fmt_minutes(v, buf, len);
|
||
return;
|
||
}
|
||
/* 长按弹菜单/强关:毫秒换算成秒(0.5 步进,如 "2.5 秒") */
|
||
if (!strcmp(r->key, "power.longpress_menu_ms") || !strcmp(r->key, "power.forceoff_ms")) {
|
||
if (v % 1000 == 0) snprintf(buf, len, "%d 秒", v / 1000);
|
||
else snprintf(buf, len, "%.1f 秒", v / 1000.0);
|
||
return;
|
||
}
|
||
snprintf(buf, len, "%d", v);
|
||
}
|
||
static void s_display_value(const srow_t *r, char *buf, int len)
|
||
{
|
||
if (r->ctrl == SC_ENUM) {
|
||
int idx = 0;
|
||
if (meow_settings_get_enum(r->key, &idx) != 0) idx = 0;
|
||
if (meow_settings_enum_label(r->key, idx, buf, len) != 0) snprintf(buf, len, "-");
|
||
return;
|
||
}
|
||
/* SC_PRESET */
|
||
char cur[MEOW_SET_MAX_LEN];
|
||
if (meow_settings_get_str(r->key, cur, sizeof cur) != 0) { snprintf(buf, len, "-"); return; }
|
||
/* M2.5x 多字体:class 行空值 = 跟随全局 font.file */
|
||
if (r->key && !strncmp(r->key, "font.class_", 11) && !cur[0]) {
|
||
snprintf(buf, len, "跟随全局");
|
||
return;
|
||
}
|
||
int pn = 0;
|
||
const char *const *pps = s_row_presets(r, &pn);
|
||
for (int i = 0; i < pn; i++) {
|
||
if (!strcmp(pps[i], cur)) {
|
||
snprintf(buf, len, "%s", (r->preset_labels && r->preset_labels[i][0]) ? r->preset_labels[i] : cur);
|
||
return;
|
||
}
|
||
}
|
||
snprintf(buf, len, "%s", cur);
|
||
}
|
||
|
||
/* ===== M2.39/M2.40 网络/WiFi/蓝牙(设置页「网络」分组)=====
|
||
* 背景:launcher 已读 wlan0 operstate(wlan_connected)/ hci_powered(hciconfig UP)。
|
||
* 本组在设置页内做 WiFi/蓝牙开关 + 当前连接 SSID + 已保存网络 + 附近WiFi(WiFi 扫描,
|
||
* 按 BSSID 去重)+ 蓝牙设备(已配对 + 附近扫描发现)。读取全部经带超时 popen(wpa_cli/
|
||
* bluetoothctl 在 supplicant/BlueZ 未跑时可能挂起),绝不阻塞主循环;WiFi/蓝牙扫描均异步
|
||
* (先触发、后读缓存/结果),超时余量。开关/计数/列表只读展示走 meow_settings 新键
|
||
* network.wifi_enabled/network.bluetooth_enabled。
|
||
* M2.40:wpa_cli 对非 ASCII(中文)SSID 输出转义文字 \xNN,新增 wpa_decode_ssid() 还原成
|
||
* 真实 UTF-8 字节;net_wifi_scan 按 BSSID 去重(同 BSSID 只留一条,优先具名);蓝牙附近
|
||
* 扫描经后台有界 bluetoothctl 写 /tmp 缓存(system() 尾部 & 立即返回,不阻塞主循环)。
|
||
* 设计:绘制路径只读缓存(g_net_*,s_net_refresh 刷新一次),下拉打开时才重新 popen 解析,
|
||
* 避免绘制热点反复 spawn。 */
|
||
|
||
/* 带超时跑一条命令并读 stdout(防 wpa_supplicant/BlueZ 未跑时命令挂起)。
|
||
* 返回 malloc 字符串(调用方 free);超时/失败返回 NULL;子进程超时被 SIGKILL。 */
|
||
static char *popen_read_tmo(int tmo_ms, const char *cmd)
|
||
{
|
||
int fds[2];
|
||
if (pipe(fds) != 0) return NULL;
|
||
pid_t pid = fork();
|
||
if (pid < 0) { close(fds[0]); close(fds[1]); return NULL; }
|
||
if (pid == 0) {
|
||
close(fds[0]);
|
||
dup2(fds[1], 1);
|
||
dup2(fds[1], 2);
|
||
close(fds[1]);
|
||
execl("/bin/sh", "sh", "-c", cmd, (char *)NULL);
|
||
_exit(127);
|
||
}
|
||
close(fds[1]);
|
||
int fd = fds[0];
|
||
int fl = fcntl(fd, F_GETFL, 0);
|
||
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||
char *out = NULL; size_t cap = 0, len = 0;
|
||
char tmp[256];
|
||
int done = 0;
|
||
unsigned long long deadline = now_ms() + tmo_ms;
|
||
while (!done) {
|
||
struct pollfd pfd = { fd, POLLIN, 0 };
|
||
int pr = poll(&pfd, 1, 25);
|
||
if (pr < 0) break;
|
||
if (pr == 0) { if (now_ms() > deadline) break; continue; }
|
||
ssize_t r = read(fd, tmp, sizeof tmp);
|
||
if (r > 0) {
|
||
if (len + (size_t)r + 1 > cap) {
|
||
size_t nc = cap ? cap * 2 : 512;
|
||
char *nn = realloc(out, nc);
|
||
if (!nn) { free(out); out = NULL; break; }
|
||
out = nn; cap = nc;
|
||
}
|
||
memcpy(out + len, tmp, (size_t)r); len += (size_t)r; out[len] = 0;
|
||
} else if (r == 0) {
|
||
done = 1;
|
||
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||
done = 1;
|
||
}
|
||
}
|
||
/* 子进程收尸:已退出则不杀;仍在跑(读到 EOF 前超时)则 SIGKILL,防僵尸。 */
|
||
int status = 0;
|
||
pid_t rp = waitpid(pid, &status, WNOHANG);
|
||
if (rp == 0) { kill(pid, SIGKILL); waitpid(pid, &status, 0); }
|
||
close(fd);
|
||
return out;
|
||
}
|
||
|
||
/* 已保存网络 / 扫描结果 / 蓝牙设备名称列表(用于下拉展示与连接动作) */
|
||
typedef struct { int id; char name[48]; } meow_net_t;
|
||
|
||
/* 十六进制字符值('0'-'9'/'a'-'f'/'A'-'F');非法返回 -1。避免依赖 ctype.h(musl/内存受限)。
|
||
* M2.40:wpa_cli 对非 ASCII SSID 输出 "\xNN" 转义文字(`\`+x+两位hex 的纯文字,非真字节),
|
||
* 需先还原成真实 UTF-8 字节才能进界面文本。 */
|
||
static int hex_val(char c)
|
||
{
|
||
if (c >= '0' && c <= '9') return c - '0';
|
||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||
return -1;
|
||
}
|
||
|
||
/* wpa_decode_ssid:把 wpa_cli 的 "\xNN" 转义文字还原成真实字节(\xe5\xa4\xa9… = "天…")。
|
||
* 遇 `\x`+两位合法 hex → 拼一字节;普通字符原样拷贝;dlen 为 dst 容量(含 NUL)。
|
||
* 无 \x 的纯 ASCII 名(如 "<ssid>")原样通过。 */
|
||
static void wpa_decode_ssid(const char *src, char *dst, int dlen)
|
||
{
|
||
int o = 0;
|
||
while (*src && o < dlen - 1) {
|
||
if (src[0] == '\\' && src[1] == 'x' &&
|
||
hex_val(src[2]) >= 0 && hex_val(src[3]) >= 0) {
|
||
dst[o++] = (char)((hex_val(src[2]) << 4) | hex_val(src[3]));
|
||
src += 4;
|
||
} else {
|
||
dst[o++] = *src++;
|
||
}
|
||
}
|
||
dst[o] = 0;
|
||
}
|
||
|
||
/* 通用选择器下拉(定义在 s_net_open 之后);网络列表复用同款模态 */
|
||
static int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur);
|
||
|
||
|
||
/* 取 WiFi 当前连接 SSID(wpa_cli -i wlan0 status 的 ssid= 行);未连接返回空串 */
|
||
static void net_wifi_ssid(char *buf, int len)
|
||
{
|
||
buf[0] = 0;
|
||
if (!wlan_connected()) return;
|
||
char *o = popen_read_tmo(1200, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 status 2>/dev/null");
|
||
if (!o) return;
|
||
char *s = strstr(o, "ssid=");
|
||
if (s) {
|
||
s += 5;
|
||
char *e = s; while (*e && *e != '\n' && *e != '\r') e++;
|
||
int l = (int)(e - s);
|
||
/* M2.40:status 的 ssid= 同样可能是中文 \xNN 转义文字,还原成真实 UTF-8 字节 */
|
||
char raw[128];
|
||
if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
|
||
memcpy(raw, s, (size_t)l); raw[l] = 0;
|
||
wpa_decode_ssid(raw, buf, len);
|
||
}
|
||
free(o);
|
||
}
|
||
|
||
/* 当前 WiFi 连接详情(点开「WiFi连接」行展示):解析 wpa_cli status + 路由网关。
|
||
* 填充 info[][48](调用方提供的二维缓冲),每行 "标签: 值",返回行数(≤max)。
|
||
* 当前SSID / AP MAC(bssid) / 本机MAC(address) / AP IP(默认网关) / 本机IP(ip_address) / 频道freq。
|
||
* 未连接返回 0(调用方显示"未连接")。绝不阻塞:popen 带超时。 */
|
||
static int net_wifi_info(char (*info)[48], int max)
|
||
{
|
||
if (!wlan_connected()) return 0;
|
||
char *o = popen_read_tmo(1500,
|
||
"PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 status 2>/dev/null; ip route 2>/dev/null | grep default");
|
||
if (!o) return 0;
|
||
char ssid[64] = "", bssid[24] = "", addr[24] = "", ip[32] = "", freq[16] = "", gw[32] = "";
|
||
char *p = o;
|
||
while (p && *p) {
|
||
char *e = strchr(p, '\n'); if (e) *e = 0;
|
||
if (!strncmp(p, "ssid=", 5)) { char raw[128]; snprintf(raw, sizeof raw, "%s", p + 5); wpa_decode_ssid(raw, ssid, sizeof ssid); }
|
||
else if (!strncmp(p, "bssid=", 6)) snprintf(bssid, sizeof bssid, "%s", p + 6);
|
||
else if (!strncmp(p, "address=", 8)) snprintf(addr, sizeof addr, "%s", p + 8);
|
||
else if (!strncmp(p, "ip_address=", 11)) snprintf(ip, sizeof ip, "%s", p + 11);
|
||
else if (!strncmp(p, "freq=", 5)) snprintf(freq, sizeof freq, "%s", p + 5);
|
||
else if (!strncmp(p, "default via ", 11)) { char *s = p + 11; char *g = s; while (*g && *g != ' ') g++; snprintf(gw, sizeof gw, "%s", s); }
|
||
p = e ? e + 1 : NULL;
|
||
}
|
||
free(o);
|
||
int n = 0;
|
||
if (n < max && ssid[0]) snprintf(info[n++], 48, "SSID: %s", ssid);
|
||
if (n < max && bssid[0]) snprintf(info[n++], 48, "AP MAC: %s", bssid);
|
||
if (n < max && addr[0]) snprintf(info[n++], 48, "本机MAC: %s", addr);
|
||
if (n < max && gw[0]) snprintf(info[n++], 48, "AP IP: %s", gw);
|
||
if (n < max && ip[0]) snprintf(info[n++], 48, "本机IP: %s", ip);
|
||
if (n < max && freq[0]) snprintf(info[n++], 48, "频道: %s MHz", freq);
|
||
return n;
|
||
}
|
||
|
||
/* 已保存网络(wpa_cli -i wlan0 list_networks):首行为表头,其余每行 "<id>\t<ssid>\t<bssid>\t<flag>"。
|
||
* 填 out[i].id/.name(SSID,中文经 wpa_decode_ssid 还原 \xNN),返回项数(≤max)。 */
|
||
static int net_wifi_saved(meow_net_t *out, int max)
|
||
{
|
||
int n = 0;
|
||
char *o = popen_read_tmo(1500, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 list_networks 2>/dev/null");
|
||
if (!o) return 0;
|
||
char *line = strchr(o, '\n');
|
||
if (line) line++; /* 跳表头 */
|
||
while (line && *line && n < max) {
|
||
char *eol = strchr(line, '\n');
|
||
if (eol) *eol = 0;
|
||
char *p = line; while (*p == ' ' || *p == '\t') p++;
|
||
if (*p && *p != '#') {
|
||
int id = atoi(p);
|
||
char *sp = p; while (*sp && *sp != ' ' && *sp != '\t') sp++;
|
||
while (*sp == ' ' || *sp == '\t') sp++;
|
||
char *se = sp; while (*se && *se != ' ' && *se != '\t' && *se != '\n' && *se != '\r') se++;
|
||
if (sp != se) {
|
||
/* M2.40:list_networks 对中文 SSID 输出 \xNN 转义文字(如 \xe5\xa4\xa9… = "天…"),
|
||
* 先拷进临时缓冲再还原成真实 UTF-8 字节存入 name,否则界面显示超长乱码。 */
|
||
char raw[160];
|
||
int l = (int)(se - sp);
|
||
if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
|
||
memcpy(raw, sp, (size_t)l); raw[l] = 0;
|
||
out[n].id = id;
|
||
wpa_decode_ssid(raw, out[n].name, sizeof out[n].name);
|
||
n++;
|
||
}
|
||
}
|
||
line = eol ? eol + 1 : NULL;
|
||
}
|
||
free(o);
|
||
return n;
|
||
}
|
||
|
||
/* WiFi 扫描触发(异步:wpa_cli scan 只是通知 supplicant 发起扫描,立即返回)。
|
||
* 结果稍后经 net_wifi_scan() 读。 */
|
||
static void net_wifi_scan_trigger(void)
|
||
{
|
||
if (!wlan_connected()) return;
|
||
system("PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 scan 2>/dev/null");
|
||
}
|
||
|
||
/* 读 WiFi 扫描结果(wpa_cli -i wlan0 scan_results):首行列头,数据行形如
|
||
* <bssid> <freq> <signal> [flags..] [ssid...]
|
||
* flags 恒以 '[' 起;SSID 为 flags 后的整段余量(可含空格;为空=隐藏网络)。
|
||
* M2.40:同一 AP 常出现多条(不同频段/空 SSID/重名),按 BSSID 去重——每个 BSSID 只留一条,
|
||
* 优先采用含完整 SSID 的行(后到且有 SSID 则覆盖先到的空 SSID 行),隐藏网络显示 "(隐藏网络)"。
|
||
* 填 out[i].id=-1;name 存 SSID(或隐藏标记)。返回项数(≤max,且 ≤S_NET_MAX)。 */
|
||
static int net_wifi_scan(meow_net_t *out, int max)
|
||
{
|
||
int n = 0;
|
||
if (max > S_NET_MAX) max = S_NET_MAX;
|
||
char *o = popen_read_tmo(2500, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 scan_results 2>/dev/null");
|
||
if (!o) return 0;
|
||
char seen_bssid[S_NET_MAX][24];
|
||
int named[S_NET_MAX] = {0};
|
||
char *line = strchr(o, '\n');
|
||
if (line) line++;
|
||
while (line && *line && n < max) {
|
||
char *eol = strchr(line, '\n');
|
||
if (eol) *eol = 0;
|
||
char *p = line; while (*p == ' ' || *p == '\t') p++;
|
||
if (*p) {
|
||
/* 首 token = bssid */
|
||
char bssid[24] = "";
|
||
{ char *b = p; while (*p && *p != ' ' && *p != '\t') p++;
|
||
int l = (int)(p - b); if (l > 22) l = 22; memcpy(bssid, b, (size_t)l); bssid[l] = 0; }
|
||
/* 找 flags(首个以 '[' 起始的 token),之后(trim 掉前导空白)才是 SSID */
|
||
const char *ssid = "";
|
||
char *br = strchr(p, '[');
|
||
if (br) {
|
||
char *q = br;
|
||
while (*q) {
|
||
while (*q == ' ' || *q == '\t') q++;
|
||
if (*q == '[') { while (*q && *q != ' ' && *q != '\t') q++; }
|
||
else break;
|
||
}
|
||
ssid = q;
|
||
while (*ssid == ' ' || *ssid == '\t') ssid++;
|
||
}
|
||
/* SSID 去尾部空白/换行 */
|
||
char *se = (char *)ssid;
|
||
while (*se && *se != '\n' && *se != '\r') se++;
|
||
while (se > ssid && (se[-1] == ' ' || se[-1] == '\t')) se--;
|
||
int has = (se > ssid && *ssid);
|
||
/* 去重:同一 BSSID 只留一条,优先具名 SSID */
|
||
int idx = -1;
|
||
for (int j = 0; j < n; j++) if (!strcmp(seen_bssid[j], bssid)) { idx = j; break; }
|
||
if (idx < 0) {
|
||
if (n >= max) { line = eol ? eol + 1 : NULL; continue; }
|
||
idx = n++;
|
||
strncpy(seen_bssid[idx], bssid, sizeof seen_bssid[idx] - 1);
|
||
seen_bssid[idx][sizeof seen_bssid[idx] - 1] = 0;
|
||
named[idx] = has;
|
||
out[idx].id = -1;
|
||
if (has) {
|
||
/* M2.41:扫描结果 SSID 也可能是 wpa_cli 的 \xNN 转义(中文),还原成真 UTF-8 */
|
||
char raw[96];
|
||
int l = (int)(se - ssid); if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
|
||
memcpy(raw, ssid, (size_t)l); raw[l] = 0;
|
||
wpa_decode_ssid(raw, out[idx].name, sizeof out[idx].name);
|
||
} else {
|
||
snprintf(out[idx].name, sizeof out[idx].name, "(隐藏网络)");
|
||
}
|
||
} else if (has && !named[idx]) {
|
||
/* 同 BSSID 先出现无 SSID 行、后有具名行 → 用后者 SSID 覆盖(解码 \xNN) */
|
||
char raw[96];
|
||
int l = (int)(se - ssid); if (l > (int)sizeof raw - 1) l = (int)sizeof raw - 1;
|
||
memcpy(raw, ssid, (size_t)l); raw[l] = 0;
|
||
wpa_decode_ssid(raw, out[idx].name, sizeof out[idx].name);
|
||
named[idx] = 1;
|
||
}
|
||
}
|
||
line = eol ? eol + 1 : NULL;
|
||
}
|
||
free(o);
|
||
return n;
|
||
}
|
||
|
||
/* 蓝牙已配对设备(bluetoothctl paired-devices):每行 "Device <MAC> <Name>"。
|
||
* hci 未上电则返回 0(避免 spawn bluetoothctl 白跑)。 */
|
||
static int net_bt_paired(meow_net_t *out, int max)
|
||
{
|
||
if (!hci_powered()) return 0;
|
||
int n = 0;
|
||
char *o = popen_read_tmo(1800, "bluetoothctl paired-devices 2>/dev/null");
|
||
if (!o) return 0;
|
||
char *line = o;
|
||
while (line && *line && n < max) {
|
||
char *eol = strchr(line, '\n');
|
||
if (eol) *eol = 0;
|
||
char *p = strstr(line, "Device ");
|
||
if (p) {
|
||
p += 7;
|
||
char *mac = p; while (*mac && *mac != ' ' && *mac != '\t') mac++;
|
||
char *nm = mac; while (*nm == ' ' || *nm == '\t') nm++;
|
||
/* 设备名每行即名称 token;名称可含空格,这里取整行剩余部分(含 MAC 后的名字) */
|
||
if (*nm)
|
||
snprintf(out[n].name, sizeof out[n].name, "%s", nm);
|
||
else {
|
||
int ml = (int)(mac - p); if (ml > 47) ml = 47;
|
||
memcpy(out[n].name, p, (size_t)ml); out[n].name[ml] = 0;
|
||
}
|
||
out[n].id = -1;
|
||
n++;
|
||
}
|
||
line = eol ? eol + 1 : NULL;
|
||
}
|
||
free(o);
|
||
return n;
|
||
}
|
||
|
||
/* ---- 蓝牙附近设备(未配对扫描发现;异步,不阻塞主循环)----
|
||
* M2.40:用户要的「附近的设备」= 蓝牙扫描发现(含未配对),而不只是已配对。扫描较慢(2-6s),
|
||
* 故后台有界驱动 bluetoothctl:scan on → sleep 4 → scan off → devices → quit,输出写 /tmp 缓存;
|
||
* system() 因尾部 & 立即返回(约 5s 后自灭),绝不卡死主循环。读取端 net_bt_scan 只解析缓存
|
||
* (bluetoothctl `devices` 行 "Device <MAC> <Name>",含已配对+未配对发现,去重)。 */
|
||
static long g_bt_scan_ms = 0; /* 最近一次发起扫描的 now_ms();0=从未 */
|
||
|
||
static void net_bt_scan_trigger(void)
|
||
{
|
||
if (!hci_powered()) return;
|
||
long t = (long)now_ms();
|
||
if (g_bt_scan_ms && t - g_bt_scan_ms < 7000) return; /* 7s 内不重复触发,防堆积 */
|
||
g_bt_scan_ms = t;
|
||
system("(sleep 1; echo 'scan on'; sleep 4; echo 'scan off'; echo 'devices'; echo 'quit') | bluetoothctl > /tmp/meow_bt_scan.txt 2>&1 &");
|
||
}
|
||
|
||
/* 读蓝牙附近设备缓存:解析 bluetoothctl `devices` 输出("Device <MAC> <Name>",去重;含已配对
|
||
* 与未配对但已发现的设备)。缓存不存在/为空则退回已配对列表(保证至少展示已知设备)。
|
||
* 返回项数(≤max,且 ≤S_NET_MAX)。 */
|
||
static int net_bt_scan(meow_net_t *out, int max)
|
||
{
|
||
if (!hci_powered()) return 0;
|
||
if (max > S_NET_MAX) max = S_NET_MAX;
|
||
int n = 0;
|
||
FILE *f = fopen("/tmp/meow_bt_scan.txt", "r");
|
||
if (f) {
|
||
char line[256];
|
||
char seen[S_NET_MAX][24];
|
||
while (fgets(line, sizeof line, f)) {
|
||
char *p = line; while (*p == ' ' || *p == '\t') p++;
|
||
/* 认两种行:`devices` 输出的 "Device <MAC> <Name>";发现会话里的 "[NEW] Device <MAC> <Name>"。
|
||
* 跳过 [CHG]/[DEL] 等属性/移除行(无稳定名称)。 */
|
||
char *dv = NULL;
|
||
if (!strncmp(p, "Device ", 7)) dv = p + 7;
|
||
else if (!strncmp(p, "[NEW] Device ", 13)) dv = p + 13;
|
||
if (!dv) continue;
|
||
char *mac = dv; while (*mac && *mac != ' ' && *mac != '\t' && *mac != '\n' && *mac != '\r') mac++;
|
||
int ml = (int)(mac - dv);
|
||
if (ml < 6 || !memchr(dv, ':', (size_t)ml)) continue; /* 需像 MAC(含冒号);过滤属性行 */
|
||
/* 去重(同 MAC 只留一条,取首个出现) */
|
||
int dup = 0;
|
||
for (int j = 0; j < n; j++) if (!strncmp(seen[j], dv, (size_t)ml)) { dup = 1; break; }
|
||
if (dup) continue;
|
||
if (n >= max) break;
|
||
char *nm = mac; while (*nm == ' ' || *nm == '\t') nm++;
|
||
char *e = nm; while (*e && *e != '\n' && *e != '\r') e++;
|
||
*e = 0; /* 去行尾换行 */
|
||
strncpy(seen[n], dv, (size_t)ml); seen[n][ml] = 0;
|
||
if (*nm)
|
||
snprintf(out[n].name, sizeof out[n].name, "%s", nm);
|
||
else {
|
||
int l = ml; if (l > 47) l = 47;
|
||
memcpy(out[n].name, dv, (size_t)l); out[n].name[l] = 0;
|
||
}
|
||
out[n].id = -1;
|
||
n++;
|
||
}
|
||
fclose(f);
|
||
if (n > 0) return n;
|
||
}
|
||
return net_bt_paired(out, max); /* 无扫描缓存 → 退回已配对列表 */
|
||
}
|
||
|
||
/* ---- 蓝牙角色判定(M2.48:状态栏多态 + 设置页"蓝牙模式"行)----
|
||
* 返回 BT_ROLE_*;desc 非空则写中文描述(供设置页只读行展示)。全部 popen 带超时,绝不阻塞。
|
||
* 判定(best-effort,蓝牙栈=BlueZ 5.54):
|
||
* 关 :hci0 不存在或 DOWN(hci_powered)。
|
||
* 开未连接:hciconfig UP 但 hcitool con 无 ACL 连接。
|
||
* 主模式 :有 ACL 连接(连了外围设备如耳机);设备侧 A2DP sink 命中 → 主。
|
||
* 从模式 :有 ACL 连接且设备侧 A2DP source 命中(远端接入,设备被当耳机用)。
|
||
* 注:master/slave 依赖 bluealsa-cli a2dp list 端点角色,其输出随固件格式不同;解析不到时,
|
||
* 有 ACL 一律按「主模式」兜底(仍可靠区分 关/开未连接/已连接三态)。若真机 a2dp 角色
|
||
* 与注释相反,只需交换 a2dp_side() 的 1/2 返回值即可。 */
|
||
static void copy_utf8_trunc(char *dst, int dlen, const char *src)
|
||
{
|
||
int o = 0;
|
||
while (*src && o < dlen - 1) {
|
||
unsigned char c = (unsigned char)*src;
|
||
int nb = 1;
|
||
if ((c & 0xE0) == 0xC0) nb = 2;
|
||
else if ((c & 0xF0) == 0xE0) nb = 3;
|
||
else if ((c & 0xF8) == 0xF0) nb = 4;
|
||
if (o + nb > dlen - 1) break; /* 放不下整个字符就停(不截半字符) */
|
||
for (int k = 0; k < nb; k++) { if (!src[k]) break; dst[o++] = src[k]; }
|
||
src += nb;
|
||
}
|
||
dst[o] = 0;
|
||
}
|
||
|
||
/* A2DP 段端点角色:1=设备侧 sink(主模式),2=设备侧 source(从模式),-1=无法判定。
|
||
* 宽松匹配:同段内出现 connected/running 且出现对应角色词。优先认 BlueALSA 对象路径
|
||
* a2dpsnk/a2dpsrc(跨版本稳定),再退到 source/sink/source 单词。 */
|
||
static int a2dp_side(const char *sec)
|
||
{
|
||
if (!sec) return -1;
|
||
if (!strstr(sec, "connected") && !strstr(sec, "running")) return -1;
|
||
int src = (strstr(sec, "a2dpsrc") || strstr(sec, "source") || strstr(sec, "Source")) ? 1 : 0;
|
||
int snk = (strstr(sec, "a2dpsnk") || strstr(sec, "sink") ||
|
||
strstr(sec, "Sink") || strstr(sec, "target")) ? 1 : 0;
|
||
if (src && !snk) return 2; /* 设备侧 source → 被当耳机(从) */
|
||
if (snk && !src) return 1; /* 设备侧 sink → 送流给外围(主) */
|
||
return -1;
|
||
}
|
||
|
||
/* 蓝牙角色:见上方注释。一次 popen 拿 ACL + A2DP + 已连设备名三类信息,避免反复 spawn。 */
|
||
static int bt_role(char *desc, int len)
|
||
{
|
||
if (desc && len > 0) desc[0] = 0;
|
||
if (!g_sys.bt_seen || !hci_powered()) {
|
||
if (desc && len > 0) snprintf(desc, len, "关闭");
|
||
return BT_ROLE_OFF;
|
||
}
|
||
/* 快的放前(HCI 连接、A2DP 端点),bluetoothctl 最慢放最后;整体超时防挂起(超时 SIGKILL)。 */
|
||
char *o = popen_read_tmo(1800,
|
||
"PATH=/bin:/sbin:/usr/bin:/usr/sbin hcitool con 2>/dev/null; "
|
||
"echo '===A2DP==='; bluealsa-cli a2dp list 2>/dev/null; "
|
||
"echo '===NAMES==='; printf 'devices Connected\\nquit\\n' | bluetoothctl 2>/dev/null");
|
||
if (!o) { /* 探测失败:保守按「开未连接」 */
|
||
if (desc && len > 0) snprintf(desc, len, "开启未连接");
|
||
return BT_ROLE_ON_NO_CONN;
|
||
}
|
||
/* 按标记分节(把标记首字符置 0 切断) */
|
||
char *s_acl = o;
|
||
char *s_a2dp = NULL, *s_names = NULL;
|
||
char *p = strstr(o, "===A2DP===");
|
||
if (p) { *p = 0; s_a2dp = p + 10; } /* "===A2DP===" 长 10 */
|
||
if (s_a2dp) { char *q = strstr(s_a2dp, "===NAMES==="); if (q) { *q = 0; s_names = q + 11; } }
|
||
else { char *q = strstr(o, "===NAMES==="); if (q) { *q = 0; s_names = q + 11; } }
|
||
/* ACL:hcitool con 的连接行含 "ACL <MAC>"(如 " < ACL 00:1A:.. handle.. state.. lm MASTER") */
|
||
int has_acl = 0;
|
||
{
|
||
char *line = s_acl;
|
||
while (line && *line) {
|
||
char *eol = strchr(line, '\n'); if (eol) *eol = 0;
|
||
if (strstr(line, "ACL") && strchr(line, ':')) { has_acl = 1; break; }
|
||
line = eol ? eol + 1 : NULL;
|
||
}
|
||
}
|
||
/* 已连设备名(bluetoothctl `devices Connected`:"Device <MAC> <Name>",取首个)。
|
||
* 定长 22:配合「从模式·被连(耳机…」前缀(UTF-8 约 25B)仍落在展示缓冲 48B 内。
|
||
* 同时也作「已连接」的第二证据(hcitool 缺失时 bluetoothctl 兜底)。 */
|
||
char dev[22] = "";
|
||
int has_conn = has_acl;
|
||
if (s_names) {
|
||
char *line = s_names;
|
||
while (line && *line) {
|
||
char *eol = strchr(line, '\n'); if (eol) *eol = 0;
|
||
char *q = line; while (*q == ' ' || *q == '\t') q++;
|
||
if (!strncmp(q, "Device ", 7)) {
|
||
has_conn = 1; /* bluetoothctl 也确认有设备已连 */
|
||
if (!dev[0]) {
|
||
q += 7;
|
||
char *mac = q; while (*mac && *mac != ' ' && *mac != '\t' &&
|
||
*mac != '\n' && *mac != '\r') mac++;
|
||
char *nm = mac; while (*nm == ' ' || *nm == '\t') nm++;
|
||
char *e = nm; while (*e && *e != '\n' && *e != '\r') e++;
|
||
*e = 0;
|
||
if (*nm) copy_utf8_trunc(dev, sizeof dev, nm);
|
||
else copy_utf8_trunc(dev, sizeof dev, q);
|
||
}
|
||
}
|
||
line = eol ? eol + 1 : NULL;
|
||
}
|
||
}
|
||
int side = a2dp_side(s_a2dp);
|
||
int role;
|
||
if (!has_conn) role = BT_ROLE_ON_NO_CONN;
|
||
else if (side == 2) role = BT_ROLE_SLAVE; /* 设备侧 source → 被当耳机 */
|
||
else role = BT_ROLE_MASTER; /* sink 或 a2dp 不明但有连接 → 主 */
|
||
if (desc && len > 0) {
|
||
if (role == BT_ROLE_ON_NO_CONN) snprintf(desc, len, "开启未连接");
|
||
else if (role == BT_ROLE_SLAVE) snprintf(desc, len, "从模式·被连(耳机%s%s)",
|
||
dev[0] ? " " : "", dev);
|
||
else snprintf(desc, len, "主模式·已连%s%s",
|
||
dev[0] ? " " : "", dev[0] ? dev : "设备");
|
||
}
|
||
free(o);
|
||
return role;
|
||
}
|
||
|
||
/* 节流重探蓝牙角色(约 5s 一次;force=1 立即)。结果写 g_sys.bt_role + g_bt_mode,
|
||
* 并同步 g_sys.bt_on/bt_seen,保证状态栏与本行一致。蓝牙关时免费(只 spawn hciconfig)。 */
|
||
static long g_bt_role_ms = 0; /* 最近一次 bt_role 探测的 now_ms();0=从未 */
|
||
static void bt_role_refresh(int force)
|
||
{
|
||
if (!force && g_bt_role_ms && (now_ms() - g_bt_role_ms) < 5000) return;
|
||
g_bt_role_ms = now_ms();
|
||
char d[48];
|
||
g_sys.bt_role = bt_role(d, sizeof d);
|
||
snprintf(g_bt_mode, sizeof g_bt_mode, "%s", d);
|
||
g_sys.bt_seen = access("/sys/class/bluetooth/hci0", F_OK) == 0;
|
||
g_sys.bt_on = (g_sys.bt_role != BT_ROLE_OFF) ? 1 : 0;
|
||
}
|
||
|
||
/* 网络信息缓存(绘制路径只读,s_net_refresh 刷新;下拉打开时才重新解析列表) */
|
||
static char g_net_ssid[64] = "未连接";
|
||
static int g_net_saved_n = 0;
|
||
static int g_net_scan_n = 0;
|
||
static int g_net_bt_n = 0;
|
||
|
||
/* 刷新网络信息缓存(进入设置页 / 网络动作后调用;含一次 WiFi 扫描触发) */
|
||
static void s_net_refresh(void)
|
||
{
|
||
if (wlan_connected()) {
|
||
char ss[64]; net_wifi_ssid(ss, sizeof ss);
|
||
if (ss[0]) snprintf(g_net_ssid, sizeof g_net_ssid, "已连接: %s", ss);
|
||
else snprintf(g_net_ssid, sizeof g_net_ssid, "已连接");
|
||
} else {
|
||
snprintf(g_net_ssid, sizeof g_net_ssid, "未连接");
|
||
}
|
||
net_wifi_scan_trigger(); /* 异步发起扫描,稍后附近/计数更接近真实 */
|
||
net_bt_scan_trigger(); /* M2.40:异步发起蓝牙附近扫描(写 /tmp 缓存,不阻塞) */
|
||
bt_role_refresh(1); /* M2.48:进入设置/网络动作时立即重探蓝牙角色,本行/状态栏最新 */
|
||
meow_net_t tmp[S_NET_MAX];
|
||
g_net_saved_n = net_wifi_saved(tmp, S_NET_MAX);
|
||
g_net_scan_n = net_wifi_scan(tmp, S_NET_MAX);
|
||
g_net_bt_n = net_bt_scan(tmp, S_NET_MAX); /* 附近(已配对+扫描发现) */
|
||
}
|
||
|
||
/* SC_INFO/SC_LIST 行的即时展示文本(读缓存;draw 路径不 popen) */
|
||
static int s_net_get(const srow_t *r, char *buf, int len)
|
||
{
|
||
switch (r->net_list) {
|
||
case NET_LIST_WIFI_SSID: snprintf(buf, len, "%s", g_net_ssid); return 0;
|
||
case NET_LIST_WIFI_SAVED: snprintf(buf, len, "%d 个网络", g_net_saved_n); return 0;
|
||
case NET_LIST_WIFI_SCAN: snprintf(buf, len, "%d 个附近WiFi", g_net_scan_n); return 0;
|
||
case NET_LIST_BT_NEARBY: snprintf(buf, len, "%d 台设备", g_net_bt_n); return 0;
|
||
case NET_LIST_BT_MODE: snprintf(buf, len, "%s", g_bt_mode); return 0;
|
||
case NET_LIST_ABOUT: snprintf(buf, len, "MeowOS v%s", g_meowos_version); return 0;
|
||
}
|
||
snprintf(buf, len, "-");
|
||
return 0;
|
||
}
|
||
|
||
/* 网络动作开关:wifi/bluetooth 开/关(写 wlan0/hci0;立即生效,异步刷新缓存)。
|
||
* 走 system(wifi_up.sh 自身后台化子进程、ip link/hciconfig 均为快 ioctl)。 */
|
||
static void s_net_toggle_wifi(int on)
|
||
{
|
||
if (on) system("PATH=/bin:/sbin:/usr/bin:/usr/sbin /usr/bin/wifi_up.sh");
|
||
else system("PATH=/bin:/sbin:/usr/bin:/usr/sbin ip link set wlan0 down 2>/dev/null");
|
||
}
|
||
static void s_net_toggle_bt(int on)
|
||
{
|
||
if (on) system("hciconfig hci0 up 2>/dev/null");
|
||
else system("hciconfig hci0 down 2>/dev/null");
|
||
}
|
||
|
||
/* 进入设置页时把 network.wifi_enabled/bluetooth_enabled 同步为真实硬件态,
|
||
* 让开关行反映 reality(仅同步内存 + 刷新缓存;不落盘 —— s_commit 的 save 才真正写)。
|
||
* settings 未 init/取值失败则不动(保默认)。 */
|
||
static void s_net_sync_from_sys(void)
|
||
{
|
||
int cur = 0;
|
||
if (meow_settings_get_bool("network.wifi_enabled", &cur) == 0) {
|
||
int real = wlan_connected() ? 1 : 0;
|
||
if (real != cur) meow_settings_set_bool("network.wifi_enabled", real);
|
||
}
|
||
if (meow_settings_get_bool("network.bluetooth_enabled", &cur) == 0) {
|
||
int real = hci_powered() ? 1 : 0;
|
||
if (real != cur) meow_settings_set_bool("network.bluetooth_enabled", real);
|
||
}
|
||
s_net_refresh();
|
||
}
|
||
|
||
/* M2.5x 系统设置「关于本机」:复用 s_dd_modal 弹只读设备信息(定义见下,供 s_net_open 分发)。
|
||
* 前置声明:s_net_open 在其定义之前调用它。 */
|
||
static void s_about_open(void);
|
||
|
||
/* 打开 SC_LIST 网络下拉(重新解析最新列表):已保存=可 select_network 连接;
|
||
* 附近WiFi(扫描)/蓝牙设备(附近扫描+已配对)=只读展示(选择即关闭;带密码/配对仍留后期)。
|
||
* 返回后刷新缓存。 */
|
||
static void s_net_open(const srow_t *r)
|
||
{
|
||
meow_net_t items[S_NET_MAX];
|
||
const char *txt[S_NET_MAX];
|
||
char back[S_NET_MAX][48];
|
||
int n = 0;
|
||
switch (r->net_list) {
|
||
case NET_LIST_WIFI_SSID: /* 点开「WiFi连接」→ 显示当前连接详情(只读) */
|
||
n = net_wifi_info(back, S_NET_MAX);
|
||
if (n == 0) { snprintf(back[0], sizeof back[0], "%s", wlan_connected() ? "已连接(无详情)" : "未连接"); n = 1; }
|
||
for (int i = 0; i < n; i++) txt[i] = back[i];
|
||
s_dd_modal(r->label, txt, n, -1); /* 只读展示 */
|
||
break;
|
||
case NET_LIST_WIFI_SAVED:
|
||
n = net_wifi_saved(items, S_NET_MAX);
|
||
if (n == 0) { snprintf(back[0], sizeof back[0], "无已保存网络"); txt[0] = back[0]; n = 1; items[0].id = -1; }
|
||
else for (int i = 0; i < n; i++) {
|
||
snprintf(back[i], sizeof back[i], "%s", items[i].name[0] ? items[i].name : "<未命名>");
|
||
txt[i] = back[i];
|
||
}
|
||
{
|
||
int pick = s_dd_modal(r->label, txt, n, -1); /* cur=-1:不预勾选 */
|
||
if (pick >= 0 && pick < n && items[pick].id >= 0) {
|
||
char cmd[160];
|
||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 select_network %d 2>/dev/null",
|
||
items[pick].id);
|
||
system(cmd);
|
||
}
|
||
}
|
||
break;
|
||
case NET_LIST_WIFI_SCAN:
|
||
net_wifi_scan_trigger();
|
||
n = net_wifi_scan(items, S_NET_MAX);
|
||
if (n == 0) { snprintf(back[0], sizeof back[0], "未扫描到附近网络"); txt[0] = back[0]; n = 1; }
|
||
else for (int i = 0; i < n; i++) { snprintf(back[i], sizeof back[i], "%s", items[i].name); txt[i] = back[i]; }
|
||
s_dd_modal(r->label, txt, n, -1); /* 只读展示 */
|
||
break;
|
||
case NET_LIST_BT_NEARBY:
|
||
net_bt_scan_trigger(); /* M2.40:异步发起/刷新附近扫描(读缓存;绝不阻塞主循环) */
|
||
n = net_bt_scan(items, S_NET_MAX);
|
||
if (n == 0) {
|
||
long t = (long)now_ms();
|
||
if (g_bt_scan_ms && t - g_bt_scan_ms < 6000)
|
||
snprintf(back[0], sizeof back[0], "扫描中...");
|
||
else
|
||
snprintf(back[0], sizeof back[0], "未发现附近设备");
|
||
txt[0] = back[0]; n = 1; items[0].id = -1;
|
||
}
|
||
else for (int i = 0; i < n; i++) { snprintf(back[i], sizeof back[i], "%s", items[i].name); txt[i] = back[i]; }
|
||
s_dd_modal(r->label, txt, n, -1); /* 只读展示 */
|
||
break;
|
||
case NET_LIST_ABOUT:
|
||
s_about_open(); /* M2.5x 系统设置「关于本机」:只读设备信息弹窗 */
|
||
return; /* 不上网/蓝牙缓存刷新(信息已现场读取) */
|
||
default:
|
||
return;
|
||
}
|
||
s_net_refresh();
|
||
}
|
||
|
||
/* M2.5x 系统设置「关于本机」:复用 s_dd_modal(与「WiFi连接状态」弹窗同款)列只读设备信息,
|
||
* 点卡片外/返回即关。信息源:settings system.hostname / g_meowos_version(=/etc/meowos_version) /
|
||
* uname -r / /proc/cpuinfo / /proc/meminfo / df / wpa_cli status / hciconfig / fb 分辨率+状态栏高。
|
||
* 只展示安全信息(不含任何密钥/凭证);实时读,短超时不阻塞主循环;行数 ≤12 适配下拉视口。
|
||
* 说明:launcher/bootmenu/watchdog/meow_v 未单独暴露版本号,统一以 /etc/meowos_version 的
|
||
* MeowOS 版本表示(与固件打包同源);无独立固件版本文件。 */
|
||
static void s_about_open(void)
|
||
{
|
||
char info[12][96];
|
||
const char *item[12];
|
||
int n = 0;
|
||
char host[64];
|
||
char *o;
|
||
|
||
/* 设备名(settings system.hostname;缺省 meowos) */
|
||
if (meow_settings_get_str("system.hostname", host, sizeof host) != 0 || !host[0])
|
||
snprintf(host, sizeof host, "meowos");
|
||
snprintf(info[n++], sizeof info[0], "设备名: %s", host);
|
||
|
||
/* MeowOS / 固件版本(/etc/meowos_version;与固件打包同源) */
|
||
snprintf(info[n++], sizeof info[0], "MeowOS: v%s", g_meowos_version);
|
||
|
||
/* 内核版本 */
|
||
o = popen_read_tmo(400, "PATH=/bin:/sbin:/usr/bin:/usr/sbin uname -r 2>/dev/null");
|
||
if (o) { for (char *p = o; *p; p++) if (*p == '\n' || *p == '\r') { *p = 0; break; }
|
||
snprintf(info[n++], sizeof info[0], "内核: %s", o[0] ? o : "-"); free(o); }
|
||
else snprintf(info[n++], sizeof info[0], "内核: -");
|
||
|
||
/* CPU(/proc/cpuinfo:Processor/CPU model/Hardware/system type 任取首非空行) */
|
||
o = popen_read_tmo(500,
|
||
"PATH=/bin:/sbin:/usr/bin:/usr/sbin awk -F: '/(model name|cpu model|system type|machine|hardware)/{print $2; exit}' /proc/cpuinfo 2>/dev/null");
|
||
if (o) { char *q = o; for (char *p = o; *p; p++) if (*p == '\n' || *p == '\r') { *p = 0; break; }
|
||
while (*q == ' ' || *q == '\t') q++;
|
||
snprintf(info[n++], sizeof info[0], "CPU: %s", q[0] ? q : "-"); free(o); }
|
||
else snprintf(info[n++], sizeof info[0], "CPU: -");
|
||
|
||
/* 内存(/proc/meminfo MemTotal / MemAvailable,KB→MB) */
|
||
long mtot = 0, mavail = 0;
|
||
o = popen_read_tmo(500, "PATH=/bin:/sbin:/usr/bin:/usr/sbin awk '/^MemTotal:/{print $2}/^MemAvailable:/{print $2}' /proc/meminfo 2>/dev/null");
|
||
if (o) { if (sscanf(o, "%ld%ld", &mtot, &mavail) < 2) mavail = 0; free(o); }
|
||
if (mtot > 0) snprintf(info[n++], sizeof info[0], "内存: %ld/%ld MB", mavail / 1024, mtot / 1024);
|
||
else snprintf(info[n++], sizeof info[0], "内存: -");
|
||
|
||
/* 板载 /usr/data:df -k → 可用/总量(MB) */
|
||
long dfree = 0, dtot = 0;
|
||
o = popen_read_tmo(600, "PATH=/bin:/sbin:/usr/bin:/usr/sbin df -k /usr/data 2>/dev/null | awk 'NR==2{print $4, $2}'");
|
||
if (o) { if (sscanf(o, "%ld%ld", &dfree, &dtot) < 2) dtot = 0; free(o); }
|
||
if (dtot > 0) snprintf(info[n++], sizeof info[0], "板载: %ld/%ld MB", dfree / 1024, dtot / 1024);
|
||
else snprintf(info[n++], sizeof info[0], "板载: -");
|
||
|
||
/* TF 卡 /usr/data/mnt/sd_0:卡在(/proc/mounts 含该挂载点)则 df,否则「无卡」 */
|
||
if (card_present()) {
|
||
long sfree = 0, stot = 0;
|
||
o = popen_read_tmo(600, "PATH=/bin:/sbin:/usr/bin:/usr/sbin df -k /usr/data/mnt/sd_0 2>/dev/null | awk 'NR==2{print $4, $2}'");
|
||
if (o) { if (sscanf(o, "%ld%ld", &sfree, &stot) < 2) stot = 0; free(o); }
|
||
if (stot > 0) snprintf(info[n++], sizeof info[0], "TF卡: %ld/%ld MB", sfree / 1024, stot / 1024);
|
||
else snprintf(info[n++], sizeof info[0], "TF卡: -");
|
||
} else {
|
||
snprintf(info[n++], sizeof info[0], "TF卡: 无卡");
|
||
}
|
||
|
||
/* 当前屏幕分辨率 + 状态栏高 */
|
||
snprintf(info[n++], sizeof info[0], "屏幕: %dx%d 状态栏%dpx", fb_xres, fb_yres, (int)SB_BAR_H);
|
||
|
||
/* WiFi:连上则 SSID(decode)+ 本机IP + 本机MAC;未连接显示「未连接」 */
|
||
if (wlan_connected()) {
|
||
char ss[64]; net_wifi_ssid(ss, sizeof ss);
|
||
snprintf(info[n++], sizeof info[0], "WiFi: %s", ss[0] ? ss : "已连接");
|
||
o = popen_read_tmo(1200, "PATH=/bin:/sbin:/usr/bin:/usr/sbin wpa_cli -i wlan0 status 2>/dev/null");
|
||
char ip[32] = "", mac[24] = "";
|
||
if (o) {
|
||
char *p = o;
|
||
while (p && *p) {
|
||
char *e = strchr(p, '\n'); if (e) *e = 0;
|
||
if (!strncmp(p, "ip_address=", 11)) snprintf(ip, sizeof ip, "%s", p + 11);
|
||
else if (!strncmp(p, "address=", 8)) snprintf(mac, sizeof mac, "%s", p + 8);
|
||
p = e ? e + 1 : NULL;
|
||
}
|
||
free(o);
|
||
}
|
||
if (ip[0]) snprintf(info[n++], sizeof info[0], "本机IP: %s", ip);
|
||
if (mac[0]) snprintf(info[n++], sizeof info[0], "本机MAC: %s", mac);
|
||
} else {
|
||
snprintf(info[n++], sizeof info[0], "WiFi: 未连接");
|
||
}
|
||
|
||
/* 蓝牙:hci_powered 决定开/关 */
|
||
snprintf(info[n++], sizeof info[0], "蓝牙: %s", hci_powered() ? "开" : "关");
|
||
|
||
for (int i = 0; i < n && i < 12; i++) item[i] = info[i];
|
||
s_dd_modal("关于本机", item, n, -1); /* cur=-1:只读展示,无预勾选 */
|
||
}
|
||
|
||
|
||
/* ---- 控件绘制(MD3 形态;全部用主题角色色,无散落硬编码)---- */
|
||
static void s_draw_switch(int x, int y, int on, const meow_ui_theme_t *th)
|
||
{
|
||
int w = 48, h = 28;
|
||
meow_gfx_fill_round(x, y, w, h, h / 2, on ? th->primary : th->surface_container);
|
||
int knob_r = h / 2 - 3;
|
||
int kx = on ? x + w - knob_r * 2 - 3 : x + 3;
|
||
meow_gfx_fill_round(kx, y + 3, knob_r * 2, knob_r * 2, knob_r,
|
||
on ? 0xFFFFFF : th->on_surface_variant);
|
||
}
|
||
static void s_draw_slider(int x, int y, int w, int val, int mn, int mx,
|
||
const meow_ui_theme_t *th)
|
||
{
|
||
int h = 6, ty = y;
|
||
fill_rect(x, ty, w, h, th->surface_container); /* 未达段 */
|
||
int range = mx - mn;
|
||
int frac = range ? (val - mn) * w / range : 0;
|
||
if (frac > 0) fill_rect(x, ty, frac, h, th->primary); /* 已达段 */
|
||
int tx = x + frac, cy = ty + h / 2;
|
||
meow_gfx_fill_round(tx - 12, cy - 12, 24, 24, 12, th->primary); /* thumb */
|
||
meow_gfx_fill_round(tx - 5, cy - 5, 10, 10, 5, 0xFFFFFF); /* thumb 中点 */
|
||
}
|
||
|
||
/* ---- set 后统一侧效应(DAC/背光/刷新率)+ 可选落盘 ---- */
|
||
static void s_commit(const srow_t *r, int save_now)
|
||
{
|
||
if (!r || !r->key) return;
|
||
if (!strcmp(r->key, "audio.volume")) {
|
||
/* M2.38 Bug1/2:音量变更——拖动中节流写 DAC(防 system() 高频卡死),抬手才无条件直写;
|
||
* 同时把 g_sys.vol 同步为当前值并刷整条状态栏(顶层 VOL 数字随之更新,不整屏重绘)。 */
|
||
g_sys.vol = settings_volume_get();
|
||
paint_sb_right();
|
||
if (save_now) meow_dac_vol_flush(); /* 抬手/点击落盘=无条件直写真实 DAC */
|
||
else meow_dac_vol_write_throttled();/* 拖动中=墙钟节流写 DAC */
|
||
}
|
||
else if (!strcmp(r->key, "display.brightness")) {
|
||
/* M2.46-2 ①:亮度滑条拖动时状态栏百分比数字实时跟。根因:旧代码只 write_brightness(v)
|
||
* 写背光,未同步 g_sys.brightness(状态栏亮度数字取它)也未重绘 → 数字卡住。
|
||
* 修法:写背光后同步 g_sys.brightness 并 paint_sb_right()(≡sb_redraw 整条标脏)。
|
||
* 一致性:其后 meow_sys_refresh 从 sysfs 读回的也是刚写入的 v,二者一致不打架。 */
|
||
int v; if (meow_settings_get_int("display.brightness", &v) == 0) {
|
||
write_brightness(v);
|
||
g_sys.brightness = v;
|
||
paint_sb_right();
|
||
}
|
||
}
|
||
else if (!strcmp(r->key, "display.refresh_rate"))
|
||
meow_ui_set_refresh_rate(refresh_rate_cfg);
|
||
else if (!strcmp(r->key, "display.refresh_always_max"))
|
||
meow_ui_set_refresh_always_max(refresh_always_max);
|
||
else if (!strcmp(r->key, "network.wifi_enabled")) {
|
||
int on = 0; meow_settings_get_bool(r->key, &on);
|
||
s_net_toggle_wifi(on);
|
||
s_net_refresh();
|
||
}
|
||
else if (!strcmp(r->key, "network.bluetooth_enabled")) {
|
||
int on = 0; meow_settings_get_bool(r->key, &on);
|
||
s_net_toggle_bt(on);
|
||
s_net_refresh();
|
||
}
|
||
/* M2.44 背景选项真正生效:display.background(black/blue/green) → menu_bg(主界面背景)。
|
||
* 此前 SC_PRESET 只写 meow_settings,未应用 menu_bg → 黑/绿点了没用。 */
|
||
else if (!strcmp(r->key, "display.background")) {
|
||
char bg[16]; meow_settings_get_str(r->key, bg, sizeof bg);
|
||
if (!strcmp(bg, "blue")) menu_bg = 0x0A1428; /* 深蓝 */
|
||
else if (!strcmp(bg, "green")) menu_bg = 0x0A2814; /* 深绿 */
|
||
else menu_bg = 0x000000; /* black(默认) */
|
||
/* 状态栏(menu_bg)与主界面背景都会用到 menu_bg;退出设置页回主界面时重绘即可。 */
|
||
}
|
||
/* M2.44 主题/mode 变更:状态栏文字/底色已接 s_ui_theme(),此处仅确保回主界面重绘。 */
|
||
else if (!strcmp(r->key, "display.theme") || !strcmp(r->key, "display.theme_mode") ||
|
||
!strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) {
|
||
paint_sb_right(); /* 状态栏文字/底色随主题刷新(整条标脏) */
|
||
}
|
||
/* M2.5x:字体文件(font.file)变更 → 立即 unload + reload(若已加载)。
|
||
* 置 mfont_forced 跳过 meow_font_ensure 的 SDK 复用捷径,强制走
|
||
* font.file → 卡默认 → 固件 链;全失败则回退 5x7 点阵(系统不死)。 */
|
||
else if (!strcmp(r->key, "font.file")) {
|
||
mfont_ready = 0;
|
||
mfont_forced = 1;
|
||
if (!meow_font_ensure())
|
||
logmsg("font.file: reload failed -> dotmatrix fallback");
|
||
else
|
||
logmsg("font.file: reloaded");
|
||
meow_font_reload_multifont(); /* M2.5x:全局字体变更 → 重配多字体(各类跟随全局者随之更新) */
|
||
meow_dirty_full(); /* 字体变化可能影响所有文本渲染,整帧重绘 */
|
||
}
|
||
/* M2.5x「加载大字库」开关(font.load_full)变更 → 强制重载 launcher 自身字体(小/大字库切换),
|
||
* 并重配多字体;大字库 atlas 大,meow_font_ensure 先 unload 旧再 load 新(防双驻留)。 */
|
||
else if (!strcmp(r->key, "font.load_full")) {
|
||
mfont_ready = 0;
|
||
mfont_forced = 1; /* 跳过 SDK 复用捷径,强制按生效档(gbk/common)真实 reload */
|
||
if (!meow_font_ensure())
|
||
logmsg("font.load_full: reload failed -> dotmatrix fallback");
|
||
else {
|
||
int lf = 0; meow_settings_get_switch("font.load_full", &lf);
|
||
logmsg("font.load_full: reloaded (load_full=%d)", lf);
|
||
}
|
||
meow_font_reload_multifont();
|
||
meow_dirty_full();
|
||
}
|
||
/* M2.5x 多字体:开关或任一类别字体变更 → 重新去重装载类别字库并重绘(多字体可能影响文本渲染)。 */
|
||
else if (!strcmp(r->key, "display.multi_font") || !strncmp(r->key, "font.class_", 11)) {
|
||
meow_font_reload_multifont();
|
||
meow_dirty_full();
|
||
}
|
||
if (save_now) meow_settings_save();
|
||
}
|
||
static void s_set_int_value(const srow_t *r, int v, int save_now)
|
||
{
|
||
if (!strcmp(r->key, "audio.volume")) v = vol_clamp(v); /* 尊重 audio.volume_max */
|
||
meow_settings_set_int(r->key, v);
|
||
s_commit(r, save_now);
|
||
}
|
||
/* 滑条拖动/点击落值:对「档位型」息屏时间做 索引→秒 换算,其余行原样写盘。
|
||
* sl_v 为滑条轨迹值(s_slider_val_from_x 返回值;息屏时间=档位索引)。 */
|
||
static void s_slider_set_from_track(const srow_t *r, int sl_v, int save_now)
|
||
{
|
||
if (!strcmp(r->key, "display.screen_off_timeout")) sl_v = s_time_tier_val(sl_v);
|
||
/* B2b 深色时段滑条:分钟按 15 分钟步进吸附(0-1439,整 15 分钟),避免拖出任意分钟 */
|
||
if (!strcmp(r->key, "display.theme_dark_start") || !strcmp(r->key, "display.theme_dark_end")) {
|
||
sl_v = ((sl_v + 7) / 15) * 15;
|
||
if (sl_v < 0) sl_v = 0;
|
||
if (sl_v > 1439) sl_v = 1439;
|
||
}
|
||
s_set_int_value(r, sl_v, save_now);
|
||
}
|
||
|
||
/* 滑条拖动值域映射:x(屏幕坐标)→ value,基于布局常量 S_SLIDER_X/W。
|
||
* 所有滑条行共用同一 track 几何,故任意滑条行都能正确映射。 */
|
||
static int s_slider_val_from_x(const srow_t *r, int px)
|
||
{
|
||
int range = r->max - r->min;
|
||
int v = r->min + (range ? (px - S_SLIDER_X) * range / S_SLIDER_W : 0);
|
||
if (v < r->min) v = r->min;
|
||
if (v > r->max) v = r->max;
|
||
return v;
|
||
}
|
||
|
||
/* ---- 绘制设置页(整页画进 SDK back_buf;调用方再 meow_dirty_add 局部 + flush)---- */
|
||
static void draw_settings_page(int sel, int scroll_off)
|
||
{
|
||
const meow_ui_theme_t *th = s_ui_theme();
|
||
int W = fb_xres, H = fb_yres;
|
||
fill_rect(0, 0, W, H, th->surface);
|
||
draw_statusbar(); /* 复用状态栏骨架 */
|
||
if (mfont_ready) ui_draw(24, 50, "系统设置", th->on_surface, 28);
|
||
else draw_text(24, 30, "SETTINGS", 3, th->on_surface);
|
||
fill_rect(0, 50 + (mfont_ready ? meow_font_line_height(28) : 24) + 2, W, 2, th->outline);
|
||
|
||
/* M2.38 Bug5:整屏背景替代带圆角的单张卡片(用户反馈圆角非全屏底很怪)。
|
||
* 内容区从 SET_CARD_Y 起整屏 fill surface_variant、去圆角、满宽直到屏幕底;
|
||
* 状态栏(0-28)与标题区(28-92)仍为 surface,标题/分隔线/分组 outline/选中高亮/滚动条布局不变。 */
|
||
fill_rect(0, SET_CARD_Y, W, H - SET_CARD_Y, th->surface_variant);
|
||
|
||
int clip_top = SET_CONTENT_Y, clip_bot = SET_CONTENT_BOT;
|
||
s_sel_track_y = -1;
|
||
for (int i = 0; i < SMENU_N; i++) {
|
||
int pos = s_vis_of(i);
|
||
if (pos < 0) continue; /* 隐藏行(多字体关闭时 6 个 class 行) */
|
||
int sy = SET_CONTENT_Y + pos * SROW_H - scroll_off;
|
||
if (sy + SROW_H < clip_top || sy >= clip_bot) continue; /* 视野外行跳过(配滚动) */
|
||
const srow_t *r = &SMENU[i];
|
||
int selected = (i == sel);
|
||
if (selected)
|
||
meow_ui_fill_rect_alpha(SET_CARD_X + 4, sy, SET_CARD_W - 8, SROW_H, th->primary, 34);
|
||
meow_ui_fill_rect(SET_CARD_X + 12, sy + SROW_H - 1, SET_CONTENT_W, 1, th->outline);
|
||
|
||
if (r->ctrl == SC_HEADER) {
|
||
ui_draw(SET_CARD_X + 16, sy + 14, r->label, th->on_surface_variant, 18);
|
||
} else if (r->ctrl == SC_SWITCH) {
|
||
ui_draw(SET_CARD_X + 16, sy + (SROW_H - meow_font_line_height(22)) / 2, r->label,
|
||
th->on_surface, 22);
|
||
int on = 0; meow_settings_get_switch(r->key, &on);
|
||
s_draw_switch(SET_CARD_X + SET_CARD_W - 16 - 48, sy + (SROW_H - 28) / 2, on, th);
|
||
} else if (r->ctrl == SC_SLIDER) {
|
||
int ly = sy + (SROW_H - meow_font_line_height(22)) / 2 - 6; /* 标题略上移,给下方滑条留位 */
|
||
ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22);
|
||
char vb[32]; s_fmt_value(r, vb, sizeof vb);
|
||
ui_draw(SET_CARD_X + SET_CARD_W - 16 - ui_width(vb, 22), ly, vb, th->on_surface_variant, 22);
|
||
int v = 0; meow_settings_get_int(r->key, &v);
|
||
/* 息屏时间为档位索引型:把秒换算成档位索引再画(各档等距,便于触摸对位);
|
||
* 其余滑条防御性 clamp 到 [min,max],防止后端暂存值越界(如 brightness 旧值 101
|
||
* 在画时被 clamp 到 100,thumb 不冲出 track)。 */
|
||
if (!strcmp(r->key, "display.screen_off_timeout")) v = s_time_tier_idx(v);
|
||
else {
|
||
if (v < r->min) v = r->min;
|
||
if (v > r->max) v = r->max;
|
||
}
|
||
int tyy = sy + 40;
|
||
s_draw_slider(S_SLIDER_X, tyy, S_SLIDER_W, v, r->min, r->max, th);
|
||
if (selected) s_sel_track_y = tyy;
|
||
} else if (r->ctrl == SC_INFO) { /* M2.39 只读动态信息(如 WiFi 连接 SSID) */
|
||
int ly = sy + (SROW_H - meow_font_line_height(22)) / 2;
|
||
ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22);
|
||
char vb[48]; s_net_get(r, vb, sizeof vb);
|
||
int vx = SET_CARD_X + SET_CARD_W - 16 - ui_width(vb, 22);
|
||
ui_draw(vx, ly, vb, th->on_surface_variant, 22);
|
||
} else if (r->ctrl == SC_LIST) { /* M2.39 网络列表行:摘要 + 右向展开提示 */
|
||
int ly = sy + (SROW_H - meow_font_line_height(22)) / 2;
|
||
ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22);
|
||
char vb[48]; s_net_get(r, vb, sizeof vb);
|
||
int vx = SET_CARD_X + SET_CARD_W - 16 - 12 - ui_width(vb, 22); /* 给右向 chevron 留位 */
|
||
ui_draw(vx, ly, vb, th->on_surface_variant, 22);
|
||
s_draw_chevron(SET_CARD_X + SET_CARD_W - 16, sy + SROW_H / 2, th->on_surface_variant);
|
||
} else { /* SC_ENUM / SC_PRESET */
|
||
int ly = sy + (SROW_H - meow_font_line_height(22)) / 2;
|
||
ui_draw(SET_CARD_X + 16, ly, r->label, th->on_surface, 22);
|
||
char vb[40]; s_display_value(r, vb, sizeof vb);
|
||
int vx = SET_CARD_X + SET_CARD_W - 16 - ui_width(vb, 22);
|
||
ui_draw(vx, ly, vb, th->on_surface_variant, 22);
|
||
s_draw_chevron(vx - 8, sy + SROW_H / 2, th->on_surface_variant);
|
||
}
|
||
}
|
||
|
||
/* 滚动条(内容超出视口时显示 thumb;on_surface_variant 弱化) */
|
||
int vis_total = s_vis_count() * SROW_H;
|
||
if (vis_total > SET_CONTENT_H) {
|
||
int total = vis_total;
|
||
int thumb_h = SET_CONTENT_H * SET_CONTENT_H / total;
|
||
if (thumb_h < 24) thumb_h = 24;
|
||
int th_y = SET_CONTENT_Y + (SET_CONTENT_H - thumb_h) * scroll_off / (total - SET_CONTENT_H);
|
||
meow_gfx_fill_round(fb_xres - 12, SET_CONTENT_Y, 5, SET_CONTENT_H, 3, th->surface_container);
|
||
meow_gfx_fill_round(fb_xres - 12, th_y, 5, thumb_h, 3, th->on_surface_variant);
|
||
}
|
||
|
||
/* 底部:返回提示(左)+ 版本号(右),复用应用列表底部对齐逻辑。
|
||
* M2.39:返回按钮改用像素左向箭头 + 「返回」——点阵回退字库缺少 '‹' 字形,
|
||
* 原 「‹ 返回」在 !mfont_ready 时 '‹' 渲染成方框。 */
|
||
{
|
||
char ver[32];
|
||
snprintf(ver, sizeof ver, "MeowOS v%s dev", g_meowos_version);
|
||
int foot_y = fb_yres - meow_font_line_height(20) - 8;
|
||
s_draw_chevron_left(SET_CARD_X + 12, foot_y + meow_font_line_height(20) / 2, th->on_surface_variant);
|
||
ui_draw(SET_CARD_X + 24, foot_y, "返回", th->on_surface_variant, 20);
|
||
ui_draw(fb_xres - 8 - ui_width(ver, 20), foot_y, ver, th->outline, 20);
|
||
}
|
||
}
|
||
|
||
/* M2.5x 多字体:class 行下拉的「跟随全局」选项(值=空串)在 write 路径经此映射回真实存储值。 */
|
||
static const char *s_preset_value(const srow_t *r, int pick)
|
||
{
|
||
int is_cls = r->key && !strncmp(r->key, "font.class_", 11);
|
||
if (is_cls) {
|
||
if (pick == 0) return ""; /* 跟随全局 font.file */
|
||
pick--;
|
||
}
|
||
int pn = 0;
|
||
const char *const *pps = s_row_presets(r, &pn);
|
||
if (pick < 0 || pick >= pn) return is_cls ? "" : (pps && pn > 0 ? pps[0] : "");
|
||
return pps[pick];
|
||
}
|
||
|
||
/* ---- 选择器子对话框(MD3 §3.3:居中圆角卡片,列选项;中文标签)----
|
||
* ENUM 用 meow_settings_enum_names/enum_label;PRESET 用本地预设表。
|
||
* 返回选中索引;取消返回 -1。阻塞(自开输入设备)。
|
||
* M2.39:modal 核心抽到 s_dd_modal(网络列表复用),本函数只负责组装选项数组。 */
|
||
static int s_dropdown(const srow_t *r)
|
||
{
|
||
int n = 0, cur = 0;
|
||
const char *names[8];
|
||
char labels[8][48];
|
||
const char *item[S_NET_MAX];
|
||
if (r->ctrl == SC_ENUM) {
|
||
int kn = 0;
|
||
meow_settings_enum_names(r->key, names, &kn);
|
||
n = kn;
|
||
if (n > S_NET_MAX) n = S_NET_MAX;
|
||
for (int i = 0; i < n; i++) {
|
||
meow_settings_enum_label(r->key, i, labels[i], sizeof labels[i]);
|
||
item[i] = labels[i];
|
||
}
|
||
int ci = 0; meow_settings_get_enum(r->key, &ci); cur = ci;
|
||
} else {
|
||
char cstr[MEOW_SET_MAX_LEN];
|
||
meow_settings_get_str(r->key, cstr, sizeof cstr);
|
||
int pn = 0;
|
||
const char *const *pps = s_row_presets(r, &pn);
|
||
int is_cls = r->key && !strncmp(r->key, "font.class_", 11);
|
||
int base = is_cls ? 1 : 0; /* class 行 0 号留给「跟随全局」 */
|
||
int cnt = pn; if (cnt > S_NET_MAX - base) cnt = S_NET_MAX - base;
|
||
n = base + cnt;
|
||
if (is_cls) {
|
||
names[0] = "";
|
||
snprintf(labels[0], sizeof labels[0], "跟随全局字体");
|
||
item[0] = labels[0];
|
||
if (!cstr[0]) cur = 0;
|
||
}
|
||
for (int i = 0; i < cnt; i++) {
|
||
names[base + i] = pps[i];
|
||
snprintf(labels[base + i], sizeof labels[base + i], "%s",
|
||
(r->preset_labels && r->preset_labels[i][0]) ? r->preset_labels[i] : pps[i]);
|
||
item[base + i] = labels[base + i];
|
||
if (!strcmp(pps[i], cstr)) cur = base + i;
|
||
}
|
||
}
|
||
if (n <= 0) return -1;
|
||
if (cur < 0 || cur >= n) cur = 0;
|
||
return s_dd_modal(r->label, item, n, cur);
|
||
}
|
||
|
||
/* ---- 通用选择器下拉模态(MD3 §3.3:居中圆角卡片,列选项)----
|
||
* title=标题;item[i]=第 i 项显示文本;n=项数(≤S_NET_MAX);cur=当前值下标(画勾选)。
|
||
* 返回选中下标;取消/点卡片外返回 -1。阻塞(自开输入设备)。
|
||
* s_dropdown(设置项 enum/preset)与 s_net_open(网络列表)共用。
|
||
* M2.40:触摸命中判定改在抬手用抬手时刻最新 abs 坐标(见循环内注释),修「点选项无反应」。
|
||
* 说明:模态内部仍保留音量键=移动选项高亮(这是「选值」不是「设置页列表光标」,属另一语义,
|
||
* 与 M2.39 仅移除 run_settings_page 列表光标移动的约束不冲突)。 */
|
||
static int s_dd_modal(const char *title, const char *item[S_NET_MAX], int n, int cur)
|
||
{
|
||
int sel = cur;
|
||
const meow_ui_theme_t *th = s_ui_theme();
|
||
int W = fb_xres, H = fb_yres;
|
||
int opt_row = 48, pad = 20, title_h = 40;
|
||
int d_h = title_h + n * opt_row + pad;
|
||
int d_w = 348, d_x = (W - d_w) / 2, d_y = (H - d_h) / 2;
|
||
if (d_y < SB_BAR_H) d_y = SB_BAR_H; /* 下拉不压状态栏(栏高 40→28,随常量) */
|
||
if (H - d_y - d_h < 20) d_y = H - d_h - 20;
|
||
|
||
struct keypoll kp_power, kp_keys;
|
||
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
||
struct kd_state kd_vold, kd_volu, kd_pwr;
|
||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_pwr, 0, sizeof kd_pwr);
|
||
int m_prev_vold = 0, m_prev_volu = 0, m_prev_pwr = 0;
|
||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
|
||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
||
}
|
||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||
if (tfd >= 0) {
|
||
/* M2.37:彻底清空「打开下拉」这次触摸的残留边沿。goodix 的抬起序列(ABS 坐标/
|
||
* BTN_TOUCH=0/MT_TRACKING=-1)在开 fd 后仍可能陆续送达,若只清空一次,第一轮
|
||
* 下拉循环会读到这些残边 → 误判抬起/选项错位/点卡片外穿透。故 排空→短延时→再排空。 */
|
||
unsigned char ev[16];
|
||
while (read(tfd, ev, sizeof ev) == 16) {}
|
||
usleep(40000); /* M2.38 Bug3:排空→40ms→再排空,吸收 goodix 抬起序列长尾 */
|
||
while (read(tfd, ev, sizeof ev) == 16) {}
|
||
}
|
||
int pwr_pending = 0;
|
||
int result = -1;
|
||
int dlx = -1, dly = -1, dtd = 0; /* 下拉触摸解析(dlx/dly=最新有效坐标;dtd=已按下) */
|
||
/* M2.40:不再锁按下时刻的 ddx/ddy —— goodix 时序 BTN_TOUCH 先于 ABS,按下边沿时坐标可能
|
||
* 还是 -1/上一手势残留,锁原点会把点击判成「点卡片外=取消」。命中改在抬手时用抬手时刻的
|
||
* dlx/dly(抬起时必已更新到本次最新有效坐标),与 meow_touch mt_up 语义同源。 */
|
||
|
||
/* 初始绘制:全屏压暗覆盖层 + 居中圆角卡片(fill 后画空心圆角描边) */
|
||
meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150);
|
||
meow_gfx_fill_round(d_x, d_y, d_w, d_h, 16, th->surface_container);
|
||
meow_ui_draw_rect_round(d_x, d_y, d_w, d_h, 16, th->outline);
|
||
if (mfont_ready) ui_draw(d_x + 18, d_y + 8, title, th->on_surface, 20);
|
||
else draw_text(d_x + 18, d_y + 8, title, 2, th->on_surface);
|
||
for (int i = 0; i < n; i++) {
|
||
int ry = d_y + title_h + i * opt_row;
|
||
int cur_sel = (i == sel);
|
||
if (cur_sel) meow_ui_fill_rect_alpha(d_x + 4, ry, d_w - 8, opt_row, th->primary, 36);
|
||
ui_draw(d_x + 18, ry + (opt_row - meow_font_line_height(20)) / 2, item[i] ? item[i] : "",
|
||
cur_sel ? 0xFFFFFF : th->on_surface, 20);
|
||
if (i == cur) meow_ui_fill_rect(d_x + d_w - 28, ry + (opt_row - 12) / 2, 12, 12, th->primary);
|
||
}
|
||
meow_dirty_full(); meow_ui_flush();
|
||
|
||
for (;;) {
|
||
if (sigterm_flag) { result = -1; break; }
|
||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||
int vold = 0, volu = 0, pwr = 0;
|
||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
|
||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
|
||
}
|
||
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu); (void)kd_tick(&kd_pwr, pwr);
|
||
vold = kd_vold.stable; volu = kd_volu.stable; pwr = kd_pwr.stable;
|
||
int need = 0;
|
||
if (vold && !m_prev_vold && sel < n - 1) { sel++; need = 1; }
|
||
if (volu && !m_prev_volu && sel > 0) { sel--; need = 1; }
|
||
if (pwr && !m_prev_pwr) pwr_pending = 1; /* 按下:待抬起确认 */
|
||
if (!pwr && m_prev_pwr && pwr_pending) { pwr_pending = 0; result = sel; break; } /* 抬起=确认 */
|
||
m_prev_vold = vold; m_prev_volu = volu; m_prev_pwr = pwr;
|
||
|
||
/* 触摸:抬起命中选项=确认,点卡片外=取消 */
|
||
if (tfd >= 0) {
|
||
struct pollfd pfd = { tfd, POLLIN, 0 };
|
||
if (poll(&pfd, 1, 0) > 0) {
|
||
unsigned char ev[16];
|
||
while (read(tfd, ev, sizeof ev) == 16) {
|
||
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
|
||
int32_t val = *(int32_t *)(ev + 12);
|
||
if (type == EV_ABS) {
|
||
if (code == ABS_X || code == ABS_MT_POSITION_X) dlx = val;
|
||
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) dly = val;
|
||
} else if (type == EV_KEY && code == BTN_TOUCH) {
|
||
if (val) {
|
||
/* 按下边沿:只标「已按下」,绝不锁原点(goodix 时序 BTN_TOUCH 先于
|
||
* ABS,此刻 dlx/dly 可能还是 -1 或上一手势残留)。 */
|
||
dtd = 1;
|
||
} else if (dtd) {
|
||
dtd = 0;
|
||
/* 抬手判定:此刻 dlx/dly 必为本次最新有效坐标(抬起时 ABS 已先到)。
|
||
* 卡片内命中 → 返回选项下标;卡片外 → 取消(-1);从未见有效坐标 → 丢弃。 */
|
||
if (dlx >= 0 && dly >= 0) {
|
||
if (dlx >= d_x && dlx < d_x + d_w && dly >= d_y && dly < d_y + d_h) {
|
||
int i = (dly - d_y - title_h) / opt_row;
|
||
if (i >= 0 && i < n) { sel = i; result = sel; goto ddone; }
|
||
/* 卡片内但落在标题/内边距:保持弹出态,等待下一次点选 */
|
||
} else {
|
||
result = -1; goto ddone; /* 点卡片外=取消 */
|
||
}
|
||
}
|
||
/* dlx/dly 为 -1:ghost/残留抬手,丢弃不动作 */
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (need) {
|
||
/* 重绘覆盖层+选项(仅 sel 高亮变化) */
|
||
meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150);
|
||
meow_gfx_fill_round(d_x, d_y, d_w, d_h, 16, th->surface_container);
|
||
meow_ui_draw_rect_round(d_x, d_y, d_w, d_h, 16, th->outline);
|
||
if (mfont_ready) ui_draw(d_x + 18, d_y + 8, title, th->on_surface, 20);
|
||
for (int i = 0; i < n; i++) {
|
||
int ry = d_y + title_h + i * opt_row;
|
||
int cur_sel = (i == sel);
|
||
if (cur_sel) meow_ui_fill_rect_alpha(d_x + 4, ry, d_w - 8, opt_row, th->primary, 36);
|
||
ui_draw(d_x + 18, ry + (opt_row - meow_font_line_height(20)) / 2, item[i] ? item[i] : "",
|
||
cur_sel ? 0xFFFFFF : th->on_surface, 20);
|
||
if (i == cur) meow_ui_fill_rect(d_x + d_w - 28, ry + (opt_row - 12) / 2, 12, 12, th->primary);
|
||
}
|
||
meow_dirty_full(); meow_ui_flush();
|
||
}
|
||
usleep(POLL_MS * 1000);
|
||
}
|
||
ddone:
|
||
if (tfd >= 0) close(tfd);
|
||
kp_close(&kp_power); kp_close(&kp_keys);
|
||
return result;
|
||
}
|
||
|
||
/* ---- 运行设置页(独立子循环;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/<family>/,刷新「字体文件」下拉。
|
||
* 进设置时卡通常在位;此步只枚举/校验,不加载(加载由 meow_font_ensure 按 font.file 决定)。 */
|
||
meow_font_scan_presets();
|
||
|
||
struct keypoll kp_power, kp_keys;
|
||
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
|
||
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
|
||
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
|
||
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
|
||
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0;
|
||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
|
||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
|
||
}
|
||
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
|
||
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
|
||
unsigned long long touch_cooldown_until = now_ms() + 300;
|
||
unsigned long long last_tick = now_ms();
|
||
unsigned long long last_act = now_ms(); /* 最近一次交互时间(自动息屏用) */
|
||
int after_modal = 0; /* 选择器子对话框刚关闭:其全屏 dim 覆盖过状态栏,需整帧重绘还原 */
|
||
|
||
/* 触摸拖动滑条状态 */
|
||
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
|
||
int drag = -1; /* 被拖动的行索引;-1=无 */
|
||
/* M2.37 列表拖动滚动状态 */
|
||
int scroll_drag = 0; /* 是否处于列表拖动滚动手势 */
|
||
int scroll_base = 0; /* 进入滚动时对应的 scroll_off 基准(连续跟手) */
|
||
int scroll_y0 = 0; /* 按下原点纵向坐标 */
|
||
int scroll_invert = 0; /* M2.38 Bug4:滑动方向切换(input.swipe_invert;0=标准方向) */
|
||
int sel_changed = 0; /* 本轮是否改变了选中行(仅此场景才强制选中行可见) */
|
||
int wake_suppress = 0; /* 自动息屏唤醒后:吸收唤醒键边沿,待全部抬起再接管 */
|
||
/* M2.x(KEY_CONTROL_DESIGN §2)电源键状态:设置页电源键 = 系统级语义(非返回),
|
||
* 对照主界面 run_menu 3130-3137 的同名变量。单击=息屏/亮屏、长按=电源菜单、8s=强关。 */
|
||
unsigned long long pwr_hold_at = 0; /* 本次电源按下计时起点 */
|
||
int pwr_menu_state = 0; /* 是否已因此次 hold 弹过电源菜单 */
|
||
int pwr_woke = 0; /* 本次按下=自动息屏唤醒(释放只唤醒不 toggle) */
|
||
int pwr_suppress_click = 0; /* 菜单刚关闭:松开不得误判单击熄屏(M2.34 同款) */
|
||
|
||
/* 首次全帧绘制 */
|
||
s_net_sync_from_sys(); /* M2.39:WiFi/蓝牙开关行同步真实硬件态 + 刷新网络信息缓存 */
|
||
scroll_off = s_clamp_scroll(sel, scroll_off);
|
||
draw_settings_page(sel, scroll_off);
|
||
meow_dirty_full(); meow_ui_flush();
|
||
|
||
for (;;) {
|
||
unsigned long long now = now_ms();
|
||
if (sigterm_flag) break;
|
||
/* 重绘触发:值/选中/滚动变化 */
|
||
int need_paint = 0;
|
||
|
||
/* ---- M2.37 自动息屏态:留在设置页,等待任意输入唤醒,恢复后重绘(不丢上下文)----
|
||
* 对照 run_menu 主循环 meow_auto_slept 的唤醒重绘(M2.17/2.18)。休眠期间仅探测
|
||
* 输入(按键原始按下 / black 模式触摸按下);一旦唤醒:亮屏、清自动态并整帧重绘。 */
|
||
if (meow_auto_slept && !meow_screen_on) {
|
||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||
int wake = 0;
|
||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) wake = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) wake = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) wake = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) wake = 1;
|
||
}
|
||
if (!wake && screen_off_mode == 1 && tfd >= 0) { /* black 模式:面板仍上电,触摸可唤醒 */
|
||
struct pollfd spfd = { tfd, POLLIN, 0 };
|
||
if (poll(&spfd, 1, 0) > 0) {
|
||
unsigned char ev[16];
|
||
while (read(tfd, ev, sizeof ev) == 16) {
|
||
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
|
||
int32_t val = *(int32_t *)(ev + 12);
|
||
if (type == EV_KEY && code == BTN_TOUCH && val) wake = 1;
|
||
}
|
||
}
|
||
}
|
||
if (wake) {
|
||
meow_panel_on();
|
||
meow_auto_slept = 0;
|
||
wake_suppress = 1;
|
||
last_act = now_ms();
|
||
/* 重置按键防抖与 m_prev_*(含唤醒键原始态对齐),并丢唤醒的这次触摸 */
|
||
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
|
||
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
|
||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||
touch_cooldown_until = now_ms() + 300;
|
||
td = 0; td_ok = 0; drag = -1; scroll_drag = 0; tlx = tly = -1;
|
||
logmsg("settings: auto screen-off -> input wake, stayed in settings page");
|
||
scroll_off = s_clamp_scroll_range(scroll_off);
|
||
draw_settings_page(sel, scroll_off);
|
||
meow_dirty_full(); meow_ui_flush();
|
||
}
|
||
usleep(POLL_MS * 1000);
|
||
continue;
|
||
}
|
||
|
||
/* ---- 按键 ---- */
|
||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||
int vold = 0, volu = 0, play = 0, pwr = 0;
|
||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
|
||
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
|
||
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
|
||
}
|
||
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu);
|
||
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
|
||
vold = kd_vold.stable; volu = kd_volu.stable;
|
||
play = kd_play.stable; pwr = kd_pwr.stable;
|
||
if (wake_suppress) {
|
||
/* 刚被唤醒:吸收唤醒键的边沿,等所有按键完全抬起后再接管动作,
|
||
* 避免「电源唤醒→抬起→被当成电源单击返回」或音量唤醒→误导航。 */
|
||
if (!pwr && !vold && !volu && !play) wake_suppress = 0;
|
||
} else {
|
||
/* M2.39:设置页音量键 = 调系统音量(写 DAC),不做光标移动(用户约束:
|
||
* 设置页仅触摸导航;音量键在电菜单/应用列表的系统级导航保持不动)。
|
||
* 按下边沿即起线性渐变(vol_ramp_begin),下方 vol_ramp_tick 逐 1% 爬 + 刷状态栏。 */
|
||
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
|
||
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
|
||
/* M2.x(KEY_CONTROL_DESIGN §2/§3)播放键在设置页同样被系统「抓住」:
|
||
* 捕获+日志+消费,不再映射为「返回/确认」(原 play 边沿=break 已删,播放键不再退出设置页)。
|
||
* 仍视为输入(重置自动息屏计时,与主界面 play 的媒体预留/any_input 一致)。 */
|
||
if (play && !m_prev_play) {
|
||
logmsg("settings: play key caught (media-reserved, no exit)");
|
||
last_act = now;
|
||
}
|
||
/* M2.x(KEY_CONTROL_DESIGN §2)电源键 = 系统级语义,对齐主界面 run_menu 3284-3366:
|
||
* 单击(按<长按阈值后松开)= 息屏/亮屏;长按>=longpress_menu_ms = 电源菜单;
|
||
* 按住>=forceoff_ms(且未开过菜单)= 强制关机;自动息屏按下只唤醒。
|
||
* ※ 电源键在设置页不再映射为「返回」——退出只能靠触摸(底部返回按钮 + 左缘左滑)。 */
|
||
if (pwr && !m_prev_pwr) {
|
||
pwr_hold_at = now; pwr_menu_state = 0;
|
||
pwr_suppress_click = 0; /* M2.34:新一次电源按下清除旧的抑制标志 */
|
||
last_act = now; /* 视为输入,防按住期间误触发自动息屏 */
|
||
/* M2.18 Bug 2 同款:按下正落在自动息屏态(auto_slept 且屏已关)→ 本次只做唤醒 */
|
||
if (meow_auto_slept && !meow_screen_on) pwr_woke = 1;
|
||
}
|
||
if (pwr && pwr_hold_at) {
|
||
/* M2.10/M2.11 B/M2.37 同款:8s 强关只在该「按住」从未开过菜单(!pwr_menu_state)
|
||
* 时触发;开过菜单则交给菜单内确认/取消,杜绝「开菜单->取消->仍按住->强关」残留。 */
|
||
if (now - pwr_hold_at >= power_forceoff_ms && !pwr_menu_state) {
|
||
if (sim_no_exec) { logmsg("settings pm: [sim-no-exec] POWER HOLD %dms -> would force poweroff", power_forceoff_ms); }
|
||
else { logmsg("settings pm: POWER HOLD %dms -> force poweroff", power_forceoff_ms); sys_do_reboot(LINUX_REBOOT_CMD_POWER_OFF); }
|
||
/* 关机一般不复返;若 reboot 失败返回仍按按住处理(m_prev 由下方统一复写) */
|
||
} else if (now - pwr_hold_at >= power_longpress_menu_ms && !pwr_menu_state) {
|
||
pwr_menu_state = 1;
|
||
logmsg("settings pm: POWER HOLD %dms -> show power menu", power_longpress_menu_ms);
|
||
show_power_menu(); /* 动作在函数内 system();取消则续循环 */
|
||
last_act = now_ms(); /* 菜单返回即视为「刚有输入」,重置自动息屏计时 */
|
||
/* M2.11 B 同款:菜单已关闭。清空按住计时与菜单态,绝不把已互动的 hold 再累计到 8s 强关。 */
|
||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
||
pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击息屏 */
|
||
/* M2.25 同款:阻塞式 show_power_menu() 期间 kd_* 冻结,m_prev_* 与稳定值可能错配,
|
||
* 重播 kd_* 并以当前 raw 对齐 m_prev_*,杜绝「菜单内触摸点取消->返回后被误判单击息屏」。 */
|
||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||
memset(&kd_vold, 0, sizeof kd_vold);
|
||
memset(&kd_volu, 0, sizeof kd_volu);
|
||
memset(&kd_play, 0, sizeof kd_play);
|
||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||
after_modal = 1; /* 全屏 dim 覆盖过状态栏,需整帧重绘还原 */
|
||
need_paint = 1;
|
||
/* 已按 raw 对齐 m_prev_*,跳过下方基线复写,防把 raw 值覆盖回旧稳定值 → 假单边沿 */
|
||
goto settings_keys_seeded;
|
||
}
|
||
}
|
||
if (!pwr && m_prev_pwr) {
|
||
/* 松开:短按(<长按阈值且菜单未开)→ 单击 = 息屏/亮屏切换 */
|
||
if (pwr_suppress_click) {
|
||
logmsg("settings pm: power release after power-menu -> no toggle (suppress)");
|
||
} else if (pwr_hold_at && (now - pwr_hold_at < power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) {
|
||
logmsg("settings pm: power single-click -> toggle panel (was %s)", meow_screen_on ? "on" : "off");
|
||
meow_toggle_panel();
|
||
meow_auto_slept = 0; /* M2.17:手动/自动息屏后电源单击唤醒均清自动态 */
|
||
last_act = now_ms();
|
||
need_paint = 1; /* 亮屏重绘 / 息屏留屏(面板已黑,下次亮屏重绘) */
|
||
} else if (pwr_woke) {
|
||
logmsg("settings pm: power release after auto screen-off wake -> no toggle (wake only)");
|
||
}
|
||
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
|
||
pwr_suppress_click = 0; /* M2.34:电源已松开,抑制标志一次性用完即清 */
|
||
}
|
||
}
|
||
/* M2.39:驱动音量线性渐变(长按续爬/松开即止)+ 刷新状态栏 VOL;「音量」行随 need_paint 更新 */
|
||
if (vol_ramp_dir != 0) {
|
||
int held = (vol_ramp_dir > 0) ? volu : vold;
|
||
int going = vol_ramp_tick(held);
|
||
g_sys.vol = settings_volume_get();
|
||
paint_sb_right();
|
||
need_paint = 1;
|
||
if (!going) logmsg("settings vol ramp end -> %d", g_sys.vol);
|
||
}
|
||
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr;
|
||
settings_keys_seeded:
|
||
;
|
||
|
||
/* ---- 触摸 ---- */
|
||
if (tfd >= 0 && meow_screen_on) { /* M2.x:手动电源息屏后不处理触摸(与主界面一致,且防屏黑时误退出/误改值) */
|
||
struct pollfd pfd = { tfd, POLLIN, 0 };
|
||
if (poll(&pfd, 1, 0) > 0) {
|
||
unsigned char ev[16];
|
||
while (read(tfd, ev, sizeof ev) == 16) {
|
||
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
|
||
int32_t val = *(int32_t *)(ev + 12);
|
||
if (type == EV_ABS) {
|
||
if (code == ABS_X || code == ABS_MT_POSITION_X) tlx = val;
|
||
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val;
|
||
if (td && !td_ok && tlx >= 0 && tly >= 0) {
|
||
tdx = tlx; tdy = tly; td_ok = 1;
|
||
scroll_y0 = tdy; /* 列表拖动基准:按下原点(触发时才锁 scroll_base) */
|
||
/* 按下原点落在选中滑条 track 带内 → 开始拖动 */
|
||
if (SMENU[sel].ctrl == SC_SLIDER && s_sel_track_y >= 0 &&
|
||
tlx >= S_SLIDER_X - 12 && tlx <= S_SLIDER_X + S_SLIDER_W + 12 &&
|
||
tly >= s_sel_track_y - 14 && tly <= s_sel_track_y + 20) {
|
||
drag = sel;
|
||
const srow_t *r = &SMENU[sel];
|
||
int v = s_slider_val_from_x(r, tlx);
|
||
s_slider_set_from_track(r, v, 0);
|
||
need_paint = 1;
|
||
}
|
||
}
|
||
/* M2.37 列表拖动滚动:非滑条行按下后纵向位移超阈值 → 进入列表滚动。
|
||
* scroll_base 在触发瞬间反推,保证进入滚动那一刻不跳变(连续跟手)。
|
||
* M2.38 Bug4:默认标准滑动(手指上滑 → scroll_off 增大 = 内容往下翻看后面项);
|
||
* 方向可经 input.swipe_invert 切换(1=反向:手指上滑 → 内容上移)。 */
|
||
if (td_ok && drag < 0 && !scroll_drag) {
|
||
int ddy = tly - scroll_y0;
|
||
if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) {
|
||
scroll_drag = 1;
|
||
meow_settings_get_bool("input.swipe_invert", &scroll_invert);
|
||
scroll_base = scroll_off + (scroll_invert ? -ddy : ddy);
|
||
}
|
||
}
|
||
if (scroll_drag && td_ok) {
|
||
int dydt = tly - scroll_y0;
|
||
scroll_off = s_clamp_scroll_range(scroll_base + (scroll_invert ? dydt : -dydt));
|
||
need_paint = 1;
|
||
}
|
||
if (drag >= 0 && td_ok) {
|
||
const srow_t *r = &SMENU[drag];
|
||
int v = s_slider_val_from_x(r, tlx);
|
||
s_slider_set_from_track(r, v, 0); /* 拖动实时 set_*,抬起才落盘 */
|
||
need_paint = 1;
|
||
}
|
||
} else if (type == EV_KEY && code == BTN_TOUCH) {
|
||
if (val && !td) {
|
||
if (now_ms() < touch_cooldown_until) {
|
||
tlx = tly = -1; /* 冷却丢弃 */
|
||
} else {
|
||
td = 1; td_ok = 0; tlx = -1; tly = -1; drag = -1; scroll_drag = 0;
|
||
}
|
||
} else if (!val && td) {
|
||
td = 0;
|
||
if (drag >= 0) { /* 滑条拖动结束 → 落盘 */
|
||
const srow_t *r = &SMENU[drag];
|
||
s_commit(r, 1);
|
||
drag = -1;
|
||
need_paint = 1;
|
||
} else if (scroll_drag) {
|
||
scroll_drag = 0; /* 列表滚动结束(已实时重绘),不再当 tap */
|
||
scroll_base = 0; scroll_y0 = 0;
|
||
need_paint = 1;
|
||
} else if (td_ok) {
|
||
int dx = tlx - tdx, dy = tly - tdy;
|
||
/* M2.39 左缘右滑返回:复用全局左缘返回判定 meow_edge_back()
|
||
* (与 SDK 全局左缘返回 A-0 同语义:左缘起点 + 右滑≥MEOW_EDGE_SWIPE
|
||
* 且横向主导),避免设置页自造一套重复判定。 */
|
||
if (meow_edge_back(tdx, dx, dy)) {
|
||
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
|
||
goto done;
|
||
}
|
||
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
|
||
if (adx < 60 && ady < 60) { /* 视为 tap */
|
||
int tx = tdx, ty = tdy;
|
||
/* 底部返回按钮 */
|
||
if (ty >= H - 52 && tx >= SET_CARD_X && tx <= SET_CARD_X + 120) {
|
||
if (tfd >= 0) { unsigned char e[16]; while (read(tfd, e, sizeof e) == 16) {} }
|
||
goto done;
|
||
}
|
||
int cy = ty - SET_CONTENT_Y + scroll_off;
|
||
int idx = s_row_at_content_y(cy);
|
||
if (idx >= 0 && !s_setting_idx(idx)) {
|
||
/* 点分组标题行 → 仅选中下一设置行 */
|
||
int ns = s_next_setting(idx);
|
||
sel = ns; need_paint = 1; sel_changed = 1;
|
||
} else if (idx >= 0) {
|
||
const srow_t *r = &SMENU[idx];
|
||
sel = idx; need_paint = 1; sel_changed = 1;
|
||
if (r->ctrl == SC_SWITCH) {
|
||
int on = 0; meow_settings_get_switch(r->key, &on);
|
||
meow_settings_set_switch(r->key, !on);
|
||
s_commit(r, 1);
|
||
} else if (r->ctrl == SC_LIST) {
|
||
/* M2.39 网络列表行:开下拉(重新解析)+ 重绘 + 排空本页 tfd
|
||
* (复刻 s_dropdown 后的穿透防护,见下方 ENUM/PRESET 分支)。 */
|
||
after_modal = 1;
|
||
s_net_open(r);
|
||
if (tfd >= 0) {
|
||
unsigned char e[16];
|
||
while (read(tfd, e, sizeof e) == 16) {}
|
||
usleep(40000);
|
||
while (read(tfd, e, sizeof e) == 16) {}
|
||
}
|
||
td = 0; td_ok = 0; drag = -1; scroll_drag = 0;
|
||
scroll_base = 0; scroll_y0 = 0;
|
||
tdx = tdy = tlx = tly = -1;
|
||
touch_cooldown_until = now_ms() + 300;
|
||
memset(&kd_pwr, 0, sizeof kd_pwr);
|
||
memset(&kd_vold, 0, sizeof kd_vold);
|
||
memset(&kd_volu, 0, sizeof kd_volu);
|
||
memset(&kd_play, 0, sizeof kd_play);
|
||
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
|
||
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
|
||
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
|
||
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
|
||
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
|
||
need_paint = 1;
|
||
} else if (r->ctrl == SC_ENUM || r->ctrl == SC_PRESET) {
|
||
/* 单次点击即开选择器子对话框 */
|
||
after_modal = 1; /* 关闭后整帧重绘(还原被 dim 的状态栏) */
|
||
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/<pkg>/<binary> → <pkg> */
|
||
static int meow_pkg_name(const char *exec, char *out, int n)
|
||
{
|
||
if (!exec || strncmp(exec, MEOWAPPS, strlen(MEOWAPPS)) != 0) { if (n>0) out[0]=0; return -1; }
|
||
const char *p = exec + strlen(MEOWAPPS);
|
||
if (*p != '/') { if (n>0) out[0]=0; return -1; }
|
||
p++;
|
||
const char *end = strchr(p, '/');
|
||
int len = end ? (int)(end - p) : (int)strlen(p);
|
||
if (len <= 0 || len >= n) { if (n>0) out[0]=0; return -1; }
|
||
memcpy(out, p, len); out[len] = 0;
|
||
return 0;
|
||
}
|
||
|
||
/* 启动应用前准备数据目录:建 MeowData/<pkg>/{data,cache}(如缺)。幂等。
|
||
* 失败只 log(MeowData 所在卡不存在/只读),绝不崩 —— 零卡依赖:应用需按
|
||
* APPS.md 规范容忍无数据目录。返回 0=就绪,-1=pkg 不在卡上/不能判名。 */
|
||
static int meow_app_prepare(const char *exec)
|
||
{
|
||
char pkg[64];
|
||
if (meow_pkg_name(exec, pkg, sizeof pkg) != 0) return -1;
|
||
if (!sd_device_present()) return -1; /* 卡不在,跳过(应用仍可跑非卡资源) */
|
||
char cmd[320];
|
||
snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' "
|
||
"'/usr/data/mnt/sd_0/MeowData/%s/cache' 2>/dev/null", pkg, pkg);
|
||
int rc = system(cmd);
|
||
logmsg("app_prepare: pkg=%s rc=%d", pkg, rc);
|
||
return rc == 0 ? 0 : -1;
|
||
}
|
||
|
||
/* .mpk 安装(本期接口/CLI + 文档;UI 后期):解压 .mpk → MeowAPPs/<pkg>/。
|
||
* 约定 .mpk = tar.gz,顶层即包目录(含 app.json);包名 = 文件名茎 <pkg>.mpk。
|
||
* 同名先备份;解压后校验 app.json;随后数据 init(MeowData/<pkg>/{data,cache})。
|
||
* 返回 0 ok / 负=错误。 */
|
||
static int meow_pkg_install(const char *mpk_path)
|
||
{
|
||
if (!mpk_path || access(mpk_path, R_OK) != 0) { logmsg("pkg_install: bad mpk %s", mpk_path?mpk_path:"(null)"); return -1; }
|
||
if (!sd_device_present()) { logmsg("pkg_install: no card"); return -1; }
|
||
if (strpbrk(mpk_path, " ;`$'\"&|<>") != NULL) { logmsg("pkg_install: unsafe path"); return -2; }
|
||
/* 包名 = 文件名茎(去掉 .mpk) */
|
||
const char *bn = strrchr(mpk_path, '/');
|
||
bn = bn ? bn + 1 : mpk_path;
|
||
const char *dot = strrchr(bn, '.');
|
||
char pkg[64];
|
||
if (!dot || strcmp(dot, ".mpk") != 0) { logmsg("pkg_install: need <pkg>.mpk"); return -1; }
|
||
int len = (int)(dot - bn);
|
||
if (len <= 0 || len >= (int)sizeof pkg) { logmsg("pkg_install: bad pkg name"); return -1; }
|
||
memcpy(pkg, bn, len); pkg[len] = 0;
|
||
/* 目标 MeowAPPs/<pkg>/;同名先备份(.bak_<pid>),再解压,再校验 app.json */
|
||
char dst[256], bak[256], cmd[700];
|
||
snprintf(dst, sizeof dst, "/usr/data/mnt/sd_0/MeowAPPs/%s", pkg);
|
||
snprintf(bak, sizeof bak, "%s.bak_%d", dst, (int)getpid());
|
||
/* BusyBox tar 无 -z:用 gzip -dc | tar -x 处理 tar.gz。 */
|
||
snprintf(cmd, sizeof cmd,
|
||
"rm -rf '%s' 2>/dev/null; "
|
||
"[ -d '%s' ] && mv '%s' '%s' 2>/dev/null; "
|
||
"mkdir -p '%s' && gzip -dc '%s' | tar -x -C '%s' 2>/dev/null && "
|
||
"[ -f '%s/app.json' ]", bak, dst, dst, bak, dst, mpk_path, dst, dst);
|
||
if (system(cmd) != 0) { logmsg("pkg_install: extract/validate fail for %s", pkg); return -1; }
|
||
/* 数据 init:MeowData/<pkg>/{data,cache} */
|
||
snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' "
|
||
"'/usr/data/mnt/sd_0/MeowData/%s/cache' 2>/dev/null", pkg, pkg);
|
||
system(cmd);
|
||
logmsg("pkg_install: %s installed (old backed up %s)", pkg, bak);
|
||
return 0;
|
||
}
|
||
|
||
/* .mpk 导出:打包 MeowAPPs/<pkg>/(数据目录本就由 env 指向 MeowData,MeowAPPs/<pkg> 下无
|
||
* data/cache 实体,直接全打包)→ <pkg>.mpk。返回 0 ok。 */
|
||
static int meow_pkg_export(const char *pkg, const char *out_path)
|
||
{
|
||
if (!pkg || !out_path || strpbrk(pkg, " ;`$'\"&|<>/" ) || strpbrk(out_path, " ;`$'\"&|<>")) {
|
||
logmsg("pkg_export: unsafe path"); return -2;
|
||
}
|
||
if (!sd_mounted()) { logmsg("pkg_export: card not mounted"); return -1; }
|
||
char src[256], dst[256];
|
||
snprintf(src, sizeof src, "/usr/data/mnt/sd_0/MeowAPPs/%s", pkg);
|
||
if (access(src, R_OK) != 0) { logmsg("pkg_export: no such pkg"); return -1; }
|
||
snprintf(dst, sizeof dst, "%s", out_path);
|
||
/* BusyBox tar 无 -z:tar -c | gzip -c > out(先写 .tmp 再 mv,防半成品)。 */
|
||
char cmd[512];
|
||
snprintf(cmd, sizeof cmd, "tar -c -C '%s' . | gzip -c > '%s.tmp' 2>/dev/null && "
|
||
"mv '%s.tmp' '%s'", src, dst, dst, dst);
|
||
if (system(cmd) != 0) { logmsg("pkg_export: tar/gzip fail"); return -1; }
|
||
logmsg("pkg_export: %s -> %s", pkg, dst);
|
||
return 0;
|
||
}
|
||
|
||
/* ================================================================== */
|
||
/* 阶段 D · M0:AF_UNIX IPC 骨架 */
|
||
/* IPC 线程只读帧+解析+入队(SPSC 无锁环 + eventfd 唤醒主循环); */
|
||
/* 主循环 meow_ipc_process() 单写者改状态并回 ACK/ERR/RESULT。 */
|
||
/* 遵守 FRAMEWORK_DESIGN §1/§6:主循环 10ms 热键轮询绝不被 socket IO 阻塞。 */
|
||
/* ================================================================== */
|
||
#define MEOW_IPC_RING 16 /* 无锁环深度(单生产者=IPC线程,单消费者=主循环) */
|
||
#define MEOW_IPC_CONNS 10 /* 同时接入连接上限(backlog 4 + 余量) */
|
||
#define MEOW_IPC_BACKLOG 4
|
||
|
||
/* 入队消息:类型 + 请求 id + 客户端 fd(回包用)+ 负载拷贝(≤CARGO) */
|
||
struct meow_ipc_msg {
|
||
uint16_t type;
|
||
uint16_t plen;
|
||
uint32_t req_id;
|
||
int fd;
|
||
uint8_t payload[MEOW_IPC_CARGO];
|
||
};
|
||
|
||
static struct meow_ipc_msg ipc_ring[MEOW_IPC_RING];
|
||
static volatile uint32_t ipc_head = 0; /* 消费端(主循环)推进 */
|
||
static volatile uint32_t ipc_tail = 0; /* 生产端(IPC线程)推进 */
|
||
static void ipc_dispatch(struct meow_ipc_msg *m); /* 定义在下方,主循环排空用 */
|
||
static int ipc_event_fd = -1;
|
||
static volatile int ipc_shutdown = 0;
|
||
static volatile int ipc_thread_ok = 0; /* bind+listen 成功 */
|
||
static char ipc_sock_path[MEOW_IPC_SOCK_LEN] = MEOW_IPC_PATH;
|
||
static pthread_t ipc_pthread;
|
||
|
||
/* 每连接字节流帧组装状态(TCP 流语义下逐字节缓冲,跨 poll 保留) */
|
||
struct meow_ipc_conn {
|
||
int fd;
|
||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||
int hdr_n;
|
||
uint8_t pay[MEOW_IPC_MAX];
|
||
uint32_t pay_tot;
|
||
uint32_t pay_n;
|
||
};
|
||
static struct meow_ipc_conn ipc_conns[MEOW_IPC_CONNS];
|
||
|
||
/* 写一帧(16B 头 + 负载)。返回写入字节;<=0 视为失败。 */
|
||
static size_t ipc_send(int fd, uint16_t type, uint32_t req_id,
|
||
const void *payload, uint32_t len)
|
||
{
|
||
if (fd < 0) return 0;
|
||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||
uint32_t magic = MEOW_IPC_MAGIC;
|
||
uint16_t ver = MEOW_IPC_VERSION;
|
||
memcpy(hdr + 0, &magic, 4);
|
||
memcpy(hdr + 4, &ver, 2);
|
||
memcpy(hdr + 6, &type, 2);
|
||
memcpy(hdr + 8, &req_id, 4);
|
||
memcpy(hdr + 12, &len, 4);
|
||
size_t off = 0;
|
||
while (off < sizeof hdr) {
|
||
ssize_t w = write(fd, hdr + off, sizeof hdr - off);
|
||
if (w <= 0) return off;
|
||
off += (size_t)w;
|
||
}
|
||
if (len && payload) {
|
||
size_t p = 0;
|
||
while (p < len) {
|
||
ssize_t w = write(fd, (const uint8_t *)payload + p, len - p);
|
||
if (w <= 0) break;
|
||
p += (size_t)w;
|
||
}
|
||
off += p;
|
||
}
|
||
return off;
|
||
}
|
||
|
||
/* 组装完的帧入队(SPSC 无锁,先写槽再推进 tail,__sync 发布)。 */
|
||
static void ipc_enqueue_frame(struct meow_ipc_conn *c)
|
||
{
|
||
uint16_t type;
|
||
uint32_t req_id, len;
|
||
memcpy(&type, c->hdr + 6, 2);
|
||
memcpy(&req_id, c->hdr + 8, 4);
|
||
len = c->pay_tot;
|
||
if ((ipc_tail - ipc_head) >= MEOW_IPC_RING) {
|
||
logmsg("ipc: ring full, drop frame type=0x%04x", type);
|
||
return;
|
||
}
|
||
uint32_t slot = ipc_tail % MEOW_IPC_RING;
|
||
struct meow_ipc_msg *m = &ipc_ring[slot];
|
||
m->type = type;
|
||
m->req_id = req_id;
|
||
m->fd = c->fd;
|
||
uint32_t copy = len < MEOW_IPC_CARGO ? len : MEOW_IPC_CARGO;
|
||
m->plen = (uint16_t)copy;
|
||
memcpy(m->payload, c->pay, copy);
|
||
__sync_synchronize();
|
||
ipc_tail++;
|
||
if (ipc_event_fd >= 0) { uint64_t one = 1; (void)write(ipc_event_fd, &one, sizeof one); }
|
||
}
|
||
|
||
/* 读一个连接的可用字节并组装成帧(>=1 完整帧即入队)。返回 0=ok(可能已入队),
|
||
* -1=连接关闭/坏帧(调用方应关闭并回收)。 */
|
||
static int ipc_conn_read(struct meow_ipc_conn *c)
|
||
{
|
||
for (;;) {
|
||
if (c->hdr_n < MEOW_IPC_HDR_LEN) {
|
||
ssize_t r = read(c->fd, c->hdr + c->hdr_n, MEOW_IPC_HDR_LEN - c->hdr_n);
|
||
if (r < 0) { if (errno == EAGAIN) return 0; return -1; }
|
||
if (r == 0) return -1;
|
||
c->hdr_n += (int)r;
|
||
if (c->hdr_n < MEOW_IPC_HDR_LEN) return 0;
|
||
uint32_t magic, len;
|
||
uint16_t ver;
|
||
memcpy(&magic, c->hdr, 4);
|
||
memcpy(&ver, c->hdr + 4, 2);
|
||
memcpy(&len, c->hdr + 12, 4);
|
||
if (magic != MEOW_IPC_MAGIC) { logmsg("ipc: bad magic %08x", magic); return -1; }
|
||
if (ver != MEOW_IPC_VERSION) { logmsg("ipc: bad version %u", ver); return -1; }
|
||
if (len > MEOW_IPC_MAX) { logmsg("ipc: frame len %u too long", len); return -1; }
|
||
c->pay_tot = len;
|
||
c->pay_n = 0;
|
||
if (len == 0) {
|
||
ipc_enqueue_frame(c);
|
||
c->hdr_n = 0;
|
||
continue;
|
||
}
|
||
}
|
||
if (c->pay_n < c->pay_tot) {
|
||
ssize_t r = read(c->fd, c->pay + c->pay_n, c->pay_tot - c->pay_n);
|
||
if (r < 0) { if (errno == EAGAIN) return 0; return -1; }
|
||
if (r == 0) return -1;
|
||
c->pay_n += (uint32_t)r;
|
||
if (c->pay_n < c->pay_tot) return 0;
|
||
}
|
||
ipc_enqueue_frame(c);
|
||
c->hdr_n = 0;
|
||
c->pay_n = 0;
|
||
}
|
||
}
|
||
|
||
static void *ipc_thread(void *arg)
|
||
{
|
||
(void)arg;
|
||
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||
if (lfd < 0) { logmsg("ipc: socket() fail %s", strerror(errno)); return NULL; }
|
||
unlink(ipc_sock_path);
|
||
struct sockaddr_un a;
|
||
memset(&a, 0, sizeof a);
|
||
a.sun_family = AF_UNIX;
|
||
strncpy(a.sun_path, ipc_sock_path, sizeof a.sun_path - 1);
|
||
if (bind(lfd, (struct sockaddr *)&a, sizeof a) < 0) {
|
||
logmsg("ipc: bind %s fail %s", ipc_sock_path, strerror(errno));
|
||
close(lfd);
|
||
return NULL;
|
||
}
|
||
chmod(ipc_sock_path, 0666);
|
||
if (listen(lfd, MEOW_IPC_BACKLOG) < 0) {
|
||
logmsg("ipc: listen fail %s", strerror(errno));
|
||
close(lfd);
|
||
return NULL;
|
||
}
|
||
fcntl(lfd, F_SETFL, O_NONBLOCK);
|
||
ipc_thread_ok = 1;
|
||
logmsg("ipc: listening on %s (backlog %d)", ipc_sock_path, MEOW_IPC_BACKLOG);
|
||
|
||
struct pollfd fds[MEOW_IPC_CONNS + 1];
|
||
int nfds = 1;
|
||
fds[0].fd = lfd; fds[0].events = POLLIN; fds[0].revents = 0;
|
||
while (!ipc_shutdown) {
|
||
int r = poll(fds, nfds, 400);
|
||
if (r < 0) { if (errno == EINTR) continue; break; }
|
||
if (r == 0) continue;
|
||
if (fds[0].revents & POLLIN) {
|
||
for (;;) {
|
||
int c = accept(lfd, NULL, NULL);
|
||
if (c < 0) break;
|
||
fcntl(c, F_SETFL, O_NONBLOCK);
|
||
if (nfds <= MEOW_IPC_CONNS) {
|
||
ipc_conns[nfds - 1].fd = c;
|
||
ipc_conns[nfds - 1].hdr_n = 0;
|
||
ipc_conns[nfds - 1].pay_n = 0;
|
||
ipc_conns[nfds - 1].pay_tot = 0;
|
||
fds[nfds].fd = c;
|
||
fds[nfds].events = POLLIN;
|
||
fds[nfds].revents = 0;
|
||
nfds++;
|
||
logmsg("ipc: conn %d accept (nfds=%d)", c, nfds - 1);
|
||
} else {
|
||
close(c);
|
||
logmsg("ipc: conn %d rejected (full)", c);
|
||
}
|
||
}
|
||
}
|
||
int i = 1;
|
||
while (i < nfds) {
|
||
int ev = fds[i].revents;
|
||
if (ev & (POLLIN | POLLHUP | POLLERR | POLLNVAL)) {
|
||
struct meow_ipc_conn *cc = &ipc_conns[i - 1];
|
||
if (ipc_conn_read(cc) < 0) {
|
||
logmsg("ipc: conn %d closed", cc->fd);
|
||
close(cc->fd);
|
||
for (int j = i; j < nfds - 1; j++) {
|
||
fds[j] = fds[j + 1];
|
||
ipc_conns[j - 1] = ipc_conns[j];
|
||
}
|
||
nfds--;
|
||
continue;
|
||
}
|
||
}
|
||
i++;
|
||
}
|
||
for (int j = 0; j < nfds; j++) fds[j].revents = 0;
|
||
}
|
||
for (int i = 1; i < nfds; i++) close(fds[i].fd);
|
||
close(lfd);
|
||
unlink(ipc_sock_path);
|
||
logmsg("ipc: thread exit");
|
||
return NULL;
|
||
}
|
||
|
||
/* 启动 IPC 线程(主循环任意时刻调一次)。返回 0=就绪,-1=线程创建失败。
|
||
* (bind/listen 失败记 ipc_thread_ok=0,随后 meow_ipc_process 仍安全轮询空队列。) */
|
||
static int meow_ipc_init(void)
|
||
{
|
||
ipc_event_fd = eventfd(0, EFD_NONBLOCK);
|
||
if (ipc_event_fd < 0)
|
||
logmsg("ipc: eventfd fail %s (10ms poll drains anyway)", strerror(errno));
|
||
if (pthread_create(&ipc_pthread, NULL, ipc_thread, NULL) != 0) {
|
||
logmsg("ipc: pthread_create fail");
|
||
return -1;
|
||
}
|
||
usleep(60000); /* 给 bind+listen 一点时间,log 就绪状态 */
|
||
if (ipc_thread_ok) logmsg("ipc: ready (eventfd=%d)", ipc_event_fd);
|
||
else logmsg("ipc: NOT ready (bind/listen failed, thread exiting)");
|
||
return ipc_thread_ok ? 0 : -1;
|
||
}
|
||
|
||
/* 主循环:非阻塞排空 eventfd + 处理入队帧(单写者改状态并回包)。
|
||
* 事件fd 仅为低延迟优化;若为 -1,10ms 轮询仍保证每圈排空。 */
|
||
static void meow_ipc_process(void)
|
||
{
|
||
if (ipc_event_fd >= 0) {
|
||
uint64_t v;
|
||
while (read(ipc_event_fd, &v, sizeof v) == (ssize_t)sizeof v) {}
|
||
}
|
||
while (ipc_head != ipc_tail) {
|
||
struct meow_ipc_msg *m = &ipc_ring[ipc_head % MEOW_IPC_RING];
|
||
ipc_dispatch(m);
|
||
ipc_head++;
|
||
}
|
||
__sync_synchronize();
|
||
}
|
||
|
||
/* 处理一条入队帧:HELLO→ACK、ACT_QUERY→ACT_RESULT/ERR、ACT_DECLARE→ACK、其他→ERR。 */
|
||
static void ipc_dispatch(struct meow_ipc_msg *m)
|
||
{
|
||
switch (m->type) {
|
||
case MSG_HELLO: {
|
||
m_hello_t h;
|
||
memset(&h, 0, sizeof h);
|
||
memcpy(&h, m->payload, m->plen < (uint16_t)sizeof h ? m->plen : (uint16_t)sizeof h);
|
||
h.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||
meow_acct_hello(h.app_id, h.version, h.flags);
|
||
m_ack_t a;
|
||
a.code = MEOW_OK;
|
||
a.status = (uint32_t)g_naccts;
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
logmsg("ipc: HELLO app=%s ver=%u -> ACK naccts=%d", h.app_id, h.version, g_naccts);
|
||
break;
|
||
}
|
||
case MSG_ACT_DECLARE: {
|
||
m_act_declare_t d;
|
||
memset(&d, 0, sizeof d);
|
||
memcpy(&d, m->payload, m->plen < (uint16_t)sizeof d ? m->plen : (uint16_t)sizeof d);
|
||
d.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||
meow_acct_declare(d.app_id, d.entries, d.count);
|
||
m_ack_t a;
|
||
a.code = MEOW_OK;
|
||
a.status = (uint32_t)g_naccts;
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
logmsg("ipc: ACT_DECLARE app=%s n=%u -> ACK", d.app_id, d.count);
|
||
break;
|
||
}
|
||
case MSG_ACT_QUERY: {
|
||
m_act_query_t q;
|
||
memset(&q, 0, sizeof q);
|
||
memcpy(&q, m->payload, m->plen < (uint16_t)sizeof q ? m->plen : (uint16_t)sizeof q);
|
||
q.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||
q.activity[MEOW_ACT_MAX - 1] = 0;
|
||
char chain[MEOW_CHAIN_MAX] = "";
|
||
int depth = 0;
|
||
int code = resolve_chain(q.app_id, q.activity, chain, &depth);
|
||
if (code == MEOW_OK) {
|
||
m_act_result_t r;
|
||
memset(&r, 0, sizeof r);
|
||
r.depth = (uint32_t)depth;
|
||
r.code = MEOW_OK;
|
||
size_t cl = strlen(chain);
|
||
if (cl >= MEOW_CHAIN_MAX) cl = MEOW_CHAIN_MAX - 1;
|
||
memcpy(r.chain, chain, cl);
|
||
r.chain_len = (uint16_t)cl;
|
||
ipc_send(m->fd, MSG_ACT_RESULT, m->req_id, &r, sizeof r);
|
||
logmsg("ipc: ACT_QUERY app=%s act=%s -> depth=%d chain=%s",
|
||
q.app_id, q.activity, depth, chain);
|
||
} else {
|
||
m_err_t e;
|
||
memset(&e, 0, sizeof e);
|
||
e.code = (uint32_t)code;
|
||
const char *msg = (code == MEOW_ERR_NOENT) ? "unregistered app/activity"
|
||
: "resolve failed";
|
||
e.msg_len = (uint16_t)strlen(msg);
|
||
memcpy(e.msg, msg, e.msg_len);
|
||
ipc_send(m->fd, MSG_ERR, m->req_id, &e, sizeof e);
|
||
logmsg("ipc: ACT_QUERY app=%s act=%s -> ERR(0x%x)",
|
||
q.app_id, q.activity, code);
|
||
}
|
||
break;
|
||
}
|
||
case MSG_ACT_FOCUS: {
|
||
m_act_focus_t f;
|
||
memset(&f, 0, sizeof f);
|
||
memcpy(&f, m->payload, m->plen < (uint16_t)sizeof f ? m->plen : (uint16_t)sizeof f);
|
||
f.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||
f.activity[MEOW_ACT_MAX - 1] = 0;
|
||
meow_fg_act_set(f.app_id, f.activity); /* M2.17:当前顶层 activity(keep_awake 判定) */
|
||
m_ack_t a;
|
||
a.code = MEOW_OK;
|
||
a.status = (uint32_t)g_naccts;
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
logmsg("ipc: ACT_FOCUS app=%s act=%s -> ACK", f.app_id, f.activity);
|
||
break;
|
||
}
|
||
case MSG_ACT_BACK: {
|
||
meow_fg_act_back(); /* 回到根/无 activity:清 keep_awake */
|
||
m_ack_t a;
|
||
a.code = MEOW_OK;
|
||
a.status = (uint32_t)g_naccts;
|
||
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
|
||
logmsg("ipc: ACT_BACK -> ACK");
|
||
break;
|
||
}
|
||
/* M2a:通知存储(NOTIFICATION_SYSTEM.md §4/§7/§8)。IPC 线程只入队,此处在主循环
|
||
* 单写者落存储 + 回 ACK(与 HELLO/QUERY 同一线程模型,绝不在 socket IO 阻塞)。 */
|
||
case MSG_NOTIFY: {
|
||
m_notify_t mn;
|
||
memset(&mn, 0, sizeof mn);
|
||
memcpy(&mn, m->payload, m->plen < (uint16_t)sizeof mn ? m->plen : (uint16_t)sizeof mn);
|
||
mn.app_id[MEOW_APP_ID_MAX - 1] = 0;
|
||
mn.category[23] = 0;
|
||
mn.summary[63] = 0;
|
||
mn.body[95] = 0;
|
||
mn.app_icon[47] = 0;
|
||
mn.target_activity[MEOW_ACT_MAX - 1] = 0;
|
||
/* 映射到存储结构(freedesktop + MeowOS 扩展;actions/hints v1 空) */
|
||
meow_notif_t n;
|
||
memset(&n, 0, sizeof n);
|
||
n.id = mn.replaces_id; /* replaces_id==0 => 新建;>0 => 替换(不存在则新建) */
|
||
snprintf(n.app_id, sizeof n.app_id, "%s", mn.app_id);
|
||
snprintf(n.category, sizeof n.category, "%s", mn.category);
|
||
snprintf(n.summary, sizeof n.summary, "%s", mn.summary);
|
||
snprintf(n.body, sizeof n.body, "%s", mn.body);
|
||
snprintf(n.app_icon, sizeof n.app_icon, "%s", mn.app_icon);
|
||
snprintf(n.target_activity, sizeof n.target_activity, "%s", mn.target_activity);
|
||
n.expire_timeout = mn.expire_timeout;
|
||
n.progress = mn.progress;
|
||
n.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/<pkg>/{data,cache}(幂等;卡不在/失败只 log 不崩)。
|
||
* 应用经环境变量 MEOW_DATA_DIR/MEOW_CACHE_DIR 定位数据(exFAT 无 symlink)。 */
|
||
meow_app_prepare(a->exec);
|
||
|
||
/* save the brightness the player had before the menu/app session, then
|
||
* drain input queues so the app starts clean */
|
||
saved_brightness = read_brightness();
|
||
if (saved_brightness <= 0) saved_brightness = 60; /* standby/unknown */
|
||
logmsg("saved brightness = %d", saved_brightness);
|
||
drain_input();
|
||
|
||
/* Bug 3: distinguish a REAL exec failure from a clean app exit. The child
|
||
* holds the write end of a CLOEXEC pipe; if execve() fails it writes the
|
||
* errno and _exit(127), if it succeeds the fd closes on exec so the parent
|
||
* read() returns EOF. Parent blocks on read() until the child either execs
|
||
* (success) or reports the error (failure); only then do we spawn the
|
||
* watchdog and waitpid — so an exec failure never leaves an orphan watchdog
|
||
* and never masquerades as "app ran and exited". */
|
||
int execpipe[2];
|
||
if (pipe(execpipe) < 0) { logmsg("exec pipe failed: %s", strerror(errno)); return -1; }
|
||
fcntl(execpipe[1], F_SETFD, FD_CLOEXEC);
|
||
|
||
pid_t pid = fork();
|
||
if (pid < 0) {
|
||
close(execpipe[0]); close(execpipe[1]);
|
||
logmsg("fork failed: %s", strerror(errno)); return -1;
|
||
}
|
||
if (pid == 0) {
|
||
/* child: exec the app. M2.12 注入 MEOW_LOG_DIR + MEOW_DATA_DIR/MEOW_CACHE_DIR
|
||
* (exFAT 无 symlink,数据目录用 env 传给应用)。PATH 继承 launcher(含卡上 opt/bin
|
||
* 若在),卡不在则回退普通 PATH。M2.52 追加注入 MEOW_LOG_APP(app_id)+MEOW_LOG_LEVEL
|
||
* (读全局 log.level),使 SDK meow_log() 按 app 分离写 apps/<app_id>.log 并做级别过滤。 */
|
||
close(execpipe[0]);
|
||
char *argv[] = { a->exec, NULL };
|
||
char env_path[320], env_log[320], env_data[320], env_cache[320];
|
||
char env_app[MEOW_APP_ID_MAX + 16], env_lvl[48];
|
||
char env_fpng[400], env_fidx[400], env_fapp[96];
|
||
char env_mf[24];
|
||
char env_tier[24];
|
||
char env_fs[20];
|
||
char env_cls[MEOW_FCLS_COUNT][2][400];
|
||
char *envp[40]; /* PATH,HOME,log_dir,log_app,log_lvl,font 全局(3),多字体(1),tier(1),class(≤12),fullscreen(1),data,cache */
|
||
int ne = 0;
|
||
const char *cp = getenv("PATH");
|
||
snprintf(env_path, sizeof env_path, "PATH=%s", cp ? cp : "/usr/bin:/bin");
|
||
envp[ne++] = env_path;
|
||
envp[ne++] = "HOME=/root";
|
||
if (log_dir[0]) { snprintf(env_log, sizeof env_log, "MEOW_LOG_DIR=%s", log_dir); envp[ne++] = env_log; }
|
||
/* M2.52:MEOW_LOG_APP=app_id(=app.json.package,缺省回退目录名;与注册表一致;无 id 则跳过,SDK 兜底 "app") */
|
||
if (a->id[0]) { snprintf(env_app, sizeof env_app, "MEOW_LOG_APP=%s", a->id); envp[ne++] = env_app; }
|
||
/* M2.52:MEOW_LOG_LEVEL 读全局 log.level(error/warn/info/debug→索引0/1/2/3),
|
||
* SDK 只认大写字符串名(ERROR/WARN/INFO/DEBUG),故转大写注入;缺省兜底 info。 */
|
||
{
|
||
static const char *lvl_name[] = { "ERROR", "WARN", "INFO", "DEBUG" };
|
||
int lv = 2; /* 默认 info */
|
||
meow_settings_get_enum("log.level", &lv);
|
||
if (lv < 0 || lv > 3) lv = 2;
|
||
snprintf(env_lvl, sizeof env_lvl, "MEOW_LOG_LEVEL=%s", lvl_name[lv]);
|
||
envp[ne++] = env_lvl;
|
||
}
|
||
/* M2.x:注入「当前生效字体」= 系统 font.file(launcher 已 load 的那套),
|
||
* SDK meow_ui_gfx.load_font 优先读 MEOW_FONT_PNG/IDX,app 未自带字库(§5.7 load_handle
|
||
* 未实现)时跟随系统字体,不再硬编码 MeowAPPs/fonts。无卡/无可用字体则跳过注入,
|
||
* SDK 自行回退卡默认→固件→点阵。 */
|
||
{
|
||
static const char *cls_env_png[MEOW_FCLS_COUNT] = {
|
||
"MEOW_FONT_CLS_CJK_PNG", "MEOW_FONT_CLS_CJK_SYM_PNG", "MEOW_FONT_CLS_NUM_PNG",
|
||
"MEOW_FONT_CLS_LATIN_PNG", "MEOW_FONT_CLS_LATIN_SYM_PNG", "MEOW_FONT_CLS_OTHER_PNG" };
|
||
static const char *cls_env_idx[MEOW_FCLS_COUNT] = {
|
||
"MEOW_FONT_CLS_CJK_IDX", "MEOW_FONT_CLS_CJK_SYM_IDX", "MEOW_FONT_CLS_NUM_IDX",
|
||
"MEOW_FONT_CLS_LATIN_IDX", "MEOW_FONT_CLS_LATIN_SYM_IDX", "MEOW_FONT_CLS_OTHER_IDX" };
|
||
static const char *cls_keys[MEOW_FCLS_COUNT] = {
|
||
"font.class_cjk", "font.class_cjk_sym", "font.class_num",
|
||
"font.class_latin", "font.class_latin_sym", "font.class_other" };
|
||
char fpng[400], fidx[400], fam[96];
|
||
int have_global;
|
||
int mf_on = 0;
|
||
meow_settings_get_switch("display.multi_font", &mf_on);
|
||
snprintf(env_mf, sizeof env_mf, "MEOW_MULTI_FONT=%d", mf_on ? 1 : 0);
|
||
envp[ne++] = env_mf;
|
||
/* M2.5x:字库档 MEOW_FONT_TIER=<0|1|2> —— 按 app.json "fonts" 声明注入,并合并
|
||
* 「加载大字库」开关(font.load_full → 强制 ≥GBK)得**生效档**;0=默认4K常用,
|
||
* 1=大字库GBK,2=Full。SDK meow_ui_gfx 读入存档并按档装载(见
|
||
* meow_font_effective_paths_tiered;真正装载由 meow_font_reload_tier 实现)。 */
|
||
int eff_tier = meow_font_effective_tier(a->fonts);
|
||
snprintf(env_tier, sizeof env_tier, "MEOW_FONT_TIER=%d", eff_tier);
|
||
envp[ne++] = env_tier;
|
||
have_global = (meow_font_effective_paths_tiered(eff_tier, fpng, sizeof fpng,
|
||
fidx, sizeof fidx, fam, sizeof fam) == 0);
|
||
if (have_global) {
|
||
snprintf(env_fpng, sizeof env_fpng, "MEOW_FONT_PNG=%s", fpng);
|
||
snprintf(env_fidx, sizeof env_fidx, "MEOW_FONT_IDX=%s", fidx);
|
||
snprintf(env_fapp, sizeof env_fapp, "MEOW_FONT_APP=%s", fam);
|
||
envp[ne++] = env_fpng;
|
||
envp[ne++] = env_fidx;
|
||
envp[ne++] = env_fapp;
|
||
logmsg("launch: font env %s/%s (family=%s)", fpng, fidx, fam);
|
||
}
|
||
/* M2.5x 多字体:每类注入 png/idx(命中则覆盖全局;类值为空=跟随全局,不注入)。
|
||
* SDK 读这些 env 构建多字体映射;同 family 去重由 SDK 侧 meow_font_config_multifont 处理。 */
|
||
if (mf_on)
|
||
for (int c = 0; c < MEOW_FCLS_COUNT; c++) {
|
||
char ref[MEOW_SET_MAX_LEN], abs[400], idxabs[400];
|
||
if (meow_settings_get_str(cls_keys[c], ref, sizeof ref) != 0 || !ref[0]) continue;
|
||
if (meow_font_resolve_path(ref, abs, sizeof abs) != 0 || access(abs, R_OK) != 0) continue;
|
||
snprintf(idxabs, sizeof idxabs, "%s", abs);
|
||
size_t n4 = strlen(idxabs);
|
||
if (n4 > 4 && !strcmp(idxabs + n4 - 4, ".png")) {
|
||
idxabs[n4-4]='.'; idxabs[n4-3]='i'; idxabs[n4-2]='d'; idxabs[n4-1]='x'; idxabs[n4]=0;
|
||
}
|
||
snprintf(env_cls[c][0], sizeof env_cls[c][0], "%s=%s", cls_env_png[c], abs);
|
||
snprintf(env_cls[c][1], sizeof env_cls[c][1], "%s=%s", cls_env_idx[c], idxabs);
|
||
envp[ne++] = env_cls[c][0];
|
||
envp[ne++] = env_cls[c][1];
|
||
}
|
||
}
|
||
/* 全屏声明:app.json "fullscreen"(app 级)→ 注入 MEOW_FULLSCREEN=1,SDK 端
|
||
* meow_ui_init 据此预先设全屏。基座只接 app 级;活动级 route.<id>.fullscreen
|
||
* (meow_route.fullscreen)在 M3 活动切换时由后续代理消费(此处暂不下发)。
|
||
* 未声明则不注入(SDK 默认非全屏)。 */
|
||
if (a->fullscreen) {
|
||
snprintf(env_fs, sizeof env_fs, "MEOW_FULLSCREEN=1");
|
||
envp[ne++] = env_fs;
|
||
}
|
||
char pkg[64];
|
||
if (meow_pkg_name(a->exec, pkg, sizeof pkg) == 0) {
|
||
snprintf(env_data, sizeof env_data, "MEOW_DATA_DIR=/usr/data/mnt/sd_0/MeowData/%s/data", pkg);
|
||
snprintf(env_cache, sizeof env_cache, "MEOW_CACHE_DIR=/usr/data/mnt/sd_0/MeowData/%s/cache", pkg);
|
||
envp[ne++] = env_data;
|
||
envp[ne++] = env_cache;
|
||
}
|
||
envp[ne] = NULL;
|
||
execve(a->exec, argv, envp);
|
||
int e = errno;
|
||
(void)write(execpipe[1], &e, sizeof e); /* reached only on exec failure */
|
||
_exit(127);
|
||
}
|
||
close(execpipe[1]);
|
||
int exe_err = 0;
|
||
ssize_t er = read(execpipe[0], &exe_err, sizeof exe_err);
|
||
close(execpipe[0]);
|
||
if (er > 0) {
|
||
logmsg("launch FAILED execve(%s): %s (errno %d)", a->exec,
|
||
strerror(exe_err), exe_err);
|
||
int st = 0;
|
||
waitpid(pid, &st, 0); /* reap the failed child */
|
||
return -1;
|
||
}
|
||
|
||
/* GESTURE_BACK §2.4:应用启动即写 immersive 状态,watchdog 依此隐藏/保留虚拟返回键 */
|
||
meow_write_app_state(a->immersive == '1' ? 1 : 0);
|
||
|
||
/* watchdog for virtual back key / gestures */
|
||
char wd1[192], wd2[192], pidstr[16];
|
||
pid_t wpid = 0;
|
||
snprintf(pidstr, sizeof pidstr, "%d", pid);
|
||
if (access(WATCHDOG1, X_OK) == 0)
|
||
snprintf(wd1, sizeof wd1, "%s", WATCHDOG1);
|
||
else if (access(WATCHDOG2, X_OK) == 0)
|
||
snprintf(wd1, sizeof wd1, "%s", WATCHDOG2);
|
||
else
|
||
wd1[0] = 0;
|
||
if (wd1[0]) {
|
||
wpid = fork();
|
||
if (wpid == 0) {
|
||
/* pass the same boot log file + dir so all MeowAPPs components
|
||
* log to one place */
|
||
char *argv[] = { wd1, pidstr, NULL };
|
||
char *envp[] = { "PATH=/usr/bin:/bin", NULL, NULL, NULL };
|
||
char file_env[320], dir_env[256];
|
||
if (log_file[0]) {
|
||
snprintf(file_env, sizeof file_env, "MEOW_LOG_FILE=%s", log_file);
|
||
envp[1] = file_env;
|
||
}
|
||
if (log_dir[0]) {
|
||
snprintf(dir_env, sizeof dir_env, "MEOW_LOG_DIR=%s", log_dir);
|
||
envp[2] = dir_env;
|
||
}
|
||
execve(wd1, argv, envp);
|
||
_exit(0);
|
||
}
|
||
if (wpid > 0) logmsg("watchdog %s pid=%d", wd1, wpid);
|
||
}
|
||
|
||
/* wait for app; in-app kill = VOL_DOWN x3 quick taps (the old
|
||
* PREVIOUS+VOL_UP hold combo is retired — the PREVIOUS key's GPIO
|
||
* never fires on this unit, §18.12). Volume is snapped on the first
|
||
* tap and restored when the sequence completes. */
|
||
int status = 0;
|
||
int app_prev_vold = 0;
|
||
int app_seq_hits = 0;
|
||
unsigned long long app_seq_last = 0;
|
||
struct vol_snap app_vol0 = { -1, -1 };
|
||
/* M2.11 D:应用二进制/资源若在卡上,卡被拔可能 SIGBUS 崩。启动前记录,运行期监测卡移除。
|
||
* (拷贝到内存/板载 staging 的方案先不做,仅记录 APPS.md 规范。) */
|
||
int app_on_card = (strncmp(a->exec, MEOWAPPS, strlen(MEOWAPPS)) == 0);
|
||
if (app_on_card) logmsg("app on card (%s): monitoring card removal during run", a->exec);
|
||
struct keypoll kp_power, kp_keys;
|
||
kp_open(&kp_power, DEV_POWER);
|
||
kp_open(&kp_keys, DEV_KEYS);
|
||
/* M2.17 前台应用会话:系统仍统一控制息屏(应用不自管)。launcher 无法看到应用内触摸,
|
||
* 故以「可物理键」为 idle 信号;前台当前 activity(经 IPC MSG_ACT_FOCUS 上报)若 keep_awake
|
||
* 则不解屏(fg_keep_awake)。唤醒时应用随其输入重绘,launcher 仅 fb_pan_buf0_force 兜底
|
||
* (M2.8 唤醒重绘路径的首步)。见 §18.46。 */
|
||
unsigned long long app_last_input = now_ms();
|
||
unsigned long save_pow[KEYBUF_WORDS], save_keys[KEYBUF_WORDS];
|
||
memcpy(save_pow, kp_power.cur, sizeof save_pow);
|
||
memcpy(save_keys, kp_keys.cur, sizeof save_keys);
|
||
/* M2.24:清上一次会话可能残留的电源菜单标记,只认本次新 watchdog 生成的请求。 */
|
||
unlink(POWMENU_MARKER);
|
||
unlink(POWMENU_ACTIVE);
|
||
for (;;) {
|
||
int r = waitpid(pid, &status, WNOHANG);
|
||
if (r == pid) break;
|
||
if (r < 0 && errno != EINTR) break;
|
||
if (sigterm_flag) { logmsg("sigterm during app"); kill(pid, SIGTERM); break; }
|
||
/* M2.11 D:卡被拔(设备节点消失)且前台应用在卡上 → 主动 SIGTERM 干净退出,
|
||
* 避免应用等 SIGBUS 崩。回菜单走正常应用退出路径。 */
|
||
if (app_on_card && !sd_device_present()) {
|
||
logmsg("hotplug: card removed during app run -> SIGTERM app ");
|
||
kill(pid, SIGTERM);
|
||
usleep(300000);
|
||
if (waitpid(pid, &status, WNOHANG) != pid) { kill(pid, SIGKILL); waitpid(pid, &status, 0); }
|
||
break;
|
||
}
|
||
/* in-app kill: VOL_DOWN x3 quick taps */
|
||
kp_poll(&kp_power, NULL, NULL);
|
||
kp_poll(&kp_keys, NULL, NULL);
|
||
int vold = 0;
|
||
for (int w = 0; w < KEYBUF_WORDS; w++) {
|
||
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
|
||
}
|
||
/* M2.17 自动息屏(app 会话):任意物理键边沿重置计时;空闲>timeout 且非 keep_awake → 息屏;
|
||
* 息屏后再按键 → 唤醒(fb_pan_buf0_force 兜底 LCDC 落 buffer0,内容随应用输入重绘)。 */
|
||
{
|
||
int key_edge = 0;
|
||
for (int w = 0; w < KEYBUF_WORDS; w++)
|
||
if (kp_power.cur[w] != save_pow[w] || kp_keys.cur[w] != save_keys[w]) { key_edge = 1; break; }
|
||
if (key_edge) {
|
||
memcpy(save_pow, kp_power.cur, sizeof save_pow);
|
||
memcpy(save_keys, kp_keys.cur, sizeof save_keys);
|
||
app_last_input = now_ms();
|
||
if (meow_auto_slept) {
|
||
logmsg("auto screen-off (app): key wake");
|
||
meow_panel_on();
|
||
meow_auto_slept = 0;
|
||
fb_pan_buf0_force();
|
||
}
|
||
}
|
||
if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept && !fg_keep_awake()) {
|
||
if (now_ms() - app_last_input >= (unsigned long long)screen_off_timeout * 1000) {
|
||
meow_panel_off();
|
||
meow_auto_slept = 1;
|
||
logmsg("auto screen-off (app %s) after %ds idle (mode=%s keep_awake=%d)",
|
||
a->name, screen_off_timeout, screen_off_mode ? "black" : "panel",
|
||
fg_keep_awake());
|
||
}
|
||
}
|
||
}
|
||
if (vold && !app_prev_vold) {
|
||
unsigned long long nw = now_ms();
|
||
if (app_seq_hits == 0) app_vol0 = vol_snapshot();
|
||
else if (nw - app_seq_last > (unsigned)seq_gap_ms) {
|
||
app_seq_hits = 0;
|
||
app_vol0 = vol_snapshot();
|
||
}
|
||
app_seq_hits++;
|
||
app_seq_last = nw;
|
||
if (app_seq_hits >= 3) {
|
||
logmsg("kill seq: VOL_DOWN x3 -> SIGTERM app");
|
||
vol_restore(app_vol0);
|
||
kill(pid, SIGTERM);
|
||
usleep(500000);
|
||
kill(pid, SIGKILL);
|
||
waitpid(pid, &status, 0);
|
||
break;
|
||
}
|
||
}
|
||
app_prev_vold = vold;
|
||
if (app_seq_hits > 0 && now_ms() - app_seq_last > (unsigned)seq_gap_ms * 2)
|
||
app_seq_hits = 0;
|
||
/* M2.24 全面按键接管:watchdog 检测到电源长按≥3s 写 /tmp/meow_power_menu → 此处消费:
|
||
* SIGSTOP 应用冻结 → 写 powmenu_active 让 watchdog 让位 → 统一电源菜单(launcher 一处绘制,
|
||
* 复用 show_power_menu,含触摸/音量导航/电源确认/8s 强关/pm_interacted 保护)→ 菜单关 →
|
||
* 清 active → SIGCONT 应用续跑。 */
|
||
if (access(POWMENU_MARKER, F_OK) == 0) {
|
||
unlink(POWMENU_MARKER);
|
||
logmsg("watchdog: power menu request (app %s) -> SIGSTOP + menu", a->name);
|
||
kill(pid, SIGSTOP);
|
||
int mfd = open(POWMENU_ACTIVE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||
if (mfd >= 0) { write(mfd, "1", 1); close(mfd); }
|
||
int pm_ret = show_power_menu();
|
||
unlink(POWMENU_ACTIVE);
|
||
kill(pid, SIGCONT);
|
||
logmsg("watchdog: power menu closed (ret=%d) -> SIGCONT app", pm_ret);
|
||
/* M2.25:菜单期间按键可能已变化,重播 save_pow/save_keys 防误判 key_edge
|
||
* (避免菜单关闭瞬间误触发「键唤醒」或刷新自动息屏计时)。 */
|
||
kp_poll(&kp_power, NULL, NULL);
|
||
kp_poll(&kp_keys, NULL, NULL);
|
||
memcpy(save_pow, kp_power.cur, sizeof save_pow);
|
||
memcpy(save_keys, kp_keys.cur, sizeof save_keys);
|
||
app_last_input = now_ms(); /* M2.17:菜单交互即输入,重置应用会话自动息屏计时 */
|
||
if (pm_ret != 0) break; /* 关机/重启/回HibyOS:系统通常已 reboot,防御性退出 */
|
||
}
|
||
usleep(100000);
|
||
}
|
||
if (wpid > 0) {
|
||
/* reap the watchdog: SIGTERM then wait up to 2s (it exits when the
|
||
* app dies; bounded so we never hang if it is stuck) */
|
||
kill(wpid, SIGTERM);
|
||
unsigned long long wstart = now_ms();
|
||
while (now_ms() - wstart < 2000) {
|
||
if (waitpid(wpid, NULL, WNOHANG) == wpid) break;
|
||
usleep(50000);
|
||
}
|
||
}
|
||
meow_write_app_state(-1); /* GESTURE_BACK §2.4:应用退出,清 immersive 状态 */
|
||
/* M2.24:应用运行期音量由 watchdog 写(settings.conf 同源)。返回菜单前 re-load,
|
||
* 让菜单音量/状态栏与真实 DAC 一致(watchdog 可能已改)。 */
|
||
settings_load();
|
||
logmsg("app %s exited status=%d", a->name, WEXITSTATUS(status));
|
||
return 0;
|
||
}
|
||
|
||
/* ================================================================== */
|
||
/* main */
|
||
/* ================================================================== */
|
||
static void usage(void)
|
||
{
|
||
fprintf(stderr,
|
||
"usage: meow_launcherd [--sim-hotkey N] [--sim-tap N] [--sim-exit N]\n"
|
||
" [--sim-exit-tap N] [--sim-power-hold N]\n"
|
||
" [--sim-power-pick N] [--sim-no-exec]\n"
|
||
" [--dumpkeys N] [--scan] [--sysdump]\n"
|
||
" [--pkg-install <pkg.mpk>] [--pkg-export <pkg> <out.mpk>]\n"
|
||
" [--ipc-only N] [--ipc-sock <path>] [--appinfo <app.json>]\n"
|
||
" [--fontscan <card_root>]\n");
|
||
}
|
||
|
||
int main(int argc, char **argv)
|
||
{
|
||
long sim_hotkey = 0, sim_tap = 0, sim_exit = 0, dumpkeys = 0;
|
||
int do_scan = 0, do_sysdump = 0, sim_vol = 0;
|
||
long ipc_only_ms = 0;
|
||
const char *appinfo_path = NULL;
|
||
const char *fontscan_root = NULL; /* --fontscan <root>:宿主/qemu 测试字形扫描 */
|
||
const char *pkg_install_mpk = NULL, *pkg_export_name = NULL, *pkg_export_out = NULL;
|
||
for (int i = 1; i < argc; i++) {
|
||
if (!strcmp(argv[i], "--sim-hotkey") && i + 1 < argc) sim_hotkey = atol(argv[++i]);
|
||
else if (!strcmp(argv[i], "--sim-tap") && i + 1 < argc) sim_tap = atol(argv[++i]);
|
||
else if (!strcmp(argv[i], "--sim-exit") && i + 1 < argc) sim_exit = atol(argv[++i]);
|
||
else if (!strcmp(argv[i], "--sim-exit-tap") && i + 1 < argc) sim_exit_tap_ms = atol(argv[++i]);
|
||
else if (!strcmp(argv[i], "--sim-power-hold") && i + 1 < argc) sim_power_hold_ms = atol(argv[++i]);
|
||
else if (!strcmp(argv[i], "--sim-power-pick") && i + 1 < argc) sim_power_pick = atoi(argv[++i]);
|
||
else if (!strcmp(argv[i], "--sim-no-exec")) sim_no_exec = 1;
|
||
else if (!strcmp(argv[i], "--dumpkeys") && i + 1 < argc) dumpkeys = atol(argv[++i]);
|
||
else if (!strcmp(argv[i], "--scan")) do_scan = 1;
|
||
else if (!strcmp(argv[i], "--sysdump")) do_sysdump = 1;
|
||
else if (!strcmp(argv[i], "--sim-vol") && i + 1 < argc) sim_vol = atoi(argv[++i]);
|
||
else if (!strcmp(argv[i], "--ipc-only") && i + 1 < argc) ipc_only_ms = atol(argv[++i]);
|
||
else if (!strcmp(argv[i], "--ipc-sock") && i + 1 < argc) {
|
||
snprintf(ipc_sock_path, sizeof ipc_sock_path, "%s", argv[++i]);
|
||
}
|
||
else if (!strcmp(argv[i], "--appinfo") && i + 1 < argc) appinfo_path = argv[++i];
|
||
else if (!strcmp(argv[i], "--fontscan") && i + 1 < argc) fontscan_root = argv[++i];
|
||
else if (!strcmp(argv[i], "--pkg-install") && i + 1 < argc) pkg_install_mpk = argv[++i];
|
||
else if (!strcmp(argv[i], "--pkg-export") && i + 2 < argc) { pkg_export_name = argv[++i]; pkg_export_out = argv[++i]; }
|
||
else { usage(); return 1; }
|
||
}
|
||
|
||
/* unified log: TF card (or /tmp boot buffer when the card is absent) */
|
||
log_cleanup_old();
|
||
if (log_open_card() == 0)
|
||
fprintf(stderr, "log file: %s\n", log_file);
|
||
else
|
||
fprintf(stderr, "log: no TF card, using /tmp boot buffer (%s)\n",
|
||
BOOT_BUF_TMP);
|
||
logmsg("=== meow_launcherd start (sim_hotkey=%ld sim_tap=%ld sim_exit=%ld dumpkeys=%ld scan=%d) ===",
|
||
sim_hotkey, sim_tap, sim_exit, dumpkeys, do_scan);
|
||
|
||
load_config(); /* hotkey_seq / hotkey_seq_gap / bg_color (also run per
|
||
* menu open so edits apply without a daemon restart) */
|
||
settings_load(); /* M2.11:读系统设置(audio.volume 等),供音量/状态栏使用 */
|
||
|
||
/* M2.5x:启动扫描一次 TF 卡 MeowOS/fonts/<family>/,填充「字体文件」下拉 preset。
|
||
* 无卡/无字库目录 → 仅默认字体(等价现状)。进设置页时 run_settings_page 会再扫一次。 */
|
||
meow_font_scan_presets();
|
||
|
||
if (fontscan_root) {
|
||
/* M2.5x 测试钩子:扫描指定卡根下的 MeowOS/fonts/<family>/ 并打印 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;
|
||
}
|