diff --git a/libraries/AP_RCTelemetry/AP_CRSF_Telem.cpp b/libraries/AP_RCTelemetry/AP_CRSF_Telem.cpp new file mode 100644 index 0000000000..36bb7cd7ff --- /dev/null +++ b/libraries/AP_RCTelemetry/AP_CRSF_Telem.cpp @@ -0,0 +1,463 @@ +/* + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "AP_CRSF_Telem.h" +#include "AP_VideoTX.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if HAL_CRSF_TELEM_ENABLED + +#define CRSF_DEBUG +#ifdef CRSF_DEBUG +# define debug(fmt, args...) hal.console->printf("CRSF: " fmt "\n", ##args) +#else +# define debug(fmt, args...) do {} while(0) +#endif + +extern const AP_HAL::HAL& hal; + +AP_CRSF_Telem *AP_CRSF_Telem::singleton; + +AP_CRSF_Telem::AP_CRSF_Telem() : AP_RCTelemetry(0) +{ + singleton = this; +} + +AP_CRSF_Telem::~AP_CRSF_Telem(void) +{ + singleton = nullptr; +} + +bool AP_CRSF_Telem::init(void) +{ + // sanity check that we are using a UART for RC input + if (!AP::serialmanager().find_serial(AP_SerialManager::SerialProtocol_RCIN, 0) + && !AP::serialmanager().find_serial(AP_SerialManager::SerialProtocol_CRSF, 0)) { + return false; + } + return AP_RCTelemetry::init(); +} + +/* + setup ready for passthrough telem + */ +void AP_CRSF_Telem::setup_wfq_scheduler(void) +{ + // initialize packet weights for the WFQ scheduler + // priority[i] = 1/_scheduler.packet_weight[i] + // rate[i] = LinkRate * ( priority[i] / (sum(priority[1-n])) ) + + // CSRF telemetry rate is 150Hz (4ms) max, so these rates must fit + add_scheduler_entry(50, 100); // heartbeat 10Hz + add_scheduler_entry(50, 120); // Attitude and compass 8Hz + add_scheduler_entry(200, 1000); // parameters 1Hz + add_scheduler_entry(1300, 500); // battery 2Hz + add_scheduler_entry(550, 280); // GPS 3Hz + add_scheduler_entry(550, 500); // flight mode 2Hz +} + +void AP_CRSF_Telem::adjust_packet_weight(bool queue_empty) +{ +} + +// WFQ scheduler +bool AP_CRSF_Telem::is_packet_ready(uint8_t idx, bool queue_empty) +{ + switch (idx) { + case PARAMETERS: + return AP::vtx().have_params_changed() ||_vtx_power_change_pending || _vtx_freq_change_pending || _vtx_options_change_pending; + default: + return _enable_telemetry; + } +} + +// WFQ scheduler +void AP_CRSF_Telem::process_packet(uint8_t idx) +{ + // send packet + switch (idx) { + case HEARTBEAT: // HEARTBEAT + calc_heartbeat(); + break; + case ATTITUDE: + calc_attitude(); + break; + case PARAMETERS: // update various parameters + update_params(); + break; + case BATTERY: // BATTERY + calc_battery(); + break; + case GPS: // GPS + calc_gps(); + break; + case FLIGHT_MODE: // GPS + calc_flight_mode(); + break; + default: + break; + } +} + +// Process a frame from the CRSF protocol decoder +bool AP_CRSF_Telem::_process_frame(AP_RCProtocol_CRSF::FrameType frame_type, void* data) { + switch (frame_type) { + // this means we are connected to an RC receiver and can send telemetry + case AP_RCProtocol_CRSF::CRSF_FRAMETYPE_RC_CHANNELS_PACKED: + // the EVO sends battery frames and we should send telemetry back to populate the OS + case AP_RCProtocol_CRSF::CRSF_FRAMETYPE_BATTERY_SENSOR: + _enable_telemetry = true; + break; + + case AP_RCProtocol_CRSF::CRSF_FRAMETYPE_VTX: + process_vtx_frame((VTXFrame*)data); + break; + + case AP_RCProtocol_CRSF::CRSF_FRAMETYPE_VTX_TELEM: + process_vtx_telem_frame((VTXTelemetryFrame*)data); + break; + + default: + break; + } + return true; +} + +void AP_CRSF_Telem::process_vtx_frame(VTXFrame* vtx) { + vtx->user_frequency = be16toh(vtx->user_frequency); + + debug("VTX: SmartAudio: %d, Avail: %d, FreqMode: %d, Band: %d, Channel: %d, Freq: %d, PitMode: %d, Pwr: %d, Pit: %d", + vtx->smart_audio_ver, vtx->is_vtx_available, vtx->is_in_user_frequency_mode, + vtx->band, vtx->channel, vtx->is_in_user_frequency_mode ? vtx->user_frequency : AP_VideoTX::get_frequency_mhz(vtx->band, vtx->channel), + vtx->is_in_pitmode, vtx->power, vtx->pitmode); + AP_VideoTX& apvtx = AP::vtx(); + + apvtx.set_enabled(vtx->is_vtx_available); + apvtx.set_band(vtx->band); + apvtx.set_channel(vtx->channel); + if (vtx->is_in_user_frequency_mode) { + apvtx.set_frequency_mhz(vtx->user_frequency); + } else { + apvtx.set_frequency_mhz(AP_VideoTX::get_frequency_mhz(vtx->band, vtx->channel)); + } + // 14dBm (25mW), 20dBm (100mW), 26dBm (400mW), 29dBm (800mW) + switch (vtx->power) { + case 0: + apvtx.set_power_mw(25); + break; + case 1: + apvtx.set_power_mw(200); + break; + case 2: + apvtx.set_power_mw(500); + break; + case 3: + apvtx.set_power_mw(800); + break; + } + if (vtx->is_in_pitmode) { + apvtx.set_options(apvtx.get_options() | uint16_t(AP_VideoTX::VideoOptions::VTX_PITMODE)); + } else { + apvtx.set_options(apvtx.get_options() & ~uint16_t(AP_VideoTX::VideoOptions::VTX_PITMODE)); + } + // make sure the configured values now reflect reality + apvtx.set_defaults(); + + _vtx_power_change_pending = _vtx_freq_change_pending = _vtx_options_change_pending = false; +} + +void AP_CRSF_Telem::process_vtx_telem_frame(VTXTelemetryFrame* vtx) { + vtx->frequency = be16toh(vtx->frequency); + debug("VTXTelemetry: Freq: %d, PitMode: %d, Power: %d", vtx->frequency, vtx->pitmode, vtx->power); + + AP_VideoTX& apvtx = AP::vtx(); + + apvtx.set_frequency_mhz(vtx->frequency); + + AP_VideoTX::VideoBand band; + uint8_t channel; + if (AP_VideoTX::get_band_and_channel(vtx->frequency, band, channel)) { + apvtx.set_band(uint8_t(band)); + apvtx.set_channel(channel); + } + + apvtx.set_power_dbm(vtx->power); + + if (vtx->pitmode) { + apvtx.set_options(apvtx.get_options() | uint16_t(AP_VideoTX::VideoOptions::VTX_PITMODE)); + } else { + apvtx.set_options(apvtx.get_options() & ~uint16_t(AP_VideoTX::VideoOptions::VTX_PITMODE)); + } + // make sure the configured values now reflect reality + apvtx.set_defaults(); + + _vtx_power_change_pending = _vtx_freq_change_pending = _vtx_options_change_pending = false; +} + +// process any changed settings and schedule for transmission +void AP_CRSF_Telem::update() +{ +} + +void AP_CRSF_Telem::update_params() +{ + AP_VideoTX& vtx = AP::vtx(); + + _vtx_freq_change_pending = vtx.update_band() || vtx.update_channel() || _vtx_freq_change_pending; + _vtx_power_change_pending = vtx.update_power() || _vtx_power_change_pending; + _vtx_options_change_pending = vtx.update_options() || _vtx_options_change_pending; + + if (_vtx_freq_change_pending || _vtx_power_change_pending || _vtx_options_change_pending) { + debug("update_params(): freq %d, chan: %d->%d, band: %d->%d, pwr: %d->%d", + vtx.get_frequency_mhz(), + vtx.get_channel(), vtx.get_configured_channel(), + vtx.get_band(), vtx.get_configured_band(), + vtx.get_power_mw(), vtx.get_configured_power_mw()); + + _telem_type = AP_RCProtocol_CRSF::CRSF_FRAMETYPE_COMMAND; + _telem.ext.command.destination = AP_RCProtocol_CRSF::CRSF_ADDRESS_VTX; + _telem.ext.command.origin = AP_RCProtocol_CRSF::CRSF_ADDRESS_FLIGHT_CONTROLLER; + _telem.ext.command.command_id = AP_RCProtocol_CRSF::CRSF_COMMAND_VTX; + + + // make the desired frequency match the desired band and channel + if (_vtx_freq_change_pending) { + vtx.set_frequency_mhz(AP_VideoTX::get_frequency_mhz(vtx.get_configured_band(), vtx.get_configured_channel())); + } + + uint8_t len = 5; + if (_vtx_freq_change_pending && _vtx_freq_update) { + _telem.ext.command.payload[0] = AP_RCProtocol_CRSF::CRSF_COMMAND_VTX_FREQ; + _telem.ext.command.payload[1] = (vtx.get_frequency_mhz() & 0xFF00) >> 8; + _telem.ext.command.payload[2] = (vtx.get_frequency_mhz() & 0xFF); + _vtx_freq_update = false; + len++; + } else if (_vtx_freq_change_pending) { + _telem.ext.command.payload[0] = AP_RCProtocol_CRSF::CRSF_COMMAND_VTX_CHANNEL; + _telem.ext.command.payload[1] = vtx.get_configured_band() * VTX_MAX_CHANNELS + vtx.get_configured_channel(); + _vtx_freq_update = true; + } else if (_vtx_power_change_pending && _vtx_dbm_update) { + _telem.ext.command.payload[0] = AP_RCProtocol_CRSF::CRSF_COMMAND_VTX_POWER_DBM; + _telem.ext.command.payload[1] = vtx.get_configured_power_dbm(); + _vtx_dbm_update = false; + } else if (_vtx_power_change_pending) { + _telem.ext.command.payload[0] = AP_RCProtocol_CRSF::CRSF_COMMAND_VTX_POWER; + if (vtx.get_configured_power_mw() < 26) { + vtx.set_configured_power_mw(25); + _telem.ext.command.payload[1] = 0; + } else if (vtx.get_configured_power_mw() < 201) { + if (vtx.get_configured_power_mw() < 101) { + vtx.set_configured_power_mw(100); + } else { + vtx.set_configured_power_mw(200); + } + _telem.ext.command.payload[1] = 1; + } else if (vtx.get_configured_power_mw() < 501) { + if (vtx.get_configured_power_mw() < 401) { + vtx.set_configured_power_mw(400); + } else { + vtx.set_configured_power_mw(500); + } + _telem.ext.command.payload[1] = 2; + } else { + vtx.set_configured_power_mw(800); + _telem.ext.command.payload[1] = 3; + } + _vtx_dbm_update = true; + } else if (_vtx_options_change_pending) { + _telem.ext.command.payload[0] = AP_RCProtocol_CRSF::CRSF_COMMAND_VTX_PITMODE; + _telem.ext.command.payload[1] = vtx.get_configured_options() & uint16_t(AP_VideoTX::VideoOptions::VTX_PITMODE); + } + _telem_pending = true; + // calculate command crc + uint8_t* crcptr = &_telem.ext.command.destination; + uint8_t crc = crc8_dvb(0, AP_RCProtocol_CRSF::CRSF_FRAMETYPE_COMMAND, 0xBA); + for (uint8_t i = 0; i < len; i++) { + crc = crc8_dvb(crc, crcptr[i], 0xBA); + } + crcptr[len] = crc; + _telem_size = len + 1; + } +} + +// prepare parameter ping data +void AP_CRSF_Telem::calc_parameter_ping() +{ + _telem_type = AP_RCProtocol_CRSF::CRSF_FRAMETYPE_PARAM_DEVICE_PING; + _telem.ext.ping.destination = AP_RCProtocol_CRSF::CRSF_ADDRESS_VTX; + _telem.ext.ping.origin = AP_RCProtocol_CRSF::CRSF_ADDRESS_FLIGHT_CONTROLLER; + _telem_size = sizeof(ParameterPingFrame); + _telem_pending = true; +} + +// prepare qos data - mandatory frame that must be sent periodically +void AP_CRSF_Telem::calc_heartbeat() +{ + _telem.bcast.heartbeat.origin = AP_RCProtocol_CRSF::CRSF_ADDRESS_FLIGHT_CONTROLLER; + _telem_size = sizeof(HeartbeatFrame); + _telem_type = AP_RCProtocol_CRSF::CRSF_FRAMETYPE_HEARTBEAT; + _telem_pending = true; +} + +// prepare battery data +void AP_CRSF_Telem::calc_battery() +{ + const AP_BattMonitor &_battery = AP::battery(); + + _telem.bcast.battery.voltage = htobe16(uint16_t(roundf(_battery.voltage(0) * 10.0f))); + + float current; + if (!_battery.current_amps(current, 0)) { + current = 0; + } + _telem.bcast.battery.current = htobe16(int16_t(roundf(current * 10.0f))); + + float used_mah; + if (!_battery.consumed_mah(used_mah, 0)) { + used_mah = 0; + } + + _telem.bcast.battery.remaining = _battery.capacity_remaining_pct(0); + + int32_t capacity = _battery.pack_capacity_mah(0); + _telem.bcast.battery.capacity[0] = (capacity & 0xFF0000) >> 16; + _telem.bcast.battery.capacity[1] = (capacity & 0xFF00) >> 8; + _telem.bcast.battery.capacity[2] = (capacity & 0xFF); + + _telem_size = sizeof(BatteryFrame); + _telem_type = AP_RCProtocol_CRSF::CRSF_FRAMETYPE_BATTERY_SENSOR; + + _telem_pending = true; +} + +// prepare gps data +void AP_CRSF_Telem::calc_gps() +{ + const Location &loc = AP::gps().location(0); // use the first gps instance (same as in send_mavlink_gps_raw) + + _telem.bcast.gps.latitude = htobe32(loc.lat); + _telem.bcast.gps.longitude = htobe32(loc.lng); + _telem.bcast.gps.groundspeed = htobe16(roundf(AP::gps().ground_speed() * 100000 / 3600)); + _telem.bcast.gps.altitude = htobe16(loc.alt + 1000); + _telem.bcast.gps.gps_heading = htobe16(roundf(AP::gps().ground_course() * 100.0f)); + _telem.bcast.gps.satellites = AP::gps().num_sats(); + + _telem_size = sizeof(AP_CRSF_Telem::GPSFrame); + _telem_type = AP_RCProtocol_CRSF::CRSF_FRAMETYPE_GPS; + + _telem_pending = true; +} + +// prepare attitude data +void AP_CRSF_Telem::calc_attitude() +{ + AP_AHRS &_ahrs = AP::ahrs(); + WITH_SEMAPHORE(_ahrs.get_semaphore()); + + _telem.bcast.attitude.roll_angle = htobe16(roundf(_ahrs.roll * 10000.0f)); + _telem.bcast.attitude.pitch_angle = htobe16(roundf(_ahrs.pitch * 10000.0f)); + _telem.bcast.attitude.yaw_angle = htobe16(roundf(_ahrs.yaw * 10000.0f)); + + _telem_size = sizeof(AP_CRSF_Telem::AttitudeFrame); + _telem_type = AP_RCProtocol_CRSF::CRSF_FRAMETYPE_ATTITUDE; + + _telem_pending = true; +} + +// prepare flight mode data +void AP_CRSF_Telem::calc_flight_mode() +{ + AP_Notify * notify = AP_Notify::get_singleton(); + if (notify) { + hal.util->snprintf(_telem.bcast.flightmode.flight_mode, 16, "%s", notify->get_flight_mode_str()); + + _telem_size = sizeof(AP_CRSF_Telem::FlightModeFrame); + _telem_type = AP_RCProtocol_CRSF::CRSF_FRAMETYPE_FLIGHT_MODE; + _telem_pending = true; + } +} + +/* + fetch CRSF frame data + */ +bool AP_CRSF_Telem::_get_telem_data(AP_RCProtocol_CRSF::Frame* data) +{ + memset(&_telem, 0, sizeof(TelemetryPayload)); + run_wfq_scheduler(); + if (!_telem_pending) { + return false; + } + memcpy(data->payload, &_telem, _telem_size); + data->device_address = AP_RCProtocol_CRSF::CRSF_ADDRESS_FLIGHT_CONTROLLER; // sync byte + data->length = _telem_size + 2; + data->type = _telem_type; + + _telem_pending = false; + + return true; +} + +/* + fetch data for an external transport, such as CRSF + */ +bool AP_CRSF_Telem::process_frame(AP_RCProtocol_CRSF::FrameType frame_type, void* data) +{ + if (!get_singleton()) { + return false; + } + return singleton->_process_frame(frame_type, data); +} + +/* + fetch data for an external transport, such as CRSF + */ +bool AP_CRSF_Telem::get_telem_data(AP_RCProtocol_CRSF::Frame* data) +{ + if (!get_singleton()) { + return false; + } + return singleton->_get_telem_data(data); +} + +AP_CRSF_Telem *AP_CRSF_Telem::get_singleton(void) { + if (!singleton && !hal.util->get_soft_armed()) { + // if telem data is requested when we are disarmed and don't + // yet have a AP_CRSF_Telem object then try to allocate one + new AP_CRSF_Telem(); + // initialize the passthrough scheduler + if (singleton) { + singleton->init(); + } + } + return singleton; +} + +namespace AP { + AP_CRSF_Telem *crsf_telem() { + return AP_CRSF_Telem::get_singleton(); + } +}; + +#endif \ No newline at end of file diff --git a/libraries/AP_RCTelemetry/AP_CRSF_Telem.h b/libraries/AP_RCTelemetry/AP_CRSF_Telem.h new file mode 100644 index 0000000000..5ae7c6c0c9 --- /dev/null +++ b/libraries/AP_RCTelemetry/AP_CRSF_Telem.h @@ -0,0 +1,212 @@ +/* + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +#pragma once + +#include + +#ifndef HAL_CRSF_TELEM_ENABLED +#define HAL_CRSF_TELEM_ENABLED !HAL_MINIMIZE_FEATURES +#endif + +#if HAL_CRSF_TELEM_ENABLED + +#include +#include +#include +#include +#include "AP_RCTelemetry.h" +#include + +class AP_CRSF_Telem : public AP_RCTelemetry { +public: + AP_CRSF_Telem(); + ~AP_CRSF_Telem() override; + + /* Do not allow copies */ + AP_CRSF_Telem(const AP_CRSF_Telem &other) = delete; + AP_CRSF_Telem &operator=(const AP_CRSF_Telem&) = delete; + + // init - perform required initialisation + virtual bool init() override; + + static AP_CRSF_Telem *get_singleton(void); + + // Broadcast frame definitions courtesy of TBS + struct GPSFrame { // curious fact, calling this GPS makes sizeof(GPS) return 1! + int32_t latitude; // ( degree / 10`000`000 ) + int32_t longitude; // (degree / 10`000`000 ) + uint16_t groundspeed; // ( km/h / 100 ) + uint16_t gps_heading; // ( degree / 100 ) + uint16_t altitude; // ( meter - 1000m offset ) + uint8_t satellites; // in use ( counter ) + } PACKED; + + struct HeartbeatFrame { + uint8_t origin; // Device addres + }; + + struct BatteryFrame { + uint16_t voltage; // ( mV * 100 ) + uint16_t current; // ( mA * 100 ) + uint8_t capacity[3]; // ( mAh ) + uint8_t remaining; // ( percent ) + } PACKED; + + struct VTXFrame { +#if __BYTE_ORDER != __LITTLE_ENDIAN +#error "Only supported on little-endian architectures" +#endif + uint8_t origin; // address + // status + uint8_t is_in_pitmode : 1; + uint8_t is_in_user_frequency_mode : 1; + uint8_t unused : 2; + uint8_t is_vtx_available : 1; + uint8_t smart_audio_ver : 3; // SmartAudio_V1 = 0, SmartAudio_V2 = 1 + // band / channel + uint8_t channel : 3; // 1x-8x + uint8_t band : 5; // A, B, E, AirWave, Race + uint16_t user_frequency; + uint8_t power : 4; // 25mW = 0, 200mW = 1, 500mW = 2, 800mW = 3 + uint8_t pitmode : 4; // off = 0, In_Band = 1, Out_Band = 2; + } PACKED; + + struct VTXTelemetryFrame { + uint8_t origin; // address + uint8_t power; // power in dBm + uint16_t frequency; // frequency in Mhz + bool pitmode; // disable 0, enable 1 + } PACKED; + + struct LinkStatisticsFrame { + uint8_t uplink_rssi_ant1; // ( dBm * -1 ) + uint8_t uplink_rssi_ant2; // ( dBm * -1 ) + uint8_t uplink_status; // Package success rate / Link quality ( % ) + int8_t uplink_snr; // ( db ) + uint8_t active_antenna; // Diversity active antenna ( enum ant. 1 = 0, ant. 2 ) + uint8_t rf_mode; // ( enum 4fps = 0 , 50fps, 150hz) + uint8_t uplink_tx_power; // ( enum 0mW = 0, 10mW, 25 mW, 100 mW, 500 mW, 1000 mW, 2000mW ) + uint8_t downlink_rssi; // ( dBm * -1 ) + uint8_t downlink_status; // Downlink package success rate / Link quality ( % ) ● int8_t Downlink SNR ( db ) + } PACKED; + + struct AttitudeFrame { + int16_t pitch_angle; // ( rad / 10000 ) + int16_t roll_angle; // ( rad / 10000 ) + int16_t yaw_angle; // ( rad / 10000 ) + } PACKED; + + struct FlightModeFrame { + char flight_mode[16]; // ( Null-terminated string ) + } PACKED; + + // CRSF_FRAMETYPE_COMMAND + struct CommandFrame { + uint8_t destination; + uint8_t origin; + uint8_t command_id; + uint8_t payload[9]; // 8 maximum for LED command + crc8 + } PACKED; + + // CRSF_FRAMETYPE_PARAM_DEVICE_PING + struct ParameterPingFrame { + uint8_t destination; + uint8_t origin; + } PACKED; + + union BroadcastFrame { + GPSFrame gps; + HeartbeatFrame heartbeat; + BatteryFrame battery; + VTXFrame vtx; + LinkStatisticsFrame link; + AttitudeFrame attitude; + FlightModeFrame flightmode; + } PACKED; + + union ExtendedFrame { + CommandFrame command; + ParameterPingFrame ping; + } PACKED; + + union TelemetryPayload { + BroadcastFrame bcast; + ExtendedFrame ext; + } PACKED; + + // Process a frame from the CRSF protocol decoder + static bool process_frame(AP_RCProtocol_CRSF::FrameType frame_type, void* data); + // process any changed settings and schedule for transmission + void update(); + // get next telemetry data for external consumers of SPort data + static bool get_telem_data(AP_RCProtocol_CRSF::Frame* frame); + +private: + + enum SensorType { + HEARTBEAT, + ATTITUDE, + PARAMETERS, + BATTERY, + GPS, + FLIGHT_MODE, + NUM_SENSORS + }; + + // passthrough WFQ scheduler + bool is_packet_ready(uint8_t idx, bool queue_empty) override; + void process_packet(uint8_t idx) override; + void adjust_packet_weight(bool queue_empty) override; + + void calc_parameter_ping(); + void calc_heartbeat(); + void calc_battery(); + void calc_gps(); + void calc_attitude(); + void calc_flight_mode(); + void update_params(); + + void process_vtx_frame(VTXFrame* vtx); + void process_vtx_telem_frame(VTXTelemetryFrame* vtx); + + // setup ready for passthrough operation + void setup_wfq_scheduler(void) override; + + // get next telemetry data for external consumers + bool _get_telem_data(AP_RCProtocol_CRSF::Frame* data); + bool _process_frame(AP_RCProtocol_CRSF::FrameType frame_type, void* data); + + TelemetryPayload _telem; + uint8_t _telem_size; + uint8_t _telem_type; + + bool _telem_pending; + bool _enable_telemetry; + + // vtx state + bool _vtx_freq_update; // update using the frequency method or not + bool _vtx_dbm_update; // update using the dbm method or not + bool _vtx_freq_change_pending; // a vtx command has been issued but not confirmed by a vtx broadcast frame + bool _vtx_power_change_pending; + bool _vtx_options_change_pending; + + static AP_CRSF_Telem *singleton; +}; + +namespace AP { + AP_CRSF_Telem *crsf_telem(); +}; + +#endif diff --git a/libraries/AP_RCTelemetry/AP_RCTelemetry.cpp b/libraries/AP_RCTelemetry/AP_RCTelemetry.cpp index 003899d121..2316ab7794 100644 --- a/libraries/AP_RCTelemetry/AP_RCTelemetry.cpp +++ b/libraries/AP_RCTelemetry/AP_RCTelemetry.cpp @@ -89,7 +89,7 @@ void AP_RCTelemetry::run_wfq_scheduler(void) update_avg_packet_rate(); uint32_t now = AP_HAL::millis(); - uint8_t max_delay_idx = 0; + int8_t max_delay_idx = -1; float max_delay = 0; float delay = 0; @@ -125,6 +125,10 @@ void AP_RCTelemetry::run_wfq_scheduler(void) } } } + + if (max_delay_idx < 0) { // nothing was ready + return; + } now = AP_HAL::millis(); #ifdef TELEM_DEBUG _scheduler.packet_rate[max_delay_idx] = (_scheduler.packet_rate[max_delay_idx] + 1000 / (now - _scheduler.packet_timer[max_delay_idx])) / 2; diff --git a/libraries/AP_RCTelemetry/AP_RCTelemetry.h b/libraries/AP_RCTelemetry/AP_RCTelemetry.h index 43056340f3..18b1ec9dba 100644 --- a/libraries/AP_RCTelemetry/AP_RCTelemetry.h +++ b/libraries/AP_RCTelemetry/AP_RCTelemetry.h @@ -84,10 +84,10 @@ private: // passthrough WFQ scheduler virtual void setup_wfq_scheduler() = 0; - virtual bool get_next_msg_chunk(void) = 0; - virtual bool is_packet_ready(uint8_t idx, bool queue_empty) = 0; + virtual bool get_next_msg_chunk(void) { return false; } + virtual bool is_packet_ready(uint8_t idx, bool queue_empty) { return true; } virtual void process_packet(uint8_t idx) = 0; - virtual void adjust_packet_weight(bool queue_empty) = 0; + virtual void adjust_packet_weight(bool queue_empty) {}; void update_avg_packet_rate(); diff --git a/libraries/AP_RCTelemetry/AP_VideoTX.cpp b/libraries/AP_RCTelemetry/AP_VideoTX.cpp new file mode 100644 index 0000000000..2e4a73177b --- /dev/null +++ b/libraries/AP_RCTelemetry/AP_VideoTX.cpp @@ -0,0 +1,233 @@ +/* + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "AP_VideoTX.h" +#include "AP_CRSF_Telem.h" +#include + +extern const AP_HAL::HAL& hal; + +AP_VideoTX *AP_VideoTX::singleton; + +const AP_Param::GroupInfo AP_VideoTX::var_info[] = { + + // @Param: ENABLE + // @DisplayName: Is the Video Transmitter enabled or not + // @Description: Toggles the Video Transmitter on and off + // @Values: 0:Disable,1:Enable + AP_GROUPINFO_FLAGS("ENABLE", 1, AP_VideoTX, _enabled, 0, AP_PARAM_FLAG_ENABLE), + + // @Param: POWER + // @DisplayName: Video Transmitter Power Level + // @Description: Video Transmitter Power Level. Different VTXs support different power levels, the power level chosen will be rounded down to the nearest supported power level + // @Range: 1 1000 + AP_GROUPINFO("POWER", 2, AP_VideoTX, _power_mw, 0), + + // @Param: CHANNEL + // @DisplayName: Video Transmitter Channel + // @Description: Video Transmitter Channel + // @User: Standard + // @Range: 0 7 + AP_GROUPINFO("CHANNEL", 3, AP_VideoTX, _channel, 0), + + // @Param: BAND + // @DisplayName: Video Transmitter Band + // @Description: Video Transmitter Band + // @User: Standard + // @Values: 0:Band A,1:Band B,2:Band E,3:Airwave,4:RaceBand,5:Low RaceBand + AP_GROUPINFO("BAND", 4, AP_VideoTX, _band, 0), + + // @Param: FREQ + // @DisplayName: Video Transmitter Frequency + // @Description: Video Transmitter Frequency. The frequency is derived from the setting of BAND and CHANNEL + // @User: Standard + // @ReadOnly: True + // @Range: 5000 6000 + AP_GROUPINFO("FREQ", 5, AP_VideoTX, _frequency_mhz, 0), + + // @Param: OPTIONS + // @DisplayName: Video Transmitter Options + // @Description: Video Transmitter Options. + // @User: Advanced + // @Bitmask: 0:Pitmode + AP_GROUPINFO("OPTIONS", 6, AP_VideoTX, _options, 0), + + AP_GROUPEND +}; + +extern const AP_HAL::HAL& hal; + +const uint16_t AP_VideoTX::VIDEO_CHANNELS[AP_VideoTX::MAX_BANDS][VTX_MAX_CHANNELS] = +{ + { 5865, 5845, 5825, 5805, 5785, 5765, 5745, 5725}, /* Band A */ + { 5733, 5752, 5771, 5790, 5809, 5828, 5847, 5866}, /* Band B */ + { 5705, 5685, 5665, 5645, 5885, 5905, 5925, 5945}, /* Band E */ + { 5740, 5760, 5780, 5800, 5820, 5840, 5860, 5880}, /* Airwave */ + { 5658, 5695, 5732, 5769, 5806, 5843, 5880, 5917}, /* Race */ + { 5621, 5584, 5547, 5510, 5473, 5436, 5399, 5362} /* LO Race */ +}; + +AP_VideoTX::AP_VideoTX() +{ + if (singleton) { + AP_HAL::panic("Too many VTXs"); + return; + } + singleton = this; + + AP_Param::setup_object_defaults(this, var_info); +} + +AP_VideoTX::~AP_VideoTX(void) +{ + singleton = nullptr; +} + +bool AP_VideoTX::init(void) +{ + if (_initialized) { + return false; + } + + _current_power = _power_mw; + _current_band = _band; + _current_channel = _channel; + _current_options = _options; + _current_enabled = _enabled; + _initialized = true; + + return true; +} + +bool AP_VideoTX::get_band_and_channel(uint16_t freq, VideoBand& band, uint8_t& channel) +{ + for (uint8_t i = 0; i < AP_VideoTX::MAX_BANDS; i++) { + for (uint8_t j = 0; j < VTX_MAX_CHANNELS; j++) { + if (VIDEO_CHANNELS[i][j] == freq) { + band = VideoBand(i); + channel = j; + return true; + } + } + } + return false; +} + +// set the current power +void AP_VideoTX::set_configured_power_mw(uint16_t power) { + _power_mw.set_and_save(power); +} + +// set the power in dbm, rounding appropriately +void AP_VideoTX::set_power_dbm(uint8_t power) { + switch (power) { + case 14: + _current_power = 25; + break; + case 20: + _current_power = 100; + break; + case 23: + _current_power = 200; + break; + case 26: + _current_power = 400; + break; + case 27: + _current_power = 500; + break; + case 29: + _current_power = 800; + break; + default: + _current_power = uint16_t(roundf(powf(10, power / 10.0f))); + break; + } +} + +// get the power in dbm, rounding appropriately +uint8_t AP_VideoTX::get_configured_power_dbm() const { + switch (_power_mw.get()) { + case 25: return 14; + case 100: return 20; + case 200: return 23; + case 400: return 26; + case 500: return 27; + case 800: return 29; + default: + return uint8_t(roundf(10.0f * log10f(_power_mw))); + } +} + +// set the current channel +void AP_VideoTX::set_enabled(bool enabled) { + _current_enabled = enabled; + if (!_enabled.configured_in_storage()) { + _enabled.set_and_save(enabled); + } +} + +// peiodic update +void AP_VideoTX::update(void) +{ +#if HAL_CRSF_TELEM_ENABLED + AP_CRSF_Telem* crsf = AP::crsf_telem(); + + if (crsf != nullptr) { + crsf->update(); + } +#endif +} + +bool AP_VideoTX::have_params_changed() const +{ + return update_power() + || update_band() + || update_channel() + || update_options(); +} + +// set the current configured values if not currently set in storage +// this is necessary so that the current settings can be seen +void AP_VideoTX::set_defaults() +{ + if (_defaults_set) { + return; + } + + if (!_options.configured()) { + _options.set_and_save(_current_options); + } + if (!_channel.configured()) { + _channel.set_and_save(_current_channel); + } + if (!_band.configured()) { + _band.set_and_save(_current_band); + } + if (!_power_mw.configured()) { + _power_mw.set_and_save(_current_power); + } + + _defaults_set = true; + // Output a friendly message so the user knows the VTX has been detected + GCS_SEND_TEXT(MAV_SEVERITY_INFO, "VTX: Freq: %dMHz, Power: %dmw, Band: %d, Chan: %d", + _frequency_mhz.get(), _power_mw.get(), _band.get() + 1, _channel.get() + 1); +} + +namespace AP { + AP_VideoTX& vtx() { + return *AP_VideoTX::get_singleton(); + } +}; \ No newline at end of file diff --git a/libraries/AP_RCTelemetry/AP_VideoTX.h b/libraries/AP_RCTelemetry/AP_VideoTX.h new file mode 100644 index 0000000000..cb512d2de3 --- /dev/null +++ b/libraries/AP_RCTelemetry/AP_VideoTX.h @@ -0,0 +1,128 @@ +/* + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +#pragma once + +#include +#include + +#define VTX_MAX_CHANNELS 8 + +class AP_VideoTX { +public: + AP_VideoTX(); + ~AP_VideoTX(); + + /* Do not allow copies */ + AP_VideoTX(const AP_VideoTX &other) = delete; + AP_VideoTX &operator=(const AP_VideoTX&) = delete; + + // init - perform required initialisation + bool init(); + + // run any required updates + void update(); + + static AP_VideoTX *get_singleton(void) { + return singleton; + } + static const struct AP_Param::GroupInfo var_info[]; + + enum class VideoOptions { + VTX_PITMODE = 1 << 0 + }; + + enum VideoBand { + VTX_BAND_A, + VTX_BAND_B, + VTX_BAND_E, + VTX_BAND_FATSHARK, + VTX_BAND_RACEBAND, + VTX_BAND_LOW_RACEBAND, + MAX_BANDS + }; + + static const uint16_t VIDEO_CHANNELS[MAX_BANDS][VTX_MAX_CHANNELS]; + + static uint16_t get_frequency_mhz(uint8_t band, uint8_t channel) { return VIDEO_CHANNELS[band][channel]; } + static bool get_band_and_channel(uint16_t freq, VideoBand& band, uint8_t& channel); + + void set_frequency_mhz(uint16_t freq) { _frequency_mhz.set_and_save(freq); } + uint16_t get_frequency_mhz() const { return _frequency_mhz; } + // get / set power level + void set_power_mw(uint16_t power) { _current_power = power; } + void set_power_dbm(uint8_t power); + void set_configured_power_mw(uint16_t power); + uint16_t get_configured_power_mw() const { return _power_mw; } + uint16_t get_power_mw() const { return _current_power; } + uint8_t get_configured_power_dbm() const; + bool update_power() const { return _defaults_set && _power_mw != _current_power; } + // get / set the frequency band + void set_band(uint8_t band) { _current_band = band; } + uint8_t get_configured_band() const { return _band; } + uint8_t get_band() const { return _current_band; } + bool update_band() const { return _defaults_set && _band != _current_band; } + // get / set the frequency channel + void set_channel(uint8_t channel) { _current_channel = channel; } + uint8_t get_configured_channel() const { return _channel; } + uint8_t get_channel() const { return _current_channel; } + bool update_channel() const { return _defaults_set && _channel != _current_channel; } + // get / set vtx option + void set_options(uint8_t options) { _current_options = options; } + uint8_t get_configured_options() const { return _options; } + uint8_t get_options() const { return _current_options; } + bool update_options() const { return _defaults_set && _options != _current_options; } + // get / set whether the vtx is enabled + void set_enabled(bool enabled); + bool get_enabled() const { return _enabled; } + bool update_enabled() const { return _defaults_set && _enabled != _current_enabled; } + + // have the parameters been updated + bool have_params_changed() const; + // set configured defaults from current settings + void set_defaults(); + + static AP_VideoTX *singleton; + +private: + // channel frequency + AP_Int16 _frequency_mhz; + + // power output in mw + AP_Int16 _power_mw; + uint16_t _current_power; + + // frequency band + AP_Int8 _band; + uint16_t _current_band; + + // frequency channel + AP_Int8 _channel; + uint8_t _current_channel; + + // vtx options + AP_Int8 _options; + uint8_t _current_options; + + AP_Int8 _enabled; + bool _current_enabled; + + bool _initialized; + // when defaults have been configured + bool _defaults_set; +}; + +namespace AP { + AP_VideoTX& vtx(); +}; \ No newline at end of file