Ardupilot Controller Script
This commit is contained in:
39
README.md
39
README.md
@@ -22,32 +22,25 @@ ros2 launch gazebo/launch/drone_landing.launch.py
|
|||||||
```bash
|
```bash
|
||||||
source activate.sh
|
source activate.sh
|
||||||
python run_gazebo.py --pattern circular
|
python run_gazebo.py --pattern circular
|
||||||
python camera_viewer.py # View camera
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### ArduPilot GPS-Denied (2 Terminals)
|
### ArduPilot GPS-Denied (2 Terminals)
|
||||||
|
|
||||||
**Terminal 1:**
|
**Terminal 1 - Gazebo:**
|
||||||
```bash
|
```bash
|
||||||
./scripts/run_ardupilot_sim.sh runway
|
./scripts/run_ardupilot_sim.sh runway
|
||||||
# Options: runway, warehouse, zephyr
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Terminal 2:**
|
**Terminal 2 - Controller:**
|
||||||
```bash
|
```bash
|
||||||
sim_vehicle.py -v ArduCopter -f gazebo-iris --model JSON --console
|
./scripts/run_ardupilot_controller.sh
|
||||||
|
|
||||||
# GPS-denied mode:
|
|
||||||
param set ARMING_CHECK 0
|
|
||||||
mode stabilize
|
|
||||||
arm throttle force
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./setup/install_ubuntu.sh
|
./setup/install_ubuntu.sh
|
||||||
./setup/install_ardupilot.sh # Optional
|
./setup/install_ardupilot.sh # Optional for ArduPilot
|
||||||
source activate.sh
|
source activate.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -55,12 +48,12 @@ source activate.sh
|
|||||||
|
|
||||||
| File | Description |
|
| File | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `standalone_simulation.py` | All-in-one simulation |
|
| `drone_controller.py` | **Your landing algorithm (used everywhere)** |
|
||||||
| `run_gazebo.py` | Gazebo controllers |
|
| `standalone_simulation.py` | PyBullet simulation |
|
||||||
| `scripts/run_ardupilot_sim.sh` | ArduPilot launcher (auto GPU) |
|
| `run_gazebo.py` | Gazebo + ROS 2 interface |
|
||||||
|
| `run_ardupilot.py` | ArduPilot + MAVLink interface |
|
||||||
|
| `config.py` | Configuration settings |
|
||||||
| `camera_viewer.py` | Camera feed window |
|
| `camera_viewer.py` | Camera feed window |
|
||||||
| `drone_controller.py` | **Your landing algorithm** |
|
|
||||||
| `config.py` | Configuration |
|
|
||||||
|
|
||||||
## Sensors (GPS-Denied)
|
## Sensors (GPS-Denied)
|
||||||
|
|
||||||
@@ -71,16 +64,10 @@ source activate.sh
|
|||||||
| Camera | Downward image |
|
| Camera | Downward image |
|
||||||
| Landing Pad | Relative position (when visible) |
|
| Landing Pad | Relative position (when visible) |
|
||||||
|
|
||||||
## Options
|
## Documentation
|
||||||
|
|
||||||
```bash
|
|
||||||
--pattern stationary, linear, circular, square, random
|
|
||||||
--speed Speed in m/s (default: 0.5)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docs
|
|
||||||
|
|
||||||
- [Installation](docs/installation.md)
|
- [Installation](docs/installation.md)
|
||||||
- [Architecture](docs/architecture.md)
|
- [Architecture](docs/architecture.md)
|
||||||
- [ArduPilot](docs/ardupilot.md)
|
- [ArduPilot Guide](docs/ardupilot.md)
|
||||||
- [Gazebo](docs/gazebo.md)
|
- [Gazebo Guide](docs/gazebo.md)
|
||||||
|
- [Drone Logic Guide](docs/drone_guide.md)
|
||||||
@@ -24,14 +24,20 @@ Terminal 1 Terminal 2
|
|||||||
└───────────────────┘ ROS └───────────────────┘
|
└───────────────────┘ ROS └───────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. ArduPilot GPS-Denied (2 Terminals)
|
### 3. ArduPilot (2 Terminals)
|
||||||
|
|
||||||
```
|
```
|
||||||
Terminal 1 Terminal 2
|
Terminal 1 Terminal 2
|
||||||
┌───────────────────┐ ┌───────────────────┐
|
┌───────────────────┐ ┌────────────────────────────┐
|
||||||
│ Gazebo + │◄──────►│ ArduPilot SITL │
|
│ Gazebo + │◄──────►│ run_ardupilot_controller.sh│
|
||||||
│ ArduPilot Plugin │ JSON │ + MAVProxy │
|
│ ArduPilot Plugin │ JSON │ ┌──────────────────┐ │
|
||||||
└───────────────────┘ └───────────────────┘
|
└───────────────────┘ │ │ ArduPilot SITL │ │
|
||||||
|
│ └─────────┬────────┘ │
|
||||||
|
│ │ MAVLink │
|
||||||
|
│ ┌─────────▼────────┐ │
|
||||||
|
│ │ run_ardupilot.py │ │
|
||||||
|
│ └──────────────────┘ │
|
||||||
|
└────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
## Data Flow
|
## Data Flow
|
||||||
@@ -48,31 +54,45 @@ Controller → /cmd_vel → Gazebo → /odometry → Controller
|
|||||||
|
|
||||||
### ArduPilot
|
### ArduPilot
|
||||||
```
|
```
|
||||||
Gazebo ◄─── JSON/UDP ───► SITL ◄─── MAVLink ───► MAVProxy
|
Gazebo ◄─── JSON ───► SITL ◄─── MAVLink ───► Controller
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `drone_controller.py` | Your landing algorithm |
|
| `drone_controller.py` | **Your landing algorithm (used in ALL modes)** |
|
||||||
| `gazebo_bridge.py` | Gazebo ↔ ROS bridge |
|
| `run_ardupilot.py` | MAVLink interface for ArduPilot |
|
||||||
| `camera_viewer.py` | Camera display |
|
| `run_gazebo.py` | ROS 2 interface for Gazebo |
|
||||||
| `config.py` | Configuration |
|
| `standalone_simulation.py` | PyBullet simulation engine |
|
||||||
|
| `config.py` | Shared configuration |
|
||||||
|
|
||||||
## GPS-Denied Sensors
|
## GPS-Denied Sensors
|
||||||
|
|
||||||
| Sensor | Data |
|
The controller receives this standardized telemetry structure in all modes:
|
||||||
|--------|------|
|
|
||||||
| IMU | Orientation, angular velocity |
|
|
||||||
| Altimeter | Altitude, vertical velocity |
|
|
||||||
| Camera | Downward image |
|
|
||||||
| Landing Pad | Relative position |
|
|
||||||
|
|
||||||
## Topics
|
```python
|
||||||
|
telemetry = {
|
||||||
| Topic | Direction |
|
"altimeter": {
|
||||||
|-------|-----------|
|
"altitude": float, # Meters
|
||||||
| `/cmd_vel` | → Drone commands |
|
"vertical_velocity": float # m/s (positive = up)
|
||||||
| `/drone/telemetry` | ← Sensor data |
|
},
|
||||||
| `/drone/camera` | ← Camera images |
|
"velocity": { # Body or Local frame
|
||||||
|
"x": float,
|
||||||
|
"y": float,
|
||||||
|
"z": float
|
||||||
|
},
|
||||||
|
"imu": {
|
||||||
|
"orientation": {
|
||||||
|
"roll": float,
|
||||||
|
"pitch": float,
|
||||||
|
"yaw": float
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"landing_pad": { # If visible (None otherwise)
|
||||||
|
"relative_x": float,
|
||||||
|
"relative_y": float,
|
||||||
|
"distance": float
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,9 +1,38 @@
|
|||||||
# ArduPilot GPS-Denied Simulation
|
# ArduPilot GPS-Denied Simulation
|
||||||
|
|
||||||
Realistic flight controller simulation without GPS.
|
Realistic flight controller simulation with your drone logic.
|
||||||
|
|
||||||
## Quick Start (2 Terminals)
|
## Quick Start (2 Terminals)
|
||||||
|
|
||||||
|
**Terminal 1 - Gazebo:**
|
||||||
|
```bash
|
||||||
|
./scripts/run_ardupilot_sim.sh runway
|
||||||
|
# Options: runway, warehouse, zephyr
|
||||||
|
```
|
||||||
|
|
||||||
|
**Terminal 2 - Controller + SITL:**
|
||||||
|
```bash
|
||||||
|
./scripts/run_ardupilot_controller.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
The `run_ardupilot_controller.sh` script starts ArduPilot SITL in the background and connects your controller to it via MAVLink.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ Gazebo │◄───►│ ArduPilot SITL │◄───►│ run_ardupilot.py│
|
||||||
|
│ (Physics) │JSON │ (Hidden) │MAV │ (Your Logic) │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
drone_controller.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Mode (Debugging)
|
||||||
|
|
||||||
|
If you need to debug with MAVProxy console (3 Terminals):
|
||||||
|
|
||||||
**Terminal 1:**
|
**Terminal 1:**
|
||||||
```bash
|
```bash
|
||||||
./scripts/run_ardupilot_sim.sh runway
|
./scripts/run_ardupilot_sim.sh runway
|
||||||
@@ -12,10 +41,13 @@ Realistic flight controller simulation without GPS.
|
|||||||
**Terminal 2:**
|
**Terminal 2:**
|
||||||
```bash
|
```bash
|
||||||
sim_vehicle.py -v ArduCopter -f gazebo-iris --model JSON --console
|
sim_vehicle.py -v ArduCopter -f gazebo-iris --model JSON --console
|
||||||
|
```
|
||||||
|
|
||||||
# GPS-denied mode:
|
**Terminal 3:**
|
||||||
|
```bash
|
||||||
|
# Debug commands in MAVProxy:
|
||||||
param set ARMING_CHECK 0
|
param set ARMING_CHECK 0
|
||||||
mode stabilize
|
mode guided
|
||||||
arm throttle force
|
arm throttle force
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -26,67 +58,32 @@ arm throttle force
|
|||||||
source ~/.bashrc
|
source ~/.bashrc
|
||||||
```
|
```
|
||||||
|
|
||||||
## World Options
|
## Configuration
|
||||||
|
|
||||||
```bash
|
Your `drone_controller.py` receives telemetry and returns control inputs.
|
||||||
./scripts/run_ardupilot_sim.sh runway # Iris on runway (default)
|
|
||||||
./scripts/run_ardupilot_sim.sh warehouse # Iris in warehouse
|
|
||||||
./scripts/run_ardupilot_sim.sh zephyr # Zephyr plane
|
|
||||||
./scripts/run_ardupilot_sim.sh gimbal # Gimbal test
|
|
||||||
./scripts/run_ardupilot_sim.sh parachute # Parachute test
|
|
||||||
```
|
|
||||||
|
|
||||||
## GPS-Denied Configuration
|
The simulation translates your inputs:
|
||||||
|
- `thrust` → Vertical velocity
|
||||||
In MAVProxy console:
|
- `pitch/roll` → Horizontal velocity
|
||||||
|
- `yaw` → Yaw rate
|
||||||
```bash
|
|
||||||
param set ARMING_CHECK 0 # Disable pre-arm checks
|
|
||||||
mode stabilize # Manual with stabilization
|
|
||||||
arm throttle force # Force arm
|
|
||||||
```
|
|
||||||
|
|
||||||
## GPU Support
|
|
||||||
|
|
||||||
Auto-detects: NVIDIA > Intel > AMD > Software
|
|
||||||
|
|
||||||
Check your GPU:
|
|
||||||
```bash
|
|
||||||
glxinfo | grep "OpenGL renderer"
|
|
||||||
```
|
|
||||||
|
|
||||||
## MAVProxy Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mode stabilize # Manual mode
|
|
||||||
mode alt_hold # Altitude hold
|
|
||||||
arm throttle force # Arm motors
|
|
||||||
takeoff 5 # Takeoff 5m
|
|
||||||
mode land # Land
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "SITL failed to start"
|
||||||
|
Check if `sim_vehicle.py` is in your PATH:
|
||||||
|
```bash
|
||||||
|
export PATH=$PATH:~/ardupilot/Tools/autotest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Drone drift
|
||||||
|
ArduPilot in GUIDED mode requires good position estimation. Without GPS, it relies on optical flow or visual odometry (not yet implemented in default setup). The drone might drift if relying only on IMU.
|
||||||
|
|
||||||
### "No JSON sensor message"
|
### "No JSON sensor message"
|
||||||
|
Ensure Gazebo (Terminal 1) is running before starting the controller.
|
||||||
|
|
||||||
Gazebo isn't sending data to SITL. Check:
|
## Visualizing Camera
|
||||||
1. Start Gazebo FIRST, then SITL
|
|
||||||
2. Plugin path is set:
|
|
||||||
```bash
|
|
||||||
echo $GZ_SIM_SYSTEM_PLUGIN_PATH | grep ardupilot
|
|
||||||
```
|
|
||||||
|
|
||||||
### Simulation laggy
|
|
||||||
|
|
||||||
Check GPU is being used (not software):
|
|
||||||
```bash
|
|
||||||
glxinfo | grep "OpenGL renderer"
|
|
||||||
# Should NOT show "llvmpipe"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Can't arm
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
param set ARMING_CHECK 0
|
python camera_viewer.py --topic /drone/camera
|
||||||
arm throttle force
|
|
||||||
```
|
```
|
||||||
|
(Requires bridging the topic if using ROS 2 bridge)
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ Implement your GPS-denied landing algorithm.
|
|||||||
1. Edit `drone_controller.py`
|
1. Edit `drone_controller.py`
|
||||||
2. Find `calculate_landing_maneuver()`
|
2. Find `calculate_landing_maneuver()`
|
||||||
3. Implement your algorithm
|
3. Implement your algorithm
|
||||||
4. Test: `python standalone_simulation.py`
|
4. Test with any simulation mode
|
||||||
|
|
||||||
## Function to Implement
|
## Function to Implement
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def calculate_landing_maneuver(self, telemetry, rover_telemetry):
|
def calculate_landing_maneuver(self, telemetry, rover_telemetry):
|
||||||
|
# Your logic here
|
||||||
return (thrust, pitch, roll, yaw)
|
return (thrust, pitch, roll, yaw)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -38,12 +39,16 @@ if landing_pad:
|
|||||||
|
|
||||||
| Value | Range | Effect |
|
| Value | Range | Effect |
|
||||||
|-------|-------|--------|
|
|-------|-------|--------|
|
||||||
| thrust | ±1.0 | Up/down |
|
| thrust | ±1.0 | Up/down (positive = up) |
|
||||||
| pitch | ±0.5 | Forward/back |
|
| pitch | ±0.5 | Forward/back |
|
||||||
| roll | ±0.5 | Left/right |
|
| roll | ±0.5 | Left/right |
|
||||||
| yaw | ±0.5 | Rotation |
|
| yaw | ±0.5 | Rotation |
|
||||||
|
|
||||||
## Example Algorithm
|
Note: In ArduPilot mode, these are scaled to velocities:
|
||||||
|
- Thrust → Z velocity
|
||||||
|
- Pitch/Roll → X/Y velocity
|
||||||
|
|
||||||
|
## Example Algorithm (PD Control)
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def calculate_landing_maneuver(self, telemetry, rover_telemetry):
|
def calculate_landing_maneuver(self, telemetry, rover_telemetry):
|
||||||
@@ -51,48 +56,18 @@ def calculate_landing_maneuver(self, telemetry, rover_telemetry):
|
|||||||
altitude = alt.get('altitude', 5.0)
|
altitude = alt.get('altitude', 5.0)
|
||||||
vert_vel = alt.get('vertical_velocity', 0.0)
|
vert_vel = alt.get('vertical_velocity', 0.0)
|
||||||
|
|
||||||
vel = telemetry.get('velocity', {})
|
|
||||||
vel_x = vel.get('x', 0.0)
|
|
||||||
vel_y = vel.get('y', 0.0)
|
|
||||||
|
|
||||||
pad = telemetry.get('landing_pad')
|
|
||||||
|
|
||||||
# Altitude PD control
|
# Altitude PD control
|
||||||
thrust = 0.5 * (0 - altitude) - 0.3 * vert_vel
|
thrust = 0.5 * (target_alt - altitude) - 0.3 * vert_vel
|
||||||
|
|
||||||
# Horizontal control
|
# Horizontal control
|
||||||
|
pad = telemetry.get('landing_pad')
|
||||||
if pad:
|
if pad:
|
||||||
pitch = 0.3 * pad['relative_x'] - 0.2 * vel_x
|
pitch = 0.3 * pad['relative_x']
|
||||||
roll = 0.3 * pad['relative_y'] - 0.2 * vel_y
|
roll = 0.3 * pad['relative_y']
|
||||||
else:
|
else:
|
||||||
pitch = -0.2 * vel_x
|
# Hover
|
||||||
roll = -0.2 * vel_y
|
pitch = 0
|
||||||
|
roll = 0
|
||||||
|
|
||||||
return (thrust, pitch, roll, 0.0)
|
return (thrust, pitch, roll, 0.0)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Easy
|
|
||||||
python standalone_simulation.py --pattern stationary
|
|
||||||
|
|
||||||
# Medium
|
|
||||||
python standalone_simulation.py --pattern circular --speed 0.3
|
|
||||||
|
|
||||||
# Hard
|
|
||||||
python standalone_simulation.py --pattern random --speed 0.5
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
Edit `config.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
CONTROLLER = {
|
|
||||||
"Kp_z": 0.5,
|
|
||||||
"Kd_z": 0.3,
|
|
||||||
"Kp_xy": 0.3,
|
|
||||||
"Kd_xy": 0.2,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|||||||
439
run_ardupilot.py
439
run_ardupilot.py
@@ -1,235 +1,284 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
ArduPilot ROS 2 Launcher - Official DDS Integration.
|
ArduPilot Drone Controller
|
||||||
|
==========================
|
||||||
This script provides a convenient way to launch the ArduPilot SITL simulation
|
Connects your drone_controller.py logic to ArduPilot SITL via MAVLink.
|
||||||
using the official ardupilot_gz packages with ROS 2 DDS support.
|
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python run_ardupilot.py # Launch Iris on runway
|
Terminal 1: ./scripts/run_ardupilot_sim.sh runway
|
||||||
python run_ardupilot.py --world maze # Launch Iris in maze
|
Terminal 2: sim_vehicle.py -v ArduCopter -f gazebo-iris --model JSON --console
|
||||||
python run_ardupilot.py --vehicle rover # Launch WildThumper rover
|
Terminal 3: python run_ardupilot.py
|
||||||
python run_ardupilot.py --mavproxy # Also start MAVProxy
|
|
||||||
|
|
||||||
Prerequisites:
|
This script:
|
||||||
- ArduPilot ROS 2 packages installed (./setup/install_ardupilot.sh)
|
1. Connects to ArduPilot SITL via MAVLink
|
||||||
- ROS 2 Humble/Jazzy sourced
|
2. Gets telemetry (position, attitude, velocity)
|
||||||
- ~/ardu_ws workspace built and sourced
|
3. Runs your drone_controller.calculate_landing_maneuver()
|
||||||
|
4. Sends velocity commands to ArduPilot
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import signal
|
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
import math
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pymavlink import mavutil
|
||||||
|
except ImportError:
|
||||||
|
print("ERROR: pymavlink not installed")
|
||||||
|
print("Run: pip install pymavlink")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
from config import ARDUPILOT, CONTROLLER, MAVLINK
|
||||||
|
from drone_controller import DroneController
|
||||||
|
|
||||||
|
|
||||||
def check_ros2():
|
class ArduPilotInterface:
|
||||||
"""Check if ROS 2 is available."""
|
"""Interface to ArduPilot SITL via MAVLink."""
|
||||||
try:
|
|
||||||
subprocess.run(['ros2', '--help'], capture_output=True, check=True)
|
|
||||||
return True
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
def __init__(self, connection_string=None):
|
||||||
|
self.connection_string = connection_string or MAVLINK["connection_string"]
|
||||||
|
self.mav = None
|
||||||
|
self.armed = False
|
||||||
|
self.mode = "UNKNOWN"
|
||||||
|
|
||||||
def check_ardupilot_packages():
|
# Telemetry state
|
||||||
"""Check if ArduPilot ROS 2 packages are installed."""
|
self.position = {"x": 0, "y": 0, "z": 0}
|
||||||
try:
|
self.velocity = {"x": 0, "y": 0, "z": 0}
|
||||||
result = subprocess.run(
|
self.attitude = {"roll": 0, "pitch": 0, "yaw": 0}
|
||||||
['ros2', 'pkg', 'list'],
|
self.altitude = 0
|
||||||
capture_output=True,
|
self.vertical_velocity = 0
|
||||||
text=True,
|
|
||||||
check=True
|
|
||||||
)
|
|
||||||
packages = result.stdout
|
|
||||||
return 'ardupilot_gz_bringup' in packages or 'ardupilot_sitl' in packages
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
def connect(self, timeout=30):
|
||||||
|
"""Connect to ArduPilot SITL."""
|
||||||
|
print(f"[INFO] Connecting to {self.connection_string}...")
|
||||||
|
|
||||||
def source_workspace():
|
|
||||||
"""Source the ArduPilot workspace."""
|
|
||||||
ardu_ws = os.path.expanduser("~/ardu_ws")
|
|
||||||
setup_bash = os.path.join(ardu_ws, "install", "setup.bash")
|
|
||||||
|
|
||||||
if os.path.exists(setup_bash):
|
|
||||||
# Update environment by sourcing the workspace
|
|
||||||
# This is done by running commands in a sourced shell
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_launch_command(world: str, vehicle: str) -> list:
|
|
||||||
"""Get the appropriate launch command."""
|
|
||||||
|
|
||||||
launch_files = {
|
|
||||||
# Copter configurations
|
|
||||||
'runway': ('ardupilot_gz_bringup', 'iris_runway.launch.py'),
|
|
||||||
'maze': ('ardupilot_gz_bringup', 'iris_maze.launch.py'),
|
|
||||||
'iris': ('ardupilot_gz_bringup', 'iris_runway.launch.py'),
|
|
||||||
|
|
||||||
# Rover configurations
|
|
||||||
'rover': ('ardupilot_gz_bringup', 'wildthumper_playpen.launch.py'),
|
|
||||||
'wildthumper': ('ardupilot_gz_bringup', 'wildthumper_playpen.launch.py'),
|
|
||||||
|
|
||||||
# SITL only (no Gazebo)
|
|
||||||
'sitl': ('ardupilot_sitl', 'sitl_dds_udp.launch.py'),
|
|
||||||
}
|
|
||||||
|
|
||||||
if vehicle == 'rover':
|
|
||||||
key = 'rover'
|
|
||||||
else:
|
|
||||||
key = world if world in launch_files else 'runway'
|
|
||||||
|
|
||||||
package, launch_file = launch_files.get(key, launch_files['runway'])
|
|
||||||
|
|
||||||
cmd = ['ros2', 'launch', package, launch_file]
|
|
||||||
|
|
||||||
# Add SITL-specific parameters if using sitl_dds_udp
|
|
||||||
if launch_file == 'sitl_dds_udp.launch.py':
|
|
||||||
cmd.extend([
|
|
||||||
'transport:=udp4',
|
|
||||||
'synthetic_clock:=True',
|
|
||||||
'model:=quad' if vehicle == 'copter' else 'rover',
|
|
||||||
'speedup:=1',
|
|
||||||
])
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
|
|
||||||
|
|
||||||
def launch_mavproxy(master_port: int = 14550):
|
|
||||||
"""Launch MAVProxy in a new terminal."""
|
|
||||||
mavproxy_cmd = f"mavproxy.py --console --map --master=:{master_port}"
|
|
||||||
|
|
||||||
# Try different terminal emulators
|
|
||||||
terminals = [
|
|
||||||
['gnome-terminal', '--', 'bash', '-c', mavproxy_cmd],
|
|
||||||
['xterm', '-e', mavproxy_cmd],
|
|
||||||
['konsole', '-e', mavproxy_cmd],
|
|
||||||
]
|
|
||||||
|
|
||||||
for term_cmd in terminals:
|
|
||||||
try:
|
try:
|
||||||
subprocess.Popen(term_cmd)
|
self.mav = mavutil.mavlink_connection(self.connection_string)
|
||||||
|
self.mav.wait_heartbeat(timeout=timeout)
|
||||||
|
print(f"[OK] Connected to system {self.mav.target_system}")
|
||||||
return True
|
return True
|
||||||
except FileNotFoundError:
|
except Exception as e:
|
||||||
continue
|
print(f"[ERROR] Connection failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
print(f"[WARN] Could not open terminal for MAVProxy")
|
def update_telemetry(self):
|
||||||
print(f"[INFO] Run manually: {mavproxy_cmd}")
|
"""Read and update telemetry from ArduPilot."""
|
||||||
return False
|
# Read all available messages
|
||||||
|
while True:
|
||||||
|
msg = self.mav.recv_match(blocking=False)
|
||||||
|
if msg is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
msg_type = msg.get_type()
|
||||||
|
|
||||||
def parse_args():
|
if msg_type == "HEARTBEAT":
|
||||||
parser = argparse.ArgumentParser(
|
self.armed = (msg.base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED) != 0
|
||||||
description='Launch ArduPilot SITL with ROS 2 and Gazebo',
|
# Get mode name
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
if hasattr(mavutil.mavlink, 'enums'):
|
||||||
epilog="""
|
mode_map = mavutil.mavlink.enums.get('COPTER_MODE', {})
|
||||||
Examples:
|
self.mode = mode_map.get(msg.custom_mode, {}).get('name', str(msg.custom_mode))
|
||||||
python run_ardupilot.py # Iris on runway
|
|
||||||
python run_ardupilot.py --world maze # Iris in maze
|
|
||||||
python run_ardupilot.py --vehicle rover # WildThumper rover
|
|
||||||
python run_ardupilot.py --mavproxy # With MAVProxy
|
|
||||||
|
|
||||||
Available worlds: runway, maze, sitl (no Gazebo)
|
elif msg_type == "LOCAL_POSITION_NED":
|
||||||
Available vehicles: copter, rover
|
self.position = {"x": msg.x, "y": msg.y, "z": msg.z}
|
||||||
|
self.velocity = {"x": msg.vx, "y": msg.vy, "z": msg.vz}
|
||||||
|
self.altitude = -msg.z # NED: down is positive
|
||||||
|
self.vertical_velocity = -msg.vz
|
||||||
|
|
||||||
|
elif msg_type == "ATTITUDE":
|
||||||
|
self.attitude = {
|
||||||
|
"roll": msg.roll,
|
||||||
|
"pitch": msg.pitch,
|
||||||
|
"yaw": msg.yaw
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_telemetry(self):
|
||||||
|
"""Get telemetry in the same format as standalone simulation."""
|
||||||
|
self.update_telemetry()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"imu": {
|
||||||
|
"orientation": self.attitude,
|
||||||
|
"angular_velocity": {"x": 0, "y": 0, "z": 0} # Not available via MAVLink easily
|
||||||
|
},
|
||||||
|
"altimeter": {
|
||||||
|
"altitude": self.altitude,
|
||||||
|
"vertical_velocity": self.vertical_velocity
|
||||||
|
},
|
||||||
|
"velocity": self.velocity,
|
||||||
|
"landing_pad": None, # Not available in ArduPilot mode - use vision
|
||||||
|
"position": self.position, # Extra: actual position (GPS-like)
|
||||||
|
"armed": self.armed,
|
||||||
|
"mode": self.mode
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_mode(self, mode_name):
|
||||||
|
"""Set flight mode."""
|
||||||
|
mode_map = self.mav.mode_mapping()
|
||||||
|
if mode_name.upper() not in mode_map:
|
||||||
|
print(f"[WARN] Unknown mode: {mode_name}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
mode_id = mode_map[mode_name.upper()]
|
||||||
|
self.mav.set_mode(mode_id)
|
||||||
|
print(f"[OK] Mode set to {mode_name}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def arm(self, force=True):
|
||||||
|
"""Arm motors."""
|
||||||
|
if force:
|
||||||
|
# Force arm (bypass checks)
|
||||||
|
self.mav.mav.command_long_send(
|
||||||
|
self.mav.target_system,
|
||||||
|
self.mav.target_component,
|
||||||
|
mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM,
|
||||||
|
0, 1, 21196, 0, 0, 0, 0, 0 # 21196 = force arm
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.mav.arducopter_arm()
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
self.update_telemetry()
|
||||||
|
if self.armed:
|
||||||
|
print("[OK] Armed")
|
||||||
|
return self.armed
|
||||||
|
|
||||||
|
def disarm(self):
|
||||||
|
"""Disarm motors."""
|
||||||
|
self.mav.arducopter_disarm()
|
||||||
|
print("[OK] Disarmed")
|
||||||
|
|
||||||
|
def takeoff(self, altitude=5.0):
|
||||||
|
"""Takeoff to specified altitude."""
|
||||||
|
self.mav.mav.command_long_send(
|
||||||
|
self.mav.target_system,
|
||||||
|
self.mav.target_component,
|
||||||
|
mavutil.mavlink.MAV_CMD_NAV_TAKEOFF,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, altitude
|
||||||
|
)
|
||||||
|
print(f"[OK] Takeoff to {altitude}m")
|
||||||
|
|
||||||
|
def send_velocity(self, vx, vy, vz, yaw_rate=0):
|
||||||
|
"""Send velocity command in body frame.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vx: Forward velocity (m/s)
|
||||||
|
vy: Right velocity (m/s)
|
||||||
|
vz: Down velocity (m/s, positive = descend)
|
||||||
|
yaw_rate: Yaw rate (rad/s)
|
||||||
"""
|
"""
|
||||||
)
|
# Type mask: use velocity only
|
||||||
|
type_mask = (
|
||||||
|
mavutil.mavlink.POSITION_TARGET_TYPEMASK_X_IGNORE |
|
||||||
|
mavutil.mavlink.POSITION_TARGET_TYPEMASK_Y_IGNORE |
|
||||||
|
mavutil.mavlink.POSITION_TARGET_TYPEMASK_Z_IGNORE |
|
||||||
|
mavutil.mavlink.POSITION_TARGET_TYPEMASK_AX_IGNORE |
|
||||||
|
mavutil.mavlink.POSITION_TARGET_TYPEMASK_AY_IGNORE |
|
||||||
|
mavutil.mavlink.POSITION_TARGET_TYPEMASK_AZ_IGNORE |
|
||||||
|
mavutil.mavlink.POSITION_TARGET_TYPEMASK_YAW_IGNORE
|
||||||
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
self.mav.mav.set_position_target_local_ned_send(
|
||||||
'--world', '-w', type=str, default='runway',
|
0, # time_boot_ms
|
||||||
choices=['runway', 'maze', 'sitl'],
|
self.mav.target_system,
|
||||||
help='Simulation world (default: runway)'
|
self.mav.target_component,
|
||||||
)
|
mavutil.mavlink.MAV_FRAME_BODY_NED,
|
||||||
parser.add_argument(
|
type_mask,
|
||||||
'--vehicle', '-v', type=str, default='copter',
|
0, 0, 0, # position (ignored)
|
||||||
choices=['copter', 'rover'],
|
vx, vy, vz, # velocity
|
||||||
help='Vehicle type (default: copter)'
|
0, 0, 0, # acceleration (ignored)
|
||||||
)
|
0, yaw_rate # yaw, yaw_rate
|
||||||
parser.add_argument(
|
)
|
||||||
'--mavproxy', '-m', action='store_true',
|
|
||||||
help='Also launch MAVProxy in a new terminal'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--mavproxy-port', type=int, default=14550,
|
|
||||||
help='MAVProxy master port (default: 14550)'
|
|
||||||
)
|
|
||||||
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = parse_args()
|
parser = argparse.ArgumentParser(description="ArduPilot Drone Controller")
|
||||||
|
parser.add_argument("--connection", "-c", default=None,
|
||||||
|
help="MAVLink connection string (default: from config.py)")
|
||||||
|
parser.add_argument("--takeoff-alt", "-a", type=float, default=5.0,
|
||||||
|
help="Takeoff altitude (meters)")
|
||||||
|
parser.add_argument("--no-takeoff", action="store_true",
|
||||||
|
help="Don't auto takeoff, just control")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
print("=" * 60)
|
# Create ArduPilot interface
|
||||||
print(" ArduPilot SITL + Gazebo (Official ROS 2 DDS)")
|
ardupilot = ArduPilotInterface(args.connection)
|
||||||
print("=" * 60)
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Check ROS 2
|
# Connect
|
||||||
if not check_ros2():
|
if not ardupilot.connect():
|
||||||
print("[ERROR] ROS 2 not found!")
|
print("[ERROR] Could not connect to ArduPilot")
|
||||||
print("Please source ROS 2:")
|
print("Make sure sim_vehicle.py is running")
|
||||||
print(" source /opt/ros/humble/setup.bash")
|
sys.exit(1)
|
||||||
return 1
|
|
||||||
|
|
||||||
print("[OK] ROS 2 available")
|
# Create drone controller
|
||||||
|
controller = DroneController()
|
||||||
|
|
||||||
# Check ArduPilot packages
|
print("\n" + "=" * 50)
|
||||||
if not check_ardupilot_packages():
|
print(" ArduPilot GPS-Denied Controller")
|
||||||
print("[ERROR] ArduPilot ROS 2 packages not found!")
|
print("=" * 50)
|
||||||
print()
|
print(f"Mode: {ardupilot.mode}")
|
||||||
print("Please install ArduPilot ROS 2:")
|
print(f"Armed: {ardupilot.armed}")
|
||||||
print(" ./setup/install_ardupilot.sh")
|
print("")
|
||||||
print()
|
|
||||||
print("Then source the workspace:")
|
|
||||||
print(" source ~/ardu_ws/install/setup.bash")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print("[OK] ArduPilot ROS 2 packages found")
|
if not args.no_takeoff:
|
||||||
|
# Set GUIDED mode and arm
|
||||||
|
print("[INFO] Setting up for takeoff...")
|
||||||
|
ardupilot.set_mode("GUIDED")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
# Get launch command
|
ardupilot.arm(force=True)
|
||||||
launch_cmd = get_launch_command(args.world, args.vehicle)
|
time.sleep(1)
|
||||||
|
|
||||||
print()
|
if ardupilot.armed:
|
||||||
print(f"World: {args.world}")
|
ardupilot.takeoff(args.takeoff_alt)
|
||||||
print(f"Vehicle: {args.vehicle}")
|
print(f"[INFO] Taking off to {args.takeoff_alt}m...")
|
||||||
print(f"Launch: {' '.join(launch_cmd)}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Launch MAVProxy if requested
|
# Wait for takeoff
|
||||||
if args.mavproxy:
|
for _ in range(50): # 5 seconds
|
||||||
print("[INFO] Starting MAVProxy...")
|
telemetry = ardupilot.get_telemetry()
|
||||||
# Delay to let SITL start first
|
if telemetry["altimeter"]["altitude"] > args.takeoff_alt * 0.9:
|
||||||
time.sleep(2)
|
print("[OK] Reached target altitude")
|
||||||
launch_mavproxy(args.mavproxy_port)
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
print("Starting simulation...")
|
print("\n[INFO] Starting controller loop...")
|
||||||
print("Press Ctrl+C to stop.")
|
print("[INFO] Press Ctrl+C to stop\n")
|
||||||
print()
|
|
||||||
print("-" * 60)
|
|
||||||
|
|
||||||
# Handle Ctrl+C gracefully
|
rate = 20 # Hz
|
||||||
def signal_handler(sig, frame):
|
|
||||||
print("\nShutting down...")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
|
||||||
|
|
||||||
# Run the launch command
|
|
||||||
try:
|
try:
|
||||||
subprocess.run(launch_cmd, check=True)
|
while True:
|
||||||
except subprocess.CalledProcessError as e:
|
# Get telemetry
|
||||||
print(f"[ERROR] Launch failed: {e}")
|
telemetry = ardupilot.get_telemetry()
|
||||||
return 1
|
|
||||||
|
# Run your controller
|
||||||
|
thrust, pitch, roll, yaw = controller.calculate_landing_maneuver(
|
||||||
|
telemetry,
|
||||||
|
rover_telemetry=None # No rover in ArduPilot mode yet
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert control outputs to velocities
|
||||||
|
# thrust -> vertical velocity (negative = up)
|
||||||
|
# pitch -> forward velocity
|
||||||
|
# roll -> right velocity
|
||||||
|
vz = -thrust * 2.0 # Scale factor
|
||||||
|
vx = pitch * 2.0
|
||||||
|
vy = roll * 2.0
|
||||||
|
yaw_rate = yaw * 1.0
|
||||||
|
|
||||||
|
# Send velocity command
|
||||||
|
ardupilot.send_velocity(vx, vy, vz, yaw_rate)
|
||||||
|
|
||||||
|
# Print status
|
||||||
|
alt = telemetry["altimeter"]["altitude"]
|
||||||
|
mode = telemetry.get("mode", "?")
|
||||||
|
print(f"\rAlt: {alt:5.1f}m | Mode: {mode:10} | Cmd: vx={vx:+.2f} vy={vy:+.2f} vz={vz:+.2f}", end="")
|
||||||
|
|
||||||
|
time.sleep(1.0 / rate)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nShutdown complete.")
|
print("\n\n[INFO] Stopping...")
|
||||||
|
ardupilot.set_mode("LAND")
|
||||||
return 0
|
print("[OK] Landing mode set")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
main()
|
||||||
|
|||||||
61
scripts/run_ardupilot_controller.sh
Executable file
61
scripts/run_ardupilot_controller.sh
Executable file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# ArduPilot Controller Launcher
|
||||||
|
# =============================================================================
|
||||||
|
# Starts ArduPilot SITL and your drone controller together.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# Terminal 1: ./scripts/run_ardupilot_sim.sh runway
|
||||||
|
# Terminal 2: ./scripts/run_ardupilot_controller.sh
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
echo "=============================================="
|
||||||
|
echo " ArduPilot Controller"
|
||||||
|
echo "=============================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check dependencies
|
||||||
|
if ! command -v sim_vehicle.py &> /dev/null; then
|
||||||
|
echo "[ERROR] sim_vehicle.py not found"
|
||||||
|
echo "Add to PATH: export PATH=\$PATH:~/ardupilot/Tools/autotest"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Activate virtual environment if exists
|
||||||
|
if [ -f "$PROJECT_DIR/venv/bin/activate" ]; then
|
||||||
|
source "$PROJECT_DIR/venv/bin/activate"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start SITL in background
|
||||||
|
echo "[INFO] Starting ArduPilot SITL..."
|
||||||
|
sim_vehicle.py -v ArduCopter -f gazebo-iris --model JSON --no-mavproxy &
|
||||||
|
SITL_PID=$!
|
||||||
|
|
||||||
|
# Wait for SITL to start
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Check if SITL started
|
||||||
|
if ! kill -0 $SITL_PID 2>/dev/null; then
|
||||||
|
echo "[ERROR] SITL failed to start"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[OK] SITL started (PID: $SITL_PID)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Trap to kill SITL on exit
|
||||||
|
trap "echo ''; echo '[INFO] Stopping SITL...'; kill $SITL_PID 2>/dev/null; exit 0" INT TERM
|
||||||
|
|
||||||
|
# Run controller
|
||||||
|
echo "[INFO] Starting drone controller..."
|
||||||
|
echo ""
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
python run_ardupilot.py "$@"
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
kill $SITL_PID 2>/dev/null
|
||||||
Reference in New Issue
Block a user