Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Software/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ __pycache__
.venv
*.pyc
build
dist
dist
.DS_Store
239 changes: 145 additions & 94 deletions Software/O-Scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.utils import platform
from kivy.metrics import dp

from plyer import filechooser

import numpy as np
import sigfig
Expand Down Expand Up @@ -589,6 +592,14 @@ class LinearSlider(BoxLayout):
label_text = StringProperty('')
units = StringProperty('')

def update_value_from_text(self, text):
"""Safely updates the slider value from text input. You never know if an error arises!"""
try:
new_val = float(text)
self.value = max(self.minimum, min(self.maximum, new_val))
except ValueError:
pass

class LogarithmicSlider(BoxLayout):

minimum = NumericProperty(1.)
Expand All @@ -598,6 +609,14 @@ class LogarithmicSlider(BoxLayout):
label_text = StringProperty('')
units = StringProperty('')

def update_value_from_text(self, text):
"""Safely updates the slider value from text input. You never know if an error arises!"""
try:
new_val = float(text)
self.value = max(self.minimum, min(self.maximum, new_val))
except ValueError:
pass

def nearest_one_two_five(self, x):
exponent = math.floor(x)
mantissa = x - exponent
Expand Down Expand Up @@ -1764,7 +1783,7 @@ def draw_plot(self):

def draw_background(self):
self.canvas.add(Color(*get_color_from_hex(self.canvas_background_color + 'CD')))
# self.canvas.add(Rectangle(pos = [self.canvas_left, self.canvas_bottom], size = [self.canvas_width, self.canvas_height]))
# self.canvas.add(Rectangle(pos = [self.canvas_left, self.canvas_bottom], size = [self.canvas_width, self.canvas_height]))
self.canvas.add(Rectangle(pos = [self.canvas_left, self.canvas_bottom], size = [self.canvas_width, self.axes_bottom - self.canvas_bottom]))
self.canvas.add(Rectangle(pos = [self.canvas_left, self.axes_top], size = [self.canvas_width, self.canvas_bottom + self.canvas_height - self.axes_top]))
self.canvas.add(Rectangle(pos = [self.canvas_left, self.axes_bottom], size = [self.axes_left - self.canvas_left, self.axes_height]))
Expand Down Expand Up @@ -1856,54 +1875,44 @@ def home_view(self):
self.update_preview()
self.refresh_plot()

def decrease_frequency(self):
foo = math.log10(self.frequency)
bar = math.floor(foo)
foobar = foo - bar
if foobar < 0.5 * math.log10(2.001):
self.frequency = 0.5 * math.pow(10., bar)
elif foobar < 0.5 * math.log10(2.001 * 5.001):
self.frequency = math.pow(10., bar)
elif foobar < 0.5 * math.log10(5.001 * 10.001):
self.frequency = 2. * math.pow(10., bar)
else:
self.frequency = 5. * math.pow(10., bar)
def increase_frequency(self):
app = App.get_running_app()
if self.frequency >= self.MAX_FREQUENCY:
return

if self.frequency < self.MIN_FREQUENCY:
self.frequency = self.MIN_FREQUENCY
if self.frequency > self.MAX_FREQUENCY:
self.frequency = self.MAX_FREQUENCY
log_f = math.log10(max(self.frequency, 1e-10))
log_f += app.wavegen_snap_step

snapped_log = app.nearest_one_two_five(log_f)
new_freq = 10 ** snapped_log

self.frequency = min(max(new_freq, self.MIN_FREQUENCY), self.MAX_FREQUENCY)

self.xlim = [0., 1. / self.frequency]
self.xmin = self.xlim[0]
self.xmax = self.xlim[1]

app.root.scope.set_frequency(self.frequency)
self.update_preview()
self.refresh_plot()

def increase_frequency(self):
foo = math.log10(self.frequency)
bar = math.floor(foo)
foobar = foo - bar
if foobar < 0.5 * math.log10(2.001):
self.frequency = 2. * math.pow(10., bar)
elif foobar < 0.5 * math.log10(2.001 * 5.001):
self.frequency = 5. * math.pow(10., bar)
elif foobar < 0.5 * math.log10(5.001 * 10.001):
self.frequency = 10. * math.pow(10., bar)
else:
self.frequency = 20. * math.pow(10., bar)
def decrease_frequency(self):
app = App.get_running_app()
if self.frequency <= self.MIN_FREQUENCY:
return

if self.frequency < self.MIN_FREQUENCY:
self.frequency = self.MIN_FREQUENCY
if self.frequency > self.MAX_FREQUENCY:
self.frequency = self.MAX_FREQUENCY
log_f = math.log10(max(self.frequency, 1e-10))
log_f -= app.wavegen_snap_step

snapped_log = app.nearest_one_two_five(log_f)
new_freq = 10 ** snapped_log

self.frequency = min(max(new_freq, self.MIN_FREQUENCY), self.MAX_FREQUENCY)

