闪电过后数几秒:用气温修正声速,做成可核对的鸿蒙听雷测距应用

看见闪电后默数,听到雷再除以三,得到的是「大约几公里」。这个算法默认声速 340 米每秒,三秒一公里。干空气里声速随气温变化:0°C 约 331.3 米每秒,每升高 1°C 约增加 0.606 米每秒。同一段 5 秒,0°C 是 1656.5 米,40°C 是 1777.7 米,比「一公里七」分别近 43.5 米、远 77.7 米。

本文把这道估算做成鸿蒙应用 听雷测距台 thunder-lab:用气温和光声间隔两根滑杆,按同一条线性式算出直线距离、声速和光延迟,并给出按间隔分档的躲避建议。目标读者是两类人:想用手算核对「三秒一公里」差在哪里的人,以及要用华为云码道交付一个可测试鸿蒙小工具的人。

环境与验证(2026-09)

系统HarmonyOS,compatibleSdkVersion 5.0.0(12)
工程DevEco Studio;模拟器 127.0.0.1:5555;Pura X View 7.0.0(26.0.0)
实现ArkTS + ArkUI;包名 com.thunderlab.app
开发华为云码道 CodeArts,模型 GLM-5.2(建仓用 GLM-5.2-ArkTS-SPARK)
测试DevEco 自带 Node,node:test,36 项通过、0 项失败
权限 / 网络requestPermissions: [],不联网

仓库(MIT):https://atomgit.com/2601_95131637/thunder-lab

民间算法差在哪

取整声速 v = 340 m/s 时,距离就是 340 × t。三秒对应 1020 米,口语里收成一公里。干空气在常见气温下可以用直线近似:

v(T) = 331.3 + 0.606 × T

T 是摄氏温度。15°C 时 v = 340.39,和 340 只差 0.39,所以民间算法在室温附近很好用。气温离开 15°C,差值被秒数放大。下表固定看几个常用点,容差放到后文测试的 1e-6。

气温声速 (m/s)间隔 3 秒间隔 5 秒与 340×t 的差(5 秒)
0°C331.3993.9 m1656.5 m近 43.5 m
15°C340.391021.2 m1701.95 m远 1.95 m
20°C343.421030.3 m1717.1 m远 17.1 m
40°C355.541066.6 m1777.7 m远 77.7 m

0°C、3 秒是 993.9 米,不是 1000 米,也不是 1020 米。15°C、5 秒是 1701.95 米,屏上保留一位小数,写成 1701.9 米。应用要展示的就是这张表,而不是再做一次「除以三」。

这条式子是干空气、无风、直线传播下的线性近似,气温取 -20°C 到 40°C。它不处理湿度、风把声线吹偏、以及建筑带来的回声。这些边界放在文末,页脚也写给使用者。

两个滑杆对应两个观测量

人在现场真正拿到的是两个数:自己感到的冷热,以及闪电和雷声之间数出来的秒数。应用只保留这两个状态,其余都现算。

  1. 气温:-20°C 到 40°C,默认 15°C,步进 1°C。超出区间夹回;不是有限数时回到 15°C。
  2. 光声间隔:0 到 90 秒,默认 5 秒,步进 1 秒。0 秒表示闪电和雷声同时到达,距离是 0,不能当成「没填」再填回 5 秒。
  3. 雷暴示意:Canvas 按间隔画声浪。间隔为 0 时去掉声浪,改为「闪电就在附近」。
  4. 结果:直线距离(米、千米)、声速、光延迟(微秒)、四档建议。
  5. 页脚:写明干空气直线估算,并提醒进入坚固建筑。

默认一屏可以手算复原:

v(15) = 331.3 + 0.606 × 15 = 340.39 m/s
s     = 340.39 × 5 = 1701.95 m ≈ 1701.9 m
光延迟 = 1701.95 / 299792458 × 1000000 ≈ 5.6771 μs

