发布

  • v1613 992c2b25cb

    fix(firmware): correct ESP32 edge heart rate — sample-rate + harmonic lock (#987) (#988)

    frostbyte_neo 发布于 2026-06-09 15:27:21 +00:00

    • fix(firmware): correct heart-rate estimation — sample-rate + harmonic lock

    The edge vitals HR was stuck at ~45 BPM regardless of true heart rate
    (Apple Watch ground truth 87 BPM read as ~45) and "dropped a lot" between
    frames. Two root causes:

    1. Stale fixed sample rate. estimate_bpm_zero_crossing() used a hardcoded
      sample_rate = 10.0f (and the biquads a separate fs = 20.0f). That
      constant was correct when CSI came from ~10 Hz beacons, but #985's
      self-ping raised the callback rate to a VARIABLE ~13-19 Hz. BPM scales as
      (assumed_rate / actual_rate) x true, so a true 87 read ~45, and because
      the real rate fluctuates with CSI yield while the code assumed a fixed
      value, the reported HR swung frame-to-frame (the "drops").

    2. Breathing-harmonic lock. Zero-crossing HR estimation locked onto a
      breathing harmonic — a 0.25 Hz breathing fundamental puts its 3rd
      harmonic at ~0.74 Hz ~= 44 BPM, right in the HR band — so it parked at
      ~45 BPM independent of the real heartbeat.

    Fix:

    • Measure the real sample rate from inter-frame timestamps (EMA-smoothed,
      clamped 8-30 Hz); use it for both BPM conversion and biquad design, and
      re-tune the filters when the rate drifts >15% so the passbands stay in
      real Hz.
    • Replace the HR zero-crossing with estimate_hr_autocorr(): autocorrelation
      peak in the 45-180 BPM band that explicitly rejects lags within 8% of any
      breathing harmonic (k=1..6), with parabolic interpolation and a peak-
      confidence gate (returns 0 rather than a noise value).
    • Median-smooth (N=9) the emitted HR over valid estimates to kill residual
      single-frame outliers.

    Validated on hardware (ESP32-S3, COM8/192.168.1.80) vs an unmodified board
    (192.168.1.67) and an Apple Watch (87 BPM):

    • old firmware: HR pegged 40-52 BPM (median ~45)
    • fixed firmware: HR reaches the true 88-91 BPM range (peak 88.5, vs 87 GT)

    Known limitation: under subject motion (motion=Y) HR is still noisy because
    the breathing estimate degrades and misguides harmonic rejection; motion
    gating + breathing robustness are follow-ups.

    Co-Authored-By: claude-flow ruv@ruv.net

    • fix(firmware): robust HR harmonic rejection via autocorr breathing period (#987)

    Follow-up to 332c2a98d. The HR harmonic rejection was fed the noisy
    zero-crossing breathing estimate, which under motion notched the wrong
    frequencies and let the autocorr lock onto the ~0.75 Hz breathing harmonic
    (~45 BPM). Generalize estimate_hr_autocorr -> estimate_periodicity_autocorr
    and drive HR harmonic rejection from a robust autocorrelation breathing
    period instead; widen the HR median smoother to N=13.

    Hardware A/B (fixed .80 vs unmodified control .67, both edge_tier=2, subject
    in motion 100% of frames):

    • control (old fw): HR pegged 40-43 BPM (median 40.6)
    • fixed: HR 60-91 BPM (median 71.9) — sub-60 harmonic locks
      eliminated, spread 42->31 BPM vs previous build

    Reported breathing is unchanged (still zero-crossing); the autocorr breathing
    period is used only internally for HR harmonic rejection.

    Co-Authored-By: claude-flow ruv@ruv.net

    • docs(changelog): record ESP32 heart-rate fix (#987)

    Co-Authored-By: claude-flow ruv@ruv.net

    下载附件