LCOV - code coverage report
Current view: top level - AP_Mount - AP_Mount_Topotek.cpp (source / functions) Hit Total Coverage
Test: lcov.info Lines: 253 508 49.8 %
Date: 2026-09-09 11:02:08 Functions: 23 36 63.9 %

          Line data    Source code
       1             : #include "AP_Mount_config.h"
       2             : 
       3             : #if HAL_MOUNT_TOPOTEK_ENABLED
       4             : 
       5             : #include "AP_Mount_Topotek.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_RTC/AP_RTC.h>
      11             : 
      12             : extern const AP_HAL::HAL& hal;
      13             : 
      14             : #define ANGULAR_VELOCITY_CONVERSION         1.220740379         // gimbal angular velocity conversion ratio
      15             : #define TRACK_TOTAL_WIDTH                   1920                // track the maximum width of the image range
      16             : #define TRACK_TOTAL_HEIGHT                  1080                // track the maximum height of the image range
      17             : #define TRACK_RANGE                         60                  // the size of the image at point tracking
      18             : #define AP_MOUNT_TOPOTEK_UPDATE_INTERVAL_MS 100                 // resend angle or rate targets to gimbal at this interval
      19             : #define AP_MOUNT_TOPOTEK_HEALTH_TIMEOUT_MS  1000                // timeout for health and rangefinder readings
      20             : #define AP_MOUNT_TOPOTEK_PACKETLEN_MIN      12                  // packet length not including the data segment
      21             : #define AP_MOUNT_TOPOTEK_DATALEN_MAX        (AP_MOUNT_TOPOTEK_PACKETLEN_MAX - AP_MOUNT_TOPOTEK_PACKETLEN_MIN) // data segment lens can be no more tha this
      22             : 
      23             : // 3 character identifiers
      24             : # define AP_MOUNT_TOPOTEK_ID3CHAR_CAPTURE       "CAP"           // take picture, data bytes: 01:RGB + thermal, 02:RGB, 03:thermal, 05:RGB + thermal (with temp measurement)
      25             : # define AP_MOUNT_TOPOTEK_ID3CHAR_RECORD_VIDEO  "REC"           // record video, data bytes: 00:stop, 01:start, 0A:toggle start/stop
      26             : # define AP_MOUNT_TOPOTEK_ID3CHAR_CONTROL_ZOOM  "ZMC"           // control zoom, data bytes: 00:stop, 01:zoom out, 02:zoom in
      27             : # define AP_MOUNT_TOPOTEK_ID3CHAR_GET_ZOOM      "ZOM"           // get zoom, no data bytes
      28             : # define AP_MOUNT_TOPOTEK_ID3CHAR_CONTROL_FOCUS "FCC"           // control focus, data bytes: 00:stop, 01:focus+, 02:focus-, 0x10:auto focus, 0x11:manual focus, 0x12:manu focus (save), 0x13:auto focus (save)
      29             : # define AP_MOUNT_TOPOTEK_ID3CHAR_GET_FOCUS     "FOC"           // get focus, no data bytes
      30             : # define AP_MOUNT_TOPOTEK_ID3CHAR_SET_ZOOM_AND_FOCU "ZFP"       // set zoom and focus
      31             : # define AP_MOUNT_TOPOTEK_ID3CHAR_DAY_NIGHT_SWITCHING "IRC"     // set day/night setting, data bytes: 00:day, 01:night, 0A:toggle state
      32             : # define AP_MOUNT_TOPOTEK_ID3CHAR_TRACKING      "TRC"           // get/set image tracking, data bytes: 00:get status (use with "r"), 01:stop (use with "w")
      33             : # define AP_MOUNT_TOPOTEK_ID3CHAR_START_TRACKING "LOC"          // start image tracking
      34             : # define AP_MOUNT_TOPOTEK_ID3CHAR_LRF           "LRF"           // laser rangefinder control, data bytes: 00:ranging stop, 01:ranging start, 02:single measurement, 03:continuous measurement
      35             : # define AP_MOUNT_TOPOTEK_ID3CHAR_PIP           "PIP"           // set picture-in-picture setting, data bytes: // 00:main only, 01:main+sub, 02:sub+main, 03:sub only, 0A:next
      36             : # define AP_MOUNT_TOPOTEK_ID3CHAR_GIMBAL_ATT    "GIA"           // get gimbal attitude, data bytes: 00:stop, 01:start
      37             : # define AP_MOUNT_TOPOTEK_ID3CHAR_SD_CARD       "SDC"           // get SD card state, data bytes: 00:get remaining capacity, 01:get total capacity
      38             : # define AP_MOUNT_TOPOTEK_ID3CHAR_TIME          "UTC"           // set time and date, data bytes: HHMMSSDDMMYY
      39             : # define AP_MOUNT_TOPOTEK_ID3CHAR_GET_VERSION   "VSN"           // get firmware version, data bytes always 00
      40             : # define AP_MOUNT_TOPOTEK_ID3CHAR_GET_MODEL_NAME "PA2"          // get model name, data bytes always 00
      41             : # define AP_MOUNT_TOPOTEK_ID3CHAR_GIMBAL_MODE   "PTZ"           // set gimbal mode, data bytes: 00:stop, 01:up, 02:down, 03:left, 04:right, 05:home position, 06:lock, 07:follow, 08:lock/follow toggle, 09:calibration, 0A:one button down
      42             : # define AP_MOUNT_TOPOTEK_ID3CHAR_YPR_RATE      "YPR"           // set the rate yaw, pitch and roll targets of the gimbal yaw in range -99 ~ +99
      43             : # define AP_MOUNT_TOPOTEK_ID3CHAR_YAW_ANGLE     "GIY"           // set the yaw angle target in the range -150 ~ 150, speed 0 ~ 99 (0.1deg/sec)
      44             : # define AP_MOUNT_TOPOTEK_ID3CHAR_YAW_ANGLE_BF  "GAY"           // set the yaw angle target in body-frame in the range -150 ~ 150, speed 0 ~ 99 (0.1deg/sec)
      45             : # define AP_MOUNT_TOPOTEK_ID3CHAR_PITCH_ANGLE   "GIP"           // set the pitch angle target in the range -90 ~ 90, speed 0 ~ 99 (0.1deg/sec)
      46             : # define AP_MOUNT_TOPOTEK_ID3CHAR_ROLL_ANGLE    "GIR"           // set the roll angle target in the range -90 ~ 90, speed 0 ~ 99 (0.1deg/sec)
      47             : # define AP_MOUNT_TOPOTEK_ID3CHAR_SET_LAT       "LAT"           // set the gimbal's latitude
      48             : # define AP_MOUNT_TOPOTEK_ID3CHAR_SET_LON       "LON"           // set the gimbal's longitude
      49             : # define AP_MOUNT_TOPOTEK_ID3CHAR_SET_ALT       "ALT"           // set the gimbal's altitude
      50             : # define AP_MOUNT_TOPOTEK_ID3CHAR_SET_AZIMUTH   "AZI"           // set the gimbal's yaw (aka azimuth)
      51             : 
      52             : #define AP_MOUNT_TOPOTEK_DEBUG 0
      53             : #define debug(fmt, args ...) do { if (AP_MOUNT_TOPOTEK_DEBUG) { GCS_SEND_TEXT(MAV_SEVERITY_INFO, "Topotek: " fmt, ## args); } } while (0)
      54             : 
      55             : const char* AP_Mount_Topotek::send_message_prefix = "Mount: Topotek";
      56             : 
      57             : // update mount position - should be called periodically
      58       10408 : void AP_Mount_Topotek::update()
      59             : {
      60       10408 :     AP_Mount_Backend::update();
      61             : 
      62             :     // exit immediately if not initialised
      63       10408 :     if (!_initialised) {
      64           0 :         return;
      65             :     }
      66             : 
      67             :     // reading incoming packets from gimbal
      68       10408 :     read_incoming_packets();
      69             : 
      70             :     // everything below updates at 10hz
      71       10408 :     uint32_t now_ms = AP_HAL::millis();
      72       10408 :     if ((now_ms - _last_req_current_info_ms) < 100) {
      73        8326 :         return;
      74             :     }
      75        2082 :     _last_req_current_info_ms = now_ms;
      76             : 
      77             :     // re-send the stop zoom command a second time to prevent data transmission errors.
      78        2082 :     if (_last_zoom_stop) {
      79           0 :         _last_zoom_stop = false;
      80           0 :         send_fixedlen_packet(AddressByte::LENS, AP_MOUNT_TOPOTEK_ID3CHAR_CONTROL_ZOOM, true, 0);
      81             :     }
      82             : 
      83             :     // re-send the stop focus command a second time to prevent data transmission errors.
      84        2082 :     if (_last_focus_stop) {
      85           0 :         _last_focus_stop = false;
      86           0 :         send_fixedlen_packet(AddressByte::LENS, AP_MOUNT_TOPOTEK_ID3CHAR_CONTROL_FOCUS, true, 0);
      87             :     }
      88             : 
      89             :     // send GPS-related information to the gimbal
      90        2082 :     send_location_info();
      91             : 
      92             :     // calls below here called at 1hz
      93        2082 :     _last_req_step++;
      94        2082 :     if (_last_req_step >= 10) {
      95         207 :         _last_req_step = 0;
      96             :     }
      97        2082 :     switch (_last_req_step) {
      98         207 :     case 0:
      99             :         // get gimbal version
     100         207 :         if (!_got_gimbal_version) {
     101           2 :             request_gimbal_version();
     102             :         }
     103         207 :         break;
     104         209 :     case 2:
     105             :         // request gimbal attitude at 1hz
     106             :         // gimbal will continue to send attitude information during the next period
     107         209 :         request_gimbal_attitude();
     108         209 :         break;
     109         209 :     case 4:
     110             :         // request memory card information
     111         209 :         request_gimbal_sdcard_info();
     112         209 :         break;
     113         209 :     case 6:
     114             :         // request tracking info
     115         209 :         request_track_status();
     116         209 :         break;
     117         207 :     case 8:
     118             :         // get gimbal model name
     119         207 :         if (!_got_gimbal_model_name) {
     120           2 :             request_gimbal_model_name();
     121             :         }
     122         207 :         break;
     123             :     }
     124             : 
     125             :     // handle tracking state
     126        2082 :     if (_is_tracking) {
     127             :         // cancel tracking if mode has changed
     128           0 :         if (_last_mode != _mode) {
     129           0 :             _last_mode = _mode;
     130           0 :             cancel_tracking();
     131             :         }
     132           0 :         return;
     133             :     }
     134        2082 :     _last_mode = _mode;
     135             : 
     136             :     // update based on mount mode
     137        2082 :     update_mnt_target();
     138             : 
     139             :     // send target angles or rates depending on the target type
     140        2082 :     send_target_to_gimbal();
     141             : }
     142             : 
     143             : // return true if healthy
     144        2134 : bool AP_Mount_Topotek::healthy() const
     145             : {
     146             :     // exit immediately if not initialised
     147        2134 :     if (!_initialised) {
     148           0 :         return false;
     149             :     }
     150             : 
     151             :     // unhealthy if attitude information not received recently
     152        2134 :     const uint32_t last_current_angle_ms = _last_current_angle_ms;
     153        2134 :     return (AP_HAL::millis() - last_current_angle_ms < AP_MOUNT_TOPOTEK_HEALTH_TIMEOUT_MS);
     154             : }
     155             : 
     156             : // take a picture.  returns true on success
     157           0 : bool AP_Mount_Topotek::take_picture()
     158             : {
     159             :     // exit immediately if not initialised
     160           0 :     if (!_initialised) {
     161           0 :         return false;
     162             :     }
     163             : 
     164             :     // exit immediately if the memory card is abnormal
     165           0 :     if (!_sdcard_status) {
     166           0 :         GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "%s SD card error", send_message_prefix);
     167           0 :         return false;
     168             :     }
     169             : 
     170             :     // sample command: #TPUD2wCAP01
     171           0 :     return send_fixedlen_packet(AddressByte::SYSTEM_AND_IMAGE, AP_MOUNT_TOPOTEK_ID3CHAR_CAPTURE, true, 1);
     172             : }
     173             : 
     174             : // start or stop video recording.  returns true on success
     175             : // set start_recording = true to start record, false to stop recording
     176           0 : bool AP_Mount_Topotek::record_video(bool start_recording)
     177             : {
     178             :     // exit immediately if not initialised
     179           0 :     if (!_initialised) {
     180           0 :         return false;
     181             :     }
     182             : 
     183             :     // exit immediately if the memory card is abnormal
     184           0 :     if (!_sdcard_status) {
     185           0 :         GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "%s SD card error", send_message_prefix);
     186           0 :         return false;
     187             :     }
     188             : 
     189             :     // sample command: #TPUD2wREC01
     190           0 :     return send_fixedlen_packet(AddressByte::SYSTEM_AND_IMAGE, AP_MOUNT_TOPOTEK_ID3CHAR_RECORD_VIDEO, true, start_recording ? 1 : 0);
     191             : }
     192             : 
     193             : // set zoom specified as a rate
     194           0 : bool AP_Mount_Topotek::set_zoom(ZoomType zoom_type, float zoom_value)
     195             : {
     196             :     // exit immediately if not initialised
     197           0 :     if (!_initialised) {
     198           0 :         return false;
     199             :     }
     200             : 
     201             :     // zoom rate
     202           0 :     if (zoom_type == ZoomType::RATE) {
     203             :         uint8_t zoom_cmd;
     204           0 :         if (is_zero(zoom_value)) {
     205             :             // stop zoom
     206           0 :             zoom_cmd = 0;
     207           0 :             _last_zoom_stop = true;
     208           0 :         } else if (zoom_value < 0) {
     209             :             // zoom out
     210           0 :             zoom_cmd = 1;
     211             :         } else {
     212             :             // zoom in
     213           0 :             zoom_cmd = 2;
     214             :         }
     215             :         // sample command: #TPUM2wZMC00
     216           0 :         return send_fixedlen_packet(AddressByte::LENS, AP_MOUNT_TOPOTEK_ID3CHAR_CONTROL_ZOOM, true, zoom_cmd);
     217             :     }
     218             : 
     219             :     // unsupported zoom type
     220           0 :     return false;
     221             : }
     222             : 
     223             : // set focus specified as rate, percentage or auto
     224             : // focus in = -1, focus hold = 0, focus out = 1
     225           0 : SetFocusResult AP_Mount_Topotek::set_focus(FocusType focus_type, float focus_value)
     226             : {
     227             :     // exit immediately if not initialised
     228           0 :     if (!_initialised) {
     229           0 :         return SetFocusResult::FAILED;
     230             :     }
     231             : 
     232           0 :     switch (focus_type) {
     233           0 :     case FocusType::RATE: {
     234             :         // focus stop
     235             :         uint8_t focus_cmd;
     236           0 :         if (is_zero(focus_value)) {
     237           0 :             focus_cmd = 0;
     238           0 :             _last_focus_stop = true;
     239           0 :         } else if (focus_value < 0) {
     240             :             // focus-
     241           0 :             focus_cmd = 2;
     242             :         } else {
     243             :             // focus+
     244           0 :             focus_cmd = 1;
     245             :         }
     246             :         // send focus command and switch to manual focus
     247             :         // sample command: #TPUM2wFCC00
     248           0 :         if (send_fixedlen_packet(AddressByte::LENS, AP_MOUNT_TOPOTEK_ID3CHAR_CONTROL_FOCUS, true, focus_cmd) &&
     249           0 :             send_fixedlen_packet(AddressByte::LENS, AP_MOUNT_TOPOTEK_ID3CHAR_CONTROL_FOCUS, true, 0x11)) {
     250           0 :                 return SetFocusResult::ACCEPTED;
     251             :         }
     252           0 :         return SetFocusResult::FAILED;
     253             :     }
     254           0 :     case FocusType::PCT:
     255             :         // not supported
     256           0 :         return SetFocusResult::INVALID_PARAMETERS;
     257           0 :     case FocusType::AUTO:
     258             :         // auto focus
     259           0 :         if (send_fixedlen_packet(AddressByte::LENS, AP_MOUNT_TOPOTEK_ID3CHAR_CONTROL_FOCUS, true, 0x10)) {
     260           0 :             return SetFocusResult::ACCEPTED;
     261             :         }
     262           0 :         return SetFocusResult::FAILED;
     263             :     }
     264             : 
     265             :     // unsupported focus type
     266           0 :     return SetFocusResult::INVALID_PARAMETERS;
     267             : }
     268             : 
     269             : // set tracking to none, point or rectangle (see TrackingType enum)
     270             : // if POINT only p1 is used, if RECTANGLE then p1 is top-left, p2 is bottom-right
     271             : // p1,p2 are in range 0 to 1.  0 is left or top, 1 is right or bottom
     272           0 : bool AP_Mount_Topotek::set_tracking(TrackingType tracking_type, const Vector2f& p1, const Vector2f& p2)
     273             : {
     274             :     // exit immediately if not initialised
     275           0 :     if (!_initialised) {
     276           0 :         return false;
     277             :     }
     278             : 
     279             :     // local variables holding tracker center and width
     280             :     int16_t track_center_x, track_center_y, track_width, track_height;
     281           0 :     bool send_tracking_cmd = false;
     282             : 
     283           0 :     switch (tracking_type) {
     284             : 
     285           0 :     case TrackingType::TRK_NONE:
     286           0 :         return cancel_tracking();
     287             : 
     288           0 :     case TrackingType::TRK_POINT: {
     289             :         // calculate tracking center, width and height
     290           0 :         track_center_x = (int16_t)((p1.x*TRACK_TOTAL_WIDTH - 960) /  0.96);
     291           0 :         track_center_y = (int16_t)((p1.y*TRACK_TOTAL_HEIGHT - 540) /  0.54);
     292           0 :         track_width = (int16_t)(TRACK_RANGE / 0.96);
     293           0 :         track_height = (int16_t)(TRACK_RANGE / 0.54);
     294           0 :         send_tracking_cmd = true;
     295           0 :         break;
     296             :     }
     297             : 
     298           0 :     case TrackingType::TRK_RECTANGLE:
     299             :         // calculate upper left and bottom right points
     300             :         // handle case where p1 and p2 are in an unexpected order
     301           0 :         int16_t upper_leftx = (int16_t)(MIN(p1.x, p2.x)*TRACK_TOTAL_WIDTH);
     302           0 :         int16_t upper_lefty = (int16_t)(MIN(p1.y, p2.y)*TRACK_TOTAL_HEIGHT);
     303           0 :         int16_t bottom_rightx = (int16_t)(MAX(p1.x, p2.x)*TRACK_TOTAL_WIDTH);
     304           0 :         int16_t bottom_righty = (int16_t)(MAX(p1.y, p2.y)*TRACK_TOTAL_HEIGHT);
     305             : 
     306             :         // calculated width and height and sanity check 
     307           0 :         const int16_t frame_selection_width = bottom_rightx - upper_leftx;
     308           0 :         const int16_t frame_selection_height = bottom_righty - upper_lefty;
     309           0 :         if (frame_selection_width <= 0 || frame_selection_height <= 0) {
     310           0 :             return false;
     311             :         }
     312             : 
     313             :         // calculate tracking center
     314           0 :         track_center_x = (int16_t)((((upper_leftx + bottom_rightx) * 0.5) - 960) / 0.96);
     315           0 :         track_center_y = (int16_t)((((upper_lefty + bottom_righty) * 0.5) - 540) / 0.54);
     316             : 
     317             :         // tracking range after conversion
     318           0 :         track_width = (int16_t)(frame_selection_width / 0.96);
     319           0 :         track_height = (int16_t)(frame_selection_height / 0.54);
     320             : 
     321           0 :         send_tracking_cmd = true;
     322           0 :         break;
     323             :     }
     324             : 
     325           0 :     if (send_tracking_cmd) {
     326             :         // set the gimbal to the ready-to-track state when the gimbal tracking status is stopped
     327           0 :         if (_last_tracking_state == TrackingStatus::STOPPED_TRACKING) {
     328           0 :             send_fixedlen_packet(AddressByte::SYSTEM_AND_IMAGE, AP_MOUNT_TOPOTEK_ID3CHAR_TRACKING, true, 2);
     329             :         }
     330             : 
     331             :         // prepare data bytes
     332             :         uint8_t databuff[10];
     333           0 :         databuff[0] = HIGHBYTE(track_center_x);
     334           0 :         databuff[1] = LOWBYTE(track_center_x);
     335           0 :         databuff[2] = HIGHBYTE(track_center_y);
     336           0 :         databuff[3] = LOWBYTE(track_center_y);
     337           0 :         databuff[4] = HIGHBYTE(track_width);
     338           0 :         databuff[5] = LOWBYTE(track_width);
     339           0 :         databuff[6] = HIGHBYTE(track_height);
     340           0 :         databuff[7] = LOWBYTE(track_height);
     341           0 :         databuff[8] = 0;
     342           0 :         databuff[9] = (tracking_type == TrackingType::TRK_POINT) ? 9 : 1;   // when tracking point, enable fuzzy click function
     343             : 
     344             :         // send tracking command
     345           0 :         bool res = send_variablelen_packet(HeaderType::VARIABLE_LEN,
     346             :                                            AddressByte::SYSTEM_AND_IMAGE,
     347             :                                            AP_MOUNT_TOPOTEK_ID3CHAR_START_TRACKING,
     348             :                                            true,
     349             :                                            (uint8_t*)databuff, ARRAY_SIZE(databuff));
     350             : 
     351             :         // display error message on failure
     352           0 :         if (!res) {
     353           0 :             GCS_SEND_TEXT(MAV_SEVERITY_ERROR, "%s tracking failed", send_message_prefix);
     354             :         }
     355             : 
     356           0 :         return res;
     357             :     }
     358             : 
     359             :     // should never reach here
     360           0 :     return false;
     361             : }
     362             : 
     363             : // send command to gimbal to cancel tracking (if necessary)
     364             : // returns true on success, false on failure to send message
     365           0 : bool AP_Mount_Topotek::cancel_tracking()
     366             : {
     367             :     // exit immediately if not initialised
     368           0 :     if (!_initialised) {
     369           0 :         return false;
     370             :     }
     371             : 
     372             :     // if gimbal is tracking-in-progress change to waiting state, otherwise stop
     373           0 :     const uint8_t track_set = _last_tracking_state == TrackingStatus::TRACKING_IN_PROGRESS ? 1 : 0;
     374             : 
     375             :     // send tracking command
     376           0 :     return send_fixedlen_packet(AddressByte::SYSTEM_AND_IMAGE, AP_MOUNT_TOPOTEK_ID3CHAR_TRACKING, true, track_set);
     377             : }
     378             : 
     379             : // set camera picture-in-picture mode
     380           0 : bool AP_Mount_Topotek::set_lens(uint8_t lens)
     381             : {
     382             :     // exit immediately if not initialised
     383           0 :     if (!_initialised) {
     384           0 :         return false;
     385             :     }
     386             : 
     387             :     // sanity check lens number
     388             :     // 00:main only, 01:main+sub, 02:sub+main, 03:sub only, 0A:next
     389             :     // sample command: #TPUD2wPIP0A
     390           0 :     if (lens > 3) {
     391           0 :         return false;
     392             :     }
     393             : 
     394             :     // send pip command
     395           0 :     return send_fixedlen_packet(AddressByte::SYSTEM_AND_IMAGE, AP_MOUNT_TOPOTEK_ID3CHAR_PIP, true, lens);
     396             : }
     397             : 
     398             : #if HAL_MOUNT_SET_CAMERA_SOURCE_ENABLED
     399             : // set_camera_source is functionally the same as set_lens except primary and secondary lenses are specified by type
     400             : // primary and secondary sources use the AP_Camera::CameraSource enum cast to uint8_t
     401           0 : bool AP_Mount_Topotek::set_camera_source(uint8_t primary_source, uint8_t secondary_source)
     402             : {
     403             :     // exit immediately if not initialised
     404           0 :     if (!_initialised) {
     405           0 :         return false;
     406             :     }
     407             : 
     408             :     // maps primary and secondary source to pip setting
     409             :     // pip settings 00:main only, 01:main+sub, 02:sub+main, 03:sub only, 0A:next
     410             :     // sample command: #TPUD2wPIP0A
     411           0 :     uint8_t pip_setting = 0;
     412           0 :     switch (primary_source) {
     413           0 :     case 0: // Default (RGB)
     414             :         FALLTHROUGH;
     415             :     case 1: // RGB
     416           0 :         switch (secondary_source) {
     417           0 :         case 0: // RGB + Default (None)
     418           0 :             pip_setting = 0;    // main only
     419           0 :             break;
     420           0 :         case 2: // PIP RGB+IR
     421           0 :             pip_setting = 1;    // main+sub
     422           0 :             break;
     423           0 :         default:
     424           0 :             return false;
     425             :         }
     426           0 :         break;
     427           0 :     case 2: // IR
     428           0 :         switch (secondary_source) {
     429           0 :         case 0: // IR + Default (None)
     430           0 :             pip_setting = 3;    // sub only
     431           0 :             break;
     432           0 :         case 1: // IR+RGB
     433           0 :             pip_setting = 2;    // sub+main
     434           0 :             break;
     435           0 :         default:
     436           0 :             return false;
     437             :         }
     438           0 :         break;
     439           0 :     default:
     440           0 :         return false;
     441             :     }
     442             : 
     443             :     // send pip command
     444           0 :     return send_fixedlen_packet(AddressByte::SYSTEM_AND_IMAGE, AP_MOUNT_TOPOTEK_ID3CHAR_PIP, true, pip_setting);
     445             : }
     446             : #endif  // HAL_MOUNT_SET_CAMERA_SOURCE_ENABLED
     447             : 
     448             : // send camera settings message to GCS
     449           0 : void AP_Mount_Topotek::send_camera_settings(mavlink_channel_t chan) const
     450             : {
     451             :     // exit immediately if not initialised
     452           0 :     if (!_initialised) {
     453           0 :         return;
     454             :     }
     455             : 
     456             :     // send CAMERA_SETTINGS message
     457           0 :     mavlink_msg_camera_settings_send(
     458             :         chan,
     459             :         AP_HAL::millis(),   // time_boot_ms
     460           0 :         _recording ? CAMERA_MODE_VIDEO : CAMERA_MODE_IMAGE, // camera mode (0:image, 1:video, 2:image survey)
     461             :         NaNf,               // zoomLevel float, percentage from 0 to 100, NaN if unknown
     462             :         NaNf);              // focusLevel float, percentage from 0 to 100, NaN if unknown
     463             : }
     464             : 
     465             : // get rangefinder distance.  Returns true on success
     466        2080 : bool AP_Mount_Topotek::get_rangefinder_distance(float& distance_m) const
     467             : {
     468             :     // if not healthy or negative distance return false
     469             :     // healthy() checks attitude timeout which is in same message as rangefinder distance
     470        2080 :     if (!healthy() || (_measure_dist_m < 0)) {
     471        2080 :         return false;
     472             :     }
     473             : 
     474           0 :     distance_m = _measure_dist_m;
     475           0 :     return true;
     476             : }
     477             : 
     478             : // enable/disable rangefinder.  Returns true on success
     479           0 : bool AP_Mount_Topotek::set_rangefinder_enable(bool enable)
     480             : {
     481             :     // exit immediately if not initialised
     482           0 :     if (!_initialised) {
     483           0 :         return false;
     484             :     }
     485             : 
     486             :     // 00:ranging stop, 01:ranging start, 02:single measurement, 03:continuous measurement
     487             :     // sample command: #TPUM2wLRF00
     488           0 :     return send_fixedlen_packet(AddressByte::LENS, AP_MOUNT_TOPOTEK_ID3CHAR_LRF, true, enable ? 3 : 0);
     489             : }
     490             : 
     491             : // get attitude as a quaternion.  returns true on success
     492        3121 : bool AP_Mount_Topotek::get_attitude_quaternion(Quaternion& att_quat)
     493             : {
     494        3121 :     att_quat.from_euler(_current_angle_rad.x, _current_angle_rad.y, _current_angle_rad.z);
     495        3121 :     return true;
     496             : }
     497             : 
     498             : // reading incoming packets from gimbal and confirm they are of the correct format
     499       10408 : void AP_Mount_Topotek::read_incoming_packets()
     500             : {
     501             :     // check for bytes on the serial port
     502       10408 :     int16_t nbytes = MIN(_uart->available(), 1024U);
     503       10408 :     if (nbytes <= 0 ) {
     504        7695 :         return;
     505             :     }
     506             : 
     507             :     // flag to allow cases below to reset parser state
     508        2713 :     bool reset_parser = false;
     509             : 
     510             :     // process bytes received
     511       64589 :     for (int16_t i = 0; i < nbytes; i++) {
     512             :         uint8_t b;
     513       61876 :         if (!_uart->read(b)) {
     514           0 :             continue;
     515             :         }
     516             : 
     517             :         // add latest byte to buffer
     518       61876 :         _msg_buff[_msg_buff_len++] = b;
     519             : 
     520             :         // protect against overly long messages
     521       61876 :         if (_msg_buff_len >= AP_MOUNT_TOPOTEK_PACKETLEN_MAX) {
     522           0 :             reset_parser = true;
     523             :         }
     524             : 
     525             :         // process byte depending upon current state
     526       61876 :         switch (_parser.state) {
     527             : 
     528        2736 :         case ParseState::WAITING_FOR_HEADER1:
     529        2736 :             if (b == '#') {
     530        2736 :                 _parser.state = ParseState::WAITING_FOR_HEADER2;
     531        2736 :                 break;
     532             :             }
     533           0 :             reset_parser = true;
     534           0 :             break;
     535             : 
     536        2736 :         case ParseState::WAITING_FOR_HEADER2:
     537        2736 :             if (b == 't' || b == 'T') {
     538        2736 :                 _parser.state = ParseState::WAITING_FOR_HEADER3;
     539        2736 :                 break;
     540             :             }
     541           0 :             reset_parser = true;
     542           0 :             break;
     543             : 
     544        2736 :         case ParseState::WAITING_FOR_HEADER3:
     545        2736 :             if (b == 'p' || b == 'P') {
     546        2736 :                 _parser.state = ParseState::WAITING_FOR_ADDR1;
     547        2736 :                 break;
     548             :             }
     549           0 :             reset_parser = true;
     550           0 :             break;
     551             : 
     552        5472 :         case ParseState::WAITING_FOR_ADDR1:
     553             :         case ParseState::WAITING_FOR_ADDR2:
     554        5472 :             if (b == 'U' || b =='M' || b == 'D' || b =='E' || b =='P' || b =='G') {
     555             :                 // advance to next state
     556        5472 :                 _parser.state = (ParseState)((uint8_t)_parser.state+1);
     557        5472 :                 break;
     558             :             }
     559           0 :             reset_parser = true;
     560           0 :             break;
     561             : 
     562        2736 :         case ParseState::WAITING_FOR_DATALEN: {
     563             :             // sanity check data length
     564             :             uint8_t data_len;
     565        2736 :             if (hex_char_to_nibble(b, data_len) && data_len <= AP_MOUNT_TOPOTEK_DATALEN_MAX) {
     566        2736 :                 _parser.data_len = data_len;
     567        2736 :                 _parser.state = ParseState::WAITING_FOR_CONTROL;
     568        2736 :                 break;
     569             :             }
     570           0 :             reset_parser = true;
     571           0 :             break;
     572             :         }
     573             : 
     574        2736 :         case ParseState::WAITING_FOR_CONTROL:
     575             :             // r or w
     576        2736 :             if (b == 'r' || b == 'w') {
     577        2736 :                 _parser.state = ParseState::WAITING_FOR_ID1;
     578        2736 :                 break;
     579             :             }
     580           0 :             reset_parser = true;
     581           0 :             break;
     582             : 
     583        8208 :         case ParseState::WAITING_FOR_ID1:
     584             :         case ParseState::WAITING_FOR_ID2:
     585             :         case ParseState::WAITING_FOR_ID3:
     586             :             // check all uppercase letters and numbers.  eg 'GAC'
     587        8208 :             if ((b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')) {
     588             :                 // advance to next state
     589        8208 :                 _parser.state = (ParseState)((uint8_t)_parser.state+1);
     590        8208 :                 break;
     591             :             }
     592           0 :             reset_parser = true;
     593           0 :             break;
     594             : 
     595       29044 :         case ParseState::WAITING_FOR_DATA: {
     596             :             // normally hex numbers in char form (e.g. '0A')
     597       29044 :             const uint8_t data_bytes_received = _msg_buff_len - (AP_MOUNT_TOPOTEK_PACKETLEN_MIN - 2);
     598             : 
     599             :             // sanity check to protect against programming errors
     600       29044 :             if (data_bytes_received > AP_MOUNT_TOPOTEK_DATALEN_MAX) {
     601           0 :                 INTERNAL_ERROR(AP_InternalError::error_t::flow_of_control);
     602           0 :                 reset_parser = true;
     603           0 :                 break;
     604             :             }
     605             :             
     606             :             // advance parser state once expected number of bytes have been received
     607       29044 :             if (data_bytes_received == _parser.data_len) {
     608        2736 :                 _parser.state = ParseState::WAITING_FOR_CRC_LOW;
     609             :             }
     610       29044 :             break;
     611             :         }
     612             : 
     613        2736 :         case ParseState::WAITING_FOR_CRC_LOW:
     614        2736 :             _parser.state = ParseState::WAITING_FOR_CRC_HIGH;
     615        2736 :             break;
     616             : 
     617        2736 :         case ParseState::WAITING_FOR_CRC_HIGH:
     618             :             // this is the last byte in the message so reset the parser
     619        2736 :             reset_parser = true;
     620             : 
     621             :             // sanity check to protect against programming errors
     622        2736 :             if (_msg_buff_len < AP_MOUNT_TOPOTEK_PACKETLEN_MIN) {
     623           0 :                 INTERNAL_ERROR(AP_InternalError::error_t::flow_of_control);
     624           0 :                 break;
     625             :             }
     626             : 
     627             :             // calculate and check CRC
     628        2736 :             const uint8_t crc_value = calculate_crc(_msg_buff, _msg_buff_len - 2);
     629        2736 :             const char crc_char1 = hex2char((crc_value >> 4) & 0x0f);
     630        2736 :             const char crc_char2 = hex2char((crc_value) & 0x0f);
     631        2736 :             if (crc_char1 != _msg_buff[_msg_buff_len - 2] || crc_char2 != _msg_buff[_msg_buff_len-1]) {
     632             :                 debug("CRC expected:%x got:%c%c", (int)crc_value, crc_char1, crc_char2);
     633           0 :                 break;
     634             :             }
     635             : 
     636             :             // CRC is OK, call function to process the message
     637        4012 :             for (uint8_t count = 0; count < AP_MOUNT_RECV_GIMBAL_CMD_CATEGORIES_NUM; count++) {
     638        4012 :                 if (strncmp((const char*)_msg_buff + 7, (const char*)(uart_recv_cmd_compare_list[count].uart_cmd_key), 3) == 0) {
     639        2736 :                     (this->*(uart_recv_cmd_compare_list[count].func))();
     640        2736 :                     break;
     641             :                 }
     642             :             }
     643             :         }
     644             : 
     645             :         // handle reset of parser
     646       61876 :         if (reset_parser) {
     647        2736 :             _parser.state = ParseState::WAITING_FOR_HEADER1;
     648        2736 :             _msg_buff_len = 0;
     649        2736 :             reset_parser = false;
     650             :         }
     651             :     }
     652             : }
     653             : 
     654             : // request gimbal attitude
     655         209 : void AP_Mount_Topotek::request_gimbal_attitude()
     656             : {
     657             :     // sample command: #TPUG2wGIA01
     658         209 :     send_fixedlen_packet(AddressByte::GIMBAL, AP_MOUNT_TOPOTEK_ID3CHAR_GIMBAL_ATT, true, 1);
     659         209 : }
     660             : 
     661             : // request gimbal memory card information
     662         209 : void AP_Mount_Topotek::request_gimbal_sdcard_info()
     663             : {
     664             :     // request remaining capacity
     665             :     // sample command including CRC: #TPUD2rSDC003E
     666             :     // 00:get remaining capacity, 01:get total capacity
     667         209 :     send_fixedlen_packet(AddressByte::SYSTEM_AND_IMAGE, AP_MOUNT_TOPOTEK_ID3CHAR_SD_CARD, false, 0);
     668         209 : }
     669             : 
     670             : // request gimbal tracking status
     671         209 : void AP_Mount_Topotek::request_track_status()
     672             : {
     673             :     // 00:get status (use with "r"), 01:stop (use with "w")
     674             :     // sample command: #TPUD2rTRC00
     675         209 :     send_fixedlen_packet(AddressByte::SYSTEM_AND_IMAGE, AP_MOUNT_TOPOTEK_ID3CHAR_TRACKING, false, 0);
     676         209 : }
     677             : 
     678             : // request gimbal version
     679           2 : void AP_Mount_Topotek::request_gimbal_version()
     680             : {
     681             :     // sample command: #TPUD2rVSN00
     682           2 :     send_fixedlen_packet(AddressByte::SYSTEM_AND_IMAGE, AP_MOUNT_TOPOTEK_ID3CHAR_GET_VERSION, false, 0);
     683           2 : }
     684             : 
     685             : // request gimbal model name
     686           2 : void AP_Mount_Topotek::request_gimbal_model_name()
     687             : {
     688             :     // sample command: #TPUG2rPA200
     689           2 :     send_fixedlen_packet(AddressByte::GIMBAL, AP_MOUNT_TOPOTEK_ID3CHAR_GET_MODEL_NAME, false, 0);
     690           2 : }
     691             : 
     692             : // send angle target in radians to gimbal
     693        2082 : void AP_Mount_Topotek::send_target_angles(const MountAngleTarget& angle_rad)
     694             : {
     695             :     // gimbal's earth-frame angle control drifts so always use body frame
     696             :     // set gimbal's lock state if it has changed
     697        2082 :     if (!set_gimbal_lock(false)) {
     698           0 :         return;
     699             :     }
     700             : 
     701             :     // calculate and send yaw target
     702             :     // sample command #tpUG6wGIY
     703        2082 :     const char* format_str = "%04X%02X";
     704        2082 :     const uint8_t speed = 99;
     705        2082 :     const uint16_t yaw_angle_cd = (uint16_t)constrain_int16(degrees(angle_rad.get_bf_yaw()) * 100, MAX(-18000, _params.yaw_angle_min * 100), MIN(18000, _params.yaw_angle_max * 100));
     706             : 
     707             :     uint8_t databuff[7];
     708        2082 :     hal.util->snprintf((char *)databuff, ARRAY_SIZE(databuff), format_str, yaw_angle_cd, speed);
     709        2082 :     send_variablelen_packet(HeaderType::VARIABLE_LEN,
     710             :                             AddressByte::GIMBAL,
     711             :                             AP_MOUNT_TOPOTEK_ID3CHAR_YAW_ANGLE_BF,
     712             :                             true,
     713             :                             (uint8_t*)databuff, ARRAY_SIZE(databuff)-1);
     714             : 
     715             :     // send pitch target
     716             :     // sample command: #tpUG6wGIP
     717        2082 :     const uint16_t pitch_angle_cd = (uint16_t)constrain_int16(-degrees(angle_rad.pitch) * 100, -9000, 9000);
     718        2082 :     hal.util->snprintf((char *)databuff, ARRAY_SIZE(databuff), format_str, pitch_angle_cd, speed);
     719        2082 :     send_variablelen_packet(HeaderType::VARIABLE_LEN,
     720             :                             AddressByte::GIMBAL,
     721             :                             AP_MOUNT_TOPOTEK_ID3CHAR_PITCH_ANGLE,
     722             :                             true,
     723             :                             (uint8_t*)databuff, ARRAY_SIZE(databuff)-1);
     724             : 
     725             :     // send roll target
     726             :     // sample command: #tpUG6wGIR
     727        2082 :     const uint16_t roll_angle_cd = (uint16_t)constrain_int16(degrees(angle_rad.roll) * 100, -18000, 18000);
     728        2082 :     hal.util->snprintf((char *)databuff, ARRAY_SIZE(databuff), format_str, roll_angle_cd, speed);
     729        2082 :     send_variablelen_packet(HeaderType::VARIABLE_LEN,
     730             :                             AddressByte::GIMBAL,
     731             :                             AP_MOUNT_TOPOTEK_ID3CHAR_ROLL_ANGLE,
     732             :                             true,
     733             :                             (uint8_t*)databuff, ARRAY_SIZE(databuff)-1);
     734             : }
     735             : 
     736             : // send rate target in rad/s to gimbal
     737           0 : void AP_Mount_Topotek::send_target_rates(const MountRateTarget& rate_rads)
     738             : {
     739             :     // set gimbal's lock state if it has changed
     740           0 :     if (!set_gimbal_lock(rate_rads.yaw_is_ef)) {
     741           0 :         return;
     742             :     }
     743             : 
     744             :     // convert and constrain rates
     745           0 :     const uint8_t roll_angle_speed = constrain_int16(degrees(rate_rads.roll) * ANGULAR_VELOCITY_CONVERSION, -99, 99);
     746           0 :     const uint8_t pitch_angle_speed = constrain_int16(-degrees(rate_rads.pitch) * ANGULAR_VELOCITY_CONVERSION, -99, 99);
     747           0 :     const uint8_t yaw_angle_speed = constrain_int16(degrees(rate_rads.yaw) * ANGULAR_VELOCITY_CONVERSION, -99, 99);
     748             : 
     749             :     // send stop rotation command three times if target roll, pitch and yaw are zero
     750           0 :     if (roll_angle_speed == 0 && pitch_angle_speed == 0 && yaw_angle_speed == 0) {
     751           0 :         if (_stop_order_count < 3) {
     752             :             // sample command: #TPUG2wPTZ00
     753           0 :             if (send_fixedlen_packet(AddressByte::GIMBAL, AP_MOUNT_TOPOTEK_ID3CHAR_GIMBAL_MODE, true, 0)) {
     754           0 :                 _stop_order_count++;
     755             :             }
     756             :         }
     757           0 :         return;
     758             :     }
     759           0 :     _stop_order_count = 0;
     760             : 
     761             :     // prepare and send command
     762             :     // sample command: #tpUG6wYPR
     763             :     uint8_t databuff[7];
     764           0 :     hal.util->snprintf((char *)databuff, ARRAY_SIZE(databuff), "%02X%02X%02X", yaw_angle_speed, pitch_angle_speed, roll_angle_speed);
     765           0 :     send_variablelen_packet(HeaderType::VARIABLE_LEN, AddressByte::GIMBAL, AP_MOUNT_TOPOTEK_ID3CHAR_YPR_RATE, true, databuff, ARRAY_SIZE(databuff)-1);
     766             : }
     767             : 
     768             : // send time and date to gimbal
     769          19 : bool AP_Mount_Topotek::send_time_to_gimbal()
     770             : {
     771             : #if AP_RTC_ENABLED
     772             :     // get date and time
     773             :     // year is the regular Gregorian year, month is 0~11, day is 1~31, hour is 0~23, minute is 0~59, second is 0~60 (1 leap second), ms is 0~999
     774             :     uint16_t year, ms;
     775             :     uint8_t month, day, hour, min, sec;
     776          19 :     if (!AP::rtc().get_date_and_time_utc(year, month, day, hour, min, sec, ms)) {
     777          12 :         return false;
     778             :     }
     779             : 
     780             :     // sample command: #tpUDCwUTCHHMMSSDDMMYY
     781             :     uint8_t databuff[13];
     782           7 :     hal.util->snprintf((char*)databuff, ARRAY_SIZE(databuff), "%02d%02d%02d%02d%02d%02d", hour, min, sec, day, month + 1, year - 2000);
     783           7 :     return send_variablelen_packet(HeaderType::VARIABLE_LEN, AddressByte::SYSTEM_AND_IMAGE, AP_MOUNT_TOPOTEK_ID3CHAR_TIME, true, (uint8_t*)databuff, ARRAY_SIZE(databuff)-1);
     784             : #else
     785             :     return false;
     786             : #endif
     787             : }
     788             : 
     789             : // send GPS-related information to the gimbal
     790        2082 : bool AP_Mount_Topotek::send_location_info()
     791             : {
     792             :     // get current location
     793        2082 :     Location loc;
     794        2082 :     int32_t alt_amsl_cm = 0;
     795        2082 :     if (!AP::ahrs().get_location(loc) || !loc.get_alt_cm(Location::AltFrame::ABSOLUTE, alt_amsl_cm)) {
     796         112 :         return false;
     797             :     }
     798             : 
     799             :     // convert latitude and longitude to positive angles in degrees
     800        1970 :     const double latitude = labs(loc.lat) * 1e-7;
     801        1970 :     const double longitude = labs(loc.lng) * 1e-7;
     802             : 
     803             :     // get the degree part
     804        1970 :     const int16_t lat_deg = (int16_t)latitude;
     805        1970 :     const int16_t lng_deg = (int16_t)longitude;
     806             : 
     807             :     // get the minute part
     808        1970 :     const double lat_min = (latitude - lat_deg) * 60.0;
     809        1970 :     const double lng_min = (longitude - lng_deg) * 60.0;
     810             : 
     811             :     // prepare and send latitude
     812             :     // first byte is N or S, followed by GPS coordinates in degree division format, in the format of ddmm.mmmm
     813             :     // first byte is zero and will also be transmitted.  same as the data format in $GPGGA
     814             :     // sample command: #tpUDAwLATNddmm.mmmm
     815             :     uint8_t databuff_lat[11];
     816        1970 :     hal.util->snprintf((char*)databuff_lat, ARRAY_SIZE(databuff_lat), "%c%02d%07.4f", loc.lat > 0 ? 'N':'S', lat_deg, lat_min);
     817        1970 :     if (!send_variablelen_packet(HeaderType::VARIABLE_LEN, AddressByte::SYSTEM_AND_IMAGE, AP_MOUNT_TOPOTEK_ID3CHAR_SET_LAT, true, (uint8_t*)databuff_lat, ARRAY_SIZE(databuff_lat)-1)) {
     818           0 :         return false;
     819             :     }
     820             : 
     821             :     // prepare and send longitude
     822             :     // sample command: #tpUDBwLONEdddmm.mmmm
     823             :     uint8_t databuff_lon[12];
     824        1970 :     hal.util->snprintf((char*)databuff_lon, ARRAY_SIZE(databuff_lon), "%c%03d%07.4f", loc.lng > 0 ? 'E':'W', lng_deg, lng_min);
     825        1970 :     if (!send_variablelen_packet(HeaderType::VARIABLE_LEN, AddressByte::SYSTEM_AND_IMAGE, AP_MOUNT_TOPOTEK_ID3CHAR_SET_LON, true, (uint8_t*)databuff_lon, ARRAY_SIZE(databuff_lon)-1)) {
     826           0 :         return false;
     827             :     }
     828             : 
     829             :     // get the height in meters
     830        1970 :     float alt_amsl_m = alt_amsl_cm * 0.01;
     831             : 
     832             :     // prepare and send vehicle altitude
     833             :     // sample command: #tpUD8wALT000000.0, similar format to $GPGGA
     834             :     uint8_t databuff_alt[9];
     835        1970 :     hal.util->snprintf((char*)databuff_alt, ARRAY_SIZE(databuff_alt), "%08.1f", alt_amsl_m);
     836        1970 :     if (!send_variablelen_packet(HeaderType::VARIABLE_LEN, AddressByte::SYSTEM_AND_IMAGE, AP_MOUNT_TOPOTEK_ID3CHAR_SET_ALT, true, (uint8_t*)databuff_alt, ARRAY_SIZE(databuff_alt)-1)) {
     837           0 :         return false;
     838             :     }
     839             : 
     840             :     // prepare and send vehicle yaw
     841             :     // sample command: #tpUD5wAZI359.9, similar format to $GPRMC
     842        1970 :     const float veh_yaw_deg = AP::ahrs().get_yaw_deg();
     843             :     uint8_t databuff_azimuth[6];
     844        1970 :     hal.util->snprintf((char*)databuff_azimuth, ARRAY_SIZE(databuff_azimuth), "%05.1f", veh_yaw_deg);
     845        1970 :     if (!send_variablelen_packet(HeaderType::VARIABLE_LEN, AddressByte::SYSTEM_AND_IMAGE, AP_MOUNT_TOPOTEK_ID3CHAR_SET_AZIMUTH, true, (uint8_t*)databuff_azimuth, ARRAY_SIZE(databuff_azimuth)-1)) {
     846           0 :         return false;
     847             :     }
     848             : 
     849        1970 :     return true;
     850             : }
     851             : 
     852             : // attitude information analysis of gimbal
     853        2314 : void AP_Mount_Topotek::gimbal_angle_analyse()
     854             : {
     855             :     // consume current angles
     856             :     uint32_t yaw_raw, pitch_raw, roll_raw;
     857        2314 :     if (!hex_chars_to_uint32((const char*)&_msg_buff[10], 4, yaw_raw) ||
     858        4628 :         !hex_chars_to_uint32((const char*)&_msg_buff[14], 4, pitch_raw) ||
     859        2314 :         !hex_chars_to_uint32((const char*)&_msg_buff[18], 4, roll_raw)) {
     860           0 :         return;
     861             :     }
     862        2314 :     const int16_t yaw_angle_cd = wrap_180_cd((int16_t)yaw_raw);
     863        2314 :     const int16_t pitch_angle_cd = -(int16_t)pitch_raw;
     864        2314 :     const int16_t roll_angle_cd = (int16_t)roll_raw;
     865             : 
     866             :     // convert cd to radians
     867        2314 :     _current_angle_rad.x = cd_to_rad(roll_angle_cd);
     868        2314 :     _current_angle_rad.y = cd_to_rad(pitch_angle_cd);
     869        2314 :     _current_angle_rad.z = cd_to_rad(yaw_angle_cd);
     870        2314 :     _last_current_angle_ms = AP_HAL::millis();
     871             : 
     872        2314 :     return;
     873             : }
     874             : 
     875             : // gimbal video information analysis
     876           0 : void AP_Mount_Topotek::gimbal_record_analyse()
     877             : {
     878           0 :     _recording = (_msg_buff[10] == '1' || _msg_buff[11] == '1');
     879           0 :     GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s recording %s", send_message_prefix, _recording ? "ON" : "OFF");
     880           0 : }
     881             : 
     882             : // information analysis of gimbal storage card
     883         209 : void AP_Mount_Topotek::gimbal_sdcard_analyse()
     884             : {
     885         209 :     if (('N' == _msg_buff[10]) && ('N' == _msg_buff[11]) && ('N' == _msg_buff[12]) && ('N' == _msg_buff[13])) {
     886             :         // memory card exception
     887           0 :         _sdcard_status = false;
     888           0 :         return;
     889             :     }
     890         209 :     _sdcard_status = true;
     891             : 
     892             :     // send UTC time to the camera
     893         209 :     if (_sent_time_count < 7) {
     894          19 :         if (send_time_to_gimbal()) {
     895           7 :             _sent_time_count++;
     896             :         }
     897             :     }
     898             : 
     899         209 :     return;
     900             : }
     901             : 
     902             : // gimbal tracking information analysis
     903         209 : void AP_Mount_Topotek::gimbal_track_analyse()
     904             : {
     905             :     // ignore tracking state if unchanged
     906         209 :     TrackingStatus tracking_state = (TrackingStatus)_msg_buff[11];
     907         209 :     if (tracking_state == _last_tracking_state) {
     908         209 :         return;
     909             :     }
     910           0 :     _last_tracking_state = tracking_state;
     911             : 
     912             :     // inform user
     913           0 :     const char* tracking_str = "tracking";
     914           0 :     switch (tracking_state) {
     915           0 :     case TrackingStatus::STOPPED_TRACKING:
     916           0 :         GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s %s stopped", send_message_prefix, tracking_str);
     917           0 :         _is_tracking = false;
     918           0 :         break;
     919           0 :     case TrackingStatus::WAITING_FOR_TRACKING:
     920           0 :         GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s %s waiting", send_message_prefix, tracking_str);
     921           0 :         _is_tracking = false;
     922           0 :         break;
     923           0 :     case TrackingStatus::TRACKING_IN_PROGRESS:
     924           0 :         GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s %s started", send_message_prefix, tracking_str);
     925           0 :         _is_tracking = true;
     926           0 :         break;
     927           0 :     case TrackingStatus::LENS_UNSUPPORT_TRACK:
     928           0 :         GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s %s unsupported lens", send_message_prefix, tracking_str);
     929           0 :         break;
     930             :     }
     931             : }
     932             : 
     933             : // gimbal distance information analysis
     934           0 : void AP_Mount_Topotek::gimbal_dist_info_analyse()
     935             : {
     936           0 :     if ('E' == _msg_buff[10] && 'R' == _msg_buff[11] && 'R' ==_msg_buff[12]) {
     937           0 :         _measure_dist_m = -1.0f;
     938           0 :         return;
     939             :     }
     940             : 
     941             :     // distance is in meters in the format, "12345.6" where each digit is in decimal
     942             :     uint8_t d0, d1, d2, d3, d4, d5;
     943           0 :     if (!hex_char_to_nibble(_msg_buff[10], d0) ||
     944           0 :         !hex_char_to_nibble(_msg_buff[11], d1) ||
     945           0 :         !hex_char_to_nibble(_msg_buff[12], d2) ||
     946           0 :         !hex_char_to_nibble(_msg_buff[13], d3) ||
     947           0 :         !hex_char_to_nibble(_msg_buff[14], d4) ||
     948           0 :         !hex_char_to_nibble(_msg_buff[16], d5)) {
     949           0 :         return;
     950             :     }
     951           0 :     _measure_dist_m = d0 * 10000.0 + d1 * 1000.0 + d2 * 100.0 + d3 * 10.0 + d4 + d5 * 0.1;
     952             : }
     953             : 
     954             : // gimbal basic information analysis
     955           2 : void AP_Mount_Topotek::gimbal_version_analyse()
     956             : {
     957             :     // version array with index 0=major, 1=minor, 2=patch
     958           2 :     uint8_t version[3] {};
     959             : 
     960             :     // extract firmware version
     961             :     // the version can be in the format "1.2.3" or "123"
     962             :     uint8_t data_buf_len;
     963           2 :     if (!hex_char_to_nibble(_msg_buff[5], data_buf_len)) {
     964           0 :         return;
     965             :     }
     966             : 
     967             :     // check for "."
     968           2 :     bool contains_period = false;
     969          12 :     for (uint8_t i = 0; i < data_buf_len; i++) {
     970          10 :         contains_period |= _msg_buff[10 + i] == '.';
     971             :     }
     972             : 
     973             :     // if contains period, extract version number
     974           2 :     uint32_t ver_num = 0;
     975           2 :     uint8_t ver_count = 0;
     976           2 :     if (contains_period) {
     977          12 :         for (uint8_t i = 0; i < data_buf_len; i++) {
     978          10 :             if (_msg_buff[10 + i] != '.') {
     979             :                 uint8_t digit;
     980           6 :                 if (!hex_char_to_nibble(_msg_buff[10 + i], digit)) {
     981           0 :                     return;
     982             :                 }
     983           6 :                 ver_num = ver_num * 10 + digit;
     984             :             } else {
     985           4 :                 version[ver_count++] = ver_num;
     986           4 :                 ver_num = 0;
     987             :             }
     988          10 :             if (ver_count >= ARRAY_SIZE(version)) {
     989           0 :                 break;
     990             :             }
     991             :         }
     992             :     } else {
     993             :         uint8_t d;
     994           0 :         if (data_buf_len >= 1) {
     995           0 :             if (!hex_char_to_nibble(_msg_buff[10], d)) { return; }
     996           0 :             version[0] = d;
     997             :         }
     998           0 :         if (data_buf_len >= 2) {
     999           0 :             if (!hex_char_to_nibble(_msg_buff[11], d)) { return; }
    1000           0 :             version[1] = d;
    1001             :         }
    1002           0 :         if (data_buf_len >= 3) {
    1003           0 :             if (!hex_char_to_nibble(_msg_buff[12], d)) { return; }
    1004           0 :             version[2] = d;
    1005             :         }
    1006             :     }
    1007           2 :     _firmware_ver = (version[2] << 16) | (version[1] << 8) | (version[0]);
    1008             : 
    1009             :     // display gimbal model and firmware version to user
    1010           2 :     GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s v%u.%u.%u",
    1011             :         send_message_prefix,
    1012             :         version[0],     // major version
    1013             :         version[1],     // minor version
    1014             :         version[2]);    // patch version
    1015             : 
    1016           2 :     _got_gimbal_version = true;
    1017             : }
    1018             : 
    1019             : // gimbal model name message analysis
    1020           2 : void AP_Mount_Topotek::gimbal_model_name_analyse()
    1021             : {
    1022             :     uint8_t len;
    1023           2 :     if (!hex_char_to_nibble(_msg_buff[5], len)) {
    1024           0 :         return;
    1025             :     }
    1026           2 :     memset(_model_name, 0, sizeof(_model_name));
    1027           2 :     memcpy(_model_name, _msg_buff + 10, MIN((uint8_t)(sizeof(_model_name)-1), len));
    1028             : 
    1029             :     // display gimbal model name to user
    1030           2 :     GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s %s", send_message_prefix, _model_name);
    1031             : 
    1032           2 :     _got_gimbal_model_name = true;
    1033             : }
    1034             : 
    1035             : // calculate checksum
    1036       17500 : uint8_t AP_Mount_Topotek::calculate_crc(const uint8_t *cmd, uint8_t len) const
    1037             : {
    1038       17500 :     uint8_t crc = 0;
    1039      327346 :     for (uint16_t i = 0; i<len; i++) {
    1040      309846 :         crc += cmd[i];
    1041             :     }
    1042       17500 :     return(crc);
    1043             : }
    1044             : 
    1045             : // hexadecimal to character conversion
    1046       49764 : uint8_t AP_Mount_Topotek::hex2char(uint8_t data) const
    1047             : {
    1048       49764 :     if ((9 >= data)) {
    1049       32412 :         return (data + '0');
    1050             :     } else {
    1051       17352 :         return (data - 10 + 'A');
    1052             :     }
    1053             : }
    1054             : 
    1055             : 
    1056             : // send a fixed length packet
    1057         631 : bool AP_Mount_Topotek::send_fixedlen_packet(AddressByte address, const Identifier id, bool write, uint8_t value)
    1058             : {
    1059             :     uint8_t databuff[3];
    1060         631 :     hal.util->snprintf((char *)databuff, ARRAY_SIZE(databuff), "%02X", value);
    1061         631 :     return send_variablelen_packet(HeaderType::FIXED_LEN, address, id, write, databuff, ARRAY_SIZE(databuff)-1);
    1062             : }
    1063             : 
    1064             : // send variable length packet
    1065       14764 : bool AP_Mount_Topotek::send_variablelen_packet(HeaderType header, AddressByte address, const Identifier id, bool write, const uint8_t* databuff, uint8_t databuff_len)
    1066             : {
    1067             :     // exit immediately if not initialised
    1068       14764 :     if (!_initialised) {
    1069           0 :         return false;
    1070             :     }
    1071             : 
    1072             :     // calculate and sanity check packet size
    1073       14764 :     const uint16_t packet_size = AP_MOUNT_TOPOTEK_PACKETLEN_MIN + databuff_len;
    1074       14764 :     if (packet_size > AP_MOUNT_TOPOTEK_PACKETLEN_MAX) {
    1075             :         debug("send_packet data buff too large");
    1076           0 :         return false;
    1077             :     }
    1078             : 
    1079             :     // check for sufficient space in outgoing buffer
    1080       14764 :     if (_uart->txspace() < packet_size) {
    1081             :         debug("tx buffer full");
    1082           0 :         return false;
    1083             :     }
    1084             : 
    1085             :     // create buffer for holding outgoing packet
    1086       14764 :     uint8_t send_buff[packet_size];
    1087       14764 :     uint8_t send_buff_ofs = 0;
    1088             : 
    1089             :     // packet header (bytes 0 ~ 2)
    1090       14764 :     send_buff[send_buff_ofs++] = '#';
    1091       14764 :     send_buff[send_buff_ofs++] = (header == HeaderType::FIXED_LEN) ? 'T' : 't';
    1092       14764 :     send_buff[send_buff_ofs++] = (header == HeaderType::FIXED_LEN) ? 'P' : 'p';
    1093             : 
    1094             :     // address (bytes 3, 4)
    1095       14764 :     send_buff[send_buff_ofs++] = (uint8_t)source_address();
    1096       14764 :     send_buff[send_buff_ofs++] = (uint8_t)address;
    1097             : 
    1098             :     // data length (byte 5)
    1099       14764 :     send_buff[send_buff_ofs++] = hex2char(databuff_len);
    1100             : 
    1101             :     // control byte (byte 6)
    1102       14764 :     send_buff[send_buff_ofs++] = write ? (uint8_t)ControlByte::WRITE : (uint8_t)ControlByte::READ;
    1103             : 
    1104             :     // identified (bytes 7 ~ 9)
    1105       14764 :     send_buff[send_buff_ofs++] = id[0];
    1106       14764 :     send_buff[send_buff_ofs++] = id[1];
    1107       14764 :     send_buff[send_buff_ofs++] = id[2];
    1108             : 
    1109             :     // data
    1110       14764 :     if (databuff_len != 0) {
    1111       14764 :         memcpy(&send_buff[send_buff_ofs], databuff, databuff_len);
    1112       14764 :         send_buff_ofs += databuff_len;
    1113             :     }
    1114             : 
    1115             :     // crc
    1116       14764 :     uint8_t crc = calculate_crc(send_buff, send_buff_ofs);
    1117       14764 :     send_buff[send_buff_ofs++] = hex2char((crc >> 4) & 0x0f);
    1118       14764 :     send_buff[send_buff_ofs++] = hex2char(crc & 0x0f);
    1119             : 
    1120             :     // send packet
    1121       14764 :     _uart->write(send_buff, send_buff_ofs);
    1122       14764 :     return true;
    1123       14764 : }
    1124             : 
    1125             : // set gimbal's lock vs follow mode
    1126             : // lock should be true if gimbal should maintain an earth-frame target
    1127             : // lock is false to follow / maintain a body-frame target
    1128        2082 : bool AP_Mount_Topotek::set_gimbal_lock(bool lock)
    1129             : {
    1130        2082 :     if (_last_lock == lock) {
    1131        2082 :         return true;
    1132             :     }
    1133             : 
    1134             :     // send message and update lock state
    1135           0 :     if (send_fixedlen_packet(AddressByte::GIMBAL, AP_MOUNT_TOPOTEK_ID3CHAR_GIMBAL_MODE, true, lock ? 6 : 7)) {
    1136           0 :         _last_lock = lock;
    1137           0 :         return true;
    1138             :     }
    1139           0 :     return false;
    1140             : }
    1141             : 
    1142             : #endif // HAL_MOUNT_TOPOTEK_ENABLED

Generated by: LCOV version 1.14