diff --git a/Software/.gitignore b/Software/.gitignore index e57c109..cd68a53 100644 --- a/Software/.gitignore +++ b/Software/.gitignore @@ -2,4 +2,5 @@ __pycache__ .venv *.pyc build -dist \ No newline at end of file +dist +.DS_Store \ No newline at end of file diff --git a/Software/O-Scope.py b/Software/O-Scope.py index 749033b..6cfff80 100755 --- a/Software/O-Scope.py +++ b/Software/O-Scope.py @@ -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 @@ -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.) @@ -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 @@ -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])) @@ -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() @@ -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: @@ -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]: @@ -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() @@ -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: @@ -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 @@ -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.""" @@ -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: @@ -4376,5 +4428,4 @@ def load_offset_waveform(self, path, filename): if __name__ == '__main__': app = MainApp() oscope.app = app - app.run() - + app.run() \ No newline at end of file diff --git a/Software/pyproject.toml b/Software/pyproject.toml index 39e9add..61830f9 100644 --- a/Software/pyproject.toml +++ b/Software/pyproject.toml @@ -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", diff --git a/Software/scopeui.kv b/Software/scopeui.kv index a6b3978..0ff7197 100644 --- a/Software/scopeui.kv +++ b/Software/scopeui.kv @@ -315,7 +315,7 @@ BoxLayout: orientation: 'vertical' size_hint_y: None - height: 130 + height: 200 Label: text: '[b]Display Settings[/b]' @@ -360,8 +360,9 @@ BoxLayout: orientation: 'horizontal' size_hint_y: None - height: 40 + height: 50 spacing: 10 + padding: [0, 4, 0, 4] Label: text: 'Text Scale:' @@ -393,8 +394,9 @@ BoxLayout: orientation: 'horizontal' size_hint_y: None - height: 40 + height: 50 spacing: 10 + padding: [0, 4, 0, 4] Label: text: 'Tooltip Delay:' @@ -422,6 +424,40 @@ text_size: self.size valign: 'middle' + # step size setting + BoxLayout: + orientation: 'horizontal' + size_hint_y: None + height: 50 + spacing: 10 + padding: [0, 4, 0, 4] + + Label: + text: 'Wavegen Snap:' + font_size: int(16 * app.fontscale) + size_hint_x: 0.2 + halign: 'right' + text_size: self.size + valign: 'middle' + + Slider: + id: wavegen_snap_slider + min: 0.1 + max: 2.0 + step: 0.01 + value: app.wavegen_snap_step + size_hint_x: 0.5 + on_value: app.wavegen_snap_step = self.value + + Label: + text: str(round(wavegen_snap_slider.value, 2)) + ' V' + font_size: int(14 * app.fontscale) + size_hint_x: 0.3 + color: 0.6, 0.6, 0.6, 1 + halign: 'left' + text_size: self.size + valign: 'middle' + # Color Theme Section @@ -796,15 +832,28 @@ slider.step = root.step if self.state == 'down' else 0. slider.value = root.minimum + root.step * round((slider.value - root.minimum) / root.step) if self.state == 'down' else slider.value - Label: - size_hint_x: 0.1 - text: root.label_text + app.num2str(root.value, 4) + root.units - font_size: int(18 * app.fontscale) - halign: 'center' + BoxLayout: + size_hint_x: 0.25 + orientation: 'horizontal' + Label: + text: root.label_text + size_hint_x: 0.3 + font_size: int(14 * app.fontscale) + TextInput: + text: app.num2str(root.value, 4) + size_hint_x: 0.45 + multiline: False + font_size: int(16 * app.fontscale) + halign: 'center' + on_text_validate: root.update_value_from_text(self.text) + Label: + text: root.units + size_hint_x: 0.25 + font_size: int(14 * app.fontscale) Slider: id: slider - size_hint_x: 0.8 + size_hint_x: 0.65 orientation: 'horizontal' min: root.minimum max: root.maximum @@ -827,15 +876,28 @@ slider.step = 1/3 if self.state == 'down' else 0. slider.value = root.nearest_one_two_five(slider.value) if snap_button.state == 'down' else slider.value - Label: - size_hint_x: 0.1 - text: root.label_text + app.num2str(root.value, 4) + root.units - font_size: int(18 * app.fontscale) - halign: 'center' + BoxLayout: + size_hint_x: 0.25 + orientation: 'horizontal' + Label: + text: root.label_text + size_hint_x: 0.3 + font_size: int(14 * app.fontscale) + TextInput: + text: app.num2str(root.value, 4) + size_hint_x: 0.45 + multiline: False + font_size: int(16 * app.fontscale) + halign: 'center' + on_text_validate: root.update_value_from_text(self.text) + Label: + text: root.units + size_hint_x: 0.25 + font_size: int(14 * app.fontscale) Slider: id: slider - size_hint_x: 0.8 + size_hint_x: 0.65 orientation: 'horizontal' min: math.log10(root.minimum) max: math.log10(root.maximum) @@ -1301,9 +1363,7 @@ source: kivy_resources.resource_find('save.png') size_hint_y: 1 / 9 tooltip_text: 'Save Scope Trace to CSV' - on_release: - app.save_dialog_visible = True - Factory.ScopeSaveDialog().open() + on_release: app.open_save_waveform_dialog() ImageButton: id: play_pause_button @@ -1788,9 +1848,7 @@ source: kivy_resources.resource_find('save.png') size_hint_y: 1 / 9 tooltip_text: 'Save Bode Plot Data to CSV' - on_release: - app.save_dialog_visible = True - Factory.BodeSaveDialog().open() + on_release: app.open_save_bode_dialog() ImageButton: id: play_stop_button @@ -1905,4 +1963,4 @@ BodeRoot: id: bode_root - name: 'bode' + name: 'bode' \ No newline at end of file diff --git a/Software/uv.lock b/Software/uv.lock index b49d323..1a733d1 100644 --- a/Software/uv.lock +++ b/Software/uv.lock @@ -501,6 +501,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" }, ] +[[package]] +name = "plyer" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/85/f61425aa9be1f9108eec1c13861c1e11c9a04eb786eb4832a8f7188317df/plyer-2.1.0.tar.gz", hash = "sha256:65b7dfb7e11e07af37a8487eb2aa69524276ef70dad500b07228ce64736baa61", size = 121371, upload-time = "2022-11-12T13:36:48.978Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/89/a41c2643fc8eabeb84791acb9d0e4d139b1e4b53473cc4dae947b5fa33ed/plyer-2.1.0-py2.py3-none-any.whl", hash = "sha256:1b1772060df8b3045ed4f08231690ec8f7de30f5a004aa1724665a9074eed113", size = 142266, upload-time = "2022-11-12T13:36:47.181Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -551,6 +560,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/b1/9da6ec3e88696018ee7bb9dc4a7310c2cfaebf32923a19598cd342767c10/pyinstaller_hooks_contrib-2026.0-py3-none-any.whl", hash = "sha256:0590db8edeba3e6c30c8474937021f5cd39c0602b4d10f74a064c73911efaca5", size = 452318, upload-time = "2026-01-20T00:15:21.88Z" }, ] +[[package]] +name = "pyobjus" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/75/f7ae38607648f9ccb363447b83480fcf0e582c42d62908bbcce05afbe754/pyobjus-1.2.4.tar.gz", hash = "sha256:4877101ff3b70b7fd2b12b2878ab23ee488018ce613f12948b58ff4c7e363388", size = 167844, upload-time = "2025-12-29T14:48:18.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/78/e75613faed2f1336d1a2549b5376d80c83f21f0aee6842d6cac91893c8ae/pyobjus-1.2.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf261b1388003095199a8f0267f31505573cbceef6ad3e4724be364268b16148", size = 547886, upload-time = "2025-12-29T14:48:05.968Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9c/0328355bbe08c4a71d839c11c11f929b3c4e11f064d2f9b7480fd0d8bfc3/pyobjus-1.2.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e83175b910bae4946e6464396d7a68e336b5ccfa81f7e9d1c7a46f06ec97efc5", size = 551941, upload-time = "2025-12-29T14:48:07.298Z" }, + { url = "https://files.pythonhosted.org/packages/94/24/40bbd10854f110ba5812881553d017c276e52c477dfddf151cdac3667f81/pyobjus-1.2.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f4da2314b85a57b67e1101493b94da245f03cef8465da698ff5949bec13e8d37", size = 529439, upload-time = "2025-12-29T14:48:08.741Z" }, + { url = "https://files.pythonhosted.org/packages/50/d4/730e1767fb55db56b20561180cf3553f429b78f56f24c34064fd41a893c1/pyobjus-1.2.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5ce6202c9d4f3ef62acc132ece498f7ae599061c8071c26623ad545cb37ea7ee", size = 279773, upload-time = "2025-12-29T14:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a0/fb/879560b837eedfabebe16d7fcf1ca96e9139535651d9a42acb14ef9351c5/pyobjus-1.2.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:737124a6ee884150f0d74a3193830e0cf04704334c1c41a4fc1738abec433571", size = 286164, upload-time = "2025-12-29T14:48:11.098Z" }, + { url = "https://files.pythonhosted.org/packages/34/e2/7d49052b1fe81025dd134905b2580a291aa496d3bf8e684aaf32e6ab750e/pyobjus-1.2.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7e98cd16ff66404783d0629d28a58122c6789d6b6b54dab200aa176afa35e2e4", size = 302408, upload-time = "2025-12-29T14:48:12.249Z" }, + { url = "https://files.pythonhosted.org/packages/3f/51/ed32815d349a27ff9e46f814f61420e8b6147bdd9f94b22bb0b7be17efc8/pyobjus-1.2.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:248c70d84ab026aeaecb25703042ba3e25ec72a42e5f30ff2f2b7ec1197ac621", size = 525787, upload-time = "2025-12-29T14:48:13.339Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/7626f16162c3e9a2df6eac876d5cff2a2eb31f6a9f204a421cd9d3902909/pyobjus-1.2.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1a23280fd78db520cafe01b4637b1aaa9ff09845fd74e37b9d5682914f838700", size = 282928, upload-time = "2025-12-29T14:48:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/820b8f73057cdecff00cea5b6c84becb9f73430578f6b61fa009d1c4b777/pyobjus-1.2.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:369a509de7ef951284ea448654c1b8c532fe22c2a29704a0e1636aa087df3463", size = 289512, upload-time = "2025-12-29T14:48:15.325Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7d/3c2e4f7579176c90faf22efda6840fc4f87d9486bb2e2889320fc7989241/pyobjus-1.2.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e729ecca4e37429a9b00148853e73dc36c413b415086060722fa3d7fe58ede4a", size = 303774, upload-time = "2025-12-29T14:48:16.234Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d5/0591cb8e3441b2ecaf68db91ea9b2aad10a5376b11136b69cd3498456632/pyobjus-1.2.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ef95271d1ec30d4ae61a4f6a66e6fcbf2e41e2420c3fcde5062acc6545565e3d", size = 526356, upload-time = "2025-12-29T14:48:17.155Z" }, +] + [[package]] name = "pypiwin32" version = "223" @@ -675,6 +703,8 @@ dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pillow" }, + { name = "plyer" }, + { name = "pyobjus", marker = "sys_platform == 'darwin'" }, { name = "pyserial" }, { name = "sigfig" }, { name = "toml" }, @@ -690,6 +720,8 @@ requires-dist = [ { name = "kivy", specifier = ">=2.3.1" }, { name = "numpy", specifier = ">=2.2.6" }, { name = "pillow", specifier = ">=12.1.0" }, + { name = "plyer", specifier = ">=2.1.0" }, + { name = "pyobjus", marker = "sys_platform == 'darwin'", specifier = ">=1.2.4" }, { name = "pyserial", specifier = ">=3.5" }, { name = "sigfig", specifier = ">=1.3.19" }, { name = "toml", specifier = ">=0.10.2" },