DIY Golf Launch Monitor using the OPS243-A Doppler Radar.
Warning
This project is in active development. Features may be incomplete, unstable, or change without notice. Expect bugs and breaking changes. Contributions and bug reports are welcome!
OpenFlight is an open-source golf launch monitor that measures ball speed using a commercial Doppler radar sensor. The OPS243-A from OmniPreSense provides professional-grade speed measurement (±0.5% accuracy) in a simple USB-connected package.
- Ball Speed: 30-220 mph range with ±0.5% accuracy
- Club Speed: Detected from pre-impact readings
- Smash Factor: Ball speed / club speed ratio
- Estimated Carry Distance: Based on ball speed (simplified model)
- Launch Angle (optional): With Raspberry Pi camera module
- Spin Rate (experimental): Via rolling buffer I/Q analysis (~50-60% detection rate)
| Component | Description | Cost |
|---|---|---|
| OPS243-A | OmniPreSense Doppler Radar | ~$225 |
| Raspberry Pi 5 | (or any computer with USB) | ~$80 |
| USB Cable | Micro USB to connect radar | ~$5 |
| Total | ~$310 |
Optional for spin detection:
| Component | Description | Cost |
|---|---|---|
| SparkFun SEN-14262 | Sound detector for rolling buffer trigger | ~$12 |
See docs/PARTS.md for the full parts list including optional camera module and sound trigger wiring.
# Clone the repository
git clone https://github.com/jewbetcha/openflight.git
cd openflight
# Run the setup script (installs Python + Node dependencies, builds UI)
./scripts/setup.sh# Run the launch monitor
openflight
# Specify serial port manually
openflight --port /dev/ttyACM0
# Show live readings
openflight --live
# Show radar info
openflight --info# Run the UI server with radar
openflight-server
# Run in mock mode (no radar needed, for development)
openflight-server --mock
# Disable camera (auto-enabled if available)
openflight-server --no-camera
# Use rolling buffer mode for spin detection
openflight-server --mode rolling-buffer
# Rolling buffer with sound trigger (requires SparkFun SEN-14262)
openflight-server --mode rolling-buffer --trigger sound-passthroughThen open http://localhost:8080 in a browser.
For kiosk mode on Raspberry Pi (fullscreen):
./scripts/start-kiosk.shSee docs/raspberry-pi-setup.md for complete Raspberry Pi setup instructions including auto-start and camera configuration.
from openflight import LaunchMonitor
# Simple usage
with LaunchMonitor() as monitor:
print("Swing when ready...")
shot = monitor.wait_for_shot(timeout=60)
if shot:
print(f"Ball Speed: {shot.ball_speed_mph:.1f} mph")
print(f"Est. Carry: {shot.estimated_carry_yards:.0f} yards")# Continuous monitoring with callbacks
from openflight import LaunchMonitor
def on_shot(shot):
print(f"Shot detected: {shot.ball_speed_mph:.1f} mph")
monitor = LaunchMonitor()
monitor.connect()
monitor.start(shot_callback=on_shot)
# ... do other things ...
stats = monitor.get_session_stats()
print(f"Average ball speed: {stats['avg_ball_speed']:.1f} mph")from openflight import OPS243Radar
# Direct radar control
with OPS243Radar() as radar:
# Get radar info
info = radar.get_info()
print(f"Firmware: {info.get('Version')}")
# Configure for golf
radar.configure_for_golf()
# Read speeds
while True:
reading = radar.read_speed()
if reading:
print(f"{reading.speed:.1f} {reading.unit}")Connect the OPS243-A to your computer via USB. The radar will appear as a serial device:
- Linux/Raspberry Pi:
/dev/ttyACM0or/dev/ttyUSB0 - macOS:
/dev/tty.usbmodem* - Windows:
COM3(or similar)
For best results, position the radar 3-5 feet behind the tee, pointing at the hitting area. The radar has a 23° beam width.
Ball Flight Direction
======================>
[Tee] ←--- 3-5 ft ---→ [OPS243-A Radar]
↓
Points at ball
Important: The radar measures radial velocity (speed toward/away from sensor). For accurate readings, the ball should travel roughly along the radar's line of sight.
openflightThe system will automatically detect the radar, configure it for golf ball detection, and start monitoring.
The OPS243-A transmits a 24 GHz signal. When this signal bounces off a moving object (the golf ball), the frequency shifts proportionally to the object's speed - this is the Doppler effect.
The relationship is:
Speed = (Doppler_Frequency × c) / (2 × Transmit_Frequency)
At 24.125 GHz, each 1 mph of speed creates a ~71.7 Hz Doppler shift.
Golf balls are challenging targets for radar due to:
- Small size: ~1.68" diameter
- Low RCS: Radar cross-section of ~0.001 m²
- High speed: 100-180+ mph for well-struck shots
- Brief detection window: Ball is in range for < 100ms
The OPS243-A handles this with:
- High transmit power (11 dBm typical)
- 15 dBi antenna gain
- 24 GHz frequency (short wavelength suits small objects)
- Fast sampling (up to 100k samples/sec)
Based on link budget analysis, the OPS243-A should reliably detect golf balls at 4-5 meters (13-16 feet), making the 3-5 foot positioning ideal.
The data flows from radar to UI like this:
┌─────────────┐ USB/Serial ┌─────────────┐ Callback ┌─────────────┐ WebSocket ┌─────────────┐
│ OPS243-A │ ───────────▶ │ Launch │ ──────────▶ │ Flask │ ──────────▶ │ React │
│ Radar │ Speed data │ Monitor │ on_shot() │ Server │ "shot" │ UI │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
-
Radar streams data - The OPS243-A continuously sends speed readings over USB serial whenever it detects motion
-
LaunchMonitor processes readings - A background thread reads serial data, accumulates readings, and when there's a gap (no readings for 0.5s), analyzes the data to create a
Shotobject with ball speed, club speed, and smash factor -
Callback fires - When a shot is detected, the callback function registered via
monitor.start(shot_callback=...)is called with theShotobject -
Server broadcasts to clients - The Flask server's callback converts the shot to JSON and emits it to all connected browsers via WebSocket
-
React updates UI - The
useSockethook receives the event and updates state, triggering a re-render with the new shot data
This callback pattern keeps the components decoupled - LaunchMonitor doesn't know about Flask or WebSockets, it just calls whatever function you give it.
The radar can be configured via API commands:
from openflight import OPS243Radar, SpeedUnit, Direction
radar = OPS243Radar()
radar.connect()
# Set units to MPH
radar.set_units(SpeedUnit.MPH)
# Increase sample rate for faster balls (up to 139 mph at 20kHz)
radar.set_sample_rate(20000)
# Filter out slow movements
radar.set_min_speed_filter(20) # Ignore < 20 mph
# Only detect outbound (ball going away)
radar.set_direction_filter(Direction.OUTBOUND)
# Save to persistent memory
radar.save_config()| Setting | Value | Why |
|---|---|---|
| Sample Rate | 20 kHz | Supports up to ~139 mph |
| Buffer Size | 512 | Faster updates (~10-15 Hz) |
| Min Speed | 10 mph | Filter slow movements |
| Direction | Outbound | Ball moving away from radar |
| Power | Max (0) | Best detection range |
- Side Spin / Curve: Requires multiple sensors or camera
Note: Launch Angle is available with the optional camera module. See Camera Setup. Spin Rate is available experimentally via rolling buffer mode (see docs/rolling_buffer_spin_detection.md).
- Distance estimates are rough: Without launch angle and spin, carry distance is estimated using a simplified model (~2.5 yards per mph of ball speed)
- Cosine error: If ball doesn't travel directly toward/away from radar, measured speed will be slightly lower than actual
- Detection is probabilistic: Very fast shots with weak returns may be missed
- Check USB connection
- Try a different USB cable
- Check if device appears:
ls /dev/tty*(Linux/Mac) - Try specifying port manually:
openflight --port /dev/ttyACM0
- Move radar closer to hitting area (try 3 feet)
- Ensure radar is pointing at ball flight path
- Check for obstructions
- Try increasing transmit power:
radar.set_transmit_power(0)
- Increase minimum speed filter to reduce noise
- Increase magnitude filter to require stronger signals
- Ensure stable mounting (vibration causes false readings)
openflight/
├── src/openflight/
│ ├── __init__.py
│ ├── ops243.py # OPS243-A radar driver
│ ├── launch_monitor.py # Main launch monitor & shot detection
│ ├── server.py # Flask-SocketIO server for web UI
│ ├── session_logger.py # JSONL session logging
│ ├── camera_tracker.py # Ball tracking (Hough circles + ByteTrack)
│ ├── camera/ # Camera subsystem
│ │ ├── capture.py # Frame capture utilities
│ │ ├── detector.py # Ball detector (Hough circles)
│ │ ├── launch_angle.py # Launch angle calculation
│ │ └── tracker.py # Tracking logic
│ ├── streaming/ # Real-time I/Q processing
│ │ ├── processor.py # FFT processing pipeline
│ │ └── cfar.py # 2D CFAR noise rejection
│ └── rolling_buffer/ # Spin rate detection
│ ├── monitor.py # Rolling buffer monitor
│ ├── processor.py # I/Q processing for spin
│ ├── trigger.py # Trigger strategies
│ └── types.py # Data types
├── ui/ # React frontend
│ ├── src/
│ │ ├── components/ # React components
│ │ ├── hooks/ # Custom hooks (WebSocket)
│ │ └── App.tsx # Main app
│ └── package.json
├── scripts/ # Utility & setup scripts
├── docs/ # Documentation
├── pyproject.toml
└── README.md
Contributions welcome! See CONTRIBUTING.md for guidelines.
Areas of interest:
- Better distance models: Improve carry estimates with more physics
- Improved spin detection: Increase rolling buffer spin detection reliability beyond 50-60%
- Mobile app: Bluetooth connection to phone
- Hardware acceleration: Optimize detection for Hailo/Coral accelerators
If you have the optional camera module for launch angle detection, first install the camera dependencies:
# Install camera support (OpenCV, NumPy, picamera2)
uv pip install -e ".[camera]"Then use the calibration script to tune detection settings:
# Live view with ball detection overlay
python scripts/calibrate_camera.py
# Without detection overlay (just camera feed)
python scripts/calibrate_camera.py --no-detect
# Mock mode (no camera hardware, uses synthetic frames)
python scripts/calibrate_camera.py --mock
# Adjust camera settings
python scripts/calibrate_camera.py --exposure 2000 --gain 4.0 --threshold 200Keyboard controls during calibration:
| Key | Action |
|---|---|
q |
Quit |
s |
Save current frame |
t |
Toggle threshold view |
d |
Toggle detection overlay |
+/- |
Adjust brightness threshold |
[/] |
Adjust minimum radius |
{/} |
Adjust maximum radius |
The script will display final settings when you exit - use these values to configure detection in your setup.
The default detection uses Hough circle transform, tunable via --hough-param2 (lower = more sensitive, default 27). YOLO models can optionally be used via --camera-model. For YOLO optimization, see docs/yolo-performance-tuning.md.
# Install dev dependencies
uv pip install -e ".[ui]"
uv pip install pytest
# Run tests
pytest tests/ -vSee CONTRIBUTING.md for full guidelines. Quick summary:
- Tests required: Run
pytest tests/ -vbefore submitting a PR - Code quality: Run
pylint src/openflight/(score 9+) - UI changes: Run
cd ui && npm run buildto verify
- Raspberry Pi Setup Guide - Complete Pi 5 setup with touchscreen and camera
- Parts List - Full hardware requirements
- Rolling Buffer & Spin Detection - Spin rate measurement via I/Q analysis
- YOLO Performance Tuning - Optimize YOLO ball detection FPS
- Contributing Guide - How to contribute
- Changelog - Version history
MIT License - see LICENSE file.
- OmniPreSense for the OPS243-A radar and documentation
- The golf hacker community for inspiration