3131
3232# Serial Configuration
3333BAUD_RATE = 250000
34+ # Default values; overridden by WaterfallApp instance settings
3435NUM_SAMPLES = 1800 # (X-axis)
3536
3637MAX_ROWS = 300 # Number of time steps (Y-axis)
4344# SAMPLE_TIME = 47.0e-6
4445# SAMPLE_TIME = 41.666e-6 # 13.2 microseconds on Atmega328 max sample speed plus 40 microseconds delay in sampling loop
4546# SAMPLE_TIME = 22.22e-6 # 13.2 microseconds on Atmega328 max sample speed plus 20 microseconds delay in sampling loop
46- SAMPLE_TIME = 13.2e-6 # 13.2 microseconds on Atmega328 max sample speed without additional delay
47+ SAMPLE_TIME = (
48+ 13.2e-6 # 13.2 microseconds on Atmega328 max sample speed without additional delay
49+ )
4750# SAMPLE_TIME = 11.0e-6 # 13.2 microseconds on RP2040 max sample speed with 10 microseconds additional delay per sample
4851# SAMPLE_TIME = 7.682e-6 # 7.682 microseconds on STM32F103 max sample speed
4952# SAMPLE_TIME = 6.0e-6 # 6 microseconds on RP2040 max sample speed with 5 microseconds additional delay per sample
5053# SAMPLE_TIME = 1.290e-6 # 13.2 microseconds on RP2040 max sample speed without additional delay
5154
5255DEFAULT_LEVELS = (0 , 256 ) # Expected data range
5356
54- SAMPLE_RESOLUTION = (
55- SPEED_OF_SOUND * SAMPLE_TIME * 100
56- ) / 2 # cm per row (0.99 cm per row)
57- PACKET_SIZE = 1 + 6 + NUM_SAMPLES + 1 # header + payload + checksum
58- MAX_DEPTH = NUM_SAMPLES * SAMPLE_RESOLUTION # Total depth in cm
57+ # Module-level derived values are kept for defaults only; instance values are used in UI
58+ SAMPLE_RESOLUTION = (SPEED_OF_SOUND * SAMPLE_TIME * 100 ) / 2
59+ PACKET_SIZE = 1 + 6 + NUM_SAMPLES + 1
60+ MAX_DEPTH = NUM_SAMPLES * SAMPLE_RESOLUTION
5961depth_labels = {
6062 int (i / SAMPLE_RESOLUTION ): f"{ i / 100 } "
6163 for i in range (0 , int (MAX_DEPTH ), Y_LABEL_DISTANCE )
@@ -101,13 +103,15 @@ def __init__(
101103 parent = None ,
102104 current_gradient = "cyclic" ,
103105 current_speed = 343 ,
106+ current_num_samples = NUM_SAMPLES ,
107+ current_sample_time_us = SAMPLE_TIME * 1e6 ,
104108 nmea_enabled = False ,
105109 nmea_port = 10110 ,
106110 nmea_address = "127.0.0.1" ,
107111 ):
108112 super ().__init__ (parent )
109113 self .setWindowTitle ("Chart Settings" )
110- self .setFixedSize (320 , 550 )
114+ self .setFixedSize (340 , 640 )
111115
112116 self .main_app = parent
113117
@@ -150,6 +154,43 @@ def __init__(
150154 self .speed_dropdown .setCurrentIndex (1 if current_speed == 1440 else 0 )
151155 card_layout .addWidget (self .speed_dropdown )
152156
157+ # --- Sampling Parameters ---
158+ sampling_section = QVBoxLayout ()
159+ sampling_section .setSpacing (8 )
160+
161+ sampling_label = QLabel ("Sampling Parameters:" )
162+ sampling_label .setStyleSheet ("font-weight: bold;" )
163+ sampling_section .addWidget (sampling_label )
164+
165+ # Number of Samples
166+ ns_row = QHBoxLayout ()
167+ ns_label = QLabel ("Num. Samples:" )
168+ ns_label .setMinimumWidth (100 )
169+ self .num_samples_input = QLineEdit ()
170+ self .num_samples_input .setPlaceholderText ("e.g. 1800" )
171+ self .num_samples_input .setText (str (current_num_samples ))
172+ self .num_samples_input .setMaximumWidth (200 )
173+ ns_row .addWidget (ns_label )
174+ ns_row .addWidget (self .num_samples_input )
175+ ns_row .addStretch ()
176+ sampling_section .addLayout (ns_row )
177+
178+ # Sample Time (microseconds)
179+ st_row = QHBoxLayout ()
180+ st_label = QLabel ("Sample Time (µs):" )
181+ st_label .setMinimumWidth (100 )
182+ self .sample_time_input = QLineEdit ()
183+ self .sample_time_input .setPlaceholderText ("e.g. 13.2" )
184+ # Accept display in microseconds for user convenience
185+ self .sample_time_input .setText (f"{ current_sample_time_us :.6f} " )
186+ self .sample_time_input .setMaximumWidth (200 )
187+ st_row .addWidget (st_label )
188+ st_row .addWidget (self .sample_time_input )
189+ st_row .addStretch ()
190+ sampling_section .addLayout (st_row )
191+
192+ card_layout .addLayout (sampling_section )
193+
153194 # --- NMEA Output Section ---
154195 nmea_section = QVBoxLayout ()
155196 nmea_section .setSpacing (8 )
@@ -282,11 +323,27 @@ def apply_settings(self):
282323 int (self .port_input .text ()) if self .port_input .text ().isdigit () else 10110
283324 )
284325
326+ # Parse sampling params
327+ try :
328+ ns_value = int (self .num_samples_input .text ())
329+ except Exception :
330+ ns_value = None
331+ try :
332+ st_us_value = float (self .sample_time_input .text ())
333+ except Exception :
334+ st_us_value = None
335+
285336 if self .main_app :
286337 self .main_app .set_gradient (selected_gradient )
287338 self .main_app .set_sound_speed (selected_speed )
288339 self .main_app .configure_nmea_output (enabled = nmea_enabled , port = nmea_port )
289340 self .main_app .set_large_depth_display (self .large_depth_checkbox .isChecked ())
341+ # Apply sampling settings if valid
342+ if ns_value and ns_value > 0 :
343+ self .main_app .set_num_samples (ns_value )
344+ if st_us_value and st_us_value > 0 :
345+ # convert microseconds to seconds
346+ self .main_app .set_sample_time (st_us_value * 1e-6 )
290347
291348 self .close ()
292349
@@ -308,10 +365,15 @@ def __init__(self):
308365 self .current_gradient = "cyclic" # default color scheme
309366 self .current_speed = SPEED_OF_SOUND # default sound speed (343)
310367
368+ # User-configurable sampling parameters
369+ self .num_samples = NUM_SAMPLES
370+ self .sample_time = SAMPLE_TIME
371+
311372 self .setWindowTitle ("Open Echo Interface" )
312373 self .setGeometry (0 , 0 , 480 , 800 ) # Portrait mode for Raspberry Pi screen
313374
314- self .data = np .zeros ((MAX_ROWS , NUM_SAMPLES ))
375+ self ._recompute_sampling_derived ()
376+ self .data = np .zeros ((MAX_ROWS , self .num_samples ))
315377
316378 # Disable window translucency
317379 self .setAttribute (Qt .WA_TranslucentBackground , False )
@@ -342,7 +404,7 @@ def __init__(self):
342404
343405 main_layout .addWidget (self .waterfall )
344406
345- inverted_depth_labels = list (depth_labels .items ())[::- 1 ]
407+ inverted_depth_labels = list (self . depth_labels .items ())[::- 1 ]
346408 self .waterfall .getAxis ("left" ).setTicks ([inverted_depth_labels ])
347409 self .depth_line = pg .InfiniteLine (angle = 0 , pen = pg .mkPen ("r" , width = 2 ))
348410 self .waterfall .addItem (self .depth_line )
@@ -353,14 +415,16 @@ def __init__(self):
353415 right_axis .setStyle (showValues = True )
354416
355417 # dd horizontal lines
356- for i in range (0 , int (MAX_DEPTH ), Y_LABEL_DISTANCE ):
357- row_index = int (i / SAMPLE_RESOLUTION )
418+ self ._depth_lines = []
419+ for i in range (0 , int (self .max_depth ), Y_LABEL_DISTANCE ):
420+ row_index = int (i / self .sample_resolution )
358421 hline = pg .InfiniteLine (
359422 pos = row_index ,
360423 angle = 0 ,
361424 pen = pg .mkPen (color = "w" , style = pg .QtCore .Qt .DotLine ),
362425 )
363426 self .waterfall .addItem (hline )
427+ self ._depth_lines .append (hline )
364428
365429 # === Colorbar BELOW the plot to save width ===
366430 self .colorbar = pg .HistogramLUTWidget ()
@@ -493,7 +557,9 @@ def connect_udp(self):
493557 try :
494558 udp_port = int (self .udp_port_input .text ())
495559 settings = Settings (
496- connection_type = ConnectionTypeEnum .UDP , udp_port = udp_port
560+ connection_type = ConnectionTypeEnum .UDP ,
561+ udp_port = udp_port ,
562+ num_samples = self .num_samples ,
497563 )
498564 self ._start_reader (settings )
499565 self ._reader_task_type = ConnectionTypeEnum .UDP
@@ -578,22 +644,11 @@ def set_gradient(self, gradient_name):
578644 self .colorbar .item .gradient .loadPreset (gradient_name )
579645
580646 def set_sound_speed (self , speed ):
581- global SPEED_OF_SOUND , SAMPLE_RESOLUTION , MAX_DEPTH , depth_labels
582-
647+ global SPEED_OF_SOUND
583648 SPEED_OF_SOUND = speed
584649 self .current_speed = speed
585- SAMPLE_RESOLUTION = (SPEED_OF_SOUND * SAMPLE_TIME * 100 ) / 2
586- print (SAMPLE_RESOLUTION )
587- MAX_DEPTH = NUM_SAMPLES * SAMPLE_RESOLUTION
588- depth_labels = {
589- int (i / SAMPLE_RESOLUTION ): f"{ i / 100 } "
590- for i in range (0 , int (MAX_DEPTH ), Y_LABEL_DISTANCE )
591- }
592-
593- # Re-apply Y-axis ticks
594- inverted_depth_labels = list (depth_labels .items ())[::- 1 ]
595- self .waterfall .getAxis ("left" ).setTicks ([inverted_depth_labels ])
596- self .waterfall .getAxis ("right" ).setTicks ([inverted_depth_labels ])
650+ self ._recompute_sampling_derived ()
651+ self ._refresh_axes_and_grid ()
597652
598653 def key_press_event (self , event ):
599654 print ("key pressed" )
@@ -610,7 +665,9 @@ def connect_serial(self):
610665 selected_port = self .serial_dropdown .currentText ()
611666 try :
612667 settings = Settings (
613- connection_type = ConnectionTypeEnum .SERIAL , serial_port = selected_port
668+ connection_type = ConnectionTypeEnum .SERIAL ,
669+ serial_port = selected_port ,
670+ num_samples = self .num_samples ,
614671 )
615672 self ._start_reader (settings )
616673 self ._reader_task_type = ConnectionTypeEnum .SERIAL
@@ -649,7 +706,7 @@ def waterfall_plot_callback(
649706 mean = np .mean (self .data )
650707 self .imageitem .setLevels ((mean - 2 * sigma , mean + 2 * sigma ))
651708
652- depth_cm = depth_index * SAMPLE_RESOLUTION
709+ depth_cm = depth_index * self . sample_resolution
653710 self .depth_label .setText (f"Depth: { depth_cm :.1f} cm | Index: { depth_index :.0f} " )
654711 self .temperature_label .setText (f"Temperature: { temperature :.1f} °C" )
655712 self .drive_voltage_label .setText (f"vDRV: { drive_voltage :.1f} V" )
@@ -669,7 +726,7 @@ def waterfall_plot_callback(
669726 ):
670727 print ("Sending NMEA data" )
671728 try :
672- depth_cm = depth_index * SAMPLE_RESOLUTION
729+ depth_cm = depth_index * self . sample_resolution
673730 depth_m = depth_cm / 100
674731 depth_ft = depth_m * 3.28084
675732 depth_fathoms = depth_m * 0.546807
@@ -755,12 +812,76 @@ def open_settings(self):
755812 parent = self ,
756813 current_gradient = self .current_gradient ,
757814 current_speed = self .current_speed ,
815+ current_num_samples = self .num_samples ,
816+ current_sample_time_us = self .sample_time * 1e6 ,
758817 nmea_enabled = self .nmea_output_enabled ,
759818 nmea_port = self .nmea_port ,
760819 nmea_address = device_ip ,
761820 )
762821 self .settings_dialog .show ()
763822
823+ def _recompute_sampling_derived (self ):
824+ # Derived values based on current sampling configuration and speed of sound
825+ self .sample_resolution = (SPEED_OF_SOUND * self .sample_time * 100 ) / 2
826+ self .max_depth = int (self .num_samples * self .sample_resolution )
827+ self .depth_labels = {
828+ int (i / self .sample_resolution ): f"{ i / 100 } "
829+ for i in range (0 , int (self .max_depth ), Y_LABEL_DISTANCE )
830+ }
831+
832+ def _refresh_axes_and_grid (self ):
833+ inverted_depth_labels = list (self .depth_labels .items ())[::- 1 ]
834+ self .waterfall .getAxis ("left" ).setTicks ([inverted_depth_labels ])
835+ self .waterfall .getAxis ("right" ).setTicks ([inverted_depth_labels ])
836+
837+ # Remove old grid lines
838+ if hasattr (self , "_depth_lines" ):
839+ for ln in self ._depth_lines :
840+ try :
841+ self .waterfall .removeItem (ln )
842+ except Exception :
843+ pass
844+ self ._depth_lines = []
845+
846+ # Add new grid lines
847+ for i in range (0 , int (self .max_depth ), Y_LABEL_DISTANCE ):
848+ row_index = int (i / self .sample_resolution )
849+ hline = pg .InfiniteLine (
850+ pos = row_index ,
851+ angle = 0 ,
852+ pen = pg .mkPen (color = "w" , style = pg .QtCore .Qt .DotLine ),
853+ )
854+ self .waterfall .addItem (hline )
855+ self ._depth_lines .append (hline )
856+
857+ def set_num_samples (self , n : int ):
858+ try :
859+ n = int (n )
860+ except Exception :
861+ return
862+ if n <= 0 :
863+ return
864+ if n == self .num_samples :
865+ return
866+ self .num_samples = n
867+ # Resize data buffer
868+ self .data = np .zeros ((MAX_ROWS , self .num_samples ))
869+ self ._recompute_sampling_derived ()
870+ self ._refresh_axes_and_grid ()
871+
872+ def set_sample_time (self , seconds : float ):
873+ try :
874+ seconds = float (seconds )
875+ except Exception :
876+ return
877+ if seconds <= 0 :
878+ return
879+ if abs (seconds - self .sample_time ) < 1e-12 :
880+ return
881+ self .sample_time = seconds
882+ self ._recompute_sampling_derived ()
883+ self ._refresh_axes_and_grid ()
884+
764885
765886def set_gradient (self , gradient_name ):
766887 try :
0 commit comments