Use when modifying SmartDash BLE telemetry ingestion, ride energy accumulation, SoC estimation, range estimation, ride CSV export, replay tooling, or any code that mixes controller/BMS/GPS streams. Enforces the rule that physical integration must be driven only by fresh controller telemetry samples instead of generic UI combine flows.
本 skill 用于所有涉及以下主题的修改:
MainViewModel.kt 中的实时指标聚合RideAccumulator.kt 中的 Wh / Ah / distance / peak 物理积分BatteryStateEstimator.kt 中的 SoC / 内阻 估算RangeEstimator.kt 中的 range / 窗口效率 估算TelemetryStreamProcessor.kt 中的去重、质量分类、有限值守卫BLE 遥测样本解析、去重、时间戳与积分CSV 导出、平均能耗与估计续航SmartDash 的控制器数据来自 BLE notify / poll,它不是严格等间隔、严格同步、严格无抖动的理想采样源。
因此:
GPS / BMS / 设置项 / UI 状态 的变更,不能重复触发 Wh / Ah / distance / peak 等累计量积分。SoC / range / Wh/km 算法都必须显式区分:
rawderivedestimationpresentationProtocolParser._metrics 使用 MutableSharedFlow<VehicleMetrics>(extraBufferCapacity = 64)。
绝对不能改回 MutableStateFlow,因为 StateFlow 的 equals 去重会在连续两帧相同数值时静默丢弃,导致 TelemetryStreamProcessor 漏帧、dtMs 断裂、allowIntegration 永远为 false、里程/能耗/平均能耗全部为 0。
UI 展示层的 combine 使用 ProtocolParser.latestMetrics(StateFlow 快照),遥测积分链路使用 ProtocolParser.metrics(SharedFlow 每帧必达)。
combine(...) 的 UI 聚合流里做物理累计禁止在这类函数里直接累计:
_rideEnergyWh += ..._rideRecoveredEnergyWh += ..._ridePeakRegenKw = ..._maxSpeed = ..._speedSamples++这些动作必须迁移到「新控制器样本到达」的专用处理链:
ProtocolParser.metrics.onEach {
val sample = telemetryStreamProcessor.process(it)
if (_isRideActive.value && !isRidePausedForStop()) {
rideAccumulator.accumulate(sample)
}
}.launchIn(viewModelScope)
TelemetrySample任何新算法都优先围绕统一样本对象实现:
data class TelemetrySample(
val timestampMs: Long,
val sourceFrameId: Long?,
val voltageV: Float,
val busCurrentA: Float,
val phaseCurrentA: Float,
val rpm: Float,
val controllerSpeedKmH: Float,
val motorTempC: Float,
val controllerTempC: Float,
val braking: Boolean,
val cruise: Boolean,
val quality: SampleQuality,
val dtMs: Long,
val allowIntegration: Boolean,
val allowLearning: Boolean
)
所有输入先 isFinite() 检查。任一非有限值 → 直接返回 OUTLIER 样本(payload 归零,allowIntegration = false),不更新状态跟踪变量。
dt < 20ms 且电压/电流/RPM 变化极小 → DUPLICATE
dt < 15ms → TOO_DENSE
dt > 5000ms → GAP_RESET(BLE 断流后的第一帧,不积分)
allowIntegration = GOOD && dtMs >= 20msallowLearning = GOOD && dtMs in 50..2000msdtMs 必须钳位到安全范围:
val safeDtMs = sample.dtMs.coerceIn(50L, 500L)
防止 BLE 断流造成 2-3s 间隔导致过度积分,也防止突发回调 20ms 间隔导致欠积分。
距离、能量、移动时间都使用 safeDtMs 计算。
为了防止逻辑复杂化后出现的口径分裂风险,SmartDash 强制执行以下能量口径标准:
Net Wh (Traction - Regen) 口径。Net Wh 能效基准。avgNetEfficiencyWhKm 与 avgTractionEfficiencyWhKm。SoC 必须区分内部估算值与显示值OCV 校准显著介入range 不要按固定 1 km 台阶刷新优先使用:
EMA 平滑所有涉及 VehicleProfile 浮点字段的学习函数,必须在入口和出口都加 isFinite() 守卫:
blendLearnedEfficiencyWhKm():输入 NaN → 返回上一次有效值calculateLearnedUsableEnergyRatio():SoC drop NaN → 返回 profile 默认值blendLearnedUsableEnergyRatio():输出 NaN → 返回上一次值toJson():所有 Float 字段 finiteOr(default) 后再写 JSONfromJson():读入后 NaN 清洗 + 二次 coerce 归一化normalizedProfile():先 finiteOr() 再 coerceAtLeast/coerceInsaveVehicleProfiles():外层 runCatching 防止 crash 传播ZhikeProtocol 检测到连续全 0 实时帧(电压 < 2V, 电流 < 0.1A, RPM < 1, 速度 < 0.1)时:
emit 给上层lastRealtimeMetrics这防止无效遥测流导致 SoC 像掷骰子、续航乱跳、里程/能耗长期为 0。
BLE Controller Frame (Zhike/etc)
│
▼
ProtocolParser.parse(data)
│
├── emit() ──→ _metrics.tryEmit(VehicleMetrics) [SharedFlow, 每帧必达]
│ _latestMetrics.value = metrics [StateFlow, UI 快照]
│
▼
MainViewModel: ProtocolParser.metrics.onEach { rawMetrics }
│
▼
TelemetryStreamProcessor.process(rawMetrics)
│ → isFinite() 守卫 → OUTLIER if non-finite
│ → Duplicate / TooDense / GapReset / Outlier 分类
│ → allowIntegration = GOOD && dtMs >= 20ms
│ → allowLearning = GOOD && dtMs in 50..2000ms
▼
TelemetrySample (with quality flags)
│
├── allowIntegration ──→ RideAccumulator.accumulate(sample)
│ → safeDtMs = dtMs.coerceIn(50, 500)
│ → tractionWh / regenWh / netAh / distance
│
├── BatteryStateEstimator.estimate(sample)
│ → EMA smoothing (voltage, current)
│ → Internal resistance learning (dV/dI)
│ → SoC = Ah integration + OCV fusion
│
└── RangeEstimator.estimate(batteryState, accumulatorState)
→ Sliding window efficiency (2.0km target)
→ remainingEnergy = SoC% * capacityAh * series * 3.7V * usableRatio
→ range = remainingWh / avgEfficiencyWhKm
修改遥测相关代码后,必须确认:
ProtocolParser._metrics 仍是 SharedFlowTelemetryStreamProcessor 有 isFinite() 守卫RideAccumulator 使用 safeDtMs = coerceIn(50, 500)VehicleProfile.toJson() 使用 finiteOr()saveVehicleProfiles() 外层有 runCatching./gradlew :app:compileDebugKotlin --console plain