高度提醒最初是这么写的constband:numberMath.floor(meters/100)*100if(band!lastBand){notify(${band}米)lastBandband}看起来没有破绽。实际戴上山的第一天就发现问题一段平缓的碎石坡上手表连着震了四次每次都播报2,900 米。原因在于气压高度计的读数是波动的。人站在 2,900 米附近不动气压计给出的高度会在 2,898 到 2,902 之间来回跳。上面这段代码把每一次跨越 100 的整数倍都当成新事件于是反复播报。山里越往上气压越低、风越大波动也越明显一天下来重复播报十几条很正常。这篇记录一下高度里程碑的判定是怎么改的。气压先转成高度再决定要不要采信海拔不直接来自传感器。手表测的是气压需要用气压高度公式换算// 国际标准大气模型海平面参考气压默认 1013.25 hPaconstSTANDARD_SEA_LEVEL_HPA:number1013.25exportinterfaceAltitudeSample{capturedAt:number// 毫秒时间戳pressureHpa:number}exportinterfaceAltitudeState{smoothedMeters:number// 平滑后的当前海拔maxMeters:number// 本次行程最高点ascentMeters:number// 累计爬升lastAnnouncedBand:number// 最近一次播报过的整百米referenceHpa:number// 起点校准气压}exportfunctionpressureToMeters(pressureHpa:number,referenceHpa:number):number{if(pressureHpa0||referenceHpa0){return0}// 44330 是标准大气下每降低 1 hPa 对应的高度系数return44330*(1-Math.pow(pressureHpa/referenceHpa,0.1903))}referenceHpa取的是出发时在地面校准的气压不能写死成 1013.25。同一个山谷里阴天和晴天的气压能差 10 hPa 以上换算出来就是八九十米的误差——还没开始爬海拔已经显示 2,000 米了。采样值进状态机之前先过一道物理合理性检查。人爬升的速度有上限越野跑下坡最猛也不过每秒几米constMAX_CLIMB_RATE_MPS:number6exportfunctionacceptSample(previous:AltitudeSample,current:AltitudeSample,state:AltitudeState):boolean{constseconds:numberMath.max(1,(current.capturedAt-previous.capturedAt)/1000)constbefore:numberpressureToMeters(previous.pressureHpa,state.referenceHpa)constafter:numberpressureToMeters(current.pressureHpa,state.referenceHpa)constrate:numberMath.abs(after-before)/secondsif(rateMAX_CLIMB_RATE_MPS){returnfalse}// 时间戳倒退说明采样乱序同样丢弃returncurrent.capturedAtprevious.capturedAt}这条规则能挡掉大部分由阵风、剧烈摆臂和取下手表造成的假跳变。代价是它也挡掉了真实的极速升降比如缆车和滑雪下降。所以这个上限只作用在徒步模式滑雪模式下会放宽到 25。平滑只减小幅度压不住贴边抖动被采信的样本还要做一次指数平滑constSMOOTH_ALPHA:number0.28exportfunctionsmoothAltitude(previous:number,raw:number):number{if(previous0){returnraw}returnprevious(raw-previous)*SMOOTH_ALPHA}alpha取值 0.28 是权衡的结果。调大到 0.5界面跟手但血压高度计的噪声会直接显在数字上海拔一直在跳调小到 0.1数字稳了可快速爬升时显示器会明显滞后人都到坡顶了还显示在半坡。光靠平滑压不住这个问题。真正管用的是下面这一步。迟滞让判定记住自己来过平滑只能减小波动幅度压不住贴着边界抖动。2,900 米附近的平滑值依然会在 2,899.6 和 2,900.4 之间来回了很久。思路是给判定加一个回退门槛只有高度实打实地跌回上一档以下一段距离才认为这一档可以重新播报。constMILESTONE_STEP:number100constHYSTERESIS_METERS:number12exportinterfaceMilestoneResult{announce:booleanband:number}exportfunctionresolveMilestone(meters:number,lastBand:number):MilestoneResult{constband:numberMath.floor(meters/MILESTONE_STEP)*MILESTONE_STEP// 爬到新的一档播报if(bandlastBand){return{announce:true,band}}// 回落超过迟滞距离才允许这一档重新计if(meterslastBand-HYSTERESIS_METERS){return{announce:false,band:band}}return{announce:false,band:lastBand}}迟滞距离定 12 米来自几次实测。海拔计的短期漂移大多在 5 米以内12 米留了一倍余量再往上调用户在山脊上上下下走十几米时就会漏掉播报。有一个细节容易被忽略回落到 2,888 米以下时返回的是当前档位而不是lastBand。这样重新爬回 2,900 米时会是一次干净的播报而不是被自己刚才的回退逻辑吃掉。累计爬升不能把所有正增量都加上同一个噪声问题换个地方又出现了一次。界面上的累计爬升是把每个正增量累加得到的exportfunctionaccumulateAscent(state:AltitudeState,meters:number):AltitudeState{constdelta:numbermeters-state.smoothedMeters// 小于 1.5 米的波动不计入否则站原地不动也会爬升几十米constcounted:numberdelta1.5?delta:0return{...state,smoothedMeters:meters,maxMeters:delta0?Math.max(state.maxMeters,meters):state.maxMeters,ascentMeters:state.ascentMeterscounted,lastAnnouncedBand:state.lastAnnouncedBand,referenceHpa:state.referenceHpa}}1.5 米以下的正增量被丢掉。第一版没做这个限制从停车场走到山脚下这段平路累计爬升就虚报了一百多米。maxMeters只在新值更大时更新而且用的是平滑后的值。用原始值的话一条噪声尖峰能把最高海拔永久拉高行程结束后这个数字是错的还没法修。界面只消费判定结果界面侧不参与任何计算只把状态铺到剖面上interfaceRouteNode{name:stringstatus:stringdetail:stringreached:boolean}exportfunctionbuildRouteNodes(state:AltitudeState,nodes:RouteNode[]):RouteNode[]{returnnodes.map((node:RouteNode,index:number){// 节点海拔由路线数据给出这里只做展示用的状态归类return{...node,reached:index0?state.maxMeters2400:node.reached}})}三张卡片上的数值全部来自AltitudeState视图不做第二次平滑。同一份数据被两个地方各自平滑一次是这类界面出现卡片数字和曲线对不上的常见原因。目录结构SummitWatch/ ├─ entry/src/main/ets/ │ ├─ pages/Index.ets # 高程剖面与路线卡片 │ ├─ features/ │ │ ├─ AltitudeModel.ets # 样本、状态与里程碑结构 │ │ ├─ BarometricFilter.ets # 合理性检查与指数平滑 │ │ └─ MilestoneResolver.ets # 迟滞判定与累计爬升 │ ├─ entryability/EntryAbility.ets │ └─ adapters/ │ └─ PressureSensorAdapter.ets # 气压计订阅与校准 ├─ entry/src/main/resources/ └─ build-profile.json5BarometricFilter和MilestoneResolver分开是因为两者的验证方式不同滤波可以用录制的气压序列离线跑回归里程碑判定更适合在真机上走一遍真实路线。本文讨论的是本地数据处理思路不能作为登山安全依据。真实行程中高度仅作参考路线判断、天气评估和撤退决策要依据专业设备与当地气象信息不能依赖手表读数。