5 秒小于 30 秒,建议是「间隔不足30秒,雷击很近,尽快躲避」。光延迟放在结果里,是为了把「先看见、后听见」量化:同一段路上,光只要几微秒,人默数的那几秒几乎全是声音在走。

模拟器:15°C、间隔 5 秒,距离 1701.9 米,光延迟 5.6771 微秒

间隔拖到 0,距离为 0.0 米,示意区不再画声浪:

间隔 0 秒:距离 0.0 米,示意区只留「闪电就在附近」

45 秒已经过了 30 秒、还没到 60 秒。340.39 × 45 = 15317.55,屏上 15317.5 米 / 15.3 千米,建议停在「听得见雷声,仍在雷暴范围内」:

15°C、45 秒:15317.5 米,建议仍停在雷暴范围内

61 秒进入最后一档。340.39 × 61 = 20763.79,屏上 20763.8 米,光延迟 69.2605 微秒:

15°C、61 秒:20763.8 米,建议为远雷

这四张由 uitest 在本机 HarmonyOS 模拟器上点出。同一组 15°C、61 秒又在 DevEco 的 Pura X View 上看过,工程类型 harmonyApp,设备版本 7.0.0(26.0.0):

DevEco:Pura X View 上的听雷测距台,15°C、间隔 61 秒,距离 20763.8 米

近景:20763.8 米,20.76 千米,声速 340.39 米每秒

下滑:光延迟 69.2605 微秒,建议为远雷

三组非零间隔换的只是秒数,声速公式没有换。20.76 千米是 20763.8 / 1000 保留两位的显示,不是另一套计算。

四个纯函数

业务写在 entry/src/main/ets/domain/thunder.ets:没有界面,没有类,四个函数加一组常量。界面禁止再写一遍系数,避免滑杆和测试各算各的。

声速直接实现上面的线性式。气温不是有限数时,不把 NaN 乘进结果,退回 15°C 对应的 340.39:

export const SOUND_T0: number = 331.3;
export const SOUND_K: number = 0.606;

export function speedOfSound(celsius: number): number {
  if (!Number.isFinite(celsius)) {
    return SOUND_T0 + SOUND_K * 15;
  }
  return SOUND_T0 + SOUND_K * celsius;
}

距离是声速乘秒数。秒数小于等于 0 时返回 0,因为「还没数到」或「同时到达」都不是一段正的传播时间。结果限制在 0 到 40000 米:40°C、90 秒约 32.0 千米,仍在上限内;上限用来接住滑杆以外的调用。气温或秒数不是有限数时,按默认的 15°C、5 秒计算,和界面默认值相同。

export function strikeDistance(celsius: number, seconds: number): number {
  if (!Number.isFinite(celsius) || !Number.isFinite(seconds)) {
    const fallbackSpeed: number = SOUND_T0 + SOUND_K * 15;
    const fallbackDist: number = fallbackSpeed * 5;
    return Math.min(Math.max(fallbackDist, 0), DISTANCE_MAX);
  }
  if (seconds <= 0) {
    return 0;
  }
  const speed: number = speedOfSound(celsius);
  const dist: number = speed * seconds;
  return Math.min(Math.max(dist, 0), DISTANCE_MAX);
}

光延迟用真空光速 c = 299792458 m/s,单位换成微秒。距离不是正数时返回 0。它故意很小,用来对照「光可以当成立刻到达」:

export const LIGHT_MPS: number = 299792458;

export function lightDelayMicros(meters: number): number {
  if (!Number.isFinite(meters) || meters <= 0) {
    return 0;
  }
  return (meters / LIGHT_MPS) * 1e6;
}

1701.9 米量级对应约 5.6771 微秒,20763.8 米对应 69.2605 微秒。人感知到的先后,差的是声音走完这段路的时间。

躲避建议按数出来的秒数分档,不按算出的米数分档。原因是:秒数是观测值,米数还依赖气温滑杆;雷暴教学里常见的分界也是 30 秒和 60 秒,而不是某一个公里数。阈值写死为:

