From f59d3dbe36913a6feb81067bfda8ddc2c375360e Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Tue, 5 Aug 2025 22:46:43 -0700 Subject: [PATCH 1/2] implement ADSHardware.__del__ - clean up open WF_SDK references --- ads.py | 114 ++++++++++++++++++++++++ lab_10_template.py | 81 +---------------- lab_10_template_notebook.ipynb | 158 ++++++++++----------------------- 3 files changed, 162 insertions(+), 191 deletions(-) create mode 100644 ads.py diff --git a/ads.py b/ads.py new file mode 100644 index 0000000..3eabcb6 --- /dev/null +++ b/ads.py @@ -0,0 +1,114 @@ +import ctypes +import faulthandler +import logging + +from WF_SDK import device +from WF_SDK import scope +from WF_SDK import wavegen + +# initialize a log handle +# Docs: https://docs.python.org/3/library/logging.html +logger = logging.getLogger(__name__) + +# faulthandler.enable() installs fault handlers that dump traceback on +# SIGSEGV, SIGFPE, SIGABRT, SIGBUS, and SIGILL signals +# by default, the traceback is written to sys.stderr (unbuffered) +# Docs: https://docs.python.org/3/library/faulthandler.html +# Related to: https://github.com/berkeley-physics111a/lab10/issues/2 +faulthandler.enable() + + +class ADSHardware(): + """Class of functions for interfacing with the ADS. + """ + + def __init__(self): + self.handle = None + + def __del__(self): + """ + Called when an instance of ADSHardware is about to be destroyed, used to cleanup handles + Docs: https://docs.python.org/3/reference/datamodel.html#object.__del__ + """ + if self.handle is not None: + self.close_wavegen() + self.close_scope() + self.disconnect() + + def startup(self): + """Connects to the ADS. Defines 'handle', the address to the ADS. + Must be run at the beginning of every program using the ADS. + """ + logger.warning(f"[ATTEMPT] Opening ADS device") + self.handle = device.open() + logger.warning(f"[SUCCESS] Opened device {self.handle.name} handle={self.handle.handle} addr={ctypes.addressof(self.handle.handle)}") + + def open_scope(self, buffer_size=1000, sample_freq=1e6): + """Opens connection to the scope. + + Args: + buffer_size (int, optional): How many data points are temporarily stored + before being returned. The buffer is a temporary slot for storing a small amount of + data before it is transferred to its final destination. Defaults to 1000. + sample_freq (int, optional): How frequently the oscilloscope will sample + from the input. Defaults to 1e6. You can decrease this if you have too + many data points/the function is taking awhile to run for the time scale you need. + (16e3 can be a reasonable selection.) + """ + scope.open(self.handle, buffer_size=buffer_size, sampling_frequency=sample_freq) + + def trigger_scope(self, channel=1, level=0.1): + """Sets trigger level for the scope. Kind of a buggy function; not used. + + Args: + channel (int, optional): Selects which channel of scope to read out. + Defaults to 1. + level (float, optional): Sets trigger level for scope. Defaults to 0.1. + """ + scope.trigger(self.handle, enable=True, source=scope.trigger_source.analog, channel=channel, + edge_rising=True, level=level) + + def read_scope(self, channel=1): + """Collects data from the scope. + + Args: + channel (int, optional): Which channel to read from. Defaults to 1. + + Returns: + buffer (array): An array of output data points. The buffer is a temporary slot + for storing a small amount of data before it is transferred to its final destination. + """ + buffer = scope.record(self.handle, channel=channel) + return buffer + + def close_scope(self): + """Closes connection to the scope. + """ + scope.close(self.handle) + + def use_wavegen(self, channel=1, function=wavegen.function.sine, offset_v=0, freq_hz=1e3, amp_v=1): + """Runs the wavegen producing function with given parameters. + + Args: + channel (int, optional): Which channel output is at. Defaults to 1. + function (function object, optional): What type of function to output. + Defaults to wavegen.function.sine. + offset (int, optional): Voltage offset (V). Defaults to 0. + freq (int, optional): Frequency (Hz). Defaults to 1e3. + amp (int, optional): Amplitude (V). Defaults to 1. + """ + wavegen.generate(self.handle, channel=channel, function=function, offset=offset_v, + frequency=freq_hz, amplitude=amp_v) + + def close_wavegen(self): + """Closes wavegen. + """ + wavegen.close(self.handle) + + def disconnect(self): + """Closes ADS connection. Must be run at the end of every program. + """ + addr = ctypes.addressof(self.handle.handle) + logger.warning(f"[ATTEMPT] Closing device {self.handle.name} handle={self.handle.handle} addr={addr}") + device.close(self.handle) + logger.warning(f"[SUCCESS] Closed device {self.handle.name} handle={self.handle.handle} addr={addr}") diff --git a/lab_10_template.py b/lab_10_template.py index 53623b1..3be9a9c 100644 --- a/lab_10_template.py +++ b/lab_10_template.py @@ -26,86 +26,7 @@ from WF_SDK import device from WF_SDK import scope from WF_SDK import wavegen - -class ADSHardware(): - """Class of functions for interfacing with the ADS. - """ - - def __init__(self): - self.handle = None - - def startup(self): - """Connects to the ADS. Defines 'handle', the address to the ADS. - Must be run at the beginning of every program using the ADS. - """ - self.handle = device.open() - - def open_scope(self, buffer_size=1000, sample_freq=1e6): - """Opens connection to the scope. - - Args: - buffer_size (int, optional): How many data points are temporarily stored - before being returned. The buffer is a temporary slot for storing a small amount of - data before it is transferred to its final destination. Defaults to 1000. - sample_freq (int, optional): How frequently the oscilloscope will sample - from the input. Defaults to 1e6. You can decrease this if you have too - many data points/the function is taking awhile to run for the time scale you need. - (16e3 can be a reasonable selection.) - """ - scope.open(self.handle, buffer_size=buffer_size, sampling_frequency=sample_freq) - - def trigger_scope(self, channel=1, level=0.1): - """Sets trigger level for the scope. Kind of a buggy function; not used. - - Args: - channel (int, optional): Selects which channel of scope to read out. - Defaults to 1. - level (float, optional): Sets trigger level for scope. Defaults to 0.1. - """ - scope.trigger(self.handle, enable=True, source=scope.trigger_source.analog, channel=channel, - edge_rising=True, level=level) - - def read_scope(self, channel=1): - """Collects data from the scope. - - Args: - channel (int, optional): Which channel to read from. Defaults to 1. - - Returns: - buffer (array): An array of output data points. The buffer is a temporary slot - for storing a small amount of data before it is transferred to its final destination. - """ - buffer = scope.record(self.handle, channel=channel) - return buffer - - def close_scope(self): - """Closes connection to the scope. - """ - scope.close(self.handle) - - def use_wavegen(self, channel=1, function=wavegen.function.sine, offset_v=0, freq_hz=1e3, amp_v=1): - """Runs the wavegen producing function with given parameters. - - Args: - channel (int, optional): Which channel output is at. Defaults to 1. - function (function object, optional): What type of function to output. - Defaults to wavegen.function.sine. - offset (int, optional): Voltage offset (V). Defaults to 0. - freq (int, optional): Frequency (Hz). Defaults to 1e3. - amp (int, optional): Amplitude (V). Defaults to 1. - """ - wavegen.generate(self.handle, channel=channel, function=function, offset=offset_v, - frequency=freq_hz, amplitude=amp_v) - - def close_wavegen(self): - """Closes wavegen. - """ - wavegen.close(self.handle) - - def disconnect(self): - """Closes ADS connection. Must be run at the end of every program. - """ - device.close(self.handle) +from ads import ADSHardware def oscilloscope_run(ads_object: ADSHardware, n_points: int, channel: int, sampling_freq=1e6): """Collects data from the oscilloscope. diff --git a/lab_10_template_notebook.ipynb b/lab_10_template_notebook.ipynb index ff2ba79..ea15c43 100644 --- a/lab_10_template_notebook.ipynb +++ b/lab_10_template_notebook.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "7555ee07", "metadata": {}, "outputs": [], @@ -12,98 +12,10 @@ "from WF_SDK import device\n", "from WF_SDK import scope\n", "from WF_SDK import wavegen\n", - "\n", + "from ads import ADSHardware\n", "%matplotlib inline" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "b8343031", - "metadata": {}, - "outputs": [], - "source": [ - "class ADSHardware():\n", - " \"\"\"Class of functions for interfacing with the ADS.\n", - " \"\"\"\n", - "\n", - " def __init__(self):\n", - " self.handle = None\n", - "\n", - " def startup(self):\n", - " \"\"\"Connects to the ADS. Defines 'handle', the address to the ADS.\n", - " Must be run at the beginning of every program using the ADS.\n", - " \"\"\"\n", - " self.handle = device.open()\n", - "\n", - " def open_scope(self, buffer_size=1000, sample_freq=100e6):\n", - " \"\"\"Opens connection to the scope.\n", - "\n", - " Args:\n", - " buffer_size (int, optional): How many data points are temporarily stored\n", - " before being returned. The buffer is a temporary slot for storing a small amount of\n", - " data before it is transferred to its final destination. Defaults to 1000.\n", - " sample_freq (int, optional): How frequently the oscilloscope will sample\n", - " from the input. Defaults to 100e6. You can decrease this if you have too\n", - " many data points/the function is taking awhile to run for the time scale you need.\n", - " (1e6 or even 16e3 can be reasonable selections.)\n", - " \"\"\"\n", - " scope.open(self.handle, buffer_size=buffer_size, sampling_frequency=sample_freq)\n", - "\n", - " def trigger_scope(self, channel=1, level=0.1):\n", - " \"\"\"Sets trigger level for the scope. Kind of a buggy function; not used.\n", - "\n", - " Args:\n", - " channel (int, optional): Selects which channel of scope to read out. \n", - " Defaults to 1.\n", - " level (float, optional): Sets trigger level for scope. Defaults to 0.1.\n", - " \"\"\"\n", - " scope.trigger(self.handle, enable=True, source=scope.trigger_source.analog, channel=channel,\n", - " edge_rising=True, level=level)\n", - "\n", - " def read_scope(self, channel=1):\n", - " \"\"\"Collects data from the scope.\n", - "\n", - " Args:\n", - " channel (int, optional): Which channel to read from. Defaults to 1.\n", - "\n", - " Returns:\n", - " buffer (array): An array of output data points. The buffer is a temporary slot \n", - " for storing a small amount of data before it is transferred to its final destination.\n", - " \"\"\"\n", - " buffer = scope.record(self.handle, channel=channel)\n", - " return buffer\n", - "\n", - " def close_scope(self):\n", - " \"\"\"Closes connection to the scope.\n", - " \"\"\"\n", - " scope.close(self.handle)\n", - "\n", - " def use_wavegen(self, channel=1, function=wavegen.function.sine, offset_v=0, freq_hz=1e3, amp_v=1):\n", - " \"\"\"Runs the wavegen producing function with given parameters.\n", - "\n", - " Args:\n", - " channel (int, optional): Which channel output is at. Defaults to 1.\n", - " function (function object, optional): What type of function to output. \n", - " Defaults to wavegen.function.sine.\n", - " offset (int, optional): Voltage offset (V). Defaults to 0.\n", - " freq (int, optional): Frequency (Hz). Defaults to 1e3.\n", - " amp (int, optional): Amplitude (V). Defaults to 1.\n", - " \"\"\"\n", - " wavegen.generate(self.handle, channel=channel, function=function, offset=offset_v,\n", - " frequency=freq_hz, amplitude=amp_v)\n", - "\n", - " def close_wavegen(self):\n", - " \"\"\"Closes wavegen.\n", - " \"\"\"\n", - " wavegen.close(self.handle)\n", - "\n", - " def disconnect(self):\n", - " \"\"\"Closes ADS connection. Must be run at the end of every program.\n", - " \"\"\"\n", - " device.close(self.handle)" - ] - }, { "cell_type": "code", "execution_count": null, @@ -111,7 +23,9 @@ "metadata": {}, "outputs": [], "source": [ - "def oscilloscope_run(ads_object: ADSHardware, n_points: int, channel: int, sampling_freq=1e6):\n", + "def oscilloscope_run(\n", + " ads_object: ADSHardware, n_points: int, channel: int, sampling_freq=1e6\n", + "):\n", " \"\"\"Collects data from the oscilloscope.\n", "\n", " Args:\n", @@ -126,7 +40,7 @@ " Returns:\n", " data (dict): has two keys, \"x\" and \"y\" which have time (ms) and voltage (V) data\n", " \"\"\"\n", - " #test 16 khz, 1 mhz as well for sampling_freq\n", + " # test 16 khz, 1 mhz as well for sampling_freq\n", " data = {}\n", " ads_object.open_scope(sample_freq=sampling_freq)\n", " buffer = ads_object.read_scope(channel=channel)\n", @@ -135,7 +49,7 @@ " for i in range(n_points):\n", " buffer = np.mean(ads_object.read_scope(channel=channel))\n", " data[\"y\"] = np.append(data[\"y\"], buffer)\n", - " #MODIFY THE LINE BELOW THIS ONE IN L10.2(d)\n", + " # MODIFY THE LINE BELOW THIS ONE IN L10.2(d)\n", " data[\"x\"] = np.append(data[\"x\"], np.array([i]))\n", " ads_object.close_scope()\n", " return data" @@ -158,7 +72,7 @@ " containing the frequencies and magnitudes from the FFT.\n", " \"\"\"\n", " fft_result = {}\n", - " #FILL IN THIS FUNCTION FOR L10.3(b) and L10.3(c)\n", + " # FILL IN THIS FUNCTION FOR L10.3(b) and L10.3(c)\n", "\n", " fft_result[\"frequencies\"] = ...\n", " fft_result[\"magnitudes\"] = ...\n", @@ -173,16 +87,28 @@ "metadata": {}, "outputs": [], "source": [ - "wavegen_functions = {\"sine\":wavegen.function.sine, \"square\":wavegen.function.square,\n", - " \"triangle\":wavegen.function.triangle}" + "wavegen_functions = {\n", + " \"sine\": wavegen.function.sine,\n", + " \"square\": wavegen.function.square,\n", + " \"triangle\": wavegen.function.triangle,\n", + "}" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "2549832f", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[ATTEMPT] Opening ADS device\n", + "[SUCCESS] Opened device Analog Discovery 2 handle=c_int(1) addr=4459821592\n" + ] + } + ], "source": [ "ads = ADSHardware()\n", "ads.startup()" @@ -195,8 +121,9 @@ "metadata": {}, "outputs": [], "source": [ - "\n", - "ads.use_wavegen(channel=1, function=wavegen_functions[\"sine\"], offset_v=0, freq_hz=1e3, amp_v=1)\n", + "ads.use_wavegen(\n", + " channel=1, function=wavegen_functions[\"sine\"], offset_v=0, freq_hz=1e3, amp_v=1\n", + ")\n", "### FILL IN THIS LINE FOR L10.2(a)\n", "raw_data = ...\n", "ads.close_wavegen()" @@ -210,7 +137,7 @@ "outputs": [], "source": [ "### UNCOMMENT THIS CODE FOR L10.3(a)\n", - "#fft_data = fft(raw_data)" + "# fft_data = fft(raw_data)" ] }, { @@ -221,15 +148,15 @@ "outputs": [], "source": [ "### UNCOMMENT THIS CODE FOR L10.2(b)\n", - "#plt.plot(raw_data[\"x\"], raw_data[\"y\"])\n", - "#plt.xlabel('Time (ms)')\n", - "#plt.ylabel('Voltage (V)')\n", - "#plt.show()" + "# plt.plot(raw_data[\"x\"], raw_data[\"y\"])\n", + "# plt.xlabel('Time (ms)')\n", + "# plt.ylabel('Voltage (V)')\n", + "# plt.show()" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "0e8d7036", "metadata": {}, "outputs": [], @@ -244,7 +171,7 @@ "metadata": {}, "outputs": [], "source": [ - "#Only run this cell if your code errored in the middle of oscilloscope_run()\n", + "# Only run this cell if your code errored in the middle of oscilloscope_run()\n", "ads.close_scope()" ] }, @@ -253,9 +180,18 @@ "execution_count": null, "id": "e65688c3", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[ATTEMPT] Closing device Analog Discovery 2 handle=c_int(1) addr=4459821592\n", + "[SUCCESS] Closed device handle=c_int(0) addr=4459821592\n" + ] + } + ], "source": [ - "#Run this cell at the end of every test\n", + "# Run this cell at the end of every test\n", "ads.close_wavegen()\n", "ads.disconnect()" ] @@ -263,7 +199,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "phys111a-lab10-py3.11", "language": "python", "name": "python3" }, @@ -277,7 +213,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.3" + "version": "3.11.12" } }, "nbformat": 4, From 6a0c86a23299d56408cd240808b98fdc034127e5 Mon Sep 17 00:00:00 2001 From: Leigh Johnson Date: Tue, 5 Aug 2025 22:50:40 -0700 Subject: [PATCH 2/2] clean lab_10_template_notebook.ipynb output --- lab_10_template_notebook.ipynb | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/lab_10_template_notebook.ipynb b/lab_10_template_notebook.ipynb index ea15c43..670bf06 100644 --- a/lab_10_template_notebook.ipynb +++ b/lab_10_template_notebook.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "7555ee07", "metadata": {}, "outputs": [], @@ -96,19 +96,10 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "2549832f", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[ATTEMPT] Opening ADS device\n", - "[SUCCESS] Opened device Analog Discovery 2 handle=c_int(1) addr=4459821592\n" - ] - } - ], + "outputs": [], "source": [ "ads = ADSHardware()\n", "ads.startup()" @@ -156,7 +147,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "0e8d7036", "metadata": {}, "outputs": [], @@ -180,16 +171,7 @@ "execution_count": null, "id": "e65688c3", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[ATTEMPT] Closing device Analog Discovery 2 handle=c_int(1) addr=4459821592\n", - "[SUCCESS] Closed device handle=c_int(0) addr=4459821592\n" - ] - } - ], + "outputs": [], "source": [ "# Run this cell at the end of every test\n", "ads.close_wavegen()\n",