├── LICENSE ├── README.md ├── images └── py_digital_phosphor_gui.png ├── jupyter_notebook └── ig_fft_gui.ipynb └── py_digital_phosphor.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Thomas Schucker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python-Digital-Phosphor-Display 2 | Real-time GUI display for the intensity graded FFT function using RTLSDR as an input device 3 | 4 | ![](images/py_digital_phosphor_gui.png) 5 | 6 | ## Run Instructions 7 | 8 | Install the following libraries with pip 9 | ``` 10 | pip install numpy 11 | pip install dearpygui 12 | pip install pyrtlsdr 13 | ``` 14 | Run the GUI with python 15 | 16 | ``` 17 | python py_digital_phosphor.py 18 | ``` 19 | 20 | Make sure to have the RTLSDR connected via USB when running. 21 | 22 | ## Features 23 | 24 | I created a short video that shows all the current features of the GUI interface and how they relate the spectrum display. 25 | 26 | [YouTube: Python Digital Phosphor Display](https://youtu.be/GPoQYTfQMxw) 27 | 28 | 29 | ## Blog Post 30 | 31 | If you want to learn more about how I created the GUI with DearPyGui check out my blog post below. 32 | 33 | [Tea and Tech Time: Python Digital Phosphor Display with DearPyGui](https://teaandtechtime.com/python-digital-phosphor-display-with-dearpygui/) 34 | 35 | ## Notes 36 | This GUI is based on my previous work creating intensity graded FFTs with Matplotlib 37 | 38 | [Python Intensity Graded FFT Plots](https://github.com/Tschucker/Python-Intensity-Graded-FFT) 39 | -------------------------------------------------------------------------------- /images/py_digital_phosphor_gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tschucker/Python-Digital-Phosphor-Display/03b1bc724a1a2e4b910a46ae5d8e776b85289326/images/py_digital_phosphor_gui.png -------------------------------------------------------------------------------- /jupyter_notebook/ig_fft_gui.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "#Imports\n", 10 | "from dearpygui.core import *\n", 11 | "from dearpygui.simple import *\n", 12 | "import math\n", 13 | "import numpy as np\n", 14 | "from scipy import interpolate\n", 15 | "from rtlsdr import RtlSdr" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": 2, 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [ 24 | "#Default Variables\n", 25 | "fs = 2.048e6\n", 26 | "fc = 915e6\n", 27 | "fft_len = 4096\n", 28 | "fft_div = 8\n", 29 | "mag_steps = 300\n", 30 | "max_m = 0\n", 31 | "min_m = 0\n", 32 | "decay = 0.04\n", 33 | "sdr_open = False" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": 3, 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "#FFT Function\n", 43 | "def fft_intensity_gui(samples: np.ndarray, fft_len: int = 256, fft_div: int = 2, mag_steps: int = 100):\n", 44 | " \n", 45 | " num_ffts = math.floor(len(samples)/fft_len)\n", 46 | " \n", 47 | " fft_array = []\n", 48 | " for i in range(num_ffts):\n", 49 | " temp = np.fft.fftshift(np.fft.fft(samples[i*fft_len:(i+1)*fft_len]))\n", 50 | " temp_mag = 20.0 * np.log10(np.abs(temp))\n", 51 | " fft_array.append(temp_mag)\n", 52 | " \n", 53 | " max_mag = np.amax(fft_array)\n", 54 | " min_mag = np.abs(np.amin(fft_array))\n", 55 | " \n", 56 | " return(fft_array, max_mag, min_mag)" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": 4, 62 | "metadata": {}, 63 | "outputs": [], 64 | "source": [ 65 | "#Open RTLSDR\n", 66 | "try:\n", 67 | " sdr = RtlSdr()\n", 68 | " sdr.sample_rate = fs\n", 69 | " sdr.center_freq = fc\n", 70 | " sdr.gain = 'auto'\n", 71 | " sdr_open = True\n", 72 | "except:\n", 73 | " print(\"No RTLSDR found\")\n", 74 | " sdr_open = False\n" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": 5, 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "#Callbacks\n", 84 | "def retune_callback(sender, data):\n", 85 | " global max_m, min_m, sdr, fs, fc\n", 86 | " \n", 87 | " max_m = 0\n", 88 | " min_m = 0\n", 89 | " \n", 90 | " fs = float(get_value(\"fs\"))\n", 91 | " fc = float(get_value(\"fc\"))\n", 92 | " \n", 93 | " sdr.sample_rate = fs\n", 94 | " sdr.center_freq = fc\n", 95 | " \n", 96 | "def ig_fft_callback(sender, data):\n", 97 | " global max_m, min_m, sdr, sdr_open\n", 98 | " \n", 99 | " if sdr_open:\n", 100 | " samples = sdr.read_samples(fft_len)\n", 101 | " fft_array, max_mag, min_mag = fft_intensity_gui(samples, fft_len, fft_div, mag_steps)\n", 102 | " \n", 103 | " if max_mag > max_m:\n", 104 | " max_m = max_mag\n", 105 | " if min_mag > min_m:\n", 106 | " min_m = min_mag\n", 107 | " \n", 108 | " norm_fft_array = fft_array\n", 109 | " norm_fft_array[0] = (fft_array[0]+min_m)/(max_m+min_m)\n", 110 | " \n", 111 | " #update data\n", 112 | " hitmap_array = get_data(\"hitmap\")*np.exp(-decay)\n", 113 | "\n", 114 | " mag_step = 1/mag_steps\n", 115 | " \n", 116 | " for m in range(fft_len):\n", 117 | " hit_mag = int(norm_fft_array[0][m]/mag_step)\n", 118 | " hitmap_array[hit_mag][int(m/fft_div)] = hitmap_array[hit_mag][int(m/fft_div)] + .8 \n", 119 | " \n", 120 | " max_hit = np.amax(hitmap_array)/2\n", 121 | " \n", 122 | " add_heat_series(\"Spectrum\", \"\", hitmap_array, (mag_steps+1), (int(fft_len/fft_div)), 0, max_hit, format='')\n", 123 | " add_data(\"hitmap\", hitmap_array)\n", 124 | " \n", 125 | "def fft_plus_callback(sender, data):\n", 126 | " global fft_len, fft_div, mag_steps\n", 127 | " \n", 128 | " temp = int(np.log2(fft_len))\n", 129 | " fft_len = int(2**(temp+1))\n", 130 | " fft_div = int(fft_div*2)\n", 131 | " hitmap_array = np.random.random((mag_steps+1,int(fft_len/fft_div)))*np.exp(-10) \n", 132 | " add_data(\"hitmap\", hitmap_array)\n", 133 | " set_value(\"fft_size\", str(fft_len))\n", 134 | " \n", 135 | "def fft_min_callback(sender, data):\n", 136 | " global fft_len, fft_div, mag_steps\n", 137 | " \n", 138 | " temp = int(np.log2(fft_len))\n", 139 | " fft_len = int(2**(temp-1))\n", 140 | " fft_div = int(fft_div/2)\n", 141 | " hitmap_array = np.random.random((mag_steps+1,int(fft_len/fft_div)))*np.exp(-10) \n", 142 | " add_data(\"hitmap\", hitmap_array)\n", 143 | " set_value(\"fft_size\", str(fft_len))\n", 144 | " \n", 145 | "def time_plus_callback(sender, data):\n", 146 | " global decay\n", 147 | " \n", 148 | " decay = decay/2\n", 149 | " set_value(\"per_decay\", str(decay))\n", 150 | " \n", 151 | "def time_min_callback(sender, data):\n", 152 | " global decay\n", 153 | " \n", 154 | " decay = decay*2\n", 155 | " set_value(\"per_decay\", str(decay))\n", 156 | " \n", 157 | "def set_color_viridis_callback(sender, data):\n", 158 | " set_color_map(\"Spectrum\", 5) \n", 159 | "def set_color_plasma_callback(sender, data):\n", 160 | " set_color_map(\"Spectrum\", 6)\n", 161 | "def set_color_hot_callback(sender, data):\n", 162 | " set_color_map(\"Spectrum\", 7)\n", 163 | "def set_color_pink_callback(sender, data):\n", 164 | " set_color_map(\"Spectrum\", 9)\n", 165 | "def set_color_jet_callback(sender, data):\n", 166 | " set_color_map(\"Spectrum\", 10)\n", 167 | " \n", 168 | "def close_gui_callback(sender, data):\n", 169 | " stop_dearpygui()" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 6, 175 | "metadata": {}, 176 | "outputs": [], 177 | "source": [ 178 | "#GUI\n", 179 | "with window(\"Settings\", width=300, height=500):\n", 180 | " set_window_pos(\"Settings\", 700, 0)\n", 181 | " add_text(\"RTLSDR Tuning\")\n", 182 | " add_input_text(\"fs\", default_value=str(fs))\n", 183 | " add_input_text(\"fc\", default_value=str(fc))\n", 184 | " add_button(\"Retune\", callback=retune_callback)\n", 185 | " add_text(\"FFT Resolution\")\n", 186 | " with group(\"updown1\", horizontal=True):\n", 187 | " add_button(\"FFT +\", label=\"+\", callback=fft_plus_callback)\n", 188 | " add_button(\"FFT -\", label=\"-\", callback=fft_min_callback)\n", 189 | " add_label_text(\"fft_size\", label=\"FFT Length\", default_value=str(fft_len))\n", 190 | " add_text(\"Persistance\")\n", 191 | " with group(\"updown2\", horizontal=True):\n", 192 | " add_button(\"Time +\", label=\"+\", callback=time_plus_callback)\n", 193 | " add_button(\"Time -\", label=\"-\", callback=time_min_callback)\n", 194 | " add_label_text(\"per_decay\", label=\"Decay\", default_value=str(decay))\n", 195 | " \n", 196 | " \n", 197 | " \n", 198 | "with window(\"Py-Digital Phosphor\", width=700, height=500):\n", 199 | " set_window_pos(\"Py-Digital Phosphor\", 0, 0)\n", 200 | " add_plot(\"Spectrum\", height=-1, yaxis_invert=True, xaxis_no_tick_labels=True, yaxis_no_tick_labels=True)\n", 201 | " set_color_map(\"Spectrum\", 6)\n", 202 | " \n", 203 | " hitmap_array = np.random.random((mag_steps+1,int(fft_len/fft_div)))*np.exp(-10)\n", 204 | " add_data(\"hitmap\", hitmap_array)\n", 205 | " \n", 206 | " with menu_bar(\"Main Menu Bar\"):\n", 207 | " with menu(\"Main\"):\n", 208 | " with menu(\"Color Map\"):\n", 209 | " add_menu_item(\"Plasma\", callback=set_color_plasma_callback)\n", 210 | " add_menu_item(\"Hot\", callback=set_color_hot_callback)\n", 211 | " add_menu_item(\"Viridis\", callback=set_color_viridis_callback)\n", 212 | " add_menu_item(\"Jet\", callback=set_color_jet_callback)\n", 213 | " add_menu_item(\"Pink\", callback=set_color_pink_callback)\n", 214 | " \n", 215 | " set_render_callback(ig_fft_callback)\n", 216 | "\n", 217 | "if not sdr_open:\n", 218 | " with window(\"RTLSDR Error\", width=1000, height=500):\n", 219 | " set_window_pos(\"RTLSDR Error\", 0, 0)\n", 220 | " add_text(\"No RTLSDR device was found\")\n", 221 | " add_button(\"Exit\", callback=close_gui_callback)\n", 222 | " \n", 223 | "start_dearpygui()" 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": 7, 229 | "metadata": {}, 230 | "outputs": [], 231 | "source": [ 232 | "#Close SDR\n", 233 | "if sdr_open:\n", 234 | " sdr.close()" 235 | ] 236 | } 237 | ], 238 | "metadata": { 239 | "kernelspec": { 240 | "display_name": "Python 3", 241 | "language": "python", 242 | "name": "python3" 243 | }, 244 | "language_info": { 245 | "codemirror_mode": { 246 | "name": "ipython", 247 | "version": 3 248 | }, 249 | "file_extension": ".py", 250 | "mimetype": "text/x-python", 251 | "name": "python", 252 | "nbconvert_exporter": "python", 253 | "pygments_lexer": "ipython3", 254 | "version": "3.7.6" 255 | } 256 | }, 257 | "nbformat": 4, 258 | "nbformat_minor": 4 259 | } 260 | -------------------------------------------------------------------------------- /py_digital_phosphor.py: -------------------------------------------------------------------------------- 1 | #Imports 2 | from dearpygui.core import * 3 | from dearpygui.simple import * 4 | import math 5 | import numpy as np 6 | from scipy import interpolate 7 | from rtlsdr import RtlSdr 8 | 9 | #Default Variables 10 | fs = 2.048e6 11 | fc = 915e6 12 | fft_len = 4096 13 | fft_div = 8 14 | mag_steps = 300 15 | max_m = 0 16 | min_m = 0 17 | decay = 0.04 18 | sdr_open = False 19 | 20 | #FFT Function 21 | def fft_intensity_gui(samples: np.ndarray, fft_len: int = 256, fft_div: int = 2, mag_steps: int = 100): 22 | 23 | num_ffts = math.floor(len(samples)/fft_len) 24 | 25 | fft_array = [] 26 | for i in range(num_ffts): 27 | temp = np.fft.fftshift(np.fft.fft(samples[i*fft_len:(i+1)*fft_len])) 28 | temp_mag = 20.0 * np.log10(np.abs(temp)) 29 | fft_array.append(temp_mag) 30 | 31 | max_mag = np.amax(fft_array) 32 | min_mag = np.abs(np.amin(fft_array)) 33 | 34 | return(fft_array, max_mag, min_mag) 35 | 36 | #Open RTLSDR 37 | try: 38 | sdr = RtlSdr() 39 | sdr.sample_rate = fs 40 | sdr.center_freq = fc 41 | sdr.gain = 'auto' 42 | sdr_open = True 43 | except: 44 | print("No RTLSDR found") 45 | sdr_open = False 46 | 47 | #Callbacks 48 | def retune_callback(sender, data): 49 | global max_m, min_m, sdr, fs, fc 50 | 51 | max_m = 0 52 | min_m = 0 53 | 54 | fs = float(get_value("fs")) 55 | fc = float(get_value("fc")) 56 | 57 | sdr.sample_rate = fs 58 | sdr.center_freq = fc 59 | 60 | def ig_fft_callback(sender, data): 61 | global max_m, min_m, sdr, sdr_open 62 | 63 | if sdr_open: 64 | samples = sdr.read_samples(fft_len) 65 | fft_array, max_mag, min_mag = fft_intensity_gui(samples, fft_len, fft_div, mag_steps) 66 | 67 | if max_mag > max_m: 68 | max_m = max_mag 69 | if min_mag > min_m: 70 | min_m = min_mag 71 | 72 | norm_fft_array = fft_array 73 | norm_fft_array[0] = (fft_array[0]+min_m)/(max_m+min_m) 74 | 75 | #update data 76 | hitmap_array = get_data("hitmap")*np.exp(-decay) 77 | 78 | mag_step = 1/mag_steps 79 | 80 | for m in range(fft_len): 81 | hit_mag = int(norm_fft_array[0][m]/mag_step) 82 | hitmap_array[hit_mag][int(m/fft_div)] = hitmap_array[hit_mag][int(m/fft_div)] + .8 83 | 84 | max_hit = np.amax(hitmap_array)/2 85 | 86 | add_heat_series("Spectrum", "", hitmap_array, (mag_steps+1), (int(fft_len/fft_div)), 0, max_hit, format='') 87 | add_data("hitmap", hitmap_array) 88 | 89 | def fft_plus_callback(sender, data): 90 | global fft_len, fft_div, mag_steps 91 | 92 | temp = int(np.log2(fft_len)) 93 | fft_len = int(2**(temp+1)) 94 | fft_div = int(fft_div*2) 95 | hitmap_array = np.random.random((mag_steps+1,int(fft_len/fft_div)))*np.exp(-10) 96 | add_data("hitmap", hitmap_array) 97 | set_value("fft_size", str(fft_len)) 98 | 99 | def fft_min_callback(sender, data): 100 | global fft_len, fft_div, mag_steps 101 | 102 | temp = int(np.log2(fft_len)) 103 | fft_len = int(2**(temp-1)) 104 | fft_div = int(fft_div/2) 105 | hitmap_array = np.random.random((mag_steps+1,int(fft_len/fft_div)))*np.exp(-10) 106 | add_data("hitmap", hitmap_array) 107 | set_value("fft_size", str(fft_len)) 108 | 109 | def time_plus_callback(sender, data): 110 | global decay 111 | 112 | decay = decay/2 113 | set_value("per_decay", str(decay)) 114 | 115 | def time_min_callback(sender, data): 116 | global decay 117 | 118 | decay = decay*2 119 | set_value("per_decay", str(decay)) 120 | 121 | def set_color_viridis_callback(sender, data): 122 | set_color_map("Spectrum", 5) 123 | def set_color_plasma_callback(sender, data): 124 | set_color_map("Spectrum", 6) 125 | def set_color_hot_callback(sender, data): 126 | set_color_map("Spectrum", 7) 127 | def set_color_pink_callback(sender, data): 128 | set_color_map("Spectrum", 9) 129 | def set_color_jet_callback(sender, data): 130 | set_color_map("Spectrum", 10) 131 | 132 | def close_gui_callback(sender, data): 133 | stop_dearpygui() 134 | 135 | #GUI 136 | with window("Settings", width=300, height=500): 137 | set_window_pos("Settings", 700, 0) 138 | add_text("RTLSDR Tuning") 139 | add_input_text("fs", default_value=str(fs)) 140 | add_input_text("fc", default_value=str(fc)) 141 | add_button("Retune", callback=retune_callback) 142 | add_text("FFT Resolution") 143 | with group("updown1", horizontal=True): 144 | add_button("FFT +", label="+", callback=fft_plus_callback) 145 | add_button("FFT -", label="-", callback=fft_min_callback) 146 | add_label_text("fft_size", label="FFT Length", default_value=str(fft_len)) 147 | add_text("Persistance") 148 | with group("updown2", horizontal=True): 149 | add_button("Time +", label="+", callback=time_plus_callback) 150 | add_button("Time -", label="-", callback=time_min_callback) 151 | add_label_text("per_decay", label="Decay", default_value=str(decay)) 152 | 153 | 154 | 155 | with window("Py-Digital Phosphor", width=700, height=500): 156 | set_window_pos("Py-Digital Phosphor", 0, 0) 157 | add_plot("Spectrum", height=-1, yaxis_invert=True, xaxis_no_tick_labels=True, yaxis_no_tick_labels=True) 158 | set_color_map("Spectrum", 6) 159 | 160 | hitmap_array = np.random.random((mag_steps+1,int(fft_len/fft_div)))*np.exp(-10) 161 | add_data("hitmap", hitmap_array) 162 | 163 | with menu_bar("Main Menu Bar"): 164 | with menu("Main"): 165 | with menu("Color Map"): 166 | add_menu_item("Plasma", callback=set_color_plasma_callback) 167 | add_menu_item("Hot", callback=set_color_hot_callback) 168 | add_menu_item("Viridis", callback=set_color_viridis_callback) 169 | add_menu_item("Jet", callback=set_color_jet_callback) 170 | add_menu_item("Pink", callback=set_color_pink_callback) 171 | 172 | set_render_callback(ig_fft_callback) 173 | 174 | if not sdr_open: 175 | with window("RTLSDR Error", width=1000, height=500): 176 | set_window_pos("RTLSDR Error", 0, 0) 177 | add_text("No RTLSDR device was found") 178 | add_button("Exit", callback=close_gui_callback) 179 | 180 | start_dearpygui() 181 | 182 | #Close SDR 183 | if sdr_open: 184 | sdr.close() --------------------------------------------------------------------------------