LCOV - code coverage report
Current view: top level - AP_Mount - AP_Mount_Siyi.cpp (source / functions) Hit Total Coverage
Test: lcov.info Lines: 260 565 46.0 %
Date: 2026-09-09 11:02:08 Functions: 17 30 56.7 %

          Line data    Source code
       1             : #include "AP_Mount_config.h"
       2             : 
       3             : #if HAL_MOUNT_SIYI_ENABLED
       4             : 
       5             : #include "AP_Mount_Siyi.h"
       6             : 
       7             : #include <AP_HAL/AP_HAL.h>
       8             : #include <AP_AHRS/AP_AHRS.h>
       9             : #include <GCS_MAVLink/GCS.h>
      10             : #include <GCS_MAVLink/include/mavlink/v2.0/checksum.h>
      11             : #include <AP_RTC/AP_RTC.h>
      12             : 
      13             : extern const AP_HAL::HAL& hal;
      14             : 
      15             : #define AP_MOUNT_SIYI_HEADER1       0x55    // first header byte
      16             : #define AP_MOUNT_SIYI_HEADER2       0x66    // second header byte
      17             : #define AP_MOUNT_SIYI_PACKETLEN_MIN 10      // minimum number of bytes in a packet.  this is a packet with no data bytes
      18             : #define AP_MOUNT_SIYI_DATALEN_MAX   (AP_MOUNT_SIYI_PACKETLEN_MAX-AP_MOUNT_SIYI_PACKETLEN_MIN) // max bytes for data portion of packet
      19             : #define AP_MOUNT_SIYI_RATE_MAX_RADS radians(90) // maximum physical rotation rate of gimbal in radans/sec
      20             : #define AP_MOUNT_SIYI_PITCH_P       1.50    // pitch controller P gain (converts pitch angle error to target rate)
      21             : #define AP_MOUNT_SIYI_YAW_P         1.50    // yaw controller P gain (converts yaw angle error to target rate)
      22             : #define AP_MOUNT_SIYI_TIMEOUT_MS    1000    // timeout for health and rangefinder readings
      23             : #define AP_MOUNT_SIYI_THERM_TIMEOUT_MS  3000// timeout for temp min/max readings
      24             : 
      25             : #define AP_MOUNT_SIYI_DEBUG 0
      26             : #define debug(fmt, args ...) do { if (AP_MOUNT_SIYI_DEBUG) { GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Siyi: " fmt, ## args); } } while (0)
      27             : 
      28             : // hardware lookup table indexed by HardwareModel enum values
      29             : const AP_Mount_Siyi::HWInfo AP_Mount_Siyi::hardware_lookup_table[] {
      30             :         {{'0','0'}, "Unknown"},
      31             :         {{'7','5'}, "A2"},
      32             :         {{'7','3'}, "A8"},
      33             :         {{'6','B'}, "ZR10"},
      34             :         {{'7','8'}, "ZR30"},
      35             :         {{'8','3'}, "ZT6"},
      36             :         {{'7','A'}, "ZT30"},
      37             : };
      38             : 
      39             : // update mount position - should be called periodically
      40         129 : void AP_Mount_Siyi::update()
      41             : {
      42         129 :     AP_Mount_Backend::update();
      43             : 
      44             :     // exit immediately if not initialised
      45         129 :     if (!_initialised) {
      46           0 :         return;
      47             :     }
      48             : 
      49             :     // reading incoming packets from gimbal
      50         129 :     read_incoming_packets();
      51             : 
      52             :     // request firmware version during startup at 1hz
      53             :     // during regular operation request configuration at 1hz
      54         129 :     uint32_t now_ms = AP_HAL::millis();
      55         129 :     if ((now_ms - _last_send_ms) >= 1000) {
      56           3 :         _last_send_ms = now_ms;
      57           3 :         if (!_got_hardware_id) {
      58           1 :             request_hardware_id();
      59           1 :             return;
      60           2 :         } else if (!_fw_version.received) {
      61           1 :             request_firmware_version();
      62           1 :             return;
      63             :         } else {
      64           1 :             request_configuration();
      65             :         }
      66             : 
      67             : #if AP_RTC_ENABLED
      68             :         // send UTC time to the camera
      69           1 :         if (sent_time_count < 5) {
      70             :             uint64_t utc_usec;
      71           1 :             if (AP::rtc().get_utc_usec(utc_usec) &&
      72           0 :                 send_packet(SiyiCommandId::SET_TIME, (const uint8_t *)&utc_usec, sizeof(utc_usec))) {
      73           0 :                 sent_time_count++;
      74             :             }
      75             :         }
      76             : #endif
      77             :     }
      78             : 
      79             :     // request attitude at regular intervals
      80         127 :     if ((now_ms - _last_req_current_angle_rad_ms) >= 50) {
      81          43 :         request_gimbal_attitude();
      82          43 :         _last_req_current_angle_rad_ms = now_ms;
      83             :     }
      84             : 
      85             :     // request rangefinder distance from ZT30 at 10hz
      86         127 :     if ((_hardware_model == HardwareModel::ZT30) && (now_ms - _last_rangefinder_req_ms > 100)) {
      87          22 :         request_rangefinder_distance();
      88          22 :         _last_rangefinder_req_ms = now_ms;
      89             :     }
      90             : 
      91             : #if AP_MOUNT_SEND_THERMAL_RANGE_ENABLED
      92             :     // request thermal min/max from ZT30 or ZT6
      93         127 :     request_thermal_minmax();
      94             : #endif
      95             : 
      96             :     // send attitude to gimbal at 10Hz
      97         127 :     if (now_ms - _last_attitude_send_ms > 100) {
      98          22 :         _last_attitude_send_ms = now_ms;
      99          22 :         send_attitude_position();
     100             :     }
     101             : 
     102             :     // run zoom control
     103         127 :     update_zoom_control();
     104             : 
     105             :     // update based on mount mode
     106         127 :     update_mnt_target();
     107             : 
     108             :     // send target angles or rates depending on the target type
     109         127 :     send_target_to_gimbal();
     110             : }
     111             : 
     112             : // return true if healthy
     113           2 : bool AP_Mount_Siyi::healthy() const
     114             : {
     115             :     // unhealthy until gimbal has been found and replied with firmware version info
     116           2 :     if (!_initialised || !_fw_version.received) {
     117           1 :         return false;
     118             :     }
     119             : 
     120             :     // unhealthy if attitude information NOT received recently
     121           1 :     const uint32_t now_ms = AP_HAL::millis();
     122           1 :     if (now_ms - _last_current_angle_rad_ms > AP_MOUNT_SIYI_TIMEOUT_MS) {
     123           0 :         return false;
     124             :     }
     125             : 
     126             :     // if we get this far return healthy
     127           1 :     return true;
     128             : }
     129             : 
     130             : // get attitude as a quaternion.  returns true on success
     131          38 : bool AP_Mount_Siyi::get_attitude_quaternion(Quaternion& att_quat)
     132             : {
     133             :     // Order of rotation for Siyi gimbals is (yaw, roll, pitch), which is 312 order
     134          38 :     att_quat.from_vector312(_current_angle_rad.x, _current_angle_rad.y, _current_angle_rad.z);
     135          38 :     return true;
     136             : }
     137             : 
     138             : // reading incoming packets from gimbal and confirm they are of the correct format
     139             : // results are held in the _parsed_msg structure
     140         129 : void AP_Mount_Siyi::read_incoming_packets()
     141             : {
     142             :     // check for bytes on the serial port
     143         129 :     int16_t nbytes = MIN(_uart->available(), 1024U);
     144         129 :     if (nbytes <= 0 ) {
     145          84 :         return;
     146             :     }
     147             : 
     148             :     // flag to allow cases below to reset parser state
     149          45 :     bool reset_parser = false;
     150             : 
     151             :     // process bytes received
     152        1289 :     for (int16_t i = 0; i < nbytes; i++) {
     153             :         uint8_t b;
     154        1244 :         if (!_uart->read(b)) {
     155           0 :             continue;
     156             :         }
     157             : 
     158        1244 :         _msg_buff[_msg_buff_len++] = b;
     159             : 
     160             :         // protect against overly long messages
     161        1244 :         if (_msg_buff_len >= AP_MOUNT_SIYI_PACKETLEN_MAX) {
     162           0 :             reset_parser = true;
     163             :         }
     164             : 
     165             :         // process byte depending upon current state
     166        1244 :         switch (_parsed_msg.state) {
     167             : 
     168          67 :         case ParseState::WAITING_FOR_HEADER_LOW:
     169          67 :             if (b == AP_MOUNT_SIYI_HEADER1) {
     170          67 :                 _parsed_msg.state = ParseState::WAITING_FOR_HEADER_HIGH;
     171             :             } else {
     172           0 :                 reset_parser = true;
     173             :             }
     174          67 :             break;
     175             : 
     176          67 :         case ParseState::WAITING_FOR_HEADER_HIGH:
     177          67 :             if (b == AP_MOUNT_SIYI_HEADER2) {
     178          67 :                 _parsed_msg.state = ParseState::WAITING_FOR_CTRL;
     179             :             } else {
     180           0 :                 reset_parser = true;
     181             :             }
     182          67 :             break;
     183             : 
     184          67 :         case ParseState::WAITING_FOR_CTRL:
     185          67 :             _parsed_msg.state = ParseState::WAITING_FOR_DATALEN_LOW;
     186          67 :             break;
     187             : 
     188          67 :         case ParseState::WAITING_FOR_DATALEN_LOW:
     189          67 :             _parsed_msg.data_len = b;
     190          67 :             _parsed_msg.state = ParseState::WAITING_FOR_DATALEN_HIGH;
     191          67 :             break;
     192             : 
     193          67 :         case ParseState::WAITING_FOR_DATALEN_HIGH:
     194          67 :             _parsed_msg.data_len |= ((uint16_t)b << 8);
     195             :             // sanity check data length
     196          67 :             if (_parsed_msg.data_len <= AP_MOUNT_SIYI_DATALEN_MAX) {
     197          67 :                 _parsed_msg.state = ParseState::WAITING_FOR_SEQ_LOW;
     198             :             } else {
     199           0 :                 reset_parser = true;
     200             :                 debug("data len too large:%u (>%u)", (unsigned)_parsed_msg.data_len, (unsigned)AP_MOUNT_SIYI_DATALEN_MAX);
     201             :             }
     202          67 :             break;
     203             : 
     204          67 :         case ParseState::WAITING_FOR_SEQ_LOW:
     205          67 :             _parsed_msg.state = ParseState::WAITING_FOR_SEQ_HIGH;
     206          67 :             break;
     207             : 
     208          67 :         case ParseState::WAITING_FOR_SEQ_HIGH:
     209          67 :             _parsed_msg.state = ParseState::WAITING_FOR_CMDID;
     210          67 :             break;
     211             : 
     212          67 :         case ParseState::WAITING_FOR_CMDID:
     213          67 :             _parsed_msg.command_id = b;
     214          67 :             _parsed_msg.data_bytes_received = 0;
     215          67 :             if (_parsed_msg.data_len > 0) {
     216          67 :                 _parsed_msg.state = ParseState::WAITING_FOR_DATA;
     217             :             } else {
     218           0 :                 _parsed_msg.state = ParseState::WAITING_FOR_CRC_LOW;
     219             :             }
     220          67 :             break;
     221             : 
     222         574 :         case ParseState::WAITING_FOR_DATA:
     223         574 :             _parsed_msg.data_bytes_received++;
     224         574 :             if (_parsed_msg.data_bytes_received >= _parsed_msg.data_len) {
     225          67 :                 _parsed_msg.state = ParseState::WAITING_FOR_CRC_LOW;
     226             :             }
     227         574 :             break;
     228             : 
     229          67 :         case ParseState::WAITING_FOR_CRC_LOW:
     230          67 :             _parsed_msg.crc16 = b;
     231          67 :             _parsed_msg.state = ParseState::WAITING_FOR_CRC_HIGH;
     232          67 :             break;
     233             : 
     234          67 :         case ParseState::WAITING_FOR_CRC_HIGH:
     235          67 :             _parsed_msg.crc16 |= ((uint16_t)b << 8);
     236             : 
     237             :             // check crc
     238          67 :             const uint16_t expected_crc = crc16_ccitt(_msg_buff, _msg_buff_len-2, 0);
     239          67 :             if (expected_crc == _parsed_msg.crc16) {
     240             :                 // successfully received a message, do something with it
     241          67 :                 process_packet();
     242             : #if AP_MOUNT_SIYI_DEBUG
     243             :             } else {
     244             :                 debug("crc expected:%x got:%x", (unsigned)expected_crc, (unsigned)_parsed_msg.crc16);
     245             : #endif
     246             :             }
     247          67 :             reset_parser = true;
     248          67 :             break;
     249             :         }
     250             : 
     251             :         // handle reset of parser
     252        1244 :         if (reset_parser) {
     253          67 :             _parsed_msg.state = ParseState::WAITING_FOR_HEADER_LOW;
     254          67 :             _msg_buff_len = 0;
     255          67 :             reset_parser = false;
     256             :         }
     257             :     }
     258             : }
     259             : 
     260             : // process successfully decoded packets held in the _parsed_msg structure
     261          67 : void AP_Mount_Siyi::process_packet()
     262             : {
     263             : #if AP_MOUNT_SIYI_DEBUG
     264             :     // flag to warn of unexpected data buffer length
     265             :     bool unexpected_len = false;
     266             : #endif
     267             : 
     268             :     // process packet depending upon command id
     269          67 :     switch ((SiyiCommandId)_parsed_msg.command_id) {
     270             : 
     271           1 :     case SiyiCommandId::ACQUIRE_FIRMWARE_VERSION: {
     272           1 :         if (_parsed_msg.data_bytes_received != 12 &&    // ZR10 firmware version reply is 12bytes
     273           0 :             _parsed_msg.data_bytes_received != 8) {     // A8 firmware version reply is 8 bytes
     274             : #if AP_MOUNT_SIYI_DEBUG
     275             :             unexpected_len = true;
     276             : #endif
     277           0 :             break;
     278             :         }
     279             : 
     280             :         // consume and display camera firmware version
     281           1 :         _fw_version = {};
     282           1 :         _fw_version.camera.major = _msg_buff[_msg_buff_data_start+2];  // firmware major version
     283           1 :         _fw_version.camera.minor = _msg_buff[_msg_buff_data_start+1];  // firmware minor version
     284           1 :         _fw_version.camera.patch = _msg_buff[_msg_buff_data_start+0];  // firmware revision (aka patch)
     285             : 
     286           1 :         _fw_version.gimbal.major = _msg_buff[_msg_buff_data_start+6];  // firmware major version
     287           1 :         _fw_version.gimbal.minor = _msg_buff[_msg_buff_data_start+5];  // firmware minor version
     288           1 :         _fw_version.gimbal.patch = _msg_buff[_msg_buff_data_start+4];  // firmware revision (aka patch)
     289             : 
     290             :         // camera firmware version may be all zero soon after startup.  giveup and try again later
     291           1 :         if (_fw_version.camera.major == 0 && _fw_version.camera.minor == 0 && _fw_version.camera.patch == 0) {
     292           0 :             break;
     293             :         }
     294             : 
     295           1 :         _fw_version.received = true;
     296             : 
     297             :         // display camera info to user
     298           1 :         GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Mount: Siyi camera fw v%u.%u.%u",
     299             :                 _fw_version.camera.major,
     300             :                 _fw_version.camera.minor,
     301             :                 _fw_version.camera.patch);
     302             : 
     303             :         // display gimbal info to user
     304           1 :         GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Mount: Siyi gimbal fw v%u.%u.%u",
     305             :                 _fw_version.gimbal.major,
     306             :                 _fw_version.gimbal.minor,
     307             :                 _fw_version.gimbal.patch);
     308             : 
     309             :         // display zoom firmware version for those that have it
     310           1 :         if (_parsed_msg.data_bytes_received >= 12) {
     311           1 :             _fw_version.zoom.major = _msg_buff[_msg_buff_data_start+10];
     312           1 :             _fw_version.zoom.minor = _msg_buff[_msg_buff_data_start+ 9];
     313           1 :             _fw_version.zoom.patch = _msg_buff[_msg_buff_data_start+ 8];
     314           1 :             GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Mount: Siyi zoom fw v%u.%u.%u",
     315             :                 _fw_version.zoom.major,
     316             :                 _fw_version.zoom.minor,
     317             :                 _fw_version.zoom.patch);
     318             :         }
     319             : 
     320             :         // report to the user if gimbal firmware is not up-to-date
     321           1 :         check_firmware_version();
     322             : 
     323           1 :         break;
     324             :     }
     325             : 
     326           1 :     case SiyiCommandId::HARDWARE_ID: {
     327             :         // lookup first two digits of hardware id
     328           1 :         const uint8_t hwid0 = _msg_buff[_msg_buff_data_start];
     329           1 :         const uint8_t hwid1 = _msg_buff[_msg_buff_data_start+1];
     330           7 :         for (uint8_t i=1; i<ARRAY_SIZE(hardware_lookup_table); i++) {
     331           6 :             if (hwid0 == hardware_lookup_table[i].hwid[0] && hwid1 == hardware_lookup_table[i].hwid[1]) {
     332           1 :                _hardware_model = (HardwareModel)i;
     333           1 :                GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Mount: Siyi %s", get_model_name());
     334             :             }
     335             :         }
     336           1 :         _got_hardware_id = true;
     337           1 :         break;
     338             :     }
     339             : 
     340           0 :     case SiyiCommandId::AUTO_FOCUS:
     341             : #if AP_MOUNT_SIYI_DEBUG
     342             :         if (_parsed_msg.data_bytes_received != 1) {
     343             :             unexpected_len = true;
     344             :             break;
     345             :         }
     346             :         debug("AutoFocus:%u", (unsigned)_msg_buff[_msg_buff_data_start]);
     347             : #endif
     348           0 :         break;
     349             : 
     350           0 :     case SiyiCommandId::MANUAL_ZOOM_AND_AUTO_FOCUS: {
     351           0 :         if (_parsed_msg.data_bytes_received != 2) {
     352             : #if AP_MOUNT_SIYI_DEBUG
     353             :             unexpected_len = true;
     354             : #endif
     355           0 :             break;
     356             :         }
     357           0 :         _zoom_mult = UINT16_VALUE(_msg_buff[_msg_buff_data_start+1], _msg_buff[_msg_buff_data_start]) * 0.1;
     358             :         debug("ZoomMult:%4.1f", (double)_zoom_mult);
     359           0 :         break;
     360             :     }
     361             : 
     362           0 :     case SiyiCommandId::MANUAL_FOCUS:
     363             : #if AP_MOUNT_SIYI_DEBUG
     364             :         if (_parsed_msg.data_bytes_received != 1) {
     365             :             unexpected_len = true;
     366             :             break;
     367             :         }
     368             :         debug("ManualFocus:%u", (unsigned)_msg_buff[_msg_buff_data_start]);
     369             : #endif
     370           0 :         break;
     371             : 
     372           0 :     case SiyiCommandId::GIMBAL_ROTATION:
     373             : #if AP_MOUNT_SIYI_DEBUG
     374             :         if (_parsed_msg.data_bytes_received != 1) {
     375             :             unexpected_len = true;
     376             :             break;
     377             :         }
     378             :         debug("GimbRot:%u", (unsigned)_msg_buff[_msg_buff_data_start]);
     379             : #endif
     380           0 :         break;
     381             : 
     382           0 :     case SiyiCommandId::CENTER:
     383             : #if AP_MOUNT_SIYI_DEBUG
     384             :         if (_parsed_msg.data_bytes_received != 1) {
     385             :             unexpected_len = true;
     386             :             break;
     387             :         }
     388             :         debug("Center:%u", (unsigned)_msg_buff[_msg_buff_data_start]);
     389             : #endif
     390           0 :         break;
     391             : 
     392           0 :     case SiyiCommandId::ACQUIRE_GIMBAL_CONFIG_INFO: {
     393           0 :         const RecordingStatus prev_record_status = _config_info.record_status;
     394             : 
     395             :         // Update Gimbal Config Information
     396           0 :         size_t config_sz = MIN(_parsed_msg.data_bytes_received, sizeof(_config_info));
     397           0 :         memcpy(&_config_info, &_msg_buff[_msg_buff_data_start], config_sz);
     398             : 
     399             :         // Alert user if recording status changed
     400           0 :         if (prev_record_status != _config_info.record_status) {
     401           0 :             const char * msg = "?";
     402           0 :             MAV_SEVERITY sev = MAV_SEVERITY_INFO;
     403           0 :             switch (_config_info.record_status) {
     404           0 :                 case RecordingStatus::OFF:
     405           0 :                     msg = "OFF";
     406           0 :                     break;
     407           0 :                 case RecordingStatus::ON:
     408           0 :                     msg = "ON";
     409           0 :                     break;
     410           0 :                 case RecordingStatus::NO_CARD:
     411           0 :                     msg = "NO CARD!";
     412           0 :                     sev = MAV_SEVERITY_WARNING;
     413           0 :                     break;
     414           0 :                 case RecordingStatus::DATA_LOSS:
     415           0 :                     msg = "DATA LOSS!";
     416           0 :                     sev = MAV_SEVERITY_WARNING;
     417           0 :                     break;
     418             :             }
     419             :             (void)msg;  // in case GCS_SEND_TEXT not available
     420             :             (void)sev;  // in case GCS_SEND_TEXT not available
     421           0 :             GCS_SEND_TEXT(sev, "Siyi: recording %s", msg);
     422             :         }
     423             : 
     424             :         debug(
     425             :             "GimConf hdr:%u rec:%u foll:%u mntdir:%u vid:%u",
     426             :             (uint8_t)_config_info.hdr_status,
     427             :             (uint8_t)_config_info.record_status,
     428             :             (uint8_t)_config_info.motion_mode,
     429             :             (uint8_t)_config_info.mounting_dir,
     430             :             (uint8_t)_config_info.video_mode
     431             :         );
     432           0 :         break;
     433             :     }
     434             : 
     435           0 :     case SiyiCommandId::FUNCTION_FEEDBACK_INFO: {
     436           0 :         if (_parsed_msg.data_bytes_received != 1) {
     437             : #if AP_MOUNT_SIYI_DEBUG
     438             :             unexpected_len = true;
     439             : #endif
     440           0 :             break;
     441             :         }
     442           0 :         const uint8_t func_feedback_info = _msg_buff[_msg_buff_data_start];
     443           0 :         const char* err_prefix = "Mount: Siyi";
     444             :         (void)err_prefix;  // in case !HAL_GCS_ENABLED
     445             :         switch ((FunctionFeedbackInfo)func_feedback_info) {
     446           0 :         case FunctionFeedbackInfo::SUCCESS:
     447             :             debug("FnFeedB success");
     448           0 :             break;
     449           0 :         case FunctionFeedbackInfo::FAILED_TO_TAKE_PHOTO:
     450           0 :             GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "%s failed to take picture", err_prefix);
     451           0 :             break;
     452           0 :         case FunctionFeedbackInfo::HDR_ON:
     453             :             debug("HDR on");
     454           0 :             break;
     455           0 :         case FunctionFeedbackInfo::HDR_OFF:
     456             :             debug("HDR off");
     457           0 :             break;
     458           0 :         case FunctionFeedbackInfo::FAILED_TO_RECORD_VIDEO:
     459           0 :             GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "%s failed to record video", err_prefix);
     460           0 :             break;
     461           0 :         default:
     462             :             debug("FnFeedB unexpected val:%u", (unsigned)func_feedback_info);
     463             :         }
     464           0 :         break;
     465             :     }
     466             : 
     467           0 :     case SiyiCommandId::PHOTO:
     468             :         // no ack should ever be sent by the gimbal
     469           0 :         break;
     470             : 
     471          43 :     case SiyiCommandId::ACQUIRE_GIMBAL_ATTITUDE: {
     472          43 :         if (_parsed_msg.data_bytes_received != 12) {
     473             : #if AP_MOUNT_SIYI_DEBUG
     474             :             unexpected_len = true;
     475             : #endif
     476           0 :             break;
     477             :         }
     478          43 :         _last_current_angle_rad_ms = AP_HAL::millis();
     479          43 :         _current_angle_rad.z = -radians((int16_t)UINT16_VALUE(_msg_buff[_msg_buff_data_start+1], _msg_buff[_msg_buff_data_start]) * 0.1);   // yaw angle
     480          43 :         _current_angle_rad.y = radians((int16_t)UINT16_VALUE(_msg_buff[_msg_buff_data_start+3], _msg_buff[_msg_buff_data_start+2]) * 0.1);  // pitch angle
     481          43 :         _current_angle_rad.x = radians((int16_t)UINT16_VALUE(_msg_buff[_msg_buff_data_start+5], _msg_buff[_msg_buff_data_start+4]) * 0.1);  // roll angle
     482          43 :         _current_rates_rads.z = -radians((int16_t)UINT16_VALUE(_msg_buff[_msg_buff_data_start+7], _msg_buff[_msg_buff_data_start+6]) * 0.1);   // yaw rate
     483          43 :         _current_rates_rads.y = radians((int16_t)UINT16_VALUE(_msg_buff[_msg_buff_data_start+9], _msg_buff[_msg_buff_data_start+8]) * 0.1);   // pitch rate
     484          43 :         _current_rates_rads.x = radians((int16_t)UINT16_VALUE(_msg_buff[_msg_buff_data_start+11], _msg_buff[_msg_buff_data_start+10]) * 0.1);  // roll rate
     485          43 :         break;
     486             :     }
     487             : 
     488             : #if AP_MOUNT_SEND_THERMAL_RANGE_ENABLED
     489           0 :     case SiyiCommandId::GET_TEMP_FULL_IMAGE: {
     490           0 :         if (_parsed_msg.data_bytes_received != 12) {
     491             : #if AP_MOUNT_SIYI_DEBUG
     492             :             unexpected_len = true;
     493             : #endif
     494           0 :             break;
     495             :         }
     496           0 :         _thermal.last_update_ms = AP_HAL::millis();
     497           0 :         _thermal.max_C = (int16_t)UINT16_VALUE(_msg_buff[_msg_buff_data_start+1], _msg_buff[_msg_buff_data_start]) * 0.01;
     498           0 :         _thermal.min_C = (int16_t)UINT16_VALUE(_msg_buff[_msg_buff_data_start+3], _msg_buff[_msg_buff_data_start+2]) * 0.01;
     499           0 :         _thermal.max_pos.x = UINT16_VALUE(_msg_buff[_msg_buff_data_start+5], _msg_buff[_msg_buff_data_start+4]);
     500           0 :         _thermal.max_pos.y = UINT16_VALUE(_msg_buff[_msg_buff_data_start+7], _msg_buff[_msg_buff_data_start+6]);
     501           0 :         _thermal.min_pos.x = UINT16_VALUE(_msg_buff[_msg_buff_data_start+9], _msg_buff[_msg_buff_data_start+8]);
     502           0 :         _thermal.min_pos.y = UINT16_VALUE(_msg_buff[_msg_buff_data_start+11], _msg_buff[_msg_buff_data_start+10]);
     503           0 :         break;
     504             :     }
     505             : #endif
     506             : 
     507          22 :     case SiyiCommandId::READ_RANGEFINDER: {
     508          22 :         _rangefinder_dist_m = UINT16_VALUE(_msg_buff[_msg_buff_data_start+1], _msg_buff[_msg_buff_data_start]);
     509          22 :         _last_rangefinder_dist_ms = AP_HAL::millis();
     510          22 :         break;
     511             :     }
     512             : 
     513           0 :     default:
     514             :         debug("Unhandled CmdId:%u", (unsigned)_parsed_msg.command_id);
     515           0 :         break;
     516             :     }
     517             : 
     518             : #if AP_MOUNT_SIYI_DEBUG
     519             :     // handle unexpected data buffer length
     520             :     if (unexpected_len) {
     521             :         debug("CmdId:%u unexpected len:%u", (unsigned)_parsed_msg.command_id, (unsigned)_parsed_msg.data_bytes_received);
     522             :     }
     523             : #endif
     524          67 : }
     525             : 
     526             : // methods to send commands to gimbal
     527             : // returns true on success, false if outgoing serial buffer is full
     528         220 : bool AP_Mount_Siyi::send_packet(SiyiCommandId cmd_id, const uint8_t* databuff, uint8_t databuff_len)
     529             : {
     530         220 :     if (!_initialised) {
     531           0 :         return false;
     532             :     }
     533             :     // calculate and sanity check packet size
     534         220 :     const uint16_t packet_size = AP_MOUNT_SIYI_PACKETLEN_MIN + databuff_len;
     535         220 :     if (packet_size > AP_MOUNT_SIYI_PACKETLEN_MAX) {
     536             :         debug("send_packet data buff too large");
     537           0 :         return false;
     538             :     }
     539             : 
     540             :     // check for sufficient space in outgoing buffer
     541         220 :     if (_uart->txspace() < packet_size) {
     542           0 :         return false;
     543             :     }
     544             : 
     545             :     // buffer for holding outgoing packet
     546         220 :     uint8_t send_buff[packet_size];
     547         220 :     uint8_t send_buff_ofs = 0;
     548             : 
     549             :     // packet header
     550         220 :     send_buff[send_buff_ofs++] = AP_MOUNT_SIYI_HEADER1;
     551         220 :     send_buff[send_buff_ofs++] = AP_MOUNT_SIYI_HEADER2;
     552             : 
     553             :     // CTRL.  Always request ACK
     554         220 :     send_buff[send_buff_ofs++] = 1;
     555             : 
     556             :     // Data_len.  protocol supports uint16_t but messages are never longer than 22 bytes
     557         220 :     send_buff[send_buff_ofs++] = databuff_len;
     558         220 :     send_buff[send_buff_ofs++] = 0;
     559             : 
     560             :     // SEQ (sequence)
     561         220 :     send_buff[send_buff_ofs++] = LOWBYTE(_last_seq);
     562         220 :     send_buff[send_buff_ofs++] = HIGHBYTE(_last_seq++);
     563             : 
     564             :     // CMD_ID
     565         220 :     send_buff[send_buff_ofs++] = (uint8_t)cmd_id;
     566             : 
     567             :     // DATA
     568         220 :     if (databuff_len != 0) {
     569         151 :         memcpy(&send_buff[send_buff_ofs], databuff, databuff_len);
     570         151 :         send_buff_ofs += databuff_len;
     571             :     }
     572             : 
     573             :     // CRC16
     574         220 :     const uint16_t crc = crc16_ccitt(send_buff, send_buff_ofs, 0);
     575         220 :     send_buff[send_buff_ofs++] = LOWBYTE(crc);
     576         220 :     send_buff[send_buff_ofs++] = HIGHBYTE(crc);
     577             : 
     578             :     // send packet
     579         220 :     _uart->write(send_buff, send_buff_ofs);
     580             : 
     581         220 :     return true;
     582         220 : }
     583             : 
     584             : // send a packet with a single data byte to gimbal
     585             : // returns true on success, false if outgoing serial buffer is full
     586           2 : bool AP_Mount_Siyi::send_1byte_packet(SiyiCommandId cmd_id, uint8_t data_byte)
     587             : {
     588           2 :     return send_packet(cmd_id, &data_byte, 1);
     589             : }
     590             : 
     591             : // rotate gimbal.  pitch_rate and yaw_rate are scalars in the range -100 ~ +100
     592             : // yaw_is_ef should be true if gimbal should maintain an earth-frame target (aka lock)
     593         127 : void AP_Mount_Siyi::rotate_gimbal(int8_t pitch_scalar, int8_t yaw_scalar, bool yaw_is_ef)
     594             : {
     595             :     // send lock/follow value
     596         127 :     const GimbalMotionMode mode = yaw_is_ef ? GimbalMotionMode::LOCK : GimbalMotionMode::FOLLOW;
     597         127 :     if (!set_motion_mode(mode)) {
     598             :         // couldn't set mode, so don't send rotation
     599           0 :         return;
     600             :     }
     601             : 
     602         127 :     const uint8_t yaw_and_pitch_rates[] {(uint8_t)yaw_scalar, (uint8_t)pitch_scalar};
     603         127 :     send_packet(SiyiCommandId::GIMBAL_ROTATION, yaw_and_pitch_rates, ARRAY_SIZE(yaw_and_pitch_rates));
     604             : }
     605             : 
     606             : // Set gimbal's motion mode if it has changed. Use force=true to always send.
     607             : //   FOLLOW: roll and pitch are in earth-frame, yaw is in body-frame
     608             : //   LOCK: roll, pitch and yaw are all in earth-frame
     609             : //   FPV: roll, pitch and yaw are all in body-frame
     610             : // Returns true if mode successfully sent to Gimbal
     611         127 : bool AP_Mount_Siyi::set_motion_mode(const GimbalMotionMode mode, const bool force)
     612             : {
     613         127 :     if (!force && (mode == _config_info.motion_mode)) {
     614             :         // we're already in the right mode...
     615         126 :         return true;
     616             :     }
     617             : 
     618           1 :     PhotoFunction data = PhotoFunction::LOCK_MODE;
     619           1 :     switch (mode) {
     620           0 :         case GimbalMotionMode::LOCK:   data = PhotoFunction::LOCK_MODE; break;
     621           1 :         case GimbalMotionMode::FOLLOW: data = PhotoFunction::FOLLOW_MODE; break;
     622           0 :         case GimbalMotionMode::FPV:    data = PhotoFunction::FPV_MODE; break;
     623             :     }
     624           1 :     bool sent = send_1byte_packet(SiyiCommandId::PHOTO, (uint8_t)data);
     625           1 :     if (sent) {
     626             :         // assume the mode is set correctly until told otherwise
     627           1 :         _config_info.motion_mode = mode;
     628           1 :         request_configuration();
     629             :     }
     630           1 :     return sent;
     631             : }
     632             : 
     633             : // send target pitch and yaw rates to gimbal
     634           0 : void AP_Mount_Siyi::send_target_rates(const MountRateTarget &rate_rads)
     635             : {
     636           0 :     const float pitch_rads = rate_rads.pitch;
     637           0 :     const float yaw_rads = rate_rads.yaw;
     638           0 :     const bool yaw_is_ef = rate_rads.yaw_is_ef;
     639             : 
     640           0 :     const float pitch_rate_scalar = constrain_float(100.0 * pitch_rads / AP_MOUNT_SIYI_RATE_MAX_RADS, -100, 100);
     641           0 :     const float yaw_rate_scalar = constrain_float(100.0 * yaw_rads / AP_MOUNT_SIYI_RATE_MAX_RADS, -100, 100);
     642           0 :     rotate_gimbal(pitch_rate_scalar, yaw_rate_scalar, yaw_is_ef);
     643           0 : }
     644             : 
     645             : // send target pitch and yaw angles to gimbal
     646         127 : void AP_Mount_Siyi::send_target_angles(const MountAngleTarget &angle_rad)
     647             : {
     648         127 :     const float pitch_rad = angle_rad.pitch;
     649         127 :     const float yaw_rad = angle_rad.yaw;
     650         127 :     bool yaw_is_ef = angle_rad.yaw_is_ef;
     651             : 
     652             :     // stop gimbal if no recent actual angles
     653         127 :     uint32_t now_ms = AP_HAL::millis();
     654         127 :     if (now_ms - _last_current_angle_rad_ms >= 200) {
     655           1 :         rotate_gimbal(0, 0, false);
     656           1 :         return;
     657             :     }
     658             : 
     659             :     // if gimbal mounting direction is 2 i.e. upside down, then transform the angles
     660         126 :     Vector3f current_angle_transformed = _current_angle_rad;
     661         126 :     if (_config_info.mounting_dir == GimbalMountingDirection::UPSIDE_DOWN) {
     662           0 :         current_angle_transformed.y = -wrap_PI(_current_angle_rad.y + M_PI);
     663           0 :         current_angle_transformed.z = -_current_angle_rad.z;
     664             :     }
     665             : 
     666             :     // use simple P controller to convert pitch angle error (in radians) to a target rate scalar (-100 to +100)
     667         126 :     const float pitch_err_rad = (pitch_rad - current_angle_transformed.y);
     668         126 :     const float pitch_rate_scalar = constrain_float(100.0 * pitch_err_rad * AP_MOUNT_SIYI_PITCH_P / AP_MOUNT_SIYI_RATE_MAX_RADS, -100, 100);
     669             : 
     670             :     // convert yaw angle to body-frame
     671         126 :     float yaw_bf_rad = yaw_is_ef ? wrap_PI(yaw_rad - AP::ahrs().get_yaw_rad()) : yaw_rad;
     672             : 
     673             :     // enforce body-frame yaw angle limits.  If beyond limits always use body-frame control
     674         126 :     const float yaw_bf_min = radians(_params.yaw_angle_min);
     675         126 :     const float yaw_bf_max = radians(_params.yaw_angle_max);
     676         126 :     if (yaw_bf_rad < yaw_bf_min || yaw_bf_rad > yaw_bf_max) {
     677           0 :         yaw_bf_rad = constrain_float(yaw_bf_rad, yaw_bf_min, yaw_bf_max);
     678           0 :         yaw_is_ef = false;
     679             :     }
     680             : 
     681             :     // use simple P controller to convert yaw angle error to a target rate scalar (-100 to +100)
     682         126 :     const float yaw_err_rad = (yaw_bf_rad - current_angle_transformed.z);
     683         126 :     const float yaw_rate_scalar = constrain_float(100.0 * yaw_err_rad * AP_MOUNT_SIYI_YAW_P / AP_MOUNT_SIYI_RATE_MAX_RADS, -100, 100);
     684             : 
     685             :     // rotate gimbal.  pitch_rate and yaw_rate are scalars in the range -100 ~ +100
     686         126 :     rotate_gimbal(pitch_rate_scalar, yaw_rate_scalar, yaw_is_ef);
     687             : }
     688             : 
     689             : // take a picture.  returns true on success
     690           0 : bool AP_Mount_Siyi::take_picture()
     691             : {
     692           0 :     return send_1byte_packet(SiyiCommandId::PHOTO, (uint8_t)PhotoFunction::TAKE_PICTURE);
     693             : }
     694             : 
     695             : // start or stop video recording.  returns true on success
     696             : // set start_recording = true to start record, false to stop recording
     697           0 : bool AP_Mount_Siyi::record_video(bool start_recording)
     698             : {
     699             :     // exit immediately if not initialised to reduce mismatch
     700             :     // between internal and actual state of recording
     701           0 :     if (!_initialised) {
     702           0 :         return false;
     703             :     }
     704             : 
     705           0 :     bool success = true;
     706           0 :     bool send_toggle = false;
     707           0 :     if (start_recording) {
     708           0 :         switch (_config_info.record_status) {
     709           0 :             case RecordingStatus::ON:
     710             :                 // already recording...
     711           0 :                 break;
     712             :             // assume that DATA_LOSS is the same as OFF
     713           0 :             case RecordingStatus::DATA_LOSS:
     714             :             case RecordingStatus::OFF:
     715           0 :                 send_toggle = true;
     716           0 :                 break;
     717           0 :             case RecordingStatus::NO_CARD:
     718           0 :                 GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "Siyi: can't start recording: No Card");
     719           0 :                 success = false;
     720           0 :                 break;
     721             :         }
     722             :     } else {
     723           0 :         switch (_config_info.record_status) {
     724           0 :             case RecordingStatus::ON:
     725           0 :                 send_toggle = true;
     726           0 :                 break;
     727             :             // assume that DATA_LOSS is the same as OFF
     728           0 :             case RecordingStatus::DATA_LOSS:
     729             :             case RecordingStatus::OFF:
     730             :             case RecordingStatus::NO_CARD:
     731             :                 // already off...
     732           0 :                 break;
     733             :         }
     734             :     }
     735             : 
     736           0 :     if (send_toggle) {
     737           0 :         success = send_1byte_packet(SiyiCommandId::PHOTO, (uint8_t)PhotoFunction::RECORD_VIDEO_TOGGLE);
     738             :     }
     739             : 
     740             :     // request recording state update from gimbal
     741           0 :     request_configuration();
     742             : 
     743           0 :     return success;
     744             : }
     745             : 
     746             : // send zoom rate command to camera. zoom out = -1, hold = 0, zoom in = 1
     747           0 : bool AP_Mount_Siyi::send_zoom_rate(float zoom_value)
     748             : {
     749           0 :     uint8_t zoom_step = 0;
     750           0 :     if (zoom_value > 0) {
     751             :         // zoom in
     752           0 :         zoom_step = 1;
     753             :     }
     754           0 :     if (zoom_value < 0) {
     755             :         // zoom out. Siyi API specifies -1 should be sent as 255
     756           0 :         zoom_step = UINT8_MAX;
     757             :     }
     758           0 :     return send_1byte_packet(SiyiCommandId::MANUAL_ZOOM_AND_AUTO_FOCUS, zoom_step);
     759             : }
     760             : 
     761             : // send zoom multiple command to camera. e.g. 1x, 10x, 30x
     762             : // only works on ZR10 and ZR30
     763           0 : bool AP_Mount_Siyi::send_zoom_mult(float zoom_mult)
     764             : {
     765             :     // separate zoom_mult into integral and fractional parts
     766             :     float intpart;
     767           0 :     uint8_t fracpart = (uint8_t)constrain_int16(modf(zoom_mult, &intpart) * 10, 0, UINT8_MAX);
     768             : 
     769             :     // create and send 2 byte array
     770           0 :     const uint8_t zoom_mult_data[] {(uint8_t)(intpart), fracpart};
     771           0 :     return send_packet(SiyiCommandId::ABSOLUTE_ZOOM, zoom_mult_data, ARRAY_SIZE(zoom_mult_data));
     772             : }
     773             : 
     774             : // get zoom multiple max
     775           0 : float AP_Mount_Siyi::get_zoom_mult_max() const
     776             : {
     777           0 :     switch (_hardware_model) {
     778           0 :     case HardwareModel::UNKNOWN:
     779           0 :         return 0;
     780           0 :     case HardwareModel::A2:
     781             :     case HardwareModel::A8:
     782             :     case HardwareModel::ZT6:
     783             :         // a8, zt6 have 6x digital zoom
     784           0 :         return 6;
     785           0 :     case HardwareModel::ZR10:
     786             :     case HardwareModel::ZR30:
     787             :     case HardwareModel::ZT30:
     788             :         // 30x hybrid zoom (optical + digital)
     789           0 :         return 30;
     790             :     }
     791           0 :     return 0;
     792             : }
     793             : 
     794             : // set zoom specified as a rate or percentage
     795           0 : bool AP_Mount_Siyi::set_zoom(ZoomType zoom_type, float zoom_value)
     796             : {
     797           0 :     switch (zoom_type) {
     798           0 :     case ZoomType::RATE:
     799           0 :         if (send_zoom_rate(zoom_value)) {
     800           0 :             _zoom_type = zoom_type;
     801           0 :             _zoom_rate_target = zoom_value;
     802           0 :             return true;
     803             :         }
     804           0 :         return false;
     805           0 :     case ZoomType::PCT: {
     806             :         // absolute zoom
     807           0 :         float zoom_mult_max = get_zoom_mult_max();
     808           0 :         if (is_positive(zoom_mult_max)) {
     809             :             // convert zoom percentage (0~100) to target zoom multiple (e.g. 0~6x or 0~30x)
     810           0 :             const float zoom_mult = linear_interpolate(1, zoom_mult_max, zoom_value, 0, 100);
     811           0 :             if (send_zoom_mult(zoom_mult)) {
     812           0 :                 _zoom_type = zoom_type;
     813           0 :                 return true;
     814             :             }
     815           0 :             return false;
     816             :         }
     817           0 :         return false;
     818             :     }
     819             :     }
     820             : 
     821             :     // unsupported zoom type
     822           0 :     return false;
     823             : }
     824             : 
     825             : // update zoom controller
     826         127 : void AP_Mount_Siyi::update_zoom_control()
     827             : {
     828         127 :     if (_zoom_type == ZoomType::RATE) {
     829             :         // limit updates to 1hz
     830           0 :         const uint32_t now_ms = AP_HAL::millis();
     831           0 :         if (now_ms - _last_zoom_control_ms < 1000) {
     832           0 :             return;
     833             :         }
     834           0 :         _last_zoom_control_ms = now_ms;
     835             : 
     836             :         // only send zoom rate target if it's non-zero because if zero it has already been sent
     837             :         // and sending zero rate also triggers autofocus
     838           0 :         if (!is_zero(_zoom_rate_target)) {
     839           0 :             send_zoom_rate(_zoom_rate_target);
     840             :         }
     841             :     }
     842             : }
     843             : 
     844             : // set focus specified as rate, percentage or auto
     845             : // focus in = -1, focus hold = 0, focus out = 1
     846           0 : SetFocusResult AP_Mount_Siyi::set_focus(FocusType focus_type, float focus_value)
     847             : {
     848           0 :     switch (focus_type) {
     849           0 :     case FocusType::RATE: {
     850           0 :         uint8_t focus_step = 0;
     851           0 :         if (focus_value > 0) {
     852           0 :             focus_step = 1;
     853           0 :         } else if (focus_value < 0) {
     854             :             // Siyi API specifies -1 should be sent as 255
     855           0 :             focus_step = UINT8_MAX;
     856             :         }
     857           0 :         if (!send_1byte_packet(SiyiCommandId::MANUAL_FOCUS, (uint8_t)focus_step)) {
     858           0 :             return SetFocusResult::FAILED;
     859             :         }
     860           0 :         return SetFocusResult::ACCEPTED;
     861             :     }
     862           0 :     case FocusType::PCT:
     863             :         // not supported
     864           0 :         return SetFocusResult::INVALID_PARAMETERS;
     865           0 :     case FocusType::AUTO:
     866           0 :         if (!send_1byte_packet(SiyiCommandId::AUTO_FOCUS, 1)) {
     867           0 :             return SetFocusResult::FAILED;
     868             :         }
     869           0 :         return SetFocusResult::ACCEPTED;
     870             :     }
     871             : 
     872             :     // unsupported focus type
     873           0 :     return SetFocusResult::INVALID_PARAMETERS;
     874             : }
     875             : 
     876             : // set camera lens as a value from 0 to 8
     877           0 : bool AP_Mount_Siyi::set_lens(uint8_t lens)
     878             : {
     879             :     CameraImageType selected_lens;
     880             : 
     881           0 :     switch (_hardware_model) {
     882             : 
     883           0 :     case HardwareModel::ZT30: {
     884             :         // maps lens to siyi camera image type so that lens of 0, 1, 2 are more useful
     885             :         static const CameraImageType cam_image_type_table[] {
     886             :             CameraImageType::MAIN_ZOOM_SUB_THERMAL,               // 3
     887             :             CameraImageType::MAIN_WIDEANGLE_SUB_THERMAL,          // 5
     888             :             CameraImageType::MAIN_THERMAL_SUB_ZOOM,               // 7
     889             :             CameraImageType::MAIN_PIP_ZOOM_THERMAL_SUB_WIDEANGLE, // 0
     890             :             CameraImageType::MAIN_PIP_WIDEANGLE_THERMAL_SUB_ZOOM, // 1
     891             :             CameraImageType::MAIN_PIP_ZOOM_WIDEANGLE_SUB_THERMAL, // 2
     892             :             CameraImageType::MAIN_ZOOM_SUB_WIDEANGLE,             // 4
     893             :             CameraImageType::MAIN_WIDEANGLE_SUB_ZOOM,             // 6
     894             :             CameraImageType::MAIN_THERMAL_SUB_WIDEANGLE,          // 8
     895             :         };
     896             : 
     897             :         // sanity check lens values
     898           0 :         if (lens >= ARRAY_SIZE(cam_image_type_table)) {
     899           0 :             return false;
     900             :         }
     901           0 :         selected_lens = cam_image_type_table[lens];
     902           0 :         break;
     903             :     }
     904             : 
     905           0 :     case HardwareModel::ZT6: {
     906             :         // maps lens to siyi camera image type so that lens of 0, 1, 2 are more useful
     907             :         static const CameraImageType cam_image_type_table[] {
     908             :             CameraImageType::MAIN_ZOOM_SUB_THERMAL,               // 3
     909             :             CameraImageType::MAIN_THERMAL_SUB_ZOOM,               // 7
     910             :             CameraImageType::MAIN_PIP_ZOOM_THERMAL_SUB_WIDEANGLE, // 0
     911             :         };
     912             : 
     913             :         // sanity check lens values
     914           0 :         if (lens >= ARRAY_SIZE(cam_image_type_table)) {
     915           0 :             return false;
     916             :         }
     917           0 :         selected_lens = cam_image_type_table[lens];
     918           0 :         break;
     919             :     }
     920             : 
     921           0 :     default:
     922             :         // set lens not supported on this camera
     923           0 :         return false;
     924             :     }
     925             : 
     926             :     // send desired image type to camera
     927           0 :     return send_1byte_packet(SiyiCommandId::SET_CAMERA_IMAGE_TYPE, (uint8_t)selected_lens);
     928             : }
     929             : 
     930             : // set_camera_source is functionally the same as set_lens except primary and secondary lenses are specified by type
     931             : // primary and secondary sources use the AP_Camera::CameraSource enum cast to uint8_t
     932           0 : bool AP_Mount_Siyi::set_camera_source(uint8_t primary_source, uint8_t secondary_source)
     933             : {
     934             :     // only supported on ZT30.  sanity check lens values
     935           0 :     if (_hardware_model != HardwareModel::ZT30) {
     936           0 :         return false;
     937             :     }
     938             : 
     939             :     // maps primary and secondary source to siyi camera image type
     940             :     CameraImageType cam_image_type;
     941           0 :     switch (primary_source) {
     942           0 :     case 0: // Default (RGB)
     943             :         FALLTHROUGH;
     944             :     case 1: // RGB
     945           0 :         switch (secondary_source) {
     946           0 :         case 0: // RGB + Default (None)
     947           0 :             cam_image_type = CameraImageType::MAIN_ZOOM_SUB_THERMAL;                // 3
     948           0 :             break;
     949           0 :         case 2: // PIP RGB+IR
     950           0 :             cam_image_type = CameraImageType::MAIN_PIP_ZOOM_THERMAL_SUB_WIDEANGLE;  // 0
     951           0 :             break;
     952           0 :         case 4: // PIP RGB+RGB_WIDEANGLE
     953           0 :             cam_image_type = CameraImageType::MAIN_PIP_ZOOM_WIDEANGLE_SUB_THERMAL;  // 2
     954           0 :             break;
     955           0 :         default:
     956           0 :             return false;
     957             :         }
     958           0 :         break;
     959           0 :     case 2: // IR
     960           0 :         switch (secondary_source) {
     961           0 :         case 0: // IR + Default (None)
     962           0 :             cam_image_type = CameraImageType::MAIN_THERMAL_SUB_ZOOM;                // 7
     963           0 :             break;
     964           0 :         default:
     965           0 :             return false;
     966             :         }
     967           0 :         break;
     968           0 :     case 4: // RGB_WIDEANGLE
     969           0 :         switch (secondary_source) {
     970           0 :         case 0: // RGB_WIDEANGLE + Default (None)
     971           0 :             cam_image_type = CameraImageType::MAIN_WIDEANGLE_SUB_THERMAL;           // 5
     972           0 :             break;
     973           0 :         case 2: // PIP RGB_WIDEANGLE+IR
     974           0 :             cam_image_type = CameraImageType::MAIN_PIP_WIDEANGLE_THERMAL_SUB_ZOOM;  // 1
     975           0 :             break;
     976           0 :         default:
     977           0 :             return false;
     978             :         }
     979           0 :         break;
     980           0 :     default:
     981           0 :         return false;
     982             :     }
     983             : 
     984             :     // send desired image type to camera
     985           0 :     return send_1byte_packet(SiyiCommandId::SET_CAMERA_IMAGE_TYPE, (uint8_t)cam_image_type);
     986             : }
     987             : 
     988             : // return focal length in mm for the current hardware model
     989           2 : float AP_Mount_Siyi::get_camera_focal_length_mm() const
     990             : {
     991             :     // To-Do: check these values are correct for A2, ZR30, ZT30
     992           2 :     switch (_hardware_model) {
     993           0 :     case HardwareModel::UNKNOWN:
     994             :     case HardwareModel::A2:
     995             :     case HardwareModel::A8:
     996             :     case HardwareModel::ZT6:
     997           0 :         return 21;
     998           2 :     case HardwareModel::ZR10:
     999             :     case HardwareModel::ZR30:
    1000             :     case HardwareModel::ZT30:
    1001             :         // focal length range from 5.15 ~ 47.38
    1002           2 :         return 5.15;
    1003             :     }
    1004           0 :     return 0;
    1005             : }
    1006             : 
    1007             : // send camera settings message to GCS
    1008           0 : void AP_Mount_Siyi::send_camera_settings(mavlink_channel_t chan) const
    1009             : {
    1010           0 :     const uint8_t mode_id = (_config_info.record_status == RecordingStatus::ON) ? CAMERA_MODE_VIDEO : CAMERA_MODE_IMAGE;
    1011           0 :     const float zoom_mult_max = get_zoom_mult_max();
    1012           0 :     float zoom_pct = 0.0;
    1013           0 :     if (is_positive(zoom_mult_max)) {
    1014           0 :         zoom_pct = linear_interpolate(0, 100, _zoom_mult, 1.0, zoom_mult_max);
    1015             :     }
    1016             : 
    1017             :     // send CAMERA_SETTINGS message
    1018           0 :     mavlink_msg_camera_settings_send(
    1019             :         chan,
    1020             :         AP_HAL::millis(),   // time_boot_ms
    1021             :         mode_id,            // camera mode (0:image, 1:video, 2:image survey)
    1022             :         zoom_pct,           // zoomLevel float, percentage from 0 to 100, NaN if unknown
    1023             :         NaNf);              // focusLevel float, percentage from 0 to 100, NaN if unknown
    1024           0 : }
    1025             : 
    1026             : #if AP_MOUNT_SEND_THERMAL_RANGE_ENABLED
    1027             : // send camera thermal range message to GCS
    1028           0 : void AP_Mount_Siyi::send_camera_thermal_range(mavlink_channel_t chan) const
    1029             : {
    1030           0 :     const uint32_t now_ms = AP_HAL::millis();
    1031           0 :     bool timeout = now_ms - _thermal.last_update_ms > AP_MOUNT_SIYI_THERM_TIMEOUT_MS;
    1032             : 
    1033             :     // send CAMERA_THERMAL_RANGE message
    1034           0 :     mavlink_msg_camera_thermal_range_send(
    1035             :         chan,
    1036             :         now_ms,             // time_boot_ms
    1037           0 :         _instance + 1,      // video stream id (assume one-to-one mapping with camera id)
    1038           0 :         _instance + 1,      // camera device id
    1039             :         timeout ? NaNf : _thermal.max_C,     // max in degC
    1040           0 :         timeout ? NaNf : _thermal.max_pos.x, // max x position
    1041           0 :         timeout ? NaNf : _thermal.max_pos.y, // max y position
    1042             :         timeout ? NaNf : _thermal.min_C,     // min in degC
    1043           0 :         timeout ? NaNf : _thermal.min_pos.x, // min x position
    1044           0 :         timeout ? NaNf : _thermal.min_pos.y);// min y position
    1045           0 : }
    1046             : #endif
    1047             : 
    1048             : // change camera settings not normally used by autopilot
    1049             : // THERMAL_PALETTE: 0:WhiteHot, 2:Sepia, 3:IronBow, 4:Rainbow, 5:Night, 6:Aurora, 7:RedHot, 8:Jungle, 9:Medical, 10:BlackHot, 11:GloryHot
    1050             : // THERMAL_GAIN: 0:Low gain (50C ~ 550C), 1:High gain (-20C ~ 150C)
    1051             : // THERMAL_RAW_DATA: 0:Disable Raw Data (30fps), 1:Enable Raw Data (25fps)
    1052           0 : bool AP_Mount_Siyi::change_setting(CameraSetting setting, float value)
    1053             : {
    1054           0 :     switch (setting) {
    1055           0 :     case CameraSetting::THERMAL_PALETTE:
    1056           0 :         return send_1byte_packet(SiyiCommandId::SET_THERMAL_PALETTE, (uint8_t)value);
    1057           0 :     case CameraSetting::THERMAL_GAIN:
    1058           0 :         return send_1byte_packet(SiyiCommandId::SET_THERMAL_GAIN, (uint8_t)value);
    1059           0 :     case CameraSetting::THERMAL_RAW_DATA:
    1060           0 :         return send_1byte_packet(SiyiCommandId::SET_THERMAL_RAW_DATA, (uint8_t)value);
    1061             :     }
    1062             : 
    1063             :     // invalid setting so return false
    1064           0 :     return false;
    1065             : }
    1066             : 
    1067             : // get model name string. returns "Unknown" if hardware model is not yet known
    1068           3 : const char* AP_Mount_Siyi::get_model_name() const
    1069             : {
    1070           3 :     uint8_t model_idx = (uint8_t)_hardware_model;
    1071           3 :     if (model_idx < ARRAY_SIZE(hardware_lookup_table)) {
    1072           3 :         return hardware_lookup_table[model_idx].model_name;
    1073             :     }
    1074           0 :     return hardware_lookup_table[0].model_name;
    1075             : }
    1076             : 
    1077             : // get rangefinder distance.  Returns true on success
    1078          25 : bool AP_Mount_Siyi::get_rangefinder_distance(float& distance_m) const
    1079             : {
    1080             :     // only supported on ZT30
    1081          25 :     if (_hardware_model != HardwareModel::ZT30) {
    1082           0 :         return false;
    1083             :     }
    1084             : 
    1085             :     // unhealthy if distance not received recently
    1086          25 :     const uint32_t now_ms = AP_HAL::millis();
    1087          25 :     if (now_ms - _last_rangefinder_dist_ms > AP_MOUNT_SIYI_TIMEOUT_MS) {
    1088           0 :         return false;
    1089             :     }
    1090             : 
    1091          25 :     distance_m = _rangefinder_dist_m;
    1092          25 :     return true;
    1093             : }
    1094             : 
    1095             : // Checks that the firmware version on the Gimbal meets the minimum supported version.
    1096           1 : void AP_Mount_Siyi::check_firmware_version() const
    1097             : {
    1098           1 :     if (!_fw_version.received) {
    1099             :         debug("Can't check firmware if we haven't received it...");
    1100           0 :         return;
    1101             :     }
    1102             : 
    1103           1 :     if (!_got_hardware_id) {
    1104             :         debug("Can't check firmware without Hardware ID!");
    1105           0 :         return;
    1106             :     }
    1107             : 
    1108           1 :     FirmwareVersion minimum_ver {};
    1109           1 :     switch (_hardware_model) {
    1110           0 :         case HardwareModel::A8:
    1111           0 :                 minimum_ver.camera = {.major = 0, .minor = 2, .patch = 1};
    1112           0 :             break;
    1113             : 
    1114           0 :         case HardwareModel::ZT6:
    1115           0 :             minimum_ver.camera = {.major = 0, .minor = 1, .patch = 9};
    1116           0 :             break;
    1117             : 
    1118           1 :         case HardwareModel::A2:
    1119             :         case HardwareModel::ZR10:
    1120             :         case HardwareModel::ZR30:
    1121             :         case HardwareModel::ZT30:
    1122             :             // TBD
    1123           1 :             break;
    1124             : 
    1125           0 :         case HardwareModel::UNKNOWN:
    1126             :             debug("Can't check FW on unknown hardware model!");
    1127           0 :             return;
    1128             :     }
    1129             : 
    1130           1 :     const uint32_t minimum_camera_val =  (minimum_ver.camera.major << 16) + (minimum_ver.camera.minor << 8) + minimum_ver.camera.patch;
    1131           1 :     const uint32_t firmware_camera_val = (_fw_version.camera.major << 16) + (_fw_version.camera.minor << 8) + _fw_version.camera.patch;
    1132             : 
    1133           1 :     const bool is_camera_supported = firmware_camera_val >= minimum_camera_val;
    1134             : 
    1135           1 :     if (!is_camera_supported) {
    1136           0 :         GCS_SEND_TEXT(
    1137             :             MAV_SEVERITY_WARNING,
    1138             :             "Mount: Siyi running old camera fw (need v%u.%u.%u)",
    1139             :             minimum_ver.camera.major, minimum_ver.camera.minor, minimum_ver.camera.patch
    1140             :         );
    1141             :     }
    1142             : }
    1143             : 
    1144             : #if AP_MOUNT_SEND_THERMAL_RANGE_ENABLED
    1145             : // get thermal min/max if available at 5hz
    1146         127 : void AP_Mount_Siyi::request_thermal_minmax()
    1147             : {
    1148             :     // only supported on ZT6 and ZT30
    1149         127 :     if (_hardware_model != HardwareModel::ZT6 &&
    1150         127 :         _hardware_model != HardwareModel::ZT30) {
    1151           0 :         return;
    1152             :     }
    1153             : 
    1154             :     // check for timeout
    1155         127 :     uint32_t now_ms = AP_HAL::millis();
    1156         127 :     if ((now_ms - _thermal.last_update_ms > AP_MOUNT_SIYI_THERM_TIMEOUT_MS) &&
    1157         100 :         (now_ms - _thermal.last_req_ms > AP_MOUNT_SIYI_THERM_TIMEOUT_MS)) {
    1158             :         // request thermal min/max at 5hz
    1159           1 :         send_1byte_packet(SiyiCommandId::GET_TEMP_FULL_IMAGE, 2);
    1160           1 :         _thermal.last_req_ms = now_ms;
    1161             :     }
    1162             : }
    1163             : #endif
    1164             : 
    1165             : /*
    1166             :   send ArduPilot attitude and position to gimbal
    1167             : */
    1168          22 : void AP_Mount_Siyi::send_attitude_position(void)
    1169             : {
    1170          22 :     const auto &ahrs = AP::ahrs();
    1171             :     struct {
    1172             :         uint32_t time_boot_ms;
    1173             :         float roll, pitch, yaw;
    1174             :         float rollspeed, pitchspeed, yawspeed;
    1175             :     } attitude;
    1176             : 
    1177             :     // get attitude as euler 321
    1178          22 :     const auto &gyro = ahrs.get_gyro();
    1179          22 :     const uint32_t now_ms = AP_HAL::millis();
    1180             : 
    1181          22 :     attitude.time_boot_ms = now_ms;
    1182          22 :     attitude.roll = ahrs.get_roll_rad();
    1183          22 :     attitude.pitch = ahrs.get_pitch_rad();
    1184          22 :     attitude.yaw = ahrs.get_yaw_rad();
    1185          22 :     attitude.rollspeed = gyro.x;
    1186          22 :     attitude.pitchspeed = gyro.y;
    1187          22 :     attitude.yawspeed = gyro.z;
    1188             : 
    1189          22 :     send_packet(SiyiCommandId::EXTERNAL_ATTITUDE, (const uint8_t *)&attitude, sizeof(attitude));
    1190             : 
    1191             :     // send location and velocity
    1192             :     struct {
    1193             :         uint32_t time_boot_ms;
    1194             :         int32_t lat, lon;
    1195             :         int32_t alt_msl, alt_ellipsoid;
    1196             :         Vector3l velocity_ned_int32;
    1197          22 :     } position;
    1198          22 :     Location loc;
    1199          22 :     Vector3f velocity_ned;
    1200          22 :     float undulation = 0;
    1201          22 :     if (!ahrs.get_location(loc) ||
    1202           0 :         !ahrs.get_velocity_NED(velocity_ned)) {
    1203          22 :         return;
    1204             :     }
    1205           0 :     AP::gps().get_undulation(undulation);
    1206             : 
    1207           0 :     position.time_boot_ms = now_ms;
    1208           0 :     position.lat = loc.lat;
    1209           0 :     position.lon = loc.lng;
    1210           0 :     position.alt_msl = loc.alt;
    1211           0 :     position.alt_ellipsoid = position.alt_msl - undulation*100;
    1212             : 
    1213             :     // convert velocity to int32 and scale to mm/s
    1214           0 :     position.velocity_ned_int32.x = velocity_ned.x * 1000;
    1215           0 :     position.velocity_ned_int32.y = velocity_ned.y * 1000;
    1216           0 :     position.velocity_ned_int32.z = velocity_ned.z * 1000;
    1217             : 
    1218           0 :     send_packet(SiyiCommandId::POSITION_DATA, (const uint8_t *)&position, sizeof(position));
    1219             : }
    1220             : 
    1221             : #endif // HAL_MOUNT_SIYI_ENABLED

Generated by: LCOV version 1.14