{"id":4682,"date":"2024-08-28T12:00:59","date_gmt":"2024-08-28T19:00:59","guid":{"rendered":"https:\/\/www.cloudacm.com\/?p=4682"},"modified":"2024-08-19T06:38:30","modified_gmt":"2024-08-19T13:38:30","slug":"solving-data-mysteries-with-plots","status":"publish","type":"post","link":"https:\/\/www.cloudacm.com\/?p=4682","title":{"rendered":"Solving Data Mysteries with Plots"},"content":{"rendered":"<p>This post will expand on the topic of data plotting which was discussed in this post, <a href=\"https:\/\/www.cloudacm.com\/?p=4136\">https:\/\/www.cloudacm.com\/?p=4136<\/a>. The concept is to give a quick at a glance view of information. Details can be lost in lengthy data sets formatted in tables or flat files, which are tedious to sift through.\u00a0 <a href=\"https:\/\/www.forbes.com\/councils\/forbestechcouncil\/2020\/02\/11\/how-organizations-can-avoid-alternative-data-fatigue\/\">Data fatigue<\/a> is problem for those not prepared to handle large amounts of information.\u00a0 The apathy it can foster can turn a resource into a liability.\u00a0 By using plots, it&#8217;s the hope that the fatigue can be reduced.<\/p>\n<p>Below are plot examples. Some of these plots were originally intended to represent data of a given nature.\u00a0 However, the limitations became clear and this led to other plot methods being used. Those details will be covered in the following examples.<\/p>\n<p><strong>Interpolated Heat-map Plots<\/strong><br \/>\nThis was a fundamental plot that was covered in the earlier post. It fills in missing data points from the available data. Details can be found here, <a href=\"https:\/\/docs.astropy.org\/en\/stable\/convolution\/index.html\">https:\/\/docs.astropy.org\/en\/stable\/convolution\/index.html<\/a>. The limitation of this type of plot is that the sparsely available data points should be evenly distributed across the array. Using data sets that are grouped on one region of the array would result in a plot that poorly represents the entire array.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Heatmap_rev2.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-4683 size-full\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Heatmap_rev2.png\" alt=\"\" width=\"1000\" height=\"600\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Heatmap_rev2.png 1000w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Heatmap_rev2-300x180.png 300w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Heatmap_rev2-768x461.png 768w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Heatmap_rev2-450x270.png 450w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/a><\/p>\n<p>The plot was created from this data set file called &#8220;Heatmap.csv&#8221;.\u00a0 Here is the content of that file.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">x,y,dBm\r\n84,211,-66\r\n45,541,-85\r\n150,223,-71\r\n152,400,-72\r\n208,504,-81\r\n297,160,-66\r\n403,259,-70\r\n442,518,-79\r\n483,166,-53\r\n537,257,-60\r\n557,440,-63<\/pre>\n<p>The python script used to create the plot image is as follows.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import matplotlib.pyplot as plt\r\nimport numpy as np\r\nfrom scipy.interpolate import Rbf  # radial basis functions\r\nimport pandas as pd\r\n\r\n# Create the figure with a colored background\r\nfig = plt.figure(facecolor='#111111')\r\n\r\n# Read the CSV file\r\ndf = pd.read_csv('Heatmap.csv')\r\n\r\n# Extract the columns into arrays\r\nx = df['x'].values\r\ny = df['y'].values\r\nz = df['dBm'].values\r\n\r\n# RBF Func\r\nrbf_fun = Rbf(x, y, z, function='linear')\r\n\r\nx_new = np.linspace(0, 774, 81)\r\ny_new = np.linspace(0, 609, 82)\r\n\r\nx_grid, y_grid = np.meshgrid(x_new, y_new)\r\nz_new = rbf_fun(x_grid.ravel(), y_grid.ravel()).reshape(x_grid.shape)\r\n\r\n# Create the plot with a black background\r\nplt.pcolor(x_new, y_new, z_new, cmap=plt.cm.inferno)\r\nplt.plot(x, y, '.', color='black')\r\n# Marker styles - https:\/\/matplotlib.org\/stable\/api\/markers_api.html\r\n\r\n# Flip the y-axis\r\nplt.gca().invert_yaxis()\r\n\r\n# Set the text and labels to white\r\nplt.xlabel('x', color='white')\r\nplt.ylabel('y', color='white')\r\nplt.title('WiFi dBm Level Interpolation Map', color='white')\r\n\r\n# Set the tick labels to white\r\nplt.gca().tick_params(axis='x', colors='white')\r\nplt.gca().tick_params(axis='y', colors='white')\r\n\r\n# Set the colorbar tick labels to white\r\ncbar = plt.colorbar()\r\ncbar.ax.set_title('dBm', color='white')\r\ncbar.ax.yaxis.set_tick_params(color='white')\r\nplt.setp(plt.getp(cbar.ax.axes, 'yticklabels'), color='white')\r\n\r\n# Scale the plot\r\nF = plt.gcf()\r\nSize = F.get_size_inches()\r\nF.set_size_inches(10, 6)\r\n\r\n# Show the plot\r\n# plt.show()\r\n\r\n# Save the plot to an image file\r\nplt.savefig('Heatmap_rev2.png', facecolor=fig.get_facecolor())<\/pre>\n<p><strong>X Y Scatter Map Plots<\/strong><br \/>\nThis plot addresses the issue of data set grouping. The data is clearly presented, whereas an interpolated heat-map would misrepresent the data. This plot method lends itself well to data collection that isn&#8217;t sparsely distributed. Data collection along a linear path is easier to understand.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Heatmap_example4.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4684\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Heatmap_example4.png\" alt=\"\" width=\"700\" height=\"500\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Heatmap_example4.png 700w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Heatmap_example4-300x214.png 300w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Heatmap_example4-378x270.png 378w\" sizes=\"auto, (max-width: 700px) 100vw, 700px\" \/><\/a><\/p>\n<p>The &#8220;dB Levels from AP&#8221; plot was created from this data set file called &#8220;Walk 1 Modified 1.csv&#8221;.\u00a0 Here is the content of that file.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">latitude,longitude,dB\r\n45.6230045250113,-123.943566307541,51\r\n45.623003686821,-123.943566726636,64\r\n45.6230130058772,-123.943584069679,51\r\n45.6230464727475,-123.943560211466,45\r\n45.6230522372454,-123.943588495543,50\r\n45.6230514542379,-123.943646113292,26\r\n45.6230331887133,-123.943659706114,28\r\n45.6230278109592,-123.943669336702,36\r\n45.6230324078192,-123.943658347377,29\r\n45.623013621687,-123.943694105842,35\r\n45.6230161084721,-123.94388152638,22\r\n45.623026389686,-123.944034475971,20\r\n45.6230828880484,-123.944061958182,22\r\n45.6232268704852,-123.944003476674,13\r\n45.6234610491972,-123.943965270902,6\r\n45.6227654362132,-123.944021701665,8\r\n45.6229013909339,-123.944003657099,9\r\n45.622977980632,-123.943984013679,20\r\n45.6230378102009,-123.943905805539,21\r\n45.6230241161965,-123.943767929974,33\r\n45.6230300339537,-123.943641204668,45\r\n45.6230406270525,-123.943643787416,26\r\n45.6230383571552,-123.943630397927,39\r\n45.6230462019293,-123.943590268304,32\r\n45.623071628887,-123.943540407012,39\r\n45.6230697504328,-123.943534749932,43\r\n45.6230595847248,-123.943563926808,46\r\n45.6229937469911,-123.943580520518,64\r\n45.6229876694104,-123.943603757507,56\r\n45.6229820100337,-123.943600383094,59\r\n45.622966645897,-123.943648465306,64\r\n<\/pre>\n<p>The python script used to create the plot image is as follows.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\"># See, https:\/\/stackabuse.com\/plotly-scatter-plot-tutorial-with-examples\/\r\n\r\nimport pandas as pd\r\nimport plotly.express as px\r\n\r\n\r\ndf = pd.read_csv('Walk 1 Modified 1.csv')\r\n\r\nfig = px.scatter_mapbox(df, \r\n    lat = 'latitude', \r\n    lon = 'longitude', \r\n    color = 'dB',\r\n    size = 'dB',\r\n    range_color = (0,70),\r\n    center = dict(lat = 45.623081, lon = -123.943805),\r\n    zoom = 18,\r\n    mapbox_style = 'open-street-map',\r\n    title = 'dB Levels from AP',\r\n    template='plotly_dark' )\r\n    \r\n        \r\nfig.write_image(\"Heatmap_example4.png\") \r\n\r\n\r\n\r\n<\/pre>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Drive-1-MPH-Rev-2.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4685\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Drive-1-MPH-Rev-2.png\" alt=\"\" width=\"800\" height=\"600\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Drive-1-MPH-Rev-2.png 800w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Drive-1-MPH-Rev-2-300x225.png 300w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Drive-1-MPH-Rev-2-768x576.png 768w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Drive-1-MPH-Rev-2-360x270.png 360w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><\/a><\/p>\n<p>The &#8220;MPH Heatmap&#8221; plot was created from this data set file called &#8220;Drive 1 MPH.csv&#8221;.\u00a0 The data is available here for download, <a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Drive-1-MPH.csv\">Drive 1 MPH<\/a><\/p>\n<p>The python script used to create the plot image is as follows.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\"># See, https:\/\/stackabuse.com\/plotly-scatter-plot-tutorial-with-examples\/\r\n\r\nimport pandas as pd\r\nimport plotly.express as px\r\n\r\n\r\ndf = pd.read_csv('Drive 1 MPH.csv')\r\n\r\nfig = px.scatter_mapbox(df, \r\n    lat = 'latitude', \r\n    lon = 'longitude', \r\n    color = 'mph',\r\n    size = 'mph',\r\n    range_color = (0,60),\r\n    center = dict(lat = 45.549226, lon = -123.896205),\r\n    zoom = 10,\r\n    mapbox_style = 'open-street-map',\r\n    title = 'MPH Heatmap',\r\n    width = 800,\r\n    height = 600,\r\n    template='plotly_dark' )\r\n    \r\n        \r\nfig.write_image('Drive 1 MPH Rev 2.png')<\/pre>\n<p><strong>X Y Scatter Plots<\/strong><br \/>\nThe method of scatter plots is well suited when merging data values from different sources and scales. This plot shows AP channels at a given point in time and their corresponding power level.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/ReProcess-CSV5_00020.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4686\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/ReProcess-CSV5_00020.png\" alt=\"\" width=\"700\" height=\"500\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/ReProcess-CSV5_00020.png 700w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/ReProcess-CSV5_00020-300x214.png 300w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/ReProcess-CSV5_00020-378x270.png 378w\" sizes=\"auto, (max-width: 700px) 100vw, 700px\" \/><\/a><\/p>\n<p>The &#8220;AP Channels and dBM Levels&#8221; plot was created from a data set file called &#8220;Wifi_Readings.csv&#8221;.\u00a0 The information contained in this file is confidential and can not be released to the public.\u00a0 It was generated from an ESP32-Cam module that performed a wifi scan and wrote the results to the microSD media.\u00a0 The format of the readings followed this format.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\">13631,5,SSID-Name-1,-64,AA:BB:CC:DD:EE:FF,1,WPA2\r\n15447,5,SSID-Name-1,-91,BB:CC:DD:EE:FF:AA,6,WPA2\r\n15578,5,SSID-Name-2,-91,CC:DD:EE:FF:AA:BB,11,WPA2\r\n15807,5,*.*Hidden*.*,-91,DD:EE:FF:AA:BB:CC,11,WPA2\r\n15924,5,*.*Hidden*.*,-92,EE:FF:AA:BB:CC:DD,11,WPA2<\/pre>\n<p>The following bash script was used to process the csv data set file to allow each scan moment to be plotted, allowing a series of images to be created spanning the entire survey time.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\">#!\/bin\/bash\r\n# cloudacm.com\r\n# a shell script to convert ESP32 Wifi Scan Data into dBm Channel Plots.\r\n\r\n\r\nsed 's\/^\\s*$\/Time in millis,Networks Found,SSID,dBm,AP Mac Address,Channel,Security\/' Wifi_Readings.csv &gt; Process-CSV1.tmp\r\n\r\nsed -e 's\/$\/^\/g' Process-CSV1.tmp &gt; Process-CSV2.tmp\r\n\r\ntr -d '\\n' &lt; Process-CSV2.tmp &gt; Process-CSV3.tmp\r\n\r\nsed -e 's\/Time in millis,\/\\nTime in millis,\/g' Process-CSV3.tmp &gt; Process-CSV4.tmp\r\n\r\nsplit -l 1 Process-CSV4.tmp Process-CSV5_ -a 5 -d\r\n\r\n\r\nfor i in Process-CSV5_*; do \r\n    echo \"$i\"\r\n    tr \"^\" \"\\n\" &lt; \"$i\" &gt; \"Re$i\"\r\ndone\r\n\r\nfor i in ReProcess-CSV5_*; do \r\n    echo \"$i\"\r\n    python dBm-Channel-Readings-FileArgument.py \"$i\"\r\n    # sleep .5\r\ndone\r\n\r\nexit\r\n\r\n# \r\n<\/pre>\n<p>Here is the python code that is called by the bash script.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import pandas as pd\r\nimport plotly.express as px\r\nimport sys\r\nimport os\r\n\r\npath = sys.argv[1]\r\n\r\n# Check if the file is empty or has only headers\r\ndf = pd.read_csv(path)\r\nif df.empty:\r\n    print(f\"Skipping file with no data: {path}\")\r\n    sys.exit()\r\n\r\nfig = px.scatter(df, \r\n    x='Channel', \r\n    y='dBm', \r\n    color='dBm', \r\n    range_color=(-100,0), \r\n    template='plotly_dark',\r\n    color_continuous_scale='Plasma')\r\n\r\n# fig.show()\r\nfig.update_layout(title=\"AP Channels and dBm Levels\", \r\n    xaxis_range=[0,15], yaxis_range=[-100,0], \r\n    paper_bgcolor=\"#111111\", plot_bgcolor='#000000')\r\n        \r\nfig.update_traces(marker=dict(size=20,\r\n        line=dict(width=1,\r\n        color='silver')),\r\n        selector=dict(mode='markers'))\r\n\r\nfig.write_image(f\"{(path)}.png\")<\/pre>\n<p>The plot that follows shows RF levels over a period of time. The time scale of this plot spans the entire time of the data survey.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/NRF24L01_PowerReadings.csv.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4687\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/NRF24L01_PowerReadings.csv.png\" alt=\"\" width=\"700\" height=\"500\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/NRF24L01_PowerReadings.csv.png 700w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/NRF24L01_PowerReadings.csv-300x214.png 300w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/NRF24L01_PowerReadings.csv-378x270.png 378w\" sizes=\"auto, (max-width: 700px) 100vw, 700px\" \/><\/a><\/p>\n<p>The &#8220;RF Levels over Time&#8221; plot was created from this data set file called &#8220;NRF24L01_PowerReadings.csv&#8221;.\u00a0 The data is available here for download, <a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/NRF24L01_PowerReadings.csv\">NRF24L01_PowerReadings<\/a><\/p>\n<p>The following bash script was used to allow the data set to be passed into the python script as an argument.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\">#!\/bin\/bash\r\n# cloudacm.com\r\n# a shell script to plot NRF24Lo1 Power Levels.\r\n\r\npython NRF24L01_PowerReadings.py NRF24L01_PowerReadings.csv\r\nsleep 10\r\nexit\r\n\r\n# \r\n<\/pre>\n<p>Here is the python code that is called by the bash script.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import pandas as pd\r\nimport plotly.express as px\r\nimport sys\r\nimport os\r\n\r\npath = sys.argv[1]\r\n\r\n# Check if the file is empty or has only headers\r\ndf = pd.read_csv(path)\r\nif df.empty:\r\n    print(f\"Skipping file with no data: {path}\")\r\n    sys.exit()\r\n\r\nfig = px.scatter(df, \r\n    x='millis', \r\n    y='level', \r\n    color='level', \r\n    range_color=(0,30), \r\n    template='plotly_dark',\r\n    color_continuous_scale='Plasma')\r\n\r\n# fig.show()\r\nfig.update_layout(title=\"RF Levels over Time\", \r\n    xaxis_range=[0,2292858], yaxis_range=[0,30], \r\n    paper_bgcolor=\"#111111\", plot_bgcolor='#000000')\r\n        \r\nfig.update_traces(marker=dict(size=5),\r\n        selector=dict(mode='markers'))\r\n\r\nfig.write_image(f\"{(path)}.png\")<\/pre>\n<p><strong>Line-space Plots<\/strong><br \/>\nThis plot is similar to the earlier AP channel plot. But the plot uses a line and area fill that contains a gradient value defined by the power level. The intensity of the larger values are highlighted, giving focus to the observer.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/X_Process_NRF-CSV_rev1_00226.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4688\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/X_Process_NRF-CSV_rev1_00226.png\" alt=\"\" width=\"640\" height=\"480\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/X_Process_NRF-CSV_rev1_00226.png 640w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/X_Process_NRF-CSV_rev1_00226-300x225.png 300w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/X_Process_NRF-CSV_rev1_00226-360x270.png 360w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/><\/a><\/p>\n<p>The following bash script was used to process the NRF data set and call the python script for each image.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\">#!\/bin\/bash\r\n# cloudacm.com\r\n# a shell script to convert NRF24L01 data into Plots.\r\n\r\n\r\nsplit -l 1 NRF24L01_Readings.csv X_Process_NRF-CSV_rev1_ -a 5 -d\r\n\r\nfor i in X_Process_NRF-CSV_rev1_*; do # Whitespace-safe but not recursive.\r\n    echo \"$i\"\r\n    python Process_NRF-CSV_rev1.py \"$i\"\r\n    # sleep .5\r\ndone\r\n\r\nexit\r\n\r\n# \r\n<\/pre>\n<p>Here is the python script that the bash script calls.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import matplotlib.pyplot as plt\r\nimport numpy as np\r\n# Uncomment for a custom colormap\r\n# from matplotlib.colors import LinearSegmentedColormap\r\nimport csv\r\nimport sys\r\n\r\npath = sys.argv[1]\r\n\r\n# Read data from readings.csv\r\nwith open((path), 'r') as file:\r\n    reader = csv.reader(file)\r\n    y = list(map(int, next(reader)))\r\n\r\n# Generate x values\r\nx = np.linspace(0, len(y), len(y))\r\n\r\n# Create a custom colormap\r\n# cmap = LinearSegmentedColormap.from_list('gradient', ['red', 'yellow'])\r\ncmap = plt.cm.get_cmap('plasma')\r\n\r\n# Create the plot\r\nfig, ax = plt.subplots()\r\nfig.patch.set_facecolor('#111111')\r\nax.set_facecolor('black')\r\nax.spines['bottom'].set_color('white')\r\nax.spines['top'].set_color('white')\r\nax.spines['right'].set_color('white')\r\nax.spines['left'].set_color('white')\r\nax.xaxis.label.set_color('white')\r\nax.yaxis.label.set_color('white')\r\nax.title.set_color('white')\r\nax.tick_params(axis='x', colors='white')\r\nax.tick_params(axis='y', colors='white')\r\nline = ax.plot(x, y, label='Data Line', color='white', alpha=0.25)\r\n\r\n# Fill the area underneath the line with a vertical gradient\r\nfor i in range(len(x)-1):\r\n    ax.fill_between(x[i:i+2], y[i:i+2], color=cmap(y[i]\/max(y)), alpha=1)\r\n\r\n# Add labels and title\r\nplt.xlabel('2.4 Ghz Band')\r\nplt.ylabel('Power Level')\r\nplt.title('NRF24L01 2.4Ghz Band Readings')\r\n\r\n# Show the plot\r\n# plt.show()\r\nplt.savefig(f\"{(path)}\", facecolor=fig.get_facecolor())\r\n\r\n<\/pre>\n<p>Here is the data set from the NRF24L01 which was captured using an ESP32-Cam module to the microSD media, <a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/NRF24L01_Readings.csv\">NRF24L01_Readings<\/a><\/p>\n<p><strong>Polar Plots<\/strong><br \/>\nThe polar plot is ideal when data is gathered from a given direction. This gives a top down view of the data collection occurring at the center point. The source can be located using this method.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Sample-1_Networks_Readings-rev4.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4689\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Sample-1_Networks_Readings-rev4.png\" alt=\"\" width=\"700\" height=\"500\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Sample-1_Networks_Readings-rev4.png 700w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Sample-1_Networks_Readings-rev4-300x214.png 300w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Sample-1_Networks_Readings-rev4-378x270.png 378w\" sizes=\"auto, (max-width: 700px) 100vw, 700px\" \/><\/a><\/p>\n<p>This is the python script used to create the plot.\u00a0 The data set is available here for download, <a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/Sample-1_Networks_Readings.csv\">Sample-1_Networks_Readings<\/a><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import pandas as pd\r\nimport plotly.express as px\r\n\r\ndf = pd.read_csv('Sample-1_Networks_Readings.csv')\r\nfig = px.scatter_polar(df,\r\n         r=\"Networks\", \r\n         theta=\"Heading\", \r\n         color=\"Networks\", \r\n         template='plotly_dark',\r\n         color_continuous_scale='Plasma')\r\n         \r\nfig.update_layout(title=\"AP Count\", \r\n    paper_bgcolor=\"#111111\")\r\n        \r\nfig.update_polars(bgcolor='#000000')\t\r\n\r\nfig.write_image('Sample-1_Networks_Readings-rev4.png')\r\n\r\n\r\n# See https:\/\/plotly.com\/python\/colorscales\/\r\n# or https:\/\/plotly.com\/python\/builtin-colorscales\/\r\n# color_continuous_scale='viridis'\r\n# color_continuous_scale='viridis_r'\r\n# color_continuous_scale='plasma_r'\r\n# color_continuous_scale='ice'\r\n# color_continuous_scale=[\"red\", \"green\", \"blue\"]<\/pre>\n<p><strong>Waterfall Plots<\/strong><br \/>\nThese plots are time based, typically with recent readings at the bottom of the plot window. Fast Fourier transform data is often represented with a waterfall plot to show deviation over time.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/NRF24L01_Waterfall_skip_99.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-4690\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/NRF24L01_Waterfall_skip_99.png\" alt=\"\" width=\"700\" height=\"500\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/NRF24L01_Waterfall_skip_99.png 700w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/NRF24L01_Waterfall_skip_99-300x214.png 300w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/NRF24L01_Waterfall_skip_99-378x270.png 378w\" sizes=\"auto, (max-width: 700px) 100vw, 700px\" \/><\/a><\/p>\n<p>Here is the bash script used to call the python script.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\">#!\/bin\/bash\r\n# cloudacm.com\r\n# a shell script to convert NRF24L01 Data into waterfall Plots.\r\n\r\npython NRF24L01_Waterfall.py\r\n\r\nexit\r\n\r\n#<\/pre>\n<p>This is the python code used to plot each instance of the NRF24L01 data with some blank buffering at the beginning.\u00a0 The data set can be downloaded here, <a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2024\/08\/NRF24L01_Readings-1.csv\">NRF24L01_Readings<\/a><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\">import sys, os\r\nimport plotly.express as px\r\nfrom numpy import genfromtxt\r\n\r\n# Read the CSV file to get the total number of rows\r\nwith open('NRF24L01_Readings.csv', 'r') as file:\r\n    total_rows = sum(1 for row in file)\r\n\r\n# Define the max_rows value\r\nmax_rows = 90\r\n\r\n# Initialize skip_header value\r\nskip_header = 0\r\n\r\nwhile skip_header &lt;= total_rows - max_rows:\r\n    # Read the data with the current skip_header value\r\n    numpy_array = genfromtxt(\r\n        'NRF24L01_Readings.csv', \r\n        delimiter=',',\r\n        skip_header=skip_header,\r\n        max_rows=max_rows\r\n    )\r\n\r\n    # Create the plot\r\n    fig = px.imshow(numpy_array, color_continuous_scale='plasma')\r\n\r\n    fig.update_layout(\r\n        title=\"RF Levels over Time\",\r\n        paper_bgcolor=\"#111111\",\r\n        coloraxis_colorbar=dict(title=\"Intensity\"),\r\n        font=dict(color=\"white\")\r\n    )\r\n\r\n    fig.update_xaxes(\r\n        showticklabels=True, \r\n        title_text=\"2.4 Ghz RF Band (128 bit)\",\r\n        tickfont=dict(color=\"white\")\r\n    ).update_yaxes(\r\n        showticklabels=True, \r\n        title_text=\"Time (Newest at bottom to Oldest on top)\",\r\n        tickfont=dict(color=\"white\")\r\n    )\r\n\r\n    # Save the image with the skip_header value in the filename\r\n    fig.write_image(f\"NRF24L01_Waterfall_skip_{skip_header}.png\")\r\n\r\n    # Increment the skip_header value by 1\r\n    skip_header += 1<\/pre>\n<p>The data in these plots were captured using an ESP32-Cam module, Arduino Pro Mini, and\/or NRF24L01 module.\u00a0 Data was also captured in tandem using the iPhone app SensorLog.\u00a0 <a href=\"https:\/\/apps.apple.com\/us\/app\/sensorlog\/id388014573\">Here is a link to the app<\/a>.\u00a0 The app can be used on iPads and the Apple Watch.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4109.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-4525 size-large\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4109-1024x540.png\" alt=\"\" width=\"640\" height=\"338\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4109-1024x540.png 1024w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4109-300x158.png 300w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4109-768x405.png 768w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4109-512x270.png 512w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4109.png 1226w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/><\/a><\/p>\n<p>Here is the code used for the stand alone ESP32-Cam module to survey WiFi Networks.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"c\">\/*\r\n *  This sketch demonstrates how to scan WiFi networks.\r\n *  The API is based on the Arduino WiFi Shield library, but has significant changes as newer WiFi functions are supported.\r\n *  E.g. the return value of `encryptionType()` different because more modern encryption is supported.\r\n *\/\r\n#include \"WiFi.h\"\r\n#include \"FS.h\"                \/\/ SD Card ESP32\r\n#include \"SD_MMC.h\"            \/\/ SD Card ESP32\r\nconst char* WifiScanReadings = \"\/Wifi-Scan_Readings.csv\";\r\n\r\nvoid HardwareSetup()\r\n{\r\n    \/\/ Set WiFi to station mode and disconnect from an AP if it was previously connected.\r\n    WiFi.mode(WIFI_STA);\r\n    WiFi.disconnect();\r\n    delay(100);\r\n}\r\n\r\nvoid PrintHeader()\r\n{\r\n   appendFile(SD_MMC, WifiScanReadings, \"ESP32-Cam-Wifi-Scan-MicroSD_Ver3\");\r\n   appendFile(SD_MMC, WifiScanReadings, \"\\n\");\r\n   appendFile(SD_MMC, WifiScanReadings, \"Time in millis,Networks Found,SSID,dBm,AP Mac Address,Channel,Security\");\r\n   appendFile(SD_MMC, WifiScanReadings, \"\\n\");\r\n   appendFile(SD_MMC, WifiScanReadings, \"\\n\");\r\n}\r\n\r\nvoid StartupMicroSD() {  \r\n  if(!SD_MMC.begin()){\r\n    return;\r\n  } \r\n  uint8_t cardType = SD_MMC.cardType();\r\n  if(cardType == CARD_NONE){\r\n    return;\r\n  }  \r\n}\r\n\r\n\r\n\/\/Append to the end of file in SD card\r\nvoid appendFile(fs::FS &amp;fs, const char * path, const char * message){\r\n\r\n    File file = fs.open(path, FILE_APPEND);\r\n    if(!file){\r\n        return;\r\n    }\r\n    if(file.print(message)){\r\n    } else {\r\n    }\r\n}\r\n\r\n\r\nvoid ScanWifiNetworks()\r\n{\r\n      \/\/ WiFi.scanNetworks will return the number of networks found.\r\n    int IntWifiNetworks = WiFi.scanNetworks(\/*async=*\/false, \/*hidden=*\/true);\r\n         \r\n    if (IntWifiNetworks == 0) {\r\n    }\r\n    \r\n    else {\r\n      \r\n        for (int NetworkCount = 0; NetworkCount &lt; IntWifiNetworks; ++NetworkCount) \r\n           {\r\n      \r\n            String MillisString = String(millis());         \/\/ read string until meet newline character  \r\n            appendFile(SD_MMC, WifiScanReadings, MillisString.c_str()); \r\n            appendFile(SD_MMC, WifiScanReadings, \",\");\r\n        \r\n            String StringWifiNetworks = String(IntWifiNetworks);         \/\/ read string until meet newline character  \r\n            appendFile(SD_MMC, WifiScanReadings, StringWifiNetworks.c_str()); \r\n            appendFile(SD_MMC, WifiScanReadings, \",\");\r\n            \r\n            String StringSSID = String(WiFi.SSID(NetworkCount));\r\n            if(StringSSID != NULL) {\r\n              appendFile(SD_MMC, WifiScanReadings, StringSSID.c_str()); \r\n              }\r\n            else {\r\n              appendFile(SD_MMC, WifiScanReadings, \"*.*Hidden*.*\");\r\n              }\r\n            appendFile(SD_MMC, WifiScanReadings, \",\");\r\n           \r\n            String StringRSSI = String(WiFi.RSSI(NetworkCount));\r\n            appendFile(SD_MMC, WifiScanReadings, StringRSSI.c_str()); \r\n            appendFile(SD_MMC, WifiScanReadings, \",\");\r\n\r\n            String StringBSSIDstr = String(WiFi.BSSIDstr(NetworkCount));\r\n            appendFile(SD_MMC, WifiScanReadings, StringBSSIDstr.c_str()); \r\n            appendFile(SD_MMC, WifiScanReadings, \",\");\r\n\r\n\r\n            String StringChannel = String(WiFi.channel(NetworkCount));\r\n            appendFile(SD_MMC, WifiScanReadings, StringChannel.c_str()); \r\n            appendFile(SD_MMC, WifiScanReadings, \",\");\r\n\r\n            printEncryptionType(WiFi.encryptionType(NetworkCount));\r\n            appendFile(SD_MMC, WifiScanReadings, \"\\n\");\r\n           }\r\n       }\r\n\r\n      \/\/ Delete the scan result to free memory for code below.\r\n      WiFi.scanDelete();\r\n}\r\n\r\n\r\nvoid printEncryptionType(int thisType) {\r\n\r\n  \/\/ read the encryption type and print out the name:\r\n  switch (thisType) {\r\n            case WIFI_AUTH_OPEN:\r\n                appendFile(SD_MMC, WifiScanReadings, \"open\");\r\n                break;\r\n            case WIFI_AUTH_WEP:\r\n                appendFile(SD_MMC, WifiScanReadings, \"WEP\");\r\n                break;\r\n            case WIFI_AUTH_WPA_PSK:\r\n                appendFile(SD_MMC, WifiScanReadings, \"WPA\");\r\n                break;\r\n            case WIFI_AUTH_WPA2_PSK:\r\n                appendFile(SD_MMC, WifiScanReadings, \"WPA2\");\r\n                break;\r\n            case WIFI_AUTH_WPA_WPA2_PSK:\r\n                appendFile(SD_MMC, WifiScanReadings, \"WPA+WPA2\");\r\n                break;\r\n            case WIFI_AUTH_WPA2_ENTERPRISE:\r\n                appendFile(SD_MMC, WifiScanReadings, \"WPA2-EAP\");\r\n                break;\r\n            case WIFI_AUTH_WPA3_PSK:\r\n                appendFile(SD_MMC, WifiScanReadings, \"WPA3\");\r\n                break;\r\n            case WIFI_AUTH_WPA2_WPA3_PSK:\r\n                appendFile(SD_MMC, WifiScanReadings, \"WPA2+WPA3\");\r\n                break;\r\n            case WIFI_AUTH_WAPI_PSK:\r\n                appendFile(SD_MMC, WifiScanReadings, \"WAPI\");\r\n                break;\r\n            default:\r\n                appendFile(SD_MMC, WifiScanReadings, \"unknown\");\r\n  }\r\n}\r\n\r\n\r\nvoid setup() {\r\n  StartupMicroSD();\r\n  HardwareSetup();\r\n  PrintHeader();\r\n\r\n  \/\/ Let things settle before logging\r\n  delay(1000);   \r\n}\r\n\r\n\r\n\r\n\r\n\r\nvoid loop() {\r\n  \/\/ This can take 12 seconds to complete\r\n  ScanWifiNetworks();\r\n  appendFile(SD_MMC, WifiScanReadings, \"\\n\");    \r\n}\r\n<\/pre>\n<p>The following code is in two parts, the first is the Arduino Pro Mini code that interfaces with the NRF24L01 and provides a serial stream to the ESP32-Cam module.\u00a0 The second part is the ESP-32 Cam module code that listens for the serial stream from the Pro Mini and then performs a wifi scan once the stream is finished, all data is then written to the microSD media.\u00a0 Oh yeah, I almost forgot to mention that it also takes a photo and saves the image file.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"c\">#include &lt;SPI.h&gt;\r\n\r\n\/\/ Increased define CHANNELS from 64 to 256 in attempt to increase resolution\r\n\r\n\/\/ Poor Man's Wireless 2.4GHz Scanner\r\n\/\/ See, https:\/\/forum.arduino.cc\/t\/poor-mans-2-4-ghz-scanner\/54846\r\n\/\/\r\n\/\/ uses an nRF24L01p connected to an Arduino\r\n\/\/ \r\n\/\/ Cables are:\r\n\/\/     SS       -&gt; 10   Chip Select Not - This is held to VCC to turn on chip, GPIO is not needed\r\n\/\/     MOSI     -&gt; 11   Master Out Slave In - Now called PICO\r\n\/\/     MISO     -&gt; 12   Master In Slave Out - Now called POCI\r\n\/\/     SCK      -&gt; 13   Clock Signal\r\n\/\/ \r\n\/\/     GND and VCC sould have a 10uf capacitor between them\r\n\/\/\r\n\/\/ and CE       -&gt;  9   Chip Enable - This is needed because SPI requires 2 way comms\r\n\/\/\r\n\/\/ created March 2011 by Rolf Henkel\r\n\/\/\r\n\r\n#define CE  9  \/\/ Chip Enable - This is needed because SPI requires 2 way comms\r\n\r\nunsigned long myTime;\r\n\r\n\/\/ Array to hold Channel data (32 = 1 sec, 64 = 2 sec, 128 = 4 sec, 256 = 8 sec, etc)\r\n#define CHANNELS 128\r\nint channel[CHANNELS];\r\n\r\n\/\/ greyscale mapping \r\nint  line;\r\nchar grey[] = \"0123456789\";\r\n\/\/ char grey[] = \" .:-=+*aRW\";\r\n\r\n\/\/ nRF24L01P registers we need\r\n#define _NRF24_CONFIG      0x00\r\n#define _NRF24_EN_AA       0x01\r\n#define _NRF24_RF_CH       0x05\r\n#define _NRF24_RF_SETUP    0x06\r\n#define _NRF24_RPD         0x09\r\n\r\n\/\/ get the value of a nRF24L01p register\r\nbyte getRegister(byte r)\r\n{\r\n  byte c;\r\n  \r\n  PORTB &amp;=~_BV(2);\r\n  c = SPI.transfer(r&amp;0x1F);\r\n  c = SPI.transfer(0);  \r\n  PORTB |= _BV(2);\r\n\r\n  return(c);\r\n}\r\n\r\n\/\/ set the value of a nRF24L01p register\r\nvoid setRegister(byte r, byte v)\r\n{\r\n  PORTB &amp;=~_BV(2);\r\n  SPI.transfer((r&amp;0x1F)|0x20);\r\n  SPI.transfer(v);\r\n  PORTB |= _BV(2);\r\n}\r\n  \r\n\/\/ power up the nRF24L01p chip\r\nvoid powerUp(void)\r\n{\r\n  setRegister(_NRF24_CONFIG,getRegister(_NRF24_CONFIG)|0x02);\r\n  delayMicroseconds(130);\r\n}\r\n\r\n\/\/ switch nRF24L01p off\r\nvoid powerDown(void)\r\n{\r\n  setRegister(_NRF24_CONFIG,getRegister(_NRF24_CONFIG)&amp;~0x02);\r\n}\r\n\r\n\/\/ enable RX \r\nvoid enable(void)\r\n{\r\n    PORTB |= _BV(1);\r\n}\r\n\r\n\/\/ disable RX\r\nvoid disable(void)\r\n{\r\n    PORTB &amp;=~_BV(1);\r\n}\r\n\r\n\/\/ setup RX-Mode of nRF24L01p\r\nvoid setRX(void)\r\n{\r\n  setRegister(_NRF24_CONFIG,getRegister(_NRF24_CONFIG)|0x01);\r\n  enable();\r\n  \/\/ this is slightly shorter than\r\n  \/\/ the recommended delay of 130 usec\r\n  \/\/ - but it works for me and speeds things up a little...\r\n  delayMicroseconds(100);\r\n}\r\n\r\n\/\/ scanning all channels in the 2.4GHz band\r\nvoid scanChannels(void)\r\n{\r\n  disable();\r\n  for( int j=0 ; j&lt;200  ; j++)\r\n  {\r\n    for( int i=0 ; i&lt;CHANNELS ; i++)\r\n    {\r\n      \/\/ select a new channel\r\n      setRegister(_NRF24_RF_CH,(128*i)\/CHANNELS);\r\n      \r\n      \/\/ switch on RX\r\n      setRX();\r\n      \r\n      \/\/ wait enough for RX-things to settle\r\n      delayMicroseconds(40);\r\n      \r\n      \/\/ this is actually the point where the RPD-flag\r\n      \/\/ is set, when CE goes low\r\n      disable();\r\n      \r\n      \/\/ read out RPD flag; set to 1 if \r\n      \/\/ received power &gt; -64dBm\r\n      if( getRegister(_NRF24_RPD)&gt;0 )   channel[i]++;\r\n    }\r\n  }\r\n}\r\n\r\n\/\/ outputs channel data as a simple grey map\r\nvoid outputChannels(void)\r\n{\r\n  int norm = 0;\r\n  \r\n  \/\/ find the maximal count in channel array\r\n  for( int i=0 ; i&lt;CHANNELS ; i++)\r\n    if( channel[i]&gt;norm ) norm = channel[i];\r\n    \r\n  \/\/ now output the data\r\n  for( int i=0 ; i&lt;CHANNELS ; i++)\r\n  {\r\n    int pos;\r\n    \r\n    \/\/ calculate grey value position\r\n    if( norm!=0 ) pos = (channel[i]*10)\/norm;\r\n    else          pos = 0;\r\n    \r\n    \/\/ boost low values\r\n    if( pos==0 &amp;&amp; channel[i]&gt;0 ) pos++;\r\n    \r\n    \/\/ clamp large values\r\n    if( pos&gt;9 ) pos = 9;\r\n   \r\n    \/\/ print it out\r\n    Serial.print(grey[pos]);\r\n    Serial.print(',');\r\n    channel[i] = 0;\r\n  }\r\n  \r\n  \/\/ indicate overall power\r\n  Serial.print(norm);\r\n  Serial.print(',');\r\n  myTime = millis();\r\n  Serial.print(myTime);\r\n  Serial.println(',');\r\n}\r\n\r\n\r\nvoid setup()\r\n{\r\n  Serial.begin(115200);\r\n  \r\n  \/\/ Setup SPI\r\n  SPI.begin();\r\n  \/\/ Clock Speed, Bit Order, and Data Mode\r\n  SPI.beginTransaction(SPISettings(20000000, MSBFIRST, SPI_MODE0));\r\n  \r\n  \/\/ Activate Chip Enable\r\n  pinMode(CE,OUTPUT);\r\n  disable();\r\n  \r\n  \/\/ now start receiver\r\n  powerUp();\r\n  \r\n  \/\/ switch off Shockburst\r\n  setRegister(_NRF24_EN_AA,0x0);\r\n  \r\n  \/\/ make sure RF-section is set properly \r\n  \/\/ - just write default value... \r\n  setRegister(_NRF24_RF_SETUP,0x0F); \r\n  \r\n  \/\/ reset line counter\r\n  line = 0;\r\n}\r\n\r\nvoid loop() \r\n{ \r\n  \/\/ do the scan\r\n  scanChannels();\r\n\r\n  Serial.print('#');\r\n  Serial.print(',');\r\n \r\n  \/\/ output the result\r\n  outputChannels();\r\n}<\/pre>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"c\">\/*********\r\n  Rui Santos\r\n  Complete project details at https:\/\/RandomNerdTutorials.com\/esp32-cam-take-photo-save-microsd-card\r\n  \r\n  IMPORTANT!!! \r\n   - Select Board \"AI Thinker ESP32-CAM\"\r\n   - GPIO 0 must be connected to GND to upload a sketch\r\n   - After connecting GPIO 0 to GND, press the ESP32-CAM on-board RESET button to put your board in flashing mode\r\n  \r\n  Permission is hereby granted, free of charge, to any person obtaining a copy\r\n  of this software and associated documentation files.\r\n  The above copyright notice and this permission notice shall be included in all\r\n  copies or substantial portions of the Software.\r\n\r\n*********\/\r\n\r\n#include \"WiFi.h\"\r\n#include \"esp_camera.h\"\r\n#include \"FS.h\"                \/\/ SD Card ESP32\r\n#include \"SD_MMC.h\"            \/\/ SD Card ESP32\r\nconst char* NRFReadings = \"\/NRF24L01_Readings.csv\";\r\nconst char* WifiScanReadings = \"\/Wifi_Readings.csv\";\r\nString MillisString;\r\n\r\n\r\n\/\/ Pin definition for CAMERA_MODEL_AI_THINKER\r\n#define PWDN_GPIO_NUM     32\r\n#define RESET_GPIO_NUM    -1\r\n#define XCLK_GPIO_NUM      0\r\n#define SIOD_GPIO_NUM     26\r\n#define SIOC_GPIO_NUM     27\r\n#define Y9_GPIO_NUM       35\r\n#define Y8_GPIO_NUM       34\r\n#define Y7_GPIO_NUM       39\r\n#define Y6_GPIO_NUM       36\r\n#define Y5_GPIO_NUM       21\r\n#define Y4_GPIO_NUM       19\r\n#define Y3_GPIO_NUM       18\r\n#define Y2_GPIO_NUM        5\r\n#define VSYNC_GPIO_NUM    25\r\n#define HREF_GPIO_NUM     23\r\n#define PCLK_GPIO_NUM     22\r\n\r\nunsigned long pictureNumber = 0;\r\nunsigned long photodelay = 0; \/\/ Time counter for timed photos\r\nunsigned long delaytime = 3000; \/\/ Time of delay\r\n\r\nunsigned long ExecuteInterval = 2000;\r\nunsigned long LastInterval = 0;\r\nunsigned long CurrentInterval = 0;\r\nint HoldInterval = 0;\r\n\r\nvoid HardwareSetup()\r\n{\r\n  \/\/ Note the format for setting a serial port is as follows: \r\n  \/\/ Serial.begin(baud-rate, protocol, RX pin, TX pin);\r\n  \/\/ initialize serial:\r\n  Serial.begin(115200);\r\n  delay(100);\r\n\r\n}\r\n\r\n\r\n\r\nvoid PrintHeader()\r\n{\r\n  \r\n   appendFile(SD_MMC, WifiScanReadings, \"ESP32-Cam-NRF24L01-Scan-Wifi-Scan-Take-Photo-Save-MicroSD_Ver4\");\r\n   appendFile(SD_MMC, WifiScanReadings, \"\\n\");\r\n   appendFile(SD_MMC, WifiScanReadings, \"Arduino Pro Mini Firmware - Arduino_ProMini_NRF24L01-128bit-Ver1.ino\");\r\n   appendFile(SD_MMC, WifiScanReadings, \"\\n\");\r\n   appendFile(SD_MMC, WifiScanReadings, \"Time in millis,Networks Found,SSID,dBm,AP Mac Address,Channel,Security\");\r\n   appendFile(SD_MMC, WifiScanReadings, \"\\n\");\r\n   appendFile(SD_MMC, WifiScanReadings, \"\\n\");\r\n\r\n  \r\n}\r\n\r\n\r\n\r\nvoid StartupCamera() {\r\n  \r\n  camera_config_t config;\r\n  config.ledc_channel = LEDC_CHANNEL_0;\r\n  config.ledc_timer = LEDC_TIMER_0;\r\n  config.pin_d0 = Y2_GPIO_NUM;\r\n  config.pin_d1 = Y3_GPIO_NUM;\r\n  config.pin_d2 = Y4_GPIO_NUM;\r\n  config.pin_d3 = Y5_GPIO_NUM;\r\n  config.pin_d4 = Y6_GPIO_NUM;\r\n  config.pin_d5 = Y7_GPIO_NUM;\r\n  config.pin_d6 = Y8_GPIO_NUM;\r\n  config.pin_d7 = Y9_GPIO_NUM;\r\n  config.pin_xclk = XCLK_GPIO_NUM;\r\n  config.pin_pclk = PCLK_GPIO_NUM;\r\n  config.pin_vsync = VSYNC_GPIO_NUM;\r\n  config.pin_href = HREF_GPIO_NUM;\r\n  config.pin_sscb_sda = SIOD_GPIO_NUM;\r\n  config.pin_sscb_scl = SIOC_GPIO_NUM;\r\n  config.pin_pwdn = PWDN_GPIO_NUM;\r\n  config.pin_reset = RESET_GPIO_NUM;\r\n  config.xclk_freq_hz = 20000000;\r\n  config.pixel_format = PIXFORMAT_JPEG;  \/\/YUV422,GRAYSCALE,RGB565,JPEG\r\n  config.frame_size = FRAMESIZE_XGA; \/\/ FRAMESIZE_ + QVGA (320 x 240) | CIF (352 x 288) |VGA (640 x 480) | SVGA (800 x 600) |XGA (1024 x 768) |SXGA (1280 x 1024) |UXGA (1600 x 1200)\r\n  config.jpeg_quality = 10; \/\/ 10-63 lower number means higher quality\r\n  config.fb_count = 1;\r\n  \r\n  \/\/ Init Camera\r\n  esp_err_t err = esp_camera_init(&amp;config);\r\n  if (err != ESP_OK) {\r\n    return;\r\n  }\r\n\r\n  sensor_t * s = esp_camera_sensor_get();\r\n  s-&gt;set_whitebal(s, 0);       \/\/ 0 = disable , 1 = enable\r\n  s-&gt;set_awb_gain(s, 0);       \/\/ 0 = disable , 1 = enable\r\n \r\n}\r\n\r\n\r\n\r\nvoid StartupMicroSD() {\r\n  \r\n  if(!SD_MMC.begin()){\r\n    return;\r\n  }\r\n  \r\n  uint8_t cardType = SD_MMC.cardType();\r\n  if(cardType == CARD_NONE){\r\n    return;\r\n  }\r\n  \r\n}\r\n\r\n\r\n\/\/Append to the end of file in SD card\r\nvoid appendFile(fs::FS &amp;fs, const char * path, const char * message){\r\n\r\n    File file = fs.open(path, FILE_APPEND);\r\n    if(!file){\r\n        return;\r\n    }\r\n    if(file.print(message)){\r\n    } else {\r\n    }\r\n}\r\n\r\n\r\n\r\n\r\n\/\/ https:\/\/mischianti.org\/esp32-practical-power-saving-manage-wifi-and-cpu-1\/\r\n\r\n\r\nvoid disableWiFi(){\r\n  \r\n  \/\/ Switch WiFi off\r\n  WiFi.mode(WIFI_OFF);    \/\/ Switch WiFi off\r\n  delay(100);\r\n}\r\n\r\n\r\nvoid enableWiFi(){\r\n\r\n  \/\/ Set WiFi to station mode and disconnect from an AP if it was previously connected.\r\n  WiFi.mode(WIFI_STA);\r\n  WiFi.disconnect();\r\n  delay(100);\r\n}\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\nvoid ScanWifiNetworks()\r\n{\r\n\r\n      \/\/ WiFi.scanNetworks will return the number of networks found.\r\n    int IntWifiNetworks = WiFi.scanNetworks(\/*async=*\/false, \/*hidden=*\/true);\r\n    \r\n\r\n      \r\n    if (IntWifiNetworks == 0) {\r\n    }\r\n    \r\n    else {\r\n      \r\n        for (int NetworkCount = 0; NetworkCount &lt; IntWifiNetworks; ++NetworkCount) \r\n           {\r\n      \r\n            MillisString = String(millis());         \/\/ read string until meet newline character  \r\n            appendFile(SD_MMC, WifiScanReadings, MillisString.c_str()); \r\n            appendFile(SD_MMC, WifiScanReadings, \",\");\r\n        \r\n            String StringWifiNetworks = String(IntWifiNetworks);         \/\/ read string until meet newline character  \r\n            appendFile(SD_MMC, WifiScanReadings, StringWifiNetworks.c_str()); \r\n            appendFile(SD_MMC, WifiScanReadings, \",\");\r\n            \r\n            String StringSSID = String(WiFi.SSID(NetworkCount));\r\n            if(StringSSID != NULL) {\r\n              appendFile(SD_MMC, WifiScanReadings, StringSSID.c_str()); \r\n              }\r\n            else {\r\n              appendFile(SD_MMC, WifiScanReadings, \"*.*Hidden*.*\");\r\n              }\r\n            appendFile(SD_MMC, WifiScanReadings, \",\");\r\n           \r\n            String StringRSSI = String(WiFi.RSSI(NetworkCount));\r\n            appendFile(SD_MMC, WifiScanReadings, StringRSSI.c_str()); \r\n            appendFile(SD_MMC, WifiScanReadings, \",\");\r\n\r\n            String StringBSSIDstr = String(WiFi.BSSIDstr(NetworkCount));\r\n            appendFile(SD_MMC, WifiScanReadings, StringBSSIDstr.c_str()); \r\n            appendFile(SD_MMC, WifiScanReadings, \",\");\r\n\r\n\r\n            String StringChannel = String(WiFi.channel(NetworkCount));\r\n            appendFile(SD_MMC, WifiScanReadings, StringChannel.c_str()); \r\n            appendFile(SD_MMC, WifiScanReadings, \",\");\r\n\r\n            printEncryptionType(WiFi.encryptionType(NetworkCount));\r\n            appendFile(SD_MMC, WifiScanReadings, \"\\n\");\r\n           }\r\n       }\r\n\r\n      \/\/ Delete the scan result to free memory for code below.\r\n      WiFi.scanDelete();\r\n    \r\n  \r\n}\r\n\r\n\r\nvoid printEncryptionType(int thisType) {\r\n\r\n  \/\/ read the encryption type and print out the name:\r\n  switch (thisType) {\r\n            case WIFI_AUTH_OPEN:\r\n                appendFile(SD_MMC, WifiScanReadings, \"open\");\r\n                break;\r\n            case WIFI_AUTH_WEP:\r\n                appendFile(SD_MMC, WifiScanReadings, \"WEP\");\r\n                break;\r\n            case WIFI_AUTH_WPA_PSK:\r\n                appendFile(SD_MMC, WifiScanReadings, \"WPA\");\r\n                break;\r\n            case WIFI_AUTH_WPA2_PSK:\r\n                appendFile(SD_MMC, WifiScanReadings, \"WPA2\");\r\n                break;\r\n            case WIFI_AUTH_WPA_WPA2_PSK:\r\n                appendFile(SD_MMC, WifiScanReadings, \"WPA+WPA2\");\r\n                break;\r\n            case WIFI_AUTH_WPA2_ENTERPRISE:\r\n                appendFile(SD_MMC, WifiScanReadings, \"WPA2-EAP\");\r\n                break;\r\n            case WIFI_AUTH_WPA3_PSK:\r\n                appendFile(SD_MMC, WifiScanReadings, \"WPA3\");\r\n                break;\r\n            case WIFI_AUTH_WPA2_WPA3_PSK:\r\n                appendFile(SD_MMC, WifiScanReadings, \"WPA2+WPA3\");\r\n                break;\r\n            case WIFI_AUTH_WAPI_PSK:\r\n                appendFile(SD_MMC, WifiScanReadings, \"WAPI\");\r\n                break;\r\n            default:\r\n                appendFile(SD_MMC, WifiScanReadings, \"unknown\");\r\n  }\r\n}\r\n\r\n\r\n\r\nvoid CaptureImage() {\r\n\r\n  camera_fb_t * fb = NULL;\r\n  \r\n  \/\/ Take Picture with Camera\r\n  fb = esp_camera_fb_get();  \r\n  if(!fb) {\r\n    return;\r\n  }\r\n  \r\n  \/\/ Construct a filename that looks like \"\/photo_0000000001.jpg\"\r\n  pictureNumber = millis(); \r\n  pictureNumber = pictureNumber \/ 1000; \r\n  String filename = \"\/photo_\";\r\n  if(pictureNumber &lt; 1000000000) filename += \"0\";\r\n  if(pictureNumber &lt; 100000000) filename += \"0\";\r\n  if(pictureNumber &lt; 10000000) filename += \"0\";\r\n  if(pictureNumber &lt; 1000000) filename += \"0\";\r\n  if(pictureNumber &lt; 100000) filename += \"0\";\r\n  if(pictureNumber &lt; 10000) filename += \"0\";\r\n  if(pictureNumber &lt; 1000) filename += \"0\";\r\n  if(pictureNumber &lt; 100) filename += \"0\";\r\n  if(pictureNumber &lt; 10) filename += \"0\";\r\n  filename += pictureNumber;\r\n  filename += \".jpg\";\r\n  \/\/ Path where new picture will be saved in SD Card\r\n  String imagefile = String(filename);\r\n\r\n  fs::FS &amp;fs = SD_MMC; \r\n  \r\n  File file = fs.open(imagefile.c_str(), FILE_WRITE);\r\n  file.write(fb-&gt;buf, fb-&gt;len); \/\/ payload (image), payload length\r\n  \/\/ file.close();\r\n  esp_camera_fb_return(fb);   \r\n}\r\n\r\nvoid setup() {\r\n\r\n  StartupCamera();\r\n  StartupMicroSD();\r\n  HardwareSetup();\r\n  PrintHeader();\r\n \r\n  \/\/ Let things settle before logging\r\n  delay(delaytime);\r\n  \r\n    appendFile(SD_MMC, NRFReadings, \"ESP32-Cam Firmware - ESP32-Cam-NRF24L01-Scan-Wifi-Scan-Take-Photo-Save-MicroSD_Ver5.ino\");\r\n    appendFile(SD_MMC, NRFReadings, \"\\n\");\r\n    appendFile(SD_MMC, NRFReadings, \"Arduino Pro Mini Firmware - Arduino_ProMini_NRF24L01-128bit-Ver1.ino\");\r\n    appendFile(SD_MMC, NRFReadings, \"\\n\");\r\n    \r\n}\r\n\r\nvoid loop() {\r\n\r\n  CurrentInterval = millis();\r\n  \r\n  if (HoldInterval == 0)\r\n    {\r\n    if (Serial.available())                                   \/\/ if there is data comming\r\n      {            \r\n      while (Serial.available() &gt; 0)\r\n        {\r\n            char incomingByte = Serial.read();\r\n            if (incomingByte == '#') {\r\n      \r\n              MillisString = String(millis());         \/\/ read string until meet newline character  \r\n              appendFile(SD_MMC, NRFReadings, MillisString.c_str()); \r\n              appendFile(SD_MMC, NRFReadings, \",\");\r\n    \r\n              String inputString = Serial.readStringUntil('\\n');         \/\/ read string until meet newline character  \r\n              appendFile(SD_MMC, NRFReadings, inputString.c_str()); \r\n              HoldInterval = 1;        \r\n          }\r\n        }\r\n      }\r\n    }\r\n\r\n if (CurrentInterval - LastInterval &gt;= ExecuteInterval) {\r\n  if (HoldInterval == 1){\r\n  \r\n      LastInterval = CurrentInterval; \r\n\r\n      enableWiFi();\r\n      ScanWifiNetworks();\r\n      disableWiFi();\r\n      appendFile(SD_MMC, WifiScanReadings, \"\\n\");\r\n    \r\n      CaptureImage();\r\n      HoldInterval = 0;\r\n    }  \r\n  }\r\n}\r\n\r\n<\/pre>\n<p>Plotting can take the mystery out of data and allow broader understanding.<\/p>\n<p><iframe loading=\"lazy\" title=\"&quot;This Far, And No Farther!&quot; | Columbo\" width=\"640\" height=\"360\" src=\"https:\/\/www.youtube.com\/embed\/2CRXCmpB4bM?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This post will expand on the topic of data plotting which was discussed in this post, https:\/\/www.cloudacm.com\/?p=4136. The concept is to give a quick at a glance view of information. Details can be lost in lengthy data sets formatted in tables or flat files, which are tedious to sift through.\u00a0 Data fatigue is problem for those not prepared to handle large amounts of information.\u00a0 The apathy it can foster can turn a resource into a liability.\u00a0 By using plots, it&#8217;s&#8230;<\/p>\n<p class=\"read-more\"><a class=\"btn btn-default\" href=\"https:\/\/www.cloudacm.com\/?p=4682\"> Read More<span class=\"screen-reader-text\">  Read More<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-4682","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts\/4682","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=4682"}],"version-history":[{"count":27,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts\/4682\/revisions"}],"predecessor-version":[{"id":4724,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts\/4682\/revisions\/4724"}],"wp:attachment":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=4682"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=4682"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=4682"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}