self.xlim = [0., 1. / self.frequency]
self.xmin = self.xlim[0]
self.xmax = self.xlim[1]

app.root.scope.set_frequency(self.frequency)
self.update_preview()
self.refresh_plot()
Expand Down Expand Up @@ -1958,8 +1967,12 @@ def on_touch_move(self, touch):
except ValueError:
return

# Use the dynamic step size from the app settings
step = app.wavegen_snap_step

if self.dragging_offset_control_pt and (i == 0):
self.offset = self.from_canvas_y(self.to_canvas_y(self.offset) + touch.pos[1] - self.touch_positions[i][1])
val = self.from_canvas_y(self.to_canvas_y(self.offset) + touch.pos[1] - self.touch_positions[i][1])
self.offset = round(val / step) * step
if self.offset < self.MIN_OFFSET:
self.offset = self.MIN_OFFSET
if self.offset > self.MAX_OFFSET:
Expand All @@ -1969,12 +1982,17 @@ def on_touch_move(self, touch):
self.refresh_plot()

if self.dragging_amp_control_pt and (i == 0):
self.amplitude = self.from_canvas_y(self.to_canvas_y(self.offset + self.amplitude) + touch.pos[1] - self.touch_positions[i][1]) - self.offset
# Vertical move for amplitude
val_amp = self.from_canvas_y(self.to_canvas_y(self.offset + self.amplitude) + touch.pos[1] - self.touch_positions[i][1]) - self.offset
self.amplitude = round(val_amp / step) * step
if self.amplitude < self.MIN_AMPLITUDE:
self.amplitude = self.MIN_AMPLITUDE
if self.amplitude > self.MAX_AMPLITUDE:
self.amplitude = self.MAX_AMPLITUDE

# Horizontal move for frequency
t_peak = self.from_canvas_x(self.to_canvas_x(0.25 / self.frequency) + touch.pos[0] - self.touch_positions[i][0])
self.frequency = round(t_peak / step) * step
if t_peak < 0.025 * self.xlim[1]:
t_peak = 0.025 * self.xlim[1]
if t_peak > self.xlim[1]:
Expand All @@ -1984,6 +2002,7 @@ def on_touch_move(self, touch):
self.frequency = self.MIN_FREQUENCY
if self.frequency > self.MAX_FREQUENCY:
self.frequency = self.MAX_FREQUENCY

app.root.scope.set_amplitude(self.amplitude)
app.root.scope.set_frequency(self.frequency)
self.update_preview()
Expand All @@ -1998,7 +2017,8 @@ def on_touch_move(self, touch):
else:
self.drag_direction = 'VERTICAL'
if self.drag_direction == 'VERTICAL':
self.amplitude = self.from_canvas_y(self.to_canvas_y(self.offset + self.amplitude) + touch.pos[1] - self.touch_positions[i][1]) - self.offset
val_amp = self.from_canvas_y(self.to_canvas_y(self.offset + self.amplitude) + touch.pos[1] - self.touch_positions[i][1]) - self.offset
self.amplitude = round(val_amp / step) * step
if self.amplitude < self.MIN_AMPLITUDE:
self.amplitude = self.MIN_AMPLITUDE
if self.amplitude > self.MAX_AMPLITUDE:
Expand Down Expand Up @@ -3789,6 +3809,9 @@ class MainApp(App):
# Color theme property
color_theme = StringProperty('default')

# Snap step thing (i dont know where to put it)
wavegen_snap_step = NumericProperty(0.5)

def __init__(self, **kwargs):
super(MainApp, self).__init__(**kwargs)
# Settings manager already initialized at module load time for font config
Expand Down Expand Up @@ -4177,6 +4200,22 @@ def on_ok(instance):
ok_btn.bind(on_release=on_ok)

popup.open()

# yes this is in the wrong place
# no, i will not move it
def nearest_one_two_five(self, x):
exponent = math.floor(x)
mantissa = x - exponent
if mantissa < math.log10(math.sqrt(2.)):
mantissa = 0.
elif mantissa < math.log10(math.sqrt(10.)):
mantissa = math.log10(2.)
elif mantissa < math.log10(math.sqrt(50.)):
mantissa = math.log10(5.)
else:
mantissa = 0.
exponent += 1.
return exponent + mantissa

def get_settings_directory(self):
"""Get the settings directory path for display."""
Expand Down Expand Up @@ -4282,71 +4321,84 @@ def process_selection(self, selection):
else:
return pathlib.Path(selection).name

def export_waveforms(self, path, filename, overwrite_existing_file = False):
self.save_dialog_path = path
self.save_dialog_file = None

if filename == '':
return

if os.path.exists(os.path.join(path, filename)) and not overwrite_existing_file:
self.save_dialog_file = filename
Factory.FileExistsAlert().open()
return

try:
outfile = open(os.path.join(path, filename), 'w')
except:
return

