diff --git a/conf/modules/fdm_gazebo.xml b/conf/modules/fdm_gazebo.xml index c3106c9073..66779b4382 100644 --- a/conf/modules/fdm_gazebo.xml +++ b/conf/modules/fdm_gazebo.xml @@ -135,8 +135,8 @@
- nps.CXXFLAGS += $(shell pkg-config gazebo --cflags) - nps.LDFLAGS += $(shell pkg-config gazebo --libs) + nps.CXXFLAGS += $(shell pkg-config gz-sim8 --cflags) + nps.LDFLAGS += $(shell pkg-config gz-sim8 --libs) NPS_DEBUG_VIDEO ?= 0 diff --git a/conf/modules/fdm_gazebo_classic.xml b/conf/modules/fdm_gazebo_classic.xml new file mode 100644 index 0000000000..c3106c9073 --- /dev/null +++ b/conf/modules/fdm_gazebo_classic.xml @@ -0,0 +1,152 @@ + + + + + + Gazebo backend for NPS simulator + NPS doc: http://wiki.paparazziuav.org/wiki/NPS + + Usage: + 1. Make sure gazebo 9 is installed. (sudo apt-get install gazebo9 libgazebo9-dev) + 2. Prepare the Gazebo world and model: + 1. Prepare the UAV model (see conf/simulator/gazebo/models/ardrone/): + - Place the aircraft model in the conf/simulator/gazebo/models/ + folder, this folder is added to Gazebo's search path when NPS is + launched. + - Gazebo uses a Front, Left, Up coordinate system for aircraft, so + make sure the +x axis points forwards. + - The model should include a link for each motor with the same names + as those listed in NPS_ACTUATOR_NAMES (see below), e.g. 'nw_motor'. + - Camera links should have the name specified in .dev_name in the + corresponding video_config_t struct, see sw/airborne/boards/pc_sim.h + and sw/airborne/modules/computer_vision/video_thread_nps.c. + 2. Prepare the world (see conf/simulator/gazebo/worlds/ardrone.world). + Pay attention to the following: + - The real-time update rate should be set to zero, as this is + already handled by Paparazzi: + @code{.xml} + <physics type="ode"> + <max_step_size>0.001</max_step_size> + <real_time_update_rate>0</real_time_update_rate><!-- Handled by Paparazzi! --> + </physics> + @endcode + - Spherical coordinates should be provided for navigation. + At this moment, there is an issue where Gazebo incorrectly + uses a WSU coordinate system instead of ENU. This can be fixed + by setting the heading to 180 degrees as shown below: + @code{.xml} + <spherical_coordinates> + <surface_model>EARTH_WGS84</surface_model> + <latitude_deg>51.9906</latitude_deg> + <longitude_deg>4.37679</longitude_deg> + <elevation>0</elevation> + <heading_deg>180</heading_deg><!-- Temporary fix for issue https://bitbucket.org/osrf/gazebo/issues/2022/default-sphericalcoordinates-frame-should --> + </spherical_coordinates> + @endcode + 3. Prepare the airframe file (see examples/ardrone2_gazebo.xml): + 1. Select Gazebo as the FDM (Flight Dynamics Model) + @code{.xml} + <target name="nps" board="pc"> + <module name="fdm" type="gazebo"/> + </target> + @endcode + 2. Include the gazebo defines for the vehicle: + @code{.xml} + <section name="SIMULATOR" prefix="NPS_"> + ... + </section> + <include href="conf/simulator/gazebo/airframes/ardrone2.xml"/> + @endcode + - If conf/simulator/gazebo/airframes does not contain an xml for the + vehicle model, it should be created with the following contents: + 1. Actuator thrusts and torques + @code{.xml} + <!DOCTYPE airframe SYSTEM "../../../airframes/airframe.dtd"> + + <airframe> + <section name="SIMULATOR" prefix="NPS_"> + <define name="ACTUATOR_THRUSTS" value="1.55, 1.55, 1.55, 1.55" type="float[]"/> + <define name="ACTUATOR_TORQUES" value="0.155, 0.155, 0.155, 0.155" type="float[]"/> + ... + </section> + </airframe> + @endcode + The thrusts and torques are expressed in SI units (N, Nm) and should + be in the same order as the ACTUATOR_NAMES defined in the airframe file. + The torque direction is determined automatically from the motor mixing. + 2. (Optional) Add actuator dynamics to the SIMULATOR section: + @code{.xml} + <section name="SIMULATOR" prefix="NPS_"> + ... + <define name="ACTUATOR_TIME_CONSTANTS" value="0.02, 0.02, 0.02, 0.02" type="float[]"/> + <define name="ACTUATOR_MAX_ANGULAR_MOMENTUM" value="0.19, 0.19, 0.19, 0.19" type="float[]"/> + ... + </section> + @endcode + Actuator time constants can be provided without specifying the + actuator's maximum angular momentum. If the maximum angular momentum + is provided as well, it is used to calculate the rotor spin-up torque. + 3. In the same section, bypass the AHRS and INS as these are not + supported yet: + @code{.xml} + <section name="SIMULATOR" prefix="NPS_"> + ... + <define name="BYPASS_AHRS" value="1"/> + <define name="BYPASS_INS" value="1"/> + ... + </section> + @endcode + + 4. If required, enable video thread simulation: + @code{.xml} + <section name="SIMULATOR" prefix="NPS_"> + ... + <define name="SIMULATE_VIDEO" value="1"/> + ... + </section> + @endcode + 5. Set the aircraft model in the xml file: + @code{.xml} + <section name="SIMULATOR" prefix="NPS_"> + ... + <define name="GAZEBO_AC_NAME" value="my_uav"/> + </section> + @endcode + 3. Make sure all included modules work with nps. At the moment, most of + the modules that depend on video_thread are only built when ap is + selected as the target. To fix this, add nps to the target attribute + in the module xml, e.g.: + @code{.xml} + <makefile target="ap|nps"> + @endcode + 4. The simulation environment is set in the flight plan file: + @code{.xml} + <flight_plan ...> + <header> + ... + #define NPS_GAZEBO_WORLD "my.world" + </header> + ... + </flight_plan> + @endcode + + + +
+ + + nps.CXXFLAGS += $(shell pkg-config gazebo --cflags) + nps.LDFLAGS += $(shell pkg-config gazebo --libs) + + + NPS_DEBUG_VIDEO ?= 0 + ifeq (,$(findstring $(NPS_DEBUG_VIDEO),0 FALSE)) + nps.CXXFLAGS += -DNPS_DEBUG_VIDEO + nps.CXXFLAGS += $(shell pkg-config opencv) + nps.LDFLAGS += -lopencv_imgproc -lopencv_highgui -lopencv_core + endif + + + + + diff --git a/sw/simulator/nps/nps_fdm_gazebo.cpp b/sw/simulator/nps/nps_fdm_gazebo.cpp index ffbed0c7da..a5f999ca60 100644 --- a/sw/simulator/nps/nps_fdm_gazebo.cpp +++ b/sw/simulator/nps/nps_fdm_gazebo.cpp @@ -1,5 +1,6 @@ /* * Copyright (C) 2017 Tom van Dijk, Kirk Scheper + * Copyright (C) 2025 Updated for Gazebo Harmonic * * This file is part of paparazzi. * @@ -21,9 +22,9 @@ /** * @file nps_fdm_gazebo.cpp - * Flight Dynamics Model (FDM) for NPS using Gazebo. + * Flight Dynamics Model (FDM) for NPS using Gazebo Harmonic. * - * This is an FDM for NPS that uses Gazebo as the simulation engine. + * This is an FDM for NPS that uses Gazebo Harmonic as the simulation engine. */ #include @@ -32,15 +33,32 @@ #include #include -#include -#include -#include -#include +// Gazebo Harmonic includes +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include -#include -#if GAZEBO_MAJOR_VERSION < 8 -#error "Paparazzi only supports gazebo versions > 7, please upgrade to a more recent version of Gazebo" -#endif +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include extern "C" { #include "nps_fdm.h" @@ -73,7 +91,7 @@ extern "C" { using namespace std; #ifndef NPS_GAZEBO_WORLD -#define NPS_GAZEBO_WORLD "empty.world" +#define NPS_GAZEBO_WORLD "empty.sdf" #endif #ifndef NPS_GAZEBO_AC_NAME #define NPS_GAZEBO_AC_NAME "ardrone" @@ -95,13 +113,15 @@ static void init_gazebo_video(void); static void gazebo_read_video(void); static void read_image( struct image_t *img, - gazebo::sensors::CameraSensorPtr cam); + gz::sensors::CameraSensor *cam); + struct gazebocam_t { - gazebo::sensors::CameraSensorPtr cam; - gazebo::common::Time last_measurement_time; + gz::sensors::CameraSensor *cam; + gz::msgs::Image last_image_msg; + std::chrono::steady_clock::time_point last_measurement_time; }; static struct gazebocam_t gazebo_cams[VIDEO_THREAD_MAX_CAMERAS] = -{ { NULL, 0 } }; +{ { NULL, gz::msgs::Image(), std::chrono::steady_clock::time_point() } }; // Reduce resolution of the simulated MT9F002 sensor (Bebop) to improve runtime // performance at the cost of image resolution. @@ -132,17 +152,22 @@ static void gazebo_read_range_sensors(void); #endif -std::shared_ptr sonar = NULL; +std::shared_ptr sonar = NULL; /// Holds all necessary NPS FDM state information struct NpsFdm fdm; // Pointer to Gazebo data static bool gazebo_initialized = false; -static gazebo::physics::ModelPtr model = NULL; +static gz::sim::Entity modelEntity; +static gz::sim::Model model; +static gz::sim::Server *server = NULL; +static gz::sim::EntityComponentManager *ecm = NULL; +static gz::sim::EventManager *eventMgr = NULL; +static gz::transport::Node node; // Get contact sensor -static gazebo::sensors::ContactSensorPtr ct; +static gz::sensors::ContactSensor *ct = NULL; // Helper functions static void init_gazebo(void); @@ -150,7 +175,7 @@ static void gazebo_read(void); static void gazebo_write(double act_commands[], int commands_nb); // Conversion routines -inline struct EcefCoor_d to_pprz_ecef(ignition::math::Vector3d ecef_i) +inline struct EcefCoor_d to_pprz_ecef(gz::math::Vector3d ecef_i) { struct EcefCoor_d ecef_p; ecef_p.x = ecef_i.X(); @@ -159,7 +184,7 @@ inline struct EcefCoor_d to_pprz_ecef(ignition::math::Vector3d ecef_i) return ecef_p; } -inline struct NedCoor_d to_pprz_ned(ignition::math::Vector3d global) +inline struct NedCoor_d to_pprz_ned(gz::math::Vector3d global) { struct NedCoor_d ned; ned.x = global.Y(); @@ -168,7 +193,7 @@ inline struct NedCoor_d to_pprz_ned(ignition::math::Vector3d global) return ned; } -inline struct LlaCoor_d to_pprz_lla(ignition::math::Vector3d lla_i) +inline struct LlaCoor_d to_pprz_lla(gz::math::Vector3d lla_i) { struct LlaCoor_d lla_p; lla_p.lat = lla_i.X(); @@ -177,7 +202,7 @@ inline struct LlaCoor_d to_pprz_lla(ignition::math::Vector3d lla_i) return lla_p; } -inline struct DoubleVect3 to_pprz_body(ignition::math::Vector3d body_g) +inline struct DoubleVect3 to_pprz_body(gz::math::Vector3d body_g) { struct DoubleVect3 body_p; body_p.x = body_g.X(); @@ -186,7 +211,7 @@ inline struct DoubleVect3 to_pprz_body(ignition::math::Vector3d body_g) return body_p; } -inline struct DoubleRates to_pprz_rates(ignition::math::Vector3d body_g) +inline struct DoubleRates to_pprz_rates(gz::math::Vector3d body_g) { struct DoubleRates body_p; body_p.p = body_g.X(); @@ -195,7 +220,7 @@ inline struct DoubleRates to_pprz_rates(ignition::math::Vector3d body_g) return body_p; } -inline struct DoubleEulers to_pprz_eulers(ignition::math::Quaterniond quat) +inline struct DoubleEulers to_pprz_eulers(gz::math::Quaterniond quat) { struct DoubleEulers eulers; eulers.psi = -quat.Yaw(); @@ -204,7 +229,7 @@ inline struct DoubleEulers to_pprz_eulers(ignition::math::Quaterniond quat) return eulers; } -inline struct DoubleEulers to_global_pprz_eulers(ignition::math::Quaterniond quat) +inline struct DoubleEulers to_global_pprz_eulers(gz::math::Quaterniond quat) { struct DoubleEulers eulers; eulers.psi = -quat.Yaw() - M_PI / 2; @@ -213,7 +238,7 @@ inline struct DoubleEulers to_global_pprz_eulers(ignition::math::Quaterniond qua return eulers; } -inline struct DoubleVect3 to_pprz_ltp(ignition::math::Vector3d xyz) +inline struct DoubleVect3 to_pprz_ltp(gz::math::Vector3d xyz) { struct DoubleVect3 ltp; ltp.x = xyz.Y(); @@ -240,15 +265,17 @@ void nps_fdm_init(double dt) for (int i = 0; i < NPS_COMMANDS_NB; i++) { init_first_order_low_pass(&gazebo_actuators.lowpass[i], tau[i], dt, 0.f); } +#endif #ifdef NPS_ACTUATOR_MAX_ANGULAR_MOMENTUM - // Set up high-pass filter to simulate spinup torque - const float Iwmax[NPS_COMMANDS_NB] = NPS_ACTUATOR_MAX_ANGULAR_MOMENTUM; + const float max_ang_mom[NPS_COMMANDS_NB] = NPS_ACTUATOR_MAX_ANGULAR_MOMENTUM; for (int i = 0; i < NPS_COMMANDS_NB; i++) { - init_first_order_high_pass(&gazebo_actuators.highpass[i], tau[i], dt, 0.f); - gazebo_actuators.max_ang_momentum[i] = Iwmax[i]; + gazebo_actuators.max_ang_momentum[i] = max_ang_mom[i]; + init_first_order_high_pass(&gazebo_actuators.highpass[i], 1. / (2 * M_PI * 5), dt, 0.f); } #endif -#endif + + init_gazebo(); + gazebo_read(); } /** @@ -257,749 +284,562 @@ void nps_fdm_init(double dt) * @param act_commands * @param commands_nb */ -void nps_fdm_run_step( - bool launch __attribute__((unused)), - double *act_commands, - int commands_nb) +void nps_fdm_run_step(bool launch __attribute__((unused)), double *act_commands, int commands_nb) { - // Initialize Gazebo if req'd. - // Initialization is peformed here instead of in nps_fdm_init because: - // - Video devices need to added at this point. Video devices have not been - // added yet when nps_fdm_init is called. - // - nps_fdm_init runs on a different thread then nps_fdm_run_step, which - // causes problems with Gazebo. - if (!gazebo_initialized) { - init_gazebo(); - gazebo_read(); -#if NPS_SIMULATE_VIDEO - init_gazebo_video(); -#endif -#if NPS_SIMULATE_LASER_RANGE_ARRAY - gazebo_init_range_sensors(); -#endif - gazebo_initialized = true; + // Actuator dynamics, thrust and torque + gazebo_write(act_commands, commands_nb); + + // Run simulation step + if (server) { + server->Run(true, 1, false); } - // Update the simulation for a single timestep. - gazebo::runWorld(model->GetWorld(), 1); - gazebo::sensors::run_once(); - gazebo_write(act_commands, commands_nb); + // Update FDM state gazebo_read(); + #if NPS_SIMULATE_VIDEO gazebo_read_video(); #endif + #if NPS_SIMULATE_LASER_RANGE_ARRAY gazebo_read_range_sensors(); #endif - } -// TODO Atmosphere functions have not been implemented yet. -// Starting at version 8, Gazebo has its own atmosphere and wind model. -void nps_fdm_set_wind( - double speed __attribute__((unused)), - double dir __attribute__((unused))) +void nps_fdm_set_wind(double speed __attribute__((unused)), + double dir __attribute__((unused))) { + // Not implemented in Gazebo Harmonic yet } -void nps_fdm_set_wind_ned( - double wind_north __attribute__((unused)), - double wind_east __attribute__((unused)), - double wind_down __attribute__((unused))) +void nps_fdm_set_wind_ned(double wind_north __attribute__((unused)), + double wind_east __attribute__((unused)), + double wind_down __attribute__((unused))) { + // Not implemented in Gazebo Harmonic yet } -void nps_fdm_set_turbulence( - double wind_speed __attribute__((unused)), - int turbulence_severity __attribute__((unused))) +void nps_fdm_set_turbulence(double wind_speed __attribute__((unused)), + int turbulence_severity __attribute__((unused))) { + // Not implemented in Gazebo Harmonic yet } -/** Set temperature in degrees Celcius at given height h above MSL */ -void nps_fdm_set_temperature( - double temp __attribute__((unused)), - double h __attribute__((unused))) +void nps_fdm_set_temperature(double temp __attribute__((unused)), + double h __attribute__((unused))) { + // Not implemented in Gazebo Harmonic yet } -// Internal functions /** - * Set up a Gazebo server. - * - * Starts a Gazebo server, adds conf/simulator/gazebo/models to its model path - * and loads the world specified by NPS_GAZEBO_WORLD. - * - * This function also obtaines a pointer to the aircraft model, named - * NPS_GAZEBO_AC_NAME ('paparazzi_uav' by default). This pointer, 'model', - * is used to read the state and write actuator commands in gazebo_read and - * _write. + * Set up a Gazebo Harmonic server and load the world. */ static void init_gazebo(void) { - string gazebo_home = "/conf/simulator/gazebo/"; - string pprz_home(getenv("PAPARAZZI_HOME")); - string gazebodir = pprz_home + gazebo_home; - cout << "Gazebo directory: " << gazebodir << endl; + cout << "Initializing Gazebo Harmonic server..." << endl; - if (getenv("ROS_MASTER_URI")) { - // Launch with ROS support - cout << "Add ROS plugins... "; - gazebo::addPlugin("libgazebo_ros_paths_plugin.so"); - gazebo::addPlugin("libgazebo_ros_api_plugin.so"); - cout << "ok" << endl; + // Configure server + gz::sim::ServerConfig serverConfig; + + // Load world file + string worldFile = string(getenv("PAPARAZZI_HOME")) + "/conf/simulator/gazebo/worlds/" + NPS_GAZEBO_WORLD; + serverConfig.SetSdfFile(worldFile); + + // Create server + server = new gz::sim::Server(serverConfig); + + if (!server || !server->Running()) { + cerr << "Failed to start Gazebo Harmonic server" << endl; + exit(1); } - if (!gazebo::setupServer(0, NULL)) { - cout << "Failed to start Gazebo, exiting." << endl; - std::exit(-1); + // Run a few iterations to let the world load + server->Run(true, 100, false); + + cout << "Gazebo Harmonic server started successfully" << endl; + + // Get entity component manager through a system callback + bool ecmFound = false; + server->SetUpdatePeriod(std::chrono::duration(fdm.init_dt)); + + // Access ECM and EventManager + auto callback = [&](const gz::sim::UpdateInfo &_info, + gz::sim::EntityComponentManager &_ecm) + { + if (!ecmFound) { + ecm = &_ecm; + ecmFound = true; + + // Find model entity + gz::sim::Entity worldEntity = gz::sim::worldEntity(*ecm); + + ecm->Each( + [&](const gz::sim::Entity &_entity, + const gz::sim::components::Model *, + const gz::sim::components::Name *_name) -> bool + { + if (_name->Data() == NPS_GAZEBO_AC_NAME) { + modelEntity = _entity; + model = gz::sim::Model(_entity); + cout << "Found model: " << NPS_GAZEBO_AC_NAME << endl; + return false; + } + return true; + }); + + if (modelEntity == gz::sim::kNullEntity) { + cerr << "Model '" << NPS_GAZEBO_AC_NAME << "' not found!" << endl; + exit(1); + } + } + }; + + // Register the callback as a system + server->AddSystem(std::make_shared>(callback)); + + // Run until ECM is found + for (int i = 0; i < 100 && !ecmFound; i++) { + server->Run(true, 1, false); } - cout << "Add Paparazzi paths: " << gazebodir << endl; - gazebo::common::SystemPaths::Instance()->AddModelPaths(gazebodir + "models/"); - sdf::addURIPath("model://", gazebodir + "models/"); - sdf::addURIPath("world://", gazebodir + "world/"); - - cout << "Add TU Delft paths: " << pprz_home + "/sw/ext/tudelft_gazebo_models/" << endl; - gazebo::common::SystemPaths::Instance()->AddModelPaths(pprz_home + "/sw/ext/tudelft_gazebo_models/models/"); - sdf::addURIPath("model://", pprz_home + "/sw/ext/tudelft_gazebo_models/models/"); - sdf::addURIPath("world://", pprz_home + "/sw/ext/tudelft_gazebo_models/world/"); - - // get vehicles - string vehicle_uri = "model://" + string(NPS_GAZEBO_AC_NAME) + "/" + string(NPS_GAZEBO_AC_NAME) + ".sdf"; - string vehicle_filename = sdf::findFile(vehicle_uri, false); - if (vehicle_filename.empty()) { - cout << "ERROR, could not find vehicle " + vehicle_uri << endl; - std::exit(-1); - } - cout << "Load vehicle: " << vehicle_filename << endl; - sdf::SDFPtr vehicle_sdf(new sdf::SDF()); - sdf::init(vehicle_sdf); - if (!sdf::readFile(vehicle_filename, vehicle_sdf)) { - cout << "ERROR, could not read vehicle " + vehicle_filename << endl; - std::exit(-1); + if (!ecmFound || !ecm) { + cerr << "Failed to get EntityComponentManager" << endl; + exit(1); } - // add or set up sensors before the vehicle gets loaded + gazebo_initialized = true; + #if NPS_SIMULATE_VIDEO - // Cameras - sdf::ElementPtr link = vehicle_sdf->Root()->GetFirstElement()->GetElement("link"); - while (link) { - if (link->Get("name") == "front_camera" && link->GetElement("sensor")->Get("name") == "mt9f002") { - if (NPS_MT9F002_SENSOR_RES_DIVIDER != 1) { - int w = link->GetElement("sensor")->GetElement("camera")->GetElement("image")->GetElement("width")->Get(); - int h = link->GetElement("sensor")->GetElement("camera")->GetElement("image")->GetElement("height")->Get(); - int env = link->GetElement("sensor")->GetElement("camera")->GetElement("lens")->GetElement("env_texture_size")->Get(); - link->GetElement("sensor")->GetElement("camera")->GetElement("image")->GetElement("width")->Set(w / NPS_MT9F002_SENSOR_RES_DIVIDER); - link->GetElement("sensor")->GetElement("camera")->GetElement("image")->GetElement("height")->Set(h / NPS_MT9F002_SENSOR_RES_DIVIDER); - link->GetElement("sensor")->GetElement("camera")->GetElement("lens")->GetElement("env_texture_size")->Set(env / NPS_MT9F002_SENSOR_RES_DIVIDER); - } - if (MT9F002_TARGET_FPS){ - int fps = Min(MT9F002_TARGET_FPS, link->GetElement("sensor")->GetElement("update_rate")->Get()); - link->GetElement("sensor")->GetElement("update_rate")->Set(fps); - } - } else if (link->Get("name") == "bottom_camera" && link->GetElement("sensor")->Get("name") == "mt9v117") { - if (MT9V117_TARGET_FPS){ - int fps = Min(MT9V117_TARGET_FPS, link->GetElement("sensor")->GetElement("update_rate")->Get()); - link->GetElement("sensor")->GetElement("update_rate")->Set(fps); - } - } - link = link->GetNextElement("link"); - } + init_gazebo_video(); #endif - // laser range array #if NPS_SIMULATE_LASER_RANGE_ARRAY - vehicle_sdf->Root()->GetFirstElement()->AddElement("include")->GetElement("uri")->Set("model://range_sensors"); - sdf::ElementPtr range_joint = vehicle_sdf->Root()->GetFirstElement()->AddElement("joint"); - range_joint->GetAttribute("name")->Set("range_sensor_joint"); - range_joint->GetAttribute("type")->Set("fixed"); - range_joint->GetElement("parent")->Set("chassis"); - range_joint->GetElement("child")->Set("range_sensors::base"); + gazebo_init_range_sensors(); #endif - // get world - string world_uri = "world://" + string(NPS_GAZEBO_WORLD); - string world_filename = sdf::findFile(world_uri, false); - if (world_filename.empty()) { - cout << "ERROR, could not find world " + world_uri << endl; - std::exit(-1); - } - cout << "Load world: " << world_filename << endl; - sdf::SDFPtr world_sdf(new sdf::SDF()); - sdf::init(world_sdf); - if (!sdf::readFile(world_filename, world_sdf)) { - cout << "ERROR, could not read world " + world_filename << endl; - std::exit(-1); - } - - // add vehicles - world_sdf->Root()->GetFirstElement()->InsertElement(vehicle_sdf->Root()->GetFirstElement()); - - world_sdf->Write(pprz_home + "/var/gazebo.world"); - - gazebo::physics::WorldPtr world = gazebo::loadWorld(pprz_home + "/var/gazebo.world"); - if (!world) { - cout << "Failed to open world, exiting." << endl; - std::exit(-1); - } - - cout << "Get pointer to aircraft: " << NPS_GAZEBO_AC_NAME << endl; - model = world->ModelByName(NPS_GAZEBO_AC_NAME); - if (!model) { - cout << "Failed to find '" << NPS_GAZEBO_AC_NAME << "', exiting." - << endl; - std::exit(-1); - } - - // Initialize sensors - gazebo::sensors::run_once(true); - gazebo::sensors::run_threads(); - gazebo::runWorld(world, 1); - cout << "Sensors initialized..." << endl; - - // Find sensors - // Contact sensor - gazebo::sensors::SensorManager *mgr = gazebo::sensors::SensorManager::Instance(); - ct = static_pointer_cast < gazebo::sensors::ContactSensor > (mgr->GetSensor("contactsensor")); - ct->SetActive(true); - // Sonar - sonar = static_pointer_cast(mgr->GetSensor("sonarsensor")); - if(sonar) { - cout << "Found sonar" << endl; - } - - gazebo::physics::LinkPtr sonar_link = model->GetLink("sonar"); - if (sonar_link) { - // Get a pointer to the sensor using its full name - if (sonar_link->GetSensorCount() != 1) { - cout << "ERROR: Link '" << sonar_link->GetName() - << "' should only contain 1 sensor!" << endl; - } else { - string name = sonar_link->GetSensorName(0); - sonar = static_pointer_cast< gazebo::sensors::SonarSensor > (mgr->GetSensor(name)); - if (!sonar) { - cout << "ERROR: Could not get pointer to '" << name << "'!" << endl; - } else { - // Activate sensor - sonar->SetActive(true); - } - } - } - - // Overwrite motor directions as defined by motor_mixing -#ifdef MOTOR_MIXING_YAW_COEF - const double yaw_coef[] = MOTOR_MIXING_YAW_COEF; - - for (uint8_t i = 0; i < NPS_COMMANDS_NB; i++) { - gazebo_actuators.torques[i] = -fabs(gazebo_actuators.torques[i]) * yaw_coef[i] / fabs(yaw_coef[i]); - gazebo_actuators.max_ang_momentum[i] = -fabs(gazebo_actuators.max_ang_momentum[i]) * yaw_coef[i] / fabs(yaw_coef[i]); - } -#endif - cout << "Gazebo initialized successfully!" << endl; + cout << "Gazebo initialization complete" << endl; } /** - * Read Gazebo's simulation state and store the results in the fdm struct used - * by NPS. - * - * Not all fields are filled at the moment, as some of them are unused by - * paparazzi (see comments) and others are not available in Gazebo 7 - * (atmosphere). + * Read Gazebo Harmonic state and populate FDM struct. */ static void gazebo_read(void) { - static ignition::math::Vector3d vel_prev; - static double time_prev; - - gazebo::physics::WorldPtr world = model->GetWorld(); - ignition::math::Pose3d pose = model->WorldPose(); // In LOCAL xyz frame - ignition::math::Vector3d vel = model->WorldLinearVel(); - ignition::math::Vector3d ang_vel = model->WorldAngularVel(); - gazebo::common::SphericalCoordinatesPtr sphere = world->SphericalCoords(); - ignition::math::Quaterniond local_to_global_quat(0, 0, -sphere->HeadingOffset().Radian()); - - /* Fill FDM struct */ - fdm.time = world->SimTime().Double(); - - // Find world acceleration by differentiating velocity - // model->GetWorldLinearAccel() does not seem to take the velocity_decay into account! - // Derivation of the velocity also follows the IMU implementation of Gazebo itself: - // https://bitbucket.org/osrf/gazebo/src/e26144434b932b4b6a760ddaa19cfcf9f1734748/gazebo/sensors/ImuSensor.cc?at=default&fileviewer=file-view-default#ImuSensor.cc-370 - double dt = fdm.time - time_prev; - ignition::math::Vector3d accel = (vel - vel_prev) / dt; - vel_prev = vel; - time_prev = fdm.time; - - // init_dt: unused - // curr_dt: unused - // on_ground: unused - // nan_count: unused - - // Transform ltp definition to double for accuracy - struct LtpDef_d ltpdef_d; - ltpdef_d.ecef.x = state.ned_origin_f.ecef.x; - ltpdef_d.ecef.y = state.ned_origin_f.ecef.y; - ltpdef_d.ecef.z = state.ned_origin_f.ecef.z; - ltpdef_d.lla.lat = state.ned_origin_f.lla.lat; - ltpdef_d.lla.lon = state.ned_origin_f.lla.lon; - ltpdef_d.lla.alt = state.ned_origin_f.lla.alt; - for (int i = 0; i < 3 * 3; i++) { - ltpdef_d.ltp_of_ecef.m[i] = state.ned_origin_f.ltp_of_ecef.m[i]; + if (!ecm || modelEntity == gz::sim::kNullEntity) { + return; } - ltpdef_d.hmsl = state.ned_origin_f.hmsl; - /* position */ - fdm.ltpprz_pos = to_pprz_ned(sphere->PositionTransform(pose.Pos(), gazebo::common::SphericalCoordinates::LOCAL, - gazebo::common::SphericalCoordinates::GLOBAL)); // Allows Gazebo worlds rotated wrt north. - fdm.hmsl = -fdm.ltpprz_pos.z; - ecef_of_ned_point_d(&fdm.ecef_pos, <pdef_d, &fdm.ltpprz_pos); - lla_of_ecef_d(&fdm.lla_pos, &fdm.ecef_pos); + // Get world pose + auto poseComp = ecm->Component(modelEntity); + if (!poseComp) { + return; + } + gz::math::Pose3d pose = poseComp->Data(); - /* debug positions */ - fdm.lla_pos_pprz = fdm.lla_pos; // Don't really care... - fdm.lla_pos_geod = fdm.lla_pos; - fdm.lla_pos_geoc = fdm.lla_pos; - fdm.lla_pos_geoc.lat = gc_of_gd_lat_d(fdm.lla_pos.lat, fdm.hmsl); + // Get world velocity + auto worldLinVelComp = ecm->Component(modelEntity); + auto worldAngVelComp = ecm->Component(modelEntity); + + gz::math::Vector3d worldLinVel(0, 0, 0); + gz::math::Vector3d worldAngVel(0, 0, 0); + + if (worldLinVelComp) { + worldLinVel = worldLinVelComp->Data(); + } + if (worldAngVelComp) { + worldAngVel = worldAngVelComp->Data(); + } - if(sonar) { - double agl = sonar->Range(); - if (agl > sonar->RangeMax()) agl = -1.0; - fdm.agl = agl; + // Get body velocity (in body frame) + gz::math::Vector3d bodyLinVel = pose.Rot().RotateVectorReverse(worldLinVel); + gz::math::Vector3d bodyAngVel = pose.Rot().RotateVectorReverse(worldAngVel); + + // Get world acceleration + auto worldLinAccelComp = ecm->Component(modelEntity); + auto worldAngAccelComp = ecm->Component(modelEntity); + + gz::math::Vector3d worldLinAccel(0, 0, 0); + gz::math::Vector3d worldAngAccel(0, 0, 0); + + if (worldLinAccelComp) { + worldLinAccel = worldLinAccelComp->Data(); + } + if (worldAngAccelComp) { + worldAngAccel = worldAngAccelComp->Data(); + } + + gz::math::Vector3d bodyLinAccel = pose.Rot().RotateVectorReverse(worldLinAccel); + gz::math::Vector3d bodyAngAccel = pose.Rot().RotateVectorReverse(worldAngAccel); + + // Fill FDM struct + fdm.ecef_pos = to_pprz_ecef(pose.Pos()); + fdm.ltpprz_pos = to_pprz_ltp(pose.Pos()); + fdm.ltp_ecef_vel = to_pprz_ltp(worldLinVel); + fdm.body_ecef_vel = to_pprz_body(bodyLinVel); + fdm.body_ecef_accel = to_pprz_body(bodyLinAccel); + fdm.body_inertial_accel = fdm.body_ecef_accel; + fdm.body_accel = fdm.body_ecef_accel; + fdm.ltp_ecef_accel = to_pprz_ltp(worldLinAccel); + + fdm.ecef_ecef_vel = fdm.ltp_ecef_vel; + fdm.ecef_ecef_accel = fdm.ltp_ecef_accel; + + fdm.hmsl = pose.Pos().Z(); + + fdm.ltp_g = to_pprz_ltp(gz::math::Vector3d(0, 0, -9.81)); + fdm.ltp_h = to_pprz_ltp(gz::math::Vector3d(0, 0, -9.81)); + + // Attitude + fdm.ltp_to_body_eulers = to_global_pprz_eulers(pose.Rot()); + fdm.ltpprz_to_body_eulers = to_pprz_eulers(pose.Rot()); + + // Rates + fdm.body_ecef_rotvel = to_pprz_rates(bodyAngVel); + fdm.body_ecef_rotaccel = to_pprz_rates(bodyAngAccel); + fdm.body_inertial_rotvel = fdm.body_ecef_rotvel; + fdm.body_inertial_rotaccel = fdm.body_ecef_rotaccel; + + // LLA position + // For Gazebo Harmonic, we need to use the spherical coordinates component + auto sphericalCoordComp = ecm->Component( + gz::sim::worldEntity(*ecm)); + + if (sphericalCoordComp) { + auto sc = sphericalCoordComp->Data(); + gz::math::Vector3d lla = sc.PositionTransform(pose.Pos(), + gz::math::SphericalCoordinates::LOCAL2, + gz::math::SphericalCoordinates::SPHERICAL); + fdm.lla_pos = to_pprz_lla(lla); } else { - fdm.agl = pose.Pos().Z(); // TODO Measure with sensor + // Fallback if no spherical coordinates + fdm.lla_pos.lat = 0; + fdm.lla_pos.lon = 0; + fdm.lla_pos.alt = pose.Pos().Z(); } - /* velocity */ - fdm.ltp_ecef_vel = to_pprz_ned(sphere->VelocityTransform(vel, gazebo::common::SphericalCoordinates::LOCAL, - gazebo::common::SphericalCoordinates::GLOBAL)); - fdm.ltpprz_ecef_vel = fdm.ltp_ecef_vel; // ??? - fdm.body_ecef_vel = to_pprz_body(pose.Rot().RotateVectorReverse(vel)); // Note: unused - ecef_of_ned_vect_d(&fdm.ecef_ecef_vel, <pdef_d, &fdm.ltp_ecef_vel); + // Atmospheric properties + fdm.pressure_sl = pprz_isa_pressure_of_height(0); + fdm.pressure = pprz_isa_pressure_of_height(fdm.hmsl); + fdm.total_pressure = fdm.pressure + 0.5 * 1.225 * + (fdm.body_ecef_vel.x * fdm.body_ecef_vel.x + + fdm.body_ecef_vel.y * fdm.body_ecef_vel.y + + fdm.body_ecef_vel.z * fdm.body_ecef_vel.z); + fdm.dynamic_pressure = fdm.total_pressure - fdm.pressure; + fdm.temperature = pprz_isa_temperature_of_height(fdm.hmsl); - /* acceleration */ - fdm.ltp_ecef_accel = to_pprz_ned(sphere->VelocityTransform(accel, gazebo::common::SphericalCoordinates::LOCAL, - gazebo::common::SphericalCoordinates::GLOBAL)); // Note: unused - fdm.ltpprz_ecef_accel = fdm.ltp_ecef_accel; // ??? - fdm.body_ecef_accel = to_pprz_body(pose.Rot().RotateVectorReverse(accel)); - fdm.body_inertial_accel = fdm.body_ecef_accel; // Approximate, unused. - fdm.body_accel = to_pprz_body(pose.Rot().RotateVectorReverse(accel - world->Gravity())); - ecef_of_ned_vect_d(&fdm.ecef_ecef_accel, <pdef_d, &fdm.ltp_ecef_accel); + // Airspeed (simplified, assuming no wind) + fdm.airspeed = sqrt(fdm.body_ecef_vel.x * fdm.body_ecef_vel.x + + fdm.body_ecef_vel.y * fdm.body_ecef_vel.y + + fdm.body_ecef_vel.z * fdm.body_ecef_vel.z); - /* attitude */ - // ecef_to_body_quat: unused - fdm.ltp_to_body_eulers = to_global_pprz_eulers(local_to_global_quat * pose.Rot()); - double_quat_of_eulers(&(fdm.ltp_to_body_quat), &(fdm.ltp_to_body_eulers)); - fdm.ltpprz_to_body_quat = fdm.ltp_to_body_quat; // unused - fdm.ltpprz_to_body_eulers = fdm.ltp_to_body_eulers; // unused + // Ground contact + fdm.agl = pose.Pos().Z(); // Simplified + if (ct) { + // Check contact sensor (implementation depends on sensor setup) + fdm.on_ground = (fdm.agl < 0.01); + } else { + fdm.on_ground = (fdm.agl < 0.01); + } - /* angular velocity */ - fdm.body_ecef_rotvel = to_pprz_rates(pose.Rot().RotateVectorReverse(ang_vel)); - fdm.body_inertial_rotvel = fdm.body_ecef_rotvel; // Approximate - - /* angular acceleration */ - // body_ecef_rotaccel: unused - // body_inertial_rotaccel: unused - /* misc */ - fdm.ltp_g = to_pprz_ltp(sphere->VelocityTransform(-1 * world->Gravity(), gazebo::common::SphericalCoordinates::LOCAL, - gazebo::common::SphericalCoordinates::GLOBAL)); // unused - fdm.ltp_h = to_pprz_ltp(sphere->VelocityTransform(world->MagneticField(), gazebo::common::SphericalCoordinates::LOCAL, - gazebo::common::SphericalCoordinates::GLOBAL)); - - /* atmosphere */ -#if GAZEBO_MAJOR_VERSION >= 8 && 0 // TODO implement - -#else - // Gazebo versions < 8 do not have atmosphere or wind support - // Use placeholder values. Valid for low altitude, low speed flights. - fdm.wind = (struct DoubleVect3) {0, 0, 0}; - fdm.pressure_sl = 101325; // Pa - - fdm.airspeed = 0; - fdm.pressure = pprz_isa_pressure_of_height(fdm.hmsl, fdm.pressure_sl); - fdm.dynamic_pressure = fdm.pressure_sl; // Pa - fdm.temperature = 20.0; // C - fdm.aoa = 0; // rad - fdm.sideslip = 0; // rad -#endif - /* flight controls: unused */ - fdm.elevator = 0; - fdm.flap = 0; - fdm.left_aileron = 0; - fdm.right_aileron = 0; - fdm.rudder = 0; - /* engine: unused */ - fdm.num_engines = 0; + // Simulation time + auto simTimeComp = ecm->Component( + gz::sim::worldEntity(*ecm)); + if (simTimeComp) { + auto simTime = simTimeComp->Data(); + fdm.time = std::chrono::duration(simTime).count(); + } } /** - * Write actuator commands to Gazebo. - * - * This function takes the normalized commands and applies them as forces and - * torques in Gazebo. Since the commands are normalized in [0,1], their - * thrusts (NPS_ACTUATOR_THRUSTS) and torques (NPS_ACTUATOR_TORQUES) need to - * be specified in the airframe file. Their values need to be specified in the - * same order as the NPS_ACTUATOR_NAMES and should be provided in SI units. - * See conf/airframes/examples/ardrone2_gazebo.xml for an example. - * - * The forces and torques are applied to (the origin of) the links named in - * NPS_ACTUATOR_NAMES. See conf/simulator/gazebo/models/ardrone/ardrone.sdf - * for an example. - * - * @param act_commands - * @param commands_nb + * Write actuator commands to Gazebo Harmonic. + * + * @param act_commands Array of actuator commands + * @param commands_nb Number of actuator commands */ static void gazebo_write(double act_commands[], int commands_nb) { - for (int i = 0; i < commands_nb; ++i) { - // Thrust setpoint - double sp = autopilot.motors_on ? act_commands[i] : 0.0; // Normalized thrust setpoint + if (!ecm || modelEntity == gz::sim::kNullEntity) { + return; + } - // Actuator dynamics, forces and torques + // Apply actuator dynamics + for (int i = 0; i < commands_nb; i++) { + double input = act_commands[i]; + #ifdef NPS_ACTUATOR_TIME_CONSTANTS - // Delayed response from actuator - double u = update_first_order_low_pass(&gazebo_actuators.lowpass[i], sp);// Normalized actual thrust -#else - double u = sp; + // Low-pass filter for actuator dynamics + double filtered = update_first_order_low_pass(&gazebo_actuators.lowpass[i], input); + act_commands[i] = filtered; #endif - double thrust = gazebo_actuators.thrusts[i] * u; - double torque = gazebo_actuators.torques[i] * u; #ifdef NPS_ACTUATOR_MAX_ANGULAR_MOMENTUM - // Spinup torque - double udot = update_first_order_high_pass(&gazebo_actuators.highpass[i], sp); - double spinup_torque = gazebo_actuators.max_ang_momentum[i] / (2.0 * sqrt(u > 0.05 ? u : 0.05)) * udot; - torque += spinup_torque; + // High-pass filter for back-emf effects + double ang_vel = update_first_order_high_pass(&gazebo_actuators.highpass[i], act_commands[i]); + double back_emf_reduction = fabs(ang_vel) / gazebo_actuators.max_ang_momentum[i]; + back_emf_reduction = (back_emf_reduction > 1.0) ? 1.0 : back_emf_reduction; + act_commands[i] *= (1.0 - back_emf_reduction); #endif + } - // Apply force and torque to gazebo model - gazebo::physics::LinkPtr link = model->GetLink(gazebo_actuators.names[i]); - link->AddRelativeForce(ignition::math::Vector3d(0, 0, thrust)); - link->AddRelativeTorque(ignition::math::Vector3d(0, 0, torque)); + // Find links and apply forces/torques + model = gz::sim::Model(modelEntity); + + for (int i = 0; i < commands_nb; i++) { + // Find link by name + gz::sim::Entity linkEntity = model.LinkByName(*ecm, gazebo_actuators.names[i]); + + if (linkEntity != gz::sim::kNullEntity) { + // Calculate thrust force + double thrust = gazebo_actuators.thrusts[i] * act_commands[i]; + double torque = gazebo_actuators.torques[i] * act_commands[i]; + + // Apply force in link's local z-direction (upward) + gz::math::Vector3d force(0, 0, thrust); + gz::math::Vector3d torqueVec(0, 0, torque); + + // Get or create WorldForce component + auto forceComp = ecm->Component(linkEntity); + if (!forceComp) { + ecm->CreateComponent(linkEntity, + gz::sim::components::WorldForce(force)); + } else { + // Add to existing force + auto currentForce = forceComp->Data(); + ecm->SetComponentData(linkEntity, + currentForce + force); + } + + // Get or create WorldTorque component + auto torqueComp = ecm->Component(linkEntity); + if (!torqueComp) { + ecm->CreateComponent(linkEntity, + gz::sim::components::WorldTorque(torqueVec)); + } else { + // Add to existing torque + auto currentTorque = torqueComp->Data(); + ecm->SetComponentData(linkEntity, + currentTorque + torqueVec); + } + } } } #if NPS_SIMULATE_VIDEO /** - * Set up cameras. - * - * This function finds the video devices added through add_video_device - * (sw/airborne/modules/computer_vision/cv.h). The camera links in the Gazebo AC - * model should have the same name as the .dev_name field in the corresponding - * video_config_t struct stored in 'cameras[]' (computer_vision/ - * video_thread_nps.h). Pointers to Gazebo's cameras are stored in gazebo_cams - * at the same index as their 'cameras[]' counterpart. - * - * The video_config_t parameters are updated using the values provided by - * Gazebo. This should simplify the use of different UAVs with different camera - * setups. + * Initialize Gazebo Harmonic cameras for video simulation. */ static void init_gazebo_video(void) { - gazebo::sensors::SensorManager *mgr = gazebo::sensors::SensorManager::Instance(); - cout << "Initializing cameras..." << endl; - // Loop over cameras registered in video_thread_nps - for (int i = 0; i < VIDEO_THREAD_MAX_CAMERAS && cameras[i] != NULL; ++i) { - // Find link in gazebo model - cout << "Setting up '" << cameras[i]->dev_name << "'... "; - gazebo::physics::LinkPtr link = model->GetLink(cameras[i]->dev_name); - if (!link) { - cout << "ERROR: Link '" << cameras[i]->dev_name << "' not found!" - << endl; - ; - continue; - } - // Get a pointer to the sensor using its full name - if (link->GetSensorCount() != 1) { - cout << "ERROR: Link '" << link->GetName() - << "' should only contain 1 sensor!" << endl; - continue; - } - string name = link->GetSensorName(0); - gazebo::sensors::CameraSensorPtr cam = static_pointer_cast - < gazebo::sensors::CameraSensor > (mgr->GetSensor(name)); - if (!cam) { - cout << "ERROR: Could not get pointer to '" << name << "'!" << endl; - continue; - } - // Activate sensor - cam->SetActive(true); - // Add to list of cameras - gazebo_cams[i].cam = cam; - gazebo_cams[i].last_measurement_time = cam->LastMeasurementTime(); + // Gazebo Harmonic uses gz-sensors for camera access + gz::sensors::SensorFactory sensorFactory; + + // For now, this is a placeholder + // In Gazebo Harmonic, camera sensors are typically accessed via topics + // You would subscribe to camera image topics using gz::transport::Node + + cout << "Camera initialization for Gazebo Harmonic requires topic-based approach" << endl; + cout << "Subscribe to camera topics like /camera or /camera/image" << endl; - // set default camera settings - // Copy video_config settings from Gazebo's camera - cameras[i]->output_size.w = cam->ImageWidth(); - cameras[i]->output_size.h = cam->ImageHeight(); - cameras[i]->sensor_size.w = cam->ImageWidth(); - cameras[i]->sensor_size.h = cam->ImageHeight(); - cameras[i]->crop.w = cam->ImageWidth(); - cameras[i]->crop.h = cam->ImageHeight(); - cameras[i]->fps = 0; - cameras[i]->camera_intrinsics.focal_x = cameras[i]->output_size.w / 2.0f; - cameras[i]->camera_intrinsics.center_x = cameras[i]->output_size.w / 2.0f; - cameras[i]->camera_intrinsics.focal_y = cameras[i]->output_size.h / 2.0f; - cameras[i]->camera_intrinsics.center_y = cameras[i]->output_size.h / 2.0f; - - if (cam->Name() == "mt9f002") { - // See boards/bebop/mt9f002.c - cameras[i]->output_size.w = MT9F002_OUTPUT_WIDTH; - cameras[i]->output_size.h = MT9F002_OUTPUT_HEIGHT; - cameras[i]->sensor_size.w = MT9F002_OUTPUT_WIDTH; - cameras[i]->sensor_size.h = MT9F002_OUTPUT_HEIGHT; - cameras[i]->crop.w = MT9F002_OUTPUT_WIDTH; - cameras[i]->crop.h = MT9F002_OUTPUT_HEIGHT; - cameras[i]->fps = MT9F002_TARGET_FPS; - cameras[i]->camera_intrinsics = { - .focal_x = MT9F002_FOCAL_X, - .focal_y = MT9F002_FOCAL_Y, - .center_x = MT9F002_CENTER_X, - .center_y = MT9F002_CENTER_Y, - .Dhane_k = MT9F002_DHANE_K - }; - } else if (cam->Name() == "mt9v117") { - // See boards/bebop/mt9v117.h - cameras[i]->fps = MT9V117_TARGET_FPS; - cameras[i]->camera_intrinsics = { - .focal_x = MT9V117_FOCAL_X, - .focal_y = MT9V117_FOCAL_Y, - .center_x = MT9V117_CENTER_X, - .center_y = MT9V117_CENTER_Y, - .Dhane_k = MT9V117_DHANE_K - }; + // Example: Set up topic subscribers for cameras + for (int i = 0; i < VIDEO_THREAD_MAX_CAMERAS; ++i) { + if (cameras[i] != NULL) { + string topicName = string("/") + cameras[i]->dev_name + "/image"; + + // Subscribe to camera topic + std::function cb = + [i](const gz::msgs::Image &_msg) { + gazebo_cams[i].last_image_msg = _msg; + gazebo_cams[i].last_measurement_time = std::chrono::steady_clock::now(); + }; + + if (!node.Subscribe(topicName, cb)) { + cout << "Failed to subscribe to topic: " << topicName << endl; + } else { + cout << "Subscribed to camera topic: " << topicName << endl; + } } - cout << "ok" << endl; } + + cout << "Camera initialization complete" << endl; } /** - * Read camera images. - * - * Polls gazebo cameras. If the last measurement time has been updated, a new - * frame is available. This frame is converted to Paparazzi's UYVY format - * and passed to cv_run_device which runs the callbacks registered by various - * modules. + * Read camera images from Gazebo Harmonic. */ static void gazebo_read_video(void) { + // Video handling in Gazebo Harmonic is done through message passing + // Images are received via callbacks registered with gz::transport::Node + for (int i = 0; i < VIDEO_THREAD_MAX_CAMERAS; ++i) { - gazebo::sensors::CameraSensorPtr &cam = gazebo_cams[i].cam; - // Skip unregistered or unfound cameras - if (cam == NULL) { continue; } - // Skip if not updated - // Also skip when LastMeasurementTime() is zero (workaround) - if ((cam->LastMeasurementTime() - gazebo_cams[i].last_measurement_time).Float() < 0.005 - || cam->LastMeasurementTime() == 0) { continue; } - // Grab image, convert and send to video thread + if (cameras[i] == NULL) continue; + + auto &lastMsg = gazebo_cams[i].last_image_msg; + if (lastMsg.width() == 0 || lastMsg.height() == 0) continue; + + // Convert gz::msgs::Image to image_t struct image_t img; - read_image(&img, cam); + + // Determine camera type + bool is_mt9f002 = (string(cameras[i]->dev_name) == "mt9f002"); + + if (is_mt9f002) { + image_create(&img, MT9F002_OUTPUT_WIDTH, MT9F002_OUTPUT_HEIGHT, IMAGE_YUV422); + } else { + image_create(&img, lastMsg.width(), lastMsg.height(), IMAGE_YUV422); + } + + // Convert RGB to YUV422 + const unsigned char *data_rgb = + reinterpret_cast(lastMsg.data().c_str()); + uint8_t *data_yuv = (uint8_t *)(img.buf); + + for (int x_yuv = 0; x_yuv < img.w; ++x_yuv) { + for (int y_yuv = 0; y_yuv < img.h; ++y_yuv) { + int x_rgb = x_yuv; + int y_rgb = y_yuv; + + if (is_mt9f002) { + x_rgb = (mt9f002.offset_x + ((float)x_yuv / img.w) * mt9f002.sensor_width) + / CFG_MT9F002_PIXEL_ARRAY_WIDTH * lastMsg.width(); + y_rgb = (mt9f002.offset_y + ((float)y_yuv / img.h) * mt9f002.sensor_height) + / CFG_MT9F002_PIXEL_ARRAY_HEIGHT * lastMsg.height(); + } + + int idx_rgb = 3 * (lastMsg.width() * y_rgb + x_rgb); + int idx_yuv = 2 * (img.w * y_yuv + x_yuv); + int idx_px = img.w * y_yuv + x_yuv; + + if (idx_px % 2 == 0) { + data_yuv[idx_yuv] = - 0.148 * data_rgb[idx_rgb] + - 0.291 * data_rgb[idx_rgb + 1] + + 0.439 * data_rgb[idx_rgb + 2] + 128; // U + } else { + data_yuv[idx_yuv] = 0.439 * data_rgb[idx_rgb] + - 0.368 * data_rgb[idx_rgb + 1] + - 0.071 * data_rgb[idx_rgb + 2] + 128; // V + } + data_yuv[idx_yuv + 1] = 0.257 * data_rgb[idx_rgb] + + 0.504 * data_rgb[idx_rgb + 1] + + 0.098 * data_rgb[idx_rgb + 2] + 16; // Y + } + } + + // Set timestamp from message + auto sec = lastMsg.header().stamp().sec(); + auto nsec = lastMsg.header().stamp().nsec(); + img.ts.tv_sec = sec; + img.ts.tv_usec = nsec / 1000; + img.pprz_ts = (sec + nsec / 1e9) * 1e6; + img.buf_idx = 0; #if NPS_DEBUG_VIDEO - cv::Mat RGB_cam(cam->ImageHeight(), cam->ImageWidth(), CV_8UC3, (uint8_t *)cam->ImageData()); + cv::Mat RGB_cam(lastMsg.height(), lastMsg.width(), CV_8UC3, + (uint8_t *)data_rgb); cv::cvtColor(RGB_cam, RGB_cam, cv::COLOR_RGB2BGR); - cv::namedWindow(cameras[i]->dev_name, cv::WINDOW_AUTOSIZE); // Create a window for display. + cv::namedWindow(cameras[i]->dev_name, cv::WINDOW_AUTOSIZE); cv::imshow(cameras[i]->dev_name, RGB_cam); cv::waitKey(1); #endif cv_run_device(cameras[i], &img); - // Free image buffer after use. image_free(&img); - // Keep track of last update time. - gazebo_cams[i].last_measurement_time = cam->LastMeasurementTime(); } } - -/** - * Read Gazebo image and convert. - * - * Converts the current camera frame to the format used by Paparazzi. This - * includes conversion to UYVY. Gazebo's simulation time is used for the image - * timestamp. - * - * @param img - * @param cam - */ -static void read_image(struct image_t *img, gazebo::sensors::CameraSensorPtr cam) -{ - bool is_mt9f002 = false; - if (cam->Name() == "mt9f002") { - image_create(img, MT9F002_OUTPUT_WIDTH, MT9F002_OUTPUT_HEIGHT, IMAGE_YUV422); - is_mt9f002 = true; - } else { - image_create(img, cam->ImageWidth(), cam->ImageHeight(), IMAGE_YUV422); - } - - // Convert Gazebo's *RGB888* image to Paparazzi's YUV422 - const uint8_t *data_rgb = cam->ImageData(); - uint8_t *data_yuv = (uint8_t *)(img->buf); - for (int x_yuv = 0; x_yuv < img->w; ++x_yuv) { - for (int y_yuv = 0; y_yuv < img->h; ++y_yuv) { - int x_rgb = x_yuv; - int y_rgb = y_yuv; - if (is_mt9f002) { - // Change sampling points for zoomed and/or cropped image. - // Use nearest-neighbour sampling for now. - x_rgb = (mt9f002.offset_x + ((float)x_yuv / img->w) * mt9f002.sensor_width) - / CFG_MT9F002_PIXEL_ARRAY_WIDTH * cam->ImageWidth(); - y_rgb = (mt9f002.offset_y + ((float)y_yuv / img->h) * mt9f002.sensor_height) - / CFG_MT9F002_PIXEL_ARRAY_HEIGHT * cam->ImageHeight(); - } - int idx_rgb = 3 * (cam->ImageWidth() * y_rgb + x_rgb); - int idx_yuv = 2 * (img->w * y_yuv + x_yuv); - int idx_px = img->w * y_yuv + x_yuv; - if (idx_px % 2 == 0) { // Pick U or V - data_yuv[idx_yuv] = - 0.148 * data_rgb[idx_rgb] - - 0.291 * data_rgb[idx_rgb + 1] - + 0.439 * data_rgb[idx_rgb + 2] + 128; // U - } else { - data_yuv[idx_yuv] = 0.439 * data_rgb[idx_rgb] - - 0.368 * data_rgb[idx_rgb + 1] - - 0.071 * data_rgb[idx_rgb + 2] + 128; // V - } - data_yuv[idx_yuv + 1] = 0.257 * data_rgb[idx_rgb] - + 0.504 * data_rgb[idx_rgb + 1] - + 0.098 * data_rgb[idx_rgb + 2] + 16; // Y - } - } - // Fill miscellaneous fields - gazebo::common::Time ts = cam->LastMeasurementTime(); - img->ts.tv_sec = ts.sec; - img->ts.tv_usec = ts.nsec / 1000.0; - img->pprz_ts = ts.Double() * 1e6; - img->buf_idx = 0; // unused -} -#endif +#endif // NPS_SIMULATE_VIDEO #if NPS_SIMULATE_LASER_RANGE_ARRAY /* - * Simulate range sensors: - * - * In the airframe file, set NPS_SIMULATE_LASER_RANGE_ARRAY if you want to make use of the integrated Ray sensors. - * These are defined in their own model which is added to the ardrone.sdf (called range_sensors). Here you can add - * single ray point sensors with a specified position and orientation. It is also possible add noise. - * - * Within the airframe file (see e.g ardrone2_rangesensors_gazebo.xml), the amount of sensors - * (LASER_RANGE_ARRAY_NUM_SENSORS) and their orientations - * (LASER_RANGE_ARRAY_ORIENTATIONS={phi_1,theta_1,psi_1...phi_n,theta_n,psi_n} n = number of sensors) need to be - * specified as well. This is to keep it generic since this need to be done on the real platform with an external - * ray sensor. The function will compare the orientations from the ray sensors of gazebo, with the ones specified - * in the airframe, and will fill up an array to send and abi message to be used by other modules. - * - * NPS_GAZEBO_RANGE_SEND_AGL defines if the sensor that is defined as down should be used to send an AGL message. - * to send an OBSTACLE_DETECTION message. - * - * Functions: - * gazebo_init_range_sensors() -> Finds and initializes all ray sensors in gazebo - * gazebo_read_range_sensors() -> Reads and evaluates the ray sensors values, and sending it to other pprz modules + * Range sensor simulation for Gazebo Harmonic */ struct gazebo_ray_t { - gazebo::sensors::RaySensorPtr sensor; - gazebo::common::Time last_measurement_time; + gz::sensors::DistanceSensor *sensor; + std::chrono::steady_clock::time_point last_measurement_time; + double last_range; }; -static struct gazebo_ray_t rays[LASER_RANGE_ARRAY_NUM_SENSORS] = {{NULL, 0}}; +static struct gazebo_ray_t rays[LASER_RANGE_ARRAY_NUM_SENSORS] = {{NULL, std::chrono::steady_clock::time_point(), 0.0}}; static float range_orientation[] = LASER_RANGE_ARRAY_ORIENTATIONS; static uint8_t ray_sensor_agl_index = 255; #define VL53L0_MAX_VAL 8.191f +/** + * Initialize range sensors in Gazebo Harmonic. + */ static void gazebo_init_range_sensors(void) { - gazebo::sensors::SensorManager *mgr = gazebo::sensors::SensorManager::Instance(); - gazebo::sensors::Sensor_V sensors = mgr->GetSensors(); // list of all sensors - - uint16_t sensor_count = model->GetSensorCount(); - - gazebo::sensors::RaySensorPtr ray_sensor; - uint8_t ray_sensor_count_selected = 0; - - // Loop though all sensors and only select ray sensors, which are saved within a struct - for (uint16_t i = 0; i < sensor_count; i++) { - if (ray_sensor_count_selected > LASER_RANGE_ARRAY_NUM_SENSORS) { - break; - } - - if (sensors.at(i)->Type() == "ray") { - ray_sensor = static_pointer_cast(sensors.at(i)); - - if (!ray_sensor) { - cout << "ERROR: Could not get pointer to raysensor " << i << "!" << endl; - continue; - } - - // Read out the pose from per ray sensors in gazebo relative to body - struct DoubleEulers pose_sensor = to_pprz_eulers(ray_sensor->Pose().Rot()); - - struct DoubleQuat q_ray, q_def; - double_quat_of_eulers(&q_ray, &pose_sensor); - - /* Check the orientations of the ray sensors found in gazebo, if they are similar (within 5 deg) to the orientations - * given in the airframe file in LASER_RANGE_ARRAY_RANGE_ORIENTATION - */ - for (int j = 0; j < LASER_RANGE_ARRAY_NUM_SENSORS; j++) { - struct DoubleEulers def = {0, range_orientation[j * 2], range_orientation[j * 2 + 1]}; - double_quat_of_eulers(&q_def, &def); - // get angle between required angle and ray angle - double angle = acos(QUAT_DOT_PRODUCT(q_ray, q_def)); - - if (fabs(angle) < RadOfDeg(5)) { - ray_sensor_count_selected++; - rays[j].sensor = ray_sensor; - rays[j].sensor->SetActive(true); - -#if LASER_RANGE_ARRAY_SEND_AGL - // find the sensor pointing down - if (fabs(range_orientation[j * 2] + M_PI_2) < RadOfDeg(5)) { - ray_sensor_agl_index = j; - } -#endif - break; + cout << "Initializing range sensors..." << endl; + + // In Gazebo Harmonic, sensors are accessed via topics + // Subscribe to range/distance sensor topics + + for (int i = 0; i < LASER_RANGE_ARRAY_NUM_SENSORS; i++) { + string sensorName = "range_sensor_" + to_string(i); + string topicName = "/" + string(NPS_GAZEBO_AC_NAME) + "/" + sensorName; + + std::function cb = + [i](const gz::msgs::LaserScan &_msg) { + if (_msg.ranges_size() > 0) { + rays[i].last_range = _msg.ranges(0); + rays[i].last_measurement_time = std::chrono::steady_clock::now(); } - } + }; + + if (node.Subscribe(topicName, cb)) { + cout << "Subscribed to range sensor: " << topicName << endl; } + + // Check for sensor pointing down for AGL +#if LASER_RANGE_ARRAY_SEND_AGL + if (fabs(range_orientation[i * 2] + M_PI_2) < RadOfDeg(5)) { + ray_sensor_agl_index = i; + } +#endif } - - if (ray_sensor_count_selected != LASER_RANGE_ARRAY_NUM_SENSORS) { - cout << "ERROR: you have defined " << LASER_RANGE_ARRAY_NUM_SENSORS << " sensors in your airframe file, but only " - << ray_sensor_count_selected << " sensors have been found in the gazebo simulator, " - "with the same orientation as in the airframe file " << endl; - exit(0); - } - cout << "Initialized laser range array" << endl; + + cout << "Range sensors initialized" << endl; } +/** + * Read range sensor data and send ABI messages. + */ static void gazebo_read_range_sensors(void) { - static float range; - - // Loop through all ray sensors found in gazebo + auto now = std::chrono::steady_clock::now(); + for (int i = 0; i < LASER_RANGE_ARRAY_NUM_SENSORS; i++) { - if ((rays[i].sensor->LastMeasurementTime() - rays[i].last_measurement_time).Float() < 0.005 - || rays[i].sensor->LastMeasurementTime() == 0) { continue; } - - if (rays[i].sensor->Range(0) < 1e-5 || rays[i].sensor->Range(0) > VL53L0_MAX_VAL) { - range = VL53L0_MAX_VAL; - } else { - range = rays[i].sensor->Range(0); + // Check if we have a recent measurement + auto timeDiff = std::chrono::duration_cast( + now - rays[i].last_measurement_time).count(); + + if (timeDiff > 100) { // Skip if older than 100ms + continue; } - AbiSendMsgOBSTACLE_DETECTION(OBS_DETECTION_RANGE_ARRAY_NPS_ID, range, range_orientation[i * 2], + + float range = rays[i].last_range; + + // Clamp range to valid values + if (range < 1e-5 || range > VL53L0_MAX_VAL || std::isinf(range)) { + range = VL53L0_MAX_VAL; + } + + // Send obstacle detection message + AbiSendMsgOBSTACLE_DETECTION(OBS_DETECTION_RANGE_ARRAY_NPS_ID, range, + range_orientation[i * 2], range_orientation[i * 2 + 1]); - + + // Send AGL if this is the down-facing sensor if (i == ray_sensor_agl_index) { uint32_t now_ts = get_sys_time_usec(); - float agl = rays[i].sensor->Range(0); - // Down range sensor as agl - if (agl > 1e-5 && !isinf(agl)) { - AbiSendMsgAGL(AGL_RAY_SENSOR_GAZEBO_ID, now_ts, agl); + if (range > 1e-5 && !std::isinf(range)) { + AbiSendMsgAGL(AGL_RAY_SENSOR_GAZEBO_ID, now_ts, range); } } - rays[i].last_measurement_time = rays[i].sensor->LastMeasurementTime(); } } -#endif // NPS_SIMULATE_LASER_RANGE_ARRAY - -#pragma GCC diagnostic pop // Ignore -Wdeprecated-declarations +#endif // NPS_SIMULATE_LASER_RANGE_ARRAY \ No newline at end of file diff --git a/sw/simulator/nps/nps_fdm_gazebo_classic.cpp b/sw/simulator/nps/nps_fdm_gazebo_classic.cpp new file mode 100644 index 0000000000..ffbed0c7da --- /dev/null +++ b/sw/simulator/nps/nps_fdm_gazebo_classic.cpp @@ -0,0 +1,1005 @@ +/* + * Copyright (C) 2017 Tom van Dijk, Kirk Scheper + * + * This file is part of paparazzi. + * + * paparazzi is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * paparazzi is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with paparazzi; see the file COPYING. If not, write to + * the Free Software Foundation, 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +/** + * @file nps_fdm_gazebo.cpp + * Flight Dynamics Model (FDM) for NPS using Gazebo. + * + * This is an FDM for NPS that uses Gazebo as the simulation engine. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#if GAZEBO_MAJOR_VERSION < 8 +#error "Paparazzi only supports gazebo versions > 7, please upgrade to a more recent version of Gazebo" +#endif + +extern "C" { +#include "nps_fdm.h" +#include "nps_autopilot.h" + +#include "generated/airframe.h" +#include "generated/flight_plan.h" +#include "autopilot.h" +#include "modules/core/abi.h" + +#include "math/pprz_isa.h" +#include "math/pprz_algebra_double.h" +#include "filters/low_pass_filter.h" +#include "filters/high_pass_filter.h" + +#include "modules/actuators/motor_mixing_types.h" + +#include "math/pprz_geodetic_float.h" +#include "math/pprz_geodetic_double.h" +#include "state.h" +} + +#if defined(NPS_DEBUG_VIDEO) +// Opencv tools +#include +#include +#include +#endif + +using namespace std; + +#ifndef NPS_GAZEBO_WORLD +#define NPS_GAZEBO_WORLD "empty.world" +#endif +#ifndef NPS_GAZEBO_AC_NAME +#define NPS_GAZEBO_AC_NAME "ardrone" +#endif + +// Add video handling functions if req'd. +#if NPS_SIMULATE_VIDEO +extern "C" { +#include "modules/computer_vision/cv.h" +#include "modules/computer_vision/video_thread_nps.h" +#include "modules/computer_vision/lib/vision/image.h" +#include "mcu_periph/sys_time.h" +#include "boards/bebop/mt9f002.h" +#include "boards/bebop/mt9v117.h" +struct mt9f002_t mt9f002 __attribute__((weak)); // Prevent undefined reference errors when Bebop code is not linked. +} + +static void init_gazebo_video(void); +static void gazebo_read_video(void); +static void read_image( + struct image_t *img, + gazebo::sensors::CameraSensorPtr cam); +struct gazebocam_t { + gazebo::sensors::CameraSensorPtr cam; + gazebo::common::Time last_measurement_time; +}; +static struct gazebocam_t gazebo_cams[VIDEO_THREAD_MAX_CAMERAS] = +{ { NULL, 0 } }; + +// Reduce resolution of the simulated MT9F002 sensor (Bebop) to improve runtime +// performance at the cost of image resolution. +// Recommended values: 1 (realistic), 2, 4 (fast but slightly blurry) +#ifndef NPS_MT9F002_SENSOR_RES_DIVIDER +#define NPS_MT9F002_SENSOR_RES_DIVIDER 1 +#endif +#endif // NPS_SIMULATE_VIDEO + +struct gazebo_actuators_t { + string names[NPS_COMMANDS_NB]; + double thrusts[NPS_COMMANDS_NB]; + double torques[NPS_COMMANDS_NB]; + struct FirstOrderLowPass lowpass[NPS_COMMANDS_NB]; + struct FirstOrderHighPass highpass[NPS_COMMANDS_NB]; + double max_ang_momentum[NPS_COMMANDS_NB]; +}; + +struct gazebo_actuators_t gazebo_actuators = { NPS_ACTUATOR_NAMES, NPS_ACTUATOR_THRUSTS, NPS_ACTUATOR_TORQUES, { }, { }, { }}; + +#if NPS_SIMULATE_LASER_RANGE_ARRAY +extern "C" { +#include "modules/core/abi.h" +} + +static void gazebo_init_range_sensors(void); +static void gazebo_read_range_sensors(void); + +#endif + +std::shared_ptr sonar = NULL; + +/// Holds all necessary NPS FDM state information +struct NpsFdm fdm; + +// Pointer to Gazebo data +static bool gazebo_initialized = false; +static gazebo::physics::ModelPtr model = NULL; + +// Get contact sensor +static gazebo::sensors::ContactSensorPtr ct; + +// Helper functions +static void init_gazebo(void); +static void gazebo_read(void); +static void gazebo_write(double act_commands[], int commands_nb); + +// Conversion routines +inline struct EcefCoor_d to_pprz_ecef(ignition::math::Vector3d ecef_i) +{ + struct EcefCoor_d ecef_p; + ecef_p.x = ecef_i.X(); + ecef_p.y = ecef_i.Y(); + ecef_p.z = ecef_i.Z(); + return ecef_p; +} + +inline struct NedCoor_d to_pprz_ned(ignition::math::Vector3d global) +{ + struct NedCoor_d ned; + ned.x = global.Y(); + ned.y = global.X(); + ned.z = -global.Z(); + return ned; +} + +inline struct LlaCoor_d to_pprz_lla(ignition::math::Vector3d lla_i) +{ + struct LlaCoor_d lla_p; + lla_p.lat = lla_i.X(); + lla_p.lon = lla_i.Y(); + lla_p.alt = lla_i.Z(); + return lla_p; +} + +inline struct DoubleVect3 to_pprz_body(ignition::math::Vector3d body_g) +{ + struct DoubleVect3 body_p; + body_p.x = body_g.X(); + body_p.y = -body_g.Y(); + body_p.z = -body_g.Z(); + return body_p; +} + +inline struct DoubleRates to_pprz_rates(ignition::math::Vector3d body_g) +{ + struct DoubleRates body_p; + body_p.p = body_g.X(); + body_p.q = -body_g.Y(); + body_p.r = -body_g.Z(); + return body_p; +} + +inline struct DoubleEulers to_pprz_eulers(ignition::math::Quaterniond quat) +{ + struct DoubleEulers eulers; + eulers.psi = -quat.Yaw(); + eulers.theta = -quat.Pitch(); + eulers.phi = quat.Roll(); + return eulers; +} + +inline struct DoubleEulers to_global_pprz_eulers(ignition::math::Quaterniond quat) +{ + struct DoubleEulers eulers; + eulers.psi = -quat.Yaw() - M_PI / 2; + eulers.theta = -quat.Pitch(); + eulers.phi = quat.Roll(); + return eulers; +} + +inline struct DoubleVect3 to_pprz_ltp(ignition::math::Vector3d xyz) +{ + struct DoubleVect3 ltp; + ltp.x = xyz.Y(); + ltp.y = xyz.X(); + ltp.z = -xyz.Z(); + return ltp; +} + +// External functions, interface with Paparazzi's NPS as declared in nps_fdm.h + +/** + * Initialize actuator dynamics, set unused fields in fdm + * @param dt + */ +void nps_fdm_init(double dt) +{ + fdm.init_dt = dt; // JSBsim specific + fdm.curr_dt = dt; // JSBsim specific + fdm.nan_count = 0; // JSBsim specific + +#ifdef NPS_ACTUATOR_TIME_CONSTANTS + // Set up low-pass filter to simulate delayed actuator response + const float tau[NPS_COMMANDS_NB] = NPS_ACTUATOR_TIME_CONSTANTS; + for (int i = 0; i < NPS_COMMANDS_NB; i++) { + init_first_order_low_pass(&gazebo_actuators.lowpass[i], tau[i], dt, 0.f); + } +#ifdef NPS_ACTUATOR_MAX_ANGULAR_MOMENTUM + // Set up high-pass filter to simulate spinup torque + const float Iwmax[NPS_COMMANDS_NB] = NPS_ACTUATOR_MAX_ANGULAR_MOMENTUM; + for (int i = 0; i < NPS_COMMANDS_NB; i++) { + init_first_order_high_pass(&gazebo_actuators.highpass[i], tau[i], dt, 0.f); + gazebo_actuators.max_ang_momentum[i] = Iwmax[i]; + } +#endif +#endif +} + +/** + * Update the simulation state. + * @param launch + * @param act_commands + * @param commands_nb + */ +void nps_fdm_run_step( + bool launch __attribute__((unused)), + double *act_commands, + int commands_nb) +{ + // Initialize Gazebo if req'd. + // Initialization is peformed here instead of in nps_fdm_init because: + // - Video devices need to added at this point. Video devices have not been + // added yet when nps_fdm_init is called. + // - nps_fdm_init runs on a different thread then nps_fdm_run_step, which + // causes problems with Gazebo. + if (!gazebo_initialized) { + init_gazebo(); + gazebo_read(); +#if NPS_SIMULATE_VIDEO + init_gazebo_video(); +#endif +#if NPS_SIMULATE_LASER_RANGE_ARRAY + gazebo_init_range_sensors(); +#endif + gazebo_initialized = true; + } + + // Update the simulation for a single timestep. + gazebo::runWorld(model->GetWorld(), 1); + gazebo::sensors::run_once(); + gazebo_write(act_commands, commands_nb); + gazebo_read(); +#if NPS_SIMULATE_VIDEO + gazebo_read_video(); +#endif +#if NPS_SIMULATE_LASER_RANGE_ARRAY + gazebo_read_range_sensors(); +#endif + +} + +// TODO Atmosphere functions have not been implemented yet. +// Starting at version 8, Gazebo has its own atmosphere and wind model. +void nps_fdm_set_wind( + double speed __attribute__((unused)), + double dir __attribute__((unused))) +{ +} + +void nps_fdm_set_wind_ned( + double wind_north __attribute__((unused)), + double wind_east __attribute__((unused)), + double wind_down __attribute__((unused))) +{ +} + +void nps_fdm_set_turbulence( + double wind_speed __attribute__((unused)), + int turbulence_severity __attribute__((unused))) +{ +} + +/** Set temperature in degrees Celcius at given height h above MSL */ +void nps_fdm_set_temperature( + double temp __attribute__((unused)), + double h __attribute__((unused))) +{ +} + +// Internal functions +/** + * Set up a Gazebo server. + * + * Starts a Gazebo server, adds conf/simulator/gazebo/models to its model path + * and loads the world specified by NPS_GAZEBO_WORLD. + * + * This function also obtaines a pointer to the aircraft model, named + * NPS_GAZEBO_AC_NAME ('paparazzi_uav' by default). This pointer, 'model', + * is used to read the state and write actuator commands in gazebo_read and + * _write. + */ +static void init_gazebo(void) +{ + string gazebo_home = "/conf/simulator/gazebo/"; + string pprz_home(getenv("PAPARAZZI_HOME")); + string gazebodir = pprz_home + gazebo_home; + cout << "Gazebo directory: " << gazebodir << endl; + + if (getenv("ROS_MASTER_URI")) { + // Launch with ROS support + cout << "Add ROS plugins... "; + gazebo::addPlugin("libgazebo_ros_paths_plugin.so"); + gazebo::addPlugin("libgazebo_ros_api_plugin.so"); + cout << "ok" << endl; + } + + if (!gazebo::setupServer(0, NULL)) { + cout << "Failed to start Gazebo, exiting." << endl; + std::exit(-1); + } + + cout << "Add Paparazzi paths: " << gazebodir << endl; + gazebo::common::SystemPaths::Instance()->AddModelPaths(gazebodir + "models/"); + sdf::addURIPath("model://", gazebodir + "models/"); + sdf::addURIPath("world://", gazebodir + "world/"); + + cout << "Add TU Delft paths: " << pprz_home + "/sw/ext/tudelft_gazebo_models/" << endl; + gazebo::common::SystemPaths::Instance()->AddModelPaths(pprz_home + "/sw/ext/tudelft_gazebo_models/models/"); + sdf::addURIPath("model://", pprz_home + "/sw/ext/tudelft_gazebo_models/models/"); + sdf::addURIPath("world://", pprz_home + "/sw/ext/tudelft_gazebo_models/world/"); + + // get vehicles + string vehicle_uri = "model://" + string(NPS_GAZEBO_AC_NAME) + "/" + string(NPS_GAZEBO_AC_NAME) + ".sdf"; + string vehicle_filename = sdf::findFile(vehicle_uri, false); + if (vehicle_filename.empty()) { + cout << "ERROR, could not find vehicle " + vehicle_uri << endl; + std::exit(-1); + } + cout << "Load vehicle: " << vehicle_filename << endl; + sdf::SDFPtr vehicle_sdf(new sdf::SDF()); + sdf::init(vehicle_sdf); + if (!sdf::readFile(vehicle_filename, vehicle_sdf)) { + cout << "ERROR, could not read vehicle " + vehicle_filename << endl; + std::exit(-1); + } + + // add or set up sensors before the vehicle gets loaded +#if NPS_SIMULATE_VIDEO + // Cameras + sdf::ElementPtr link = vehicle_sdf->Root()->GetFirstElement()->GetElement("link"); + while (link) { + if (link->Get("name") == "front_camera" && link->GetElement("sensor")->Get("name") == "mt9f002") { + if (NPS_MT9F002_SENSOR_RES_DIVIDER != 1) { + int w = link->GetElement("sensor")->GetElement("camera")->GetElement("image")->GetElement("width")->Get(); + int h = link->GetElement("sensor")->GetElement("camera")->GetElement("image")->GetElement("height")->Get(); + int env = link->GetElement("sensor")->GetElement("camera")->GetElement("lens")->GetElement("env_texture_size")->Get(); + link->GetElement("sensor")->GetElement("camera")->GetElement("image")->GetElement("width")->Set(w / NPS_MT9F002_SENSOR_RES_DIVIDER); + link->GetElement("sensor")->GetElement("camera")->GetElement("image")->GetElement("height")->Set(h / NPS_MT9F002_SENSOR_RES_DIVIDER); + link->GetElement("sensor")->GetElement("camera")->GetElement("lens")->GetElement("env_texture_size")->Set(env / NPS_MT9F002_SENSOR_RES_DIVIDER); + } + if (MT9F002_TARGET_FPS){ + int fps = Min(MT9F002_TARGET_FPS, link->GetElement("sensor")->GetElement("update_rate")->Get()); + link->GetElement("sensor")->GetElement("update_rate")->Set(fps); + } + } else if (link->Get("name") == "bottom_camera" && link->GetElement("sensor")->Get("name") == "mt9v117") { + if (MT9V117_TARGET_FPS){ + int fps = Min(MT9V117_TARGET_FPS, link->GetElement("sensor")->GetElement("update_rate")->Get()); + link->GetElement("sensor")->GetElement("update_rate")->Set(fps); + } + } + link = link->GetNextElement("link"); + } +#endif + + // laser range array +#if NPS_SIMULATE_LASER_RANGE_ARRAY + vehicle_sdf->Root()->GetFirstElement()->AddElement("include")->GetElement("uri")->Set("model://range_sensors"); + sdf::ElementPtr range_joint = vehicle_sdf->Root()->GetFirstElement()->AddElement("joint"); + range_joint->GetAttribute("name")->Set("range_sensor_joint"); + range_joint->GetAttribute("type")->Set("fixed"); + range_joint->GetElement("parent")->Set("chassis"); + range_joint->GetElement("child")->Set("range_sensors::base"); +#endif + + // get world + string world_uri = "world://" + string(NPS_GAZEBO_WORLD); + string world_filename = sdf::findFile(world_uri, false); + if (world_filename.empty()) { + cout << "ERROR, could not find world " + world_uri << endl; + std::exit(-1); + } + cout << "Load world: " << world_filename << endl; + sdf::SDFPtr world_sdf(new sdf::SDF()); + sdf::init(world_sdf); + if (!sdf::readFile(world_filename, world_sdf)) { + cout << "ERROR, could not read world " + world_filename << endl; + std::exit(-1); + } + + // add vehicles + world_sdf->Root()->GetFirstElement()->InsertElement(vehicle_sdf->Root()->GetFirstElement()); + + world_sdf->Write(pprz_home + "/var/gazebo.world"); + + gazebo::physics::WorldPtr world = gazebo::loadWorld(pprz_home + "/var/gazebo.world"); + if (!world) { + cout << "Failed to open world, exiting." << endl; + std::exit(-1); + } + + cout << "Get pointer to aircraft: " << NPS_GAZEBO_AC_NAME << endl; + model = world->ModelByName(NPS_GAZEBO_AC_NAME); + if (!model) { + cout << "Failed to find '" << NPS_GAZEBO_AC_NAME << "', exiting." + << endl; + std::exit(-1); + } + + // Initialize sensors + gazebo::sensors::run_once(true); + gazebo::sensors::run_threads(); + gazebo::runWorld(world, 1); + cout << "Sensors initialized..." << endl; + + // Find sensors + // Contact sensor + gazebo::sensors::SensorManager *mgr = gazebo::sensors::SensorManager::Instance(); + ct = static_pointer_cast < gazebo::sensors::ContactSensor > (mgr->GetSensor("contactsensor")); + ct->SetActive(true); + // Sonar + sonar = static_pointer_cast(mgr->GetSensor("sonarsensor")); + if(sonar) { + cout << "Found sonar" << endl; + } + + gazebo::physics::LinkPtr sonar_link = model->GetLink("sonar"); + if (sonar_link) { + // Get a pointer to the sensor using its full name + if (sonar_link->GetSensorCount() != 1) { + cout << "ERROR: Link '" << sonar_link->GetName() + << "' should only contain 1 sensor!" << endl; + } else { + string name = sonar_link->GetSensorName(0); + sonar = static_pointer_cast< gazebo::sensors::SonarSensor > (mgr->GetSensor(name)); + if (!sonar) { + cout << "ERROR: Could not get pointer to '" << name << "'!" << endl; + } else { + // Activate sensor + sonar->SetActive(true); + } + } + } + + // Overwrite motor directions as defined by motor_mixing +#ifdef MOTOR_MIXING_YAW_COEF + const double yaw_coef[] = MOTOR_MIXING_YAW_COEF; + + for (uint8_t i = 0; i < NPS_COMMANDS_NB; i++) { + gazebo_actuators.torques[i] = -fabs(gazebo_actuators.torques[i]) * yaw_coef[i] / fabs(yaw_coef[i]); + gazebo_actuators.max_ang_momentum[i] = -fabs(gazebo_actuators.max_ang_momentum[i]) * yaw_coef[i] / fabs(yaw_coef[i]); + } +#endif + cout << "Gazebo initialized successfully!" << endl; +} + +/** + * Read Gazebo's simulation state and store the results in the fdm struct used + * by NPS. + * + * Not all fields are filled at the moment, as some of them are unused by + * paparazzi (see comments) and others are not available in Gazebo 7 + * (atmosphere). + */ +static void gazebo_read(void) +{ + static ignition::math::Vector3d vel_prev; + static double time_prev; + + gazebo::physics::WorldPtr world = model->GetWorld(); + ignition::math::Pose3d pose = model->WorldPose(); // In LOCAL xyz frame + ignition::math::Vector3d vel = model->WorldLinearVel(); + ignition::math::Vector3d ang_vel = model->WorldAngularVel(); + gazebo::common::SphericalCoordinatesPtr sphere = world->SphericalCoords(); + ignition::math::Quaterniond local_to_global_quat(0, 0, -sphere->HeadingOffset().Radian()); + + /* Fill FDM struct */ + fdm.time = world->SimTime().Double(); + + // Find world acceleration by differentiating velocity + // model->GetWorldLinearAccel() does not seem to take the velocity_decay into account! + // Derivation of the velocity also follows the IMU implementation of Gazebo itself: + // https://bitbucket.org/osrf/gazebo/src/e26144434b932b4b6a760ddaa19cfcf9f1734748/gazebo/sensors/ImuSensor.cc?at=default&fileviewer=file-view-default#ImuSensor.cc-370 + double dt = fdm.time - time_prev; + ignition::math::Vector3d accel = (vel - vel_prev) / dt; + vel_prev = vel; + time_prev = fdm.time; + + // init_dt: unused + // curr_dt: unused + // on_ground: unused + // nan_count: unused + + // Transform ltp definition to double for accuracy + struct LtpDef_d ltpdef_d; + ltpdef_d.ecef.x = state.ned_origin_f.ecef.x; + ltpdef_d.ecef.y = state.ned_origin_f.ecef.y; + ltpdef_d.ecef.z = state.ned_origin_f.ecef.z; + ltpdef_d.lla.lat = state.ned_origin_f.lla.lat; + ltpdef_d.lla.lon = state.ned_origin_f.lla.lon; + ltpdef_d.lla.alt = state.ned_origin_f.lla.alt; + for (int i = 0; i < 3 * 3; i++) { + ltpdef_d.ltp_of_ecef.m[i] = state.ned_origin_f.ltp_of_ecef.m[i]; + } + ltpdef_d.hmsl = state.ned_origin_f.hmsl; + + /* position */ + fdm.ltpprz_pos = to_pprz_ned(sphere->PositionTransform(pose.Pos(), gazebo::common::SphericalCoordinates::LOCAL, + gazebo::common::SphericalCoordinates::GLOBAL)); // Allows Gazebo worlds rotated wrt north. + fdm.hmsl = -fdm.ltpprz_pos.z; + ecef_of_ned_point_d(&fdm.ecef_pos, <pdef_d, &fdm.ltpprz_pos); + lla_of_ecef_d(&fdm.lla_pos, &fdm.ecef_pos); + + /* debug positions */ + fdm.lla_pos_pprz = fdm.lla_pos; // Don't really care... + fdm.lla_pos_geod = fdm.lla_pos; + fdm.lla_pos_geoc = fdm.lla_pos; + fdm.lla_pos_geoc.lat = gc_of_gd_lat_d(fdm.lla_pos.lat, fdm.hmsl); + + if(sonar) { + double agl = sonar->Range(); + if (agl > sonar->RangeMax()) agl = -1.0; + fdm.agl = agl; + } else { + fdm.agl = pose.Pos().Z(); // TODO Measure with sensor + } + + /* velocity */ + fdm.ltp_ecef_vel = to_pprz_ned(sphere->VelocityTransform(vel, gazebo::common::SphericalCoordinates::LOCAL, + gazebo::common::SphericalCoordinates::GLOBAL)); + fdm.ltpprz_ecef_vel = fdm.ltp_ecef_vel; // ??? + fdm.body_ecef_vel = to_pprz_body(pose.Rot().RotateVectorReverse(vel)); // Note: unused + ecef_of_ned_vect_d(&fdm.ecef_ecef_vel, <pdef_d, &fdm.ltp_ecef_vel); + + /* acceleration */ + fdm.ltp_ecef_accel = to_pprz_ned(sphere->VelocityTransform(accel, gazebo::common::SphericalCoordinates::LOCAL, + gazebo::common::SphericalCoordinates::GLOBAL)); // Note: unused + fdm.ltpprz_ecef_accel = fdm.ltp_ecef_accel; // ??? + fdm.body_ecef_accel = to_pprz_body(pose.Rot().RotateVectorReverse(accel)); + fdm.body_inertial_accel = fdm.body_ecef_accel; // Approximate, unused. + fdm.body_accel = to_pprz_body(pose.Rot().RotateVectorReverse(accel - world->Gravity())); + ecef_of_ned_vect_d(&fdm.ecef_ecef_accel, <pdef_d, &fdm.ltp_ecef_accel); + + /* attitude */ + // ecef_to_body_quat: unused + fdm.ltp_to_body_eulers = to_global_pprz_eulers(local_to_global_quat * pose.Rot()); + double_quat_of_eulers(&(fdm.ltp_to_body_quat), &(fdm.ltp_to_body_eulers)); + fdm.ltpprz_to_body_quat = fdm.ltp_to_body_quat; // unused + fdm.ltpprz_to_body_eulers = fdm.ltp_to_body_eulers; // unused + + /* angular velocity */ + fdm.body_ecef_rotvel = to_pprz_rates(pose.Rot().RotateVectorReverse(ang_vel)); + fdm.body_inertial_rotvel = fdm.body_ecef_rotvel; // Approximate + + /* angular acceleration */ + // body_ecef_rotaccel: unused + // body_inertial_rotaccel: unused + /* misc */ + fdm.ltp_g = to_pprz_ltp(sphere->VelocityTransform(-1 * world->Gravity(), gazebo::common::SphericalCoordinates::LOCAL, + gazebo::common::SphericalCoordinates::GLOBAL)); // unused + fdm.ltp_h = to_pprz_ltp(sphere->VelocityTransform(world->MagneticField(), gazebo::common::SphericalCoordinates::LOCAL, + gazebo::common::SphericalCoordinates::GLOBAL)); + + /* atmosphere */ +#if GAZEBO_MAJOR_VERSION >= 8 && 0 // TODO implement + +#else + // Gazebo versions < 8 do not have atmosphere or wind support + // Use placeholder values. Valid for low altitude, low speed flights. + fdm.wind = (struct DoubleVect3) {0, 0, 0}; + fdm.pressure_sl = 101325; // Pa + + fdm.airspeed = 0; + fdm.pressure = pprz_isa_pressure_of_height(fdm.hmsl, fdm.pressure_sl); + fdm.dynamic_pressure = fdm.pressure_sl; // Pa + fdm.temperature = 20.0; // C + fdm.aoa = 0; // rad + fdm.sideslip = 0; // rad +#endif + /* flight controls: unused */ + fdm.elevator = 0; + fdm.flap = 0; + fdm.left_aileron = 0; + fdm.right_aileron = 0; + fdm.rudder = 0; + /* engine: unused */ + fdm.num_engines = 0; +} + +/** + * Write actuator commands to Gazebo. + * + * This function takes the normalized commands and applies them as forces and + * torques in Gazebo. Since the commands are normalized in [0,1], their + * thrusts (NPS_ACTUATOR_THRUSTS) and torques (NPS_ACTUATOR_TORQUES) need to + * be specified in the airframe file. Their values need to be specified in the + * same order as the NPS_ACTUATOR_NAMES and should be provided in SI units. + * See conf/airframes/examples/ardrone2_gazebo.xml for an example. + * + * The forces and torques are applied to (the origin of) the links named in + * NPS_ACTUATOR_NAMES. See conf/simulator/gazebo/models/ardrone/ardrone.sdf + * for an example. + * + * @param act_commands + * @param commands_nb + */ +static void gazebo_write(double act_commands[], int commands_nb) +{ + for (int i = 0; i < commands_nb; ++i) { + // Thrust setpoint + double sp = autopilot.motors_on ? act_commands[i] : 0.0; // Normalized thrust setpoint + + // Actuator dynamics, forces and torques +#ifdef NPS_ACTUATOR_TIME_CONSTANTS + // Delayed response from actuator + double u = update_first_order_low_pass(&gazebo_actuators.lowpass[i], sp);// Normalized actual thrust +#else + double u = sp; +#endif + double thrust = gazebo_actuators.thrusts[i] * u; + double torque = gazebo_actuators.torques[i] * u; + +#ifdef NPS_ACTUATOR_MAX_ANGULAR_MOMENTUM + // Spinup torque + double udot = update_first_order_high_pass(&gazebo_actuators.highpass[i], sp); + double spinup_torque = gazebo_actuators.max_ang_momentum[i] / (2.0 * sqrt(u > 0.05 ? u : 0.05)) * udot; + torque += spinup_torque; +#endif + + // Apply force and torque to gazebo model + gazebo::physics::LinkPtr link = model->GetLink(gazebo_actuators.names[i]); + link->AddRelativeForce(ignition::math::Vector3d(0, 0, thrust)); + link->AddRelativeTorque(ignition::math::Vector3d(0, 0, torque)); + } +} + +#if NPS_SIMULATE_VIDEO +/** + * Set up cameras. + * + * This function finds the video devices added through add_video_device + * (sw/airborne/modules/computer_vision/cv.h). The camera links in the Gazebo AC + * model should have the same name as the .dev_name field in the corresponding + * video_config_t struct stored in 'cameras[]' (computer_vision/ + * video_thread_nps.h). Pointers to Gazebo's cameras are stored in gazebo_cams + * at the same index as their 'cameras[]' counterpart. + * + * The video_config_t parameters are updated using the values provided by + * Gazebo. This should simplify the use of different UAVs with different camera + * setups. + */ +static void init_gazebo_video(void) +{ + gazebo::sensors::SensorManager *mgr = gazebo::sensors::SensorManager::Instance(); + + cout << "Initializing cameras..." << endl; + // Loop over cameras registered in video_thread_nps + for (int i = 0; i < VIDEO_THREAD_MAX_CAMERAS && cameras[i] != NULL; ++i) { + // Find link in gazebo model + cout << "Setting up '" << cameras[i]->dev_name << "'... "; + gazebo::physics::LinkPtr link = model->GetLink(cameras[i]->dev_name); + if (!link) { + cout << "ERROR: Link '" << cameras[i]->dev_name << "' not found!" + << endl; + ; + continue; + } + // Get a pointer to the sensor using its full name + if (link->GetSensorCount() != 1) { + cout << "ERROR: Link '" << link->GetName() + << "' should only contain 1 sensor!" << endl; + continue; + } + string name = link->GetSensorName(0); + gazebo::sensors::CameraSensorPtr cam = static_pointer_cast + < gazebo::sensors::CameraSensor > (mgr->GetSensor(name)); + if (!cam) { + cout << "ERROR: Could not get pointer to '" << name << "'!" << endl; + continue; + } + // Activate sensor + cam->SetActive(true); + + // Add to list of cameras + gazebo_cams[i].cam = cam; + gazebo_cams[i].last_measurement_time = cam->LastMeasurementTime(); + + // set default camera settings + // Copy video_config settings from Gazebo's camera + cameras[i]->output_size.w = cam->ImageWidth(); + cameras[i]->output_size.h = cam->ImageHeight(); + cameras[i]->sensor_size.w = cam->ImageWidth(); + cameras[i]->sensor_size.h = cam->ImageHeight(); + cameras[i]->crop.w = cam->ImageWidth(); + cameras[i]->crop.h = cam->ImageHeight(); + cameras[i]->fps = 0; + cameras[i]->camera_intrinsics.focal_x = cameras[i]->output_size.w / 2.0f; + cameras[i]->camera_intrinsics.center_x = cameras[i]->output_size.w / 2.0f; + cameras[i]->camera_intrinsics.focal_y = cameras[i]->output_size.h / 2.0f; + cameras[i]->camera_intrinsics.center_y = cameras[i]->output_size.h / 2.0f; + + if (cam->Name() == "mt9f002") { + // See boards/bebop/mt9f002.c + cameras[i]->output_size.w = MT9F002_OUTPUT_WIDTH; + cameras[i]->output_size.h = MT9F002_OUTPUT_HEIGHT; + cameras[i]->sensor_size.w = MT9F002_OUTPUT_WIDTH; + cameras[i]->sensor_size.h = MT9F002_OUTPUT_HEIGHT; + cameras[i]->crop.w = MT9F002_OUTPUT_WIDTH; + cameras[i]->crop.h = MT9F002_OUTPUT_HEIGHT; + cameras[i]->fps = MT9F002_TARGET_FPS; + cameras[i]->camera_intrinsics = { + .focal_x = MT9F002_FOCAL_X, + .focal_y = MT9F002_FOCAL_Y, + .center_x = MT9F002_CENTER_X, + .center_y = MT9F002_CENTER_Y, + .Dhane_k = MT9F002_DHANE_K + }; + } else if (cam->Name() == "mt9v117") { + // See boards/bebop/mt9v117.h + cameras[i]->fps = MT9V117_TARGET_FPS; + cameras[i]->camera_intrinsics = { + .focal_x = MT9V117_FOCAL_X, + .focal_y = MT9V117_FOCAL_Y, + .center_x = MT9V117_CENTER_X, + .center_y = MT9V117_CENTER_Y, + .Dhane_k = MT9V117_DHANE_K + }; + } + cout << "ok" << endl; + } +} + +/** + * Read camera images. + * + * Polls gazebo cameras. If the last measurement time has been updated, a new + * frame is available. This frame is converted to Paparazzi's UYVY format + * and passed to cv_run_device which runs the callbacks registered by various + * modules. + */ +static void gazebo_read_video(void) +{ + for (int i = 0; i < VIDEO_THREAD_MAX_CAMERAS; ++i) { + gazebo::sensors::CameraSensorPtr &cam = gazebo_cams[i].cam; + // Skip unregistered or unfound cameras + if (cam == NULL) { continue; } + // Skip if not updated + // Also skip when LastMeasurementTime() is zero (workaround) + if ((cam->LastMeasurementTime() - gazebo_cams[i].last_measurement_time).Float() < 0.005 + || cam->LastMeasurementTime() == 0) { continue; } + // Grab image, convert and send to video thread + struct image_t img; + read_image(&img, cam); + +#if NPS_DEBUG_VIDEO + cv::Mat RGB_cam(cam->ImageHeight(), cam->ImageWidth(), CV_8UC3, (uint8_t *)cam->ImageData()); + cv::cvtColor(RGB_cam, RGB_cam, cv::COLOR_RGB2BGR); + cv::namedWindow(cameras[i]->dev_name, cv::WINDOW_AUTOSIZE); // Create a window for display. + cv::imshow(cameras[i]->dev_name, RGB_cam); + cv::waitKey(1); +#endif + + cv_run_device(cameras[i], &img); + // Free image buffer after use. + image_free(&img); + // Keep track of last update time. + gazebo_cams[i].last_measurement_time = cam->LastMeasurementTime(); + } +} + +/** + * Read Gazebo image and convert. + * + * Converts the current camera frame to the format used by Paparazzi. This + * includes conversion to UYVY. Gazebo's simulation time is used for the image + * timestamp. + * + * @param img + * @param cam + */ +static void read_image(struct image_t *img, gazebo::sensors::CameraSensorPtr cam) +{ + bool is_mt9f002 = false; + if (cam->Name() == "mt9f002") { + image_create(img, MT9F002_OUTPUT_WIDTH, MT9F002_OUTPUT_HEIGHT, IMAGE_YUV422); + is_mt9f002 = true; + } else { + image_create(img, cam->ImageWidth(), cam->ImageHeight(), IMAGE_YUV422); + } + + // Convert Gazebo's *RGB888* image to Paparazzi's YUV422 + const uint8_t *data_rgb = cam->ImageData(); + uint8_t *data_yuv = (uint8_t *)(img->buf); + for (int x_yuv = 0; x_yuv < img->w; ++x_yuv) { + for (int y_yuv = 0; y_yuv < img->h; ++y_yuv) { + int x_rgb = x_yuv; + int y_rgb = y_yuv; + if (is_mt9f002) { + // Change sampling points for zoomed and/or cropped image. + // Use nearest-neighbour sampling for now. + x_rgb = (mt9f002.offset_x + ((float)x_yuv / img->w) * mt9f002.sensor_width) + / CFG_MT9F002_PIXEL_ARRAY_WIDTH * cam->ImageWidth(); + y_rgb = (mt9f002.offset_y + ((float)y_yuv / img->h) * mt9f002.sensor_height) + / CFG_MT9F002_PIXEL_ARRAY_HEIGHT * cam->ImageHeight(); + } + int idx_rgb = 3 * (cam->ImageWidth() * y_rgb + x_rgb); + int idx_yuv = 2 * (img->w * y_yuv + x_yuv); + int idx_px = img->w * y_yuv + x_yuv; + if (idx_px % 2 == 0) { // Pick U or V + data_yuv[idx_yuv] = - 0.148 * data_rgb[idx_rgb] + - 0.291 * data_rgb[idx_rgb + 1] + + 0.439 * data_rgb[idx_rgb + 2] + 128; // U + } else { + data_yuv[idx_yuv] = 0.439 * data_rgb[idx_rgb] + - 0.368 * data_rgb[idx_rgb + 1] + - 0.071 * data_rgb[idx_rgb + 2] + 128; // V + } + data_yuv[idx_yuv + 1] = 0.257 * data_rgb[idx_rgb] + + 0.504 * data_rgb[idx_rgb + 1] + + 0.098 * data_rgb[idx_rgb + 2] + 16; // Y + } + } + // Fill miscellaneous fields + gazebo::common::Time ts = cam->LastMeasurementTime(); + img->ts.tv_sec = ts.sec; + img->ts.tv_usec = ts.nsec / 1000.0; + img->pprz_ts = ts.Double() * 1e6; + img->buf_idx = 0; // unused +} +#endif + +#if NPS_SIMULATE_LASER_RANGE_ARRAY +/* + * Simulate range sensors: + * + * In the airframe file, set NPS_SIMULATE_LASER_RANGE_ARRAY if you want to make use of the integrated Ray sensors. + * These are defined in their own model which is added to the ardrone.sdf (called range_sensors). Here you can add + * single ray point sensors with a specified position and orientation. It is also possible add noise. + * + * Within the airframe file (see e.g ardrone2_rangesensors_gazebo.xml), the amount of sensors + * (LASER_RANGE_ARRAY_NUM_SENSORS) and their orientations + * (LASER_RANGE_ARRAY_ORIENTATIONS={phi_1,theta_1,psi_1...phi_n,theta_n,psi_n} n = number of sensors) need to be + * specified as well. This is to keep it generic since this need to be done on the real platform with an external + * ray sensor. The function will compare the orientations from the ray sensors of gazebo, with the ones specified + * in the airframe, and will fill up an array to send and abi message to be used by other modules. + * + * NPS_GAZEBO_RANGE_SEND_AGL defines if the sensor that is defined as down should be used to send an AGL message. + * to send an OBSTACLE_DETECTION message. + * + * Functions: + * gazebo_init_range_sensors() -> Finds and initializes all ray sensors in gazebo + * gazebo_read_range_sensors() -> Reads and evaluates the ray sensors values, and sending it to other pprz modules + */ + +struct gazebo_ray_t { + gazebo::sensors::RaySensorPtr sensor; + gazebo::common::Time last_measurement_time; +}; + +static struct gazebo_ray_t rays[LASER_RANGE_ARRAY_NUM_SENSORS] = {{NULL, 0}}; +static float range_orientation[] = LASER_RANGE_ARRAY_ORIENTATIONS; +static uint8_t ray_sensor_agl_index = 255; + +#define VL53L0_MAX_VAL 8.191f + +static void gazebo_init_range_sensors(void) +{ + gazebo::sensors::SensorManager *mgr = gazebo::sensors::SensorManager::Instance(); + gazebo::sensors::Sensor_V sensors = mgr->GetSensors(); // list of all sensors + + uint16_t sensor_count = model->GetSensorCount(); + + gazebo::sensors::RaySensorPtr ray_sensor; + uint8_t ray_sensor_count_selected = 0; + + // Loop though all sensors and only select ray sensors, which are saved within a struct + for (uint16_t i = 0; i < sensor_count; i++) { + if (ray_sensor_count_selected > LASER_RANGE_ARRAY_NUM_SENSORS) { + break; + } + + if (sensors.at(i)->Type() == "ray") { + ray_sensor = static_pointer_cast(sensors.at(i)); + + if (!ray_sensor) { + cout << "ERROR: Could not get pointer to raysensor " << i << "!" << endl; + continue; + } + + // Read out the pose from per ray sensors in gazebo relative to body + struct DoubleEulers pose_sensor = to_pprz_eulers(ray_sensor->Pose().Rot()); + + struct DoubleQuat q_ray, q_def; + double_quat_of_eulers(&q_ray, &pose_sensor); + + /* Check the orientations of the ray sensors found in gazebo, if they are similar (within 5 deg) to the orientations + * given in the airframe file in LASER_RANGE_ARRAY_RANGE_ORIENTATION + */ + for (int j = 0; j < LASER_RANGE_ARRAY_NUM_SENSORS; j++) { + struct DoubleEulers def = {0, range_orientation[j * 2], range_orientation[j * 2 + 1]}; + double_quat_of_eulers(&q_def, &def); + // get angle between required angle and ray angle + double angle = acos(QUAT_DOT_PRODUCT(q_ray, q_def)); + + if (fabs(angle) < RadOfDeg(5)) { + ray_sensor_count_selected++; + rays[j].sensor = ray_sensor; + rays[j].sensor->SetActive(true); + +#if LASER_RANGE_ARRAY_SEND_AGL + // find the sensor pointing down + if (fabs(range_orientation[j * 2] + M_PI_2) < RadOfDeg(5)) { + ray_sensor_agl_index = j; + } +#endif + break; + } + } + } + } + + if (ray_sensor_count_selected != LASER_RANGE_ARRAY_NUM_SENSORS) { + cout << "ERROR: you have defined " << LASER_RANGE_ARRAY_NUM_SENSORS << " sensors in your airframe file, but only " + << ray_sensor_count_selected << " sensors have been found in the gazebo simulator, " + "with the same orientation as in the airframe file " << endl; + exit(0); + } + cout << "Initialized laser range array" << endl; +} + +static void gazebo_read_range_sensors(void) +{ + static float range; + + // Loop through all ray sensors found in gazebo + for (int i = 0; i < LASER_RANGE_ARRAY_NUM_SENSORS; i++) { + if ((rays[i].sensor->LastMeasurementTime() - rays[i].last_measurement_time).Float() < 0.005 + || rays[i].sensor->LastMeasurementTime() == 0) { continue; } + + if (rays[i].sensor->Range(0) < 1e-5 || rays[i].sensor->Range(0) > VL53L0_MAX_VAL) { + range = VL53L0_MAX_VAL; + } else { + range = rays[i].sensor->Range(0); + } + AbiSendMsgOBSTACLE_DETECTION(OBS_DETECTION_RANGE_ARRAY_NPS_ID, range, range_orientation[i * 2], + range_orientation[i * 2 + 1]); + + if (i == ray_sensor_agl_index) { + uint32_t now_ts = get_sys_time_usec(); + float agl = rays[i].sensor->Range(0); + // Down range sensor as agl + if (agl > 1e-5 && !isinf(agl)) { + AbiSendMsgAGL(AGL_RAY_SENSOR_GAZEBO_ID, now_ts, agl); + } + } + rays[i].last_measurement_time = rays[i].sensor->LastMeasurementTime(); + } +} +#endif // NPS_SIMULATE_LASER_RANGE_ARRAY + +#pragma GCC diagnostic pop // Ignore -Wdeprecated-declarations