条件文案15°C 时大约距离
0≤ 0 秒闪电就在附近,立即进入坚固建筑0 m
1< 30 秒间隔不足30秒,雷击很近,尽快躲避不足 10.2 km
2< 60 秒听得见雷声,仍在雷暴范围内不足 20.4 km
3≥ 60 秒远雷,仍不是解除警报约 20.4 km 起

30 和 60 本身进入更高一档:adviceLevel(30) 为 2,adviceLevel(60) 为 3。函数只返回 0 到 3,文案留在 string.json

export function adviceLevel(seconds: number): number {
  if (!Number.isFinite(seconds)) {
    return 0;
  }
  if (seconds <= ADVICE_THRESHOLD_NEAR) {
    return 0;
  }
  if (seconds < ADVICE_THRESHOLD_30) {
    return 1;
  }
  if (seconds < ADVICE_THRESHOLD_60) {
    return 2;
  }
  return 3;
}

60 秒、15°C 约 20.4 千米,屏上标成远雷,页脚的躲避提醒仍然保留。距离用来感受远近,不代替「听到雷声就进入坚固建筑」。

提示词里的尺子就是测试断言

tests/thunder.test.ts 不另写一套「大约等于」。容差 1e-6,和手算用同一组系数。

声速:

  • T = 0 → 331.3
  • T = 15 → 340.39
  • T = 20 → 343.42
  • T = -10 → 325.24

距离:

  • T = 0,t = 3 → 993.9 米
  • T = 20,t = 5 → 1717.1 米
  • T = 15,t = 30 → 10211.7 米
  • 任意气温,t ≤ 0 → 0
  • 40°C、90 秒仍落在 [0, 40000]

光延迟不把微秒写成死数,断言比较的是 1000 / 299792458 * 1e6,光速常量锁在 299792458。这样改了光速,测试会失败,而不是和某个抄下来的小数一起错。

档位在分界两侧成对断言:adviceLevel(0) = 029.9 → 130 → 259.9 → 260 → 3,负数回到 0 档。另有 1000 次随机输入,只检查结果有限、距离在 0 到 40000 米之间。

对应断言例如:

test('T=0, t=3 → 993.9 m', () => {
  assert.ok(Math.abs(strikeDistance(0, 3) - 993.9) < 1e-6);
});

test('T=15 → 340.39', () => {
  assert.ok(Math.abs(speedOfSound(15) - 340.39) < 1e-6);
});

test('adviceLevel(30) = 2', () => {
  assert.equal(adviceLevel(30), 2);
});

331.3 × 3 = 993.9,331.3 + 0.606 × 15 = 340.39。30 秒整进入 2 档,29.9 秒仍是 1 档。

领域层是 ArkTS,但没有装饰器,也没有 ArkUI。tests/transpile.mjsthunder.etssanitize.ets 拷成 .ts,给相对 import 补上扩展名后再测。tests/.transpiled/ 进了 .gitignore,仓库里不会出现第二份公式。

node tests/transpile.mjs
node --test --experimental-strip-types tests/thunder.test.ts tests/sanitize.test.ts

零第三方依赖。本机复跑结果:

ℹ tests 36
ℹ pass 36
ℹ fail 0

0 秒必须还是 0

滑杆本身给出有限数。净化层仍单独放在 security/sanitize.ets:气温夹到 [-20, 40],间隔夹到 [0, 90];不是有限数时回到默认 15°C 和 5 秒。

0 是合法观测,不是空值。若把 0 当成「没填」再填成 5 秒,距离会从 0 跳到约 1.7 千米,和「闪电就在附近」相反。测试写明 sanitizeSeconds(0) === 0。上一节的 0 秒截图就是这条规则的界面结果。

export const CELSIUS_MIN: number = -20;
export const CELSIUS_MAX: number = 40;
export const CELSIUS_DEFAULT: number = 15;
export const SECONDS_MIN: number = 0;
export const SECONDS_MAX: number = 90;
export const SECONDS_DEFAULT: number = 5;

