| 1 | /* |
| 2 | * Copyright (C) 2022 Brendan Doherty <2bndy5@gmail.com> |
| 3 | * |
| 4 | * This program is free software; you can redistribute it and/or |
| 5 | * modify it under the terms of the GNU General Public License |
| 6 | * version 2 as published by the Free Software Foundation. |
| 7 | */ |
| 8 | |
| 9 | /* |
| 10 | * This example uses 1 of 2 different popular displays. To control which display to use, |
| 11 | * comment/uncomment the lines below that define |
| 12 | * - `SPI_DISPLAY`: This requires the "Adafruit ST7735 and ST7789 Library" installed |
| 13 | * - `I2C_DISPLAY`: This requires the "Adafruit SSD1306" library installed |
| 14 | * |
| 15 | * Use the Arduino Library manager to ensure the required libraries are installed. |
| 16 | * By default, this sketch uses the SPI_DISPLAY (ST7789). Using both displays at the same |
| 17 | * time is not supported by this sketch. |
| 18 | * |
| 19 | * NOTES: |
| 20 | * The `SCREEN_HEIGHT` and `SCREEN_WIDTH` defines may need to be adjusted according |
| 21 | * to your display module's capability. This example expects the display to be at |
| 22 | * least 128 pixels wide. Otherwise, you would have to reduce the `numChannels` |
| 23 | * constant to fit within your display's width. |
| 24 | * |
| 25 | * The SPI_DISPLAY uses its own pins defined by `TFT_CS`, `TFT_DC`, and the |
| 26 | * optional `TFT_RST` (see below). The SPI bus is shared between radio and display, |
| 27 | * so the display's CS pin must be connected as specified by `TFT_CS`. |
| 28 | * If your ST7789 display does not have a CS pin, then further modification must |
| 29 | * be made so it does not use the same SPI bus that the radio uses. |
| 30 | * |
| 31 | * `DEBUGGING` can be enabled (uncommented) to show Serial output. This is just a |
| 32 | * convenience to set radio data rate or further development. See our other |
| 33 | * RF24/scanner example that only uses the Serial Monitor instead of a graphic |
| 34 | * display. |
| 35 | * |
| 36 | * See documentation at https://nRF24.github.io/RF24 |
| 37 | */ |
| 38 | #include <Adafruit_GFX.h> // dependency of Adafruit display libraries |
| 39 | #include "RF24.h" |
| 40 | |
| 41 | /******************************************************************** |
| 42 | * CHOOSE A DISPLAY INTERFACE |
| 43 | * uncomment/comment only 1 of the following to use the desired display |
| 44 | ********************************************************************/ |
| 45 | // #define I2C_DISPLAY // using the SSD1306 |
| 46 | #define SPI_DISPLAY // using ST7789 |
| 47 | |
| 48 | /******************************************************************** |
| 49 | * Choose a sketch feature |
| 50 | * uncomment any of the following to enable a special feature |
| 51 | ********************************************************************/ |
| 52 | // #define DEBUGGING // uncomment to enable Serial output (optional) |
| 53 | // #define HOLD_PEAKS // uncomment to disable decay of maxPeak pixels (useful for assessing total noise) |
| 54 | |
| 55 | /******************************************************************** |
| 56 | * Instantiate the radio and app-specific attributes |
| 57 | ********************************************************************/ |
| 58 | |
| 59 | #define CE_PIN 7 |
| 60 | #define CSN_PIN 8 |
| 61 | // instantiate an object for the nRF24L01 transceiver |
| 62 | RF24 radio(CE_PIN, CSN_PIN); |
| 63 | |
| 64 | // To detect noise, we'll use the worst addresses possible (a reverse engineering tactic). |
| 65 | // These addresses are designed to confuse the radio into thinking |
| 66 | // that the RF signal's preamble is part of the packet/payload. |
| 67 | const uint8_t noiseAddress[][2] = { { 0x55, 0x55 }, { 0xAA, 0xAA }, { 0xA0, 0xAA }, { 0xAB, 0xAA }, { 0xAC, 0xAA }, { 0xAD, 0xAA } }; |
| 68 | |
| 69 | const uint8_t numChannels = 126; // 0-125 are supported |
| 70 | |
| 71 | /*********************************************************************** |
| 72 | * Declare caching mechanism to track history of signals for peak decay |
| 73 | **********************************************************************/ |
| 74 | |
| 75 | const uint8_t cacheMax = 4; |
| 76 | |
| 77 | /// A data structure to organize the cache of signals for a certain channel. |
| 78 | struct ChannelHistory { |
| 79 | /// max peak value is (at most) 2 * CACHE_MAX to allow for half-step decays |
| 80 | uint8_t maxPeak = 0; |
| 81 | |
| 82 | /// Push a signal's value into cached history while popping |
| 83 | /// oldest cached value. This also sets the maxPeak value. |
| 84 | /// @returns The sum of signals found in the cached history |
| 85 | uint8_t push(bool value) { |
| 86 | uint8_t sum = value; |
| 87 | for (uint8_t i = 0; i < cacheMax - 1; ++i) { |
| 88 | history[i] = history[i + 1]; |
| 89 | sum += history[i]; |
| 90 | } |
| 91 | history[cacheMax - 1] = value; |
| 92 | maxPeak = max((uint8_t)(sum * 2), maxPeak); // sum * 2 to allow half-step decay |
| 93 | return sum; |
| 94 | } |
| 95 | |
| 96 | private: |
| 97 | bool history[cacheMax] = { 0 }; |
| 98 | }; |
| 99 | |
| 100 | /// An array of caches to use as channels' history |
| 101 | ChannelHistory stored[numChannels]; |
| 102 | |
| 103 | /******************************************************************** |
| 104 | * Instantiate the appropriate display objects according to the |
| 105 | * defines (above near top of file) |
| 106 | ********************************************************************/ |
| 107 | |
| 108 | #ifdef I2C_DISPLAY |
| 109 | |
| 110 | #include <Wire.h> |
| 111 | #include <Adafruit_SSD1306.h> |
| 112 | |
| 113 | #define SCREEN_WIDTH 128 // OLED display width, in pixels |
| 114 | #define SCREEN_HEIGHT 64 // OLED display height, in pixels |
| 115 | |
| 116 | // Declaration for an SSD1306 display connected to I2C (SDA, SCL pins) |
| 117 | // The pins for I2C are defined by the Wire-library. |
| 118 | // On an arduino UNO: A4(SDA), A5(SCL) |
| 119 | // On an arduino MEGA 2560: 20(SDA), 21(SCL) |
| 120 | // On an arduino LEONARDO: 2(SDA), 3(SCL), ... |
| 121 | #define OLED_RESET -1 // Or set to -1 and connect to Arduino RESET pin |
| 122 | #define SCREEN_ADDRESS 0x3D // See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32 |
| 123 | Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); |
| 124 | |
| 125 | #define BLACK SSD1306_BLACK |
| 126 | #define WHITE SSD1306_WHITE |
| 127 | #define REFRESH ({ display.display(); }) |
| 128 | #define CLEAR_DISPLAY ({ display.clearDisplay(); }) |
| 129 | |
| 130 | #elif defined(SPI_DISPLAY) |
| 131 | |
| 132 | #include <Adafruit_ST7789.h> // Hardware-specific library for ST7789 |
| 133 | |
| 134 | #define TFT_CS 9 |
| 135 | #define TFT_RST -1 // Or set to -1 and connect to Arduino RESET pin |
| 136 | #define TFT_DC 6 |
| 137 | |
| 138 | #define SCREEN_WIDTH 135 // TFT display width, in pixels |
| 139 | #define SCREEN_HEIGHT 240 // TFT display height, in pixels |
| 140 | |
| 141 | // For 1.14", 1.3", 1.54", 1.69", and 2.0" TFT with ST7789: |
| 142 | Adafruit_ST7789 display = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST); |
| 143 | |
| 144 | #define BLACK ST77XX_BLACK |
| 145 | #define WHITE ST77XX_WHITE |
| 146 | #define REFRESH |
| 147 | #define CLEAR_DISPLAY ({ display.fillScreen(BLACK); }) |
| 148 | |
| 149 | #endif // if defined(I2C_DISPLAY) || defined(SPI_DISPLAY) |
| 150 | |
| 151 | // constant chart size attributes |
| 152 | const uint16_t margin = 1; // use 1 pixel margin for markers on each side of chart |
| 153 | const uint16_t barWidth = (SCREEN_WIDTH - (margin * 2)) / numChannels; |
| 154 | const uint16_t chartHeight = SCREEN_HEIGHT - 10; |
| 155 | const uint16_t chartWidth = margin * 2 + (numChannels * barWidth); |
| 156 | |
| 157 | /******************************************************************** |
| 158 | * Configure debugging on Serial output |
| 159 | ********************************************************************/ |
| 160 | |
| 161 | #ifdef DEBUGGING |
| 162 | #include "printf.h" |
| 163 | #define SERIAL_DEBUG(x) ({ x; }) |
| 164 | #else |
| 165 | #define SERIAL_DEBUG(x) |
| 166 | #endif |
| 167 | |
| 168 | /******************************************************************** |
| 169 | * Setup the app |
| 170 | ********************************************************************/ |
| 171 | void setup(void) { |
| 172 | |
| 173 | #ifdef DEBUGGING |
| 174 | // Print preamble |
| 175 | Serial.begin(115200); |
| 176 | while (!Serial) { |
| 177 | // some boards need this to wait for Serial connection |
| 178 | } |
| 179 | Serial.println(F("RF24/examples/scannerGraphic")); |
| 180 | #endif |
| 181 | |
| 182 | #ifdef I2C_DISPLAY |
| 183 | // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally |
| 184 | if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { |
| 185 | SERIAL_DEBUG(Serial.println(F("SSD1306 allocation failed"));); |
| 186 | while (true) { |
| 187 | // Don't proceed, loop forever |
| 188 | } |
| 189 | } |
| 190 | #elif defined(SPI_DISPLAY) |
| 191 | // use this initializer for a 1.14" 240x135 TFT: |
| 192 | display.init(SCREEN_WIDTH, SCREEN_HEIGHT); // Init ST7789 240x135 |
| 193 | #endif |
| 194 | |
| 195 | // Clear the buffer |
| 196 | CLEAR_DISPLAY; |
| 197 | |
| 198 | // Setup and configure rf radio |
| 199 | if (!radio.begin()) { |
| 200 | SERIAL_DEBUG(Serial.println(F("radio hardware not responding!"));); |
| 201 | display.setCursor(1, 1); |
| 202 | display.setTextColor(WHITE); |
| 203 | display.print(F("radio hardware\nnot responding!")); |
| 204 | REFRESH; |
| 205 | while (true) { |
| 206 | // hold in an infinite loop |
| 207 | } |
| 208 | } |
| 209 | displayChartAxis(); |
| 210 | |
| 211 | radio.setAutoAck(false); // Don't acknowledge arbitrary signals |
| 212 | radio.disableCRC(); // accept any signal we find |
| 213 | radio.setAddressWidth(2); // a reverse engineering tactic (not typically recommended) |
| 214 | for (uint8_t i = 0; i < 6; ++i) { |
| 215 | radio.openReadingPipe(i, noiseAddress[i]); |
| 216 | } |
| 217 | |
| 218 | // set the data rate |
| 219 | #ifdef DEBUGGING |
| 220 | unsigned long inputTimeout = millis() + 7000; |
| 221 | Serial.print(F("Select your Data Rate. ")); |
| 222 | Serial.println(F("Enter '1' for 1Mbps, '2' for 2Mbps, '3' for 250kbps. Defaults to 1 Mbps.")); |
| 223 | while (!Serial.available() && millis() < inputTimeout) { |
| 224 | // Wait for user input. Timeout after 7 seconds. |
| 225 | } |
| 226 | char dataRate = !Serial.available() ? '1' : Serial.parseInt(); |
| 227 | #else |
| 228 | char dataRate = '1'; |
| 229 | #endif |
| 230 | if (dataRate == '2') { |
| 231 | SERIAL_DEBUG(Serial.println(F("Using 2 Mbps."));); |
| 232 | radio.setDataRate(RF24_2MBPS); |
| 233 | } else if (dataRate == '3') { |
| 234 | SERIAL_DEBUG(Serial.println(F("Using 250 kbps."));); |
| 235 | radio.setDataRate(RF24_250KBPS); |
| 236 | } else { // dataRate == '1' or invalid values |
| 237 | SERIAL_DEBUG(Serial.println(F("Using 1 Mbps."));); |
| 238 | radio.setDataRate(RF24_1MBPS); |
| 239 | } |
| 240 | |
| 241 | // Get into standby mode |
| 242 | radio.startListening(); |
| 243 | radio.stopListening(); |
| 244 | radio.flush_rx(); |
| 245 | } |
| 246 | |
| 247 | /******************************************************************** |
| 248 | * Make the app loop forever |
| 249 | ********************************************************************/ |
| 250 | void loop(void) { |
| 251 | // Print out channel measurements, clamped to a single hex digit |
| 252 | for (uint8_t channel = 0; channel < numChannels; ++channel) { |
| 253 | bool foundSignal = scanChannel(channel); |
| 254 | uint8_t cacheSum = stored[channel].push(foundSignal); |
| 255 | uint8_t x = (barWidth * channel) + 1 + margin - (barWidth * (bool)channel); |
| 256 | // reset bar for current channel to 0 |
| 257 | display.fillRect(x, 0, barWidth, chartHeight, BLACK); |
| 258 | if (stored[channel].maxPeak > cacheSum * 2) { |
| 259 | // draw a peak line only if it is greater than current sum of cached signal counts |
| 260 | uint16_t y = chartHeight - (chartHeight * stored[channel].maxPeak / (cacheMax * 2)); |
| 261 | display.drawLine(x, y, x + barWidth, y, WHITE); |
| 262 | #ifndef HOLD_PEAKS |
| 263 | stored[channel].maxPeak -= 1; // decrement max peak |
| 264 | #endif |
| 265 | } |
| 266 | if (cacheSum) { // draw the cached signal count |
| 267 | uint8_t barHeight = chartHeight * cacheSum / cacheMax; |
| 268 | display.fillRect(x, chartHeight - barHeight, barWidth, barHeight, WHITE); |
| 269 | } |
| 270 | } |
| 271 | REFRESH; |
| 272 | } // end loop() |
| 273 | |
| 274 | /// Scan a specified channel and return the resulting flag |
| 275 | bool scanChannel(uint8_t channel) { |
| 276 | radio.setChannel(channel); |
| 277 | |
| 278 | // Listen for a little |
| 279 | radio.startListening(); |
| 280 | delayMicroseconds(130); |
| 281 | bool foundSignal = radio.testRPD(); |
| 282 | radio.stopListening(); |
| 283 | |
| 284 | // Did we get a signal? |
| 285 | if (foundSignal || radio.testRPD() || radio.available()) { |
| 286 | radio.flush_rx(); // discard packets of noise |
| 287 | return true; |
| 288 | } |
| 289 | return false; |
| 290 | } |
| 291 | |
| 292 | /// Draw the chart axis and labels |
| 293 | void displayChartAxis() { |
| 294 | // draw base line |
| 295 | display.drawLine(0, chartHeight + 1, chartWidth - margin, chartHeight + 1, WHITE); |
| 296 | |
| 297 | // draw base line border |
| 298 | display.drawLine(margin, SCREEN_HEIGHT, margin, chartHeight - 2, WHITE); |
| 299 | display.drawLine(chartWidth - margin, SCREEN_HEIGHT, chartWidth - margin, chartHeight - 2, WHITE); |
| 300 | |
| 301 | // draw scalar marks |
| 302 | for (uint8_t i = 0; i < cacheMax; ++i) { |
| 303 | uint8_t scalarHeight = chartHeight * i / cacheMax; |
| 304 | display.drawLine(0, scalarHeight, chartWidth, scalarHeight, WHITE); |
| 305 | } |
| 306 | |
| 307 | // draw channel range labels |
| 308 | display.setTextSize(1); |
| 309 | display.setTextColor(WHITE); |
| 310 | uint8_t maxChannelDigits = 0; |
| 311 | uint8_t tmp = numChannels; |
| 312 | while (tmp) { |
| 313 | maxChannelDigits += 1; |
| 314 | tmp /= 10; |
| 315 | } |
| 316 | display.setCursor(chartWidth - (7 * maxChannelDigits), chartHeight + 3); |
| 317 | display.print(numChannels - 1); |
| 318 | display.setCursor(margin + 2, chartHeight + 3); |
| 319 | display.print(0); |
| 320 | |
| 321 | // refresh display |
| 322 | REFRESH; |
| 323 | } |