feat(pose): pinch engage debounce

This commit is contained in:
L'électron rare
2026-07-02 09:14:18 +02:00
parent 021c53f078
commit ea42ba4bb3
2 changed files with 49 additions and 6 deletions
+16 -6
View File
@@ -133,11 +133,12 @@ class PinchEvent:
class _PinchState:
__slots__ = ("engaged", "last_t")
__slots__ = ("engaged", "last_t", "qual")
def __init__(self) -> None:
self.engaged: bool = False
self.last_t: float = -1e9
self.qual: int = 0 # consecutive qualifying frames (debounce)
class PinchDetector:
@@ -217,13 +218,22 @@ class PinchDetector:
st.engaged = False
events.append(PinchEvent(hand=slot, finger=i + 1, state=0))
elif i == winner and (t_now - st.last_t) >= self.refractory_s:
st.engaged = True
st.last_t = t_now
events.append(PinchEvent(hand=slot, finger=i + 1, state=1))
# engage only after debounce_frames consecutive qualifying
# frames; release below stays edge-immediate.
st.qual += 1
if st.qual >= self.debounce_frames:
st.engaged = True
st.last_t = t_now
st.qual = 0
events.append(PinchEvent(hand=slot, finger=i + 1, state=1))
else:
st.qual = 0
for slot in range(len(self._state)):
if slot not in present:
for i in range(4):
if self._state[slot][i].engaged:
self._state[slot][i].engaged = False
st = self._state[slot][i]
st.qual = 0
if st.engaged:
st.engaged = False
events.append(PinchEvent(hand=slot, finger=i + 1, state=0))
return events
+33
View File
@@ -221,3 +221,36 @@ def test_extension_gate_defeat_reproduces_old_behavior():
ext_min=0)
ev = det.step([_relaxed_hand()], 0.1)
assert len(ev) == 1 and ev[0].state == 1
def test_debounce_delays_engage_to_nth_frame():
det = PinchDetector(ratio_on=0.45, ratio_off=0.65, refractory_ms=0,
debounce_frames=3)
det.step([_pinch_hand(*_OPEN)], 0.00)
a = det.step([_pinch_hand(*_PINCH)], 0.10) # qualifying 1
b = det.step([_pinch_hand(*_PINCH)], 0.13) # qualifying 2
c = det.step([_pinch_hand(*_PINCH)], 0.16) # qualifying 3 -> engage
assert a == [] and b == []
assert len(c) == 1 and c[0].state == 1 and c[0].finger == 1
def test_debounce_jitter_resets_counter():
det = PinchDetector(ratio_on=0.45, ratio_off=0.65, refractory_ms=0,
debounce_frames=3)
det.step([_pinch_hand(*_PINCH)], 0.00) # qualifying 1
det.step([_pinch_hand(*_PINCH)], 0.03) # qualifying 2
det.step([_pinch_hand(*_OPEN)], 0.06) # jitter -> counter resets
a = det.step([_pinch_hand(*_PINCH)], 0.09) # qualifying 1 again
b = det.step([_pinch_hand(*_PINCH)], 0.12) # qualifying 2
assert a == [] and b == []
def test_release_immediate_after_debounced_engage():
det = PinchDetector(ratio_on=0.45, ratio_off=0.65, refractory_ms=0,
debounce_frames=3)
det.step([_pinch_hand(*_PINCH)], 0.00)
det.step([_pinch_hand(*_PINCH)], 0.03)
eng = det.step([_pinch_hand(*_PINCH)], 0.06) # 3rd frame -> engage
rel = det.step([_pinch_hand(*_OPEN)], 0.09) # very next frame
assert len(eng) == 1 and eng[0].state == 1
assert len(rel) == 1 and rel[0].state == 0