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 : /*
17 : * Architectural Principles of CRSF Scripted Menu Support
18 : *
19 : * The CRSF scripted menu system provides a mechanism for Lua scripts to create and manage
20 : * custom menu hierarchies on a CRSF-compatible transmitter. The architecture is a hybrid
21 : * model where the C++ backend manages the menu structure, navigation, and response
22 : * generation for folders, while the Lua script handles the content of "leaf" parameters
23 : * (like selections, commands, and numbers).
24 : *
25 : * The entire menu system is built on two main C++ classes: `ScriptedMenu` and
26 : * `ScriptedParameter`.
27 : *
28 : * 1. Global Menu List - All `ScriptedMenu` objects, regardless of their position in the
29 : * hierarchy, are stored in a single, flat, singly-linked list. The list is anchored
30 : * at the `scripted_menus` object. This flat structure is crucial for the global lookup
31 : * functions (`find_menu` and `find_parameter`).
32 : *
33 : * 2. Parent-Child Hierarchy - The hierarchical relationship between menus is established
34 : * using a "parameter-as-a-shortcut" or "symbolic link" pattern:
35 : * - Each `ScriptedMenu` object contains a `parent_id` member that correctly stores the
36 : * ID of its parent menu. This is used when generating responses for "back" navigation.
37 : * - To make a sub-menu appear inside a parent, a special `ScriptedParameter` object is
38 : * created within the parent's `params` array.
39 : * - The `id` of this special "parameter-link" is manually set to be identical to the
40 : * `id` of the sub-menu object it points to.
41 : *
42 : * This dual-representation (an ID belonging to both a menu object and a parameter-link) is
43 : * the key to the entire architecture.
44 : *
45 : * The interaction to navigate into a sub-menu is a multi-step process that relies on
46 : * careful request routing within the C++ backend.
47 : *
48 : * 1. Transmitter Request - When a user selects a sub-menu (e.g., "Roll Tuning" with ID 62),
49 : * the transmitter sends a `AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAMETER_READ` request for that ID. It does not
50 : * know whether the ID represents a parameter or a menu.
51 : *
52 : * 2. Initial Routing (`process_scripted_param_read`) - This is the central routing function.
53 : * - It first calls `find_parameter(62)`. This function searches the *entire global list* of
54 : * menus, checking the `params` array of each one. It successfully finds the "parameter-link"
55 : * with ID 62 inside the "Gain Tuner" menu object.
56 : * - Having found a parameter, it then performs a second check: `find_menu(62)`. This also
57 : * succeeds because a `ScriptedMenu` object with ID 62 exists.
58 : * - Because *both* lookups succeeded, the function knows this is a sub-menu navigation request.
59 : * It returns `false` to delegate the request to the generic C++ handler.
60 : * - If only the `find_parameter` call had succeeded, it would be a regular "leaf" parameter,
61 : * and the function would return `true` to send the event to the Lua script for handling.
62 : *
63 : * 3. Response Generation (`calc_parameter`) -
64 : * - The delegated request is handled by `calc_parameter`. It calls `find_menu(62)` to get the
65 : * `ScriptedMenu` object for "Roll Tuning".
66 : * - It then constructs a `FOLDER` type response (`0x2B Parameter settings (entry)`).
67 : * - Critically, it packs the `parent_id` (e.g., 41) from the found menu object into the response
68 : * payload.
69 : * - It also includes the list of child parameter IDs from that menu's `params` array.
70 : * - This response is sent to the transmitter, which uses the `parent_id` for "back" navigation
71 : * and the child list to populate the new menu screen.
72 : *
73 : * This architecture ensures a clean separation of concerns: C++ handles the complex and
74 : * stateful task of menu navigation, while Lua provides the dynamic content for the final
75 : * user-interactive elements.
76 : */
77 :
78 : #include "AP_RCTelemetry_config.h"
79 :
80 : #if HAL_CRSF_TELEM_ENABLED
81 :
82 : #include "AP_CRSF_Telem.h"
83 : #include <AP_VideoTX/AP_VideoTX.h>
84 : #include <AP_HAL/utility/sparse-endian.h>
85 : #include <AP_BattMonitor/AP_BattMonitor.h>
86 : #include <AP_Common/AP_FWVersion.h>
87 : #include <AP_GPS/AP_GPS.h>
88 : #include <GCS_MAVLink/GCS.h>
89 : #include <AP_RCProtocol/AP_RCProtocol_CRSF.h>
90 : #include <AP_SerialManager/AP_SerialManager.h>
91 : #include <AP_AHRS/AP_AHRS.h>
92 : #include <AP_Notify/AP_Notify.h>
93 : #include <AP_OSD/AP_OSD.h>
94 : #include <AP_Frsky_Telem/AP_Frsky_SPort_Passthrough.h>
95 : #include <math.h>
96 : #include <stdio.h>
97 : #include <AP_HAL/AP_HAL.h>
98 :
99 : #include <AP_VideoTX/AP_VideoTX.h>
100 :
101 : #include <AP_Vehicle/AP_Vehicle_Type.h>
102 :
103 : //#define CRSF_DEBUG
104 : #ifdef CRSF_DEBUG
105 : # define debug(fmt, args...) hal.console->printf("CRSF: " fmt "\n", ##args)
106 : #else
107 : # define debug(fmt, args...) do {} while(0)
108 : #endif
109 :
110 : extern const AP_HAL::HAL& hal;
111 : const uint8_t AP_CRSF_Telem::PASSTHROUGH_STATUS_TEXT_FRAME_MAX_SIZE;
112 : const uint8_t AP_CRSF_Telem::PASSTHROUGH_MULTI_PACKET_FRAME_MAX_SIZE;
113 : const uint8_t AP_CRSF_Telem::CRSF_RX_DEVICE_PING_MAX_RETRY;
114 :
115 : AP_CRSF_Telem *AP_CRSF_Telem::singleton;
116 :
117 2094 : AP_CRSF_Telem::AP_CRSF_Telem() : AP_RCTelemetry(0)
118 : {
119 2096 : singleton = this;
120 2096 : }
121 :
122 0 : AP_CRSF_Telem::~AP_CRSF_Telem(void)
123 : {
124 0 : singleton = nullptr;
125 0 : }
126 :
127 2096 : bool AP_CRSF_Telem::init(void)
128 : {
129 : // sanity check that we are using a UART for RC input
130 2096 : if (!AP::serialmanager().have_serial(AP_SerialManager::SerialProtocol_RCIN, 0)
131 2096 : && !AP::serialmanager().have_serial(AP_SerialManager::SerialProtocol_CRSF, 0)) {
132 2095 : return false;
133 : }
134 :
135 : #if AP_VIDEOTX_ENABLED
136 : // Someone explicitly configured CRSF control for VTX; set_provider_enabled
137 : // ignores this if the user has excluded CRSF from VTX_TYPES
138 1 : if (AP::serialmanager().have_serial(AP_SerialManager::SerialProtocol_CRSF, 0)) {
139 0 : AP::vtx().set_provider_enabled(AP_VideoTX::VTXType::CRSF);
140 : }
141 : #endif
142 :
143 1 : return AP_RCTelemetry::init();
144 : }
145 :
146 : /*
147 : setup ready for passthrough telem
148 : */
149 1 : void AP_CRSF_Telem::setup_wfq_scheduler(void)
150 : {
151 : // initialize packet weights for the WFQ scheduler
152 : // priority[i] = 1/_scheduler.packet_weight[i]
153 : // rate[i] = LinkRate * ( priority[i] / (sum(priority[1-n])) )
154 :
155 : // CRSF telemetry rate is 150Hz (4ms) max, so these rates must fit
156 1 : add_scheduler_entry(50, 100); // heartbeat 10Hz
157 1 : add_scheduler_entry(5, 20); // parameters 50Hz (generally not active unless requested by the TX)
158 1 : add_scheduler_entry(50, 200); // baro_vario 5Hz
159 1 : add_scheduler_entry(50, 200); // vario 5Hz
160 1 : add_scheduler_entry(50, 120); // Attitude and compass 8Hz
161 1 : add_scheduler_entry(200, 1000); // VTX parameters 1Hz
162 1 : add_scheduler_entry(1300, 500); // battery 2Hz
163 1 : add_scheduler_entry(550, 280); // GPS 3Hz
164 1 : add_scheduler_entry(550, 500); // flight mode 2Hz
165 1 : add_scheduler_entry(5000, 100); // passthrough max 10Hz
166 1 : add_scheduler_entry(5000, 500); // status text max 2Hz
167 1 : add_scheduler_entry(5, 20); // command 50Hz (generally not active unless requested by the TX)
168 1 : add_scheduler_entry(5, 500); // version ping 2Hz (only active at startup)
169 1 : add_scheduler_entry(5, 100); // device ping 10Hz (only active during TX loss, also see CRSF_RX_TIMEOUT)
170 1 : disable_scheduler_entry(DEVICE_PING);
171 1 : }
172 :
173 0 : void AP_CRSF_Telem::setup_custom_telemetry()
174 : {
175 0 : if (_custom_telem.init_done) {
176 0 : return;
177 : }
178 :
179 : // we need crossfire firmware version
180 0 : if (_crsf_version.pending) {
181 0 : return;
182 : }
183 :
184 0 : if (!rc().option_is_enabled(RC_Channels::Option::CRSF_CUSTOM_TELEMETRY)) {
185 0 : _custom_telem.init_done = true;
186 0 : GCS_SEND_TEXT(MAV_SEVERITY_DEBUG,"%s: bootstrap complete, fw %d.%02d", get_protocol_string(), _crsf_version.major, _crsf_version.minor);
187 0 : return;
188 : }
189 :
190 : // check if passthru already assigned
191 0 : const int8_t frsky_port = AP::serialmanager().find_portnum(AP_SerialManager::SerialProtocol_FrSky_SPort_Passthrough,0);
192 0 : if (frsky_port != -1) {
193 0 : GCS_SEND_TEXT(MAV_SEVERITY_CRITICAL, "%s: passthrough telemetry conflict on SERIAL%d", get_protocol_string(), frsky_port);
194 0 : _custom_telem.init_done = true;
195 0 : return;
196 : }
197 :
198 0 : AP_Frsky_SPort_Passthrough* passthrough = AP::frsky_passthrough_telem();
199 0 : if (passthrough == nullptr) {
200 0 : return;
201 : }
202 :
203 : // setup the frsky scheduler for crossfire and elrs
204 0 : passthrough->disable_scheduler_entry(AP_Frsky_SPort_Passthrough::GPS_LAT);
205 0 : passthrough->disable_scheduler_entry(AP_Frsky_SPort_Passthrough::GPS_LON);
206 0 : passthrough->disable_scheduler_entry(AP_Frsky_SPort_Passthrough::TEXT);
207 0 : passthrough->set_scheduler_entry_min_period(AP_Frsky_SPort_Passthrough::ATTITUDE, 350); // 3Hz
208 :
209 : // setup the crossfire scheduler for custom telemetry
210 0 : set_scheduler_entry(FLIGHT_MODE, 1200, 2000); // 0.5Hz
211 0 : set_scheduler_entry(HEARTBEAT, 2000, 5000); // 0.2Hz
212 :
213 0 : _telem_rf_mode = get_rf_mode();
214 : // setup custom telemetry for current rf_mode
215 0 : update_custom_telemetry_rates(_telem_rf_mode);
216 :
217 0 : GCS_SEND_TEXT(MAV_SEVERITY_DEBUG,"%s: custom telem init done, fw %d.%02d", get_protocol_string(), _crsf_version.major, _crsf_version.minor);
218 :
219 0 : _custom_telem.init_done = true;
220 : }
221 :
222 0 : void AP_CRSF_Telem::update_custom_telemetry_rates(AP_RCProtocol_CRSF::RFMode rf_mode)
223 : {
224 : // ignore rf mode changes if we are processing parameter packets
225 0 : if (_custom_telem.params_mode_active) {
226 0 : return;
227 : }
228 :
229 0 : if (is_high_speed_telemetry(rf_mode)) {
230 : // standard telemetry for high data rates
231 0 : set_scheduler_entry(BATTERY, 1000, 1000); // 1Hz
232 0 : set_scheduler_entry(ATTITUDE, 1000, 1000); // 1Hz
233 0 : set_scheduler_entry(BARO_VARIO, 1000, 1000); // 1Hz
234 0 : set_scheduler_entry(VARIO, 1000, 1000); // 1Hz
235 : // custom telemetry for high data rates
236 0 : set_scheduler_entry(GPS, 550, 500); // 2.0Hz
237 0 : set_scheduler_entry(PASSTHROUGH, 100, 100); // 8Hz
238 0 : set_scheduler_entry(STATUS_TEXT, 200, 750); // 1.5Hz
239 : } else {
240 : // standard telemetry for low data rates
241 0 : set_scheduler_entry(BATTERY, 1000, 2000); // 0.5Hz
242 0 : set_scheduler_entry(ATTITUDE, 1000, 3000); // 0.33Hz
243 0 : set_scheduler_entry(BARO_VARIO, 1000, 3000); // 0.33Hz
244 0 : set_scheduler_entry(VARIO, 1000, 3000); // 0.33Hz
245 0 : if (is_elrs()) {
246 : // ELRS custom telemetry for low data rates
247 0 : set_scheduler_entry(GPS, 550, 1000); // 1.0Hz
248 0 : set_scheduler_entry(PASSTHROUGH, 350, 500); // 2.0Hz
249 0 : set_scheduler_entry(STATUS_TEXT, 500, 2000); // 0.5Hz
250 : } else {
251 : // CRSF custom telemetry for low data rates
252 0 : set_scheduler_entry(GPS, 550, 1000); // 1.0Hz
253 0 : set_scheduler_entry(PASSTHROUGH, 500, 3000); // 0.3Hz
254 0 : set_scheduler_entry(STATUS_TEXT, 600, 2000); // 0.5Hz
255 : }
256 : }
257 : }
258 :
259 0 : bool AP_CRSF_Telem::process_rf_mode_changes()
260 : {
261 0 : const AP_RCProtocol_CRSF::RFMode current_rf_mode = get_rf_mode();
262 0 : uint32_t now = AP_HAL::millis();
263 :
264 : // the presence of a uart indicates that we are using CRSF for RC control
265 0 : AP_RCProtocol_CRSF* crsf = AP::crsf();
266 0 : AP_HAL::UARTDriver* uart = nullptr;
267 0 : if (crsf != nullptr) {
268 0 : uart = crsf->get_UART();
269 : }
270 :
271 0 : if (uart == nullptr) {
272 0 : return true;
273 : }
274 :
275 0 : if (!crsf->is_detected()) {
276 0 : return false;
277 : }
278 : // not ready yet
279 0 : if (!uart->is_initialized()) {
280 0 : return false;
281 : }
282 : #if !defined (STM32H7)
283 : // warn the user if their setup is sub-optimal, H7 does not need DMA on serial port
284 0 : if (_telem_bootstrap_msg_pending && !uart->is_dma_enabled()) {
285 0 : GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "%s: running on non-DMA serial port", get_protocol_string());
286 : }
287 : #endif
288 : // note if option was set to show LQ in place of RSSI
289 0 : bool current_lq_as_rssi_active = rc().option_is_enabled(RC_Channels::Option::USE_CRSF_LQ_AS_RSSI);
290 0 : if(_telem_bootstrap_msg_pending || _noted_lq_as_rssi_active != current_lq_as_rssi_active){
291 0 : _noted_lq_as_rssi_active = current_lq_as_rssi_active;
292 0 : GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s: RSSI now displays %s", get_protocol_string(), current_lq_as_rssi_active ? " as LQ" : "normally");
293 : }
294 0 : _telem_bootstrap_msg_pending = false;
295 :
296 0 : const bool is_high_speed = is_high_speed_telemetry(current_rf_mode);
297 0 : if ((now - _telem_last_report_ms > 5000)) {
298 : // report an RF mode change or a change in telemetry rate if we haven't done so in the last 5s
299 0 : if (!rc().option_is_enabled(RC_Channels::Option::SUPPRESS_CRSF_MESSAGE) && (_telem_rf_mode != current_rf_mode || abs(int16_t(_telem_last_avg_rate) - int16_t(_scheduler.avg_packet_rate)) > 25)) {
300 0 : GCS_SEND_TEXT(MAV_SEVERITY_INFO, "%s: Link rate %dHz, Telemetry rate %dHz",
301 : get_protocol_string(), crsf->get_link_rate(_crsf_version.protocol), get_telemetry_rate());
302 : }
303 : // tune the scheduler based on telemetry speed high/low transitions
304 0 : if (_telem_is_high_speed != is_high_speed) {
305 0 : update_custom_telemetry_rates(current_rf_mode);
306 : }
307 0 : _telem_is_high_speed = is_high_speed;
308 0 : _telem_rf_mode = current_rf_mode;
309 0 : _telem_last_avg_rate = _scheduler.avg_packet_rate;
310 0 : if (_telem_last_report_ms == 0) { // only want to show bootstrap messages once
311 0 : _telem_bootstrap_msg_pending = true;
312 : }
313 0 : _telem_last_report_ms = now;
314 : }
315 0 : return true;
316 : }
317 :
318 : // return custom frame id based on fw version
319 0 : uint8_t AP_CRSF_Telem::get_custom_telem_frame_id() const
320 : {
321 0 : if (!_crsf_version.pending &&
322 0 : ((_crsf_version.major > 4 || (_crsf_version.major == 4 && _crsf_version.minor >= 6))
323 0 : || is_elrs())) {
324 0 : return AP_CRSF_Protocol::CRSF_FRAMETYPE_AP_CUSTOM_TELEM;
325 : }
326 0 : return AP_CRSF_Protocol::CRSF_FRAMETYPE_AP_CUSTOM_TELEM_LEGACY;
327 : }
328 :
329 0 : AP_RCProtocol_CRSF::RFMode AP_CRSF_Telem::get_rf_mode() const
330 : {
331 0 : AP_RCProtocol_CRSF* crsf = AP::crsf();
332 0 : if (crsf == nullptr) {
333 0 : return AP_RCProtocol_CRSF::RFMode::RF_MODE_UNKNOWN;
334 : }
335 :
336 0 : if (!_crsf_version.pending && _crsf_version.use_rf_mode) {
337 0 : if (is_elrs()) {
338 0 : return static_cast<AP_RCProtocol_CRSF::RFMode>(uint8_t(AP_RCProtocol_CRSF::RFMode::CRSF_RF_MAX_MODES) + crsf->get_link_status().rf_mode);
339 : }
340 0 : return static_cast<AP_RCProtocol_CRSF::RFMode>(crsf->get_link_status().rf_mode);
341 0 : } else if (is_tracer()) {
342 0 : return AP_RCProtocol_CRSF::RFMode::CRSF_RF_MODE_250HZ;
343 : }
344 :
345 : /*
346 : Note:
347 : - CRSF rf mode 2 on UARTS with DMA runs @160Hz
348 : - CRSF rf mode 2 on UARTS with no DMA runs @70Hz
349 : */
350 0 : if (get_avg_packet_rate() < 40U) {
351 : // no DMA CRSF rf mode 1
352 0 : return AP_RCProtocol_CRSF::RFMode::CRSF_RF_MODE_50HZ;
353 : }
354 0 : if (get_avg_packet_rate() > 120U) {
355 : // DMA CRSF rf mode 2
356 0 : return AP_RCProtocol_CRSF::RFMode::CRSF_RF_MODE_150HZ;
357 : }
358 0 : if (get_max_packet_rate() < 120U) {
359 : // no CRSF DMA rf mode 2
360 0 : return AP_RCProtocol_CRSF::RFMode::CRSF_RF_MODE_150HZ;
361 : }
362 0 : return AP_RCProtocol_CRSF::RFMode::CRSF_RF_MODE_50HZ;
363 : }
364 :
365 0 : bool AP_CRSF_Telem::is_high_speed_telemetry(const AP_RCProtocol_CRSF::RFMode rf_mode) const
366 : {
367 0 : if (_crsf_version.protocol != AP_CRSF_Protocol::ProtocolType::PROTOCOL_ELRS) {
368 0 : return rf_mode == AP_RCProtocol_CRSF::RFMode::CRSF_RF_MODE_150HZ || rf_mode == AP_RCProtocol_CRSF::RFMode::CRSF_RF_MODE_250HZ;
369 : }
370 0 : return get_telemetry_rate() > 30;
371 : }
372 :
373 0 : uint16_t AP_CRSF_Telem::get_telemetry_rate() const
374 : {
375 0 : if (_crsf_version.protocol != AP_CRSF_Protocol::ProtocolType::PROTOCOL_ELRS) {
376 0 : return get_avg_packet_rate();
377 : }
378 0 : AP_RCProtocol_CRSF* crsf = AP::crsf();
379 0 : if (crsf == nullptr) {
380 0 : return get_avg_packet_rate();
381 : }
382 : // ELRS sends 1 telemetry frame every n RC frames
383 : // the 1:n ratio is user selected
384 : // RC rate is measured by get_avg_packet_rate()
385 : // telemetry rate = air rate - RC rate
386 0 : return crsf->get_link_rate(_crsf_version.protocol) - get_avg_packet_rate();
387 : }
388 :
389 262929 : void AP_CRSF_Telem::queue_message(MAV_SEVERITY severity, const char *text)
390 : {
391 : // no need to queue status text messages when crossfire
392 : // custom telemetry is not enabled
393 262929 : if (!rc().option_is_enabled(RC_Channels::Option::CRSF_CUSTOM_TELEMETRY)) {
394 262929 : return;
395 : }
396 0 : AP_RCTelemetry::queue_message(severity, text);
397 : }
398 :
399 : /*
400 : disable telemetry entries that require a transmitter to be present
401 : */
402 0 : void AP_CRSF_Telem::disable_tx_entries()
403 : {
404 0 : disable_scheduler_entry(ATTITUDE);
405 0 : disable_scheduler_entry(BARO_VARIO);
406 0 : disable_scheduler_entry(VARIO);
407 0 : disable_scheduler_entry(BATTERY);
408 0 : disable_scheduler_entry(GPS);
409 0 : disable_scheduler_entry(FLIGHT_MODE);
410 0 : disable_scheduler_entry(PASSTHROUGH);
411 0 : disable_scheduler_entry(STATUS_TEXT);
412 : // GENERAL_COMMAND and PARAMETERS will only be sent under very specific circumstances
413 0 : }
414 :
415 : /*
416 : enable telemetry entries that require a transmitter to be present
417 : */
418 0 : void AP_CRSF_Telem::enable_tx_entries()
419 : {
420 0 : enable_scheduler_entry(ATTITUDE);
421 0 : enable_scheduler_entry(BARO_VARIO);
422 0 : enable_scheduler_entry(VARIO);
423 0 : enable_scheduler_entry(BATTERY);
424 0 : enable_scheduler_entry(GPS);
425 0 : enable_scheduler_entry(FLIGHT_MODE);
426 0 : enable_scheduler_entry(PASSTHROUGH);
427 0 : enable_scheduler_entry(STATUS_TEXT);
428 :
429 0 : update_custom_telemetry_rates(_telem_rf_mode);
430 0 : }
431 :
432 0 : void AP_CRSF_Telem::enter_scheduler_params_mode()
433 : {
434 : debug("parameter passthrough enabled");
435 0 : set_scheduler_entry(HEARTBEAT, 50, 200); // heartbeat 5Hz
436 0 : disable_tx_entries();
437 0 : }
438 :
439 0 : void AP_CRSF_Telem::exit_scheduler_params_mode()
440 : {
441 : debug("parameter passthrough disabled");
442 : // setup the crossfire scheduler for custom telemetry
443 0 : set_scheduler_entry(HEARTBEAT, 2000, 5000); // 0.2Hz
444 0 : enable_tx_entries();
445 0 : }
446 :
447 0 : bool AP_CRSF_Telem::should_enter_scheduler_params_mode() const
448 : {
449 0 : if (hal.util->get_soft_armed()) {
450 0 : return false;
451 : }
452 :
453 0 : if (_pending_request.frame_type > 0 && _pending_request.frame_type != AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAM_DEVICE_INFO) {
454 0 : return true;
455 : }
456 : #if AP_CRSF_SCRIPTING_ENABLED
457 0 : if (!ready_params.is_empty()) {
458 0 : return true;
459 : }
460 : #endif
461 0 : return false;
462 : }
463 :
464 0 : void AP_CRSF_Telem::adjust_packet_weight(bool queue_empty)
465 : {
466 0 : uint32_t now_ms = AP_HAL::millis();
467 0 : setup_custom_telemetry();
468 :
469 : /*
470 : whenever we detect a pending request we configure the scheduler
471 : to allow faster parameters processing.
472 : We start a "fast parameter window" that we close after 5sec
473 : */
474 0 : bool expired = (now_ms - _custom_telem.params_mode_start_ms) > 5000;
475 0 : if (!_custom_telem.params_mode_active && should_enter_scheduler_params_mode()) {
476 : // fast window start
477 0 : _custom_telem.params_mode_start_ms = now_ms;
478 0 : _custom_telem.params_mode_active = true;
479 0 : enter_scheduler_params_mode();
480 0 : } else if (expired && _custom_telem.params_mode_active) {
481 : // fast window stop
482 0 : _custom_telem.params_mode_active = false;
483 0 : exit_scheduler_params_mode();
484 : }
485 0 : }
486 :
487 : // WFQ scheduler
488 0 : bool AP_CRSF_Telem::is_packet_ready(uint8_t idx, bool queue_empty)
489 : {
490 0 : if (!process_rf_mode_changes() && !_bind_request_pending) {
491 0 : return false;
492 : }
493 :
494 0 : switch (idx) {
495 0 : case PARAMETERS:
496 0 : if (_pending_request.frame_type > 0) {
497 0 : return true;
498 : }
499 : #if AP_CRSF_SCRIPTING_ENABLED
500 0 : if (!ready_params.is_empty()) {
501 0 : return true;
502 : }
503 : #endif
504 0 : return false;
505 0 : case VTX_PARAMETERS:
506 : #if AP_VIDEOTX_ENABLED
507 0 : return AP::vtx().have_params_changed() ||_vtx_power_change_pending || _vtx_freq_change_pending || _vtx_options_change_pending;
508 : #else
509 : return false;
510 : #endif
511 0 : case PASSTHROUGH:
512 0 : return rc().option_is_enabled(RC_Channels::Option::CRSF_CUSTOM_TELEMETRY);
513 0 : case STATUS_TEXT:
514 0 : return rc().option_is_enabled(RC_Channels::Option::CRSF_CUSTOM_TELEMETRY) && !queue_empty;
515 0 : case GENERAL_COMMAND:
516 0 : return _baud_rate_request.pending || _bind_request_pending;
517 0 : case VERSION_PING:
518 0 : return _crsf_version.pending && AP::crsf()->is_detected(); // only send pings if protocol has been detected
519 0 : case HEARTBEAT:
520 0 : return true; // always send heartbeat if enabled
521 0 : case DEVICE_PING:
522 0 : return !_crsf_version.pending; // only send pings if version has been negotiated
523 0 : default:
524 0 : return _enable_telemetry;
525 : }
526 : }
527 :
528 : // WFQ scheduler
529 0 : void AP_CRSF_Telem::process_packet(uint8_t idx)
530 : {
531 : // send packet
532 0 : switch (idx) {
533 0 : case HEARTBEAT: // HEARTBEAT
534 0 : calc_heartbeat();
535 0 : break;
536 0 : case PARAMETERS: // update parameter settings
537 0 : process_pending_requests();
538 0 : break;
539 0 : case BARO_VARIO:
540 0 : calc_baro_vario();
541 0 : break;
542 0 : case VARIO:
543 0 : calc_vario();
544 0 : break;
545 0 : case ATTITUDE:
546 0 : calc_attitude();
547 0 : break;
548 : #if AP_VIDEOTX_ENABLED
549 0 : case VTX_PARAMETERS: // update various VTX parameters
550 0 : update_vtx_params();
551 0 : break;
552 : #endif
553 0 : case BATTERY: // BATTERY
554 0 : calc_battery();
555 0 : break;
556 0 : case GPS: // GPS
557 0 : calc_gps();
558 0 : break;
559 0 : case FLIGHT_MODE: // GPS
560 0 : calc_flight_mode();
561 0 : break;
562 0 : case PASSTHROUGH:
563 0 : if (is_high_speed_telemetry(_telem_rf_mode)) {
564 : // on fast links we have 1:1 ratio between
565 : // passthrough frames and crossfire frames
566 0 : get_single_packet_passthrough_telem_data();
567 : } else {
568 : // on slower links we pack many passthrough
569 : // frames in a single crossfire one (up to 9)
570 0 : const uint8_t size = is_elrs() ? 3 : AP_CRSF_Telem::PASSTHROUGH_MULTI_PACKET_FRAME_MAX_SIZE;
571 0 : get_multi_packet_passthrough_telem_data(size);
572 : }
573 0 : break;
574 0 : case STATUS_TEXT:
575 0 : calc_status_text();
576 0 : break;
577 0 : case GENERAL_COMMAND:
578 0 : if (_bind_request_pending) {
579 0 : calc_bind();
580 : } else {
581 0 : calc_command_response();
582 : }
583 0 : break;
584 0 : case VERSION_PING:
585 : // to get crossfire firmware version we send an RX device ping
586 0 : if (_crsf_version.retry_count++ > CRSF_RX_DEVICE_PING_MAX_RETRY) {
587 0 : _crsf_version.pending = false;
588 0 : _crsf_version.minor = 0;
589 0 : _crsf_version.major = 0;
590 0 : disable_scheduler_entry(VERSION_PING);
591 0 : GCS_SEND_TEXT(MAV_SEVERITY_DEBUG,"%s: RX device ping failed", get_protocol_string());
592 : } else {
593 0 : calc_device_ping(AP_CRSF_Protocol::CRSF_ADDRESS_CRSF_RECEIVER);
594 0 : uint32_t tnow_ms = AP_HAL::millis();
595 0 : if ((tnow_ms - _crsf_version.last_request_info_ms) > 5000) {
596 0 : _crsf_version.last_request_info_ms = tnow_ms;
597 0 : GCS_SEND_TEXT(MAV_SEVERITY_DEBUG,"%s: requesting RX device info", get_protocol_string());
598 : }
599 : }
600 0 : break;
601 0 : case DEVICE_PING:
602 0 : calc_device_ping(AP_CRSF_Protocol::CRSF_ADDRESS_CRSF_RECEIVER);
603 0 : break;
604 0 : default:
605 0 : break;
606 : }
607 0 : }
608 :
609 : // Process a frame from the CRSF protocol decoder
610 0 : bool AP_CRSF_Telem::_process_frame(AP_RCProtocol_CRSF::FrameType frame_type, void* data, uint8_t length) {
611 0 : switch (frame_type) {
612 : // this means we are connected to an RC receiver and can send telemetry
613 0 : case AP_CRSF_Protocol::CRSF_FRAMETYPE_RC_CHANNELS_PACKED:
614 : case AP_CRSF_Protocol::CRSF_FRAMETYPE_SUBSET_RC_CHANNELS_PACKED:
615 : // the EVO sends battery frames and we should send telemetry back to populate the OSD
616 : case AP_CRSF_Protocol::CRSF_FRAMETYPE_BATTERY_SENSOR:
617 0 : _enable_telemetry = true;
618 0 : break;
619 :
620 : #if AP_VIDEOTX_ENABLED
621 0 : case AP_CRSF_Protocol::CRSF_FRAMETYPE_VTX:
622 0 : process_vtx_frame((VTXFrame*)data);
623 0 : break;
624 :
625 0 : case AP_CRSF_Protocol::CRSF_FRAMETYPE_VTX_TELEM:
626 0 : process_vtx_telem_frame((VTXTelemetryFrame*)data);
627 0 : break;
628 : #endif
629 :
630 0 : case AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAM_DEVICE_PING:
631 0 : process_ping_frame((ParameterPingFrame*)data);
632 0 : break;
633 :
634 0 : case AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAMETER_READ:
635 0 : process_param_read_frame((ParameterSettingsReadFrame*)data);
636 0 : break;
637 :
638 0 : case AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAMETER_WRITE:
639 0 : process_param_write_frame((ParameterSettingsWriteFrame*)data, length);
640 0 : break;
641 :
642 0 : case AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAM_DEVICE_INFO:
643 0 : process_device_info_frame((ParameterDeviceInfoFrame*)data);
644 0 : break;
645 :
646 0 : case AP_CRSF_Protocol::CRSF_FRAMETYPE_COMMAND:
647 0 : process_command_frame((CommandFrame*)data);
648 0 : break;
649 :
650 0 : default:
651 0 : break;
652 : }
653 0 : return true;
654 : }
655 :
656 : #if AP_VIDEOTX_ENABLED
657 0 : void AP_CRSF_Telem::process_vtx_frame(VTXFrame* vtx) {
658 0 : vtx->user_frequency = be16toh(vtx->user_frequency);
659 :
660 : debug("VTX: SmartAudio: %d, Avail: %d, FreqMode: %d, Band: %d, Channel: %d, Freq: %d, PitMode: %d, Pwr: %d, Pit: %d",
661 : vtx->smart_audio_ver, vtx->is_vtx_available, vtx->is_in_user_frequency_mode,
662 : vtx->band, vtx->channel, vtx->is_in_user_frequency_mode ? vtx->user_frequency : AP_VideoTX::get_frequency_mhz(vtx->band, vtx->channel),
663 : vtx->is_in_pitmode, vtx->power, vtx->pitmode);
664 0 : AP_VideoTX& apvtx = AP::vtx();
665 :
666 : // the user may have a VTX connected but not want AP to control it
667 : // (for instance because they are using myVTX on the transmitter), or may
668 : // have handed VTX control to another transport via VTX_TYPES
669 0 : if (!apvtx.get_enabled() || !apvtx.is_type_enabled(AP_VideoTX::VTXType::CRSF)) {
670 0 : return;
671 : }
672 :
673 0 : apvtx.set_provider_enabled(AP_VideoTX::VTXType::CRSF);
674 :
675 0 : apvtx.set_band(vtx->band);
676 0 : apvtx.set_channel(vtx->channel);
677 0 : if (vtx->is_in_user_frequency_mode) {
678 0 : apvtx.set_frequency_mhz(vtx->user_frequency);
679 : } else {
680 0 : apvtx.set_frequency_mhz(AP_VideoTX::get_frequency_mhz(vtx->band, vtx->channel));
681 : }
682 : // 14dBm (25mW), 20dBm (100mW), 26dBm (400mW), 29dBm (800mW)
683 0 : switch (vtx->power) {
684 0 : case 0:
685 0 : apvtx.set_power_mw(25);
686 0 : break;
687 0 : case 1:
688 0 : apvtx.set_power_mw(100);
689 0 : break;
690 0 : case 2:
691 0 : apvtx.set_power_mw(400);
692 0 : break;
693 0 : case 3:
694 0 : apvtx.set_power_mw(800);
695 0 : break;
696 : }
697 0 : if (vtx->is_in_pitmode) {
698 0 : apvtx.set_options(apvtx.get_options() | uint8_t(AP_VideoTX::VideoOptions::VTX_PITMODE));
699 : } else {
700 0 : apvtx.set_options(apvtx.get_options() & ~uint8_t(AP_VideoTX::VideoOptions::VTX_PITMODE));
701 : }
702 : // make sure the configured values now reflect reality
703 0 : if (!apvtx.set_defaults() && (_vtx_power_change_pending || _vtx_freq_change_pending || _vtx_options_change_pending)) {
704 0 : AP::vtx().announce_vtx_settings();
705 : }
706 :
707 0 : _vtx_power_change_pending = _vtx_freq_change_pending = _vtx_options_change_pending = false;
708 : }
709 :
710 0 : void AP_CRSF_Telem::process_vtx_telem_frame(VTXTelemetryFrame* vtx)
711 : {
712 0 : vtx->frequency = be16toh(vtx->frequency);
713 : debug("VTXTelemetry: Freq: %d, PitMode: %d, Power: %d", vtx->frequency, vtx->pitmode, vtx->power);
714 :
715 0 : AP_VideoTX& apvtx = AP::vtx();
716 :
717 0 : if (!apvtx.get_enabled() || !apvtx.is_type_enabled(AP_VideoTX::VTXType::CRSF)) {
718 0 : return;
719 : }
720 :
721 0 : apvtx.set_provider_enabled(AP_VideoTX::VTXType::CRSF);
722 :
723 0 : apvtx.set_frequency_mhz(vtx->frequency);
724 :
725 : AP_VideoTX::VideoBand band;
726 : uint8_t channel;
727 0 : if (AP_VideoTX::get_band_and_channel(vtx->frequency, band, channel)) {
728 0 : apvtx.set_band(uint8_t(band));
729 0 : apvtx.set_channel(channel);
730 : }
731 :
732 0 : apvtx.set_power_dbm(vtx->power);
733 :
734 0 : if (vtx->pitmode) {
735 0 : apvtx.set_options(apvtx.get_options() | uint8_t(AP_VideoTX::VideoOptions::VTX_PITMODE));
736 : } else {
737 0 : apvtx.set_options(apvtx.get_options() & ~uint8_t(AP_VideoTX::VideoOptions::VTX_PITMODE));
738 : }
739 : // make sure the configured values now reflect reality
740 0 : if (!apvtx.set_defaults() && (_vtx_power_change_pending || _vtx_freq_change_pending || _vtx_options_change_pending)) {
741 0 : AP::vtx().announce_vtx_settings();
742 : }
743 :
744 0 : _vtx_power_change_pending = _vtx_freq_change_pending = _vtx_options_change_pending = false;
745 : }
746 : #endif // AP_VIDEOTX_ENABLED
747 :
748 : // request for device info
749 0 : void AP_CRSF_Telem::process_ping_frame(ParameterPingFrame* ping)
750 : {
751 : debug("process_ping_frame: %d -> %d", ping->origin, ping->destination);
752 0 : if (ping->destination != 0 && ping->destination != AP_CRSF_Protocol::CRSF_ADDRESS_FLIGHT_CONTROLLER) {
753 0 : return; // request was not for us
754 : }
755 :
756 0 : _param_request.origin = ping->origin;
757 0 : _pending_request.frame_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAM_DEVICE_INFO;
758 0 : _pending_request.destination = ping->origin;
759 : }
760 :
761 : // request for device info
762 0 : void AP_CRSF_Telem::process_device_info_frame(ParameterDeviceInfoFrame* info)
763 : {
764 : debug("process_device_info_frame: 0x%x -> 0x%x", info->origin, info->destination);
765 0 : if (info->destination != 0 && info->destination != AP_CRSF_Protocol::CRSF_ADDRESS_FLIGHT_CONTROLLER) {
766 0 : return; // request was not for us
767 : }
768 :
769 : // we are only interested in RC device info for firmware version detection
770 0 : if (info->origin != 0 && info->origin != AP_CRSF_Protocol::CRSF_ADDRESS_CRSF_RECEIVER) {
771 0 : return;
772 : }
773 : /*
774 : Payload size is 58:
775 : char[] Device name ( Null-terminated string, max len is 42 )
776 : uint32_t Serial number
777 : uint32_t Hardware ID
778 : uint32_t Firmware ID (0x00:0x00:0xAA:0xBB AA=major, BB=minor)
779 : uint8_t Parameters count
780 : uint8_t Parameter version number
781 : */
782 : // get the terminator of the device name string
783 0 : const uint8_t offset = strnlen((char*)info->payload,42U);
784 0 : if (strncmp((char*)info->payload, "Tracer", 6) == 0) {
785 0 : _crsf_version.protocol = AP_CRSF_Protocol::ProtocolType::PROTOCOL_TRACER;
786 0 : } else if (strncmp((char*)&info->payload[offset+1], "ELRS", 4) == 0) {
787 : // ELRS magic number is ELRS encoded in the serial number
788 : // 0x45 'E' 0x4C 'L' 0x52 'R' 0x53 'S'
789 0 : _crsf_version.protocol = AP_CRSF_Protocol::ProtocolType::PROTOCOL_ELRS;
790 : }
791 :
792 0 : if (!is_elrs()) {
793 : /*
794 : fw major ver = offset + terminator (8bits) + serial (32bits) + hw id (32bits) + 3rd byte of sw id = 11bytes
795 : fw minor ver = offset + terminator (8bits) + serial (32bits) + hw id (32bits) + 4th byte of sw id = 12bytes
796 : */
797 0 : _crsf_version.major = info->payload[offset+11];
798 0 : _crsf_version.minor = info->payload[offset+12];
799 : } else {
800 : // ELRS does not populate the version field so cook up something sensible
801 0 : _crsf_version.major = 1;
802 0 : _crsf_version.minor = 0;
803 : }
804 :
805 : // should we use rf_mode reported by link statistics?
806 0 : if (is_elrs() || (!is_tracer() && (_crsf_version.major > 3 || (_crsf_version.major == 3 && _crsf_version.minor >= 72)))) {
807 0 : _crsf_version.use_rf_mode = true;
808 : }
809 :
810 0 : _crsf_version.pending = false;
811 0 : disable_scheduler_entry(VERSION_PING);
812 : }
813 :
814 : // request for a general command
815 0 : void AP_CRSF_Telem::process_command_frame(CommandFrame* command)
816 : {
817 : debug("process_command_frame: 0x%x -> 0x%x: 0x%x", command->origin, command->destination, command->payload[0]);
818 0 : if (command->destination != 0 && command->destination != AP_CRSF_Protocol::CRSF_ADDRESS_FLIGHT_CONTROLLER) {
819 0 : return; // request was not for us
820 : }
821 :
822 : // we are only interested in commands from the RX
823 0 : if (command->origin != 0 && command->origin != AP_CRSF_Protocol::CRSF_ADDRESS_CRSF_RECEIVER) {
824 0 : return;
825 : }
826 :
827 0 : switch (command->payload[0]) {
828 0 : case AP_CRSF_Protocol::CRSF_COMMAND_GENERAL_CRSF_SPEED_PROPOSAL: {
829 0 : uint32_t baud_rate = command->payload[2] << 24 | command->payload[3] << 16
830 0 : | command->payload[4] << 8 | command->payload[5];
831 0 : _baud_rate_request.port_id = command->payload[1];
832 0 : _baud_rate_request.valid = AP::crsf()->change_baud_rate(baud_rate);
833 0 : _baud_rate_request.pending = true;
834 : debug("requested baud rate change %lu", baud_rate);
835 0 : break;
836 : }
837 0 : default:
838 0 : break; // do nothing
839 : }
840 : }
841 :
842 0 : void AP_CRSF_Telem::process_param_read_frame(ParameterSettingsReadFrame* read_frame)
843 : {
844 0 : if (read_frame->destination != 0 && read_frame->destination != AP_CRSF_Protocol::CRSF_ADDRESS_FLIGHT_CONTROLLER) {
845 : debug("process_param_read_frame: %d -> %d for %d[%d]: NAK", read_frame->origin, read_frame->destination,
846 : read_frame->param_num, read_frame->param_chunk);
847 0 : return; // request was not for us
848 : }
849 : #if AP_CRSF_SCRIPTING_ENABLED
850 : // write parameter in scripted menus
851 0 : if (process_scripted_param_read(read_frame)) {
852 : debug("process_param_read_frame: %d -> %d for %d[%d]: scripted response OK", read_frame->origin, read_frame->destination,
853 : read_frame->param_num, read_frame->param_chunk);
854 0 : return;
855 : }
856 : #endif // AP_CRSF_SCRIPTING_ENABLED
857 : debug("process_param_read_frame: %d -> %d for %d[%d]: generic response", read_frame->origin, read_frame->destination,
858 : read_frame->param_num, read_frame->param_chunk);
859 0 : _param_request.origin = read_frame->origin;
860 0 : _param_request.destination = read_frame->destination;
861 0 : _param_request.param_num = read_frame->param_num;
862 0 : _param_request.param_chunk = read_frame->param_chunk;
863 0 : _param_request.payload.payload_length = 0;
864 :
865 0 : _pending_request.frame_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAMETER_READ;
866 : }
867 :
868 0 : void AP_CRSF_Telem::process_pending_requests()
869 : {
870 : #if AP_CRSF_SCRIPTING_ENABLED
871 : // handle SCRIPTED parameter requests first, as they are now self-contained
872 0 : if (_pending_request.frame_type == 0 && calc_scripted_parameter()) {
873 : // scripted request was popped from outbound_params and handled.
874 0 : _custom_telem.params_mode_start_ms = AP_HAL::millis();
875 0 : return;
876 : }
877 : #endif
878 : // handle general parameter requests
879 0 : switch (_pending_request.frame_type) {
880 : // construct a response to a ping frame
881 0 : case AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAM_DEVICE_INFO:
882 0 : _custom_telem.params_mode_start_ms = AP_HAL::millis();
883 0 : calc_device_info();
884 0 : break;
885 : // construct a ping frame originating here
886 0 : case AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAM_DEVICE_PING:
887 0 : calc_device_ping(_pending_request.destination);
888 0 : break;
889 0 : case AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAMETER_READ:
890 : case AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAMETER_WRITE:
891 : // reset parameter passthrough timeout
892 0 : _custom_telem.params_mode_start_ms = AP_HAL::millis();
893 0 : calc_parameter();
894 0 : break;
895 0 : default:
896 0 : break;
897 : }
898 :
899 0 : _pending_request.frame_type = 0;
900 : }
901 :
902 : #if AP_VIDEOTX_ENABLED
903 0 : void AP_CRSF_Telem::update_vtx_params()
904 : {
905 0 : AP_VideoTX& vtx = AP::vtx();
906 :
907 : // This function does ugly things with the vtx parameters which will upset other providers
908 0 : if (!vtx.get_enabled() || !vtx.is_provider_enabled(AP_VideoTX::VTXType::CRSF)) {
909 0 : return;
910 : }
911 :
912 0 : _vtx_freq_change_pending = vtx.update_band() || vtx.update_channel() || vtx.update_frequency() || _vtx_freq_change_pending;
913 : // don't update the power if we are supposed to be in pitmode as this will take us out of pitmode
914 0 : const bool pitmode = vtx.get_configured_options() & uint8_t(AP_VideoTX::VideoOptions::VTX_PITMODE);
915 0 : _vtx_power_change_pending = !pitmode && (vtx.update_power() || _vtx_power_change_pending);
916 0 : _vtx_options_change_pending = vtx.update_options() || _vtx_options_change_pending;
917 :
918 0 : if (_vtx_freq_change_pending || _vtx_power_change_pending || _vtx_options_change_pending) {
919 : // make the desired frequency match the desired band and channel
920 0 : if (_vtx_freq_change_pending) {
921 0 : if (vtx.update_band() || vtx.update_channel()) {
922 0 : vtx.update_configured_frequency();
923 : } else {
924 0 : vtx.update_configured_channel_and_band();
925 : }
926 : }
927 :
928 : debug("update_params(): freq %d->%d, chan: %d->%d, band: %d->%d, pwr: %d->%d, opts: %d->%d",
929 : vtx.get_frequency_mhz(), vtx.get_configured_frequency_mhz(),
930 : vtx.get_channel(), vtx.get_configured_channel(),
931 : vtx.get_band(), vtx.get_configured_band(),
932 : vtx.get_power_mw(), vtx.get_configured_power_mw(),
933 : vtx.get_options(), vtx.get_configured_options());
934 :
935 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_COMMAND;
936 0 : _telem.ext.command.destination = AP_CRSF_Protocol::CRSF_ADDRESS_VTX;
937 0 : _telem.ext.command.origin = AP_CRSF_Protocol::CRSF_ADDRESS_FLIGHT_CONTROLLER;
938 0 : _telem.ext.command.command_id = AP_CRSF_Protocol::CRSF_COMMAND_VTX;
939 :
940 0 : uint8_t len = 5;
941 : // prioritize option changes so that the pilot can get in and out of pitmode
942 0 : if (_vtx_options_change_pending) {
943 0 : _telem.ext.command.payload[0] = AP_CRSF_Protocol::CRSF_COMMAND_VTX_PITMODE;
944 0 : if (vtx.get_configured_options() & uint8_t(AP_VideoTX::VideoOptions::VTX_PITMODE)) {
945 0 : _telem.ext.command.payload[1] = 1;
946 : } else {
947 0 : _telem.ext.command.payload[1] = 0;
948 : }
949 0 : } else if (_vtx_freq_change_pending && _vtx_freq_update) {
950 0 : _telem.ext.command.payload[0] = AP_CRSF_Protocol::CRSF_COMMAND_VTX_FREQ;
951 0 : _telem.ext.command.payload[1] = (vtx.get_frequency_mhz() & 0xFF00) >> 8;
952 0 : _telem.ext.command.payload[2] = (vtx.get_frequency_mhz() & 0xFF);
953 0 : _vtx_freq_update = false;
954 0 : len++;
955 0 : } else if (_vtx_freq_change_pending) {
956 0 : _telem.ext.command.payload[0] = AP_CRSF_Protocol::CRSF_COMMAND_VTX_CHANNEL;
957 0 : _telem.ext.command.payload[1] = vtx.get_configured_band() * VTX_MAX_CHANNELS + vtx.get_configured_channel();
958 0 : _vtx_freq_update = true;
959 0 : } else if (_vtx_power_change_pending && _vtx_dbm_update) {
960 0 : _telem.ext.command.payload[0] = AP_CRSF_Protocol::CRSF_COMMAND_VTX_POWER_DBM;
961 0 : _telem.ext.command.payload[1] = vtx.get_configured_power_dbm();
962 0 : _vtx_dbm_update = false;
963 0 : } else if (_vtx_power_change_pending) {
964 0 : _telem.ext.command.payload[0] = AP_CRSF_Protocol::CRSF_COMMAND_VTX_POWER;
965 0 : if (vtx.get_configured_power_mw() < 26) {
966 0 : vtx.set_configured_power_mw(25);
967 0 : } else if (vtx.get_configured_power_mw() < 201) {
968 0 : if (vtx.get_configured_power_mw() < 101) {
969 0 : vtx.set_configured_power_mw(100);
970 : } else {
971 0 : vtx.set_configured_power_mw(200);
972 : }
973 0 : } else if (vtx.get_configured_power_mw() < 501) {
974 0 : if (vtx.get_configured_power_mw() < 401) {
975 0 : vtx.set_configured_power_mw(400);
976 : } else {
977 0 : vtx.set_configured_power_mw(500);
978 : }
979 : } else {
980 0 : vtx.set_configured_power_mw(800);
981 : }
982 0 : _telem.ext.command.payload[1] = vtx.get_configured_power_level();
983 0 : _vtx_dbm_update = true;
984 : }
985 0 : _telem_pending = true;
986 : // calculate command crc
987 : #pragma GCC diagnostic push
988 : #if defined(__GNUC__) && __GNUC__ >= 10
989 : #pragma GCC diagnostic ignored "-Wstringop-overflow"
990 : #endif
991 0 : uint8_t* crcptr = &_telem.ext.command.destination;
992 0 : uint8_t crc = crc8_dvb(0, AP_CRSF_Protocol::CRSF_FRAMETYPE_COMMAND, 0xBA);
993 0 : for (uint8_t i = 0; i < len; i++) {
994 0 : crc = crc8_dvb(crc, crcptr[i], 0xBA);
995 : }
996 0 : crcptr[len] = crc;
997 0 : _telem_size = len + 1;
998 : #pragma GCC diagnostic pop
999 : }
1000 : }
1001 : #endif // AP_VIDEOTX_ENABLED
1002 :
1003 : // prepare parameter ping data
1004 0 : void AP_CRSF_Telem::calc_parameter_ping()
1005 : {
1006 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAM_DEVICE_PING;
1007 0 : _telem.ext.ping.destination = AP_CRSF_Protocol::CRSF_ADDRESS_VTX;
1008 0 : _telem.ext.ping.origin = AP_CRSF_Protocol::CRSF_ADDRESS_FLIGHT_CONTROLLER;
1009 0 : _telem_size = sizeof(ParameterPingFrame);
1010 0 : _telem_pending = true;
1011 0 : }
1012 :
1013 : // prepare qos data - mandatory frame that must be sent periodically
1014 0 : void AP_CRSF_Telem::calc_heartbeat()
1015 : {
1016 0 : _telem.bcast.heartbeat.origin = AP_CRSF_Protocol::CRSF_ADDRESS_FLIGHT_CONTROLLER;
1017 0 : _telem_size = sizeof(HeartbeatFrame);
1018 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_HEARTBEAT;
1019 0 : _telem_pending = true;
1020 0 : }
1021 :
1022 : // prepare battery data
1023 0 : void AP_CRSF_Telem::calc_battery()
1024 : {
1025 0 : const AP_BattMonitor &_battery = AP::battery();
1026 :
1027 0 : _telem.bcast.battery.voltage = htobe16(uint16_t(roundf(_battery.voltage(0) * 10.0f)));
1028 :
1029 : float current;
1030 0 : if (!_battery.current_amps(current, 0)) {
1031 0 : current = 0;
1032 : }
1033 0 : _telem.bcast.battery.current = htobe16(int16_t(roundf(current * 10.0f)));
1034 :
1035 : float used_mah;
1036 0 : if (!_battery.consumed_mah(used_mah, 0)) {
1037 0 : used_mah = 0;
1038 : }
1039 :
1040 0 : uint8_t percentage = 0;
1041 0 : IGNORE_RETURN(_battery.capacity_remaining_pct(percentage, 0));
1042 :
1043 0 : _telem.bcast.battery.remaining = percentage;
1044 :
1045 0 : const int32_t capacity = used_mah;
1046 0 : _telem.bcast.battery.capacity[0] = (capacity & 0xFF0000) >> 16;
1047 0 : _telem.bcast.battery.capacity[1] = (capacity & 0xFF00) >> 8;
1048 0 : _telem.bcast.battery.capacity[2] = (capacity & 0xFF);
1049 :
1050 0 : _telem_size = sizeof(BatteryFrame);
1051 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_BATTERY_SENSOR;
1052 :
1053 0 : _telem_pending = true;
1054 0 : }
1055 :
1056 0 : uint16_t AP_CRSF_Telem::get_altitude_packed()
1057 : {
1058 0 : int32_t altitude_dm = get_nav_alt_m(Location::AltFrame::ABOVE_HOME) * 10;
1059 :
1060 : enum : int32_t {
1061 : ALT_MIN_DM = 10000, // minimum altitude in dm
1062 : ALT_THRESHOLD_DM = 0x8000 - ALT_MIN_DM, // altitude of precision changing in dm
1063 : ALT_MAX_DM = 0x7ffe * 10 - 5, // maximum altitude in dm
1064 : };
1065 :
1066 0 : if (altitude_dm < -ALT_MIN_DM) { // less than minimum altitude
1067 0 : return 0; // minimum
1068 : }
1069 0 : if (altitude_dm > ALT_MAX_DM) { // more than maximum
1070 0 : return 0xFFFEU; // maximum
1071 : }
1072 0 : if(altitude_dm < ALT_THRESHOLD_DM) { //dm-resolution range
1073 0 : return uint16_t(altitude_dm + ALT_MIN_DM);
1074 : }
1075 0 : return uint16_t((altitude_dm + 5) / 10) | uint16_t(0x8000); // meter-resolution range
1076 : }
1077 :
1078 0 : int8_t AP_CRSF_Telem::get_vertical_speed_packed()
1079 : {
1080 0 : float vspeed = get_vspeed_ms();
1081 0 : float vertical_speed_cms = vspeed * 100.0f;
1082 0 : const int16_t Kl = 100; // linearity constant;
1083 0 : const float Kr = 0.026f; // range constant;
1084 0 : int8_t vspeed_packed = int8_t(logf(fabsf(vertical_speed_cms)/Kl + 1)/Kr);
1085 0 : return vspeed_packed * (is_negative(vertical_speed_cms) ? -1 : 1);
1086 : }
1087 :
1088 : // prepare vario data
1089 0 : void AP_CRSF_Telem::calc_baro_vario()
1090 : {
1091 0 : _telem.bcast.baro_vario.altitude_packed = htobe16(get_altitude_packed());
1092 0 : _telem.bcast.baro_vario.vertical_speed_packed = get_vertical_speed_packed();
1093 :
1094 0 : _telem_size = sizeof(BaroVarioFrame);
1095 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_BARO_VARIO;
1096 :
1097 0 : _telem_pending = true;
1098 0 : }
1099 :
1100 : // prepare vario data
1101 0 : void AP_CRSF_Telem::calc_vario()
1102 : {
1103 0 : _telem.bcast.vario.v_speed = htobe16(int16_t(get_vspeed_ms() * 100.0f));
1104 0 : _telem_size = sizeof(VarioFrame);
1105 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_VARIO;
1106 :
1107 0 : _telem_pending = true;
1108 0 : }
1109 :
1110 : // prepare gps data
1111 0 : void AP_CRSF_Telem::calc_gps()
1112 : {
1113 0 : const Location &loc = AP::gps().location(0); // use the first gps instance (same as in send_mavlink_gps_raw)
1114 :
1115 0 : _telem.bcast.gps.latitude = htobe32(loc.lat);
1116 0 : _telem.bcast.gps.longitude = htobe32(loc.lng);
1117 0 : _telem.bcast.gps.groundspeed = htobe16(roundf(AP::gps().ground_speed() * 36.0f));
1118 0 : _telem.bcast.gps.altitude = htobe16(constrain_int16(loc.alt / 100, 0, 5000) + 1000);
1119 0 : _telem.bcast.gps.gps_heading = htobe16(roundf(AP::gps().ground_course() * 100.0f));
1120 0 : _telem.bcast.gps.satellites = AP::gps().num_sats();
1121 :
1122 0 : _telem_size = sizeof(AP_CRSF_Telem::GPSFrame);
1123 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_GPS;
1124 :
1125 0 : _telem_pending = true;
1126 0 : }
1127 :
1128 : // prepare attitude data
1129 0 : void AP_CRSF_Telem::calc_attitude()
1130 : {
1131 0 : AP_AHRS &_ahrs = AP::ahrs();
1132 0 : WITH_SEMAPHORE(_ahrs.get_semaphore());
1133 :
1134 0 : const int16_t INT_PI = 31415;
1135 : // units are radians * 10000
1136 0 : _telem.bcast.attitude.roll_angle = htobe16(constrain_int16(roundf(wrap_PI(_ahrs.get_roll_rad()) * 10000.0f), -INT_PI, INT_PI));
1137 0 : _telem.bcast.attitude.pitch_angle = htobe16(constrain_int16(roundf(wrap_PI(_ahrs.get_pitch_rad()) * 10000.0f), -INT_PI, INT_PI));
1138 0 : _telem.bcast.attitude.yaw_angle = htobe16(constrain_int16(roundf(wrap_PI(_ahrs.get_yaw_rad()) * 10000.0f), -INT_PI, INT_PI));
1139 :
1140 0 : _telem_size = sizeof(AP_CRSF_Telem::AttitudeFrame);
1141 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_ATTITUDE;
1142 :
1143 0 : _telem_pending = true;
1144 0 : }
1145 :
1146 : // prepare flight mode data
1147 0 : void AP_CRSF_Telem::calc_flight_mode()
1148 : {
1149 0 : AP_Notify * notify = AP_Notify::get_singleton();
1150 0 : if (notify) {
1151 : // Note: snprintf() always terminates the string
1152 0 : hal.util->snprintf(
1153 0 : _telem.bcast.flightmode.flight_mode,
1154 : sizeof(AP_CRSF_Telem::FlightModeFrame),
1155 : "%s%s",
1156 : notify->get_flight_mode_str(),
1157 0 : rc().option_is_enabled(RC_Channels::Option::CRSF_FM_DISARM_STAR) && !hal.util->get_soft_armed() ? "*" : ""
1158 : );
1159 : // Note: strlen(_telem.bcast.flightmode.flight_mode) is safe because called on a guaranteed null terminated string
1160 0 : _telem_size = strlen(_telem.bcast.flightmode.flight_mode) + 1; //send the terminator as well
1161 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_FLIGHT_MODE;
1162 0 : _telem_pending = true;
1163 : }
1164 0 : }
1165 :
1166 : // return device information about ArduPilot
1167 0 : void AP_CRSF_Telem::calc_device_info() {
1168 : #if !APM_BUILD_TYPE(APM_BUILD_UNKNOWN)
1169 0 : _telem.ext.info.destination = _param_request.origin;
1170 0 : _telem.ext.info.origin = AP_CRSF_Protocol::CRSF_ADDRESS_FLIGHT_CONTROLLER;
1171 :
1172 0 : const AP_FWVersion &fwver = AP::fwversion();
1173 : // write out the name with version, max width is 60 - 18 = the meaning of life
1174 0 : int32_t n = strlen(fwver.fw_short_string);
1175 0 : strncpy((char*)_telem.ext.info.payload, fwver.fw_short_string, 41);
1176 0 : n = MIN(n + 1, 42);
1177 :
1178 0 : put_be32_ptr(&_telem.ext.info.payload[n], // serial number
1179 0 : uint32_t(fwver.major) << 24 | uint32_t(fwver.minor) << 16 | uint32_t(fwver.patch) << 8 | uint32_t(fwver.fw_type));
1180 0 : n += 4;
1181 0 : put_be32_ptr(&_telem.ext.info.payload[n], // hardware id
1182 0 : uint32_t(fwver.vehicle_type) << 24 | uint32_t(fwver.board_type) << 16 | uint32_t(fwver.board_subtype));
1183 0 : n += 4;
1184 0 : put_be32_ptr(&_telem.ext.info.payload[n], fwver.os_sw_version); // software id
1185 0 : n += 4;
1186 : #if OSD_PARAM_ENABLED
1187 0 : _telem.ext.info.payload[n] = AP_OSD_ParamScreen::NUM_PARAMS * AP_OSD_NUM_PARAM_SCREENS; // param count
1188 : #if AP_CRSF_SCRIPTING_ENABLED
1189 0 : for (ScriptedMenu* m = &scripted_menus; m != nullptr; m = m->next_menu) {
1190 0 : _telem.ext.info.payload[n] += m->num_params;
1191 : }
1192 : #endif // AP_CRSF_SCRIPTING_ENABLED
1193 0 : n++;
1194 : #else
1195 : _telem.ext.info.payload[n++] = 0; // param count
1196 : #endif
1197 0 : _telem.ext.info.payload[n++] = 0; // param version
1198 :
1199 0 : _telem_size = n + 2;
1200 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAM_DEVICE_INFO;
1201 :
1202 0 : _pending_request.frame_type = 0;
1203 0 : _telem_pending = true;
1204 : #endif
1205 0 : }
1206 :
1207 : // send a device ping
1208 0 : void AP_CRSF_Telem::calc_device_ping(uint8_t destination) {
1209 0 : _telem.ext.ping.destination = destination;
1210 0 : _telem.ext.ping.origin = AP_CRSF_Protocol::CRSF_ADDRESS_FLIGHT_CONTROLLER;
1211 0 : _telem_size = 2;
1212 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAM_DEVICE_PING;
1213 0 : _pending_request.frame_type = 0;
1214 :
1215 0 : _telem_pending = true;
1216 0 : }
1217 :
1218 : // send a command response
1219 0 : void AP_CRSF_Telem::calc_command_response() {
1220 0 : _telem.ext.command.destination = AP_CRSF_Protocol::CRSF_ADDRESS_CRSF_RECEIVER;
1221 0 : _telem.ext.command.origin = AP_CRSF_Protocol::CRSF_ADDRESS_FLIGHT_CONTROLLER;
1222 0 : _telem.ext.command.command_id = AP_CRSF_Protocol::CRSF_COMMAND_GENERAL;
1223 0 : _telem.ext.command.payload[0] = AP_CRSF_Protocol::CRSF_COMMAND_GENERAL_CRSF_SPEED_RESPONSE;
1224 0 : _telem.ext.command.payload[1] = _baud_rate_request.port_id;
1225 0 : _telem.ext.command.payload[2] = _baud_rate_request.valid;
1226 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_COMMAND;
1227 :
1228 : // calculate command crc
1229 0 : uint8_t len = 6;
1230 0 : uint8_t* crcptr = &_telem.ext.command.destination;
1231 0 : uint8_t crc = crc8_dvb(0, AP_CRSF_Protocol::CRSF_FRAMETYPE_COMMAND, 0xBA);
1232 0 : for (uint8_t i = 0; i < len; i++) {
1233 0 : crc = crc8_dvb(crc, crcptr[i], 0xBA);
1234 : }
1235 0 : crcptr[len] = crc;
1236 0 : _telem_size = len + 1;
1237 :
1238 0 : _pending_request.frame_type = 0;
1239 0 : _baud_rate_request.pending = false;
1240 :
1241 : debug("sent baud rate response: %u", _baud_rate_request.valid);
1242 0 : _telem_pending = true;
1243 0 : }
1244 :
1245 : // send a command response
1246 0 : void AP_CRSF_Telem::calc_bind() {
1247 0 : _telem.ext.command.destination = AP_CRSF_Protocol::CRSF_ADDRESS_CRSF_RECEIVER;
1248 0 : _telem.ext.command.origin = AP_CRSF_Protocol::CRSF_ADDRESS_FLIGHT_CONTROLLER;
1249 0 : _telem.ext.command.command_id = AP_CRSF_Protocol::CRSF_COMMAND_RX;
1250 0 : _telem.ext.command.payload[0] = AP_CRSF_Protocol::CRSF_COMMAND_RX_BIND;
1251 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_COMMAND;
1252 :
1253 : // calculate command crc
1254 0 : uint8_t len = 4;
1255 0 : uint8_t* crcptr = &_telem.ext.command.destination;
1256 0 : uint8_t crc = crc8_dvb(0, AP_CRSF_Protocol::CRSF_FRAMETYPE_COMMAND, 0xBA);
1257 0 : for (uint8_t i = 0; i < len; i++) {
1258 0 : crc = crc8_dvb(crc, crcptr[i], 0xBA);
1259 : }
1260 0 : crcptr[len] = crc;
1261 0 : _telem_size = len + 1;
1262 :
1263 0 : _pending_request.frame_type = 0;
1264 0 : _bind_request_pending = false;
1265 :
1266 : debug("sent bind request");
1267 0 : _telem_pending = true;
1268 0 : }
1269 :
1270 : // return parameter information
1271 0 : void AP_CRSF_Telem::calc_parameter() {
1272 : #if OSD_PARAM_ENABLED
1273 : #if AP_CRSF_SCRIPTING_ENABLED
1274 0 : WITH_SEMAPHORE(scr_sem);
1275 : #endif
1276 :
1277 0 : _telem.ext.param_entry.header.destination = _param_request.origin;
1278 0 : _telem.ext.param_entry.header.origin = AP_CRSF_Protocol::CRSF_ADDRESS_FLIGHT_CONTROLLER;
1279 0 : size_t idx = 0;
1280 :
1281 : // root folder request
1282 0 : if (_param_request.param_num == 0) {
1283 0 : _telem.ext.param_entry.header.param_num = 0;
1284 0 : _telem.ext.param_entry.header.chunks_left = 0;
1285 0 : _telem.ext.param_entry.payload[idx++] = 0; // parent folder
1286 0 : _telem.ext.param_entry.payload[idx++] = ParameterType::FOLDER; // type
1287 0 : _telem.ext.param_entry.payload[idx++] = 'r'; // "root" name
1288 0 : _telem.ext.param_entry.payload[idx++] = 'o';
1289 0 : _telem.ext.param_entry.payload[idx++] = 'o';
1290 0 : _telem.ext.param_entry.payload[idx++] = 't';
1291 0 : _telem.ext.param_entry.payload[idx++] = 0; // null terminator
1292 :
1293 : #if AP_CRSF_SCRIPTING_ENABLED
1294 0 : if (scripted_menus.next_menu != nullptr) {
1295 0 : _telem.ext.param_entry.payload[idx++] = PARAMETER_MENU_ID; // root parameter screen ardupilot menu
1296 0 : for (ScriptedMenu* m = scripted_menus.next_menu; m != nullptr; m = m->next_menu) {
1297 0 : if (m->parent_id == 0) {
1298 0 : _telem.ext.param_entry.payload[idx++] = m->id; // scripted menus
1299 : }
1300 : }
1301 : } else
1302 : #endif // AP_CRSF_SCRIPTING_ENABLED
1303 : {
1304 : // write out all of the ids we are going to send
1305 0 : for (uint8_t i = 0; i < AP_OSD_ParamScreen::NUM_PARAMS * AP_OSD_NUM_PARAM_SCREENS; i++) {
1306 0 : _telem.ext.param_entry.payload[idx++] = PARAMETER_MENU_ID + i + 1;
1307 : }
1308 : }
1309 0 : _telem.ext.param_entry.payload[idx] = 0xFF; // terminator
1310 :
1311 0 : _telem_size = sizeof(AP_CRSF_Telem::ParameterSettingsEntryHeader) + 1 + idx;
1312 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAMETER_SETTINGS_ENTRY;
1313 0 : _pending_request.frame_type = 0;
1314 0 : _telem_pending = true;
1315 0 : return;
1316 : }
1317 :
1318 : #if AP_CRSF_SCRIPTING_ENABLED
1319 : // root folder request
1320 0 : if (scripted_menus.next_menu != nullptr && _param_request.param_num == PARAMETER_MENU_ID) {
1321 0 : _telem.ext.param_entry.header.param_num = PARAMETER_MENU_ID;
1322 0 : _telem.ext.param_entry.header.chunks_left = 0;
1323 0 : _telem.ext.param_entry.payload[idx++] = 0; // parent folder
1324 0 : _telem.ext.param_entry.payload[idx++] = ParameterType::FOLDER; // type
1325 : // name
1326 0 : strncpy((char*)&_telem.ext.param_entry.payload[idx], "Parameters", ARRAY_SIZE(_telem.ext.param_entry.payload) - idx - 1);
1327 0 : idx += strlen("Parameters");
1328 0 : _telem.ext.param_entry.payload[idx++] = 0; // null terminator
1329 : // write out all of the ids we are going to send
1330 0 : for (uint8_t i = 0; i < AP_OSD_ParamScreen::NUM_PARAMS * AP_OSD_NUM_PARAM_SCREENS; i++) {
1331 0 : _telem.ext.param_entry.payload[idx++] = PARAMETER_MENU_ID + i + 1;
1332 : }
1333 0 : _telem.ext.param_entry.payload[idx] = 0xFF; // terminator
1334 :
1335 0 : _telem_size = sizeof(AP_CRSF_Telem::ParameterSettingsEntryHeader) + 1 + idx;
1336 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAMETER_SETTINGS_ENTRY;
1337 0 : _pending_request.frame_type = 0;
1338 0 : _telem_pending = true;
1339 0 : return;
1340 : }
1341 :
1342 : // scripted menu folder request indexed as the set of parameter ids from SCRIPTED_MENU_START_ID
1343 0 : AP_CRSF_Telem::ScriptedMenu* menu = scripted_menus.find_menu(_param_request.param_num);
1344 :
1345 0 : if (menu != nullptr) {
1346 : debug("menu request %d -> %s[%d]", _param_request.param_num, menu->name, menu->num_params);
1347 :
1348 0 : _telem.ext.param_entry.header.param_num = _param_request.param_num;
1349 0 : _telem.ext.param_entry.header.chunks_left = 0; // assume menus fit in a single chunk
1350 :
1351 0 : _telem.ext.param_entry.payload[idx++] = menu->parent_id; // parent folder
1352 0 : _telem.ext.param_entry.payload[idx++] = ParameterType::FOLDER; // type
1353 : // name
1354 0 : const uint8_t namelen = ARRAY_SIZE(_telem.ext.param_entry.payload) - idx - 1;
1355 0 : strncpy((char*)&_telem.ext.param_entry.payload[idx], menu->name, namelen);
1356 0 : idx += strnlen(menu->name, namelen);
1357 0 : _telem.ext.param_entry.payload[idx++] = 0; // null terminator
1358 :
1359 : // menu entries of parameter ids
1360 0 : for (uint8_t i = 0; i < menu->num_params; i++) {
1361 0 : _telem.ext.param_entry.payload[idx++] = menu->params[i].id;
1362 : }
1363 0 : _telem.ext.param_entry.payload[idx] = 0xFF; // terminator
1364 :
1365 0 : _telem_size = sizeof(AP_CRSF_Telem::ParameterSettingsEntryHeader) + 1 + idx;
1366 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAMETER_SETTINGS_ENTRY;
1367 0 : _pending_request.frame_type = 0;
1368 0 : _telem_pending = true;
1369 0 : return;
1370 : }
1371 : #endif // AP_CRSF_SCRIPTING_ENABLED
1372 :
1373 0 : AP_OSD* osd = AP::osd();
1374 :
1375 0 : if (osd == nullptr) {
1376 0 : return;
1377 : }
1378 :
1379 0 : AP_OSD_ParamSetting* setting = osd->get_setting((_param_request.param_num - (PARAMETER_MENU_ID + 1)) / AP_OSD_ParamScreen::NUM_PARAMS,
1380 0 : (_param_request.param_num - (PARAMETER_MENU_ID + 1)) % AP_OSD_ParamScreen::NUM_PARAMS);
1381 :
1382 0 : if (setting == nullptr) {
1383 0 : return;
1384 : }
1385 :
1386 0 : _telem.ext.param_entry.header.param_num = _param_request.param_num;
1387 : #if HAL_CRSF_TELEM_TEXT_SELECTION_ENABLED
1388 0 : if (setting->get_custom_metadata() != nullptr) {
1389 0 : calc_text_selection(setting, _param_request.param_chunk);
1390 0 : return;
1391 : }
1392 : #endif
1393 0 : _telem.ext.param_entry.header.chunks_left = 0;
1394 0 : _telem.ext.param_entry.payload[idx++] = 0; // parent folder
1395 0 : idx++; // leave a gap for the type
1396 0 : setting->copy_name_camel_case((char*)&_telem.ext.param_entry.payload[idx], 17);
1397 0 : idx += strnlen((char*)&_telem.ext.param_entry.payload[idx], 16) + 1;
1398 :
1399 0 : switch (setting->_param_type) {
1400 0 : case AP_PARAM_INT8: {
1401 0 : AP_Int8* p = (AP_Int8*)setting->_param;
1402 0 : _telem.ext.param_entry.payload[1] = ParameterType::INT8;
1403 0 : _telem.ext.param_entry.payload[idx] = p->get(); // value
1404 0 : _telem.ext.param_entry.payload[idx+1] = int8_t(setting->_param_min.get()); // min
1405 0 : _telem.ext.param_entry.payload[idx+2] = int8_t(setting->_param_max.get()); // max
1406 0 : _telem.ext.param_entry.payload[idx+3] = int8_t(0); // default
1407 0 : idx += 4;
1408 0 : break;
1409 : }
1410 0 : case AP_PARAM_INT16: {
1411 0 : AP_Int16* p = (AP_Int16*)setting->_param;
1412 0 : _telem.ext.param_entry.payload[1] = ParameterType::INT16;
1413 0 : put_be16_ptr(&_telem.ext.param_entry.payload[idx], p->get()); // value
1414 0 : put_be16_ptr(&_telem.ext.param_entry.payload[idx+2], setting->_param_min.get()); // min
1415 0 : put_be16_ptr(&_telem.ext.param_entry.payload[idx+4], setting->_param_max.get()); // max
1416 0 : put_be16_ptr(&_telem.ext.param_entry.payload[idx+6], 0); // default
1417 0 : idx += 8;
1418 0 : break;
1419 : }
1420 0 : case AP_PARAM_INT32: {
1421 0 : AP_Int32* p = (AP_Int32*)setting->_param;
1422 0 : _telem.ext.param_entry.payload[1] = ParameterType::FLOAT;
1423 : #define FLOAT_ENCODE(f) (int32_t(roundf(f)))
1424 0 : put_be32_ptr(&_telem.ext.param_entry.payload[idx], p->get()); // value
1425 0 : put_be32_ptr(&_telem.ext.param_entry.payload[idx+4], FLOAT_ENCODE(setting->_param_min)); // min
1426 0 : put_be32_ptr(&_telem.ext.param_entry.payload[idx+8], FLOAT_ENCODE(setting->_param_max)); // max
1427 0 : put_be32_ptr(&_telem.ext.param_entry.payload[idx+12], FLOAT_ENCODE(0.0f)); // default
1428 : #undef FLOAT_ENCODE
1429 0 : _telem.ext.param_entry.payload[idx+16] = 0; // decimal point
1430 0 : put_be32_ptr(&_telem.ext.param_entry.payload[idx+17], 1); // step size
1431 0 : idx += 21;
1432 0 : break;
1433 : }
1434 0 : case AP_PARAM_FLOAT: {
1435 0 : AP_Float* p = (AP_Float*)setting->_param;
1436 0 : _telem.ext.param_entry.payload[1] = ParameterType::FLOAT;
1437 0 : uint8_t digits = 0;
1438 0 : const float incr = MAX(0.001f, setting->_param_incr); // a bug in OpenTX prevents this going any smaller
1439 :
1440 0 : for (float floatp = incr; floatp < 1.0f; floatp *= 10) {
1441 0 : digits++;
1442 : }
1443 0 : const float mult = powf(10, digits);
1444 : #define FLOAT_ENCODE(f) (int32_t(roundf(mult * f)))
1445 0 : put_be32_ptr(&_telem.ext.param_entry.payload[idx], FLOAT_ENCODE(p->get())); // value
1446 0 : put_be32_ptr(&_telem.ext.param_entry.payload[idx+4], FLOAT_ENCODE(setting->_param_min)); // min
1447 0 : put_be32_ptr(&_telem.ext.param_entry.payload[idx+8], FLOAT_ENCODE(setting->_param_max)); // max
1448 0 : put_be32_ptr(&_telem.ext.param_entry.payload[idx+12], FLOAT_ENCODE(0.0f)); // default
1449 0 : _telem.ext.param_entry.payload[idx+16] = digits; // decimal point
1450 0 : put_be32_ptr(&_telem.ext.param_entry.payload[idx+17], FLOAT_ENCODE(incr)); // step size
1451 : #undef FLOAT_ENCODE
1452 : //debug("Encoding param %f(%f -> %f, %f) as %d(%d) (%d -> %d, %d)", p->get(),
1453 : // param->_param_min.get(), param->_param_max.get(), param->_param_incr.get(),
1454 : // int(FLOAT_ENCODE(p->get())), digits, int(FLOAT_ENCODE(param->_param_min)),
1455 : // int(FLOAT_ENCODE(param->_param_max)), int(FLOAT_ENCODE(param->_param_incr)));
1456 0 : idx += 21;
1457 0 : break;
1458 : }
1459 0 : default:
1460 0 : return;
1461 : }
1462 0 : _telem.ext.param_entry.payload[idx] = 0; // units
1463 :
1464 0 : _telem_size = sizeof(AP_CRSF_Telem::ParameterSettingsEntryHeader) + 1 + idx;
1465 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAMETER_SETTINGS_ENTRY;
1466 :
1467 0 : _pending_request.frame_type = 0;
1468 0 : _telem_pending = true;
1469 : #endif // OSD_PARAM_ENABLED
1470 0 : }
1471 :
1472 : #if AP_CRSF_SCRIPTING_ENABLED
1473 : // process a completed scripted parameter request from the outbound queue
1474 0 : bool AP_CRSF_Telem::calc_scripted_parameter()
1475 : {
1476 0 : WITH_SEMAPHORE(scr_sem);
1477 : ScriptedParameterWrite spw;
1478 : // Check if there's a fully-formed scripted request to process from a Lua script.
1479 : // This is the new thread-safe path that avoids using shared global variables.
1480 0 : if (!ready_params.pop(spw)) {
1481 0 : return false;
1482 : }
1483 :
1484 : // The spw object now contains all context needed to build the response.
1485 0 : size_t idx = 0;
1486 0 : _telem.ext.param_entry.header.destination = spw.origin;
1487 0 : _telem.ext.param_entry.header.origin = AP_CRSF_Protocol::CRSF_ADDRESS_FLIGHT_CONTROLLER;
1488 0 : _telem.ext.param_entry.header.param_num = spw.param_num;
1489 :
1490 : // For a WRITE to a non-COMMAND parameter, the response type is PARAMETER_WRITE (0x2D).
1491 0 : if (spw.type == ScriptedParameterEvents::PARAMETER_WRITE &&
1492 0 : !(spw.param->length > 0 && spw.param->data[0] == ParameterType::COMMAND)) {
1493 :
1494 0 : memcpy((uint8_t*)_telem.ext.param_write.payload, spw.payload.payload, MIN(spw.payload.payload_length, 57));
1495 0 : idx += spw.payload.payload_length;
1496 :
1497 0 : _telem_size = sizeof(AP_CRSF_Telem::ParameterSettingsHeader) + idx;
1498 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAMETER_WRITE;
1499 :
1500 : } else {
1501 : // For a READ, or a WRITE to a COMMAND parameter, the response is a SETTINGS_ENTRY (0x2B).
1502 :
1503 0 : if (spw.type == ScriptedParameterEvents::PARAMETER_WRITE) { // COMMAND type
1504 0 : _telem.ext.param_entry.header.chunks_left = 0;
1505 0 : _telem.ext.param_entry.payload[idx++] = spw.param->parent_id;
1506 0 : memcpy((uint8_t*)&_telem.ext.param_entry.payload[idx], spw.payload.payload, MIN(spw.payload.payload_length, 57-idx));
1507 0 : idx += spw.payload.payload_length;
1508 : } else { // READ type
1509 0 : const uint8_t CHUNK_SIZE = 55; // This wastes a byte on chunks > 0 but makes the maths easier
1510 0 : const uint8_t chunks = ((spw.param->length) / CHUNK_SIZE) + 1; // includes the extra byte for the parent folder id
1511 0 : _telem.ext.param_entry.header.chunks_left = (chunks - 1) - spw.param_chunk;
1512 :
1513 0 : if (spw.param_chunk == 0) {
1514 0 : _telem.ext.param_entry.payload[idx++] = spw.param->parent_id; // parent folder only in first chunk
1515 : }
1516 :
1517 0 : const uint8_t chunk_len = _telem.ext.param_entry.header.chunks_left > 0 ? CHUNK_SIZE : (spw.param->length + 1) % CHUNK_SIZE;
1518 0 : memcpy((uint8_t*)&_telem.ext.param_entry.payload[idx],
1519 0 : &spw.param->data[spw.param_chunk * CHUNK_SIZE], chunk_len);
1520 0 : idx += chunk_len;
1521 : }
1522 0 : _telem_size = sizeof(AP_CRSF_Telem::ParameterSettingsEntryHeader) + idx;
1523 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAMETER_SETTINGS_ENTRY;
1524 : }
1525 :
1526 0 : _pending_request.frame_type = 0;
1527 0 : _telem_pending = true;
1528 0 : return true; // Scripted request handled.
1529 0 : }
1530 : #endif // AP_CRSF_SCRIPTING_ENABLED
1531 :
1532 : #if HAL_CRSF_TELEM_TEXT_SELECTION_ENABLED
1533 : // class that spits out a chunk of data from a larger stream of contiguous chunks
1534 : // the caller describes which chunk it needs and provides this class with all of the data
1535 : // data is not written until the start position is reached and after a whole chunk
1536 : // is accumulated the rest of the data is skipped in order to determine how many chunks
1537 : // are left to be sent
1538 : class BufferChunker {
1539 : public:
1540 0 : BufferChunker(uint8_t* buf, uint16_t chunk_size, uint16_t start_chunk) :
1541 0 : _buf(buf),
1542 0 : _idx(0),
1543 0 : _bytes(0),
1544 0 : _chunk(0),
1545 0 : _start_chunk(start_chunk),
1546 0 : _chunk_size(chunk_size)
1547 0 : { }
1548 :
1549 : // accumulate a string, writing to the underlying buffer as required
1550 0 : void put_string(const char* str, uint16_t str_len) {
1551 : // skip over data we have already written or have yet to write
1552 0 : if (_chunk != _start_chunk) {
1553 0 : if (skip_bytes(str_len)) {
1554 : // partial write
1555 0 : strncpy((char*)_buf, &str[str_len - _idx], _idx);
1556 0 : _bytes += _idx;
1557 : }
1558 0 : return;
1559 : }
1560 :
1561 0 : uint16_t rem = remaining();
1562 0 : if (rem > str_len) {
1563 0 : strncpy_noterm((char*)&_buf[_idx], str, str_len);
1564 0 : _idx += str_len;
1565 0 : _bytes += str_len;
1566 : } else {
1567 0 : strncpy_noterm((char*)&_buf[_idx], str, rem);
1568 0 : _chunk++;
1569 0 : _idx += str_len;
1570 0 : _bytes += rem;
1571 0 : _idx %= _chunk_size;
1572 : }
1573 : }
1574 :
1575 : // accumulate a byte, writing to the underlying buffer as required
1576 0 : void put_byte(uint8_t b) {
1577 0 : if (_chunk != _start_chunk) {
1578 0 : if (skip_bytes(1)) {
1579 0 : _buf[0] = b;
1580 0 : _bytes++;
1581 : }
1582 0 : return;
1583 : }
1584 0 : if (remaining() > 0) {
1585 0 : _buf[_idx++] = b;
1586 0 : _bytes++;
1587 : } else {
1588 0 : _chunk++;
1589 0 : _idx = 0;
1590 : }
1591 : }
1592 :
1593 0 : uint8_t chunks_remaining() const { return _chunk - _start_chunk; }
1594 0 : uint8_t bytes_written() const { return _bytes; }
1595 :
1596 : private:
1597 0 : uint16_t remaining() const { return _chunk_size - _bytes; }
1598 :
1599 : // skip over the requested number of bytes
1600 : // returns true if we overflow into a chunk that needs to be written
1601 0 : bool skip_bytes(uint16_t len) {
1602 0 : _idx += len;
1603 0 : if (_idx >= _chunk_size) {
1604 0 : _chunk++;
1605 0 : _idx %= _chunk_size;
1606 : // partial write
1607 0 : if (_chunk == _start_chunk && _idx > 0) {
1608 0 : return true;
1609 : }
1610 : }
1611 0 : return false;
1612 : }
1613 :
1614 : uint8_t* _buf;
1615 : uint16_t _idx;
1616 : uint16_t _bytes;
1617 : uint8_t _chunk;
1618 : const uint16_t _start_chunk;
1619 : const uint16_t _chunk_size;
1620 : };
1621 :
1622 : // provide information about a text selection, possibly over multiple chunks
1623 0 : void AP_CRSF_Telem::calc_text_selection(AP_OSD_ParamSetting* param, uint8_t chunk)
1624 : {
1625 0 : const uint8_t CHUNK_SIZE = 56;
1626 0 : const AP_OSD_ParamSetting::ParamMetadata* metadata = param->get_custom_metadata();
1627 :
1628 : // chunk the output
1629 0 : BufferChunker chunker(_telem.ext.param_entry.payload, CHUNK_SIZE, chunk);
1630 :
1631 0 : chunker.put_byte(0); // parent folder
1632 0 : chunker.put_byte(ParameterType::TEXT_SELECTION); // parameter type
1633 :
1634 : char name[17];
1635 0 : param->copy_name_camel_case(name, 17);
1636 0 : chunker.put_string(name, strnlen(name, 16)); // parameter name
1637 0 : chunker.put_byte(0); // trailing null
1638 :
1639 0 : for (uint8_t i = 0; i < metadata->values_max; i++) {
1640 0 : uint8_t len = strnlen(metadata->values[i], 16);
1641 0 : if (len == 0) {
1642 0 : chunker.put_string("---", 3);
1643 : } else {
1644 0 : chunker.put_string(metadata->values[i], len);
1645 : }
1646 0 : if (i == metadata->values_max - 1) {
1647 0 : chunker.put_byte(0);
1648 : } else {
1649 0 : chunker.put_byte(';');
1650 : }
1651 : }
1652 :
1653 0 : int32_t val = -1;
1654 0 : switch (param->_param_type) {
1655 0 : case AP_PARAM_INT8:
1656 0 : val = ((AP_Int8*)param->_param)->get();
1657 0 : break;
1658 0 : case AP_PARAM_INT16:
1659 0 : val = ((AP_Int16*)param->_param)->get();
1660 0 : break;
1661 0 : case AP_PARAM_INT32:
1662 0 : val = ((AP_Int32*)param->_param)->get();
1663 0 : break;
1664 0 : default:
1665 0 : return;
1666 : }
1667 :
1668 : // out of range values really confuse the TX
1669 0 : val = constrain_int16(val, 0, metadata->values_max - 1);
1670 0 : chunker.put_byte(val); // value
1671 0 : chunker.put_byte(0); // min
1672 0 : chunker.put_byte(metadata->values_max); // max
1673 0 : chunker.put_byte(0); // default
1674 0 : chunker.put_byte(0); // units
1675 :
1676 0 : _telem.ext.param_entry.header.chunks_left = chunker.chunks_remaining();
1677 :
1678 0 : _telem_size = sizeof(AP_CRSF_Telem::ParameterSettingsEntryHeader) + chunker.bytes_written();
1679 0 : _telem_type = AP_CRSF_Protocol::CRSF_FRAMETYPE_PARAMETER_SETTINGS_ENTRY;
1680 :
1681 0 : _pending_request.frame_type = 0;
1682 0 : _telem_pending = true;
1683 : }
1684 : #endif // HAL_CRSF_TELEM_TEXT_SELECTION_ENABLED
1685 :
1686 : // write parameter information back into AP - assumes we already know the encoding for floats
1687 0 : void AP_CRSF_Telem::process_param_write_frame(ParameterSettingsWriteFrame* write_frame, uint8_t length)
1688 : {
1689 : debug("process_param_write_frame: %d -> %d", write_frame->origin, write_frame->destination);
1690 0 : if (write_frame->destination != AP_CRSF_Protocol::CRSF_ADDRESS_FLIGHT_CONTROLLER) {
1691 0 : return; // request was not for us
1692 : }
1693 : #if AP_CRSF_SCRIPTING_ENABLED
1694 : // write paramter in scripted menus
1695 0 : if (process_scripted_param_write(write_frame, length)) {
1696 0 : return;
1697 : }
1698 : #endif
1699 : #if OSD_PARAM_ENABLED
1700 0 : AP_OSD* osd = AP::osd();
1701 :
1702 0 : if (osd == nullptr) {
1703 0 : return;
1704 : }
1705 :
1706 0 : AP_OSD_ParamSetting* param = osd->get_setting((write_frame->param_num - (PARAMETER_MENU_ID + 1)) / AP_OSD_ParamScreen::NUM_PARAMS,
1707 0 : (write_frame->param_num - (PARAMETER_MENU_ID + 1)) % AP_OSD_ParamScreen::NUM_PARAMS);
1708 :
1709 0 : if (param == nullptr) {
1710 0 : return;
1711 : }
1712 :
1713 : #if HAL_CRSF_TELEM_TEXT_SELECTION_ENABLED
1714 0 : bool text_selection = param->get_custom_metadata() != nullptr;
1715 : #else
1716 : bool text_selection = false;
1717 : #endif
1718 :
1719 0 : switch (param->_param_type) {
1720 0 : case AP_PARAM_INT8: {
1721 0 : AP_Int8* p = (AP_Int8*)param->_param;
1722 0 : p->set_and_save(write_frame->payload[0]);
1723 0 : break;
1724 : }
1725 0 : case AP_PARAM_INT16: {
1726 0 : AP_Int16* p = (AP_Int16*)param->_param;
1727 0 : if (text_selection) {
1728 : // if we have custom metadata then the parameter is a text selection
1729 0 : p->set_and_save(write_frame->payload[0]);
1730 : } else {
1731 0 : p->set_and_save(be16toh_ptr(write_frame->payload));
1732 : }
1733 0 : break;
1734 : }
1735 0 : case AP_PARAM_INT32: {
1736 0 : AP_Int32* p = (AP_Int32*)param->_param;
1737 0 : if (text_selection) {
1738 : // if we have custom metadata then the parameter is a text selection
1739 0 : p->set_and_save(write_frame->payload[0]);
1740 : } else {
1741 0 : p->set_and_save(be32toh_ptr(write_frame->payload));
1742 : }
1743 0 : break;
1744 : }
1745 0 : case AP_PARAM_FLOAT: {
1746 0 : AP_Float* p = (AP_Float*)param->_param;
1747 0 : const int32_t val = be32toh_ptr(write_frame->payload);
1748 0 : uint8_t digits = 0;
1749 0 : const float incr = MAX(0.001f, param->_param_incr); // a bug in OpenTX prevents this going any smaller
1750 :
1751 0 : for (float floatp = incr; floatp < 1.0f; floatp *= 10) {
1752 0 : digits++;
1753 : }
1754 0 : p->set_and_save(float(val) / powf(10, digits));
1755 0 : break;
1756 : }
1757 0 : default:
1758 0 : break;
1759 : }
1760 : #endif // OSD_PARAM_ENABLED
1761 : }
1762 :
1763 : #if AP_CRSF_SCRIPTING_ENABLED
1764 :
1765 : // get a menu event filtered by event type, this adds the event to the outbound queue for processing
1766 : // called from lua
1767 0 : uint8_t AP_CRSF_Telem::get_menu_event(uint8_t menu_events, uint8_t& param_id, ScriptedPayload& payload)
1768 : {
1769 0 : WITH_SEMAPHORE(scr_sem);
1770 :
1771 0 : uint8_t events = 0;
1772 : ScriptedParameterWrite spw;
1773 0 : if (inbound_params.pop(spw)) {
1774 : // not listening for the event, send a response directly
1775 0 : if ((spw.type & menu_events) == 0) {
1776 0 : ready_params.push(spw);
1777 0 : return 0;
1778 : }
1779 0 : switch (spw.type) {
1780 0 : case ScriptedParameterEvents::PARAMETER_WRITE:
1781 0 : events |= PARAMETER_WRITE;
1782 0 : payload = spw.payload;
1783 0 : param_id = spw.param->id;
1784 0 : break;
1785 0 : case ScriptedParameterEvents::PARAMETER_READ:
1786 0 : events |= PARAMETER_READ;
1787 0 : payload.payload_length = 0;
1788 0 : param_id = spw.param->id;
1789 0 : break;
1790 0 : default:
1791 0 : break;
1792 : }
1793 : // respond with parameter entry
1794 0 : outbound_params.push(spw);
1795 : debug("get_menu_event(%u) -> %s", param_id, spw.type == ScriptedParameterEvents::PARAMETER_WRITE ? "WRITE" : "READ");
1796 : }
1797 :
1798 0 : return events;
1799 0 : }
1800 :
1801 : // peek for a menu event filtered by event type, this makes no changes to the outbound queue
1802 : // called from lua
1803 0 : uint8_t AP_CRSF_Telem::peek_menu_event(uint8_t& param_id, ScriptedPayload& payload, uint8_t& events)
1804 : {
1805 0 : WITH_SEMAPHORE(scr_sem);
1806 :
1807 : ScriptedParameterWrite spw;
1808 0 : if (!inbound_params.peek(spw)) {
1809 0 : events = 0;
1810 0 : param_id = 0;
1811 0 : payload.payload_length = 0;
1812 0 : return 0;
1813 : }
1814 :
1815 0 : switch (spw.type) {
1816 0 : case ScriptedParameterEvents::PARAMETER_WRITE:
1817 0 : payload = spw.payload;
1818 0 : break;
1819 0 : case ScriptedParameterEvents::PARAMETER_READ:
1820 0 : payload.payload_length = 0;
1821 0 : break;
1822 0 : default:
1823 0 : break;
1824 : }
1825 0 : events = spw.type;
1826 0 : param_id = spw.param->id;
1827 :
1828 : debug("peek_menu_event(%u) -> %s, size: %u", param_id, events == ScriptedParameterEvents::PARAMETER_WRITE ? "WRITE" : "READ", payload.payload_length);
1829 :
1830 0 : return inbound_params.available();
1831 0 : }
1832 :
1833 : // pop a menu event from the inbound queue and add to the outbound queue for processing via lua
1834 : // called from lua
1835 0 : void AP_CRSF_Telem::pop_menu_event()
1836 : {
1837 : ScriptedParameterWrite spw;
1838 : // Pop the event from the queue. We must check if it's empty in case of a race condition
1839 : // where another script handled the event between our peek and pop.
1840 0 : if (inbound_params.pop(spw)) {
1841 : debug("pop_menu_event(%u) -> %s", spw.param->id, spw.type == ScriptedParameterEvents::PARAMETER_WRITE ? "WRITE" : "READ");
1842 : // respond with parameter entry
1843 0 : outbound_params.push(spw);
1844 : } else {
1845 : debug("pop_menu_event(): EMPTY");
1846 : }
1847 0 : }
1848 :
1849 : // send a new response from the first item in the outbound queue
1850 : // called from lua
1851 0 : bool AP_CRSF_Telem::send_write_response(uint8_t length, const char* data)
1852 : {
1853 0 : WITH_SEMAPHORE(scr_sem);
1854 :
1855 : ScriptedParameterWrite spw;
1856 0 : if (!outbound_params.pop(spw)) {
1857 : // another script might have handled it
1858 0 : return false;
1859 : }
1860 :
1861 0 : switch (spw.type) {
1862 0 : case ScriptedParameterEvents::PARAMETER_READ:
1863 : // For a READ response, Lua provides the full data payload to be sent.
1864 : // We update the parameter's data buffer directly.
1865 0 : if (length > spw.param->length) {
1866 0 : spw.param->data = (const char*)mem_realloc((char*)spw.param->data, 0, length);
1867 : }
1868 0 : if (spw.param->data == nullptr) {
1869 : // failed to realloc
1870 0 : return false;
1871 : }
1872 0 : memcpy((char*)spw.param->data, data, length);
1873 0 : spw.param->length = length;
1874 0 : break;
1875 0 : case ScriptedParameterEvents::PARAMETER_WRITE:
1876 : // For a WRITE response, Lua provides the data for the response packet.
1877 : // This is stored in the payload field of the queued request.
1878 0 : memcpy(spw.payload.payload, data, MIN(length, sizeof(spw.payload.payload)));
1879 0 : spw.payload.payload_length = MIN(length, sizeof(spw.payload.payload));
1880 0 : break;
1881 : }
1882 :
1883 : // Update the object in the queue with the new data
1884 0 : ready_params.push(spw);
1885 :
1886 : // The scheduler will now be triggered by the is_packet_ready() check on ready_params
1887 : // and process this request in calc_parameter().
1888 0 : return true;
1889 0 : }
1890 :
1891 : // send a generic response from the first item in the outbound queue
1892 : // called from lua
1893 0 : bool AP_CRSF_Telem::send_response()
1894 : {
1895 : ScriptedParameterWrite spw;
1896 0 : if (outbound_params.pop(spw)) {
1897 : // Update the object in the queue with the new data
1898 0 : ready_params.push(spw);
1899 0 : return true;
1900 : }
1901 0 : return false;
1902 : }
1903 :
1904 0 : AP_CRSF_Telem::ScriptedParameter* AP_CRSF_Telem::ScriptedMenu::find_parameter(uint8_t param_num)
1905 : {
1906 : // This function is called on the root, so loop through all menus.
1907 0 : for (ScriptedMenu* m = next_menu; m != nullptr; m = m->next_menu) {
1908 : // Search all parameters within the current menu 'm'.
1909 0 : for (uint8_t i = 0; i < m->num_params; i++) {
1910 0 : if (m->params[i].id == param_num) {
1911 : // Found the parameter (or menu link).
1912 0 : return &m->params[i];
1913 : }
1914 : }
1915 : }
1916 :
1917 : // Parameter was not found in any menu.
1918 0 : return nullptr;
1919 : }
1920 :
1921 : // called from lua
1922 0 : AP_CRSF_Telem::ScriptedParameter* AP_CRSF_Telem::ScriptedMenu::add_parameter(uint8_t length, const char* data)
1923 : {
1924 0 : WITH_SEMAPHORE(AP::crsf_telem()->get_semaphore());
1925 :
1926 0 : ScriptedParameter* new_params = (ScriptedParameter*)mem_realloc(params,
1927 0 : sizeof(ScriptedParameter) * num_params,
1928 0 : sizeof(ScriptedParameter) * (num_params+1));
1929 0 : if (new_params == nullptr) {
1930 0 : return nullptr;
1931 : }
1932 0 : num_params++;
1933 0 : params = new_params;
1934 0 : ScriptedParameter& param = params[num_params-1];
1935 0 : param.id = id + num_params;
1936 0 : param.parent_id = id;
1937 0 : param.length = length;
1938 0 : if (length > 0) {
1939 0 : param.data = (const char*)malloc(length);
1940 :
1941 0 : if (param.data == nullptr) {
1942 0 : num_params--;
1943 0 : return nullptr;
1944 : }
1945 :
1946 0 : memcpy((char*)param.data, data, length);
1947 : }
1948 : debug("adding menu item %d", param.id);
1949 :
1950 0 : return ¶m;
1951 0 : }
1952 :
1953 0 : AP_CRSF_Telem::ScriptedMenu* AP_CRSF_Telem::ScriptedMenu::find_menu(uint8_t param_num)
1954 : {
1955 : // find the parameter to write
1956 0 : for (ScriptedMenu* m = next_menu; m != nullptr; m = m->next_menu) {
1957 0 : if (m->id == param_num) {
1958 0 : return m;
1959 : }
1960 : }
1961 0 : return nullptr;
1962 : }
1963 :
1964 0 : bool AP_CRSF_Telem::ScriptedMenu::remove_menu(uint8_t param_num)
1965 : {
1966 0 : WITH_SEMAPHORE(AP::crsf_telem()->get_semaphore());
1967 :
1968 : // find the parameter to write
1969 0 : ScriptedMenu* prev = this;
1970 0 : for (ScriptedMenu* m = next_menu; m != nullptr; m = m->next_menu) {
1971 0 : if (m->id == param_num) {
1972 0 : prev->next_menu = m->next_menu;
1973 0 : delete m;
1974 0 : return true;
1975 : }
1976 : }
1977 0 : return false;
1978 0 : }
1979 :
1980 0 : AP_CRSF_Telem::ScriptedMenu::~ScriptedMenu()
1981 : {
1982 0 : if (params != nullptr) {
1983 0 : for (uint8_t i = 0; i < num_params; i++) {
1984 0 : free((char*)params[i].data);
1985 : }
1986 0 : free(params);
1987 : }
1988 0 : if (name != nullptr) {
1989 0 : free((char*)name);
1990 : }
1991 0 : }
1992 :
1993 : // add a scripted sub-menu to this menu
1994 : // called from lua
1995 0 : AP_CRSF_Telem::ScriptedMenu* AP_CRSF_Telem::ScriptedMenu::add_menu(const char* menu_name, uint8_t size, uint8_t parent_menu)
1996 : {
1997 0 : WITH_SEMAPHORE(AP::crsf_telem()->get_semaphore());
1998 :
1999 : // find the menu to create
2000 0 : ScriptedMenu* tail = this;
2001 0 : for (; tail->next_menu != nullptr; tail = tail->next_menu) {
2002 : }
2003 :
2004 0 : ScriptedMenu* menu = NEW_NOTHROW ScriptedMenu(menu_name, size, parent_menu);
2005 0 : if (menu == nullptr) {
2006 0 : return nullptr;
2007 : }
2008 :
2009 0 : menu->id = tail->id == 0 ? SCRIPTED_MENU_START_ID : tail->id + MAX_SCRIPTED_MENU_SIZE + 1;
2010 0 : tail->next_menu = menu;
2011 :
2012 : // dummy parameter for a submenu
2013 0 : if (parent_menu != 0) {
2014 0 : ScriptedParameter* param = add_parameter(0, nullptr);
2015 0 : param->id = menu->id;
2016 : }
2017 0 : return menu;
2018 0 : }
2019 :
2020 0 : AP_CRSF_Telem::ScriptedMenu::ScriptedMenu(const char* menu_name, uint8_t size, uint8_t parent_menu)
2021 : {
2022 0 : parent_id = parent_menu;
2023 0 : name = (const char*)malloc(MAX_SCRIPTED_MENU_NAME_LEN+1);
2024 0 : strncpy((char*)name, menu_name, MAX_SCRIPTED_MENU_NAME_LEN);
2025 : // num_params = size;
2026 0 : params = (ScriptedParameter*)calloc(size, sizeof(ScriptedParameter));
2027 0 : }
2028 :
2029 0 : bool AP_CRSF_Telem::process_scripted_param_write(ParameterSettingsWriteFrame* write_frame, uint8_t length)
2030 : {
2031 0 : if (write_frame->param_num < SCRIPTED_MENU_START_ID) {
2032 0 : return false;
2033 : }
2034 :
2035 0 : WITH_SEMAPHORE(scr_sem); // not called from lua, but interacting with data structures that might change
2036 :
2037 : // find the parameter to write
2038 0 : ScriptedParameter* param = scripted_menus.find_parameter(write_frame->param_num);
2039 :
2040 0 : if (param == nullptr || length < 3) {
2041 0 : return false;
2042 : }
2043 0 : ScriptedParameterWrite spw {
2044 : ScriptedParameterEvents::PARAMETER_WRITE,
2045 0 : write_frame->destination,
2046 0 : write_frame->origin,
2047 0 : write_frame->param_num,
2048 : 0,
2049 : param
2050 0 : };
2051 :
2052 0 : spw.payload.payload_length = uint8_t(length - 3U); // remove destination, origin and param number from payload length
2053 0 : memcpy(spw.payload.payload, write_frame->payload, spw.payload.payload_length);
2054 0 : inbound_params.push(spw); // payload size in frame
2055 :
2056 0 : return true;
2057 0 : }
2058 :
2059 0 : bool AP_CRSF_Telem::process_scripted_param_read(ParameterSettingsReadFrame* read_frame)
2060 : {
2061 0 : if (read_frame->param_num < SCRIPTED_MENU_START_ID) {
2062 0 : return false;
2063 : }
2064 :
2065 0 : WITH_SEMAPHORE(scr_sem); // not called from lua, but interacting with data structures that might change
2066 :
2067 0 : if (scripted_menus.find_menu(read_frame->param_num) != nullptr) {
2068 : // This is a request for a menu folder, not a leaf parameter.
2069 : // The exception is if it's a multi-chunk read for a parameter that happens
2070 : // to have the same ID as a menu (which is the case for submenu links).
2071 : // If it's a chunked read, we must handle it as a scripted param read.
2072 0 : if (read_frame->param_chunk == 0) {
2073 0 : return false;
2074 : }
2075 : }
2076 :
2077 : // First, check if the requested ID corresponds to a parameter link.
2078 0 : ScriptedParameter* param = scripted_menus.find_parameter(read_frame->param_num);
2079 :
2080 0 : if (param != nullptr) {
2081 : // We found a parameter. NOW, we must check if this parameter's ID
2082 : // ALSO corresponds to a menu object. If it does, it's a sub-menu link.
2083 0 : if (scripted_menus.find_menu(read_frame->param_num) != nullptr) {
2084 : // This is a sub-menu link. Return false to let the C++ 'calc_parameter'
2085 : // function handle it as a FOLDER request.
2086 0 : return false;
2087 : }
2088 :
2089 0 : ScriptedParameterWrite spw{
2090 : ScriptedParameterEvents::PARAMETER_READ,
2091 0 : read_frame->destination,
2092 0 : read_frame->origin,
2093 0 : read_frame->param_num,
2094 0 : read_frame->param_chunk,
2095 : param
2096 0 : };
2097 :
2098 0 : if (read_frame->param_chunk > 0) {
2099 0 : ready_params.push(spw); // handle directly - no need to involve lua
2100 : } else {
2101 : // This is a regular parameter (not a menu link). Handle it in Lua.
2102 0 : inbound_params.push(spw);
2103 : }
2104 0 : return true;
2105 : }
2106 :
2107 : // If it's not a parameter at all, check if it's a top-level menu request.
2108 0 : if (scripted_menus.find_menu(read_frame->param_num) != nullptr) {
2109 0 : return false; // Delegate to C++ handler.
2110 : }
2111 :
2112 0 : return false;
2113 0 : }
2114 :
2115 : // add a scripted top-level menu
2116 : // called from lua
2117 0 : AP_CRSF_Telem::ScriptedMenu* AP_CRSF_Telem::add_menu(const char* name)
2118 : {
2119 0 : WITH_SEMAPHORE(scr_sem);
2120 :
2121 : // create a new menu
2122 0 : ScriptedMenu* new_menu = scripted_menus.add_menu(name, 2, 0);
2123 : debug("adding menu %d", new_menu->id);
2124 0 : return new_menu;
2125 0 : }
2126 :
2127 : // called from lua
2128 56 : void AP_CRSF_Telem::clear_menus()
2129 : {
2130 56 : WITH_SEMAPHORE(scr_sem);
2131 :
2132 : #ifdef CRSF_DEBUG
2133 : dump_menu_structure();
2134 : #endif
2135 56 : for (ScriptedMenu* m = scripted_menus.next_menu; m != nullptr; ) {
2136 0 : ScriptedMenu* next = m->next_menu;
2137 0 : delete m;
2138 0 : m = next;
2139 : }
2140 56 : scripted_menus.next_menu = nullptr;
2141 56 : }
2142 :
2143 : #ifdef CRSF_DEBUG
2144 : void AP_CRSF_Telem::dump_menu_structure()
2145 : {
2146 : for (ScriptedMenu* m = scripted_menus.next_menu; m != nullptr; m = m->next_menu) {
2147 : if (m->parent_id == 0) {
2148 : m->dump_structure(0);
2149 : }
2150 : }
2151 : }
2152 :
2153 : void AP_CRSF_Telem::ScriptedMenu::dump_structure(uint8_t indent)
2154 : {
2155 : char indent_str[] = " ";
2156 : indent_str[indent] = '\0';
2157 : hal.console->printf("%s[\'%s\': id: %d, parent: %d\n", indent_str, name, id, parent_id);
2158 : for (uint8_t i = 0; i<num_params; i++) {
2159 : if (params[i].length == 0) {
2160 : ScriptedMenu* menu = find_menu(params[i].id);
2161 : if (menu != nullptr) {
2162 : menu->dump_structure(indent+1);
2163 : } else {
2164 : hal.console->printf("No menu %d!\n", params[i].id);
2165 : }
2166 : } else {
2167 : hal.console->printf("%s \'%s\': id:%d, parent:%d, len:%d\n", indent_str, ¶ms[i].data[1], params[i].id, params[i].parent_id, params[i].length);
2168 : }
2169 : }
2170 : hal.console->printf("%s]\n", indent_str);
2171 : }
2172 :
2173 : #endif
2174 :
2175 : #endif //AP_CRSF_SCRIPTING_ENABLED
2176 :
2177 : // get status text data
2178 0 : void AP_CRSF_Telem::calc_status_text()
2179 : {
2180 0 : if (!_statustext.available) {
2181 0 : WITH_SEMAPHORE(_statustext.sem);
2182 : // check link speed
2183 0 : if (_crsf_version.protocol != AP_CRSF_Protocol::ProtocolType::PROTOCOL_ELRS
2184 0 : && !is_high_speed_telemetry(_telem_rf_mode)) {
2185 : // keep only warning/error/critical/alert/emergency status text messages
2186 0 : bool got_message = false;
2187 0 : while (_statustext.queue.pop(_statustext.next)) {
2188 0 : if (_statustext.next.severity <= MAV_SEVERITY_WARNING) {
2189 0 : got_message = true;
2190 0 : break;
2191 : }
2192 : }
2193 0 : if (!got_message) {
2194 0 : return;
2195 : }
2196 0 : } else if (!_statustext.queue.pop(_statustext.next)) {
2197 0 : return;
2198 : }
2199 0 : _statustext.available = true;
2200 0 : }
2201 :
2202 0 : _telem_type = get_custom_telem_frame_id();
2203 0 : _telem.bcast.custom_telem.status_text.sub_type = AP_RCProtocol_CRSF::CustomTelemSubTypeID::CRSF_AP_CUSTOM_TELEM_STATUS_TEXT;
2204 0 : _telem.bcast.custom_telem.status_text.severity = _statustext.next.severity;
2205 : // Note: snprintf() always terminates the string
2206 0 : hal.util->snprintf(_telem.bcast.custom_telem.status_text.text, AP_CRSF_Telem::PASSTHROUGH_STATUS_TEXT_FRAME_MAX_SIZE, "%s", _statustext.next.text);
2207 : // frame size = sub_type(1) + severity(1) + strlen(text) + terminator
2208 : // Note: strlen(_telem.bcast.custom_telem.status_text.text) is safe because called on a guaranteed null terminated string
2209 0 : _telem_size = 2 + strlen(_telem.bcast.custom_telem.status_text.text) + 1;
2210 0 : _telem_pending = true;
2211 0 : _statustext.available = false;
2212 : }
2213 :
2214 : /*
2215 : Get 1 packet of passthrough telemetry data
2216 : */
2217 0 : void AP_CRSF_Telem::get_single_packet_passthrough_telem_data()
2218 : {
2219 0 : _telem_pending = false;
2220 : uint8_t packet_count;
2221 : AP_Frsky_SPort::sport_packet_t packet;
2222 0 : if (!AP_Frsky_Telem::get_telem_data(&packet, packet_count, 1)) {
2223 0 : return;
2224 : }
2225 0 : _telem.bcast.custom_telem.single_packet_passthrough.sub_type = AP_RCProtocol_CRSF::CustomTelemSubTypeID::CRSF_AP_CUSTOM_TELEM_SINGLE_PACKET_PASSTHROUGH;
2226 0 : _telem.bcast.custom_telem.single_packet_passthrough.appid = packet.appid;
2227 0 : _telem.bcast.custom_telem.single_packet_passthrough.data = packet.data;
2228 0 : _telem_size = sizeof(AP_CRSF_Telem::PassthroughSinglePacketFrame);
2229 0 : _telem_type = get_custom_telem_frame_id();
2230 0 : _telem_pending = true;
2231 : }
2232 :
2233 : /*
2234 : Get up to PASSTHROUGH_MULTI_PACKET_FRAME_MAX_SIZE packets of passthrough telemetry data (for slow links)
2235 : Note: we have 2 distinct frame types (single packet vs multi packet) because
2236 : whenever possible we use smaller frames for they have a higher "chance"
2237 : of being transmitted by the crossfire RX scheduler.
2238 : */
2239 0 : void AP_CRSF_Telem::get_multi_packet_passthrough_telem_data(uint8_t size)
2240 : {
2241 0 : size = MIN(size, AP_CRSF_Telem::PASSTHROUGH_MULTI_PACKET_FRAME_MAX_SIZE);
2242 0 : _telem_pending = false;
2243 0 : uint8_t count = 0;
2244 0 : AP_Frsky_SPort::sport_packet_t buffer[AP_CRSF_Telem::PASSTHROUGH_MULTI_PACKET_FRAME_MAX_SIZE] {};
2245 : // we request a PASSTHROUGH_MULTI_PACKET_FRAME_MAX_SIZE packet array, i.e. 9 packets
2246 0 : if (!AP_Frsky_Telem::get_telem_data(buffer, count, size)) {
2247 0 : return;
2248 : }
2249 0 : _telem.bcast.custom_telem.multi_packet_passthrough.sub_type = AP_RCProtocol_CRSF::CustomTelemSubTypeID::CRSF_AP_CUSTOM_TELEM_MULTI_PACKET_PASSTHROUGH;
2250 0 : for (uint8_t idx=0; idx<count; idx++) {
2251 0 : _telem.bcast.custom_telem.multi_packet_passthrough.packets[idx].appid = buffer[idx].appid;
2252 0 : _telem.bcast.custom_telem.multi_packet_passthrough.packets[idx].data = buffer[idx].data;
2253 : }
2254 0 : _telem.bcast.custom_telem.multi_packet_passthrough.size = count;
2255 0 : _telem_size = 2 + sizeof(AP_CRSF_Telem::PassthroughMultiPacketFrame::PassthroughTelemetryPacket)*count; //subtype + size + 6*count
2256 0 : _telem_type = get_custom_telem_frame_id();
2257 0 : _telem_pending = true;
2258 : }
2259 :
2260 : /*
2261 : fetch CRSF frame data
2262 : if is_tx_active is true then this will be a request for telemetry after receiving an RC frame
2263 : */
2264 0 : bool AP_CRSF_Telem::_get_telem_data(AP_RCProtocol_CRSF::Frame* data, bool is_tx_active)
2265 : {
2266 0 : memset(&_telem, 0, sizeof(TelemetryPayload));
2267 : // update telemetry tasks if we either lost or regained the transmitter
2268 0 : if (_is_tx_active != is_tx_active) {
2269 0 : if (is_tx_active) {
2270 0 : disable_scheduler_entry(DEVICE_PING);
2271 0 : enable_tx_entries();
2272 : } else {
2273 0 : disable_tx_entries();
2274 0 : enable_scheduler_entry(DEVICE_PING);
2275 : }
2276 0 : _is_tx_active = is_tx_active;
2277 : }
2278 :
2279 0 : run_wfq_scheduler();
2280 0 : if (!_telem_pending) {
2281 0 : return false;
2282 : }
2283 0 : memcpy(data->payload, &_telem, _telem_size);
2284 0 : data->device_address = AP_CRSF_Protocol::CRSF_ADDRESS_FLIGHT_CONTROLLER; // sync byte
2285 0 : data->length = _telem_size + 2;
2286 0 : data->type = _telem_type;
2287 :
2288 0 : _telem_pending = false;
2289 0 : return true;
2290 : }
2291 :
2292 : /*
2293 : fetch data for an external transport, such as CRSF
2294 : */
2295 0 : bool AP_CRSF_Telem::process_frame(AP_RCProtocol_CRSF::FrameType frame_type, void* data, uint8_t length)
2296 : {
2297 0 : if (!get_singleton()) {
2298 0 : return false;
2299 : }
2300 0 : return singleton->_process_frame(frame_type, data, length);
2301 : }
2302 :
2303 : /*
2304 : fetch data for an external transport, such as CRSF
2305 : */
2306 0 : bool AP_CRSF_Telem::get_telem_data(AP_RCProtocol_CRSF::Frame* data, bool is_tx_active)
2307 : {
2308 0 : if (!get_singleton()) {
2309 0 : return false;
2310 : }
2311 0 : return singleton->_get_telem_data(data, is_tx_active);
2312 : }
2313 :
2314 36440478 : AP_CRSF_Telem *AP_CRSF_Telem::get_singleton(void) {
2315 36440478 : if (!singleton && !hal.util->get_soft_armed()) {
2316 : // if telem data is requested when we are disarmed and don't
2317 : // yet have a AP_CRSF_Telem object then try to allocate one
2318 2096 : NEW_NOTHROW AP_CRSF_Telem();
2319 : // initialize the passthrough scheduler
2320 2096 : if (singleton) {
2321 2096 : singleton->init();
2322 : }
2323 : }
2324 36440478 : return singleton;
2325 : }
2326 :
2327 : namespace AP {
2328 36440478 : AP_CRSF_Telem *crsf_telem() {
2329 36440478 : return AP_CRSF_Telem::get_singleton();
2330 : }
2331 : };
2332 :
2333 : #endif // HAL_CRSF_TELEM_ENABLED
2334 :
2335 :
|