LCOV - code coverage report
Current view: top level - SITL - SIM_SlungPayload.cpp (source / functions) Hit Total Coverage
Test: lcov.info Lines: 9 163 5.5 %
Date: 2026-09-09 11:02:08 Functions: 3 8 37.5 %

          Line data    Source code
       1             : /*
       2             :    This program is free software: you can redistribute it and/or modify
       3             :    it under the terms of the GNU General Public License as published by
       4             :    the Free Software Foundation, either version 3 of the License, or
       5             :    (at your option) any later version.
       6             : 
       7             :    This program is distributed in the hope that it will be useful,
       8             :    but WITHOUT ANY WARRANTY; without even the implied warranty of
       9             :    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      10             :    GNU General Public License for more details.
      11             : 
      12             :    You should have received a copy of the GNU General Public License
      13             :    along with this program.  If not, see <http://www.gnu.org/licenses/>.
      14             :  */
      15             : /*
      16             :   simulate a slung payload
      17             : */
      18             : 
      19             : #include "SIM_config.h"
      20             : 
      21             : #if AP_SIM_SLUNGPAYLOAD_ENABLED
      22             : 
      23             : #include "SIM_SlungPayload.h"
      24             : #include "SITL.h"
      25             : #include <stdio.h>
      26             : #include "SIM_Aircraft.h"
      27             : #include <AP_HAL_SITL/SITL_State.h>
      28             : #include <GCS_MAVLink/GCS.h>
      29             : #include <AP_Logger/AP_Logger.h>
      30             : 
      31             : using namespace SITL;
      32             : 
      33             : // SlungPayloadSim parameters
      34             : const AP_Param::GroupInfo SlungPayloadSim::var_info[] = {
      35             :     // @Param: ENABLE
      36             :     // @DisplayName: Slung Payload Sim enable/disable
      37             :     // @Description: Slung Payload Sim enable/disable
      38             :     // @Values: 0:Disabled,1:Enabled
      39             :     // @User: Advanced
      40             :     AP_GROUPINFO_FLAGS("ENABLE",  1, SlungPayloadSim,  enable, 0, AP_PARAM_FLAG_ENABLE),
      41             : 
      42             :     // @Param: WEIGHT
      43             :     // @DisplayName: Slung Payload weight
      44             :     // @Description: Slung Payload weight in kg
      45             :     // @Units: kg
      46             :     // @Range: 0 15
      47             :     // @User: Advanced
      48             :     AP_GROUPINFO("WEIGHT",  2, SlungPayloadSim,  weight_kg, 1.0),
      49             : 
      50             :     // @Param: LINELEN
      51             :     // @DisplayName: Slung Payload line length
      52             :     // @Description: Slung Payload line length in meters
      53             :     // @Units: m
      54             :     // @Range: 0 100
      55             :     // @User: Advanced
      56             :     AP_GROUPINFO("LINELEN", 3, SlungPayloadSim,  line_length, 30.0),
      57             : 
      58             :     // @Param: DRAG
      59             :     // @DisplayName: Slung Payload drag coefficient
      60             :     // @Description: Slung Payload drag coefficient.  Higher values increase drag and slow the payload more quickly
      61             :     // @Units: m
      62             :     // @Range: 0 10
      63             :     // @User: Advanced
      64             :     AP_GROUPINFO("DRAG",    4, SlungPayloadSim,  drag_coef, 1),
      65             : 
      66             :     // @Param: SYSID
      67             :     // @DisplayName: Slung Payload MAVLink system ID
      68             :     // @Description: Slung Payload MAVLink system id to distinguish it from others on the same network
      69             :     // @Range: 0 255
      70             :     // @User: Advanced
      71             :     AP_GROUPINFO("SYSID",   5, SlungPayloadSim,  sys_id, 2),
      72             : 
      73             :     AP_GROUPEND
      74             : };
      75             : 
      76             : // SlungPayloadSim handles interaction with main vehicle
      77        2096 : SlungPayloadSim::SlungPayloadSim()
      78             : {
      79        2096 :     AP_Param::setup_object_defaults(this, var_info);
      80        2096 : }
      81             : 
      82             : // update the SlungPayloadSim's state using the vehicle's earth-frame position, velocity, acceleration and wind
      83   169080479 : void SlungPayloadSim::update(const Vector3p& veh_pos, const Vector3f& veh_vel_ef, const Vector3f& veh_accel_ef, const Vector3f& wind_ef)
      84             : {
      85   169080479 :     if (!enable) {
      86   169080479 :         return;
      87             :     }
      88             : 
      89             :     // initialise slung payload location
      90           0 :     const uint32_t now_us = AP_HAL::micros();
      91           0 :     if (!initialised) {
      92             :         // capture EKF origin
      93           0 :         auto *sitl = AP::sitl();
      94           0 :         const Location ekf_origin = sitl->state.home;
      95           0 :         if (ekf_origin.lat == 0 && ekf_origin.lng == 0) {
      96           0 :             return;
      97             :         }
      98             : 
      99             :         // more initialisation
     100           0 :         last_update_us = now_us;
     101           0 :         initialised = true;
     102             :     }
     103             : 
     104             :     // calculate dt and update slung payload
     105           0 :     const float dt = (now_us - last_update_us)*1.0e-6;
     106           0 :     last_update_us = now_us;
     107           0 :     update_payload(veh_pos, veh_vel_ef, veh_accel_ef, wind_ef, dt);
     108             : 
     109             :     // send payload location to GCS at 5hz
     110           0 :     const uint32_t now_ms = AP_HAL::millis();
     111           0 :     if (now_ms - last_report_ms >= reporting_period_ms) {
     112           0 :         last_report_ms = now_ms;
     113           0 :         send_report();
     114           0 :         write_log();
     115             :     }
     116             : }
     117             : 
     118             : // get earth-frame forces on the vehicle from slung payload
     119             : // returns true on success and fills in forces_ef argument, false on failure
     120    81459271 : bool SlungPayloadSim::get_forces_on_vehicle(Vector3f& forces_ef) const
     121             : {
     122    81459271 :     if (!enable) {
     123    81459271 :         return false;
     124             :     }
     125             : 
     126           0 :     forces_ef = veh_forces_ef;
     127           0 :     return true;
     128             : }
     129             : 
     130             : // send a report to the vehicle control code over MAVLink
     131           0 : void SlungPayloadSim::send_report(void)
     132             : {
     133           0 :     if (!mavlink_connected && mav_socket.connect(target_address, target_port)) {
     134           0 :         ::printf("SlungPayloadSim connected to %s:%u\n", target_address, (unsigned)target_port);
     135           0 :         mavlink_connected = true;
     136             :     }
     137           0 :     if (!mavlink_connected) {
     138           0 :         return;
     139             :     }
     140             : 
     141             :     // get current time
     142           0 :     uint32_t now_ms = AP_HAL::millis();
     143             : 
     144             :     // send heartbeat at 1hz
     145           0 :     const uint8_t component_id = MAV_COMP_ID_USER11;
     146           0 :     if (now_ms - last_heartbeat_ms >= 1000) {
     147           0 :         last_heartbeat_ms = now_ms;
     148             : 
     149           0 :         const mavlink_heartbeat_t heartbeat{
     150             :             custom_mode: 0,
     151             :             type : MAV_TYPE_AIRSHIP,
     152             :             autopilot : MAV_AUTOPILOT_INVALID,
     153             :             base_mode: 0,
     154             :             system_status: 0,
     155             :             mavlink_version: 0,
     156             :         };
     157             : 
     158             :         mavlink_message_t msg;
     159           0 :         mavlink_msg_heartbeat_encode_status(
     160           0 :             sys_id.get(),
     161             :             component_id,
     162             :             &mav_status,
     163             :             &msg,
     164             :             &heartbeat);
     165             :         uint8_t buf[300];
     166           0 :         const uint16_t len = mavlink_msg_to_send_buffer(buf, &msg);
     167           0 :         mav_socket.send(buf, len);
     168             :     }
     169             : 
     170             :     // send a GLOBAL_POSITION_INT messages
     171             :     {
     172           0 :         Location payload_loc;
     173             :         int32_t alt_amsl_cm, alt_rel_cm;
     174           0 :         if (!get_payload_location(payload_loc) ||
     175           0 :             !payload_loc.get_alt_cm(Location::AltFrame::ABSOLUTE, alt_amsl_cm) ||
     176           0 :             !payload_loc.get_alt_cm(Location::AltFrame::ABOVE_HOME, alt_rel_cm)) {
     177           0 :             return;
     178             :         }
     179           0 :         const mavlink_global_position_int_t global_position_int{
     180             :             time_boot_ms: now_ms,
     181           0 :             lat: payload_loc.lat,
     182           0 :             lon: payload_loc.lng,
     183           0 :             alt: alt_amsl_cm * 10,              // amsl alt in mm
     184           0 :             relative_alt: alt_rel_cm * 10,      // relative alt in mm
     185           0 :             vx: int16_t(velocity_NED.x * 100),  // velocity in cm/s
     186           0 :             vy: int16_t(velocity_NED.y * 100),  // velocity in cm/s
     187           0 :             vz: int16_t(velocity_NED.z * 100),  // velocity in cm/s
     188             :             hdg: 0                              // heading in centi-degrees
     189           0 :         };
     190             :         mavlink_message_t msg;
     191           0 :         mavlink_msg_global_position_int_encode_status(sys_id, component_id, &mav_status, &msg, &global_position_int);
     192             :         uint8_t buf[300];
     193           0 :         const uint16_t len = mavlink_msg_to_send_buffer(buf, &msg);
     194           0 :         if (len > 0) {
     195           0 :             mav_socket.send(buf, len);
     196             :         }
     197             :     }
     198             : 
     199             :     // send ATTITUDE so MissionPlanner can display orientation
     200             :     {
     201           0 :         const mavlink_attitude_t attitude{
     202             :             time_boot_ms: now_ms,
     203             :             roll: 0,
     204             :             pitch: 0,
     205             :             yaw: 0,         // heading in radians
     206             :             rollspeed: 0,
     207             :             pitchspeed: 0,
     208             :             yawspeed: 0
     209           0 :         };
     210             :         mavlink_message_t msg;
     211           0 :         mavlink_msg_attitude_encode_status(
     212             :                 sys_id,
     213             :                 component_id,
     214             :                 &mav_status,
     215             :                 &msg,
     216             :                 &attitude);
     217             :         uint8_t buf[300];
     218           0 :         const uint16_t len = mavlink_msg_to_send_buffer(buf, &msg);
     219           0 :         if (len > 0) {
     220           0 :             mav_socket.send(buf, len);
     221             :         }
     222             :     }
     223             : }
     224             : 
     225             : // write onboard log
     226           0 : void SlungPayloadSim::write_log()
     227             : {
     228             : #if HAL_LOGGING_ENABLED
     229             :     // write log of slung payload state
     230             :     // @LoggerMessage: SLUP
     231             :     // @Description: Slung payload
     232             :     // @Field: TimeUS: Time since system startup
     233             :     // @Field: Land: 1 if payload is landed, 0 otherwise
     234             :     // @Field: Tens: Tension ratio, 1 if line is taut, 0 if slack
     235             :     // @Field: Len: Line length
     236             :     // @Field: PN: Payload position as offset from vehicle in North direction
     237             :     // @Field: PE: Payload position as offset from vehicle in East direction
     238             :     // @Field: PD: Payload position as offset from vehicle in Down direction
     239             :     // @Field: VN: Payload velocity in North direction
     240             :     // @Field: VE: Payload velocity in East direction
     241             :     // @Field: VD: Payload velocity in Down direction
     242             :     // @Field: AN: Payload acceleration in North direction
     243             :     // @Field: AE: Payload acceleration in East direction
     244             :     // @Field: AD: Payload acceleration in Down direction
     245             :     // @Field: VFN: Force on vehicle in North direction
     246             :     // @Field: VFE: Force on vehicle in East direction
     247             :     // @Field: VFD: Force on vehicle in Down direction
     248           0 :     AP::logger().WriteStreaming("SLUP",
     249             :                                 "TimeUS,Land,Tens,Len,PN,PE,PD,VN,VE,VD,AN,AE,AD,VFN,VFE,VFD",  // labels
     250             :                                 "s-%mmmmnnnooo---",  // units
     251             :                                 "F-20000000000000",  // multipliers
     252             :                                 "Qbffffffffffffff",  // format
     253             :                                 AP_HAL::micros64(),
     254           0 :                                 (uint8_t)landed,
     255           0 :                                 (float)tension_ratio,
     256           0 :                                 (float)payload_to_veh.length(),
     257           0 :                                 (double)-payload_to_veh.x,
     258           0 :                                 (double)-payload_to_veh.y,
     259           0 :                                 (double)-payload_to_veh.z,
     260           0 :                                 (double)velocity_NED.x,
     261           0 :                                 (double)velocity_NED.y,
     262           0 :                                 (double)velocity_NED.z,
     263           0 :                                 (double)accel_NED.x,
     264           0 :                                 (double)accel_NED.y,
     265           0 :                                 (double)accel_NED.z,
     266           0 :                                 (double)veh_forces_ef.x,
     267           0 :                                 (double)veh_forces_ef.y,
     268           0 :                                 (double)veh_forces_ef.z);
     269             : #endif
     270           0 : }
     271             : 
     272             : // returns true on success and fills in payload_loc argument, false on failure
     273           0 : bool SlungPayloadSim::get_payload_location(Location& payload_loc) const
     274             : {
     275             :     // get EKF origin
     276           0 :     auto *sitl = AP::sitl();
     277           0 :     if (sitl == nullptr) {
     278           0 :         return false;
     279             :     }
     280           0 :     const Location ekf_origin = sitl->state.home;
     281           0 :     if (ekf_origin.lat == 0 && ekf_origin.lng == 0) {
     282           0 :         return false;
     283             :     }
     284             : 
     285             :     // calculate location
     286           0 :     payload_loc = ekf_origin;
     287           0 :     payload_loc.offset(position_NED);
     288           0 :     return true;
     289             : }
     290             : 
     291             : // update the slung payloads position, velocity, acceleration
     292             : // vehicle position, velocity, acceleration and wind should be in earth-frame NED frame
     293           0 : void SlungPayloadSim::update_payload(const Vector3p& veh_pos, const Vector3f& veh_vel_ef, const Vector3f& veh_accel_ef,
     294             :                                      const Vector3f& wind_ef, float dt)
     295             : {
     296             :     // how we calculate the payload's position, velocity and acceleration
     297             :     //   1. update the payload's position, velocity using the previous iterations acceleration
     298             :     //   2. check that the payload does not fall below the terrain
     299             :     //   3. check if the line is taught and that the payload does not move more than the line length from the vehicle
     300             :     //   4. calculate gravity and drag forces on the payload
     301             :     //   5. calculate the tension force between the payload and vehicle including force countering gravity, drag and centripetal force
     302             :     //   6. update the payload's acceleration using the sum of the above forces
     303             : 
     304             :     // initialise position_NED from vehicle position
     305           0 :     if (position_NED.is_zero()) {
     306           0 :         if (!veh_pos.is_zero()) {
     307           0 :             position_NED = veh_pos;
     308           0 :             GCS_SEND_TEXT(MAV_SEVERITY_INFO, "SlungPayload: initialised at %f %f %f", position_NED.x, position_NED.y, position_NED.z);
     309             :         }
     310           0 :         return;
     311             :     }
     312             : 
     313             :     // integrate previous iterations acceleration into velocity and position
     314           0 :     velocity_NED += accel_NED * dt;
     315           0 :     position_NED += (velocity_NED * dt).todouble();
     316             : 
     317             :     // calculate distance from payload to vehicle
     318           0 :     payload_to_veh = veh_pos - position_NED;
     319           0 :     float payload_to_veh_length = payload_to_veh.length();
     320             : 
     321             :     // update landed state by checking if payload has dropped below terrain
     322           0 :     Location payload_loc;
     323           0 :     if (get_payload_location(payload_loc)) {
     324             :         int32_t alt_terrain_cm;
     325           0 :         bool landed_orig = landed;
     326           0 :         if (payload_loc.get_alt_cm(Location::AltFrame::ABOVE_TERRAIN, alt_terrain_cm)) {
     327             : 
     328             :             // landed if below terrain
     329           0 :             if (alt_terrain_cm <= 0) {
     330           0 :                 landed = true;
     331             : 
     332             :                 // raise payload to match terrain
     333           0 :                 position_NED.z += (alt_terrain_cm * 0.01);
     334             : 
     335             :                 // zero out velocity and acceleration in horizontal and downward direction
     336           0 :                 velocity_NED.xy().zero();
     337           0 :                 velocity_NED.z = MIN(velocity_NED.z, 0);
     338           0 :                 accel_NED.xy().zero();
     339           0 :                 accel_NED.z = MIN(accel_NED.z, 0);
     340             : 
     341             :                 // zero out forces on vehicle
     342           0 :                 veh_forces_ef.zero();
     343             :             }
     344             : 
     345             :             // not landed if above terrain
     346           0 :             if (landed && (alt_terrain_cm > 1)) {
     347           0 :                 landed = false;
     348             :             }
     349             :         }
     350             : 
     351             :         // inform user if landed state has changed
     352           0 :         if (landed != landed_orig) {
     353           0 :             if (landed) {
     354             :                 // get payload location again in case it has moved
     355           0 :                 get_payload_location(payload_loc);
     356           0 :                 GCS_SEND_TEXT(MAV_SEVERITY_INFO, "SlungPayload: landed lat:%f lon:%f alt:%4.1f",
     357             :                               (double)payload_loc.lat * 1e-7,
     358             :                               (double)payload_loc.lng * 1e-7,
     359             :                               (double)payload_loc.alt * 1e-2);
     360             :             } else {
     361           0 :                 GCS_SEND_TEXT(MAV_SEVERITY_INFO, "SlungPayload: liftoff");
     362             :             }
     363             :         }
     364             :     }
     365             : 
     366             :     // calculate forces of gravity
     367           0 :     Vector3f force_gravity_NED = Vector3f(0.0f, 0.0f, GRAVITY_MSS * weight_kg);
     368             : 
     369             :     // tension force on payload (resists gravity, drag, centripetal force)
     370           0 :     Vector3f tension_force_NED;
     371             : 
     372             :     // tension ratio to smooth transition from line being taut to slack
     373           0 :     tension_ratio = 0;
     374             : 
     375             :     // calculate drag force (0.5 * drag_coef * air_density * velocity^2 * surface area)
     376           0 :     Vector3f force_drag_NED;
     377           0 :     Vector3f velocity_air_NED = velocity_NED;
     378           0 :     if (!landed) {
     379           0 :         velocity_air_NED -= wind_ef;
     380             :     }
     381           0 :     if (drag_coef > 0 && !velocity_air_NED.is_zero()) {
     382           0 :         const float air_density = 1.225;    // 1.225 kg/m^3 (standard sea-level density)
     383           0 :         const float surface_area_m2 = 0.07; // 30cm diameter sphere
     384           0 :         const float drag_force = 0.5 * drag_coef * air_density * velocity_air_NED.length_squared() * surface_area_m2;
     385           0 :         force_drag_NED = -velocity_air_NED.normalized() * drag_force;
     386             :     }
     387             : 
     388             :     // sanity check payload distance from vehicle and calculate tension force
     389           0 :     if (is_positive(payload_to_veh_length)) {
     390             : 
     391             :         // calculate unit vector from payload to vehicle
     392           0 :         const Vector3f payload_to_veh_norm = payload_to_veh.normalized().tofloat();
     393             : 
     394             :         // ensure payload is no more than line_length from vehicle
     395           0 :         if (payload_to_veh_length > line_length) {
     396           0 :             payload_to_veh *= (line_length / payload_to_veh_length);
     397           0 :             position_NED = veh_pos - payload_to_veh;
     398             :         }
     399             : 
     400             :         // calculate tension ratio as value between 0 and 1
     401             :         // tension ratio is 0 when payload-to-vehicle distance is 10cm less than line length
     402             :         // tension ratio is 1 when payload-to-vehicle distance is equal to line length
     403           0 :         tension_ratio = constrain_float(1.0 - (line_length - payload_to_veh_length) * 10, 0, 1);
     404             : 
     405             :         // calculate tension forces when line is taut
     406           0 :         if (is_positive(tension_ratio)) {
     407             : 
     408             :             // tension resists gravity if vehicle is above payload
     409           0 :             if (is_negative(payload_to_veh_norm.z)) {
     410           0 :                 tension_force_NED += -force_gravity_NED.projected(payload_to_veh_norm);
     411             :             }
     412             : 
     413             :             // calculate tension force resulting from velocity difference between vehicle and payload
     414             :             // use time constant to convert velocity to acceleration
     415           0 :             const float velocity_to_accel_TC = 2.0;
     416           0 :             Vector3f velocity_diff_NED = (veh_vel_ef - velocity_NED).projected(payload_to_veh_norm);
     417             : 
     418             :             // add to tension force if the vehicle is moving faster than the payload
     419           0 :             if (vectors_same_direction(velocity_diff_NED, payload_to_veh_norm)) {
     420           0 :                 tension_force_NED += velocity_diff_NED / velocity_to_accel_TC * weight_kg;
     421             :             }
     422             : 
     423             :             // tension force resisting payload drag
     424           0 :             tension_force_NED += -force_drag_NED.projected(payload_to_veh_norm);
     425             : 
     426             :             // calculate centripetal force
     427           0 :             const Vector3f velocity_parallel = velocity_NED.projected(payload_to_veh_norm);
     428           0 :             const Vector3f velocity_perpendicular = velocity_NED - velocity_parallel;
     429           0 :             const float tension_force_centripetal = velocity_perpendicular.length_squared() * weight_kg / line_length;
     430           0 :             const Vector3f tension_force_centripetal_NED = payload_to_veh_norm * tension_force_centripetal;
     431             : 
     432             :             // add centripetal force to tension force
     433           0 :             tension_force_NED += tension_force_centripetal_NED;
     434             : 
     435             :             // scale tension force by tension ratio
     436           0 :             tension_force_NED *= tension_ratio;
     437             :         }
     438             :     }
     439             : 
     440             :     // force on vehicle is opposite to tension force on payload
     441           0 :     veh_forces_ef = -tension_force_NED;
     442             : 
     443             :     // convert force to acceleration (f=m*a => a=f/m)
     444           0 :     accel_NED = (force_gravity_NED + force_drag_NED + tension_force_NED) / weight_kg;
     445             : 
     446             :     // if slung payload is landed we zero out downward (e.g positive) acceleration
     447           0 :     if (landed) {
     448           0 :         accel_NED.z = MIN(accel_NED.z, 0);
     449             :         // should probably zero out forces_ef vertical component as well?
     450             :     }
     451             : }
     452             : 
     453             : // returns true if the two vectors point in the same direction, false if perpendicular or opposite
     454           0 : bool SlungPayloadSim::vectors_same_direction(const Vector3f& v1, const Vector3f& v2) const
     455             : {
     456             :     // check both vectors are non-zero
     457           0 :     if (v1.is_zero() || v2.is_zero()) {
     458           0 :         return false;
     459             :     }
     460           0 :     return v1.dot(v2) > 0;
     461             : }
     462             : 
     463             : #endif

Generated by: LCOV version 1.14