diff --git a/meson.build b/meson.build index 9b918715..f188d423 100644 --- a/meson.build +++ b/meson.build @@ -90,7 +90,9 @@ openrtx_ui_default = ['openrtx/src/ui/default/ui.c', 'openrtx/src/ui/default/ui_menu.c', 'openrtx/src/ui/default/ui_strings.c'] -openrtx_ui_module17 = [] +openrtx_ui_module17 = ['openrtx/src/ui/module17/ui.c', + 'openrtx/src/ui/module17/ui_main.c', + 'openrtx/src/ui/module17/ui_menu.c'] ## ## Selection of main entrypoint diff --git a/openrtx/include/ui/ui_mod17.h b/openrtx/include/ui/ui_mod17.h new file mode 100644 index 00000000..e5ce1e05 --- /dev/null +++ b/openrtx/include/ui/ui_mod17.h @@ -0,0 +1,212 @@ +/*************************************************************************** + * Copyright (C) 2022 by Federico Amedeo Izzo IU2NUO, * + * Niccolò Izzo IU2KIN, * + * Silvano Seva IU2KWO * + * * + * 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 * + ***************************************************************************/ + +#ifndef UI_MOD17_H +#define UI_MOD17_H + +#include +#include +#include +#include +#include +#include +#include +#include + +// Maximum menu entry length +#define MAX_ENTRY_LEN 21 +// Frequency digits +#define FREQ_DIGITS 7 +// Time & Date digits +#define TIMEDATE_DIGITS 10 +// Max number of UI events +#define MAX_NUM_EVENTS 16 + +enum uiScreen +{ + MAIN_VFO = 0, + MAIN_VFO_INPUT, + MAIN_MEM, + MODE_VFO, + MODE_MEM, + MENU_TOP, + MENU_BANK, + MENU_CHANNEL, + MENU_CONTACTS, + MENU_GPS, + MENU_SETTINGS, + MENU_BACKUP_RESTORE, + MENU_BACKUP, + MENU_RESTORE, + MENU_INFO, + MENU_ABOUT, + SETTINGS_TIMEDATE, + SETTINGS_TIMEDATE_SET, + SETTINGS_DISPLAY, + SETTINGS_GPS, + SETTINGS_M17, + SETTINGS_RESET2DEFAULTS, + LOW_BAT +}; + +enum SetRxTx +{ + SET_RX = 0, + SET_TX +}; + +// This enum is needed to have item numbers that match +// menu elements even if some elements may be missing (GPS) +enum menuItems +{ + M_SETTINGS = 0, +#ifdef GPS_PRESENT + M_GPS, +#endif + M_INFO, + M_ABOUT +}; + +enum settingsItems +{ + S_DISPLAY = 0 +#ifdef RTC_PRESENT + ,S_TIMEDATE +#endif +#ifdef GPS_PRESENT + ,S_GPS +#endif + ,S_M17 + ,S_RESET2DEFAULTS +}; + +enum backupRestoreItems +{ + BR_BACKUP = 0, + BR_RESTORE +}; + +enum displayItems +{ + D_BRIGHTNESS = 0 +#ifdef SCREEN_CONTRAST + ,D_CONTRAST +#endif + ,D_TIMER +}; + +#ifdef GPS_PRESENT +enum settingsGPSItems +{ + G_ENABLED = 0, + G_SET_TIME, + G_TIMEZONE +}; +#endif + +/** + * Struct containing a set of positions and sizes that get + * calculated for the selected display size. + * Using these parameters make the UI automatically adapt + * To displays of different sizes + */ +typedef struct layout_t +{ + uint16_t hline_h; + uint16_t top_h; + uint16_t line1_h; + uint16_t line2_h; + uint16_t line3_h; + uint16_t menu_h; + uint16_t bottom_h; + uint16_t bottom_pad; + uint16_t status_v_pad; + uint16_t horizontal_pad; + uint16_t text_v_offset; + point_t top_pos; + point_t line1_pos; + point_t line2_pos; + point_t line3_pos; + point_t bottom_pos; + fontSize_t top_font; + fontSize_t line1_font; + fontSize_t line2_font; + fontSize_t line3_font; + fontSize_t bottom_font; + fontSize_t input_font; + fontSize_t menu_font; + fontSize_t mode_font_big; + fontSize_t mode_font_small; +} layout_t; + +/** + * This structs contains state variables internal to the + * UI that need to be kept between executions of the UI + * This state does not need to be saved on device poweroff + */ +typedef struct ui_state_t +{ + // Index of the currently selected menu entry + uint8_t menu_selected; + // If true we can change a menu entry value with UP/DOWN + bool edit_mode; + // Variables used for VFO input + uint8_t input_number; + uint8_t input_position; + uint8_t input_set; + long long last_keypress; + freq_t new_rx_frequency; + freq_t new_tx_frequency; + char new_rx_freq_buf[14]; + char new_tx_freq_buf[14]; +#ifdef RTC_PRESENT + // Variables used for Time & Date input + datetime_t new_timedate; + char new_date_buf[9]; + char new_time_buf[9]; +#endif + char new_callsign[10]; + // Which state to return to when we exit menu + uint8_t last_main_state; +} +ui_state_t; + +extern layout_t layout; +// Copy of the radio state +extern state_t last_state; +extern const char *menu_items[]; +extern const char *settings_items[]; +extern const char *display_items[]; +extern const char *settings_gps_items[]; +extern const char *backup_restore_items[]; +extern const char *info_items[]; +extern const char *authors[]; +extern const uint8_t menu_num; +extern const uint8_t settings_num; +extern const uint8_t display_num; +extern const uint8_t settings_gps_num; +extern const uint8_t backup_restore_num; +extern const uint8_t info_num; +extern const uint8_t author_num; +extern const color_t color_black; +extern const color_t color_grey; +extern const color_t color_white; +extern const color_t yellow_fab413; + +#endif /* UI_MOD17_H */ diff --git a/openrtx/src/ui/module17/ui.c b/openrtx/src/ui/module17/ui.c new file mode 100644 index 00000000..82a31da0 --- /dev/null +++ b/openrtx/src/ui/module17/ui.c @@ -0,0 +1,1357 @@ +/*************************************************************************** + * Copyright (C) 2020 - 2022 by Federico Amedeo Izzo IU2NUO, * + * Niccolò Izzo IU2KIN * + * Frederik Saraci IU2NRO * + * Silvano Seva IU2KWO * + * * + * 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 +#include +#include +#include +#include +#include +#include +#ifdef GPS_PRESENT +#include +#endif +#include +#include +#include +#include +#include + +/* UI main screen functions, their implementation is in "ui_main.c" */ +extern void _ui_drawMainBackground(); +extern void _ui_drawMainTop(); +extern void _ui_drawVFOMiddle(); +extern void _ui_drawMEMMiddle(); +extern void _ui_drawVFOBottom(); +extern void _ui_drawMEMBottom(); +extern void _ui_drawMainVFO(ui_state_t* ui_state); +extern void _ui_drawMainVFOInput(ui_state_t* ui_state); +extern void _ui_drawMainMEM(ui_state_t* ui_state); +/* UI menu functions, their implementation is in "ui_menu.c" */ +extern void _ui_drawMenuTop(ui_state_t* ui_state); +#ifdef GPS_PRESENT +extern void _ui_drawMenuGPS(); +extern void _ui_drawSettingsGPS(ui_state_t* ui_state); +#endif +extern void _ui_drawMenuSettings(ui_state_t* ui_state); +extern void _ui_drawMenuInfo(ui_state_t* ui_state); +extern void _ui_drawMenuAbout(); +#ifdef RTC_PRESENT +extern void _ui_drawSettingsTimeDate(); +extern void _ui_drawSettingsTimeDateSet(ui_state_t* ui_state); +#endif +extern void _ui_drawSettingsDisplay(ui_state_t* ui_state); +extern void _ui_drawSettingsM17(ui_state_t* ui_state); +extern void _ui_drawSettingsReset2Defaults(ui_state_t* ui_state); +extern bool _ui_drawMacroMenu(); + +const char *menu_items[] = +{ + "Settings", +#ifdef GPS_PRESENT + "GPS", +#endif + "Info", + "About" +}; + +const char *settings_items[] = +{ + "Display", +#ifdef RTC_PRESENT + "Time & Date", +#endif +#ifdef GPS_PRESENT + "GPS", +#endif + "M17", + "Default Settings" +}; + +const char *display_items[] = +{ +#ifdef SCREEN_CONTRAST + "Contrast", +#endif + "Timer" +}; + +#ifdef GPS_PRESENT +const char *settings_gps_items[] = +{ + "GPS Enabled", + "GPS Set Time", + "UTC Timezone" +}; +#endif + +const char *info_items[] = +{ + "", + "Bat. Voltage", + "Bat. Charge", + "RSSI", + "Used heap", + "Band", + "VHF", + "UHF", + "LCD Type" +}; + +const char *authors[] = +{ + "Niccolo' IU2KIN", + "Silvano IU2KWO", + "Federico IU2NUO", + "Fred IU2NRO", +}; + +static const char *symbols_ITU_T_E161[] = +{ + " 0", + ",.?1", + "abc2ABC", + "def3DEF", + "ghi4GHI", + "jkl5JKL", + "mno6MNO", + "pqrs7PQRS", + "tuv8TUV", + "wxyz9WXYZ", + "-/*", + "#" +}; + +static const char *symbols_ITU_T_E161_callsign[] = +{ + "0 ", + "1", + "ABC2", + "DEF3", + "GHI4", + "JKL5", + "MNO6", + "PQRS7", + "TUV8", + "WXYZ9", + "-/", + "" +}; + +// Calculate number of menu entries +const uint8_t menu_num = sizeof(menu_items)/sizeof(menu_items[0]); +const uint8_t settings_num = sizeof(settings_items)/sizeof(settings_items[0]); +const uint8_t display_num = sizeof(display_items)/sizeof(display_items[0]); +#ifdef GPS_PRESENT +const uint8_t settings_gps_num = sizeof(settings_gps_items)/sizeof(settings_gps_items[0]); +#endif +const uint8_t info_num = sizeof(info_items)/sizeof(info_items[0]); +const uint8_t author_num = sizeof(authors)/sizeof(authors[0]); + +const color_t color_black = {0, 0, 0, 255}; +const color_t color_grey = {60, 60, 60, 255}; +const color_t color_white = {255, 255, 255, 255}; +const color_t yellow_fab413 = {250, 180, 19, 255}; + +layout_t layout; +state_t last_state; +static ui_state_t ui_state; +static bool layout_ready = false; +static bool redraw_needed = true; + +static bool standby = false; +static long long last_event_tick = 0; + +// UI event queue +static uint8_t evQueue_rdPos; +static uint8_t evQueue_wrPos; +static event_t evQueue[MAX_NUM_EVENTS]; + +layout_t _ui_calculateLayout() +{ + // Horizontal line height + const uint16_t hline_h = 1; + // Compensate for fonts printing below the start position + const uint16_t text_v_offset = 1; + + // Calculate UI layout depending on vertical resolution + // Tytera MD380, MD-UV380 + #if SCREEN_HEIGHT > 127 + + // Height and padding shown in diagram at beginning of file + const uint16_t top_h = 16; + const uint16_t top_pad = 4; + const uint16_t line1_h = 20; + const uint16_t line2_h = 20; + const uint16_t line3_h = 40; + const uint16_t menu_h = 16; + const uint16_t bottom_h = 23; + const uint16_t bottom_pad = top_pad; + const uint16_t status_v_pad = 2; + const uint16_t small_line_v_pad = 2; + const uint16_t big_line_v_pad = 6; + const uint16_t horizontal_pad = 4; + + // Top bar font: 8 pt + const fontSize_t top_font = FONT_SIZE_8PT; + // Text line font: 8 pt + const fontSize_t line1_font = FONT_SIZE_8PT; + const fontSize_t line2_font = FONT_SIZE_8PT; + // Frequency line font: 16 pt + const fontSize_t line3_font = FONT_SIZE_16PT; + // Bottom bar font: 8 pt + const fontSize_t bottom_font = FONT_SIZE_8PT; + // TimeDate/Frequency input font + const fontSize_t input_font = FONT_SIZE_12PT; + // Menu font + const fontSize_t menu_font = FONT_SIZE_8PT; + // Mode screen frequency font: 12 pt + const fontSize_t mode_font_big = FONT_SIZE_12PT; + // Mode screen details font: 9 pt + const fontSize_t mode_font_small = FONT_SIZE_9PT; + + // Radioddity GD-77 + #elif SCREEN_HEIGHT > 63 + + // Height and padding shown in diagram at beginning of file + const uint16_t top_h = 11; + const uint16_t top_pad = 1; + const uint16_t line1_h = 10; + const uint16_t line2_h = 10; + const uint16_t line3_h = 16; + const uint16_t menu_h = 10; + const uint16_t bottom_h = 15; + const uint16_t bottom_pad = 0; + const uint16_t status_v_pad = 1; + const uint16_t small_line_v_pad = 1; + const uint16_t big_line_v_pad = 0; + const uint16_t horizontal_pad = 4; + + // Top bar font: 6 pt + const fontSize_t top_font = FONT_SIZE_6PT; + // Middle line fonts: 5, 8, 8 pt + const fontSize_t line1_font = FONT_SIZE_6PT; + const fontSize_t line2_font = FONT_SIZE_6PT; + const fontSize_t line3_font = FONT_SIZE_10PT; + // Bottom bar font: 6 pt + const fontSize_t bottom_font = FONT_SIZE_6PT; + // TimeDate/Frequency input font + const fontSize_t input_font = FONT_SIZE_8PT; + // Menu font + const fontSize_t menu_font = FONT_SIZE_6PT; + // Mode screen frequency font: 9 pt + const fontSize_t mode_font_big = FONT_SIZE_9PT; + // Mode screen details font: 6 pt + const fontSize_t mode_font_small = FONT_SIZE_6PT; + + // Radioddity RD-5R + #elif SCREEN_HEIGHT > 47 + + // Height and padding shown in diagram at beginning of file + const uint16_t top_h = 11; + const uint16_t top_pad = 1; + const uint16_t line1_h = 0; + const uint16_t line2_h = 10; + const uint16_t line3_h = 18; + const uint16_t menu_h = 10; + const uint16_t bottom_h = 0; + const uint16_t bottom_pad = 0; + const uint16_t status_v_pad = 1; + const uint16_t small_line_v_pad = 1; + const uint16_t big_line_v_pad = 0; + const uint16_t horizontal_pad = 4; + + // Top bar font: 8 pt + const fontSize_t top_font = FONT_SIZE_6PT; + // Middle line fonts: 16, 16 + const fontSize_t line2_font = FONT_SIZE_6PT; + const fontSize_t line3_font = FONT_SIZE_12PT; + // TimeDate/Frequency input font + const fontSize_t input_font = FONT_SIZE_8PT; + // Menu font + const fontSize_t menu_font = FONT_SIZE_6PT; + // Mode screen frequency font: 9 pt + const fontSize_t mode_font_big = FONT_SIZE_9PT; + // Mode screen details font: 6 pt + const fontSize_t mode_font_small = FONT_SIZE_6PT; + // Not present on this resolution + const fontSize_t line1_font = 0; + const fontSize_t bottom_font = 0; + + #else + #error Unsupported vertical resolution! + #endif + + // Calculate printing positions + point_t top_pos = {horizontal_pad, top_h - status_v_pad - text_v_offset}; + point_t line1_pos = {horizontal_pad, top_h + top_pad + line1_h - small_line_v_pad - text_v_offset}; + point_t line2_pos = {horizontal_pad, top_h + top_pad + line1_h + line2_h - small_line_v_pad - text_v_offset}; + point_t line3_pos = {horizontal_pad, top_h + top_pad + line1_h + line2_h + line3_h - big_line_v_pad - text_v_offset}; + point_t bottom_pos = {horizontal_pad, SCREEN_HEIGHT - bottom_pad - status_v_pad - text_v_offset}; + + layout_t new_layout = + { + hline_h, + top_h, + line1_h, + line2_h, + line3_h, + menu_h, + bottom_h, + bottom_pad, + status_v_pad, + horizontal_pad, + text_v_offset, + top_pos, + line1_pos, + line2_pos, + line3_pos, + bottom_pos, + top_font, + line1_font, + line2_font, + line3_font, + bottom_font, + input_font, + menu_font, + mode_font_big, + mode_font_small + }; + return new_layout; +} + + +void ui_init() +{ + last_event_tick = getTick(); + redraw_needed = true; + layout = _ui_calculateLayout(); + layout_ready = true; + // Initialize struct ui_state to all zeroes + // This syntax is called compound literal + // https://stackoverflow.com/questions/6891720/initialize-reset-struct-to-zero-null + ui_state = (const struct ui_state_t){ 0 }; +} + +void ui_drawSplashScreen(bool centered) +{ + gfx_clearScreen(); + point_t splash_origin = {0,0}; + + if(centered) + splash_origin.y = SCREEN_HEIGHT / 2 - 6; + else + splash_origin.y = SCREEN_HEIGHT / 5; + gfx_print(splash_origin, FONT_SIZE_12PT, TEXT_ALIGN_CENTER, yellow_fab413, "O P N\nR T X"); +} + +freq_t _ui_freq_add_digit(freq_t freq, uint8_t pos, uint8_t number) +{ + freq_t coefficient = 100; + for(uint8_t i=0; i < FREQ_DIGITS - pos; i++) + { + coefficient *= 10; + } + return freq += number * coefficient; +} + +#ifdef RTC_PRESENT +void _ui_timedate_add_digit(datetime_t *timedate, uint8_t pos, uint8_t number) +{ + switch(pos) + { + // Set date + case 1: + timedate->date += number * 10; + break; + case 2: + timedate->date += number; + break; + // Set month + case 3: + timedate->month += number * 10; + break; + case 4: + timedate->month += number; + break; + // Set year + case 5: + timedate->year += number * 10; + break; + case 6: + timedate->year += number; + break; + // Set hour + case 7: + timedate->hour += number * 10; + break; + case 8: + timedate->hour += number; + break; + // Set minute + case 9: + timedate->minute += number * 10; + break; + case 10: + timedate->minute += number; + break; + } +} +#endif + +bool _ui_freq_check_limits(freq_t freq) +{ + bool valid = false; + const hwInfo_t* hwinfo = platform_getHwInfo(); + if(hwinfo->vhf_band) + { + // hwInfo_t frequencies are in MHz + if(freq >= (hwinfo->vhf_minFreq * 1000000) && + freq <= (hwinfo->vhf_maxFreq * 1000000)) + valid = true; + } + if(hwinfo->uhf_band) + { + // hwInfo_t frequencies are in MHz + if(freq >= (hwinfo->uhf_minFreq * 1000000) && + freq <= (hwinfo->uhf_maxFreq * 1000000)) + valid = true; + } + return valid; +} + +bool _ui_channel_valid(channel_t* channel) +{ +return _ui_freq_check_limits(channel->rx_frequency) && + _ui_freq_check_limits(channel->tx_frequency); +} + +int _ui_fsm_loadChannel(int16_t channel_index, bool *sync_rtx) { + channel_t channel; + int32_t selected_channel = channel_index; + // If a bank is active, get index from current bank + if(state.bank_enabled) + { + bankHdr_t bank = { 0 }; + cps_readBankHeader(&bank, state.bank); + if((channel_index < 0) || (channel_index >= bank.ch_count)) + return -1; + channel_index = cps_readBankData(state.bank, channel_index); + } + int result = cps_readChannel(&channel, channel_index); + // Read successful and channel is valid + if(result != -1 && _ui_channel_valid(&channel)) + { + // Set new channel index + state.channel_index = selected_channel; + // Copy channel read to state + state.channel = channel; + *sync_rtx = true; + } + return result; +} + +void _ui_fsm_confirmVFOInput(bool *sync_rtx) +{ + // Switch to TX input + if(ui_state.input_set == SET_RX) + { + ui_state.input_set = SET_TX; + // Reset input position + ui_state.input_position = 0; + } + else if(ui_state.input_set == SET_TX) + { + // Save new frequency setting + // If TX frequency was not set, TX = RX + if(ui_state.new_tx_frequency == 0) + { + ui_state.new_tx_frequency = ui_state.new_rx_frequency; + } + // Apply new frequencies if they are valid + if(_ui_freq_check_limits(ui_state.new_rx_frequency) && + _ui_freq_check_limits(ui_state.new_tx_frequency)) + { + state.channel.rx_frequency = ui_state.new_rx_frequency; + state.channel.tx_frequency = ui_state.new_tx_frequency; + *sync_rtx = true; + } + state.ui_screen = MAIN_VFO; + } +} + +void _ui_fsm_insertVFONumber(kbd_msg_t msg, bool *sync_rtx) +{ + // Advance input position + ui_state.input_position += 1; + // Save pressed number to calculate frequency and show in GUI + ui_state.input_number = input_getPressedNumber(msg); + if(ui_state.input_set == SET_RX) + { + if(ui_state.input_position == 1) + ui_state.new_rx_frequency = 0; + // Calculate portion of the new RX frequency + ui_state.new_rx_frequency = _ui_freq_add_digit(ui_state.new_rx_frequency, + ui_state.input_position, ui_state.input_number); + if(ui_state.input_position >= FREQ_DIGITS) + { + // Switch to TX input + ui_state.input_set = SET_TX; + // Reset input position + ui_state.input_position = 0; + // Reset TX frequency + ui_state.new_tx_frequency = 0; + } + } + else if(ui_state.input_set == SET_TX) + { + if(ui_state.input_position == 1) + ui_state.new_tx_frequency = 0; + // Calculate portion of the new TX frequency + ui_state.new_tx_frequency = _ui_freq_add_digit(ui_state.new_tx_frequency, + ui_state.input_position, ui_state.input_number); + if(ui_state.input_position >= FREQ_DIGITS) + { + // Save both inserted frequencies + if(_ui_freq_check_limits(ui_state.new_rx_frequency) && + _ui_freq_check_limits(ui_state.new_tx_frequency)) + { + state.channel.rx_frequency = ui_state.new_rx_frequency; + state.channel.tx_frequency = ui_state.new_tx_frequency; + *sync_rtx = true; + } + state.ui_screen = MAIN_VFO; + } + } +} + +void _ui_changeContrast(int variation) +{ + if(variation >= 0) + state.settings.contrast = + (255 - state.settings.contrast < variation) ? 255 : state.settings.contrast + variation; + else + state.settings.contrast = + (state.settings.contrast < -variation) ? 0 : state.settings.contrast + variation; + display_setContrast(state.settings.contrast); +} + +void _ui_changeTimer(int variation) +{ + if ((state.settings.display_timer == TIMER_OFF && variation < 0) || + (state.settings.display_timer == TIMER_1H && variation > 0)) + { + return; + } + + state.settings.display_timer += variation; +} + +bool _ui_checkStandby(long long time_since_last_event) +{ + if (standby) + { + return false; + } + + switch (state.settings.display_timer) + { + case TIMER_OFF: + return false; + case TIMER_5S: + case TIMER_10S: + case TIMER_15S: + case TIMER_20S: + case TIMER_25S: + case TIMER_30S: + return time_since_last_event >= + (5000 * state.settings.display_timer); + case TIMER_1M: + case TIMER_2M: + case TIMER_3M: + case TIMER_4M: + case TIMER_5M: + return time_since_last_event >= + (60000 * (state.settings.display_timer - (TIMER_1M - 1))); + case TIMER_15M: + case TIMER_30M: + case TIMER_45M: + return time_since_last_event >= + (60000 * 15 * (state.settings.display_timer - (TIMER_15M - 1))); + case TIMER_1H: + return time_since_last_event >= 60 * 60 * 1000; + } + + // unreachable code + return false; +} + +void _ui_enterStandby() +{ + if(standby) + return; + + standby = true; + redraw_needed = false; + platform_setBacklightLevel(0); +} + +bool _ui_exitStandby(long long now) +{ + last_event_tick = now; + + if(!standby) + return false; + + standby = false; + redraw_needed = true; + platform_setBacklightLevel(state.settings.brightness); + return true; +} + +void _ui_menuUp(uint8_t menu_entries) +{ + if(ui_state.menu_selected > 0) + ui_state.menu_selected -= 1; + else + ui_state.menu_selected = menu_entries - 1; +} + +void _ui_menuDown(uint8_t menu_entries) +{ + if(ui_state.menu_selected < menu_entries - 1) + ui_state.menu_selected += 1; + else + ui_state.menu_selected = 0; +} + +void _ui_menuBack(uint8_t prev_state) +{ + if(ui_state.edit_mode) + { + ui_state.edit_mode = false; + } + else + { + // Return to previous menu + state.ui_screen = prev_state; + // Reset menu selection + ui_state.menu_selected = 0; + } +} + +void _ui_textInputReset(char *buf) +{ + ui_state.input_number = 0; + ui_state.input_position = 0; + ui_state.input_set = 0; + ui_state.last_keypress = 0; + memset(buf, 0, 9); + buf[0] = '_'; +} + +void _ui_textInputKeypad(char *buf, uint8_t max_len, kbd_msg_t msg, bool callsign) +{ + if(ui_state.input_position >= max_len) + return; + long long now = getTick(); + // Get currently pressed number key + uint8_t num_key = input_getPressedNumber(msg); + // Get number of symbols related to currently pressed key + uint8_t num_symbols = 0; + if(callsign) + num_symbols = strlen(symbols_ITU_T_E161_callsign[num_key]); + else + num_symbols = strlen(symbols_ITU_T_E161[num_key]); + + // Skip keypad logic for first keypress + if(ui_state.last_keypress != 0) + { + // Same key pressed and timeout not expired: cycle over chars of current key + if((ui_state.input_number == num_key) && ((now - ui_state.last_keypress) < input_longPressTimeout)) + { + ui_state.input_set = (ui_state.input_set + 1) % num_symbols; + } + // Differnt key pressed: save current char and change key + else + { + ui_state.input_position += 1; + ui_state.input_set = 0; + } + } + // Show current character on buffer + if(callsign) + buf[ui_state.input_position] = symbols_ITU_T_E161_callsign[num_key][ui_state.input_set]; + else + buf[ui_state.input_position] = symbols_ITU_T_E161[num_key][ui_state.input_set]; + // Update reference values + ui_state.input_number = num_key; + ui_state.last_keypress = now; +} + +void _ui_textInputConfirm(char *buf) +{ + buf[ui_state.input_position + 1] = '\0'; +} + +void _ui_textInputDel(char *buf) +{ + buf[ui_state.input_position] = '\0'; + // Move back input cursor + if(ui_state.input_position > 0) + ui_state.input_position--; + // If we deleted the initial character, reset starting condition + else + ui_state.last_keypress = 0; + ui_state.input_set = 0; +} + +void ui_saveState() +{ + last_state = state; +} + +void ui_updateFSM(bool *sync_rtx) +{ + // Check for events + if(evQueue_wrPos == evQueue_rdPos) return; + + // Pop an event from the queue + uint8_t newTail = (evQueue_rdPos + 1) % MAX_NUM_EVENTS; + event_t event = evQueue[evQueue_rdPos]; + evQueue_rdPos = newTail; + + // There is some event to process, we need an UI redraw. + // UI redraw request is cancelled if we're in standby mode. + redraw_needed = true; + if(standby) redraw_needed = false; + + long long now = getTick(); + // Process pressed keys + if(event.type == EVENT_KBD) + { + kbd_msg_t msg; + msg.value = event.payload; + + // If we get out of standby, we ignore the kdb event + // unless is the MONI key for the MACRO functions + if (_ui_exitStandby(now) && !(msg.keys & KEY_MONI)) + return; + + switch(state.ui_screen) + { + // VFO screen + case MAIN_VFO: + // M17 Destination callsign input + if(ui_state.edit_mode) + { + if(state.channel.mode == OPMODE_M17) + { + if(msg.keys & KEY_ENTER) + { + _ui_textInputConfirm(ui_state.new_callsign); + // Save selected dst ID and disable input mode + strncpy(state.m17_data.dst_addr, ui_state.new_callsign, 10); + ui_state.edit_mode = false; + *sync_rtx = true; + } + else if(msg.keys & KEY_HASH) + { + // Save selected dst ID and disable input mode + strncpy(state.m17_data.dst_addr, "", 1); + ui_state.edit_mode = false; + *sync_rtx = true; + } + else if(msg.keys & KEY_ESC) + // Discard selected dst ID and disable input mode + ui_state.edit_mode = false; + else if(msg.keys & KEY_UP || msg.keys & KEY_DOWN || + msg.keys & KEY_LEFT || msg.keys & KEY_RIGHT) + _ui_textInputDel(ui_state.new_callsign); + else if(input_isNumberPressed(msg)) + _ui_textInputKeypad(ui_state.new_callsign, 9, msg, true); + break; + } + } + else + { + if(msg.keys & KEY_ENTER) + { + // Save current main state + ui_state.last_main_state = state.ui_screen; + // Open Menu + state.ui_screen = MENU_TOP; + } + else if(msg.keys & KEY_HASH) + { + // Enable dst ID input + ui_state.edit_mode = true; + // Reset text input variables + _ui_textInputReset(ui_state.new_callsign); + } + else if(msg.keys & KEY_UP || msg.keys & KNOB_RIGHT) + { + // Increment TX and RX frequency of 12.5KHz + if(_ui_freq_check_limits(state.channel.rx_frequency + 12500) && + _ui_freq_check_limits(state.channel.tx_frequency + 12500)) + { + state.channel.rx_frequency += 12500; + state.channel.tx_frequency += 12500; + *sync_rtx = true; + } + } + else if(msg.keys & KEY_DOWN || msg.keys & KNOB_LEFT) + { + // Decrement TX and RX frequency of 12.5KHz + if(_ui_freq_check_limits(state.channel.rx_frequency - 12500) && + _ui_freq_check_limits(state.channel.tx_frequency - 12500)) + { + state.channel.rx_frequency -= 12500; + state.channel.tx_frequency -= 12500; + *sync_rtx = true; + } + } + else if(msg.keys & KEY_ENTER) + { + // Save current main state + ui_state.last_main_state = state.ui_screen; + // Open Menu + state.ui_screen = MENU_TOP; + } + else if(msg.keys & KEY_ESC) + { + // Save VFO channel + state.vfo_channel = state.channel; + int result = _ui_fsm_loadChannel(state.channel_index, sync_rtx); + // Read successful and channel is valid + if(result != -1) + { + // Switch to MEM screen + state.ui_screen = MAIN_MEM; + } + } + else if(input_isNumberPressed(msg)) + { + // Open Frequency input screen + state.ui_screen = MAIN_VFO_INPUT; + // Reset input position and selection + ui_state.input_position = 1; + ui_state.input_set = SET_RX; + ui_state.new_rx_frequency = 0; + ui_state.new_tx_frequency = 0; + // Save pressed number to calculare frequency and show in GUI + ui_state.input_number = input_getPressedNumber(msg); + // Calculate portion of the new frequency + ui_state.new_rx_frequency = _ui_freq_add_digit(ui_state.new_rx_frequency, + ui_state.input_position, ui_state.input_number); + } + } + break; + // VFO frequency input screen + case MAIN_VFO_INPUT: + if(msg.keys & KEY_ENTER) + { + _ui_fsm_confirmVFOInput(sync_rtx); + } + else if(msg.keys & KEY_ESC) + { + // Cancel frequency input, return to VFO mode + state.ui_screen = MAIN_VFO; + } + else if(msg.keys & KEY_UP || msg.keys & KEY_DOWN) + { + if(ui_state.input_set == SET_RX) + ui_state.input_set = SET_TX; + else if(ui_state.input_set == SET_TX) + ui_state.input_set = SET_RX; + // Reset input position + ui_state.input_position = 0; + } + else if(input_isNumberPressed(msg)) + { + _ui_fsm_insertVFONumber(msg, sync_rtx); + } + break; + // MEM screen + case MAIN_MEM: + // M17 Destination callsign input + if(ui_state.edit_mode) + { + if(state.channel.mode == OPMODE_M17) + { + if(msg.keys & KEY_ENTER) + { + _ui_textInputConfirm(ui_state.new_callsign); + // Save selected dst ID and disable input mode + strncpy(state.m17_data.dst_addr, ui_state.new_callsign, 10); + ui_state.edit_mode = false; + *sync_rtx = true; + } + else if(msg.keys & KEY_HASH) + { + // Save selected dst ID and disable input mode + strncpy(state.m17_data.dst_addr, "", 1); + ui_state.edit_mode = false; + *sync_rtx = true; + } + else if(msg.keys & KEY_ESC) + // Discard selected dst ID and disable input mode + ui_state.edit_mode = false; + else if(msg.keys & KEY_UP || msg.keys & KEY_DOWN || + msg.keys & KEY_LEFT || msg.keys & KEY_RIGHT) + _ui_textInputDel(ui_state.new_callsign); + else if(input_isNumberPressed(msg)) + _ui_textInputKeypad(ui_state.new_callsign, 9, msg, true); + break; + } + } + else + { + if(msg.keys & KEY_ENTER) + { + // Save current main state + ui_state.last_main_state = state.ui_screen; + // Open Menu + state.ui_screen = MENU_TOP; + } + else if(msg.keys & KEY_ESC) + { + // Restore VFO channel + state.channel = state.vfo_channel; + // Update RTX configuration + *sync_rtx = true; + // Switch to VFO screen + state.ui_screen = MAIN_VFO; + } + else if(msg.keys & KEY_HASH) + { + // Enable dst ID input + ui_state.edit_mode = true; + // Reset text input variables + _ui_textInputReset(ui_state.new_callsign); + } + else if(msg.keys & KEY_UP || msg.keys & KNOB_RIGHT) + { + _ui_fsm_loadChannel(state.channel_index + 1, sync_rtx); + } + else if(msg.keys & KEY_DOWN || msg.keys & KNOB_LEFT) + { + _ui_fsm_loadChannel(state.channel_index - 1, sync_rtx); + } + } + break; + // Top menu screen + case MENU_TOP: + if(msg.keys & KEY_UP || msg.keys & KNOB_LEFT) + _ui_menuUp(menu_num); + else if(msg.keys & KEY_DOWN || msg.keys & KNOB_RIGHT) + _ui_menuDown(menu_num); + else if(msg.keys & KEY_ENTER) + { + switch(ui_state.menu_selected) + { +#ifdef GPS_PRESENT + case M_GPS: + state.ui_screen = MENU_GPS; + break; +#endif + case M_SETTINGS: + state.ui_screen = MENU_SETTINGS; + break; + case M_INFO: + state.ui_screen = MENU_INFO; + break; + case M_ABOUT: + state.ui_screen = MENU_ABOUT; + break; + } + // Reset menu selection + ui_state.menu_selected = 0; + } + else if(msg.keys & KEY_ESC) + _ui_menuBack(ui_state.last_main_state); + break; +#ifdef GPS_PRESENT + // GPS menu screen + case MENU_GPS: + if(msg.keys & KEY_ESC) + _ui_menuBack(MENU_TOP); + break; +#endif + // Settings menu screen + case MENU_SETTINGS: + if(msg.keys & KEY_UP || msg.keys & KNOB_LEFT) + _ui_menuUp(settings_num); + else if(msg.keys & KEY_DOWN || msg.keys & KNOB_RIGHT) + _ui_menuDown(settings_num); + else if(msg.keys & KEY_ENTER) + { + + switch(ui_state.menu_selected) + { + case S_DISPLAY: + state.ui_screen = SETTINGS_DISPLAY; + break; +#ifdef RTC_PRESENT + case S_TIMEDATE: + state.ui_screen = SETTINGS_TIMEDATE; + break; +#endif +#ifdef GPS_PRESENT + case S_GPS: + state.ui_screen = SETTINGS_GPS; + break; +#endif + case S_M17: + state.ui_screen = SETTINGS_M17; + break; + case S_RESET2DEFAULTS: + state.ui_screen = SETTINGS_RESET2DEFAULTS; + break; + default: + state.ui_screen = MENU_SETTINGS; + } + // Reset menu selection + ui_state.menu_selected = 0; + } + else if(msg.keys & KEY_ESC) + _ui_menuBack(MENU_TOP); + break; + // Info menu screen + case MENU_INFO: + if(msg.keys & KEY_UP || msg.keys & KNOB_LEFT) + _ui_menuUp(info_num); + else if(msg.keys & KEY_DOWN || msg.keys & KNOB_RIGHT) + _ui_menuDown(info_num); + else if(msg.keys & KEY_ESC) + _ui_menuBack(MENU_TOP); + break; + // About screen + case MENU_ABOUT: + if(msg.keys & KEY_ESC) + _ui_menuBack(MENU_TOP); + break; +#ifdef RTC_PRESENT + // Time&Date settings screen + case SETTINGS_TIMEDATE: + if(msg.keys & KEY_ENTER) + { + // Switch to set Time&Date mode + state.ui_screen = SETTINGS_TIMEDATE_SET; + // Reset input position and selection + ui_state.input_position = 0; + memset(&ui_state.new_timedate, 0, sizeof(datetime_t)); + } + else if(msg.keys & KEY_ESC) + _ui_menuBack(MENU_SETTINGS); + break; + // Time&Date settings screen, edit mode + case SETTINGS_TIMEDATE_SET: + if(msg.keys & KEY_ENTER) + { + // Save time only if all digits have been inserted + if(ui_state.input_position < TIMEDATE_DIGITS) + break; + // Return to Time&Date menu, saving values + // NOTE: The user inserted a local time, we must save an UTC time + datetime_t utc_time = localTimeToUtc(ui_state.new_timedate, + state.settings.utc_timezone); + rtc_setTime(utc_time); + state.time = utc_time; + state.ui_screen = SETTINGS_TIMEDATE; + } + else if(msg.keys & KEY_ESC) + _ui_menuBack(SETTINGS_TIMEDATE); + else if(input_isNumberPressed(msg)) + { + // Discard excess digits + if(ui_state.input_position > TIMEDATE_DIGITS) + break; + ui_state.input_position += 1; + ui_state.input_number = input_getPressedNumber(msg); + _ui_timedate_add_digit(&ui_state.new_timedate, ui_state.input_position, + ui_state.input_number); + } + break; +#endif + case SETTINGS_DISPLAY: + if(msg.keys & KEY_LEFT || (ui_state.edit_mode && + (msg.keys & KEY_DOWN || msg.keys & KNOB_LEFT))) + { + switch(ui_state.menu_selected) + { +#ifdef SCREEN_CONTRAST + case D_CONTRAST: + _ui_changeContrast(-4); + break; +#endif + case D_TIMER: + _ui_changeTimer(-1); + break; + default: + state.ui_screen = SETTINGS_DISPLAY; + } + } + else if(msg.keys & KEY_RIGHT || (ui_state.edit_mode && + (msg.keys & KEY_UP || msg.keys & KNOB_RIGHT))) + { + switch(ui_state.menu_selected) + { +#ifdef SCREEN_CONTRAST + case D_CONTRAST: + _ui_changeContrast(+4); + break; +#endif + case D_TIMER: + _ui_changeTimer(+1); + break; + default: + state.ui_screen = SETTINGS_DISPLAY; + } + } + else if(msg.keys & KEY_UP || msg.keys & KNOB_LEFT) + _ui_menuUp(display_num); + else if(msg.keys & KEY_DOWN || msg.keys & KNOB_RIGHT) + _ui_menuDown(display_num); + else if(msg.keys & KEY_ENTER) + ui_state.edit_mode = !ui_state.edit_mode; + else if(msg.keys & KEY_ESC) + _ui_menuBack(MENU_SETTINGS); + break; +#ifdef GPS_PRESENT + case SETTINGS_GPS: + if(msg.keys & KEY_LEFT || msg.keys & KEY_RIGHT || + (ui_state.edit_mode && + (msg.keys & KEY_DOWN || msg.keys & KNOB_LEFT || + msg.keys & KEY_UP || msg.keys & KNOB_RIGHT))) + { + switch(ui_state.menu_selected) + { + case G_ENABLED: + if(state.settings.gps_enabled) + state.settings.gps_enabled = 0; + else + state.settings.gps_enabled = 1; + break; + case G_SET_TIME: + state.gps_set_time = !state.gps_set_time; + break; + case G_TIMEZONE: + if(msg.keys & KEY_LEFT || msg.keys & KEY_UP || + msg.keys & KNOB_LEFT) + state.settings.utc_timezone -= 1; + else if(msg.keys & KEY_RIGHT || msg.keys & KEY_DOWN || + msg.keys & KNOB_RIGHT) + state.settings.utc_timezone += 1; + break; + default: + state.ui_screen = SETTINGS_GPS; + } + } + else if(msg.keys & KEY_UP || msg.keys & KNOB_LEFT) + _ui_menuUp(settings_gps_num); + else if(msg.keys & KEY_DOWN || msg.keys & KNOB_RIGHT) + _ui_menuDown(settings_gps_num); + else if(msg.keys & KEY_ENTER) + ui_state.edit_mode = !ui_state.edit_mode; + else if(msg.keys & KEY_ESC) + _ui_menuBack(MENU_SETTINGS); + break; +#endif + // M17 Settings + case SETTINGS_M17: + if(ui_state.edit_mode) + { + if(msg.keys & KEY_ENTER) + { + _ui_textInputConfirm(ui_state.new_callsign); + // Save selected callsign and disable input mode + strncpy(state.settings.callsign, ui_state.new_callsign, 10); + ui_state.edit_mode = false; + *sync_rtx = true; + } + else if(msg.keys & KEY_ESC) + // Discard selected callsign and disable input mode + ui_state.edit_mode = false; + else if(msg.keys & KEY_UP || msg.keys & KEY_DOWN || + msg.keys & KEY_LEFT || msg.keys & KEY_RIGHT) + _ui_textInputDel(ui_state.new_callsign); + else if(input_isNumberPressed(msg)) + _ui_textInputKeypad(ui_state.new_callsign, 9, msg, true); + } + else + { + if(msg.keys & KEY_ENTER) + { + // Enable callsign input + ui_state.edit_mode = true; + // Reset text input variables + _ui_textInputReset(ui_state.new_callsign); + } + else if(msg.keys & KEY_ESC) + _ui_menuBack(MENU_SETTINGS); + } + break; + case SETTINGS_RESET2DEFAULTS: + if(! ui_state.edit_mode){ + //require a confirmation ENTER, then another + //edit_mode is slightly misused to allow for this + if(msg.keys & KEY_ENTER) + { + ui_state.edit_mode = true; + } + else if(msg.keys & KEY_ESC) + { + _ui_menuBack(MENU_SETTINGS); + } + } else { + if(msg.keys & KEY_ENTER) + { + ui_state.edit_mode = false; + state_resetSettingsAndVfo(); + _ui_menuBack(MENU_SETTINGS); + } + else if(msg.keys & KEY_ESC) + { + ui_state.edit_mode = false; + _ui_menuBack(MENU_SETTINGS); + } + } + break; + } + } + else if(event.type == EVENT_STATUS) + { + if (platform_getPttStatus() || rtx_rxSquelchOpen()) + { + _ui_exitStandby(now); + return; + } + + if (_ui_checkStandby(now - last_event_tick)) + { + _ui_enterStandby(); + } + } +} + +bool ui_updateGUI() +{ + if(redraw_needed == false) + return false; + + if(!layout_ready) + { + layout = _ui_calculateLayout(); + layout_ready = true; + } + // Draw current GUI page + switch(last_state.ui_screen) + { + // VFO main screen + case MAIN_VFO: + _ui_drawMainVFO(&ui_state); + break; + // VFO frequency input screen + case MAIN_VFO_INPUT: + _ui_drawMainVFOInput(&ui_state); + break; + // MEM main screen + case MAIN_MEM: + _ui_drawMainMEM(&ui_state); + break; + // Top menu screen + case MENU_TOP: + _ui_drawMenuTop(&ui_state); + break; +#ifdef GPS_PRESENT + // GPS menu screen + case MENU_GPS: + _ui_drawMenuGPS(); + break; +#endif + // Settings menu screen + case MENU_SETTINGS: + _ui_drawMenuSettings(&ui_state); + break; + // Info menu screen + case MENU_INFO: + _ui_drawMenuInfo(&ui_state); + break; + // About menu screen + case MENU_ABOUT: + _ui_drawMenuAbout(); + break; +#ifdef RTC_PRESENT + // Time&Date settings screen + case SETTINGS_TIMEDATE: + _ui_drawSettingsTimeDate(); + break; + // Time&Date settings screen, edit mode + case SETTINGS_TIMEDATE_SET: + _ui_drawSettingsTimeDateSet(&ui_state); + break; +#endif + // Display settings screen + case SETTINGS_DISPLAY: + _ui_drawSettingsDisplay(&ui_state); + break; +#ifdef GPS_PRESENT + // GPS settings screen + case SETTINGS_GPS: + _ui_drawSettingsGPS(&ui_state); + break; +#endif + // M17 settings screen + case SETTINGS_M17: + _ui_drawSettingsM17(&ui_state); + break; + // Screen to support resetting Settings and VFO to defaults + case SETTINGS_RESET2DEFAULTS: + _ui_drawSettingsReset2Defaults(&ui_state); + break; + } + + redraw_needed = false; + return true; +} + +bool ui_pushEvent(const uint8_t type, const uint32_t data) +{ + uint8_t newHead = (evQueue_wrPos + 1) % MAX_NUM_EVENTS; + + // Queue is full + if(newHead == evQueue_rdPos) return false; + + // Preserve atomicity when writing the new element into the queue. + event_t event; + event.type = type; + event.payload = data; + + evQueue[evQueue_wrPos] = event; + evQueue_wrPos = newHead; + + return true; +} + +void ui_terminate() +{ +} diff --git a/openrtx/src/ui/module17/ui_main.c b/openrtx/src/ui/module17/ui_main.c new file mode 100644 index 00000000..6a2ca4b4 --- /dev/null +++ b/openrtx/src/ui/module17/ui_main.c @@ -0,0 +1,271 @@ +/*************************************************************************** + * Copyright (C) 2020 - 2022 by Federico Amedeo Izzo IU2NUO, * + * Niccolò Izzo IU2KIN * + * Frederik Saraci IU2NRO * + * Silvano Seva IU2KWO * + * * + * 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 +#include +#include +#include +#include + +void _ui_drawMainBackground() +{ + // Print top bar line of hline_h pixel height + gfx_drawHLine(layout.top_h, layout.hline_h, color_grey); + // Print bottom bar line of 1 pixel height + gfx_drawHLine(SCREEN_HEIGHT - layout.bottom_h - 1, layout.hline_h, color_grey); +} + +void _ui_drawMainTop() +{ +#ifdef RTC_PRESENT + // Print clock on top bar + datetime_t local_time = utcToLocalTime(last_state.time, + last_state.settings.utc_timezone); + gfx_print(layout.top_pos, layout.top_font, TEXT_ALIGN_CENTER, + color_white, "%02d:%02d:%02d", local_time.hour, + local_time.minute, local_time.second); +#endif + // If the radio has no built-in battery, print input voltage +#ifdef BAT_NONE + gfx_print(layout.top_pos, layout.top_font, TEXT_ALIGN_RIGHT, + color_white,"%.1fV", last_state.v_bat); +#else + // Otherwise print battery icon on top bar, use 4 px padding + uint16_t bat_width = SCREEN_WIDTH / 9; + uint16_t bat_height = layout.top_h - (layout.status_v_pad * 2); + point_t bat_pos = {SCREEN_WIDTH - bat_width - layout.horizontal_pad, + layout.status_v_pad}; + gfx_drawBattery(bat_pos, bat_width, bat_height, last_state.charge); +#endif + // Print radio mode on top bar + switch(last_state.channel.mode) + { + case OPMODE_FM: + gfx_print(layout.top_pos, layout.top_font, TEXT_ALIGN_LEFT, + color_white, "FM"); + break; + case OPMODE_DMR: + gfx_print(layout.top_pos, layout.top_font, TEXT_ALIGN_LEFT, + color_white, "DMR"); + break; + case OPMODE_M17: + gfx_print(layout.top_pos, layout.top_font, TEXT_ALIGN_LEFT, + color_white, "M17"); + break; + } +} + +void _ui_drawBankChannel() +{ + // Print Bank number, channel number and Channel name + uint16_t b = (last_state.bank_enabled) ? last_state.bank : 0; + gfx_print(layout.line1_pos, layout.line1_font, TEXT_ALIGN_CENTER, + color_white, "%01d-%03d: %.12s", + b, last_state.channel_index + 1, last_state.channel.name); +} + +void _ui_drawModeInfo(ui_state_t* ui_state) +{ + char bw_str[8] = { 0 }; + char encdec_str[9] = { 0 }; + + rtxStatus_t cfg = rtx_getCurrentStatus(); + + switch(last_state.channel.mode) + { + case OPMODE_FM: + // Get Bandwidth string + if(last_state.channel.bandwidth == BW_12_5) + snprintf(bw_str, 8, "12.5"); + else if(last_state.channel.bandwidth == BW_20) + snprintf(bw_str, 8, "20"); + else if(last_state.channel.bandwidth == BW_25) + snprintf(bw_str, 8, "25"); + // Get encdec string + bool tone_tx_enable = last_state.channel.fm.txToneEn; + bool tone_rx_enable = last_state.channel.fm.rxToneEn; + if (tone_tx_enable && tone_rx_enable) + snprintf(encdec_str, 9, "E+D"); + else if (tone_tx_enable && !tone_rx_enable) + snprintf(encdec_str, 9, "E"); + else if (!tone_tx_enable && tone_rx_enable) + snprintf(encdec_str, 9, "D"); + else + snprintf(encdec_str, 9, " "); + + // Print Bandwidth, Tone and encdec info + gfx_print(layout.line2_pos, layout.line2_font, TEXT_ALIGN_CENTER, + color_white, "B:%s T:%4.1f S:%s", + bw_str, ctcss_tone[last_state.channel.fm.txTone]/10.0f, + encdec_str); + break; + case OPMODE_DMR: + // Print talkgroup + gfx_print(layout.line2_pos, layout.line2_font, TEXT_ALIGN_CENTER, + color_white, "TG:%s", + ""); + break; + case OPMODE_M17: + { + // Print M17 Destination ID on line 3 of 3 + char *dst = NULL; + if(ui_state->edit_mode) + dst = ui_state->new_callsign; + else + dst = (!strnlen(cfg.destination_address, 10)) ? + "Broadcast" : cfg.destination_address; + gfx_print(layout.line2_pos, layout.line2_font, TEXT_ALIGN_CENTER, + color_white, "#%s", dst); + break; + } + } +} + +void _ui_drawFrequency() +{ + unsigned long frequency = platform_getPttStatus() ? + frequency = last_state.channel.tx_frequency : last_state.channel.rx_frequency; + + // Print big numbers frequency + gfx_print(layout.line3_pos, layout.line3_font, TEXT_ALIGN_CENTER, + color_white, "%03lu.%05lu", + (unsigned long)frequency/1000000, + (unsigned long)frequency%1000000/10); +} + +void _ui_drawVFOMiddleInput(ui_state_t* ui_state) +{ + // Add inserted number to string, skipping "Rx: "/"Tx: " and "." + uint8_t insert_pos = ui_state->input_position + 3; + if(ui_state->input_position > 3) insert_pos += 1; + char input_char = ui_state->input_number + '0'; + + if(ui_state->input_set == SET_RX) + { + if(ui_state->input_position == 0) + { + gfx_print(layout.line2_pos, layout.input_font, TEXT_ALIGN_CENTER, + color_white, ">Rx:%03lu.%04lu", + (unsigned long)ui_state->new_rx_frequency/1000000, + (unsigned long)(ui_state->new_rx_frequency%1000000)/100); + } + else + { + // Replace Rx frequency with underscorses + if(ui_state->input_position == 1) + strcpy(ui_state->new_rx_freq_buf, ">Rx:___.____"); + ui_state->new_rx_freq_buf[insert_pos] = input_char; + gfx_print(layout.line2_pos, layout.input_font, TEXT_ALIGN_CENTER, + color_white, ui_state->new_rx_freq_buf); + } + gfx_print(layout.line3_pos, layout.input_font, TEXT_ALIGN_CENTER, + color_white, " Tx:%03lu.%04lu", + (unsigned long)last_state.channel.tx_frequency/1000000, + (unsigned long)(last_state.channel.tx_frequency%1000000)/100); + } + else if(ui_state->input_set == SET_TX) + { + gfx_print(layout.line2_pos, layout.input_font, TEXT_ALIGN_CENTER, + color_white, " Rx:%03lu.%04lu", + (unsigned long)ui_state->new_rx_frequency/1000000, + (unsigned long)(ui_state->new_rx_frequency%1000000)/100); + // Replace Rx frequency with underscorses + if(ui_state->input_position == 0) + { + gfx_print(layout.line3_pos, layout.input_font, TEXT_ALIGN_CENTER, + color_white, ">Tx:%03lu.%04lu", + (unsigned long)ui_state->new_rx_frequency/1000000, + (unsigned long)(ui_state->new_rx_frequency%1000000)/100); + } + else + { + if(ui_state->input_position == 1) + strcpy(ui_state->new_tx_freq_buf, ">Tx:___.____"); + ui_state->new_tx_freq_buf[insert_pos] = input_char; + gfx_print(layout.line3_pos, layout.input_font, TEXT_ALIGN_CENTER, + color_white, ui_state->new_tx_freq_buf); + } + } +} + +void _ui_drawMainBottom() +{ + // Squelch bar + float rssi = last_state.rssi; + float squelch = last_state.settings.sqlLevel / 16.0f; + uint16_t meter_width = SCREEN_WIDTH - 2 * layout.horizontal_pad; + uint16_t meter_height = layout.bottom_h; + point_t meter_pos = { layout.horizontal_pad, + SCREEN_HEIGHT - meter_height - layout.bottom_pad}; + uint8_t mic_level = platform_getMicLevel(); + switch(last_state.channel.mode) + { + case OPMODE_FM: + gfx_drawSmeter(meter_pos, + meter_width, + meter_height, + rssi, + squelch, + yellow_fab413); + break; + case OPMODE_DMR: + gfx_drawSmeterLevel(meter_pos, + meter_width, + meter_height, + rssi, + mic_level); + break; + case OPMODE_M17: + gfx_drawSmeterLevel(meter_pos, + meter_width, + meter_height, + rssi, + mic_level); + break; + } +} + +void _ui_drawMainVFO(ui_state_t* ui_state) +{ + gfx_clearScreen(); + _ui_drawMainTop(); + _ui_drawModeInfo(ui_state); + _ui_drawFrequency(); + _ui_drawMainBottom(); +} + +void _ui_drawMainVFOInput(ui_state_t* ui_state) +{ + gfx_clearScreen(); + _ui_drawMainTop(); + _ui_drawVFOMiddleInput(ui_state); + _ui_drawMainBottom(); +} + +void _ui_drawMainMEM(ui_state_t* ui_state) +{ + gfx_clearScreen(); + _ui_drawMainTop(); + _ui_drawBankChannel(); + _ui_drawModeInfo(ui_state); + _ui_drawFrequency(); + _ui_drawMainBottom(); +} diff --git a/openrtx/src/ui/module17/ui_menu.c b/openrtx/src/ui/module17/ui_menu.c new file mode 100644 index 00000000..a222381e --- /dev/null +++ b/openrtx/src/ui/module17/ui_menu.c @@ -0,0 +1,531 @@ +/*************************************************************************** + * Copyright (C) 2020 - 2022 by Federico Amedeo Izzo IU2NUO, * + * Niccolò Izzo IU2KIN * + * Frederik Saraci IU2NRO * + * Silvano Seva IU2KWO * + * * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include + +/* UI main screen helper functions, their implementation is in "ui_main.c" */ +extern void _ui_drawMainBottom(); + +const char *display_timer_values[] = +{ + "Off", + "5 s", + "10 s", + "15 s", + "20 s", + "25 s", + "30 s", + "1 min", + "2 min", + "3 min", + "4 min", + "5 min", + "15 min", + "30 min", + "45 min", + "1 hour" +}; + +void _ui_drawMenuList(uint8_t selected, int (*getCurrentEntry)(char *buf, uint8_t max_len, uint8_t index)) +{ + point_t pos = layout.line1_pos; + // Number of menu entries that fit in the screen height + uint8_t entries_in_screen = (SCREEN_HEIGHT - 1 - pos.y) / layout.menu_h + 1; + uint8_t scroll = 0; + char entry_buf[MAX_ENTRY_LEN] = ""; + color_t text_color = color_white; + for(int item=0, result=0; (result == 0) && (pos.y < SCREEN_HEIGHT); item++) + { + // If selection is off the screen, scroll screen + if(selected >= entries_in_screen) + scroll = selected - entries_in_screen + 1; + // Call function pointer to get current menu entry string + result = (*getCurrentEntry)(entry_buf, sizeof(entry_buf), item+scroll); + if(result != -1) + { + text_color = color_white; + if(item + scroll == selected) + { + text_color = color_black; + // Draw rectangle under selected item, compensating for text height + point_t rect_pos = {0, pos.y - layout.menu_h + 3}; + gfx_drawRect(rect_pos, SCREEN_WIDTH, layout.menu_h, color_white, true); + } + gfx_print(pos, layout.menu_font, TEXT_ALIGN_LEFT, text_color, entry_buf); + pos.y += layout.menu_h; + } + } +} + +void _ui_drawMenuListValue(ui_state_t* ui_state, uint8_t selected, + int (*getCurrentEntry)(char *buf, uint8_t max_len, uint8_t index), + int (*getCurrentValue)(char *buf, uint8_t max_len, uint8_t index)) +{ + point_t pos = layout.line1_pos; + // Number of menu entries that fit in the screen height + uint8_t entries_in_screen = (SCREEN_HEIGHT - 1 - pos.y) / layout.menu_h + 1; + uint8_t scroll = 0; + char entry_buf[MAX_ENTRY_LEN] = ""; + char value_buf[MAX_ENTRY_LEN] = ""; + color_t text_color = color_white; + for(int item=0, result=0; (result == 0) && (pos.y < SCREEN_HEIGHT); item++) + { + // If selection is off the screen, scroll screen + if(selected >= entries_in_screen) + scroll = selected - entries_in_screen + 1; + // Call function pointer to get current menu entry string + result = (*getCurrentEntry)(entry_buf, sizeof(entry_buf), item+scroll); + // Call function pointer to get current entry value string + result = (*getCurrentValue)(value_buf, sizeof(value_buf), item+scroll); + if(result != -1) + { + text_color = color_white; + if(item + scroll == selected) + { + // Draw rectangle under selected item, compensating for text height + // If we are in edit mode, draw a hollow rectangle + text_color = color_black; + bool full_rect = true; + if(ui_state->edit_mode) + { + text_color = color_white; + full_rect = false; + } + point_t rect_pos = {0, pos.y - layout.menu_h + 3}; + gfx_drawRect(rect_pos, SCREEN_WIDTH, layout.menu_h, color_white, full_rect); + } + gfx_print(pos, layout.menu_font, TEXT_ALIGN_LEFT, text_color, entry_buf); + gfx_print(pos, layout.menu_font, TEXT_ALIGN_RIGHT, text_color, value_buf); + pos.y += layout.menu_h; + } + } +} + +int _ui_getMenuTopEntryName(char *buf, uint8_t max_len, uint8_t index) +{ + if(index >= menu_num) return -1; + snprintf(buf, max_len, "%s", menu_items[index]); + return 0; +} + +int _ui_getSettingsEntryName(char *buf, uint8_t max_len, uint8_t index) +{ + if(index >= settings_num) return -1; + snprintf(buf, max_len, "%s", settings_items[index]); + return 0; +} + +int _ui_getDisplayEntryName(char *buf, uint8_t max_len, uint8_t index) +{ + if(index >= display_num) return -1; + snprintf(buf, max_len, "%s", display_items[index]); + return 0; +} + +int _ui_getDisplayValueName(char *buf, uint8_t max_len, uint8_t index) +{ + if(index >= display_num) return -1; + uint8_t value = 0; + switch(index) + { +#ifdef SCREEN_CONTRAST + case D_CONTRAST: + value = last_state.settings.contrast; + break; +#endif + case D_TIMER: + snprintf(buf, max_len, "%s", + display_timer_values[last_state.settings.display_timer]); + return 0; + } + snprintf(buf, max_len, "%d", value); + return 0; +} + +#ifdef GPS_PRESENT +int _ui_getSettingsGPSEntryName(char *buf, uint8_t max_len, uint8_t index) +{ + if(index >= settings_gps_num) return -1; + snprintf(buf, max_len, "%s", settings_gps_items[index]); + return 0; +} + +int _ui_getSettingsGPSValueName(char *buf, uint8_t max_len, uint8_t index) +{ + if(index >= settings_gps_num) return -1; + switch(index) + { + case G_ENABLED: + snprintf(buf, max_len, "%s", (last_state.settings.gps_enabled) ? "ON" : "OFF"); + break; + case G_SET_TIME: + snprintf(buf, max_len, "%s", (last_state.gps_set_time) ? "ON" : "OFF"); + break; + case G_TIMEZONE: + // Add + prefix to positive numbers + if(last_state.settings.utc_timezone > 0) + snprintf(buf, max_len, "+%d", last_state.settings.utc_timezone); + else + snprintf(buf, max_len, "%d", last_state.settings.utc_timezone); + break; + } + return 0; +} +#endif + +int _ui_getInfoEntryName(char *buf, uint8_t max_len, uint8_t index) +{ + if(index >= info_num) return -1; + snprintf(buf, max_len, "%s", info_items[index]); + return 0; +} + +int _ui_getInfoValueName(char *buf, uint8_t max_len, uint8_t index) +{ + const hwInfo_t* hwinfo = platform_getHwInfo(); + if(index >= info_num) return -1; + switch(index) + { + case 0: // Git Version + snprintf(buf, max_len, "%s", GIT_VERSION); + break; + case 1: // Battery voltage + { + // Compute integer part and mantissa of voltage value, adding 50mV + // to mantissa for rounding to nearest integer + uint16_t volt = (last_state.v_bat + 50) / 1000; + uint16_t mvolt = ((last_state.v_bat - volt * 1000) + 50) / 100; + snprintf(buf, max_len, "%d.%dV", volt, mvolt); + } + break; + case 2: // Battery charge + snprintf(buf, max_len, "%d%%", last_state.charge); + break; + case 3: // RSSI + snprintf(buf, max_len, "%.1fdBm", last_state.rssi); + break; + case 4: // Heap usage + snprintf(buf, max_len, "%dB", getHeapSize() - getCurrentFreeHeap()); + break; + case 5: // Band + snprintf(buf, max_len, "%s %s", hwinfo->vhf_band ? "VHF" : "", hwinfo->uhf_band ? "UHF" : ""); + break; + case 6: // VHF + snprintf(buf, max_len, "%d - %d", hwinfo->vhf_minFreq, hwinfo->vhf_maxFreq); + break; + case 7: // UHF + snprintf(buf, max_len, "%d - %d", hwinfo->uhf_minFreq, hwinfo->uhf_maxFreq); + break; + case 8: // LCD Type + snprintf(buf, max_len, "%d", hwinfo->lcd_type); + break; + } + return 0; +} + +void _ui_drawMenuTop(ui_state_t* ui_state) +{ + gfx_clearScreen(); + // Print "Menu" on top bar + gfx_print(layout.top_pos, layout.top_font, TEXT_ALIGN_CENTER, + color_white, "Menu"); + // Print menu entries + _ui_drawMenuList(ui_state->menu_selected, _ui_getMenuTopEntryName); +} + +#ifdef GPS_PRESENT +void _ui_drawMenuGPS() +{ + char *fix_buf, *type_buf; + gfx_clearScreen(); + // Print "GPS" on top bar + gfx_print(layout.top_pos, layout.top_font, TEXT_ALIGN_CENTER, + color_white, "GPS"); + point_t fix_pos = {layout.line2_pos.x, SCREEN_HEIGHT * 2 / 5}; + // Print GPS status, if no fix, hide details + if(!last_state.settings.gps_enabled) + gfx_print(fix_pos, layout.line3_font, TEXT_ALIGN_CENTER, + color_white, "GPS OFF"); + else if (last_state.gps_data.fix_quality == 0) + gfx_print(fix_pos, layout.line3_font, TEXT_ALIGN_CENTER, + color_white, "No Fix"); + else if (last_state.gps_data.fix_quality == 6) + gfx_print(fix_pos, layout.line3_font, TEXT_ALIGN_CENTER, + color_white, "Fix Lost"); + else + { + switch(last_state.gps_data.fix_quality) + { + case 1: + fix_buf = "SPS"; + break; + case 2: + fix_buf = "DGPS"; + break; + case 3: + fix_buf = "PPS"; + break; + default: + fix_buf = "ERROR"; + break; + } + + switch(last_state.gps_data.fix_type) + { + case 1: + type_buf = ""; + break; + case 2: + type_buf = "2D"; + break; + case 3: + type_buf = "3D"; + break; + default: + type_buf = "ERROR"; + break; + } + gfx_print(layout.line1_pos, layout.top_font, TEXT_ALIGN_LEFT, + color_white, fix_buf); + gfx_print(layout.line1_pos, layout.top_font, TEXT_ALIGN_CENTER, + color_white, "N "); + gfx_print(layout.line1_pos, layout.top_font, TEXT_ALIGN_RIGHT, + color_white, "%8.6f", last_state.gps_data.latitude); + gfx_print(layout.line2_pos, layout.top_font, TEXT_ALIGN_LEFT, + color_white, type_buf); + // Convert from signed longitude, to unsigned + direction + float longitude = last_state.gps_data.longitude; + char *direction = (longitude < 0) ? "W " : "E "; + longitude = (longitude < 0) ? -longitude : longitude; + gfx_print(layout.line2_pos, layout.top_font, TEXT_ALIGN_CENTER, + color_white, direction); + gfx_print(layout.line2_pos, layout.top_font, TEXT_ALIGN_RIGHT, + color_white, "%8.6f", longitude); + gfx_print(layout.bottom_pos, layout.bottom_font, TEXT_ALIGN_CENTER, + color_white, "S %4.1fkm/h A %4.1fm", + last_state.gps_data.speed, + last_state.gps_data.altitude); + } + // Draw compass + point_t compass_pos = {layout.horizontal_pad * 2, SCREEN_HEIGHT / 2}; + gfx_drawGPScompass(compass_pos, + SCREEN_WIDTH / 9 + 2, + last_state.gps_data.tmg_true, + last_state.gps_data.fix_quality != 0 && + last_state.gps_data.fix_quality != 6); + // Draw satellites bar graph + point_t bar_pos = {layout.line3_pos.x + SCREEN_WIDTH * 1 / 3, SCREEN_HEIGHT / 2}; + gfx_drawGPSgraph(bar_pos, + (SCREEN_WIDTH * 2 / 3) - layout.horizontal_pad, + SCREEN_HEIGHT / 3, + last_state.gps_data.satellites, + last_state.gps_data.active_sats); +} +#endif + +void _ui_drawMenuSettings(ui_state_t* ui_state) +{ + gfx_clearScreen(); + // Print "Settings" on top bar + gfx_print(layout.top_pos, layout.top_font, TEXT_ALIGN_CENTER, + color_white, "Settings"); + // Print menu entries + _ui_drawMenuList(ui_state->menu_selected, _ui_getSettingsEntryName); +} + +void _ui_drawMenuInfo(ui_state_t* ui_state) +{ + gfx_clearScreen(); + // Print "Info" on top bar + gfx_print(layout.top_pos, layout.top_font, TEXT_ALIGN_CENTER, + color_white, "Info"); + // Print menu entries + _ui_drawMenuListValue(ui_state, ui_state->menu_selected, _ui_getInfoEntryName, + _ui_getInfoValueName); +} + +void _ui_drawMenuAbout() +{ + gfx_clearScreen(); + point_t openrtx_pos = {layout.horizontal_pad, layout.line3_h}; + if(SCREEN_HEIGHT >= 100) + ui_drawSplashScreen(false); + else + gfx_print(openrtx_pos, layout.line3_font, TEXT_ALIGN_CENTER, + color_white, "OpenRTX"); + uint8_t line_h = layout.menu_h; + point_t pos = {SCREEN_WIDTH / 7, SCREEN_HEIGHT - (line_h * (author_num - 1)) - 5}; + for(int author = 0; author < author_num; author++) + { + gfx_print(pos, layout.top_font, TEXT_ALIGN_LEFT, + color_white, "%s", authors[author]); + pos.y += line_h; + } +} + +void _ui_drawSettingsDisplay(ui_state_t* ui_state) +{ + gfx_clearScreen(); + // Print "Display" on top bar + gfx_print(layout.top_pos, layout.top_font, TEXT_ALIGN_CENTER, + color_white, "Display"); + // Print display settings entries + _ui_drawMenuListValue(ui_state, ui_state->menu_selected, _ui_getDisplayEntryName, + _ui_getDisplayValueName); +} + +#ifdef GPS_PRESENT +void _ui_drawSettingsGPS(ui_state_t* ui_state) +{ + gfx_clearScreen(); + // Print "GPS Settings" on top bar + gfx_print(layout.top_pos, layout.top_font, TEXT_ALIGN_CENTER, + color_white, "GPS Settings"); + // Print display settings entries + _ui_drawMenuListValue(ui_state, ui_state->menu_selected, + _ui_getSettingsGPSEntryName, + _ui_getSettingsGPSValueName); +} +#endif + +#ifdef RTC_PRESENT +void _ui_drawSettingsTimeDate() +{ + gfx_clearScreen(); + datetime_t local_time = utcToLocalTime(last_state.time, + last_state.settings.utc_timezone); + // Print "Time&Date" on top bar + gfx_print(layout.top_pos, layout.top_font, TEXT_ALIGN_CENTER, + color_white, "Time&Date"); + // Print current time and date + gfx_print(layout.line2_pos, layout.input_font, TEXT_ALIGN_CENTER, + color_white, "%02d/%02d/%02d", + local_time.date, local_time.month, local_time.year); + gfx_print(layout.line3_pos, layout.input_font, TEXT_ALIGN_CENTER, + color_white, "%02d:%02d:%02d", + local_time.hour, local_time.minute, local_time.second); +} + +void _ui_drawSettingsTimeDateSet(ui_state_t* ui_state) +{ + (void) last_state; + + gfx_clearScreen(); + // Print "Time&Date" on top bar + gfx_print(layout.top_pos, layout.top_font, TEXT_ALIGN_CENTER, + color_white, "Time&Date"); + if(ui_state->input_position <= 0) + { + strcpy(ui_state->new_date_buf, "__/__/__"); + strcpy(ui_state->new_time_buf, "__:__:00"); + } + else + { + char input_char = ui_state->input_number + '0'; + // Insert date digit + if(ui_state->input_position <= 6) + { + uint8_t pos = ui_state->input_position -1; + // Skip "/" + if(ui_state->input_position > 2) pos += 1; + if(ui_state->input_position > 4) pos += 1; + ui_state->new_date_buf[pos] = input_char; + } + // Insert time digit + else + { + uint8_t pos = ui_state->input_position -7; + // Skip ":" + if(ui_state->input_position > 8) pos += 1; + ui_state->new_time_buf[pos] = input_char; + } + } + gfx_print(layout.line2_pos, layout.input_font, TEXT_ALIGN_CENTER, + color_white, ui_state->new_date_buf); + gfx_print(layout.line3_pos, layout.input_font, TEXT_ALIGN_CENTER, + color_white, ui_state->new_time_buf); +} +#endif + +void _ui_drawSettingsM17(ui_state_t* ui_state) +{ + gfx_clearScreen(); + // Print "M17 Settings" on top bar + gfx_print(layout.top_pos, layout.top_font, TEXT_ALIGN_CENTER, + color_white, "M17 Settings"); + gfx_printLine(1, 4, layout.top_h, SCREEN_HEIGHT - layout.bottom_h, + layout.horizontal_pad, layout.menu_font, + TEXT_ALIGN_LEFT, color_white, "Callsign:"); + if(ui_state->edit_mode) + { + uint16_t rect_width = SCREEN_WIDTH - (layout.horizontal_pad * 2); + uint16_t rect_height = (SCREEN_HEIGHT - (layout.top_h + layout.bottom_h))/2; + point_t rect_origin = {(SCREEN_WIDTH - rect_width) / 2, + (SCREEN_HEIGHT - rect_height) / 2}; + gfx_drawRect(rect_origin, rect_width, rect_height, color_white, false); + // Print M17 callsign being typed + gfx_printLine(1, 1, layout.top_h, SCREEN_HEIGHT - layout.bottom_h, + layout.horizontal_pad, layout.input_font, + TEXT_ALIGN_CENTER, color_white, ui_state->new_callsign); + } + else + { + // Print M17 current callsign + gfx_printLine(1, 1, layout.top_h, SCREEN_HEIGHT - layout.bottom_h, + layout.horizontal_pad, layout.input_font, + TEXT_ALIGN_CENTER, color_white, last_state.settings.callsign); + } +} + +void _ui_drawSettingsReset2Defaults(ui_state_t* ui_state) +{ + (void) ui_state; + + static int drawcnt = 0; + static long long lastDraw = 0; + + gfx_clearScreen(); + gfx_print(layout.top_pos, layout.top_font, TEXT_ALIGN_CENTER, + color_white, "Reset to Defaults"); + + // Make text flash yellow once every 1s + color_t textcolor = drawcnt % 2 == 0 ? color_white : yellow_fab413; + gfx_printLine(1, 4, layout.top_h, SCREEN_HEIGHT - layout.bottom_h, + layout.horizontal_pad, layout.top_font, + TEXT_ALIGN_CENTER, textcolor, "To reset:"); + gfx_printLine(2, 4, layout.top_h, SCREEN_HEIGHT - layout.bottom_h, + layout.horizontal_pad, layout.top_font, + TEXT_ALIGN_CENTER, textcolor, "Press Enter twice"); + + if((getTick() - lastDraw) > 1000) + { + drawcnt++; + lastDraw = getTick(); + } +}