From 7a5134bb86e6d73d9baf56b6f9b3aa1d667bdb78 Mon Sep 17 00:00:00 2001 From: Joshinken <65787590+Joshinken@users.noreply.github.com> Date: Tue, 21 Jul 2020 23:45:20 +0200 Subject: [PATCH] =?UTF-8?q?nye=C2=B3=20(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update Display.cpp I THINK I FIGURED OUT WHAT IM DOING * Add files via upload * Update esp32_marauder.ino * Update MenuFunctions.cpp * Update MenuFunctions.h * Update MenuFunctions.cpp * Update README.md * Update README.md * Update MenuFunctions.cpp * Update MenuFunctions.h * Update MenuFunctions.h * Add files via upload * Update esp32_marauder.ino * Update README.md * Update Display.cpp moved the float "wd", as the draw function would continously change it, which resulted in those weird effects when trying to draw different thicknesses * Update MenuFunctions.cpp * Update Display.cpp made it so the drawing code doesnt need to draw every pixel individually and made it so it can handle higher thiccnesses... at all. it did not handly thicknesses above 2 with any kind of grace * Update Display.cpp removed unneccessary variable changes, just realized i kept making certain variables be a certain value over and over * Fix screen change battery percentage * restore TFT touch data Co-authored-by: Just Call Me Koko <25190487+justcallmekoko@users.noreply.github.com> --- README.md | 10 + esp32_marauder/Display.cpp | 50 +- esp32_marauder/MenuFunctions.cpp | 1351 +++++++++++++++------------ esp32_marauder/MenuFunctions.h | 41 +- esp32_marauder/data/marauder3L1.jpg | Bin 0 -> 24399 bytes esp32_marauder/esp32_marauder.ino | 6 +- 6 files changed, 834 insertions(+), 624 deletions(-) create mode 100644 esp32_marauder/data/marauder3L1.jpg diff --git a/README.md b/README.md index 45824c3..c1169ee 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,14 @@ Make the following connections between your 2.8" TFT Screen and your ESP32 board | | T_IRQ | | | SD_CS | | GPIO12 | +For the analog battery circuit, use a 4 to 1 voltage divider, and (optional) a mosfet +For the charge detection circuit, use a 1 to 2 voltage divider (the charge detection is optional and only changes the battery icon colour while charging) +| BATTERY | ESP32 | +| ------- | ------ | +| BAT + | GPIO34 | +| MOSFET | GPIO13 | +| CHARGE +| GPIO27 | + ## Flashing Firmware ### Using Arduino IDE 1. Install the [Arduino IDE](https://www.arduino.cc/en/main/software) @@ -115,8 +123,10 @@ Make the following connections between your 2.8" TFT Screen and your ESP32 board 8. Install the [CH340 Drivers](https://github.com/justcallmekoko/ESP32Marauder/blob/master/Drivers/CH34x_Install_Windows_v3_4.EXE) 9. Download or clone this repository 10. Open `esp32_marauder.ino` +10.5. If you're using the analog battery measuring circuit, go to the MenuFunctions.h and change "#define BATTERY_ANALOG_ON" to 1 11. Plug your ESP32 into a USB port and select the COM port under `Tools`>`Port` 12. Select `LOLIN D32` under `Tools`>`Boards` +12.5 If you want an upscaled version of the logo, go to the data folder and rename "marauder3L1.jpg" to "marauder3L.jpg" 13. Click `ESP32 Sketch Data Upload` and wait for the SPIFFS upload to finish 14. Click the upload button diff --git a/esp32_marauder/Display.cpp b/esp32_marauder/Display.cpp index 438519b..40196db 100644 --- a/esp32_marauder/Display.cpp +++ b/esp32_marauder/Display.cpp @@ -465,7 +465,6 @@ void Display::drawJpeg(const char *filename, int xpos, int ypos) { uint16_t xlast; uint16_t ylast; uint32_t AH; -float wd = 3; void Display::drawStylus() { uint16_t x = 0, y = 0; // To store the touch coordinates @@ -477,26 +476,35 @@ void Display::drawStylus() if (pressed) { // tft.fillCircle(x, y, 2, TFT_WHITE); if ( xlast > 0 && ylast > 0 ) { - int dx = abs(x - xlast), sx = xlast < x ? 1 : -1; - int dy = abs(y - ylast), sy = ylast < y ? 1 : -1; - int err = dx - dy, e2, x2, y2; /* error value e_xy */ - float ed = dx + dy == 0 ? 1 : sqrt((float)dx * dx + (float)dy * dy); - - for (wd = (wd + 1) / 2; ; ) { /* pixel loop */ - tft.drawPixel(xlast, ylast, TFT_WHITE); - e2 = err; x2 = xlast; - if (2 * e2 >= -dx) { /* x step */ - for (e2 += dy, y2 = ylast; e2 < ed * wd && (y != y2 || dx > dy); e2 += dx) - tft.drawPixel(xlast, y2 += sy, TFT_WHITE); - if (xlast == x) break; - e2 = err; err -= dy; xlast += sx; - } - if (2 * e2 <= dy) { /* y step */ - for (e2 = dx - e2; e2 < ed * wd && (x != x2 || dx < dy); e2 += dy) - tft.drawPixel(x2 += sx, ylast, TFT_WHITE); - if (ylast == y) break; - err += dx; ylast += sy; - } + uint16_t the_color = TFT_WHITE; + uint16_t wd = 1; + int xlast2; + int ylast2; + int x2; + int y2; + int n; + int n2 = -wd; + xlast2 = xlast - wd; + x2 = x - wd; + for (n = -wd; n <= wd; n++) { + ylast2 = ylast + n; + y2 = y + n; + tft.drawLine(xlast2, ylast2, x2, y2, the_color); + } + for (n2 = -wd; n2 <= wd; n2++) { + xlast2 = xlast + n2; + x2 = x + n2; + tft.drawLine(xlast2, ylast2, x2, y2, the_color); + } + for (n = wd; n >= -wd; n--) { + ylast2 = ylast + n; + y2 = y + n; + tft.drawLine(xlast2, ylast2, x2, y2, the_color); + } + for (n2 = wd; n2 >= -wd; n2--) { + xlast2 = xlast + n2; + x2 = x + n2; + tft.drawLine(xlast2, ylast2, x2, y2, the_color); } // tft.drawLine(xlast, ylast, x, y, TFT_WHITE); } diff --git a/esp32_marauder/MenuFunctions.cpp b/esp32_marauder/MenuFunctions.cpp index a62805b..e25e8c2 100644 --- a/esp32_marauder/MenuFunctions.cpp +++ b/esp32_marauder/MenuFunctions.cpp @@ -1,587 +1,764 @@ -#include "MenuFunctions.h" -//#include "icons.h" - -extern const unsigned char menu_icons[][66]; - -MenuFunctions::MenuFunctions() -{ -} - -// Function to check menu input -void MenuFunctions::main(uint32_t currentTime) -{ - if ((wifi_scan_obj.currentScanMode == WIFI_SCAN_OFF) || - (wifi_scan_obj.currentScanMode == OTA_UPDATE) || - (wifi_scan_obj.currentScanMode == SHOW_INFO)) { - if (wifi_scan_obj.orient_display) { - this->orientDisplay(); - wifi_scan_obj.orient_display = false; - } - //if ((display_obj.current_banner_pos <= 0) || (display_obj.current_banner_pos == SCREEN_WIDTH)) - //{ - // this->drawStatusBar(); - //} - display_obj.updateBanner(current_menu->name); - } - - if (currentTime != 0) { - if (currentTime - initTime >= 100) { - this->initTime = millis(); - this->updateStatusBar(); - } - } - - //this->displayCurrentMenu(); - - boolean pressed = false; - // This is code from bodmer's keypad example - uint16_t t_x = 0, t_y = 0; // To store the touch coordinates - - // Get the display buffer out of the way - if ((wifi_scan_obj.currentScanMode != WIFI_SCAN_OFF ) && - (wifi_scan_obj.currentScanMode != WIFI_ATTACK_BEACON_SPAM) && - (wifi_scan_obj.currentScanMode != WIFI_ATTACK_RICK_ROLL)) - display_obj.displayBuffer(); - //Serial.println(wifi_scan_obj.freeRAM()); - - - // Pressed will be set true is there is a valid touch on the screen - int pre_getTouch = millis(); - - // getTouch causes a 10ms delay which makes beacon spam less effective - //if (wifi_scan_obj.currentScanMode == WIFI_SCAN_OFF) - pressed = display_obj.tft.getTouch(&t_x, &t_y); - - //if (pressed) - // Serial.println("Pressed, son"); - //boolean pressed = false; - - //Serial.print("getTouch: "); - //Serial.print(millis() - pre_getTouch); - //Serial.println("ms"); - - - // This is if there are scans/attacks going on - if ((wifi_scan_obj.currentScanMode != WIFI_SCAN_OFF) && - (pressed) && - (wifi_scan_obj.currentScanMode != OTA_UPDATE) && - (wifi_scan_obj.currentScanMode != SHOW_INFO)) - { - // Stop the current scan - if ((wifi_scan_obj.currentScanMode == WIFI_SCAN_PROBE) || - (wifi_scan_obj.currentScanMode == WIFI_SCAN_AP) || - (wifi_scan_obj.currentScanMode == WIFI_SCAN_PWN) || - (wifi_scan_obj.currentScanMode == WIFI_SCAN_ESPRESSIF) || - (wifi_scan_obj.currentScanMode == WIFI_SCAN_ALL) || - (wifi_scan_obj.currentScanMode == WIFI_SCAN_DEAUTH) || - (wifi_scan_obj.currentScanMode == WIFI_ATTACK_BEACON_SPAM) || - (wifi_scan_obj.currentScanMode == WIFI_ATTACK_RICK_ROLL) || - (wifi_scan_obj.currentScanMode == BT_SCAN_ALL) || - (wifi_scan_obj.currentScanMode == BT_SCAN_SKIMMERS)) - { - Serial.println("Stopping scan..."); - wifi_scan_obj.StartScan(WIFI_SCAN_OFF); - - // If we don't do this, the text and button coordinates will be off - display_obj.tft.init(); - - // Take us back to the menu - changeMenu(current_menu); - } - - x = -1; - y = -1; - - return; - } - - // Check if any key coordinate boxes contain the touch coordinates - // This is for when on a menu - if ((wifi_scan_obj.currentScanMode != WIFI_ATTACK_BEACON_SPAM) && - (wifi_scan_obj.currentScanMode != WIFI_ATTACK_RICK_ROLL)) - { - // Need this to set all keys to false - for (uint8_t b = 0; b < BUTTON_ARRAY_LEN; b++) { - if (pressed && display_obj.key[b].contains(t_x, t_y)) { - display_obj.key[b].press(true); // tell the button it is pressed - } else { - display_obj.key[b].press(false); // tell the button it is NOT pressed - } - } - - // Check if any key has changed state - for (uint8_t b = 0; b < current_menu->list->size(); b++) { - display_obj.tft.setFreeFont(MENU_FONT); - if (display_obj.key[b].justPressed()) { - //display_obj.key[b].drawButton2(current_menu->list->get(b).name, true); // draw invert - //display_obj.key[b].drawButton(ML_DATUM, BUTTON_PADDING, current_menu->list->get(b).name, true); - display_obj.key[b].drawButton(true, current_menu->list->get(b).name); - if (current_menu->list->get(b).name != "Back") - display_obj.tft.drawXBitmap(0, - KEY_Y + b * (KEY_H + KEY_SPACING_Y) - (ICON_H / 2), - menu_icons[current_menu->list->get(b).icon], - ICON_W, - ICON_H, - current_menu->list->get(b).color, - TFT_BLACK); - } - //else if (pressed) - // display_obj.key[b].drawButton(false, current_menu->list->get(b).name); - - // If button was just release, execute the button's function - if ((display_obj.key[b].justReleased()) && (!pressed)) - { - //display_obj.key[b].drawButton2(current_menu->list->get(b).name); // draw normal - //display_obj.key[b].drawButton(ML_DATUM, BUTTON_PADDING, current_menu->list->get(b).name); - display_obj.key[b].drawButton(false, current_menu->list->get(b).name); - current_menu->list->get(b).callable(); - } - // This - else if ((display_obj.key[b].justReleased()) && (pressed)) { - display_obj.key[b].drawButton(false, current_menu->list->get(b).name); - if (current_menu->list->get(b).name != "Back") - display_obj.tft.drawXBitmap(0, - KEY_Y + b * (KEY_H + KEY_SPACING_Y) - (ICON_H / 2), - menu_icons[current_menu->list->get(b).icon], - ICON_W, - ICON_H, - TFT_BLACK, - current_menu->list->get(b).color); - } - - display_obj.tft.setFreeFont(NULL); - } - } - x = -1; - y = -1; -} - -void MenuFunctions::updateStatusBar() -{ - uint16_t the_color; - - // Draw temp info - if (temp_obj.current_temp < 70) - the_color = TFT_GREEN; - else if ((temp_obj.current_temp >= 70) && (temp_obj.current_temp < 80)) - the_color = TFT_YELLOW; - else if ((temp_obj.current_temp >= 80) && (temp_obj.current_temp < 90)) - the_color = TFT_ORANGE; - else if ((temp_obj.current_temp >= 90) && (temp_obj.current_temp < 100)) - the_color = TFT_RED; - else - the_color = TFT_MAROON; - - display_obj.tft.setTextColor(the_color, STATUSBAR_COLOR); - if (temp_obj.current_temp != temp_obj.old_temp) { - temp_obj.old_temp = temp_obj.current_temp; - display_obj.tft.fillRect(0, 0, 50, STATUS_BAR_WIDTH, STATUSBAR_COLOR); - display_obj.tft.drawString((String)temp_obj.current_temp + " C", 4, 0, 2); - } - display_obj.tft.setTextColor(TFT_WHITE, STATUSBAR_COLOR); - - // WiFi Channel Stuff - if (wifi_scan_obj.set_channel != wifi_scan_obj.old_channel) { - wifi_scan_obj.old_channel = wifi_scan_obj.set_channel; - display_obj.tft.fillRect(50, 0, 50, STATUS_BAR_WIDTH, STATUSBAR_COLOR); - display_obj.tft.drawString("CH: " + (String)wifi_scan_obj.set_channel, 50, 0, 2); - } - - // RAM Stuff - wifi_scan_obj.freeRAM(); - if (wifi_scan_obj.free_ram != wifi_scan_obj.old_free_ram) { - wifi_scan_obj.old_free_ram = wifi_scan_obj.free_ram; - display_obj.tft.fillRect(100, 0, 60, STATUS_BAR_WIDTH, STATUSBAR_COLOR); - display_obj.tft.drawString((String)wifi_scan_obj.free_ram + "B", 100, 0, 2); - } - - // Draw battery info - if (battery_obj.i2c_supported) - { - if ((String)battery_obj.battery_level != "25") - the_color = TFT_GREEN; - else - the_color = TFT_RED; - - if (battery_obj.battery_level != battery_obj.old_level) { - battery_obj.old_level = battery_obj.battery_level; - display_obj.tft.fillRect(204, 0, SCREEN_WIDTH, STATUS_BAR_WIDTH, STATUSBAR_COLOR); - display_obj.tft.setCursor(0, 1); - display_obj.tft.drawXBitmap(186, - 0, - menu_icons[STATUS_BAT], - 16, - 16, - STATUSBAR_COLOR, - the_color); - display_obj.tft.drawString((String)battery_obj.battery_level + "%", 204, 0, 2); - } - } - - // Draw SD info - if (sd_obj.supported) - the_color = TFT_GREEN; - else - the_color = TFT_RED; - - display_obj.tft.drawXBitmap(170, - 0, - menu_icons[STATUS_SD], - 16, - 16, - STATUSBAR_COLOR, - the_color); - //display_obj.tft.print((String)battery_obj.battery_level + "%"); -} - -void MenuFunctions::drawStatusBar() -{ - display_obj.tft.fillRect(0, 0, 240, STATUS_BAR_WIDTH, STATUSBAR_COLOR); - //display_obj.tft.fillRect(0, STATUS_BAR_WIDTH + 1, 240, 1, TFT_DARKGREY); - display_obj.tft.setTextColor(TFT_WHITE, STATUSBAR_COLOR); - //display_obj.tft.setTextSize(2); - - uint16_t the_color; - - // Draw temp info - if (temp_obj.current_temp < 70) - the_color = TFT_GREEN; - else if ((temp_obj.current_temp >= 70) && (temp_obj.current_temp < 80)) - the_color = TFT_YELLOW; - else if ((temp_obj.current_temp >= 80) && (temp_obj.current_temp < 90)) - the_color = TFT_ORANGE; - else if ((temp_obj.current_temp >= 90) && (temp_obj.current_temp < 100)) - the_color = TFT_RED; - else - the_color = TFT_MAROON; - - display_obj.tft.setTextColor(the_color, STATUSBAR_COLOR); - temp_obj.old_temp = temp_obj.current_temp; - display_obj.tft.fillRect(0, 0, 50, STATUS_BAR_WIDTH, STATUSBAR_COLOR); - display_obj.tft.drawString((String)temp_obj.current_temp + " C", 4, 0, 2); - display_obj.tft.setTextColor(TFT_WHITE, STATUSBAR_COLOR); - - - // WiFi Channel Stuff - wifi_scan_obj.old_channel = wifi_scan_obj.set_channel; - display_obj.tft.fillRect(50, 0, 50, STATUS_BAR_WIDTH, STATUSBAR_COLOR); - display_obj.tft.drawString("CH: " + (String)wifi_scan_obj.set_channel, 50, 0, 2); - - // RAM Stuff - wifi_scan_obj.freeRAM(); - wifi_scan_obj.old_free_ram = wifi_scan_obj.free_ram; - display_obj.tft.fillRect(100, 0, 60, STATUS_BAR_WIDTH, STATUSBAR_COLOR); - display_obj.tft.drawString((String)wifi_scan_obj.free_ram + "B", 100, 0, 2); - - // Draw battery info - if (battery_obj.i2c_supported) - { - if ((String)battery_obj.battery_level != "25") - the_color = TFT_GREEN; - else - the_color = TFT_RED; - - battery_obj.old_level = battery_obj.battery_level; - display_obj.tft.fillRect(204, 0, SCREEN_WIDTH, STATUS_BAR_WIDTH, STATUSBAR_COLOR); - display_obj.tft.setCursor(0, 1); - display_obj.tft.drawXBitmap(186, - 0, - menu_icons[STATUS_BAT], - 16, - 16, - STATUSBAR_COLOR, - the_color); - display_obj.tft.drawString((String)battery_obj.battery_level + "%", 204, 0, 2); - } - - // Draw SD info - if (sd_obj.supported) - the_color = TFT_GREEN; - else - the_color = TFT_RED; - - display_obj.tft.drawXBitmap(170, - 0, - menu_icons[STATUS_SD], - 16, - 16, - STATUSBAR_COLOR, - the_color); - //display_obj.tft.print((String)battery_obj.battery_level + "%"); -} - -void MenuFunctions::orientDisplay() -{ - display_obj.tft.init(); - - display_obj.tft.setRotation(0); // Portrait - - display_obj.tft.setCursor(0, 0); - - //uint16_t calData[5] = { 275, 3494, 361, 3528, 4 }; // tft.setRotation(0); // Portrait - //uint16_t calData[5] = { 339, 3470, 237, 3438, 2 }; // tft.setRotation(0); // Portrait with DIY TFT - - #ifdef TFT_SHIELD - uint16_t calData[5] = { 275, 3494, 361, 3528, 4 }; // tft.setRotation(0); // Portrait with TFT Shield - Serial.println("Using TFT Shield"); - #else if defined(TFT_DIY) - uint16_t calData[5] = { 339, 3470, 237, 3438, 2 }; // tft.setRotation(0); // Portrait with DIY TFT - Serial.println("Using TFT DIY"); - #endif - - display_obj.tft.setTouch(calData); - - //display_obj.clearScreen(); - - changeMenu(current_menu); -} - - -// Function to build the menus -void MenuFunctions::RunSetup() -{ - // root menu stuff - mainMenu.list = new LinkedList(); // Get list in first menu ready - - // Main menu stuff - wifiMenu.list = new LinkedList(); // Get list in second menu ready - bluetoothMenu.list = new LinkedList(); // Get list in third menu ready - generalMenu.list = new LinkedList(); - deviceMenu.list = new LinkedList(); - - // Device menu stuff - failedUpdateMenu.list = new LinkedList(); - whichUpdateMenu.list = new LinkedList(); - confirmMenu.list = new LinkedList(); - updateMenu.list = new LinkedList(); - infoMenu.list = new LinkedList(); - - // WiFi menu stuff - wifiSnifferMenu.list = new LinkedList(); - wifiScannerMenu.list = new LinkedList(); - wifiAttackMenu.list = new LinkedList(); - - // Bluetooth menu stuff - bluetoothSnifferMenu.list = new LinkedList(); - bluetoothScannerMenu.list = new LinkedList(); - - // Work menu names - mainMenu.name = " ESP32 Marauder "; - wifiMenu.name = " WiFi "; - deviceMenu.name = " Device "; - generalMenu.name = " General Apps "; - failedUpdateMenu.name = " Updating... "; - whichUpdateMenu.name = "Select Method "; - confirmMenu.name = " Confirm Update "; - updateMenu.name = " Update Firmware "; - infoMenu.name = " Device Info "; - bluetoothMenu.name = " Bluetooth "; - wifiSnifferMenu.name = " WiFi Sniffers "; - wifiScannerMenu.name = " WiFi Scanners"; - wifiAttackMenu.name = " WiFi Attacks "; - bluetoothSnifferMenu.name = " Bluetooth Sniffers "; - bluetoothScannerMenu.name = " Bluetooth Scanners "; - - // Build Main Menu - mainMenu.parentMenu = NULL; - addNodes(&mainMenu, "WiFi", TFT_GREEN, NULL, WIFI, [this](){changeMenu(&wifiMenu);}); - addNodes(&mainMenu, "Bluetooth", TFT_CYAN, NULL, BLUETOOTH, [this](){changeMenu(&bluetoothMenu);}); - addNodes(&mainMenu, "General Apps", TFT_MAGENTA, NULL, GENERAL_APPS, [this](){changeMenu(&generalMenu);}); - addNodes(&mainMenu, "Device", TFT_BLUE, NULL, DEVICE, [this](){changeMenu(&deviceMenu);}); - addNodes(&mainMenu, "Reboot", TFT_LIGHTGREY, NULL, REBOOT, [](){ESP.restart();}); - - // Build WiFi Menu - wifiMenu.parentMenu = &mainMenu; // Main Menu is second menu parent - addNodes(&wifiMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this](){changeMenu(wifiMenu.parentMenu);}); - addNodes(&wifiMenu, "Sniffers", TFT_YELLOW, NULL, SNIFFERS, [this](){changeMenu(&wifiSnifferMenu);}); - addNodes(&wifiMenu, "Scanners", TFT_ORANGE, NULL, SCANNERS, [this](){changeMenu(&wifiScannerMenu);}); - addNodes(&wifiMenu, "Attacks", TFT_RED, NULL, ATTACKS, [this](){changeMenu(&wifiAttackMenu);}); - - // Build WiFi sniffer Menu - wifiSnifferMenu.parentMenu = &wifiMenu; // Main Menu is second menu parent - addNodes(&wifiSnifferMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this](){changeMenu(wifiSnifferMenu.parentMenu);}); - addNodes(&wifiSnifferMenu, "Probe Request Sniff", TFT_CYAN, NULL, PROBE_SNIFF, [this](){display_obj.clearScreen(); this->drawStatusBar(); wifi_scan_obj.StartScan(WIFI_SCAN_PROBE, TFT_CYAN);}); - addNodes(&wifiSnifferMenu, "Beacon Sniff", TFT_MAGENTA, NULL, BEACON_SNIFF, [this](){display_obj.clearScreen(); this->drawStatusBar(); wifi_scan_obj.StartScan(WIFI_SCAN_AP, TFT_MAGENTA);}); - addNodes(&wifiSnifferMenu, "Deauth Sniff", TFT_RED, NULL, DEAUTH_SNIFF, [this](){display_obj.clearScreen(); this->drawStatusBar(); wifi_scan_obj.StartScan(WIFI_SCAN_DEAUTH, TFT_RED);}); - - // Build WiFi scanner Menu - wifiScannerMenu.parentMenu = &wifiMenu; // Main Menu is second menu parent - addNodes(&wifiScannerMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this](){changeMenu(wifiScannerMenu.parentMenu);}); - addNodes(&wifiScannerMenu, "Packet Monitor", TFT_BLUE, NULL, PACKET_MONITOR, [this](){wifi_scan_obj.StartScan(WIFI_PACKET_MONITOR, TFT_BLUE);}); - addNodes(&wifiScannerMenu, "EAPOL/PMKID Scan", TFT_VIOLET, NULL, EAPOL, [this](){wifi_scan_obj.StartScan(WIFI_SCAN_EAPOL, TFT_VIOLET);}); - addNodes(&wifiScannerMenu, "Detect Pwnagotchi", TFT_RED, NULL, PWNAGOTCHI, [this](){display_obj.clearScreen(); this->drawStatusBar(); wifi_scan_obj.StartScan(WIFI_SCAN_PWN, TFT_RED);}); - addNodes(&wifiScannerMenu, "Detect Espressif", TFT_ORANGE, NULL, ESPRESSIF, [this](){display_obj.clearScreen(); this->drawStatusBar(); wifi_scan_obj.StartScan(WIFI_SCAN_ESPRESSIF, TFT_ORANGE);}); - - // Build WiFi attack menu - wifiAttackMenu.parentMenu = &wifiMenu; // Main Menu is second menu parent - addNodes(&wifiAttackMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this](){changeMenu(wifiAttackMenu.parentMenu);}); - addNodes(&wifiAttackMenu, "Beacon Spam Random", TFT_ORANGE, NULL, BEACON_SPAM, [this](){display_obj.clearScreen(); this->drawStatusBar(); wifi_scan_obj.StartScan(WIFI_ATTACK_BEACON_SPAM, TFT_ORANGE);}); - addNodes(&wifiAttackMenu, "Rick Roll Beacon", TFT_YELLOW, NULL, RICK_ROLL, [this](){display_obj.clearScreen(); this->drawStatusBar(); wifi_scan_obj.StartScan(WIFI_ATTACK_RICK_ROLL, TFT_YELLOW);}); - - // Build Bluetooth Menu - bluetoothMenu.parentMenu = &mainMenu; // Second Menu is third menu parent - addNodes(&bluetoothMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this](){changeMenu(bluetoothMenu.parentMenu);}); - addNodes(&bluetoothMenu, "Sniffers", TFT_YELLOW, NULL, SNIFFERS, [this](){changeMenu(&bluetoothSnifferMenu);}); - addNodes(&bluetoothMenu, "Scanners", TFT_ORANGE, NULL, SCANNERS, [this](){changeMenu(&bluetoothScannerMenu);}); - - // Build bluetooth sniffer Menu - bluetoothSnifferMenu.parentMenu = &bluetoothMenu; // Second Menu is third menu parent - addNodes(&bluetoothSnifferMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this](){changeMenu(bluetoothSnifferMenu.parentMenu);}); - addNodes(&bluetoothSnifferMenu, "Bluetooth Sniffer", TFT_GREEN, NULL, BLUETOOTH_SNIFF, [this](){display_obj.clearScreen(); this->drawStatusBar(); wifi_scan_obj.StartScan(BT_SCAN_ALL, TFT_GREEN);}); - - // Build bluetooth scanner Menu - bluetoothScannerMenu.parentMenu = &bluetoothMenu; // Second Menu is third menu parent - addNodes(&bluetoothScannerMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this](){changeMenu(bluetoothScannerMenu.parentMenu);}); - addNodes(&bluetoothScannerMenu, "Detect Card Skimmers", TFT_MAGENTA, NULL, CC_SKIMMERS, [this](){display_obj.clearScreen(); this->drawStatusBar(); wifi_scan_obj.StartScan(BT_SCAN_SKIMMERS, TFT_MAGENTA);}); - - // General apps menu - generalMenu.parentMenu = &mainMenu; - addNodes(&generalMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this](){display_obj.draw_tft = false; changeMenu(generalMenu.parentMenu);}); - addNodes(&generalMenu, "Draw", TFT_WHITE, NULL, DRAW, [this](){display_obj.clearScreen(); display_obj.draw_tft = true;}); - - // Device menu - deviceMenu.parentMenu = &mainMenu; - addNodes(&deviceMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this](){changeMenu(deviceMenu.parentMenu);}); - //addNodes(&deviceMenu, "Update Firmware", TFT_ORANGE, NULL, UPDATE, [this](){wifi_scan_obj.currentScanMode = OTA_UPDATE; changeMenu(&updateMenu); web_obj.setupOTAupdate();}); - addNodes(&deviceMenu, "Update Firmware", TFT_ORANGE, NULL, UPDATE, [this](){wifi_scan_obj.currentScanMode = OTA_UPDATE; changeMenu(&whichUpdateMenu);}); - addNodes(&deviceMenu, "Device Info", TFT_WHITE, NULL, DEVICE_INFO, [this](){wifi_scan_obj.currentScanMode = SHOW_INFO; changeMenu(&infoMenu); wifi_scan_obj.RunInfo();}); - //addNodes(&deviceMenu, "Join WiFi", TFT_YELLOW, NULL, SNIFFERS, [this](){display_obj.clearScreen(); wifi_scan_obj.currentScanMode = LV_JOIN_WIFI; wifi_scan_obj.StartScan(LV_JOIN_WIFI, TFT_YELLOW);}); - - // Select update - whichUpdateMenu.parentMenu = &deviceMenu; - addNodes(&whichUpdateMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this](){changeMenu(whichUpdateMenu.parentMenu);}); - addNodes(&whichUpdateMenu, "Web Update", TFT_GREEN, NULL, WEB_UPDATE, [this](){wifi_scan_obj.currentScanMode = OTA_UPDATE; changeMenu(&updateMenu); web_obj.setupOTAupdate();}); - if (sd_obj.supported) addNodes(&whichUpdateMenu, "SD Update", TFT_MAGENTA, NULL, SD_UPDATE, [this](){wifi_scan_obj.currentScanMode = OTA_UPDATE; changeMenu(&confirmMenu);}); - - // Confirm SD update menu - confirmMenu.parentMenu = &whichUpdateMenu; - addNodes(&confirmMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this](){changeMenu(confirmMenu.parentMenu);}); - //addNodes(&confirmMenu, "Yes", TFT_ORANGE, NULL, UPDATE, [this](){wifi_scan_obj.currentScanMode = OTA_UPDATE; changeMenu(&updateMenu); sd_obj.runUpdate();}); - addNodes(&confirmMenu, "Yes", TFT_ORANGE, NULL, UPDATE, [this](){wifi_scan_obj.currentScanMode = OTA_UPDATE; changeMenu(&failedUpdateMenu); sd_obj.runUpdate();}); - - // Web Update - updateMenu.parentMenu = &deviceMenu; - addNodes(&updateMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this](){wifi_scan_obj.currentScanMode = WIFI_SCAN_OFF; changeMenu(updateMenu.parentMenu); WiFi.softAPdisconnect(true); web_obj.shutdownServer();}); - //addNodes(&updateMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this](){wifi_scan_obj.currentScanMode = WIFI_SCAN_OFF; changeMenu(updateMenu.parentMenu);}); - - // Failed update menu - failedUpdateMenu.parentMenu = &whichUpdateMenu; - addNodes(&failedUpdateMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this](){wifi_scan_obj.currentScanMode = WIFI_SCAN_OFF; changeMenu(failedUpdateMenu.parentMenu);}); - - // Device info menu - infoMenu.parentMenu = &deviceMenu; - addNodes(&infoMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this](){wifi_scan_obj.currentScanMode = WIFI_SCAN_OFF; changeMenu(infoMenu.parentMenu);}); - - // Set the current menu to the mainMenu - changeMenu(&mainMenu); - - this->initTime = millis(); -} - -// Function to change menu -void MenuFunctions::changeMenu(Menu* menu) -{ - display_obj.initScrollValues(); - display_obj.setupScrollArea(TOP_FIXED_AREA, BOT_FIXED_AREA); - display_obj.tft.init(); - current_menu = menu; - - buildButtons(menu); - - displayCurrentMenu(); -} - -// Function to show all MenuNodes in a Menu -void MenuFunctions::showMenuList(Menu* menu, int layer) -{ - // Iterate through all of the menu nodes in the menu - for (int i = 0; i < menu->list->size(); i++) - { - // Depending on layer, indent - for (int x = 0; x < layer * 4; x++) - Serial.print(" "); - Serial.print("Node: "); - Serial.println(menu->list->get(i).name); - - // If the current menu node points to another menu, list that menu - //if (menu->list->get(i).childMenu != NULL) - // showMenuList(menu->list->get(i).childMenu, layer+1); - } - Serial.println(); -} - - -// Function to add MenuNodes to a menu -void MenuFunctions::addNodes(Menu* menu, String name, uint16_t color, Menu* child, int place, std::function callable) -{ - TFT_eSPI_Button new_button; - menu->list->add(MenuNode{name, color, place, &new_button, callable}); - //strcpy(menu->list->get(-1).icon, bluetooth_icon); -} - -void MenuFunctions::buildButtons(Menu* menu) -{ - Serial.println("Bulding buttons..."); - if (menu->list != NULL) - { - //for (int i = 0; i < sizeof(key); i++) - // key[i] = NULL; - for (int i = 0; i < menu->list->size(); i++) - { - TFT_eSPI_Button new_button; - char buf[menu->list->get(i).name.length() + 1] = {}; - menu->list->get(i).name.toCharArray(buf, menu->list->get(i).name.length() + 1); - display_obj.key[i].initButton(&display_obj.tft, - KEY_X + 0 * (KEY_W + KEY_SPACING_X), - KEY_Y + i * (KEY_H + KEY_SPACING_Y), // x, y, w, h, outline, fill, text - KEY_W, - KEY_H, - TFT_BLACK, // Outline - TFT_BLACK, // Fill - menu->list->get(i).color, // Text - buf, - KEY_TEXTSIZE); - - display_obj.key[i].setLabelDatum(BUTTON_PADDING - (KEY_W/2), 2, ML_DATUM); - } - } -} - - -void MenuFunctions::displayCurrentMenu() -{ - Serial.println("Displaying current menu..."); - display_obj.clearScreen(); - display_obj.tft.setTextColor(TFT_LIGHTGREY, TFT_DARKGREY); - this->drawStatusBar(); - //display_obj.tft.fillRect(0,0,240,16, TFT_DARKGREY); - //display_obj.tft.drawCentreString(" ESP32 Marauder ",120,0,2); - //Serial.println("Getting size..."); - //char buf[¤t_menu->parentMenu->name.length() + 1] = {}; - //Serial.println("Got size..."); - //current_menu->parentMenu->name.toCharArray(buf, current_menu->parentMenu->name.length() + 1); - //String current_name = ¤t_menu->parentMenu->name; - //Serial.println("gottem"); - //display_obj.tft.drawCentreString(current_menu->name,120,0,2); - if (current_menu->list != NULL) - { - display_obj.tft.setFreeFont(MENU_FONT); - for (int i = 0; i < current_menu->list->size(); i++) - { - //display_obj.key[i].drawButton2(current_menu->list->get(i).name); - //display_obj.key[i].drawButton(ML_DATUM, BUTTON_PADDING, current_menu->list->get(i).name); - //display_obj.key[i].drawButton(true); - display_obj.key[i].drawButton(false, current_menu->list->get(i).name); - - if (current_menu->list->get(i).name != "Back") - display_obj.tft.drawXBitmap(0, - KEY_Y + i * (KEY_H + KEY_SPACING_Y) - (ICON_H / 2), - menu_icons[current_menu->list->get(i).icon], - ICON_W, - ICON_H, - TFT_BLACK, - current_menu->list->get(i).color); - } - display_obj.tft.setFreeFont(NULL); - } -} +#include "MenuFunctions.h" +//#include "icons.h" + +extern const unsigned char menu_icons[][66]; + +MenuFunctions::MenuFunctions() +{ +} + +// Function to check menu input +void MenuFunctions::main(uint32_t currentTime) +{ + if ((wifi_scan_obj.currentScanMode == WIFI_SCAN_OFF) || + (wifi_scan_obj.currentScanMode == OTA_UPDATE) || + (wifi_scan_obj.currentScanMode == SHOW_INFO)) { + if (wifi_scan_obj.orient_display) { + this->orientDisplay(); + wifi_scan_obj.orient_display = false; + } + //if ((display_obj.current_banner_pos <= 0) || (display_obj.current_banner_pos == SCREEN_WIDTH)) + //{ + // this->drawStatusBar(); + //} + display_obj.updateBanner(current_menu->name); + } + + if (currentTime != 0) { + if (currentTime - initTime >= 100) { + this->initTime = millis(); + this->updateStatusBar(); + } + } + + //this->displayCurrentMenu(); + + boolean pressed = false; + // This is code from bodmer's keypad example + uint16_t t_x = 0, t_y = 0; // To store the touch coordinates + + // Get the display buffer out of the way + if ((wifi_scan_obj.currentScanMode != WIFI_SCAN_OFF ) && + (wifi_scan_obj.currentScanMode != WIFI_ATTACK_BEACON_SPAM) && + (wifi_scan_obj.currentScanMode != WIFI_ATTACK_RICK_ROLL)) + display_obj.displayBuffer(); + //Serial.println(wifi_scan_obj.freeRAM()); + + + // Pressed will be set true is there is a valid touch on the screen + int pre_getTouch = millis(); + + // getTouch causes a 10ms delay which makes beacon spam less effective + //if (wifi_scan_obj.currentScanMode == WIFI_SCAN_OFF) + pressed = display_obj.tft.getTouch(&t_x, &t_y); + + //if (pressed) + // Serial.println("Pressed, son"); + //boolean pressed = false; + + //Serial.print("getTouch: "); + //Serial.print(millis() - pre_getTouch); + //Serial.println("ms"); + + + // This is if there are scans/attacks going on + if ((wifi_scan_obj.currentScanMode != WIFI_SCAN_OFF) && + (pressed) && + (wifi_scan_obj.currentScanMode != OTA_UPDATE) && + (wifi_scan_obj.currentScanMode != SHOW_INFO)) + { + // Stop the current scan + if ((wifi_scan_obj.currentScanMode == WIFI_SCAN_PROBE) || + (wifi_scan_obj.currentScanMode == WIFI_SCAN_AP) || + (wifi_scan_obj.currentScanMode == WIFI_SCAN_PWN) || + (wifi_scan_obj.currentScanMode == WIFI_SCAN_ESPRESSIF) || + (wifi_scan_obj.currentScanMode == WIFI_SCAN_ALL) || + (wifi_scan_obj.currentScanMode == WIFI_SCAN_DEAUTH) || + (wifi_scan_obj.currentScanMode == WIFI_ATTACK_BEACON_SPAM) || + (wifi_scan_obj.currentScanMode == WIFI_ATTACK_RICK_ROLL) || + (wifi_scan_obj.currentScanMode == BT_SCAN_ALL) || + (wifi_scan_obj.currentScanMode == BT_SCAN_SKIMMERS)) + { + Serial.println("Stopping scan..."); + wifi_scan_obj.StartScan(WIFI_SCAN_OFF); + + // If we don't do this, the text and button coordinates will be off + display_obj.tft.init(); + + // Take us back to the menu + changeMenu(current_menu); + } + + x = -1; + y = -1; + + return; + } + + // Check if any key coordinate boxes contain the touch coordinates + // This is for when on a menu + if ((wifi_scan_obj.currentScanMode != WIFI_ATTACK_BEACON_SPAM) && + (wifi_scan_obj.currentScanMode != WIFI_ATTACK_RICK_ROLL)) + { + // Need this to set all keys to false + for (uint8_t b = 0; b < BUTTON_ARRAY_LEN; b++) { + if (pressed && display_obj.key[b].contains(t_x, t_y)) { + display_obj.key[b].press(true); // tell the button it is pressed + } else { + display_obj.key[b].press(false); // tell the button it is NOT pressed + } + } + + // Check if any key has changed state + for (uint8_t b = 0; b < current_menu->list->size(); b++) { + display_obj.tft.setFreeFont(MENU_FONT); + if (display_obj.key[b].justPressed()) { + //display_obj.key[b].drawButton2(current_menu->list->get(b).name, true); // draw invert + //display_obj.key[b].drawButton(ML_DATUM, BUTTON_PADDING, current_menu->list->get(b).name, true); + display_obj.key[b].drawButton(true, current_menu->list->get(b).name); + if (current_menu->list->get(b).name != "Back") + display_obj.tft.drawXBitmap(0, + KEY_Y + b * (KEY_H + KEY_SPACING_Y) - (ICON_H / 2), + menu_icons[current_menu->list->get(b).icon], + ICON_W, + ICON_H, + current_menu->list->get(b).color, + TFT_BLACK); + } + //else if (pressed) + // display_obj.key[b].drawButton(false, current_menu->list->get(b).name); + + // If button was just release, execute the button's function + if ((display_obj.key[b].justReleased()) && (!pressed)) + { + //display_obj.key[b].drawButton2(current_menu->list->get(b).name); // draw normal + //display_obj.key[b].drawButton(ML_DATUM, BUTTON_PADDING, current_menu->list->get(b).name); + display_obj.key[b].drawButton(false, current_menu->list->get(b).name); + current_menu->list->get(b).callable(); + } + // This + else if ((display_obj.key[b].justReleased()) && (pressed)) { + display_obj.key[b].drawButton(false, current_menu->list->get(b).name); + if (current_menu->list->get(b).name != "Back") + display_obj.tft.drawXBitmap(0, + KEY_Y + b * (KEY_H + KEY_SPACING_Y) - (ICON_H / 2), + menu_icons[current_menu->list->get(b).icon], + ICON_W, + ICON_H, + TFT_BLACK, + current_menu->list->get(b).color); + } + + display_obj.tft.setFreeFont(NULL); + } + } + x = -1; + y = -1; +} + +#if BATTERY_ANALOG_ON == 1 +byte battery_analog_array[10]; +byte battery_count = 0; +byte battery_analog_last = 101; +#define BATTERY_CHECK 50 +uint16_t battery_analog = 0; +void MenuFunctions::battery(bool initial) +{ + if (BATTERY_ANALOG_ON) { + uint8_t n = 0; + byte battery_analog_sample[10]; + byte deviation; + if (battery_count == BATTERY_CHECK - 5) digitalWrite(BATTERY_PIN, HIGH); + else if (battery_count == 5) digitalWrite(BATTERY_PIN, LOW); + if (battery_count == 0) { + battery_analog = 0; + for (n = 9; n > 0; n--)battery_analog_array[n] = battery_analog_array[n - 1]; + for (n = 0; n < 10; n++) { + battery_analog_sample[n] = map((analogRead(ANALOG_PIN) * 5), 2400, 4200, 0, 100); + if (battery_analog_sample[n] > 100) battery_analog_sample[n] = 100; + else if (battery_analog_sample[n] < 0) battery_analog_sample[n] = 0; + battery_analog += battery_analog_sample[n]; + } + battery_analog = battery_analog / 10; + for (n = 0; n < 10; n++) { + deviation = abs(battery_analog - battery_analog_sample[n]); + if (deviation >= 10) battery_analog_sample[n] = battery_analog; + } + battery_analog = 0; + for (n = 0; n < 10; n++) battery_analog += battery_analog_sample[n]; + battery_analog = battery_analog / 10; + battery_analog_array[0] = battery_analog; + if (battery_analog_array[9] > 0 ) { + battery_analog = 0; + for (n = 0; n < 10; n++) battery_analog += battery_analog_array[n]; + battery_analog = battery_analog / 10; + } + battery_count ++; + } + else if (battery_count < BATTERY_CHECK) battery_count++; + else if (battery_count >= BATTERY_CHECK) battery_count = 0; + + if (battery_analog_last != battery_analog) { + battery_analog_last = battery_analog; + MenuFunctions::battery2(); + } + } +} +void MenuFunctions::battery2(bool initial) +{ + uint16_t the_color; + if ( digitalRead(CHARGING_PIN) == 1) the_color = TFT_BLUE; + else if (battery_analog < 20) the_color = TFT_RED; + else if (battery_analog < 40) the_color = TFT_YELLOW; + else the_color = TFT_GREEN; + + display_obj.tft.setTextColor(the_color, STATUSBAR_COLOR); + display_obj.tft.fillRect(186, 0, 50, STATUS_BAR_WIDTH, STATUSBAR_COLOR); + display_obj.tft.drawXBitmap(186, + 0, + menu_icons[STATUS_BAT], + 16, + 16, + STATUSBAR_COLOR, + the_color); + display_obj.tft.drawString((String) battery_analog + "%", 204, 0, 2); +} +#else +void MenuFunctions::battery(bool initial) +{ + uint16_t the_color; + if (battery_obj.i2c_supported) + { + // Could use int compare maybe idk + if (((String)battery_obj.battery_level != "25") && ((String)battery_obj.battery_level != "0")) + the_color = TFT_GREEN; + else + the_color = TFT_RED; + + if ((battery_obj.battery_level != battery_obj.old_level) || (initial)) { + battery_obj.old_level = battery_obj.battery_level; + display_obj.tft.fillRect(204, 0, SCREEN_WIDTH, STATUS_BAR_WIDTH, STATUSBAR_COLOR); + display_obj.tft.setCursor(0, 1); + display_obj.tft.drawXBitmap(186, + 0, + menu_icons[STATUS_BAT], + 16, + 16, + STATUSBAR_COLOR, + the_color); + display_obj.tft.drawString((String)battery_obj.battery_level + "%", 204, 0, 2); + } + } +} +void MenuFunctions::battery2(bool initial) +{ + MenuFunctions::battery(initial); +} +#endif + +void MenuFunctions::updateStatusBar() +{ + uint16_t the_color; + + // Draw temp info + if (temp_obj.current_temp < 70) + the_color = TFT_GREEN; + else if ((temp_obj.current_temp >= 70) && (temp_obj.current_temp < 80)) + the_color = TFT_YELLOW; + else if ((temp_obj.current_temp >= 80) && (temp_obj.current_temp < 90)) + the_color = TFT_ORANGE; + else if ((temp_obj.current_temp >= 90) && (temp_obj.current_temp < 100)) + the_color = TFT_RED; + else + the_color = TFT_MAROON; + + display_obj.tft.setTextColor(the_color, STATUSBAR_COLOR); + if (temp_obj.current_temp != temp_obj.old_temp) { + temp_obj.old_temp = temp_obj.current_temp; + display_obj.tft.fillRect(0, 0, 50, STATUS_BAR_WIDTH, STATUSBAR_COLOR); + display_obj.tft.drawString((String)temp_obj.current_temp + " C", 4, 0, 2); + } + display_obj.tft.setTextColor(TFT_WHITE, STATUSBAR_COLOR); + + // WiFi Channel Stuff + if (wifi_scan_obj.set_channel != wifi_scan_obj.old_channel) { + wifi_scan_obj.old_channel = wifi_scan_obj.set_channel; + display_obj.tft.fillRect(50, 0, 50, STATUS_BAR_WIDTH, STATUSBAR_COLOR); + display_obj.tft.drawString("CH: " + (String)wifi_scan_obj.set_channel, 50, 0, 2); + } + + // RAM Stuff + wifi_scan_obj.freeRAM(); + if (wifi_scan_obj.free_ram != wifi_scan_obj.old_free_ram) { + wifi_scan_obj.old_free_ram = wifi_scan_obj.free_ram; + display_obj.tft.fillRect(100, 0, 60, STATUS_BAR_WIDTH, STATUSBAR_COLOR); + display_obj.tft.drawString((String)wifi_scan_obj.free_ram + "B", 100, 0, 2); + } + + // Draw battery info + MenuFunctions::battery(false); + + // Draw SD info + if (sd_obj.supported) + the_color = TFT_GREEN; + else + the_color = TFT_RED; + + display_obj.tft.drawXBitmap(170, + 0, + menu_icons[STATUS_SD], + 16, + 16, + STATUSBAR_COLOR, + the_color); + //display_obj.tft.print((String)battery_obj.battery_level + "%"); +} + +void MenuFunctions::drawStatusBar() +{ + display_obj.tft.fillRect(0, 0, 240, STATUS_BAR_WIDTH, STATUSBAR_COLOR); + //display_obj.tft.fillRect(0, STATUS_BAR_WIDTH + 1, 240, 1, TFT_DARKGREY); + display_obj.tft.setTextColor(TFT_WHITE, STATUSBAR_COLOR); + //display_obj.tft.setTextSize(2); + + uint16_t the_color; + + // Draw temp info + if (temp_obj.current_temp < 70) + the_color = TFT_GREEN; + else if ((temp_obj.current_temp >= 70) && (temp_obj.current_temp < 80)) + the_color = TFT_YELLOW; + else if ((temp_obj.current_temp >= 80) && (temp_obj.current_temp < 90)) + the_color = TFT_ORANGE; + else if ((temp_obj.current_temp >= 90) && (temp_obj.current_temp < 100)) + the_color = TFT_RED; + else + the_color = TFT_MAROON; + + display_obj.tft.setTextColor(the_color, STATUSBAR_COLOR); + temp_obj.old_temp = temp_obj.current_temp; + display_obj.tft.fillRect(0, 0, 50, STATUS_BAR_WIDTH, STATUSBAR_COLOR); + display_obj.tft.drawString((String)temp_obj.current_temp + " C", 4, 0, 2); + display_obj.tft.setTextColor(TFT_WHITE, STATUSBAR_COLOR); + + + // WiFi Channel Stuff + wifi_scan_obj.old_channel = wifi_scan_obj.set_channel; + display_obj.tft.fillRect(50, 0, 50, STATUS_BAR_WIDTH, STATUSBAR_COLOR); + display_obj.tft.drawString("CH: " + (String)wifi_scan_obj.set_channel, 50, 0, 2); + + // RAM Stuff + wifi_scan_obj.freeRAM(); + wifi_scan_obj.old_free_ram = wifi_scan_obj.free_ram; + display_obj.tft.fillRect(100, 0, 60, STATUS_BAR_WIDTH, STATUSBAR_COLOR); + display_obj.tft.drawString((String)wifi_scan_obj.free_ram + "B", 100, 0, 2); + + + MenuFunctions::battery2(true); + + // Draw SD info + if (sd_obj.supported) + the_color = TFT_GREEN; + else + the_color = TFT_RED; + + display_obj.tft.drawXBitmap(170, + 0, + menu_icons[STATUS_SD], + 16, + 16, + STATUSBAR_COLOR, + the_color); + //display_obj.tft.print((String)battery_obj.battery_level + "%"); +} + +void MenuFunctions::orientDisplay() +{ + display_obj.tft.init(); + + display_obj.tft.setRotation(0); // Portrait + + display_obj.tft.setCursor(0, 0); + + //uint16_t calData[5] = { 275, 3494, 361, 3528, 4 }; // tft.setRotation(0); // Portrait + //uint16_t calData[5] = { 339, 3470, 237, 3438, 2 }; // tft.setRotation(0); // Portrait with DIY TFT + +#ifdef TFT_SHIELD + uint16_t calData[5] = { 275, 3494, 361, 3528, 4 }; // tft.setRotation(0); // Portrait with TFT Shield + Serial.println("Using TFT Shield"); +#else if defined(TFT_DIY) + uint16_t calData[5] = { 339, 3470, 237, 3438, 2 }; // tft.setRotation(0); // Portrait with DIY TFT + Serial.println("Using TFT DIY"); +#endif + + display_obj.tft.setTouch(calData); + + //display_obj.clearScreen(); + + changeMenu(current_menu); +} + + +// Function to build the menus +void MenuFunctions::RunSetup() +{ + // root menu stuff + mainMenu.list = new LinkedList(); // Get list in first menu ready + + // Main menu stuff + wifiMenu.list = new LinkedList(); // Get list in second menu ready + bluetoothMenu.list = new LinkedList(); // Get list in third menu ready + generalMenu.list = new LinkedList(); + deviceMenu.list = new LinkedList(); + + // Device menu stuff + failedUpdateMenu.list = new LinkedList(); + whichUpdateMenu.list = new LinkedList(); + confirmMenu.list = new LinkedList(); + updateMenu.list = new LinkedList(); + infoMenu.list = new LinkedList(); + + // WiFi menu stuff + wifiSnifferMenu.list = new LinkedList(); + wifiScannerMenu.list = new LinkedList(); + wifiAttackMenu.list = new LinkedList(); + + // Bluetooth menu stuff + bluetoothSnifferMenu.list = new LinkedList(); + bluetoothScannerMenu.list = new LinkedList(); + + // Work menu names + mainMenu.name = " ESP32 Marauder "; + wifiMenu.name = " WiFi "; + deviceMenu.name = " Device "; + generalMenu.name = " General Apps "; + failedUpdateMenu.name = " Updating... "; + whichUpdateMenu.name = "Select Method "; + confirmMenu.name = " Confirm Update "; + updateMenu.name = " Update Firmware "; + infoMenu.name = " Device Info "; + bluetoothMenu.name = " Bluetooth "; + wifiSnifferMenu.name = " WiFi Sniffers "; + wifiScannerMenu.name = " WiFi Scanners"; + wifiAttackMenu.name = " WiFi Attacks "; + bluetoothSnifferMenu.name = " Bluetooth Sniffers "; + bluetoothScannerMenu.name = " Bluetooth Scanners "; + + // Build Main Menu + mainMenu.parentMenu = NULL; + addNodes(&mainMenu, "WiFi", TFT_GREEN, NULL, WIFI, [this]() { + changeMenu(&wifiMenu); + }); + addNodes(&mainMenu, "Bluetooth", TFT_CYAN, NULL, BLUETOOTH, [this]() { + changeMenu(&bluetoothMenu); + }); + addNodes(&mainMenu, "General Apps", TFT_MAGENTA, NULL, GENERAL_APPS, [this]() { + changeMenu(&generalMenu); + }); + addNodes(&mainMenu, "Device", TFT_BLUE, NULL, DEVICE, [this]() { + changeMenu(&deviceMenu); + }); + addNodes(&mainMenu, "Reboot", TFT_LIGHTGREY, NULL, REBOOT, []() { + ESP.restart(); + }); + + // Build WiFi Menu + wifiMenu.parentMenu = &mainMenu; // Main Menu is second menu parent + addNodes(&wifiMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this]() { + changeMenu(wifiMenu.parentMenu); + }); + addNodes(&wifiMenu, "Sniffers", TFT_YELLOW, NULL, SNIFFERS, [this]() { + changeMenu(&wifiSnifferMenu); + }); + addNodes(&wifiMenu, "Scanners", TFT_ORANGE, NULL, SCANNERS, [this]() { + changeMenu(&wifiScannerMenu); + }); + addNodes(&wifiMenu, "Attacks", TFT_RED, NULL, ATTACKS, [this]() { + changeMenu(&wifiAttackMenu); + }); + + // Build WiFi sniffer Menu + wifiSnifferMenu.parentMenu = &wifiMenu; // Main Menu is second menu parent + addNodes(&wifiSnifferMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this]() { + changeMenu(wifiSnifferMenu.parentMenu); + }); + addNodes(&wifiSnifferMenu, "Probe Request Sniff", TFT_CYAN, NULL, PROBE_SNIFF, [this]() { + display_obj.clearScreen(); + this->drawStatusBar(); + wifi_scan_obj.StartScan(WIFI_SCAN_PROBE, TFT_CYAN); + }); + addNodes(&wifiSnifferMenu, "Beacon Sniff", TFT_MAGENTA, NULL, BEACON_SNIFF, [this]() { + display_obj.clearScreen(); + this->drawStatusBar(); + wifi_scan_obj.StartScan(WIFI_SCAN_AP, TFT_MAGENTA); + }); + addNodes(&wifiSnifferMenu, "Deauth Sniff", TFT_RED, NULL, DEAUTH_SNIFF, [this]() { + display_obj.clearScreen(); + this->drawStatusBar(); + wifi_scan_obj.StartScan(WIFI_SCAN_DEAUTH, TFT_RED); + }); + + // Build WiFi scanner Menu + wifiScannerMenu.parentMenu = &wifiMenu; // Main Menu is second menu parent + addNodes(&wifiScannerMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this]() { + changeMenu(wifiScannerMenu.parentMenu); + }); + addNodes(&wifiScannerMenu, "Packet Monitor", TFT_BLUE, NULL, PACKET_MONITOR, [this]() { + wifi_scan_obj.StartScan(WIFI_PACKET_MONITOR, TFT_BLUE); + }); + addNodes(&wifiScannerMenu, "EAPOL/PMKID Scan", TFT_VIOLET, NULL, EAPOL, [this]() { + wifi_scan_obj.StartScan(WIFI_SCAN_EAPOL, TFT_VIOLET); + }); + addNodes(&wifiScannerMenu, "Detect Pwnagotchi", TFT_RED, NULL, PWNAGOTCHI, [this]() { + display_obj.clearScreen(); + this->drawStatusBar(); + wifi_scan_obj.StartScan(WIFI_SCAN_PWN, TFT_RED); + }); + addNodes(&wifiScannerMenu, "Detect Espressif", TFT_ORANGE, NULL, ESPRESSIF, [this]() { + display_obj.clearScreen(); + this->drawStatusBar(); + wifi_scan_obj.StartScan(WIFI_SCAN_ESPRESSIF, TFT_ORANGE); + }); + + // Build WiFi attack menu + wifiAttackMenu.parentMenu = &wifiMenu; // Main Menu is second menu parent + addNodes(&wifiAttackMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this]() { + changeMenu(wifiAttackMenu.parentMenu); + }); + addNodes(&wifiAttackMenu, "Beacon Spam Random", TFT_ORANGE, NULL, BEACON_SPAM, [this]() { + display_obj.clearScreen(); + this->drawStatusBar(); + wifi_scan_obj.StartScan(WIFI_ATTACK_BEACON_SPAM, TFT_ORANGE); + }); + addNodes(&wifiAttackMenu, "Rick Roll Beacon", TFT_YELLOW, NULL, RICK_ROLL, [this]() { + display_obj.clearScreen(); + this->drawStatusBar(); + wifi_scan_obj.StartScan(WIFI_ATTACK_RICK_ROLL, TFT_YELLOW); + }); + + // Build Bluetooth Menu + bluetoothMenu.parentMenu = &mainMenu; // Second Menu is third menu parent + addNodes(&bluetoothMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this]() { + changeMenu(bluetoothMenu.parentMenu); + }); + addNodes(&bluetoothMenu, "Sniffers", TFT_YELLOW, NULL, SNIFFERS, [this]() { + changeMenu(&bluetoothSnifferMenu); + }); + addNodes(&bluetoothMenu, "Scanners", TFT_ORANGE, NULL, SCANNERS, [this]() { + changeMenu(&bluetoothScannerMenu); + }); + + // Build bluetooth sniffer Menu + bluetoothSnifferMenu.parentMenu = &bluetoothMenu; // Second Menu is third menu parent + addNodes(&bluetoothSnifferMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this]() { + changeMenu(bluetoothSnifferMenu.parentMenu); + }); + addNodes(&bluetoothSnifferMenu, "Bluetooth Sniffer", TFT_GREEN, NULL, BLUETOOTH_SNIFF, [this]() { + display_obj.clearScreen(); + this->drawStatusBar(); + wifi_scan_obj.StartScan(BT_SCAN_ALL, TFT_GREEN); + }); + + // Build bluetooth scanner Menu + bluetoothScannerMenu.parentMenu = &bluetoothMenu; // Second Menu is third menu parent + addNodes(&bluetoothScannerMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this]() { + changeMenu(bluetoothScannerMenu.parentMenu); + }); + addNodes(&bluetoothScannerMenu, "Detect Card Skimmers", TFT_MAGENTA, NULL, CC_SKIMMERS, [this]() { + display_obj.clearScreen(); + this->drawStatusBar(); + wifi_scan_obj.StartScan(BT_SCAN_SKIMMERS, TFT_MAGENTA); + }); + + // General apps menu + generalMenu.parentMenu = &mainMenu; + addNodes(&generalMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this]() { + display_obj.draw_tft = false; + changeMenu(generalMenu.parentMenu); + }); + addNodes(&generalMenu, "Draw", TFT_WHITE, NULL, DRAW, [this]() { + display_obj.clearScreen(); + display_obj.draw_tft = true; + }); + + // Device menu + deviceMenu.parentMenu = &mainMenu; + addNodes(&deviceMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this]() { + changeMenu(deviceMenu.parentMenu); + }); + //addNodes(&deviceMenu, "Update Firmware", TFT_ORANGE, NULL, UPDATE, [this](){wifi_scan_obj.currentScanMode = OTA_UPDATE; changeMenu(&updateMenu); web_obj.setupOTAupdate();}); + addNodes(&deviceMenu, "Update Firmware", TFT_ORANGE, NULL, UPDATE, [this]() { + wifi_scan_obj.currentScanMode = OTA_UPDATE; + changeMenu(&whichUpdateMenu); + }); + addNodes(&deviceMenu, "Device Info", TFT_WHITE, NULL, DEVICE_INFO, [this]() { + wifi_scan_obj.currentScanMode = SHOW_INFO; + changeMenu(&infoMenu); + wifi_scan_obj.RunInfo(); + }); + //addNodes(&deviceMenu, "Join WiFi", TFT_YELLOW, NULL, SNIFFERS, [this](){display_obj.clearScreen(); wifi_scan_obj.currentScanMode = LV_JOIN_WIFI; wifi_scan_obj.StartScan(LV_JOIN_WIFI, TFT_YELLOW);}); + + // Select update + whichUpdateMenu.parentMenu = &deviceMenu; + addNodes(&whichUpdateMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this]() { + changeMenu(whichUpdateMenu.parentMenu); + }); + addNodes(&whichUpdateMenu, "Web Update", TFT_GREEN, NULL, WEB_UPDATE, [this]() { + wifi_scan_obj.currentScanMode = OTA_UPDATE; + changeMenu(&updateMenu); + web_obj.setupOTAupdate(); + }); + if (sd_obj.supported) addNodes(&whichUpdateMenu, "SD Update", TFT_MAGENTA, NULL, SD_UPDATE, [this]() { + wifi_scan_obj.currentScanMode = OTA_UPDATE; + changeMenu(&confirmMenu); + }); + + // Confirm SD update menu + confirmMenu.parentMenu = &whichUpdateMenu; + addNodes(&confirmMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this]() { + changeMenu(confirmMenu.parentMenu); + }); + //addNodes(&confirmMenu, "Yes", TFT_ORANGE, NULL, UPDATE, [this](){wifi_scan_obj.currentScanMode = OTA_UPDATE; changeMenu(&updateMenu); sd_obj.runUpdate();}); + addNodes(&confirmMenu, "Yes", TFT_ORANGE, NULL, UPDATE, [this]() { + wifi_scan_obj.currentScanMode = OTA_UPDATE; + changeMenu(&failedUpdateMenu); + sd_obj.runUpdate(); + }); + + // Web Update + updateMenu.parentMenu = &deviceMenu; + addNodes(&updateMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this]() { + wifi_scan_obj.currentScanMode = WIFI_SCAN_OFF; + changeMenu(updateMenu.parentMenu); + WiFi.softAPdisconnect(true); + web_obj.shutdownServer(); + }); + //addNodes(&updateMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this](){wifi_scan_obj.currentScanMode = WIFI_SCAN_OFF; changeMenu(updateMenu.parentMenu);}); + + // Failed update menu + failedUpdateMenu.parentMenu = &whichUpdateMenu; + addNodes(&failedUpdateMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this]() { + wifi_scan_obj.currentScanMode = WIFI_SCAN_OFF; + changeMenu(failedUpdateMenu.parentMenu); + }); + + // Device info menu + infoMenu.parentMenu = &deviceMenu; + addNodes(&infoMenu, "Back", TFT_LIGHTGREY, NULL, 0, [this]() { + wifi_scan_obj.currentScanMode = WIFI_SCAN_OFF; + changeMenu(infoMenu.parentMenu); + }); + + // Set the current menu to the mainMenu + changeMenu(&mainMenu); + + this->initTime = millis(); +} + +// Function to change menu +void MenuFunctions::changeMenu(Menu * menu) +{ + display_obj.initScrollValues(); + display_obj.setupScrollArea(TOP_FIXED_AREA, BOT_FIXED_AREA); + display_obj.tft.init(); + current_menu = menu; + + buildButtons(menu); + + displayCurrentMenu(); +} + +// Function to show all MenuNodes in a Menu +void MenuFunctions::showMenuList(Menu * menu, int layer) +{ + // Iterate through all of the menu nodes in the menu + for (int i = 0; i < menu->list->size(); i++) + { + // Depending on layer, indent + for (int x = 0; x < layer * 4; x++) + Serial.print(" "); + Serial.print("Node: "); + Serial.println(menu->list->get(i).name); + + // If the current menu node points to another menu, list that menu + //if (menu->list->get(i).childMenu != NULL) + // showMenuList(menu->list->get(i).childMenu, layer+1); + } + Serial.println(); +} + + +// Function to add MenuNodes to a menu +void MenuFunctions::addNodes(Menu * menu, String name, uint16_t color, Menu * child, int place, std::function callable) +{ + TFT_eSPI_Button new_button; + menu->list->add(MenuNode{name, color, place, &new_button, callable}); + //strcpy(menu->list->get(-1).icon, bluetooth_icon); +} + +void MenuFunctions::buildButtons(Menu * menu) +{ + Serial.println("Bulding buttons..."); + if (menu->list != NULL) + { + //for (int i = 0; i < sizeof(key); i++) + // key[i] = NULL; + for (int i = 0; i < menu->list->size(); i++) + { + TFT_eSPI_Button new_button; + char buf[menu->list->get(i).name.length() + 1] = {}; + menu->list->get(i).name.toCharArray(buf, menu->list->get(i).name.length() + 1); + display_obj.key[i].initButton(&display_obj.tft, + KEY_X + 0 * (KEY_W + KEY_SPACING_X), + KEY_Y + i * (KEY_H + KEY_SPACING_Y), // x, y, w, h, outline, fill, text + KEY_W, + KEY_H, + TFT_BLACK, // Outline + TFT_BLACK, // Fill + menu->list->get(i).color, // Text + buf, + KEY_TEXTSIZE); + + display_obj.key[i].setLabelDatum(BUTTON_PADDING - (KEY_W / 2), 2, ML_DATUM); + } + } +} + + +void MenuFunctions::displayCurrentMenu() +{ + Serial.println("Displaying current menu..."); + display_obj.clearScreen(); + display_obj.tft.setTextColor(TFT_LIGHTGREY, TFT_DARKGREY); + this->drawStatusBar(); + //display_obj.tft.fillRect(0,0,240,16, TFT_DARKGREY); + //display_obj.tft.drawCentreString(" ESP32 Marauder ",120,0,2); + //Serial.println("Getting size..."); + //char buf[¤t_menu->parentMenu->name.length() + 1] = {}; + //Serial.println("Got size..."); + //current_menu->parentMenu->name.toCharArray(buf, current_menu->parentMenu->name.length() + 1); + //String current_name = ¤t_menu->parentMenu->name; + //Serial.println("gottem"); + //display_obj.tft.drawCentreString(current_menu->name,120,0,2); + if (current_menu->list != NULL) + { + display_obj.tft.setFreeFont(MENU_FONT); + for (int i = 0; i < current_menu->list->size(); i++) + { + //display_obj.key[i].drawButton2(current_menu->list->get(i).name); + //display_obj.key[i].drawButton(ML_DATUM, BUTTON_PADDING, current_menu->list->get(i).name); + //display_obj.key[i].drawButton(true); + display_obj.key[i].drawButton(false, current_menu->list->get(i).name); + + if (current_menu->list->get(i).name != "Back") + display_obj.tft.drawXBitmap(0, + KEY_Y + i * (KEY_H + KEY_SPACING_Y) - (ICON_H / 2), + menu_icons[current_menu->list->get(i).icon], + ICON_W, + ICON_H, + TFT_BLACK, + current_menu->list->get(i).color); + } + display_obj.tft.setFreeFont(NULL); + } +} diff --git a/esp32_marauder/MenuFunctions.h b/esp32_marauder/MenuFunctions.h index cac1723..756f0df 100644 --- a/esp32_marauder/MenuFunctions.h +++ b/esp32_marauder/MenuFunctions.h @@ -1,12 +1,15 @@ #ifndef MenuFunctions_h #define MenuFunctions_h +#define BATTERY_ANALOG_ON 0 + #include "WiFiScan.h" #include "Display.h" #include "BatteryInterface.h" #include "SDInterface.h" #include "Web.h" + extern Display display_obj; extern WiFiScan wifi_scan_obj; extern Web web_obj; @@ -28,6 +31,12 @@ extern BatteryInterface battery_obj; #define FLASH_BUTTON 0 +#if BATTERY_ANALOG_ON == 1 +#define BATTERY_PIN 13 +#define ANALOG_PIN 34 +#define CHARGING_PIN 27 +#endif + // Icon definitions #define ATTACKS 0 #define BEACON_SNIFF 1 @@ -61,35 +70,35 @@ struct Menu; // Individual Nodes of a menu struct MenuNode { - String name; - uint16_t color; - int icon; - TFT_eSPI_Button* button; - std::function callable; + String name; + uint16_t color; + int icon; + TFT_eSPI_Button* button; + std::function callable; }; // Full Menus struct Menu { - String name; - LinkedList* list; - Menu * parentMenu; - //uint8_t selected; + String name; + LinkedList* list; + Menu * parentMenu; + //uint8_t selected; }; class MenuFunctions { - private: + private: String u_result = ""; uint32_t initTime = 0; - + Menu* current_menu; // Main menu stuff Menu mainMenu; - + Menu wifiMenu; Menu bluetoothMenu; Menu generalMenu; @@ -112,17 +121,19 @@ class MenuFunctions Menu bluetoothScannerMenu; // Menu icons - + //TFT_eSPI_Button key[BUTTON_ARRAY_LEN]; - + void addNodes(Menu* menu, String name, uint16_t color, Menu* child, int place, std::function callable); void drawStatusBar(); void updateStatusBar(); + void battery(bool initial = false); + void battery2(bool initial = false); void showMenuList(Menu* menu, int layer); void orientDisplay(); - public: + public: MenuFunctions(); uint16_t x = -1, y = -1; diff --git a/esp32_marauder/data/marauder3L1.jpg b/esp32_marauder/data/marauder3L1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..eedd5ebcce48188d194383d56f02b17445fd6f8e GIT binary patch literal 24399 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