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();
+ }
+}