if filename[-4:] == '.txt' or filename[-4:] == '.TXT':
outfile.write('t1\tch1\tt2\tch2\n')
sep = '\t'
else:
outfile.write('t1,ch1,t2,ch2\n')
sep = ','
def open_save_waveform_dialog(self):
filechooser.save_file(
on_selection=self._on_waveform_save_selection,
title="Save Waveforms",
filters=[("CSV Files", "*.csv"), ("Text Files", "*.txt")]
)

for i in range(len(self.root.scope.scope_plot.curves['CH1'].points_x)):
for j in range(len(self.root.scope.scope_plot.curves['CH1'].points_x[i])):
line = '{!s}{!s}'.format(self.root.scope.scope_plot.curves['CH1'].points_x[i][j], sep)
line += '{!s}{!s}'.format(self.root.scope.scope_plot.curves['CH1'].points_y[i][j], sep)
line += '{!s}{!s}'.format(self.root.scope.scope_plot.curves['CH2'].points_x[i][j], sep)
line += '{!s}\n'.format(self.root.scope.scope_plot.curves['CH2'].points_y[i][j])
outfile.write(line)
def _on_waveform_save_selection(self, selection):
if selection:
# selection is a list, so take only the first item
self.export_waveforms(selection[0])

outfile.close()
def open_save_bode_dialog(self):
filechooser.save_file(
on_selection=self._on_bode_save_selection,
title="Save Frequency Response",
filters=[("CSV Files", "*.csv"), ("Text Files", "*.txt")]
)

def export_freqresp(self, path, filename, overwrite_existing_file = False):
self.save_dialog_path = path
self.save_dialog_file = None
def _on_bode_save_selection(self, selection):
if selection:
self.export_freqresp(selection[0])

if filename == '':
def export_waveforms(self, filepath):
if not filepath:
return

if os.path.exists(os.path.join(path, filename)) and not overwrite_existing_file:
self.save_dialog_file = filename
Factory.FileExistsAlert().open()
return
# Ensure correct extension if missing
if not (filepath.lower().endswith('.csv') or filepath.lower().endswith('.txt')):
filepath += '.csv'

try:
outfile = open(os.path.join(path, filename), 'w')
except:
return
with open(filepath, 'w') as outfile:
if filepath.lower().endswith('.txt'):
outfile.write('t1\tch1\tt2\tch2\n')
sep = '\t'
else:
outfile.write('t1,ch1,t2,ch2\n')
sep = ','

# Access plot data directly
curve_ch1 = self.root.scope.scope_plot.curves['CH1']
curve_ch2 = self.root.scope.scope_plot.curves['CH2']

for i in range(len(curve_ch1.points_x)):
for j in range(len(curve_ch1.points_x[i])):
line = f'{curve_ch1.points_x[i][j]}{sep}'
line += f'{curve_ch1.points_y[i][j]}{sep}'
line += f'{curve_ch2.points_x[i][j]}{sep}'
line += f'{curve_ch2.points_y[i][j]}\n'
outfile.write(line)
except Exception as e:
print(f"Error saving waveforms: {e}")

if filename[-4:] == '.txt' or filename[-4:] == '.TXT':
outfile.write('freq\tgain\tphase\n')
sep = '\t'
else:
outfile.write('freq,gain,phase\n')
sep = ','
def export_freqresp(self, filepath):
if not filepath:
return

for i in range(len(self.root.bode.freq)):
line = '{!s}{!s}'.format(self.root.bode.freq[i], sep)
line += '{!s}{!s}'.format(self.root.bode.gain[i], sep)
line += '{!s}\n'.format(self.root.bode.phase[i])
outfile.write(line)
if not (filepath.lower().endswith('.csv') or filepath.lower().endswith('.txt')):
filepath += '.csv'

outfile.close()
try:
with open(filepath, 'w') as outfile:
if filepath.lower().endswith('.txt'):
outfile.write('freq\tgain\tphase\n')
sep = '\t'
else:
outfile.write('freq,gain,phase\n')
sep = ','

bode_root = self.root.bode
for i in range(len(bode_root.freq)):
line = f'{bode_root.freq[i]}{sep}'
line += f'{bode_root.gain[i]}{sep}'
line += f'{bode_root.phase[i]}\n'
outfile.write(line)
except Exception as e:
print(f"Error saving frequency response: {e}")

def load_offset_waveform(self, path, filename):
if not self.dev.connected:
Expand Down Expand Up @@ -4376,5 +4428,4 @@ def load_offset_waveform(self, path, filename):
if __name__ == '__main__':
app = MainApp()
oscope.app = app
app.run()

app.run()
2 changes: 2 additions & 0 deletions Software/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ dependencies = [
"kivy>=2.3.1",
"numpy>=2.2.6",
"pillow>=12.1.0",
"plyer>=2.1.0",
"pyobjus>=1.2.4; sys_platform == 'darwin'",
"pyserial>=3.5",
"sigfig>=1.3.19",
"toml>=0.10.2",
Expand Down
Loading