闪光摩斯:让手电筒替你说话
想象一个很具体的场景:山里没有信号,手机只剩 30% 电,天已经黑了。你想让两公里外的人知道「我在这里,我需要帮助」,但你没有对讲机、没有卫星电话,连短信都发不出去。 这时候你手里其实已经握着两样东西:一颗能打到几百米外的 LED 灯,和一块能照亮整间屋子的屏幕。它们缺的只是一套语法。 「闪光摩斯」…

想象一个很具体的场景:山里没有信号,手机只剩 30% 电,天已经黑了。你想让两公里外的人知道「我在这里,我需要帮助」,但你没有对讲机、没有卫星电话,连短信都发不出去。
这时候你手里其实已经握着两样东西:一颗能打到几百米外的 LED 灯,和一块能照亮整间屋子的屏幕。它们缺的只是一套语法。
「闪光摩斯」(FlashMorse,包名 com.flashmorse.app,版本 1.0.0)就是把这两样东西变成发报机的 Android 应用:输入一句话,它按国际摩斯电码的节奏让灯亮灭,把文字变成光。
源码已以 MIT 许可开源:Fq77/FlashMorse。本文所有结论都可以在仓库里逐行核对,也可以自行构建安装。
它是什么
| 项 | 值 |
|---|---|
| 形态 | 单 Activity、Compose 全声明式 UI |
| 发光通道 | 后置闪光灯(LED)/ 整屏白光 |
| 速度范围 | 5–25 WPM,默认 10 WPM(单位时长 120 ms) |
| 编码标准 | ITU-R M.1677-1 国际摩斯码表 |
| 系统要求 | Android 7.0(API 24)及以上 |
| 权限 | 一个都不申请 |
| 网络 | 无 INTERNET 权限,物理上不可能联网 |
minSdk 24、targetSdk 37、compileSdk 37,Kotlin 2.4.10 + Jetpack Compose,界面体系用小米开源的 Miuix Compose(top.yukonga.miuix.kmp 0.9.4)。工程拆成 11 个 Gradle 模块,依赖方向严格单向。
为什么用光,而不是声音或震动
三种可选载体,各自的天花板很明确:
- 声音:传得远,但夜间发报等于告诉所有人你在哪;而且风噪一大就废。
- 震动:只能贴着身体,传输距离以厘米计。
- 光:视距内几乎无损,夜里几公里可见,而且安静。
摩斯电码在海上用灯语已经用了一百多年,这不是复古趣味,是它在「没有基础设施」这个约束下的最优解。所以这个 App 的定位不是「学摩斯的玩具」,而是基础设施消失时的最后一条信道——这一点决定了后面所有设计取舍。
三块主界面,一屏一件事
底部只有三个 TAB,左右滑动切换,没有第二层菜单:
- 发报:一个输入框 + 一个发送按钮。输入时实时显示点划流预览,发送时全屏只剩光和进度环。
- 短语:常用报文(「我在这里」「需要水」「一切正常」)一点即发,支持行内编辑、删除、长按拖拽排序;下方是同页的发送历史。
- 对照:完整码表网格,每个字符可单独试闪,用来给对方看「这就是 A」。
顶栏放两个入口:左侧是「求生与求救指南」,右侧是设置。设置里的速度、补偿、诊断、关于全部是压栈子页,天然带返回语义。
一处刻意的减法:没有暂停。光信号暂停几秒,对接收方来说就是乱码——要么发完,要么停止。
两块通道,怎么选
| 通道 | 实现 | 优势 | 代价 |
|---|---|---|---|
| LED | CameraManager.setTorchMode |
指向性强、单点亮度高、几公里可见 | HAL 有指令延迟,快节奏不可靠 |
| 屏幕 | 全屏白/黑切换 | 发光面积大、无延迟、任何设备都有 | OLED 长时间全白有烧屏风险 |
自动模式下有一条硬阈值:单位时长 ≥ 80 ms(≤ 15 WPM)才走 LED。超过 15 WPM,点只有 40 ms 出头,Camera HAL 的调度延迟会把点划比例彻底揉碎;这时自动切到屏幕,因为屏幕的通断是渲染管线里的一个 drawRect,没有中间层。用户也可以在设置里强制指定通道。
手电筒额外支持真实档位调光(API 33+ 的 turnOnTorchWithStrengthLevel,最多 100 级)。档位到亮度的映射做了 γ=2.2 的感知补偿——硬件档位近似线性对应电流,而人眼是幂律响应,不做补偿的话滑杆低段会陡得没法用。
零权限这件事,是可以核实的
AndroidManifest.xml 里没有任何 <uses-permission>:
<!-- 主力方案 CameraManager.setTorchMode 自 API 23 起无需 CAMERA 权限。
本应用不申请 INTERNET 权限,离线是核心卖点。 -->
setTorchMode 自 API 23 起就是免权限接口;turnOnTorchWithStrengthLevel 在 API 37 的 android.jar 里同样没有 @RequiresPermission 标注。所以「装完即用、不弹窗、不联网」不是文案,是三条可以从安装包直接验证的事实。
数据也全部留在本地:设置走 DataStore,短语与历史是应用私有目录下的 JSON 文件,没有数据库、没有同步、没有账号。
一些被否掉的设计
产品做到后面,删掉的东西比加上的更能说明取向:
- 浅色主题:盘点过成本,代码改动很小,难的是二次标定——发送卡的主色叠加、符号流的待发色在浅色下全部要重算。更关键的是,夜间发报时从亮界面切到全屏黑底,那次跳变会被误读成「信号已开始」。暂不做。
- 跟随系统主题:同上,夜间暗适应是功能约束,不是审美偏好。
- 屏幕通道调光:屏幕通道的全部价值就是「最亮、最容易被看见」,允许调暗等于自断唯一的优势。亮度设置只作用于手电筒。
- 前台服务:规划里有,实测发现发报是一次几十秒的交互,用户全程盯着屏幕,Service 带来的常驻通知和权限复杂度不值得。留到后续版本再评估。
技术栈
| 层 | 选型 |
|---|---|
| 语言 / UI | Kotlin 2.4.10 · Jetpack Compose(BOM 2026.02.01)· Miuix 0.9.4 |
| 构建 | Gradle 9.6 · AGP 9.4.1 · JDK 21 · compileSdk 37 |
| 导航 | Miuix Nav(NavDisplay + 可预测式返回) |
| 持久化 | DataStore Preferences + 应用私有 JSON |
| 时序 | HandlerThread(URGENT_AUDIO)+ 绝对时间轴调度 |
| 硬件 | CameraManager Torch API · AudioTrack 侧音 |
11 个模块的分法很直接:core:model 是纯数据,core:morse 是编解码与时序编译(这两个是纯 Kotlin JVM 模块,零 Android 依赖,可以直接跑单测),core:torch 管发光与调度,core:data 管持久化,core:designsystem 管主题与复用组件;feature:* 五块各自持有 ViewModel 与页面;app 只做装配。
源码与构建
仓库:github.com/Fq77/FlashMorse,MIT 许可,无须授权即可阅读、修改、分发。
git clone https://github.com/Fq77/FlashMorse.git
cd FlashMorse
./gradlew installDebug # 或直接用 Android Studio Run
环境要求:JDK 21、支持 AGP 9.x 的 Android Studio、Android SDK 24–37。项目不提供预编译 APK 下载,需要自行构建——这也是「零权限、无网络」能被独立验证的前提:包是你自己编译出来的。
想只跑逻辑层的验证,不需要模拟器:
./gradlew :core:morse:test :core:model:test # 26 个 JVM 单测
core:model 与 core:morse 是纯 Kotlin/JVM 模块,时序编译与码表比对全部在 JVM 上完成。
现状
核心链路已经跑通并在真机(1200×2670 / 520dpi)上验证:发报、SOS 循环、短语库、对照表试闪、手动发报、手电筒无级调光、全屏闪光、键盘态滚动。core:morse + core:model 共 26 个单元测试覆盖 ITU 码表逐字比对、SOS=27t、点划比例 3.0、事件时间轴单调无漂移。
当前版本 v1.0.0,代码已全部开源(MIT)。仍在推进的方向:前台服务、亮度分级校准、prosign 对照表页、短语 JSON 导入导出。技术上值得写下来的部分——纳秒级时序、屏幕通道的帧预算、Miuix 导航的几处硬坑——都整理在同系列的另一篇《把点划钉在纳秒上》里。
光比声音安静,比震动远,比网络可靠。当你什么都用不上的时候,至少还能眨个眼。