/* 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 . */ #include #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) // flags for wakeup after each ADC measure _Bool menu_adc_wakeup; // don't stop main loop and check keys u8 menu_check_keys; // temporary flag used when doing reset (global/all models/model) _Bool menu_tmp_flag; // 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)); lcd_segment(LS_SYM_PERCENT, (u8)((u8)(model / 40) & 1)); lcd_segment(LS_SYM_MODELNO, LS_ON); } // show main screen (model number and name/battery/...) static void main_screen(u8 item) { menu_adc_wakeup = 0; // chars is item dependent if (item == MS_NAME) { // model name lcd_segment(LS_SYM_CHANNEL, LS_OFF); lcd_segment(LS_SYM_DOT, LS_OFF); lcd_segment(LS_SYM_VOLTS, LS_OFF); show_model_number(cg.model); lcd_chars(cm.name); } else if (item == MS_BATTERY) { static u16 bat_val; static u16 bat_time; // battery voltage lcd_segment(LS_SYM_CHANNEL, LS_OFF); lcd_segment(LS_SYM_DOT, LS_ON); lcd_segment(LS_SYM_VOLTS, LS_ON); show_model_number(cg.model); // 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_adc_wakeup = 1; } else { // timers menu_timer_show((u8)(item - MS_TIMER0)); } lcd_update(); } // common menu processing, selecting channel and then changing values #define menu_adc_direction menu_tmp_flag // channel if from 0 static void menu_set_adc_direction(u8 channel) { u16 adc, calm; // for channel 2 use throttle, for others steering if (channel == 1) { adc = adc_throttle_ovs; calm = cg.calib_throttle_mid; } else { adc = adc_steering_ovs; calm = cg.calib_steering_mid; } // check steering firstly if (adc_steering_ovs < ((cg.calib_steering_mid - 40) << ADC_OVS_SHIFT)) menu_adc_direction = 0; else if (adc_steering_ovs > ((cg.calib_steering_mid + 40) << ADC_OVS_SHIFT)) menu_adc_direction = 1; // then check throttle if (adc_throttle_ovs < ((cg.calib_throttle_mid - 40) << ADC_OVS_SHIFT)) menu_adc_direction = 0; else if (adc_throttle_ovs > ((cg.calib_throttle_mid + 40) << ADC_OVS_SHIFT)) menu_adc_direction = 1; // and then CH3 button else if (btn(BTN_CH3)) menu_adc_direction ^= 1; // 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); } // channel if from 0 static void menu_set_adc_force(u8 channel, u8 use_adc, u8 force_values) { // check if force servos to positions (left/center/right) if ((u8)(force_values & (u8)(1 << channel))) { menu_force_value_channel = (u8)(channel + 1); menu_force_value = 0; // to center by default } else menu_force_value_channel = 0; // check use of ADC if ((u8)(use_adc & (u8)(1 << channel))) { menu_adc_wakeup = 1; menu_set_adc_direction(channel); } // don't use ADC else menu_adc_wakeup = 0; } typedef void (*menu_channel_subfunc_t)(u8, u8); typedef struct { u8 end_channel; u8 use_adc; u8 forced_values; menu_channel_subfunc_t func; u8 last_direction; } menu_channel_t; static void menu_channel_func(u8 action, menu_channel_t *p) { switch (action) { case MCA_INIT: menu_set_adc_force(menu_id, p->use_adc, p->forced_values); break; case MCA_SET_CHG: p->func(menu_id, 1); break; case MCA_ID_CHG: menu_id = (u8)menu_change_val(menu_id, 0, p->end_channel - 1, 1, 1); menu_set_adc_force(menu_id, p->use_adc, p->forced_values); break; case MCA_ADC_PRE: menu_set_adc_direction(menu_id); return; // show nothing at ADC_PRE case MCA_ADC_POST: // do nothing if left-right didn't changed if (p->last_direction == menu_adc_direction) return; // else flag it to show value menu_id_set = 1; // flag that new value is showed return; } // show value lcd_segment(LS_SYM_CHANNEL, LS_ON); if (action != MCA_SET_CHG) { // already showed lcd_7seg((u8)(menu_id + 1)); p->func(menu_id, 0); } if (menu_adc_wakeup) { // show arrow if (menu_adc_direction) lcd_segment(LS_SYM_RIGHT, LS_ON); else lcd_segment(LS_SYM_LEFT, LS_ON); } p->last_direction = menu_adc_direction; } static void menu_channel(u8 end_channel, u8 use_adc, u8 forced_values, menu_channel_subfunc_t subfunc) { menu_channel_t p; p.end_channel = end_channel; p.use_adc = use_adc; p.forced_values = forced_values; p.func = subfunc; menu_adc_direction = 0; menu_common(menu_channel_func, &p, MCF_NONE); config_model_save(); } // *************************** MODEL MENUS ******************************* // select model/save model as (to selected model position) #define MIN(a, b) (a < b ? a : b) static void menu_model_func(u8 action, u8 *model) { if (action == MCA_ID_CHG) *model = (u8)menu_change_val((s16)*model, 0, MIN(CONFIG_MODEL_MAX, 80) - 1, MODEL_FAST, 1); show_model_number(*model); lcd_chars(config_model_name(*model)); } static void menu_model(u8 saveas) { u8 model = cg.model; if (saveas) lcd_set_blink(LMENU, LB_SPC); menu_common(menu_model_func, &model, MCF_ENTER); // if new model choosed, save it if (model != cg.model) { config_set_model(model); 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_func(u8 action, void *p) { u8 letter; if (action == MCA_SET_CHG) { // change letter letter = cm.name[menu_set]; 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[menu_set] = letter; } else if (action == MCA_SET_NEXT) { // next char if (++menu_set > 2) menu_set = 0; } // show name menu_blink = (u8)(1 << menu_set); // blink only selected char show_model_number(cg.model); lcd_chars(cm.name); } static void menu_name(void) { menu_common(menu_name_func, NULL, MCF_SET_ONLY); config_model_save(); } // set number of model channels static void menu_channels(u8 action) { // change value if (action == MLA_CHG) cm.channels = (u8)(menu_change_val(cm.channels + 1, 2, MAX_CHANNELS, 1, 0) - 1); // show value lcd_7seg(L7_C); lcd_segment(LS_SYM_CHANNEL, LS_ON); lcd_char_num3(cm.channels + 1); } // reset model to defaults static void menu_reset_model(u8 action) { // change value if (action == MLA_CHG) menu_tmp_flag ^= 1; // select next value, reset when flag is set else if (action == MLA_NEXT) { if (menu_tmp_flag) { menu_tmp_flag = 0; config_model_set_default(); buzzer_on(60, 0, 1); } } // show value lcd_7seg(L7_R); lcd_chars(menu_tmp_flag ? "YES" : "NO "); } static const menu_list_t chanres_funcs[] = { menu_channels, menu_reset_model, }; // set number of model channels, reset model to default values static void menu_channels_reset(void) { menu_tmp_flag = 0; lcd_set_blink(LMENU, LB_SPC); menu_list(chanres_funcs, sizeof(chanres_funcs) / sizeof(void *), MCF_NONE); lcd_set_blink(LMENU, LB_OFF); 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(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_segment(LS_SYM_PERCENT, LS_ON); lcd_char_num3(*addr); } @inline static void menu_endpoint(void) { menu_channel(channels, 0xff, 0xfc, sf_endpoint); } // 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(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_segment(LS_SYM_PERCENT, LS_ON); lcd_char_num3(*addr); } @inline static void menu_dualrate(void) { menu_channel(2, 0x2, 0, sf_dualrate); } // set servo speed static void sf_speed(u8 channel, u8 change) { u8 *addr = &cm.speed[channel]; u8 thfwdonly = (u8)(channel == 1 && menu_adc_direction ? 1 : 0); if (channel == 0 && menu_adc_direction) addr = &cm.stspd_return; if (change) { if (thfwdonly) // throttle forward only setting cm.thspd_onlyfwd ^= 1; else *addr = (u8)menu_change_val(*addr, 1, 100, SPEED_FAST, 0); } if (thfwdonly) lcd_chars(cm.thspd_onlyfwd ? "OFF" : "ON "); else { lcd_char_num3(*addr); lcd_segment(LS_SYM_PERCENT, LS_ON); } } static void menu_speed(void) { lcd_set_blink(LMENU, LB_SPC); menu_channel(channels, 0x3, 0, sf_speed); 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_segment(LS_SYM_PERCENT, LS_ON); lcd_char_num3(*addr); } @inline static void menu_expo(void) { menu_channel(2, 0x2, 0, sf_expo); } // set channel value, exclude 4WS and DIG channels // for channels 3..8 so add 2 to channel number static void sf_channel_val(u8 channel, u8 change) { s8 *addr = &menu_channel3_8[channel]; if (change && !(menu_channels_mixed & (u8)(1 << (channel + 2)))) *addr = (s8)menu_change_val(*addr, -100, 100, CHANNEL_FAST, 0); // show value lcd_7seg((u8)(channel + 2 + 1)); lcd_char_num3(*addr); } static void menu_channel_value(void) { if (channels < 3) return; lcd_set_blink(LMENU, LB_SPC); menu_channel((u8)(channels - 2), 0, 0, sf_channel_val); lcd_set_blink(LMENU, LB_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) / 4) static void menu_abs_func(u8 action, void *p) { // change value if (action == MCA_SET_CHG) cm.abs_type = (u8)menu_change_val(cm.abs_type, 0, ABS_LABEL_SIZE-1, 1, 1); // show value lcd_segment(LS_SYM_CHANNEL, LS_ON); lcd_7seg(2); lcd_chars(abs_labels[cm.abs_type]); } static void menu_abs(void) { menu_common(menu_abs_func, NULL, (u8)(MCF_ENTER | MCF_SET_ONLY)); 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_channels_reset(); 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) { if (btnl(BTN_ENTER)) menu_channel_value(); else 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 u8 menu_main_screen; static void menu_loop(void) { menu_main_screen = MS_NAME; lcd_clear(); while (1) { if (!menu_check_keys) { main_screen(menu_main_screen); btnra(); menu_stop(); } else menu_check_keys = 0; // don't wanted in submenus, will be set back in main_screen() menu_adc_wakeup = 0; menu_timer_wakeup = 0; // Enter long key - global/calibrate/key-test if (btnl(BTN_ENTER)) { if (menu_main_screen >= MS_TIMER0) { key_beep(); menu_timer_lap_times((u8)(menu_main_screen - MS_TIMER0)); btnra(); } else if (adc_steering_ovs > (CALIB_ST_MID_HIGH << ADC_OVS_SHIFT)) menu_calibrate(0); 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(); if (menu_main_screen >= MS_TIMER0) menu_timer_setup((u8)(menu_main_screen - MS_TIMER0)); else 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)) menu_main_screen = (u8)menu_change_val(menu_main_screen, 0, MS_MAX - 1, 1, 1); } } // read config from eeprom, initialize and call menu loop void menu_init(void) { // variables menu_key_mapping_prepare(); // wait for some time to read ADC/buttons several times and get // stable values while (time_sec == 0 && time_5ms < 10) pause(); // read global config from eeprom, if calibrate values was set to defaults, // call calibrate if (config_global_read()) { menu_calibrate(1); btnra(); reset_inactivity_timer(); } else { u16 steering = ADC_OVS(steering); u16 throttle = ADC_OVS(throttle); apply_global_config(); reset_inactivity_timer(); // if actual steering/throttle value is not in dead zone, beep 3 times if (cg.poweron_warn && (steering < (cg.calib_steering_mid - cg.steering_dead_zone) || steering > (cg.calib_steering_mid + cg.steering_dead_zone) || throttle < (cg.calib_throttle_mid - cg.throttle_dead_zone) || throttle > (cg.calib_throttle_mid + cg.throttle_dead_zone))) buzzer_on(30, 30, 3); // else beep 1 times else if (cg.poweron_beep) beep(30); } // reset global timers menu_timer_clear(0, 1); menu_timer_clear(1, 1); // read model config from eeprom menu_load_model(); // and main loop menu_loop(); }