From ef08a4068e0616e0ab21da1b9db3c4b7d73b1258 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Sun, 10 Mar 2024 21:52:06 -0700 Subject: [PATCH] Promiscuous scanners (#955) * adjust scanner examples as discussed in #852 * add new curses python scanner * replace scanner.py w/ curses implementation * update pico scanner example * update arduino scanner * introducing an Arduino scannerGraphic example - supports 2 types of displays (& optional Serial debugginng) 1. SPI display (uses same bus as radio) is tested with an ST7789 (https://www.adafruit.com/product/4383) 2. I2C display is tested with SSd1306 (https://www.adafruit.com/product/326) Use defines to toggle debugging and EITHER (not both) of the displays. * [Arduino scanner] ensure radio is not stuck with CONT_WAVE flag asserted after MCU reset --------- Co-authored-by: TMRh20 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .github/workflows/build_linux.yml | 4 +- .gitignore | 3 +- examples/scanner/scanner.ino | 201 ++++++++---- examples/scannerGraphic/scannerGraphic.ino | 323 ++++++++++++++++++++ examples_linux/CMakeLists.txt | 51 +++- examples_linux/ncurses/CMakeLists.txt | 23 ++ examples_linux/ncurses/scanner_curses.cpp | 339 +++++++++++++++++++++ examples_linux/scanner.cpp | 168 ++++++---- examples_linux/scanner.py | 292 ++++++++++++------ examples_pico/scanner.cpp | 185 +++++++---- 10 files changed, 1325 insertions(+), 264 deletions(-) create mode 100644 examples/scannerGraphic/scannerGraphic.ino create mode 100644 examples_linux/ncurses/CMakeLists.txt create mode 100644 examples_linux/ncurses/scanner_curses.cpp diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml index fd1449642..038a476ac 100644 --- a/.github/workflows/build_linux.yml +++ b/.github/workflows/build_linux.yml @@ -17,7 +17,7 @@ on: - "utility/MRAAA/*" - "utility/SPIDEV/*" - "utility/pigpio" - - "examples_linux/*" + - "examples_linux/**" - "!examples_linux/*.py" - "!examples_linux/*.md" - "pyRF24/setup.py" @@ -39,7 +39,7 @@ on: - "utility/MRAAA/*" - "utility/SPIDEV/*" - "utility/pigpio" - - "examples_linux/*" + - "examples_linux/**" - "!examples_linux/*.py" - "!examples_linux/*.md" - "pyRF24/setup.py" diff --git a/.gitignore b/.gitignore index 783d309a4..11a99decf 100644 --- a/.gitignore +++ b/.gitignore @@ -18,8 +18,9 @@ Session.vim .DS_Store Makefile.inc utility/includes.h +!examples_linux/ examples_linux/* -examples_linux/**/* +!examples_linux/**/ !examples_linux/*.cpp !examples_linux/*.py !examples_linux/**/*.cpp diff --git a/examples/scanner/scanner.ino b/examples/scanner/scanner.ino index 119a81f65..c8b800931 100644 --- a/examples/scanner/scanner.ino +++ b/examples/scanner/scanner.ino @@ -14,12 +14,36 @@ * This is a good diagnostic tool to check whether you're picking a * good channel for your application. * - * Run this sketch on two devices. On one device, start CCW output by sending a 'g' - * character over Serial. The other device scanning should detect the output of the sending - * device on the given channel. Adjust channel and output power of CCW below. + * Run this sketch on two devices. On one device, start emitting a constant carrier wave + * by sending a channel number in the Serial Monitor. The other device scanning should + * detect the constant carrier wave from the sending device on the given channel. + * Send a negative number in the Serial Monitor to stop emitting a constant carrier wave + * and resume scanning. * * Inspired by cpixip. - * See http://arduino.cc/forum/index.php/topic,54795.0.html + * See https://forum.arduino.cc/t/poor-mans-2-4-ghz-scanner/54846 + * + * See documentation at https://nRF24.github.io/RF24 + */ + +/* + * How to read the output: + * - The header is a list of supported channels in decimal written vertically. + * - Each column corresponding to the vertical header is a hexadecimal count of + * detected signals (max is 15 or 'f'). + * + * The following example + * 000 + * 111 + * 789 + * ~~~ <- just a divider between the channel's vertical labels and signal counts + * 1-2 + * can be interpreted as + * - 1 signal detected on channel 17 + * - 0 signals (denoted as '-') detected on channel 18 + * - 2 signals detected on channel 19 + * + * Each line of signal counts represent 100 passes of the supported spectrum. */ #include "RF24.h" @@ -38,76 +62,112 @@ RF24 radio(CE_PIN, CSN_PIN); // Channel info // -const uint8_t num_channels = 126; -uint8_t values[num_channels]; +const uint8_t num_channels = 126; // 0-125 are supported +uint8_t values[num_channels]; // the array to store summary of signal counts per channel + +// To detect noise, we'll use the worst addresses possible (a reverse engineering tactic). +// These addresses are designed to confuse the radio into thinking +// that the RF signal's preamble is part of the packet/payload. +const uint8_t noiseAddress[][2] = { { 0x55, 0x55 }, { 0xAA, 0xAA }, { 0xA0, 0xAA }, { 0xAB, 0xAA }, { 0xAC, 0xAA }, { 0xAD, 0xAA } }; + +const int num_reps = 100; // number of passes for each scan of the entire spectrum +bool constCarrierMode = 0; // this flag controls example behavior (scan mode is default) + +void printHeader(); // prototype function for printing the channels' header -// -// Setup -// void setup(void) { - // - // Print preamble - // + // Print preamble Serial.begin(115200); - printf_begin(); - Serial.println(F("\n\rRF24/examples/scanner/")); + while (!Serial) { + // some boards need this to wait for Serial connection + } + Serial.println(F("RF24/examples/scanner/")); - // // Setup and configure rf radio - // + if (!radio.begin()) { + Serial.println(F("radio hardware not responding!")); + while (true) { + // hold in an infinite loop + } + } + radio.stopConstCarrier(); // in case MCU was reset while radio was emitting carrier wave + radio.setAutoAck(false); // Don't acknowledge arbitrary signals + radio.disableCRC(); // Accept any signal we find + radio.setAddressWidth(2); // A reverse engineering tactic (not typically recommended) + for (uint8_t i = 0; i < 6; ++i) { + radio.openReadingPipe(i, noiseAddress[i]); + } - radio.begin(); - radio.setAutoAck(false); + // set the data rate + Serial.print(F("Select your Data Rate. ")); + Serial.print(F("Enter '1' for 1 Mbps, '2' for 2 Mbps, '3' for 250 kbps. ")); + Serial.println(F("Defaults to 1Mbps.")); + while (!Serial.available()) { + // wait for user input + } + uint8_t dataRate = Serial.parseInt(); + if (dataRate == 50) { + Serial.println(F("Using 2 Mbps.")); + radio.setDataRate(RF24_2MBPS); + } else if (dataRate == 51) { + Serial.println(F("Using 250 kbps.")); + radio.setDataRate(RF24_250KBPS); + } else { + Serial.println(F("Using 1 Mbps.")); + radio.setDataRate(RF24_1MBPS); + } + Serial.println(F("***Enter a channel number to emit a constant carrier wave.")); + Serial.println(F("***Enter a negative number to switch back to scanner mode.")); // Get into standby mode radio.startListening(); radio.stopListening(); - radio.printDetails(); - - //delay(1000); - // Print out header, high then low digit - int i = 0; - while (i < num_channels) { - Serial.print(i >> 4, HEX); - ++i; - } - Serial.println(); - i = 0; - while (i < num_channels) { - Serial.print(i & 0xf, HEX); - ++i; - } - Serial.println(); - //delay(1000); -} + radio.flush_rx(); -// -// Loop -// + // printf_begin(); + // radio.printPrettyDetails(); + // delay(1000); -const int num_reps = 100; -bool constCarrierMode = 0; + // Print out vertical header + printHeader(); +} void loop(void) { /****************************************/ - // Send g over Serial to begin CCW output - // Configure the channel and power level below + // Send a number over Serial to begin Constant Carrier Wave output + // Configure the power amplitude level below if (Serial.available()) { - char c = Serial.read(); - if (c == 'g') { + int8_t c = Serial.parseInt(); + if (c >= 0) { + c = min(125, max(0, c)); // clamp channel to supported range constCarrierMode = 1; radio.stopListening(); delay(2); - Serial.println("Starting Carrier Out"); - radio.startConstCarrier(RF24_PA_LOW, 40); - } else if (c == 'e') { + Serial.print("\nStarting Carrier Wave Output on channel "); + Serial.println(c); + // for non-plus models, startConstCarrier() changes address on pipe 0 and sets address width to 5 + radio.startConstCarrier(RF24_PA_LOW, c); + } else { constCarrierMode = 0; radio.stopConstCarrier(); - Serial.println("Stopping Carrier Out"); + radio.setAddressWidth(2); // reset address width + radio.openReadingPipe(0, noiseAddress[0]); // ensure address is looking for noise + Serial.println("\nStopping Carrier Wave Output"); + printHeader(); + } + + // discard any CR and LF sent + while (Serial.peek() != -1) { + if (Serial.peek() == '\r' || Serial.peek() == '\n') { + Serial.read(); + } else { // got a charater that isn't a line feed + break; // handle it on next loop() iteration + } } } + /****************************************/ if (constCarrierMode == 0) { @@ -125,27 +185,52 @@ void loop(void) { // Listen for a little radio.startListening(); delayMicroseconds(128); + bool foundSignal = radio.testRPD(); radio.stopListening(); - // Did we get a carrier? - if (radio.testCarrier()) { + // Did we get a signal? + if (foundSignal || radio.testRPD() || radio.available()) { ++values[i]; + radio.flush_rx(); // discard packets of noise } } } - // Print out channel measurements, clamped to a single hex digit - int i = 0; - while (i < num_channels) { + for (int i = 0; i < num_channels; ++i) { if (values[i]) Serial.print(min(0xf, values[i]), HEX); else Serial.print(F("-")); - - ++i; } Serial.println(); - } //If constCarrierMode == 0 -} + } // if constCarrierMode == 0 + else { + // show some output to prove that the program isn't bricked + Serial.print(F(".")); + delay(1000); // delay a second to keep output readable + } +} // end loop() + +void printHeader() { + // Print the hundreds digits + for (uint8_t i = 0; i < num_channels; ++i) + Serial.print(i / 100); + Serial.println(); + + // Print the tens digits + for (uint8_t i = 0; i < num_channels; ++i) + Serial.print((i % 100) / 10); + Serial.println(); + + // Print the singles digits + for (uint8_t i = 0; i < num_channels; ++i) + Serial.print(i % 10); + Serial.println(); + + // Print the header's divider + for (uint8_t i = 0; i < num_channels; ++i) + Serial.print(F("~")); + Serial.println(); +} \ No newline at end of file diff --git a/examples/scannerGraphic/scannerGraphic.ino b/examples/scannerGraphic/scannerGraphic.ino new file mode 100644 index 000000000..e11ef6ea3 --- /dev/null +++ b/examples/scannerGraphic/scannerGraphic.ino @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2022 Brendan Doherty <2bndy5@gmail.com> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * version 2 as published by the Free Software Foundation. + */ + +/* + * This example uses 1 of 2 different popular displays. To control which display to use, + * comment/uncomment the lines below that define + * - `SPI_DISPLAY`: This requires the "Adafruit ST7735 and ST7789 Library" installed + * - `I2C_DISPLAY`: This requires the "Adafruit SSD1306" library installed + * + * Use the Arduino Library manager to ensure the required libraries are installed. + * By default, this sketch uses the SPI_DISPLAY (ST7789). Using both displays at the same + * time is not supported by this sketch. + * + * NOTES: + * The `SCREEN_HEIGHT` and `SCREEN_WIDTH` defines may need to be adjusted according + * to your display module's capability. This example expects the display to be at + * least 128 pixels wide. Otherwise, you would have to reduce the `numChannels` + * constant to fit within your display's width. + * + * The SPI_DISPLAY uses its own pins defined by `TFT_CS`, `TFT_DC`, and the + * optional `TFT_RST` (see below). The SPI bus is shared between radio and display, + * so the display's CS pin must be connected as specified by `TFT_CS`. + * If your ST7789 display does not have a CS pin, then further modification must + * be made so it does not use the same SPI bus that the radio uses. + * + * `DEBUGGING` can be enabled (uncommented) to show Serial output. This is just a + * convenience to set radio data rate or further development. See our other + * RF24/scanner example that only uses the Serial Monitor instead of a graphic + * display. + * + * See documentation at https://nRF24.github.io/RF24 + */ +#include // dependency of Adafruit display libraries +#include "RF24.h" + +/******************************************************************** + * CHOOSE A DISPLAY INTERFACE + * uncomment/comment only 1 of the following to use the desired display + ********************************************************************/ +// #define I2C_DISPLAY // using the SSD1306 +#define SPI_DISPLAY // using ST7789 + +/******************************************************************** + * Choose a sketch feature + * uncomment any of the following to enable a special feature + ********************************************************************/ +// #define DEBUGGING // uncomment to enable Serial output (optional) +// #define HOLD_PEAKS // uncomment to disable decay of maxPeak pixels (useful for assessing total noise) + +/******************************************************************** + * Instantiate the radio and app-specific attributes + ********************************************************************/ + +#define CE_PIN 7 +#define CSN_PIN 8 +// instantiate an object for the nRF24L01 transceiver +RF24 radio(CE_PIN, CSN_PIN); + +// To detect noise, we'll use the worst addresses possible (a reverse engineering tactic). +// These addresses are designed to confuse the radio into thinking +// that the RF signal's preamble is part of the packet/payload. +const uint8_t noiseAddress[][2] = { { 0x55, 0x55 }, { 0xAA, 0xAA }, { 0xA0, 0xAA }, { 0xAB, 0xAA }, { 0xAC, 0xAA }, { 0xAD, 0xAA } }; + +const uint8_t numChannels = 126; // 0-125 are supported + +/*********************************************************************** + * Declare caching mechanism to track history of signals for peak decay + **********************************************************************/ + +const uint8_t cacheMax = 4; + +/// A data structure to organize the cache of signals for a certain channel. +struct ChannelHistory { + /// max peak value is (at most) 2 * CACHE_MAX to allow for half-step decays + uint8_t maxPeak = 0; + + /// Push a signal's value into cached history while popping + /// oldest cached value. This also sets the maxPeak value. + /// @returns The sum of signals found in the cached history + uint8_t push(bool value) { + uint8_t sum = value; + for (uint8_t i = 0; i < cacheMax - 1; ++i) { + history[i] = history[i + 1]; + sum += history[i]; + } + history[cacheMax - 1] = value; + maxPeak = max(sum * 2, maxPeak); // sum * 2 to allow half-step decay + return sum; + } + +private: + bool history[cacheMax] = { 0 }; +}; + +/// An array of caches to use as channels' history +ChannelHistory stored[numChannels]; + +/******************************************************************** + * Instantiate the appropriate display objects according to the + * defines (above near top of file) + ********************************************************************/ + +#ifdef I2C_DISPLAY + +#include +#include + +#define SCREEN_WIDTH 128 // OLED display width, in pixels +#define SCREEN_HEIGHT 64 // OLED display height, in pixels + +// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins) +// The pins for I2C are defined by the Wire-library. +// On an arduino UNO: A4(SDA), A5(SCL) +// On an arduino MEGA 2560: 20(SDA), 21(SCL) +// On an arduino LEONARDO: 2(SDA), 3(SCL), ... +#define OLED_RESET -1 // Or set to -1 and connect to Arduino RESET pin +#define SCREEN_ADDRESS 0x3D // See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32 +Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); + +#define BLACK SSD1306_BLACK +#define WHITE SSD1306_WHITE +#define REFRESH ({ display.display(); }) +#define CLEAR_DISPLAY ({ display.clearDisplay(); }) + +#elif defined(SPI_DISPLAY) + +#include // Hardware-specific library for ST7789 + +#define TFT_CS 9 +#define TFT_RST -1 // Or set to -1 and connect to Arduino RESET pin +#define TFT_DC 6 + +#define SCREEN_WIDTH 135 // TFT display width, in pixels +#define SCREEN_HEIGHT 240 // TFT display height, in pixels + +// For 1.14", 1.3", 1.54", 1.69", and 2.0" TFT with ST7789: +Adafruit_ST7789 display = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST); + +#define BLACK ST77XX_BLACK +#define WHITE ST77XX_WHITE +#define REFRESH +#define CLEAR_DISPLAY ({ display.fillScreen(BLACK); }) + +#endif // if defined(I2C_DISPLAY) || defined(SPI_DISPLAY) + +// constant chart size attributes +const uint16_t margin = 1; // use 1 pixel margin for markers on each side of chart +const uint16_t barWidth = (SCREEN_WIDTH - (margin * 2)) / numChannels; +const uint16_t chartHeight = SCREEN_HEIGHT - 10; +const uint16_t chartWidth = margin * 2 + (numChannels * barWidth); + +/******************************************************************** + * Configure debugging on Serial output + ********************************************************************/ + +#ifdef DEBUGGING +#include "printf.h" +#define SERIAL_DEBUG(x) ({ x; }) +#else +#define SERIAL_DEBUG(x) +#endif + +/******************************************************************** + * Setup the app + ********************************************************************/ +void setup(void) { + +#ifdef DEBUGGING + // Print preamble + Serial.begin(115200); + while (!Serial) { + // some boards need this to wait for Serial connection + } + Serial.println(F("RF24/examples/scannerGraphic")); +#endif + +#ifdef I2C_DISPLAY + // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally + if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { + SERIAL_DEBUG(Serial.println(F("SSD1306 allocation failed"));); + while (true) { + // Don't proceed, loop forever + } + } +#elif defined(SPI_DISPLAY) + // use this initializer for a 1.14" 240x135 TFT: + display.init(SCREEN_WIDTH, SCREEN_HEIGHT); // Init ST7789 240x135 +#endif + + // Clear the buffer + CLEAR_DISPLAY; + + // Setup and configure rf radio + if (!radio.begin()) { + SERIAL_DEBUG(Serial.println(F("radio hardware not responding!"));); + display.setCursor(1, 1); + display.setTextColor(WHITE); + display.print(F("radio hardware\nnot responding!")); + REFRESH; + while (true) { + // hold in an infinite loop + } + } + displayChartAxis(); + + radio.setAutoAck(false); // Don't acknowledge arbitrary signals + radio.disableCRC(); // accept any signal we find + radio.setAddressWidth(2); // a reverse engineering tactic (not typically recommended) + for (uint8_t i = 0; i < 6; ++i) { + radio.openReadingPipe(i, noiseAddress[i]); + } + + // set the data rate +#ifdef DEBUGGING + unsigned long inputTimeout = millis() + 7000; + Serial.print(F("Select your Data Rate. ")); + Serial.println(F("Enter '1' for 1Mbps, '2' for 2Mbps, '3' for 250kbps. Defaults to 1 Mbps.")); + while (!Serial.available() && millis() < inputTimeout) { + // Wait for user input. Timeout after 7 seconds. + } + char dataRate = !Serial.available() ? '1' : Serial.parseInt(); +#else + char dataRate = '1'; +#endif + if (dataRate == '2') { + SERIAL_DEBUG(Serial.println(F("Using 2 Mbps."));); + radio.setDataRate(RF24_2MBPS); + } else if (dataRate == '3') { + SERIAL_DEBUG(Serial.println(F("Using 250 kbps."));); + radio.setDataRate(RF24_250KBPS); + } else { // dataRate == '1' or invalid values + SERIAL_DEBUG(Serial.println(F("Using 1 Mbps."));); + radio.setDataRate(RF24_1MBPS); + } + + // Get into standby mode + radio.startListening(); + radio.stopListening(); + radio.flush_rx(); +} + +/******************************************************************** + * Make the app loop forever + ********************************************************************/ +void loop(void) { + // Print out channel measurements, clamped to a single hex digit + for (uint8_t channel = 0; channel < numChannels; ++channel) { + bool foundSignal = scanChannel(channel); + uint8_t cacheSum = stored[channel].push(foundSignal); + uint8_t x = (barWidth * channel) + 1 + margin - (barWidth * (bool)channel); + // reset bar for current channel to 0 + display.fillRect(x, 0, barWidth, chartHeight, BLACK); + if (stored[channel].maxPeak > cacheSum * 2) { + // draw a peak line only if it is greater than current sum of cached signal counts + uint16_t y = chartHeight - (chartHeight * stored[channel].maxPeak / (cacheMax * 2)); + display.drawLine(x, y, x + barWidth, y, WHITE); +#ifndef HOLD_PEAKS + stored[channel].maxPeak -= 1; // decrement max peak +#endif + } + if (cacheSum) { // draw the cached signal count + uint8_t barHeight = chartHeight * cacheSum / cacheMax; + display.fillRect(x, chartHeight - barHeight, barWidth, barHeight, WHITE); + } + } + REFRESH; +} // end loop() + +/// Scan a specified channel and return the resulting flag +bool scanChannel(uint8_t channel) { + radio.setChannel(channel); + + // Listen for a little + radio.startListening(); + delayMicroseconds(130); + bool foundSignal = radio.testRPD(); + radio.stopListening(); + + // Did we get a signal? + if (foundSignal || radio.testRPD() || radio.available()) { + radio.flush_rx(); // discard packets of noise + return true; + } + return false; +} + +/// Draw the chart axis and labels +void displayChartAxis() { + // draw base line + display.drawLine(0, chartHeight + 1, chartWidth - margin, chartHeight + 1, WHITE); + + // draw base line border + display.drawLine(margin, SCREEN_HEIGHT, margin, chartHeight - 2, WHITE); + display.drawLine(chartWidth - margin, SCREEN_HEIGHT, chartWidth - margin, chartHeight - 2, WHITE); + + // draw scalar marks + for (uint8_t i = 0; i < cacheMax; ++i) { + uint8_t scalarHeight = chartHeight * i / cacheMax; + display.drawLine(0, scalarHeight, chartWidth, scalarHeight, WHITE); + } + + // draw channel range labels + display.setTextSize(1); + display.setTextColor(WHITE); + uint8_t maxChannelDigits = 0; + uint8_t tmp = numChannels; + while (tmp) { + maxChannelDigits += 1; + tmp /= 10; + } + display.setCursor(chartWidth - (7 * maxChannelDigits), chartHeight + 3); + display.print(numChannels - 1); + display.setCursor(margin + 2, chartHeight + 3); + display.print(0); + + // refresh display + REFRESH; +} diff --git a/examples_linux/CMakeLists.txt b/examples_linux/CMakeLists.txt index 2a46b61f1..eb8421042 100644 --- a/examples_linux/CMakeLists.txt +++ b/examples_linux/CMakeLists.txt @@ -30,25 +30,50 @@ else() # not using MRAA or wiringPi drivers (or pigpio lib was found) list(APPEND EXAMPLES_LIST interruptConfigure) endif() + +set(linked_libs + ${RF24} + pthread # Notice we specify pthread as a linked lib here +) + +# append additional libs for linking to the executable +if("${RF24_DRIVER}" STREQUAL "MRAA") + if(NOT "${LibMRAA}" STREQUAL "LibMRAA-NOTFOUND") + message(STATUS "linking to ${LibMRAA}") + list(APPEND linked_libs ${LibMRAA}) + else() + message(FATAL "Lib ${RF24_DRIVER} not found.") + endif() +elseif("${RF24_DRIVER}" STREQUAL "wiringPi") + if(NOT "${LibWiringPi}" STREQUAL "LibWiringPi-NOTFOUND") + message(STATUS "linking to ${LibWiringPi}") + # wiringPi additionally needs to link to crypt and shm_open libraries + list(APPEND linked_libs ${LibWiringPi} crypt rt) + else() + message(FATAL "Lib ${RF24_DRIVER} not found.") + endif() +elseif(NOT "${LibPIGPIO}" STREQUAL "LibPIGPIO-NOTFOUND" AND NOT DEFINED RF24_NO_INTERUPT) + if(NOT "${RF24_DRIVER}" STREQUAL "pigpio") + message(STATUS "linking to ${LibPIGPIO} for interrupt support") + else() + message(STATUS "linking to ${LibPIGPIO}") + endif() + # linking to pigpio requires pthread to be listed as last linked lib + list(APPEND linked_libs ${LibPIGPIO} pthread) +else() + message(STATUS "Disabling IRQ pin support") +endif() + foreach(example ${EXAMPLES_LIST}) #make a target add_executable(${example} ${example}.cpp) # avoid including interrupt.h when pigpio is not available - if("${LibPIGPIO}" STREQUAL "LibPIGPIO-NOTFOUND") + if("${LibPIGPIO}" STREQUAL "LibPIGPIO-NOTFOUND" OR DEFINED RF24_NO_INTERRUPT) target_compile_definitions(${example} PUBLIC RF24_NO_INTERRUPT) endif() - # link the RF24 lib to the target. Notice we specify pthread as a linked lib here - if("${RF24_DRIVER}" STREQUAL "MRAA") - target_link_libraries(${example} PUBLIC ${RF24} pthread ${LibMRAA}) - elseif("${RF24_DRIVER}" STREQUAL "wiringPi") - # wiringPi additionally needs to link to crypt and shm_open libraries - target_link_libraries(${example} PUBLIC ${RF24} pthread ${LibWiringPi} crypt rt) - elseif("${RF24_DRIVER}" STREQUAL "pigpio" OR NOT "${LibPIGPIO}" STREQUAL "LibPIGPIO-NOTFOUND") - # linking to pigpio requires pthread to be listed as last linked lib - target_link_libraries(${example} PUBLIC ${RF24} ${LibPIGPIO} pthread) - else() # not using MRAA or wiringPi drivers - target_link_libraries(${example} PUBLIC ${RF24} pthread) - endif() + target_link_libraries(${example} PUBLIC ${linked_libs}) endforeach() + +add_subdirectory(ncurses) diff --git a/examples_linux/ncurses/CMakeLists.txt b/examples_linux/ncurses/CMakeLists.txt new file mode 100644 index 000000000..d7718e1b4 --- /dev/null +++ b/examples_linux/ncurses/CMakeLists.txt @@ -0,0 +1,23 @@ +# this example needs the ncurses package installed +find_package(Curses) +if(Curses_FOUND) + message(STATUS "Including ncurses example") + include_directories(${CURSES_INCLUDE_DIR}) +else() + message(STATUS "libncurses5-dev not found. Skipping ncurses example") + return() +endif() + +set(example scanner_curses) + +# make a target +add_executable(${example} ${example}.cpp) + +# link the RF24 lib to the target. Notice we specify pthread as a linked lib here +target_link_libraries(${example} PUBLIC + ${linked_libs} + ${CURSES_LIBRARIES} +) +if("${LibPIGPIO}" STREQUAL "LibPIGPIO-NOTFOUND" OR DEFINED RF24_NO_INTERRUPT) + target_compile_definitions(${example} PUBLIC RF24_NO_INTERRUPT) +endif() diff --git a/examples_linux/ncurses/scanner_curses.cpp b/examples_linux/ncurses/scanner_curses.cpp new file mode 100644 index 000000000..1c45bf3db --- /dev/null +++ b/examples_linux/ncurses/scanner_curses.cpp @@ -0,0 +1,339 @@ +/* + Copyright (C) 2022 Brendan Doherty <2bndy5@gmail.com> + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + version 2 as published by the Free Software Foundation. + */ + +/** + * Channel scanner + * + * Example to detect interference on the various channels available. + * This is a good diagnostic tool to check whether you're picking a + * good channel for your application. + * + * See documentation at https://nRF24.github.io/RF24 + */ +#include // sprintf() +#include // string, getline() +#include // time_t, time(), difftime() +#include // cout, endl, flush, cin +#include // setprecision() +#include +#include + +using namespace std; + +/****************** Linux ***********************/ +// Radio CE Pin, CSN Pin, SPI Speed +// CE Pin uses GPIO number with BCM and SPIDEV drivers, other platforms use their own pin numbering +// CS Pin addresses the SPI bus number at /dev/spidev. +// ie: RF24 radio(, *10+); spidev1.0 is 10, spidev1.1 is 11 etc.. +#define CSN_PIN 0 +#ifdef MRAA + #define CE_PIN 15 // GPIO22 +#else + #define CE_PIN 22 +#endif +// Generic: +RF24 radio(CE_PIN, CSN_PIN); +/****************** Linux (BBB,x86,etc) ***********************/ +// See http://nRF24.github.io/RF24/pages.html for more information on usage +// See http://iotdk.intel.com/docs/master/mraa/ for more information on MRAA +// See https://www.kernel.org/doc/Documentation/spi/spidev for more information on SPIDEV + +// Channel info +const uint8_t MAX_CHANNELS = 126; // 0-125 are supported +const uint8_t CACHE_MAX = 5; // maximum depth of history for calculating peaks per channel + +struct ChannelHistory +{ + unsigned int total = 0; // the summary of signal counts for the channel + + /** + * Push new scan result for a channel into the history. + * This function also increments the total signal count accordingly. + * @returns The count of cached signals found (including pushed result) + */ + uint8_t push(bool value) + { + uint8_t sum = value; + total += value; + for (uint8_t i = 0; i < CACHE_MAX - 1; ++i) { + history[i] = history[i + 1]; + sum += history[i]; + } + history[CACHE_MAX - 1] = value; + return sum; + } + +private: + bool history[CACHE_MAX]; // a cache of history for the channel +}; +ChannelHistory stored[MAX_CHANNELS]; + +// To detect noise, we'll use the worst addresses possible (a reverse engineering tactic). +// These addresses are designed to confuse the radio into thinking +// that the RF signal's preamble is part of the packet/payload. +const uint8_t noiseAddress[][6] = {{0x55, 0x55}, {0xAA, 0xAA}, {0x0A, 0xAA}, {0xA0, 0xAA}, {0x00, 0xAA}, {0xAB, 0xAA}}; + +unsigned int passesCount = 0; // count of passes for each scan of the entire spectrum + +WINDOW* win; // curses base window object + +// function prototypes + +uint8_t initRadio(); +void initCurses(); +void deinitCurses(); +void initBars(); +bool scanChannel(uint8_t); +uint8_t historyPush(uint8_t index, bool value); + +class ProgressBar +{ + +private: + const int x, y, w, color; + +public: + ProgressBar(int cols, int rows, int width, string label, int color_index) + : x(cols), y(rows), w(width), color(color_index) + { + attron(COLOR_PAIR(color)); + mvaddstr(y, x, label.c_str()); + addch(' '); + for (uint8_t i = 0; i < w - 8; ++i) + addch(ACS_HLINE); + addstr(" - "); + attroff(COLOR_PAIR(color)); + }; + + void update(int completed, int sig_count) + { + int filled = (w - 8) * completed / CACHE_MAX; + int offset_x = 5; + move(y, x + offset_x); + for (int i = offset_x; i < w - 3; ++i) { + bool bar_filled = i < (filled + offset_x); + int bar_color = bar_filled ? 5 : color; + attron(COLOR_PAIR(bar_color)); + addch(bar_filled ? '=' : ACS_HLINE); + attroff(COLOR_PAIR(bar_color)); + } + attron(COLOR_PAIR(color)); + printw(" %x ", sig_count); + attroff(COLOR_PAIR(color)); + }; +}; + +// our table of progress bars used to represent channels in the curses window +ProgressBar* table[MAX_CHANNELS]; + +int main(int argc, char** argv) +{ + // print example's name + cout << argv[0] << endl; + + // Setup the radio + if (!radio.begin()) { + cout << "Radio hardware not responding!" << endl; + return 1; + } + uint8_t d_rate = initRadio(); + char bpsUnit = d_rate > 2 ? 'k' : 'M'; + + string input = ""; + int duration = 0; + while (!input.length()) { + cout << "Enter the duration (in seconds) of the scan: "; + getline(cin, input); + if (input.length()) { + duration = stoi(input); + if (!duration) + input.clear(); + } + } + + // create out interface + initCurses(); + initBars(); + mvaddstr(0, 0, "Channels are labeled in MHz."); + mvaddstr(1, 0, "Signal counts are clamped to a single hexadecimal digit."); + + uint8_t channel = 0; + time_t start = time(nullptr); + while (static_cast(difftime(time(nullptr), start)) < duration) { + mvprintw(2, + 0, + "Scanning for %3d seconds at %d %cbps", + static_cast(difftime(start + duration, time(nullptr))), + static_cast(d_rate), + bpsUnit); + + bool foundSignal = scanChannel(channel); + uint8_t cachedCount = stored[channel].push(foundSignal); + + // output the summary/snapshot for this channel + if (stored[channel].total) { + // make changes to the screen + table[channel]->update(static_cast(cachedCount), rf24_min(stored[channel].total, 0xF)); + } + + refresh(); + if (channel + 1 == MAX_CHANNELS) { + channel = 0; + ++passesCount; + } + else { + ++channel; + } + } + + deinitCurses(); + return 0; +} + +/** init radio according to user-specified data rate */ +uint8_t initRadio() +{ + uint8_t returnVal = 0; + // set the data rate + cout << "Select your Data Rate. " + << "Enter '1' for 1 Mbps, '2' for 2 Mbps, '3' for 250 kbps. " + << "Defaults to 1Mbps: "; + string dataRate = ""; + getline(cin, dataRate); + if (dataRate.length() >= 1 && static_cast(dataRate[0]) == '2') { + cout << "Using 2 Mbps." << endl; + radio.setDataRate(RF24_2MBPS); + returnVal = 2; + } + else if (dataRate.length() >= 1 && static_cast(dataRate[0]) == '3') { + cout << "Using 250 kbps." << endl; + radio.setDataRate(RF24_250KBPS); + returnVal = 250; + } + else { + cout << "Using 1 Mbps." << endl; + radio.setDataRate(RF24_1MBPS); + returnVal = 1; + } + + // configure the radio + radio.setAutoAck(false); // Don't acknowledge arbitrary signals + radio.disableCRC(); // Accept any signal we find + radio.setAddressWidth(2); // A reverse engineering tactic (not typically recommended) + for (uint8_t i = 0; i < 6; ++i) { + radio.openReadingPipe(i, noiseAddress[i]); + } + + // Get into standby mode + radio.startListening(); + radio.stopListening(); + radio.flush_rx(); + // radio.printPrettyDetails(); + + return returnVal; +} + +/** scan the specified channel and increment signal count accordingly */ +bool scanChannel(uint8_t channel) +{ + // Select this channel + radio.setChannel(channel); + + // Listen for a little + radio.startListening(); + delayMicroseconds(130); + // for some reason, this flag is more accurate on Linux when still in RX mode. + bool foundSignal = radio.testRPD(); + radio.stopListening(); + + // Did we get a signal? + if (foundSignal || radio.testRPD() || radio.available()) { + radio.flush_rx(); // discard packets of noise + return true; + } + return false; +} + +/** init the curses interface */ +void initCurses() +{ + win = initscr(); // Start curses mode + noecho(); + cbreak(); + start_color(); + use_default_colors(); + init_pair(3, COLOR_YELLOW, -1); + init_pair(5, COLOR_MAGENTA, -1); + init_pair(7, COLOR_WHITE, -1); +} + +/** de-init the curses interface & show total signal counts */ +void deinitCurses() +{ + nocbreak(); + echo(); + endwin(); + + // print out the total signal counts (if any) + uint8_t active_channels = 0; // the sum of channels with detected noise + uint8_t digitW = 0; + unsigned int tmp = passesCount; + while (tmp) { + digitW += 1; + tmp /= 10; + } + + for (uint8_t channel = 0; channel < MAX_CHANNELS; ++channel) { + if (stored[channel].total) { + active_channels++; + float noiseRatio = static_cast(stored[channel].total) / passesCount * 100; + cout << " " + << setfill(' ') + << setw(3) + << static_cast(channel) + << ": " + << setw(digitW) + << static_cast(stored[channel].total) + << " / " + << passesCount + << " (" + << setprecision(3) + << noiseRatio + << " %)" + << endl; + } + } + cout << static_cast(active_channels) + << " channels detected signals after " + << passesCount + << " passes." + << endl; +} + +/** init arrays for the history and progress bars */ +void initBars() +{ + // init our progress bars + int bar_w = COLS / 6; // total progress bar width (including all contents) + + for (uint8_t i = 0; i < 21; ++i) { // 21 rows + for (uint8_t j = 0; j < 6; ++j) { // 6 columns + + uint8_t channel = j * 21 + i; + table[channel] = new ProgressBar( + bar_w * j, // x + i + 3, // y + bar_w, // width + to_string(2400 + channel), // label + j % 2 ? 7 : 3 // 3 is yellow, 7 is white + ); + } + } +} + +// vim:ai:cin:sts=2 sw=2 ft=cpp diff --git a/examples_linux/scanner.cpp b/examples_linux/scanner.cpp index b02b323ff..e50becc50 100644 --- a/examples_linux/scanner.cpp +++ b/examples_linux/scanner.cpp @@ -9,7 +9,7 @@ 03/17/2013 : Charles-Henri Hallard (http://hallard.me) Modified to use with Arduipi board http://hallard.me/arduipi Changed to use modified bcm2835 and RF24 library - + 07/12/2022: Modified to be more promiscuous using reverse engineering tactics. */ /** @@ -20,11 +20,32 @@ * good channel for your application. * * Inspired by cpixip. - * See http://arduino.cc/forum/index.php/topic,54795.0.html + * See https://forum.arduino.cc/t/poor-mans-2-4-ghz-scanner/54846 + * + * See documentation at https://nRF24.github.io/RF24 */ -#include -#include +/* + * How to read the output: + * - The header is a list of supported channels in decimal written vertically. + * - Each column corresponding to the vertical header is a hexadecimal count of + * detected signals (max is 15 or 'f'). + * + * The following example + * 000 + * 111 + * 789 + * ~~~ <- just a divider between the channel's vertical labels and signal counts + * 1-2 + * can be interpreted as + * - 1 signal detected on channel 17 + * - 0 signals (denoted as '-') detected on channel 18 + * - 2 signals detected on channel 19 + * + * Each line of signal counts represent 100 passes of the supported spectrum. + */ +#include // string, getline() +#include // cout, endl, flush, cin #include using namespace std; @@ -48,47 +69,69 @@ RF24 radio(CE_PIN, CSN_PIN); // See https://www.kernel.org/doc/Documentation/spi/spidev for more information on SPIDEV // Channel info -const uint8_t num_channels = 126; -uint8_t values[num_channels]; +const uint8_t num_channels = 126; // 0-125 are supported +uint8_t values[num_channels]; // the array to store summary of signal counts per channel + +// To detect noise, we'll use the worst addresses possible (a reverse engineering tactic). +// These addresses are designed to confuse the radio into thinking +// that the RF signal's preamble is part of the packet/payload. +const uint8_t noiseAddress[][6] = {{0x55, 0x55}, {0xAA, 0xAA}, {0x0A, 0xAA}, {0xA0, 0xAA}, {0x00, 0xAA}, {0xAB, 0xAA}}; + +const int num_reps = 100; // number of passes for each scan of the entire spectrum -const int num_reps = 100; -int reset_array = 0; +void printHeader(); // prototype function for printing the channels' header int main(int argc, char** argv) { - // Print preamble - // print example's name - printf("%s", argv[0]); + cout << argv[0] << endl; - // - // Setup and configure rf radio - // - radio.begin(); + // Setup the radio + if (!radio.begin()) { + cout << "Radio hardware not responding!" << endl; + return 1; + } - radio.setAutoAck(false); + // print a line that should not be wrapped + cout << "\n!!! This example requires a width of at least 126 characters. " + << "If this text uses multiple lines, then the output will look bad." + << endl; + + // set the data rate + cout << "Select your Data Rate. " + << "Enter '1' for 1 Mbps, '2' for 2 Mbps, '3' for 250 kbps. " + << "Defaults to 1Mbps." + << endl; + string dataRate = ""; + getline(cin, dataRate); + if (dataRate.length() >= 1 && static_cast(dataRate[0]) == '2') { + cout << "Using 2 Mbps." << endl; + radio.setDataRate(RF24_2MBPS); + } + else if (dataRate.length() >= 1 && static_cast(dataRate[0]) == '3') { + cout << "Using 250 kbps." << endl; + radio.setDataRate(RF24_250KBPS); + } + else { + cout << "Using 1 Mbps." << endl; + radio.setDataRate(RF24_1MBPS); + } + + // configure the radio + radio.setAutoAck(false); // Don't acknowledge arbitrary signals + radio.disableCRC(); // Accept any signal we find + radio.setAddressWidth(2); // A reverse engineering tactic (not typically recommended) + for (uint8_t i = 0; i < 6; ++i) { + radio.openReadingPipe(i, noiseAddress[i]); + } // Get into standby mode radio.startListening(); radio.stopListening(); - - radio.printDetails(); - - // Print out header, high then low digit - int i = 0; - - while (i < num_channels) { - printf("%x", i >> 4); - ++i; - } - printf("\n"); - - i = 0; - while (i < num_channels) { - printf("%x", i & 0xf); - ++i; - } - printf("\n"); + radio.flush_rx(); + // radio.printPrettyDetails(); + // print the vertical header + printHeader(); // forever loop while (1) { @@ -99,38 +142,61 @@ int main(int argc, char** argv) int rep_counter = num_reps; while (rep_counter--) { - int i = num_channels; - while (i--) { + for (int i = 0; i < num_channels; ++i) { // Select this channel radio.setChannel(i); // Listen for a little radio.startListening(); - delayMicroseconds(128); + delayMicroseconds(130); + bool foundSignal = radio.testRPD(); radio.stopListening(); - // Did we get a carrier? - if (radio.testCarrier()) { + // Did we get a signal? + if (foundSignal || radio.testRPD() || radio.available()) { ++values[i]; + radio.flush_rx(); // discard packets of noise } - } - } - // Print out channel measurements, clamped to a single hex digit - i = 0; - while (i < num_channels) { - if (values[i]) - printf("%x", min(0xf, (values[i] & 0xf))); - else - printf("-"); - - ++i; + // output the summary/snapshot for this channel + if (values[i]) { + // Print out channel measurements, clamped to a single hex digit + cout << hex << min(0xF, static_cast(values[i])) << flush; + } + else { + cout << '-' << flush; + } + } + cout << '\r' << flush; } - printf("\n"); + cout << endl; } return 0; } +void printHeader() +{ + // print the hundreds digits + for (uint8_t i = 0; i < num_channels; ++i) + cout << static_cast(i / 100); + cout << endl; + + // print the tens digits + for (uint8_t i = 0; i < num_channels; ++i) + cout << static_cast((i % 100) / 10); + cout << endl; + + // print the singles digits + for (uint8_t i = 0; i < num_channels; ++i) + cout << static_cast(i % 10); + cout << endl; + + // print the header's divider + for (uint8_t i = 0; i < num_channels; ++i) + cout << '~'; + cout << endl; +} + // vim:ai:cin:sts=2 sw=2 ft=cpp diff --git a/examples_linux/scanner.py b/examples_linux/scanner.py index 014bca636..1d01cd7bc 100644 --- a/examples_linux/scanner.py +++ b/examples_linux/scanner.py @@ -1,64 +1,180 @@ -"""A scanner example that uses the python `rich` module to provide -a user-friendly output.""" +"""A scanner example written in python using the std lib's ncurses wrapper. + +This is a good diagnostic tool to check whether you're picking a +good channel for your application. + +See documentation at https://nRF24.github.io/RF24 +""" + +# pylint: disable=no-member +import curses import time -from typing import List - -try: - from rich.table import Table - from rich.console import Console - from rich.progress import BarColumn, Progress, TextColumn - from rich.live import Live - from rich.prompt import Prompt, IntPrompt - from rich.style import Style -except ImportError as exc: - raise ImportError( - "This example requires the python `rich` module installed." - "\nInstall it using 'python3 -m pip install rich'" - ) from exc +from typing import List, Tuple, Any + from RF24 import RF24, RF24_1MBPS, RF24_2MBPS, RF24_250KBPS CSN_PIN = 0 # connected to GPIO8 CE_PIN = 22 # connected to GPIO22 radio = RF24(CE_PIN, CSN_PIN) -if not radio.begin(): - raise RuntimeError("Radio hardware not responding!") -radio.setAutoAck(False) -offered_rates = ["1 Mbps", "2 Mbps", "250 kbps"] -available_rates = [RF24_1MBPS, RF24_2MBPS, RF24_250KBPS] -console = Console() -for i, rate in enumerate(offered_rates): - console.print(f"{i + 1}. {rate}") -DATA_RATE = ( - int(Prompt.ask("Choose the data rate", choices=["1", "2", "3"], default="1")) - 1 -) -radio.setDataRate(available_rates[DATA_RATE]) +OFFERED_DATA_RATES = ["1 Mbps", "2 Mbps", "250 kbps"] +AVAILABLE_RATES = [RF24_1MBPS, RF24_2MBPS, RF24_250KBPS] +TOTAL_CHANNELS = 126 +CACHE_MAX = 5 # the depth of history to calculate peaks -DURATION = IntPrompt.ask("Enter the scan duration (in whole seconds)") -SELECTED_RATE = offered_rates[DATA_RATE] +# To detect noise, we'll use the worst addresses possible (a reverse engineering +# tactic). These addresses are designed to confuse the radio into thinking that the +# RF signal's preamble is part of the packet/payload. +noise_address = [ + b"\x55\x55", + b"\xaa\xaa", + b"\x0a\xaa", + b"\xa0\xaa", + b"\x00\xaa", + b"\xab\xaa", +] + + +class ChannelHistory: + def __init__(self) -> None: + #: FIFO for tracking peak decays + self._history: List[bool] = [False] * CACHE_MAX + #: for the total signal counts + self.total: int = 0 + + def push(self, value: bool) -> int: + """Push a scan result's value into history while returning the sum of cached + signals found. This function also increments the total signal count accordingly. + """ + self._history = self._history[1:] + [value] + self.total += value + return self._history.count(True) + + +#: An array of histories for each channel +stored = [ChannelHistory() for _ in range(TOTAL_CHANNELS)] + + +class ProgressBar: # pylint: disable=too-few-public-methods + """This represents a progress bar using a curses window object.""" + + def __init__( # pylint: disable=too-many-arguments,invalid-name + self, + x: int, + y: int, + cols: int, + std_scr: Any, # type: curses.window, + label: str, + color: int, + ): + self.x, self.y, self.width, self.win, self.color = (x, y, cols, std_scr, color) + self.win.move(self.y, self.x) + self.win.attron(curses.color_pair(self.color)) + self.win.addstr(label + " ") # always labeled in MHz (4 digits) + for _ in range(self.width - 8): # draw the empty bar + self.win.addch(curses.ACS_HLINE) + self.win.addstr(" - ") # draw the initial signal count + self.win.attroff(curses.color_pair(self.color)) + + def update(self, completed: int, signal_count: int): + """Update the progress bar.""" + count = " - " + if signal_count: + count = " %X " % min(0xF, signal_count) + filled = (self.width - 8) * completed / CACHE_MAX + offset_x = 5 + self.win.move(self.y, self.x + offset_x) + for i in range(offset_x, self.width - 3): + bar_filled = i < (filled + offset_x) + bar_color = 5 if bar_filled else self.color + self.win.attron(curses.color_pair(bar_color)) + self.win.addch("=" if bar_filled else curses.ACS_HLINE) + self.win.attroff(curses.color_pair(bar_color)) + self.win.attron(curses.color_pair(self.color)) + self.win.addstr(count) + self.win.attroff(curses.color_pair(self.color)) + + +def init_display(window) -> List[ProgressBar]: + """Creates a table of progress bars (1 for each channel).""" + progress_bars: List[ProgressBar] = [None] * TOTAL_CHANNELS + bar_w = int(curses.COLS / 6) + for i in range(21): # 21 rows + for j in range(i, i + (21 * 6), 21): # 6 columns + color = 7 if int(j / 21) % 2 else 3 + progress_bars[j] = ProgressBar( + x=bar_w * int(j / 21), + y=i + 3, + cols=bar_w, + std_scr=window, + label=f"{2400 + (j)} ", + color=color, + ) + return progress_bars + + +def init_radio(): + """init the radio""" + if not radio.begin(): + raise RuntimeError("Radio hardware not responding!") + radio.setAutoAck(False) + radio.disableCRC() + radio.setAddressWidth(2) + for pipe, address in enumerate(noise_address): + radio.openReadingPipe(pipe, address) + radio.startListening() + radio.stopListening() + radio.flush_rx() + + +def init_curses(): + """init the curses interface""" + std_scr = curses.initscr() + curses.noecho() + curses.cbreak() + curses.start_color() + curses.use_default_colors() + curses.init_pair(3, curses.COLOR_YELLOW, -1) + curses.init_pair(5, curses.COLOR_MAGENTA, -1) + curses.init_pair(7, curses.COLOR_WHITE, -1) + return std_scr + + +def deinit_curses(spectrum_passes: int): + """de-init the curses interface""" + curses.nocbreak() + curses.echo() + curses.endwin() + noisy_channels: int = 0 + digit_w = len(str(spectrum_passes)) + for channel, data in enumerate(stored): + if data.total: + count_padding = " " * (digit_w - len(str(data.total))) + percentage = round(data.total / spectrum_passes * 100, 3) + print( + f" {channel:>3}: {count_padding}{data.total} / {spectrum_passes} ({percentage} %)" + ) + noisy_channels += 1 + print( + f"{noisy_channels} channels detected signals out of {spectrum_passes}", + "passes on the entire spectrum.", + ) -CACHE_MAX = 5 # the depth of history to calculate peaks -history = [[False] * CACHE_MAX] * 126 # for tracking peak decay on each channel -signals = [False] * 126 # for tracking the signal count on each channel -totals = [0] * 126 # for the total signal count on each channel - -# create table of progress bars (labeled by frequency channel in MHz) -table = Table.grid(padding=(0, 1)) -progress_bars: List[Progress] = [None] * 126 -for i in range(21): # 21 rows - row = [] - for j in range(i, i + (21 * 6), 21): # 6 columns - COLOR = "white" if int(j / 21) % 2 else "yellow" - progress_bars[j] = Progress( - TextColumn("{task.description}", style=Style(color=COLOR)), - BarColumn(style=Style(color=COLOR)), - TextColumn("{task.fields[signals]}", style=Style(color=COLOR)), - ) - # add only 1 task for each progress bar - progress_bars[j].add_task(f"{2400 + (j)}", total=CACHE_MAX, signals="-") - row.append(progress_bars[j]) - table.add_row(*row) + +def get_user_input() -> Tuple[int, int]: + """Get input parameters for the scan from the user.""" + for i, d_rate in enumerate(OFFERED_DATA_RATES): + print(f"{i + 1}. {d_rate}") + d_rate = input("Select your data rate [1, 2, 3] (defaults to 1 Mbps) ") + duration = input("How long (in seconds) to perform scan? ") + while not duration.isdigit(): + print("Please enter a positive number.") + duration = input("How long (in seconds) to perform scan? ") + return ( + max(1, min(3, 1 if not d_rate.isdigit() else int(d_rate))) - 1, + abs(int(duration)), + ) def scan_channel(channel: int) -> bool: @@ -66,44 +182,46 @@ def scan_channel(channel: int) -> bool: radio.channel = channel radio.startListening() time.sleep(0.00013) - result = radio.testRPD() + found_signal = radio.testRPD() radio.stopListening() - return result - - -def scan(duration: int = DURATION): - """Perform scan.""" - timeout = time.monotonic() + duration - console.print( - f"Scanning all channels using {SELECTED_RATE} for", - f"{duration} seconds. Channel labels are in MHz.", - ) - with Live(table, refresh_per_second=1000): - try: - while time.monotonic() < timeout: - for chl, p_bar in enumerate(progress_bars): - # save the latest in history (FIFO ordering) - history[chl] = history[chl][1:] + [signals[chl]] - - # refresh the latest - signals[chl] = scan_channel(chl) - - # update total signal count for the channel - totals[chl] += int(signals[chl]) - - p_bar.update( - p_bar.task_ids[0], - completed=history[chl].count(True), - signals="-" if not totals[chl] else totals[chl], - ) - except KeyboardInterrupt: - console.print(" Keyboard interrupt detected. Powering down radio.") - radio.powerDown() + if found_signal or radio.testRPD() or radio.available(): + radio.flush_rx() + return True + return False + + +def main(): + spectrum_passes = 0 + data_rate, duration = get_user_input() + print(f"Scanning for {duration} seconds at {OFFERED_DATA_RATES[data_rate]}") + init_radio() + radio.setDataRate(AVAILABLE_RATES[data_rate]) + try: + std_scr = init_curses() + timer_prompt = "Scanning for {:>3} seconds at " + OFFERED_DATA_RATES[data_rate] + std_scr.addstr(0, 0, "Channels are labeled in MHz.") + std_scr.addstr(1, 0, "Signal counts are clamped to a single hexadecimal digit.") + bars = init_display(std_scr) + channel, val = (0, False) + end = time.monotonic() + duration + while time.monotonic() < end: + std_scr.addstr(2, 0, timer_prompt.format(int(end - time.monotonic()))) + val = scan_channel(channel) + cache_sum = stored[channel].push(val) + if stored[channel].total: + bars[channel].update(cache_sum, stored[channel].total) + std_scr.refresh() + if channel + 1 == TOTAL_CHANNELS: + channel = 0 + spectrum_passes += 1 + else: + channel += 1 + finally: + radio.powerDown() + deinit_curses(spectrum_passes) if __name__ == "__main__": - scan() - radio.powerDown() + main() else: - console.print("Enter `scan()` to run a scan.") - console.print("Change data rate using `radio.setDataRate(RF24_**BPS)`") + print("Enter 'main()' to run the program.") diff --git a/examples_pico/scanner.cpp b/examples_pico/scanner.cpp index d67bd8b43..5d43d4d96 100644 --- a/examples_pico/scanner.cpp +++ b/examples_pico/scanner.cpp @@ -19,7 +19,27 @@ * good channel for your application. * * Inspired by cpixip. - * See http://arduino.cc/forum/index.php/topic,54795.0.html + * See https://forum.arduino.cc/t/poor-mans-2-4-ghz-scanner/54846 + */ + +/* + * How to read the output: + * - The header is a list of supported channels in decimal written vertically. + * - Each column corresponding to the vertical header is a hexadecimal count of + * detected signals (max is 15 or 'f'). + * + * The following example + * 000 + * 111 + * 789 + * ~~~ <- just a divider between the channel's vertical labels and signal counts + * 1-2 + * can be interpreted as + * - 1 signal detected on channel 17 + * - 0 signals (denoted as '-') detected on channel 18 + * - 2 signals detected on channel 19 + * + * Each line of signal counts represent 100 passes of the supported spectrum. */ #include "pico/stdlib.h" // printf(), sleep_ms(), getchar_timeout_us(), to_us_since_boot(), get_absolute_time() @@ -32,11 +52,19 @@ RF24 radio(CE_PIN, CSN_PIN); // Channel info -const uint8_t num_channels = 126; -uint8_t values[num_channels]; +const uint8_t num_channels = 126; // 0-125 are supported +uint8_t values[num_channels]; // the array to store summary of signal counts per channel + +// To detect noise, we'll use the worst addresses possible (a reverse engineering tactic). +// These addresses are designed to confuse the radio into thinking +// that the RF signal's preamble is part of the packet/payload. +const uint8_t noiseAddress[][6] = {{0x55, 0x55}, {0xAA, 0xAA}, {0x0A, 0xAA}, {0xA0, 0xAA}, {0x00, 0xAA}, {0xAB, 0xAA}}; -const int num_reps = 100; -int reset_array = 0; +const int num_reps = 100; // number of passes for each scan of the entire spectrum + +void printHeader(); +void scanChannel(uint8_t); +void initRadio(); int main() { @@ -47,37 +75,21 @@ int main() sleep_ms(10); } - // initialize the transceiver on the SPI bus - while (!radio.begin()) { - printf("radio hardware is not responding!!\n"); - } - // print example's name printf("RF24/examples_pico/scanner\n"); - radio.setAutoAck(false); - - // Get into standby mode - radio.startListening(); - radio.stopListening(); - - // radio.printDetails(); + // print a line that should not be wrapped + printf("\n!!! This example requires a width of at least 126 characters. "); + printf("If this text uses multiple lines, then the output will look bad.\n"); - // Print out header, high then low digit - int i = 0; - - while (i < num_channels) { - printf("%x", i >> 4); - ++i; + // initialize the transceiver on the SPI bus + while (!radio.begin()) { + printf("radio hardware is not responding!!\n"); } - printf("\n"); + initRadio(); - i = 0; - while (i < num_channels) { - printf("%x", i & 0xf); - ++i; - } - printf("\n"); + // Print out header + printHeader(); // forever loop while (1) { @@ -88,36 +100,105 @@ int main() int rep_counter = num_reps; while (rep_counter--) { - int i = num_channels; - while (i--) { + for (uint8_t i = 0; i < num_channels; ++i) { // Select this channel - radio.setChannel(i); - - // Listen for a little - radio.startListening(); - sleep_us(128); - radio.stopListening(); + scanChannel(i); // updates values[i] accordingly - // Did we get a carrier? - if (radio.testCarrier()) { - ++values[i]; - } + // Print out channel measurements, clamped to a single hex digit + if (values[i]) + printf("%x", rf24_min(0xf, values[i])); + else + printf("-"); } + printf("\r"); } + printf("\n"); - // Print out channel measurements, clamped to a single hex digit - i = 0; - while (i < num_channels) { - if (values[i]) - printf("%x", rf24_min(0xf, (values[i] & 0xf))); - else - printf("-"); - - ++i; + char input = getchar_timeout_us(0); // get char from buffer for user input + if (input != PICO_ERROR_TIMEOUT) { + if (input == 'b' || input == 'B') { + // reset to bootloader + radio.powerDown(); + reset_usb_boot(0, 0); + } } - printf("\n"); } return 0; } + +void initRadio() +{ + // configure the radio + radio.setAutoAck(false); // Don't acknowledge arbitrary signals + radio.disableCRC(); // Accept any signal we find + radio.setAddressWidth(2); // A reverse engineering tactic (not typically recommended) + for (uint8_t i = 0; i < 6; ++i) { + radio.openReadingPipe(i, noiseAddress[i]); + } + + // To set the radioNumber via the Serial terminal on startup + printf("\nSelect your data rate. "); + printf("Enter '1' for 1 Mbps, '2' for 2 Mbps, or '3' for 250 kbps. "); + printf("Defaults to 1 Mbps.\n"); + char input = getchar(); + if (input == 50) { + printf("\nUsing 2 Mbps.\n"); + radio.setDataRate(RF24_2MBPS); + } + else if (input == 51) { + printf("\nUsing 250 kbps.\n"); + radio.setDataRate(RF24_250KBPS); + } + else { + printf("\nUsing 1 Mbps.\n"); + radio.setDataRate(RF24_1MBPS); + } + + // Get into standby mode + radio.startListening(); + radio.stopListening(); + radio.flush_rx(); + // radio.printPrettyDetails(); +} + +void scanChannel(uint8_t channel) +{ + radio.setChannel(channel); + + // Listen for a little + radio.startListening(); + sleep_us(130); + bool foundSignal = radio.testRPD(); + radio.stopListening(); + + // Did we get a carrier? + if (foundSignal || radio.testRPD() || radio.available()) { + ++values[channel]; + radio.flush_rx(); + } +} + +void printHeader() +{ + // print the hundreds digits + for (uint8_t i = 0; i < num_channels; ++i) + printf("%d", (i / 100)); + printf("\n"); + + // print the tens digits + for (uint8_t i = 0; i < num_channels; ++i) + printf("%d", ((i % 100) / 10)); + printf("\n"); + + // print the singles digits + for (uint8_t i = 0; i < num_channels; ++i) + printf("%d", (i % 10)); + printf("\n"); + + // print the header's divider + for (uint8_t i = 0; i < num_channels; ++i) + printf("~"); + printf("\n"); +}