From 9c21e1b71414780fe6cbaaa32d8e4eceb001457f Mon Sep 17 00:00:00 2001 From: Luca Toniolo Date: Sat, 17 Jan 2026 20:44:28 +0800 Subject: [PATCH 1/4] the scope gets double the samples, and starts with full Pos not half --- src/hal/utils/scope_rt.c | 2 +- src/hal/utils/scope_shm.h | 2 +- src/hal/utils/scope_trig.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hal/utils/scope_rt.c b/src/hal/utils/scope_rt.c index d3c9efe6d97..34b9f2aa72e 100644 --- a/src/hal/utils/scope_rt.c +++ b/src/hal/utils/scope_rt.c @@ -44,7 +44,7 @@ MODULE_AUTHOR("John Kasunich"); MODULE_DESCRIPTION("Oscilloscope for EMC HAL"); MODULE_LICENSE("GPL"); -long num_samples = 16000; +long num_samples = 32000; long shm_size; RTAPI_MP_LONG(num_samples, "Number of samples in the shared memory block"); diff --git a/src/hal/utils/scope_shm.h b/src/hal/utils/scope_shm.h index a40a9d7814b..b5d24902bb0 100644 --- a/src/hal/utils/scope_shm.h +++ b/src/hal/utils/scope_shm.h @@ -42,7 +42,7 @@ ************************************************************************/ #define SCOPE_SHM_KEY 0x130CF406 -#define SCOPE_NUM_SAMPLES_DEFAULT 16000 +#define SCOPE_NUM_SAMPLES_DEFAULT 32000 typedef enum { IDLE = 0, /* waiting for run command */ diff --git a/src/hal/utils/scope_trig.c b/src/hal/utils/scope_trig.c index a3134b05c87..8c757c6165f 100644 --- a/src/hal/utils/scope_trig.c +++ b/src/hal/utils/scope_trig.c @@ -256,7 +256,7 @@ static void init_trigger_info_window(void) vbox = gtk_vbox_new_in_box(FALSE, 0, 0, hbox, TRUE, TRUE, 0); gtk_label_new_in_box(_("Pos"), vbox, FALSE, FALSE, 0); trig->pos_adj = - gtk_adjustment_new(TRIG_POS_RESOLUTION / 2, 0, TRIG_POS_RESOLUTION, 1, + gtk_adjustment_new(TRIG_POS_RESOLUTION, 0, TRIG_POS_RESOLUTION, 1, 1, 0); trig->pos_slider = gtk_scale_new( GTK_ORIENTATION_VERTICAL, GTK_ADJUSTMENT(trig->pos_adj)); From c2a4427e854705bc0d413031e9cd6633b150174c Mon Sep 17 00:00:00 2001 From: Luca Toniolo Date: Tue, 20 Jan 2026 23:00:09 +0800 Subject: [PATCH 2/4] halscope: add configurable sample memory and simplify UI - Add GUI control for sample count in acquire dialog with config file persistence (SAMPLES command read before scope_rt loads) - Always enable all 16 channels - remove confusing record length radio buttons that forced tradeoff between channels and samples - Initialize sample_len and rec_len in init_horiz() so sampling works immediately without opening acquire dialog - Add bounds checking for num_samples (min 1000, max 1000000) - Increase default window size to 1050x550 for better channel display - Show restart message when sample count changed (requires reload) --- src/hal/utils/scope.c | 63 ++++++++++++- src/hal/utils/scope_files.c | 21 +++-- src/hal/utils/scope_horiz.c | 180 +++++++++++++----------------------- src/hal/utils/scope_rt.c | 13 +++ src/hal/utils/scope_shm.h | 2 + src/hal/utils/scope_usr.h | 3 + src/hal/utils/scope_vert.c | 38 -------- 7 files changed, 159 insertions(+), 161 deletions(-) diff --git a/src/hal/utils/scope.c b/src/hal/utils/scope.c index 7c5508b709b..5b7a31850e1 100644 --- a/src/hal/utils/scope.c +++ b/src/hal/utils/scope.c @@ -99,6 +99,28 @@ static void exit_on_signal(int signum) { exit_from_hal(); exit(1); } + +/* Read just the SAMPLES value from config file before loading scope_rt */ +static int read_samples_from_config(const char *filename) +{ + FILE *fp; + char buf[100]; + int samples = 0; + + fp = fopen(filename, "r"); + if (fp == NULL) { + return 0; /* file doesn't exist, use default */ + } + while (fgets(buf, sizeof(buf), fp) != NULL) { + if (strncasecmp(buf, "SAMPLES ", 8) == 0) { + samples = atoi(buf + 8); + break; + } + } + fclose(fp); + return samples; +} + /*********************************************************************** * MAIN() FUNCTION * ************************************************************************/ @@ -141,9 +163,30 @@ int main(int argc, gchar * argv[]) break; } } - if(argc > optind) num_samples = atoi(argv[argc-1]); + /* first try to read samples from config file */ + num_samples = read_samples_from_config(ifilename); + /* command line num_samples overrides config file, but only if it's a valid number */ + if(argc > optind) { + int cmdline_samples = atoi(argv[optind]); + if(cmdline_samples > 0) { + num_samples = cmdline_samples; + } + } + /* apply defaults and bounds */ if(num_samples <= 0) num_samples = SCOPE_NUM_SAMPLES_DEFAULT; + if(num_samples < SCOPE_NUM_SAMPLES_MIN) { + rtapi_print_msg(RTAPI_MSG_WARN, + "SCOPE: num_samples %d too small, using %d\n", + num_samples, SCOPE_NUM_SAMPLES_MIN); + num_samples = SCOPE_NUM_SAMPLES_MIN; + } + if(num_samples > SCOPE_NUM_SAMPLES_MAX) { + rtapi_print_msg(RTAPI_MSG_WARN, + "SCOPE: num_samples %d too large, using %d\n", + num_samples, SCOPE_NUM_SAMPLES_MAX); + num_samples = SCOPE_NUM_SAMPLES_MAX; + } /* connect to the HAL */ comp_id = hal_init("halscope"); @@ -161,6 +204,10 @@ int main(int argc, gchar * argv[]) hal_exit(comp_id); exit(1); } + } else { + /* scope_rt already loaded - we'll check if sample count matches later */ + rtapi_print_msg(RTAPI_MSG_DBG, + "SCOPE: scope_rt already loaded, requested %d samples\n", num_samples); } /* set up a shared memory region for the scope data */ shm_id = rtapi_shmem_new(SCOPE_SHM_KEY, comp_id, sizeof(scope_shm_control_t)); @@ -191,6 +238,16 @@ int main(int argc, gchar * argv[]) ctrl_usr = &ctrl_struct; init_usr_control_struct(shm_base); + /* check if loaded scope_rt has different sample count than requested */ + if (ctrl_shm->buf_len != num_samples) { + rtapi_print_msg(RTAPI_MSG_WARN, + "SCOPE: scope_rt was loaded with %d samples, but config requested %d.\n" + "To change sample count, unload scope_rt first or restart LinuxCNC.\n", + ctrl_shm->buf_len, num_samples); + } + /* store requested samples for saving to config */ + ctrl_usr->horiz.requested_samples = num_samples; + /* init watchdog */ ctrl_shm->watchdog = 10; /* set main window */ @@ -626,9 +683,9 @@ static void define_scope_windows(void) { GtkWidget *vbox, *hbox, *vboxtop, *vboxbottom, *vboxleft, *vboxright, *hboxright; - /* create main window, set its minimum size and title */ + /* create main window, set its default size and title */ ctrl_usr->main_win = gtk_window_new(GTK_WINDOW_TOPLEVEL); - gtk_widget_set_size_request(GTK_WIDGET(ctrl_usr->main_win), 650, 400); + gtk_window_set_default_size(GTK_WINDOW(ctrl_usr->main_win), 1050, 550); gtk_window_set_title(GTK_WINDOW(ctrl_usr->main_win), _("HAL Oscilloscope")); /* top level - big vbox, menu above, everything else below */ diff --git a/src/hal/utils/scope_files.c b/src/hal/utils/scope_files.c index 92b7a908d49..1d8c7d7a07e 100644 --- a/src/hal/utils/scope_files.c +++ b/src/hal/utils/scope_files.c @@ -144,8 +144,11 @@ static char *rmode_cmd(void * arg); * LOCAL VARIABLES * ************************************************************************/ -static const cmd_lut_entry_t cmd_lut[25] = +static char *samples_cmd(void * arg); + +static const cmd_lut_entry_t cmd_lut[26] = { + { "samples", INT, samples_cmd }, { "thread", STRING, thread_cmd }, { "maxchan", INT, maxchan_cmd }, { "hmult", INT, hmult_cmd }, @@ -453,13 +456,19 @@ static char *thread_cmd(void * arg) static char *maxchan_cmd(void * arg) { - int *argp, rv; + /* maxchan is now ignored - we always use 16 channels */ + /* kept for backwards compatibility with old config files */ + (void)arg; + return NULL; +} +static char *samples_cmd(void * arg) +{ + int *argp; + /* SAMPLES is handled early in main() before scope_rt is loaded */ + /* Here we just store it in requested_samples so it gets saved back */ argp = (int *)(arg); - rv = set_rec_len(*argp); - if ( rv < 0 ) { - return "could not set record length"; - } + ctrl_usr->horiz.requested_samples = *argp; return NULL; } diff --git a/src/hal/utils/scope_horiz.c b/src/hal/utils/scope_horiz.c index ce1552c305c..e6c30455b67 100644 --- a/src/hal/utils/scope_horiz.c +++ b/src/hal/utils/scope_horiz.c @@ -86,7 +86,6 @@ static void deactivate_sample_thread(void); static void mult_changed(GtkAdjustment * adj, gpointer gdata); static void zoom_changed(GtkAdjustment * adj, gpointer gdata); static void pos_changed(GtkAdjustment * adj, gpointer gdata); -static void rec_len_button(GtkWidget * widget, gpointer gdata); static void calc_horiz_scaling(void); @@ -113,6 +112,10 @@ void init_horiz(void) { /* stop sampling */ ctrl_shm->state = IDLE; + /* always use 16 channels - initialize sample_len and rec_len + so sampling works without opening the acquire dialog */ + ctrl_shm->sample_len = 16; + ctrl_shm->rec_len = ctrl_shm->buf_len / 16; /* init non-zero members of the horizontal structure */ /* set up the window */ init_horiz_window(); @@ -310,10 +313,15 @@ void refresh_state_info(void) void write_horiz_config(FILE *fp) { scope_horiz_t *horiz; + int samples_to_save; horiz = &(ctrl_usr->horiz); + /* SAMPLES must be first - it's read early before scope_rt loads */ + /* save requested_samples if set, otherwise current buf_len */ + samples_to_save = (horiz->requested_samples > 0) ? + horiz->requested_samples : ctrl_shm->buf_len; + fprintf(fp, "SAMPLES %d\n", samples_to_save); fprintf(fp, "THREAD %s\n", horiz->thread_name); - fprintf(fp, "MAXCHAN %d\n", ctrl_shm->sample_len); fprintf(fp, "HMULT %d\n", ctrl_shm->mult); fprintf(fp, "HZOOM %d\n", horiz->zoom_setting); fprintf(fp, "HPOS %e\n", horiz->pos_setting); @@ -340,33 +348,11 @@ int set_sample_thread(char *name) int set_rec_len(int setting) { - int count, n; - - switch ( setting ) { - case 1: - case 2: - case 4: - case 8: - case 16: - /* acceptable value */ - break; - default: - /* bad value */ - return -1; - } - /* count enabled channels */ - count = 0; - for (n = 0; n < 16; n++) { - if (ctrl_usr->vert.chan_enabled[n]) { - count++; - } - } - if (count > setting) { - /* too many channels already enabled */ - return -1; - } - ctrl_shm->sample_len = setting; - ctrl_shm->rec_len = ctrl_shm->buf_len / ctrl_shm->sample_len; + /* This function is kept for backwards compatibility */ + /* We now always use 16 channels, setting is ignored */ + (void)setting; + ctrl_shm->sample_len = 16; + ctrl_shm->rec_len = ctrl_shm->buf_len / 16; calc_horiz_scaling(); refresh_horiz_info(); return 0; @@ -507,7 +493,6 @@ static void dialog_realtime_not_linked(void) GtkWidget *hbox, *label; GtkWidget *content_area; GtkWidget *dialog; - GtkWidget *buttons[5]; GtkWidget *scrolled_window; GtkTreeSelection *selection; @@ -653,65 +638,31 @@ static void dialog_realtime_not_linked(void) gtk_box_pack_start(GTK_BOX(GTK_CONTAINER(content_area)), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL), FALSE, FALSE , 0); - /* box for record length buttons */ - label = gtk_label_new(_("Record Length")); - gtk_box_pack_start(GTK_BOX(GTK_CONTAINER(content_area)), - label, TRUE, TRUE, 0); - - /* now define the radio buttons */ - snprintf(buf, BUFLEN, _("%5d samples (1 channel)"), ctrl_shm->buf_len); - buttons[0] = gtk_radio_button_new_with_label(NULL, buf); - snprintf(buf, BUFLEN, _("%5d samples (2 channels)"), ctrl_shm->buf_len / 2); - buttons[1] = - gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(buttons - [0]), buf); - snprintf(buf, BUFLEN, _("%5d samples (4 channels)"), ctrl_shm->buf_len / 4); - buttons[2] = - gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(buttons - [0]), buf); - snprintf(buf, BUFLEN, _("%5d samples (8 channels)"), ctrl_shm->buf_len / 8); - buttons[3] = - gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(buttons - [0]), buf); - snprintf(buf, BUFLEN, _("%5d samples (16 channels)"), - ctrl_shm->buf_len / 16); - buttons[4] = - gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON(buttons - [0]), buf); - /* now put them into the box and make visible */ - for (n = 0; n < 5; n++) { - gtk_box_pack_start(GTK_BOX(GTK_CONTAINER(content_area)), - buttons[n], FALSE, FALSE, 0); - } - /* determine which button should be pressed by default */ - if (ctrl_shm->sample_len == 1) { - n = 0; - } else if (ctrl_shm->sample_len == 2) { - n = 1; - } else if (ctrl_shm->sample_len == 4) { - n = 2; - } else if (ctrl_shm->sample_len == 8) { - n = 3; - } else if (ctrl_shm->sample_len == 16) { - n = 4; - } else { - n = 2; - ctrl_shm->sample_len = 4; - ctrl_shm->rec_len = ctrl_shm->buf_len / ctrl_shm->sample_len; + /* Samples setting - all 16 channels always available */ + hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); + gtk_box_set_homogeneous(GTK_BOX(hbox), TRUE); + gtk_label_new_in_box(_("Samples (16 channels):"), hbox, FALSE, FALSE, 0); + /* initialize requested_samples from current if not set */ + if (horiz->requested_samples <= 0) { + horiz->requested_samples = ctrl_shm->buf_len; } - /* set the default button */ - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(buttons[n]), TRUE); - /* set up callbacks for the buttons */ - g_signal_connect(buttons[0], "clicked", - G_CALLBACK(rec_len_button), (gpointer) 1); - g_signal_connect(buttons[1], "clicked", - G_CALLBACK(rec_len_button), (gpointer) 2); - g_signal_connect(buttons[2], "clicked", - G_CALLBACK(rec_len_button), (gpointer) 4); - g_signal_connect(buttons[3], "clicked", - G_CALLBACK(rec_len_button), (gpointer) 8); - g_signal_connect(buttons[4], "clicked", - G_CALLBACK(rec_len_button), (gpointer) 16); + horiz->samples_adj = gtk_adjustment_new(horiz->requested_samples, + SCOPE_NUM_SAMPLES_MIN, SCOPE_NUM_SAMPLES_MAX, 1000, 10000, 0); + horiz->samples_spinbutton = + gtk_spin_button_new(GTK_ADJUSTMENT(horiz->samples_adj), 1000, 0); + gtk_box_pack_start(GTK_BOX(hbox), horiz->samples_spinbutton, FALSE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(GTK_CONTAINER(content_area)), + hbox, FALSE, TRUE, 0); + /* show current record length info */ + snprintf(buf, BUFLEN, _("Current: %d samples (%d per channel)"), + ctrl_shm->buf_len, ctrl_shm->buf_len / 16); + label = gtk_label_new(buf); + gtk_box_pack_start(GTK_BOX(GTK_CONTAINER(content_area)), + label, FALSE, TRUE, 0); + + /* always use 16 channels */ + ctrl_shm->sample_len = 16; + ctrl_shm->rec_len = ctrl_shm->buf_len / 16; /* was a thread previously used? */ if (sel_row > -1) { @@ -721,6 +672,31 @@ static void dialog_realtime_not_linked(void) gtk_widget_show_all(dialog); retval = gtk_dialog_run(GTK_DIALOG(dialog)); + + /* save requested samples before destroying dialog */ + if (retval == GTK_RESPONSE_OK) { + int new_samples = gtk_spin_button_get_value_as_int( + GTK_SPIN_BUTTON(horiz->samples_spinbutton)); + horiz->requested_samples = new_samples; + if (new_samples != ctrl_shm->buf_len) { + /* samples changed, show restart message */ + GtkWidget *info_dialog = gtk_message_dialog_new( + GTK_WINDOW(ctrl_usr->main_win), + GTK_DIALOG_MODAL, + GTK_MESSAGE_INFO, + GTK_BUTTONS_OK, + _("Sample count changed")); + gtk_message_dialog_format_secondary_text( + GTK_MESSAGE_DIALOG(info_dialog), + _("The new sample count (%d) will take effect\n" + "the next time halscope is started.\n\n" + "The setting has been saved to the configuration file."), + new_samples); + gtk_dialog_run(GTK_DIALOG(info_dialog)); + gtk_widget_destroy(info_dialog); + } + } + gtk_widget_destroy(dialog); /* these items no longer exist - NULL them */ @@ -730,6 +706,8 @@ static void dialog_realtime_not_linked(void) horiz->sample_period_label = NULL; horiz->mult_adj = NULL; horiz->mult_spinbutton = NULL; + horiz->samples_adj = NULL; + horiz->samples_spinbutton = NULL; /* we get here when the user hits OK or Quit */ if (retval == GTK_RESPONSE_CLOSE) { @@ -963,32 +941,6 @@ static void pos_changed(GtkAdjustment * adj, gpointer gdata) set_horiz_pos(gtk_adjustment_get_value(adj) / 1000.0); } -static void rec_len_button(GtkWidget * widget, gpointer gdata) -{ - int retval; - GtkWidget *dialog; - - if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget)) != TRUE) { - /* not pressed, ignore it */ - return; - } - retval = set_rec_len((long)gdata); - if (retval < 0) { - /* too many channels already enabled */ - dialog = gtk_message_dialog_new(GTK_WINDOW(ctrl_usr->main_win), - GTK_DIALOG_MODAL, - GTK_MESSAGE_INFO, - GTK_BUTTONS_OK, - _("Not enough channels")); - gtk_message_dialog_format_secondary_text( - GTK_MESSAGE_DIALOG(dialog), - _("This record length cannot handle the channels\n" - "that are currently enabled. Pick a shorter\n" - "record length that supports more channels.")); - gtk_dialog_run(GTK_DIALOG(dialog)); - gtk_widget_destroy(dialog); - } -} static void calc_horiz_scaling(void) { diff --git a/src/hal/utils/scope_rt.c b/src/hal/utils/scope_rt.c index 34b9f2aa72e..ee0572ea166 100644 --- a/src/hal/utils/scope_rt.c +++ b/src/hal/utils/scope_rt.c @@ -89,6 +89,19 @@ int rtapi_app_main(void) rtapi_print_msg(RTAPI_MSG_ERR, "SCOPE: ERROR: hal_init() failed\n"); return -1; } + /* sanity check num_samples */ + if (num_samples < SCOPE_NUM_SAMPLES_MIN) { + rtapi_print_msg(RTAPI_MSG_WARN, + "SCOPE_RT: num_samples %ld too small, using %d\n", + num_samples, SCOPE_NUM_SAMPLES_MIN); + num_samples = SCOPE_NUM_SAMPLES_MIN; + } + if (num_samples > SCOPE_NUM_SAMPLES_MAX) { + rtapi_print_msg(RTAPI_MSG_WARN, + "SCOPE_RT: num_samples %ld too large, using %d\n", + num_samples, SCOPE_NUM_SAMPLES_MAX); + num_samples = SCOPE_NUM_SAMPLES_MAX; + } /* connect to scope shared memory block */ skip = (sizeof(scope_shm_control_t) + 3) & ~3; shm_size = skip + num_samples * sizeof(scope_data_t); diff --git a/src/hal/utils/scope_shm.h b/src/hal/utils/scope_shm.h index b5d24902bb0..34dbb76be65 100644 --- a/src/hal/utils/scope_shm.h +++ b/src/hal/utils/scope_shm.h @@ -43,6 +43,8 @@ #define SCOPE_SHM_KEY 0x130CF406 #define SCOPE_NUM_SAMPLES_DEFAULT 32000 +#define SCOPE_NUM_SAMPLES_MIN 1000 +#define SCOPE_NUM_SAMPLES_MAX 1000000 typedef enum { IDLE = 0, /* waiting for run command */ diff --git a/src/hal/utils/scope_usr.h b/src/hal/utils/scope_usr.h index d66262a510c..cbe09f5a2d2 100644 --- a/src/hal/utils/scope_usr.h +++ b/src/hal/utils/scope_usr.h @@ -77,6 +77,9 @@ typedef struct { GtkWidget *sample_period_label; GtkAdjustment *mult_adj; GtkWidget *mult_spinbutton; + GtkAdjustment *samples_adj; + GtkWidget *samples_spinbutton; + int requested_samples; /* samples requested for next restart */ } scope_horiz_t; /* this struct holds control data related to a single channel */ diff --git a/src/hal/utils/scope_vert.c b/src/hal/utils/scope_vert.c index d82219a085d..227b5c97d01 100644 --- a/src/hal/utils/scope_vert.c +++ b/src/hal/utils/scope_vert.c @@ -133,7 +133,6 @@ void init_vert(void) int set_active_channel(int chan_num) { - int n, count; scope_vert_t *vert; scope_chan_t *chan; if (( chan_num < 1 ) || ( chan_num > 16 )) { @@ -148,16 +147,6 @@ int set_active_channel(int chan_num) /* acquisition in progress, must restart it */ prepare_scope_restart(); } - count = 0; - for (n = 0; n < 16; n++) { - if (vert->chan_enabled[n]) { - count++; - } - } - if (count >= ctrl_shm->sample_len) { - /* max number of channels already enabled */ - return -2; - } if (chan->name == NULL) { /* no signal source */ return -3; @@ -788,10 +777,8 @@ static void offset_activated(GtkEntry *entry, GtkWidget *dialog) static void chan_sel_button(GtkWidget * widget, gpointer gdata) { long chan_num; - int n, count; scope_vert_t *vert; scope_chan_t *chan; - GtkWidget *dialog; vert = &(ctrl_usr->vert); chan_num = (long) gdata; @@ -808,31 +795,6 @@ static void chan_sel_button(GtkWidget * widget, gpointer gdata) /* acquisition in progress, must restart it */ prepare_scope_restart(); } - count = 0; - for (n = 0; n < 16; n++) { - if (vert->chan_enabled[n]) { - count++; - } - } - if (count >= ctrl_shm->sample_len) { - /* max number of channels already enabled */ - /* force the button to pop back out */ - ignore_click = 1; - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), FALSE); - dialog = gtk_message_dialog_new(GTK_WINDOW(ctrl_usr->main_win), - GTK_DIALOG_MODAL, - GTK_MESSAGE_INFO, - GTK_BUTTONS_CLOSE, - _("Too many channels")); - gtk_message_dialog_format_secondary_text( - GTK_MESSAGE_DIALOG(dialog), - _("You cannot add another channel.\n\n" - "Either turn off one or more channels, or shorten\n" - "the record length to allow for more channels")); - gtk_dialog_run(GTK_DIALOG(dialog)); - gtk_widget_destroy(dialog); - return; - } if (chan->name == NULL) { /* need to assign a source */ From 13755b980b1eea3e31b735c7d47c290f45f561f0 Mon Sep 17 00:00:00 2001 From: Luca Toniolo Date: Wed, 21 Jan 2026 17:46:27 +0800 Subject: [PATCH 3/4] Parse new configs from a comment, to allow 2.9 halscope to open new configs --- src/hal/utils/scope.c | 5 +++++ src/hal/utils/scope_files.c | 5 ++++- src/hal/utils/scope_horiz.c | 7 ++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/hal/utils/scope.c b/src/hal/utils/scope.c index 5b7a31850e1..d611bf6805b 100644 --- a/src/hal/utils/scope.c +++ b/src/hal/utils/scope.c @@ -112,9 +112,14 @@ static int read_samples_from_config(const char *filename) return 0; /* file doesn't exist, use default */ } while (fgets(buf, sizeof(buf), fp) != NULL) { + /* Support both "SAMPLES nnn" and "# SAMPLES nnn" for backward compatibility */ + /* Older halscope versions will ignore "# SAMPLES" as a comment */ if (strncasecmp(buf, "SAMPLES ", 8) == 0) { samples = atoi(buf + 8); break; + } else if (strncasecmp(buf, "# SAMPLES ", 10) == 0) { + samples = atoi(buf + 10); + break; } } fclose(fp); diff --git a/src/hal/utils/scope_files.c b/src/hal/utils/scope_files.c index 1d8c7d7a07e..3b5f6f9ba4a 100644 --- a/src/hal/utils/scope_files.c +++ b/src/hal/utils/scope_files.c @@ -60,8 +60,9 @@ */ /* + # SAMPLES total sample buffer size (written as comment for compatibility) THREAD name of thread to sample in - MAXCHAN 1,2,4,8,16, maximum channel count + MAXCHAN 1,2,4,8,16, maximum channel count (ignored, kept for compatibility) HMULT multiplier, sample every N runs of thread HZOOM 1-9, horizontal zoom setting HPOS 0.0-1.0, horizontal position setting @@ -467,6 +468,8 @@ static char *samples_cmd(void * arg) int *argp; /* SAMPLES is handled early in main() before scope_rt is loaded */ /* Here we just store it in requested_samples so it gets saved back */ + /* This handles both "SAMPLES nnn" from config files */ + /* The "# SAMPLES nnn" comment version is handled by read_samples_from_config() */ argp = (int *)(arg); ctrl_usr->horiz.requested_samples = *argp; return NULL; diff --git a/src/hal/utils/scope_horiz.c b/src/hal/utils/scope_horiz.c index e6c30455b67..b2c2b013019 100644 --- a/src/hal/utils/scope_horiz.c +++ b/src/hal/utils/scope_horiz.c @@ -320,7 +320,12 @@ void write_horiz_config(FILE *fp) /* save requested_samples if set, otherwise current buf_len */ samples_to_save = (horiz->requested_samples > 0) ? horiz->requested_samples : ctrl_shm->buf_len; - fprintf(fp, "SAMPLES %d\n", samples_to_save); + /* Write SAMPLES as a comment for backward compatibility with old halscope */ + /* Old versions will ignore "# SAMPLES", new versions parse it in read_samples_from_config() */ + fprintf(fp, "# SAMPLES %d\n", samples_to_save); + /* Also write MAXCHAN for backward compatibility with old halscope */ + /* Old versions use MAXCHAN, new versions ignore it (always use 16 channels) */ + fprintf(fp, "MAXCHAN 16\n"); fprintf(fp, "THREAD %s\n", horiz->thread_name); fprintf(fp, "HMULT %d\n", ctrl_shm->mult); fprintf(fp, "HZOOM %d\n", horiz->zoom_setting); From 402a96dee1f0de184be3b0e7cde3e50f7725b47f Mon Sep 17 00:00:00 2001 From: Luca Toniolo Date: Wed, 21 Jan 2026 22:34:13 +0800 Subject: [PATCH 4/4] halscope open logs --- src/hal/utils/scope.c | 60 ++++- src/hal/utils/scope_files.c | 481 ++++++++++++++++++++++++++++++++++++ src/hal/utils/scope_usr.h | 4 + src/hal/utils/scope_vert.c | 3 + 4 files changed, 546 insertions(+), 2 deletions(-) diff --git a/src/hal/utils/scope.c b/src/hal/utils/scope.c index d611bf6805b..c6ba918bc83 100644 --- a/src/hal/utils/scope.c +++ b/src/hal/utils/scope.c @@ -340,6 +340,33 @@ void start_capture(void) /* already running! */ return; } + + /* Check if data is from log file */ + if (ctrl_usr->data_from_log_file) { + GtkWidget *dialog = gtk_message_dialog_new( + GTK_WINDOW(ctrl_usr->main_win), + GTK_DIALOG_MODAL, + GTK_MESSAGE_WARNING, + GTK_BUTTONS_OK_CANCEL, + _("Overwrite loaded log file data?")); + + gtk_message_dialog_format_secondary_text( + GTK_MESSAGE_DIALOG(dialog), + _("Starting acquisition will clear the data loaded from the CSV file. Continue?")); + + int response = gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + + if (response != GTK_RESPONSE_OK) { + /* User cancelled, switch back to STOP mode */ + set_run_mode(STOP); + return; + } + + /* Clear the flag */ + ctrl_usr->data_from_log_file = 0; + } + for (n = 0; n < 16; n++) { /* point to user space channel data */ chan = &(ctrl_usr->chan[n]); @@ -498,12 +525,41 @@ static void init_usr_control_struct(void *shmem) /* set all 16 channels to "no source assigned" */ for (n = 0; n < 16; n++) { ctrl_usr->chan[n].data_source_type = -1; + ctrl_usr->chan[n].is_phantom = 0; } + ctrl_usr->data_from_log_file = 0; /* done */ } static void menuitem_response(gchar *string) { - printf("%s\n", string); + if (strcmp(string, "file/open datafile") == 0) { + open_log_cb(GTK_WINDOW(ctrl_usr->main_win)); + } else { + printf("%s\n", string); + } +} + +void open_log_cb(GtkWindow *parent) +{ + GtkWidget *filew; + GtkFileChooser *chooser; + + filew = gtk_file_chooser_dialog_new(_("Open Log File:"), + parent, + GTK_FILE_CHOOSER_ACTION_OPEN, + _("_Cancel"), GTK_RESPONSE_CANCEL, + _("_Open"), GTK_RESPONSE_ACCEPT, + NULL); + + chooser = GTK_FILE_CHOOSER(filew); + set_file_filter(chooser, "Text CSV (.csv)", "*.csv"); + + if (gtk_dialog_run(GTK_DIALOG(filew)) == GTK_RESPONSE_ACCEPT) { + char *filename = gtk_file_chooser_get_filename(chooser); + read_log_file(filename); + g_free(filename); + } + gtk_widget_destroy(filew); } static void about(void) { @@ -608,7 +664,7 @@ static void define_menubar(GtkWidget *vboxtop) { gtk_menu_shell_append(GTK_MENU_SHELL(filemenu), fileopendatafile); g_signal_connect_swapped(fileopendatafile, "activate", G_CALLBACK(menuitem_response), "file/open datafile"); - gtk_widget_set_sensitive(GTK_WIDGET(fileopendatafile), FALSE); // XXX + gtk_widget_set_sensitive(GTK_WIDGET(fileopendatafile), TRUE); gtk_widget_show(fileopendatafile); filesavedatafile = gtk_menu_item_new_with_mnemonic(_("S_ave Log File")); diff --git a/src/hal/utils/scope_files.c b/src/hal/utils/scope_files.c index 3b5f6f9ba4a..36825af8f47 100644 --- a/src/hal/utils/scope_files.c +++ b/src/hal/utils/scope_files.c @@ -43,6 +43,7 @@ #include "rtapi.h" /* RTAPI realtime OS API */ #include "hal.h" /* HAL public API decls */ +#include "../hal_priv.h" /* HAL private API decls */ #include #include "miscgtk.h" /* generic GTK stuff */ @@ -295,6 +296,36 @@ void write_log_file(char *filename) } fprintf(fp, "\n"); + /* write channel positions */ + fprintf(fp, "# Position: "); + chan_active = 0; + for (chan_num = 0; chan_num < 16; chan_num++) { + if (vert->chan_enabled[chan_num] == 1) { + chan = &(ctrl_usr->chan[chan_num]); + fprintf(fp, "%.6f", chan->position); + chan_active++; + if (chan_active < sample_len) { + fprintf(fp, ";"); + } + } + } + fprintf(fp, "\n"); + + /* write channel scale indices */ + fprintf(fp, "# Scale: "); + chan_active = 0; + for (chan_num = 0; chan_num < 16; chan_num++) { + if (vert->chan_enabled[chan_num] == 1) { + chan = &(ctrl_usr->chan[chan_num]); + fprintf(fp, "%d", chan->scale_index); + chan_active++; + if (chan_active < sample_len) { + fprintf(fp, ";"); + } + } + } + fprintf(fp, "\n"); + /* * Specify LC_NUMERIC, makes the number format consistent, regardless * which locale previously in use. Necessary since the number format changes @@ -339,6 +370,456 @@ void write_log_file(char *filename) printf("Log file '%s' written.\n", filename); } +/* reads captured data from disk and loads it into display buffer */ +void read_log_file(char *filename) +{ + scope_chan_t *chan; + scope_horiz_t *horiz; + scope_vert_t *vert; + + char line[16384]; /* buffer for reading lines */ + char *token; + char *channel_names[16]; + hal_type_t channel_types[16]; + double channel_positions[16]; + int channel_scales[16]; + int has_position_config = 0; + int has_scale_config = 0; + int channel_count = 0; + int sample_period_ns = 0; + int sample_count = 0; + int line_num = 0; + char *old_locale, *saved_locale; + FILE *fp; + int i; + scope_data_t *dptr; + + /* Open file */ + fp = fopen(filename, "r"); + if (fp == NULL) { + fprintf(stderr, "ERROR: log file '%s' could not be opened\n", filename); + return; + } + + /* Parse line 1: sample period comment */ + if (fgets(line, sizeof(line), fp) == NULL) { + fprintf(stderr, "ERROR: log file is empty\n"); + fclose(fp); + return; + } + line_num++; + + /* Expected format: "# Sampling period is 12345 ns" */ + if (sscanf(line, "# Sampling period is %d ns", &sample_period_ns) != 1) { + fprintf(stderr, "ERROR: line %d: invalid sample period format\n", line_num); + fclose(fp); + return; + } + + if (sample_period_ns <= 0) { + fprintf(stderr, "ERROR: invalid sample period %d ns\n", sample_period_ns); + fclose(fp); + return; + } + + /* Parse line 2: channel names (semicolon-separated) */ + if (fgets(line, sizeof(line), fp) == NULL) { + fprintf(stderr, "ERROR: log file missing channel header\n"); + fclose(fp); + return; + } + line_num++; + + /* Remove trailing newline */ + line[strcspn(line, "\n")] = 0; + + /* Parse channel names */ + token = strtok(line, ";"); + while (token != NULL && channel_count < 16) { + /* Allocate and store channel name */ + channel_names[channel_count] = strdup(token); + channel_count++; + token = strtok(NULL, ";"); + } + + if (channel_count == 0) { + fprintf(stderr, "ERROR: no channels found in header\n"); + fclose(fp); + return; + } + + if (token != NULL) { + fprintf(stderr, "WARNING: CSV has more than 16 channels, loading first 16 only\n"); + } + + /* Initialize default values for position and scale */ + for (i = 0; i < channel_count; i++) { + channel_positions[i] = 0.5; /* Default center position */ + channel_scales[i] = 0; /* Default scale index */ + } + + /* Try to parse optional position line */ + long optional_line_pos = ftell(fp); + if (fgets(line, sizeof(line), fp) != NULL) { + line_num++; + if (strncmp(line, "# Position: ", 12) == 0) { + /* Parse position values */ + char *pos_start = line + 12; + pos_start[strcspn(pos_start, "\n")] = 0; + token = strtok(pos_start, ";"); + i = 0; + while (token != NULL && i < channel_count) { + if (sscanf(token, "%lf", &channel_positions[i]) == 1) { + has_position_config = 1; + } + i++; + token = strtok(NULL, ";"); + } + + /* Try to parse optional scale line */ + optional_line_pos = ftell(fp); + if (fgets(line, sizeof(line), fp) != NULL) { + line_num++; + if (strncmp(line, "# Scale: ", 9) == 0) { + /* Parse scale values */ + char *scale_start = line + 9; + scale_start[strcspn(scale_start, "\n")] = 0; + token = strtok(scale_start, ";"); + i = 0; + while (token != NULL && i < channel_count) { + if (sscanf(token, "%d", &channel_scales[i]) == 1) { + has_scale_config = 1; + } + i++; + token = strtok(NULL, ";"); + } + } else { + /* Not a scale line, rewind */ + fseek(fp, optional_line_pos, SEEK_SET); + line_num--; + } + } + } else { + /* Not a position line, rewind */ + fseek(fp, optional_line_pos, SEEK_SET); + line_num--; + } + } + + /* Count samples by reading through the file */ + long data_start_pos = ftell(fp); + sample_count = 0; + while (fgets(line, sizeof(line), fp) != NULL) { + sample_count++; + } + + if (sample_count == 0) { + fprintf(stderr, "ERROR: no data samples found in file\n"); + fclose(fp); + for (i = 0; i < channel_count; i++) { + free(channel_names[i]); + } + return; + } + + /* Check if samples exceed buffer capacity */ + if (sample_count > ctrl_shm->buf_len) { + fprintf(stderr, "WARNING: CSV has %d samples but buffer is %d, truncating\n", + sample_count, ctrl_shm->buf_len); + sample_count = ctrl_shm->buf_len; + } + + /* Set locale for consistent number parsing */ + old_locale = setlocale(LC_NUMERIC, NULL); + if (old_locale == NULL) { + fprintf(stderr, "ERROR: Could not read locale.\n"); + fclose(fp); + for (i = 0; i < channel_count; i++) { + free(channel_names[i]); + } + return; + } + saved_locale = strdup(old_locale); + if (saved_locale == NULL) { + fprintf(stderr, "ERROR: Could not copy old locale.\n"); + fclose(fp); + for (i = 0; i < channel_count; i++) { + free(channel_names[i]); + } + return; + } + setlocale(LC_NUMERIC, "C"); + + /* Setup channels - try to match names to HAL entities */ + vert = &(ctrl_usr->vert); + + /* Disable all channels and mark data offsets as invalid */ + for (i = 0; i < 16; i++) { + vert->chan_enabled[i] = 0; + vert->data_offset[i] = -1; + ctrl_usr->chan[i].data_source_type = -1; + ctrl_usr->chan[i].is_phantom = 0; + } + + for (i = 0; i < channel_count; i++) { + chan = &(ctrl_usr->chan[i]); + int matched = 0; + + /* Try to find matching HAL pin */ + hal_pin_t *pin = halpr_find_pin_by_name(channel_names[i]); + if (pin != NULL) { + chan->data_source_type = 0; + chan->data_source = SHMOFF(pin); + chan->data_type = pin->type; + chan->name = pin->name; + matched = 1; + } else { + /* Try to find matching HAL signal */ + hal_sig_t *sig = halpr_find_sig_by_name(channel_names[i]); + if (sig != NULL) { + chan->data_source_type = 1; + chan->data_source = SHMOFF(sig); + chan->data_type = sig->type; + chan->name = sig->name; + matched = 1; + } else { + /* Try to find matching HAL parameter */ + hal_param_t *param = halpr_find_param_by_name(channel_names[i]); + if (param != NULL) { + chan->data_source_type = 2; + chan->data_source = SHMOFF(param); + chan->data_type = param->type; + chan->name = param->name; + matched = 1; + } + } + } + + /* If no match found, create phantom channel */ + if (!matched) { + char *phantom_name = malloc(strlen(channel_names[i]) + 8); + if (phantom_name != NULL) { + snprintf(phantom_name, strlen(channel_names[i]) + 8, + "[CSV] %s", channel_names[i]); + + chan->data_source_type = -1; /* No source */ + chan->data_source = 0; + chan->data_type = HAL_FLOAT; /* Default to float for CSV data */ + chan->name = phantom_name; + chan->is_phantom = 1; + } + } + + /* Set up channel data length and scale limits based on type */ + switch (chan->data_type) { + case HAL_BIT: + chan->data_len = sizeof(hal_bit_t); + chan->min_index = -2; + chan->max_index = 2; + break; + case HAL_FLOAT: + chan->data_len = sizeof(hal_float_t); + chan->min_index = -36; + chan->max_index = 36; + break; + case HAL_S32: + chan->data_len = sizeof(hal_s32_t); + chan->min_index = -2; + chan->max_index = 30; + break; + case HAL_U32: + chan->data_len = sizeof(hal_u32_t); + chan->min_index = -2; + chan->max_index = 30; + break; + default: + chan->data_len = 0; + chan->min_index = -1; + chan->max_index = 1; + } + + /* Set default scale and offset */ + chan->vert_offset = 0.0; + chan->ac_offset = 0; + + /* Apply position and scale from CSV if available */ + if (has_position_config) { + chan->position = channel_positions[i]; + } else { + chan->position = 0.5; /* Center position */ + } + + if (has_scale_config) { + chan->scale_index = channel_scales[i]; + /* Clamp to valid range for this data type */ + if (chan->scale_index < chan->min_index) { + chan->scale_index = chan->min_index; + } + if (chan->scale_index > chan->max_index) { + chan->scale_index = chan->max_index; + } + } else { + chan->scale_index = 0; + } + + /* Compute the actual scale factor from scale_index */ + { + double scale = 1.0; + int index = chan->scale_index; + while (index >= 3) { + scale *= 10.0; + index -= 3; + } + while (index <= -3) { + scale *= 0.1; + index += 3; + } + switch (index) { + case 2: + scale *= 5.0; + break; + case 1: + scale *= 2.0; + break; + case -1: + scale *= 0.5; + break; + case -2: + scale *= 0.2; + break; + default: + break; + } + chan->scale = scale; + } + + /* Enable this channel */ + vert->chan_enabled[i] = 1; + vert->data_offset[i] = i; /* Sequential offsets */ + + channel_types[i] = chan->data_type; + } + + /* Update channel selection buttons to show enabled state */ + for (i = 0; i < channel_count; i++) { + if (vert->chan_sel_buttons[i] != NULL) { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(vert->chan_sel_buttons[i]), TRUE); + } + } + + /* Update shared memory settings */ + ctrl_shm->sample_len = channel_count; + ctrl_usr->samples = sample_count; + + /* Allocate/clear display buffer */ + memset(ctrl_usr->disp_buf, 0, sizeof(scope_data_t) * ctrl_shm->buf_len); + + /* Rewind to start of data section */ + fseek(fp, data_start_pos, SEEK_SET); + + /* Read and parse data rows */ + int row = 0; + while (row < sample_count && fgets(line, sizeof(line), fp) != NULL) { + /* Remove trailing newline */ + line[strcspn(line, "\n")] = 0; + + /* Parse values for this sample */ + token = strtok(line, ";"); + for (i = 0; i < channel_count && token != NULL; i++) { + double value; + if (sscanf(token, "%lf", &value) != 1) { + fprintf(stderr, "WARNING: invalid data at row %d, channel %d\n", + row + 1, i + 1); + value = 0.0; + } + + /* Calculate buffer position (interleaved format) */ + dptr = ctrl_usr->disp_buf + (row * channel_count) + i; + + /* Convert to appropriate type */ + switch (channel_types[i]) { + case HAL_BIT: + dptr->d_u8 = (value != 0.0) ? 1 : 0; + break; + case HAL_FLOAT: + dptr->d_real = (real_t)value; + break; + case HAL_S32: + dptr->d_s32 = (rtapi_s32)value; + break; + case HAL_U32: + dptr->d_u32 = (rtapi_u32)value; + break; + default: + break; + } + + token = strtok(NULL, ";"); + } + row++; + } + + /* Restore locale */ + setlocale(LC_NUMERIC, saved_locale); + free(saved_locale); + + /* Close file */ + fclose(fp); + + /* Free channel name copies */ + for (i = 0; i < channel_count; i++) { + if (channel_names[i] != NULL && !ctrl_usr->chan[i].is_phantom) { + free(channel_names[i]); + } + /* Don't free phantom names - they're stored in chan->name */ + } + + /* Restore horizontal timing settings */ + horiz = &(ctrl_usr->horiz); + + /* Back-calculate thread period and multiplier + * We'll use a reasonable default thread period and calculate mult + * For example, assume 1ms (1000000ns) base thread period + */ + if (horiz->thread_period_ns > 0) { + /* Use existing thread period */ + ctrl_shm->mult = sample_period_ns / horiz->thread_period_ns; + if (ctrl_shm->mult < 1) ctrl_shm->mult = 1; + } else { + /* No thread selected, use a default */ + horiz->thread_period_ns = 1000000; /* 1ms default */ + ctrl_shm->mult = sample_period_ns / horiz->thread_period_ns; + if (ctrl_shm->mult < 1) { + /* Sample period is shorter than thread period, adjust */ + horiz->thread_period_ns = sample_period_ns; + ctrl_shm->mult = 1; + } + } + + /* Update horizontal display settings */ + horiz->sample_period_ns = sample_period_ns; + horiz->sample_period = (double)sample_period_ns * 1.0e-9; + + /* Mark that data is from log file */ + ctrl_usr->data_from_log_file = 1; + + /* Switch to STOP mode to prevent overwriting the data */ + set_run_mode(STOP); + + /* Select the first loaded channel so user can see the data */ + if (channel_count > 0) { + vert->selected = 1; /* Select first channel (1-based index) */ + } + + /* Update channel and display */ + channel_changed(); + refresh_display(); + redraw_window(); + + printf("Log file '%s' loaded: %d channels, %d samples, %d ns period\n", + filename, channel_count, sample_count, sample_period_ns); +} + /* format the data and print it */ static void write_sample(FILE *fp, scope_data_t *dptr, hal_type_t type) { diff --git a/src/hal/utils/scope_usr.h b/src/hal/utils/scope_usr.h index cbe09f5a2d2..66c1d3222a2 100644 --- a/src/hal/utils/scope_usr.h +++ b/src/hal/utils/scope_usr.h @@ -99,6 +99,7 @@ typedef struct { int min_index; double scale; /* scaling (units/div) */ double position; /* vertical pos (0.0-1.0) */ + int is_phantom; /* TRUE if loaded from CSV without HAL source */ } scope_chan_t; /* this struct holds control data related to vertical control */ @@ -198,6 +199,7 @@ typedef struct { scope_run_mode_t run_mode; /* current run mode */ scope_run_mode_t old_run_mode; /* run mode to restore*/ int pending_restart; /* nonzero if run mode to be restored */ + int data_from_log_file; /* TRUE if current data loaded from CSV */ /* top level windows */ GtkWidget *main_win; GtkWidget *horiz_info_win; @@ -259,6 +261,8 @@ void write_horiz_config(FILE *fp); void write_vert_config(FILE *fp); void write_trig_config(FILE *fp); void write_log_file (char *filename); +void read_log_file (char *filename); +void open_log_cb(GtkWindow *parent); /* the following functions set various parameters, they are normally called by the GUI, but can also be called by code reading a file diff --git a/src/hal/utils/scope_vert.c b/src/hal/utils/scope_vert.c index 227b5c97d01..139680318be 100644 --- a/src/hal/utils/scope_vert.c +++ b/src/hal/utils/scope_vert.c @@ -1134,6 +1134,9 @@ static void write_chan_config(FILE *fp, scope_chan_t *chan) } else if ( chan->data_source_type == 2 ) { // pin fprintf(fp, "PARAM %s\n", chan->name); + } else if ( chan->is_phantom ) { + // phantom channel - save as comment for reference + fprintf(fp, "# PHANTOM %s\n", chan->name); } else { // not configured return;