LCOV - code coverage report
Current view: top level - SITL - SIM_Viewpro.cpp (source / functions) Hit Total Coverage
Test: lcov.info Lines: 158 173 91.3 %
Date: 2026-09-09 11:02:08 Functions: 6 6 100.0 %

          Line data    Source code
       1             : /*
       2             :    This program is free software: you can redistribute it and/or modify
       3             :    it under the terms of the GNU General Public License as published by
       4             :    the Free Software Foundation, either version 3 of the License, or
       5             :    (at your option) any later version.
       6             : 
       7             :    This program is distributed in the hope that it will be useful,
       8             :    but WITHOUT ANY WARRANTY; without even the implied warranty of
       9             :    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      10             :    GNU General Public License for more details.
      11             : 
      12             :    You should have received a copy of the GNU General Public License
      13             :    along with this program.  If not, see <http://www.gnu.org/licenses/>.
      14             :  */
      15             : /*
      16             :   Simulator for Viewpro gimbal
      17             : */
      18             : 
      19             : #include "SIM_config.h"
      20             : 
      21             : #if AP_SIM_VIEWPRO_ENABLED
      22             : 
      23             : #include "SIM_Viewpro.h"
      24             : #include "SIM_Aircraft.h"
      25             : #include <AP_Math/AP_Math.h>
      26             : #include <errno.h>
      27             : 
      28             : using namespace SITL;
      29             : 
      30             : // scalar from AP_Mount_Viewpro.cpp
      31             : #define VIEWPRO_DEG_TO_OUTPUT  (65536.0f / 360.0f)
      32             : 
      33      249745 : void Viewpro::update(const Aircraft &aircraft)
      34             : {
      35             :     // Drive GimbalSim toward the angles commanded via A1 packets.
      36             :     // The A1 wire encoding is: pitch_raw = -pitch_deg * DEG_TO_OUTPUT,
      37             :     // yaw_raw = yaw_deg * DEG_TO_OUTPUT.  Invert to recover target radians.
      38             :     {
      39             :         static constexpr float OUTPUT_TO_RAD = (360.0f / 65536.0f) * (float)DEG_TO_RAD;
      40      249745 :         const float target_pitch_rad = -_target_pitch_raw * OUTPUT_TO_RAD;
      41      249745 :         const float target_yaw_rad   =  _target_yaw_raw   * OUTPUT_TO_RAD;
      42             : 
      43      249745 :         Vector3f ja;
      44      249745 :         gimbal.get_joint_angles(ja);
      45             : 
      46             :         // P-gain: drives gimbal to commanded angle with ~0.1 s time constant.
      47             :         // joint_angles.y = pitch (negative = down), .z = azimuth.
      48      249745 :         Matrix3f gimbal_dcm;
      49      249745 :         gimbal.get_dcm(gimbal_dcm);
      50      249745 :         const Vector3f vehicle_rate_gimbal = gimbal_dcm.transposed() * aircraft.get_dcm() * aircraft.get_gyro();
      51             : 
      52             :         static constexpr float GAIN = 10.0f;
      53      249745 :         gimbal.set_demanded_rates(Vector3f(
      54      249745 :             vehicle_rate_gimbal.x,
      55      249745 :             vehicle_rate_gimbal.y + (target_pitch_rad - ja.y) * GAIN,
      56      249745 :             vehicle_rate_gimbal.z + (target_yaw_rad   - ja.z) * GAIN));
      57             :     }
      58             : 
      59      249745 :     gimbal.update(aircraft);
      60      249745 :     update_input();
      61             : 
      62             :     // send attitude at 10 Hz
      63      249745 :     const uint32_t now_ms = AP_HAL::millis();
      64      249745 :     if (now_ms - _last_attitude_ms >= 100) {
      65        2080 :         _last_attitude_ms = now_ms;
      66        2080 :         send_t1_f1_b1_d1();
      67             :     }
      68      249745 : }
      69             : 
      70             : /*
      71             :   read bytes from the autopilot and process them byte-by-byte through the
      72             :   Viewpro protocol state machine.
      73             : 
      74             :   The driver discards header bytes without storing them.  After all three
      75             :   header bytes are consumed the internal packet buffer starts fresh:
      76             :     _buf[0]   = length+frame_counter byte  (bits 0-5 = body_length)
      77             :     _buf[1]   = frame_id byte
      78             :     _buf[2+]  = data bytes
      79             :     _buf[N]   = CRC byte (last)
      80             : 
      81             :   body_length = 1(length byte) + 1(frame_id) + data_len + 1(crc)
      82             :   => data_len = body_length - 3
      83             : */
      84      249745 : void Viewpro::update_input()
      85             : {
      86             :     // read new bytes into a scratch buffer; process one at a time
      87             :     uint8_t scratch[128];
      88      249745 :     const ssize_t n = read_from_autopilot((char*)scratch, sizeof(scratch));
      89      249745 :     if (n < 0) {
      90           0 :         if (errno != EAGAIN && errno != EWOULDBLOCK && errno != 0) {
      91           0 :             AP_HAL::panic("Failed to read from autopilot");
      92             :         }
      93           0 :         return;
      94             :     }
      95             : 
      96      390049 :     for (ssize_t i = 0; i < n; i++) {
      97      140304 :         const uint8_t b = scratch[i];
      98             : 
      99      140304 :         switch (_parse_state) {
     100        6094 :         case ParseState::HEADER1:
     101        6094 :             if (b == HEADER1) {
     102        6094 :                 _buflen = 0;
     103        6094 :                 _parse_state = ParseState::HEADER2;
     104             :             }
     105        6094 :             break;
     106             : 
     107        6094 :         case ParseState::HEADER2:
     108        6094 :             if (b == HEADER2) {
     109        6094 :                 _buflen = 0;
     110        6094 :                 _parse_state = ParseState::HEADER3;
     111             :             } else {
     112           0 :                 _buflen = 0;
     113           0 :                 _parse_state = ParseState::HEADER1;
     114             :             }
     115        6094 :             break;
     116             : 
     117        6094 :         case ParseState::HEADER3:
     118        6094 :             if (b == HEADER3) {
     119        6094 :                 _buflen = 0;
     120        6094 :                 _parse_state = ParseState::LENGTH;
     121             :             } else {
     122           0 :                 _buflen = 0;
     123           0 :                 _parse_state = ParseState::HEADER1;
     124             :             }
     125        6094 :             break;
     126             : 
     127        6094 :         case ParseState::LENGTH: {
     128        6094 :             const uint8_t body_length = b & 0x3F;
     129        6094 :             if (body_length < 3) {
     130           0 :                 _buflen = 0;
     131           0 :                 _parse_state = ParseState::HEADER1;
     132           0 :                 break;
     133             :             }
     134        6094 :             _data_len = body_length - 3;
     135        6094 :             _buf[_buflen++] = b;    // _buf[0] = length+fc byte
     136        6094 :             _parse_state = ParseState::FRAMEID;
     137        6094 :             break;
     138             :         }
     139             : 
     140        6094 :         case ParseState::FRAMEID:
     141        6094 :             _data_bytes_received = 0;
     142        6094 :             _buf[_buflen++] = b;    // _buf[1] = frame_id
     143        6094 :             _parse_state = (_data_len > 0) ? ParseState::DATA : ParseState::CHECKSUM;
     144        6094 :             break;
     145             : 
     146      103740 :         case ParseState::DATA:
     147      103740 :             if (_buflen < PACKETLEN_MAX) {
     148      103740 :                 _buf[_buflen++] = b;
     149             :             }
     150      103740 :             _data_bytes_received++;
     151      103740 :             if (_data_bytes_received >= _data_len) {
     152        6094 :                 _parse_state = ParseState::CHECKSUM;
     153             :             }
     154      103740 :             break;
     155             : 
     156        6094 :         case ParseState::CHECKSUM: {
     157        6094 :             if (_buflen < PACKETLEN_MAX) {
     158        6094 :                 _buf[_buflen++] = b;
     159             :             }
     160             :             // verify CRC: XOR of _buf[0.._buflen-2] (all bytes except the crc byte itself)
     161        6094 :             const uint8_t expected_crc = crc_xor_of_bytes(_buf, _buflen - 1);
     162        6094 :             if (expected_crc == b) {
     163        6094 :                 dispatch_packet();
     164             :             }
     165        6094 :             _buflen = 0;
     166        6094 :             _parse_state = ParseState::HEADER1;
     167        6094 :             break;
     168             :         }
     169             :         }
     170             :     }
     171             : }
     172             : 
     173        6094 : void Viewpro::dispatch_packet()
     174             : {
     175             :     // _buf[1] = frame_id; _buf[2+] = data (data_start = 2)
     176        6094 :     const FrameId frame_id = (FrameId)_buf[1];
     177             : 
     178        6094 :     switch (frame_id) {
     179        2056 :     case FrameId::HANDSHAKE:
     180        2056 :         send_t1_f1_b1_d1();
     181        2056 :         break;
     182             : 
     183           2 :     case FrameId::U:
     184             :         // _buf[2] = data[0] = CommConfigCmd
     185           2 :         if (_data_len >= 1) {
     186           2 :             send_v_response((CommConfigCmd)_buf[2]);
     187             :         }
     188           2 :         break;
     189             : 
     190        2056 :     case FrameId::A1:
     191             :         // _buf layout: [0]=length_fc, [1]=frame_id, [2]=servo_status,
     192             :         //              [3..4]=yaw big-endian, [5..6]=pitch big-endian
     193             :         // Store raw int16 values so the driver round-trips them without
     194             :         // any additional float rounding.
     195        2056 :         if (_data_len >= 5) {
     196        2056 :             _target_yaw_raw   = (int16_t)(((uint16_t)_buf[3] << 8) | _buf[4]);
     197        2056 :             _target_pitch_raw = (int16_t)(((uint16_t)_buf[5] << 8) | _buf[6]);
     198             :         }
     199        2056 :         break;
     200        1980 :     case FrameId::C1:
     201             :     case FrameId::C2:
     202             :     case FrameId::E1:
     203             :     case FrameId::E2:
     204             :     case FrameId::M_AHRS:
     205             :     case FrameId::HEARTBEAT:
     206             :         // absorb; gimbal physics is updated via update()
     207        1980 :         break;
     208             : 
     209           0 :     default:
     210           0 :         break;
     211             :     }
     212        6094 : }
     213             : 
     214             : /*
     215             :   send T1_F1_B1_D1 (0x40) packet with current gimbal attitude.
     216             : 
     217             :   Driver reads angles using _msg_buff_data_start=2:
     218             :     data[22]     = tracking status (bits 3-4; 0 = stopped)
     219             :     data[23]     = servo_status(upper 4 bits) | roll_12bit_MSByte(lower 4 bits)
     220             :     data[24]     = roll LSByte   → roll_deg = 12-bit * 180/4095 - 90
     221             :     data[25..26] = yaw int16 big-endian  → yaw_deg = val * 360/65536
     222             :     data[27..28] = pitch int16 big-endian  → pitch_deg = val * 360/65536  (driver negates)
     223             :     data[29]     = image sensor (bits 0-2; stored as val+1; 0→EO1)
     224             :     data[32]     = recording status (bits 0-2; 0=stopped)
     225             :     data[33..34] = rangefinder × 0.1 m
     226             :     data[39..40] = zoom × 0.1
     227             : */
     228        4136 : void Viewpro::send_t1_f1_b1_d1()
     229             : {
     230             :     static constexpr uint8_t DATA_LEN = 41;
     231             :     // databuff: frame_id (1 byte) + data (DATA_LEN bytes)
     232        4136 :     uint8_t databuff[1 + DATA_LEN] {};
     233        4136 :     databuff[0] = (uint8_t)FrameId::T1_F1_B1_D1;
     234             : 
     235        4136 :     uint8_t *data = &databuff[1];
     236             : 
     237             :     // Report actual GimbalSim joint angles.
     238             :     // joint_angles.y = pitch (negative = down), .z = azimuth.
     239             :     // Wire encoding: pitch_out = -pitch_deg * DEG_TO_OUTPUT (driver negates on read).
     240             :     //                yaw_out   =  yaw_deg   * DEG_TO_OUTPUT.
     241             :     // roll: 12-bit value where 2048 (midpoint of 0..4095) ≈ 0 deg (actual = +0.022 deg,
     242             :     //       within the neutral_tol_deg=0.05 used by MountViewPro).
     243             :     // Pitch clamp to ±16380 (±89.978 deg): at exactly ±90 deg (raw ±16384) the float32
     244             :     // from_euler quaternion with the small roll (~0.022 deg) has DCM[2][0] > 1.0 in
     245             :     // float64 (Python), triggering pymavlink gimbal-lock → 180 deg instead of ±90 deg.
     246        4136 :     Vector3f ja;
     247        4136 :     gimbal.get_joint_angles(ja);
     248        4136 :     const int16_t yaw_out   = (int16_t)(degrees(ja.z) * VIEWPRO_DEG_TO_OUTPUT);
     249        4136 :     const int16_t pitch_out = (int16_t)constrain_int16(
     250        4136 :         (int16_t)(-degrees(ja.y) * VIEWPRO_DEG_TO_OUTPUT),
     251             :         -16380, 16380);
     252        4136 :     const uint16_t roll_out = 2048U;
     253             : 
     254             :     // bytes 0..21: T1/F1 fields — leave as zero
     255        4136 :     data[22] = 0x00;    // tracking stopped
     256             : 
     257        4136 :     data[23] = (uint8_t)((roll_out >> 8) & 0x0F);   // lower nibble = roll MSB; upper nibble = 0 (servo status)
     258        4136 :     data[24] = (uint8_t)(roll_out & 0xFF);
     259             : 
     260        4136 :     data[25] = (uint8_t)((uint16_t)yaw_out >> 8);
     261        4136 :     data[26] = (uint8_t)((uint16_t)yaw_out & 0xFF);
     262             : 
     263        4136 :     data[27] = (uint8_t)((uint16_t)pitch_out >> 8);
     264        4136 :     data[28] = (uint8_t)((uint16_t)pitch_out & 0xFF);
     265             : 
     266        4136 :     data[29] = 0x00;    // image sensor bits 0-2 = 0 → driver stores as EO1
     267        4136 :     data[32] = 0x00;    // recording stopped
     268        4136 :     data[33] = 0x00;    // rangefinder MSB
     269        4136 :     data[34] = 0x00;    // rangefinder LSB
     270        4136 :     data[39] = 0x00;    // zoom MSB
     271        4136 :     data[40] = 0x0A;    // zoom LSB: 10 × 0.1 = 1.0× zoom
     272             : 
     273        4136 :     send_packet(databuff, sizeof(databuff));
     274        4136 : }
     275             : 
     276             : /*
     277             :   send V (0x02) response to a U communication config command.
     278             : 
     279             :   Firmware version response (QUERY_FIRMWARE_VER = 0xD0):
     280             :     data[0] = 0xD0 (echo of CommConfigCmd)
     281             :     data[1] = 'S'  prefix
     282             :     data[2..9] = "yyyymmdd" date string
     283             :     driver reads: major=atoi(data[4..5]), minor=atoi(data[6..7]), patch=atoi(data[8..9])
     284             :     Using "20220301": major=22, minor=03, patch=01
     285             : 
     286             :   Model name response (QUERY_MODEL = 0xE4):
     287             :     data[0] = 0xE4
     288             :     data[1+] = model name bytes
     289             :     driver: memcpy(_model_name, &data[1], data_bytes_received-1)
     290             : */
     291           2 : void Viewpro::send_v_response(CommConfigCmd cmd)
     292             : {
     293           2 :     uint8_t databuff[32] {};
     294             :     uint8_t total_len;
     295             : 
     296           2 :     databuff[0] = (uint8_t)FrameId::V;
     297             : 
     298           2 :     switch (cmd) {
     299           1 :     case CommConfigCmd::QUERY_FIRMWARE_VER:
     300           1 :         databuff[1] = (uint8_t)cmd;
     301           1 :         databuff[2] = 'S';          // prefix character
     302           1 :         databuff[3] = 'y';
     303           1 :         databuff[4] = 's';
     304           1 :         databuff[5] = '2';          // major hi: "20"
     305           1 :         databuff[6] = '0';
     306           1 :         databuff[7] = '2';          // minor hi: "22"
     307           1 :         databuff[8] = '2';
     308           1 :         databuff[9] = '0';          // patch hi: "01"
     309           1 :         databuff[10] = '1';
     310           1 :         total_len = 11;
     311           1 :         break;
     312             : 
     313           1 :     case CommConfigCmd::QUERY_MODEL:
     314             :         // datasheet specifies a 10-byte model name field in the response
     315           1 :         databuff[1] = (uint8_t)cmd;
     316           1 :         databuff[2] = 'S';
     317           1 :         databuff[3] = 'I';
     318           1 :         databuff[4] = 'M';
     319           1 :         databuff[5] = '_';
     320           1 :         databuff[6] = 'V';
     321           1 :         databuff[7] = 'P';
     322           1 :         databuff[8] = 0;
     323           1 :         databuff[9] = 0;
     324           1 :         databuff[10] = 0;
     325           1 :         databuff[11] = 0;
     326           1 :         total_len = 12;
     327           1 :         break;
     328             : 
     329           0 :     default:
     330           0 :         return;
     331             :     }
     332             : 
     333           2 :     send_packet(databuff, total_len);
     334             : }
     335             : 
     336             : /*
     337             :   build and write a complete Viewpro packet to the autopilot.
     338             :   databuff[0] = frame_id, databuff[1+] = data payload.
     339             : 
     340             :   Wire format:
     341             :     0x55 0xAA 0xDC [length_fc] [databuff...] [crc]
     342             :   where:
     343             :     length_fc bits 6-7 = frame counter (0-3, increments per packet)
     344             :     length_fc bits 0-5 = databuff_len + 2  (= body_length)
     345             :     crc = XOR of [length_fc .. last_databuff_byte]
     346             : */
     347        4138 : void Viewpro::send_packet(const uint8_t *databuff, uint8_t databuff_len)
     348             : {
     349             :     // total wire bytes: 3 (header) + 1 (length_fc) + databuff_len + 1 (crc)
     350        4138 :     const uint8_t wire_len = 5 + databuff_len;
     351        4138 :     if (wire_len > (uint8_t)(PACKETLEN_MAX + 4)) {
     352           0 :         return;
     353             :     }
     354             : 
     355             :     uint8_t pkt[PACKETLEN_MAX + 4];
     356        4138 :     uint8_t ofs = 0;
     357             : 
     358        4138 :     pkt[ofs++] = HEADER1;
     359        4138 :     pkt[ofs++] = HEADER2;
     360        4138 :     pkt[ofs++] = HEADER3;
     361             : 
     362        4138 :     _frame_counter = (_frame_counter + 1) & 0x03;
     363        4138 :     const uint8_t body_length = (uint8_t)(databuff_len + 2);
     364        4138 :     pkt[ofs++] = (uint8_t)((_frame_counter << 6) | (body_length & 0x3F));
     365             : 
     366      177873 :     for (uint8_t i = 0; i < databuff_len; i++) {
     367      173735 :         pkt[ofs++] = databuff[i];
     368             :     }
     369             : 
     370             :     // CRC: XOR of bytes [3]..[ofs-1] (length_fc through last data byte)
     371        4138 :     pkt[ofs] = crc_xor_of_bytes(&pkt[3], ofs - 3);
     372        4138 :     ofs++;
     373             : 
     374        4138 :     write_to_autopilot((const char*)pkt, ofs);
     375             : }
     376             : 
     377             : 
     378             : #endif  // AP_SIM_VIEWPRO_ENABLED

Generated by: LCOV version 1.14