diff --git a/python/fusion_engine_client/analysis/analyzer.py b/python/fusion_engine_client/analysis/analyzer.py index 3ef47a18..0d084c75 100755 --- a/python/fusion_engine_client/analysis/analyzer.py +++ b/python/fusion_engine_client/analysis/analyzer.py @@ -1943,24 +1943,24 @@ def _plot_imu_data(self, message_cls, filename, figure_title): self._add_figure(name=filename, figure=figure, title=figure_title) - def plot_heading_measurements(self): + def plot_gnss_attitude_measurements(self): """! - @brief Generate time series plots for heading (degrees) and baseline distance (meters) data. + @brief Generate time series plots for GNSS attitude (degrees) and baseline distance (meters) data. """ if self.output_dir is None: return - # Read the heading measurement data. - result = self.reader.read(message_types=[RawHeadingOutput, HeadingOutput], **self.params) - raw_heading_data = result[RawHeadingOutput.MESSAGE_TYPE] - heading_data = result[HeadingOutput.MESSAGE_TYPE] + # Read the attitude measurement data. + result = self.reader.read(message_types=[RawGNSSAttitudeOutput, GNSSAttitudeOutput], **self.params) + raw_heading_data = result[RawGNSSAttitudeOutput.MESSAGE_TYPE] + heading_data = result[GNSSAttitudeOutput.MESSAGE_TYPE] if (len(heading_data.p1_time) == 0) and (len(raw_heading_data.p1_time) == 0): - self.logger.info('No heading measurement data available. Skipping plot.') + self.logger.info('No GNSS attitude measurement data available. Skipping plot.') return - # Note that we read the pose data after heading, that way we don't bother reading pose data from disk if there's - # no heading data in the log. + # Note that we read the pose data after attitude, that way we don't bother reading pose data from disk if + # there's no heading data in the log. result = self.reader.read(message_types=[PoseMessage], source_ids=self.default_source_id, **self.params) primary_pose_data = result[PoseMessage.MESSAGE_TYPE] @@ -1987,7 +1987,6 @@ def plot_heading_measurements(self): fig.update_layout(title='Heading Plots', legend_traceorder='normal', modebar_add=['v1hovermode']) - # Display the navigation engine's heading estimate, if available, for comparison with the heading sensor # measurement. if primary_pose_data is not None: @@ -2003,7 +2002,7 @@ def plot_heading_measurements(self): customdata=primary_pose_data.p1_time, mode='lines', line={'color': 'yellow'}, - name='Primary Device Heading Estimate', + name='Navigation Engine Heading Estimate', hovertemplate='Time: %{x:.3f} sec (%{customdata:.3f} sec)' '
Heading: %{y:.2f} deg' ), @@ -2013,10 +2012,11 @@ def plot_heading_measurements(self): # Corrected heading plot if len(heading_data.p1_time) > 0: heading_time = heading_data.p1_time - float(self.t0) + heading_deg = 90.0 - heading_data.ypr_deg[0, :] fig.add_trace( go.Scatter( x=heading_time, - y=heading_data.heading_true_north_deg, + y=heading_deg, customdata=heading_data.p1_time, mode='markers', marker={'size': 2, "color": "green"}, @@ -2028,9 +2028,26 @@ def plot_heading_measurements(self): row=1, col=1 ) + fig.add_trace( + go.Scatter( + x=heading_time, + y=heading_data.baseline_distance_m, + customdata=heading_data.p1_time, + marker={'size': 2, "color": "green"}, + hovertemplate='Time: %{x:.3f} sec (%{customdata:.3f} sec)' + '
Baseline: %{y:.2f} m', + name='Baseline' + ), + row=2, col=1 + ) + # Uncorrected heading plot if len(raw_heading_data.p1_time) > 0: raw_heading_time = raw_heading_data.p1_time - float(self.t0) + raw_heading_deg = np.degrees(np.arctan2(raw_heading_data.relative_position_enu_m[1, :], + raw_heading_data.relative_position_enu_m[0, :])) + raw_baseline_distance_m = np.linalg.norm(raw_heading_data.relative_position_enu_m, axis=0) + # Compute heading uncertainty envelop. denom = raw_heading_data.relative_position_enu_m[0]**2 + raw_heading_data.relative_position_enu_m[1]**2 dh_e = raw_heading_data.relative_position_enu_m[0] / denom @@ -2042,13 +2059,13 @@ def plot_heading_measurements(self): ) envelope = np.arctan( - (2 * heading_std / raw_heading_data.baseline_distance_m) + (2 * heading_std / raw_baseline_distance_m) ) envelope *= 180. / np.pi fig.add_trace( go.Scatter( x=raw_heading_time, - y=raw_heading_data.heading_true_north_deg, + y=raw_heading_deg, customdata=raw_heading_data.p1_time, mode='markers', marker={'size': 2, "color": "red"}, @@ -2059,12 +2076,12 @@ def plot_heading_measurements(self): ), row=1, col=1 ) - idx = ~np.isnan(raw_heading_data.heading_true_north_deg) + idx = ~np.isnan(raw_heading_deg) fig.add_trace( go.Scatter( x=raw_heading_time[idx], - y=raw_heading_data.heading_true_north_deg[idx] + envelope[idx], + y=raw_heading_deg[idx] + envelope[idx], mode='lines', marker={'size': 2, "color": "red"}, line=dict(width=0), @@ -2078,7 +2095,7 @@ def plot_heading_measurements(self): fig.add_trace( go.Scatter( x=raw_heading_time[idx], - y=raw_heading_data.heading_true_north_deg[idx] - envelope[idx], + y=raw_heading_deg[idx] - envelope[idx], mode='lines', marker={'size': 2, "color": "red"}, line=dict(width=0), @@ -2131,7 +2148,7 @@ def plot_heading_measurements(self): fig.add_trace( go.Scatter( x=raw_heading_time, - y=raw_heading_data.baseline_distance_m, + y=raw_baseline_distance_m, customdata=raw_heading_data.p1_time, marker={'size': 2, "color": "red"}, hovertemplate='Time: %{x:.3f} sec (%{customdata:.3f} sec)' @@ -2188,7 +2205,7 @@ def plot_heading_measurements(self): row=3, col=1 ) - self._add_figure(name='heading_measurement', figure=fig, title='Measurements: Heading') + self._add_figure(name='gnss_attitude_measurement', figure=fig, title='Measurements: GNSS Attitude') def plot_system_status_profiling(self): """! @@ -3002,9 +3019,9 @@ def main(args=None): analyzer.plot_gnss_corrections_status() analyzer.plot_dop() - # By default, we always plot heading measurements (i.e., output from a secondary heading device like an + # By default, we always plot attitude measurements (i.e., output from a secondary GNSS attitude sensor like an # LG69T-AH), separate from other sensor measurements controlled by --measurements. - analyzer.plot_heading_measurements() + analyzer.plot_gnss_attitude_measurements() if truth_lla_deg is not None: analyzer.plot_stationary_position_error(truth_lla_deg) diff --git a/python/fusion_engine_client/messages/defs.py b/python/fusion_engine_client/messages/defs.py index 8fcfb0c2..84fab7dc 100644 --- a/python/fusion_engine_client/messages/defs.py +++ b/python/fusion_engine_client/messages/defs.py @@ -99,10 +99,12 @@ class MessageType(IntEnum): # Sensor measurement messages. IMU_OUTPUT = 11000 - RAW_HEADING_OUTPUT = 11001 + DEPRECATED_RAW_HEADING_OUTPUT = 11001 RAW_IMU_OUTPUT = 11002 - HEADING_OUTPUT = 11003 + DEPRECATED_HEADING_OUTPUT = 11003 IMU_INPUT = 11004 + GNSS_ATTITUDE_OUTPUT = 11005 + RAW_GNSS_ATTITUDE_OUTPUT = 11006 # Vehicle measurement messages. DEPRECATED_WHEEL_SPEED_MEASUREMENT = 11101 @@ -587,6 +589,13 @@ def get_p1_time(self) -> Timestamp: else: return getattr(self, 'p1_time', None) + def get_gps_time(self) -> Timestamp: + measurement_details = getattr(self, 'details', None) + if isinstance(measurement_details, MeasurementDetails): + if measurement_details.measurement_time_source == SystemTimeSource.GPS_TIME: + return measurement_details.measurement_time + return getattr(self, 'gps_time', None) + def get_system_time_ns(self) -> float: measurement_details = getattr(self, 'details', None) if isinstance(measurement_details, MeasurementDetails): @@ -785,3 +794,11 @@ def PackedDataToBuffer(packed_data: bytes, buffer: Optional[bytes] = None, offse return buffer else: return len(packed_data) + + +def yaw_to_heading(yaw_deg: Union[float, np.ndarray]): + return 90.0 - yaw_deg + + +def heading_to_yaw(heading_deg: Union[float, np.ndarray]): + return 90.0 - heading_deg diff --git a/python/fusion_engine_client/messages/measurement_details.py b/python/fusion_engine_client/messages/measurement_details.py index ac17b288..08280f9e 100644 --- a/python/fusion_engine_client/messages/measurement_details.py +++ b/python/fusion_engine_client/messages/measurement_details.py @@ -131,6 +131,12 @@ def to_numpy(cls, messages): 'p1_time': p1_time, } + idx = time_source == SystemTimeSource.GPS_TIME + if np.any(idx): + gps_time = np.full_like(time_source, np.nan) + gps_time[idx] = measurement_time[idx] + result['gps_time'] = gps_time + idx = time_source == SystemTimeSource.TIMESTAMPED_ON_RECEPTION if np.any(idx): system_time = np.full_like(time_source, np.nan) diff --git a/python/fusion_engine_client/messages/measurements.py b/python/fusion_engine_client/messages/measurements.py index 2847e8f5..cc6382d5 100644 --- a/python/fusion_engine_client/messages/measurements.py +++ b/python/fusion_engine_client/messages/measurements.py @@ -1,3 +1,4 @@ +import math import struct from typing import Sequence @@ -1165,18 +1166,18 @@ def to_numpy(cls, messages): return result ################################################################################ -# Heading Sensor Definitions +# GNSS Heading Sensor Definitions ################################################################################ -class HeadingOutput(MessagePayload): +class GNSSAttitudeOutput(MessagePayload): """! - @brief Heading sensor measurement output with heading bias corrections applied. + @brief Multi-antenna GNSS attitude sensor measurement output with offset corrections applied. """ - MESSAGE_TYPE = MessageType.HEADING_OUTPUT + MESSAGE_TYPE = MessageType.GNSS_ATTITUDE_OUTPUT MESSAGE_VERSION = 0 - _STRUCT = struct.Struct(' (bytes, int): if buffer is None: @@ -1216,7 +1222,11 @@ def pack(self, buffer: bytes = None, offset: int = 0, return_buffer: bool = True self.ypr_deg[0], self.ypr_deg[1], self.ypr_deg[2], - self.heading_true_north_deg) + self.ypr_std_deg[0], + self.ypr_std_deg[1], + self.ypr_std_deg[2], + self.baseline_distance_m, + self.baseline_distance_std_m) offset += self._STRUCT.size if return_buffer: @@ -1234,7 +1244,11 @@ def unpack(self, buffer: bytes, offset: int = 0, message_version: int = MessageP self.ypr_deg[0], self.ypr_deg[1], self.ypr_deg[2], - self.heading_true_north_deg) = \ + self.ypr_std_deg[0], + self.ypr_std_deg[1], + self.ypr_std_deg[2], + self.baseline_distance_m, + self.baseline_distance_std_m) = \ self._STRUCT.unpack_from(buffer, offset) offset += self._STRUCT.size @@ -1244,46 +1258,61 @@ def unpack(self, buffer: bytes, offset: int = 0, message_version: int = MessageP def __repr__(self): result = super().__repr__()[:-1] - result += f', solution_type={self.solution_type}, heading={self.heading_true_north_deg:.1f} deg]' + ypr_str = '(%.1f, %.1f, %.1f)' % tuple(self.ypr_deg) + result += f', solution_type={self.solution_type}, ypr={ypr_str} deg, ' \ + f'baseline={self.baseline_distance_m} m]' return result def __str__(self): + gps_time = self.get_gps_time() + if gps_time is not None: + gps_str = f'{str(gps_time).replace("GPS: ", "")}' + utc_str = f'{datetime_to_string(gps_time.as_utc())}' + else: + gps_str = 'None' + utc_str = 'None' return f"""\ -Heading Output @ {str(self.details.p1_time)} +GNSS Attitude Output @ {str(self.details.p1_time)} + GPS time: {gps_str} + UTC time: {utc_str} Solution Type: {self.solution_type} - YPR (ENU) (deg): {self.ypr_deg[0]:.2f}, {self.ypr_deg[1]:.2f}, {self.ypr_deg[2]:.2f} - Heading (deg): {self.heading_true_north_deg:.2f}""" + YPR (deg): {self.ypr_deg[0]:.2f}, {self.ypr_deg[1]:.2f}, {self.ypr_deg[2]:.2f} + YPR std (deg): {self.ypr_std_deg[0]:.2f}, {self.ypr_std_deg[1]:.2f}, {self.ypr_std_deg[2]:.2f} + Baseline distance (m): {self.baseline_distance_m:.2f} + Baseline std (m): {self.baseline_distance_std_m:.2f}""" @classmethod def calcsize(cls) -> int: return cls._STRUCT.size + MeasurementDetails.calcsize() @classmethod - def to_numpy(cls, messages: Sequence['HeadingOutput']): + def to_numpy(cls, messages: Sequence['GNSSAttitudeOutput']): result = { 'solution_type': np.array([int(m.solution_type) for m in messages], dtype=int), 'flags': np.array([int(m.flags) for m in messages], dtype=np.uint32), 'ypr_deg': np.array([m.ypr_deg for m in messages]).T, - 'heading_true_north_deg': np.array([m.heading_true_north_deg for m in messages], dtype=float), + 'ypr_std_deg': np.array([m.ypr_std_deg for m in messages]).T, + 'baseline_distance_m': np.array([float(m.baseline_distance_m) for m in messages]), + 'baseline_distance_std_m': np.array([float(m.baseline_distance_std_m) for m in messages]), } result.update(MeasurementDetails.to_numpy([m.details for m in messages])) return result -class RawHeadingOutput(MessagePayload): +class RawGNSSAttitudeOutput(MessagePayload): """! - @brief Raw (uncorrected) heading sensor measurement output. + @brief Raw (uncorrected) GNSS attitude sensor measurement output. """ - MESSAGE_TYPE = MessageType.RAW_HEADING_OUTPUT + MESSAGE_TYPE = MessageType.RAW_GNSS_ATTITUDE_OUTPUT MESSAGE_VERSION = 0 - _STRUCT = struct.Struct(' (bytes, int): if buffer is None: buffer = bytearray(self.calcsize()) @@ -1329,8 +1358,8 @@ def pack(self, buffer: bytes = None, offset: int = 0, return_buffer: bool = True self.position_std_enu_m[0], self.position_std_enu_m[1], self.position_std_enu_m[2], - self.heading_true_north_deg, - self.baseline_distance_m) + self.baseline_distance_m, + self.baseline_distance_std_m) offset += self._STRUCT.size if return_buffer: @@ -1350,9 +1379,7 @@ def unpack(self, buffer: bytes, offset: int = 0, message_version: int = MessageP self.relative_position_enu_m[2], self.position_std_enu_m[0], self.position_std_enu_m[1], - self.position_std_enu_m[2], - self.heading_true_north_deg, - self.baseline_distance_m) = self._STRUCT.unpack_from(buffer, offset) + self.position_std_enu_m[2]) = self._STRUCT.unpack_from(buffer, offset) offset += self._STRUCT.size self.solution_type = SolutionType(solution_type_int) @@ -1361,36 +1388,47 @@ def unpack(self, buffer: bytes, offset: int = 0, message_version: int = MessageP def __repr__(self): result = super().__repr__()[:-1] - result += f', solution_type={self.solution_type}, heading={self.heading_true_north_deg:.1f} deg, ' \ - f'baseline={self.baseline_distance_m} m]' + enu_str = '(%.2f, %.2f, %.3f)' % tuple(self.relative_position_enu_m) + heading_deg = self.get_heading_deg() + result += f', solution_type={self.solution_type}, enu={enu_str} m, heading={heading_deg:.1f} deg]' return result def __str__(self): + gps_time = self.get_gps_time() + if gps_time is not None: + gps_str = f'{str(gps_time).replace("GPS: ", "")}' + utc_str = f'{datetime_to_string(gps_time.as_utc())}' + else: + gps_str = 'None' + utc_str = 'None' return f"""\ -Raw Heading Output @ {str(self.details.p1_time)} +Raw GNSS Attitude Output @ {str(self.details.p1_time)} + GPS time: {gps_str} + UTC time: {utc_str} Solution Type: {self.solution_type} Relative position (ENU) (m): {self.relative_position_enu_m[0]:.2f}, {self.relative_position_enu_m[1]:.2f}, {self.relative_position_enu_m[2]:.2f} Position std (ENU) (m): {self.position_std_enu_m[0]:.2f}, {self.position_std_enu_m[1]:.2f}, {self.position_std_enu_m[2]:.2f} - Heading (deg): {self.heading_true_north_deg:.2f} - Baseline distance (m): {self.baseline_distance_m:.2f}""" + Heading (deg): {self.get_heading_deg():.2f}""" @classmethod def calcsize(cls) -> int: return cls._STRUCT.size + MeasurementDetails.calcsize() @classmethod - def to_numpy(cls, messages: Sequence['RawHeadingOutput']): + def to_numpy(cls, messages: Sequence['RawGNSSAttitudeOutput']): result = { 'solution_type': np.array([int(m.solution_type) for m in messages], dtype=int), 'flags': np.array([int(m.flags) for m in messages], dtype=np.uint32), 'relative_position_enu_m': np.array([m.relative_position_enu_m for m in messages]).T, 'position_std_enu_m': np.array([m.position_std_enu_m for m in messages]).T, - 'heading_true_north_deg': np.array([float(m.heading_true_north_deg) for m in messages]), - 'baseline_distance_m': np.array([float(m.baseline_distance_m) for m in messages]), } result.update(MeasurementDetails.to_numpy([m.details for m in messages])) return result +################################################################################ +# Binary Sensor Data Definitions +################################################################################ + class InputDataWrapperMessage(MessagePayload): """! diff --git a/python/fusion_engine_client/messages/solution.py b/python/fusion_engine_client/messages/solution.py index efaafa18..e6b910be 100644 --- a/python/fusion_engine_client/messages/solution.py +++ b/python/fusion_engine_client/messages/solution.py @@ -813,6 +813,12 @@ def to_numpy(cls, messages: Sequence['CalibrationStatus']): class RelativeENUPositionMessage(MessagePayload): """! @brief Relative ENU position to base station. + + @note + This message represents the relationship between the navigation engine's + position solution and a nearby RTK base station. It is not used to convey + unfiltered vehicle body orientation measurements generated using multiple + GNSS antennas. See @ref GNSSAttitudeOutput instead. """ MESSAGE_TYPE = MessageType.RELATIVE_ENU_POSITION MESSAGE_VERSION = 0 diff --git a/src/point_one/fusion_engine/messages/configuration.h b/src/point_one/fusion_engine/messages/configuration.h index f73cee94..042160d6 100644 --- a/src/point_one/fusion_engine/messages/configuration.h +++ b/src/point_one/fusion_engine/messages/configuration.h @@ -1222,7 +1222,7 @@ struct P1_ALIGNAS(4) HardwareTickConfig { * If one value is NOT set, the system will not output the corrected * heading message. * - * @ref HeadingOutput + * @ref GNSSAttitudeOutput */ struct P1_ALIGNAS(4) HeadingBias { /** diff --git a/src/point_one/fusion_engine/messages/defs.h b/src/point_one/fusion_engine/messages/defs.h index 26e964a4..b91c4da7 100644 --- a/src/point_one/fusion_engine/messages/defs.h +++ b/src/point_one/fusion_engine/messages/defs.h @@ -47,10 +47,12 @@ enum class MessageType : uint16_t { // Sensor measurement messages. IMU_OUTPUT = 11000, ///< @ref IMUOutput - RAW_HEADING_OUTPUT = 11001, ///< @ref RawHeadingOutput + DEPRECATED_RAW_HEADING_OUTPUT = 11001, RAW_IMU_OUTPUT = 11002, ///< @ref RawIMUOutput - HEADING_OUTPUT = 11003, ///< @ref HeadingOutput + DEPRECATED_HEADING_OUTPUT = 11003, IMU_INPUT = 11004, ///< @ref IMUInput + GNSS_ATTITUDE_OUTPUT = 11005, ///< @ref GNSSAttitudeOutput + RAW_GNSS_ATTITUDE_OUTPUT = 11006, ///< @ref RawGNSSAttitudeOutput // Vehicle measurement messages. DEPRECATED_WHEEL_SPEED_MEASUREMENT = @@ -151,18 +153,24 @@ P1_CONSTEXPR_FUNC const char* to_string(MessageType type) { case MessageType::IMU_OUTPUT: return "IMU Output"; - case MessageType::RAW_HEADING_OUTPUT: - return "Raw heading output"; + case MessageType::DEPRECATED_RAW_HEADING_OUTPUT: + return "Raw GNSS Heading Output"; case MessageType::RAW_IMU_OUTPUT: return "Raw IMU Output"; - case MessageType::HEADING_OUTPUT: - return "Heading Output"; + case MessageType::DEPRECATED_HEADING_OUTPUT: + return "GNSS Heading Output"; case MessageType::IMU_INPUT: return "IMU Input"; + case MessageType::GNSS_ATTITUDE_OUTPUT: + return "GNSS Attitude Output"; + + case MessageType::RAW_GNSS_ATTITUDE_OUTPUT: + return "Raw GNSS Attitude Output"; + case MessageType::DEPRECATED_WHEEL_SPEED_MEASUREMENT: return "Wheel Speed Measurement"; @@ -320,6 +328,7 @@ P1_CONSTEXPR_FUNC bool IsCommand(MessageType message_type) { case MessageType::GET_MESSAGE_RATE: case MessageType::STA5635_COMMAND: return true; + case MessageType::INVALID: case MessageType::POSE: case MessageType::GNSS_INFO: @@ -329,10 +338,12 @@ P1_CONSTEXPR_FUNC bool IsCommand(MessageType message_type) { case MessageType::RELATIVE_ENU_POSITION: case MessageType::SYSTEM_STATUS: case MessageType::IMU_OUTPUT: - case MessageType::RAW_HEADING_OUTPUT: + case MessageType::DEPRECATED_RAW_HEADING_OUTPUT: case MessageType::RAW_IMU_OUTPUT: - case MessageType::HEADING_OUTPUT: + case MessageType::DEPRECATED_HEADING_OUTPUT: case MessageType::IMU_INPUT: + case MessageType::GNSS_ATTITUDE_OUTPUT: + case MessageType::RAW_GNSS_ATTITUDE_OUTPUT: case MessageType::DEPRECATED_WHEEL_SPEED_MEASUREMENT: case MessageType::DEPRECATED_VEHICLE_SPEED_MEASUREMENT: case MessageType::WHEEL_TICK_INPUT: diff --git a/src/point_one/fusion_engine/messages/device.h b/src/point_one/fusion_engine/messages/device.h index 56c3e497..d4fdda71 100644 --- a/src/point_one/fusion_engine/messages/device.h +++ b/src/point_one/fusion_engine/messages/device.h @@ -286,11 +286,6 @@ struct P1_ALIGNAS(4) EventNotificationMessage : public MessagePayload { * @brief System status information (@ref * MessageType::SYSTEM_STATUS, version 1.0). * @ingroup device_status - * - * @note - * All data is timestamped using the Point One Time, which is a monotonic - * timestamp referenced to the start of the device. Corresponding messages (@ref - * SystemStatusMessage) may be associated using their @ref p1_time values. */ struct P1_ALIGNAS(4) SystemStatusMessage : public MessagePayload { diff --git a/src/point_one/fusion_engine/messages/measurements.h b/src/point_one/fusion_engine/messages/measurements.h index f414293e..d25ad09a 100644 --- a/src/point_one/fusion_engine/messages/measurements.h +++ b/src/point_one/fusion_engine/messages/measurements.h @@ -1134,23 +1134,33 @@ struct P1_ALIGNAS(4) DeprecatedVehicleSpeedMeasurement : public MessagePayload { }; //////////////////////////////////////////////////////////////////////////////// -// Heading Sensor Definitions +// Attitude Sensor Definitions //////////////////////////////////////////////////////////////////////////////// /** - * @brief Heading sensor measurement output with heading bias corrections - * applied (@ref MessageType::HEADING_OUTPUT, version 1.0). + * @brief Multi-antenna GNSS attitude sensor measurement output with offset + * corrections applied (@ref MessageType::GNSS_ATTITUDE_OUTPUT, version + * 1.0). * @ingroup measurement_messages * - * This message is an output from the device contaning heading sensor - * measurements after applying user-specified horizontal and vertical bias - * corrections to account for the orientation of the primary and secondary GNSS - * antennas. + * This message is an output from the device contaning orientation measurements + * generated using multiple GNSS antennas/receivers. On supported devices, the + * device will measure vehicle yaw (heading) and pitch based on the relative + * positions of two GNSS antennas. When more than two antennas are present, the + * device may additionally measure roll angle. * - * See also @ref RawHeadingOutput. + * @note + * This message contains vehicle body angle measurements generated from GNSS + * measurements. These measurements inputs to the navigation engine, not the + * filtered output from engine. They may be less accurate than the vehicle body + * orientation estimate in @ref PoseMessage. + * + * The measurements in this message have user-specified corrections applied for + * the horizontal and vertical offsets between the two GNSS antennas. See also + * @ref RawGNSSAttitudeOutput. */ -struct P1_ALIGNAS(4) HeadingOutput : public MessagePayload { - static constexpr MessageType MESSAGE_TYPE = MessageType::HEADING_OUTPUT; +struct P1_ALIGNAS(4) GNSSAttitudeOutput : public MessagePayload { + static constexpr MessageType MESSAGE_TYPE = MessageType::GNSS_ATTITUDE_OUTPUT; static constexpr uint8_t MESSAGE_VERSION = 0; /** @@ -1171,44 +1181,54 @@ struct P1_ALIGNAS(4) HeadingOutput : public MessagePayload { uint32_t flags = 0; /** - * The measured YPR vector (in degrees), resolved in the ENU frame. + * The measured vehicle body orientation (in degrees). * * YPR is defined as an intrinsic Euler-321 rotation, i.e., yaw, pitch, then - * roll. + * roll with respect to the local ENU tangent plane. See @ref + * PoseMessage::ypr_deg for a complete rotation definition. + * + * If any angles are not available, they will be set to `NAN`. For + * dual-antenna systems, the device will measure yaw and pitch, but not roll. * - * @note - * This field contains the measured attitude information (@ref - * RawHeadingOutput) from a secondary heading device after applying @ref - * ConfigType::HEADING_BIAS configuration settings for yaw (horizontal) and - * pitch (vertical) offsets between the primary and secondary GNSS antennas. - * If either bias value is not specified, the corresponding measurement values - * will be set to `NAN`. + * Note that yaw is measured from east in a counter-clockwise direction. For + * example, north is +90 degrees. Heading with respect to true north can be + * computed as `heading = 90.0 - ypr_deg[0]`. */ float ypr_deg[3] = {NAN, NAN, NAN}; /** - * The heading angle (in degrees) with respect to true north, pointing from - * the primary antenna to the secondary antenna, after applying bias - * corrections. - * - * @note - * Reported in the range [0, 360). + * The standard deviation of the orientation measurement (in degrees). + */ + float ypr_std_deg[3] = {NAN, NAN, NAN}; + + /** + * The estimated distance between primary and secondary antennas (in meters). + */ + float baseline_distance_m = NAN; + + /** + * The standard deviation of the baseline distance estimate (in meters). */ - float heading_true_north_deg = NAN; + float baseline_distance_std_m = NAN; }; /** - * @brief Raw (uncorrected) heading sensor measurement output (@ref - * MessageType::RAW_HEADING_OUTPUT, version 1.0). + * @brief Raw (uncorrected) GNSS attitude sensor measurement output (@ref + * MessageType::RAW_GNSS_ATTITUDE_OUTPUT, version 1.0). * @ingroup measurement_messages * - * This message is an output from the device contaning raw heading sensor - * measurements that have not been corrected for mounting angle biases. + * This message is an output from the device contaning raw orientation + * measurements generated using multiple GNSS antennas/receivers that have not + * been corrected for horizontal/vertical offsets between the antennas. Here, + * orientation is represented as the vector from a primary GNSS antenna to a + * secondary GNSS antenna. * - * See also @ref HeadingOutput. + * For vehicle body angle measurements, and for measurements corrected for + * horizontal/vertical offsets, see @ref GNSSAttitudeOutput. */ -struct P1_ALIGNAS(4) RawHeadingOutput : public MessagePayload { - static constexpr MessageType MESSAGE_TYPE = MessageType::RAW_HEADING_OUTPUT; +struct P1_ALIGNAS(4) RawGNSSAttitudeOutput : public MessagePayload { + static constexpr MessageType MESSAGE_TYPE = + MessageType::RAW_GNSS_ATTITUDE_OUTPUT; static constexpr uint8_t MESSAGE_VERSION = 0; /** @@ -1241,26 +1261,16 @@ struct P1_ALIGNAS(4) RawHeadingOutput : public MessagePayload { float relative_position_enu_m[3] = {NAN, NAN, NAN}; /** - * The position standard deviation (in meters), resolved with respect to the - * local ENU tangent plane: east, north, up. + * The standard deviation of the relative position vector (in meters), + * resolved with respect to the local ENU tangent plane: east, north, up. */ float position_std_enu_m[3] = {NAN, NAN, NAN}; - - /** - * The measured heading angle (in degrees) with respect to true north, - * pointing from the primary antenna to the secondary antenna. - * - * @note - * Reported in the range [0, 360). - */ - float heading_true_north_deg = NAN; - - /** - * The estimated distance between primary and secondary antennas (in meters). - */ - float baseline_distance_m = NAN; }; +//////////////////////////////////////////////////////////////////////////////// +// Binary Sensor Data Definitions +//////////////////////////////////////////////////////////////////////////////// + /** * @brief A block of incoming sensor data whose definition depends on the value * of @ ref data_type. (@ref MessageType::INPUT_DATA_WRAPPER). diff --git a/src/point_one/fusion_engine/messages/solution.h b/src/point_one/fusion_engine/messages/solution.h index 02dc1a01..f66422d8 100644 --- a/src/point_one/fusion_engine/messages/solution.h +++ b/src/point_one/fusion_engine/messages/solution.h @@ -32,7 +32,7 @@ namespace messages { * @ingroup solution_messages * * @note - * All data is timestamped using the Point One Time, which is a monotonic + * All data is timestamped using Point One (P1) time, which is a monotonic * timestamp referenced to the start of the device. Corresponding messages (@ref * GNSSInfoMessage, @ref GNSSSatelliteMessage, etc.) may be associated using * their @ref p1_time values. @@ -504,10 +504,10 @@ struct P1_ALIGNAS(4) CalibrationStatusMessage : public MessagePayload { * @ingroup solution_messages * * @note - * All data is timestamped using the Point One Time, which is a monotonic - * timestamp referenced to the start of the device. Corresponding messages (@ref - * PoseMessage, @ref GNSSSatelliteMessage, etc.) may be associated using - * their @ref p1_time values. + * This message represents the relationship between the navigation engine's + * position solution and a nearby RTK base station. It is not used to convey + * unfiltered vehicle body orientation measurements generated using multiple + * GNSS antennas. See @ref GNSSAttitudeOutput instead. */ struct P1_ALIGNAS(4) RelativeENUPositionMessage : public MessagePayload { static constexpr MessageType MESSAGE_TYPE =