1
1
from __future__ import annotations
2
2
3
+ import time
4
+
3
5
import numpy as np
4
6
import rospy
5
7
import yaml
6
8
from rosgraph import Master
7
9
from tf2_msgs .msg import TFMessage
8
10
9
11
from lsy_drone_racing .import_utils import get_ros_package_path
10
- from lsy_drone_racing .utils import euler_from_quaternion
12
+ from lsy_drone_racing .utils import euler_from_quaternion , map2pi
11
13
12
14
13
- class ViconWatcher :
15
+ class Vicon :
14
16
"""Vicon interface for the pose estimation data for the drone and any other tracked objects.
15
17
16
18
Vicon sends a stream of ROS messages containing the current pose data. We subscribe to these
17
19
messages and save the pose data for each object in dictionaries. Users can then retrieve the
18
20
latest pose data directly from these dictionaries.
19
21
"""
20
22
21
- def __init__ (self , track_names : list [str ] = []):
23
+ def __init__ (
24
+ self , track_names : list [str ] = [], auto_track_drone : bool = True , timeout : float = 0.0
25
+ ):
22
26
"""Load the crazyflies.yaml file and register the subscribers for the Vicon pose data.
23
27
24
28
Args:
25
29
track_names: The names of any additional objects besides the drone to track.
30
+ auto_track_drone: Infer the drone name and add it to the positions if True.
31
+ timeout: If greater than 0, Vicon waits for position updates of all tracked objects
32
+ before returning.
26
33
"""
27
34
assert Master ("/rosnode" ).is_online (), "ROS is not running. Please run hover.launch first!"
28
35
try :
29
36
rospy .init_node ("playback_node" )
30
37
except rospy .exceptions .ROSException :
31
38
... # ROS node is already running which is fine for us
32
- config_path = get_ros_package_path ("crazyswarm" ) / "launch/crazyflies.yaml"
33
- assert config_path .exists (), "Crazyfly config file missing!"
34
- with open (config_path , "r" ) as f :
35
- config = yaml .load (f , yaml .SafeLoader )
36
- assert len (config ["crazyflies" ]) == 1 , "Only one crazyfly allowed at a time!"
37
- self .drone_name = f"cf{ config ['crazyflies' ][0 ]['id' ]} "
38
-
39
+ self .drone_name = None
40
+ if auto_track_drone :
41
+ with open (get_ros_package_path ("crazyswarm" ) / "launch/crazyflies.yaml" , "r" ) as f :
42
+ config = yaml .load (f , yaml .SafeLoader )
43
+ assert len (config ["crazyflies" ]) == 1 , "Only one crazyfly allowed at a time!"
44
+ self .drone_name = f"cf{ config ['crazyflies' ][0 ]['id' ]} "
45
+ track_names .insert (0 , self .drone_name )
46
+ self .track_names = track_names
39
47
# Register the Vicon subscribers for the drone and any other tracked object
40
- self .pos : dict [str , np .ndarray ] = {"cf" : np .array ([])}
41
- self .rpy : dict [str , np .ndarray ] = {"cf" : np .array ([])}
42
- for track_name in track_names : # Initialize the objects' pose
43
- self .pos [track_name ], self .rpy [track_name ] = np .array ([]), np .array ([])
48
+ self .pos : dict [str , np .ndarray ] = {}
49
+ self .rpy : dict [str , np .ndarray ] = {}
50
+ self .vel : dict [str , np .ndarray ] = {}
51
+ self .ang_vel : dict [str , np .ndarray ] = {}
52
+ self .time : dict [str , float ] = {}
44
53
45
54
self .sub = rospy .Subscriber ("/tf" , TFMessage , self .save_pose )
55
+ if timeout :
56
+ tstart = time .time ()
57
+ while not self .active and time .time () - tstart < timeout :
58
+ time .sleep (0.01 )
59
+ if not self .active :
60
+ raise TimeoutError (
61
+ "Timeout while fetching initial position updates for all tracked objects."
62
+ f"Missing objects: { [k for k in self .track_names if k not in self .ang_vel ]} "
63
+ )
46
64
47
65
def save_pose (self , data : TFMessage ):
48
66
"""Save the position and orientation of all transforms.
@@ -51,10 +69,18 @@ def save_pose(self, data: TFMessage):
51
69
data: The TF message containing the objects' pose.
52
70
"""
53
71
for tf in data .transforms :
54
- name = "cf" if tf .child_frame_id == self .drone_name else tf .child_frame_id
72
+ name = tf .child_frame_id .split ("/" )[- 1 ]
73
+ if name not in self .pos :
74
+ continue
55
75
T , R = tf .transform .translation , tf .transform .rotation
56
- self .pos [name ] = np .array ([T .x , T .y , T .z ])
57
- self .rpy [name ] = np .array (euler_from_quaternion (R .x , R .y , R .z , R .w ))
76
+ pos = np .array ([T .x , T .y , T .z ])
77
+ rpy = np .array (euler_from_quaternion (R .x , R .y , R .z , R .w ))
78
+ if self .pos [name ]:
79
+ self .vel [name ] = (pos - self .pos [name ]) / (time .time () - self .time [name ])
80
+ self .ang_vel [name ] = map2pi (rpy - self .rpy [name ]) / (time .time () - self .time [name ])
81
+ self .time [name ] = time .time ()
82
+ self .pos [name ] = pos
83
+ self .rpy [name ] = rpy
58
84
59
85
def pose (self , name : str ) -> tuple [np .ndarray , np .ndarray ]:
60
86
"""Get the latest pose of a tracked object.
@@ -67,7 +93,17 @@ def pose(self, name: str) -> tuple[np.ndarray, np.ndarray]:
67
93
"""
68
94
return self .pos [name ], self .rpy [name ]
69
95
96
+ @property
97
+ def poses (self ) -> tuple [np .ndarray , np .ndarray ]:
98
+ """Get the latest poses of all objects."""
99
+ return np .stack (self .pos .values ()), np .stack (self .rpy .values ())
100
+
101
+ @property
102
+ def names (self ) -> list [str ]:
103
+ """Get a list of actively tracked names."""
104
+ return list (self .pos .keys ())
105
+
70
106
@property
71
107
def active (self ) -> bool :
72
108
"""Check if Vicon has sent data for each object."""
73
- return all (p . size > 0 for p in self .pos . values () )
109
+ return all ([ name in self . ang_vel for name in self .track_names ] )
0 commit comments