Class: RoadToRubykaigi::JumpDetector

Inherits:
Object
  • Object
show all
Defined in:
lib/road_to_rubykaigi/jump_detector.rb

Overview

Detects squat-jumps from the accelerometer stream.

Axis-agnostic: consumes the gravity-compensated vertical acceleration instead of any single raw axis. vertical_acceleration = projection of the sample onto the gravity vector, minus |gravity|. So rest = 0, upward proper acceleration > 0, downward (or free fall) < 0.

Rule: loaded hold + bottom turnover. Running strides briefly peak the vertical acceleration above the loaded threshold at each footstrike. A squat-jump holds it above the threshold noticeably longer. Fire at the bottom of that span (slope becomes clearly negative), whether the signal is still above the threshold or has just crossed back below.

Firing at the bottom of the load (not at landing or at full takeoff) minimizes latency.

The loaded span must start from an up-crossing (a prior sample at or below the threshold) inside the buffer. Otherwise a stationary sensor reading near 0 could drift above threshold and accumulate an unbounded “loaded hold” that passes the duration gate on its own.

Constant Summary collapse

LOADED_THRESHOLD =

vertical_acceleration above this counts as “loaded” (body under extra g-load)

0.20
LOADED_MIN_SECONDS =

loaded span qualifying as squat hold

0.2
TAKEOFF_SLOPE_MAX =

g/s — fall slope indicating takeoff turnover

-1.0        # g/s — fall slope indicating takeoff turnover
SLOPE_WINDOW_SECONDS =
0.08
LOADED_END_GRACE_SECONDS =

allow fire shortly past the span’s last above-threshold point

0.15
COOLDOWN_SECONDS =

min gap between consecutive fires (covers takeoff→landing)

0.8
SAMPLE_BUFFER_SECONDS =

retain samples long enough to cover an elongated squat hold

1.2
MIN_SAMPLES_FOR_ANALYSIS =

slope + hold both need a few samples to be meaningful

5

Instance Method Summary collapse

Constructor Details

#initialize(gravity:) ⇒ JumpDetector

Returns a new instance of JumpDetector.



33
34
35
36
37
38
# File 'lib/road_to_rubykaigi/jump_detector.rb', line 33

def initialize(gravity:)
  @gravity = gravity
  @gravity_magnitude = Math.sqrt(gravity[0] ** 2 + gravity[1] ** 2 + gravity[2] ** 2)
  @last_samples = [] # [{time:, vertical_acceleration:}] sliding window
  @last_jump_time = nil
end

Instance Method Details

#detect(sample:) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/road_to_rubykaigi/jump_detector.rb', line 40

def detect(sample:)
  now = Time.now
  @last_samples << { time: now, vertical_acceleration: vertical_acceleration(sample) }
  cutoff = now - SAMPLE_BUFFER_SECONDS
  @last_samples.shift while !@last_samples.empty? && @last_samples.first[:time] < cutoff

  if !buffer_ready? || !squat_takeoff? || cooling_down?(now)
    false
  else
    @last_jump_time = now
    debug_log('JUMP_FIRED')
    true
  end
end