Files
gt3b/menu.c

792 lines
19 KiB
C

/*
menu - handle showing to display, menus for basic channel settings
Copyright (C) 2011 Pavel Semerad
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <string.h>
#include "menu.h"
#include "config.h"
#include "calc.h"
#include "timer.h"
#include "ppm.h"
#include "lcd.h"
#include "buzzer.h"
#include "input.h"
// variables to be used in CALC task
u8 menu_force_value_channel; // set PPM value for this channel
s16 menu_force_value; // to this value (-500..500)
s8 menu_channel3_8[MAX_CHANNELS - 2]; // values -100..100 for channels >=3
u8 menu_channels_mixed; // channel with 1 here will not be set from
// menu_channel3_8
s8 menu_4WS_mix; // mix -100..100
_Bool menu_4WS_crab; // when 1, crab steering
s8 menu_DIG_mix; // mix -100..100
// flags for wakeup after each ADC measure
_Bool menu_wants_adc;
// battery low flag
_Bool menu_battery_low;
// raw battery ADC value for check to battery low
u16 battery_low_raw;
// don't stop main loop and check keys
u8 menu_check_keys;
// ****************** UTILITY FUNCTIONS ******************************
// apply model settings to variables
void apply_model_config(void) {
u8 i, autorepeat = 0;
// set number of channels for this model
if (channels != MAX_CHANNELS) {
ppm_set_channels(MAX_CHANNELS); // maybe sometime cm.channels
// task CALC must be awaked to do first PPM calculation
if (input_initialized) awake(CALC);
}
// set mixed channels to ignore them from menu_channel3_8
menu_channels_mixed = 0;
if (cm.channel_4WS)
menu_channels_mixed |= (u8)(1 << (u8)(cm.channel_4WS - 1));
if (cm.channel_DIG)
menu_channels_mixed |= (u8)(1 << (u8)(cm.channel_DIG - 1));
// set autorepeat
for (i = 0; i < 4; i++) {
if (!ck.et_map[i].is_trim) continue; // trim is off, skip
if (ck.et_map[i].buttons == ETB_AUTORPT)
autorepeat |= (u8)((u8)et_buttons[i][0] | (u8)et_buttons[i][1]);
}
button_autorepeat(autorepeat);
}
// load model config from eeprom and set model settings
void menu_load_model(void) {
u8 i;
// load config
config_model_read();
// set values of channels >= 3 to default left state,
// for channels mapped to some trims/keys, it will next be set
// to corresponding centre/reset value
for (i = 0; i <= MAX_CHANNELS - 3; i++)
menu_channel3_8[i] = -100;
// set 4WS and DIG to defaults
menu_4WS_mix = 0;
menu_4WS_crab = 0;
menu_DIG_mix = 0;
// set state of buttons to do initialize
menu_buttons_initialize();
// apply config to radio setting
apply_model_config();
}
// apply global setting to variables
void apply_global_config(void) {
backlight_set_default(cg.backlight_time);
backlight_on();
// compute raw value for battery low voltage
battery_low_raw = (u16)(((u32)cg.battery_calib * cg.battery_low + 50) / 100);
}
// show model number, extra function to handle more than 10 models
static void show_model_number(u8 model) {
lcd_7seg((u8)(model % 10));
lcd_segment(LS_SYM_RIGHT, (u8)((u8)(model / 10) & 1));
lcd_segment(LS_SYM_LEFT, (u8)((u8)(model / 20) & 1));
}
// menu stop - checks low battery
void menu_stop(void) {
static _Bool battery_low_on;
stop();
// low_bat is disabled in calibrate, key-test and global menus,
// check it by buzzer_running
if (menu_battery_low && !buzzer_running) battery_low_on = 0;
if (battery_low_on == menu_battery_low) return; // no change
// battery low status changed
if (menu_battery_low) {
// battery low firstly
battery_low_on = 1;
lcd_segment(LS_SYM_LOWPWR, LS_ON);
lcd_segment_blink(LS_SYM_LOWPWR, LB_SPC);
buzzer_on(40, 160, BUZZER_MAX);
}
else {
// battery low now OK
battery_low_on = 0;
lcd_segment(LS_SYM_LOWPWR, LS_OFF);
buzzer_off();
}
lcd_update();
}
// show main screen (model number and name/battery/...)
#define MS_NAME 0
#define MS_BATTERY 1
#define MS_MAX 2
static void main_screen(u8 item) {
lcd_segment(LS_SYM_MODELNO, LS_ON);
lcd_segment(LS_SYM_CHANNEL, LS_OFF);
lcd_segment(LS_SYM_PERCENT, LS_OFF);
show_model_number(cg.model);
menu_wants_adc = 0;
// chars is item dependent
if (item == MS_NAME) {
// model name
lcd_segment(LS_SYM_DOT, LS_OFF);
lcd_segment(LS_SYM_VOLTS, LS_OFF);
lcd_chars(cm.name);
}
else if (item == MS_BATTERY) {
static u16 bat_val;
static u16 bat_time;
// battery voltage
lcd_segment(LS_SYM_DOT, LS_ON);
lcd_segment(LS_SYM_VOLTS, LS_ON);
// calculate voltage from current raw value and calib value
if (time_sec >= bat_time) {
bat_time = time_sec + 2;
bat_val = (u16)(((u32)adc_battery * 100 + 300) / cg.battery_calib);
}
lcd_char_num3(bat_val);
menu_wants_adc = 1;
}
lcd_update();
}
// common menu processing, selecting channel and then changing values
static _Bool menu_adc_direction;
static void menu_set_adc_direction(u8 channel) {
u16 adc, calm;
// for channel 2 use throttle, for others steering
if (channel == 2) {
adc = adc_throttle_ovs;
calm = cg.calib_throttle_mid;
}
else {
adc = adc_steering_ovs;
calm = cg.calib_steering_mid;
}
// if over threshold to one side, set menu_adc_direction
if (adc < ((calm - 40) << ADC_OVS_SHIFT)) {
if (menu_adc_direction) {
menu_adc_direction = 0;
lcd_segment(LS_SYM_LEFT, LS_ON);
lcd_segment(LS_SYM_RIGHT, LS_OFF);
}
}
else if (adc > ((calm + 40) << ADC_OVS_SHIFT)) {
if (!menu_adc_direction) {
menu_adc_direction = 1;
lcd_segment(LS_SYM_LEFT, LS_OFF);
lcd_segment(LS_SYM_RIGHT, LS_ON);
}
}
else if (channel > 2 && btn(BTN_CH3)) {
// use CH3 button to toggle also
menu_adc_direction ^= 1;
lcd_segment(LS_SYM_LEFT, (u8)(menu_adc_direction ? LS_OFF : LS_ON));
lcd_segment(LS_SYM_RIGHT, (u8)(menu_adc_direction ? LS_ON : LS_OFF));
}
// if this channel is using forced values, set it to left/right
if (menu_force_value_channel)
menu_force_value = menu_adc_direction ? PPM(500) : PPM(-500);
}
static _Bool menu_set_adc(u8 channel, u8 use_adc, u8 force_values) {
// check if force servos to positions (left/center/right)
if ((u8)(force_values & (1 << (channel - 1)))) {
menu_force_value_channel = channel;
menu_force_value = 0; // to center by default
}
else menu_force_value_channel = 0;
// check use of ADC
if ((u8)(use_adc & (1 << (channel - 1)))) {
// use ADC
if (menu_adc_direction) {
lcd_segment(LS_SYM_LEFT, LS_OFF);
lcd_segment(LS_SYM_RIGHT, LS_ON);
}
else {
lcd_segment(LS_SYM_LEFT, LS_ON);
lcd_segment(LS_SYM_RIGHT, LS_OFF);
}
menu_wants_adc = 1;
menu_set_adc_direction(channel);
return 1;
}
else {
// don't use ADC
lcd_segment(LS_SYM_LEFT, LS_OFF);
lcd_segment(LS_SYM_RIGHT, LS_OFF);
menu_wants_adc = 0;
return 0;
}
}
static void menu_channel(u8 end_channel, u8 use_adc, u8 forced_values,
void (*subfunc)(u8, u8)) {
u8 channel = 1;
_Bool chan_val = 0; // now in channel
_Bool adc_active;
u8 last_direction;
// show CHANNEL
lcd_segment(LS_SYM_MODELNO, LS_OFF);
lcd_segment(LS_SYM_CHANNEL, LS_ON);
// show channel number and possible direction
menu_adc_direction = 0;
lcd_7seg(channel);
lcd_set_blink(L7SEG, LB_SPC);
adc_active = menu_set_adc(channel, use_adc, forced_values);
subfunc((u8)(channel - 1), 0); // show current value
lcd_update();
while (1) {
btnra();
menu_stop();
if (btn(BTN_BACK | BTN_END) || btnl(BTN_ENTER)) break;
last_direction = menu_adc_direction;
if (adc_active) menu_set_adc_direction(channel);
if (btn(BTN_ROT_ALL)) {
if (chan_val) {
// change value
subfunc((u8)(channel - 1), 1);
lcd_chars_blink(LB_SPC);
}
else {
// change channel number
if (btn(BTN_ROT_L)) {
if (!--channel) channel = end_channel;
}
else {
if (++channel > end_channel) channel = 1;
}
lcd_7seg(channel);
lcd_set_blink(L7SEG, LB_SPC);
adc_active = menu_set_adc(channel, use_adc, forced_values);
subfunc((u8)(channel - 1), 0);
}
lcd_update();
last_direction = menu_adc_direction; // was already showed
}
else if (btn(BTN_ENTER)) {
// switch channel/value
key_beep();
if (chan_val) {
// switch to channel number
lcd_set_blink(L7SEG, LB_SPC);
lcd_chars_blink(LB_OFF);
chan_val = 0;
}
else {
// switch to value
lcd_set_blink(L7SEG, LB_OFF);
lcd_chars_blink(LB_SPC);
chan_val = 1;
}
}
if (last_direction != menu_adc_direction) {
// show other dir value
subfunc((u8)(channel - 1), 0);
lcd_update();
if (chan_val) {
lcd_chars_blink(LB_SPC);
}
}
}
menu_wants_adc = 0;
menu_force_value_channel = 0;
key_beep();
config_model_save();
}
// change value based on state of rotate encoder
s16 menu_change_val(s16 val, s16 min, s16 max, u8 amount_fast, u8 rotate) {
u8 amount = 1;
if (btn(BTN_ROT_L)) {
// left
if (btnl(BTN_ROT_L)) amount = amount_fast;
val -= amount;
if (val < min)
if (rotate) val = max;
else val = min;
}
else {
// right
if (btnl(BTN_ROT_R)) amount = amount_fast;
val += amount;
if (val > max)
if (rotate) val = min;
else val = max;
}
return val;
}
// *************************** MODEL MENUS *******************************
// select model/save model as (to selected model position)
#define MIN(a, b) (a < b ? a : b)
static void menu_model(u8 saveas) {
u8 model = cg.model;
if (saveas) lcd_set_blink(LMENU, LB_SPC);
lcd_set_blink(L7SEG, LB_SPC);
while (1) {
btnra();
menu_stop();
if (btn(BTN_BACK | BTN_END | BTN_ENTER)) break;
if (btn(BTN_ROT_ALL)) {
model = (u8)menu_change_val((s16)model, 0,
MIN(CONFIG_MODEL_MAX, 40) - 1,
MODEL_FAST, 1);
show_model_number(model);
lcd_set_blink(L7SEG, LB_SPC);
lcd_chars(config_model_name(model));
lcd_update();
}
}
key_beep();
// if new model choosed, save it
if (model != cg.model) {
cg.model = model;
config_global_save();
if (saveas) {
// save to new model position
config_model_save();
}
else {
// load selected model
menu_load_model();
}
}
if (saveas) lcd_set_blink(LMENU, LB_OFF);
}
// change model name
static void menu_name(void) {
u8 pos = LCHR1;
u8 letter = cm.name[0];
lcd_set_blink(LCHR1, LB_SPC);
while (1) {
btnra();
menu_stop();
if (btn(BTN_BACK | BTN_END) || btnl(BTN_ENTER)) break;
if (btn(BTN_ENTER)) {
key_beep();
// to next char
lcd_set_blink(pos, LB_OFF);
if (++pos > LCHR3) pos = LCHR1;
lcd_set_blink(pos, LB_SPC);
lcd_update();
letter = cm.name[pos];
}
else if (btn(BTN_ROT_ALL)) {
// change letter
if (btn(BTN_ROT_L)) {
// lower
if (letter == '0') letter = 'Z';
else if (letter == 'A') letter = '9';
else letter--;
}
else {
// upper
if (letter == '9') letter = 'A';
else if (letter == 'Z') letter = '0';
else letter++;
}
cm.name[pos] = letter;
lcd_char(pos, letter);
lcd_set_blink(pos, LB_SPC);
lcd_update();
}
}
key_beep();
config_model_save();
}
// reset model to default values
static void menu_reset_model(void) {
config_model_set_default();
config_model_save();
apply_model_config();
}
// set reverse
void sf_reverse(u8 channel, u8 change) {
u8 bit = (u8)(1 << channel);
if (change) cm.reverse ^= bit;
if (cm.reverse & bit) lcd_chars("REV");
else lcd_chars("NOR");
}
@inline static void menu_reverse(void) {
menu_channel(MAX_CHANNELS, 0, 0, sf_reverse);
}
// set endpoints
void sf_endpoint(u8 channel, u8 change) {
u8 *addr = &cm.endpoint[channel][menu_adc_direction];
if (change) *addr = (u8)menu_change_val(*addr, 0, cg.endpoint_max,
ENDPOINT_FAST, 0);
lcd_char_num3(*addr);
}
static void menu_endpoint(void) {
lcd_segment(LS_SYM_PERCENT, LS_ON);
menu_channel(MAX_CHANNELS, 0xff, 0xfc, sf_endpoint);
lcd_segment(LS_SYM_PERCENT, LS_OFF);
}
// set trims
static void sf_trim(u8 channel, u8 change) {
s8 *addr = &cm.trim[channel];
if (change) *addr = (s8)menu_change_val(*addr, -TRIM_MAX, TRIM_MAX,
TRIM_FAST, 0);
if (channel == 0) lcd_char_num2_lbl(*addr, "LNR");
else lcd_char_num2_lbl(*addr, "FNB");
}
@inline static void menu_trim(void) {
menu_channel(2, 0, 0, sf_trim);
}
// set subtrims
static void sf_subtrim(u8 channel, u8 change) {
s8 *addr = &cm.subtrim[channel];
if (change)
*addr = (s8)menu_change_val(*addr, -SUBTRIM_MAX, SUBTRIM_MAX,
SUBTRIM_FAST, 0);
lcd_char_num3(*addr);
}
static void menu_subtrim(void) {
lcd_set_blink(LMENU, LB_SPC);
menu_channel(MAX_CHANNELS, 0, 0xfc, sf_subtrim);
lcd_set_blink(LMENU, LB_OFF);
}
// set dualrate
static void sf_dualrate(u8 channel, u8 change) {
u8 *addr = &cm.dualrate[channel];
if (channel == 1 && menu_adc_direction) addr = &cm.dualrate[2];
if (change) *addr = (u8)menu_change_val(*addr, 0, 100, DUALRATE_FAST, 0);
lcd_char_num3(*addr);
}
static void menu_dualrate(void) {
lcd_segment(LS_SYM_PERCENT, LS_ON);
menu_channel(2, 0x2, 0, sf_dualrate);
lcd_segment(LS_SYM_PERCENT, LS_OFF);
}
// set servo speed
static void sf_speed(u8 channel, u8 change) {
u8 *addr = menu_adc_direction ? &cm.stspd_return : &cm.stspd_turn;
if (change) *addr = (u8)menu_change_val(*addr, 1, 100, SPEED_FAST, 0);
lcd_char_num3(*addr);
}
static void menu_speed(void) {
lcd_set_blink(LMENU, LB_SPC);
lcd_segment(LS_SYM_PERCENT, LS_ON);
menu_channel(1, 0x1, 0, sf_speed);
lcd_segment(LS_SYM_PERCENT, LS_OFF);
lcd_set_blink(LMENU, LB_OFF);
}
// set expos
static void sf_expo(u8 channel, u8 change) {
s8 *addr = &cm.expo[channel];
if (channel == 1 && menu_adc_direction) addr = &cm.expo[2];
if (change) *addr = (s8)menu_change_val(*addr, -EXPO_MAX, EXPO_MAX,
EXPO_FAST, 0);
lcd_char_num3(*addr);
}
static void menu_expo(void) {
lcd_segment(LS_SYM_PERCENT, LS_ON);
menu_channel(2, 0x2, 0, sf_expo);
lcd_segment(LS_SYM_PERCENT, LS_OFF);
}
// set abs: OFF, SLO(6), NOR(4), FAS(3)
// pulses between full brake and 1/2 brake and only when enought brake applied
static const u8 abs_labels[][4] = {
"OFF", "SLO", "NOR", "FAS"
};
#define ABS_LABEL_SIZE (sizeof(abs_labels) / sizeof(u8 *))
static void menu_abs(void) {
lcd_segment(LS_SYM_MODELNO, LS_OFF);
lcd_segment(LS_SYM_LEFT, LS_OFF);
lcd_segment(LS_SYM_RIGHT, LS_OFF);
lcd_segment(LS_SYM_CHANNEL, LS_ON);
lcd_7seg(2);
lcd_chars(abs_labels[cm.abs_type]);
lcd_chars_blink(LB_SPC);
lcd_update();
while (1) {
btnra();
menu_stop();
if (btn(BTN_BACK | BTN_END | BTN_ENTER)) break;
if (btn(BTN_ROT_ALL)) {
cm.abs_type = (u8)menu_change_val(cm.abs_type, 0, ABS_LABEL_SIZE-1,
1, 1);
lcd_chars(abs_labels[cm.abs_type]);
lcd_chars_blink(LB_SPC);
lcd_update();
}
}
key_beep();
config_model_save();
}
// choose from menu items
static void select_menu(void) {
u8 menu = LM_MODEL;
lcd_menu(menu);
main_screen(MS_NAME); // show model number and name
while (1) {
btnra();
menu_stop();
// Back/End key to end this menu
if (btn(BTN_BACK | BTN_END)) break;
// Enter key - goto submenu
if (btn(BTN_ENTER)) {
key_beep();
if (menu == LM_MODEL) {
if (btnl(BTN_ENTER)) menu_model(1);
else {
menu_model(0);
break;
}
}
else if (menu == LM_NAME) {
if (btnl(BTN_ENTER)) menu_reset_model();
else menu_name();
}
else if (menu == LM_REV) {
if (btnl(BTN_ENTER)) menu_key_mapping();
else menu_reverse();
}
else if (menu == LM_EPO) {
if (btnl(BTN_ENTER)) menu_mix();
else menu_endpoint();
}
else if (menu == LM_TRIM) {
if (btnl(BTN_ENTER)) menu_subtrim();
else menu_trim();
}
else if (menu == LM_DR) {
if (btnl(BTN_ENTER)) menu_speed();
else menu_dualrate();
}
else if (menu == LM_EXP) menu_expo();
else {
if (btnl(BTN_ENTER)) break;
else menu_abs();
}
main_screen(MS_NAME); // show model number and name
// exit when BACK
if (btn(BTN_BACK)) break;
}
// rotate keys
else if (btn(BTN_ROT_ALL)) {
if (btn(BTN_ROT_R)) {
menu >>= 1;
if (!menu) menu = LM_MODEL;
}
else {
menu <<= 1;
if (!menu) menu = LM_ABS;
}
lcd_menu(menu);
lcd_update();
}
}
key_beep();
lcd_menu(0);
}
// ****************** MAIN LOOP and init *********************************
// main menu loop, shows main screen and handle keys
static void menu_loop(void) {
u8 item = MS_NAME;
lcd_clear();
while (1) {
if (!menu_check_keys) {
main_screen(item);
btnra();
menu_stop();
// don't wanted in submenus, will be set back in main_screen()
menu_wants_adc = 0;
}
else menu_check_keys = 0;
// Enter long key - global/calibrate/key-test
if (btnl(BTN_ENTER)) {
if (adc_steering_ovs > (CALIB_ST_MID_HIGH << ADC_OVS_SHIFT))
menu_calibrate();
else if (adc_steering_ovs < (CALIB_ST_LOW_MID << ADC_OVS_SHIFT))
menu_key_test();
else menu_global_setup();
}
// Enter key - menu
else if (btn(BTN_ENTER)) {
key_beep();
select_menu();
btnra();
}
// electronic trims
else if (menu_electronic_trims())
menu_check_keys = 1;
// buttons (CH3, Back, End)
else if (menu_buttons())
menu_check_keys = 1;
// rotate encoder - change model name/battery/...
else if (btn(BTN_ROT_ALL)) {
if (btn(BTN_ROT_L)) {
if (item) item--;
else item = MS_MAX - 1;
}
else {
if (++item >= MS_MAX) item = 0;
}
}
}
}
// read config from eeprom, initialize and call menu loop
void menu_init(void) {
// variables
menu_key_mapping_prepare();
// read global config from eeprom, if calibrate values changed,
// call calibrate
if (config_global_read())
menu_calibrate();
apply_global_config();
// read model config from eeprom, but now awake CALC yet
menu_load_model();
// and main loop
menu_loop();
}