export function sanitizeCelsius(celsius: number): number {
  if (!Number.isFinite(celsius)) {
    return CELSIUS_DEFAULT;
  }
  return Math.min(Math.max(celsius, CELSIUS_MIN), CELSIUS_MAX);
}

export function sanitizeSeconds(seconds: number): number {
  if (!Number.isFinite(seconds)) {
    return SECONDS_DEFAULT;
  }
  return Math.min(Math.max(seconds, SECONDS_MIN), SECONDS_MAX);
}

滑杆回调写回页面前会再过一遍这两个函数,所以界面上的数字和领域层拿到的数字是同一对。

页面只存两个状态

Index.ets 只有 tempCelsiusintervalSeconds。声速、距离、光延迟、建议档不另存,每次绘制从这两个数现算。多存一份就会出现「数字已是 0,声浪还在」这类错位;现算则滑杆是唯一来源。

@Entry
@Component
struct Index {
  @State tempCelsius: number = 15;
  @State intervalSeconds: number = 5;

  private getSafeTemp(): number {
    return sanitizeCelsius(this.tempCelsius);
  }

  private getSafeInterval(): number {
    return sanitizeSeconds(this.intervalSeconds);
  }

  private getDistance(): number {
    return strikeDistance(this.getSafeTemp(), this.getSafeInterval());
  }
}

页面是一个 Scroll:标题、雷暴示意、两根滑杆、结果卡、页脚。底色 #1A0E2E,标题 #FFD700。滑杆范围与净化层相同,步进都是 1。

结果卡把米换成千米(除以 1000),再调用 speedOfSoundlightDelayMicros。距离保留一位小数,声速保留两位,光延迟保留四位,对应截图上的 1701.9 m、340.39 m/s、5.6771 μs。

建议文案不拼接资源名。ArkTS 的 $r 需要字面量资源名,四档各自写成 $r('app.string.advice_0')advice_3,字符串仍在 string.json

if (adviceLevel(this.intervalSeconds) === 0) {
  Text($r('app.string.advice_0'))
} else if (adviceLevel(this.intervalSeconds) === 1) {
  Text($r('app.string.advice_1'))
} else if (adviceLevel(this.intervalSeconds) === 2) {
  Text($r('app.string.advice_2'))
} else {
  Text($r('app.string.advice_3'))
}

雷暴示意高 280。distanceMetersintervalSeconds@Watch,滑杆移动就重绘。间隔大于 0 时画四圈声浪,颜色跟着建议档:近雷偏红,雷暴范围内用金,远雷用紫。间隔为 0 时不画弧。圆心下方满 1 千米用 km,否则用米。

@Prop @Watch('onInputsChanged') distanceMeters: number = 0;
@Prop @Watch('onInputsChanged') intervalSeconds: number = 0;

private onInputsChanged(): void {
  this.drawScene();
}

private drawSoundArcs(ctx: CanvasRenderingContext2D, cx: number, cy: number): void {
  const maxRadius: number = 120;
  const arcCount: number = 4;
  const level: number = adviceLevel(this.intervalSeconds);
  const arcColor: string = level <= 1 ? '#FF6B6B' : level === 2 ? '#FFD700' : '#7B68EE';

  for (let i: number = 0; i < arcCount; i++) {
    const radius: number = 30 + (i + 1) * (maxRadius / (arcCount + 1));
    ctx.strokeStyle = arcColor;
    ctx.globalAlpha = 0.8 - i * 0.15;
    ctx.beginPath();
    ctx.arc(cx, cy, radius, 0, Math.PI * 2);
    ctx.stroke();
  }
}

目录按「公式 / 净化 / 界面」拆开。领域层不引用 ArkUI,界面不自己写声速公式:

entry/src/main/ets/
  domain/thunder.ets          声速、距离、光延迟、建议档
  security/sanitize.ets       气温和间隔的取值范围
  pages/Index.ets             两个状态和整页排布
  ui/ConditionList.ets        两根滑杆
  ui/ResultCard.ets           距离、声速、光延迟、建议
  ui/StormPanel.ets           Canvas 闪电和声浪
