ardupilot/libraries/AP_OSD/AP_OSD_ParamScreen.cpp
Andy Piper c483c04d4b AP_OSD: separate parameter screen
display parameter names and types and allow modification via stick gestures
add support for updating selected parameters
support symbolic names for cetain options with add vehicle specific labels
add support for OSD parameter access and modification over mavlink
save OSD parameter when setting
add missing serial protocols
set defaults on settings correctly
re-organise defaults for NTSC screens and add 9th parameter
allow parameter control to be disabled
add plane aux options (from vierfuffzig)
only enable osd param on bitmap enabled backends
make sure draw() is elided on non-bitmap backends
2020-09-09 20:36:42 +10:00

707 lines
23 KiB
C++

/*
* This file is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This file is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*
* AP_OSD partially based on betaflight and inav osd.c implemention.
* clarity.mcm font is taken from inav configurator.
* Many thanks to their authors.
*/
/*
parameter settings for one screen
*/
#include "AP_OSD.h"
#include "AP_OSD_Backend.h"
#include <AP_HAL/AP_HAL.h>
#include <AP_HAL/Util.h>
#include <limits.h>
#include <ctype.h>
#include <GCS_MAVLink/GCS.h>
#include <AP_RCMapper/AP_RCMapper.h>
#include <AP_Arming/AP_Arming.h>
extern const AP_HAL::HAL& hal;
#if OSD_PARAM_ENABLED
const AP_Param::GroupInfo AP_OSD_ParamScreen::var_info[] = {
// @Param: ENABLE
// @DisplayName: Enable screen
// @Description: Enable this screen
// @Values: 0:Disabled,1:Enabled
// @User: Standard
AP_GROUPINFO_FLAGS("ENABLE", 1, AP_OSD_ParamScreen, enabled, 0, AP_PARAM_FLAG_ENABLE),
// @Param: CHAN_MIN
// @DisplayName: Transmitter switch screen minimum pwm
// @Description: This sets the PWM lower limit for this screen
// @Range: 900 2100
// @User: Standard
AP_GROUPINFO("CHAN_MIN", 2, AP_OSD_ParamScreen, channel_min, 900),
// @Param: CHAN_MAX
// @DisplayName: Transmitter switch screen maximum pwm
// @Description: This sets the PWM upper limit for this screen
// @Range: 900 2100
// @User: Standard
AP_GROUPINFO("CHAN_MAX", 3, AP_OSD_ParamScreen, channel_max, 2100),
// @Param: PARAM1_EN
// @DisplayName: PARAM1_EN
// @Description: Enables display of parameter 1
// @Values: 0:Disabled,1:Enabled
// @Param: PARAM1_X
// @DisplayName: PARAM1_X
// @Description: Horizontal position on screen
// @Range: 0 29
// @Param: PARAM1_Y
// @DisplayName: PARAM1_Y
// @Description: Vertical position on screen
// @Range: 0 15
AP_SUBGROUPINFO(params[0], "PARAM1", 4, AP_OSD_ParamScreen, AP_OSD_ParamSetting),
// @Param: PARAM2_EN
// @DisplayName: PARAM21_EN
// @Description: Enables display of parameter 2
// @Values: 0:Disabled,1:Enabled
// @Param: PARAM2_X
// @DisplayName: PARAM2_X
// @Description: Horizontal position on screen
// @Range: 0 29
// @Param: PARAM2_Y
// @DisplayName: PARAM2_Y
// @Description: Vertical position on screen
// @Range: 0 15
AP_SUBGROUPINFO(params[1], "PARAM2", 5, AP_OSD_ParamScreen, AP_OSD_ParamSetting),
// @Param: PARAM3_EN
// @DisplayName: PARAM3_EN
// @Description: Enables display of parameter 3
// @Values: 0:Disabled,1:Enabled
// @Param: PARAM3_X
// @DisplayName: PARAM3_X
// @Description: Horizontal position on screen
// @Range: 0 29
// @Param: PARAM3_Y
// @DisplayName: PARAM3_Y
// @Description: Vertical position on screen
// @Range: 0 15
AP_SUBGROUPINFO(params[2], "PARAM3", 6, AP_OSD_ParamScreen, AP_OSD_ParamSetting),
// @Param: PARAM4_EN
// @DisplayName: PARAM4_EN
// @Description: Enables display of parameter 4
// @Values: 0:Disabled,1:Enabled
// @Param: PARAM4_X
// @DisplayName: PARAM4_X
// @Description: Horizontal position on screen
// @Range: 0 29
// @Param: PARAM4_Y
// @DisplayName: PARAM4_Y
// @Description: Vertical position on screen
// @Range: 0 15
AP_SUBGROUPINFO(params[3], "PARAM4", 7, AP_OSD_ParamScreen, AP_OSD_ParamSetting),
// @Param: PARAM5_EN
// @DisplayName: PARAM5_EN
// @Description: Enables display of parameter 5
// @Values: 0:Disabled,1:Enabled
// @Param: PARAM5_X
// @DisplayName: PARAM5_X
// @Description: Horizontal position on screen
// @Range: 0 29
// @Param: PARAM5_Y
// @DisplayName: PARAM5_Y
// @Description: Vertical position on screen
// @Range: 0 15
AP_SUBGROUPINFO(params[4], "PARAM5", 8, AP_OSD_ParamScreen, AP_OSD_ParamSetting),
// @Param: PARAM6_EN
// @DisplayName: PARAM6_EN
// @Description: Enables display of parameter 6
// @Values: 0:Disabled,1:Enabled
// @Param: PARAM6_X
// @DisplayName: PARAM6_X
// @Description: Horizontal position on screen
// @Range: 0 29
// @Param: PARAM6_Y
// @DisplayName: PARAM6_Y
// @Description: Vertical position on screen
// @Range: 0 15
AP_SUBGROUPINFO(params[5], "PARAM6", 9, AP_OSD_ParamScreen, AP_OSD_ParamSetting),
// @Param: PARAM7_EN
// @DisplayName: PARAM7_EN
// @Description: Enables display of parameter 7
// @Values: 0:Disabled,1:Enabled
// @Param: PARAM7_X
// @DisplayName: PARAM7_X
// @Description: Horizontal position on screen
// @Range: 0 29
// @Param: PARAM7_Y
// @DisplayName: PARAM7_Y
// @Description: Vertical position on screen
// @Range: 0 15
AP_SUBGROUPINFO(params[6], "PARAM7", 10, AP_OSD_ParamScreen, AP_OSD_ParamSetting),
// @Param: PARAM8_EN
// @DisplayName: PARAM8_EN
// @Description: Enables display of parameter 8
// @Values: 0:Disabled,1:Enabled
// @Param: PARAM8_X
// @DisplayName: PARAM8_X
// @Description: Horizontal position on screen
// @Range: 0 29
// @Param: PARAM8_Y
// @DisplayName: PARAM8_Y
// @Description: Vertical position on screen
// @Range: 0 15
AP_SUBGROUPINFO(params[7], "PARAM8", 11, AP_OSD_ParamScreen, AP_OSD_ParamSetting),
// @Param: PARAM9_EN
// @DisplayName: PARAM9_EN
// @Description: Enables display of parameter 8
// @Values: 0:Disabled,1:Enabled
// @Param: PARAM9_X
// @DisplayName: PARAM9_X
// @Description: Horizontal position on screen
// @Range: 0 29
// @Param: PARAM9_Y
// @DisplayName: PARAM9_Y
// @Description: Vertical position on screen
// @Range: 0 15
AP_SUBGROUPINFO(params[8], "PARAM9", 12, AP_OSD_ParamScreen, AP_OSD_ParamSetting),
// @Param: SAVE_X
// @DisplayName: SAVE_X
// @Description: Horizontal position of Save button on screen
// @Range: 0 25
// @User: Advanced
AP_GROUPINFO("SAVE_X", 13, AP_OSD_ParamScreen, save_x, 23),
// @Param: SAVE_Y
// @DisplayName: SAVE_Y
// @Description: Vertical position of Save button on screen
// @Range: 0 15
// @User: Advanced
AP_GROUPINFO("SAVE_Y", 14, AP_OSD_ParamScreen, save_y, 11),
AP_GROUPEND
};
#define OSD_HOLD_BUTTON_PRESS_DELAY 100
#define OSD_HOLD_BUTTON_PRESS_COUNT 18
#define OSD_PARAM_DEBUG 0
#if OSD_PARAM_DEBUG
static const char* event_names[5] = {
"NONE", "MENU_ENTER", "MENU_UP", "MENU_DOWN", "IN_MENU_EXIT"
};
#define debug(fmt, args ...) do { hal.console->printf("OSD: " fmt, args); } while (0)
#else
#define debug(fmt, args ...)
#endif
AP_OSD_ParamScreen::AP_OSD_ParamScreen() {
AP_Param::setup_object_defaults(this, var_info);
}
void AP_OSD_ParamScreen::draw_parameter(uint8_t number, uint8_t x, uint8_t y)
{
bool param_blink = false;
bool value_blink = false;
const bool selected = number == _selected_param;
switch(_menu_state) {
case MenuState::PARAM_SELECT:
param_blink = selected;
break;
case MenuState::PARAM_VALUE_MODIFY:
value_blink = selected;
break;
case MenuState::PARAM_PARAM_MODIFY:
param_blink = value_blink = selected;
break;
}
if (number >= NUM_PARAMS + 1) {
backend->write(x, y, param_blink, "%s", _requires_save ? " SAVE" : "REBOOT");
return;
}
AP_OSD_ParamSetting& setting = params[number-1];
setting.update();
ap_var_type type = setting._param_type;
AP_Param* p = setting._param;
if (p != nullptr) {
// grab the name of the parameter
char name[17];
p->copy_name_token(setting._current_token, name, 16);
name[16] = 0;
const AP_OSD_ParamSetting::ParamMetadata* metadata = setting.get_custom_metadata();
uint16_t value_pos = 19;
backend->write(x, y, param_blink, "%s:", name);
switch (type) {
case AP_PARAM_INT8: {
int8_t val = ((AP_Int8*)p)->get();
if (metadata != nullptr && val >= 0 && val < metadata->values_max) {
backend->write(value_pos, y, value_blink, "%s", metadata->values[val]);
} else {
backend->write(value_pos, y, value_blink, "%hhd", val);
}
break;
}
case AP_PARAM_INT16: {
int16_t val = ((AP_Int16*)p)->get();
if (metadata != nullptr && val >= 0 && val < metadata->values_max) {
backend->write(value_pos, y, value_blink, "%s", metadata->values[val]);
} else {
backend->write(value_pos, y, value_blink, "%hd", val);
}
break;
}
case AP_PARAM_INT32: {
int32_t val = ((AP_Int16*)p)->get();
if (metadata != nullptr && val >= 0 && val < metadata->values_max) {
backend->write(value_pos, y, value_blink, "%s", metadata->values[val]);
} else {
backend->write(value_pos, y, value_blink, "%d", val);
}
break;
}
case AP_PARAM_FLOAT: {
const float val = ((AP_Float*)p)->get();
// cope with really small value
if (val < 0.01 && !is_zero(val)) {
backend->write(value_pos, y, value_blink, "%.4f", val);
} else if (val < 0.001 && !is_zero(val)) {
backend->write(value_pos, y, value_blink, "%.5f", val);
} else {
backend->write(value_pos, y, value_blink, "%.3f", val);
}
break;
}
case AP_PARAM_VECTOR3F:
case AP_PARAM_NONE:
case AP_PARAM_GROUP:
break;
}
}
}
// modify the selected parameter number
void AP_OSD_ParamScreen::modify_parameter(uint8_t number, Event ev)
{
if (number > NUM_PARAMS) {
return;
}
const AP_OSD_ParamSetting& setting = params[number-1];
AP_Param* p = setting._param;
if (p->is_read_only()) {
return;
}
_requires_save |= 1 << (number-1);
float incr = setting._param_incr * ((ev == Event::MENU_DOWN) ? -1.0f : 1.0f);
int32_t incr_int = int32_t(roundf(incr));
int32_t max_int = int32_t(roundf(setting._param_max));
int32_t min_int = int32_t(roundf(setting._param_min));
if (p != nullptr) {
switch (setting._param_type) {
// there is no way to validate the ranges, so as a rough guess prevent
// integer types going below -1;
case AP_PARAM_INT8: {
AP_Int8* param = (AP_Int8*)p;
param->set(constrain_int16(param->get() + incr_int, min_int, max_int));
break;
}
case AP_PARAM_INT16: {
AP_Int16* param = (AP_Int16*)p;
param->set(constrain_int16(param->get() + incr_int, min_int, max_int));
break;
}
case AP_PARAM_INT32: {
AP_Int32* param = (AP_Int32*)p;
param->set(constrain_int32(param->get() + incr_int, min_int, max_int));
break;
}
case AP_PARAM_FLOAT: {
AP_Float* param = (AP_Float*)p;
param->set(constrain_float(param->get() + incr, setting._param_min, setting._param_max));
break;
}
case AP_PARAM_VECTOR3F:
case AP_PARAM_NONE:
case AP_PARAM_GROUP:
break;
}
}
}
// modify which parameter is configued for the given selection
void AP_OSD_ParamScreen::modify_configured_parameter(uint8_t number, Event ev)
{
if (number > NUM_PARAMS) {
return;
}
_requires_save |= 1 << (number-1);
AP_OSD_ParamSetting& setting = params[number-1];
AP_Param* param;
if (ev == Event::MENU_DOWN) {
param = AP_Param::next_scalar(&setting._current_token, &setting._param_type);
} else {
// going backwards is somewhat convoluted as the param code is geared for going forward
ap_var_type type = AP_PARAM_NONE, prev_type = AP_PARAM_NONE, prev_prev_type = AP_PARAM_NONE;
AP_Param::ParamToken token, prev_token, prev_prev_token;
for (param = AP_Param::first(&token, &type);
param && (setting._current_token.key != token.key
|| setting._current_token.idx != token.idx
|| setting._current_token.group_element != token.group_element);
param = AP_Param::next_scalar(&token, &type)) {
prev_prev_token = prev_token;
prev_prev_type = prev_type;
prev_token = token;
prev_type = type;
}
if (param != nullptr) {
param = AP_Param::next_scalar(&prev_prev_token, &prev_prev_type);
setting._current_token = prev_prev_token;
setting._param_type = prev_prev_type;
}
}
if (param != nullptr) {
// update the stored index
setting._param_group = setting._current_token.group_element;
setting._param_key = AP_Param::get_persistent_key(setting._current_token.key);
setting._param_idx = setting._current_token.idx;
setting._param = param;
setting._type = OSD_PARAM_NONE;
// force update() to refresh the token
setting._current_token.key = 0;
setting._current_token.idx = 0;
setting._current_token.group_element = 0;
}
}
// save all of the parameters
void AP_OSD_ParamScreen::save_parameters()
{
if (!_requires_save) {
return;
}
for (uint8_t i = 0; i < NUM_PARAMS; i++) {
if (params[i].enabled && (_requires_save & (1 << i))) {
AP_Param* p = params[i]._param;
if (p != nullptr) {
p->save();
}
params[i].save_as_new();
}
}
_requires_save = 0;
}
// return radio values as LOW, MIDDLE, HIGH
// this function uses different threshold values to RC_Chanel::get_channel_pos()
// to avoid glitching on the stick travel
RC_Channel::AuxSwitchPos AP_OSD_ParamScreen::get_channel_pos(uint8_t rcmapchan) const
{
const RC_Channel* chan = rc().channel(rcmapchan-1);
if (chan == nullptr) {
return RC_Channel::AuxSwitchPos::LOW;
}
const uint16_t in = chan->get_radio_in();
if (in <= 900 || in >= 2200) {
return RC_Channel::AuxSwitchPos::LOW;
}
// switch is reversed if 'reversed' option set on channel and switches reverse is allowed by RC_OPTIONS
bool switch_reversed = chan->get_reverse() && rc().switch_reverse_allowed();
if (in < 1300) {
return switch_reversed ? RC_Channel::AuxSwitchPos::HIGH : RC_Channel::AuxSwitchPos::LOW;
} else if (in > 1700) {
return switch_reversed ? RC_Channel::AuxSwitchPos::LOW : RC_Channel::AuxSwitchPos::HIGH;
} else {
return RC_Channel::AuxSwitchPos::MIDDLE;
}
}
// map rc input to an event
AP_OSD_ParamScreen::Event AP_OSD_ParamScreen::map_rc_input_to_event() const
{
const RC_Channel::AuxSwitchPos throttle = get_channel_pos(AP::rcmap()->throttle());
const RC_Channel::AuxSwitchPos yaw = get_channel_pos(AP::rcmap()->yaw());
const RC_Channel::AuxSwitchPos roll = get_channel_pos(AP::rcmap()->roll());
const RC_Channel::AuxSwitchPos pitch = get_channel_pos(AP::rcmap()->pitch());
Event result = Event::NONE;
if (yaw != RC_Channel::AuxSwitchPos::MIDDLE || throttle != RC_Channel::AuxSwitchPos::LOW) {
return result;
}
if (pitch == RC_Channel::AuxSwitchPos::MIDDLE && roll == RC_Channel::AuxSwitchPos::LOW) {
result = Event::MENU_EXIT;
} else if (pitch == RC_Channel::AuxSwitchPos::MIDDLE && roll == RC_Channel::AuxSwitchPos::HIGH) {
result = Event::MENU_ENTER;
} else if (pitch == RC_Channel::AuxSwitchPos::LOW && roll == RC_Channel::AuxSwitchPos::MIDDLE) {
result = Event::MENU_UP;
} else if (pitch == RC_Channel::AuxSwitchPos::HIGH && roll == RC_Channel::AuxSwitchPos::MIDDLE) {
result = Event::MENU_DOWN;
} else {
// OSD option has not changed so assume stick re-centering
result = Event::NONE;
}
return result;
}
// update the state machine when disarmed
void AP_OSD_ParamScreen::update_state_machine()
{
const uint32_t now = AP_HAL::millis();
if ((now - _transition_start_ms) < _transition_timeout_ms) {
return;
}
const Event ev = map_rc_input_to_event();
// only take action on transitions
if (ev == Event::NONE && ev == _last_rc_event) {
return;
}
debug("update_state_machine(%s)\n", event_names[int(ev)]);
_transition_start_ms = now;
if (ev == _last_rc_event) {
_transition_timeout_ms = OSD_HOLD_BUTTON_PRESS_DELAY;
_transition_count++;
} else {
_transition_timeout_ms = osd->button_delay_ms;
_transition_count = 0;
}
_last_rc_event = ev;
// if we were armed then there is no selected parameter - so find one
if (_selected_param == 0) {
_selected_param = 1;
for (uint8_t i = 0; i < NUM_PARAMS && !params[_selected_param-1].enabled; i++) {
_selected_param++;
}
}
switch (ev) {
case Event::MENU_ENTER:
switch(_menu_state) {
case MenuState::PARAM_SELECT:
if (_selected_param == SAVE_PARAM) {
if (_transition_count >= OSD_HOLD_BUTTON_PRESS_COUNT) {
save_parameters();
hal.scheduler->reboot(false);
} else {
save_parameters();
}
} else {
_menu_state = MenuState::PARAM_VALUE_MODIFY;
}
break;
case MenuState::PARAM_VALUE_MODIFY:
if (_transition_count >= OSD_HOLD_BUTTON_PRESS_COUNT) {
_menu_state = MenuState::PARAM_PARAM_MODIFY;
}
break;
case MenuState::PARAM_PARAM_MODIFY:
break;
}
break;
case Event::MENU_UP:
switch (_menu_state) {
case MenuState::PARAM_SELECT:
_selected_param--;
if (_selected_param < 1) {
_selected_param = SAVE_PARAM;
}
// skip over parameters that are not enabled
for (uint8_t i = 0; i < NUM_PARAMS + 1 && (_selected_param != SAVE_PARAM && !params[_selected_param-1].enabled); i++) {
_selected_param--;
if (_selected_param < 1) {
_selected_param = SAVE_PARAM;
}
}
// repeat at the standard rate
_transition_timeout_ms = osd->button_delay_ms;
break;
case MenuState::PARAM_VALUE_MODIFY:
modify_parameter(_selected_param, ev);
break;
case MenuState::PARAM_PARAM_MODIFY:
modify_configured_parameter(_selected_param, ev);
break;
}
break;
case Event::MENU_DOWN:
switch (_menu_state) {
case MenuState::PARAM_SELECT:
_selected_param++;
if (_selected_param > SAVE_PARAM) {
_selected_param = 1;
}
// skip over parameters that are not enabled
for (uint8_t i = 0; i < NUM_PARAMS + 1 && (_selected_param != SAVE_PARAM && !params[_selected_param-1].enabled); i++) {
_selected_param++;
if (_selected_param > SAVE_PARAM) {
_selected_param = 1;
}
}
// repeat at the standard rate
_transition_timeout_ms = osd->button_delay_ms;
break;
case MenuState::PARAM_VALUE_MODIFY:
modify_parameter(_selected_param, ev);
break;
case MenuState::PARAM_PARAM_MODIFY:
modify_configured_parameter(_selected_param, ev);
break;
}
break;
case Event::MENU_EXIT:
switch(_menu_state) {
case MenuState::PARAM_SELECT:
break;
case MenuState::PARAM_VALUE_MODIFY:
_menu_state = MenuState::PARAM_SELECT;
break;
case MenuState::PARAM_PARAM_MODIFY:
_menu_state = MenuState::PARAM_VALUE_MODIFY;
break;
}
break;
case Event::NONE:
break;
}
}
void AP_OSD_ParamScreen::draw(void)
{
if (!enabled || !backend) {
return;
}
// first update the state machine
if (!AP::arming().is_armed()) {
update_state_machine();
} else {
_selected_param = 0;
}
for (uint8_t i = 0; i < NUM_PARAMS; i++) {
AP_OSD_ParamSetting n = params[i];
if (n.enabled) {
draw_parameter(n._param_number, n.xpos, n.ypos);
}
}
// the save button
draw_parameter(SAVE_PARAM, save_x, save_y);
}
// handle OSD configuration messages
void AP_OSD_ParamScreen::handle_write_msg(const mavlink_osd_param_config_t& packet, const GCS_MAVLINK& link)
{
// request out of range - return an error
if (packet.osd_index < 1 || packet.osd_index > AP_OSD_ParamScreen::NUM_PARAMS) {
mavlink_msg_osd_param_config_reply_send(link.get_chan(), packet.request_id, OSD_PARAM_INVALID_PARAMETER_INDEX);
return;
}
// set the parameter
bool ret = params[packet.osd_index - 1].set_by_name(packet.param_id, packet.config_type, packet.min_value, packet.max_value, packet.increment);
mavlink_msg_osd_param_config_reply_send(link.get_chan(), packet.request_id, ret ? OSD_PARAM_SUCCESS : OSD_PARAM_INVALID_PARAMETER);
}
// handle OSD show configuration messages
void AP_OSD_ParamScreen::handle_read_msg(const mavlink_osd_param_show_config_t& packet, const GCS_MAVLINK& link)
{
// request out of range - return an error
if (packet.osd_index < 1 || packet.osd_index > AP_OSD_ParamScreen::NUM_PARAMS) {
mavlink_msg_osd_param_show_config_reply_send(link.get_chan(), packet.request_id, OSD_PARAM_INVALID_PARAMETER_INDEX,
nullptr, OSD_PARAM_NONE, 0, 0, 0);
return;
}
// get the parameter and make sure it is fresh
AP_OSD_ParamSetting& param = params[packet.osd_index - 1];
param.update();
// check for bad things
if (param._param == nullptr) {
mavlink_msg_osd_param_show_config_reply_send(link.get_chan(), packet.request_id, OSD_PARAM_INVALID_PARAMETER_INDEX,
nullptr, OSD_PARAM_NONE, 0, 0, 0);
return;
}
// get the name and send back the details
char buf[AP_MAX_NAME_SIZE+1];
param._param->copy_name_token(param._current_token, buf, AP_MAX_NAME_SIZE);
buf[AP_MAX_NAME_SIZE] = 0;
mavlink_msg_osd_param_show_config_reply_send(link.get_chan(), packet.request_id, OSD_PARAM_SUCCESS,
buf, param._type, param._param_min, param._param_max, param._param_incr);
}
#endif // OSD_PARAM_ENABLED
// pre_arm_check - returns true if all pre-takeoff checks have completed successfully
bool AP_OSD::pre_arm_check(char *failure_msg, const uint8_t failure_msg_len) const
{
// currently in the OSD menu, do not allow arming
if (!is_readonly_screen()) {
hal.util->snprintf(failure_msg, failure_msg_len, "In OSD menu");
return false;
}
// if we got this far everything must be ok
return true;
}