{"id":4521,"date":"2023-11-28T12:00:49","date_gmt":"2023-11-28T20:00:49","guid":{"rendered":"https:\/\/www.cloudacm.com\/?p=4521"},"modified":"2023-10-12T07:21:11","modified_gmt":"2023-10-12T14:21:11","slug":"esp32-cam-nrf24l01-radio-spectrum-data-logger","status":"publish","type":"post","link":"https:\/\/www.cloudacm.com\/?p=4521","title":{"rendered":"ESP32-Cam NRF24L01 Radio Spectrum Data Logger"},"content":{"rendered":"<p>This post will cover recent uses of the NRF24L01 with the EPS32-Cam module. An earlier example used an the NRF24L01 module attached to an Arduino Uno, a laptop, and a logged putty session, details can be found here, <a href=\"https:\/\/www.cloudacm.com\/?p=3836\">https:\/\/www.cloudacm.com\/?p=3836<\/a>. This configuration had limitations due to its size, the required pieces of equipment, and operating steps. The purpose of this scanner type is to survey 2.4Ghz radio transmissions. The NRF module can detect power levels across the band and store those values into its register. The band resolution can increased, but doing so will increase the amount of time needed for the module to complete a scan. A microcontroller is used to operate the NRF module and to read the values stored in the module&#8217;s register. These readings can then be stored, in this case to microSD storage in the form of a CSV file. Below is an example of a plot.<\/p>\n<p><iframe loading=\"lazy\" title=\"Persistent RF from Drive - NRF24L01 Sampling\" src=\"https:\/\/player.vimeo.com\/video\/854684378?badge=0&amp;autopause=0&amp;quality_selector=1&amp;progress_bar=1&amp;player_id=0&amp;app_id=58479\" width=\"640\" height=\"360\" frameborder=\"0\"><\/iframe><\/p>\n<p>The second generation used an Arduino pro mini and the ESP32-Cam module. This design was smaller and mobile. Data logging was automatic and continuous. Using this device revealed an unknown source of RF which was from the console of the vehicle, shown in the video above.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4109.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-4525\" 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>After further investigation the source of the RF was the vehicle attempting to bind with a paired Bluetooth device. After removing the pairing, the vehicle no longer broadcasted RF.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF_Readings.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-4527 size-medium\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF_Readings-137x300.png\" alt=\"\" width=\"137\" height=\"300\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF_Readings-137x300.png 137w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF_Readings-468x1024.png 468w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF_Readings-123x270.png 123w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF_Readings.png 640w\" sizes=\"auto, (max-width: 137px) 100vw, 137px\" \/><\/a><\/p>\n<p>Here is the python code used to plot the readings shown above.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\"># Python Code\r\nimport sys,os\r\nimport plotly.express as px\r\n\r\n\r\nfrom numpy import genfromtxt\r\nnumpy_array = genfromtxt('NRF_Readings.csv', delimiter=',')\r\n\r\nfig = px.imshow(numpy_array, color_continuous_scale='thermal')\r\n                 \r\nfig.update_layout(width=640, height=1400, margin=dict(l=0, r=0, b=0, t=0), coloraxis_showscale=False)\r\nfig.update_xaxes(showticklabels=False).update_yaxes(showticklabels=False)\r\n# fig.show()\r\n\r\nfig.write_image(\"NRF_Readings.png\") \r\n\r\n\r\n# Command - python3 NRF_Readings.py\r\n# Source - https:\/\/plotly.com\/python\/heatmaps\/\r\n\r\n# https:\/\/plotly.com\/python\/imshow\/\r\n# https:\/\/pypi.org\/project\/plotly-express\/\r\n# https:\/\/www.cloudacm.com\/?p=2674\r\n# https:\/\/plotly.com\/python\/plotly-express\/\r\n\r\n# https:\/\/plotly.com\/python\/builtin-colorscales\/\r\n\r\n# aggrnyl     agsunset    blackbody   bluered     blues       blugrn      bluyl       brwnyl\r\n# bugn        bupu        burg        burgyl      cividis     darkmint    electric    emrld\r\n# gnbu        greens      greys       hot         inferno     jet         magenta     magma\r\n# mint        orrd        oranges     oryel       peach       pinkyl      plasma      plotly3\r\n# pubu        pubugn      purd        purp        purples     purpor      rainbow     rdbu\r\n# rdpu        redor       reds        sunset      sunsetdark  teal        tealgrn     turbo\r\n# viridis     ylgn        ylgnbu      ylorbr      ylorrd      algae       amp         deep\r\n# dense       gray        haline      ice         matter      solar       speed       tempo\r\n# thermal     turbid      armyrose    brbg        earth       fall        geyser      prgn\r\n# piyg        picnic      portland    puor        rdgy        rdylbu      rdylgn      spectral\r\n# tealrose    temps       tropic      balance     curl        delta       oxy         edge\r\n# hsv         icefire     phase       twilight    mrybm       mygbm\r\n\r\n\r\n# https:\/\/video.stackexchange.com\/questions\/32419\/ffmpeg-vertical-pan-over-an-image-top-to-bottom-scrolling-effect\r\n\r\n# Then make a video the scrolls the image.\r\n\r\n# ffmpeg -f lavfi -i color=s=640x360 -i NRF24L01_Readings-128bit_MorningCommute_Aug14-0900_NoTimeStamp_NoTotalRF.png -filter_complex \"[0][1]overlay=x=0:y=H-0.5*n\" -vframes 6625 NRF24L01_Readings-128bit_MorningCommute_Aug14-0900_NoTimeStamp_NoTotalRF.mp4\r\n\r\n<\/pre>\n<p>Here is the build interconnects and wiring when using the device with an external battery, however the build above used the ESP32-Cam module USB board.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF24L01_ESP32-Cam_Arduino-ProMini_bb.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-4530\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF24L01_ESP32-Cam_Arduino-ProMini_bb-1024x775.png\" alt=\"\" width=\"640\" height=\"484\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF24L01_ESP32-Cam_Arduino-ProMini_bb-1024x775.png 1024w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF24L01_ESP32-Cam_Arduino-ProMini_bb-300x227.png 300w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF24L01_ESP32-Cam_Arduino-ProMini_bb-768x581.png 768w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF24L01_ESP32-Cam_Arduino-ProMini_bb-357x270.png 357w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF24L01_ESP32-Cam_Arduino-ProMini_bb.png 1527w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/><\/a><\/p>\n<p>Here is the microcontroller code used for the Arduino Pro Mini.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">#include &lt;SPI.h&gt;\r\n\r\n\/\/ Based off of this work, https:\/\/www.riyas.org\/2013\/12\/working-quick-start-guide-for-nrf24l01.html\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\/\/\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  64\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,(64*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(57600);\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  \/\/ output the result\r\n  outputChannels();\r\n}<\/pre>\n<p>Here is the microcontroller code used for the ESP32-Cam module.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">\/*********\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 \"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\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 between each photo\r\n\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(57600);\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\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  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\n}\r\n\r\nvoid setup() {\r\n\r\n  StartupCamera();\r\n  StartupMicroSD();\r\n  HardwareSetup();\r\n\r\n  \/\/ Let things settle before logging\r\n  delay(3000);\r\n    \r\n}\r\n\r\nvoid loop() {\r\n\r\n  if(Serial.available())                                   \/\/ if there is data comming\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    appendFile(SD_MMC, NRFReadings, \"\\n\");\r\n    CaptureImage();\r\n    \r\n  }\r\n\r\n\r\n  \r\n  \r\n}\r\n\r\n\/*\r\n  Some Notes\r\n\r\n\r\n  \r\n\r\n*\/<\/pre>\n<p>The third generation added a GPS module. Doing so allowed RF readings to be plotted in a geospatial domain. One of the biggest challenges in this design was the power distribution. Initially all power on the 3.3 V bus was provided by the ESP32-Cam module. That power configuration overloaded the 3.3 V regulator on the ESP32-Cam module, causing it to fail. The final power distribution provided 5 V separately to the ESP32-Cam module and the Arduino pro mini. Wi-Fi scanning was also introduced in this build.<\/p>\n<p>&nbsp;<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF24L01_GPS_ESP32-Cam_Arduino-ProMini_bb.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-4536\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF24L01_GPS_ESP32-Cam_Arduino-ProMini_bb-1024x467.png\" alt=\"\" width=\"640\" height=\"292\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF24L01_GPS_ESP32-Cam_Arduino-ProMini_bb-1024x467.png 1024w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF24L01_GPS_ESP32-Cam_Arduino-ProMini_bb-300x137.png 300w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF24L01_GPS_ESP32-Cam_Arduino-ProMini_bb-768x350.png 768w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF24L01_GPS_ESP32-Cam_Arduino-ProMini_bb-1536x700.png 1536w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF24L01_GPS_ESP32-Cam_Arduino-ProMini_bb-2048x933.png 2048w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/NRF24L01_GPS_ESP32-Cam_Arduino-ProMini_bb-592x270.png 592w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/><\/a><\/p>\n<p>Here is a photo of the build which uses the ESP32-Cam module USB board instead of regulated battery source.\u00a0 As seen on the photo, the device is only 34 grams.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4355.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-4540 size-full\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4355.png\" alt=\"\" width=\"934\" height=\"525\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4355.png 934w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4355-300x169.png 300w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4355-768x432.png 768w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4355-480x270.png 480w\" sizes=\"auto, (max-width: 934px) 100vw, 934px\" \/><\/a><\/p>\n<p>Here is the microcontroller code used on the Arduino Pro Mini for the GPS added configuration.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">\/** \r\n\r\nArduino_ProMini_NRF24L01-GPS-64bit-Ver1\r\nPro Mini Controller that monitors the following devices\r\nNRF24L01 for 2.4 Ghz spectrum readings\r\nGT-U7 GPS module vai software serial port\r\nAll readings are streamed out \r\n\r\nAug 27th, 2023 - 13:30\r\n\r\n**\/\r\n\r\n\r\n\/\/ Libraries\r\n#include &lt;TinyGPS++.h&gt;\r\n#include &lt;SoftwareSerial.h&gt;\r\nSoftwareSerial SecondSerial(8, 7); \/\/ RX, TX\r\n#include &lt;SPI.h&gt;\r\n\r\n\r\n\r\n\/\/ Variables and Constants\r\n\r\n\/\/ Set these based on your needs\r\n\/\/ GPS Home, https:\/\/www.google.com\/maps\r\nstatic const double SixtyAcres_LAT = 47.702875, SixtyAcres_LON = -122.137535;\r\n\r\n\r\nTinyGPSPlus gps;    \/\/ The TinyGPS++ object\r\nunsigned long last = 0UL;    \/\/ For stats that happen every 5 seconds\r\nint PadSeconds;\r\nint PadMinutes;\r\nint PadDays;\r\nint PadMonths;\r\nfloat OnboardSeconds;\r\n\r\nunsigned long serialdelay = 0; \/\/ Time counter for timed GPS checks\r\nunsigned long delaytime = 17000; \/\/ Time of delay to allow ESP32-Cam functions to run ( Wifi Scan 12 sec ) and ( Photo 5 sec )\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\/\/\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\/\/ https:\/\/forum.arduino.cc\/t\/poor-mans-2-4-ghz-scanner\/54846\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    \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,(64*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.print(',');\r\n\r\n\r\n\r\n  \r\n}\r\n\r\n\r\n\r\n\r\n\r\n\/\/ Printout Millis Timestamp\r\nvoid PrintOutMillis()\r\n{\r\n    \/\/ OnboardSeconds = millis();\r\n    \/\/ OnboardSeconds = OnboardSeconds\/1000;\r\n    \/\/ Serial.print(OnboardSeconds, 3);\r\n    myTime = millis();\r\n    Serial.print(myTime);\r\n    Serial.print(\",\");\r\n}\r\n\r\n\r\n\/\/ Printout Location Routine\r\nvoid PrintOutLocation()\r\n{\r\n    String StringLat = String(gps.location.lat(), 6);\r\n    String StringLong = String(gps.location.lng(), 6);\r\n    String Location = String(StringLat + \",\" + StringLong + \",\");\r\n    Serial.print(Location);\r\n}\r\n\r\n\r\n\/\/ Printout Date Routine\r\nvoid PrintOutDate()\r\n{\r\n    PadMonths = gps.date.month();\r\n      if (PadMonths &lt; 10)\r\n        {        \r\n         Serial.print(\"0\");\r\n        }        \r\n    String StringMonth = String(gps.date.month());\r\n    Serial.print(StringMonth);\r\n    Serial.print(\"\/\");\r\n    PadDays = gps.date.day();\r\n      if (PadDays &lt; 10)\r\n        {        \r\n         Serial.print(\"0\");\r\n        }        \r\n    String StringDay = String(gps.date.day());\r\n    Serial.print(StringDay);\r\n    Serial.print(\"\/\");\r\n    String StringYear = String(gps.date.year());\r\n    Serial.print(StringYear);\r\n    Serial.print(\",\");\r\n}\r\n\r\n\r\n\/\/ Printout Time Routine\r\nvoid PrintOutTime()\r\n{\r\n    String StringHour = String(gps.time.hour());\r\n    Serial.print(StringHour);\r\n    Serial.print(\":\");\r\n    PadMinutes = gps.time.minute();\r\n      if (PadMinutes &lt; 10)\r\n        {\r\n         Serial.print(\"0\");\r\n        }\r\n    String StringMinutes = String(gps.time.minute());\r\n    Serial.print(StringMinutes);\r\n    Serial.print(\":\");\r\n    PadSeconds = gps.time.second();\r\n      if (PadSeconds &lt; 10)\r\n        {\r\n         Serial.print(\"0\");\r\n        }\r\n    String StringSeconds = String(gps.time.second());\r\n    Serial.print(StringSeconds);\r\n    Serial.print(\",\");\r\n}\r\n\r\n\r\n\/\/ Printout Speed Routine\r\nvoid PrintOutSpeed()\r\n{\r\n    String StringMph = String(gps.speed.mph());\r\n    Serial.print(StringMph);\r\n    Serial.print(\",\");\r\n}\r\n\r\n\r\n\/\/ Printout Course Routine\r\nvoid PrintOutCourse()\r\n{\r\n    String StringCourse = String(gps.course.deg());\r\n    Serial.print(StringCourse);\r\n    Serial.print(\",\");\r\n}\r\n\r\n\r\n\/\/ Printout Altitude Routine\r\nvoid PrintOutAltitude()\r\n{\r\n    String StringAltitude = String(gps.altitude.feet());\r\n    Serial.print(StringAltitude);\r\n    Serial.print(\",\");\r\n}\r\n\r\n\r\n\/\/ Reference Bearing Routine\r\nvoid BearingReference()\r\n{\r\n      \r\n      double distanceToSixtyAcres =\r\n        TinyGPSPlus::distanceBetween(\r\n          gps.location.lat(),\r\n          gps.location.lng(),\r\n          SixtyAcres_LAT, \r\n          SixtyAcres_LON);\r\n      double courseToSixtyAcres =\r\n        TinyGPSPlus::courseTo(\r\n          gps.location.lat(),\r\n          gps.location.lng(),\r\n          SixtyAcres_LAT, \r\n          SixtyAcres_LON);\r\n          \r\n      String StringDistToHome = String(distanceToSixtyAcres\/1609, 6);\r\n      Serial.print(StringDistToHome);\r\n      Serial.print(\",\");\r\n      String StringCourseToHome = String(courseToSixtyAcres, 6);\r\n      Serial.print(StringCourseToHome);\r\n      Serial.print(\",\");\r\n      String StringCardinal = String(TinyGPSPlus::cardinal(courseToSixtyAcres));\r\n      Serial.print(StringCardinal);\r\n      Serial.print(',');\r\n}\r\n\r\n\r\n\r\n\r\n\/\/ End of serial message\r\nvoid SerialMessageEnd()\r\n{\r\n\r\n    Serial.print(\"^\");  \/\/ Break Character for end of serial data message\r\n    \r\n}\r\n\r\n\r\n\/\/ This custom version of delay() ensures that the gps object\r\n\/\/ is being \"fed\".\r\n\/\/ source https:\/\/forum.arduino.cc\/t\/tinygps-dont-run-the-sketch-until-gps-is-fixed\/1053105\r\nvoid gpsLock()\r\n{\r\n    \r\n  \/\/ Keep waiting until the fix is valid\r\n  while( !gps.location.isValid() )\r\n  {\r\n\r\n    unsigned long start = millis();\r\n    do \r\n      {\r\n        while (SecondSerial.available())\r\n        gps.encode(SecondSerial.read());\r\n      } \r\n    \r\n    while (millis() - start &lt; 1000);\r\n    \r\n    \/\/ if (millis() &gt; 5000 &amp;&amp; gps.charsProcessed() &lt; 10)\r\n    \/\/ Serial.println(F(\"No GPS data received: check wiring\"));\r\n \r\n    \r\n  }\r\n  \r\n}\r\n\r\n\r\n\r\n\r\n\/\/ give ESP-32 Cam module time to complete its functions before providing serial readings\r\nvoid ESP32CamWait()\r\n{\r\n\r\n  serialdelay = millis();\r\n  while (millis() - serialdelay &lt; delaytime) {\r\n    \/\/ I'm not waiting on a lady, I'm just waiting on a friend - https:\/\/www.youtube.com\/watch?v=MKLVmBOOqVU\r\n  }\r\n  \r\n}\r\n\r\n\r\n\r\n\r\n\r\n\r\n\/\/ Printout Global Results Routine\r\nvoid PrintOutGlobalReadings()\r\n{\r\n    PrintOutMillis ();\r\n    PrintOutLocation();\r\n    PrintOutDate();\r\n    PrintOutTime();\r\n    PrintOutSpeed();\r\n    PrintOutCourse();\r\n    PrintOutAltitude();\r\n    BearingReference();\r\n    SerialMessageEnd();\r\n}\r\n\r\n\r\n\r\nvoid setup()\r\n{\r\n  Serial.begin(57600);\r\n  \r\n  SecondSerial.begin(9600);\r\n  delay(1000);  \/\/ give GPS time to settle \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\r\n  \/\/ wait for gps to get a valid lock\r\n  gpsLock();\r\n  \r\n  \/\/ do the scan with nrf24l01\r\n  scanChannels();\r\n  \r\n  \/\/ output the results from nrf24l01\r\n  outputChannels();\r\n\r\n  \/\/ monitor for serial data from gps\r\n  \/\/ InboundSerialData();\r\n\r\n  \/\/ output the results from gps\r\n  PrintOutGlobalReadings();\r\n\r\n  \/\/ give ESP-32 Cam module time to complete its functions before providing serial readings\r\n  ESP32CamWait();\r\n  \r\n}<\/pre>\n<p>Here is the microcontroller code used on the ESP32-Cam module.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"cpp\">\/*********\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 = \"\/NRF-GPS-Wifi_Readings.csv\";\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 = 5000; \/\/ Time of delay between each photo\r\n\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(57600);\r\n  \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\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_UXGA; \/\/ 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\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  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\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();\r\n\r\n      \r\n    if (IntWifiNetworks == 0) {\r\n    }\r\n    \r\n    else {\r\n\r\n        String 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 StringWifiNetworks = String(IntWifiNetworks);         \/\/ read string until meet newline character  \r\n        appendFile(SD_MMC, NRFReadings, StringWifiNetworks.c_str()); \r\n        appendFile(SD_MMC, NRFReadings, \",\");\r\n        \r\n        for (int NetworkCount = 0; NetworkCount &lt; IntWifiNetworks; ++NetworkCount) \r\n           {\r\n           \r\n            String StringSSID = String(WiFi.SSID(NetworkCount));\r\n            appendFile(SD_MMC, NRFReadings, StringSSID.c_str()); \r\n            appendFile(SD_MMC, NRFReadings, \",\");\r\n           \r\n            String StringRSSI = String(WiFi.RSSI(NetworkCount));\r\n            appendFile(SD_MMC, NRFReadings, StringRSSI.c_str()); \r\n            appendFile(SD_MMC, NRFReadings, \",\");\r\n\r\n            String StringBSSIDstr = String(WiFi.BSSIDstr(NetworkCount));\r\n            appendFile(SD_MMC, NRFReadings, StringBSSIDstr.c_str()); \r\n            appendFile(SD_MMC, NRFReadings, \",\");\r\n\r\n\r\n            String StringChannel = String(WiFi.channel(NetworkCount));\r\n            appendFile(SD_MMC, NRFReadings, StringChannel.c_str()); \r\n            appendFile(SD_MMC, NRFReadings, \",\");\r\n\r\n            printEncryptionType(WiFi.encryptionType(NetworkCount));\r\n            appendFile(SD_MMC, NRFReadings, \",\");\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\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, NRFReadings, \"open\");\r\n                break;\r\n            case WIFI_AUTH_WEP:\r\n                appendFile(SD_MMC, NRFReadings, \"WEP\");\r\n                break;\r\n            case WIFI_AUTH_WPA_PSK:\r\n                appendFile(SD_MMC, NRFReadings, \"WPA\");\r\n                break;\r\n            case WIFI_AUTH_WPA2_PSK:\r\n                appendFile(SD_MMC, NRFReadings, \"WPA2\");\r\n                break;\r\n            case WIFI_AUTH_WPA_WPA2_PSK:\r\n                appendFile(SD_MMC, NRFReadings, \"WPA+WPA2\");\r\n                break;\r\n            case WIFI_AUTH_WPA2_ENTERPRISE:\r\n                appendFile(SD_MMC, NRFReadings, \"WPA2-EAP\");\r\n                break;\r\n            case WIFI_AUTH_WPA3_PSK:\r\n                appendFile(SD_MMC, NRFReadings, \"WPA3\");\r\n                break;\r\n            case WIFI_AUTH_WPA2_WPA3_PSK:\r\n                appendFile(SD_MMC, NRFReadings, \"WPA2+WPA3\");\r\n                break;\r\n            case WIFI_AUTH_WAPI_PSK:\r\n                appendFile(SD_MMC, NRFReadings, \"WAPI\");\r\n                break;\r\n            default:\r\n                appendFile(SD_MMC, NRFReadings, \"unknown\");\r\n  }\r\n}\r\n\r\n\r\nvoid setup() {\r\n\r\n  StartupCamera();\r\n  StartupMicroSD();\r\n  HardwareSetup();\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\n\r\nvoid loop() {\r\n\r\n  \/\/ Wait for serial data input, then perform internal functions\r\n  if(Serial.available())                                   \/\/ if there is data comming\r\n  {\r\n    String inputString = Serial.readStringUntil('^');         \/\/ read string until meet newline character  \r\n    appendFile(SD_MMC, NRFReadings, inputString.c_str()); \r\n    \r\n    \/\/ This can take 12 seconds to complete\r\n    ScanWifiNetworks();\r\n    appendFile(SD_MMC, NRFReadings, \"\\n\");\r\n\r\n    \/\/ This can take 5 seconds to complete\r\n    CaptureImage();\r\n    \r\n  }\r\n\r\n\r\n  \r\n  \r\n}\r\n\r\n\/*\r\n  Some Notes\r\n\r\n\r\n*\/<\/pre>\n<p>Field tests were inconclusive as to what RF was being detected by the NRF module in relation with the ESP32-Cam Wi-Fi scan. A fourth generation scanner was hastily made. The purpose of this scanner was to focus RF reception with a directional antenna. An arbitrary can was used and the NRF module was placed inside the can. This configuration did not include a GPS module, but did include a Compass sensor. in addition to the scanner modules, a ESP32-Cam module was used separately to photograph where the antenna was pointing with each reading. It was discovered later on that the Compass module had a fault, and the bearing would have to be provided by the photos. This configuration did not include Wi-Fi scanning.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4145.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-4546 size-full\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4145.png\" alt=\"\" width=\"807\" height=\"454\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4145.png 807w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4145-300x169.png 300w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4145-768x432.png 768w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2023\/11\/IMG_4145-480x270.png 480w\" sizes=\"auto, (max-width: 807px) 100vw, 807px\" \/><\/a><\/p>\n<p>Readings from the directional antenna were limited to RF in close proximity. This range limitation was noted by others, see <a href=\"https:\/\/hallard.me\/nrf24l01-real-life-range-test\/\">https:\/\/hallard.me\/nrf24l01-real-life-range-test\/<\/a>. The less likely cause of this is from the antenna dimensions not being at center frequency.\u00a0 Additional attempts were made to detect the radio frequency source however there were no bearing readings to cross reference.<\/p>\n<p>One possible design consideration would be a tuned antenna in a polar array. The polar array would consist of at least four quadrants as well as a device to provide bearing. The quadrants would spread out like petals from a flower. If an RF source was from a given direction, the pedals closest in that direction would indicate a higher reading. This device would operate best from a higher altitude with line of sight to the RF source. This presents a challenge because the aircraft would need to be equipped with a radio that operates outside of the frequency being surveyed.<\/p>\n<p>The effectiveness of RF scanning to locate a source is highly dependent on some known limitations. FCC regulations dictate how much power a standard Wi-Fi transmitter can operate at. The NRF module has a set sensitivity, which is documented in its data sheet. Power loss from a radio signal follows an inverse square constant. See this link for more information, <a href=\"https:\/\/helpfulcolin.com\/radio-wave-power-and-how-it-gets-reduced-by-distance\/\">https:\/\/helpfulcolin.com\/radio-wave-power-and-how-it-gets-reduced-by-distance\/<\/a>.\u00a0 Knowing the maximum transmit power and the sensitivity limit, the inverse power constant can be applied to determine how far the NRF will receive a signal.\u00a0 Do keep in mind that these are ideal conditions with outside factors not considered.<\/p>\n<p>The other challenge is building a wave guide that is small enough to fit on a model RC aircraft, but efficient enough to propagate the Wi-Fi frequency. Based on the wavelenght of 2.4Ghz RF (2.4 Ghz RF \u03bb = 12.4913524166667 cm), the parabola would be roughly 5 inches or 12.5 cm across. This means the array would likely be 1 foot across. Keeping the array thin enough so it reamains aerodynamic will result in a narrow horizontal beam. So this device has many hurdles to overcome in order to provide an accurate location.\u00a0 Using multiple devices to triagulate adds a whole level of complexity well outside to scope of this post.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This post will cover recent uses of the NRF24L01 with the EPS32-Cam module. An earlier example used an the NRF24L01 module attached to an Arduino Uno, a laptop, and a logged putty session, details can be found here, https:\/\/www.cloudacm.com\/?p=3836. This configuration had limitations due to its size, the required pieces of equipment, and operating steps. The purpose of this scanner type is to survey 2.4Ghz radio transmissions. The NRF module can detect power levels across the band and store those&#8230;<\/p>\n<p class=\"read-more\"><a class=\"btn btn-default\" href=\"https:\/\/www.cloudacm.com\/?p=4521\"> 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-4521","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts\/4521","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=4521"}],"version-history":[{"count":22,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts\/4521\/revisions"}],"predecessor-version":[{"id":4523,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts\/4521\/revisions\/4523"}],"wp:attachment":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=4521"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=4521"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=4521"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}