tests/thunder.test.ts         手算尺子
tests/sanitize.test.ts        0 秒和夹取
tests/transpile.mjs           .ets 换成 .ts 再测

中文文案集中在 entry/src/main/resources/base/element/string.json。标题是「听雷测距台」。页脚写明声速取 331.3 + 0.606×气温,并提醒进入坚固建筑,远离树木、高处、水域和金属物。

码道一次交付可核对的实现

公式、分档和手算尺子先写进提示词,再交给华为云码道。模型和测试用的是同一组数,交付结果可以用本机命令复核。

码道上分两条对话。第一条只建公开空仓:仓库名 thunder-lab,初始化 README、MIT、.gitignore,不写测距代码。模型是 GLM-5.2-ArkTS-SPARK。

建仓提示词:只要空仓和三件套,不写测距代码

空仓建好:thunder-lab 已公开,并完成初始化推送

第二条对话绑定 2601_95131637/thunder-labmain,模型 GLM-5.2。提示词一次写清五块:只做测距、不联网、不申请权限;四个函数和常量;手算尺子;目录和 ArkTS 约定;测试命令,以及提交并 push 到 origin/main

开发提示词:常数、尺子、分层和提交要求一次写清

码道交出领域层、净化层、三个组件和页面。ab0f750 是应用本身,5e817ac 是 README 里的测试说明,汇报 36 项通过。

码道结果:36 项通过,提交 ab0f750 与 5e817ac

仓库克隆到本机后,用 DevEco 自带的 Node 复跑,仍是 36 通过、0 失败。assembleHap 通过后,HAP 装进模拟器 127.0.0.1:5555。上文运行图,包括 Pura X View 那三张,都来自这次安装;屏上的 1701.9 米、15317.5 米、20763.8 米和手算一致。

module.json5 中与权限相关的是空数组。气温和秒数都来自滑杆,不需要定位、网络、麦克风或存储,首次打开不会弹出授权:

"deviceTypes": ["phone", "tablet"],
"pages": "$profile:main_pages",
"requestPermissions": []

提示词里一次写清的约定:

  1. 先建空仓,再开绑定 main 的开发对话,在这个仓里写应用并推送。
  2. 331.30.606 同时出现在提示词和断言里。
  3. 间隔为 0 时距离为 0,对应「闪电就在附近」。
  4. 档位边界成对写:29.9 秒是 1 档,30 秒是 2 档。
  5. 四档建议各自引用资源字面量。
  6. 测试把要发布的 .ets 换扩展名再跑,不另放一份算法。

适用边界

这个应用计算的是干空气中的直线距离,气温限定在 -20°C 到 40°C,间隔限定在 0 到 90 秒,距离上限 40 千米。

以下情况它不修正:

  • :声线会被吹偏,数到的秒数不再等于直线距离除以声速。
  • 湿度:湿空气的声速和干空气线性式不同。
  • 回声:山体和建筑会带来更晚的一声,应数第一次听到的雷。
  • 民间口算:15°C 附近与「三秒一公里」很接近;更冷会估得偏远,更热会估得偏近。差值见第一节的表。

建议四档用的是固定的 0、30、60 秒,不是气象预警接口。页脚的躲避动作——进入坚固建筑,远离树木、高处、水域和金属物——不随「已经算出 20 千米」而取消。

总结

「除以三」默认声速约 340 米每秒,15°C 时误差不到 2 米(间隔 5 秒);换到 0°C 或 40°C,同一间隔会差出几十米。听雷测距台把修正收成两个滑杆和四个纯函数:331.3 + 0.606 × T,距离是声速乘秒数,光延迟用 299792458 米每秒换算成微秒,建议按 30 秒和 60 秒分档。

华为云码道按写好的尺子交出领域层、净化层和界面。36 项测试与发布代码同源,本机复跑 36/36。HAP 在 HarmonyOS 模拟器和 Pura X View 上跑通,屏上的 1701.9 米、15317.5 米、20763.8 米和手算一致。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