Skip to content

Commit 4e64146

Browse files
authored
Added external pose input message and pose relay script. (#393)
# New Features - Added new `ExternalPoseInput` message definition. - Added `pose_relay.py` example script for transmitting pose from a source device to a target device.
2 parents 3363f77 + 2bcd889 commit 4e64146

7 files changed

Lines changed: 763 additions & 1 deletion

File tree

python/examples/pose_relay.py

Lines changed: 421 additions & 0 deletions
Large diffs are not rendered by default.

python/fusion_engine_client/analysis/attitude.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,133 @@ def get_enu_rotation_matrix(latitude, longitude, deg=False):
4949
C_enu_e = C_ned_e[(1, 0, 2), :]
5050
C_enu_e[2, :] = -C_enu_e[2, :]
5151
return C_enu_e
52+
53+
54+
def axis_rotation_dcm(angle, axis, deg=False):
55+
"""!
56+
@brief Create a direction cosine matrix rotating a coordinate frame around the specified axis.
57+
58+
The resulting DCM represents a passive transformation of a vector expressed in the original coordinate frame (@f$
59+
\alpha @f$) to one expressed in the rotated coordinate frame (@f$ \beta @f$).
60+
61+
@param angle The desired rotation angle.
62+
@param axis The axis to be rotated around (@c 'x', @c 'y', or @c 'z').
63+
@param deg If @c True, interpret the input angles as degrees. Otherwise, interpret as radians.
64+
65+
@return The 3x3 rotation matrix @f$ C_\alpha^\beta @f$.
66+
"""
67+
if deg:
68+
angle = np.deg2rad(angle)
69+
70+
cos = np.cos(angle)
71+
sin = np.sin(angle)
72+
73+
if axis == 'x':
74+
return np.array(((1, 0, 0),
75+
(0, cos, sin),
76+
(0, -sin, cos)))
77+
elif axis == 'y':
78+
return np.array(((cos, 0, -sin),
79+
( 0, 1, 0),
80+
(sin, 0, cos)))
81+
elif axis == 'z':
82+
return np.array((( cos, sin, 0),
83+
(-sin, cos, 0),
84+
( 0, 0, 1)))
85+
else:
86+
raise RuntimeError('Invalid axis specification.')
87+
88+
89+
def euler_angles_to_dcm(x, y=None, z=None, order='321', deg=False):
90+
"""!
91+
@brief Convert an Euler (Tait-Bryan) angle attitude (in radians) into a direction cosine matrix.
92+
93+
The input angles represent the rotation from coordinate frame @f$ \alpha @f$ to coordinate frame @f$ \beta @f$ (@f$
94+
C_\alpha^\beta @f$. This is frequently the rotation from the navigation frame (local level frame) to the body frame,
95+
@f$ C_n^b @f$.
96+
97+
@note
98+
This DCM represents a passive transformation of a vector from one coordinate frame to another, as opposed to an
99+
active rotation of a vector within a single coordinate frame.
100+
101+
This function supports any intrinsic rotation order through the @c order argument. The most common convention is a
102+
three-two-one rotation (@c '321'), in which the coordinate frame is first rotated around the Z-axis (i.e., the third
103+
axis) by the angle @c z, then the second axis in the resulting intermediate coordinate frame (the Y'-axis), and
104+
finally the third axis in the last intermediate coordinate frame (the X''-axis). 3-2-1 rotation angles are commonly
105+
referred to as yaw, pitch, and roll for the Z, Y, and X rotations respectively.
106+
107+
You may specify all three angles separately using the @c x, @c y, and @c z parameters, or provide a Numpy array
108+
containing all three angles.
109+
110+
@param x The angle to rotate around the X-axis (i.e., axis 1). May be a Numpy array containing the rotation angles
111+
for all three axes in the order X, Y, Z.
112+
@param y The angle to rotate around the Y-axis (i.e., axis 2).
113+
@param z The angle to rotate around the Z-axis (i.e., axis 3).
114+
@param order A string specifying the rotation order to be applied.
115+
@param deg If @c True, interpret the input angles as degrees. Otherwise, interpret as radians.
116+
117+
@return The 3x3 rotation matrix rotating from the @f$ \alpha @f$ frame to the @f$ \beta @f$ frame (@f$
118+
C_\alpha^\beta @f$).
119+
"""
120+
if isinstance(x, np.ndarray):
121+
if y is not None or z is not None:
122+
raise RuntimeError('Cannot specify Y/Z angles when using a Numpy array.')
123+
else:
124+
angles = x
125+
else:
126+
if y is None or z is None:
127+
raise RuntimeError('You must specify Y and Z angles.')
128+
else:
129+
angles = np.array((x, y, z))
130+
131+
if deg:
132+
angles = np.deg2rad(angles)
133+
134+
# 3-2-1 order written explicitly for efficiency since it's the most common order. Taken from Groves section 2.2.2.
135+
if order == '321':
136+
sin_phi, sin_theta, sin_psi = np.sin(angles)
137+
cos_phi, cos_theta, cos_psi = np.cos(angles)
138+
139+
return np.array((
140+
(cos_theta * cos_psi, cos_theta * sin_psi, -sin_theta),
141+
(-cos_phi * sin_psi + sin_phi * sin_theta * cos_psi,
142+
cos_phi * cos_psi + sin_phi * sin_theta * sin_psi,
143+
sin_phi * cos_theta),
144+
(sin_phi * sin_psi + cos_phi * sin_theta * cos_psi,
145+
-sin_phi * cos_psi + cos_phi * sin_theta * sin_psi,
146+
cos_phi * cos_theta)
147+
))
148+
else:
149+
dcm = {}
150+
dcm['1'] = axis_rotation_dcm(angles[0], axis='x')
151+
dcm['2'] = axis_rotation_dcm(angles[1], axis='y')
152+
dcm['3'] = axis_rotation_dcm(angles[2], axis='z')
153+
154+
return dcm[order[2]].dot(dcm[order[1]]).dot(dcm[order[0]])
155+
156+
157+
def dcm_to_euler_angles(dcm, order='321', deg=False):
158+
"""!
159+
@brief Convert a direction cosine matrix into an Euler (Tait-Bryan) angle attitude representation.
160+
161+
Convert a DCM representing the rotation from coordinate frame @f$ \alpha @f$ to coordinate frame @f$ \beta @f$ into
162+
corresponding Euler angles with the specified rotation order.
163+
164+
@param dcm The 3x3 rotation matrix.
165+
@param order The desired Euler angle order.
166+
@param deg If @c True, return angles in degrees. Otherwise, return radians.
167+
168+
@return A Numpy array containing the X, Y, and Z rotation angles.
169+
"""
170+
if order == '321':
171+
x = np.arctan2(dcm[1, 2], dcm[2, 2])
172+
y = -np.arcsin(dcm[0, 2])
173+
z = np.arctan2(dcm[0, 1], dcm[0, 0])
174+
else:
175+
raise RuntimeError('Unsupported rotation order.')
176+
177+
result = np.array((x, y, z))
178+
if deg:
179+
result = np.rad2deg(result)
180+
181+
return result

python/fusion_engine_client/messages/defs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ class MessageType(IntEnum):
106106
IMU_INPUT = 11004
107107
GNSS_ATTITUDE_OUTPUT = 11005
108108
RAW_GNSS_ATTITUDE_OUTPUT = 11006
109+
EXTERNAL_POSE_INPUT = 11007
109110

110111
# Vehicle measurement messages.
111112
DEPRECATED_WHEEL_SPEED_MEASUREMENT = 11101

python/fusion_engine_client/messages/measurements.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,6 +1417,130 @@ def to_numpy(cls, messages: Sequence['RawGNSSAttitudeOutput']):
14171417
result.update(MeasurementDetails.to_numpy([m.details for m in messages]))
14181418
return result
14191419

1420+
################################################################################
1421+
# External Pose Measurements
1422+
################################################################################
1423+
1424+
1425+
class ExternalPoseInput(MessagePayload):
1426+
"""! @brief External pose measurement input.
1427+
1428+
Position is expressed in the ECEF frame. Orientation is the yaw, pitch, roll
1429+
(YPR) angles in the local ENU frame. Velocity is expressed in the local ENU
1430+
frame. Any elements that are not available should be set to `nan`. Standard
1431+
deviation fields are specified in the same units.
1432+
"""
1433+
MESSAGE_TYPE = MessageType.EXTERNAL_POSE_INPUT
1434+
MESSAGE_VERSION = 0
1435+
1436+
FLAG_RESET_POSITION_DATA = 0x1
1437+
1438+
_STRUCT = struct.Struct('<B3xI3d3f3f3f3f3f')
1439+
1440+
def __init__(self):
1441+
self.details = MeasurementDetails()
1442+
1443+
self.solution_type = SolutionType.Invalid
1444+
1445+
self.flags = 0
1446+
1447+
self.position_ecef_m = np.full((3,), np.nan)
1448+
self.position_std_ecef_m = np.full((3,), np.nan)
1449+
1450+
self.ypr_deg = np.full((3,), np.nan)
1451+
self.ypr_std_deg = np.full((3,), np.nan)
1452+
1453+
self.velocity_enu_mps = np.full((3,), np.nan)
1454+
self.velocity_std_enu_mps = np.full((3,), np.nan)
1455+
1456+
def pack(self, buffer: bytes = None, offset: int = 0,
1457+
return_buffer: bool = True) -> (bytes, int):
1458+
if buffer is None:
1459+
buffer = bytearray(self.calcsize())
1460+
offset = 0
1461+
1462+
initial_offset = offset
1463+
1464+
offset += self.details.pack(buffer, offset, return_buffer=False)
1465+
1466+
offset += self.pack_values(
1467+
self._STRUCT, buffer, offset,
1468+
int(self.solution_type),
1469+
self.flags,
1470+
self.position_ecef_m[0], self.position_ecef_m[1], self.position_ecef_m[2],
1471+
self.position_std_ecef_m[0], self.position_std_ecef_m[1], self.position_std_ecef_m[2],
1472+
self.ypr_deg[0], self.ypr_deg[1], self.ypr_deg[2],
1473+
self.ypr_std_deg[0], self.ypr_std_deg[1], self.ypr_std_deg[2],
1474+
self.velocity_enu_mps[0], self.velocity_enu_mps[1], self.velocity_enu_mps[2],
1475+
self.velocity_std_enu_mps[0], self.velocity_std_enu_mps[1], self.velocity_std_enu_mps[2])
1476+
if return_buffer:
1477+
return buffer
1478+
else:
1479+
return offset - initial_offset
1480+
1481+
def unpack(self, buffer: bytes, offset: int = 0,
1482+
message_version: int = MessagePayload._UNSPECIFIED_VERSION) -> int:
1483+
initial_offset = offset
1484+
1485+
offset += self.details.unpack(buffer, offset)
1486+
1487+
(solution_type_int,
1488+
self.flags,
1489+
self.position_ecef_m[0], self.position_ecef_m[1], self.position_ecef_m[2],
1490+
self.position_std_ecef_m[0], self.position_std_ecef_m[1], self.position_std_ecef_m[2],
1491+
self.ypr_deg[0], self.ypr_deg[1], self.ypr_deg[2],
1492+
self.ypr_std_deg[0], self.ypr_std_deg[1], self.ypr_std_deg[2],
1493+
self.velocity_enu_mps[0], self.velocity_enu_mps[1], self.velocity_enu_mps[2],
1494+
self.velocity_std_enu_mps[0], self.velocity_std_enu_mps[1], self.velocity_std_enu_mps[2]) = \
1495+
self._STRUCT.unpack_from(buffer, offset)
1496+
offset += self._STRUCT.size
1497+
1498+
self.solution_type = SolutionType(solution_type_int)
1499+
1500+
return offset - initial_offset
1501+
1502+
@classmethod
1503+
def calcsize(cls) -> int:
1504+
return MeasurementDetails.calcsize() + cls._STRUCT.size
1505+
1506+
@classmethod
1507+
def to_numpy(cls, messages: Sequence['ExternalPoseInput']):
1508+
result = {
1509+
'solution_type': np.array([int(m.solution_type) for m in messages], dtype=int),
1510+
'flags': np.array([m.flags for m in messages], dtype=np.uint32),
1511+
'position_ecef_m': np.array([m.position_ecef_m for m in messages]).T,
1512+
'position_std_ecef_m': np.array([m.position_std_ecef_m for m in messages]).T,
1513+
'ypr_deg': np.array([m.ypr_deg for m in messages]).T,
1514+
'ypr_std_deg': np.array([m.ypr_std_deg for m in messages]).T,
1515+
'velocity_enu_mps': np.array([m.velocity_enu_mps for m in messages]).T,
1516+
'velocity_std_enu_mps': np.array([m.velocity_std_enu_mps for m in messages]).T,
1517+
}
1518+
result.update(MeasurementDetails.to_numpy([m.details for m in messages]))
1519+
return result
1520+
1521+
def __getattr__(self, item):
1522+
if item == 'p1_time':
1523+
return self.details.p1_time
1524+
else:
1525+
return super().__getattr__(item)
1526+
1527+
def __repr__(self):
1528+
result = super().__repr__()[:-1]
1529+
result += f', solution_type={self.solution_type}, position_ecef={self.position_ecef_m}, ' \
1530+
f'ypr_deg={self.ypr_deg}, velocity_enu_mps={self.velocity_enu_mps}]'
1531+
return result
1532+
1533+
def __str__(self):
1534+
string = 'External Pose Measurement @ %s\n' % str(self.details.p1_time)
1535+
string += ' Solution type: %s\n' % self.solution_type.name
1536+
string += ' Position (ECEF): %.2f, %.2f, %.2f (m, m, m)\n' % tuple(self.position_ecef_m)
1537+
string += ' Position std (ECEF): %.2f, %.2f, %.2f (m, m, m)\n' % tuple(self.position_std_ecef_m)
1538+
string += ' YPR: %.2f, %.2f, %.2f (deg, deg, deg)\n' % tuple(self.ypr_deg)
1539+
string += ' YPR std: %.2f, %.2f, %.2f (deg, deg, deg)\n' % tuple(self.ypr_std_deg)
1540+
string += ' Velocity (ENU): %.2f, %.2f, %.2f (m/s, m/s, m/s)\n' % tuple(self.velocity_enu_mps)
1541+
string += ' Velocity std (ENU): %.2f, %.2f, %.2f (m/s, m/s, m/s)' % tuple(self.velocity_std_enu_mps)
1542+
return string
1543+
14201544
################################################################################
14211545
# Binary Sensor Data Definitions
14221546
################################################################################

python/tests/test_message_defs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def test_find_message_types():
9898
# Use wildcards to match multiple types.
9999
assert MessagePayload.find_matching_message_types(['pose*']) == {MessageType.POSE, MessageType.POSE_AUX}
100100
assert MessagePayload.find_matching_message_types(['*pose*']) == {MessageType.POSE, MessageType.POSE_AUX,
101-
MessageType.ROS_POSE}
101+
MessageType.EXTERNAL_POSE_INPUT, MessageType.ROS_POSE}
102102
assert MessagePayload.find_matching_message_types(['pose*', 'gnssi*']) == {MessageType.POSE, MessageType.POSE_AUX,
103103
MessageType.GNSS_INFO}
104104
assert MessagePayload.find_matching_message_types(['pose*,gnssi*']) == {MessageType.POSE, MessageType.POSE_AUX,

src/point_one/fusion_engine/messages/defs.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ enum class MessageType : uint16_t {
5454
IMU_INPUT = 11004, ///< @ref IMUInput
5555
GNSS_ATTITUDE_OUTPUT = 11005, ///< @ref GNSSAttitudeOutput
5656
RAW_GNSS_ATTITUDE_OUTPUT = 11006, ///< @ref RawGNSSAttitudeOutput
57+
EXTERNAL_POSE_INPUT = 11007, ///< @ref ExternalPoseInput
5758

5859
// Vehicle measurement messages.
5960
DEPRECATED_WHEEL_SPEED_MEASUREMENT =
@@ -176,6 +177,9 @@ P1_CONSTEXPR_FUNC const char* to_string(MessageType type) {
176177
case MessageType::RAW_GNSS_ATTITUDE_OUTPUT:
177178
return "Raw GNSS Attitude Output";
178179

180+
case MessageType::EXTERNAL_POSE_INPUT:
181+
return "External Pose Input";
182+
179183
case MessageType::DEPRECATED_WHEEL_SPEED_MEASUREMENT:
180184
return "Wheel Speed Measurement";
181185

@@ -353,6 +357,7 @@ P1_CONSTEXPR_FUNC bool IsCommand(MessageType message_type) {
353357
case MessageType::IMU_INPUT:
354358
case MessageType::GNSS_ATTITUDE_OUTPUT:
355359
case MessageType::RAW_GNSS_ATTITUDE_OUTPUT:
360+
case MessageType::EXTERNAL_POSE_INPUT:
356361
case MessageType::DEPRECATED_WHEEL_SPEED_MEASUREMENT:
357362
case MessageType::DEPRECATED_VEHICLE_SPEED_MEASUREMENT:
358363
case MessageType::WHEEL_TICK_INPUT:

src/point_one/fusion_engine/messages/measurements.h

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,6 +1264,87 @@ struct P1_ALIGNAS(4) RawGNSSAttitudeOutput : public MessagePayload {
12641264
float position_std_enu_m[3] = {NAN, NAN, NAN};
12651265
};
12661266

1267+
////////////////////////////////////////////////////////////////////////////////
1268+
// External Pose Measurements
1269+
////////////////////////////////////////////////////////////////////////////////
1270+
1271+
/**
1272+
* @brief External pose measurement input (@ref
1273+
* MessageType::EXTERNAL_POSE_INPUT, version 1.0).
1274+
* @ingroup measurement_messages
1275+
*
1276+
* This message is an input to the device containing pose and velocity
1277+
* measurements generated by an external source (for example, another Point One
1278+
* device or a vision system).
1279+
*
1280+
* Position is expressed in the ECEF frame, and velocity is expressed in the
1281+
* local ENU frame. @ref position_ecef_m should correspond to the output lever
1282+
* arm point configured on the receiving device (see @ref
1283+
* ConfigType::OUTPUT_LEVER_ARM), so that the position matches the point the
1284+
* device will report in its own @ref PoseMessage after initialization.
1285+
*
1286+
* Orientation is specified as yaw, pitch, roll (YPR) angles in the local ENU
1287+
* frame, following the same intrinsic Euler-321 convention as @ref
1288+
* PoseMessage::ypr_deg.
1289+
*
1290+
* Any elements that are not available should be set to `NAN`. Standard
1291+
* deviation fields are specified in the same units as the corresponding
1292+
* measurement.
1293+
*/
1294+
struct P1_ALIGNAS(4) ExternalPoseInput : public MessagePayload {
1295+
static constexpr MessageType MESSAGE_TYPE = MessageType::EXTERNAL_POSE_INPUT;
1296+
static constexpr uint8_t MESSAGE_VERSION = 0;
1297+
1298+
/** Equivalent to sending a reset message. See @ref ResetRequest. */
1299+
static constexpr uint32_t FLAG_RESET_POSITION_DATA = 0x01;
1300+
1301+
/** Measurement timestamp and additional information. */
1302+
MeasurementDetails details;
1303+
1304+
/** The pose solution type provided by the external source. */
1305+
SolutionType solution_type = SolutionType::Invalid;
1306+
1307+
uint8_t reserved[3] = {0};
1308+
1309+
/** A bitmask of flags associated with the measurement. */
1310+
uint32_t flags = 0;
1311+
1312+
/**
1313+
* An estimate of the device's output position (in meters), resolved in the
1314+
* ECEF frame.
1315+
*/
1316+
double position_ecef_m[3] = {NAN, NAN, NAN};
1317+
1318+
/**
1319+
* An estimate of the device's output position standard deviation (in meters),
1320+
* resolved in the ECEF frame.
1321+
*/
1322+
float position_std_ecef_m[3] = {NAN, NAN, NAN};
1323+
1324+
/** An estimate of the device's output orientation (in degrees), resolved in
1325+
* the local ENU tangent plane. See @ref PoseMessage::ypr_deg for a complete
1326+
* rotation definition.
1327+
*/
1328+
float ypr_deg[3] = {NAN, NAN, NAN};
1329+
1330+
/** An estimate of the device's output orientation standard deviation (in
1331+
* degrees).
1332+
*/
1333+
float ypr_std_deg[3] = {NAN, NAN, NAN};
1334+
1335+
/**
1336+
* An estimate of the device's output velocity (in m/s), resolved in the local
1337+
* ENU tangent plane.
1338+
*/
1339+
float velocity_enu_mps[3] = {NAN, NAN, NAN};
1340+
1341+
/**
1342+
* An estimate of the device's output velocity standard deviation (in m/s),
1343+
* resolved in the local ENU tangent plane.
1344+
*/
1345+
float velocity_std_enu_mps[3] = {NAN, NAN, NAN};
1346+
};
1347+
12671348
////////////////////////////////////////////////////////////////////////////////
12681349
// Binary Sensor Data Definitions
12691350
////////////////////////////////////////////////////////////////////////////////

0 commit comments

Comments
 (0)