diff --git a/include/config.h b/include/config.h index 968b65c..24dc3a1 100644 --- a/include/config.h +++ b/include/config.h @@ -65,4 +65,9 @@ // How long to dwell on each hop frequency (ms) #define SWEEP_DWELL_MS 5 +// 0.96" SSD1306 OLED display — I2C via SW_I2C (any free GPIO) +// Wiring: VCC→3V3, GND→GND, SDA→GPIO17, SCL→GPIO18 +#define OLED_SDA_PIN 17 +#define OLED_SCL_PIN 18 + #endif diff --git a/platformio.ini b/platformio.ini index 7ef49a1..ebf5010 100644 --- a/platformio.ini +++ b/platformio.ini @@ -6,7 +6,9 @@ platform = espressif32 board = esp32-s3-devkitc-1 framework = arduino -lib_deps = jgromes/RadioLib +lib_deps = + jgromes/RadioLib + olikraus/U8g2 board_build.arduino.memory_type = qio_opi board_build.flash_mode = qio diff --git a/src/main.cpp b/src/main.cpp index e917639..ad9adc8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include "config.h" // Shared SPI; each Module uses its own CS. @@ -76,6 +78,20 @@ static uint32_t hopCount2 = 0; static uint32_t minFreeHeap = 0xFFFFFFFF; // lowest heap ever observed static uint32_t lastTempWarnMs = 0; // rate-limit temperature warnings +// ─── OLED (0.96" SSD1306 128x64) ───────────────────────────────────────────── +// SW_I2C: any GPIO, bit-banged — tolerant of missing display (oledOk gate) +static U8G2_SSD1306_128X64_NONAME_F_SW_I2C + u8g2(U8G2_R0, OLED_SCL_PIN, OLED_SDA_PIN, U8X8_PIN_NONE); +static bool oledOk = false; +static uint8_t oledPage = 0; // 0=status, 1=freq/hops, 2=health +static uint32_t oledPageMs = 0; +static uint32_t oledTickMs = 0; +static uint8_t waveFrame = 0; // 0-3 animated arc count +static uint32_t waveMs = 0; +static uint32_t notifEnd = 0; // millis() when current notification expires +static char notifL1[22] = {}; +static char notifL2[22] = {}; + // Log ring buffer static constexpr size_t LOG_LINES = 100; static String logRing[LOG_LINES]; @@ -281,6 +297,198 @@ static void stopJamming() { logLine("[JAM] Jamming stopped"); } +// ─── OLED functions ────────────────────────────────────────────────────────── + +// Queue a full-screen notification overlay for dur ms. +static void oledNotify(const char* l1, const char* l2, uint32_t dur = 2500) { + if (!oledOk) return; + strlcpy(notifL1, l1, sizeof(notifL1)); + strlcpy(notifL2, l2, sizeof(notifL2)); + notifEnd = millis() + dur; + oledPageMs = notifEnd; // reset page timer after notification clears +} + +// Show a synchronous one-shot boot status message (called during setup). +static void oledBootMsg(const char* line) { + if (!oledOk) return; + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_7x13_tf); + u8g2.drawStr(0, 14, "CC1101 JAMMER"); + u8g2.setFont(u8g2_font_6x10_tf); + u8g2.drawStr(0, 27, "ESP32-S3 INIT"); + u8g2.drawHLine(0, 30, 128); + u8g2.drawStr(0, 46, line); + u8g2.sendBuffer(); +} + +// Draw animated right-half radio-wave arcs at (cx, cy), n arcs (0-3). +static void oledDrawWaves(uint8_t cx, uint8_t cy, uint8_t n) { + for (uint8_t i = 0; i < n; i++) { + u8g2.drawCircle(cx, cy, (i + 1) * 3, + U8G2_DRAW_UPPER_RIGHT | U8G2_DRAW_LOWER_RIGHT); + } +} + +// Page 0 — Live Status +static void oledDrawStatus() { + const bool jam = jammingEnabled; + const bool r1 = (radio1Status == 2); + const bool r2 = (radio2Status == 2); + + // Header bar (inverted when active) + if (jam) { + u8g2.drawBox(0, 0, 128, 13); + u8g2.setDrawColor(0); + u8g2.setFont(u8g2_font_6x10_tf); + u8g2.drawStr(4, 10, ">> JAMMING ACTIVE <<"); + u8g2.setDrawColor(1); + } else { + u8g2.setFont(u8g2_font_6x10_tf); + u8g2.drawStr(0, 10, "-- STANDBY --"); + } + + const uint8_t nWaves = waveFrame == 0 ? 0 : waveFrame; // 0/1/2/3 arcs + + // ANT1 row (baseline y=23) + u8g2.setFont(u8g2_font_6x10_tf); + if (r1) { + char buf[18]; + snprintf(buf, sizeof(buf), "ANT1 %.3fMHz", (double)sweepFreq1); + u8g2.drawStr(0, 23, buf); + oledDrawWaves((uint8_t)(strlen(buf) * 6 + 3), 17, nWaves); + } else { + u8g2.drawStr(0, 23, "ANT1 [OFFLINE]"); + } + + // ANT2 row (baseline y=35) + if (r2) { + char buf[18]; + snprintf(buf, sizeof(buf), "ANT2 %.3fMHz", (double)sweepFreq2); + u8g2.drawStr(0, 35, buf); + oledDrawWaves((uint8_t)(strlen(buf) * 6 + 3), 29, nWaves); + } else { + u8g2.drawStr(0, 35, "ANT2 [OFFLINE]"); + } + + // Power line (small font, baseline y=46) + u8g2.setFont(u8g2_font_5x7_tf); + { + char pbuf[26]; + snprintf(pbuf, sizeof(pbuf), "TX:%ddBm+%ddB=%ddBm", + (int)jamPower, (int)ampGainDb, (int)jamPower + (int)ampGainDb); + u8g2.drawStr(0, 46, pbuf); + } + + // Bottom line — show FULL TX badge if both active, else temp/heap + if (jam && r1 && r2) { + u8g2.drawStr(0, 57, "[ FULL DUAL-BAND TX ]"); + } else { + char tbuf[26]; + snprintf(tbuf, sizeof(tbuf), "TEMP:%.1fC HEAP:%lukB", + (double)temperatureRead(), (unsigned long)(ESP.getFreeHeap() / 1024)); + u8g2.drawStr(0, 57, tbuf); + } +} + +// Page 1 — Frequency + Hops +static void oledDrawFreq() { + u8g2.setFont(u8g2_font_6x10_tf); + u8g2.drawStr(0, 10, "- FREQ / HOPS -"); + u8g2.drawHLine(0, 12, 128); + + char buf[22]; + snprintf(buf, sizeof(buf), "R1: %.4fMHz", (double)sweepFreq1); + u8g2.drawStr(0, 25, buf); + u8g2.setFont(u8g2_font_5x7_tf); + snprintf(buf, sizeof(buf), " %lu hops", (unsigned long)hopCount1); + u8g2.drawStr(0, 35, buf); + + u8g2.setFont(u8g2_font_6x10_tf); + snprintf(buf, sizeof(buf), "R2: %.4fMHz", (double)sweepFreq2); + u8g2.drawStr(0, 48, buf); + u8g2.setFont(u8g2_font_5x7_tf); + snprintf(buf, sizeof(buf), " %lu hops", (unsigned long)hopCount2); + u8g2.drawStr(0, 58, buf); +} + +// Page 2 — System Health +static void oledDrawHealth() { + u8g2.setFont(u8g2_font_6x10_tf); + u8g2.drawStr(0, 10, "- SYSTEM HEALTH -"); + u8g2.drawHLine(0, 12, 128); + + char buf[22]; + snprintf(buf, sizeof(buf), "TEMP %.1f C", (double)temperatureRead()); + u8g2.drawStr(0, 25, buf); + + snprintf(buf, sizeof(buf), "HEAP %lukB (min %lu)", + (unsigned long)(ESP.getFreeHeap() / 1024), + (unsigned long)((minFreeHeap == 0xFFFFFFFF ? ESP.getFreeHeap() : minFreeHeap) / 1024)); + u8g2.setFont(u8g2_font_5x7_tf); + u8g2.drawStr(0, 36, buf); + + // Uptime + u8g2.setFont(u8g2_font_6x10_tf); + const uint32_t up = millis() - uptimeStart; + const uint32_t ss = (up / 1000) % 60, mm = (up / 60000) % 60, hh = up / 3600000; + snprintf(buf, sizeof(buf), "UP %uh %um %us", (unsigned)hh, (unsigned)mm, (unsigned)ss); + u8g2.drawStr(0, 49, buf); + + // Effective power in mW + const int effDbm = (int)jamPower + (int)ampGainDb; + const uint32_t effMw = (uint32_t)roundf(powf(10.0f, effDbm / 10.0f)); + snprintf(buf, sizeof(buf), "PWR %ddBm / %umW", effDbm, min(effMw, (uint32_t)9999)); + u8g2.drawStr(0, 61, buf); +} + +// Full-screen inverted notification overlay +static void oledDrawNotif() { + u8g2.drawBox(0, 0, 128, 64); + u8g2.setDrawColor(0); + + u8g2.setFont(u8g2_font_7x13_tf); + int16_t x1 = (128 - (int16_t)strlen(notifL1) * 7) / 2; + u8g2.drawStr((uint8_t)max((int16_t)0, x1), 26, notifL1); + + u8g2.setFont(u8g2_font_6x10_tf); + int16_t x2 = (128 - (int16_t)strlen(notifL2) * 6) / 2; + u8g2.drawStr((uint8_t)max((int16_t)0, x2), 44, notifL2); + + u8g2.setDrawColor(1); +} + +// Main OLED update — call from loop() every pass; self-throttles to 100ms. +static void oledTick() { + if (!oledOk) return; + const uint32_t now = millis(); + if (now - oledTickMs < 100) return; + oledTickMs = now; + + // Advance wave animation every 220ms (4 frames → ~1.1s full cycle) + if (now - waveMs >= 220) { + waveMs = now; + waveFrame = (waveFrame + 1) & 3; + } + + // Auto page-advance every 4s (not during notification) + if (now > notifEnd && now - oledPageMs >= 4000) { + oledPageMs = now; + oledPage = (oledPage + 1) % 3; + } + + u8g2.clearBuffer(); + if (now < notifEnd) { + oledDrawNotif(); + } else if (oledPage == 0) { + oledDrawStatus(); + } else if (oledPage == 1) { + oledDrawFreq(); + } else { + oledDrawHealth(); + } + u8g2.sendBuffer(); +} + // Drive each CC1101's GDO0 pin with a 120 kHz square wave (LEDC channels 0 & 1). // In transmitDirectAsync mode the CC1101 reads GDO0 as serial data, // producing an FM signal that spans ±120 kHz (240 kHz bandwidth) per hop @@ -763,11 +971,15 @@ static void handleTelemetry() { static void handleToggle() { if (jammingEnabled) { stopJamming(); + oledNotify("STANDBY", "Jamming stopped"); } else { jammingEnabled = true; // must be set before startJamming so sweep loop and watchdog see it startJamming(); if (radio1Status != 2 && radio2Status != 2) { jammingEnabled = false; // both radios failed — don't pretend we're jamming + oledNotify("RADIO FAIL", "Check connections"); + } else { + oledNotify("JAMMING", "STARTED"); } } @@ -794,6 +1006,12 @@ static void handleSettings() { valStr.trim(); uint8_t idx = (uint8_t)constrain(valStr.toInt(), 0, JAM_POWER_LEVELS - 1); updateJamPower(idx); + { + char l2[22]; + snprintf(l2, sizeof(l2), "%d dBm (eff %d dBm)", + (int)kPowerTable[idx], (int)kPowerTable[idx] + (int)ampGainDb); + oledNotify("POWER SET", l2); + } } } else { logLine("[HTTP] No power_idx in JSON body"); @@ -888,7 +1106,20 @@ static void handleNotFound() { void setup() { // Shorter delay for Serial to initialize on ESP32-S3 in production Serial.begin(115200); - + + // OLED init (before anything else so boot messages are visible) + Wire.begin(OLED_SDA_PIN, OLED_SCL_PIN); + oledOk = u8g2.begin(); + if (oledOk) { + u8g2.clearBuffer(); + u8g2.setFont(u8g2_font_7x13_tf); + u8g2.drawStr(18, 22, "CC1101"); + u8g2.drawStr(12, 38, "JAMMER"); + u8g2.setFont(u8g2_font_5x7_tf); + u8g2.drawStr(14, 54, "ESP32-S3 BOOTING..."); + u8g2.sendBuffer(); + } + // Wait for Serial to be ready (timeout after 500ms for production) unsigned long start = millis(); while (!Serial && (millis() - start) < 500) { @@ -924,10 +1155,12 @@ void setup() { logLine("[BOOT] ESP32-S3 DevKitC-1"); Serial.flush(); + oledBootMsg("SPI init..."); + // Initialize SPI (required for CC1101 communication) Serial.println("[SPI] Initializing SPI bus..."); Serial.flush(); - + // Drive CS pins HIGH before SPI init to prevent bus collisions pinMode(CC1101_1_CS, OUTPUT); digitalWrite(CC1101_1_CS, HIGH); @@ -944,6 +1177,8 @@ void setup() { " speed=" + String(SPI_SPEED_HZ)); delay(150); // Allow CC1101 VCC to stabilize + oledBootMsg("WiFi AP start..."); + // Start WiFi AP Serial.println("[DEBUG] Starting WiFi AP..."); Serial.flush(); @@ -1016,14 +1251,23 @@ void setup() { // Start jamming immediately if enabled if (jammingEnabled) { + oledBootMsg("Radio 1 init..."); + // (radio 2 init happens inside startJamming immediately after radio 1) startJamming(); + if (radio1Status == 2 || radio2Status == 2) { + oledBootMsg("JAMMING - ACTIVE!"); + } else { + oledBootMsg("RADIO INIT FAILED"); + } } else { radio1Status = -1; radio2Status = -1; radio1Error = "Disabled / not initialized"; radio2Error = "Disabled / not initialized"; logLine("[JAM] Jamming disabled on boot"); + oledBootMsg("Standby. Press START."); } + delay(800); // hold boot result on display briefly before switching to live pages } // Advance one radio to the next sweep frequency. @@ -1050,6 +1294,7 @@ static void tickSweep(CC1101& radio, uint8_t& step, uint8_t steps, void loop() { ArduinoOTA.handle(); server.handleClient(); + oledTick(); yield(); const uint32_t now = millis(); @@ -1060,6 +1305,7 @@ void loop() { bool needReinit = (radio1Status != 2 || radio2Status != 2); if (needReinit) { logLine("[WDT] Radio failure detected, attempting reinit..."); + oledNotify("RADIO REINIT", "R1 + R2..."); startJamming(); } }