diff --git a/.gitignore b/.gitignore index 89cc49c..3711360 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch + +out/ diff --git a/.vscode/settings.json b/.vscode/settings.json index c2f89c3..b5ca201 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,4 +12,7 @@ }, "files.insertFinalNewline": true, "files.autoSave": "onFocusChange", + "files.associations": { + "cstdint": "cpp" + }, } diff --git a/README.md b/README.md index 957a5a0..3e7cc40 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ - Heltec Wireless Stick Lite V3 No Display (Not Tested) - Heltec Vision Master E290 - e-Ink 296 x 128 (No OSD) - Heltec Vision MAster T190 - color TFT 320X170 (No OSD) -- LilyGo Radio Lora T3S3 V.2 SX1262 +- LilyGo Radio Lora T3S3 V.2 SX1262 +- LilyGo Radio Lora T3S3 V.2 SX1280 +- LilyGo Radio Lora T3_V1.6.1 SX1276 (Not Tested) ## RF Spectrum Analyzer using Lora Radio @@ -158,6 +160,10 @@ If less, ESP32 will turn off. Fast pressing(less than 0.5 second) P button chang 3. Connect ESP32 to USB. Install USB CP2101 drivers for Windows or other OS https://docs.heltec.org/general/establish_serial_connection.html#for-windows https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers?tab=downloads + + ## NOTE: MACOS Heltec USB driver + https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers?tab=downloads
+ I used legacy driver 5. Clone this Git Repo or download zip of the sources ![image](https://github.com/user-attachments/assets/971b6592-3b71-414c-971c-2ecd20f0f0b7) @@ -174,6 +180,15 @@ If less, ESP32 will turn off. Fast pressing(less than 0.5 second) P button chang 7. Select Proper Environment ![image](https://github.com/user-attachments/assets/a9c6557b-a387-4457-b59b-b3d7242d2826) + + --- + + >**Important note:** If using a Heltec V3 board, make sure your ESP32 Expressif catalog is up to date before selecting environment, otherwise might get a build time error such as: `Error: Unknown board ID 'heltec_wifi_lora_32_V3'` when trying to select the environment. + > + >Open a PlatformIO CLI: https://docs.platformio.org/en/latest/integration/ide/vscode.html#platformio-core-cli + > + >Run: `pio pkg update -g -p espressif32` + 8. Select ESP32 USB Device to program ![image](https://github.com/user-attachments/assets/af76c4b1-7122-45e1-b26b-08b59e03ca3b) Note: It is theoretically possible to program via WiFi and BTH. @@ -222,6 +237,24 @@ or buy : ![image](https://github.com/user-attachments/assets/a1e00b51-5566-4ff5-98fe-67eaeb5bc81f) We are using pin 41 as a Buzzer trigger. Connect buzzer + leg with pin 41 and - leg with the ground (GND). You can change the buzzer pin in the code. + +## Analog FPV OSD (ON SCREEN DISPLAY) +To Enable OSD, Uncomment these lines
+``` +// #define OSD_ENABLED true +``` +**OSD sidebar enabled/disable** +comment or uncomment this line +``` +#define OSD_SIDE_BAR true +``` + +Or you can set this and other variables as a build parameter: +``` +build_flags = + -DOSD_ENABLED +``` + ## DFRobot OSD Wiring **Heltec V3 -> DFRobot OSD**
GND -> GND
@@ -287,3 +320,11 @@ Edit **paltformio.io** uncommenting/selecting your sources ``` for LilyGo use env:heltec_wifi_lora_32_V3 +# WiFi and Bluetooth BT Scanning +Works only with OSD enabled
+Uncomment this lines +``` +// #define OSD_ENABLED true +// #define WIFI_SCANNING_ENABLED true +// #define BT_SCANNING_ENABLED true +``` diff --git a/SpectrumScan.py b/SpectrumScan.py index acc37a0..ee713a3 100644 --- a/SpectrumScan.py +++ b/SpectrumScan.py @@ -7,30 +7,21 @@ import sys import numpy as np import matplotlib as mpl import matplotlib.pyplot as plt - +import json from datetime import datetime from argparse import RawTextHelpFormatter -# number of samples in each scanline -SCAN_WIDTH = 33 +# Constants +SCAN_WIDTH = 33 # number of samples in each scanline +OUT_PATH = "out" # output path for saved files -# scanline Serial start/end markers -SCAN_MARK_START = 'SCAN ' -SCAN_MARK_FREQ = 'FREQ ' -SCAN_MARK_END = ' END' - -# output path -OUT_PATH = 'out' - -# default settings +# Default settings DEFAULT_BAUDRATE = 115200 DEFAULT_COLOR_MAP = 'viridis' DEFAULT_SCAN_LEN = 200 DEFAULT_RSSI_OFFSET = -11 -# Print iterations progress -# from https://stackoverflow.com/questions/3173320/text-progress-bar-in-terminal-with-block-characters -def printProgressBar (iteration, total, prefix = '', suffix = '', decimals = 1, length = 50, fill = '█', printEnd = "\r"): +def print_progress_bar(iteration, total, prefix='', suffix='', decimals=1, length=50, fill='█', print_end="\r"): """ Call in a loop to create terminal progress bar @params: @@ -41,136 +32,104 @@ def printProgressBar (iteration, total, prefix = '', suffix = '', decimals = 1, decimals - Optional : positive number of decimals in percent complete (Int) length - Optional : character length of bar (Int) fill - Optional : bar fill character (Str) - printEnd - Optional : end character (e.g. "\r", "\r\n") (Str) + print_end - Optional : end character (e.g. "\r", "\r\n") (Str) """ percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total))) - filledLength = int(length * iteration // total) - bar = fill * filledLength + '-' * (length - filledLength) - print(f'\r{prefix} |{bar}| {percent}% {suffix}', end = printEnd) + filled_length = int(length * iteration // total) + bar = fill * filled_length + '-' * (length - filled_length) + print(f'\r{prefix} |{bar}| {percent}% {suffix}', end=print_end) if iteration == total: print() +def parse_line(line): + """Parse a JSON line from the serial input.""" + return json.loads(line) def main(): - parser = argparse.ArgumentParser(formatter_class=RawTextHelpFormatter, description=''' - RadioLib SX126x_Spectrum_Scan plotter script. Displays output from SX126x_Spectrum_Scan example - as grayscale and + parser = argparse.ArgumentParser(formatter_class=RawTextHelpFormatter, description='''\ + Parse serial data from LOG_DATA_JSON functionality. - Depends on pyserial and matplotlib, install by: - 'python3 -m pip install pyserial matplotlib' - - Step-by-step guide on how to use the script: - 1. Upload the SX126x_Spectrum_Scan example to your Arduino board with SX1262 connected. + 1. #define LOG_DATA_JSON true - add this line in main.cpp, upload to device 2. Run the script with appropriate arguments. 3. Once the scan is complete, output files will be saved to out/ ''') - parser.add_argument('port', - type=str, - help='COM port to connect to the device') - parser.add_argument('--speed', - default=DEFAULT_BAUDRATE, - type=int, - help=f'COM port baudrate (defaults to {DEFAULT_BAUDRATE})') - parser.add_argument('--map', - default=DEFAULT_COLOR_MAP, - type=str, - help=f'Matplotlib color map to use for the output (defaults to "{DEFAULT_COLOR_MAP}")') - parser.add_argument('--len', - default=DEFAULT_SCAN_LEN, - type=int, - help=f'Number of scanlines to record (defaults to {DEFAULT_SCAN_LEN})') - parser.add_argument('--offset', - default=DEFAULT_RSSI_OFFSET, - type=int, - help=f'Default RSSI offset in dBm (defaults to {DEFAULT_RSSI_OFFSET})') - parser.add_argument('--freq', - default=-1, - type=float, - help=f'Default starting frequency in MHz') + parser.add_argument('port', type=str, help='COM port to connect to the device') + parser.add_argument('--speed', default=DEFAULT_BAUDRATE, type=int, + help=f'COM port baudrate (defaults to {DEFAULT_BAUDRATE})') + parser.add_argument('--map', default=DEFAULT_COLOR_MAP, type=str, + help=f'Matplotlib color map to use for the output (defaults to "{DEFAULT_COLOR_MAP}")') + parser.add_argument('--len', default=DEFAULT_SCAN_LEN, type=int, + help=f'Number of scanlines to record (defaults to {DEFAULT_SCAN_LEN})') + parser.add_argument('--offset', default=DEFAULT_RSSI_OFFSET, type=int, + help=f'Default RSSI offset in dBm (defaults to {DEFAULT_RSSI_OFFSET})') + parser.add_argument('--freq', default=-1, type=float, + help='Default starting frequency in MHz') args = parser.parse_args() - freq_mode = False + # Create the result array scan_len = args.len - if (args.freq != -1): - freq_mode = True - scan_len = 1000 + arr = np.zeros((scan_len, SCAN_WIDTH)) - # create the color map and the result array - arr = np.zeros((SCAN_WIDTH, scan_len)) - - # scanline counter + # Scanline counter row = 0 - # list of frequencies in frequency mode + # List of frequencies freq_list = [] - # open the COM port + # Open the COM port with serial.Serial(args.port, args.speed, timeout=None) as com: - while(True): - # update the progress bar - if not freq_mode: - printProgressBar(row, scan_len) + while True: + # Update the progress bar + print_progress_bar(row, scan_len) - # read a single line + # Read a single line try: - line = com.readline().decode('utf-8') - except: + line = com.readline().decode('utf-8').strip() + except UnicodeDecodeError: continue - if SCAN_MARK_FREQ in line: - new_freq = float(line.split(' ')[1]) - if (len(freq_list) > 1) and (new_freq < freq_list[-1]): - break + if line.startswith("{"): + try: + data = parse_line(line) + except json.JSONDecodeError: + continue - freq_list.append(new_freq) - print('{:.3f}'.format(new_freq), end = '\r') - continue + # get the lowest frequency for now, could be averaged too. + freq = data["low_range_freq"] - # check the markers - if (SCAN_MARK_START in line) and (SCAN_MARK_END in line): - # get the values - scanline = line[len(SCAN_MARK_START):-len(SCAN_MARK_END)].split(',') - for col in range(SCAN_WIDTH): - arr[col][row] = int(scanline[col]) + # value in negative, eg: -70 + rssi = int(data["value"]) + + if freq not in freq_list: + freq_list.append(freq) - # increment the row counter - row = row + 1 + col = freq_list.index(freq) + arr[row][col] = rssi + + # Increment the row counter + row += 1 - # check if we're done - if (not freq_mode) and (row >= scan_len): + # Check if we're done + if row >= scan_len: break - - # scale to the number of scans (sum of any given scanline) - num_samples = arr.sum(axis=0)[0] - arr *= (num_samples/arr.max()) - if freq_mode: - scan_len = len(freq_list) + # Create the figure + fig, ax = plt.subplots(figsize=(12, 8)) - # create the figure - fig, ax = plt.subplots() + # Display the result as heatmap + extent = [0, scan_len, freq_list[0], freq_list[-1]] + im = ax.imshow(arr.T, cmap=args.map, extent=extent, aspect='auto', origin='lower') + fig.colorbar(im, label='RSSI (dBm)') - # display the result as heatmap - extent = [0, scan_len, -4*(SCAN_WIDTH + 1), args.offset] - if freq_mode: - extent[0] = freq_list[0] - extent[1] = freq_list[-1] - im = ax.imshow(arr[:,:scan_len], cmap=args.map, extent=extent) - fig.colorbar(im) - - # set some properites and show + # Set plot properties and show timestamp = datetime.now().strftime('%y-%m-%d %H-%M-%S') - title = f'RadioLib SX126x Spectral Scan {timestamp}' - if freq_mode: - plt.xlabel("Frequency [Hz]") - else: - plt.xlabel("Time [sample]") - plt.ylabel("RSSI [dBm]") - ax.set_aspect('auto') + title = f'LoraSA Spectral Scan {timestamp}' + plt.xlabel("Time (sample)") + plt.ylabel("Frequency (MHz)") fig.suptitle(title) fig.canvas.manager.set_window_title(title) plt.savefig(f'{OUT_PATH}/{title.replace(" ", "_")}.png', dpi=300) plt.show() if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/data/index.html b/data/index.html new file mode 100644 index 0000000..82bfe6a --- /dev/null +++ b/data/index.html @@ -0,0 +1,42 @@ + + + + + ESP Wi-Fi Manager + + + + + + +
+

LORA SA ESP32 CONFIG

+
+
+
+
+
+

+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +

+
+
+
+
+ + + diff --git a/data/style.css b/data/style.css new file mode 100644 index 0000000..491d8c4 --- /dev/null +++ b/data/style.css @@ -0,0 +1,118 @@ +html { + font-family: Arial, Helvetica, sans-serif; + display: inline-block; + text-align: center; +} + +h1 { + font-size: 1.8rem; + color: white; +} + +p { + font-size: 1.4rem; +} + +.topnav { + overflow: hidden; + background-color: #0A1128; +} + +body { + margin: 0; +} + +.content { + padding: 5%; +} + +.card-grid { + max-width: 800px; + margin: 0 auto; + display: grid; + grid-gap: 2rem; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); +} + +.card { + background-color: white; + box-shadow: 2px 2px 12px 1px rgba(140, 140, 140, .5); +} + +.card-title { + font-size: 1.2rem; + font-weight: bold; + color: #034078 +} + +input[type=submit] { + border: none; + color: #FEFCFB; + background-color: #034078; + padding: 15px 15px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + width: 100px; + margin-right: 10px; + border-radius: 4px; + transition-duration: 0.4s; +} + +input[type=submit]:hover { + background-color: #1282A2; +} + +input[type=text], +input[type=number], +select { + width: 50%; + padding: 12px 20px; + margin: 18px; + display: inline-block; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; +} + +label { + font-size: 1.2rem; +} + +.value { + font-size: 1.2rem; + color: #1282A2; +} + +.state { + font-size: 1.2rem; + color: #1282A2; +} + +button { + border: none; + color: #FEFCFB; + padding: 15px 32px; + text-align: center; + font-size: 16px; + width: 100px; + border-radius: 4px; + transition-duration: 0.4s; +} + +.button-on { + background-color: #034078; +} + +.button-on:hover { + background-color: #1282A2; +} + +.button-off { + background-color: #858585; +} + +.button-off:hover { + background-color: #252524; +} diff --git a/data/text.txt b/data/text.txt new file mode 100644 index 0000000..e69de29 diff --git a/eink_src/main.cpp b/eink_src/main.cpp index a87ece8..ee82ccb 100644 --- a/eink_src/main.cpp +++ b/eink_src/main.cpp @@ -23,6 +23,7 @@ #define DISPLAY_WIDTH 296 #define DISPLAY_HEIGHT 128 // Without this line Lora Radio doesn't work with heltec lib +#define BUTTON 21 #define ARDUINO_heltec_wifi_32_lora_V3 #include "heltec_unofficial.h" @@ -96,6 +97,7 @@ bool first_run, new_pixel, detected_x = false; // drone detection flag bool detected = false; uint64_t drone_detection_level = 90; +bool detection_level_changed = false; uint64_t drone_detected_frequency_start = 0; uint64_t drone_detected_frequency_end = 0; uint64_t detection_count = 0; @@ -250,18 +252,21 @@ void VextOFF(void) // Vext default OFF constexpr int lower_level = 108; constexpr int up_level = 40; +constexpr int start_pixel = 80; + int rssiToPix(int rssi) { // Bigger is lower signal if (abs(rssi) >= lower_level) { - return lower_level - 1; + return start_pixel - 1; } if (abs(rssi) <= up_level) { - return up_level; + return start_pixel - up_level; } - return abs(rssi); + + return start_pixel - (lower_level - abs(rssi)); } long timeSinceLastModeSwitch = 0; @@ -287,10 +292,61 @@ long display_scan_start = 0; long display_scan_end = 0; long display_scan_i_end = 0; int scan_iterations = 0; +bool waterfall_values[DISPLAY_HEIGHT][DISPLAY_WIDTH] = {false}; constexpr unsigned int SCANS_PER_DISPLAY = 5; constexpr unsigned int STATUS_BAR_HEIGHT = 5; +void clear_rectangle(int x, int y, int width, int height) +{ + for (int x = 0; x < width; x++) + { + for (int y = 0; y < height; y++) + { + display.clearPixel(x, y); + } + } +} + +void button_logic(void) +{ + heltec_loop(); + button_pressed_counter = 0; + if (button.pressed()) + { + drone_detection_level++; + detection_level_changed = true; + if (drone_detection_level > 107) + drone_detection_level = DEFAULT_DRONE_DETECTION_LEVEL - 20; + clear_rectangle(0, 0, 10, 10); + display.setFont(ArialMT_Plain_10); + + display.drawString(0, 0, + "T:" + String(display_scan_end - display_scan_start) + "/" + + String(display_scan_i_end - display_scan_start) + " L:-" + + String(drone_detection_level) + "dB"); + while (button.pressedNow()) + { + + display.display(); + button_pressed_counter++; + // button.update(); + + if (button_pressed_counter > 18) + { + // Some sign there + } + } + } + + if (button_pressed_counter < 9 && button_pressed_counter > 5) + { + display.clear(); + display.display(); + heltec_deep_sleep(); + } +} + void loop() { if (screen_update_loop_counter == 0) @@ -307,9 +363,13 @@ void loop() int u = 0; for (int i = 0; i < SAMPLES_RSSI; i++) { - - radio.setFrequency((float)fr + (float)(rssi_mhz_step * u), - false); // false = no calibration need here + state = radio.setFrequency((float)fr + (float)(rssi_mhz_step * u), + true); // false = no calibration need here + int radio_error_count = 0; + if (state != RADIOLIB_ERR_NONE) + { + Serial.println("E:setFrequency:" + String(freq)); + } u++; if (rssi_mhz_step * u >= mhz_step) { @@ -319,10 +379,14 @@ void loop() rssi2 = radio.getRSSI(false); scan_iterations++; if (rssi2 > lower_level) + { + max_scan_rssi[x1] = rssi2; continue; + } #ifdef PRINT_DEBUG Serial.println(String(fr) + ":" + String(rssi2)); #endif + button_logic(); // display.drawString(x1, (int)y2, String(fr) + ":" + String(rssi2)); display.setPixel(x1, rssiToPix(rssi2)); @@ -332,6 +396,16 @@ void loop() } } + // Waterfall per scan not per screen + if (abs(max_scan_rssi[x1]) <= drone_detection_level) + { + waterfall_values[w][x1] = true; + } + else + { + waterfall_values[w][x1] = false; + } + // drone detection level line if (x1 % 2 == 0) { @@ -343,9 +417,15 @@ void loop() { display_scan_i_end = millis(); } + button_logic(); // Main N x-axis full loop end logic if (x1 >= STEPS) { + w++; + if (w >= DISPLAY_HEIGHT - start_pixel - 13) + { + w = 0; + } if (screen_update_loop_counter == SCANS_PER_DISPLAY) { // max Mhz and dB in window @@ -372,17 +452,29 @@ void loop() display.drawString(i - rssi_window_size + 5, y2 + 10, String(window_max_fr)); // Vertical lines between windows - for (int l = y2; l < 100; l += 4) + for (int l = y2; l < start_pixel; l += 4) { display.setPixel(i, l); } } window_max_rssi = -999; } + + // Draw Waterfall + for (int y = 0; y < DISPLAY_HEIGHT; y++) + if (waterfall_values[y][i] == true) + { + display.setPixel(i, start_pixel + 5 + y); + } } + // Draw Waterfall cursor + display.drawHorizontalLine(0, start_pixel + 5 + w, DISPLAY_WIDTH); display_scan_end = millis(); - + if (detection_level_changed == true) + { + clear_rectangle(0, 0, 100, 10); + } display.setFont(ArialMT_Plain_10); display.drawString(0, 0, "T:" + String(display_scan_end - display_scan_start) + @@ -396,6 +488,7 @@ void loop() esp_restart(); } + // ToDo: it doesn't work battery(); // iteration full scan / samples pixel step / numbers of scan per display display.drawString(DISPLAY_WIDTH - ((DISPLAY_WIDTH / 6) * 2) - 5, 0, @@ -409,21 +502,21 @@ void loop() "s:" + String(mhz_step)); // Draw a line horizontally - display.drawHorizontalLine(0, lower_level + 1, DISPLAY_WIDTH); + display.drawHorizontalLine(0, 1 + start_pixel, DISPLAY_WIDTH); // Generate Ticks for (int x = 0; x < DISPLAY_WIDTH; x++) { if (x % (DISPLAY_WIDTH / 2) == 0 && x > 5) { - display.drawVerticalLine(x, lower_level + 1, 11); + display.drawVerticalLine(x, 1 + start_pixel, 11); // central tick width // display.drawVerticalLine(x - 1, lower_level + 1, 8); // display.drawVerticalLine(x + 1, lower_level + 1, 8); } if (x % 10 == 0 || x == 0) - display.drawVerticalLine(x, lower_level + 1, 6); + display.drawVerticalLine(x, 1 + start_pixel, 6); if (x % 5 == 0) - display.drawVerticalLine(x, lower_level + 1, 3); + display.drawVerticalLine(x, 1 + start_pixel, 3); } display.setFont(ArialMT_Plain_10); @@ -441,13 +534,14 @@ void loop() String(FREQ_BEGIN + (((int)fr - FREQ_BEGIN) - ((int)fr - FREQ_BEGIN) / 4))); // End Mhz - display.drawString(DISPLAY_WIDTH - 24, DISPLAY_HEIGHT - 10, String((int)fr)); + display.drawString(DISPLAY_WIDTH - 20, DISPLAY_HEIGHT - 10, String((int)fr)); display.display(); // display will be cleared next scan iteration. it is just buffer clear // memset(buffer, 0, displayBufferSize); display.clear(); screen_update_loop_counter = 0; + scan_iterations = 0; display_scan_i_end = 0; } @@ -480,7 +574,7 @@ void setup() delay(1000); display.clear(); Serial.begin(115200); - w = WATERFALL_START; + w = 0; // WATERFALL_START; init_radio(); state = radio.startReceive(RADIOLIB_SX126X_RX_TIMEOUT_NONE); if (state != RADIOLIB_ERR_NONE) diff --git a/gps_src/main.cpp b/gps_src/main.cpp new file mode 100644 index 0000000..0ea23d7 --- /dev/null +++ b/gps_src/main.cpp @@ -0,0 +1,126 @@ +#include +#include + +// Objects for GNSS parsing and serial communication +TinyGPSPlus gps; +HardwareSerial GNSSSerial(2); + +// Pin definitions for GNSS module communication +const int GNSS_RXPin = 34; +const int GNSS_TXPin = 33; +const int GNSS_RSTPin = 35; // There is a function built for this in the example below- + // currently it isn't used +const int GNSS_PPS_Pin = 36; + +// Flags for PPS handling and synchronization status +volatile bool ppsFlag = false; +volatile bool initialSyncDone = false; + +// Timestamp for the last valid GNSS data received +unsigned long lastGNSSDataMillis = 0; + +void setup() +{ + // Initialize serial communication for debugging + USBSerial.begin(115200); + while (!USBSerial) + ; + + // Start GNSS module communication + GNSSSerial.begin(115200, SERIAL_8N1, GNSS_TXPin, GNSS_RXPin); + while (!GNSSSerial) + ; + + // Configure GNSS reset pin + pinMode(GNSS_RSTPin, OUTPUT); + digitalWrite(GNSS_RSTPin, HIGH); + + // Set up PPS pin and attach an interrupt handler + pinMode(GNSS_PPS_Pin, INPUT); + attachInterrupt(digitalPinToInterrupt(GNSS_PPS_Pin), ppsInterrupt, RISING); + + // Short delay for GNSS module initialization + delay(1000); +} + +void loop() +{ + // Process incoming GNSS data + while (GNSSSerial.available()) + { + if (gps.encode(GNSSSerial.read())) + { + // Update the timestamp when valid GNSS data is received + lastGNSSDataMillis = millis(); + displayGNSSData(); // Display GNSS data for debugging + } + } + + // Perform initial synchronization using NMEA time data + if (!initialSyncDone && gps.date.isValid() && gps.time.isValid()) + { + setSystemTime(); + initialSyncDone = true; + USBSerial.println("Initial time synchronization done using NMEA data."); + } + + // Disable interrupts to safely check and reset the PPS flag + noInterrupts(); + if (ppsFlag) + { + fineTuneSystemTime(); // Adjust system time based on the PPS pulse + ppsFlag = false; + } + // Re-enable interrupts + interrupts(); + + // Check if GNSS data has been absent for more than a minute + if (millis() - lastGNSSDataMillis > 60000) + { + USBSerial.println("Warning: Haven't received GNSS data for more than 1 minute!"); + // Additional actions can be added here, like alerts or module resets. + } +} + +// Interrupt handler for the PPS signal +void ppsInterrupt() { ppsFlag = true; } + +// Function to set system time using GNSS data +void setSystemTime() +{ + struct tm timeinfo; + timeinfo.tm_year = gps.date.year() - 1900; + timeinfo.tm_mon = gps.date.month() - 1; + timeinfo.tm_mday = gps.date.day(); + timeinfo.tm_hour = gps.time.hour(); + timeinfo.tm_min = gps.time.minute(); + timeinfo.tm_sec = gps.time.second(); + time_t t = mktime(&timeinfo); + + timeval tv = {t, 0}; + settimeofday(&tv, NULL); // Update system time +} + +// Function to fine-tune system time using the PPS pulse +void fineTuneSystemTime() +{ + timeval tv; + gettimeofday(&tv, NULL); + tv.tv_usec = 0; // Reset microseconds to zero + settimeofday(&tv, NULL); // Update system time + USBSerial.println("System time fine-tuned using PPS signal."); +} + +// Debugging function to display GNSS data +void displayGNSSData() +{ + USBSerial.print("Latitude: "); + USBSerial.println(gps.location.lat(), 6); + USBSerial.print("Longitude: "); + USBSerial.println(gps.location.lng(), 6); + USBSerial.print("Altitude: "); + USBSerial.println(gps.altitude.meters()); + USBSerial.print("Speed: "); + USBSerial.println(gps.speed.kmph()); + USBSerial.println("-----------------------------"); +} diff --git a/include/DFRobot_OSD.h b/include/DFRobot_OSD.h index d446a86..063753c 100644 --- a/include/DFRobot_OSD.h +++ b/include/DFRobot_OSD.h @@ -13,8 +13,67 @@ #ifndef _DFRobot_OSD_H_ #define _DFRobot_OSD_H_ +#define MAX_POWER_LEVELS 33 + #include +/*Define Custom characters Example*/ +static const int buf0[36] = {0x02, 0x80, 0x02, 0x40, 0x7F, 0xE0, 0x42, 0x00, + 0x42, 0x00, 0x7A, 0x40, 0x4A, 0x40, 0x4A, 0x80, + 0x49, 0x20, 0x5A, 0xA0, 0x44, 0x60, 0x88, 0x20}; + +static constexpr uint16_t levels[10] = { + 0x105, // 0 + 0x10E, // 1 + 0x10D, // 2 + 0x10C, // 3 + 0x10B, // 4 + 0x10A, // 5 + 0x109, // 6 + 0x108, // 7 + 0x107, // 8 + 0x106, // 9 +}; + +static constexpr uint16_t power_level[MAX_POWER_LEVELS + 1] = { + 0x10E, // 0 + 0x10E, // 1 + 0x10D, // 2 + 0x10C, // 3 + 0x10B, // 4 + 0x10A, // 5 + 0x109, // 6 + 0x108, // 7 + 0x107, // 8 + 0x106, // 9 not using 106 to accent rise + // new line + 0x10E, // 10 + 0x10D, // 11 + 0x10C, // 12 + 0x10B, // 13 + 0x10A, // 14 + 0x109, // 15 + 0x108, // 16 + 0x107, // 17 + 0x106, // 18 not using 106 + // new line + 0x10E, // 19 + 0x10D, // 20 + 0x10C, // 21 + 0x10B, // 22 + 0x10A, // 23 + 0x109, // 24 + 0x108, // 25 + 0x107, // 26 + 0x106, // 27 + 0x105, // 28 --- + 0x105, // 29 + 0x105, // 30 + 0x105, // 31 + 0x105, // 32 + 0x105 // 33 +}; + // #define ENABLE_DBG //!< Open this macro and you can see the details of the program #ifdef ENABLE_DBG #define DBG(...) \ diff --git a/include/File.h b/include/File.h new file mode 100644 index 0000000..6f502ec --- /dev/null +++ b/include/File.h @@ -0,0 +1,55 @@ +#include "FS.h" +#include + +// Initialize LittleFS +void initLittleFS() +{ + if (!LittleFS.begin(true)) + { + Serial.println("An error has occurred while mounting LittleFS"); + } + Serial.println("LittleFS mounted successfully"); +} + +String readFile(fs::FS &fs, const char *path) +{ + Serial.printf("Reading file: %s\r\n", path); + + File file = fs.open(path); + if (!file || file.isDirectory()) + { + Serial.println("- failed to open file for reading"); + return String(""); + } + String content; + Serial.println("- read from file:"); + while (file.available()) + { + content = file.readStringUntil('\n'); + } + file.close(); + return content; +} + +void writeFile(fs::FS &fs, const char *path, const char *message) +{ + Serial.printf("Writing file: %s\r\n", path); + Serial.printf("Content: %s\r\n", message); + + File file = fs.open(path, FILE_WRITE); + if (!file) + { + Serial.println("- failed to open file for writing"); + return; + } + if (file.print(message)) + { + Serial.println("- file written"); + delay(500); + } + else + { + Serial.println("- write failed"); + } + file.close(); +} diff --git a/include/LiLyGo.h b/include/LiLyGo.h index ec8754c..1d44603 100644 --- a/include/LiLyGo.h +++ b/include/LiLyGo.h @@ -1,32 +1,4 @@ -#define UNUSED_PIN (0) -// LilyGo defined - -#define I2C_SDA 18 -#define I2C_SCL 17 -#define OLED_RST UNUSED_PIN - -#define RADIO_SCLK_PIN 5 -#define RADIO_MISO_PIN 3 -#define RADIO_MOSI_PIN 6 -#define RADIO_CS_PIN 7 - -#define SDCARD_MOSI 11 -#define SDCARD_MISO 2 -#define SDCARD_SCLK 14 -#define SDCARD_CS 13 - -#define BOARD_LED 37 -#define LED_ON HIGH - -#define BUTTON_PIN 0 -#define ADC_PIN 1 - -#define RADIO_RST_PIN 8 - -#define RADIO_DIO1_PIN 33 -#define RADIO_BUSY_PIN 34 - // Define for our code #define RST_OLED UNUSED_PIN #define LED BOARD_LED @@ -41,8 +13,11 @@ #else #define DISPLAY_WIDTH 128 #define DISPLAY_HEIGHT 64 -#include "OLEDDisplayUi.h" +// #include "OLEDDisplayUi.h" +// #include "SH1106Wire.h" +// #include "SSD1306Brzo.h" #include "SSD1306Wire.h" + #endif #define ARDUINO_heltec_wifi_32_lora_V3 #ifndef HELTEC_NO_RADIO_INSTANCE @@ -52,13 +27,24 @@ #include SPIClass *hspi = new SPIClass(2); SX1262 radio = new Module(SS, DIO1, RST_LoRa, BUSY_LoRa, *hspi); -#else +#else // ARDUINO_heltec_wifi_32_lora_V3 +#ifdef USING_SX1280PA +SX1280 radio = new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN); +#endif // end USING_SX1280PA +#ifdef USING_SX1262 // Default SPI on pins from pins_arduino.h SX1262 radio = new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN); -#endif -#endif - -void heltec_loop() {} +#endif // end USING_SX1262 +#ifdef USING_LR1121 +// Default SPI on pins from pins_arduino.h +LR1121 radio = new Module(RADIO_CS_PIN, RADIO_DIO9_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN); +#endif // end USING_LR1121 +#ifdef USING_SX1276 +// Default SPI on pins from pins_arduino.h +SX1276 radio = new Module(RADIO_CS_PIN, RADIO_DIO1_PIN, RADIO_RST_PIN, RADIO_BUSY_PIN); +#endif // end USING_SX1276 +#endif // end ARDUINO_heltec_wifi_32_lora_V3 +#endif // end HELTEC_NO_RADIO_INSTANCE void heltec_led(int led) {} @@ -101,16 +87,29 @@ class PrintSplitter : public Print #else #define DISPLAY_GEOMETRY GEOMETRY_128_64 #endif -SSD1306Wire display(0x3c, 18, 17, DISPLAY_GEOMETRY); +#define SCREEN_ADDRESS 0x3C + +SSD1306Wire display(SCREEN_ADDRESS, I2C_SDA, I2C_SCL, DISPLAY_GEOMETRY); +// SH1106Wire display(0x3c, I2C_SDA, I2C_SCL, DISPLAY_GEOMETRY); PrintSplitter both(Serial, display); #else Print &both = Serial; #endif // some fake pin -#define BUTTON 38 +#ifdef T3_V1_6_SX1276 +#define BUTTON_PIN 22 +#endif +#define BUTTON BUTTON_PIN #include "HotButton.h" HotButton button(BUTTON); +void heltec_loop() +{ +#ifndef DT3_V1_6_SX1276 + button.update(); +#endif +} + // This file contains a binary patch for the SX1262 #include "modules/SX126x/patches/SX126x_patch_scan.h" @@ -161,7 +160,7 @@ void heltec_setup() #ifndef HELTEC_NO_DISPLAY_INSTANCE heltec_display_power(true); display.init(); - display.setContrast(200); + // display.setContrast(200); display.flipScreenVertically(); #endif } diff --git a/include/WIFI_SERVER.h b/include/WIFI_SERVER.h new file mode 100644 index 0000000..93a715d --- /dev/null +++ b/include/WIFI_SERVER.h @@ -0,0 +1,205 @@ +#include +#include +#include +#include + +// Create AsyncWebServer object on port 80 +AsyncWebServer server(80); + +// Search for parameter in HTTP POST request +const String SSID = "ssid"; +const String PASS = "pass"; +const String IP = "ip"; +const String GATEWAY = "gateway"; +const String FSTART = "fstart"; +const String FEND = "fend"; + +// File paths to save input values permanently +// const char *ssidPath = "/ssid.txt"; + +// Variables to save values from HTML form +String ssid = "LoraSA", pass = "1234567890", ip = "192.168.1.100", + gateway = "192.168.1.1", fstart = "", fend = "", smpls = ""; + +IPAddress localIP; +// Set your Gateway IP address +IPAddress localGateway; +IPAddress subnet(255, 255, 0, 0); + +// Timer variables +unsigned long previousMillis = 0; +const long interval = 10000; // interval to wait for Wi-Fi connection (milliseconds) + +// Initialize WiFi +bool initWiFi() +{ + Serial.println("SSID:" + ssid); + Serial.println("PSWD:" + pass); + Serial.println("IP:" + ip); + Serial.println("SUB:" + subnet); + Serial.println("GATAWAY:" + gateway); + if (ssid == "" || ip == "") + { + Serial.println("Undefined SSID or IP address."); + return false; + } + + WiFi.mode(WIFI_STA); + localIP.fromString(ip.c_str()); + localGateway.fromString(gateway.c_str()); + + if (!WiFi.config(localIP, localGateway, subnet)) + { + Serial.println("STA Failed to configure"); + return false; + } + WiFi.begin(ssid.c_str(), pass.c_str()); + Serial.println("Connecting to WiFi..."); + + unsigned long currentMillis = millis(); + previousMillis = currentMillis; + + while (WiFi.status() != WL_CONNECTED) + { + currentMillis = millis(); + if (currentMillis - previousMillis >= interval) + { + Serial.println("Failed to connect."); + return false; + } + } + + Serial.println(WiFi.localIP()); + return true; +} + +void writeParameterToFile(String value, String file) +{ + // Write file to save value + writeFile(LittleFS, file.c_str(), value.c_str()); +} + +void writeParameterToParameterFile(String param, String value) +{ + String file = String("/" + param + ".txt"); + // Write file to save value + writeParameterToFile(value, file.c_str()); +} + +String readParameterFromParameterFile(String param) +{ + String file = String("/" + param + ".txt"); + return readFile(LittleFS, file.c_str()); +} + +void serverServer() +{ + // Route for root / web page + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) + { request->send(LittleFS, "/index.html", "text/html"); }); + + server.serveStatic("/", LittleFS, "/"); + + server.on("/", HTTP_POST, + [](AsyncWebServerRequest *request) + { + int params = request->params(); + for (int i = 0; i < params; i++) + { + Serial.println("Parameter " + String(i) + ": " + + request->getParam(i)->value()); + } + Serial.println(request->params()); + + String p; + if (request->hasParam(IP, true)) + { + p = request->getParam(IP, true)->value(); + writeParameterToParameterFile(IP, p); + } + + if (request->hasParam(IP, true)) + { + p = request->getParam(IP, true)->value(); + writeParameterToParameterFile(IP, p); + } + + if (request->hasParam(IP, true)) + { + p = request->getParam(IP, true)->value(); + writeParameterToParameterFile(IP, p); + } + + if (request->hasParam(GATEWAY, true)) + { + p = request->getParam(GATEWAY, true)->value(); + writeParameterToParameterFile(GATEWAY, p); + } + + if (request->hasParam(FSTART, true)) + { + p = request->getParam(FSTART, true)->value(); + writeParameterToParameterFile(FSTART, p); + } + + if (request->hasParam(FEND, true)) + { + p = request->getParam(FEND, true)->value(); + writeParameterToParameterFile(FEND, p); + } + + if (request->hasParam("samples", true)) + { + p = request->getParam("samples", true)->value(); + writeParameterToParameterFile("samples", p); + } + + request->send(200, "text/plain", + "Done. ESP will restart, connect to your router and " + "go to IP address: " + + ip); + delay(3000); + ESP.restart(); + }); + + /* // Route to set GPIO state to HIGH + server.on("/on", HTTP_GET, + [](AsyncWebServerRequest *request) + { + digitalWrite(ledPin, HIGH); + request->send(LittleFS, "/index.html", "text/html", false, + processor); + }); + + // Route to set GPIO state to LOW + server.on("/off", HTTP_GET, + [](AsyncWebServerRequest *request) + { + digitalWrite(ledPin, LOW); + request->send(LittleFS, "/index.html", "text/html", false, + processor); + });*/ + server.begin(); +} + +void serverStart() +{ + if (initWiFi()) + { + Serial.println("Setting Secure WIFI (Access Point)"); + serverServer(); + } + else + { + // Connect to Wi-Fi network with default SSID and password + Serial.println("Setting AP (Access Point)"); + // NULL sets an open Access Point + WiFi.softAP("LoraSA", NULL); + + IPAddress IP = WiFi.softAPIP(); + Serial.print("AP IP address: "); + Serial.println(IP); + + serverServer(); + } +} diff --git a/include/global_config.h b/include/global_config.h index d169fff..a2014f7 100644 --- a/include/global_config.h +++ b/include/global_config.h @@ -1,28 +1,50 @@ #ifndef __GLOBAL_CONFIG_H__ #define __GLOBAL_CONFIG_H__ +#include "utilities.h" + +#ifndef FREQ_BEGIN // frequency range in MHz to scan #define FREQ_BEGIN 850 +#endif + +#ifndef FREQ_END // TODO: if % RANGE_PER_PAGE != 0 #define FREQ_END 950 +#endif // Measurement bandwidth. Allowed bandwidth values (in kHz) are: // 4.8, 5.8, 7.3, 9.7, 11.7, 14.6, 19.5, 23.4, 29.3, 39.0, 46.9, 58.6, // 78.2, 93.8, 117.3, 156.2, 187.2, 234.3, 312.0, 373.6 and 467.0 #define BANDWIDTH 467.0 +#define BANDWIDTH_SX1280 406. // Detection level from the 33 levels. The higher number is more sensitive #define DEFAULT_DRONE_DETECTION_LEVEL 18 #define BUZZER_PIN 41 +#ifdef LILYGO +#define BUZZER_PIN 45 +#endif +#ifdef T3_V1_6_SX1276 +#define BUZZER_PIN 35 +#endif + // REB trigger PIN #define REB_PIN 42 +#ifdef T3_V1_6_SX1276 +#define REB_PIN 35 +#endif #define WATERFALL_ENABLED true #define WATERFALL_START 37 #ifdef LILYGO -#define LED 46 +#define LED BOARD_LED #endif // end not LILYGO +#ifdef T3_V1_6_SX1276 +#define LED BOARD_LED +#endif + #endif diff --git a/include/ui.h b/include/ui.h index 2ca7525..ddf7a73 100644 --- a/include/ui.h +++ b/include/ui.h @@ -8,6 +8,9 @@ #include #endif +#include +#include + // #include // (optional) major and minor tick-marks at x MHz @@ -31,13 +34,21 @@ #define SCREEN_HEIGHT 64 // ???? not used -// publish functions -#ifdef Vision_Master_E290 -extern void UI_Init(DEPG0290BxS800FxX_BW *); -#else -extern void UI_Init(SSD1306Wire *); -#endif -extern void UI_displayDecorate(int, int, bool); -extern void UI_setLedFlag(bool); +extern void UI_Init(Display_t *); extern void UI_clearPlotter(void); +extern void UI_clearTopStatus(void); extern void UI_drawCursor(int16_t); + +struct StatusBar : Chart +{ + Scan &r; + bool ui_initialized; + uint16_t scan_progress_count; + + StatusBar(Display_t &d, uint16_t x, uint16_t y, uint16_t w, Scan &r) + : Chart(d, x, y, w, LABEL_HEIGHT), r(r), ui_initialized(false), + scan_progress_count(0) {}; + + virtual void clearStatus(); + virtual void draw() override; +}; diff --git a/lib/charts/BarChart.cpp b/lib/charts/BarChart.cpp new file mode 100644 index 0000000..305cdbd --- /dev/null +++ b/lib/charts/BarChart.cpp @@ -0,0 +1,194 @@ +#include "charts.h" + +void BarChart::reset(uint16_t x, uint16_t y, uint16_t w, uint16_t h) +{ + if (w != width) + { + delete[] ys; + delete[] changed; + + ys = new float[w]; + changed = new bool[w]; + } + + memset(ys, 0, w * sizeof(float)); + memset(changed, false, w * sizeof(bool)); + redraw_all = true; + + Chart::reset(x, y, w, h); +} + +int BarChart::updatePoint(float x, float y) +{ + if (x < min_x || x >= max_x) + { + return -1; + } + + size_t idx = width * (x - min_x) / (max_x - min_x); + if (idx >= width) + { + idx = width - 1; + } + + if (!changed[idx] || ys[idx] < y) + { + ys[idx] = y; + changed[idx] = true; + } + + return idx; +} + +void BarChart::draw() +{ + for (int x = 0; x < width; x++) + { + if (!changed[x] && !redraw_all) + continue; + + drawOne(x); + } + + redraw_all = false; +} + +void BarChart::drawOne(int x) +{ + if (x < 0) + return; + + int y = y2pos(ys[x]); + + if (y < height) + { + display.setColor(BLACK); + display.drawVerticalLine(pos_x + x, pos_y, y); + display.setColor(WHITE); + display.drawVerticalLine(pos_x + x, pos_y + y, height - y); + } + else + { + display.setColor(BLACK); + display.drawVerticalLine(pos_x + x, pos_y, height); + } + + if (x % 2 == 0) + { + display.setColor(INVERSE); + display.setPixel(pos_x + x, pos_y + y2pos(level_y)); + } + + changed[x] = false; +} + +int BarChart::x2pos(float x) +{ + if (x < min_x) + x = min_x; + if (x > max_x) + x = max_x; + + return width * (x - min_x) / (max_x - min_x); +} + +int BarChart::y2pos(float y) +{ + if (y < min_y) + y = min_y; + if (y > max_y) + y = max_y; + + return height - height * (y - min_y) / (max_y - min_y); +} + +void BarChart::onEvent(Event &e) +{ + if (e.type != DETECTED) + { + return; + } + + level_y = e.emitter.trigger_level; + + int u = updatePoint(e.emitter.current_frequency, e.detected.rssi); + if (e.emitter.animated) + { + drawOne(u); + } +} + +void DecoratedBarChart::reset(uint16_t x, uint16_t y, uint16_t w, uint16_t h) +{ + Chart::reset(x, y, w, h); + bar.reset(x, y + LABEL_HEIGHT, w, h - LABEL_HEIGHT - AXIS_HEIGHT); +} + +void DecoratedBarChart::draw() +{ + bool draw_axis = bar.redraw_all; + + bar.draw(); + + display.setColor(BLACK); + display.fillRect(pos_x, pos_y, width, bar.pos_y - pos_y); + + display.setColor(WHITE); + display.setTextAlignment(TEXT_ALIGN_LEFT); + + uint16_t first_untouched = 0; + + for (uint16_t x = 0; x < width; x++) + { + float y = bar.ys[x]; + if (y >= bar.level_y) + { + String s = String(bar.ys[x], 0); + uint16_t w = display.getStringWidth(s); + uint16_t x1 = x; + for (; x < x1 + w && x < width; x++) + { + if (bar.ys[x] > y) + { + y = bar.ys[x]; + s = String(y, 0); + w = max(w, display.getStringWidth(s)); + } + } + + if (x > width && first_untouched <= width - w) + { + x1 = width - w; + } + + first_untouched = x; + + if (x1 + w <= width) + display.drawString(pos_x + x1, pos_y, s); + } + } + + if (draw_axis) + { + display.setColor(WHITE); + + uint16_t y = pos_y + height - AXIS_HEIGHT + 2; + display.fillRect(pos_x, y - 1, width, X_AXIS_WEIGHT); + + // Start and end ticks + display.fillRect(pos_x, y - 1, 2, AXIS_HEIGHT); + display.fillRect(pos_x + width - 2, y - 1, 2, AXIS_HEIGHT); + + for (float step = 0; bar.min_x + step * MAJOR_TICKS < bar.max_x; step += 1) + { + int tick_pos = bar.x2pos(bar.min_x + step * MAJOR_TICKS); + display.drawVerticalLine(pos_x + tick_pos, y, MAJOR_TICK_LENGTH); + } + + for (float step = 0; bar.min_x + step * MINOR_TICKS < bar.max_x; step += 1) + { + int tick_pos = bar.x2pos(bar.min_x + step * MINOR_TICKS); + display.drawVerticalLine(pos_x + tick_pos, y, MINOR_TICK_LENGTH); + } + } +} diff --git a/lib/charts/StackedChart.cpp b/lib/charts/StackedChart.cpp new file mode 100644 index 0000000..2c6ca73 --- /dev/null +++ b/lib/charts/StackedChart.cpp @@ -0,0 +1,95 @@ +#include "charts.h" + +uint16_t trim_w(uint16_t pos, uint16_t width, uint16_t w) +{ + return min(width, (uint16_t)(max(w, pos) - pos)); +} + +size_t StackedChart::addChart(Chart *c) +{ + Chart **cc = new Chart *[charts_sz + 1]; + memcpy(cc, charts, charts_sz * sizeof(Chart *)); + cc[charts_sz] = c; + free(charts); + + c->reset(pos_x + c->pos_x, pos_y + c->pos_y, trim_w(c->pos_x, c->width, width), + c->height); + charts = cc; + return charts_sz++; +} + +uint16_t StackedChart::setHeight(size_t c, uint16_t h) +{ + if (h < height) + { + charts[c]->reset(charts[c]->pos_x, charts[c]->pos_y, charts[c]->width, h); + + uint16_t used_space = 0; + for (int i = 0; i < charts_sz; i++) + { + used_space += charts[i]->height; + } + + return used_space; + } + // this chart gets special treatment - pack all other charts, + // and make this one as big as possible + uint16_t used_space = 0; + for (int i = 0; i < c; i++) + { + charts[i]->reset(charts[i]->pos_x, pos_y + used_space, charts[i]->width, + charts[i]->height); + used_space += charts[i]->height; + } + + uint16_t more_used_space = used_space; + for (int i = c + 1; i < charts_sz; i++) + { + more_used_space += charts[i]->height; + } + + if (more_used_space < height) + { + charts[c]->reset(charts[c]->pos_x, pos_y + used_space, charts[c]->width, + height - more_used_space); + used_space += charts[c]->height; + } + + for (int i = c + 1; i < charts_sz; i++) + { + charts[i]->reset(charts[i]->pos_x, pos_y + used_space, charts[i]->width, + charts[i]->height); + used_space += charts[i]->height; + } + + return used_space; +} + +void StackedChart::reset(uint16_t x, uint16_t y, uint16_t w, uint16_t h) +{ + for (int i = 0; i < charts_sz; i++) + { + uint16_t rel_x = charts[i]->pos_x - pos_x; + uint16_t rel_y = charts[i]->pos_y - pos_y; + charts[i]->reset(x + rel_x, y + rel_y, trim_w(rel_x, charts[i]->width, w), + charts[i]->height); + } + + Chart::reset(x, y, w, h); +} + +void StackedChart::draw() +{ + for (int i = 0; i < charts_sz; i++) + charts[i]->draw(); +} + +void StackedChart::onEvent(Event &e) +{ + if (e.type != SCAN_TASK_COMPLETE) + { + return; + } + + draw(); +} diff --git a/lib/charts/UptimeClock.cpp b/lib/charts/UptimeClock.cpp new file mode 100644 index 0000000..f7e229e --- /dev/null +++ b/lib/charts/UptimeClock.cpp @@ -0,0 +1,28 @@ +#include "charts.h" + +void UptimeClock::draw(uint64_t t) +{ + t1 = t; + draw(); +} + +void UptimeClock::draw() +{ + uint64_t uptime = t1 - t0; + int mils = uptime % 1000; + int seconds = (uptime / 1000) % 60; + int minutes = (uptime / 60000) % 60; + int hours = uptime / 3600000; + String s = String(hours) + (minutes < 10 ? ":0" : ":") + String(minutes) + + (seconds < 10 ? ":0" : ":") + String(seconds) + + (mils < 10 ? ".00" + : mils < 100 ? ".0" + : ".") + + String(mils); + int w = display.getStringWidth(s); + display.setColor(BLACK); + display.fillRect((display.width() - w) / 2, display.height() / 2 - 3, w, 7); + display.setColor(WHITE); + display.setTextAlignment(TEXT_ALIGN_CENTER_BOTH); + display.drawString(display.width() / 2, display.height() / 2, s); +} diff --git a/lib/charts/WaterfallChart.cpp b/lib/charts/WaterfallChart.cpp new file mode 100644 index 0000000..f3b6553 --- /dev/null +++ b/lib/charts/WaterfallChart.cpp @@ -0,0 +1,69 @@ +#include "charts.h" +#include + +void WaterfallChart::reset(uint16_t x, uint16_t y, uint16_t w, uint16_t h) +{ + Chart::reset(x, y, w, h); + + model->reset(model->times[0], w); + + update_to = model->buckets; +} + +void WaterfallChart::updatePoint(uint64_t t, float x, float y) +{ + if (x < min_x || x >= max_x) + { + return; + } + + update_to = max(update_to, model->updateModel(t, x2pos(x), y >= level_y)); +} + +void WaterfallChart::draw() +{ + size_t h = min(update_to, (size_t)height); + + for (int y = 0; y < h; y++) + { + for (int x = 0; x < width; x++) + { + bool b = model->counts[y][x] > 0 && + (model->events[y][x] >= model->counts[y][x] * threshold); + if (b) + { + display.setColor(WHITE); + } + else + { + display.setColor(BLACK); + } + + display.setPixel(pos_x + x, pos_y + y); + } + } + + update_to = 0; +} + +int WaterfallChart::x2pos(float x) +{ + if (x < min_x) + x = min_x; + if (x > max_x) + x = max_x; + + return width * (x - min_x) / (max_x - min_x); +} + +void WaterfallChart::onEvent(Event &e) +{ + if (e.type != DETECTED) + { + return; + } + + level_y = e.emitter.trigger_level; + + updatePoint(e.time_ms, e.detected.freq, e.detected.rssi); +} diff --git a/lib/charts/charts.h b/lib/charts/charts.h new file mode 100644 index 0000000..d159cc0 --- /dev/null +++ b/lib/charts/charts.h @@ -0,0 +1,178 @@ +#ifndef CHARTS_H +#define CHARTS_H + +#ifdef Vision_Master_E290 +#include "HT_DEPG0290BxS800FxX_BW.h" +typedef DEPG0290BxS800FxX_BW Display_t; +#else +#include +typedef OLEDDisplay Display_t; +#endif + +#include +#include +#include +#include + +struct Chart +{ + uint16_t pos_x, pos_y; + uint16_t width, height; + Display_t &display; + + Chart(Display_t &d, uint16_t x, uint16_t y, uint16_t w, uint16_t h) + : display(d), pos_x(x), pos_y(y), width(w), height(h) {}; + + /* + * This method resets the state and sets the reference time. + */ + virtual void reset(uint16_t x, uint16_t y, uint16_t w, uint16_t h) + { + pos_x = x; + pos_y = y; + width = w; + height = h; + } + + /* + * Redraw everything that needs redrawing. + */ + virtual void draw() {}; +}; + +/* + * ProgressChart supports updates with progressive redraw of just the affected area. + */ +struct ProgressChart : Chart +{ + ProgressChart(Display_t &d, uint16_t x, uint16_t y, uint16_t w, uint16_t h) + : Chart(d, x, y, w, h) {}; + + /* + * Update one data point, and return what column needs redrawing. + */ + virtual int updatePoint(float x, float y) = 0; + + /* + * If you fancy animated progress, then pass the output of updatePoint to here. + */ + virtual void drawOne(int x) = 0; +}; + +struct BarChart : ProgressChart, Listener +{ + float min_x, max_x, min_y, max_y; + float level_y; + + float *ys; + bool *changed; + bool redraw_all; + + BarChart(Display_t &d, uint16_t x, uint16_t y, uint16_t w, uint16_t h, float min_x, + float max_x, float min_y, float max_y, float level_y) + : ProgressChart(d, x, y, w, h), min_x(min_x), max_x(max_x), min_y(min_y), + max_y(max_y), level_y(level_y), redraw_all(true) + { + ys = new float[w]; + changed = new bool[w]; + + memset(ys, 0, w * sizeof(float)); + memset(changed, 0, w * sizeof(bool)); + }; + + void reset(uint16_t x, uint16_t y, uint16_t w, uint16_t h) override; + int updatePoint(float x, float y) override; + void drawOne(int x) override; + void draw() override; + void onEvent(Event &) override; + + int x2pos(float x); + int y2pos(float y); +}; + +#define LABEL_HEIGHT 7 +#define X_AXIS_WEIGHT 1 +#define MAJOR_TICK_LENGTH 2 +#define MAJOR_TICKS 10 +#define MINOR_TICK_LENGTH 1 +#define MINOR_TICKS 5 +#define AXIS_HEIGHT (X_AXIS_WEIGHT + MAJOR_TICK_LENGTH + 2) +struct DecoratedBarChart : Chart +{ + BarChart bar; + + DecoratedBarChart(Display_t &d, uint16_t x, uint16_t y, uint16_t w, uint16_t h, + float min_x, float max_x, float min_y, float max_y, float level_y) + : Chart(d, x, y, w, h), + bar(d, x, y + LABEL_HEIGHT, w, h - LABEL_HEIGHT - AXIS_HEIGHT, min_x, max_x, + min_y, max_y, level_y) {}; + + void reset(uint16_t x, uint16_t y, uint16_t w, uint16_t h) override; + void draw() override; +}; + +struct StackedChart : Chart, Listener +{ + Chart **charts; + size_t charts_sz; + + StackedChart(Display_t &d, uint16_t x, uint16_t y, uint16_t w, uint16_t h) + : Chart(d, x, y, w, h), charts(NULL), charts_sz(0) {}; + + /* + * addChart adds c to the StackedChart, treats pos_x and pos_y of the chart + * as relative to this chart's origin, and trims width to fit. Adjust the + * height and pack charts using setHeight. + */ + size_t addChart(Chart *c); + + /* + * Adjust the height of the chart and return the resulting required height. + * If h is >= height, the chart gets a special treatment: packs all other + * charts, and uses up the rest of space. + */ + uint16_t setHeight(size_t c, uint16_t h); + + void reset(uint16_t x, uint16_t y, uint16_t w, uint16_t h) override; + + void draw() override; + + void onEvent(Event &e) override; +}; + +struct WaterfallChart : Chart, Listener +{ + float min_x, max_x; + float level_y, threshold; + + size_t update_to; + + WaterfallModel *model; + + WaterfallChart(Display_t &d, uint16_t x, uint16_t y, uint16_t w, uint16_t h, + float min_x, float max_x, float level_y, float threshold, + WaterfallModel *m) + : Chart(d, x, y, w, h), model(m), min_x(min_x), max_x(max_x), level_y(level_y), + threshold(threshold), update_to(m->buckets) {}; + + void updatePoint(uint64_t t, float x, float y); + + void reset(uint16_t x, uint16_t y, uint16_t w, uint16_t h) override; + + void draw() override; + void onEvent(Event &e) override; + + int x2pos(float x); +}; + +struct UptimeClock : Chart +{ + uint64_t t0; + uint64_t t1; + UptimeClock(Display_t &d, uint64_t t0) : Chart(d, 0, 0, 0, 0), t0(t0), t1(t0) {}; + + void draw(uint64_t t); + virtual void draw() override; +}; + +#endif diff --git a/lib/events/events.h b/lib/events/events.h new file mode 100644 index 0000000..329dea5 --- /dev/null +++ b/lib/events/events.h @@ -0,0 +1,46 @@ +#ifndef LORASA_EVENTS_H +#define LORASA_EVENTS_H + +struct Event; +enum EventType +{ + ALL_EVENTS = 0, // used only at registration time + DETECTED, + SCAN_TASK_COMPLETE, + _MAX_EVENT_TYPE = SCAN_TASK_COMPLETE // unused as event type +}; +struct Listener; + +#include +#include + +struct Event +{ + EventType type; + uint64_t epoch; + uint64_t time_ms; + + Scan &emitter; + + union + { + struct + { + float rssi; + float freq; + bool trigger; + bool detected; + size_t detected_at; + } detected; + }; + + Event(Scan &emitter, EventType type, uint64_t time_ms) + : emitter(emitter), type(type), epoch(emitter.epoch), time_ms(time_ms) {}; +}; + +struct Listener +{ + virtual void onEvent(Event &event) = 0; +}; + +#endif diff --git a/include/LoRaBoards.cpp b/lib/loraboards/LoRaBoards.cpp similarity index 99% rename from include/LoRaBoards.cpp rename to lib/loraboards/LoRaBoards.cpp index 04c93a6..0331489 100644 --- a/include/LoRaBoards.cpp +++ b/lib/loraboards/LoRaBoards.cpp @@ -8,6 +8,8 @@ * */ +#ifdef LILYGO + #include "LoRaBoards.h" #if defined(HAS_SDCARD) @@ -927,3 +929,4 @@ bool beginGPS() return result; } #endif +#endif // #ifdef LILYGO diff --git a/include/LoRaBoards.h b/lib/loraboards/LoRaBoards.h similarity index 100% rename from include/LoRaBoards.h rename to lib/loraboards/LoRaBoards.h diff --git a/include/utilities.h b/lib/loraboards/utilities.h similarity index 99% rename from include/utilities.h rename to lib/loraboards/utilities.h index 1d1b3ef..32a804b 100644 --- a/include/utilities.h +++ b/lib/loraboards/utilities.h @@ -486,6 +486,8 @@ #define USING_DIO2_AS_RF_SWITCH +#elif defined(HELTEC) +// just to prevent error #elif defined(T_BEAM_S3_BPF) #ifndef USING_SX1278 diff --git a/lib/models/WaterfallModel.cpp b/lib/models/WaterfallModel.cpp new file mode 100644 index 0000000..99f7816 --- /dev/null +++ b/lib/models/WaterfallModel.cpp @@ -0,0 +1,153 @@ +#include "models.h" +#include + +WaterfallModel::WaterfallModel(size_t w, uint64_t base_dt, size_t m_sz, + const size_t *multiples) +{ + width = w; + + size_t dt_sz = 0; + for (int i = 0; i < m_sz; i++) + dt_sz += multiples[i]; + + buckets = dt_sz; + dt = new uint64_t[buckets]; + events = new uint32_t *[buckets]; + counts = new uint32_t *[buckets]; + times = new uint64_t[buckets]; + + uint64_t m = base_dt; + for (int i = 0, j = 0; i < m_sz; i++) + { + for (int k = 0; k < multiples[i]; k++, j++) + { + dt[j] = m; + + events[j] = new uint32_t[width]; + counts[j] = new uint32_t[width]; + } + + m *= multiples[i]; + } +} + +void WaterfallModel::reset(uint64_t t0, size_t w) +{ + if (w != width) + { + width = w; + for (int i = 0; i < buckets; i++) + { + delete[] counts[i]; + delete[] events[i]; + + counts[i] = new uint32_t[w]; + events[i] = new uint32_t[w]; + } + } + + for (int i = 0; i < buckets; i++) + { + memset(counts[i], 0, width * sizeof(uint32_t)); + memset(events[i], 0, width * sizeof(uint32_t)); + times[i] = t0 + dt[i]; + } +} + +/* + * The model is literally a stack of counters: + * - incomplete second + * - n complete seconds + * - incomplete minute + * - n complete minutes + * - ... + * + * updateModel updates incomplete second. When the second becomes complete, it + * gets pushed to complete seconds, and the last complete second is rotated out + * and it gets added to incomplete minute. This gets repeated for incomplete + * minutes, etc. + */ +size_t WaterfallModel::updateModel(uint16_t t, size_t x, uint16_t y) +{ + size_t changed = 1; + + while (t > times[0]) + { + changed = push(); + } + + counts[0][x]++; + events[0][x] += y; + return changed; +} + +size_t WaterfallModel::push() +{ + size_t i = 1; + for (; i < buckets; i++) + { + if (dt[i - 1] == dt[i]) + continue; + if (times[i - 1] <= times[i]) + break; + times[i - 1] = times[i] + dt[i]; + } + + uint64_t t0 = times[0]; + + uint32_t *cc = counts[i - 1]; + uint32_t *ee = events[i - 1]; + memmove(times + 1, times, (i - 1) * sizeof(uint64_t)); + memmove(counts + 1, counts, (i - 1) * sizeof(uint32_t *)); + memmove(events + 1, events, (i - 1) * sizeof(uint32_t *)); + + if (i < buckets) + { + for (int j = 0; j < width; j++) + { + counts[i][j] += cc[j]; + events[i][j] += ee[j]; + } + + i++; + } + + memset(cc, 0, width * sizeof(uint32_t)); + memset(ee, 0, width * sizeof(uint32_t)); + counts[0] = cc; + events[0] = ee; + times[0] = t0 + dt[0]; + + return i; +} + +#ifdef TO_STRING + +#include +#include + +#endif + +char *WaterfallModel::toString() +{ +#ifdef TO_STRING + std::stringstream r; + r << "w:" << width << " b:" << buckets << " ["; + + for (int i = 0; i < buckets; i++) + { + r << "dt:" << dt[i] << " t:" << times[i] << " ["; + for (int j = 0; j < width; j++) + r << " c:" << counts[i][j] << " e:" << events[i][j]; + r << " ]"; + } + + r << " ]"; + + char *ret = new char[r.str().length() + 1]; + strncpy(ret, r.str().c_str(), r.str().length()); +#else + char *ret = NULL; +#endif + return ret; +} diff --git a/lib/models/models.h b/lib/models/models.h new file mode 100644 index 0000000..ce8de8f --- /dev/null +++ b/lib/models/models.h @@ -0,0 +1,27 @@ +#ifndef CHARTS_MODELS_H +#define CHARTS_MODELS_H + +#include +#include +#include + +struct WaterfallModel +{ + uint32_t **events; + uint32_t **counts; + uint64_t *times; + uint64_t *dt; + + size_t buckets; + size_t width; + + WaterfallModel(size_t w, uint64_t base_dt, size_t m_sz, const size_t *multiples); + + void reset(uint64_t t0, size_t width); + size_t updateModel(uint16_t t, size_t x, uint16_t y); + size_t push(); + + char *toString(); +}; + +#endif diff --git a/lib/scan/scan.cpp b/lib/scan/scan.cpp new file mode 100644 index 0000000..89ff530 --- /dev/null +++ b/lib/scan/scan.cpp @@ -0,0 +1,189 @@ +#ifndef LORASA_CORE_CPP +#define LORASA_CORE_CPP + +#include "scan.h" +#include +#include +#include + +uint16_t Scan::rssiMethod(size_t samples, uint16_t *result, size_t res_size) +{ + float scale((float)res_size / (HI_RSSI_THRESHOLD - LO_RSSI_THRESHOLD + 0.1)); + + memset(result, 0, res_size * sizeof(uint16_t)); + int result_index = 0; + + // + uint16_t max_signal = 65535; + // N of samples + for (int r = 0; r < samples; r++) + { + float rssi = getRSSI(); + if (rssi < -65535) + rssi = -65535; + + uint16_t abs_rssi = abs(rssi); + if (abs_rssi < max_signal) + { + max_signal = abs_rssi; + } + // ToDO: check if 4 is correct value for 33 power bins + // Now we have more space because we are ignoring low dB values + // we can / 3 default 4 + if (RSSI_OUTPUT_FORMULA == 1) + { + result_index = + /// still not clear formula but it works + uint8_t(abs(rssi) / 4); + } + else if (RSSI_OUTPUT_FORMULA == 2) + { + if (rssi > HI_RSSI_THRESHOLD) + { + rssi = HI_RSSI_THRESHOLD; + } + else if (rssi < LO_RSSI_THRESHOLD) + { + rssi = LO_RSSI_THRESHOLD; + } + + result_index = uint8_t((HI_RSSI_THRESHOLD - rssi) * scale); + } + + if (result_index >= res_size) + { + // Maximum index possible + result_index = res_size - 1; + } + + LOG("RSSI: %f IDX: %d\n", rssi, result_index); + if (result[result_index] == 0 || result[result_index] > abs_rssi) + { + result[result_index] = abs_rssi; + } + } + + return max_signal; +} + +Event Scan::detect(uint16_t *result, bool *filtered_result, size_t result_size, + int samples) +{ + size_t max_rssi_x = result_size; + + for (int y = 0; y < result_size; y++) + { + + LOG("%i:%i,", y, result[y]); +#if !defined(FILTER_SPECTRUM_RESULTS) || FILTER_SPECTRUM_RESULTS == false + if (result[y] && result[y] != 0) + { + filtered_result[y] = 1; + } + else + { + filtered_result[y] = 0; + } +#endif + +// if samples low ~1 filter removes all values +#if FILTER_SPECTRUM_RESULTS + + filtered_result[y] = 0; + // Filter Elements without neighbors + // if RSSI method actual value is -xxx dB + if (result[y] > 0 && samples > 1) + { + // do not process 'first' and 'last' row to avoid out of index + // access. + if ((y > 0) && (y < (result_size - 2))) + { + if (((result[y + 1] != 0) && (result[y + 2] != 0)) || + (result[y - 1] != 0)) + { + filtered_result[y] = 1; + // Fill empty pixel + result[y + 1] = 1; + } + else + { + LOG("Filtered::%i,", y); + } + } + } // not filtering if samples == 1 because it will be filtered + else if (result[y] > 0 && samples == 1) + { + filtered_result[y] = 1; + } +#endif + if (filtered_result[y] && max_rssi_x > y) + { + max_rssi_x = y; + } + } + + Event event(*this, EventType::DETECTED, 0); + event.epoch = epoch; + event.detected.detected = max_rssi_x < result_size; + event.detected.freq = current_frequency; + event.detected.rssi = + event.detected.detected ? -(float)result[max_rssi_x] : LO_RSSI_THRESHOLD; + event.detected.detected_at = max_rssi_x; + event.detected.trigger = + event.detected.detected && event.detected.rssi >= trigger_level; + detection_count++; + + return event; +} + +size_t Scan::addEventListener(EventType t, Listener &l) +{ + size_t c = listener_count[(size_t)t]; + Listener **new_list = new Listener *[c + 1]; + new_list[c] = &l; + listener_count[(size_t)t] = c + 1; + + if (c > 0) + { + Listener **old_list = eventListeners[(size_t)t]; + memcpy(new_list, old_list, c * sizeof(Listener *)); + delete[] old_list; + } + + eventListeners[(size_t)t] = new_list; + return c; +} + +struct CallbackFunction : Listener +{ + void (*cb)(void *arg, Event &e); + void *arg; + + CallbackFunction(void cb(void *arg, Event &e), void *arg) : cb(cb), arg(arg) {} + + void onEvent(Event &e) { cb(arg, e); } +}; + +size_t Scan::addEventListener(EventType t, void cb(void *arg, Event &e), void *arg) +{ + return addEventListener(t, *(new CallbackFunction(cb, arg))); +} + +void Scan::fireEvent(Event &event) +{ + Listener **list = eventListeners[(size_t)event.type]; + size_t c = listener_count[(size_t)event.type]; + for (int i = 0; i < c; i++) + { + list[i]->onEvent(event); + } + + list = eventListeners[(size_t)EventType::ALL_EVENTS]; + c = listener_count[(size_t)EventType::ALL_EVENTS]; + for (int i = 0; i < c; i++) + { + list[i]->onEvent(event); + } +} + +#endif diff --git a/lib/scan/scan.h b/lib/scan/scan.h new file mode 100644 index 0000000..38b610e --- /dev/null +++ b/lib/scan/scan.h @@ -0,0 +1,81 @@ +#include +#include +#include + +#ifndef LORASA_CORE_H + +#define LORASA_CORE_H + +#ifdef PRINT_DEBUG +#define LOG(args...) Serial.printf(args...) +#define LOG_IF(cond, args...) \ + if (cond) \ + LOG(args...) +#elif !defined(LOG) +#define LOG(args...) +#define LOG_IF(cond, args...) +#endif + +// Output Pixel Formula +// 1 = rssi / 4, 2 = (rssi / 2) - 22 or 20 +constexpr int RSSI_OUTPUT_FORMULA = 2; + +// based on the formula for RSSI_OUTPUT_FORMULA == 2 +// -2 * (22 + RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE) < rssi =< -44 +// practice may require a better pair of thresholds +constexpr float HI_RSSI_THRESHOLD = -44.0; +constexpr float LO_RSSI_THRESHOLD = HI_RSSI_THRESHOLD - 66; + +// number of samples for RSSI method +#ifndef SAMPLES_RSSI +#define SAMPLES_RSSI 13 // 21 // +#endif +#ifdef USING_SX1280PA +#define SAMPLES_RSSI 20 +#endif + +struct Scan +{ + uint64_t epoch; + float current_frequency; + uint64_t fr_begin; + uint64_t fr_end; + uint64_t drone_detection_level; + bool sound_on; + bool led_flag; + uint64_t detection_count; + bool animated; + float trigger_level; + + Listener **eventListeners[(size_t)EventType::_MAX_EVENT_TYPE + 1]; + size_t listener_count[(size_t)EventType::_MAX_EVENT_TYPE + 1]; + + Scan() + : epoch(0), current_frequency(0), fr_begin(0), fr_end(0), + drone_detection_level(0), sound_on(false), led_flag(false), detection_count(0), + animated(false), trigger_level(0), listener_count{ + 0, + } {}; + + virtual float getRSSI() = 0; + + // rssiMethod gets the data similar to the scan method, + // but uses getRSSI directly. + uint16_t rssiMethod(size_t samples, uint16_t *result, size_t res_size); + + // detect method analyses result, and produces filtered_result, marking + // those values that represent a detection event. + // It returns index that represents strongest signal at which a detection event + // occurred. + Event detect(uint16_t *result, bool *filtered_result, size_t result_size, + int samples); + + size_t addEventListener(EventType t, Listener &l); + size_t addEventListener(EventType t, void cb(void *, Event &), void *arg); + void fireEvent(Event &e); +}; + +// Remove reading without neighbors +#define FILTER_SPECTRUM_RESULTS true + +#endif diff --git a/ml_research/Diagram_ML.svg b/ml_research/Diagram_ML.svg new file mode 100644 index 0000000..f96baa4 --- /dev/null +++ b/ml_research/Diagram_ML.svg @@ -0,0 +1,4 @@ + + + +
Device produces a scan of N samples in Frequency F and loops over the entire Frequency band

Device cannot produce I/Q data
Device produces a sc...
Datasets contain I/Q data or Spectrograms (that can be built out of I/Q data)
Datasets contain I/Q...
SAME FORMAT











SAME FORMAT...
Generation of pseudo spectrogram with frequency bins and time length similar to train dataset
Generation of pseudo...
Spectrograms are cut so that the shown frequencies and time length is congruent with device data
Spectrograms are cut...
Train data
Convert to TF-Lite
(Model: MobileNetV3?)
Train data...
Prediction on device using TF-Lite library and saved model weights
Prediction on device...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/ml_research/datasets.md b/ml_research/datasets.md new file mode 100644 index 0000000..ebf796a --- /dev/null +++ b/ml_research/datasets.md @@ -0,0 +1,13 @@ +## Overview of ML Dataets + +| dataset name | data content | data format | frequencies / Sampling rate | other infos | source paper/website | source data | open questions | +|----------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------| +| Noisy Drone RF Signal Classification (Glüge et al.) | Drones: DJI, FutabaT14, FutabaT7, Graupner, Taranis, Turnigy | I/Q Data as well as generated Spectrograms available | The device can scan a range of 6Ghz. But the newest plot is very weird. +/- 7 Mhz, can this really be? "non-overlapping signal vectors of length of 1048576 samples, which corresponds to approx. 74.9ms at 14MHz" | mixed with either Labnoise (50%) or Gaussian noise (50%). The noise class was created by mixing Labnoise and Gaussian noise in all possible combinations. Several levels of SNR were used over the entire dataset. | https://www.scitepress.org/Link.aspx?doi=10.5220/0012176800003595 https://github.com/sgluege/Noisy-Drone-RF-Signal-Classification https://github.com/sgluege/Noisy-Drone-RF-Signal-Classification-v2/tree/main | https://www.kaggle.com/datasets/sgluege/noisy-drone-rf-signal-classification-v2/data https://www.kaggle.com/datasets/sgluege/noisy-drone-rf-signal-classification | Not sure about the frequencies. | +| DroneDataset (Swinney and Woods) | Drones: new DJI Mavic 2 Air S, DJI Mavic Pro, DJI Mavic Pro 2, DJI Inspire 2, DJI Mavic Mini, DJI Phantom 4 and the Parrot Disco. | Raw I/Q Data | TODO; Recordings were collected using a Nuand BladeRF SDR and using open source software GNURadio | There are 4 subsets of data included in this dataset, the UAS signals in the presence of Bluetooth interference, in the presence of Wi-Fi signals, in the presence of both and with no interference. 3 flight modes are captured - switched on, hovering and flying. | No paper seen | https://ieee-dataport.org/open-access/dronedetect-dataset-radio-frequency-dataset-unmanned-aerial-system-uas-signals-machine | Sampling rate? | +| DroneRF (Allahham et al.) | Drones: Bepop; AR; Phantom | "the dataset contains only time series data, and not the complex IQ signals" (From Glüge and not from the authors) - but what does this exactly mean? | capture the whole 2.4GHz bandwidth, we have used 2 RF receivers. Each RF receiver has a maximum instantaneous bandwidth of 40 MHz, so both receivers must be operating simultaneously to at least capture a technology spectrum such as WiFi (i.e. 80 MHz). Recorded using universal soft- ware radio peripheral (USRP) software-defined radio (SDR) transceivers. Signals that could be considered noise in the 2.4 GHz band (Bluetooth, Wi-Fi) were not recorded. | modes, including off, on and connected, hovering, flying, and video recording. | https://www.sciencedirect.com/science/article/pii/S2352340919306675?ref=pdf_download&fr=RR-2&rr=8bf5de727fa35d7f | https://data.mendeley.com/datasets/f4c2b4n755/1 | Apparently time versus db? Not very clear what is in the data | +| Radio-Frequency Control and Video Signal Recordings of Drones (Vuorenmaa et al.) | Drones: DJI Inspire 2 (2.44 and 5.8 GHz), DJI Matrice 100 (2.44 GHz), DJI Matrice 210 (2.44 and 5.8 GHz), DJI Mavic Mini (2.44 GHz), DJI Mavic Pro (2.44 GHz), DJI Phantom 4 (2.44 GHz), DJI Phantom 4 Pro Plus (2.44 and 5.8 GHz), Parrot Disco (2.44 GHz), Parrot Mambo (2.44 GHz), Yuneec Typhoon H (2.44 and 5.7 GHz) | I/Q Data (And Video data) | TODO: not clear yet | | https://zenodo.org/records/4264467 | | | +| Spectrogram Dataset (Wicht et al.) | Wi-Fi and Bluetooth signals (NOT DRONES) | Sepctrograms | | | https://www.mdpi.com/2306-5729/7/12/168 | | | + + +## Current Proposal for ML +![Diagram ML](./Diagram_ML.svg) diff --git a/platformio.ini b/platformio.ini index 0b7648d..e85aa6e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -9,12 +9,15 @@ ; https://docs.platformio.org/page/projectconf.html [platformio] +default_envs = heltec_wifi_lora_32_V3 ; for env:vision-master-e190 ; src_dir = tft_src ; for env:vision-master-e290 ; src_dir = eink_src ; for env:heltec_wifi_lora_32_V3 ; src_dir = src ;;Default +; for heltec_wifi_lora_32_V3-test-signal-generator +; src_dir = trans_src [env:heltec_wifi_lora_32_V3] platform = espressif32 @@ -23,22 +26,49 @@ framework = arduino upload_speed = 921600 monitor_speed = 115200 board_build.f_cpu = 240000000 +board_build.filesystem = littlefs lib_deps = ropg/Heltec_ESP32_LoRa_v3@^0.9.1 - adafruit/Adafruit ST7735 and ST7789 Library@^1.10.4 -build_flags = -DHELTEC_POWER_BUTTON + bblanchon/ArduinoJson@^7.2.0 + ESP Async WebServer +build_flags = + -DHELTEC_POWER_BUTTON + -DHELTEC + -[env:lilygo-T3S3-v1-2] +[env:heltec_wifi_lora_32_V3_433] +platform = espressif32 +board = heltec_wifi_lora_32_V3 +framework = arduino +upload_speed = 921600 +monitor_speed = 115200 +board_build.f_cpu = 240000000 +board_build.filesystem = littlefs +lib_deps = + ropg/Heltec_ESP32_LoRa_v3@^0.9.1 + bblanchon/ArduinoJson@^7.2.0 + ESP Async WebServer +build_flags = + -DHELTEC_POWER_BUTTON + -DHELTEC + -DFREQ_BEGIN=130 + -DFREQ_END=180 + -DRADIOLIB_CHECK_PARAMS=0 + +[env:lilygo-T3S3-v1-2-sx1262] platform = espressif32 board = t3_s3_v1_x framework = arduino upload_speed = 921600 monitor_speed = 115200 board_build.f_cpu = 240000000 +board_build.filesystem = littlefs lib_deps = ropg/Heltec_ESP32_LoRa_v3@^0.9.1 RadioLib - adafruit/Adafruit ST7735 and ST7789 Library@^1.10.4 + U8g2 + XPowersLib + ESP Async WebServer build_flags = -DLILYGO -DT3_S3_V1_2_SX1262 @@ -51,6 +81,91 @@ build_flags = -DARDUINO_LILYGO_T3_S3_V1_X -DARDUINO_USB_MODE=1 +[env:lilygo-T3S3-v1-2-lr1121] +platform = espressif32 +board = t3_s3_v1_x +framework = arduino +upload_speed = 921600 +monitor_speed = 115200 +board_build.f_cpu = 240000000 +board_build.filesystem = littlefs +lib_deps = + ropg/Heltec_ESP32_LoRa_v3@^0.9.1 + RadioLib + U8g2 + XPowersLib + bblanchon/ArduinoJson@^7.2.0 + ESP Async WebServer +build_flags = + -DLILYGO + -DT3_S3_V1_2_LR1121 + -DT3_V1_3_SX1262 + -DARDUINO_LILYGO_T3S3_LR1121 + -DESP32 + -DSAMPLES_RSSI=5 + -DUSING_LR1121 + -DFREQ_BEGIN=2400 + -DFREQ_END=2500 + -DARDUINO_ARCH_ESP32 + -DARDUINO_USB_CDC_ON_BOOT=1 + -DARDUINO_LILYGO_T3_S3_V1_X + -DARDUINO_USB_MODE=1 + + +[env:lilygo-T3S3-v1-2-sx1280] +platform = espressif32 +board = t3_s3_v1_x +framework = arduino +upload_speed = 921600 +monitor_speed = 115200 +board_build.f_cpu = 240000000 +board_build.filesystem = littlefs +lib_deps = + ropg/Heltec_ESP32_LoRa_v3@^0.9.1 + RadioLib + U8g2 + XPowersLib + ESP Async WebServer +build_flags = + -DLILYGO + -DT3_S3_V1_2_SX1280_PA + -DARDUINO_LILYGO_T3S3_SX1280_PA + -DESP32 + -DUSING_SX1280PA + -DFREQ_BEGIN=2400 + -DFREQ_END=2500 + -DARDUINO_ARCH_ESP32 + -DARDUINO_USB_CDC_ON_BOOT=1 + -DARDUINO_LILYGO_T3_S3_V1_X + -DARDUINO_USB_MODE=1 + +[env:lilygo-T3-v1-6-xs1276] +platform = espressif32 +board = esp32dev +framework = arduino +upload_speed = 115200 +monitor_speed = 115200 +board_build.filesystem = littlefs +lib_deps = + ropg/Heltec_ESP32_LoRa_v3@^0.9.1 + RadioLib + U8g2 + XPowersLib + ESP Async WebServer +build_flags = + -DLILYGO + -DT3_V1_6_SX1276 + -DUSING_SX1276 + -DESP32 + -DARDUINO_ARCH_ESP32 + -DARDUINO_USB_CDC_ON_BOOT=0 ;; if not 0 - Serial issue + -DARDUINO_LILYGO_T3_V1_6 + -DARDUINO_USB_MODE=1 + +;; More old lylygo/titygo boeads defenitions you can find here: +;; https://github.com/PTR-projects/PTR_GroundStation_firmware/blob/main/platformio.ini +;; https://github.com/Xinyuan-LilyGO/LilyGo-LoRa-Series/blob/master/platformio.ini + [env:heltec_wifi_lora_32_V3-test-signal-generator] platform = espressif32 board = heltec_wifi_lora_32_V3 @@ -61,8 +176,9 @@ board_build.f_cpu = 240000000 board_build.flash_size = 80000000L lib_deps = ropg/Heltec_ESP32_LoRa_v3@^0.9.1 - adafruit/Adafruit ST7735 and ST7789 Library@^1.10.4 -build_flags = -DLILYGO +build_flags = + -DHELTEC + -DHELTEC_POWER_BUTTON [env:vision-master-e290] platform = espressif32 @@ -71,7 +187,8 @@ framework = arduino monitor_speed = 115200 monitor_filters = esp32_exception_decoder board_upload.use_1200bps_touch = true -build_flags = +build_flags = + -DHELTEC -DHELTEC_BOARD=37 -DSLOW_CLK_TPYE=1 -DARDUINO_USB_CDC_ON_BOOT=1 @@ -95,7 +212,6 @@ lib_deps = https://github.com/HelTecAutomation/Heltec_ESP32/ adafruit/Adafruit GFX Library@^1.11.10 ropg/Heltec_ESP32_LoRa_v3@^0.9.1 - adafruit/Adafruit ST7735 and ST7789 Library@^1.10.4 [env:vision-master-t190] platform = espressif32 @@ -105,6 +221,7 @@ monitor_speed = 115200 monitor_filters = esp32_exception_decoder board_upload.use_1200bps_touch = true build_flags = + -DHELTEC -DHELTEC_BOARD=38 -DSLOW_CLK_TPYE=1 -DARDUINO_USB_CDC_ON_BOOT=1 @@ -129,3 +246,6 @@ lib_deps = adafruit/Adafruit GFX Library@^1.11.10 ropg/Heltec_ESP32_LoRa_v3@^0.9.1 adafruit/Adafruit ST7735 and ST7789 Library@^1.10.4 + +[env:native] +platform = native diff --git a/src/main.cpp b/src/main.cpp index acfc13b..76803a8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -21,28 +21,58 @@ https://jgromes.github.io/RadioLib/ */ -// #define HELTEC_NO_DISPLAY +// #define HELTEC_NO_DISPLAY #include +#ifdef HELTEC +#include +#endif +#include "FS.h" +#include +#include +#include +#include +#include +#include -// #define OSD_ENABLED true -// #define WIFI_SCANNING_ENABLED true -// #define BT_SCANNING_ENABLED true +#include "WIFI_SERVER.h" + +#define FORMAT_LITTLEFS_IF_FAILED true + +// #define OSD_ENABLED true +// #define WIFI_SCANNING_ENABLED true +// #define BT_SCANNING_ENABLED true + +// Direct access to the low-level SPI communication between RadioLib and the radio module. +#define RADIOLIB_LOW_LEVEL (1) +// In this mode, all methods and member variables of all RadioLib classes will be made +// public and so will be exposed to the user. This allows direct manipulation of the +// library internals. +#define RADIOLIB_GODMODE (1) +#define RADIOLIB_CHECK_PARAMS (0) + +#include +#include +#include +#include #ifndef LILYGO #include // This file contains a binary patch for the SX1262 #include "modules/SX126x/patches/SX126x_patch_scan.h" -#elif defined(LILYGO) +#endif // end ifndef LILYGO + +#if defined(LILYGO) // LiLyGO device does not support the auto download mode, you need to get into the // download mode manually. To do so, press and hold the BOOT button and then press the // RESET button once. After that release the BOOT button. Or OFF->ON together with BOOT // Default LilyGO code -#include "utilities.h" -// Our Code -#include "LiLyGo.h" +#include +// #include "utilities.h" +// Our Code +#include "LiLyGo.h" #endif // end LILYGO #define BT_SCAN_DELAY 60 * 1 * 1000 @@ -62,58 +92,6 @@ uint64_t bt_start = 0; #include "DFRobot_OSD.h" #define OSD_SIDE_BAR true -static constexpr uint16_t levels[10] = { - 0x105, // 0 - 0x10E, // 1 - 0x10D, // 2 - 0x10C, // 3 - 0x10B, // 4 - 0x10A, // 5 - 0x109, // 6 - 0x108, // 7 - 0x107, // 8 - 0x106, // 9 -}; - -static constexpr uint16_t power_level[MAX_POWER_LEVELS + 1] = { - 0x10E, // 0 - 0x10E, // 1 - 0x10D, // 2 - 0x10C, // 3 - 0x10B, // 4 - 0x10A, // 5 - 0x109, // 6 - 0x108, // 7 - 0x107, // 8 - 0x106, // 9 not using 106 to accent rise - // new line - 0x10E, // 10 - 0x10D, // 11 - 0x10C, // 12 - 0x10B, // 13 - 0x10A, // 14 - 0x109, // 15 - 0x108, // 16 - 0x107, // 17 - 0x106, // 18 not using 106 - // new line - 0x10E, // 19 - 0x10D, // 20 - 0x10C, // 21 - 0x10B, // 22 - 0x10A, // 23 - 0x109, // 24 - 0x108, // 25 - 0x107, // 26 - 0x106, // 27 - 0x105, // 28 --- - 0x105, // 29 - 0x105, // 30 - 0x105, // 31 - 0x105, // 32 - 0x105 // 33 -}; - // SPI pins #define OSD_CS 47 #define OSD_MISO 33 @@ -138,11 +116,6 @@ int global_counter = 0; DFRobot_OSD osd(OSD_CS); #endif -/*Define Custom characters Example*/ -static const int buf0[36] = {0x02, 0x80, 0x02, 0x40, 0x7F, 0xE0, 0x42, 0x00, - 0x42, 0x00, 0x7A, 0x40, 0x4A, 0x40, 0x4A, 0x80, - 0x49, 0x20, 0x5A, 0xA0, 0x44, 0x60, 0x88, 0x20}; - #include "global_config.h" #include "ui.h" @@ -156,13 +129,15 @@ typedef enum METHOD_SPECTRAL } TSCAN_METOD_ENUM; +// #define SCAN_METHOD METHOD_SPECTRAL + #define SCAN_METHOD // #define METHOD_SPECTRAL // Spectral scan method #define METHOD_RSSI // Uncomment this and comment METHOD_SPECTRAL fot RSSI // Output Pixel Formula // 1 = rssi / 4, 2 = (rssi / 2) - 22 or 20 -constexpr int RSSI_OUTPUT_FORMULA = 2; +// constexpr int RSSI_OUTPUT_FORMULA = 2; // Feature to scan diapasones. Other frequency settings will be ignored. // int SCAN_RANGES[] = {850890, 920950}; @@ -170,16 +145,20 @@ int SCAN_RANGES[] = {}; // MHZ per page // to put everything into one page set RANGE_PER_PAGE = FREQ_END - 800 -uint64_t RANGE_PER_PAGE = FREQ_END - FREQ_BEGIN; // FREQ_END - FREQ_BEGIN +uint64_t RANGE_PER_PAGE; // FREQ_END - CONF_FREQ_BEGIN + +uint64_t CONF_FREQ_END, CONF_FREQ_BEGIN; // To Enable Multi Screen scan +// uint64_t RANGE_PER_PAGE = 50; +// Default Range on Menu Button Switch // multiplies STEPS * N to increase scan resolution. #define SCAN_RBW_FACTOR 2 -constexpr int OSD_PIXELS_PER_CHAR = (STEPS * SCAN_RBW_FACTOR) / OSD_CHART_WIDTH; +#ifdef USING_SX1280PA +#define SCAN_RBW_FACTOR 2 +#endif -// To Enable Multi Screen scan -// uint64_t RANGE_PER_PAGE = 50; -// Default Range on Menu Button Switch +constexpr int OSD_PIXELS_PER_CHAR = (STEPS * SCAN_RBW_FACTOR) / OSD_CHART_WIDTH; #define DEFAULT_RANGE_PER_PAGE 50 @@ -190,43 +169,30 @@ bool ANIMATED_RELOAD = false; #define UP_FILTER 5 // Trim low signals - nose level #define START_LOW 6 -// Remove reading without neighbors -#define FILTER_SPECTRUM_RESULTS true #define FILTER_SAMPLES_MIN constexpr bool DRAW_DETECTION_TICKS = true; - +int16_t max_x_rssi[STEPS] = {999}; +int16_t max_x_window[STEPS / 14] = {999}; +int x_window = 0; +constexpr int WINDOW_SIZE = 15; // Number of samples for each frequency scan. Fewer samples = better temporal resolution. // if more than 100 it can freeze #define SAMPLES 35 //(scan time = 1294) -// number of samples for RSSI method -#define SAMPLES_RSSI 20 // 21 // -#define RANGE (int)(FREQ_END - FREQ_BEGIN) - -#define SINGLE_STEP (float)(RANGE / (STEPS * SCAN_RBW_FACTOR)) - -uint64_t range = (int)(FREQ_END - FREQ_BEGIN); -uint64_t fr_begin = FREQ_BEGIN; -uint64_t fr_end = FREQ_BEGIN; - -uint64_t iterations = RANGE / RANGE_PER_PAGE; - -// uint64_t range_frequency = FREQ_END - FREQ_BEGIN; -uint64_t median_frequency = FREQ_BEGIN + FREQ_END - FREQ_BEGIN / 2; +uint64_t RANGE, range, iterations, median_frequency; +float SINGLE_STEP; // #define DISABLE_PLOT_CHART false // unused // Array to store the scan results uint16_t result[RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE]; -uint16_t result_display_set[RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE]; -uint16_t result_detections[RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE]; -uint16_t filtered_result[RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE]; + +bool filtered_result[RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE]; int max_bins_array_value[MAX_POWER_LEVELS]; int max_step_range = 32; -// Waterfall array -bool waterfall[STEPS], detected_y[STEPS]; // 20 - ??? steps of the waterfall +bool detected_y[STEPS]; // 20 - ??? steps // global variable @@ -235,11 +201,10 @@ bool first_run, new_pixel, detected_x = false; // drone detection flag bool detected = false; uint64_t drone_detection_level = DEFAULT_DRONE_DETECTION_LEVEL; +#define TRIGGER_LEVEL -80.0 uint64_t drone_detected_frequency_start = 0; uint64_t drone_detected_frequency_end = 0; -uint64_t detection_count = 0; bool single_page_scan = false; -bool SOUND_ON = false; // #define PRINT_DEBUG #define PRINT_PROFILE_TIME @@ -251,24 +216,27 @@ uint64_t scan_time = 0; uint64_t scan_start_time = 0; #endif +// log data via serial console, JSON format: +// #define LOG_DATA_JSON true +int LOG_DATA_JSON_INTERVAL = 1000; // Log at least every second + uint64_t x, y, range_item, w = WATERFALL_START, i = 0; int osd_x = 1, osd_y = 2, col = 0, max_bin = 32; uint64_t ranges_count = 0; -float freq = 0; int rssi = 0; int state = 0; +int CONF_SAMPLES; #ifdef METHOD_SPECTRAL -constexpr int samples = SAMPLES; +int samples = SAMPLES; #endif #ifdef METHOD_RSSI -constexpr int samples = SAMPLES_RSSI; +int samples = SAMPLES_RSSI; #endif uint8_t result_index = 0; uint8_t button_pressed_counter = 0; -uint64_t loop_cnt = 0; #ifndef LILYGO // #define JOYSTICK_ENABLED @@ -337,8 +305,8 @@ void osdProcess() // memset(max_step_range, 33, 30); max_bin = 32; - osd.displayString(12, 1, String(FREQ_BEGIN)); - osd.displayString(12, OSD_WIDTH - 8, String(FREQ_END)); + osd.displayString(12, 1, String(CONF_FREQ_BEGIN)); + osd.displayString(12, OSD_WIDTH - 8, String(CONF_FREQ_END)); // Finding biggest in result // Skiping 0 and 32 31 to avoid overflow for (int i = 1; i < MAX_POWER_LEVELS - 3; i++) @@ -375,7 +343,7 @@ void osdProcess() #ifdef OSD_SIDE_BAR { osd.displayString(col, OSD_WIDTH - 7, - String(FREQ_BEGIN + (col * osd_mhz_in_bin)) + "-" + + String(CONF_FREQ_BEGIN + (col * osd_mhz_in_bin)) + "-" + String(max_step_range) + " "); } #endif @@ -396,18 +364,59 @@ void osdProcess() } #endif +struct RadioScan : Scan +{ + float getRSSI() override; +}; + +float RadioScan::getRSSI() +{ +#if defined(USING_SX1280PA) + // radio.startReceive(); + // get instantaneous RSSI value + // When PR will be merged we can use radi.getRSSI(false); + uint8_t data[3] = {0, 0, 0}; // RssiInst, Status, RFU + radio.mod->SPIreadStream(RADIOLIB_SX128X_CMD_GET_RSSI_INST, data, 3); + return ((float)data[0] / (-2.0)); + +#elif defined(USING_LR1121) + // Try getRssiInst + float rssi; + radio.getRssiInst(&rssi); + // pass the replies + return rssi; +#else + return radio.getRSSI(false); +#endif +} + +RadioScan r; + +#define WATERFALL_SENSITIVITY 0.05 +DecoratedBarChart *bar; +WaterfallChart *waterChart; +StackedChart stacked(display, 0, 0, 0, 0); + +UptimeClock *uptime; + void init_radio() { // initialize SX1262 FSK modem at the initial frequency both.println("Init radio"); - state == radio.beginFSK(FREQ_BEGIN); - +#if defined(USING_SX1280PA) + state = radio.beginGFSK(CONF_FREQ_BEGIN); +#elif defined(USING_LR1121) + state = radio.beginGFSK(CONF_FREQ_BEGIN, 4.8F, 5.0F, 156.2F, 10, 16U, 1.7F); +#else + state = radio.beginFSK(CONF_FREQ_BEGIN); +#endif if (state == RADIOLIB_ERR_NONE) { Serial.println(F("success!")); } else { + display.println("Error:" + String(state)); Serial.print(F("failed, code ")); Serial.println(state); while (true) @@ -429,20 +438,183 @@ void init_radio() #endif both.println("Setting up radio"); +#ifdef USING_SX1280PA + // RADIOLIB_OR_HALT(radio.setBandwidth(RADIOLIB_SX128X_LORA_BW_406_25)); +#elif USING_SX1276 + // Receiver bandwidth in kHz. Allowed values + // are 2.6, 3.1, 3.9, 5.2, 6.3, 7.8, 10.4, 12.5, 15.6, 20.8, 25, 31.3, 41.7, + // 50, 62.5, 83.3, 100, 125, 166.7, 200 and 250 kHz. + RADIOLIB_OR_HALT(radio.setRxBandwidth(250)); +#else RADIOLIB_OR_HALT(radio.setRxBandwidth(BANDWIDTH)); +#endif // and disable the data shaping - RADIOLIB_OR_HALT(radio.setDataShaping(RADIOLIB_SHAPING_NONE)); + state = radio.setDataShaping(RADIOLIB_SHAPING_NONE); + if (state != RADIOLIB_ERR_NONE) + { + Serial.println("Error:setDataShaping:" + String(state)); + } both.println("Starting scanning..."); - // calibrate only once ,,, at startup - // TODO: check documentation (9.2.1) if we must calibrate in certain ranges - radio.setFrequency(FREQ_BEGIN, true); +// calibrate only once ,,, at startup +// TODO: check documentation (9.2.1) if we must calibrate in certain ranges +#ifdef USING_SX1280PA + state = radio.setFrequency(CONF_FREQ_BEGIN); + if (state != RADIOLIB_ERR_NONE) + { + Serial.println("Error:setFrequency:" + String(state)); + } + state = radio.startReceive(); + if (state != RADIOLIB_ERR_NONE) + { + Serial.println("Error:startReceive:" + String(state)); + } +#elif USING_SX1276 + // Sets carrier frequency. Allowed values range from 137.0 MHz to 1020.0 MHz. + radio.setFrequency(CONF_FREQ_BEGIN); +#else + radio.setFrequency(CONF_FREQ_BEGIN, true); +#endif + delay(50); } +struct frequency_scan_result +{ + uint64_t begin; + uint64_t end; + uint64_t last_epoch; + int16_t rssi; // deliberately not a float; floats can pin task to wrong core forever +} frequency_scan_result; + +TaskHandle_t logToSerial = NULL; + +void eventListenerForMSP(void *arg, Event &e) +{ + if (e.type == EventType::DETECTED) + { + if (e.epoch != frequency_scan_result.last_epoch || + e.detected.rssi > frequency_scan_result.rssi) + { + frequency_scan_result.last_epoch = e.epoch; + frequency_scan_result.rssi = e.detected.rssi; + } + + return; + } + + if (e.type == EventType::SCAN_TASK_COMPLETE) + { + // notify async communication that the data is ready + if (logToSerial != NULL) + { + xTaskNotifyGive(logToSerial); + } + return; + } +} + +void logToSerialTask(void *parameter) +{ +#ifdef HELTEC + JsonDocument doc; + char jsonOutput[200]; +#endif + + uint64_t last_epoch = frequency_scan_result.last_epoch; + frequency_scan_result.rssi = -999; + + for (;;) + { + ulTaskNotifyTake(true, pdMS_TO_TICKS(LOG_DATA_JSON_INTERVAL)); + if (frequency_scan_result.begin != frequency_scan_result.end || + frequency_scan_result.last_epoch != last_epoch) + { + int16_t highest_value_scanned = frequency_scan_result.rssi; + frequency_scan_result.rssi = -999; + last_epoch = frequency_scan_result.last_epoch; + if (highest_value_scanned == -999) + { + continue; + } + +#ifdef HELTEC + doc["low_range_freq"] = frequency_scan_result.begin; + doc["high_range_freq"] = frequency_scan_result.end; + doc["value"] = String(highest_value_scanned); + + serializeJson(doc, jsonOutput); + Serial.println(jsonOutput); +#else + Serial.printf("{\"low_range_freq\": %" PRIu64 + ", \"high_range_freq\": %" PRIu64 ", " + "\"value\": \"%" PRIi16 "\"}\n", + frequency_scan_result.begin, frequency_scan_result.end, + highest_value_scanned); +#endif + } + } +} + +void drone_sound_alarm(void *arg, Event &e); + +void readConfigFile() +{ + // writeFile(LittleFS, "/text.txt", "{WIFI:{name:\"sdfsdf\", Password:\"sdfsdf\"}"); + ssid = readParameterFromParameterFile(SSID); + Serial.println("SSID: " + ssid); + + pass = readParameterFromParameterFile(PASS); + Serial.println("PASS: " + pass); + + ip = readParameterFromParameterFile(IP); + Serial.println("PASS: " + ip); + + gateway = readParameterFromParameterFile(GATEWAY); + Serial.println("GATEWAY: " + gateway); + + fstart = readParameterFromParameterFile(FSTART); + Serial.println("FSTART: " + fstart); + + fend = readParameterFromParameterFile(FEND); + Serial.println("FEND: " + fend); + + smpls = readParameterFromParameterFile("samples"); + Serial.println("SAMPLES: " + smpls); + + CONF_SAMPLES = (smpls == "") ? samples : atoi(smpls.c_str()); + samples = CONF_SAMPLES; + CONF_FREQ_BEGIN = (fstart == "") ? FREQ_BEGIN : atoi(fstart.c_str()); + CONF_FREQ_END = (fend == "") ? FREQ_END : atoi(fend.c_str()); + + both.println("C FREQ BEGIN:" + String(CONF_FREQ_BEGIN)); + both.println("C FREQ END:" + String(CONF_FREQ_END)); + both.println("C SAMPLES:" + String(CONF_SAMPLES)); + + RANGE_PER_PAGE = CONF_FREQ_END - CONF_FREQ_BEGIN; // FREQ_END - CONF_FREQ_BEGIN + + RANGE = (int)(CONF_FREQ_END - CONF_FREQ_BEGIN); + + SINGLE_STEP = (float)(RANGE / (STEPS * SCAN_RBW_FACTOR)); + + range = (int)(CONF_FREQ_END - CONF_FREQ_BEGIN); + + iterations = RANGE / RANGE_PER_PAGE; + + // uint64_t range_frequency = FREQ_END - CONF_FREQ_BEGIN; + median_frequency = (CONF_FREQ_BEGIN + CONF_FREQ_END) / 2; +} + void setup(void) { + +#ifdef LILYGO + setupBoards(); // true for disable U8g2 display library + delay(500); + Serial.println("Setup LiLyGO board is done"); +#endif + // LED brightness heltec_led(25); #ifdef OSD_ENABLED @@ -462,7 +634,6 @@ void setup(void) #endif float vbat; float resolution; - loop_cnt = 0; bt_start = millis(); wf_start = millis(); @@ -470,6 +641,7 @@ void setup(void) pinMode(BUZZER_PIN, OUTPUT); pinMode(REB_PIN, OUTPUT); heltec_setup(); + #ifdef JOYSTICK_ENABLED calibrate_joy(); pinMode(JOY_BTN_PIN, INPUT_PULLUP); @@ -481,7 +653,7 @@ void setup(void) delay(10); if (button.pressed()) { - SOUND_ON = !SOUND_ON; + r.sound_on = !r.sound_on; tone(BUZZER_PIN, 205, 100); delay(50); tone(BUZZER_PIN, 205, 100); @@ -489,10 +661,46 @@ void setup(void) } } + display.clear(); + + both.println("CLICK for WIFI settings."); + + for (int i = 0; i < 200; i++) + { + both.print("."); + + button.update(); + delay(10); + if (button.pressedNow()) + { + both.println("-----------"); + both.println("Starting WIFI-SERVER..."); + // Error here: E (15752) ledc: ledc_get_duty(745): LEDC is not initialized + tone(BUZZER_PIN, 205, 100); + delay(50); + tone(BUZZER_PIN, 205, 500); + tone(BUZZER_PIN, 205, 100); + delay(50); + + serverStart(); + both.println("Ready to Connect: 192.168.4.1"); + delay(600); + break; + } + } + both.print("\n"); + + both.println("Init File System"); + initLittleFS(); + + readConfigFile(); + init_radio(); + #ifndef LILYGO vbat = heltec_vbat(); both.printf("V battery: %.2fV (%d%%)\n", vbat, heltec_battery_percent(vbat)); + delay(1000); #endif // end not LILYGO #ifdef WIFI_SCANNING_ENABLED WiFi.mode(WIFI_STA); @@ -504,7 +712,7 @@ void setup(void) delay(400); display.clear(); - resolution = RANGE / (STEPS * SCAN_RBW_FACTOR); + resolution = (float)RANGE / (STEPS * SCAN_RBW_FACTOR); single_page_scan = (RANGE_PER_PAGE == range); @@ -567,7 +775,12 @@ void setup(void) #ifdef METHOD_RSSI // TODO: try RADIOLIB_SX126X_RX_TIMEOUT_INF +#ifdef USING_SX1280PA + state = radio.startReceive(RADIOLIB_SX128X_RX_TIMEOUT_NONE); +#else state = radio.startReceive(RADIOLIB_SX126X_RX_TIMEOUT_NONE); +#endif + if (state != RADIOLIB_ERR_NONE) { Serial.print(F("Failed to start receive mode, error code: ")); @@ -582,6 +795,52 @@ void setup(void) #ifdef OSD_ENABLED osd.clear(); #endif + +#ifdef LOG_DATA_JSON + xTaskCreate(logToSerialTask, "LOG_DATA_JSON", 2048, NULL, 1, &logToSerial); +#endif + + r.trigger_level = TRIGGER_LEVEL; + stacked.reset(0, 0, display.width(), display.height()); + + bar = new DecoratedBarChart(display, 0, 0, display.width(), 0, CONF_FREQ_BEGIN, + CONF_FREQ_END, LO_RSSI_THRESHOLD, HI_RSSI_THRESHOLD, + r.trigger_level); + + size_t b = stacked.addChart(bar); + + Chart *statusBar = new StatusBar(display, 0, 0, display.width(), r); + +#if (WATERFALL_ENABLED == true) + size_t *multiples = new size_t[6]{5, 3, 4, 15, 4, 3}; + WaterfallModel *model = + new WaterfallModel((size_t)display.width(), 1000, 6, multiples); + model->reset(millis(), display.width()); + + delete[] multiples; + + waterChart = new WaterfallChart(display, 0, WATERFALL_START, display.width(), 0, + CONF_FREQ_BEGIN, CONF_FREQ_END, r.trigger_level, + WATERFALL_SENSITIVITY, model); + + size_t c = stacked.addChart(waterChart); + stacked.setHeight(c, stacked.height - WATERFALL_START - statusBar->height); + + r.addEventListener(DETECTED, *waterChart); +#endif + + size_t d = stacked.addChart(statusBar); + stacked.setHeight(b, stacked.height); + + r.addEventListener(DETECTED, bar->bar); + r.addEventListener(DETECTED, drone_sound_alarm, &r); + r.addEventListener(SCAN_TASK_COMPLETE, stacked); + + r.addEventListener(ALL_EVENTS, eventListenerForMSP, NULL); + +#ifdef UPTIME_CLOCK + uptime = new UptimeClock(display, millis()); +#endif } // Formula to translate 33 bin to approximate RSSI value @@ -591,10 +850,9 @@ int binToRSSI(int bin) return 11 + (bin * 4); } -// return true if continue the code is false break the loop -bool buttonPressHandler(float freq) +// is there an input using Hot Button or joystick +bool buttonInputRequested() { - // Detection level button short press if (button.pressedFor(100) #ifdef JOYSTICK_ENABLED || joy_btn_click() @@ -602,88 +860,105 @@ bool buttonPressHandler(float freq) ) { button.update(); - button_pressed_counter = 0; - // if long press stop - while (button.pressedNow() + if (button.pressedNow() #ifdef JOYSTICK_ENABLED - || joy_btn_click() + || joy_btn_click() #endif ) { - delay(10); - // Print Curent frequency - display.setTextAlignment(TEXT_ALIGN_CENTER); - display.drawString(128 / 2, 0, String(freq)); - display.display(); - button_pressed_counter++; - if (button_pressed_counter > 150) - { - digitalWrite(LED, HIGH); - delay(150); - digitalWrite(LED, LOW); - } - } - if (button_pressed_counter > 150) - { - // Remove Curent Frequency Text - display.setTextAlignment(TEXT_ALIGN_CENTER); - display.setColor(BLACK); - display.drawString(128 / 2, 0, String(freq)); - display.setColor(WHITE); - display.display(); - return false; - } - if (button_pressed_counter > 50 && button_pressed_counter < 150) - { - if (!joy_btn_clicked) - { - // Visually confirm it's off so user releases button - display.displayOff(); - // Deep sleep (has wait for release so we don't wake up - // immediately) - heltec_deep_sleep(); - } - return false; - } - button.update(); - display.setTextAlignment(TEXT_ALIGN_RIGHT); - // erase old drone detection level value - display.setColor(BLACK); - display.fillRect(128 - 13, 0, 13, 13); - display.setColor(WHITE); - drone_detection_level++; - // print new value - display.drawString(128, 0, String(drone_detection_level)); - tone(BUZZER_PIN, 104, 150); - if (drone_detection_level > 30) - { - drone_detection_level = 1; + return true; } } - return true; + + return false; } -void drone_sound_alarm(int drone_detection_level, int detection_count) +enum ButtonEvent { + NONE = 0, + LONG_PRESS, + SHORT_PRESS, + TOO_SHORT, + SUSPEND +}; + +ButtonEvent buttonPressEvent() +{ + button_pressed_counter = 0; + // if long press stop + while (button.pressedNow() +#ifdef JOYSTICK_ENABLED + || joy_btn_click() +#endif + ) + { + delay(10); + button_pressed_counter++; + if (button_pressed_counter > 150) + { + digitalWrite(LED, HIGH); + delay(150); + digitalWrite(LED, LOW); + } + } + if (button_pressed_counter > 150) + { + return LONG_PRESS; + } + + if (button_pressed_counter > 50) + { + if (!joy_btn_clicked) + { + return SUSPEND; + } + return SHORT_PRESS; + } + button.update(); + + return TOO_SHORT; +} + +void drone_sound_alarm(void *arg, Event &e) +{ + if (e.type != DETECTED) + { + return; + } + + Scan &r = *((Scan *)arg); + if (!r.sound_on) + return; + + int tone_freq_db = e.detected.detected_at * 2; + int drone_detection_level = r.drone_detection_level; + int detection_count = r.detection_count; + // If level is set to sensitive, // start beeping every 10th frequency and shorter // it improves performance less short beep delays... if (drone_detection_level <= 25) { - if (detection_count == 1 && SOUND_ON) + + if (tone_freq_db != 205) { - tone(BUZZER_PIN, 205, + tone_freq_db = 285 - tone_freq_db; + } + + if (r.detection_count == 1 && r.sound_on) + { + tone(BUZZER_PIN, tone_freq_db, 10); // same action ??? but first time } - if (detection_count % 5 == 0 && SOUND_ON) + if (r.detection_count % 5 == 0 && r.sound_on) { - tone(BUZZER_PIN, 205, + tone(BUZZER_PIN, tone_freq_db, 10); // same action ??? but every 5th time } } else { - if (detection_count % 20 == 0 && SOUND_ON) + if (r.detection_count % 20 == 0 && r.sound_on) { tone(BUZZER_PIN, 205, 10); // same action ??? but every 20th detection @@ -697,7 +972,7 @@ void joystickMoveCursor(int joy_x_pressed) if (joy_x_pressed > 0) { cursor_x_position--; - display.drawString(cursor_x_position, 0, String((int)freq)); + display.drawString(cursor_x_position, 0, String((int)r.current_frequency)); display.drawLine(cursor_x_position, 1, cursor_x_position, 10); display.display(); delay(10); @@ -705,7 +980,7 @@ void joystickMoveCursor(int joy_x_pressed) else if (joy_x_pressed < 0) { cursor_x_position++; - display.drawString(cursor_x_position, 0, String((int)freq)); + display.drawString(cursor_x_position, 0, String((int)r.current_frequency)); display.drawLine(cursor_x_position, 1, cursor_x_position, 10); display.display(); delay(10); @@ -713,7 +988,7 @@ void joystickMoveCursor(int joy_x_pressed) if (cursor_x_position > DISPLAY_WIDTH || cursor_x_position < 0) { cursor_x_position = 0; - display.drawString(cursor_x_position, 0, String((int)freq)); + display.drawString(cursor_x_position, 0, String((int)r.current_frequency)); display.drawLine(cursor_x_position, 1, cursor_x_position, 10); display.display(); delay(10); @@ -750,42 +1025,41 @@ void check_ranges() single_page_scan = false; } } -// MAX Frequency RSSI value of the samples + +// MAX Frequency RSSI BIN value of the samples int max_rssi_x = 999; void loop(void) { - UI_displayDecorate(0, 0, false); // some default values + r.led_flag = false; - detection_count = 0; + r.detection_count = 0; drone_detected_frequency_start = 0; ranges_count = 0; - // reset scan time - scan_time = 0; - - // general purpose loop counter - loop_cnt++; - +// reset scan time #ifdef PRINT_PROFILE_TIME + scan_time = 0; loop_start = millis(); #endif + r.epoch++; if (!ANIMATED_RELOAD || !single_page_scan) { // clear the scan plot rectangle UI_clearPlotter(); + UI_clearTopStatus(); } // do the scan - range = FREQ_END - FREQ_BEGIN; + range = CONF_FREQ_END - CONF_FREQ_BEGIN; if (RANGE_PER_PAGE > range) { RANGE_PER_PAGE = range; } - fr_begin = FREQ_BEGIN; - fr_end = fr_begin; + r.fr_begin = CONF_FREQ_BEGIN; + r.fr_end = r.fr_begin; // 50 is a single-screen range // TODO: Make 50 a variable with the option to show the full range @@ -807,14 +1081,14 @@ void loop(void) range = RANGE_PER_PAGE; if (ranges_count == 0) { - fr_begin = (range_item == 0) ? fr_begin : fr_begin += range; - fr_end = fr_begin + RANGE_PER_PAGE; + r.fr_begin = (range_item == 0) ? r.fr_begin : r.fr_begin + range; + r.fr_end = r.fr_begin + RANGE_PER_PAGE; } else { - fr_begin = SCAN_RANGES[range_item] / 1000; - fr_end = SCAN_RANGES[range_item] % 1000; - range = fr_end - fr_begin; + r.fr_begin = SCAN_RANGES[range_item] / 1000; + r.fr_end = SCAN_RANGES[range_item] % 1000; + range = r.fr_end - r.fr_begin; } #ifdef DISABLED_CODE @@ -825,11 +1099,6 @@ void loop(void) } #endif - if (single_page_scan == false) - { - UI_displayDecorate(fr_begin, fr_end, true); - } - drone_detected_frequency_start = 0; display.setTextAlignment(TEXT_ALIGN_RIGHT); @@ -860,25 +1129,29 @@ void loop(void) // Because of the SCAN_RBW_FACTOR x is not a display coordinate anymore // x > STEPS on SCAN_RBW_FACTOR int display_x = x / SCAN_RBW_FACTOR; - waterfall[display_x] = false; float step = (range * ((float)x / (STEPS * SCAN_RBW_FACTOR))); - freq = fr_begin + step; -#ifdef PRINT_DEBUG - Serial.println("setFrequency:" + String(freq)); -#endif + r.current_frequency = r.fr_begin + step; + LOG("setFrequency:%f\n", r.current_frequency); -#ifdef LILYGO - state = radio.setFrequency(freq, false); // false = no calibration need here +#ifdef USING_SX1280PA + state = + radio.setFrequency(r.current_frequency); // 1280 doesn't have calibration + radio.startReceive(RADIOLIB_SX128X_RX_TIMEOUT_INF); +#elif USING_SX1276 + state = radio.setFrequency(freq); #else - state = radio.setFrequency(freq, false); // false = no calibration need here + state = radio.setFrequency(r.current_frequency, + true); // true = no calibration need here #endif int radio_error_count = 0; if (state != RADIOLIB_ERR_NONE) { - display.drawString(0, 64 - 10, "E:setFrequency:" + String(freq)); - // display.drawString(0, 64 - 10, "E:setFrequency:" + String(freq)); - Serial.println("E:setFrequency:" + String(freq)); + display.drawString(0, 64 - 10, + "E(" + String(state) + + "):setFrequency:" + String(r.current_frequency)); + Serial.println("E(" + String(state) + + "):setFrequency:" + String(r.current_frequency)); display.display(); delay(2); radio_error_count++; @@ -886,9 +1159,7 @@ void loop(void) continue; } -#ifdef PRINT_DEBUG - Serial.printf("Step:%d Freq: %f\n", x, freq); -#endif + LOG("Step:%d Freq: %f\n", x, r.current_frequency); // SpectralScan Method #ifdef METHOD_SPECTRAL { @@ -919,59 +1190,20 @@ void loop(void) #ifdef METHOD_RSSI // Spectrum analyzer using getRSSI { -#ifdef PRINT_DEBUG - Serial.println("METHOD RSSI"); -#endif - // memset - // memset(result, 0, RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE); - // Some issues with memset function - for (i = 0; i < RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE; i++) - { - result[i] = 0; - } - result_index = 0; - // N of samples - for (int r = 0; r < SAMPLES_RSSI; r++) - { - rssi = radio.getRSSI(false); - // ToDO: check if 4 is correct value for 33 power bins - // Now we have more space because we are ignoring low dB values - // we can / 3 default 4 - if (RSSI_OUTPUT_FORMULA == 1) - { - result_index = - /// still not clear formula but it works - uint8_t(abs(rssi) / 4); - } - else if (RSSI_OUTPUT_FORMULA == 2) - { - // I like this formula better - result_index = uint8_t(abs(rssi) / 2) - 22; - } + LOG("METHOD RSSI"); + uint16_t max_rssi = r.rssiMethod(CONF_SAMPLES, result, + RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE); -#ifdef PRINT_DEBUG - Serial.printf("RSSI: %d IDX: %d\n", rssi, result_index); -#endif - // avoid buffer overflow - if (result_index < RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE) - { - // Saving max value only rss is negative so smaller is bigger - if (result[result_index] > rssi) - { - result[result_index] = rssi; - } - } - else - { - Serial.print("Out-of-Range: result_index %d\n"); - } + if (max_x_rssi[display_x] > max_rssi) + { + max_x_rssi[display_x] = max_rssi; } } #endif // SCAN_METHOD == METHOD_RSSI // if this code is not executed LORA radio doesn't work // basically SX1262 requires delay - // osd.displayString(12, 1, String(FREQ_BEGIN)); + // osd.displayString(12, 1, String(CONF_FREQ_BEGIN)); // osd.displayString(12, 30 - 8, String(FREQ_END)); // delay(2); @@ -986,158 +1218,65 @@ void loop(void) display.setColor(WHITE); } #endif - detected = false; - detected_y[display_x] = false; - max_rssi_x = 999; + Event event = r.detect(result, filtered_result, + RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE, samples); + event.time_ms = millis(); - for (y = 0; y < RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE; y++) + size_t detected_at = event.detected.detected_at; + if (max_rssi_x > detected_at) { + // MAx bin Value not RSSI + max_rssi_x = detected_at; + } -#ifdef PRINT_DEBUG - Serial.print(String(y) + ":"); - Serial.print(String(result[y]) + ","); -#endif -#if !defined(FILTER_SPECTRUM_RESULTS) || FILTER_SPECTRUM_RESULTS == false - if (result[y] && result[y] != 0) - { - filtered_result[y] = 1; - } - else - { - filtered_result[y] = 0; - } -#endif + detected = event.detected.detected; + detected_y[display_x] = false; -// if samples low ~1 filter removes all values -#if FILTER_SPECTRUM_RESULTS + float rr = event.detected.rssi; + r.drone_detection_level = drone_detection_level; - filtered_result[y] = 0; - // Filter Elements without neighbors - // if RSSI method actual value is -xxx dB - if (result[y] > 0 && samples > 1) - { - // do not process 'first' and 'last' row to avoid out of index - // access. - if ((y > 0) && (y != (RADIOLIB_SX126X_SPECTRAL_SCAN_RES_SIZE - 3))) - { - if (((result[y + 1] != 0) && (result[y + 2] != 0)) || - (result[y - 1] != 0)) - { - filtered_result[y] = 1; - } - else - { -#ifdef PRINT_DEBUG - Serial.print("Filtered:" + String(x) + ":" + String(y) + ","); -#endif - } - } - } // not filtering if samples == 1 - else if (result[y] > 0 && samples == 1) - { - filtered_result[y] = 1; - } - -#endif + if (event.detected.trigger) + { // check if we should alarm about a drone presence - if ((filtered_result[y] == 1) // we have some data and - && (y <= drone_detection_level) && - detected_y[display_x] == false) // detection threshold match + if (detected_y[display_x] == false) // detection threshold match { // Set LED to ON (filtered in UI component) - UI_setLedFlag(true); -#if (WATERFALL_ENABLED == true) - if (single_page_scan) - { - // Drone detection true for waterfall - if (!waterfall[display_x]) - { - waterfall[display_x] = true; - display.setColor(WHITE); - display.setPixel(display_x, w); - } - } -#endif + r.led_flag = true; if (drone_detected_frequency_start == 0) { // mark freq start - drone_detected_frequency_start = freq; + drone_detected_frequency_start = r.current_frequency; } // mark freq end ... will shift right to last detected range - drone_detected_frequency_end = freq; - if (SOUND_ON == true) - { - drone_sound_alarm(drone_detection_level, detection_count); - } + drone_detected_frequency_end = r.current_frequency; +#ifdef LOG_DATA_JSON + frequency_scan_result.begin = drone_detected_frequency_start; + frequency_scan_result.end = drone_detected_frequency_end; +#endif if (DRAW_DETECTION_TICKS == true) { - // draw vertical line on top of display for "drone detected" - // frequencies +// draw vertical line on top of display for "drone detected" +// frequencies +#ifdef METHOD_SPECTRAL if (!detected_y[display_x]) { display.drawLine(display_x, 1, display_x, 4); detected_y[display_x] = true; } - } - } -#if (WATERFALL_ENABLED == true) - if ((filtered_result[y] == 1) && (y <= drone_detection_level) && - (single_page_scan) && (waterfall[display_x] != true) && new_pixel) - { - // If drone not found set dark pixel on the waterfall - // TODO: make something like scrolling up if possible - waterfall[display_x] = false; - display.setColor(BLACK); - display.setPixel(display_x, w); - display.setColor(WHITE); - } #endif - // next 2 If's ... adds !!!! 10ms of runtime ......tfk ??? - if (filtered_result[y] == 1) - { -#ifdef PRINT_DEBUG - Serial.print("Pixel:" + String(display_x) + "(" + String(x) + ")" + - ":" + String(y) + ","); -#endif - if (max_rssi_x > y) - { - max_rssi_x = y; } - // Set signal level pixel - if (y < MAX_POWER_LEVELS - START_LOW) - { - display.setPixel(display_x, y + START_LOW); - } - if (!detected) - { - detected = true; - } - } - - // ------------------------------------------------------------- - // Draw "Detection Level line" every 2 pixel - // ------------------------------------------------------------- - if ((y == drone_detection_level) && (display_x % 2 == 0)) - { - display.setColor(WHITE); - if (filtered_result[y] == 1) - { - display.setColor(INVERSE); - } - display.setPixel(display_x, y + START_LOW); - // display.setPixel(display_x, y + START_LOW - 1); // 2 px wide - - display.setColor(WHITE); } } + r.fireEvent(event); + #ifdef JOYSTICK_ENABLED // Draw joystick cursor and Frequency RSSI value if (display_x == cursor_x_position) { - display.drawString(display_x - 1, 0, String((int)freq)); + display.drawString(display_x - 1, 0, String((int)r.current_frequency)); display.drawLine(display_x, 1, display_x, 12); // if method scan RSSI we can get exact RSSI value display.drawString(display_x + 17, 0, "-" + String((int)max_rssi_x * 4)); @@ -1147,26 +1286,76 @@ void loop(void) #ifdef PRINT_PROFILE_TIME scan_time += (millis() - scan_start_time); #endif - // count detected - if (detected) - { - detection_count++; - } - #ifdef PRINT_DEBUG Serial.println("....\n"); #endif - if (first_run || ANIMATED_RELOAD) + if (r.animated) { display.display(); } -// LiLyGo doesn't have button ;( -// ToDO: Check if we use BOOT button -#ifndef LILYGO - if (buttonPressHandler(freq) == false) - break; -#endif // END LILYGO + if (buttonInputRequested()) + { + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.drawString(display.width() / 2, 0, String(r.current_frequency)); + display.display(); + + ButtonEvent e = buttonPressEvent(); + + if (e == LONG_PRESS) + { + // Remove Curent Frequency Text + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.setColor(BLACK); + display.drawString(display.width() / 2, 0, + String(r.current_frequency)); + display.setColor(WHITE); + display.display(); + + break; + } + + if (e == SUSPEND) + { + // Visually confirm it's off so user releases button + display.displayOff(); + // Deep sleep (has wait for release so we don't wake up + // immediately) + heltec_deep_sleep(); + break; + } + + if (e == SHORT_PRESS) + break; + + if (e == TOO_SHORT) + { + String v = String(r.trigger_level) + " dB"; + uint16_t w = display.getStringWidth(v); + display.setTextAlignment(TEXT_ALIGN_RIGHT); + // erase old drone detection level value + display.setColor(BLACK); + display.fillRect(display.width() - w, 0, 13, w); + display.setColor(WHITE); + + // dt is roughly single-pixel increment + float dt = + bar->bar.height == 0 + ? 0.0 + : (LO_RSSI_THRESHOLD - HI_RSSI_THRESHOLD) / bar->bar.height; + r.trigger_level += dt; + if (r.trigger_level <= LO_RSSI_THRESHOLD) + { + r.trigger_level = HI_RSSI_THRESHOLD; + } + + // print new value + display.drawString(display.width(), 0, v); + tone(BUZZER_PIN, 104, 150); + + bar->bar.redraw_all = true; + } + } // wait a little bit before the next scan, // otherwise the SX1262 hangs @@ -1214,16 +1403,16 @@ void loop(void) { w = WATERFALL_START; } -#if (WATERFALL_ENABLED == true) - // Draw waterfall position cursor - if (single_page_scan) + { - display.setColor(BLACK); - display.drawHorizontalLine(0, w, STEPS); - display.setColor(WHITE); + Event event(r, SCAN_TASK_COMPLETE, millis()); + r.fireEvent(event); } -#endif // Render display data here + +#ifdef UPTIME_CLOCK + uptime->draw(millis()); +#endif display.display(); #ifdef OSD_ENABLED // Sometimes OSD prints entire screen with the digits. @@ -1242,13 +1431,13 @@ void loop(void) #endif } #ifdef PRINT_DEBUG - // Serial.println("----"); +// Serial.println("----"); #endif - loop_time = millis() - loop_start; joy_btn_clicked = false; #ifdef PRINT_PROFILE_TIME + loop_time = millis() - loop_start; Serial.printf("LOOP: %lld ms; SCAN: %lld ms;\n ", loop_time, scan_time); #endif // No WiFi and BT Scan Without OSD diff --git a/src/ui.cpp b/src/ui.cpp index 48a7bf1..77bdc58 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -2,6 +2,8 @@ #include "RadioLib.h" #include "global_config.h" #include "images.h" +#include +#include // ------------------------------------------------- // LOCAL DEFINES @@ -12,24 +14,11 @@ // #define SCALE_TEXT_TOP (HEIGHT + X_AXIS_WEIGHT + MAJOR_TICK_LENGTH) -static unsigned int start_scan_text = (128 / 2) - 3; -// initialized flag -static bool ui_initialized = false; -static bool led_flag = false; -static unsigned short int scan_progress_count = 0; - -#ifdef Vision_Master_E290 -static DEPG0290BxS800FxX_BW *display_instance; -#else -//(0x3c, SDA_OLED, SCL_OLED, DISPLAY_GEOMETRY); -static SSD1306Wire *display_instance; -#endif // temporary dirty import ... to be solved durring upcoming refactoring -extern unsigned int drone_detection_level; extern unsigned int RANGE_PER_PAGE; +extern uint64_t CONF_FREQ_BEGIN; +extern uint64_t CONF_FREQ_END; extern unsigned int median_frequency; -extern unsigned int detection_count; -extern bool SOUND_ON; extern unsigned int drone_detected_frequency_start; extern unsigned int drone_detected_frequency_end; extern unsigned int ranges_count; @@ -40,225 +29,139 @@ extern unsigned int range_item; extern uint64_t loop_time; -#ifndef Vision_Master_E290 -void UI_Init(SSD1306Wire *display_ptr) +void UI_Init(Display_t *display_ptr) { - // init pointer to display instance. - display_instance = display_ptr; // check for null ??? - display_instance->clear(); + display_ptr->clear(); // draw the UCOG welcome logo - display_instance->drawXbm(0, 2, 128, 64, epd_bitmap_ucog); - display_instance->display(); + display_ptr->drawXbm(0, 2, 128, 64, epd_bitmap_ucog); + display_ptr->display(); } -#endif -#ifdef Vision_Master_E290 -void UI_Init(DEPG0290BxS800FxX_BW *display_ptr) -{ - // init pointer to display instance. - display_instance = display_ptr; - // check for null ??? - display_instance->clear(); - // draw the UCOG welcome logo - display_instance->drawXbm(0, 2, 128, 64, epd_bitmap_ucog); - display_instance->display(); -} -#endif - -void UI_setLedFlag(bool new_status) { led_flag = new_status; } - -void clearStatus(void) +void StatusBar::clearStatus(void) { // clear status line - display_instance->setColor(BLACK); - display_instance->fillRect(0, ROW_STATUS_TEXT + 2, 128, 13); - display_instance->setColor(WHITE); + display.setColor(BLACK); + display.fillRect(pos_x, pos_y, width, height); } void UI_clearPlotter(void) { // clear the scan plot rectangle (top part) - display_instance->setColor(BLACK); - display_instance->fillRect(0, 0, STEPS, HEIGHT); - display_instance->setColor(WHITE); + // display_instance->setColor(BLACK); + // display_instance->fillRect(0, 10, STEPS, HEIGHT - 10); + // display_instance->setColor(WHITE); } -/** - * @brief Draws ticks on the display at regular whole intervals. - * - * @param every The interval between ticks in MHz. - * @param length The length of each tick in pixels. - */ -void drawTicks(float every, int length) +void UI_clearTopStatus(void) { - int first_tick; - bool correction; - int pixels_per_step; - int correction_number; - int tick; - int tick_minor; - int median; - - first_tick = 0; - //+ (every - (fr_begin - (int)(fr_begin / every) * every)); - /*if (first_tick < fr_begin) - { - first_tick += every; - }*/ - correction = false; - pixels_per_step = STEPS / (RANGE_PER_PAGE / every); - if (STEPS / RANGE_PER_PAGE != 0) - { - correction = true; - } - correction_number = STEPS - (int)(pixels_per_step * (RANGE_PER_PAGE / every)); - tick = 0; - tick_minor = 0; - median = (RANGE_PER_PAGE / every) / 2; - // TODO: (RANGE_PER_PAGE / every) - // * 2 has twice extra steps we need to figureout correct logic or minor - // ticks is not showing to the end - for (int t = 0; t <= (RANGE_PER_PAGE / every) * 2; t++) - { - // fix if pixels per step is not int and we have shift - if (correction && t % 2 != 0 && correction_number > 1) - { - // pixels_per_step++; - correction_number--; - } - tick += pixels_per_step; - tick_minor = tick / 2; - if (tick <= 128 - 3) - { - display_instance->drawLine(tick, HEIGHT + X_AXIS_WEIGHT, tick, - HEIGHT + X_AXIS_WEIGHT + length); - // Central tick - if (tick > (128 / 2) - 3 && tick < (128 / 2) + 3) - { - display_instance->drawLine(tick + 1, HEIGHT + X_AXIS_WEIGHT, tick + 1, - HEIGHT + X_AXIS_WEIGHT + length); - } - } -#ifdef MINOR_TICKS - // Fix two ticks together - if ((tick_minor + 1 != tick) && (tick_minor - 1 != tick) && - (tick_minor + 2 != tick) && (tick_minor - 2 != tick)) - { - display_instance->drawLine(tick_minor, HEIGHT + X_AXIS_WEIGHT, tick_minor, - HEIGHT + X_AXIS_WEIGHT + MINOR_TICK_LENGTH); - } - // Central tick - if (tick_minor > (128 / 2) - 3 && tick_minor < (128 / 2) + 3) - { - display_instance->drawLine(tick_minor + 1, HEIGHT + X_AXIS_WEIGHT, - tick_minor + 1, - HEIGHT + X_AXIS_WEIGHT + MINOR_TICK_LENGTH); - } -#endif - } + // clear the scan plot rectangle (top part) + // display_instance->setColor(BLACK); + // display_instance->fillRect(0, 0, STEPS, 10); + // display_instance->setColor(WHITE); } void UI_drawCursor(int16_t possition) { // Draw animated vertical cursor on reload process - display_instance->setColor(BLACK); - display_instance->drawVerticalLine(possition, 0, HEIGHT); - display_instance->drawVerticalLine(possition + 1, 0, HEIGHT); - display_instance->drawVerticalLine(possition + 2, 0, HEIGHT); - display_instance->setColor(WHITE); + // display_instance->setColor(BLACK); + // display_instance->drawVerticalLine(possition, 0, HEIGHT); + // display_instance->drawVerticalLine(possition + 1, 0, HEIGHT); + // display_instance->drawVerticalLine(possition + 2, 0, HEIGHT); + // display_instance->setColor(WHITE); } /** * @brief Decorates the display: everything but the plot itself. */ -void UI_displayDecorate(int begin = 0, int end = 0, bool redraw = false) +void StatusBar::draw() { + uint16_t text_y = pos_y + height - 10; + if (!ui_initialized) { - // Start and end ticks - display_instance->fillRect(0, HEIGHT + X_AXIS_WEIGHT, 2, MAJOR_TICK_LENGTH + 1); - display_instance->fillRect(126, HEIGHT + X_AXIS_WEIGHT, 2, MAJOR_TICK_LENGTH + 1); // Drone detection level - display_instance->setTextAlignment(TEXT_ALIGN_RIGHT); - display_instance->drawString(128, 0, String(drone_detection_level)); + display.setTextAlignment(TEXT_ALIGN_RIGHT); + display.drawString(width, 0, String(r.drone_detection_level)); } - if (!ui_initialized || redraw) + if (!ui_initialized) { // Clear something - display_instance->setColor(BLACK); - display_instance->fillRect(0, SCALE_TEXT_TOP + 1, 128, 12); - display_instance->setColor(WHITE); - + /* display_instance->setColor(BLACK); + display_instance->fillRect(0, SCALE_TEXT_TOP + 1, 128, 12); + display_instance->setColor(WHITE); + */ // Drone detection level - display_instance->setTextAlignment(TEXT_ALIGN_RIGHT); - display_instance->drawString(128, 0, String(drone_detection_level)); + display.setTextAlignment(TEXT_ALIGN_RIGHT); + display.drawString(pos_x + width, 0, String(r.drone_detection_level)); // Frequency start - display_instance->setTextAlignment(TEXT_ALIGN_LEFT); - display_instance->drawString(0, ROW_STATUS_TEXT, - (begin == 0) ? String(FREQ_BEGIN) : String(begin)); + display.setTextAlignment(TEXT_ALIGN_LEFT); + display.drawString(pos_x, text_y, + (r.fr_begin == 0) ? String(CONF_FREQ_BEGIN) + : String(r.fr_begin)); // Frequency detected - display_instance->setTextAlignment(TEXT_ALIGN_CENTER); - display_instance->drawString(128 / 2, ROW_STATUS_TEXT, - (begin == 0) ? String(median_frequency) - : String(begin + ((end - begin) / 2))); + display.setTextAlignment(TEXT_ALIGN_CENTER); + display.drawString(pos_x + width / 2, text_y, + (r.fr_begin == 0) + ? String(median_frequency) + : String(r.fr_begin + ((r.fr_end - r.fr_begin) / 2))); // Frequency end - display_instance->setTextAlignment(TEXT_ALIGN_RIGHT); - display_instance->drawString(128, ROW_STATUS_TEXT, - (end == 0) ? String(FREQ_END) : String(end)); + display.setTextAlignment(TEXT_ALIGN_RIGHT); + display.drawString(pos_x + width, text_y, + (r.fr_end == 0) ? String(CONF_FREQ_END) : String(r.fr_end)); } // Status text block - if (led_flag) // 'drone' detected + if (r.led_flag) // 'drone' detected { - display_instance->setTextAlignment(TEXT_ALIGN_CENTER); + display.setTextAlignment(TEXT_ALIGN_CENTER); // clear status line clearStatus(); - display_instance->drawString(start_scan_text, ROW_STATUS_TEXT, - String(drone_detected_frequency_start) + ">RF<" + - String(drone_detected_frequency_end)); + display.setColor(WHITE); + display.drawString(pos_x + width / 2, text_y, + String(drone_detected_frequency_start) + ">RF<" + + String(drone_detected_frequency_end)); } else { // "Scanning" - display_instance->setTextAlignment(TEXT_ALIGN_CENTER); + display.setTextAlignment(TEXT_ALIGN_CENTER); // clear status line clearStatus(); - if (scan_progress_count == 0) + String s = "Scan \\"; + if (scan_progress_count == 1) { - display_instance->drawString(start_scan_text, ROW_STATUS_TEXT, "Scan \\"); - } - else if (scan_progress_count == 1) - { - display_instance->drawString(start_scan_text, ROW_STATUS_TEXT, "Scan |"); + s = "Scan |"; } else if (scan_progress_count == 2) { - display_instance->drawString(start_scan_text, ROW_STATUS_TEXT, "Scan /"); + s = "Scan /"; } else if (scan_progress_count == 3) { - display_instance->drawString(start_scan_text, ROW_STATUS_TEXT, "Scan -"); + s = "Scan -"; } scan_progress_count++; if (scan_progress_count >= 4) { scan_progress_count = 0; } + display.setColor(WHITE); + display.drawString(pos_x + width / 2 - 3, text_y, s); } - if (led_flag == true && detection_count >= 5) + if (r.led_flag && r.detection_count >= 5) { digitalWrite(LED, HIGH); - if (SOUND_ON) + if (r.sound_on) { tone(BUZZER_PIN, 104, 100); } digitalWrite(REB_PIN, HIGH); - led_flag = false; + r.led_flag = false; } - else if (!redraw) + else if (!r.led_flag) { digitalWrite(LED, LOW); } @@ -266,39 +169,29 @@ void UI_displayDecorate(int begin = 0, int end = 0, bool redraw = false) if (ranges_count == 0) { #ifdef DEBUG - display_instance->setTextAlignment(TEXT_ALIGN_LEFT); - display_instance->drawString(0, ROW_STATUS_TEXT, String(loop_time)); + display.setTextAlignment(TEXT_ALIGN_LEFT); + display.drawString(pos_x, text_y, String(loop_time)); #else - display_instance->setTextAlignment(TEXT_ALIGN_LEFT); - display_instance->drawString(0, ROW_STATUS_TEXT, String(FREQ_BEGIN)); + display.setTextAlignment(TEXT_ALIGN_LEFT); + display.drawString(pos_x, text_y, String(CONF_FREQ_BEGIN)); #endif - display_instance->setTextAlignment(TEXT_ALIGN_RIGHT); - display_instance->drawString(128, ROW_STATUS_TEXT, String(FREQ_END)); + display.setTextAlignment(TEXT_ALIGN_RIGHT); + display.drawString(pos_x + width, text_y, String(CONF_FREQ_END)); } else if (ranges_count > 0) { - display_instance->setTextAlignment(TEXT_ALIGN_LEFT); - display_instance->drawString(0, ROW_STATUS_TEXT, - String(SCAN_RANGES[range_item] / 1000) + "-" + - String(SCAN_RANGES[range_item] % 1000)); + display.setTextAlignment(TEXT_ALIGN_LEFT); + display.drawString(pos_x, text_y, + String(SCAN_RANGES[range_item] / 1000) + "-" + + String(SCAN_RANGES[range_item] % 1000)); if (range_item + 1 < iterations) { - display_instance->setTextAlignment(TEXT_ALIGN_RIGHT); - display_instance->drawString(128, ROW_STATUS_TEXT, - String(SCAN_RANGES[range_item + 1] / 1000) + - "-" + - String(SCAN_RANGES[range_item + 1] % 1000)); + display.setTextAlignment(TEXT_ALIGN_RIGHT); + display.drawString(pos_x + width, text_y, + String(SCAN_RANGES[range_item + 1] / 1000) + "-" + + String(SCAN_RANGES[range_item + 1] % 1000)); } } - if (ui_initialized == false) - { - // X-axis - display_instance->fillRect(0, HEIGHT, STEPS, X_AXIS_WEIGHT); -// ticks -#ifdef MAJOR_TICKS - drawTicks(MAJOR_TICKS, MAJOR_TICK_LENGTH); -#endif - } ui_initialized = true; } diff --git a/test/test_rssi.cpp b/test/test_rssi.cpp new file mode 100644 index 0000000..a38d166 --- /dev/null +++ b/test/test_rssi.cpp @@ -0,0 +1,67 @@ +#include +#define LOG(args...) printf(args) +#include "../lib/scan/scan.cpp" +#include + +struct TestScan : Scan +{ + TestScan(float *ctx, int sz) : ctx(ctx), sz(sz), idx(0) {} + + float getRSSI() override; + + float *ctx; + int sz; + int idx; +}; + +float TestScan::getRSSI() +{ + if (idx >= sz) + { + return -1000000; + } + + return ctx[idx++]; +} + +constexpr int test_sz = 13; +constexpr int inputs_sz = 13; + +void test_rssi(void) +{ + uint16_t samples[test_sz]; + float inputs[inputs_sz] = {-40.0, -100.0, -200.0, -50.0, -400.0, -60.0, -20, + -75.5, -70, -80, -90, -55.9, -110}; + + TestScan t = TestScan(inputs, inputs_sz); + + uint16_t r = t.rssiMethod(inputs_sz, samples, test_sz); + + uint16_t expect[test_sz] = {20, 50, 55, 60, 0, 70, 75, 80, 0, 90, 0, 100, 110}; + + TEST_ASSERT_EQUAL_INT16(20, r); + TEST_ASSERT_EQUAL_INT16_ARRAY(expect, samples, test_sz); +} + +void test_detect() +{ + uint16_t samples[test_sz] = {20, 50, 55, 60, 0, 70, 75, 80, 0, 90, 0, 100, 110}; + bool result[test_sz]; + + TestScan test_scan({}, 0); + Event e = test_scan.detect(samples, result, test_sz, 1); + size_t r = e.detected.detected_at; + + bool expect[test_sz] = {1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1}; + + TEST_ASSERT_EQUAL_INT16(0, r); + TEST_ASSERT_EQUAL_INT8_ARRAY(expect, result, test_sz); + + Event e2 = test_scan.detect(samples, result, test_sz, 2); + r = e2.detected.detected_at; + + bool expect2[test_sz] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0}; + + TEST_ASSERT_EQUAL_INT16(1, r); + TEST_ASSERT_EQUAL_INT8_ARRAY(expect2, result, test_sz); +} diff --git a/test/test_waterfall.cpp b/test/test_waterfall.cpp new file mode 100644 index 0000000..1875065 --- /dev/null +++ b/test/test_waterfall.cpp @@ -0,0 +1,220 @@ +#define TO_STRING +#include "../lib/models/WaterfallModel.cpp" +#include +#include + +void test_push() +{ + size_t *ms = new size_t[6]{5, 3, 4, 15, 4, 3}; + WaterfallModel m(1, 1, 6, ms); + delete ms; + + char *r = m.toString(); + TEST_ASSERT_EQUAL_STRING("w:1 b:34 " + "[dt:1 t:0 [ c:0 e:0 ]" + "dt:1 t:0 [ c:0 e:0 ]" + "dt:1 t:0 [ c:0 e:0 ]" + "dt:1 t:0 [ c:0 e:0 ]" + "dt:1 t:0 [ c:0 e:0 ]" + "dt:5 t:0 [ c:0 e:0 ]" + "dt:5 t:0 [ c:0 e:0 ]" + "dt:5 t:0 [ c:0 e:0 ]" + "dt:15 t:0 [ c:0 e:0 ]" + "dt:15 t:0 [ c:0 e:0 ]" + "dt:15 t:0 [ c:0 e:0 ]" + "dt:15 t:0 [ c:0 e:0 ]" + "dt:60 t:0 [ c:0 e:0 ]" + "dt:60 t:0 [ c:0 e:0 ]" + "dt:60 t:0 [ c:0 e:0 ]" + "dt:60 t:0 [ c:0 e:0 ]" + "dt:60 t:0 [ c:0 e:0 ]" + "dt:60 t:0 [ c:0 e:0 ]" + "dt:60 t:0 [ c:0 e:0 ]" + "dt:60 t:0 [ c:0 e:0 ]" + "dt:60 t:0 [ c:0 e:0 ]" + "dt:60 t:0 [ c:0 e:0 ]" + "dt:60 t:0 [ c:0 e:0 ]" + "dt:60 t:0 [ c:0 e:0 ]" + "dt:60 t:0 [ c:0 e:0 ]" + "dt:60 t:0 [ c:0 e:0 ]" + "dt:60 t:0 [ c:0 e:0 ]" + "dt:900 t:0 [ c:0 e:0 ]" + "dt:900 t:0 [ c:0 e:0 ]" + "dt:900 t:0 [ c:0 e:0 ]" + "dt:900 t:0 [ c:0 e:0 ]" + "dt:3600 t:0 [ c:0 e:0 ]" + "dt:3600 t:0 [ c:0 e:0 ]" + "dt:3600 t:0 [ c:0 e:0 ] ]", + r); + delete r; + + m.reset(0, 1); + uint64_t i = 0; + for (; i < 10; i++) + m.updateModel(i, 0, 1); + + r = m.toString(); + TEST_ASSERT_EQUAL_STRING("w:1 b:34 " + "[dt:1 t:9 [ c:1 e:1 ]" + "dt:1 t:8 [ c:1 e:1 ]" + "dt:1 t:7 [ c:1 e:1 ]" + "dt:1 t:6 [ c:1 e:1 ]" + "dt:1 t:5 [ c:1 e:1 ]" + "dt:5 t:5 [ c:5 e:5 ]" + "dt:5 t:5 [ c:0 e:0 ]" + "dt:5 t:5 [ c:0 e:0 ]" + "dt:15 t:15 [ c:0 e:0 ]" + "dt:15 t:15 [ c:0 e:0 ]" + "dt:15 t:15 [ c:0 e:0 ]" + "dt:15 t:15 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:900 t:900 [ c:0 e:0 ]" + "dt:900 t:900 [ c:0 e:0 ]" + "dt:900 t:900 [ c:0 e:0 ]" + "dt:900 t:900 [ c:0 e:0 ]" + "dt:3600 t:3600 [ c:0 e:0 ]" + "dt:3600 t:3600 [ c:0 e:0 ]" + "dt:3600 t:3600 [ c:0 e:0 ] ]", + r); + delete r; + + for (; i < 100; i += 10) + m.updateModel(i, 0, 1); + + r = m.toString(); + TEST_ASSERT_EQUAL_STRING("w:1 b:34 " + "[dt:1 t:90 [ c:1 e:1 ]" + "dt:1 t:89 [ c:0 e:0 ]" + "dt:1 t:88 [ c:0 e:0 ]" + "dt:1 t:87 [ c:0 e:0 ]" + "dt:1 t:86 [ c:0 e:0 ]" + "dt:5 t:85 [ c:0 e:0 ]" + "dt:5 t:80 [ c:1 e:1 ]" + "dt:5 t:75 [ c:0 e:0 ]" + "dt:15 t:75 [ c:1 e:1 ]" + "dt:15 t:60 [ c:2 e:2 ]" + "dt:15 t:45 [ c:1 e:1 ]" + "dt:15 t:30 [ c:2 e:2 ]" + "dt:60 t:60 [ c:11 e:11 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:60 t:60 [ c:0 e:0 ]" + "dt:900 t:900 [ c:0 e:0 ]" + "dt:900 t:900 [ c:0 e:0 ]" + "dt:900 t:900 [ c:0 e:0 ]" + "dt:900 t:900 [ c:0 e:0 ]" + "dt:3600 t:3600 [ c:0 e:0 ]" + "dt:3600 t:3600 [ c:0 e:0 ]" + "dt:3600 t:3600 [ c:0 e:0 ] ]", + r); + delete r; + + for (; i < 10000; i++) + m.updateModel(i, 0, 1); + + r = m.toString(); + TEST_ASSERT_EQUAL_STRING("w:1 b:34 " + "[dt:1 t:9999 [ c:1 e:1 ]" + "dt:1 t:9998 [ c:1 e:1 ]" + "dt:1 t:9997 [ c:1 e:1 ]" + "dt:1 t:9996 [ c:1 e:1 ]" + "dt:1 t:9995 [ c:1 e:1 ]" + "dt:5 t:9995 [ c:4 e:4 ]" + "dt:5 t:9990 [ c:5 e:5 ]" + "dt:5 t:9985 [ c:5 e:5 ]" + "dt:15 t:9990 [ c:5 e:5 ]" + "dt:15 t:9975 [ c:15 e:15 ]" + "dt:15 t:9960 [ c:15 e:15 ]" + "dt:15 t:9945 [ c:15 e:15 ]" + "dt:60 t:9960 [ c:30 e:30 ]" + "dt:60 t:9900 [ c:60 e:60 ]" + "dt:60 t:9840 [ c:60 e:60 ]" + "dt:60 t:9780 [ c:60 e:60 ]" + "dt:60 t:9720 [ c:60 e:60 ]" + "dt:60 t:9660 [ c:60 e:60 ]" + "dt:60 t:9600 [ c:60 e:60 ]" + "dt:60 t:9540 [ c:60 e:60 ]" + "dt:60 t:9480 [ c:60 e:60 ]" + "dt:60 t:9420 [ c:60 e:60 ]" + "dt:60 t:9360 [ c:60 e:60 ]" + "dt:60 t:9300 [ c:60 e:60 ]" + "dt:60 t:9240 [ c:60 e:60 ]" + "dt:60 t:9180 [ c:60 e:60 ]" + "dt:60 t:9120 [ c:60 e:60 ]" + "dt:900 t:9900 [ c:60 e:60 ]" + "dt:900 t:9000 [ c:900 e:900 ]" + "dt:900 t:8100 [ c:900 e:900 ]" + "dt:900 t:7200 [ c:900 e:900 ]" + "dt:3600 t:7200 [ c:2700 e:2700 ]" + "dt:3600 t:3600 [ c:3520 e:3520 ]" + "dt:3600 t:3600 [ c:0 e:0 ] ]", + r); + delete r; + + for (; i < 5000; i++) + m.updateModel(i, 0, 1); + + r = m.toString(); + TEST_ASSERT_EQUAL_STRING("w:1 b:34 " + "[dt:1 t:9999 [ c:1 e:1 ]" + "dt:1 t:9998 [ c:1 e:1 ]" + "dt:1 t:9997 [ c:1 e:1 ]" + "dt:1 t:9996 [ c:1 e:1 ]" + "dt:1 t:9995 [ c:1 e:1 ]" + "dt:5 t:9995 [ c:4 e:4 ]" + "dt:5 t:9990 [ c:5 e:5 ]" + "dt:5 t:9985 [ c:5 e:5 ]" + "dt:15 t:9990 [ c:5 e:5 ]" + "dt:15 t:9975 [ c:15 e:15 ]" + "dt:15 t:9960 [ c:15 e:15 ]" + "dt:15 t:9945 [ c:15 e:15 ]" + "dt:60 t:9960 [ c:30 e:30 ]" + "dt:60 t:9900 [ c:60 e:60 ]" + "dt:60 t:9840 [ c:60 e:60 ]" + "dt:60 t:9780 [ c:60 e:60 ]" + "dt:60 t:9720 [ c:60 e:60 ]" + "dt:60 t:9660 [ c:60 e:60 ]" + "dt:60 t:9600 [ c:60 e:60 ]" + "dt:60 t:9540 [ c:60 e:60 ]" + "dt:60 t:9480 [ c:60 e:60 ]" + "dt:60 t:9420 [ c:60 e:60 ]" + "dt:60 t:9360 [ c:60 e:60 ]" + "dt:60 t:9300 [ c:60 e:60 ]" + "dt:60 t:9240 [ c:60 e:60 ]" + "dt:60 t:9180 [ c:60 e:60 ]" + "dt:60 t:9120 [ c:60 e:60 ]" + "dt:900 t:9900 [ c:60 e:60 ]" + "dt:900 t:9000 [ c:900 e:900 ]" + "dt:900 t:8100 [ c:900 e:900 ]" + "dt:900 t:7200 [ c:900 e:900 ]" + "dt:3600 t:7200 [ c:2700 e:2700 ]" + "dt:3600 t:3600 [ c:3520 e:3520 ]" + "dt:3600 t:3600 [ c:0 e:0 ] ]", + r); + delete r; +} diff --git a/test/tests.cpp b/test/tests.cpp new file mode 100644 index 0000000..46fa133 --- /dev/null +++ b/test/tests.cpp @@ -0,0 +1,21 @@ +#include + +void test_rssi(); +void test_detect(); +void test_push(); + +void setUp(void) {} + +void tearDown(void) {} + +int main(int argc, char **argv) +{ + UNITY_BEGIN(); + + RUN_TEST(test_rssi); + RUN_TEST(test_detect); + + RUN_TEST(test_push); + + UNITY_END(); +} diff --git a/tft_src/main.cpp b/tft_src/main.cpp index 9eae332..ac220ef 100644 --- a/tft_src/main.cpp +++ b/tft_src/main.cpp @@ -13,11 +13,28 @@ // #define ARDUINO_USB_CDC_ON_BOOT 1 // #define LoRaWAN_DEBUG_LEVEL 0 #include "HT_ST7789spi.h" -#include "global_config.h" +// #include "global_config.h" #include "images.h" -#include "ui.h" +// #include "ui.h" #include #include +#include + +struct Entry +{ + String drone; // Drone name + int fstart; // Fr Start + int fend; // Fr End + int y; // y(vertical) position + uint16_t color; // color +}; + +// Define and initialize the vector +std::vector fpvArray = {{"FPV-ELRS", 160, 350, 100, ST7789_BLUE}, + {"915-ELRS", 700, 1000, 100, ST7789_ORANGE}, + {"FPV433-ELRS", 350, 530, 100, ST7789_YELLOW}, + {"Orlan", 820, 940, 98, ST7789_GREEN}, + {"Zala", 830, 950, 80, ST7789_MAGENTA}}; #define st7789_CS_Pin 39 #define st7789_REST_Pin 40 @@ -70,16 +87,20 @@ constexpr bool DRAW_DETECTION_TICKS = true; // number of samples for RSSI method #define SAMPLES_RSSI 5 // 21 // -#define FREQ_BEGIN 650 -#define DEFAULT_DRONE_DETECTION_LEVEL 90 +#define FREQ_BEGIN 150 +#define FREQ_END 950 +#define BANDWIDTH 467.0 +#define MHZ_PX (float)((float)(FREQ_END - FREQ_BEGIN) / DISPLAY_WIDTH) +#define DEFAULT_DRONE_DETECTION_LEVEL -90 +#define DRONE_LEGEND 1; #define RANGE (int)(FREQ_END - FREQ_BEGIN) -#define SINGLE_STEP (float)(RANGE / (STEPS * SCAN_RBW_FACTOR)) +// #define SINGLE_STEP (float)(RANGE / (STEPS * SCAN_RBW_FACTOR)) uint64_t range = (int)(FREQ_END - FREQ_BEGIN); uint64_t fr_begin = FREQ_BEGIN; -uint64_t fr_end = FREQ_BEGIN; +uint64_t fr_end = FREQ_END; // Feature to scan diapasones. Other frequency settings will be ignored. // int SCAN_RANGES[] = {850890, 920950}; @@ -90,7 +111,7 @@ int SCAN_RANGES[] = {}; // uint64_t RANGE_PER_PAGE = FREQ_END - FREQ_BEGIN; // FREQ_END - FREQ_BEGIN // Override or e-ink -uint64_t RANGE_PER_PAGE = FREQ_BEGIN + DISPLAY_WIDTH; +uint64_t RANGE_PER_PAGE = FREQ_END - FREQ_BEGIN; // FREQ_BEGIN + DISPLAY_WIDTH; uint64_t iterations = RANGE / RANGE_PER_PAGE; @@ -114,7 +135,7 @@ bool waterfall[STEPS], detected_y[STEPS]; // 20 - ??? steps of the waterfall bool first_run, new_pixel, detected_x = false; // drone detection flag bool detected = false; -uint64_t drone_detection_level = DEFAULT_DRONE_DETECTION_LEVEL; +int64_t drone_detection_level = DEFAULT_DRONE_DETECTION_LEVEL; uint64_t drone_detected_frequency_start = 0; uint64_t drone_detected_frequency_end = 0; uint64_t detection_count = 0; @@ -131,9 +152,24 @@ uint64_t scan_time = 0; uint64_t scan_start_time = 0; #endif -#define WATERFALL_START 115 +// To remove waterfall adjust this and this +#define ZERO_LEVEL 110 // Equal to minimal RSSI +#define ZERO_SHIFT 42 +#define LOWER_LEVEL ZERO_LEVEL + ZERO_SHIFT // 108(zero) - (40 moving down) +#define SPECTR_CHART_STAR_TOP 42; +#define WATERFALL_START 119 #define WATERFALL_END DISPLAY_HEIGHT - 10 - 2 +#ifndef DISABLE_WATERFALL +#define DISABLE_WATERFALL 1 // to disable set to 1 +#endif + +#if DISABLE_WATERFALL == 0 +#define ZERO_LEVEL 110 +#define ZERO_SHIFT 0 +#define LOWER_LEVEL ZERO_LEVEL +#endif + uint64_t x, y, range_item, w = WATERFALL_START, i = 0; int osd_x = 1, osd_y = 2, col = 0, max_bin = 32; uint64_t ranges_count = 0; @@ -247,8 +283,27 @@ void battery() } } -constexpr int lower_level = 108; -constexpr int up_level = 40; +void drawDroneLegend() +{ + // Draw FPV array Names + for (const auto &entry : fpvArray) + { + int pixelStart = (entry.fstart - FREQ_BEGIN) / MHZ_PX; + int pixelEnd = (entry.fend - FREQ_BEGIN) / MHZ_PX; + int length = (pixelEnd - pixelStart); + // Serial.println("Pixel Start: " + String(pixelStart)); + // Serial.println("MHinPIX: " + String(MHZ_PX)); + int median = length / 2; + if (entry.fstart < FREQ_END) + { + st7789->drawFastHLine(pixelStart, entry.y, length, entry.color); + drawText(pixelStart, entry.y - 10, entry.drone, entry.color); + } + } +} + +constexpr int lower_level = LOWER_LEVEL; +constexpr int up_level = SPECTR_CHART_STAR_TOP; int rssiToPix(int rssi) { // Bigger is lower signal @@ -256,11 +311,25 @@ int rssiToPix(int rssi) { return lower_level - 1; } - if (abs(rssi) <= up_level) + if (abs(rssi) <= up_level && lower_level < 130) { return up_level; } - return abs(rssi); + // if chart moved to the bottom + if (lower_level > 130) + { + int returnRssi = abs(rssi - ZERO_SHIFT); + // Serial.println("RSSI: " + String(rssi)); + if (returnRssi >= lower_level) + { + return lower_level - 1; + } + return returnRssi; + } + else + { + return abs(rssi); + } } // @@ -286,7 +355,7 @@ long timeSinceLastModeSwitch = 0; float fr = FREQ_BEGIN, fr_x[STEPS + 5], vbat = 0; // MHz in one screen pix step // END will be Begin + 289 * mhz_step -constexpr int mhz_step = 1; +float mhz_step = MHZ_PX; // TODO: make end_freq // Measure RSS every step constexpr float rssi_mhz_step = 0.33; @@ -301,6 +370,7 @@ int window_max_rssi = -999; int window_max_fr = -999; int max_scan_rssi[STEPS + 2]; int max_history_rssi[STEPS + 2]; +int historical_loops = 50, h = 0; long display_scan_start = 0; long display_scan_end = 0; long display_scan_i_end = 0; @@ -315,6 +385,7 @@ constexpr unsigned int STATUS_BAR_HEIGHT = 5; void loop() { + // Serial.println("Loop"); if (screen_update_loop_counter == 0) { fr_x[x1] = 0; @@ -331,11 +402,20 @@ void loop() fr_x[x1] = fr; int u = 0; - int additional_samples = 10; + int additional_samples = 0; // Clear old data with the cursor ... st7789->drawFastVLine(x1, lower_level, -lower_level + 11, ST7789_BLACK); + // Draw max history line + if (h == historical_loops) + { + st7789->drawLine(x1, rssiToPix(max_history_rssi[x1]), x1, lower_level, + ST7789_BLACK /*gray*/); + // clear history + max_history_rssi[x1] = -999; + } + st7789->drawLine(x1, rssiToPix(max_history_rssi[x1]), x1, lower_level, 12710 /*gray*/); // Fetch samples @@ -351,8 +431,15 @@ void loop() additional_samples--; } - radio.setFrequency((float)fr + (float)(rssi_mhz_step * u), - false); // false = no calibration need here + bool calibrate = true; + float freq = (float)fr + (float)(rssi_mhz_step * u); + if ((int)freq % 10 == 0) + { + calibrate = true; + } + radio.setFrequency(freq, + /*false*/ calibrate); // false = no calibration need here + // Serial.println((float)fr + (float)(rssi_mhz_step * u)); u++; if (rssi_mhz_step * u >= mhz_step) { @@ -363,6 +450,7 @@ void loop() rssi_single_start = millis(); } rssi2 = radio.getRSSI(false); + // Serial.print(" RSSI : " + String(rssi2)); scan_iterations++; if (rssi_single_end == 0) { @@ -383,13 +471,24 @@ void loop() #ifdef PRINT_DEBUG Serial.println(String(fr) + ":" + String(rssi2)); #endif + int lineHeight = 0; + st7789->drawPixel(x1, rssiToPix(rssi2), rssiToColor(abs(rssi2))); st7789->drawPixel(x1, rssiToPix(rssi2) - 1, rssiToColor(abs(rssi2))); st7789->drawPixel(x1, rssiToPix(rssi2) - 2, rssiToColor(abs(rssi2))); + st7789->drawPixel(x1, rssiToPix(rssi2) - 3, rssiToColor(abs(rssi2))); + st7789->drawPixel(x1, rssiToPix(rssi2) - 4, rssiToColor(abs(rssi2))); + + if (true /*draw full line*/) + { + st7789->drawFastVLine(x1, rssiToPix(rssi2), lower_level - rssiToPix(rssi2), + rssiToColor(abs(rssi2))); + } // Draw Update Cursor st7789->drawFastVLine(x1 + 1, lower_level, -lower_level + 11, ST7789_BLACK); st7789->drawFastVLine(x1 + 2, lower_level, -lower_level + 11, ST7789_BLACK); - st7789->drawFastVLine(x1 + 3, lower_level, -lower_level + 11, ST7789_BLACK); + // st7789->drawFastVLine(x1 + 3, lower_level, -lower_level + 11, + // ST7789_BLACK); if (max_scan_rssi[x1] == -999) { @@ -417,10 +516,13 @@ void loop() } } // Writing pixel only if it is bigger than drone detection level - if (abs(max_scan_rssi[x1]) < drone_detection_level) + if (abs(max_scan_rssi[x1]) < abs(drone_detection_level)) { - // Waterfall Pixel - st7789->drawPixel(x1, w, rssiToColor(abs(max_scan_rssi[x1]), true)); + if (DISABLE_WATERFALL == 0) + { + // Waterfall Pixel + st7789->drawPixel(x1, w, rssiToColor(abs(max_scan_rssi[x1]), true)); + } detailed_scan_candidate[(int)fr] = (int)fr; } @@ -431,7 +533,7 @@ void loop() // Draw legend for windows if (x1 % rssi_window_size == 0 || x1 == DISPLAY_WIDTH) { - if (abs(window_max_rssi) < drone_detection_level && window_max_rssi != 0 && + if (abs(window_max_rssi) < abs(drone_detection_level) && window_max_rssi != 0 && window_max_rssi != -999) { y2 = 15; @@ -450,9 +552,15 @@ void loop() window_max_rssi = -999; } - // Waterfall cursor - st7789->drawFastHLine(0, w + 1, DISPLAY_WIDTH, ST7789_BLACK); - st7789->drawFastHLine(0, w + 2, DISPLAY_WIDTH, ST7789_BLACK); + if (DISABLE_WATERFALL == 0) + { + // Waterfall cursor + st7789->drawFastHLine(0, w + 1, DISPLAY_WIDTH, ST7789_BLACK); + if (w < WATERFALL_END) + { + st7789->drawFastHLine(0, w + 2, DISPLAY_WIDTH, ST7789_ORANGE); + } + } // drone detection level line if (x1 % 2 == 0) @@ -470,9 +578,9 @@ void loop() button_pressed_counter = 0; if (button.pressed()) { - drone_detection_level++; - if (drone_detection_level > 107) - drone_detection_level = DEFAULT_DRONE_DETECTION_LEVEL - 20; + drone_detection_level--; + if (drone_detection_level < -107) + drone_detection_level = DEFAULT_DRONE_DETECTION_LEVEL + 20; while (button.pressedNow()) { delay(100); @@ -491,6 +599,14 @@ void loop() heltec_deep_sleep(); } + // Drone legend every 1/4 of the screen + if (x1 % (STEPS / 4) == 0) + { +#ifdef DRONE_LEGEND + drawDroneLegend(); +#endif + } + // Main N x-axis full loop end logic if (x1 >= STEPS) { @@ -502,17 +618,28 @@ void loop() #ifdef PRINT_DEBUG Serial.println("Screen End for Output: " + String(screen_update_loop_counter)); #endif + // Doing output only after full scan if (screen_update_loop_counter + 1 == SCANS_PER_DISPLAY) { +#ifdef DRONE_LEGEND + drawDroneLegend(); +#endif + + h++; + if (h == historical_loops - 1) + { + h = 0; + } + // Scan results to max Mhz and dB in window display_scan_end = millis(); st7789->fillRect(0, 0, DISPLAY_WIDTH, 11, ST7789_BLACK); drawText(0, 0, "T:" + String(display_scan_end - display_scan_start) + "/" + - String(rssi_single_end - rssi_single_start) + " L:-" + - String(drone_detection_level) + "dB", + String(rssi_single_end - rssi_single_start) + + " L:" + String(drone_detection_level) + "dB", ST7789_BLUE); /// battery(); diff --git a/trans_src/main.cpp b/trans_src/main.cpp index 2010f48..e40a4c2 100644 --- a/trans_src/main.cpp +++ b/trans_src/main.cpp @@ -8,6 +8,7 @@ * This works on the stick, but the output on the screen gets cut off. */ +#include // Turns the 'PRG' button into the power button, long press is off #define HELTEC_POWER_BUTTON // must be before "#include " #include @@ -15,11 +16,11 @@ // Pause between transmited packets in mseconds. // Set to zero to only transmit a packet when pressing the user button // Will not exceed 1% duty cycle, even if you set a lower value. -#define PAUSE 20 +#define PAUSE 10 // Frequency in MHz. Keep the decimal point to designate float. // Check your own rules and regulations to see what is legal where you are. -#define FREQUENCY 866.3 // for Europe +#define FREQUENCY 915 // for Europe // #define FREQUENCY 905.2 // for US // LoRa bandwidth. Keep the decimal point to designate float. @@ -29,13 +30,13 @@ // Number from 5 to 12. Higher means slower but higher "processor gain", // meaning (in nutshell) longer range and more robust against interference. -#define SPREADING_FACTOR 9 +#define SPREADING_FACTOR 7 // Transmit power in dBm. 0 dBm = 1 mW, enough for tabletop-testing. This value can be // set anywhere between -9 dBm (0.125 mW) to 22 dBm (158 mW). Note that the maximum ERP // (which is what your antenna maximally radiates) on the EU ISM band is 25 mW, and that // transmissting without an antenna can damage your hardware. -#define TRANSMIT_POWER -9 +#define TRANSMIT_POWER 22 String rxdata; volatile bool rxFlag = false; @@ -67,35 +68,41 @@ void setup() RADIOLIB_OR_HALT(radio.startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF)); } +int FHSS = 0.25; +float FHSS_counter = -10; void loop() { heltec_loop(); - bool tx_legal = millis() > last_tx + minimum_pause; + bool tx_legal = true; // millis() > last_tx + minimum_pause; + // Emulate frequency hopping spread spectrum (FHSS) is a method of transmitting radio + // signals by rapidly switching the carrier between different frequency channels. + float fr = (float)(FREQUENCY + (float)(FHSS_counter)); + RADIOLIB_OR_HALT(radio.setFrequency(fr, false)); // Transmit a packet every PAUSE seconds or when the button is pressed if ((PAUSE && tx_legal && millis() - last_tx > (PAUSE)) || button.isSingleClick()) { - // In case of button click, tell user to wait - if (!tx_legal) + if (button.isSingleClick()) { - both.printf("Legal limit, wait %i sec.\n", - (int)((minimum_pause - (millis() - last_tx)) / 1000) + 1); - return; + fr = 1000; + RADIOLIB_OR_HALT(radio.setFrequency(fr, false)); } - both.printf("TX [%s] ", String(counter).c_str()); + // In case of button click, tell user to wait + display.printf("TX[%s]", String(counter).c_str()); radio.clearDio1Action(); heltec_led(50); // 50% brightness is plenty for this LED tx_time = millis(); - RADIOLIB(radio.transmit(String(counter++).c_str())); + RADIOLIB(radio.transmit(String("Putin Huylo!!! LA-LA-LA-LA").c_str())); tx_time = millis() - tx_time; heltec_led(0); if (_radiolib_status == RADIOLIB_ERR_NONE) { - both.printf("OK (%i ms)\n", (int)tx_time); + display.printf("OK(%ims)/%.2fMhz\n", (int)tx_time, fr); } else { - both.printf("fail (%i)\n", _radiolib_status); + display.printf("fail (%i)\n", _radiolib_status); + heltec_delay(100); } // Maximum 1% duty cycle minimum_pause = tx_time * 100; @@ -111,10 +118,16 @@ void loop() radio.readData(rxdata); if (_radiolib_status == RADIOLIB_ERR_NONE) { - both.printf("RX [%s]\n", rxdata.c_str()); - both.printf(" RSSI: %.2f dBm\n", radio.getRSSI()); - both.printf(" SNR: %.2f dB\n", radio.getSNR()); + display.printf("RX [%s]\n", rxdata.c_str()); + display.printf(" RSSI: %.2f dBm\n", radio.getRSSI()); + display.printf(" SNR: %.2f dB\n", radio.getSNR()); } RADIOLIB_OR_HALT(radio.startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF)); } + FHSS_counter += 0.250; + counter++; + if (FHSS_counter > 20) + { + FHSS_counter = -10; + } }