From f98d2d38206243100ac3a4273dadb826c2e54242 Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Tue, 3 Nov 2026 17:11:22 +0100 Subject: [PATCH 01/11] Generator: PRESET menu; a loaded preset opens the menu of its mode, a generator preset starts with the output off (issue #98) --- ui.c | 22 +++++++++++++++++++++- vna_browser.c | 7 +++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/ui.c b/ui.c index e38ee8b..7c27be8 100644 --- a/ui.c +++ b/ui.c @@ -2200,6 +2200,23 @@ static UI_FUNCTION_ADV_CALLBACK(menu_mode_acb) redraw_request |= REDRAW_CAL_STATUS; } +// After a preset load: a generator preset starts with the output off, a mode change shows the menu of the new mode +static void menu_preset_loaded(int old_mode) +{ + bool output = MODE_OUTPUT(setting.mode); + if (output) + setting.mute = true; + if (setting.mode == old_mode) { + menu_move_back(!output); // a generator returns to its form menu + return; + } + menu_move_top(); + if (output) + menu_push_submenu(setting.mode == M_GENLOW ? menu_lowoutputmode : menu_highoutputmode); + else + ui_mode_normal(); +} + static UI_FUNCTION_ADV_CALLBACK(menu_load_preset_acb) { (void)item; @@ -2216,11 +2233,12 @@ static UI_FUNCTION_ADV_CALLBACK(menu_load_preset_acb) plot_printf(b->text, sizeof(b->text), "EMPTY %d", (int)data); return; } + int old_mode = setting.mode; if (caldata_recall(data) == -1) { if (data == 0) reset_settings(setting.mode); // Restore factory defaults } - menu_move_back(true); + menu_preset_loaded(old_mode); } static UI_FUNCTION_ADV_CALLBACK(menu_store_preset_acb) @@ -4758,6 +4776,7 @@ const menuitem_t menu_lowoutputmode[] = { #ifdef TINYSA4 { MT_FORM | MT_ADV_CALLBACK, 255, "OUTPUT: %s", menu_lowoutput_settings_acb}, #endif + { MT_FORM | MT_SUBMENU, 0, "PRESET", menu_load_preset}, { MT_FORM | MT_CANCEL, 0, "MODE", NULL }, { MT_FORM | MT_NONE, 0, NULL, NULL } // sentinel }; @@ -4775,6 +4794,7 @@ const menuitem_t menu_highoutputmode[] = { #ifdef TINYSA4 { MT_FORM | MT_SUBMENU, 255, S_RARROW" Settings", menu_settings3}, #endif + { MT_FORM | MT_SUBMENU, 0, "PRESET", menu_load_preset}, { MT_FORM | MT_CANCEL, 0, "MODE", NULL }, { MT_FORM | MT_NONE, 0, NULL, NULL } // sentinel }; diff --git a/vna_browser.c b/vna_browser.c index 2b4605a..12d4ea7 100644 --- a/vna_browser.c +++ b/vna_browser.c @@ -468,7 +468,14 @@ static void browser_key_press(int key) { break; case FILE_BUTTON_FILE: // Open or delete file default: + { + int old_mode = setting.mode; browser_open_file(key - FILE_BUTTON_FILE + (current_page - 1) * FILES_PER_PAGE); + if (!sel_mode && keypad_mode == FMT_PRS_FILE) { + menu_preset_loaded(old_mode); + return; + } + } if (sel_mode) { file_count = 0; // Reeset file count (recalculate on draw page) selection = -1; // Reset delection From f00d6ef940dabdcf2222cad7e0ece3521a638810 Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Tue, 3 Nov 2026 17:11:23 +0100 Subject: [PATCH 02/11] Generator sweep: the SW button shows the start and stop frequency (issue #153) --- ui.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui.c b/ui.c index 7c27be8..090388e 100644 --- a/ui.c +++ b/ui.c @@ -1690,8 +1690,13 @@ static UI_FUNCTION_ADV_CALLBACK(menu_sweep_acb) (void)item; if (b){ if (setting.level_sweep != 0 || get_sweep_frequency(ST_SPAN) != 0) { +#ifdef TINYSA4 + plot_printf(b->text, sizeof b->text, "SW:%.3Q-%.3QHz %+ddB %.3Fs", + get_sweep_frequency(ST_START), get_sweep_frequency(ST_STOP), +#else plot_printf(b->text, sizeof b->text, "SW:%3.2fMHz %+ddB %.3Fs", get_sweep_frequency(ST_SPAN) / 1000000.0, +#endif (int)setting.level_sweep, setting.sweep_time_us/(float)ONE_SECOND_TIME); } From a7956d7a2672914d8963571f7fb0babd737e4d8e Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Tue, 3 Nov 2026 17:11:23 +0100 Subject: [PATCH 03/11] SD card icon: REPEAT saves every interval, a tap on the icon starts and stops it (issue #62) --- nanovna.h | 1 + plot.c | 2 +- ui.c | 58 ++++++++++++++++++++++++------------------------------- 3 files changed, 27 insertions(+), 34 deletions(-) diff --git a/nanovna.h b/nanovna.h index 0f8d7d2..fe0a695 100644 --- a/nanovna.h +++ b/nanovna.h @@ -829,6 +829,7 @@ float marker_to_value(const int i); #define SDIS_VALID_MASK (1 << 7) #define SDIS_DEFAULT (SDIS_CAPTURE | SDIS_VALID_MASK) #define SDIS_IS_ENABLED ((config.sd_icon_save & ~SDIS_VALID_MASK) != 0) +extern bool sd_repeat; #endif // __USE_SD_CARD__ #pragma pack(push, 4) diff --git a/plot.c b/plot.c index 942f976..6479802 100644 --- a/plot.c +++ b/plot.c @@ -2131,7 +2131,7 @@ static const uint8_t sd_icon [] = { _BMP16(0b0111111111111000) // }; if (SD_Inserted() && SDIS_IS_ENABLED) { - ili9341_set_foreground(LCD_BRIGHT_COLOR_GREEN); + ili9341_set_foreground(sd_repeat ? LCD_BRIGHT_COLOR_RED : LCD_BRIGHT_COLOR_GREEN); ili9341_blitBitmap(4, SD_CARD_START, 16, 16, sd_icon); // ili9341_drawstring("-SD-", x, SD_CARD_START); } diff --git a/ui.c b/ui.c index 090388e..849fe6e 100644 --- a/ui.c +++ b/ui.c @@ -1566,7 +1566,7 @@ static const struct { #ifdef __USE_RTC__ [KM_RTC_DATE] = {keypads_positive , "SET DATE\n YYMMDD"}, // Date [KM_RTC_TIME] = {keypads_positive , "SET TIME\n HHMMSS"}, // Time -[KM_INTERVAL] = {keypads_positive , "SET INTERVAL\n HHMMSS"}, // Interval +[KM_INTERVAL] = {keypads_positive , "REPEAT\nHHMMSS"}, // Interval #endif [KM_PRESET_NAME] = {keypads_text , "NAME"}, // KM_PRESET_NAME #endif @@ -5635,6 +5635,7 @@ static const menuitem_t menu_stimulus[] = { static const menuitem_t menu_storage_sd_icon[] = { { MT_ADV_CALLBACK, 0, "SAVE\nCAPTURE", menu_sd_icon_save_capture_acb }, { MT_ADV_CALLBACK, 0, "SAVE\nTRACES", menu_sd_icon_save_traces_acb }, + { MT_KEYPAD, KM_INTERVAL, "REPEAT\n\b%s", "0 = single save" }, { MT_NONE, 0, NULL, menu_back } }; @@ -8376,7 +8377,7 @@ static void sa_save_file(uint8_t format) { #endif // Prepare filename and open for write - if (config._mode & _MODE_AUTO_FILENAME) { // Auto name, use date / time + if ((config._mode & _MODE_AUTO_FILENAME) || sd_repeat) { // Auto name, use date / time #if FF_USE_LFN >= 1 uint32_t tr = rtc_get_tr_bcd(); // TR read first uint32_t dr = rtc_get_dr_bcd(); // DR read second @@ -8546,6 +8547,17 @@ void save_csv(uint8_t mask) { menu_sdcard_cb(0, FMT_CSV_FILE); } +bool sd_repeat; // SD icon saves every setting.interval seconds +static systime_t sd_repeat_start; + +static void sd_icon_save(void) { + sd_repeat_start = chVTGetSystemTimeX(); + if (config.sd_icon_save & SDIS_CAPTURE) + menu_sdcard_cb(0, FMT_BMP_FILE); + if (config.sd_icon_save & SDIS_TRACES) + menu_save_traces_cb(0, 0); +} + static bool made_screenshot(int touch_x, int touch_y) { if (touch_y < SD_CARD_START || touch_y > SD_CARD_START + 20 || touch_x > OFFSETX) @@ -8553,10 +8565,12 @@ made_screenshot(int touch_x, int touch_y) { ili9341_set_background(LCD_BG_COLOR); ili9341_fill(4, SD_CARD_START, 16, 16); touch_wait_release(); - if (config.sd_icon_save & SDIS_CAPTURE) - menu_sdcard_cb(0, FMT_BMP_FILE); - if (config.sd_icon_save & SDIS_TRACES) - menu_save_traces_cb(0, 0); + if (setting.interval) { // Repeat set: the icon starts and stops saving every interval + sd_repeat = !sd_repeat; + redraw_request |= REDRAW_BATTERY; // icon color + if (!sd_repeat) return TRUE; + } + sd_icon_save(); return TRUE; } #endif @@ -8716,36 +8730,14 @@ void ui_process_touch(void) static uint16_t previous_button_state = 0; -#ifdef TINYSA4 -uint64_t time_since_start = 0; -uint64_t interval_stop_time = 0; - -systime_t old_ticks; -void set_interval_stop_time(void) -{ - if (setting.interval == 0) { - interval_stop_time = 0; - } else { - interval_stop_time = time_since_start + CH_CFG_ST_FREQUENCY * (uint64_t)setting.interval; - } -} -#endif - - void ui_process(void) { -#ifdef TINYSA4 - systime_t ticks = chVTGetSystemTimeX(); - if (ticks < old_ticks) - time_since_start += 0x100000000; - old_ticks = ticks; - time_since_start = (time_since_start & 0xffffffff00000000) | ticks; - - if (interval_stop_time != 0 && interval_stop_time < time_since_start) { - set_interval_stop_time(); - } - +#ifdef __USE_SD_CARD__ + // SD icon repeat, only from the normal screen: a capture would close an open menu + if (sd_repeat && setting.interval && ui_mode == UI_NORMAL && + chVTGetSystemTimeX() - sd_repeat_start >= (systime_t)setting.interval * CH_CFG_ST_FREQUENCY) + sd_icon_save(); #endif int button_state = READ_PORT() & BUTTON_MASK; From eff762b474f7c8efde35fc21168894fd2e1f2f6d Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Tue, 3 Nov 2026 17:11:23 +0100 Subject: [PATCH 04/11] Markers: TRACKING L->R keeps the tracking markers in frequency order (issue #124) --- nanovna.h | 2 ++ sa_core.c | 17 +++++++++++++++++ ui.c | 25 +++++++------------------ 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/nanovna.h b/nanovna.h index fe0a695..ad0dd7f 100644 --- a/nanovna.h +++ b/nanovna.h @@ -818,6 +818,8 @@ float marker_to_value(const int i); // auto name #define _MODE_AUTO_FILENAME 0x10 #define _MODE_MHZ_CSV 0x20 +// tracking markers left to right +#define _MODE_MARKER_ORDER 0x40 #ifdef __USE_SD_CARD__ // SD Icon Save (SDIS) configuration diff --git a/sa_core.c b/sa_core.c index 6293f6b..7697ce8 100644 --- a/sa_core.c +++ b/sa_core.c @@ -5769,6 +5769,23 @@ static volatile int dummy; if (MODE_INPUT(setting.mode)) { // Assign maxima found to tracking markers int i = 0; int m = 0; +#ifdef TINYSA4 + if (config._mode & _MODE_MARKER_ORDER) { // The highest maxima, one per tracking marker, left to right + for (; m < MARKERS_MAX; m++) + if (markers[m].enabled && markers[m].mtype & M_TRACKING && markers[m].trace == t) + i++; + if (i > cur_max) i = cur_max; + while (--i > 0) { // Bubble sort these first maxima on frequency + for (m = 0; m < i; m++) + if (max_index[m] > max_index[m+1]) { + uint16_t s = max_index[m]; + max_index[m] = max_index[m+1]; + max_index[m+1] = s; + } + } + i = m = 0; + } +#endif while (i < cur_max) { // For all maxima found while (m < MARKERS_MAX) { if (markers[m].enabled && markers[m].mtype & M_TRACKING && markers[m].trace == t) { // Available marker found diff --git a/ui.c b/ui.c index 849fe6e..178fdb7 100644 --- a/ui.c +++ b/ui.c @@ -4615,27 +4615,15 @@ __attribute__((noinline)) static void fw_update_ui(const char *fname) { -static UI_FUNCTION_ADV_CALLBACK(menu_autoname_acb) +// Toggle the config._mode bit in data +static UI_FUNCTION_ADV_CALLBACK(menu_config_mode_acb) { (void)item; - (void)data; - if (b){ - b->icon = config._mode & _MODE_AUTO_FILENAME ? BUTTON_ICON_CHECK : BUTTON_ICON_NOCHECK; - return; - } - config._mode^= _MODE_AUTO_FILENAME; - config_save(); -} - -static UI_FUNCTION_ADV_CALLBACK(menu_mhz_csv_acb) -{ - (void)item; - (void)data; if (b){ - b->icon = config._mode & _MODE_MHZ_CSV ? BUTTON_ICON_CHECK : BUTTON_ICON_NOCHECK; + b->icon = config._mode & data ? BUTTON_ICON_CHECK : BUTTON_ICON_NOCHECK; return; } - config._mode^= _MODE_MHZ_CSV; + config._mode^= data; config_save(); } @@ -4871,6 +4859,7 @@ const menuitem_t menu_marker_search[] = { { MT_ADV_CALLBACK, 0, "ENTER\n%s", menu_enter_marker_acb}, { MT_ADV_CALLBACK, M_TRACKING, "TRACKING",menu_marker_modify_acb }, #ifdef TINYSA4 + { MT_ADV_CALLBACK, _MODE_MARKER_ORDER, "TRACKING\nL"S_RARROW"R", menu_config_mode_acb }, { MT_ADV_CALLBACK, 0, "JOG JUMP\nMAX",menu_jog_jump_acb }, { MT_KEYPAD, KM_NOISE, "PEAK\n\b%s", "2..120 dB"}, #endif @@ -5640,8 +5629,8 @@ static const menuitem_t menu_storage_sd_icon[] = { }; static const menuitem_t menu_storage_more[] = { - { MT_ADV_CALLBACK, 0, "AUTO NAME", menu_autoname_acb }, - { MT_ADV_CALLBACK, 0, "MHz\nCSV", menu_mhz_csv_acb }, + { MT_ADV_CALLBACK, _MODE_AUTO_FILENAME, "AUTO NAME", menu_config_mode_acb }, + { MT_ADV_CALLBACK, _MODE_MHZ_CSV, "MHz\nCSV", menu_config_mode_acb }, { MT_SUBMENU, 0, "SD CARD\nICON", menu_storage_sd_icon }, #ifdef __USE_USB_MSC__ { MT_CALLBACK, 0, "USB\nDISK", menu_usb_disk_cb }, From a62fa7ddc576e17c16990002e2f5fb2535bceafe Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Tue, 3 Nov 2026 17:11:24 +0100 Subject: [PATCH 05/11] Menu: small arrow on buttons that open a submenu (issue #89) --- ui.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui.c b/ui.c index 178fdb7..5181b61 100644 --- a/ui.c +++ b/ui.c @@ -7442,6 +7442,11 @@ draw_menu_buttons(const menuitem_t *menu, uint32_t mask) #else ili9341_drawstring(text, text_offs, y+(button_height-linesFONT_GET_HEIGHT)/2); #endif + if (MT_MASK(m->type) == MT_SUBMENU) { // Opens a submenu: small arrow in the top right corner + ili9341_set_background(LCD_MENU_TEXT_COLOR); + for (int r = -2; r <= 2; r++) + ili9341_fill(LCD_WIDTH - 6, y + 4 + r, 3 - (r < 0 ? -r : r), 1); + } } } // Cleanup other buttons (less flicker) From ee691b49f26970d0433a70f10978e0f0ca68afa6 Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Tue, 3 Nov 2026 17:11:24 +0100 Subject: [PATCH 06/11] SHOW BANDS: the band table is highlighted with its names when multi band is off (issue #169) --- nanovna.h | 1 - plot.c | 25 ++++++++++++++++++++++++- sa_core.c | 6 ------ ui.c | 11 ++++++----- 4 files changed, 30 insertions(+), 13 deletions(-) diff --git a/nanovna.h b/nanovna.h index ad0dd7f..810ff8b 100644 --- a/nanovna.h +++ b/nanovna.h @@ -1912,7 +1912,6 @@ extern int current_index; extern uint16_t actual_rbw_x10; void toggle_tracking(void); -void toggle_hambands(void); void reset_calibration(void); void set_reflevel(float); void set_external_gain(float); diff --git a/plot.c b/plot.c index 6479802..9348ada 100644 --- a/plot.c +++ b/plot.c @@ -255,7 +255,16 @@ const int ham_band_max = 18; int ham_band(int x) // Search which index in the frequency tabled matches with frequency f using actual_rbw { - freq_t f = getFrequency(x) + (setting.frequency_offset - FREQUENCY_SHIFT); + freq_t f = getFrequency(x); +#ifdef __BANDS__ + if ((config.hambands & 2) && !setting.multi_band) // band table + for (int i = 0; i < BANDS_MAX; i++) + if (setting.bands[i].enabled && setting.bands[i].start <= f && f <= setting.bands[i].end) + return true; + if (!(config.hambands & 1)) + return false; +#endif + f += (setting.frequency_offset - FREQUENCY_SHIFT); int L = 0; int R = ham_band_max - 1; while (L <= R) { @@ -1235,6 +1244,20 @@ draw_cell(int m, int n) // PULSE; #endif +#ifdef __BANDS__ + // Band table highlights (ham_band): name at the bottom left of each band + if ((config.hambands & 2) && !setting.multi_band && y0 + h >= HEIGHT - 12) { + ili9341_set_foreground(LCD_FG_COLOR); + for (int band = 0; band < BANDS_MAX; band++) { + band_t *b = &setting.bands[band]; + if (!b->enabled || b->start > getFrequency(sweep_points - 1) || b->end < getFrequency(0)) continue; + i = 0; + while (getFrequency(i) < b->start) i++; + cell_printf(trace_index_x[i] + 2 - x0, HEIGHT - 12 - y0, "%s", b->name); + } + } +#endif + // Draw trigger and band splitter lines bool draw_trigger = (setting.trigger != T_AUTO #ifdef __TRIGGER_TRACE__ diff --git a/sa_core.c b/sa_core.c index 7697ce8..44511d2 100644 --- a/sa_core.c +++ b/sa_core.c @@ -1115,12 +1115,6 @@ void toggle_mute(void) dirty = true; } -void toggle_hambands(void) -{ - config.hambands = !config.hambands; - dirty = true; -} - void toggle_below_IF(void) { if (S_IS_AUTO(setting.below_IF )) diff --git a/ui.c b/ui.c index 5181b61..9c71f9b 100644 --- a/ui.c +++ b/ui.c @@ -4134,14 +4134,14 @@ static UI_FUNCTION_ADV_CALLBACK(menu_settings_draw_line_acb){ } #endif #ifdef __HAM_BAND__ +// data: 1 = HAM bands, 2 = band table (multi band off) static UI_FUNCTION_ADV_CALLBACK(menu_settings_ham_bands){ (void)item; - (void)data; if(b){ - b->icon = config.hambands ? BUTTON_ICON_CHECK : BUTTON_ICON_NOCHECK; + b->icon = config.hambands & data ? BUTTON_ICON_CHECK : BUTTON_ICON_NOCHECK; return; } - toggle_hambands(); + config.hambands ^= data; redraw_request|= REDRAW_AREA; } #endif @@ -5268,7 +5268,8 @@ static const menuitem_t menu_settings[] = { MT_CALLBACK, FMT_BIN_FILE, "DUMP\nFIRMWARE", menu_sdcard_cb}, #endif #ifdef __HAM_BAND__ - { MT_ADV_CALLBACK, 0, "HAM\nBANDS", menu_settings_ham_bands}, + { MT_ADV_CALLBACK, 1, "HAM\nBANDS", menu_settings_ham_bands}, + { MT_ADV_CALLBACK, 2, "SHOW\nBANDS", menu_settings_ham_bands}, #endif #ifdef __SD_CARD_LOAD__ { MT_CALLBACK, 0 , "LOAD\nCONFIG.INI", menu_load_config_cb}, @@ -5442,7 +5443,7 @@ static const menuitem_t menu_config2[] = { MT_SUBMENU, 0, "EXPERT\nCONFIG", menu_settings}, #else #ifdef __HAM_BAND__ - { MT_ADV_CALLBACK, 0, "HAM\nBANDS", menu_settings_ham_bands}, + { MT_ADV_CALLBACK, 1, "HAM\nBANDS", menu_settings_ham_bands}, #endif { MT_SUBMENU, 0, "EXPERT\nCONFIG", menu_settings2}, #endif From f30af055dc963cabb9cb0ec219d7414afbddc676 Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Tue, 3 Nov 2026 17:11:25 +0100 Subject: [PATCH 07/11] File browser: REN renames a file (issue #100) --- vna_browser.c | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/vna_browser.c b/vna_browser.c index 12d4ea7..9eedcc0 100644 --- a/vna_browser.c +++ b/vna_browser.c @@ -4,7 +4,7 @@ static uint16_t current_page; static uint16_t sel_mode; // Buttons in browser -enum {FILE_BUTTON_LEFT = 0, FILE_BUTTON_RIGHT, FILE_BUTTON_EXIT, FILE_BUTTON_DEL, FILE_BUTTON_FILE}; +enum {FILE_BUTTON_LEFT = 0, FILE_BUTTON_RIGHT, FILE_BUTTON_EXIT, FILE_BUTTON_DEL, FILE_BUTTON_REN, FILE_BUTTON_FILE}; // Button position on screen typedef struct { uint16_t x; @@ -14,10 +14,11 @@ typedef struct { uint8_t ofs; } browser_btn_t; static const browser_btn_t browser_btn[] = { - [FILE_BUTTON_LEFT] = { 0 + 40, LCD_HEIGHT - FILE_BOTTOM_HEIGHT, LCD_WIDTH/2 - 80, FILE_BOTTOM_HEIGHT, (LCD_WIDTH/2 - 80 - FONT_WIDTH)/2}, // < previous + [FILE_BUTTON_LEFT] = { 0 + 80, LCD_HEIGHT - FILE_BOTTOM_HEIGHT, LCD_WIDTH/2 -120, FILE_BOTTOM_HEIGHT, (LCD_WIDTH/2 -120 - FONT_WIDTH)/2}, // < previous [FILE_BUTTON_RIGHT]= {LCD_WIDTH/2 + 40, LCD_HEIGHT - FILE_BOTTOM_HEIGHT, LCD_WIDTH/2 - 80, FILE_BOTTOM_HEIGHT, (LCD_WIDTH/2 - 80 - FONT_WIDTH)/2}, // > next [FILE_BUTTON_EXIT] = {LCD_WIDTH - 40, LCD_HEIGHT - FILE_BOTTOM_HEIGHT, 40, FILE_BOTTOM_HEIGHT, ( 40 - FONT_WIDTH)/2}, // X exit [FILE_BUTTON_DEL] = { 0 + 0, LCD_HEIGHT - FILE_BOTTOM_HEIGHT, 40, FILE_BOTTOM_HEIGHT, ( 40 - 3*FONT_WIDTH)/2}, // DEL + [FILE_BUTTON_REN] = { 0 + 40, LCD_HEIGHT - FILE_BOTTOM_HEIGHT, 40, FILE_BOTTOM_HEIGHT, ( 40 - 3*FONT_WIDTH)/2}, // REN // File button, only size and start position, must be idx = FILE_BUTTON_FILE [FILE_BUTTON_FILE] = { 0, 0, LCD_WIDTH/FILES_COLUMNS, FILE_BUTTON_HEIGHT, 5}, }; @@ -45,8 +46,8 @@ static void browser_draw_button(int idx, const char *txt) { ui_button_t b; browser_btn_t btn; browser_get_button_pos(idx, &btn); - // Mark DEL button in file delete mode - b.bg = (idx == FILE_BUTTON_DEL && sel_mode) ? LCD_LOW_BAT_COLOR : LCD_MENU_COLOR; + // Mark DEL button in file delete mode, REN in rename mode + b.bg = (sel_mode && idx == FILE_BUTTON_DEL - 1 + sel_mode) ? LCD_LOW_BAT_COLOR : LCD_MENU_COLOR; b.fg = LCD_MENU_TEXT_COLOR; b.border = (idx == selection) ? BROWSER_BUTTON_BORDER|BUTTON_BORDER_FALLING : BROWSER_BUTTON_BORDER|BUTTON_BORDER_RISE; if (txt == NULL) b.border|= BUTTON_BORDER_NO_FILL; @@ -103,7 +104,22 @@ repeat: if (cnt != 0) return; // Delete file if in delete mode - if (sel_mode) {f_unlink(fno.fname); return;} + if (sel_mode == 1) {f_unlink(fno.fname); return;} + // Rename mode: edit the name without extension in the text keypad + if (sel_mode == 2) { + int format = keypad_mode; + ui_preset_keypad_text(fno.fname, strlen(fno.fname) - strlen(file_ext[format]) - 1); + ui_mode_keypad(KM_FILENAME); + if (kp_buf[0]) { + char new_name[FF_LFN_BUF]; + plot_printf(new_name, FF_LFN_BUF, "%s.%s", kp_buf, file_ext[format]); + f_rename(fno.fname, new_name); + } + ui_mode = UI_BROWSER; // the keypad left to the normal screen + area_width = area_height = 0; + keypad_mode = format; + return; + } run_file: {} const char *error = NULL; @@ -401,6 +417,7 @@ static char cmd_buffer[256+128]; static void browser_draw_buttons(void) { browser_draw_button(FILE_BUTTON_DEL, "DEL"); + browser_draw_button(FILE_BUTTON_REN, "REN"); browser_draw_button(FILE_BUTTON_LEFT, "<"); browser_draw_button(FILE_BUTTON_RIGHT, ">"); browser_draw_button(FILE_BUTTON_EXIT, "X"); @@ -462,8 +479,10 @@ static void browser_key_press(int key) { case FILE_BUTTON_EXIT: //Exit ui_mode_normal(); break; - case FILE_BUTTON_DEL: // Toggle delete mode - sel_mode^= 1; + case FILE_BUTTON_DEL: // Toggle delete mode (1) or rename mode (2) + case FILE_BUTTON_REN: + key = key - FILE_BUTTON_DEL + 1; + sel_mode = sel_mode == key ? 0 : key; browser_draw_buttons(); break; case FILE_BUTTON_FILE: // Open or delete file From f16e7903891d0af1bb296399c0d767631111c257 Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Mon, 9 Nov 2026 12:13:15 +0100 Subject: [PATCH 08/11] Fixed a crash when a band frequency went below 0 Hz, e.g. SPAN on a new band: the frequency printer wrote past its buffer for values from 1 THz (issue #175) --- chprintf.c | 7 ++++++- ui.c | 29 +++++++++++------------------ 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/chprintf.c b/chprintf.c index 6bba7fe..4dfcc90 100644 --- a/chprintf.c +++ b/chprintf.c @@ -87,6 +87,11 @@ static char *long_to_string_with_divisor(char *p, // default prescision = 13 // g.mmm kkk hhh #define MAX_FREQ_PRESCISION 15 +#ifdef TINYSA4 +#define FREQ_DIGITS 23 // any 64 bit value: 20 digits and 3 separators, fits tmpbuf[2*MAX_FILLER + 1] +#else +#define FREQ_DIGITS MAX_FREQ_PRESCISION +#endif #define FREQ_PSET 1 #define FREQ_NO_SPACE 2 #define FREQ_PREFIX_SPACE 4 @@ -99,7 +104,7 @@ ulong_freq(char *p, ulong_t freq, uint32_t width, uint32_t precision) if (/* precision == 0 || */ precision > MAX_FREQ_PRESCISION) precision = MAX_FREQ_PRESCISION; - char *q = p + MAX_FREQ_PRESCISION; + char *q = p + FREQ_DIGITS; // digits are written backwards from here char *b = q; // Prefix counter uint32_t s = 0; diff --git a/ui.c b/ui.c index 9c71f9b..6689c5b 100644 --- a/ui.c +++ b/ui.c @@ -5945,7 +5945,7 @@ static void fetch_numeric_target(uint8_t mode) plot_printf(uistat.text, sizeof uistat.text, out_format, uistat.freq_value); break; case KM_BAND_SPAN: - uistat.freq_value = abs((long)setting.bands[active_band].end-(long)setting.bands[active_band].start); + uistat.freq_value = setting.bands[active_band].end - setting.bands[active_band].start; #ifdef TINYSA4 if (uistat.freq_value > 990000000UL) out_format = "%.6Q"; @@ -6233,28 +6233,21 @@ set_numeric_value(void) // BANDs_update(); break; case KM_BAND_CENTER: + case KM_BAND_SPAN: { - freq_t span = abs((long)setting.bands[active_band].end - (long)setting.bands[active_band].start); - freq_t center = uistat.freq_value - (setting.frequency_offset - FREQUENCY_SHIFT); - setting.bands[active_band].start = center - span/2; - setting.bands[active_band].end = center + span/2; + band_t *b = &setting.bands[active_band]; + freq_t center = (b->end + b->start)/2; + freq_t span = b->end - b->start; // START and STOP keep start <= end + if (keypad_mode == KM_BAND_CENTER) + center = uistat.freq_value - (setting.frequency_offset - FREQUENCY_SHIFT); + else + span = uistat.freq_value; + b->start = center > span/2 ? center - span/2 : 0; // unsigned, a new band (0..0) with a span went "below 0 Hz" + b->end = b->start + span; update_frequencies(); dirty = true; update_grid(); } -// BANDs_update(); - break; - case KM_BAND_SPAN: - { - freq_t span = uistat.freq_value; - freq_t center = (setting.bands[active_band].end + setting.bands[active_band].start)/2; - setting.bands[active_band].start = center - span/2; - setting.bands[active_band].end = center + span/2; - update_frequencies(); - dirty = true; - update_grid(); -// BANDs_update(); - } break; case KM_BAND_LEVEL: setting.bands[active_band].level = to_dBm(uistat.value); From ab678dcc8ce8e686d37f0e169336eaca73363114 Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Mon, 9 Nov 2026 14:42:14 +0100 Subject: [PATCH 09/11] Screen capture always runs in the sweep thread, also while a screen waits for the user: capturing the version screen hung the tinySA Ultra and the calibration end screen gave "Command timeout" --- main.c | 90 ++++++++++++++++++++++++++++++------------------------- nanovna.h | 2 +- ui.c | 2 ++ 3 files changed, 53 insertions(+), 41 deletions(-) diff --git a/main.c b/main.c index f4cbcfb..59a0f98 100644 --- a/main.c +++ b/main.c @@ -63,6 +63,7 @@ char shell_line[VNA_SHELL_MAX_LENGTH]; char *shell_args[VNA_SHELL_MAX_ARGUMENTS + 1]; uint16_t shell_nargs; static volatile vna_shellcmd_t shell_function = 0; +static volatile bool shell_function_ui; // shell_function may also run in a UI wait loop (CMD_RUN_IN_UI) //#define ENABLED_DUMP // Allow get threads debug info @@ -143,6 +144,49 @@ systime_t restart_interval = 0; static THD_WORKING_AREA(waThread1, 768); bool has_esd = false; #endif +// Run the console command the shell thread handed over. Always in the sweep thread, so a command +// never uses the LCD at the same time as the screen (two threads on the SPI DMA hang the tinySA). +// ui_wait: called from a loop that waits for the user (keypad, version screen, "touch to continue"), +// there only a CMD_RUN_IN_UI command (capture) runs, the others wait for the sweep loop. +void shell_run_pending(bool ui_wait) +{ + chSysLock(); + vna_shellcmd_t local_func = shell_function; + if (ui_wait && !shell_function_ui) + local_func = 0; + else { + shell_function = 0; + shell_function_ui = false; + // The shell sets OP_CONSOLE together with shell_function, a request without a + // command (direct command, timed out command) has done its job: the sweep stopped + operation_requested &= ~OP_CONSOLE; + } + chSysUnlock(); + + if (local_func) { + if (!ui_wait) // a UI wait loop keeps a pending touch or lever event + operation_requested = OP_NONE; // otherwise commands will be aborted + local_func(shell_nargs - 1, &shell_args[1]); + + if (ui_wait || operation_requested == OP_NONE) // Don't prompt if aborted + shell_printf(VNA_SHELL_PROMPT_STR); + + // Resume shell thread + if (!abort_enabled) { + chSysLock(); + osalThreadDequeueNextI(&shell_thread, MSG_OK); + chSysUnlock(); + } + + if (dirty && !ui_wait) { // not over the screen that waits + if (MODE_OUTPUT(setting.mode)) + draw_menu(); // update screen if in output mode and dirty + else + redraw_request |= REDRAW_CAL_STATUS | REDRAW_AREA | REDRAW_FREQUENCY; + } + } +} + static THD_FUNCTION(Thread1, arg) { (void)arg; @@ -262,38 +306,7 @@ static THD_FUNCTION(Thread1, arg) __WFI(); } // STOP_PROFILE - // Run Shell command in sweep thread - vna_shellcmd_t local_func; - - chSysLock(); - local_func = shell_function; - shell_function = 0; - // The shell sets OP_CONSOLE together with shell_function, a request without a - // command (direct command, timed out command) has done its job: the sweep stopped - operation_requested &= ~OP_CONSOLE; - chSysUnlock(); - - if (local_func) { - operation_requested = OP_NONE; // otherwise commands will be aborted - local_func(shell_nargs - 1, &shell_args[1]); - - if (operation_requested == OP_NONE) // Don't prompt if aborted - shell_printf(VNA_SHELL_PROMPT_STR); - - // Resume shell thread - if (!abort_enabled) { - chSysLock(); - osalThreadDequeueNextI(&shell_thread, MSG_OK); - chSysUnlock(); - } - - if (dirty) { - if (MODE_OUTPUT(setting.mode)) - draw_menu(); // update screen if in output mode and dirty - else - redraw_request |= REDRAW_CAL_STATUS | REDRAW_AREA | REDRAW_FREQUENCY; - } - } + shell_run_pending(false); #ifdef __USE_USB_MSC__ // "usbdisk on" from the shell if (msc_enter_request) { @@ -304,11 +317,8 @@ static THD_FUNCTION(Thread1, arg) #endif // START_PROFILE // Process UI inputs - if (!(sweep_mode & SWEEP_SELFTEST)) { - sweep_mode|= SWEEP_UI_MODE; + if (!(sweep_mode & SWEEP_SELFTEST)) ui_process(); - sweep_mode&=~SWEEP_UI_MODE; - } // Process collected data, calculate trace coordinates and plot only if scan // completed if (completed) { @@ -3193,8 +3203,6 @@ static void VNAShell_executeLine(char *line) const VNAShellCommand *scp = VNAShell_parceLine(line); if (scp) { uint16_t cmd_flag = scp->flags; - // Skip wait mutex if process UI - if ((cmd_flag & CMD_RUN_IN_UI) && (sweep_mode&SWEEP_UI_MODE)) cmd_flag&=~CMD_WAIT_MUTEX; #ifdef __USE_USB_MSC__ // In USB DISK mode SPI1 and spi_buffer belong to the mass storage, only run commands that do not use them. // shell_direct_cmd tells usb_disk_mode() to wait for a command already running in this thread. @@ -3217,13 +3225,15 @@ static void VNAShell_executeLine(char *line) return; } #endif - // The sweep thread stays in an open keypad until it closes: answer at once instead of a 20 s timeout - if ((cmd_flag & CMD_WAIT_MUTEX) && keypad_open) { + // The sweep thread stays in an open keypad until it closes: answer at once instead of a 20 s timeout. + // A CMD_RUN_IN_UI command (capture) runs there in shell_run_pending(). + if ((cmd_flag & (CMD_WAIT_MUTEX | CMD_RUN_IN_UI)) == CMD_WAIT_MUTEX && keypad_open) { shell_printf("keypad active" VNA_SHELL_NEWLINE_STR VNA_SHELL_PROMPT_STR); return; } if (cmd_flag & CMD_WAIT_MUTEX) { chSysLock(); + shell_function_ui = cmd_flag & CMD_RUN_IN_UI; shell_function = scp->sc_function; operation_requested|=OP_CONSOLE; // this will abort current sweep to give priority to the new request chSysUnlock(); diff --git a/nanovna.h b/nanovna.h index 810ff8b..03b06f0 100644 --- a/nanovna.h +++ b/nanovna.h @@ -293,6 +293,7 @@ void my_veryfast_delay(int t); float my_atof(const char *p); freq_t my_atoui(const char *p); int shell_printf(const char *fmt, ...); +void shell_run_pending(bool ui_wait); // run a console command in the sweep thread int usage_printf(const char *fmt, ...); void clear_backup(void); const char *get_hw_version_text(void); @@ -380,7 +381,6 @@ enum { //#define SWEEP_FACTORY 0x20 #endif #define SWEEP_CALIBRATE_HARMONIC 0x40 -#define SWEEP_UI_MODE 0x80 extern uint8_t sweep_mode; extern uint8_t completed; diff --git a/ui.c b/ui.c index 6689c5b..bd1c030 100644 --- a/ui.c +++ b/ui.c @@ -212,6 +212,7 @@ int btn_side(void) static int btn_check(void) { systime_t ticks; + shell_run_pending(true); // capture while a screen waits for the jog button #ifdef __WATCHDOG__ wdgReset(&WDGD1); #endif @@ -402,6 +403,7 @@ static void touch_init(void){ static int touch_check(void) { + shell_run_pending(true); // capture while a screen waits for a touch #ifdef TINYSA4 if (setting.lock_display) return EVT_TOUCH_NONE; From efb24ec1dd6646656ce16a4cb091bc7ed1e2d14b Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Mon, 9 Nov 2026 16:03:11 +0100 Subject: [PATCH 10/11] Level calibration: "Touch screen to continue" is visible on the tinySA (BASIC), its message lines start at y 180 instead of 200 so the last one fits the 240 pixel screen --- sa_core.c | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/sa_core.c b/sa_core.c index 44511d2..f022447 100644 --- a/sa_core.c +++ b/sa_core.c @@ -7957,6 +7957,14 @@ void calibrate_harmonic(void) } #endif +// First of the three 20 pixel message lines at the end of the calibration, the last one +// ("Touch screen to continue") must be on the 240 pixel screen of the tinySA +#ifdef TINYSA4 +#define CAL_MSG_Y 200 +#else +#define CAL_MSG_Y 180 +#endif + void calibrate(void) { int local_test_status; @@ -8209,14 +8217,14 @@ void calibrate(void) low_level: #endif ili9341_set_foreground(LCD_BRIGHT_COLOR_RED); - ili9341_drawstring_7x13("Signal level incorrect", 30, 200); - ili9341_drawstring_7x13("Check cable between connectors", 30, 220); + ili9341_drawstring_7x13("Signal level incorrect", 30, CAL_MSG_Y); + ili9341_drawstring_7x13("Check cable between connectors", 30, CAL_MSG_Y + 20); goto quit; } // chThdSleepMilliseconds(1000); if (local_test_status != TS_PASS) { ili9341_set_foreground(LCD_BRIGHT_COLOR_RED); - ili9341_drawstring_7x13("Calibration failed", 30, 220); + ili9341_drawstring_7x13("Calibration failed", 30, CAL_MSG_Y + 20); goto quit; } #ifdef TINYSA4 @@ -8296,12 +8304,12 @@ low_level: #endif config_save(); ili9341_set_foreground(LCD_BRIGHT_COLOR_GREEN); - ili9341_drawstring_7x13("Calibration complete", 40, 220); + ili9341_drawstring_7x13("Calibration complete", 40, CAL_MSG_Y + 20); quit: #ifdef TINYSA4 config.ultra = old_ultra; #endif - ili9341_drawstring_7x13("Touch screen to continue", 40, 240); + ili9341_drawstring_7x13("Touch screen to continue", 40, CAL_MSG_Y + 40); wait_user(); ili9341_clear_screen(); set_sweep_points(old_sweep_points); From d1ad555f40c4b5acd1efba725ce4d41feedf0097 Mon Sep 17 00:00:00 2001 From: erikkaashoek Date: Mon, 9 Nov 2026 16:17:08 +0100 Subject: [PATCH 11/11] Changelog: V1.4302 P3 issues #98 #153 #62 #124 #89 #169 #100 #175, screen capture fix --- changelog.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/changelog.txt b/changelog.txt index 12bd110..288c581 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,15 @@ +V1.4302 2026-11-09 For all tinySA models +- Signal generator: PRESET in the generator menu loads and stores presets (on the tinySA Ultra also from and to SD). A loaded preset opens the menu of its own mode, and a signal generator preset loaded from the menu starts with the output off +- The SW button of the signal generator shows the start and stop frequency of the sweep (Ultra only) +- STORAGE > MORE > SD CARD ICON > REPEAT: with a repeat time set, touching the SD card icon starts saving every interval (with automatic file names) and touching it again stops, the icon is red while it repeats (Ultra only) +- MARKER > SEARCH MARKER > TRACKING L->R: the tracking markers stay on the highest peaks in frequency order from left to right instead of changing places (Ultra only) +- CONFIG > MORE > EXPERT CONFIG > SHOW BANDS: with MULTI BAND off the bands of the band table (e.g. loaded from a .bnd file) are highlighted with their names (Ultra only) +- File browser: REN renames a file, the name keypad starts with the current name (Ultra only) +- Menu buttons that open a submenu show a small arrow in the top right corner +- Fixed a crash when a band frequency went below 0 Hz, e.g. after entering only a SPAN for a new band: such a band now starts at 0 Hz. A band wider than 2.1 GHz also showed a wrong SPAN and changed its width when its CENTER was set (Ultra only) +- Screen capture ("capture" console command) no longer draws at the same time as the tinySA itself: capturing the VERSION screen hung the tinySA Ultra, the end screen of the level calibration gave "Command timeout" +- Level calibration: "Touch screen to continue" was drawn below the screen (tinySA basic only) + V1.4290 2026-11-02 For all tinySA models - Recalling a preset turns NORMALIZE off: the normalization reference trace is not stored in a preset, so the normalized trace was wrong - Trace CSV files load with their own start and stop frequency again: with CONFIG > MHz CSV (the default) the frequencies were read as Hz (Ultra only)