@@ -409,12 +409,97 @@ def last_trigger_speed(self) -> float:
409409 return self ._last_trigger_speed
410410
411411
412+ class SoundTrigger (TriggerStrategy ):
413+ """
414+ Hardware sound trigger using SparkFun SEN-14262.
415+
416+ Wiring: SEN-14262 GATE → OPS243-A J3 Pin 3 (HOST_INT)
417+ The GATE output goes HIGH on loud sound (club impact).
418+ OPS243-A uses rising edge detection on HOST_INT as trigger.
419+
420+ No software trigger (S!) needed — the radar triggers itself
421+ via hardware. We just need to wait for data to appear on serial.
422+ """
423+
424+ def __init__ (
425+ self ,
426+ pre_trigger_segments : int = 12 ,
427+ ):
428+ """
429+ Initialize sound trigger.
430+
431+ Args:
432+ pre_trigger_segments: Number of pre-trigger segments for S# command.
433+ Each segment = 128 samples = ~4.27ms at 30ksps.
434+ Default 12 gives ~51ms pre-trigger, ~85ms post-trigger.
435+ Tune based on mic-to-impact distance.
436+ """
437+ self .pre_trigger_segments = pre_trigger_segments
438+ self ._split_configured = False
439+
440+ def wait_for_trigger (
441+ self ,
442+ radar : "OPS243Radar" ,
443+ processor : RollingBufferProcessor ,
444+ timeout : float = 30.0 ,
445+ ) -> Optional [IQCapture ]:
446+ """
447+ Wait for hardware sound trigger and capture buffer.
448+
449+ Unlike other triggers, no S! command is sent. The radar's
450+ HOST_INT pin receives the trigger from the SEN-14262 GATE
451+ output, causing the radar to dump its rolling buffer automatically.
452+ We just block on serial read waiting for the I/Q data to arrive.
453+ """
454+ # Set pre-trigger split once (persists across captures)
455+ if not self ._split_configured :
456+ radar .set_trigger_split (self .pre_trigger_segments )
457+ self ._split_configured = True
458+
459+ logger .info (
460+ "Waiting for hardware sound trigger (timeout=%ss, S#%s)..." ,
461+ timeout , self .pre_trigger_segments
462+ )
463+
464+ response = radar .wait_for_hardware_trigger (timeout = timeout )
465+
466+ if not response :
467+ logger .info ("Sound trigger timeout - no hardware trigger received" )
468+ return None
469+
470+ logger .info ("Hardware trigger fired, received %d bytes" , len (response ))
471+
472+ # Re-arm for next capture
473+ radar .rearm_rolling_buffer ()
474+
475+ capture = processor .parse_capture (response )
476+
477+ if capture :
478+ timeline = processor .process_standard (capture )
479+ outbound = [
480+ r for r in timeline .readings
481+ if r .is_outbound and r .speed_mph >= 15.0
482+ ]
483+ if outbound :
484+ peak = max (r .speed_mph for r in outbound )
485+ logger .info ("Sound trigger capture: %d readings, peak %.1f mph" ,
486+ len (outbound ), peak )
487+ else :
488+ logger .info ("Sound trigger capture: no significant outbound speed" )
489+
490+ return capture
491+
492+ def reset (self ):
493+ """Reset trigger state."""
494+ self ._split_configured = False
495+
496+
412497def create_trigger (trigger_type : str = "speed" , ** kwargs ) -> TriggerStrategy :
413498 """
414499 Factory function to create trigger strategy.
415500
416501 Args:
417- trigger_type: "speed" (recommended), "polling", "threshold", or "manual "
502+ trigger_type: "speed" (recommended), "polling", "threshold", "manual", or "sound "
418503 **kwargs: Arguments passed to trigger constructor
419504
420505 Returns:
@@ -426,12 +511,14 @@ def create_trigger(trigger_type: str = "speed", **kwargs) -> TriggerStrategy:
426511 - "polling": Continuously capture and check for activity. Simple but slow.
427512 - "threshold": Speed threshold triggers capture. Less efficient than "speed".
428513 - "manual": External trigger for testing.
514+ - "sound": Hardware sound trigger via SparkFun SEN-14262. <1ms response.
429515 """
430516 triggers = {
431517 "speed" : SpeedTriggeredCapture ,
432518 "polling" : PollingTrigger ,
433519 "threshold" : ThresholdTrigger ,
434520 "manual" : ManualTrigger ,
521+ "sound" : SoundTrigger ,
435522 }
436523
437524 if trigger_type not in triggers :
0 commit comments