LCOV - code coverage report
Current view: top level - AP_Mount - AP_Mount_Xacti.cpp (source / functions) Hit Total Coverage
Test: lcov.info Lines: 4 436 0.9 %
Date: 2026-09-09 11:02:08 Functions: 1 35 2.9 %

          Line data    Source code
       1             : #include "AP_Mount_config.h"
       2             : 
       3             : #if HAL_MOUNT_XACTI_ENABLED
       4             : 
       5             : #include "AP_Mount_Xacti.h"
       6             : 
       7             : #include <AP_HAL/AP_HAL.h>
       8             : #include <AP_AHRS/AP_AHRS.h>
       9             : #include <GCS_MAVLink/GCS.h>
      10             : #include <AP_BoardConfig/AP_BoardConfig.h>
      11             : #include <AP_CANManager/AP_CANManager.h>
      12             : #include <AP_DroneCAN/AP_DroneCAN.h>
      13             : #include <AP_RTC/AP_RTC.h>
      14             : 
      15             : extern const AP_HAL::HAL& hal;
      16             : 
      17             : #define XACTI_MSG_SEND_MIN_MS 20                    // messages should not be sent to camera more often than 20ms
      18             : #define XACTI_DIGITAL_ZOOM_RATE_UPDATE_INTERVAL_MS  500 // digital zoom rate control updates 11% up or down every 0.5sec
      19             : #define XACTI_OPTICAL_ZOOM_RATE_UPDATE_INTERVAL_MS  250 // optical zoom rate control updates 6.6% up or down every 0.25sec
      20             : #define XACTI_STATUS_REQ_INTERVAL_MS 3000           // request status every 3 seconds
      21             : #define XACTI_SET_PARAM_QUEUE_SIZE 3                // three set-param requests may be queued
      22             : 
      23             : #define AP_MOUNT_XACTI_DEBUG 0
      24             : #define debug(fmt, args ...) do { if (AP_MOUNT_XACTI_DEBUG) { GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Xacti: " fmt, ## args); } } while (0)
      25             : 
      26             : AP_Mount_Xacti::DetectedModules AP_Mount_Xacti::_detected_modules[];
      27             : HAL_Semaphore AP_Mount_Xacti::_sem_registry;
      28             : const char* AP_Mount_Xacti::send_text_prefix = "Xacti:";
      29             : const char* AP_Mount_Xacti::sensor_mode_str[] = { "RGB", "IR", "PIP", "NDVI" };
      30             : const char* AP_Mount_Xacti::_param_names[] = {"SingleShot", "Recording", "FocusMode",
      31             :                                               "SensorMode", "DigitalZoomMagnification",
      32             :                                               "FirmwareVersion", "Status", "DateTime",
      33             :                                               "OpticalZoomMagnification"};
      34             : 
      35             : // Constructor
      36           0 : AP_Mount_Xacti::AP_Mount_Xacti(class AP_Mount &frontend, class AP_Mount_Params &params, uint8_t instance) :
      37           0 :     AP_Mount_Backend(frontend, params, instance)
      38             : {
      39           0 :     register_backend();
      40             : 
      41           0 :     param_int_cb = FUNCTOR_BIND_MEMBER(&AP_Mount_Xacti::handle_param_get_set_response_int, bool, AP_DroneCAN*, const uint8_t, const char*, int32_t &);
      42           0 :     param_string_cb = FUNCTOR_BIND_MEMBER(&AP_Mount_Xacti::handle_param_get_set_response_string, bool, AP_DroneCAN*, const uint8_t, const char*, AP_DroneCAN::string &);
      43           0 :     param_save_cb = FUNCTOR_BIND_MEMBER(&AP_Mount_Xacti::handle_param_save_response, void, AP_DroneCAN*, const uint8_t, bool);
      44             : 
      45             :     // static assert that Param enum matches parameter names array
      46             :     static_assert((uint8_t)AP_Mount_Xacti::Param::LAST+1 == ARRAY_SIZE(AP_Mount_Xacti::_param_names), "AP_Mount_Xacti::_param_names array must match Param enum");
      47           0 : }
      48             : 
      49             : // init - performs any required initialisation for this instance
      50           0 : void AP_Mount_Xacti::init()
      51             : {
      52             :     // instantiate parameter queue, return on failure so init fails
      53           0 :     _set_param_int32_queue = NEW_NOTHROW ObjectArray<SetParamQueueItem>(XACTI_SET_PARAM_QUEUE_SIZE);
      54           0 :     if (_set_param_int32_queue == nullptr) {
      55           0 :         GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "%s init failed", send_text_prefix);
      56           0 :         return;
      57             :     }
      58           0 :     _initialised = true;
      59             : }
      60             : 
      61             : // update mount position - should be called periodically
      62           0 : void AP_Mount_Xacti::update()
      63             : {
      64             :     // exit immediately if not initialised
      65           0 :     if (!_initialised) {
      66           0 :         return;
      67             :     }
      68             : 
      69             :     // return immediately if any message sent is unlikely to be processed
      70           0 :     const uint32_t now_ms = AP_HAL::millis();
      71           0 :     if (!is_safe_to_send(now_ms)) {
      72           0 :         return;
      73             :     }
      74             : 
      75             :     // get firmware version
      76           0 :     if (request_firmware_version(now_ms)) {
      77           0 :         return;
      78             :     }
      79             : 
      80             :     // additional initial setup
      81           0 :     if (_firmware_version.received) {
      82             :         // set date and time
      83           0 :         if (set_datetime(now_ms)) {
      84           0 :             return;
      85             :         }
      86             :          // request camera capabilities
      87           0 :         if (request_capabilities(now_ms)) {
      88           0 :             return;
      89             :         }
      90             :     }
      91             : 
      92             :     // request status
      93           0 :     if (request_status(now_ms)) {
      94           0 :         return;
      95             :     }
      96             : 
      97             :     // process queue of set parameter items
      98           0 :     if (process_set_param_int32_queue()) {
      99           0 :         return;
     100             :     }
     101             : 
     102             :     // periodically send copter attitude and GPS status
     103           0 :     if (send_copter_att_status(now_ms)) {
     104             :         // if message sent avoid sending other messages
     105           0 :         return;
     106             :     }
     107             : 
     108             :     // update zoom rate control
     109           0 :     if (update_zoom_rate_control(now_ms)) {
     110             :         // if message sent avoid sending other messages
     111           0 :         return;
     112             :     }
     113             : 
     114             :     // update based on mount mode
     115           0 :     update_mnt_target();
     116             : 
     117             :     // send target angles or rates depending on the target type
     118           0 :     send_target_to_gimbal();
     119             : }
     120             : 
     121             : // return true if healthy
     122           0 : bool AP_Mount_Xacti::healthy() const
     123             : {
     124             :     // unhealthy until gimbal has been found and replied with firmware version and no motor errors
     125           0 :     if (!_initialised || !_firmware_version.received || _motor_error) {
     126           0 :         return false;
     127             :     }
     128             : 
     129             :     // unhealthy if attitude information NOT received recently
     130           0 :     const uint32_t now_ms = AP_HAL::millis();
     131           0 :     if (now_ms - _last_current_attitude_quat_ms > 1000) {
     132           0 :         return false;
     133             :     }
     134             : 
     135             :     // if we get this far return healthy
     136           0 :     return true;
     137             : }
     138             : 
     139             : // take a picture.  returns true on success
     140           0 : bool AP_Mount_Xacti::take_picture()
     141             : {
     142             :     // fail if camera errored
     143           0 :     if (_camera_error) {
     144           0 :         GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "%s unable to take pic", send_text_prefix);
     145           0 :         return false;
     146             :     }
     147             : 
     148           0 :     return set_param_int32(Param::SingleShot, 0);
     149             : }
     150             : 
     151             : // start or stop video recording.  returns true on success
     152             : // set start_recording = true to start record, false to stop recording
     153           0 : bool AP_Mount_Xacti::record_video(bool start_recording)
     154             : {
     155           0 :     return set_param_int32(Param::Recording, start_recording ? 1 : 0);
     156             : }
     157             : 
     158             : // set zoom specified as a rate or percentage
     159           0 : bool AP_Mount_Xacti::set_zoom(ZoomType zoom_type, float zoom_value)
     160             : {
     161             :     // zoom rate
     162           0 :     if (zoom_type == ZoomType::RATE) {
     163           0 :         if (is_zero(zoom_value)) {
     164             :             // stop zooming
     165           0 :             _zoom_rate_control.enabled = false;
     166             :         } else {
     167             :             // zoom in or out
     168           0 :             _zoom_rate_control.enabled = true;
     169           0 :             _zoom_rate_control.dir = (zoom_value < 0) ? -1 : 1;
     170             :         }
     171           0 :         return true;
     172             :     }
     173             : 
     174             :     // zoom percentage
     175           0 :     if (zoom_type == ZoomType::PCT) {
     176           0 :         if (capabilities.optical_zoom == Capability::True) {
     177             :             // optical zoom capable cameras use combination of optical and digital zoom
     178             :             // convert zoom percentage (0 ~ 100) to zoom times using linear interpolation
     179             :             // optical zoom covers 1x to 2.5x, param values are in 100 to 250
     180             :             // digital zoom covers 2.5x to 25x, param values are 100 to 1000
     181           0 :             const float zoom_times = linear_interpolate(1, 25, zoom_value, 0, 100);
     182           0 :             const uint16_t optical_zoom_param = constrain_uint16(uint16_t(zoom_times * 10) * 10, 100, 250);
     183           0 :             const uint16_t digital_zoom_param = constrain_uint16(uint16_t(zoom_times * 0.4) * 100, 100, 1000);
     184           0 :             bool ret = true;
     185           0 :             if (optical_zoom_param != _last_optical_zoom_param_value) {
     186           0 :                 ret = set_param_int32(Param::OpticalZoomMagnification, optical_zoom_param);
     187             :             }
     188           0 :             if (digital_zoom_param != _last_digital_zoom_param_value) {
     189           0 :                 ret &= set_param_int32(Param::DigitalZoomMagnification, digital_zoom_param);
     190             :             }
     191           0 :             return ret;
     192             :         }
     193             :         // digital only zoom
     194             :         // convert zoom percentage (0 ~ 100) to zoom parameter value (100, 200, 300, ... 1000)
     195             :         // 0~11pct:100, 12~22pct:200, 23~33pct:300, 34~44pct:400, 45~55pct:500, 56~66pct:600, 67~77pct:700, 78~88pct:800, 89~99pct:900, 100:1000
     196           0 :         const uint16_t zoom_param_value = uint16_t(linear_interpolate(1, 10, zoom_value, 0, 100)) * 100;
     197           0 :         return set_param_int32(Param::DigitalZoomMagnification, zoom_param_value);
     198             :     }
     199             : 
     200             :     // unsupported zoom type
     201           0 :     return false;
     202             : }
     203             : 
     204             : // set focus specified as rate, percentage or auto
     205             : // focus in = -1, focus hold = 0, focus out = 1
     206           0 : SetFocusResult AP_Mount_Xacti::set_focus(FocusType focus_type, float focus_value)
     207             : {
     208             :     // convert focus type and value to parameter value
     209             :     uint8_t focus_param_value;
     210           0 :     switch (focus_type) {
     211           0 :     case FocusType::RATE:
     212             :     case FocusType::PCT:
     213             :         // focus rate and percentage control not supported so simply switch to manual focus
     214             :         // FocusMode of 0:Manual Focus
     215           0 :         focus_param_value = 0;
     216           0 :         break;
     217           0 :     case FocusType::AUTO:
     218             :         // FocusMode of 1:Single AutoFocus, 2:Continuous AutoFocus
     219           0 :         focus_param_value = 2;
     220           0 :         break;
     221           0 :     default:
     222             :         // unsupported forucs mode
     223           0 :         return SetFocusResult::INVALID_PARAMETERS;
     224             :     }
     225             : 
     226             :     // set FocusMode parameter
     227           0 :     return set_param_int32(Param::FocusMode, focus_param_value) ? SetFocusResult::ACCEPTED : SetFocusResult::FAILED;
     228             : }
     229             : 
     230             : // set camera lens as a value from 0 to 5
     231           0 : bool AP_Mount_Xacti::set_lens(uint8_t lens)
     232             : {
     233             :     // sanity check
     234           0 :     if (lens > (uint8_t)SensorsMode::NDVI) {
     235           0 :         return false;
     236             :     }
     237             : 
     238           0 :     return set_param_int32(Param::SensorMode, lens);
     239             : }
     240             : 
     241             : // set_camera_source is functionally the same as set_lens except primary and secondary lenses are specified by type
     242             : // primary and secondary sources use the AP_Camera::CameraSource enum cast to uint8_t
     243           0 : bool AP_Mount_Xacti::set_camera_source(uint8_t primary_source, uint8_t secondary_source)
     244             : {
     245             :     // maps primary and secondary source to xacti SensorsMode
     246             :     SensorsMode new_sensor_mode;
     247           0 :     switch (primary_source) {
     248           0 :     case 0: // Default (RGB)
     249             :         FALLTHROUGH;
     250             :     case 1: // RGB
     251           0 :         switch (secondary_source) {
     252           0 :         case 0: // RGB + Default (None)
     253           0 :             new_sensor_mode = SensorsMode::RGB;
     254           0 :             break;
     255           0 :         case 2: // PIP RGB+IR
     256           0 :             new_sensor_mode = SensorsMode::PIP;
     257           0 :             break;
     258           0 :         default:
     259           0 :             return false;
     260             :         }
     261           0 :         break;
     262           0 :     case 2: // IR
     263           0 :         if (secondary_source != 0) {
     264           0 :             return false;
     265             :         }
     266           0 :         new_sensor_mode = SensorsMode::IR;
     267           0 :         break;
     268           0 :     case 3: // NDVI
     269           0 :         if (secondary_source != 0) {
     270           0 :             return false;
     271             :         }
     272             :         // NDVI + Default (None)
     273           0 :         new_sensor_mode = SensorsMode::NDVI;
     274           0 :         break;
     275           0 :     default:
     276           0 :         return false;
     277             :     }
     278             : 
     279             :     // send desired sensor mode to camera
     280           0 :     return set_param_int32(Param::SensorMode, (uint8_t)new_sensor_mode);
     281             : }
     282             : 
     283             : // send camera settings message to GCS
     284           0 : void AP_Mount_Xacti::send_camera_settings(mavlink_channel_t chan) const
     285             : {
     286             :     // send CAMERA_SETTINGS message
     287           0 :     mavlink_msg_camera_settings_send(
     288             :         chan,
     289             :         AP_HAL::millis(),   // time_boot_ms
     290           0 :         _recording_video ? CAMERA_MODE_VIDEO : CAMERA_MODE_IMAGE,   // camera mode (0:image, 1:video, 2:image survey)
     291             :         0,                  // zoomLevel float, percentage from 0 to 100, NaN if unknown
     292             :         NaNf);              // focusLevel float, percentage from 0 to 100, NaN if unknown
     293           0 : }
     294             : 
     295             : // get attitude as a quaternion.  returns true on success
     296           0 : bool AP_Mount_Xacti::get_attitude_quaternion(Quaternion& att_quat)
     297             : {
     298           0 :     att_quat = _current_attitude_quat;
     299           0 :     return true;
     300             : }
     301             : 
     302             : // send target pitch and yaw rates to gimbal
     303           0 : void AP_Mount_Xacti::send_target_rates(const MountRateTarget &rate_rads)
     304             : {
     305           0 :     const float pitch_rads = rate_rads.pitch;
     306           0 :     const float yaw_rads = rate_rads.yaw;
     307             : 
     308             :     // send gimbal rate target to gimbal
     309           0 :     send_gimbal_control(3, degrees(pitch_rads) * 100, degrees(yaw_rads) * 100);
     310           0 : }
     311             : 
     312             : // send target pitch and yaw angles to gimbal
     313           0 : void AP_Mount_Xacti::send_target_angles(const MountAngleTarget &angle_rad)
     314             : {
     315           0 :     const float pitch_rad = angle_rad.pitch;
     316           0 :     const float yaw_rad = angle_rad.yaw;
     317           0 :     const bool yaw_is_ef = angle_rad.yaw_is_ef;
     318             : 
     319             :     // convert yaw to body frame
     320           0 :     const float yaw_bf_rad = yaw_is_ef ? wrap_PI(yaw_rad - AP::ahrs().get_yaw_rad()) : yaw_rad;
     321             : 
     322             :     // send angle target to gimbal
     323           0 :     send_gimbal_control(2, degrees(pitch_rad) * 100, degrees(yaw_bf_rad) * 100);
     324           0 : }
     325             : 
     326             : // subscribe to Xacti DroneCAN messages
     327           5 : bool AP_Mount_Xacti::subscribe_msgs(AP_DroneCAN* ap_dronecan)
     328             : {
     329           5 :     const auto driver_index = ap_dronecan->get_driver_index();
     330             : 
     331           5 :     return (Canard::allocate_sub_arg_callback(ap_dronecan, &handle_gimbal_attitude_status, driver_index) != nullptr)
     332           5 :         && (Canard::allocate_sub_arg_callback(ap_dronecan, &handle_gnss_status_req, driver_index) != nullptr)
     333             :     ;
     334             : }
     335             : 
     336             : // register backend in detected modules array used to map DroneCAN port and node id to backend
     337           0 : void AP_Mount_Xacti::register_backend()
     338             : {
     339           0 :     WITH_SEMAPHORE(_sem_registry);
     340             : 
     341             :     // add this backend to _detected_modules array
     342           0 :     _detected_modules[_instance].driver = this;
     343             : 
     344             :     // return if devid is zero meaning this backend has not yet been associated with a mount
     345           0 :     const uint32_t devid = (uint32_t)_params.dev_id.get();
     346           0 :     if (devid == 0) {
     347           0 :         return;
     348             :     }
     349             : 
     350             :     // get DroneCan port from device id
     351           0 :     const uint8_t can_driver_index = AP_HAL::Device::devid_get_bus(devid);
     352           0 :     const uint8_t can_num_drivers = AP::can().get_num_drivers();
     353           0 :     for (uint8_t i = 0; i < can_num_drivers; i++) {
     354           0 :         AP_DroneCAN *ap_dronecan = AP_DroneCAN::get_dronecan(i);
     355           0 :         if (ap_dronecan != nullptr && ap_dronecan->get_driver_index() == can_driver_index) {
     356           0 :             _detected_modules[_instance].ap_dronecan = ap_dronecan;
     357             :         }
     358             :     }
     359             : 
     360             :     // get node_id from device id
     361           0 :     _detected_modules[_instance].node_id = AP_HAL::Device::devid_get_address(devid);
     362           0 : }
     363             : 
     364             : // find backend associated with the given dronecan port and node_id.  also associates backends with zero node ids
     365             : // returns pointer to backend on success, nullptr on failure
     366           0 : AP_Mount_Xacti* AP_Mount_Xacti::get_dronecan_backend(AP_DroneCAN* ap_dronecan, uint8_t node_id)
     367             : {
     368           0 :     WITH_SEMAPHORE(_sem_registry);
     369             : 
     370             :     // exit immediately if DroneCAN is unavailable or invalid node id
     371           0 :     if (ap_dronecan == nullptr || node_id == 0) {
     372           0 :         return nullptr;
     373             :     }
     374             : 
     375             :     // search for backend with matching dronecan port and node id
     376           0 :     for (uint8_t i = 0; i < ARRAY_SIZE(_detected_modules); i++) {
     377           0 :         if (_detected_modules[i].driver != nullptr &&
     378           0 :             _detected_modules[i].ap_dronecan == ap_dronecan &&
     379           0 :             _detected_modules[i].node_id == node_id ) {
     380           0 :             return _detected_modules[i].driver;
     381             :         }
     382             :     }
     383             : 
     384             :     // if we got this far, this dronecan port and node id are not associated with any backend
     385             :     // associate with first backend with node id of zero
     386           0 :     for (uint8_t i = 0; i < ARRAY_SIZE(_detected_modules); i++) {
     387           0 :         if (_detected_modules[i].driver != nullptr &&
     388           0 :             _detected_modules[i].node_id == 0) {
     389           0 :                 _detected_modules[i].ap_dronecan = ap_dronecan;
     390           0 :                 _detected_modules[i].node_id = node_id;
     391           0 :                 const auto dev_id = AP_HAL::Device::make_bus_id(AP_HAL::Device::BUS_TYPE_UAVCAN,
     392           0 :                                                                 ap_dronecan->get_driver_index(),
     393             :                                                                 node_id, 0);
     394           0 :                 _detected_modules[i].driver->set_dev_id(dev_id);
     395           0 :                 return _detected_modules[i].driver;
     396             :         }
     397             :     }
     398             : 
     399           0 :     return nullptr;
     400           0 : }
     401             : 
     402             : // handle xacti gimbal attitude status message
     403           0 : void AP_Mount_Xacti::handle_gimbal_attitude_status(AP_DroneCAN* ap_dronecan, const CanardRxTransfer& transfer, const com_xacti_GimbalAttitudeStatus &msg)
     404             : {
     405             :     // fetch the matching backend driver, node id and gimbal id backend instance
     406           0 :     AP_Mount_Xacti* driver = get_dronecan_backend(ap_dronecan, transfer.source_node_id);
     407           0 :     if (driver == nullptr) {
     408           0 :         return;
     409             :     }
     410             : 
     411             :     // convert body-frame Euler angles to Quaternion.  Note yaw direction is reversed from normal
     412           0 :     driver->_current_attitude_quat.from_euler(radians(msg.gimbal_roll * 0.01), radians(msg.gimbal_pitch * 0.01), radians(-msg.gimbal_yaw * 0.01));
     413           0 :     driver->_last_current_attitude_quat_ms = AP_HAL::millis();
     414             : }
     415             : 
     416             : // handle xacti gnss status request message
     417           0 : void AP_Mount_Xacti::handle_gnss_status_req(AP_DroneCAN* ap_dronecan, const CanardRxTransfer& transfer, const com_xacti_GnssStatusReq &msg)
     418             : {
     419             :     // sanity check dronecan port
     420           0 :     if (ap_dronecan == nullptr) {
     421           0 :         return;
     422             :     }
     423             : 
     424             :     // get current location
     425           0 :     uint8_t gps_status = 2;
     426           0 :     Location loc;
     427           0 :     if (!AP::ahrs().get_location(loc)) {
     428           0 :         gps_status = 0;
     429             :     }
     430             : 
     431             :     // get date and time
     432             :     uint16_t year, ms;
     433             :     uint8_t month, day, hour, min, sec;
     434             : #if AP_RTC_ENABLED
     435           0 :     if (!AP::rtc().get_date_and_time_utc(year, month, day, hour, min, sec, ms)) {
     436           0 :         year = month = day = hour = min = sec = 0;
     437             :     }
     438             : #else
     439             :     year = month = day = hour = min = sec = 0;
     440             :     (void)ms;
     441             : #endif
     442             : 
     443             :     // send xacti specific gnss status message
     444           0 :     com_xacti_GnssStatus xacti_gnss_status_msg {};
     445           0 :     xacti_gnss_status_msg.gps_status = gps_status;
     446           0 :     xacti_gnss_status_msg.order = msg.requirement;
     447           0 :     xacti_gnss_status_msg.remain_buffer = 1;
     448           0 :     xacti_gnss_status_msg.utc_year = year;
     449           0 :     xacti_gnss_status_msg.utc_month = month + 1;
     450           0 :     xacti_gnss_status_msg.utc_day = day;
     451           0 :     xacti_gnss_status_msg.utc_hour = hour;
     452           0 :     xacti_gnss_status_msg.utc_minute = min;
     453           0 :     xacti_gnss_status_msg.utc_seconds = sec;
     454           0 :     xacti_gnss_status_msg.latitude = loc.lat * 1e-7;
     455           0 :     xacti_gnss_status_msg.longitude = loc.lng * 1e-7;
     456           0 :     xacti_gnss_status_msg.altitude = loc.alt * 1e-2;
     457           0 :     ap_dronecan->xacti_gnss_status.broadcast(xacti_gnss_status_msg);
     458             : }
     459             : 
     460             : // handle param get/set response
     461           0 : bool AP_Mount_Xacti::handle_param_get_set_response_int(AP_DroneCAN* ap_dronecan, uint8_t node_id, const char* name, int32_t &value)
     462             : {
     463             :     // error string prefix to save on flash
     464           0 :     const char* err_prefix_str = "Xacti: failed to";
     465             : 
     466             :     // take picture
     467           0 :     if (strcmp(name, get_param_name_str(Param::SingleShot)) == 0) {
     468           0 :         if (value < 0) {
     469           0 :             GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "%s take pic", err_prefix_str);
     470             :         }
     471           0 :         return false;
     472             :     }
     473             : 
     474             :     // recording
     475           0 :     if (strcmp(name, get_param_name_str(Param::Recording)) == 0) {
     476           0 :         if (value < 0) {
     477           0 :             _recording_video = false;
     478           0 :             GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "%s record", err_prefix_str);
     479             :         } else {
     480           0 :             _recording_video = (value == 1);
     481           0 :             GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s recording %s", send_text_prefix, _recording_video ? "ON" : "OFF");
     482             :         }
     483           0 :         return false;
     484             :     }
     485             : 
     486             :     // focus
     487           0 :     if (strcmp(name, get_param_name_str(Param::FocusMode)) == 0) {
     488           0 :         if (value < 0) {
     489           0 :             GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "%s change focus", err_prefix_str);
     490             :         } else {
     491           0 :             GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s %s focus", send_text_prefix, value == 0 ? "manual" : "auto");
     492             :         }
     493           0 :         return false;
     494             :     }
     495             : 
     496             :     // camera lens (aka sensor mode)
     497           0 :     if (strcmp(name, get_param_name_str(Param::SensorMode)) == 0) {
     498           0 :         if (value < 0) {
     499           0 :             GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "%s change lens", err_prefix_str);
     500           0 :         } else if ((uint32_t)value < ARRAY_SIZE(sensor_mode_str)) {
     501           0 :             GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s %s", send_text_prefix, sensor_mode_str[(uint8_t)value]);
     502             :         }
     503           0 :         return false;
     504             :     }
     505             : 
     506             :     // digital zoom
     507           0 :     if (strcmp(name, get_param_name_str(Param::DigitalZoomMagnification)) == 0) {
     508           0 :         if (value < 0) {
     509           0 :             GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "%s change zoom", err_prefix_str);
     510             :             // disable zoom rate control (if active) to avoid repeated failures
     511           0 :             _zoom_rate_control.enabled = false;
     512           0 :         } else if (value >= 100 && value <= 1000) {
     513           0 :             _last_digital_zoom_param_value = value;
     514             :         }
     515           0 :         return false;
     516             :     }
     517             : 
     518             :     // optical zoom
     519           0 :     if (strcmp(name, get_param_name_str(Param::OpticalZoomMagnification)) == 0) {
     520           0 :         if (value < 0) {
     521           0 :             GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "%s change optical zoom", err_prefix_str);
     522             :             // disable zoom rate control (if active) to avoid repeated failures
     523           0 :             _zoom_rate_control.enabled = false;
     524           0 :         } else if (value >= 100 && value <= 250) {
     525           0 :             capabilities.optical_zoom = Capability::True;
     526           0 :             capabilities.received = true;
     527           0 :             _last_optical_zoom_param_value = value;
     528             :         }
     529           0 :         return false;
     530             :     }
     531             :     
     532             :     // unhandled parameter get or set
     533           0 :     GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s get/set %s res:%ld", send_text_prefix, name, (long int)value);
     534           0 :     return false;
     535             : }
     536             : 
     537             : // handle param get/set response
     538           0 : bool AP_Mount_Xacti::handle_param_get_set_response_string(AP_DroneCAN* ap_dronecan, uint8_t node_id, const char* name, AP_DroneCAN::string &value)
     539             : {
     540           0 :     if (strcmp(name, get_param_name_str(Param::FirmwareVersion)) == 0) {
     541           0 :         _firmware_version.received = true;
     542           0 :         const uint8_t len = MIN(value.len, ARRAY_SIZE(_firmware_version.str)-1);
     543           0 :         memcpy(_firmware_version.str, (const char*)value.data, len);
     544           0 :         GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Mount: Xacti fw:%s", _firmware_version.str);
     545             : 
     546             :         // firmware str from gimbal is of the format YYMMDD[b]xx.  Convert to uint32 for reporting to GCS
     547           0 :         if (len >= 9) {
     548           0 :             const char major_str[3] = {_firmware_version.str[0], _firmware_version.str[1], 0};
     549           0 :             const char minor_str[3] = {_firmware_version.str[2], _firmware_version.str[3], 0};
     550           0 :             const char patch_str[3] = {_firmware_version.str[4], _firmware_version.str[5], 0};
     551           0 :             const char dev_str[3] = {_firmware_version.str[7], _firmware_version.str[8], 0};
     552           0 :             const uint8_t major_ver_num = atoi(major_str) & 0xFF;
     553           0 :             const uint8_t minor_ver_num = atoi(minor_str) & 0xFF;
     554           0 :             const uint8_t patch_ver_num = atoi(patch_str) & 0xFF;
     555           0 :             const uint8_t dev_ver_num = atoi(dev_str) & 0xFF;
     556           0 :             _firmware_version.mav_ver = UINT32_VALUE(dev_ver_num, patch_ver_num, minor_ver_num, major_ver_num);
     557             :         }
     558           0 :         return false;
     559           0 :     } else if (strcmp(name, get_param_name_str(Param::DateTime)) == 0) {
     560             :         // display when time and date have been set
     561           0 :         GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s datetime set %s", send_text_prefix, (const char*)value.data);
     562           0 :         return false;
     563           0 :     } else if (strcmp(name, get_param_name_str(Param::Status)) == 0) {
     564             :         // check for expected length
     565           0 :         const char* error_str = "error";
     566           0 :         if (value.len != sizeof(_status)) {
     567           0 :             INTERNAL_ERROR(AP_InternalError::error_t::invalid_arg_or_result);
     568           0 :             return false;
     569             :         }
     570             : 
     571             :         // backup error status and copy to structure
     572           0 :         const uint32_t last_error_status = _status.error_status;
     573           0 :         memcpy(&_status, value.data, value.len);
     574             : 
     575             :         // report change in status
     576           0 :         uint32_t changed_bits = last_error_status ^ _status.error_status;
     577           0 :         const char* ok_str = "OK";
     578           0 :         if (changed_bits & (uint32_t)ErrorStatus::TIME_NOT_SET) {
     579           0 :             GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s time %sset", send_text_prefix, _status.error_status & (uint32_t)ErrorStatus::TIME_NOT_SET ? "not " : "");
     580           0 :             if (_status.error_status & (uint32_t)ErrorStatus::TIME_NOT_SET) {
     581             :                 // try to set time again
     582           0 :                 _datetime.set = false;
     583             :             }
     584             :         }
     585           0 :         if (changed_bits & (uint32_t)ErrorStatus::MEDIA_ERROR) {
     586           0 :             GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s media %s", send_text_prefix, _status.error_status & (uint32_t)ErrorStatus::MEDIA_ERROR ? error_str : ok_str);
     587             :         }
     588           0 :         if (changed_bits & (uint32_t)ErrorStatus::LENS_ERROR) {
     589           0 :             GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s lens %s", send_text_prefix, _status.error_status & (uint32_t)ErrorStatus::LENS_ERROR ? error_str : ok_str);
     590             :         }
     591           0 :         if (changed_bits & (uint32_t)ErrorStatus::MOTOR_INIT_ERROR) {
     592           0 :             GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s motor %s", send_text_prefix, _status.error_status & (uint32_t)ErrorStatus::MOTOR_INIT_ERROR ? "init error" : ok_str);
     593             :         }
     594           0 :         if (changed_bits & (uint32_t)ErrorStatus::MOTOR_OPERATION_ERROR) {
     595           0 :             GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s motor op %s", send_text_prefix, _status.error_status & (uint32_t)ErrorStatus::MOTOR_OPERATION_ERROR ? error_str : ok_str);
     596             :         }
     597           0 :         if (changed_bits & (uint32_t)ErrorStatus::GIMBAL_CONTROL_ERROR) {
     598           0 :             GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s control %s", send_text_prefix, _status.error_status & (uint32_t)ErrorStatus::GIMBAL_CONTROL_ERROR ? error_str : ok_str);
     599             :         }
     600           0 :         if (changed_bits & (uint32_t)ErrorStatus::TEMP_WARNING) {
     601           0 :             GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s temp %s", send_text_prefix, _status.error_status & (uint32_t)ErrorStatus::TEMP_WARNING ? "warning" : ok_str);
     602             :         }
     603             : 
     604             :         // set motor error for health reporting
     605           0 :         _motor_error = _status.error_status & ((uint32_t)ErrorStatus::MOTOR_INIT_ERROR | (uint32_t)ErrorStatus::MOTOR_OPERATION_ERROR | (uint32_t)ErrorStatus::GIMBAL_CONTROL_ERROR);
     606           0 :         _camera_error = _status.error_status & ((uint32_t)ErrorStatus::LENS_ERROR | (uint32_t)ErrorStatus::MEDIA_ERROR);
     607           0 :         return false;
     608             :     }
     609             : 
     610             :     // unhandled parameter get or set
     611           0 :     GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s get/set string %s res:%s", send_text_prefix, name, (const char*)value.data);
     612           0 :     return false;
     613             : }
     614             : 
     615           0 : void AP_Mount_Xacti::handle_param_save_response(AP_DroneCAN* ap_dronecan, const uint8_t node_id, bool success)
     616             : {
     617             :     // display failure to save parameter
     618           0 :     if (!success) {
     619           0 :         GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "%s CAM%u failed to set param", send_text_prefix, (int)_instance+1);
     620             :     }
     621           0 : }
     622             : 
     623             : // get parameter name for a particular param enum value
     624             : // returns an empty string if not found (which should never happen)
     625           0 : const char* AP_Mount_Xacti::get_param_name_str(Param param) const
     626             : {
     627             :     // check to avoid reading beyond end of array.  This should never happen
     628           0 :     if ((uint8_t)param > ARRAY_SIZE(_param_names)) {
     629           0 :         INTERNAL_ERROR(AP_InternalError::error_t::invalid_arg_or_result);
     630           0 :         return "";
     631             :     }
     632           0 :     return _param_names[(uint8_t)param];
     633             : }
     634             : 
     635             : // helper function to set integer parameters
     636           0 : bool AP_Mount_Xacti::set_param_int32(Param param, int32_t param_value)
     637             : {
     638           0 :     if (_set_param_int32_queue == nullptr) {
     639           0 :         return false;
     640             :     }
     641             : 
     642             :     // set param request added to queue to be sent.  throttling requests improves reliability
     643           0 :     return _set_param_int32_queue->push(SetParamQueueItem{param, param_value});
     644             : }
     645             : 
     646             : // helper function to set string parameters
     647           0 : bool AP_Mount_Xacti::set_param_string(Param param, const AP_DroneCAN::string& param_value)
     648             : {
     649           0 :     if (_detected_modules[_instance].ap_dronecan == nullptr) {
     650           0 :         return false;
     651             :     }
     652             : 
     653             :     // convert param to string
     654           0 :     const char* param_name_str = get_param_name_str(param);
     655           0 :     if (param_name_str == nullptr) {
     656           0 :         return false;
     657             :     }
     658             : 
     659           0 :     if (_detected_modules[_instance].ap_dronecan->set_parameter_on_node(_detected_modules[_instance].node_id, param_name_str, param_value, &param_string_cb)) {
     660           0 :         last_send_getset_param_ms = AP_HAL::millis();
     661           0 :         return true;
     662             :     }
     663           0 :     return false;
     664             : }
     665             : 
     666             : // helper function to get string parameters
     667           0 : bool AP_Mount_Xacti::get_param_string(Param param)
     668             : {
     669           0 :     if (_detected_modules[_instance].ap_dronecan == nullptr) {
     670           0 :         return false;
     671             :     }
     672             : 
     673             :     // convert param to string
     674           0 :     const char* param_name_str = get_param_name_str(param);
     675           0 :     if (_detected_modules[_instance].ap_dronecan->get_parameter_on_node(_detected_modules[_instance].node_id, param_name_str, &param_string_cb)) {
     676           0 :         last_send_getset_param_ms = AP_HAL::millis();
     677           0 :         return true;
     678             :     }
     679           0 :     return false;
     680             : }
     681             : 
     682             : // process queue of set parameter items
     683           0 : bool AP_Mount_Xacti::process_set_param_int32_queue()
     684             : {
     685           0 :     if ((_set_param_int32_queue == nullptr) || (_detected_modules[_instance].ap_dronecan == nullptr)) {
     686           0 :         return false;
     687             :     }
     688             : 
     689             :     SetParamQueueItem param_to_set;
     690           0 :     if (_set_param_int32_queue->pop(param_to_set)) {
     691             :         // convert param to string
     692           0 :         const char* param_name_str = get_param_name_str(param_to_set.param);
     693           0 :         if (_detected_modules[_instance].ap_dronecan->set_parameter_on_node(_detected_modules[_instance].node_id, param_name_str, param_to_set.value, &param_int_cb)) {
     694           0 :             last_send_getset_param_ms = AP_HAL::millis();
     695           0 :             return true;
     696             :         }
     697           0 :         return false;
     698             :     }
     699           0 :     return false;
     700             : }
     701             : 
     702             : // send gimbal control message via DroneCAN
     703             : // mode is 2:angle control or 3:rate control
     704             : // pitch_cd is pitch angle in centi-degrees or pitch rate in cds
     705             : // yaw_cd is angle in centi-degrees or yaw rate in cds
     706           0 : void AP_Mount_Xacti::send_gimbal_control(uint8_t mode, int16_t pitch_cd, int16_t yaw_cd)
     707             : {
     708             :     // exit immediately if no DroneCAN port
     709           0 :     if (_detected_modules[_instance].ap_dronecan == nullptr) {
     710           0 :         return;
     711             :     }
     712             : 
     713             :     // send at no faster than 5hz
     714           0 :     const uint32_t now_ms = AP_HAL::millis();
     715           0 :     if (now_ms - last_send_gimbal_control_ms < 200) {
     716           0 :         return;
     717             :     }
     718           0 :     last_send_gimbal_control_ms = now_ms;
     719             : 
     720             :     // send xacti specific gimbal control message
     721           0 :     com_xacti_GimbalControlData gimbal_control_data_msg {};
     722           0 :     gimbal_control_data_msg.pitch_cmd_type = mode;
     723           0 :     gimbal_control_data_msg.yaw_cmd_type = mode;
     724           0 :     gimbal_control_data_msg.pitch_cmd_value = pitch_cd;
     725           0 :     gimbal_control_data_msg.yaw_cmd_value = -yaw_cd;
     726           0 :     _detected_modules[_instance].ap_dronecan->xacti_gimbal_control_data.broadcast(gimbal_control_data_msg);
     727             : }
     728             : 
     729             : // send copter attitude status message to gimbal.  now_ms is current system time
     730             : // returns true if sent so that we avoid immediately trying to also send other messages
     731           0 : bool AP_Mount_Xacti::send_copter_att_status(uint32_t now_ms)
     732             : {
     733             :     // exit immediately if no DroneCAN port
     734           0 :     if (_detected_modules[_instance].ap_dronecan == nullptr) {
     735           0 :         return false;
     736             :     }
     737             : 
     738             :     // send at no faster than 5hz
     739           0 :     if (now_ms - last_send_copter_att_status_ms < 100) {
     740           0 :         return false;
     741             :     }
     742             : 
     743             :     // send xacti specific vehicle attitude message
     744           0 :     Quaternion veh_att;
     745           0 :     if (!AP::ahrs().get_quaternion(veh_att)) {
     746           0 :         return false;
     747             :     }
     748             : 
     749           0 :     last_send_copter_att_status_ms = now_ms;
     750           0 :     com_xacti_CopterAttStatus copter_att_status_msg {};
     751           0 :     copter_att_status_msg.quaternion_wxyz_e4[0] = veh_att.q1 * 1e4;
     752           0 :     copter_att_status_msg.quaternion_wxyz_e4[1] = veh_att.q2 * 1e4;
     753           0 :     copter_att_status_msg.quaternion_wxyz_e4[2] = veh_att.q3 * 1e4;
     754           0 :     copter_att_status_msg.quaternion_wxyz_e4[3] = veh_att.q4 * 1e4;
     755           0 :     copter_att_status_msg.reserved.len = 2;
     756           0 :     copter_att_status_msg.reserved.data[0] = 0;
     757           0 :     copter_att_status_msg.reserved.data[1] = 0;
     758           0 :     _detected_modules[_instance].ap_dronecan->xacti_copter_att_status.broadcast(copter_att_status_msg);
     759           0 :     return true;
     760             : }
     761             : 
     762             : // update zoom rate controller.  now_ms is current system time
     763             : // returns true if sent so that we avoid immediately trying to also send other messages
     764           0 : bool AP_Mount_Xacti::update_zoom_rate_control(uint32_t now_ms)
     765             : {
     766             :     // return immediately if zoom rate control is not enabled
     767           0 :     if (!_zoom_rate_control.enabled) {
     768           0 :         return false;
     769             :     }
     770             : 
     771             :     // we are controlling optical zoom if the camera has it and we are below the optical zoom upper limit
     772             :     // or at the optical zoom upper limit, the lower digital zoom limit and are zooming out
     773           0 :     bool optical_zoom_control = (capabilities.optical_zoom == Capability::True) &&
     774           0 :                                 ((_last_optical_zoom_param_value < 250) ||
     775           0 :                                  ((_last_optical_zoom_param_value == 250) && (_last_digital_zoom_param_value == 100) && (_zoom_rate_control.dir < 0)));
     776             : 
     777             :     // update every 0.25 or 0.5 sec
     778           0 :     const uint32_t update_interval_ms = optical_zoom_control ? XACTI_OPTICAL_ZOOM_RATE_UPDATE_INTERVAL_MS : XACTI_DIGITAL_ZOOM_RATE_UPDATE_INTERVAL_MS;
     779           0 :     if (now_ms - _zoom_rate_control.last_update_ms < update_interval_ms) {
     780           0 :         return false;
     781             :     }
     782           0 :     _zoom_rate_control.last_update_ms = now_ms;
     783             : 
     784             :     // optical zoom
     785           0 :     if (optical_zoom_control) {
     786           0 :         const uint16_t optical_zoom_value = _last_optical_zoom_param_value + _zoom_rate_control.dir * 10;
     787             :         // if reached lower limit of optical zoom, disable zoom control
     788           0 :         if (optical_zoom_value < 100) {
     789           0 :             _zoom_rate_control.enabled = false;
     790           0 :             return false;
     791             :         }
     792             : 
     793             :         // send desired optical zoom to camera
     794           0 :         return set_param_int32(Param::OpticalZoomMagnification, MIN(optical_zoom_value, 250));
     795             :     }
     796             : 
     797             :     // digital zoom
     798           0 :     const uint16_t digital_zoom_value = _last_digital_zoom_param_value + _zoom_rate_control.dir * 100;
     799             :     // if reached limit then disable zoom
     800           0 :     if (((capabilities.optical_zoom != Capability::True) && (digital_zoom_value < 100)) || (digital_zoom_value > 1000)) {
     801           0 :         _zoom_rate_control.enabled = false;
     802           0 :         return false;
     803             :     }
     804             : 
     805             :     // send desired digital zoom to camera
     806           0 :     return set_param_int32(Param::DigitalZoomMagnification, digital_zoom_value);
     807             : }
     808             : 
     809             : // request firmware version. now_ms should be current system time (reduces calls to AP_HAL::millis)
     810             : // returns true if sent so that we avoid immediately trying to also send other messages
     811           0 : bool AP_Mount_Xacti::request_firmware_version(uint32_t now_ms)
     812             : {
     813             :     // return immediately if already have version or no dronecan
     814           0 :     if (_firmware_version.received) {
     815           0 :         return false;
     816             :     }
     817             : 
     818             :     // send request once per second until received
     819           0 :     if (now_ms - _firmware_version.last_request_ms < 1000) {
     820           0 :         return false;
     821             :     }
     822           0 :     _firmware_version.last_request_ms = now_ms;
     823           0 :     return get_param_string(Param::FirmwareVersion);
     824             : }
     825             : 
     826             : // request parameters used to determine camera capabilities.  now_ms is current system time
     827             : // returns true if a param get/set was sent so that we avoid sending other messages
     828           0 : bool AP_Mount_Xacti::request_capabilities(uint32_t now_ms)
     829             : {
     830             :     // return immediately if we have already determined this models capabilities
     831           0 :     if (capabilities.received) {
     832           0 :         return false;
     833             :     }
     834             : 
     835             :     // send requests once per second until received
     836           0 :     if (now_ms - capabilities.last_request_ms < 1000) {
     837           0 :         return false;
     838             :     }
     839           0 :     capabilities.last_request_ms = now_ms;
     840             : 
     841             :     // record start time
     842           0 :     if (capabilities.first_request_ms == 0) {
     843           0 :         capabilities.first_request_ms = now_ms;
     844             :     }
     845             : 
     846             :     // timeout after 10 seconds
     847           0 :     if (now_ms - capabilities.first_request_ms > 10000) {
     848           0 :         capabilities.optical_zoom = Capability::False;
     849           0 :         capabilities.received = true;
     850           0 :         return false;
     851             :     }
     852             : 
     853             :     // optical zoom: try setting optical zoom to 1x
     854             :     // return is handled in handle_param_get_set_response_int
     855           0 :     return set_param_int32(Param::OpticalZoomMagnification, 100);
     856             : }
     857             : 
     858             : // set date and time.  now_ms is current system time
     859           0 : bool AP_Mount_Xacti::set_datetime(uint32_t now_ms)
     860             : {
     861             :     // return immediately if gimbal's date/time has been set
     862           0 :     if (_datetime.set) {
     863           0 :         return false;
     864             :     }
     865             : 
     866             :     // attempt to set datetime once per second until received
     867           0 :     if (now_ms - _datetime.last_request_ms < 1000) {
     868           0 :         return false;
     869             :     }
     870           0 :     _datetime.last_request_ms = now_ms;
     871             : 
     872             :     // get date and time
     873             :     uint16_t year, ms;
     874             :     uint8_t month, day, hour, min, sec;
     875             : #if AP_RTC_ENABLED
     876           0 :     if (!AP::rtc().get_date_and_time_utc(year, month, day, hour, min, sec, ms)) {
     877           0 :         return false;
     878             :     }
     879             : #else
     880             :     (void)ms;
     881             :     return false;
     882             : #endif
     883             : 
     884             :     // date time is of the format YYYYMMDDHHMMSS (14 bytes)
     885             :     // convert month from 0~11 to 1~12 range
     886           0 :     AP_DroneCAN::string datetime_string {};
     887           0 :     const int num_bytes = snprintf((char *)datetime_string.data, sizeof(AP_DroneCAN::string::data), "%04u%02u%02u%02u%02u%02u",
     888           0 :                              (unsigned)year, (unsigned)month+1, (unsigned)day, (unsigned)hour, (unsigned)min, (unsigned)sec);
     889             :     // sanity check bytes to be sent
     890           0 :     if (num_bytes != 14) {
     891           0 :         INTERNAL_ERROR(AP_InternalError::error_t::invalid_arg_or_result);
     892           0 :         return false;
     893             :     }
     894           0 :     datetime_string.len = num_bytes;
     895           0 :     _datetime.set = set_param_string(Param::DateTime, datetime_string);
     896           0 :     if (!_datetime.set) {
     897           0 :         GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "%s failed to set date/time", send_text_prefix);
     898             :     }
     899             : 
     900           0 :     return _datetime.set;
     901             : }
     902             : 
     903             : // request status. now_ms should be current system time (reduces calls to AP_HAL::millis)
     904             : // returns true if sent so that we avoid immediately trying to also send other messages
     905           0 : bool AP_Mount_Xacti::request_status(uint32_t now_ms)
     906             : {
     907             :     // return immediately if 3 seconds has not passed
     908           0 :     if (now_ms - _status_report.last_request_ms < XACTI_STATUS_REQ_INTERVAL_MS) {
     909           0 :         return false;
     910             :     }
     911             : 
     912           0 :     _status_report.last_request_ms = now_ms;
     913           0 :     return get_param_string(Param::Status);
     914             : }
     915             : 
     916             : // check if safe to send message (if messages sent too often camera will not respond)
     917             : // now_ms should be current system time (reduces calls to AP_HAL::millis)
     918           0 : bool AP_Mount_Xacti::is_safe_to_send(uint32_t now_ms) const
     919             : {
     920             :     // check time since last attitude sent
     921           0 :     if (now_ms - last_send_copter_att_status_ms < XACTI_MSG_SEND_MIN_MS) {
     922           0 :         return false;
     923             :     }
     924             : 
     925             :     // check time since last angle target sent
     926           0 :     if (now_ms - last_send_gimbal_control_ms < XACTI_MSG_SEND_MIN_MS) {
     927           0 :         return false;
     928             :     }
     929             : 
     930             :     // check time since last set param message sent
     931           0 :     if (now_ms - last_send_getset_param_ms < XACTI_MSG_SEND_MIN_MS) {
     932           0 :         return false;
     933             :     }
     934             : 
     935           0 :     return true;
     936             : }
     937             : 
     938             : #endif // HAL_MOUNT_XACTI_ENABLED

Generated by: LCOV version 1.14