Add OLED display, rotary encoder, max-coverage sweep, updated README
- OLED SSD1306 0.96in via SW_I2C (GPIO17=SDA, GPIO18=SCL): boot messages, 3-page cycling display (status/freq+hops/health), animated wave arcs, full-screen notifications on state changes, page dot indicators - Rotary encoder (GPIO14=CLK, GPIO21=DT) with IRAM ISR: manual page navigation, resets 8s auto-advance timer on interaction - Sweep reworked for zero-gap coverage: deviation 120->380 kHz (CC1101 max), dwell 5->3ms, Radio2 steps 47->60; ~1 MHz noise per hop, R1 cycle 75ms, R2 cycle 180ms, all target fob frequencies hit multiple times per press - HW_I2C->SW_I2C revert after confirming SW_I2C more reliable on ESP32-S3 with custom pins; Wire.begin probing both 0x3C and 0x3D addresses - README fully rewritten: all pins, parameters, features, architecture, troubleshooting, no emoji or unicode box characters Made-with: Cursor
This commit is contained in:
232
src/main.cpp
232
src/main.cpp
@@ -79,7 +79,8 @@ 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)
|
||||
// SW_I2C: bit-bangs GPIO directly — no Wire library involved, always works
|
||||
// if the pins are physically correct. SDA=GPIO17, SCL=GPIO18.
|
||||
static U8G2_SSD1306_128X64_NONAME_F_SW_I2C
|
||||
u8g2(U8G2_R0, OLED_SCL_PIN, OLED_SDA_PIN, U8X8_PIN_NONE);
|
||||
static bool oledOk = false;
|
||||
@@ -92,6 +93,19 @@ static uint32_t notifEnd = 0; // millis() when current notification expir
|
||||
static char notifL1[22] = {};
|
||||
static char notifL2[22] = {};
|
||||
|
||||
// ─── Rotary encoder ──────────────────────────────────────────────────────────
|
||||
static volatile int8_t encDelta = 0; // +1 CW / -1 CCW per detent
|
||||
static uint8_t encLastClk = HIGH;
|
||||
|
||||
void IRAM_ATTR encISR() {
|
||||
const uint8_t clk = digitalRead(ENC_CLK_PIN);
|
||||
if (clk == encLastClk) return; // filter glitch
|
||||
encLastClk = clk;
|
||||
if (clk == LOW) { // falling edge = one detent
|
||||
encDelta += (digitalRead(ENC_DT_PIN) == HIGH) ? +1 : -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Log ring buffer
|
||||
static constexpr size_t LOG_LINES = 100;
|
||||
static String logRing[LOG_LINES];
|
||||
@@ -329,116 +343,147 @@ static void oledDrawWaves(uint8_t cx, uint8_t cy, uint8_t n) {
|
||||
}
|
||||
}
|
||||
|
||||
// Draw page indicator dots in the yellow zone (top-right corner)
|
||||
// page = current page (0-2)
|
||||
static void oledPageDots(uint8_t page) {
|
||||
for (uint8_t i = 0; i < 3; i++) {
|
||||
const uint8_t x = 116 + i * 5;
|
||||
if (i == page) u8g2.drawBox(x, 4, 3, 3); // filled = active
|
||||
else u8g2.drawFrame(x, 4, 3, 3); // outline = inactive
|
||||
}
|
||||
}
|
||||
|
||||
// Page 0 — Live Status
|
||||
// Yellow zone (y 0-15): status header
|
||||
// Blue zone (y16-63): 4 data lines with 5x7 font
|
||||
static void oledDrawStatus() {
|
||||
const bool jam = jammingEnabled;
|
||||
const bool r1 = (radio1Status == 2);
|
||||
const bool r2 = (radio2Status == 2);
|
||||
|
||||
// Header bar (inverted when active)
|
||||
// Yellow zone header — inverted box when jamming
|
||||
u8g2.setFont(u8g2_font_6x10_tf);
|
||||
if (jam) {
|
||||
u8g2.drawBox(0, 0, 128, 13);
|
||||
u8g2.drawBox(0, 0, 110, 13);
|
||||
u8g2.setDrawColor(0);
|
||||
u8g2.setFont(u8g2_font_6x10_tf);
|
||||
u8g2.drawStr(4, 10, ">> JAMMING ACTIVE <<");
|
||||
u8g2.drawStr(2, 10, ">> JAMMING ACTIVE <<");
|
||||
u8g2.setDrawColor(1);
|
||||
} else {
|
||||
u8g2.setFont(u8g2_font_6x10_tf);
|
||||
u8g2.drawStr(0, 10, "-- STANDBY --");
|
||||
u8g2.drawStr(2, 10, "-- STANDBY --");
|
||||
}
|
||||
oledPageDots(0);
|
||||
|
||||
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)
|
||||
// Blue zone — 5x7 font, 4 rows at y=22,32,43,54 then 5th at y=63
|
||||
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);
|
||||
const uint8_t nW = waveFrame;
|
||||
|
||||
// Row 1: ANT1
|
||||
if (r1) {
|
||||
char buf[20];
|
||||
snprintf(buf, sizeof(buf), "1: %.3f MHz", (double)sweepFreq1);
|
||||
u8g2.drawStr(0, 24, buf);
|
||||
oledDrawWaves(101, 19, nW);
|
||||
} else {
|
||||
u8g2.drawStr(0, 24, "1: [OFFLINE]");
|
||||
}
|
||||
|
||||
// Bottom line — show FULL TX badge if both active, else temp/heap
|
||||
if (jam && r1 && r2) {
|
||||
u8g2.drawStr(0, 57, "[ FULL DUAL-BAND TX ]");
|
||||
// Row 2: ANT2
|
||||
if (r2) {
|
||||
char buf[20];
|
||||
snprintf(buf, sizeof(buf), "2: %.3f MHz", (double)sweepFreq2);
|
||||
u8g2.drawStr(0, 33, buf);
|
||||
oledDrawWaves(101, 28, nW);
|
||||
} else {
|
||||
char tbuf[26];
|
||||
snprintf(tbuf, sizeof(tbuf), "TEMP:%.1fC HEAP:%lukB",
|
||||
u8g2.drawStr(0, 33, "2: [OFFLINE]");
|
||||
}
|
||||
|
||||
// Row 3: power
|
||||
{
|
||||
char buf[28];
|
||||
snprintf(buf, sizeof(buf), "TX %d+%d=%ddBm",
|
||||
(int)jamPower, (int)ampGainDb, (int)jamPower + (int)ampGainDb);
|
||||
u8g2.drawStr(0, 44, buf);
|
||||
}
|
||||
|
||||
// Row 4: temp + heap OR FULL TX badge
|
||||
if (jam && r1 && r2) {
|
||||
u8g2.drawStr(0, 55, "[ FULL DUAL-BAND TX ]");
|
||||
} else {
|
||||
char buf[28];
|
||||
snprintf(buf, sizeof(buf), "%.1fC %lukB",
|
||||
(double)temperatureRead(), (unsigned long)(ESP.getFreeHeap() / 1024));
|
||||
u8g2.drawStr(0, 57, tbuf);
|
||||
u8g2.drawStr(0, 55, buf);
|
||||
}
|
||||
|
||||
// Row 5: uptime small
|
||||
{
|
||||
const uint32_t up = millis() - uptimeStart;
|
||||
char buf[20];
|
||||
snprintf(buf, sizeof(buf), "up %uh%um%us",
|
||||
(unsigned)(up/3600000), (unsigned)((up/60000)%60), (unsigned)((up/1000)%60));
|
||||
u8g2.drawStr(0, 63, buf);
|
||||
}
|
||||
}
|
||||
|
||||
// Page 1 — Frequency + Hops
|
||||
static void oledDrawFreq() {
|
||||
// Yellow zone header
|
||||
u8g2.setFont(u8g2_font_6x10_tf);
|
||||
u8g2.drawStr(0, 10, "- FREQ / HOPS -");
|
||||
u8g2.drawHLine(0, 12, 128);
|
||||
u8g2.drawStr(2, 10, "FREQ & HOPS");
|
||||
oledPageDots(1);
|
||||
|
||||
char buf[22];
|
||||
snprintf(buf, sizeof(buf), "R1: %.4fMHz", (double)sweepFreq1);
|
||||
u8g2.drawStr(0, 25, buf);
|
||||
u8g2.setFont(u8g2_font_5x7_tf);
|
||||
char buf[24];
|
||||
|
||||
snprintf(buf, sizeof(buf), "R1 %.4f MHz", (double)sweepFreq1);
|
||||
u8g2.drawStr(0, 24, buf);
|
||||
snprintf(buf, sizeof(buf), " %lu hops", (unsigned long)hopCount1);
|
||||
u8g2.drawStr(0, 35, buf);
|
||||
u8g2.drawStr(0, 33, 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), "R2 %.4f MHz", (double)sweepFreq2);
|
||||
u8g2.drawStr(0, 45, buf);
|
||||
snprintf(buf, sizeof(buf), " %lu hops", (unsigned long)hopCount2);
|
||||
u8g2.drawStr(0, 58, buf);
|
||||
u8g2.drawStr(0, 54, buf);
|
||||
|
||||
// Total hops per second (approx from 5s heartbeat window)
|
||||
const uint32_t up = (millis() - uptimeStart) / 1000;
|
||||
if (up > 0) {
|
||||
snprintf(buf, sizeof(buf), "~%lu h/s total",
|
||||
(unsigned long)((hopCount1 + hopCount2) / up));
|
||||
u8g2.drawStr(0, 63, buf);
|
||||
}
|
||||
}
|
||||
|
||||
// Page 2 — System Health
|
||||
static void oledDrawHealth() {
|
||||
// Yellow zone header
|
||||
u8g2.setFont(u8g2_font_6x10_tf);
|
||||
u8g2.drawStr(0, 10, "- SYSTEM HEALTH -");
|
||||
u8g2.drawHLine(0, 12, 128);
|
||||
u8g2.drawStr(2, 10, "SYS HEALTH");
|
||||
oledPageDots(2);
|
||||
|
||||
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);
|
||||
char buf[24];
|
||||
|
||||
snprintf(buf, sizeof(buf), "TEMP %.1f C", (double)temperatureRead());
|
||||
u8g2.drawStr(0, 24, buf);
|
||||
|
||||
const uint32_t freeK = ESP.getFreeHeap() / 1024;
|
||||
const uint32_t minK = (minFreeHeap == 0xFFFFFFFF ? ESP.getFreeHeap() : minFreeHeap) / 1024;
|
||||
snprintf(buf, sizeof(buf), "HEAP %lukB min%lukB", freeK, minK);
|
||||
u8g2.drawStr(0, 33, 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);
|
||||
snprintf(buf, sizeof(buf), "UP %uh %um %us",
|
||||
(unsigned)(up/3600000), (unsigned)((up/60000)%60), (unsigned)((up/1000)%60));
|
||||
u8g2.drawStr(0, 44, 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);
|
||||
u8g2.drawStr(0, 55, buf);
|
||||
|
||||
snprintf(buf, sizeof(buf), "WIFI %d client(s)", WiFi.softAPgetStationNum());
|
||||
u8g2.drawStr(0, 63, buf);
|
||||
}
|
||||
|
||||
// Full-screen inverted notification overlay
|
||||
@@ -470,8 +515,16 @@ static void oledTick() {
|
||||
waveFrame = (waveFrame + 1) & 3;
|
||||
}
|
||||
|
||||
// Auto page-advance every 4s (not during notification)
|
||||
if (now > notifEnd && now - oledPageMs >= 4000) {
|
||||
// Consume encoder — manual page change resets the auto-cycle timer
|
||||
if (encDelta != 0) {
|
||||
const int8_t d = encDelta;
|
||||
encDelta = 0;
|
||||
oledPage = (uint8_t)((oledPage + 3 + (d > 0 ? 1 : -1)) % 3);
|
||||
oledPageMs = now; // reset auto-advance so page stays visible
|
||||
}
|
||||
|
||||
// Auto page-advance every 8s (not during notification, not if encoder just moved)
|
||||
if (now > notifEnd && now - oledPageMs >= 8000) {
|
||||
oledPageMs = now;
|
||||
oledPage = (oledPage + 1) % 3;
|
||||
}
|
||||
@@ -1107,18 +1160,20 @@ 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();
|
||||
}
|
||||
// OLED init — SW_I2C bit-bangs GPIO17/18 directly; no Wire needed.
|
||||
// begin() always returns true for SW_I2C so just call it and force oledOk.
|
||||
u8g2.begin();
|
||||
u8g2.setContrast(255); // max brightness — some panels boot dim
|
||||
oledOk = true;
|
||||
Serial.println("[OLED] SW_I2C init done (GPIO17=SDA GPIO18=SCL)");
|
||||
|
||||
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();
|
||||
@@ -1154,7 +1209,14 @@ void setup() {
|
||||
logLine("[BOOT] CC1101 Key-Fob Jammer starting");
|
||||
logLine("[BOOT] ESP32-S3 DevKitC-1");
|
||||
Serial.flush();
|
||||
|
||||
|
||||
// Rotary encoder — interrupt on CLK falling edge
|
||||
pinMode(ENC_CLK_PIN, INPUT_PULLUP);
|
||||
pinMode(ENC_DT_PIN, INPUT_PULLUP);
|
||||
encLastClk = digitalRead(ENC_CLK_PIN);
|
||||
attachInterrupt(digitalPinToInterrupt(ENC_CLK_PIN), encISR, CHANGE);
|
||||
logLine("[ENC] Rotary encoder ready GPIO14=CLK GPIO21=DT");
|
||||
|
||||
oledBootMsg("SPI init...");
|
||||
|
||||
// Initialize SPI (required for CC1101 communication)
|
||||
|
||||
Reference in New Issue
Block a user