From 52c17e74e8b44aada30abe1a816f0a021d87022f Mon Sep 17 00:00:00 2001 From: Adam Shapiro Date: Mon, 2 Dec 2024 10:22:05 -0500 Subject: [PATCH 01/12] Added missing separator for input wrapper messages. --- python/fusion_engine_client/messages/measurements.py | 4 ++++ src/point_one/fusion_engine/messages/measurements.h | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/python/fusion_engine_client/messages/measurements.py b/python/fusion_engine_client/messages/measurements.py index 2847e8f5..5af26208 100644 --- a/python/fusion_engine_client/messages/measurements.py +++ b/python/fusion_engine_client/messages/measurements.py @@ -1391,6 +1391,10 @@ def to_numpy(cls, messages: Sequence['RawHeadingOutput']): result.update(MeasurementDetails.to_numpy([m.details for m in messages])) return result +################################################################################ +# Binary Sensor Data Definitions +################################################################################ + class InputDataWrapperMessage(MessagePayload): """! diff --git a/src/point_one/fusion_engine/messages/measurements.h b/src/point_one/fusion_engine/messages/measurements.h index f414293e..8d7e832e 100644 --- a/src/point_one/fusion_engine/messages/measurements.h +++ b/src/point_one/fusion_engine/messages/measurements.h @@ -1261,6 +1261,10 @@ struct P1_ALIGNAS(4) RawHeadingOutput : public MessagePayload { 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). From 23d1e382118db8c6f338af8c54d2fb49d9fc6573 Mon Sep 17 00:00:00 2001 From: Adam Shapiro Date: Mon, 2 Dec 2024 10:30:17 -0500 Subject: [PATCH 02/12] Renamed HeadingOutput -> GNSSHeadingOutput. This distinguishes between a vehicle body heading (orientation) _measurement_ computed from two GNSS antennas, which is a sensor input to the navigation engine, and the YPR vehicle body orientation being estimated by the navigation engine (reported in PoseMessage). --- .../fusion_engine_client/analysis/analyzer.py | 19 +++--- python/fusion_engine_client/messages/defs.py | 4 +- .../messages/measurements.py | 28 ++++---- .../fusion_engine/messages/configuration.h | 2 +- src/point_one/fusion_engine/messages/defs.h | 17 ++--- .../fusion_engine/messages/measurements.h | 66 +++++++++++-------- 6 files changed, 74 insertions(+), 62 deletions(-) diff --git a/python/fusion_engine_client/analysis/analyzer.py b/python/fusion_engine_client/analysis/analyzer.py index 3ef47a18..17078158 100755 --- a/python/fusion_engine_client/analysis/analyzer.py +++ b/python/fusion_engine_client/analysis/analyzer.py @@ -1943,20 +1943,20 @@ 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_heading_measurements(self): """! - @brief Generate time series plots for heading (degrees) and baseline distance (meters) data. + @brief Generate time series plots for GNSS heading (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] + result = self.reader.read(message_types=[RawGNSSHeadingOutput, GNSSHeadingOutput], **self.params) + raw_heading_data = result[RawGNSSHeadingOutput.MESSAGE_TYPE] + heading_data = result[GNSSHeadingOutput.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 heading 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 @@ -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' ), @@ -2188,7 +2187,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_heading_measurement', figure=fig, title='Measurements: GNSS Heading') def plot_system_status_profiling(self): """! @@ -3004,7 +3003,7 @@ def main(args=None): # By default, we always plot heading measurements (i.e., output from a secondary heading device like an # LG69T-AH), separate from other sensor measurements controlled by --measurements. - analyzer.plot_heading_measurements() + analyzer.plot_gnss_heading_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..49429d9d 100644 --- a/python/fusion_engine_client/messages/defs.py +++ b/python/fusion_engine_client/messages/defs.py @@ -99,9 +99,9 @@ class MessageType(IntEnum): # Sensor measurement messages. IMU_OUTPUT = 11000 - RAW_HEADING_OUTPUT = 11001 + RAW_GNSS_HEADING_OUTPUT = 11001 RAW_IMU_OUTPUT = 11002 - HEADING_OUTPUT = 11003 + GNSS_HEADING_OUTPUT = 11003 IMU_INPUT = 11004 # Vehicle measurement messages. diff --git a/python/fusion_engine_client/messages/measurements.py b/python/fusion_engine_client/messages/measurements.py index 5af26208..ac6dd533 100644 --- a/python/fusion_engine_client/messages/measurements.py +++ b/python/fusion_engine_client/messages/measurements.py @@ -1165,15 +1165,15 @@ def to_numpy(cls, messages): return result ################################################################################ -# Heading Sensor Definitions +# GNSS Heading Sensor Definitions ################################################################################ -class HeadingOutput(MessagePayload): +class GNSSHeadingOutput(MessagePayload): """! - @brief Heading sensor measurement output with heading bias corrections applied. + @brief Multi-antenna GNSS heading sensor measurement output with offset corrections applied. """ - MESSAGE_TYPE = MessageType.HEADING_OUTPUT + MESSAGE_TYPE = MessageType.GNSS_HEADING_OUTPUT MESSAGE_VERSION = 0 _STRUCT = struct.Struct(' int: return cls._STRUCT.size + MeasurementDetails.calcsize() @classmethod - def to_numpy(cls, messages: Sequence['HeadingOutput']): + def to_numpy(cls, messages: Sequence['GNSSHeadingOutput']): 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), @@ -1270,11 +1270,11 @@ def to_numpy(cls, messages: Sequence['HeadingOutput']): return result -class RawHeadingOutput(MessagePayload): +class RawGNSSHeadingOutput(MessagePayload): """! - @brief Raw (uncorrected) heading sensor measurement output. + @brief Raw (uncorrected) GNSS heading sensor measurement output. """ - MESSAGE_TYPE = MessageType.RAW_HEADING_OUTPUT + MESSAGE_TYPE = MessageType.RAW_GNSS_HEADING_OUTPUT MESSAGE_VERSION = 0 _STRUCT = struct.Struct(' int: return cls._STRUCT.size + MeasurementDetails.calcsize() @classmethod - def to_numpy(cls, messages: Sequence['RawHeadingOutput']): + def to_numpy(cls, messages: Sequence['RawGNSSHeadingOutput']): 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), diff --git a/src/point_one/fusion_engine/messages/configuration.h b/src/point_one/fusion_engine/messages/configuration.h index f73cee94..b3bb010f 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 GNSSHeadingOutput */ 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..7e7df482 100644 --- a/src/point_one/fusion_engine/messages/defs.h +++ b/src/point_one/fusion_engine/messages/defs.h @@ -47,9 +47,9 @@ enum class MessageType : uint16_t { // Sensor measurement messages. IMU_OUTPUT = 11000, ///< @ref IMUOutput - RAW_HEADING_OUTPUT = 11001, ///< @ref RawHeadingOutput + RAW_GNSS_HEADING_OUTPUT = 11001, ///< @ref RawGNSSHeadingOutput RAW_IMU_OUTPUT = 11002, ///< @ref RawIMUOutput - HEADING_OUTPUT = 11003, ///< @ref HeadingOutput + GNSS_HEADING_OUTPUT = 11003, ///< @ref GNSSHeadingOutput IMU_INPUT = 11004, ///< @ref IMUInput // Vehicle measurement messages. @@ -151,14 +151,14 @@ 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::RAW_GNSS_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::GNSS_HEADING_OUTPUT: + return "GNSS Heading Output"; case MessageType::IMU_INPUT: return "IMU Input"; @@ -320,6 +320,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,9 +330,9 @@ 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::RAW_GNSS_HEADING_OUTPUT: case MessageType::RAW_IMU_OUTPUT: - case MessageType::HEADING_OUTPUT: + case MessageType::GNSS_HEADING_OUTPUT: case MessageType::IMU_INPUT: case MessageType::DEPRECATED_WHEEL_SPEED_MEASUREMENT: case MessageType::DEPRECATED_VEHICLE_SPEED_MEASUREMENT: diff --git a/src/point_one/fusion_engine/messages/measurements.h b/src/point_one/fusion_engine/messages/measurements.h index 8d7e832e..22b1e113 100644 --- a/src/point_one/fusion_engine/messages/measurements.h +++ b/src/point_one/fusion_engine/messages/measurements.h @@ -1138,19 +1138,29 @@ struct P1_ALIGNAS(4) DeprecatedVehicleSpeedMeasurement : public MessagePayload { //////////////////////////////////////////////////////////////////////////////// /** - * @brief Heading sensor measurement output with heading bias corrections - * applied (@ref MessageType::HEADING_OUTPUT, version 1.0). + * @brief Multi-antenna GNSS heading sensor measurement output with offset + * corrections applied (@ref MessageType::GNSS_HEADING_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 heading/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 RawGNSSHeadingOutput. */ -struct P1_ALIGNAS(4) HeadingOutput : public MessagePayload { - static constexpr MessageType MESSAGE_TYPE = MessageType::HEADING_OUTPUT; +struct P1_ALIGNAS(4) GNSSHeadingOutput : public MessagePayload { + static constexpr MessageType MESSAGE_TYPE = MessageType::GNSS_HEADING_OUTPUT; static constexpr uint8_t MESSAGE_VERSION = 0; /** @@ -1171,25 +1181,22 @@ 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. * - * @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`. + * 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. */ 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. + * the primary antenna to the secondary antenna. + * + * The reported angle includes the user-specified heading offset correction. * * @note * Reported in the range [0, 360). @@ -1198,17 +1205,22 @@ struct P1_ALIGNAS(4) HeadingOutput : public MessagePayload { }; /** - * @brief Raw (uncorrected) heading sensor measurement output (@ref - * MessageType::RAW_HEADING_OUTPUT, version 1.0). + * @brief Raw (uncorrected) GNSS heading sensor measurement output (@ref + * MessageType::RAW_GNSS_HEADING_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 GNSSHeadingOutput. */ -struct P1_ALIGNAS(4) RawHeadingOutput : public MessagePayload { - static constexpr MessageType MESSAGE_TYPE = MessageType::RAW_HEADING_OUTPUT; +struct P1_ALIGNAS(4) RawGNSSHeadingOutput : public MessagePayload { + static constexpr MessageType MESSAGE_TYPE = + MessageType::RAW_GNSS_HEADING_OUTPUT; static constexpr uint8_t MESSAGE_VERSION = 0; /** From 43a633c05a69714a22a86c668b964a381987322e Mon Sep 17 00:00:00 2001 From: Adam Shapiro Date: Mon, 2 Dec 2024 11:56:30 -0500 Subject: [PATCH 03/12] Minor P1 time description cleanup. --- src/point_one/fusion_engine/messages/device.h | 5 ----- src/point_one/fusion_engine/messages/solution.h | 8 +------- 2 files changed, 1 insertion(+), 12 deletions(-) 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/solution.h b/src/point_one/fusion_engine/messages/solution.h index 02dc1a01..0aac8e73 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. @@ -502,12 +502,6 @@ struct P1_ALIGNAS(4) CalibrationStatusMessage : public MessagePayload { * @brief Relative ENU position to base station (@ref * MessageType::RELATIVE_ENU_POSITION, version 1.1). * @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. */ struct P1_ALIGNAS(4) RelativeENUPositionMessage : public MessagePayload { static constexpr MessageType MESSAGE_TYPE = From 702ebd077c5c24796db3a71afb468ec3062a719f Mon Sep 17 00:00:00 2001 From: Adam Shapiro Date: Mon, 2 Dec 2024 12:00:02 -0500 Subject: [PATCH 04/12] Added a note about RelativeENUPositionMessage vs GNSSHeadingOutput. --- python/fusion_engine_client/messages/solution.py | 6 ++++++ src/point_one/fusion_engine/messages/solution.h | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/python/fusion_engine_client/messages/solution.py b/python/fusion_engine_client/messages/solution.py index efaafa18..c607ebc5 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 GNSSHeadingOutput instead. """ MESSAGE_TYPE = MessageType.RELATIVE_ENU_POSITION MESSAGE_VERSION = 0 diff --git a/src/point_one/fusion_engine/messages/solution.h b/src/point_one/fusion_engine/messages/solution.h index 0aac8e73..956da115 100644 --- a/src/point_one/fusion_engine/messages/solution.h +++ b/src/point_one/fusion_engine/messages/solution.h @@ -502,6 +502,12 @@ struct P1_ALIGNAS(4) CalibrationStatusMessage : public MessagePayload { * @brief Relative ENU position to base station (@ref * MessageType::RELATIVE_ENU_POSITION, version 1.1). * @ingroup solution_messages + * + * @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 GNSSHeadingOutput instead. */ struct P1_ALIGNAS(4) RelativeENUPositionMessage : public MessagePayload { static constexpr MessageType MESSAGE_TYPE = From 29df29537812c74e609e0830e5275a7a6043524e Mon Sep 17 00:00:00 2001 From: Adam Shapiro Date: Mon, 2 Dec 2024 17:18:39 -0500 Subject: [PATCH 05/12] Renamed GNSSHeadingOutput -> GNSSAttitudeOutput. --- .../fusion_engine_client/analysis/analyzer.py | 24 +++++++------- python/fusion_engine_client/messages/defs.py | 4 +-- .../messages/measurements.py | 22 ++++++------- .../fusion_engine_client/messages/solution.py | 2 +- .../fusion_engine/messages/configuration.h | 2 +- src/point_one/fusion_engine/messages/defs.h | 17 +++++----- .../fusion_engine/messages/measurements.h | 32 +++++++++---------- .../fusion_engine/messages/solution.h | 2 +- 8 files changed, 53 insertions(+), 52 deletions(-) diff --git a/python/fusion_engine_client/analysis/analyzer.py b/python/fusion_engine_client/analysis/analyzer.py index 17078158..3f4eb570 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_gnss_heading_measurements(self): + def plot_gnss_attitude_measurements(self): """! - @brief Generate time series plots for GNSS 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=[RawGNSSHeadingOutput, GNSSHeadingOutput], **self.params) - raw_heading_data = result[RawGNSSHeadingOutput.MESSAGE_TYPE] - heading_data = result[GNSSHeadingOutput.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 GNSS 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] @@ -2187,7 +2187,7 @@ def plot_gnss_heading_measurements(self): row=3, col=1 ) - self._add_figure(name='gnss_heading_measurement', figure=fig, title='Measurements: GNSS Heading') + self._add_figure(name='gnss_attitude_measurement', figure=fig, title='Measurements: GNSS Attitude') def plot_system_status_profiling(self): """! @@ -3001,9 +3001,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_gnss_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 49429d9d..23bd6904 100644 --- a/python/fusion_engine_client/messages/defs.py +++ b/python/fusion_engine_client/messages/defs.py @@ -99,9 +99,9 @@ class MessageType(IntEnum): # Sensor measurement messages. IMU_OUTPUT = 11000 - RAW_GNSS_HEADING_OUTPUT = 11001 + RAW_GNSS_ATTITUDE_OUTPUT = 11001 RAW_IMU_OUTPUT = 11002 - GNSS_HEADING_OUTPUT = 11003 + GNSS_ATTITUDE_OUTPUT = 11003 IMU_INPUT = 11004 # Vehicle measurement messages. diff --git a/python/fusion_engine_client/messages/measurements.py b/python/fusion_engine_client/messages/measurements.py index ac6dd533..bf831924 100644 --- a/python/fusion_engine_client/messages/measurements.py +++ b/python/fusion_engine_client/messages/measurements.py @@ -1169,11 +1169,11 @@ def to_numpy(cls, messages): ################################################################################ -class GNSSHeadingOutput(MessagePayload): +class GNSSAttitudeOutput(MessagePayload): """! - @brief Multi-antenna GNSS heading sensor measurement output with offset corrections applied. + @brief Multi-antenna GNSS attitude sensor measurement output with offset corrections applied. """ - MESSAGE_TYPE = MessageType.GNSS_HEADING_OUTPUT + MESSAGE_TYPE = MessageType.GNSS_ATTITUDE_OUTPUT MESSAGE_VERSION = 0 _STRUCT = struct.Struct(' int: return cls._STRUCT.size + MeasurementDetails.calcsize() @classmethod - def to_numpy(cls, messages: Sequence['GNSSHeadingOutput']): + 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), @@ -1270,11 +1270,11 @@ def to_numpy(cls, messages: Sequence['GNSSHeadingOutput']): return result -class RawGNSSHeadingOutput(MessagePayload): +class RawGNSSAttitudeOutput(MessagePayload): """! - @brief Raw (uncorrected) GNSS heading sensor measurement output. + @brief Raw (uncorrected) GNSS attitude sensor measurement output. """ - MESSAGE_TYPE = MessageType.RAW_GNSS_HEADING_OUTPUT + MESSAGE_TYPE = MessageType.RAW_GNSS_ATTITUDE_OUTPUT MESSAGE_VERSION = 0 _STRUCT = struct.Struct(' int: return cls._STRUCT.size + MeasurementDetails.calcsize() @classmethod - def to_numpy(cls, messages: Sequence['RawGNSSHeadingOutput']): + 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), diff --git a/python/fusion_engine_client/messages/solution.py b/python/fusion_engine_client/messages/solution.py index c607ebc5..e6b910be 100644 --- a/python/fusion_engine_client/messages/solution.py +++ b/python/fusion_engine_client/messages/solution.py @@ -818,7 +818,7 @@ class RelativeENUPositionMessage(MessagePayload): 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 GNSSHeadingOutput instead. + 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 b3bb010f..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 GNSSHeadingOutput + * @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 7e7df482..bcf22dd1 100644 --- a/src/point_one/fusion_engine/messages/defs.h +++ b/src/point_one/fusion_engine/messages/defs.h @@ -47,9 +47,10 @@ enum class MessageType : uint16_t { // Sensor measurement messages. IMU_OUTPUT = 11000, ///< @ref IMUOutput - RAW_GNSS_HEADING_OUTPUT = 11001, ///< @ref RawGNSSHeadingOutput + RAW_GNSS_ATTITUDE_OUTPUT = 11001, ///< @ref RawGNSSAttitudeOutput RAW_IMU_OUTPUT = 11002, ///< @ref RawIMUOutput - GNSS_HEADING_OUTPUT = 11003, ///< @ref GNSSHeadingOutput + // TODO Change the numbers + GNSS_ATTITUDE_OUTPUT = 11003, ///< @ref GNSSAttitudeOutput IMU_INPUT = 11004, ///< @ref IMUInput // Vehicle measurement messages. @@ -151,14 +152,14 @@ P1_CONSTEXPR_FUNC const char* to_string(MessageType type) { case MessageType::IMU_OUTPUT: return "IMU Output"; - case MessageType::RAW_GNSS_HEADING_OUTPUT: - return "Raw GNSS Heading Output"; + case MessageType::RAW_GNSS_ATTITUDE_OUTPUT: + return "Raw GNSS Attitude Output"; case MessageType::RAW_IMU_OUTPUT: return "Raw IMU Output"; - case MessageType::GNSS_HEADING_OUTPUT: - return "GNSS Heading Output"; + case MessageType::GNSS_ATTITUDE_OUTPUT: + return "GNSS Attitude Output"; case MessageType::IMU_INPUT: return "IMU Input"; @@ -330,9 +331,9 @@ P1_CONSTEXPR_FUNC bool IsCommand(MessageType message_type) { case MessageType::RELATIVE_ENU_POSITION: case MessageType::SYSTEM_STATUS: case MessageType::IMU_OUTPUT: - case MessageType::RAW_GNSS_HEADING_OUTPUT: + case MessageType::RAW_GNSS_ATTITUDE_OUTPUT: case MessageType::RAW_IMU_OUTPUT: - case MessageType::GNSS_HEADING_OUTPUT: + case MessageType::GNSS_ATTITUDE_OUTPUT: case MessageType::IMU_INPUT: case MessageType::DEPRECATED_WHEEL_SPEED_MEASUREMENT: case MessageType::DEPRECATED_VEHICLE_SPEED_MEASUREMENT: diff --git a/src/point_one/fusion_engine/messages/measurements.h b/src/point_one/fusion_engine/messages/measurements.h index 22b1e113..d7f74bb0 100644 --- a/src/point_one/fusion_engine/messages/measurements.h +++ b/src/point_one/fusion_engine/messages/measurements.h @@ -1134,20 +1134,20 @@ struct P1_ALIGNAS(4) DeprecatedVehicleSpeedMeasurement : public MessagePayload { }; //////////////////////////////////////////////////////////////////////////////// -// Heading Sensor Definitions +// Attitude Sensor Definitions //////////////////////////////////////////////////////////////////////////////// /** - * @brief Multi-antenna GNSS heading sensor measurement output with offset - * corrections applied (@ref MessageType::GNSS_HEADING_OUTPUT, version + * @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/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. + * 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. * * @note * This message contains vehicle body angle measurements generated from GNSS @@ -1157,10 +1157,10 @@ struct P1_ALIGNAS(4) DeprecatedVehicleSpeedMeasurement : public MessagePayload { * * The measurements in this message have user-specified corrections applied for * the horizontal and vertical offsets between the two GNSS antennas. See also - * @ref RawGNSSHeadingOutput. + * @ref RawGNSSAttitudeOutput. */ -struct P1_ALIGNAS(4) GNSSHeadingOutput : public MessagePayload { - static constexpr MessageType MESSAGE_TYPE = MessageType::GNSS_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; /** @@ -1205,8 +1205,8 @@ struct P1_ALIGNAS(4) GNSSHeadingOutput : public MessagePayload { }; /** - * @brief Raw (uncorrected) GNSS heading sensor measurement output (@ref - * MessageType::RAW_GNSS_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 orientation @@ -1216,11 +1216,11 @@ struct P1_ALIGNAS(4) GNSSHeadingOutput : public MessagePayload { * secondary GNSS antenna. * * For vehicle body angle measurements, and for measurements corrected for - * horizontal/vertical offsets, see @ref GNSSHeadingOutput. + * horizontal/vertical offsets, see @ref GNSSAttitudeOutput. */ -struct P1_ALIGNAS(4) RawGNSSHeadingOutput : public MessagePayload { +struct P1_ALIGNAS(4) RawGNSSAttitudeOutput : public MessagePayload { static constexpr MessageType MESSAGE_TYPE = - MessageType::RAW_GNSS_HEADING_OUTPUT; + MessageType::RAW_GNSS_ATTITUDE_OUTPUT; static constexpr uint8_t MESSAGE_VERSION = 0; /** diff --git a/src/point_one/fusion_engine/messages/solution.h b/src/point_one/fusion_engine/messages/solution.h index 956da115..f66422d8 100644 --- a/src/point_one/fusion_engine/messages/solution.h +++ b/src/point_one/fusion_engine/messages/solution.h @@ -507,7 +507,7 @@ struct P1_ALIGNAS(4) CalibrationStatusMessage : public MessagePayload { * 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 GNSSHeadingOutput instead. + * GNSS antennas. See @ref GNSSAttitudeOutput instead. */ struct P1_ALIGNAS(4) RelativeENUPositionMessage : public MessagePayload { static constexpr MessageType MESSAGE_TYPE = From 92b3b5b63a6c78cbe91dae34c91db932d25a93c3 Mon Sep 17 00:00:00 2001 From: Adam Shapiro Date: Mon, 2 Dec 2024 18:27:02 -0500 Subject: [PATCH 06/12] Added Python yaw/heading helper functions. --- python/fusion_engine_client/messages/defs.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/python/fusion_engine_client/messages/defs.py b/python/fusion_engine_client/messages/defs.py index 23bd6904..63e53ad8 100644 --- a/python/fusion_engine_client/messages/defs.py +++ b/python/fusion_engine_client/messages/defs.py @@ -785,3 +785,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 From aebbc45542390786254caaac5476fcb9b20aa473 Mon Sep 17 00:00:00 2001 From: Adam Shapiro Date: Mon, 2 Dec 2024 18:28:15 -0500 Subject: [PATCH 07/12] Redefined GNSS attitude messages and deprecated old IDs. - Removed redundant heading convenience fields (compute from the ENU vector or yaw) - Added YPR std dev - Added baseline distance std dev - Compute heading angle as needed in Analyzer --- .../fusion_engine_client/analysis/analyzer.py | 14 ++-- python/fusion_engine_client/messages/defs.py | 6 +- .../messages/measurements.py | 64 ++++++++++--------- src/point_one/fusion_engine/messages/defs.h | 27 +++++--- .../fusion_engine/messages/measurements.h | 30 ++++----- 5 files changed, 78 insertions(+), 63 deletions(-) diff --git a/python/fusion_engine_client/analysis/analyzer.py b/python/fusion_engine_client/analysis/analyzer.py index 3f4eb570..c9e68878 100755 --- a/python/fusion_engine_client/analysis/analyzer.py +++ b/python/fusion_engine_client/analysis/analyzer.py @@ -2012,10 +2012,11 @@ def plot_gnss_attitude_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"}, @@ -2030,6 +2031,9 @@ def plot_gnss_attitude_measurements(self): # 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, :])) + # 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 @@ -2047,7 +2051,7 @@ def plot_gnss_attitude_measurements(self): 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"}, @@ -2058,12 +2062,12 @@ def plot_gnss_attitude_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), @@ -2077,7 +2081,7 @@ def plot_gnss_attitude_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), diff --git a/python/fusion_engine_client/messages/defs.py b/python/fusion_engine_client/messages/defs.py index 63e53ad8..bc4ec613 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_GNSS_ATTITUDE_OUTPUT = 11001 + DEPRECATED_RAW_HEADING_OUTPUT = 11001 RAW_IMU_OUTPUT = 11002 - GNSS_ATTITUDE_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 diff --git a/python/fusion_engine_client/messages/measurements.py b/python/fusion_engine_client/messages/measurements.py index bf831924..e45ce835 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 @@ -1176,7 +1177,7 @@ class GNSSAttitudeOutput(MessagePayload): MESSAGE_TYPE = MessageType.GNSS_ATTITUDE_OUTPUT MESSAGE_VERSION = 0 - _STRUCT = struct.Struct(' (bytes, int): if buffer is None: @@ -1216,7 +1214,9 @@ 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]) offset += self._STRUCT.size if return_buffer: @@ -1234,7 +1234,9 @@ 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._STRUCT.unpack_from(buffer, offset) offset += self._STRUCT.size @@ -1244,15 +1246,16 @@ 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]' return result def __str__(self): return f"""\ GNSS Attitude Output @ {str(self.details.p1_time)} 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}""" @classmethod def calcsize(cls) -> int: @@ -1264,7 +1267,7 @@ def to_numpy(cls, messages: Sequence['GNSSAttitudeOutput']): '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, } result.update(MeasurementDetails.to_numpy([m.details for m in messages])) return result @@ -1292,23 +1295,23 @@ def __init__(self): # The position of the secondary GNSS antenna relative to the primary antenna (in meters), resolved with respect # to the local ENU tangent plane: east, north, up. self.relative_position_enu_m = np.full((3,), np.nan) + ## # The position standard deviation (in meters), resolved with respect to the # local ENU tangent plane: east, north, up. self.position_std_enu_m = np.full((3,), np.nan) - ## - # The heading between the primary device antenna and the secondary (in degrees) with - # respect to true north. - # - # @note - # Reported in the range [0, 360). - self.heading_true_north_deg = np.nan - ## # The estimated distance between primary and secondary antennas (in meters). self.baseline_distance_m = np.nan + ## + # The standard deviation of the baseline distance estimate (in meters). + self.baseline_distance_std_m = np.nan + + def get_heading_deg(self): + return math.degrees(math.atan2(self.relative_position_enu_m[1], self.relative_position_enu_m[0])) + def pack(self, buffer: bytes = None, offset: int = 0, return_buffer: bool = True) -> (bytes, int): if buffer is None: buffer = bytearray(self.calcsize()) @@ -1329,8 +1332,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: @@ -1351,8 +1354,8 @@ def unpack(self, buffer: bytes, offset: int = 0, message_version: int = MessageP 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.baseline_distance_m, + self.baseline_distance_std_m) = self._STRUCT.unpack_from(buffer, offset) offset += self._STRUCT.size self.solution_type = SolutionType(solution_type_int) @@ -1361,7 +1364,9 @@ 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, ' \ + 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, ' \ f'baseline={self.baseline_distance_m} m]' return result @@ -1371,8 +1376,9 @@ def __str__(self): 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} + Baseline distance (m): {self.baseline_distance_m:.2f} + Baseline std (m): {self.baseline_distance_std_m:.2f}""" @classmethod def calcsize(cls) -> int: @@ -1385,8 +1391,8 @@ def to_numpy(cls, messages: Sequence['RawGNSSAttitudeOutput']): '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]), + '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 diff --git a/src/point_one/fusion_engine/messages/defs.h b/src/point_one/fusion_engine/messages/defs.h index bcf22dd1..b91c4da7 100644 --- a/src/point_one/fusion_engine/messages/defs.h +++ b/src/point_one/fusion_engine/messages/defs.h @@ -47,11 +47,12 @@ enum class MessageType : uint16_t { // Sensor measurement messages. IMU_OUTPUT = 11000, ///< @ref IMUOutput - RAW_GNSS_ATTITUDE_OUTPUT = 11001, ///< @ref RawGNSSAttitudeOutput + DEPRECATED_RAW_HEADING_OUTPUT = 11001, RAW_IMU_OUTPUT = 11002, ///< @ref RawIMUOutput - // TODO Change the numbers - GNSS_ATTITUDE_OUTPUT = 11003, ///< @ref GNSSAttitudeOutput + 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 = @@ -152,18 +153,24 @@ P1_CONSTEXPR_FUNC const char* to_string(MessageType type) { case MessageType::IMU_OUTPUT: return "IMU Output"; - case MessageType::RAW_GNSS_ATTITUDE_OUTPUT: - return "Raw GNSS Attitude Output"; + case MessageType::DEPRECATED_RAW_HEADING_OUTPUT: + return "Raw GNSS Heading Output"; case MessageType::RAW_IMU_OUTPUT: return "Raw IMU Output"; - case MessageType::GNSS_ATTITUDE_OUTPUT: - return "GNSS Attitude 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"; @@ -331,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_GNSS_ATTITUDE_OUTPUT: + case MessageType::DEPRECATED_RAW_HEADING_OUTPUT: case MessageType::RAW_IMU_OUTPUT: - case MessageType::GNSS_ATTITUDE_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/measurements.h b/src/point_one/fusion_engine/messages/measurements.h index d7f74bb0..a1e766e7 100644 --- a/src/point_one/fusion_engine/messages/measurements.h +++ b/src/point_one/fusion_engine/messages/measurements.h @@ -1189,19 +1189,17 @@ struct P1_ALIGNAS(4) GNSSAttitudeOutput : public MessagePayload { * * 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 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. - * - * The reported angle includes the user-specified heading offset correction. - * - * @note - * Reported in the range [0, 360). + * The standard deviation of the orientation measurement (in degrees). */ - float heading_true_north_deg = NAN; + float ypr_std_deg[3] = {NAN, NAN, NAN}; }; /** @@ -1253,24 +1251,20 @@ struct P1_ALIGNAS(4) RawGNSSAttitudeOutput : 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). + * The estimated distance between primary and secondary antennas (in meters). */ - float heading_true_north_deg = NAN; + float baseline_distance_m = NAN; /** - * The estimated distance between primary and secondary antennas (in meters). + * The standard deviation of the baseline distance estimate (in meters). */ - float baseline_distance_m = NAN; + float baseline_distance_std_m = NAN; }; //////////////////////////////////////////////////////////////////////////////// From 45df8009386a189a25491f66a2a37cd16d797107 Mon Sep 17 00:00:00 2001 From: Adam Shapiro Date: Tue, 3 Dec 2024 08:44:31 -0500 Subject: [PATCH 08/12] Include baseline distance in corrected attitude message. --- .../fusion_engine_client/analysis/analyzer.py | 13 +++++++++ .../messages/measurements.py | 27 +++++++++++++++---- .../fusion_engine/messages/measurements.h | 10 +++++++ 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/python/fusion_engine_client/analysis/analyzer.py b/python/fusion_engine_client/analysis/analyzer.py index c9e68878..ac0cbc7c 100755 --- a/python/fusion_engine_client/analysis/analyzer.py +++ b/python/fusion_engine_client/analysis/analyzer.py @@ -2028,6 +2028,19 @@ def plot_gnss_attitude_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) diff --git a/python/fusion_engine_client/messages/measurements.py b/python/fusion_engine_client/messages/measurements.py index e45ce835..f95c9dae 100644 --- a/python/fusion_engine_client/messages/measurements.py +++ b/python/fusion_engine_client/messages/measurements.py @@ -1177,7 +1177,7 @@ class GNSSAttitudeOutput(MessagePayload): MESSAGE_TYPE = MessageType.GNSS_ATTITUDE_OUTPUT MESSAGE_VERSION = 0 - _STRUCT = struct.Struct(' (bytes, int): if buffer is None: buffer = bytearray(self.calcsize()) @@ -1216,7 +1224,9 @@ def pack(self, buffer: bytes = None, offset: int = 0, return_buffer: bool = True self.ypr_deg[2], self.ypr_std_deg[0], self.ypr_std_deg[1], - self.ypr_std_deg[2]) + self.ypr_std_deg[2], + self.baseline_distance_m, + self.baseline_distance_std_m) offset += self._STRUCT.size if return_buffer: @@ -1236,7 +1246,9 @@ def unpack(self, buffer: bytes, offset: int = 0, message_version: int = MessageP self.ypr_deg[2], self.ypr_std_deg[0], self.ypr_std_deg[1], - self.ypr_std_deg[2]) = \ + self.ypr_std_deg[2], + self.baseline_distance_m, + self.baseline_distance_std_m) = \ self._STRUCT.unpack_from(buffer, offset) offset += self._STRUCT.size @@ -1247,7 +1259,8 @@ def unpack(self, buffer: bytes, offset: int = 0, message_version: int = MessageP def __repr__(self): result = super().__repr__()[:-1] ypr_str = '(%.1f, %.1f, %.1f)' % tuple(self.ypr_deg) - result += f', solution_type={self.solution_type}, ypr={ypr_str} deg]' + result += f', solution_type={self.solution_type}, ypr={ypr_str} deg, ' \ + f'baseline={self.baseline_distance_m} m]' return result def __str__(self): @@ -1255,7 +1268,9 @@ def __str__(self): GNSS Attitude Output @ {str(self.details.p1_time)} Solution Type: {self.solution_type} 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}""" + 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: @@ -1268,6 +1283,8 @@ def to_numpy(cls, messages: Sequence['GNSSAttitudeOutput']): '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, '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 diff --git a/src/point_one/fusion_engine/messages/measurements.h b/src/point_one/fusion_engine/messages/measurements.h index a1e766e7..5076cfef 100644 --- a/src/point_one/fusion_engine/messages/measurements.h +++ b/src/point_one/fusion_engine/messages/measurements.h @@ -1200,6 +1200,16 @@ struct P1_ALIGNAS(4) GNSSAttitudeOutput : public MessagePayload { * 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 baseline_distance_std_m = NAN; }; /** From c117cd84674edd253dab599d59ba028fed108b4d Mon Sep 17 00:00:00 2001 From: Adam Shapiro Date: Tue, 3 Dec 2024 10:00:45 -0500 Subject: [PATCH 09/12] Removed baseline distance from RawGNSSAttitudeOutput. This is just the norm of the ENU vector contained in the message, so it's redundant. --- python/fusion_engine_client/analysis/analyzer.py | 5 +++-- .../fusion_engine_client/messages/measurements.py | 15 ++++----------- .../fusion_engine/messages/measurements.h | 10 ---------- 3 files changed, 7 insertions(+), 23 deletions(-) diff --git a/python/fusion_engine_client/analysis/analyzer.py b/python/fusion_engine_client/analysis/analyzer.py index ac0cbc7c..0d084c75 100755 --- a/python/fusion_engine_client/analysis/analyzer.py +++ b/python/fusion_engine_client/analysis/analyzer.py @@ -2046,6 +2046,7 @@ def plot_gnss_attitude_measurements(self): 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 @@ -2058,7 +2059,7 @@ def plot_gnss_attitude_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( @@ -2147,7 +2148,7 @@ def plot_gnss_attitude_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)' diff --git a/python/fusion_engine_client/messages/measurements.py b/python/fusion_engine_client/messages/measurements.py index f95c9dae..6708f1bb 100644 --- a/python/fusion_engine_client/messages/measurements.py +++ b/python/fusion_engine_client/messages/measurements.py @@ -1297,7 +1297,7 @@ class RawGNSSAttitudeOutput(MessagePayload): MESSAGE_TYPE = MessageType.RAW_GNSS_ATTITUDE_OUTPUT MESSAGE_VERSION = 0 - _STRUCT = struct.Struct(' int: @@ -1408,8 +1403,6 @@ def to_numpy(cls, messages: Sequence['RawGNSSAttitudeOutput']): '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, - '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 diff --git a/src/point_one/fusion_engine/messages/measurements.h b/src/point_one/fusion_engine/messages/measurements.h index 5076cfef..d25ad09a 100644 --- a/src/point_one/fusion_engine/messages/measurements.h +++ b/src/point_one/fusion_engine/messages/measurements.h @@ -1265,16 +1265,6 @@ struct P1_ALIGNAS(4) RawGNSSAttitudeOutput : public MessagePayload { * resolved with respect to the local ENU tangent plane: east, north, up. */ float position_std_enu_m[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 baseline_distance_std_m = NAN; }; //////////////////////////////////////////////////////////////////////////////// From 29ac75c0feaed93be4ff3029a30371fd81b00b80 Mon Sep 17 00:00:00 2001 From: Adam Shapiro Date: Tue, 3 Dec 2024 10:27:40 -0500 Subject: [PATCH 10/12] Added get_gps_time() helper function. --- python/fusion_engine_client/messages/defs.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/python/fusion_engine_client/messages/defs.py b/python/fusion_engine_client/messages/defs.py index bc4ec613..84fab7dc 100644 --- a/python/fusion_engine_client/messages/defs.py +++ b/python/fusion_engine_client/messages/defs.py @@ -589,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): From aebb1a4ba7f5b589e2cad6e7d15acda3109cd3f1 Mon Sep 17 00:00:00 2001 From: Adam Shapiro Date: Tue, 3 Dec 2024 10:31:26 -0500 Subject: [PATCH 11/12] Add gps_time numpy element if measurements are timestamped in GPS time. --- python/fusion_engine_client/messages/measurement_details.py | 6 ++++++ 1 file changed, 6 insertions(+) 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) From 064ae6806aec3ad7cdc7bc957a8808d4ef26a556 Mon Sep 17 00:00:00 2001 From: Adam Shapiro Date: Tue, 3 Dec 2024 10:32:25 -0500 Subject: [PATCH 12/12] Include GPS time in GNSSAttitudeOutput string if available. --- .../messages/measurements.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/python/fusion_engine_client/messages/measurements.py b/python/fusion_engine_client/messages/measurements.py index 6708f1bb..cc6382d5 100644 --- a/python/fusion_engine_client/messages/measurements.py +++ b/python/fusion_engine_client/messages/measurements.py @@ -1264,8 +1264,17 @@ def __repr__(self): 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"""\ GNSS Attitude Output @ {str(self.details.p1_time)} + GPS time: {gps_str} + UTC time: {utc_str} Solution Type: {self.solution_type} 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} @@ -1385,8 +1394,17 @@ def __repr__(self): 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 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}