Skip to content

Commit f80b2ce

Browse files
author
John Harrington
committed
feat: Adjust num_samples and sample_time in app settings
1 parent d72e046 commit f80b2ce

3 files changed

Lines changed: 161 additions & 41 deletions

File tree

.githooks/pre-commit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ git add -A
2424

2525
# Run tests
2626
echo "[pre-commit] Pytest"
27-
uv run pytest -q
27+
uv run pytest -q python/
2828

2929
# Type checking
3030
echo "[pre-commit] Mypy"

documentation/contributing.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ nav_order: 2
1010

1111
This project welcomes contributions from the community. There are 3 main aspects to the project:
1212

13-
- Hardware, the TUSS4470 shields which can be used to power transducers
13+
- Hardware, the TUSS4470 shields which can be used to drive transducers
1414
- Firmware, written in arduino IDE and uploaded to various microcontroller boards (which the TUSS4470 shields connect to)
1515
- Software, python code for viewing the outputs from the firmware/hardware (in future also to be used for configuring them!)
1616

@@ -24,9 +24,10 @@ This project welcomes contributions from the community. There are 3 main aspects
2424
1. Clone the repository
2525
2. Open arduino IDE (or VSCode with Arduino community extension) the sketch for the board you are planning to develop for
2626
3. Select your board - you may need to install the relevant library.
27-
4. Upload the sketch!
27+
4. Make your changes
28+
5. Upload the sketch!
2829

29-
## Software
30+
## Python Software
3031

3132
### Prerequisites
3233
- Git
@@ -42,7 +43,7 @@ This project welcomes contributions from the community. There are 3 main aspects
4243
This will create a virtual environment and install dependencies defined in pyproject.toml.
4344
4445
### Git hooks
45-
The repository provides Git hooks to run typechecking, linting and unit tests:
46+
The repository provides optional Git hooks to run typechecking, linting and unit tests:
4647
4748
```
4849
git config core.hooksPath .githooks
@@ -51,7 +52,7 @@ chmod +x .githooks/*
5152
5253
If you want to commit without these checks (e.g. when you haven't written unit tests yet!) you can use `git commit --no-verify`
5354
54-
### Formatting, linting and typechecks
55+
### Formatting, linting, typecheck and test
5556
- Format:
5657
```
5758
uvx ruff format
@@ -64,12 +65,10 @@ If you want to commit without these checks (e.g. when you haven't written unit t
6465
```
6566
uv run mypy
6667
```
67-
68-
### Testing
69-
Run tests locally:
70-
```
71-
uv run pytest
72-
```
68+
- Unit test
69+
```
70+
uv run pytest
71+
```
7372
7473
## Contribution workflow
7574
- Fork and create a new branch for your changes.

python/src/open_echo/desktop.py

Lines changed: 150 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
# Serial Configuration
3333
BAUD_RATE = 250000
34+
# Default values; overridden by WaterfallApp instance settings
3435
NUM_SAMPLES = 1800 # (X-axis)
3536

3637
MAX_ROWS = 300 # Number of time steps (Y-axis)
@@ -43,19 +44,20 @@
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

5255
DEFAULT_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
5961
depth_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

765886
def set_gradient(self, gradient_name):
766887
try:

0 commit comments

Comments
 (0)