LCOV - code coverage report
Current view: top level - AP_MSP - AP_MSP_Telem_Backend.cpp (source / functions) Hit Total Coverage
Test: lcov.info Lines: 536 764 70.2 %
Date: 2026-09-09 11:02:08 Functions: 49 66 74.2 %

          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             : #include <AP_AHRS/AP_AHRS.h>
      17             : #include <AP_Baro/AP_Baro.h>
      18             : #include <AP_Airspeed/AP_Airspeed.h>
      19             : #include <AP_BattMonitor/AP_BattMonitor.h>
      20             : #include <AP_Compass/AP_Compass.h>
      21             : #include <AP_ESC_Telem/AP_ESC_Telem.h>
      22             : #include <RC_Channel/RC_Channel.h>
      23             : #include <AP_Common/AP_FWVersion.h>
      24             : #include <AP_GPS/AP_GPS.h>
      25             : #include <AP_Notify/AP_Notify.h>
      26             : #include <AP_OpticalFlow/AP_OpticalFlow.h>
      27             : #include <AP_RangeFinder/AP_RangeFinder.h>
      28             : #include <AP_RSSI/AP_RSSI.h>
      29             : #include <AP_RTC/AP_RTC.h>
      30             : #include <AP_VideoTX/AP_VideoTX.h>
      31             : #include <GCS_MAVLink/GCS.h>
      32             : #include <AP_Vehicle/AP_Vehicle.h>
      33             : 
      34             : #include "AP_MSP.h"
      35             : #include "AP_MSP_Telem_Backend.h"
      36             : 
      37             : #include <ctype.h>
      38             : #include <stdio.h>
      39             : 
      40             : #if HAL_MSP_ENABLED
      41             : 
      42             : extern const AP_HAL::HAL& hal;
      43             : constexpr uint8_t AP_MSP_Telem_Backend::arrows[8];
      44             : 
      45             : using namespace MSP;
      46             : 
      47           6 : AP_MSP_Telem_Backend::AP_MSP_Telem_Backend(AP_HAL::UARTDriver *uart) : AP_RCTelemetry(MSP_TIME_SLOT_MAX)
      48             : {
      49           6 :     _msp_port.uart = uart;
      50           6 : }
      51             : 
      52             : /*
      53             :   Scheduler helper
      54             :  */
      55           6 : void AP_MSP_Telem_Backend::setup_wfq_scheduler(void)
      56             : {
      57             :     // initialize packet weights for the WFQ scheduler
      58             :     // priority[i] = 1/_scheduler.packet_weight[i]
      59             :     // rate[i] = LinkRate * ( priority[i] / (sum(priority[1-n])) )
      60             : 
      61           6 :     set_scheduler_entry(EMPTY_SLOT, 50, 50);          // nothing to send
      62           6 :     set_scheduler_entry(NAME, 200, 200);              // 5Hz  12 chars string used for general purpose text messages
      63           6 :     set_scheduler_entry(STATUS, 500, 500);            // 2Hz  flightmode
      64           6 :     set_scheduler_entry(CONFIG, 200, 200);            // 5Hz  OSD item positions
      65           6 :     set_scheduler_entry(RAW_GPS, 250, 250);           // 4Hz  GPS lat/lon
      66           6 :     set_scheduler_entry(COMP_GPS, 250, 250);          // 4Hz  home direction and distance
      67           6 :     set_scheduler_entry(ATTITUDE, 200, 200);          // 5Hz  attitude
      68           6 :     set_scheduler_entry(ALTITUDE, 250, 250);          // 4Hz  altitude(cm) and velocity(cm/s)
      69           6 :     set_scheduler_entry(ANALOG, 250, 250);            // 4Hz  rssi + batt
      70             : #if AP_BATTERY_ENABLED
      71           6 :     set_scheduler_entry(BATTERY_STATE, 500, 500);     // 2Hz  battery
      72             : #endif
      73             : #if HAL_WITH_ESC_TELEM
      74           6 :     set_scheduler_entry(ESC_SENSOR_DATA, 500, 500);   // 2Hz  ESC telemetry
      75             : #endif
      76           6 :     set_scheduler_entry(RTC_DATETIME, 1000, 1000);    // 1Hz  RTC
      77             : #if AP_MSP_VIDEOTX_ENABLED
      78           6 :     set_scheduler_entry(VTX_PARAMETERS, 1000, 1000);  // 1Hz  VTX
      79             : #endif
      80           6 : }
      81             : 
      82             : /*
      83             :  * init - perform required initialisation
      84             :  */
      85           6 : bool AP_MSP_Telem_Backend::init()
      86             : {
      87           6 :     enable_warnings();
      88           6 :     return AP_RCTelemetry::init();
      89             : }
      90             : 
      91           3 : bool AP_MSP_Telem_Backend::init_uart()
      92             : {
      93           3 :     if (_msp_port.uart != nullptr)  {
      94             :         // re-init port here for use in this thread
      95           3 :         _msp_port.uart->begin(0);
      96           3 :         return true;
      97             :     }
      98           0 :     return false;
      99             : }
     100             : 
     101        9158 : void AP_MSP_Telem_Backend::process_outgoing_data()
     102             : {
     103        9158 :     if (is_scheduler_enabled()) {
     104        2373 :         AP_RCTelemetry::run_wfq_scheduler();
     105             :     }
     106        9158 : }
     107             : 
     108             : /*
     109             :   Scheduler helper
     110             :  */
     111        3116 : bool AP_MSP_Telem_Backend::is_packet_ready(uint8_t idx, bool queue_empty)
     112             : {
     113        3116 :     switch (idx) {
     114        1368 :     case EMPTY_SLOT:        // empty slot
     115             :     case NAME:              // used for status_text messages
     116             :     case STATUS:            // flightmode
     117             :     case CONFIG:            // OSD config
     118             :     case RAW_GPS:           // lat,lon, speed
     119             :     case COMP_GPS:          // home dir,dist
     120             :     case ATTITUDE:          // Attitude
     121             :     case ALTITUDE:          // Altitude and Vario
     122             :     case ANALOG:            // Rssi, Battery, mAh, Current
     123             : #if AP_BATTERY_ENABLED
     124             :     case BATTERY_STATE:     // voltage, capacity, current, mAh
     125             : #endif
     126             : #if HAL_WITH_ESC_TELEM
     127             :     case ESC_SENSOR_DATA:   // esc temp + rpm
     128             : #endif
     129             :     case RTC_DATETIME:      // RTC
     130        1368 :         return true;
     131             : #if AP_MSP_VIDEOTX_ENABLED
     132        1748 :     case VTX_PARAMETERS:    // VTX control
     133        1748 :         return AP::vtx().have_params_changed();
     134             : #endif
     135           0 :     default:
     136           0 :         return false;
     137             :     }
     138             : }
     139             : 
     140             : /*
     141             :   Invoked at each scheduler step
     142             :  */
     143        1232 : void AP_MSP_Telem_Backend::process_packet(uint8_t idx)
     144             : {
     145        1232 :     if (idx == EMPTY_SLOT) {
     146         436 :         return;
     147             :     }
     148             : 
     149             : #if AP_MSP_VIDEOTX_ENABLED
     150             :     // the VTX config is pushed on change rather than every tick, and only once
     151             :     // the air unit has completed its boot handshake
     152         849 :     if (msp_packet_type_map[idx] == MSP_VTX_CONFIG && !vtx_should_push_config()) {
     153          53 :         return;
     154             :     }
     155             : #endif
     156             : 
     157         796 :     uint8_t out_buf[MSP_PORT_OUTBUF_SIZE] {};
     158             : 
     159         796 :     msp_packet_t reply = {
     160             :         .buf = { .ptr = out_buf, .end = MSP_ARRAYEND(out_buf), },
     161         796 :         .cmd = (int16_t)msp_packet_type_map[idx],
     162             :         .flags = 0,
     163             :         .result = 0,
     164         796 :     };
     165         796 :     uint8_t *out_buf_head = reply.buf.ptr;
     166             : 
     167         796 :     msp_process_out_command(msp_packet_type_map[idx], nullptr, &reply.buf);
     168         796 :     uint32_t len = reply.buf.ptr - &out_buf[0];
     169         796 :     sbuf_switch_to_reader(&reply.buf, out_buf_head); // change streambuf direction
     170             : 
     171             :     // the negotiated version is only known once the peer has sent us something;
     172             :     // betaflight pushes the VTX config as MSP v2 native, so match that for an
     173             :     // MSP VTX which otherwise ignores the unsolicited v1 frame
     174         796 :     msp_version_e version = _msp_port.msp_version;
     175             : #if AP_MSP_VIDEOTX_ENABLED
     176         796 :     if (msp_packet_type_map[idx] == MSP_VTX_CONFIG) {
     177           0 :         version = MSP_V2_NATIVE;
     178             :     }
     179             : #endif
     180         796 :     if (len > 0) {
     181             :         // don't send zero length packets
     182         796 :         msp_serial_encode(&_msp_port, &reply, version);
     183             :     }
     184             : 
     185         796 :     _msp_port.c_state = MSP_IDLE;
     186             : }
     187             : 
     188             : #if AP_BATTERY_ENABLED
     189         126 : uint8_t AP_MSP_Telem_Backend::calc_cell_count(const float battery_voltage)
     190             : {
     191         126 :     return floorf((battery_voltage / CELLFULL) + 1);
     192             : }
     193             : #endif
     194             : 
     195          84 : float AP_MSP_Telem_Backend::get_vspeed_ms(void) const
     196             : {
     197             :     {
     198             :         // release semaphore as soon as possible
     199          84 :         AP_AHRS &_ahrs = AP::ahrs();
     200          84 :         Vector3f v {};
     201          84 :         WITH_SEMAPHORE(_ahrs.get_semaphore());
     202          84 :         if (_ahrs.get_velocity_NED(v)) {
     203          49 :             return -v.z;
     204             :         }
     205          84 :     }
     206          35 :     AP_Baro &_baro = AP::baro();
     207          35 :     WITH_SEMAPHORE(_baro.get_semaphore());
     208          35 :     return _baro.get_climb_rate();
     209          35 : }
     210             : 
     211        4407 : void AP_MSP_Telem_Backend::update_home_pos(home_state_t &home_state)
     212             : {
     213        4407 :     AP_AHRS &_ahrs = AP::ahrs();
     214        4407 :     WITH_SEMAPHORE(_ahrs.get_semaphore());
     215        4407 :     Location loc;
     216             :     float alt;
     217        4407 :     if (_ahrs.get_location(loc) && _ahrs.home_is_set()) {
     218        2095 :         const Location &home_loc = _ahrs.get_home();
     219        2095 :         home_state.home_distance_m = home_loc.get_distance(loc);
     220        2095 :         home_state.home_bearing_cd = loc.get_bearing_to(home_loc);
     221             :     } else {
     222        2312 :         home_state.home_distance_m = 0;
     223        2312 :         home_state.home_bearing_cd = 0;
     224             :     }
     225        4407 :     _ahrs.get_relative_position_D_home(alt);
     226        4407 :     home_state.rel_altitude_cm = -alt * 100;
     227        4407 :     home_state.home_is_set = _ahrs.home_is_set();
     228        4407 : }
     229             : 
     230             : #if AP_GPS_ENABLED
     231        4323 : void AP_MSP_Telem_Backend::update_gps_state(gps_state_t &gps_state)
     232             : {
     233        4323 :     AP_GPS& gps = AP::gps();
     234             : 
     235        4323 :     memset(&gps_state, 0, sizeof(gps_state));
     236             : 
     237        4323 :     WITH_SEMAPHORE(gps.get_semaphore());
     238        4323 :     gps_state.fix_type = gps.status() >= AP_GPS_FixType::FIX_3D? 2:0;
     239        4323 :     gps_state.num_sats = gps.num_sats();
     240             : 
     241        4323 :     if (gps_state.fix_type > 0) {
     242        3112 :         const Location &loc = AP::gps().location(); //get gps instance 0
     243        3112 :         gps_state.lat = loc.lat;
     244        3112 :         gps_state.lon = loc.lng;
     245        3112 :         gps_state.alt_m = loc.alt/100; // 1m resolution
     246        3112 :         gps_state.speed_cms = gps.ground_speed() * 100;
     247        3112 :         gps_state.ground_course_dd = gps.ground_course_cd() / 10;
     248             :     }
     249        4323 : }
     250             : #endif
     251             : 
     252             : #if AP_BATTERY_ENABLED
     253         126 : void AP_MSP_Telem_Backend::update_battery_state(battery_state_t &battery_state)
     254             : {
     255         126 :     memset(&battery_state, 0, sizeof(battery_state));
     256             : 
     257         126 :     const AP_BattMonitor &_battery = AP::battery();
     258         126 :     if (!_battery.current_amps(battery_state.batt_current_a)) {
     259           0 :         battery_state.batt_current_a = 0;
     260             :     }
     261         126 :     if (!_battery.consumed_mah(battery_state.batt_consumed_mah)) {
     262           0 :         battery_state.batt_consumed_mah = 0;
     263             :     }
     264         126 :     battery_state.batt_voltage_v =_battery.voltage();
     265         126 :     battery_state.batt_capacity_mah = _battery.pack_capacity_mah();
     266             : 
     267         126 :     const AP_Notify& notify = AP::notify();
     268         126 :     if (notify.flags.failsafe_battery) {
     269           0 :         battery_state.batt_state = MSP_BATTERY_CRITICAL;
     270             :     } else {
     271         126 :         battery_state.batt_state = MSP_BATTERY_OK;
     272             :     }
     273             :     // detect cellcount and update only if we get a higher values, we do not want to update it while discharging
     274         126 :     uint8_t cc = calc_cell_count(battery_state.batt_voltage_v);
     275         126 :     if (cc > battery_state.batt_cellcount) {
     276         126 :         battery_state.batt_cellcount = cc;
     277             :     }
     278         126 : }
     279             : #endif  // AP_BATTERY_ENABLED
     280             : 
     281           0 : void AP_MSP_Telem_Backend::update_airspeed(airspeed_state_t &airspeed_state)
     282             : {
     283           0 :     AP_AHRS &ahrs = AP::ahrs();
     284           0 :     WITH_SEMAPHORE(ahrs.get_semaphore());
     285           0 :     airspeed_state.airspeed_have_estimate = ahrs.airspeed_EAS(airspeed_state.airspeed_estimate_ms);
     286           0 :     if (!airspeed_state.airspeed_have_estimate) {
     287           0 :         airspeed_state.airspeed_estimate_ms = 0.0;
     288             :     }
     289           0 : }
     290             : 
     291             : /*
     292             :     MSP OSDs can display up to MSP_TXT_VISIBLE_CHARS chars (UTF8 characters are supported)
     293             :     We display the flight mode string either with or without wind state
     294             : */
     295          17 : void AP_MSP_Telem_Backend::update_flight_mode_str(char *flight_mode_str, uint8_t size, bool wind_enabled)
     296             : {
     297             : #if OSD_ENABLED
     298          17 :     AP_OSD *osd = AP::osd();
     299          17 :     if (osd == nullptr) {
     300           0 :         return;
     301             :     }
     302             : #endif
     303          17 :     AP_Notify *notify = AP_Notify::get_singleton();
     304          17 :     if (notify == nullptr) {
     305           0 :         return;
     306             :     }
     307             :     // clear
     308          17 :     memset(flight_mode_str, 0, size);
     309             : 
     310          17 :     if (wind_enabled) {
     311             :         /*
     312             :           Wind is rendered next to the current flight mode, for the direction we use an UTF8 arrow (bytes 0xE286[nn])
     313             :           example: MANU 4m/s ↗
     314             :         */
     315           0 :         AP_AHRS &ahrs = AP::ahrs();
     316           0 :         Vector3f v;
     317             :         {
     318           0 :             WITH_SEMAPHORE(ahrs.get_semaphore());
     319             :             // use the estimate even if it is not marked valid, to
     320             :             // preserve existing behaviour
     321           0 :             IGNORE_RETURN(ahrs.get_wind(v));
     322           0 :         }
     323           0 :         bool invert_wind = false;
     324             : #if OSD_ENABLED
     325           0 :         AP_MSP *msp = AP::msp();
     326           0 :         if (msp == nullptr) {
     327           0 :             return;
     328             :         }
     329             : 
     330           0 :         invert_wind = osd->screen[msp->_msp_status.current_screen].check_option(AP_OSD::OPTION_INVERTED_WIND);
     331             : #endif
     332           0 :         if (invert_wind) {
     333           0 :             v = -v;
     334             :         }
     335           0 :         uint8_t units = OSD_UNIT_METRIC;
     336             : #if OSD_ENABLED
     337           0 :         units = osd->units == AP_OSD::UNITS_IMPERIAL ?  OSD_UNIT_IMPERIAL : OSD_UNIT_METRIC;
     338             : #endif
     339             :         // if needed convert m/s to ft/s
     340           0 :         const float v_length = (units == OSD_UNIT_METRIC) ? v.length() : v.length() * 3.28084;
     341           0 :         const char* unit = (units == OSD_UNIT_METRIC) ? "m/s" : "f/s";
     342             : 
     343           0 :         if (v_length > 1.0f) {
     344             : #if AP_VEHICLE_ENABLED
     345             :             float roll_rad, pitch_rad, yaw_rad;
     346           0 :             AP::vehicle()->get_osd_attitude_rad(roll_rad, pitch_rad, yaw_rad);
     347             : #else
     348             :             float yaw_rad = ahrs.get_yaw_rad();
     349             : #endif // AP_VEHICLE_ENABLED
     350             : 
     351           0 :             const int32_t angle = wrap_360_cd(rad_to_cd(atan2f(v.y, v.x)) - rad_to_cd(yaw_rad));
     352           0 :             const int32_t interval = 36000 / ARRAY_SIZE(arrows);
     353           0 :             uint8_t arrow = arrows[((angle + interval / 2) / interval) % ARRAY_SIZE(arrows)];
     354           0 :             snprintf(flight_mode_str, size, "%s %d%s%c%c%c", notify->get_flight_mode_str(),  (uint8_t)roundf(v_length), unit, 0xE2, 0x86, arrow);
     355             :         } else {
     356           0 :             snprintf(flight_mode_str, size, "%s ---%s", notify->get_flight_mode_str(), unit);
     357             :         }
     358             :     } else {
     359             :         /*
     360             :             Flight mode is rendered with simple mode flags
     361             :             examples:
     362             :                 MANU
     363             :                 MANU [S]
     364             :                 MANU [SS]
     365             :         */
     366             : #if HAL_GCS_ENABLED
     367          17 :         const char* simple_mode_str = gcs().simple_input_active() ? " [S]" : (gcs().supersimple_input_active() ? " [SS]" : "");
     368          17 :         snprintf(flight_mode_str, size, "%s%s", notify->get_flight_mode_str(), simple_mode_str);
     369             : #else
     370             :         snprintf(flight_mode_str, size, "%s", notify->get_flight_mode_str());
     371             : #endif
     372             :     }
     373             : }
     374             : 
     375           6 : void AP_MSP_Telem_Backend::enable_warnings()
     376             : {
     377           6 :     AP_MSP *msp = AP::msp();
     378           6 :     if (msp == nullptr) {
     379           0 :         return;
     380             :     }
     381           6 :     BIT_SET(msp->_osd_config.enabled_warnings, OSD_WARNING_FAIL_SAFE);
     382             : #if AP_BATTERY_ENABLED
     383           6 :     BIT_SET(msp->_osd_config.enabled_warnings, OSD_WARNING_BATTERY_CRITICAL);
     384             : #endif
     385             : }
     386             : 
     387        9158 : void AP_MSP_Telem_Backend::process_incoming_data()
     388             : {
     389        9158 :     if (_msp_port.uart == nullptr) {
     390           0 :         return;
     391             :     }
     392             : 
     393        9158 :     uint32_t numc = MIN(_msp_port.uart->available(), 1024U);
     394             : 
     395        9158 :     if (numc > 0) {
     396             :         // Process incoming bytes
     397        8217 :         while (numc-- > 0) {
     398        8116 :             const uint8_t c = _msp_port.uart->read();
     399        8116 :             msp_parse_received_data(&_msp_port, c);
     400             : 
     401        8116 :             if (_msp_port.c_state == MSP_COMMAND_RECEIVED) {
     402         217 :                 msp_process_received_command();
     403             :             }
     404             :         }
     405             :     }
     406             : }
     407             : 
     408             : /*
     409             :   send an MSP packet
     410             :  */
     411       21035 : void AP_MSP_Telem_Backend::msp_send_packet(uint16_t cmd, MSP::msp_version_e msp_version, const void *p, uint16_t size, bool is_request)
     412             : {
     413             :     uint8_t out_buf[MSP_PORT_OUTBUF_SIZE];
     414             : 
     415       21035 :     msp_packet_t pkt = {
     416             :         .buf = { .ptr = out_buf, .end = MSP_ARRAYEND(out_buf), },
     417       21035 :         .cmd = (int16_t)cmd,
     418             :         .flags = 0,
     419             :         .result = 0,
     420       21035 :     };
     421             : 
     422       21035 :     sbuf_write_data(&pkt.buf, p, size);
     423       21035 :     sbuf_switch_to_reader(&pkt.buf, &out_buf[0]);
     424       21035 :     msp_serial_encode(&_msp_port, &pkt, msp_version, is_request);
     425       21035 : }
     426             : 
     427             : /*
     428             :   ported from betaflight/src/main/msp/msp_serial.c
     429             :  */
     430         217 : void AP_MSP_Telem_Backend::msp_process_received_command()
     431             : {
     432             :     uint8_t out_buf[MSP_PORT_OUTBUF_SIZE];
     433             : 
     434         217 :     msp_packet_t reply = {
     435             :         .buf = { .ptr = out_buf, .end = MSP_ARRAYEND(out_buf), },
     436             :         .cmd = -1,
     437             :         .flags = 0,
     438             :         .result = 0,
     439         217 :     };
     440         217 :     uint8_t *out_buf_head = reply.buf.ptr;
     441             : 
     442         217 :     msp_packet_t command = {
     443         217 :         .buf = { .ptr = _msp_port.in_buf, .end = _msp_port.in_buf + _msp_port.data_size, },
     444         217 :         .cmd = (int16_t)_msp_port.cmd_msp,
     445         217 :         .flags = _msp_port.cmd_flags,
     446             :         .result = 0,
     447         217 :     };
     448             : 
     449         217 :     const MSPCommandResult status = msp_process_command(&command, &reply);
     450             : 
     451         217 :     if (status != MSP_RESULT_NO_REPLY) {
     452         141 :         sbuf_switch_to_reader(&reply.buf, out_buf_head); // change streambuf direction
     453         141 :         msp_serial_encode(&_msp_port, &reply, _msp_port.msp_version);
     454             :     }
     455             : 
     456         217 :     _msp_port.c_state = MSP_IDLE;
     457         217 : }
     458             : 
     459             : /*
     460             :   ported from inav/src/main/fc/fc_msp.c
     461             :   pull-mode telemetry
     462             :  */
     463         217 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_command(msp_packet_t *cmd, msp_packet_t *reply)
     464             : {
     465         217 :     MSPCommandResult ret = MSP_RESULT_ACK;
     466         217 :     sbuf_t *dst = &reply->buf;
     467         217 :     sbuf_t *src = &cmd->buf;
     468         217 :     const uint16_t cmd_msp = cmd->cmd;
     469             :     // initialize reply by default
     470         217 :     reply->cmd = cmd->cmd;
     471             : 
     472         217 :     if (MSP2_IS_SENSOR_MESSAGE(cmd_msp)) {
     473          76 :         ret = msp_process_sensor_command(cmd_msp, src);
     474             :     } else {
     475         141 :         ret = msp_process_out_command(cmd_msp, src, dst);
     476             :     }
     477             : 
     478             :     // Process DONT_REPLY flag
     479         217 :     if (cmd->flags & MSP_FLAG_DONT_REPLY) {
     480           0 :         ret = MSP_RESULT_NO_REPLY;
     481             :     }
     482             : 
     483         217 :     reply->result = ret;
     484         217 :     return ret;
     485             : }
     486             : 
     487         937 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_command(uint16_t cmd_msp, sbuf_t *src, sbuf_t *dst)
     488             : {
     489         937 :     switch (cmd_msp) {
     490           0 :     case MSP_API_VERSION:
     491           0 :         return msp_process_out_api_version(dst);
     492           0 :     case MSP_FC_VARIANT:
     493           0 :         return msp_process_out_fc_variant(dst);
     494           0 :     case MSP_FC_VERSION:
     495           0 :         return msp_process_out_fc_version(dst);
     496           0 :     case MSP_BOARD_INFO:
     497           0 :         return msp_process_out_board_info(dst);
     498           0 :     case MSP_BUILD_INFO:
     499           0 :         return msp_process_out_build_info(dst);
     500         104 :     case MSP_NAME:
     501         104 :         return msp_process_out_name(dst);
     502         104 :     case MSP_OSD_CONFIG:
     503         104 :         return msp_process_out_osd_config(dst);
     504           0 :     case MSP_OSD_CANVAS:
     505           0 :         return msp_process_out_osd_canvas(dst);
     506          42 :     case MSP_STATUS:
     507             :     case MSP_STATUS_EX:
     508          42 :         return msp_process_out_status(dst);
     509          84 :     case MSP_RAW_GPS:
     510          84 :         return msp_process_out_raw_gps(dst);
     511          84 :     case MSP_COMP_GPS:
     512          84 :         return msp_process_out_comp_gps(dst);
     513         105 :     case MSP_ATTITUDE:
     514         105 :         return msp_process_out_attitude(dst);
     515          84 :     case MSP_ALTITUDE:
     516          84 :         return msp_process_out_altitude(dst);
     517          84 :     case MSP_ANALOG:
     518          84 :         return msp_process_out_analog(dst);
     519             : #if AP_BATTERY_ENABLED
     520          42 :     case MSP_BATTERY_STATE:
     521          42 :         return msp_process_out_battery_state(dst);
     522             : #endif
     523           0 :     case MSP_UID:
     524           0 :         return msp_process_out_uid(dst);
     525             : #if HAL_WITH_ESC_TELEM
     526          42 :     case MSP_ESC_SENSOR_DATA:
     527          42 :         return msp_process_out_esc_sensor_data(dst);
     528             : #endif
     529          21 :     case MSP_RTC:
     530          21 :         return msp_process_out_rtc(dst);
     531           0 :     case MSP_RC:
     532           0 :         return msp_process_out_rc(dst);
     533             : #if AP_MSP_VIDEOTX_ENABLED
     534         125 :     case MSP_VTX_CONFIG:
     535             :         // reply with our config whether this is our proactive push (src == null)
     536             :         // or a GET request from the VTX/goggle (src != null), which polls it
     537         125 :         return msp_process_out_vtx_config(src, dst);
     538          10 :     case MSP_SET_VTX_CONFIG:
     539          10 :         return msp_process_in_vtx_config(src, dst);
     540           6 :     case MSP_SET_VTXTABLE_POWERLEVEL:
     541           6 :         return msp_process_in_vtxtable_powerlevel(src, dst);
     542             : #endif  // AP_MSP_VIDEOTX_ENABLED
     543           0 :     default:
     544             :         // MSP always requires an ACK even for unsupported messages
     545           0 :         return MSP_RESULT_ACK;
     546             :     }
     547             : }
     548             : 
     549          76 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_sensor_command(uint16_t cmd_msp, sbuf_t *src)
     550             : {
     551             :     MSP_UNUSED(src);
     552             : 
     553          76 :     switch (cmd_msp) {
     554             : #if HAL_MSP_RANGEFINDER_ENABLED
     555           0 :     case MSP2_SENSOR_RANGEFINDER: {
     556           0 :         const MSP::msp_rangefinder_data_message_t *pkt = (const MSP::msp_rangefinder_data_message_t *)src->ptr;
     557           0 :         msp_handle_rangefinder(*pkt);
     558             :     }
     559           0 :     break;
     560             : #endif
     561             : #if HAL_MSP_OPTICALFLOW_ENABLED
     562           0 :     case MSP2_SENSOR_OPTIC_FLOW: {
     563           0 :         const MSP::msp_opflow_data_message_t *pkt = (const MSP::msp_opflow_data_message_t *)src->ptr;
     564           0 :         msp_handle_opflow(*pkt);
     565             :     }
     566           0 :     break;
     567             : #endif
     568             : #if HAL_MSP_GPS_ENABLED
     569          76 :     case MSP2_SENSOR_GPS: {
     570          76 :         const MSP::msp_gps_data_message_t *pkt = (const MSP::msp_gps_data_message_t *)src->ptr;
     571          76 :         msp_handle_gps(*pkt);
     572             :     }
     573          76 :     break;
     574             : #endif
     575             : #if AP_COMPASS_MSP_ENABLED
     576           0 :     case MSP2_SENSOR_COMPASS: {
     577           0 :         const MSP::msp_compass_data_message_t *pkt = (const MSP::msp_compass_data_message_t *)src->ptr;
     578           0 :         msp_handle_compass(*pkt);
     579             :     }
     580           0 :     break;
     581             : #endif
     582             : #if AP_BARO_MSP_ENABLED
     583           0 :     case MSP2_SENSOR_BAROMETER: {
     584           0 :         const MSP::msp_baro_data_message_t *pkt = (const MSP::msp_baro_data_message_t *)src->ptr;
     585           0 :         msp_handle_baro(*pkt);
     586             :     }
     587           0 :     break;
     588             : #endif
     589             : #if AP_AIRSPEED_MSP_ENABLED && AP_AIRSPEED_ENABLED
     590           0 :     case MSP2_SENSOR_AIRSPEED: {
     591           0 :         const MSP::msp_airspeed_data_message_t *pkt = (const MSP::msp_airspeed_data_message_t *)src->ptr;
     592           0 :         msp_handle_airspeed(*pkt);
     593             :     }
     594           0 :     break;
     595             : #endif
     596             :     }
     597             : 
     598          76 :     return MSP_RESULT_NO_REPLY;
     599             : }
     600             : 
     601             : #if HAL_MSP_OPTICALFLOW_ENABLED
     602           0 : void AP_MSP_Telem_Backend::msp_handle_opflow(const MSP::msp_opflow_data_message_t &pkt)
     603             : {
     604           0 :     AP_OpticalFlow *optflow = AP::opticalflow();
     605           0 :     if (optflow == nullptr) {
     606           0 :         return;
     607             :     }
     608           0 :     optflow->handle_msp(pkt);
     609             : }
     610             : #endif
     611             : 
     612             : #if HAL_MSP_RANGEFINDER_ENABLED
     613           0 : void AP_MSP_Telem_Backend::msp_handle_rangefinder(const MSP::msp_rangefinder_data_message_t &pkt)
     614             : {
     615           0 :     RangeFinder *rangefinder = AP::rangefinder();
     616           0 :     if (rangefinder == nullptr) {
     617           0 :         return;
     618             :     }
     619           0 :     rangefinder->handle_msp(pkt);
     620             : }
     621             : #endif
     622             : 
     623             : #if HAL_MSP_GPS_ENABLED
     624          76 : void AP_MSP_Telem_Backend::msp_handle_gps(const MSP::msp_gps_data_message_t &pkt)
     625             : {
     626          76 :     AP::gps().handle_msp(pkt);
     627          76 : }
     628             : #endif
     629             : 
     630             : #if AP_COMPASS_MSP_ENABLED
     631           0 : void AP_MSP_Telem_Backend::msp_handle_compass(const MSP::msp_compass_data_message_t &pkt)
     632             : {
     633           0 :     AP::compass().handle_msp(pkt);
     634           0 : }
     635             : #endif
     636             : 
     637             : #if AP_BARO_MSP_ENABLED
     638           0 : void AP_MSP_Telem_Backend::msp_handle_baro(const MSP::msp_baro_data_message_t &pkt)
     639             : {
     640           0 :     AP::baro().handle_msp(pkt);
     641           0 : }
     642             : #endif
     643             : 
     644             : #if AP_AIRSPEED_MSP_ENABLED && AP_AIRSPEED_ENABLED
     645           0 : void AP_MSP_Telem_Backend::msp_handle_airspeed(const MSP::msp_airspeed_data_message_t &pkt)
     646             : {
     647           0 :     auto *airspeed = AP::airspeed();
     648           0 :     if (airspeed) {
     649           0 :         airspeed->handle_msp(pkt);
     650             :     }
     651           0 : }
     652             : #endif
     653             : 
     654          42 : uint32_t AP_MSP_Telem_Backend::get_osd_flight_mode_bitmask(void)
     655             : {
     656             :     // Note: we only set the BOXARM bit (bit 0) which is the same for BF, INAV and DJI VTX
     657             :     // When armed we simply return 1 (1 == 1 << 0)
     658             :     // VTXs that drop to low power when disarmed watch this bit, so the
     659             :     // ForceVTXHighPower option keeps it set to hold the VTX at full power
     660          42 :     const AP_MSP *msp = AP::msp();
     661          42 :     if (hal.util->get_soft_armed()
     662          42 :         || (msp != nullptr && msp->is_option_enabled(AP_MSP::Option::VTX_HIGH_POWER))) {
     663           0 :         return 1U;
     664             :     }
     665          42 :     return 0U;
     666             : }
     667             : 
     668           0 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_api_version(sbuf_t *dst)
     669             : {
     670             :     const struct {
     671             :         uint8_t proto;
     672             :         uint8_t major;
     673             :         uint8_t minor;
     674           0 :     } api_version  {
     675             :         proto : MSP_PROTOCOL_VERSION,
     676             :         major : API_VERSION_MAJOR,
     677             :         minor : API_VERSION_MINOR
     678             :     };
     679             : 
     680           0 :     sbuf_write_data(dst, &api_version, sizeof(api_version));
     681           0 :     return MSP_RESULT_ACK;
     682             : }
     683             : 
     684           0 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_fc_version(sbuf_t *dst)
     685             : {
     686             :     const struct {
     687             :         uint8_t major;
     688             :         uint8_t minor;
     689             :         uint8_t patch;
     690           0 :     } fc_version {
     691             :         major : FC_VERSION_MAJOR,
     692             :         minor : FC_VERSION_MINOR,
     693             :         patch : FC_VERSION_PATCH_LEVEL
     694             :     };
     695             : 
     696           0 :     sbuf_write_data(dst, &fc_version, sizeof(fc_version));
     697           0 :     return MSP_RESULT_ACK;
     698             : }
     699             : 
     700           0 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_fc_variant(sbuf_t *dst)
     701             : {
     702           0 :     sbuf_write_data(dst, "ARDU", FLIGHT_CONTROLLER_IDENTIFIER_LENGTH);
     703           0 :     return MSP_RESULT_ACK;
     704             : }
     705             : 
     706          84 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_raw_gps(sbuf_t *dst)
     707             : {
     708             : #if OSD_ENABLED
     709          84 :     AP_OSD *osd = AP::osd();
     710          84 :     if (osd == nullptr) {
     711           0 :         return MSP_RESULT_ERROR;
     712             :     }
     713             : #endif
     714          84 :     gps_state_t gps_state {};
     715             : #if AP_GPS_ENABLED
     716          84 :     update_gps_state(gps_state);
     717             : #endif
     718             : 
     719             :     // handle airspeed override
     720          84 :     bool airspeed_en = false;
     721             : #if OSD_ENABLED
     722          84 :     AP_MSP *msp = AP::msp();
     723          84 :     if (msp == nullptr) {
     724           0 :         return MSP_RESULT_ERROR;
     725             :     }
     726             : 
     727          84 :     airspeed_en = osd->screen[msp->_msp_status.current_screen].aspeed.enabled;
     728             : #endif
     729          84 :     if (airspeed_en) {
     730             :         airspeed_state_t airspeed_state;
     731           0 :         update_airspeed(airspeed_state);
     732           0 :         gps_state.speed_cms = airspeed_state.airspeed_estimate_ms * 100;           // airspeed in cm/s
     733             :     }
     734             : 
     735          84 :     sbuf_write_data(dst, &gps_state, sizeof(gps_state));
     736          84 :     return MSP_RESULT_ACK;
     737             : }
     738             : 
     739          84 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_comp_gps(sbuf_t *dst)
     740             : {
     741             :     home_state_t home_state;
     742          84 :     update_home_pos(home_state);
     743             : 
     744             :     // no need to apply yaw compensation, the DJI air unit will do it for us :-)
     745          84 :     uint16_t angle_deg = home_state.home_bearing_cd * 0.01;
     746          84 :     if (home_state.home_distance_m < 2) {
     747             :         //avoid fast rotating arrow at small distances
     748          84 :         angle_deg = 0;
     749             :     }
     750             : 
     751             :     const struct PACKED {
     752             :         uint16_t dist_home_m;
     753             :         uint16_t home_angle_deg;
     754             :         uint8_t toggle_gps;
     755          84 :     } gps {
     756          84 :         dist_home_m : uint16_t(constrain_int32(home_state.home_distance_m, 0, 0xFFFF)),
     757             :         home_angle_deg : angle_deg,
     758             :         toggle_gps : 1
     759          84 :     };
     760             : 
     761          84 :     sbuf_write_data(dst, &gps, sizeof(gps));
     762          84 :     return MSP_RESULT_ACK;
     763             : }
     764             : 
     765             : // Autoscroll message is the same as in minimosd-extra.
     766             : // Thanks to night-ghost for the approach.
     767         104 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_name(sbuf_t *dst)
     768             : {
     769             : #if OSD_ENABLED
     770         104 :     AP_OSD *osd = AP::osd();
     771         104 :     if (osd == nullptr) {
     772           0 :         return MSP_RESULT_ERROR;
     773             :     }
     774             : #endif
     775         104 :     AP_MSP *msp = AP::msp();
     776         104 :     if (msp == nullptr) {
     777           0 :         return MSP_RESULT_ERROR;
     778             :     }
     779         104 :     AP_Notify * notify = AP_Notify::get_singleton();
     780         104 :     if (notify) {
     781         104 :         uint16_t msgtime_ms = 10000; //default is 10 secs
     782             : #if OSD_ENABLED
     783         104 :         msgtime_ms = AP::osd()->msgtime_s * 1000;
     784             : #endif
     785             :         // text message is visible for _msp.msgtime_s but only if
     786             :         // a flight mode change did not steal focus
     787         104 :         const uint32_t visible_time_ms = AP_HAL::millis() - notify->get_text_updated_millis();
     788         104 :         if (visible_time_ms < msgtime_ms && !msp->_msp_status.flight_mode_focus) {
     789             :             char buffer[NOTIFY_TEXT_BUFFER_SIZE];
     790          87 :             strncpy(buffer, notify->get_text(), ARRAY_SIZE(buffer));
     791          87 :             const uint8_t len = strnlen(buffer, ARRAY_SIZE(buffer));
     792             : 
     793        2552 :             for (uint8_t i=0; i<len; i++) {
     794             :                 //normalize whitespace
     795        2465 :                 if (isspace(buffer[i])) {
     796         289 :                     buffer[i] = ' ';
     797             :                 } else {
     798             :                     //converted to uppercase,
     799        2176 :                     buffer[i] = toupper(buffer[i]);
     800             :                 }
     801             :             }
     802             : 
     803          87 :             int8_t start_position = 0;
     804             :             //scroll if required
     805             :             //scroll pattern: wait, scroll to the left, wait, scroll to the right
     806          87 :             if (len > MSP_TXT_VISIBLE_CHARS) {
     807          87 :                 const uint8_t chars_to_scroll = len - MSP_TXT_VISIBLE_CHARS;
     808          87 :                 const uint8_t total_cycles = 2*message_scroll_delay + 2*chars_to_scroll;
     809          87 :                 const uint8_t current_cycle = (visible_time_ms / message_scroll_time_ms) % total_cycles;
     810             : 
     811             :                 //calculate scroll start_position
     812          87 :                 if (current_cycle < total_cycles/2) {
     813             :                     //move to the left
     814          75 :                     start_position = current_cycle - message_scroll_delay;
     815             :                 } else {
     816             :                     //move to the right
     817          12 :                     start_position = total_cycles - current_cycle;
     818             :                 }
     819          87 :                 start_position = constrain_int16(start_position, 0, chars_to_scroll);
     820          87 :                 uint8_t end_position = start_position + MSP_TXT_VISIBLE_CHARS;
     821             : 
     822             :                 //ensure array boundaries
     823          87 :                 start_position = MIN(start_position, int8_t(ARRAY_SIZE(buffer)-1));
     824          87 :                 end_position = MIN(end_position, int8_t(ARRAY_SIZE(buffer)-1));
     825             : 
     826             :                 //trim invisible part
     827          87 :                 buffer[end_position] = 0;
     828             :             }
     829             : 
     830          87 :             sbuf_write_data(dst, buffer + start_position, strlen(buffer + start_position));  // max MSP_TXT_VISIBLE_CHARS chars general text...
     831          87 :         } else {
     832          17 :             bool wind_en = false;
     833             :             char flight_mode_str[MSP_TXT_BUFFER_SIZE];
     834             : #if OSD_ENABLED
     835          17 :             wind_en = osd->screen[msp->_msp_status.current_screen].wind.enabled;
     836             : #endif
     837          17 :             update_flight_mode_str(flight_mode_str, ARRAY_SIZE(flight_mode_str), wind_en);
     838          17 :             sbuf_write_data(dst, flight_mode_str, ARRAY_SIZE(flight_mode_str));  // rendered as up to MSP_TXT_VISIBLE_CHARS chars with UTF8 support
     839             :         }
     840             :     }
     841         104 :     return MSP_RESULT_ACK;
     842             : }
     843             : 
     844          42 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_status(sbuf_t *dst)
     845             : {
     846             : 
     847             :     struct PACKED {
     848             :         uint16_t task_delta_time;
     849             :         uint16_t i2c_error_count;
     850             :         uint16_t sensor_status;
     851             :         uint32_t flight_mode_flags;
     852             :         uint8_t pid_profile;
     853             :         uint16_t system_load;
     854             :         uint16_t gyro_cycle_time;
     855             :         uint8_t box_mode_flags;
     856             :         uint8_t arming_disable_flags_count;
     857             :         uint32_t arming_disable_flags;
     858             :         uint8_t extra_flags;
     859          42 :     } status {};
     860             : 
     861          42 :     status.flight_mode_flags = get_osd_flight_mode_bitmask();
     862          42 :     status.arming_disable_flags_count = 1;
     863          42 :     status.arming_disable_flags = !AP::notify().flags.armed;
     864             : 
     865          42 :     sbuf_write_data(dst, &status, sizeof(status));
     866          42 :     return MSP_RESULT_ACK;
     867             : }
     868             : 
     869           0 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_osd_canvas(sbuf_t *dst)
     870             : {
     871             :     // report the canvas matching the current screen's text resolution so an HD
     872             :     // goggle sizes its OSD grid correctly (TXT_RES 0:30x16, 1:50x18, 2:60x22)
     873             :     struct PACKED {
     874             :         uint8_t cols;
     875             :         uint8_t rows;
     876           0 :     } canvas { 30, 16 };
     877             : #if OSD_ENABLED
     878           0 :     const AP_OSD *osd = AP::osd();
     879           0 :     const AP_MSP *msp = AP::msp();
     880           0 :     if (osd != nullptr && msp != nullptr) {
     881           0 :         switch (osd->screen[msp->_msp_status.current_screen].get_txt_resolution()) {
     882           0 :         case 1:
     883           0 :             canvas = { 50, 18 };
     884           0 :             break;
     885           0 :         case 2:
     886           0 :             canvas = { 60, 22 };
     887           0 :             break;
     888           0 :         default:
     889           0 :             break;
     890             :         }
     891             :     }
     892             : #endif
     893           0 :     sbuf_write_data(dst, &canvas, sizeof(canvas));
     894           0 :     return MSP_RESULT_ACK;
     895             : }
     896             : 
     897         104 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_osd_config(sbuf_t *dst)
     898             : {
     899             : #if OSD_ENABLED
     900         104 :     AP_OSD *osd = AP::osd();
     901         104 :     if (osd == nullptr) {
     902           0 :         return MSP_RESULT_ERROR;
     903             :     }
     904             : #endif
     905         104 :     const AP_MSP *msp = AP::msp();
     906         104 :     if (msp == nullptr) {
     907           0 :         return MSP_RESULT_ERROR;
     908             :     }
     909             :     struct PACKED {
     910             :         uint8_t flags;
     911             :         uint8_t video_system;
     912             :         uint8_t units;
     913             :         uint8_t rssi_alarm;
     914             :         uint16_t capacity_alarm;
     915             :         uint8_t unused_0;
     916             :         uint8_t item_count;
     917             :         uint16_t alt_alarm;
     918             :         uint16_t items_position[OSD_ITEM_COUNT];
     919             :         uint8_t stats_items_count;
     920             :         uint16_t stats_items[OSD_STAT_COUNT] ;
     921             :         uint8_t timers_count;
     922             :         uint16_t timers[OSD_TIMER_COUNT];
     923             :         uint16_t enabled_warnings_old;
     924             :         uint8_t warnings_count_new;
     925             :         uint32_t enabled_warnings_new;
     926             :         uint8_t available_profiles;
     927             :         uint8_t selected_profile;
     928             :         uint8_t osd_stick_overlay;
     929         104 :     } osd_config {};
     930             : 
     931             :     // Configuration
     932         104 :     osd_config.units = OSD_UNIT_METRIC;
     933             : #if OSD_ENABLED
     934         104 :     osd_config.units = osd->units == AP_OSD::UNITS_METRIC ? OSD_UNIT_METRIC : OSD_UNIT_IMPERIAL;
     935             : #endif
     936             :     // Alarms
     937         104 :     osd_config.rssi_alarm = msp->_osd_config.rssi_alarm;
     938         104 :     osd_config.capacity_alarm = msp->_osd_config.cap_alarm;
     939         104 :     osd_config.alt_alarm = msp->_osd_config.alt_alarm;
     940             :     // Reuse old timer alarm (U16) as OSD_ITEM_COUNT
     941         104 :     osd_config.item_count = OSD_ITEM_COUNT;
     942             :     // Element position and visibility
     943         104 :     uint16_t pos = 0;   // default is hide this element
     944        6136 :     for (uint8_t i = 0; i < OSD_ITEM_COUNT; i++) {
     945        6032 :         pos = 0;    // 0 is hide this item
     946        6032 :         if (msp->_osd_item_settings[i] != nullptr) {      // ok supported
     947        2704 :             if (msp->_osd_item_settings[i]->enabled) {    // ok enabled
     948             :                 // let's check if we need to hide this dynamically
     949        2080 :                 if (!BIT_IS_SET_64(osd_hidden_items_bitmask, i)) {
     950        1846 :                     pos = MSP_OSD_POS(msp->_osd_item_settings[i]);
     951             :                 }
     952             :             }
     953             :         }
     954        6032 :         osd_config.items_position[i] = pos;
     955             :     }
     956             :     // Post flight statistics
     957         104 :     osd_config.stats_items_count = OSD_STAT_COUNT;             // stats items count
     958             :     // Timers
     959         104 :     osd_config.timers_count = OSD_TIMER_COUNT;                      // timers
     960             :     // Enabled warnings
     961             :     // API < 1.41
     962             :     // Send low word first for backwards compatibility
     963         104 :     osd_config.enabled_warnings_old = (uint16_t)(msp->_osd_config.enabled_warnings & 0xFFFF);
     964             :     // API >= 1.41
     965             :     // Send the warnings count and 32bit enabled warnings flags.
     966             :     // Add currently active OSD profile (0 indicates OSD profiles not available).
     967             :     // Add OSD stick overlay mode (0 indicates OSD stick overlay not available).
     968         104 :     osd_config.warnings_count_new = OSD_WARNING_COUNT;
     969         104 :     osd_config.enabled_warnings_new = msp->_osd_config.enabled_warnings;
     970             :     // If the feature is not available there is only 1 profile and it's always selected
     971         104 :     osd_config.available_profiles = 1;
     972         104 :     osd_config.selected_profile = 1;
     973             : 
     974         104 :     sbuf_write_data(dst, &osd_config, sizeof(osd_config));
     975         104 :     return MSP_RESULT_ACK;
     976             : }
     977             : 
     978         105 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_attitude(sbuf_t *dst)
     979             : {
     980             : #if AP_VEHICLE_ENABLED
     981             :     float roll_rad, pitch_rad, yaw_rad;
     982         105 :     AP::vehicle()->get_osd_attitude_rad(roll_rad, pitch_rad, yaw_rad);
     983             : #else
     984             :     AP_AHRS &ahrs = AP::ahrs();
     985             :     WITH_SEMAPHORE(ahrs.get_semaphore());
     986             :     float roll_rad = ahrs.get_roll_rad();
     987             :     float pitch_rad = ahrs.get_pitch_rad();
     988             :     float yaw_rad = ahrs.get_yaw_rad();
     989             : #endif // AP_VEHICLE_ENABLED
     990             : 
     991             :     const struct PACKED {
     992             :         int16_t roll;
     993             :         int16_t pitch;
     994             :         int16_t yaw;
     995             :     } attitude {
     996         105 :         roll : int16_t(degrees(roll_rad) * 10),     // degress to decidegrees
     997         210 :         pitch : int16_t(degrees(pitch_rad) * 10),   // degress to decidegrees
     998         105 :         yaw : int16_t(wrap_360(degrees(yaw_rad)))
     999         105 :     };
    1000             : 
    1001         105 :     sbuf_write_data(dst, &attitude, sizeof(attitude));
    1002         105 :     return MSP_RESULT_ACK;
    1003             : }
    1004             : 
    1005          84 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_altitude(sbuf_t *dst)
    1006             : {
    1007             :     home_state_t home_state;
    1008          84 :     update_home_pos(home_state);
    1009             : 
    1010             :     const struct PACKED {
    1011             :         int32_t rel_altitude_cm;    // relative altitude cm
    1012             :         int16_t vspeed_cms;         // climb rate cm/s
    1013             :     } altitude {
    1014          84 :         rel_altitude_cm : home_state.rel_altitude_cm,
    1015         168 :         vspeed_cms : int16_t(get_vspeed_ms() * 100)
    1016          84 :     };
    1017             : 
    1018          84 :     sbuf_write_data(dst, &altitude, sizeof(altitude));
    1019          84 :     return MSP_RESULT_ACK;
    1020             : }
    1021             : 
    1022          84 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_analog(sbuf_t *dst)
    1023             : {
    1024             : #if AP_BATTERY_ENABLED
    1025             :     battery_state_t battery_state;
    1026          84 :     update_battery_state(battery_state);
    1027             : 
    1028             :     float rssi;
    1029             :     const struct PACKED {
    1030             :         uint8_t voltage_dv;
    1031             :         uint16_t mah;
    1032             :         uint16_t rssi;
    1033             :         int16_t current_ca;
    1034             :         uint16_t voltage_cv;
    1035             :     } analog {
    1036          84 :         voltage_dv : (uint8_t)constrain_int16(battery_state.batt_voltage_v * 10, 0, 255),                   // battery voltage V to dV
    1037          84 :         mah : (uint16_t)constrain_int32(battery_state.batt_consumed_mah, 0, 0xFFFF),                        // milliamp hours drawn from battery
    1038          84 :         rssi : uint16_t(get_rssi(rssi) ? constrain_float(rssi,0,1) * 1023 : 0),                             // rssi 0-1 to 0-1023)
    1039          84 :         current_ca : (int16_t)constrain_int32(battery_state.batt_current_a * 100, -0x8000, 0x7FFF),         // current A to cA (0.01 steps, range is -320A to 320A)
    1040          84 :         voltage_cv : (uint16_t)constrain_int32(battery_state.batt_voltage_v * 100,0,0xFFFF)                 // battery voltage in 0.01V steps
    1041         168 :     };
    1042             : #else
    1043             :     float rssi;
    1044             :     const struct PACKED {
    1045             :         uint8_t voltage_dv;
    1046             :         uint16_t mah;
    1047             :         uint16_t rssi;
    1048             :         int16_t current_ca;
    1049             :         uint16_t voltage_cv;
    1050             :     } analog {
    1051             :         voltage_dv : 0,
    1052             :         mah : 0,
    1053             :         rssi : uint16_t(get_rssi(rssi) ? constrain_float(rssi,0,1) * 1023 : 0),                             // rssi 0-1 to 0-1023)
    1054             :         current_ca : 0,
    1055             :         voltage_cv : 0
    1056             :     };
    1057             : #endif
    1058          84 :     sbuf_write_data(dst, &analog, sizeof(analog));
    1059          84 :     return MSP_RESULT_ACK;
    1060             : }
    1061             : 
    1062             : #if AP_BATTERY_ENABLED
    1063          42 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_battery_state(sbuf_t *dst)
    1064             : {
    1065          42 :     const AP_MSP *msp = AP::msp();
    1066          42 :     if (msp == nullptr) {
    1067           0 :         return MSP_RESULT_ERROR;
    1068             :     }
    1069             :     battery_state_t battery_state;
    1070          42 :     update_battery_state(battery_state);
    1071             : 
    1072             :     const struct PACKED {
    1073             :         uint8_t cellcount;
    1074             :         uint16_t capacity_mah;
    1075             :         uint8_t voltage_dv;
    1076             :         uint16_t mah;
    1077             :         int16_t current_ca;
    1078             :         uint8_t state;
    1079             :         uint16_t voltage_cv;
    1080             :     } battery {
    1081          42 :         cellcount : (uint8_t)constrain_int16((msp->_cellcount > 0 ? msp->_cellcount : battery_state.batt_cellcount), 0, 255),   // cell count 0 indicates battery not detected.
    1082          42 :         capacity_mah : (uint16_t)battery_state.batt_capacity_mah,                                                               // in mAh
    1083          42 :         voltage_dv : (uint8_t)constrain_int16(battery_state.batt_voltage_v * 10, 0, 255),                                       // battery voltage V to dV
    1084          84 :         mah : (uint16_t)MIN(battery_state.batt_consumed_mah, 0xFFFF),                                                           // milliamp hours drawn from battery
    1085          42 :         current_ca : (int16_t)constrain_int32(battery_state.batt_current_a * 100, -0x8000, 0x7FFF),                             // current A to cA (0.01 steps, range is -320A to 320A)
    1086          42 :         state : (uint8_t)battery_state.batt_state,                                                                              // BATTERY: OK=0, CRITICAL=2
    1087          42 :         voltage_cv : (uint16_t)constrain_int32(battery_state.batt_voltage_v * 100, 0, 0x7FFF)                                   // battery voltage in 0.01V steps
    1088          42 :     };
    1089             : 
    1090          42 :     sbuf_write_data(dst, &battery, sizeof(battery));
    1091          42 :     return MSP_RESULT_ACK;
    1092             : }
    1093             : #endif
    1094             : 
    1095           0 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_esc_sensor_data(sbuf_t *dst)
    1096             : {
    1097             : #if HAL_WITH_ESC_TELEM
    1098           0 :     AP_ESC_Telem& telem = AP::esc_telem();
    1099           0 :     if (telem.get_last_telem_data_ms(0)) {
    1100             :         struct PACKED {
    1101             :             uint8_t num_motors;
    1102             :             struct PACKED {
    1103             :                 uint8_t temp;
    1104             :                 uint16_t rpm;
    1105             :             } data[ESC_TELEM_MAX_ESCS];
    1106           0 :         } esc_sensor {};
    1107             : 
    1108           0 :         esc_sensor.num_motors = telem.get_num_active_escs();
    1109           0 :         for (uint8_t i = 0; i < esc_sensor.num_motors; i++) {
    1110           0 :             int16_t temp = 0;
    1111           0 :             float rpm = 0.0f;
    1112           0 :             IGNORE_RETURN(telem.get_rpm(i, rpm));
    1113           0 :             IGNORE_RETURN(telem.get_temperature(i, temp));
    1114           0 :             esc_sensor.data[i].temp = uint8_t(temp * 0.01f);
    1115           0 :             esc_sensor.data[i].rpm = uint16_t(rpm * 0.1f);
    1116             :         }
    1117           0 :         sbuf_write_data(dst, &esc_sensor, 1 + 3*esc_sensor.num_motors);
    1118             :     }
    1119             : #endif
    1120           0 :     return MSP_RESULT_ACK;
    1121             : }
    1122             : 
    1123          21 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_rtc(sbuf_t *dst)
    1124             : {
    1125          21 :     tm localtime_tm {}; // year is relative to 1900
    1126          21 :     uint64_t time_usec = 0;
    1127             : #if AP_RTC_ENABLED
    1128          21 :     if (AP::rtc().get_utc_usec(time_usec)) { // may fail, leaving time_unix at 0
    1129          12 :         const time_t time_sec = time_usec / 1000000;
    1130          12 :         struct tm tmd {};
    1131          12 :         localtime_tm = *gmtime_r(&time_sec, &tmd);
    1132             :     }
    1133             : #endif
    1134             :     const struct PACKED {
    1135             :         uint16_t year;
    1136             :         uint8_t mon;
    1137             :         uint8_t mday;
    1138             :         uint8_t hour;
    1139             :         uint8_t min;
    1140             :         uint8_t sec;
    1141             :         uint16_t millis;
    1142             :     } rtc {
    1143          21 :         year : uint16_t(localtime_tm.tm_year + 1900),   // tm_year is relative to year 1900
    1144          21 :         mon : uint8_t(localtime_tm.tm_mon + 1),        // MSP requires [1-12] months
    1145          21 :         mday : uint8_t(localtime_tm.tm_mday),
    1146          21 :         hour : uint8_t(localtime_tm.tm_hour),
    1147          21 :         min : uint8_t(localtime_tm.tm_min),
    1148          21 :         sec : uint8_t(localtime_tm.tm_sec),
    1149          21 :         millis : uint16_t((time_usec / 1000U) % 1000U)
    1150          21 :     };
    1151             : 
    1152          21 :     sbuf_write_data(dst, &rtc, sizeof(rtc));
    1153          21 :     return MSP_RESULT_ACK;
    1154             : }
    1155             : 
    1156             : #if AP_RC_CHANNEL_ENABLED
    1157           0 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_rc(sbuf_t *dst)
    1158             : {
    1159           0 :     float roll = rc().get_roll_channel().norm_input_dz();
    1160           0 :     float pitch = -rc().get_pitch_channel().norm_input_dz();
    1161           0 :     float yaw = rc().get_yaw_channel().norm_input_dz();
    1162           0 :     float throttle = rc().get_throttle_channel().norm_input_dz();
    1163             : 
    1164             :     const struct PACKED {
    1165             :         uint16_t a;
    1166             :         uint16_t e;
    1167             :         uint16_t r;
    1168             :         uint16_t t;
    1169             :     } rc {
    1170             :         // send only 4 channels, MSP order is AERT
    1171           0 :         a : uint16_t(roll*500+1500),       // A
    1172           0 :         e : uint16_t(pitch*500+1500),      // E
    1173           0 :         r : uint16_t(yaw*500+1500),        // R
    1174           0 :         t : uint16_t(throttle*1000+1000)    // T
    1175           0 :     };
    1176             : 
    1177           0 :     sbuf_write_data(dst, &rc, sizeof(rc));
    1178           0 :     return MSP_RESULT_ACK;
    1179             : }
    1180             : #endif  // AP_RC_CHANNEL_ENABLED
    1181             : 
    1182             : #if AP_MSP_VIDEOTX_ENABLED
    1183             : // set the configured pitmode option without disturbing the other option bits
    1184          10 : void AP_MSP_Telem_Backend::msp_vtx_set_pitmode(bool pitmode)
    1185             : {
    1186          10 :     AP::vtx().set_option_enabled(AP_VideoTX::VideoOptions::VTX_PITMODE, pitmode);
    1187          10 : }
    1188             : 
    1189             : // fallback power plan used until the connected VTX declares its own table via
    1190             : // MSP_SET_VTXTABLE_POWERLEVEL; the index is one based (0 means "unset"), so
    1191             : // index 1 == 25mW. 14dBm (25mW), 20dBm (100mW), 26dBm (400mW), 29dBm (800mW)
    1192             : static const uint16_t msp_vtx_default_power_mw[] { 25, 100, 400, 800 };
    1193             : 
    1194             : // apply a one-based betaflight power index, leaving power unchanged for 0
    1195          10 : void AP_MSP_Telem_Backend::msp_vtx_set_power_index(uint8_t index)
    1196             : {
    1197          10 :     AP_VideoTX& vtx = AP::vtx();
    1198             :     // prefer the table the VTX declared for itself, fall back to the default
    1199          10 :     uint16_t mw = vtx.get_power_mw_for_index(index);
    1200          10 :     if (mw == 0 && index >= 1 && index <= ARRAY_SIZE(msp_vtx_default_power_mw)) {
    1201           4 :         mw = msp_vtx_default_power_mw[index - 1];
    1202             :     }
    1203          10 :     if (mw > 0) {
    1204          10 :         vtx.set_configured_power_mw(mw);
    1205             :     }
    1206          10 : }
    1207             : 
    1208             : // convert the configured power back to the one-based index, 0 if unmatched
    1209         125 : uint8_t AP_MSP_Telem_Backend::msp_vtx_get_power_index() const
    1210             : {
    1211         125 :     const AP_VideoTX& vtx = AP::vtx();
    1212         125 :     const uint16_t mw = vtx.get_configured_power_mw();
    1213         125 :     const uint8_t index = vtx.get_power_index_for_mw(mw);
    1214         125 :     if (index > 0) {
    1215          27 :         return index;
    1216             :     }
    1217         490 :     for (uint8_t i = 0; i < ARRAY_SIZE(msp_vtx_default_power_mw); i++) {
    1218         392 :         if (mw == msp_vtx_default_power_mw[i]) {
    1219           0 :             return i + 1;
    1220             :         }
    1221             :     }
    1222          98 :     return 0;
    1223             : }
    1224             : 
    1225             : // store a frequency along with the matching band/channel; all setters are
    1226             : // set-and-save-if-changed so repeated identical messages cause no flash writes
    1227           0 : void AP_MSP_Telem_Backend::msp_vtx_set_frequency(uint16_t freq_mhz)
    1228             : {
    1229           0 :     AP_VideoTX& vtx = AP::vtx();
    1230           0 :     vtx.set_configured_frequency_mhz(freq_mhz);
    1231             :     AP_VideoTX::VideoBand band;
    1232             :     uint8_t channel;
    1233           0 :     if (AP_VideoTX::get_band_and_channel(freq_mhz, band, channel)) {
    1234           0 :         vtx.set_configured_band(uint8_t(band));
    1235           0 :         vtx.set_configured_channel(channel);
    1236             :     }
    1237           0 : }
    1238             : 
    1239             : // store a (zero based) band/channel along with the matching frequency
    1240          12 : void AP_MSP_Telem_Backend::msp_vtx_set_band_and_channel(uint8_t band, uint8_t channel)
    1241             : {
    1242          12 :     AP_VideoTX& vtx = AP::vtx();
    1243          12 :     vtx.set_configured_band(band);
    1244          12 :     vtx.set_configured_channel(channel);
    1245          12 :     vtx.set_configured_frequency_mhz(AP_VideoTX::get_frequency_mhz(band, channel));
    1246          12 : }
    1247             : 
    1248          10 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_in_vtx_config(sbuf_t *src, sbuf_t *dst)
    1249             : {
    1250          10 :     AP_VideoTX& vtx = AP::vtx();
    1251             :     // the user may have a VTX connected but not want AP to control it
    1252             :     // (for instance because they are using myVTX on the transmitter)
    1253          10 :     if (!vtx.get_enabled()) {
    1254           0 :         return MSP_RESULT_ERROR;
    1255             :     }
    1256             : 
    1257             :     // MSP_SET_VTX_CONFIG is variable length: clients send anything from the
    1258             :     // legacy 4-byte payload up to the full 1.42 message, so every field beyond
    1259             :     // the first must be guarded by the remaining byte count (betaflight msp.c)
    1260          10 :     if (sbuf_bytes_remaining(src) < 2) {
    1261           0 :         return MSP_RESULT_ERROR;
    1262             :     }
    1263             : 
    1264             :     // the leading u16 is overloaded: values <= VTX_MSP_BANDCHAN_CHKVAL encode
    1265             :     // band and channel (band = v/8, channel = v%8, both zero based internally),
    1266             :     // larger values are a direct frequency in MHz
    1267          10 :     const uint16_t VTX_MSP_BANDCHAN_CHKVAL = (7 << 3) + 7;
    1268          10 :     const uint16_t freq_or_bandchan = sbuf_read_u16(src);
    1269          10 :     if (freq_or_bandchan <= VTX_MSP_BANDCHAN_CHKVAL) {
    1270          10 :         msp_vtx_set_band_and_channel(freq_or_bandchan / VTX_MAX_CHANNELS, freq_or_bandchan % VTX_MAX_CHANNELS);
    1271             :     } else {
    1272           0 :         msp_vtx_set_frequency(freq_or_bandchan);
    1273             :     }
    1274             : 
    1275          10 :     if (sbuf_bytes_remaining(src) >= 2) {
    1276          10 :         msp_vtx_set_power_index(sbuf_read_u8(src));
    1277          10 :         msp_vtx_set_pitmode(sbuf_read_u8(src) != 0);
    1278             :     }
    1279             : 
    1280             :     // lowPowerDisarm - not supported, consume to stay aligned
    1281          10 :     if (sbuf_bytes_remaining(src) >= 1) {
    1282           2 :         sbuf_read_u8(src);
    1283             :     }
    1284             : 
    1285             :     // API 1.42 - pitModeFreq - not supported, consume to stay aligned
    1286          10 :     if (sbuf_bytes_remaining(src) >= 2) {
    1287           2 :         sbuf_read_u16(src);
    1288             :     }
    1289             : 
    1290             :     // API 1.42 - standalone (non-encoded) band/channel/frequency; betaflight
    1291             :     // sends band/channel one based with band == 0 meaning "use raw frequency".
    1292             :     // When present this supersedes the legacy encoding decoded above.
    1293          10 :     if (sbuf_bytes_remaining(src) >= 4) {
    1294           2 :         const uint8_t band = sbuf_read_u8(src);
    1295           2 :         const uint8_t channel = sbuf_read_u8(src);
    1296           2 :         const uint16_t freq = sbuf_read_u16(src);
    1297           2 :         if (band > 0) {
    1298           2 :             msp_vtx_set_band_and_channel(band - 1, channel > 0 ? channel - 1 : 0);
    1299             :         } else {
    1300           0 :             msp_vtx_set_frequency(freq);
    1301             :         }
    1302             :     }
    1303             : 
    1304          10 :     vtx.set_provider_enabled(AP_VideoTX::VTXType::MSP);
    1305             :     // the VTX has pushed its own config, so the boot handshake is complete and
    1306             :     // we may now advertise readiness and push config changes back to it
    1307          10 :     _vtx_config_received = true;
    1308             :     // make sure the configured values now reflect reality
    1309          10 :     if (!vtx.set_defaults()) {
    1310           8 :         vtx.announce_vtx_settings();
    1311             :     }
    1312             :     // seed the push snapshot with the VTX's own values so we only push once the
    1313             :     // user actually changes something, not immediately after the handshake
    1314          10 :     _vtx_pushed = {
    1315          10 :         vtx.get_configured_band(),
    1316          10 :         vtx.get_configured_channel(),
    1317          10 :         vtx.get_configured_frequency_mhz(),
    1318          10 :         vtx.get_configured_power_mw(),
    1319          10 :         vtx.get_configured_pitmode(),
    1320             :         true
    1321             :     };
    1322          10 :     _vtx_config_push_remaining = 0;
    1323             : 
    1324          10 :     return MSP_RESULT_ACK;
    1325             : }
    1326             : 
    1327             : // push the VTX config on change, repeating MSP_VTX_CONFIG_PUSH_COUNT times for
    1328             : // delivery robustness. Returns true when the caller should emit a config frame
    1329             : // this tick. We only control the VTX after its boot handshake; the air unit
    1330             : // does not report its state back, so receipt cannot be confirmed (unlike Tramp)
    1331          53 : bool AP_MSP_Telem_Backend::vtx_should_push_config()
    1332             : {
    1333          53 :     AP_VideoTX& vtx = AP::vtx();
    1334          53 :     if (!vtx.get_enabled() || !_vtx_config_received) {
    1335          47 :         return false;
    1336             :     }
    1337             : 
    1338           6 :     const uint8_t band = vtx.get_configured_band();
    1339           6 :     const uint8_t channel = vtx.get_configured_channel();
    1340           6 :     const uint16_t freq_mhz = vtx.get_configured_frequency_mhz();
    1341           6 :     const uint16_t power_mw = vtx.get_configured_power_mw();
    1342           6 :     const bool pitmode = vtx.get_configured_pitmode();
    1343             : 
    1344           6 :     if (!_vtx_pushed.valid
    1345           6 :         || band != _vtx_pushed.band
    1346           6 :         || channel != _vtx_pushed.channel
    1347           6 :         || freq_mhz != _vtx_pushed.freq_mhz
    1348           6 :         || power_mw != _vtx_pushed.power_mw
    1349           6 :         || pitmode != _vtx_pushed.pitmode) {
    1350             :         // configuration changed, (re)start a push burst
    1351           0 :         _vtx_pushed = { band, channel, freq_mhz, power_mw, pitmode, true };
    1352           0 :         _vtx_config_push_remaining = MSP_VTX_CONFIG_PUSH_COUNT;
    1353           0 :         vtx.set_configuration_finished(false);
    1354             :     }
    1355             : 
    1356           6 :     if (_vtx_config_push_remaining == 0) {
    1357           6 :         return false;
    1358             :     }
    1359           0 :     if (--_vtx_config_push_remaining == 0) {
    1360             :         // optimistically consider the VTX configured once the burst completes
    1361           0 :         vtx.set_configuration_finished(true);
    1362             :     }
    1363           0 :     return true;
    1364             : }
    1365             : 
    1366         125 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_vtx_config(sbuf_t *src, sbuf_t *dst)
    1367             : {
    1368         125 :     AP_VideoTX& vtx = AP::vtx();
    1369             : 
    1370         125 :     if (!vtx.get_enabled()) {
    1371           0 :         return MSP_RESULT_ERROR;
    1372             :     }
    1373             : 
    1374             :     // band/channel are one based on the wire (band == 0 means raw frequency),
    1375             :     // zero based internally
    1376         125 :     const uint8_t VTXDEV_MSP = 5;   // betaflight vtxDevType_e
    1377             :     struct PACKED {
    1378             :         uint8_t type;
    1379             :         uint8_t band;
    1380             :         uint8_t channel;
    1381             :         uint8_t power;
    1382             :         uint8_t pitmode;
    1383             :         uint16_t freq;
    1384             :         uint8_t deviceIsReady;
    1385             :         uint8_t lowPowerDisarm;
    1386             :         // API version 1.42
    1387             :         uint16_t pitModeFreq;
    1388             :         uint8_t table;
    1389             :         uint8_t table_bands;
    1390             :         uint8_t table_channels;
    1391             :         uint8_t table_powerLevels;
    1392         125 :     } vtx_config {
    1393             :         // band/channel are one based on the wire (band == 0 means raw frequency),
    1394             :         // zero based internally. Keep the frequency consistent with band/channel:
    1395             :         // the user may change VTX_CHANNEL without VTX_FREQ, and a stale frequency
    1396             :         // would contradict the band/channel a betaflight-style VTX keys off
    1397             :         .type = VTXDEV_MSP,
    1398         125 :         .band = uint8_t(vtx.get_configured_band() + 1),
    1399         250 :         .channel = uint8_t(vtx.get_configured_channel() + 1),
    1400         125 :         .power = msp_vtx_get_power_index(),
    1401         250 :         .pitmode = vtx.get_configured_pitmode(),
    1402         125 :         .freq = AP_VideoTX::get_frequency_mhz(vtx.get_configured_band(), vtx.get_configured_channel()),
    1403             :         // report not-ready until the VTX has uploaded its own config: the HDZero
    1404             :         // air unit is the MSP master and only runs that handshake against a
    1405             :         // betaflight FC (FC_VARIANT) that is not yet configured. Once it has
    1406             :         // pushed its config to us we report ready so it honours our config pushes
    1407         125 :         .deviceIsReady = _vtx_config_received ? uint8_t(1) : uint8_t(0),
    1408             :         .table = 0,
    1409             :         .table_bands = 0,
    1410             :         .table_channels = 0,
    1411             :         .table_powerLevels = 0
    1412         250 :     };
    1413             : 
    1414         125 :     sbuf_write_data(dst, &vtx_config, sizeof(vtx_config));
    1415         125 :     return MSP_RESULT_ACK;
    1416             : }
    1417             : 
    1418           6 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_in_vtxtable_powerlevel(sbuf_t *src, sbuf_t *dst)
    1419             : {
    1420           6 :     AP_VideoTX& vtx = AP::vtx();
    1421           6 :     if (!vtx.get_enabled()) {
    1422           0 :         return MSP_RESULT_ERROR;
    1423             :     }
    1424             : 
    1425             :     // [u8 powerLevel (one based)][u16 power dBm][u8 label length][label...]; the
    1426             :     // index is implied by the dBm ordering and the label is display only, so
    1427             :     // both are ignored and only the supported power level is recorded. The value
    1428             :     // is dBm (betaflight stores power tables in dBm), not mW; a zero entry is an
    1429             :     // unused slot.
    1430           6 :     if (sbuf_bytes_remaining(src) < 3) {
    1431           0 :         return MSP_RESULT_ERROR;
    1432             :     }
    1433           6 :     sbuf_read_u8(src);   // power level index, implied by ordering, ignored
    1434           6 :     const uint16_t power_dbm = sbuf_read_u16(src);
    1435           6 :     if (power_dbm > 0) {
    1436           6 :         vtx.update_power_dbm(uint8_t(power_dbm));
    1437             :     }
    1438             : 
    1439           6 :     return MSP_RESULT_ACK;
    1440             : }
    1441             : 
    1442             : #endif // AP_MSP_VIDEOTX_ENABLED
    1443             : 
    1444           0 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_board_info(sbuf_t *dst)
    1445             : {
    1446           0 :     const AP_FWVersion &fwver = AP::fwversion();
    1447             : 
    1448             :     struct PACKED {
    1449             :         uint16_t hw_revision;
    1450             :         uint8_t aio_flags;
    1451             :         uint8_t capabilities;
    1452             :         uint8_t fw_string_len;
    1453           0 :     } fw_info {};
    1454             : 
    1455             : #if HAL_WITH_OSD_BITMAP
    1456             :     fw_info.aio_flags = 2; // 2 == FC with MAX7456
    1457             : #else
    1458           0 :     fw_info.aio_flags = 0; // 0 == FC without MAX7456
    1459             : #endif
    1460           0 :     fw_info.fw_string_len = strlen(fwver.fw_string);
    1461             : 
    1462           0 :     sbuf_write_data(dst, "ARDU", BOARD_IDENTIFIER_LENGTH);
    1463           0 :     sbuf_write_data(dst, &fw_info, sizeof(fw_info));
    1464           0 :     sbuf_write_data(dst, fwver.fw_string, strlen(fwver.fw_string));
    1465           0 :     return MSP_RESULT_ACK;
    1466             : }
    1467             : 
    1468           0 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_build_info(sbuf_t *dst)
    1469             : {
    1470           0 :     const AP_FWVersion &fwver = AP::fwversion();
    1471             : 
    1472             :     // we don't use real dates here as that would mean we don't get
    1473             :     // consistent builds. Being able to reproduce the exact build at a
    1474             :     // later date is a valuable property of the code
    1475           0 :     sbuf_write_data(dst, "Jan 01 1980", BUILD_DATE_LENGTH);
    1476           0 :     sbuf_write_data(dst, "00:00:00", BUILD_TIME_LENGTH);
    1477           0 :     sbuf_write_data(dst, fwver.fw_hash_str, GIT_SHORT_REVISION_LENGTH);
    1478           0 :     return MSP_RESULT_ACK;
    1479             : }
    1480             : 
    1481           0 : MSPCommandResult AP_MSP_Telem_Backend::msp_process_out_uid(sbuf_t *dst)
    1482             : {
    1483           0 :     uint8_t id[12] {};
    1484           0 :     uint8_t len = sizeof(id);
    1485           0 :     hal.util->get_system_id_unformatted(id, len);
    1486           0 :     sbuf_write_data(dst, id, sizeof(id));
    1487           0 :     return MSP_RESULT_ACK;
    1488             : }
    1489             : 
    1490        8610 : void AP_MSP_Telem_Backend::hide_osd_items(void)
    1491             : {
    1492             : #if OSD_ENABLED
    1493        8610 :     AP_OSD *osd = AP::osd();
    1494        8610 :     if (osd == nullptr) {
    1495           0 :         return;
    1496             :     }
    1497             : #endif
    1498        8610 :     AP_MSP *msp = AP::msp();
    1499        8610 :     if (msp == nullptr) {
    1500           0 :         return;
    1501             :     }
    1502             : #if AP_BATTERY_ENABLED
    1503        8610 :     const AP_Notify &notify = AP::notify();
    1504             : #endif
    1505             :     // clear all and only set the flashing ones
    1506        8610 :     BIT_CLEAR(osd_hidden_items_bitmask, OSD_GPS_SATS);
    1507        8610 :     BIT_CLEAR(osd_hidden_items_bitmask, OSD_HOME_DIR);
    1508        8610 :     BIT_CLEAR(osd_hidden_items_bitmask, OSD_HOME_DIST);
    1509        8610 :     BIT_CLEAR(osd_hidden_items_bitmask, OSD_GPS_SPEED);
    1510        8610 :     BIT_CLEAR(osd_hidden_items_bitmask, OSD_CRAFT_NAME);
    1511        8610 :     BIT_CLEAR(osd_hidden_items_bitmask, OSD_AVG_CELL_VOLTAGE);
    1512        8610 :     BIT_CLEAR(osd_hidden_items_bitmask, OSD_MAIN_BATT_VOLTAGE);
    1513        8610 :     BIT_CLEAR(osd_hidden_items_bitmask, OSD_RTC_DATETIME);
    1514        8610 :     BIT_CLEAR(osd_hidden_items_bitmask, OSD_RSSI_VALUE);
    1515             : 
    1516        8610 :     if (msp->_msp_status.flashing_on) {
    1517             :         // flash satcount when no 3D Fix
    1518             :         gps_state_t gps_state;
    1519        4239 :         update_gps_state(gps_state);
    1520        4239 :         if (gps_state.fix_type == 0) {
    1521        1177 :             BIT_SET(osd_hidden_items_bitmask, OSD_GPS_SATS);
    1522             :         }
    1523             :         // flash home dir and distance if home is not set
    1524             :         home_state_t home_state;
    1525        4239 :         update_home_pos(home_state);
    1526        4239 :         if (!home_state.home_is_set) {
    1527        2236 :             BIT_SET(osd_hidden_items_bitmask, OSD_HOME_DIR);
    1528        2236 :             BIT_SET(osd_hidden_items_bitmask, OSD_HOME_DIST);
    1529             :         }
    1530             :         // flash airspeed if there's no estimate
    1531        4239 :         bool airspeed_en = false;
    1532             : #if OSD_ENABLED
    1533        4239 :         airspeed_en = osd->screen[msp->_msp_status.current_screen].aspeed.enabled;
    1534             : #endif
    1535        4239 :         if (airspeed_en) {
    1536             :             airspeed_state_t airspeed_state;
    1537           0 :             update_airspeed(airspeed_state);
    1538           0 :             if (!airspeed_state.airspeed_have_estimate) {
    1539           0 :                 BIT_SET(osd_hidden_items_bitmask, OSD_GPS_SPEED);
    1540             :             }
    1541             :         }
    1542             :         // flash text flightmode for 3secs after each change
    1543        4239 :         if (msp->_msp_status.flight_mode_focus) {
    1544         286 :             BIT_SET(osd_hidden_items_bitmask, OSD_CRAFT_NAME);
    1545             :         }
    1546             : #if AP_BATTERY_ENABLED
    1547             :         // flash battery on failsafe
    1548        4239 :         if (notify.flags.failsafe_battery) {
    1549           0 :             BIT_SET(osd_hidden_items_bitmask, OSD_AVG_CELL_VOLTAGE);
    1550           0 :             BIT_SET(osd_hidden_items_bitmask, OSD_MAIN_BATT_VOLTAGE);
    1551             :         }
    1552             : #endif
    1553             :         // flash rtc if no time available
    1554             : #if AP_RTC_ENABLED
    1555             :         uint64_t time_usec;
    1556        4239 :         if (!AP::rtc().get_utc_usec(time_usec)) {
    1557        1177 :             BIT_SET(osd_hidden_items_bitmask, OSD_RTC_DATETIME);
    1558             :         }
    1559             : #else
    1560             :             BIT_SET(osd_hidden_items_bitmask, OSD_RTC_DATETIME);
    1561             : #endif
    1562             :         // flash rssi if disabled
    1563             :         float rssi;
    1564        4239 :         if (!get_rssi(rssi)) {
    1565        4239 :             BIT_SET(osd_hidden_items_bitmask, OSD_RSSI_VALUE);
    1566             :         }
    1567             :     }
    1568             :     // disable flashing for min/max items
    1569        8610 :     if (displaying_stats_screen()) {
    1570           0 :         BIT_CLEAR(osd_hidden_items_bitmask, OSD_HOME_DIST);
    1571           0 :         BIT_CLEAR(osd_hidden_items_bitmask, OSD_GPS_SPEED);
    1572           0 :         BIT_CLEAR(osd_hidden_items_bitmask, OSD_CRAFT_NAME);
    1573           0 :         BIT_CLEAR(osd_hidden_items_bitmask, OSD_MAIN_BATT_VOLTAGE);
    1574           0 :         BIT_CLEAR(osd_hidden_items_bitmask, OSD_RSSI_VALUE);
    1575             :     }
    1576             : }
    1577             : 
    1578             : #if HAL_WITH_MSP_DISPLAYPORT
    1579             : // ported from betaflight/src/main/io/displayport_msp.c
    1580         548 : void AP_MSP_Telem_Backend::msp_displayport_heartbeat()
    1581             : {
    1582         548 :     const uint8_t subcmd[] = { msp_displayport_subcmd_e::MSP_DISPLAYPORT_HEARTBEAT };
    1583             : 
    1584             :     // heartbeat is used to:
    1585             :     // a) ensure display is not released by remote OSD software
    1586             :     // b) prevent OSD Slave boards from displaying a 'disconnected' status.
    1587         548 :     msp_send_packet(MSP_DISPLAYPORT, MSP::MSP_V1, subcmd, sizeof(subcmd), false);
    1588         548 : }
    1589             : 
    1590         548 : void AP_MSP_Telem_Backend::msp_displayport_grab()
    1591             : {
    1592         548 :     msp_displayport_heartbeat();
    1593         548 : }
    1594             : 
    1595           0 : void AP_MSP_Telem_Backend::msp_displayport_release()
    1596             : {
    1597           0 :     const uint8_t subcmd[] = { msp_displayport_subcmd_e::MSP_DISPLAYPORT_RELEASE };
    1598             : 
    1599           0 :     msp_send_packet(MSP_DISPLAYPORT, MSP::MSP_V1, subcmd, sizeof(subcmd), false);
    1600           0 : }
    1601             : 
    1602         548 : void AP_MSP_Telem_Backend::msp_displayport_clear_screen()
    1603             : {
    1604         548 :     const uint8_t subcmd[] = { msp_displayport_subcmd_e::MSP_DISPLAYPORT_CLEAR_SCREEN };
    1605             : 
    1606         548 :     msp_send_packet(MSP_DISPLAYPORT, MSP::MSP_V1, subcmd, sizeof(subcmd), false);
    1607         548 : }
    1608             : 
    1609         548 : void AP_MSP_Telem_Backend::msp_displayport_draw_screen()
    1610             : {
    1611         548 :     const uint8_t subcmd[] = { msp_displayport_subcmd_e::MSP_DISPLAYPORT_DRAW_SCREEN };
    1612         548 :     msp_send_packet(MSP_DISPLAYPORT, MSP::MSP_V1, subcmd, sizeof(subcmd), false);
    1613         548 : }
    1614             : 
    1615       18843 : void AP_MSP_Telem_Backend::msp_displayport_write_string(uint8_t col, uint8_t row, bool blink, const char *string, const uint8_t font_table)
    1616             : {
    1617       18843 :     const uint8_t len = strnlen(string, OSD_MSP_DISPLAYPORT_MAX_STRING_LENGTH);
    1618             : 
    1619             :     struct PACKED {
    1620             :         uint8_t sub_cmd;
    1621             :         uint8_t row;
    1622             :         uint8_t col;
    1623             :         uint8_t attr;
    1624             :         uint8_t text[OSD_MSP_DISPLAYPORT_MAX_STRING_LENGTH];
    1625       18843 :     } packet {};
    1626             : 
    1627       18843 :     packet.sub_cmd = msp_displayport_subcmd_e::MSP_DISPLAYPORT_WRITE_STRING;
    1628       18843 :     packet.row = row;
    1629       18843 :     packet.col = col;
    1630       18843 :     packet.attr |= (font_table & 0x03); // first 2 bits are for the table index
    1631       18843 :     if (blink) {
    1632           0 :         packet.attr |= DISPLAYPORT_MSP_ATTR_BLINK;
    1633             :     }
    1634       18843 :     memcpy(packet.text, string, len);
    1635             : 
    1636       18843 :     msp_send_packet(MSP_DISPLAYPORT, MSP::MSP_V1, &packet, 4 + len, false);
    1637       18843 : }
    1638             : 
    1639         548 : void AP_MSP_Telem_Backend::msp_displayport_set_options(const uint8_t font_index, const uint8_t screen_resolution)
    1640             : {
    1641         548 :     const uint8_t subcmd[] = { msp_displayport_subcmd_e::MSP_DISPLAYPORT_SET_OPTIONS, font_index, screen_resolution };
    1642         548 :     msp_send_packet(MSP_DISPLAYPORT, MSP::MSP_V1, subcmd, sizeof(subcmd), false);
    1643         548 : }
    1644             : #endif //HAL_WITH_MSP_DISPLAYPORT
    1645       10909 : bool AP_MSP_Telem_Backend::displaying_stats_screen() const
    1646             : {
    1647             : #if OSD_ENABLED
    1648       10909 :     AP_OSD *osd = AP::osd();
    1649       10909 :     if (osd == nullptr) {
    1650           0 :         return false;
    1651             :     }
    1652       10909 :     AP_MSP *msp = AP::msp();
    1653       10909 :     if (msp == nullptr) {
    1654           0 :         return false;
    1655             :     }
    1656       10909 :     return osd->screen[msp->_msp_status.current_screen].stat.enabled;
    1657             : #else
    1658             :     return false;
    1659             : #endif
    1660             : }
    1661             : 
    1662        4323 : bool AP_MSP_Telem_Backend::get_rssi(float &rssi) const
    1663             : {
    1664             : #if AP_RSSI_ENABLED
    1665        4323 :     AP_RSSI* ap_rssi = AP::rssi();
    1666        4323 :     if (ap_rssi == nullptr) {
    1667           0 :         return false;
    1668             :     }
    1669        4323 :     if (!ap_rssi->enabled()) {
    1670        4323 :         return false;
    1671             :     }
    1672           0 :     rssi =  ap_rssi->read_receiver_rssi(); // range is [0-1]
    1673           0 :     return true;
    1674             : #else
    1675             :     return false;
    1676             : #endif
    1677             : }
    1678             : 
    1679             : #endif //HAL_MSP_ENABLED

Generated by: LCOV version 1.14