From c2ec6cf36beb8f07946dfe6cecf53da210915b51 Mon Sep 17 00:00:00 2001 From: Samuel Toledano Date: Thu, 12 Sep 2024 18:19:02 +0200 Subject: [PATCH 01/10] Add new INS driver sbgECom Implement sbgECom messages handling to provide IMU sensors, GNSS and EKF data to the autopilot Be able to parametrize the serial port, baudrate and the communicating mode Clone sbgECom library only if sbgecom support is enabled and apply a patch Be able to send SBG Systems INS settings in several ways when starting sbgecom driver --- src/drivers/drv_sensor.h | 2 + src/drivers/ins/Kconfig | 3 +- src/drivers/ins/sbgecom/CMakeLists.txt | 60 ++ src/drivers/ins/sbgecom/Kconfig | 5 + src/drivers/ins/sbgecom/module.yaml | 50 ++ src/drivers/ins/sbgecom/sbgecom.cpp | 1102 ++++++++++++++++++++++++ src/drivers/ins/sbgecom/sbgecom.hpp | 294 +++++++ src/drivers/ins/sbgecom/sbgecom.patch | 124 +++ 8 files changed, 1639 insertions(+), 1 deletion(-) create mode 100644 src/drivers/ins/sbgecom/CMakeLists.txt create mode 100644 src/drivers/ins/sbgecom/Kconfig create mode 100644 src/drivers/ins/sbgecom/module.yaml create mode 100644 src/drivers/ins/sbgecom/sbgecom.cpp create mode 100644 src/drivers/ins/sbgecom/sbgecom.hpp create mode 100644 src/drivers/ins/sbgecom/sbgecom.patch diff --git a/src/drivers/drv_sensor.h b/src/drivers/drv_sensor.h index 32eadbb067c3..f7a294e9e238 100644 --- a/src/drivers/drv_sensor.h +++ b/src/drivers/drv_sensor.h @@ -256,6 +256,8 @@ #define DRV_INS_DEVTYPE_MICROSTRAIN 0xEA #define DRV_INS_DEVTYPE_BAHRS 0xEB +#define DRV_INS_DEVTYPE_SBG 0xEB + #define DRV_DEVTYPE_UNUSED 0xff #endif /* _DRV_SENSOR_H */ diff --git a/src/drivers/ins/Kconfig b/src/drivers/ins/Kconfig index 70db3d9fcb82..8b99c3a215ee 100644 --- a/src/drivers/ins/Kconfig +++ b/src/drivers/ins/Kconfig @@ -5,7 +5,8 @@ menu "Inertial Navigation Systems (INS)" select DRIVERS_INS_VECTORNAV select DRIVERS_INS_ILABS select DRIVERS_INS_MICROSTRAIN - select DRIVERS_INS_EULERNAV_BAHRS + select DRIVERS_INS_EULERNAV_BAHRS + select DRIVERS_INS_SBGECOM ---help--- Enable default set of INS sensors rsource "*/Kconfig" diff --git a/src/drivers/ins/sbgecom/CMakeLists.txt b/src/drivers/ins/sbgecom/CMakeLists.txt new file mode 100644 index 000000000000..1235e2c7eeda --- /dev/null +++ b/src/drivers/ins/sbgecom/CMakeLists.txt @@ -0,0 +1,60 @@ +############################################################################ +# +# Copyright (c) 2022 PX4 Development Team. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# 3. Neither the name PX4 nor the names of its contributors may be +# used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED +# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +############################################################################ + +include(FetchContent) + +FetchContent_Declare( + sbgECom + GIT_REPOSITORY https://github.com/SBG-Systems/sbgECom.git + GIT_TAG 5.3.2276-stable + GIT_SHALLOW TRUE + PATCH_COMMAND git apply --reverse --check "${CMAKE_CURRENT_LIST_DIR}/sbgecom.patch" || git apply --ignore-whitespace "${CMAKE_CURRENT_LIST_DIR}/sbgecom.patch" +) + +FetchContent_MakeAvailable(sbgECom) + +px4_add_module( + MODULE drivers__ins__sbgecom + MAIN sbgecom + INCLUDES + ${sbgECom_SOURCE_DIR}/common + ${sbgECom_SOURCE_DIR}/src + COMPILE_FLAGS + SRCS + sbgecom.cpp + sbgecom.hpp + MODULE_CONFIG + module.yaml + DEPENDS + sbgECom + ) diff --git a/src/drivers/ins/sbgecom/Kconfig b/src/drivers/ins/sbgecom/Kconfig new file mode 100644 index 000000000000..2569367c7a48 --- /dev/null +++ b/src/drivers/ins/sbgecom/Kconfig @@ -0,0 +1,5 @@ +menuconfig DRIVERS_INS_SBGECOM + bool "sbgecom" + default n + ---help--- + Enable support for sbgecom diff --git a/src/drivers/ins/sbgecom/module.yaml b/src/drivers/ins/sbgecom/module.yaml new file mode 100644 index 000000000000..c32971e18302 --- /dev/null +++ b/src/drivers/ins/sbgecom/module.yaml @@ -0,0 +1,50 @@ +module_name: sbgECom + +serial_config: + - command: sbgecom start -d ${SERIAL_DEV} + port_config_param: + name: SENS_SBG_CFG + group: Sensors + +parameters: + - group: Sensors + definitions: + SBG_MODE: + description: + short: sbgECom driver mode + long: | + Modes available for sbgECom driver. + In Sensors Only mode, use external IMU and magnetometer. + In GNSS mode, use external GNSS in addition to sensors only mode. + In INS mode, use external Kalman Filter in addition to GNSS mode. + + In INS mode, requires EKF2_EN 0. Keeping both enabled + can lead to an unexpected behavior and vehicle instability. + category: System + type: enum + values: + 0: Sensors Only + 1: GNSS + 2: INS (default) + default: 2 + SBG_BAUDRATE: + description: + short: sbgECom driver baudrate + long: | + Baudrate used by default for serial communication between PX4 + and SBG Systems INS through sbgECom driver. + category: System + type: int32 + min: 9600 + max: 921600 + default: 921600 + reboot_required: true + SBG_CONFIGURE_EN: + description: + short: sbgECom driver INS configuration enable + long: | + Enable SBG Systems INS configuration through sbgECom driver + on start. + category: System + type: boolean + default: 0 diff --git a/src/drivers/ins/sbgecom/sbgecom.cpp b/src/drivers/ins/sbgecom/sbgecom.cpp new file mode 100644 index 000000000000..21153d44137b --- /dev/null +++ b/src/drivers/ins/sbgecom/sbgecom.cpp @@ -0,0 +1,1102 @@ +/**************************************************************************** + * + * Copyright (c) 2012-2022 PX4 Development Team. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name PX4 nor the names of its contributors may be + * used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + ****************************************************************************/ + +/** + * @file sbgecom.cpp + * Driver for the SBG Systems products + * + * @author SBG Systems + */ + +#include "sbgecom.hpp" + +#include +#include + +#include +#include + +#define DEFAULT_DEVNAME "/dev/ttyS0" + +#define SBG_MODE_SENSOR 0 +#define SBG_MODE_GNSS 1 +#define SBG_MODE_INS 2 + +#define SBG_ESTIMATOR_ATTITUDE (1 << 0) ///< 0 - attitude estimate is good +#define SBG_ESTIMATOR_VELOCITY_HORIZ (1 << 1) ///< 1 - horizontal velocity estimate is good +#define SBG_ESTIMATOR_VELOCITY_VERT (1 << 2) ///< 2 - vertical velocity estimate is good +#define SBG_ESTIMATOR_POS_HORIZ_REL (1 << 3) ///< 3 - horizontal position (relative) estimate is good +#define SBG_ESTIMATOR_POS_HORIZ_ABS (1 << 4) ///< 4 - horizontal position (absolute) estimate is good +#define SBG_ESTIMATOR_POS_VERT_ABS (1 << 5) ///< 5 - vertical position (absolute) estimate is good +#define SBG_ESTIMATOR_POS_VERT_AGL (1 << 6) ///< 6 - vertical position (above ground) estimate is good +#define SBG_ESTIMATOR_CONST_POS_MODE (1 << 7) ///< 7 - EKF is in a constant position mode and is not using external measurements (eg GPS or optical flow) +#define SBG_ESTIMATOR_PRED_POS_HORIZ_REL (1 << 8) ///< 8 - EKF has sufficient data to enter a mode that will provide a (relative) position estimate +#define SBG_ESTIMATOR_PRED_POS_HORIZ_ABS (1 << 9) ///< 9 - EKF has sufficient data to enter a mode that will provide a (absolute) position estimate +#define SBG_ESTIMATOR_GPS_GLITCH (1 << 10) ///< 10 - EKF has detected a GPS glitch +#define SBG_ESTIMATOR_ACCEL_ERROR (1 << 11) ///< 11 - EKF has detected bad accelerometer data + +#define DEFAULT_CONFIG_PATH "/etc/extras/sbg_settings.json" +#define OVERRIDE_CONFIG_PATH CONFIG_BOARD_ROOT_PATH DEFAULT_CONFIG_PATH +#define NEED_REBOOT_STR "\"needReboot\":true" + +using matrix::Vector2f; + +SbgEcom::SbgEcom(const char *device_name, uint32_t baudrate, const char *config_file, const char *config_string): + ModuleParams(nullptr), + ScheduledWorkItem(MODULE_NAME, px4::serial_port_to_wq(device_name)), + _baudrate(baudrate), + _config_file(config_file), + _config_string(config_string) +{ + if (device_name) { + strncpy(_device_name, device_name, sizeof(_device_name) - 1); + _device_name[sizeof(_device_name) - 1] = '\0'; + } + + device::Device::DeviceId device_id{}; + device_id.devid_s.bus_type = device::Device::DeviceBusType_SERIAL; + device_id.devid_s.devtype = DRV_INS_DEVTYPE_SBG; + + set_device_id(device_id.devid); + _px4_accel.set_device_id(device_id.devid); + _px4_gyro.set_device_id(device_id.devid); + _px4_mag.set_device_id(device_id.devid); +} + +SbgEcom::~SbgEcom() +{ + sbgEComClose(&_com_handle); + sbgInterfaceDestroy(&_sbg_interface); + + perf_free(_accel_pub_interval_perf); + perf_free(_gyro_pub_interval_perf); + perf_free(_mag_pub_interval_perf); + perf_free(_gnss_pub_interval_perf); + + perf_free(_attitude_pub_interval_perf); + perf_free(_local_position_pub_interval_perf); + perf_free(_global_position_pub_interval_perf); +} + +void SbgEcom::set_device_id(uint32_t device_id) +{ + _device_id = device_id; +} + +uint32_t SbgEcom::get_device_id() +{ + return _device_id; +} + +SbgErrorCode SbgEcom::getAndPrintProductInfo(SbgEComHandle *handle) +{ + SbgErrorCode error_code; + SbgEComDeviceInfo device_info; + + assert(handle); + + error_code = sbgEComCmdGetInfo(handle, &device_info); + + if (error_code == SBG_NO_ERROR) { + char calib_version_str[32]; + char hw_revision_str[32]; + char fmw_version_str[32]; + + sbgVersionToStringEncoded(device_info.calibationRev, calib_version_str, sizeof(calib_version_str)); + sbgVersionToStringEncoded(device_info.hardwareRev, hw_revision_str, sizeof(hw_revision_str)); + sbgVersionToStringEncoded(device_info.firmwareRev, fmw_version_str, sizeof(fmw_version_str)); + + PX4_INFO(" Serial Number: %09" PRIu32, device_info.serialNumber); + PX4_INFO(" Product Code: %s", device_info.productCode); + PX4_INFO(" Hardware Revision: %s", hw_revision_str); + PX4_INFO(" Firmware Version: %s", fmw_version_str); + PX4_INFO(" Calib. Version: %s", calib_version_str); + + } else { + SBG_LOG_WARNING(error_code, "Unable to retrieve device information"); + } + + return error_code; +} + +void SbgEcom::printLogCallBack(const char *file_name, const char *function_name, uint32_t line, const char *category, + SbgDebugLogType log_type, SbgErrorCode error_code, const char *message) +{ + const char *base_name; + + assert(file_name); + assert(function_name); + assert(category); + assert(message); + + base_name = strrchr(file_name, '/'); + + if (!base_name) { + base_name = file_name; + + } else { + base_name++; + } + + switch (log_type) { + case SBG_DEBUG_LOG_TYPE_DEBUG: + PX4_DEBUG("%s:%" PRIu32 ": %s: %s", base_name, line, function_name, message); + break; + + case SBG_DEBUG_LOG_TYPE_INFO: + PX4_INFO("%s:%" PRIu32 ": %s: %s", base_name, line, function_name, message); + break; + + case SBG_DEBUG_LOG_TYPE_WARNING: + PX4_WARN("%s:%" PRIu32 ": %s: err:%s: %s", base_name, line, function_name, sbgErrorCodeToString(error_code), message); + break; + + case SBG_DEBUG_LOG_TYPE_ERROR: + PX4_ERR("%s:%" PRIu32 ": %s: err:%s: %s", base_name, line, function_name, sbgErrorCodeToString(error_code), message); + break; + } +} + +void SbgEcom::handleLogImuShort(const SbgEComLogUnion *ref_sbg_data, void *user_arg) +{ + assert(ref_sbg_data); + assert(user_arg); + + SbgEcom *instance = static_cast(user_arg); + + const float temperature = sbgEComLogImuShortGetTemperature(&ref_sbg_data->imuShort); + + // publish sensor_accel + instance->_px4_accel.update(hrt_absolute_time(), + sbgEComLogImuShortGetDeltaVelocity(&ref_sbg_data->imuShort, 0), + sbgEComLogImuShortGetDeltaVelocity(&ref_sbg_data->imuShort, 1), + sbgEComLogImuShortGetDeltaVelocity(&ref_sbg_data->imuShort, 2)); + instance->_px4_accel.set_error_count(perf_event_count(instance->_comms_errors)); + instance->_px4_accel.set_temperature(temperature); + perf_count(instance->_accel_pub_interval_perf); + + // publish sensor_gyro + instance->_px4_gyro.update(hrt_absolute_time(), + sbgEComLogImuShortGetDeltaAngle(&ref_sbg_data->imuShort, 0), + sbgEComLogImuShortGetDeltaAngle(&ref_sbg_data->imuShort, 1), + sbgEComLogImuShortGetDeltaAngle(&ref_sbg_data->imuShort, 2)); + instance->_px4_gyro.set_error_count(perf_event_count(instance->_comms_errors)); + instance->_px4_gyro.set_temperature(temperature); + perf_count(instance->_gyro_pub_interval_perf); +} + +void SbgEcom::handleLogMag(const SbgEComLogUnion *ref_sbg_data, void *user_arg) +{ + assert(ref_sbg_data); + assert(user_arg); + + SbgEcom *instance = static_cast(user_arg); + + // publish sensor_mag + instance->_px4_mag.update(ref_sbg_data->magData.timeStamp, + (ref_sbg_data->magData.magnetometers[0]), + (ref_sbg_data->magData.magnetometers[1]), + (ref_sbg_data->magData.magnetometers[2])); + instance->_px4_mag.set_error_count(perf_event_count(instance->_comms_errors)); + perf_count(instance->_mag_pub_interval_perf); +} + +void SbgEcom::updateEstimatorStatus(uint32_t ekf_status, estimator_status_s *estimator_status) +{ + SbgEComSolutionMode ekf_nav_status = sbgEComLogEkfGetSolutionMode(ekf_status); + + estimator_status->solution_status_flags |= ((ekf_status & SBG_ECOM_SOL_ATTITUDE_VALID) + && (ekf_status & SBG_ECOM_SOL_HEADING_VALID)) ? SBG_ESTIMATOR_ATTITUDE : 0; + estimator_status->solution_status_flags |= (ekf_status & SBG_ECOM_SOL_VELOCITY_VALID) ? SBG_ESTIMATOR_VELOCITY_HORIZ : + 0; + estimator_status->solution_status_flags |= (ekf_status & SBG_ECOM_SOL_VELOCITY_VALID) ? SBG_ESTIMATOR_VELOCITY_VERT : 0; + estimator_status->solution_status_flags |= (ekf_status & SBG_ECOM_SOL_POSITION_VALID) ? SBG_ESTIMATOR_POS_HORIZ_REL : 0; + estimator_status->solution_status_flags |= (ekf_status & SBG_ECOM_SOL_POSITION_VALID) ? SBG_ESTIMATOR_POS_HORIZ_ABS : 0; + estimator_status->solution_status_flags |= (ekf_status & SBG_ECOM_SOL_POSITION_VALID) ? SBG_ESTIMATOR_POS_VERT_ABS : 0; + estimator_status->solution_status_flags |= (ekf_status & SBG_ECOM_SOL_POSITION_VALID) ? SBG_ESTIMATOR_POS_VERT_AGL : 0; + estimator_status->solution_status_flags |= (ekf_status & SBG_ECOM_SOL_ZUPT_USED) ? SBG_ESTIMATOR_CONST_POS_MODE : 0; + + estimator_status->solution_status_flags |= (ekf_nav_status == SBG_ECOM_SOL_MODE_NAV_POSITION) ? + SBG_ESTIMATOR_PRED_POS_HORIZ_REL : 0; + estimator_status->solution_status_flags |= (ekf_nav_status == SBG_ECOM_SOL_MODE_NAV_POSITION) ? + SBG_ESTIMATOR_PRED_POS_HORIZ_ABS : 0; +} + +void SbgEcom::handleLogEkfQuat(const SbgEComLogUnion *ref_sbg_data, void *user_arg) +{ + const hrt_abstime time_now_us = hrt_absolute_time(); + + assert(ref_sbg_data); + assert(user_arg); + + SbgEcom *instance = static_cast(user_arg); + + // publish estimator_status + estimator_status_s estimator_status{}; + estimator_status.timestamp = time_now_us; + estimator_status.timestamp_sample = ref_sbg_data->ekfQuatData.timeStamp; + estimator_status.accel_device_id = instance->get_device_id(); + estimator_status.gyro_device_id = instance->get_device_id(); + estimator_status.mag_device_id = instance->get_device_id(); + + instance->updateEstimatorStatus(ref_sbg_data->ekfQuatData.status, &estimator_status); + + instance->_estimator_status_pub.publish(estimator_status); + + // publish attitude + vehicle_attitude_s attitude{}; + + attitude.timestamp = time_now_us; + attitude.timestamp_sample = ref_sbg_data->ekfQuatData.timeStamp; + + attitude.q[0] = ref_sbg_data->ekfQuatData.quaternion[0]; + attitude.q[1] = ref_sbg_data->ekfQuatData.quaternion[1]; + attitude.q[2] = ref_sbg_data->ekfQuatData.quaternion[2]; + attitude.q[3] = ref_sbg_data->ekfQuatData.quaternion[3]; + + instance->_attitude_pub.publish(attitude); + perf_count(instance->_attitude_pub_interval_perf); + + matrix::Quatf q{attitude.q}; + instance->_heading = matrix::Eulerf{q}.psi(); +} + +void SbgEcom::handleLogEkfNav(const SbgEComLogUnion *ref_sbg_data, void *user_arg) +{ + const hrt_abstime time_now_us = hrt_absolute_time(); + + assert(ref_sbg_data); + assert(user_arg); + + SbgEcom *instance = static_cast(user_arg); + + // publish estimator_status + estimator_status_s estimator_status{}; + estimator_status.timestamp = time_now_us; + estimator_status.timestamp_sample = ref_sbg_data->ekfNavData.timeStamp; + + instance->updateEstimatorStatus(ref_sbg_data->ekfNavData.status, &estimator_status); + + instance->_estimator_status_pub.publish(estimator_status); + + SbgEComSolutionMode ekf_nav_status = sbgEComLogEkfGetSolutionMode(ref_sbg_data->ekfNavData.status); + + // don't publish local and global positions if not reliable + if (ekf_nav_status != SBG_ECOM_SOL_MODE_NAV_POSITION) { + return; + } + + const double latitude = ref_sbg_data->ekfNavData.position[0]; + const double longitude = ref_sbg_data->ekfNavData.position[1]; + const double altitude = ref_sbg_data->ekfNavData.position[2]; + + const double north_velocity = ref_sbg_data->ekfNavData.velocity[0]; + const double east_velocity = ref_sbg_data->ekfNavData.velocity[1]; + const double down_velocity = ref_sbg_data->ekfNavData.velocity[2]; + + if (!instance->_pos_ref.isInitialized()) { + instance->_pos_ref.initReference(latitude, longitude, time_now_us); + instance->_gps_alt_ref = altitude; + } + + const Vector2f pos_ned = instance->_pos_ref.project(latitude, longitude); + + // publish local_position + vehicle_local_position_s local_position{}; + + local_position.timestamp = time_now_us; + local_position.timestamp_sample = ref_sbg_data->ekfNavData.timeStamp; + + local_position.xy_valid = math::isFinite(latitude) && math::isFinite(longitude); + local_position.z_valid = math::isFinite(altitude); + local_position.v_xy_valid = math::isFinite(north_velocity) && math::isFinite(east_velocity); + local_position.v_z_valid = math::isFinite(down_velocity); + + local_position.x = pos_ned(0); + local_position.y = pos_ned(1); + local_position.z = -(altitude - instance->_gps_alt_ref); + + local_position.vx = north_velocity; + local_position.vy = east_velocity; + local_position.vz = down_velocity; + + local_position.heading = instance->_heading; + local_position.heading_good_for_control = true;; + + if (instance->_pos_ref.isInitialized()) { + local_position.xy_global = true; + local_position.z_global = true; + local_position.ref_timestamp = instance->_pos_ref.getProjectionReferenceTimestamp(); + local_position.ref_lat = instance->_pos_ref.getProjectionReferenceLat(); + local_position.ref_lon = instance->_pos_ref.getProjectionReferenceLon(); + local_position.ref_alt = instance->_gps_alt_ref; + } + + local_position.dist_bottom_valid = false; + + local_position.eph = sqrt(pow(ref_sbg_data->ekfNavData.positionStdDev[0], 2) + + pow(ref_sbg_data->ekfNavData.positionStdDev[1], 2)); + local_position.epv = ref_sbg_data->ekfNavData.positionStdDev[2]; + local_position.evh = sqrt(pow(ref_sbg_data->ekfNavData.velocityStdDev[0], 2) + + pow(ref_sbg_data->ekfNavData.velocityStdDev[1], 2)); + local_position.evv = ref_sbg_data->ekfNavData.velocityStdDev[2]; + + + local_position.dead_reckoning = false; + + local_position.vxy_max = INFINITY; + local_position.vz_max = INFINITY; + local_position.hagl_min = INFINITY; + local_position.hagl_max_xy = INFINITY; + local_position.hagl_max_z = INFINITY; + + instance->_local_position_pub.publish(local_position); + perf_count(instance->_local_position_pub_interval_perf); + + // publish global_position + vehicle_global_position_s global_position{}; + + global_position.timestamp = time_now_us; + global_position.timestamp_sample = ref_sbg_data->ekfNavData.timeStamp; + + global_position.lat = latitude; + global_position.lon = longitude; + global_position.alt = altitude; + global_position.alt_ellipsoid = ref_sbg_data->ekfNavData.undulation; + + global_position.lat_lon_valid = math::isFinite(latitude) && math::isFinite(longitude); + global_position.alt_valid = math::isFinite(altitude); + + global_position.eph = sqrt(pow(ref_sbg_data->ekfNavData.positionStdDev[0], 2) + + pow(ref_sbg_data->ekfNavData.positionStdDev[1], 2)); + global_position.epv = ref_sbg_data->ekfNavData.positionStdDev[2]; + + global_position.dead_reckoning = false; + + instance->_global_position_pub.publish(global_position); + perf_count(instance->_global_position_pub_interval_perf); +} + +void SbgEcom::handleLogGnssPosVelHdt(SbgEComMsgId msg, const SbgEComLogUnion *ref_sbg_data, void *user_arg) +{ + const hrt_abstime time_now_us = hrt_absolute_time(); + uint8_t type; + uint8_t state; + uint8_t spoofing; + + assert(msg); + assert(ref_sbg_data); + assert(user_arg); + + SbgEcom *instance = static_cast(user_arg); + GnssData *gnss_data = &instance->gnss_data; + + // Store the data based on its type + switch (msg) { + case SBG_ECOM_LOG_GPS1_POS: + gnss_data->gps_pos = ref_sbg_data->gpsPosData; + gnss_data->pos_received = true; + gnss_data->pos_timestamp = time_now_us; + break; + + case SBG_ECOM_LOG_GPS1_VEL: + gnss_data->gps_vel = ref_sbg_data->gpsVelData; + gnss_data->vel_received = true; + gnss_data->vel_timestamp = time_now_us; + break; + + case SBG_ECOM_LOG_GPS1_HDT: + gnss_data->gps_hdt = ref_sbg_data->gpsHdtData; + gnss_data->hdt_received = true; + gnss_data->hdt_timestamp = time_now_us; + break; + } + + if (gnss_data->pos_received && gnss_data->vel_received && gnss_data->hdt_received) { + // publish sensor_gps + sensor_gps_s sensor_gps{}; + + sensor_gps.timestamp = time_now_us; + sensor_gps.timestamp_sample = gnss_data->gps_pos.timeStamp; + + sensor_gps.device_id = instance->get_device_id(); + + sensor_gps.latitude_deg = gnss_data->gps_pos.latitude; + sensor_gps.longitude_deg = gnss_data->gps_pos.longitude; + sensor_gps.altitude_msl_m = gnss_data->gps_pos.altitude; + sensor_gps.altitude_ellipsoid_m = gnss_data->gps_pos.undulation; + + sensor_gps.s_variance_m_s = sqrt(pow(gnss_data->gps_vel.velocityAcc[0], 2) + + pow(gnss_data->gps_vel.velocityAcc[1], 2) + + pow(gnss_data->gps_vel.velocityAcc[2], 2)); + sensor_gps.c_variance_rad = math::radians(gnss_data->gps_vel.courseAcc); + + type = sbgEComLogGnssPosGetType(&gnss_data->gps_pos); + + switch (type) { + case SBG_ECOM_GNSS_POS_TYPE_NO_SOLUTION: + sensor_gps.fix_type = 0; + break; + + case SBG_ECOM_GNSS_POS_TYPE_PSRDIFF: + case SBG_ECOM_GNSS_POS_TYPE_SBAS: + sensor_gps.fix_type = 4; + break; + + case SBG_ECOM_GNSS_POS_TYPE_RTK_FLOAT: + sensor_gps.fix_type = 5; + break; + + case SBG_ECOM_GNSS_POS_TYPE_RTK_INT: + sensor_gps.fix_type = 6; + break; + + case SBG_ECOM_GNSS_POS_TYPE_FIXED: + sensor_gps.fix_type = 7; + break; + + case SBG_ECOM_GNSS_POS_TYPE_PPP_FLOAT: + case SBG_ECOM_GNSS_POS_TYPE_PPP_INT: + sensor_gps.fix_type = 8; + break; + + default: + sensor_gps.fix_type = 3; + break; + } + + sensor_gps.eph = sqrt(pow(gnss_data->gps_pos.longitudeAccuracy, 2) + + pow(gnss_data->gps_pos.latitudeAccuracy, 2)); + sensor_gps.epv = gnss_data->gps_pos.altitudeAccuracy; + + state = sbgEComLogGnssPosGetIfmStatus(&gnss_data->gps_pos); + + switch (state) { + case SBG_ECOM_GNSS_IFM_STATUS_UNKNOWN: + sensor_gps.jamming_state = 0; + break; + + case SBG_ECOM_GNSS_IFM_STATUS_CLEAN: + sensor_gps.jamming_state = 1; + break; + + case SBG_ECOM_GNSS_IFM_STATUS_MITIGATED: + sensor_gps.jamming_state = 2; + break; + + case SBG_ECOM_GNSS_IFM_STATUS_CRITICAL: + sensor_gps.jamming_state = 3; + break; + } + + spoofing = sbgEComLogGnssPosGetSpoofingStatus(&gnss_data->gps_pos); + + switch (spoofing) { + case SBG_ECOM_GNSS_SPOOFING_STATUS_UNKNOWN: + sensor_gps.spoofing_state = 0; + break; + + case SBG_ECOM_GNSS_SPOOFING_STATUS_CLEAN: + sensor_gps.spoofing_state = 1; + break; + + case SBG_ECOM_GNSS_SPOOFING_STATUS_SINGLE: + sensor_gps.spoofing_state = 2; + break; + + case SBG_ECOM_GNSS_SPOOFING_STATUS_MULTIPLE: + sensor_gps.spoofing_state = 3; + break; + } + + sensor_gps.vel_m_s = sqrt(pow(gnss_data->gps_vel.velocity[0], 2) + + pow(gnss_data->gps_vel.velocity[1], 2) + + pow(gnss_data->gps_vel.velocity[2], 2)); + sensor_gps.vel_n_m_s = gnss_data->gps_vel.velocity[0]; + sensor_gps.vel_e_m_s = gnss_data->gps_vel.velocity[1]; + sensor_gps.vel_d_m_s = gnss_data->gps_vel.velocity[2]; + sensor_gps.vel_ned_valid = true; + + sensor_gps.cog_rad = math::radians(gnss_data->gps_vel.course); + + sensor_gps.timestamp_time_relative = sensor_gps.timestamp_sample - time_now_us; + sensor_gps.time_utc_usec = 0; + + sensor_gps.satellites_used = gnss_data->gps_pos.numSvUsed; + + sensor_gps.heading = math::radians(gnss_data->gps_hdt.heading); + sensor_gps.heading_offset = math::radians(gnss_data->gps_hdt.pitch); + sensor_gps.heading_accuracy = math::radians(gnss_data->gps_hdt.headingAccuracy); + + // Check timestamp synchronization + const hrt_abstime max_time_diff = 1000000; // Maximum allowed time difference in microseconds (e.g., 1 second) + hrt_abstime pos_time = gnss_data->pos_timestamp; + hrt_abstime vel_time = gnss_data->vel_timestamp; + hrt_abstime hdt_time = gnss_data->hdt_timestamp; + + if (((time_now_us - pos_time) < max_time_diff) && + ((time_now_us - vel_time) < max_time_diff) && + ((time_now_us - hdt_time) < max_time_diff) && + ((pos_time - vel_time) < max_time_diff) && + ((pos_time - hdt_time) < max_time_diff) && + ((vel_time - hdt_time) < max_time_diff)) { + instance->_sensor_gps_pub.publish(sensor_gps); + perf_count(instance->_gnss_pub_interval_perf); + } + + // Reset the flags and timestamps + gnss_data->pos_received = false; + gnss_data->vel_received = false; + gnss_data->hdt_received = false; + + gnss_data->pos_timestamp = 0; + gnss_data->vel_timestamp = 0; + gnss_data->hdt_timestamp = 0; + } +} + +SbgErrorCode SbgEcom::onLogReceived(SbgEComHandle *handle, SbgEComClass msg_class, SbgEComMsgId msg, + const SbgEComLogUnion *ref_sbg_data, void *user_arg) +{ + SBG_UNUSED_PARAMETER(handle); + + assert(msg_class); + assert(msg); + assert(ref_sbg_data); + assert(user_arg); + + SbgEcom *instance = static_cast(user_arg); + + if (msg_class == SBG_ECOM_CLASS_LOG_ECOM_0) { + int32_t mode; + int32_t ekf2_en; + param_get(param_find("SBG_MODE"), &mode); + param_get(param_find("EKF2_EN"), &ekf2_en); + + bool ekf_failure = (ekf2_en && mode == SBG_MODE_INS); + + if (!instance->_ekf_failure && ekf_failure) { + PX4_WARN("Both SBG EKF and EKF2 are configured, this can lead to an unexpected behaviour"); + instance->_ekf_failure = true; + + } else if (instance->_ekf_failure && !ekf_failure) { + PX4_INFO("EKF management is back to good"); + instance->_ekf_failure = false; + } + + switch (msg) { + case SBG_ECOM_LOG_IMU_SHORT: + instance->handleLogImuShort(ref_sbg_data, user_arg); + break; + + case SBG_ECOM_LOG_MAG: + instance->handleLogMag(ref_sbg_data, user_arg); + break; + + case SBG_ECOM_LOG_GPS1_POS: + case SBG_ECOM_LOG_GPS1_VEL: + case SBG_ECOM_LOG_GPS1_HDT: + if (mode == SBG_MODE_GNSS || mode == SBG_MODE_INS) { + instance->handleLogGnssPosVelHdt(msg, ref_sbg_data, user_arg); + } + + break; + + case SBG_ECOM_LOG_EKF_QUAT: + if (mode == SBG_MODE_INS) { + instance->handleLogEkfQuat(ref_sbg_data, user_arg); + } + + break; + + case SBG_ECOM_LOG_EKF_NAV: + if (mode == SBG_MODE_INS) { + instance->handleLogEkfNav(ref_sbg_data, user_arg); + } + + break; + + default: + PX4_DEBUG("Received unknown SBG message (class %u, id %u)", msg_class, msg); + break; + } + + } else { + PX4_INFO("Received message from unsupported SBGEcom class %u", msg_class); + } + + return SBG_NO_ERROR; +} + +SbgErrorCode SbgEcom::handleOneLog(SbgEComHandle *handle) +{ + SbgErrorCode error_code; + SbgEComProtocolPayload payload; + uint8_t received_msg; + uint8_t received_msg_class; + + assert(handle); + + sbgEComProtocolPayloadConstruct(&payload); + + perf_begin(_sample_perf); + + error_code = sbgEComProtocolReceive2(&handle->protocolHandle, &received_msg_class, &received_msg, &payload); + + if (error_code == SBG_NO_ERROR) { + if (sbgEComMsgClassIsALog((SbgEComClass)received_msg_class)) { + error_code = sbgEComLogParse((SbgEComClass)received_msg_class, (SbgEComMsgId)received_msg, + sbgEComProtocolPayloadGetBuffer(&payload), sbgEComProtocolPayloadGetSize(&payload), &_log_data); + + if (error_code == SBG_NO_ERROR) { + if (handle->pReceiveLogCallback) { + error_code = handle->pReceiveLogCallback(handle, (SbgEComClass)received_msg_class, (SbgEComMsgId)received_msg, + &_log_data, handle->pUserArg); + } + + sbgEComLogCleanup(&_log_data, (SbgEComClass)received_msg_class, (SbgEComMsgId)received_msg); + + perf_end(_sample_perf); + + } else { + perf_count(_comms_errors); + } + + } else { + PX4_ERR("command received %d", error_code); + } + + } else if (error_code != SBG_NOT_READY) { + PX4_WARN("Invalid frame received %d", error_code); + perf_count(_comms_errors); + } + + sbgEComProtocolPayloadDestroy(&payload); + + return error_code; +} + +SbgErrorCode SbgEcom::sendAirDataLog(SbgEComHandle *handle, SbgEcom *instance) +{ + SbgErrorCode error_code = SBG_NO_ERROR; + SbgEComLogAirData air_data_log; + uint8_t output_buffer[64]; + SbgStreamBuffer output_stream; + + assert(handle); + assert(instance); + + vehicle_air_data_s air_data{}; + + if (instance->_air_data_sub.update(&air_data)) { + memset(&air_data_log, 0x00, sizeof(air_data_log)); + + air_data_log.timeStamp = hrt_absolute_time() - air_data.timestamp; + air_data_log.status |= SBG_ECOM_AIR_DATA_TIME_IS_DELAY; + + air_data_log.pressureAbs = air_data.baro_pressure_pa; + air_data_log.status |= SBG_ECOM_AIR_DATA_PRESSURE_ABS_VALID; + + air_data_log.altitude = air_data.baro_alt_meter; + air_data_log.status |= SBG_ECOM_AIR_DATA_ALTITUDE_VALID; + + air_data_log.airTemperature = air_data.ambient_temperature; + air_data_log.status |= SBG_ECOM_AIR_DATA_TEMPERATURE_VALID; + + differential_pressure_s differential_pressure{}; + + if (instance->_diff_pressure_sub.update(&differential_pressure)) { + air_data_log.pressureDiff = differential_pressure.differential_pressure_pa; + air_data_log.status |= SBG_ECOM_AIR_DATA_PRESSURE_DIFF_VALID; + } + + airspeed_s airspeed{}; + + if (instance->_airspeed_sub.update(&airspeed)) { + air_data_log.trueAirspeed = airspeed.true_airspeed_m_s; + air_data_log.status |= SBG_ECOM_AIR_DATA_AIRPSEED_VALID; + } + + sbgStreamBufferInitForWrite(&output_stream, output_buffer, sizeof(output_buffer)); + + perf_begin(_write_perf); + + error_code = sbgEComLogAirDataWriteToStream(&air_data_log, &output_stream); + + if (error_code == SBG_NO_ERROR) { + error_code = sbgEComProtocolSend(&handle->protocolHandle, SBG_ECOM_CLASS_LOG_ECOM_0, SBG_ECOM_LOG_AIR_DATA, + sbgStreamBufferGetLinkedBuffer(&output_stream), sbgStreamBufferGetLength(&output_stream)); + + if (error_code != SBG_NO_ERROR) { + PX4_ERR("Unable to send the AirData log %d", error_code); + + } else { + perf_end(_write_perf); + } + + } else { + PX4_ERR("Unable to write the AirData payload. %d", error_code); + } + } + + return error_code; +} + +SbgErrorCode SbgEcom::sendMagLog(SbgEComHandle *handle, SbgEcom *instance) +{ + SbgErrorCode error_code = SBG_NO_ERROR; + SbgEComLogMag mag_log; + uint8_t output_buffer[64]; + SbgStreamBuffer output_stream; + + assert(handle); + assert(instance); + + vehicle_magnetometer_s mag{}; + + if (instance->_mag_sub.update(&mag)) { + memset(&mag_log, 0x00, sizeof(mag_log)); + + mag_log.timeStamp = mag.timestamp_sample; + // mag_log.status = 0; // STO: don't know how to set it + + mag_log.magnetometers[0] = mag.magnetometer_ga[0]; + mag_log.magnetometers[1] = mag.magnetometer_ga[1]; + mag_log.magnetometers[2] = mag.magnetometer_ga[2]; + + sbgStreamBufferInitForWrite(&output_stream, output_buffer, sizeof(output_buffer)); + + perf_begin(_write_perf); + + error_code = sbgEComLogMagWriteToStream(&mag_log, &output_stream); + + if (error_code == SBG_NO_ERROR) { + error_code = sbgEComProtocolSend(&handle->protocolHandle, SBG_ECOM_CLASS_LOG_ECOM_0, SBG_ECOM_LOG_MAG, + sbgStreamBufferGetLinkedBuffer(&output_stream), sbgStreamBufferGetLength(&output_stream)); + + if (error_code != SBG_NO_ERROR) { + PX4_ERR("Unable to send the Mag log %d", error_code); + + } else { + perf_end(_write_perf); + } + + } else { + PX4_ERR("Unable to write the Mag payload. %d", error_code); + } + } + + return error_code; +} + +void SbgEcom::send_config(SbgEComHandle *pHandle, const char *config) +{ + SbgEComCmdApiReply reply; + + assert(pHandle); + assert(config); + + sbgEComCmdApiReplyConstruct(&reply); + + sbgEComCmdApiPost(pHandle, "/api/v1/settings", NULL, config, &reply); + + if (!sbgEComCmdApiReplySuccessful(&reply)) { + PX4_ERR("Fail to apply SBG configuration: %s", reply.pContent); + + } else { + bool need_reboot = (strstr(reply.pContent, NEED_REBOOT_STR) != NULL); + sbgEComCmdApiPost(pHandle, "/api/v1/settings/save", NULL, NULL, &reply); + + if (need_reboot) { + PX4_INFO("Reboot SBG device"); + sbgEComCmdApiPost(pHandle, "/api/v1/system/reboot", NULL, NULL, &reply); + } + } + + sbgEComCmdApiReplyDestroy(&reply); +} + +void SbgEcom::send_config_file(SbgEComHandle *pHandle, const char *file_path) +{ + int fd; + char *body = NULL; + struct stat s; + + assert(pHandle); + assert(file_path); + + fd = open(file_path, O_RDONLY); + + if (fd == -1) { + PX4_ERR("Failed to open config"); + return; + } + + fstat(fd, &s); + body = (char *)malloc(s.st_size + 1); + + if (!body) { + PX4_ERR("Failed to allocate memory (%ld) - %s", s.st_size + 1, strerror(get_errno())); + close(fd); + return; + } + + read(fd, body, s.st_size); + body[s.st_size] = '\0'; + + send_config(pHandle, body); + + free(body); + + if (close(fd) == -1) { + perror("Error closing the file"); + return; + } +} + +int SbgEcom::init() +{ + SbgErrorCode error_code; + + error_code = sbgInterfaceSerialCreate(&_sbg_interface, _device_name, _baudrate); + + if (error_code == SBG_NO_ERROR) { + PX4_INFO("Serial interface created successfully on port: %s, baudrate: %ld", _device_name, _baudrate); + } + + error_code = sbgEComInit(&_com_handle, &_sbg_interface); + // Increase sbgECom timeout for the initialization + sbgEComSetCmdTrialsAndTimeOut(&_com_handle, 3, 5000); + + if (error_code == SBG_NO_ERROR) { + int32_t ins_config_enable; + param_get(param_find("SBG_CONFIGURE_EN"), &ins_config_enable); + + getAndPrintProductInfo(&_com_handle); + + if (ins_config_enable) { + if (_config_string != nullptr) { + PX4_INFO("Apply config string instead of config file"); + send_config(&_com_handle, _config_string); + + } else { + send_config_file(&_com_handle, _config_file); + } + } + + // Reset sbgECom timeout to its defaut value + sbgEComSetCmdTrialsAndTimeOut(&_com_handle, 3, SBG_ECOM_DEFAULT_CMD_TIME_OUT); + sbgEComSetReceiveLogCallback(&_com_handle, onLogReceived, this); + return PX4_OK; + + } else { + PX4_ERR("sbgECom initialization failed (%d)", error_code); + return PX4_ERROR; + } +} + +void SbgEcom::Run() +{ + if (should_exit()) { + ScheduleClear(); + exit_and_cleanup(); + return; + } + + if (!_initialized) { + init_result = init(); + _initialized = true; + } + + if (init_result == PX4_OK) { + SbgErrorCode error_code; + + error_code = handleOneLog(&_com_handle); + + if (error_code == SBG_NO_ERROR) { + ScheduleDelayed(time_literals::operator ""_ms(0)); + + if (failure) { + assert(iteration_count >= 0); + iteration_count--; + failure = false; + } + + } else if (error_code != SBG_NOT_READY) { + PX4_ERR("Unable to process incoming sbgECom logs %d", error_code); + } + + if (error_code != SBG_NO_ERROR) { + ScheduleDelayed(time_literals::operator ""_ms(1)); + failure = true; + } + + error_code = sendAirDataLog(&_com_handle, this); + + if (error_code != SBG_NO_ERROR) { + PX4_WARN("Unable to send AirData log %d", error_code); + } + + error_code = sendMagLog(&_com_handle, this); + + if (error_code != SBG_NO_ERROR) { + PX4_WARN("Unable to send Mag log %d", error_code); + } + } +} + +int SbgEcom::task_spawn(int argc, char **argv) +{ + sbgCommonLibSetLogCallback(printLogCallBack); + + bool error_flag = false; + + const char *myoptarg = nullptr; + int myoptind = 1; + int ch; + + int32_t baudrate; + param_get(param_find("SBG_BAUDRATE"), &baudrate); + const char *dev_name = DEFAULT_DEVNAME; + const char *config_file = DEFAULT_CONFIG_PATH; + + /* INS settings can be overwritten from the SD card */ + if (access(OVERRIDE_CONFIG_PATH, F_OK) == 0) { + config_file = OVERRIDE_CONFIG_PATH; + + } else { + config_file = DEFAULT_CONFIG_PATH; + } + + const char *config_string = nullptr; + + while ((ch = px4_getopt(argc, argv, "b:d:f:s:", &myoptind, &myoptarg)) != EOF) { + switch (ch) { + case 'b': + baudrate = atoi(myoptarg); + break; + + case 'd': + dev_name = myoptarg; + break; + + case 'f': + config_file = myoptarg; + break; + + case 's': + config_string = myoptarg; + break; + + case '?': + PX4_WARN("unrecognized flag ?"); + error_flag = true; + break; + + default: + PX4_WARN("unrecognized flag"); + error_flag = true; + break; + } + } + + if (error_flag) { + return -1; + } + + if (dev_name && (access(dev_name, R_OK | W_OK) == 0)) { + SbgEcom *instance = new SbgEcom(dev_name, baudrate, config_file, config_string); + + if (instance == nullptr) { + PX4_ERR("alloc failed"); + return PX4_ERROR; + } + + _task_id = task_id_is_work_queue; + _object.store(instance); + instance->ScheduleNow(); + return PX4_OK; + + } else { + if (dev_name) { + PX4_ERR("invalid device (-d) %s", dev_name); + + } else { + PX4_ERR("valid device required"); + } + } + + return PX4_ERROR; +} + +int SbgEcom::custom_command(int argc, char **argv) +{ + return print_usage("unrecognized command"); +} + +int SbgEcom::print_usage(const char *reason) +{ + if (reason) { + PX4_WARN("%s\n", reason); + } + + PRINT_MODULE_DESCRIPTION("Description du module"); + + PRINT_MODULE_USAGE_NAME("sbgecom", "driver"); + PRINT_MODULE_USAGE_SUBCATEGORY("ins"); + PRINT_MODULE_USAGE_COMMAND_DESCR("start", "Start driver"); + PRINT_MODULE_USAGE_PARAM_STRING('d', DEFAULT_DEVNAME, nullptr, "Serial device", true); + PRINT_MODULE_USAGE_PARAM_INT('b', 921600, 9600, 921600, "Baudrate device", true); + PRINT_MODULE_USAGE_PARAM_STRING('f', DEFAULT_CONFIG_PATH, nullptr, "Config JSON file path", true); + PRINT_MODULE_USAGE_PARAM_STRING('s', nullptr, nullptr, "Config JSON string", true); + PRINT_MODULE_USAGE_COMMAND_DESCR("status", "Driver status"); + PRINT_MODULE_USAGE_COMMAND_DESCR("stop", "Stop driver"); + + return PX4_OK; +} + +int SbgEcom::print_status() +{ + printf("Using port '%s'\n", _device_name); + + perf_print_counter(_sample_perf); + perf_print_counter(_write_perf); + perf_print_counter(_comms_errors); + + return 0; +} + +extern "C" __EXPORT int sbgecom_main(int argc, char **argv) +{ + return SbgEcom::main(argc, argv); +} diff --git a/src/drivers/ins/sbgecom/sbgecom.hpp b/src/drivers/ins/sbgecom/sbgecom.hpp new file mode 100644 index 000000000000..81bf3382d18c --- /dev/null +++ b/src/drivers/ins/sbgecom/sbgecom.hpp @@ -0,0 +1,294 @@ +/**************************************************************************** + * + * Copyright (c) 2012-2022 PX4 Development Team. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * 3. Neither the name PX4 nor the names of its contributors may be + * used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS + * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + ****************************************************************************/ + +/** + * @file sbgecom.hpp + * Driver for the SBG Systems products + * + * @author SBG Systems + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class SbgEcom : public ModuleBase, public ModuleParams, public px4::ScheduledWorkItem +{ +public: + + SbgEcom(const char *port, uint32_t baudrate, const char *config_file, const char *config_string); + ~SbgEcom() override; + + /** @see ModuleBase */ + static int task_spawn(int argc, char **argv); + + /** @see ModuleBase */ + static int custom_command(int argc, char **argv); + + /** @see ModuleBase */ + static int print_usage(const char *reason = nullptr); + + /** @see ModuleBase */ + int print_status() override; + + /** @see ModuleBase::run() */ + void Run() override; + + int init(); + +private: + + /** + * @brief Type for logging functions. + * + * @param file_name File name where the error occurred. + * @param function_name Function name where the error occurred. + * @param line Line number where the error occurred. + * @param category Category for this log or "None" if no category has been specified. + * @param log_type Define if we have an error, a warning, an info or a debug log. + * @param error_code The error code associated with the message. + * @param message The message to log. + */ + static void printLogCallBack(const char *file_name, const char *function_name, uint32_t line, const char *category, + SbgDebugLogType log_type, SbgErrorCode error_code, const char *message); + + /** + * @brief Parse IMU (Inertial Measurement Unit) measurement logs. + * + * @param ref_sbg_data Contains the received log data as an union. + * @param user_arg Optional user supplied argument. + */ + static void handleLogImuShort(const SbgEComLogUnion *ref_sbg_data, void *user_arg); + + /** + * @brief Parse magnetic field measurements logs. + * + * @param ref_sbg_data Contains the received log data as an union. + * @param user_arg Optional user supplied argument. + */ + static void handleLogMag(const SbgEComLogUnion *ref_sbg_data, void *user_arg); + + /** + * @brief Parse EKF quaternion measurement logs. + * + * @param ref_sbg_data Contains the received log data as an union. + * @param user_arg Optional user supplied argument. + */ + static void handleLogEkfQuat(const SbgEComLogUnion *ref_sbg_data, void *user_arg); + + /** + * @brief Parse EKF navigation measurement logs. + * + * @param ref_sbg_data Contains the received log data as an union. + * @param user_arg Optional user supplied argument. + */ + static void handleLogEkfNav(const SbgEComLogUnion *ref_sbg_data, void *user_arg); + + /** + * @brief GNSS position, velocity and heading related logs. + * + * @param msg Message ID of the log received. + * @param ref_sbg_data Contains the received log data as an union. + * @param user_arg Optional user supplied argument. + */ + static void handleLogGnssPosVelHdt(SbgEComMsgId msg, const SbgEComLogUnion *ref_sbg_data, void *user_arg); + + /** + * @brief Update estimator status message from EKF status flags. + * + * @param ekf_status EKF status flags. + * @param estimator_status Estimator status message. + */ + static void updateEstimatorStatus(uint32_t ekf_status, estimator_status_s *estimator_status); + + /** + * @brief Callback definition called each time a new log is received. + * + * @param handle Valid handle on the sbgECom instance that has called this callback. + * @param msg_class Class of the message we have received + * @param msg Message ID of the log received. + * @param ref_sbg_data Contains the received log data as an union. + * @param user_arg Optional user supplied argument. + * @return SBG_NO_ERROR if the received log has been used successfully. + */ + static SbgErrorCode onLogReceived(SbgEComHandle *handle, SbgEComClass msg_class, SbgEComMsgId msg, + const SbgEComLogUnion *ref_sbg_data, void *user_arg); + + /** + * @brief Send a config to the INS + * + * @param pHandle SbgECom instance. + * @param config Config json string. + */ + static void send_config(SbgEComHandle *pHandle, const char *config); + + /** + * @brief Send a config file to the INS + * + * @param pHandle SbgECom instance. + * @param file_path Config file path. + */ + static void send_config_file(SbgEComHandle *pHandle, const char *file_path); + + /** + * @brief Get and print product info. + * + * @param handle SbgECom instance. + * @return SBG_NO_ERROR if successful. + */ + SbgErrorCode getAndPrintProductInfo(SbgEComHandle *handle); + + /** + * @brief Try to parse one log from the input interface and then return. + * + * @param handle A valid sbgECom handle. + * @return SBG_NO_ERROR if no error occurs during incoming log parsing. + */ + SbgErrorCode handleOneLog(SbgEComHandle *handle); + + /** + * @brief Get air data and send it. + * + * @param handle A valid sbgECom handle. + * @param instance An SbgEcom object. + * @return SBG_NO_ERROR if no error occurs during incoming log parsing. + */ + SbgErrorCode sendAirDataLog(SbgEComHandle *handle, SbgEcom *instance); + + /** + * @brief Get magnetometer data and send it. + * + * @param handle A valid sbgECom handle. + * @param instance An SbgEcom object. + * @return SBG_NO_ERROR if no error occurs during incoming log parsing. + */ + SbgErrorCode sendMagLog(SbgEComHandle *handle, SbgEcom *instance); + + void set_device_id(uint32_t device_id); + uint32_t get_device_id(void); + + // SBG interface and state variables + SbgInterface _sbg_interface; + SbgEComHandle _com_handle; + SbgEComLogUnion _log_data; + + uint32_t _baudrate; + const char *_config_file; + const char *_config_string; + char _device_name[25]; + uint32_t _device_id{0}; + + const int log_interval = 10; + int iteration_count = log_interval; + + bool failure = false; + bool _ekf_failure = false; + + bool _initialized = false; + int init_result; + + MapProjection _pos_ref{}; + double _gps_alt_ref{NAN}; + + struct GnssData { + bool pos_received = false; + bool vel_received = false; + bool hdt_received = false; + + SbgEComLogGnssPos gps_pos; + SbgEComLogGnssVel gps_vel; + SbgEComLogGnssHdt gps_hdt; + + hrt_abstime pos_timestamp = 0; + hrt_abstime vel_timestamp = 0; + hrt_abstime hdt_timestamp = 0; + }; + + GnssData gnss_data; + float _heading; + + px4::atomic _time_last_valid_imu_us{false}; + + // Sensors topics + PX4Accelerometer _px4_accel{0}; + PX4Gyroscope _px4_gyro{0}; + PX4Magnetometer _px4_mag{0}; + + // Publications with topic dependent on multi-mode + uORB::PublicationMulti _sensor_gps_pub{ORB_ID(sensor_gps)}; + uORB::PublicationMulti _attitude_pub{ORB_ID(vehicle_attitude)}; + uORB::PublicationMulti _local_position_pub{ORB_ID(vehicle_local_position)}; + uORB::PublicationMulti _global_position_pub{ORB_ID(vehicle_global_position)}; + uORB::Publication _estimator_status_pub{ORB_ID(estimator_status)}; + + // Subscription for INS EKF aiding + uORB::Subscription _air_data_sub{ORB_ID(vehicle_air_data)}; + uORB::Subscription _airspeed_sub{ORB_ID(airspeed)}; + uORB::Subscription _diff_pressure_sub{ORB_ID(differential_pressure)}; + uORB::Subscription _mag_sub{ORB_ID(vehicle_magnetometer)}; + + // Performance mounters for monitoring and debugging + perf_counter_t _comms_errors{perf_alloc(PC_COUNT, MODULE_NAME": errors")}; + perf_counter_t _sample_perf{perf_alloc(PC_ELAPSED, MODULE_NAME": sample")}; + perf_counter_t _write_perf{perf_alloc(PC_ELAPSED, MODULE_NAME": write")}; + + perf_counter_t _accel_pub_interval_perf{perf_alloc(PC_INTERVAL, MODULE_NAME": accel publish interval")}; + perf_counter_t _gyro_pub_interval_perf{perf_alloc(PC_INTERVAL, MODULE_NAME": gyro publish interval")}; + perf_counter_t _mag_pub_interval_perf{perf_alloc(PC_INTERVAL, MODULE_NAME": mag publish interval")}; + perf_counter_t _gnss_pub_interval_perf{perf_alloc(PC_INTERVAL, MODULE_NAME": GNSS publish interval")}; + + perf_counter_t _attitude_pub_interval_perf{perf_alloc(PC_INTERVAL, MODULE_NAME": attitude publish interval")}; + perf_counter_t _local_position_pub_interval_perf{perf_alloc(PC_INTERVAL, MODULE_NAME": local position publish interval")}; + perf_counter_t _global_position_pub_interval_perf{perf_alloc(PC_INTERVAL, MODULE_NAME": global position publish interval")}; +}; diff --git a/src/drivers/ins/sbgecom/sbgecom.patch b/src/drivers/ins/sbgecom/sbgecom.patch new file mode 100644 index 000000000000..67903e27f401 --- /dev/null +++ b/src/drivers/ins/sbgecom/sbgecom.patch @@ -0,0 +1,124 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 42d1404..3355005 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -36,6 +36,7 @@ if (USE_DEPRECATED_MACROS) + endif() + + add_library(${PROJECT_NAME} STATIC) ++add_dependencies(${PROJECT_NAME} prebuild_targets) + + file(GLOB_RECURSE COMMON_SRC ${PROJECT_SOURCE_DIR}/common/*.c) + file(GLOB_RECURSE ECOM_SRC ${PROJECT_SOURCE_DIR}/src/*.c) +@@ -194,3 +195,18 @@ install(DIRECTORY common/ + install(DIRECTORY src/ + DESTINATION include + FILES_MATCHING PATTERN "*.h") ++ ++target_compile_options(${PROJECT_NAME} ++ PRIVATE ++ -Wno-format ++ -Wno-format-security ++ -Wno-bad-function-cast ++ -Wno-double-promotion ++ -Wno-type-limits ++ -Wno-maybe-uninitialized ++ -Wno-float-equal ++) ++ ++if("${PX4_PLATFORM}" MATCHES "nuttx") ++ target_compile_definitions(${PROJECT_NAME} PUBLIC __NUTTX__) ++endif() +diff --git a/common/interfaces/sbgInterfaceSerialUnix.c b/common/interfaces/sbgInterfaceSerialUnix.c +index eb7796c..99c52e0 100644 +--- a/common/interfaces/sbgInterfaceSerialUnix.c ++++ b/common/interfaces/sbgInterfaceSerialUnix.c +@@ -6,10 +6,17 @@ + #include + #include + #include ++#include + + // sbgCommonLib headers + #include + #include ++#include ++ ++#ifdef __PX4_NUTTX ++#include ++#include ++#endif + + //----------------------------------------------------------------------// + //- Definitions -// +@@ -447,19 +454,21 @@ SbgErrorCode sbgInterfaceSerialCreate(SbgInterface *pInterface, const char *devi + // Define com port options + // + options.c_cflag |= (CLOCAL | CREAD); // Enable the receiver and set local mode... +- options.c_cflag &= ~(PARENB|CSTOPB|CSIZE); // No parity, 1 stop bit, mask character size bits ++ options.c_cflag &= ~(PARENB | CSTOPB); // No parity, 1 stop bit, mask character size bits ++ options.c_cflag &= CSIZE; + options.c_cflag |= CS8; // Select 8 data bits + options.c_cflag &= ~CRTSCTS; // Disable Hardware flow control + + // + // Disable software flow control + // +- options.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON); ++ options.c_iflag &= ~(IXON | IXOFF | IXANY); // Disable software flow control ++ options.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|ICRNL|INPCK); // Disable any special handling of received bytes + + // + // We would like raw input + // +- options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG /*| IEXTEN | ECHONL*/); ++ options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG | ECHONL | IEXTEN); + options.c_oflag &= ~OPOST; + + // +diff --git a/common/platform/sbgPlatform.c b/common/platform/sbgPlatform.c +index 95a50ac..0c9f358 100644 +--- a/common/platform/sbgPlatform.c ++++ b/common/platform/sbgPlatform.c +@@ -119,28 +119,4 @@ SBG_COMMON_LIB_API void sbgPlatformDebugLogMsg(const char *pFileName, const char + { + gLogCallback(pFileName, pFunctionName, line, pCategory, logType, errorCode, errorMsg); + } +- else +- { +- // +- // Log the correct message according to the log type +- // +- switch (logType) +- { +- case SBG_DEBUG_LOG_TYPE_ERROR: +- fprintf(stderr, "*ERR * %s(%"PRIu32"): %s - %s\n\r", pFunctionName, line, sbgErrorCodeToString(errorCode), errorMsg); +- break; +- case SBG_DEBUG_LOG_TYPE_WARNING: +- fprintf(stderr, "*WARN* %s(%"PRIu32"): %s - %s\n\r", pFunctionName, line, sbgErrorCodeToString(errorCode), errorMsg); +- break; +- case SBG_DEBUG_LOG_TYPE_INFO: +- fprintf(stderr, "*INFO* %s(%"PRIu32"): %s\n\r", pFunctionName, line, errorMsg); +- break; +- case SBG_DEBUG_LOG_TYPE_DEBUG: +- fprintf(stderr, "*DBG * %s(%"PRIu32"): %s\n\r", pFunctionName, line, errorMsg); +- break; +- default: +- fprintf(stderr, "*UKNW* %s(%"PRIu32"): %s\n\r", pFunctionName, line, errorMsg); +- break; +- } +- } + } +diff --git a/common/sbgCommon.h b/common/sbgCommon.h +index 4bef011..fd858d4 100644 +--- a/common/sbgCommon.h ++++ b/common/sbgCommon.h +@@ -103,7 +103,7 @@ extern "C" { + * Default: 1024 + */ + #ifndef SBG_CONFIG_LOG_MAX_SIZE +-#define SBG_CONFIG_LOG_MAX_SIZE ((size_t)(1024)) ++#define SBG_CONFIG_LOG_MAX_SIZE ((size_t)(128)) + #endif + + /*! From 028e688907c4473f3a889dd53a65021b35e0e804 Mon Sep 17 00:00:00 2001 From: Samuel Toledano Date: Thu, 12 Sep 2024 18:21:49 +0200 Subject: [PATCH 02/10] Fix sensor airspeed simulator units --- .../simulation/sensor_airspeed_sim/SensorAirspeedSim.cpp | 2 +- .../simulation/sensor_airspeed_sim/SensorAirspeedSim.hpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/simulation/sensor_airspeed_sim/SensorAirspeedSim.cpp b/src/modules/simulation/sensor_airspeed_sim/SensorAirspeedSim.cpp index 7b0d9556cadf..5566efbb1c7d 100644 --- a/src/modules/simulation/sensor_airspeed_sim/SensorAirspeedSim.cpp +++ b/src/modules/simulation/sensor_airspeed_sim/SensorAirspeedSim.cpp @@ -144,7 +144,7 @@ void SensorAirspeedSim::Run() // report.timestamp_sample = time; differential_pressure.device_id = 1377548; // 1377548: DRV_DIFF_PRESS_DEVTYPE_SIM, BUS: 1, ADDR: 5, TYPE: SIMULATION differential_pressure.differential_pressure_pa = (double)diff_pressure * 100.0; // hPa to Pa; - differential_pressure.temperature = temperature_local; + differential_pressure.temperature = temperature_local + ABSOLUTE_ZERO_C; // K to C differential_pressure.timestamp = hrt_absolute_time(); _differential_pressure_pub.publish(differential_pressure); diff --git a/src/modules/simulation/sensor_airspeed_sim/SensorAirspeedSim.hpp b/src/modules/simulation/sensor_airspeed_sim/SensorAirspeedSim.hpp index 64305dcccada..3aafc9ca938e 100644 --- a/src/modules/simulation/sensor_airspeed_sim/SensorAirspeedSim.hpp +++ b/src/modules/simulation/sensor_airspeed_sim/SensorAirspeedSim.hpp @@ -49,6 +49,7 @@ using namespace time_literals; +static constexpr float ABSOLUTE_ZERO_C = -273.15; // absolute 0 temperature [C] static constexpr float TEMPERATURE_MSL = 288.15; // temperature at MSL [K] (15 [C]) static constexpr float PRESSURE_MSL = 101325.0; // pressure at MSL [Pa] static constexpr float LAPSE_RATE = 0.0065; // reduction in temperature with altitude for troposphere [K/m] From fecc9f44973e6fb56997f20c9ba1b885b6bba451 Mon Sep 17 00:00:00 2001 From: Samuel Toledano Date: Thu, 12 Sep 2024 18:22:12 +0200 Subject: [PATCH 03/10] Fix HIGHRES_IMU pressure unit --- src/modules/mavlink/streams/HIGHRES_IMU.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/mavlink/streams/HIGHRES_IMU.hpp b/src/modules/mavlink/streams/HIGHRES_IMU.hpp index 7a3d96cb3891..399747522715 100644 --- a/src/modules/mavlink/streams/HIGHRES_IMU.hpp +++ b/src/modules/mavlink/streams/HIGHRES_IMU.hpp @@ -190,8 +190,8 @@ class MavlinkStreamHighresIMU : public MavlinkStream msg.xmag = mag(0); msg.ymag = mag(1); msg.zmag = mag(2); - msg.abs_pressure = air_data.baro_pressure_pa; - msg.diff_pressure = differential_pressure.differential_pressure_pa; + msg.abs_pressure = air_data.baro_pressure_pa * 0.01f; // Pa to hPa + msg.diff_pressure = differential_pressure.differential_pressure_pa * 0.01f; // Pa to hPa msg.pressure_alt = air_data.baro_alt_meter; msg.temperature = air_data.ambient_temperature; msg.fields_updated = fields_updated; From eedd0fe20992d35e03f4b46384b8741040debe06 Mon Sep 17 00:00:00 2001 From: Samuel Toledano Date: Fri, 10 Jan 2025 19:07:45 +0100 Subject: [PATCH 04/10] Allow HIGHRES_IMU to support 4 IMUs --- src/modules/mavlink/streams/HIGHRES_IMU.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/mavlink/streams/HIGHRES_IMU.hpp b/src/modules/mavlink/streams/HIGHRES_IMU.hpp index 399747522715..c6e4d7580bcb 100644 --- a/src/modules/mavlink/streams/HIGHRES_IMU.hpp +++ b/src/modules/mavlink/streams/HIGHRES_IMU.hpp @@ -63,7 +63,7 @@ class MavlinkStreamHighresIMU : public MavlinkStream private: explicit MavlinkStreamHighresIMU(Mavlink *mavlink) : MavlinkStream(mavlink) {} - uORB::SubscriptionMultiArray _vehicle_imu_subs{ORB_ID::vehicle_imu}; + uORB::SubscriptionMultiArray _vehicle_imu_subs{ORB_ID::vehicle_imu}; uORB::Subscription _estimator_sensor_bias_sub{ORB_ID(estimator_sensor_bias)}; uORB::Subscription _estimator_selector_status_sub{ORB_ID(estimator_selector_status)}; uORB::Subscription _sensor_selection_sub{ORB_ID(sensor_selection)}; From 8333314dacf62524455e996de312b19828f8e54e Mon Sep 17 00:00:00 2001 From: Samuel TOLEDANO Date: Tue, 5 Aug 2025 11:51:41 +0200 Subject: [PATCH 05/10] sbgECom: Add documentation --- .../ellipse-inertial-navigation-system.png | Bin 0 -> 46104 bytes docs/en/SUMMARY.md | 1 + docs/en/releases/main.md | 2 +- docs/en/sensor/inertial_navigation_systems.md | 1 + docs/en/sensor/sbgecom.md | 150 ++++++++++++++++++ 5 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 docs/assets/hardware/sensors/inertial/ellipse-inertial-navigation-system.png create mode 100644 docs/en/sensor/sbgecom.md diff --git a/docs/assets/hardware/sensors/inertial/ellipse-inertial-navigation-system.png b/docs/assets/hardware/sensors/inertial/ellipse-inertial-navigation-system.png new file mode 100644 index 0000000000000000000000000000000000000000..617f2e676cc8e35d0f7ef046b8a8eff6e215e5d5 GIT binary patch literal 46104 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 S4KGyy0000 Date: Tue, 5 Aug 2025 17:47:54 +0200 Subject: [PATCH 06/10] Use submodule instead of fetching sbgECom using CMake --- .gitmodules | 3 +++ src/drivers/ins/sbgecom/CMakeLists.txt | 28 ++++++++++++++++---------- src/drivers/ins/sbgecom/sbgECom | 1 + 3 files changed, 21 insertions(+), 11 deletions(-) create mode 160000 src/drivers/ins/sbgecom/sbgECom diff --git a/.gitmodules b/.gitmodules index a4c12ecebd88..4061b65c1154 100644 --- a/.gitmodules +++ b/.gitmodules @@ -100,3 +100,6 @@ [submodule "src/drivers/ins/microstrain/mip_sdk"] path = src/drivers/ins/microstrain/mip_sdk url = https://github.com/PX4/LORD-MicroStrain_mip_sdk.git +[submodule "src/drivers/ins/sbgecom/sbgECom"] + path = src/drivers/ins/sbgecom/sbgECom + url = https://github.com/PX4/sbgECom.git diff --git a/src/drivers/ins/sbgecom/CMakeLists.txt b/src/drivers/ins/sbgecom/CMakeLists.txt index 1235e2c7eeda..05f1ba3089f4 100644 --- a/src/drivers/ins/sbgecom/CMakeLists.txt +++ b/src/drivers/ins/sbgecom/CMakeLists.txt @@ -31,24 +31,30 @@ # ############################################################################ -include(FetchContent) - -FetchContent_Declare( - sbgECom - GIT_REPOSITORY https://github.com/SBG-Systems/sbgECom.git - GIT_TAG 5.3.2276-stable - GIT_SHALLOW TRUE - PATCH_COMMAND git apply --reverse --check "${CMAKE_CURRENT_LIST_DIR}/sbgecom.patch" || git apply --ignore-whitespace "${CMAKE_CURRENT_LIST_DIR}/sbgecom.patch" +execute_process( + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/sbgECom + COMMAND git apply --reverse --check ${CMAKE_CURRENT_LIST_DIR}/sbgecom.patch + RESULT_VARIABLE patch_check_result ) -FetchContent_MakeAvailable(sbgECom) +if(patch_check_result EQUAL 1) + execute_process( + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/sbgECom + COMMAND git apply --ignore-whitespace ${CMAKE_CURRENT_LIST_DIR}/sbgecom.patch + RESULT_VARIABLE patch_check_result + ) +endif() + +if(patch_check_result EQUAL 0) + add_subdirectory(sbgECom) +endif() px4_add_module( MODULE drivers__ins__sbgecom MAIN sbgecom INCLUDES - ${sbgECom_SOURCE_DIR}/common - ${sbgECom_SOURCE_DIR}/src + sbgECom/common + sbgECom/src COMPILE_FLAGS SRCS sbgecom.cpp diff --git a/src/drivers/ins/sbgecom/sbgECom b/src/drivers/ins/sbgecom/sbgECom new file mode 160000 index 000000000000..80b121c77140 --- /dev/null +++ b/src/drivers/ins/sbgecom/sbgECom @@ -0,0 +1 @@ +Subproject commit 80b121c7714083cc4868c0fdb8c41623c7ef9c93 From 723008b551327adfee587b328c6755f594da7ac7 Mon Sep 17 00:00:00 2001 From: Samuel TOLEDANO Date: Wed, 6 Aug 2025 11:43:54 +0200 Subject: [PATCH 07/10] Remove patch strategy --- src/drivers/ins/sbgecom/CMakeLists.txt | 33 ++++++++++++++++---------- src/drivers/ins/sbgecom/sbgecom.cpp | 23 ++++++++++++++++++ 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/drivers/ins/sbgecom/CMakeLists.txt b/src/drivers/ins/sbgecom/CMakeLists.txt index 05f1ba3089f4..86127e623608 100644 --- a/src/drivers/ins/sbgecom/CMakeLists.txt +++ b/src/drivers/ins/sbgecom/CMakeLists.txt @@ -31,23 +31,30 @@ # ############################################################################ -execute_process( - WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/sbgECom - COMMAND git apply --reverse --check ${CMAKE_CURRENT_LIST_DIR}/sbgecom.patch - RESULT_VARIABLE patch_check_result +add_compile_definitions(SBG_CONFIG_LOG_MAX_SIZE=128) + +set(SBGECOM_DIR ${CMAKE_CURRENT_SOURCE_DIR}/sbgECom) +px4_add_git_submodule(TARGET git_sbgECom PATH ${SBGECOM_DIR}) + +add_subdirectory(sbgECom) + +add_dependencies(sbgECom prebuild_targets) + +target_compile_options(sbgECom + PRIVATE + -Wno-format + -Wno-format-security + -Wno-bad-function-cast + -Wno-double-promotion + -Wno-type-limits + -Wno-maybe-uninitialized + -Wno-float-equal ) -if(patch_check_result EQUAL 1) - execute_process( - WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/sbgECom - COMMAND git apply --ignore-whitespace ${CMAKE_CURRENT_LIST_DIR}/sbgecom.patch - RESULT_VARIABLE patch_check_result - ) +if("${PX4_PLATFORM}" MATCHES "nuttx") + target_compile_definitions(sbgECom PUBLIC __NUTTX__) endif() -if(patch_check_result EQUAL 0) - add_subdirectory(sbgECom) -endif() px4_add_module( MODULE drivers__ins__sbgecom diff --git a/src/drivers/ins/sbgecom/sbgecom.cpp b/src/drivers/ins/sbgecom/sbgecom.cpp index 21153d44137b..6d632616a787 100644 --- a/src/drivers/ins/sbgecom/sbgecom.cpp +++ b/src/drivers/ins/sbgecom/sbgecom.cpp @@ -45,6 +45,7 @@ #include #include +#include #define DEFAULT_DEVNAME "/dev/ttyS0" @@ -887,6 +888,8 @@ void SbgEcom::send_config_file(SbgEComHandle *pHandle, const char *file_path) int SbgEcom::init() { SbgErrorCode error_code; + struct termios options; + int *pSerialHandle; error_code = sbgInterfaceSerialCreate(&_sbg_interface, _device_name, _baudrate); @@ -894,6 +897,26 @@ int SbgEcom::init() PX4_INFO("Serial interface created successfully on port: %s, baudrate: %ld", _device_name, _baudrate); } + pSerialHandle = (int *)_sbg_interface.handle; + + if (tcgetattr((*pSerialHandle), &options) != -1) { + // add custom options + options.c_cflag &= CSIZE; + options.c_iflag &= ~(IXON | IXOFF | IXANY); + options.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | ICRNL | INPCK); + options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG | ECHONL | IEXTEN); + + if (tcsetattr((*pSerialHandle), TCSANOW, &options) != -1) { + error_code = sbgInterfaceFlush(&_sbg_interface, SBG_IF_FLUSH_ALL); + + } else { + error_code = SBG_ERROR; + } + + } else { + error_code = SBG_ERROR; + } + error_code = sbgEComInit(&_com_handle, &_sbg_interface); // Increase sbgECom timeout for the initialization sbgEComSetCmdTrialsAndTimeOut(&_com_handle, 3, 5000); From 4551532580f26fdca2cc440075a0f87a951e2423 Mon Sep 17 00:00:00 2001 From: Samuel TOLEDANO Date: Wed, 13 Aug 2025 10:41:11 +0200 Subject: [PATCH 08/10] Fix dates --- src/drivers/ins/sbgecom/CMakeLists.txt | 2 +- src/drivers/ins/sbgecom/sbgecom.cpp | 2 +- src/drivers/ins/sbgecom/sbgecom.hpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/drivers/ins/sbgecom/CMakeLists.txt b/src/drivers/ins/sbgecom/CMakeLists.txt index 86127e623608..e4c8362da370 100644 --- a/src/drivers/ins/sbgecom/CMakeLists.txt +++ b/src/drivers/ins/sbgecom/CMakeLists.txt @@ -1,6 +1,6 @@ ############################################################################ # -# Copyright (c) 2022 PX4 Development Team. All rights reserved. +# Copyright (c) 2025 PX4 Development Team. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions diff --git a/src/drivers/ins/sbgecom/sbgecom.cpp b/src/drivers/ins/sbgecom/sbgecom.cpp index 6d632616a787..17c422839df0 100644 --- a/src/drivers/ins/sbgecom/sbgecom.cpp +++ b/src/drivers/ins/sbgecom/sbgecom.cpp @@ -1,6 +1,6 @@ /**************************************************************************** * - * Copyright (c) 2012-2022 PX4 Development Team. All rights reserved. + * Copyright (c) 2012-2025 PX4 Development Team. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions diff --git a/src/drivers/ins/sbgecom/sbgecom.hpp b/src/drivers/ins/sbgecom/sbgecom.hpp index 81bf3382d18c..332f8def2c47 100644 --- a/src/drivers/ins/sbgecom/sbgecom.hpp +++ b/src/drivers/ins/sbgecom/sbgecom.hpp @@ -1,6 +1,6 @@ /**************************************************************************** * - * Copyright (c) 2012-2022 PX4 Development Team. All rights reserved. + * Copyright (c) 2012-2025 PX4 Development Team. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions From 6b5aeb09152b5011b2f334b02b2c39944dc12858 Mon Sep 17 00:00:00 2001 From: Samuel TOLEDANO Date: Wed, 13 Aug 2025 10:41:26 +0200 Subject: [PATCH 09/10] Remove patch file --- src/drivers/ins/sbgecom/sbgecom.patch | 124 -------------------------- 1 file changed, 124 deletions(-) delete mode 100644 src/drivers/ins/sbgecom/sbgecom.patch diff --git a/src/drivers/ins/sbgecom/sbgecom.patch b/src/drivers/ins/sbgecom/sbgecom.patch deleted file mode 100644 index 67903e27f401..000000000000 --- a/src/drivers/ins/sbgecom/sbgecom.patch +++ /dev/null @@ -1,124 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 42d1404..3355005 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -36,6 +36,7 @@ if (USE_DEPRECATED_MACROS) - endif() - - add_library(${PROJECT_NAME} STATIC) -+add_dependencies(${PROJECT_NAME} prebuild_targets) - - file(GLOB_RECURSE COMMON_SRC ${PROJECT_SOURCE_DIR}/common/*.c) - file(GLOB_RECURSE ECOM_SRC ${PROJECT_SOURCE_DIR}/src/*.c) -@@ -194,3 +195,18 @@ install(DIRECTORY common/ - install(DIRECTORY src/ - DESTINATION include - FILES_MATCHING PATTERN "*.h") -+ -+target_compile_options(${PROJECT_NAME} -+ PRIVATE -+ -Wno-format -+ -Wno-format-security -+ -Wno-bad-function-cast -+ -Wno-double-promotion -+ -Wno-type-limits -+ -Wno-maybe-uninitialized -+ -Wno-float-equal -+) -+ -+if("${PX4_PLATFORM}" MATCHES "nuttx") -+ target_compile_definitions(${PROJECT_NAME} PUBLIC __NUTTX__) -+endif() -diff --git a/common/interfaces/sbgInterfaceSerialUnix.c b/common/interfaces/sbgInterfaceSerialUnix.c -index eb7796c..99c52e0 100644 ---- a/common/interfaces/sbgInterfaceSerialUnix.c -+++ b/common/interfaces/sbgInterfaceSerialUnix.c -@@ -6,10 +6,17 @@ - #include - #include - #include -+#include - - // sbgCommonLib headers - #include - #include -+#include -+ -+#ifdef __PX4_NUTTX -+#include -+#include -+#endif - - //----------------------------------------------------------------------// - //- Definitions -// -@@ -447,19 +454,21 @@ SbgErrorCode sbgInterfaceSerialCreate(SbgInterface *pInterface, const char *devi - // Define com port options - // - options.c_cflag |= (CLOCAL | CREAD); // Enable the receiver and set local mode... -- options.c_cflag &= ~(PARENB|CSTOPB|CSIZE); // No parity, 1 stop bit, mask character size bits -+ options.c_cflag &= ~(PARENB | CSTOPB); // No parity, 1 stop bit, mask character size bits -+ options.c_cflag &= CSIZE; - options.c_cflag |= CS8; // Select 8 data bits - options.c_cflag &= ~CRTSCTS; // Disable Hardware flow control - - // - // Disable software flow control - // -- options.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON); -+ options.c_iflag &= ~(IXON | IXOFF | IXANY); // Disable software flow control -+ options.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|ICRNL|INPCK); // Disable any special handling of received bytes - - // - // We would like raw input - // -- options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG /*| IEXTEN | ECHONL*/); -+ options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG | ECHONL | IEXTEN); - options.c_oflag &= ~OPOST; - - // -diff --git a/common/platform/sbgPlatform.c b/common/platform/sbgPlatform.c -index 95a50ac..0c9f358 100644 ---- a/common/platform/sbgPlatform.c -+++ b/common/platform/sbgPlatform.c -@@ -119,28 +119,4 @@ SBG_COMMON_LIB_API void sbgPlatformDebugLogMsg(const char *pFileName, const char - { - gLogCallback(pFileName, pFunctionName, line, pCategory, logType, errorCode, errorMsg); - } -- else -- { -- // -- // Log the correct message according to the log type -- // -- switch (logType) -- { -- case SBG_DEBUG_LOG_TYPE_ERROR: -- fprintf(stderr, "*ERR * %s(%"PRIu32"): %s - %s\n\r", pFunctionName, line, sbgErrorCodeToString(errorCode), errorMsg); -- break; -- case SBG_DEBUG_LOG_TYPE_WARNING: -- fprintf(stderr, "*WARN* %s(%"PRIu32"): %s - %s\n\r", pFunctionName, line, sbgErrorCodeToString(errorCode), errorMsg); -- break; -- case SBG_DEBUG_LOG_TYPE_INFO: -- fprintf(stderr, "*INFO* %s(%"PRIu32"): %s\n\r", pFunctionName, line, errorMsg); -- break; -- case SBG_DEBUG_LOG_TYPE_DEBUG: -- fprintf(stderr, "*DBG * %s(%"PRIu32"): %s\n\r", pFunctionName, line, errorMsg); -- break; -- default: -- fprintf(stderr, "*UKNW* %s(%"PRIu32"): %s\n\r", pFunctionName, line, errorMsg); -- break; -- } -- } - } -diff --git a/common/sbgCommon.h b/common/sbgCommon.h -index 4bef011..fd858d4 100644 ---- a/common/sbgCommon.h -+++ b/common/sbgCommon.h -@@ -103,7 +103,7 @@ extern "C" { - * Default: 1024 - */ - #ifndef SBG_CONFIG_LOG_MAX_SIZE --#define SBG_CONFIG_LOG_MAX_SIZE ((size_t)(1024)) -+#define SBG_CONFIG_LOG_MAX_SIZE ((size_t)(128)) - #endif - - /*! From 23f1c58e738ed9cabb0e8144cc24c4e9cdbc1092 Mon Sep 17 00:00:00 2001 From: Samuel Toledano Date: Wed, 10 Sep 2025 21:40:58 +0200 Subject: [PATCH 10/10] Update SBG dev type ID Co-authored-by: Jacob Dahl <37091262+dakejahl@users.noreply.github.com> --- src/drivers/drv_sensor.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/drivers/drv_sensor.h b/src/drivers/drv_sensor.h index f7a294e9e238..147eb049d6a4 100644 --- a/src/drivers/drv_sensor.h +++ b/src/drivers/drv_sensor.h @@ -256,7 +256,7 @@ #define DRV_INS_DEVTYPE_MICROSTRAIN 0xEA #define DRV_INS_DEVTYPE_BAHRS 0xEB -#define DRV_INS_DEVTYPE_SBG 0xEB +#define DRV_INS_DEVTYPE_SBG 0xEC #define DRV_DEVTYPE_UNUSED 0xff