/* * 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 . * * 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 #include #include #include #include #include #include #include #include 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), // @Group: PARAM1 // @Path: AP_OSD_ParamSetting.cpp AP_SUBGROUPINFO(params[0], "PARAM1", 4, AP_OSD_ParamScreen, AP_OSD_ParamSetting), // @Group: PARAM2 // @Path: AP_OSD_ParamSetting.cpp AP_SUBGROUPINFO(params[1], "PARAM2", 5, AP_OSD_ParamScreen, AP_OSD_ParamSetting), // @Group: PARAM3 // @Path: AP_OSD_ParamSetting.cpp AP_SUBGROUPINFO(params[2], "PARAM3", 6, AP_OSD_ParamScreen, AP_OSD_ParamSetting), // @Group: PARAM4 // @Path: AP_OSD_ParamSetting.cpp AP_SUBGROUPINFO(params[3], "PARAM4", 7, AP_OSD_ParamScreen, AP_OSD_ParamSetting), // @Group: PARAM5 // @Path: AP_OSD_ParamSetting.cpp AP_SUBGROUPINFO(params[4], "PARAM5", 8, AP_OSD_ParamScreen, AP_OSD_ParamSetting), // @Group: PARAM6 // @Path: AP_OSD_ParamSetting.cpp AP_SUBGROUPINFO(params[5], "PARAM6", 9, AP_OSD_ParamScreen, AP_OSD_ParamSetting), // @Group: PARAM7 // @Path: AP_OSD_ParamSetting.cpp AP_SUBGROUPINFO(params[6], "PARAM7", 10, AP_OSD_ParamScreen, AP_OSD_ParamSetting), // @Group: PARAM8 // @Path: AP_OSD_ParamSetting.cpp AP_SUBGROUPINFO(params[7], "PARAM8", 11, AP_OSD_ParamScreen, AP_OSD_ParamSetting), // @Group: PARAM9 // @Path: AP_OSD_ParamSetting.cpp 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 static const AP_OSD_ParamSetting::Initializer PARAM_DEFAULTS[AP_OSD_NUM_PARAM_SCREENS][AP_OSD_ParamScreen::NUM_PARAMS] { #if APM_BUILD_COPTER_OR_HELI { { 1, { 102, 0, 4033 }, OSD_PARAM_NONE }, // ATC_RAT_RLL_P { 2, { 102, 0, 129 }, OSD_PARAM_NONE }, // ATC_RAT_RLL_D { 3, { 102, 0, 705 }, OSD_PARAM_NONE }, // ATC_RAT_RLL_FLTD { 4, { 102, 0, 4034 }, OSD_PARAM_NONE }, // ATC_RAT_PIT_P { 5, { 102, 0, 130 }, OSD_PARAM_NONE }, // ATC_RAT_PIT_D { 6, { 102, 0, 706 }, OSD_PARAM_NONE }, // ATC_RAT_PIT_FLTD { 7, { 102, 0, 4035 }, OSD_PARAM_NONE }, // ATC_RAT_YAW_P { 8, { 102, 0, 131 }, OSD_PARAM_NONE }, // ATC_RAT_YAW_D { 9, { 102, 0, 643 }, OSD_PARAM_NONE } // ATC_RAT_YAW_FLTE }, { { 1, { 3, 0, 231 }, OSD_PARAM_NONE }, // INS_LOG_BAT_OPT { 2, { 3, 0, 167 }, OSD_PARAM_NONE }, // INS_LOG_BAT_MASK { 3, { 60, 0, 0 }, OSD_PARAM_NONE }, // LOG_BITMASK { 4, { 3, 0, 18 }, OSD_PARAM_NONE }, // INS_GYRO_FILT { 5, { 102, 0, 6 }, OSD_PARAM_NONE }, // ATC_THR_MIX_MAN { 6, { 102, 0, 5 }, OSD_PARAM_NONE }, // ATC_THR_MIX_MAX { 7, { 6, 0, 25041 }, OSD_PARAM_AUX_FUNCTION }, // RC7_OPTION { 8, { 6, 0, 25105 }, OSD_PARAM_AUX_FUNCTION }, // RC8_OPTION { 9, { 36, 0, 1047 }, OSD_PARAM_FAILSAFE_ACTION_2 } // BATT_FS_LOW_ACT } #elif APM_BUILD_TYPE(APM_BUILD_ArduPlane) { { 1, { 232, 0, 265 }, OSD_PARAM_NONE }, // RLL_RATE_FF { 2, { 232, 0, 4041 }, OSD_PARAM_NONE }, // RLL_RATE_P { 3, { 232, 0, 73 }, OSD_PARAM_NONE }, // RLL_RATE_I { 4, { 233, 0, 267 }, OSD_PARAM_NONE }, // PTCH_RATE_FF { 5, { 233, 0, 4043 }, OSD_PARAM_NONE }, // PTCH_RATE_P { 6, { 233, 0, 75 }, OSD_PARAM_NONE }, // PTCH_RATE_I { 7, { 233, 0, 6 }, OSD_PARAM_NONE }, // PTCH2SRV_RLL { 8, { 199, 0, 1 }, OSD_PARAM_NONE }, // TUNE_PARAM { 9, { 199, 0, 320 }, OSD_PARAM_NONE } // TUNE_RANGE }, { { 1, { 185, 0, 0 }, OSD_PARAM_NONE }, // TRIM_THROTTLE { 2, { 155, 0, 0 }, OSD_PARAM_NONE }, // AIRSPEED_CRUISE { 3, { 4, 0, 1094 }, OSD_PARAM_NONE }, // SERVO_AUTO_TRIM { 4, { 120, 0, 0 }, OSD_PARAM_NONE}, // AIRSPEED_MIN { 5, { 121, 0, 0 }, OSD_PARAM_NONE }, // AIRSPEED_MAX { 6, { 156, 0, 0 }, OSD_PARAM_NONE }, // RTL_ALTITUDE { 7, { 140, 2, 8 }, OSD_PARAM_NONE }, // AHRS_TRIM_Y { 8, { 182, 0, 0 }, OSD_PARAM_NONE }, // THR_MAX { 9, { 189, 0, 0 }, OSD_PARAM_NONE } // THR_SLEWRATE } #else { { 1 }, { 2 }, { 3 }, { 4 }, { 5 }, { 6 }, { 7 }, { 8 }, { 9 }, }, { { 1 }, { 2 }, { 3 }, { 4 }, { 5 }, { 6 }, { 7 }, { 8 }, { 9 } } #endif }; AP_OSD_ParamScreen::AP_OSD_ParamScreen(uint8_t index) { for (uint8_t i = 0; i < NUM_PARAMS; i++) { params[i] = PARAM_DEFAULTS[index][i]; } AP_Param::setup_object_defaults(this, var_info); } AP_OSD_ParamSetting* AP_OSD_ParamScreen::get_setting(uint8_t param_idx) { if (param_idx >= NUM_PARAMS) { return nullptr; } params[param_idx].update(); // make sure we are fresh return ¶ms[param_idx]; } #if OSD_ENABLED 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]; setting.copy_name(name, 17); 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", (signed)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 == nullptr || p->is_read_only()) { return; } _requires_save |= 1 << (number-1); const float incr = setting._param_incr * ((ev == Event::MENU_DOWN) ? -1.0f : 1.0f); const int32_t incr_int = int32_t(roundf(incr)); const int32_t max_int = int32_t(roundf(setting._param_max)); const int32_t min_int = int32_t(roundf(setting._param_min)); 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 configured 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.set(setting._current_token.group_element); setting._param_key.set(AP_Param::get_persistent_key(setting._current_token.key)); setting._param_idx.set(setting._current_token.idx); setting._param = param; setting._type.set(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; } } // 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().option_is_enabled(RC_Channels::Option::ALLOW_SWITCH_REV); if (in < RC_Channel::AUX_PWM_TRIGGER_LOW) { return switch_reversed ? RC_Channel::AuxSwitchPos::HIGH : RC_Channel::AuxSwitchPos::LOW; } else if (in > RC_Channel::AUX_PWM_TRIGGER_HIGH) { 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; } } #if HAL_WITH_OSD_BITMAP || HAL_WITH_MSP_DISPLAYPORT 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); } #endif #endif // OSD_ENABLED // 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; } // handle OSD configuration messages #if HAL_GCS_ENABLED void AP_OSD_ParamScreen::handle_write_msg(const mavlink_osd_param_config_t& packet, const class 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 class 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 #endif // OSD_PARAM_ENABLED