Add 0.96" SSD1306 OLED display — boot sequence, live status, 3 cycling pages
Hardware: GPIO17=SDA, GPIO18=SCL, 3V3, GND — I2C address 0x3C (SW_I2C)
Library: olikraus/U8g2 (SW_I2C full-buffer mode, tolerant of missing display)
Boot sequence (shown synchronously during setup):
BOOTING splash → SPI init → WiFi AP start → Radio 1 init → JAMMING ACTIVE
or RADIO INIT FAILED / STANDBY on error
Live display cycles every 4 seconds between 3 pages:
Page 0 — Status:
Inverted header: ">> JAMMING ACTIVE <<" (animated pulsing glow banner) or STANDBY
ANT1 309.583MHz ))) ← animated radio-wave arcs (1-3 arcs cycling ~1.1s)
ANT2 433.920MHz )))
TX:10dBm+20dB=30dBm
"[ FULL DUAL-BAND TX ]" when both radios active, else TEMP+HEAP
Page 1 — Frequency/Hops:
R1: 309.5830MHz
12,456 hops
R2: 433.9200MHz
11,234 hops
Page 2 — System Health:
TEMP 48.2 C
HEAP 185kB (min 183)
UP 2h 34m 12s
PWR 30dBm / 1000mW
Notification overlays (full-screen inverted, 2.5s):
POWER SET / 10 dBm (eff 30 dBm) ← on any power change from UI
JAMMING / STARTED ← on toggle on
STANDBY / Jamming stopped ← on toggle off
RADIO REINIT / R1 + R2... ← on watchdog reinit
RADIO FAIL / Check connections ← if both radios fail to start
Made-with: Cursor
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
250
src/main.cpp
250
src/main.cpp
@@ -15,6 +15,8 @@
|
||||
#include <ArduinoOTA.h>
|
||||
#include <Preferences.h>
|
||||
#include <math.h>
|
||||
#include <Wire.h>
|
||||
#include <U8g2lib.h>
|
||||
#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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user