diff --git a/libraries/SITL/SIM_Aircraft.cpp b/libraries/SITL/SIM_Aircraft.cpp index c0469abc7cf20..138e5ae875534 100644 --- a/libraries/SITL/SIM_Aircraft.cpp +++ b/libraries/SITL/SIM_Aircraft.cpp @@ -839,6 +839,11 @@ void Aircraft::update_dynamics(const Vector3f &rot_accel) sitl->models.slung_payload_sim.update(get_position_relhome(), velocity_ef, accel_earth, wind_ef); #endif + // update tether +#if AP_SIM_TETHER_ENABLED + sitl->models.tether_sim.update(location); +#endif + // allow for changes in physics step adjust_frame_time(constrain_float(sitl->loop_rate_hz, rate_hz-1, rate_hz+1)); } @@ -1273,18 +1278,26 @@ void Aircraft::add_twist_forces(Vector3f &rot_accel) } } -#if AP_SIM_SLUNGPAYLOAD_ENABLED -// add body-frame force due to slung payload -void Aircraft::add_slungpayload_forces(Vector3f &body_accel) +// add body-frame force due to slung payload and tether +void Aircraft::add_external_forces(Vector3f &body_accel) { - Vector3f forces_ef; - sitl->models.slung_payload_sim.get_forces_on_vehicle(forces_ef); + Vector3f total_force; +#if AP_SIM_SLUNGPAYLOAD_ENABLED + Vector3f forces_ef_slung; + sitl->models.slung_payload_sim.get_forces_on_vehicle(forces_ef_slung); + total_force += forces_ef_slung; +#endif + +#if AP_SIM_TETHER_ENABLED + Vector3f forces_ef_tether; + sitl->models.tether_sim.get_forces_on_vehicle(forces_ef_tether); + total_force += forces_ef_tether; +#endif // convert ef forces to body-frame accelerations (acceleration = force / mass) - const Vector3f accel_bf = dcm.transposed() * forces_ef / mass; - body_accel += accel_bf; + const Vector3f accel_bf_tether = dcm.transposed() * total_force / mass; + body_accel += accel_bf_tether; } -#endif /* get position relative to home diff --git a/libraries/SITL/SIM_Aircraft.h b/libraries/SITL/SIM_Aircraft.h index e84a078ac8faf..a6d7b273c09b6 100644 --- a/libraries/SITL/SIM_Aircraft.h +++ b/libraries/SITL/SIM_Aircraft.h @@ -322,10 +322,8 @@ class Aircraft { void add_shove_forces(Vector3f &rot_accel, Vector3f &body_accel); void add_twist_forces(Vector3f &rot_accel); -#if AP_SIM_SLUNGPAYLOAD_ENABLED - // add body-frame force due to slung payload - void add_slungpayload_forces(Vector3f &body_accel); -#endif + // add body-frame force due to payload + void add_external_forces(Vector3f &body_accel); // get local thermal updraft float get_local_updraft(const Vector3d ¤tPos); diff --git a/libraries/SITL/SIM_Multicopter.cpp b/libraries/SITL/SIM_Multicopter.cpp index 4e019dde9b4de..273c2fc06dad7 100644 --- a/libraries/SITL/SIM_Multicopter.cpp +++ b/libraries/SITL/SIM_Multicopter.cpp @@ -49,12 +49,10 @@ void MultiCopter::calculate_forces(const struct sitl_input &input, Vector3f &rot add_shove_forces(rot_accel, body_accel); add_twist_forces(rot_accel); -#if AP_SIM_SLUNGPAYLOAD_ENABLED - // add forces from slung payload - add_slungpayload_forces(body_accel); -#endif + // add forces from slung payload or tether payload + add_external_forces(body_accel); } - + /* update the multicopter simulation by one time step */ diff --git a/libraries/SITL/SIM_Tether.cpp b/libraries/SITL/SIM_Tether.cpp new file mode 100644 index 0000000000000..005347b4c25ce --- /dev/null +++ b/libraries/SITL/SIM_Tether.cpp @@ -0,0 +1,309 @@ +/* + This program 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 3 of the License, or + (at your option) any later version. + + This program 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 this program. If not, see . + */ +/* + simulate a static tether attached to the vehicle and ground +*/ + +#include "SIM_config.h" + +#if AP_SIM_TETHER_ENABLED + +#include "SIM_Tether.h" +#include "SITL.h" +#include +#include "SIM_Aircraft.h" +#include +#include +#include + +using namespace SITL; + +// TetherSim parameters +const AP_Param::GroupInfo TetherSim::var_info[] = { + // @Param: ENABLE + // @DisplayName: Tether Simulation Enable/Disable + // @Description: Enable or disable the tether simulation + // @Values: 0:Disabled,1:Enabled + // @User: Advanced + AP_GROUPINFO_FLAGS("ENABLE", 1, TetherSim, enable, 0, AP_PARAM_FLAG_ENABLE), + + // @Param: DENSITY + // @DisplayName: Tether Wire Density + // @Description: Linear mass density of the tether wire + // @Range: 0 1 + // @User: Advanced + AP_GROUPINFO("DENSITY", 2, TetherSim, tether_linear_density, 0.0167), + + // @Param: LINELEN + // @DisplayName: Tether Maximum Line Length + // @Description: Maximum length of the tether line in meters + // @Units: m + // @Range: 0 100 + // @User: Advanced + AP_GROUPINFO("LINELEN", 3, TetherSim, max_line_length, 100.0), + + // @Param: SYSID + // @DisplayName: Tether Simulation MAVLink System ID + // @Description: MAVLink system ID for the tether simulation, used to distinguish it from other systems on the network + // @Range: 0 255 + // @User: Advanced + AP_GROUPINFO("SYSID", 4, TetherSim, sys_id, 2), + + // @Param: STUCK + // @DisplayName: Tether Stuck Enable/Disable + // @Description: Enable or disable a stuck tether simulation + // @Values: 0:Disabled,1:Enabled + // @User: Advanced + AP_GROUPINFO("STUCK", 5, TetherSim, tether_stuck, 0), + + // @Param: SPGCNST + // @DisplayName: Tether Spring Constant + // @Description: Spring constant for the tether to simulate elastic forces when stretched beyond its maximum length + // @Range: 0 255 + // @User: Advanced + AP_GROUPINFO("SPGCNST", 6, TetherSim, tether_spring_constant, 100), + + AP_GROUPEND +}; + +// TetherSim handles interaction with main vehicle +TetherSim::TetherSim() +{ + AP_Param::setup_object_defaults(this, var_info); +} + + +void TetherSim::update(const Location& veh_pos) +{ + if (!enable) { + return; + } + + if (veh_pos.is_zero()) { + return; + } + + // initialise fixed tether ground location + const uint32_t now_us = AP_HAL::micros(); + if (!initialised) { + // capture EKF origin + auto *sitl = AP::sitl(); + const Location ekf_origin = sitl->state.home; + if (ekf_origin.lat == 0 && ekf_origin.lng == 0) { + return; + } + + // more initialisation + last_update_us = now_us; + initialised = true; + } + + // calculate dt and update tether forces + const float dt = (now_us - last_update_us)*1.0e-6; + last_update_us = now_us; + + update_tether_force(veh_pos, dt); + + // send tether location to GCS at 5hz + // TO-Do: add provision to make the tether movable + const uint32_t now_ms = AP_HAL::millis(); + if (now_ms - last_report_ms >= reporting_period_ms) { + last_report_ms = now_ms; + send_report(); + write_log(); + } +} + +// get earth-frame forces on the vehicle from the tether +// returns true on success and fills in forces_ef argument, false on failure +bool TetherSim::get_forces_on_vehicle(Vector3f& forces_ef) const +{ + if (!enable) { + return false; + } + + forces_ef = veh_forces_ef; + return true; +} + +// send a report to the vehicle control code over MAVLink +void TetherSim::send_report(void) +{ + if (!mavlink_connected && mav_socket.connect(target_address, target_port)) { + ::printf("Tether System connected to %s:%u\n", target_address, (unsigned)target_port); + mavlink_connected = true; + } + if (!mavlink_connected) { + return; + } + + // get current time + uint32_t now_ms = AP_HAL::millis(); + + // send heartbeat at 1hz + const uint8_t component_id = MAV_COMP_ID_USER11; + if (now_ms - last_heartbeat_ms >= 1000) { + last_heartbeat_ms = now_ms; + + const mavlink_heartbeat_t heartbeat{ + custom_mode: 0, + type : MAV_TYPE_GROUND_ROVER, + autopilot : MAV_AUTOPILOT_INVALID, + base_mode: 0, + system_status: 0, + mavlink_version: 0, + }; + + mavlink_message_t msg; + mavlink_msg_heartbeat_encode_status( + sys_id.get(), + component_id, + &mav_status, + &msg, + &heartbeat); + uint8_t buf[300]; + const uint16_t len = mavlink_msg_to_send_buffer(buf, &msg); + mav_socket.send(buf, len); + } + + // send a GLOBAL_POSITION_INT messages + { + Location tether_anchor_loc; + int32_t alt_amsl_cm, alt_rel_cm; + if (!get_tether_ground_location(tether_anchor_loc) || + !tether_anchor_loc.get_alt_cm(Location::AltFrame::ABSOLUTE, alt_amsl_cm) || + !tether_anchor_loc.get_alt_cm(Location::AltFrame::ABOVE_HOME, alt_rel_cm)) { + return; + } + const mavlink_global_position_int_t global_position_int{ + time_boot_ms: now_ms, + lat: tether_anchor_loc.lat, + lon: tether_anchor_loc.lng, + alt: alt_amsl_cm * 10, // amsl alt in mm + relative_alt: alt_rel_cm * 10, // relative alt in mm + vx: 0, // velocity in cm/s + vy: 0, // velocity in cm/s + vz: 0, // velocity in cm/s + hdg: 0 // heading in centi-degrees + }; + mavlink_message_t msg; + mavlink_msg_global_position_int_encode_status(sys_id, component_id, &mav_status, &msg, &global_position_int); + uint8_t buf[300]; + const uint16_t len = mavlink_msg_to_send_buffer(buf, &msg); + if (len > 0) { + mav_socket.send(buf, len); + } + } +} + +void TetherSim::write_log() +{ +#if HAL_LOGGING_ENABLED + // write log of tether state + // @LoggerMessage: TETH + // @Description: Tether state + // @Field: TimeUS: Time since system startup + // @Field: Len: Tether length + // @Field: VFN: Force on vehicle in North direction + // @Field: VFE: Force on vehicle in East direction + // @Field: VFD: Force on vehicle in Down direction + AP::logger().WriteStreaming("TETH", + "TimeUS,Len,VFN,VFE,VFD", // labels + "s----", // units + "F----", // multipliers + "Qffff", // format + AP_HAL::micros64(), + (float)tether_length, + (double)veh_forces_ef.x, + (double)veh_forces_ef.y, + (double)veh_forces_ef.z); +#endif +} +// returns true on success and fills in tether_ground_loc argument, false on failure +bool TetherSim::get_tether_ground_location(Location& tether_ground_loc) const +{ + // get EKF origin + auto *sitl = AP::sitl(); + if (sitl == nullptr) { + return false; + } + const Location ekf_origin = sitl->state.home; + if (ekf_origin.lat == 0 && ekf_origin.lng == 0) { + return false; + } + + tether_ground_loc = ekf_origin; + return true; +} + +void TetherSim::update_tether_force(const Location& veh_pos, float dt) +{ + + Location tether_anchor_loc; + if (!get_tether_ground_location(tether_anchor_loc)) { + return; + } + + // Step 1: Calculate the vector from the tether anchor to the vehicle + Vector3f tether_vector = veh_pos.get_distance_NED(tether_anchor_loc); + tether_length = tether_vector.length(); + + // Step 2: Check if tether is taut (length exceeds maximum allowed length) or stuck + if (tether_length > max_line_length) { + + // Calculate the stretch beyond the maximum length + float stretch = MAX(tether_length - max_line_length, 0.0f); + + // Apply a spring-like penalty force proportional to the stretch + float penalty_force_magnitude = tether_spring_constant * stretch; + + // Direction of force is along the tether, pulling toward the anchor + veh_forces_ef = tether_vector.normalized() * penalty_force_magnitude; + + GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "Tether: Exceeded maximum length."); + + return; + } + + if (tether_stuck) { + + // Calculate the stretch beyond the maximum length + float stretch = MAX(tether_length - tether_not_stuck_length, 0.0f); + + // Apply a spring-like penalty force proportional to the stretch + float penalty_force_magnitude = tether_spring_constant * stretch; + + // Direction of force is directly along the tether, towards the tether anchor point + veh_forces_ef = tether_vector.normalized() * penalty_force_magnitude; + + GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "Tether: Stuck."); + + return; + } else { + tether_not_stuck_length = tether_length; + } + + // Step 3: Calculate the weight of the tether being lifted + // The weight is proportional to the current tether length + const float tether_weight_force = tether_linear_density * tether_length * GRAVITY_MSS; + + // Step 4: Calculate the tension force + Vector3f tension_force_NED = tether_vector.normalized() * tether_weight_force; + + // Step 5: Apply the force to the vehicle + veh_forces_ef = tension_force_NED; +} + +#endif diff --git a/libraries/SITL/SIM_Tether.h b/libraries/SITL/SIM_Tether.h new file mode 100644 index 0000000000000..4a7366e1089cd --- /dev/null +++ b/libraries/SITL/SIM_Tether.h @@ -0,0 +1,95 @@ +/* + This program 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 3 of the License, or + (at your option) any later version. + + This program 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 this program. If not, see . + */ + +/* + Simulate a tethered vehicle. + Models the forces exerted by the tether and reports them to the vehicle simulation. The dynamics are not accurate but provide a very rough approximation intended to test a "stuck tether". + */ + +#pragma once + +#include "SIM_config.h" + +#if AP_SIM_TETHER_ENABLED + +#include +#include +#include +#include + +namespace SITL { + +// TetherSim simulates a tethered to a vehicle +class TetherSim { +public: + // constructor + TetherSim(); + + // Update the tether simulation state using the vehicle's earth-frame position + void update(const Location& veh_pos); + + // Retrieve earth-frame forces on the vehicle due to the tether + // Returns true on success and fills in the forces_ef argument; false on failure + bool get_forces_on_vehicle(Vector3f& forces_ef) const; + + // Parameter table for configuration + static const struct AP_Param::GroupInfo var_info[]; + +private: + // Parameters + AP_Int8 enable; // Enable or disable the tether simulation + AP_Float tether_linear_density; // Linear mass density of the tether in kg/m + AP_Float max_line_length; // Maximum allowed tether length in meters + AP_Int8 sys_id; // MAVLink system ID for GCS reporting + AP_Int8 tether_stuck; // Set to 1 to simulate a stuck tether + AP_Float tether_spring_constant; // Spring constant for modeling tether stretch + + // Send MAVLink messages to the GCS + void send_report(); + + // Write tether simulation state to onboard log + void write_log(); + + // Retrieve the location of the tether anchor point on the ground + // Returns true on success and fills in the tether_anchor_loc argument; false on failure + bool get_tether_ground_location(Location& tether_anchor_loc) const; + + // Update forces exerted by the tether based on the vehicle's position + // Takes the vehicle's position and the simulation time step (dt) as inputs + void update_tether_force(const Location& veh_pos, float dt); + + // Socket connection variables for MAVLink communication + const char *target_address = "127.0.0.1"; // Address for MAVLink socket communication + const uint16_t target_port = 5763; // Port for MAVLink socket communication + SocketAPM_native mav_socket { false }; // Socket for MAVLink communication + bool initialised; // True if the simulation class is initialized + uint32_t last_update_us; // Timestamp of the last update in microseconds + + // MAVLink reporting variables + const float reporting_period_ms = 100; // Reporting period in milliseconds + uint32_t last_report_ms; // Timestamp of the last MAVLink report sent to GCS + uint32_t last_heartbeat_ms; // Timestamp of the last MAVLink heartbeat sent to GCS + bool mavlink_connected; // True if MAVLink connection is established + mavlink_status_t mav_status; // Status of MAVLink communication + + // Tether simulation variables + Vector3f veh_forces_ef; // Earth-frame forces on the vehicle due to the tether + float tether_length = 0.0f; // Current tether length in meters + float tether_not_stuck_length = 0.0f; // Tether length when the tether is not stuck +}; + +} // namespace SITL + +#endif // AP_SIM_TETHER_ENABLED diff --git a/libraries/SITL/SIM_config.h b/libraries/SITL/SIM_config.h index 557c3077ed9de..cb30fc71a8784 100644 --- a/libraries/SITL/SIM_config.h +++ b/libraries/SITL/SIM_config.h @@ -57,6 +57,10 @@ #define AP_SIM_SLUNGPAYLOAD_ENABLED (CONFIG_HAL_BOARD == HAL_BOARD_SITL) #endif +#ifndef AP_SIM_TETHER_ENABLED +#define AP_SIM_TETHER_ENABLED (CONFIG_HAL_BOARD == HAL_BOARD_SITL) +#endif + #ifndef AP_SIM_FLIGHTAXIS_ENABLED #define AP_SIM_FLIGHTAXIS_ENABLED (CONFIG_HAL_BOARD == HAL_BOARD_SITL) #endif diff --git a/libraries/SITL/SITL.cpp b/libraries/SITL/SITL.cpp index e329ed232a960..03ba5f1771718 100644 --- a/libraries/SITL/SITL.cpp +++ b/libraries/SITL/SITL.cpp @@ -1367,6 +1367,12 @@ const AP_Param::GroupInfo SIM::ModelParm::var_info[] = { AP_SUBGROUPPTR(flightaxis_ptr, "RFL_", 5, SIM::ModelParm, FlightAxis), #endif +#if AP_SIM_TETHER_ENABLED + // @Group: TETH_ + // @Path: ./SIM_Tether.cpp + AP_SUBGROUPINFO(tether_sim, "TETH_", 6, SIM::ModelParm, TetherSim), +#endif + AP_GROUPEND }; diff --git a/libraries/SITL/SITL.h b/libraries/SITL/SITL.h index 4edda5efd21e4..54d935043718a 100644 --- a/libraries/SITL/SITL.h +++ b/libraries/SITL/SITL.h @@ -28,6 +28,7 @@ #include "SIM_IntelligentEnergy24.h" #include "SIM_Ship.h" #include "SIM_SlungPayload.h" +#include "SIM_Tether.h" #include "SIM_GPS.h" #include "SIM_DroneCANDevice.h" #include "SIM_ADSB_Sagetech_MXS.h" @@ -338,6 +339,9 @@ class SIM { #if AP_SIM_SLUNGPAYLOAD_ENABLED SlungPayloadSim slung_payload_sim; #endif +#if AP_SIM_TETHER_ENABLED + TetherSim tether_sim; +#endif #if AP_SIM_FLIGHTAXIS_ENABLED FlightAxis *flightaxis_ptr; #endif